User:Qwerfjkl/scripts/massXFD.js: Difference between revisions

Content deleted Content added
Update with bug fix for CfD, cleaning up code significantly, and code for getting redirect data [Factotum]
m (via CAE)
 
(19 intermediate revisions by the same user not shown)
Line 1:
// <nowiki>
// TFD:
// handle redirects
// Cat support (when emptying a cat of templates)
// oldtfdlist for previous TfDs
// notify Twinkle/UV/AWB
 
 
// 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);
function capitalise(s) {
}
return s[0].toUpperCase() + s.slice(1);
}
 
const NS_TEMPLATE = 10;
const NS_CATEGORY = 14;
const NS_MODULE = 828;
 
 
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:BishopssectionName}}",
"titleDemoText": "Category:FirstNominatedCategory|Category:FirstNominatedCategoryTarget1|Category:Target2\nCategory:Foo|Category:Bar\nCategory:Earth",
"discussionLinkRegex": /^Wikipedia:Categories for discussion\/Log\/\d\d\d\d \w+ \d\d?#(.+)$/,
"discussionLinkRegex": /^Wikipedia:Categories for discussion\/Log\/\d\d\d\d \w+ \d\d?#(.+)$/,
"nominationReplacement": [/==== ?NEW NOMINATIONS ?====\s*(?:<!-- ?Please add the newest nominations below this line ?-->)?/, '$&\n\n${nominationText}'],
"nominationReplacement": [/==== ?NEW NOMINATIONS ?====\s*(?:<!-- ?Please add the newest nominations below this line ?-->)?/, '$&\n\n${nominationText}'],
"userNotificationTemplate": 'Cfd mass notice',
"baseDiscussionPageuserNotificationTemplate": 'Wikipedia:CategoriesCfd formass discussion/Log/notice',
"baseDiscussionPage": 'Wikipedia:Categories for discussion/Log/',
"normaliseFunction": (title) => { return 'Category:' + capitalise(title.replace(/^ *[Cc]ategory:/, '').trim()); },
"normaliseFunction": (title) => { return (new mw.Title(title, NS_CATEGORY)).getPrefixedText() },
"actions": {
"Deleteactions": {
"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': ''
},
},
"RenamedisplayTemplates": [{
'prepend'data: '{{subst:Cfr|$1|${sectionName}}}lc',
'action'label: 'renamingCategory link with extra links – {{lc}}'
},
"Merge": {
'prepend'data: '{{subst:Cfm|$1|${sectionName}}}clc',
'action'label: 'mergingCategory link with count – {{clc}}'
},
"Split": {
'prepend'data: '{{subst:Cfs|$1|$2|${sectionName}}}cl',
'action'label: 'splittingPlain 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": "",
"titleDemoText": "Title1\nTitle2\nTitle2",
"discussionLinkRegex": /^Wikipedia:Redirects for discussion\/Log\/\d\d\d\d \w+ \d\d?#(.+)$/,
"nominationReplacement": [/<!-- ?Add new entries directly below this line\. ?-->/, '$&\n${nominationText}\n'],
"userNotificationTemplate": 'Rfd mass notice',
"baseDiscussionPage": 'Wikipedia:Redirects for discussion/Log/',
"normaliseFunction": (title) => { return new mw.Title(title).getPrefixedText() },
"actions":
{
'prepend': '{{subst:rfd|${sectionName}|content=\n${pageText}\n}}'
},
"displayTemplate": "{{subst:rfd2|multi=yes|redirect=${pageName}|target=${redirectTarget}}}"
"Listify": {
},
'prepend': '{{subst:Cfl|$1|${sectionName}}}',
'action'"TFD": 'listifying'{
"title": "Mass TfD",
"placeholderDiscussionLink": 'Wikipedia:Templates for discussion/Log/2025 August 4#Template:COVID-19 pandemic interactive maps/India/India cases',
"placeholderNominationTitle": 'COVID-19 pandemic interactive maps/India templates',
"placeholderRationale": 'Unused templates.',
"pageDemoText": "{{subst:Tfd|sectionName}}",
"titleDemoText": "Template:Foo\nTemplate:Bar",
"discussionLinkRegex": /^Wikipedia:Templates for discussion\/Log\/\d\d\d\d \w+ \d\d?#(.+)$/,
"nominationReplacement": [/=== ?\[\[Wikipedia:Templates for discussion\/Log\/\d\d\d\d \w+ \d\d?\|\w+ \d\d?\]\] ?===\s*(?:<!-- *Add new listings at the top of the list with the following formats for deletion and merging respectively:[\s\S]+?-->)?/, '$&\n${nominationText}'],
"userNotificationTemplate": 'Tfd mass notice',
"baseDiscussionPage": 'Wikipedia:Templates for discussion/Log/',
"normaliseFunction": (title) => {
let titleObj = new mw.Title(title);
if (titleObj.getNamespaceId() != 0) {
return titleObj.getPrefixedText()
} else {
return new mw.Title(title, NS_TEMPLATE).getPrefixedText(); // assume Template:
}
},
"Customactions": {
'prepend'"Delete": '{{subst:Cfd|type=|${sectionName}}}',
'actionprepend': '{{subst:Tfd|heading=${sectionName}}}',
'action': 'deleting'
},
"Delete (inline notice)": {
'prepend': '{{subst:Tfd|type=inline|heading=${sectionName}}}',
'action': 'deleting'
},
"Delete (tiny notice)": {
'prepend': '{{subst:Tfd|type=tiny|heading=${sectionName}}}',
'action': 'deleting'
},
"Delete (sidebar notice)": {
'prepend': '{{subst:Tfd|type=sidebar|heading=${sectionName}}}',
'action': 'deleting'
},
"Delete (disabled notice)": {
'prepend': '{{subst:Tfd|type=disabled|heading=${sectionName}}}',
'action': 'deleting'
},
"Merge": {
'prepend': '{{subst:Tfm|$1|heading=${sectionName}}}}',
'action': 'merging'
},
"Merge (inline notice)": {
'prepend': '{{subst:Tfm|$1|type=inline|heading=${sectionName}}}',
'action': 'merging'
},
"Merge (tiny notice)": {
'prepend': '{{subst:Tfm|$1|type=tiny|heading=${sectionName}}}',
'action': 'merging'
},
"Merge (sidebar notice)": {
'prepend': '{{subst:Tfm|$1|type=sidebar|heading=${sectionName}}}',
'action': 'merging'
},
"Merge (disabled notice)": {
'prepend': '{{subst:Tfm|$1|type=disabled|heading=${sectionName}}}',
'action': 'merging'
},
"Custom delete": {
'prepend': '{{subst:Tfd|type=|heading=${sectionName}}}',
'action': ''
},
"Custom merge": {
'prepend': '{{subst:Tfm|$1|type=|heading=${sectionName}}}',
'action': ''
},
},
"displayTemplate": "{{Tfd links|${pageName}${moduleText}}}"
},
};
"displayTemplates": [{
const match = /Special:Mass(\w+)/.exec(mw.config.get('wgPageName'));
data: 'lc',
const XFD = match ? match[1].toUpperCase() : false;
label: 'Category link with extra links – {{lc}}'
const config = },XFDconfig[XFD];
{
data: 'clc',
label: 'Category link with count – {{clc}}'
},
{
data: 'cl',
label: 'Plain category link – {{cl}}'
}],
 
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({
"RFD": {
"title": "Mass RfD" padded: true,
expanded: false,
"placeholderDiscussionLink": 'Wikipedia:Redirects for discussion/Log/2024 May 13#Knightfall (comics)',
"placeholderNominationTitle" classes: ['Knightfallsticky-container',]
});
"placeholderRationale": 'No mention of "Knightfall" in the target article.',
"pageDemoText":return "",progressContainer;
"discussionLinkRegex": /^Wikipedia:Redirects for discussion\/Log\/\d\d\d\d \w+ \d\d?#(.+)$/,
"nominationReplacement": [/<!-- ?Add new entries directly below this line\. ?-->/, '$&\n${nominationText}\n'],
"userNotificationTemplate": 'Rfd mass notice',
"baseDiscussionPage": 'Wikipedia:Redirects for discussion/Log/',
"normaliseFunction": (title) => { return 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 wipePageContentmakeInfoPopup(info) {
var infoPopup = new OO.ui.PopupButtonWidget({
var bodyContent = $('#bodyContent');
icon: 'info',
if (bodyContent) {
bodyContent.empty(); 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;
}
 
var header = $('#firstHeading');
function makeCategoryTemplateDropdown(label) {
if (header) {
var dropdown = new OO.ui.DropdownInputWidget({
header.text(config.title);
required: true,
options: config.displayTemplates
});
var fieldlayout = new OO.ui.FieldLayout(
dropdown,
{
label,
align: 'inline',
classes: ['newnomonly'],
}
);
return { container: fieldlayout, dropdown };
}
$('title').text(`${config.title} - Wikipedia`);
}
 
function createTitleAndInputFieldWithLabel(label, placeholder, classes = []) {
function createProgressElement() {
var progressContainerinput = new OO.ui.PanelLayoutTextInputWidget({
padded: true, placeholder
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;
}
 
var fieldset = new OO.ui.FieldsetLayout({
function makeCategoryTemplateDropdown(label) {
classes
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 };
}
 
fieldset.addItems([
function createTitleAndInputFieldWithLabel(label, placeholder, classes = []) {
var input = new OO.ui.TextInputWidgetFieldLayout(input, {
placeholder 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 fieldsettitleLabel = new OO.ui.FieldsetLayoutLabelWidget({
label: $(`<span>${title}</span>`)
classes
});
 
var infoPopup = makeInfoPopup(info);
fieldset.addItems([
var inputField = new OO.ui.FieldLayoutMultilineTextInputWidget(input, {
labelplaceholder,
}) 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
return {
function createTitleAndSingleInputField(title, placeholder) {
container: fieldset,
var container = new OO.ui.PanelLayout({
inputField: input,
expanded: false
};
});
}
// 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 infoPopupinputField = makeInfoPopupnew OO.ui.TextInputWidget(info);{
placeholder,
var inputField = new OO.ui.MultilineTextInputWidget({
placeholder, indicator: 'required'
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,
};
}
 
container.$element.append(titleLabel.$element, inputField.$element);
// 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(return {
label: title titleLabel,
inputField,
});
container
};
}
 
function createStartButton() {
var inputField = new OO.ui.TextInputWidget({
var button = new OO.ui.ButtonWidget({
placeholder,
indicator label: 'requiredStart',
flags: ['primary', 'progressive']
});
});
 
return button;
container.$element.append(titleLabel.$element, inputField.$element);
}
 
function createAbortButton() {
return {
var button = new OO.ui.ButtonWidget({
titleLabel,
inputField label: 'Abort',
flags: ['primary', 'destructive']
container
});
}
 
return button;
function createStartButton() {
}
var button = new OO.ui.ButtonWidget({
label: 'Start',
flags: ['primary', 'progressive']
});
 
function createRemoveBatchButton() {
return button;
var button = new OO.ui.ButtonWidget({
}
label: 'Remove',
icon: 'close',
title: 'Remove',
classes: [
'remove-batch-button'
],
flags: [
'destructive'
]
});
return button;
}
 
function createAbortButtoncreateNominationToggle() {
var button = new OO.ui.ButtonWidget({
label: 'Abort',
flags: ['primary', 'destructive']
});
 
var newNomToggle = new OO.ui.ButtonOptionWidget({
return button;
data: 'new',
}
label: 'New nomination',
selected: true
});
var oldNomToggle = new OO.ui.ButtonOptionWidget({
data: 'old',
label: 'Old nomination',
});
 
var toggle = new OO.ui.ButtonSelectWidget({
function createRemoveBatchButton() {
items: [
var button = new OO.ui.ButtonWidget({
label: 'Remove' newNomToggle,
icon: 'close', oldNomToggle
title: 'Remove', ]
classes: [});
return 'remove-batch-button'{
] toggle,
flags: [ newNomToggle,
'destructive'oldNomToggle,
]};
});
return button;
}
 
function createNominationToggle() {
 
function createMessageElement() {
var newNomToggle = new OO.ui.ButtonOptionWidget({
data:var messageElement = 'new', OO.ui.MessageWidget({
label type: 'New nominationprogress',
selected inline: true,
progressType: 'infinite'
});
});
var oldNomToggle = new OO.ui.ButtonOptionWidget({
data:return 'old',messageElement;
}
label: 'Old nomination',
});
 
function createWarningMessage() {
var toggle = new OO.ui.ButtonSelectWidget({
var warningMessage = new OO.ui.MessageWidget({
items: [
newNomToggletype: 'warning',
oldNomTogglestyle: 'background-color: yellow;'
]});
return warningMessage;
});
return {}
toggle,
newNomToggle,
oldNomToggle,
};
}
 
function createCompletedElement() {
var messageElement = new OO.ui.MessageWidget({
type: 'success',
});
return messageElement;
}
 
function createMessageElementcreateDoingElement() {
var messageElement = new OO.ui.MessageWidget({
type: 'progressinfo',
inline: true,});
progressType:return 'infinite'messageElement;
});
return messageElement;
}
 
function createAbortMessage() { // pretty much a duplicate of ratelimitMessage
function createRatelimitMessage() {
var ratelimitMessageabortMessage = new OO.ui.MessageWidget({
type: 'warning',
style: 'background-color: yellow});'
return abortMessage;
});
}
return ratelimitMessage;
}
 
function createCompletedElementcreateErrorMessage(text) {
var messageElementerrorMessage = new OO.ui.MessageWidget({
type: 'successerror',
});
errorMessage.setLabel(text);
return messageElement;
return errorMessage;
}
}
 
function createAbortMessagecreateNominationErrorMessage() { // pretty much a duplicate of ratelimitMessage
return createErrorMessage('Could not detect where to add new nomination.')
var abortMessage = new OO.ui.MessageWidget({
}
type: 'warning',
});
return abortMessage;
}
 
function createFieldset(headingLabel) {
function createNominationErrorMessage() { // pretty much a duplicate of ratelimitMessage
var nominationErrorMessagefieldset = new OO.ui.MessageWidgetFieldsetLayout({
type label: 'error'headingLabel,
});
text: 'Could not detect where to add new nomination.'
}) return fieldset;
}
return nominationErrorMessage;
}
 
function createFieldsetcreateCheckboxWithLabel(headingLabellabel) {
var fieldsetcheckbox = new OO.ui.FieldsetLayoutCheckboxInputWidget({
label value: headingLabel'a',
selected: true,
});
label: "Foo",
return fieldset;
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({
function createCheckboxWithLabel(label) {
label: 'Mass action',
var checkbox = new OO.ui.CheckboxInputWidget({
value menu: 'a',{
selected: true, items
label: "Foo", }
data: "foo"});
return { dropdown });
}
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));
 
function createMultiOptionButton() {
var dropdown = new OO.ui.DropdownWidget({
label:var 'Massbutton action',= new OO.ui.ButtonWidget({
menu label: {'Additional action',
itemsicon: 'add',
} flags: [
'progressive'
});
return { dropdown }; ]
});
}
return button;
}
 
function createMultiOptionButtonsleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
var button = new OO.ui.ButtonWidget({
}
label: 'Additional action',
icon: 'add',
flags: [
'progressive'
]
});
return button;
}
 
function sleepmakeLink(mstitle) {
return `<a href="/wiki/${title}" target="_blank">${title}</a>`;
return new Promise(resolve => setTimeout(resolve, ms));
}
}
 
function makeLinkgetDateDifference(titledate1) {
const currentDate = new Date();
return `<a href="/wiki/${title}" target="_blank">${title}</a>`;
// now
}
let date2 = `${currentDate.getUTCFullYear()} ${currentDate.toLocaleString('en', { month: 'long', timeZone: 'UTC' })} ${currentDate.getUTCDate()}`
 
// Parse the dates
function parseHTML(html) {
const parseDate = (dateString) => {
// Create a temporary div to parse the HTML
const [year, month, day] = dateString.split(' ');
var tempDiv = $('<div>').html(html);
return new Date(`${year}-${month}-${day}`);
};
 
const d1 = parseDate(date1);
// Find all li elements
var liElements const d2 = tempDiv.findparseDate('li'date2);
 
// Calculate the time difference in milliseconds
// Array to store extracted hrefs
const timeDifference = Math.abs(d2 - d1);
var hrefs = [];
 
// Convert the time difference from milliseconds to days
let existinghrefRegexp = /^https:\/\/en\.wikipedia.org\/wiki\/([^?&]+?)$/;
const dayDifference = Math.ceil(timeDifference / (1000 * 60 * 60 * 24));
let nonexistinghrefRegexp = /^https:\/\/en\.wikipedia\.org\/w\/index\.php\?title=([^&?]+?)&action=edit&redlink=1$/;
 
return dayDifference;
// Iterate through each li element
}
liElements.each(function () {
// Find all anchor (a) elements within the current li
let hrefline = [];
var anchorElements = $(this).find('a');
 
function deepCopy(obj) {
// Extract href attribute from each anchor element
if (obj === null || typeof obj !== 'object') {
anchorElements.each(function () {
varreturn href = $(this).attr('href')obj;
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());
}
 
if (obj instanceof OO.ui.Element) {
return obj;
}
 
if (Array.isArray(obj)) {
const copy = [];
for (let i = 0; i < obj.length; i++) {
copy[i] = deepCopy(obj[i]);
}
return copy;
}
 
const copy = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepCopy(obj[key]);
}
}
return copy;
}
 
function delinkWikitext(text) {
// both piped and unpiped wikilinks
const wikilinkPattern = /\[\[([^\]]+)\|([^\]]+)\]\]|\[\[([^\]]+)\]\]/g;
 
return text.replace(wikilinkPattern, (match, p1, p2, p3) => {
if (p1 && p2) {
// If there is both link text and target (piped)
return p2; // Just return the link text
} else if (p3) {
// If there is only link text, without a target (unpiped)
return p3;
}
return match;
});
}
hrefs.push(hrefline);
});
 
function parseHTML(html) {
return hrefs;
// Create a temporary div to parse the HTML
}
var tempDiv = $('<div>').html(html);
 
// Find all li elements
function handlepaste(widget, e) {
var types,liElements pastedData,= parsedDatatempDiv.find('li');
// 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');
 
// Array to store parsedDataextracted = parseHTML(pastedData);hrefs
var hrefs = [];
 
let existinghrefRegexp = /^https:\/\/en\.wikipedia.org\/wiki\/([^?&]+?)$/;
// Check if it's an empty array
let nonexistinghrefRegexp = /^https:\/\/en\.wikipedia\.org\/w\/index\.php\?title=([^&?]+?)&action=edit&redlink=1$/;
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 dataIterate fromthrough actuallyeach beingli pastedelement
eliElements.stopPropagationeach(function (); {
e.preventDefault// Find all anchor (a); elements within the current li
returnlet falsehrefline = [];
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) {
// Allow the paste event to propagate for plain text
var types, pastedData, parsedData;
return true;
// Browsers that support the 'text/html' type in the Clipboard API (Chrome, Firefox 22+)
}
if (e && e.clipboardData && e.clipboardData.types && e.clipboardData.getData) {
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);
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();
 
// RestoreCheck if it's an savedempty contentarray
if (!parsedData || parsedData.length === 0) {
widget.setValue(savedContent);
// 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
// Call callback
e.stopPropagation();
processPaste(widget, pastedData);
e.preventDefault();
return false;
}
}
 
// Allow the paste event to propagate for plain text
return true;
}
 
// Else wait 20ms and try again
function waitForPastedData(widget, savedContent) {
else {
// If data has been processed by the browser, process it
setTimeout(function () {
if waitForPastedData(widget,.getValue() !== savedContent); {
// Retrieve pasted content via widget's getValue()
}, 20);
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);
}
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 getRedirectData(titles) {
var requestData = {
action:var 'edit',api = new mw.Api();
title,return api.get({
undo action: revid'query',
format: 'json' titles,
redirects: 1,
};
format: 'json'
return new Promise(function (resolve, reject) {
api.postWithEditToken(requestData}).then(function (data) {
ifreturn (data.edit && data.edit.result === 'Success') {query;
resolve(true);
} else {
console.error('Error occurred while undoing edit:', data);
reject();
}
}).catch(function (error) {
console.error('Error occurred while undoingfetching editpage author:', error); // handle: editconflict, ratelimit (retry)
ifreturn (error == 'editconflict') {false;
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 getRedirectDatagetUserData(titles) {
var api = new mw.Api();
return api.get({
action: 'query',
titles list: 'users',
redirects ususers: 1titles,
usprop: 'blockinfo|groups', // blockinfo - check if indeffed, groups - check if bot
format: 'json'
format: 'json'
}).then(function (data) {
}).then(function (data) {
return data.query; // needs redirects and normalized
return data.query.users;
}).catch(function (error) {
}).catch(function (error) {
console.error('Error occurred while fetching page author:', error);
return false;
});
}
}
 
function getUserDatagetPageAuthor(titlestitle) {
var api = new mw.Api();
return api.get({
action: 'query',
list prop: 'usersrevisions',
ususers: titles: title,
rvprop: 'user',
usprop: 'blockinfo|groups', // blockinfo - check if indeffed, groups - check if bot
rvdir: 'newer', // Sort the revisions in ascending order (oldest first)
format: 'json'
rvlimit: 1,
}).then(function (data) {
return data.query.users; format: 'json'
}).catchthen(function (errordata) {
var pages = data.query.pages;
console.error('Error occurred while fetching page author:', error);
var pageId = Object.keys(pages)[0];
return false;
var revisions = pages[pageId].revisions;
});
if (revisions && revisions.length > 0) {
}
 
return revisions[0].user;
function getPageAuthor(title) {
var api = new mw.Api(); } else {
return api.get({false;
action: 'query', }
prop:}).catch(function 'revisions',(error) {
console.error('Error occurred while fetching page author:', error);
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 = [];
console.log(titles)
var authorListpromises = [];titles.map(function (title) {
var promises = titles.map(function return getPageAuthor(title) {;
return getPageAuthor(title});
}); try {
const authors = await Promise.all(promises);
try {
const authors = await Promise.all(promises)let queryBatchSize = 50;
let authorTitles = authors.filter(Boolean).map(author => author.replace(/ /g, '_')); // Replace spaces with underscores, remove false values
let queryBatchSize = 50;
let filteredAuthorList = [];
let authorTitles = authors.map(author => author.replace(/ /g, '_')); // Replace spaces with underscores
for (let i = 0; i < authorTitles.length; i += queryBatchSize) {
let filteredAuthorList = [];
for (let i = 0; i < let batch = authorTitles.length;slice(i, i += queryBatchSize) {;
let batchbatchTitles = authorTitlesbatch.slicejoin(i, i + queryBatchSize'|');
let batchTitles = batch.join('|');
 
await getUserData(batchTitles)
.then(response => {
response.forEach(user => {
if (window.debuggingMode) console.log(user);
if (user
&& (!user.blockexpiry || user.blockexpiry !== "infinite" || 'blockpartial' in user)
&& !user.groups?.includes('bot')
&& !filteredAuthorList.includes('User talk:' + user.name))
filteredAuthorList.push('User talk:' + user.name);
});
 
})
.catch(error => {
console.error("Error querying API:", error);
});
}
return filteredAuthorList;
} catch (error_1) {
console.error('Error occurred while creating author list:', error_1);
return authorList;
}
return filteredAuthorList;
} catch (error_1) {
console.error('Error occurred while creating author list:', error_1);
return authorList;
}
}
 
// Function to create a list of page authors and filter duplicates
async function createRedirectTargetsList(titles) {
try {
let queryBatchSize = 50;
let redirectTitles = titles.map(title => title.replace(/ /g, '_')); // Replace spaces with underscores
let redirectTargets = {};
let nonredirects = [];
for (let i = 0; i < redirectTitles.length; i += queryBatchSize) {
let batch = redirectTitles.slice(i, i + queryBatchSize);
let batchTitles = batch.join('|');
 
await getRedirectData(batchTitles)
.then(data => {
 
let normalized = {};
data.normalized.forEach if (normalizedTitle'redirects' =>in data) {
normalized[normalizedTitle data.to]redirects.forEach(redirect => normalizedTitle.from;{
redirectTargets[redirect.from] = redirect.tofragment ? redirect.to + "#" + redirect.tofragment : redirect.to;
}
});
let redirects = new Set(data.redirects.map(r => r.to));
let pages = new Set(Object.values(data.pages).map(p => p.title));
nonredirects.push(...[...pages].filter(x => !redirects.has(x)));
} else {
nonredirects.push(...Object.values(data.pages).map(p => p.title));
}
 
redirects.forEach(item => {
if (item.from in normalized) redirectTargets[normalized[item.from]] = item.to;
else redirectTargets[item.from] = item.to;
})
}) .catch(error => {
console.catch(error("Error =>querying {API:", error);
console.error("Error querying API:", error});
});
return [redirectTargets, nonredirects];
} catch (error_1) {
console.error('Error occurred while fetching redirect targets', error_1);
return [redirectTargets, nonredirects];
}
return redirectTargets;
} catch (error_1) {
console.error('Error occurred while fetching redirect targets', error_1);
return redirectTargets;
}
}
 
function editPage(options) {
console.log const localOptions = deepCopy(options);
options localOptions.text = optionslocalOptions.textToModify;
var const api = new mw.Api();
const messageElement = createMessageElement();
 
messageElement.setLabel((localOptions.retry)
var messageElement = createMessageElement();
? $('<span>').text('Retrying ').append($(makeLink(localOptions.title)))
: $('<span>').text('Editing ').append($(makeLink(localOptions.title))));
 
localOptions.progressElement.$element.append(messageElement.$element);
const container = $('.sticky-container');
container.scrollTop(container.prop("scrollHeight"));
 
if (localOptions.retry) {
sleep(1000);
}
 
const requestData = {
messageElement.setLabel((options.retry) ? $('<span>').text('Retrying ').append($(makeLink(options.title))) : $('<span>').text('Editing ').append($(makeLink(options.title))));
action: 'edit',
options.progressElement.$element.append(messageElement.$element);
title: window.debuggingMode ? 'User:Qwerfjkl/sandbox/51' : localOptions.title,
var container = $('.sticky-container');
summary: localOptions.summary,
container.scrollTop(container.prop("scrollHeight"));
format: 'json'
if (options.retry) {
sleep(1000)};
}
 
var requestData = {
action: 'edit',
//title: 'User:Qwerfjkl/sandbox/51',
title: options.title,
summary: options.summary,
format: 'json'
};
 
if (optionslocalOptions.type === 'prepend') { // tagging
requestData.nocreate = 1; // don't create new page when tagging
const targets = localOptions.titlesDict[localOptions.title];
// parse title
var targets = options.titlesDict[options.title];
 
for (let i = 0; i < targets.length; i++) {
// we add 1 toconst iplaceholder in= the'$' replace+ function(i because+ placeholders start from $1 not $0);
localOptions.text = localOptions.text.replace(placeholder, targets[i]);
let placeholder = '$' + (i + 1);
}
options.text = options.text.replace(placeholder, targets[i]);
localOptions.text = localOptions.text.replace(/\$\d/g, '');
requestData.prependtext = localOptions.text.trim() + '\n\n';
} else if (localOptions.type === 'append') {
requestData.appendtext = '\n\n' + localOptions.text.trim();
} else if (localOptions.type === 'text') {
requestData.text = localOptions.text;
}
options.text = options.text.replace(/\$\d/g, ''); // remove unmatched |$x
requestData.prependtext = options.text.trim() + '\n\n';
 
 
} else if (options.type === 'append') { // user
requestData.appendtext = '\n\n' + options.text.trim();
} else if (options.type === 'text') {
requestData.text = options.text;
}
console.log(requestData)
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(options.title) + ' edited successfully</span><span class="massxfdundo" data-revid="' + data.edit.newrevid + '" data-title="' + options.title + '"></span>'));
 
return new Promise((resolve, reject) => {
if (window.abortEdits) {
messageElement.toggle(false);
resolve();
} else { return;
 
messageElement.setType('error');
messageElement.setLabel($('<span>Error occurred while editing ' + makeLink(options.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(options.title) + ': ' + error + '</span>'));
console.error('Error occurred while prepending text to page:', error); // handle: editconflict, ratelimit (retry)
 
if api.postWithEditToken(error == 'editconflict'requestData) {
editPage(options).then(function (data) => {
resolveif (data.edit && data.edit.result === 'Success'); {
} messageElement.setType('success');
messageElement.setLabel($('<span>' + makeLink(localOptions.title) + ' edited successfully</span><span class="massxfdundo" data-revid="' + data.edit.newrevid + '" data-title="' + localOptions.title + '"></span>'));
} else if (error == 'ratelimited') {
options.progress.setDisabled(true);
 
handleRateLimitError(options.ratelimitMessage).then(function () {
options.progress.setDisabled(false);
editPage(options).then(function () {
resolve();
}); else {
handleError('Error occurred while editing', data, localOptions, messageElement, resolve, reject);
});
}
else { })
.catch((error) => handleError('Error occurred while editing', error, localOptions, messageElement, resolve, reject));
reject();
}
});
});
}
 
function handleError(msg, error, options, messageElement, resolve, reject) {
// global scope - needed to syncronise ratelimits
messageElement.setType('error');
var massXFDratelimitPromise = null;
messageElement.setLabel($('<span>' + msg + ' ' + makeLink(options.title) + ': ' + error + '</span>'));
// Function to handle rate limit errors
console.error(msg + ' page:', error);
function handleRateLimitError(ratelimitMessage) {
var modify = !(ratelimitMessage.isVisible()); // only do something if the element hasn't already been shown
 
if (massXFDratelimitPromiseerror !=== null'editconflict') {
editPage(deepCopy(options)).then(resolve);
return massXFDratelimitPromise;
} else if (error === 'ratelimited') {
options.progress.setDisabled(true);
handleRateLimitError(options.ratelimitMessage).then(() => {
options.progress.setDisabled(false);
editPage(deepCopy(options)).then(resolve);
});
} else {
reject();
}
}
 
massXFDratelimitPromise = new Promise(function (resolve) {
var remainingSeconds = 60;
var secondsToWait = remainingSeconds * 1000;
console.log('Rate limit reached. Waiting for ' + remainingSeconds + ' seconds...');
 
// global scope - needed to syncronise ratelimits
ratelimitMessage.setType('warning');
var massXFDratelimitPromise = null;
ratelimitMessage.setLabel('Rate limit reached. Waiting for ' + remainingSeconds + ' seconds...');
// Function to handle rate limit errors
ratelimitMessage.toggle(true);
function handleRateLimitError(ratelimitMessage) {
var modify = !(ratelimitMessage.isVisible()); // only do something if the element hasn't already been shown
 
varif countdownInterval(massXFDratelimitPromise !== setInterval(function (null) {
remainingSeconds--return massXFDratelimitPromise;
if (modify) {}
ratelimitMessage.setLabel('Rate limit reached. Waiting for ' + remainingSeconds + ' second' + ((remainingSeconds === 1) ? '' : 's') + '...');
}
 
massXFDratelimitPromise = new Promise(function (resolve) {
if (remainingSeconds <= 0 || window.abortEdits) {
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);
massXFDratelimitPromise = null; // reset
ratelimitMessage.toggle(false);
massXFDratelimitPromise = null; // reset
resolve();
}, secondsToWait);
}, 1000);
return massXFDratelimitPromise;
}
 
// Function to show progress visually
// Use setTimeout to ensure the promise is resolved even if the countdown is not reached
setTimeout(function createProgressBar(label) {
var progressBar = new clearIntervalOO.ui.ProgressBarWidget(countdownInterval);
ratelimitMessageprogressBar.togglesetProgress(false0);
var fieldlayout = new OO.ui.FieldLayout(progressBar, {
massXFDratelimitPromise = null; // reset
resolve();label,
}, secondsToWait); align: 'inline'
});
return massXFDratelimitPromise;{
progressBar,
}
fieldlayout
};
}
 
// 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) {
// Main function to execute the script
mw.util.addPortletLink('p-tb', mw.util.getUrl(`Special:Mass${XfD}`), `Mass ${XfD}`, `pt-mass${XfD.toLowerCase()}`, `Create a mass ${XfD} nomination`);
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 && config) {
// Load the required modules
mw.loader.using('oojs-ui').done(function () {
wipePageContent();
if (!window.debuggingMode) { // annoying when reloading for debugging
onbeforeunload = function () {
return "Closing this tab will cause you to lose all progress.";
};
}
elementsToDisable = [];
var bodyContent = $('#bodyContent');
 
mw.util.addCSS(`.sticky-container {
if (XFD) {
// Load the required modules bottom: 0;
width: 100%;
mw.loader.using('oojs-ui').done(function () {
wipePageContent() max-height: 600px;
onbeforeunload = function()overflow-y: {auto;
}`); // should probably be styled returndirectly "Closingon thisthe tabelement willthan causevia youthe to lose all progress.";stylesheet
} var nominationToggleObj = createNominationToggle();
elementsToDisable var nominationToggle = []nominationToggleObj.toggle;
var bodyContent = $('#bodyContent');
 
bodyContent.append(nominationToggle.$element);
mw.util.addCSS(`.sticky-container {
bottom: 0 elementsToDisable.push(nominationToggle);
width: 100%;
max-height: 600px;
overflow-y: auto;
}`);
var nominationToggleObj = createNominationToggle();
var nominationToggle = nominationToggleObj.toggle;
 
var rationaleObj = createTitleAndInputField('Rationale:', config.placeholderRationale);
bodyContent.append(nominationToggle.$element);
var rationaleContainer = rationaleObj.container;
elementsToDisable.push(nominationToggle);
var rationaleInputField = rationaleObj.inputField;
elementsToDisable.push(rationaleInputField);
 
var nominationToggleOld = nominationToggleObj.oldNomToggle;
var rationaleObj = createTitleAndInputField('Rationale:', config.placeholderRationale);
var rationaleContainernominationToggleNew = rationaleObjnominationToggleObj.containernewNomToggle;
var rationaleInputField = rationaleObj.inputField;
elementsToDisable.push(rationaleInputField);
 
var discussionLinkObj = createTitleAndSingleInputField('Discussion link', config.placeholderDiscussionLink);
var nominationToggleOld = nominationToggleObj.oldNomToggle;
var nominationToggleNewdiscussionLinkContainer = nominationToggleObjdiscussionLinkObj.newNomTogglecontainer;
var discussionLinkInputField = discussionLinkObj.inputField;
elementsToDisable.push(discussionLinkInputField);
 
var discussionLinkObjnewNomHeaderObj = createTitleAndSingleInputField('DiscussionNomination linktitle', config.placeholderDiscussionLinkplaceholderNominationTitle);
var discussionLinkContainernewNomHeaderContainer = discussionLinkObjnewNomHeaderObj.container;
var discussionLinkInputFieldnewNomHeaderInputField = discussionLinkObjnewNomHeaderObj.inputField;
elementsToDisable.push(discussionLinkInputFieldnewNomHeaderInputField);
 
bodyContent.append(discussionLinkContainer.$element);
var newNomHeaderObj = createTitleAndSingleInputField('Nomination title', config.placeholderNominationTitle);
bodyContent.append(newNomHeaderContainer.$element, rationaleContainer.$element);
var newNomHeaderContainer = newNomHeaderObj.container;
var newNomHeaderInputField = newNomHeaderObj.inputField; function displayElements() {
if (nominationToggleOld.isSelected()) {
elementsToDisable.push(newNomHeaderInputField);
discussionLinkContainer.$element.show();
newNomHeaderContainer.$element.hide();
rationaleContainer.$element.hide();
}
else if (nominationToggleNew.isSelected()) {
discussionLinkContainer.$element.hide();
newNomHeaderContainer.$element.show();
rationaleContainer.$element.show();
 
}
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 displayElements(nominationToggleNew.isSelected()) {;
discussionLinkContainernominationToggle.$element.hideon('select', displayElements);
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' || XFD === 'TFD') ? 'Action batch #' + count : '';
var container = createFieldset(actionNominationTitle);
actionsContainer.append(container.$element);
 
var actionDropdownObj = createActionDropdown();
function createActionNomination(actionsContainer, first = false) {
var countdropdown = actionsactionDropdownObj.length + 1dropdown;
let actionNominationTitle = XFD === 'CFD' ? 'Action batch #' + count : ''
var container = createFieldset(actionNominationTitle);
actionsContainer.append(container.$element);
 
var actionDropdownObj = createActionDropdown elementsToDisable.push(dropdown);
var dropdown = actionDropdownObj. 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(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() || XFD === 'TFD') actionContainer.$element.hide(); // make invisible until needed
prependTextContainer.$element.append(prependTextLabel.$element, prependTextInfoPopup.$element, dropdown.$element, actionContainer.$element, prependTextInputField.$element);
 
elementsToDisable nominationToggle.pushon('select', function (prependTextInputField); {
var prependTextContainer = new OO if (nominationToggleOld.ui.PanelLayoutisSelected()) {
expanded: false $('.newnomonly').hide();
if (discussionLinkInputField.getValue().trim()) discussionLinkInputField.emit('change');
});
var actionObj = createTitleAndInputFieldWithLabel('Action', 'renaming', classes = ['newnomonly']); }
var actionContainer = actionObj else if (nominationToggleNew.container;isSelected()) {
var actionInputField if (XFD === actionObj'CFD' || XFD === 'TFD') $('.newnomonly').inputFieldshow();
if (newNomHeaderInputField.getValue().trim()) newNomHeaderInputField.emit('change');
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()) {
$if ('discussionLinkInputField.newnomonly'getValue().hidematch(config.discussionLinkRegex)); {
if ( sectionName = discussionLinkInputField.getValue().trim()) discussionLinkInputField.emitmatch('change'config.discussionLinkRegex)[1];
}
}
else if (nominationToggleNew.isSelected()) {
if (XFDsectionName === 'CFD') $newNomHeaderInputField.getValue('.newnomonly').showtrim();
if (newNomHeaderInputField.getValue().trim()) newNomHeaderInputField.emit('change');
}
});
 
// helper function, makes more accurate.
if (nominationToggleOld.isSelected()) {
iffunction replaceOccurence(discussionLinkInputField.getValue().match(config.discussionLinkRegex)str, find, replace) {
sectionName = discussionLinkInputField.getValue().trim();
}
}
else if (nominationToggleNew.isSelected()) {
sectionName = newNomHeaderInputField.getValue().trim();
}
 
// helper function, makes more accurate. if (XFD === 'CFD' || XFD === 'TFD') {
function replaceOccurence(str, find, replace) { // last occurence
if (XFD == let index = 'CFD'str.lastIndexOf(find) {;
// last occurence
let index = str.lastIndexOf(find);
 
if (index >= 0) {
return str.substring(0, index) + replace + str.substring(index + find.length);
} else {
return str;
}
} else if (XFD === 'RFD') { // RfD has stuff after it so we can't just replace last occurence
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') {
if (XFD === 'RFD') {
prependTextInputField.setValue(config.actions.prepend.replace('${sectionName}', delinkWikitext(sectionName)));
 
}
if (discussionLinkInputField.ongetValue('change', function ).match(config.discussionLinkRegex)) {
if ( let date = discussionLinkInputField.getValue().trim().match(config/^Wikipedia:Redirects for discussion\/Log\/(\d\d\d\d \w+ \d\d)?#.discussionLinkRegex+$/)) {[1];
oldSectionName let difference = sectionNamegetDateDifference(date);
sectionName = discussionLinkInputField.getValue if ().replace(config.discussionLinkRegex,difference '$1'!== 0).trim(); {
var text = prependTextInputField.getValuesetValue(config.actions.prepend.replace('{{subst:rfd|${sectionName}|', `{{subst:rfd|${delinkWikitext(sectionName)}|days=${difference}|`));
text = replaceOccurence(text, oldSectionName, sectionName);} // else leave as default above
prependTextInputField.setValue(text);}
}
});
 
newNomHeaderInputField discussionLinkInputField.on('change', function () {
if (newNomHeaderInputFielddiscussionLinkInputField.getValue().trimmatch(config.discussionLinkRegex)) {
oldSectionName = sectionName;
sectionName = newNomHeaderInputFielddiscussionLinkInputField.getValue().replace(config.discussionLinkRegex, '$1').trim();
var text = prependTextInputField.getValue();
 
text = replaceOccurence(text, oldSectionName, sectionName);
prependTextInputField.setValue if (textXFD === 'RFD'); {
const date = discussionLinkInputField.getValue().trim().match(/^Wikipedia:Redirects for discussion\/Log\/(\d\d\d\d \w+ \d\d?)#.+$/)[1];
 
if (/(\| *days *= *)\d+/.test(text)) { // already has days=, update
text = text.replace(/(\| *days *= *)\d+/, '$1' + getDateDifference(date));
text = replaceOccurence(text, delinkWikitext(oldSectionName), delinkWikitext(sectionName));
} else {
text = replaceOccurence(text, delinkWikitext(oldSectionName), delinkWikitext(sectionName) + '|days=' + getDateDifference(date));
}
} else text = replaceOccurence(text, delinkWikitext(oldSectionName), delinkWikitext(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, delinkWikitext(oldSectionName), delinkWikitext(sectionName));
prependTextInputField.setValue(text);
}
});
 
dropdown.on('labelChange', function () {
let actionData = config.actions[dropdown.getLabel()];
prependTextInputField.setValue(actionData.prepend.replace('${sectionName}', delinkWikitext(sectionName)));
actionInputField.setValue(actionData.action);
});
 
 
 
 
var titleListObj = createTitleAndInputField(`List of titles (one per line${XFD === 'CFD' ? ', <code>Category:</code> prefix is optional' : ''})`, config.titleDemoText, 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);
}
});
 
dropdown.on('labelChange', function () {
let actionData = config.actions[dropdown.getLabel()];
prependTextInputField.setValue(actionData.prepend.replace('${sectionName}', sectionName));
actionInputField.setValue(actionData.action);
});
 
titleListObj.inputField.$element.on('paste', handlepaste);
 
if (XFD === 'RFD') {
// some 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 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 titleListremoveButton = titleListObj.containercreateRemoveBatchButton();
var titleListInputField = titleListObj elementsToDisable.inputFieldpush(removeButton);
var titleListInfoPopup = titleListObj removeButton.infoPopup;on('click', function () {
elementsToDisable container.push$element.remove(titleListInputField);
// filter based on the container element
let handler = handlepaste.bind(this, titleListInputField);
let textInputElement actions = titleListInputFieldactions.$element.getfilter(0function (item); {
// Modern browsers. Note: 3rd argument is required for Firefox < return item.container !== 6container;
if (textInputElement.addEventListener) { });
textInputElement.addEventListener('paste', handler, false); // Reset labels
} for (i = 0; i < actions.length; i++) {
actions[i].container.setLabel('Action batch #' + (i + 1));
// IE <= 8
else { actions[i].label = 'Action batch #' + (i + 1);
textInputElement.attachEvent('onpaste', handler); }
});
 
container.addItems([removeButton, prependTextContainer, titleList]);
 
titleListObj.inputField.$element.on('paste', handlepaste); } else {
container.addItems([prependTextContainer, titleList]);
}
 
if (XFD !== 'CFD') return {
// most XfDs don't need multiple actionstitleListInputField, they're just delete. so hide unnecessary elements'
actionContainer.$element.hide(); prependTextInputField,
dropdown.$element.hide(); label: 'Action batch #' + count,
container,
prependTextInfoPopup.$element.hide() // both popups give info about targets which aren't relevant here
titleListInfoPopup.$element.hide() 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);
 
ifvar (!firstmultiOptionButton && XFD !== 'CFD'createMultiOptionButton() {;
var removeButton = createRemoveBatchButtonelementsToDisable.push(multiOptionButton);
elementsToDisablemultiOptionButton.push$element.css(removeButton'margin-bottom', '10px');
removeButtonbodyContent.on('click', function append(multiOptionButton.$element) {;
containerbodyContent.$element.removeappend('<br />');
// 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]);
 
}multiOptionButton.on('click', else() => {
containeractions.addItemspush([prependTextContainer, titleList]createActionNomination(actionsContainer));
});
if (XFD !== 'CFD' && XFD !== 'TFD') {
multiOptionButton.$element.hide();
}
if (XFD === 'CFD') {
 
var categoryTemplateDropdownObj = makeCategoryTemplateDropdown('Category template: (used on the discussion page)');
return {
titleListInputField,categoryTemplateDropdownContainer = categoryTemplateDropdownObj.container;
prependTextInputField,categoryTemplateDropdown = categoryTemplateDropdownObj.dropdown;
label: 'Action batch #' + count,categoryTemplateDropdown.$element.css(
container, {
actionInputField 'display': 'inline-block',
}; 'max-width': 'fit-content',
} 'margin-bottom': '10px'
var actionsContainer = $('<div />'); }
bodyContent.append(actionsContainer );
elementsToDisable.push(categoryTemplateDropdown);
var actions = [];
if (nominationToggleOld.isSelected()) categoryTemplateDropdownContainer.$element.hide();
actions.push(createActionNomination(actionsContainer, first = true));
bodyContent.append(categoryTemplateDropdownContainer.$element);
}
 
var checkboxObjstartButton = createCheckboxWithLabelcreateStartButton('Notify users?');
var notifyCheckbox = checkboxObj elementsToDisable.checkboxpush(startButton);
elementsToDisable bodyContent.pushappend(notifyCheckboxstartButton.$element);
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 startButton.on('click', async function () => {
 
actions.push(createActionNomination(actionsContainer));
} var isOld = nominationToggleOld.isSelected();
if (XFD != var isNew = 'CFD'nominationToggleNew.isSelected() {;
multiOptionButton.$element.hide() // First check elements
} else { 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);
}
 
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);
}
 
batches = actions.map(function ({ titleListInputField, prependTextInputField, label, actionInputField }) {
var startButton = createStartButton();
if (!(prependTextInputField.getValue().trim()) || (XFD === 'RFD' && !prependTextInputField.getValue().includes('${pageText}'))) {
elementsToDisable.push(startButton);
prependTextInputField.setValidityFlag(false);
bodyContent.append(startButton.$element);
error = true;
} else {
prependTextInputField.setValidityFlag(true);
 
}
 
if (isNew && (XFD === 'CFD' || XFD === 'TFD')) {
if (!(actionInputField.getValue().trim())) {
actionInputField.setValidityFlag(false);
error = true;
} else {
actionInputField.setValidityFlag(true);
}
}
 
if (!(titleListInputField.getValue().trim())) {
startButton.on('click', async function () {
titleListInputField.setValidityFlag(false);
error = true;
} else {
titleListInputField.setValidityFlag(true);
}
 
var isOld = nominationToggleOld.isSelected(); // Retreive titles, handle dups
var isNewtitles = nominationToggleNew.isSelected(){};
var titleList = titleListInputField.getValue().split('\n');
// First check elements
var error = false; function normalise(title) {
var regex = return config.discussionLinkRegexnormaliseFunction(title);
if (isOld) { }
let hasModules = false;
if (!(discussionLinkInputField.getValue().trim()) || !regex.test(discussionLinkInputField.getValue().trim())) {
discussionLinkInputField.setValidityFlag(let hasCSSPages = false);
errortitleList.forEach(function =(title) true;{
} else try {
discussionLinkInputField.setValidityFlag if (truetitle); {
} var targets = title.split('|');
} else if var newTitle = targets.shift(isNew) {;
if (!(newNomHeaderInputField.getValue().trim())) {
newNomHeaderInputField.setValidityFlag(false);
error = true;
} else {
newNomHeaderInputField.setValidityFlag(true);
}
 
if newTitle = normalise(!(rationaleInputField.getValue(newTitle).trim())) {;
rationaleInputField.setValidityFlag(false); // make sure all titles are in template or module namespaces
error if (XFD === 'TFD') true;{
} else if (!newTitle.includes('Template:') && !newTitle.includes('Module:')) {
rationaleInputField titleListInputField.setValidityFlag(truefalse);
} error = true;
}
}
if (!Object.keys(titles).includes(newTitle)) {
// for TfD skip modules, we deal with them elsewhere
if (XFD === "TFD") {
if (newTitle.includes('Module:')) hasModules = true;
else if (newTitle.endsWith('.css')) hasCSSPages = true;
else titles[newTitle] = targets.map(normalise).map(t => t.replace(/^Template:/, ""));
// tfd templates don't use the prefix, and the replacement is safe after normalistation
} else {
titles[newTitle] = targets.map(normalise);
}
 
}
batches = actions.map(function ({ titleListInputField, prependTextInputField, label, actionInputField }) {
if (!(prependTextInputField.getValue().trim()) || (XFD === 'RFD' && !prependTextInputField.getValue().includes('${pageText}'))) {
prependTextInputField.setValidityFlag(false);
error = true;
} else {
prependTextInputField.setValidityFlag(true);
 
}
}
} catch (e) {
console.error(`[MassXFD] Error parsing title "${title}": ${e}`);
titleListInputField.setValidityFlag(false);
error = true;
}
});
 
if (isNew!(Object.keys(titles).length) && XFD!hasModules ===&& 'CFD'!hasCSSPages) {
if (!(actionInputField titleListInputField.getValuesetValidityFlag(false).trim())) {;
actionInputField.setValidityFlag(false);
error = true;
}
if (Object.keys(titles).length) {
return {
titles,
prependText: prependTextInputField.getValue().trim(),
label,
actionInputField
};
} else {
actionInputField.setValidityFlag(true)return null;
}
 
}).filter(function (item) {
return item !== null;
});
 
// for TFD, we need to remove the Module: titles, and treat them as seperate batches, one for merging, one for deleting
if (XFD === 'TFD') {
let mergingModules = {};
let deletingModules = {};
let cssPages = {};
actions.forEach(function ({ titleListInputField, prependTextInputField }) {
titleListInputField.getValue().split('\n').forEach(title => {
let targets = title.split('|');
let newTitle = targets.shift();
if (newTitle.trim().endsWith('.css')) {
newTitle = config.normaliseFunction(newTitle);
cssPages[newTitle] = targets.map(config.normaliseFunction);
}
else if ((new mw.Title(newTitle)).getNamespaceId() === NS_MODULE) {
 
newTitle = config.normaliseFunction(newTitle);
if (prependTextInputField.getValue().trim().match(/\{\{ *subst: *Tfm/i)) {
mergingModules[newTitle] = targets.map(config.normaliseFunction);
} else {
deletingModules[newTitle] = targets.map(config.normaliseFunction);
// this will be the default, even if we can't detect the action
}
}
});
});
let sectionName = '';
let discussionPageLink = '';
if (nominationToggleOld.isSelected()) {
if (discussionLinkInputField.getValue().match(config.discussionLinkRegex)) {
sectionName = discussionLinkInputField.getValue().trim().match(config.discussionLinkRegex)[1];
discussionPageLink = (new mw.Title(discussionLinkInputField.getValue().trim())).getUrl();
}
}
else if (nominationToggleNew.isSelected()) {
sectionName = newNomHeaderInputField.getValue().trim();
discussionPageLink = (new mw.Title(sectionName)).getUrl();
 
const date = new Date();
 
const year = date.getUTCFullYear();
const month = date.toLocaleString('en', { month: 'long', timeZone: 'UTC' });
const day = date.getUTCDate();
 
var discussionPage = `${config.baseDiscussionPage}${year} ${month} ${day}`;
discussionPageLink = `${discussionPage}#${newNomHeaderInputField.getValue().trim()}`;
}
if (Object.keys(mergingModules).length) {
batches.unshift({
titles: Object.fromEntries(
Object.entries(mergingModules).map(
([title, targets]) => [`${title}/doc`, targets.map(t => t.replace(/^Module:/, ""))]
)
),
prependText: `{{subst:Tfm|$1|type=module|page={{subst:BASEPAGENAME}}|heading=${sectionName}}}`,
label: "Merging modules",
actionInputField: null
});
}
if (Object.keys(deletingModules).length) {
batches.unshift({
titles: Object.fromEntries(
Object.entries(deletingModules).map(
([title, targets]) => [`${title}/doc`, targets.map(t => t.replace(/^Module:/, ""))]
)
),
prependText: `{{subst:Tfd|type=module|page={{subst:BASEPAGENAME}}|heading=${sectionName}}}`,
label: "Deleting modules",
actionInputField: null
});
}
if (Object.keys(cssPages).length) {
batches.unshift({
titles: cssPages, // we don't need to substitute out the prefix because targets will never be used, as the prependText is hardcoded
prependText: `/* This template is being discussed in accordance with Wikipedia's deletion policy. Help reach a consensus at its entry: ${discussionPageLink} */\n`,
label: "CSS pages",
actionInputField: null
});
}
 
}
 
 
if (!(titleListInputField.getValue().trim())) {
 
titleListInputField.setValidityFlag(false);
if (error) = true;{
} else { return;
titleListInputField.setValidityFlag(true);
}
 
//for Retreive(let titles,element handleof dupselementsToDisable) {
var titles = {} element.setDisabled(true);
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(titlesremove-batch-button').includesremove(newTitle)) {;
 
titles[newTitle] = targets.map(normalise);
var abortButton = }createAbortButton();
bodyContent.append(abortButton.$element);
window.abortEdits = false; // initialise
abortButton.on('click', function () {
 
// Set abortEdits flag to true
if (confirm('Are you sure you want to abort?')) {
abortButton.setDisabled(true);
window.abortEdits = true;
}
});
var allTitles = batches.reduce((allTitles, obj) => {
return allTitles.concat(Object.keys(obj.titles));
}, []);
 
if (!(Object.keys(titles).length)) {
titleListInputField.setValidityFlag(false);
error = true;
} else {
titleListInputField.setValidityFlag(true);
}
return {
titles,
prependText: prependTextInputField.getValue().trim(),
label,
actionInputField
};
});
 
if (XFD === 'RFD') {
let fetchingRedirectsElement = createDoingElement();
fetchingRedirectsElement.setLabel('Fetching redirect targets...');
fetchingRedirectsElement.$element.css('margin-top', '16px');
bodyContent.append(fetchingRedirectsElement.$element);
 
let fetchedRedirectsElement = createCompletedElement();
fetchedRedirectsElement.setLabel('Fetched redirect targets');
fetchedRedirectsElement.$element.css('margin-top', '16px');
 
var [redirectTargets, nonredirects] = await createRedirectTargetsList(allTitles);
if (error) {
if (window.debuggingMode) console.log(`Redirect targets: ${JSON.stringify(redirectTargets)}`);
return;
// console.log(Object.values(redirectTargets).map(title => {
}
// let page = new mw.Title(title)
// return page.getTalkPage().getPrefixedText()
// }))
// console.log([... new Set(Object.values(redirectTargets).map(title => {
// let page = new mw.Title(title)
// return page.getTalkPage().getPrefixedText()
// }))])
// window.batches=batches
batches[0].titles = Object.keys(batches[0].titles)
.filter(x => !nonredirects.includes(x))
.reduce((acc, curr) => {
acc[curr] = [];
return acc;
}, {});
 
for (let element of elementsToDisable) {
element.setDisabled(true);
}
 
 
$ if ('!Object.remove-batch-button'keys(redirectTargets).remove(length); {
var errorMessageElement = createErrorMessage('None of the titles are redirects, aborting.');
bodyContent.append(errorMessageElement.$element);
return;
}
if (nonredirects.length) {
let nonredirectsWarningMessage = createWarningMessage();
nonredirectsWarningMessage.$element.css({ 'max-height': '20em', 'overflow-y': 'auto' }); // normally shouldn't be needed
let nonRedirectsHTML = $('<div>').append($('<span>').text('The following pages were ignored because they are not redirects:'));
let $listElement = $('<ul>');
nonredirects.forEach(item => {
const $listItem = $('<li>').html(makeLink(item));
$listElement.append($listItem);
});
nonRedirectsHTML.append($listElement);
nonredirectsWarningMessage.setLabel(nonRedirectsHTML);
bodyContent.append(nonredirectsWarningMessage.$element);
}
 
var abortButton = createAbortButton fetchingRedirectsElement.$element.hide();
bodyContent.append(abortButtonfetchedRedirectsElement.$element);
window.abortEdits = false; // initialise}
abortButton.on('click', function () {
 
 
// Set abortEdits flag to true
iflet (confirm('ArefetchingAuthorsElement you= sure you want to abort?'createDoingElement()) {;
abortButtonfetchingAuthorsElement.setDisabledsetLabel(true'Fetching authors...');
windowfetchingAuthorsElement.abortEdits =$element.css('margin-top', true'16px');
bodyContent.append(fetchingAuthorsElement.$element);
 
let fetchedAuthorsElement = createCompletedElement();
fetchedAuthorsElement.setLabel('Fetched authors')
fetchedAuthorsElement.$element.css('margin-top', '16px');
let authors;
if (redirectTargets) {
authors = await createAuthorList(Object.keys(redirectTargets));
} else {
authors = await createAuthorList(allTitles);
}
});
var allTitles = batches.reduce((allTitles, obj) => {
return allTitles.concat(Object.keys(obj.titles));
}, []);
 
let authors = await createAuthorListfetchingAuthorsElement.$element.hide(allTitles);
bodyContent.append(fetchedAuthorsElement.$element);
if (XFD === 'RFD') var redirectTargets = await createRedirectTargetsList(allTitles);
console.log(redirectTargets);
function processContent(options) {
// let {content, titles, textToModify, summary, type, doneMessage, headingLabel} = options
console.log(options)
console.log(options.titles)
if (!Array.isArray(options.titles)) {
options.titlesDict = options.titles;
options.titles = Object.keys(options.titles);
} else options.titlesDict = {}
var fieldset = createFieldset(options.headingLabel);
 
bodyContent.append(fieldset.$element);
 
options.progressElementasync =function createProgressElementprocessContent(options); {
fieldset.addItems function getKeyByValue([options.progressElement]object, value); {
return Object.keys(object).find(key => object[key] === value);
}
 
options.ratelimitMessage = createRatelimitMessage();
options.ratelimitMessage.toggle(false);
fieldset.addItems([options.ratelimitMessage]);
 
var progressObj = createProgressBar if (`!Array.isArray(0 / ${options.titles.length}, 0 errors)`); // with label{
options.titlesDict = options.titles; // dictionary is confusingly used for different things for prepend batches and for redirect notify batches
options.progress = progressObj.progressBar;
var progressContainer options.titles = progressObjObject.keys(options.fieldlayouttitles);
// Add margin or padding} toelse the progress bar widget{
options.progress.$element.css('margin-top',titlesDict = '5px'){};
options.progress.pushPending(); }
fieldset.addItems([progressContainer]);
 
let resolvedCount const fieldset = 0createFieldset(options.headingLabel);
let rejectedCount = 0 bodyContent.append(fieldset.$element);
 
function updateCounter options.progressElement = createProgressElement() {;
progressContainerfieldset.setLabeladdItems(`(${resolvedCount} / ${[options.titles.length}, ${rejectedCount} errors)`progressElement]);
}
function updateProgress() {
var percentage = (resolvedCount + rejectedCount) / options.titles.length * 100;
options.progress.setProgress(percentage);
 
} options.ratelimitMessage = createWarningMessage();
options.ratelimitMessage.toggle(false);
fieldset.addItems([options.ratelimitMessage]);
 
function trackPromise const progressObj = createProgressBar(promise)`(0 / ${options.titles.length}, 0 errors)`);
return new Promise((resolve, reject)options.progress => {progressObj.progressBar;
const progressContainer = promiseprogressObj.fieldlayout;
options.thenprogress.$element.css(value =>'margin-top', {'5px');
resolvedCount++options.progress.pushPending();
updateCounterfieldset.addItems([progressContainer]);
 
updateProgress();
let resolvedCount = resolve(value)0;
let rejectedCount = })0;
 
.catch(error => {
function updateCounter() rejectedCount++;{
progressContainer.setLabel(`(${resolvedCount} / ${options.titles.length}, ${rejectedCount} updateCounter(errors)`);
}

function updateProgress(); {
const percentage = (resolvedCount + rejectedCount) / options.titles.length resolve(error)* 100;
}options.progress.setProgress(percentage);
.error(error => {}
 
rejectedCount++;
function updateCountertrackPromise(promise); {
return new Promise((resolve) => updateProgress();{
resolve(error);promise
}); .then(value => {
}) resolvedCount++;
} updateCounter();
updateProgress();
resolve(value);
})
.catch(error => {
rejectedCount++;
updateCounter();
updateProgress();
resolve(error);
});
});
}
 
return new Promise(async function (resolve)const {promises = [];
var promises = [];
for (const title of options.titles) {
//let RfDdata needs= special handling here, because it wraps around the whole page contentdeepCopy(options);
if (XFD === 'RFD' && optionsdata.type === 'prepend') { // prepend implicitly means page tagging, not actually prepend in this case
const text = await getWikitext(title);
consoledata.logtextToModify = data.textToModify.replace('${pageText}', text);
consoledata.log(options.textToModify)type = 'text';
options.textToModify = options.textToModify.replace('${pageText}', text);
options.type = 'text';
}
 
options.title = title
varif promise(data.id === editPage(options'rfd-notify-target'); {
// ${redirectTitle} is a placeholder for the redirect being nominated
// this code needs a more intelligent way of checking which redirect was tagged.
data.textToModify = data.textToModify.replace('${redirectTitle}', data.titlesDict[title]);
}
 
data.title = title;
 
const promise = editPage(data);
promises.push(trackPromise(promise));
 
if (!window.abortEdits) await sleep(100); // space out calls - not needed if they're being rejected
await massXFDratelimitPromise; // stop if ratelimit reached (global variable)
}
 
await Promise.allSettled(promises);
.then(function () {
options.progress.toggle(false);
if (window.abortEdits) {
var abortMessage = createAbortMessage();
let revertEditsLink = $('<a id="massxfdrevertlink">Revert?</a>')
revertEditsLink.on('click', revertEdits)
abortMessage.setLabel($('<span>').append('Edits manually aborted. ').append(revertEditsLink));
 
bodyContentoptions.append(abortMessageprogress.$elementtoggle(false);
} else {
var completedElement = createCompletedElement();
completedElement.setLabel(options.doneMessage);
completedElement.$element.css('margin-bottom', '16px');
bodyContent.append(completedElement.$element);
}
resolve();
})
.catch(function (error) {
console.error("Error occurred during title processing:", error);
resolve();
});
});
}
 
const date = new Date if (window.abortEdits); {
const abortMessage = createAbortMessage();
const revertEditsLink = $('<a id="massxfdrevertlink">Revert?</a>');
revertEditsLink.on('click', revertEdits);
abortMessage.setLabel($('<span>').append('Edits manually aborted. ').append(revertEditsLink));
bodyContent.append(abortMessage.$element);
} else {
const completedElement = createCompletedElement();
completedElement.setLabel(options.doneMessage);
completedElement.$element.css('margin-bottom', '16px');
bodyContent.append(completedElement.$element);
}
}
 
const 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 const date = discussionLinkInputField.getValue().trimnew Date();
else if (isNew) summaryDiscussionLink = `${discussionPage}#${newNomHeaderInputField.getValue().trim()}`;
 
const advSummaryyear = ' ([[User:Qwerfjkl/scripts/massXFDdate.js|via MassXfD.js]]getUTCFullYear()';
const month = date.toLocaleString('en', { month: 'long', timeZone: 'UTC' });
// WIP, not finished
const categorySummary = 'Tagging pageconst forday [[' + summaryDiscussionLink + ']]' += advSummarydate.getUTCDate();
const userSummary = 'Notifying user about [[' + summaryDiscussionLink + ']]' + advSummary;
const userNotification = `{{ subst: ${config.userNotificationTemplate} | ${summaryDiscussionLink} }} ~~~~`;
const nominationSummary = `Adding mass nomination at [[#${newNomHeaderInputField.getValue().trim()}]]` + advSummary;
if (XFD === 'RFD') {
var redirectTargetNotification = `{{subst:Rfd notice|\${redirectTitle}|${summaryDiscussionLink}}}`
var redirectTargetNotificationSummary = `Notice of [[${summaryDiscussionLink}]]${advSummary}`
}
var batchesToProcess = [];
 
var newNomPromise = new Promise(function (resolve)var {summaryDiscussionLink;
ifvar (isNew)discussionPage = `${config.baseDiscussionPage}${year} ${month} ${day}`;
 
nominationText = `==== ${newNomHeaderInputField.getValue().trim()} ====\n`;
if (isOld) summaryDiscussionLink = for discussionLinkInputField.getValue(const batch of batches) {.trim();
else if (isNew) var actionsummaryDiscussionLink = batch.actionInputField`${discussionPage}#${newNomHeaderInputField.getValue().trim()}`;
summaryDiscussionLink = delinkWikitext(summaryDiscussionLink); // links can't be nested
for (const page of Object.keys(batch.titles)) {
 
if (XFD == 'CFD') {
const advSummary = ' ([[User:Qwerfjkl/scripts/massXFD.js|via MassXfD.js]])';
var targets = batch.titles[page].slice(); // copy array
// WIP, not var targetText = '';finished
const taggingSummary = 'Tagging page for [[' + summaryDiscussionLink + ']]' + if (targets.length) {advSummary;
const userSummary = 'Notifying user about [[' + summaryDiscussionLink + ']]' + if (targets.length === 2) {advSummary;
const targetTextuserNotification = `{{ tosubst: [[:${targets[0]config.userNotificationTemplate}]] and| [[:${targets[1]summaryDiscussionLink}]] }} ~~~~`;
const nominationSummary = `Adding mass nomination at [[#${newNomHeaderInputField.getValue().trim()}]]` + }advSummary;
else if (targets.lengthXFD >=== 2'RFD') {
var lastTargetredirectTargetNotification = targets`{{subst:Rfd notice|\${redirectTitle}|${newNomHeaderInputField.popgetValue().trim()}}} ~~~~`;
var targetTextredirectTargetNotificationSummary = '`Notice toof [[:' + targets.join(']], [[:') + ']], and [[:' + lastTarget + '${summaryDiscussionLink}]]'${advSummary}`;
} else { // 1 target
var targetTextbatchesToProcess = ' to [[:' + targets[0] + ']]';
 
var newNomPromise = new Promise(function (resolve) {
if (isNew) {
nominationText = `==== ${newNomHeaderInputField.getValue().trim()} ====\n`;
for (const batch of batches) {
var action = batch.actionInputField?.getValue()?.trim() || false;
for (const page of Object.keys(batch.titles)) {
if (XFD == 'CFD') {
var targets = batch.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()}|${categoryTemplateDropdown.getValue() === 'cl' ? page.replace(/^ *Category:/i, '') : page}}}${targetText}\n`;
} else if (XFD === 'RFD') {
nominationText += config.displayTemplate.replaceAll('${pageName}', page).replaceAll('${redirectTarget}', redirectTargets[page]) + '\n';
} else if (XFD === 'TFD') {
let moduleText = '';
if ((new mw.Title(page)).getNamespacePrefix() === "Module:") moduleText = `|module=Module:`; // looking at the actual implementation, module can be anything that's not empty
nominationText += `* ${config.displayTemplate.replaceAll('${pageName}', (new mw.Title(page)).getMainText()).replaceAll('${moduleText}', moduleText)}\n`;
} else {
throw new Error("Unimplemented.")
}
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 rationale = rationaleInputField.getValue().trim().replace(/\n/, '<br />');
nominationText += `${ if (XFD === 'CFDTFD') ?nominationText += "\n":'''Nominator's; rationale:'''// "formatting :is ''}${rationale}inconsistent ~~~~`;accross XfDs
var newText;
 
getWikitext(discussionPage).then(function (wikitext) {
if (!wikitext.match(config.nominationReplacement[0])) {
var nominationErrorMessage = createNominationErrorMessage console.log(wikitext);
bodyContent console.appendlog(nominationErrorMessageconfig.$elementnominationReplacement);
} else { var nominationErrorMessage = createNominationErrorMessage();
newText = wikitext bodyContent.replaceappend(nominationErrorMessage...config.nominationReplacement).replace('${nominationText}', nominationTextelement);
batchesToProcess.push(} else {
titles:newText [discussionPage]= wikitext.replace(...config.nominationReplacement).replace('${nominationText}', nominationText);
textToModify: newText,batchesToProcess.push({
summary titles: nominationSummary[discussionPage],
type textToModify: 'text'newText,
doneMessage: 'Nomination added' summary: nominationSummary,
headingLabel type: 'Creating nominationtext',
}); doneMessage: 'Nomination added',
headingLabel: 'Creating nomination'
});
resolve();
}
}).catch(function (error) {
console.error('An error occurred in fetching wikitext:', error);
resolve();
});
}).catch(function else resolve(error) {;
console.error('An error occurred in fetching wikitext:', error);
resolve();
});
} else resolve();
});
newNomPromise.then(async function () {
batches.forEach(batch => {
batchesToProcess.push({
titles: batch.titles,
textToModify: batch.prependText,
summary: categorySummary,
type: 'prepend',
doneMessage: 'All pages edited.',
headingLabel: 'Editing nominated pages' + ((batches.length > 1) ? ' — ' + batch.label : '')
});
});
if newNomPromise.then(XFDasync ===function 'RFD'() {
batchesToProcessbatches.pushforEach(batch => {
titles: ObjectbatchesToProcess.valuespush(redirectTargets).map(title => {
lettitles: page = new mwbatch.Title(title)titles,
returntextToModify: pagebatch.getTalkPage().getPrefixedText()prependText,
}) summary: taggingSummary,
textToModify type: redirectTargetNotification'prepend',
summary doneMessage: redirectTargetNotificationSummary'All pages edited.',
type headingLabel: 'Editing nominated pages' + ((batches.length > 1) ? ' — ' + batch.label : 'append',)
doneMessage: 'All target talk pages notified.',});
headingLabel: 'Notifying targets'
});
} if (XFD === 'RFD') {
if (notifyCheckbox batchesToProcess.isSelectedpush()) {
batchesToProcess.push({ id: 'rfd-notify-target',
titles: authors,Object.fromEntries(Object.keys(redirectTargets).map(title => {
textToModify: userNotification, let page = new mw.Title(redirectTargets[title]);
summary: userSummary return [page.getTalkPage().getPrefixedText(), title];
})), // return a map of page we want to edit : orginal title - this will (as a dicitionary) remove duplicates, but in an unpredictable way - it seems to retain the last one
type: 'append',
doneMessage: 'All users notified.' textToModify: redirectTargetNotification,
headingLabel: 'Notifying users' summary: redirectTargetNotificationSummary,
}); type: 'append',
} doneMessage: 'All target talk pages notified.',
let promise = Promise.resolve(); headingLabel: 'Notifying targets'
// abort handling is now only in the editPage(}) function;
for (const batch of batchesToProcess) {}
awaitif processContent(batchnotifyCheckbox.isSelected();) {
} batchesToProcess.push({
titles: authors,
textToModify: userNotification,
summary: userSummary,
type: 'append',
doneMessage: 'All users notified.',
headingLabel: 'Notifying users'
});
}
let promise = Promise.resolve();
// abort handling is now only in the editPage() function
for (const batch of batchesToProcess) {
// alert(`starting batch ${batch.headingLabel}`)
await processContent(batch);
// alert(`batch ${batch.headingLabel} done`)
}
 
promise.then(() => {
abortButton.setLabel('Revert');
// All done
}).catch(err => {
console.error('Error occurred:', err);
});
});
});
 
});
});
});
}
}
 
// Run the script when the page is ready
$(document).ready(runMassXFD);
 
}());
// </nowiki>