User:Qwerfjkl/scripts/massCFD.js

// // todo: make counter inline, remove progresss and progressElement from editPAge, more dynamic reatelimit wait. // counter semi inline; adjust align in createProgressBar // Function to wipe the text content of the page inside #bodyContent function wipePageContent { var bodyContent = $('#bodyContent'); if (bodyContent) { bodyContent.empty; } var header = $('#firstHeading'); if (header) { header.text('Mass CfD'); } $('title').text('Mass CfD - Wikipedia'); }

function createProgressElement { var progressContainer = new OO.ui.PanelLayout({       padded: true,        expanded: false,        classes: ['sticky-container']      }); return progressContainer; }

function makeInfoPopup (info) { var infoPopup = new OO.ui.PopupButtonWidget( {		icon: 'info',		framed: false,		label: 'More information',		invisibleLabel: true,		popup: {			head: true,			icon: 'infoFilled',			label: 'More information',			$content: $( ` ${info} ` ),			padded: true,			align: 'force-left',			autoFlip: false		}	} ); return infoPopup; }

function makeCategoryTemplateDropdown (label) { var dropdown = new OO.ui.DropdownInputWidget( {		required: true,		options: [			{				data: 'lc',				label: 'Category link with extra links – '			},			{				data: 'clc',				label: 'Category link with count – '			},			{				data: 'cl',				label: 'Plain category link – '			}		]	} ); var fieldlayout = new OO.ui.FieldLayout( 		dropdown, 		{ label: label,		 align: 'inline',		  classes: ['newnomonly'],		}	); return {container: fieldlayout, dropdown: dropdown}; }

function createTitleAndInputFieldWithLabel(label, placeholder, classes=[]) { var input = new OO.ui.TextInputWidget( {	   placeholder: placeholder	} ); var fieldset = new OO.ui.FieldsetLayout( {		classes: classes	} );

fieldset.addItems( [	   new OO.ui.FieldLayout( input, { label: label } ),	] );

return { container: fieldset, inputField: input, }; } // Function to create a title and an input field function createTitleAndInputField(title, placeholder, info = false) { var container = new OO.ui.PanelLayout({   expanded: false  });

var titleLabel = new OO.ui.LabelWidget({   label: $(` ${title} `)  }); var infoPopup = makeInfoPopup(info);

var inputField = new OO.ui.MultilineTextInputWidget({   placeholder: placeholder,    indicator: 'required',    rows: 10,    autosize: true  }); if (info) container.$element.append(titleLabel.$element, infoPopup.$element, inputField.$element); else container.$element.append(titleLabel.$element, inputField.$element); return { titleLabel: titleLabel, inputField: inputField, container: container, infoPopup: infoPopup }; }

// Function to create a title and an input field function createTitleAndSingleInputField(title, placeholder) { var container = new OO.ui.PanelLayout({   expanded: false  });

var titleLabel = new OO.ui.LabelWidget({   label: title  });

var inputField = new OO.ui.TextInputWidget({   placeholder: placeholder,    indicator: 'required'  });

container.$element.append(titleLabel.$element, inputField.$element);

return { titleLabel: titleLabel, inputField: inputField, container: container }; }

function createStartButton { var button = new OO.ui.ButtonWidget({       label: 'Start',        flags: ['primary', 'progressive']      }); return button; }

function createAbortButton { var button = new OO.ui.ButtonWidget({       label: 'Abort',        flags: ['primary', 'destructive']      }); return button; }

function createRemoveBatchButton { var button = new OO.ui.ButtonWidget( {	   label: 'Remove',	    icon: 'close',	    title: 'Remove',	    classes: [	    	'remove-batch-button'	    	],	    flags: [	    	'destructive'	    	]	} ); return button; }

function createNominationToggle { var newNomToggle = new OO.ui.ButtonOptionWidget( {				data: 'new',				label: 'New nomination',			} ); var oldNomToggle = new OO.ui.ButtonOptionWidget( {				data: 'old',				label: 'Old nomination',				selected: true			} ); var toggle = new OO.ui.ButtonSelectWidget( {		items: [			newNomToggle,			oldNomToggle		]	} ); return { toggle: toggle, newNomToggle: newNomToggle, oldNomToggle: oldNomToggle }; }

function createMessageElement { var messageElement = new OO.ui.MessageWidget({       type: 'progress',        inline: true,        progressType: 'infinite'    }); return messageElement; }

function createRatelimitMessage { var ratelimitMessage = new OO.ui.MessageWidget({		type: 'warning',		style: 'background-color: yellow;'   }); return ratelimitMessage; }

function createCompletedElement { var messageElement = new OO.ui.MessageWidget({       type: 'success',    }); return messageElement; }

function createAbortMessage { // pretty much a duplicate of ratelimitMessage var abortMessage = new OO.ui.MessageWidget({		type: 'warning',   }); return abortMessage; }

function createNominationErrorMessage { // pretty much a duplicate of ratelimitMessage var nominationErrorMessage = new OO.ui.MessageWidget({		type: 'error',		text: 'Could not detect where to add new nomination.'   }); return nominationErrorMessage; }

function createFieldset(headingLabel) { var fieldset = new OO.ui.FieldsetLayout({		         	label: headingLabel,		          }); return fieldset; }

function createCheckboxWithLabel(label) { var checkbox = new OO.ui.CheckboxInputWidget( {       value: 'a',         selected: true,    label: "Foo",    data: "foo"    } ); var fieldlayout = new OO.ui.FieldLayout( 		checkbox, 		{ label: label,		 align: 'inline',		  selected: true		} 	); return { fieldlayout: fieldlayout, checkbox: checkbox }; } function createMenuOptionWidget(data, label) { var menuOptionWidget = new OO.ui.MenuOptionWidget( {			data: data,			label: label		} ); return menuOptionWidget; } function createActionDropdown { var dropdown = new OO.ui.DropdownWidget( {		label: 'Mass action',		menu: {			items: [				createMenuOptionWidget('delete', 'Delete'),				createMenuOptionWidget('merge', 'Merge'),				createMenuOptionWidget('rename', 'Rename'),				createMenuOptionWidget('split', 'Split'),				createMenuOptionWidget('listfy', 'Listify'),				createMenuOptionWidget('custom', 'Custom'),			]		}	} ); return dropdown; }

function createMultiOptionButton { var button = new OO.ui.ButtonWidget( {	   label: 'Additional action',	    icon: 'add',	    flags: [	        'progressive'	        ]	} ); return button; }

function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }

function makeLink(title) { return `${title}`; }

function getWikitext(pageTitle) { var api = new mw.Api; var requestData ={ "action": "query", "format": "json", "prop": "revisions", "titles": pageTitle, "formatversion": "2", "rvprop": "content", "rvlimit": "1", };	return api.get(requestData).then(function (data) {       var pages = data.query.pages;        return pages[0].revisions[0].content; // Return the wikitext    }).catch(function (error) {        console.error('Error fetching wikitext:', error);    }); }

// function to revert edits function revertEdits { var revertAllCount = 0; var revertElements = $('.masscfdundo'); if (!revertElements.length) { $('#masscfdrevertlink').replaceWith('Reverts done.'); } else { $('#masscfdrevertlink').replaceWith(' Reverting... ( 0 / '+revertElements.length+' done) ');

revertElements.each(function (index, element) {			element = $(element); // jQuery-ify			var title = element.attr('data-title');			var revid = element.attr('data-revid');			revertEdit(title, revid)			   .then(function { element.text('. Reverted.'); revertAllCount++; $('#revertall-done').text( revertAllCount ); }).catch(function { element.html('. Revert failed. Click here to view the diff.'); });		}).promise.done(function {			$('#revertall-text').text('Reverts done.');		}); } }

function revertEdit(title, revid, retry=false) { var api = new mw.Api;

if (retry) { sleep(1000); }	var requestData = { action: 'edit', title: title, undo: revid, format: 'json' };	return new Promise(function(resolve, reject) {	 api.postWithEditToken(requestData).then(function(data) { if (data.edit && data.edit.result === 'Success') { resolve(true); } else { console.error('Error occurred while undoing edit:', data); reject; }	 }).catch(function(error) { console.error('Error occurred while undoing edit:', error); // handle: editconflict, ratelimit (retry) if (error == 'editconflict') { resolve(revertEdit(title, revid, retry=true)); } else if (error == 'ratelimited') { setTimeout(function { // wait a minute			 resolve(revertEdit(title, revid, retry=true));			}, 60000); } else { reject; }	 });    }); }

function getUserData(titles) { var api = new mw.Api; return api.get({   action: 'query',    list: 'users',    ususers: titles,    usprop: 'blockinfo|groups', // blockinfo - check if indeffed, groups - check if bot    format: 'json'  }).then(function(data) {      return data.query.users;  }).catch(function(error) {    console.error('Error occurred while fetching page author:', error);    return false;  }); }

function getPageAuthor(title) { var api = new mw.Api; return api.get({   action: 'query',    prop: 'revisions',    titles: title,    rvprop: 'user',    rvdir: 'newer', // Sort the revisions in ascending order (oldest first)    rvlimit: 1,    format: 'json'  }).then(function(data) {    var pages = data.query.pages;    var pageId = Object.keys(pages)[0];    var revisions = pages[pageId].revisions;    if (revisions && revisions.length > 0) {

return revisions[0].user; } else { return false; } }).catch(function(error) { console.error('Error occurred while fetching page author:', error); return false; }); }

// Function to create a list of page authors and filter duplicates function createAuthorList(titles) { var authorList = []; var promises = titles.map(function(title) {   return getPageAuthor(title);  }); return Promise.all(promises).then(async function(authors) { 	let queryBatchSize = 50;    let authorTitles = authors.map(author => author.replace(/ /g, '_')); // Replace spaces with underscores  	let filteredAuthorList = [];  	for (let i = 0; i < authorTitles.length; i += queryBatchSize) {	    let batch = authorTitles.slice(i, i + queryBatchSize);	    let batchTitles = batch.join('|');	    await getUserData(batchTitles)	        .then(response => { response.forEach(user => {                   if (user && (!user.blockexpiry || user.blockexpiry !== "infinite") && !user.groups.includes('bot') && !filteredAuthorList.includes('User talk:'+user.name) )                   filteredAuthorList.push('User talk:'+user.name);                }); })	       .catch(error => { console.error("Error querying API:", error); });	}   return filteredAuthorList;  }).catch(function(error) {    console.error('Error occurred while creating author list:', error);    return authorList;  }); }

// Function to prepend text to a page function editPage(title, text, summary, progressElement, ratelimitMessage, progress, type, titlesDict, retry=false) { var api = new mw.Api;

var messageElement = createMessageElement;

messageElement.setLabel((retry) ? $(' ').text('Retrying ').append($(makeLink(title))) : $(' ').text('Editing ').append($(makeLink(title))) ); progressElement.$element.append(messageElement.$element); var container = $('.sticky-container'); container.scrollTop(container.prop("scrollHeight")); if (retry) { sleep(1000); }

var requestData = { action: 'edit', title: title, summary: summary, format: 'json' }; if (type === 'prepend') { // cat requestData.nocreate = 1; // don't create new cat // parse title var targets = titlesDict[title];

for (let i = 0; i < targets.length; i++) { // we add 1 to i in the replace function because placeholders start from $1 not $0 let placeholder = '$' + (i + 1); text = text.replace(placeholder, targets[i]); }   text = text.replace(/\$\d/g, ''); // remove unmatched |$x requestData.prependtext = text.trim + '\n\n';

} else if (type === 'append') { // user requestData.appendtext = '\n\n' + text.trim; } else if (type === 'text') { requestData.text = text; } return new Promise(function(resolve, reject) {  	if (window.abortEdits) {  		// hide message and return  		messageElement.toggle(false);  		resolve;  		return;  	}	  api.postWithEditToken(requestData).then(function(data) { if (data.edit && data.edit.result === 'Success') { messageElement.setType('success'); messageElement.setLabel( $(' ' + makeLink(title) + ' edited successfully  ') );

resolve; } else { messageElement.setType('error'); messageElement.setLabel( $(' Error occurred while editing ' + makeLink(title) + ': '+ data + ' ') ); console.error('Error occurred while prepending text to page:', data);

reject; }	 }).catch(function(error) { messageElement.setType('error'); messageElement.setLabel( $(' Error occurred while editing ' + makeLink(title) + ': '+ error + ' ') ); console.error('Error occurred while prepending text to page:', error); // handle: editconflict, ratelimit (retry) if (error == 'editconflict') { editPage(title, text, summary, progressElement, ratelimitMessage, progress, type, titlesDict, retry=true).then(function {	       	resolve;	        }); } else if (error == 'ratelimited') { progress.setDisabled(true);

handleRateLimitError(ratelimitMessage).then(function {	    	   progress.setDisabled(false);	    	   editPage(title, text, summary, progressElement, ratelimitMessage, progress, type, titlesDict, retry=true).then(function { resolve; });	   	});	    }	    else { reject; }	 });  }); }

// global scope - needed to syncronise ratelimits var massCFDratelimitPromise = null; // Function to handle rate limit errors function handleRateLimitError(ratelimitMessage) { var modify = !(ratelimitMessage.isVisible); // only do something if the element hasn't already been shown if (massCFDratelimitPromise !== null) { return massCFDratelimitPromise; } massCFDratelimitPromise =  new Promise(function(resolve) {    var remainingSeconds = 60;    var secondsToWait = remainingSeconds * 1000;    console.log('Rate limit reached. Waiting for ' + remainingSeconds + ' seconds...');   ratelimitMessage.setType('warning');    ratelimitMessage.setLabel('Rate limit reached. Waiting for ' + remainingSeconds + ' seconds...');   ratelimitMessage.toggle(true);

var countdownInterval = setInterval(function {     remainingSeconds--;      if (modify) {        ratelimitMessage.setLabel('Rate limit reached. Waiting for ' + remainingSeconds + ' second' + ((remainingSeconds === 1) ? '' : 's') + '...');     }

if (remainingSeconds <= 0 || window.abortEdits) { clearInterval(countdownInterval); massCFDratelimitPromise = null; // reset ratelimitMessage.toggle(false); resolve; }   }, 1000);

// Use setTimeout to ensure the promise is resolved even if the countdown is not reached setTimeout(function {     clearInterval(countdownInterval);      ratelimitMessage.toggle(false);      massCFDratelimitPromise = null; // reset      resolve;    }, secondsToWait); }); return massCFDratelimitPromise; }

// Function to show progress visually function createProgressBar(label) { var progressBar = new OO.ui.ProgressBarWidget; progressBar.setProgress(0); var fieldlayout = new OO.ui.FieldLayout( progressBar, {       label: label, 		align: 'inline'    }); return {progressBar: progressBar, fieldlayout: fieldlayout}; }

// Main function to execute the script async function runMassCFD { mw.util.addPortletLink ( 'p-tb', mw.util.getUrl( 'Special:MassCFD' ), 'Mass CfD', 'pt-masscfd', 'Create a mass CfD nomination'); if (mw.config.get('wgPageName') === 'Special:MassCFD') { // Load the required modules mw.loader.using('oojs-ui').done(function {	   wipePageContent;	    onbeforeunload = function {			return "Closing this tab will cause you to lose all progress.";		};		elementsToDisable = [];	    var bodyContent = $('#bodyContent');	    mw.util.addCSS(`.sticky-container { bottom: 0; width: 100%; max-height: 600px; overflow-y: auto; }`);		var nominationToggleObj = createNominationToggle;		var nominationToggle = nominationToggleObj.toggle;		var nominationToggleOld = nominationToggleObj.oldNomToggle;		var nominationToggleNew = nominationToggleObj.newNomToggle;		bodyContent.append(nominationToggle.$element);		elementsToDisable.push(nominationToggle);	   var discussionLinkObj = createTitleAndSingleInputField('Discussion link', 'Wikipedia:Categories for discussion/Log/2023 July 23#Category:Archaeological cultures by ethnic group');	    var discussionLinkContainer = discussionLinkObj.container;	    var discussionLinkInputField = discussionLinkObj.inputField;	    elementsToDisable.push(discussionLinkInputField);        var newNomHeaderObj = createTitleAndSingleInputField('Nomination title', 'Archaeological cultures by ethnic group');	    var newNomHeaderContainer = newNomHeaderObj.container;	    var newNomHeaderInputField = newNomHeaderObj.inputField; elementsToDisable.push(newNomHeaderInputField); var rationaleObj = createTitleAndInputField('Rationale:', 'Non-defining category.'); var rationaleContainer = rationaleObj.container; var rationaleInputField = rationaleObj.inputField; elementsToDisable.push(rationaleInputField);

bodyContent.append(discussionLinkContainer.$element); bodyContent.append(newNomHeaderContainer.$element, rationaleContainer.$element); if (nominationToggleOld.isSelected) { discussionLinkContainer.$element.show; newNomHeaderContainer.$element.hide; rationaleContainer.$element.hide; }		else if (nominationToggleNew.isSelected) { discussionLinkContainer.$element.hide; newNomHeaderContainer.$element.show; rationaleContainer.$element.show; }		nominationToggle.on('select',function {			if (nominationToggleOld.isSelected) {				discussionLinkContainer.$element.show;				newNomHeaderContainer.$element.hide;				rationaleContainer.$element.hide;			}			else if (nominationToggleNew.isSelected) {				discussionLinkContainer.$element.hide;				newNomHeaderContainer.$element.show;				rationaleContainer.$element.show;			}		});

function createActionNomination (actionsContainer, first=false) { var count = actions.length+1; var container = createFieldset('Action batch #'+count); actionsContainer.append(container.$element); var dropdown = createActionDropdown; elementsToDisable.push(dropdown); dropdown.$element.css('max-width', 'fit-content'); var prependTextObj = createTitleAndInputField('CfD text to add to the start of the page', 'Category:Bishops', info='A dollar sign  followed by a number, such as , will be replaced with a target specified in the title field, or if not target is specified, will be removed.'); var prependTextLabel = prependTextObj.titleLabel; var prependTextInfoPopup = prependTextObj.infoPopup; var prependTextInputField = prependTextObj.inputField; elementsToDisable.push(prependTextInputField); var prependTextContainer = new OO.ui.PanelLayout({			   expanded: false			  }); var actionObj = createTitleAndInputFieldWithLabel('Action', 'renaming', classes=['newnomonly']); var actionContainer = actionObj.container; var actionInputField = actionObj.inputField; elementsToDisable.push(actionInputField); actionInputField.$element.css('max-width', 'fit-content'); if ( nominationToggleOld.isSelected ) actionContainer.$element.hide; // make invisible until needed prependTextContainer.$element.append(prependTextLabel.$element, prependTextInfoPopup.$element, dropdown.$element, actionContainer.$element, prependTextInputField.$element); nominationToggle.on('select',function {				if (nominationToggleOld.isSelected) {					$('.newnomonly').hide;					if( discussionLinkInputField.getValue.trim ) discussionLinkInputField.emit('change');				}				else if (nominationToggleNew.isSelected) {					$('.newnomonly').show;					if ( newNomHeaderInputField.getValue.trim ) newNomHeaderInputField.emit('change');				}			}); if (nominationToggleOld.isSelected) { if (discussionLinkInputField.getValue.match(/^Wikipedia:Categories for discussion\/Log\/\d\d\d\d \w+ \d\d?#(.+)$/)) { sectionName = discussionLinkInputField.getValue.trim; }			}			else if (nominationToggleNew.isSelected) { sectionName = newNomHeaderInputField.getValue.trim; }			// helper function, makes ore accurate. function replaceLastOccurrence(str, find, replace) { let index = str.lastIndexOf(find); if (index >= 0) { return str.substring(0, index) + replace + str.substring(index + find.length); } else { return str; }			}		   var sectionName = sectionName || 'sectionName'; var oldSectionName = sectionName; discussionLinkInputField.on('change',function {				if (discussionLinkInputField.getValue.match(/^Wikipedia:Categories for discussion\/Log\/\d\d\d\d \w+ \d\d?#(.+)$/)) {					oldSectionName = sectionName;					sectionName = discussionLinkInputField.getValue.replace(/^Wikipedia:Categories for discussion\/Log\/\d\d\d\d \w+ \d\d?#(.+)$/, '$1').trim;					var text = prependTextInputField.getValue;					text = replaceLastOccurrence(text, oldSectionName, sectionName);					prependTextInputField.setValue(text);				}			}); newNomHeaderInputField.on('change',function {				if ( newNomHeaderInputField.getValue.trim ) {					oldSectionName = sectionName;					sectionName = newNomHeaderInputField.getValue.trim;					var text = prependTextInputField.getValue;					text = replaceLastOccurrence(text, oldSectionName, sectionName);					prependTextInputField.setValue(text);				}			}); dropdown.on('labelChange',function {				switch (dropdown.getLabel) {					case "Delete":						prependTextInputField.setValue(`${sectionName}`);						actionInputField.setValue('deleting');						break;					case "Rename":						prependTextInputField.setValue(`$1`);						actionInputField.setValue('renaming');						break;					case "Merge":						prependTextInputField.setValue(`$1`);						actionInputField.setValue('merging');						break;					case "Split":						prependTextInputField.setValue(`$1`);						actionInputField.setValue('splitting');						break;					case "Listify":						prependTextInputField.setValue(`$1`);						actionInputField.setValue('listifying');						break;					case "Custom":						prependTextInputField.setValue(`${sectionName}`);						actionInputField.setValue(''); // blank it as a precaution						break;				}			});

var titleListObj = createTitleAndInputField('List of titles (one per line,  prefix is optional)', 'Title1\nTitle2\nTitle3', info='You can specify targets by adding a pipe   and then the target, e.g.  . These targets can be used in the category tagging step.'); var titleList = titleListObj.container; var titleListInputField = titleListObj.inputField; elementsToDisable.push(titleListInputField); if (!first) { var removeButton = createRemoveBatchButton; elementsToDisable.push(removeButton); removeButton.on('click',function {					container.$element.remove;					// filter based on the container element					actions = actions.filter(function(item) { return item.container !== container; });					// Reset labels					for (i=0; i<actions.length;i++) {						actions[i].container.setLabel('Action batch #'+(i+1));						actions[i].label = 'Action batch #'+(i+1);					}				}); container.addItems([removeButton, prependTextContainer, titleList]);

} else { container.addItems([prependTextContainer, titleList]); }		   return { titleListInputField: titleListInputField, prependTextInputField: prependTextInputField, label: 'Action batch #'+count, container: container, actionInputField: actionInputField };		}		var actionsContainer = $(' '); bodyContent.append(actionsContainer); var actions = []; actions.push(createActionNomination(actionsContainer, first=true));

var checkboxObj = createCheckboxWithLabel('Notify users?'); var notifyCheckbox = checkboxObj.checkbox; elementsToDisable.push(notifyCheckbox); var checkboxFieldlayout = checkboxObj.fieldlayout; checkboxFieldlayout.$element.css('margin-bottom', '10px'); bodyContent.append(checkboxFieldlayout.$element); var multiOptionButton = createMultiOptionButton; elementsToDisable.push(multiOptionButton); multiOptionButton.$element.css('margin-bottom', '10px'); bodyContent.append(multiOptionButton.$element); bodyContent.append(' '); multiOptionButton.on('click', => {	    	actions.push( createActionNomination(actionsContainer) );	    }); var categoryTemplateDropdownObj = makeCategoryTemplateDropdown('Category template:'); categoryTemplateDropdownContainer = categoryTemplateDropdownObj.container; categoryTemplateDropdown = categoryTemplateDropdownObj.dropdown; categoryTemplateDropdown.$element.css(	   	{	    		'display': 'inline-block',	    		'max-width': 'fit-content',	    		'margin-bottom': '10px'	    	}	    ); elementsToDisable.push(categoryTemplateDropdown); if ( nominationToggleOld.isSelected ) categoryTemplateDropdownContainer.$element.hide; bodyContent.append(categoryTemplateDropdownContainer.$element); var startButton = createStartButton; elementsToDisable.push(startButton); bodyContent.append(startButton.$element);

startButton.on('click', function {	   	var isOld = nominationToggleOld.isSelected;	    	var isNew = nominationToggleNew.isSelected;	    	// First check elements	    	var error = false;	    	var regex = /^Wikipedia:Categories for discussion\/Log\/\d\d\d\d \w+ \d\d?#.+$/;	    	if (isOld) {		    	if ( !(discussionLinkInputField.getValue.trim) || !regex.test(discussionLinkInputField.getValue.trim) ) {		    		discussionLinkInputField.setValidityFlag(false);		    		error = true;		    	} else {		    		discussionLinkInputField.setValidityFlag(true);		    	}	    	} else if (isNew) {	    		if ( !(newNomHeaderInputField.getValue.trim) ) {		    		newNomHeaderInputField.setValidityFlag(false);		    		error = true;	    		} else {	    			newNomHeaderInputField.setValidityFlag(true);	    		}	    		if ( !(rationaleInputField.getValue.trim) ) {		    		rationaleInputField.setValidityFlag(false); error = true; } else { rationaleInputField.setValidityFlag(true); }	   	}	    	batches = actions.map(function ({titleListInputField, prependTextInputField, label, actionInputField}) {	    		if ( !(prependTextInputField.getValue.trim) ) {		    		prependTextInputField.setValidityFlag(false);		    		error = true;		    	} else {		    		prependTextInputField.setValidityFlag(true);		    	}		    	if (isNew) {		    		if ( !(actionInputField.getValue.trim) ) {			    		actionInputField.setValidityFlag(false);			    		error = true;			    	} else {			    		actionInputField.setValidityFlag(true);			    	}		    	}		    	if ( !(titleListInputField.getValue.trim) ) {		    		titleListInputField.setValidityFlag(false);		    		error = true;		    	} else {		    		titleListInputField.setValidityFlag(true);		    	}		    	// Retreive titles, handle dups	            var titles = {};			    var titleList = titleListInputField.getValue.split('\n');			    function capitalise(s) { return s[0].toUpperCase + s.slice(1); }				function normalise(title) { return 'Category:' + capitalise(title.replace(/^ *[Cc]ategory:/, '').trim); }			   titleList.forEach(function(title) {	                if (title) {	                	var targets = title.split('|');	                	var newTitle = targets.shift;	                	newTitle = normalise(newTitle);	                	if (!Object.keys(titles).includes(newTitle) ) {	                    	titles[newTitle] = targets.map(normalise);	                	}	                 }	            }); if ( !(Object.keys(titles).length) ) { titleListInputField.setValidityFlag(false); error = true; } else { titleListInputField.setValidityFlag(true); }		   	return { titles: titles, prependText: prependTextInputField.getValue.trim, label: label, actionInputField: actionInputField };	   	});

if (error) { return; }

for (let element of elementsToDisable) { element.setDisabled(true); }			$('.remove-batch-button').remove; var abortButton = createAbortButton; bodyContent.append(abortButton.$element); window.abortEdits = false; // initialise abortButton.on('click', function {			 // Set abortEdits flag to true			  if (confirm('Are you sure you want to abort?')) {			   	  abortButton.setDisabled(true);			      window.abortEdits = true;			  }			}); var allTitles = batches.reduce((allTitles, obj) => {			   return allTitles.concat(Object.keys(obj.titles));			}, []); createAuthorList(allTitles).then(function(authors) {

function processContent(content, titles, textToModify, summary, type, doneMessage, headingLabel) { if (!Array.isArray(titles)) { var titlesDict = titles; titles = Object.keys(titles); }					var fieldset = createFieldset(headingLabel); content.append(fieldset.$element); var progressElement = createProgressElement; fieldset.addItems([progressElement]); var ratelimitMessage = createRatelimitMessage; ratelimitMessage.toggle(false); fieldset.addItems([ratelimitMessage]); var progressObj = createProgressBar(`(0 / ${titles.length}, 0 errors)`); // with label var progress = progressObj.progressBar; var progressContainer = progressObj.fieldlayout; // Add margin or padding to the progress bar widget progress.$element.css('margin-top', '5px'); progress.pushPending; fieldset.addItems([progressContainer]); let resolvedCount = 0; let rejectedCount = 0;

function updateCounter { progressContainer.setLabel(`(${resolvedCount} / ${titles.length}, ${rejectedCount} errors)`); }					function updateProgress { var percentage = (resolvedCount + rejectedCount) / titles.length * 100; progress.setProgress(percentage); }					function trackPromise(promise) { return new Promise((resolve, reject) => {					       promise					            .then(value => { resolvedCount++; updateCounter; updateProgress; resolve(value); })					           .catch(error => { rejectedCount++; updateCounter; updateProgress; resolve(error); });					   });					}					return new Promise(async function(resolve) {						var promises = [];						for (const title of titles) {						  var promise = editPage(title, textToModify, summary, progressElement, ratelimitMessage, progress, type, titlesDict);							  promises.push(trackPromise(promise));							  await sleep(100); // space out calls							  await massCFDratelimitPromise; // stop if ratelimit reached (global variable)						}						Promise.allSettled(promises)						  .then(function { progress.toggle(false); if (window.abortEdits) { var abortMessage = createAbortMessage; abortMessage.setLabel( $(' Edits manually aborted. Revert? ') ); content.append(abortMessage.$element); } else { var completedElement = createCompletedElement; completedElement.setLabel(doneMessage); completedElement.$element.css('margin-bottom', '16px'); content.append(completedElement.$element); }						   resolve; })						 .catch(function(error) { console.error("Error occurred during title processing:", error); resolve; });					});				}				const date = new Date;

const year = date.getUTCFullYear; const month = date.toLocaleString('default', { month: 'long', timeZone: 'UTC' }); const day = date.getUTCDate; var summaryDiscussionLink; var discussionPage = `Wikipedia:Categories for discussion/Log/${year} ${month} ${day}`; if (isOld) summaryDiscussionLink = discussionLinkInputField.getValue.trim; else if (isNew) summaryDiscussionLink = `${discussionPage}#${newNomHeaderInputField.getValue.trim}`; const advSummary = ' (via script)'; const categorySummary = 'Tagging page for ' +summaryDiscussionLink+'' + advSummary; const userSummary = 'Notifying user about ' +summaryDiscussionLink+'' + advSummary; const userNotification = ''+summaryDiscussionLink+' ~'; const nominationSummary = `Adding mass nomination at ${advSummary}`; var batchesToProcess = []; var newNomPromise = new Promise(function (resolve) {					if (isNew) {						nominationText = `==== ${newNomHeaderInputField.getValue.trim} ====\n`;						for (const batch of batches) {							var action = batch.actionInputField.getValue.trim;							for (const category of Object.keys(batch.titles)) {								var targets = batch.titles[category].slice; // copy array								var targetText = ;								if (targets.length) {									if (targets.length === 2) {										targetText = ` to ${targets[0]} and ${targets[1]}`;									}									else if (targets.length > 2) {										var lastTarget = targets.pop;										targetText = ' to ' + targets.join(', ') + ', and ' + lastTarget + ;									} else { // 1 target										targetText = ' to ' + targets[0] + '';									}								}								nominationText +=`:* Propose ${action} ${targetText}\n`;							} }						var rationale = rationaleInputField.getValue.trim.replace(/\n/, ' '); nominationText += `:Nominator's rationale: ${rationale} ~`; var newText; var nominationRegex = /==== ?NEW NOMINATIONS ?====\s*(?:)?/; getWikitext(discussionPage).then(function(wikitext) {							if ( !wikitext.match(nominationRegex) ) {								var nominationErrorMessage = createNominationErrorMessage;								bodyContent.append(nominationErrorMessage.$element);							} else {								newText = wikitext.replace(nominationRegex, '$&\n\n'+nominationText); // $& contains all the matched text								batchesToProcess.push({ content: bodyContent, titles: [discussionPage], textToModify: newText, summary: nominationSummary, type: 'text', doneMessage: 'Nomination added', headingLabel: 'Creating nomination' });								resolve;							}						}).catch(function (error) {						   console.error('An error occurred in fetching wikitext:', error);						    resolve;						}); } else resolve; });				newNomPromise.then(async function { batches.forEach(batch => {						batchesToProcess.push({ content: bodyContent, titles: batch.titles, textToModify: batch.prependText, summary: categorySummary, type: 'prepend', doneMessage: 'All categories edited.', headingLabel: 'Editing categories' + ((batches.length > 1) ? ' — '+batch.label : '') });				   });				    if (notifyCheckbox.isSelected) { batchesToProcess.push({							content: bodyContent,							titles: authors,							textToModify: userNotification,							summary: userSummary,							type: 'append',							doneMessage: 'All users notified.',							headingLabel: 'Notifying users'						}); }				   let promise = Promise.resolve; // abort handling is now only in the editPage function for (const batch of batchesToProcess) { await processContent(...Object.values(batch)); }					promise.then( => {				   	abortButton.setLabel('Revert');				    	// All done					}).catch(err => {					    console.error('Error occurred:', err);					}); });		   });	    });    });   } }

// Run the script when the page is ready $(document).ready(runMassCFD); //