User:Qwerfjkl/scripts/massXFD.js

From Wikipedia, the free encyclopedia

This is an old revision of this page, as edited by Qwerfjkl (talk | contribs) at 06:36, 17 May 2024 (English date [Factotum]). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

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 capitalise(s) {
    return s[0].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?#(.+)$/,
        "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?#(.+)$/,
        "userNotificationTemplate": 'Rfd mass notice',
        "baseDiscussionPage": 'Wikipedia:Redirects for discussion/Log/',
        "normaliseFunction": (title) => {return capitalise(title.trim())},
        "actions":  
        {
            'prepend': '{{subst:rfd|${sectionName}|content=\n${pageText}\n}}'
        },
        "displayTemplate": "{{subst:rfd2|multi=yes|redirect=${pageName}|target={{subst:target of|${pageName}}}}}"
        }
};
const match = /Special:Mass(\w+)/.exec(mw.config.get('wgPageName'));
const XFD = match ? match[1].toUpperCase() : false;
const config = XFDconfig[XFD];

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 createRatelimitMessage() {
      var ratelimitMessage = new OO.ui.MessageWidget({
          type: 'warning',
          style: 'background-color: yellow;'
      });
      return ratelimitMessage;
  }
  
  function createCompletedElement() {
      var messageElement = new OO.ui.MessageWidget({
          type: 'success',
      });
      return messageElement;
  }
  
  function createAbortMessage() { // pretty much a duplicate of ratelimitMessage
      var abortMessage = new OO.ui.MessageWidget({
          type: 'warning',
      });
      return abortMessage;
  }
  
  function createNominationErrorMessage() { // pretty much a duplicate of ratelimitMessage
      var nominationErrorMessage = new OO.ui.MessageWidget({
          type: 'error',
          text: 'Could not detect where to add new nomination.'
      });
      return nominationErrorMessage;
  }
  
  function createFieldset(headingLabel) {
      var fieldset = new OO.ui.FieldsetLayout({
                        label: headingLabel,
                    });
      return fieldset;
  }
  
  function createCheckboxWithLabel(label) {
      var checkbox = new OO.ui.CheckboxInputWidget( {
          value: 'a',
           selected: true,
      label: "Foo",
      data: "foo"
      } );
      var fieldlayout = new OO.ui.FieldLayout( 
          checkbox, 
          { label,
            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 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(existingMatch[1]);
                  if (nonexistingMatch) page = new mw.Title(nonexistingMatch[1]);
                  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 pages[0].revisions[0].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.edit && data.edit.result === 'Success') {
              resolve(true);
          } else {
              console.error('Error occurred while undoing edit:', data);
              reject();
          }
        }).catch(function(error) {
          console.error('Error occurred while undoing edit:', error); // handle: editconflict, ratelimit (retry)
          if (error == 'editconflict') {
              resolve(revertEdit(title, revid, retry=true));
          } else if (error == 'ratelimited') {
              setTimeout(function() { // wait a minute
                resolve(revertEdit(title, revid, retry=true));
              }, 60000);
          } else {
              reject();
          }
        });
      });
  }
  
  function getUserData(titles) {
    var api = new mw.Api();
    return api.get({
      action: 'query',
      list: 'users',
      ususers: titles,
      usprop: 'blockinfo|groups', // blockinfo - check if indeffed, groups - check if bot
      format: 'json'
    }).then(function(data) {
        return data.query.users;
    }).catch(function(error) {
      console.error('Error occurred while fetching page author:', error);
      return false;
    });
  }
  
  function getPageAuthor(title) {
    var api = new mw.Api();
    return api.get({
      action: 'query',
      prop: 'revisions',
      titles: title,
      rvprop: 'user',
      rvdir: 'newer', // Sort the revisions in ascending order (oldest first)
      rvlimit: 1,
      format: 'json'
    }).then(function(data) {
      var pages = data.query.pages;
      var pageId = Object.keys(pages)[0];
      var revisions = pages[pageId].revisions;
      if (revisions && revisions.length > 0) {
  
        return revisions[0].user;
      } else {
        return false;
      }
    }).catch(function(error) {
      console.error('Error occurred while fetching page author:', error);
      return false;
    });
  }
  
  // Function to create a list of page authors and filter duplicates
  async function createAuthorList(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 => {
                          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 (error_1) {
          console.error('Error occurred while creating author list:', error_1);
          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,
      summary,
      format: 'json'
    };
    
    if (type === 'prepend') { // cat
        requestData.nocreate = 1; // don't create new cat
        // parse title
        var targets = titlesDict[title];
  
       for (let i = 0; i < targets.length; i++) {
          // we add 1 to i in the replace function because placeholders start from $1 not $0
          let placeholder = '$' + (i + 1);
          text = text.replace(placeholder, targets[i]);
      }
      text = text.replace(/\$\d/g, ''); // remove unmatched |$x
        requestData.prependtext = text.trim() + '\n\n';
  
     
    } else if (type === 'append') { // user
        requestData.appendtext = '\n\n' + text.trim();
    } else if (type === 'text') {
        requestData.text = text;
    }
    return new Promise(function(resolve, reject) {
        if (window.abortEdits) {
            // hide message and return
            messageElement.toggle(false);
            resolve();
            return;
        }
        api.postWithEditToken(requestData).then(function(data) {
          if (data.edit && data.edit.result === 'Success') {
              messageElement.setType('success');
              messageElement.setLabel( $('<span>' + makeLink(title) + ' edited successfully</span><span class="massxfdundo" data-revid="'+data.edit.newrevid+'" data-title="'+title+'"></span>') );
  
              resolve();
          } else {
              
              messageElement.setType('error');
              messageElement.setLabel( $('<span>Error occurred while editing ' + makeLink(title) + ': '+ data + '</span>') );
              console.error('Error occurred while prepending text to page:', data);
  
              reject();
          }
        }).catch(function(error) {
            messageElement.setType('error');
          messageElement.setLabel( $('<span>Error occurred while editing ' + makeLink(title) + ': '+ error + '</span>') );
          console.error('Error occurred while prepending text to page:', error); // handle: editconflict, ratelimit (retry)
          if (error == 'editconflict') {
              editPage(title, text, summary, progressElement, ratelimitMessage, progress, type, titlesDict, retry=true).then(function() {
                  resolve();
              });
          } else if (error == 'ratelimited') {
              progress.setDisabled(true);
  
              handleRateLimitError(ratelimitMessage).then(function () {
                 progress.setDisabled(false);
                 editPage(title, text, summary, progressElement, ratelimitMessage, progress, type, titlesDict, retry=true).then(function() {
                  resolve();
                 });
              });
          }
          else {
              reject();
          }
        });
    });
  }
  
  // global scope - needed to syncronise ratelimits
  var 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();
          onbeforeunload = function() {
              return "Closing this tab will cause you to lose all progress.";
          };
          elementsToDisable = [];
          var bodyContent = $('#bodyContent');
          
          mw.util.addCSS(`.sticky-container {
            bottom: 0;
            width: 100%;
            max-height: 600px; 
            overflow-y: auto;
          }`);
          var nominationToggleObj = createNominationToggle();
          var nominationToggle = nominationToggleObj.toggle;

          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();
                  }
              }
              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 (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));
              }
              discussionLinkInputField.on('change',function() {
                  if (discussionLinkInputField.getValue().match(config.discussionLinkRegex)) {
                      oldSectionName = sectionName;
                      sectionName = discussionLinkInputField.getValue().replace(config.discussionLinkRegex, '$1').trim();
                      var text = prependTextInputField.getValue();
                      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.actions[dropdown.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++) {
                          actions[i].container.setLabel('Action batch #'+(i+1));
                          actions[i].label = 'Action batch #'+(i+1);
                      }
                  });
                  
                  container.addItems([removeButton, prependTextContainer, titleList]);
  
              } else {
                  container.addItems([prependTextContainer, titleList]);
              }
              
              return {
                  titleListInputField,
                  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', 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()) || !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) ) {
                              titles[newTitle] = 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));
              }, []);

              createAuthorList(allTitles).then(function(authors) {
  
          
                  function processContent(content, titles, textToModify, summary, type, doneMessage, headingLabel) {
                      if (!Array.isArray(titles)) {
                        var titlesDict = titles;
                        titles = Object.keys(titles);
                      }
                      var fieldset = createFieldset(headingLabel);
                      
                      content.append(fieldset.$element);
                      
                      var progressElement =  createProgressElement();
                      fieldset.addItems([progressElement]);
                      
                      var ratelimitMessage = createRatelimitMessage();
                      ratelimitMessage.toggle(false);
                      fieldset.addItems([ratelimitMessage]);
                      
                      var progressObj = createProgressBar(`(0 / ${titles.length}, 0 errors)`); // with label
                      var progress = progressObj.progressBar;
                      var progressContainer = progressObj.fieldlayout;
                      // Add margin or padding to the progress bar widget
                      progress.$element.css('margin-top', '5px');
                      progress.pushPending();
                      fieldset.addItems([progressContainer]);
                      
                      let resolvedCount = 0;
                      let rejectedCount = 0;
  
                      function updateCounter() {
                          progressContainer.setLabel(`(${resolvedCount} / ${titles.length}, ${rejectedCount} errors)`);
                      }
                      function updateProgress() {
                          var percentage = (resolvedCount + rejectedCount) / titles.length * 100;
                          progress.setProgress(percentage);
                      
                      }
                      
                      function trackPromise(promise) {
                          return new Promise((resolve, reject) => {
                              promise
                                  .then(value => {
                                      resolvedCount++;
                                      updateCounter();
                                      updateProgress();
                                      resolve(value);
                                  })
                                  .catch(error => {
                                      rejectedCount++;
                                      updateCounter();
                                      updateProgress();
                                      resolve(error);
                                  });
                          });
                      }
                      
                      return new Promise(async function(resolve) {
                          var promises = [];
                          for (const title of titles) {
                            // RfD needs special handling here, because it wraps around the whole page content
                            if (XFD === 'RFD' && type === 'prepend') { // prepend implicitly means page tagging, not actually prepend in this case
                                text = await getWikitext(title);
                                textToModify = textToModify.replace('${pageText}', text);
                                type = 'text';
                            }
                            var promise = editPage(title, textToModify, summary, progressElement, ratelimitMessage, progress, type, titlesDict);
                                promises.push(trackPromise(promise));
                                await sleep(100); // space out calls
                                await massXFDratelimitPromise; // 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="massxfdrevertlink" onclick="revertEdits()">Revert?</a></span>') );
                          
                                  content.append(abortMessage.$element);
                              } else {
                              var completedElement = createCompletedElement();
                              completedElement.setLabel(doneMessage);
                              completedElement.$element.css('margin-bottom', '16px');
                              content.append(completedElement.$element);
                              }
                              resolve();
                            })
                            .catch(function(error) {
                              console.error("Error occurred during title processing:", error);
                              resolve();
                            });
                      });
                  }
                  
                  const date = new Date();
  
                  const year = date.getUTCFullYear();
                  const month = date.toLocaleString('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 script]])';
                  // 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;
                  
                  var batchesToProcess = [];
                  
                  var newNomPromise = new Promise(function (resolve) {
                      if (isNew) {
                          nominationText = `==== ${newNomHeaderInputField.getValue().trim()} ====\n`;
                          for (const batch of batches) {
                              var action = batch.actionInputField.getValue().trim();
                              for (const page of Object.keys(batch.titles)) {
                                if (XFD == 'CFD' ){
                                  var targets = batch.titles[page].slice(); // copy array
                                  var targetText = '';
                                  if (targets.length) {
                                      if (targets.length === 2) {
                                          targetText = ` to [[:${targets[0]}]] and [[:${targets[1]}]]`;
                                      }
                                      else if (targets.length > 2) {
                                          var lastTarget = targets.pop();
                                          targetText = ' to [[:' + targets.join(']], [[:') + ']], and [[:' + lastTarget + ']]';
                                      } else { // 1 target
                                          targetText = ' to [[:' + targets[0] + ']]';
                                      }
                                  }
                                  nominationText +=`:* '''Propose ${action}''' {{${categoryTemplateDropdown.getValue()}|${page}}}${targetText}\n`;
                                } else {
                                    nominationText += config.displayTemplate.replaceAll('${pageName}', page) + '\n';
                                }
                              }
                          }
                          var rationale = rationaleInputField.getValue().trim().replace(/\n/, '<br />');
                          nominationText += `${XFD === 'CFD' ? ":'''Nominator's rationale:''' " : ''}${rationale} ~~~~`;
                          var newText;
                          var nominationRegex = /(?:==== ?NEW NOMINATIONS ?====\s*(?:<!-- ?Please add the newest nominations below this line ?-->)?|<noinclude>This is a list of redirects that have been proposed for deletion or other action on .+\.<\/noinclude>)/;
                          getWikitext(discussionPage).then(function(wikitext) {
                              if ( !wikitext.match(nominationRegex) ) {
                                  var nominationErrorMessage = createNominationErrorMessage();
                                  bodyContent.append(nominationErrorMessage.$element);
                              } else {
                                  newText = wikitext.replace(nominationRegex, '$&\n\n'+nominationText); // $& contains all the matched text
                                  batchesToProcess.push({
                                      content: bodyContent,
                                      titles: [discussionPage],
                                      textToModify: newText,
                                      summary: nominationSummary,
                                      type: 'text',
                                      doneMessage: 'Nomination added',
                                      headingLabel: 'Creating nomination'
                                  });
                                  resolve();
                              }
                          }).catch(function (error) {
                              console.error('An error occurred in fetching wikitext:', error);
                              resolve();
                          });
                      } else resolve();
                  });
                  newNomPromise.then(async function () {
                      batches.forEach(batch => {
                          batchesToProcess.push({
                                  content: bodyContent,
                                  titles: batch.titles,
                                  textToModify: batch.prependText,
                                  summary: categorySummary,
                                  type: 'prepend',
                                  doneMessage: 'All pages edited.',
                                  headingLabel: 'Editing nominated pages' + ((batches.length > 1) ? ' — '+batch.label : '')
                              });
                      });
                      if (notifyCheckbox.isSelected()) { 
                          batchesToProcess.push({
                              content: bodyContent,
                              titles: authors,
                              textToModify: userNotification,
                              summary: userSummary,
                              type: 'append',
                              doneMessage: 'All users notified.',
                              headingLabel: 'Notifying users'
                          });
                      }
                      let promise = Promise.resolve();
                      // abort handling is now only in the editPage() function
                      for (const batch of batchesToProcess) {
                          await processContent(...Object.values(batch));
                      }
                      
                      promise.then(() => {
                          abortButton.setLabel('Revert');
                          // All done
                      }).catch(err => {
                          console.error('Error occurred:', err);
                      });
                  });
              });
          });
      }); 
    }
  }
  
  // Run the script when the page is ready
  $(document).ready(runMassXFD);
  // </nowiki>