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



// update normalise function for CfD - use mw.Title()

function capitalise(s) {

    return s0].toUpperCase() + s.slice(1);

}



var XFDconfig = {

    "CFD": {

        "title": "Mass CfD",

        "placeholderDiscussionLink": 'Wikipedia:Categories for discussion/Log/2023 July 23#Category:Archaeological cultures by ethnic group',

        "placeholderNominationTitle": 'Archaeological cultures by ethnic group',

        "placeholderRationale": '[[WP:DEFINING|Non-defining]] category.',

        "pageDemoText": "{{subst:Cfd|Category:Bishops}}",

        "discussionLinkRegex": /^Wikipedia:Categories for discussion\/Log\/\d\d\d\d \w+ \d\d?#(.+)$/,

        "nominationReplacement": /==== ?NEW NOMINATIONS ?====\s*(?:<!-- ?Please add the newest nominations below this line ?-->)?/, '$&\n\n${nominationText}'],

        "userNotificationTemplate": 'Cfd mass notice',

        "baseDiscussionPage": 'Wikipedia:Categories for discussion/Log/',

        "normaliseFunction": (title) => { return 'Category:' + capitalise(title.replace(/^ *[Cc]ategory:/, '').trim()); },

        "actions": {

            "Delete": {

                'prepend': '{{subst:Cfd|${sectionName}}}',

                'action': 'deleting'

            },

            "Rename": {

                'prepend': '{{subst:Cfr|$1|${sectionName}}}',

                'action': 'renaming'

            },

            "Merge": {

                'prepend': '{{subst:Cfm|$1|${sectionName}}}',

                'action': 'merging'

            },

            "Split": {

                'prepend': '{{subst:Cfs|$1|$2|${sectionName}}}',

                'action': 'splitting'

            },

            "Listify": {

                'prepend': '{{subst:Cfl|$1|${sectionName}}}',

                'action': 'listifying'

            },

            "Custom": {

                'prepend': '{{subst:Cfd|type=|${sectionName}}}',

                'action': ''

            },

        },

        "displayTemplates": [{

            data: 'lc',

            label: 'Category link with extra links – {{lc}}'

        },

        {

            data: 'clc',

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

        },

        {

            data: 'cl',

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

        }],





    },

    "RFD": {

        "title": "Mass RfD",

        "placeholderDiscussionLink": 'Wikipedia:Redirects for discussion/Log/2024 May 13#Knightfall (comics)',

        "placeholderNominationTitle": 'Knightfall',

        "placeholderRationale": 'No mention of "Knightfall" in the target article.',

        "pageDemoText": "",

        "discussionLinkRegex": /^Wikipedia:Redirects for discussion\/Log\/\d\d\d\d \w+ \d\d?#(.+)$/,

        "nominationReplacement": /<!-- ?Add new entries directly below this line\. ?-->/, '$&\n${nominationText}\n'],

        "userNotificationTemplate": 'Rfd mass notice',

        "baseDiscussionPage": 'Wikipedia:Redirects for discussion/Log/',

        "normaliseFunction": (title) => { return new mw.Title(title).getPrefixedText() },

        "actions":

        {

            'prepend': '{{subst:rfd|${sectionName}|content=\n${pageText}\n}}'

        },

        "displayTemplate": "{{subst:rfd2|multi=yes|redirect=${pageName}|target=${redirectTarget}}}"

    }

}

const match = /Special:Mass(\w+)/.exec(mw.config.get('wgPageName'))

const XFD = match ? match1].toUpperCase() : false

const config = XFDconfigXFD



function wipePageContent() {

    var bodyContent = $('#bodyContent');

    if (bodyContent) {

        bodyContent.empty();

    }

    var header = $('#firstHeading');

    if (header) {

        header.text(config.title);

    }

    $('title').text(`${config.title} - 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: config.displayTemplates

    });

    var fieldlayout = new OO.ui.FieldLayout(

        dropdown,

        {

            label,

            align: 'inline',

            classes: 'newnomonly'],

        }

    );

    return { container: fieldlayout, dropdown };

}



function createTitleAndInputFieldWithLabel(label, placeholder, classes = []) {

    var input = new OO.ui.TextInputWidget({

        placeholder

    });





    var fieldset = new OO.ui.FieldsetLayout({

        classes

    });



    fieldset.addItems([

        new OO.ui.FieldLayout(input, {

            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,

        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,

        inputField,

        container,

        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,

        indicator: 'required'

    });



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



    return {

        titleLabel,

        inputField,

        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',

        selected: true

    });

    var oldNomToggle = new OO.ui.ButtonOptionWidget({

        data: 'old',

        label: 'Old nomination',

    });



    var toggle = new OO.ui.ButtonSelectWidget({

        items: 

            newNomToggle,

            oldNomToggle

        

    });

    return {

        toggle,

        newNomToggle,

        oldNomToggle,

    };

}





function createMessageElement() {

    var messageElement = new OO.ui.MessageWidget({

        type: 'progress',

        inline: true,

        progressType: 'infinite'

    });

    return messageElement;

}



function createWarningMessage() {

    var warningMessage = new OO.ui.MessageWidget({

        type: 'warning',

        style: 'background-color: yellow;'

    });

    return warningMessage;

}



function createCompletedElement() {

    var messageElement = new OO.ui.MessageWidget({

        type: 'success',

    });

    return messageElement;

}



function createDoingElement() {

    var messageElement = new OO.ui.MessageWidget({

        type: 'info',

    });

    return messageElement;

}



function createAbortMessage() { // pretty much a duplicate of ratelimitMessage

    var abortMessage = new OO.ui.MessageWidget({

        type: 'warning',

    });

    return abortMessage;

}



function createErrorMessage(text) {

    var errorMessage = new OO.ui.MessageWidget({

        type: 'error',

    });

    errorMessage.setLabel(text);

    return errorMessage;

}



function createNominationErrorMessage() { // pretty much a duplicate of ratelimitMessage

    return createErrorMessage('Could not detect where to add new nomination.')

}



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,

            align: 'inline',

            selected: true

        }

    );

    return {

        fieldlayout,

        checkbox

    };

}

function createMenuOptionWidget(data, label) {

    var menuOptionWidget = new OO.ui.MenuOptionWidget({

        data,

        label

    });

    return menuOptionWidget;

}

function createActionDropdown() {

    var items = Object.keys(config.actions)

        .map(action => action, action]) // [label, data]

        .map(action => createMenuOptionWidget(...action));



    var dropdown = new OO.ui.DropdownWidget({

        label: 'Mass action',

        menu: {

            items

        }

    });

    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 getDateDifference(date1) {

    const currentDate = new Date();

    // now

    let date2 = `${currentDate.getUTCFullYear()} ${currentDate.toLocaleString('en', { month: 'long', timeZone: 'UTC' })} ${currentDate.getUTCDate()}`



    // Parse the dates

    const parseDate = (dateString) => {

        const year, month, day = dateString.split(' ');

        return new Date(`${year}-${month}-${day}`);

    };



    const d1 = parseDate(date1);

    const d2 = parseDate(date2);



    // Calculate the time difference in milliseconds

    const timeDifference = Math.abs(d2 - d1);



    // Convert the time difference from milliseconds to days

    const dayDifference = Math.ceil(timeDifference / (1000 * 60 * 60 * 24));



    return dayDifference;

}



function deepCopy(obj) {

    if (obj === null || typeof obj !== 'object') {

        return obj;

    }



    if (obj instanceof OO.ui.Element) {

        return obj;

    }



    if (Array.isArray(obj)) {

        const copy = [];

        for (let i = 0; i < obj.length; i++) {

            copyi = deepCopy(obji]);

        }

        return copy;

    }



    const copy = {};

    for (const key in obj) {

        if (obj.hasOwnProperty(key)) {

            copykey = deepCopy(objkey]);

        }

    }

    return copy;

}



function parseHTML(html) {

    // Create a temporary div to parse the HTML

    var tempDiv = $('<div>').html(html);



    // Find all li elements

    var liElements = tempDiv.find('li');



    // Array to store extracted hrefs

    var hrefs = [];



    let existinghrefRegexp = /^https:\/\/en\.wikipedia.org\/wiki\/([^?&]+?)$/;

    let nonexistinghrefRegexp = /^https:\/\/en\.wikipedia\.org\/w\/index\.php\?title=([^&?]+?)&action=edit&redlink=1$/;



    // Iterate through each li element

    liElements.each(function () {

        // Find all anchor (a) elements within the current li

        let hrefline = [];

        var anchorElements = $(this).find('a');



        // Extract href attribute from each anchor element

        anchorElements.each(function () {

            var href = $(this).attr('href');

            if (href) {

                var existingMatch = existinghrefRegexp.exec(href);

                var nonexistingMatch = nonexistinghrefRegexp.exec(href);

                let page;

                if (existingMatch) page = new mw.Title(existingMatch1]);

                if (nonexistingMatch) page = new mw.Title(nonexistingMatch1]);

                if (page && page.getNamespaceId() > -1 && !page.isTalkPage()) {

                    hrefline.push(page.getPrefixedText());

                }





            }

        });

        hrefs.push(hrefline);

    });



    return hrefs;

}



function handlepaste(widget, e) {

    var types, pastedData, parsedData;

    // Browsers that support the 'text/html' type in the Clipboard API (Chrome, Firefox 22+)

    if (e && e.clipboardData && e.clipboardData.types && e.clipboardData.getData) {

        // Check for 'text/html' in types list

        types = e.clipboardData.types;

        if (((types instanceof DOMStringList) && types.contains("text/html")) ||

            ($.inArray && $.inArray('text/html', types) !== -1)) {

            // Extract data and pass it to callback

            pastedData = e.clipboardData.getData('text/html');



            parsedData = parseHTML(pastedData);



            // Check if it's an empty array

            if (!parsedData || parsedData.length === 0) {

                // Allow the paste event to propagate for plain text or empty array

                return true;

            }

            let confirmed = confirm('You have pasted formatted text. Do you want this to be converted into wikitext?');

            if (!confirmed) return true;

            processPaste(widget, pastedData);



            // Stop the data from actually being pasted

            e.stopPropagation();

            e.preventDefault();

            return false;

        }

    }



    // Allow the paste event to propagate for plain text

    return true;

}



function waitForPastedData(widget, savedContent) {

    // If data has been processed by the browser, process it

    if (widget.getValue() !== savedContent) {

        // Retrieve pasted content via widget's getValue()

        var pastedData = widget.getValue();



        // Restore saved content

        widget.setValue(savedContent);



        // Call callback

        processPaste(widget, pastedData);

    }

    // Else wait 20ms and try again

    else {

        setTimeout(function () {

            waitForPastedData(widget, savedContent);

        }, 20);

    }

}



function processPaste(widget, pastedData) {

    // Parse the HTML

    var parsedArray = parseHTML(pastedData);

    let stringOutput = '';

    for (const pages of parsedArray) {

        stringOutput += pages.join('|') + '\n';

    }

    widget.insertContent(stringOutput);

}





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 - this is hacky, and potentially unreliable

function revertEdits() {

    var revertAllCount = 0;

    var revertElements = $('.massxfdundo');

    if (!revertElements.length) {

        $('#massxfdrevertlink').replaceWith('Reverts done.');

    } else {

        $('#massxfdrevertlink').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,

        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 getRedirectData(titles) {

    var api = new mw.Api();

    return api.get({

        action: 'query',

        titles,

        redirects: 1,

        format: 'json'

    }).then(function (data) {

        return data.query;

    }).catch(function (error) {

        console.error('Error occurred while fetching page author:', error);

        return false;

    });

}



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

async function createAuthorList(titles) {

    console.log(titles)

    var authorList = [];

    var promises = titles.map(function (title) {

        return getPageAuthor(title);

    });

    try {

        const authors = await Promise.all(promises);

        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 => {

                        console.log(user)

                        if (user

                            && (!user.blockexpiry || user.blockexpiry !== "infinite" || 'blockpartial' in user)

                            && !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 (error_1) {

        console.error('Error occurred while creating author list:', error_1);

        return authorList;

    }

}



// Function to create a list of page authors and filter duplicates

async function createRedirectTargetsList(titles) {

    try {

        let queryBatchSize = 50;

        let redirectTitles = titles.map(title => title.replace(/ /g, '_')); // Replace spaces with underscores

        let redirectTargets = {};

        let nonredirects = [];

        for (let i = 0; i < redirectTitles.length; i += queryBatchSize) {

            let batch = redirectTitles.slice(i, i + queryBatchSize);

            let batchTitles = batch.join('|');



            await getRedirectData(batchTitles)

                .then(data => {



                    if ('redirects' in data) {

                        data.redirects.forEach(redirect => {

                            redirectTargetsredirect.from = redirect.to

                        });

                        let redirects = new Set(data.redirects.map(r => r.to))

                        let pages = new Set(Object.values(data.pages).map(p => p.title));

                        nonredirects.push(...[...pages].filter(x => !redirects.has(x)))

                    } else {

                        nonredirects.push(...Object.values(data.pages).map(p => p.title))

                    }



                })

                .catch(error => {

                    console.error("Error querying API:", error);

                });

        }

        return redirectTargets, nonredirects];

    } catch (error_1) {

        console.error('Error occurred while fetching redirect targets', error_1);

        return redirectTargets, nonredirects];

    }

}



function editPage(options) {

    const localOptions = deepCopy(options);

    console.log(localOptions);

    localOptions.text = localOptions.textToModify;

    const api = new mw.Api();

    const messageElement = createMessageElement();



    messageElement.setLabel((localOptions.retry)

        ? $('<span>').text('Retrying ').append($(makeLink(localOptions.title)))

        : $('<span>').text('Editing ').append($(makeLink(localOptions.title))));



    localOptions.progressElement.$element.append(messageElement.$element);

    const container = $('.sticky-container');

    container.scrollTop(container.prop("scrollHeight"));



    if (localOptions.retry) {

        sleep(1000);

    }



    const requestData = {

        action: 'edit',

        title: window.debuggingMode ? 'User:Qwerfjkl/sandbox/51' : localOptions.title,

        summary: localOptions.summary,

        format: 'json'

    };



    if (localOptions.type === 'prepend') {

        requestData.nocreate = 1;

        const targets = localOptions.titlesDictlocalOptions.title];



        for (let i = 0; i < targets.length; i++) {

            const placeholder = '$' + (i + 1);

            localOptions.text = localOptions.text.replace(placeholder, targetsi]);

        }

        localOptions.text = localOptions.text.replace(/\$\d/g, '');

        requestData.prependtext = localOptions.text.trim() + '\n\n';

    } else if (localOptions.type === 'append') {

        requestData.appendtext = '\n\n' + localOptions.text.trim();

    } else if (localOptions.type === 'text') {

        requestData.text = localOptions.text;

    }



    console.log(requestData);



    return new Promise((resolve, reject) => {

        if (window.abortEdits) {

            messageElement.toggle(false);

            resolve();

            return;

        }



        api.postWithEditToken(requestData)

            .then((data) => {

                if (data. && data..result === 'Success') {

                    messageElement.setType('success');

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

                    resolve();

                } else {

                    handleError('Error occurred while editing', data, localOptions, messageElement, resolve, reject);

                }

            })

            .catch((error) => handleError('Error occurred while editing', error, localOptions, messageElement, resolve, reject));

    });

}



function handleError(msg, error, options, messageElement, resolve, reject) {

    messageElement.setType('error');

    messageElement.setLabel($('<span>' + msg + ' ' + makeLink(options.title) + ': ' + error + '</span>'));

    console.error(msg + ' page:', error);



    if (error === 'editconflict') {

        editPage(deepCopy(options)).then(resolve);

    } else if (error === 'ratelimited') {

        options.progress.setDisabled(true);

        handleRateLimitError(options.ratelimitMessage).then(() => {

            options.progress.setDisabled(false);

            editPage(deepCopy(options)).then(resolve);

        });

    } else {

        reject();

    }

}





// global scope - needed to syncronise ratelimits

var massXFDratelimitPromise = 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 (massXFDratelimitPromise !== null) {

        return massXFDratelimitPromise;

    }



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

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

            massXFDratelimitPromise = null; // reset

            resolve();

        }, secondsToWait);

    });

    return massXFDratelimitPromise;

}



// 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,

        align: 'inline'

    });

    return {

        progressBar,

        fieldlayout

    };

}





// Main function to execute the script

async function runMassXFD() {



    Object.keys(XFDconfig).forEach(function (XfD) {

        mw.util.addPortletLink('p-tb', mw.util.getUrl(`Special:Mass${XfD}`), `Mass ${XfD}`, `pt-mass${XfD.toLowerCase()}`, `Create a mass ${XfD} nomination`);

    })





    if (XFD) {

        // Load the required modules

        mw.loader.using('oojs-ui').done(function () {

            wipePageContent();

            if (!window.debuggingMode) { // annoying when reloading for debugging

                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;

            }`); // should probably be styled directly on the element than via the stylesheet

            var nominationToggleObj = createNominationToggle();

            var nominationToggle = nominationToggleObj.toggle;



            bodyContent.append(nominationToggle.$element);

            elementsToDisable.push(nominationToggle);



            var rationaleObj = createTitleAndInputField('Rationale:', config.placeholderRationale);

            var rationaleContainer = rationaleObj.container;

            var rationaleInputField = rationaleObj.inputField;

            elementsToDisable.push(rationaleInputField);



            var nominationToggleOld = nominationToggleObj.oldNomToggle;

            var nominationToggleNew = nominationToggleObj.newNomToggle;



            var discussionLinkObj = createTitleAndSingleInputField('Discussion link', config.placeholderDiscussionLink);

            var discussionLinkContainer = discussionLinkObj.container;

            var discussionLinkInputField = discussionLinkObj.inputField;

            elementsToDisable.push(discussionLinkInputField);



            var newNomHeaderObj = createTitleAndSingleInputField('Nomination title', config.placeholderNominationTitle);

            var newNomHeaderContainer = newNomHeaderObj.container;

            var newNomHeaderInputField = newNomHeaderObj.inputField;

            elementsToDisable.push(newNomHeaderInputField);



            bodyContent.append(discussionLinkContainer.$element);

            bodyContent.append(newNomHeaderContainer.$element, rationaleContainer.$element);

            function displayElements() {

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



                }

            }

            displayElements();

            nominationToggle.on('select', displayElements);









            function createActionNomination(actionsContainer, first = false) {

                var count = actions.length + 1;

                let actionNominationTitle = XFD === 'CFD' ? 'Action batch #' + count : ''

                var container = createFieldset(actionNominationTitle);

                actionsContainer.append(container.$element);



                var actionDropdownObj = createActionDropdown();

                var dropdown = actionDropdownObj.dropdown;



                elementsToDisable.push(dropdown);

                dropdown.$element.css('max-width', 'fit-content');

                let demoText = config.pageDemoText

                var prependTextObj = createTitleAndInputField('Text to tag the nominated pages with:', demoText, 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()) {

                        if (XFD === 'CFD') $('.newnomonly').show();

                        if (newNomHeaderInputField.getValue().trim()) newNomHeaderInputField.emit('change');

                    }

                });



                if (nominationToggleOld.isSelected()) {

                    if (discussionLinkInputField.getValue().match(config.discussionLinkRegex)) {

                        sectionName = discussionLinkInputField.getValue().trim().match(config.discussionLinkRegex)[1];

                    }

                }

                else if (nominationToggleNew.isSelected()) {

                    sectionName = newNomHeaderInputField.getValue().trim();

                }



                // helper function, makes more accurate.

                function replaceOccurence(str, find, replace) {

                    if (XFD === 'CFD') {

                        // last occurence

                        let index = str.lastIndexOf(find);



                        if (index >= 0) {

                            return str.substring(0, index) + replace + str.substring(index + find.length);

                        } else {

                            return str;

                        }

                    } else if (XFD === 'RFD') {

                        if (str.toLowerCase().startsWith('{{subst:rfd|')) {

                            str = str.replace(/\{\{subst:rfd\|/i, '')

                            return '{{subst:rfd|' + str.replace(find, replace)

                        } else {

                            return str.replace(find, replace) // first occurence

                        }

                    }

                }



                var sectionName = sectionName || 'sectionName';

                var oldSectionName = sectionName;

                if (XFD !== 'CFD') {

                    prependTextInputField.setValue(config.actions.prepend.replace('${sectionName}', sectionName))

                    if (XFD === 'RFD') {

                        if (discussionLinkInputField.getValue().match(config.discussionLinkRegex)) {

                            let date = discussionLinkInputField.getValue().trim().match(/^Wikipedia:Redirects for discussion\/Log\/(\d\d\d\d \w+ \d\d)?#.+$/)[1

                            let difference = getDateDifference(date)

                            if (difference !== 0) {

                                prependTextInputField.setValue(config.actions.prepend.replace('{{subst:rfd|${sectionName}|', `{{subst:rfd|${sectionName}|days=${difference}|`))

                            } // else leave as default above

                        }

                    }

                }

                discussionLinkInputField.on('change', function () {

                    if (discussionLinkInputField.getValue().match(config.discussionLinkRegex)) {

                        oldSectionName = sectionName;

                        sectionName = discussionLinkInputField.getValue().replace(config.discussionLinkRegex, '$1').trim();

                        var text = prependTextInputField.getValue();



                        if (XFD === 'RFD') {

                            const date = discussionLinkInputField.getValue().trim().match(/^Wikipedia:Redirects for discussion\/Log\/(\d\d\d\d \w+ \d\d?)#.+$/)[1



                            if (/(\| *days *= *)\d+/.test(text)) { // already has days=, update

                                text = text.replace(/(\| *days *= *)\d+/, '$1' + getDateDifference(date))

                                text = replaceOccurence(text, oldSectionName, sectionName);

                            } else {

                                text = replaceOccurence(text, oldSectionName, sectionName + '|days=' + getDateDifference(date));

                            }

                        } else text = replaceOccurence(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 = replaceOccurence(text, oldSectionName, sectionName);

                        prependTextInputField.setValue(text);

                    }

                });



                dropdown.on('labelChange', function () {

                    let actionData = config.actionsdropdown.getLabel()];

                    prependTextInputField.setValue(actionData.prepend.replace('${sectionName}', sectionName));

                    actionInputField.setValue(actionData.action);

                });









                var titleListObj = createTitleAndInputField(`List of titles (one per line${XFD === 'CFD' ? ', <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>Example|Target1|Target2</code>. These targets can be used in the tagging step.');

                var titleList = titleListObj.container;

                var titleListInputField = titleListObj.inputField;

                var titleListInfoPopup = titleListObj.infoPopup;

                elementsToDisable.push(titleListInputField);

                let handler = handlepaste.bind(this, titleListInputField);

                let textInputElement = titleListInputField.$element.get(0);

                // Modern browsers. Note: 3rd argument is required for Firefox <= 6

                if (textInputElement.addEventListener) {

                    textInputElement.addEventListener('paste', handler, false);

                }

                // IE <= 8

                else {

                    textInputElement.attachEvent('onpaste', handler);

                }





                titleListObj.inputField.$element.on('paste', handlepaste);



                if (XFD !== 'CFD') {

                    // most XfDs don't need multiple actions, they're just delete. so hide unnecessary elements'

                    actionContainer.$element.hide();

                    dropdown.$element.hide();

                    prependTextInfoPopup.$element.hide() // both popups give info about targets which aren't relevant here

                    titleListInfoPopup.$element.hide()

                }





                if (!first && XFD !== 'CFD') {

                    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,

                    prependTextInputField,

                    label: 'Action batch #' + count,

                    container,

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

            });

            if (XFD !== 'CFD') {

                multiOptionButton.$element.hide()

            } else {



                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', async function () {



                var isOld = nominationToggleOld.isSelected();

                var isNew = nominationToggleNew.isSelected();

                // First check elements

                var error = false;

                var regex = config.discussionLinkRegex;

                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()) || (XFD === 'RFD' && !prependTextInputField.getValue().includes('${pageText}'))) {

                        prependTextInputField.setValidityFlag(false);

                        error = true;

                    } else {

                        prependTextInputField.setValidityFlag(true);



                    }



                    if (isNew && XFD === 'CFD') {

                        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 normalise(title) {

                        return config.normaliseFunction(title)

                    }

                    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,

                        prependText: prependTextInputField.getValue().trim(),

                        label,

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

                }, []);





                if (XFD === 'RFD') {

                    let fetchingRedirectsElement = createDoingElement();

                    fetchingRedirectsElement.setLabel('Fetching redirect targets...')

                    fetchingRedirectsElement.$element.css('margin-top', '16px');

                    bodyContent.append(fetchingRedirectsElement.$element);



                    let fetchedRedirectsElement = createCompletedElement();

                    fetchedRedirectsElement.setLabel('Fetched redirect targets')

                    fetchedRedirectsElement.$element.css('margin-top', '16px');



                    var redirectTargets, nonredirects = await createRedirectTargetsList(allTitles);

                    // window.batches=batches

                    batches0].titles = Object.keys(batches0].titles)

                        .filter(x => !nonredirects.includes(x))

                        .reduce((acc, curr) => {

                        acccurr = [];

                        return acc;

                        }, {});







                    console.log(batches)

                    if (!Object.keys(redirectTargets).length) {

                        var errorMessageElement = createErrorMessage('None of the titles are redirects, aborting.');

                        bodyContent.append(errorMessageElement.$element);

                        return;

                    }

                    console.log(nonredirects)

                    if (nonredirects.length) {

                        let nonredirectsWarningMessage = createWarningMessage();

                        nonredirectsWarningMessage.$element.css({ 'max-height': '20em', 'overflow-y': 'auto' }) // normally shouldn't be needed

                        let nonRedirectsHTML = $('<div>').append($('<span>').text('The following pages were ignored because they are not redirects:'))

                        let $listElement = $('<ul>')

                        nonredirects.forEach(item => {

                            const $listItem = $('<li>').html(makeLink(item));

                            $listElement.append($listItem);

                        });

                        nonRedirectsHTML.append($listElement)

                        nonredirectsWarningMessage.setLabel(nonRedirectsHTML)

                        bodyContent.append(nonredirectsWarningMessage.$element)

                    }



                    fetchingRedirectsElement.$element.hide();

                    bodyContent.append(fetchedRedirectsElement.$element);

                }

                console.log(redirectTargets);





                let fetchingAuthorsElement = createDoingElement();

                fetchingAuthorsElement.setLabel('Fetching authors...')

                fetchingAuthorsElement.$element.css('margin-top', '16px');

                bodyContent.append(fetchingAuthorsElement.$element);



                let fetchedAuthorsElement = createCompletedElement();

                fetchedAuthorsElement.setLabel('Fetched authors')

                fetchedAuthorsElement.$element.css('margin-top', '16px');

                let authors;

                if (redirectTargets) {

                    authors = await createAuthorList(Object.keys(redirectTargets));

                } else {

                    authors = await createAuthorList(allTitles);

                }



                fetchingAuthorsElement.$element.hide();

                bodyContent.append(fetchedAuthorsElement.$element);





                async function processContent(options) {

                    function getKeyByValue(object, value) {

                        return Object.keys(object).find(key => objectkey === value);

                    }



                    console.log(options);

                    console.log(options.titles);



                    if (!Array.isArray(options.titles)) {

                        options.titlesDict = options.titles;

                        options.titles = Object.keys(options.titles);

                    } else {

                        options.titlesDict = {};

                    }



                    const fieldset = createFieldset(options.headingLabel);

                    bodyContent.append(fieldset.$element);



                    options.progressElement = createProgressElement();

                    fieldset.addItems([options.progressElement]);



                    options.ratelimitMessage = createWarningMessage();

                    options.ratelimitMessage.toggle(false);

                    fieldset.addItems([options.ratelimitMessage]);



                    const progressObj = createProgressBar(`(0 / ${options.titles.length}, 0 errors)`);

                    options.progress = progressObj.progressBar;

                    const progressContainer = progressObj.fieldlayout;

                    options.progress.$element.css('margin-top', '5px');

                    options.progress.pushPending();

                    fieldset.addItems([progressContainer]);



                    let resolvedCount = 0;

                    let rejectedCount = 0;



                    function updateCounter() {

                        progressContainer.setLabel(`(${resolvedCount} / ${options.titles.length}, ${rejectedCount} errors)`);

                    }



                    function updateProgress() {

                        const percentage = (resolvedCount + rejectedCount) / options.titles.length * 100;

                        options.progress.setProgress(percentage);

                    }



                    function trackPromise(promise) {

                        return new Promise((resolve) => {

                            promise

                                .then(value => {

                                    resolvedCount++;

                                    updateCounter();

                                    updateProgress();

                                    resolve(value);

                                })

                                .catch(error => {

                                    rejectedCount++;

                                    updateCounter();

                                    updateProgress();

                                    resolve(error);

                                });

                        });

                    }



                    const promises = [];

                    for (const title of options.titles) {

                        let data = deepCopy(options);

                        if (XFD === 'RFD' && data.type === 'prepend') {

                            const text = await getWikitext(title);

                            data.textToModify = data.textToModify.replace('${pageText}', text);

                            data.type = 'text';

                        }



                        if (data.id === 'rfd-notify-target') {

                            data.textToModify = data.textToModify.replace('${redirectTitle}', getKeyByValue(redirectTargets, new mw.Title(title).getSubjectPage().getPrefixedText()));

                        }



                        data.title = title;

                        console.log('Data:', data);



                        const promise = editPage(data);

                        promises.push(trackPromise(promise));



                        if (!window.abortEdits) await sleep(100); // space out calls - not needed if they're being rejected

                        await massXFDratelimitPromise; // stop if ratelimit reached (global variable)

                    }



                    await Promise.allSettled(promises);



                    options.progress.toggle(false);



                    if (window.abortEdits) {

                        const abortMessage = createAbortMessage();

                        const revertEditsLink = $('<a id="massxfdrevertlink">Revert?</a>');

                        revertEditsLink.on('click', revertEdits);

                        abortMessage.setLabel($('<span>').append('Edits manually aborted. ').append(revertEditsLink));

                        bodyContent.append(abortMessage.$element);

                    } else {

                        const completedElement = createCompletedElement();

                        completedElement.setLabel(options.doneMessage);

                        completedElement.$element.css('margin-bottom', '16px');

                        bodyContent.append(completedElement.$element);

                    }

                }







                const date = new Date();



                const year = date.getUTCFullYear();

                const month = date.toLocaleString('en', { month: 'long', timeZone: 'UTC' });

                const day = date.getUTCDate();



                var summaryDiscussionLink;

                var discussionPage = `${config.baseDiscussionPage}${year} ${month} ${day}`;



                if (isOld) summaryDiscussionLink = discussionLinkInputField.getValue().trim();

                else if (isNew) summaryDiscussionLink = `${discussionPage}#${newNomHeaderInputField.getValue().trim()}`;



                const advSummary = ' ([[User:Qwerfjkl/scripts/massXFD.js|via MassXfD.js]])';

                // WIP, not finished

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

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

                const userNotification = `{{ subst: ${config.userNotificationTemplate} | ${summaryDiscussionLink} }} ~~~~`;

                const nominationSummary = `Adding mass nomination at [[#${newNomHeaderInputField.getValue().trim()}]]` + advSummary;

                if (XFD === 'RFD') {

                    var redirectTargetNotification = `{{subst:Rfd notice|\${redirectTitle}|${newNomHeaderInputField.getValue().trim()}}}`

                    var redirectTargetNotificationSummary = `Notice of [[${summaryDiscussionLink}]]${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() || false;

                            for (const page of Object.keys(batch.titles)) {

                                if (XFD == 'CFD') {

                                    var targets = batch.titlespage].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()}|${page}}}${targetText}\n`;

                                } else {

                                    nominationText += config.displayTemplate.replaceAll('${pageName}', page).replaceAll('${redirectTarget}', redirectTargetspage]) + '\n';

                                }

                            }

                        }

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

                        nominationText += `${XFD === 'CFD' ? ":'''Nominator's rationale:''' " : ''}${rationale} ~~~~`;

                        var newText;



                        getWikitext(discussionPage).then(function (wikitext) {

                            if (!wikitext.match(config.nominationReplacement0])) {

                                var nominationErrorMessage = createNominationErrorMessage();

                                bodyContent.append(nominationErrorMessage.$element);

                            } else {

                                newText = wikitext.replace(...config.nominationReplacement).replace('${nominationText}', nominationText);

                                batchesToProcess.push({

                                    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({

                            titles: batch.titles,

                            textToModify: batch.prependText,

                            summary: categorySummary,

                            type: 'prepend',

                            doneMessage: 'All pages edited.',

                            headingLabel: 'Editing nominated pages' + ((batches.length > 1) ? ' — ' + batch.label : '')

                        });

                    });

                    if (XFD === 'RFD') {

                        batchesToProcess.push({

                            id: 'rfd-notify-target',

                            titles: Object.values(redirectTargets).map(title => {

                                let page = new mw.Title(title)

                                return page.getTalkPage().getPrefixedText()

                            }),

                            textToModify: redirectTargetNotification,

                            summary: redirectTargetNotificationSummary,

                            type: 'append',

                            doneMessage: 'All target talk pages notified.',

                            headingLabel: 'Notifying targets'

                        });

                    }

                    if (notifyCheckbox.isSelected()) {

                        batchesToProcess.push({

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

                        // alert(`starting batch ${batch.headingLabel}`)

                        await processContent(batch);

                        // alert(`batch ${batch.headingLabel} done`)

                    }



                    promise.then(() => {

                        abortButton.setLabel('Revert');

                        // All done

                    }).catch(err => {

                        console.error('Error occurred:', err);

                    });

                });



            });

        });

    }

}



// Run the script when the page is ready

$(document).ready(runMassXFD);

// </nowiki>
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



// update normalise function for CfD - use mw.Title()

function capitalise(s) {

    return s0].toUpperCase() + s.slice(1);

}



var XFDconfig = {

    "CFD": {

        "title": "Mass CfD",

        "placeholderDiscussionLink": 'Wikipedia:Categories for discussion/Log/2023 July 23#Category:Archaeological cultures by ethnic group',

        "placeholderNominationTitle": 'Archaeological cultures by ethnic group',

        "placeholderRationale": '[[WP:DEFINING|Non-defining]] category.',

        "pageDemoText": "{{subst:Cfd|Category:Bishops}}",

        "discussionLinkRegex": /^Wikipedia:Categories for discussion\/Log\/\d\d\d\d \w+ \d\d?#(.+)$/,

        "nominationReplacement": /==== ?NEW NOMINATIONS ?====\s*(?:<!-- ?Please add the newest nominations below this line ?-->)?/, '$&\n\n${nominationText}'],

        "userNotificationTemplate": 'Cfd mass notice',

        "baseDiscussionPage": 'Wikipedia:Categories for discussion/Log/',

        "normaliseFunction": (title) => { return 'Category:' + capitalise(title.replace(/^ *[Cc]ategory:/, '').trim()); },

        "actions": {

            "Delete": {

                'prepend': '{{subst:Cfd|${sectionName}}}',

                'action': 'deleting'

            },

            "Rename": {

                'prepend': '{{subst:Cfr|$1|${sectionName}}}',

                'action': 'renaming'

            },

            "Merge": {

                'prepend': '{{subst:Cfm|$1|${sectionName}}}',

                'action': 'merging'

            },

            "Split": {

                'prepend': '{{subst:Cfs|$1|$2|${sectionName}}}',

                'action': 'splitting'

            },

            "Listify": {

                'prepend': '{{subst:Cfl|$1|${sectionName}}}',

                'action': 'listifying'

            },

            "Custom": {

                'prepend': '{{subst:Cfd|type=|${sectionName}}}',

                'action': ''

            },

        },

        "displayTemplates": [{

            data: 'lc',

            label: 'Category link with extra links – {{lc}}'

        },

        {

            data: 'clc',

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

        },

        {

            data: 'cl',

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

        }],





    },

    "RFD": {

        "title": "Mass RfD",

        "placeholderDiscussionLink": 'Wikipedia:Redirects for discussion/Log/2024 May 13#Knightfall (comics)',

        "placeholderNominationTitle": 'Knightfall',

        "placeholderRationale": 'No mention of "Knightfall" in the target article.',

        "pageDemoText": "",

        "discussionLinkRegex": /^Wikipedia:Redirects for discussion\/Log\/\d\d\d\d \w+ \d\d?#(.+)$/,

        "nominationReplacement": /<!-- ?Add new entries directly below this line\. ?-->/, '$&\n${nominationText}\n'],

        "userNotificationTemplate": 'Rfd mass notice',

        "baseDiscussionPage": 'Wikipedia:Redirects for discussion/Log/',

        "normaliseFunction": (title) => { return new mw.Title(title).getPrefixedText() },

        "actions":

        {

            'prepend': '{{subst:rfd|${sectionName}|content=\n${pageText}\n}}'

        },

        "displayTemplate": "{{subst:rfd2|multi=yes|redirect=${pageName}|target=${redirectTarget}}}"

    }

}

const match = /Special:Mass(\w+)/.exec(mw.config.get('wgPageName'))

const XFD = match ? match1].toUpperCase() : false

const config = XFDconfigXFD



function wipePageContent() {

    var bodyContent = $('#bodyContent');

    if (bodyContent) {

        bodyContent.empty();

    }

    var header = $('#firstHeading');

    if (header) {

        header.text(config.title);

    }

    $('title').text(`${config.title} - 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: config.displayTemplates

    });

    var fieldlayout = new OO.ui.FieldLayout(

        dropdown,

        {

            label,

            align: 'inline',

            classes: 'newnomonly'],

        }

    );

    return { container: fieldlayout, dropdown };

}



function createTitleAndInputFieldWithLabel(label, placeholder, classes = []) {

    var input = new OO.ui.TextInputWidget({

        placeholder

    });





    var fieldset = new OO.ui.FieldsetLayout({

        classes

    });



    fieldset.addItems([

        new OO.ui.FieldLayout(input, {

            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,

        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,

        inputField,

        container,

        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,

        indicator: 'required'

    });



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



    return {

        titleLabel,

        inputField,

        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',

        selected: true

    });

    var oldNomToggle = new OO.ui.ButtonOptionWidget({

        data: 'old',

        label: 'Old nomination',

    });



    var toggle = new OO.ui.ButtonSelectWidget({

        items: 

            newNomToggle,

            oldNomToggle

        

    });

    return {

        toggle,

        newNomToggle,

        oldNomToggle,

    };

}





function createMessageElement() {

    var messageElement = new OO.ui.MessageWidget({

        type: 'progress',

        inline: true,

        progressType: 'infinite'

    });

    return messageElement;

}



function createWarningMessage() {

    var warningMessage = new OO.ui.MessageWidget({

        type: 'warning',

        style: 'background-color: yellow;'

    });

    return warningMessage;

}



function createCompletedElement() {

    var messageElement = new OO.ui.MessageWidget({

        type: 'success',

    });

    return messageElement;

}



function createDoingElement() {

    var messageElement = new OO.ui.MessageWidget({

        type: 'info',

    });

    return messageElement;

}



function createAbortMessage() { // pretty much a duplicate of ratelimitMessage

    var abortMessage = new OO.ui.MessageWidget({

        type: 'warning',

    });

    return abortMessage;

}



function createErrorMessage(text) {

    var errorMessage = new OO.ui.MessageWidget({

        type: 'error',

    });

    errorMessage.setLabel(text);

    return errorMessage;

}



function createNominationErrorMessage() { // pretty much a duplicate of ratelimitMessage

    return createErrorMessage('Could not detect where to add new nomination.')

}



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,

            align: 'inline',

            selected: true

        }

    );

    return {

        fieldlayout,

        checkbox

    };

}

function createMenuOptionWidget(data, label) {

    var menuOptionWidget = new OO.ui.MenuOptionWidget({

        data,

        label

    });

    return menuOptionWidget;

}

function createActionDropdown() {

    var items = Object.keys(config.actions)

        .map(action => action, action]) // [label, data]

        .map(action => createMenuOptionWidget(...action));



    var dropdown = new OO.ui.DropdownWidget({

        label: 'Mass action',

        menu: {

            items

        }

    });

    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 getDateDifference(date1) {

    const currentDate = new Date();

    // now

    let date2 = `${currentDate.getUTCFullYear()} ${currentDate.toLocaleString('en', { month: 'long', timeZone: 'UTC' })} ${currentDate.getUTCDate()}`



    // Parse the dates

    const parseDate = (dateString) => {

        const year, month, day = dateString.split(' ');

        return new Date(`${year}-${month}-${day}`);

    };



    const d1 = parseDate(date1);

    const d2 = parseDate(date2);



    // Calculate the time difference in milliseconds

    const timeDifference = Math.abs(d2 - d1);



    // Convert the time difference from milliseconds to days

    const dayDifference = Math.ceil(timeDifference / (1000 * 60 * 60 * 24));



    return dayDifference;

}



function deepCopy(obj) {

    if (obj === null || typeof obj !== 'object') {

        return obj;

    }



    if (obj instanceof OO.ui.Element) {

        return obj;

    }



    if (Array.isArray(obj)) {

        const copy = [];

        for (let i = 0; i < obj.length; i++) {

            copyi = deepCopy(obji]);

        }

        return copy;

    }



    const copy = {};

    for (const key in obj) {

        if (obj.hasOwnProperty(key)) {

            copykey = deepCopy(objkey]);

        }

    }

    return copy;

}



function parseHTML(html) {

    // Create a temporary div to parse the HTML

    var tempDiv = $('<div>').html(html);



    // Find all li elements

    var liElements = tempDiv.find('li');



    // Array to store extracted hrefs

    var hrefs = [];



    let existinghrefRegexp = /^https:\/\/en\.wikipedia.org\/wiki\/([^?&]+?)$/;

    let nonexistinghrefRegexp = /^https:\/\/en\.wikipedia\.org\/w\/index\.php\?title=([^&?]+?)&action=edit&redlink=1$/;



    // Iterate through each li element

    liElements.each(function () {

        // Find all anchor (a) elements within the current li

        let hrefline = [];

        var anchorElements = $(this).find('a');



        // Extract href attribute from each anchor element

        anchorElements.each(function () {

            var href = $(this).attr('href');

            if (href) {

                var existingMatch = existinghrefRegexp.exec(href);

                var nonexistingMatch = nonexistinghrefRegexp.exec(href);

                let page;

                if (existingMatch) page = new mw.Title(existingMatch1]);

                if (nonexistingMatch) page = new mw.Title(nonexistingMatch1]);

                if (page && page.getNamespaceId() > -1 && !page.isTalkPage()) {

                    hrefline.push(page.getPrefixedText());

                }





            }

        });

        hrefs.push(hrefline);

    });



    return hrefs;

}



function handlepaste(widget, e) {

    var types, pastedData, parsedData;

    // Browsers that support the 'text/html' type in the Clipboard API (Chrome, Firefox 22+)

    if (e && e.clipboardData && e.clipboardData.types && e.clipboardData.getData) {

        // Check for 'text/html' in types list

        types = e.clipboardData.types;

        if (((types instanceof DOMStringList) && types.contains("text/html")) ||

            ($.inArray && $.inArray('text/html', types) !== -1)) {

            // Extract data and pass it to callback

            pastedData = e.clipboardData.getData('text/html');



            parsedData = parseHTML(pastedData);



            // Check if it's an empty array

            if (!parsedData || parsedData.length === 0) {

                // Allow the paste event to propagate for plain text or empty array

                return true;

            }

            let confirmed = confirm('You have pasted formatted text. Do you want this to be converted into wikitext?');

            if (!confirmed) return true;

            processPaste(widget, pastedData);



            // Stop the data from actually being pasted

            e.stopPropagation();

            e.preventDefault();

            return false;

        }

    }



    // Allow the paste event to propagate for plain text

    return true;

}



function waitForPastedData(widget, savedContent) {

    // If data has been processed by the browser, process it

    if (widget.getValue() !== savedContent) {

        // Retrieve pasted content via widget's getValue()

        var pastedData = widget.getValue();



        // Restore saved content

        widget.setValue(savedContent);



        // Call callback

        processPaste(widget, pastedData);

    }

    // Else wait 20ms and try again

    else {

        setTimeout(function () {

            waitForPastedData(widget, savedContent);

        }, 20);

    }

}



function processPaste(widget, pastedData) {

    // Parse the HTML

    var parsedArray = parseHTML(pastedData);

    let stringOutput = '';

    for (const pages of parsedArray) {

        stringOutput += pages.join('|') + '\n';

    }

    widget.insertContent(stringOutput);

}





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 - this is hacky, and potentially unreliable

function revertEdits() {

    var revertAllCount = 0;

    var revertElements = $('.massxfdundo');

    if (!revertElements.length) {

        $('#massxfdrevertlink').replaceWith('Reverts done.');

    } else {

        $('#massxfdrevertlink').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,

        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 getRedirectData(titles) {

    var api = new mw.Api();

    return api.get({

        action: 'query',

        titles,

        redirects: 1,

        format: 'json'

    }).then(function (data) {

        return data.query;

    }).catch(function (error) {

        console.error('Error occurred while fetching page author:', error);

        return false;

    });

}



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

async function createAuthorList(titles) {

    console.log(titles)

    var authorList = [];

    var promises = titles.map(function (title) {

        return getPageAuthor(title);

    });

    try {

        const authors = await Promise.all(promises);

        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 => {

                        console.log(user)

                        if (user

                            && (!user.blockexpiry || user.blockexpiry !== "infinite" || 'blockpartial' in user)

                            && !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 (error_1) {

        console.error('Error occurred while creating author list:', error_1);

        return authorList;

    }

}



// Function to create a list of page authors and filter duplicates

async function createRedirectTargetsList(titles) {

    try {

        let queryBatchSize = 50;

        let redirectTitles = titles.map(title => title.replace(/ /g, '_')); // Replace spaces with underscores

        let redirectTargets = {};

        let nonredirects = [];

        for (let i = 0; i < redirectTitles.length; i += queryBatchSize) {

            let batch = redirectTitles.slice(i, i + queryBatchSize);

            let batchTitles = batch.join('|');



            await getRedirectData(batchTitles)

                .then(data => {



                    if ('redirects' in data) {

                        data.redirects.forEach(redirect => {

                            redirectTargetsredirect.from = redirect.to

                        });

                        let redirects = new Set(data.redirects.map(r => r.to))

                        let pages = new Set(Object.values(data.pages).map(p => p.title));

                        nonredirects.push(...[...pages].filter(x => !redirects.has(x)))

                    } else {

                        nonredirects.push(...Object.values(data.pages).map(p => p.title))

                    }



                })

                .catch(error => {

                    console.error("Error querying API:", error);

                });

        }

        return redirectTargets, nonredirects];

    } catch (error_1) {

        console.error('Error occurred while fetching redirect targets', error_1);

        return redirectTargets, nonredirects];

    }

}



function editPage(options) {

    const localOptions = deepCopy(options);

    console.log(localOptions);

    localOptions.text = localOptions.textToModify;

    const api = new mw.Api();

    const messageElement = createMessageElement();



    messageElement.setLabel((localOptions.retry)

        ? $('<span>').text('Retrying ').append($(makeLink(localOptions.title)))

        : $('<span>').text('Editing ').append($(makeLink(localOptions.title))));



    localOptions.progressElement.$element.append(messageElement.$element);

    const container = $('.sticky-container');

    container.scrollTop(container.prop("scrollHeight"));



    if (localOptions.retry) {

        sleep(1000);

    }



    const requestData = {

        action: 'edit',

        title: window.debuggingMode ? 'User:Qwerfjkl/sandbox/51' : localOptions.title,

        summary: localOptions.summary,

        format: 'json'

    };



    if (localOptions.type === 'prepend') {

        requestData.nocreate = 1;

        const targets = localOptions.titlesDictlocalOptions.title];



        for (let i = 0; i < targets.length; i++) {

            const placeholder = '$' + (i + 1);

            localOptions.text = localOptions.text.replace(placeholder, targetsi]);

        }

        localOptions.text = localOptions.text.replace(/\$\d/g, '');

        requestData.prependtext = localOptions.text.trim() + '\n\n';

    } else if (localOptions.type === 'append') {

        requestData.appendtext = '\n\n' + localOptions.text.trim();

    } else if (localOptions.type === 'text') {

        requestData.text = localOptions.text;

    }



    console.log(requestData);



    return new Promise((resolve, reject) => {

        if (window.abortEdits) {

            messageElement.toggle(false);

            resolve();

            return;

        }



        api.postWithEditToken(requestData)

            .then((data) => {

                if (data. && data..result === 'Success') {

                    messageElement.setType('success');

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

                    resolve();

                } else {

                    handleError('Error occurred while editing', data, localOptions, messageElement, resolve, reject);

                }

            })

            .catch((error) => handleError('Error occurred while editing', error, localOptions, messageElement, resolve, reject));

    });

}



function handleError(msg, error, options, messageElement, resolve, reject) {

    messageElement.setType('error');

    messageElement.setLabel($('<span>' + msg + ' ' + makeLink(options.title) + ': ' + error + '</span>'));

    console.error(msg + ' page:', error);



    if (error === 'editconflict') {

        editPage(deepCopy(options)).then(resolve);

    } else if (error === 'ratelimited') {

        options.progress.setDisabled(true);

        handleRateLimitError(options.ratelimitMessage).then(() => {

            options.progress.setDisabled(false);

            editPage(deepCopy(options)).then(resolve);

        });

    } else {

        reject();

    }

}





// global scope - needed to syncronise ratelimits

var massXFDratelimitPromise = 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 (massXFDratelimitPromise !== null) {

        return massXFDratelimitPromise;

    }



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

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

            massXFDratelimitPromise = null; // reset

            resolve();

        }, secondsToWait);

    });

    return massXFDratelimitPromise;

}



// 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,

        align: 'inline'

    });

    return {

        progressBar,

        fieldlayout

    };

}





// Main function to execute the script

async function runMassXFD() {



    Object.keys(XFDconfig).forEach(function (XfD) {

        mw.util.addPortletLink('p-tb', mw.util.getUrl(`Special:Mass${XfD}`), `Mass ${XfD}`, `pt-mass${XfD.toLowerCase()}`, `Create a mass ${XfD} nomination`);

    })





    if (XFD) {

        // Load the required modules

        mw.loader.using('oojs-ui').done(function () {

            wipePageContent();

            if (!window.debuggingMode) { // annoying when reloading for debugging

                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;

            }`); // should probably be styled directly on the element than via the stylesheet

            var nominationToggleObj = createNominationToggle();

            var nominationToggle = nominationToggleObj.toggle;



            bodyContent.append(nominationToggle.$element);

            elementsToDisable.push(nominationToggle);



            var rationaleObj = createTitleAndInputField('Rationale:', config.placeholderRationale);

            var rationaleContainer = rationaleObj.container;

            var rationaleInputField = rationaleObj.inputField;

            elementsToDisable.push(rationaleInputField);



            var nominationToggleOld = nominationToggleObj.oldNomToggle;

            var nominationToggleNew = nominationToggleObj.newNomToggle;



            var discussionLinkObj = createTitleAndSingleInputField('Discussion link', config.placeholderDiscussionLink);

            var discussionLinkContainer = discussionLinkObj.container;

            var discussionLinkInputField = discussionLinkObj.inputField;

            elementsToDisable.push(discussionLinkInputField);



            var newNomHeaderObj = createTitleAndSingleInputField('Nomination title', config.placeholderNominationTitle);

            var newNomHeaderContainer = newNomHeaderObj.container;

            var newNomHeaderInputField = newNomHeaderObj.inputField;

            elementsToDisable.push(newNomHeaderInputField);



            bodyContent.append(discussionLinkContainer.$element);

            bodyContent.append(newNomHeaderContainer.$element, rationaleContainer.$element);

            function displayElements() {

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



                }

            }

            displayElements();

            nominationToggle.on('select', displayElements);









            function createActionNomination(actionsContainer, first = false) {

                var count = actions.length + 1;

                let actionNominationTitle = XFD === 'CFD' ? 'Action batch #' + count : ''

                var container = createFieldset(actionNominationTitle);

                actionsContainer.append(container.$element);



                var actionDropdownObj = createActionDropdown();

                var dropdown = actionDropdownObj.dropdown;



                elementsToDisable.push(dropdown);

                dropdown.$element.css('max-width', 'fit-content');

                let demoText = config.pageDemoText

                var prependTextObj = createTitleAndInputField('Text to tag the nominated pages with:', demoText, 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()) {

                        if (XFD === 'CFD') $('.newnomonly').show();

                        if (newNomHeaderInputField.getValue().trim()) newNomHeaderInputField.emit('change');

                    }

                });



                if (nominationToggleOld.isSelected()) {

                    if (discussionLinkInputField.getValue().match(config.discussionLinkRegex)) {

                        sectionName = discussionLinkInputField.getValue().trim().match(config.discussionLinkRegex)[1];

                    }

                }

                else if (nominationToggleNew.isSelected()) {

                    sectionName = newNomHeaderInputField.getValue().trim();

                }



                // helper function, makes more accurate.

                function replaceOccurence(str, find, replace) {

                    if (XFD === 'CFD') {

                        // last occurence

                        let index = str.lastIndexOf(find);



                        if (index >= 0) {

                            return str.substring(0, index) + replace + str.substring(index + find.length);

                        } else {

                            return str;

                        }

                    } else if (XFD === 'RFD') {

                        if (str.toLowerCase().startsWith('{{subst:rfd|')) {

                            str = str.replace(/\{\{subst:rfd\|/i, '')

                            return '{{subst:rfd|' + str.replace(find, replace)

                        } else {

                            return str.replace(find, replace) // first occurence

                        }

                    }

                }



                var sectionName = sectionName || 'sectionName';

                var oldSectionName = sectionName;

                if (XFD !== 'CFD') {

                    prependTextInputField.setValue(config.actions.prepend.replace('${sectionName}', sectionName))

                    if (XFD === 'RFD') {

                        if (discussionLinkInputField.getValue().match(config.discussionLinkRegex)) {

                            let date = discussionLinkInputField.getValue().trim().match(/^Wikipedia:Redirects for discussion\/Log\/(\d\d\d\d \w+ \d\d)?#.+$/)[1

                            let difference = getDateDifference(date)

                            if (difference !== 0) {

                                prependTextInputField.setValue(config.actions.prepend.replace('{{subst:rfd|${sectionName}|', `{{subst:rfd|${sectionName}|days=${difference}|`))

                            } // else leave as default above

                        }

                    }

                }

                discussionLinkInputField.on('change', function () {

                    if (discussionLinkInputField.getValue().match(config.discussionLinkRegex)) {

                        oldSectionName = sectionName;

                        sectionName = discussionLinkInputField.getValue().replace(config.discussionLinkRegex, '$1').trim();

                        var text = prependTextInputField.getValue();



                        if (XFD === 'RFD') {

                            const date = discussionLinkInputField.getValue().trim().match(/^Wikipedia:Redirects for discussion\/Log\/(\d\d\d\d \w+ \d\d?)#.+$/)[1



                            if (/(\| *days *= *)\d+/.test(text)) { // already has days=, update

                                text = text.replace(/(\| *days *= *)\d+/, '$1' + getDateDifference(date))

                                text = replaceOccurence(text, oldSectionName, sectionName);

                            } else {

                                text = replaceOccurence(text, oldSectionName, sectionName + '|days=' + getDateDifference(date));

                            }

                        } else text = replaceOccurence(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 = replaceOccurence(text, oldSectionName, sectionName);

                        prependTextInputField.setValue(text);

                    }

                });



                dropdown.on('labelChange', function () {

                    let actionData = config.actionsdropdown.getLabel()];

                    prependTextInputField.setValue(actionData.prepend.replace('${sectionName}', sectionName));

                    actionInputField.setValue(actionData.action);

                });









                var titleListObj = createTitleAndInputField(`List of titles (one per line${XFD === 'CFD' ? ', <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>Example|Target1|Target2</code>. These targets can be used in the tagging step.');

                var titleList = titleListObj.container;

                var titleListInputField = titleListObj.inputField;

                var titleListInfoPopup = titleListObj.infoPopup;

                elementsToDisable.push(titleListInputField);

                let handler = handlepaste.bind(this, titleListInputField);

                let textInputElement = titleListInputField.$element.get(0);

                // Modern browsers. Note: 3rd argument is required for Firefox <= 6

                if (textInputElement.addEventListener) {

                    textInputElement.addEventListener('paste', handler, false);

                }

                // IE <= 8

                else {

                    textInputElement.attachEvent('onpaste', handler);

                }





                titleListObj.inputField.$element.on('paste', handlepaste);



                if (XFD !== 'CFD') {

                    // most XfDs don't need multiple actions, they're just delete. so hide unnecessary elements'

                    actionContainer.$element.hide();

                    dropdown.$element.hide();

                    prependTextInfoPopup.$element.hide() // both popups give info about targets which aren't relevant here

                    titleListInfoPopup.$element.hide()

                }





                if (!first && XFD !== 'CFD') {

                    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,

                    prependTextInputField,

                    label: 'Action batch #' + count,

                    container,

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

            });

            if (XFD !== 'CFD') {

                multiOptionButton.$element.hide()

            } else {



                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', async function () {



                var isOld = nominationToggleOld.isSelected();

                var isNew = nominationToggleNew.isSelected();

                // First check elements

                var error = false;

                var regex = config.discussionLinkRegex;

                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()) || (XFD === 'RFD' && !prependTextInputField.getValue().includes('${pageText}'))) {

                        prependTextInputField.setValidityFlag(false);

                        error = true;

                    } else {

                        prependTextInputField.setValidityFlag(true);



                    }



                    if (isNew && XFD === 'CFD') {

                        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 normalise(title) {

                        return config.normaliseFunction(title)

                    }

                    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,

                        prependText: prependTextInputField.getValue().trim(),

                        label,

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

                }, []);





                if (XFD === 'RFD') {

                    let fetchingRedirectsElement = createDoingElement();

                    fetchingRedirectsElement.setLabel('Fetching redirect targets...')

                    fetchingRedirectsElement.$element.css('margin-top', '16px');

                    bodyContent.append(fetchingRedirectsElement.$element);



                    let fetchedRedirectsElement = createCompletedElement();

                    fetchedRedirectsElement.setLabel('Fetched redirect targets')

                    fetchedRedirectsElement.$element.css('margin-top', '16px');



                    var redirectTargets, nonredirects = await createRedirectTargetsList(allTitles);

                    // window.batches=batches

                    batches0].titles = Object.keys(batches0].titles)

                        .filter(x => !nonredirects.includes(x))

                        .reduce((acc, curr) => {

                        acccurr = [];

                        return acc;

                        }, {});







                    console.log(batches)

                    if (!Object.keys(redirectTargets).length) {

                        var errorMessageElement = createErrorMessage('None of the titles are redirects, aborting.');

                        bodyContent.append(errorMessageElement.$element);

                        return;

                    }

                    console.log(nonredirects)

                    if (nonredirects.length) {

                        let nonredirectsWarningMessage = createWarningMessage();

                        nonredirectsWarningMessage.$element.css({ 'max-height': '20em', 'overflow-y': 'auto' }) // normally shouldn't be needed

                        let nonRedirectsHTML = $('<div>').append($('<span>').text('The following pages were ignored because they are not redirects:'))

                        let $listElement = $('<ul>')

                        nonredirects.forEach(item => {

                            const $listItem = $('<li>').html(makeLink(item));

                            $listElement.append($listItem);

                        });

                        nonRedirectsHTML.append($listElement)

                        nonredirectsWarningMessage.setLabel(nonRedirectsHTML)

                        bodyContent.append(nonredirectsWarningMessage.$element)

                    }



                    fetchingRedirectsElement.$element.hide();

                    bodyContent.append(fetchedRedirectsElement.$element);

                }

                console.log(redirectTargets);





                let fetchingAuthorsElement = createDoingElement();

                fetchingAuthorsElement.setLabel('Fetching authors...')

                fetchingAuthorsElement.$element.css('margin-top', '16px');

                bodyContent.append(fetchingAuthorsElement.$element);



                let fetchedAuthorsElement = createCompletedElement();

                fetchedAuthorsElement.setLabel('Fetched authors')

                fetchedAuthorsElement.$element.css('margin-top', '16px');

                let authors;

                if (redirectTargets) {

                    authors = await createAuthorList(Object.keys(redirectTargets));

                } else {

                    authors = await createAuthorList(allTitles);

                }



                fetchingAuthorsElement.$element.hide();

                bodyContent.append(fetchedAuthorsElement.$element);





                async function processContent(options) {

                    function getKeyByValue(object, value) {

                        return Object.keys(object).find(key => objectkey === value);

                    }



                    console.log(options);

                    console.log(options.titles);



                    if (!Array.isArray(options.titles)) {

                        options.titlesDict = options.titles;

                        options.titles = Object.keys(options.titles);

                    } else {

                        options.titlesDict = {};

                    }



                    const fieldset = createFieldset(options.headingLabel);

                    bodyContent.append(fieldset.$element);



                    options.progressElement = createProgressElement();

                    fieldset.addItems([options.progressElement]);



                    options.ratelimitMessage = createWarningMessage();

                    options.ratelimitMessage.toggle(false);

                    fieldset.addItems([options.ratelimitMessage]);



                    const progressObj = createProgressBar(`(0 / ${options.titles.length}, 0 errors)`);

                    options.progress = progressObj.progressBar;

                    const progressContainer = progressObj.fieldlayout;

                    options.progress.$element.css('margin-top', '5px');

                    options.progress.pushPending();

                    fieldset.addItems([progressContainer]);



                    let resolvedCount = 0;

                    let rejectedCount = 0;



                    function updateCounter() {

                        progressContainer.setLabel(`(${resolvedCount} / ${options.titles.length}, ${rejectedCount} errors)`);

                    }



                    function updateProgress() {

                        const percentage = (resolvedCount + rejectedCount) / options.titles.length * 100;

                        options.progress.setProgress(percentage);

                    }



                    function trackPromise(promise) {

                        return new Promise((resolve) => {

                            promise

                                .then(value => {

                                    resolvedCount++;

                                    updateCounter();

                                    updateProgress();

                                    resolve(value);

                                })

                                .catch(error => {

                                    rejectedCount++;

                                    updateCounter();

                                    updateProgress();

                                    resolve(error);

                                });

                        });

                    }



                    const promises = [];

                    for (const title of options.titles) {

                        let data = deepCopy(options);

                        if (XFD === 'RFD' && data.type === 'prepend') {

                            const text = await getWikitext(title);

                            data.textToModify = data.textToModify.replace('${pageText}', text);

                            data.type = 'text';

                        }



                        if (data.id === 'rfd-notify-target') {

                            data.textToModify = data.textToModify.replace('${redirectTitle}', getKeyByValue(redirectTargets, new mw.Title(title).getSubjectPage().getPrefixedText()));

                        }



                        data.title = title;

                        console.log('Data:', data);



                        const promise = editPage(data);

                        promises.push(trackPromise(promise));



                        if (!window.abortEdits) await sleep(100); // space out calls - not needed if they're being rejected

                        await massXFDratelimitPromise; // stop if ratelimit reached (global variable)

                    }



                    await Promise.allSettled(promises);



                    options.progress.toggle(false);



                    if (window.abortEdits) {

                        const abortMessage = createAbortMessage();

                        const revertEditsLink = $('<a id="massxfdrevertlink">Revert?</a>');

                        revertEditsLink.on('click', revertEdits);

                        abortMessage.setLabel($('<span>').append('Edits manually aborted. ').append(revertEditsLink));

                        bodyContent.append(abortMessage.$element);

                    } else {

                        const completedElement = createCompletedElement();

                        completedElement.setLabel(options.doneMessage);

                        completedElement.$element.css('margin-bottom', '16px');

                        bodyContent.append(completedElement.$element);

                    }

                }







                const date = new Date();



                const year = date.getUTCFullYear();

                const month = date.toLocaleString('en', { month: 'long', timeZone: 'UTC' });

                const day = date.getUTCDate();



                var summaryDiscussionLink;

                var discussionPage = `${config.baseDiscussionPage}${year} ${month} ${day}`;



                if (isOld) summaryDiscussionLink = discussionLinkInputField.getValue().trim();

                else if (isNew) summaryDiscussionLink = `${discussionPage}#${newNomHeaderInputField.getValue().trim()}`;



                const advSummary = ' ([[User:Qwerfjkl/scripts/massXFD.js|via MassXfD.js]])';

                // WIP, not finished

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

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

                const userNotification = `{{ subst: ${config.userNotificationTemplate} | ${summaryDiscussionLink} }} ~~~~`;

                const nominationSummary = `Adding mass nomination at [[#${newNomHeaderInputField.getValue().trim()}]]` + advSummary;

                if (XFD === 'RFD') {

                    var redirectTargetNotification = `{{subst:Rfd notice|\${redirectTitle}|${newNomHeaderInputField.getValue().trim()}}}`

                    var redirectTargetNotificationSummary = `Notice of [[${summaryDiscussionLink}]]${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() || false;

                            for (const page of Object.keys(batch.titles)) {

                                if (XFD == 'CFD') {

                                    var targets = batch.titlespage].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()}|${page}}}${targetText}\n`;

                                } else {

                                    nominationText += config.displayTemplate.replaceAll('${pageName}', page).replaceAll('${redirectTarget}', redirectTargetspage]) + '\n';

                                }

                            }

                        }

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

                        nominationText += `${XFD === 'CFD' ? ":'''Nominator's rationale:''' " : ''}${rationale} ~~~~`;

                        var newText;



                        getWikitext(discussionPage).then(function (wikitext) {

                            if (!wikitext.match(config.nominationReplacement0])) {

                                var nominationErrorMessage = createNominationErrorMessage();

                                bodyContent.append(nominationErrorMessage.$element);

                            } else {

                                newText = wikitext.replace(...config.nominationReplacement).replace('${nominationText}', nominationText);

                                batchesToProcess.push({

                                    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({

                            titles: batch.titles,

                            textToModify: batch.prependText,

                            summary: categorySummary,

                            type: 'prepend',

                            doneMessage: 'All pages edited.',

                            headingLabel: 'Editing nominated pages' + ((batches.length > 1) ? ' — ' + batch.label : '')

                        });

                    });

                    if (XFD === 'RFD') {

                        batchesToProcess.push({

                            id: 'rfd-notify-target',

                            titles: Object.values(redirectTargets).map(title => {

                                let page = new mw.Title(title)

                                return page.getTalkPage().getPrefixedText()

                            }),

                            textToModify: redirectTargetNotification,

                            summary: redirectTargetNotificationSummary,

                            type: 'append',

                            doneMessage: 'All target talk pages notified.',

                            headingLabel: 'Notifying targets'

                        });

                    }

                    if (notifyCheckbox.isSelected()) {

                        batchesToProcess.push({

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

                        // alert(`starting batch ${batch.headingLabel}`)

                        await processContent(batch);

                        // alert(`batch ${batch.headingLabel} done`)

                    }



                    promise.then(() => {

                        abortButton.setLabel('Revert');

                        // All done

                    }).catch(err => {

                        console.error('Error occurred:', err);

                    });

                });



            });

        });

    }

}



// Run the script when the page is ready

$(document).ready(runMassXFD);

// </nowiki>

Videos

Youtube | Vimeo | Bing

Websites

Google | Yahoo | Bing

Encyclopedia

Google | Yahoo | Bing

Facebook