From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.

// <nowiki>

// todo: make counter inline, remove progresss and progressElement from editPAge(), more dynamic reatelimit wait.

// counter semi inline; adjust align in createProgressBar()

// Function to wipe the text content of the page inside #bodyContent

function wipePageContent() {

    var bodyContent = $('#bodyContent');

    if (bodyContent) {

        bodyContent.empty();

    }

    var header = $('#firstHeading');

    if (header) {

        header.text('Mass CfDS');

    }

    $('title').text('Mass CfDS - 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 createTitleAndInputFieldWithLabel(label, placeholder, classes = []) {

    var input = new OO.ui.TextInputWidget({

        placeholder: placeholder

    });





    var fieldset = new OO.ui.FieldsetLayout({

        classes: classes

    });



    fieldset.addItems([

        new OO.ui.FieldLayout(input, {

            label: label

        }),

    ]);



    return {

        container: fieldset,

        inputField: input,

    };

}

// Function to create a title and an input field

function createTitleAndInputField(title, placeholder, info = false) {

    var container = new OO.ui.PanelLayout({

        expanded: false

    });



    var titleLabel = new OO.ui.LabelWidget({

        label: $(`<span>${title}</span>`)

    });



    var infoPopup = makeInfoPopup(info);



    var inputField = new OO.ui.MultilineTextInputWidget({

        placeholder: placeholder,

        indicator: 'required',

        rows: 10,

        autosize: true

    });

    if (info) container.$element.append(titleLabel.$element, infoPopup.$element, inputField.$element);

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

    return {

        titleLabel: titleLabel,

        inputField: inputField,

        container: container,

        infoPopup: infoPopup

    };

}



// Function to create a title and an input field

function createTitleAndSingleInputField(title, placeholder) {

    var container = new OO.ui.PanelLayout({

        expanded: false

    });



    var titleLabel = new OO.ui.LabelWidget({

        label: title

    });



    var inputField = new OO.ui.TextInputWidget({

        placeholder: placeholder,

        indicator: 'required'

    });



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



    return {

        titleLabel: titleLabel,

        inputField: inputField,

        container: container

    };

}



function createStartButton() {

    var button = new OO.ui.ButtonWidget({

        label: 'Start',

        flags: 'primary', 'progressive'

    });



    return button;

}



function createAbortButton() {

    var button = new OO.ui.ButtonWidget({

        label: 'Abort',

        flags: 'primary', 'destructive'

    });



    return button;

}











function createMessageElement() {

    var messageElement = new OO.ui.MessageWidget({

        type: 'progress',

        inline: true,

        progressType: 'infinite'

    });

    return messageElement;

}



function createRatelimitMessage() {

    var ratelimitMessage = new OO.ui.MessageWidget({

        type: 'warning',

        style: 'background-color: yellow;'

    });

    return ratelimitMessage;

}



function createCompletedElement() {

    var messageElement = new OO.ui.MessageWidget({

        type: 'success',

    });

    return messageElement;

}



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

    var abortMessage = new OO.ui.MessageWidget({

        type: 'warning',

    });

    return abortMessage;

}



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

    var nominationErrorMessage = new OO.ui.MessageWidget({

        type: 'error',

        text: 'Could not detect where to add new nomination.'

    });

    return nominationErrorMessage;

}



function createFieldset(headingLabel) {

    var fieldset = new OO.ui.FieldsetLayout({

        label: headingLabel,

    });

    return fieldset;

}





function createMenuOptionWidget(data, label) {

    var menuOptionWidget = new OO.ui.MenuOptionWidget({

        data: data,

        label: label

    });

    return menuOptionWidget;

}

function createActionDropdown() {

    var items = 

        'C2A-rename', 'C2A (rename)'],

        'C2B-rename', 'C2B (rename)'],

        'C2C-rename', 'C2C (rename)'],

        'C2D-rename', 'C2D (rename)'],

        'C2E-rename', 'C2E (rename)'],

        'C2F-rename', 'C2F (rename)'],

        'C2A-merge', 'C2A (merge)'],

        'C2B-merge', 'C2B (merge)'],

        'C2C-merge', 'C2C (merge)'],

        'C2D-merge', 'C2D (merge)'],

        'C2E-merge', 'C2E (merge)'],

        'C2F-merge', 'C2F (merge)'],

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







    var dropdown = new OO.ui.DropdownWidget({

        label: 'Mass action',

        menu: {

            items

        }

    });

    return dropdown;

}







function sleep(ms) {

    return new Promise(resolve => setTimeout(resolve, ms));

}



function makeLink(title) {

    return `<a href="/wiki/${title}" target="_blank">${title}</a>`;

}



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

function revertEdits() {

    var revertAllCount = 0;

    var revertElements = $('.masscfdsundo');

    if (!revertElements.length) {

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

    } else {

        $('#masscfdsrevertlink').replaceWith('<span><span id="revertall-text">Reverting...</span> (<span id="revertall-done">0</span> / <span id="revertall-total">' + revertElements.length + '</span> done)</span>');



        revertElements.each(function (index, element) {

            element = $(element); // jQuery-ify

            var title = element.attr('data-title');

            var revid = element.attr('data-revid');

            revertEdit(title, revid)

                .then(function () {

                    element.text('. Reverted.');

                    revertAllCount++;

                    $('#revertall-done').text(revertAllCount);

                }).catch(function () {

                    element.html('. Revert failed. <a href="/wiki/Special:Diff/' + revid + '">Click here</a> to view the diff.');

                });

        }).promise().done(function () {

            $('#revertall-text').text('Reverts done.');

        });

    }

}



function revertEdit(title, revid, retry = false) {

    var api = new mw.Api();





    if (retry) {

        sleep(1000);

    }



    var requestData = {

        action: 'edit',

        title: title,

        undo: revid,

        format: 'json'

    };

    return new Promise(function (resolve, reject) {

        api.postWithEditToken(requestData).then(function (data) {

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

                resolve(true);

            } else {

                console.error('Error occurred while undoing edit:', data);

                reject();

            }

        }).catch(function (error) {

            console.error('Error occurred while undoing edit:', error); // handle: editconflict, ratelimit (retry)

            if (error == 'editconflict') {

                resolve(revertEdit(title, revid, retry = true));

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

                setTimeout(function () { // wait a minute

                    resolve(revertEdit(title, revid, retry = true));

                }, 60000);

            } else {

                reject();

            }

        });

    });

}



function getUserData(titles) {

    var api = new mw.Api();

    return api.get({

        action: 'query',

        list: 'users',

        ususers: titles,

        usprop: 'blockinfo|groups', // blockinfo - check if indeffed, groups - check if bot

        format: 'json'

    }).then(function (data) {

        return data.query.users;

    }).catch(function (error) {

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

        return false;

    });

}



function getPageAuthor(title) {

    var api = new mw.Api();

    return api.get({

        action: 'query',

        prop: 'revisions',

        titles: title,

        rvprop: 'user',

        rvdir: 'newer', // Sort the revisions in ascending order (oldest first)

        rvlimit: 1,

        format: 'json'

    }).then(function (data) {

        var pages = data.query.pages;

        var pageId = Object.keys(pages)[0];

        var revisions = pagespageId].revisions;

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



            return revisions0].user;

        } else {

            return false;

        }

    }).catch(function (error) {

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

        return false;

    });

}



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

function createAuthorList(titles) {

    var authorList = [];

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

        return getPageAuthor(title);

    });

    return Promise.all(promises).then(async function (authors) {

        let queryBatchSize = 50;

        let authorTitles = authors.map(author => author.replace(/ /g, '_')); // Replace spaces with underscores

        let filteredAuthorList = [];

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

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

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



            await getUserData(batchTitles)

                .then(response => {

                    response.forEach(user => {

                        if (user

                            && (!user.blockexpiry || user.blockexpiry !== "infinite")

                            && !user.groups.includes('bot')

                            && !filteredAuthorList.includes('User talk:' + user.name)

                        )



                            filteredAuthorList.push('User talk:' + user.name);

                    });



                })

                .catch(error => {

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

                });

        }

        return filteredAuthorList;

    }).catch(function (error) {

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

        return authorList;

    });

}



// Function to prepend text to a page

function editPage(title, text, summary, progressElement, ratelimitMessage, progress, type, titlesDict, retry = false) {

    var api = new mw.Api();



    var messageElement = createMessageElement();







    messageElement.setLabel((retry) ? $('<span>').text('Retrying ').append($(makeLink(title))) : $('<span>').text('Editing ').append($(makeLink(title))));

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

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

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

    if (retry) {

        sleep(1000);

    }



    var requestData = {

        action: 'edit',

        title: title,

        summary: summary,

        format: 'json'

    };



    if (type === 'prepend') { // cat

        requestData.nocreate = 1; // don't create new cat

        // parse title

        var targets = titlesDicttitle];



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

            // we add 1 to i in the replace function because placeholders start from $1 not $0

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

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

        }

        text = text.replace(/\$\d/g, ''); // remove unmatched |$x

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





    } else if (type === 'append') { // user

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

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

        requestData.text = text;

    }

    return new Promise(function (resolve, reject) {

        if (window.abortEdits) {

            // hide message and return

            messageElement.toggle(false);

            resolve();

            return;

        }

        api.postWithEditToken(requestData).then(function (data) {

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

                messageElement.setType('success');

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



                resolve();

            } else {



                messageElement.setType('error');

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

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



                reject();

            }

        }).catch(function (error) {

            messageElement.setType('error');

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

            console.error('Error occurred while prepending text to page:', error); // handle: editconflict, ratelimit (retry)

            if (error == 'editconflict') {

                editPage(title, text, summary, progressElement, ratelimitMessage, progress, type, titlesDict, retry = true).then(function () {

                    resolve();

                });

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

                progress.setDisabled(true);



                handleRateLimitError(ratelimitMessage).then(function () {

                    progress.setDisabled(false);

                    editPage(title, text, summary, progressElement, ratelimitMessage, progress, type, titlesDict, retry = true).then(function () {

                        resolve();

                    });

                });

            }

            else {

                reject();

            }

        });

    });

}



// global scope - needed to syncronise ratelimits

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

        return massCFDSratelimitPromise;

    }



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

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

            massCFDSratelimitPromise = null; // reset

            resolve();

        }, secondsToWait);

    });

    return massCFDSratelimitPromise;

}



// Function to show progress visually

function createProgressBar(label) {

    var progressBar = new OO.ui.ProgressBarWidget();

    progressBar.setProgress(0);

    var fieldlayout = new OO.ui.FieldLayout(progressBar, {

        label: label,

        align: 'inline'

    });

    return {

        progressBar: progressBar,

        fieldlayout: fieldlayout

    };

}





// Main function to execute the script

async function runMassCFDS() {



    mw.util.addPortletLink('p-tb', mw.util.getUrl('Special:MassCFDS'), 'Mass CfDS', 'pt-masscfds', 'Create a mass CfDS nomination');



    if (/Special:MassCFDS/i.test(mw.config.get('wgPageName'))) {

        // Load the required modules

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

            wipePageContent();

            //   onbeforeunload = function() {

            //       return "Closing this tab will cause you to lose all progress.";

            //   };

            elementsToDisable = [];

            var bodyContent = $('#bodyContent');



            mw.util.addCSS(`.sticky-container {

            bottom: 0;

            width: 100%;

            max-height: 600px; 

            overflow-y: auto;

          }`);





            var rationaleObj = createTitleAndSingleInputField('Rationale:', 'Per [[Talk:Libyan civil war (2011)/Archive 13#Requested move 17 July 2023|past move discussion]] that resulted in [[First Libyan Civil War]] being moved to [[Libyan civil war (2011)]].'); // from [[:Special:Diff/1223231909#mw-diff-ntitle1]]

            var rationaleContainer = rationaleObj.container;

            var rationaleInputField = rationaleObj.inputField;

            elementsToDisable.push(rationaleInputField);



            bodyContent.append(rationaleContainer.$element);











            var dropdown = createActionDropdown();

            elementsToDisable.push(dropdown);

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

            bodyContent.append(dropdown.$element);



            var prependTextObj = createTitleAndInputField('Wikitext to tag category page with:', '{{subst:cfr-speedy|Category:Bishops}}', info = 'A dollar sign <code>$</code> followed by a number, such as <code>$1</code>, will be replaced with a target specified in the title field, or if not target is specified, will be removed.');

            var prependTextLabel = prependTextObj.titleLabel;

            var prependTextInfoPopup = prependTextObj.infoPopup;

            var prependTextInputField = prependTextObj.inputField;

            elementsToDisable.push(prependTextInputField);

            var prependTextContainer = new OO.ui.PanelLayout({

                expanded: false

            });



            prependTextContainer.$element.append(prependTextLabel.$element, prependTextInfoPopup.$element, dropdown.$element, prependTextInputField.$element);

            bodyContent.append(prependTextContainer.$element);





            var nominationType = false;

            var C2X = false;

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

                switch (dropdown.getMenu().findSelectedItem().getData().split("-").pop()) {

                    case "rename":

                        prependTextInputField.setValue(`{{subst:cfr-speedy|$1}}`);

                        nominationType = 'renaming';

                        break;

                    case "merge":

                        prependTextInputField.setValue(`{{subst:cfm-speedy|$1}}`);

                        nominationType = 'merging';

                        break;

                }

                C2X = dropdown.getMenu().findSelectedItem().getData().split("-").shift();



            });









            var titleListObj = createTitleAndInputField('List of titles (one per line, <code>Category:</code> prefix is optional)', 'Title1|Target1\nTitle2|Target2a|Target2b\nTitle3|Target3', info = 'You can specify targets by adding a pipe <code>|</code> and then the target, e.g. <code>Category:Example|Category:Target1|Category:Target2</code>. These targets can be used in the category tagging step.');

            var titleList = titleListObj.container;

            var titleListInputField = titleListObj.inputField;

            elementsToDisable.push(titleListInputField);

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

            bodyContent.append(titleList.$element);



            var startButton = createStartButton();

            elementsToDisable.push(startButton);

            bodyContent.append(startButton.$element);







            startButton.on('click', async function () {



                // First check elements

                var error = false;



                if (!(rationaleInputField.getValue().trim())) {

                    rationaleInputField.setValidityFlag(false);

                    error = true;

                } else {

                    rationaleInputField.setValidityFlag(true);

                }



                if (!titleListInputField.getValue().trim() || !titleListInputField.getValue().includes('|') ) { // for CfDS there should always be a target

                    titleListInputField.setValidityFlag(false);

                    error = true;

                } else {

                    titleListInputField.setValidityFlag(true);

                }



                if (!nominationType) { // needed to select C2X

                    // dropdown.setValidityFlag(false);

                    error = true;

                } else {

                    // dropdown.setValidityFlag(true);

                }



                // Retreive titles, handle dups

                var titles = {};

                var titleList = titleListInputField.getValue().split('\n');

                function capitalise(s) {

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

                }

                function normalise(title) {

                    return 'Category:' + capitalise(title.replace(/^ *[Cc]ategory:/, '').trim());

                }

                titleList.forEach(function (title) {

                    if (title) {

                        var targets = title.split('|');

                        var newTitle = targets.shift();

                        newTitle = normalise(newTitle);

                        if (!Object.keys(titles).includes(newTitle)) {

                            titlesnewTitle = targets.map(normalise);

                        }

                    }

                });

                

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

                    titleListInputField.setValidityFlag(false);

                    error = true;

                } else {

                    titleListInputField.setValidityFlag(true);

                }





                if (error) {

                    return;

                }



                for (let element of elementsToDisable) {

                    element.setDisabled(true);

                }







                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;

                    }

                });





                function processContent(content, titles, textToModify, summary, type, doneMessage, headingLabel) {

                    if (!Array.isArray(titles)) {

                        var titlesDict = titles;

                        titles = Object.keys(titles);

                    }

                    var fieldset = createFieldset(headingLabel);



                    content.append(fieldset.$element);



                    var progressElement = createProgressElement();

                    fieldset.addItems([progressElement]);



                    var ratelimitMessage = createRatelimitMessage();

                    ratelimitMessage.toggle(false);

                    fieldset.addItems([ratelimitMessage]);



                    var progressObj = createProgressBar(`(0 / ${titles.length}, 0 errors)`); // with label

                    var progress = progressObj.progressBar;

                    var progressContainer = progressObj.fieldlayout;

                    // Add margin or padding to the progress bar widget

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

                    progress.pushPending();

                    fieldset.addItems([progressContainer]);



                    let resolvedCount = 0;

                    let rejectedCount = 0;



                    function updateCounter() {

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

                    }

                    function updateProgress() {

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

                        progress.setProgress(percentage);



                    }



                    function trackPromise(promise) {

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

                            promise

                                .then(value => {

                                    resolvedCount++;

                                    updateCounter();

                                    updateProgress();

                                    resolve(value);

                                })

                                .catch(error => {

                                    rejectedCount++;

                                    updateCounter();

                                    updateProgress();

                                    resolve(error);

                                });

                        });

                    }



                    return new Promise(async function (resolve) {

                        var promises = [];

                        for (const title of titles) {

                            var promise = editPage(title, textToModify, summary, progressElement, ratelimitMessage, progress, type, titlesDict);

                            promises.push(trackPromise(promise));

                            await sleep(100); // space out calls

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

                        }



                        Promise.allSettled(promises)

                            .then(function () {

                                progress.toggle(false);

                                if (window.abortEdits) {

                                    var abortMessage = createAbortMessage();

                                    abortMessage.setLabel($('<span>Edits manually aborted. <a id="masscfdsrevertlink" onclick="revertEdits()">Revert?</a></span>'));



                                    content.append(abortMessage.$element);

                                } else {

                                    var completedElement = createCompletedElement();

                                    completedElement.setLabel(doneMessage);

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

                                    content.append(completedElement.$element);

                                }

                                resolve();

                            })

                            .catch(function (error) {

                                console.error("Error occurred during title processing:", error);

                                resolve();

                            });

                    });

                }







                var discussionPage = 'Wikipedia:Categories for discussion/Speedy';



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

                const categorySummary = `Tagging category for [[Wikipedia:Categories for discussion/Speedy|speedy ${nominationType ? nominationType : 'nomination'})]]` + advSummary;

                const nominationSummary = 'Adding mass speedy nomination' + advSummary;





                var batchesToProcess = [];

                const titlesForTagging = structuredClone(titles);

                var newNomPromise = new Promise(function (resolve) {

                let nominationText = '';

                    function makeCategoryNominationText(category, targets, first = false) {

                        let targetText = '';

                        if (targets.length) {

                            if (targets.length === 2) {

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

                            }

                            else if (targets.length > 2) {

                                let lastTarget = targets.pop();

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

                            } else { // 1 target

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

                            }

                        }

                        return `${first ? '' : '*'}* [[:${category}]] ${targetText}${first ? ' – ' + C2X + ': ' + rationaleInputField.getValue().trim() + ' ~~~~' : ''}\n`;

                    }

                    let firstCategory = Object.keys(titles)[0];

                    let firstTargets = titlesfirstCategory];

                    delete titlesfirstCategory];



                    nominationText += makeCategoryNominationText(firstCategory, firstTargets, first = true);

                    for (const category in titles) {

                        var targets = titlescategory].slice(); // copy array

                        nominationText += makeCategoryNominationText(category, targets);

                    }



                    var newText;

                    var nominationRegex = /<!-- *PLACE NEW NOMINATIONS AT THE TOP OF THIS LIST, BELOW THIS LINE *-->/;

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

                        if (!wikitext.match(nominationRegex)) {

                            var nominationErrorMessage = createNominationErrorMessage();

                            bodyContent.append(nominationErrorMessage.$element);

                        } else {

                            newText = wikitext.replace(nominationRegex, '$&\n' + nominationText.trimEnd()); // $& contains all the matched text

                            batchesToProcess.push({

                                content: bodyContent,

                                titles: discussionPage],

                                textToModify: newText,

                                summary: nominationSummary,

                                type: 'text',

                                doneMessage: 'Nomination added',

                                headingLabel: 'Creating nomination'

                            });

                            resolve();

                        }

                    }).catch(function (error) {

                        console.error('An error occurred in fetching wikitext:', error);

                        resolve();

                    });

                });

                await newNomPromise;

                batchesToProcess.push({

                    content: bodyContent,

                    titles: titlesForTagging,

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

                    summary: categorySummary,

                    type: 'prepend',

                    doneMessage: 'All categories edited.',

                    headingLabel: 'Tagging categories'

                });

                

                let promise = Promise.resolve();

                // abort handling is now only in the editPage() function

                for (const batch of batchesToProcess) {

                    await processContent(...Object.values(batch));

                }



                promise.then(() => {

                    abortButton.setLabel('Revert');

                    // All done

                }).catch(err => {

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

                });

            });



        });

    }

}





// Run the script when the page is ready

$(document).ready(runMassCFDS);

// </nowiki>