User:Thilio/UniversalXfDVote.js
Appearance
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 code will be executed when previewing this page.
This user script seems to have a documentation page at User:Thilio/UniversalXfDVote.
/* Thilio UniversalScript inspired from User:Awesome Aasim/xfdvote with some improvements included all AfD, CfD, TfD, MfD, FfD, RfD that's why the name is Universal. Install using importScript('User:Thilio/UniversalXfDVote.js');
*/
// GEAR UP VROM VROM
(function ($, mw) {
'use strict';
// Prevent the script from being initialized more than once.
if (window.ThilioUniversalXfDVoteLoaded) { return; }
window.ThilioUniversalXfDVoteLoaded = true;
// Her basic settings starts here
var CONFIG = {
portlet: 'p-cactions',
watchlist: 'preferences',
accent: '#36c'
};
// Options logic for all votes, I made it this way so that won't get lost for back tracing
var VOTES = {
keep: { label: 'Keep', text: "'''Keep'''", color: 'green' },
delete: { label: 'Delete', text: "'''Delete'''", color: 'destructive' },
merge: { label: 'Merge', text: "'''Merge'''", target: 'Merge into', color: 'blue' },
redirect: { label: 'Redirect', text: "'''Redirect'''", target: 'Redirect to', color: 'gray' },
draftify: { label: 'Draftify', text: "'''Draftify'''" },
userfy: { label: 'Userfy', text: "'''Userfy'''", target: 'Userfy to' },
transwiki: { label: 'Transwiki', text: "'''Transwiki'''", target: 'Project or destination' },
skeep: { label: 'Speedy keep', text: "'''Speedy keep'''" },
sdelete: { label: 'Speedy delete', text: "'''Speedy delete'''", target: 'CSD criterion (optional)' },
pkeep: { label: 'Procedural keep', text: "'''Procedural keep'''" },
relist: { label: 'Relist', text: "'''Relist'''" },
noconsensus:{ label: 'No consensus', text: "'''No consensus'''" },
comment: { label: 'Comment', text: "'''Comment'''" },
custom: { label: 'Custom', text: '', custom: true }
};
// Here is her more extra vote options which including withdraw, snowball and othe stuffs
$.extend(VOTES, {
rename: { label: 'Rename', text: "'''Rename'''", target: 'Rename to' },
split: { label: 'Split', text: "'''Split'''", target: 'Split into' },
convert: { label: 'Convert', text: "'''Convert'''", target: 'Convert to' },
substitute: { label: 'Substitute', text: "'''Substitute'''" },
review: { label: 'Review', text: "'''Review'''" },
retarget: { label: 'Retarget', text: "'''Retarget'''", target: 'Retarget to' },
refine: { label: 'Refine', text: "'''Refine'''", target: 'Refine to' },
disambiguate: { label: 'Disambiguate', text: "'''Disambiguate'''", target: 'Disambiguation page' },
selectivemerge:{ label: 'Selective merge', text: "'''Selective merge'''", target: 'Merge selected content into' },
reversemerge: { label: 'Reverse merge', text: "'''Reverse merge'''", target: 'Reverse merge with' },
deleteredirect:{ label: 'Delete and redirect', text: "'''Delete and redirect'''", target: 'Redirect to' },
moveproject: { label: 'Move to projectspace', text: "'''Move to projectspace'''", target: 'Projectspace destination' },
snowball: { label: 'Snowball', text: "'''Snowball'''" },
withdraw: { label: 'Withdraw', text: "'''Withdraw'''" },
transfer: { label: 'Transfer to Commons', text: "'''Transfer to Commons'''" }
});
// Logic for XfD all venues starts from here
var VENUES = {
afd: { name: 'AfD', prefix: 'Wikipedia:Articles for deletion', primary: ['keep','delete','merge','redirect','comment'], more: ['sdelete','skeep','selectivemerge','reversemerge','deleteredirect','disambiguate','draftify','userfy','moveproject','transwiki','snowball','withdraw','pkeep','relist','noconsensus','custom'] },
cfd: { name: 'CfD', prefix: 'Wikipedia:Categories for discussion', primary: ['keep','delete','merge','rename','comment'], more: ['split','relist','noconsensus','custom'] },
tfd: { name: 'TfD', prefix: 'Wikipedia:Templates for discussion', primary: ['keep','delete','merge','redirect','comment'], more: ['convert','substitute','review','relist','noconsensus','custom'] },
mfd: { name: 'MfD', prefix: 'Wikipedia:Miscellany for deletion', primary: ['keep','delete','merge','redirect','comment'], more: ['userfy','relist','noconsensus','custom'] },
ffd: { name: 'FfD', prefix: 'Wikipedia:Files for discussion', primary: ['keep','delete','comment'], more: ['transfer','rename','relist','noconsensus','custom'] },
rfd: { name: 'RfD', prefix: 'Wikipedia:Redirects for discussion', primary: ['keep','delete','retarget','refine','comment'], more: ['disambiguate','relist','noconsensus','custom'] }
};
// I made this simple to avoid confusion, where the venue can be fine from the title of the page
function venueFromTitle(title) {
var normalized = (title || '').replace(/_/g, ' ').split('#')[0];
var found = null;
$.each(VENUES, function (key, venue) {
if (normalized === venue.prefix || normalized.indexOf(venue.prefix + '/') === 0) {
found = $.extend({ key: key }, venue);
return false;
}
});
return found;
}
// This one is a checkmate exactly to check if this is XfD page
function isDiscussionPage() {
return mw.config.get('wgNamespaceNumber') === 4 && !!venueFromTitle(mw.config.get('wgPageName')) && mw.config.get('wgAction') === 'view';
}
// This logic is to find nomination
function getNominationContext() {
if (mw.config.get('wgNamespaceNumber') % 2 === 1) { return null; }
if (mw.config.get('wgAction') !== 'view' || mw.config.get('wgNamespaceNumber') === 4 && venueFromTitle(mw.config.get('wgPageName'))) {
return null;
}
var result = null;
$('#mw-content-text .ambox, #mw-content-text .tmbox, #mw-content-text .ombox, #mw-content-text .fmbox').each(function () {
var $box = $(this);
var boxText = $.trim($box.text()).replace(/\s+/g, ' ');
if (/\bresult of (?:the )?discussion was\b|\bdiscussion (?:has been|was) closed\b/i.test(boxText)) { return; }
var $link = $box.find('a').filter(function () {
var title = $(this).attr('title') || '';
var href = $(this).attr('href') || '';
var titleMatch = href.match(/[?&]title=([^&#]+)/);
var pathMatch = href.match(/\/wiki\/(Wikipedia:[^#?]+)/);
var linkedTitle = titleMatch ? decodeURIComponent(titleMatch[1]) : pathMatch ? decodeURIComponent(pathMatch[1]) : title;
return !!venueFromTitle(linkedTitle);
}).first();
if (!$link.length) { return; }
var title = $link.attr('title') || '';
var href = $link.attr('href') || '';
var titleMatch = href.match(/[?&]title=([^&#]+)/);
var pathMatch = href.match(/\/wiki\/(Wikipedia:[^#?]+)/);
if (titleMatch) { title = decodeURIComponent(titleMatch[1]); }
else if (pathMatch) { title = decodeURIComponent(pathMatch[1]); }
var hash = href.indexOf('#') === -1 ? '' : decodeURIComponent(href.slice(href.indexOf('#') + 1));
var venue = venueFromTitle(title);
if (title && venue) {
var mergeNomination = venue.key === 'afd' && /\b(?:nominated|considered)\s+for\s+merg(?:e|ing)\b|\bproposed\s+(?:for\s+)?(?:merging|to\s+merge|to\s+be\s+merged)\b|\bmerge discussion\b/i.test(boxText);
result = { title: title.split('#')[0].replace(/_/g, ' '), anchor: hash, box: $box, venue: venue, mergeNomination: mergeNomination };
return false;
}
});
return result;
}
// Extra check way for a log
function isLogStylePage() {
var title = mw.config.get('wgPageName').replace(/_/g, ' ');
var venue = venueFromTitle(title);
return !!venue && (title === venue.prefix || /\/Log\//.test(title) || venue.key === 'ffd' && /^Wikipedia:Files for discussion\/\d{4} [A-Z][a-z]+ \d{1,2}$/.test(title));
}
// She can also clean the reason word using the below logic
function cleanReason(value) {
return $.trim(value || '').replace(/~{4,}/g, '').replace(/^[-:;,.\s]+|\s+$/g, '');
}
// She can make the target page blue
function linkTarget(value) {
var target = $.trim(value || '').replace(/^\[\[|\]\]$/g, '');
return target ? ' [[' + target + ']]' : '';
}
// Now the work of building vote text starts here
function buildWikitext(key, reason, target, customLabel) {
var vote = VOTES[key];
var icon = '';
var bold = vote.custom ? "'''" + cleanReason(customLabel) + "'''" : vote.text;
var targetText = vote.target && target ? linkTarget(target) : '';
var cleanTarget = $.trim(target || '').replace(/^\[\[|\]\]$/g, '');
if (vote.target && key !== 'sdelete' && cleanTarget) {
var connector = key === 'split' ? ' into ' : ' to ';
bold = "'''" + vote.label + connector + '[[' + cleanTarget + "]]'''";
targetText = '';
}
var punctuation = /[.!?]$/.test(reason) ? '' : '.';
return (icon || '* ') + bold + targetText + (reason ? ': ' + reason + punctuation : '') + ' ' + '~~' + '~~';
}
// Follow up section title and section number
function getHeadingContext($heading) {
var $headingOuter = $heading.closest('.mw-heading');
var edit = ($headingOuter.length ? $headingOuter : $heading).find('.mw-editsection a[href]').first();
var href = edit.length ? edit[0].href : '';
var titleMatch = href.match(/[?&]title=([^&]+)/);
var sectionMatch = href.match(/[?&]section=([^&]+)/);
return {
title: titleMatch ? decodeURIComponent(titleMatch[1]).replace(/_/g, ' ') : mw.config.get('wgPageName').replace(/_/g, ' '),
section: sectionMatch ? decodeURIComponent(sectionMatch[1]) : null,
href: href
};
}
// Link of the heading she can find it using this logic
function getHeadingEditSection($heading) {
var $headingOuter = $heading.closest('.mw-heading');
return ($headingOuter.length ? $headingOuter : $heading).find('.mw-editsection').first();
}
function insertVoteBeforeEdit($heading, context, headingText) {
var $headingOuter = $heading.closest('.mw-heading');
var $host = $headingOuter.length ? $headingOuter : $heading;
if ($host.find('.afdvote-inline').length) { return false; }
var $vote = inlineVote(context, headingText);
var $editSection = getHeadingEditSection($heading);
if ($editSection.length) {
$editSection.before($vote);
} else {
$heading.after($vote);
}
return true;
}
// This one designed from XfDcloser tool style where it shows [Vote]
function addDiscussionVoteLinks(pageVenue) {
var currentTitle = mw.config.get('wgPageName').replace(/_/g, ' ');
var onMainVenuePage = currentTitle === pageVenue.prefix;
var onDatedLogPage = currentTitle !== pageVenue.prefix && isLogStylePage();
var added = 0;
var selector = pageVenue.key === 'afd' ? 'h3' : 'h4';
$('#mw-content-text .mw-parser-output ' + selector).each(function () {
var $heading = $(this);
var context = getHeadingContext($heading);
var headingText = getSectionTitle($heading);
var targetVenue = venueFromTitle(context.title);
var separateNomination = context.title !== currentTitle && !!targetVenue;
if (!separateNomination && !onDatedLogPage && !onMainVenuePage) { return; }
if (onMainVenuePage && !separateNomination && !onDatedLogPage) {
if (pageVenue.key !== 'ffd' || !/^File:/i.test(headingText)) { return; }
}
if (/^(contents|current discussions|older discussions|new nominations|old nominations|instructions|see also|articles for deletion|categories for discussion|templates for discussion|miscellany for deletion|files for discussion|redirects for discussion|how to use this page|reasons for nominating a file|reasons for nominating a template|what not to nominate here|what not to nominate here|nominate a file|nominate a template|twinkle|manual method|methods|discussion|closing discussion|closing instructions|old business|closed discussions|completed discussions|old discussions|discussions approaching conclusion|recent nominations|current and past redirects for discussion.*|before listing a redirect for discussion|the guiding principles of rfd)$/i.test(headingText) || /^step\s+\d+\./i.test(headingText)) { return; }
if (onDatedLogPage && /^(?:\d{1,2}\s+[A-Z][a-z]+|[A-Z][a-z]+\s+\d{1,2})(?:\s+\d{4})?$/.test(headingText)) { return; }
context.venue = targetVenue || pageVenue;
if (insertVoteBeforeEdit($heading, context, headingText)) { added++; }
});
return added;
}
// C&H title
function getSectionTitle($heading) {
var $headline = $heading.find('.mw-headline').first();
if ($headline.length) {
return $.trim($headline.text());
}
var text = $heading.clone().find('.mw-editsection, .afdvote-inline').remove().end().text();
return $.trim(text);
}
function wikitextToHtml(text) {
text = text.replace(/\[\[([^|\]#]+)(?:\|[^\]]*)?\]\]/g, function(match, page) {
var display = page;
var pipeMatch = match.match(/\[\[([^|\]]+)\|([^\]]+)\]\]/);
if (pipeMatch) {
page = pipeMatch[1];
display = pipeMatch[2];
}
var url = mw.util.getUrl(page);
return '<a href="' + url + '">' + display + '</a>';
});
text = text.replace(/\[\[User:([^|\]#]+)(?:\|[^\]]*)?\]\]/g, function(match, username) {
var display = username;
var pipeMatch = match.match(/\[\[User:([^|\]]+)\|([^\]]+)\]\]/);
if (pipeMatch) {
username = pipeMatch[1];
display = pipeMatch[2];
}
var url = mw.util.getUrl('User:' + username);
return '<a href="' + url + '">' + display + '</a>';
});
text = text.replace(/\[\[User talk:([^|\]#]+)(?:\|[^\]]*)?\]\]/g, function(match, username) {
var display = username;
var pipeMatch = match.match(/\[\[User talk:([^|\]]+)\|([^\]]+)\]\]/);
if (pipeMatch) {
username = pipeMatch[1];
display = pipeMatch[2];
}
var url = mw.util.getUrl('User_talk:' + username);
return '<a href="' + url + '">' + display + '</a>';
});
return text;
}
// Fetch the exact nomination section and prepare its reason and nominator for the article's AfD notice.
function loadNominationDetails(api, pageTitle) {
return api.get({
action: 'parse',
page: pageTitle,
prop: 'text|wikitext',
formatversion: 2
}).then(function (data) {
var html = data.parse && data.parse.text && data.parse.text['*'] || '';
var wikitext = data.parse && data.parse.wikitext || '';
if (!html && !wikitext) {
throw new Error('The page could not be loaded.');
}
var $html = $('<div>').html(html);
$html.find('.mw-editsection, .afd-help, .delsort-notice, .boilerplate, .navbox, .toc, .toctitle').remove();
var $paragraphs = $html.find('p');
var reasonHtml = '';
var nominatorHtml = '';
var nominatorUsername = '';
var nominatorDate = '';
for (var i = 0; i < $paragraphs.length; i++) {
var $p = $($paragraphs[i]);
var text = $p.text().trim();
if (!text || text.length < 5) continue;
if (/^Note:|^Please|^Find sources/i.test(text)) continue;
if (/^[\s]*~{4,}[\s]*$/.test(text)) continue;
if ($p.closest('ul, ol').length > 0) continue;
reasonHtml = $p.html();
break;
}
if (!reasonHtml) {
var $contentDivs = $html.find('div').filter(function() {
var $this = $(this);
var text = $this.text().trim();
return text.length > 20 &&
!$this.closest('.editsection').length &&
!$this.hasClass('editsection');
});
if ($contentDivs.length > 0) {
reasonHtml = $contentDivs.first().html();
}
}
if (!reasonHtml && wikitext) {
var lines = wikitext.split(/\r?\n/);
for (var j = 0; j < lines.length; j++) {
var trimmed = lines[j].trim();
if (!trimmed) continue;
if (/^<noinclude>/.test(trimmed)) continue;
if (/^{{/.test(trimmed)) continue;
if (/^:/.test(trimmed)) continue;
if (/^\*/.test(trimmed)) continue;
if (/^===/.test(trimmed)) continue;
if (/^Note:/.test(trimmed)) continue;
if (trimmed.length > 10) {
reasonHtml = trimmed;
break;
}
}
}
if (!reasonHtml || reasonHtml.length < 5) {
throw new Error('The nomination reason could not be found.');
}
var $reason = $('<div>').html(reasonHtml);
var $userLink = $reason.find('a').filter(function () {
var href = $(this).attr('href') || '';
var title = $(this).attr('title') || '';
return /(?:^|[/:])User:/i.test(href) ||
/^User:/i.test(title);
}).first();
if ($userLink.length) {
var userHref = $userLink.attr('href') || '';
var userTitle = $userLink.attr('title') || '';
var userMatch =
userHref.match(/(?:^|[/:])User:([^#?]+)/i) ||
userTitle.match(/^User:([^#?]+)/i);
if (userMatch) {
nominatorUsername = decodeURIComponent(userMatch[1]).replace(/_/g, ' ');
}
}
var reasonPlainText = $reason.text().replace(/\s+/g, ' ').trim();
var dateMatch = reasonPlainText.match(
/(\d{1,2}:\d{2},\s*\d{1,2}\s+[A-Z][a-z]+\s+\d{4})\s+\(UTC\)/i
);
if (dateMatch) {
nominatorDate = dateMatch[1];
}
if (!nominatorUsername || !nominatorDate) {
var signatureMatch = wikitext.match(
/\[\[User:([^|\]#]+)(?:\|[^\]]*)?\]\][\s\S]{0,120}?(\d{1,2}:\d{2},\s*\d{1,2}\s+[A-Z][a-z]+\s+\d{4})\s*\(UTC\)/i
);
if (signatureMatch) {
if (!nominatorUsername) {
nominatorUsername = $.trim(signatureMatch[1]);
}
if (!nominatorDate) {
nominatorDate = $.trim(signatureMatch[2]);
}
}
}
if (nominatorUsername) {
var userUrl = mw.util.getUrl('User:' + nominatorUsername);
var talkUrl = mw.util.getUrl('User talk:' + nominatorUsername);
nominatorHtml =
'<a href="' + userUrl + '">' + nominatorUsername + '</a> ' +
'<a href="' + talkUrl + '">(talk)</a>';
}
var cleanedReasonHtml = $reason.html();
cleanedReasonHtml = cleanedReasonHtml.replace(
/\s*(?:--\s*)?<a[^>]+(?:href=["'][^"']*(?:User:|User%3A)[^"']*|title=["']User:[^"']*)[^>]*>.*?<\/a>\s*(?:\(\s*<a[^>]+(?:User_talk:|User_talk%3A)[^>]*>.*?<\/a>\s*\))?\s*\d{1,2}:\d{2},\s*\d{1,2}\s+[A-Z][a-z]+\s+\d{4}\s*\(UTC\)\s*$/i,
''
).trim();
cleanedReasonHtml = cleanedReasonHtml.replace(
/\s*(?:--\s*)?\[\[User:[^|\]#]+(?:\|[^\]]*)?\]\]\s*\(\s*\[\[User talk:[^|\]#]+(?:\|[^\]]*)?\]\]\s*\)\s*\d{1,2}:\d{2},\s*\d{1,2}\s+[A-Z][a-z]+\s+\d{4}\s*\(UTC\)\s*$/i,
''
).trim();
cleanedReasonHtml = cleanedReasonHtml.replace(
/\s*\d{1,2}:\d{2},\s*\d{1,2}\s+[A-Z][a-z]+\s+\d{4}\s*\(UTC\)\s*$/i,
''
).trim();
var reasonHtmlConverted = wikitextToHtml(cleanedReasonHtml);
reasonHtmlConverted = reasonHtmlConverted
.replace(/\d{1,2}:\d{2},\s*\d{1,2}\s+[A-Z][a-z]+\s+\d{4}\s*\(UTC\)/gi, '')
.replace(/\s+/g, ' ')
.trim();
return {
reasonHtml: reasonHtmlConverted,
nominatorHtml: nominatorHtml,
nominatorDate: nominatorDate
};
});
}
function addStyles() {
mw.util.addCSS([
'.afdvote-toolbar{border-left:4px solid ' + CONFIG.accent + ';padding:6px 8px;margin:8px 0;background:var(--background-color-neutral-subtle,#f8f9fa);color:var(--color-base,#202122)}',
'.afdvote-label{display:inline-block;margin:0 8px 4px 0;font-weight:500}',
'.afdvote-buttons{display:inline-flex;flex-wrap:wrap;gap:4px;vertical-align:middle}',
'.afdvote-buttons .mw-ui-button{margin:0}',
'.afdvote-keep{background:#14866d!important;border-color:#14866d!important;color:#fff!important}',
'.afdvote-keep:hover{background:#096450!important;border-color:#096450!important}',
'.afdvote-merge{background:#674ea7!important;border-color:#674ea7!important;color:#fff!important}',
'.afdvote-merge:hover{background:#4f3a83!important;border-color:#4f3a83!important}',
'.afdvote-merge-nomination .afdvote-merge{background:#674ea7!important;border-color:#674ea7!important;color:#fff!important}',
'.afdvote-merge-nomination .afdvote-merge:hover{background:#4f3a83!important;border-color:#4f3a83!important}',
'.afdvote-merge-nomination .mw-ui-destructive{background:#eaecf0!important;border-color:#72777d!important;color:#202122!important}',
'.afdvote-merge-nomination .mw-ui-destructive:hover{background:#c8ccd1!important;border-color:#54595d!important}',
'.afdvote-redirect{background:#36c!important;border-color:#36c!important;color:#fff!important}',
'.afdvote-redirect:hover{background:#2a4b8d!important;border-color:#2a4b8d!important}',
'.afdvote-more{min-width:9em}',
'.afdvote-inline{font-size:small;font-weight:normal;white-space:nowrap;margin-left:.35em;background-color:#ffc6c6}',
'.afdvote-inline a{cursor:pointer}',
'.afdvote-dialog-layer{position:fixed;inset:0;z-index:1000;pointer-events:none}',
'.afdvote-dialog{width:min(520px,calc(100vw - 24px));max-height:80vh;overflow:auto;background:var(--background-color-base,#fff);color:var(--color-base,#202122);border:1px solid var(--border-color-base,#a2a9b1);box-shadow:0 4px 18px rgba(0,0,0,.35);padding:10px;box-sizing:border-box}',
'.afdvote-dialog-floating{position:absolute;left:50%;top:90px;transform:translateX(-50%);pointer-events:auto}',
'.afdvote-dialog-floating h2{cursor:move;user-select:none;background:var(--background-color-neutral-subtle,#f8f9fa);margin:-10px -10px 6px;padding:7px 10px;border-bottom:1px solid var(--border-color-base,#a2a9b1);font-size:1.2em}',
'.afdvote-dialog-inline-wrap{margin:5px 0}',
'.afdvote-dialog-inline{width:100%;max-height:none;box-shadow:none;padding:8px}',
'.afdvote-dialog-inline h2{margin:0 0 4px;border:0;font-size:1.05em}',
'.afdvote-field{margin:5px 0}',
'.afdvote-field label{display:block;font-weight:bold;margin-bottom:2px}',
'.afdvote-field input,.afdvote-field select,.afdvote-field textarea{width:100%;box-sizing:border-box;padding:5px 6px;border:1px solid var(--border-color-base,#a2a9b1);background:var(--background-color-base,#fff);color:inherit}',
'.afdvote-position-display{width:100%;box-sizing:border-box;padding:5px 7px;border:1px solid var(--border-color-base,#a2a9b1);background:var(--background-color-neutral-subtle,#f8f9fa);font-weight:bold}',
'.afdvote-field textarea{min-height:58px;resize:vertical}',
'.afdvote-preview{padding:6px;margin:6px 0;border:1px solid var(--border-color-muted,#c8ccd1);background:var(--background-color-neutral-subtle,#f8f9fa)}',
'.afdvote-actions{display:flex;justify-content:center;align-items:center;gap:32px;margin-top:14px}',
'.afdvote-actions .mw-ui-button{margin:0!important;padding:2px 10px!important;min-height:0!important;background:linear-gradient(#fff,#e6e6e6)!important;border:1px solid #a2a9b1!important;border-radius:2px!important;box-shadow:0 1px 2px rgba(0,0,0,.18)!important;color:#202122!important;font-weight:normal!important;line-height:1.4!important}',
'.afdvote-actions .mw-ui-button:hover{background:linear-gradient(#fff,#d8d8d8)!important;border-color:#72777d!important}',
'.afdvote-status{min-height:1em;color:var(--color-destructive,#b32424);font-size:.9em}',
'.afdvote-nomination-details{margin:4px 0 0 4px;padding:2px 0;}',
'@media(max-width:600px){.afdvote-label{display:block}.afdvote-buttons{display:flex}.afdvote-buttons .mw-ui-button{flex:1 1 auto}.afdvote-dialog{padding:8px}}'
].join('\n'));
}
// Her button maker logic
function button(label, key, extraClass) {
return $('<button>')
.attr('type', 'button')
.addClass('mw-ui-button ' + (extraClass || ''))
.text(label)
.attr('data-afdvote', key);
}
// D&B toolbar
function toolbar($heading, venue, mergeNomination) {
var $bar = $('<div>').addClass('afdvote-toolbar');
if (mergeNomination) {
$bar.addClass('afdvote-merge-nomination');
}
if ($heading) {
$bar.data('afd-heading', $heading);
}
venue = venue || VENUES.afd;
$bar.append(
$('<span>').addClass('afdvote-label').text(venue.name + ' discussion actions:')
);
$bar.attr('data-xfd-venue', venue.key || 'afd');
var $buttons = $('<span>').addClass('afdvote-buttons');
venue.primary.forEach(function (key) {
var cssClass =
key === 'keep' ? 'afdvote-keep' :
key === 'delete' ? 'mw-ui-destructive' :
key === 'merge' ? 'afdvote-merge' :
key === 'redirect' ? 'afdvote-redirect' : '';
var label = key === 'comment' ?
'Leave a comment' :
'Vote to ' + VOTES[key].label.toLowerCase();
$buttons.append(button(label, key, cssClass));
});
var $more = $('<select>')
.addClass('afdvote-more')
.attr('aria-label', 'More deletion discussion actions');
$more.append(
$('<option>').val('').text('More actions…')
);
venue.more.forEach(function (key) {
$more.append(
$('<option>').val(key).text(VOTES[key].label)
);
});
$buttons.append($more);
$bar.append($buttons);
return $bar;
}
// Also this logic designed from XfDcloser tool style where it shows [Vote]
function inlineVote(context, sectionTitle) {
var $wrapper = $('<span>').addClass('afdvote-inline');
var $link = $('<a>').attr({ href: '#', role: 'button' }).text('Vote');
$wrapper.append(' [', $link, ']');
$link.on('click', function (e) {
e.preventDefault();
var title = $.trim(sectionTitle || '');
if (!title) {
var $heading = $(this).closest('.mw-heading').find('h1,h2,h3,h4,h5,h6').first();
if ($heading.length) {
title = getSectionTitle($heading);
}
}
openDialog(
'keep',
context.title,
context.section,
title,
null,
context.anchor,
true
);
});
return $wrapper;
}
function resolveSectionNumber(api, pageTitle, section, anchor) {
if (section || !anchor) {
return $.Deferred().resolve(section || null).promise();
}
var normalizedAnchor = decodeURIComponent(anchor)
.replace(/^#/, '')
.replace(/ /g, '_');
function normalize(value) {
return decodeURIComponent(value || '')
.replace(/ /g, '_')
.replace(/\.([0-9A-F]{2})/gi, function (all, hex) {
return String.fromCharCode(parseInt(hex, 16));
});
}
return api.get({
action: 'parse',
page: pageTitle,
prop: 'sections',
formatversion: 2
}).then(function (data) {
var sections = data.parse && data.parse.sections || [];
var match = sections.filter(function (item) {
return normalize(item.anchor) === normalize(normalizedAnchor) ||
item.line === decodeURIComponent(anchor).replace(/_/g, ' ');
})[0];
if (!match) {
throw new Error('The linked discussion section could not be found.');
}
return match.index;
});
}
// Did my best on this tho vote dialog
function openDialog(key, pageTitle, section, sectionTitle, $inlineHost, sectionAnchor, allowSelection) {
var vote = VOTES[key];
var activeVenue =
venueFromTitle(pageTitle || mw.config.get('wgPageName')) ||
$.extend({ key: 'afd' }, VENUES.afd);
var nominationTitle = $.trim(sectionTitle || '');
var normalizedPageTitle = (pageTitle || '').replace(/_/g, ' ');
if (!nominationTitle &&
normalizedPageTitle.indexOf(activeVenue.prefix + '/') === 0 &&
normalizedPageTitle.indexOf('/Log/') === -1) {
nominationTitle =
normalizedPageTitle.slice(activeVenue.prefix.length + 1);
}
var isInline = !!($inlineHost && $inlineHost.length);
var $container = $('<div>')
.addClass(isInline ? 'afdvote-dialog-inline-wrap' : 'afdvote-dialog-layer');
var $dialog = $('<div>')
.addClass(
'afdvote-dialog ' +
(isInline ? 'afdvote-dialog-inline' : 'afdvote-dialog-floating')
)
.attr({
role: 'dialog',
'aria-labelledby': 'afdvote-title'
});
if (!isInline) {
$dialog.attr('aria-modal', 'true');
}
var $voteSelect = $('<select>');
var availableKeys = allowSelection ?
activeVenue.primary.concat(activeVenue.more) :
[key];
availableKeys
.filter(function (item, index, list) {
return list.indexOf(item) === index;
})
.forEach(function (item) {
$voteSelect.append(
$('<option>').val(item).text(VOTES[item].label)
);
});
$voteSelect.val(key);
var $positionDisplay = $('<div>')
.addClass('afdvote-position-display')
.text(VOTES[key].label)
.toggle(!allowSelection);
$voteSelect.toggle(!!allowSelection);
var $custom = $('<input>').attr({
type: 'text',
placeholder: 'For example: Weak keep'
});
var $target = $('<input>').attr({
type: 'text',
placeholder: 'Page name without [[brackets]]'
});
var $reason = $('<textarea>').attr(
'placeholder',
'Explain your policy-based reasoning. Do not add your signature; it is added automatically.'
);
var $customField = $('<div>')
.addClass('afdvote-field')
.append('<label>Custom bolded position</label>', $custom);
var $targetField = $('<div>')
.addClass('afdvote-field')
.append($('<label>'), $target);
var $preview = $('<div>')
.addClass('afdvote-preview')
.hide();
var $status = $('<div>')
.addClass('afdvote-status')
.attr('role', 'status');
var $publish = button('Proceed', '');
var $previewButton = button('Preview', '');
var $cancelButton = button('Cancel', '');
var busy = false;
function updateFields() {
vote = VOTES[$voteSelect.val()];
$positionDisplay.text(vote.label);
$customField.toggle(!!vote.custom);
$targetField.toggle(!!vote.target);
$targetField.find('label').text(
vote.target || 'Target'
);
$publish.text('Proceed');
$preview.hide().empty();
}
function validate() {
var reason = cleanReason($reason.val());
if (vote.custom && !cleanReason($custom.val())) {
return 'Enter your custom position.';
}
if (
vote.target &&
!$.trim($target.val()) &&
[
'merge',
'redirect',
'userfy',
'transwiki',
'rename',
'split',
'convert',
'retarget',
'refine',
'disambiguate',
'selectivemerge',
'reversemerge',
'deleteredirect',
'moveproject'
].indexOf($voteSelect.val()) !== -1
) {
return 'Enter the target page or destination.';
}
if (!reason) {
return 'Enter a reason or comment.';
}
return '';
}
function wikitext() {
return buildWikitext(
$voteSelect.val(),
cleanReason($reason.val()),
$target.val(),
$custom.val(),
activeVenue
);
}
var $dialogTitle = $('<h2>').attr(
'id',
'afdvote-title'
);
if (nominationTitle) {
$dialogTitle.append(
document.createTextNode('Participating in '),
$('<a>')
.attr('href', mw.util.getUrl(nominationTitle))
.text(nominationTitle),
document.createTextNode(' in ' + activeVenue.name)
);
} else {
$dialogTitle.text(
'Participate in this ' + activeVenue.name
);
}
$dialog.append(
$dialogTitle,
$('<div>')
.addClass('afdvote-field')
.append(
'<label>Position</label>',
$positionDisplay,
$voteSelect
),
$customField,
$targetField,
$('<div>')
.addClass('afdvote-field')
.append(
'<label>Reason</label>',
$reason
),
$preview,
$status,
$('<div>')
.addClass('afdvote-actions')
.append(
$publish,
$previewButton,
$cancelButton
)
);
function closeDialog() {
$container.remove();
$(document).off('.afdvoteDrag').off('keydown.afdvote');
}
function makeDraggable() {
var dragging = false;
var offsetX = 0;
var offsetY = 0;
$dialog.find('h2').first().on(
'pointerdown',
function (e) {
var rect = $dialog[0].getBoundingClientRect();
dragging = true;
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
$dialog.css({
left: rect.left,
top: rect.top,
transform: 'none'
});
e.preventDefault();
}
);
$(document)
.on('pointermove.afdvoteDrag', function (e) {
if (!dragging) {
return;
}
var maxLeft = Math.max(
0,
window.innerWidth - $dialog.outerWidth()
);
var maxTop = Math.max(
0,
window.innerHeight - 50
);
$dialog.css({
left: Math.max(
0,
Math.min(maxLeft, e.clientX - offsetX)
),
top: Math.max(
0,
Math.min(maxTop, e.clientY - offsetY)
)
});
})
.on(
'pointerup.afdvoteDrag pointercancel.afdvoteDrag',
function () {
dragging = false;
}
);
}
$container.append($dialog);
if (isInline) {
$('.afdvote-dialog-inline-wrap').remove();
$inlineHost.after($container);
} else {
$('.afdvote-dialog-layer').remove();
$container.appendTo(document.body);
makeDraggable();
}
updateFields();
$reason.trigger('focus');
$voteSelect.on('change', updateFields);
$previewButton.on('click', function () {
var error = validate();
if (error) {
$status.text(error);
return;
}
$status
.css('color', 'var(--color-subtle,#54595d)')
.text('Loading preview…');
new mw.Api()
.post({
action: 'parse',
text: wikitext(),
contentmodel: 'wikitext',
prop: 'text',
pst: true
})
.done(function (data) {
$preview
.html(data.parse.text['*'])
.show();
$status
.css('color', '')
.text('');
})
.fail(function (code, data) {
$status
.css('color', '')
.text(
'Preview failed: ' +
(
data &&
data.error &&
data.error.info ||
code
)
);
});
});
$cancelButton.on('click', closeDialog);
$(document).on(
'keydown.afdvote',
function (e) {
if (e.key === 'Escape' && !busy) {
closeDialog();
}
}
);
$publish.on('click', function () {
var error = validate();
if (error || busy) {
$status.text(error);
return;
}
var targetTitle =
(pageTitle || '').replace(/_/g, ' ');
var targetVenue = venueFromTitle(targetTitle);
if (
!targetVenue ||
targetVenue.key !== activeVenue.key
) {
$status.text(
'Publishing stopped: the exact ' +
activeVenue.name +
' discussion page could not be verified.'
);
return;
}
busy = true;
$publish.prop('disabled', true);
$status
.css('color', 'var(--color-subtle,#54595d)')
.text('Proceeding…');
var summaryLabel =
$voteSelect.val() === 'custom' ?
cleanReason($custom.val()) :
VOTES[$voteSelect.val()].label;
var summary =
'+' +
summaryLabel +
' ' +
activeVenue.name +
' using [[User:Thilio/UniversalXfDVote|Universal XfD Vote Tool]]';
var api = new mw.Api();
var editSection = section || null;
resolveSectionNumber(
api,
targetTitle,
editSection,
sectionAnchor
).then(function (resolvedSection) {
editSection = resolvedSection;
return api.get({
action: 'query',
prop: 'revisions',
titles: targetTitle,
rvprop: 'ids|content',
rvslots: 'main',
formatversion: 2
});
}).then(function (queryData) {
var page =
queryData.query &&
queryData.query.pages &&
queryData.query.pages[0];
var revision =
page &&
page.revisions &&
page.revisions[0];
var content =
revision &&
revision.slots &&
revision.slots.main &&
revision.slots.main.content ||
'';
if (!page || page.missing || !revision) {
throw new Error(
'The linked deletion discussion page does not exist.'
);
}
var checkPromise = editSection ?
api.get({
action: 'parse',
page: targetTitle,
section: editSection,
prop: 'wikitext',
formatversion: 2
}).then(function (sectionData) {
return sectionData.parse &&
sectionData.parse.wikitext ||
'';
}) :
$.Deferred().resolve(content).promise();
return checkPromise.then(function (discussionText) {
if (
/\{\{\s*(?:afd top|mfd top|tfd top|cfd top|rfd top|ffd top|at)\b|archived discussion/i
.test(discussionText)
) {
throw new Error(
'This discussion appears to have been closed.'
);
}
return api.postWithToken('csrf', {
action: 'edit',
title: targetTitle,
section: editSection || undefined,
appendtext: '\n' + wikitext(),
summary: summary,
watchlist: CONFIG.watchlist,
baserevid: revision.revid
});
});
}).done(function () {
mw.notify(
'Your deletion discussion was published.',
{ type: 'success' }
);
location.reload();
}).fail(function (code, data) {
busy = false;
$publish.prop('disabled', false);
var info =
code instanceof Error ?
code.message :
data &&
data.error &&
data.error.info;
$status
.css('color', '')
.text(
code === 'editconflict' ?
'The page changed. Reload it and review the discussion before trying again.' :
'Publish failed: ' +
(info || code)
);
});
});
}
function init() {
var nominationContext = getNominationContext();
var pageVenue = venueFromTitle(
mw.config.get('wgPageName')
);
if (!isDiscussionPage() && !nominationContext) {
return;
}
addStyles();
if (nominationContext) {
var $textCell =
nominationContext.box.find(
'.ambox-text, .mbox-text'
).first();
if (!$textCell.length) {
$textCell = nominationContext.box;
}
if ($textCell.find('.afdvote-toolbar').length) {
return;
}
var $nominationBar =
toolbar(
null,
nominationContext.venue,
nominationContext.mergeNomination
)
.attr('data-xfd-page', nominationContext.title)
.attr(
'data-xfd-anchor',
nominationContext.anchor || ''
);
$textCell.append($nominationBar);
if (nominationContext.venue.key === 'afd') {
var $nominationDetails =
$('<div>')
.addClass('afdvote-nomination-details')
.html(
'<div style="padding: 8px 0; color: #54595d;">' +
'Loading nomination details...' +
'</div>'
);
$nominationBar.before($nominationDetails);
loadNominationDetails(
new mw.Api(),
nominationContext.title
)
.then(function (details) {
var html =
'<div style="padding: 8px 0; border-top: 1px solid #eaecf0; margin-top: 4px; font-size: 0.95em;">';
html +=
'<div style="margin-bottom: 4px;"><strong>Reason:</strong> ' +
details.reasonHtml +
'</div>';
if (details.nominatorHtml) {
html +=
'<div style="font-size: 0.85em; color: #54595d; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap;">';
html +=
'<span><strong>Nominated by:</strong> ' +
details.nominatorHtml +
'</span>';
if (details.nominatorDate) {
html +=
'<span style="font-size: 0.80em; color: #72777d;"><strong>Date and Time:</strong> ' +
details.nominatorDate +
' (UTC)</span>';
}
html += '</div>';
}
html += '</div>';
$nominationDetails
.html(html)
.show();
})
.fail(function (error) {
console.error(
'AfD load error:',
error
);
$nominationDetails
.html(
'<div style="padding: 8px 0; color: #b32424;">' +
'Could not load nomination details. Check console for details.' +
'</div>'
)
.show();
});
}
} else if (isLogStylePage()) {
addDiscussionVoteLinks(pageVenue);
} else {
addDiscussionVoteLinks(pageVenue);
}
$('.afdvote-toolbar')
.off('click.afdvote')
.on(
'click.afdvote',
'button[data-afdvote]',
function () {
var key =
$(this).attr('data-afdvote');
if (!key) {
return;
}
var $bar =
$(this).closest(
'.afdvote-toolbar'
);
openDialog(
key,
$bar.attr('data-xfd-page'),
null,
'',
$bar,
$bar.attr('data-xfd-anchor')
);
}
);
$('.afdvote-more')
.off('change.afdvote')
.on(
'change.afdvote',
function () {
if (!this.value) {
return;
}
var $bar =
$(this).closest(
'.afdvote-toolbar'
);
openDialog(
this.value,
$bar.attr('data-xfd-page'),
null,
'',
$bar,
$bar.attr('data-xfd-anchor')
);
this.value = '';
}
);
}
mw.loader.using([
'mediawiki.util',
'mediawiki.api'
]).then(function () {
$(init);
});
}(jQuery, mediaWiki));