User:Qwerfjkl/scripts/massCFD.js
Appearance
< 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. |
This user script seems to have a documentation page at User:Qwerfjkl/scripts/massCFD. |
// <nowiki>
// todo: make counter inline, remove progresss and progressElement from editPAge(), more dynamic reatelimit wait.
// counter semi inline; adjust align in createProgressBar()
// Function to wipe the text content of the page inside #bodyContent
function wipePageContent() {
var bodyContent = $('#bodyContent');
if (bodyContent) {
bodyContent.empty();
}
var header = $('#firstHeading');
if (header) {
header.text('Mass CfD');
}
$('title').text('Mass CfD - 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: [
{
data: 'lc',
label: 'Category link with extra links – {{lc}}'
},
{
data: 'clc',
label: 'Category link with count – {{clc}}'
},
{
data: 'cl',
label: 'Plain category link – {{cl}}'
}
]
} );
var fieldlayout = new OO.ui.FieldLayout(
dropdown,
{ label: label,
align: 'inline',
classes: ['newnomonly'],
}
);
return {container: fieldlayout, dropdown: dropdown};
}
function createTitleAndInputFieldWithLabel(label, placeholder, classes=[]) {
var input = new OO.ui.TextInputWidget( {
placeholder: placeholder
} );
var fieldset = new OO.ui.FieldsetLayout( {
classes: classes
} );
fieldset.addItems( [
new OO.ui.FieldLayout( input, {
label: label
} ),
] );
return {
container: fieldset,
inputField: input,
};
}
// Function to create a title and an input field
function createTitleAndInputField(title, placeholder, info = false) {
var container = new OO.ui.PanelLayout({
expanded: false
});
var titleLabel = new OO.ui.LabelWidget({
label: $(`<span>${title}</span>`)
});
var infoPopup = makeInfoPopup(info);
var inputField = new OO.ui.MultilineTextInputWidget({
placeholder: placeholder,
indicator: 'required',
rows: 10,
autosize: true
});
if (info) container.$element.append(titleLabel.$element, infoPopup.$element, inputField.$element);
else container.$element.append(titleLabel.$element, inputField.$element);
return {
titleLabel: titleLabel,
inputField: inputField,
container: container,
infoPopup: infoPopup
};
}
// Function to create a title and an input field
function createTitleAndSingleInputField(title, placeholder) {
var container = new OO.ui.PanelLayout({
expanded: false
});
var titleLabel = new OO.ui.LabelWidget({
label: title
});
var inputField = new OO.ui.TextInputWidget({
placeholder: placeholder,
indicator: 'required'
});
container.$element.append(titleLabel.$element, inputField.$element);
return {
titleLabel: titleLabel,
inputField: inputField,
container: container
};
}
function createStartButton() {
var button = new OO.ui.ButtonWidget({
label: 'Start',
flags: ['primary', 'progressive']
});
return button;
}
function createAbortButton() {
var button = new OO.ui.ButtonWidget({
label: 'Abort',
flags: ['primary', 'destructive']
});
return button;
}
function 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',
} );
var oldNomToggle = new OO.ui.ButtonOptionWidget( {
data: 'old',
label: 'Old nomination',
selected: true
} );
var toggle = new OO.ui.ButtonSelectWidget( {
items: [
newNomToggle,
oldNomToggle
]
} );
return {
toggle: toggle,
newNomToggle: newNomToggle,
oldNomToggle: 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: label,
align: 'inline',
selected: true
}
);
return {
fieldlayout: fieldlayout,
checkbox: checkbox
};
}
function createMenuOptionWidget(data, label) {
var menuOptionWidget = new OO.ui.MenuOptionWidget( {
data: data,
label: label
} );
return menuOptionWidget;
}
function createActionDropdown() {
var dropdown = new OO.ui.DropdownWidget( {
label: 'Mass action',
menu: {
items: [
createMenuOptionWidget('delete', 'Delete'),
createMenuOptionWidget('merge', 'Merge'),
createMenuOptionWidget('rename', 'Rename'),
createMenuOptionWidget('split', 'Split'),
createMenuOptionWidget('listfy', 'Listify'),
createMenuOptionWidget('custom', 'Custom'),
]
}
} );
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 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
function revertEdits() {
var revertAllCount = 0;
var revertElements = $('.masscfdundo');
if (!revertElements.length) {
$('#masscfdrevertlink').replaceWith('Reverts done.');
} else {
$('#masscfdrevertlink').replaceWith('<span><span id="revertall-text">Reverting...</span> (<span id="revertall-done">0</span> / <span id="revertall-total">'+revertElements.length+'</span> done)</span>');
revertElements.each(function (index, element) {
element = $(element); // jQuery-ify
var title = element.attr('data-title');
var revid = element.attr('data-revid');
revertEdit(title, revid)
.then(function() {
element.text('. Reverted.');
revertAllCount++;
$('#revertall-done').text( revertAllCount );
}).catch(function () {
element.html('. Revert failed. <a href="/wiki/Special:Diff/'+revid+'">Click here</a> to view the diff.');
});
}).promise().done(function () {
$('#revertall-text').text('Reverts done.');
});
}
}
function revertEdit(title, revid, retry=false) {
var api = new mw.Api();
if (retry) {
sleep(1000);
}
var requestData = {
action: 'edit',
title: title,
undo: revid,
format: 'json'
};
return new Promise(function(resolve, reject) {
api.postWithEditToken(requestData).then(function(data) {
if (data.edit && data.edit.result === 'Success') {
resolve(true);
} else {
console.error('Error occurred while undoing edit:', data);
reject();
}
}).catch(function(error) {
console.error('Error occurred while undoing edit:', error); // handle: editconflict, ratelimit (retry)
if (error == 'editconflict') {
resolve(revertEdit(title, revid, retry=true));
} else if (error == 'ratelimited') {
setTimeout(function() { // wait a minute
resolve(revertEdit(title, revid, retry=true));
}, 60000);
} else {
reject();
}
});
});
}
function getUserData(titles) {
var api = new mw.Api();
return api.get({
action: 'query',
list: 'users',
ususers: titles,
usprop: 'blockinfo|groups', // blockinfo - check if indeffed, groups - check if bot
format: 'json'
}).then(function(data) {
return data.query.users;
}).catch(function(error) {
console.error('Error occurred while fetching page author:', error);
return false;
});
}
function getPageAuthor(title) {
var api = new mw.Api();
return api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'user',
rvdir: 'newer', // Sort the revisions in ascending order (oldest first)
rvlimit: 1,
format: 'json'
}).then(function(data) {
var pages = data.query.pages;
var pageId = Object.keys(pages)[0];
var revisions = pages[pageId].revisions;
if (revisions && revisions.length > 0) {
return revisions[0].user;
} else {
return false;
}
}).catch(function(error) {
console.error('Error occurred while fetching page author:', error);
return false;
});
}
// Function to create a list of page authors and filter duplicates
function createAuthorList(titles) {
var authorList = [];
var promises = titles.map(function(title) {
return getPageAuthor(title);
});
return Promise.all(promises).then(async function(authors) {
let queryBatchSize = 50;
let authorTitles = authors.map(author => author.replace(/ /g, '_')); // Replace spaces with underscores
let filteredAuthorList = [];
for (let i = 0; i < authorTitles.length; i += queryBatchSize) {
let batch = authorTitles.slice(i, i + queryBatchSize);
let batchTitles = batch.join('|');
await getUserData(batchTitles)
.then(response => {
response.forEach(user => {
if (user
&& (!user.blockexpiry || user.blockexpiry !== "infinite")
&& !user.groups.includes('bot')
&& !filteredAuthorList.includes('User talk:'+user.name)
)
filteredAuthorList.push('User talk:'+user.name);
});
})
.catch(error => {
console.error("Error querying API:", error);
});
}
return filteredAuthorList;
}).catch(function(error) {
console.error('Error occurred while creating author list:', error);
return authorList;
});
}
// Function to prepend text to a page
function editPage(title, text, summary, progressElement, ratelimitMessage, progress, type, titlesDict, retry=false) {
var api = new mw.Api();
var messageElement = createMessageElement();
messageElement.setLabel((retry) ? $('<span>').text('Retrying ').append($(makeLink(title))) : $('<span>').text('Editing ').append($(makeLink(title))) );
progressElement.$element.append(messageElement.$element);
var container = $('.sticky-container');
container.scrollTop(container.prop("scrollHeight"));
if (retry) {
sleep(1000);
}
var requestData = {
action: 'edit',
title: title,
summary: summary,
format: 'json'
};
if (type === 'prepend') { // cat
requestData.nocreate = 1; // don't create new cat
// parse title
var targets = titlesDict[title];
for (let i = 0; i < targets.length; i++) {
// we add 1 to i in the replace function because placeholders start from $1 not $0
let placeholder = '$' + (i + 1);
text = text.replace(placeholder, targets[i]);
}
text = text.replace(/\$\d/g, ''); // remove unmatched |$x
requestData.prependtext = text.trim() + '\n\n';
} else if (type === 'append') { // user
requestData.appendtext = '\n\n' + text.trim();
} else if (type === 'text') {
requestData.text = text;
}
return new Promise(function(resolve, reject) {
if (window.abortEdits) {
// hide message and return
messageElement.toggle(false);
resolve();
return;
}
api.postWithEditToken(requestData).then(function(data) {
if (data.edit && data.edit.result === 'Success') {
messageElement.setType('success');
messageElement.setLabel( $('<span>' + makeLink(title) + ' edited successfully</span><span class="masscfdundo" data-revid="'+data.edit.newrevid+'" data-title="'+title+'"></span>') );
resolve();
} else {
messageElement.setType('error');
messageElement.setLabel( $('<span>Error occurred while editing ' + makeLink(title) + ': '+ data + '</span>') );
console.error('Error occurred while prepending text to page:', data);
reject();
}
}).catch(function(error) {
messageElement.setType('error');
messageElement.setLabel( $('<span>Error occurred while editing ' + makeLink(title) + ': '+ error + '</span>') );
console.error('Error occurred while prepending text to page:', error); // handle: editconflict, ratelimit (retry)
if (error == 'editconflict') {
editPage(title, text, summary, progressElement, ratelimitMessage, progress, type, titlesDict, retry=true).then(function() {
resolve();
});
} else if (error == 'ratelimited') {
progress.setDisabled(true);
handleRateLimitError(ratelimitMessage).then(function () {
progress.setDisabled(false);
editPage(title, text, summary, progressElement, ratelimitMessage, progress, type, titlesDict, retry=true).then(function() {
resolve();
});
});
}
else {
reject();
}
});
});
}
// global scope - needed to syncronise ratelimits
var massCFDratelimitPromise = 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 (massCFDratelimitPromise !== null) {
return massCFDratelimitPromise;
}
massCFDratelimitPromise = 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);
massCFDratelimitPromise = 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);
massCFDratelimitPromise = null; // reset
resolve();
}, secondsToWait);
});
return massCFDratelimitPromise;
}
// Function to show progress visually
function createProgressBar(label) {
var progressBar = new OO.ui.ProgressBarWidget();
progressBar.setProgress(0);
var fieldlayout = new OO.ui.FieldLayout( progressBar, {
label: label,
align: 'inline'
});
return {progressBar: progressBar,
fieldlayout: fieldlayout};
}
// Main function to execute the script
async function runMassCFD() {
mw.util.addPortletLink ( 'p-tb', mw.util.getUrl( 'Special:MassCFD' ), 'Mass CfD', 'pt-masscfd', 'Create a mass CfD nomination');
if (mw.config.get('wgPageName') === 'Special:MassCFD') {
// 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;
var nominationToggleOld = nominationToggleObj.oldNomToggle;
var nominationToggleNew = nominationToggleObj.newNomToggle;
bodyContent.append(nominationToggle.$element);
elementsToDisable.push(nominationToggle);
var discussionLinkObj = createTitleAndSingleInputField('Discussion link', 'Wikipedia:Categories for discussion/Log/2023 July 23#Category:Archaeological cultures by ethnic group');
var discussionLinkContainer = discussionLinkObj.container;
var discussionLinkInputField = discussionLinkObj.inputField;
elementsToDisable.push(discussionLinkInputField);
var newNomHeaderObj = createTitleAndSingleInputField('Nomination title', 'Archaeological cultures by ethnic group');
var newNomHeaderContainer = newNomHeaderObj.container;
var newNomHeaderInputField = newNomHeaderObj.inputField;
elementsToDisable.push(newNomHeaderInputField);
var rationaleObj = createTitleAndInputField('Rationale:', '[[WP:DEFINING|Non-defining]] category.');
var rationaleContainer = rationaleObj.container;
var rationaleInputField = rationaleObj.inputField;
elementsToDisable.push(rationaleInputField);
bodyContent.append(discussionLinkContainer.$element);
bodyContent.append(newNomHeaderContainer.$element, rationaleContainer.$element);
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();
}
nominationToggle.on('select',function() {
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();
}
});
function createActionNomination (actionsContainer, first=false) {
var count = actions.length+1;
var container = createFieldset('Action batch #'+count);
actionsContainer.append(container.$element);
var dropdown = createActionDropdown();
elementsToDisable.push(dropdown);
dropdown.$element.css('max-width', 'fit-content');
var prependTextObj = createTitleAndInputField('CfD text to add to the start of the page', '{{subst:Cfd|Category:Bishops}}', info='A dollar sign <code>$</code> followed by a number, such as <code>$1</code>, will be replaced with a target specified in the title field, or if not target is specified, will be removed.');
var prependTextLabel = prependTextObj.titleLabel;
var prependTextInfoPopup = prependTextObj.infoPopup;
var prependTextInputField = prependTextObj.inputField;
elementsToDisable.push(prependTextInputField);
var prependTextContainer = new OO.ui.PanelLayout({
expanded: false
});
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()) {
$('.newnomonly').show();
if ( newNomHeaderInputField.getValue().trim() ) newNomHeaderInputField.emit('change');
}
});
if (nominationToggleOld.isSelected()) {
if (discussionLinkInputField.getValue().match(/^Wikipedia:Categories for discussion\/Log\/\d\d\d\d \w+ \d\d?#(.+)$/)) {
sectionName = discussionLinkInputField.getValue().trim();
}
}
else if (nominationToggleNew.isSelected()) {
sectionName = newNomHeaderInputField.getValue().trim();
}
// helper function, makes ore accurate.
function replaceLastOccurrence(str, find, replace) {
let index = str.lastIndexOf(find);
if (index >= 0) {
return str.substring(0, index) + replace + str.substring(index + find.length);
} else {
return str;
}
}
var sectionName = sectionName || 'sectionName';
var oldSectionName = sectionName;
discussionLinkInputField.on('change',function() {
if (discussionLinkInputField.getValue().match(/^Wikipedia:Categories for discussion\/Log\/\d\d\d\d \w+ \d\d?#(.+)$/)) {
oldSectionName = sectionName;
sectionName = discussionLinkInputField.getValue().replace(/^Wikipedia:Categories for discussion\/Log\/\d\d\d\d \w+ \d\d?#(.+)$/, '$1').trim();
var text = prependTextInputField.getValue();
text = replaceLastOccurrence(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 = replaceLastOccurrence(text, oldSectionName, sectionName);
prependTextInputField.setValue(text);
}
});
dropdown.on('labelChange',function() {
switch (dropdown.getLabel()) {
case "Delete":
prependTextInputField.setValue(`{{subst:Cfd|${sectionName}}}`);
actionInputField.setValue('deleting');
break;
case "Rename":
prependTextInputField.setValue(`{{subst:Cfr|$1|${sectionName}}}`);
actionInputField.setValue('renaming');
break;
case "Merge":
prependTextInputField.setValue(`{{subst:Cfm|$1|${sectionName}}}`);
actionInputField.setValue('merging');
break;
case "Split":
prependTextInputField.setValue(`{{subst:Cfs|$1|$2|${sectionName}}}`);
actionInputField.setValue('splitting');
break;
case "Listify":
prependTextInputField.setValue(`{{subst:Cfl|$1|${sectionName}}}`);
actionInputField.setValue('listifying');
break;
case "Custom":
prependTextInputField.setValue(`{{subst:Cfd|type=|${sectionName}}}`);
actionInputField.setValue(''); // blank it as a precaution
break;
}
});
var titleListObj = createTitleAndInputField('List of titles (one per line, <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>Category:Example|Category:Target1|Category:Target2</code>. These targets can be used in the category tagging step.');
var titleList = titleListObj.container;
var titleListInputField = titleListObj.inputField;
elementsToDisable.push(titleListInputField);
if (!first) {
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: titleListInputField,
prependTextInputField: prependTextInputField,
label: 'Action batch #'+count,
container: container,
actionInputField: 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) );
});
var categoryTemplateDropdownObj = makeCategoryTemplateDropdown('Category template:');
categoryTemplateDropdownContainer = categoryTemplateDropdownObj.container;
categoryTemplateDropdown = categoryTemplateDropdownObj.dropdown;
categoryTemplateDropdown.$element.css(
{
'display': 'inline-block',
'max-width': 'fit-content',
'margin-bottom': '10px'
}
);
elementsToDisable.push(categoryTemplateDropdown);
if ( nominationToggleOld.isSelected() ) categoryTemplateDropdownContainer.$element.hide();
bodyContent.append(categoryTemplateDropdownContainer.$element);
var startButton = createStartButton();
elementsToDisable.push(startButton);
bodyContent.append(startButton.$element);
startButton.on('click', function() {
var isOld = nominationToggleOld.isSelected();
var isNew = nominationToggleNew.isSelected();
// First check elements
var error = false;
var regex = /^Wikipedia:Categories for discussion\/Log\/\d\d\d\d \w+ \d\d?#.+$/;
if (isOld) {
if ( !(discussionLinkInputField.getValue().trim()) || !regex.test(discussionLinkInputField.getValue().trim()) ) {
discussionLinkInputField.setValidityFlag(false);
error = true;
} else {
discussionLinkInputField.setValidityFlag(true);
}
} else if (isNew) {
if ( !(newNomHeaderInputField.getValue().trim()) ) {
newNomHeaderInputField.setValidityFlag(false);
error = true;
} else {
newNomHeaderInputField.setValidityFlag(true);
}
if ( !(rationaleInputField.getValue().trim()) ) {
rationaleInputField.setValidityFlag(false);
error = true;
} else {
rationaleInputField.setValidityFlag(true);
}
}
batches = actions.map(function ({titleListInputField, prependTextInputField, label, actionInputField}) {
if ( !(prependTextInputField.getValue().trim()) ) {
prependTextInputField.setValidityFlag(false);
error = true;
} else {
prependTextInputField.setValidityFlag(true);
}
if (isNew) {
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 capitalise(s) {
return s[0].toUpperCase() + s.slice(1);
}
function normalise(title) {
return 'Category:' + capitalise(title.replace(/^ *[Cc]ategory:/, '').trim());
}
titleList.forEach(function(title) {
if (title) {
var targets = title.split('|');
var newTitle = targets.shift();
newTitle = normalise(newTitle);
if (!Object.keys(titles).includes(newTitle) ) {
titles[newTitle] = targets.map(normalise);
}
}
});
if ( !(Object.keys(titles).length) ) {
titleListInputField.setValidityFlag(false);
error = true;
} else {
titleListInputField.setValidityFlag(true);
}
return {
titles: titles,
prependText: prependTextInputField.getValue().trim(),
label: label,
actionInputField: actionInputField
};
});
if (error) {
return;
}
for (let element of elementsToDisable) {
element.setDisabled(true);
}
$('.remove-batch-button').remove();
var abortButton = createAbortButton();
bodyContent.append(abortButton.$element);
window.abortEdits = false; // initialise
abortButton.on('click', function() {
// Set abortEdits flag to true
if (confirm('Are you sure you want to abort?')) {
abortButton.setDisabled(true);
window.abortEdits = true;
}
});
var allTitles = batches.reduce((allTitles, obj) => {
return allTitles.concat(Object.keys(obj.titles));
}, []);
createAuthorList(allTitles).then(function(authors) {
function processContent(content, titles, textToModify, summary, type, doneMessage, headingLabel) {
if (!Array.isArray(titles)) {
var titlesDict = titles;
titles = Object.keys(titles);
}
var fieldset = createFieldset(headingLabel);
content.append(fieldset.$element);
var progressElement = createProgressElement();
fieldset.addItems([progressElement]);
var ratelimitMessage = createRatelimitMessage();
ratelimitMessage.toggle(false);
fieldset.addItems([ratelimitMessage]);
var progressObj = createProgressBar(`(0 / ${titles.length}, 0 errors)`); // with label
var progress = progressObj.progressBar;
var progressContainer = progressObj.fieldlayout;
// Add margin or padding to the progress bar widget
progress.$element.css('margin-top', '5px');
progress.pushPending();
fieldset.addItems([progressContainer]);
let resolvedCount = 0;
let rejectedCount = 0;
function updateCounter() {
progressContainer.setLabel(`(${resolvedCount} / ${titles.length}, ${rejectedCount} errors)`);
}
function updateProgress() {
var percentage = (resolvedCount + rejectedCount) / titles.length * 100;
progress.setProgress(percentage);
}
function trackPromise(promise) {
return new Promise((resolve, reject) => {
promise
.then(value => {
resolvedCount++;
updateCounter();
updateProgress();
resolve(value);
})
.catch(error => {
rejectedCount++;
updateCounter();
updateProgress();
resolve(error);
});
});
}
return new Promise(async function(resolve) {
var promises = [];
for (const title of titles) {
var promise = editPage(title, textToModify, summary, progressElement, ratelimitMessage, progress, type, titlesDict);
promises.push(trackPromise(promise));
await sleep(100); // space out calls
await massCFDratelimitPromise; // stop if ratelimit reached (global variable)
}
Promise.allSettled(promises)
.then(function() {
progress.toggle(false);
if (window.abortEdits) {
var abortMessage = createAbortMessage();
abortMessage.setLabel( $('<span>Edits manually aborted. <a id="masscfdrevertlink" onclick="revertEdits()">Revert?</a></span>') );
content.append(abortMessage.$element);
} else {
var completedElement = createCompletedElement();
completedElement.setLabel(doneMessage);
completedElement.$element.css('margin-bottom', '16px');
content.append(completedElement.$element);
}
resolve();
})
.catch(function(error) {
console.error("Error occurred during title processing:", error);
resolve();
});
});
}
const date = new Date();
const year = date.getUTCFullYear();
const month = date.toLocaleString('default', { month: 'long', timeZone: 'UTC' });
const day = date.getUTCDate();
var summaryDiscussionLink;
var discussionPage = `Wikipedia:Categories for discussion/Log/${year} ${month} ${day}`;
if (isOld) summaryDiscussionLink = discussionLinkInputField.getValue().trim();
else if (isNew) summaryDiscussionLink = `${discussionPage}#${newNomHeaderInputField.getValue().trim()}`;
const advSummary = ' ([[User:Qwerfjkl/scripts/massCFD.js|via script]])';
const categorySummary = 'Tagging page for [[' +summaryDiscussionLink+']]' + advSummary;
const userSummary = 'Notifying user about [[' +summaryDiscussionLink+']]' + advSummary;
const userNotification = '{{ subst:Cfd mass notice |'+summaryDiscussionLink+'}} ~~~~';
const nominationSummary = `Adding mass nomination at [[#${newNomHeaderInputField.getValue().trim()}]]${advSummary}`;
var batchesToProcess = [];
var newNomPromise = new Promise(function (resolve) {
if (isNew) {
nominationText = `==== ${newNomHeaderInputField.getValue().trim()} ====\n`;
for (const batch of batches) {
var action = batch.actionInputField.getValue().trim();
for (const category of Object.keys(batch.titles)) {
var targets = batch.titles[category].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()}|${category}}}${targetText}\n`;
}
}
var rationale = rationaleInputField.getValue().trim().replace(/\n/, '<br />');
nominationText += `:'''Nominator's rationale:''' ${rationale} ~~~~`;
var newText;
var nominationRegex = /==== ?NEW NOMINATIONS ?====\s*(?:<!-- ?Please add the newest nominations below this line ?-->)?/;
getWikitext(discussionPage).then(function(wikitext) {
if ( !wikitext.match(nominationRegex) ) {
var nominationErrorMessage = createNominationErrorMessage();
bodyContent.append(nominationErrorMessage.$element);
} else {
newText = wikitext.replace(nominationRegex, '$&\n\n'+nominationText); // $& contains all the matched text
batchesToProcess.push({
content: bodyContent,
titles: [discussionPage],
textToModify: newText,
summary: nominationSummary,
type: 'text',
doneMessage: 'Nomination added',
headingLabel: 'Creating nomination'
});
resolve();
}
}).catch(function (error) {
console.error('An error occurred in fetching wikitext:', error);
resolve();
});
} else resolve();
});
newNomPromise.then(async function () {
batches.forEach(batch => {
batchesToProcess.push({
content: bodyContent,
titles: batch.titles,
textToModify: batch.prependText,
summary: categorySummary,
type: 'prepend',
doneMessage: 'All categories edited.',
headingLabel: 'Editing categories' + ((batches.length > 1) ? ' — '+batch.label : '')
});
});
if (notifyCheckbox.isSelected()) {
batchesToProcess.push({
content: bodyContent,
titles: authors,
textToModify: userNotification,
summary: userSummary,
type: 'append',
doneMessage: 'All users notified.',
headingLabel: 'Notifying users'
});
}
let promise = Promise.resolve();
// abort handling is now only in the editPage() function
for (const batch of batchesToProcess) {
await processContent(...Object.values(batch));
}
promise.then(() => {
abortButton.setLabel('Revert');
// All done
}).catch(err => {
console.error('Error occurred:', err);
});
});
});
});
});
}
}
// Run the script when the page is ready
$(document).ready(runMassCFD);
// </nowiki>