Jump to content

User:BilledMammal/MovePlus.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
//movePlus
//<nowiki>
var movePlus = {
	numberOfMoves: 0,
	multiMove: false,
	destinations: [],
	parsedDate: undefined,
	pages: [],
	templateIndex: -1,
	moveQueue: [],
	editQueue: [],
	linkAdjustWarning: '\t<span style="color: red;"><b>Warning:</b></span> This will automatically update pages, retargeting all links from the current value to the value you specify. You take full responsibility for any action you perform using this script.'
};
window.movePlus = movePlus;

$.when(
	mw.loader.using([ 'mediawiki.api', 'ext.gadget.morebits', 'ext.gadget.libExtraUtil' ]),
	$.ready
).then(function() {
	if (document.getElementById("requestedmovetag") !== null && Morebits.pageNameNorm.indexOf("alk:") !== -1 && mw.config.get('wgCategories').includes('Requested moves') && !document.getElementById("wikiPreview") && mw.config.get('wgDiffOldId') == null) {
		document.getElementById("requestedmovetag").innerHTML = "<button id='movePlusClose'>Close</button><button id='movePlusRelist'>Relist</button><button id='movePlusNotify'>Notify WikiProjects</button><span id='movePlusRelistOptions' style='display:none'><input id='movePlusRelistComment' placeholder='Relisting comment' oninput='if(this.value.length>20){this.size=this.value.length} else{this.size=20}'/><br><button id='movePlusConfirm'>Confirm relist</button><button id='movePlusCancel'>Cancel relist</button></span>";
		$('#movePlusClose').click(movePlus.callback);
		$('#movePlusRelist').click(movePlus.confirmRelist);
		$('#movePlusConfirm').click(movePlus.relist);
		$('#movePlusCancel').click(movePlus.cancelRelist);
		$('#movePlusNotify').click(movePlus.notify);
	}

	var portletLink = mw.util.addPortletLink("p-cactions", "#movePlusMove", "Move\+",
		"ca-movepages", "Move pages (expanded options)");

	$( portletLink ).click(movePlus.displayWindowMove);

});

movePlus.confirmRelist = function movePlusConfirmRelist(e) {
	if (e) e.preventDefault();
	document.getElementById("movePlusRelistOptions").style.display = "inline";
	document.getElementById("movePlusClose").style.display = "none";
	document.getElementById("movePlusRelist").style.display = "none";
	document.getElementById("movePlusNotify").style.display = "none";
};

movePlus.cancelRelist = function movePlusCancelRelist(e) {
	if (e) e.preventDefault();
	document.getElementById("movePlusRelistOptions").style.display = "none";
	document.getElementById("movePlusClose").style.display = "inline";
	document.getElementById("movePlusRelist").style.display = "inline";
	document.getElementById("movePlusNotify").style.display = "inline";
};

movePlus.advert = ' using [[User:BilledMammal/Move+|Move+]]';

movePlus.preEvaluate = async function() {
	try {
		const talkPageContent = await loadTalkPage();
		return extractTemplateData(talkPageContent);
	} catch (error) {
		console.error('Error during pre-evaluation:', error);
	}
};

async function loadTalkPage() {

	var title_obj = mw.Title.newFromText(Morebits.pageNameNorm);

	const talkpage = new Morebits.wiki.page(title_obj.getTalkPage().toText(), 'Retrive move proposals.');
	return new Promise((resolve, reject) => {
		talkpage.load(function(talkpage) {
			if (talkpage.exists()) {
				resolve(talkpage.getPageText());
			} else {
				reject('Page does not exist');
			}
		}, reject);
	});
}

function extractTemplateData(text) {
	const templatesOnPage = extraJs.parseTemplates(text, false);
	let templateData = {};

	templatesOnPage.forEach(template => {
		if (template.name.toLowerCase() === "requested move/dated") {
			templateData = { ...templateData, ...parseRequestedMoveTemplate(template) };
		}
	});

	return templateData;
}

function parseRequestedMoveTemplate(template) {

	const data = {
		moves: [],
		multiMove: template.parameters.some(param => param.name === "multiple")
	};

	const pairs = {};

	template.parameters.forEach(param => {
		const match = param.name.toString().match(/^(current|new)?(\d+)$/);
		if (match) {
			const type = match[1] ? match[1] : "new";
			const index = match[2];

			if (!pairs[index]) {
				pairs[index] = {};
			}

			if (!pairs[index][type] || param.value != "") {
				pairs[index][type] = param.value;
			}
		}
	});

	if(!pairs[1]["current"]) {
		let title_obj = mw.Title.newFromText(Morebits.pageNameNorm);
		pairs[1]["current"] = title_obj.getSubjectPage().toText();
	}

	Object.keys(pairs).forEach(index => {
		const pair = pairs[index];
		if (pair.current && pair.new) {
			data.moves.push({current: pair.current, destination: pair.new});
		}
	});

	return data;
}

movePlus.callback = async function movePlusCallback(e) {
	e.preventDefault(e);

	try {
		const evaluationData = await movePlus.preEvaluate();
		if (evaluationData) {
			movePlus.displayWindowClose(evaluationData);
		} else {
			throw new Error("Failed to retrieve necessary data for processing.");
		}
	} catch (error) {
		console.error('Error during callback execution:', error);
	}
};

movePlus.displayWindowClose = function movePlusDisplayWindowClose(data) {

	let checkboxStates = {};

	movePlus.Window = new Morebits.simpleWindow(600, 450);
	movePlus.Window.setTitle( "Close requested move" );
	movePlus.Window.setScriptName('Move+');
	movePlus.Window.addFooterLink('RM Closing instruction', 'WP:RMCI');
	movePlus.Window.addFooterLink('Script documentation', 'User:BilledMammal/Move+');
	movePlus.Window.addFooterLink('Give feedback', 'User talk:BilledMammal/Move+');

	var form = new Morebits.quickForm(function(e) {
		movePlus.evaluate(e, data);
	});
	setupForm();

	function setupForm() {
		var resultContainer = form.append({
			type: 'div',
			style: 'display: flex; flex-direction: row;  gap: 10px;'
		});

		var resultField = setupResultOptions(resultContainer);
		setupCustomResult(resultField);
		var movedOptionsField = setupMoveOptions(resultContainer);
		setupCustomTitles();
		setupClosingComment(form);
	}

	function setupResultOptions(container) {
		var resultField = container.append({
			type: 'field',
			label: 'Result',
			style: 'flex: 1;'
		});

		resultField.append({
			type: 'radio',
			name: 'result',
			required: true,
			list: [
				{
					label: 'Moved',
					value: 'moved',
					event: function() { updateResultOptions('moved'); }
				},
				{
					label: 'Not moved',
					value: 'not moved',
					event: function() { updateResultOptions('not moved'); }
				},
				{
					label: 'No consensus',
					value: 'no consensus',
					event: function() { updateResultOptions('no consensus'); }
				},
				{
					label: 'Custom',
					value: 'custom',
					event: function() { updateResultOptions('custom'); }
				}
			]
		});

		return resultField;
	}

	function updateResultOptions(result) {
		const customResultDisplay = document.getElementsByName('customResult')[0];
		const movedOptionsDisplay = document.getElementsByName('movedOptionsField')[0];
		const customTitlesDisplay = document.getElementById('customTitles');
		const checkboxes = document.querySelectorAll('input[name="movedOptionsInputs"]');

		// Default settings
		customResultDisplay.style.display = 'none';
		customResultDisplay.required = false;
		movedOptionsDisplay.style.display = 'none';
		customTitlesDisplay.style.display = 'none';

		// Unset move options
		if (result != 'moved') {
			checkboxes.forEach(checkbox => {
				if (checkbox.checked) {
					checkboxStates[checkbox.value] = true;
					checkbox.checked = false;
					const event = new Event('change');
					checkbox.dispatchEvent(event);
				} else {
					checkboxStates[checkbox.value] = false;
				}
			});
		}

		switch (result) {
			case 'moved':
				movedOptionsDisplay.style.display = 'block';
				// Reset move options
				checkboxes.forEach(checkbox => {
					if (checkboxStates[checkbox.value]) {
						checkbox.checked = true;
						const event = new Event('change');
						checkbox.dispatchEvent(event);
					}
				});
				break;
			case 'custom':
				customResultDisplay.style.display = 'inline';
				customResultDisplay.required = true;
				break;
		}
	}

	function setupMoveOptions(container) {
		let originalClosingComment = '';

		const movedOptionsField = container.append({
			type: 'field',
			label: 'Specify move type',
			style: 'display: none; flex: 1;',
			name: 'movedOptionsField'
		});

		movedOptionsField.append({
			type: 'checkbox',
			name: 'movedOptionsInputs',
			list: [
				{
					label: 'Close as uncontested',
					value: 'moved-uncontested',
					tooltip: 'We treat discussions where no objections have been raised, but community support has also not been demonstrated, as uncontested technical requests.',
					event: function(event) {
						const closingComment = document.getElementsByName('closingComment')[0];
						if (event.target.checked) {
							originalClosingComment = closingComment ? closingComment.value : '';
							closingComment.value = 'Moved as an [[WP:RMNOMIN|uncontested request with minimal participation]]. If there is any objection within a reasonable time frame, please ask me to reopen the discussion; if I am not available, please ask at the [[WP:RM/TR#Requests to revert undiscussed moves|technical requests]] page.';
						} else {
							closingComment.value = originalClosingComment;
						}
					}
				},
				{
					label: 'Specify different titles',
					value: 'moved-different-title',
					tooltip: 'If no title was origionally proposed, or if there is a consensus to move to a title other than that which was origionally proposed.',
					event: function() {
						if (event.target.checked) {
							customTitles.style.display = 'block';
						} else {
							customTitles.style.display = 'none';
						}
					}
				}
			]
		});

		return movedOptionsField;
	}

	function setupCustomResult(resultField) {
		resultField.append({
			type: 'input',
			name: 'customResult',
			style: 'display: none;'
		});
	}

	function setupCustomTitles() {
		const customTitles = form.append({
			type: 'field',
			label: 'Specify titles',
			id: 'customTitles',
			name: 'customTitles',
			style: 'display: none;'
		});

		data.moves.forEach((pair, index) => {
			const titleField = customTitles.append({
				type: 'div',
				className: 'customTitleInput',
				style: 'display: flex; align-items: center; margin-bottom: 5px;'
			});

			titleField.append({
				type: 'div',
				style: 'flex: 0 1 47.5%; text-align: left;',
				label: pair.current
			});

			titleField.append({
				type: 'div',
				style: 'flex: 0 1 5%; text-align: center;',
				label: '→'
			});

			const inputDiv = titleField.append({
				type: 'div',
				style: 'flex: 1;'
			});

			inputDiv.append({
				type: 'input',
				name: pair.current,
				value: pair.destination,
				style: 'width: 95%; text-align: left;'
			});
		});

		const toggleButton = customTitles.append({
			type: 'button',
			label: 'Hide titles',
			event: function(event) {
				const titleInputs = document.querySelectorAll('.customTitleInput');
				const button = event.target;
				titleInputs.forEach(input => {
					if (input.style.display === 'none' || input.style.display === '') {
						input.style.display = 'flex';
						button.value = 'Hide titles';
					} else {
						input.style.display = 'none';
						button.value = 'Show titles';
					}
				});
			}
		});

	}

	function setupClosingComment(form) {
		const closingCommentField = form.append({
			type: 'field',
			label: 'Closing comment'
		});

		closingCommentField.append({
			type: 'textarea',
			name: 'closingComment'
		});
	}

	form.append({ type: 'submit', label: 'Submit' });

	var formResult = form.render();
	movePlus.Window.setContent(formResult);
	movePlus.Window.display();

};

movePlus.displayWindowMove = function movePlusDisplayWindowMove() {

	var title_obj = mw.Title.newFromText(Morebits.pageNameNorm);
	movePlus.title = title_obj.getSubjectPage().toText();
	movePlus.displayWindowInit();
	movePlus.displayWindowAction();

}

movePlus.displayWindowInit = function movePlusDisplayWindowInit() {

	movePlus.Window = new Morebits.simpleWindow(600, 450);
	movePlus.Window.setScriptName('Move+');
	movePlus.Window.addFooterLink('Moving instructions', 'Wikipedia:Moving a page');
	movePlus.Window.addFooterLink('Script documentation', 'User:BilledMammal/Move+');
	movePlus.Window.addFooterLink('Give feedback', 'User talk:BilledMammal/Move+');
}


movePlus.displayWindowAction = function movePlusDisplayWindowAction() {


	var moveData = [{
		current: movePlus.title,
		target: ''
	}];

	var retargetData = [{
		current: '',
		target: ''
	}];

	var config = {
		move: {
			data: moveData,
			reason: '',
			label: 'Specify moves',
			reasonLabel: 'Move reason',
			reasonName: 'moveReason',
			actionLimit: 100,
			buttonLabel: 'Add move',
			title: 'Move pages',
			information: ''
		},
		retarget: {
			data: retargetData,
			reason: '',
			label: 'Specify link retargets',
			reasonLabel: 'Link retarget reason',
			reasonName: 'retargetReason',
			actionLimit: 2,
			buttonLabel: 'Add retarget',
			title: 'Retarget links',
			information: movePlus.linkAdjustWarning
		}
	};

	function updateForm(action) {

		movePlus.Window.setTitle(config[action].title);

		function updateActionDataFromForm() {
			config[action].data = [];
			config[action].reason = document.querySelector(`textarea[name="${config[action].reasonName}"]`).value;
			var currentInputs = document.querySelectorAll('input[name="curr"]');
			var targetInputs = document.querySelectorAll('input[name="dest"]');

			currentInputs.forEach((input, index) => {
				config[action].data.push({
					current: input.value,
					target: targetInputs[index].value
				});
			});
		};

		var form = new Morebits.quickForm(function(e) {
			e.preventDefault();
			movePlus.params = Morebits.quickForm.getInputData(e.target);

			var currentPages = [];
			var targetPages = [];

			$('input[name="curr"]').each(function(index) {
				var currentPage = $(this).val();
				var targetPage = $('input[name="dest"]').eq(index).val();

				if (currentPage && targetPage) {
					currentPages.push(currentPage);
					targetPages.push(targetPage);
				}
			});

			if (action == 'move') {
				movePlus.movePages(currentPages, targetPages, movePlus.params.moveReason, false);
			}
			if (action == 'retarget') {
				movePlus.retargetLinks(currentPages, targetPages, movePlus.params.retargetReason);
			}

		});

		movePlus.appendOptions(form, action, updateForm, updateActionDataFromForm);

		var actionsContainer = form.append({
			type: 'field',
			label: config[action].label,
			id: 'actionList',
			name: 'actionList'
		});

		config[action].data.forEach((data, index) => {
			const titleField = actionsContainer.append({
				type: 'div',
				className: 'titleInput',
				style: 'display: flex; align-items: center; margin-bottom: 5px;'
			});

			const currentDiv = titleField.append({
				type: 'div',
				style: 'flex: 0 1 47.5%; text-align: left;'
			});

			currentDiv.append({
				type: 'input',
				name: 'curr',
				value: data.current,
				placeholder: 'Current page',
				required: true,
				style: 'width: 95%; text-align: left;'
			});

			titleField.append({
				type: 'div',
				style: 'flex: 0 1 5%; text-align: center;',
				label: '→'
			});

			const destDiv = titleField.append({
				type: 'div',
				style: 'flex: 0 1 47.5%; text-align: left;'
			});

			destDiv.append({
				type: 'input',
				name: 'dest',
				value: data.target,
				required: true,
				placeholder: 'Target page',
				style: 'width: 95%; text-align: left;'
			});

			titleField.append({
				type: 'button',
				label: 'Remove',
				disabled: config[action].data.length < 2 ? true : false,
				event: function() {
					updateActionDataFromForm();
					config[action].data.splice(index, 1);
					updateForm(action);
				}
			});
		});

		actionsContainer.append({
			type: 'button',
			label: config[action].buttonLabel,
			disabled: config[action].data.length < config[action].actionLimit ? false : true,
			event: function() {
				updateActionDataFromForm();
				config[action].data.push({ current: '', target: '' });
				updateForm(action);
			}
		});

		movePlus.appendReason(form, config[action].reasonLabel, config[action].reasonName, config[action].reason);

		form.append({
			type: 'div',
			label: config[action].information,
			style: 'margin-left: 15px; margin-right: 15px;'
		});

		form.append({ type: 'submit', label: 'Submit' });

		var formResult = form.render();
		movePlus.Window.setContent(formResult);

		movePlus.appendReasonAlert(config[action].reasonLabel, config[action].reasonName);

		movePlus.Window.display();

	}

	updateForm('move');
}

movePlus.retargetLinks = async function movePlusRetargetLinks(currentLinks, targetLinks, reason) {

	var form = new Morebits.quickForm();

	var actionContainer = form.append({
		type: 'field',
		label: 'Retargeting'
	});

	actionContainer.append({
		type: 'div',
		className: 'movePlusProgressBox',
		label: ''
	});

	var multiple = currentLinks[1] ? true : false;

	var config = {
		currTarget: targetLinks[0],
		destTarget: multiple ? targetLinks[1] : ''
	}

	var formResult = form.render();
	movePlus.Window.setContent(formResult);
	movePlus.Window.display();

	const progressBox = document.querySelector('.movePlusProgressBox');

	movePlus.linkEditSummary = reason + ': ';

	await movePlus.correctLinks(currentLinks[0], multiple ? currentLinks[1] : '', config, progressBox);

	progressBox.innerText = 'Done.'
	setTimeout(function(){ movePlus.Window.close(); }, 1250);

}

movePlus.appendOptions = function movePlusAppendOptions(form, action, updateForm, updateActionDataFromForm) {


	var optionsContainer = form.append({
		type: 'field',
		label: 'Options',
		style: 'display: flex; flex-direction: row;'
	});

	optionsContainer.append({
		type: 'button',
		label: 'Move pages',
		name: 'movePages',
		disabled: action == 'move' ? true : false,
		event: function() {
			updateActionDataFromForm();
			updateForm('move');
		}
	});

	optionsContainer.append({
		type: 'button',
		label: 'Retarget page links',
		name: 'retargetLinks',
		disabled: action == 'retarget' ? true : false,
		event: function() {
			updateActionDataFromForm();
			updateForm('retarget');
		}
	});

}

movePlus.appendReason = function movePlusAppendReason(form, label, name, value) {
	var moveReason = form.append({
		type: 'field',
		label: label
	});

	moveReason.append({
		type: 'textarea',
		name: name,
		value: value,
		required: true
	});

	moveReason.append({
		type: 'div',
		name: name + 'Alert',
		style: 'display: block',
		label: ''
	});
}

movePlus.appendReasonAlert = function appendReasonAlert(label, name) {

	const reasonAlert = document.getElementsByName(name + 'Alert')[0];
	$(`textarea[name="${name}"]`).on('input', function() {
		if (this.value.length > 400) {
			reasonAlert.innerHTML = `<span style="color: red;"><b>Warning:</b></span> ${label} contains ${this.value.length} characters. It may be truncated in the edit summary.`;
			reasonAlert.style.display = 'block';
		} else {
			reasonAlert.style.display = 'none';
		}
	});

}

movePlus.evaluate = function(e, data) {
	var form = e.target;
	movePlus.params = Morebits.quickForm.getInputData(form);

	Morebits.simpleWindow.setButtonsEnabled(false);
	Morebits.status.init(form);

	var title_obj = mw.Title.newFromText(Morebits.pageNameNorm);
	movePlus.title = title_obj.getSubjectPage().toText();
	movePlus.talktitle = title_obj.getTalkPage().toText();

	var result = movePlus.params.result;
	if(result == 'custom'){
		result = movePlus.params.customResult;
	}

	var closingComment = movePlus.params.closingComment;
	if(closingComment != ""){
		closingComment = ' ' + closingComment;
		closingComment = closingComment.replace(/\|/g, "{{!}}");
		closingComment = closingComment.replace(/=/g, "{{=}}");
	}

	if (movePlus.params.movedOptionsInputs.includes('moved-different-title')) {
		data.moves.forEach(function(pair, index) {
			if (movePlus.params[pair.current]) {
				data.moves[index].destination = movePlus.params[pair.current];
			}
		});
	}

	var talkpage = new Morebits.wiki.page(movePlus.talktitle, 'Closing move.');
	talkpage.load(function(talkpage) {
		var text = talkpage.getPageText();

		var templatesOnPage = extraJs.parseTemplates(text,false);
		var oldMovesPresent = [];
		var template;
		for (var i = 0; i < templatesOnPage.length; i++) {
			if (templatesOnPage[i].name.toLowerCase() == "old moves" || templatesOnPage[i].name.toLowerCase() == "old move") {
				oldMovesPresent.push(templatesOnPage[i]);
			} else if (templatesOnPage[i].name.toLowerCase() == "requested move/dated") {
				template = templatesOnPage[i];
			}
		}

		var templateFound = false;
		var numberOfMoves = 0;
		var line;
		var templateIndex = -1;
		var parsedDate;
		var rmSection;
		var nextSection = false;
		var textToFind = text.split('\n');
		for (var i = 0; i < textToFind.length; i++) {
			line = textToFind[i];
			if(templateFound == false){
				if(/{{[Rr]equested move\/dated/.test(line)){
					templateFound = true;
					templateIndex = i;
				}
			} else if(templateFound == true){
				if (/ \(UTC\)/.test(line)){
					line = line.substring(line.indexOf("This is a contested technical request"));
					parsedDate = line.match(/, ([0-9]{1,2} (January|February|March|April|May|June|July|August|September|October|November|December) [0-9]{4}) \(UTC\)/)[1];
					break;
				} else if(/→/.test(line)){
					numberOfMoves++;
				}
			}
		}


		for (var i = templateIndex; i >= 0; i--) {
			line = textToFind[i];
			if (line.match(/^(==)[^=].+\1/)) {
				rmSection = line.match(/^(==)[^=](.+)\1/)[2].trim();
				break;
			}
		}

		for (var i = templateIndex+1; i < textToFind.length; i++) {
			line = textToFind[i];
			if (line.match(/^(==)[^=].+\1/)) {
				nextSection = true;
				var escapedLine = line.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
				var regex = new RegExp('(' + escapedLine + ')(?![\s\S]*(' + escapedLine + '))', 'm');
				text = text.replace(regex, '{{subst:RM bottom}}\n\n' + line);
				break;
			}
		}

		var userGroupText = "";
		if(Morebits.userIsInGroup('sysop')){
			userGroupText = "";
		} else if(Morebits.userIsInGroup('extendedmover')){
			userGroupText = "|pmc=y";
		} else{
			userGroupText = "|nac=y";
		}
		text = text.replace(/{{[Rr]equested move\/dated\|.*\n?[^\[]*}}/, "{{subst:RM top|'''" + result + ".'''" + closingComment + userGroupText +"}}");

		if (!nextSection) {
			text += '\n{{subst:RM bottom}}';
		}

		var multiMove = data.multiMove;
		var moveSectionPlain = rmSection;

		var date = parsedDate;
		var from = '';

		var destination = data.moves[0].destination
		if(destination == "?"){
			destination = "";
		}

		var link = 'Special:Permalink/' + talkpage.getCurrentID() + '#' + moveSectionPlain;

		var archives = text.match(/{{[Aa]rchives/);
		if(archives == null){
			archives = text.match(/{{[Aa]rchive box/);
			if(archives == null){
				archives = text.match(/{{[Aa]rchivebox/);
				if(archives == null){
					archives = text.match(/==.*==/);
				}
			}
		}

		if (oldMovesPresent.length == 0) {
			if(result == "moved"){
				from = '|from=' + movePlus.title;
			}
			text = text.replace(archives[0], '{{old move'+ '|date=' + date + from + '|destination=' + destination + '|result=' + result + '|link=' + link +'}}\n\n' + archives[0]);
		} else if (oldMovesPresent.length == 1) {
			var isValidFormat = false;
			var isListFormat = false;
			var numOldMoves = 0;
			for (var i = 0; i < oldMovesPresent[0].parameters.length; i++) {
				var parameterName = oldMovesPresent[0].parameters[i].name;
				parameterName = parameterName.toString();
				if (parameterName == "list") {
					isListFormat = true;
					break;
				} else if (parameterName == "result1") {
					isValidFormat = true;
					numOldMoves++;
				} else if (parameterName.includes("result")) {
					numOldMoves++;
				}
			}

			if (isValidFormat && !isListFormat) {
				var oldMovesText = oldMovesPresent[0].wikitext;
				numOldMoves++;
				if(result == "moved"){
					from = '|from' + numOldMoves + '=' + movePlus.title;
				}
				var newTextToAdd = '|date' + numOldMoves + '=' + date + from + '|destination' + numOldMoves + '=' + destination + '|result' + numOldMoves + '=' + result + '|link' + numOldMoves + '=' + link + '}}';
				oldMovesText = oldMovesText.substring(0, oldMovesText.length-2) + newTextToAdd;
				text = text.replace(oldMovesPresent[0].wikitext, oldMovesText);
			} else if (isListFormat) {
				if(result == "moved"){
					from = '|from=' + movePlus.title;
				}
				text = text.replace(archives[0], '{{old move'+ '|date=' + date + from + '|destination=' + destination + '|result=' + result + '|link=' + link +'}}\n\n' + archives[0]);
			} else {
				var oldMovesText = '{{' + oldMovesPresent[0].name;
				for (var i = 0; i < oldMovesPresent[0].parameters.length; i++) {
					if (oldMovesPresent[0].parameters[i].name == "date") {
						oldMovesText += '|date1=' + oldMovesPresent[0].parameters[i].value;
					} else if (oldMovesPresent[0].parameters[i].name == "from") {
						oldMovesText += '|name1=' + oldMovesPresent[0].parameters[i].value;
					} else if (oldMovesPresent[0].parameters[i].name == "destination") {
						oldMovesText += '|destination1=' + oldMovesPresent[0].parameters[i].value;
					} else if (oldMovesPresent[0].parameters[i].name == "result") {
						oldMovesText += '|result1=' + oldMovesPresent[0].parameters[i].value;
					} else if (oldMovesPresent[0].parameters[i].name == "link") {
						oldMovesText += '|link1=' + oldMovesPresent[0].parameters[i].value;
					} else {
						oldMovesText += oldMovesPresent[0].parameters[i].wikitext;
					}
				}
				if(result == "moved"){
					from = '|from2=' + movePlus.title;
				}
				var newTextToAdd = '|date2=' + date + from + '|destination2=' + destination + '|result2=' + result + '|link2=' + link + '}}';
				oldMovesText += newTextToAdd;
				text = text.replace(oldMovesPresent[0].wikitext, oldMovesText);
			}

		} else {
			var oldMovesText = '{{Old moves';
			var numOldMoves = 1;
			for (var i = 0; i < oldMovesPresent.length; i++) {
				for (var j = 0; j < oldMovesPresent[i].parameters.length; j++) {
					if (oldMovesPresent[i].parameters[j].name == "date") {
						oldMovesText += '|date' + numOldMoves + '=' + oldMovesPresent[i].parameters[j].value;
					} else if (oldMovesPresent[i].parameters[j].name == "from") {
						oldMovesText += '|name' + numOldMoves + '=' + oldMovesPresent[i].parameters[j].value;
					} else if (oldMovesPresent[i].parameters[j].name == "destination") {
						oldMovesText += '|destination' + numOldMoves + '=' + oldMovesPresent[i].parameters[j].value;
					} else if (oldMovesPresent[i].parameters[j].name == "result") {
						oldMovesText += '|result' + numOldMoves + '=' + oldMovesPresent[i].parameters[j].value;
					} else if (oldMovesPresent[i].parameters[j].name == "link") {
						oldMovesText += '|link' + numOldMoves + '=' + oldMovesPresent[i].parameters[j].value;
					} else {
						oldMovesText += oldMovesPresent[i].parameters[j].wikitext;
					}
				}
				numOldMoves++;
			}
			if(result == "moved"){
				from = '|from' + numOldMoves + '=' + movePlus.title;
			}
			var newTextToAdd = '|date' + numOldMoves + '=' + date + from + '|destination' + numOldMoves + '=' + destination + '|result' + numOldMoves + '=' + result + '|link' + numOldMoves + '=' + link + '}}';
			oldMovesText += newTextToAdd;
			text = text.replace(oldMovesPresent[0].wikitext, oldMovesText);
			for (var i = 1; i < oldMovesPresent.length; i++) {
				text = text.replace(oldMovesPresent[i].wikitext, "");
			}
		}

		talkpage.setPageText(text);
		talkpage.setEditSummary('Closing requested move; ' + result + movePlus.advert);
		talkpage.save(Morebits.status.actionCompleted('Moved closed.'));

		if(multiMove == true){
			var otherDestinations = []
			var otherPages = []

			for(var m=1; m<data.moves.length; m++) {
				otherDestinations.push(data.moves[m].destination);
				otherPages.push(data.moves[m].current);
			}

			var pagesLeft = otherPages.length;
			for(var j=0; j<otherPages.length; j++){
				var otherTitle_obj = mw.Title.newFromText(otherPages[j]);
				movePlus.otherTalktitle = otherTitle_obj.getTalkPage().toText();
				var otherPage = new Morebits.wiki.page(movePlus.otherTalktitle, 'Adding {{old move}} to ' + movePlus.otherTalktitle + '.');
				otherPage.load(function(otherPage) {
					var otherText = otherPage.getPageText();

					var templatesOnOtherPage = extraJs.parseTemplates(otherText,false);
					var otherOldMovesPresent = [];
					for (var i = 0; i < templatesOnOtherPage.length; i++) {
						if (templatesOnOtherPage[i].name.toLowerCase() == "old moves" || templatesOnOtherPage[i].name.toLowerCase() == "old move") {
							otherOldMovesPresent.push(templatesOnOtherPage[i]);
						}
					}

					var title = mw.Title.newFromText(otherPage.getPageName()).getSubjectPage().toText();
					var OMcurr = otherPages[otherPages.indexOf(title)];
					var OMdest = otherDestinations[otherPages.indexOf(title)];
					var otherFrom = '';
					if(OMdest == "?"){
						OMdest == "";
					}
					var otherDestination = OMdest;
					var otherArchives = otherText.match(/{{[Aa]rchives/);
					if(otherArchives == null){
						otherArchives = otherText.match(/{{[Aa]rchive box/);
						if(otherArchives == null){
							otherArchives = otherText.match(/{{[Aa]rchivebox/);
							if(otherArchives == null){
								otherArchives = otherText.match(/==.*==/);
								if(otherArchives == null){
									//Otherwise, skip it
									otherArchives = ''
								}
							}
						}
					}

					if (otherOldMovesPresent.length == 0) {
						if(result == "moved"){
							otherFrom = '|from=' + OMcurr;
						}
						otherText = otherText.replace(otherArchives[0], '{{old move'+ '|date=' + date + otherFrom + '|destination=' + otherDestination + '|result=' + result + '|link=' + link +'}}\n\n' + otherArchives[0]);
					} else if (otherOldMovesPresent.length == 1) {
						var isValidFormat = false;
						var isListFormat = false;
						var numOldMoves = 0;
						for (var i = 0; i < otherOldMovesPresent[0].parameters.length; i++) {
							var parameterName = otherOldMovesPresent[0].parameters[i].name;
							parameterName = parameterName.toString();
							if (parameterName == "list") {
								isListFormat = true;
								break;
							} else if (parameterName == "result1") {
								isValidFormat = true;
								numOldMoves++;
							} else if (parameterName.includes("result")) {
								numOldMoves++;
							}
						}

						if (isValidFormat && !isListFormat) {
							var oldMovesText = otherOldMovesPresent[0].wikitext;
							numOldMoves++;
							if(result == "moved"){
								otherFrom = '|from' + numOldMoves + '=' + OMcurr;
							}
							var newTextToAdd = '|date' + numOldMoves + '=' + date + otherFrom + '|destination' + numOldMoves + '=' + otherDestination + '|result' + numOldMoves + '=' + result + '|link' + numOldMoves + '=' + link + '}}';
							oldMovesText = oldMovesText.substring(0, oldMovesText.length-2) + newTextToAdd;
							otherText = otherText.replace(otherOldMovesPresent[0].wikitext, oldMovesText);
						} else if (isListFormat) {
							if(result == "moved"){
								otherFrom = '|from=' + OMcurr;
							}
							otherText = otherText.replace(otherArchives[0], '{{old move'+ '|date=' + date + otherFrom + '|destination=' + otherDestination + '|result=' + result + '|link=' + link +'}}\n\n' + otherArchives[0]);
						} else {
							var oldMovesText = '{{' + otherOldMovesPresent[0].name;
							for (var i = 0; i < otherOldMovesPresent[0].parameters.length; i++) {
								if (otherOldMovesPresent[0].parameters[i].name == "date") {
									oldMovesText += '|date1=' + otherOldMovesPresent[0].parameters[i].value;
								} else if (otherOldMovesPresent[0].parameters[i].name == "from") {
									oldMovesText += '|name1=' + otherOldMovesPresent[0].parameters[i].value;
								} else if (otherOldMovesPresent[0].parameters[i].name == "destination") {
									oldMovesText += '|destination1=' + otherOldMovesPresent[0].parameters[i].value;
								} else if (otherOldMovesPresent[0].parameters[i].name == "result") {
									oldMovesText += '|result1=' + otherOldMovesPresent[0].parameters[i].value;
								} else if (otherOldMovesPresent[0].parameters[i].name == "link") {
									oldMovesText += '|link1=' + otherOldMovesPresent[0].parameters[i].value;
								} else {
									oldMovesText += otherOldMovesPresent[0].parameters[i].wikitext;
								}
							}
							if(result == "moved"){
								otherFrom = '|from2=' + OMcurr;
							}
							var newTextToAdd = '|date2=' + date + otherFrom + '|destination2=' + otherDestination + '|result2=' + result + '|link2=' + link + '}}';
							oldMovesText += newTextToAdd;
							otherText = otherText.replace(otherOldMovesPresent[0].wikitext, oldMovesText);
						}

					} else {
						var oldMovesText = '{{Old moves';
						var numOldMoves = 1;
						for (var i = 0; i < otherOldMovesPresent.length; i++) {
							for (var j = 0; j < otherOldMovesPresent[i].parameters.length; j++) {
								if (otherOldMovesPresent[i].parameters[j].name == "date") {
									oldMovesText += '|date' + numOldMoves + '=' + otherOldMovesPresent[i].parameters[j].value;
								} else if (otherOldMovesPresent[i].parameters[j].name == "from") {
									oldMovesText += '|name' + numOldMoves + '=' + otherOldMovesPresent[i].parameters[j].value;
								} else if (otherOldMovesPresent[i].parameters[j].name == "destination") {
									oldMovesText += '|destination' + numOldMoves + '=' + otherOldMovesPresent[i].parameters[j].value;
								} else if (otherOldMovesPresent[i].parameters[j].name == "result") {
									oldMovesText += '|result' + numOldMoves + '=' + otherOldMovesPresent[i].parameters[j].value;
								} else if (otherOldMovesPresent[i].parameters[j].name == "link") {
									oldMovesText += '|link' + numOldMoves + '=' + otherOldMovesPresent[i].parameters[j].value;
								} else {
									oldMovesText += otherOldMovesPresent[i].parameters[j].wikitext;
								}
							}
							numOldMoves++;
						}
						if(result == "moved"){
							otherFrom = '|from' + numOldMoves + '=' + OMcurr;
						}
						var newTextToAdd = '|date' + numOldMoves + '=' + date + otherFrom + '|destination' + numOldMoves + '=' + otherDestination + '|result' + numOldMoves + '=' + result + '|link' + numOldMoves + '=' + link + '}}';
						oldMovesText += newTextToAdd;
						otherText = otherText.replace(otherOldMovesPresent[0].wikitext, oldMovesText);
						for (var i = 1; i < otherOldMovesPresent.length; i++) {
							otherText = otherText.replace(otherOldMovesPresent[i].wikitext, "");
						}
					}

					otherPage.setPageText(otherText);
					otherPage.setEditSummary('Closing requested move; ' + result + movePlus.advert);
					otherPage.save(Morebits.status.actionCompleted('Moved closed.'));
					pagesLeft--;
				});
			}

			if(result == "moved"){
				var waitInterval = setInterval(function(){
					if(pagesLeft == 0){
						movePlus.movePages([movePlus.title].concat(otherPages),[destination].concat(otherDestinations),link);
						clearInterval(waitInterval);
					}
				}, 500);
			} else{
				setTimeout(function(){ location.reload() }, 2000);
			}
		} else if(result == "moved"){
			var emptyArray = [];
			movePlus.movePages([movePlus.title],[destination],link);
		} else{
			setTimeout(function(){ location.reload() }, 2000);
		}
	});
};

getTalkPageTitles = function getTalkPageTitles(curr, dest) {
	var currTitleObj = new mw.Title(curr);
	var destTitleObj = new mw.Title(dest);

	var currTalkPage = currTitleObj.getTalkPage().getPrefixedText();
	var destTalkPage = destTitleObj.getTalkPage().getPrefixedText();

	return {
		currTalk: currTalkPage,
		destTalk: destTalkPage
	};
}


getPageByTitle = function getPageByTitle(data, title) {
	return data.find(page => page.title === title);
}

movePlus.checkPage = async function movePlusCheckPage(curr, dest) {

	let talkPages = getTalkPageTitles(curr, dest);

	let query = {
		action: 'query',
		prop: 'info|revisions',
		inprop: 'protection',
		titles: `${curr} | ${talkPages.currTalk} | ${dest} | ${talkPages.destTalk}`,
		format: 'json',
		rvprop: 'ids'
	};

	let redirectsQuery = {
		...query,
		redirects: 1
	}

	let protection = {
		curr: {
			article: {}
		},
		dest: {
			article: {}
		},

		createLabel: function(type) {
			let protections = [];
			let data = this[type];
			let label = '';

			if (data.move) protections.push("move");
			if (data.create) protections.push("create");

			if (protections.length > 0) {
				const formattedProtection = protections.join(" and ");
				label = ` (${formattedProtection} protected)`;
			}
			//As only sysop's can overwrite pages with history we only warn sysops
			if (data.history && Morebits.userIsInGroup('sysop')) {
				label = label + ' <span style="color: red;">(<b>Warning:</b> Page has history)</span>';
			}
			return label;
		},

		checkProtection: function() {
			return this.curr.all || this.dest.all
		},

		checkSysopProtection: function() {
			return this.curr.admin || this.dest.admin
		},

		checkHistory: function() {
			return this.dest.history
		},

		checkRedirect: function() {
			return this.dest.redirect
		},

		checkTarget: function() {
			return this.curr.target
		},

		checkClosed: function() {
			return (this.curr.article.redirect && this.dest.article.target) || (this.dest.article.redirect && this.curr.article.target)
		}
	}

	function getRedirectByTitle(data, title) {
		if (data) {
			let redirect = data.find(redirect => redirect.from === title);
			return redirect ? redirect : [];
		}

		return [];
	}

	try {
		const pageResponse = await new Morebits.wiki.api(`Accessing information on about edit restrictions on ${dest} and ${curr}`, query).post();
		const currData = getPageByTitle(pageResponse.response.query.pages, curr)
		const currTalkData = getPageByTitle(pageResponse.response.query.pages, talkPages.currTalk)
		const destData = getPageByTitle(pageResponse.response.query.pages, dest)
		const destTalkData = getPageByTitle(pageResponse.response.query.pages, talkPages.destTalk)

		function checkProtection(data, admin = false) {
			return data.protection.some(protection => {
				if (admin) {
					return protection.level == "sysop";
				} else {
					return !(Morebits.userIsInGroup(protection.level) || Morebits.userIsSysop);
				}
			});
		}

		function checkHistory(data) {
			if (data.revisions) {
				return data.revisions[0].parentid === 0;
			}
			return true;
		}

		function checkMissing(data) {
			return data.missing
		}

		function processHistory(data, talkData) {

			protection.dest.history = !checkHistory(data) || !checkHistory(talkData);

		}

		async function processRedirects() {
			const redirectResponse = await new Morebits.wiki.api(`Accessing information about redirect status of ${curr} and ${dest}`, redirectsQuery).post();
			processRedirect(redirectResponse, dest, curr, protection.dest, protection.curr);
			processRedirect(redirectResponse, talkPages.destTalk, talkPages.currTalk, protection.dest, protection.curr);

			processRedirect(redirectResponse, curr, dest, protection.curr.article, protection.dest.article);
			processRedirect(redirectResponse, dest, curr, protection.dest.article, protection.curr.article);
		}

		function processRedirect(redirectData, origin, target, originStatus, targetStatus) {
			let data = getRedirectByTitle(redirectData.response.query.redirects, origin)

			originStatus.redirect = originStatus.redirect !== undefined ? originStatus.redirect : true;
			targetStatus.target = targetStatus.target !== undefined ? targetStatus.target : true;

			if (data.length == 0) {
				if (!checkMissing(getPageByTitle(redirectData.response.query.pages, origin))) {
					originStatus.history = true;
					originStatus.redirect = false;
					targetStatus.target = false;
				}
				return;
			}

			if (data.to != target) {
				targetStatus.target = false;
			}

		}

		protection.curr.all = checkProtection(currData) || checkProtection(currTalkData);
		protection.curr.admin = checkProtection(currData, true) || checkProtection(currTalkData, true);
		protection.dest.all = checkProtection(destData) || checkProtection(destTalkData);
		protection.dest.admin = checkProtection(destData, true) || checkProtection(destTalkData, true);

		protection.dest.history = !checkHistory(destData) || !checkHistory(destTalkData);
		await processRedirects();

		return protection;
	} catch (error) {
		console.error('Failed to fetch page details:', error);
		throw error;
	}
};

movePlus.movePages = function movePlusMovePages(currList, destList, link, closer = true){

	movePlus.numberToRemove = currList.length;
	movePlus.talktitle = mw.Title.newFromText(Morebits.pageNameNorm).getTalkPage().toText();
	var pageAndSection = link;

	var moveSummary, rmtrReason;

	var promises = [];
	var configurations = [];

	function sanitizeClassName(name) {
		return name.replace(/[^a-zA-Z0-9\-_]/g, '-');
	}

	if (closer) {
		if (movePlus.params.movedOptionsInputs.includes('moved-uncontested')) {
			moveSummary = 'Moved, as an [[WP:RMTR|uncontested technical request]], per [[' + pageAndSection + ']]';
			rmtrReason = 'Per lack of objection at [[' + pageAndSection + ']].';
		} else {
			moveSummary = 'Moved per [[' + pageAndSection + ']]';
			rmtrReason = 'Per consensus at [[' + pageAndSection + ']].';
		}
	} else {
		moveSummary = link;
		rmtrReason = link;
	}

	var form = new Morebits.quickForm();

	var movesContainer = form.append({
		type: 'field',
		label: 'Moves'
	});

	function addButtons(curr, dest, config) {
		const moveContainer = movesContainer.append({
			type: 'div',
			className: 'movePlusMovePagesRow' + sanitizeClassName(curr),
			style: 'display: flex; flex-direction: column; margin-bottom: 7px',
			label: ''
		});

		const rowContainer = moveContainer.append({
			type: 'div',
			className: 'movePlusMovePagesSubRow' + sanitizeClassName(curr),
			style: 'display: flex; flex-direction: row;',
			label: ''
		});

		const actionContainer = rowContainer.append({
			type: 'div',
			style: 'display: flex; flex-direction: column; flex: 75%; text-align: left;',
			className: 'moves'
		});

		const optionsContainer = rowContainer.append({
			type: 'div',
			style: 'display: flex; flex: 25%; text-align: left;',
			className: 'moveOptions' + sanitizeClassName(curr)
		});

		actionContainer.append({
			type: 'div',
			className: 'movePlusMovePagesLabel',
			label: config.label
		});

		actionContainer.append({
			type: 'div',
			className: 'movePlusProgressBox',
			name: sanitizeClassName(curr),
			label: '',
			style: 'margin-left: 15px'
		});

		const buttonsContainer = actionContainer.append({
			type: 'div',
			style: 'display: flex; flex-direction: row; margin-left: 15px'
		});

		const linksContainer = moveContainer.append({
			type: 'field',
			label: 'Specify new link targets',
			style: 'display: none',
			className: 'specifyLinks' + sanitizeClassName(curr),
			name: 'specifyLinks',
			id: 'specifyLinks'
		});

		addRedirectSpecification(linksContainer, curr, dest);
		addRedirectSpecification(linksContainer, dest, curr);

		linksContainer.append({
			type: 'div',
			label: movePlus.linkAdjustWarning
		});

		var isSysop = Morebits.userIsInGroup('sysop');
		var isMover = Morebits.userIsInGroup('extendedmover');

		if (!config.isProtected) {
			if (config.hasHistory) {
				if (isMover || isSysop) {
					addCheckboxes(optionsContainer, curr, isSysop, true, true, !config.isClosed);
					if (isSysop) {
						addButton(buttonsContainer, curr, dest, "moveOverPage", true);
					}
					addButton(buttonsContainer, curr, dest, 'roundRobin');
				} else {
					addButton(buttonsContainer, curr, dest, 'technicalRequest', config.isSysopProtected);
				}
			} else if (config.isRedirect && !config.isTarget) {
				if (isSysop || isMover) {
					addCheckboxes(optionsContainer, curr, true, true, true, false);
					addButton(buttonsContainer, curr, dest, "moveOverPage")
				} else {
					addButton(buttonsContainer, curr, dest, 'technicalRequest', config.isSysopProtected);
				}
			} else {
				addButton(buttonsContainer, curr, dest, "move");
				if (isSysop || isMover) {
					addCheckboxes(optionsContainer, curr, true, true, true, false);
				} else {
					addCheckboxes(optionsContainer, curr, false, false, true, false);
				}
			}
		} else {
			addButton(buttonsContainer, curr, dest, 'technicalRequest', config.isSysopProtected);
		}

	}

	function addRedirectSpecification(container, origin, target) {
		const titleField = container.append({
			type: 'div',
			className: 'titleInput',
			style: 'display: flex; align-items: center; margin-bottom: 5px;'
		});

		const currentDiv = titleField.append({
			type: 'div',
			style: 'flex: 0 1 47.5%; text-align: left;'
		});

		currentDiv.append({
			type: 'div',
			label: origin,
			style: 'width: 95%; text-align: left;'
		});

		titleField.append({
			type: 'div',
			style: 'flex: 0 1 5%; text-align: center;',
			label: '→'
		});

		const futDiv = titleField.append({
			type: 'div',
			style: 'flex: 0 1 47.5%; text-align: left;'
		});

		futDiv.append({
			type: 'input',
			id: 'specifyLinks' + sanitizeClassName(origin),
			value: target,
			placeholder: 'Future page',
			style: 'width: 95%; text-align: left;'
		});
	}

	function addCheckboxes(container, label, suppressRedirect, moveSubpages, moveTalkPage, correctLinks) {

		let options = [];

		if(correctLinks) {
			options.push({
				name: 'correctLinks' + sanitizeClassName(label),
				label: "Correct links",
				checked: false,
				event: function() {
					if (event.target.checked) {
						document.querySelector('.specifyLinks' + sanitizeClassName(label)).style.display = 'block';
					} else {
						document.querySelector('.specifyLinks' + sanitizeClassName(label)).style.display = 'none';
					}
				}
			});
		}

		if(suppressRedirect) {
			options.push({
				name: 'suppressRedirect' + sanitizeClassName(label),
				label: "Suppress redirect",
				checked: false
			});
		};

		if(moveSubpages) {
			options.push({
				name: 'moveSubpages' + sanitizeClassName(label),
				label: 'Move subpages',
				checked: true
			});
		};

		if(moveTalkPage) {
			options.push({
				name: 'moveTalkPage' + sanitizeClassName(label),
				label: 'Move talk page',
				checked: true
			});
		};

		container.append({
			type: 'checkbox',
			style: 'display: flex; flex-direction: column; align-items: left; margin-right: 10px;',
			list: options
		});

	}

	function addButton(container, curr, dest, type, admin = false) {
		let operation;
		let label;
		let id;
		let tooltip = "";
		let disabled = false;

		switch (type) {
			case "move":
				operation = async function(progressBox) {
					config = checkCheckboxes();
					await movePlus.movePage(this.name, this.extra, moveSummary, progressBox, config);
				};
				label = 'Move directly';
				id = 'moveDirectly';
				break;
			case "moveOverPage":
				operation = async function(progressBox) {
					config = checkCheckboxes();
					await movePlus.moveOverPage(this.name, this.extra, config.suppressRedirect, moveSummary, admin, progressBox, config.moveSubpages, config.moveTalkPage);
				};
				label = 'Move directly (o)';
				id = 'moveOverPage';
				break;
			case "roundRobin":
				operation = async function(progressBox) {
					config = checkCheckboxes();
					await movePlus.moveRoundRobin(this.name, this.extra, moveSummary, progressBox, config);
				};
				label = 'Move via Round Robin';
				id = 'roundRobin';
				break;
			case "technicalRequest":
				operation = async function(progressBox) {
					config = checkCheckboxes();
					await movePlus.submitRMTR(this.name, this.extra, admin, rmtrReason, progressBox);
				};
				label = 'Submit technical request';
				id = 'rmtr';
				break;
		}

		container.append({
			type: 'button',
			className: 'movePlusMovePages' + sanitizeClassName(curr),
			name: curr,
			extra: dest,
			label: label,
			tooltip: tooltip,
			disabled: disabled,
			id: id,
			event: async function() {
				const rowElements = document.querySelectorAll('.movePlusMovePages' + sanitizeClassName(curr));
				rowElements.forEach(element => {
					element.style.display = 'none';
				});

				const progressBox = document.querySelector('.movePlusProgressBox[name="' + sanitizeClassName(curr) + '"]');
				if (progressBox) {
					progressBox.textContent = 'In progress...';

					try {
						await operation.call(this, progressBox);
						progressBox.textContent = 'Completed!';
						setTimeout(() => {
							document.querySelector('.movePlusMovePagesRow' + sanitizeClassName(curr)).style.display = 'none';
							movePlus.numberToRemove--;
						}, 1000);
					} catch (error) {
						progressBox.textContent = 'Failed. Please implement manually and report this error to the script maintainer.';
					}
				}
			}
		});

		function checkCheckboxes() {
			const suppressRedirectElem = document.querySelector('input[name="suppressRedirect' + sanitizeClassName(curr) + '"]');
			const moveSubpagesElem = document.querySelector('input[name="moveSubpages' + sanitizeClassName(curr) + '"]');
			const moveTalkPageElem = document.querySelector('input[name="moveTalkPage' + sanitizeClassName(curr) + '"]');
			const correctLinksElem = document.querySelector('input[name="correctLinks' + sanitizeClassName(curr) + '"]');

			let suppressRedirect = suppressRedirectElem ? suppressRedirectElem.checked : false;
			let moveSubpages = moveSubpagesElem ? moveSubpagesElem.checked : true;
			let moveTalkPage = moveTalkPageElem ? moveTalkPageElem.checked : true;
			let correctLinks = correctLinksElem ? correctLinksElem.checked : false;

			let currTarget, destTarget;
			if (correctLinks) {
				currTarget = document.querySelector('#specifyLinks' + sanitizeClassName(curr)).value;
				destTarget = document.querySelector('#specifyLinks' + sanitizeClassName(dest)).value;
			}

			document.querySelector('.specifyLinks' + sanitizeClassName(curr)).style.display = 'none';
			document.querySelector('.moveOptions' + sanitizeClassName(curr)).style.display = 'none';

			return {
				suppressRedirect: suppressRedirect,
				moveSubpages: moveSubpages,
				moveTalkPage: moveTalkPage,
				correctLinks: correctLinks,
				currTarget: currTarget,
				destTarget: destTarget
			}
		}
	}

	for(let i=0; i<currList.length; i++){
		let promise = movePlus.checkPage(currList[i], destList[i]).then(data => {
			configurations[i] = {
				label: currList[i] + data.createLabel("curr") + ' → ' + destList[i] + data.createLabel("dest"),
				isProtected: data.checkProtection(),
				isSysopProtected: data.checkSysopProtection(),
				hasHistory: data.checkHistory(),
				isRedirect: data.checkRedirect(),
				isTarget: data.checkTarget(),
				isClosed: data.checkClosed()
			}
		});

		promises.push(promise)
	}

	function setupMulti() {

		var multiContainer = form.append({
			type: 'field',
			label: 'Multi-action',
			style: 'display: flex; flex-direction: row'
		});

		multiContainer.append({
			type: 'button',
			label: 'Move all',
			tooltip: 'Moves all pages. If there are multiple options it will move the page via round robin instead of overwriting history at the destination.',
			event: async function() {
				const rows = document.querySelectorAll('[class^="movePlusMovePagesRow"]');
				for (const row of rows) {
					const moveDirectlyButton = row.querySelector('div span input#moveDirectly');
					const roundRobinButton = row.querySelector('div span input#roundRobin');
					const technicalRequestButton = row.querySelector('div span input#rmtr');

					if (moveDirectlyButton) {
						await moveDirectlyButton.click();
					} else if (roundRobinButton) {
						await roundRobinButton.click();
					} else if (technicalRequestButton) {
						await technicalRequestButton.click();
					}
				}
			}
		});

		var multiOptionContainer = multiContainer.append({
			type: 'div',
			style: "display: flex; flex-direction: row; justify-content: flex-end; width: 85%;"
		});

		multiOptionContainer.append({
			type: 'button',
			label: 'Suppress all redirects',
			event: function() {
				const buttons = document.querySelectorAll('input[name^="suppressRedirect"]');
				const button = event.target;
				const currentLabel = button.value;
				buttons.forEach(button => {
					button.checked = currentLabel === 'Suppress all redirects';
				});
				button.value = currentLabel === 'Suppress all redirects' ? 'Suppress no redirects' : 'Suppress all redirects';
			}
		});

		multiOptionContainer.append({
			type: 'button',
			label: 'Move no subpages',
			event: function() {
				const buttons = document.querySelectorAll('input[name^="moveSubpages"]');
				const button = event.target;
				const currentLabel = button.value;
				buttons.forEach(button => {
					button.checked = currentLabel === 'Move all subpages';
				});
				button.value = currentLabel === 'Move all subpages' ? 'Move no subpages' : 'Move all subpages';
			}
		});

		multiOptionContainer.append({
			type: 'button',
			label: 'Move no talk pages',
			event: function() {
				const buttons = document.querySelectorAll('input[name^="moveTalkPage"]');
				const button = event.target;
				const currentLabel = button.value;
				buttons.forEach(button => {
					button.checked = currentLabel === 'Move all talk pages';
				});
				button.value = currentLabel === 'Move all talk pages' ? 'Move no talk pages' : 'Move all talk pages';
			}
		});
	}

	Promise.all(promises).then(() => {
		configurations.forEach((config, index) => {
			addButtons(currList[index], destList[index], config);
		});

		setupMulti();

		var formResult = form.render();
		movePlus.Window.setContent(formResult);
		movePlus.Window.display();

		var moveInterval = setInterval(function(){
			if(movePlus.numberToRemove == 0){
				movePlus.Window.close();
				clearInterval(moveInterval);
				setTimeout(function(){ location.reload() }, 750);
			}
		}, 500);

	});

};

movePlus.moveOverPage = function movePlusMoveOverPage(curr, dest, suppressRedirect, editSummary, warn, progressBox, subpages = true, talkpage = true) {

	let destSplit = movePlus.splitPageName(dest);

	if (warn) {
		if (!confirm('Warning: You are about to delete a page with history. Do you want to proceed?')) {
			progressBox.innerText = 'Move cancelled';
			throw new Error('Move cancelled.');
		}
	}

	return new Promise((resolve, reject) => {
		const moveTask = async () => {
			progressBox.innerText = `Moving ${curr} to ${dest}.`;

			const url = 'https://en.wikipedia.org/w/index.php?title=Special:MovePage&action=submit';

			const formData = new FormData();
			formData.append('wpNewTitleNs', destSplit.namespace);
			formData.append('wpNewTitleMain', destSplit.pageName);
			formData.append('wpReasonList', 'other');
			formData.append('wpReason', editSummary + movePlus.advert);
			formData.append('wpWatch', '0');
			formData.append('wpLeaveRedirect', suppressRedirect ? '0' : '1');
			formData.append('wpMovetalk', talkpage ? '1' : '0');
			formData.append('wpMovesubpages', subpages ? '1' : '0');
			formData.append('wpDeleteAndMove', '1');
			formData.append('wpMove', 'Move page');
			formData.append('wpOldTitle', curr);
			formData.append('wpEditToken', mw.user.tokens.get('csrfToken'));

			const response = await fetch(url, {
				method: 'POST',
				body: formData,
				credentials: 'include'
			});

			if (response.ok) {
				progressBox.innerText = `Moved ${curr} to ${dest}.`;
				Morebits.status.actionCompleted('Moved.');
				resolve();
			} else {
				progressBox.innerText = `Failed to move ${curr} to ${dest}.`;
				reject('Move request failed');
			}
		};

		movePlus.moveQueue.push(moveTask);
		movePlus.startMoveQueue();

	});
};

movePlus.movePage = function movePlusMovePage(from, to, editSummary, progressBox, config) {

	return new Promise((resolve, reject) => {
		const moveTask = () => {
			progressBox.innerText = `Moving ${from} to ${to}...`;

			let pageToMove = new Morebits.wiki.page(from, `Moving ${from} to ${to}.`);
			pageToMove.setMoveDestination(to);
			pageToMove.setMoveSubpages(config.moveSubpages);
			pageToMove.setMoveTalkPage(config.moveTalkPage);
			pageToMove.setMoveSuppressRedirect(config.suppressRedirect);
			pageToMove.setEditSummary(`${editSummary}${movePlus.advert}`);

			console.log(`Moving ${from} to ${to}`);
			pageToMove.move(() => {
				progressBox.innerText = `Moved ${from} to ${to}.`;
				Morebits.status.actionCompleted('Moved.');
				resolve();
			}, (error) => {
				reject(error);
			});
		};

		movePlus.moveQueue.push(moveTask);
		movePlus.startMoveQueue();
	});
};

movePlus.moveRoundRobin = async function movePlusMoveRoundRobin(curr, dest, editSummary, progressBox, config) {

	progressBox.innerText = 'Round robin pending...';
	config.suppressRedirect = true;

	try {
		var destDetails = movePlus.splitPageName(dest);
		var intermediateTitle = `Draft:Move/${destDetails.pageName}`;
		editSummary = `${editSummary} via a [[WP:ROUNDROBIN|round robin]]`;

		progressBox.innerText = `Moving ${dest} to ${intermediateTitle}...`;
		await movePlus.movePage(dest, intermediateTitle, editSummary, progressBox, config);

		progressBox.innerText = `Moving ${curr} to ${dest}...`;
		await movePlus.movePage(curr, dest, editSummary, progressBox, config);

		progressBox.innerText = `Moving ${intermediateTitle} to ${curr}...`;
		await movePlus.movePage(intermediateTitle, curr, editSummary, progressBox, config);

		progressBox.innerText = `Cleaning up round robin for ${curr} and ${dest}...`;
		await movePlus.roundRobinCleanup(curr, dest, config);
		if (config.moveTalkPage) {
			await movePlus.roundRobinCleanup(movePlus.getTalkPageName(curr), movePlus.getTalkPageName(dest), config);
		}

		if (config.correctLinks) {
			movePlus.linkEditSummary = `Post-move cleanup, following [[WP:ROBIN|swap]] of [[${curr}]] and [[${dest}]]: `

			progressBox.innerText = `Correcting redirects for ${curr} and ${dest}...`;
			movePlus.correctRedirects(curr, dest, config);

			progressBox.innerText = `Correcting links for ${curr} and ${dest}...`;
			await movePlus.correctLinks(curr, dest, config, progressBox);
		}

	} catch (error) {
		console.error('Error during move operation:', error);
	}
};

movePlus.getLinksHere = async function movePlusGetLinksHere(page) {

	let query = {
		action: 'query',
		prop: 'linkshere',
		titles: page,
		lhlimit: 'max',
		format: 'json',
		lhnamespace: `${movePlus.splitPageName(page).namespace}|10|14`,
		rawcontinue: 1
	};

	let pages = [];

	do {
		let response = await new Morebits.wiki.api(`Listing links to ${page}`, query).post();

		if (response.response.query.pages[0].linkshere) {
			pages = pages.concat(response.response.query.pages[0].linkshere);
		}

		query.lhcontinue = response.response['query-continue'] ? response.response['query-continue'].linkshere.lhcontinue : 0;
	} while (query.lhcontinue)

	return pages;

}

movePlus.roundRobinCleanup = async function movePlusRoundRobinCleanup(curr, dest, config) {

	async function queryPagesAndRedirects(page) {
		let details = movePlus.splitPageName(page);

		let query = {
			action: 'query',
			list: 'allpages',
			apfrom: `${details.pageName}/`,
			apto: `${details.pageName}0`,
			apnamespace: details.namespace,
			format: 'json'
		};

		let allpages = [];
		if (config.moveSubpages) {
			const response = await new Morebits.wiki.api(`Listing subpages of ${details.pageName}`, query).post();
			allpages = response.response.query.allpages || [];
		}

		let pageTitles = allpages.map(page => page.title).join('|');
		if (pageTitles) {
			pageTitles += '|';
		}
		pageTitles += page;

		let pageQuery = {
			action: "query",
			format: "json",
			prop: "",
			titles: pageTitles
		}

		const responsePages = await new Morebits.wiki.api(`Getting details of subpages of ${details.pageName}`, pageQuery).post();
		const pages = (responsePages.response.query.pages || []).filter(page => !page.missing);

		pageQuery.redirects = 1;

		const responseRedirects = await new Morebits.wiki.api(`Getting details of subpages of ${details.pageName}`, pageQuery).post();
		const redirects = responseRedirects.response.query.redirects || [];
		return {redirects, pages};
	};

	function getPagePairs(pages, pair, self) {
		return pages.map(page => {
			const subpageName = movePlus.splitSubpageName(page.title, self);
			const pagename = subpageName === '' ? pair : `${pair}/${subpageName}`;
			return {
				pagename: pagename,
				target: page.title
			};
		});
	}

	function getRedirectPairs(redirects, pair, self) {
		return redirects.filter(redirect => redirect.from !== redirect.to).map(redirect => {
			const subpageName = movePlus.splitSubpageName(redirect.from, self);
			const pagename = subpageName === '' ? pair : `${pair}/${subpageName}`;
			return {
				pagename: pagename,
				target: redirect.to
			};
		});
	}

	function findSelfRedirects(redirects, pair, self) {
		return redirects.filter(redirect => redirect.from === redirect.to).map(redirect => {
			const subpageName = movePlus.splitSubpageName(redirect.from, self);
			const target = subpageName === '' ? pair : `${pair}/${subpageName}`;
			return {
				pagename: redirect.from,
				target: target
			};
		});
	}

	async function processPages(pairs, existingPages, existingRedirects) {
		const existingPageNames = new Set(existingPages.map(page => page.title));
		const existingRedirectNames = new Set(existingRedirects.map(redirect => redirect.from));

		for (const pair of pairs) {
			if (!existingPageNames.has(pair.pagename) && !existingRedirectNames.has(pair.pagename)) {
				await movePlus.createRedirect(pair.pagename, pair.target,
					`Redirecting to [[${pair.target}]] as part of post-[[WP:ROUNDROBIN|round robin]] cleanup${movePlus.advert}`);
			}
		};
	};

	const currResponse = await queryPagesAndRedirects(curr);
	const destResponse = await queryPagesAndRedirects(dest);

	const currRedirects = currResponse.redirects;
	const currPages = currResponse.pages;

	const destRedirects = destResponse.redirects;
	const destPages = destResponse.pages;

	const currPagePairs = getPagePairs(currPages, dest, curr);
	const currRedirectPairs = getRedirectPairs(currRedirects, dest, curr);
	const currSelfRedirects = findSelfRedirects(currRedirects, dest, curr);

	const destPagePairs = getPagePairs(destPages, curr, dest);
	const destRedirectPairs = getRedirectPairs(destRedirects, curr, dest);
	const destSelfRedirects = findSelfRedirects(destRedirects, curr, dest);

	await processPages(currPagePairs, destPages, destRedirects);
	await processPages(currRedirectPairs, destPages, destRedirects);

	await processPages(destPagePairs, currPages, currRedirects);
	await processPages(destRedirectPairs, currPages, currRedirects);

	for (const pair of currSelfRedirects.concat(destSelfRedirects)) {
		await movePlus.createRedirect(pair.pagename, pair.target, `Redirecting to [[${pair.target}]] as part of post-[[WP:ROUNDROBIN|round robin]] cleanup${movePlus.advert}`);
	}
};

movePlus.correctRedirects = async function movePlusCorrectRedirects(curr, dest, config) {

	if (config.currTarget != dest && config.currTarget != curr) {
		await movePlus.createRedirectPair(dest, config.destTarget, `Redirecting to [[${config.destTarget}]] as part of post-[[WP:ROUNDROBIN|round robin]] cleanup${movePlus.advert}`);
	}

	if (config.destTarget != curr && config.destTarget != dest) {
		await movePlus.createRedirect(curr, config.currTarget, `Redirecting to [[${config.currTarget}]] as part of post-[[WP:ROUNDROBIN|round robin]] cleanup${movePlus.advert}`);
	}

}

movePlus.correctLinks = async function movePlusCorrectLinks(curr, dest, config, progressBox) {

	let currPages = [];
	let destPages = [];
	progressBox.innerText = `Getting links to ${curr}...`;
	if (curr != config.currTarget) {
		currPages = await movePlus.getLinksHere(curr);
	}
	progressBox.innerText = `Getting links to ${dest}...`;
	if (dest != "" && dest != config.destTarget) {
		destPages = await movePlus.getLinksHere(dest);
	}

	const pageIdsCurr = new Set(currPages.map(item => item.pageid));
	const pageIdsDest = new Set(destPages.map(item => item.pageid));

	await processBatches(curr, config.currTarget, currPages, pageIdsDest, progressBox, true, dest, config.destTarget);
	await processBatches(dest, config.destTarget, destPages, pageIdsCurr, progressBox, false);

	async function processBatches(origin, target, pages, otherPages, progressBox, processDuplicates, otherOrigin = "", otherTarget = "") {

		if (origin == target) {
			return;
		}

		let query = {
			action: "query",
			format: "json",
			prop: "categories",
			titles: "",
			clcategories: "Category:All_disambiguation_pages|Category:All_set_index_articles"
		}

		for (let i = 0; i < pages.length; i += 50) {
			const batch = pages.slice(i, i + 50);
			query.titles = batch.map(item => item.title).join('|');

			const response = await new Morebits.wiki.api(`Listing categories of pages linking to ${origin}`, query).post();

			const dabPages = new Set();
			response.response.query.pages.forEach(page => {
				if (page.categories && page.categories.length > 0) {
					dabPages.add(page.pageid);
				}
			});

			let j = i

			for (const item of batch) {
				j++;
				//Skip archives
				if (item.ns != 0 && item.title.toLowerCase().includes('/archive')) {
					continue;
				}

				progressBox.innerHTML = `${origin}${target} (${j}/${pages.length}):<br>Updating <a href="https://en.wikipedia.org/wiki/${item.title}" target="_blank" rel="noopener noreferrer">${item.title}</a>...`;
				if (otherPages.has(item.pageid)) {
					if (processDuplicates) {
						await movePlus.correctLink(item, origin, target, dabPages.has(item.pageid), otherOrigin, otherTarget);
					}
				} else {
					await movePlus.correctLink(item, origin, target, dabPages.has(item.pageid));
				}
			}
		}
	}
}

movePlus.correctLink = function movePlusCorrectLink(item, from, to, dab, otherFrom = "", otherTo = "") {

	return new Promise((resolve, reject) => {
		console.log(`Updating links at ${item.title}`);
		let page = new Morebits.wiki.page(item.title, `Updating links at ${item.title}`);
		page.load(function(linkCorrection) {
			let originalText = page.getPageText();

			if (!allowBots(originalText, mw.config.get('wgUserName'))) {
				console.log(`Forbidden from making edits at ${item.title}`)
				resolve();
				return;
			}

			if (otherFrom != "" && otherTo != "") {
				var updatedText = updateTextRoundRobin(originalText, item, from, to, otherFrom, otherTo, dab);
			} else {
				var updatedText = updateText(originalText, item, from, to, dab);
			}

			if (updatedText.matches == 0) {
				console.log(`No edits to make at ${item.title}`);
				resolve();
				return;
			}

			let text = updatedText.text;
			let editSummary = updatedText.editSummary;

			const editTask = () => {
				page.setPageText(text);
				page.setEditSummary(editSummary);
				page.setMinorEdit(true);
				page.setBotEdit(true);
				console.log(`Successfully updated ${item.title}`);
				page.save(() => {
					Morebits.status.actionCompleted(`Replaced link to ${from} with link to ${to} at ${item.title}.`);
					resolve();
				}, (error) => {
					reject(error);
				});
			};

			movePlus.editQueue.push(editTask);
			movePlus.startEditQueue();

		});

	});

	function updateText(text, item, from, to, dab) {
		let updatedText = processText(text, item, from, to, dab);

		let editSummary = `${movePlus.linkEditSummary}Changed link from [[${from}]] to [[${to}]]${updatedText.matches > 1 ? ` (×${updatedText.matches})` : ""}${movePlus.advert}`;

		return {text: updatedText.text, editSummary: editSummary, matches: updatedText.matches};
	}

	function updateTextRoundRobin(text, item, from, to, otherFrom, otherTo, dab) {
		let stageOneText = processText(text, item, from, "INTERMEDIARYSTAGE", dab);
		let stageTwoText = processText(stageOneText.text, item, otherFrom, otherTo, dab);
		let updatedText = processText(stageTwoText.text, item, "INTERMEDIARYSTAGE", to, dab);

		let editSummary = `${movePlus.linkEditSummary}Changed link from [[${from}]] to [[${to}]] ${stageOneText.matches > 1 ? `(×${stageOneText.matches}) ` : ""}and from [[${otherFrom}]] to [[${otherTo}]]${stageTwoText.matches > 1 ? ` (×${stageTwoText.matches})` : ""}${movePlus.advert}`;

		return {text: updatedText.text, editSummary: editSummary, matches: stageOneText.matches + stageTwoText.matches};
	}

	function processText(text, item, from, to, dab) {

		function escapeRegExp(string) {
			return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
		}

		const linkRegex = new RegExp(`\\[\\[${escapeRegExp(from)}(\\|.+|#.*)?\\]\\]`, 'g');

		const matches = text.match(linkRegex);
		if (!matches) {
			return {text : text, matches: 0}
		}

		let updatedText = text.replace(linkRegex, match => {
			const hashedMatch = match.includes('#');
			const pipedMatch = match.includes('|');

			if (hashedMatch) {
				return match.replace(from, to);
			}

			if (item.redirect || (dab && !pipedMatch)) {
				return `[[${to}]]`;
			}

			if (pipedMatch) {
				var link = match.replace(from, to);
				link = link.replace(`${to}|${to}`, to);
				return link
			}

			return `[[${to}|${from}]]`;
		});

		return {text: updatedText, matches: matches.length};

	}
};

movePlus.createRedirectPair = async function movePlusCreateRedirectPair(page, target, editSummary, config) {

	await movePlus.createRedirect(page, target, editSummary);
	if (config.moveTalkPage) {
		let currTalk = movePlus.GetTalkPageName(page);
		let targetTalk = movePlus.GetTalkPageName(target);
		await movePlus.createRedirect(currTalk, targetTalk, editSummary);
	}
}

movePlus.createRedirect = async function movePlusCreateRedirect(page, target, editSummary) {
	return new Promise((resolve, reject) => {

		const editTask = () => {
			let redirect = new Morebits.wiki.page(page, `Creating redirect to ${target}`);
			redirect.load(function(redirect) {
				redirect.setPageText(`#REDIRECT [[${target}]]\n{{Rcat shell|\n{{R from move}}\n}}`);
				redirect.setEditSummary(editSummary);
				console.log(`Attempting to redirect ${page} to ${target}`);
				redirect.save(() => {
					Morebits.status.actionCompleted(`Cleaned up ${page}.`);
					resolve();
				}, (error) => {
					reject(error);
				});
			});
		};

		movePlus.editQueue.push(editTask);
		movePlus.startEditQueue();
	});
};

movePlus.splitPageName = function movePlusSplitPageName(page) {
	const namespaceSeparator = ':';
	const namespaceMap = {
		'': 0,
		'Talk': 1,
		'User': 2,
		'User talk': 3,
		'Wikipedia': 4,
		'Wikipedia talk': 5,
		'File': 6,
		'File talk': 7,
		'MediaWiki': 8,
		'MediaWiki talk': 9,
		'Template': 10,
		'Template talk': 11,
		'Help': 12,
		'Help talk': 13,
		'Category': 14,
		'Category talk': 15,
		'Portal': 100,
		'Draft': 118,
		'Draft talk': 119,
		'TimedText': 710,
		'TimedText talk': 711,
		'Module': 828,
		'Module talk': 829
	};

	const separatorIndex = page.indexOf(namespaceSeparator);
	if (separatorIndex === -1) {
		return { namespace: 0, pageName: page };
	}

	const potentialNamespace = page.substring(0, separatorIndex).trim();
	const pageName = page.substring(separatorIndex + 1).trim();

	const namespaceNumber = namespaceMap.hasOwnProperty(potentialNamespace) ? namespaceMap[potentialNamespace] : 0;

	if (namespaceNumber === 0) {
		return { namespace: 0, pageName: page };
	}

	return { namespace: namespaceNumber, pageName: pageName };
};

movePlus.getTalkPageName = function movePlusGetTalkPageName(page) {
	const talkNamespaceMap = {
		0: 'Talk',
		2: 'User talk',
		4: 'Wikipedia talk',
		6: 'File talk',
		8: 'MediaWiki talk',
		10: 'Template talk',
		12: 'Help talk',
		14: 'Category talk',
		100: 'Portal talk',
		118: 'Draft talk',
		710: 'TimedText talk',
		828: 'Module talk'
	};

	const splitResult = movePlus.splitPageName(page);
	const talkNamespace = talkNamespaceMap[splitResult.namespace];

	return `${talkNamespace}:${splitResult.pageName}`;
};

movePlus.splitSubpageName = function movePlusSplitSubpageName(page, self) {

	let selfIndex = page.indexOf(self);

	if (selfIndex === -1 || selfIndex === page.length - self.length) {
		return "";
	}

	return page.substring(selfIndex + self.length + 1);
}

movePlus.submitRMTR = function movePlusSubmitRMTR(curr, dest, adminRequired, reason, progressBox) {

	progressBox.innerText = `Submitting technical request for ${curr} to ${dest}...`;

	var rmtr = new Morebits.wiki.page('Wikipedia:Requested moves/Technical requests', 'Submitting request at WP:RM/TR');

	rmtr.load(function(page) {
		if (adminRequired) {
			rmtr.setAppendText('{{subst:RMassist|1=' + curr + '|2=' + dest + '|reason=' + reason + '}}');
		} else {
			var text = rmtr.getPageText();
			var textToFind = /\n{1,}(==== ?Requests to revert undiscussed moves ?====)/i;
			var rmtrText = '{{subst:RMassist|1=' + curr + '|2=' + dest + '|reason=' + reason + '}}';
			text = text.replace(textToFind, '\n' + rmtrText + '\n\n$1');
			rmtr.setPageText(text);
		}
		rmtr.setEditSummary('Add request' + movePlus.advert);
		rmtr.save(() => {
			progressBox.innerText = 'Technical request submitted';
			Morebits.status.actionCompleted('Requested.')
		});
	});
};

movePlus.relist = function movePlusRelist(e) {
	if (e) e.preventDefault();
	var title_obj = mw.Title.newFromText(Morebits.pageNameNorm);
	movePlus.talktitle = title_obj.getTalkPage().toText();
	var talkpage = new Morebits.wiki.page(movePlus.talktitle, 'Relisting.');

	var relistingComment = document.getElementById('movePlusRelistComment').value;

	talkpage.load(function(talkpage) {
		var text = talkpage.getPageText();

		var templateFound = false;
		var sig;
		var line;
		var templateIndex = -1;
		var textToFind = text.split('\n');
		for (var i = 0; i < textToFind.length; i++) {
			line = textToFind[i];
			if(templateFound == false){
				if(/{{[Rr]equested move\/dated/.test(line)){
					templateFound = true;
					templateIndex = i;
				}
			} else if(templateFound == true){
				if (/ \(UTC\)/.test(line)){
					sig = line;
					break;
				}
			}
		}

		text = text.replace(sig, sig + " {{subst:RM relist}}");

		if(relistingComment != ''){
			var nextSection = false;
			for (var i = templateIndex+1; i < textToFind.length; i++) {
				line = textToFind[i];
				if (line.match(/^(==)[^=].+\1/)) {
					nextSection = true;
					var escapedLine = line.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
					var regex = new RegExp('(' + escapedLine + ')(?![\s\S]*(' + escapedLine + '))', 'm');
					text = text.replace(regex, ':<small>\'\'\'Relisting comment\'\'\': ' + relistingComment + ' ~~~~</small>\n\n' + line);
					break;
				}
			}

			if (!nextSection) {
				text += '\n:<small>\'\'\'Relisting comment\'\'\': ' + relistingComment + ' ~~~~</small>';
			}
		}

		talkpage.setPageText(text);
		talkpage.setEditSummary('Relisted requested move' + movePlus.advert);
		talkpage.save(Morebits.status.actionCompleted('Relisted.'));
		document.getElementById("requestedmovetag").innerHTML = "";
		setTimeout(function(){ location.reload() }, 2000);
	});
};

movePlus.notify = function movePlusNotify(e) {
	if (e) e.preventDefault();
	var wikiProjectTemplates = document.getElementsByClassName("wpb-project_link");
	var wikiProjectNames = [];
	var wikiProjects = [];
	for(var i=0; i<wikiProjectTemplates.length; i++){
		var wikiProjectName = wikiProjectTemplates[i].innerHTML;
		var wikiProjectTalk = mw.Title.newFromText(wikiProjectTemplates[i].innerHTML).getTalkPage().toText();
		if (!wikiProjectNames.includes(wikiProjectName)) {
			wikiProjectNames.push(wikiProjectName);
			wikiProjects.push(wikiProjectTalk);
		}
	}

	var wikiProjectBannerShellHeaders = document.getElementsByClassName("wpb-header-combined");
	for (var i=0; i<wikiProjectBannerShellHeaders.length; i++) {
		var subprojectList = wikiProjectBannerShellHeaders[i];
		if (subprojectList.hasChildNodes() && subprojectList.children.length > 2) {
			subprojectList = subprojectList.children[2];
			if (subprojectList.hasChildNodes() && subprojectList.children.length > 0) {
				subprojectList = subprojectList.children;
				for (var j=0; j<subprojectList.length; j++) {
					var wikiProjectName = subprojectList[j].title;
					var wikiProjectTalk = mw.Title.newFromText(subprojectList[j].title).getTalkPage().toText();
					if (!wikiProjectNames.includes(wikiProjectName)) {
						wikiProjectNames.push(wikiProjectName);
						wikiProjects.push(wikiProjectTalk);
					}
				}
			}
		}
	}

	if(wikiProjects.length == 0){
		mw.notify('No WikiProject banners found on this page');
	} else{
		var Window = new Morebits.simpleWindow(600, 450);
		Window.setTitle( "Notify WikiProjects about requested move" );
		Window.setScriptName('movePlus');
		Window.addFooterLink('Script documentation', 'User:BilledMammal/movePlus');
		Window.addFooterLink('Give feedback', 'User talk:BilledMammal/movePlus');

		var form = new Morebits.quickForm(movePlus.notifyCheck);

		form.append({
			type: 'div',
			label: 'WikiProjects with banners on this page:'
		});

		form.append({
			type: 'checkbox',
			name: 'wikiProject',
			list: wikiProjects.map(function (wp) {
				var wplabel = wikiProjectNames[wikiProjects.indexOf(wp)];
				return { type: 'option', label: wplabel, value: wp };
			})
		});

		if(wikiProjects[0] != 'none'){
			form.append({ type: 'submit', label: 'Notify selected WikiProject(s)' });
		}

		var formResult = form.render();
		Window.setContent(formResult);
		Window.display();
	}
};

movePlus.notifyCheck = function(e) {
	var form = e.target;
	movePlus.params = Morebits.quickForm.getInputData(form);

	Morebits.simpleWindow.setButtonsEnabled(false);
	Morebits.status.init(form);

	var wikiProjectsToNotify = movePlus.params.wikiProject;

	if (wikiProjectsToNotify.length == 0) {
		Morebits.status.error('Error', 'No WikiProjects selected');
	} else {
		var uniqueWikiProjects = [];
		var wikiProjectCount = 0;
		for (var i=0; i<wikiProjectsToNotify.length; i++) {
			var talkpage = new Morebits.wiki.page(wikiProjectsToNotify[i], 'Checking ' + wikiProjectsToNotify[i] + '.');
			talkpage.setFollowRedirect(true);
			talkpage.load(function(talkpage) {
				var wikiProjectToNotify = talkpage.getPageName();
				if (!uniqueWikiProjects.includes(wikiProjectToNotify)) {
					uniqueWikiProjects.push(wikiProjectToNotify);
				}
				wikiProjectCount++;
				if (wikiProjectCount == wikiProjectsToNotify.length && uniqueWikiProjects.length > 0) {
					movePlus.notifyGetSection(uniqueWikiProjects);
				}
			});
		}
	}
};

movePlus.notifyGetSection = function(wikiProjectsToNotify) {
	var title_obj = mw.Title.newFromText(Morebits.pageNameNorm);
	movePlus.talktitle = title_obj.getTalkPage().toText();
	var talkpage = new Morebits.wiki.page(movePlus.talktitle, 'Getting section.');

	talkpage.load(function(talkpage) {
		var text = talkpage.getPageText();
		var line;
		var templateIndex = -1;
		var rmSection;
		var textToFind = text.split('\n');
		for (var i = 0; i < textToFind.length; i++) {
			line = textToFind[i];
			if(/{{[Rr]equested move\/dated/.test(line)){
				templateIndex = i;
				break;
			}
		}

		for (var i = templateIndex; i >= 0; i--) {
			line = textToFind[i];
			if (line.match(/^(==)[^=].+\1/)) {
				rmSection = line.match(/^(==)[^=](.+)\1/)[2].trim();
				break;
			}
		}

		movePlus.notifyEvaluate(wikiProjectsToNotify, rmSection);
	});
};

movePlus.notifyEvaluate = function(wikiProjectsToNotify, moveSection) {
	var wikiProjectsNotified = [];
	var wikiProjectCount = 0;
	for (var j=0; j<wikiProjectsToNotify.length; j++) {
		var talkpage = new Morebits.wiki.page(wikiProjectsToNotify[j], 'Notifying ' + wikiProjectsToNotify[j] + '.');
		talkpage.setFollowRedirect(true);
		talkpage.load(function(talkpage) {
			var wikiProjectToNotify = talkpage.getPageName();
			var text = talkpage.getPageText();

			movePlus.talktitle = mw.Title.newFromText(Morebits.pageNameNorm).getTalkPage().toText();
			var pageAndSection = movePlus.talktitle + "#" + moveSection;

			var notified;

			if(confirm("\"" + wikiProjectToNotify + "\" may have already been notified of the discussion. Do you wish to proceed?")){
				text += "\n\n== Requested move at [[" + pageAndSection + "]] ==\n[[File:Information.svg|30px|left]] There is a requested move discussion at [[" + pageAndSection + "]] that may be of interest to members of this WikiProject. ~~~~";

				talkpage.setPageText(text);
				talkpage.setEditSummary('Notifying of [[' + pageAndSection + '\|requested move]]' + movePlus.advert);
				talkpage.save(Morebits.status.actionCompleted('Notified.'));
				notified = true;
			} else{
				var cancelNotify = new Morebits.status('Error', 'Notification canceled', 'error');
				notified = false;
			}

			if(notified){
				wikiProjectsNotified.push(wikiProjectToNotify);
			}

			wikiProjectCount++;

			if (wikiProjectCount == wikiProjectsToNotify.length && wikiProjectsNotified.length > 0) {
				movePlus.notifyListOnTalkPage(wikiProjectsNotified);
			}
		});
	}
};

movePlus.notifyListOnTalkPage = function(wikiProjectsNotified) {
	var discussionPage = new Morebits.wiki.page(movePlus.talktitle, 'Adding note about notification to requested move');
	discussionPage.load(function(discussionPage) {
		var discussionPageText = discussionPage.getPageText();

		var templateFound = false;
		var line;
		var nextSection = false;
		var textToFind = discussionPageText.split('\n');
		for (var i = 0; i < textToFind.length; i++) {
			line = textToFind[i];
			if(templateFound == false){
				if(/{{[Rr]equested move\/dated/.test(line)){
					templateFound = true;
				}
			} else if(templateFound == true){
				if (line.match(/^(==)[^=].+\1/)) {
					nextSection = true;
					var escapedLine = line.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
					var regex = new RegExp('(' + escapedLine + ')(?![\s\S]*(' + escapedLine + '))', 'm');
					if (wikiProjectsNotified.length == 1) {
						var wikiProjectToNotify = wikiProjectsNotified[0];
						discussionPageText = discussionPageText.replace(regex, ':<small>Note: [[' + wikiProjectToNotify + '|' + wikiProjectToNotify.slice(15) + ']] has been notified of this discussion. ~~~~</small>\n\n' + line);
					} else {
						var textToInsert = ':<small>Note: ';
						for (var j=0; j<wikiProjectsNotified.length; j++) {
							var wikiProjectToNotify = wikiProjectsNotified[j];
							textToInsert += '[[' + wikiProjectToNotify + '|' + wikiProjectToNotify.slice(15) + ']]';
							if (j == wikiProjectsNotified.length-2) {
								if (wikiProjectsNotified.length == 2) {
									textToInsert += ' and ';
								} else {
									textToInsert += ', and ';
								}
							} else if (j != wikiProjectsNotified.length-1) {
								textToInsert += ', ';
							}
						}
						textToInsert += ' have been notified of this discussion. ~~~~</small>\n\n';
						discussionPageText = discussionPageText.replace(regex, textToInsert + line);
					}
					break;
				}
			}
		}

		if (!nextSection) {
			if (wikiProjectsNotified.length == 1) {
				var wikiProjectToNotify = wikiProjectsNotified[0];
				discussionPageText+='\n:<small>Note: [[' + wikiProjectToNotify + '|' + wikiProjectToNotify.slice(15) + ']] has been notified of this discussion. ~~~~</small>';
			} else {
				discussionPageText += '\n:<small>Note: ';
				for (var j=0; j<wikiProjectsNotified.length; j++) {
					var wikiProjectToNotify = wikiProjectsNotified[j];
					discussionPageText += '[[' + wikiProjectToNotify + '|' + wikiProjectToNotify.slice(15) + ']]';
					if (j == wikiProjectsNotified.length-2) {
						if (wikiProjectsNotified.length == 2) {
							discussionPageText += ' and ';
						} else {
							discussionPageText += ', and ';
						}
					} else if (j != wikiProjectsNotified.length-1) {
						discussionPageText += ', ';
					}
				}
				discussionPageText += ' have been notified of this discussion. ~~~~</small>';
			}
		}

		discussionPage.setPageText(discussionPageText);
		discussionPage.setEditSummary('Added note about notifying WikiProject about requested move' + movePlus.advert);
		discussionPage.save(Morebits.status.actionCompleted('Note added.'));
		setTimeout(function(){ location.reload() }, 2000);
	});
};

//Queues
movePlus.processMoveQueue = function movePlusProcessMoveQueue() {
	if (movePlus.moveQueue.length > 0) {
		let moveTask = movePlus.moveQueue.shift();
		moveTask();
	}
};

movePlus.startMoveQueue = function movePlusStartMoveQueue() {

	if (!movePlus.moveInterval) {
		var moveRateLimit = Morebits.userIsInGroup('sysop') ? 39 : Morebits.userIsInGroup('extendedmover') ? 15 : 7;
		movePlus.moveInterval = setInterval(movePlus.processMoveQueue, 60000 / moveRateLimit);
	}
};

movePlus.processEditQueue = function movePlusProcessEditQueue() {
	if (movePlus.editQueue.length > 0) {
		let editTask = movePlus.editQueue.shift();
		editTask();
	}
};

movePlus.startEditQueue = function movePlusStartEditQueue() {

	if (!movePlus.editInterval) {
		var editRateLimit = 20;
		movePlus.editInterval = setInterval(movePlus.processEditQueue, 60000 / editRateLimit);
	}
};

function allowBots(text, user){
	if (!new RegExp("\\{\\{\\s*(nobots|bots[^}]*)\\s*\\}\\}", "i").test(text)) return true;
	return (new RegExp("\\{\\{\\s*bots\\s*\\|\\s*deny\\s*=\\s*([^}]*,\\s*)*" + user.replace(/([\(\)\*\+\?\.\-\:\!\=\/\^\$])/g, "\\$1") + "\\s*(?=[,\\}])[^}]*\\s*\\}\\}", "i").test(text)) ? false : new RegExp("\\{\\{\\s*((?!nobots)|bots(\\s*\\|\\s*allow\\s*=\\s*((?!none)|([^}]*,\\s*)*" + user.replace(/([\(\)\*\+\?\.\-\:\!\=\/\^\$])/g, "\\$1") + "\\s*(?=[,\\}])[^}]*|all))?|bots\\s*\\|\\s*deny\\s*=\\s*(?!all)[^}]*|bots\\s*\\|\\s*optout=(?!all)[^}]*)\\s*\\}\\}", "i").test(text);
}
//</nowiki>