User:Qwerfjkl/scripts/massXFD.js
< User:Qwerfjkl | scripts
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
Documentation for this user script can be added at User:Qwerfjkl/scripts/massXFD. |
// <nowiki>
// handle redirects
// 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?#(.+)$/,
"nominationReplacement": [/==== ?NEW NOMINATIONS ?====\s*(?:<!-- ?Please add the newest nominations below this line ?-->)?/, '$&\n\n${nominationText}'],
"userNotificationTemplate": 'Cfd mass notice',
"baseDiscussionPage": 'Wikipedia:Categories for discussion/Log/',
"normaliseFunction": (title) => { return 'Category:' + capitalise(title.replace(/^ *[Cc]ategory:/, '').trim()); },
"actions": {
"Delete": {
'prepend': '{{subst:Cfd|${sectionName}}}',
'action': 'deleting'
},
"Rename": {
'prepend': '{{subst:Cfr|$1|${sectionName}}}',
'action': 'renaming'
},
"Merge": {
'prepend': '{{subst:Cfm|$1|${sectionName}}}',
'action': 'merging'
},
"Split": {
'prepend': '{{subst:Cfs|$1|$2|${sectionName}}}',
'action': 'splitting'
},
"Listify": {
'prepend': '{{subst:Cfl|$1|${sectionName}}}',
'action': 'listifying'
},
"Custom": {
'prepend': '{{subst:Cfd|type=|${sectionName}}}',
'action': ''
},
},
"displayTemplates": [{
data: 'lc',
label: 'Category link with extra links – {{lc}}'
},
{
data: 'clc',
label: 'Category link with count – {{clc}}'
},
{
data: 'cl',
label: 'Plain category link – {{cl}}'
}],
},
"RFD": {
"title": "Mass RfD",
"placeholderDiscussionLink": 'Wikipedia:Redirects for discussion/Log/2024 May 13#Knightfall (comics)',
"placeholderNominationTitle": 'Knightfall',
"placeholderRationale": 'No mention of "Knightfall" in the target article.',
"pageDemoText": "",
"discussionLinkRegex": /^Wikipedia:Redirects for discussion\/Log\/\d\d\d\d \w+ \d\d?#(.+)$/,
"nominationReplacement": [/<!-- ?Add new entries directly below this line\. ?-->/, '$&\n${nominationText}\n'],
"userNotificationTemplate": 'Rfd mass notice',
"baseDiscussionPage": 'Wikipedia:Redirects for discussion/Log/',
"normaliseFunction": (title) => { return 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 getRedirectData(titles) {
var api = new mw.Api();
return api.get({
action: 'query',
titles,
redirects: 1,
format: 'json'
}).then(function (data) {
return data.query; // needs redirects and normalized
}).catch(function (error) {
console.error('Error occurred while fetching page author:', error);
return false;
});
}
function getUserData(titles) {
var api = new mw.Api();
return api.get({
action: 'query',
list: 'users',
ususers: titles,
usprop: 'blockinfo|groups', // blockinfo - check if indeffed, groups - check if bot
format: 'json'
}).then(function (data) {
return data.query.users;
}).catch(function (error) {
console.error('Error occurred while fetching page author:', error);
return false;
});
}
function getPageAuthor(title) {
var api = new mw.Api();
return api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'user',
rvdir: 'newer', // Sort the revisions in ascending order (oldest first)
rvlimit: 1,
format: 'json'
}).then(function (data) {
var pages = data.query.pages;
var pageId = Object.keys(pages)[0];
var revisions = 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) {
console.log(titles)
var authorList = [];
var promises = titles.map(function (title) {
return getPageAuthor(title);
});
try {
const authors = await Promise.all(promises);
let queryBatchSize = 50;
let authorTitles = authors.map(author => author.replace(/ /g, '_')); // Replace spaces with underscores
let filteredAuthorList = [];
for (let i = 0; i < authorTitles.length; i += queryBatchSize) {
let batch = authorTitles.slice(i, i + queryBatchSize);
let batchTitles = batch.join('|');
await getUserData(batchTitles)
.then(response => {
response.forEach(user => {
console.log(user)
if (user
&& (!user.blockexpiry || user.blockexpiry !== "infinite" || 'blockpartial' in user)
&& !user.groups?.includes('bot')
&& !filteredAuthorList.includes('User talk:' + user.name))
filteredAuthorList.push('User talk:' + user.name);
});
})
.catch(error => {
console.error("Error querying API:", error);
});
}
return filteredAuthorList;
} catch (error_1) {
console.error('Error occurred while creating author list:', error_1);
return authorList;
}
}
// Function to create a list of page authors and filter duplicates
async function createRedirectTargetsList(titles) {
try {
let queryBatchSize = 50;
let redirectTitles = titles.map(title => title.replace(/ /g, '_')); // Replace spaces with underscores
let redirectTargets = {};
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(normalizedTitle => {
normalized[normalizedTitle.to] = normalizedTitle.from;
}
)
let redirects = data.redirects;
redirects.forEach(item => {
if (item.from in normalized) redirectTargets[normalized[item.from]] = item.to;
else redirectTargets[item.from] = item.to;
})
})
.catch(error => {
console.error("Error querying API:", error);
});
}
return redirectTargets;
} catch (error_1) {
console.error('Error occurred while fetching redirect targets', error_1);
return redirectTargets;
}
}
function editPage(options) {
console.log(options)
options.text = options.textToModify;
var api = new mw.Api();
var messageElement = createMessageElement();
messageElement.setLabel((options.retry) ? $('<span>').text('Retrying ').append($(makeLink(options.title))) : $('<span>').text('Editing ').append($(makeLink(options.title))));
options.progressElement.$element.append(messageElement.$element);
var container = $('.sticky-container');
container.scrollTop(container.prop("scrollHeight"));
if (options.retry) {
sleep(1000);
}
var requestData = {
action: 'edit',
//title: 'User:Qwerfjkl/sandbox/51',
title: options.title,
summary: options.summary,
format: 'json'
};
if (options.type === 'prepend') { // tagging
requestData.nocreate = 1; // don't create new page when tagging
// parse title
var targets = options.titlesDict[options.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);
options.text = options.text.replace(placeholder, targets[i]);
}
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>'));
resolve();
} else {
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 (error == 'editconflict') {
editPage(options).then(function () {
resolve();
});
} else if (error == 'ratelimited') {
options.progress.setDisabled(true);
handleRateLimitError(options.ratelimitMessage).then(function () {
options.progress.setDisabled(false);
editPage(options).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', async function () {
var isOld = nominationToggleOld.isSelected();
var isNew = nominationToggleNew.isSelected();
// First check elements
var error = false;
var regex = config.discussionLinkRegex;
if (isOld) {
if (!(discussionLinkInputField.getValue().trim()) || !regex.test(discussionLinkInputField.getValue().trim())) {
discussionLinkInputField.setValidityFlag(false);
error = true;
} else {
discussionLinkInputField.setValidityFlag(true);
}
} else if (isNew) {
if (!(newNomHeaderInputField.getValue().trim())) {
newNomHeaderInputField.setValidityFlag(false);
error = true;
} else {
newNomHeaderInputField.setValidityFlag(true);
}
if (!(rationaleInputField.getValue().trim())) {
rationaleInputField.setValidityFlag(false);
error = true;
} else {
rationaleInputField.setValidityFlag(true);
}
}
batches = actions.map(function ({ titleListInputField, prependTextInputField, label, actionInputField }) {
if (!(prependTextInputField.getValue().trim()) || (XFD === 'RFD' && !prependTextInputField.getValue().includes('${pageText}'))) {
prependTextInputField.setValidityFlag(false);
error = true;
} else {
prependTextInputField.setValidityFlag(true);
}
if (isNew && XFD === 'CFD') {
if (!(actionInputField.getValue().trim())) {
actionInputField.setValidityFlag(false);
error = true;
} else {
actionInputField.setValidityFlag(true);
}
}
if (!(titleListInputField.getValue().trim())) {
titleListInputField.setValidityFlag(false);
error = true;
} else {
titleListInputField.setValidityFlag(true);
}
// Retreive titles, handle dups
var titles = {};
var titleList = titleListInputField.getValue().split('\n');
function normalise(title) {
return config.normaliseFunction(title)
}
titleList.forEach(function (title) {
if (title) {
var targets = title.split('|');
var newTitle = targets.shift();
newTitle = normalise(newTitle);
if (!Object.keys(titles).includes(newTitle)) {
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));
}, []);
let authors = await createAuthorList(allTitles);
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.progressElement = createProgressElement();
fieldset.addItems([options.progressElement]);
options.ratelimitMessage = createRatelimitMessage();
options.ratelimitMessage.toggle(false);
fieldset.addItems([options.ratelimitMessage]);
var progressObj = createProgressBar(`(0 / ${options.titles.length}, 0 errors)`); // with label
options.progress = progressObj.progressBar;
var progressContainer = progressObj.fieldlayout;
// Add margin or padding to the progress bar widget
options.progress.$element.css('margin-top', '5px');
options.progress.pushPending();
fieldset.addItems([progressContainer]);
let resolvedCount = 0;
let rejectedCount = 0;
function updateCounter() {
progressContainer.setLabel(`(${resolvedCount} / ${options.titles.length}, ${rejectedCount} errors)`);
}
function updateProgress() {
var percentage = (resolvedCount + rejectedCount) / options.titles.length * 100;
options.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);
})
.error(error => {
rejectedCount++;
updateCounter();
updateProgress();
resolve(error);
});
});
}
return new Promise(async function (resolve) {
var promises = [];
for (const title of options.titles) {
// RfD needs special handling here, because it wraps around the whole page content
if (XFD === 'RFD' && options.type === 'prepend') { // prepend implicitly means page tagging, not actually prepend in this case
text = await getWikitext(title);
console.log(text)
console.log(options.textToModify)
options.textToModify = options.textToModify.replace('${pageText}', text);
options.type = 'text';
}
options.title = title
var promise = editPage(options);
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)
}
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));
bodyContent.append(abortMessage.$element);
} 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();
const year = date.getUTCFullYear();
const month = date.toLocaleString('en', { month: 'long', timeZone: 'UTC' });
const day = date.getUTCDate();
var summaryDiscussionLink;
var discussionPage = `${config.baseDiscussionPage}${year} ${month} ${day}`;
if (isOld) summaryDiscussionLink = discussionLinkInputField.getValue().trim();
else if (isNew) summaryDiscussionLink = `${discussionPage}#${newNomHeaderInputField.getValue().trim()}`;
const advSummary = ' ([[User:Qwerfjkl/scripts/massXFD.js|via MassXfD.js]])';
// WIP, not finished
const categorySummary = 'Tagging page for [[' + summaryDiscussionLink + ']]' + advSummary;
const userSummary = 'Notifying user about [[' + summaryDiscussionLink + ']]' + advSummary;
const userNotification = `{{ subst: ${config.userNotificationTemplate} | ${summaryDiscussionLink} }} ~~~~`;
const nominationSummary = `Adding mass nomination at [[#${newNomHeaderInputField.getValue().trim()}]]` + advSummary;
if (XFD === 'RFD') {
var redirectTargetNotification = `{{subst:Rfd notice|\${redirectTitle}|${summaryDiscussionLink}}}`
var redirectTargetNotificationSummary = `Notice of [[${summaryDiscussionLink}]]${advSummary}`
}
var batchesToProcess = [];
var newNomPromise = new Promise(function (resolve) {
if (isNew) {
nominationText = `==== ${newNomHeaderInputField.getValue().trim()} ====\n`;
for (const batch of batches) {
var action = batch.actionInputField.getValue().trim();
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;
getWikitext(discussionPage).then(function (wikitext) {
if (!wikitext.match(config.nominationReplacement[0])) {
var nominationErrorMessage = createNominationErrorMessage();
bodyContent.append(nominationErrorMessage.$element);
} else {
newText = wikitext.replace(...config.nominationReplacement).replace('${nominationText}', nominationText);
batchesToProcess.push({
titles: [discussionPage],
textToModify: newText,
summary: nominationSummary,
type: 'text',
doneMessage: 'Nomination added',
headingLabel: 'Creating nomination'
});
resolve();
}
}).catch(function (error) {
console.error('An error occurred in fetching wikitext:', error);
resolve();
});
} else resolve();
});
newNomPromise.then(async function () {
batches.forEach(batch => {
batchesToProcess.push({
titles: batch.titles,
textToModify: batch.prependText,
summary: categorySummary,
type: 'prepend',
doneMessage: 'All pages edited.',
headingLabel: 'Editing nominated pages' + ((batches.length > 1) ? ' — ' + batch.label : '')
});
});
if (XFD === 'RFD') {
batchesToProcess.push({
titles: Object.values(redirectTargets).map(title => {
let page = new mw.Title(title)
return page.getTalkPage().getPrefixedText()
}),
textToModify: redirectTargetNotification,
summary: redirectTargetNotificationSummary,
type: 'append',
doneMessage: 'All target talk pages notified.',
headingLabel: 'Notifying targets'
});
}
if (notifyCheckbox.isSelected()) {
batchesToProcess.push({
titles: authors,
textToModify: userNotification,
summary: userSummary,
type: 'append',
doneMessage: 'All users notified.',
headingLabel: 'Notifying users'
});
}
let promise = Promise.resolve();
// abort handling is now only in the editPage() function
for (const batch of batchesToProcess) {
await processContent(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>