User:Max/EasyVoteMeta.js
Appearance
< User:Max
(Redirected from User:DreamRimmer/EasyVoteMeta.js)Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
// Works on Steward_requests/Global_permissions and Meta:Requests for adminship
// <nowiki>
$(() => {
'use strict';
const EasyVoteMeta = {
config: {
messages: {
windowTitle: 'EasyVoteMeta',
windowButton: 'Submit',
vote: 'Please select your !vote:',
pleaseSelectVote: 'Please select your !vote',
confirmPossibleDouble: 'It appears that you have previously commented on this request. Are you sure you want to continue? Please strike out past comments!',
editError: 'Error: Reload the page (F5) and try again',
comment: 'Comment (optional):',
signatureAutoAdd: 'Your signature will be added automatically.'
},
voteOptions: {
support: {
label: 'Support',
template: 'support'
},
strongsupport: {
label: 'Strong support',
template: 'strong support'
},
weaksupport: {
label: 'Weak support',
template: 'weak support'
},
oppose: {
label: 'Oppose',
template: 'oppose'
},
strongoppose: {
label: 'Strong oppose',
template: 'strong oppose'
},
weakoppose: {
label: 'Weak oppose',
template: 'weak oppose'
},
neutral: {
label: 'Neutral',
template: 'neutral'
},
moralsupport: {
label: 'Moral support',
template: 'moral support'
},
abstain: {
label: 'Abstain',
template: 'abstain'
}
},
iconMap: {
support: 'https://upload.wikimedia.org/wikipedia/commons/9/94/Symbol_support_vote.svg',
strongsupport: 'https://upload.wikimedia.org/wikipedia/commons/8/84/Symbol_strong_support_vote.svg',
weaksupport: 'https://upload.wikimedia.org/wikipedia/commons/8/8c/GA_candidate.svg',
moralsupport: 'https://upload.wikimedia.org/wikipedia/commons/9/94/Symbol_support_vote.svg',
neutral: 'https://upload.wikimedia.org/wikipedia/commons/8/89/Symbol_neutral_vote.svg',
oppose: 'https://upload.wikimedia.org/wikipedia/commons/7/7f/Symbol_oppose_vote.svg',
weakoppose: 'https://upload.wikimedia.org/wikipedia/commons/f/f5/BA_candidate.svg',
strongoppose: 'https://upload.wikimedia.org/wikipedia/commons/5/5e/Symbol_oppose_vote_oversat.svg',
abstain: 'https://upload.wikimedia.org/wikipedia/commons/6/61/Symbol_abstain_vote.svg'
},
availableLangs: ['en'],
editSummary: '',
summfix: ' ([[User:Max/EasyVoteMeta.js|EVM]])',
pageConfigs: {
'Steward requests/Global permissions': {
type: 'steward',
targetPage: 'Steward requests/Global permissions'
},
'Meta:Requests for adminship': {
type: 'meta',
targetPage: 'Meta:Requests for adminship'
}
}
},
currentPageType: null,
currentPageConfig: null,
init() {
const currentPage = mw.config.get('wgPageName').replace(/_/g, ' ');
this.currentPageConfig = this.config.pageConfigs[currentPage];
Object.values(this.config.iconMap).forEach(function(url) {
var img = new window.Image();
img.src = url;
});
if (!this.currentPageConfig) return;
this.currentPageType = this.currentPageConfig.type;
if (mw.user.isAnon()) return;
mw.loader.using('mediawiki.api', this.run.bind(this));
},
run() {
this.addStyle();
const headingSelector = 'div.mw-heading.mw-heading3';
$(headingSelector).each((_, heading) => {
const $heading = $(heading);
const editLinks = $heading.find('.mw-editsection');
const sectionTitle = $heading.find('h3').attr('id').replace(/_/g, ' ');
const normalizedTitle = sectionTitle.toLowerCase();
if (
normalizedTitle.startsWith("epic") ||
normalizedTitle.startsWith("infinite0694") ||
normalizedTitle.startsWith("global ip block exempt") ||
normalizedTitle.startsWith("captcha exemptions") ||
normalizedTitle.startsWith("add global vrt") ||
normalizedTitle.startsWith("remove global vrt") ||
normalizedTitle.startsWith("2fa tester")
) return;
if (this.currentPageType === 'steward') {
this.handleStewardPage(editLinks, sectionTitle, $heading);
} else if (this.currentPageType === 'meta') {
this.handleMetaPage(editLinks, sectionTitle, $heading);
}
});
},
handleStewardPage(editLinks, sectionTitle, $heading) {
const sectionNum = this.getSectionNumber(editLinks);
if (sectionNum !== -1) {
this.addVoteLink(editLinks, sectionTitle, { type: 'steward', sectionNum: sectionNum }, $heading);
}
},
handleMetaPage(editLinks, sectionTitle, $heading) {
const normalizedTitle = sectionTitle.toLowerCase();
if (
normalizedTitle.startsWith("regular adminship") ||
normalizedTitle.startsWith("bureaucratship") ||
normalizedTitle.startsWith("other access") ||
normalizedTitle.startsWith("wmf office staff and contractors")
) return;
const targetPageInfo = this.getTargetPageInfo(editLinks, $heading);
if (targetPageInfo && targetPageInfo.targetPage) {
this.addVoteLink(editLinks, sectionTitle, { type: 'meta', ...targetPageInfo }, $heading);
}
},
addStyle() {
mw.util.addCSS(`
.easyvote-meta-link {
color: #0645ad;
text-decoration: none;
font-weight: bold;
}
.easyvote-meta-link:hover {
text-decoration: underline;
}
.easyvote-meta-disabled {
color: #888 !important;
pointer-events: none;
}
.easyvote-meta-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
border: 1px solid #a2a9b1;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
width: 520px;
max-height: 85vh;
z-index: 1001;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Lato, Helvetica, Arial, sans-serif;
}
.easyvote-meta-dialog-header {
padding: 16px 20px;
border-bottom: 1px solid #a2a9b1;
background: #eaecf0;
font-weight: bold;
font-size: 16px;
border-radius: 8px 8px 0 0;
}
.easyvote-meta-dialog-body {
padding: 20px;
max-height: calc(85vh - 125px);
overflow-y: auto;
}
.easyvote-meta-dialog-body p {
margin-top: 0;
}
.easyvote-meta-dialog-footer {
padding: 16px 20px;
border-top: 1px solid #a2a9b1;
background: #f8f9fa;
text-align: right;
border-radius: 0 0 8px 8px;
}
.easyvote-meta-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.2);
z-index: 1000;
}
.easyvote-meta-option {
margin: 8px 0;
display: flex;
align-items: center;
}
.easyvote-meta-option input {
margin-right: 8px;
}
.easyvote-meta-option label {
margin: 0;
cursor: pointer !important;
display: flex;
align-items: center;
}
.easyvote-meta-option img {
margin-right: 6px;
vertical-align: middle;
}
.easyvote-meta-button {
background: #36c;
color: white;
border: 1px solid #36c;
border-radius: 6px;
padding: 6px 12px;
font-size: 14px;
cursor: pointer !important;
margin-left: 8px;
position: relative;
min-width: 80px;
}
.easyvote-meta-button:hover:not(:disabled) {
background: #447ff5;
border-color: #447ff5;
}
.easyvote-meta-button:disabled {
background: #c8ccd1;
border-color: #c8ccd1;
cursor: not-allowed;
}
.easyvote-meta-button-secondary {
background: #f8f9fa;
color: #202122;
border-radius: 6px;
border: 1px solid #a2a9b1;
}
.easyvote-meta-button-secondary:hover {
background: #eaecf0 !important;
border-color: #a2a9b1;
color: #202122;
}
.easyvote-meta-textarea {
width: 100%;
height: 60px;
resize: vertical;
font-family: monospace;
border: 1px solid #a2a9b1;
border-radius: 2px;
padding: 8px;
margin-top: 4px;
}
.easyvote-meta-signature-note {
font-style: italic;
color: #666;
font-size: 0.9em;
margin-top: 8px;
}
.easyvote-meta-spinner {
display: inline-block;
width: 12px;
height: 12px;
border: 1.5px solid #f3f3f3;
border-top: 1.5px solid #36c;
border-radius: 50%;
animation: easyvote-meta-spin 1s linear infinite;
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
}
@keyframes easyvote-meta-spin {
0% { transform: translateY(-50%) rotate(0deg); }
100% { transform: translateY(-50%) rotate(360deg); }
}
.easyvote-meta-button.loading {
padding-right: 35px;
}
`);
},
getSectionNumber(editLinks) {
const editSectionUrl = editLinks.find('a:first').attr('href');
const sectionMatch = editSectionUrl && editSectionUrl.match(/§ion=(\d+)(?:$|&)/);
return sectionMatch ? parseInt(sectionMatch[1]) : -1;
},
getTargetPageInfo(editLinks, $heading) {
const editSectionUrl = editLinks.find('a:first').attr('href');
if (editSectionUrl) {
const titleMatch = editSectionUrl.match(/[?&]title=([^&]+)/);
const sectionMatch = editSectionUrl.match(/[?&]section=([^&]+)/);
if (titleMatch) {
const targetPage = decodeURIComponent(titleMatch[1]).replace(/_/g, ' ');
const sectionId = sectionMatch ? sectionMatch[1] : null;
return {
targetPage: targetPage,
sectionId: sectionId
};
}
}
const headingLink = $heading.find('h3 a').first();
if (headingLink.length) {
const title = headingLink.attr('title');
if (title) {
return {
targetPage: title,
sectionId: 'T-1'
};
}
}
return null;
},
addVoteLink(editLinks, sectionTitle, pageInfo, $heading) {
const uniqueId = 'easyvote-meta-launch-' + Math.random().toString(36).substr(2, 9);
const voteLink = $('<a>')
.attr('id', uniqueId)
.addClass('easyvote-meta-link')
.text('!vote')
.attr('href', '#')
.click((event) => {
event.preventDefault();
if (!voteLink.hasClass('easyvote-meta-disabled')) {
voteLink.addClass('easyvote-meta-disabled');
this.openDialog(sectionTitle, pageInfo, $heading, uniqueId);
}
});
editLinks.children().last().before(' | ', voteLink);
},
openDialog(sectionTitle, pageInfo, $heading, uniqueId) {
const self = this;
if (pageInfo.type === 'meta') {
console.log('EasyVoteMeta: Target page that will be edited:', pageInfo.targetPage);
}
$('.easyvote-meta-dialog, .easyvote-meta-overlay').remove();
const $overlay = $('<div>').addClass('easyvote-meta-overlay').click((e) => {
if (e.target === e.currentTarget) {
this.closeDialog(uniqueId);
}
});
const $dialog = $('<div>')
.addClass('easyvote-meta-dialog')
.data('page-info', pageInfo)
.data('section-heading', $heading)
.data('unique-id', uniqueId);
const $header = $('<div>')
.addClass('easyvote-meta-dialog-header')
.text(this.config.messages.windowTitle + ': ' + sectionTitle);
const $body = $('<div>')
.addClass('easyvote-meta-dialog-body');
const $form = $('<form>')
.attr('name', 'easyVoteMetaForm')
.append($('<p>').text(this.config.messages.vote));
Object.keys(this.config.voteOptions).forEach(key => {
$form.append(this.createVoteOption(key));
});
$form.append($('<br>'))
.append($('<label>')
.attr('for', 'easyvote-meta-comment')
.text(this.config.messages.comment))
.append($('<br>'))
.append($('<textarea>')
.attr({
id: 'easyvote-meta-comment',
name: 'comment',
rows: 3,
cols: 50
})
.addClass('easyvote-meta-textarea'))
.append($('<div>')
.addClass('easyvote-meta-signature-note')
.text(this.config.messages.signatureAutoAdd));
$body.append($form);
const $footer = $('<div>')
.addClass('easyvote-meta-dialog-footer');
const $cancelButton = $('<button>')
.addClass('easyvote-meta-button easyvote-meta-button-secondary')
.text('Cancel')
.click((e) => {
e.preventDefault();
this.closeDialog(uniqueId);
});
const $submitButton = $('<button>')
.addClass('easyvote-meta-button')
.attr('id', 'easyvote-meta-button')
.attr('disabled', true)
.text(this.config.messages.windowButton)
.click((e) => {
e.preventDefault();
this.doVote($dialog);
});
$footer.append($cancelButton, $submitButton);
$dialog.append($header, $body, $footer);
$('body').append($overlay, $dialog);
$form.find('input[name="vote"]').on('change', function() {
$submitButton.prop('disabled', false);
});
setTimeout(() => {
$form.find('input[name="vote"]:first').focus();
}, 100);
},
closeDialog(uniqueId) {
$('.easyvote-meta-dialog, .easyvote-meta-overlay').remove();
$(`#${uniqueId}`).removeClass('easyvote-meta-disabled');
},
createVoteOption(type) {
const voteOption = this.config.voteOptions[type];
const iconUrl = this.config.iconMap[type];
return $('<div>')
.addClass('easyvote-meta-option')
.append(
$('<input>')
.attr({
type: 'radio',
name: 'vote',
value: type,
id: 'easyvote-meta-' + type
})
)
.append(
$('<label>')
.attr('for', 'easyvote-meta-' + type)
.append(
$('<img>')
.attr({
src: iconUrl,
alt: '',
width: 20,
height: 20
})
)
.append(voteOption.label)
);
},
doVote($dialog) {
const self = this;
function onFail(error) {
const $voteButton = $('#easyvote-meta-button');
$voteButton.removeClass('loading').prop('disabled', false).text(self.config.messages.windowButton);
$voteButton.find('.easyvote-meta-spinner').remove();
console.error('Submission failed:', error);
alert(self.config.messages.editError);
}
let vote, comment, pageInfo, $heading, sectionTitle, uniqueId;
const $voteButton = $('#easyvote-meta-button');
const $checkedVote = $dialog.find('input[name="vote"]:checked');
if (!$checkedVote.length) {
alert(this.config.messages.pleaseSelectVote);
return false;
}
vote = $checkedVote.val();
comment = $dialog.find('#easyvote-meta-comment').val().trim();
pageInfo = $dialog.data('page-info');
$heading = $dialog.data('section-heading');
uniqueId = $dialog.data('unique-id');
sectionTitle = $heading.contents().not($heading.find('.mw-editsection')).text().trim();
$voteButton.addClass('loading').prop('disabled', true);
const $spinner = $('<span>').addClass('easyvote-meta-spinner');
$voteButton.append($spinner);
const templateName = this.config.voteOptions[vote].template;
let voteLine = '\n* {{' + templateName + '}}';
if (comment) voteLine += ' ' + comment;
voteLine += ' ~~~~';
const voteDisplayName = this.config.voteOptions[vote]?.label || vote;
let editSummary = `/* ${sectionTitle} */ ${voteDisplayName}${this.config.summfix}`;
const api = new mw.Api();
if (pageInfo.type === 'steward') {
this.doStewardVote(api, pageInfo, voteLine, editSummary, $dialog, onFail);
} else if (pageInfo.type === 'meta') {
this.doMetaVote(api, pageInfo, voteLine, editSummary, $dialog, onFail);
}
},
doStewardVote(api, pageInfo, voteLine, editSummary, $dialog, onFail) {
api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get('wgPageName'),
rvprop: 'content',
rvsection: pageInfo.sectionNum,
formatversion: 2
}).then((res) => {
const page = res.query.pages[0];
if (!page || !page.revisions || !page.revisions[0]) {
throw new Error('Failed to fetch section content');
}
const sectionText = page.revisions[0].content;
const currentUser = mw.config.get('wgUserName');
const userVotePattern = new RegExp('\\{\\{(support|oppose|strong support|weak support|strong oppose|weak oppose|neutral|moral support|s|ss|o|)\\}\\}[^~]*' + mw.util.escapeRegExp(currentUser), 'i');
if (userVotePattern.test(sectionText)) {
if (!confirm(this.config.messages.confirmPossibleDouble)) {
const $voteButton = $('#easyvote-meta-button');
$voteButton.removeClass('loading').prop('disabled', false).text(this.config.messages.windowButton);
$voteButton.find('.easyvote-meta-spinner').remove();
return;
}
}
return api.postWithEditToken({
action: 'edit',
title: mw.config.get('wgPageName'),
section: pageInfo.sectionNum,
appendtext: voteLine,
summary: editSummary,
token: mw.user.tokens.get('csrfToken')
});
}).then(() => {
setTimeout(() => {
window.location.reload();
}, 1500);
}).catch(onFail);
},
doMetaVote(api, pageInfo, voteLine, editSummary, $dialog, onFail) {
api.get({
action: 'query',
prop: 'revisions',
titles: pageInfo.targetPage,
rvprop: 'content',
rvsection: pageInfo.sectionId,
formatversion: 2
}).then((res) => {
const page = res.query.pages[0];
if (!page || !page.revisions || !page.revisions[0]) {
throw new Error('failed to fetch section content from target page');
}
const sectionText = page.revisions[0].content;
const currentUser = mw.config.get('wgUserName');
const userVotePattern = new RegExp('\\{\\{(support|oppose|strong support|weak support|strong oppose|weak oppose|neutral|moral support|s|ss|o|)\\}\\}[^~]*' + mw.util.escapeRegExp(currentUser), 'i');
if (userVotePattern.test(sectionText)) {
if (!confirm(this.config.messages.confirmPossibleDouble)) {
const $voteButton = $('#easyvote-meta-button');
$voteButton.removeClass('loading').prop('disabled', false).text(this.config.messages.windowButton);
$voteButton.find('.easyvote-meta-spinner').remove();
return;
}
}
return api.postWithEditToken({
action: 'edit',
title: pageInfo.targetPage,
section: pageInfo.sectionId,
appendtext: voteLine,
summary: editSummary,
token: mw.user.tokens.get('csrfToken')
});
}).then(() => {
setTimeout(() => {
window.location.reload();
}, 1500);
}).catch(onFail);
}
};
EasyVoteMeta.init();
});
// </nowiki>