跳转到内容

User:Addonian1123/QuickEditSandbox-core.js

维基百科,自由的百科全书
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
/* var _addText="{{User:Addonian1123/qes}}" */
(function () {
	'use strict';

	const isMultiEdit = localStorage.getItem('qe_multi_edit') === 'true';
	const isEditNoticeEnabled = localStorage.getItem('qe_edit_notice') !== 'false';
	const isConfirmCancel = localStorage.getItem('qe_confirm_cancel') === 'true';
	const isWarnUnsaved = localStorage.getItem('qe_warn_unsaved') === 'true';
	const isDebugEnabled = localStorage.getItem('qeDebug') === 'true' || localStorage.getItem('qeDebug') === '1';

	const titleRegexp = new RegExp(
		mw.config
			.get('wgArticlePath')
			.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
			.replace(/\\\$1/, '([^?]+)') + '|[?&]title=([^&#]*)'
	);

	const summaryPresetsCache = {};
	let apiSingleton;
	let activeCleanupFn = null;

	function getApi() {
		if (!apiSingleton) {
			apiSingleton = new mw.Api();
		}
		return apiSingleton;
	}

	function api(func, params, silentError) {
		const client = getApi();
		$.extend(params, {
			errorformat: 'html',
			errorlang: mw.config.get('wgUserLanguage'),
			errorsuselocal: true
		});
		return client[func](params).fail(function (_, data) {
			if (!silentError) {
				mw.notify(client.getErrorMessage(data), {
					type: 'error',
					tag: 'qe'
				});
			}
		});
	}

	function qeLog() {
		if (!isDebugEnabled) return;
		try {
			console.log.apply(console, ['[QuickEditSandbox]'].concat([].slice.call(arguments)));
		} catch (_e) {}
	}

	function tryThreeWayMerge(baseText, theirText, myText) {
		if (baseText === theirText) return { success: true, text: myText };
		if (baseText === myText) return { success: true, text: theirText };
		if (theirText === myText) return { success: true, text: myText };

		const baseLines = baseText.split('\n');
		const theirLines = theirText.split('\n');
		const myLines = myText.split('\n');

		if (theirLines.length >= baseLines.length && myLines.length >= baseLines.length) {
			const theirPrefixMatch = baseLines.every((l, idx) => l === theirLines[idx]);
			const myPrefixMatch = baseLines.every((l, idx) => l === myLines[idx]);
			if (theirPrefixMatch && myPrefixMatch) {
				const merged = baseLines.concat(theirLines.slice(baseLines.length)).concat(myLines.slice(baseLines.length));
				return { success: true, text: merged.join('\n') };
			}
		}

		return { success: false };
	}

	function getPageInfo(title, sectionID, customRevId) {
		qeLog('getPageInfo:request', { title, sectionID, customRevId });

		const viewedRevId = customRevId || (
			(title === mw.config.get('wgPageName') && mw.config.get('wgRevisionId') !== mw.config.get('wgCurRevisionId') && mw.config.get('wgRevisionId') > 0)
				? mw.config.get('wgRevisionId')
				: null
		);

		const params = {
			action: 'query',
			curtimestamp: 1,
			prop: 'revisions|info',
			indexpageids: 1,
			rvprop: ['timestamp', 'content', 'ids'],
			rvslots: 'main'
		};

		if (viewedRevId) {
			params.revids = viewedRevId;
		} else {
			params.titles = title;
		}

		if (sectionID !== null && sectionID !== undefined && sectionID !== '') {
			params.rvsection = sectionID;
		}

		return api('get', params).then(function (res) {
			const pageId = res && res.query && res.query.pageids && res.query.pageids[0];
			const page = pageId && res.query.pages && res.query.pages[pageId];
			const rev = page && page.revisions && page.revisions[0];
			const exists = !!(page && !page.missing && page.revisions && page.revisions.length);

			const full = rev && rev.slots && rev.slots.main ? rev.slots.main['*'] : '';
			const base = (rev && rev.timestamp) || res.curtimestamp;
			const contentModel = (page && page.contentmodel) || 'wikitext';
			const isOldRev = !!(viewedRevId && rev && rev.revid && rev.revid !== mw.config.get('wgCurRevisionId'));

			return {
				start: res.curtimestamp,
				base: base,
				full: full,
				exists: exists,
				contentModel: contentModel,
				isOldRev: isOldRev,
				revId: rev ? rev.revid : null,
				serverLatestRevId: page ? (page.lastrevid || (rev ? rev.revid : null)) : null
			};
		});
	}

	function formatLocalSummary(summaryText) {
		if (!summaryText.trim()) return '';
		let rest = summaryText;
		let prefixHtml = '';
		const secMatch = rest.match(/^\/\*\s*(.*?)\s*\*\/\s*/);
		if (secMatch) {
			const secTitle = secMatch[1];
			const anchor = encodeURIComponent(secTitle.replace(/ /g, '_')).replace(/%/g, '.');
			prefixHtml = '<span class="autocomment">&#8594;<a href="#' + mw.html.escape(anchor) + '"><bdi dir="ltr">' + mw.html.escape(secTitle) + '</bdi></a>:</span> ';
			rest = rest.slice(secMatch[0].length);
		}
		const linkRegex = /\[\[([^|\]\n]+)(?:\|([^\]\n]+))?\]\]/g;
		let result = '';
		let lastIndex = 0;
		let match;
		while ((match = linkRegex.exec(rest)) !== null) {
			result += mw.html.escape(rest.slice(lastIndex, match.index));
			const target = match[1].trim();
			const label = (match[2] || target).trim();
			result += '<a href="' + mw.html.escape(mw.util.getUrl(target)) + '">' + mw.html.escape(label) + '</a>';
			lastIndex = linkRegex.lastIndex;
		}
		result += mw.html.escape(rest.slice(lastIndex));
		return prefixHtml + result;
	}

	function showCompareDiff(targetContainer, title, from, to) {
		mw.loader.load('mediawiki.diff.styles');
		const isNarrow = $(window).width() <= 768;
		const compareParams = {
			action: 'compare',
			fromslots: 'main',
			'fromtext-main': from,
			fromtitle: title,
			frompst: 'true',
			toslots: 'main',
			'totext-main': to,
			totitle: title,
			topst: 'true'
		};

		if (isNarrow) {
			compareParams.difftype = 'inline';
		}

		return api('post', compareParams).then(function (r) {
			if (r && r.compare && r.compare['*']) {
				const $table = $('<table>')
					.addClass('diff')
					.append(
						$('<colgroup>').append(
							$('<col>').addClass('diff-marker'),
							$('<col>').addClass('diff-content'),
							$('<col>').addClass('diff-marker'),
							$('<col>').addClass('diff-content')
						)
					)
					.append(r.compare['*']);
				targetContainer.empty().append($table);
			} else {
				targetContainer.empty().append($('<div>').addClass('qe-diff-empty-notice').text(mw.msg('qe-diff-empty')));
			}
		});
	}

	function extractSectionHeader(text) {
		const match = text.match(/^(=+)(.+)\1\s*(?:\n|$)/);
		return match ? match[2].trim() : '';
	}

	function resolveLanguageMode(req, contentModel) {
		const isWikitext = (contentModel === 'wikitext');
		if (isWikitext) {
			const { mediawiki } = req('ext.CodeMirror.mode.mediawiki');
			return { modeName: 'mediawiki', langSupport: mediawiki() };
		}
		const modes = req('ext.CodeMirror.modes');
		let fn = null;
		let mName = 'javascript';
		if (contentModel === 'javascript' || contentModel === 'js') {
			fn = modes.javascript;
			mName = 'javascript';
		} else if (contentModel === 'css' || contentModel === 'sanitized-css') {
			fn = modes.css;
			mName = 'css';
		} else if (contentModel === 'json' || contentModel === 'jsonc') {
			fn = modes.json || modes.jsonc;
			mName = 'json';
		} else if (contentModel === 'Scribunto' || contentModel === 'lua') {
			fn = modes.lua;
			mName = 'lua';
		} else if (modes.html && contentModel === 'html') {
			fn = modes.html;
			mName = 'html';
		} else if (typeof modes.javascript === 'function') {
			fn = modes.javascript;
			mName = 'javascript';
		}
		return { modeName: mName, langSupport: typeof fn === 'function' ? fn() : null };
	}

	function getRequiredModules(isMobile, isWikitext) {
		if (!isMobile) {
			return isWikitext
				? ['ext.wikiEditor', 'ext.CodeMirror.WikiEditor', 'ext.CodeMirror.mode.mediawiki']
				: ['ext.wikiEditor', 'ext.CodeMirror.WikiEditor', 'ext.CodeMirror.modes'];
		}
		return isWikitext
			? ['ext.CodeMirror', 'ext.CodeMirror.mode.mediawiki']
			: ['ext.CodeMirror', 'ext.CodeMirror.modes'];
	}

	function getSummaryPresetsHtml() {
		const lang = mw.config.get('wgUserLanguage') || 'default';
		if (!summaryPresetsCache[lang]) {
			summaryPresetsCache[lang] = api('get', {
				action: 'parse',
				text: '{{int:summary}}',
				prop: 'text',
				disablelimitreport: true
			}, true).then(function (parseRes) {
				if (parseRes && parseRes.parse && parseRes.parse.text && parseRes.parse.text['*']) {
					return parseRes.parse.text['*'];
				}
				return Promise.reject();
			}).catch(function (err) {
				delete summaryPresetsCache[lang];
				return Promise.reject(err);
			});
		}
		return summaryPresetsCache[lang];
	}

	function buildErrorBox(htmlSnippet) {
		const errorNotice = new OO.ui.MessageWidget({
			type: 'error',
			inline: false,
			label: new OO.ui.HtmlSnippet(htmlSnippet)
		});
		return $('<div>').addClass('qe-server-error-container').append(errorNotice.$element);
	}

	function buildServerErrorNotice(code, data) {
		let rawHtml = '';
		let isAlreadyHtml = false;
		const client = getApi();

		if (data && data.errors && data.errors.length) {
			const allHaveHtml = data.errors.every(function (e) {
				return !!(e.html || e['*']);
			});
			if (allHaveHtml) {
				isAlreadyHtml = true;
				rawHtml = data.errors.map(function (e) {
					return e.html || e['*'];
				}).join('<hr>');
			} else {
				rawHtml = data.errors.map(function (e) {
					return e.html || e['*'] || e.message || (e.code && mw.message(e.code).exists() ? mw.message(e.code).text() : e.code) || '';
				}).filter(Boolean).join('\n');
			}
		} else if (data && data.error) {
			if (data.error.html || data.error['*']) {
				isAlreadyHtml = true;
				rawHtml = data.error.html || data.error['*'];
			} else {
				rawHtml = data.error.info || (data.error.code && mw.message(data.error.code).exists() ? mw.message(data.error.code).text() : data.error.code) || '';
			}
		}

		if (!rawHtml && data && typeof client.getErrorMessage === 'function') {
			try {
				const $apiMsg = client.getErrorMessage(data);
				if ($apiMsg && $apiMsg.length) {
					rawHtml = $('<div>').append($apiMsg.clone()).html();
					isAlreadyHtml = true;
				}
			} catch (_e) {}
		}

		if (!rawHtml && typeof code === 'string' && mw.message(code).exists()) {
			rawHtml = mw.message(code).text();
		}

		if (!rawHtml) {
			rawHtml = (typeof code === 'string') ? code : mw.msg('unknown-error');
		}

		const containsWikitext = !isAlreadyHtml && /\[\[|\{\{|'''/.test(rawHtml);
		const renderPromise = containsWikitext
			? api('get', {
				action: 'parse',
				text: rawHtml,
				prop: 'text',
				disablelimitreport: true
			}, true).then(function (pRes) {
				return (pRes && pRes.parse && pRes.parse.text && pRes.parse.text['*']) || mw.html.escape(rawHtml).replace(/\n/g, '<br>');
			}).catch(function () {
				return mw.html.escape(rawHtml).replace(/\n/g, '<br>');
			})
			: Promise.resolve(isAlreadyHtml ? rawHtml : mw.html.escape(rawHtml).replace(/\n/g, '<br>'));

		return renderPromise.then(buildErrorBox);
	}

	function updatePageContentDom(contentText, contentModel, parsedHtml) {
		if (contentModel === 'wikitext') {
			let $directParserOutput = contentText.children('.mw-parser-output.mw-content-ltr, .mw-parser-output.mw-content-rtl');
			if (!$directParserOutput.length) {
				$directParserOutput = contentText.children('.mw-parser-output');
			}
			if ($directParserOutput.length) {
				$directParserOutput.first().replaceWith(parsedHtml);
				$directParserOutput.slice(1).remove();
			} else {
				contentText.html(parsedHtml);
			}
			return;
		}

		const $directCode = contentText.children('.mw-highlight, pre.mw-code, pre.mw-css, pre.mw-js, table.mw-json, pre');
		if ($directCode.length) {
			$directCode.first().replaceWith(parsedHtml);
			$directCode.slice(1).remove();
			return;
		}

		if (/json/i.test(contentModel)) {
			const $jsonOut = contentText.children('.mw-parser-output').filter(function () {
				return $(this).find('.mw-json, pre, .mw-highlight').length > 0;
			});
			if ($jsonOut.length) {
				$jsonOut.first().replaceWith(parsedHtml);
				$jsonOut.slice(1).remove();
				return;
			}
		}

		const $nestedCode = contentText.find('.mw-highlight, pre.mw-code, pre.mw-css, pre.mw-js, table.mw-json').filter(function () {
			return $(this).closest('#mw-clearyourcache, .mw-warning-with-logexcerpt, .mw-editsection, .mw-parser-output').length === 0;
		});
		if ($nestedCode.length) {
			const $firstWrapper = $nestedCode.first().parentsUntil(contentText).last();
			const $target = $firstWrapper.length ? $firstWrapper : $nestedCode.first();
			$target.replaceWith(parsedHtml);
			$nestedCode.slice(1).each(function () {
				const $w = $(this).parentsUntil(contentText).last();
				($w.length ? $w : $(this)).remove();
			});
			return;
		}

		const $directOut = contentText.children('.mw-parser-output');
		if ($directOut.length && /json/i.test(contentModel)) {
			$directOut.first().replaceWith(parsedHtml);
			$directOut.slice(1).remove();
			return;
		}

		const $clearCache = contentText.children('#mw-clearyourcache');
		if ($clearCache.length) {
			$clearCache.last().after(parsedHtml);
		} else {
			contentText.html(parsedHtml);
		}
	}

	function reloadPageCleanly() {
		try {
			const reloadUrl = new URL(location.href);
			if (reloadUrl.searchParams.has('redlink') || reloadUrl.searchParams.get('action') === 'edit') {
				reloadUrl.searchParams.delete('redlink');
				reloadUrl.searchParams.delete('action');
				location.href = reloadUrl.toString();
				return;
			}
		} catch (_e) {}
		location.reload();
	}

	async function showEditor($element, customTitle, customSectionID) {
		const isMobile = mw.config.get('skin') === 'minerva' || window.innerWidth <= 768;
		let title = customTitle;
		let sectionID = customSectionID;

		const isTriggerLink = !!($element && $element.length && !$element.is('#mw-content-text'));
		const heading = isTriggerLink ? $element.parents(':header, .mw-heading').last() : $();
		let matcher;
		let inserter;

		if (heading.length) {
			matcher = heading.nextUntil.bind(heading);
			inserter = heading.after.bind(heading);
			const targetEl = $element.siblings('.qe-target').last();
			const href = (targetEl && targetEl.attr && targetEl.attr('href')) || '';
			const titleMatch = (href && href.match && href.match(titleRegexp)) || [];
			const fallbackTitle = mw.config.get('wgPageName');
			if (!title) title = decodeURIComponent(titleMatch[1] || titleMatch[2] || fallbackTitle);
			if (sectionID === undefined) {
				const sectionMatch = /[?&]v?e?section=(T?-?\d+)/.exec(href);
				sectionID = sectionMatch ? sectionMatch[1] : null;
			}
		} else {
			const $articleContent = ($element && $element.length && $element.is('#mw-content-text')) ? $element : $('#mw-content-text');
			if (!title) title = mw.config.get('wgPageName');
			if (sectionID === undefined) sectionID = null;

			matcher = function (selector) { return $articleContent.find(selector); };
			inserter = function ($el) { $articleContent.prepend($el); };
		}

		if (!isMultiEdit) {
			if (typeof activeCleanupFn === 'function') {
				activeCleanupFn(true);
				activeCleanupFn = null;
			}
			$('.qe-hide').removeClass('qe-hide');
			$('.qe-heading').removeClass('qe-heading');
			$('#qe-editor').remove();
		}

		let editor;
		let isAborted = false;
		const progress = new OO.ui.ProgressBarWidget();
		progress.$element.addClass('qe-progress-bar');

		if (isTriggerLink) $element.addClass('qe-loading');

		const abortLoading = function () {
			if (isAborted) return;
			isAborted = true;
			if (isTriggerLink) $element.removeClass('qe-loading');
			progress.$element.remove();
			if (editor) editor.remove();
			if (isMobile && !$('#qe-editor.qe-mobile-fullscreen').length) {
				$('html, body').removeClass('qe-mobile-lock');
			}
			if (activeCleanupFn === abortLoading) {
				activeCleanupFn = null;
			}
		};

		if (!isMultiEdit) {
			activeCleanupFn = abortLoading;
		}

		if (isMobile) {
			$('html, body').addClass('qe-mobile-lock');
			const $loadingBox = $('<div>').addClass('qe-mobile-loading-box').append(
				progress.$element,
				new OO.ui.ButtonWidget({ label: mw.msg('cancel'), flags: ['destructive'], framed: false })
					.on('click', abortLoading).$element
			);
			editor = $('<div id="qe-editor" class="qe-mobile-fullscreen qe-loading">').append($loadingBox);
			$(document.body).append(editor);
		} else {
			inserter(progress.$element);
		}

		const pageInfoPromise = getPageInfo(title, sectionID);
		const messagesPromise = getApi().loadMessagesIfMissing([
			'showpreview', 'showdiff', 'cancel', 'minoredit', 'summary-preview', 'unknown-error'
		]);

		let guessedIsWikitext;
		if (title === mw.config.get('wgPageName') && mw.config.get('wgPageContentModel')) {
			guessedIsWikitext = (mw.config.get('wgPageContentModel') === 'wikitext');
		} else {
			guessedIsWikitext = !/\.(js|css|json)$/i.test(title) && !/^(Module|模組|模块):/i.test(title);
		}

		const modulesToPreload = getRequiredModules(isMobile, guessedIsWikitext);
		const editorModulesPromise = mw.loader.using(modulesToPreload).catch(function (e) {
			qeLog(mw.msg('qe-error-load-modules'), e);
			return null;
		});

		Promise.all([pageInfoPromise, messagesPromise, editorModulesPromise]).then(async function ([r, , preloadedReq]) {
			if (isAborted) return;

			let start = r.start;
			let base = r.base;
			let initialServerLatestRevId = r.serverLatestRevId;
			const full = r.full;
			const contentModel = r.contentModel;
			const isNewPage = !r.exists;
			let saving = false;
			let isDestroyed = false;
			let expanded = (sectionID === null);
			let previewRequestSeq = 0;
			let scrollRafId = 0;
			let scrollTimer1 = 0;
			let scrollTimer2 = 0;
			let cmInputAttached = false;
			const remainderStart = (sectionID !== null && sectionID !== undefined && sectionID !== '') ? full.match(/\n=+.+=+(?:\n|$)/) : null;
			const part = isNewPage ? '' : remainderStart ? full.substring(0, remainderStart.index) : full;
			const remainder = isNewPage ? '' : remainderStart ? full.substring(remainderStart.index) : '';
			let level = 0;
			let cmInstance = null;

			const actualIsWikitext = (contentModel === 'wikitext');
			let modulesRequire = preloadedReq;
			if (actualIsWikitext !== guessedIsWikitext || !modulesRequire) {
				const actualModules = getRequiredModules(isMobile, actualIsWikitext);
				modulesRequire = await mw.loader.using(actualModules).catch(function (e) {
					qeLog(mw.msg('qe-error-load-modules'), e);
					return null;
				});
			}

			if (isAborted) return;

			full.replace(/^(=+).+?(=+)(?:\n|$)/, function (m, a, b) {
				level = Math.min(a.length, b.length);
				return m;
			});

			let levelMatch = 'h1:has(*), .mw-heading1';
			for (let i = 2; i <= level; i++) levelMatch += ', h' + i + ':has(*), .mw-heading' + i;

			const partSection = heading.length ? matcher(':header:has(*), .mw-heading') : $();
			const fullSection = heading.length ? matcher(levelMatch) : $();

			const textarea = new OO.ui.MultilineTextInputWidget({
				rows: isMobile ? 12 : 8,
				maxRows: isMobile ? 25 : 35,
				autosize: false,
				value: part
			});

			textarea.$element.css({ 'width': '100%', 'max-width': '100%' });

			function getEditorText() {
				if (typeof $(textarea.$input).textSelection === 'function') {
					const val = $(textarea.$input).textSelection('getContents');
					if (val !== undefined && val !== null) return val;
				}
				if (cmInstance && cmInstance.view && cmInstance.view.state && cmInstance.view.state.doc) {
					return cmInstance.view.state.doc.toString();
				}
				return textarea.getValue();
			}

			function getFirstLine() {
				if (cmInstance && cmInstance.view && cmInstance.view.state && cmInstance.view.state.doc) {
					return cmInstance.view.state.doc.line(1).text;
				}
				const val = getEditorText();
				const nl = val.indexOf('\n');
				return nl === -1 ? val : val.slice(0, nl);
			}

			function setEditorText(val) {
				if (typeof $(textarea.$input).textSelection === 'function') {
					$(textarea.$input).textSelection('setContents', val);
				} else {
					textarea.setValue(val);
				}
			}

			function isDirty() {
				return getEditorText() !== (expanded ? full : part);
			}

			let currentSectionHeader = extractSectionHeader(part);

			const summary = new OO.ui.TextInputWidget({
				value: (isNewPage || sectionID === null || !currentSectionHeader) ? '' : '/* ' + currentSectionHeader + ' */ '
			});

			const mobileTopSummary = new OO.ui.TextInputWidget({
				placeholder: mw.msg('qe-summary-placeholder'),
				value: summary.getValue()
			});

			let presetItems = [];
			function updatePresetSelectionStates() {
				const curSummary = summary.getValue();
				for (let i = 0; i < presetItems.length; i++) {
					presetItems[i].$el.toggleClass('qe-summary-selected', presetItems[i].regex.test(curSummary));
				}
			}

			summary.on('change', function (val) {
				if (mobileTopSummary.getValue() !== val) mobileTopSummary.setValue(val);
				updatePresetSelectionStates();
			});
			mobileTopSummary.on('change', function (val) {
				if (summary.getValue() !== val) summary.setValue(val);
			});

			function checkAndUpdateSectionHeader() {
				if (isDestroyed || sectionID === null) return;
				const newHeader = extractSectionHeader(getFirstLine());
				if (newHeader && newHeader !== currentSectionHeader) {
					const curSummary = summary.getValue();
					if (/^\/\*\s*.*?\s*\*\//.test(curSummary)) {
						summary.setValue(curSummary.replace(/^\/\*\s*.*?\s*\*\//, '/* ' + newHeader + ' */'));
					} else {
						summary.setValue('/* ' + newHeader + ' */ ' + curSummary.trim());
					}
					currentSectionHeader = newHeader;
				}
			}

			if (sectionID !== null) {
				textarea.$input.on('input', checkAndUpdateSectionHeader);
				textarea.on('change', checkAndUpdateSectionHeader);
			}

			function beforeUnloadHandler(e) {
				if (!isDestroyed && isWarnUnsaved && isDirty()) {
					e.preventDefault();
					e.returnValue = '';
					return '';
				}
			}
			if (isWarnUnsaved) {
				window.addEventListener('beforeunload', beforeUnloadHandler);
			}

			const isDefaultMinor = localStorage.getItem('qe_default_minor') === 'true';
			const isUserMinor = mw.user.options.get('minoredit') === 1 || mw.user.options.get('minoredit') === '1';

			const minor = new OO.ui.CheckboxInputWidget({
				accessKey: 'i',
				selected: isDefaultMinor || isUserMinor
			});

			const save = new OO.ui.ButtonInputWidget({
				label: mw.msg('qe-save'),
				flags: ['primary', 'progressive'],
				accessKey: 's'
			});

			const preview = new OO.ui.ButtonInputWidget({
				label: mw.msg('showpreview')
			});

			const compare = new OO.ui.ButtonInputWidget({
				label: mw.msg('showdiff')
			});

			const cancel = new OO.ui.ButtonInputWidget({
				label: mw.msg('cancel'),
				title: mw.msg('qe-btn-cancel-title'),
				flags: ['destructive']
			});

			const more = new OO.ui.ButtonInputWidget({
				label: '+',
				title: mw.msg('qe-btn-edit-all-section')
			});

			const buttons = new OO.ui.HorizontalLayout({
				items: [save, preview, compare, cancel]
			});

			if (!isNewPage && part !== full && sectionID !== null) {
				buttons.addItems([more], 3);
			}

			if (partSection.length) partSection.addClass('qe-hide');
			if (heading.length) heading.addClass('qe-heading');
			if (isTriggerLink) $element.removeClass('qe-loading');
			progress.$element.remove();
			textarea.$input.addClass('qe-textarea');

			function blurAll() {
				if (document.activeElement && typeof document.activeElement.blur === 'function') {
					document.activeElement.blur();
				}
				if (textarea && textarea.$input) {
					textarea.$input.blur();
				}
				if (cmInstance && cmInstance.view && cmInstance.view.contentDOM) {
					cmInstance.view.contentDOM.blur();
				}
			}

			function closeEditor(isSuccess, silent) {
				if (isDestroyed) return;
				isDestroyed = true;
				previewRequestSeq++;

				if (scrollRafId) cancelAnimationFrame(scrollRafId);
				if (scrollTimer1) clearTimeout(scrollTimer1);
				if (scrollTimer2) clearTimeout(scrollTimer2);

				blurAll();

				if (isWarnUnsaved) {
					window.removeEventListener('beforeunload', beforeUnloadHandler);
				}

				if (cmInputAttached && cmInstance && cmInstance.view && cmInstance.view.dom) {
					try {
						cmInstance.view.dom.removeEventListener('input', checkAndUpdateSectionHeader);
					} catch (_e) {}
					cmInputAttached = false;
				}

				if (cmInstance) {
					if (typeof cmInstance.destroy === 'function') {
						try { cmInstance.destroy(); } catch (_e) {}
					}
					cmInstance = null;
				}

				if (typeof textarea.$input.wikiEditor === 'function') {
					try { textarea.$input.wikiEditor('destroy'); } catch (_e) {}
				}

				if (editor) {
					editor.remove();
					editor = null;
				}

				if (isMobile && !$('#qe-editor.qe-mobile-fullscreen').length) {
					$('html, body').removeClass('qe-mobile-lock');
				}
				if (isTriggerLink) $element.removeClass('qe-loading');
				if (heading.length) heading.removeClass('qe-heading');
				if (partSection.length) partSection.removeClass('qe-hide');
				if (fullSection.length) fullSection.removeClass('qe-hide');

				if (activeCleanupFn === cleanupRef) {
					activeCleanupFn = null;
				}

				if (window.qeIntegrations && typeof window.qeIntegrations.onEditorClose === 'function') {
					try { window.qeIntegrations.onEditorClose(); } catch (_e) {}
				}

				if (!silent && isEditNoticeEnabled) {
					mw.notify(isSuccess ? mw.msg('qe-save-notice') : mw.msg('qe-cancel-notice'), {
						type: isSuccess ? 'success' : 'warn',
						tag: 'qe-status'
					});
				}
			}

			const cleanupRef = function (silent) {
				closeEditor(false, silent);
			};
			if (!isMultiEdit) {
				activeCleanupFn = cleanupRef;
			}

			summary.on('enter', function () {
				if (!saving) save.emit('click');
			});
			mobileTopSummary.on('enter', function () {
				if (saving) return;
				if (isMobile) switchMobileView('edit');
				save.emit('click');
			});

			function checkLatestServerRevision() {
				return api('get', {
					action: 'query',
					curtimestamp: 1,
					prop: 'info',
					indexpageids: 1,
					titles: title
				}, true).then(function (res) {
					const pageId = res && res.query && res.query.pageids && res.query.pageids[0];
					const page = pageId && res.query.pages && res.query.pages[pageId];
					const currentLatest = page ? page.lastrevid : null;

					if (initialServerLatestRevId && currentLatest && currentLatest > initialServerLatestRevId) {
						return { hasConflict: true, currentLatest: currentLatest };
					}
					return { hasConflict: false };
				}).catch(function () {
					return { hasConflict: false };
				});
			}

			function handleEditConflict(myFullText) {
				previewRequestSeq++;
				return getPageInfo(title, sectionID).then(function (rLive) {
					if (isDestroyed) return;
					const theirText = rLive.full;

					const autoMerge = tryThreeWayMerge(full, theirText, myFullText);
					if (autoMerge.success) {
						mw.notify(mw.msg('qe-conflict-automerged'), { type: 'warn' });
						setEditorText(autoMerge.text);
						base = rLive.base;
						start = rLive.start;
						initialServerLatestRevId = rLive.serverLatestRevId;
						expanded = true;
						if (fullSection.length) fullSection.addClass('qe-hide');
						more.setDisabled(true);
						return;
					}

					const conflictNotice = new OO.ui.MessageWidget({
						type: 'error',
						label: $('<div>')
							.append($('<strong>').text(mw.msg('qe-conflict-title')))
							.append($('<p>').text(mw.msg('qe-conflict-explain')))
					});

					const btnMergeInEditor = new OO.ui.ButtonWidget({
						label: mw.msg('qe-conflict-btn-mark-editor'),
						flags: ['primary', 'progressive']
					});
					const btnOverwrite = new OO.ui.ButtonWidget({
						label: mw.msg('qe-conflict-btn-overwrite'),
						flags: ['destructive']
					});
					const btnReload = new OO.ui.ButtonWidget({
						label: mw.msg('qe-conflict-btn-reload')
					});

					const conflictToolbar = new OO.ui.HorizontalLayout({
						items: [btnMergeInEditor, btnOverwrite, btnReload]
					});

					const $diffBox = $('<div>').css({ 'margin': '10px 0', 'max-height': '260px', 'overflow-y': 'auto' });
					showCompareDiff($diffBox, title, theirText, myFullText);

					const $conflictBlock = $('<div>')
						.addClass('qe-conflict-box')
						.append(conflictNotice.$element, conflictToolbar.$element.css({ 'margin': '8px 0' }), $diffBox);

					btnMergeInEditor.on('click', function () {
						const conflictMarked =
							'<<<<<<< ' + mw.msg('qe-conflict-tab-your') + '\n' +
							myFullText + '\n' +
							'=======\n' +
							theirText + '\n' +
							'>>>>>>> ' + mw.msg('qe-conflict-tab-their');
						setEditorText(conflictMarked);
						base = rLive.base;
						start = rLive.start;
						initialServerLatestRevId = rLive.serverLatestRevId;
						expanded = true;
						if (fullSection.length) fullSection.addClass('qe-hide');
						more.setDisabled(true);
						$conflictBlock.remove();
						if (isMobile) switchMobileView('edit');
					});

					btnOverwrite.on('click', function () {
						base = rLive.base;
						start = rLive.start;
						initialServerLatestRevId = rLive.serverLatestRevId;
						expanded = true;
						if (fullSection.length) fullSection.addClass('qe-hide');
						more.setDisabled(true);
						$conflictBlock.remove();
						if (isMobile) switchMobileView('edit');
						save.emit('click');
					});

					btnReload.on('click', function () {
						if (navigator.clipboard && navigator.clipboard.writeText) {
							navigator.clipboard.writeText(myFullText);
							mw.notify(mw.msg('qe-conflict-copied-backup'), { type: 'success' });
						}
						setEditorText(theirText);
						base = rLive.base;
						start = rLive.start;
						initialServerLatestRevId = rLive.serverLatestRevId;
						expanded = true;
						if (fullSection.length) fullSection.addClass('qe-hide');
						more.setDisabled(true);
						$conflictBlock.remove();
						if (isMobile) switchMobileView('edit');
					});

					if (isMobile) {
						switchMobileView('conflict', $conflictBlock);
					} else if (editor) {
						editor.children('.qe-editor-block').prepend($conflictBlock);
					}
				});
			}

			let $mobileViewPanel = null;
			let $mobileViewContent = null;
			let $mobileEditorMain = null;
			let $mobileTopSaveBtn = null;

			function switchMobileView(viewMode, $customContent) {
				if (!isMobile || !$mobileViewPanel || !$mobileViewContent || isDestroyed) return;

				if (viewMode === 'edit') {
					$mobileViewPanel.addClass('qe-hide');
					$mobileViewContent.empty();
					$mobileEditorMain.removeClass('qe-hide');
					editOptions.removeClass('qe-hide');
					if (cmInstance && cmInstance.view && typeof cmInstance.view.requestMeasure === 'function') {
						cmInstance.view.requestMeasure();
					}
				} else {
					const hideTopSave = (viewMode === 'conflict');
					mobileTopSummary.$element.toggleClass('qe-hide', hideTopSave);
					if ($mobileTopSaveBtn) $mobileTopSaveBtn.toggleClass('qe-hide', hideTopSave);

					$mobileEditorMain.addClass('qe-hide');
					editOptions.addClass('qe-hide');
					$mobileViewContent.empty().append($customContent);
					$mobileViewPanel.removeClass('qe-hide');
				}
			}

			function showServerErrorDialog(code, data) {
				previewRequestSeq++;
				buildServerErrorNotice(code, data).then(function ($errorContainer) {
					if (isDestroyed || !editor || !editor[0] || !editor[0].isConnected) return;

					if (isMobile) {
						switchMobileView('error', $errorContainer);
						mw.hook('wikipage.content').fire($errorContainer);
					} else {
						editor.children('.preview').remove();
						const $preview = $('<div>')
							.addClass('qe-border-color-base qe-preview-block preview')
							.append(
								$('<div>').css('margin-bottom', '8px').append(
									$('<strong>').css('color', '#d33').text(mw.msg('qe-error-publish-title'))
								),
								$errorContainer
							);

						$preview.appendTo(editor);
						mw.hook('wikipage.content').fire($preview);
						if ($preview[0] && typeof $preview[0].scrollIntoView === 'function') {
							$preview[0].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
						}
					}
				});
			}

			save.on('click', function () {
				blurAll();
				if (saving || isDestroyed) return;

				previewRequestSeq++;
				const currentContent = getEditorText();
				const fullText = currentContent + (expanded ? '' : remainder);
				saving = true;
				save.setLabel(mw.msg('qe-saving'));
				[compare, preview, cancel, more, save].forEach(b => b.setDisabled(true));

				checkLatestServerRevision().then(function (checkResult) {
					if (isDestroyed) return;
					if (checkResult.hasConflict) {
						saving = false;
						save.setLabel(mw.msg('qe-save'));
						[compare, preview, cancel, save].forEach(b => b.setDisabled(false));
						more.setDisabled(expanded);
						handleEditConflict(fullText);
						return;
					}

					const userSummary = summary.getValue().trim();
					const editParams = {
						action: 'edit',
						title: title,
						summary: userSummary ? userSummary + ' // ' + mw.msg('qe-summary-tag') : mw.msg('qe-summary-tag'),
						text: fullText,
						basetimestamp: base,
						starttimestamp: start
					};

					if (minor.isSelected()) {
						editParams.minor = true;
					} else {
						editParams.notminor = true;
					}

					if (sectionID !== null && sectionID !== undefined && sectionID !== '') {
						editParams.section = sectionID;
					}

					api('postWithEditToken', editParams, true).then(
						function (editRes) {
							if (isNewPage) {
								closeEditor(true);
								reloadPageCleanly();
								return;
							}

							const editInfo = editRes && editRes.edit;
							if (title === mw.config.get('wgPageName')) {
								if (editInfo && editInfo.newrevid) {
									mw.config.set({
										wgCurRevisionId: editInfo.newrevid,
										wgRevisionId: editInfo.newrevid
									});
								}

								if (editInfo && editInfo.nochange !== undefined) {
									closeEditor(true);
									return;
								}

								const catLinks = $('#catlinks');
								const parseParams = {
									action: 'parse',
									prop: catLinks.length ? ['text', 'categorieshtml'] : 'text'
								};
								if (editInfo && editInfo.newrevid) {
									parseParams.oldid = editInfo.newrevid;
								} else {
									parseParams.page = mw.config.get('wgPageName');
								}

								api('get', parseParams).then(function (parseRes) {
									const contentText = $('#mw-content-text');
									const parsedHtml = parseRes && parseRes.parse && parseRes.parse.text && parseRes.parse.text['*'];
									closeEditor(true);

									if (parsedHtml !== undefined) {
										updatePageContentDom(contentText, contentModel, parsedHtml);
										mw.hook('wikipage.content').fire(contentText);
									}

									if (catLinks.length && parseRes && parseRes.parse && parseRes.parse.categorieshtml) {
										catLinks.replaceWith(parseRes.parse.categorieshtml['*']);
										mw.hook('wikipage.categories').fire($('#catlinks'));
									}
								}, function () {
									closeEditor(true);
								});
							} else {
								closeEditor(true);
							}
						},
						function (code, data) {
							if (isDestroyed) return;
							saving = false;
							save.setLabel(mw.msg('qe-save'));
							[compare, preview, cancel, save].forEach(b => b.setDisabled(false));
							more.setDisabled(expanded);

							if (code === 'editconflict') {
								handleEditConflict(fullText);
							} else {
								showServerErrorDialog(code, data);
							}
						}
					);
				});
			});

			preview.on('click', function () {
				if (isDestroyed) return;
				blurAll();
				const reqSeq = ++previewRequestSeq;
				const currentContent = getEditorText();
				const summaryValue = summary.getValue().trim();
				const loader = new OO.ui.ProgressBarWidget();
				loader.$element.addClass('qe-progress-bar');

				let $previewTarget;
				if (isMobile) {
					$previewTarget = $('<div>').append(loader.$element);
					switchMobileView('preview', $previewTarget);
				} else {
					editor.children('.preview').remove();
					editor.children('.qe-progress-bar').remove();
					loader.$element.appendTo(editor);
				}

				let localJsonInvalid = false;
				if (contentModel === 'json') {
					try {
						JSON.parse(currentContent);
					} catch (_e) {
						localJsonInvalid = true;
					}
				}

				const jsonMsgPromise = localJsonInvalid
					? getApi().loadMessagesIfMissing(['invalid-json-data', 'json-error-syntax']).catch(function () { return null; })
					: Promise.resolve(null);

				const canServerValidate = !localJsonInvalid && (contentModel === 'javascript' || contentModel === 'sanitized-css' || contentModel === 'Scribunto');
				const validatePromise = canServerValidate
					? api('post', {
						action: 'codemirror-validate',
						content: currentContent,
						contentmodel: contentModel,
						title: title
					}, true).catch(function () {
						return null;
					})
					: Promise.resolve(null);

				let parsePromise;
				if (localJsonInvalid && !summaryValue) {
					parsePromise = Promise.resolve(null);
				} else {
					const parseParams = {
						action: 'parse',
						title: title,
						contentmodel: localJsonInvalid ? 'wikitext' : contentModel,
						summary: summaryValue,
						prop: localJsonInvalid ? 'summary' : 'text|summary|parsewarningshtml',
						pst: 'true',
						preview: 'true',
						disablelimitreport: 'true',
						disableeditsection: 'true',
						disabletoc: 'true',
						text: localJsonInvalid ? '' : currentContent
					};
					if (sectionID !== null && sectionID !== undefined && sectionID !== '') {
						parseParams.sectionpreview = 'true';
					}
					parsePromise = api('post', parseParams, true);
				}

				Promise.all([parsePromise, validatePromise, jsonMsgPromise]).then(async function ([mainRes, valRes]) {
					if (isDestroyed || reqSeq !== previewRequestSeq) return;
					loader.$element.remove();
					if (!editor || !editor[0] || !editor[0].isConnected) return;

					let summaryBlock = '';
					if (summaryValue) {
						let parsedSummaryHtml = '';
						if (mainRes && mainRes.parse && mainRes.parse.parsedsummary) {
							parsedSummaryHtml = typeof mainRes.parse.parsedsummary === 'string'
								? mainRes.parse.parsedsummary
								: (mainRes.parse.parsedsummary['*'] || '');
						}
						if (!parsedSummaryHtml) {
							parsedSummaryHtml = formatLocalSummary(summaryValue);
						}

						summaryBlock =
							'<div class="qe-summary-preview qe-border-color-base">' +
							'<b>' + mw.msg('summary-preview') + '</b><br>' +
							'<span class="comment">' + parsedSummaryHtml + '</span>' +
							'</div>';
					}

					const mainHtml = (mainRes && mainRes.parse && mainRes.parse.text && mainRes.parse.text['*']) || '';
					const $container = $('<div>');
					if (summaryBlock) {
						$container.append(summaryBlock);
					}

					const hasUnfilledJsonError = !localJsonInvalid && /json/i.test(contentModel) && /\$1/.test(mainHtml);
					if (localJsonInvalid || hasUnfilledJsonError) {
						if (hasUnfilledJsonError) {
							await getApi().loadMessagesIfMissing(['invalid-json-data', 'json-error-syntax']).catch(function () {});
							if (isDestroyed || reqSeq !== previewRequestSeq || !editor || !editor[0] || !editor[0].isConnected) return;
						}
						const jsonErrText = mw.message('invalid-json-data').exists()
							? mw.message('invalid-json-data', mw.message('json-error-syntax').exists() ? mw.msg('json-error-syntax') : 'Syntax error').escaped()
							: mw.html.escape(mainHtml.replace(/<[^>]+>/g, '').trim());
						$container.append(buildErrorBox(jsonErrText));
					} else {
						if (valRes && valRes['codemirror-validate'] && valRes['codemirror-validate'].valid === false) {
							const errs = valRes['codemirror-validate'].errors || [];
							if (errs.length) {
								const errHtml = errs.map(function (err) {
									const msg = err.html || mw.html.escape(err.message || err.code || '');
									const loc = (err.line !== undefined)
										? ' (' + err.line + (err.column !== undefined ? ':' + err.column : '') + ')'
										: '';
									return msg + mw.html.escape(loc);
								}).join('<br>');
								$container.append(buildErrorBox(errHtml));
							}
						}

						const parseWarnings = (mainRes && mainRes.parse && mainRes.parse.parsewarningshtml) || [];
						if (parseWarnings.length) {
							const warnNotice = new OO.ui.MessageWidget({
								type: 'warning',
								inline: false,
								label: new OO.ui.HtmlSnippet(parseWarnings.join('<br>'))
							});
							$container.append(warnNotice.$element.css('margin-bottom', '0.5em'));
						}

						$container.append(mainHtml, '<div style="clear:both;"></div>');
					}

					if (isMobile) {
						if (!$previewTarget[0].isConnected) return;
						$previewTarget.empty().append($container);
					} else {
						editor.children('.preview').remove();
						$container.addClass('qe-border-color-base qe-preview-block preview').appendTo(editor);
					}
					mw.hook('wikipage.content').fire($container);
				}, function (code, data) {
					if (isDestroyed || reqSeq !== previewRequestSeq) return;
					buildServerErrorNotice(code, data).then(function ($errorContainer) {
						if (isDestroyed || reqSeq !== previewRequestSeq) return;
						loader.$element.remove();
						if (!editor || !editor[0] || !editor[0].isConnected) return;

						if (isMobile) {
							if (!$previewTarget[0].isConnected) return;
							$previewTarget.empty().append($errorContainer);
							mw.hook('wikipage.content').fire($errorContainer);
						} else {
							editor.children('.preview').remove();
							const $preview = $('<div>')
								.addClass('qe-border-color-base qe-preview-block preview')
								.append($errorContainer)
								.appendTo(editor);
							mw.hook('wikipage.content').fire($preview);
						}
					});
				});
			});

			compare.on('click', function () {
				if (isDestroyed) return;
				blurAll();
				const reqSeq = ++previewRequestSeq;
				const currentContent = getEditorText();
				const baseCompareText = part + (expanded ? remainder : '');
				const loader = new OO.ui.ProgressBarWidget();
				loader.$element.addClass('qe-progress-bar');

				const removeLoader = function () {
					loader.$element.remove();
				};

				if (isMobile) {
					const $diffBox = $('<div>').append(loader.$element);
					switchMobileView('diff', $diffBox);
					showCompareDiff($diffBox, title, baseCompareText, currentContent).then(removeLoader, removeLoader);
				} else {
					editor.children('.preview').remove();
					editor.children('.qe-progress-bar').remove();
					loader.$element.appendTo(editor);

					const $preview = $('<div>').addClass('qe-border-color-base qe-preview-block preview');
					showCompareDiff($preview, title, baseCompareText, currentContent).then(function () {
						removeLoader();
						if (isDestroyed || reqSeq !== previewRequestSeq) return;
						if (editor && editor[0] && editor[0].isConnected) {
							$preview.appendTo(editor);
						}
					}, removeLoader);
				}
			});

			cancel.on('click', function () {
				if (isDestroyed) return;
				blurAll();
				if (isConfirmCancel && isDirty()) {
					OO.ui.confirm(mw.msg('qe-confirm-cancel-prompt')).then(function (confirmed) {
						if (confirmed) {
							closeEditor(false);
						}
					});
				} else {
					closeEditor(false);
				}
			});

			more.on('click', function () {
				if (isDestroyed) return;
				expanded = true;
				setEditorText(getEditorText() + remainder);
				if (!cmInstance && typeof textarea.adjustSize === 'function') {
					textarea.adjustSize(true);
				}
				if (fullSection.length) fullSection.addClass('qe-hide');
				more.setDisabled(true);
			});

			const $summaryLabel = $('<div>').addClass('qe-summary-presets');

			function bindSummaryPresets(rawHtml) {
				if (isDestroyed) return;
				const $temp = $('<div>').html(rawHtml);
				const $span = $temp.find('span[style*="text-align"]').first();
				const $target = $span.length ? $span : $temp;
				$summaryLabel.empty().append($target.contents());

				presetItems = [];
				$summaryLabel.find('a').each(function () {
					const $a = $(this);
					const href = $a.attr('href') || '';
					if (href.indexOf('Help:') !== -1 || href.indexOf('%E7%BC%96%E8%BE%91%E6%91%98%E8%A6%81') !== -1) return;
					const tag = $a.text().trim();
					if (!tag) return;
					const escaped = tag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
					presetItems.push({
						$el: $a,
						regex: new RegExp('(?:^|\\s)' + escaped + '(?=\\s|$)', 'i')
					});
				});
				updatePresetSelectionStates();
			}

			getSummaryPresetsHtml().then(bindSummaryPresets).catch(function () {
				if (isDestroyed) return;
				$summaryLabel.text(mw.msg('qe-summary-placeholder'));
				presetItems = [];
			});

			$summaryLabel.on('click', 'a', function (e) {
				const href = $(this).attr('href') || '';
				if (href.indexOf('Help:') !== -1 || href.indexOf('%E7%BC%96%E8%BE%91%E6%91%98%E8%A6%81') !== -1) {
					return;
				}
				e.preventDefault();
				const tag = $(this).text().trim();
				if (!tag) return;

				let cur = summary.getValue();
				const escaped = tag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
				const regex = new RegExp('(\\s|^)' + escaped + '(?=\\s|$)', 'g');

				if (regex.test(cur)) {
					cur = cur.replace(regex, ' ').replace(/\s{2,}/g, ' ').trim();
					cur = cur.replace(/(\/\*.*?\*\/)\s*/, '$1 ');
				} else {
					cur = cur.trim() ? (cur.trim() + ' ' + tag) : tag;
				}
				summary.setValue(cur);
			});

			const editOptions = $('<div>')
				.addClass('qe-edit-options')
				.append(
					$('<div id="wpSummaryWidget">')
						.addClass('qe-border-color-base')
						.append(
							$summaryLabel,
							summary.$element.addClass('qe-summary-input')
						),
					new OO.ui.FieldLayout(minor, {
						label: new OO.ui.HtmlSnippet(mw.message('minoredit').parse()),
						align: 'inline'
					}).$element.addClass('qe-minoredit-block'),
					buttons.$element.addClass('qe-buttons qe-border-color-base'),
					r.isOldRev
						? new OO.ui.MessageWidget({
							type: 'warning',
							inline: true,
							label: mw.msg('qe-editing-old-revision', r.revId)
						}).$element.css({ 'margin-top': '0.5em' })
						: undefined,
					title !== mw.config.get('wgPageName')
						? $('<div>')
							.addClass('qe-not-current-page qe-border-color-base')
							.append(
								mw.msg('qe-page-edit-notice'),
								$('<a>')
									.attr('href', mw.util.getUrl(title))
									.addClass('qe-not-current-page-link')
									.text(title.replace(/_/g, ' '))
							)
						: undefined
				);

			if (isMobile) {
				const toggleBtn = new OO.ui.ButtonWidget({
					label: mw.msg('qe-toggle-options'),
					icon: 'expand',
					framed: false
				});
				toggleBtn.on('click', function () {
					editOptions.toggleClass('qe-mobile-collapsed');
				});
				editOptions.addClass('qe-mobile-collapsed').prepend(toggleBtn.$element);
			}

			const $editorBlock = $('<div>').addClass('qe-editor-block qe-background-color-interactive qe-color-base qe-border-color-base');

			if (isMobile) {
				$mobileEditorMain = $('<div>').addClass('qe-editor-main-container').append(textarea.$element);
				$mobileViewContent = $('<div>').addClass('qe-mobile-view-content');
				const saveBtnWidget = new OO.ui.ButtonWidget({
					label: mw.msg('qe-save'),
					flags: ['primary', 'progressive']
				}).on('click', () => {
					if (saving) return;
					switchMobileView('edit');
					save.emit('click');
				});
				$mobileTopSaveBtn = saveBtnWidget.$element;

				const $mobileTopBar = $('<div>').addClass('qe-mobile-topbar').append(
					new OO.ui.ButtonWidget({
						label: mw.msg('qe-btn-back-to-edit'),
						icon: 'previous',
						flags: ['safe']
					}).on('click', () => switchMobileView('edit')).$element,
					mobileTopSummary.$element.addClass('qe-mobile-topbar-summary'),
					$mobileTopSaveBtn
				);
				$mobileViewPanel = $('<div>').addClass('qe-view-panel qe-hide').append($mobileTopBar, $mobileViewContent);
				$editorBlock.append($mobileEditorMain, $mobileViewPanel, editOptions);
				editor.empty().removeClass('qe-loading').append($editorBlock);
				if (!editor.parent().is(document.body)) {
					$(document.body).append(editor);
				}
			} else {
				$editorBlock.append(textarea.$element, editOptions);
				editor = $('<div id="qe-editor">').append($editorBlock);
				inserter(editor);
			}

			if (modulesRequire) {
				try {
					if (textarea.$input && textarea.$input[0]) {
						textarea.$input[0].setSelectionRange(0, 0);
						textarea.$input[0].scrollTop = 0;
					}

					if (!isMobile) {
						if (typeof mw.addWikiEditor === 'function') {
							mw.addWikiEditor(textarea.$input);
							textarea.$input.wikiEditor({
								toolbar: { format: {}, insert: {}, advanced: {} }
							});
						}

						const CodeMirrorWikiEditor = modulesRequire('ext.CodeMirror.WikiEditor');
						const { modeName, langSupport } = resolveLanguageMode(modulesRequire, contentModel);

						if (typeof CodeMirrorWikiEditor === 'function' && langSupport) {
							const cm = new CodeMirrorWikiEditor(textarea.$input[0], langSupport);
							cm.mode = modeName;
							cm.initialize();
							cmInstance = cm;
						}
					} else {
						const CodeMirror = modulesRequire('ext.CodeMirror');
						const { modeName, langSupport } = resolveLanguageMode(modulesRequire, contentModel);

						if (typeof CodeMirror === 'function' && langSupport) {
							const cm = new CodeMirror(textarea.$input[0], langSupport);
							cm.mode = modeName;
							cm.initialize();
							cmInstance = cm;
						}
					}

					const resetScrollToTop = function () {
						if (isDestroyed) return;
						if (cmInstance && cmInstance.view) {
							const sel = cmInstance.view.state.selection.main;
							if (sel.from !== 0 || sel.to !== 0) {
								cmInstance.view.dispatch({
									selection: { anchor: 0, head: 0 },
									scrollIntoView: false
								});
							}
							if (cmInstance.view.scrollDOM) {
								if (cmInstance.view.scrollDOM.scrollTop !== 0) {
									cmInstance.view.scrollDOM.scrollTop = 0;
								}
								if (cmInstance.view.scrollDOM.scrollLeft !== 0) {
									cmInstance.view.scrollDOM.scrollLeft = 0;
								}
							}
						}
						if (textarea.$input && textarea.$input[0]) {
							if (textarea.$input[0].scrollTop !== 0) {
								textarea.$input[0].scrollTop = 0;
							}
							if (textarea.$input[0].selectionStart !== 0 || textarea.$input[0].selectionEnd !== 0) {
								textarea.$input[0].setSelectionRange(0, 0);
							}
						}
						if (isMobile && $mobileEditorMain && $mobileEditorMain[0] && $mobileEditorMain[0].scrollTop !== 0) {
							$mobileEditorMain[0].scrollTop = 0;
						}
					};

					resetScrollToTop();
					scrollRafId = requestAnimationFrame(resetScrollToTop);
					scrollTimer1 = setTimeout(resetScrollToTop, 100);
					scrollTimer2 = setTimeout(resetScrollToTop, 300);

					if (sectionID !== null && cmInstance && cmInstance.view && cmInstance.view.dom) {
						cmInstance.view.dom.addEventListener('input', checkAndUpdateSectionHeader);
						cmInputAttached = true;
					}
				} catch (e) {
					qeLog(mw.msg('qe-error-init-cm'), e);
				}
			}

			if (!cmInstance && !isMobile && typeof textarea.setAutosize === 'function') {
				textarea.setAutosize(true);
				textarea.adjustSize(true);
			}
		}, function () {
			abortLoading();
		});
	}

	function clickHandler(event) {
		const $element = $(event.target).closest('.qe-editlink');
		if (!$element.length || $element.hasClass('qe-loading')) return;
		event.preventDefault();
		showEditor($element);
	}

	window.qeEditSection = function (title, sectionID, $mountTarget) {
		showEditor($mountTarget || $('#mw-content-text'), title, sectionID);
	};

	window.qeCoreReady = $.when(
		mw.loader.using([
			'oojs-ui-core',
			'oojs-ui-widgets',
			'oojs-ui-windows',
			'oojs-ui.styles.icons-interactions',
			'oojs-ui.styles.icons-editing-core',
			'oojs-ui.styles.icons-moderation'
		]),
		$.ready
	).then(function () {
		$(document.body).on('click', clickHandler);
		return true;
	});
})();