跳转到内容

User:Addonian1123/QuickRedirect.js

维基百科,自由的百科全书
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// <nowiki>

/*
var _addText="{{工具警告|QuickRedirect}}";
var _addText="{{Notice|<big>'''文檔請見:[[User:Addonian1123/QuickRedirect]]'''</big>}}";
-----
QuickRedirect - [[User:SuperGrey/gadgets/RedirectHelper]]的分支 - CC BY-SA 4.0或GPL-3.0 
*/

const userLang = mw.config.get('wgUserLanguage') || mw.config.get('wgUserVariant') || 'zh-hant';
const lowerLang = userLang.toLowerCase();
let langType = 'hant';
if (['zh-hans', 'zh-cn', 'zh-my', 'zh-sg'].some(l => lowerLang.startsWith(l))) {
	langType = 'hans';
} else if (lowerLang.startsWith('zh-tw')) {
	langType = 'tw';
}

function i18n(hans, hant, tw) {
	if (langType === 'hans') return hans;
	if (langType === 'tw') return tw || hant;
	return hant;
}

function getSysMsg(key, defaultText) {
	return (mw.message && mw.message(key).exists()) ? mw.msg(key) : defaultText;
}

function locText(val) {
	if (val === null || val === undefined) return '';
	if (typeof val === 'string') return val;
	if (typeof val !== 'object') return String(val);
	
	const variant = (mw.config.get('wgUserVariant') || mw.config.get('wgUserLanguage') || 'zh-hant').toLowerCase();
	if (val[variant]) return val[variant];
	
	let fallbacks = [];
	if (variant === 'zh-tw') {
		fallbacks = ['zh-tw', 'zh-hant', 'zh-hk', 'zh-mo', 'zh', 'zh-hans', 'zh-cn', 'en'];
	} else if (variant === 'zh-hant') {
		fallbacks = ['zh-hant', 'zh-tw', 'zh-hk', 'zh-mo', 'zh', 'zh-hans', 'zh-cn', 'en'];
	} else if (variant === 'zh-cn') {
		fallbacks = ['zh-cn', 'zh-hans', 'zh-sg', 'zh-my', 'zh', 'zh-hant', 'zh-tw', 'en'];
	} else if (variant === 'zh-hans') {
		fallbacks = ['zh-hans', 'zh-cn', 'zh-sg', 'zh-my', 'zh', 'zh-hant', 'zh-tw', 'en'];
	} else if (['zh-hk', 'zh-mo'].includes(variant)) {
		fallbacks = [variant, 'zh-hant', 'zh-tw', 'zh', 'zh-hans', 'zh-cn', 'en'];
	} else if (['zh-sg', 'zh-my'].includes(variant)) {
		fallbacks = [variant, 'zh-hans', 'zh-cn', 'zh', 'zh-hant', 'zh-tw', 'en'];
	} else {
		fallbacks = ['zh-hant', 'zh-tw', 'zh', 'zh-hans', 'zh-cn', 'en'];
	}
	
	for (const l of fallbacks) {
		if (val[l]) return val[l];
	}
	return Object.values(val)[0] || '';
}

const msg = {
	quickRedirect: 'QuickRedirect',
	close: getSysMsg('parentheses-close', i18n('关闭', '關閉')),
	save: getSysMsg('savechanges', i18n('保存更改', '儲存變更')),
	cancel: getSysMsg('cancel', i18n('取消', '取消')),
	createRedirect: i18n('创建重定向', '创建重定向', '新增重新導向'),
	editRedirect: i18n('编辑重定向', '編輯重定向', '編輯重新導向'),
	changeToRedirect: i18n('设定重定向', '設定重定向', '設定重新導向'),
	targetLabel: i18n('重定向目标:', '重定向目標:', '重新導向目標:'),
	targetPlaceholderForward: i18n('目标页面名称', '目標頁面名稱'),
	targetPlaceholderReverse: i18n('目标页面名称 (批量请以 | 分隔)', '目標頁面名稱 (批次請以 | 分隔)', '目標頁面名稱 (批次請以 | 分隔)'),
	directionLabel: i18n('重定向方向:', '重定向方向:', '重新導向方向:'),
	forward: i18n('此页面重定向至目标', '此頁面重定向至目標', '此頁面重新導向至目標'),
	reverse: i18n('目标页面重定向至此', '目標頁面重定向至此', '目標頁面重新導向至此'),
	categoryLabel: i18n('重定向分类模板:', '重定向分類模板:', '重新導向分類模板:'),
	summaryLabel: i18n('编辑摘要:', '編輯摘要:', '編輯摘要:'),
	summaryCleanup: i18n('整理重定向', '整理重定向', '整理重新導向'),
	summaryFixDouble: i18n('修复双重重定向', '修復雙重重定向', '修復雙重重新導向'),
	summaryFixLoop: i18n('修复循环重定向', '修復循環重定向', '修復循環重新導向'),
	summaryRemoveBad: i18n('移除误植的重定向分类模板', '移除誤植的重定向分類模板', '移除誤植的重新導向分類模板'),
	missingTarget: i18n('请输入重定向目标!', '請輸入重定向目標!', '請輸入重新導向目標!'),
	invalidTarget: i18n('无效的重定向目标:$1', '無效的重定向目標:$1', '無效的重新導向目標:$1'),
	selfRedirect: i18n('不能将页面重定向至同页!', '不能將頁面重定向至同一頁!', '不能將頁面重新導向至同一頁!'),
	forwardMultipleError: i18n('从此页重定向至其他页只能有一个目标!', '從此頁重定向至其他頁面只能有一個目標!', '從此頁面重新導向至其他頁面只能有一個目標!'),
	success: i18n('成功!', '成功!'),
	error: getSysMsg('error', i18n('发生错误', '發生錯誤')),
	checking: i18n('正在检查…', '正在檢查…'),
	processing: i18n('正在处理…', '正在處理…'),
	removeRedirect: i18n('移除重定向', '移除重定向', '移除重新導向'),
	removeConfirmTitle: getSysMsg('confirm', i18n('确认移除', '確認移除')),
	removeConfirmMessage: i18n('您确定要移除此页面的重定向与相关模板吗?', '您確定要移除此頁面的重定向與相關模板嗎?', '您確定要移除此頁面的重新導向與相關模板嗎?'),
	summaryRemoveRedirect: i18n('移除重定向', '移除重定向', '移除重新導向'),
	watchPage: getSysMsg('watchthis', i18n('加入监视列表', '加入監視清單')),
	syncTalkPage: i18n('同步讨论页:', '同步討論頁:'),
	syncAlways: i18n('永远', '永遠'),
	syncIfExists: i18n('仅讨论页存在时', '僅討論頁存在時'),
	syncIfMissing: i18n('仅讨论页不存在时', '僅討論頁不存在時'),
	syncNever: i18n('永不', '永不'),
	templateParameters1: i18n('模板参数', '模板參數'),
	templateParameters2: i18n('用于', '用於'),
	templateParameters3: i18n('个模板', '個模板'),
	templateParametersNoTemplate: i18n('无', '無'),
	templateParamsEmpty: i18n('模板参数(无)', '模板參數(無)'),
	noTemplateParams: i18n('无模板参数', '無模板參數'),
	autoFix: i18n('自动修复(推荐)', '自動修復(建議)'),
	doubleRedirectWarning: i18n('警告:目标页面已重定向到“$1”。', '警告:目標頁面已重定向到「$1」。', '警告:目標頁面已重新導向至「$1」。'),
	crossNamespaceWarning: i18n('警告:将建立跨空间重定向(从$1至$2)。', '警告:將建立跨空間重定向(從$1至$2)。', '警告:將建立跨空間重新導向(從$1至$2)。'),
	targetHasContentWarning: i18n('警告:“$1”已存在非重定向内容,设定重定向将覆盖现有内容!', '警告:「$1」已存在非重定向內容,設定重定向將覆蓋現有內容!', '警告:「$1」已存在非重新導向內容,設定重新導向將覆蓋現有內容!'),
	targetIsRedirectWarning: i18n('警告:“$1”目前已重定向至其他页面,继续操作将修改重定向目标。', '警告:「$1」目前已重定向至其他頁面,繼續操作將修改重定向目標。', '警告:「$1」目前已重新導向至其他頁面,繼續操作將修改重新導向目標。'),
	batchLimitError5: i18n('错误:为避免此工具遭到滥用,批量修改5个以上页面需要自动确认用户权限。', '錯誤:為避免此工具遭到濫用,批次修改5個以上頁面需要自動確認用戶權限。', '錯誤:為避免此工具遭到濫用,批次修改5個以上頁面需要自動確認使用者權限。'),
	batchLimitError10: i18n('错误:为避免此工具遭到滥用,批量修改10个以上页面需要延伸确认用户权限。', '錯誤:為避免此工具遭到濫用,批次修改10個以上頁面需要延伸確認用戶權限。', '錯誤:為避免此工具遭到濫用,批次修改10個以上頁面需要延伸確認使用者權限。'),
	batchLimitError100: i18n('错误:为避免此工具遭到滥用,批量修改100个以上页面需要管理员权限。', '錯誤:為避免此工具遭到濫用,批次修改100個以上頁面需要管理員權限。', '錯誤:為避免此工具遭到濫用,批次修改100個以上頁面需要管理員權限。'),
	batchLimitError500: i18n('错误:为避免此工具遭到滥用,批量修改的最大值为500个页面。', '錯誤:為避免此工具遭到濫用,批次修改的最大值為500個頁面。', '錯誤:為避免此工具遭到濫用,批次修改的最大值為500個頁面。'),
};

const CONFIG = {
	STYLES: `
		#create-redirect-button { margin-bottom: 20px }
		#quick-redirect-box {
			position: relative;
			margin-right: auto;
			margin-bottom: 25px !important;
			margin-left: auto;
			background-color: var(--background-color-interactive-subtle, #f8f9fa);
			width: 800px;
			max-width: calc(100% - 50px);
			color: var(--color-base, #202122);
			border: 1px solid var(--border-color-base, #a2a9b1);
		}
		.redirect-input-layout label { font-weight: 700 }
		.quick-redirect-warning-box {
			display: flex;
			align-items: center;
			justify-content: space-between;
			gap: 12px;
			background-color: var(--background-color-warning-subtle, #fdf2d5);
			border: 1px solid var(--border-color-warning, #fca652);
			color: var(--color-warning, #734500);
			padding: 8px 12px;
			border-radius: 2px;
			margin-top: 8px;
			margin-bottom: 8px;
		}
		.quick-redirect-warning-text {
			font-size: 0.9em;
			line-height: 1.4;
			flex: 1;
			white-space: pre-wrap;
		}
		.redirect-helper-template-parameters-container,
		.quick-redirect-template-parameters-container {
			margin-top: 10px;
			margin-bottom: 10px;
			background-color: var(--background-color-interactive-subtle, #f8f9fa);
			border: 1px solid var(--border-color-base, #a2a9b1);
			padding: 5px;
		}
		.redirect-helper-template-parameters-container summary,
		.quick-redirect-template-parameters-container summary {
			cursor: pointer;
			font-weight: 700;
		}
		.redirect-helper-template-parameters-container details,
		.quick-redirect-template-parameters-container details {
			margin-top: 5px;
			margin-bottom: 5px;
			background-color: var(--background-color-interactive, #eaecf0);
			border: 1px solid var(--border-color-muted, #c8ccd1);
			padding: 5px;
		}
		#redirect-helper-no-templates-message { margin-top: 5px; font-style: italic; }
		#quick-redirect-summary-layout {
			margin-top: 15px;
			border-top: 1px solid var(--border-color-base, gray);
			padding-top: 10px;
		}
		#quick-redirect-submit-layout { margin-top: 10px; display: flex; justify-content: space-between; align-items: center; }
		.quick-redirect-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; justify-content: flex-end; }
		.quick-redirect-left-actions { display: flex; gap: 8px; align-items: center; }
		.quick-redirect-inline-watch { margin-bottom: 0 !important; margin-right: 5px; }
		.quick-redirect-inline-sync { margin-bottom: 0 !important; }
		.quick-redirect-inline-sync .oo-ui-layout .oo-ui-labelElement .oo-ui-fieldLayout .oo-ui-fieldLayout-align-left { width: auto }
		.quick-redirect-inline-sync .oo-ui-fieldLayout-header { width: auto !important; padding-right: 6px !important; padding-bottom: 0 !important; }
		.quick-redirect-inline-sync .oo-ui-fieldLayout-body { display: flex !important; align-items: center; width: 100% !important; }
		.quick-redirect-inline-sync .oo-ui-fieldLayout-field { width: auto !important; }
		.quick-redirect-inline-sync .oo-ui-labelElement-label { padding-top: 0px !important; margin-right: 0px !important; }
		.quick-redirect-inline-sync .oo-ui-dropdownInputWidget { width: 185px; }
	`,
	REQUIRED_MODULES: [
		'mediawiki.util',
		'oojs-ui-core',
		'oojs-ui-widgets',
		'oojs-ui-windows'
	],
	SCRIPT_MESSAGE: ' // [[User:Addonian1123/QuickRedirect|QuickRedirect]]'
};

const Utils = {
	async fetchRedirectTemplates(api) {
		try {
			const res = await api.get({
				action: "query", formatversion: "2", prop: "revisions", rvprop: "content", rvslots: "main",
				titles: "User:Addonian1123/QuickRedirect/categories.json|User:SuperGrey/gadgets/RedirectHelper/categories.json",
			});
			const pages = res.query.pages.filter(p => !p.missing);
			const bestPage = pages.find(p => p.title.includes('Addonian1123')) || pages[0];
			return JSON.parse(bestPage?.revisions?.[0]?.slots?.main?.content || "{}");
		} catch (e) {
			return {};
		}
	},
	async getPageContent(api, title) {
		const response = await api.get({
			action: 'query', formatversion: '2', prop: 'revisions',
			rvprop: 'content', rvslots: 'main', titles: title
		});
		return response.query.pages[0]?.revisions?.[0]?.slots?.main?.content?.trim() || '';
	}
};

class RedirectInputWidget extends OO.ui.TextInputWidget {
	constructor(config, pageTitleParsed) {
		super(config);
		OO.ui.mixin.LookupElement.call(this, config);
		this.api = new mw.Api({ userAgent: 'QuickRedirect/3.0.0' });
		this.pageTitleParsed = pageTitleParsed;
		this.allowMultiple = false;
	}
}
OO.mixinClass(RedirectInputWidget, OO.ui.mixin.LookupElement);

RedirectInputWidget.prototype.getLookupRequest = function () {
	const val = this.getValue();
	const terms = this.allowMultiple ? val.split(/[\n|,,、]/) : [val];
	const lastTerm = terms[terms.length - 1].trim();
	if (!lastTerm) {
		const deferred = $.Deferred();
		deferred.resolve({ query: { search: [] } });
		return deferred.promise();
	}
	return this.api.get({ action: 'query', formatversion: '2', list: 'search', srsearch: lastTerm, srnamespace: '*', srlimit: 10 });
};
RedirectInputWidget.prototype.getLookupCacheDataFromResponse = function (data) { return data?.query?.search ?? []; };
RedirectInputWidget.prototype.getLookupMenuOptionsFromData = function (data) {
	return data.map(result => {
		const title = mw.Title.newFromText(result.title)?.getPrefixedText() || result.title;
		return new OO.ui.MenuOptionWidget({ data: title, label: title });
	});
};
RedirectInputWidget.prototype.onLookupMenuItemChoose = function (item) {
	if (item) {
		if (this.allowMultiple) {
			const val = this.getValue();
			const terms = val.split(/[\n|,,、]/);
			terms.pop(); 
			terms.push(item.getData());
			this.setValue(terms.join(' | ') + (terms.length > 0 ? ' | ' : ''));
		} else {
			this.setValue(item.getData());
		}
	}
	this.closeLookupMenu();
};

class QuickRedirectEditor {
	constructor(config, pageState, buttonText, createdWatchMethod) {
		this.api = new mw.Api({ userAgent: 'QuickRedirect/3.0.0' });
		this.redirectTemplates = config.redirectTemplates;
		this.contentText = config.contentText;
		this.pageTitle = config.pageTitle;
		this.pageTitleParsed = config.pageTitleParsed;
		this.pageState = pageState;
		this.buttonText = buttonText;
		this.createdWatchMethod = createdWatchMethod;
		this.redirectRegex = /^\s*#.*?:?\s*\[\[\s*:?([^[\]{\vert{}}]+?)\s*(?:\\Vert{}[^[\]{|}]+?)?\]\]\s*/i;
		this.pageContent = '';
		this.direction = 'forward'; 
		this.templateEditorsInfo = [];
		this.checkTimeout = null;
		this.lastRequestId = 0;
	}

	async load() {
		this.editorBox = new OO.ui.PanelLayout({
			id: 'quick-redirect-box', padded: true, expanded: false, framed: true
		});

		this.initializeUIComponents();
		await this.loadSubmitElements();
		
		const inputContainer = $('<div>').addClass('input-container');
		inputContainer.append(this.redirectInputLayout.$element);
		inputContainer.append(this.warningContainer);
		if (this.directionLayout) inputContainer.append(this.directionLayout.$element);
		inputContainer.append(this.tagSelectLayout.$element, this.abbrInputLayout.$element, this.templateParametersEditor, this.summaryInputLayout.$element);
		
		this.editorBox.$element.append(inputContainer, this.submitLayout.$element);
		this.contentText.prepend(this.editorBox.$element[0]);

		if (this.pageState !== 'missing') await this.loadExistingData();
	}

	initializeUIComponents() {
		this.redirectInput = new RedirectInputWidget({ placeholder: msg.targetPlaceholderForward, required: true }, this.pageTitleParsed);
		this.redirectInput.allowMultiple = false;
		this.redirectInput.on('change', () => this.handleInputChange());
		this.redirectInputLayout = new OO.ui.FieldLayout(this.redirectInput, { label: msg.targetLabel, align: 'top' });

		this.warningLabel = new OO.ui.LabelWidget({ classes: ['quick-redirect-warning-text'] });
		this.fixRedirectButton = new OO.ui.ButtonWidget({
			label: msg.autoFix, flags: ['warning'], framed: true
		});
		this.fixRedirectButton.on('click', () => {
			if (this.finalTarget) {
				this.redirectInput.setValue(this.finalTarget);
				this.warningContainer.hide();
			}
		});

		this.warningContainer = $('<div>')
			.addClass('quick-redirect-warning-box')
			.append(this.warningLabel.$element, this.fixRedirectButton.$element)
			.hide();

		if (this.pageState !== 'missing') {
			this.directionSelect = new OO.ui.RadioSelectWidget({
				items: [
					new OO.ui.RadioOptionWidget({ data: 'forward', label: msg.forward }),
					new OO.ui.RadioOptionWidget({ data: 'reverse', label: msg.reverse })
				]
			});
			this.directionSelect.selectItemByData('forward');
			this.directionSelect.on('select', (item) => {
				if (item) {
					this.direction = item.getData();
					if (this.direction === 'reverse') {
						this.redirectInput.$input.attr('placeholder', msg.targetPlaceholderReverse);
						this.redirectInput.allowMultiple = true;
					} else {
						this.redirectInput.$input.attr('placeholder', msg.targetPlaceholderForward);
						this.redirectInput.allowMultiple = false;
					}
					this.updateSummary();
					this.handleInputChange();
				}
			});
			this.directionLayout = new OO.ui.FieldLayout(this.directionSelect, { label: msg.directionLabel, align: 'top' });
		}

		const tagOptions = Object.entries(this.redirectTemplates).map(([key, template]) => {
			const labelText = locText(template.label) || key;
			if (!template.redirect) return { data: key, label: labelText };
			const rwpText = i18n(' (具有可能性的重定向)', ' (具有可能性的重新導向)');
			const label = new OO.ui.HtmlSnippet(`<span>${labelText}</span><span style="font-style:italic; color:var(--color-subtle, #54595d);">${rwpText}</span>`);
			return { data: key, label: label };
		});

		this.tagSelect = new OO.ui.MenuTagMultiselectWidget({
			allowArbitrary: false,
			allowReordering: false,
			options: tagOptions
		});
		this.tagSelect.getMenu().filterMode = 'substring';

		this.tagSelectLayout = new OO.ui.FieldLayout(this.tagSelect, {
			label: msg.categoryLabel,
			classes: ['redirect-input-layout'],
			align: 'top'
		});

		this.abbrInput = new OO.ui.TextInputWidget({ placeholder: i18n('原名称(如全称)', '原名稱(如全稱)') });
		this.abbrInputLayout = new OO.ui.FieldLayout(this.abbrInput, { label: i18n('缩写原名称:', '縮寫原名稱:'), align: 'top' });
		this.abbrInputLayout.toggle(false);

		this.templateParametersEditor = document.createElement('details');
		this.templateParametersEditor.classList.add('redirect-helper-template-parameters-container');
		const summary = document.createElement('summary');
		summary.textContent = msg.templateParamsEmpty;
		this.templateParametersEditor.append(summary);

		for (const [tag, template] of Object.entries(this.redirectTemplates)) {
			const parameters = template.parameters ? Object.entries(template.parameters) : [];
			if (parameters.length === 0) continue;

			const details = document.createElement('details');
			details.style.display = 'none';

			const paramSummary = document.createElement('summary');
			paramSummary.textContent = locText(template.label) || tag;
			details.append(paramSummary);

			const editorInfo = {
				name: tag,
				details: details,
				parameters: []
			};

			for (const [param, config] of parameters) {
				const labelText = locText(config.label);
				const descText = locText(config.description);
				const defaultText = locText(config.default);
				const exampleText = locText(config.example);
				
				const typeTranslations = {
					'string': i18n('字符串', '字串'),
					'number': i18n('数字', '數字'),
					'boolean': i18n('布尔值', '布林值'),
					'page': i18n('页面', '頁面'),
					'user': i18n('用户', '使用者'),
					'date': i18n('日期', '日期')
				};
				const typeText = typeTranslations[config.type] || config.type || i18n('未知', '未知');

				const input = new OO.ui.TextInputWidget({
					placeholder: defaultText?.toString(),
					required: config.required
				});

				input.on('change', () => {
					this.updateSummary();
					if(this.submitButton) this.submitButton.setLabel(msg.save);
				});

				let labelHtml = `<b>${param}</b>`;
				if (labelText && param.toLowerCase() !== labelText.toLowerCase()) {
					labelHtml += ` (${labelText})`;
				}
				if (descText) {
					labelHtml += ` - ${descText}`;
				}
				labelHtml += ` <span style="color:var(--color-subtle, #54595d); font-size:0.9em;">(${i18n('类型', '類型')}: ${typeText})</span>`;
				if (config.suggested) labelHtml += i18n(' <span style="color:#00af89; font-size:0.9em;">(建议)</span>', ' <span style="color:#00af89; font-size:0.9em;">(建議)</span>');
				if (exampleText) labelHtml += ` <span style="color:var(--color-subtle, #54595d); font-size:0.9em;">(${i18n('例', '例')}: "${exampleText}")</span>`;

				const layout = new OO.ui.FieldLayout(input, {
					label: new OO.ui.HtmlSnippet(labelHtml),
					align: 'top'
				});

				details.append(layout.$element[0]);
				editorInfo.parameters.push({
					name: param,
					aliases: config.aliases,
					editor: input
				});
			}
			this.templateParametersEditor.append(details);
			this.templateEditorsInfo.push(editorInfo);
		}

		const noTemplatesMessage = document.createElement('div');
		noTemplatesMessage.id = 'redirect-helper-no-templates-message';
		noTemplatesMessage.textContent = msg.noTemplateParams;
		this.templateParametersEditor.append(noTemplatesMessage);

		this.tagSelect.on('change', (items) => {
			const values = items.map(item => typeof item === 'string' ? item : item.getData());
			const sortedValues = [...values].sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));

			if (values.join(';') !== sortedValues.join(';')) {
				const lastItem = items.at(-1);
				this.tagSelect.reorder(lastItem, sortedValues.indexOf(typeof lastItem === 'string' ? lastItem : lastItem.getData()));
			}

			this.updateSummary();
			if(this.submitButton) this.submitButton.setLabel(msg.save);

			this.abbrInputLayout.toggle(values.includes('捷徑重定向'));

			for (const info of this.templateEditorsInfo) {
				info.details.style.display = 'none';
			}

			let count = 0;
			for (const tag of this.tagSelect.getValue()) {
				const editor = this.templateEditorsInfo.find(info => info.name === tag);
				if (editor) {
					editor.details.style.display = 'block';
					count++;
				}
			}

			this.templateParametersEditor.querySelector('summary').textContent =
				`${msg.templateParameters1}(${count > 0 ? msg.templateParameters2 + count + msg.templateParameters3 : msg.templateParametersNoTemplate})`;
			this.templateParametersEditor.querySelector('#redirect-helper-no-templates-message').style.display =
				count > 0 ? 'none' : 'block';
				
			this.handleInputChange();
		});

		this.summaryInput = new OO.ui.ComboBoxInputWidget({
			options: [{ data: msg.summaryCleanup }, { data: msg.summaryFixDouble }, { data: msg.summaryFixLoop }, { data: msg.summaryRemoveBad }]
		});
		this.summaryInputLayout = new OO.ui.FieldLayout(this.summaryInput, { id: 'quick-redirect-summary-layout', label: msg.summaryLabel, align: 'top' });
	}

	handleInputChange() {
		const val = this.redirectInput.getValue().trim();
		this.submitButton?.setDisabled(val.length === 0);
		this.updateSummary();

		clearTimeout(this.checkTimeout);
		if (val.length > 0) {
			this.checkTimeout = setTimeout(() => this.checkStatusAndWarnings(val), 400);
		} else {
			this.warningContainer.hide();
		}
	}

	async checkStatusAndWarnings(targetRaw) {
		const requestId = ++this.lastRequestId;
		const targets = targetRaw.split(/[\n|,,、]/).map(s => s.trim()).filter(s => s);
		
		if (targets.length === 0) {
			this.warningContainer.hide();
			return;
		}

		let warnings = [];
		let canAutoFix = false;
		let hasBlockingError = false;
		this.finalTarget = null;

		if (targets.length > 5) {
			const userGroups = mw.config.get('wgUserGroups') || [];
			const isAutoconfirmed = userGroups.some(group => ['autoconfirmed', 'confirmed'].includes(group));
			if (!isAutoconfirmed) {
				warnings.push(msg.batchLimitError5);
				hasBlockingError = true;
			}
		}

		if (targets.length > 10) {
			const userGroups = mw.config.get('wgUserGroups') || [];
			const isExtendedconfirmed = userGroups.some(group => ['extendedconfirmed'].includes(group));
			if (!isExtendedconfirmed) {
				warnings.push(msg.batchLimitError10);
				hasBlockingError = true;
			}
		}

		if (targets.length > 100) {
			const userGroups = mw.config.get('wgUserGroups') || [];
			const isSysop = userGroups.some(group => ['sysop', 'interfaceadmin'].includes(group));
			if (!isSysop) {
				warnings.push(msg.batchLimitError100);
				hasBlockingError = true;
			}
		}

		if (targets.length > 500) {
			warnings.push(msg.batchLimitError500);
			hasBlockingError = true;
		}

		// 測跨名字空間重定向
		for (const t of targets) {
			const parsedTarget = mw.Title.newFromText(t);
			if (!parsedTarget) continue;

			const sourceMwTitle = this.direction === 'forward' ? this.pageTitleParsed : parsedTarget;
			const targetMwTitle = this.direction === 'forward' ? parsedTarget : this.pageTitleParsed;

			if (sourceMwTitle && targetMwTitle && sourceMwTitle.getNamespaceId() !== targetMwTitle.getNamespaceId()) {
				const getNsName = (titleObj) => {
					const nsId = titleObj.getNamespaceId();
					if (nsId === 0) return i18n('(主空间)', '(主空間)');
					return mw.config.get('wgFormattedNamespaces')[nsId] || `Namespace ${nsId}`;
				};
				warnings.push(msg.crossNamespaceWarning.replace('$1', getNsName(sourceMwTitle)).replace('$2', getNsName(targetMwTitle)));
				break; 
			}
		}

		// 檢測頁面狀態(雙重重定向/覆蓋非重定向內容/變更現有重定向)
		try {
			if (this.direction === 'forward' && targets.length === 1) {
				const res = await this.api.get({
					action: 'query', formatversion: '2', titles: targets[0], redirects: 1
				});
				if (requestId !== this.lastRequestId) return; 

				const redirects = res.query.redirects || [];
				if (redirects.length > 0) {
					this.finalTarget = redirects[0].to + (redirects[0].tofragment ? `#${redirects[0].tofragment}` : '');
					warnings.push(msg.doubleRedirectWarning.replace('$1', this.finalTarget));
					canAutoFix = true;
				}
			} else if (this.direction === 'reverse' && targets.length > 0) {
				const res = await this.api.get({
					action: 'query', formatversion: '2', prop: 'info', titles: targets.join('|')
				});
				if (requestId !== this.lastRequestId) return; 

				const pages = res.query.pages || [];
				for (const page of pages) {
					if (page.missing) continue;
					if (page.redirect) {
						warnings.push(msg.targetIsRedirectWarning.replace('$1', page.title));
					} else {
						warnings.push(msg.targetHasContentWarning.replace('$1', page.title));
					}
				}
			}
		} catch (e) {
			console.error('Error checking redirect warnings:', e);
		}

		if (requestId !== this.lastRequestId) return;

		if (warnings.length > 0) {
			this.warningLabel.setLabel(warnings.join('\n'));
			this.fixRedirectButton.toggle(canAutoFix);
			this.warningContainer.css('display', 'flex');
		} else {
			this.warningContainer.hide();
		}

		this.submitButton?.setDisabled(hasBlockingError);
	}

	updateSummary() {
		const targetRaw = this.redirectInput.getValue();
		const targets = targetRaw.split(/[\n|,,、]/).map(s => s.trim()).filter(s => s);
		
		let targetText = targets[0] || '';
		if (targets.length > 1) {
			targetText = `[[${targets[0]}]] ` + i18n(`等${targets.length}个页面`, `等${targets.length}個頁面`);
		} else if (targets.length === 1) {
			targetText = `[[${targets[0]}]]`;
		}

		const placeholderText = this.direction === 'reverse' 
			? i18n(`将${targetText}重定向至[[${this.pageTitle}]]`, `將${targetText}重定向至[[${this.pageTitle}]]`, `將${targetText}重新導向至[[${this.pageTitle}]]`)
			: i18n(`重定向到${targetText}`, `重定向到${targetText}`, `重新導向到${targetText}`);
			
		this.summaryInput.$input.attr('placeholder', placeholderText);
	}

	handleCancel() {
		this.editorBox.$element[0].remove();
		
		if (this.pageState === 'missing' || this.pageState === 'redirect') {
			const btn = new OO.ui.ButtonWidget({ id: 'create-redirect-button', label: this.buttonText, icon: 'articleRedirect', flags: ['progressive'] });
			btn.on('click', () => { btn.$element[0].remove(); this.load(); });
			this.contentText.prepend(btn.$element[0]);
		} else {
			if (document.getElementById('quick-redirect')) return;
			const portletId = mw.config.get('skin') === 'minerva' ? 'p-tb' : 'p-cactions';
			const portlet = mw.util.addPortletLink(portletId, '#', this.buttonText, 'quick-redirect');
			if (portlet) {
				portlet.addEventListener('click', (e) => {
					e.preventDefault();
					this.load();
					window.scrollTo({ top: 0, behavior: 'smooth' });
					portlet.remove();
				});
			}
		}
	}

	async loadSubmitElements() {
		this.submitButton = new OO.ui.ButtonWidget({ label: msg.save, disabled: true, flags: ['progressive', 'primary'] });
		const cancelBtn = new OO.ui.ButtonWidget({ label: msg.cancel });
		
		this.submitButton.on('click', () => this.handleSubmit());
		cancelBtn.on('click', () => this.handleCancel());
		
		this.watchCheckbox = new OO.ui.CheckboxInputWidget({ selected: false });
		this.watchLayout = new OO.ui.FieldLayout(this.watchCheckbox, { label: msg.watchPage, align: 'inline' });
		this.watchLayout.$element.addClass('quick-redirect-inline-watch');
		
		this.syncDropdown = new OO.ui.DropdownInputWidget({
			options: [
				{ data: 'never', label: msg.syncNever },
				{ data: 'always', label: msg.syncAlways },
				{ data: 'if_exists', label: msg.syncIfExists },
				{ data: 'if_missing', label: msg.syncIfMissing }
			],
			value: 'never'
		});
		this.syncLayout = new OO.ui.FieldLayout(this.syncDropdown, { label: msg.syncTalkPage, align: 'left' });
		this.syncLayout.$element.addClass('quick-redirect-inline-sync');
		
		const mainActions = new OO.ui.HorizontalLayout({ 
			items: [this.watchLayout, this.syncLayout, this.submitButton, cancelBtn] 
		});
		mainActions.$element.addClass('quick-redirect-actions');
		
		this.submitLayout = new OO.ui.HorizontalLayout({ id: 'quick-redirect-submit-layout' });
		
		const userGroups = mw.config.get('wgUserGroups') || [];
		const isSysOp = userGroups.includes('sysop');
		
		this.removeButton = new OO.ui.ButtonWidget({ 
			label: isSysOp ? i18n('清除', '清除') : msg.removeRedirect, 
			flags: ['destructive'], 
			icon: 'trash' 
		});
		this.removeButton.on('click', () => this.handleRemove());
		this.removeButton.toggle(false);
		
		let leftActions = this.removeButton;
		
		if (isSysOp) {
			this.deleteButton = new OO.ui.ButtonWidget({ 
				label: getSysMsg('delete', i18n('速删', '速刪')), 
				flags: ['destructive'], 
				icon: 'trash' 
			});
			this.deleteButton.on('click', () => {
				window.location.href = mw.util.getUrl(this.pageTitle, { action: 'delete' });
			});
			this.deleteButton.toggle(false);
			leftActions = new OO.ui.HorizontalLayout({
				items: [this.removeButton, this.deleteButton],
				classes: ['quick-redirect-left-actions']
			});
		}
		
		this.submitLayout.addItems([leftActions, mainActions]);
	}

	async handleRemove() {
		const confirmDialog = new OO.ui.MessageDialog();
		const windowManager = new OO.ui.WindowManager();
		$('body').append(windowManager.$element);
		windowManager.addWindows([confirmDialog]);

		windowManager.openWindow(confirmDialog, {
			title: msg.removeConfirmTitle,
			message: msg.removeConfirmMessage,
			actions: [
				{ action: 'accept', label: i18n('确定', '確定'), flags: ['destructive'] },
				{ action: 'reject', label: msg.cancel, flags: 'safe' }
			]
		}).closed.then(async (data) => {
			if (data && data.action === 'accept') {
				this.removeButton.setDisabled(true);
				
				let newContent = this.pageContent.replace(this.redirectRegex, '');
				newContent = newContent.replace(/{{\s*(?:Redirect category shell|重定向分类壳|重定向分類殼|重定向模板简介|重定向模板簡介)[\s\S]*?}}\s*/gi, '');
				Object.keys(this.redirectTemplates).forEach(tag => {
					const aliases = this.redirectTemplates[tag].aliases || [];
					const names = [tag, ...aliases].map(s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
					const namePattern = names.join('|');
					const regex = new RegExp(`{{\\s*(?:[Tt]emplate:|模板:)?(?:${namePattern})\\s*(?:\\|[^}]*)?}}\\s*`, 'gi');
					newContent = newContent.replace(regex, '');
				});
				newContent = newContent.trim();
				
				const watchChoice = this.watchCheckbox.isSelected() ? 'watch' : 'preferences';
				const success = await this.editOrCreate(
					this.pageTitle, 
					newContent, 
					msg.summaryRemoveRedirect + CONFIG.SCRIPT_MESSAGE,
					watchChoice
				);

				if (success) {
					mw.notify(msg.success, { type: 'success' });
					setTimeout(() => window.location.reload(), 1000);
				} else {
					this.removeButton.setDisabled(false);
				}
			}
			windowManager.destroy();
		});
	}

	async editOrCreate(title, content, summary, watchlist) {
		try {
			return await this.api.edit(title, () => ({ text: content, summary }), { watchlist });
		} catch (error) {
			if (error === 'nocreate-missing') {
				return await this.api.create(title, { summary, watchlist }, content);
			}
			mw.notify(`${msg.error}: ${error?.error?.info ?? error}`, { type: 'error' });
			return null;
		}
	}

	async handleSubmit() {
		this.submitButton.setDisabled(true).setLabel(msg.checking);
		const targetRaw = this.redirectInput.getValue().trim();
		const targetStrings = targetRaw.split(/[\n|,,、]/).map(s => s.trim()).filter(s => s);
		
		if (targetStrings.length === 0) {
			mw.notify(msg.missingTarget, { type: 'error' });
			this.submitButton.setDisabled(false).setLabel(msg.save);
			return;
		}

		if (this.direction === 'forward' && targetStrings.length > 1) {
			mw.notify(msg.forwardMultipleError, { type: 'error' });
			this.submitButton.setDisabled(false).setLabel(msg.save);
			return;
		}

		if (targetStrings.length > 500) {
			mw.notify(msg.batchLimitError500), { type: 'error' };
			this.submitButton.setDisabled(false).setLabel(msg.save);
			return;
		} else {
			if (targetStrings.length > 100) {
				const userGroups = mw.config.get('wgUserGroups') || [];
				const isSysop = userGroups.some(group => ['sysop', 'interfaceadmin'].includes(group));
				if (!isSysop) {
					mw.notify(msg.batchLimitError100), { type: 'error' };
					this.submitButton.setDisabled(false).setLabel(msg.save);
					return;
			} else {
				if (targetStrings.length > 10) {
					const userGroups = mw.config.get('wgUserGroups') || [];
					const isExtendedconfirmed = userGroups.some(group => ['extendedconfirmed'].includes(group));
					if (!isExtendedconfirmed) {
						mw.notify(msg.batchLimitError10), { type: 'error' };
						this.submitButton.setDisabled(false).setLabel(msg.save);
						return;
					} else {
						if (targetStrings.length > 5) {
							const userGroups = mw.config.get('wgUserGroups') || [];
							const isAutoconfirmed = userGroups.some(group => ['autoconfirmed', 'confirmed'].includes(group));
							if (!isAutoconfirmed) {
								mw.notify(msg.batchLimitError5), { type: 'error' };
								this.submitButton.setDisabled(false).setLabel(msg.save);
								return;
							}
						}
					}
				}
			}
		}
			
		}
		this.submitButton.setLabel(msg.processing);
		
		const tags = this.tagSelect.getValue();
		const templateOutput = tags.map(tag => {
			let paramStr = '';
			const editorInfo = this.templateEditorsInfo.find(info => info.name === tag);
			if (editorInfo) {
				editorInfo.parameters.forEach(param => {
					const val = param.editor.getValue().trim();
					if (val) paramStr += `|${param.name}=${val}`;
				});
			}
			if (tag === '捷徑重定向' && !paramStr) {
				const abbrVal = this.abbrInput.getValue().trim();
				if (abbrVal) paramStr = `|1=${abbrVal}`;
			}
			return `{{${tag}${paramStr}}}`;
		}).join('\n');
		
		const placeholder = this.summaryInput.$input.attr('placeholder') || '';
		const summary = (this.summaryInput.getValue() || placeholder) + CONFIG.SCRIPT_MESSAGE;
		
		const watchChoice = this.watchCheckbox.isSelected() ? 'watch' : 'preferences';
		const syncChoice = this.syncDropdown.getValue();

		let successCount = 0;

		for (const t of targetStrings) {
			const parsedTarget = mw.Title.newFromText(t);
			if (!parsedTarget) {
				mw.notify(msg.invalidTarget.replace('$1', t), { type: 'error' });
				continue;
			}

			let editTitle, redirectTargetTitle;
			if (this.direction === 'reverse') {
				editTitle = parsedTarget.getPrefixedText();
				redirectTargetTitle = this.pageTitleParsed.getPrefixedText();
			} else {
				editTitle = this.pageTitleParsed.getPrefixedText();
				redirectTargetTitle = parsedTarget.getPrefixedText();
			}

			if (editTitle === redirectTargetTitle) {
				mw.notify(msg.selfRedirect, { type: 'error' });
				continue;
			}

			const output = `#REDIRECT [[${redirectTargetTitle}]]\n${tags.length > 0 ? `{{Redirect category shell|\n${templateOutput}\n}}` : ''}`;
			
			if (await this.editOrCreate(editTitle, output, summary, watchChoice)) {
				successCount++;
				
				if (syncChoice !== 'never') {
					const editMwTitle = mw.Title.newFromText(editTitle);
					const targetMwTitle = mw.Title.newFromText(redirectTargetTitle);
					
					if (editMwTitle && targetMwTitle && !editMwTitle.isTalkPage() && editMwTitle.canHaveTalkPage() && targetMwTitle.canHaveTalkPage()) {
						const editTalk = editMwTitle.getTalkPage().getPrefixedText();
						const targetTalk = targetMwTitle.getTalkPage().getPrefixedText();
						
						if (editTalk !== targetTalk) {
							const talkInfo = await this.api.get({ action: 'query', formatversion: '2', titles: editTalk });
							const talkMissing = !!talkInfo.query.pages[0].missing;
							
							let shouldSync = false;
							if (syncChoice === 'always') shouldSync = true;
							else if (syncChoice === 'if_exists' && !talkMissing) shouldSync = true;
							else if (syncChoice === 'if_missing' && talkMissing) shouldSync = true;
							
							if (shouldSync) {
								const talkOutput = `#REDIRECT [[${targetTalk}]]`;
								await this.editOrCreate(editTalk, talkOutput, summary, watchChoice);
							}
						}
					}
				}
			}
		}

		if (successCount > 0) {
			mw.notify(msg.success, { type: 'success' });
			setTimeout(() => window.location.reload(), 1000);
		} else {
			this.submitButton.setDisabled(false).setLabel(msg.save);
		}
	}

	async detectExistingTags() {
		let contentToParse = this.pageContent;
		contentToParse = contentToParse.replace(/{{\s*(?:Redirect category shell|重定向分类壳|重定向分類殼|重定向模板简介|重定向模板簡介|This is a redirect)\s*\|([\s\S]*?)}}/gi, '$1');
		const regex = /{{\s*(?:[Tt]emplate:|模板:)?([^|{}]+)((?:\|[^}]+)*)}}/gi;
		let m;
		const extracted = [];
		const paramMap = {}; 
		while ((m = regex.exec(contentToParse)) !== null) {
			const tName = m[1].trim();
			const tParam = m[2] ? m[2].trim() : null;
			const tNameLower = tName.toLowerCase();
			if (!tNameLower.includes('redirect category shell') && 
				!tNameLower.includes('this is a redirect') && 
				!tNameLower.includes('重定向模板簡介') && 
				!tNameLower.includes('重定向分类壳') &&
				!tNameLower.includes('重定向模板简介') &&
				!tNameLower.includes('重定向分類殼')) {
				const prefixed = `Template:${tName}`;
				extracted.push(prefixed);
				if (tParam) paramMap[prefixed] = tParam;
			}
		}
		
		if (extracted.length === 0) return;
		const uniqueTitles = [...new Set(extracted)].slice(0, 50);
		
		try {
			const res = await this.api.get({
				action: 'query', formatversion: '2', titles: uniqueTitles.join('|'), redirects: 1
			});
			
			const titleMap = {};
			uniqueTitles.forEach(t => { titleMap[t] = t; });
			(res.query.normalized || []).forEach(n => { if (titleMap[n.from]) titleMap[n.from] = n.to; });
			(res.query.redirects || []).forEach(r => {
				Object.keys(titleMap).forEach(k => { if (titleMap[k] === r.from) titleMap[k] = r.to; });
			});
			
			const validTags = [];
			const resolvedParams = {};
			
			const norm = s => s.toLowerCase().replace(/_/g, ' ').trim();
			
			Object.keys(titleMap).forEach(k => {
				const resolvedName = titleMap[k].replace(/^Template:/i, '').trim();
				const originalName = k.replace(/^Template:/i, '').trim(); 
				
				const normResolved = norm(resolvedName);
				const normOriginal = norm(originalName);

				let matchedKey = Object.keys(this.redirectTemplates).find(key => {
					const normKey = norm(key);
					if (normKey === normResolved || normKey === normOriginal) return true;
					
					const template = this.redirectTemplates[key];
					if (template.aliases && Array.isArray(template.aliases)) {
						if (template.aliases.some(alias => norm(alias) === normResolved || norm(alias) === normOriginal)) return true;
					}
					return false;
				});

				if (matchedKey) {
					if (!validTags.includes(matchedKey)) validTags.push(matchedKey);
					
					if (paramMap[k]) {
						resolvedParams[matchedKey] = {};
						const pList = paramMap[k].split('|').slice(1);
						let autoIdx = 1;
						pList.forEach(p => {
							const eqIdx = p.indexOf('=');
							if (eqIdx !== -1) {
								resolvedParams[matchedKey][p.substring(0, eqIdx).trim()] = p.substring(eqIdx + 1).trim();
							} else {
								resolvedParams[matchedKey][autoIdx++] = p.trim();
							}
						});
					}
				}
			});
			
			if (validTags.length > 0) {
				this.tagSelect.setValue(validTags); 
				
				validTags.forEach(tag => {
					const editorInfo = this.templateEditorsInfo.find(info => info.name === tag);
					const params = resolvedParams[tag];
					if (params) {
						if (tag === '捷徑重定向' && (params['1'] || params['abbr'])) {
							this.abbrInput.setValue(params['1'] || params['abbr']);
						}
						if (editorInfo) {
							editorInfo.parameters.forEach(paramConfig => {
								let val = params[paramConfig.name];
								if (val === undefined && paramConfig.aliases) {
									for (const alias of paramConfig.aliases) {
										if (params[alias] !== undefined) { val = params[alias]; break; }
									}
								}
								if (val !== undefined) paramConfig.editor.setValue(val);
							});
						}
					}
				});
			}
		} catch (e) {
			console.error('Error resolving template redirects:', e);
		}
	}
	
	async loadExistingData() {
			this.pageContent = await Utils.getPageContent(this.api, this.pageTitle);
			const oldTarget = this.redirectRegex.exec(this.pageContent)?.[1];
			if (oldTarget) this.redirectInput.setValue(oldTarget.replaceAll('_', ' '));
			
			await this.detectExistingTags();
			
			const hasRedirectTemplate = /{{\s*(?:Redirect category shell|重定向分类壳|重定向分類殼|重定向模板简介|重定向模板簡介)[\s\S]*?}}/gi.test(this.pageContent) || 
										Object.keys(this.redirectTemplates).some(tag => {
											const aliases = this.redirectTemplates[tag].aliases || [];
											const names = [tag, ...aliases].map(s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
											const namePattern = names.join('|');
											return new RegExp(`{{\\s*(?:[Tt]emplate:|模板:)?(?:${namePattern})\\s*(?:\\|[^}]*)?}}`, 'gi').test(this.pageContent);
										});
										
			if (this.pageState === 'redirect' || oldTarget || hasRedirectTemplate) {
				this.removeButton.toggle(true);
				this.removeButton.setDisabled(false);
				if (this.deleteButton) {
					this.deleteButton.toggle(true);
					this.deleteButton.setDisabled(false);
				}
			} else {
				this.removeButton.toggle(true);
				this.removeButton.setDisabled(true);
				if (this.deleteButton) {
					this.deleteButton.toggle(true);
					this.deleteButton.setDisabled(true);
				}
			}
	
		this.updateSummary();
	}
}

class QuickRedirectInit {
	constructor() { this.api = new mw.Api({ userAgent: 'QuickRedirect/3.0.0' }); }
	
	async run() {
		if (!['view', 'edit'].includes(mw.config.get('wgAction')) || !mw.config.get('wgIsProbablyEditable')) return;
		mw.util.addCSS(CONFIG.STYLES);
		
		const templates = await Utils.fetchRedirectTemplates(this.api);
		const title = mw.config.get('wgPageName');
		const parsed = mw.Title.newFromText(title);
		const info = await this.api.get({ action: 'query', formatversion: '2', prop: 'info', titles: title });

		const exists = !info.query.pages[0].missing;
		const isRedirect = info.query.pages[0].redirect || mw.config.get('wgIsRedirect');
		
		let pageState = 'missing';
		let buttonText = msg.createRedirect;
		if (exists) {
			if (isRedirect) {
				pageState = 'redirect';
				buttonText = msg.editRedirect;
			} else {
				pageState = 'normal';
				buttonText = msg.changeToRedirect;
			}
		}
		
		const config = { redirectTemplates: templates, contentText: document.querySelector('#mw-content-text'), pageTitle: title, pageTitleParsed: parsed };
		
		if (pageState === 'missing') {
			const btn = new OO.ui.ButtonWidget({ id: 'create-redirect-button', label: buttonText, icon: 'articleRedirect', flags: ['progressive'] });
			btn.on('click', () => { btn.$element[0].remove(); new QuickRedirectEditor(config, pageState, buttonText, 'preferences').load(); });
			config.contentText.prepend(btn.$element[0]);
		} else if (pageState === 'redirect') {
			new QuickRedirectEditor(config, pageState, buttonText, 'preferences').load();
		} else {
			const editor = new QuickRedirectEditor(config, pageState, buttonText, 'preferences');
			const portletId = mw.config.get('skin') === 'minerva' ? 'p-tb' : 'p-cactions';
			const portlet = mw.util.addPortletLink(portletId, '#', buttonText, 'quick-redirect');
			if (portlet) {
				portlet.addEventListener('click', (e) => {
					e.preventDefault();
					editor.load();
					window.scrollTo({ top: 0, behavior: 'smooth' });
					portlet.remove();
				});
			}
		}
	}
}

mw.loader.using(CONFIG.REQUIRED_MODULES, () => { new QuickRedirectInit().run(); });
// </nowiki>