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.

// <nowiki>

// 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: $( `<p>${info}</p>` ),

			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 – {{lc}}'

			},

			{

				data: 'clc',

				label: 'Category link with count – {{clc}}'

			},

			{

				data: 'cl',

				label: 'Plain category link – {{cl}}'

			}

		

	} );

	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: $(`<span>${title}</span>`)

  });

  

  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 `<a href="/wiki/${title}" target="_blank">${title}</a>`;

}



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 pages0].revisions0].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('<span><span id="revertall-text">Reverting...</span> (<span id="revertall-done">0</span> / <span id="revertall-total">'+revertElements.length+'</span> done)</span>');



		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. <a href="/wiki/Special:Diff/'+revid+'">Click here</a> 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. && data..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 = pagespageId].revisions;

    if (revisions && revisions.length > 0) {



      return revisions0].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) ? $('<span>').text('Retrying ').append($(makeLink(title))) : $('<span>').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 = titlesDicttitle];



     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, targetsi]);

    }

    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. && data..result === 'Success') {

	        messageElement.setType('success');

	        messageElement.setLabel( $('<span>' + makeLink(title) + ' edited successfully</span><span class="masscfdundo" data-revid="'+data..newrevid+'" data-title="'+title+'"></span>') );



	        resolve();

	    } else {

	        

	    	messageElement.setType('error');

	        messageElement.setLabel( $('<span>Error occurred while editing ' + makeLink(title) + ': '+ data + '</span>') );

	        console.error('Error occurred while prepending text to page:', data);



	        reject();

	    }

	  }).catch(function(error) {

	  	messageElement.setType('error');

	    messageElement.setLabel( $('<span>Error occurred while editing ' + makeLink(title) + ': '+ error + '</span>') );

	    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:', '[[WP:DEFINING|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', '{{subst:Cfd|Category:Bishops}}', info='A dollar sign <code>$</code> followed by a number, such as <code>$1</code>, 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(`{{subst:Cfd|${sectionName}}}`);

						actionInputField.setValue('deleting');

						break;

					case "Rename":

						prependTextInputField.setValue(`{{subst:Cfr|$1|${sectionName}}}`);

						actionInputField.setValue('renaming');

						break;

					case "Merge":

						prependTextInputField.setValue(`{{subst:Cfm|$1|${sectionName}}}`);

						actionInputField.setValue('merging');

						break;

					case "Split":

						prependTextInputField.setValue(`{{subst:Cfs|$1|$2|${sectionName}}}`);

						actionInputField.setValue('splitting');

						break;

					case "Listify":

						prependTextInputField.setValue(`{{subst:Cfl|$1|${sectionName}}}`);

						actionInputField.setValue('listifying');

						break;

					case "Custom":

						prependTextInputField.setValue(`{{subst:Cfd|type=|${sectionName}}}`);

						actionInputField.setValue(''); // blank it as a precaution

						break;

				}

			});

			



			

				

		    var titleListObj = createTitleAndInputField('List of titles (one per line, <code>Category:</code> prefix is optional)', 'Title1\nTitle2\nTitle3', info='You can specify targets by adding a pipe <code>|</code> and then the target, e.g. <code>Category:Example|Category:Target1|Category:Target2</code>. 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++) {

						actionsi].container.setLabel('Action batch #'+(i+1));

						actionsi].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 = $('<div />');

		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('<br />');

	    

	    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 s0].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) ) {

	                    	titlesnewTitle = 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( $('<span>Edits manually aborted. <a id="masscfdrevertlink" onclick="revertEdits()">Revert?</a></span>') );

						

						    	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 = ' ([[User:Qwerfjkl/scripts/massCFD.js|via script]])';

				const categorySummary = 'Tagging page for [[' +summaryDiscussionLink+']]' + advSummary;

			    const userSummary = 'Notifying user about [[' +summaryDiscussionLink+']]' + advSummary;

			    const userNotification = '{{ subst:Cfd mass notice |'+summaryDiscussionLink+'}} ~~~~';

				const nominationSummary = `Adding mass nomination at [[#${newNomHeaderInputField.getValue().trim()}]]${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.titlescategory].slice(); // copy array

								var targetText = '';

								if (targets.length) {

									if (targets.length === 2) {

										targetText = ` to [[:${targets0}]] and [[:${targets1}]]`;

									}

									else if (targets.length > 2) {

										var lastTarget = targets.pop();

										targetText = ' to [[:' + targets.join(']], [[:') + ']], and [[:' + lastTarget + ']]';

									} else { // 1 target

										targetText = ' to [[:' + targets0 + ']]';

									}

								}

								nominationText +=`:* '''Propose ${action}''' {{${categoryTemplateDropdown.getValue()}|${category}}}${targetText}\n`;

								

							}

						}

						var rationale = rationaleInputField.getValue().trim().replace(/\n/, '<br />');

						nominationText += `:'''Nominator's rationale:''' ${rationale} ~~~~`;

						var newText;

						var nominationRegex = /==== ?NEW NOMINATIONS ?====\s*(?:<!-- ?Please add the newest nominations below this line ?-->)?/;

						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);

// </nowiki>