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 – {{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: $(`${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', '{{subst:Cfd|Category:Bishops}}', info='A dollar sign $ followed by a number, such as $1, 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, Category: prefix is optional)', 'Title1\nTitle2\nTitle3', info='You can specify targets by adding a pipe | and then the target, e.g. Category:Example|Category:Target1|Category:Target2. 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[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 = '{{ 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/, '
');

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

//