MediaWiki:Gadget-PatrollingEnhancements.js: Difference between revisions
Chrysophylax (talk | contribs) Created page with "var GPE = {}; →</pre> ==Configuration options== <pre>: // The initial value to put in the "deletion reason" text-field; you can // override this in your common.js (or ve..." |
m 1 revision imported |
||
| (2 intermediate revisions by 2 users not shown) | |||
| Line 1: | Line 1: | ||
// {{documentation}} | |||
/* | // ******** imported from [[MediaWiki:Gadget-LegacyScriptsNewNode.js]] ******** | ||
== | var newNode = window.newNode = function newNode(tagname) { | ||
var node = document.createElement(tagname); | |||
for (var i = 1; i < arguments.length; i++) { | |||
var argument = arguments[i]; | |||
if (typeof argument == 'string') { //Text | |||
node.appendChild(document.createTextNode(argument)); | |||
} else if (typeof argument == 'object') { | |||
if (argument instanceof Node) { // If it is a DOM Node | |||
node.appendChild(argument); | |||
} else { // Attributes (hopefully) | |||
for (var j in argument) { | |||
if (j === 'class') { // Classname different because... | |||
node.className = argument[j]; | |||
} else if (j == 'style') { // Style is special | |||
node.style.cssText = argument[j]; | |||
} else if (typeof argument[j] == 'function') { // Basic event handlers | |||
node.addEventListener(j, argument[j], false); | |||
} else { | |||
node.setAttribute(j, argument[j]); //Normal attributes | |||
} | |||
} | |||
} | |||
} | |||
} | |||
return node; | |||
{ | |||
{ | |||
/ | |||
{ | |||
}; | }; | ||
// **************************************************************************** | |||
(function PatrollingEnhancements_IIFE() { | |||
{ | window.GPE = typeof window.GPE == 'object' ? window.GPE : {}; | ||
const GPE = window.GPE; | |||
/* </pre> | |||
==Configuration options== | |||
<pre> */ | |||
// The initial value to put in the "deletion reason" text-field; you can | |||
// override this in your common.js (or vector.js or whatnot). | |||
GPE.initialDeleteReason = GPE.initialDeleteReason == undefined ? '' : GPE.initialDeleteReason; | |||
// The value to use as a deletion reason if you leave the text-field blank; you | |||
// can override it in your common.js (or vector.js or whatnot). If you *don't* | |||
// override this, then MediaWiki will generate an automatic deletion reason that | |||
// indicates the entry's last editor and the beginning of its content. | |||
GPE.deleteReasonIfBlank = GPE.deleteReasonIfBlank == undefined ? '' : GPE.deleteReasonIfBlank; | |||
// By DCDuring's request. If you set this to true, then Special:Watchlist will | |||
// show the deletion-reason text-input, but *not* the deletion-reason dropdown, | |||
// when there's an unpatrolled new-page-creation. | |||
GPE.hideDeleteReasonDropdownOnWatchlist = GPE.hideDeleteReasonDropdownOnWatchlist == undefined ? false : GPE.hideDeleteReasonDropdownOnWatchlist; | |||
/* </pre> | |||
==Automated patrolling (whitelisting)== | |||
<pre> */ | |||
// set GPE.currMonth and GPE.lastMonth (in the form of, e.g., '2013/February') | |||
( | |||
function () | |||
{ | |||
var monthNames = [ 'January', 'February', 'March', 'April', 'May', 'June', | |||
'July', 'August', 'September', 'October', 'November', | |||
'December' ]; | |||
var now = new Date(); | |||
var currYear = now.getFullYear(); | |||
var currMonthNum = now.getMonth(); | |||
GPE.currMonth = currYear + '/' + monthNames[currMonthNum]; | |||
var lastYear = (currMonthNum == 0 ? currYear - 1 : currYear); | |||
var lastMonthNum = (currMonthNum == 0 ? 11 : currMonthNum - 1); | |||
GPE.lastMonth = lastYear + '/' + monthNames[lastMonthNum]; | |||
} | |||
)(); | |||
GPE.individualWhiteListedPages = | |||
{ | |||
"Wiktionary:Requests for cleanup": true, | |||
"Wiktionary:Requests for verification": true, | |||
}; | "Wiktionary:Requests for deletion": true, | ||
"Wiktionary:Requests for deletion/Others": true, | |||
"Wiktionary:Requests for moves, mergers and splits": true, | |||
"Wiktionary:Information desk": true, | |||
"Wiktionary:Tea room": true, | |||
"Wiktionary:Etymology scriptorium": true, | |||
"Wiktionary:Requested entries (English)": true, | |||
"Wiktionary:Requested entries (Spanish)": true, | |||
"Wiktionary:List of protologisms": true, | |||
"Wiktionary:Translation requests": true, | |||
"Wiktionary:Feedback": true, | |||
"Wiktionary:Sandbox": true, | |||
"Wiktionary talk:Sandbox": true, | |||
"Wiktionary:Tutorial (Editing)/sandbox": true, | |||
"Wiktionary:Featured word candidates": true, | |||
"Wiktionary:Word of the day/Nominations": true | |||
}; | |||
GPE.individualWhiteListedPages["Wiktionary:Beer parlour/" + GPE.currMonth] = | |||
GPE.individualWhiteListedPages["Wiktionary:Beer parlour/" + GPE.lastMonth] = | |||
GPE.individualWhiteListedPages["Wiktionary:Grease pit/" + GPE.currMonth] = | |||
GPE.individualWhiteListedPages["Wiktionary:Grease pit/" + GPE.lastMonth] = | |||
true; | |||
// per-user white-listed sub-pages (for example, edits by user Foo | |||
// to User:Foo/vector.js should be autopatrolled): | |||
GPE.perUserWhiteListedSubPages = | |||
{ | |||
"/Sandbox": true, | |||
"/sandbox": true, | |||
"/chick.js": true, | |||
"/chick.css": true, | |||
"/standard.js": true, | |||
"/standard.css": true, | |||
"/cologneblue.js": true, | |||
"/cologneblue.css": true, | |||
"/modern.js": true, | |||
"/modern.css": true, | |||
"/myskin.js": true, | |||
"/myskin.css": true, | |||
"/nostalgia.js": true, | |||
"/nostalgia.css": true, | |||
"/simple.js": true, | |||
"/simple.css": true, | |||
"/vector.js": true, | |||
"/vector.css": true, | |||
"/common.js": true, | |||
"/common.css": true | |||
}; | |||
GPE.individualWhiteListedContributors = | |||
/ | { | ||
}; | |||
GPE.shouldAutoPatrol = function(link) | |||
{ | |||
var pagename = link.title; | |||
if(pagename.indexOf('User talk:') == 0) | |||
return true; | |||
if(pagename in GPE.individualWhiteListedPages) | |||
return true; | |||
var contributor; | |||
if(mediaWiki.config.get('wgCanonicalSpecialPageName') === 'Contributions') | |||
{ | |||
contributor = | |||
document.getElementById('t-contributions') | |||
.getElementsByTagName('a')[0].href.replace(/.*\//, ''); | |||
} | |||
else | |||
{ | |||
var li = link.parentNode; | |||
if(li.tagName.toUpperCase() == 'SPAN') | |||
li = li.parentNode; | |||
var links = li.getElementsByTagName('a'); | |||
for(var i = 0; i < links.length; ++i) | |||
if(links[i].title.indexOf('Special:Contributions/') == 0) | |||
{ | |||
contributor = links[i].title.substr('Special:Contributions/'.length); | |||
break; | |||
} | |||
} | |||
if(pagename.indexOf('User:' + contributor + '/') == 0) | |||
if(pagename.substr(contributor.length + 5) in GPE.perUserWhiteListedSubPages) | |||
return true; | |||
if(contributor in GPE.individualWhiteListedContributors) | |||
return true; | |||
return false; | |||
}; | |||
/* </pre> | |||
==Utility functions== | |||
<pre> */ | |||
GPE.newButton = function(text, color, hoverText) | |||
{ | |||
var button = newNode('button', text); | |||
button.style.background = color; | |||
button.style.color = '#FFF'; | |||
button.style.border = '0'; | |||
button.style.padding = '0'; | |||
button.style.cursor = 'pointer'; | |||
button.title = hoverText; | |||
return button; | |||
}; | |||
GPE.disableButton = function(button, text, hoverText) | |||
{ | |||
button.onclick = null; | |||
button.title = (hoverText || ''); | |||
button.innerHTML = text; | |||
// clear out explicit styling and disable, so we can get appropriate | |||
// disabled-button styles: | |||
button.style.background = ''; | |||
button.style.color = ''; | |||
button.style.cursor = ''; | |||
button.disabled = 'disabled'; | |||
}; | |||
/* </pre> | |||
==Individual patrol-buttons== | |||
<pre> */ | |||
GPE.addPatrolButton = function(link, rcid) | |||
{ | |||
if(link.className.search(/(?:^|\s)gpe-hasPatrolButton(?:\s|$)/) > -1) | |||
return; | |||
link.className = (link.className + ' gpe-hasPatrolButton').trim(); | |||
var button = GPE.newButton('M', '#009', 'click to mark as patrolled'); | |||
link.parentNode.insertBefore(button, link.nextSibling); | |||
link.parentNode.insertBefore(document.createTextNode(' · '), button); | |||
button.onclick = | |||
function () | |||
{ | |||
var token = mediaWiki.user.tokens.get('patrolToken'); | |||
$.post | |||
( | |||
'/w/api.php?format=json&action=patrol&assert=user', | |||
{ token: token, rcid: rcid }, | |||
function (data) | |||
{ | |||
if(data.patrol) | |||
GPE.disableButton(button, 'm', 'marked as patrolled'); | |||
else if(data.error) | |||
{ | |||
var msg = data.error.code + ': ' + data.error.info; | |||
if(data.error.code == 'badtoken') | |||
msg += ': "' + token + '"'; | |||
alert(msg); | |||
} | |||
}, | |||
'json' | |||
); | |||
}; | |||
if(GPE.shouldAutoPatrol(link)) | |||
button.click(); | |||
// remove the exclamation point: | |||
var tmp = link; | |||
while(tmp && tmp.nodeName.toUpperCase() !== 'LI') | |||
tmp = tmp.parentNode; | |||
if(tmp) | |||
tmp = tmp.getElementsByClassName('unpatrolled')[0]; | |||
if(tmp) | |||
tmp.parentNode.removeChild(tmp); | |||
== | }; | ||
/* </pre> | |||
==Individual delete-buttons== | |||
<pre> */ | |||
GPE.addDeleteButton = function(link, title) | |||
{ | |||
if(link.className.search(/(?:^|\s)gpe-hasDeleteButton(?:\s|$)/) > -1) | |||
return; | |||
link.className = (link.className + ' gpe-hasDeleteButton').trim(); | |||
var button = GPE.newButton('D', '#900', 'click to delete'); | |||
link.parentNode.insertBefore(button, link.nextSibling); | |||
link.parentNode.insertBefore(document.createTextNode(' · '), button); | |||
button.onclick = | |||
function () | |||
{ | |||
var dropdownReason = | |||
document.getElementById('deleteReasonsDropdown') | |||
? document.getElementById('deleteReasonsDropdown').value | |||
: ''; | |||
if(dropdownReason == 'other') | |||
dropdownReason = ''; | |||
var textInputReason = | |||
document.getElementById('deleteReasonTextInput').value; | |||
var reason; | |||
if(dropdownReason.length && textInputReason.length) | |||
reason = dropdownReason + ': ' + textInputReason; | |||
GPE. | else if(dropdownReason.length || textInputReason.length) | ||
reason = dropdownReason + textInputReason; | |||
else | |||
reason = GPE.deleteReasonIfBlank; | |||
var token = mediaWiki.user.tokens.get('deleteToken'); | |||
$.post | |||
( | |||
'/w/api.php?format=json&action=delete&assert=user', | |||
{ title: title, token: token, reason: reason }, | |||
function (data) | |||
{ | |||
if(data['delete']) | |||
GPE.disableButton(button, 'd', 'deleted'); | |||
else if(data.error) | |||
{ | |||
var msg = data.error.code + ': ' + data.error.info; | |||
/ | if(data.error.code == 'badtoken') | ||
== | msg += ': "' + token + '"'; | ||
alert(msg); | |||
} | |||
}, | |||
{ | 'json' | ||
); | |||
}; | |||
}; | |||
/* </pre> | |||
==Delete-reasons== | |||
<pre> */ | |||
}; | GPE.addDeleteReasonInput = function () | ||
{ var deleteReasonDiv = | |||
( newNode | |||
{ | ( 'div', | ||
{ style: | |||
'background:#900; color:#FFF; ' + | |||
'position:fixed; bottom:0; right:0; margin-bottom:0' | |||
}, | |||
'\u00A0Deletion reason:\u00A0' | |||
) | |||
); | |||
deleteReasonDiv.title = | |||
'the deletion reason (message/summary) to use when you click "D"'; | |||
var deleteReasonTextInput = | |||
( newNode | |||
( 'input', | |||
{ type: 'text', | |||
size: 80, | |||
id: 'deleteReasonTextInput', | |||
value: GPE.initialDeleteReason, | |||
style: 'position:fixed; right:0; margin-bottom:0' | |||
} | |||
) | |||
); | |||
deleteReasonDiv.appendChild(deleteReasonTextInput); | |||
document.getElementById('bodyContent').appendChild(deleteReasonDiv); | |||
if(GPE.hideDeleteReasonDropdownOnWatchlist) | |||
if(mediaWiki.config.get('wgPageName') == 'Special:Watchlist') | |||
return; | |||
// get canned messages from [[MediaWiki:Deletereason-dropdown]]: | |||
$.getJSON | |||
( '/w/api.php?format=json&action=query&meta=allmessages&ammessages=Deletereason-dropdown', | |||
function (data) | |||
{ var rawDeleteReasons = data.query.allmessages[0]['*']; | |||
var deleteReasonsDropdown = | |||
newNode('select', | |||
{ id: 'deleteReasonsDropdown', style: 'vertical-align: bottom' }); | |||
deleteReasonsDropdown.appendChild | |||
(newNode('option', { value: 'other' }, 'Other reason')); | |||
var optGroup = deleteReasonsDropdown; | |||
rawDeleteReasons.replace | |||
( /^(\*\*?) *(.+)$/gm, | |||
function (s, asterisks, text) | |||
{ if(asterisks == '*') | |||
deleteReasonsDropdown.appendChild | |||
(optGroup = newNode('optgroup', { label: text })); | |||
else // '**' | |||
optGroup.appendChild(newNode('option', { value: text }, text)); | |||
} | |||
); | |||
deleteReasonDiv.insertBefore(deleteReasonsDropdown, deleteReasonTextInput); | |||
deleteReasonDiv.insertBefore(newNode('br'), deleteReasonTextInput); | |||
deleteReasonDiv.insertBefore(document.createTextNode('\u00A0'), deleteReasonTextInput); | |||
} | |||
); | |||
}; | |||
/* </pre> | |||
==Namespaces== | |||
<pre> */ | |||
GPE.computeNamespaces = function | |||
(selected, includeAssociated, invertSelection) | |||
{ | |||
GPE. | var associated = Number(selected) + (selected % 2 === 0 ? 1 : -1); | ||
{ | if(invertSelection) | ||
{ | |||
var selector = document.getElementById('namespace'); | |||
if(! selector) | |||
return []; | |||
var ret = []; | |||
for(var option = selector.firstChild; option; option = option.nextSibling) | |||
if(option.nodeName.toUpperCase() === 'OPTION' && option.value) | |||
if(option.value != selected) | |||
if(! includeAssociated || option.value != associated) | |||
ret.push(option.value); | |||
return ret; | |||
} | |||
else | |||
{ | |||
if(includeAssociated) | |||
return [selected, associated]; | |||
else | |||
return [selected]; | |||
} | |||
}; | |||
GPE.generateRcnamespace = function () | |||
{ | |||
var currUrl = document.location.href; | |||
if(! /[?&]namespace=\d+(?:&|$)/.test(currUrl)) | |||
return; | |||
var selected = /[?&]namespace=(\d+)(?:&|$)/.exec(currUrl)[1]; | |||
}; | var includeAssociated = | ||
mediaWiki.config.get('wgPageName') !== 'Special:NewPages' | |||
/* </pre> | && /[?&]associated=(?!0?&|0?$)/.test(currUrl); | ||
== | var invertSelection = /[?&]invert=(?!0?&|0?$)/.test(currUrl); | ||
<pre> */ | var namespaces = | ||
GPE.computeNamespaces(selected, includeAssociated, invertSelection); | |||
if(namespaces.length > 0) | |||
return namespaces.join('|'); | |||
}; | |||
/* </pre> | |||
==Find and handle links== | |||
<pre> */ | |||
GPE.handleUnpatrolledEdits = function (rcidsByRevid) | |||
{ | |||
var links = | |||
document.getElementById('bodyContent').getElementsByTagName('a'); | |||
for(var i = links.length - 1; i >= 0; --i) | |||
{ | |||
var mapKey = /&diff=(prev&oldid=)?(\d+)(&|$)/.exec(links[i].href); | |||
if(mapKey && rcidsByRevid.hasOwnProperty(mapKey[2])) | |||
GPE.addPatrolButton(links[i], rcidsByRevid[mapKey[2]]); | |||
} | |||
}; | |||
GPE.findLinksToUnpatrolledNewPages = function (rcidsByTitle) | |||
{ | |||
if(mediaWiki.config.get('wgPageName') === 'Special:NewPages') | |||
{ | |||
var ret = []; | |||
$('li.not-patrolled a.mw-newpages-pagename').each(function () { | |||
if (this.title && rcidsByTitle.hasOwnProperty(this.title)) | |||
ret.push(this); | |||
}); | |||
return ret; | |||
} | |||
else | |||
{ | |||
var ret = []; | |||
var abbrs = | |||
document.getElementById('bodyContent').getElementsByTagName('abbr'); | |||
for(var i = abbrs.length - 1; i >= 0; --i) | |||
{ | |||
if(abbrs[i].className != 'newpage') | |||
continue; | |||
var link = abbrs[i]; | |||
while(link && link.nodeName.toUpperCase() != 'A') | |||
if(link.nodeName.toUpperCase() === 'SPAN' && link.className === 'mw-title') | |||
link = link.firstChild; | |||
else | |||
link = link.nextSibling; | |||
if(link && link.title && rcidsByTitle.hasOwnProperty(link.title)) | |||
); | ret.push(link); | ||
} | |||
return ret; | |||
} | |||
}; | |||
GPE.handleUnpatrolledNewPages = function (rcidsByTitle) | |||
{ | |||
var userIsSysop = | |||
mediaWiki.config.get('wgUserGroups').indexOf('sysop') > -1; | |||
var links = GPE.findLinksToUnpatrolledNewPages(rcidsByTitle); | |||
for(var i = links.length - 1; i >= 0; --i) | |||
{ | |||
var link = links[i]; | |||
// 2016-04: Equinox removing red D delete button because it hasn't worked for a year. | |||
// if(userIsSysop) | |||
// GPE.addDeleteButton(link, link.title); | |||
GPE.addPatrolButton(link, rcidsByTitle[link.title]); | |||
} | |||
// Remove the rest of the delete features as above | |||
// if(userIsSysop && links.length > 0) | |||
// { | |||
// GPE.getAndStoreDeleteToken(); | |||
// GPE.addDeleteReasonInput(); | |||
// } | |||
}; | |||
GPE.getAndStoreDeleteToken = function () | |||
{ | |||
$.getJSON | |||
( | |||
'/w/api.php?format=json&action=tokens&type=delete', | |||
function (data) | |||
{ | |||
var token = data.tokens.deletetoken; | |||
if(! token || token.search(/^[0-9a-f]{32}\+\\$/) != 0) | |||
return; | |||
mediaWiki.user.tokens.set('deleteToken', token); | |||
} | |||
); | |||
}; | |||
GPE.main = function (params) | |||
{ | |||
var url = | |||
'/w/api.php?format=json&action=query&list=recentchanges' + | |||
'&rcprop=ids|title' + | |||
'&rcshow=!patrolled' + (params.hasOwnProperty('rcshow') ? '|' + params.rcshow : '') + | |||
'&rclimit=' + (params.hasOwnProperty('rclimit') ? params.rclimit : 500) + | |||
'&rctype=' + (params.hasOwnProperty('rctype') ? params.rctype : 'edit|new') + | |||
(params.hasOwnProperty('rcdir') ? '&rcdir=' + params.rcdir : '') + | |||
(params.hasOwnProperty('rcstart') ? '&rcstart=' + params.rcstart : '') + | |||
(params.hasOwnProperty('rcnamespace') ? '&rcnamespace=' + params.rcnamespace : '') + | |||
(params.hasOwnProperty('rcuser') ? '&rcuser=' + params.rcuser : ''); | |||
$.getJSON | |||
( | |||
url, | |||
function (data) | |||
{ | |||
data = data.query.recentchanges; | |||
var rcidsByRevid = {}; // for unpatrolled edits | |||
var rcidsByTitle = {}; // for unpatrolled new pages | |||
for(var i = 0; i < data.length; ++i) | |||
if(data[i].type == 'edit') | |||
rcidsByRevid[data[i].revid] = data[i].rcid; | |||
else | |||
rcidsByTitle[data[i].title] = data[i].rcid; | |||
GPE.handleUnpatrolledEdits(rcidsByRevid); | |||
GPE.handleUnpatrolledNewPages(rcidsByTitle); | |||
} | |||
); | |||
}; | |||
/* </pre> | |||
==Onload-hooks== | |||
<pre> */ | |||
$( document ).ready | |||
( function () | |||
{ | |||
if(mediaWiki.config.get('wgPageName') === 'Special:RecentChanges') { | |||
var currUrl = document.location.href; | |||
var params = {}; | |||
var rcshow = []; | |||
if(currUrl.search(/[?&]hideliu=(?!0?$|0?&)/) > -1) | |||
rcshow.push('anon'); | |||
else if(currUrl.search(/[?&]hideanons=(?!0?$|0?&)/) > -1) | |||
rcshow.push('!anon'); | |||
if(document.getElementsByClassName('minoredit').length === 0) | |||
rcshow.push('!minor'); | |||
if(rcshow.length > 0) | |||
params.rcshow = rcshow.join('|'); | |||
var rcnamespace = GPE.generateRcnamespace(); | |||
if(rcnamespace) | |||
params.rcnamespace = rcnamespace; | |||
GPE.main(params); | |||
} else if(mediaWiki.config.get('wgPageName') === 'Special:NewPages') { | |||
var currUrl = document.location.href; | |||
var params = { rctype: 'new' }; | |||
var rcshow = []; | |||
if(currUrl.search(/[?&]hideliu=(?!0?$|0?&)/) > -1) | |||
rcshow.push('anon'); | |||
if(currUrl.search(/[?&]hideredirs=0?(?:$|&)/) === -1) | |||
rcshow.push('!redirect'); | |||
if(rcshow.length > 0) | |||
params.rcshow = rcshow.join('|'); | |||
if(currUrl.search(/[?&]dir=prev(?=$|&)/) > -1) { | |||
params.rcdir = 'newer'; | |||
if(currUrl.search(/[?&]offset=\d+(?=$|&)/) > -1) | |||
params.rcstart = currUrl.match(/[?&]offset=(\d+)(?=$|&)/)[1]; | |||
} | |||
var rcnamespace = GPE.generateRcnamespace(); | |||
if(rcnamespace) | |||
params.rcnamespace = rcnamespace; | |||
GPE.main(params); | |||
} else if(mediaWiki.config.get('wgPageName') === 'Special:Watchlist') { | |||
var params = {}; | |||
var rcnamespace = GPE.generateRcnamespace(); | |||
if(rcnamespace) | |||
params.rcnamespace = rcnamespace; | |||
// TODO is this the best way to find what we need for the watchlist? | |||
GPE.main(params); | |||
} else if(mediaWiki.config.get('wgPageName').search(/^Special:Contributions(\/|$)/) === 0) | |||
GPE.main({ rcuser: document.getElementById('t-contributions').firstChild.href.replace(/^.*?\/Special:Contributions\//, '') }); | |||
else if(mediaWiki.config.get('wgAction') === 'markpatrolled' | |||
|| mediaWiki.config.get('wgAction') === 'delete' | |||
|| mediaWiki.config.get('wgAction') === 'rollback') | |||
GPE.main({ rclimit: 15 }); | |||
} | |||
); | |||
})(); | |||