User:Qwerfjkl/scripts/massCFD.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.
// <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 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('<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.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) ? $('<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 = 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( $('<span>' + makeLink(title) + ' edited successfully</span><span class="masscfdundo" data-revid="'+data.edit.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();
		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++) {
						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 = $('<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 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( $('<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.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}''' {{${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>