// Fork of [[User:Writ Keeper/Scripts/autoCloser.js]]
//<nowiki>
console.log("Test script loaded.");
mw.loader.using(["oojs-ui-core", "oojs-ui-windows", "oojs-ui-widgets"], function() {
function escapeRegexp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
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\/(Category:[^?&]+?)$/;
let nonexistinghrefRegexp = /^https:\/\/en\.wikipedia\.org\/w\/index\.php\?title=(Category:[^&?]+?)&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);
if (existingMatch) {
hrefline.push(decodeURIComponent(existingMatch[1]).replaceAll('_', ' '));
}
if (nonexistingMatch) {
hrefline.push(decodeURIComponent(nonexistingMatch[1]).replaceAll('_', ' '));
}
}
});
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 cats of parsedArray) {
if (cats.length === 1) stringOutput += `* [[:${cats[0]}]]\n`;
if (cats.length === 2) stringOutput += `* [[:${cats[0]}]] to [[:${cats[1]}]]\n`;
if (cats.length === 3) stringOutput += `* [[:${cats[0]}]] to [[:${cats[1]}]] and [[:${cats[2]}]]\n`;
if (cats.length > 3) {
let firstCat = cats.pop(0);
let lastCat = cats.pop(0);
stringOutput += `* [[:${firstCat}}]] to [[:${cats.join(']], [[:')}]] and [[:${lastCat}]]\n`;
}
}
widget.insertContent(stringOutput);
}
// Code from https://doc.wikimedia.org/oojs-ui/master/js/#!/api/OO.ui.Dialog
// Add interface shell
function CfdDialog(config) {
CfdDialog.super.call(this, config);
this.InputText = config.InputText;
this.sectionIndex = config.sectionIndex || null;
CfdDialog.static.title = config.dialogTitle;
CfdDialog.static.actions = config.dialogActions;
}
OO.inheritClass(CfdDialog, OO.ui.ProcessDialog);
CfdDialog.static.name = 'CfdDialog';
CfdDialog.prototype.initialize = function() {
CfdDialog.super.prototype.initialize.call(this);
this.content = new OO.ui.PanelLayout({
padded: false,
expanded: false
});
this.content.$element.append('<p style="padding-left: 5px">Make any changes necessary:</p>');
CfdDialog.prototype.cfdlisterTextBox = new OO.ui.MultilineTextInputWidget({
autosize: true,
value: this.InputText,
id: "CFD-lister-text",
rows: Math.min((this.InputText.match(/\n/g) || []).length + 2, 10),
maxrows: 25
});
let textInputElement = CfdDialog.prototype.cfdlisterTextBox.$element.get(0);
let handler = handlepaste.bind(this, CfdDialog.prototype.cfdlisterTextBox);
// 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);
}
mw.loader.using('ext.wikiEditor', function() {
mw.addWikiEditor(CfdDialog.prototype.cfdlisterTextBox.$input);
});
CfdDialog.prototype.cfdlisterTextBox.$input.on('input', () => this.updateSize());
this.content.$element.append(CfdDialog.prototype.cfdlisterTextBox.$element);
this.$body.append(this.content.$element);
};
CfdDialog.prototype.getActionProcess = function(action) {
var dialog = this;
if (action) {
return new OO.ui.Process(function() {
dialog.close({
text: '\n\n' + CfdDialog.prototype.cfdlisterTextBox.value,
sectionIndex: this.sectionIndex
});
});
}
return CfdDialog.super.prototype.getActionProcess.call(this, action);
};
function editDiscussions() {
var text = localStorage.getItem('CFDNAClist');
if (!text) {
mw.notify('No discussions listed yet.', {
type: 'error'
});
return;
}
var windowManager = new OO.ui.WindowManager();
var cfdDialog = new CfdDialog({
InputText: text.trim(), // newlines will be stripped here and added back implicitly in the closing call
dialogTitle: 'Edit listed discussions',
dialogActions: [{
action: 'add',
label: 'Save',
flags: ['primary', 'progressive']
},
{
label: 'Cancel',
flags: ['destructive', 'safe']
}
]
});
windowManager.defaultSize = 'full';
$(document.body).append(windowManager.$element);
windowManager.addWindows([cfdDialog]);
windowManager.openWindow(cfdDialog);
windowManager.on('closing', (win, closing, data) => {
if (!data) return;
if (!data.text.trim()) {
OO.ui.confirm('Are you sure you want to delete all listed discussions?').done((response) => {
if (response) {
localStorage.setItem('CFDNAClist', '');
localStorage.setItem('CFDNAClist-count', '');
} else mw.notify('Aborted changes to listed discussions.');
});
} else {
localStorage.setItem('CFDNAClist', data.text);
mw.notify('Listed discussions updated.');
}
});
}
var editDiscussionslink = mw.util.addPortletLink('p-cactions', '#', 'Edit discussions', 'pt-cfdnaceditlist', 'Edit listed discussions', null, listDiscussionslink);
$(editDiscussionslink).click(function(event) {
event.preventDefault();
editDiscussions();
});
function quickClose(option, editLink, headerElement) {
if (typeof editLink !== "undefined") {
var regexResults = /title=([^&]+).*§ion=[\D]*(\d+)/.exec(editLink.href);
if (regexResults === null) {
return false;
}
var pageTitle = regexResults[1].replaceAll('_', ' '); // prettify
var sectionIndex = regexResults[2];
const params = {
action: "parse",
format: "json",
page: pageTitle,
prop: "wikitext|sections",
section: sectionIndex
};
const api = new mw.Api();
api.get(params).done(data => {
sectionTitle = data.parse.sections[0].line.replaceAll('_', ' ');
wikitext = data.parse.wikitext["*"];
const closedRegexp = /<div class="boilerplate cfd vfd xfd-closed mw-archivedtalk"/i;
if (closedRegexp.test(wikitext)) { // already closed
mw.notify('Discussion already closed, aborted closure.', {
type: 'error'
});
return;
}
const newWikitext = `\n${wikitext.replace(/====.+====/, `$&\n{{subst:cfd top|'''${option}'''}}${mw.config.get('wgUserGroups').includes('sysop') ? '' : ' {{subst:nacd}}'}. ~~~~\n`)}\n{{subst:cfd bottom}}`;
var requestData = {
action: "edit",
title: pageTitle,
format: "json",
section: sectionIndex,
text: newWikitext,
summary: `/* ${sectionTitle} */ Quick close as ${option} via [[User:Qwerfjkl/scripts/CFDlister.js|script]]`,
notminor: 1,
nocreate: 1,
token: mw.user.tokens.get('csrfToken')
};
$.ajax({
url: mw.util.wikiScript('api'),
type: 'POST',
dataType: 'json',
data: requestData
})
.then(function(data) {
if (data && data.edit && data.edit.result && data.edit.result == 'Success') {
mw.notify(`Discussion closed as ${option}.`);
// Now use wikitext from before, don't bother refetching
let result = option;
const categoryRegex = /====.+====\s+([\s\S]+)\s+[:\*]* *'''(?:Nominator'?s )?rationale?/i;
if (categoryRegex.test(wikitext)) { // correctly formatted
wikitext = wikitext.match(categoryRegex)[1];
} else {
alert("This nomination is missing a '''Nominator's rationale''': and so the script cannot recognise the categories nominated. Please manually fix this by adding '''Nominator's rationale''': just before the nominator's rationale.");
return;
}
// Cleanup
wikitext = wikitext.replace(/\{\{(?:lc|cl|cls|lcs|clc|cat)\|(.+?)\}\}/gi, '[[:Category:$1]]'); // fix category templates
wikitext = wikitext.replaceAll(':Category:Category:', ':Category:'); // fix double category - can be caused by above
wikitext = wikitext.replace(/'''(propose|delet|renam|split|(?:up)?merg|container).*?'''/gi, '');
wikitext = wikitext.replace(/^ *[:#\*]* */gm, '* '); // fix indentation
wikitext = wikitext.replace(/^\s*[\*\: ]*\s*\n/gm, ''); // remove lines with just *, : and whitespace
wikitext = wikitext.replace(/\n?\s*\*\s*$/, ''); // remove trailing asterisk
wikitext = wikitext.replace(/<br ?\/?>/g, ''); // remove br tags
wikitext = `* [[${pageTitle}#${sectionTitle}]]\nResult: '''${result}'''\n<syntaxhighlight lang="wikitext" copy>\n; [[${pageTitle}]]\n${wikitext}\n</syntaxhighlight>`;
var incorrectOptionRegexp;
switch (option) {
case 'rename':
case 'merge':
incorrectOptionRegexp = /\* *\[\[:Category:[^\]\n]+?\]\]$/gim;
if (incorrectOptionRegexp.test(wikitext)) {
mw.notify('Error parsing nomination. Please click "list discussion" to list it manually.', {
type: 'error'
});
return;
}
break;
case 'delete':
incorrectOptionRegexp = /\* *\[\[:Category:[^\]\n]+?\]\].+\[\[:Category:/gim;
if (incorrectOptionRegexp.test(wikitext)) {
mw.notify('Error parsing nomination. Please click "list discussion" to list it manually.', {
type: 'error'
});
return;
}
break;
default: // shouldn't happen unless the user has modified their html
mw.notify(`Error handling closure: ${option}. Please click "list discussion" to list it manually.`, {
type: 'error'
});
return;
}
if (wikitext.includes('<s>') || wikitext.includes('{{') || wikitext.includes('<!--')) {
// something probably needs manual review
mw.notify('Error parsing nomination. Please click "list discussion" to list it manually.', {
type: 'error'
});
return;
}
addCFD(null, null, {
text: '\n\n' + wikitext,
sectionIndex: sectionIndex
});
} else {
console.error('The edit query returned an error. =(\n\n' + JSON.stringify(data));
mw.notify('Error closing discussion, see console for details', {
type: 'error'
});
}
})
.catch(function(e) {
console.error('The ajax request failed.\n\n' + JSON.stringify(e));
});
});
}
}
function listDiscussion() {
editLink = $(this).siblings("a.sectionEditLink")[0];
if (typeof editLink !== "undefined") {
var regexResults = /title=([^&]+).*§ion=[\D]*(\d+)/.exec(editLink.href);
if (regexResults === null) {
return false;
}
var pageTitle = regexResults[1].replaceAll('_', ' ');
var sectionIndex = regexResults[2];
const params = {
action: "parse",
format: "json",
page: pageTitle,
prop: "wikitext|sections",
section: sectionIndex
};
const api = new mw.Api();
api.get(params).done(data => {
sectionTitle = data.parse.sections[0].line.replaceAll('_', ' ');
wikitext = data.parse.wikitext["*"];
resultRegexp = /<div class="boilerplate cfd vfd xfd-closed(?: mw-archivedtalk)?" style="background(?:-color)?:#bff9fc; margin:0 auto; padding:0 10px 0 10px; border:1px solid #AAAAAA;">\n:''The following is an archived discussion concerning one or more categories\. <span style="color:red"\>'''Please do not modify it\.'''<\/span> Subsequent comments should be made on an appropriate discussion page \(such as the category's \[\[Help:Using talk pages\|talk page\]\] or in a \[\[Wikipedia:Deletion review\|deletion review\]\]\)\. No further edits should be made to this section\.''\n\n:''The result of the discussion was:'' ?(?:<!-- ?Template:Cfd top ?-->)? ?'''(.+?)'''/i;
if (resultRegexp.test(wikitext)) { // match
result = wikitext.match(resultRegexp)[1];
} else {
result = 'RESULT';
}
wikitext = wikitext.replace(new RegExp(resultRegexp.source + '.+', 'i'), ''); // remove closure text, unneeded
const categoryRegex = /====.+====\s+([\s\S]+)\s+[:\*]* *'''(?:Nominator'?s )?rationale?/i;
if (categoryRegex.test(wikitext)) { // correctly formatted
wikitext = wikitext.match(categoryRegex)[1];
} else {
alert("This nomination is missing a '''Nominator's rationale''': and so the script cannot recognise the categories nominated. Please manually fix this by adding '''Nominator's rationale''': just before the nominator's rationale.");
return;
}
// Cleanup
wikitext = wikitext.replace(/\{\{(?:lc|cl|cls|lcs|clc|cat)\|(.+?)\}\}/gi, '[[:Category:$1]]'); // fix category templates
wikitext = wikitext.replaceAll(':Category:Category:', ':Category:'); // fix double category - can be caused by above
wikitext = wikitext.replace(/'''(propose|delet|renam|split|(?:up)?merg|container).*?'''/gi, '');
wikitext = wikitext.replace(/^ *[:#\*]* */gm, '* '); // fix indentation
wikitext = wikitext.replace(/^\s*[\*\: ]*\s*\n/gm, ''); // remove lines with just *, : and whitespace
wikitext = wikitext.replace(/\n?\s*\*\s*$/, ''); // remove trailing asterisk
wikitext = wikitext.replace(/<br ?\/?>/g, ''); // remove br tags
wikitext = "* [[" + pageTitle + "#" + sectionTitle + "]]\nResult: '''" + result + "'''\n<syntaxhighlight lang=\"wikitext\" copy>\n; [[" + pageTitle + "]]\n" + wikitext + "\n</syntaxhighlight>";
var windowManager = new OO.ui.WindowManager();
var cfdDialog = new CfdDialog({
InputText: wikitext,
sectionIndex: sectionIndex,
dialogTitle: 'List discussion for processing',
dialogActions: [{
action: 'add',
label: 'Add',
flags: ['primary', 'progressive']
},
{
label: 'Cancel',
flags: ['destructive', 'safe']
}
]
});
windowManager.defaultSize = 'full';
$(document.body).append(windowManager.$element);
windowManager.addWindows([cfdDialog]);
windowManager.openWindow(cfdDialog);
windowManager.on('closing', addCFD);
});
}
}
function addCFD(win, closing, data) {
if (data == null || data === undefined || data.text == '' || !data.text) {
return;
}
var wikitext = data.text;
var text = localStorage.getItem('CFDNAClist');
var count = localStorage.getItem('CFDNAClist-count') || 0;
if (text == '' || text == null) {
localStorage.setItem('CFDNAClist', wikitext);
} else {
localStorage.setItem('CFDNAClist', text + wikitext);
}
localStorage.setItem('CFDNAClist-count', Number(count) + 1);
mw.notify('Added discussion');
// double strike through handled sections
if (data.sectionIndex) {
// apply styles to show discussion has been closed (like XFDCloser)
$($("h4")[data.sectionIndex - 2]).css({
'text-decoration': 'line-through',
'text-decoration-style': 'double'
});
var startH4 = document.querySelectorAll("h4")[data.sectionIndex - 2].parentElement;
if (startH4) {
var elementsBetweenH4 = [];
var currentElement = startH4.nextElementSibling;
while (currentElement) {
if (currentElement.classList.contains("mw-heading") && currentElement.classList.contains("mw-heading4")) {
break;
}
elementsBetweenH4.push(currentElement);
currentElement = currentElement.nextElementSibling;
}
elementsBetweenH4.forEach(function(element) {
$(element).css('opacity', '50%');
});
}
}
}
function discussionListerSetup() {
console.log("Adding links to sections");
function createDropdownLink(text, clickHandler) {
var link = document.createElement("a");
link.href = "#";
link.innerHTML = text;
link.onclick = function(event) {
event.preventDefault();
clickHandler();
};
return link;
}
var sectionHeaders = $("h4 ~ .mw-editsection");
$('.mw-heading, h1, h2, h3, h4, h5, h6').css('overflow', 'visible'); // allow for dropdown, no idea what damage it could do
sectionHeaders.each(function(index, element) {
console.log("Potential sect");
var editLink = $(element).children("a")[0];
if (typeof editLink !== "undefined" && /§ion=[\D]*(\d+)/.exec(editLink.href)) {
console.log("Adding to sect");
$(editLink).addClass("sectionEditLink");
var discussionLister = $("<a>List discussion</a>");
discussionLister.click(listDiscussion);
// Create dropdown elements
var dropdownContainer = $(`<span class='dropdown-container' style="position:relative; display:inline-block; "></span>`);
var dropdownTrigger = $(`<a class='dropdown-trigger' style="color: #0645AD; text-decoration: none; cursor: pointer">One click close</a>`);
var dropdownMenu = $(`<span class='dropdown-menu' style="display: none; position: absolute; background-color: #fff; border: 1px solid #ddd; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); padding: 5px; min-width: 6em; z-index: 1; left: 0px; top: 0.8em;" ></span>`);
var actions = ['delete', 'rename', 'merge']; // needs to correspond with switch statement in quickClose()
for (var i = 0; i < actions.length; i++) {
const action = actions[i];
var menuItem = $(`<a style="display: block; color: #0645AD; text-decoration: none; padding: 10px; margin-top: 5px; font-size: 150%" class='dropdown-item'>${action}</a>`);
menuItem.click(function() {
quickClose(action, editLink, element);
dropdownMenu.hide();
});
// make red on hover
menuItem.on("mouseenter", function() {
$(this).css('color', 'red');
}).on("mouseleave", function() {
$(this).css('color', 'color: #0645AD');
});
dropdownMenu.append(menuItem);
}
dropdownTrigger.click(function() {
dropdownMenu.toggle();
});
// Append elements to the existing element
dropdownContainer.append(dropdownTrigger, dropdownMenu);
// Close the dropdown if the user clicks outside of it
$(document).click(function(event) {
if (!$(event.target).closest('.dropdown-container').length) {
dropdownMenu.hide();
}
});
let bracket = $(element).find('.mw-editsection-bracket').filter(function() {
return $(this).closest(".arky-span").length === 0; // compatibility with archiver.js
}).last();
$(bracket).before(' | ', discussionLister, " | ", dropdownContainer);
}
});
}
if (mw.config.get("wgPageName").match('Wikipedia:Categories_for_discussion')) $(document).ready(discussionListerSetup);
});
async function listPages() {
var text = localStorage.getItem('CFDNAClist');
var count = localStorage.getItem('CFDNAClist-count');
if (text == '' || text == null) {
mw.notify('No discussions to list, aborting');
return;
}
text = "\n\nPlease can an admin add the following:" + text + "\n~~~~";
const date = new Date();
const monthNames = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
const day = date.getUTCDate();
const month = monthNames[date.getUTCMonth()];
const year = date.getUTCFullYear();
// Return the formatted string
let current_date = `${day} ${month} ${year}`;
// Check if we need to make a new section
let sectionRequestData = {
"action": "parse",
"format": "json",
"page": "Wikipedia talk:Categories for discussion/Working",
"prop": "sections",
"formatversion": "2"
};
let sectionData;
try {
sectionData = await $.ajax({
url: mw.util.wikiScript('api'),
type: 'POST',
dataType: 'json',
data: sectionRequestData
});
} catch (error) {
console.error('Error occurred:', error);
// Handle the error or rethrow it
}
var requestData = {
action: "edit",
title: "Wikipedia talk:Categories for discussion/Working",
format: "json",
summary: "Add NAC request (" + count + " dicussions listed) via [[User:Qwerfjkl/scripts/CFDlister.js|script]]",
notminor: 1,
nocreate: 1,
redirect: 1,
token: mw.user.tokens.get('csrfToken')
};
if (sectionData.parse.sections.length != 0 && sectionData.parse.sections.slice(-1)[0].line.toLowerCase().startsWith("non-admin closure")) { // no need for a new section
requestData.appendtext = text;
} else { // new section
// helper function
const incrementSectionTitle = (sectTitle) => {
const regex = /(.*?)(\d+)?$/; // Matches the main text and optional numeric suffix
const match = sectTitle.match(regex);
if (!match) return sectTitle; // If no match, return the original sectTitle
const base = match[1].trim(); // Main text without the number
const number = match[2] ? parseInt(match[2], 10) : 1; // Extract or default to 1
return `${base} ${number + 1}`; // Increment the number and return
};
// make sure we don't duplicate titles
var sectionTitle = `Non-admin closure request (${current_date})`;
while (sectionData.parse.sections.some(section => section.line === sectionTitle)) {
sectionTitle = incrementSectionTitle(sectionTitle);
}
requestData.section = "new";
requestData.sectiontitle = sectionTitle;
requestData.text = text.replace(/^\s+/, ''); // rstrip
}
mw.notify('Editing WT:CFDW...', {
tag: 'CFDListerEdit'
});
$.ajax({
url: mw.util.wikiScript('api'),
type: 'POST',
dataType: 'json',
data: requestData
})
.then(function(data) {
if (data && data.edit && data.edit.result && data.edit.result == 'Success') {
mw.notify('Discussions listed.', {
tag: 'CFDListerEdit'
});
localStorage.setItem('CFDNAClist', '');
localStorage.setItem('CFDNAClist-count', 0);
window.location.href = "https://en.wikipedia.orghttps://demo.azizisearch.com/lite/wikipedia/page/Wikipedia_talk:Categories_for_discussion/Working#footer"; // redirect
} else {
alert('The edit query returned an error. =(');
}
})
.catch(function() {
alert('The ajax request failed.');
});
}
var listDiscussionslink = mw.util.addPortletLink('p-cactions', '#', 'List discussions', 'pt-cfdnaclist', 'List closed discussions for admins to handle');
$(listDiscussionslink).click(function(event) {
event.preventDefault();
listPages();
});
//</nowiki>