Wikipedia
testwiki
https://test.wikipedia.org/wiki/Main_Page
MediaWiki 1.46.0-wmf.26
first-letter
Media
Special
Talk
User
User talk
Wikipedia
Wikipedia talk
File
File talk
MediaWiki
MediaWiki talk
Template
Template talk
Help
Help talk
Category
Category talk
Thread
Thread talk
Summary
Summary talk
Test namespace 1
Test namespace 1 talk
Test namespace 2
Test namespace 2 talk
Draft
Draft talk
Campaign
Campaign talk
TimedText
TimedText talk
Module
Module talk
SecurePoll
SecurePoll talk
CNBanner
CNBanner talk
Translations
Translations talk
Event
Event talk
Topic
Newsletter
Newsletter talk
MediaWiki:RefToolbarNoDialogs.js
8
40745
739888
197525
2026-04-30T21:33:59Z
Neriah
49609
Standardisation of thumbnail sizes
739888
javascript
text/javascript
var numforms = 0;
var wikEdAutoUpdateUrl;
function refbuttons() {
if (mwCustomEditButtons && (document.getElementById('toolbar') || document.getElementById('wikiEditor-ui-toolbar'))/* && wikEdAutoUpdateUrl == null*/) {
if (document.getElementById('toolbar')) {
button = document.createElement('a');
button.href = "javascript:easyCiteMain()";
button.title = "Insert citation";
buttonimage = document.createElement('img');
buttonimage.src = "//upload.wikimedia.org/wikipedia/commons/0/00/Button_easy_cite_%281%29.png";
buttonimage.alt = "Insert footnote";
button.appendChild(buttonimage);
document.getElementById('toolbar').appendChild(button);
} else {
button = document.createElement('a');
button.href = "#";
button.title = "Insert citation";
button.id = 'reftoolbar-button';
buttonimage = document.createElement('img');
buttonimage.src = "//upload.wikimedia.org/wikipedia/commons/thumb/9/9a/Curly_Brackets.svg/40px-Curly_Brackets.svg.png";
buttonimage.alt = "Insert citation";
button.classname = "tool tool-button";
buttonimage.style.width = "22px";
buttonimage.style.height = "17px";
buttonimage.style.paddingTop = "5px";
buttonimage.style.paddingLeft = "3px";
button.appendChild(buttonimage)
document.getElementById('wikiEditor-ui-toolbar').childNodes[0].childNodes[1].appendChild(button);
//document.getElementById('reftoolbar-button').onclick = easyCiteMain;
$('#reftoolbar-button').live('click', function() { easyCiteMain(); });
}
if (navigator.userAgent.indexOf('MSIE') == -1) {
citemain = document.createElement('div');
citemain.style.display = 'none';
citemain.setAttribute('Id', 'citeselect');
citemain.appendChild( addOption("citeWeb()", "Cite web") );
citemain.appendChild( addOption("citeBook()", "Cite book") );
citemain.appendChild( addOption("citeJournal()", "Cite journal") );
citemain.appendChild( addOption("citeNews()", "Cite news") );
citemain.appendChild( addOption("citeNamedRef()", "Named ref") );
citemain.appendChild( addOption("dispErrors()", "Error check") );
citemain.appendChild( addOption("hideInitial()", "Cancel") );
document.getElementById('wpTextbox1').parentNode.insertBefore(citemain, document.getElementById('wpTextbox1'));
}
else {
selection = '<div id="citeselect" style="display:none"><input type="button" value="Cite web" onclick="citeWeb()" />'+
'<input type="button" value="Cite book" onclick="citeBook()" />'+
'<input type="button" value="Cite journal" onclick="citeJournal()" />'+
'<input type="button" value="Cite news" onclick="citeNews()" />'+
'<input type="button" value="Named ref" onclick="citeNamedRef()" />'+
'<input type="button" value="Error check" onclick="dispErrors()" />'+
'<input type="button" value="Cancel" onclick="hideInitial()" /></div>';
document.getElementById('editform').innerHTML = selection + document.getElementById('editform').innerHTML;
}
}
}
function addOption(script, text) {
option = document.createElement('input');
option.setAttribute('type', 'button');
option.setAttribute('onclick', script);
option.setAttribute("value", text);
return option;
}
function hideInitial() {
document.getElementById('citeselect').style.display = 'none';
oldFormHide();
}
function oldFormHide() {
if (numforms != 0) {
document.getElementById('citediv'+numforms).style.display = 'none';
}
if (document.getElementById('errorform') != null) {
document.getElementById('citeselect').removeChild(document.getElementById('errorform'));
}
}
function easyCiteMain() {
document.getElementById('citeselect').style.display = '';
}
var months = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'];
var citeGlobalDateFormat = "<date> <monthname> <year>";
function getTime() {
var datestr = '';
if (typeof citeUserDateFormat != 'undefined') {
datestr = citeUserDateFormat;
} else {
datestr = citeGlobalDateFormat;
}
var DT = new Date();
var zmonth = '';
var month = DT.getUTCMonth()+1;
if (month < 10) {
zmonth = "0"+month.toString();
} else {
zmonth = month.toString();
}
month = month.toString();
var zdate = '';
var date = DT.getUTCDate()
if (date < 10) {
zdate = "0"+date.toString();
} else {
zdate = date.toString();
}
date = date.toString()
datestr = datestr.replace('<date>', date);
datestr = datestr.replace('<month>', month);
datestr = datestr.replace('<zdate>', zdate);
datestr = datestr.replace('<zmonth>', zmonth);
datestr = datestr.replace('<monthname>', months[DT.getUTCMonth()]);
datestr = datestr.replace('<year>', DT.getUTCFullYear().toString());
return (datestr);
}
function citeWeb() {
citeNewsWeb("cite web");
}
function citeNews() {
citeNewsWeb("cite news");
}
function citeNewsWeb(templatename) {
oldFormHide();
template = templatename;
var legend;
if (template == "cite web") {
legend = "Cite web source";
} else {
legend = "Cite news source";
}
newtime = getTime();
numforms++;
form = '<div id="citediv'+numforms+'">'+
'<fieldset><legend>'+legend+'</legend>'+
'<table cellspacing="5">'+
'<input type="hidden" value="'+template+'" id="template">'+
'<tr><td width="120"><label for="url"> URL: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="url"></td>'+
'<td width="120"><label for="title"> Title: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="title"></td></tr>'+
'<tr><td width="120"><label for="last"> Last name: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="last"></td>'+
'<td width="120"><label for="first"> First name: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="first"></td></tr>'+
'<tr><td width="120"><label for="coauthors"> Coauthors: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="coauthors"></td>'+
'<td width="120"><label for="date"> Publication date: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="date"></td></tr>'+
'<tr><td width="120"><label for="work"> Work: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="work"></td>'+
'<td width="120"><label for="publisher"> Publisher: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="publisher"></td></tr>'+
'<tr><td width="120"><label for="pages"> Pages: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="pages"></td>'+
'<td width="120"><label for="language"> Language: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="language"></td></tr>'+
'<tr><td width="120"><label for="accessdate"> Access date: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="accessdate" value="'+ newtime +'"></td>'+
'<td width="120"><label for="location"> Location: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="location"></td></tr>'+
'<tr><td width="120"><label for="refname"> Reference name: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="refname"></td></tr>'+
'</table>'+
'<input type="button" value="Add citation" onClick="addcites()">'+
'</fieldset></div>';
document.getElementById('citeselect').innerHTML += form;
}
function citeBook() {
oldFormHide();
template = "cite book";
numforms++;
form = '<div id="citediv'+numforms+'">'+
'<fieldset><legend>Cite book source</legend>'+
'<table cellspacing="5">'+
'<input type="hidden" value="'+template+'" id="template">'+
'<tr><td width="120"><label for="last"> Last name: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="last"></td>'+
'<td width="120"><label for="first"> First name: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="first"></td></tr>'+
'<tr><td width="120"><label for="coauthors"> Coauthors: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="coauthors"></td>'+
'<td width="120"><label for="others"> Others: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="others"></td></tr>'+
'<tr><td width="120"><label for="title"> Title: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="title"></td>'+
'<td width="120"><label for="editor"> Editor: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="editor"></td></tr>'+
'<tr><td width="120"><label for="publisher"> Publisher: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="publisher"></td>'+
'<td width="120"><label for="location"> Location: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="location"></td></tr>'+
'<tr><td width="120"><label for="date"> Publication date: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="date"></td>'+
'<td width="120"><label for="edition"> Edition: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="edition"></td></tr>'+
'<tr><td width="120"><label for="series"> Series: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="series"></td>'+
'<td width="120"><label for="volume"> Volume: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="volume"></td></tr>'+
'<tr><td width="120"><label for="pages"> Pages: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="pages"></td>'+
'<td width="120"><label for="chapter"> Chapter: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="chapter"></td></tr>'+
'<tr><td width="120"><label for="isbn"> ISBN: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="isbn"></td>'+
'<td width="120"><label for="oclc"> OCLC: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="oclc"></td></tr>'+
'<tr><td width="120"><label for="url"> URL: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="url"></td>'+
'<td width="120"><label for="accessdate"> Access date: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="accessdate"></td></tr>'+
'<tr><td width="120"><label for="language"> Language: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="language"></td>'+
'<td width="120"><label for="refname"> Reference name: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="refname"></td></tr>'+
'</table>'+
'<input type="button" value="Add citation" onClick="addcites()">'+
'</fieldset></div>';
document.getElementById('citeselect').innerHTML += form;
}
function citeJournal() {
oldFormHide();
template = "cite journal";
numforms++;
form = '<div id="citediv'+numforms+'">'+
'<fieldset><legend>Cite book source</legend>'+
'<table cellspacing="5">'+
'<input type="hidden" value="'+template+'" id="template">'+
'<tr><td width="120"><label for="last"> Last name: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="last"></td>'+
'<td width="120"><label for="first"> First name: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="first"></td></tr>'+
'<tr><td width="120"><label for="coauthors"> Coauthors: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="coauthors"></td>'+
'<td width="120"><label for="date"> Publication date: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="date"></td></tr>'+
'<tr><td width="120"><label for="title"> Title: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="title"></td>'+
'<td width="120"><label for="journal"> Journal: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="journal"></td></tr>'+
'<tr><td width="120"><label for="publisher"> Publisher: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="publisher"></td>'+
'<td width="120"><label for="location"> Location: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="location"></td></tr>'+
'<tr><td width="120"><label for="volume"> Volume: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="volume"></td>'+
'<td width="120"><label for="issue"> Issue: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="issue"></td></tr>'+
'<tr><td width="120"><label for="pages"> Pages: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="pages"></td>'+
'<td width="120"><label for="issn"> ISSN: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="issn"></td></tr>'+
'<tr><td width="120"><label for="oclc"> OCLC: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="oclc"></td>'+
'<td width="120"><label for="language"> Language: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="language"></td></tr>'+
'<tr><td width="120"><label for="url"> URL: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="url"></td>'+
'<td width="120"><label for="accessdate"> Access date: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="accessdate"></td></tr>'+
'<tr><td width="120"><label for="refname"> Reference name: </label></td>'+
'<td width="400"><input type="text" tabindex=1 style="width:100%" id="refname"></td></tr>'+
'</table>'+
'<input type="button" value="Add citation" onClick="addcites()">'+
'</fieldset></div>';
document.getElementById('citeselect').innerHTML += form;
}
function addcites(template) {
cites = document.getElementById('citediv'+numforms).getElementsByTagName('input');
var citebegin = '<ref';
var citename = '';
var citeinner = '';
for (var i=0; i<cites.length-1; i++) {
if (cites[i].value != '' && cites[i].id != "refname" && cites[i].id != "template") {
citeinner += "|" + cites[i].id + "=" + cites[i].value;
}
else if (cites[i].value != '' && cites[i].id == "refname" && cites[i].id != "template") {
citebegin += ' name="' + cites[i].value + '"';
}
else if (cites[i].value != '' && cites[i].id != "refname" && cites[i].id == "template") {
citename = '>{{' + cites[i].value;
}
}
cite = citebegin + citename + citeinner + "}}</ref>";
$("#wpTextbox1").focus();
insertTags(cite, '', '');
document.getElementById('citediv'+numforms).style.display = 'none';
}
function getNamedRefs(calls) {
if (typeof(wikEdUseWikEd) != 'undefined') {
if (wikEdUseWikEd == true) {
WikEdUpdateTextarea();
}
}
text = document.getElementById('wpTextbox1').value;
var regex;
if (calls) {
regex = /< *?ref +?name *?= *?(('([^']*?)')|("([^"]*?)")|([^'"\s]*?[^\/]\b)) *?\/ *?>/gi //'
} else {
regex = /< *?ref +?name *?= *?(('([^']*?)')|("([^"]*?)")|([^'"\s]*?[^\/]\b)) *?>/gi //'
}
var namedrefs = [];
var i=0;
var nr=true;
do {
ref = regex.exec(text);
if(ref != null){
if (ref[5]) {
namedrefs[i] = ref[5];
} else if (ref[3]) {
namedrefs[i] = ref[3];
} else {
namedrefs[i] = ref[6];
}
i++;
} else {
nr=false;
}
} while (nr==true);
return namedrefs;
}
function citeNamedRef() {
namedrefs = getNamedRefs(false);
if (namedrefs == '') {
oldFormHide();
numforms++;
out = '<div id="citediv'+numforms+'"><fieldset>'+
'<legend>References in text</legend>There are no named refs (<tt><ref name="Name"></tt>) in the text</fieldset></div>';
document.getElementById('citeselect').innerHTML += out;
}
else {
oldFormHide();
numforms++;
form = '<div id="citediv'+numforms+'">'+
'<fieldset><legend>References in article</legend>'+
'<table cellspacing="5">'+
'<tr><td><label for="namedrefs"> Named references in text</label></td>'+
'<td><select name="namedrefs" id="namedrefs">';
for (var i=0;i<namedrefs.length;i++) {
form+= '<option value="'+namedrefs[i]+'">'+namedrefs[i]+'</option>';
}
form+= '</select>'+
'</td></tr></table>'+
'<input type="button" value="Add citation" onClick="addnamedcite()">'+
'</fieldset></div>';
document.getElementById('citeselect').innerHTML += form;
}
}
function addnamedcite() {
name = document.getElementById('citediv'+numforms).getElementsByTagName('select')[0].value;
ref = '<ref name="'+name+'" />';
$("#wpTextbox1").focus();
insertTags(ref, '', '');
document.getElementById('citediv'+numforms).style.display = 'none';
}
function getAllRefs() {
if (typeof(wikEdUseWikEd) != 'undefined') {
if (wikEdUseWikEd == true) {
WikEdUpdateTextarea();
}
}
text = document.getElementById('wpTextbox1').value;
regex = /< *?ref( +?name *?= *?(('([^']*?)')|("([^"]*?)")|([^'"\s]*?[^\/]\b)))? *?>((.|\n)*?)< *?\/? *?ref *?>/gim //"
var allrefs = [];
var i=0;
var nr=true;
do {
ref = regex.exec(text);
if(ref != null){
if (ref[0].search(/[^\s]{150}/) != -1) {
ref[0] = ref[0].replace(/\|([^\s])/g, "| $1");
}
ref[0] = ref[0].replace(/</g, "<");
ref[0] = ref[0].replace(/>/g, ">");
allrefs[i] = ref[0];
i++;
} else {
nr=false;
}
} while (nr==true);
return allrefs;
}
function NRcallError(namedrefs, refname) {
for (var i=0; i<namedrefs.length; i++) {
if (namedrefs[i] == refname) {
return true;
}
}
return false;
}
function errorCheck() {
var allrefs = getAllRefs();
var allrefscontent = [];
var samecontentexclude = [];
var sx=0;
var templateexclude = [];
var tx=0;
var skipcheck = false;
var namedrefcalls = getNamedRefs(true);
for (var i=0; i<allrefs.length; i++) {
allrefscontent[i] = allrefs[i].replace(/< *?ref( +?name *?= *?(('([^']*?)')|("([^"]*?)")|([^'"\s]*?[^\/]\b)))? *?>((.|\n)*?)< *?\/? *?ref *?>/gim, "$8"); //"
}
var namedrefs = getNamedRefs(false);
var errorlist = [];
var q=0;
unclosed = document.getElementById('unclosed').checked;
samecontent = document.getElementById('samecontent').checked;
templates = document.getElementById('templates').checked;
repeated = document.getElementById('repeated').checked;
undef = document.getElementById('undef').checked;
for (var i=0; i<allrefs.length; i++) {
if (allrefs[i].search(/< *?\/ *?ref *?>/) == -1 && unclosed) {
errorlist[q] = '<tr><td width="75%"><tt>'+allrefs[i]+'</tt></td>';
errorlist[q] += '<td width="25%">Unclosed <tt><ref></tt> tag</td></tr>';
q++;
}
if (samecontent) {
for (var d=0; d<samecontentexclude.length; d++) {
if (allrefscontent[i] == samecontentexclude[d]) {
skipcheck = true;
}
}
var p=0;
while (p<allrefs.length && !skipcheck) {
if (allrefscontent[i] == allrefscontent[p] && i != p) {
errorlist[q] = '<tr><td width="75%"><tt>'+allrefscontent[i]+'</tt></td>';
errorlist[q] += '<td width="25%">Multiple refs contain this content, a <a href="//en.wikipedia.org/wiki/Wikipedia:Footnotes#Naming_a_ref_tag_so_it_can_be_used_more_than_once">named reference</a> should be used instead</td></tr>';
q++;
samecontentexclude[sx] = allrefscontent[i]
sx++;
break;
}
p++;
}
skipcheck=false;
}
if (templates) {
if (allrefscontent[i].search(/\{\{cite/i) == -1 && allrefscontent[i].search(/\{\{citation/i) == -1 && allrefscontent[i].search(/\{\{Comic (book|strip) reference/i) == -1 && allrefscontent[i].search(/\{\{Editorial cartoon reference/i) == -1 && allrefscontent[i].search(/\{\{harv/i) == -1) {
for (var x=0; x<templateexclude.length; x++) {
if (allrefscontent[i] == templateexclude[x]) {
skipcheck = true;
}
}
if (!skipcheck) {
errorlist[q] = '<tr><td width="75%"><tt>'+allrefs[i]+'</tt></td>';
errorlist[q] += '<td width="25%">Does not use a <a href="//en.wikipedia.org/wiki/Wikipedia:Citation_templates">citation template</a></td></tr>';
q++;
templateexclude[tx] = allrefscontent[i];
tx++;
}
skipcheck = false;
}
}
}
if (repeated) {
var repeatnameexclude = [];
var rx=0;
for (var k=0; k<namedrefs.length; k++) {
for (var d=0; d<repeatnameexclude.length; d++) {
if (namedrefs[k] == repeatnameexclude[d]) {
skipcheck = true;
}
}
var z=0;
while (z<namedrefs.length && !skipcheck) {
if (namedrefs[k] == namedrefs[z] && k != z) {
errorlist[q] = '<tr><td width="75%"><tt>'+namedrefs[k]+'</tt></td>';
errorlist[q] += '<td width="25%">Multiple references are given the same <a href="//en.wikipedia.org/wiki/Wikipedia:Footnotes#Naming_a_ref_tag_so_it_can_be_used_more_than_once">name</a></td></tr>';
q++;
repeatnameexclude[rx] = namedrefs[z];
rx++;
break;
}
z++;
}
skipcheck = false;
}
}
if (undef) {
var undefexclude = [];
var ux=0;
for (var p=0; p<namedrefcalls.length; p++) {
for (var d=0; d<undefexclude.length; d++) {
if (allrefscontent[i] == undefexclude[d]) {
skipcheck = true;
}
}
if (!skipcheck) {
if (!NRcallError(namedrefs, namedrefcalls[p])) {
errorlist[q] = '<tr><td width="75%"><tt>'+namedrefcalls[p]+'</tt></td>';
errorlist[q] += '<td width="25%">A <a href="//en.wikipedia.org/wiki/Wikipedia:Footnotes#Naming_a_ref_tag_so_it_can_be_used_more_than_once">named reference</a> is used but not defined</td></tr>';
q++;
undefexclude[ux] = namedrefs[p];
ux++;
}
}
skipcheck = false;
}
}
if (q > 0) {
return errorlist;
} else {
return 0;
}
}
function dispErrors() {
oldFormHide();
form = '<div id="errorform"><fieldset>'+
'<legend>Error checking</legend>'+
'<b>Check for:</b><br/>'+
'<input type="checkbox" id="unclosed" /> Unclosed <tt><ref></tt> tags<br/>'+
'<input type="checkbox" id="samecontent" /> References with the same content<br/>'+
'<input type="checkbox" id="templates" /> References not using a <a href="//en.wikipedia.org/wiki/Wikipedia:Citation_templates">citation template</a><br/>'+
'<input type="checkbox" id="repeated" /> Multiple references with the same name<br/>'+
'<input type="checkbox" id="undef" /> Usage of undefined named references<br/>'+
'<input type="button" id="errorchecksubmit" value="Check for selected errors" onclick="doErrorCheck()"/>'+
'</fieldset></div>';
document.getElementById('citeselect').innerHTML += form;
}
function doErrorCheck() {
var errors = errorCheck();
document.getElementById('citeselect').removeChild(document.getElementById('errorform'));
if (errors == 0) {
if (numforms != 0) {
document.getElementById('citediv'+numforms).style.display = 'none';
}
numforms++;
out = '<div id="citediv'+numforms+'"><fieldset>'+
'<legend>Error checking</legend>No errors found.</fieldset></div>';
document.getElementById('citeselect').innerHTML += out;
}
else {
if (numforms != 0) {
document.getElementById('citediv'+numforms).style.display = 'none';
}
numforms++;
form = '<div id="citediv'+numforms+'">'+
'<fieldset><legend>Error checking</legend>'+
'<table border="1px">';
for (var i=0; i<errors.length; i++) {
form+=errors[i];
}
form+= '</table>'+
'</fieldset></div>';
document.getElementById('citeselect').innerHTML += form;
}
}
$( refbuttons );
5kxj79g4mddej6hvfqd6tdzqr5pgbro
Páhina prencipal
0
49746
739948
112882
2026-05-01T10:01:35Z
~2026-26501-59
73775
739948
wikitext
text/x-wiki
[https://dukslfd.com]{{UntranslatedMainPage}}
kabubiy2906dulzv4th8i8dplz3k4yi
739949
739948
2026-05-01T10:03:15Z
~2026-26501-59
73775
showcaptcha
739949
wikitext
text/x-wiki
[https://dukslfd.com]
af edit{{UntranslatedMainPage}}
rg4ws9e7tl10i6oiu7q5obdwxc1r5r6
739950
739949
2026-05-01T10:04:15Z
~2026-26501-59
73775
739950
wikitext
text/x-wiki
[https://dukslfd.com]
af edit
[http://erikjsf.com]{{UntranslatedMainPage}}
5rc7tv1hag9hkimysrmgbqwkw6xelra
Test page for app testing/AbuseFilter
0
75440
739952
261377
2026-05-01T10:15:37Z
GiovanniPen
65549
Bot: Automated text replacement (-asd +melo)
739952
wikitext
text/x-wiki
== Section 2 ==
Triggering AbuseFilter number 1 by saying the abuse filter will block this at 1390850600122
== Section 2 ==
Triggering AbuseFilter number 1 by saying the abusefilter will block this at 1390850420379
melo
nzt6x7ngzbumvx646acyksbel1qdu0c
Testesd1231
0
91653
739908
657027
2026-05-01T03:56:12Z
PieWriter
72123
Tagging broken redirect for deletion([[:m:User:PieWriter/BR.js|BR]])
739908
wikitext
text/x-wiki
{{delete|Broken redirect|delete gsr}}
#REDIRECT [[Usuario:!Silent/Taller]]
0y3a99mybx7n50x1ub2zqa0hedlm87l
User:Sam Sailor/test.js
2
98186
739972
739854
2026-05-01T11:54:26Z
Sam Sailor
26820
Test
739972
javascript
text/javascript
//<nowiki>
/* global ComradeLib, mw, $ */
// Quality suite
(async function() {
await mw.loader.getScript('https://test.wikipedia.org/w/index.php?title=User:Sam_Sailor/test.js/comrade-lib.js&action=raw&ctype=text/javascript');
async function getTargetPage(api, name, type) {
const titles = type === 'surname' ? [`List of people with the English surname ${name}`, `List of people with surname ${name}`, `${name} (surname)`, `${name} (name)`, name] : [`List of people with given name ${name}`, `List of people named ${name}`, `${name} (given name)`, `${name} (name)`, name];
const res = await api.get({
action: 'query',
titles: titles.join('|'),
formatversion: 2
});
if (!res.query || !res.query.pages) return null;
return titles.find(t => {
const pg = res.query.pages.find(p => p.title === t);
return pg && !pg.missing;
});
}
async function processHumanNameLists(api, entity, wikitext, currentTitle, hasShortDesc, isOrphan, tClean) {
const {
givens,
surnames
} = await fetchWikidataNameLabels(entity);
const nameParts = tClean.split(' ');
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]);
for (const name of givens) {
if (name.toLowerCase() === primarySurname.toLowerCase()) continue;
const nameInTitle = new RegExp('\\b' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i').test(tClean);
if (!nameInTitle) continue;
const target = await getTargetPage(api, name, 'given');
if (target) await verifyAndNudge(api, name, 'given name', currentTitle, hasShortDesc, isOrphan, target);
}
const finalSurnames = surnames.length > 0 ? surnames : (nameParts.length >= 2 ? [nameParts[nameParts.length - 1]] : []);
for (const name of finalSurnames) {
const nameInTitle = new RegExp('\\b' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i').test(tClean);
if (!nameInTitle) continue;
const target = await getTargetPage(api, name, 'surname');
if (target) await verifyAndNudge(api, name, 'surname', currentTitle, hasShortDesc, isOrphan, target);
}
}
async function verifyAndNudge(api, name, type, currentTitle, hasShortDesc, isOrphan, foundTitle) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: foundTitle,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (!page || page.missing || !page.revisions) return;
const text = page.revisions[0].content;
const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text);
const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text);
const isNamePageTitle = foundTitle.includes('List of people with');
if (!isNameMatch && !isNameDab && !isNamePageTitle) return;
const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]');
const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text);
if (alreadyListed) return;
const $container = $('<div>').addClass('comrade-container').addClass(isOrphan ? 'comrade-nudge' : 'comrade-info');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text(isOrphan ? 'Comrade nudge:' : 'Informative:'));
const $content = $('<div>').addClass('comrade-content');
$content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({
href: mw.util.getUrl(foundTitle),
target: '_blank'
}).text(foundTitle), "; should it be?"));
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $instructionSpan = $('<span>').append('Consider adding ', $('<code>').text(snippet));
const $copyBtn = $('<button>').text('Copy & insert').on('click', function() {
navigator.clipboard.writeText('\n' + snippet).then(() => {
$(this).text('Copied!').prop('disabled', true);
const editUrl = mw.util.getUrl(foundTitle, {
action: 'edit',
summary: 'Adding * {{[[Template:Annotated biography link|anbl]]|[[' + currentTitle + ']]}}'
});
window.open(editUrl, '_blank');
});
});
$snippetWrapper.append($instructionSpan, $copyBtn);
$content.append($snippetWrapper);
if (!hasShortDesc) {
$content.append($('<span>').css({
'color': '#d33',
'font-weight': 'bold',
'margin-top': '5px'
}).text('⚠️ Missing short description: Please add a concise one before listing.'));
}
$container.append($header, $content);
ComradeLib.appendDismiss($container);
}
async function checkNamingPrecision(currentTitle, isHuman) {
const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/);
if (isHuman || !disambigMatch) return;
const baseTitle = disambigMatch[1].trim();
const api = new mw.Api();
try {
const baseRes = await api.get({
action: 'query',
titles: baseTitle,
redirects: 1,
formatversion: 2
});
const page = baseRes.query.pages[0];
const baseExists = !page.missing;
let baseRedirectsToSelf = false;
if (baseRes.query.redirects) {
baseRedirectsToSelf = baseRes.query.redirects.some(r => r.to === currentTitle);
}
const searchRes = await api.get({
action: 'query',
list: 'allpages',
apprefix: baseTitle,
aplimit: 50,
formatversion: 2
});
const competitors = searchRes.query.allpages.filter(p => {
const t = p.title;
return t !== currentTitle && t !== baseTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' ('));
});
if (!baseExists || baseRedirectsToSelf) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $content = $('<div>').addClass('comrade-content');
if (competitors.length === 0) {
$header.append($('<strong>').text('Precision:'));
const reason = baseRedirectsToSelf ? "already redirects here" : "is free";
$content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`));
const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, {
wpNewTitle: baseTitle,
wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]'
});
$content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank')));
} else {
$header.append($('<strong>').text('Observation:'));
const state = baseRedirectsToSelf ? 'redirect to this page' : 'redlink';
$content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${state}.`));
$content.append($('<span>').text('Consider if a disambiguation page is needed.'));
}
$container.append($header, $content);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.warn("Comrade: Precision check failed", e);
}
}
async function checkHumanSort(wikitext, entity, currentTitle, tClean, refName) {
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
const {
givens
} = await fetchWikidataNameLabels(entity);
if (dsMatch) {
const currentDS = dsMatch[1].trim();
if (currentDS.includes(',')) {
let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim());
if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) {
let fixedFirst = firstPart.substring(0, firstPart.length - lastPart.length).trim();
let suggestion = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(fixedFirst, refName)}`;
if (suggestion !== currentDS) renderSortNudge(suggestion, "Redundant surname in given name field.");
} else if (givens.length > 0 && lastPart.split(' ').length > 1) {
const parts = lastPart.split(' ');
const misplacedGiven = parts.find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase()));
if (misplacedGiven) {
const newSurname = parts.filter(p => p !== misplacedGiven).join(' ');
let suggestion = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`;
if (suggestion !== currentDS) renderSortNudge(suggestion, `Wikidata suggests "${misplacedGiven}" is a given name.`);
}
}
}
} else {
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const nameParts = tClean.split(' ');
const targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity);
if (targetSortName.toLowerCase() !== currentTitle.toLowerCase()) {
const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn));
$dsMissing.append($header);
ComradeLib.appendDismiss($dsMissing);
}
}
}
async function fetchWikidataEntity(qid) {
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetentities',
ids: qid,
props: 'claims|labels',
languages: 'en',
format: 'json',
origin: '*'
});
try {
const wikiData = await fetch(url).then(r => r.json());
return wikiData.entities[qid];
} catch (e) {
return null;
}
}
function getSmartSortKey(fullName, entity) {
const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || [];
const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || [];
const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean);
const allRelevantIds = [...countryIds, ...locationIds, ...placeIds];
const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null;
if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", "");
const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i;
if (fullName.match(arabicParticles)) {
if (birthYear && birthYear > 1900) {
const parts = fullName.split(' ');
const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben');
if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' ');
} else {
return fullName;
}
}
const eastAsianQids = ["Q148", "Q865", "Q884", "Q424", "Q17", "Q881"];
if (allRelevantIds.some(id => eastAsianQids.includes(id))) {
if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName);
const parts = fullName.split(' ');
if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`;
}
return westernSort(fullName);
}
function westernSort(name) {
let cleanName = name;
if (name.startsWith("O'")) cleanName = name.replace("O'", "O");
const parts = cleanName.split(' ');
if (parts.length < 2) return cleanName;
const surname = parts.pop();
if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) {
const actualSurname = parts.pop();
return `${actualSurname}, ${parts.join(' ')} ${surname}`;
}
return `${surname}, ${parts.join(' ')}`;
}
async function performSortCorrection(newName, type) {
const api = new mw.Api();
const title = mw.config.get("wgPageName");
try {
const data = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = data.query.pages[0].revisions[0].content;
const dsRegex = /\{\{\s*(?:DEFAULTSORT|Defaultsort)\s*:[^}]+\}\}/i;
const newTag = `{{DEFAULTSORT:${newName}}}`;
if (dsRegex.test(content)) {
content = content.replace(dsRegex, newTag);
} else {
const catRegex = /\[\[Category:/i;
content = catRegex.test(content) ? content.replace(catRegex, `${newTag}\n[[Category:`) : content.trim() + `\n\n${newTag}`;
}
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Setting DEFAULTSORT to ${newName}`,
nocreate: true
});
mw.notify(`DEFAULTSORT ${type === "add" ? "added" : "corrected"} to ${newName}!`, {
title: 'Comrade Success',
type: 'success'
});
setTimeout(() => location.reload(), 700);
} catch (err) {
mw.notify('Failed to save DEFAULTSORT.', {
type: 'error'
});
}
}
function renderSortNudge(target, reason) {
const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn));
$dsNudge.append($header);
ComradeLib.appendDismiss($dsNudge);
}
async function fetchWikidataNameLabels(entity) {
const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || [];
const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || [];
const allNameIds = [...new Set([...familyIds, ...givenIds])];
if (allNameIds.length === 0) return {
givens: [],
surnames: []
};
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetentities',
ids: allNameIds.join('|'),
props: 'labels',
languages: 'en',
format: 'json',
origin: '*'
});
const nameData = await fetch(url).then(r => r.json());
return {
givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean),
surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean)
};
}
function fixNameCasing(part, referenceName) {
return part.split(' ').map(word => {
const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i'));
if (match) return match[0];
return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word;
}).join(' ');
}
async function init() {
const api = new mw.Api();
const ns = mw.config.get("wgNamespaceNumber");
const action = mw.config.get("wgAction");
if (mw.config.get("wgDBname") !== "enwiki" || action !== "view") return;
if (ns === 14) {
const $btn = $('<button>').attr('id', 'comrade-audit-btn').text('Audit category quality').css({
'margin-bottom': '10px',
'padding': '5px 10px',
'cursor': 'pointer'
}).on('click', ComradeLib.performCategoryAudit);
$('.mw-category-generated').prepend($btn);
return;
}
if (ns !== 0) return;
const qid = mw.config.get("wgWikibaseItemId");
const currentTitle = mw.config.get("wgTitle");
try {
const [pageData, backlinkData] = await Promise.all([
api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
}),
api.get({
action: 'query',
list: 'backlinks',
blfilterredir: 'nonredirects',
bllimit: 50,
blnamespace: 0,
bltitle: mw.config.get("wgPageName"),
formatversion: 2
})
]);
const page = pageData.query.pages[0];
if (!page || page.missing || !page.revisions) return;
const wikitext = page.revisions[0].content;
// Ligature/ASCII-only/diacritic and domain
const norms = ComradeLib.getNormalizedTitles(currentTitle);
if (norms.ascii) {
let label = "ASCII-only";
let category = "R from ASCII-only";
if (/[æÆœŒijIJßẞ]/.test(currentTitle)) {
label = "Ligature";
category = "R to ligature";
} else if (currentTitle.normalize("NFD").match(/[\u0300-\u036f]/)) {
label = "Diacritic";
category = "R to diacritic";
}
await ComradeLib.checkAndRenderRedirect(norms.ascii, currentTitle, category, label);
}
await ComradeLib.checkDomainRedirect(qid, currentTitle);
// Orphan logic
const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext);
const backLinkCount = backlinkData.query.backlinks.length;
if (isOrphanTagged && backLinkCount >= 1) {
const $container = $('<div>').addClass('comrade-container comrade-status');
const $btn = $('<button>').text('Remove orphan tag').on('click', async () => await ComradeLib.performDeorphan());
const $statusText = $('<span>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn);
$container.append($('<div>').addClass('comrade-header').append($statusText));
ComradeLib.appendDismiss($container);
}
// Wikidata/human logic
let entity = qid ? await fetchWikidataEntity(qid) : null;
let isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
await checkNamingPrecision(currentTitle, isHuman);
if (isHuman && entity) {
const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim();
const refName = entity.labels?.en?.value || currentTitle;
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const isOrphan = isOrphanTagged && backLinkCount < 1;
await ComradeLib.checkShortDescDates(wikitext, entity);
// Check DEFAULTSORT
await checkHumanSort(wikitext, entity, currentTitle, tClean, refName);
const longName = ComradeLib.getLongNameFromWikitext(wikitext);
if (longName && longName.length > tClean.length) {
let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity);
const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`;
await ComradeLib.checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext);
}
// Sort name R
const targetSortName = hasFamilyNameHatnote ? `${tClean.split(' ')[0]}, ${tClean.split(' ').slice(1).join(' ')}` : getSmartSortKey(tClean, entity);
if (targetSortName && targetSortName.toLowerCase() !== currentTitle.toLowerCase()) {
let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle;
let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name";
if (targetSortName.includes(',')) {
const p = targetSortName.split(',');
rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`;
}
await ComradeLib.checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name");
}
// Name lists
await processHumanNameLists(api, entity, wikitext, currentTitle, hasShortDesc, isOrphan, tClean);
}
// Quality/ORES
ComradeLib.checkQualityConsistency();
} catch (err) {
console.error("Comrade error:", err);
}
}
init();
})();
(function() {
if (window.Headmaster) return;
const VERSION = "0.9.2.1";
const defaultNS = [0, 118];
const userNS = window.headmaster_ns || [];
const config = {
menu: window.headmaster_menu || 'p-cactions',
enableSummary: window.headmaster_do_summary !== false,
customSummary: window.headmaster_summary || "",
allowedNamespaces: Array.from(new Set([...defaultNS, ...userNS])),
autoScan: window.headmaster_auto_scan !== false,
autoRun: window.headmaster_auto_run === false
};
const Headmaster = {
VERSION: VERSION,
DefaultSummary: "Converting [[MOS:PSEUDOHEAD|pseudo-headings]] {details}using [[User:Sam_Sailor/Scripts/Headmaster.js|Headmaster]]. Semicolon markup is reserved for [[MOS:DLIST|description lists]].",
styleInjected: false,
css: `
#hm-panel { clear: both; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; margin-bottom: 1.5em; border-radius: 2px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
#hm-table { width: 100%; border-collapse: collapse; background: white; margin-top: 10px; }
#hm-table th { background: #f2f2f2; border: 1px solid #a2a9b1; padding: 8px; text-align: left; }
#hm-table td { border: 1px solid #a2a9b1; padding: 8px; vertical-align: middle; }
.hm-context { font-family: 'Courier New', monospace; font-size: 0.9em; color: #54595d; background: #fdfdfd; }
#hm-controls { display: flex; justify-content: space-between; align-items: center; margin-top: 15px; }
#hm-controls button { margin-left: 10px; padding: 6px 16px; cursor: pointer; border-radius: 2px; }
.hm-btn-apply { background: #36c; color: white; border: 1px solid #36c; font-weight: bold; }
.hm-btn-apply:hover { background: #447ff5; }
.hm-global-selector { font-size: 0.9em; color: #202122; background: #eaecf0; padding: 8px; border-radius: 2px; border: 1px solid #a2a9b1; }
.hm-global-selector a { font-weight: bold; text-decoration: none; color: #36c; }
.hm-global-selector a:hover { text-decoration: underline; }
.hm-locate-btn { cursor: pointer; color: #36c; border: 1px solid #a2a9b1; background: #f8f9fa; padding: 2px 6px; font-size: 0.85em; border-radius: 2px; }
.hm-locate-btn:hover { background: #fff; border-color: #36c; }
`,
setup() {
if (!config.allowedNamespaces.includes(mw.config.get("wgNamespaceNumber"))) return;
const action = mw.config.get("wgAction");
this.addPortlet(action);
if (action === 'view' && (config.autoScan || config.autoRun)) {
this.silentScan();
}
if (mw.util.getParamValue('headmaster') === '1' && ['edit', 'submit'].includes(action)) {
const params = new URLSearchParams(window.location.search);
params.delete('headmaster');
const searchString = params.toString();
const newUrl = window.location.pathname + (searchString ? '?' + searchString : '');
window.history.replaceState(null, '', newUrl);
this.init();
}
},
addPortlet(action) {
const link = mw.util.addPortletLink(config.menu, '#', 'Headmaster', 'ca-hm', 'Interactively convert pseudo-headings');
if (link) {
$(link).on('click', (e) => {
e.preventDefault();
if (action === 'view') {
this.redirectToEdit();
} else if (['edit', 'submit'].includes(action)) {
this.init();
}
});
}
},
redirectToEdit() {
window.location.href = mw.util.getUrl(mw.config.get("wgPageName"), {
action: 'edit',
headmaster: '1'
});
},
silentScan() {
new mw.Api().get({
action: 'query',
prop: 'revisions',
titles: mw.config.get('wgPageName'),
rvprop: 'content',
rvlimit: 1,
formatversion: 2
}).done((data) => {
const page = data.query.pages[0];
if (!page || !page.revisions) return;
const wikitext = page.revisions[0].content;
const tasks = this.analyzeText(wikitext);
if (tasks.length > 0) {
if (config.autoRun) {
this.redirectToEdit();
} else {
this.notifyDiscovery(tasks.length);
}
}
}).fail((err) => {
mw.log.warn("Headmaster: silentScan failed", err);
});
},
notifyDiscovery(count) {
const $msg = $('<span>').text(`Headmaster: Found ${count} issues. `).append($('<a>').text('Run conversion').css({
fontWeight: 'bold',
cursor: 'pointer'
}).on('click', () => this.redirectToEdit()));
mw.notify($msg, {
tag: 'hm-discovery',
autoHide: false
});
},
init() {
if (!this.styleInjected) {
mw.loader.addStyleTag(this.css);
this.styleInjected = true;
}
const $textbox = $('#wpTextbox1');
const wikitext = $textbox.textSelection('getContents');
if (!wikitext) {
mw.notify("Textbox is empty or not found.", {
type: 'error'
});
return;
}
const tasks = this.analyzeText(wikitext);
if (tasks.length === 0) {
mw.notify("No issues found.");
return;
}
this.showUI(tasks, wikitext);
},
analyzeText(wikitext) {
const lines = wikitext.split("\n");
const tasks = [];
let currentDepth = 1;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
const headingMatch = line.match(/^(={2,6})\s*(.+?)\s*\1\s*$/);
if (headingMatch) {
currentDepth = headingMatch[1].length;
const prevLine = i > 0 ? lines[i - 1].trim() : null;
const isSmashed = prevLine !== null && prevLine !== "";
const isExtraSpace = i > 1 && lines[i - 1].trim() === "" && lines[i - 2].trim() === "";
if (isSmashed || isExtraSpace) {
tasks.push({
index: i,
type: 'WHITESPACE',
original: lines[i],
isSmashed: isSmashed,
isExtraSpace: isExtraSpace
});
}
} else if (line.startsWith(';') && !line.startsWith(';;')) {
const nextLine = lines[i + 1] || "";
if (!nextLine.trim().startsWith(':')) {
const cleanText = line.substring(1).replace(/\s*:.*$/, "").trim();
const suggestedLevel = "=".repeat(Math.min(currentDepth + 1, 6));
tasks.push({
index: i,
type: 'PSEUDO',
original: line,
cleanText: cleanText,
context: nextLine.substring(0, 60).trim() + (nextLine.length > 60 ? "..." : ""),
suggestedLevel: suggestedLevel
});
}
}
}
return tasks;
},
showUI(tasks, originalWikitext) {
$('#hm-panel').remove();
$('#editform').hide();
const visibleTasks = tasks.filter(t => t.type === 'PSEUDO');
const pseudoCount = visibleTasks.length;
const whitespaceCount = tasks.filter(t => t.type === 'WHITESPACE').length;
let statusMsg = "";
const s = (n) => n !== 1 ? 's' : '';
if (pseudoCount > 0) {
statusMsg = `Found <strong>${pseudoCount}</strong> possible pseudo-heading${s(pseudoCount)}.`;
if (whitespaceCount > 0) {
statusMsg += ` Also fixing <strong>${whitespaceCount}</strong> spacing issue${s(whitespaceCount)} quietly.`;
}
} else {
statusMsg = `Did not find any pseudo-headings, but fixing <strong>${whitespaceCount}</strong> spacing issue${s(whitespaceCount)} quietly.`;
}
const tableRows = pseudoCount > 0 ? visibleTasks.map((task, i) => `
<tr data-task-index="${task.index}">
<td><button class="hm-locate-btn" data-hm-i="${tasks.indexOf(task)}" title="Show in editor">Locate</button></td>
<td class="hm-context"><code>${mw.html.escape(task.original)}</code></td>
<td><label><input type="radio" name="hm-opt-${i}" value="keep"> Keep</label></td>
<td><label><input type="radio" name="hm-opt-${i}" value="bold" checked> <b>Bold</b></label></td>
<td><label><input type="radio" name="hm-opt-${i}" value="head"> Heading (${task.suggestedLevel.length})</label></td>
<td class="hm-context">${mw.html.escape(task.context || "(end of page)")}</td>
</tr>
`).join("") : `<tr><td colspan="6" style="text-align:center; padding: 2em; color: #54595d; background: #f8f9fa;">
No interactive heading changes detected. Only background spacing normalization will be applied.
</td></tr>`;
const $panel = $(`
<div id="hm-panel">
<h3 style="margin-top:0">Headmaster v${this.VERSION}: Review pseudo-headings</h3>
<div class="hm-global-selector" ${pseudoCount === 0 ? 'style="display:none"' : ''}>
<strong>Bulk action:</strong>
<a href="#" id="hm-all-keep">Set all to Keep</a> |
<a href="#" id="hm-all-bold">Set all to Bold</a> |
<a href="#" id="hm-all-head">Set all to Heading</a>
</div>
<table id="hm-table">
<thead>
<tr>
<th>Find</th>
<th>Source</th>
<th colspan="3">Action</th>
<th>Next line context</th>
</tr>
</thead>
<tbody>${tableRows}</tbody>
</table>
<div id="hm-controls">
<span>${statusMsg}</span>
<div>
<button id="hm-cancel" class="mw-ui-button">Cancel</button>
<button id="hm-apply" class="hm-btn-apply">Show changes (diff)</button>
</div>
</div>
</div>
`);
$('#content').prepend($panel);
window.scrollTo(0, 0);
$('.hm-locate-btn').on('click', function() {
$(document).off('mousedown.hmlocate');
const idx = $(this).data('hm-i');
const task = tasks[idx];
const lines = originalWikitext.split('\n');
let charOffset = 0;
for (let j = 0; j < task.index; j++) {
charOffset += lines[j].length + 1;
}
$('#hm-panel').hide();
$('#editform').show();
const $textbox = $('#wpTextbox1');
$textbox.textSelection('setSelection', {
start: charOffset,
end: charOffset + lines[task.index].length
});
$textbox.textSelection('scrollToCaretPosition');
const locateNotify = mw.notify("Locating line " + (task.index + 1) + ". Click outside the editor to return.", {
tag: 'hm-locate',
type: 'success',
autoHide: false
});
setTimeout(() => {
$(document).on('mousedown.hmlocate', function(e) {
if (!$(e.target).closest('#editform, .mw-notification-area').length) {
$(document).off('mousedown.hmlocate');
Promise.resolve(locateNotify).then(n => n.close());
$('#editform').hide();
$('#hm-panel').show();
}
});
}, 200);
});
$('#hm-all-keep').on('click', (e) => {
e.preventDefault();
$('input[value="keep"]').prop('checked', true);
});
$('#hm-all-bold').on('click', (e) => {
e.preventDefault();
$('input[value="bold"]').prop('checked', true);
});
$('#hm-all-head').on('click', (e) => {
e.preventDefault();
$('input[value="head"]').prop('checked', true);
});
$('#hm-cancel').on('click', () => {
$('#hm-panel').remove();
$('#editform').show();
});
$('#hm-apply').on('click', () => this.applyChanges(tasks, originalWikitext));
},
getProcessedSummary(existingSummary, bolds, heads, spaces) {
if (!config.enableSummary) return existingSummary;
let mySummary = "";
let isDefault = false;
if (config.customSummary) {
mySummary = config.customSummary;
} else {
isDefault = true;
if (bolds > 0 || heads > 0) {
const detailsArr = [];
if (bolds > 0) detailsArr.push(`${bolds} to bold`);
if (heads > 0) detailsArr.push(`${heads} to heading`);
const detailsStr = detailsArr.length > 0 ? `(${detailsArr.join(', ')}) ` : "";
mySummary = this.DefaultSummary.replace("{details}", detailsStr);
if (spaces > 0) {
mySummary += " Also handled [[MOS:BLANKLINE]].";
}
} else if (spaces > 0) {
mySummary = "Handling [[MOS:BLANKLINE]] using [[User:Sam_Sailor/Scripts/Headmaster.js|Headmaster]]";
}
}
if (!mySummary) return existingSummary;
const trimmedOld = existingSummary.trim();
if (!trimmedOld) return mySummary;
if (/[.!?]$/.test(trimmedOld)) {
return trimmedOld + " " + mySummary;
} else {
const finalSummary = isDefault ? mySummary.charAt(0).toLowerCase() + mySummary.slice(1) : mySummary;
return trimmedOld + "; " + finalSummary;
}
},
applyChanges(tasks, wikitext) {
try {
let lines = wikitext.split("\n");
let bCount = 0,
hCount = 0,
wCount = 0;
const pseudoTasks = tasks.filter(t => t.type === 'PSEUDO');
for (let i = pseudoTasks.length - 1; i >= 0; i--) {
const task = pseudoTasks[i];
const idx = task.index;
const action = $('#hm-table tbody tr').filter(`[data-task-index="${idx}"]`).find('input:checked').val();
if (action === 'bold') {
let newValue = `'''${task.cleanText}'''`;
if (idx > 0 && lines[idx - 1].trim() !== "") {
newValue = "\n" + newValue;
}
lines[idx] = newValue;
bCount++;
} else if (action === 'head') {
const h = task.suggestedLevel;
let newValue = `${h} ${task.cleanText} ${h}`;
if (idx > 0 && lines[idx - 1].trim() !== "") {
newValue = "\n" + newValue;
}
lines[idx] = newValue;
hCount++;
}
}
const spaceTasks = tasks.filter(t => t.type === 'WHITESPACE');
for (let i = spaceTasks.length - 1; i >= 0; i--) {
const task = spaceTasks[i];
const idx = task.index;
if (task.isSmashed) {
lines.splice(idx, 0, "");
} else if (task.isExtraSpace) {
let backIdx = idx - 1;
while (backIdx > 0 && lines[backIdx].trim() === "" && lines[backIdx - 1].trim() === "") {
lines.splice(backIdx, 1);
backIdx--;
}
}
wCount++;
}
if (bCount + hCount + wCount === 0) {
mw.notify("No changes selected.");
$('#hm-panel').remove();
$('#editform').show();
return;
}
$('#wpTextbox1').textSelection('setContents', lines.join("\n"));
const $summary = $('#wpSummary');
$summary.val(this.getProcessedSummary($summary.val(), bCount, hCount, wCount));
$('#hm-panel').remove();
$('#editform').show();
mw.notify(`Headmaster: Fixed ${wCount} spacing issue(s) and converted ${bCount + hCount} heading(s).`);
$('html, body').animate({
scrollTop: 0
}, 'fast');
$('#wpDiff').click();
} catch (err) {
$('#editform').show();
mw.notify("Headmaster error: " + err.message, {
type: 'error'
});
}
}
};
window.Headmaster = Headmaster;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => Headmaster.setup());
});
})();
//</nowiki>
40y1a2htuafz7hfav5bmr6q6wvja6n5
Category:Test2
14
98400
739912
435716
2026-05-01T06:42:14Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: добавление шаблона номинации на удаление
739912
wikitext
text/x-wiki
<noinclude>{{Обсуждаемая категория|2026-05-01}}</noinclude>
{Category redirect|Test}
[[বিষয়শ্রেণী:মৃদু পুনর্নির্দেশিত বিষয়শ্রেণী পরীক্ষণ প্রয়োজন]]
prsrib6jhjd042tad5041x33drmlto1
739915
739912
2026-05-01T06:45:37Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: добавление шаблона номинации на удаление
739915
wikitext
text/x-wiki
<noinclude>{{Обсуждаемая категория|2026-05-01}}
{{Обсуждаемая категория|2026-05-01}}
</noinclude>
{Category redirect|Test}
[[বিষয়শ্রেণী:মৃদু পুনর্নির্দেশিত বিষয়শ্রেণী পরীক্ষণ প্রয়োজন]]
6zhiotnt2x9qpf3cn24600gmandh43l
739918
739915
2026-05-01T06:48:07Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: добавление шаблона номинации на удаление
739918
wikitext
text/x-wiki
<noinclude>{{Обсуждаемая категория|2026-05-01}}
{{Обсуждаемая категория|2026-05-01}}
{{Обсуждаемая категория|2026-05-01}}
</noinclude>
{Category redirect|Test}
[[বিষয়শ্রেণী:মৃদু পুনর্নির্দেশিত বিষয়শ্রেণী পরীক্ষণ প্রয়োজন]]
l4f0efiec85xxjhau8utjv3qx99agtd
739924
739918
2026-05-01T07:09:30Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: обсуждение категории завершено
739924
wikitext
text/x-wiki
<noinclude>
{{Обсуждаемая категория|2026-05-01}}
{{Обсуждаемая категория|2026-05-01}}
</noinclude>
{Category redirect|Test}
[[বিষয়শ্রেণী:মৃদু পুনর্নির্দেশিত বিষয়শ্রেণী পরীক্ষণ প্রয়োজন]]
0j4tqz1bzabiuk0qxc8n275w9ejxp5n
739926
739924
2026-05-01T07:09:37Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: обсуждение категории завершено
739926
wikitext
text/x-wiki
<noinclude>
{{Обсуждаемая категория|2026-05-01}}
</noinclude>
{Category redirect|Test}
[[বিষয়শ্রেণী:মৃদু পুনর্নির্দেশিত বিষয়শ্রেণী পরীক্ষণ প্রয়োজন]]
hcgjj2dlje4d100njhiplyqh9j6054d
739927
739926
2026-05-01T07:09:49Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: обсуждение категории завершено
739927
wikitext
text/x-wiki
{Category redirect|Test}
[[বিষয়শ্রেণী:মৃদু পুনর্নির্দেশিত বিষয়শ্রেণী পরীক্ষণ প্রয়োজন]]
dfg5ekvpbfq3iuct1afuq9u6fjh742u
739929
739927
2026-05-01T07:10:00Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: добавление шаблона номинации на удаление
739929
wikitext
text/x-wiki
<noinclude>{{Обсуждаемая категория|2026-05-01}}</noinclude>
{Category redirect|Test}
[[বিষয়শ্রেণী:মৃদু পুনর্নির্দেশিত বিষয়শ্রেণী পরীক্ষণ প্রয়োজন]]
prsrib6jhjd042tad5041x33drmlto1
Wikipedia:Sandbox
4
107092
739902
739798
2026-05-01T01:18:50Z
Cewbot
33876
Sandbox test edit (section) mmm
739902
wikitext
text/x-wiki
<noinclude>{{Sandbox}}</noinclude>
== Please start your testing below this line ==
== Sandbox test section ==
...
3hs0e8gbiq1xjrjx6lrxd1ct539lkvb
739903
739902
2026-05-01T01:18:55Z
Cewbot
33876
Sandbox test edit
739903
wikitext
text/x-wiki
..
iegvvlibf0prco1vgdvofg9se788l1c
739907
739903
2026-05-01T02:25:15Z
Cewbot
33876
rollback test
739758
wikitext
text/x-wiki
<noinclude>{{Sandbox}}</noinclude>
== Please start your testing below this line ==
* {{if empty}}{{if empty|1}}{{if empty||2}}
* 12
* 12
k7muqa39v0dtamk9hg86cjrht1l8j12
User:MrJaroslavik/userinfo.js
2
108875
739931
736163
2026-05-01T08:36:46Z
MrJaroslavik
44012
test
739931
javascript
text/javascript
/**
* UserStatus v2.0
* Modernized version with Former Groups tracking (local & global)
* Features: 48h cache, Date intervals, Duration calculation, Purge button.
*/
(function ($, mw) {
'use strict';
if ([2, 3].indexOf(mw.config.get('wgNamespaceNumber')) === -1 || mw.config.get('wgTitle').indexOf('/') !== -1) return;
const config = {
user: mw.config.get('wgTitle').replace(/ /g, '_'),
db: mw.config.get('wgDBname'),
lang: mw.config.get('wgUserLanguage'),
cacheHours: 48,
expiryGroups: ['*', 'user', 'autoconfirmed'] // Implicit groups to hide
};
const i18n = {
cs: {
edits: 'Editace: ',
reg: 'Registrace: ',
local: 'Lokální skupiny: ',
global: 'Globální skupiny: ',
formerL: 'Bývalé lokální skupiny: ',
formerG: 'Bývalé globální skupiny: ',
last: 'Poslední edit: ',
blocks: 'Stav blokování: ',
never: 'nikdy',
ago: 'před $1',
days: 'dní',
present: 'současnost'
},
en: {
edits: 'Edits: ',
reg: 'Registration: ',
local: 'Local groups: ',
global: 'Global groups: ',
formerL: 'Former local groups: ',
formerG: 'Former global groups: ',
last: 'Last edit: ',
blocks: 'Block status: ',
never: 'never',
ago: '$1 ago',
days: 'days',
present: 'present'
}
};
const msg = i18n[config.lang] || i18n.en;
const api = new mw.Api();
const metaApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php');
const utils = {
formatDate: (ts) => ts ? new Date(ts).toISOString().split('T')[0] : '?',
getDiffDays: (start, end) => {
const s = new Date(start), e = end === 'present' ? new Date() : new Date(end);
const diff = Math.floor((e - s) / (1000 * 60 * 60 * 24));
return diff + ' ' + msg.days;
},
saveCache: (data) => {
const cacheObj = { timestamp: Date.now(), data: data };
localStorage.setItem('us_cache_' + config.user, JSON.stringify(cacheObj));
},
getCache: () => {
const cached = localStorage.getItem('us_cache_' + config.user);
if (!cached) return null;
const parsed = JSON.parse(cached);
if (Date.now() - parsed.timestamp > config.cacheHours * 60 * 60 * 1000) return null;
return parsed.data;
}
};
async function fetchFormerGroups(isGlobal = false) {
const client = isGlobal ? metaApi : api;
const type = isGlobal ? 'gblrights' : 'rights';
const target = isGlobal ? `User:${config.user}@global` : `User:${config.user}`;
let former = {};
try {
const res = await client.get({
action: 'query',
list: 'logevents',
letype: type,
letitle: target,
lelimit: 500
});
const logs = res.query.logevents;
// Map logic: find when groups were removed and track back to when added
logs.forEach(log => {
const p = log.params;
const date = log.timestamp;
const added = p.add || p.newGroups || [];
const removed = p.remove || p.oldGroups || [];
removed.forEach(g => {
if (config.expiryGroups.indexOf(g) !== -1) return;
if (!former[g]) former[g] = { to: date, from: null };
});
added.forEach(g => {
if (former[g] && !former[g].from) former[g].from = date;
});
});
} catch (e) { console.error("Rights log fail", e); }
return former;
}
function renderBox(data) {
$('#us_box').remove();
const $box = $('<div>', { id: 'us_box', style: 'border-bottom:1px solid #aaa; padding:10px; font-size:0.9em; line-height:1.5em;' });
const $ul = $('<ul>', { style: 'list-style:none; margin:0; padding:0;' });
const addRow = (label, content, style = '') => {
if (!content) return;
$ul.append($('<li>').append($('<b>').text(label), $('<span>').css('cssText', style).append(content)));
};
const addFormerRow = (label, formerData) => {
const entries = Object.keys(formerData).map(g => {
const item = formerData[g];
const dur = item.from ? ` (${utils.formatDate(item.from)} – ${utils.formatDate(item.to)}, ${utils.getDiffDays(item.from, item.to)})` : ` (ends ${utils.formatDate(item.to)})`;
return g + dur;
});
if (entries.length) {
$ul.append($('<li>', { style: 'margin-left:15px; color:#72777d; font-style:italic; font-size:0.9em;' })
.append($('<b>').text(label), entries.join(', ')));
}
};
// UI Building
addRow(msg.edits, data.editcount);
addRow(msg.reg, `${utils.formatDate(data.registration)} (${utils.getDiffDays(data.registration, 'present')} ${msg.ago})`);
addRow(msg.local, data.groups.filter(g => config.expiryGroups.indexOf(g) === -1).join(', '));
addFormerRow(msg.formerL, data.formerLocal);
addRow(msg.global, (data.globalGroups || []).join(', '));
addFormerRow(msg.formerG, data.formerGlobal);
addRow(msg.last, data.lastEdit ? `${utils.getDiffDays(data.lastEdit, 'present')} ${msg.ago} (${utils.formatDate(data.lastEdit)})` : msg.never);
const blockText = data.block ? `BLOCKED (${data.block.reason})` : 'not blocked';
addRow(msg.blocks, blockText, data.block ? 'color:#c00; font-weight:bold;' : 'color:#080;');
// Purge button
$box.append($('<a>', {
style: 'float:right; font-size:0.8em; color:#999; cursor:pointer;',
text: '[purge cache]',
click: () => {
localStorage.removeItem('us_cache_' + config.user);
location.reload();
}
}));
$box.append($ul);
$('#firstHeading').after($box);
}
async function init() {
let data = utils.getCache();
if (data) {
renderBox(data);
return;
}
// Fetch fresh data
const userRes = await api.get({
action: 'query',
list: 'users',
ususers: config.user,
usprop: 'editcount|groups|registration|blockinfo'
});
const contribRes = await api.get({
action: 'query',
list: 'usercontribs',
ucuser: config.user,
uclimit: 1,
ucprop: 'timestamp'
});
const globalRes = await api.get({
action: 'query',
meta: 'globaluserinfo',
guiuser: config.user,
guiprop: 'groups'
});
const formerL = await fetchFormerGroups(false);
const formerG = await fetchFormerGroups(true);
const u = userRes.query.users[0];
data = {
editcount: u.editcount,
registration: u.registration,
groups: u.groups || [],
block: u.blockreason ? { reason: u.blockreason, expiry: u.blockexpiry } : null,
lastEdit: contribRes.query.usercontribs[0] ? contribRes.query.usercontribs[0].timestamp : null,
globalGroups: globalRes.query.globaluserinfo.groups || [],
formerLocal: formerL,
formerGlobal: formerG
};
utils.saveCache(data);
renderBox(data);
}
init();
})(jQuery, mediaWiki);
pdfhzhkkb3g6xybnyf2lnm136hm4c0r
739932
739931
2026-05-01T08:46:49Z
MrJaroslavik
44012
test
739932
javascript
text/javascript
/**
* @Description: Modified Userstatus.js with Former Groups (Local & Global)
* Original by Steef389, Perhelion.
**/
(function ($, mw) {
'use strict';
var project = window.project || mw.config.get('wgDBname');
var msg, i18n = {
en: {
noReason: 'reason removed',
blockCmt: 'no block comment',
never: 'never',
not: ' not ',
block: 'blocked',
and: '$1 and$2',
count: 'Count',
noedit: 'None (only deleted contributions?)',
nodb: '‹not in database›',
noGrp: 'no extended group',
dates: ['years', 'months', 'days', 'hours', 'minutes', 'seconds'],
date: ['year', 'month', 'day', 'hour', 'minute', 'second'],
nosec: 'less than one ',
curtimeDiff: ' Locale time difference to server time in s: ',
thxGn: 'given',
thxRd: 'received',
nova: 'new',
ago: '$1 ago',
blocklog: 'Block log',
contrib: 'Edits: ',
usub: 'Subpages⬇',
thxGvng: 'Thanksgivings: ',
reviews: 'Active reviews: ',
regdate: 'Registration: ',
laedit: 'Last edited: ',
lala: 'Last log activity',
fiedit: 'First edit: ',
blocks: 'Block-status: ',
loGrp: 'Local user-groups: ',
glGrp: 'Global user-groups: ',
formerLoGrp: 'Former local user-groups: ',
formerGlGrp: 'Former global user-groups: ',
blockEnd: 'Block-end: ',
blocker: 'Blocker: ',
blockReason: 'Block-reason: '
},
cs: {
noReason: 'důvod odstraněn',
blockCmt: 'bez komentáře',
never: 'nikdy',
not: ' ne ',
block: 'zablokován',
and: '$1 a$2',
count: 'Počet',
noedit: 'Žádné (jen smazané?)',
nodb: '‹není v databázi›',
noGrp: 'žádná rozšířená skupina',
dates: ['let', 'měsíců', 'dní', 'hodin', 'minut', 'sekund'],
date: ['rok', 'měsíc', 'den', 'hodina', 'minuta', 'sekunda'],
nosec: 'méně než ',
curtimeDiff: ' Rozdíl lokálního času k serveru v s: ',
thxGn: 'poděkoval',
thxRd: 'obdržel poděkování',
nova: 'nové',
ago: 'před $1',
blocklog: 'Kniha zablokování',
contrib: 'Editace: ',
usub: 'Podstránky⬇',
thxGvng: 'Poděkování: ',
reviews: 'Aktivní prověření: ',
regdate: 'Registrace: ',
laedit: 'Poslední editace: ',
lala: 'Poslední aktivita v protokolech',
fiedit: 'První editace: ',
blocks: 'Stav bloku: ',
loGrp: 'Lokální skupiny: ',
glGrp: 'Globální skupiny: ',
formerLoGrp: 'Bývalé lokální skupiny: ',
formerGlGrp: 'Bývalé globální skupiny: ',
blockEnd: 'Konec bloku: ',
blocker: 'Zablokoval: ',
blockReason: 'Důvod bloku: '
}
};
var data;
var us = mw.libs.userstatus = {
name: 'Userstatus',
version: 1.80, // Bumping version for former groups update
lastEditSeconds: false,
styleMissingData: 'color:#999;font-size:90%;',
styleLoading: 'font-style:italic;',
styleBlocked: 'color:#c00',
styleNotBlocked: 'color:#080',
styleFormer: 'margin-left:15px; color:#72777d; font-style:italic; font-size:0.9em;',
viewPatrolNumber: false,
lang: mw.config.get('wgUserLanguage'),
user: mw.config.get('wgTitle'),
cookie: [],
thanks: 0,
patrols: 0,
actions: [],
dbVersion: 0,
implicitGroups: ['*', 'user', 'autoconfirmed'],
getLocalNames: function (groups, specialpage) {
if (groups) {
var arr = [];
specialpage = specialpage || 'ListGroupRights';
for (var i = 0; i < groups.length; ++i) {
var g = groups[i];
var n = us.groupNames[g];
if (n) arr.push('<a href="/wiki/Special:' + specialpage + '#' + g + '">' + n + '</a>');
else arr.push(g);
}
groups = arr.join(', ').replace(/(.*),([^,]*)$/, msg.and);
}
return groups;
},
writeGroups: function (groups) {
groups = us.getLocalNames(groups);
if (!groups) {
groups = $('<span>', {
style: us.styleMissingData + us.styleLoading,
text: msg.noGrp
});
}
us.loading_groups.replaceWith(groups);
// After local groups are written, fetch former local groups
us.fetchFormerRights(false);
},
getGroupNames: function (aw) {
if (!aw || !aw.query) return;
aw = aw.query.allmessages;
var al = aw.length;
var groups = {};
while (al--) {
var an = aw[al];
var name = an.name;
if (/^group-/.test(name)) {
name = RegExp.rightContext || name.substring(6);
groups[name] = an['*'];
}
}
us.groupNames = groups;
us.init();
},
ajaxRequest: function (params, onSuccess, trial) {
var url = us.api;
if (!(params instanceof Object)) {
url += params + '&maxage=2419200&smaxage=2419200';
params = {};
} else {
params.maxlag = 3;
params.maxage = 2419200;
params.smaxage = 2419200;
if (trial) params.maxlag *= trial;
}
var api = $.getJSON(url, params, onSuccess)
.fail(function (jqXHR, status) {
var note = 'Timeout fail, maybe try again!';
if (trial) {
if (!status.textStatus || (status.textStatus !== 'timeout' && jqXHR !== 'maxlag')) note = $('<span>Fehler bitte <a href="' + mw.util.getUrl('User_talk:Perhelion') + '">Perhelion</a> melden.</span>');
mw.notify(note, { title: us.name + ':', type: 'error' });
} else if (status.textStatus === 'timeout' || jqXHR === 'maxlag') {
api.abort();
return us.ajaxRequest(params, onSuccess, 2);
}
});
},
// --- NEW FORMER RIGHTS LOGIC ---
fetchFormerRights: function(isGlobal) {
var apiTarget = isGlobal ? 'https://meta.wikimedia.org/w/api.php' : mw.util.wikiScript('api');
var logType = isGlobal ? 'gblrights' : 'rights';
var userTarget = isGlobal ? 'User:' + us.user + '@global' : 'User:' + us.user;
var containerId = isGlobal ? 'us_former_global_groups' : 'us_former_local_groups';
var params = {
action: 'query',
list: 'logevents',
letype: logType,
letitle: userTarget,
lelimit: 500,
format: 'json',
origin: isGlobal ? '*' : undefined
};
$.getJSON(apiTarget, params, function(res) {
if(!res.query || !res.query.logevents) return;
var logs = res.query.logevents;
var former = {};
logs.forEach(function(log) {
var p = log.params;
var date = log.timestamp;
var added = p.add || p.newGroups || [];
var removed = p.remove || p.oldGroups || [];
removed.forEach(function(g) {
if (us.implicitGroups.indexOf(g) !== -1) return;
if (!former[g]) former[g] = { to: date, from: null };
});
added.forEach(function(g) {
if (former[g] && !former[g].from) former[g].from = date;
});
});
var entries = [];
Object.keys(former).forEach(function(g) {
var item = former[g];
var dateTo = new Date(item.to);
var dateFrom = item.from ? new Date(item.from) : null;
var duration = "";
if(dateFrom) {
var diff = Math.floor((dateTo - dateFrom) / (1000 * 60 * 60 * 24));
duration = ' (' + dateFrom.toISOString().split('T')[0] + ' – ' + dateTo.toISOString().split('T')[0] + ', ' + diff + ' ' + msg.date[2] + 's)';
} else {
duration = ' (ends ' + dateTo.toISOString().split('T')[0] + ')';
}
entries.push(g + duration);
});
if(entries.length > 0) {
$('#' + containerId).show().find('.us_former_content').text(entries.join(', '));
}
});
},
getThanks: function (e) {
var params = '&list=logevents&continue=-%7C%7C&leprop=&letype=thanks&ledir=newer&lelimit=max';
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
us.e.attr('title', us.e.text());
us.e.text('');
us.e.injectSpinner('thx');
us.thanks = 0;
} else if (e.lecontinue) {
params += '&lecontinue=' + e.lecontinue;
}
if (us.e.attr('title') === msg.thxGn) params += '&leuser=' + us.user;
else params += '&letitle=User:' + us.user;
}
us.actions.push({ params: params, func: us.writeThanks });
us.doRequest();
},
writeThanks: function (uq) {
if (!uq.query || !uq.query.logevents) return mw.log.warn(uq);
var e = us.e;
var user = 'page=';
var aw = uq.query.logevents;
us.thanks += aw.length;
if (uq.continue) return us.getThanks(uq.continue);
if (e[0].title === msg.thxGn) user += '&user=';
user += us.user;
$.removeSpinner('thx');
e.text(us.thanks);
e.off('click');
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special%3ALog&type=thanks&' + user + '&year=&month=-1&tagfilter=&hide_thanks_log=0';
delete us.e;
},
getUploads: function (e) {
var params = '&list=logevents&letype=upload&ledir=newer&lelimit=max&leuser=' + us.user;
function _start(u) {
var text = u.text();
u.prop('title', text);
u.text('');
u.injectSpinner('upload' + text);
u.data('upl', 0);
u.data('del', 0);
}
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
if (us.e.text() !== msg.nova) {
us.e2 = us.e.nextAll('a').eq(0);
if (us.e2.text() === msg.nova) _start(us.e2); else delete us.e2;
}
_start(us.e);
} else if (e.lecontinue) { params += '&lecontinue=' + e.lecontinue; }
params += '&leprop=ids';
if (us.e.prop('title') === msg.nova) params += '&leaction=upload/upload';
else if (us.e2) params += '|type';
}
us.actions.push({ params: params, func: us.writeUploads });
us.doRequest();
},
writeUploads: function (uq) {
if (!uq.query || !uq.query.logevents || !us.e) return mw.log(uq, us.e);
function _insert(e) {
$.removeSpinner('upload' + e.prop('title'));
e.text('');
e.off('click');
e.append(e.data('del'), $('<span>', { style: us.styleMissingData + us.styleLoading, text: ' (+' + (e.data('upl') - e.data('del')) + ' del.)' }));
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special:Log&type=upload&user=' + us.user + '&subtype=' + ((e[0].title === msg.nova) ? 'upload' : '');
}
var e = us.e;
var e2 = us.e2;
var aw = uq.query.logevents;
var alen = aw.length, i = 0;
var del = e.data('del');
if (e2) {
var upl = e2.data('upl');
var del2 = e2.data('del');
for (i; i < alen; ++i) {
var a = aw[i];
var c = a.pageid ? 1 : 0;
if (a.action === 'upload') { upl++; del2 += c; }
del += c;
}
e2.data('upl', upl);
e2.data('del', del2);
} else {
for (i; i < alen; ++i) if (aw[i].pageid) del++;
}
e.data('upl', e.data('upl') + alen);
e.data('del', del);
if (uq.continue) return us.getUploads(uq.continue);
_insert(e);
if (e2) { _insert(e2); delete us.e2; }
delete us.e;
},
writeCommonInfo: function (uq) {
var aw = uq.query;
if (!aw || !aw.users[0] || Object.prototype.hasOwnProperty.call(aw, 'missing') || Object.prototype.hasOwnProperty.call(aw.users[0], 'invalid')) return us.statusBox.remove();
aw = aw.users[0];
var edits = aw.editcount,
groups = data.groups || aw.groups,
blocked = data.blockreason || aw.blockexpiry,
gender = data.gender || aw.gender,
uploads = $('<a>', { title: msg.count, text: msg.nova, style: us.styleLoading, click: us.getUploads });
if (aw.registration) data.registration = aw.registration;
us.now = us.getDateFromTimestamp(uq.curtimestamp || us.now || mw.now());
us.user = us.user.replace(/ /g, '_');
us.first = 1;
if (edits) {
if (data.editcount && data.lastedit && data.editcount === edits) {
uq.query.usercontribs = [{ timestamp: data.lastedit }];
us.writeLastEdit(uq);
} else {
data.editcount = edits;
us.loading_last_edit.css('display', 'block');
us.actions.push({
params: '&list=usercontribs&ucuser=' + us.user + '&uclimit=1&uccontinue=' + (new Date(us.now).toISOString().replace(/[^\d]/g, '').slice(0, 14) + '|2'),
func: us.writeLastEdit
});
}
$('#t-contributions').remove();
} else { us.writeLastEdit({}); }
if (groups) {
edits = $('<a>', { title: 'Supercount', href: '//tools.wmflabs.org/supercount/index.php?user=' + us.user + '&project=' + location.hostname, text: edits });
if ($.inArray('bot', groups) === -1) {
us.review = $.inArray('editor', groups) !== -1;
if (us.review) {
if (typeof data.reviews === 'number') us.writePatrolCount(data.reviews);
} else if (us.viewPatrolNumber) { us.us_patrolcount_loading.parent().remove(); }
edits = [
edits, ' • ',
$('<a>', { href: '//xtools.wmflabs.org/ec' + mw.config.get('wgServer').substr(1) + '/' + us.user, title: 'XTools', text: 'XTools', style: us.styleLoading, target: '_blank' }), ' • ',
$('<a>', { href: '//commons.wikimedia.org/w/index.php?title=Commons:MyGallery/' + us.user + '&withJS=MediaWiki:JSONListUploads.js', title: 'MyGallery', text: (project === 'commonswiki' ? '' : 'c:') + 'MyGallery', style: us.styleLoading, target: '_blank' }), ' • ',
(project !== 'commonswiki' ? $('<a>', { href: '/wiki/Special:ListFiles/' + us.user, text: 'Upload', title: 'Special:Listfiles', style: us.styleLoading }) : 'Upload'),
(us.lang === 'de' ? '-' : ' ') + msg.count + ': ',
uploads.clone(1).text('total'), ' / ', uploads
];
if (!$('#t-subpages').length) {
edits.push($('<a>', { href: mw.util.getUrl('Special:Prefixindex/User:' + us.user + '/'), text: ' • ' + msg.usub, style: us.styleMissingData + us.styleLoading }));
}
} else if (us.us_patrolcount_loading) { us.us_patrolcount_loading.parent().remove(); }
us.loading_editcount.replaceWith(edits);
if (!aw.implicitgroups && !data.groups) {
aw.implicitgroups = ['*', 'user', 'autoconfirmed'];
if (groups.length < 4) aw.implicitgroups.pop();
}
data.groups = $.grep(groups, function (n) { return $.inArray(n, aw.implicitgroups) === -1; });
us.writeGroups(data.groups);
$('#t-userrights').remove();
}
if (!data.registration) {
us.actions.push({ params: '&list=logevents&leprop=timestamp|type&letype=newusers&lelimit=1&ledir=newer&leend=2005-12-30T00:00:00Z&leuser=' + us.user, func: us.writeRegistration });
if (data.firstedit) {
uq.query.usercontribs = [{ timestamp: data.firstedit }];
us.writeFirstEdit(uq);
} else if (edits) {
us.actions.push({ params: { list: 'usercontribs', ucuser: us.user, uclimit: 1, ucend: '2005-09-08T00:00:00Z', ucdir: 'newer', ucprop: 'timestamp' }, func: us.writeFirstEdit });
}
} else { us.writeRegistration(data.registration); }
us.loading_blocked.replaceWith($('<a>', { style: ((blocked) ? us.styleBlocked : us.styleNotBlocked), id: 'us_block_status_span', href: '/w/index.php?title=Special:Log/block&page=User:' + us.user, title: msg.blocklog, text: ((blocked) ? ' ' : msg.not) + msg.block }));
if (blocked) {
data.blockreason = aw.blockreason || ' ';
us.ul.append([
$('<li>', { id: 'us_block_time' }).append([$('<b>').text('• ' + msg.blockEnd), $.createSpinner('us_block_time_loading')]),
$('<li>', { id: 'us_block_reason' }).append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment, (aw.commenthidden))]),
$('<li>', { id: 'us_blocker' }).append([$('<b>').text('• ' + msg.blocker), $.createSpinner('us_blocker_loading')])
]);
us.actions.push({ params: { list: 'logevents', letitle: 'User:' + us.user, letype: 'block', lelimit: 1 }, func: us.writeBlockDetail });
}
if (data.glGrp && !data.locked) {
uq.query.globaluserinfo = { groups: (data.glGrp.length) ? data.glGrp : null };
us.writeGlobalGroup(uq);
} else {
us.actions.push({ params: '&meta=globaluserinfo&guiprop=groups&guiuser=' + us.user, func: us.writeGlobalGroup });
}
if (gender) {
data.gender = gender;
var genderSn = (gender === 'male') ? ' \u2642' : (gender === 'female' ? ' \u2640' : '');
$('#firstHeading').append($('<span>', { id: 'ps-gender-' + gender, style: 'font-size:80%', text: genderSn }));
}
if (us.actions.length) us.doRequest();
},
createBox: function () {
var statusBox = $('<div>', { style: 'border-bottom:1px solid #aaa;text-shadow:1px 1px 1px #eff;', id: 'us_box' });
if (mw.config.get('skin') === 'vector') statusBox.css('font-size', '0.8em');
var spanFrag = $.createSpinner();
us.loading_editcount = spanFrag.clone();
us.loading_registration = spanFrag.clone();
us.loading_groups = spanFrag.clone();
us.loading_blocked = spanFrag.clone();
us.us_global_group_loading = spanFrag.clone();
us.us_last_edit_loading = $.createSpinner('contribs');
us.loading_last_edit = $('<li>');
us.us_global_group = $('<li>', { style: 'display: none' });
var thx = $('<a>', { title: msg.count, text: msg.thxRd, style: us.styleLoading, click: us.getThanks });
var $userrights = $('#t-userrights a');
var $contributions = $('#t-contributions a');
if (!$userrights.length) $userrights = $('<a>', { href: '/wiki/Special:UserRights/' + us.user });
if (!$contributions.length) $contributions = $('<a>', { href: '/wiki/Special:Contributions/' + us.user });
var ul = $('<ul>', { style: 'list-style: none' }).append($('<li>', { id: 'us_editcount' }).append([$('<b>').append($contributions.text(msg.contrib)), us.loading_editcount]));
us.us_log_count = $('<span>');
if (us.viewPatrolNumber) {
us.us_patrolcount_loading = $('<a>', { style: us.styleLoading, click: us.getPatrolCount, text: msg.count });
us.us_log_count = $('<span>').append([$('<b>').text(msg.reviews), us.us_patrolcount_loading, ' • ']);
}
ul.append(us.us_log_count)
.append($('<li>', { id: 'us_reg_date' }).append([$('<b>').text(msg.regdate), us.loading_registration, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://tools.wmflabs.org/meta/userpages/' + us.user, text: '• User pages' })]))
.append($('<li>').append([$('<b>').append($userrights.text(msg.loGrp)), us.loading_groups, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://tools.wmflabs.org/meta/stalktoy/' + us.user, text: '• Stalk toy' })]))
// Container for former local groups
.append($('<li>', { id: 'us_former_local_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerLoGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.us_global_group.append([$('<b>').append($('<a>', { href: '/w/index.php?title=Special:GlobalUsers&limit=1&username=' + us.user, text: msg.glGrp })), us.us_global_group_loading, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://tools.wmflabs.org/meta/globalgroups/', text: '• GlobalGroups' })]))
// Container for former global groups
.append($('<li>', { id: 'us_former_global_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerGlGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.loading_last_edit.append([$('<b>').text(msg.laedit), us.us_last_edit_loading, ' ', $('<a>', { target: '_blank', style: us.styleMissingData, href: '/w/index.php?title=Special:Log/' + us.user + '&hide_thanks_log=0&hide_patrol_log=0&hide_tag_log=0', text: '• LLA' }), $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://tools.wmflabs.org/meta/crossactivity/' + us.user, text: '• CrossActivity' })]));
statusBox.append(ul);
us.ul = $('<ul>', { style: 'list-style:none' }).append($('<li>', { id: 'us_block_status' }).append([$('<b>').text(msg.blocks), us.loading_blocked, ' • ', $('<b>').text(msg.thxGvng), thx, ' / ', thx.clone(1).text(msg.thxGn), $('<a>', { style: 'float:right;' + us.styleMissingData, title: 'Purge data', text: 'purge', click: us.removeDataStore })]));
statusBox.append(us.ul);
$('#firstHeading').after(statusBox);
us.statusBox = statusBox;
},
removeDataStore: function (e) {
var key = project + us.user, db = window.indexedDB;
e.preventDefault();
if (!db || !us.dbVersion) return mw.cookie.set(us.name + us.user, null);
var request = db.open(us.name, us.dbVersion + 1);
request.onupgradeneeded = function () { db = this.result; if (db.objectStoreNames.contains(key)) db.deleteObjectStore(key); };
request.onsuccess = function () { db = this.result; us.dbVersion = db.version; db.close(); location.reload(); };
},
setCookie: function () {
var domain = (mw.config.get('wgNoticeProject') === 'wikipedia') ? 'wikipedia.org' : '';
var name = us.name + us.user;
var _saveCookie = function (k, d) { mw.cookie.set(name, JSON.stringify(d), { expires: 604800, domain: domain }); };
if (!us.actions[0] && JSON && data.editcount) {
if (us.cookie.length) window.clearTimeout(us.cookie.shift());
var saveData = window.indexedDB ? function (n, JSd, v) {
var key = project + us.user, db = window.indexedDB, request = db.open(us.name, v);
request.onupgradeneeded = function () {
db = this.result;
if (!db.objectStoreNames.contains(key)) {
var store = db.createObjectStore(key, { keyPath: 'name' });
store.createIndex('data', 'data', { unique: false });
}
};
request.onsuccess = function () {
db = this.result; us.dbVersion = db.version;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readwrite').objectStore(key);
store.put({ name: key, data: JSd });
} else { saveData(n, JSd, us.dbVersion + 1); }
};
request.onerror = function() { _saveCookie(n, JSd); };
} : _saveCookie;
us.cookie.push(setTimeout(function () {
data.timestamp = new Date().valueOf();
saveData('', data);
}, 500));
}
},
doRequest: function () {
var action = us.actions.shift();
if (action) us.ajaxRequest(action.params, action.func);
if (us.actions.length) window.setTimeout(function () { us.doRequest(); }, 100);
},
getDateDiff: function (now, date) {
var dStr = [];
var d = { years: 0, months: 0, days: 0, hours: 0, minutes: 0, seconds: 0 };
var diffDays = Math.floor(Math.abs(now - date) / 86400000);
if (now > date) {
var year = now.getFullYear();
var months = now.getMonth();
d.years = date.getFullYear();
date.setFullYear(year);
d.years = year - d.years;
if (date > now) {
d.years--;
date.setFullYear(year - 1);
}
var mDiff = months - date.getMonth();
if (mDiff < 0) mDiff += 12;
d.months = mDiff;
var s = Math.abs(now - date) / 1000;
d.seconds = Math.floor(s % 60);
s /= 60;
d.minutes = Math.floor(s % 60);
s /= 60;
d.hours = Math.floor(s % 24);
d.days = Math.floor(s / 24);
var units = [d.years, d.months, d.days];
for (var i = 0; i < 3; ++i) {
if (units[i]) dStr.push(units[i] + ' ' + ((units[i] > 1) ? msg.dates[i] : msg.date[i]));
}
}
var mainDiff = (dStr.length) ? dStr.join(', ').replace(/(.*),([^,]*)/, msg.and).replace(/(.*)/, msg.ago) : msg.nosec + msg.date[2];
return mainDiff + (diffDays > 0 ? ' = ' + diffDays + ' ' + msg.days : '');
},
getDateFromTimestamp: function (t) { if (!t) return false; t = new Date(t); return isNaN(t.valueOf()) ? false : t; },
writeRegistration: function (aw) {
if (aw) {
if (aw instanceof Object) {
if (aw.query && aw.query.logevents.length) aw = aw.query.logevents[0].timestamp;
}
if (typeof aw === 'string') {
us.loading_registration.replaceWith(us.formatDate(aw) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(aw)) + ')' }));
data.registration = aw;
return us.setCookie();
}
}
us.loading_registration.replaceWith($('<span>', { style: us.styleMissingData + us.styleLoading, text: msg.nodb }));
},
writeFirstEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs.length) return;
var date = data.firstedit = aw.query.usercontribs[0].timestamp;
$('#us_reg_date').append($('<li>').append([$('<b>', { text: '• ' + msg.fiedit }), us.formatDate(date) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(date)) + ')' })]));
},
writeLastEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs || !aw.query.usercontribs.length) return us.us_last_edit_loading.replaceWith($('<span>', { style: us.styleMissingData, text: msg.noedit }));
var date = data.lastedit = aw.query.usercontribs[0].timestamp;
us.us_last_edit_loading.replaceWith(us.getDateDiff(us.now, us.getDateFromTimestamp(date)), ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.formatDate(date) + ')' }));
data.timediff = mw.now() - us.now;
$('#us_editcount').append($('<span>', { style: 'float:right;' + us.styleMissingData, text: msg.curtimeDiff }).append($('<b>', { text: Math.round(data.timediff / 1000) })));
us.setCookie();
},
writeGlobalGroup: function (aw) {
aw = aw.query.globaluserinfo;
var groups = data.glGrp = aw.groups;
if (groups && groups.length) {
us.us_global_group_loading.replaceWith(us.getLocalNames(groups, 'GlobalGroupPermissions'));
us.us_global_group.css('display', 'block');
// Fetch former global rights
us.fetchFormerRights(true);
}
if (aw.locked !== undefined) {
data.locked = 1;
mw.loader.using('mediawiki.ForeignApi').done(function () {
var fApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php');
fApi.get({ action: 'query', list: 'logevents', leprop: 'user|timestamp|comment', letype: 'globalauth', letitle: 'User:' + us.user + '@global', lelimit: '1' }).done(us.writeGlobalBlock);
});
}
us.setCookie();
},
writeGlobalBlock: function (aw) {
if (!aw.query || !aw.query.logevents.length) return;
aw = aw.query.logevents[0];
$('#us_block_status_span').replaceWith($('<a>', { style: us.styleBlocked, id: 'us_block_status_span', href: '//meta.wikimedia.org/w/index.php?title=Special:Log/&type=globalauth&user=&page=User%3A' + us.user + '%40global', text: 'locked (global)' }).append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) })));
$('#us_block_status').parent().append([$('<li>').append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment)]), $('<li>').append([$('<b>').text('• ' + msg.blocker), $('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user })])]);
},
writeBlockDetail: function (aw) {
var duration = 'infinite', expiry = '';
if (aw.query && aw.query.logevents.length) {
aw = aw.query.logevents[0];
if (aw && aw.params) {
duration = aw.params.duration;
expiry = /in(de)?finite/.test(duration) ? msg.never : us.formatDate(aw.params.expiry);
}
$('#us_block_status_span').text(msg.block + ' (' + duration + ')').append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) }));
if (expiry) $('#mw-spinner-us_block_time_loading').replaceWith(expiry); else $('#us_block_time').remove();
$('#mw-spinner-us_block_reason_loading').replaceWith(us.parseComment(aw.comment, aw.commenthidden));
if (aw.user) $('#mw-spinner-us_blocker_loading').replaceWith($('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user }));
else $('#mw-spinner-us_blocker_loading').replaceWith(msg.nodb);
}
},
writePatrolCount: function (aw) {
if (aw instanceof Object) {
if (!aw.query || !aw.query.logevents) return;
us.patrols += aw.query.logevents.length;
if (aw.continue) return us.getPatrolCount(aw.continue);
$.removeSpinner('pat');
aw = us.patrols;
}
us.us_patrolcount_loading.replaceWith($('<a>', { href: '/w/index.php?title=Special:Log&type=' + (us.review ? 'review&subtype=accept' : 'patrol&subtype=patrol') + '&user=' + us.user, text: aw }));
data.reviews = aw;
us.setCookie();
},
getPatrolCount: function (e) {
var params = '&list=logevents&leprop=&ledir=newer&lelimit=max&leuser=' + us.user + '&leaction=' + (us.review ? 'review/approve' : 'patrol/patrol');
if (e instanceof Object) {
if (e.target) { $(e.target).injectSpinner('pat'); us.patrols = 0; }
else if (e.lecontinue) params += '&lecontinue=' + e.lecontinue;
}
us.actions.push({ params: params, func: us.writePatrolCount });
us.doRequest();
},
parseComment: function (text, hidden) {
var comment = $('<span>', { 'class': 'comment' });
if (typeof text === 'undefined') return comment.append(hidden ? msg.noReason : 'undefined').css('color', hidden ? '#999' : 'red');
if (!text) return comment.append(msg.blockCmt).css('color', '#999');
var intLink = /(.*?)\[\[((.*?)\|)?(.*?)\]\](.*)/, suche = text, erg;
while ((erg = intLink.exec(suche)) !== null) {
var link = erg[3] || erg[4];
comment.append([erg[1], $('<a>', { href: mw.util.getUrl(link), title: link }).text(erg[4])]);
suche = erg[5];
}
return comment.append(suche);
},
formatDate: function (datum) {
if (!(datum instanceof Date)) datum = new Date(datum);
try {
return datum.toLocaleDateString(us.lang, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
} catch (e) { return datum.toLocaleString(); }
},
getStoredData: function () {
var key = project + us.user, db, request;
try {
request = indexedDB.open(us.name);
request.onsuccess = function () {
db = this.result;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readonly').objectStore(key);
store.transaction.oncomplete = function () { us.runDataStore(); db.close(); };
store.get(key).onsuccess = function () { if (this.result) data = this.result.data; else us.getCookie(); };
} else { us.getCookie(); }
};
request.onerror = function () { us.getCookie(); };
} catch (e) { us.getCookie(); }
},
getCookie: function () {
data = mw.cookie.get(us.name + us.user);
if (data) try { data = JSON.parse(data); } catch(e) { data = null; }
us.runDataStore();
},
initI18N: function (i18n) {
var chain = mw.language.getFallbackLanguageChain();
for (var i = chain.length - 1; i >= 0; i--) if (chain[i] in i18n) msg = i18n[chain[i]];
},
init: function () {
if (this.statusBox || (this.statusBox = $('#us_box'))[0]) this.statusBox.remove();
this.usprop = 'blockinfo|editcount|gender|groups|implicitgroups|registration';
this.self = mw.config.get('wgUserName') === this.user;
this.initI18N(i18n);
this.createBox();
if (!this.first) {
if (mw.cookie && JSON) { if (window.indexedDB) this.getStoredData(); else this.getCookie(); }
else this.run();
} else this.run();
},
runDataStore: function () {
if (data) {
if (typeof data === 'string') try { data = JSON.parse(data); } catch(e) { data = {}; }
var q = { query: { users: [data] } };
if (data.timestamp) this.now = mw.now() - (data.timediff || 0);
if (data.timestamp && Math.abs((mw.now() - data.timestamp - (data.timediff || 0)) / 1000) < 90) return this.writeCommonInfo(q);
if (data.editcount) this.usprop = 'editcount';
}
data = data || {};
this.run();
},
run: function () {
if (this.self) {
return this.writeCommonInfo({ query: { users: [{ editcount: mw.config.get('wgUserEditCount'), registration: mw.config.get('wgUserRegistration'), groups: mw.config.get('wgUserGroups'), blockreason: '' }] } });
}
us.ajaxRequest({ curtimestamp: 1, list: 'users', ususers: us.user, usprop: us.usprop }, us.writeCommonInfo);
}
};
if ([2, 3].indexOf(mw.config.get('wgNamespaceNumber')) !== -1 && us.user.indexOf('/') === -1 && (!mw.config.get('wgArticleId') || mw.config.get('wgAction') === 'view')) {
$.when(mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.language', 'mediawiki.cookie', 'jquery.spinner', 'mediawiki.ForeignApi']), $.ready).then(function () {
us.api = mw.util.wikiScript('api') + '?action=query&format=json';
us.ajaxRequest('&meta=allmessages&amenableparser=1&amincludelocal=1&refix=group-', us.getGroupNames);
if (!mw.libs.viewerInfo) mw.loader.load('//meta.wikimedia.org/w/index.php?title=User:Perhelion/viewerInfo.js&action=raw&ctype=text/javascript');
});
}
}(jQuery, mediaWiki));
eucewywadyx545g311ju6qj3hv8w890
739937
739932
2026-05-01T09:03:59Z
MrJaroslavik
44012
test
739937
javascript
text/javascript
/**
* @Description: Modified Userstatus.js with Former Groups (Local & Global)
* Original by Steef389, Perhelion.
**/
(function ($, mw) {
'use strict';
var project = window.project || mw.config.get('wgDBname');
var msg, i18n = {
en: {
noReason: 'reason removed',
blockCmt: 'no block comment',
never: 'never',
not: ' not ',
block: 'blocked',
and: '$1 and$2',
count: 'Count',
noedit: 'None (only deleted contributions?)',
nodb: '‹not in database›',
noGrp: 'no extended group',
dates: ['years', 'months', 'days', 'hours', 'minutes', 'seconds'],
date: ['year', 'month', 'day', 'hour', 'minute', 'second'],
nosec: 'less than one ',
curtimeDiff: ' Locale time difference to server time in s: ',
thxGn: 'given',
thxRd: 'received',
nova: 'new',
ago: '$1 ago',
blocklog: 'Block log',
contrib: 'Edits: ',
usub: 'Subpages⬇',
thxGvng: 'Thanksgivings: ',
reviews: 'Active reviews: ',
regdate: 'Registration: ',
laedit: 'Last edited: ',
lala: 'Last log activity',
fiedit: 'First edit: ',
blocks: 'Block-status: ',
loGrp: 'Local user-groups: ',
glGrp: 'Global user-groups: ',
formerLoGrp: 'Former local user-groups: ',
formerGlGrp: 'Former global user-groups: ',
blockEnd: 'Block-end: ',
blocker: 'Blocker: ',
blockReason: 'Block-reason: '
},
cs: {
noReason: 'důvod odstraněn',
blockCmt: 'bez komentáře',
never: 'nikdy',
not: ' ne ',
block: 'zablokován',
and: '$1 a$2',
count: 'Počet',
noedit: 'Žádné (jen smazané?)',
nodb: '‹není v databázi›',
noGrp: 'žádná rozšířená skupina',
dates: ['let', 'měsíců', 'dní', 'hodin', 'minut', 'sekund'],
date: ['rok', 'měsíc', 'den', 'hodina', 'minuta', 'sekunda'],
nosec: 'méně než ',
curtimeDiff: ' Rozdíl lokálního času k serveru v s: ',
thxGn: 'poděkoval',
thxRd: 'obdržel poděkování',
nova: 'nové',
ago: 'před $1',
days: 'dní',
blocklog: 'Kniha zablokování',
contrib: 'Editace: ',
usub: 'Podstránky⬇',
thxGvng: 'Poděkování: ',
reviews: 'Aktivní prověření: ',
regdate: 'Registrace: ',
laedit: 'Poslední editace: ',
lala: 'Poslední aktivita v protokolech',
fiedit: 'První editace: ',
blocks: 'Stav bloku: ',
loGrp: 'Lokální skupiny: ',
glGrp: 'Globální skupiny: ',
formerLoGrp: 'Bývalé lokální skupiny: ',
formerGlGrp: 'Bývalé globální skupiny: ',
blockEnd: 'Konec bloku: ',
blocker: 'Zablokoval: ',
blockReason: 'Důvod bloku: '
}
};
var data;
var us = mw.libs.userstatus = {
name: 'Userstatus',
version: 1.80, // Bumping version for former groups update
lastEditSeconds: false,
styleMissingData: 'color:#999;font-size:90%;',
styleLoading: 'font-style:italic;',
styleBlocked: 'color:#c00',
styleNotBlocked: 'color:#080',
styleFormer: 'margin-left:15px; color:#72777d; font-style:italic; font-size:0.9em;',
viewPatrolNumber: false,
lang: mw.config.get('wgUserLanguage'),
user: mw.config.get('wgTitle'),
cookie: [],
thanks: 0,
patrols: 0,
actions: [],
dbVersion: 0,
implicitGroups: ['*', 'user', 'autoconfirmed'],
getLocalNames: function (groups, specialpage) {
if (groups) {
var arr = [];
specialpage = specialpage || 'ListGroupRights';
for (var i = 0; i < groups.length; ++i) {
var g = groups[i];
var n = us.groupNames[g];
if (n) arr.push('<a href="/wiki/Special:' + specialpage + '#' + g + '">' + n + '</a>');
else arr.push(g);
}
groups = arr.join(', ').replace(/(.*),([^,]*)$/, msg.and);
}
return groups;
},
writeGroups: function (groups) {
groups = us.getLocalNames(groups);
if (!groups) {
groups = $('<span>', {
style: us.styleMissingData + us.styleLoading,
text: msg.noGrp
});
}
us.loading_groups.replaceWith(groups);
// After local groups are written, fetch former local groups
us.fetchFormerRights(false);
},
getGroupNames: function (aw) {
if (!aw || !aw.query) return;
aw = aw.query.allmessages;
var al = aw.length;
var groups = {};
while (al--) {
var an = aw[al];
var name = an.name;
if (/^group-/.test(name)) {
name = RegExp.rightContext || name.substring(6);
groups[name] = an['*'];
}
}
us.groupNames = groups;
us.init();
},
ajaxRequest: function (params, onSuccess, trial) {
var url = us.api;
if (!(params instanceof Object)) {
url += params + '&maxage=2419200&smaxage=2419200';
params = {};
} else {
params.maxlag = 3;
params.maxage = 2419200;
params.smaxage = 2419200;
if (trial) params.maxlag *= trial;
}
var api = $.getJSON(url, params, onSuccess)
.fail(function (jqXHR, status) {
var note = 'Timeout fail, maybe try again!';
if (trial) {
if (!status.textStatus || (status.textStatus !== 'timeout' && jqXHR !== 'maxlag')) note = $('<span>Fehler bitte <a href="' + mw.util.getUrl('User_talk:Perhelion') + '">Perhelion</a> melden.</span>');
mw.notify(note, { title: us.name + ':', type: 'error' });
} else if (status.textStatus === 'timeout' || jqXHR === 'maxlag') {
api.abort();
return us.ajaxRequest(params, onSuccess, 2);
}
});
},
// --- NEW FORMER RIGHTS LOGIC ---
fetchFormerRights: function(isGlobal) {
var apiTarget = isGlobal ? 'https://meta.wikimedia.org/w/api.php' : mw.util.wikiScript('api');
var logType = isGlobal ? 'gblrights' : 'rights';
var userTarget = isGlobal ? 'User:' + us.user + (isGlobal ? '@global' : '') : 'User:' + us.user;
var containerId = isGlobal ? 'us_former_global_groups' : 'us_former_local_groups';
var params = {
action: 'query',
list: 'logevents',
letype: logType,
letitle: userTarget,
lelimit: 500,
format: 'json',
origin: isGlobal ? '*' : undefined
};
$.getJSON(apiTarget, params, function(res) {
if(!res || !res.query || !res.query.logevents) return;
var logs = res.query.logevents;
var former = {};
var currentGroups = isGlobal ? (data.glGrp || []) : (data.groups || []);
logs.forEach(function(log) {
var p = log.params;
if (!p) return;
var date = log.timestamp;
// EXTRAKCE PRÁV: Podpora pro staré číselné indexy "0" a "1" i moderní pole
var oldRaw = p.oldgroups || p.oldGroups || p.remove || p["0"] || "";
var newRaw = p.newgroups || p.newGroups || p.add || p["1"] || "";
// Převod na pole (pokud je to string s čárkami nebo "(none)")
var parse = function(val) {
if (Array.isArray(val)) return val;
if (typeof val === 'string') {
if (val === "(none)" || val === "") return [];
return val.split(/[,|]\s*/);
}
return [];
};
var oldG = parse(oldRaw);
var newG = parse(newRaw);
// Detekce odebrání
oldG.forEach(function(g) {
g = g.trim();
if (us.implicitGroups.indexOf(g) !== -1 || !g) return;
if (currentGroups.indexOf(g) === -1 && !former[g]) {
former[g] = { to: date, from: null };
}
});
// Detekce udělení (hledání začátku)
newG.forEach(function(g) {
g = g.trim();
if (former[g] && !former[g].from) {
former[g].from = date;
}
});
});
var entries = [];
Object.keys(former).sort().forEach(function(g) {
var item = former[g];
var dateTo = new Date(item.to);
var dateFrom = item.from ? new Date(item.from) : null;
if(dateFrom) {
var diff = Math.floor((dateTo - dateFrom) / (1000 * 60 * 60 * 24));
if (diff > 0) {
entries.push(g + ' (' + dateFrom.toISOString().split('T')[0] + ' – ' + dateTo.toISOString().split('T')[0] + ', ' + diff + ' ' + msg.days + ')');
}
} else {
entries.push(g + ' (do ' + dateTo.toISOString().split('T')[0] + ')');
}
});
if(entries.length > 0) {
$('#' + containerId).show().find('.us_former_content').text(entries.join(', '));
}
});
},
getThanks: function (e) {
var params = '&list=logevents&continue=-%7C%7C&leprop=&letype=thanks&ledir=newer&lelimit=max';
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
us.e.attr('title', us.e.text());
us.e.text('');
us.e.injectSpinner('thx');
us.thanks = 0;
} else if (e.lecontinue) {
params += '&lecontinue=' + e.lecontinue;
}
if (us.e.attr('title') === msg.thxGn) params += '&leuser=' + us.user;
else params += '&letitle=User:' + us.user;
}
us.actions.push({ params: params, func: us.writeThanks });
us.doRequest();
},
writeThanks: function (uq) {
if (!uq.query || !uq.query.logevents) return mw.log.warn(uq);
var e = us.e;
var user = 'page=';
var aw = uq.query.logevents;
us.thanks += aw.length;
if (uq.continue) return us.getThanks(uq.continue);
if (e[0].title === msg.thxGn) user += '&user=';
user += us.user;
$.removeSpinner('thx');
e.text(us.thanks);
e.off('click');
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special%3ALog&type=thanks&' + user + '&year=&month=-1&tagfilter=&hide_thanks_log=0';
delete us.e;
},
getUploads: function (e) {
var params = '&list=logevents&letype=upload&ledir=newer&lelimit=max&leuser=' + us.user;
function _start(u) {
var text = u.text();
u.prop('title', text);
u.text('');
u.injectSpinner('upload' + text);
u.data('upl', 0);
u.data('del', 0);
}
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
if (us.e.text() !== msg.nova) {
us.e2 = us.e.nextAll('a').eq(0);
if (us.e2.text() === msg.nova) _start(us.e2); else delete us.e2;
}
_start(us.e);
} else if (e.lecontinue) { params += '&lecontinue=' + e.lecontinue; }
params += '&leprop=ids';
if (us.e.prop('title') === msg.nova) params += '&leaction=upload/upload';
else if (us.e2) params += '|type';
}
us.actions.push({ params: params, func: us.writeUploads });
us.doRequest();
},
writeUploads: function (uq) {
if (!uq.query || !uq.query.logevents || !us.e) return mw.log(uq, us.e);
function _insert(e) {
$.removeSpinner('upload' + e.prop('title'));
e.text('');
e.off('click');
e.append(e.data('del'), $('<span>', { style: us.styleMissingData + us.styleLoading, text: ' (+' + (e.data('upl') - e.data('del')) + ' del.)' }));
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special:Log&type=upload&user=' + us.user + '&subtype=' + ((e[0].title === msg.nova) ? 'upload' : '');
}
var e = us.e;
var e2 = us.e2;
var aw = uq.query.logevents;
var alen = aw.length, i = 0;
var del = e.data('del');
if (e2) {
var upl = e2.data('upl');
var del2 = e2.data('del');
for (i; i < alen; ++i) {
var a = aw[i];
var c = a.pageid ? 1 : 0;
if (a.action === 'upload') { upl++; del2 += c; }
del += c;
}
e2.data('upl', upl);
e2.data('del', del2);
} else {
for (i; i < alen; ++i) if (aw[i].pageid) del++;
}
e.data('upl', e.data('upl') + alen);
e.data('del', del);
if (uq.continue) return us.getUploads(uq.continue);
_insert(e);
if (e2) { _insert(e2); delete us.e2; }
delete us.e;
},
writeCommonInfo: function (uq) {
var aw = uq.query;
if (!aw || !aw.users[0] || Object.prototype.hasOwnProperty.call(aw, 'missing') || Object.prototype.hasOwnProperty.call(aw.users[0], 'invalid')) return us.statusBox.remove();
aw = aw.users[0];
var edits = aw.editcount,
groups = data.groups || aw.groups,
blocked = data.blockreason || aw.blockexpiry,
gender = data.gender || aw.gender,
uploads = $('<a>', { title: msg.count, text: msg.nova, style: us.styleLoading, click: us.getUploads });
if (aw.registration) data.registration = aw.registration;
us.now = us.getDateFromTimestamp(uq.curtimestamp || us.now || mw.now());
us.user = us.user.replace(/ /g, '_');
us.first = 1;
if (edits) {
if (data.editcount && data.lastedit && data.editcount === edits) {
uq.query.usercontribs = [{ timestamp: data.lastedit }];
us.writeLastEdit(uq);
} else {
data.editcount = edits;
us.loading_last_edit.css('display', 'block');
us.actions.push({
params: '&list=usercontribs&ucuser=' + us.user + '&uclimit=1&uccontinue=' + (new Date(us.now).toISOString().replace(/[^\d]/g, '').slice(0, 14) + '|2'),
func: us.writeLastEdit
});
}
$('#t-contributions').remove();
} else { us.writeLastEdit({}); }
if (groups) {
edits = $('<a>', { title: 'Supercount', href: '//tools.wmflabs.org/supercount/index.php?user=' + us.user + '&project=' + location.hostname, text: edits });
if ($.inArray('bot', groups) === -1) {
us.review = $.inArray('editor', groups) !== -1;
if (us.review) {
if (typeof data.reviews === 'number') us.writePatrolCount(data.reviews);
} else if (us.viewPatrolNumber) { us.us_patrolcount_loading.parent().remove(); }
edits = [
edits, ' • ',
$('<a>', { href: '//xtools.wmflabs.org/ec' + mw.config.get('wgServer').substr(1) + '/' + us.user, title: 'XTools', text: 'XTools', style: us.styleLoading, target: '_blank' }), ' • ',
$('<a>', { href: '//commons.wikimedia.org/w/index.php?title=Commons:MyGallery/' + us.user + '&withJS=MediaWiki:JSONListUploads.js', title: 'MyGallery', text: (project === 'commonswiki' ? '' : 'c:') + 'MyGallery', style: us.styleLoading, target: '_blank' }), ' • ',
(project !== 'commonswiki' ? $('<a>', { href: '/wiki/Special:ListFiles/' + us.user, text: 'Upload', title: 'Special:Listfiles', style: us.styleLoading }) : 'Upload'),
(us.lang === 'de' ? '-' : ' ') + msg.count + ': ',
uploads.clone(1).text('total'), ' / ', uploads
];
if (!$('#t-subpages').length) {
edits.push($('<a>', { href: mw.util.getUrl('Special:Prefixindex/User:' + us.user + '/'), text: ' • ' + msg.usub, style: us.styleMissingData + us.styleLoading }));
}
} else if (us.us_patrolcount_loading) { us.us_patrolcount_loading.parent().remove(); }
us.loading_editcount.replaceWith(edits);
if (!aw.implicitgroups && !data.groups) {
aw.implicitgroups = ['*', 'user', 'autoconfirmed'];
if (groups.length < 4) aw.implicitgroups.pop();
}
data.groups = $.grep(groups, function (n) { return $.inArray(n, aw.implicitgroups) === -1; });
us.writeGroups(data.groups);
$('#t-userrights').remove();
}
if (!data.registration) {
us.actions.push({ params: '&list=logevents&leprop=timestamp|type&letype=newusers&lelimit=1&ledir=newer&leend=2005-12-30T00:00:00Z&leuser=' + us.user, func: us.writeRegistration });
if (data.firstedit) {
uq.query.usercontribs = [{ timestamp: data.firstedit }];
us.writeFirstEdit(uq);
} else if (edits) {
us.actions.push({ params: { list: 'usercontribs', ucuser: us.user, uclimit: 1, ucend: '2005-09-08T00:00:00Z', ucdir: 'newer', ucprop: 'timestamp' }, func: us.writeFirstEdit });
}
} else { us.writeRegistration(data.registration); }
us.loading_blocked.replaceWith($('<a>', { style: ((blocked) ? us.styleBlocked : us.styleNotBlocked), id: 'us_block_status_span', href: '/w/index.php?title=Special:Log/block&page=User:' + us.user, title: msg.blocklog, text: ((blocked) ? ' ' : msg.not) + msg.block }));
if (blocked) {
data.blockreason = aw.blockreason || ' ';
us.ul.append([
$('<li>', { id: 'us_block_time' }).append([$('<b>').text('• ' + msg.blockEnd), $.createSpinner('us_block_time_loading')]),
$('<li>', { id: 'us_block_reason' }).append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment, (aw.commenthidden))]),
$('<li>', { id: 'us_blocker' }).append([$('<b>').text('• ' + msg.blocker), $.createSpinner('us_blocker_loading')])
]);
us.actions.push({ params: { list: 'logevents', letitle: 'User:' + us.user, letype: 'block', lelimit: 1 }, func: us.writeBlockDetail });
}
if (data.glGrp && !data.locked) {
uq.query.globaluserinfo = { groups: (data.glGrp.length) ? data.glGrp : null };
us.writeGlobalGroup(uq);
} else {
us.actions.push({ params: '&meta=globaluserinfo&guiprop=groups&guiuser=' + us.user, func: us.writeGlobalGroup });
}
if (gender) {
data.gender = gender;
var genderSn = (gender === 'male') ? ' \u2642' : (gender === 'female' ? ' \u2640' : '');
$('#firstHeading').append($('<span>', { id: 'ps-gender-' + gender, style: 'font-size:80%', text: genderSn }));
}
if (us.actions.length) us.doRequest();
},
createBox: function () {
var statusBox = $('<div>', { style: 'border-bottom:1px solid #aaa;text-shadow:1px 1px 1px #eff;', id: 'us_box' });
if (mw.config.get('skin') === 'vector') statusBox.css('font-size', '0.8em');
var spanFrag = $.createSpinner();
us.loading_editcount = spanFrag.clone();
us.loading_registration = spanFrag.clone();
us.loading_groups = spanFrag.clone();
us.loading_blocked = spanFrag.clone();
us.us_global_group_loading = spanFrag.clone();
us.us_last_edit_loading = $.createSpinner('contribs');
us.loading_last_edit = $('<li>');
us.us_global_group = $('<li>', { style: 'display: none' });
var thx = $('<a>', { title: msg.count, text: msg.thxRd, style: us.styleLoading, click: us.getThanks });
var $userrights = $('#t-userrights a');
var $contributions = $('#t-contributions a');
if (!$userrights.length) $userrights = $('<a>', { href: '/wiki/Special:UserRights/' + us.user });
if (!$contributions.length) $contributions = $('<a>', { href: '/wiki/Special:Contributions/' + us.user });
var ul = $('<ul>', { style: 'list-style: none' }).append($('<li>', { id: 'us_editcount' }).append([$('<b>').append($contributions.text(msg.contrib)), us.loading_editcount]));
us.us_log_count = $('<span>');
if (us.viewPatrolNumber) {
us.us_patrolcount_loading = $('<a>', { style: us.styleLoading, click: us.getPatrolCount, text: msg.count });
us.us_log_count = $('<span>').append([$('<b>').text(msg.reviews), us.us_patrolcount_loading, ' • ']);
}
ul.append(us.us_log_count)
.append($('<li>', { id: 'us_reg_date' }).append([$('<b>').text(msg.regdate), us.loading_registration, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://tools.wmflabs.org/meta/userpages/' + us.user, text: '• User pages' })]))
.append($('<li>').append([$('<b>').append($userrights.text(msg.loGrp)), us.loading_groups, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://tools.wmflabs.org/meta/stalktoy/' + us.user, text: '• Stalk toy' })]))
// Container for former local groups
.append($('<li>', { id: 'us_former_local_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerLoGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.us_global_group.append([$('<b>').append($('<a>', { href: '/w/index.php?title=Special:GlobalUsers&limit=1&username=' + us.user, text: msg.glGrp })), us.us_global_group_loading, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://tools.wmflabs.org/meta/globalgroups/', text: '• GlobalGroups' })]))
// Container for former global groups
.append($('<li>', { id: 'us_former_global_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerGlGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.loading_last_edit.append([$('<b>').text(msg.laedit), us.us_last_edit_loading, ' ', $('<a>', { target: '_blank', style: us.styleMissingData, href: '/w/index.php?title=Special:Log/' + us.user + '&hide_thanks_log=0&hide_patrol_log=0&hide_tag_log=0', text: '• LLA' }), $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://tools.wmflabs.org/meta/crossactivity/' + us.user, text: '• CrossActivity' })]));
statusBox.append(ul);
us.ul = $('<ul>', { style: 'list-style:none' }).append($('<li>', { id: 'us_block_status' }).append([$('<b>').text(msg.blocks), us.loading_blocked, ' • ', $('<b>').text(msg.thxGvng), thx, ' / ', thx.clone(1).text(msg.thxGn), $('<a>', { style: 'float:right;' + us.styleMissingData, title: 'Purge data', text: 'purge', click: us.removeDataStore })]));
statusBox.append(us.ul);
$('#firstHeading').after(statusBox);
us.statusBox = statusBox;
},
removeDataStore: function (e) {
var key = project + us.user, db = window.indexedDB;
e.preventDefault();
if (!db || !us.dbVersion) return mw.cookie.set(us.name + us.user, null);
var request = db.open(us.name, us.dbVersion + 1);
request.onupgradeneeded = function () { db = this.result; if (db.objectStoreNames.contains(key)) db.deleteObjectStore(key); };
request.onsuccess = function () { db = this.result; us.dbVersion = db.version; db.close(); location.reload(); };
},
setCookie: function () {
var domain = (mw.config.get('wgNoticeProject') === 'wikipedia') ? 'wikipedia.org' : '';
var name = us.name + us.user;
var _saveCookie = function (k, d) { mw.cookie.set(name, JSON.stringify(d), { expires: 604800, domain: domain }); };
if (!us.actions[0] && JSON && data.editcount) {
if (us.cookie.length) window.clearTimeout(us.cookie.shift());
var saveData = window.indexedDB ? function (n, JSd, v) {
var key = project + us.user, db = window.indexedDB, request = db.open(us.name, v);
request.onupgradeneeded = function () {
db = this.result;
if (!db.objectStoreNames.contains(key)) {
var store = db.createObjectStore(key, { keyPath: 'name' });
store.createIndex('data', 'data', { unique: false });
}
};
request.onsuccess = function () {
db = this.result; us.dbVersion = db.version;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readwrite').objectStore(key);
store.put({ name: key, data: JSd });
} else { saveData(n, JSd, us.dbVersion + 1); }
};
request.onerror = function() { _saveCookie(n, JSd); };
} : _saveCookie;
us.cookie.push(setTimeout(function () {
data.timestamp = new Date().valueOf();
saveData('', data);
}, 500));
}
},
doRequest: function () {
var action = us.actions.shift();
if (action) us.ajaxRequest(action.params, action.func);
if (us.actions.length) window.setTimeout(function () { us.doRequest(); }, 100);
},
getDateDiff: function (now, date) {
var dStr = [];
var d = { years: 0, months: 0, days: 0 };
var diffDays = Math.floor(Math.abs(now - date) / 86400000);
if (now > date) {
var year = now.getFullYear();
var months = now.getMonth();
d.years = date.getFullYear();
date.setFullYear(year);
d.years = year - d.years;
if (date > now) {
d.years--;
date.setFullYear(year - 1);
}
var mDiff = months - date.getMonth();
if (mDiff < 0) mDiff += 12;
d.months = mDiff;
d.days = Math.floor((now - date) / 86400000) % 30;
var units = [d.years, d.months, d.days];
var labelSingle = [msg.date[0], msg.date[1], msg.date[2]];
var labelPlural = [msg.dates[0], msg.dates[1], msg.dates[2]];
for (var i = 0; i < 3; ++i) {
if (units[i]) {
dStr.push(units[i] + ' ' + ((units[i] > 1) ? labelPlural[i] : labelSingle[i]));
}
}
}
var mainDiff = (dStr.length) ? dStr.join(', ').replace(/(.*),([^,]*)/, msg.and).replace(/(.*)/, msg.ago) : msg.nosec + msg.date[2];
return mainDiff + (diffDays > 0 ? ' = ' + diffDays + ' ' + (msg.days || 'dní') : '');
},
getDateFromTimestamp: function (t) { if (!t) return false; t = new Date(t); return isNaN(t.valueOf()) ? false : t; },
writeRegistration: function (aw) {
if (aw) {
if (aw instanceof Object) {
if (aw.query && aw.query.logevents.length) aw = aw.query.logevents[0].timestamp;
}
if (typeof aw === 'string') {
us.loading_registration.replaceWith(us.formatDate(aw) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(aw)) + ')' }));
data.registration = aw;
return us.setCookie();
}
}
us.loading_registration.replaceWith($('<span>', { style: us.styleMissingData + us.styleLoading, text: msg.nodb }));
},
writeFirstEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs.length) return;
var date = data.firstedit = aw.query.usercontribs[0].timestamp;
$('#us_reg_date').append($('<li>').append([$('<b>', { text: '• ' + msg.fiedit }), us.formatDate(date) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(date)) + ')' })]));
},
writeLastEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs || !aw.query.usercontribs.length) return us.us_last_edit_loading.replaceWith($('<span>', { style: us.styleMissingData, text: msg.noedit }));
var date = data.lastedit = aw.query.usercontribs[0].timestamp;
us.us_last_edit_loading.replaceWith(us.getDateDiff(us.now, us.getDateFromTimestamp(date)), ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.formatDate(date) + ')' }));
data.timediff = mw.now() - us.now;
$('#us_editcount').append($('<span>', { style: 'float:right;' + us.styleMissingData, text: msg.curtimeDiff }).append($('<b>', { text: Math.round(data.timediff / 1000) })));
us.setCookie();
},
writeGlobalGroup: function (aw) {
aw = aw.query.globaluserinfo;
var groups = data.glGrp = aw.groups;
if (groups && groups.length) {
us.us_global_group_loading.replaceWith(us.getLocalNames(groups, 'GlobalGroupPermissions'));
us.us_global_group.css('display', 'block');
// Fetch former global rights
us.fetchFormerRights(true);
}
if (aw.locked !== undefined) {
data.locked = 1;
mw.loader.using('mediawiki.ForeignApi').done(function () {
var fApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php');
fApi.get({ action: 'query', list: 'logevents', leprop: 'user|timestamp|comment', letype: 'globalauth', letitle: 'User:' + us.user + '@global', lelimit: '1' }).done(us.writeGlobalBlock);
});
}
us.setCookie();
},
writeGlobalBlock: function (aw) {
if (!aw.query || !aw.query.logevents.length) return;
aw = aw.query.logevents[0];
$('#us_block_status_span').replaceWith($('<a>', { style: us.styleBlocked, id: 'us_block_status_span', href: '//meta.wikimedia.org/w/index.php?title=Special:Log/&type=globalauth&user=&page=User%3A' + us.user + '%40global', text: 'locked (global)' }).append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) })));
$('#us_block_status').parent().append([$('<li>').append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment)]), $('<li>').append([$('<b>').text('• ' + msg.blocker), $('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user })])]);
},
writeBlockDetail: function (aw) {
var duration = 'infinite', expiry = '';
if (aw.query && aw.query.logevents.length) {
aw = aw.query.logevents[0];
if (aw && aw.params) {
duration = aw.params.duration;
expiry = /in(de)?finite/.test(duration) ? msg.never : us.formatDate(aw.params.expiry);
}
$('#us_block_status_span').text(msg.block + ' (' + duration + ')').append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) }));
if (expiry) $('#mw-spinner-us_block_time_loading').replaceWith(expiry); else $('#us_block_time').remove();
$('#mw-spinner-us_block_reason_loading').replaceWith(us.parseComment(aw.comment, aw.commenthidden));
if (aw.user) $('#mw-spinner-us_blocker_loading').replaceWith($('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user }));
else $('#mw-spinner-us_blocker_loading').replaceWith(msg.nodb);
}
},
writePatrolCount: function (aw) {
if (aw instanceof Object) {
if (!aw.query || !aw.query.logevents) return;
us.patrols += aw.query.logevents.length;
if (aw.continue) return us.getPatrolCount(aw.continue);
$.removeSpinner('pat');
aw = us.patrols;
}
us.us_patrolcount_loading.replaceWith($('<a>', { href: '/w/index.php?title=Special:Log&type=' + (us.review ? 'review&subtype=accept' : 'patrol&subtype=patrol') + '&user=' + us.user, text: aw }));
data.reviews = aw;
us.setCookie();
},
getPatrolCount: function (e) {
var params = '&list=logevents&leprop=&ledir=newer&lelimit=max&leuser=' + us.user + '&leaction=' + (us.review ? 'review/approve' : 'patrol/patrol');
if (e instanceof Object) {
if (e.target) { $(e.target).injectSpinner('pat'); us.patrols = 0; }
else if (e.lecontinue) params += '&lecontinue=' + e.lecontinue;
}
us.actions.push({ params: params, func: us.writePatrolCount });
us.doRequest();
},
parseComment: function (text, hidden) {
var comment = $('<span>', { 'class': 'comment' });
if (typeof text === 'undefined') return comment.append(hidden ? msg.noReason : 'undefined').css('color', hidden ? '#999' : 'red');
if (!text) return comment.append(msg.blockCmt).css('color', '#999');
var intLink = /(.*?)\[\[((.*?)\|)?(.*?)\]\](.*)/, suche = text, erg;
while ((erg = intLink.exec(suche)) !== null) {
var link = erg[3] || erg[4];
comment.append([erg[1], $('<a>', { href: mw.util.getUrl(link), title: link }).text(erg[4])]);
suche = erg[5];
}
return comment.append(suche);
},
formatDate: function (datum) {
if (!(datum instanceof Date)) datum = new Date(datum);
try {
return datum.toLocaleDateString(us.lang, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
} catch (e) { return datum.toLocaleString(); }
},
getStoredData: function () {
var key = project + us.user, db, request;
try {
request = indexedDB.open(us.name);
request.onsuccess = function () {
db = this.result;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readonly').objectStore(key);
store.transaction.oncomplete = function () { us.runDataStore(); db.close(); };
store.get(key).onsuccess = function () { if (this.result) data = this.result.data; else us.getCookie(); };
} else { us.getCookie(); }
};
request.onerror = function () { us.getCookie(); };
} catch (e) { us.getCookie(); }
},
getCookie: function () {
data = mw.cookie.get(us.name + us.user);
if (data) try { data = JSON.parse(data); } catch(e) { data = null; }
us.runDataStore();
},
initI18N: function (i18n) {
var chain = mw.language.getFallbackLanguageChain();
for (var i = chain.length - 1; i >= 0; i--) if (chain[i] in i18n) msg = i18n[chain[i]];
},
init: function () {
if (this.statusBox || (this.statusBox = $('#us_box'))[0]) this.statusBox.remove();
this.usprop = 'blockinfo|editcount|gender|groups|implicitgroups|registration';
this.self = mw.config.get('wgUserName') === this.user;
this.initI18N(i18n);
this.createBox();
if (!this.first) {
if (mw.cookie && JSON) { if (window.indexedDB) this.getStoredData(); else this.getCookie(); }
else this.run();
} else this.run();
},
runDataStore: function () {
if (data) {
if (typeof data === 'string') try { data = JSON.parse(data); } catch(e) { data = {}; }
var q = { query: { users: [data] } };
if (data.timestamp) this.now = mw.now() - (data.timediff || 0);
if (data.timestamp && Math.abs((mw.now() - data.timestamp - (data.timediff || 0)) / 1000) < 90) return this.writeCommonInfo(q);
if (data.editcount) this.usprop = 'editcount';
}
data = data || {};
this.run();
},
run: function () {
if (this.self) {
return this.writeCommonInfo({ query: { users: [{ editcount: mw.config.get('wgUserEditCount'), registration: mw.config.get('wgUserRegistration'), groups: mw.config.get('wgUserGroups'), blockreason: '' }] } });
}
us.ajaxRequest({ curtimestamp: 1, list: 'users', ususers: us.user, usprop: us.usprop }, us.writeCommonInfo);
}
};
if ([2, 3].indexOf(mw.config.get('wgNamespaceNumber')) !== -1 && us.user.indexOf('/') === -1 && (!mw.config.get('wgArticleId') || mw.config.get('wgAction') === 'view')) {
$.when(mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.language', 'mediawiki.cookie', 'jquery.spinner', 'mediawiki.ForeignApi']), $.ready).then(function () {
us.api = mw.util.wikiScript('api') + '?action=query&format=json';
us.ajaxRequest('&meta=allmessages&amenableparser=1&amincludelocal=1&refix=group-', us.getGroupNames);
if (!mw.libs.viewerInfo) mw.loader.load('//meta.wikimedia.org/w/index.php?title=User:Perhelion/viewerInfo.js&action=raw&ctype=text/javascript');
});
}
}(jQuery, mediaWiki));
mrsk5v3o6ou05m98grxd2elscunbi0o
739938
739937
2026-05-01T09:11:04Z
MrJaroslavik
44012
test
739938
javascript
text/javascript
/**
* @Description: Modified Userstatus.js with Former Groups (Local & Global)
* Original by Steef389, Perhelion.
**/
(function ($, mw) {
'use strict';
var project = window.project || mw.config.get('wgDBname');
var msg, i18n = {
en: {
noReason: 'reason removed',
blockCmt: 'no block comment',
never: 'never',
not: ' not ',
block: 'blocked',
and: '$1 and$2',
count: 'Count',
noedit: 'None (only deleted contributions?)',
nodb: '‹not in database›',
noGrp: 'no extended group',
dates: ['years', 'months', 'days', 'hours', 'minutes', 'seconds'],
date: ['year', 'month', 'day', 'hour', 'minute', 'second'],
nosec: 'less than one ',
curtimeDiff: ' Locale time difference to server time in s: ',
thxGn: 'given',
thxRd: 'received',
nova: 'new',
ago: '$1 ago',
blocklog: 'Block log',
contrib: 'Edits: ',
usub: 'Subpages⬇',
thxGvng: 'Thanksgivings: ',
reviews: 'Active reviews: ',
regdate: 'Registration: ',
laedit: 'Last edited: ',
lala: 'Last log activity',
fiedit: 'First edit: ',
blocks: 'Block-status: ',
loGrp: 'Local user-groups: ',
glGrp: 'Global user-groups: ',
formerLoGrp: 'Former local user-groups: ',
formerGlGrp: 'Former global user-groups: ',
blockEnd: 'Block-end: ',
blocker: 'Blocker: ',
blockReason: 'Block-reason: '
},
cs: {
noReason: 'důvod odstraněn',
blockCmt: 'bez komentáře',
never: 'nikdy',
not: ' ne ',
block: 'zablokován',
and: '$1 a$2',
count: 'Počet',
noedit: 'Žádné (jen smazané?)',
nodb: '‹není v databázi›',
noGrp: 'žádná rozšířená skupina',
dates: ['let', 'měsíců', 'dní', 'hodin', 'minut', 'sekund'],
date: ['rok', 'měsíc', 'den', 'hodina', 'minuta', 'sekunda'],
nosec: 'méně než ',
curtimeDiff: ' Rozdíl lokálního času k serveru v s: ',
thxGn: 'poděkoval',
thxRd: 'obdržel poděkování',
nova: 'nové',
ago: 'před $1',
days: 'dní',
blocklog: 'Kniha zablokování',
contrib: 'Editace: ',
usub: 'Podstránky⬇',
thxGvng: 'Poděkování: ',
reviews: 'Aktivní prověření: ',
regdate: 'Registrace: ',
laedit: 'Poslední editace: ',
lala: 'Poslední aktivita v protokolech',
fiedit: 'První editace: ',
blocks: 'Stav bloku: ',
loGrp: 'Lokální skupiny: ',
glGrp: 'Globální skupiny: ',
formerLoGrp: 'Bývalé lokální skupiny: ',
formerGlGrp: 'Bývalé globální skupiny: ',
blockEnd: 'Konec bloku: ',
blocker: 'Zablokoval: ',
blockReason: 'Důvod bloku: '
}
};
var data;
var us = mw.libs.userstatus = {
name: 'Userstatus',
version: 1.80, // Bumping version for former groups update
lastEditSeconds: false,
styleMissingData: 'color:#999;font-size:90%;',
styleLoading: 'font-style:italic;',
styleBlocked: 'color:#c00',
styleNotBlocked: 'color:#080',
styleFormer: 'margin-left:15px; color:#72777d; font-style:italic; font-size:0.9em;',
viewPatrolNumber: false,
lang: mw.config.get('wgUserLanguage'),
user: mw.config.get('wgTitle'),
cookie: [],
thanks: 0,
patrols: 0,
actions: [],
dbVersion: 0,
implicitGroups: ['*', 'user', 'autoconfirmed'],
getLocalNames: function (groups, specialpage) {
if (groups) {
var arr = [];
specialpage = specialpage || 'ListGroupRights';
for (var i = 0; i < groups.length; ++i) {
var g = groups[i];
var n = us.groupNames[g];
if (n) arr.push('<a href="/wiki/Special:' + specialpage + '#' + g + '">' + n + '</a>');
else arr.push(g);
}
groups = arr.join(', ').replace(/(.*),([^,]*)$/, msg.and);
}
return groups;
},
writeGroups: function (groups) {
groups = us.getLocalNames(groups);
if (!groups) {
groups = $('<span>', {
style: us.styleMissingData + us.styleLoading,
text: msg.noGrp
});
}
us.loading_groups.replaceWith(groups);
// After local groups are written, fetch former local groups
us.fetchFormerRights(false);
},
getGroupNames: function (aw) {
if (!aw || !aw.query) return;
aw = aw.query.allmessages;
var al = aw.length;
var groups = {};
while (al--) {
var an = aw[al];
var name = an.name;
if (/^group-/.test(name)) {
name = RegExp.rightContext || name.substring(6);
groups[name] = an['*'];
}
}
us.groupNames = groups;
us.init();
},
ajaxRequest: function (params, onSuccess, trial) {
var url = us.api;
if (!(params instanceof Object)) {
url += params + '&maxage=2419200&smaxage=2419200';
params = {};
} else {
params.maxlag = 3;
params.maxage = 2419200;
params.smaxage = 2419200;
if (trial) params.maxlag *= trial;
}
var api = $.getJSON(url, params, onSuccess)
.fail(function (jqXHR, status) {
var note = 'Timeout fail, maybe try again!';
if (trial) {
if (!status.textStatus || (status.textStatus !== 'timeout' && jqXHR !== 'maxlag')) note = $('<span>Fehler bitte <a href="' + mw.util.getUrl('User_talk:Perhelion') + '">Perhelion</a> melden.</span>');
mw.notify(note, { title: us.name + ':', type: 'error' });
} else if (status.textStatus === 'timeout' || jqXHR === 'maxlag') {
api.abort();
return us.ajaxRequest(params, onSuccess, 2);
}
});
},
// --- NEW FORMER RIGHTS LOGIC ---
fetchFormerRights: function(isGlobal) {
var apiTarget = isGlobal ? 'https://meta.wikimedia.org/w/api.php' : mw.util.wikiScript('api');
var logType = isGlobal ? 'gblrights' : 'rights';
var userTarget = 'User:' + us.user;
var containerId = isGlobal ? 'us_former_global_groups' : 'us_former_local_groups';
var params = {
action: 'query',
list: 'logevents',
letype: logType,
letitle: userTarget,
lelimit: 500,
format: 'json',
origin: isGlobal ? '*' : undefined
};
$.getJSON(apiTarget, params, function(res) {
if(!res || !res.query || !res.query.logevents) return;
var logs = res.query.logevents;
var former = {};
var currentGroups = isGlobal ? (data.glGrp || []) : (data.groups || []);
logs.forEach(function(log) {
var p = log.params;
if (!p) return;
var date = log.timestamp;
// EXTRAKCE PRÁV: Podpora pro staré číselné indexy "0" a "1" i moderní pole
var oldRaw = p.oldgroups || p.oldGroups || p.remove || p["0"] || "";
var newRaw = p.newgroups || p.newGroups || p.add || p["1"] || "";
// Převod na pole (pokud je to string s čárkami nebo "(none)")
var parse = function(val) {
if (Array.isArray(val)) return val;
if (typeof val === 'string') {
if (val === "(none)" || val === "") return [];
return val.split(/[,|]\s*/);
}
return [];
};
var oldG = parse(oldRaw);
var newG = parse(newRaw);
// Detekce odebrání
oldG.forEach(function(g) {
g = g.trim();
if (us.implicitGroups.indexOf(g) !== -1 || !g) return;
if (currentGroups.indexOf(g) === -1 && !former[g]) {
former[g] = { to: date, from: null };
}
});
// Detekce udělení (hledání začátku)
newG.forEach(function(g) {
g = g.trim();
if (former[g] && !former[g].from) {
former[g].from = date;
}
});
});
var entries = [];
Object.keys(former).sort().forEach(function(g) {
var item = former[g];
var dateTo = new Date(item.to);
var dateFrom = item.from ? new Date(item.from) : null;
if(dateFrom) {
// Použijeme hlavní funkci pro výpočet rozdílu (odstraníme z ní slovo "před")
var diffText = us.getDateDiff(dateTo, dateFrom).replace(msg.ago.replace('$1', ''), '').trim();
entries.push(g + ' (' + dateFrom.toISOString().split('T')[0] + ' – ' + dateTo.toISOString().split('T')[0] + ', ' + diffText + ')');
} else {
entries.push(g + ' (do ' + dateTo.toISOString().split('T')[0] + ')');
}
});
if(entries.length > 0) {
$('#' + containerId).show().find('.us_former_content').text(entries.join(', '));
}
});
},
getThanks: function (e) {
var params = '&list=logevents&continue=-%7C%7C&leprop=&letype=thanks&ledir=newer&lelimit=max';
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
us.e.attr('title', us.e.text());
us.e.text('');
us.e.injectSpinner('thx');
us.thanks = 0;
} else if (e.lecontinue) {
params += '&lecontinue=' + e.lecontinue;
}
if (us.e.attr('title') === msg.thxGn) params += '&leuser=' + us.user;
else params += '&letitle=User:' + us.user;
}
us.actions.push({ params: params, func: us.writeThanks });
us.doRequest();
},
writeThanks: function (uq) {
if (!uq.query || !uq.query.logevents) return mw.log.warn(uq);
var e = us.e;
var user = 'page=';
var aw = uq.query.logevents;
us.thanks += aw.length;
if (uq.continue) return us.getThanks(uq.continue);
if (e[0].title === msg.thxGn) user += '&user=';
user += us.user;
$.removeSpinner('thx');
e.text(us.thanks);
e.off('click');
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special%3ALog&type=thanks&' + user + '&year=&month=-1&tagfilter=&hide_thanks_log=0';
delete us.e;
},
getUploads: function (e) {
var params = '&list=logevents&letype=upload&ledir=newer&lelimit=max&leuser=' + us.user;
function _start(u) {
var text = u.text();
u.prop('title', text);
u.text('');
u.injectSpinner('upload' + text);
u.data('upl', 0);
u.data('del', 0);
}
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
if (us.e.text() !== msg.nova) {
us.e2 = us.e.nextAll('a').eq(0);
if (us.e2.text() === msg.nova) _start(us.e2); else delete us.e2;
}
_start(us.e);
} else if (e.lecontinue) { params += '&lecontinue=' + e.lecontinue; }
params += '&leprop=ids';
if (us.e.prop('title') === msg.nova) params += '&leaction=upload/upload';
else if (us.e2) params += '|type';
}
us.actions.push({ params: params, func: us.writeUploads });
us.doRequest();
},
writeUploads: function (uq) {
if (!uq.query || !uq.query.logevents || !us.e) return mw.log(uq, us.e);
function _insert(e) {
$.removeSpinner('upload' + e.prop('title'));
e.text('');
e.off('click');
e.append(e.data('del'), $('<span>', { style: us.styleMissingData + us.styleLoading, text: ' (+' + (e.data('upl') - e.data('del')) + ' del.)' }));
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special:Log&type=upload&user=' + us.user + '&subtype=' + ((e[0].title === msg.nova) ? 'upload' : '');
}
var e = us.e;
var e2 = us.e2;
var aw = uq.query.logevents;
var alen = aw.length, i = 0;
var del = e.data('del');
if (e2) {
var upl = e2.data('upl');
var del2 = e2.data('del');
for (i; i < alen; ++i) {
var a = aw[i];
var c = a.pageid ? 1 : 0;
if (a.action === 'upload') { upl++; del2 += c; }
del += c;
}
e2.data('upl', upl);
e2.data('del', del2);
} else {
for (i; i < alen; ++i) if (aw[i].pageid) del++;
}
e.data('upl', e.data('upl') + alen);
e.data('del', del);
if (uq.continue) return us.getUploads(uq.continue);
_insert(e);
if (e2) { _insert(e2); delete us.e2; }
delete us.e;
},
writeCommonInfo: function (uq) {
var aw = uq.query;
if (!aw || !aw.users[0] || Object.prototype.hasOwnProperty.call(aw, 'missing') || Object.prototype.hasOwnProperty.call(aw.users[0], 'invalid')) return us.statusBox.remove();
aw = aw.users[0];
var edits = aw.editcount,
groups = data.groups || aw.groups,
blocked = data.blockreason || aw.blockexpiry,
gender = data.gender || aw.gender,
uploads = $('<a>', { title: msg.count, text: msg.nova, style: us.styleLoading, click: us.getUploads });
if (aw.registration) data.registration = aw.registration;
us.now = us.getDateFromTimestamp(uq.curtimestamp || us.now || mw.now());
us.user = us.user.replace(/ /g, '_');
us.first = 1;
if (edits) {
if (data.editcount && data.lastedit && data.editcount === edits) {
uq.query.usercontribs = [{ timestamp: data.lastedit }];
us.writeLastEdit(uq);
} else {
data.editcount = edits;
us.loading_last_edit.css('display', 'block');
us.actions.push({
params: '&list=usercontribs&ucuser=' + us.user + '&uclimit=1&uccontinue=' + (new Date(us.now).toISOString().replace(/[^\d]/g, '').slice(0, 14) + '|2'),
func: us.writeLastEdit
});
}
$('#t-contributions').remove();
} else { us.writeLastEdit({}); }
if (groups) {
edits = $('<a>', { title: 'Supercount', href: '//tools.wmflabs.org/supercount/index.php?user=' + us.user + '&project=' + location.hostname, text: edits });
if ($.inArray('bot', groups) === -1) {
us.review = $.inArray('editor', groups) !== -1;
if (us.review) {
if (typeof data.reviews === 'number') us.writePatrolCount(data.reviews);
} else if (us.viewPatrolNumber) { us.us_patrolcount_loading.parent().remove(); }
edits = [
edits, ' • ',
$('<a>', { href: '//xtools.wmflabs.org/ec' + mw.config.get('wgServer').substr(1) + '/' + us.user, title: 'XTools', text: 'XTools', style: us.styleLoading, target: '_blank' }), ' • ',
$('<a>', { href: '//commons.wikimedia.org/w/index.php?title=Commons:MyGallery/' + us.user + '&withJS=MediaWiki:JSONListUploads.js', title: 'MyGallery', text: (project === 'commonswiki' ? '' : 'c:') + 'MyGallery', style: us.styleLoading, target: '_blank' }), ' • ',
(project !== 'commonswiki' ? $('<a>', { href: '/wiki/Special:ListFiles/' + us.user, text: 'Upload', title: 'Special:Listfiles', style: us.styleLoading }) : 'Upload'),
(us.lang === 'de' ? '-' : ' ') + msg.count + ': ',
uploads.clone(1).text('total'), ' / ', uploads
];
if (!$('#t-subpages').length) {
edits.push($('<a>', { href: mw.util.getUrl('Special:Prefixindex/User:' + us.user + '/'), text: ' • ' + msg.usub, style: us.styleMissingData + us.styleLoading }));
}
} else if (us.us_patrolcount_loading) { us.us_patrolcount_loading.parent().remove(); }
us.loading_editcount.replaceWith(edits);
if (!aw.implicitgroups && !data.groups) {
aw.implicitgroups = ['*', 'user', 'autoconfirmed'];
if (groups.length < 4) aw.implicitgroups.pop();
}
data.groups = $.grep(groups, function (n) { return $.inArray(n, aw.implicitgroups) === -1; });
us.writeGroups(data.groups);
$('#t-userrights').remove();
}
if (!data.registration) {
us.actions.push({ params: '&list=logevents&leprop=timestamp|type&letype=newusers&lelimit=1&ledir=newer&leend=2005-12-30T00:00:00Z&leuser=' + us.user, func: us.writeRegistration });
if (data.firstedit) {
uq.query.usercontribs = [{ timestamp: data.firstedit }];
us.writeFirstEdit(uq);
} else if (edits) {
us.actions.push({ params: { list: 'usercontribs', ucuser: us.user, uclimit: 1, ucend: '2005-09-08T00:00:00Z', ucdir: 'newer', ucprop: 'timestamp' }, func: us.writeFirstEdit });
}
} else { us.writeRegistration(data.registration); }
us.loading_blocked.replaceWith($('<a>', { style: ((blocked) ? us.styleBlocked : us.styleNotBlocked), id: 'us_block_status_span', href: '/w/index.php?title=Special:Log/block&page=User:' + us.user, title: msg.blocklog, text: ((blocked) ? ' ' : msg.not) + msg.block }));
if (blocked) {
data.blockreason = aw.blockreason || ' ';
us.ul.append([
$('<li>', { id: 'us_block_time' }).append([$('<b>').text('• ' + msg.blockEnd), $.createSpinner('us_block_time_loading')]),
$('<li>', { id: 'us_block_reason' }).append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment, (aw.commenthidden))]),
$('<li>', { id: 'us_blocker' }).append([$('<b>').text('• ' + msg.blocker), $.createSpinner('us_blocker_loading')])
]);
us.actions.push({ params: { list: 'logevents', letitle: 'User:' + us.user, letype: 'block', lelimit: 1 }, func: us.writeBlockDetail });
}
if (data.glGrp && !data.locked) {
uq.query.globaluserinfo = { groups: (data.glGrp.length) ? data.glGrp : null };
us.writeGlobalGroup(uq);
} else {
us.actions.push({ params: '&meta=globaluserinfo&guiprop=groups&guiuser=' + us.user, func: us.writeGlobalGroup });
}
if (gender) {
data.gender = gender;
var genderSn = (gender === 'male') ? ' \u2642' : (gender === 'female' ? ' \u2640' : '');
$('#firstHeading').append($('<span>', { id: 'ps-gender-' + gender, style: 'font-size:80%', text: genderSn }));
}
if (us.actions.length) us.doRequest();
},
createBox: function () {
var statusBox = $('<div>', { style: 'border-bottom:1px solid #aaa;text-shadow:1px 1px 1px #eff;', id: 'us_box' });
if (mw.config.get('skin') === 'vector') statusBox.css('font-size', '0.8em');
var spanFrag = $.createSpinner();
us.loading_editcount = spanFrag.clone();
us.loading_registration = spanFrag.clone();
us.loading_groups = spanFrag.clone();
us.loading_blocked = spanFrag.clone();
us.us_global_group_loading = spanFrag.clone();
us.us_last_edit_loading = $.createSpinner('contribs');
us.loading_last_edit = $('<li>');
us.us_global_group = $('<li>', { style: 'display: none' });
var thx = $('<a>', { title: msg.count, text: msg.thxRd, style: us.styleLoading, click: us.getThanks });
var $userrights = $('#t-userrights a');
var $contributions = $('#t-contributions a');
if (!$userrights.length) $userrights = $('<a>', { href: '/wiki/Special:UserRights/' + us.user });
if (!$contributions.length) $contributions = $('<a>', { href: '/wiki/Special:Contributions/' + us.user });
var ul = $('<ul>', { style: 'list-style: none' }).append($('<li>', { id: 'us_editcount' }).append([$('<b>').append($contributions.text(msg.contrib)), us.loading_editcount]));
us.us_log_count = $('<span>');
if (us.viewPatrolNumber) {
us.us_patrolcount_loading = $('<a>', { style: us.styleLoading, click: us.getPatrolCount, text: msg.count });
us.us_log_count = $('<span>').append([$('<b>').text(msg.reviews), us.us_patrolcount_loading, ' • ']);
}
ul.append(us.us_log_count)
.append($('<li>', { id: 'us_reg_date' }).append([$('<b>').text(msg.regdate), us.loading_registration, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://tools.wmflabs.org/meta/userpages/' + us.user, text: '• User pages' })]))
.append($('<li>').append([$('<b>').append($userrights.text(msg.loGrp)), us.loading_groups, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://tools.wmflabs.org/meta/stalktoy/' + us.user, text: '• Stalk toy' })]))
// Container for former local groups
.append($('<li>', { id: 'us_former_local_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerLoGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.us_global_group.append([$('<b>').append($('<a>', { href: '/w/index.php?title=Special:GlobalUsers&limit=1&username=' + us.user, text: msg.glGrp })), us.us_global_group_loading, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://tools.wmflabs.org/meta/globalgroups/', text: '• GlobalGroups' })]))
// Container for former global groups
.append($('<li>', { id: 'us_former_global_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerGlGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.loading_last_edit.append([$('<b>').text(msg.laedit), us.us_last_edit_loading, ' ', $('<a>', { target: '_blank', style: us.styleMissingData, href: '/w/index.php?title=Special:Log/' + us.user + '&hide_thanks_log=0&hide_patrol_log=0&hide_tag_log=0', text: '• LLA' }), $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://tools.wmflabs.org/meta/crossactivity/' + us.user, text: '• CrossActivity' })]));
statusBox.append(ul);
us.ul = $('<ul>', { style: 'list-style:none' }).append($('<li>', { id: 'us_block_status' }).append([$('<b>').text(msg.blocks), us.loading_blocked, ' • ', $('<b>').text(msg.thxGvng), thx, ' / ', thx.clone(1).text(msg.thxGn), $('<a>', { style: 'float:right;' + us.styleMissingData, title: 'Purge data', text: 'purge', click: us.removeDataStore })]));
statusBox.append(us.ul);
$('#firstHeading').after(statusBox);
us.statusBox = statusBox;
},
removeDataStore: function (e) {
var key = project + us.user, db = window.indexedDB;
e.preventDefault();
if (!db || !us.dbVersion) return mw.cookie.set(us.name + us.user, null);
var request = db.open(us.name, us.dbVersion + 1);
request.onupgradeneeded = function () { db = this.result; if (db.objectStoreNames.contains(key)) db.deleteObjectStore(key); };
request.onsuccess = function () { db = this.result; us.dbVersion = db.version; db.close(); location.reload(); };
},
setCookie: function () {
var domain = (mw.config.get('wgNoticeProject') === 'wikipedia') ? 'wikipedia.org' : '';
var name = us.name + us.user;
var _saveCookie = function (k, d) { mw.cookie.set(name, JSON.stringify(d), { expires: 604800, domain: domain }); };
if (!us.actions[0] && JSON && data.editcount) {
if (us.cookie.length) window.clearTimeout(us.cookie.shift());
var saveData = window.indexedDB ? function (n, JSd, v) {
var key = project + us.user, db = window.indexedDB, request = db.open(us.name, v);
request.onupgradeneeded = function () {
db = this.result;
if (!db.objectStoreNames.contains(key)) {
var store = db.createObjectStore(key, { keyPath: 'name' });
store.createIndex('data', 'data', { unique: false });
}
};
request.onsuccess = function () {
db = this.result; us.dbVersion = db.version;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readwrite').objectStore(key);
store.put({ name: key, data: JSd });
} else { saveData(n, JSd, us.dbVersion + 1); }
};
request.onerror = function() { _saveCookie(n, JSd); };
} : _saveCookie;
us.cookie.push(setTimeout(function () {
data.timestamp = new Date().valueOf();
saveData('', data);
}, 500));
}
},
doRequest: function () {
var action = us.actions.shift();
if (action) us.ajaxRequest(action.params, action.func);
if (us.actions.length) window.setTimeout(function () { us.doRequest(); }, 100);
},
getDateDiff: function (now, date) {
var dStr = [];
var d = { years: 0, months: 0, days: 0 };
var diffDays = Math.floor(Math.abs(now - date) / 86400000);
if (now > date) {
var year = now.getFullYear();
var months = now.getMonth();
d.years = date.getFullYear();
date.setFullYear(year);
d.years = year - d.years;
if (date > now) {
d.years--;
date.setFullYear(year - 1);
}
var mDiff = months - date.getMonth();
if (mDiff < 0) mDiff += 12;
d.months = mDiff;
d.days = Math.floor((now - date) / 86400000) % 30;
var units = [d.years, d.months, d.days];
var labelSingle = [msg.date[0], msg.date[1], msg.date[2]];
var labelPlural = [msg.dates[0], msg.dates[1], msg.dates[2]];
for (var i = 0; i < 3; ++i) {
if (units[i]) {
dStr.push(units[i] + ' ' + ((units[i] > 1) ? labelPlural[i] : labelSingle[i]));
}
}
}
var mainDiff = (dStr.length) ? dStr.join(', ').replace(/(.*),([^,]*)/, msg.and).replace(/(.*)/, msg.ago) : msg.nosec + msg.date[2];
return mainDiff + (diffDays > 0 ? ' = ' + diffDays + ' ' + (msg.days || 'dní') : '');
},
getDateFromTimestamp: function (t) { if (!t) return false; t = new Date(t); return isNaN(t.valueOf()) ? false : t; },
writeRegistration: function (aw) {
if (aw) {
if (aw instanceof Object) {
if (aw.query && aw.query.logevents.length) aw = aw.query.logevents[0].timestamp;
}
if (typeof aw === 'string') {
us.loading_registration.replaceWith(us.formatDate(aw) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(aw)) + ')' }));
data.registration = aw;
return us.setCookie();
}
}
us.loading_registration.replaceWith($('<span>', { style: us.styleMissingData + us.styleLoading, text: msg.nodb }));
},
writeFirstEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs.length) return;
var date = data.firstedit = aw.query.usercontribs[0].timestamp;
$('#us_reg_date').append($('<li>').append([$('<b>', { text: '• ' + msg.fiedit }), us.formatDate(date) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(date)) + ')' })]));
},
writeLastEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs || !aw.query.usercontribs.length) return us.us_last_edit_loading.replaceWith($('<span>', { style: us.styleMissingData, text: msg.noedit }));
var date = data.lastedit = aw.query.usercontribs[0].timestamp;
us.us_last_edit_loading.replaceWith(us.getDateDiff(us.now, us.getDateFromTimestamp(date)), ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.formatDate(date) + ')' }));
data.timediff = mw.now() - us.now;
$('#us_editcount').append($('<span>', { style: 'float:right;' + us.styleMissingData, text: msg.curtimeDiff }).append($('<b>', { text: Math.round(data.timediff / 1000) })));
us.setCookie();
},
writeGlobalGroup: function (aw) {
aw = aw.query.globaluserinfo;
var groups = data.glGrp = aw.groups;
if (groups && groups.length) {
us.us_global_group_loading.replaceWith(us.getLocalNames(groups, 'GlobalGroupPermissions'));
us.us_global_group.css('display', 'block');
// Fetch former global rights
us.fetchFormerRights(true);
}
if (aw.locked !== undefined) {
data.locked = 1;
mw.loader.using('mediawiki.ForeignApi').done(function () {
var fApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php');
fApi.get({
action: 'query',
list: 'logevents',
leprop: 'user|timestamp|comment',
letype: 'globalauth',
letitle: 'User:' + us.user,
lelimit: '1'
}).done(us.writeGlobalBlock);
});
}
us.setCookie();
},
writeGlobalBlock: function (aw) {
if (!aw.query || !aw.query.logevents.length) return;
aw = aw.query.logevents[0];
$('#us_block_status_span').replaceWith($('<a>', { style: us.styleBlocked, id: 'us_block_status_span', href: '//meta.wikimedia.org/w/index.php?title=Special:Log/&type=globalauth&user=&page=User%3A' + us.user + '%40global', text: 'locked (global)' }).append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) })));
$('#us_block_status').parent().append([$('<li>').append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment)]), $('<li>').append([$('<b>').text('• ' + msg.blocker), $('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user })])]);
},
writeBlockDetail: function (aw) {
var duration = 'infinite', expiry = '';
if (aw.query && aw.query.logevents.length) {
aw = aw.query.logevents[0];
if (aw && aw.params) {
duration = aw.params.duration;
expiry = /in(de)?finite/.test(duration) ? msg.never : us.formatDate(aw.params.expiry);
}
$('#us_block_status_span').text(msg.block + ' (' + duration + ')').append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) }));
if (expiry) $('#mw-spinner-us_block_time_loading').replaceWith(expiry); else $('#us_block_time').remove();
$('#mw-spinner-us_block_reason_loading').replaceWith(us.parseComment(aw.comment, aw.commenthidden));
if (aw.user) $('#mw-spinner-us_blocker_loading').replaceWith($('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user }));
else $('#mw-spinner-us_blocker_loading').replaceWith(msg.nodb);
}
},
writePatrolCount: function (aw) {
if (aw instanceof Object) {
if (!aw.query || !aw.query.logevents) return;
us.patrols += aw.query.logevents.length;
if (aw.continue) return us.getPatrolCount(aw.continue);
$.removeSpinner('pat');
aw = us.patrols;
}
us.us_patrolcount_loading.replaceWith($('<a>', { href: '/w/index.php?title=Special:Log&type=' + (us.review ? 'review&subtype=accept' : 'patrol&subtype=patrol') + '&user=' + us.user, text: aw }));
data.reviews = aw;
us.setCookie();
},
getPatrolCount: function (e) {
var params = '&list=logevents&leprop=&ledir=newer&lelimit=max&leuser=' + us.user + '&leaction=' + (us.review ? 'review/approve' : 'patrol/patrol');
if (e instanceof Object) {
if (e.target) { $(e.target).injectSpinner('pat'); us.patrols = 0; }
else if (e.lecontinue) params += '&lecontinue=' + e.lecontinue;
}
us.actions.push({ params: params, func: us.writePatrolCount });
us.doRequest();
},
parseComment: function (text, hidden) {
var comment = $('<span>', { 'class': 'comment' });
if (typeof text === 'undefined') return comment.append(hidden ? msg.noReason : 'undefined').css('color', hidden ? '#999' : 'red');
if (!text) return comment.append(msg.blockCmt).css('color', '#999');
var intLink = /(.*?)\[\[((.*?)\|)?(.*?)\]\](.*)/, suche = text, erg;
while ((erg = intLink.exec(suche)) !== null) {
var link = erg[3] || erg[4];
comment.append([erg[1], $('<a>', { href: mw.util.getUrl(link), title: link }).text(erg[4])]);
suche = erg[5];
}
return comment.append(suche);
},
formatDate: function (datum) {
if (!(datum instanceof Date)) datum = new Date(datum);
try {
return datum.toLocaleDateString(us.lang, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
} catch (e) { return datum.toLocaleString(); }
},
getStoredData: function () {
var key = project + us.user, db, request;
try {
request = indexedDB.open(us.name);
request.onsuccess = function () {
db = this.result;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readonly').objectStore(key);
store.transaction.oncomplete = function () { us.runDataStore(); db.close(); };
store.get(key).onsuccess = function () { if (this.result) data = this.result.data; else us.getCookie(); };
} else { us.getCookie(); }
};
request.onerror = function () { us.getCookie(); };
} catch (e) { us.getCookie(); }
},
getCookie: function () {
data = mw.cookie.get(us.name + us.user);
if (data) try { data = JSON.parse(data); } catch(e) { data = null; }
us.runDataStore();
},
initI18N: function (i18n) {
var chain = mw.language.getFallbackLanguageChain();
for (var i = chain.length - 1; i >= 0; i--) if (chain[i] in i18n) msg = i18n[chain[i]];
},
init: function () {
if (this.statusBox || (this.statusBox = $('#us_box'))[0]) this.statusBox.remove();
this.usprop = 'blockinfo|editcount|gender|groups|implicitgroups|registration';
this.self = mw.config.get('wgUserName') === this.user;
this.initI18N(i18n);
this.createBox();
if (!this.first) {
if (mw.cookie && JSON) { if (window.indexedDB) this.getStoredData(); else this.getCookie(); }
else this.run();
} else this.run();
},
runDataStore: function () {
if (data) {
if (typeof data === 'string') try { data = JSON.parse(data); } catch(e) { data = {}; }
var q = { query: { users: [data] } };
if (data.timestamp) this.now = mw.now() - (data.timediff || 0);
if (data.timestamp && Math.abs((mw.now() - data.timestamp - (data.timediff || 0)) / 1000) < 90) return this.writeCommonInfo(q);
if (data.editcount) this.usprop = 'editcount';
}
data = data || {};
this.run();
},
run: function () {
if (this.self) {
return this.writeCommonInfo({ query: { users: [{ editcount: mw.config.get('wgUserEditCount'), registration: mw.config.get('wgUserRegistration'), groups: mw.config.get('wgUserGroups'), blockreason: '' }] } });
}
us.ajaxRequest({ curtimestamp: 1, list: 'users', ususers: us.user, usprop: us.usprop }, us.writeCommonInfo);
}
};
if ([2, 3].indexOf(mw.config.get('wgNamespaceNumber')) !== -1 && us.user.indexOf('/') === -1 && (!mw.config.get('wgArticleId') || mw.config.get('wgAction') === 'view')) {
$.when(mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.language', 'mediawiki.cookie', 'jquery.spinner', 'mediawiki.ForeignApi']), $.ready).then(function () {
us.api = mw.util.wikiScript('api') + '?action=query&format=json';
us.ajaxRequest('&meta=allmessages&amenableparser=1&amincludelocal=1&refix=group-', us.getGroupNames);
if (!mw.libs.viewerInfo) mw.loader.load('//meta.wikimedia.org/w/index.php?title=User:Perhelion/viewerInfo.js&action=raw&ctype=text/javascript');
});
}
}(jQuery, mediaWiki));
pty4wbptbl5u3knfokf7lxbcl2dhaod
739939
739938
2026-05-01T09:18:29Z
MrJaroslavik
44012
test
739939
javascript
text/javascript
/**
* @Description: Modified Userstatus.js with Former Groups (Local & Global)
* Original by Steef389, Perhelion.
**/
(function ($, mw) {
'use strict';
var project = window.project || mw.config.get('wgDBname');
var msg, i18n = {
en: {
noReason: 'reason removed',
blockCmt: 'no block comment',
never: 'never',
not: ' not ',
block: 'blocked',
and: '$1 and$2',
count: 'Count',
noedit: 'None (only deleted contributions?)',
nodb: '‹not in database›',
noGrp: 'no extended group',
dates: ['years', 'months', 'days', 'hours', 'minutes', 'seconds'],
date: ['year', 'month', 'day', 'hour', 'minute', 'second'],
nosec: 'less than one ',
curtimeDiff: ' Locale time difference to server time in s: ',
thxGn: 'given',
thxRd: 'received',
nova: 'new',
ago: '$1 ago',
blocklog: 'Block log',
contrib: 'Edits: ',
usub: 'Subpages⬇',
thxGvng: 'Thanksgivings: ',
reviews: 'Active reviews: ',
regdate: 'Registration: ',
laedit: 'Last edited: ',
lala: 'Last log activity',
fiedit: 'First edit: ',
blocks: 'Block-status: ',
loGrp: 'Local user-groups: ',
glGrp: 'Global user-groups: ',
formerLoGrp: 'Former local user-groups: ',
formerGlGrp: 'Former global user-groups: ',
blockEnd: 'Block-end: ',
blocker: 'Blocker: ',
blockReason: 'Block-reason: '
},
cs: {
noReason: 'důvod odstraněn',
blockCmt: 'bez komentáře',
never: 'nikdy',
not: ' ne ',
block: 'zablokován',
and: '$1 a$2',
count: 'Počet',
noedit: 'Žádné (jen smazané?)',
nodb: '‹není v databázi›',
noGrp: 'žádná rozšířená skupina',
dates: ['let', 'měsíců', 'dní', 'hodin', 'minut', 'sekund'],
date: ['rok', 'měsíc', 'den', 'hodina', 'minuta', 'sekunda'],
nosec: 'méně než ',
curtimeDiff: ' Rozdíl lokálního času k serveru v s: ',
thxGn: 'poděkoval',
thxRd: 'obdržel poděkování',
nova: 'nové',
ago: 'před $1',
days: 'dní',
blocklog: 'Kniha zablokování',
contrib: 'Editace: ',
usub: 'Podstránky⬇',
thxGvng: 'Poděkování: ',
reviews: 'Aktivní prověření: ',
regdate: 'Registrace: ',
laedit: 'Poslední editace: ',
lala: 'Poslední aktivita v protokolech',
fiedit: 'První editace: ',
blocks: 'Stav bloku: ',
loGrp: 'Lokální skupiny: ',
glGrp: 'Globální skupiny: ',
formerLoGrp: 'Bývalé lokální skupiny: ',
formerGlGrp: 'Bývalé globální skupiny: ',
blockEnd: 'Konec bloku: ',
blocker: 'Zablokoval: ',
blockReason: 'Důvod bloku: '
}
};
var data;
var us = mw.libs.userstatus = {
name: 'Userstatus',
version: 1.80, // Bumping version for former groups update
lastEditSeconds: false,
styleMissingData: 'color:#999;font-size:90%;',
styleLoading: 'font-style:italic;',
styleBlocked: 'color:#c00',
styleNotBlocked: 'color:#080',
styleFormer: 'margin-left:15px; color:#72777d; font-style:italic; font-size:0.9em;',
viewPatrolNumber: false,
lang: mw.config.get('wgUserLanguage'),
user: mw.config.get('wgTitle'),
cookie: [],
thanks: 0,
patrols: 0,
actions: [],
dbVersion: 0,
implicitGroups: ['*', 'user', 'autoconfirmed'],
getLocalNames: function (groups, specialpage) {
if (groups) {
var arr = [];
specialpage = specialpage || 'ListGroupRights';
for (var i = 0; i < groups.length; ++i) {
var g = groups[i];
var n = us.groupNames[g];
if (n) arr.push('<a href="/wiki/Special:' + specialpage + '#' + g + '">' + n + '</a>');
else arr.push(g);
}
groups = arr.join(', ').replace(/(.*),([^,]*)$/, msg.and);
}
return groups;
},
writeGroups: function (groups) {
groups = us.getLocalNames(groups);
if (!groups) {
groups = $('<span>', {
style: us.styleMissingData + us.styleLoading,
text: msg.noGrp
});
}
us.loading_groups.replaceWith(groups);
// After local groups are written, fetch former local groups
us.fetchFormerRights(false);
},
getGroupNames: function (aw) {
if (!aw || !aw.query) return;
aw = aw.query.allmessages;
var al = aw.length;
var groups = {};
while (al--) {
var an = aw[al];
var name = an.name;
if (/^group-/.test(name)) {
name = RegExp.rightContext || name.substring(6);
groups[name] = an['*'];
}
}
us.groupNames = groups;
us.init();
},
ajaxRequest: function (params, onSuccess, trial) {
var url = us.api;
if (!(params instanceof Object)) {
url += params + '&maxage=2419200&smaxage=2419200';
params = {};
} else {
params.maxlag = 3;
params.maxage = 2419200;
params.smaxage = 2419200;
if (trial) params.maxlag *= trial;
}
var api = $.getJSON(url, params, onSuccess)
.fail(function (jqXHR, status) {
var note = 'Timeout fail, maybe try again!';
if (trial) {
if (!status.textStatus || (status.textStatus !== 'timeout' && jqXHR !== 'maxlag')) note = $('<span>Fehler bitte <a href="' + mw.util.getUrl('User_talk:Perhelion') + '">Perhelion</a> melden.</span>');
mw.notify(note, { title: us.name + ':', type: 'error' });
} else if (status.textStatus === 'timeout' || jqXHR === 'maxlag') {
api.abort();
return us.ajaxRequest(params, onSuccess, 2);
}
});
},
// --- NEW FORMER RIGHTS LOGIC ---
fetchFormerRights: function(isGlobal) {
var apiTarget = isGlobal ? 'https://meta.wikimedia.org/w/api.php' : mw.util.wikiScript('api');
var logType = isGlobal ? 'gblrights' : 'rights';
var userTarget = 'User:' + us.user;
var containerId = isGlobal ? 'us_former_global_groups' : 'us_former_local_groups';
// Globální override pro smazané skupiny (skupina: datum smazání)
var globalOverrides = {
'oathauth-tester': '2026-01-12T10:58:00Z'
};
var params = {
action: 'query',
list: 'logevents',
letype: logType,
letitle: userTarget,
lelimit: 500,
format: 'json',
origin: isGlobal ? '*' : undefined
};
$.getJSON(apiTarget, params, function(res) {
if(!res || !res.query || !res.query.logevents) return;
var logs = res.query.logevents;
var former = {};
var currentGroups = isGlobal ? (data.glGrp || []) : (data.groups || []);
logs.forEach(function(log) {
var p = log.params;
if (!p) return;
var date = log.timestamp;
var oldRaw = p.oldgroups || p.oldGroups || p.remove || p["0"] || "";
var newRaw = p.newgroups || p.newGroups || p.add || p["1"] || "";
var parse = function(val) {
if (Array.isArray(val)) return val;
if (typeof val === 'string') {
if (val === "(none)" || val === "") return [];
return val.split(/[,|]\s*/);
}
return [];
};
var oldG = parse(oldRaw);
var newG = parse(newRaw);
oldG.forEach(function(g) {
g = g.trim();
if (us.implicitGroups.indexOf(g) !== -1 || !g) return;
if (currentGroups.indexOf(g) === -1 && !former[g]) {
former[g] = { to: date, from: null };
}
});
newG.forEach(function(g) {
g = g.trim();
if (former[g] && !former[g].from) former[g].from = date;
});
});
var entries = [];
Object.keys(former).sort().forEach(function(g) {
var item = former[g];
// Aplikace globálního overridu, pokud chybí datum odebrání a skupina je v seznamu smazaných
if (!item.to && globalOverrides[g]) {
item.to = globalOverrides[g];
}
var dateTo = new Date(item.to);
var dateFrom = item.from ? new Date(item.from) : null;
if(dateFrom && item.to) {
var diffText = us.getDateDiff(dateTo, dateFrom).replace(msg.ago.replace('$1', ''), '').trim();
entries.push(g + ' (' + dateFrom.toISOString().split('T')[0] + ' – ' + dateTo.toISOString().split('T')[0] + ', ' + diffText + ')');
} else if (item.to) {
entries.push(g + ' (do ' + dateTo.toISOString().split('T')[0] + ')');
}
});
if(entries.length > 0) {
$('#' + containerId).show().find('.us_former_content').text(entries.join(', '));
}
});
},
getThanks: function (e) {
var params = '&list=logevents&continue=-%7C%7C&leprop=&letype=thanks&ledir=newer&lelimit=max';
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
us.e.attr('title', us.e.text());
us.e.text('');
us.e.injectSpinner('thx');
us.thanks = 0;
} else if (e.lecontinue) {
params += '&lecontinue=' + e.lecontinue;
}
if (us.e.attr('title') === msg.thxGn) params += '&leuser=' + us.user;
else params += '&letitle=User:' + us.user;
}
us.actions.push({ params: params, func: us.writeThanks });
us.doRequest();
},
writeThanks: function (uq) {
if (!uq.query || !uq.query.logevents) return mw.log.warn(uq);
var e = us.e;
var user = 'page=';
var aw = uq.query.logevents;
us.thanks += aw.length;
if (uq.continue) return us.getThanks(uq.continue);
if (e[0].title === msg.thxGn) user += '&user=';
user += us.user;
$.removeSpinner('thx');
e.text(us.thanks);
e.off('click');
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special%3ALog&type=thanks&' + user + '&year=&month=-1&tagfilter=&hide_thanks_log=0';
delete us.e;
},
getUploads: function (e) {
var params = '&list=logevents&letype=upload&ledir=newer&lelimit=max&leuser=' + us.user;
function _start(u) {
var text = u.text();
u.prop('title', text);
u.text('');
u.injectSpinner('upload' + text);
u.data('upl', 0);
u.data('del', 0);
}
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
if (us.e.text() !== msg.nova) {
us.e2 = us.e.nextAll('a').eq(0);
if (us.e2.text() === msg.nova) _start(us.e2); else delete us.e2;
}
_start(us.e);
} else if (e.lecontinue) { params += '&lecontinue=' + e.lecontinue; }
params += '&leprop=ids';
if (us.e.prop('title') === msg.nova) params += '&leaction=upload/upload';
else if (us.e2) params += '|type';
}
us.actions.push({ params: params, func: us.writeUploads });
us.doRequest();
},
writeUploads: function (uq) {
if (!uq.query || !uq.query.logevents || !us.e) return mw.log(uq, us.e);
function _insert(e) {
$.removeSpinner('upload' + e.prop('title'));
e.text('');
e.off('click');
e.append(e.data('del'), $('<span>', { style: us.styleMissingData + us.styleLoading, text: ' (+' + (e.data('upl') - e.data('del')) + ' del.)' }));
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special:Log&type=upload&user=' + us.user + '&subtype=' + ((e[0].title === msg.nova) ? 'upload' : '');
}
var e = us.e;
var e2 = us.e2;
var aw = uq.query.logevents;
var alen = aw.length, i = 0;
var del = e.data('del');
if (e2) {
var upl = e2.data('upl');
var del2 = e2.data('del');
for (i; i < alen; ++i) {
var a = aw[i];
var c = a.pageid ? 1 : 0;
if (a.action === 'upload') { upl++; del2 += c; }
del += c;
}
e2.data('upl', upl);
e2.data('del', del2);
} else {
for (i; i < alen; ++i) if (aw[i].pageid) del++;
}
e.data('upl', e.data('upl') + alen);
e.data('del', del);
if (uq.continue) return us.getUploads(uq.continue);
_insert(e);
if (e2) { _insert(e2); delete us.e2; }
delete us.e;
},
writeCommonInfo: function (uq) {
var aw = uq.query;
if (!aw || !aw.users[0] || Object.prototype.hasOwnProperty.call(aw, 'missing') || Object.prototype.hasOwnProperty.call(aw.users[0], 'invalid')) return us.statusBox.remove();
aw = aw.users[0];
var edits = aw.editcount,
groups = data.groups || aw.groups,
blocked = data.blockreason || aw.blockexpiry,
gender = data.gender || aw.gender,
uploads = $('<a>', { title: msg.count, text: msg.nova, style: us.styleLoading, click: us.getUploads });
if (aw.registration) data.registration = aw.registration;
us.now = us.getDateFromTimestamp(uq.curtimestamp || us.now || mw.now());
us.user = us.user.replace(/ /g, '_');
us.first = 1;
if (edits) {
if (data.editcount && data.lastedit && data.editcount === edits) {
uq.query.usercontribs = [{ timestamp: data.lastedit }];
us.writeLastEdit(uq);
} else {
data.editcount = edits;
us.loading_last_edit.css('display', 'block');
us.actions.push({
params: '&list=usercontribs&ucuser=' + us.user + '&uclimit=1&uccontinue=' + (new Date(us.now).toISOString().replace(/[^\d]/g, '').slice(0, 14) + '|2'),
func: us.writeLastEdit
});
}
$('#t-contributions').remove();
} else { us.writeLastEdit({}); }
if (groups) {
edits = $('<a>', { title: 'Supercount', href: '//tools.wmflabs.org/supercount/index.php?user=' + us.user + '&project=' + location.hostname, text: edits });
if ($.inArray('bot', groups) === -1) {
us.review = $.inArray('editor', groups) !== -1;
if (us.review) {
if (typeof data.reviews === 'number') us.writePatrolCount(data.reviews);
} else if (us.viewPatrolNumber) { us.us_patrolcount_loading.parent().remove(); }
edits = [
edits, ' • ',
$('<a>', { href: '//xtools.wmflabs.org/ec' + mw.config.get('wgServer').substr(1) + '/' + us.user, title: 'XTools', text: 'XTools', style: us.styleLoading, target: '_blank' }), ' • ',
$('<a>', { href: '//commons.wikimedia.org/w/index.php?title=Commons:MyGallery/' + us.user + '&withJS=MediaWiki:JSONListUploads.js', title: 'MyGallery', text: (project === 'commonswiki' ? '' : 'c:') + 'MyGallery', style: us.styleLoading, target: '_blank' }), ' • ',
(project !== 'commonswiki' ? $('<a>', { href: '/wiki/Special:ListFiles/' + us.user, text: 'Upload', title: 'Special:Listfiles', style: us.styleLoading }) : 'Upload'),
(us.lang === 'de' ? '-' : ' ') + msg.count + ': ',
uploads.clone(1).text('total'), ' / ', uploads
];
if (!$('#t-subpages').length) {
edits.push($('<a>', { href: mw.util.getUrl('Special:Prefixindex/User:' + us.user + '/'), text: ' • ' + msg.usub, style: us.styleMissingData + us.styleLoading }));
}
} else if (us.us_patrolcount_loading) { us.us_patrolcount_loading.parent().remove(); }
us.loading_editcount.replaceWith(edits);
if (!aw.implicitgroups && !data.groups) {
aw.implicitgroups = ['*', 'user', 'autoconfirmed'];
if (groups.length < 4) aw.implicitgroups.pop();
}
data.groups = $.grep(groups, function (n) { return $.inArray(n, aw.implicitgroups) === -1; });
us.writeGroups(data.groups);
$('#t-userrights').remove();
}
if (!data.registration) {
us.actions.push({ params: '&list=logevents&leprop=timestamp|type&letype=newusers&lelimit=1&ledir=newer&leend=2005-12-30T00:00:00Z&leuser=' + us.user, func: us.writeRegistration });
if (data.firstedit) {
uq.query.usercontribs = [{ timestamp: data.firstedit }];
us.writeFirstEdit(uq);
} else if (edits) {
us.actions.push({ params: { list: 'usercontribs', ucuser: us.user, uclimit: 1, ucend: '2005-09-08T00:00:00Z', ucdir: 'newer', ucprop: 'timestamp' }, func: us.writeFirstEdit });
}
} else { us.writeRegistration(data.registration); }
us.loading_blocked.replaceWith($('<a>', { style: ((blocked) ? us.styleBlocked : us.styleNotBlocked), id: 'us_block_status_span', href: '/w/index.php?title=Special:Log/block&page=User:' + us.user, title: msg.blocklog, text: ((blocked) ? ' ' : msg.not) + msg.block }));
if (blocked) {
data.blockreason = aw.blockreason || ' ';
us.ul.append([
$('<li>', { id: 'us_block_time' }).append([$('<b>').text('• ' + msg.blockEnd), $.createSpinner('us_block_time_loading')]),
$('<li>', { id: 'us_block_reason' }).append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment, (aw.commenthidden))]),
$('<li>', { id: 'us_blocker' }).append([$('<b>').text('• ' + msg.blocker), $.createSpinner('us_blocker_loading')])
]);
us.actions.push({ params: { list: 'logevents', letitle: 'User:' + us.user, letype: 'block', lelimit: 1 }, func: us.writeBlockDetail });
}
if (data.glGrp && !data.locked) {
uq.query.globaluserinfo = { groups: (data.glGrp.length) ? data.glGrp : null };
us.writeGlobalGroup(uq);
} else {
us.actions.push({ params: '&meta=globaluserinfo&guiprop=groups&guiuser=' + us.user, func: us.writeGlobalGroup });
}
if (gender) {
data.gender = gender;
var genderSn = (gender === 'male') ? ' \u2642' : (gender === 'female' ? ' \u2640' : '');
$('#firstHeading').append($('<span>', { id: 'ps-gender-' + gender, style: 'font-size:80%', text: genderSn }));
}
if (us.actions.length) us.doRequest();
},
createBox: function () {
var statusBox = $('<div>', { style: 'border-bottom:1px solid #aaa;text-shadow:1px 1px 1px #eff;', id: 'us_box' });
if (mw.config.get('skin') === 'vector') statusBox.css('font-size', '0.8em');
var spanFrag = $.createSpinner();
us.loading_editcount = spanFrag.clone();
us.loading_registration = spanFrag.clone();
us.loading_groups = spanFrag.clone();
us.loading_blocked = spanFrag.clone();
us.us_global_group_loading = spanFrag.clone();
us.us_last_edit_loading = $.createSpinner('contribs');
us.loading_last_edit = $('<li>');
us.us_global_group = $('<li>', { style: 'display: none' });
var thx = $('<a>', { title: msg.count, text: msg.thxRd, style: us.styleLoading, click: us.getThanks });
var $userrights = $('#t-userrights a');
var $contributions = $('#t-contributions a');
if (!$userrights.length) $userrights = $('<a>', { href: '/wiki/Special:UserRights/' + us.user });
if (!$contributions.length) $contributions = $('<a>', { href: '/wiki/Special:Contributions/' + us.user });
var ul = $('<ul>', { style: 'list-style: none' }).append($('<li>', { id: 'us_editcount' }).append([$('<b>').append($contributions.text(msg.contrib)), us.loading_editcount]));
us.us_log_count = $('<span>');
if (us.viewPatrolNumber) {
us.us_patrolcount_loading = $('<a>', { style: us.styleLoading, click: us.getPatrolCount, text: msg.count });
us.us_log_count = $('<span>').append([$('<b>').text(msg.reviews), us.us_patrolcount_loading, ' • ']);
}
ul.append(us.us_log_count)
.append($('<li>', { id: 'us_reg_date' }).append([$('<b>').text(msg.regdate), us.loading_registration, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://tools.wmflabs.org/meta/userpages/' + us.user, text: '• User pages' })]))
.append($('<li>').append([$('<b>').append($userrights.text(msg.loGrp)), us.loading_groups, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://tools.wmflabs.org/meta/stalktoy/' + us.user, text: '• Stalk toy' })]))
// Container for former local groups
.append($('<li>', { id: 'us_former_local_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerLoGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.us_global_group.append([$('<b>').append($('<a>', { href: '/w/index.php?title=Special:GlobalUsers&limit=1&username=' + us.user, text: msg.glGrp })), us.us_global_group_loading, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://tools.wmflabs.org/meta/globalgroups/', text: '• GlobalGroups' })]))
// Container for former global groups
.append($('<li>', { id: 'us_former_global_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerGlGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.loading_last_edit.append([$('<b>').text(msg.laedit), us.us_last_edit_loading, ' ', $('<a>', { target: '_blank', style: us.styleMissingData, href: '/w/index.php?title=Special:Log/' + us.user + '&hide_thanks_log=0&hide_patrol_log=0&hide_tag_log=0', text: '• LLA' }), $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://tools.wmflabs.org/meta/crossactivity/' + us.user, text: '• CrossActivity' })]));
statusBox.append(ul);
us.ul = $('<ul>', { style: 'list-style:none' }).append($('<li>', { id: 'us_block_status' }).append([$('<b>').text(msg.blocks), us.loading_blocked, ' • ', $('<b>').text(msg.thxGvng), thx, ' / ', thx.clone(1).text(msg.thxGn), $('<a>', { style: 'float:right;' + us.styleMissingData, title: 'Purge data', text: 'purge', click: us.removeDataStore })]));
statusBox.append(us.ul);
$('#firstHeading').after(statusBox);
us.statusBox = statusBox;
},
removeDataStore: function (e) {
var key = project + us.user, db = window.indexedDB;
e.preventDefault();
if (!db || !us.dbVersion) return mw.cookie.set(us.name + us.user, null);
var request = db.open(us.name, us.dbVersion + 1);
request.onupgradeneeded = function () { db = this.result; if (db.objectStoreNames.contains(key)) db.deleteObjectStore(key); };
request.onsuccess = function () { db = this.result; us.dbVersion = db.version; db.close(); location.reload(); };
},
setCookie: function () {
var domain = (mw.config.get('wgNoticeProject') === 'wikipedia') ? 'wikipedia.org' : '';
var name = us.name + us.user;
var _saveCookie = function (k, d) { mw.cookie.set(name, JSON.stringify(d), { expires: 604800, domain: domain }); };
if (!us.actions[0] && JSON && data.editcount) {
if (us.cookie.length) window.clearTimeout(us.cookie.shift());
var saveData = window.indexedDB ? function (n, JSd, v) {
var key = project + us.user, db = window.indexedDB, request = db.open(us.name, v);
request.onupgradeneeded = function () {
db = this.result;
if (!db.objectStoreNames.contains(key)) {
var store = db.createObjectStore(key, { keyPath: 'name' });
store.createIndex('data', 'data', { unique: false });
}
};
request.onsuccess = function () {
db = this.result; us.dbVersion = db.version;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readwrite').objectStore(key);
store.put({ name: key, data: JSd });
} else { saveData(n, JSd, us.dbVersion + 1); }
};
request.onerror = function() { _saveCookie(n, JSd); };
} : _saveCookie;
us.cookie.push(setTimeout(function () {
data.timestamp = new Date().valueOf();
saveData('', data);
}, 500));
}
},
doRequest: function () {
var action = us.actions.shift();
if (action) us.ajaxRequest(action.params, action.func);
if (us.actions.length) window.setTimeout(function () { us.doRequest(); }, 100);
},
getDateDiff: function (now, date) {
var dStr = [];
var d = { years: 0, months: 0, days: 0 };
var diffDays = Math.floor(Math.abs(now - date) / 86400000);
if (now > date) {
var year = now.getFullYear();
var months = now.getMonth();
d.years = date.getFullYear();
date.setFullYear(year);
d.years = year - d.years;
if (date > now) {
d.years--;
date.setFullYear(year - 1);
}
var mDiff = months - date.getMonth();
if (mDiff < 0) mDiff += 12;
d.months = mDiff;
d.days = Math.floor((now - date) / 86400000) % 30;
var units = [d.years, d.months, d.days];
var labelSingle = [msg.date[0], msg.date[1], msg.date[2]];
var labelPlural = [msg.dates[0], msg.dates[1], msg.dates[2]];
for (var i = 0; i < 3; ++i) {
if (units[i]) {
dStr.push(units[i] + ' ' + ((units[i] > 1) ? labelPlural[i] : labelSingle[i]));
}
}
}
var mainDiff = (dStr.length) ? dStr.join(', ').replace(/(.*),([^,]*)/, msg.and).replace(/(.*)/, msg.ago) : msg.nosec + msg.date[2];
return mainDiff + (diffDays > 0 ? ' = ' + diffDays + ' ' + (msg.days || 'dní') : '');
},
getDateFromTimestamp: function (t) { if (!t) return false; t = new Date(t); return isNaN(t.valueOf()) ? false : t; },
writeRegistration: function (aw) {
if (aw) {
if (aw instanceof Object) {
if (aw.query && aw.query.logevents.length) aw = aw.query.logevents[0].timestamp;
}
if (typeof aw === 'string') {
us.loading_registration.replaceWith(us.formatDate(aw) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(aw)) + ')' }));
data.registration = aw;
return us.setCookie();
}
}
us.loading_registration.replaceWith($('<span>', { style: us.styleMissingData + us.styleLoading, text: msg.nodb }));
},
writeFirstEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs.length) return;
var date = data.firstedit = aw.query.usercontribs[0].timestamp;
$('#us_reg_date').append($('<li>').append([$('<b>', { text: '• ' + msg.fiedit }), us.formatDate(date) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(date)) + ')' })]));
},
writeLastEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs || !aw.query.usercontribs.length) return us.us_last_edit_loading.replaceWith($('<span>', { style: us.styleMissingData, text: msg.noedit }));
var date = data.lastedit = aw.query.usercontribs[0].timestamp;
us.us_last_edit_loading.replaceWith(us.getDateDiff(us.now, us.getDateFromTimestamp(date)), ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.formatDate(date) + ')' }));
data.timediff = mw.now() - us.now;
$('#us_editcount').append($('<span>', { style: 'float:right;' + us.styleMissingData, text: msg.curtimeDiff }).append($('<b>', { text: Math.round(data.timediff / 1000) })));
us.setCookie();
},
writeGlobalGroup: function (aw) {
aw = aw.query.globaluserinfo;
var groups = data.glGrp = aw.groups;
if (groups && groups.length) {
us.us_global_group_loading.replaceWith(us.getLocalNames(groups, 'GlobalGroupPermissions'));
us.us_global_group.css('display', 'block');
// Fetch former global rights
us.fetchFormerRights(true);
}
if (aw.locked !== undefined) {
data.locked = 1;
mw.loader.using('mediawiki.ForeignApi').done(function () {
var fApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php');
fApi.get({
action: 'query',
list: 'logevents',
leprop: 'user|timestamp|comment',
letype: 'globalauth',
letitle: 'User:' + us.user,
lelimit: '1'
}).done(us.writeGlobalBlock);
});
}
us.setCookie();
},
writeGlobalBlock: function (aw) {
if (!aw.query || !aw.query.logevents.length) return;
aw = aw.query.logevents[0];
$('#us_block_status_span').replaceWith($('<a>', { style: us.styleBlocked, id: 'us_block_status_span', href: '//meta.wikimedia.org/w/index.php?title=Special:Log/&type=globalauth&user=&page=User%3A' + us.user + '%40global', text: 'locked (global)' }).append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) })));
$('#us_block_status').parent().append([$('<li>').append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment)]), $('<li>').append([$('<b>').text('• ' + msg.blocker), $('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user })])]);
},
writeBlockDetail: function (aw) {
var duration = 'infinite', expiry = '';
if (aw.query && aw.query.logevents.length) {
aw = aw.query.logevents[0];
if (aw && aw.params) {
duration = aw.params.duration;
expiry = /in(de)?finite/.test(duration) ? msg.never : us.formatDate(aw.params.expiry);
}
$('#us_block_status_span').text(msg.block + ' (' + duration + ')').append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) }));
if (expiry) $('#mw-spinner-us_block_time_loading').replaceWith(expiry); else $('#us_block_time').remove();
$('#mw-spinner-us_block_reason_loading').replaceWith(us.parseComment(aw.comment, aw.commenthidden));
if (aw.user) $('#mw-spinner-us_blocker_loading').replaceWith($('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user }));
else $('#mw-spinner-us_blocker_loading').replaceWith(msg.nodb);
}
},
writePatrolCount: function (aw) {
if (aw instanceof Object) {
if (!aw.query || !aw.query.logevents) return;
us.patrols += aw.query.logevents.length;
if (aw.continue) return us.getPatrolCount(aw.continue);
$.removeSpinner('pat');
aw = us.patrols;
}
us.us_patrolcount_loading.replaceWith($('<a>', { href: '/w/index.php?title=Special:Log&type=' + (us.review ? 'review&subtype=accept' : 'patrol&subtype=patrol') + '&user=' + us.user, text: aw }));
data.reviews = aw;
us.setCookie();
},
getPatrolCount: function (e) {
var params = '&list=logevents&leprop=&ledir=newer&lelimit=max&leuser=' + us.user + '&leaction=' + (us.review ? 'review/approve' : 'patrol/patrol');
if (e instanceof Object) {
if (e.target) { $(e.target).injectSpinner('pat'); us.patrols = 0; }
else if (e.lecontinue) params += '&lecontinue=' + e.lecontinue;
}
us.actions.push({ params: params, func: us.writePatrolCount });
us.doRequest();
},
parseComment: function (text, hidden) {
var comment = $('<span>', { 'class': 'comment' });
if (typeof text === 'undefined') return comment.append(hidden ? msg.noReason : 'undefined').css('color', hidden ? '#999' : 'red');
if (!text) return comment.append(msg.blockCmt).css('color', '#999');
var intLink = /(.*?)\[\[((.*?)\|)?(.*?)\]\](.*)/, suche = text, erg;
while ((erg = intLink.exec(suche)) !== null) {
var link = erg[3] || erg[4];
comment.append([erg[1], $('<a>', { href: mw.util.getUrl(link), title: link }).text(erg[4])]);
suche = erg[5];
}
return comment.append(suche);
},
formatDate: function (datum) {
if (!(datum instanceof Date)) datum = new Date(datum);
try {
return datum.toLocaleDateString(us.lang, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
} catch (e) { return datum.toLocaleString(); }
},
getStoredData: function () {
var key = project + us.user, db, request;
try {
request = indexedDB.open(us.name);
request.onsuccess = function () {
db = this.result;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readonly').objectStore(key);
store.transaction.oncomplete = function () { us.runDataStore(); db.close(); };
store.get(key).onsuccess = function () { if (this.result) data = this.result.data; else us.getCookie(); };
} else { us.getCookie(); }
};
request.onerror = function () { us.getCookie(); };
} catch (e) { us.getCookie(); }
},
getCookie: function () {
data = mw.cookie.get(us.name + us.user);
if (data) try { data = JSON.parse(data); } catch(e) { data = null; }
us.runDataStore();
},
initI18N: function (i18n) {
var chain = mw.language.getFallbackLanguageChain();
for (var i = chain.length - 1; i >= 0; i--) if (chain[i] in i18n) msg = i18n[chain[i]];
},
init: function () {
if (this.statusBox || (this.statusBox = $('#us_box'))[0]) this.statusBox.remove();
this.usprop = 'blockinfo|editcount|gender|groups|implicitgroups|registration';
this.self = mw.config.get('wgUserName') === this.user;
this.initI18N(i18n);
this.createBox();
if (!this.first) {
if (mw.cookie && JSON) { if (window.indexedDB) this.getStoredData(); else this.getCookie(); }
else this.run();
} else this.run();
},
runDataStore: function () {
if (data) {
if (typeof data === 'string') try { data = JSON.parse(data); } catch(e) { data = {}; }
var q = { query: { users: [data] } };
if (data.timestamp) this.now = mw.now() - (data.timediff || 0);
if (data.timestamp && Math.abs((mw.now() - data.timestamp - (data.timediff || 0)) / 1000) < 90) return this.writeCommonInfo(q);
if (data.editcount) this.usprop = 'editcount';
}
data = data || {};
this.run();
},
run: function () {
if (this.self) {
return this.writeCommonInfo({ query: { users: [{ editcount: mw.config.get('wgUserEditCount'), registration: mw.config.get('wgUserRegistration'), groups: mw.config.get('wgUserGroups'), blockreason: '' }] } });
}
us.ajaxRequest({ curtimestamp: 1, list: 'users', ususers: us.user, usprop: us.usprop }, us.writeCommonInfo);
}
};
if ([2, 3].indexOf(mw.config.get('wgNamespaceNumber')) !== -1 && us.user.indexOf('/') === -1 && (!mw.config.get('wgArticleId') || mw.config.get('wgAction') === 'view')) {
$.when(mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.language', 'mediawiki.cookie', 'jquery.spinner', 'mediawiki.ForeignApi']), $.ready).then(function () {
us.api = mw.util.wikiScript('api') + '?action=query&format=json';
us.ajaxRequest('&meta=allmessages&amenableparser=1&amincludelocal=1&refix=group-', us.getGroupNames);
if (!mw.libs.viewerInfo) mw.loader.load('//meta.wikimedia.org/w/index.php?title=User:Perhelion/viewerInfo.js&action=raw&ctype=text/javascript');
});
}
}(jQuery, mediaWiki));
jyqulounuvi5l4y9segfxxcclihygtq
739942
739939
2026-05-01T09:30:24Z
MrJaroslavik
44012
test
739942
javascript
text/javascript
/**
* @Description: Modified Userstatus.js with Former Groups (Local & Global)
* Original by Steef389, Perhelion.
**/
(function ($, mw) {
'use strict';
var project = window.project || mw.config.get('wgDBname');
var msg, i18n = {
en: {
noReason: 'reason removed',
blockCmt: 'no block comment',
never: 'never',
not: ' not ',
block: 'blocked',
and: '$1 and$2',
count: 'Count',
noedit: 'None (only deleted contributions?)',
nodb: '‹not in database›',
noGrp: 'no extended group',
dates: ['years', 'months', 'days', 'hours', 'minutes', 'seconds'],
date: ['year', 'month', 'day', 'hour', 'minute', 'second'],
nosec: 'less than one ',
curtimeDiff: ' Locale time difference to server time in s: ',
thxGn: 'given',
thxRd: 'received',
nova: 'new',
ago: '$1 ago',
blocklog: 'Block log',
contrib: 'Edits: ',
usub: 'Subpages⬇',
thxGvng: 'Thanksgivings: ',
reviews: 'Active reviews: ',
regdate: 'Registration: ',
laedit: 'Last edited: ',
lala: 'Last log activity',
fiedit: 'First edit: ',
blocks: 'Block-status: ',
loGrp: 'Local user-groups: ',
glGrp: 'Global user-groups: ',
formerLoGrp: 'Former local user-groups: ',
formerGlGrp: 'Former global user-groups: ',
blockEnd: 'Block-end: ',
blocker: 'Blocker: ',
blockReason: 'Block-reason: '
},
cs: {
noReason: 'důvod odstraněn',
blockCmt: 'bez komentáře',
never: 'nikdy',
not: ' ne ',
block: 'zablokován',
and: '$1 a$2',
count: 'Počet',
noedit: 'Žádné (jen smazané?)',
nodb: '‹není v databázi›',
noGrp: 'žádná rozšířená skupina',
dates: ['let', 'měsíců', 'dní', 'hodin', 'minut', 'sekund'],
date: ['rok', 'měsíc', 'den', 'hodina', 'minuta', 'sekunda'],
nosec: 'méně než ',
curtimeDiff: ' Rozdíl lokálního času k serveru v s: ',
thxGn: 'poděkoval',
thxRd: 'obdržel poděkování',
nova: 'nové',
ago: 'před $1',
days: 'dní',
blocklog: 'Kniha zablokování',
contrib: 'Editace: ',
usub: 'Podstránky⬇',
thxGvng: 'Poděkování: ',
reviews: 'Aktivní prověření: ',
regdate: 'Registrace: ',
laedit: 'Poslední editace: ',
lala: 'Poslední aktivita v protokolech',
fiedit: 'První editace: ',
blocks: 'Stav bloku: ',
loGrp: 'Lokální skupiny: ',
glGrp: 'Globální skupiny: ',
formerLoGrp: 'Bývalé lokální skupiny: ',
formerGlGrp: 'Bývalé globální skupiny: ',
blockEnd: 'Konec bloku: ',
blocker: 'Zablokoval: ',
blockReason: 'Důvod bloku: '
}
};
var data;
var us = mw.libs.userstatus = {
name: 'Userstatus',
version: 1.80, // Bumping version for former groups update
lastEditSeconds: false,
styleMissingData: 'color:#999;font-size:90%;',
styleLoading: 'font-style:italic;',
styleBlocked: 'color:#c00',
styleNotBlocked: 'color:#080',
styleFormer: 'margin-left:15px; color:#72777d; font-style:italic; font-size:0.9em;',
viewPatrolNumber: false,
lang: mw.config.get('wgUserLanguage'),
user: mw.config.get('wgTitle'),
cookie: [],
thanks: 0,
patrols: 0,
actions: [],
dbVersion: 0,
implicitGroups: ['*', 'user', 'autoconfirmed'],
getLocalNames: function (groups, specialpage) {
if (groups) {
var arr = [];
specialpage = specialpage || 'ListGroupRights';
for (var i = 0; i < groups.length; ++i) {
var g = groups[i];
var n = us.groupNames[g];
if (n) arr.push('<a href="/wiki/Special:' + specialpage + '#' + g + '">' + n + '</a>');
else arr.push(g);
}
groups = arr.join(', ').replace(/(.*),([^,]*)$/, msg.and);
}
return groups;
},
writeGroups: function (groups) {
groups = us.getLocalNames(groups);
if (!groups) {
groups = $('<span>', {
style: us.styleMissingData + us.styleLoading,
text: msg.noGrp
});
}
us.loading_groups.replaceWith(groups);
// After local groups are written, fetch former local groups
us.fetchFormerRights(false);
},
getGroupNames: function (aw) {
if (!aw || !aw.query) return;
aw = aw.query.allmessages;
var al = aw.length;
var groups = {};
while (al--) {
var an = aw[al];
var name = an.name;
if (/^group-/.test(name)) {
name = RegExp.rightContext || name.substring(6);
groups[name] = an['*'];
}
}
us.groupNames = groups;
us.init();
},
ajaxRequest: function (params, onSuccess, trial) {
var url = us.api;
if (!(params instanceof Object)) {
url += params + '&maxage=2419200&smaxage=2419200';
params = {};
} else {
params.maxlag = 3;
params.maxage = 2419200;
params.smaxage = 2419200;
if (trial) params.maxlag *= trial;
}
var api = $.getJSON(url, params, onSuccess)
.fail(function (jqXHR, status) {
var note = 'Timeout fail, maybe try again!';
if (trial) {
if (!status.textStatus || (status.textStatus !== 'timeout' && jqXHR !== 'maxlag')) note = $('<span>Fehler bitte <a href="' + mw.util.getUrl('User_talk:Perhelion') + '">Perhelion</a> melden.</span>');
mw.notify(note, { title: us.name + ':', type: 'error' });
} else if (status.textStatus === 'timeout' || jqXHR === 'maxlag') {
api.abort();
return us.ajaxRequest(params, onSuccess, 2);
}
});
},
// --- NEW FORMER RIGHTS LOGIC ---
fetchFormerRights: function(isGlobal) {
var apiTarget = isGlobal ? 'https://meta.wikimedia.org/w/api.php' : mw.util.wikiScript('api');
var logType = isGlobal ? 'gblrights' : 'rights';
var userTarget = 'User:' + us.user;
var containerId = isGlobal ? 'us_former_global_groups' : 'us_former_local_groups';
// Globální override pro smazané skupiny (skupina: datum smazání)
var globalOverrides = {
'oathauth-tester': '2026-01-12T10:58:00Z'
};
var params = {
action: 'query',
list: 'logevents',
letype: logType,
letitle: userTarget,
lelimit: 500,
format: 'json',
origin: isGlobal ? '*' : undefined
};
$.getJSON(apiTarget, params, function(res) {
if(!res || !res.query || !res.query.logevents) return;
var logs = res.query.logevents;
var former = {};
var currentGroups = isGlobal ? (data.glGrp || []) : (data.groups || []);
logs.forEach(function(log) {
var p = log.params;
if (!p) return;
var date = log.timestamp;
var oldRaw = p.oldgroups || p.oldGroups || p.remove || p["0"] || "";
var newRaw = p.newgroups || p.newGroups || p.add || p["1"] || "";
var parse = function(val) {
if (Array.isArray(val)) return val;
if (typeof val === 'string') {
if (val === "(none)" || val === "") return [];
return val.split(/[,|]\s*/);
}
return [];
};
var oldG = parse(oldRaw);
var newG = parse(newRaw);
// 1. Nejdřív kontrolujeme udělení (v logu jdeme od nejnovějších k nejstarším)
newG.forEach(function(g) {
g = g.trim();
if (us.implicitGroups.indexOf(g) !== -1 || !g) return;
// Speciální případ: Skupina byla udělena, uživatel ji nemá a víme, že globálně zanikla
if (currentGroups.indexOf(g) === -1 && globalOverrides[g] && !former[g]) {
former[g] = { to: globalOverrides[g], from: date };
}
// Standardní případ: Už známe konec z oldG a teď jsme našli, kdy to začalo
else if (former[g] && !former[g].from) {
former[g].from = date;
}
});
// 2. Poté kontrolujeme odebrání (standardní cesta)
oldG.forEach(function(g) {
g = g.trim();
if (us.implicitGroups.indexOf(g) !== -1 || !g) return;
// Pokud skupinu nemá a ještě jsme o ní v tomto průchodu neslyšeli
if (currentGroups.indexOf(g) === -1 && !former[g]) {
former[g] = { to: date, from: null };
}
});
});
var entries = [];
Object.keys(former).sort().forEach(function(g) {
var item = former[g];
// Aplikace globálního overridu, pokud chybí datum odebrání a skupina je v seznamu smazaných
if (!item.to && globalOverrides[g]) {
item.to = globalOverrides[g];
}
var dateTo = new Date(item.to);
var dateFrom = item.from ? new Date(item.from) : null;
if(dateFrom && item.to) {
var diffText = us.getDateDiff(dateTo, dateFrom).replace(msg.ago.replace('$1', ''), '').trim();
entries.push(g + ' (' + dateFrom.toISOString().split('T')[0] + ' – ' + dateTo.toISOString().split('T')[0] + ', ' + diffText + ')');
} else if (item.to) {
entries.push(g + ' (do ' + dateTo.toISOString().split('T')[0] + ')');
}
});
if(entries.length > 0) {
$('#' + containerId).show().find('.us_former_content').text(entries.join(', '));
}
});
},
getThanks: function (e) {
var params = '&list=logevents&continue=-%7C%7C&leprop=&letype=thanks&ledir=newer&lelimit=max';
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
us.e.attr('title', us.e.text());
us.e.text('');
us.e.injectSpinner('thx');
us.thanks = 0;
} else if (e.lecontinue) {
params += '&lecontinue=' + e.lecontinue;
}
if (us.e.attr('title') === msg.thxGn) params += '&leuser=' + us.user;
else params += '&letitle=User:' + us.user;
}
us.actions.push({ params: params, func: us.writeThanks });
us.doRequest();
},
writeThanks: function (uq) {
if (!uq.query || !uq.query.logevents) return mw.log.warn(uq);
var e = us.e;
var user = 'page=';
var aw = uq.query.logevents;
us.thanks += aw.length;
if (uq.continue) return us.getThanks(uq.continue);
if (e[0].title === msg.thxGn) user += '&user=';
user += us.user;
$.removeSpinner('thx');
e.text(us.thanks);
e.off('click');
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special%3ALog&type=thanks&' + user + '&year=&month=-1&tagfilter=&hide_thanks_log=0';
delete us.e;
},
getUploads: function (e) {
var params = '&list=logevents&letype=upload&ledir=newer&lelimit=max&leuser=' + us.user;
function _start(u) {
var text = u.text();
u.prop('title', text);
u.text('');
u.injectSpinner('upload' + text);
u.data('upl', 0);
u.data('del', 0);
}
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
if (us.e.text() !== msg.nova) {
us.e2 = us.e.nextAll('a').eq(0);
if (us.e2.text() === msg.nova) _start(us.e2); else delete us.e2;
}
_start(us.e);
} else if (e.lecontinue) { params += '&lecontinue=' + e.lecontinue; }
params += '&leprop=ids';
if (us.e.prop('title') === msg.nova) params += '&leaction=upload/upload';
else if (us.e2) params += '|type';
}
us.actions.push({ params: params, func: us.writeUploads });
us.doRequest();
},
writeUploads: function (uq) {
if (!uq.query || !uq.query.logevents || !us.e) return mw.log(uq, us.e);
function _insert(e) {
$.removeSpinner('upload' + e.prop('title'));
e.text('');
e.off('click');
e.append(e.data('del'), $('<span>', { style: us.styleMissingData + us.styleLoading, text: ' (+' + (e.data('upl') - e.data('del')) + ' del.)' }));
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special:Log&type=upload&user=' + us.user + '&subtype=' + ((e[0].title === msg.nova) ? 'upload' : '');
}
var e = us.e;
var e2 = us.e2;
var aw = uq.query.logevents;
var alen = aw.length, i = 0;
var del = e.data('del');
if (e2) {
var upl = e2.data('upl');
var del2 = e2.data('del');
for (i; i < alen; ++i) {
var a = aw[i];
var c = a.pageid ? 1 : 0;
if (a.action === 'upload') { upl++; del2 += c; }
del += c;
}
e2.data('upl', upl);
e2.data('del', del2);
} else {
for (i; i < alen; ++i) if (aw[i].pageid) del++;
}
e.data('upl', e.data('upl') + alen);
e.data('del', del);
if (uq.continue) return us.getUploads(uq.continue);
_insert(e);
if (e2) { _insert(e2); delete us.e2; }
delete us.e;
},
writeCommonInfo: function (uq) {
var aw = uq.query;
if (!aw || !aw.users[0] || Object.prototype.hasOwnProperty.call(aw, 'missing') || Object.prototype.hasOwnProperty.call(aw.users[0], 'invalid')) return us.statusBox.remove();
aw = aw.users[0];
var edits = aw.editcount,
groups = data.groups || aw.groups,
blocked = data.blockreason || aw.blockexpiry,
gender = data.gender || aw.gender,
uploads = $('<a>', { title: msg.count, text: msg.nova, style: us.styleLoading, click: us.getUploads });
if (aw.registration) data.registration = aw.registration;
us.now = us.getDateFromTimestamp(uq.curtimestamp || us.now || mw.now());
us.user = us.user.replace(/ /g, '_');
us.first = 1;
if (edits) {
if (data.editcount && data.lastedit && data.editcount === edits) {
uq.query.usercontribs = [{ timestamp: data.lastedit }];
us.writeLastEdit(uq);
} else {
data.editcount = edits;
us.loading_last_edit.css('display', 'block');
us.actions.push({
params: '&list=usercontribs&ucuser=' + us.user + '&uclimit=1&uccontinue=' + (new Date(us.now).toISOString().replace(/[^\d]/g, '').slice(0, 14) + '|2'),
func: us.writeLastEdit
});
}
$('#t-contributions').remove();
} else { us.writeLastEdit({}); }
if (groups) {
edits = $('<a>', { title: 'Supercount', href: '//tools.wmflabs.org/supercount/index.php?user=' + us.user + '&project=' + location.hostname, text: edits });
if ($.inArray('bot', groups) === -1) {
us.review = $.inArray('editor', groups) !== -1;
if (us.review) {
if (typeof data.reviews === 'number') us.writePatrolCount(data.reviews);
} else if (us.viewPatrolNumber) { us.us_patrolcount_loading.parent().remove(); }
edits = [
edits, ' • ',
$('<a>', { href: '//xtools.wmflabs.org/ec' + mw.config.get('wgServer').substr(1) + '/' + us.user, title: 'XTools', text: 'XTools', style: us.styleLoading, target: '_blank' }), ' • ',
$('<a>', { href: '//commons.wikimedia.org/w/index.php?title=Commons:MyGallery/' + us.user + '&withJS=MediaWiki:JSONListUploads.js', title: 'MyGallery', text: (project === 'commonswiki' ? '' : 'c:') + 'MyGallery', style: us.styleLoading, target: '_blank' }), ' • ',
(project !== 'commonswiki' ? $('<a>', { href: '/wiki/Special:ListFiles/' + us.user, text: 'Upload', title: 'Special:Listfiles', style: us.styleLoading }) : 'Upload'),
(us.lang === 'de' ? '-' : ' ') + msg.count + ': ',
uploads.clone(1).text('total'), ' / ', uploads
];
if (!$('#t-subpages').length) {
edits.push($('<a>', { href: mw.util.getUrl('Special:Prefixindex/User:' + us.user + '/'), text: ' • ' + msg.usub, style: us.styleMissingData + us.styleLoading }));
}
} else if (us.us_patrolcount_loading) { us.us_patrolcount_loading.parent().remove(); }
us.loading_editcount.replaceWith(edits);
if (!aw.implicitgroups && !data.groups) {
aw.implicitgroups = ['*', 'user', 'autoconfirmed'];
if (groups.length < 4) aw.implicitgroups.pop();
}
data.groups = $.grep(groups, function (n) { return $.inArray(n, aw.implicitgroups) === -1; });
us.writeGroups(data.groups);
$('#t-userrights').remove();
}
if (!data.registration) {
us.actions.push({ params: '&list=logevents&leprop=timestamp|type&letype=newusers&lelimit=1&ledir=newer&leend=2005-12-30T00:00:00Z&leuser=' + us.user, func: us.writeRegistration });
if (data.firstedit) {
uq.query.usercontribs = [{ timestamp: data.firstedit }];
us.writeFirstEdit(uq);
} else if (edits) {
us.actions.push({ params: { list: 'usercontribs', ucuser: us.user, uclimit: 1, ucend: '2005-09-08T00:00:00Z', ucdir: 'newer', ucprop: 'timestamp' }, func: us.writeFirstEdit });
}
} else { us.writeRegistration(data.registration); }
us.loading_blocked.replaceWith($('<a>', { style: ((blocked) ? us.styleBlocked : us.styleNotBlocked), id: 'us_block_status_span', href: '/w/index.php?title=Special:Log/block&page=User:' + us.user, title: msg.blocklog, text: ((blocked) ? ' ' : msg.not) + msg.block }));
if (blocked) {
data.blockreason = aw.blockreason || ' ';
us.ul.append([
$('<li>', { id: 'us_block_time' }).append([$('<b>').text('• ' + msg.blockEnd), $.createSpinner('us_block_time_loading')]),
$('<li>', { id: 'us_block_reason' }).append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment, (aw.commenthidden))]),
$('<li>', { id: 'us_blocker' }).append([$('<b>').text('• ' + msg.blocker), $.createSpinner('us_blocker_loading')])
]);
us.actions.push({ params: { list: 'logevents', letitle: 'User:' + us.user, letype: 'block', lelimit: 1 }, func: us.writeBlockDetail });
}
if (data.glGrp && !data.locked) {
uq.query.globaluserinfo = { groups: (data.glGrp.length) ? data.glGrp : null };
us.writeGlobalGroup(uq);
} else {
us.actions.push({ params: '&meta=globaluserinfo&guiprop=groups&guiuser=' + us.user, func: us.writeGlobalGroup });
}
if (gender) {
data.gender = gender;
var genderSn = (gender === 'male') ? ' \u2642' : (gender === 'female' ? ' \u2640' : '');
$('#firstHeading').append($('<span>', { id: 'ps-gender-' + gender, style: 'font-size:80%', text: genderSn }));
}
if (us.actions.length) us.doRequest();
},
createBox: function () {
var statusBox = $('<div>', { style: 'border-bottom:1px solid #aaa;text-shadow:1px 1px 1px #eff;', id: 'us_box' });
if (mw.config.get('skin') === 'vector') statusBox.css('font-size', '0.8em');
var spanFrag = $.createSpinner();
us.loading_editcount = spanFrag.clone();
us.loading_registration = spanFrag.clone();
us.loading_groups = spanFrag.clone();
us.loading_blocked = spanFrag.clone();
us.us_global_group_loading = spanFrag.clone();
us.us_last_edit_loading = $.createSpinner('contribs');
us.loading_last_edit = $('<li>');
us.us_global_group = $('<li>', { style: 'display: none' });
var thx = $('<a>', { title: msg.count, text: msg.thxRd, style: us.styleLoading, click: us.getThanks });
var $userrights = $('#t-userrights a');
var $contributions = $('#t-contributions a');
if (!$userrights.length) $userrights = $('<a>', { href: '/wiki/Special:UserRights/' + us.user });
if (!$contributions.length) $contributions = $('<a>', { href: '/wiki/Special:Contributions/' + us.user });
var ul = $('<ul>', { style: 'list-style: none' }).append($('<li>', { id: 'us_editcount' }).append([$('<b>').append($contributions.text(msg.contrib)), us.loading_editcount]));
us.us_log_count = $('<span>');
if (us.viewPatrolNumber) {
us.us_patrolcount_loading = $('<a>', { style: us.styleLoading, click: us.getPatrolCount, text: msg.count });
us.us_log_count = $('<span>').append([$('<b>').text(msg.reviews), us.us_patrolcount_loading, ' • ']);
}
ul.append(us.us_log_count)
.append($('<li>', { id: 'us_reg_date' }).append([$('<b>').text(msg.regdate), us.loading_registration, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://tools.wmflabs.org/meta/userpages/' + us.user, text: '• User pages' })]))
.append($('<li>').append([$('<b>').append($userrights.text(msg.loGrp)), us.loading_groups, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://tools.wmflabs.org/meta/stalktoy/' + us.user, text: '• Stalk toy' })]))
// Container for former local groups
.append($('<li>', { id: 'us_former_local_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerLoGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.us_global_group.append([$('<b>').append($('<a>', { href: '/w/index.php?title=Special:GlobalUsers&limit=1&username=' + us.user, text: msg.glGrp })), us.us_global_group_loading, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://tools.wmflabs.org/meta/globalgroups/', text: '• GlobalGroups' })]))
// Container for former global groups
.append($('<li>', { id: 'us_former_global_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerGlGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.loading_last_edit.append([$('<b>').text(msg.laedit), us.us_last_edit_loading, ' ', $('<a>', { target: '_blank', style: us.styleMissingData, href: '/w/index.php?title=Special:Log/' + us.user + '&hide_thanks_log=0&hide_patrol_log=0&hide_tag_log=0', text: '• LLA' }), $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://tools.wmflabs.org/meta/crossactivity/' + us.user, text: '• CrossActivity' })]));
statusBox.append(ul);
us.ul = $('<ul>', { style: 'list-style:none' }).append($('<li>', { id: 'us_block_status' }).append([$('<b>').text(msg.blocks), us.loading_blocked, ' • ', $('<b>').text(msg.thxGvng), thx, ' / ', thx.clone(1).text(msg.thxGn), $('<a>', { style: 'float:right;' + us.styleMissingData, title: 'Purge data', text: 'purge', click: us.removeDataStore })]));
statusBox.append(us.ul);
$('#firstHeading').after(statusBox);
us.statusBox = statusBox;
},
removeDataStore: function (e) {
var key = project + us.user, db = window.indexedDB;
e.preventDefault();
if (!db || !us.dbVersion) return mw.cookie.set(us.name + us.user, null);
var request = db.open(us.name, us.dbVersion + 1);
request.onupgradeneeded = function () { db = this.result; if (db.objectStoreNames.contains(key)) db.deleteObjectStore(key); };
request.onsuccess = function () { db = this.result; us.dbVersion = db.version; db.close(); location.reload(); };
},
setCookie: function () {
var domain = (mw.config.get('wgNoticeProject') === 'wikipedia') ? 'wikipedia.org' : '';
var name = us.name + us.user;
var _saveCookie = function (k, d) { mw.cookie.set(name, JSON.stringify(d), { expires: 604800, domain: domain }); };
if (!us.actions[0] && JSON && data.editcount) {
if (us.cookie.length) window.clearTimeout(us.cookie.shift());
var saveData = window.indexedDB ? function (n, JSd, v) {
var key = project + us.user, db = window.indexedDB, request = db.open(us.name, v);
request.onupgradeneeded = function () {
db = this.result;
if (!db.objectStoreNames.contains(key)) {
var store = db.createObjectStore(key, { keyPath: 'name' });
store.createIndex('data', 'data', { unique: false });
}
};
request.onsuccess = function () {
db = this.result; us.dbVersion = db.version;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readwrite').objectStore(key);
store.put({ name: key, data: JSd });
} else { saveData(n, JSd, us.dbVersion + 1); }
};
request.onerror = function() { _saveCookie(n, JSd); };
} : _saveCookie;
us.cookie.push(setTimeout(function () {
data.timestamp = new Date().valueOf();
saveData('', data);
}, 500));
}
},
doRequest: function () {
var action = us.actions.shift();
if (action) us.ajaxRequest(action.params, action.func);
if (us.actions.length) window.setTimeout(function () { us.doRequest(); }, 100);
},
getDateDiff: function (now, date) {
var dStr = [];
var d = { years: 0, months: 0, days: 0 };
var diffDays = Math.floor(Math.abs(now - date) / 86400000);
if (now > date) {
var year = now.getFullYear();
var months = now.getMonth();
d.years = date.getFullYear();
date.setFullYear(year);
d.years = year - d.years;
if (date > now) {
d.years--;
date.setFullYear(year - 1);
}
var mDiff = months - date.getMonth();
if (mDiff < 0) mDiff += 12;
d.months = mDiff;
d.days = Math.floor((now - date) / 86400000) % 30;
var units = [d.years, d.months, d.days];
var labelSingle = [msg.date[0], msg.date[1], msg.date[2]];
var labelPlural = [msg.dates[0], msg.dates[1], msg.dates[2]];
for (var i = 0; i < 3; ++i) {
if (units[i]) {
dStr.push(units[i] + ' ' + ((units[i] > 1) ? labelPlural[i] : labelSingle[i]));
}
}
}
var mainDiff = (dStr.length) ? dStr.join(', ').replace(/(.*),([^,]*)/, msg.and).replace(/(.*)/, msg.ago) : msg.nosec + msg.date[2];
return mainDiff + (diffDays > 0 ? ' = ' + diffDays + ' ' + (msg.days || 'dní') : '');
},
getDateFromTimestamp: function (t) { if (!t) return false; t = new Date(t); return isNaN(t.valueOf()) ? false : t; },
writeRegistration: function (aw) {
if (aw) {
if (aw instanceof Object) {
if (aw.query && aw.query.logevents.length) aw = aw.query.logevents[0].timestamp;
}
if (typeof aw === 'string') {
us.loading_registration.replaceWith(us.formatDate(aw) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(aw)) + ')' }));
data.registration = aw;
return us.setCookie();
}
}
us.loading_registration.replaceWith($('<span>', { style: us.styleMissingData + us.styleLoading, text: msg.nodb }));
},
writeFirstEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs.length) return;
var date = data.firstedit = aw.query.usercontribs[0].timestamp;
$('#us_reg_date').append($('<li>').append([$('<b>', { text: '• ' + msg.fiedit }), us.formatDate(date) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(date)) + ')' })]));
},
writeLastEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs || !aw.query.usercontribs.length) return us.us_last_edit_loading.replaceWith($('<span>', { style: us.styleMissingData, text: msg.noedit }));
var date = data.lastedit = aw.query.usercontribs[0].timestamp;
us.us_last_edit_loading.replaceWith(us.getDateDiff(us.now, us.getDateFromTimestamp(date)), ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.formatDate(date) + ')' }));
data.timediff = mw.now() - us.now;
$('#us_editcount').append($('<span>', { style: 'float:right;' + us.styleMissingData, text: msg.curtimeDiff }).append($('<b>', { text: Math.round(data.timediff / 1000) })));
us.setCookie();
},
writeGlobalGroup: function (aw) {
aw = aw.query.globaluserinfo;
var groups = data.glGrp = aw.groups;
if (groups && groups.length) {
us.us_global_group_loading.replaceWith(us.getLocalNames(groups, 'GlobalGroupPermissions'));
us.us_global_group.css('display', 'block');
// Fetch former global rights
us.fetchFormerRights(true);
}
if (aw.locked !== undefined) {
data.locked = 1;
mw.loader.using('mediawiki.ForeignApi').done(function () {
var fApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php');
fApi.get({
action: 'query',
list: 'logevents',
leprop: 'user|timestamp|comment',
letype: 'globalauth',
letitle: 'User:' + us.user,
lelimit: '1'
}).done(us.writeGlobalBlock);
});
}
us.setCookie();
},
writeGlobalBlock: function (aw) {
if (!aw.query || !aw.query.logevents.length) return;
aw = aw.query.logevents[0];
$('#us_block_status_span').replaceWith($('<a>', { style: us.styleBlocked, id: 'us_block_status_span', href: '//meta.wikimedia.org/w/index.php?title=Special:Log/&type=globalauth&user=&page=User%3A' + us.user + '%40global', text: 'locked (global)' }).append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) })));
$('#us_block_status').parent().append([$('<li>').append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment)]), $('<li>').append([$('<b>').text('• ' + msg.blocker), $('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user })])]);
},
writeBlockDetail: function (aw) {
var duration = 'infinite', expiry = '';
if (aw.query && aw.query.logevents.length) {
aw = aw.query.logevents[0];
if (aw && aw.params) {
duration = aw.params.duration;
expiry = /in(de)?finite/.test(duration) ? msg.never : us.formatDate(aw.params.expiry);
}
$('#us_block_status_span').text(msg.block + ' (' + duration + ')').append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) }));
if (expiry) $('#mw-spinner-us_block_time_loading').replaceWith(expiry); else $('#us_block_time').remove();
$('#mw-spinner-us_block_reason_loading').replaceWith(us.parseComment(aw.comment, aw.commenthidden));
if (aw.user) $('#mw-spinner-us_blocker_loading').replaceWith($('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user }));
else $('#mw-spinner-us_blocker_loading').replaceWith(msg.nodb);
}
},
writePatrolCount: function (aw) {
if (aw instanceof Object) {
if (!aw.query || !aw.query.logevents) return;
us.patrols += aw.query.logevents.length;
if (aw.continue) return us.getPatrolCount(aw.continue);
$.removeSpinner('pat');
aw = us.patrols;
}
us.us_patrolcount_loading.replaceWith($('<a>', { href: '/w/index.php?title=Special:Log&type=' + (us.review ? 'review&subtype=accept' : 'patrol&subtype=patrol') + '&user=' + us.user, text: aw }));
data.reviews = aw;
us.setCookie();
},
getPatrolCount: function (e) {
var params = '&list=logevents&leprop=&ledir=newer&lelimit=max&leuser=' + us.user + '&leaction=' + (us.review ? 'review/approve' : 'patrol/patrol');
if (e instanceof Object) {
if (e.target) { $(e.target).injectSpinner('pat'); us.patrols = 0; }
else if (e.lecontinue) params += '&lecontinue=' + e.lecontinue;
}
us.actions.push({ params: params, func: us.writePatrolCount });
us.doRequest();
},
parseComment: function (text, hidden) {
var comment = $('<span>', { 'class': 'comment' });
if (typeof text === 'undefined') return comment.append(hidden ? msg.noReason : 'undefined').css('color', hidden ? '#999' : 'red');
if (!text) return comment.append(msg.blockCmt).css('color', '#999');
var intLink = /(.*?)\[\[((.*?)\|)?(.*?)\]\](.*)/, suche = text, erg;
while ((erg = intLink.exec(suche)) !== null) {
var link = erg[3] || erg[4];
comment.append([erg[1], $('<a>', { href: mw.util.getUrl(link), title: link }).text(erg[4])]);
suche = erg[5];
}
return comment.append(suche);
},
formatDate: function (datum) {
if (!(datum instanceof Date)) datum = new Date(datum);
try {
return datum.toLocaleDateString(us.lang, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
} catch (e) { return datum.toLocaleString(); }
},
getStoredData: function () {
var key = project + us.user, db, request;
try {
request = indexedDB.open(us.name);
request.onsuccess = function () {
db = this.result;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readonly').objectStore(key);
store.transaction.oncomplete = function () { us.runDataStore(); db.close(); };
store.get(key).onsuccess = function () { if (this.result) data = this.result.data; else us.getCookie(); };
} else { us.getCookie(); }
};
request.onerror = function () { us.getCookie(); };
} catch (e) { us.getCookie(); }
},
getCookie: function () {
data = mw.cookie.get(us.name + us.user);
if (data) try { data = JSON.parse(data); } catch(e) { data = null; }
us.runDataStore();
},
initI18N: function (i18n) {
var chain = mw.language.getFallbackLanguageChain();
for (var i = chain.length - 1; i >= 0; i--) if (chain[i] in i18n) msg = i18n[chain[i]];
},
init: function () {
if (this.statusBox || (this.statusBox = $('#us_box'))[0]) this.statusBox.remove();
this.usprop = 'blockinfo|editcount|gender|groups|implicitgroups|registration';
this.self = mw.config.get('wgUserName') === this.user;
this.initI18N(i18n);
this.createBox();
if (!this.first) {
if (mw.cookie && JSON) { if (window.indexedDB) this.getStoredData(); else this.getCookie(); }
else this.run();
} else this.run();
},
runDataStore: function () {
if (data) {
if (typeof data === 'string') try { data = JSON.parse(data); } catch(e) { data = {}; }
var q = { query: { users: [data] } };
if (data.timestamp) this.now = mw.now() - (data.timediff || 0);
if (data.timestamp && Math.abs((mw.now() - data.timestamp - (data.timediff || 0)) / 1000) < 90) return this.writeCommonInfo(q);
if (data.editcount) this.usprop = 'editcount';
}
data = data || {};
this.run();
},
run: function () {
if (this.self) {
return this.writeCommonInfo({ query: { users: [{ editcount: mw.config.get('wgUserEditCount'), registration: mw.config.get('wgUserRegistration'), groups: mw.config.get('wgUserGroups'), blockreason: '' }] } });
}
us.ajaxRequest({ curtimestamp: 1, list: 'users', ususers: us.user, usprop: us.usprop }, us.writeCommonInfo);
}
};
if ([2, 3].indexOf(mw.config.get('wgNamespaceNumber')) !== -1 && us.user.indexOf('/') === -1 && (!mw.config.get('wgArticleId') || mw.config.get('wgAction') === 'view')) {
$.when(mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.language', 'mediawiki.cookie', 'jquery.spinner', 'mediawiki.ForeignApi']), $.ready).then(function () {
us.api = mw.util.wikiScript('api') + '?action=query&format=json';
us.ajaxRequest('&meta=allmessages&amenableparser=1&amincludelocal=1&refix=group-', us.getGroupNames);
if (!mw.libs.viewerInfo) mw.loader.load('//meta.wikimedia.org/w/index.php?title=User:Perhelion/viewerInfo.js&action=raw&ctype=text/javascript');
});
}
}(jQuery, mediaWiki));
308jwpdcycr7nbs3ousquy234oc6zpc
739943
739942
2026-05-01T09:34:40Z
MrJaroslavik
44012
test
739943
javascript
text/javascript
/**
* @Description: Modified Userstatus.js with Former Groups (Local & Global)
* Original by Steef389, Perhelion.
**/
(function ($, mw) {
'use strict';
var project = window.project || mw.config.get('wgDBname');
var msg, i18n = {
en: {
noReason: 'reason removed',
blockCmt: 'no block comment',
never: 'never',
not: ' not ',
block: 'blocked',
and: '$1 and$2',
count: 'Count',
noedit: 'None (only deleted contributions?)',
nodb: '‹not in database›',
noGrp: 'no extended group',
dates: ['years', 'months', 'days', 'hours', 'minutes', 'seconds'],
date: ['year', 'month', 'day', 'hour', 'minute', 'second'],
nosec: 'less than one ',
curtimeDiff: ' Locale time difference to server time in s: ',
thxGn: 'given',
thxRd: 'received',
nova: 'new',
ago: '$1 ago',
blocklog: 'Block log',
contrib: 'Edits: ',
usub: 'Subpages⬇',
thxGvng: 'Thanksgivings: ',
reviews: 'Active reviews: ',
regdate: 'Registration: ',
laedit: 'Last edited: ',
lala: 'Last log activity',
fiedit: 'First edit: ',
blocks: 'Block-status: ',
loGrp: 'Local user-groups: ',
glGrp: 'Global user-groups: ',
formerLoGrp: 'Former local user-groups: ',
formerGlGrp: 'Former global user-groups: ',
blockEnd: 'Block-end: ',
blocker: 'Blocker: ',
blockReason: 'Block-reason: '
},
cs: {
noReason: 'důvod odstraněn',
blockCmt: 'bez komentáře',
never: 'nikdy',
not: ' ne ',
block: 'zablokován',
and: '$1 a$2',
count: 'Počet',
noedit: 'Žádné (jen smazané?)',
nodb: '‹není v databázi›',
noGrp: 'žádná rozšířená skupina',
dates: ['let', 'měsíců', 'dní', 'hodin', 'minut', 'sekund'],
date: ['rok', 'měsíc', 'den', 'hodina', 'minuta', 'sekunda'],
nosec: 'méně než ',
curtimeDiff: ' Rozdíl lokálního času k serveru v s: ',
thxGn: 'poděkoval',
thxRd: 'obdržel poděkování',
nova: 'nové',
ago: 'před $1',
days: 'dní',
blocklog: 'Kniha zablokování',
contrib: 'Editace: ',
usub: 'Podstránky⬇',
thxGvng: 'Poděkování: ',
reviews: 'Aktivní prověření: ',
regdate: 'Registrace: ',
laedit: 'Poslední editace: ',
lala: 'Poslední aktivita v protokolech',
fiedit: 'První editace: ',
blocks: 'Stav bloku: ',
loGrp: 'Lokální skupiny: ',
glGrp: 'Globální skupiny: ',
formerLoGrp: 'Bývalé lokální skupiny: ',
formerGlGrp: 'Bývalé globální skupiny: ',
blockEnd: 'Konec bloku: ',
blocker: 'Zablokoval: ',
blockReason: 'Důvod bloku: '
}
};
var data;
var us = mw.libs.userstatus = {
name: 'Userstatus',
version: 1.80, // Bumping version for former groups update
lastEditSeconds: false,
styleMissingData: 'color:#999;font-size:90%;',
styleLoading: 'font-style:italic;',
styleBlocked: 'color:#c00',
styleNotBlocked: 'color:#080',
styleFormer: 'margin-left:15px; color:#72777d; font-style:italic; font-size:0.9em;',
viewPatrolNumber: false,
lang: mw.config.get('wgUserLanguage'),
user: mw.config.get('wgTitle'),
cookie: [],
thanks: 0,
patrols: 0,
actions: [],
dbVersion: 0,
implicitGroups: ['*', 'user', 'autoconfirmed'],
getLocalNames: function (groups, specialpage) {
if (groups) {
var arr = [];
specialpage = specialpage || 'ListGroupRights';
for (var i = 0; i < groups.length; ++i) {
var g = groups[i];
var n = us.groupNames[g];
if (n) arr.push('<a href="/wiki/Special:' + specialpage + '#' + g + '">' + n + '</a>');
else arr.push(g);
}
groups = arr.join(', ').replace(/(.*),([^,]*)$/, msg.and);
}
return groups;
},
writeGroups: function (groups) {
groups = us.getLocalNames(groups);
if (!groups) {
groups = $('<span>', {
style: us.styleMissingData + us.styleLoading,
text: msg.noGrp
});
}
us.loading_groups.replaceWith(groups);
// After local groups are written, fetch former local groups
us.fetchFormerRights(false);
},
getGroupNames: function (aw) {
if (!aw || !aw.query) return;
aw = aw.query.allmessages;
var al = aw.length;
var groups = {};
while (al--) {
var an = aw[al];
var name = an.name;
if (/^group-/.test(name)) {
name = RegExp.rightContext || name.substring(6);
groups[name] = an['*'];
}
}
us.groupNames = groups;
us.init();
},
ajaxRequest: function (params, onSuccess, trial) {
var url = us.api;
if (!(params instanceof Object)) {
url += params + '&maxage=2419200&smaxage=2419200';
params = {};
} else {
params.maxlag = 3;
params.maxage = 2419200;
params.smaxage = 2419200;
if (trial) params.maxlag *= trial;
}
var api = $.getJSON(url, params, onSuccess)
.fail(function (jqXHR, status) {
var note = 'Timeout fail, maybe try again!';
if (trial) {
if (!status.textStatus || (status.textStatus !== 'timeout' && jqXHR !== 'maxlag')) note = $('<span>Fehler bitte <a href="' + mw.util.getUrl('User_talk:Perhelion') + '">Perhelion</a> melden.</span>');
mw.notify(note, { title: us.name + ':', type: 'error' });
} else if (status.textStatus === 'timeout' || jqXHR === 'maxlag') {
api.abort();
return us.ajaxRequest(params, onSuccess, 2);
}
});
},
// --- NEW FORMER RIGHTS LOGIC ---
fetchFormerRights: function(isGlobal) {
var apiTarget = isGlobal ? 'https://meta.wikimedia.org/w/api.php' : mw.util.wikiScript('api');
var logType = isGlobal ? 'gblrights' : 'rights';
var userTarget = 'User:' + us.user;
var containerId = isGlobal ? 'us_former_global_groups' : 'us_former_local_groups';
// Globální override pro smazané skupiny (skupina: datum smazání)
var globalOverrides = {
'oathauth-tester': '2026-01-12T10:58:00Z'
};
var params = {
action: 'query',
list: 'logevents',
letype: logType,
letitle: userTarget,
lelimit: 500,
format: 'json',
origin: isGlobal ? '*' : undefined
};
$.getJSON(apiTarget, params, function(res) {
if(!res || !res.query || !res.query.logevents) return;
var logs = res.query.logevents;
var former = {};
var currentGroups = isGlobal ? (data.glGrp || []) : (data.groups || []);
logs.forEach(function(log) {
var p = log.params;
if (!p) return;
var date = log.timestamp;
var oldRaw = p.oldgroups || p.oldGroups || p.remove || p["0"] || "";
var newRaw = p.newgroups || p.newGroups || p.add || p["1"] || "";
var parse = function(val) {
if (Array.isArray(val)) return val;
if (typeof val === 'string') {
if (val === "(none)" || val === "") return [];
return val.split(/[,|]\s*/);
}
return [];
};
var oldG = parse(oldRaw);
var newG = parse(newRaw);
// 1. Nejdřív kontrolujeme udělení (v logu jdeme od nejnovějších k nejstarším)
newG.forEach(function(g) {
g = g.trim();
if (us.implicitGroups.indexOf(g) !== -1 || !g) return;
// Speciální případ: Skupina byla udělena, uživatel ji nemá a víme, že globálně zanikla
if (currentGroups.indexOf(g) === -1 && globalOverrides[g] && !former[g]) {
former[g] = { to: globalOverrides[g], from: date };
}
// Standardní případ: Už známe konec z oldG a teď jsme našli, kdy to začalo
else if (former[g] && !former[g].from) {
former[g].from = date;
}
});
// 2. Poté kontrolujeme odebrání (standardní cesta)
oldG.forEach(function(g) {
g = g.trim();
if (us.implicitGroups.indexOf(g) !== -1 || !g) return;
// Pokud skupinu nemá a ještě jsme o ní v tomto průchodu neslyšeli
if (currentGroups.indexOf(g) === -1 && !former[g]) {
former[g] = { to: date, from: null };
}
});
});
var entries = [];
Object.keys(former).sort().forEach(function(g) {
var item = former[g];
// Aplikace globálního overridu, pokud chybí datum odebrání a skupina je v seznamu smazaných
if (!item.to && globalOverrides[g]) {
item.to = globalOverrides[g];
}
var dateTo = new Date(item.to);
var dateFrom = item.from ? new Date(item.from) : null;
if(dateFrom && item.to) {
var diffText = us.getDateDiff(dateTo, dateFrom).replace(msg.ago.replace('$1', ''), '').trim();
entries.push(g + ' (' + dateFrom.toISOString().split('T')[0] + ' – ' + dateTo.toISOString().split('T')[0] + ', ' + diffText + ')');
} else if (item.to) {
entries.push(g + ' (do ' + dateTo.toISOString().split('T')[0] + ')');
}
});
if(entries.length > 0) {
$('#' + containerId).show().find('.us_former_content').text(entries.join(', '));
}
});
},
getThanks: function (e) {
var params = '&list=logevents&continue=-%7C%7C&leprop=&letype=thanks&ledir=newer&lelimit=max';
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
us.e.attr('title', us.e.text());
us.e.text('');
us.e.injectSpinner('thx');
us.thanks = 0;
} else if (e.lecontinue) {
params += '&lecontinue=' + e.lecontinue;
}
if (us.e.attr('title') === msg.thxGn) params += '&leuser=' + us.user;
else params += '&letitle=User:' + us.user;
}
us.actions.push({ params: params, func: us.writeThanks });
us.doRequest();
},
writeThanks: function (uq) {
if (!uq.query || !uq.query.logevents) return mw.log.warn(uq);
var e = us.e;
var user = 'page=';
var aw = uq.query.logevents;
us.thanks += aw.length;
if (uq.continue) return us.getThanks(uq.continue);
if (e[0].title === msg.thxGn) user += '&user=';
user += us.user;
$.removeSpinner('thx');
e.text(us.thanks);
e.off('click');
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special%3ALog&type=thanks&' + user + '&year=&month=-1&tagfilter=&hide_thanks_log=0';
delete us.e;
},
getUploads: function (e) {
var params = '&list=logevents&letype=upload&ledir=newer&lelimit=max&leuser=' + us.user;
function _start(u) {
var text = u.text();
u.prop('title', text);
u.text('');
u.injectSpinner('upload' + text);
u.data('upl', 0);
u.data('del', 0);
}
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
if (us.e.text() !== msg.nova) {
us.e2 = us.e.nextAll('a').eq(0);
if (us.e2.text() === msg.nova) _start(us.e2); else delete us.e2;
}
_start(us.e);
} else if (e.lecontinue) { params += '&lecontinue=' + e.lecontinue; }
params += '&leprop=ids';
if (us.e.prop('title') === msg.nova) params += '&leaction=upload/upload';
else if (us.e2) params += '|type';
}
us.actions.push({ params: params, func: us.writeUploads });
us.doRequest();
},
writeUploads: function (uq) {
if (!uq.query || !uq.query.logevents || !us.e) return mw.log(uq, us.e);
function _insert(e) {
$.removeSpinner('upload' + e.prop('title'));
e.text('');
e.off('click');
e.append(e.data('del'), $('<span>', { style: us.styleMissingData + us.styleLoading, text: ' (+' + (e.data('upl') - e.data('del')) + ' del.)' }));
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special:Log&type=upload&user=' + us.user + '&subtype=' + ((e[0].title === msg.nova) ? 'upload' : '');
}
var e = us.e;
var e2 = us.e2;
var aw = uq.query.logevents;
var alen = aw.length, i = 0;
var del = e.data('del');
if (e2) {
var upl = e2.data('upl');
var del2 = e2.data('del');
for (i; i < alen; ++i) {
var a = aw[i];
var c = a.pageid ? 1 : 0;
if (a.action === 'upload') { upl++; del2 += c; }
del += c;
}
e2.data('upl', upl);
e2.data('del', del2);
} else {
for (i; i < alen; ++i) if (aw[i].pageid) del++;
}
e.data('upl', e.data('upl') + alen);
e.data('del', del);
if (uq.continue) return us.getUploads(uq.continue);
_insert(e);
if (e2) { _insert(e2); delete us.e2; }
delete us.e;
},
writeCommonInfo: function (uq) {
var aw = uq.query;
if (!aw || !aw.users[0] || Object.prototype.hasOwnProperty.call(aw, 'missing') || Object.prototype.hasOwnProperty.call(aw.users[0], 'invalid')) return us.statusBox.remove();
aw = aw.users[0];
var edits = aw.editcount,
groups = data.groups || aw.groups,
blocked = data.blockreason || aw.blockexpiry,
gender = data.gender || aw.gender,
uploads = $('<a>', { title: msg.count, text: msg.nova, style: us.styleLoading, click: us.getUploads });
if (aw.registration) data.registration = aw.registration;
us.now = us.getDateFromTimestamp(uq.curtimestamp || us.now || mw.now());
us.user = us.user.replace(/ /g, '_');
us.first = 1;
if (edits) {
if (data.editcount && data.lastedit && data.editcount === edits) {
uq.query.usercontribs = [{ timestamp: data.lastedit }];
us.writeLastEdit(uq);
} else {
data.editcount = edits;
us.loading_last_edit.css('display', 'block');
us.actions.push({
params: '&list=usercontribs&ucuser=' + us.user + '&uclimit=1&uccontinue=' + (new Date(us.now).toISOString().replace(/[^\d]/g, '').slice(0, 14) + '|2'),
func: us.writeLastEdit
});
}
$('#t-contributions').remove();
} else { us.writeLastEdit({}); }
if (groups) {
edits = $('<a>', { title: 'Supercount', href: '//tools.wmflabs.org/supercount/index.php?user=' + us.user + '&project=' + location.hostname, text: edits });
if ($.inArray('bot', groups) === -1) {
us.review = $.inArray('editor', groups) !== -1;
if (us.review) {
if (typeof data.reviews === 'number') us.writePatrolCount(data.reviews);
} else if (us.viewPatrolNumber) { us.us_patrolcount_loading.parent().remove(); }
edits = [
edits, ' • ',
$('<a>', { href: '//xtools.wmflabs.org/ec' + mw.config.get('wgServer').substr(1) + '/' + us.user, title: 'XTools', text: 'XTools', style: us.styleLoading, target: '_blank' }), ' • ',
$('<a>', { href: '//commons.wikimedia.org/w/index.php?title=Commons:MyGallery/' + us.user + '&withJS=MediaWiki:JSONListUploads.js', title: 'MyGallery', text: (project === 'commonswiki' ? '' : 'c:') + 'MyGallery', style: us.styleLoading, target: '_blank' }), ' • ',
(project !== 'commonswiki' ? $('<a>', { href: '/wiki/Special:ListFiles/' + us.user, text: 'Upload', title: 'Special:Listfiles', style: us.styleLoading }) : 'Upload'),
(us.lang === 'de' ? '-' : ' ') + msg.count + ': ',
uploads.clone(1).text('total'), ' / ', uploads
];
if (!$('#t-subpages').length) {
edits.push($('<a>', { href: mw.util.getUrl('Special:Prefixindex/User:' + us.user + '/'), text: ' • ' + msg.usub, style: us.styleMissingData + us.styleLoading }));
}
} else if (us.us_patrolcount_loading) { us.us_patrolcount_loading.parent().remove(); }
us.loading_editcount.replaceWith(edits);
if (!aw.implicitgroups && !data.groups) {
aw.implicitgroups = ['*', 'user', 'autoconfirmed'];
if (groups.length < 4) aw.implicitgroups.pop();
}
data.groups = $.grep(groups, function (n) { return $.inArray(n, aw.implicitgroups) === -1; });
us.writeGroups(data.groups);
$('#t-userrights').remove();
}
if (!data.registration) {
us.actions.push({ params: '&list=logevents&leprop=timestamp|type&letype=newusers&lelimit=1&ledir=newer&leend=2005-12-30T00:00:00Z&leuser=' + us.user, func: us.writeRegistration });
if (data.firstedit) {
uq.query.usercontribs = [{ timestamp: data.firstedit }];
us.writeFirstEdit(uq);
} else if (edits) {
us.actions.push({ params: { list: 'usercontribs', ucuser: us.user, uclimit: 1, ucend: '2005-09-08T00:00:00Z', ucdir: 'newer', ucprop: 'timestamp' }, func: us.writeFirstEdit });
}
} else { us.writeRegistration(data.registration); }
us.loading_blocked.replaceWith($('<a>', { style: ((blocked) ? us.styleBlocked : us.styleNotBlocked), id: 'us_block_status_span', href: '/w/index.php?title=Special:Log/block&page=User:' + us.user, title: msg.blocklog, text: ((blocked) ? ' ' : msg.not) + msg.block }));
if (blocked) {
data.blockreason = aw.blockreason || ' ';
us.ul.append([
$('<li>', { id: 'us_block_time' }).append([$('<b>').text('• ' + msg.blockEnd), $.createSpinner('us_block_time_loading')]),
$('<li>', { id: 'us_block_reason' }).append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment, (aw.commenthidden))]),
$('<li>', { id: 'us_blocker' }).append([$('<b>').text('• ' + msg.blocker), $.createSpinner('us_blocker_loading')])
]);
us.actions.push({ params: { list: 'logevents', letitle: 'User:' + us.user, letype: 'block', lelimit: 1 }, func: us.writeBlockDetail });
}
if (data.glGrp && !data.locked) {
uq.query.globaluserinfo = { groups: (data.glGrp.length) ? data.glGrp : null };
us.writeGlobalGroup(uq);
} else {
us.actions.push({ params: '&meta=globaluserinfo&guiprop=groups&guiuser=' + us.user, func: us.writeGlobalGroup });
}
if (gender) {
data.gender = gender;
var genderSn = (gender === 'male') ? ' \u2642' : (gender === 'female' ? ' \u2640' : '');
$('#firstHeading').append($('<span>', { id: 'ps-gender-' + gender, style: 'font-size:80%', text: genderSn }));
}
if (us.actions.length) us.doRequest();
},
createBox: function () {
var statusBox = $('<div>', { style: 'border-bottom:1px solid #aaa;text-shadow:1px 1px 1px #eff;', id: 'us_box' });
if (mw.config.get('skin') === 'vector') statusBox.css('font-size', '0.8em');
var spanFrag = $.createSpinner();
us.loading_editcount = spanFrag.clone();
us.loading_registration = spanFrag.clone();
us.loading_groups = spanFrag.clone();
us.loading_blocked = spanFrag.clone();
us.us_global_group_loading = spanFrag.clone();
us.us_last_edit_loading = $.createSpinner('contribs');
us.loading_last_edit = $('<li>');
us.us_global_group = $('<li>', { style: 'display: none' });
var thx = $('<a>', { title: msg.count, text: msg.thxRd, style: us.styleLoading, click: us.getThanks });
var $userrights = $('#t-userrights a');
var $contributions = $('#t-contributions a');
if (!$userrights.length) $userrights = $('<a>', { href: '/wiki/Special:UserRights/' + us.user });
if (!$contributions.length) $contributions = $('<a>', { href: '/wiki/Special:Contributions/' + us.user });
var ul = $('<ul>', { style: 'list-style: none' }).append($('<li>', { id: 'us_editcount' }).append([$('<b>').append($contributions.text(msg.contrib)), us.loading_editcount]));
us.us_log_count = $('<span>');
if (us.viewPatrolNumber) {
us.us_patrolcount_loading = $('<a>', { style: us.styleLoading, click: us.getPatrolCount, text: msg.count });
us.us_log_count = $('<span>').append([$('<b>').text(msg.reviews), us.us_patrolcount_loading, ' • ']);
}
ul.append(us.us_log_count)
.append($('<li>', { id: 'us_reg_date' }).append([$('<b>').text(msg.regdate), us.loading_registration, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://tools.wmflabs.org/meta/userpages/' + us.user, text: '• User pages' })]))
.append($('<li>').append([$('<b>').append($userrights.text(msg.loGrp)), us.loading_groups, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://tools.wmflabs.org/meta/stalktoy/' + us.user, text: '• Stalk toy' })]))
// Container for former local groups
.append($('<li>', { id: 'us_former_local_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerLoGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.us_global_group.append([$('<b>').append($('<a>', { href: '/w/index.php?title=Special:GlobalUsers&limit=1&username=' + us.user, text: msg.glGrp })), us.us_global_group_loading, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://tools.wmflabs.org/meta/globalgroups/', text: '• GlobalGroups' })]))
// Container for former global groups
.append($('<li>', { id: 'us_former_global_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerGlGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.loading_last_edit.append([$('<b>').text(msg.laedit), us.us_last_edit_loading, ' ', $('<a>', { target: '_blank', style: us.styleMissingData, href: '/w/index.php?title=Special:Log/' + us.user + '&hide_thanks_log=0&hide_patrol_log=0&hide_tag_log=0', text: '• LLA' }), $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://tools.wmflabs.org/meta/crossactivity/' + us.user, text: '• CrossActivity' })]));
statusBox.append(ul);
us.ul = $('<ul>', { style: 'list-style:none' }).append($('<li>', { id: 'us_block_status' }).append([$('<b>').text(msg.blocks), us.loading_blocked, ' • ', $('<b>').text(msg.thxGvng), thx, ' / ', thx.clone(1).text(msg.thxGn), $('<a>', { style: 'float:right;' + us.styleMissingData, title: 'Purge data', text: 'purge', click: us.removeDataStore })]));
statusBox.append(us.ul);
$('#firstHeading').after(statusBox);
us.statusBox = statusBox;
},
removeDataStore: function (e) {
var key = project + us.user, db = window.indexedDB;
e.preventDefault();
if (!db || !us.dbVersion) return mw.cookie.set(us.name + us.user, null);
var request = db.open(us.name, us.dbVersion + 1);
request.onupgradeneeded = function () { db = this.result; if (db.objectStoreNames.contains(key)) db.deleteObjectStore(key); };
request.onsuccess = function () { db = this.result; us.dbVersion = db.version; db.close(); location.reload(); };
},
setCookie: function () {
var domain = (mw.config.get('wgNoticeProject') === 'wikipedia') ? 'wikipedia.org' : '';
var name = us.name + us.user;
var _saveCookie = function (k, d) { mw.cookie.set(name, JSON.stringify(d), { expires: 604800, domain: domain }); };
if (!us.actions[0] && JSON && data.editcount) {
if (us.cookie.length) window.clearTimeout(us.cookie.shift());
var saveData = window.indexedDB ? function (n, JSd, v) {
var key = project + us.user, db = window.indexedDB, request = db.open(us.name, v);
request.onupgradeneeded = function () {
db = this.result;
if (!db.objectStoreNames.contains(key)) {
var store = db.createObjectStore(key, { keyPath: 'name' });
store.createIndex('data', 'data', { unique: false });
}
};
request.onsuccess = function () {
db = this.result; us.dbVersion = db.version;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readwrite').objectStore(key);
store.put({ name: key, data: JSd });
} else { saveData(n, JSd, us.dbVersion + 1); }
};
request.onerror = function() { _saveCookie(n, JSd); };
} : _saveCookie;
us.cookie.push(setTimeout(function () {
data.timestamp = new Date().valueOf();
saveData('', data);
}, 500));
}
},
doRequest: function () {
var action = us.actions.shift();
if (action) us.ajaxRequest(action.params, action.func);
if (us.actions.length) window.setTimeout(function () { us.doRequest(); }, 100);
},
getDateDiff: function (now, date) {
var dStr = [];
var diffMs = Math.abs(now - date);
var diffDays = Math.floor(diffMs / 86400000);
if (now > date) {
var years = now.getFullYear() - date.getFullYear();
var months = now.getMonth() - date.getMonth();
var days = now.getDate() - date.getDate();
// Korekce dnů a měsíců
if (days < 0) {
months--;
// Získáme počet dní v předchozím měsíci
var prevMonth = new Date(now.getFullYear(), now.getMonth(), 0).getDate();
days += prevMonth;
}
// Korekce měsíců a roků
if (months < 0) {
years--;
months += 12;
}
var units = [years, months, days];
var labelSingle = [msg.date[0], msg.date[1], msg.date[2]];
var labelPlural = [msg.dates[0], msg.dates[1], msg.dates[2]];
for (var i = 0; i < 3; ++i) {
if (units[i] > 0) {
dStr.push(units[i] + ' ' + ((units[i] > 1) ? labelPlural[i] : labelSingle[i]));
}
}
}
var mainDiff = (dStr.length) ? dStr.join(', ').replace(/(.*),([^,]*)/, msg.and).replace(/(.*)/, msg.ago) : msg.nosec + msg.date[2];
return mainDiff + (diffDays > 0 ? ' = ' + diffDays + ' ' + (msg.days || 'dní') : '');
},
getDateFromTimestamp: function (t) { if (!t) return false; t = new Date(t); return isNaN(t.valueOf()) ? false : t; },
writeRegistration: function (aw) {
if (aw) {
if (aw instanceof Object) {
if (aw.query && aw.query.logevents.length) aw = aw.query.logevents[0].timestamp;
}
if (typeof aw === 'string') {
us.loading_registration.replaceWith(us.formatDate(aw) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(aw)) + ')' }));
data.registration = aw;
return us.setCookie();
}
}
us.loading_registration.replaceWith($('<span>', { style: us.styleMissingData + us.styleLoading, text: msg.nodb }));
},
writeFirstEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs.length) return;
var date = data.firstedit = aw.query.usercontribs[0].timestamp;
$('#us_reg_date').append($('<li>').append([$('<b>', { text: '• ' + msg.fiedit }), us.formatDate(date) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(date)) + ')' })]));
},
writeLastEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs || !aw.query.usercontribs.length) return us.us_last_edit_loading.replaceWith($('<span>', { style: us.styleMissingData, text: msg.noedit }));
var date = data.lastedit = aw.query.usercontribs[0].timestamp;
us.us_last_edit_loading.replaceWith(us.getDateDiff(us.now, us.getDateFromTimestamp(date)), ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.formatDate(date) + ')' }));
data.timediff = mw.now() - us.now;
$('#us_editcount').append($('<span>', { style: 'float:right;' + us.styleMissingData, text: msg.curtimeDiff }).append($('<b>', { text: Math.round(data.timediff / 1000) })));
us.setCookie();
},
writeGlobalGroup: function (aw) {
aw = aw.query.globaluserinfo;
var groups = data.glGrp = aw.groups;
if (groups && groups.length) {
us.us_global_group_loading.replaceWith(us.getLocalNames(groups, 'GlobalGroupPermissions'));
us.us_global_group.css('display', 'block');
// Fetch former global rights
us.fetchFormerRights(true);
}
if (aw.locked !== undefined) {
data.locked = 1;
mw.loader.using('mediawiki.ForeignApi').done(function () {
var fApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php');
fApi.get({
action: 'query',
list: 'logevents',
leprop: 'user|timestamp|comment',
letype: 'globalauth',
letitle: 'User:' + us.user,
lelimit: '1'
}).done(us.writeGlobalBlock);
});
}
us.setCookie();
},
writeGlobalBlock: function (aw) {
if (!aw.query || !aw.query.logevents.length) return;
aw = aw.query.logevents[0];
$('#us_block_status_span').replaceWith($('<a>', { style: us.styleBlocked, id: 'us_block_status_span', href: '//meta.wikimedia.org/w/index.php?title=Special:Log/&type=globalauth&user=&page=User%3A' + us.user + '%40global', text: 'locked (global)' }).append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) })));
$('#us_block_status').parent().append([$('<li>').append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment)]), $('<li>').append([$('<b>').text('• ' + msg.blocker), $('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user })])]);
},
writeBlockDetail: function (aw) {
var duration = 'infinite', expiry = '';
if (aw.query && aw.query.logevents.length) {
aw = aw.query.logevents[0];
if (aw && aw.params) {
duration = aw.params.duration;
expiry = /in(de)?finite/.test(duration) ? msg.never : us.formatDate(aw.params.expiry);
}
$('#us_block_status_span').text(msg.block + ' (' + duration + ')').append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) }));
if (expiry) $('#mw-spinner-us_block_time_loading').replaceWith(expiry); else $('#us_block_time').remove();
$('#mw-spinner-us_block_reason_loading').replaceWith(us.parseComment(aw.comment, aw.commenthidden));
if (aw.user) $('#mw-spinner-us_blocker_loading').replaceWith($('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user }));
else $('#mw-spinner-us_blocker_loading').replaceWith(msg.nodb);
}
},
writePatrolCount: function (aw) {
if (aw instanceof Object) {
if (!aw.query || !aw.query.logevents) return;
us.patrols += aw.query.logevents.length;
if (aw.continue) return us.getPatrolCount(aw.continue);
$.removeSpinner('pat');
aw = us.patrols;
}
us.us_patrolcount_loading.replaceWith($('<a>', { href: '/w/index.php?title=Special:Log&type=' + (us.review ? 'review&subtype=accept' : 'patrol&subtype=patrol') + '&user=' + us.user, text: aw }));
data.reviews = aw;
us.setCookie();
},
getPatrolCount: function (e) {
var params = '&list=logevents&leprop=&ledir=newer&lelimit=max&leuser=' + us.user + '&leaction=' + (us.review ? 'review/approve' : 'patrol/patrol');
if (e instanceof Object) {
if (e.target) { $(e.target).injectSpinner('pat'); us.patrols = 0; }
else if (e.lecontinue) params += '&lecontinue=' + e.lecontinue;
}
us.actions.push({ params: params, func: us.writePatrolCount });
us.doRequest();
},
parseComment: function (text, hidden) {
var comment = $('<span>', { 'class': 'comment' });
if (typeof text === 'undefined') return comment.append(hidden ? msg.noReason : 'undefined').css('color', hidden ? '#999' : 'red');
if (!text) return comment.append(msg.blockCmt).css('color', '#999');
var intLink = /(.*?)\[\[((.*?)\|)?(.*?)\]\](.*)/, suche = text, erg;
while ((erg = intLink.exec(suche)) !== null) {
var link = erg[3] || erg[4];
comment.append([erg[1], $('<a>', { href: mw.util.getUrl(link), title: link }).text(erg[4])]);
suche = erg[5];
}
return comment.append(suche);
},
formatDate: function (datum) {
if (!(datum instanceof Date)) datum = new Date(datum);
try {
return datum.toLocaleDateString(us.lang, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
} catch (e) { return datum.toLocaleString(); }
},
getStoredData: function () {
var key = project + us.user, db, request;
try {
request = indexedDB.open(us.name);
request.onsuccess = function () {
db = this.result;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readonly').objectStore(key);
store.transaction.oncomplete = function () { us.runDataStore(); db.close(); };
store.get(key).onsuccess = function () { if (this.result) data = this.result.data; else us.getCookie(); };
} else { us.getCookie(); }
};
request.onerror = function () { us.getCookie(); };
} catch (e) { us.getCookie(); }
},
getCookie: function () {
data = mw.cookie.get(us.name + us.user);
if (data) try { data = JSON.parse(data); } catch(e) { data = null; }
us.runDataStore();
},
initI18N: function (i18n) {
var chain = mw.language.getFallbackLanguageChain();
for (var i = chain.length - 1; i >= 0; i--) if (chain[i] in i18n) msg = i18n[chain[i]];
},
init: function () {
if (this.statusBox || (this.statusBox = $('#us_box'))[0]) this.statusBox.remove();
this.usprop = 'blockinfo|editcount|gender|groups|implicitgroups|registration';
this.self = mw.config.get('wgUserName') === this.user;
this.initI18N(i18n);
this.createBox();
if (!this.first) {
if (mw.cookie && JSON) { if (window.indexedDB) this.getStoredData(); else this.getCookie(); }
else this.run();
} else this.run();
},
runDataStore: function () {
if (data) {
if (typeof data === 'string') try { data = JSON.parse(data); } catch(e) { data = {}; }
var q = { query: { users: [data] } };
if (data.timestamp) this.now = mw.now() - (data.timediff || 0);
if (data.timestamp && Math.abs((mw.now() - data.timestamp - (data.timediff || 0)) / 1000) < 90) return this.writeCommonInfo(q);
if (data.editcount) this.usprop = 'editcount';
}
data = data || {};
this.run();
},
run: function () {
if (this.self) {
return this.writeCommonInfo({ query: { users: [{ editcount: mw.config.get('wgUserEditCount'), registration: mw.config.get('wgUserRegistration'), groups: mw.config.get('wgUserGroups'), blockreason: '' }] } });
}
us.ajaxRequest({ curtimestamp: 1, list: 'users', ususers: us.user, usprop: us.usprop }, us.writeCommonInfo);
}
};
if ([2, 3].indexOf(mw.config.get('wgNamespaceNumber')) !== -1 && us.user.indexOf('/') === -1 && (!mw.config.get('wgArticleId') || mw.config.get('wgAction') === 'view')) {
$.when(mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.language', 'mediawiki.cookie', 'jquery.spinner', 'mediawiki.ForeignApi']), $.ready).then(function () {
us.api = mw.util.wikiScript('api') + '?action=query&format=json';
us.ajaxRequest('&meta=allmessages&amenableparser=1&amincludelocal=1&refix=group-', us.getGroupNames);
if (!mw.libs.viewerInfo) mw.loader.load('//meta.wikimedia.org/w/index.php?title=User:Perhelion/viewerInfo.js&action=raw&ctype=text/javascript');
});
}
}(jQuery, mediaWiki));
ozhl3r952dnnvxc65cj0v7bsgkvbx2e
739957
739943
2026-05-01T10:44:41Z
MrJaroslavik
44012
test
739957
javascript
text/javascript
/**
* @Description: Modified Userstatus.js with Former Groups (Local & Global)
* Original by Steef389, Perhelion.
**/
(function ($, mw) {
'use strict';
var project = window.project || mw.config.get('wgDBname');
var msg, i18n = {
en: {
noReason: 'reason removed',
blockCmt: 'no block comment',
never: 'never',
not: ' not ',
block: 'blocked',
and: '$1 and$2',
count: 'Count',
noedit: 'None (only deleted contributions?)',
nodb: '‹not in database›',
noGrp: 'no extended group',
dates: ['years', 'months', 'days', 'hours', 'minutes', 'seconds'],
date: ['year', 'month', 'day', 'hour', 'minute', 'second'],
nosec: 'less than one ',
curtimeDiff: ' Locale time difference to server time in s: ',
thxGn: 'given',
thxRd: 'received',
nova: 'new',
ago: '$1 ago',
blocklog: 'Block log',
contrib: 'Edits: ',
usub: 'Subpages⬇',
thxGvng: 'Thanksgivings: ',
reviews: 'Active reviews: ',
regdate: 'Registration: ',
laedit: 'Last edited: ',
lala: 'Last log activity',
fiedit: 'First edit: ',
blocks: 'Block-status: ',
loGrp: 'Local user-groups: ',
glGrp: 'Global user-groups: ',
formerLoGrp: 'Former local user-groups: ',
formerGlGrp: 'Former global user-groups: ',
blockEnd: 'Block-end: ',
blocker: 'Blocker: ',
blockReason: 'Block-reason: '
},
cs: {
noReason: 'důvod odstraněn',
blockCmt: 'bez komentáře',
never: 'nikdy',
not: ' ne ',
block: 'zablokován',
and: '$1 a$2',
count: 'Počet',
noedit: 'Žádné (jen smazané?)',
nodb: '‹není v databázi›',
noGrp: 'žádná rozšířená skupina',
dates: ['let', 'měsíců', 'dní', 'hodin', 'minut', 'sekund'],
date: ['rok', 'měsíc', 'den', 'hodina', 'minuta', 'sekunda'],
nosec: 'méně než ',
curtimeDiff: ' Rozdíl lokálního času k serveru v s: ',
thxGn: 'poděkoval',
thxRd: 'obdržel poděkování',
nova: 'nové',
ago: 'před $1',
days: 'dní',
blocklog: 'Kniha zablokování',
contrib: 'Editace: ',
usub: 'Podstránky⬇',
thxGvng: 'Poděkování: ',
reviews: 'Aktivní prověření: ',
regdate: 'Registrace: ',
laedit: 'Poslední editace: ',
lala: 'Poslední aktivita v protokolech',
fiedit: 'První editace: ',
blocks: 'Stav bloku: ',
loGrp: 'Lokální skupiny: ',
glGrp: 'Globální skupiny: ',
formerLoGrp: 'Bývalé lokální skupiny: ',
formerGlGrp: 'Bývalé globální skupiny: ',
blockEnd: 'Konec bloku: ',
blocker: 'Zablokoval: ',
blockReason: 'Důvod bloku: ',
laact: 'Poslední akce: ',
laadmin: 'Poslední admin akce: ',
lacu: 'Poslední CU akce: ',
laos: 'Poslední OS akce: '
}
};
var data;
var us = mw.libs.userstatus = {
name: 'Userstatus',
version: 1.80, // Bumping version for former groups update
lastEditSeconds: false,
styleMissingData: 'color:#999;font-size:90%;',
styleLoading: 'font-style:italic;',
styleBlocked: 'color:#c00',
styleNotBlocked: 'color:#080',
styleFormer: 'margin-left:15px; color:#72777d; font-style:italic; font-size:0.9em;',
viewPatrolNumber: false,
lang: mw.config.get('wgUserLanguage'),
user: mw.config.get('wgTitle'),
cookie: [],
thanks: 0,
patrols: 0,
actions: [],
dbVersion: 0,
implicitGroups: ['*', 'user', 'autoconfirmed'],
getLocalNames: function (groups, specialpage) {
if (groups) {
var arr = [];
specialpage = specialpage || 'ListGroupRights';
for (var i = 0; i < groups.length; ++i) {
var g = groups[i];
var n = us.groupNames[g];
if (n) arr.push('<a href="/wiki/Special:' + specialpage + '#' + g + '">' + n + '</a>');
else arr.push(g);
}
groups = arr.join(', ').replace(/(.*),([^,]*)$/, msg.and);
}
return groups;
},
writeGroups: function (groups) {
groups = us.getLocalNames(groups);
if (!groups) {
groups = $('<span>', {
style: us.styleMissingData + us.styleLoading,
text: msg.noGrp
});
}
us.loading_groups.replaceWith(groups);
// After local groups are written, fetch former local groups
us.fetchFormerRights(false);
},
getGroupNames: function (aw) {
if (!aw || !aw.query) return;
aw = aw.query.allmessages;
var al = aw.length;
var groups = {};
while (al--) {
var an = aw[al];
var name = an.name;
if (/^group-/.test(name)) {
name = RegExp.rightContext || name.substring(6);
groups[name] = an['*'];
}
}
us.groupNames = groups;
us.init();
},
ajaxRequest: function (params, onSuccess, trial) {
var url = us.api;
if (!(params instanceof Object)) {
url += params + '&maxage=2419200&smaxage=2419200';
params = {};
} else {
params.maxlag = 3;
params.maxage = 2419200;
params.smaxage = 2419200;
if (trial) params.maxlag *= trial;
}
var api = $.getJSON(url, params, onSuccess)
.fail(function (jqXHR, status) {
var note = 'Timeout fail, maybe try again!';
if (trial) {
if (!status.textStatus || (status.textStatus !== 'timeout' && jqXHR !== 'maxlag')) note = $('<span>Fehler bitte <a href="' + mw.util.getUrl('User_talk:Perhelion') + '">Perhelion</a> melden.</span>');
mw.notify(note, { title: us.name + ':', type: 'error' });
} else if (status.textStatus === 'timeout' || jqXHR === 'maxlag') {
api.abort();
return us.ajaxRequest(params, onSuccess, 2);
}
});
},
// --- NEW FORMER RIGHTS LOGIC ---
fetchFormerRights: function(isGlobal) {
var apiTarget = isGlobal ? 'https://meta.wikimedia.org/w/api.php' : mw.util.wikiScript('api');
var logType = isGlobal ? 'gblrights' : 'rights';
var userTarget = 'User:' + us.user;
var containerId = isGlobal ? 'us_former_global_groups' : 'us_former_local_groups';
// Globální override pro smazané skupiny (skupina: datum smazání)
var globalOverrides = {
'oathauth-tester': '2026-01-12T10:58:00Z'
};
var params = {
action: 'query',
list: 'logevents',
letype: logType,
letitle: userTarget,
lelimit: 500,
format: 'json',
origin: isGlobal ? '*' : undefined
};
$.getJSON(apiTarget, params, function(res) {
if(!res || !res.query || !res.query.logevents) return;
var logs = res.query.logevents;
var former = {};
var currentGroups = isGlobal ? (data.glGrp || []) : (data.groups || []);
logs.forEach(function(log) {
var p = log.params;
if (!p) return;
var date = log.timestamp;
var oldRaw = p.oldgroups || p.oldGroups || p.remove || p["0"] || "";
var newRaw = p.newgroups || p.newGroups || p.add || p["1"] || "";
var parse = function(val) {
if (Array.isArray(val)) return val;
if (typeof val === 'string') {
if (val === "(none)" || val === "") return [];
return val.split(/[,|]\s*/);
}
return [];
};
var oldG = parse(oldRaw);
var newG = parse(newRaw);
// 1. Nejdřív kontrolujeme udělení (v logu jdeme od nejnovějších k nejstarším)
newG.forEach(function(g) {
g = g.trim();
if (us.implicitGroups.indexOf(g) !== -1 || !g) return;
// Speciální případ: Skupina byla udělena, uživatel ji nemá a víme, že globálně zanikla
if (currentGroups.indexOf(g) === -1 && globalOverrides[g] && !former[g]) {
former[g] = { to: globalOverrides[g], from: date };
}
// Standardní případ: Už známe konec z oldG a teď jsme našli, kdy to začalo
else if (former[g] && !former[g].from) {
former[g].from = date;
}
});
// 2. Poté kontrolujeme odebrání (standardní cesta)
oldG.forEach(function(g) {
g = g.trim();
if (us.implicitGroups.indexOf(g) !== -1 || !g) return;
// Pokud skupinu nemá a ještě jsme o ní v tomto průchodu neslyšeli
if (currentGroups.indexOf(g) === -1 && !former[g]) {
former[g] = { to: date, from: null };
}
});
});
var entries = [];
Object.keys(former).sort().forEach(function(g) {
var item = former[g];
// Aplikace globálního overridu, pokud chybí datum odebrání a skupina je v seznamu smazaných
if (!item.to && globalOverrides[g]) {
item.to = globalOverrides[g];
}
var dateTo = new Date(item.to);
var dateFrom = item.from ? new Date(item.from) : null;
if(dateFrom && item.to) {
var diffText = us.getDateDiff(dateTo, dateFrom).replace(msg.ago.replace('$1', ''), '').trim();
entries.push(g + ' (' + dateFrom.toISOString().split('T')[0] + ' – ' + dateTo.toISOString().split('T')[0] + ', ' + diffText + ')');
} else if (item.to) {
entries.push(g + ' (do ' + dateTo.toISOString().split('T')[0] + ')');
}
});
if(entries.length > 0) {
$('#' + containerId).show().find('.us_former_content').text(entries.join(', '));
}
});
},
getThanks: function (e) {
var params = '&list=logevents&continue=-%7C%7C&leprop=&letype=thanks&ledir=newer&lelimit=max';
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
us.e.attr('title', us.e.text());
us.e.text('');
us.e.injectSpinner('thx');
us.thanks = 0;
} else if (e.lecontinue) {
params += '&lecontinue=' + e.lecontinue;
}
if (us.e.attr('title') === msg.thxGn) params += '&leuser=' + us.user;
else params += '&letitle=User:' + us.user;
}
us.actions.push({ params: params, func: us.writeThanks });
us.doRequest();
},
writeThanks: function (uq) {
if (!uq.query || !uq.query.logevents) return mw.log.warn(uq);
var e = us.e;
var user = 'page=';
var aw = uq.query.logevents;
us.thanks += aw.length;
if (uq.continue) return us.getThanks(uq.continue);
if (e[0].title === msg.thxGn) user += '&user=';
user += us.user;
$.removeSpinner('thx');
e.text(us.thanks);
e.off('click');
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special%3ALog&type=thanks&' + user + '&year=&month=-1&tagfilter=&hide_thanks_log=0';
delete us.e;
},
getUploads: function (e) {
var params = '&list=logevents&letype=upload&ledir=newer&lelimit=max&leuser=' + us.user;
function _start(u) {
var text = u.text();
u.prop('title', text);
u.text('');
u.injectSpinner('upload' + text);
u.data('upl', 0);
u.data('del', 0);
}
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
if (us.e.text() !== msg.nova) {
us.e2 = us.e.nextAll('a').eq(0);
if (us.e2.text() === msg.nova) _start(us.e2); else delete us.e2;
}
_start(us.e);
} else if (e.lecontinue) { params += '&lecontinue=' + e.lecontinue; }
params += '&leprop=ids';
if (us.e.prop('title') === msg.nova) params += '&leaction=upload/upload';
else if (us.e2) params += '|type';
}
us.actions.push({ params: params, func: us.writeUploads });
us.doRequest();
},
writeUploads: function (uq) {
if (!uq.query || !uq.query.logevents || !us.e) return mw.log(uq, us.e);
function _insert(e) {
$.removeSpinner('upload' + e.prop('title'));
e.text('');
e.off('click');
e.append(e.data('del'), $('<span>', { style: us.styleMissingData + us.styleLoading, text: ' (+' + (e.data('upl') - e.data('del')) + ' del.)' }));
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special:Log&type=upload&user=' + us.user + '&subtype=' + ((e[0].title === msg.nova) ? 'upload' : '');
}
var e = us.e;
var e2 = us.e2;
var aw = uq.query.logevents;
var alen = aw.length, i = 0;
var del = e.data('del');
if (e2) {
var upl = e2.data('upl');
var del2 = e2.data('del');
for (i; i < alen; ++i) {
var a = aw[i];
var c = a.pageid ? 1 : 0;
if (a.action === 'upload') { upl++; del2 += c; }
del += c;
}
e2.data('upl', upl);
e2.data('del', del2);
} else {
for (i; i < alen; ++i) if (aw[i].pageid) del++;
}
e.data('upl', e.data('upl') + alen);
e.data('del', del);
if (uq.continue) return us.getUploads(uq.continue);
_insert(e);
if (e2) { _insert(e2); delete us.e2; }
delete us.e;
},
writeCommonInfo: function (uq) {
var aw = uq.query;
if (!aw || !aw.users[0] || Object.prototype.hasOwnProperty.call(aw, 'missing') || Object.prototype.hasOwnProperty.call(aw.users[0], 'invalid')) return us.statusBox.remove();
aw = aw.users[0];
var edits = aw.editcount,
groups = data.groups || aw.groups,
blocked = data.blockreason || aw.blockexpiry,
gender = data.gender || aw.gender,
uploads = $('<a>', { title: msg.count, text: msg.nova, style: us.styleLoading, click: us.getUploads });
if (aw.registration) data.registration = aw.registration;
us.now = us.getDateFromTimestamp(uq.curtimestamp || us.now || mw.now());
us.user = us.user.replace(/ /g, '_');
us.first = 1;
// 1. Poslední akce (obecná z logů)
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=1',
func: function(res) {
if(res.query && res.query.logevents[0]) {
var date = res.query.logevents[0].timestamp;
$('#us_last_action_val').text(us.getDateDiff(us.now, new Date(date)));
}
}
});
// 2. Poslední admin akce (jen sysop)
if (groups && $.inArray('sysop', groups) !== -1) {
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=1&letype=delete|block|protect|rights',
func: function(res) {
if(res.query && res.query.logevents[0]) {
$('#us_last_admin_li').show();
$('#us_last_admin_val').text(us.getDateDiff(us.now, new Date(res.query.logevents[0].timestamp)));
}
}
});
}
// 3. Poslední CU akce (jen checkuser)
if (groups && $.inArray('checkuser', groups) !== -1) {
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=1&letype=checkuser',
func: function(res) {
if(res.query && res.query.logevents[0]) {
$('#us_last_cu_li').show();
$('#us_last_cu_val').text(us.getDateDiff(us.now, new Date(res.query.logevents[0].timestamp)));
}
}
});
}
// 4. Poslední OS akce (jen oversight/suppress)
if (groups && $.inArray('oversight', groups) !== -1) {
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=1&letype=suppress',
func: function(res) {
if(res.query && res.query.logevents[0]) {
$('#us_last_os_li').show();
$('#us_last_os_val').text(us.getDateDiff(us.now, new Date(res.query.logevents[0].timestamp)));
}
}
});
}
if (edits) {
if (data.editcount && data.lastedit && data.editcount === edits) {
uq.query.usercontribs = [{ timestamp: data.lastedit }];
us.writeLastEdit(uq);
} else {
data.editcount = edits;
us.loading_last_edit.css('display', 'block');
us.actions.push({
params: '&list=usercontribs&ucuser=' + us.user + '&uclimit=1&uccontinue=' + (new Date(us.now).toISOString().replace(/[^\d]/g, '').slice(0, 14) + '|2'),
func: us.writeLastEdit
});
}
$('#t-contributions').remove();
} else { us.writeLastEdit({}); }
if (groups) {
edits = $('<a>', { title: 'Supercount', href: '//tools.wmflabs.org/supercount/index.php?user=' + us.user + '&project=' + location.hostname, text: edits });
if ($.inArray('bot', groups) === -1) {
us.review = $.inArray('editor', groups) !== -1;
if (us.review) {
if (typeof data.reviews === 'number') us.writePatrolCount(data.reviews);
} else if (us.viewPatrolNumber) { us.us_patrolcount_loading.parent().remove(); }
edits = [
edits, ' • ',
$('<a>', { href: '//xtools.wmflabs.org/ec' + mw.config.get('wgServer').substr(1) + '/' + us.user, title: 'XTools', text: 'XTools', style: us.styleLoading, target: '_blank' }), ' • ',
$('<a>', { href: 'https://meta.wikimedia.org/wiki/Special:CentralAuth/' + us.user, title: 'CentralAuth', text: 'CentralAuth', style: us.styleLoading, target: '_blank' }), ' • ',
(project !== 'commonswiki' ? $('<a>', { href: '/wiki/Special:ListFiles/' + us.user, text: 'Upload', title: 'Special:Listfiles', style: us.styleLoading }) : 'Upload'),
(us.lang === 'de' ? '-' : ' ') + msg.count + ': ',
uploads.clone(1).text('total'), ' / ', uploads
];
if (!$('#t-subpages').length) {
edits.push($('<a>', { href: mw.util.getUrl('Special:Prefixindex/User:' + us.user + '/'), text: ' • ' + msg.usub, style: us.styleMissingData + us.styleLoading }));
}
} else if (us.us_patrolcount_loading) { us.us_patrolcount_loading.parent().remove(); }
us.loading_editcount.replaceWith(edits);
if (!aw.implicitgroups && !data.groups) {
aw.implicitgroups = ['*', 'user', 'autoconfirmed'];
if (groups.length < 4) aw.implicitgroups.pop();
}
data.groups = $.grep(groups, function (n) { return $.inArray(n, aw.implicitgroups) === -1; });
us.writeGroups(data.groups);
$('#t-userrights').remove();
}
if (!data.registration) {
us.actions.push({ params: '&list=logevents&leprop=timestamp|type&letype=newusers&lelimit=1&ledir=newer&leend=2005-12-30T00:00:00Z&leuser=' + us.user, func: us.writeRegistration });
if (data.firstedit) {
uq.query.usercontribs = [{ timestamp: data.firstedit }];
us.writeFirstEdit(uq);
} else if (edits) {
us.actions.push({ params: { list: 'usercontribs', ucuser: us.user, uclimit: 1, ucend: '2005-09-08T00:00:00Z', ucdir: 'newer', ucprop: 'timestamp' }, func: us.writeFirstEdit });
}
} else { us.writeRegistration(data.registration); }
us.loading_blocked.replaceWith($('<a>', { style: ((blocked) ? us.styleBlocked : us.styleNotBlocked), id: 'us_block_status_span', href: '/w/index.php?title=Special:Log/block&page=User:' + us.user, title: msg.blocklog, text: ((blocked) ? ' ' : msg.not) + msg.block }));
if (blocked) {
data.blockreason = aw.blockreason || ' ';
us.ul.append([
$('<li>', { id: 'us_block_time' }).append([$('<b>').text('• ' + msg.blockEnd), $.createSpinner('us_block_time_loading')]),
$('<li>', { id: 'us_block_reason' }).append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment, (aw.commenthidden))]),
$('<li>', { id: 'us_blocker' }).append([$('<b>').text('• ' + msg.blocker), $.createSpinner('us_blocker_loading')])
]);
us.actions.push({ params: { list: 'logevents', letitle: 'User:' + us.user, letype: 'block', lelimit: 1 }, func: us.writeBlockDetail });
}
if (data.glGrp && !data.locked) {
uq.query.globaluserinfo = { groups: (data.glGrp.length) ? data.glGrp : null };
us.writeGlobalGroup(uq);
} else {
us.actions.push({ params: '&meta=globaluserinfo&guiprop=groups&guiuser=' + us.user, func: us.writeGlobalGroup });
}
if (gender) {
data.gender = gender;
var genderSn = (gender === 'male') ? ' \u2642' : (gender === 'female' ? ' \u2640' : '');
$('#firstHeading').append($('<span>', { id: 'ps-gender-' + gender, style: 'font-size:80%', text: genderSn }));
}
if (us.actions.length) us.doRequest();
},
createBox: function () {
var statusBox = $('<div>', { style: 'border-bottom:1px solid #aaa;text-shadow:1px 1px 1px #eff;', id: 'us_box' });
if (mw.config.get('skin') === 'vector') statusBox.css('font-size', '0.8em');
var spanFrag = $.createSpinner();
us.loading_editcount = spanFrag.clone();
us.loading_registration = spanFrag.clone();
us.loading_groups = spanFrag.clone();
us.loading_blocked = spanFrag.clone();
us.us_global_group_loading = spanFrag.clone();
us.us_last_edit_loading = $.createSpinner('contribs');
us.loading_last_edit = $('<li>');
us.us_global_group = $('<li>', { style: 'display: none' });
var thx = $('<a>', { title: msg.count, text: msg.thxRd, style: us.styleLoading, click: us.getThanks });
var $userrights = $('#t-userrights a');
var $contributions = $('#t-contributions a');
if (!$userrights.length) $userrights = $('<a>', { href: '/wiki/Special:UserRights/' + us.user });
if (!$contributions.length) $contributions = $('<a>', { href: '/wiki/Special:Contributions/' + us.user });
var ul = $('<ul>', { style: 'list-style: none' }).append($('<li>', { id: 'us_editcount' }).append([$('<b>').append($contributions.text(msg.contrib)), us.loading_editcount]));
us.us_log_count = $('<span>');
if (us.viewPatrolNumber) {
us.us_patrolcount_loading = $('<a>', { style: us.styleLoading, click: us.getPatrolCount, text: msg.count });
us.us_log_count = $('<span>').append([$('<b>').text(msg.reviews), us.us_patrolcount_loading, ' • ']);
}
ul.append(us.us_log_count)
.append($('<li>', { id: 'us_reg_date' }).append([$('<b>').text(msg.regdate), us.loading_registration, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://meta.wikimedia.org/wiki/Special:UserPages/' + us.user, text: '• Global user pages' })]))
.append($('<li>').append([$('<b>').append($userrights.text(msg.loGrp)), us.loading_groups]))
.append($('<li>', { id: 'us_former_local_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerLoGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.us_global_group.append([$('<b>').append($('<a>', { href: 'https://meta.wikimedia.org/wiki/Special:Log?type=gblrights&user=&page=User:' + us.user, text: msg.glGrp })), us.us_global_group_loading]))
.append($('<li>', { id: 'us_former_global_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerGlGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.loading_last_edit.append([$('<b>').text(msg.laedit), us.us_last_edit_loading, ' ', $('<a>', { target: '_blank', style: us.styleMissingData, href: '/w/index.php?title=Special:Log/' + us.user, text: '• LLA' }), $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://meta.wikimedia.org/wiki/Special:GlobalContributions/' + us.user, text: '• Global contributions' })]))
.append($('<li>', { id: 'us_last_action_li' }).append([$('<b>').text(msg.laact), $('<span>', { id: 'us_last_action_val', text: '...' })]))
.append($('<li>', { id: 'us_last_admin_li', style: 'display:none' }).append([$('<b>').text(msg.laadmin), $('<span>', { id: 'us_last_admin_val', text: '...' })]))
.append($('<li>', { id: 'us_last_cu_li', style: 'display:none' }).append([$('<b>').text(msg.lacu), $('<span>', { id: 'us_last_cu_val', text: '...' })]))
.append($('<li>', { id: 'us_last_os_li', style: 'display:none' }).append([$('<b>').text(msg.laos), $('<span>', { id: 'us_last_os_val', text: '...' })]));
statusBox.append(ul);
us.ul = $('<ul>', { style: 'list-style:none' }).append($('<li>', { id: 'us_block_status' }).append([$('<b>').text(msg.blocks), us.loading_blocked, ' • ', $('<b>').text(msg.thxGvng), thx, ' / ', thx.clone(1).text(msg.thxGn), $('<a>', { style: 'float:right;' + us.styleMissingData, title: 'Purge data', text: 'purge', click: us.removeDataStore })]));
statusBox.append(us.ul);
$('#firstHeading').after(statusBox);
us.statusBox = statusBox;
},
removeDataStore: function (e) {
var key = project + us.user, db = window.indexedDB;
e.preventDefault();
if (!db || !us.dbVersion) return mw.cookie.set(us.name + us.user, null);
var request = db.open(us.name, us.dbVersion + 1);
request.onupgradeneeded = function () { db = this.result; if (db.objectStoreNames.contains(key)) db.deleteObjectStore(key); };
request.onsuccess = function () { db = this.result; us.dbVersion = db.version; db.close(); location.reload(); };
},
setCookie: function () {
var domain = (mw.config.get('wgNoticeProject') === 'wikipedia') ? 'wikipedia.org' : '';
var name = us.name + us.user;
var _saveCookie = function (k, d) { mw.cookie.set(name, JSON.stringify(d), { expires: 604800, domain: domain }); };
if (!us.actions[0] && JSON && data.editcount) {
if (us.cookie.length) window.clearTimeout(us.cookie.shift());
var saveData = window.indexedDB ? function (n, JSd, v) {
var key = project + us.user, db = window.indexedDB, request = db.open(us.name, v);
request.onupgradeneeded = function () {
db = this.result;
if (!db.objectStoreNames.contains(key)) {
var store = db.createObjectStore(key, { keyPath: 'name' });
store.createIndex('data', 'data', { unique: false });
}
};
request.onsuccess = function () {
db = this.result; us.dbVersion = db.version;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readwrite').objectStore(key);
store.put({ name: key, data: JSd });
} else { saveData(n, JSd, us.dbVersion + 1); }
};
request.onerror = function() { _saveCookie(n, JSd); };
} : _saveCookie;
us.cookie.push(setTimeout(function () {
data.timestamp = new Date().valueOf();
saveData('', data);
}, 500));
}
},
doRequest: function () {
var action = us.actions.shift();
if (action) us.ajaxRequest(action.params, action.func);
if (us.actions.length) window.setTimeout(function () { us.doRequest(); }, 100);
},
getDateDiff: function (now, date) {
var dStr = [];
if (now > date) {
var years = now.getFullYear() - date.getFullYear();
var months = now.getMonth() - date.getMonth();
var days = now.getDate() - date.getDate();
// Korekce dnů a měsíců
if (days < 0) {
months--;
// Získáme počet dní v předchozím měsíci
var prevMonth = new Date(now.getFullYear(), now.getMonth(), 0).getDate();
days += prevMonth;
}
// Korekce měsíců a roků
if (months < 0) {
years--;
months += 12;
}
var units = [years, months, days];
var labelSingle = [msg.date[0], msg.date[1], msg.date[2]];
var labelPlural = [msg.dates[0], msg.dates[1], msg.dates[2]];
for (var i = 0; i < 3; ++i) {
if (units[i] > 0) {
dStr.push(units[i] + ' ' + ((units[i] > 1) ? labelPlural[i] : labelSingle[i]));
}
}
}
var mainDiff = (dStr.length) ? dStr.join(', ').replace(/(.*),([^,]*)/, msg.and).replace(/(.*)/, msg.ago) : msg.nosec + msg.date[2];
return mainDiff;
},
getDateFromTimestamp: function (t) { if (!t) return false; t = new Date(t); return isNaN(t.valueOf()) ? false : t; },
writeRegistration: function (aw) {
if (aw) {
if (aw instanceof Object) {
if (aw.query && aw.query.logevents.length) aw = aw.query.logevents[0].timestamp;
}
if (typeof aw === 'string') {
us.loading_registration.replaceWith(us.formatDate(aw) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(aw)) + ')' }));
data.registration = aw;
return us.setCookie();
}
}
us.loading_registration.replaceWith($('<span>', { style: us.styleMissingData + us.styleLoading, text: msg.nodb }));
},
writeFirstEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs.length) return;
var date = data.firstedit = aw.query.usercontribs[0].timestamp;
$('#us_reg_date').append($('<li>').append([$('<b>', { text: '• ' + msg.fiedit }), us.formatDate(date) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(date)) + ')' })]));
},
writeLastEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs || !aw.query.usercontribs.length) return us.us_last_edit_loading.replaceWith($('<span>', { style: us.styleMissingData, text: msg.noedit }));
var date = data.lastedit = aw.query.usercontribs[0].timestamp;
us.us_last_edit_loading.replaceWith(us.getDateDiff(us.now, us.getDateFromTimestamp(date)), ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.formatDate(date) + ')' }));
data.timediff = mw.now() - us.now;
$('#us_editcount').append($('<span>', { style: 'float:right;' + us.styleMissingData, text: msg.curtimeDiff }).append($('<b>', { text: Math.round(data.timediff / 1000) })));
us.setCookie();
},
writeGlobalGroup: function (aw) {
aw = aw.query.globaluserinfo;
var groups = data.glGrp = aw.groups;
// Pokud lokální registrace chybí, použijeme tu z globálního infa
if (!data.registration && aw.registration) {
us.writeRegistration(aw.registration);
}
if (groups && groups.length) {
us.us_global_group_loading.replaceWith(us.getLocalNames(groups, 'GlobalGroupPermissions'));
us.us_global_group.css('display', 'block');
// Fetch former global rights
us.fetchFormerRights(true);
}
if (aw.locked !== undefined) {
data.locked = 1;
mw.loader.using('mediawiki.ForeignApi').done(function () {
var fApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php');
fApi.get({
action: 'query',
list: 'logevents',
leprop: 'user|timestamp|comment',
letype: 'globalauth',
letitle: 'User:' + us.user,
lelimit: '1'
}).done(us.writeGlobalBlock);
});
}
us.setCookie();
},
writeGlobalBlock: function (aw) {
if (!aw.query || !aw.query.logevents.length) return;
aw = aw.query.logevents[0];
$('#us_block_status_span').replaceWith($('<a>', { style: us.styleBlocked, id: 'us_block_status_span', href: '//meta.wikimedia.org/w/index.php?title=Special:Log/&type=globalauth&user=&page=User%3A' + us.user + '%40global', text: 'locked (global)' }).append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) })));
$('#us_block_status').parent().append([$('<li>').append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment)]), $('<li>').append([$('<b>').text('• ' + msg.blocker), $('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user })])]);
},
writeBlockDetail: function (aw) {
var duration = 'infinite', expiry = '';
if (aw.query && aw.query.logevents.length) {
aw = aw.query.logevents[0];
if (aw && aw.params) {
duration = aw.params.duration;
expiry = /in(de)?finite/.test(duration) ? msg.never : us.formatDate(aw.params.expiry);
}
$('#us_block_status_span').text(msg.block + ' (' + duration + ')').append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) }));
if (expiry) $('#mw-spinner-us_block_time_loading').replaceWith(expiry); else $('#us_block_time').remove();
$('#mw-spinner-us_block_reason_loading').replaceWith(us.parseComment(aw.comment, aw.commenthidden));
if (aw.user) $('#mw-spinner-us_blocker_loading').replaceWith($('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user }));
else $('#mw-spinner-us_blocker_loading').replaceWith(msg.nodb);
}
},
writePatrolCount: function (aw) {
if (aw instanceof Object) {
if (!aw.query || !aw.query.logevents) return;
us.patrols += aw.query.logevents.length;
if (aw.continue) return us.getPatrolCount(aw.continue);
$.removeSpinner('pat');
aw = us.patrols;
}
us.us_patrolcount_loading.replaceWith($('<a>', { href: '/w/index.php?title=Special:Log&type=' + (us.review ? 'review&subtype=accept' : 'patrol&subtype=patrol') + '&user=' + us.user, text: aw }));
data.reviews = aw;
us.setCookie();
},
getPatrolCount: function (e) {
var params = '&list=logevents&leprop=&ledir=newer&lelimit=max&leuser=' + us.user + '&leaction=' + (us.review ? 'review/approve' : 'patrol/patrol');
if (e instanceof Object) {
if (e.target) { $(e.target).injectSpinner('pat'); us.patrols = 0; }
else if (e.lecontinue) params += '&lecontinue=' + e.lecontinue;
}
us.actions.push({ params: params, func: us.writePatrolCount });
us.doRequest();
},
parseComment: function (text, hidden) {
var comment = $('<span>', { 'class': 'comment' });
if (typeof text === 'undefined') return comment.append(hidden ? msg.noReason : 'undefined').css('color', hidden ? '#999' : 'red');
if (!text) return comment.append(msg.blockCmt).css('color', '#999');
var intLink = /(.*?)\[\[((.*?)\|)?(.*?)\]\](.*)/, suche = text, erg;
while ((erg = intLink.exec(suche)) !== null) {
var link = erg[3] || erg[4];
comment.append([erg[1], $('<a>', { href: mw.util.getUrl(link), title: link }).text(erg[4])]);
suche = erg[5];
}
return comment.append(suche);
},
formatDate: function (datum) {
if (!(datum instanceof Date)) datum = new Date(datum);
try {
return datum.toLocaleDateString(us.lang, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
} catch (e) { return datum.toLocaleString(); }
},
getStoredData: function () {
var key = project + us.user, db, request;
try {
request = indexedDB.open(us.name);
request.onsuccess = function () {
db = this.result;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readonly').objectStore(key);
store.transaction.oncomplete = function () { us.runDataStore(); db.close(); };
store.get(key).onsuccess = function () { if (this.result) data = this.result.data; else us.getCookie(); };
} else { us.getCookie(); }
};
request.onerror = function () { us.getCookie(); };
} catch (e) { us.getCookie(); }
},
getCookie: function () {
data = mw.cookie.get(us.name + us.user);
if (data) try { data = JSON.parse(data); } catch(e) { data = null; }
us.runDataStore();
},
initI18N: function (i18n) {
var chain = mw.language.getFallbackLanguageChain();
for (var i = chain.length - 1; i >= 0; i--) if (chain[i] in i18n) msg = i18n[chain[i]];
},
init: function () {
if (this.statusBox || (this.statusBox = $('#us_box'))[0]) this.statusBox.remove();
this.usprop = 'blockinfo|editcount|gender|groups|implicitgroups|registration';
this.self = mw.config.get('wgUserName') === this.user;
this.initI18N(i18n);
this.createBox();
if (!this.first) {
if (mw.cookie && JSON) { if (window.indexedDB) this.getStoredData(); else this.getCookie(); }
else this.run();
} else this.run();
},
runDataStore: function () {
if (data) {
if (typeof data === 'string') try { data = JSON.parse(data); } catch(e) { data = {}; }
var q = { query: { users: [data] } };
if (data.timestamp) this.now = mw.now() - (data.timediff || 0);
if (data.timestamp && Math.abs((mw.now() - data.timestamp - (data.timediff || 0)) / 1000) < 90) return this.writeCommonInfo(q);
if (data.editcount) this.usprop = 'editcount';
}
data = data || {};
this.run();
},
run: function () {
if (this.self) {
return this.writeCommonInfo({ query: { users: [{ editcount: mw.config.get('wgUserEditCount'), registration: mw.config.get('wgUserRegistration'), groups: mw.config.get('wgUserGroups'), blockreason: '' }] } });
}
us.ajaxRequest({ curtimestamp: 1, list: 'users', ususers: us.user, usprop: us.usprop }, us.writeCommonInfo);
}
};
if ([2, 3].indexOf(mw.config.get('wgNamespaceNumber')) !== -1 && us.user.indexOf('/') === -1 && (!mw.config.get('wgArticleId') || mw.config.get('wgAction') === 'view')) {
$.when(mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.language', 'mediawiki.cookie', 'jquery.spinner', 'mediawiki.ForeignApi']), $.ready).then(function () {
us.api = mw.util.wikiScript('api') + '?action=query&format=json';
us.ajaxRequest('&meta=allmessages&amenableparser=1&amincludelocal=1&refix=group-', us.getGroupNames);
if (!mw.libs.viewerInfo) mw.loader.load('//meta.wikimedia.org/w/index.php?title=User:Perhelion/viewerInfo.js&action=raw&ctype=text/javascript');
});
}
}(jQuery, mediaWiki));
ob9smm8p7hyh77bmb75897jjaggtfo7
739958
739957
2026-05-01T10:49:13Z
MrJaroslavik
44012
test
739958
javascript
text/javascript
/**
* @Description: Modified Userstatus.js with Former Groups (Local & Global)
* Original by Steef389, Perhelion.
**/
(function ($, mw) {
'use strict';
var project = window.project || mw.config.get('wgDBname');
var msg, i18n = {
en: {
noReason: 'reason removed',
blockCmt: 'no block comment',
never: 'never',
not: ' not ',
block: 'blocked',
and: '$1 and$2',
count: 'Count',
noedit: 'None (only deleted contributions?)',
nodb: '‹not in database›',
noGrp: 'no extended group',
dates: ['years', 'months', 'days', 'hours', 'minutes', 'seconds'],
date: ['year', 'month', 'day', 'hour', 'minute', 'second'],
nosec: 'less than one ',
curtimeDiff: ' Locale time difference to server time in s: ',
thxGn: 'given',
thxRd: 'received',
nova: 'new',
ago: '$1 ago',
blocklog: 'Block log',
contrib: 'Edits: ',
usub: 'Subpages⬇',
thxGvng: 'Thanksgivings: ',
reviews: 'Active reviews: ',
regdate: 'Registration: ',
laedit: 'Last edited: ',
lala: 'Last log activity',
fiedit: 'First edit: ',
blocks: 'Block-status: ',
loGrp: 'Local user-groups: ',
glGrp: 'Global user-groups: ',
formerLoGrp: 'Former local user-groups: ',
formerGlGrp: 'Former global user-groups: ',
blockEnd: 'Block-end: ',
blocker: 'Blocker: ',
blockReason: 'Block-reason: ',
laact: 'Last action: ',
laadmin: 'Last admin action: ',
lacu: 'Last CU action: ',
laos: 'Last OS action: '
},
cs: {
noReason: 'důvod odstraněn',
blockCmt: 'bez komentáře',
never: 'nikdy',
not: ' ne ',
block: 'zablokován',
and: '$1 a$2',
count: 'Počet',
noedit: 'Žádné (jen smazané?)',
nodb: '‹není v databázi›',
noGrp: 'žádná rozšířená skupina',
dates: ['let', 'měsíců', 'dní', 'hodin', 'minut', 'sekund'],
date: ['rok', 'měsíc', 'den', 'hodina', 'minuta', 'sekunda'],
nosec: 'méně než ',
curtimeDiff: ' Rozdíl lokálního času k serveru v s: ',
thxGn: 'poděkoval',
thxRd: 'obdržel poděkování',
nova: 'nové',
ago: 'před $1',
days: 'dní',
blocklog: 'Kniha zablokování',
contrib: 'Editace: ',
usub: 'Podstránky⬇',
thxGvng: 'Poděkování: ',
reviews: 'Aktivní prověření: ',
regdate: 'Registrace: ',
laedit: 'Poslední editace: ',
lala: 'Poslední aktivita v protokolech',
fiedit: 'První editace: ',
blocks: 'Stav bloku: ',
loGrp: 'Lokální skupiny: ',
glGrp: 'Globální skupiny: ',
formerLoGrp: 'Bývalé lokální skupiny: ',
formerGlGrp: 'Bývalé globální skupiny: ',
blockEnd: 'Konec bloku: ',
blocker: 'Zablokoval: ',
blockReason: 'Důvod bloku: ',
laact: 'Poslední akce: ',
laadmin: 'Poslední admin akce: ',
lacu: 'Poslední CU akce: ',
laos: 'Poslední OS akce: '
}
};
var data;
var us = mw.libs.userstatus = {
name: 'Userstatus',
version: 1.80, // Bumping version for former groups update
lastEditSeconds: false,
styleMissingData: 'color:#999;font-size:90%;',
styleLoading: 'font-style:italic;',
styleBlocked: 'color:#c00',
styleNotBlocked: 'color:#080',
styleFormer: 'margin-left:15px; color:#72777d; font-style:italic; font-size:0.9em;',
viewPatrolNumber: false,
lang: mw.config.get('wgUserLanguage'),
user: mw.config.get('wgTitle'),
cookie: [],
thanks: 0,
patrols: 0,
actions: [],
dbVersion: 0,
implicitGroups: ['*', 'user', 'autoconfirmed'],
getLocalNames: function (groups, specialpage) {
if (groups) {
var arr = [];
specialpage = specialpage || 'ListGroupRights';
for (var i = 0; i < groups.length; ++i) {
var g = groups[i];
var n = us.groupNames[g];
if (n) arr.push('<a href="/wiki/Special:' + specialpage + '#' + g + '">' + n + '</a>');
else arr.push(g);
}
groups = arr.join(', ').replace(/(.*),([^,]*)$/, msg.and);
}
return groups;
},
writeGroups: function (groups) {
groups = us.getLocalNames(groups);
if (!groups) {
groups = $('<span>', {
style: us.styleMissingData + us.styleLoading,
text: msg.noGrp
});
}
us.loading_groups.replaceWith(groups);
// After local groups are written, fetch former local groups
us.fetchFormerRights(false);
},
getGroupNames: function (aw) {
if (!aw || !aw.query) return;
aw = aw.query.allmessages;
var al = aw.length;
var groups = {};
while (al--) {
var an = aw[al];
var name = an.name;
if (/^group-/.test(name)) {
name = RegExp.rightContext || name.substring(6);
groups[name] = an['*'];
}
}
us.groupNames = groups;
us.init();
},
ajaxRequest: function (params, onSuccess, trial) {
var url = us.api;
if (!(params instanceof Object)) {
url += params + '&maxage=2419200&smaxage=2419200';
params = {};
} else {
params.maxlag = 3;
params.maxage = 2419200;
params.smaxage = 2419200;
if (trial) params.maxlag *= trial;
}
var api = $.getJSON(url, params, onSuccess)
.fail(function (jqXHR, status) {
var note = 'Timeout fail, maybe try again!';
if (trial) {
if (!status.textStatus || (status.textStatus !== 'timeout' && jqXHR !== 'maxlag')) note = $('<span>Fehler bitte <a href="' + mw.util.getUrl('User_talk:Perhelion') + '">Perhelion</a> melden.</span>');
mw.notify(note, { title: us.name + ':', type: 'error' });
} else if (status.textStatus === 'timeout' || jqXHR === 'maxlag') {
api.abort();
return us.ajaxRequest(params, onSuccess, 2);
}
});
},
// --- NEW FORMER RIGHTS LOGIC ---
fetchFormerRights: function(isGlobal) {
var apiTarget = isGlobal ? 'https://meta.wikimedia.org/w/api.php' : mw.util.wikiScript('api');
var logType = isGlobal ? 'gblrights' : 'rights';
var userTarget = 'User:' + us.user;
var containerId = isGlobal ? 'us_former_global_groups' : 'us_former_local_groups';
// Globální override pro smazané skupiny (skupina: datum smazání)
var globalOverrides = {
'oathauth-tester': '2026-01-12T10:58:00Z'
};
var params = {
action: 'query',
list: 'logevents',
letype: logType,
letitle: userTarget,
lelimit: 500,
format: 'json',
origin: isGlobal ? '*' : undefined
};
$.getJSON(apiTarget, params, function(res) {
if(!res || !res.query || !res.query.logevents) return;
var logs = res.query.logevents;
var former = {};
var currentGroups = isGlobal ? (data.glGrp || []) : (data.groups || []);
logs.forEach(function(log) {
var p = log.params;
if (!p) return;
var date = log.timestamp;
var oldRaw = p.oldgroups || p.oldGroups || p.remove || p["0"] || "";
var newRaw = p.newgroups || p.newGroups || p.add || p["1"] || "";
var parse = function(val) {
if (Array.isArray(val)) return val;
if (typeof val === 'string') {
if (val === "(none)" || val === "") return [];
return val.split(/[,|]\s*/);
}
return [];
};
var oldG = parse(oldRaw);
var newG = parse(newRaw);
// 1. Nejdřív kontrolujeme udělení (v logu jdeme od nejnovějších k nejstarším)
newG.forEach(function(g) {
g = g.trim();
if (us.implicitGroups.indexOf(g) !== -1 || !g) return;
// Speciální případ: Skupina byla udělena, uživatel ji nemá a víme, že globálně zanikla
if (currentGroups.indexOf(g) === -1 && globalOverrides[g] && !former[g]) {
former[g] = { to: globalOverrides[g], from: date };
}
// Standardní případ: Už známe konec z oldG a teď jsme našli, kdy to začalo
else if (former[g] && !former[g].from) {
former[g].from = date;
}
});
// 2. Poté kontrolujeme odebrání (standardní cesta)
oldG.forEach(function(g) {
g = g.trim();
if (us.implicitGroups.indexOf(g) !== -1 || !g) return;
// Pokud skupinu nemá a ještě jsme o ní v tomto průchodu neslyšeli
if (currentGroups.indexOf(g) === -1 && !former[g]) {
former[g] = { to: date, from: null };
}
});
});
var entries = [];
Object.keys(former).sort().forEach(function(g) {
var item = former[g];
// Aplikace globálního overridu, pokud chybí datum odebrání a skupina je v seznamu smazaných
if (!item.to && globalOverrides[g]) {
item.to = globalOverrides[g];
}
var dateTo = new Date(item.to);
var dateFrom = item.from ? new Date(item.from) : null;
if(dateFrom && item.to) {
var diffText = us.getDateDiff(dateTo, dateFrom).replace(msg.ago.replace('$1', ''), '').trim();
entries.push(g + ' (' + dateFrom.toISOString().split('T')[0] + ' – ' + dateTo.toISOString().split('T')[0] + ', ' + diffText + ')');
} else if (item.to) {
entries.push(g + ' (do ' + dateTo.toISOString().split('T')[0] + ')');
}
});
if(entries.length > 0) {
$('#' + containerId).show().find('.us_former_content').text(entries.join(', '));
}
});
},
getThanks: function (e) {
var params = '&list=logevents&continue=-%7C%7C&leprop=&letype=thanks&ledir=newer&lelimit=max';
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
us.e.attr('title', us.e.text());
us.e.text('');
us.e.injectSpinner('thx');
us.thanks = 0;
} else if (e.lecontinue) {
params += '&lecontinue=' + e.lecontinue;
}
if (us.e.attr('title') === msg.thxGn) params += '&leuser=' + us.user;
else params += '&letitle=User:' + us.user;
}
us.actions.push({ params: params, func: us.writeThanks });
us.doRequest();
},
writeThanks: function (uq) {
if (!uq.query || !uq.query.logevents) return mw.log.warn(uq);
var e = us.e;
var user = 'page=';
var aw = uq.query.logevents;
us.thanks += aw.length;
if (uq.continue) return us.getThanks(uq.continue);
if (e[0].title === msg.thxGn) user += '&user=';
user += us.user;
$.removeSpinner('thx');
e.text(us.thanks);
e.off('click');
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special%3ALog&type=thanks&' + user + '&year=&month=-1&tagfilter=&hide_thanks_log=0';
delete us.e;
},
getUploads: function (e) {
var params = '&list=logevents&letype=upload&ledir=newer&lelimit=max&leuser=' + us.user;
function _start(u) {
var text = u.text();
u.prop('title', text);
u.text('');
u.injectSpinner('upload' + text);
u.data('upl', 0);
u.data('del', 0);
}
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
if (us.e.text() !== msg.nova) {
us.e2 = us.e.nextAll('a').eq(0);
if (us.e2.text() === msg.nova) _start(us.e2); else delete us.e2;
}
_start(us.e);
} else if (e.lecontinue) { params += '&lecontinue=' + e.lecontinue; }
params += '&leprop=ids';
if (us.e.prop('title') === msg.nova) params += '&leaction=upload/upload';
else if (us.e2) params += '|type';
}
us.actions.push({ params: params, func: us.writeUploads });
us.doRequest();
},
writeUploads: function (uq) {
if (!uq.query || !uq.query.logevents || !us.e) return mw.log(uq, us.e);
function _insert(e) {
$.removeSpinner('upload' + e.prop('title'));
e.text('');
e.off('click');
e.append(e.data('del'), $('<span>', { style: us.styleMissingData + us.styleLoading, text: ' (+' + (e.data('upl') - e.data('del')) + ' del.)' }));
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special:Log&type=upload&user=' + us.user + '&subtype=' + ((e[0].title === msg.nova) ? 'upload' : '');
}
var e = us.e;
var e2 = us.e2;
var aw = uq.query.logevents;
var alen = aw.length, i = 0;
var del = e.data('del');
if (e2) {
var upl = e2.data('upl');
var del2 = e2.data('del');
for (i; i < alen; ++i) {
var a = aw[i];
var c = a.pageid ? 1 : 0;
if (a.action === 'upload') { upl++; del2 += c; }
del += c;
}
e2.data('upl', upl);
e2.data('del', del2);
} else {
for (i; i < alen; ++i) if (aw[i].pageid) del++;
}
e.data('upl', e.data('upl') + alen);
e.data('del', del);
if (uq.continue) return us.getUploads(uq.continue);
_insert(e);
if (e2) { _insert(e2); delete us.e2; }
delete us.e;
},
writeCommonInfo: function (uq) {
var aw = uq.query;
if (!aw || !aw.users[0] || Object.prototype.hasOwnProperty.call(aw, 'missing') || Object.prototype.hasOwnProperty.call(aw.users[0], 'invalid')) return us.statusBox.remove();
aw = aw.users[0];
var edits = aw.editcount,
groups = data.groups || aw.groups,
blocked = data.blockreason || aw.blockexpiry,
gender = data.gender || aw.gender,
uploads = $('<a>', { title: msg.count, text: msg.nova, style: us.styleLoading, click: us.getUploads });
if (aw.registration) data.registration = aw.registration;
us.now = us.getDateFromTimestamp(uq.curtimestamp || us.now || mw.now());
us.user = us.user.replace(/ /g, '_');
us.first = 1;
// 1. Poslední akce
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=1',
func: function(res) {
if(res.query && res.query.logevents[0]) {
var diff = us.getDateDiff(us.now, new Date(res.query.logevents[0].timestamp));
$('#us_last_action_val').text(diff);
$('#us_last_action_li').show();
}
}
});
// 2. Poslední admin akce
if (groups && $.inArray('sysop', groups) !== -1) {
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=1&letype=delete|block|protect|rights',
func: function(res) {
if(res.query && res.query.logevents[0]) {
var diff = us.getDateDiff(us.now, new Date(res.query.logevents[0].timestamp));
$('#us_last_admin_val').text(diff);
$('#us_last_admin_li').show();
}
}
});
}
// 3. Poslední CU akce (jen checkuser)
if (groups && $.inArray('checkuser', groups) !== -1) {
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=1&letype=checkuser',
func: function(res) {
if(res.query && res.query.logevents[0]) {
$('#us_last_cu_li').show();
$('#us_last_cu_val').text(us.getDateDiff(us.now, new Date(res.query.logevents[0].timestamp)));
}
}
});
}
// 4. Poslední OS akce (jen oversight/suppress)
if (groups && $.inArray('oversight', groups) !== -1) {
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=1&letype=suppress',
func: function(res) {
if(res.query && res.query.logevents[0]) {
$('#us_last_os_li').show();
$('#us_last_os_val').text(us.getDateDiff(us.now, new Date(res.query.logevents[0].timestamp)));
}
}
});
}
if (edits) {
if (data.editcount && data.lastedit && data.editcount === edits) {
uq.query.usercontribs = [{ timestamp: data.lastedit }];
us.writeLastEdit(uq);
} else {
data.editcount = edits;
us.loading_last_edit.css('display', 'block');
us.actions.push({
params: '&list=usercontribs&ucuser=' + us.user + '&uclimit=1&uccontinue=' + (new Date(us.now).toISOString().replace(/[^\d]/g, '').slice(0, 14) + '|2'),
func: us.writeLastEdit
});
}
$('#t-contributions').remove();
} else { us.writeLastEdit({}); }
if (groups) {
edits = $('<a>', { title: 'Supercount', href: '//tools.wmflabs.org/supercount/index.php?user=' + us.user + '&project=' + location.hostname, text: edits });
if ($.inArray('bot', groups) === -1) {
us.review = $.inArray('editor', groups) !== -1;
if (us.review) {
if (typeof data.reviews === 'number') us.writePatrolCount(data.reviews);
} else if (us.viewPatrolNumber) { us.us_patrolcount_loading.parent().remove(); }
edits = [
edits, ' • ',
$('<a>', { href: '//xtools.wmflabs.org/ec' + mw.config.get('wgServer').substr(1) + '/' + us.user, title: 'XTools', text: 'XTools', style: us.styleLoading, target: '_blank' }), ' • ',
$('<a>', { href: 'https://meta.wikimedia.org/wiki/Special:CentralAuth/' + us.user, title: 'CentralAuth', text: 'CentralAuth', style: us.styleLoading, target: '_blank' }), ' • ',
(project !== 'commonswiki' ? $('<a>', { href: '/wiki/Special:ListFiles/' + us.user, text: 'Upload', title: 'Special:Listfiles', style: us.styleLoading }) : 'Upload'),
(us.lang === 'de' ? '-' : ' ') + msg.count + ': ',
uploads.clone(1).text('total'), ' / ', uploads
];
if (!$('#t-subpages').length) {
edits.push($('<a>', { href: mw.util.getUrl('Special:Prefixindex/User:' + us.user + '/'), text: ' • ' + msg.usub, style: us.styleMissingData + us.styleLoading }));
}
} else if (us.us_patrolcount_loading) { us.us_patrolcount_loading.parent().remove(); }
us.loading_editcount.replaceWith(edits);
if (!aw.implicitgroups && !data.groups) {
aw.implicitgroups = ['*', 'user', 'autoconfirmed'];
if (groups.length < 4) aw.implicitgroups.pop();
}
data.groups = $.grep(groups, function (n) { return $.inArray(n, aw.implicitgroups) === -1; });
us.writeGroups(data.groups);
$('#t-userrights').remove();
}
if (!data.registration) {
us.actions.push({ params: '&list=logevents&leprop=timestamp|type&letype=newusers&lelimit=1&ledir=newer&leend=2005-12-30T00:00:00Z&leuser=' + us.user, func: us.writeRegistration });
if (data.firstedit) {
uq.query.usercontribs = [{ timestamp: data.firstedit }];
us.writeFirstEdit(uq);
} else if (edits) {
us.actions.push({ params: { list: 'usercontribs', ucuser: us.user, uclimit: 1, ucend: '2005-09-08T00:00:00Z', ucdir: 'newer', ucprop: 'timestamp' }, func: us.writeFirstEdit });
}
} else { us.writeRegistration(data.registration); }
us.loading_blocked.replaceWith($('<a>', { style: ((blocked) ? us.styleBlocked : us.styleNotBlocked), id: 'us_block_status_span', href: '/w/index.php?title=Special:Log/block&page=User:' + us.user, title: msg.blocklog, text: ((blocked) ? ' ' : msg.not) + msg.block }));
if (blocked) {
data.blockreason = aw.blockreason || ' ';
us.ul.append([
$('<li>', { id: 'us_block_time' }).append([$('<b>').text('• ' + msg.blockEnd), $.createSpinner('us_block_time_loading')]),
$('<li>', { id: 'us_block_reason' }).append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment, (aw.commenthidden))]),
$('<li>', { id: 'us_blocker' }).append([$('<b>').text('• ' + msg.blocker), $.createSpinner('us_blocker_loading')])
]);
us.actions.push({ params: { list: 'logevents', letitle: 'User:' + us.user, letype: 'block', lelimit: 1 }, func: us.writeBlockDetail });
}
if (data.glGrp && !data.locked) {
uq.query.globaluserinfo = { groups: (data.glGrp.length) ? data.glGrp : null };
us.writeGlobalGroup(uq);
} else {
us.actions.push({ params: '&meta=globaluserinfo&guiprop=groups&guiuser=' + us.user, func: us.writeGlobalGroup });
}
if (gender) {
data.gender = gender;
var genderSn = (gender === 'male') ? ' \u2642' : (gender === 'female' ? ' \u2640' : '');
$('#firstHeading').append($('<span>', { id: 'ps-gender-' + gender, style: 'font-size:80%', text: genderSn }));
}
if (us.actions.length) us.doRequest();
},
createBox: function () {
var statusBox = $('<div>', { style: 'border-bottom:1px solid #aaa;text-shadow:1px 1px 1px #eff;', id: 'us_box' });
if (mw.config.get('skin') === 'vector') statusBox.css('font-size', '0.8em');
var spanFrag = $.createSpinner();
us.loading_editcount = spanFrag.clone();
us.loading_registration = spanFrag.clone();
us.loading_groups = spanFrag.clone();
us.loading_blocked = spanFrag.clone();
us.us_global_group_loading = spanFrag.clone();
us.us_last_edit_loading = $.createSpinner('contribs');
us.loading_last_edit = $('<li>');
us.us_global_group = $('<li>', { style: 'display: none' });
var thx = $('<a>', { title: msg.count, text: msg.thxRd, style: us.styleLoading, click: us.getThanks });
var $userrights = $('#t-userrights a');
var $contributions = $('#t-contributions a');
if (!$userrights.length) $userrights = $('<a>', { href: '/wiki/Special:UserRights/' + us.user });
if (!$contributions.length) $contributions = $('<a>', { href: '/wiki/Special:Contributions/' + us.user });
var ul = $('<ul>', { style: 'list-style: none' }).append($('<li>', { id: 'us_editcount' }).append([$('<b>').append($contributions.text(msg.contrib)), us.loading_editcount]));
us.us_log_count = $('<span>');
if (us.viewPatrolNumber) {
us.us_patrolcount_loading = $('<a>', { style: us.styleLoading, click: us.getPatrolCount, text: msg.count });
us.us_log_count = $('<span>').append([$('<b>').text(msg.reviews), us.us_patrolcount_loading, ' • ']);
}
ul.append(us.us_log_count)
.append($('<li>', { id: 'us_reg_date' }).append([$('<b>').text(msg.regdate), us.loading_registration, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://meta.wikimedia.org/wiki/Special:UserPages/' + us.user, text: '• Global user pages' })]))
.append($('<li>').append([$('<b>').append($userrights.text(msg.loGrp)), us.loading_groups]))
.append($('<li>', { id: 'us_former_local_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerLoGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.us_global_group.append([$('<b>').append($('<a>', { href: 'https://meta.wikimedia.org/wiki/Special:Log?type=gblrights&user=&page=User:' + us.user, text: msg.glGrp })), us.us_global_group_loading]))
.append($('<li>', { id: 'us_former_global_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerGlGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.loading_last_edit.append([$('<b>').text(msg.laedit), us.us_last_edit_loading, ' ', $('<a>', { target: '_blank', style: us.styleMissingData, href: '/w/index.php?title=Special:Log/' + us.user, text: '• LLA' }), $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://meta.wikimedia.org/wiki/Special:GlobalContributions/' + us.user, text: '• Global contributions' })]))
.append($('<li>', { id: 'us_last_action_li', style: 'display:none' }).append([$('<b>').text(msg.laact), $('<span>', { id: 'us_last_action_val' })]))
.append($('<li>', { id: 'us_last_admin_li', style: 'display:none' }).append([$('<b>').text(msg.laadmin), $('<span>', { id: 'us_last_admin_val' })]))
.append($('<li>', { id: 'us_last_cu_li', style: 'display:none' }).append([$('<b>').text(msg.lacu), $('<span>', { id: 'us_last_cu_val' })]))
.append($('<li>', { id: 'us_last_os_li', style: 'display:none' }).append([$('<b>').text(msg.laos), $('<span>', { id: 'us_last_os_val' })]));
statusBox.append(ul);
us.ul = $('<ul>', { style: 'list-style:none' }).append($('<li>', { id: 'us_block_status' }).append([$('<b>').text(msg.blocks), us.loading_blocked, ' • ', $('<b>').text(msg.thxGvng), thx, ' / ', thx.clone(1).text(msg.thxGn), $('<a>', { style: 'float:right;' + us.styleMissingData, title: 'Purge data', text: 'purge', click: us.removeDataStore })]));
statusBox.append(us.ul);
$('#firstHeading').after(statusBox);
us.statusBox = statusBox;
},
removeDataStore: function (e) {
var key = project + us.user, db = window.indexedDB;
e.preventDefault();
if (!db || !us.dbVersion) return mw.cookie.set(us.name + us.user, null);
var request = db.open(us.name, us.dbVersion + 1);
request.onupgradeneeded = function () { db = this.result; if (db.objectStoreNames.contains(key)) db.deleteObjectStore(key); };
request.onsuccess = function () { db = this.result; us.dbVersion = db.version; db.close(); location.reload(); };
},
setCookie: function () {
var domain = (mw.config.get('wgNoticeProject') === 'wikipedia') ? 'wikipedia.org' : '';
var name = us.name + us.user;
var _saveCookie = function (k, d) { mw.cookie.set(name, JSON.stringify(d), { expires: 604800, domain: domain }); };
if (!us.actions[0] && JSON && data.editcount) {
if (us.cookie.length) window.clearTimeout(us.cookie.shift());
var saveData = window.indexedDB ? function (n, JSd, v) {
var key = project + us.user, db = window.indexedDB, request = db.open(us.name, v);
request.onupgradeneeded = function () {
db = this.result;
if (!db.objectStoreNames.contains(key)) {
var store = db.createObjectStore(key, { keyPath: 'name' });
store.createIndex('data', 'data', { unique: false });
}
};
request.onsuccess = function () {
db = this.result; us.dbVersion = db.version;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readwrite').objectStore(key);
store.put({ name: key, data: JSd });
} else { saveData(n, JSd, us.dbVersion + 1); }
};
request.onerror = function() { _saveCookie(n, JSd); };
} : _saveCookie;
us.cookie.push(setTimeout(function () {
data.timestamp = new Date().valueOf();
saveData('', data);
}, 500));
}
},
doRequest: function () {
var action = us.actions.shift();
if (action) us.ajaxRequest(action.params, action.func);
if (us.actions.length) window.setTimeout(function () { us.doRequest(); }, 100);
},
getDateDiff: function (now, date) {
var dStr = [];
if (now > date) {
var years = now.getFullYear() - date.getFullYear();
var months = now.getMonth() - date.getMonth();
var days = now.getDate() - date.getDate();
// Korekce dnů a měsíců
if (days < 0) {
months--;
// Získáme počet dní v předchozím měsíci
var prevMonth = new Date(now.getFullYear(), now.getMonth(), 0).getDate();
days += prevMonth;
}
// Korekce měsíců a roků
if (months < 0) {
years--;
months += 12;
}
var units = [years, months, days];
var labelSingle = [msg.date[0], msg.date[1], msg.date[2]];
var labelPlural = [msg.dates[0], msg.dates[1], msg.dates[2]];
for (var i = 0; i < 3; ++i) {
if (units[i] > 0) {
dStr.push(units[i] + ' ' + ((units[i] > 1) ? labelPlural[i] : labelSingle[i]));
}
}
}
var mainDiff = (dStr.length) ? dStr.join(', ').replace(/(.*),([^,]*)/, msg.and).replace(/(.*)/, msg.ago) : msg.nosec + msg.date[2];
return mainDiff;
},
getDateFromTimestamp: function (t) { if (!t) return false; t = new Date(t); return isNaN(t.valueOf()) ? false : t; },
writeRegistration: function (aw) {
if (aw) {
if (aw instanceof Object) {
if (aw.query && aw.query.logevents.length) aw = aw.query.logevents[0].timestamp;
}
if (typeof aw === 'string') {
us.loading_registration.replaceWith(us.formatDate(aw) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(aw)) + ')' }));
data.registration = aw;
return us.setCookie();
}
}
us.loading_registration.replaceWith($('<span>', { style: us.styleMissingData + us.styleLoading, text: msg.nodb }));
},
writeFirstEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs.length) return;
var date = data.firstedit = aw.query.usercontribs[0].timestamp;
$('#us_reg_date').append($('<li>').append([$('<b>', { text: '• ' + msg.fiedit }), us.formatDate(date) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(date)) + ')' })]));
},
writeLastEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs || !aw.query.usercontribs.length) return us.us_last_edit_loading.replaceWith($('<span>', { style: us.styleMissingData, text: msg.noedit }));
var date = data.lastedit = aw.query.usercontribs[0].timestamp;
us.us_last_edit_loading.replaceWith(us.getDateDiff(us.now, us.getDateFromTimestamp(date)), ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.formatDate(date) + ')' }));
data.timediff = mw.now() - us.now;
$('#us_editcount').append($('<span>', { style: 'float:right;' + us.styleMissingData, text: msg.curtimeDiff }).append($('<b>', { text: Math.round(data.timediff / 1000) })));
us.setCookie();
},
writeGlobalGroup: function (aw) {
aw = aw.query.globaluserinfo;
var groups = data.glGrp = aw.groups;
// Pokud lokální registrace chybí, použijeme tu z globálního infa
if (!data.registration && aw.registration) {
us.writeRegistration(aw.registration);
}
if (groups && groups.length) {
us.us_global_group_loading.replaceWith(us.getLocalNames(groups, 'GlobalGroupPermissions'));
us.us_global_group.css('display', 'block');
// Fetch former global rights
us.fetchFormerRights(true);
}
if (aw.locked !== undefined) {
data.locked = 1;
mw.loader.using('mediawiki.ForeignApi').done(function () {
var fApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php');
fApi.get({
action: 'query',
list: 'logevents',
leprop: 'user|timestamp|comment',
letype: 'globalauth',
letitle: 'User:' + us.user,
lelimit: '1'
}).done(us.writeGlobalBlock);
});
}
us.setCookie();
},
writeGlobalBlock: function (aw) {
if (!aw.query || !aw.query.logevents.length) return;
aw = aw.query.logevents[0];
$('#us_block_status_span').replaceWith($('<a>', { style: us.styleBlocked, id: 'us_block_status_span', href: '//meta.wikimedia.org/w/index.php?title=Special:Log/&type=globalauth&user=&page=User%3A' + us.user + '%40global', text: 'locked (global)' }).append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) })));
$('#us_block_status').parent().append([$('<li>').append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment)]), $('<li>').append([$('<b>').text('• ' + msg.blocker), $('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user })])]);
},
writeBlockDetail: function (aw) {
var duration = 'infinite', expiry = '';
if (aw.query && aw.query.logevents.length) {
aw = aw.query.logevents[0];
if (aw && aw.params) {
duration = aw.params.duration;
expiry = /in(de)?finite/.test(duration) ? msg.never : us.formatDate(aw.params.expiry);
}
$('#us_block_status_span').text(msg.block + ' (' + duration + ')').append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) }));
if (expiry) $('#mw-spinner-us_block_time_loading').replaceWith(expiry); else $('#us_block_time').remove();
$('#mw-spinner-us_block_reason_loading').replaceWith(us.parseComment(aw.comment, aw.commenthidden));
if (aw.user) $('#mw-spinner-us_blocker_loading').replaceWith($('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user }));
else $('#mw-spinner-us_blocker_loading').replaceWith(msg.nodb);
}
},
writePatrolCount: function (aw) {
if (aw instanceof Object) {
if (!aw.query || !aw.query.logevents) return;
us.patrols += aw.query.logevents.length;
if (aw.continue) return us.getPatrolCount(aw.continue);
$.removeSpinner('pat');
aw = us.patrols;
}
us.us_patrolcount_loading.replaceWith($('<a>', { href: '/w/index.php?title=Special:Log&type=' + (us.review ? 'review&subtype=accept' : 'patrol&subtype=patrol') + '&user=' + us.user, text: aw }));
data.reviews = aw;
us.setCookie();
},
getPatrolCount: function (e) {
var params = '&list=logevents&leprop=&ledir=newer&lelimit=max&leuser=' + us.user + '&leaction=' + (us.review ? 'review/approve' : 'patrol/patrol');
if (e instanceof Object) {
if (e.target) { $(e.target).injectSpinner('pat'); us.patrols = 0; }
else if (e.lecontinue) params += '&lecontinue=' + e.lecontinue;
}
us.actions.push({ params: params, func: us.writePatrolCount });
us.doRequest();
},
parseComment: function (text, hidden) {
var comment = $('<span>', { 'class': 'comment' });
if (typeof text === 'undefined') return comment.append(hidden ? msg.noReason : 'undefined').css('color', hidden ? '#999' : 'red');
if (!text) return comment.append(msg.blockCmt).css('color', '#999');
var intLink = /(.*?)\[\[((.*?)\|)?(.*?)\]\](.*)/, suche = text, erg;
while ((erg = intLink.exec(suche)) !== null) {
var link = erg[3] || erg[4];
comment.append([erg[1], $('<a>', { href: mw.util.getUrl(link), title: link }).text(erg[4])]);
suche = erg[5];
}
return comment.append(suche);
},
formatDate: function (datum) {
if (!(datum instanceof Date)) datum = new Date(datum);
try {
return datum.toLocaleDateString(us.lang, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
} catch (e) { return datum.toLocaleString(); }
},
getStoredData: function () {
var key = project + us.user, db, request;
try {
request = indexedDB.open(us.name);
request.onsuccess = function () {
db = this.result;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readonly').objectStore(key);
store.transaction.oncomplete = function () { us.runDataStore(); db.close(); };
store.get(key).onsuccess = function () { if (this.result) data = this.result.data; else us.getCookie(); };
} else { us.getCookie(); }
};
request.onerror = function () { us.getCookie(); };
} catch (e) { us.getCookie(); }
},
getCookie: function () {
data = mw.cookie.get(us.name + us.user);
if (data) try { data = JSON.parse(data); } catch(e) { data = null; }
us.runDataStore();
},
initI18N: function (i18n) {
var chain = mw.language.getFallbackLanguageChain();
for (var i = chain.length - 1; i >= 0; i--) if (chain[i] in i18n) msg = i18n[chain[i]];
},
init: function () {
if (this.statusBox || (this.statusBox = $('#us_box'))[0]) this.statusBox.remove();
this.usprop = 'blockinfo|editcount|gender|groups|implicitgroups|registration';
this.self = mw.config.get('wgUserName') === this.user;
this.initI18N(i18n);
this.createBox();
if (!this.first) {
if (mw.cookie && JSON) { if (window.indexedDB) this.getStoredData(); else this.getCookie(); }
else this.run();
} else this.run();
},
runDataStore: function () {
if (data) {
if (typeof data === 'string') try { data = JSON.parse(data); } catch(e) { data = {}; }
var q = { query: { users: [data] } };
if (data.timestamp) this.now = mw.now() - (data.timediff || 0);
if (data.timestamp && Math.abs((mw.now() - data.timestamp - (data.timediff || 0)) / 1000) < 90) return this.writeCommonInfo(q);
if (data.editcount) this.usprop = 'editcount';
}
data = data || {};
this.run();
},
run: function () {
if (this.self) {
return this.writeCommonInfo({ query: { users: [{ editcount: mw.config.get('wgUserEditCount'), registration: mw.config.get('wgUserRegistration'), groups: mw.config.get('wgUserGroups'), blockreason: '' }] } });
}
us.ajaxRequest({ curtimestamp: 1, list: 'users', ususers: us.user, usprop: us.usprop }, us.writeCommonInfo);
}
};
if ([2, 3].indexOf(mw.config.get('wgNamespaceNumber')) !== -1 && us.user.indexOf('/') === -1 && (!mw.config.get('wgArticleId') || mw.config.get('wgAction') === 'view')) {
$.when(mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.language', 'mediawiki.cookie', 'jquery.spinner', 'mediawiki.ForeignApi']), $.ready).then(function () {
us.api = mw.util.wikiScript('api') + '?action=query&format=json';
us.ajaxRequest('&meta=allmessages&amenableparser=1&amincludelocal=1&refix=group-', us.getGroupNames);
if (!mw.libs.viewerInfo) mw.loader.load('//meta.wikimedia.org/w/index.php?title=User:Perhelion/viewerInfo.js&action=raw&ctype=text/javascript');
});
}
}(jQuery, mediaWiki));
izvjtym3rhbdbree9l1ewwepitfb36b
739962
739958
2026-05-01T10:57:18Z
MrJaroslavik
44012
+
739962
javascript
text/javascript
/**
* @Description: Modified Userstatus.js with Former Groups (Local & Global)
* Original by Steef389, Perhelion.
**/
(function ($, mw) {
'use strict';
var project = window.project || mw.config.get('wgDBname');
var msg, i18n = {
en: {
noReason: 'reason removed',
blockCmt: 'no block comment',
never: 'never',
not: ' not ',
block: 'blocked',
and: '$1 and$2',
count: 'Count',
noedit: 'None (only deleted contributions?)',
nodb: '‹not in database›',
noGrp: 'no extended group',
dates: ['years', 'months', 'days', 'hours', 'minutes', 'seconds'],
date: ['year', 'month', 'day', 'hour', 'minute', 'second'],
nosec: 'less than one ',
curtimeDiff: ' Locale time difference to server time in s: ',
thxGn: 'given',
thxRd: 'received',
nova: 'new',
ago: '$1 ago',
blocklog: 'Block log',
contrib: 'Edits: ',
usub: 'Subpages⬇',
thxGvng: 'Thanksgivings: ',
reviews: 'Active reviews: ',
regdate: 'Registration: ',
laedit: 'Last edited: ',
lala: 'Last log activity',
fiedit: 'First edit: ',
blocks: 'Block-status: ',
loGrp: 'Local user-groups: ',
glGrp: 'Global user-groups: ',
formerLoGrp: 'Former local user-groups: ',
formerGlGrp: 'Former global user-groups: ',
blockEnd: 'Block-end: ',
blocker: 'Blocker: ',
blockReason: 'Block-reason: ',
laact: 'Last action: ',
laadmin: 'Last admin action: ',
lacu: 'Last CU action: ',
laos: 'Last OS action: '
},
cs: {
noReason: 'důvod odstraněn',
blockCmt: 'bez komentáře',
never: 'nikdy',
not: ' ne ',
block: 'zablokován',
and: '$1 a$2',
count: 'Počet',
noedit: 'Žádné (jen smazané?)',
nodb: '‹není v databázi›',
noGrp: 'žádná rozšířená skupina',
dates: ['let', 'měsíců', 'dní', 'hodin', 'minut', 'sekund'],
date: ['rok', 'měsíc', 'den', 'hodina', 'minuta', 'sekunda'],
nosec: 'méně než ',
curtimeDiff: ' Rozdíl lokálního času k serveru v s: ',
thxGn: 'poděkoval',
thxRd: 'obdržel poděkování',
nova: 'nové',
ago: 'před $1',
days: 'dní',
blocklog: 'Kniha zablokování',
contrib: 'Editace: ',
usub: 'Podstránky⬇',
thxGvng: 'Poděkování: ',
reviews: 'Aktivní prověření: ',
regdate: 'Registrace: ',
laedit: 'Poslední editace: ',
lala: 'Poslední aktivita v protokolech',
fiedit: 'První editace: ',
blocks: 'Stav bloku: ',
loGrp: 'Lokální skupiny: ',
glGrp: 'Globální skupiny: ',
formerLoGrp: 'Bývalé lokální skupiny: ',
formerGlGrp: 'Bývalé globální skupiny: ',
blockEnd: 'Konec bloku: ',
blocker: 'Zablokoval: ',
blockReason: 'Důvod bloku: ',
laact: 'Poslední akce: ',
laadmin: 'Poslední admin akce: ',
lacu: 'Poslední CU akce: ',
laos: 'Poslední OS akce: '
}
};
var data;
var us = mw.libs.userstatus = {
name: 'Userstatus',
version: 1.80, // Bumping version for former groups update
lastEditSeconds: false,
styleMissingData: 'color:#999;font-size:90%;',
styleLoading: 'font-style:italic;',
styleBlocked: 'color:#c00',
styleNotBlocked: 'color:#080',
styleFormer: 'margin-left:15px; color:#72777d; font-style:italic; font-size:0.9em;',
viewPatrolNumber: false,
lang: mw.config.get('wgUserLanguage'),
user: mw.config.get('wgTitle'),
cookie: [],
thanks: 0,
patrols: 0,
actions: [],
dbVersion: 0,
implicitGroups: ['*', 'user', 'autoconfirmed'],
getLocalNames: function (groups, specialpage) {
if (groups) {
var arr = [];
specialpage = specialpage || 'ListGroupRights';
for (var i = 0; i < groups.length; ++i) {
var g = groups[i];
var n = us.groupNames[g];
if (n) arr.push('<a href="/wiki/Special:' + specialpage + '#' + g + '">' + n + '</a>');
else arr.push(g);
}
groups = arr.join(', ').replace(/(.*),([^,]*)$/, msg.and);
}
return groups;
},
writeGroups: function (groups) {
groups = us.getLocalNames(groups);
if (!groups) {
groups = $('<span>', {
style: us.styleMissingData + us.styleLoading,
text: msg.noGrp
});
}
us.loading_groups.replaceWith(groups);
// After local groups are written, fetch former local groups
us.fetchFormerRights(false);
},
getGroupNames: function (aw) {
if (!aw || !aw.query) return;
aw = aw.query.allmessages;
var al = aw.length;
var groups = {};
while (al--) {
var an = aw[al];
var name = an.name;
if (/^group-/.test(name)) {
name = RegExp.rightContext || name.substring(6);
groups[name] = an['*'];
}
}
us.groupNames = groups;
us.init();
},
ajaxRequest: function (params, onSuccess, trial) {
var url = us.api;
if (!(params instanceof Object)) {
url += params + '&maxage=2419200&smaxage=2419200';
params = {};
} else {
params.maxlag = 3;
params.maxage = 2419200;
params.smaxage = 2419200;
if (trial) params.maxlag *= trial;
}
var api = $.getJSON(url, params, onSuccess)
.fail(function (jqXHR, status) {
var note = 'Timeout fail, maybe try again!';
if (trial) {
if (!status.textStatus || (status.textStatus !== 'timeout' && jqXHR !== 'maxlag')) note = $('<span>Fehler bitte <a href="' + mw.util.getUrl('User_talk:Perhelion') + '">Perhelion</a> melden.</span>');
mw.notify(note, { title: us.name + ':', type: 'error' });
} else if (status.textStatus === 'timeout' || jqXHR === 'maxlag') {
api.abort();
return us.ajaxRequest(params, onSuccess, 2);
}
});
},
// --- NEW FORMER RIGHTS LOGIC ---
fetchFormerRights: function(isGlobal) {
var apiTarget = isGlobal ? 'https://meta.wikimedia.org/w/api.php' : mw.util.wikiScript('api');
var logType = isGlobal ? 'gblrights' : 'rights';
var userTarget = 'User:' + us.user;
var containerId = isGlobal ? 'us_former_global_groups' : 'us_former_local_groups';
// Globální override pro smazané skupiny (skupina: datum smazání)
var globalOverrides = {
'oathauth-tester': '2026-01-12T10:58:00Z'
};
var params = {
action: 'query',
list: 'logevents',
letype: logType,
letitle: userTarget,
lelimit: 500,
format: 'json',
origin: isGlobal ? '*' : undefined
};
$.getJSON(apiTarget, params, function(res) {
if(!res || !res.query || !res.query.logevents) return;
var logs = res.query.logevents;
var former = {};
var currentGroups = isGlobal ? (data.glGrp || []) : (data.groups || []);
logs.forEach(function(log) {
var p = log.params;
if (!p) return;
var date = log.timestamp;
var oldRaw = p.oldgroups || p.oldGroups || p.remove || p["0"] || "";
var newRaw = p.newgroups || p.newGroups || p.add || p["1"] || "";
var parse = function(val) {
if (Array.isArray(val)) return val;
if (typeof val === 'string') {
if (val === "(none)" || val === "") return [];
return val.split(/[,|]\s*/);
}
return [];
};
var oldG = parse(oldRaw);
var newG = parse(newRaw);
// 1. Nejdřív kontrolujeme udělení (v logu jdeme od nejnovějších k nejstarším)
newG.forEach(function(g) {
g = g.trim();
if (us.implicitGroups.indexOf(g) !== -1 || !g) return;
// Speciální případ: Skupina byla udělena, uživatel ji nemá a víme, že globálně zanikla
if (currentGroups.indexOf(g) === -1 && globalOverrides[g] && !former[g]) {
former[g] = { to: globalOverrides[g], from: date };
}
// Standardní případ: Už známe konec z oldG a teď jsme našli, kdy to začalo
else if (former[g] && !former[g].from) {
former[g].from = date;
}
});
// 2. Poté kontrolujeme odebrání (standardní cesta)
oldG.forEach(function(g) {
g = g.trim();
if (us.implicitGroups.indexOf(g) !== -1 || !g) return;
// Pokud skupinu nemá a ještě jsme o ní v tomto průchodu neslyšeli
if (currentGroups.indexOf(g) === -1 && !former[g]) {
former[g] = { to: date, from: null };
}
});
});
var entries = [];
Object.keys(former).sort().forEach(function(g) {
var item = former[g];
// Aplikace globálního overridu, pokud chybí datum odebrání a skupina je v seznamu smazaných
if (!item.to && globalOverrides[g]) {
item.to = globalOverrides[g];
}
var dateTo = new Date(item.to);
var dateFrom = item.from ? new Date(item.from) : null;
if(dateFrom && item.to) {
var diffText = us.getDateDiff(dateTo, dateFrom).replace(msg.ago.replace('$1', ''), '').trim();
entries.push(g + ' (' + dateFrom.toISOString().split('T')[0] + ' – ' + dateTo.toISOString().split('T')[0] + ', ' + diffText + ')');
} else if (item.to) {
entries.push(g + ' (do ' + dateTo.toISOString().split('T')[0] + ')');
}
});
if(entries.length > 0) {
$('#' + containerId).show().find('.us_former_content').text(entries.join(', '));
}
});
},
getThanks: function (e) {
var params = '&list=logevents&continue=-%7C%7C&leprop=&letype=thanks&ledir=newer&lelimit=max';
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
us.e.attr('title', us.e.text());
us.e.text('');
us.e.injectSpinner('thx');
us.thanks = 0;
} else if (e.lecontinue) {
params += '&lecontinue=' + e.lecontinue;
}
if (us.e.attr('title') === msg.thxGn) params += '&leuser=' + us.user;
else params += '&letitle=User:' + us.user;
}
us.actions.push({ params: params, func: us.writeThanks });
us.doRequest();
},
writeThanks: function (uq) {
if (!uq.query || !uq.query.logevents) return mw.log.warn(uq);
var e = us.e;
var user = 'page=';
var aw = uq.query.logevents;
us.thanks += aw.length;
if (uq.continue) return us.getThanks(uq.continue);
if (e[0].title === msg.thxGn) user += '&user=';
user += us.user;
$.removeSpinner('thx');
e.text(us.thanks);
e.off('click');
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special%3ALog&type=thanks&' + user + '&year=&month=-1&tagfilter=&hide_thanks_log=0';
delete us.e;
},
getUploads: function (e) {
var params = '&list=logevents&letype=upload&ledir=newer&lelimit=max&leuser=' + us.user;
function _start(u) {
var text = u.text();
u.prop('title', text);
u.text('');
u.injectSpinner('upload' + text);
u.data('upl', 0);
u.data('del', 0);
}
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
if (us.e.text() !== msg.nova) {
us.e2 = us.e.nextAll('a').eq(0);
if (us.e2.text() === msg.nova) _start(us.e2); else delete us.e2;
}
_start(us.e);
} else if (e.lecontinue) { params += '&lecontinue=' + e.lecontinue; }
params += '&leprop=ids';
if (us.e.prop('title') === msg.nova) params += '&leaction=upload/upload';
else if (us.e2) params += '|type';
}
us.actions.push({ params: params, func: us.writeUploads });
us.doRequest();
},
writeUploads: function (uq) {
if (!uq.query || !uq.query.logevents || !us.e) return mw.log(uq, us.e);
function _insert(e) {
$.removeSpinner('upload' + e.prop('title'));
e.text('');
e.off('click');
e.append(e.data('del'), $('<span>', { style: us.styleMissingData + us.styleLoading, text: ' (+' + (e.data('upl') - e.data('del')) + ' del.)' }));
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special:Log&type=upload&user=' + us.user + '&subtype=' + ((e[0].title === msg.nova) ? 'upload' : '');
}
var e = us.e;
var e2 = us.e2;
var aw = uq.query.logevents;
var alen = aw.length, i = 0;
var del = e.data('del');
if (e2) {
var upl = e2.data('upl');
var del2 = e2.data('del');
for (i; i < alen; ++i) {
var a = aw[i];
var c = a.pageid ? 1 : 0;
if (a.action === 'upload') { upl++; del2 += c; }
del += c;
}
e2.data('upl', upl);
e2.data('del', del2);
} else {
for (i; i < alen; ++i) if (aw[i].pageid) del++;
}
e.data('upl', e.data('upl') + alen);
e.data('del', del);
if (uq.continue) return us.getUploads(uq.continue);
_insert(e);
if (e2) { _insert(e2); delete us.e2; }
delete us.e;
},
writeCommonInfo: function (uq) {
var aw = uq.query;
if (!aw || !aw.users[0] || Object.prototype.hasOwnProperty.call(aw, 'missing') || Object.prototype.hasOwnProperty.call(aw.users[0], 'invalid')) return us.statusBox.remove();
aw = aw.users[0];
var edits = aw.editcount,
groups = data.groups || aw.groups,
blocked = data.blockreason || aw.blockexpiry,
gender = data.gender || aw.gender,
uploads = $('<a>', { title: msg.count, text: msg.nova, style: us.styleLoading, click: us.getUploads });
if (aw.registration) data.registration = aw.registration;
us.now = us.getDateFromTimestamp(uq.curtimestamp || us.now || mw.now());
us.user = us.user.replace(/ /g, '_');
us.first = 1;
// --- LOG LOGLOGY (Last Actions) ---
// 1. Poslední obecná akce
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=1',
func: function(res) {
if(res.query && res.query.logevents[0]) {
var diff = us.getDateDiff(us.now, new Date(res.query.logevents[0].timestamp));
$('#us_last_action_val').text(diff);
$('#us_last_action_li').show();
}
}
});
// 2. Poslední admin akce
if (groups && $.inArray('sysop', groups) !== -1) {
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=1&letype=delete|block|protect|rights',
func: function(res) {
if(res.query && res.query.logevents[0]) {
var diff = us.getDateDiff(us.now, new Date(res.query.logevents[0].timestamp));
$('#us_last_admin_val').text(diff);
$('#us_last_admin_li').show();
}
}
});
}
// 3. Poslední CU akce
if (groups && $.inArray('checkuser', groups) !== -1) {
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=1&letype=checkuser',
func: function(res) {
if(res.query && res.query.logevents[0]) {
var diff = us.getDateDiff(us.now, new Date(res.query.logevents[0].timestamp));
$('#us_last_cu_val').text(diff);
$('#us_last_cu_li').show();
}
}
});
}
// 4. Poslední OS akce
if (groups && $.inArray('oversight', groups) !== -1) {
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=1&letype=suppress',
func: function(res) {
if(res.query && res.query.logevents[0]) {
var diff = us.getDateDiff(us.now, new Date(res.query.logevents[0].timestamp));
$('#us_last_os_val').text(diff);
$('#us_last_os_li').show();
}
}
});
}
// --- ZBYTEK INFO ---
if (edits) {
if (data.editcount && data.lastedit && data.editcount === edits) {
uq.query.usercontribs = [{ timestamp: data.lastedit }];
us.writeLastEdit(uq);
} else {
data.editcount = edits;
us.loading_last_edit.css('display', 'block');
us.actions.push({
params: '&list=usercontribs&ucuser=' + us.user + '&uclimit=1&uccontinue=' + (new Date(us.now).toISOString().replace(/[^\d]/g, '').slice(0, 14) + '|2'),
func: us.writeLastEdit
});
}
$('#t-contributions').remove();
} else { us.writeLastEdit({}); }
if (groups) {
edits = $('<a>', { title: 'Supercount', href: '//tools.wmflabs.org/supercount/index.php?user=' + us.user + '&project=' + location.hostname, text: edits });
if ($.inArray('bot', groups) === -1) {
us.review = $.inArray('editor', groups) !== -1;
if (us.review) {
if (typeof data.reviews === 'number') us.writePatrolCount(data.reviews);
} else if (us.viewPatrolNumber) { us.us_patrolcount_loading.parent().remove(); }
edits = [
edits, ' • ',
$('<a>', { href: '//xtools.wmflabs.org/ec' + mw.config.get('wgServer').substr(1) + '/' + us.user, title: 'XTools', text: 'XTools', style: us.styleLoading, target: '_blank' }), ' • ',
$('<a>', { href: 'https://meta.wikimedia.org/wiki/Special:CentralAuth/' + us.user, title: 'CentralAuth', text: 'CentralAuth', style: us.styleLoading, target: '_blank' }), ' • ',
(project !== 'commonswiki' ? $('<a>', { href: '/wiki/Special:ListFiles/' + us.user, text: 'Upload', title: 'Special:Listfiles', style: us.styleLoading }) : 'Upload'),
(us.lang === 'de' ? '-' : ' ') + msg.count + ': ',
uploads.clone(1).text('total'), ' / ', uploads
];
if (!$('#t-subpages').length) {
edits.push($('<a>', { href: mw.util.getUrl('Special:Prefixindex/User:' + us.user + '/'), text: ' • ' + msg.usub, style: us.styleMissingData + us.styleLoading }));
}
} else if (us.us_patrolcount_loading) { us.us_patrolcount_loading.parent().remove(); }
us.loading_editcount.replaceWith(edits);
if (!aw.implicitgroups && !data.groups) {
aw.implicitgroups = ['*', 'user', 'autoconfirmed'];
if (groups.length < 4) aw.implicitgroups.pop();
}
data.groups = $.grep(groups, function (n) { return $.inArray(n, aw.implicitgroups) === -1; });
us.writeGroups(data.groups);
$('#t-userrights').remove();
}
if (!data.registration) {
us.actions.push({ params: '&list=logevents&leprop=timestamp|type&letype=newusers&lelimit=1&ledir=newer&leend=2005-12-30T00:00:00Z&leuser=' + us.user, func: us.writeRegistration });
if (data.firstedit) {
uq.query.usercontribs = [{ timestamp: data.firstedit }];
us.writeFirstEdit(uq);
} else if (edits) {
us.actions.push({ params: { list: 'usercontribs', ucuser: us.user, uclimit: 1, ucend: '2005-09-08T00:00:00Z', ucdir: 'newer', ucprop: 'timestamp' }, func: us.writeFirstEdit });
}
} else { us.writeRegistration(data.registration); }
us.loading_blocked.replaceWith($('<a>', { style: ((blocked) ? us.styleBlocked : us.styleNotBlocked), id: 'us_block_status_span', href: '/w/index.php?title=Special:Log/block&page=User:' + us.user, title: msg.blocklog, text: ((blocked) ? ' ' : msg.not) + msg.block }));
if (blocked) {
data.blockreason = aw.blockreason || ' ';
us.ul.append([
$('<li>', { id: 'us_block_time' }).append([$('<b>').text('• ' + msg.blockEnd), $.createSpinner('us_block_time_loading')]),
$('<li>', { id: 'us_block_reason' }).append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment, (aw.commenthidden))]),
$('<li>', { id: 'us_blocker' }).append([$('<b>').text('• ' + msg.blocker), $.createSpinner('us_blocker_loading')])
]);
us.actions.push({ params: { list: 'logevents', letitle: 'User:' + us.user, letype: 'block', lelimit: 1 }, func: us.writeBlockDetail });
}
if (data.glGrp && !data.locked) {
uq.query.globaluserinfo = { groups: (data.glGrp.length) ? data.glGrp : null };
us.writeGlobalGroup(uq);
} else {
us.actions.push({ params: '&meta=globaluserinfo&guiprop=groups&guiuser=' + us.user, func: us.writeGlobalGroup });
}
if (gender) {
data.gender = gender;
var genderSn = (gender === 'male') ? ' \u2642' : (gender === 'female' ? ' \u2640' : '');
$('#firstHeading').append($('<span>', { id: 'ps-gender-' + gender, style: 'font-size:80%', text: genderSn }));
}
if (us.actions.length) us.doRequest();
},
createBox: function () {
var statusBox = $('<div>', { style: 'border-bottom:1px solid #aaa;text-shadow:1px 1px 1px #eff;', id: 'us_box' });
if (mw.config.get('skin') === 'vector') statusBox.css('font-size', '0.8em');
var spanFrag = $.createSpinner();
us.loading_editcount = spanFrag.clone();
us.loading_registration = spanFrag.clone();
us.loading_groups = spanFrag.clone();
us.loading_blocked = spanFrag.clone();
us.us_global_group_loading = spanFrag.clone();
us.us_last_edit_loading = $.createSpinner('contribs');
us.loading_last_edit = $('<li>');
us.us_global_group = $('<li>', { style: 'display: none' });
var thx = $('<a>', { title: msg.count, text: msg.thxRd, style: us.styleLoading, click: us.getThanks });
var $userrights = $('#t-userrights a');
var $contributions = $('#t-contributions a');
if (!$userrights.length) $userrights = $('<a>', { href: '/wiki/Special:UserRights/' + us.user });
if (!$contributions.length) $contributions = $('<a>', { href: '/wiki/Special:Contributions/' + us.user });
var ul = $('<ul>', { style: 'list-style: none' }).append($('<li>', { id: 'us_editcount' }).append([$('<b>').append($contributions.text(msg.contrib)), us.loading_editcount]));
us.us_log_count = $('<span>');
if (us.viewPatrolNumber) {
us.us_patrolcount_loading = $('<a>', { style: us.styleLoading, click: us.getPatrolCount, text: msg.count });
us.us_log_count = $('<span>').append([$('<b>').text(msg.reviews), us.us_patrolcount_loading, ' • ']);
}
ul.append(us.us_log_count)
.append($('<li>', { id: 'us_reg_date' }).append([$('<b>').text(msg.regdate), us.loading_registration, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://meta.wikimedia.org/wiki/Special:UserPages/' + us.user, text: '• Global user pages' })]))
.append($('<li>').append([$('<b>').append($userrights.text(msg.loGrp)), us.loading_groups]))
.append($('<li>', { id: 'us_former_local_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerLoGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.us_global_group.append([$('<b>').append($('<a>', { href: 'https://meta.wikimedia.org/wiki/Special:Log?type=gblrights&user=&page=User:' + us.user, text: msg.glGrp })), us.us_global_group_loading]))
.append($('<li>', { id: 'us_former_global_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerGlGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.loading_last_edit.append([$('<b>').text(msg.laedit), us.us_last_edit_loading, ' ', $('<a>', { target: '_blank', style: us.styleMissingData, href: '/w/index.php?title=Special:Log/' + us.user, text: '• LLA' }), $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://meta.wikimedia.org/wiki/Special:GlobalContributions/' + us.user, text: '• Global contributions' })]))
.append($('<li>', { id: 'us_last_action_li', style: 'display:none' }).append([$('<b>').text(msg.laact), $('<span>', { id: 'us_last_action_val' })]))
.append($('<li>', { id: 'us_last_admin_li', style: 'display:none' }).append([$('<b>').text(msg.laadmin), $('<span>', { id: 'us_last_admin_val' })]))
.append($('<li>', { id: 'us_last_cu_li', style: 'display:none' }).append([$('<b>').text(msg.lacu), $('<span>', { id: 'us_last_cu_val' })]))
.append($('<li>', { id: 'us_last_os_li', style: 'display:none' }).append([$('<b>').text(msg.laos), $('<span>', { id: 'us_last_os_val' })]));
statusBox.append(ul);
us.ul = $('<ul>', { style: 'list-style:none' }).append($('<li>', { id: 'us_block_status' }).append([$('<b>').text(msg.blocks), us.loading_blocked, ' • ', $('<b>').text(msg.thxGvng), thx, ' / ', thx.clone(1).text(msg.thxGn), $('<a>', { style: 'float:right;' + us.styleMissingData, title: 'Purge data', text: 'purge', click: us.removeDataStore })]));
statusBox.append(us.ul);
$('#firstHeading').after(statusBox);
us.statusBox = statusBox;
},
removeDataStore: function (e) {
var key = project + us.user, db = window.indexedDB;
e.preventDefault();
if (!db || !us.dbVersion) return mw.cookie.set(us.name + us.user, null);
var request = db.open(us.name, us.dbVersion + 1);
request.onupgradeneeded = function () { db = this.result; if (db.objectStoreNames.contains(key)) db.deleteObjectStore(key); };
request.onsuccess = function () { db = this.result; us.dbVersion = db.version; db.close(); location.reload(); };
},
setCookie: function () {
var domain = (mw.config.get('wgNoticeProject') === 'wikipedia') ? 'wikipedia.org' : '';
var name = us.name + us.user;
var _saveCookie = function (k, d) { mw.cookie.set(name, JSON.stringify(d), { expires: 604800, domain: domain }); };
if (!us.actions[0] && JSON && data.editcount) {
if (us.cookie.length) window.clearTimeout(us.cookie.shift());
var saveData = window.indexedDB ? function (n, JSd, v) {
var key = project + us.user, db = window.indexedDB, request = db.open(us.name, v);
request.onupgradeneeded = function () {
db = this.result;
if (!db.objectStoreNames.contains(key)) {
var store = db.createObjectStore(key, { keyPath: 'name' });
store.createIndex('data', 'data', { unique: false });
}
};
request.onsuccess = function () {
db = this.result; us.dbVersion = db.version;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readwrite').objectStore(key);
store.put({ name: key, data: JSd });
} else { saveData(n, JSd, us.dbVersion + 1); }
};
request.onerror = function() { _saveCookie(n, JSd); };
} : _saveCookie;
us.cookie.push(setTimeout(function () {
data.timestamp = new Date().valueOf();
saveData('', data);
}, 500));
}
},
doRequest: function () {
var action = us.actions.shift();
if (action) us.ajaxRequest(action.params, action.func);
if (us.actions.length) window.setTimeout(function () { us.doRequest(); }, 100);
},
getDateDiff: function (now, date) {
var dStr = [];
if (now > date) {
var years = now.getFullYear() - date.getFullYear();
var months = now.getMonth() - date.getMonth();
var days = now.getDate() - date.getDate();
// Korekce dnů a měsíců
if (days < 0) {
months--;
// Získáme počet dní v předchozím měsíci
var prevMonth = new Date(now.getFullYear(), now.getMonth(), 0).getDate();
days += prevMonth;
}
// Korekce měsíců a roků
if (months < 0) {
years--;
months += 12;
}
var units = [years, months, days];
var labelSingle = [msg.date[0], msg.date[1], msg.date[2]];
var labelPlural = [msg.dates[0], msg.dates[1], msg.dates[2]];
for (var i = 0; i < 3; ++i) {
if (units[i] > 0) {
dStr.push(units[i] + ' ' + ((units[i] > 1) ? labelPlural[i] : labelSingle[i]));
}
}
}
var mainDiff = (dStr.length) ? dStr.join(', ').replace(/(.*),([^,]*)/, msg.and).replace(/(.*)/, msg.ago) : msg.nosec + msg.date[2];
return mainDiff;
},
getDateFromTimestamp: function (t) { if (!t) return false; t = new Date(t); return isNaN(t.valueOf()) ? false : t; },
writeRegistration: function (aw) {
if (aw) {
if (aw instanceof Object) {
if (aw.query && aw.query.logevents.length) aw = aw.query.logevents[0].timestamp;
}
if (typeof aw === 'string') {
us.loading_registration.replaceWith(us.formatDate(aw) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(aw)) + ')' }));
data.registration = aw;
return us.setCookie();
}
}
us.loading_registration.replaceWith($('<span>', { style: us.styleMissingData + us.styleLoading, text: msg.nodb }));
},
writeFirstEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs.length) return;
var date = data.firstedit = aw.query.usercontribs[0].timestamp;
$('#us_reg_date').append($('<li>').append([$('<b>', { text: '• ' + msg.fiedit }), us.formatDate(date) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(date)) + ')' })]));
},
writeLastEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs || !aw.query.usercontribs.length) return us.us_last_edit_loading.replaceWith($('<span>', { style: us.styleMissingData, text: msg.noedit }));
var date = data.lastedit = aw.query.usercontribs[0].timestamp;
us.us_last_edit_loading.replaceWith(us.getDateDiff(us.now, us.getDateFromTimestamp(date)), ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.formatDate(date) + ')' }));
data.timediff = mw.now() - us.now;
$('#us_editcount').append($('<span>', { style: 'float:right;' + us.styleMissingData, text: msg.curtimeDiff }).append($('<b>', { text: Math.round(data.timediff / 1000) })));
us.setCookie();
},
writeGlobalGroup: function (aw) {
aw = aw.query.globaluserinfo;
var groups = data.glGrp = aw.groups;
// Pokud lokální registrace chybí, použijeme tu z globálního infa
if (!data.registration && aw.registration) {
us.writeRegistration(aw.registration);
}
if (groups && groups.length) {
us.us_global_group_loading.replaceWith(us.getLocalNames(groups, 'GlobalGroupPermissions'));
us.us_global_group.css('display', 'block');
// Fetch former global rights
us.fetchFormerRights(true);
}
if (aw.locked !== undefined) {
data.locked = 1;
mw.loader.using('mediawiki.ForeignApi').done(function () {
var fApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php');
fApi.get({
action: 'query',
list: 'logevents',
leprop: 'user|timestamp|comment',
letype: 'globalauth',
letitle: 'User:' + us.user,
lelimit: '1'
}).done(us.writeGlobalBlock);
});
}
us.setCookie();
},
writeGlobalBlock: function (aw) {
if (!aw.query || !aw.query.logevents.length) return;
aw = aw.query.logevents[0];
$('#us_block_status_span').replaceWith($('<a>', { style: us.styleBlocked, id: 'us_block_status_span', href: '//meta.wikimedia.org/w/index.php?title=Special:Log/&type=globalauth&user=&page=User%3A' + us.user + '%40global', text: 'locked (global)' }).append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) })));
$('#us_block_status').parent().append([$('<li>').append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment)]), $('<li>').append([$('<b>').text('• ' + msg.blocker), $('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user })])]);
},
writeBlockDetail: function (aw) {
var duration = 'infinite', expiry = '';
if (aw.query && aw.query.logevents.length) {
aw = aw.query.logevents[0];
if (aw && aw.params) {
duration = aw.params.duration;
expiry = /in(de)?finite/.test(duration) ? msg.never : us.formatDate(aw.params.expiry);
}
$('#us_block_status_span').text(msg.block + ' (' + duration + ')').append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) }));
if (expiry) $('#mw-spinner-us_block_time_loading').replaceWith(expiry); else $('#us_block_time').remove();
$('#mw-spinner-us_block_reason_loading').replaceWith(us.parseComment(aw.comment, aw.commenthidden));
if (aw.user) $('#mw-spinner-us_blocker_loading').replaceWith($('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user }));
else $('#mw-spinner-us_blocker_loading').replaceWith(msg.nodb);
}
},
writePatrolCount: function (aw) {
if (aw instanceof Object) {
if (!aw.query || !aw.query.logevents) return;
us.patrols += aw.query.logevents.length;
if (aw.continue) return us.getPatrolCount(aw.continue);
$.removeSpinner('pat');
aw = us.patrols;
}
us.us_patrolcount_loading.replaceWith($('<a>', { href: '/w/index.php?title=Special:Log&type=' + (us.review ? 'review&subtype=accept' : 'patrol&subtype=patrol') + '&user=' + us.user, text: aw }));
data.reviews = aw;
us.setCookie();
},
getPatrolCount: function (e) {
var params = '&list=logevents&leprop=&ledir=newer&lelimit=max&leuser=' + us.user + '&leaction=' + (us.review ? 'review/approve' : 'patrol/patrol');
if (e instanceof Object) {
if (e.target) { $(e.target).injectSpinner('pat'); us.patrols = 0; }
else if (e.lecontinue) params += '&lecontinue=' + e.lecontinue;
}
us.actions.push({ params: params, func: us.writePatrolCount });
us.doRequest();
},
parseComment: function (text, hidden) {
var comment = $('<span>', { 'class': 'comment' });
if (typeof text === 'undefined') return comment.append(hidden ? msg.noReason : 'undefined').css('color', hidden ? '#999' : 'red');
if (!text) return comment.append(msg.blockCmt).css('color', '#999');
var intLink = /(.*?)\[\[((.*?)\|)?(.*?)\]\](.*)/, suche = text, erg;
while ((erg = intLink.exec(suche)) !== null) {
var link = erg[3] || erg[4];
comment.append([erg[1], $('<a>', { href: mw.util.getUrl(link), title: link }).text(erg[4])]);
suche = erg[5];
}
return comment.append(suche);
},
formatDate: function (datum) {
if (!(datum instanceof Date)) datum = new Date(datum);
try {
return datum.toLocaleDateString(us.lang, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
} catch (e) { return datum.toLocaleString(); }
},
getStoredData: function () {
var key = project + us.user, db, request;
try {
request = indexedDB.open(us.name);
request.onsuccess = function () {
db = this.result;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readonly').objectStore(key);
store.transaction.oncomplete = function () { us.runDataStore(); db.close(); };
store.get(key).onsuccess = function () { if (this.result) data = this.result.data; else us.getCookie(); };
} else { us.getCookie(); }
};
request.onerror = function () { us.getCookie(); };
} catch (e) { us.getCookie(); }
},
getCookie: function () {
data = mw.cookie.get(us.name + us.user);
if (data) try { data = JSON.parse(data); } catch(e) { data = null; }
us.runDataStore();
},
initI18N: function (i18n) {
var chain = mw.language.getFallbackLanguageChain();
for (var i = chain.length - 1; i >= 0; i--) if (chain[i] in i18n) msg = i18n[chain[i]];
},
init: function () {
if (this.statusBox || (this.statusBox = $('#us_box'))[0]) this.statusBox.remove();
this.usprop = 'blockinfo|editcount|gender|groups|implicitgroups|registration';
this.self = mw.config.get('wgUserName') === this.user;
this.initI18N(i18n);
this.createBox();
if (!this.first) {
if (mw.cookie && JSON) { if (window.indexedDB) this.getStoredData(); else this.getCookie(); }
else this.run();
} else this.run();
},
runDataStore: function () {
if (data) {
if (typeof data === 'string') try { data = JSON.parse(data); } catch(e) { data = {}; }
var q = { query: { users: [data] } };
if (data.timestamp) this.now = mw.now() - (data.timediff || 0);
if (data.timestamp && Math.abs((mw.now() - data.timestamp - (data.timediff || 0)) / 1000) < 90) return this.writeCommonInfo(q);
if (data.editcount) this.usprop = 'editcount';
}
data = data || {};
this.run();
},
run: function () {
if (this.self) {
return this.writeCommonInfo({ query: { users: [{ editcount: mw.config.get('wgUserEditCount'), registration: mw.config.get('wgUserRegistration'), groups: mw.config.get('wgUserGroups'), blockreason: '' }] } });
}
us.ajaxRequest({ curtimestamp: 1, list: 'users', ususers: us.user, usprop: us.usprop }, us.writeCommonInfo);
}
};
if ([2, 3].indexOf(mw.config.get('wgNamespaceNumber')) !== -1 && us.user.indexOf('/') === -1 && (!mw.config.get('wgArticleId') || mw.config.get('wgAction') === 'view')) {
$.when(mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.language', 'mediawiki.cookie', 'jquery.spinner', 'mediawiki.ForeignApi']), $.ready).then(function () {
us.api = mw.util.wikiScript('api') + '?action=query&format=json';
us.ajaxRequest('&meta=allmessages&amenableparser=1&amincludelocal=1&refix=group-', us.getGroupNames);
if (!mw.libs.viewerInfo) mw.loader.load('//meta.wikimedia.org/w/index.php?title=User:Perhelion/viewerInfo.js&action=raw&ctype=text/javascript');
});
}
}(jQuery, mediaWiki));
buhrwahfi14liblu7ddbww7t1p9wwse
739968
739962
2026-05-01T11:01:58Z
MrJaroslavik
44012
+
739968
javascript
text/javascript
/**
* @Description: Modified Userstatus.js with Former Groups (Local & Global)
* Original by Steef389, Perhelion.
**/
(function ($, mw) {
'use strict';
var project = window.project || mw.config.get('wgDBname');
var msg, i18n = {
en: {
noReason: 'reason removed',
blockCmt: 'no block comment',
never: 'never',
not: ' not ',
block: 'blocked',
and: '$1 and$2',
count: 'Count',
noedit: 'None (only deleted contributions?)',
nodb: '‹not in database›',
noGrp: 'no extended group',
dates: ['years', 'months', 'days', 'hours', 'minutes', 'seconds'],
date: ['year', 'month', 'day', 'hour', 'minute', 'second'],
nosec: 'less than one ',
curtimeDiff: ' Locale time difference to server time in s: ',
thxGn: 'given',
thxRd: 'received',
nova: 'new',
ago: '$1 ago',
blocklog: 'Block log',
contrib: 'Edits: ',
usub: 'Subpages⬇',
thxGvng: 'Thanksgivings: ',
reviews: 'Active reviews: ',
regdate: 'Registration: ',
laedit: 'Last edited: ',
lala: 'Last log activity',
fiedit: 'First edit: ',
blocks: 'Block-status: ',
loGrp: 'Local user-groups: ',
glGrp: 'Global user-groups: ',
formerLoGrp: 'Former local user-groups: ',
formerGlGrp: 'Former global user-groups: ',
blockEnd: 'Block-end: ',
blocker: 'Blocker: ',
blockReason: 'Block-reason: ',
laact: 'Last action: ',
laadmin: 'Last admin action: ',
lacu: 'Last CU action: ',
laos: 'Last OS action: '
},
cs: {
noReason: 'důvod odstraněn',
blockCmt: 'bez komentáře',
never: 'nikdy',
not: ' ne ',
block: 'zablokován',
and: '$1 a$2',
count: 'Počet',
noedit: 'Žádné (jen smazané?)',
nodb: '‹není v databázi›',
noGrp: 'žádná rozšířená skupina',
dates: ['let', 'měsíců', 'dní', 'hodin', 'minut', 'sekund'],
date: ['rok', 'měsíc', 'den', 'hodina', 'minuta', 'sekunda'],
nosec: 'méně než ',
curtimeDiff: ' Rozdíl lokálního času k serveru v s: ',
thxGn: 'poděkoval',
thxRd: 'obdržel poděkování',
nova: 'nové',
ago: 'před $1',
days: 'dní',
blocklog: 'Kniha zablokování',
contrib: 'Editace: ',
usub: 'Podstránky⬇',
thxGvng: 'Poděkování: ',
reviews: 'Aktivní prověření: ',
regdate: 'Registrace: ',
laedit: 'Poslední editace: ',
lala: 'Poslední aktivita v protokolech',
fiedit: 'První editace: ',
blocks: 'Stav bloku: ',
loGrp: 'Lokální skupiny: ',
glGrp: 'Globální skupiny: ',
formerLoGrp: 'Bývalé lokální skupiny: ',
formerGlGrp: 'Bývalé globální skupiny: ',
blockEnd: 'Konec bloku: ',
blocker: 'Zablokoval: ',
blockReason: 'Důvod bloku: ',
laact: 'Poslední akce: ',
laadmin: 'Poslední admin akce: ',
lacu: 'Poslední CU akce: ',
laos: 'Poslední OS akce: '
}
};
var data;
var us = mw.libs.userstatus = {
name: 'Userstatus',
version: 1.80, // Bumping version for former groups update
lastEditSeconds: false,
styleMissingData: 'color:#999;font-size:90%;',
styleLoading: 'font-style:italic;',
styleBlocked: 'color:#c00',
styleNotBlocked: 'color:#080',
styleFormer: 'margin-left:15px; color:#72777d; font-style:italic; font-size:0.9em;',
viewPatrolNumber: false,
lang: mw.config.get('wgUserLanguage'),
user: mw.config.get('wgTitle'),
cookie: [],
thanks: 0,
patrols: 0,
actions: [],
dbVersion: 0,
implicitGroups: ['*', 'user', 'autoconfirmed'],
getLocalNames: function (groups, specialpage) {
if (groups) {
var arr = [];
specialpage = specialpage || 'ListGroupRights';
for (var i = 0; i < groups.length; ++i) {
var g = groups[i];
var n = us.groupNames[g];
if (n) arr.push('<a href="/wiki/Special:' + specialpage + '#' + g + '">' + n + '</a>');
else arr.push(g);
}
groups = arr.join(', ').replace(/(.*),([^,]*)$/, msg.and);
}
return groups;
},
writeGroups: function (groups) {
groups = us.getLocalNames(groups);
if (!groups) {
groups = $('<span>', {
style: us.styleMissingData + us.styleLoading,
text: msg.noGrp
});
}
us.loading_groups.replaceWith(groups);
// After local groups are written, fetch former local groups
us.fetchFormerRights(false);
},
getGroupNames: function (aw) {
if (!aw || !aw.query) return;
aw = aw.query.allmessages;
var al = aw.length;
var groups = {};
while (al--) {
var an = aw[al];
var name = an.name;
if (/^group-/.test(name)) {
name = RegExp.rightContext || name.substring(6);
groups[name] = an['*'];
}
}
us.groupNames = groups;
us.init();
},
ajaxRequest: function (params, onSuccess, trial) {
var url = us.api;
if (!(params instanceof Object)) {
url += params + '&maxage=2419200&smaxage=2419200';
params = {};
} else {
params.maxlag = 3;
params.maxage = 2419200;
params.smaxage = 2419200;
if (trial) params.maxlag *= trial;
}
var api = $.getJSON(url, params, onSuccess)
.fail(function (jqXHR, status) {
var note = 'Timeout fail, maybe try again!';
if (trial) {
if (!status.textStatus || (status.textStatus !== 'timeout' && jqXHR !== 'maxlag')) note = $('<span>Fehler bitte <a href="' + mw.util.getUrl('User_talk:Perhelion') + '">Perhelion</a> melden.</span>');
mw.notify(note, { title: us.name + ':', type: 'error' });
} else if (status.textStatus === 'timeout' || jqXHR === 'maxlag') {
api.abort();
return us.ajaxRequest(params, onSuccess, 2);
}
});
},
// --- NEW FORMER RIGHTS LOGIC ---
fetchFormerRights: function(isGlobal) {
var apiTarget = isGlobal ? 'https://meta.wikimedia.org/w/api.php' : mw.util.wikiScript('api');
var logType = isGlobal ? 'gblrights' : 'rights';
var userTarget = 'User:' + us.user;
var containerId = isGlobal ? 'us_former_global_groups' : 'us_former_local_groups';
// Globální override pro smazané skupiny (skupina: datum smazání)
var globalOverrides = {
'oathauth-tester': '2026-01-12T10:58:00Z'
};
var params = {
action: 'query',
list: 'logevents',
letype: logType,
letitle: userTarget,
lelimit: 500,
format: 'json',
origin: isGlobal ? '*' : undefined
};
$.getJSON(apiTarget, params, function(res) {
if(!res || !res.query || !res.query.logevents) return;
var logs = res.query.logevents;
var former = {};
var currentGroups = isGlobal ? (data.glGrp || []) : (data.groups || []);
logs.forEach(function(log) {
var p = log.params;
if (!p) return;
var date = log.timestamp;
var oldRaw = p.oldgroups || p.oldGroups || p.remove || p["0"] || "";
var newRaw = p.newgroups || p.newGroups || p.add || p["1"] || "";
var parse = function(val) {
if (Array.isArray(val)) return val;
if (typeof val === 'string') {
if (val === "(none)" || val === "") return [];
return val.split(/[,|]\s*/);
}
return [];
};
var oldG = parse(oldRaw);
var newG = parse(newRaw);
// 1. Nejdřív kontrolujeme udělení (v logu jdeme od nejnovějších k nejstarším)
newG.forEach(function(g) {
g = g.trim();
if (us.implicitGroups.indexOf(g) !== -1 || !g) return;
// Speciální případ: Skupina byla udělena, uživatel ji nemá a víme, že globálně zanikla
if (currentGroups.indexOf(g) === -1 && globalOverrides[g] && !former[g]) {
former[g] = { to: globalOverrides[g], from: date };
}
// Standardní případ: Už známe konec z oldG a teď jsme našli, kdy to začalo
else if (former[g] && !former[g].from) {
former[g].from = date;
}
});
// 2. Poté kontrolujeme odebrání (standardní cesta)
oldG.forEach(function(g) {
g = g.trim();
if (us.implicitGroups.indexOf(g) !== -1 || !g) return;
// Pokud skupinu nemá a ještě jsme o ní v tomto průchodu neslyšeli
if (currentGroups.indexOf(g) === -1 && !former[g]) {
former[g] = { to: date, from: null };
}
});
});
var entries = [];
Object.keys(former).sort().forEach(function(g) {
var item = former[g];
// Aplikace globálního overridu, pokud chybí datum odebrání a skupina je v seznamu smazaných
if (!item.to && globalOverrides[g]) {
item.to = globalOverrides[g];
}
var dateTo = new Date(item.to);
var dateFrom = item.from ? new Date(item.from) : null;
if(dateFrom && item.to) {
var diffText = us.getDateDiff(dateTo, dateFrom).replace(msg.ago.replace('$1', ''), '').trim();
entries.push(g + ' (' + dateFrom.toISOString().split('T')[0] + ' – ' + dateTo.toISOString().split('T')[0] + ', ' + diffText + ')');
} else if (item.to) {
entries.push(g + ' (do ' + dateTo.toISOString().split('T')[0] + ')');
}
});
if(entries.length > 0) {
$('#' + containerId).show().find('.us_former_content').text(entries.join(', '));
}
});
},
getThanks: function (e) {
var params = '&list=logevents&continue=-%7C%7C&leprop=&letype=thanks&ledir=newer&lelimit=max';
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
us.e.attr('title', us.e.text());
us.e.text('');
us.e.injectSpinner('thx');
us.thanks = 0;
} else if (e.lecontinue) {
params += '&lecontinue=' + e.lecontinue;
}
if (us.e.attr('title') === msg.thxGn) params += '&leuser=' + us.user;
else params += '&letitle=User:' + us.user;
}
us.actions.push({ params: params, func: us.writeThanks });
us.doRequest();
},
writeThanks: function (uq) {
if (!uq.query || !uq.query.logevents) return mw.log.warn(uq);
var e = us.e;
var user = 'page=';
var aw = uq.query.logevents;
us.thanks += aw.length;
if (uq.continue) return us.getThanks(uq.continue);
if (e[0].title === msg.thxGn) user += '&user=';
user += us.user;
$.removeSpinner('thx');
e.text(us.thanks);
e.off('click');
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special%3ALog&type=thanks&' + user + '&year=&month=-1&tagfilter=&hide_thanks_log=0';
delete us.e;
},
getUploads: function (e) {
var params = '&list=logevents&letype=upload&ledir=newer&lelimit=max&leuser=' + us.user;
function _start(u) {
var text = u.text();
u.prop('title', text);
u.text('');
u.injectSpinner('upload' + text);
u.data('upl', 0);
u.data('del', 0);
}
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
if (us.e.text() !== msg.nova) {
us.e2 = us.e.nextAll('a').eq(0);
if (us.e2.text() === msg.nova) _start(us.e2); else delete us.e2;
}
_start(us.e);
} else if (e.lecontinue) { params += '&lecontinue=' + e.lecontinue; }
params += '&leprop=ids';
if (us.e.prop('title') === msg.nova) params += '&leaction=upload/upload';
else if (us.e2) params += '|type';
}
us.actions.push({ params: params, func: us.writeUploads });
us.doRequest();
},
writeUploads: function (uq) {
if (!uq.query || !uq.query.logevents || !us.e) return mw.log(uq, us.e);
function _insert(e) {
$.removeSpinner('upload' + e.prop('title'));
e.text('');
e.off('click');
e.append(e.data('del'), $('<span>', { style: us.styleMissingData + us.styleLoading, text: ' (+' + (e.data('upl') - e.data('del')) + ' del.)' }));
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special:Log&type=upload&user=' + us.user + '&subtype=' + ((e[0].title === msg.nova) ? 'upload' : '');
}
var e = us.e;
var e2 = us.e2;
var aw = uq.query.logevents;
var alen = aw.length, i = 0;
var del = e.data('del');
if (e2) {
var upl = e2.data('upl');
var del2 = e2.data('del');
for (i; i < alen; ++i) {
var a = aw[i];
var c = a.pageid ? 1 : 0;
if (a.action === 'upload') { upl++; del2 += c; }
del += c;
}
e2.data('upl', upl);
e2.data('del', del2);
} else {
for (i; i < alen; ++i) if (aw[i].pageid) del++;
}
e.data('upl', e.data('upl') + alen);
e.data('del', del);
if (uq.continue) return us.getUploads(uq.continue);
_insert(e);
if (e2) { _insert(e2); delete us.e2; }
delete us.e;
},
writeCommonInfo: function (uq) {
var aw = uq.query;
if (!aw || !aw.users[0] || Object.prototype.hasOwnProperty.call(aw, 'missing') || Object.prototype.hasOwnProperty.call(aw.users[0], 'invalid')) return us.statusBox.remove();
aw = aw.users[0];
var edits = aw.editcount,
groups = data.groups || aw.groups,
blocked = data.blockreason || aw.blockexpiry,
gender = data.gender || aw.gender,
uploads = $('<a>', { title: msg.count, text: msg.nova, style: us.styleLoading, click: us.getUploads });
if (aw.registration) data.registration = aw.registration;
us.now = us.getDateFromTimestamp(uq.curtimestamp || us.now || mw.now());
us.user = us.user.replace(/ /g, '_');
us.first = 1;
// POMOCNÁ FUNKCE PRO VÝPIS AKCÍ Z LOGU
var renderLogAction = function(selector, liSelector, timestamp) {
var dateObj = new Date(timestamp);
var diff = us.getDateDiff(us.now, dateObj);
var formattedDate = us.formatDate(dateObj);
$(selector).text(diff).append(' ', $('<span>', { style: us.styleMissingData, text: '(' + formattedDate + ')' }));
$(liSelector).show();
};
// 1. Poslední obecná akce
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=1',
func: function(res) {
if(res.query && res.query.logevents[0]) {
renderLogAction('#us_last_action_val', '#us_last_action_li', res.query.logevents[0].timestamp);
}
}
});
// 2. Poslední admin akce
if (groups && $.inArray('sysop', groups) !== -1) {
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=1&letype=delete|block|protect|rights',
func: function(res) {
if(res.query && res.query.logevents[0]) {
renderLogAction('#us_last_admin_val', '#us_last_admin_li', res.query.logevents[0].timestamp);
}
}
});
}
// 3. Poslední CU akce
if (groups && $.inArray('checkuser', groups) !== -1) {
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=1&letype=checkuser',
func: function(res) {
if(res.query && res.query.logevents[0]) {
renderLogAction('#us_last_cu_val', '#us_last_cu_li', res.query.logevents[0].timestamp);
}
}
});
}
// 4. Poslední OS akce
if (groups && $.inArray('oversight', groups) !== -1) {
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=1&letype=suppress',
func: function(res) {
if(res.query && res.query.logevents[0]) {
renderLogAction('#us_last_os_val', '#us_last_os_li', res.query.logevents[0].timestamp);
}
}
});
}
if (edits) {
if (data.editcount && data.lastedit && data.editcount === edits) {
uq.query.usercontribs = [{ timestamp: data.lastedit }];
us.writeLastEdit(uq);
} else {
data.editcount = edits;
us.loading_last_edit.css('display', 'block');
us.actions.push({
params: '&list=usercontribs&ucuser=' + us.user + '&uclimit=1&uccontinue=' + (new Date(us.now).toISOString().replace(/[^\d]/g, '').slice(0, 14) + '|2'),
func: us.writeLastEdit
});
}
$('#t-contributions').remove();
} else { us.writeLastEdit({}); }
if (groups) {
edits = $('<a>', { title: 'Supercount', href: '//tools.wmflabs.org/supercount/index.php?user=' + us.user + '&project=' + location.hostname, text: edits });
if ($.inArray('bot', groups) === -1) {
us.review = $.inArray('editor', groups) !== -1;
if (us.review) {
if (typeof data.reviews === 'number') us.writePatrolCount(data.reviews);
} else if (us.viewPatrolNumber) { us.us_patrolcount_loading.parent().remove(); }
edits = [
edits, ' • ',
$('<a>', { href: '//xtools.wmflabs.org/ec' + mw.config.get('wgServer').substr(1) + '/' + us.user, title: 'XTools', text: 'XTools', style: us.styleLoading, target: '_blank' }), ' • ',
$('<a>', { href: 'https://meta.wikimedia.org/wiki/Special:CentralAuth/' + us.user, title: 'CentralAuth', text: 'CentralAuth', style: us.styleLoading, target: '_blank' }), ' • ',
(project !== 'commonswiki' ? $('<a>', { href: '/wiki/Special:ListFiles/' + us.user, text: 'Upload', title: 'Special:Listfiles', style: us.styleLoading }) : 'Upload'),
(us.lang === 'de' ? '-' : ' ') + msg.count + ': ',
uploads.clone(1).text('total'), ' / ', uploads
];
if (!$('#t-subpages').length) {
edits.push($('<a>', { href: mw.util.getUrl('Special:Prefixindex/User:' + us.user + '/'), text: ' • ' + msg.usub, style: us.styleMissingData + us.styleLoading }));
}
} else if (us.us_patrolcount_loading) { us.us_patrolcount_loading.parent().remove(); }
us.loading_editcount.replaceWith(edits);
if (!aw.implicitgroups && !data.groups) {
aw.implicitgroups = ['*', 'user', 'autoconfirmed'];
if (groups.length < 4) aw.implicitgroups.pop();
}
data.groups = $.grep(groups, function (n) { return $.inArray(n, aw.implicitgroups) === -1; });
us.writeGroups(data.groups);
$('#t-userrights').remove();
}
if (!data.registration) {
us.actions.push({ params: '&list=logevents&leprop=timestamp|type&letype=newusers&lelimit=1&ledir=newer&leend=2005-12-30T00:00:00Z&leuser=' + us.user, func: us.writeRegistration });
if (data.firstedit) {
uq.query.usercontribs = [{ timestamp: data.firstedit }];
us.writeFirstEdit(uq);
} else if (edits) {
us.actions.push({ params: { list: 'usercontribs', ucuser: us.user, uclimit: 1, ucend: '2005-09-08T00:00:00Z', ucdir: 'newer', ucprop: 'timestamp' }, func: us.writeFirstEdit });
}
} else { us.writeRegistration(data.registration); }
us.loading_blocked.replaceWith($('<a>', { style: ((blocked) ? us.styleBlocked : us.styleNotBlocked), id: 'us_block_status_span', href: '/w/index.php?title=Special:Log/block&page=User:' + us.user, title: msg.blocklog, text: ((blocked) ? ' ' : msg.not) + msg.block }));
if (blocked) {
data.blockreason = aw.blockreason || ' ';
us.ul.append([
$('<li>', { id: 'us_block_time' }).append([$('<b>').text('• ' + msg.blockEnd), $.createSpinner('us_block_time_loading')]),
$('<li>', { id: 'us_block_reason' }).append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment, (aw.commenthidden))]),
$('<li>', { id: 'us_blocker' }).append([$('<b>').text('• ' + msg.blocker), $.createSpinner('us_blocker_loading')])
]);
us.actions.push({ params: { list: 'logevents', letitle: 'User:' + us.user, letype: 'block', lelimit: 1 }, func: us.writeBlockDetail });
}
if (data.glGrp && !data.locked) {
uq.query.globaluserinfo = { groups: (data.glGrp.length) ? data.glGrp : null };
us.writeGlobalGroup(uq);
} else {
us.actions.push({ params: '&meta=globaluserinfo&guiprop=groups&guiuser=' + us.user, func: us.writeGlobalGroup });
}
if (gender) {
data.gender = gender;
var genderSn = (gender === 'male') ? ' \u2642' : (gender === 'female' ? ' \u2640' : '');
$('#firstHeading').append($('<span>', { id: 'ps-gender-' + gender, style: 'font-size:80%', text: genderSn }));
}
if (us.actions.length) us.doRequest();
},
createBox: function () {
var statusBox = $('<div>', { style: 'border-bottom:1px solid #aaa;text-shadow:1px 1px 1px #eff;', id: 'us_box' });
if (mw.config.get('skin') === 'vector') statusBox.css('font-size', '0.8em');
var spanFrag = $.createSpinner();
us.loading_editcount = spanFrag.clone();
us.loading_registration = spanFrag.clone();
us.loading_groups = spanFrag.clone();
us.loading_blocked = spanFrag.clone();
us.us_global_group_loading = spanFrag.clone();
us.us_last_edit_loading = $.createSpinner('contribs');
us.loading_last_edit = $('<li>');
us.us_global_group = $('<li>', { style: 'display: none' });
var thx = $('<a>', { title: msg.count, text: msg.thxRd, style: us.styleLoading, click: us.getThanks });
var $userrights = $('#t-userrights a');
var $contributions = $('#t-contributions a');
if (!$userrights.length) $userrights = $('<a>', { href: '/wiki/Special:UserRights/' + us.user });
if (!$contributions.length) $contributions = $('<a>', { href: '/wiki/Special:Contributions/' + us.user });
var ul = $('<ul>', { style: 'list-style: none' }).append($('<li>', { id: 'us_editcount' }).append([$('<b>').append($contributions.text(msg.contrib)), us.loading_editcount]));
us.us_log_count = $('<span>');
if (us.viewPatrolNumber) {
us.us_patrolcount_loading = $('<a>', { style: us.styleLoading, click: us.getPatrolCount, text: msg.count });
us.us_log_count = $('<span>').append([$('<b>').text(msg.reviews), us.us_patrolcount_loading, ' • ']);
}
ul.append(us.us_log_count)
.append($('<li>', { id: 'us_reg_date' }).append([$('<b>').text(msg.regdate), us.loading_registration, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://meta.wikimedia.org/wiki/Special:UserPages/' + us.user, text: '• Global user pages' })]))
.append($('<li>').append([$('<b>').append($userrights.text(msg.loGrp)), us.loading_groups]))
.append($('<li>', { id: 'us_former_local_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerLoGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.us_global_group.append([$('<b>').append($('<a>', { href: 'https://meta.wikimedia.org/wiki/Special:Log?type=gblrights&user=&page=User:' + us.user, text: msg.glGrp })), us.us_global_group_loading]))
.append($('<li>', { id: 'us_former_global_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerGlGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.loading_last_edit.append([$('<b>').text(msg.laedit), us.us_last_edit_loading, ' ', $('<a>', { target: '_blank', style: us.styleMissingData, href: '/w/index.php?title=Special:Log/' + us.user, text: '• LLA' }), $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://meta.wikimedia.org/wiki/Special:GlobalContributions/' + us.user, text: '• Global contributions' })]))
.append($('<li>', { id: 'us_last_action_li', style: 'display:none' }).append([$('<b>').text(msg.laact), $('<span>', { id: 'us_last_action_val' })]))
.append($('<li>', { id: 'us_last_admin_li', style: 'display:none' }).append([$('<b>').text(msg.laadmin), $('<span>', { id: 'us_last_admin_val' })]))
.append($('<li>', { id: 'us_last_cu_li', style: 'display:none' }).append([$('<b>').text(msg.lacu), $('<span>', { id: 'us_last_cu_val' })]))
.append($('<li>', { id: 'us_last_os_li', style: 'display:none' }).append([$('<b>').text(msg.laos), $('<span>', { id: 'us_last_os_val' })]));
statusBox.append(ul);
us.ul = $('<ul>', { style: 'list-style:none' }).append($('<li>', { id: 'us_block_status' }).append([$('<b>').text(msg.blocks), us.loading_blocked, ' • ', $('<b>').text(msg.thxGvng), thx, ' / ', thx.clone(1).text(msg.thxGn), $('<a>', { style: 'float:right;' + us.styleMissingData, title: 'Purge data', text: 'purge', click: us.removeDataStore })]));
statusBox.append(us.ul);
$('#firstHeading').after(statusBox);
us.statusBox = statusBox;
},
removeDataStore: function (e) {
var key = project + us.user, db = window.indexedDB;
e.preventDefault();
if (!db || !us.dbVersion) return mw.cookie.set(us.name + us.user, null);
var request = db.open(us.name, us.dbVersion + 1);
request.onupgradeneeded = function () { db = this.result; if (db.objectStoreNames.contains(key)) db.deleteObjectStore(key); };
request.onsuccess = function () { db = this.result; us.dbVersion = db.version; db.close(); location.reload(); };
},
setCookie: function () {
var domain = (mw.config.get('wgNoticeProject') === 'wikipedia') ? 'wikipedia.org' : '';
var name = us.name + us.user;
var _saveCookie = function (k, d) { mw.cookie.set(name, JSON.stringify(d), { expires: 604800, domain: domain }); };
if (!us.actions[0] && JSON && data.editcount) {
if (us.cookie.length) window.clearTimeout(us.cookie.shift());
var saveData = window.indexedDB ? function (n, JSd, v) {
var key = project + us.user, db = window.indexedDB, request = db.open(us.name, v);
request.onupgradeneeded = function () {
db = this.result;
if (!db.objectStoreNames.contains(key)) {
var store = db.createObjectStore(key, { keyPath: 'name' });
store.createIndex('data', 'data', { unique: false });
}
};
request.onsuccess = function () {
db = this.result; us.dbVersion = db.version;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readwrite').objectStore(key);
store.put({ name: key, data: JSd });
} else { saveData(n, JSd, us.dbVersion + 1); }
};
request.onerror = function() { _saveCookie(n, JSd); };
} : _saveCookie;
us.cookie.push(setTimeout(function () {
data.timestamp = new Date().valueOf();
saveData('', data);
}, 500));
}
},
doRequest: function () {
var action = us.actions.shift();
if (action) us.ajaxRequest(action.params, action.func);
if (us.actions.length) window.setTimeout(function () { us.doRequest(); }, 100);
},
getDateDiff: function (now, date) {
var dStr = [];
if (now > date) {
var years = now.getFullYear() - date.getFullYear();
var months = now.getMonth() - date.getMonth();
var days = now.getDate() - date.getDate();
// Korekce dnů a měsíců
if (days < 0) {
months--;
// Získáme počet dní v předchozím měsíci
var prevMonth = new Date(now.getFullYear(), now.getMonth(), 0).getDate();
days += prevMonth;
}
// Korekce měsíců a roků
if (months < 0) {
years--;
months += 12;
}
var units = [years, months, days];
var labelSingle = [msg.date[0], msg.date[1], msg.date[2]];
var labelPlural = [msg.dates[0], msg.dates[1], msg.dates[2]];
for (var i = 0; i < 3; ++i) {
if (units[i] > 0) {
dStr.push(units[i] + ' ' + ((units[i] > 1) ? labelPlural[i] : labelSingle[i]));
}
}
}
var mainDiff = (dStr.length) ? dStr.join(', ').replace(/(.*),([^,]*)/, msg.and).replace(/(.*)/, msg.ago) : msg.nosec + msg.date[2];
return mainDiff;
},
getDateFromTimestamp: function (t) { if (!t) return false; t = new Date(t); return isNaN(t.valueOf()) ? false : t; },
writeRegistration: function (aw) {
if (aw) {
if (aw instanceof Object) {
if (aw.query && aw.query.logevents.length) aw = aw.query.logevents[0].timestamp;
}
if (typeof aw === 'string') {
us.loading_registration.replaceWith(us.formatDate(aw) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(aw)) + ')' }));
data.registration = aw;
return us.setCookie();
}
}
us.loading_registration.replaceWith($('<span>', { style: us.styleMissingData + us.styleLoading, text: msg.nodb }));
},
writeFirstEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs.length) return;
var date = data.firstedit = aw.query.usercontribs[0].timestamp;
$('#us_reg_date').append($('<li>').append([$('<b>', { text: '• ' + msg.fiedit }), us.formatDate(date) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(date)) + ')' })]));
},
writeLastEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs || !aw.query.usercontribs.length) return us.us_last_edit_loading.replaceWith($('<span>', { style: us.styleMissingData, text: msg.noedit }));
var date = data.lastedit = aw.query.usercontribs[0].timestamp;
us.us_last_edit_loading.replaceWith(us.getDateDiff(us.now, us.getDateFromTimestamp(date)), ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.formatDate(date) + ')' }));
data.timediff = mw.now() - us.now;
$('#us_editcount').append($('<span>', { style: 'float:right;' + us.styleMissingData, text: msg.curtimeDiff }).append($('<b>', { text: Math.round(data.timediff / 1000) })));
us.setCookie();
},
writeGlobalGroup: function (aw) {
aw = aw.query.globaluserinfo;
var groups = data.glGrp = aw.groups;
// Pokud lokální registrace chybí, použijeme tu z globálního infa
if (!data.registration && aw.registration) {
us.writeRegistration(aw.registration);
}
if (groups && groups.length) {
us.us_global_group_loading.replaceWith(us.getLocalNames(groups, 'GlobalGroupPermissions'));
us.us_global_group.css('display', 'block');
// Fetch former global rights
us.fetchFormerRights(true);
}
if (aw.locked !== undefined) {
data.locked = 1;
mw.loader.using('mediawiki.ForeignApi').done(function () {
var fApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php');
fApi.get({
action: 'query',
list: 'logevents',
leprop: 'user|timestamp|comment',
letype: 'globalauth',
letitle: 'User:' + us.user,
lelimit: '1'
}).done(us.writeGlobalBlock);
});
}
us.setCookie();
},
writeGlobalBlock: function (aw) {
if (!aw.query || !aw.query.logevents.length) return;
aw = aw.query.logevents[0];
$('#us_block_status_span').replaceWith($('<a>', { style: us.styleBlocked, id: 'us_block_status_span', href: '//meta.wikimedia.org/w/index.php?title=Special:Log/&type=globalauth&user=&page=User%3A' + us.user + '%40global', text: 'locked (global)' }).append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) })));
$('#us_block_status').parent().append([$('<li>').append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment)]), $('<li>').append([$('<b>').text('• ' + msg.blocker), $('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user })])]);
},
writeBlockDetail: function (aw) {
var duration = 'infinite', expiry = '';
if (aw.query && aw.query.logevents.length) {
aw = aw.query.logevents[0];
if (aw && aw.params) {
duration = aw.params.duration;
expiry = /in(de)?finite/.test(duration) ? msg.never : us.formatDate(aw.params.expiry);
}
$('#us_block_status_span').text(msg.block + ' (' + duration + ')').append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) }));
if (expiry) $('#mw-spinner-us_block_time_loading').replaceWith(expiry); else $('#us_block_time').remove();
$('#mw-spinner-us_block_reason_loading').replaceWith(us.parseComment(aw.comment, aw.commenthidden));
if (aw.user) $('#mw-spinner-us_blocker_loading').replaceWith($('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user }));
else $('#mw-spinner-us_blocker_loading').replaceWith(msg.nodb);
}
},
writePatrolCount: function (aw) {
if (aw instanceof Object) {
if (!aw.query || !aw.query.logevents) return;
us.patrols += aw.query.logevents.length;
if (aw.continue) return us.getPatrolCount(aw.continue);
$.removeSpinner('pat');
aw = us.patrols;
}
us.us_patrolcount_loading.replaceWith($('<a>', { href: '/w/index.php?title=Special:Log&type=' + (us.review ? 'review&subtype=accept' : 'patrol&subtype=patrol') + '&user=' + us.user, text: aw }));
data.reviews = aw;
us.setCookie();
},
getPatrolCount: function (e) {
var params = '&list=logevents&leprop=&ledir=newer&lelimit=max&leuser=' + us.user + '&leaction=' + (us.review ? 'review/approve' : 'patrol/patrol');
if (e instanceof Object) {
if (e.target) { $(e.target).injectSpinner('pat'); us.patrols = 0; }
else if (e.lecontinue) params += '&lecontinue=' + e.lecontinue;
}
us.actions.push({ params: params, func: us.writePatrolCount });
us.doRequest();
},
parseComment: function (text, hidden) {
var comment = $('<span>', { 'class': 'comment' });
if (typeof text === 'undefined') return comment.append(hidden ? msg.noReason : 'undefined').css('color', hidden ? '#999' : 'red');
if (!text) return comment.append(msg.blockCmt).css('color', '#999');
var intLink = /(.*?)\[\[((.*?)\|)?(.*?)\]\](.*)/, suche = text, erg;
while ((erg = intLink.exec(suche)) !== null) {
var link = erg[3] || erg[4];
comment.append([erg[1], $('<a>', { href: mw.util.getUrl(link), title: link }).text(erg[4])]);
suche = erg[5];
}
return comment.append(suche);
},
formatDate: function (datum) {
if (!(datum instanceof Date)) datum = new Date(datum);
try {
return datum.toLocaleDateString(us.lang, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
} catch (e) { return datum.toLocaleString(); }
},
getStoredData: function () {
var key = project + us.user, db, request;
try {
request = indexedDB.open(us.name);
request.onsuccess = function () {
db = this.result;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readonly').objectStore(key);
store.transaction.oncomplete = function () { us.runDataStore(); db.close(); };
store.get(key).onsuccess = function () { if (this.result) data = this.result.data; else us.getCookie(); };
} else { us.getCookie(); }
};
request.onerror = function () { us.getCookie(); };
} catch (e) { us.getCookie(); }
},
getCookie: function () {
data = mw.cookie.get(us.name + us.user);
if (data) try { data = JSON.parse(data); } catch(e) { data = null; }
us.runDataStore();
},
initI18N: function (i18n) {
var chain = mw.language.getFallbackLanguageChain();
for (var i = chain.length - 1; i >= 0; i--) if (chain[i] in i18n) msg = i18n[chain[i]];
},
init: function () {
if (this.statusBox || (this.statusBox = $('#us_box'))[0]) this.statusBox.remove();
this.usprop = 'blockinfo|editcount|gender|groups|implicitgroups|registration';
this.self = mw.config.get('wgUserName') === this.user;
this.initI18N(i18n);
this.createBox();
if (!this.first) {
if (mw.cookie && JSON) { if (window.indexedDB) this.getStoredData(); else this.getCookie(); }
else this.run();
} else this.run();
},
runDataStore: function () {
if (data) {
if (typeof data === 'string') try { data = JSON.parse(data); } catch(e) { data = {}; }
var q = { query: { users: [data] } };
if (data.timestamp) this.now = mw.now() - (data.timediff || 0);
if (data.timestamp && Math.abs((mw.now() - data.timestamp - (data.timediff || 0)) / 1000) < 90) return this.writeCommonInfo(q);
if (data.editcount) this.usprop = 'editcount';
}
data = data || {};
this.run();
},
run: function () {
if (this.self) {
return this.writeCommonInfo({ query: { users: [{ editcount: mw.config.get('wgUserEditCount'), registration: mw.config.get('wgUserRegistration'), groups: mw.config.get('wgUserGroups'), blockreason: '' }] } });
}
us.ajaxRequest({ curtimestamp: 1, list: 'users', ususers: us.user, usprop: us.usprop }, us.writeCommonInfo);
}
};
if ([2, 3].indexOf(mw.config.get('wgNamespaceNumber')) !== -1 && us.user.indexOf('/') === -1 && (!mw.config.get('wgArticleId') || mw.config.get('wgAction') === 'view')) {
$.when(mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.language', 'mediawiki.cookie', 'jquery.spinner', 'mediawiki.ForeignApi']), $.ready).then(function () {
us.api = mw.util.wikiScript('api') + '?action=query&format=json';
us.ajaxRequest('&meta=allmessages&amenableparser=1&amincludelocal=1&refix=group-', us.getGroupNames);
if (!mw.libs.viewerInfo) mw.loader.load('//meta.wikimedia.org/w/index.php?title=User:Perhelion/viewerInfo.js&action=raw&ctype=text/javascript');
});
}
}(jQuery, mediaWiki));
n2gzs7edlagb5v6n3ibgvnemfzivmgt
739970
739968
2026-05-01T11:35:29Z
MrJaroslavik
44012
+
739970
javascript
text/javascript
/**
* @Description: Modified Userstatus.js with Former Groups (Local & Global)
* Original by Steef389, Perhelion.
**/
(function ($, mw) {
'use strict';
var project = window.project || mw.config.get('wgDBname');
var msg, i18n = {
en: {
noReason: 'reason removed',
blockCmt: 'no block comment',
never: 'never',
not: ' not ',
block: 'blocked',
and: '$1 and$2',
count: 'Count',
noedit: 'None (only deleted contributions?)',
nodb: '‹not in database›',
noGrp: 'no extended group',
dates: ['years', 'months', 'days', 'hours', 'minutes', 'seconds'],
date: ['year', 'month', 'day', 'hour', 'minute', 'second'],
nosec: 'less than one ',
curtimeDiff: ' Locale time difference to server time in s: ',
thxGn: 'given',
thxRd: 'received',
nova: 'new',
ago: '$1 ago',
blocklog: 'Block log',
contrib: 'Edits: ',
usub: 'Subpages⬇',
thxGvng: 'Thanksgivings: ',
reviews: 'Active reviews: ',
regdate: 'Registration: ',
laedit: 'Last edited: ',
lala: 'Last log activity',
fiedit: 'First edit: ',
blocks: 'Block-status: ',
loGrp: 'Local user-groups: ',
glGrp: 'Global user-groups: ',
formerLoGrp: 'Former local user-groups: ',
formerGlGrp: 'Former global user-groups: ',
blockEnd: 'Block-end: ',
blocker: 'Blocker: ',
blockReason: 'Block-reason: ',
laact: 'Last action: ',
laadmin: 'Last admin action: ',
lacu: 'Last CU action: ',
laos: 'Last OS action: '
},
cs: {
noReason: 'důvod odstraněn',
blockCmt: 'bez komentáře',
never: 'nikdy',
not: ' ne ',
block: 'zablokován',
and: '$1 a$2',
count: 'Počet',
noedit: 'Žádné (jen smazané?)',
nodb: '‹není v databázi›',
noGrp: 'žádná rozšířená skupina',
dates: ['let', 'měsíců', 'dní', 'hodin', 'minut', 'sekund'],
date: ['rok', 'měsíc', 'den', 'hodina', 'minuta', 'sekunda'],
nosec: 'méně než ',
curtimeDiff: ' Rozdíl lokálního času k serveru v s: ',
thxGn: 'poděkoval',
thxRd: 'obdržel poděkování',
nova: 'nové',
ago: 'před $1',
days: 'dní',
blocklog: 'Kniha zablokování',
contrib: 'Editace: ',
usub: 'Podstránky⬇',
thxGvng: 'Poděkování: ',
reviews: 'Aktivní prověření: ',
regdate: 'Registrace: ',
laedit: 'Poslední editace: ',
lala: 'Poslední aktivita v protokolech',
fiedit: 'První editace: ',
blocks: 'Stav bloku: ',
loGrp: 'Lokální skupiny: ',
glGrp: 'Globální skupiny: ',
formerLoGrp: 'Bývalé lokální skupiny: ',
formerGlGrp: 'Bývalé globální skupiny: ',
blockEnd: 'Konec bloku: ',
blocker: 'Zablokoval: ',
blockReason: 'Důvod bloku: ',
laact: 'Poslední akce: ',
laadmin: 'Poslední admin akce: ',
lacu: 'Poslední CU akce: ',
laos: 'Poslední OS akce: '
}
};
var data;
var us = mw.libs.userstatus = {
name: 'Userstatus',
version: 1.80, // Bumping version for former groups update
lastEditSeconds: false,
styleMissingData: 'color:#999;font-size:90%;',
styleLoading: 'font-style:italic;',
styleBlocked: 'color:#c00',
styleNotBlocked: 'color:#080',
styleFormer: 'margin-left:15px; color:#72777d; font-style:italic; font-size:0.9em;',
viewPatrolNumber: false,
lang: mw.config.get('wgUserLanguage'),
user: mw.config.get('wgTitle'),
cookie: [],
thanks: 0,
patrols: 0,
actions: [],
dbVersion: 0,
implicitGroups: ['*', 'user', 'autoconfirmed'],
getLocalNames: function (groups, specialpage) {
if (groups) {
var arr = [];
specialpage = specialpage || 'ListGroupRights';
for (var i = 0; i < groups.length; ++i) {
var g = groups[i];
var n = us.groupNames[g];
if (n) arr.push('<a href="/wiki/Special:' + specialpage + '#' + g + '">' + n + '</a>');
else arr.push(g);
}
groups = arr.join(', ').replace(/(.*),([^,]*)$/, msg.and);
}
return groups;
},
writeGroups: function (groups) {
groups = us.getLocalNames(groups);
if (!groups) {
groups = $('<span>', {
style: us.styleMissingData + us.styleLoading,
text: msg.noGrp
});
}
us.loading_groups.replaceWith(groups);
// After local groups are written, fetch former local groups
us.fetchFormerRights(false);
},
getGroupNames: function (aw) {
if (!aw || !aw.query) return;
aw = aw.query.allmessages;
var al = aw.length;
var groups = {};
while (al--) {
var an = aw[al];
var name = an.name;
if (/^group-/.test(name)) {
name = RegExp.rightContext || name.substring(6);
groups[name] = an['*'];
}
}
us.groupNames = groups;
us.init();
},
ajaxRequest: function (params, onSuccess, trial) {
var url = us.api;
if (!(params instanceof Object)) {
url += params + '&maxage=2419200&smaxage=2419200';
params = {};
} else {
params.maxlag = 3;
params.maxage = 2419200;
params.smaxage = 2419200;
if (trial) params.maxlag *= trial;
}
var api = $.getJSON(url, params, onSuccess)
.fail(function (jqXHR, status) {
var note = 'Timeout fail, maybe try again!';
if (trial) {
if (!status.textStatus || (status.textStatus !== 'timeout' && jqXHR !== 'maxlag')) note = $('<span>Fehler bitte <a href="' + mw.util.getUrl('User_talk:Perhelion') + '">Perhelion</a> melden.</span>');
mw.notify(note, { title: us.name + ':', type: 'error' });
} else if (status.textStatus === 'timeout' || jqXHR === 'maxlag') {
api.abort();
return us.ajaxRequest(params, onSuccess, 2);
}
});
},
// --- NEW FORMER RIGHTS LOGIC ---
fetchFormerRights: function(isGlobal) {
var apiTarget = isGlobal ? 'https://meta.wikimedia.org/w/api.php' : mw.util.wikiScript('api');
var logType = isGlobal ? 'gblrights' : 'rights';
var userTarget = 'User:' + us.user;
var containerId = isGlobal ? 'us_former_global_groups' : 'us_former_local_groups';
// Globální override pro smazané skupiny (skupina: datum smazání)
var globalOverrides = {
'oathauth-tester': '2026-01-12T10:58:00Z'
};
var params = {
action: 'query',
list: 'logevents',
letype: logType,
letitle: userTarget,
lelimit: 500,
format: 'json',
origin: isGlobal ? '*' : undefined
};
$.getJSON(apiTarget, params, function(res) {
if(!res || !res.query || !res.query.logevents) return;
var logs = res.query.logevents;
var former = {};
var currentGroups = isGlobal ? (data.glGrp || []) : (data.groups || []);
logs.forEach(function(log) {
var p = log.params;
if (!p) return;
var date = log.timestamp;
var oldRaw = p.oldgroups || p.oldGroups || p.remove || p["0"] || "";
var newRaw = p.newgroups || p.newGroups || p.add || p["1"] || "";
var parse = function(val) {
if (Array.isArray(val)) return val;
if (typeof val === 'string') {
if (val === "(none)" || val === "") return [];
return val.split(/[,|]\s*/);
}
return [];
};
var oldG = parse(oldRaw);
var newG = parse(newRaw);
// 1. Nejdřív kontrolujeme udělení (v logu jdeme od nejnovějších k nejstarším)
newG.forEach(function(g) {
g = g.trim();
if (us.implicitGroups.indexOf(g) !== -1 || !g) return;
// Speciální případ: Skupina byla udělena, uživatel ji nemá a víme, že globálně zanikla
if (currentGroups.indexOf(g) === -1 && globalOverrides[g] && !former[g]) {
former[g] = { to: globalOverrides[g], from: date };
}
// Standardní případ: Už známe konec z oldG a teď jsme našli, kdy to začalo
else if (former[g] && !former[g].from) {
former[g].from = date;
}
});
// 2. Poté kontrolujeme odebrání (standardní cesta)
oldG.forEach(function(g) {
g = g.trim();
if (us.implicitGroups.indexOf(g) !== -1 || !g) return;
// Pokud skupinu nemá a ještě jsme o ní v tomto průchodu neslyšeli
if (currentGroups.indexOf(g) === -1 && !former[g]) {
former[g] = { to: date, from: null };
}
});
});
var entries = [];
Object.keys(former).sort().forEach(function(g) {
var item = former[g];
// Aplikace globálního overridu, pokud chybí datum odebrání a skupina je v seznamu smazaných
if (!item.to && globalOverrides[g]) {
item.to = globalOverrides[g];
}
var dateTo = new Date(item.to);
var dateFrom = item.from ? new Date(item.from) : null;
if(dateFrom && item.to) {
var diffText = us.getDateDiff(dateTo, dateFrom).replace(msg.ago.replace('$1', ''), '').trim();
entries.push(g + ' (' + dateFrom.toISOString().split('T')[0] + ' – ' + dateTo.toISOString().split('T')[0] + ', ' + diffText + ')');
} else if (item.to) {
entries.push(g + ' (do ' + dateTo.toISOString().split('T')[0] + ')');
}
});
if(entries.length > 0) {
$('#' + containerId).show().find('.us_former_content').text(entries.join(', '));
}
});
},
getThanks: function (e) {
var params = '&list=logevents&continue=-%7C%7C&leprop=&letype=thanks&ledir=newer&lelimit=max';
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
us.e.attr('title', us.e.text());
us.e.text('');
us.e.injectSpinner('thx');
us.thanks = 0;
} else if (e.lecontinue) {
params += '&lecontinue=' + e.lecontinue;
}
if (us.e.attr('title') === msg.thxGn) params += '&leuser=' + us.user;
else params += '&letitle=User:' + us.user;
}
us.actions.push({ params: params, func: us.writeThanks });
us.doRequest();
},
writeThanks: function (uq) {
if (!uq.query || !uq.query.logevents) return mw.log.warn(uq);
var e = us.e;
var user = 'page=';
var aw = uq.query.logevents;
us.thanks += aw.length;
if (uq.continue) return us.getThanks(uq.continue);
if (e[0].title === msg.thxGn) user += '&user=';
user += us.user;
$.removeSpinner('thx');
e.text(us.thanks);
e.off('click');
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special%3ALog&type=thanks&' + user + '&year=&month=-1&tagfilter=&hide_thanks_log=0';
delete us.e;
},
getUploads: function (e) {
var params = '&list=logevents&letype=upload&ledir=newer&lelimit=max&leuser=' + us.user;
function _start(u) {
var text = u.text();
u.prop('title', text);
u.text('');
u.injectSpinner('upload' + text);
u.data('upl', 0);
u.data('del', 0);
}
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
if (us.e.text() !== msg.nova) {
us.e2 = us.e.nextAll('a').eq(0);
if (us.e2.text() === msg.nova) _start(us.e2); else delete us.e2;
}
_start(us.e);
} else if (e.lecontinue) { params += '&lecontinue=' + e.lecontinue; }
params += '&leprop=ids';
if (us.e.prop('title') === msg.nova) params += '&leaction=upload/upload';
else if (us.e2) params += '|type';
}
us.actions.push({ params: params, func: us.writeUploads });
us.doRequest();
},
writeUploads: function (uq) {
if (!uq.query || !uq.query.logevents || !us.e) return mw.log(uq, us.e);
function _insert(e) {
$.removeSpinner('upload' + e.prop('title'));
e.text('');
e.off('click');
e.append(e.data('del'), $('<span>', { style: us.styleMissingData + us.styleLoading, text: ' (+' + (e.data('upl') - e.data('del')) + ' del.)' }));
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special:Log&type=upload&user=' + us.user + '&subtype=' + ((e[0].title === msg.nova) ? 'upload' : '');
}
var e = us.e;
var e2 = us.e2;
var aw = uq.query.logevents;
var alen = aw.length, i = 0;
var del = e.data('del');
if (e2) {
var upl = e2.data('upl');
var del2 = e2.data('del');
for (i; i < alen; ++i) {
var a = aw[i];
var c = a.pageid ? 1 : 0;
if (a.action === 'upload') { upl++; del2 += c; }
del += c;
}
e2.data('upl', upl);
e2.data('del', del2);
} else {
for (i; i < alen; ++i) if (aw[i].pageid) del++;
}
e.data('upl', e.data('upl') + alen);
e.data('del', del);
if (uq.continue) return us.getUploads(uq.continue);
_insert(e);
if (e2) { _insert(e2); delete us.e2; }
delete us.e;
},
writeCommonInfo: function (uq) {
var aw = uq.query;
if (!aw || !aw.users[0] || Object.prototype.hasOwnProperty.call(aw, 'missing') || Object.prototype.hasOwnProperty.call(aw.users[0], 'invalid')) return us.statusBox.remove();
aw = aw.users[0];
var edits = aw.editcount,
groups = data.groups || aw.groups,
blocked = data.blockreason || aw.blockexpiry,
gender = data.gender || aw.gender,
uploads = $('<a>', { title: msg.count, text: msg.nova, style: us.styleLoading, click: us.getUploads });
if (aw.registration) data.registration = aw.registration;
us.now = us.getDateFromTimestamp(uq.curtimestamp || us.now || mw.now());
us.user = us.user.replace(/ /g, '_');
us.first = 1;
// POMOCNÁ FUNKCE PRO VÝPIS AKCÍ Z LOGU
var renderLogAction = function(selector, liSelector, timestamp) {
var dateObj = new Date(timestamp);
var diff = us.getDateDiff(us.now, dateObj);
var formattedDate = us.formatDate(dateObj);
$(selector).text(diff).append(' ', $('<span>', { style: us.styleMissingData, text: '(' + formattedDate + ')' }));
$(liSelector).show();
};
// A. OBECNÉ A ADMIN AKCE (Jeden call, filtrování v JS)
var adminTypes = ['block', 'delete', 'protect', 'rights', 'merge', 'abusefilter', 'renameuser', 'gblblock'];
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=50',
func: function(res) {
if(!res.query || !res.query.logevents) return;
var logs = res.query.logevents;
// 1. Poslední jakákoliv akce (vždy první v seznamu)
if(logs[0]) renderLogAction('#us_last_action_val', '#us_last_action_li', logs[0].timestamp);
// 2. Najdi první admin akci v balíku posledních 50 akcí
var adm = logs.find(function(l) { return adminTypes.indexOf(l.type) !== -1; });
if(adm) renderLogAction('#us_last_admin_val', '#us_last_admin_li', adm.timestamp);
}
});
// B. CU AKCE (Používáme checkuserlog modul)
if (groups && $.inArray('checkuser', groups) !== -1) {
us.actions.push({
params: '&list=checkuserlog&culuser=' + us.user + '&cullimit=1',
func: function(res) {
if(res.query && res.query.checkuserlog && res.query.checkuserlog.entries && res.query.checkuserlog.entries[0]) {
renderLogAction('#us_last_cu_val', '#us_last_cu_li', res.query.checkuserlog.entries[0].timestamp);
}
}
});
}
// C. OS AKCE (Log events typ suppress)
if (groups && $.inArray('oversight', groups) !== -1) {
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=1&letype=suppress',
func: function(res) {
if(res.query && res.query.logevents[0]) {
renderLogAction('#us_last_os_val', '#us_last_os_li', res.query.logevents[0].timestamp);
}
}
});
}
if (edits) {
if (data.editcount && data.lastedit && data.editcount === edits) {
uq.query.usercontribs = [{ timestamp: data.lastedit }];
us.writeLastEdit(uq);
} else {
data.editcount = edits;
us.loading_last_edit.css('display', 'block');
us.actions.push({
params: '&list=usercontribs&ucuser=' + us.user + '&uclimit=1&uccontinue=' + (new Date(us.now).toISOString().replace(/[^\d]/g, '').slice(0, 14) + '|2'),
func: us.writeLastEdit
});
}
$('#t-contributions').remove();
} else { us.writeLastEdit({}); }
if (groups) {
edits = $('<a>', { title: 'Supercount', href: '//tools.wmflabs.org/supercount/index.php?user=' + us.user + '&project=' + location.hostname, text: edits });
if ($.inArray('bot', groups) === -1) {
us.review = $.inArray('editor', groups) !== -1;
if (us.review) {
if (typeof data.reviews === 'number') us.writePatrolCount(data.reviews);
} else if (us.viewPatrolNumber) { us.us_patrolcount_loading.parent().remove(); }
edits = [
edits, ' • ',
$('<a>', { href: '//xtools.wmflabs.org/ec' + mw.config.get('wgServer').substr(1) + '/' + us.user, title: 'XTools', text: 'XTools', style: us.styleLoading, target: '_blank' }), ' • ',
$('<a>', { href: 'https://meta.wikimedia.org/wiki/Special:CentralAuth/' + us.user, title: 'CentralAuth', text: 'CentralAuth', style: us.styleLoading, target: '_blank' }), ' • ',
(project !== 'commonswiki' ? $('<a>', { href: '/wiki/Special:ListFiles/' + us.user, text: 'Upload', title: 'Special:Listfiles', style: us.styleLoading }) : 'Upload'),
(us.lang === 'de' ? '-' : ' ') + msg.count + ': ',
uploads.clone(1).text('total'), ' / ', uploads
];
if (!$('#t-subpages').length) {
edits.push($('<a>', { href: mw.util.getUrl('Special:Prefixindex/User:' + us.user + '/'), text: ' • ' + msg.usub, style: us.styleMissingData + us.styleLoading }));
}
} else if (us.us_patrolcount_loading) { us.us_patrolcount_loading.parent().remove(); }
us.loading_editcount.replaceWith(edits);
if (!aw.implicitgroups && !data.groups) {
aw.implicitgroups = ['*', 'user', 'autoconfirmed'];
if (groups.length < 4) aw.implicitgroups.pop();
}
data.groups = $.grep(groups, function (n) { return $.inArray(n, aw.implicitgroups) === -1; });
us.writeGroups(data.groups);
$('#t-userrights').remove();
}
if (!data.registration) {
us.actions.push({ params: '&list=logevents&leprop=timestamp|type&letype=newusers&lelimit=1&ledir=newer&leend=2005-12-30T00:00:00Z&leuser=' + us.user, func: us.writeRegistration });
if (data.firstedit) {
uq.query.usercontribs = [{ timestamp: data.firstedit }];
us.writeFirstEdit(uq);
} else if (edits) {
us.actions.push({ params: { list: 'usercontribs', ucuser: us.user, uclimit: 1, ucend: '2005-09-08T00:00:00Z', ucdir: 'newer', ucprop: 'timestamp' }, func: us.writeFirstEdit });
}
} else { us.writeRegistration(data.registration); }
us.loading_blocked.replaceWith($('<a>', { style: ((blocked) ? us.styleBlocked : us.styleNotBlocked), id: 'us_block_status_span', href: '/w/index.php?title=Special:Log/block&page=User:' + us.user, title: msg.blocklog, text: ((blocked) ? ' ' : msg.not) + msg.block }));
if (blocked) {
data.blockreason = aw.blockreason || ' ';
us.ul.append([
$('<li>', { id: 'us_block_time' }).append([$('<b>').text('• ' + msg.blockEnd), $.createSpinner('us_block_time_loading')]),
$('<li>', { id: 'us_block_reason' }).append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment, (aw.commenthidden))]),
$('<li>', { id: 'us_blocker' }).append([$('<b>').text('• ' + msg.blocker), $.createSpinner('us_blocker_loading')])
]);
us.actions.push({ params: { list: 'logevents', letitle: 'User:' + us.user, letype: 'block', lelimit: 1 }, func: us.writeBlockDetail });
}
if (data.glGrp && !data.locked) {
uq.query.globaluserinfo = { groups: (data.glGrp.length) ? data.glGrp : null };
us.writeGlobalGroup(uq);
} else {
us.actions.push({ params: '&meta=globaluserinfo&guiprop=groups&guiuser=' + us.user, func: us.writeGlobalGroup });
}
if (gender) {
data.gender = gender;
var genderSn = (gender === 'male') ? ' \u2642' : (gender === 'female' ? ' \u2640' : '');
$('#firstHeading').append($('<span>', { id: 'ps-gender-' + gender, style: 'font-size:80%', text: genderSn }));
}
if (us.actions.length) us.doRequest();
},
createBox: function () {
var statusBox = $('<div>', { style: 'border-bottom:1px solid #aaa;text-shadow:1px 1px 1px #eff;', id: 'us_box' });
if (mw.config.get('skin') === 'vector') statusBox.css('font-size', '0.8em');
var spanFrag = $.createSpinner();
us.loading_editcount = spanFrag.clone();
us.loading_registration = spanFrag.clone();
us.loading_groups = spanFrag.clone();
us.loading_blocked = spanFrag.clone();
us.us_global_group_loading = spanFrag.clone();
us.us_last_edit_loading = $.createSpinner('contribs');
us.loading_last_edit = $('<li>');
us.us_global_group = $('<li>', { style: 'display: none' });
var thx = $('<a>', { title: msg.count, text: msg.thxRd, style: us.styleLoading, click: us.getThanks });
var $userrights = $('#t-userrights a');
var $contributions = $('#t-contributions a');
if (!$userrights.length) $userrights = $('<a>', { href: '/wiki/Special:UserRights/' + us.user });
if (!$contributions.length) $contributions = $('<a>', { href: '/wiki/Special:Contributions/' + us.user });
var ul = $('<ul>', { style: 'list-style: none' }).append($('<li>', { id: 'us_editcount' }).append([$('<b>').append($contributions.text(msg.contrib)), us.loading_editcount]));
us.us_log_count = $('<span>');
if (us.viewPatrolNumber) {
us.us_patrolcount_loading = $('<a>', { style: us.styleLoading, click: us.getPatrolCount, text: msg.count });
us.us_log_count = $('<span>').append([$('<b>').text(msg.reviews), us.us_patrolcount_loading, ' • ']);
}
ul.append(us.us_log_count)
.append($('<li>', { id: 'us_reg_date' }).append([$('<b>').text(msg.regdate), us.loading_registration, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://meta.wikimedia.org/wiki/Special:UserPages/' + us.user, text: '• Global user pages' })]))
.append($('<li>').append([$('<b>').append($userrights.text(msg.loGrp)), us.loading_groups]))
.append($('<li>', { id: 'us_former_local_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerLoGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.us_global_group.append([$('<b>').append($('<a>', { href: 'https://meta.wikimedia.org/wiki/Special:Log?type=gblrights&user=&page=User:' + us.user, text: msg.glGrp })), us.us_global_group_loading]))
.append($('<li>', { id: 'us_former_global_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerGlGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.loading_last_edit.append([$('<b>').text(msg.laedit), us.us_last_edit_loading, ' ', $('<a>', { target: '_blank', style: us.styleMissingData, href: '/w/index.php?title=Special:Log/' + us.user, text: '• LLA' }), $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://meta.wikimedia.org/wiki/Special:GlobalContributions/' + us.user, text: '• Global contributions' })]))
.append($('<li>', { id: 'us_last_action_li', style: 'display:none' }).append([$('<b>').text(msg.laact), $('<span>', { id: 'us_last_action_val' })]))
.append($('<li>', { id: 'us_last_admin_li', style: 'display:none' }).append([$('<b>').text(msg.laadmin), $('<span>', { id: 'us_last_admin_val' })]))
.append($('<li>', { id: 'us_last_cu_li', style: 'display:none' }).append([$('<b>').text(msg.lacu), $('<span>', { id: 'us_last_cu_val' })]))
.append($('<li>', { id: 'us_last_os_li', style: 'display:none' }).append([$('<b>').text(msg.laos), $('<span>', { id: 'us_last_os_val' })]));
statusBox.append(ul);
us.ul = $('<ul>', { style: 'list-style:none' }).append($('<li>', { id: 'us_block_status' }).append([$('<b>').text(msg.blocks), us.loading_blocked, ' • ', $('<b>').text(msg.thxGvng), thx, ' / ', thx.clone(1).text(msg.thxGn), $('<a>', { style: 'float:right;' + us.styleMissingData, title: 'Purge data', text: 'purge', click: us.removeDataStore })]));
statusBox.append(us.ul);
$('#firstHeading').after(statusBox);
us.statusBox = statusBox;
},
removeDataStore: function (e) {
var key = project + us.user, db = window.indexedDB;
e.preventDefault();
if (!db || !us.dbVersion) return mw.cookie.set(us.name + us.user, null);
var request = db.open(us.name, us.dbVersion + 1);
request.onupgradeneeded = function () { db = this.result; if (db.objectStoreNames.contains(key)) db.deleteObjectStore(key); };
request.onsuccess = function () { db = this.result; us.dbVersion = db.version; db.close(); location.reload(); };
},
setCookie: function () {
var domain = (mw.config.get('wgNoticeProject') === 'wikipedia') ? 'wikipedia.org' : '';
var name = us.name + us.user;
var _saveCookie = function (k, d) { mw.cookie.set(name, JSON.stringify(d), { expires: 604800, domain: domain }); };
if (!us.actions[0] && JSON && data.editcount) {
if (us.cookie.length) window.clearTimeout(us.cookie.shift());
var saveData = window.indexedDB ? function (n, JSd, v) {
var key = project + us.user, db = window.indexedDB, request = db.open(us.name, v);
request.onupgradeneeded = function () {
db = this.result;
if (!db.objectStoreNames.contains(key)) {
var store = db.createObjectStore(key, { keyPath: 'name' });
store.createIndex('data', 'data', { unique: false });
}
};
request.onsuccess = function () {
db = this.result; us.dbVersion = db.version;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readwrite').objectStore(key);
store.put({ name: key, data: JSd });
} else { saveData(n, JSd, us.dbVersion + 1); }
};
request.onerror = function() { _saveCookie(n, JSd); };
} : _saveCookie;
us.cookie.push(setTimeout(function () {
data.timestamp = new Date().valueOf();
saveData('', data);
}, 500));
}
},
doRequest: function () {
var action = us.actions.shift();
if (action) us.ajaxRequest(action.params, action.func);
if (us.actions.length) window.setTimeout(function () { us.doRequest(); }, 100);
},
getDateDiff: function (now, date) {
var dStr = [];
if (now > date) {
var years = now.getFullYear() - date.getFullYear();
var months = now.getMonth() - date.getMonth();
var days = now.getDate() - date.getDate();
// Korekce dnů a měsíců
if (days < 0) {
months--;
// Získáme počet dní v předchozím měsíci
var prevMonth = new Date(now.getFullYear(), now.getMonth(), 0).getDate();
days += prevMonth;
}
// Korekce měsíců a roků
if (months < 0) {
years--;
months += 12;
}
var units = [years, months, days];
var labelSingle = [msg.date[0], msg.date[1], msg.date[2]];
var labelPlural = [msg.dates[0], msg.dates[1], msg.dates[2]];
for (var i = 0; i < 3; ++i) {
if (units[i] > 0) {
dStr.push(units[i] + ' ' + ((units[i] > 1) ? labelPlural[i] : labelSingle[i]));
}
}
}
var mainDiff = (dStr.length) ? dStr.join(', ').replace(/(.*),([^,]*)/, msg.and).replace(/(.*)/, msg.ago) : msg.nosec + msg.date[2];
return mainDiff;
},
getDateFromTimestamp: function (t) { if (!t) return false; t = new Date(t); return isNaN(t.valueOf()) ? false : t; },
writeRegistration: function (aw) {
if (aw) {
if (aw instanceof Object) {
if (aw.query && aw.query.logevents.length) aw = aw.query.logevents[0].timestamp;
}
if (typeof aw === 'string') {
us.loading_registration.replaceWith(us.formatDate(aw) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(aw)) + ')' }));
data.registration = aw;
return us.setCookie();
}
}
us.loading_registration.replaceWith($('<span>', { style: us.styleMissingData + us.styleLoading, text: msg.nodb }));
},
writeFirstEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs.length) return;
var date = data.firstedit = aw.query.usercontribs[0].timestamp;
$('#us_reg_date').append($('<li>').append([$('<b>', { text: '• ' + msg.fiedit }), us.formatDate(date) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(date)) + ')' })]));
},
writeLastEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs || !aw.query.usercontribs.length) return us.us_last_edit_loading.replaceWith($('<span>', { style: us.styleMissingData, text: msg.noedit }));
var date = data.lastedit = aw.query.usercontribs[0].timestamp;
us.us_last_edit_loading.replaceWith(us.getDateDiff(us.now, us.getDateFromTimestamp(date)), ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.formatDate(date) + ')' }));
data.timediff = mw.now() - us.now;
$('#us_editcount').append($('<span>', { style: 'float:right;' + us.styleMissingData, text: msg.curtimeDiff }).append($('<b>', { text: Math.round(data.timediff / 1000) })));
us.setCookie();
},
writeGlobalGroup: function (aw) {
aw = aw.query.globaluserinfo;
var groups = data.glGrp = aw.groups;
// Pokud lokální registrace chybí, použijeme tu z globálního infa
if (!data.registration && aw.registration) {
us.writeRegistration(aw.registration);
}
if (groups && groups.length) {
us.us_global_group_loading.replaceWith(us.getLocalNames(groups, 'GlobalGroupPermissions'));
us.us_global_group.css('display', 'block');
// Fetch former global rights
us.fetchFormerRights(true);
}
if (aw.locked !== undefined) {
data.locked = 1;
mw.loader.using('mediawiki.ForeignApi').done(function () {
var fApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php');
fApi.get({
action: 'query',
list: 'logevents',
leprop: 'user|timestamp|comment',
letype: 'globalauth',
letitle: 'User:' + us.user,
lelimit: '1'
}).done(us.writeGlobalBlock);
});
}
us.setCookie();
},
writeGlobalBlock: function (aw) {
if (!aw.query || !aw.query.logevents.length) return;
aw = aw.query.logevents[0];
$('#us_block_status_span').replaceWith($('<a>', { style: us.styleBlocked, id: 'us_block_status_span', href: '//meta.wikimedia.org/w/index.php?title=Special:Log/&type=globalauth&user=&page=User%3A' + us.user + '%40global', text: 'locked (global)' }).append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) })));
$('#us_block_status').parent().append([$('<li>').append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment)]), $('<li>').append([$('<b>').text('• ' + msg.blocker), $('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user })])]);
},
writeBlockDetail: function (aw) {
var duration = 'infinite', expiry = '';
if (aw.query && aw.query.logevents.length) {
aw = aw.query.logevents[0];
if (aw && aw.params) {
duration = aw.params.duration;
expiry = /in(de)?finite/.test(duration) ? msg.never : us.formatDate(aw.params.expiry);
}
$('#us_block_status_span').text(msg.block + ' (' + duration + ')').append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) }));
if (expiry) $('#mw-spinner-us_block_time_loading').replaceWith(expiry); else $('#us_block_time').remove();
$('#mw-spinner-us_block_reason_loading').replaceWith(us.parseComment(aw.comment, aw.commenthidden));
if (aw.user) $('#mw-spinner-us_blocker_loading').replaceWith($('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user }));
else $('#mw-spinner-us_blocker_loading').replaceWith(msg.nodb);
}
},
writePatrolCount: function (aw) {
if (aw instanceof Object) {
if (!aw.query || !aw.query.logevents) return;
us.patrols += aw.query.logevents.length;
if (aw.continue) return us.getPatrolCount(aw.continue);
$.removeSpinner('pat');
aw = us.patrols;
}
us.us_patrolcount_loading.replaceWith($('<a>', { href: '/w/index.php?title=Special:Log&type=' + (us.review ? 'review&subtype=accept' : 'patrol&subtype=patrol') + '&user=' + us.user, text: aw }));
data.reviews = aw;
us.setCookie();
},
getPatrolCount: function (e) {
var params = '&list=logevents&leprop=&ledir=newer&lelimit=max&leuser=' + us.user + '&leaction=' + (us.review ? 'review/approve' : 'patrol/patrol');
if (e instanceof Object) {
if (e.target) { $(e.target).injectSpinner('pat'); us.patrols = 0; }
else if (e.lecontinue) params += '&lecontinue=' + e.lecontinue;
}
us.actions.push({ params: params, func: us.writePatrolCount });
us.doRequest();
},
parseComment: function (text, hidden) {
var comment = $('<span>', { 'class': 'comment' });
if (typeof text === 'undefined') return comment.append(hidden ? msg.noReason : 'undefined').css('color', hidden ? '#999' : 'red');
if (!text) return comment.append(msg.blockCmt).css('color', '#999');
var intLink = /(.*?)\[\[((.*?)\|)?(.*?)\]\](.*)/, suche = text, erg;
while ((erg = intLink.exec(suche)) !== null) {
var link = erg[3] || erg[4];
comment.append([erg[1], $('<a>', { href: mw.util.getUrl(link), title: link }).text(erg[4])]);
suche = erg[5];
}
return comment.append(suche);
},
formatDate: function (datum) {
if (!(datum instanceof Date)) datum = new Date(datum);
try {
return datum.toLocaleDateString(us.lang, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
} catch (e) { return datum.toLocaleString(); }
},
getStoredData: function () {
var key = project + us.user, db, request;
try {
request = indexedDB.open(us.name);
request.onsuccess = function () {
db = this.result;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readonly').objectStore(key);
store.transaction.oncomplete = function () { us.runDataStore(); db.close(); };
store.get(key).onsuccess = function () { if (this.result) data = this.result.data; else us.getCookie(); };
} else { us.getCookie(); }
};
request.onerror = function () { us.getCookie(); };
} catch (e) { us.getCookie(); }
},
getCookie: function () {
data = mw.cookie.get(us.name + us.user);
if (data) try { data = JSON.parse(data); } catch(e) { data = null; }
us.runDataStore();
},
initI18N: function (i18n) {
var chain = mw.language.getFallbackLanguageChain();
for (var i = chain.length - 1; i >= 0; i--) if (chain[i] in i18n) msg = i18n[chain[i]];
},
init: function () {
if (this.statusBox || (this.statusBox = $('#us_box'))[0]) this.statusBox.remove();
this.usprop = 'blockinfo|editcount|gender|groups|implicitgroups|registration';
this.self = mw.config.get('wgUserName') === this.user;
this.initI18N(i18n);
this.createBox();
if (!this.first) {
if (mw.cookie && JSON) { if (window.indexedDB) this.getStoredData(); else this.getCookie(); }
else this.run();
} else this.run();
},
runDataStore: function () {
if (data) {
if (typeof data === 'string') try { data = JSON.parse(data); } catch(e) { data = {}; }
var q = { query: { users: [data] } };
if (data.timestamp) this.now = mw.now() - (data.timediff || 0);
if (data.timestamp && Math.abs((mw.now() - data.timestamp - (data.timediff || 0)) / 1000) < 90) return this.writeCommonInfo(q);
if (data.editcount) this.usprop = 'editcount';
}
data = data || {};
this.run();
},
run: function () {
if (this.self) {
return this.writeCommonInfo({ query: { users: [{ editcount: mw.config.get('wgUserEditCount'), registration: mw.config.get('wgUserRegistration'), groups: mw.config.get('wgUserGroups'), blockreason: '' }] } });
}
us.ajaxRequest({ curtimestamp: 1, list: 'users', ususers: us.user, usprop: us.usprop }, us.writeCommonInfo);
}
};
if ([2, 3].indexOf(mw.config.get('wgNamespaceNumber')) !== -1 && us.user.indexOf('/') === -1 && (!mw.config.get('wgArticleId') || mw.config.get('wgAction') === 'view')) {
$.when(mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.language', 'mediawiki.cookie', 'jquery.spinner', 'mediawiki.ForeignApi']), $.ready).then(function () {
us.api = mw.util.wikiScript('api') + '?action=query&format=json';
us.ajaxRequest('&meta=allmessages&amenableparser=1&amincludelocal=1&refix=group-', us.getGroupNames);
if (!mw.libs.viewerInfo) mw.loader.load('//meta.wikimedia.org/w/index.php?title=User:Perhelion/viewerInfo.js&action=raw&ctype=text/javascript');
});
}
}(jQuery, mediaWiki));
mib65ln0pctes91p6vtsp60kvb0y9ji
739971
739970
2026-05-01T11:42:48Z
MrJaroslavik
44012
+
739971
javascript
text/javascript
/**
* @Description: Modified Userstatus.js with Former Groups (Local & Global)
* Original by Steef389, Perhelion.
**/
(function ($, mw) {
'use strict';
var project = window.project || mw.config.get('wgDBname');
var msg, i18n = {
en: {
noReason: 'reason removed',
blockCmt: 'no block comment',
never: 'never',
not: ' not ',
block: 'blocked',
and: '$1 and$2',
count: 'Count',
noedit: 'None (only deleted contributions?)',
nodb: '‹not in database›',
noGrp: 'no extended group',
dates: ['years', 'months', 'days', 'hours', 'minutes', 'seconds'],
date: ['year', 'month', 'day', 'hour', 'minute', 'second'],
nosec: 'less than one ',
curtimeDiff: ' Locale time difference to server time in s: ',
thxGn: 'given',
thxRd: 'received',
nova: 'new',
ago: '$1 ago',
blocklog: 'Block log',
contrib: 'Edits: ',
usub: 'Subpages⬇',
thxGvng: 'Thanksgivings: ',
reviews: 'Active reviews: ',
regdate: 'Registration: ',
laedit: 'Last edited: ',
lala: 'Last log activity',
fiedit: 'First edit: ',
blocks: 'Block-status: ',
loGrp: 'Local user-groups: ',
glGrp: 'Global user-groups: ',
formerLoGrp: 'Former local user-groups: ',
formerGlGrp: 'Former global user-groups: ',
blockEnd: 'Block-end: ',
blocker: 'Blocker: ',
blockReason: 'Block-reason: ',
laact: 'Last action: ',
laadmin: 'Last admin action: ',
lacu: 'Last CU action: ',
laos: 'Last OS action: ',
today: 'today',
yesterday: 'yesterday'
},
cs: {
noReason: 'důvod odstraněn',
blockCmt: 'bez komentáře',
never: 'nikdy',
not: ' ne ',
block: 'zablokován',
and: '$1 a$2',
count: 'Počet',
noedit: 'Žádné (jen smazané?)',
nodb: '‹není v databázi›',
noGrp: 'žádná rozšířená skupina',
dates: ['let', 'měsíců', 'dní', 'hodin', 'minut', 'sekund'],
date: ['rok', 'měsíc', 'den', 'hodina', 'minuta', 'sekunda'],
nosec: 'méně než ',
curtimeDiff: ' Rozdíl lokálního času k serveru v s: ',
thxGn: 'poděkoval',
thxRd: 'obdržel poděkování',
nova: 'nové',
ago: 'před $1',
days: 'dní',
blocklog: 'Kniha zablokování',
contrib: 'Editace: ',
usub: 'Podstránky⬇',
thxGvng: 'Poděkování: ',
reviews: 'Aktivní prověření: ',
regdate: 'Registrace: ',
laedit: 'Poslední editace: ',
lala: 'Poslední aktivita v protokolech',
fiedit: 'První editace: ',
blocks: 'Stav bloku: ',
loGrp: 'Lokální skupiny: ',
glGrp: 'Globální skupiny: ',
formerLoGrp: 'Bývalé lokální skupiny: ',
formerGlGrp: 'Bývalé globální skupiny: ',
blockEnd: 'Konec bloku: ',
blocker: 'Zablokoval: ',
blockReason: 'Důvod bloku: ',
laact: 'Poslední akce: ',
laadmin: 'Poslední admin akce: ',
lacu: 'Poslední CU akce: ',
laos: 'Poslední OS akce: ',
today: 'dnes',
yesterday: 'včera'
}
};
var data;
var us = mw.libs.userstatus = {
name: 'Userstatus',
version: 1.80, // Bumping version for former groups update
lastEditSeconds: false,
styleMissingData: 'color:#999;font-size:90%;',
styleLoading: 'font-style:italic;',
styleBlocked: 'color:#c00',
styleNotBlocked: 'color:#080',
styleFormer: 'margin-left:15px; color:#72777d; font-style:italic; font-size:0.9em;',
viewPatrolNumber: false,
lang: mw.config.get('wgUserLanguage'),
user: mw.config.get('wgTitle'),
cookie: [],
thanks: 0,
patrols: 0,
actions: [],
dbVersion: 0,
implicitGroups: ['*', 'user', 'autoconfirmed'],
getLocalNames: function (groups, specialpage) {
if (groups) {
var arr = [];
specialpage = specialpage || 'ListGroupRights';
for (var i = 0; i < groups.length; ++i) {
var g = groups[i];
var n = us.groupNames[g];
if (n) arr.push('<a href="/wiki/Special:' + specialpage + '#' + g + '">' + n + '</a>');
else arr.push(g);
}
groups = arr.join(', ').replace(/(.*),([^,]*)$/, msg.and);
}
return groups;
},
writeGroups: function (groups) {
groups = us.getLocalNames(groups);
if (!groups) {
groups = $('<span>', {
style: us.styleMissingData + us.styleLoading,
text: msg.noGrp
});
}
us.loading_groups.replaceWith(groups);
// After local groups are written, fetch former local groups
us.fetchFormerRights(false);
},
getGroupNames: function (aw) {
if (!aw || !aw.query) return;
aw = aw.query.allmessages;
var al = aw.length;
var groups = {};
while (al--) {
var an = aw[al];
var name = an.name;
if (/^group-/.test(name)) {
name = RegExp.rightContext || name.substring(6);
groups[name] = an['*'];
}
}
us.groupNames = groups;
us.init();
},
ajaxRequest: function (params, onSuccess, trial) {
var url = us.api;
if (!(params instanceof Object)) {
url += params + '&maxage=2419200&smaxage=2419200';
params = {};
} else {
params.maxlag = 3;
params.maxage = 2419200;
params.smaxage = 2419200;
if (trial) params.maxlag *= trial;
}
var api = $.getJSON(url, params, onSuccess)
.fail(function (jqXHR, status) {
var note = 'Timeout fail, maybe try again!';
if (trial) {
if (!status.textStatus || (status.textStatus !== 'timeout' && jqXHR !== 'maxlag')) note = $('<span>Fehler bitte <a href="' + mw.util.getUrl('User_talk:Perhelion') + '">Perhelion</a> melden.</span>');
mw.notify(note, { title: us.name + ':', type: 'error' });
} else if (status.textStatus === 'timeout' || jqXHR === 'maxlag') {
api.abort();
return us.ajaxRequest(params, onSuccess, 2);
}
});
},
// --- NEW FORMER RIGHTS LOGIC ---
fetchFormerRights: function(isGlobal) {
var apiTarget = isGlobal ? 'https://meta.wikimedia.org/w/api.php' : mw.util.wikiScript('api');
var logType = isGlobal ? 'gblrights' : 'rights';
var userTarget = 'User:' + us.user;
var containerId = isGlobal ? 'us_former_global_groups' : 'us_former_local_groups';
// Globální override pro smazané skupiny (skupina: datum smazání)
var globalOverrides = {
'oathauth-tester': '2026-01-12T10:58:00Z'
};
var params = {
action: 'query',
list: 'logevents',
letype: logType,
letitle: userTarget,
lelimit: 500,
format: 'json',
origin: isGlobal ? '*' : undefined
};
$.getJSON(apiTarget, params, function(res) {
if(!res || !res.query || !res.query.logevents) return;
var logs = res.query.logevents;
var former = {};
var currentGroups = isGlobal ? (data.glGrp || []) : (data.groups || []);
logs.forEach(function(log) {
var p = log.params;
if (!p) return;
var date = log.timestamp;
var oldRaw = p.oldgroups || p.oldGroups || p.remove || p["0"] || "";
var newRaw = p.newgroups || p.newGroups || p.add || p["1"] || "";
var parse = function(val) {
if (Array.isArray(val)) return val;
if (typeof val === 'string') {
if (val === "(none)" || val === "") return [];
return val.split(/[,|]\s*/);
}
return [];
};
var oldG = parse(oldRaw);
var newG = parse(newRaw);
// 1. Nejdřív kontrolujeme udělení (v logu jdeme od nejnovějších k nejstarším)
newG.forEach(function(g) {
g = g.trim();
if (us.implicitGroups.indexOf(g) !== -1 || !g) return;
// Speciální případ: Skupina byla udělena, uživatel ji nemá a víme, že globálně zanikla
if (currentGroups.indexOf(g) === -1 && globalOverrides[g] && !former[g]) {
former[g] = { to: globalOverrides[g], from: date };
}
// Standardní případ: Už známe konec z oldG a teď jsme našli, kdy to začalo
else if (former[g] && !former[g].from) {
former[g].from = date;
}
});
// 2. Poté kontrolujeme odebrání (standardní cesta)
oldG.forEach(function(g) {
g = g.trim();
if (us.implicitGroups.indexOf(g) !== -1 || !g) return;
// Pokud skupinu nemá a ještě jsme o ní v tomto průchodu neslyšeli
if (currentGroups.indexOf(g) === -1 && !former[g]) {
former[g] = { to: date, from: null };
}
});
});
var entries = [];
Object.keys(former).sort().forEach(function(g) {
var item = former[g];
// Aplikace globálního overridu, pokud chybí datum odebrání a skupina je v seznamu smazaných
if (!item.to && globalOverrides[g]) {
item.to = globalOverrides[g];
}
var dateTo = new Date(item.to);
var dateFrom = item.from ? new Date(item.from) : null;
if(dateFrom && item.to) {
var diffText = us.getDateDiff(dateTo, dateFrom).replace(msg.ago.replace('$1', ''), '').trim();
entries.push(g + ' (' + dateFrom.toISOString().split('T')[0] + ' – ' + dateTo.toISOString().split('T')[0] + ', ' + diffText + ')');
} else if (item.to) {
entries.push(g + ' (do ' + dateTo.toISOString().split('T')[0] + ')');
}
});
if(entries.length > 0) {
$('#' + containerId).show().find('.us_former_content').text(entries.join(', '));
}
});
},
getThanks: function (e) {
var params = '&list=logevents&continue=-%7C%7C&leprop=&letype=thanks&ledir=newer&lelimit=max';
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
us.e.attr('title', us.e.text());
us.e.text('');
us.e.injectSpinner('thx');
us.thanks = 0;
} else if (e.lecontinue) {
params += '&lecontinue=' + e.lecontinue;
}
if (us.e.attr('title') === msg.thxGn) params += '&leuser=' + us.user;
else params += '&letitle=User:' + us.user;
}
us.actions.push({ params: params, func: us.writeThanks });
us.doRequest();
},
writeThanks: function (uq) {
if (!uq.query || !uq.query.logevents) return mw.log.warn(uq);
var e = us.e;
var user = 'page=';
var aw = uq.query.logevents;
us.thanks += aw.length;
if (uq.continue) return us.getThanks(uq.continue);
if (e[0].title === msg.thxGn) user += '&user=';
user += us.user;
$.removeSpinner('thx');
e.text(us.thanks);
e.off('click');
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special%3ALog&type=thanks&' + user + '&year=&month=-1&tagfilter=&hide_thanks_log=0';
delete us.e;
},
getUploads: function (e) {
var params = '&list=logevents&letype=upload&ledir=newer&lelimit=max&leuser=' + us.user;
function _start(u) {
var text = u.text();
u.prop('title', text);
u.text('');
u.injectSpinner('upload' + text);
u.data('upl', 0);
u.data('del', 0);
}
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
if (us.e.text() !== msg.nova) {
us.e2 = us.e.nextAll('a').eq(0);
if (us.e2.text() === msg.nova) _start(us.e2); else delete us.e2;
}
_start(us.e);
} else if (e.lecontinue) { params += '&lecontinue=' + e.lecontinue; }
params += '&leprop=ids';
if (us.e.prop('title') === msg.nova) params += '&leaction=upload/upload';
else if (us.e2) params += '|type';
}
us.actions.push({ params: params, func: us.writeUploads });
us.doRequest();
},
writeUploads: function (uq) {
if (!uq.query || !uq.query.logevents || !us.e) return mw.log(uq, us.e);
function _insert(e) {
$.removeSpinner('upload' + e.prop('title'));
e.text('');
e.off('click');
e.append(e.data('del'), $('<span>', { style: us.styleMissingData + us.styleLoading, text: ' (+' + (e.data('upl') - e.data('del')) + ' del.)' }));
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special:Log&type=upload&user=' + us.user + '&subtype=' + ((e[0].title === msg.nova) ? 'upload' : '');
}
var e = us.e;
var e2 = us.e2;
var aw = uq.query.logevents;
var alen = aw.length, i = 0;
var del = e.data('del');
if (e2) {
var upl = e2.data('upl');
var del2 = e2.data('del');
for (i; i < alen; ++i) {
var a = aw[i];
var c = a.pageid ? 1 : 0;
if (a.action === 'upload') { upl++; del2 += c; }
del += c;
}
e2.data('upl', upl);
e2.data('del', del2);
} else {
for (i; i < alen; ++i) if (aw[i].pageid) del++;
}
e.data('upl', e.data('upl') + alen);
e.data('del', del);
if (uq.continue) return us.getUploads(uq.continue);
_insert(e);
if (e2) { _insert(e2); delete us.e2; }
delete us.e;
},
writeCommonInfo: function (uq) {
var aw = uq.query;
if (!aw || !aw.users[0] || Object.prototype.hasOwnProperty.call(aw, 'missing') || Object.prototype.hasOwnProperty.call(aw.users[0], 'invalid')) return us.statusBox.remove();
aw = aw.users[0];
var edits = aw.editcount,
groups = data.groups || aw.groups,
blocked = data.blockreason || aw.blockexpiry,
gender = data.gender || aw.gender,
uploads = $('<a>', { title: msg.count, text: msg.nova, style: us.styleLoading, click: us.getUploads });
if (aw.registration) data.registration = aw.registration;
us.now = us.getDateFromTimestamp(uq.curtimestamp || us.now || mw.now());
us.user = us.user.replace(/ /g, '_');
us.first = 1;
// POMOCNÁ FUNKCE PRO VÝPIS AKCÍ Z LOGU (S ODKAZEM)
var renderActionRow = function(valSelector, liSelector, timestamp, url) {
var d = new Date(timestamp);
var diff = us.getDateDiff(us.now, d);
var formatted = us.formatDate(d);
var diffNode = url ? $('<a>', { href: url, text: diff }) : diff;
$(valSelector).empty().append(diffNode, ' ', $('<span>', { style: us.styleMissingData, text: '(' + formatted + ')' }));
$(liSelector).show();
};
// A. OBECNÉ A ADMIN AKCE
var adminTypes = ['block', 'delete', 'protect', 'rights', 'merge', 'abusefilter', 'renameuser', 'gblblock'];
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=50',
func: function(res) {
if(!res.query || !res.query.logevents) return;
var logs = res.query.logevents;
if(logs[0]) {
renderActionRow('#us_last_action_val', '#us_last_action_li', logs[0].timestamp, mw.util.getUrl('Special:Log/' + us.user));
}
var adm = logs.find(function(l) { return adminTypes.indexOf(l.type) !== -1; });
if(adm) {
renderActionRow('#us_last_admin_val', '#us_last_admin_li', adm.timestamp, mw.util.getUrl('Special:Log', { user: us.user }));
}
}
});
// B. CU AKCE
if (groups && $.inArray('checkuser', groups) !== -1) {
us.actions.push({
params: '&list=checkuserlog&culuser=' + us.user + '&cullimit=1',
func: function(res) {
if(res.query && res.query.checkuserlog && res.query.checkuserlog.entries && res.query.checkuserlog.entries[0]) {
var cuUrl = mw.util.getUrl('Special:CheckUserLog', { cuSearchType: 'initiator', cuSearch: us.user });
renderActionRow('#us_last_cu_val', '#us_last_cu_li', res.query.checkuserlog.entries[0].timestamp, cuUrl);
}
}
});
}
// C. OS AKCE (Ošetřen oversight i suppress)
if (groups && ($.inArray('oversight', groups) !== -1 || $.inArray('suppress', groups) !== -1)) {
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=1&letype=suppress',
func: function(res) {
if(res.query && res.query.logevents[0]) {
var osUrl = mw.util.getUrl('Special:Log', { type: 'suppress', user: us.user });
renderActionRow('#us_last_os_val', '#us_last_os_li', res.query.logevents[0].timestamp, osUrl);
}
}
});
}
if (edits) {
if (data.editcount && data.lastedit && data.editcount === edits) {
uq.query.usercontribs = [{ timestamp: data.lastedit }];
us.writeLastEdit(uq);
} else {
data.editcount = edits;
us.loading_last_edit.css('display', 'block');
us.actions.push({
params: '&list=usercontribs&ucuser=' + us.user + '&uclimit=1&uccontinue=' + (new Date(us.now).toISOString().replace(/[^\d]/g, '').slice(0, 14) + '|2'),
func: us.writeLastEdit
});
}
$('#t-contributions').remove();
} else { us.writeLastEdit({}); }
if (groups) {
edits = $('<a>', { title: 'Supercount', href: '//tools.wmflabs.org/supercount/index.php?user=' + us.user + '&project=' + location.hostname, text: edits });
if ($.inArray('bot', groups) === -1) {
us.review = $.inArray('editor', groups) !== -1;
if (us.review) {
if (typeof data.reviews === 'number') us.writePatrolCount(data.reviews);
} else if (us.viewPatrolNumber) { us.us_patrolcount_loading.parent().remove(); }
edits = [
edits, ' • ',
$('<a>', { href: '//xtools.wmflabs.org/ec' + mw.config.get('wgServer').substr(1) + '/' + us.user, title: 'XTools', text: 'XTools', style: us.styleLoading, target: '_blank' }), ' • ',
$('<a>', { href: 'https://meta.wikimedia.org/wiki/Special:CentralAuth/' + us.user, title: 'CentralAuth', text: 'CentralAuth', style: us.styleLoading, target: '_blank' }), ' • ',
(project !== 'commonswiki' ? $('<a>', { href: '/wiki/Special:ListFiles/' + us.user, text: 'Upload', title: 'Special:Listfiles', style: us.styleLoading }) : 'Upload'),
(us.lang === 'de' ? '-' : ' ') + msg.count + ': ',
uploads.clone(1).text('total'), ' / ', uploads
];
if (!$('#t-subpages').length) {
edits.push($('<a>', { href: mw.util.getUrl('Special:Prefixindex/User:' + us.user + '/'), text: ' • ' + msg.usub, style: us.styleMissingData + us.styleLoading }));
}
} else if (us.us_patrolcount_loading) { us.us_patrolcount_loading.parent().remove(); }
us.loading_editcount.replaceWith(edits);
if (!aw.implicitgroups && !data.groups) {
aw.implicitgroups = ['*', 'user', 'autoconfirmed'];
if (groups.length < 4) aw.implicitgroups.pop();
}
data.groups = $.grep(groups, function (n) { return $.inArray(n, aw.implicitgroups) === -1; });
us.writeGroups(data.groups);
$('#t-userrights').remove();
}
if (!data.registration) {
us.actions.push({ params: '&list=logevents&leprop=timestamp|type&letype=newusers&lelimit=1&ledir=newer&leend=2005-12-30T00:00:00Z&leuser=' + us.user, func: us.writeRegistration });
if (data.firstedit) {
uq.query.usercontribs = [{ timestamp: data.firstedit }];
us.writeFirstEdit(uq);
} else if (edits) {
us.actions.push({ params: { list: 'usercontribs', ucuser: us.user, uclimit: 1, ucend: '2005-09-08T00:00:00Z', ucdir: 'newer', ucprop: 'timestamp' }, func: us.writeFirstEdit });
}
} else { us.writeRegistration(data.registration); }
us.loading_blocked.replaceWith($('<a>', { style: ((blocked) ? us.styleBlocked : us.styleNotBlocked), id: 'us_block_status_span', href: '/w/index.php?title=Special:Log/block&page=User:' + us.user, title: msg.blocklog, text: ((blocked) ? ' ' : msg.not) + msg.block }));
if (blocked) {
data.blockreason = aw.blockreason || ' ';
us.ul.append([
$('<li>', { id: 'us_block_time' }).append([$('<b>').text('• ' + msg.blockEnd), $.createSpinner('us_block_time_loading')]),
$('<li>', { id: 'us_block_reason' }).append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment, (aw.commenthidden))]),
$('<li>', { id: 'us_blocker' }).append([$('<b>').text('• ' + msg.blocker), $.createSpinner('us_blocker_loading')])
]);
us.actions.push({ params: { list: 'logevents', letitle: 'User:' + us.user, letype: 'block', lelimit: 1 }, func: us.writeBlockDetail });
}
if (data.glGrp && !data.locked) {
uq.query.globaluserinfo = { groups: (data.glGrp.length) ? data.glGrp : null };
us.writeGlobalGroup(uq);
} else {
us.actions.push({ params: '&meta=globaluserinfo&guiprop=groups&guiuser=' + us.user, func: us.writeGlobalGroup });
}
if (gender) {
data.gender = gender;
var genderSn = (gender === 'male') ? ' \u2642' : (gender === 'female' ? ' \u2640' : '');
$('#firstHeading').append($('<span>', { id: 'ps-gender-' + gender, style: 'font-size:80%', text: genderSn }));
}
if (us.actions.length) us.doRequest();
},
createBox: function () {
var statusBox = $('<div>', { style: 'border-bottom:1px solid #aaa;text-shadow:1px 1px 1px #eff;', id: 'us_box' });
if (mw.config.get('skin') === 'vector') statusBox.css('font-size', '0.8em');
var spanFrag = $.createSpinner();
us.loading_editcount = spanFrag.clone();
us.loading_registration = spanFrag.clone();
us.loading_groups = spanFrag.clone();
us.loading_blocked = spanFrag.clone();
us.us_global_group_loading = spanFrag.clone();
us.us_last_edit_loading = $.createSpinner('contribs');
us.loading_last_edit = $('<li>');
us.us_global_group = $('<li>', { style: 'display: none' });
var thx = $('<a>', { title: msg.count, text: msg.thxRd, style: us.styleLoading, click: us.getThanks });
var $userrights = $('#t-userrights a');
var $contributions = $('#t-contributions a');
if (!$userrights.length) $userrights = $('<a>', { href: '/wiki/Special:UserRights/' + us.user });
if (!$contributions.length) $contributions = $('<a>', { href: '/wiki/Special:Contributions/' + us.user });
var ul = $('<ul>', { style: 'list-style: none' }).append($('<li>', { id: 'us_editcount' }).append([$('<b>').append($contributions.text(msg.contrib)), us.loading_editcount]));
us.us_log_count = $('<span>');
if (us.viewPatrolNumber) {
us.us_patrolcount_loading = $('<a>', { style: us.styleLoading, click: us.getPatrolCount, text: msg.count });
us.us_log_count = $('<span>').append([$('<b>').text(msg.reviews), us.us_patrolcount_loading, ' • ']);
}
ul.append(us.us_log_count)
.append($('<li>', { id: 'us_reg_date' }).append([$('<b>').text(msg.regdate), us.loading_registration, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://meta.wikimedia.org/wiki/Special:UserPages/' + us.user, text: '• Global user pages' })]))
.append($('<li>').append([$('<b>').append($userrights.text(msg.loGrp)), us.loading_groups]))
.append($('<li>', { id: 'us_former_local_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerLoGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.us_global_group.append([$('<b>').append($('<a>', { href: 'https://meta.wikimedia.org/wiki/Special:Log?type=gblrights&user=&page=User:' + us.user, text: msg.glGrp })), us.us_global_group_loading]))
.append($('<li>', { id: 'us_former_global_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerGlGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.loading_last_edit.append([$('<b>').text(msg.laedit), us.us_last_edit_loading, ' ', $('<a>', { target: '_blank', style: us.styleMissingData, href: '/w/index.php?title=Special:Log/' + us.user, text: '• LLA' }), $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://meta.wikimedia.org/wiki/Special:GlobalContributions/' + us.user, text: '• Global contributions' })]))
.append($('<li>', { id: 'us_last_action_li', style: 'display:none' }).append([$('<b>').text(msg.laact), $('<span>', { id: 'us_last_action_val' })]))
.append($('<li>', { id: 'us_last_admin_li', style: 'display:none' }).append([$('<b>').text(msg.laadmin), $('<span>', { id: 'us_last_admin_val' })]))
.append($('<li>', { id: 'us_last_cu_li', style: 'display:none' }).append([$('<b>').text(msg.lacu), $('<span>', { id: 'us_last_cu_val' })]))
.append($('<li>', { id: 'us_last_os_li', style: 'display:none' }).append([$('<b>').text(msg.laos), $('<span>', { id: 'us_last_os_val' })]));
statusBox.append(ul);
us.ul = $('<ul>', { style: 'list-style:none' }).append($('<li>', { id: 'us_block_status' }).append([$('<b>').text(msg.blocks), us.loading_blocked, ' • ', $('<b>').text(msg.thxGvng), thx, ' / ', thx.clone(1).text(msg.thxGn), $('<a>', { style: 'float:right;' + us.styleMissingData, title: 'Purge data', text: 'purge', click: us.removeDataStore })]));
statusBox.append(us.ul);
$('#firstHeading').after(statusBox);
us.statusBox = statusBox;
},
removeDataStore: function (e) {
var key = project + us.user, db = window.indexedDB;
e.preventDefault();
if (!db || !us.dbVersion) return mw.cookie.set(us.name + us.user, null);
var request = db.open(us.name, us.dbVersion + 1);
request.onupgradeneeded = function () { db = this.result; if (db.objectStoreNames.contains(key)) db.deleteObjectStore(key); };
request.onsuccess = function () { db = this.result; us.dbVersion = db.version; db.close(); location.reload(); };
},
setCookie: function () {
var domain = (mw.config.get('wgNoticeProject') === 'wikipedia') ? 'wikipedia.org' : '';
var name = us.name + us.user;
var _saveCookie = function (k, d) { mw.cookie.set(name, JSON.stringify(d), { expires: 604800, domain: domain }); };
if (!us.actions[0] && JSON && data.editcount) {
if (us.cookie.length) window.clearTimeout(us.cookie.shift());
var saveData = window.indexedDB ? function (n, JSd, v) {
var key = project + us.user, db = window.indexedDB, request = db.open(us.name, v);
request.onupgradeneeded = function () {
db = this.result;
if (!db.objectStoreNames.contains(key)) {
var store = db.createObjectStore(key, { keyPath: 'name' });
store.createIndex('data', 'data', { unique: false });
}
};
request.onsuccess = function () {
db = this.result; us.dbVersion = db.version;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readwrite').objectStore(key);
store.put({ name: key, data: JSd });
} else { saveData(n, JSd, us.dbVersion + 1); }
};
request.onerror = function() { _saveCookie(n, JSd); };
} : _saveCookie;
us.cookie.push(setTimeout(function () {
data.timestamp = new Date().valueOf();
saveData('', data);
}, 500));
}
},
doRequest: function () {
var action = us.actions.shift();
if (action) us.ajaxRequest(action.params, action.func);
if (us.actions.length) window.setTimeout(function () { us.doRequest(); }, 100);
},
getDateDiff: function (now, date) {
var dStr = [];
if (now > date) {
var years = now.getFullYear() - date.getFullYear();
var months = now.getMonth() - date.getMonth();
var days = now.getDate() - date.getDate();
// Korekce dnů a měsíců
if (days < 0) {
months--;
// Získáme počet dní v předchozím měsíci
var prevMonth = new Date(now.getFullYear(), now.getMonth(), 0).getDate();
days += prevMonth;
}
// Korekce měsíců a roků
if (months < 0) {
years--;
months += 12;
}
// Návrat slovních označení pro dnešek a včerejšek
if (years === 0 && months === 0 && days === 0) return msg.today || 'today';
if (years === 0 && months === 0 && days === 1) return msg.yesterday || 'yesterday';
var units = [years, months, days];
var labelSingle = [msg.date[0], msg.date[1], msg.date[2]];
var labelPlural = [msg.dates[0], msg.dates[1], msg.dates[2]];
for (var i = 0; i < 3; ++i) {
if (units[i] > 0) {
dStr.push(units[i] + ' ' + ((units[i] > 1) ? labelPlural[i] : labelSingle[i]));
}
}
}
var mainDiff = (dStr.length) ? dStr.join(', ').replace(/(.*),([^,]*)/, msg.and).replace(/(.*)/, msg.ago) : (msg.today || 'today');
return mainDiff;
},
getDateFromTimestamp: function (t) { if (!t) return false; t = new Date(t); return isNaN(t.valueOf()) ? false : t; },
writeRegistration: function (aw) {
if (aw) {
if (aw instanceof Object) {
if (aw.query && aw.query.logevents.length) aw = aw.query.logevents[0].timestamp;
}
if (typeof aw === 'string') {
us.loading_registration.replaceWith(us.formatDate(aw) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(aw)) + ')' }));
data.registration = aw;
return us.setCookie();
}
}
us.loading_registration.replaceWith($('<span>', { style: us.styleMissingData + us.styleLoading, text: msg.nodb }));
},
writeFirstEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs.length) return;
var date = data.firstedit = aw.query.usercontribs[0].timestamp;
$('#us_reg_date').append($('<li>').append([$('<b>', { text: '• ' + msg.fiedit }), us.formatDate(date) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(date)) + ')' })]));
},
writeLastEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs || !aw.query.usercontribs.length) return us.us_last_edit_loading.replaceWith($('<span>', { style: us.styleMissingData, text: msg.noedit }));
var date = data.lastedit = aw.query.usercontribs[0].timestamp;
// Vytvoření textu s odkazem
var diffText = us.getDateDiff(us.now, us.getDateFromTimestamp(date));
var diffNode = $('<a>', { href: mw.util.getUrl('Special:Contributions/' + us.user), text: diffText });
us.us_last_edit_loading.replaceWith(diffNode, ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.formatDate(date) + ')' }));
data.timediff = mw.now() - us.now;
$('#us_editcount').append($('<span>', { style: 'float:right;' + us.styleMissingData, text: msg.curtimeDiff }).append($('<b>', { text: Math.round(data.timediff / 1000) })));
us.setCookie();
},
writeGlobalGroup: function (aw) {
aw = aw.query.globaluserinfo;
var groups = data.glGrp = aw.groups;
// Pokud lokální registrace chybí, použijeme tu z globálního infa
if (!data.registration && aw.registration) {
us.writeRegistration(aw.registration);
}
if (groups && groups.length) {
us.us_global_group_loading.replaceWith(us.getLocalNames(groups, 'GlobalGroupPermissions'));
us.us_global_group.css('display', 'block');
// Fetch former global rights
us.fetchFormerRights(true);
}
if (aw.locked !== undefined) {
data.locked = 1;
mw.loader.using('mediawiki.ForeignApi').done(function () {
var fApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php');
fApi.get({
action: 'query',
list: 'logevents',
leprop: 'user|timestamp|comment',
letype: 'globalauth',
letitle: 'User:' + us.user,
lelimit: '1'
}).done(us.writeGlobalBlock);
});
}
us.setCookie();
},
writeGlobalBlock: function (aw) {
if (!aw.query || !aw.query.logevents.length) return;
aw = aw.query.logevents[0];
$('#us_block_status_span').replaceWith($('<a>', { style: us.styleBlocked, id: 'us_block_status_span', href: '//meta.wikimedia.org/w/index.php?title=Special:Log/&type=globalauth&user=&page=User%3A' + us.user + '%40global', text: 'locked (global)' }).append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) })));
$('#us_block_status').parent().append([$('<li>').append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment)]), $('<li>').append([$('<b>').text('• ' + msg.blocker), $('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user })])]);
},
writeBlockDetail: function (aw) {
var duration = 'infinite', expiry = '';
if (aw.query && aw.query.logevents.length) {
aw = aw.query.logevents[0];
if (aw && aw.params) {
duration = aw.params.duration;
expiry = /in(de)?finite/.test(duration) ? msg.never : us.formatDate(aw.params.expiry);
}
$('#us_block_status_span').text(msg.block + ' (' + duration + ')').append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) }));
if (expiry) $('#mw-spinner-us_block_time_loading').replaceWith(expiry); else $('#us_block_time').remove();
$('#mw-spinner-us_block_reason_loading').replaceWith(us.parseComment(aw.comment, aw.commenthidden));
if (aw.user) $('#mw-spinner-us_blocker_loading').replaceWith($('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user }));
else $('#mw-spinner-us_blocker_loading').replaceWith(msg.nodb);
}
},
writePatrolCount: function (aw) {
if (aw instanceof Object) {
if (!aw.query || !aw.query.logevents) return;
us.patrols += aw.query.logevents.length;
if (aw.continue) return us.getPatrolCount(aw.continue);
$.removeSpinner('pat');
aw = us.patrols;
}
us.us_patrolcount_loading.replaceWith($('<a>', { href: '/w/index.php?title=Special:Log&type=' + (us.review ? 'review&subtype=accept' : 'patrol&subtype=patrol') + '&user=' + us.user, text: aw }));
data.reviews = aw;
us.setCookie();
},
getPatrolCount: function (e) {
var params = '&list=logevents&leprop=&ledir=newer&lelimit=max&leuser=' + us.user + '&leaction=' + (us.review ? 'review/approve' : 'patrol/patrol');
if (e instanceof Object) {
if (e.target) { $(e.target).injectSpinner('pat'); us.patrols = 0; }
else if (e.lecontinue) params += '&lecontinue=' + e.lecontinue;
}
us.actions.push({ params: params, func: us.writePatrolCount });
us.doRequest();
},
parseComment: function (text, hidden) {
var comment = $('<span>', { 'class': 'comment' });
if (typeof text === 'undefined') return comment.append(hidden ? msg.noReason : 'undefined').css('color', hidden ? '#999' : 'red');
if (!text) return comment.append(msg.blockCmt).css('color', '#999');
var intLink = /(.*?)\[\[((.*?)\|)?(.*?)\]\](.*)/, suche = text, erg;
while ((erg = intLink.exec(suche)) !== null) {
var link = erg[3] || erg[4];
comment.append([erg[1], $('<a>', { href: mw.util.getUrl(link), title: link }).text(erg[4])]);
suche = erg[5];
}
return comment.append(suche);
},
formatDate: function (datum) {
if (!(datum instanceof Date)) datum = new Date(datum);
try {
return datum.toLocaleDateString(us.lang, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
} catch (e) { return datum.toLocaleString(); }
},
getStoredData: function () {
var key = project + us.user, db, request;
try {
request = indexedDB.open(us.name);
request.onsuccess = function () {
db = this.result;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readonly').objectStore(key);
store.transaction.oncomplete = function () { us.runDataStore(); db.close(); };
store.get(key).onsuccess = function () { if (this.result) data = this.result.data; else us.getCookie(); };
} else { us.getCookie(); }
};
request.onerror = function () { us.getCookie(); };
} catch (e) { us.getCookie(); }
},
getCookie: function () {
data = mw.cookie.get(us.name + us.user);
if (data) try { data = JSON.parse(data); } catch(e) { data = null; }
us.runDataStore();
},
initI18N: function (i18n) {
var chain = mw.language.getFallbackLanguageChain();
for (var i = chain.length - 1; i >= 0; i--) if (chain[i] in i18n) msg = i18n[chain[i]];
},
init: function () {
if (this.statusBox || (this.statusBox = $('#us_box'))[0]) this.statusBox.remove();
this.usprop = 'blockinfo|editcount|gender|groups|implicitgroups|registration';
this.self = mw.config.get('wgUserName') === this.user;
this.initI18N(i18n);
this.createBox();
if (!this.first) {
if (mw.cookie && JSON) { if (window.indexedDB) this.getStoredData(); else this.getCookie(); }
else this.run();
} else this.run();
},
runDataStore: function () {
if (data) {
if (typeof data === 'string') try { data = JSON.parse(data); } catch(e) { data = {}; }
var q = { query: { users: [data] } };
if (data.timestamp) this.now = mw.now() - (data.timediff || 0);
if (data.timestamp && Math.abs((mw.now() - data.timestamp - (data.timediff || 0)) / 1000) < 90) return this.writeCommonInfo(q);
if (data.editcount) this.usprop = 'editcount';
}
data = data || {};
this.run();
},
run: function () {
if (this.self) {
return this.writeCommonInfo({ query: { users: [{ editcount: mw.config.get('wgUserEditCount'), registration: mw.config.get('wgUserRegistration'), groups: mw.config.get('wgUserGroups'), blockreason: '' }] } });
}
us.ajaxRequest({ curtimestamp: 1, list: 'users', ususers: us.user, usprop: us.usprop }, us.writeCommonInfo);
}
};
if ([2, 3].indexOf(mw.config.get('wgNamespaceNumber')) !== -1 && us.user.indexOf('/') === -1 && (!mw.config.get('wgArticleId') || mw.config.get('wgAction') === 'view')) {
$.when(mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.language', 'mediawiki.cookie', 'jquery.spinner', 'mediawiki.ForeignApi']), $.ready).then(function () {
us.api = mw.util.wikiScript('api') + '?action=query&format=json';
us.ajaxRequest('&meta=allmessages&amenableparser=1&amincludelocal=1&refix=group-', us.getGroupNames);
if (!mw.libs.viewerInfo) mw.loader.load('//meta.wikimedia.org/w/index.php?title=User:Perhelion/viewerInfo.js&action=raw&ctype=text/javascript');
});
}
}(jQuery, mediaWiki));
gq0ot5x8vy8iaesfyycm707n3wtprq7
739973
739971
2026-05-01T11:56:36Z
MrJaroslavik
44012
+
739973
javascript
text/javascript
/**
* @Description: Modified Userstatus.js with Former Groups (Local & Global)
* Original by Steef389, Perhelion.
**/
(function ($, mw) {
'use strict';
var project = window.project || mw.config.get('wgDBname');
var msg, i18n = {
en: {
noReason: 'reason removed',
blockCmt: 'no block comment',
never: 'never',
not: ' not ',
block: 'blocked',
and: '$1 and$2',
count: 'Count',
noedit: 'None (only deleted contributions?)',
nodb: '‹not in database›',
noGrp: 'no extended group',
dates: ['years', 'months', 'days', 'hours', 'minutes', 'seconds'],
date: ['year', 'month', 'day', 'hour', 'minute', 'second'],
nosec: 'less than one ',
curtimeDiff: ' Locale time difference to server time in s: ',
thxGn: 'given',
thxRd: 'received',
nova: 'new',
ago: '$1 ago',
blocklog: 'Block log',
contrib: 'Edits: ',
usub: 'Subpages⬇',
thxGvng: 'Thanksgivings: ',
reviews: 'Active reviews: ',
regdate: 'Registration: ',
laedit: 'Last edited: ',
lala: 'Last log activity',
fiedit: 'First edit: ',
blocks: 'Block-status: ',
loGrp: 'Local user-groups: ',
glGrp: 'Global user-groups: ',
formerLoGrp: 'Former local user-groups: ',
formerGlGrp: 'Former global user-groups: ',
blockEnd: 'Block-end: ',
blocker: 'Blocker: ',
blockReason: 'Block-reason: ',
laact: 'Last action: ',
laadmin: 'Last admin action: ',
lacu: 'Last CU action: ',
laos: 'Last OS action: ',
today: 'today',
yesterday: 'yesterday'
},
cs: {
noReason: 'důvod odstraněn',
blockCmt: 'bez komentáře',
never: 'nikdy',
not: ' ne ',
block: 'zablokován',
and: '$1 a$2',
count: 'Počet',
noedit: 'Žádné (jen smazané?)',
nodb: '‹není v databázi›',
noGrp: 'žádná rozšířená skupina',
dates: ['let', 'měsíců', 'dní', 'hodin', 'minut', 'sekund'],
date: ['rok', 'měsíc', 'den', 'hodina', 'minuta', 'sekunda'],
nosec: 'méně než ',
curtimeDiff: ' Rozdíl lokálního času k serveru v s: ',
thxGn: 'poděkoval',
thxRd: 'obdržel poděkování',
nova: 'nové',
ago: 'před $1',
days: 'dní',
blocklog: 'Kniha zablokování',
contrib: 'Editace: ',
usub: 'Podstránky⬇',
thxGvng: 'Poděkování: ',
reviews: 'Aktivní prověření: ',
regdate: 'Registrace: ',
laedit: 'Poslední editace: ',
lala: 'Poslední aktivita v protokolech',
fiedit: 'První editace: ',
blocks: 'Stav bloku: ',
loGrp: 'Lokální skupiny: ',
glGrp: 'Globální skupiny: ',
formerLoGrp: 'Bývalé lokální skupiny: ',
formerGlGrp: 'Bývalé globální skupiny: ',
blockEnd: 'Konec bloku: ',
blocker: 'Zablokoval: ',
blockReason: 'Důvod bloku: ',
laact: 'Poslední akce: ',
laadmin: 'Poslední admin akce: ',
lacu: 'Poslední CU akce: ',
laos: 'Poslední OS akce: ',
today: 'dnes',
yesterday: 'včera'
}
};
var data;
var us = mw.libs.userstatus = {
name: 'Userstatus',
version: 1.80, // Bumping version for former groups update
lastEditSeconds: false,
styleMissingData: 'color:#999;font-size:90%;',
styleLoading: 'font-style:italic;',
styleBlocked: 'color:#c00',
styleNotBlocked: 'color:#080',
styleFormer: 'margin-left:15px; color:#72777d; font-style:italic; font-size:0.9em;',
viewPatrolNumber: false,
lang: mw.config.get('wgUserLanguage'),
user: mw.config.get('wgTitle'),
cookie: [],
thanks: 0,
patrols: 0,
actions: [],
dbVersion: 0,
implicitGroups: ['*', 'user', 'autoconfirmed'],
getLocalNames: function (groups, specialpage) {
if (groups) {
var arr = [];
specialpage = specialpage || 'ListGroupRights';
for (var i = 0; i < groups.length; ++i) {
var g = groups[i];
var n = us.groupNames[g];
if (n) arr.push('<a href="/wiki/Special:' + specialpage + '#' + g + '">' + n + '</a>');
else arr.push(g);
}
groups = arr.join(', ').replace(/(.*),([^,]*)$/, msg.and);
}
return groups;
},
writeGroups: function (groups) {
groups = us.getLocalNames(groups);
if (!groups) {
groups = $('<span>', {
style: us.styleMissingData + us.styleLoading,
text: msg.noGrp
});
}
us.loading_groups.replaceWith(groups);
// After local groups are written, fetch former local groups
us.fetchFormerRights(false);
},
getGroupNames: function (aw) {
if (!aw || !aw.query) return;
aw = aw.query.allmessages;
var al = aw.length;
var groups = {};
while (al--) {
var an = aw[al];
var name = an.name;
if (/^group-/.test(name)) {
name = RegExp.rightContext || name.substring(6);
groups[name] = an['*'];
}
}
us.groupNames = groups;
us.init();
},
ajaxRequest: function (params, onSuccess, trial) {
var url = us.api;
if (!(params instanceof Object)) {
url += params + '&maxage=2419200&smaxage=2419200';
params = {};
} else {
params.maxlag = 3;
params.maxage = 2419200;
params.smaxage = 2419200;
if (trial) params.maxlag *= trial;
}
var api = $.getJSON(url, params, onSuccess)
.fail(function (jqXHR, status) {
var note = 'Timeout fail, maybe try again!';
if (trial) {
if (!status.textStatus || (status.textStatus !== 'timeout' && jqXHR !== 'maxlag')) note = $('<span>Fehler bitte <a href="' + mw.util.getUrl('User_talk:Perhelion') + '">Perhelion</a> melden.</span>');
mw.notify(note, { title: us.name + ':', type: 'error' });
} else if (status.textStatus === 'timeout' || jqXHR === 'maxlag') {
api.abort();
return us.ajaxRequest(params, onSuccess, 2);
}
});
},
// --- NEW FORMER RIGHTS LOGIC ---
fetchFormerRights: function(isGlobal) {
var apiTarget = isGlobal ? 'https://meta.wikimedia.org/w/api.php' : mw.util.wikiScript('api');
var logType = isGlobal ? 'gblrights' : 'rights';
var userTarget = 'User:' + us.user;
var containerId = isGlobal ? 'us_former_global_groups' : 'us_former_local_groups';
// Globální override pro smazané skupiny (skupina: datum smazání)
var globalOverrides = {
'oathauth-tester': '2026-01-12T10:58:00Z'
};
var params = {
action: 'query',
list: 'logevents',
letype: logType,
letitle: userTarget,
lelimit: 500,
format: 'json',
origin: isGlobal ? '*' : undefined
};
$.getJSON(apiTarget, params, function(res) {
if(!res || !res.query || !res.query.logevents) return;
var logs = res.query.logevents;
var former = {};
var currentGroups = isGlobal ? (data.glGrp || []) : (data.groups || []);
logs.forEach(function(log) {
var p = log.params;
if (!p) return;
var date = log.timestamp;
var oldRaw = p.oldgroups || p.oldGroups || p.remove || p["0"] || "";
var newRaw = p.newgroups || p.newGroups || p.add || p["1"] || "";
var parse = function(val) {
if (Array.isArray(val)) return val;
if (typeof val === 'string') {
if (val === "(none)" || val === "") return [];
return val.split(/[,|]\s*/);
}
return [];
};
var oldG = parse(oldRaw);
var newG = parse(newRaw);
// 1. Nejdřív kontrolujeme udělení (v logu jdeme od nejnovějších k nejstarším)
newG.forEach(function(g) {
g = g.trim();
if (us.implicitGroups.indexOf(g) !== -1 || !g) return;
// Speciální případ: Skupina byla udělena, uživatel ji nemá a víme, že globálně zanikla
if (currentGroups.indexOf(g) === -1 && globalOverrides[g] && !former[g]) {
former[g] = { to: globalOverrides[g], from: date };
}
// Standardní případ: Už známe konec z oldG a teď jsme našli, kdy to začalo
else if (former[g] && !former[g].from) {
former[g].from = date;
}
});
// 2. Poté kontrolujeme odebrání (standardní cesta)
oldG.forEach(function(g) {
g = g.trim();
if (us.implicitGroups.indexOf(g) !== -1 || !g) return;
// Pokud skupinu nemá a ještě jsme o ní v tomto průchodu neslyšeli
if (currentGroups.indexOf(g) === -1 && !former[g]) {
former[g] = { to: date, from: null };
}
});
});
var entries = [];
Object.keys(former).sort().forEach(function(g) {
var item = former[g];
// Aplikace globálního overridu, pokud chybí datum odebrání a skupina je v seznamu smazaných
if (!item.to && globalOverrides[g]) {
item.to = globalOverrides[g];
}
var dateTo = new Date(item.to);
var dateFrom = item.from ? new Date(item.from) : null;
if(dateFrom && item.to) {
var diffText = us.getDateDiff(dateTo, dateFrom).replace(msg.ago.replace('$1', ''), '').trim();
entries.push(g + ' (' + dateFrom.toISOString().split('T')[0] + ' – ' + dateTo.toISOString().split('T')[0] + ', ' + diffText + ')');
} else if (item.to) {
entries.push(g + ' (do ' + dateTo.toISOString().split('T')[0] + ')');
}
});
if(entries.length > 0) {
$('#' + containerId).show().find('.us_former_content').text(entries.join(', '));
}
});
},
getThanks: function (type, lecontinue, currentCount) {
var params = '&list=logevents&continue=-%7C%7C&leprop=&letype=thanks&ledir=newer&lelimit=max';
if (lecontinue) params += '&lecontinue=' + lecontinue;
if (type === 'gn') params += '&leuser=' + us.user;
else params += '&letitle=User:' + us.user;
us.actions.push({
params: params,
func: function(uq) {
us.writeThanks(uq, type, currentCount || 0);
}
});
},
writeThanks: function (uq, type, currentCount) {
if (!uq.query || !uq.query.logevents) return mw.log.warn(uq);
currentCount += uq.query.logevents.length;
// Pokud má uživatel poděkování více (na více stránek), jedeme dál
if (uq.continue && uq.continue.lecontinue) {
return us.getThanks(type, uq.continue.lecontinue, currentCount);
}
var e = type === 'gn' ? $('#us_thx_gn_val') : $('#us_thx_rd_val');
var userParam = type === 'gn' ? '&user=' + us.user : '&page=User:' + us.user;
e.text(currentCount).css({'font-style': 'normal'});
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special%3ALog&type=thanks' + userParam + '&year=&month=-1&tagfilter=&hide_thanks_log=0';
},
getUploads: function (e) {
var params = '&list=logevents&letype=upload&ledir=newer&lelimit=max&leuser=' + us.user;
function _start(u) {
var text = u.text();
u.prop('title', text);
u.text('');
u.injectSpinner('upload' + text);
u.data('upl', 0);
u.data('del', 0);
}
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
if (us.e.text() !== msg.nova) {
us.e2 = us.e.nextAll('a').eq(0);
if (us.e2.text() === msg.nova) _start(us.e2); else delete us.e2;
}
_start(us.e);
} else if (e.lecontinue) { params += '&lecontinue=' + e.lecontinue; }
params += '&leprop=ids';
if (us.e.prop('title') === msg.nova) params += '&leaction=upload/upload';
else if (us.e2) params += '|type';
}
us.actions.push({ params: params, func: us.writeUploads });
us.doRequest();
},
writeUploads: function (uq) {
if (!uq.query || !uq.query.logevents || !us.e) return mw.log(uq, us.e);
function _insert(e) {
$.removeSpinner('upload' + e.prop('title'));
e.text('');
e.off('click');
e.append(e.data('del'), $('<span>', { style: us.styleMissingData + us.styleLoading, text: ' (+' + (e.data('upl') - e.data('del')) + ' del.)' }));
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special:Log&type=upload&user=' + us.user + '&subtype=' + ((e[0].title === msg.nova) ? 'upload' : '');
}
var e = us.e;
var e2 = us.e2;
var aw = uq.query.logevents;
var alen = aw.length, i = 0;
var del = e.data('del');
if (e2) {
var upl = e2.data('upl');
var del2 = e2.data('del');
for (i; i < alen; ++i) {
var a = aw[i];
var c = a.pageid ? 1 : 0;
if (a.action === 'upload') { upl++; del2 += c; }
del += c;
}
e2.data('upl', upl);
e2.data('del', del2);
} else {
for (i; i < alen; ++i) if (aw[i].pageid) del++;
}
e.data('upl', e.data('upl') + alen);
e.data('del', del);
if (uq.continue) return us.getUploads(uq.continue);
_insert(e);
if (e2) { _insert(e2); delete us.e2; }
delete us.e;
},
writeCommonInfo: function (uq) {
var aw = uq.query;
if (!aw || !aw.users[0] || Object.prototype.hasOwnProperty.call(aw, 'missing') || Object.prototype.hasOwnProperty.call(aw.users[0], 'invalid')) return us.statusBox.remove();
aw = aw.users[0];
var edits = aw.editcount,
groups = data.groups || aw.groups,
blocked = data.blockreason || aw.blockexpiry,
gender = data.gender || aw.gender,
uploads = $('<a>', { title: msg.count, text: msg.nova, style: us.styleLoading, click: us.getUploads });
if (aw.registration) data.registration = aw.registration;
us.now = us.getDateFromTimestamp(uq.curtimestamp || us.now || mw.now());
us.user = us.user.replace(/ /g, '_');
us.first = 1;
// POMOCNÁ FUNKCE PRO VÝPIS AKCÍ Z LOGU (S ODKAZEM)
var renderActionRow = function(valSelector, liSelector, timestamp, url) {
var d = new Date(timestamp);
var diff = us.getDateDiff(us.now, d);
var formatted = us.formatDate(d);
var diffNode = url ? $('<a>', { href: url, text: diff }) : diff;
$(valSelector).empty().append(diffNode, ' ', $('<span>', { style: us.styleMissingData, text: '(' + formatted + ')' }));
$(liSelector).show();
};
// A. OBECNÉ A ADMIN AKCE
var adminTypes = ['block', 'delete', 'protect', 'rights', 'merge', 'abusefilter', 'renameuser', 'gblblock'];
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=50',
func: function(res) {
if(!res.query || !res.query.logevents) return;
var logs = res.query.logevents;
if(logs[0]) {
renderActionRow('#us_last_action_val', '#us_last_action_li', logs[0].timestamp, mw.util.getUrl('Special:Log/' + us.user));
}
var adm = logs.find(function(l) { return adminTypes.indexOf(l.type) !== -1; });
if(adm) {
renderActionRow('#us_last_admin_val', '#us_last_admin_li', adm.timestamp, mw.util.getUrl('Special:Log', { user: us.user }));
}
}
});
// B. CU AKCE
if (groups && $.inArray('checkuser', groups) !== -1) {
us.actions.push({
params: '&list=checkuserlog&culuser=' + us.user + '&cullimit=1',
func: function(res) {
if(res.query && res.query.checkuserlog && res.query.checkuserlog.entries && res.query.checkuserlog.entries[0]) {
var cuUrl = mw.util.getUrl('Special:CheckUserLog', { cuSearch: '', cuInitiator: us.user });
renderActionRow('#us_last_cu_val', '#us_last_cu_li', res.query.checkuserlog.entries[0].timestamp, cuUrl);
}
}
});
}
// C. OS AKCE (Ošetřen oversight i suppress)
if (groups && ($.inArray('oversight', groups) !== -1 || $.inArray('suppress', groups) !== -1)) {
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=1&letype=suppress',
func: function(res) {
if(res.query && res.query.logevents[0]) {
var osUrl = mw.util.getUrl('Special:Log', { type: 'suppress', user: us.user });
renderActionRow('#us_last_os_val', '#us_last_os_li', res.query.logevents[0].timestamp, osUrl);
}
}
});
}
if (edits) {
if (data.editcount && data.lastedit && data.editcount === edits) {
uq.query.usercontribs = [{ timestamp: data.lastedit }];
us.writeLastEdit(uq);
} else {
data.editcount = edits;
us.loading_last_edit.css('display', 'block');
us.actions.push({
params: '&list=usercontribs&ucuser=' + us.user + '&uclimit=1&uccontinue=' + (new Date(us.now).toISOString().replace(/[^\d]/g, '').slice(0, 14) + '|2'),
func: us.writeLastEdit
});
}
$('#t-contributions').remove();
} else { us.writeLastEdit({}); }
if (groups) {
edits = $('<a>', { title: 'Supercount', href: '//tools.wmflabs.org/supercount/index.php?user=' + us.user + '&project=' + location.hostname, text: edits });
if ($.inArray('bot', groups) === -1) {
us.review = $.inArray('editor', groups) !== -1;
if (us.review) {
if (typeof data.reviews === 'number') us.writePatrolCount(data.reviews);
} else if (us.viewPatrolNumber) { us.us_patrolcount_loading.parent().remove(); }
edits = [
edits, ' • ',
$('<a>', { href: '//xtools.wmflabs.org/ec' + mw.config.get('wgServer').substr(1) + '/' + us.user, title: 'XTools', text: 'XTools', style: us.styleLoading, target: '_blank' }), ' • ',
$('<a>', { href: 'https://meta.wikimedia.org/wiki/Special:CentralAuth/' + us.user, title: 'CentralAuth', text: 'CentralAuth', style: us.styleLoading, target: '_blank' }), ' • ',
(project !== 'commonswiki' ? $('<a>', { href: '/wiki/Special:ListFiles/' + us.user, text: 'Upload', title: 'Special:Listfiles', style: us.styleLoading }) : 'Upload'),
(us.lang === 'de' ? '-' : ' ') + msg.count + ': ',
uploads.clone(1).text('total'), ' / ', uploads
];
if (!$('#t-subpages').length) {
edits.push($('<a>', { href: mw.util.getUrl('Special:Prefixindex/User:' + us.user + '/'), text: ' • ' + msg.usub, style: us.styleMissingData + us.styleLoading }));
}
} else if (us.us_patrolcount_loading) { us.us_patrolcount_loading.parent().remove(); }
us.loading_editcount.replaceWith(edits);
if (!aw.implicitgroups && !data.groups) {
aw.implicitgroups = ['*', 'user', 'autoconfirmed'];
if (groups.length < 4) aw.implicitgroups.pop();
}
data.groups = $.grep(groups, function (n) { return $.inArray(n, aw.implicitgroups) === -1; });
us.writeGroups(data.groups);
$('#t-userrights').remove();
}
if (!data.registration) {
us.actions.push({ params: '&list=logevents&leprop=timestamp|type&letype=newusers&lelimit=1&ledir=newer&leend=2005-12-30T00:00:00Z&leuser=' + us.user, func: us.writeRegistration });
if (data.firstedit) {
uq.query.usercontribs = [{ timestamp: data.firstedit }];
us.writeFirstEdit(uq);
} else if (edits) {
us.actions.push({ params: { list: 'usercontribs', ucuser: us.user, uclimit: 1, ucend: '2005-09-08T00:00:00Z', ucdir: 'newer', ucprop: 'timestamp' }, func: us.writeFirstEdit });
}
} else { us.writeRegistration(data.registration); }
us.loading_blocked.replaceWith($('<a>', { style: ((blocked) ? us.styleBlocked : us.styleNotBlocked), id: 'us_block_status_span', href: '/w/index.php?title=Special:Log/block&page=User:' + us.user, title: msg.blocklog, text: ((blocked) ? ' ' : msg.not) + msg.block }));
if (blocked) {
data.blockreason = aw.blockreason || ' ';
us.ul.append([
$('<li>', { id: 'us_block_time' }).append([$('<b>').text('• ' + msg.blockEnd), $.createSpinner('us_block_time_loading')]),
$('<li>', { id: 'us_block_reason' }).append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment, (aw.commenthidden))]),
$('<li>', { id: 'us_blocker' }).append([$('<b>').text('• ' + msg.blocker), $.createSpinner('us_blocker_loading')])
]);
us.actions.push({ params: { list: 'logevents', letitle: 'User:' + us.user, letype: 'block', lelimit: 1 }, func: us.writeBlockDetail });
}
if (data.glGrp && !data.locked) {
uq.query.globaluserinfo = { groups: (data.glGrp.length) ? data.glGrp : null };
us.writeGlobalGroup(uq);
} else {
us.actions.push({ params: '&meta=globaluserinfo&guiprop=groups&guiuser=' + us.user, func: us.writeGlobalGroup });
}
if (gender) {
data.gender = gender;
var genderSn = (gender === 'male') ? ' \u2642' : (gender === 'female' ? ' \u2640' : '');
$('#firstHeading').append($('<span>', { id: 'ps-gender-' + gender, style: 'font-size:80%', text: genderSn }));
}
// Automatické načtení poděkování do fronty dotazů
us.getThanks('rd');
us.getThanks('gn');
if (us.actions.length) us.doRequest();
},
createBox: function () {
var statusBox = $('<div>', { style: 'border-bottom:1px solid #aaa;text-shadow:1px 1px 1px #eff;', id: 'us_box' });
if (mw.config.get('skin') === 'vector') statusBox.css('font-size', '0.8em');
var spanFrag = $.createSpinner();
us.loading_editcount = spanFrag.clone();
us.loading_registration = spanFrag.clone();
us.loading_groups = spanFrag.clone();
us.loading_blocked = spanFrag.clone();
us.us_global_group_loading = spanFrag.clone();
us.us_last_edit_loading = $.createSpinner('contribs');
us.loading_last_edit = $('<li>');
us.us_global_group = $('<li>', { style: 'display: none' });
var thxRd = $('<a>', { id: 'us_thx_rd_val', title: msg.thxRd, style: us.styleLoading, text: '...' });
var thxGn = $('<a>', { id: 'us_thx_gn_val', title: msg.thxGn, style: us.styleLoading, text: '...' });
var $userrights = $('#t-userrights a');
var $contributions = $('#t-contributions a');
if (!$userrights.length) $userrights = $('<a>', { href: '/wiki/Special:UserRights/' + us.user });
if (!$contributions.length) $contributions = $('<a>', { href: '/wiki/Special:Contributions/' + us.user });
var ul = $('<ul>', { style: 'list-style: none' }).append($('<li>', { id: 'us_editcount' }).append([$('<b>').append($contributions.text(msg.contrib)), us.loading_editcount]));
us.us_log_count = $('<span>');
if (us.viewPatrolNumber) {
us.us_patrolcount_loading = $('<a>', { style: us.styleLoading, click: us.getPatrolCount, text: msg.count });
us.us_log_count = $('<span>').append([$('<b>').text(msg.reviews), us.us_patrolcount_loading, ' • ']);
}
ul.append(us.us_log_count)
.append($('<li>', { id: 'us_reg_date' }).append([$('<b>').text(msg.regdate), us.loading_registration, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://meta.wikimedia.org/wiki/Special:UserPages/' + us.user, text: '• Global user pages' })]))
.append($('<li>').append([$('<b>').append($userrights.text(msg.loGrp)), us.loading_groups]))
.append($('<li>', { id: 'us_former_local_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerLoGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.us_global_group.append([$('<b>').append($('<a>', { href: 'https://meta.wikimedia.org/wiki/Special:Log?type=gblrights&user=&page=User:' + us.user, text: msg.glGrp })), us.us_global_group_loading]))
.append($('<li>', { id: 'us_former_global_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerGlGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.loading_last_edit.append([$('<b>').text(msg.laedit), us.us_last_edit_loading, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://meta.wikimedia.org/wiki/Special:GlobalContributions/' + us.user, text: '• Global contributions' })]))
.append($('<li>', { id: 'us_last_action_li', style: 'display:none' }).append([$('<b>').text(msg.laact), $('<span>', { id: 'us_last_action_val' })]))
.append($('<li>', { id: 'us_last_admin_li', style: 'display:none' }).append([$('<b>').text(msg.laadmin), $('<span>', { id: 'us_last_admin_val' })]))
.append($('<li>', { id: 'us_last_cu_li', style: 'display:none' }).append([$('<b>').text(msg.lacu), $('<span>', { id: 'us_last_cu_val' })]))
.append($('<li>', { id: 'us_last_os_li', style: 'display:none' }).append([$('<b>').text(msg.laos), $('<span>', { id: 'us_last_os_val' })]));
statusBox.append(ul);
us.ul = $('<ul>', { style: 'list-style:none' }).append($('<li>', { id: 'us_block_status' }).append([$('<b>').text(msg.blocks), us.loading_blocked, ' • ', $('<b>').text(msg.thxGvng), msg.thxRd, ': ', thxRd, ' / ', msg.thxGn, ': ', thxGn, $('<a>', { style: 'float:right;' + us.styleMissingData, title: 'Purge data', text: 'purge', click: us.removeDataStore })]));
statusBox.append(us.ul);
$('#firstHeading').after(statusBox);
us.statusBox = statusBox;
},
removeDataStore: function (e) {
var key = project + us.user, db = window.indexedDB;
e.preventDefault();
if (!db || !us.dbVersion) return mw.cookie.set(us.name + us.user, null);
var request = db.open(us.name, us.dbVersion + 1);
request.onupgradeneeded = function () { db = this.result; if (db.objectStoreNames.contains(key)) db.deleteObjectStore(key); };
request.onsuccess = function () { db = this.result; us.dbVersion = db.version; db.close(); location.reload(); };
},
setCookie: function () {
var domain = (mw.config.get('wgNoticeProject') === 'wikipedia') ? 'wikipedia.org' : '';
var name = us.name + us.user;
var _saveCookie = function (k, d) { mw.cookie.set(name, JSON.stringify(d), { expires: 604800, domain: domain }); };
if (!us.actions[0] && JSON && data.editcount) {
if (us.cookie.length) window.clearTimeout(us.cookie.shift());
var saveData = window.indexedDB ? function (n, JSd, v) {
var key = project + us.user, db = window.indexedDB, request = db.open(us.name, v);
request.onupgradeneeded = function () {
db = this.result;
if (!db.objectStoreNames.contains(key)) {
var store = db.createObjectStore(key, { keyPath: 'name' });
store.createIndex('data', 'data', { unique: false });
}
};
request.onsuccess = function () {
db = this.result; us.dbVersion = db.version;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readwrite').objectStore(key);
store.put({ name: key, data: JSd });
} else { saveData(n, JSd, us.dbVersion + 1); }
};
request.onerror = function() { _saveCookie(n, JSd); };
} : _saveCookie;
us.cookie.push(setTimeout(function () {
data.timestamp = new Date().valueOf();
saveData('', data);
}, 500));
}
},
doRequest: function () {
var action = us.actions.shift();
if (action) us.ajaxRequest(action.params, action.func);
if (us.actions.length) window.setTimeout(function () { us.doRequest(); }, 100);
},
getDateDiff: function (now, date) {
var dStr = [];
if (now > date) {
var years = now.getFullYear() - date.getFullYear();
var months = now.getMonth() - date.getMonth();
var days = now.getDate() - date.getDate();
// Korekce dnů a měsíců
if (days < 0) {
months--;
// Získáme počet dní v předchozím měsíci
var prevMonth = new Date(now.getFullYear(), now.getMonth(), 0).getDate();
days += prevMonth;
}
// Korekce měsíců a roků
if (months < 0) {
years--;
months += 12;
}
// Návrat slovních označení pro dnešek a včerejšek
if (years === 0 && months === 0 && days === 0) return msg.today || 'today';
if (years === 0 && months === 0 && days === 1) return msg.yesterday || 'yesterday';
var units = [years, months, days];
var labelSingle = [msg.date[0], msg.date[1], msg.date[2]];
var labelPlural = [msg.dates[0], msg.dates[1], msg.dates[2]];
for (var i = 0; i < 3; ++i) {
if (units[i] > 0) {
dStr.push(units[i] + ' ' + ((units[i] > 1) ? labelPlural[i] : labelSingle[i]));
}
}
}
var mainDiff = (dStr.length) ? dStr.join(', ').replace(/(.*),([^,]*)/, msg.and).replace(/(.*)/, msg.ago) : (msg.today || 'today');
return mainDiff;
},
getDateFromTimestamp: function (t) { if (!t) return false; t = new Date(t); return isNaN(t.valueOf()) ? false : t; },
writeRegistration: function (aw) {
if (aw) {
if (aw instanceof Object) {
if (aw.query && aw.query.logevents.length) aw = aw.query.logevents[0].timestamp;
}
if (typeof aw === 'string') {
us.loading_registration.replaceWith(us.formatDate(aw) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(aw)) + ')' }));
data.registration = aw;
return us.setCookie();
}
}
us.loading_registration.replaceWith($('<span>', { style: us.styleMissingData + us.styleLoading, text: msg.nodb }));
},
writeFirstEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs.length) return;
var date = data.firstedit = aw.query.usercontribs[0].timestamp;
$('#us_reg_date').append($('<li>').append([$('<b>', { text: '• ' + msg.fiedit }), us.formatDate(date) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(date)) + ')' })]));
},
writeLastEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs || !aw.query.usercontribs.length) return us.us_last_edit_loading.replaceWith($('<span>', { style: us.styleMissingData, text: msg.noedit }));
var date = data.lastedit = aw.query.usercontribs[0].timestamp;
// Vytvoření textu s odkazem
var diffText = us.getDateDiff(us.now, us.getDateFromTimestamp(date));
var diffNode = $('<a>', { href: mw.util.getUrl('Special:Contributions/' + us.user), text: diffText });
us.us_last_edit_loading.replaceWith(diffNode, ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.formatDate(date) + ')' }));
data.timediff = mw.now() - us.now;
$('#us_editcount').append($('<span>', { style: 'float:right;' + us.styleMissingData, text: msg.curtimeDiff }).append($('<b>', { text: Math.round(data.timediff / 1000) })));
us.setCookie();
},
writeGlobalGroup: function (aw) {
aw = aw.query.globaluserinfo;
var groups = data.glGrp = aw.groups;
// Pokud lokální registrace chybí, použijeme tu z globálního infa
if (!data.registration && aw.registration) {
us.writeRegistration(aw.registration);
}
if (groups && groups.length) {
us.us_global_group_loading.replaceWith(us.getLocalNames(groups, 'GlobalGroupPermissions'));
us.us_global_group.css('display', 'block');
// Fetch former global rights
us.fetchFormerRights(true);
}
if (aw.locked !== undefined) {
data.locked = 1;
mw.loader.using('mediawiki.ForeignApi').done(function () {
var fApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php');
fApi.get({
action: 'query',
list: 'logevents',
leprop: 'user|timestamp|comment',
letype: 'globalauth',
letitle: 'User:' + us.user,
lelimit: '1'
}).done(us.writeGlobalBlock);
});
}
us.setCookie();
},
writeGlobalBlock: function (aw) {
if (!aw.query || !aw.query.logevents.length) return;
aw = aw.query.logevents[0];
$('#us_block_status_span').replaceWith($('<a>', { style: us.styleBlocked, id: 'us_block_status_span', href: '//meta.wikimedia.org/w/index.php?title=Special:Log/&type=globalauth&user=&page=User%3A' + us.user + '%40global', text: 'locked (global)' }).append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) })));
$('#us_block_status').parent().append([$('<li>').append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment)]), $('<li>').append([$('<b>').text('• ' + msg.blocker), $('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user })])]);
},
writeBlockDetail: function (aw) {
var duration = 'infinite', expiry = '';
if (aw.query && aw.query.logevents.length) {
aw = aw.query.logevents[0];
if (aw && aw.params) {
duration = aw.params.duration;
expiry = /in(de)?finite/.test(duration) ? msg.never : us.formatDate(aw.params.expiry);
}
$('#us_block_status_span').text(msg.block + ' (' + duration + ')').append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) }));
if (expiry) $('#mw-spinner-us_block_time_loading').replaceWith(expiry); else $('#us_block_time').remove();
$('#mw-spinner-us_block_reason_loading').replaceWith(us.parseComment(aw.comment, aw.commenthidden));
if (aw.user) $('#mw-spinner-us_blocker_loading').replaceWith($('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user }));
else $('#mw-spinner-us_blocker_loading').replaceWith(msg.nodb);
}
},
writePatrolCount: function (aw) {
if (aw instanceof Object) {
if (!aw.query || !aw.query.logevents) return;
us.patrols += aw.query.logevents.length;
if (aw.continue) return us.getPatrolCount(aw.continue);
$.removeSpinner('pat');
aw = us.patrols;
}
us.us_patrolcount_loading.replaceWith($('<a>', { href: '/w/index.php?title=Special:Log&type=' + (us.review ? 'review&subtype=accept' : 'patrol&subtype=patrol') + '&user=' + us.user, text: aw }));
data.reviews = aw;
us.setCookie();
},
getPatrolCount: function (e) {
var params = '&list=logevents&leprop=&ledir=newer&lelimit=max&leuser=' + us.user + '&leaction=' + (us.review ? 'review/approve' : 'patrol/patrol');
if (e instanceof Object) {
if (e.target) { $(e.target).injectSpinner('pat'); us.patrols = 0; }
else if (e.lecontinue) params += '&lecontinue=' + e.lecontinue;
}
us.actions.push({ params: params, func: us.writePatrolCount });
us.doRequest();
},
parseComment: function (text, hidden) {
var comment = $('<span>', { 'class': 'comment' });
if (typeof text === 'undefined') return comment.append(hidden ? msg.noReason : 'undefined').css('color', hidden ? '#999' : 'red');
if (!text) return comment.append(msg.blockCmt).css('color', '#999');
var intLink = /(.*?)\[\[((.*?)\|)?(.*?)\]\](.*)/, suche = text, erg;
while ((erg = intLink.exec(suche)) !== null) {
var link = erg[3] || erg[4];
comment.append([erg[1], $('<a>', { href: mw.util.getUrl(link), title: link }).text(erg[4])]);
suche = erg[5];
}
return comment.append(suche);
},
formatDate: function (datum) {
if (!(datum instanceof Date)) datum = new Date(datum);
try {
return datum.toLocaleDateString(us.lang, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
} catch (e) { return datum.toLocaleString(); }
},
getStoredData: function () {
var key = project + us.user, db, request;
try {
request = indexedDB.open(us.name);
request.onsuccess = function () {
db = this.result;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readonly').objectStore(key);
store.transaction.oncomplete = function () { us.runDataStore(); db.close(); };
store.get(key).onsuccess = function () { if (this.result) data = this.result.data; else us.getCookie(); };
} else { us.getCookie(); }
};
request.onerror = function () { us.getCookie(); };
} catch (e) { us.getCookie(); }
},
getCookie: function () {
data = mw.cookie.get(us.name + us.user);
if (data) try { data = JSON.parse(data); } catch(e) { data = null; }
us.runDataStore();
},
initI18N: function (i18n) {
var chain = mw.language.getFallbackLanguageChain();
for (var i = chain.length - 1; i >= 0; i--) if (chain[i] in i18n) msg = i18n[chain[i]];
},
init: function () {
if (this.statusBox || (this.statusBox = $('#us_box'))[0]) this.statusBox.remove();
this.usprop = 'blockinfo|editcount|gender|groups|implicitgroups|registration';
this.self = mw.config.get('wgUserName') === this.user;
this.initI18N(i18n);
this.createBox();
if (!this.first) {
if (mw.cookie && JSON) { if (window.indexedDB) this.getStoredData(); else this.getCookie(); }
else this.run();
} else this.run();
},
runDataStore: function () {
if (data) {
if (typeof data === 'string') try { data = JSON.parse(data); } catch(e) { data = {}; }
var q = { query: { users: [data] } };
if (data.timestamp) this.now = mw.now() - (data.timediff || 0);
if (data.timestamp && Math.abs((mw.now() - data.timestamp - (data.timediff || 0)) / 1000) < 90) return this.writeCommonInfo(q);
if (data.editcount) this.usprop = 'editcount';
}
data = data || {};
this.run();
},
run: function () {
if (this.self) {
return this.writeCommonInfo({ query: { users: [{ editcount: mw.config.get('wgUserEditCount'), registration: mw.config.get('wgUserRegistration'), groups: mw.config.get('wgUserGroups'), blockreason: '' }] } });
}
us.ajaxRequest({ curtimestamp: 1, list: 'users', ususers: us.user, usprop: us.usprop }, us.writeCommonInfo);
}
};
if ([2, 3].indexOf(mw.config.get('wgNamespaceNumber')) !== -1 && us.user.indexOf('/') === -1 && (!mw.config.get('wgArticleId') || mw.config.get('wgAction') === 'view')) {
$.when(mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.language', 'mediawiki.cookie', 'jquery.spinner', 'mediawiki.ForeignApi']), $.ready).then(function () {
us.api = mw.util.wikiScript('api') + '?action=query&format=json';
us.ajaxRequest('&meta=allmessages&amenableparser=1&amincludelocal=1&refix=group-', us.getGroupNames);
if (!mw.libs.viewerInfo) mw.loader.load('//meta.wikimedia.org/w/index.php?title=User:Perhelion/viewerInfo.js&action=raw&ctype=text/javascript');
});
}
}(jQuery, mediaWiki));
di3ka985m65cjw8eibeu2fr0gzg78za
User:Nardog/sandbox2.js
2
118608
739895
739835
2026-04-30T23:48:56Z
Nardog
40946
739895
javascript
text/javascript
(async function listTools() {
let pageAction = mw.config.get('wgAction');
let isView = pageAction === 'view';
let isEdit = ['edit', 'submit'].includes(pageAction);
if (!isView && !isEdit) return;
let pageType = mw.config.get('wgCanonicalSpecialPageName') ||
mw.config.get('wgNamespaceNumber');
if (isView && !pageType && !mw.config.exists('wgRedirectedFrom') &&
!mw.config.get('wgIsRedirect') &&
!mw.config.get('wgPageName').includes('/')
) {
return;
}
await mw.loader.using([
'mediawiki.util', 'mediawiki.Title', 'mediawiki.api',
'mediawiki.interface.helpers.styles'
]);
mw.loader.addStyleTag(`.listtools:not(#mw-content-subtitle .listtools) {
font-size: 85%;
}
.listtools, .listtools a {
font-weight: normal !important;
font-style: normal;
}
.mw-datatable .listtools {
display: block;
}
.listtools + .mw-whatlinkshere-tools,
#watchlist-edit-form .listtools ~ .mw-changeslist-links,
.mw-special-DisambiguationPageLinks .listtools + a {
display: none;
}`);
let messages = Object.assign({
watched: 'Added "$1" to your watchlist',
watchFail: `Couldn't watch "$1"`,
unwatchFail: `Couldn't unwatch "$1"`
}, window.listtoolsMessages);
let getMsg = (key, ...args) => (
Object.hasOwn(messages, key) ? mw.format(messages[key], ...args) : key
);
let notif;
let watchHandler = async function (e) {
e.preventDefault();
let $link = $(this);
let $wrapper = $link.parent();
$link.detach();
let params = new URLSearchParams(this.search);
let action = params.get('action');
$wrapper.text(getMsg(action + 'ing'));
let pn = params.get('title').replaceAll('_', ' ');
let promise = new mw.Api()[action](pn);
if (notif) {
notif.close();
notif = null;
}
try {
let result = await promise;
if (!result || !result[action + 'ed']) throw '';
let newAction = action === 'watch' ? 'unwatch' : 'watch';
params.set('action', newAction);
$link.add(`.listtools-watch > a[href="${this.pathname + this.search}"]`)
.attr('href', this.pathname + '?' + params)
.text(getMsg(newAction));
if (action !== 'watch') return;
let require = await mw.loader.using([
'mediawiki.notification', 'mediawiki.watchstar.widgets'
]);
notif = await mw.notify(
new (require('mediawiki.watchstar.widgets'))('watch', pn, null, $.noop, {
message: getMsg('watched', pn)
}).$element,
{ tag: 'listtools' }
);
} catch {
notif = await mw.notify(getMsg(action + 'Fail', pn), {
tag: 'listtools',
type: 'error'
});
} finally {
$wrapper.html($link);
}
};
let extGetMain = function () {
return this.title;
};
let re = new RegExp(`(?:\\?title=|${
mw.util.escapeRegExp(mw.format(mw.config.get('wgArticlePath'), ''))
})([^#&?]+)`);
let processed = new WeakSet();
let processLinks = ($links, module, titles) => {
let isBatch = !!titles;
titles = titles || new Set();
$links.each(function (i) {
if (processed.has(this)) return;
let $link = $links.eq(i);
let pn;
if (module.useText) {
pn = $link.text();
} else {
let match = $link.attr('href')?.match(re);
if (!match) return;
pn = decodeURIComponent(match[1]);
}
let t = mw.Title.newFromText(pn);
if (!t) return;
if (module.titlesOnly) {
let text = $link.text();
if (text !== pn.replaceAll('_', ' ') &&
(text !== t.getMainText() || t.namespace === 2)
) {
return;
}
}
if ($link.is('.external, .extiw')) {
Object.assign(t, {
getMain: extGetMain,
host: this.host,
namespace: 0,
title: pn
});
} else {
if (t.namespace < 0) return;
if ($link.hasClass('new')) {
t.missing = true;
}
titles.add(t.getSubjectPage().toText());
}
let $tools = $('<span>').addClass('listtools mw-changeslist-links')
.data('listtools', t);
tools.forEach(tool => {
addTool($tools, tool);
});
if ($link.is(':is(del, bdi) > :only-child')) {
if (module.position === 'end') {
$link.parent().parent().append(' ', $tools);
} else {
$link.parent().after(' ', $tools);
}
} else if (module.position === 'end') {
$link.parent().append(' ', $tools);
} else {
$link.after(' ', $tools);
}
if (module.post) {
module.post($tools);
}
processed.add(this);
});
if (!isBatch) {
getWatched(titles);
}
};
let tools = [
{
name: 'edit',
url: t => t.getUrl({ action: 'edit' })
},
{
name: 'hist',
url: t => !t.missing && t.getUrl({ action: 'history' })
},
{
name: 'links',
url: t => mw.util.getUrl('Special:WhatLinksHere/' + t)
},
{
name: 'watch',
url: t => !t.host && t.getSubjectPage().getUrl({ action: 'watch' }),
callback: watchHandler
}
];
let addTool = ($tools, tool, escapedName) => {
let t = $tools.data('listtools');
let $duplicate = escapedName &&
$tools.children('.listtools-' + escapedName);
let url = tool.url;
if (typeof url === 'function') {
url = url(t);
if (!url) {
$duplicate?.remove();
return;
}
}
let $link = $('<a>').attr('href', url).text(getMsg(tool.name));
if (t.host) {
$link.prop('host', t.host);
}
if (tool.callback) {
$link.on('click', tool.callback);
}
let $wrapper = $('<span>').addClass('listtools-' + tool.name)
.append($link);
let $next = tool.next && $tools.children('.listtools-' + tool.next);
if ($next?.length) {
$duplicate?.remove();
$next.before($wrapper);
} else if ($duplicate?.length) {
$duplicate.replaceWith($wrapper);
} else {
$tools.append($wrapper);
}
};
let extend = tool => {
if (tool.label && !Object.hasOwn(messages, tool.label)) {
messages[tool.name] = tool.label;
}
if (tool.next) {
tool.next = $.escapeSelector(tool.next);
}
let existingTool = tools.find(t => t.name === tool.name);
if (existingTool) {
Object.assign(existingTool, tool);
} else {
tools.push(tool);
}
let escapedName = existingTool && $.escapeSelector(tool.name);
let $allTools = $('.listtools');
$allTools.each(function (i) {
addTool($allTools.eq(i), tool, escapedName);
});
};
let getWatched = async titles => {
if (!Array.isArray(titles)) {
titles = [...titles].slice(0, 500);
}
if (!titles.length) return;
(await new mw.Api().post({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages.forEach(page => {
if (!page.watched) return;
$(`.listtools-watch > a[href="${mw.util.getUrl(page.title, { action: 'watch' })}"]`)
.attr('href', mw.util.getUrl(page.title, { action: 'unwatch' }))
.text(getMsg('unwatch'));
});
getWatched(titles.slice(50));
};
mw.hook('listtools.ready').fire(extend);
let catTreeCallback = (records, observer) => {
let $links = $(records[0].target).find('.CategoryTreeItem > bdi > a');
if ($links.length) {
observer.takeRecords();
observer.disconnect();
processLinks($links, catTreeModule);
}
};
let catTreeModule = {
selector: '.CategoryTreeItem > bdi > a',
types: [14, 'CategoryTree'],
position: 'end',
post: $tools => {
$tools.parent().next('.CategoryTreeChildren').each(function () {
new MutationObserver(catTreeCallback)
.observe(this, { childList: true });
});
}
};
let modules = [
{
selector: '#mw-pages li > a, #mw-pages li > span > a',
types: [14]
},
catTreeModule,
{
selector: '#mw-imagepage-section-linkstoimage a, #mw-imagepage-section-globalusage a',
types: [6]
},
{
selector: '#mw-globalusage-result a',
types: ['GlobalUsage']
},
{
selector: '.mw-search-result-heading > a, .searchalttitle > a.mw-redirect, .iw-result__title > a, .mw-search-exists a',
types: ['Search']
},
{
selector: '.mw-search-createlink a',
types: ['Search'],
titlesOnly: true
},
{
selector: '#watchlist-edit-form .cdx-table td > label > a',
types: ['EditWatchlist']
},
{
selector: '.plainlinks > li > a',
types: ['AbuseLog'],
titlesOnly: true
},
{
selector: '#mw-allmessagestable td:first-child > a:first-child:not(.new)',
types: ['Allmessages'],
position: 'end'
},
{
selector: '.mw-spcontent li a',
types: ['DisambiguationPageLinks', 'Listredirects'],
titlesOnly: true
},
{
selector: 'li > a:first-child',
types: ['FileDuplicateSearch']
},
{
selector: '.TablePager_col_title > a:first-child, .TablePager_col_template > a',
types: ['LintErrors'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: 'form > ul > li > a',
types: ['Nuke'],
position: 'end',
titlesOnly: true
},
{
selector: '.page-assessments a',
types: ['PageAssessments'],
titlesOnly: true
},
{
selector: '.TablePager_col_pr_page > a',
types: ['Protectedpages'],
position: 'end'
},
{
selector: '#mw-content-text > ul a',
types: ['Protectedtitles'],
position: 'end'
},
{
selector: '.mw-fr-pending-changes-page-title',
types: ['PendingChanges'],
post: $tools => {
$tools.parent().contents().slice(3).remove();
}
},
{
selector: '#mw-content-text > ul a:first-child',
types: ['StablePages'],
position: 'end'
},
{
selector: '.TablePager_col__page a',
types: ['TopicSubscriptions']
},
{
selector: '.undeleteResult > a',
types: ['Undelete'],
position: 'end',
useText: true
},
{
selector: '.TablePager_col_img_name > a:first-child',
// types: ['Listfiles'],
position: 'end'
},
{
selector: '.mw-newpages-pagename',
post: $tools => {
let $nodes = $tools.parent().contents();
$nodes.slice(
$nodes.index($tools) + 1,
$nodes.index($nodes.filter('.mw-newpages-length'))
).replaceWith(' ');
}
},
{
selector: '#mw-whatlinkshere-list li > bdi > a'
},
{
selector: '.mw-changeslist-log-entry > a:not(.mw-changeslist-log-gblblock a, .mw-changeslist-log-globalauth a)',
titlesOnly: true
},
{
selector: '.mw-logevent-loglines > li:not(.mw-logline-gblblock, .mw-logline-globalauth) > a',
types: ['Log'],
titlesOnly: true
},
{
selector: '#mw-diff-otitle1 > strong > a, #mw-diff-ntitle1 > strong > a',
types: ['ComparePages'],
position: 'end'
},
{
selector: '#movepage-oldlink, #movepage-newlink',
types: ['Movepage']
},
{
selector: '.mw-undelete-revision a:not(.mw-userlink, .mw-usertoollinks > a)',
types: ['Undelete'],
useText: true
},
{
selector: '.galleryfilename, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-changeslist-line-inner-comment > .comment > a, ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize .mw-enhanced-rc-nested > .comment > a'
},
{
selector: '.mw-spcontent li a',
position: 'end',
titlesOnly: true
}
];
if (isEdit) {
let post = $tools => {
if (!$tools[0].closest('.templatesUsed')) return;
$tools.parent().contents().last().each(function () {
this.textContent = this.textContent.slice(1);
}).end().slice(-3, -1).remove();
};
let callback = mw.util.debounce(() => {
processLinks(
$('.mw-editfooter-list a, #wikiPreview > .previewnote a'),
{ titlesOnly: true, post }
);
}, 500);
mw.hook('wikipage.editform').add($form => {
callback();
$form.find('.templatesUsed').each(function () {
if (processed.has(this)) return;
processed.add(this);
new MutationObserver(callback)
.observe(this, { childList: true, subtree: true });
});
});
} else if (typeof pageType === 'number') {
$(() => {
processLinks($('.subpages a, .mw-redirectedfrom a, .redirectText a'), {});
});
}
mw.hook('wikipage.content').add($content => {
let titles = new Set();
let $links = $content.find('a');
modules.forEach(module => {
if (module.types && !module.types.includes(pageType)) return;
processLinks($links.filter(module.selector), module, titles);
});
getWatched(titles);
});
}());
mw.hook('listtools.ready').add(extend => {
// extend({
// name: 'talk',
// url: t => !t.isTalkPage() && t.canHaveTalkPage() && t.getTalkPage().getUrl(),
// next: 'hist'
// });
extend({
name: 'subject',
url: t => t.isTalkPage() && t.getSubjectPage().getUrl(),
next: 'hist'
});
extend({
name: 'last',
url: t => !t.missing && t.getUrl({ diff: 'cur', diffonly: 1 }),
next: 'links'
});
// extend({
// name: 'purge',
// url: t => t.getUrl({ action: 'purge' }),
// next: 'watch',
// callback: function (e) {
// e.preventDefault();
// let $link = $(this);
// let $wrapper = $link.parent();
// $link.detach();
// $wrapper.text('purging');
// let pn = $wrapper.closest('.listtools').data('listtools').toText();
// new mw.Api().post({
// action: 'purge',
// forcelinkupdate: 1,
// titles: pn,
// formatversion: 2
// }).then(response => {
// if (response.purge[0].purged) {
// mw.notify(`Purged "${pn}"'`);
// }
// }).always(() => {
// $wrapper.html($link);
// });
// }
// });
extend({
name: 'copy',
url: '#',
callback: function (e) {
e.preventDefault();
let text = $(this).closest('.listtools').data('listtools').toText();
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch (err) {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
}
});
});
(mw.config.get('wgNamespaceNumber') || mw.config.get('wgAction') !== 'view') &&
mw.config.get('wgCanonicalSpecialPageName') !== 'GlobalContributions' &&
(function consecudiff() {
mw.loader.addStyleTag('.consecudiff::before{content:" ["} .consecudiff::after{content:"]"} .consecudiff-top::before{content:" ⟨"} .consecudiff-top::after{content:"⟩"}');
let isHist = mw.config.get('wgAction') === 'history';
class Consecudiff {
constructor(lis, isContribs) {
this.isContribs = isContribs;
this.isEnhanced = !isHist && !isContribs &&
lis[0].classList.contains('mw-enhanced-rc');
this.threshold = isContribs ? window.consecudiffContribsThreshold || 120
: isHist ? window.consecudiffHistThreshold || 720
: window.consecudiffThreshold || 720;
this.strictMode = !isContribs &&
!!window.consecudiffDetectInterruptions;
this.diffSelector = isHist
? 'a.mw-history-histlinks-previous'
: '.mw-changeslist-diff';
this.permaSelector = this.isEnhanced && '.mw-enhanced-rc-time > a' ||
(isHist || isContribs) && 'a.mw-changeslist-date';
this.hybridSelector = this.diffSelector;
if (this.permaSelector) {
this.hybridSelector += ', ' + this.permaSelector;
}
this.topClass = isContribs
? 'mw-contributions-current'
: 'mw-changeslist-last';
let dependencies = ['mediawiki.util'];
if ((isHist || isContribs) && mw.config.get('wgUserLanguage') !== 'en') {
dependencies.push('mediawiki.language.months');
}
mw.loader.using(dependencies, () => {
let chunks;
if (isHist) {
chunks = this.chunkByUser(lis);
} else {
chunks = [];
this.groupByTitle(lis).forEach(group => {
chunks.push(...this.chunkByUser(group));
});
}
let subchunks = [];
chunks.forEach(chunk => {
subchunks.push(...this.divideByDate(chunk));
});
let linkPairs = [];
subchunks.forEach(subchunk => {
linkPairs.push(...this.makeLinks(subchunk));
});
linkPairs.forEach(([$span, parent]) => {
$span.appendTo(parent);
});
});
}
groupByTitle(lis) {
let selector = this.isContribs
? '.mw-contributions-title'
: '.mw-changeslist-title';
let lisByTitle = {};
lis.forEach(li => {
let link = (this.isEnhanced ? li.closest('table') : li)
.querySelector(selector);
if (!link) return;
let title = link.textContent;
if (!lisByTitle.hasOwnProperty(title)) {
lisByTitle[title] = [];
}
lisByTitle[title].push(li);
});
return Object.values(lisByTitle).filter(group => group.length > 1);
}
chunkByUser(lis) {
if (this.isSingleContribs) {
return [lis];
}
let chunks = [], lastSplitAt = 0, prevUser;
this.isSingleContribs = lis.some((li, i) => {
let link = li.querySelector('.mw-userlink');
if (!link && this.isContribs) {
return true;
}
let user = link && link.textContent;
if (!link || i && user !== prevUser) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevUser = user;
});
if (this.isSingleContribs) {
return [lis];
}
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
divideByDate(lis) {
let chunks = [], lastSplitAt = 0, prevDate;
lis.forEach((li, i) => {
let date;
if (isHist || this.isContribs) {
date = this.parseDate(
li.querySelector('.mw-changeslist-date').textContent
);
} else {
date = Date.parse(
li.dataset.mwTs.replace(/(....)(..)(..)(..)(..)(..)/, '$1-$2-$3T$4:$5:$6Z')
);
}
if (date) {
date = date / 60000;
}
if (i && prevDate - date > this.threshold) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
prevDate = date;
if (!this.strictMode || lastSplitAt === i) return;
let prevDiff = lis[i - 1].querySelector(this.diffSelector);
if (prevDiff) {
let prevNext = mw.util.getParamValue('oldid', prevDiff.search);
if (prevNext !== li.dataset.mwRevid) {
chunks.push(lis.slice(lastSplitAt, i));
lastSplitAt = i;
}
}
});
chunks.push(lis.slice(lastSplitAt));
return chunks.filter(chunk => chunk.length > 1);
}
makeLinks(lis) {
let count = lis.length;
let firstPerma;
let start = lis.findIndex(li => (
firstPerma = li.querySelector(this.hybridSelector)
));
if (start === -1 || count - start < 2) return [];
let end, lastDiff;
for (let i = count - 1; i > start; i--) {
if (!isHist && !this.isContribs) {
lastDiff = lis[i].querySelector(this.diffSelector);
if (lastDiff ||
lis[i].classList.contains('mw-changeslist-src-mw-new')
) {
end = i + 1;
break;
}
}
if (this.permaSelector && lis[i].querySelector(this.permaSelector)) {
end = i + 1;
break;
}
}
if (!end) return [];
count = end - start;
let params = { diff: lis[start].dataset.mwRevid };
if (lastDiff) {
params.oldid = mw.util.getParamValue('oldid', lastDiff.search);
} else {
params.oldid = lis[end - 1].dataset.mwRevid;
if (isHist && lis[end - 1].querySelector(this.diffSelector) ||
this.isContribs && !lis[end - 1].querySelector('.newpage')
) {
params.direction = 'prev';
}
}
let title = !isHist && mw.util.getParamValue('title', firstPerma.search);
let url = mw.util.getUrl(title, params);
let classes = 'consecudiff';
if (!isHist && lis[start].classList.contains(this.topClass)) {
classes += ' consecudiff-top';
}
return lis.slice(start, end).map((li, i) => [
$('<span>').addClass(classes).append(
$('<a>')
.attr('href', url)
.text(this.convertNumber(count - i + '/' + count))
),
this.isEnhanced
? li.tagName === 'TR'
? li.lastElementChild
: li.querySelector('.mw-changeslist-line-inner')
: li
]);
}
parseDate(s) {
let date = Date.parse(s);
if (date) {
return date;
}
if (s.includes(',')) date = Date.parse(s.replace(',', ''));
if (date) {
return date;
}
if (mw.loader.getState('mediawiki.language.months') !== 'ready') return;
s = s.replace(/\D/g, c => {
let n = mw.language.convertNumber(c, true);
return Number.isNaN(n) ? c : n;
});
let h, m;
s = s.replace(/(\d\d?)[.:h](\d\d?)/, ($0, $1, $2) => {
h = $1;
m = $2;
return ' ';
});
if (!h) return;
let y, dateFirst;
s = s.replace(/^(.*?)(\d{4})(?!\d)/, ($0, $1, $2) => {
y = $2;
dateFirst = /\d/.test($1);
return $1 + ' ';
});
if (!y) return;
let mo, d;
if (dateFirst) {
[d, s] = this.getDate(s);
if (!d) return;
[mo, s] = this.getMonth(s);
if (mo === -1) return;
} else {
[mo, s] = this.getMonth(s);
if (mo === -1) return;
[d, s] = this.getDate(s);
if (!d) return;
}
return new Date(y, mo, d, h, m).getTime();
}
getMonth(s) {
if (!this.months) {
this.months = mw.language.months.abbrev
.concat(mw.language.months.names, mw.language.months.genitive)
.reverse();
}
let mo = this.months.findIndex(mn => {
let temp = s.replace(mn, ' ');
if (temp !== s) {
s = temp;
return true;
}
});
if (mo === -1) {
let [numeric, temp] = this.getDate(s);
numeric = parseInt(numeric);
if (numeric > 0 && numeric < 13) {
mo = numeric - 1;
s = temp;
}
} else {
mo = 11 - mo % 12;
}
return [mo, s];
}
getDate(s) {
let d;
s = s.replace(/(^|\D)(\d\d?)(?!\d)/, ($0, $1, $2) => {
d = $2;
return $1 + ' ';
});
return [d, s];
}
convertNumber(num) {
try {
return mw.language.convertNumber(num);
} catch (e) {
return num;
}
}
}
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-body').each(function () {
let lis = this.querySelectorAll('.mw-contributions-list > li');
if (lis.length > 1) {
new Consecudiff([...lis], !isHist);
}
});
if (isHist) return;
let $lists = $content.filter('.mw-changeslist');
if (!$lists.length) {
$lists = $content.find('.mw-changeslist');
}
$lists.each(function () {
let lis = this.querySelectorAll('.mw-changeslist-edit:not(.mw-changeslist-src-mw-categorize)[data-mw-revid]');
if (lis.length > 1) {
new Consecudiff([...lis]);
}
});
});
}());
if (mw.config.get('wgNamespaceNumber') === 14 && (
mw.config.get('wgAction') === 'view' || !mw.config.get('wgArticleId')
)) {
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox8.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.DateFormatter',
'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.UserInputWidget', 'mediawiki.widgets.datetime',
'oojs-ui.styles.icons-interactions', 'oojs-ui.styles.icons-movement',
'mediawiki.interface.helpers.styles', 'user.options'
]);
}
$(function moveHistory() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Move history', 't-movehistory').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.moveHistoryDialog) {
window.moveHistoryDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox5.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.Title', 'mediawiki.DateFormatter',
'oojs-ui-windows', 'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.DateInputWidget', 'oojs-ui.styles.icons-interactions',
'mediawiki.interface.helpers.styles'
]);
});
});
});
$(function sectionSearch() {
if (!document.getElementById('p-tb')) return;
mw.loader.using('mediawiki.util', () => {
let clicked;
mw.util.addPortletLink('p-tb', '#', 'Section search', 't-sectionsearch').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.sectionSearchDialog) {
window.sectionSearchDialog.open();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox7.js&action=raw&ctype=text/javascript');
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'oojs-ui-core', 'oojs-ui-windows',
'mediawiki.widgets', 'mediawiki.widgets.NamespacesMultiselectWidget'
]);
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'CentralAuth' &&
mw.loader.using('jquery.tablesorter', function sortCentralAuthByEditCount() {
mw.hook('wikipage.content').add($content => {
let $table = $content.find('.mw-centralauth-wikislist').has('td');
if (!$table.length) return;
$table.tablesorter().data('tablesorter').sort([{ 4: 'desc' }, { 1: 'asc' }]);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
[10, 828].includes(mw.config.get('wgNamespaceNumber')) &&
!mw.config.get('wgTitle').endsWith('/doc') &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/AutoTestcases.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/TemplatePreviewGuard.js&action=raw&ctype=text/javascript', 's');
// ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
// $(function templatePreviewGuard() {
// let button = document.querySelector('input[name="wpTemplateSandboxPreview"]');
// if (!button) return;
// let proceed;
// button.addEventListener('click', e => {
// if (proceed) {
// proceed = false;
// return;
// }
// e.preventDefault();
// e.stopPropagation();
// let formData = new FormData(button.form);
// let page = formData.get('wpTemplateSandboxPage');
// let temp = formData.get('wpTemplateSandboxTemplate');
// if (!page || !temp) return;
// mw.loader.using('mediawiki.api').then(() => (
// new mw.Api().get({
// action: 'query',
// titles: page,
// prop: 'templates',
// tltemplates: temp,
// formatversion: 2
// })
// )).always(response => {
// if (((((response || {}).query || {}).pages || [])[0] || {}).templates ||
// confirm(`"${page}" doesn't appear to transclude "${temp}". Continue?`)
// ) {
// proceed = true;
// button.click();
// }
// });
// }, true);
// if (!mw.config.get('wgArticleId')) return;
// let widgetEl = document.querySelector('#wpTemplateSandboxPage.oo-ui-widget');
// if (!widgetEl) return;
// let pn = mw.config.get('wgPageName').replace(/_/g, ' ');
// mw.loader.using(['mediawiki.api', 'oojs-ui-core']).then(() => (
// new mw.Api().get({
// action: 'query',
// titles: pn,
// prop: 'transcludedin',
// tiprop: 'title',
// tilimit: 'max',
// formatversion: 2
// })
// )).then(response => {
// if (!response.batchcomplete) return;
// let pages = response.query.pages[0].transcludedin
// .filter(o => o.title !== pn);
// if (!pages.length) return;
// let widget = OO.ui.infuse(widgetEl);
// if (pages.length === 1) {
// widget.setValue(pages[0].title);
// return;
// }
// widget.$element.replaceWith(
// new OO.ui.ComboBoxInputWidget({
// id: 'wpTemplateSandboxPage',
// maxlength: widget.$input.prop('maxLength'),
// name: widget.$input.prop('name'),
// options: pages
// .sort((a, b) => a.ns - b.ns || -(a.title < b.title))
// .map(o => ({ data: o.title })),
// placeholder: widget.$input.prop('placeholder'),
// tabIndex: widget.getTabIndex(),
// value: widget.getValue()
// }).on('enter', e => {
// e.preventDefault();
// button.click();
// }).$element
// );
// });
// });
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(async () => {
let form = document.getElementById('editform');
if (!form) return;
let formData = new FormData(form);
let section = formData.get('wpSection');
if (section === 'new') return;
let widget = document.getElementById('wpSummaryWidget');
if (!widget) return;
let isOld = formData.get('altBaseRevId') > 0 ||
(formData.get('baseRevId') || formData.get('parentRevId')) !== formData.get('editRevId');
await mw.loader.using([
'jquery.textSelection', 'mediawiki.util', 'mediawiki.api', 'oojs-ui-core',
'oojs-ui.styles.icons-editing-core'
]);
let $textarea = $('#wpTextbox1');
let input = OO.ui.infuse(widget);
let button = new OO.ui.ButtonWidget({
framed: false,
icon: 'undo',
classes: ['autosectionlink-button'],
invisibleLabel: true,
label: 'Restore previous section link'
}).toggle().on('click', () => {
let cache = button.getData();
input.setValue(input.getValue().replace(
/^(\/\*.*?\*\/)?\s*/,
cache[0] ? '/* ' + cache[0] + ' */ ' : ''
));
updatePreview(cache[0]);
cache.reverse();
}).on('toggle', () => {
input.$input.css('width', `calc(100% - ${button.$element.width()}px)`);
});
input.$input.after(button.$element);
let update = mw.util.debounce($diff => {
let lines = $textarea.textSelection('getContents').trimEnd().split('\n');
let firstLineNum, lastLineNum;
if (isOld) {
let i;
$diff.find('td:last-child').each(function () {
if (this.classList.contains('diff-lineno')) {
i = this.textContent.replace(/\D+/g, '') - 1;
} else if (this.classList.contains('diff-context')) {
i++;
} else if (this.classList.contains('diff-addedline')) {
i++;
if (!firstLineNum) {
firstLineNum = i;
}
lastLineNum = i;
} else if (this.classList.contains('diff-empty')) {
if (!firstLineNum) {
firstLineNum = i === 0 ? 1 : i;
}
lastLineNum = i;
}
});
} else {
let origLines = $textarea.prop('defaultValue').trimEnd().split('\n');
firstLineNum = lines.findIndex((line, i) => line !== origLines[i]) + 1;
if (!firstLineNum) {
firstLineNum = lines.length < origLines.length
? lines.length
: 1;
}
lastLineNum = lines.length;
for (let i = 1, x = lastLineNum, y = origLines.length;
(section ? i < x : i <= x) && lines[x - i] === origLines[y - i];
i++
) {
lastLineNum--;
}
}
let modLines = lines.slice(0, lastLineNum || 0);
let re = /^(={1,6})\s*(.+?)\s*\1\s*(?:<!--.+-->\s*)?$/;
let lowest = 7;
modLines.slice(firstLineNum).forEach(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
lowest = match[1].length;
}
});
let head;
modLines.slice(0, firstLineNum).reverse().some(line => {
let match = line.match(re);
if (match?.[1].length < lowest) {
head = match[2];
return true;
}
});
if (head) {
head = head
.replace(/'''(.+?)'''|\[\[:?(?:[^|\]]+\|)?([^\]]+)\]\]|<\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nowiki|q|rb|ref|rp|rt|rtc|ruby|s|samp|small|span|strike|strong|sub|sup|templatestyles|time|translate|tt|u|var)(?:\s[^>]*)?>|<!--.*?-->|\[(?:https?:)?\/\/[^\s\[\]]+\s([^\]]+)\]/gi, '$1$2$3')
.replace(/''(.+?)''/g, '$1')
.trim();
} else if (modLines.length && lowest === 7 && section < 1 && (
section === '0' || lines.slice(modLines.length).some(line => re.test(line))
)) {
head = '';
}
let v = input.getValue();
let match = v.match(/^\/\*\s*(.+?)\s*\*\/\s*/);
let prev = match?.[1];
if (prev === head) return;
input.setValue((
head || head === '' ? '/* ' + head + ' */ ' : ''
) + (match ? v.slice(match[0].length) : v));
button.setData([prev, head]).toggle(true);
updatePreview(head);
}, 500);
let updatePreview = head => {
let $comment = $('.mw-summary-preview > .comment');
if (!$comment.length) return;
let url;
if (head) {
url = mw.util.getUrl() + '#' + head.replace(/[\s_]+/g, '_');
} else if (head === '') {
head = mw.messages.get('autocomment-top', '(top)');
url = mw.util.getUrl();
}
let paren = [...mw.messages.get('parentheses', '($1)')][0];
let $nodes = $comment.contents();
let $ac = $nodes.eq(1);
if ($nodes[0]?.textContent === paren && $ac.is('.autocomment:first-child')) {
if (head) {
$ac.children('a').attr('href', url).children('bdi').text(head);
} else {
if ($nodes[2]?.nodeType === 3) {
$nodes[2].textContent = $nodes[2].textContent.trimStart();
}
$ac.remove();
}
} else if (head) {
let rtl = document.body.classList.contains('sitedir-rtl');
$comment.prepend(
paren,
$('<span>').addClass('autocomment').append(
$('<a>').attr({
href: url,
title: mw.config.get('wgPageName').replaceAll('_', ' ')
}).text(rtl ? '←' : '→').append(
$('<bdi>').attr('dir', rtl ? 'rtl' : 'ltr').text(head)
),
mw.messages.get('colon-separator', ': ')
)
);
if ($nodes[0]?.nodeType === 3) {
let text = $nodes[0].textContent;
if (text.startsWith(paren)) {
text = text.slice(paren.length);
}
$nodes[0].textContent = ' ' + text;
}
}
};
if (isOld) {
mw.hook('wikipage.diff').add(update);
} else {
$textarea.on('input', update);
mw.hook('ext.CodeMirror.input').add(update);
update();
}
new mw.Api().loadMessagesIfMissing(['autocomment-top', 'colon-separator', 'parentheses']);
mw.loader.addStyleTag('.autosectionlink-button{position:absolute;top:0;right:0;margin:0}');
});
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(function copyRevId() {
let handler = function (e) {
e.preventDefault();
let text = this.closest('.diff td')?.querySelector('[data-mw-revid]')?.dataset.mwRevid ||
this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!text) return;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
document.execCommand('copy');
$input.remove();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('id')
)
);
});
}());
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
(() => {
let handler = async function (e) {
e.preventDefault();
let td = this.closest('.diff td');
let rev = td
? td.querySelector('[data-mw-revid]')?.dataset.mwRevid
: this.closest('[data-mw-revid]')?.dataset.mwRevid;
if (!rev) {
mw.notify(`Couldn't get the revision.`, {
tag: 'markasunseen',
type: 'error'
});
return;
}
let pn = td
? new URLSearchParams([...td.querySelectorAll('a')].pop()?.search).get('title')
: mw.config.get('wgPageName');
if (!pn) return;
await mw.loader.using('mediawiki.api');
let result = (await new mw.Api().postWithEditToken({
action: 'setnotificationtimestamp',
[td ? 'newerthanrevid' : 'torevid']: rev,
titles: pn,
formatversion: 2
})).setnotificationtimestamp?.[0];
if (Object.hasOwn(result, 'notificationtimestamp')) {
mw.notify(`Marked revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'success'
});
} else if (result?.notwatched) {
mw.notify('This page is not on your watchlist.', {
tag: 'markasunseen',
type: 'warn'
});
} else {
mw.notify(`Couldn't mark revisions ${td ? 'after' : 'since'} ${rev} as unseen.`, {
tag: 'markasunseen',
type: 'error'
});
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1').append(
' (',
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions after this one as unseen'
}).on('click', handler).text('unseen'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button',
title: 'Mark revisions since this one as unseen'
}).on('click', handler).text('unseen')
)
);
});
})();
(mw.config.get('wgNamespaceNumber') === -1 || mw.config.exists('wgDiffNewId') ||
mw.config.get('wgAction') === 'history') &&
((mw.config.get('wgNamespaceNumber') % 2 || mw.config.get('wgNamespaceNumber') === 4) ||
(mw.config.get('wgWikiID') === 'metawiki' && mw.config.get('wgPageContentModel') === 'wikitext')) &&
mw.loader.using(['mediawiki.util', 'mediawiki.Title'], function copyUnsig() {
let handler = function (e) {
e.preventDefault();
let parent = this.closest('li, td');
let ts = parent.textContent.match(/\d\d:\d\d, \d\d? [A-Z][a-z]+ \d{4}/)?.[0];
if (!ts) return;
let user = parent.querySelector('.mw-userlink').textContent;
if (mw.util.isIPv6Address(user)) {
user = user.toUpperCase();
}
let temp = mw.util.isIPAddress(user) ? 'unsigned IP' : 'unsigned';
let text = `{{subst:${temp}|${user}|${ts}}}`;
let $input = $('<input>').attr({
type: 'text',
readonly: '',
style: 'position:fixed;top:-100%'
}).val(text).appendTo(document.body);
$input[0].select();
let copied;
try {
copied = document.execCommand('copy');
} catch {}
$input.remove();
if (copied) {
mw.notify(`Copied "${text}"`);
} else {
mw.notify('Copy failed', { type: 'error' });
}
};
mw.hook('wikipage.diff').add($diff => {
$diff.find('#mw-diff-otitle1, #mw-diff-ntitle1').filter(function () {
if (mw.config.get('wgWikiID') === 'metawiki') {
return true;
}
let link = this.querySelector('strong > a') ||
this.parentElement.querySelector('#differences-prevlink, #differences-nextlink');
if (!link) return;
let t = mw.Title.newFromText(mw.util.getParamValue('title', link.search));
return t.isTalkPage() || t.namespace === 4;
}).append(
' (',
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig'),
')'
);
});
if (mw.config.get('wgAction') !== 'history') return;
mw.hook('wikipage.content').add($content => {
$content.find('.mw-pager-tools').append(
$('<span>').append(
$('<a>').attr({
href: '#',
role: 'button'
}).on('click', handler).text('sig')
)
);
});
});
// mw.config.get('wgAction') === 'history' &&
// mw.loader.using('mediawiki.util', function () {
// mw.hook('wikipage.content').add($content => {
// $content.find('a.mw-changeslist-date').after(function () {
// return [
// ' (',
// $('<a>').attr('href', mw.util.getUrl(null, {
// action: 'edit',
// oldid: this.closest('li').dataset.mwRevid
// })).text('e'),
// ')'
// ];
// });
// });
// });
// ['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
// mw.hook('wikipage.content').add($content => {
// $content.find('.mw-changeslist-history').parent().after(function () {
// return $('<span>').append(
// $('<a>').attr(
// 'href',
// this.firstElementChild.getAttribute('href').slice(0, -7) + 'edit'
// ).text('e')
// );
// });
// });
if (screen.width < 500) {
mw.loader.addStyleTag('@font-face{font-family:CharisW;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/017b2b2ad86e09d3c22b8cf0dfc78247/CharisSILRegular.ttf) format(truetype)} @font-face{font-family:CharisW;font-weight:700;src:url(//fontlibrary.org/assets/fonts/charis/10b9f94ed21e56254b068c91ead7ec6f/6f5069ac6a300dad45383c952e92c573/CharisSILBold.ttf) format(truetype)} body .IPA{font-family:CharisW,sans-serif} .mw-highlight-lines > pre{width:120em}');
location.hash && $(() => {
let target = document.querySelector(':target');
if (target?.getBoundingClientRect().top < 0) {
target.scrollIntoView();
}
});
}
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
(mw.config.exists('wgCodeEditorCurrentLanguage') ||
mw.config.exists('cmMode') && mw.config.get('cmMode') !== 'mediawiki') &&
(function saveNEdit() {
let notif;
$(document.body).on('click', '#wpSave', async function (e) {
if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey ||
e.originalEvent?.defaultPrevented
) {
return;
}
e.preventDefault();
await mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'jquery.textSelection', 'oojs-ui-core'
]);
let button = OO.ui.infuse(this.parentElement).setDisabled(true);
let $textarea = $('#wpTextbox1');
let text = $textarea.textSelection('getContents');
let $summary = $('#wpSummary');
let formData = new FormData(this.form);
let promise = new mw.Api().postWithEditToken({
action: 'edit',
title: mw.config.get('wgPageName'),
text: text,
section: formData.get('wpSection') || undefined,
summary: $summary.textSelection('getContents'),
[$('#wpMinoredit').prop('checked') ? 'minor' : 'notminor']: 1,
baserevid: formData.get('editRevId'),
basetimestamp: formData.get('wpEdittime'),
starttimestamp: formData.get('wpStarttime'),
watchlist: $('#wpWatchthis').prop('checked') ? 'watch' : 'unwatch',
watchlistexpiry: formData.get('wpWatchlistExpiry') || undefined,
undo: formData.get('wpUndidRevision') || undefined,
undoafter: formData.get('wpUndoAfter') || undefined,
contentformat: formData.get('format'),
contentmodel: formData.get('model'),
assertuser: mw.config.get('wgUserName'),
formatversion: 2
});
notif?.close();
notif = null;
try {
let response = await promise;
if (response?.edit?.result !== 'Success') throw '';
$('#editform > input[name="wpUndidRevision"], #editform > input[name="wpUndoAfter"]').remove();
$textarea.data('origtext', text).prop('defaultValue', text);
$summary.val($summary.prop('defaultValue'));
if (mw.loader.getState('mediawiki.editRecovery.edit') === 'ready') {
let storage = mw.loader.moduleRegistry['mediawiki.editRecovery.edit'].packageExports['storage.js'];
storage.deleteData(mw.config.get('wgPageName'));
storage.closeDatabase();
}
notif = await mw.notify(response.edit.nochange ? 'No change' : [
document.createTextNode('Saved'),
$('<p>').append(
new OO.ui.ButtonWidget({
href: mw.util.getUrl(),
target: '_blank',
label: 'View'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, {
diff: response.edit.newrevid || 'cur',
diffonly: 1
}),
target: '_blank',
label: 'Diff'
}).$element,
new OO.ui.ButtonWidget({
href: mw.util.getUrl(null, { action: 'history' }),
target: '_blank',
label: 'History'
}).$element
)[0]
], { tag: 'savenedit' });
} catch (error) {
notif = await mw.notify(error?.error?.info || error || 'Save failed', {
autoHideSeconds: 'long',
tag: 'savenedit',
type: 'error'
});
} finally {
button.setDisabled();
}
});
}());
mw.config.get('wgNamespaceNumber') === 0 &&
mw.config.get('wgAction') === 'view' &&
mw.config.get('wgCategories')?.some(c => c.endsWith(' actors') || c.endsWith(' actresses')) &&
$(() => {
let n = $.escapeSelector(mw.config.get('wgTitle').replace(/ \(.+\)$/, ''));
let $links = $(`.hatnote a[title$="${n} filmography"], .hatnote a[title*="${n} on "], .hatnote a[title*="${n} performances"]`);
if (!$links.length) return;
let titles = {};
$links = $links.filter(function () {
let text = this.textContent;
return !(titles[text] = Object.hasOwn(titles, text));
});
mw.notify(
$links.length === 1
? $links.clone()
: $('<ul>').append($links.clone().wrap('<li>').parent()),
{ autoHideSeconds: 'long' }
);
});
['Recentchanges', 'Recentchangeslinked', 'Watchlist'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/RCMuter.js&action=raw&ctype=text/javascript', 's');
location.hostname.endsWith('.wikipedia.org') &&
mw.config.get('wgNamespaceNumber') % 2 === 0 &&
// mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.util')).then(function refRenamer() {
if (!document.getElementById('p-tb')) return;
let messages = Object.assign({
portlet: 'RefRenamer',
loading: 'Loading RefRenamer...'
}, window.refrenamerMessages);
let clicked;
mw.util.addPortletLink('p-tb', '#', messages.portlet, 't-refrenamer').firstElementChild.addEventListener('click', e => {
e.preventDefault();
if (clicked) {
if (window.refRenamer) {
window.refRenamer();
}
return;
}
clicked = true;
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox6.js&action=raw&ctype=text/javascript');
mw.notify(messages.loading, {
autoHideSeconds: 'long',
tag: 'refrenamer'
});
});
});
if (['edit', 'submit'].includes(mw.config.get('wgAction'))) {
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/ExpandContractions.js&action=raw&ctype=text/javascript', 's');
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/Unpipe.js&action=raw&ctype=text/javascript', 's');
}
mw.config.get('wgAction') !== 'history' &&
mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Nardog/CopyCodeBlock.js&action=raw&ctype=text/javascript', 's');
mw.config.exists('wgDiffNewId') &&
mw.config.get('wgDiscussionToolsFeaturesEnabled') &&
(function () {
let data = {}, clickHandler, autoClear, run;
window.dtc = data;
let highlight = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
mw.loader.moduleRegistry['ext.discussionTools.init'].packageExports['highlighter.js']
.highlightNewComments(mw.dt.pageThreads, true, ids);
if (clickHandler) {
$(document.body).off('click', clickHandler);
return;
}
$._data(document.body, 'events').click.some(o => {
if (String(o.handler).includes('highlighter.clearHighlightTargetComment(')) {
$(document.body).off('click', o.handler);
clickHandler = o.handler;
return true;
}
});
$._data(window, 'events').popstate.some(o => {
if (String(o.handler).includes('highlighter.highlightTargetComment(')) {
$(window).off('popstate', o.handler);
return true;
}
});
};
let scroll = revId => {
let ids = data[revId];
if (!ids || !ids.length) return;
let yToSpan = Object.fromEntries(
ids.map(id => document.getElementById(id)).filter(Boolean)
.map(span => [span.getBoundingClientRect().y, span])
);
let ys = Object.keys(yToSpan);
if (!ys.length) return;
let lower = ys.filter(y => y > 10);
if (!lower.length ||
Math.max(...lower) < document.documentElement.clientHeight
) {
yToSpan[Math.min(...ys)].scrollIntoView({ behavior: 'smooth' });
} else {
yToSpan[Math.min(...lower)].scrollIntoView({ behavior: 'smooth' });
}
};
let scrollToNext = function (e) {
e.preventDefault();
let revId = mw.config.get('wgDiffOldId');
if (!revId || !data[revId]) return;
let i = data[revId].indexOf(
this.closest('[data-mw-thread-id]').dataset.mwThreadId
);
if (i === -1) return;
let next = data[revId][i + 1] || data[revId][0];
document.getElementById(next).scrollIntoView({ behavior: 'smooth' });
};
mw.hook('wikipage.content').add(async $content => {
let revId = mw.config.get('wgDiffOldId');
if (!revId) return;
let param = new URLSearchParams(location.search).get('diffonly');
if (param && param !== '0') return;
if (data[revId]) {
highlight(revId);
return;
}
await mw.loader.using(['ext.discussionTools.init', 'mediawiki.util']);
let begin = Date.parse($('#mw-diff-otitle1 .mw-diff-timestamp').data('timestamp'));
data[revId] = mw.dt.pageThreads.getCommentItems()
.filter(c => c.timestamp > begin).map(c => c.id);
if (!data[revId].length) return;
await new Promise(setTimeout);
highlight(revId);
$content.find('.ext-discussiontools-init-replylink-buttons').filter(function () {
return data[revId].includes(this.dataset.mwThreadId);
}).children('span:last-of-type').before(
' | ',
$('<a>').attr({
href: '#',
role: 'button'
}).text('next').on('click', scrollToNext)
);
if (run || !document.getElementById('p-tb')) return;
run = true;
let portlet = mw.util.addPortletLink('p-tb', '#', 'Scroll to next', 't-scrolltonext');
portlet.firstElementChild.addEventListener('click', e => {
e.preventDefault();
scroll(mw.config.get('wgDiffOldId'));
});
mw.util.addPortletLink('p-tb', '#', 'Toggle highlight', 't-togglehighlight').firstElementChild.addEventListener('click', e => {
e.preventDefault();
autoClear = !autoClear;
if (autoClear) {
$(document.body).on('click', clickHandler)[0].click();
} else {
highlight(mw.config.get('wgDiffOldId'));
}
});
mw.loader.addStyleTag(`#t-scrolltonext{position:fixed;bottom:${portlet.clientHeight}px} #t-togglehighlight{position:fixed;bottom:0}`);
});
}());
mw.config.get('wgNamespaceNumber') === 6 &&
mw.config.get('wgAction') === 'view' &&
mw.hook('wikipage.content').add($content => {
$content.find('.filehistory .mw-usertoollinks-contribs').after(function () {
return [
' | ',
$('<a>').attr('href', `${
mw.config.get('wgScript')
}?title=Special:ListFiles/${
this.pathname.replace(/^.+\//, '')
}&ilshowall=1`).text('uploads')
];
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$(function () {
if (!$('input[name="wpSection"]').val()) return;
mw.hook('wikipage.content').add(async $content => {
let $refs = $content.find('.mw-ext-cite-warning-sectionpreview_no_text');
if (!$refs.length) return;
let ids = {};
$refs.each(function () {
ids[this.closest('[id]').id.replace(/-\d+$/, '')] = this;
});
let response = await $.get(`/api/rest_v1/page/html/${encodeURIComponent(mw.config.get('wgPageName'))}`);
$($.parseHTML(response)).find('.mw-reference-text').each(function () {
ids[this.id.replace(/^mw-reference-text-|-\d+$/g, '')]?.replaceWith(this);
});
});
});
mw.hook('moremenu.ready').add(config => {
$('#mm-page-purge-cache > a').on('click', e => {
e.preventDefault();
new mw.Api().post({
action: 'purge',
forcelinkupdate: 1,
titles: config.page.name,
formatversion: 2
}).then(() => {
location.href = mw.util.getUrl();
});
});
$('#mm-page-search-search-history-wikiblame > a').on('click', function (e) {
e.preventDefault();
let q = prompt();
if (q === null) return;
let href = this.href;
if (q) {
let removal = q[0] === '!';
if (removal) {
q = q.slice(1);
}
href += '&needle=' + encodeURIComponent(q);
if (removal) {
href += '&binary_search_inverse=on';
}
href += '&force_wikitags=on';
}
open(href, '_blank');
});
$('#mm-page-expand-templates > a').on('click auxclick', function (e) {
if (e.which > 2) return;
e.preventDefault();
let revId = mw.config.get('wgRevisionId') || Number($('input[name=oldid]').val());
let url = revId
? '/w/rest.php/v1/revision/' + revId
: '/w/rest.php/v1/page/' + config.page.encodedName;
$.get(url).then(response => {
$('<form>').attr({
method: 'post',
action: this.href,
target: '_blank'
}).append(
[
['wpInput', response.source],
['wpContextTitle', config.page.name],
['wpRemoveComments', 1]
].map(([n, v]) => $('<input>').attr({
name: n,
type: 'hidden'
}).val(v))
).appendTo(document.body).trigger('submit').remove();
});
});
});
mw.config.get('wgCanonicalSpecialPageName') === 'ApiSandbox' &&
mw.hook('apisandbox.formatRequest').add((...args) => {
args[4].complete = function () {
setTimeout(() => {
mw.hook('wikipage.content').fire($('.oo-ui-pageLayout-active'));
}, 100);
};
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.config.get('wgArticleId') &&
mw.config.get('wgPageContentModel') === 'wikitext' &&
$.when($.ready, mw.loader.using('mediawiki.storage')).then(async () => {
let infuseAndCall = (query, method, ...args) => {
let $widget = $(query);
if ($widget.length) {
return OO.ui.infuse($widget)[method](...args);
}
};
let section = $('input[name="wpSection"]').val();
if (section) {
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let save = () => {
let newSource = $textarea.textSelection('getContents');
if (newSource === source) {
mw.storage.session.remove('editfullpage');
} else {
mw.storage.session.setObject('editfullpage', [
mw.config.get('wgPageName'),
section,
newSource.trimEnd(),
infuseAndCall('#wpSummaryWidget', 'getValue') || '',
Number(infuseAndCall('#wpMinoreditWidget', 'isSelected')) || 0,
Number(infuseAndCall('#wpWatchthisWidget', 'isSelected')) || 0,
infuseAndCall('#wpWatchlistExpiryWidget', 'getValue') || 'infinite'
]);
}
};
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
setInterval(() => {
mw.requestIdleCallback(save);
}, 3000);
window.addEventListener('beforeunload', save);
return;
}
let data = mw.storage.session.getObject('editfullpage');
mw.storage.session.remove('editfullpage');
console.log(data);
if (!data || data[0] !== mw.config.get('wgPageName')) return;
let isNew = data[1] === 'new';
let isLead = data[1] === '0';
let $textarea = $('#wpTextbox1');
let source = $textarea.prop('defaultValue');
let newSource, start, msg, notifOpts = { autoHideSeconds: 'long' };
let orig = [];
if (isNew) {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core']);
newSource = source + (data[3] ? '\n== ' + data[3] + ' ==\n\n' : '\n') + data[2] + '\n';
start = source.length;
} else {
await mw.loader.using(['jquery.textSelection', 'oojs-ui-core', 'mediawiki.api']);
let { parse } = await new mw.Api().get({
action: 'parse',
page: mw.config.get('wgPageName'),
prop: 'sections',
wrapoutputclass: '',
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1,
formatversion: 2
});
let target = !isLead && parse.sections.find(s => s.index === data[1]);
if (isLead || target) {
let next = parse.sections.find(s => s.index - 1 === Number(data[1]));
newSource = (isLead ? '' : [...source].slice(0, target.byteoffset)).join('') +
data[2] + (next ? '\n\n' + [...source].slice(next.byteoffset).join('') : '\n');
start = isLead ? 0 : target.byteoffset;
} else {
newSource = source + '\n\n' + data[2] + '\n';
start = source.length;
msg = `Section restored. Couldn't find the section. The source is appended at bottom.`;
notifOpts.type = 'warn';
}
orig[0] = infuseAndCall('#wpSummaryWidget', 'getValue');
infuseAndCall('#wpSummaryWidget', 'setValue', data[3]);
}
$textarea.textSelection('setContents', newSource);
orig[1] = infuseAndCall('#wpMinoreditWidget', 'getSelected');
infuseAndCall('#wpMinoreditWidget', 'setSelected', data[4]);
orig[2] = infuseAndCall('#wpWatchthisWidget', 'getSelected');
infuseAndCall('#wpWatchthisWidget', 'setSelected', data[5]);
orig[3] = infuseAndCall('#wpWatchlistExpiryWidget', 'getValue');
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', data[6]);
setTimeout(() => {
$textarea.textSelection('setSelection', { start });
});
let notif = await mw.notify($([
document.createTextNode(msg || 'Section restored.'),
$('<p>').append(
new OO.ui.ButtonWidget({
flags: 'destructive',
label: 'Discard'
}).on('click', () => {
$textarea.textSelection('setContents', source);
if (orig[0]) {
infuseAndCall('#wpSummaryWidget', 'setValue', orig[0]);
}
infuseAndCall('#wpMinoreditWidget', 'setSelected', orig[1]);
infuseAndCall('#wpWatchthisWidget', 'setSelected', orig[2]);
infuseAndCall('#wpWatchlistExpiryWidget', 'setValue', orig[3]);
notif.close();
}).$element
)[0]
]), notifOpts);
});
mw.config.exists('wgPostEdit') &&
mw.loader.using('mediawiki.storage', () => {
mw.storage.session.remove('editfullpage');
});
mw.config.get('wgAction') === 'history' &&
mw.hook('wikipage.content').add(async $content => {
if (!$content.has('.mw-history-line-updated').length) return;
let href = $content.find('a.mw-history-histlinks-current:not(.mw-history-line-updated a)').attr('href');
if (!href) {
await mw.loader.using(['mediawiki.api', 'mediawiki.util']);
let page = (await new mw.Api().get({
action: 'query',
titles: mw.config.get('wgPageName'),
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev >= page.lastrevid) return;
href = mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
}
$content.find('.mw-history-compareselectedversions-button').first().after(
' ',
$('<a>').attr({
class: 'unseendiff',
href: href
}).text('unseen')
);
});
(async () => {
let cspn = mw.config.get('wgCanonicalSpecialPageName');
let isBp = cspn === 'Blankpage';
if (!isBp && cspn !== 'Watchlist') return;
await mw.loader.using('mediawiki.util');
let notify = async (text, options, pn) => {
let msg = [document.createTextNode(text)];
if (pn) {
msg.push(
$('<p>').append(
$('<a>').attr('href', mw.util.getUrl(pn)).text(pn),
' ',
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'edit' }))
.text('edit')
),
$('<span>').append(
$('<a>')
.attr('href', mw.util.getUrl(pn, { action: 'history' }))
.text('history')
)
)
)[0]
);
}
if (isBp) {
await $.ready;
$('#mw-content-text').html(msg);
} else {
return mw.notify(msg, Object.assign(options || {}, {
tag: 'unseendiff'
}));
}
};
let getUrl = async pn => {
await mw.loader.using('mediawiki.api');
let page = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'info',
inprop: 'notificationtimestamp',
formatversion: 2
})).query.pages[0];
if (!page.notificationtimestamp) {
notify(`Couldn't get the last seen time.`, {
type: 'warn'
}, pn);
return;
}
let rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (rev === page.lastrevid) {
notify('Already seen.', {
type: 'warn'
}, pn);
return;
}
if (!rev) {
rev = (await new mw.Api().get({
action: 'query',
titles: pn,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
rvdir: 'newer',
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev) {
notify(`Couldn't get the last seen revision.`, {
type: 'warn'
}, pn);
return;
}
}
if (rev > page.lastrevid) {
notify(`Invalid rev for "${pn}" (rev: ${rev}, lastrevid: ${page.lastrevid})`, {
autoHideSeconds: 'long',
type: 'warn'
}, pn);
return;
}
return mw.util.getUrl(page.title, { diff: page.lastrevid, oldid: rev });
};
if (isBp) {
let pn = mw.config.get('wgTitle').match(/^[^/]+\/unseendiff\/(.+)$/)?.[1];
if (!pn) return;
notify('Loading...', null, pn);
let href = await getUrl(pn);
if (!href) return;
notify('Redirecting...', null, pn);
location.href = href;
return;
}
let handler = async function (e) {
if (e.which > 2) return;
e.preventDefault();
let pn = this.dataset.pn;
if (!pn) {
notify(`Couldn't get the page name.`, {
type: 'error'
});
return;
}
let notifPromise = notify('Loading...', {
autoHideSeconds: 'long'
});
let href = await getUrl(pn);
if (!href) return;
$(`.unseendiff-loader[data-pn="${$.escapeSelector(pn)}"]`).attr({
class: 'unseendiff',
href: href,
target: '_blank'
}).off('click auxclick', handler);
if (e.type === 'auxclick' || e.ctrlKey || e.metaKey || e.shiftKey) {
open(href);
} else {
this.click();
}
(await notifPromise).close();
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-changeslist-src-mw-edit.mw-changeslist-watchedunseen:not(.mw-changeslist-watchedseen) .mw-changeslist-line-inner'
).each(function () {
let pn = this.dataset.targetPage ||
this.closest('[data-target-page]')?.dataset.targetPage ||
this.closest('table.mw-enhanced-rc')?.querySelector('[data-target-page]')?.dataset.targetPage;
if (!pn) return;
$('<span>').append(
$('<a>').attr({
class: 'unseendiff-loader',
href: mw.util.getUrl(`Special:BlankPage/unseendiff/${pn}`),
'data-pn': pn
}).on('click auxclick', handler).text('unseen')
).appendTo(
[...this.querySelectorAll('.mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
});
})();
['Contributions', 'IPContributions', 'Blankpage'].includes(mw.config.get('wgCanonicalSpecialPageName')) &&
mw.loader.using('mediawiki.util', () => {
let watched = new Set();
let query = async lis => {
let titles = Object.keys(lis).slice(0, 50);
if (!titles.length) return;
await mw.loader.using('mediawiki.api');
let pages = (await new mw.Api().post({
action: 'query',
titles: titles,
prop: 'info',
inprop: 'notificationtimestamp|watched',
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
})).query.pages;
for (let page of pages) {
if (!Object.hasOwn(lis, page.title)) continue;
if (page.watched) {
watched.add(page);
$(lis[page.title]).addClass('watched');
}
if (!page.notificationtimestamp) continue;
let rev = (await new mw.Api().get({
action: 'query',
titles: page.title,
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
rvstart: Date.parse(page.notificationtimestamp) / 1000 - 1,
formatversion: 2
})).query.pages[0].revisions?.[0].revid;
if (!rev || rev === page.lastrevid) continue;
if (rev > page.lastrevid) {
mw.notify($([
document.createTextNode('Invalid rev for "'),
$('<a>').attr({
href: mw.util.getUrl(page.title, { action: 'history' }),
target: '_blank'
}).text(page.title)[0],
document.createTextNode(`" (rev: ${rev}, lastrevid: ${page.lastrevid})`),
]), {
autoHideSeconds: 'long',
type: 'warn'
});
continue;
}
$('<span>').append(
$('<a>').attr({
class: 'unseendiff',
href: mw.util.getUrl(page.title, {
diff: page.lastrevid,
oldid: rev
})
}).text('unseen')
).appendTo(
lis[page.title].map(li => (
[...li.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(li).before(' ')
))
);
}
titles.forEach(title => {
delete lis[title];
});
query(lis);
};
mw.hook('wikipage.content').add($content => {
$content.find(
'.mw-contributions-list > li:not(.mw-contributions-current)[data-mw-revid]'
).each(function () {
let link = this.querySelector('a.mw-changeslist-date, a.mw-changeslist-history');
let pn = link ? new URLSearchParams(link.search).get('title') : '';
$('<span>').append(
$('<a>').attr({
class: 'mw-changeslist-diff',
href: mw.util.getUrl(pn, {
diff: 'cur',
oldid: this.dataset.mwRevid
})
}).text('cur')
).appendTo(
[...this.querySelectorAll(':scope > .mw-pager-tools')].pop() ||
$('<span>').addClass('mw-changeslist-links mw-pager-tools').appendTo(this).before(' ')
);
});
if (mw.config.get('wgWikiID') === 'wikidatawiki') return;
let lis = {};
$content.find('.mw-contributions-title').each(function () {
let title = this.textContent;
if (!Object.hasOwn(lis, title)) {
lis[title] = [];
}
lis[title].push(this.closest('li'));
});
Object.keys(lis).forEach(title => {
if (watched.has(title)) {
$(lis[title]).addClass('watched');
delete lis[title];
}
});
query(lis);
});
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.loader.load('//test.wikipedia.org/w/index.php?title=User:Nardog/sandbox9.js&action=raw&ctype=text/javascript');
mw.config.get('wgWikiID') === 'metawiki' &&
(async () => {
let css = mw.loader.addStyleTag(`.wishtitle {
font-size: 90%;
font-style: italic;
word-break: break-word;
}
.wishtitle > a {
color: var(--color-warning, #886425);
}
.wishtitle > a:visited {
color: var(--border-color-warning--hover, #735421);
}
.wishtitle-declined > a {
text-decoration: line-through;
}
.wishtitle-declined > a:hover,
.wishtitle-declined > a:focus,
.mw-underline-always .wishtitle-declined > a {
text-decoration: line-through underline;
}
#watchlist-edit-form .wishtitle {
display: inline-block;
}
.mw-search-result-heading > .wishtitle,
.catchangesviewer-table .wishtitle {
display: block;
}
.catchangesviewer-table:has(.wishtitle) {
white-space: wrap;
}`);
let lang = mw.config.get('wgUserLanguage');
let titles;
let loadTitles = async () => {
await mw.loader.using('mediawiki.storage');
titles = titles || mw.storage.getObject('wishtitles');
if (titles?.lang !== lang) {
titles = { lang, w: [], fa: [] };
}
};
let updateTitles = async (crwstatuses, crwcontinue) => {
await mw.loader.using('mediawiki.api');
let params = {
action: 'query',
list: 'communityrequests-wishes',
crwlang: lang,
crwstatuses: crwstatuses,
crwprop: 'title|updated',
crwsort: 'updated',
crwdir: 'ascending',
crwlimit: 'max',
crwcontinue: crwcontinue,
formatversion: 2
};
if (!crwcontinue && !crwstatuses && titles._) {
params.crwcontinue = `|${titles._}|0`;
}
let response = await new mw.Api().get(params);
let wishes = response?.query?.['communityrequests-wishes'];
if (wishes?.length) {
let $span = $('<span>');
wishes.forEach(w => {
let id = w.crwtitle.match(/^Community Wishlist\/W(\d+)/)?.[1];
if (!id) return;
titles.w[id - 1] = $span.html(w.title).text();
if (crwstatuses === 'declined') {
(titles.wd = titles.wd || []).push(id - 1);
}
let faId = w.crfatitle?.match(/^Community Wishlist\/FA(\d+)/)?.[1];
if (!faId) return;
titles.fa[faId - 1] = w.focusareatitle;
});
if (!crwstatuses) {
titles._ = wishes.at(-1).updated.replace(/\D/g, '');
}
}
let expiry = 86400;
if (crwstatuses || crwcontinue) {
let prev = mw.storage.getObject('_EXPIRY_wishtitles');
if (prev) {
expiry = Math.round(Date.now() / 1000) + 86400 - prev;
}
}
mw.storage.setObject('wishtitles', titles, expiry);
crwcontinue = response?.continue?.crwcontinue;
if (crwcontinue) {
await updateTitles(crwstatuses, crwcontinue);
}
};
let getTitle = id => (
id[0] === 'W' ? titles.w[id.slice(1) - 1] : titles.fa[id.slice(2) - 1]
);
let renderTitle = (title, id, tag = 'span') => {
let classes = 'wishtitle';
if (id[0] === 'W' && titles.wd?.includes(id.slice(1) - 1)) {
classes += ' wishtitle-declined';
}
return $(`<${tag}>`).addClass(classes).append(
$('<a>').attr({
href: `/wiki/Community_Wishlist/${id}`,
title: `Community Wishlist/${id}`
}).text(title)
);
};
let callback = ([id, links]) => {
let title = getTitle(id);
if (!title) {
return true;
}
$(links).after(' ', renderTitle(title, id));
};
let selector = '.mw-changeslist-title, ' +
'.mw-changeslist-log-entry > a:not(.mw-userlink), ' +
'.mw-changeslist-line.mw-changeslist-src-mw-categorize :is(.mw-changeslist-line-inner, .mw-changeslist-line-inner-comment, .mw-enhanced-rc-nested) > .comment > a, ' +
'#watchlist-edit-form .cdx-table td > label > a, ' +
'.mw-search-result-heading > a:not(:has(> .ext-communityrequests-entity-link--label)), ' +
'.mw-contributions-title, ' +
'#mw-whatlinkshere-list li > bdi > a, ' +
'.mw-allpages-chunk > li > a, ' +
'.mw-prefixindex-list > li > a, ' +
'.mw-logevent-loglines > li > a, ' +
'#mw-pages li > a, ' +
'.catchangesviewer-table td:nth-child(3) > a';
mw.hook('wikipage.content').add(async $content => {
let links = {};
$content.find('a').each(function () {
if (!this.matches(selector)) return;
let id = this.textContent.match(
/^(?:Talk:|Translations:)?Community Wishlist\/((?:W|FA)\d+)/
)?.[1];
if (!id) return;
(links[id] = links[id] || []).push(this);
});
links = Object.entries(links);
if (!links.length) return;
await loadTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles();
links = links.filter(callback);
if (!links.length) return;
await updateTitles('declined');
links.forEach(callback);
});
let pn = mw.config.get('wgRelevantPageName');
let id = pn.match(/^(?:Talk:|Translations:)?Community_Wishlist\/((?:W|FA)\d+)/)?.[1];
if (!id) return;
await $.ready;
let extTitle = document.querySelector('.ext-communityrequests-wish--title');
if (extTitle && $('.mw-pt-languages-selected').attr('lang') === lang) return;
await loadTitles();
let title = getTitle(id);
if (!title) {
await updateTitles();
title = getTitle(id);
if (!title) {
await updateTitles('declined');
title = getTitle(id);
if (!title) return;
}
}
let $title = renderTitle(title, id, 'div');
if (mw.config.get('skin') === 'vector-2022') {
$title.prependTo('.vector-page-toolbar');
} else {
$title.insertAfter('#firstHeading');
}
css.textContent += ' .ext-communityrequests-entity-talk-header{display:none}';
if (extTitle) return;
document.title = document.title.replace(
pn.replaceAll('_', ' '),
`${pn.replace(`Community_Wishlist/${id}`, title)} ($&)`
);
})();
mw.config.get('wgWikiID') === 'metawiki' &&
mw.hook('wikipage.watchlistChange').add(async (isWatched, expiry) => {
if (![0, 1].includes(mw.config.get('wgNamespaceNumber'))) return;
let title = mw.config.get('wgTitle');
if (!/^Community Wishlist\/(?:W|FA)\d+$/.test(title)) return;
if (isWatched) {
await new mw.Api().watch(title + '/Votes', expiry);
mw.notify('Watching /Votes too.');
} else {
await new mw.Api().unwatch(title + '/Votes');
mw.notify('Unwatched /Votes too.');
}
});
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
$(async () => {
let $input = $('#wpTemplateSandboxTemplate');
if (!$input.length) return;
mw.loader.addStyleTag('#templatesandbox-editform .oo-ui-fieldLayout{max-width:50em} #templatesandbox-editform .oo-ui-fieldLayout-field{flex-grow:999}');
let makeTemplateField = () => new OO.ui.FieldLayout(
new mw.widgets.TitleInputWidget({
inputId: 'wpTemplateSandboxTemplate',
name: 'wpTemplateSandboxTemplate',
showMissing: false,
value: $input.val()
}),
{ label: 'Template name:' }
);
if (mw.loader.getState('ext.TemplateSandbox') !== 'registered') {
await mw.loader.using('mediawiki.widgets');
$input.parent().replaceWith(makeTemplateField().$element);
return;
}
let require = await mw.loader.using([
'ext.TemplateSandbox.TemplateSandboxTitleWidget',
'ext.TemplateSandbox.styles', 'jquery.makeCollapsible', 'user.options'
]);
let widget = new (require('ext.TemplateSandbox.TemplateSandboxTitleWidget'))({
$overlay: true,
id: 'wpTemplateSandboxPage',
maxLength: 255,
name: 'wpTemplateSandboxPage',
placeholder: 'Page title',
required: false,
tabIndex: 10,
templateTitleFunc: () => $('#wpTemplateSandboxTemplate').val()
});
widget.$element.attr('data-ooui', '{"_":"mw.widgets.TemplateSandboxTitleWidget"}')
.data('oouiInfused', widget);
let fieldset = new OO.ui.FieldsetLayout({
classes: ['mw-templatesandbox-fieldset', 'mw-collapsed'],
id: 'templatesandbox-editform',
items: [
makeTemplateField(),
new OO.ui.ActionFieldLayout(
widget,
new OO.ui.ButtonInputWidget({
id: 'wpTemplateSandboxPreview',
name: 'wpTemplateSandboxPreview',
label: 'Show preview',
tabIndex: 10,
type: 'submit',
useInputTag: true
}),
{ align: 'top' }
)
],
label: 'Preview page with this template'
});
fieldset.$label.append(' ', $('<span>').addClass('mw-collapsible-toggle-placeholder'));
fieldset.$group.addClass('mw-collapsible-content');
$('#templatesandbox-editform').replaceWith(fieldset.$element.makeCollapsible());
let modules = ['ext.TemplateSandbox'];
if (Number(mw.user.options.get('uselivepreview'))) {
modules.push('ext.TemplateSandbox.preview');
}
mw.loader.load(modules);
});
mw.config.get('wgWikiID') === 'enwiki' &&
mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist' &&
(async () => {
mw.loader.addStyleTag('.xfdnotifier-sublinks::before{content:" ["} .xfdnotifier-sublinks::after{content:"]"} .xfdnotifier-sublinks > span:not(:first-child)::before{content:"\\2009·\\2009"} .mw-portlet.vector-menu[id^="p-xfdnotifier-"] a{display:inline}');
await mw.loader.using(['mediawiki.api', 'mediawiki.Title', 'mediawiki.storage']);
let xfds = [
{
id: 'rm',
label: 'RM',
full: 'Requested moves',
cat: 'Requested moves',
},
{
id: 'rmt',
label: 'RM/T',
full: 'Requested moves (technical)',
page: 'Wikipedia:Requested_moves/Technical_requests',
titleExtractor: $page => (
$page.find(`[data-mw*='"wt":"RMassist/core"']`).closest('li').map(function () {
return this.querySelector('a[rel="mw:WikiLink"]')?.title;
}).get()
)
},
{
id: 'afd',
label: 'AfD',
full: 'Articles for deletion',
cat: 'Articles for deletion'
},
{
id: 'mfd',
label: 'MfD',
full: 'Miscellaneous for deletion',
cat: 'Miscellaneous pages for deletion'
},
{
id: 'tfd',
label: 'TfD',
full: 'Templates for deletion',
cat: 'Templates for deletion'
},
{
id: 'tfm',
label: 'TfM',
full: 'Templates for merging',
cat: 'Templates for merging'
},
{
id: 'cfd',
label: 'CfD',
full: 'Categories for deletion',
cat: 'Categories for deletion'
},
{
id: 'cfr',
label: 'CfR',
full: 'Categories for renaming',
cat: 'Categories for renaming'
},
{
id: 'cfsr',
label: 'CfSR',
full: 'Categories for speedy renaming',
cat: 'Categories for speedy renaming'
},
{
id: 'cfm',
label: 'CfM',
full: 'Categories for merging',
cat: 'Categories for merging'
},
{
id: 'cfs',
label: 'CfS',
full: 'Categories for splitting',
cat: 'Categories for splitting'
},
{
id: 'cfl',
label: 'CfL',
full: 'Categories for listifying',
cat: 'Categories for listifying'
},
{
id: 'cfc',
label: 'CfC',
full: 'Categories for conversion',
cat: 'Categories for conversion'
},
{
id: 'cfgd',
label: 'CfGD',
full: 'Categories for general discussion',
cat: 'Categories for general discussion'
},
{
id: 'ffd',
label: 'FfD',
full: 'Files for discussion',
cat: 'Wikipedia files for discussion'
},
{
id: 'rfd',
label: 'RfD',
full: 'Redirects for discussion',
cat: 'All redirects for discussion'
},
{
id: 'prod',
label: 'PROD',
full: 'Articles proposed for deletion',
cat: 'All articles proposed for deletion'
}
];
window.xfd = xfds;
let queryTitles = async (xfd, titles) => {
if (!titles.length) return;
let response = await new mw.Api().get({
action: 'query',
titles: titles.slice(0, 50),
prop: 'info',
inprop: 'watched',
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched) {
xfd.pages.push(p.title);
}
});
await queryTitles(xfd, titles.slice(50));
};
let queryPage = async xfd => {
let $page = $($.parseHTML(await $.get(
`https://en.wikipedia.org/w/rest.php/v1/page/${encodeURIComponent(xfd.page)}/html`
)));
await queryTitles(xfd, xfd.titleExtractor($page));
};
let queryCat = async (xfd, gcmcontinue) => {
let response = await new mw.Api().get({
action: 'query',
prop: 'info|categories',
inprop: 'watched',
clprop: 'sortkey',
clcategories: `Category:${xfd.cat}`,
generator: 'categorymembers',
gcmtitle: `Category:${xfd.cat}`,
gcmlimit: 'max',
gcmsort: 'timestamp',
gcmdir: 'older',
gcmcontinue: gcmcontinue,
formatversion: 2
});
response?.query?.pages?.forEach(p => {
if (p.watched && p.categories?.[0]?.sortkeyprefix !== ' ') {
xfd.pages.push(p.title);
}
});
if (response?.continue?.gcmcontinue) {
await queryCat(xfd, response.continue.gcmcontinue);
}
};
let show = async (xfd, lastId, isCache) => {
if (xfd.portlet && isCache) return;
let portletId = 'p-xfdnotifier-' + xfd.id;
if (xfd.portlet) {
$(xfd.portlet).find('ul').empty();
if (!xfd.pages.length) return;
} else {
await $.ready;
xfd.portlet = mw.util.addPortlet(portletId, xfd.label, '#' + lastId);
}
let $label = $(`#${portletId}-label`).attr('title', xfd.full);
if (xfd.page) {
$label.wrapInner($('<a>').attr('href', mw.util.getUrl(xfd.page)));
}
xfd.pages.forEach(p => {
let t = mw.Title.newFromText(p);
let isTalk = t.isTalkPage();
let $other = $('<a>').attr({
href: t[isTalk ? 'getSubjectPage' : 'getTalkPage']().getUrl(),
title: isTalk ? 'subject' : 'talk'
}).text(isTalk ? 's' : 't');
let link = mw.util.addPortletLink(portletId, t.getUrl(), p).querySelector('a');
$('<span>').addClass('xfdnotifier-sublinks').append(
$('<span>').append($other),
$('<span>').append(
$('<a>').attr({
href: t.getUrl({ action: 'history' }),
title: 'history'
}).text('h')
)
).insertAfter(link);
});
};
mw.hook('wikipage.content').add(mw.util.throttle(async () => {
let cache = mw.storage.getObject('xfdnotifier') || {};
let lastId = 'p-tb';
for (let xfd of xfds) {
let portletId = 'p-xfdnotifier-' + xfd.id;
let now = Math.floor(Date.now() / 1000);
if (now - cache[xfd.id]?.[0] < 600) {
xfd.pages = cache[xfd.id].slice(1);
await show(xfd, lastId, true);
lastId = portletId;
continue;
}
xfd.pages = [];
if (xfd.cat) {
await queryCat(xfd);
} else if (xfd.page) {
await queryPage(xfd);
}
cache[xfd.id] = [now, ...xfd.pages];
mw.storage.setObject('xfdnotifier', cache, 604800);
await show(xfd, lastId);
lastId = portletId;
}
}, 1800000));
})();
bnrxy58hy91fiar81nuwe9sn86f73x1
User:SongVĩ.Bot II
2
124239
739887
739784
2026-04-30T17:00:15Z
SongVĩ.Bot II
52414
[[User:SongVĩ.Bot II|Task 0]]: Đã 1585 ngày...
739887
wikitext
text/x-wiki
Cập nhật lần cuối: 01-05-2026
Đã 1585 ngày...
gk4bxy73lvje5y4bkvh2l2eshvh2l2q
User:Novem Linguae/CSD log
2
126005
739935
722157
2026-05-01T08:52:02Z
Novem Linguae
49714
Logging speedy deletion nomination of [[:NovemTest110]].
739935
wikitext
text/x-wiki
This is a log of all [[WP:CSD|speedy deletion]] nominations made by this user using [[WP:TW|Twinkle]]'s CSD module.
If you no longer wish to keep this log, you can turn it off using the [[Wikipedia:Twinkle/Preferences|preferences panel]], and nominate this page for speedy deletion under [[WP:CSD#U1|CSD U1]].
This log does not track outright speedy deletions made using Twinkle.
=== April 2022 ===
# [[:Draft:Test]]: [[WP:CSD#G2|CSD G2]] ({{tl|db-test}}) 22:35, 24 April 2022 (UTC)
# [[:Draft:Test]]: [[WP:G11]] ({{tl|db-spam}}) 22:40, 24 April 2022 (UTC)
=== May 2022 ===
# [[:Dradt:Cirrhilabrus humanni]]: multiple criteria ([[WP:CSD#R2|R2]], [[WP:CSD#G6|G6]]); additional information: {G6 rationale: test} 18:07, 20 May 2022 (UTC)
=== June 2022 ===
# [[:Draft:Test097097]]: multiple criteria ([[WP:CSD#G2|G2]], [[WP:CSD#G11|G11]]); notified {{user|1=NovemBot}} 05:03, 2 June 2022 (UTC)
# [[:Draft:Test097097]]: multiple criteria ([[WP:CSD#G2|G2]], [[WP:CSD#G11|G11]]); notified {{user|1=NovemBot}} 05:06, 2 June 2022 (UTC)
=== September 2024 ===
# [[:Draft:Test]]: [[WP:G12]] ({{tl|db-copyvio}}); notified {{user|1=NovemBot}} 18:58, 18 September 2024 (UTC)
=== August 2025 ===
# [[:Mainspace]]: [[WP:CSD#A3|CSD A3]] ({{tl|db-nocontent}}) 04:43, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G14|CSD G14]] ({{tl|db-disambig}}) 04:43, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}) 04:44, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}) 04:45, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}) 04:51, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 communication: yes} 04:54, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Implausible references: Test} 04:55, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Implausible references} 04:55, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Nonsensical references} 04:56, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Nonsensical references: Test} 04:56, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}) 05:00, 24 August 2025 (UTC)
# [[:User:NovemBot/sandbox]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); notified {{user|1=NovemBot}} 05:01, 24 August 2025 (UTC)
=== October 2025 ===
# [[:Test9030]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); notified {{user|1=NovemBot}} 18:47, 27 October 2025 (UTC)
=== November 2025 ===
# [[:User:NovemBot]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: rusukr} 23:20, 2 November 2025 (UTC)
# [[:User:NovemBot]]: [[WP:CSD#U6|CSD U6]] ({{tl|db-u6}}); notified {{user|1=NovemBot}} 23:23, 2 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#U7|CSD U7]] ({{tl|db-u7}}); notified {{user|1=NovemBot}} 23:26, 2 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#U7|CSD U7]] ({{tl|db-u7}}); additional information: {U7 creative: yes}; notified {{user|1=NovemBot}} 23:28, 2 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#U7|CSD U7]] ({{tl|db-u7}}); additional information: {U7 creative: yes} {U7 professional: yes} {U7 personal: yes} {U7 links: yes}; notified {{user|1=NovemBot}} 23:29, 2 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-banned}}) 06:05, 4 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: a-i}; notified {{user|1=NovemBot}} 06:06, 4 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#U7|CSD U7]] ({{tl|db-u7}}); additional information: {U7 creative: yes} {U7 personal: yes}; notified {{user|1=NovemBot}} 06:19, 4 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: a-a}; notified {{user|1=NovemBot}} 07:00, 19 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: a-a}; notified {{user|1=NovemBot}} 07:07, 19 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-banned}}); additional information: {G5 user: [[:User:Test]]} 07:09, 19 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: a-a}; notified {{user|1=NovemBot}} 07:23, 19 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G4|CSD G4]] ({{tl|db-repost}}); additional information: {G4 xfd: [[:Test]]}; notified {{user|1=NovemBot}} 07:26, 19 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: a-a}; notified {{user|1=NovemBot}} 07:32, 19 November 2025 (UTC)
# [[:TimedText:File:Foldergs.ogv.en.srt]]: [[WP:CSD#G8|CSD G8]] ({{tl|db-timedtext}}) 20:10, 19 November 2025 (UTC)
# [[:TimedText:File:Foldergs.ogv.en.srt]]: [[WP:CSD#G8|CSD G8]] ({{tl|db-timedtext}}) 20:24, 19 November 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G2|CSD G2]] ({{tl|db-test}}) 20:59, 22 November 2025 (UTC)
=== December 2025 ===
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 reason: Test 1} 08:31, 14 December 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 communication: yes} 08:31, 14 December 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Implausible references: Test 3} 08:32, 14 December 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Nonsensical references: Test 4} 08:33, 14 December 2025 (UTC)
=== January 2026 ===
# [[:Mainspace]]: [[WP:CSD#G11|CSD G11]] ({{tl|db-spam}}) 18:57, 4 January 2026 (UTC)
=== May 2026 ===
# [[:NovemTest110]]: [[WP:CSD#A10|CSD A10]] ({{tl|db-a10}}); additional information: {A10 article: [[:Test]]}; notified {{user|1=NovemBot}} 08:52, 1 May 2026 (UTC)
p2rnmi1whwgfuhqat87h6xoyc0om1b8
739941
739935
2026-05-01T09:29:19Z
Novem Linguae
49714
Logging speedy deletion nomination of [[:Mainspace]].
739941
wikitext
text/x-wiki
This is a log of all [[WP:CSD|speedy deletion]] nominations made by this user using [[WP:TW|Twinkle]]'s CSD module.
If you no longer wish to keep this log, you can turn it off using the [[Wikipedia:Twinkle/Preferences|preferences panel]], and nominate this page for speedy deletion under [[WP:CSD#U1|CSD U1]].
This log does not track outright speedy deletions made using Twinkle.
=== April 2022 ===
# [[:Draft:Test]]: [[WP:CSD#G2|CSD G2]] ({{tl|db-test}}) 22:35, 24 April 2022 (UTC)
# [[:Draft:Test]]: [[WP:G11]] ({{tl|db-spam}}) 22:40, 24 April 2022 (UTC)
=== May 2022 ===
# [[:Dradt:Cirrhilabrus humanni]]: multiple criteria ([[WP:CSD#R2|R2]], [[WP:CSD#G6|G6]]); additional information: {G6 rationale: test} 18:07, 20 May 2022 (UTC)
=== June 2022 ===
# [[:Draft:Test097097]]: multiple criteria ([[WP:CSD#G2|G2]], [[WP:CSD#G11|G11]]); notified {{user|1=NovemBot}} 05:03, 2 June 2022 (UTC)
# [[:Draft:Test097097]]: multiple criteria ([[WP:CSD#G2|G2]], [[WP:CSD#G11|G11]]); notified {{user|1=NovemBot}} 05:06, 2 June 2022 (UTC)
=== September 2024 ===
# [[:Draft:Test]]: [[WP:G12]] ({{tl|db-copyvio}}); notified {{user|1=NovemBot}} 18:58, 18 September 2024 (UTC)
=== August 2025 ===
# [[:Mainspace]]: [[WP:CSD#A3|CSD A3]] ({{tl|db-nocontent}}) 04:43, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G14|CSD G14]] ({{tl|db-disambig}}) 04:43, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}) 04:44, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}) 04:45, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}) 04:51, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 communication: yes} 04:54, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Implausible references: Test} 04:55, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Implausible references} 04:55, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Nonsensical references} 04:56, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Nonsensical references: Test} 04:56, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}) 05:00, 24 August 2025 (UTC)
# [[:User:NovemBot/sandbox]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); notified {{user|1=NovemBot}} 05:01, 24 August 2025 (UTC)
=== October 2025 ===
# [[:Test9030]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); notified {{user|1=NovemBot}} 18:47, 27 October 2025 (UTC)
=== November 2025 ===
# [[:User:NovemBot]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: rusukr} 23:20, 2 November 2025 (UTC)
# [[:User:NovemBot]]: [[WP:CSD#U6|CSD U6]] ({{tl|db-u6}}); notified {{user|1=NovemBot}} 23:23, 2 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#U7|CSD U7]] ({{tl|db-u7}}); notified {{user|1=NovemBot}} 23:26, 2 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#U7|CSD U7]] ({{tl|db-u7}}); additional information: {U7 creative: yes}; notified {{user|1=NovemBot}} 23:28, 2 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#U7|CSD U7]] ({{tl|db-u7}}); additional information: {U7 creative: yes} {U7 professional: yes} {U7 personal: yes} {U7 links: yes}; notified {{user|1=NovemBot}} 23:29, 2 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-banned}}) 06:05, 4 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: a-i}; notified {{user|1=NovemBot}} 06:06, 4 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#U7|CSD U7]] ({{tl|db-u7}}); additional information: {U7 creative: yes} {U7 personal: yes}; notified {{user|1=NovemBot}} 06:19, 4 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: a-a}; notified {{user|1=NovemBot}} 07:00, 19 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: a-a}; notified {{user|1=NovemBot}} 07:07, 19 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-banned}}); additional information: {G5 user: [[:User:Test]]} 07:09, 19 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: a-a}; notified {{user|1=NovemBot}} 07:23, 19 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G4|CSD G4]] ({{tl|db-repost}}); additional information: {G4 xfd: [[:Test]]}; notified {{user|1=NovemBot}} 07:26, 19 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: a-a}; notified {{user|1=NovemBot}} 07:32, 19 November 2025 (UTC)
# [[:TimedText:File:Foldergs.ogv.en.srt]]: [[WP:CSD#G8|CSD G8]] ({{tl|db-timedtext}}) 20:10, 19 November 2025 (UTC)
# [[:TimedText:File:Foldergs.ogv.en.srt]]: [[WP:CSD#G8|CSD G8]] ({{tl|db-timedtext}}) 20:24, 19 November 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G2|CSD G2]] ({{tl|db-test}}) 20:59, 22 November 2025 (UTC)
=== December 2025 ===
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 reason: Test 1} 08:31, 14 December 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 communication: yes} 08:31, 14 December 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Implausible references: Test 3} 08:32, 14 December 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Nonsensical references: Test 4} 08:33, 14 December 2025 (UTC)
=== January 2026 ===
# [[:Mainspace]]: [[WP:CSD#G11|CSD G11]] ({{tl|db-spam}}) 18:57, 4 January 2026 (UTC)
=== May 2026 ===
# [[:NovemTest110]]: [[WP:CSD#A10|CSD A10]] ({{tl|db-a10}}); additional information: {A10 article: [[:Test]]}; notified {{user|1=NovemBot}} 08:52, 1 May 2026 (UTC)
# [[:Mainspace]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: kurd} 09:29, 1 May 2026 (UTC)
lvrxr6exs8yyupygqmko1nir1izywqy
739946
739941
2026-05-01T09:46:08Z
Novem Linguae
49714
Logging speedy deletion nomination of [[:NovemTest110]].
739946
wikitext
text/x-wiki
This is a log of all [[WP:CSD|speedy deletion]] nominations made by this user using [[WP:TW|Twinkle]]'s CSD module.
If you no longer wish to keep this log, you can turn it off using the [[Wikipedia:Twinkle/Preferences|preferences panel]], and nominate this page for speedy deletion under [[WP:CSD#U1|CSD U1]].
This log does not track outright speedy deletions made using Twinkle.
=== April 2022 ===
# [[:Draft:Test]]: [[WP:CSD#G2|CSD G2]] ({{tl|db-test}}) 22:35, 24 April 2022 (UTC)
# [[:Draft:Test]]: [[WP:G11]] ({{tl|db-spam}}) 22:40, 24 April 2022 (UTC)
=== May 2022 ===
# [[:Dradt:Cirrhilabrus humanni]]: multiple criteria ([[WP:CSD#R2|R2]], [[WP:CSD#G6|G6]]); additional information: {G6 rationale: test} 18:07, 20 May 2022 (UTC)
=== June 2022 ===
# [[:Draft:Test097097]]: multiple criteria ([[WP:CSD#G2|G2]], [[WP:CSD#G11|G11]]); notified {{user|1=NovemBot}} 05:03, 2 June 2022 (UTC)
# [[:Draft:Test097097]]: multiple criteria ([[WP:CSD#G2|G2]], [[WP:CSD#G11|G11]]); notified {{user|1=NovemBot}} 05:06, 2 June 2022 (UTC)
=== September 2024 ===
# [[:Draft:Test]]: [[WP:G12]] ({{tl|db-copyvio}}); notified {{user|1=NovemBot}} 18:58, 18 September 2024 (UTC)
=== August 2025 ===
# [[:Mainspace]]: [[WP:CSD#A3|CSD A3]] ({{tl|db-nocontent}}) 04:43, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G14|CSD G14]] ({{tl|db-disambig}}) 04:43, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}) 04:44, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}) 04:45, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}) 04:51, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 communication: yes} 04:54, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Implausible references: Test} 04:55, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Implausible references} 04:55, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Nonsensical references} 04:56, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Nonsensical references: Test} 04:56, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}) 05:00, 24 August 2025 (UTC)
# [[:User:NovemBot/sandbox]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); notified {{user|1=NovemBot}} 05:01, 24 August 2025 (UTC)
=== October 2025 ===
# [[:Test9030]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); notified {{user|1=NovemBot}} 18:47, 27 October 2025 (UTC)
=== November 2025 ===
# [[:User:NovemBot]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: rusukr} 23:20, 2 November 2025 (UTC)
# [[:User:NovemBot]]: [[WP:CSD#U6|CSD U6]] ({{tl|db-u6}}); notified {{user|1=NovemBot}} 23:23, 2 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#U7|CSD U7]] ({{tl|db-u7}}); notified {{user|1=NovemBot}} 23:26, 2 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#U7|CSD U7]] ({{tl|db-u7}}); additional information: {U7 creative: yes}; notified {{user|1=NovemBot}} 23:28, 2 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#U7|CSD U7]] ({{tl|db-u7}}); additional information: {U7 creative: yes} {U7 professional: yes} {U7 personal: yes} {U7 links: yes}; notified {{user|1=NovemBot}} 23:29, 2 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-banned}}) 06:05, 4 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: a-i}; notified {{user|1=NovemBot}} 06:06, 4 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#U7|CSD U7]] ({{tl|db-u7}}); additional information: {U7 creative: yes} {U7 personal: yes}; notified {{user|1=NovemBot}} 06:19, 4 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: a-a}; notified {{user|1=NovemBot}} 07:00, 19 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: a-a}; notified {{user|1=NovemBot}} 07:07, 19 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-banned}}); additional information: {G5 user: [[:User:Test]]} 07:09, 19 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: a-a}; notified {{user|1=NovemBot}} 07:23, 19 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G4|CSD G4]] ({{tl|db-repost}}); additional information: {G4 xfd: [[:Test]]}; notified {{user|1=NovemBot}} 07:26, 19 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: a-a}; notified {{user|1=NovemBot}} 07:32, 19 November 2025 (UTC)
# [[:TimedText:File:Foldergs.ogv.en.srt]]: [[WP:CSD#G8|CSD G8]] ({{tl|db-timedtext}}) 20:10, 19 November 2025 (UTC)
# [[:TimedText:File:Foldergs.ogv.en.srt]]: [[WP:CSD#G8|CSD G8]] ({{tl|db-timedtext}}) 20:24, 19 November 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G2|CSD G2]] ({{tl|db-test}}) 20:59, 22 November 2025 (UTC)
=== December 2025 ===
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 reason: Test 1} 08:31, 14 December 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 communication: yes} 08:31, 14 December 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Implausible references: Test 3} 08:32, 14 December 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Nonsensical references: Test 4} 08:33, 14 December 2025 (UTC)
=== January 2026 ===
# [[:Mainspace]]: [[WP:CSD#G11|CSD G11]] ({{tl|db-spam}}) 18:57, 4 January 2026 (UTC)
=== May 2026 ===
# [[:NovemTest110]]: [[WP:CSD#A10|CSD A10]] ({{tl|db-a10}}); additional information: {A10 article: [[:Test]]}; notified {{user|1=NovemBot}} 08:52, 1 May 2026 (UTC)
# [[:Mainspace]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: kurd} 09:29, 1 May 2026 (UTC)
# [[:NovemTest110]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: kurd}; notified {{user|1=NovemBot}} 09:46, 1 May 2026 (UTC)
qt4uto03p9tt737uh4yq1njdua3cus2
739961
739946
2026-05-01T10:56:16Z
Novem Linguae
49714
Logging speedy deletion nomination of [[:NovemTest110]].
739961
wikitext
text/x-wiki
This is a log of all [[WP:CSD|speedy deletion]] nominations made by this user using [[WP:TW|Twinkle]]'s CSD module.
If you no longer wish to keep this log, you can turn it off using the [[Wikipedia:Twinkle/Preferences|preferences panel]], and nominate this page for speedy deletion under [[WP:CSD#U1|CSD U1]].
This log does not track outright speedy deletions made using Twinkle.
=== April 2022 ===
# [[:Draft:Test]]: [[WP:CSD#G2|CSD G2]] ({{tl|db-test}}) 22:35, 24 April 2022 (UTC)
# [[:Draft:Test]]: [[WP:G11]] ({{tl|db-spam}}) 22:40, 24 April 2022 (UTC)
=== May 2022 ===
# [[:Dradt:Cirrhilabrus humanni]]: multiple criteria ([[WP:CSD#R2|R2]], [[WP:CSD#G6|G6]]); additional information: {G6 rationale: test} 18:07, 20 May 2022 (UTC)
=== June 2022 ===
# [[:Draft:Test097097]]: multiple criteria ([[WP:CSD#G2|G2]], [[WP:CSD#G11|G11]]); notified {{user|1=NovemBot}} 05:03, 2 June 2022 (UTC)
# [[:Draft:Test097097]]: multiple criteria ([[WP:CSD#G2|G2]], [[WP:CSD#G11|G11]]); notified {{user|1=NovemBot}} 05:06, 2 June 2022 (UTC)
=== September 2024 ===
# [[:Draft:Test]]: [[WP:G12]] ({{tl|db-copyvio}}); notified {{user|1=NovemBot}} 18:58, 18 September 2024 (UTC)
=== August 2025 ===
# [[:Mainspace]]: [[WP:CSD#A3|CSD A3]] ({{tl|db-nocontent}}) 04:43, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G14|CSD G14]] ({{tl|db-disambig}}) 04:43, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}) 04:44, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}) 04:45, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}) 04:51, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 communication: yes} 04:54, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Implausible references: Test} 04:55, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Implausible references} 04:55, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Nonsensical references} 04:56, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Nonsensical references: Test} 04:56, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}) 05:00, 24 August 2025 (UTC)
# [[:User:NovemBot/sandbox]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); notified {{user|1=NovemBot}} 05:01, 24 August 2025 (UTC)
=== October 2025 ===
# [[:Test9030]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); notified {{user|1=NovemBot}} 18:47, 27 October 2025 (UTC)
=== November 2025 ===
# [[:User:NovemBot]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: rusukr} 23:20, 2 November 2025 (UTC)
# [[:User:NovemBot]]: [[WP:CSD#U6|CSD U6]] ({{tl|db-u6}}); notified {{user|1=NovemBot}} 23:23, 2 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#U7|CSD U7]] ({{tl|db-u7}}); notified {{user|1=NovemBot}} 23:26, 2 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#U7|CSD U7]] ({{tl|db-u7}}); additional information: {U7 creative: yes}; notified {{user|1=NovemBot}} 23:28, 2 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#U7|CSD U7]] ({{tl|db-u7}}); additional information: {U7 creative: yes} {U7 professional: yes} {U7 personal: yes} {U7 links: yes}; notified {{user|1=NovemBot}} 23:29, 2 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-banned}}) 06:05, 4 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: a-i}; notified {{user|1=NovemBot}} 06:06, 4 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#U7|CSD U7]] ({{tl|db-u7}}); additional information: {U7 creative: yes} {U7 personal: yes}; notified {{user|1=NovemBot}} 06:19, 4 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: a-a}; notified {{user|1=NovemBot}} 07:00, 19 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: a-a}; notified {{user|1=NovemBot}} 07:07, 19 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-banned}}); additional information: {G5 user: [[:User:Test]]} 07:09, 19 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: a-a}; notified {{user|1=NovemBot}} 07:23, 19 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G4|CSD G4]] ({{tl|db-repost}}); additional information: {G4 xfd: [[:Test]]}; notified {{user|1=NovemBot}} 07:26, 19 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: a-a}; notified {{user|1=NovemBot}} 07:32, 19 November 2025 (UTC)
# [[:TimedText:File:Foldergs.ogv.en.srt]]: [[WP:CSD#G8|CSD G8]] ({{tl|db-timedtext}}) 20:10, 19 November 2025 (UTC)
# [[:TimedText:File:Foldergs.ogv.en.srt]]: [[WP:CSD#G8|CSD G8]] ({{tl|db-timedtext}}) 20:24, 19 November 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G2|CSD G2]] ({{tl|db-test}}) 20:59, 22 November 2025 (UTC)
=== December 2025 ===
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 reason: Test 1} 08:31, 14 December 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 communication: yes} 08:31, 14 December 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Implausible references: Test 3} 08:32, 14 December 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Nonsensical references: Test 4} 08:33, 14 December 2025 (UTC)
=== January 2026 ===
# [[:Mainspace]]: [[WP:CSD#G11|CSD G11]] ({{tl|db-spam}}) 18:57, 4 January 2026 (UTC)
=== May 2026 ===
# [[:NovemTest110]]: [[WP:CSD#A10|CSD A10]] ({{tl|db-a10}}); additional information: {A10 article: [[:Test]]}; notified {{user|1=NovemBot}} 08:52, 1 May 2026 (UTC)
# [[:Mainspace]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: kurd} 09:29, 1 May 2026 (UTC)
# [[:NovemTest110]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: kurd}; notified {{user|1=NovemBot}} 09:46, 1 May 2026 (UTC)
# [[:NovemTest110]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: kurd}; notified {{user|1=NovemBot}} 10:56, 1 May 2026 (UTC)
2tngp2pi47timsqt18176c42mjibsz4
739965
739961
2026-05-01T10:58:15Z
Novem Linguae
49714
Logging speedy deletion nomination of [[:NovemTest110]].
739965
wikitext
text/x-wiki
This is a log of all [[WP:CSD|speedy deletion]] nominations made by this user using [[WP:TW|Twinkle]]'s CSD module.
If you no longer wish to keep this log, you can turn it off using the [[Wikipedia:Twinkle/Preferences|preferences panel]], and nominate this page for speedy deletion under [[WP:CSD#U1|CSD U1]].
This log does not track outright speedy deletions made using Twinkle.
=== April 2022 ===
# [[:Draft:Test]]: [[WP:CSD#G2|CSD G2]] ({{tl|db-test}}) 22:35, 24 April 2022 (UTC)
# [[:Draft:Test]]: [[WP:G11]] ({{tl|db-spam}}) 22:40, 24 April 2022 (UTC)
=== May 2022 ===
# [[:Dradt:Cirrhilabrus humanni]]: multiple criteria ([[WP:CSD#R2|R2]], [[WP:CSD#G6|G6]]); additional information: {G6 rationale: test} 18:07, 20 May 2022 (UTC)
=== June 2022 ===
# [[:Draft:Test097097]]: multiple criteria ([[WP:CSD#G2|G2]], [[WP:CSD#G11|G11]]); notified {{user|1=NovemBot}} 05:03, 2 June 2022 (UTC)
# [[:Draft:Test097097]]: multiple criteria ([[WP:CSD#G2|G2]], [[WP:CSD#G11|G11]]); notified {{user|1=NovemBot}} 05:06, 2 June 2022 (UTC)
=== September 2024 ===
# [[:Draft:Test]]: [[WP:G12]] ({{tl|db-copyvio}}); notified {{user|1=NovemBot}} 18:58, 18 September 2024 (UTC)
=== August 2025 ===
# [[:Mainspace]]: [[WP:CSD#A3|CSD A3]] ({{tl|db-nocontent}}) 04:43, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G14|CSD G14]] ({{tl|db-disambig}}) 04:43, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}) 04:44, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}) 04:45, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}) 04:51, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 communication: yes} 04:54, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Implausible references: Test} 04:55, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Implausible references} 04:55, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Nonsensical references} 04:56, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Nonsensical references: Test} 04:56, 24 August 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}) 05:00, 24 August 2025 (UTC)
# [[:User:NovemBot/sandbox]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); notified {{user|1=NovemBot}} 05:01, 24 August 2025 (UTC)
=== October 2025 ===
# [[:Test9030]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); notified {{user|1=NovemBot}} 18:47, 27 October 2025 (UTC)
=== November 2025 ===
# [[:User:NovemBot]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: rusukr} 23:20, 2 November 2025 (UTC)
# [[:User:NovemBot]]: [[WP:CSD#U6|CSD U6]] ({{tl|db-u6}}); notified {{user|1=NovemBot}} 23:23, 2 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#U7|CSD U7]] ({{tl|db-u7}}); notified {{user|1=NovemBot}} 23:26, 2 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#U7|CSD U7]] ({{tl|db-u7}}); additional information: {U7 creative: yes}; notified {{user|1=NovemBot}} 23:28, 2 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#U7|CSD U7]] ({{tl|db-u7}}); additional information: {U7 creative: yes} {U7 professional: yes} {U7 personal: yes} {U7 links: yes}; notified {{user|1=NovemBot}} 23:29, 2 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-banned}}) 06:05, 4 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: a-i}; notified {{user|1=NovemBot}} 06:06, 4 November 2025 (UTC)
# [[:User:NovemBot/Subpage]]: [[WP:CSD#U7|CSD U7]] ({{tl|db-u7}}); additional information: {U7 creative: yes} {U7 personal: yes}; notified {{user|1=NovemBot}} 06:19, 4 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: a-a}; notified {{user|1=NovemBot}} 07:00, 19 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: a-a}; notified {{user|1=NovemBot}} 07:07, 19 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-banned}}); additional information: {G5 user: [[:User:Test]]} 07:09, 19 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: a-a}; notified {{user|1=NovemBot}} 07:23, 19 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G4|CSD G4]] ({{tl|db-repost}}); additional information: {G4 xfd: [[:Test]]}; notified {{user|1=NovemBot}} 07:26, 19 November 2025 (UTC)
# [[:Muzaffar Derefshi]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: a-a}; notified {{user|1=NovemBot}} 07:32, 19 November 2025 (UTC)
# [[:TimedText:File:Foldergs.ogv.en.srt]]: [[WP:CSD#G8|CSD G8]] ({{tl|db-timedtext}}) 20:10, 19 November 2025 (UTC)
# [[:TimedText:File:Foldergs.ogv.en.srt]]: [[WP:CSD#G8|CSD G8]] ({{tl|db-timedtext}}) 20:24, 19 November 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G2|CSD G2]] ({{tl|db-test}}) 20:59, 22 November 2025 (UTC)
=== December 2025 ===
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 reason: Test 1} 08:31, 14 December 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 communication: yes} 08:31, 14 December 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Implausible references: Test 3} 08:32, 14 December 2025 (UTC)
# [[:Mainspace]]: [[WP:CSD#G15|CSD G15]] ({{tl|db-llm}}); additional information: {G15 references: yes} {G15 reason: Nonsensical references: Test 4} 08:33, 14 December 2025 (UTC)
=== January 2026 ===
# [[:Mainspace]]: [[WP:CSD#G11|CSD G11]] ({{tl|db-spam}}) 18:57, 4 January 2026 (UTC)
=== May 2026 ===
# [[:NovemTest110]]: [[WP:CSD#A10|CSD A10]] ({{tl|db-a10}}); additional information: {A10 article: [[:Test]]}; notified {{user|1=NovemBot}} 08:52, 1 May 2026 (UTC)
# [[:Mainspace]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: kurd} 09:29, 1 May 2026 (UTC)
# [[:NovemTest110]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: kurd}; notified {{user|1=NovemBot}} 09:46, 1 May 2026 (UTC)
# [[:NovemTest110]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: kurd}; notified {{user|1=NovemBot}} 10:56, 1 May 2026 (UTC)
# [[:NovemTest110]]: [[WP:CSD#G5|CSD G5]] ({{tl|db-gs}}); additional information: {G5 code: kurd}; notified {{user|1=NovemBot}} 10:58, 1 May 2026 (UTC)
df7hg71mpt7qy0pwly8610k2s44jhko
Wikipedia:Village pump/topic list
4
146208
739884
739726
2026-04-30T15:28:54Z
Cewbot
33876
[[User:Cewbot/log/20170915/configuration|Generate topic list: 6 topics]]
739884
wikitext
text/x-wiki
<!-- This page is auto-generated by bot. Please contact the bot operator to improve the tool. -->
{| class="wikitable sortable mw-collapsible" style="float:left;"
|-
! data-sort-type="number" style="font-weight: normal;" | <small>#</small> !! 💭 Title !! <span title="Count of comments">💬</span> !! <span title="Count of peoples in discussion">👥</span> !! 🙋 Last editor !! data-sort-type="isoDate" | <span title="Date/Time">🕒 <small>(UTC)</small></span>
|-
| style="text-align: right;" | 1
| [[Wikipedia:Village pump#Script|Script]]
| style="text-align: right;" | 8
| style="text-align: right;" | 5
| style="background-color: #bbb;" | [[User:LuniZunie|LuniZunie]]
| style="background-color: #bbb;" data-sort-type="isoDate" data-sort-value="2025-11-09T16:47:00.000Z" | 2025-11-09 <span style="color: blue;">16:47</span>
|-
| style="text-align: right;" | 2
| [[Wikipedia:Village pump#Report_concerning_Tanbiruzzammn|Report concerning Tanbiruzzammn]]
| style="text-align: right;" | 2
| style="text-align: right;" | 2
| style="background-color: #bbb;" | [[User:Barras|Barras]]
| style="background-color: #bbb;" data-sort-type="isoDate" data-sort-value="2025-12-09T21:45:00.000Z" | 2025-12-09 <span style="color: blue;">21:45</span>
|-
| style="text-align: right;" | 3
| [[Wikipedia:Village pump#Report_concerning_Bucheon|Report concerning Bucheon]]
| style="text-align: right;background-color: #fcc;" | 1
| style="text-align: right;background-color: #fcc;" | 1
| style="background-color: #bbb;" | [[User:PieWriter|PieWriter]]
| style="background-color: #bbb;" data-sort-type="isoDate" data-sort-value="2026-02-19T10:25:00.000Z" | 2026-02-19 <span style="color: blue;">10:25</span>
|-
| style="text-align: right;" | 4
| [[Wikipedia:Village pump#Versions_and_dates|Versions and dates]]
| style="text-align: right;" | 2
| style="text-align: right;background-color: #fcc;" | 1
| style="background-color: #bbb;" | [[Special:Contributions/~2026-13668-13|<span style="color: #c20;">~2026-13668-13</span>]]
| style="background-color: #bbb;" data-sort-type="isoDate" data-sort-value="2026-03-03T06:17:00.000Z" | 2026-03-03 <span style="color: blue;">06:17</span>
|-
| style="text-align: right;" | 5
| style="max-width: 24em" | <small>[[Wikipedia:Village pump#Upcoming_Wikimedia_Café_meetup_regarding_the_the_2026-2027_Wikimedia_Foundation_Annual_Plan|Upcoming Wikimedia Café meetup regarding the the 2026-2027 Wikimedia Foundation Annual Plan]]</small>
| style="text-align: right;background-color: #fcc;" | 1
| style="text-align: right;background-color: #fcc;" | 1
| style="background-color: #bbb;" | [[User:Pine|Pine]]
| style="background-color: #bbb;" data-sort-type="isoDate" data-sort-value="2026-03-30T03:46:00.000Z" | 2026-03-30 <span style="color: blue;">03:46</span>
|-
| style="text-align: right;" | 6
| [[Wikipedia:Village pump#Changes_to_electionadmin_userright|Changes to electionadmin userright]]
| style="text-align: right;" | 4
| style="text-align: right;" | 4
| style="background-color: #ddd;" | [[User:Chaotic Enby|Chaotic Enby]]
| style="background-color: #ddd;" data-sort-type="isoDate" data-sort-value="2026-04-23T15:22:00.000Z" | 2026-04-23 <span style="color: blue;">15:22</span>
|}
{| class="wikitable mw-collapsible mw-collapsed" style="float: left; margin-left: .5em;;{{#if:{{{no_time_legend|}}}|display:none;|}}"
! title="From the latest bot edit" | Legend
|-
| style="background-color: #efe;" |
* In the last hour
|-
| style="background-color: #eef;" |
* In the last day
|-
| |
* In the last week
|-
| style="background-color: #ddd;" |
* In the last month
|-
| style="background-color: #bbb;" |
* More than one month
|-
! Manual settings
|-
| style="max-width: 12em;" | <small>When exceptions occur,<br />please check [[User:Cewbot/log/20170915/configuration|the setting]] first.</small>
|-
|}
{{Clear}}
2jq3mx2ge2qh7vxantuwybzlvam73cd
User:Ponor/inline-diff-inline-patrol.js
2
156221
739882
728616
2026-04-30T14:09:05Z
Ponor
47975
button click delegation; some refactoring for "open 10 consecutive with Ctrl- and Shift-click"
739882
javascript
text/javascript
/*
Inline [diff] for revisions in RECENT CHANGES, WATCHLIST, PAGE HISTORY and USER CONTRIBUTIONS.
Adds [patrol] buttons for unpatrolled edits for users with sysop or patrol rights.
Inspired by Bradv's Expand Diffs, Writ Keeper's Common History, and Ivi1o4's Unpatrolled User Contributions scripts.
Author:Ponor (2024-06-10)
Documentation: [[User:Ponor/inline-diff-inline-patrol]]
Style sheet: [[User:Ponor/inline-diff-inline-patrol.css]]
*/
$(document).ready(function() {
//user groups
const patroller = mw.config.get("wgUserGroups").includes("patroller");
const sysop = mw.config.get("wgUserGroups").includes("sysop");
//which page are we on?
const RC = mw.config.get("wgCanonicalSpecialPageName") == "Recentchanges";
const UC = mw.config.get("wgCanonicalSpecialPageName") == "Contributions" || mw.config.get("wgCanonicalSpecialPageName") == "IPContributions";
const WL = mw.config.get("wgCanonicalSpecialPageName") == "Watchlist";
const PH = mw.config.get("wgAction") == "history";
const WIKI = mw.config.get('wgDBname');
const PAGE = (RC && "RC") || (UC && "UC") || (WL && "WL") || (PH && "PH");
if (window.inline_patrol_running ||
//!(patroller || sysop) ||
!(RC || UC || PH || WL)
) {
return;
} //get out of here
//------------------------------------ Get and set styles
//diff_styles: mw.loader.load('mediawiki.diff.styles'),
mw.loader.load('https://www.mediawiki.org/w/load.php?modules=mediawiki.diff.styles&only=styles', 'text/css');
mw.loader.load('https://meta.wikimedia.org/wiki/User:Ponor/inline-diff-inline-patrol.css?action=raw&ctype=text/css', 'text/css');
let conf = {
patrol_button: "patrol",
patrol_button_hint: "mark as patrolled: revision #",
patrol_successful: "Patrolling revision #", //pop-up notification
patrol_error: "Error patrolling revision #", //pop-up notification
patrol_disabled: false, //disable [patrol] buttons
diff_button: "diff",
diff_button_hint: "show diff",
diff_type: "table", //inline, table
max_diff_height: "40em",
watchlist_timestamp_disabled: false,
newer_older_navigation_disabled: false,
}; //conf
//------------------------------------ Helper functions
function statsUpd(s, clear = false) {
const newStats = clear ? 0 : 1 + statsGet(s);
mw.storage.set("inlineDiffInlinePatrol-" + s, newStats, 2 * 24 * 3600);
}
function statsGet(s) {
return Number(mw.storage.get("inlineDiffInlinePatrol-" + s)) || 0;
}
// === NEW: STRING & PARSING HELPERS EXTRACTED FROM diffClick ===
function proper_nesting(m, p1, p2, p3, link_class, prefix = '') {
let link = p2;
let s_start = p1;
if (p2.search(/^[^⥅⥆⥴⥳]+?⥆/) >= 0) {
s_start = s_start + "⥆";
link = "⥅" + link;
}
if (p2.search(/^[^⥅⥆⥴⥳]+?⥳/) >= 0) {
s_start = s_start + "⥳";
link = "⥴" + link;
}
let s_end = p3;
if (p2.search(/⥅[^⥅⥆⥴⥳]+$/) >= 0) {
s_end = "⥅" + s_end;
link = link + "⥆";
}
if (p2.search(/⥴[^⥅⥆⥴⥳]+$/) >= 0) {
s_end = "⥴" + s_end;
link = link + "⥳";
}
return s_start + '<a class="' + link_class + '"target="_blank" href="/wiki/' + prefix + p2.replaceAll(/[⥅⥆⥴⥳]/g, '').replaceAll(' ', '_') + '">' + link + '</a>' + s_end;
}
function linked_wl(m, p1, p2, p3) {
return proper_nesting(m, p1, p2, p3, "ip-diff-wl", "");
}
function linked_tl(m, p1, p2, p3) {
return proper_nesting(m, p1, p2, p3, "ip-diff-tl", "Template:");
}
function linked_url(m) {
return '<a class="ip-diff-url" target="_blank" href="' + m.replaceAll(/[⥅⥆⥴⥳]/g, '') + '">' + m + '</a>';
}
// === NEW: ASYNC FETCH & RENDER LOGIC ===
async function processDiff(button, parent) {
let status = button.data("status");
if (status === "shown") {
return; // Already open, do nothing
} else if (status === "hidden") {
parent.children("div.ip-diff-div").eq(0).show();
button.text(conf.diff_button + "↑").data("status", "shown");
return; // Unhidden, no API call needed
}
statsUpd('diff');
statsUpd(PAGE);
button.text("..."); // Visual cue that loading is happening
try {
const revid = button.data("revid");
const data = await get_diff(revid);
let diff_data = data.compare["*"];
if (conf.diff_type == 'table') {
let del_tag = '<del class="diffchange diffchange-inline">';
let ins_tag = '<ins class="diffchange diffchange-inline">';
diff_data = diff_data
.replaceAll(del_tag, '⥴').replaceAll('</del>', '⥳')
.replaceAll(ins_tag, '⥅').replaceAll('</ins>', '⥆')
.replaceAll('<', '¦<')
.replaceAll(/https?:\/\/[^\|\}\]¦\s]+/g, linked_url)
.replaceAll('¦<', '<')
.replaceAll(/(\[\[ *[⥅⥆⥴⥳]? *)([^\]\|]+?)( *[⥅⥆⥴⥳]? *[\]\|])/g, linked_wl)
.replaceAll(/(?<!\{)(\{\{(?![#{]) *[⥅⥆⥴⥳]? *)([^\}\|\n<]+?)( *[⥅⥆⥴⥳]? *[\}\|\n<])/g, linked_tl)
.replaceAll("⥅⥆", "").replaceAll("⥴⥳", "")
.replaceAll('⥴', del_tag).replaceAll('⥳', '</del>')
.replaceAll('⥅', ins_tag).replaceAll('⥆', '</ins>');
}
let diff_table = `<table class="diff">\n<colgroup>\n <col class="diff-marker">\n <col class="diff-content">\n <col class="diff-marker">\n <col class="diff-content">\n</colgroup>\n<tbody>\n${diff_data}\n</tbody>\n</table>`;
let diff_div = $('<div/>');
let diff_div_top = $("<div/>").addClass("ip-diff-topdiv").data("revid", revid);
let diff_div_table = $("<div/>").addClass("ip-diff-table").html(diff_table);
diff_div
.addClass("ip-diff-div")
.css({
"max-height": conf.max_diff_height,
"overflow-y": "auto"
})
.append(diff_div_top)
.append(diff_div_table)
.on("dblclick", function() {
$(this).hide();
button.data("status", "hidden").text(conf.diff_button + "↓");
statsUpd('dblhide');
});
parent.append(diff_div);
button.data("status", "shown").text(conf.diff_button + "↑");
mw.hook('userjs.inline_patrol.fetch_diff').fire("fetched", diff_div);
} catch (err) {
console.error("(inline-patrol) Error fetching diff:", err);
button.text("Error");
}
}
// === END NEW SECTIONS ===
//------------------------------------ MAIN
function on_ready() {
//the script should not be loaded twice
window.inline_patrol_running = true;
if (window.ip_configuration) {
for (const c in conf) {
conf[c] = window.ip_configuration[c] || conf[c];
}
}
//Finally
function initialize() {
if (!conf.patrol_disabled && (patroller || sysop)) {
$("li[data-mw-revid].mw-changeslist-reviewstatus-unpatrolled").each(patrolButton);
}
$("li[data-mw-revid]").each(diffButton);
if (statsGet('patrol') > 0) {
mw.track('stats.mediawiki_gadget_inlineDiffInlinePatrol_patrol_total', statsGet('patrol'), {
wiki: WIKI
});
statsUpd('patrol', true);
}
if (statsGet('diff') > 0) {
mw.track('stats.mediawiki_gadget_inlineDiffInlinePatrol_diff_total', statsGet('diff'), {
wiki: WIKI
});
statsUpd('diff', true);
}
for (const pg of ['RC', 'WL', 'PH', 'UC']) {
if (statsGet(pg) > 0) {
mw.track('stats.mediawiki_gadget_inlineDiffInlinePatrol_pages_total', statsGet(pg), {
page: pg
});
statsUpd(pg, true);
}
}
for (const ft of ['timestamp', 'navigate', 'hide', 'dblhide', 'hidesame', 'hideall']) {
if (statsGet(ft) > 0) {
mw.track('stats.mediawiki_gadget_inlineDiffInlinePatrol_features_total', statsGet(ft), {
feature: ft
});
statsUpd(ft, true)
}
}
} //function initialize
//Recent Changes and Watchlist
if (RC || WL) {
initialize();
//mw.hook because RC page likes to update itself
mw.hook("wikipage.content").add(function(el) {
if (el.hasClass('mw-changeslist')) {
initialize();
}
});
}
if ((WL || RC) && !conf.newer_older_navigation_disabled) {
mw.loader.load('https://test.wikipedia.org/wiki/User:Ponor/inline-diff-newerolder-navigation.js?action=raw&ctype=text/javascript', 'text/javascript');
console.log('loading watchlist_timestamp');
}
if ((WL || RC) && !conf.watchlist_timestamp_disabled) {
mw.loader.load('https://test.wikipedia.org/wiki/User:Ponor/inline-diff-watchlist-timestamp.js?action=raw&ctype=text/javascript', 'text/javascript');
console.log('loading watchlist_timestamp');
}
if (PH && !conf.watchlist_timestamp_disabled) {
mw.loader.load('https://test.wikipedia.org/wiki/User:Ponor/inline-diff-watchedpage-timestamp.js?action=raw&ctype=text/javascript', 'text/javascript');
console.log('loading watchedpage_timestamp');
}
//User Contributions and Page History
if (UC || PH) {
get_unpatrolled();
mw.hook('userjs.inline_patrol.get_patrol_data').add(function(status) {
initialize();
});
}
} //function on_ready
//------------------------------------ Buttons
function diffButton() {
let revid = $(this).attr("data-mw-revid");
//console.log(revid)
if (revid) {
let button = $('<span/>');
button.addClass("ip-button ip-diff")
.text(conf.diff_button + "↓")
.attr("title", conf.diff_button_hint)
.data("revid", revid);
if (RC || WL) {
button.insertBefore($("span.mw-changeslist-line-inner", $(this)));
} else if (PH || UC) {
button.prependTo($(this));
}
}
} //function diffButton
function patrolButton() {
let revid = $(this).attr("data-mw-revid");
if (revid) {
let button = $('<span/>');
button.addClass("ip-button ip-unpatrolled")
.text(conf.patrol_button)
.attr("title", conf.patrol_button_hint + revid)
.data("revid", revid)
.on("click", patrolClick);
if (RC || WL) {
button.insertAfter($("span.mw-changeslist-separator", $(this)).eq(1));
} else if (PH || UC) {
button.insertAfter($("span.mw-changeslist-separator", $(this)).eq(0));
}
}
} //function patrolButton
//------------------------------------ mw.API calls
//mw.API patrol call
function patrol(revid) {
console.log("(inline-patrol) patrolling revid ", revid);
let mwPromise = new mw.Api().postWithToken('patrol', {
'action': 'patrol',
'format': 'json',
'revid': revid
});
return mwPromise;
}
//mw.API diff call
function get_diff(revid) {
console.log("(inline-patrol) getting diff for ", revid);
let mwPromise = new mw.Api().get({
'action': 'compare',
'format': 'json',
'uselang': 'content',
'maxage': '7200',
'smaxage': '7200', //cache results for 2h
'difftype': conf.diff_type, //inline, table, unified
'torelative': 'prev',
'fromrev': revid
});
return mwPromise;
} //get_diff
//mw.API call for unpatrolled edits on Page History and User Contribution pages
function get_unpatrolled() {
let rcKey, rcValue;
if (UC) {
rcKey = "rcuser";
rcValue = mw.config.get("wgRelevantUserName");
}
if (PH) {
rcKey = "rctitle";
rcValue = mw.config.get("wgPageName");
}
if (UC || PH) {
let unpatrolledRevisionsPromise = new mw.Api().get({
"action": "query",
"format": "json",
"list": "recentchanges",
"rcprop": "ids|patrolled",
"rcshow": "unpatrolled",
"rclimit": "123",
[rcKey]: rcValue //mdn: computed property names
});
unpatrolledRevisionsPromise.done(function(jsondata) {
$.each(jsondata.query.recentchanges, function(i, rc) {
$("li[data-mw-revid=" + rc.revid + "]").addClass("mw-changeslist-reviewstatus-unpatrolled"); //same as Recent Changes <li's>
});
mw.hook('userjs.inline_patrol.get_patrol_data').fire("done");
});
unpatrolledRevisionsPromise.fail(function(jsondata) {
mw.hook('userjs.inline_patrol.get_patrol_data').fire("failed");
});
}
} //function get_unpatrolled
//------------------------------------ Button click handlers
//[patrol] button click handler
function patrolClick(event) {
let button = $(this);
const revid = button.data("revid");
const mwPromise = patrol(revid);
mwPromise.done((data) => {
mw.notify(conf.patrol_successful + revid, {
'type': 'success'
});
statsUpd('patrol');
button
.removeClass("ip-unpatrolled").addClass("ip-patrolled")
.off(); //no clicks
});
mwPromise.fail((data) => {
mw.notify(conf.patrol_error + revid, {
'type': 'error'
});
button
.removeClass("ip-unpatrolled").addClass("ip-error")
.off(); //no clicks
});
event.stopPropagation();
} //function patrolClick
// === MODIFIED: NEW ASYNC diffClick LOGIC WITH LOOP ===
async function diffClick(event) {
let button = $(this);
let parent = button.closest("li"); // Safely gets the <li> element
// 1. UI styling for selected line (Original Logic)
if (RC || WL) {
let nsclass = parent.attr('class').match(/mw-changeslist-ns\d+-[^\s]+/);
$("li[data-mw-revid]").css({
"border-right": "none"
});
if (nsclass) {
$(`li.${nsclass[0]}`).css({
"border-right": "5px solid red",
"border-radius": "4px"
});
}
}
let status = button.data("status");
// 2. EXISTING BEHAVIOR: Hiding open diffs
if (status === "shown") {
let list = parent;
if (event.shiftKey && (RC || WL)) {
let match = parent.attr('class').match(/mw-changeslist-ns\d+-[^\s]+/);
if (match) list = $(`.${match[0]}`); // hide same title
statsUpd('hidesame');
} else if (event.ctrlKey && (RC || WL)) {
list = $('.mw-changeslist-line'); // hide all
statsUpd('hideall');
} else if ((event.shiftKey || event.ctrlKey) && (PH || UC)) {
list = $("li[data-mw-revid]"); // hide all on PH/UC pages
statsUpd('hideall');
}
statsUpd('hide');
list.each(function() {
$(this).children("div.ip-diff-div").hide();
let btn = $(this).find("span.ip-diff").eq(0);
if (btn.data("status") === "shown") {
btn.text(conf.diff_button + "↓").data("status", "hidden");
}
});
event.stopPropagation();
return;
}
// 3. NEW BEHAVIOR: Smart Expansion of Next 10
if (event.shiftKey || event.ctrlKey) {
let allRows = $("li[data-mw-revid]");
let targetRows = allRows; // Default for Ctrl
if (event.shiftKey && (RC || WL)) {
let nsclass = parent.attr('class').match(/mw-changeslist-ns\d+-[^\s]+/);
if (nsclass) {
targetRows = $(`li.${nsclass[0]}`); // Restrict to same title
}
}
let currentIndex = targetRows.index(parent);
let nextTenRows = targetRows.slice(currentIndex, currentIndex + 10);
for (let i = 0; i < nextTenRows.length; i++) {
let row = $(nextTenRows[i]);
let btn = row.find(".ip-diff").eq(0);
await processDiff(btn, row);
}
} else {
// Normal click without modifiers
await processDiff(button, parent);
}
event.stopPropagation();
}
// === END MODIFIED diffClick ===
// === NEW: GLOBAL EVENT DELEGATION ===
$(document).on("click", ".ip-diff", diffClick);
// Call on_ready at the end
on_ready();
});
4lkhbaxxgs77pspmourhrvrs0bonqwe
739883
739882
2026-04-30T14:20:26Z
Ponor
47975
don't like this
739883
javascript
text/javascript
/*
Inline [diff] for revisions in RECENT CHANGES, WATCHLIST, PAGE HISTORY and USER CONTRIBUTIONS.
Adds [patrol] buttons for unpatrolled edits for users with sysop or patrol rights.
Inspired by Bradv's Expand Diffs, Writ Keeper's Common History, and Ivi1o4's Unpatrolled User Contributions scripts.
Author:Ponor (2024-06-10)
Documentation: [[User:Ponor/inline-diff-inline-patrol]]
Style sheet: [[User:Ponor/inline-diff-inline-patrol.css]]
*/
$(document).ready(function() {
//user groups
const patroller = mw.config.get("wgUserGroups").includes("patroller");
const sysop = mw.config.get("wgUserGroups").includes("sysop");
//which page are we on?
const RC = mw.config.get("wgCanonicalSpecialPageName") == "Recentchanges";
const UC = mw.config.get("wgCanonicalSpecialPageName") == "Contributions" || mw.config.get("wgCanonicalSpecialPageName") == "IPContributions";
const WL = mw.config.get("wgCanonicalSpecialPageName") == "Watchlist";
const PH = mw.config.get("wgAction") == "history";
const WIKI = mw.config.get('wgDBname');
const PAGE = (RC && "RC") || (UC && "UC") || (WL && "WL") || (PH && "PH");
if (window.inline_patrol_running ||
//!(patroller || sysop) ||
!(RC || UC || PH || WL)
) {
return;
} //get out of here
//------------------------------------ Get and set styles
//diff_styles: mw.loader.load('mediawiki.diff.styles'),
mw.loader.load('https://www.mediawiki.org/w/load.php?modules=mediawiki.diff.styles&only=styles', 'text/css');
mw.loader.load('https://meta.wikimedia.org/wiki/User:Ponor/inline-diff-inline-patrol.css?action=raw&ctype=text/css', 'text/css');
let conf = {
patrol_button: "patrol",
patrol_button_hint: "mark as patrolled: revision #",
patrol_successful: "Patrolling revision #", //pop-up notification
patrol_error: "Error patrolling revision #", //pop-up notification
patrol_disabled: false, //disable [patrol] buttons
diff_button: "diff",
diff_button_hint: "show diff",
diff_type: "table", //inline, table
max_diff_height: "40em",
watchlist_timestamp_disabled: false,
newer_older_navigation_disabled: false,
}; //conf
//------------------------------------ Helper functions
function statsUpd(s, clear = false) {
const newStats = clear ? 0 : 1 + statsGet(s);
mw.storage.set("inlineDiffInlinePatrol-" + s, newStats, 2 * 24 * 3600);
}
function statsGet(s) {
return Number(mw.storage.get("inlineDiffInlinePatrol-" + s)) || 0;
}
// === NEW: STRING & PARSING HELPERS EXTRACTED FROM diffClick ===
function proper_nesting(m, p1, p2, p3, link_class, prefix = '') {
let link = p2;
let s_start = p1;
if (p2.search(/^[^⥅⥆⥴⥳]+?⥆/) >= 0) {
s_start = s_start + "⥆";
link = "⥅" + link;
}
if (p2.search(/^[^⥅⥆⥴⥳]+?⥳/) >= 0) {
s_start = s_start + "⥳";
link = "⥴" + link;
}
let s_end = p3;
if (p2.search(/⥅[^⥅⥆⥴⥳]+$/) >= 0) {
s_end = "⥅" + s_end;
link = link + "⥆";
}
if (p2.search(/⥴[^⥅⥆⥴⥳]+$/) >= 0) {
s_end = "⥴" + s_end;
link = link + "⥳";
}
return s_start + '<a class="' + link_class + '"target="_blank" href="/wiki/' + prefix + p2.replaceAll(/[⥅⥆⥴⥳]/g, '').replaceAll(' ', '_') + '">' + link + '</a>' + s_end;
}
function linked_wl(m, p1, p2, p3) {
return proper_nesting(m, p1, p2, p3, "ip-diff-wl", "");
}
function linked_tl(m, p1, p2, p3) {
return proper_nesting(m, p1, p2, p3, "ip-diff-tl", "Template:");
}
function linked_url(m) {
return '<a class="ip-diff-url" target="_blank" href="' + m.replaceAll(/[⥅⥆⥴⥳]/g, '') + '">' + m + '</a>';
}
// === NEW: ASYNC FETCH & RENDER LOGIC ===
async function processDiff(button, parent) {
let status = button.data("status");
if (status === "shown") {
return; // Already open, do nothing
} else if (status === "hidden") {
parent.children("div.ip-diff-div").eq(0).show();
button.text(conf.diff_button + "↑").data("status", "shown");
return; // Unhidden, no API call needed
}
statsUpd('diff');
statsUpd(PAGE);
try {
const revid = button.data("revid");
const data = await get_diff(revid);
let diff_data = data.compare["*"];
if (conf.diff_type == 'table') {
let del_tag = '<del class="diffchange diffchange-inline">';
let ins_tag = '<ins class="diffchange diffchange-inline">';
diff_data = diff_data
.replaceAll(del_tag, '⥴').replaceAll('</del>', '⥳')
.replaceAll(ins_tag, '⥅').replaceAll('</ins>', '⥆')
.replaceAll('<', '¦<')
.replaceAll(/https?:\/\/[^\|\}\]¦\s]+/g, linked_url)
.replaceAll('¦<', '<')
.replaceAll(/(\[\[ *[⥅⥆⥴⥳]? *)([^\]\|]+?)( *[⥅⥆⥴⥳]? *[\]\|])/g, linked_wl)
.replaceAll(/(?<!\{)(\{\{(?![#{]) *[⥅⥆⥴⥳]? *)([^\}\|\n<]+?)( *[⥅⥆⥴⥳]? *[\}\|\n<])/g, linked_tl)
.replaceAll("⥅⥆", "").replaceAll("⥴⥳", "")
.replaceAll('⥴', del_tag).replaceAll('⥳', '</del>')
.replaceAll('⥅', ins_tag).replaceAll('⥆', '</ins>');
}
let diff_table = `<table class="diff">\n<colgroup>\n <col class="diff-marker">\n <col class="diff-content">\n <col class="diff-marker">\n <col class="diff-content">\n</colgroup>\n<tbody>\n${diff_data}\n</tbody>\n</table>`;
let diff_div = $('<div/>');
let diff_div_top = $("<div/>").addClass("ip-diff-topdiv").data("revid", revid);
let diff_div_table = $("<div/>").addClass("ip-diff-table").html(diff_table);
diff_div
.addClass("ip-diff-div")
.css({
"max-height": conf.max_diff_height,
"overflow-y": "auto"
})
.append(diff_div_top)
.append(diff_div_table)
.on("dblclick", function() {
$(this).hide();
button.data("status", "hidden").text(conf.diff_button + "↓");
statsUpd('dblhide');
});
parent.append(diff_div);
button.data("status", "shown").text(conf.diff_button + "↑");
mw.hook('userjs.inline_patrol.fetch_diff').fire("fetched", diff_div);
} catch (err) {
console.error("(inline-patrol) Error fetching diff:", err);
button.text("Error");
}
}
// === END NEW SECTIONS ===
//------------------------------------ MAIN
function on_ready() {
//the script should not be loaded twice
window.inline_patrol_running = true;
if (window.ip_configuration) {
for (const c in conf) {
conf[c] = window.ip_configuration[c] || conf[c];
}
}
//Finally
function initialize() {
if (!conf.patrol_disabled && (patroller || sysop)) {
$("li[data-mw-revid].mw-changeslist-reviewstatus-unpatrolled").each(patrolButton);
}
$("li[data-mw-revid]").each(diffButton);
if (statsGet('patrol') > 0) {
mw.track('stats.mediawiki_gadget_inlineDiffInlinePatrol_patrol_total', statsGet('patrol'), {
wiki: WIKI
});
statsUpd('patrol', true);
}
if (statsGet('diff') > 0) {
mw.track('stats.mediawiki_gadget_inlineDiffInlinePatrol_diff_total', statsGet('diff'), {
wiki: WIKI
});
statsUpd('diff', true);
}
for (const pg of ['RC', 'WL', 'PH', 'UC']) {
if (statsGet(pg) > 0) {
mw.track('stats.mediawiki_gadget_inlineDiffInlinePatrol_pages_total', statsGet(pg), {
page: pg
});
statsUpd(pg, true);
}
}
for (const ft of ['timestamp', 'navigate', 'hide', 'dblhide', 'hidesame', 'hideall']) {
if (statsGet(ft) > 0) {
mw.track('stats.mediawiki_gadget_inlineDiffInlinePatrol_features_total', statsGet(ft), {
feature: ft
});
statsUpd(ft, true)
}
}
} //function initialize
//Recent Changes and Watchlist
if (RC || WL) {
initialize();
//mw.hook because RC page likes to update itself
mw.hook("wikipage.content").add(function(el) {
if (el.hasClass('mw-changeslist')) {
initialize();
}
});
}
if ((WL || RC) && !conf.newer_older_navigation_disabled) {
mw.loader.load('https://test.wikipedia.org/wiki/User:Ponor/inline-diff-newerolder-navigation.js?action=raw&ctype=text/javascript', 'text/javascript');
console.log('loading watchlist_timestamp');
}
if ((WL || RC) && !conf.watchlist_timestamp_disabled) {
mw.loader.load('https://test.wikipedia.org/wiki/User:Ponor/inline-diff-watchlist-timestamp.js?action=raw&ctype=text/javascript', 'text/javascript');
console.log('loading watchlist_timestamp');
}
if (PH && !conf.watchlist_timestamp_disabled) {
mw.loader.load('https://test.wikipedia.org/wiki/User:Ponor/inline-diff-watchedpage-timestamp.js?action=raw&ctype=text/javascript', 'text/javascript');
console.log('loading watchedpage_timestamp');
}
//User Contributions and Page History
if (UC || PH) {
get_unpatrolled();
mw.hook('userjs.inline_patrol.get_patrol_data').add(function(status) {
initialize();
});
}
} //function on_ready
//------------------------------------ Buttons
function diffButton() {
let revid = $(this).attr("data-mw-revid");
//console.log(revid)
if (revid) {
let button = $('<span/>');
button.addClass("ip-button ip-diff")
.text(conf.diff_button + "↓")
.attr("title", conf.diff_button_hint)
.data("revid", revid);
if (RC || WL) {
button.insertBefore($("span.mw-changeslist-line-inner", $(this)));
} else if (PH || UC) {
button.prependTo($(this));
}
}
} //function diffButton
function patrolButton() {
let revid = $(this).attr("data-mw-revid");
if (revid) {
let button = $('<span/>');
button.addClass("ip-button ip-unpatrolled")
.text(conf.patrol_button)
.attr("title", conf.patrol_button_hint + revid)
.data("revid", revid)
.on("click", patrolClick);
if (RC || WL) {
button.insertAfter($("span.mw-changeslist-separator", $(this)).eq(1));
} else if (PH || UC) {
button.insertAfter($("span.mw-changeslist-separator", $(this)).eq(0));
}
}
} //function patrolButton
//------------------------------------ mw.API calls
//mw.API patrol call
function patrol(revid) {
console.log("(inline-patrol) patrolling revid ", revid);
let mwPromise = new mw.Api().postWithToken('patrol', {
'action': 'patrol',
'format': 'json',
'revid': revid
});
return mwPromise;
}
//mw.API diff call
function get_diff(revid) {
console.log("(inline-patrol) getting diff for ", revid);
let mwPromise = new mw.Api().get({
'action': 'compare',
'format': 'json',
'uselang': 'content',
'maxage': '7200',
'smaxage': '7200', //cache results for 2h
'difftype': conf.diff_type, //inline, table, unified
'torelative': 'prev',
'fromrev': revid
});
return mwPromise;
} //get_diff
//mw.API call for unpatrolled edits on Page History and User Contribution pages
function get_unpatrolled() {
let rcKey, rcValue;
if (UC) {
rcKey = "rcuser";
rcValue = mw.config.get("wgRelevantUserName");
}
if (PH) {
rcKey = "rctitle";
rcValue = mw.config.get("wgPageName");
}
if (UC || PH) {
let unpatrolledRevisionsPromise = new mw.Api().get({
"action": "query",
"format": "json",
"list": "recentchanges",
"rcprop": "ids|patrolled",
"rcshow": "unpatrolled",
"rclimit": "123",
[rcKey]: rcValue //mdn: computed property names
});
unpatrolledRevisionsPromise.done(function(jsondata) {
$.each(jsondata.query.recentchanges, function(i, rc) {
$("li[data-mw-revid=" + rc.revid + "]").addClass("mw-changeslist-reviewstatus-unpatrolled"); //same as Recent Changes <li's>
});
mw.hook('userjs.inline_patrol.get_patrol_data').fire("done");
});
unpatrolledRevisionsPromise.fail(function(jsondata) {
mw.hook('userjs.inline_patrol.get_patrol_data').fire("failed");
});
}
} //function get_unpatrolled
//------------------------------------ Button click handlers
//[patrol] button click handler
function patrolClick(event) {
let button = $(this);
const revid = button.data("revid");
const mwPromise = patrol(revid);
mwPromise.done((data) => {
mw.notify(conf.patrol_successful + revid, {
'type': 'success'
});
statsUpd('patrol');
button
.removeClass("ip-unpatrolled").addClass("ip-patrolled")
.off(); //no clicks
});
mwPromise.fail((data) => {
mw.notify(conf.patrol_error + revid, {
'type': 'error'
});
button
.removeClass("ip-unpatrolled").addClass("ip-error")
.off(); //no clicks
});
event.stopPropagation();
} //function patrolClick
// === MODIFIED: NEW ASYNC diffClick LOGIC WITH LOOP ===
async function diffClick(event) {
let button = $(this);
let parent = button.closest("li"); // Safely gets the <li> element
// 1. UI styling for selected line (Original Logic)
if (RC || WL) {
let nsclass = parent.attr('class').match(/mw-changeslist-ns\d+-[^\s]+/);
$("li[data-mw-revid]").css({
"border-right": "none"
});
if (nsclass) {
$(`li.${nsclass[0]}`).css({
"border-right": "5px solid red",
"border-radius": "4px"
});
}
}
let status = button.data("status");
// 2. EXISTING BEHAVIOR: Hiding open diffs
if (status === "shown") {
let list = parent;
if (event.shiftKey && (RC || WL)) {
let match = parent.attr('class').match(/mw-changeslist-ns\d+-[^\s]+/);
if (match) list = $(`.${match[0]}`); // hide same title
statsUpd('hidesame');
} else if (event.ctrlKey && (RC || WL)) {
list = $('.mw-changeslist-line'); // hide all
statsUpd('hideall');
} else if ((event.shiftKey || event.ctrlKey) && (PH || UC)) {
list = $("li[data-mw-revid]"); // hide all on PH/UC pages
statsUpd('hideall');
}
statsUpd('hide');
list.each(function() {
$(this).children("div.ip-diff-div").hide();
let btn = $(this).find("span.ip-diff").eq(0);
if (btn.data("status") === "shown") {
btn.text(conf.diff_button + "↓").data("status", "hidden");
}
});
event.stopPropagation();
return;
}
// 3. NEW BEHAVIOR: Smart Expansion of Next 10
if (event.shiftKey || event.ctrlKey) {
let allRows = $("li[data-mw-revid]");
let targetRows = allRows; // Default for Ctrl
if (event.shiftKey && (RC || WL)) {
let nsclass = parent.attr('class').match(/mw-changeslist-ns\d+-[^\s]+/);
if (nsclass) {
targetRows = $(`li.${nsclass[0]}`); // Restrict to same title
}
}
let currentIndex = targetRows.index(parent);
let nextTenRows = targetRows.slice(currentIndex, currentIndex + 10);
for (let i = 0; i < nextTenRows.length; i++) {
let row = $(nextTenRows[i]);
let btn = row.find(".ip-diff").eq(0);
await processDiff(btn, row);
}
} else {
// Normal click without modifiers
await processDiff(button, parent);
}
event.stopPropagation();
}
// === END MODIFIED diffClick ===
// === NEW: GLOBAL EVENT DELEGATION ===
$(document).on("click", ".ip-diff", diffClick);
// Call on_ready at the end
on_ready();
});
2fvje37uwut3zth4grobgd98vhs2axx
739885
739883
2026-04-30T16:07:28Z
Ponor
47975
739885
javascript
text/javascript
/*
Inline [diff] for revisions in RECENT CHANGES, WATCHLIST, PAGE HISTORY and USER CONTRIBUTIONS.
Adds [patrol] buttons for unpatrolled edits for users with sysop or patrol rights.
Inspired by Bradv's Expand Diffs, Writ Keeper's Common History, and Ivi1o4's Unpatrolled User Contributions scripts.
Author:Ponor (2024-06-10)
Documentation: [[User:Ponor/inline-diff-inline-patrol]]
Style sheet: [[User:Ponor/inline-diff-inline-patrol.css]]
*/
$(document).ready(function() {
//user groups
const patroller = mw.config.get("wgUserGroups").includes("patroller");
const sysop = mw.config.get("wgUserGroups").includes("sysop");
//which page are we on?
const RC = mw.config.get("wgCanonicalSpecialPageName") == "Recentchanges";
const UC = mw.config.get("wgCanonicalSpecialPageName") == "Contributions" || mw.config.get("wgCanonicalSpecialPageName") == "IPContributions";
const WL = mw.config.get("wgCanonicalSpecialPageName") == "Watchlist";
const PH = mw.config.get("wgAction") == "history";
const WIKI = mw.config.get('wgDBname');
const PAGE = (RC && "RC") || (UC && "UC") || (WL && "WL") || (PH && "PH");
if (window.inline_patrol_running ||
//!(patroller || sysop) ||
!(RC || UC || PH || WL)
) {
return;
} //get out of here
//------------------------------------ Get and set styles
//diff_styles: mw.loader.load('mediawiki.diff.styles'),
mw.loader.load('https://www.mediawiki.org/w/load.php?modules=mediawiki.diff.styles&only=styles', 'text/css');
mw.loader.load('https://meta.wikimedia.org/wiki/User:Ponor/inline-diff-inline-patrol.css?action=raw&ctype=text/css', 'text/css');
let conf = {
patrol_button: "patrol",
patrol_button_hint: "mark as patrolled: revision #",
patrol_successful: "Patrolling revision #", //pop-up notification
patrol_error: "Error patrolling revision #", //pop-up notification
patrol_disabled: false, //disable [patrol] buttons
diff_button: "diff",
diff_button_hint: "show diff",
diff_type: "table", //inline, table
max_diff_height: "40em",
watchlist_timestamp_disabled: false,
newer_older_navigation_disabled: false,
}; //conf
//------------------------------------ Helper functions
function statsUpd(s, clear = false) {
const newStats = clear ? 0 : 1 + statsGet(s);
mw.storage.set("inlineDiffInlinePatrol-" + s, newStats, 2 * 24 * 3600);
}
function statsGet(s) {
return Number(mw.storage.get("inlineDiffInlinePatrol-" + s)) || 0;
}
function proper_nesting(m, p1, p2, p3, link_class, prefix = '') {
let link = p2;
let s_start = p1;
if (p2.search(/^[^⥅⥆⥴⥳]+?⥆/) >= 0) {
s_start = s_start + "⥆";
link = "⥅" + link;
}
if (p2.search(/^[^⥅⥆⥴⥳]+?⥳/) >= 0) {
s_start = s_start + "⥳";
link = "⥴" + link;
}
let s_end = p3;
if (p2.search(/⥅[^⥅⥆⥴⥳]+$/) >= 0) {
s_end = "⥅" + s_end;
link = link + "⥆";
}
if (p2.search(/⥴[^⥅⥆⥴⥳]+$/) >= 0) {
s_end = "⥴" + s_end;
link = link + "⥳";
}
return s_start + '<a class="' + link_class + '"target="_blank" href="/wiki/' + prefix + p2.replaceAll(/[⥅⥆⥴⥳]/g, '').replaceAll(' ', '_') + '">' + link + '</a>' + s_end;
}
function linked_wl(m, p1, p2, p3) {
return proper_nesting(m, p1, p2, p3, "ip-diff-wl", "");
}
function linked_tl(m, p1, p2, p3) {
return proper_nesting(m, p1, p2, p3, "ip-diff-tl", "Template:");
}
function linked_url(m) {
return '<a class="ip-diff-url" target="_blank" href="' + m.replaceAll(/[⥅⥆⥴⥳]/g, '') + '">' + m + '</a>';
}
async function processDiff(button, parent) {
let status = button.data("status");
if (status === "shown") {
return; // Already open, do nothing
} else if (status === "hidden") {
parent.children("div.ip-diff-div").eq(0).show();
button.text(conf.diff_button + "↑").data("status", "shown");
return; // Unhidden, no API call needed
}
statsUpd('diff');
statsUpd(PAGE);
try {
const revid = button.data("revid");
const data = await get_diff(revid);
let diff_data = data.compare["*"];
if (conf.diff_type == 'table') {
let del_tag = '<del class="diffchange diffchange-inline">';
let ins_tag = '<ins class="diffchange diffchange-inline">';
diff_data = diff_data
.replaceAll(del_tag, '⥴').replaceAll('</del>', '⥳')
.replaceAll(ins_tag, '⥅').replaceAll('</ins>', '⥆')
.replaceAll('<', '¦<')
.replaceAll(/https?:\/\/[^\|\}\]¦\s]+/g, linked_url)
.replaceAll('¦<', '<')
.replaceAll(/(\[\[ *[⥅⥆⥴⥳]? *)([^\]\|]+?)( *[⥅⥆⥴⥳]? *[\]\|])/g, linked_wl)
.replaceAll(/(?<!\{)(\{\{(?![#{]) *[⥅⥆⥴⥳]? *)([^\}\|\n<]+?)( *[⥅⥆⥴⥳]? *[\}\|\n<])/g, linked_tl)
.replaceAll("⥅⥆", "").replaceAll("⥴⥳", "")
.replaceAll('⥴', del_tag).replaceAll('⥳', '</del>')
.replaceAll('⥅', ins_tag).replaceAll('⥆', '</ins>');
}
let diff_table = `<table class="diff">\n<colgroup>\n <col class="diff-marker">\n <col class="diff-content">\n <col class="diff-marker">\n <col class="diff-content">\n</colgroup>\n<tbody>\n${diff_data}\n</tbody>\n</table>`;
let diff_div = $('<div/>');
let diff_div_top = $("<div/>").addClass("ip-diff-topdiv").data("revid", revid);
let diff_div_table = $("<div/>").addClass("ip-diff-table").html(diff_table);
diff_div
.addClass("ip-diff-div")
.css({
"max-height": conf.max_diff_height,
"overflow-y": "auto"
})
.append(diff_div_top)
.append(diff_div_table)
.on("dblclick", function() {
$(this).hide();
button.data("status", "hidden").text(conf.diff_button + "↓");
statsUpd('dblhide');
});
parent.append(diff_div);
button.data("status", "shown").text(conf.diff_button + "↑");
mw.hook('userjs.inline_patrol.fetch_diff').fire("fetched", diff_div);
} catch (err) {
console.error("(inline-patrol) Error fetching diff:", err);
button.text("Error");
}
}
//------------------------------------ MAIN
function on_ready() {
//the script should not be loaded twice
window.inline_patrol_running = true;
if (window.ip_configuration) {
for (const c in conf) {
conf[c] = window.ip_configuration[c] || conf[c];
}
}
//Finally
function initialize() {
if (!conf.patrol_disabled && (patroller || sysop)) {
$("li[data-mw-revid].mw-changeslist-reviewstatus-unpatrolled").each(patrolButton);
}
$("li[data-mw-revid]").each(diffButton);
if (statsGet('patrol') > 0) {
mw.track('stats.mediawiki_gadget_inlineDiffInlinePatrol_patrol_total', statsGet('patrol'), {
wiki: WIKI
});
statsUpd('patrol', true);
}
if (statsGet('diff') > 0) {
mw.track('stats.mediawiki_gadget_inlineDiffInlinePatrol_diff_total', statsGet('diff'), {
wiki: WIKI
});
statsUpd('diff', true);
}
for (const pg of ['RC', 'WL', 'PH', 'UC']) {
if (statsGet(pg) > 0) {
mw.track('stats.mediawiki_gadget_inlineDiffInlinePatrol_pages_total', statsGet(pg), {
page: pg
});
statsUpd(pg, true);
}
}
for (const ft of ['timestamp', 'navigate', 'hide', 'dblhide', 'hidesame', 'hideall']) {
if (statsGet(ft) > 0) {
mw.track('stats.mediawiki_gadget_inlineDiffInlinePatrol_features_total', statsGet(ft), {
feature: ft
});
statsUpd(ft, true)
}
}
} //function initialize
//Recent Changes and Watchlist
if (RC || WL) {
initialize();
//mw.hook because RC page likes to update itself
mw.hook("wikipage.content").add(function(el) {
if (el.hasClass('mw-changeslist')) {
initialize();
}
});
}
if ((WL || RC) && !conf.newer_older_navigation_disabled) {
mw.loader.load('https://test.wikipedia.org/wiki/User:Ponor/inline-diff-newerolder-navigation.js?action=raw&ctype=text/javascript', 'text/javascript');
console.log('loading watchlist_timestamp');
}
if ((WL || RC) && !conf.watchlist_timestamp_disabled) {
mw.loader.load('https://test.wikipedia.org/wiki/User:Ponor/inline-diff-watchlist-timestamp.js?action=raw&ctype=text/javascript', 'text/javascript');
console.log('loading watchlist_timestamp');
}
if (PH && !conf.watchlist_timestamp_disabled) {
mw.loader.load('https://test.wikipedia.org/wiki/User:Ponor/inline-diff-watchedpage-timestamp.js?action=raw&ctype=text/javascript', 'text/javascript');
console.log('loading watchedpage_timestamp');
}
//User Contributions and Page History
if (UC || PH) {
get_unpatrolled();
mw.hook('userjs.inline_patrol.get_patrol_data').add(function(status) {
initialize();
});
}
} //function on_ready
//------------------------------------ Buttons
function diffButton() {
let revid = $(this).attr("data-mw-revid");
//console.log(revid)
if (revid) {
let button = $('<span/>');
button.addClass("ip-button ip-diff")
.text(conf.diff_button + "↓")
.attr("title", conf.diff_button_hint)
.data("revid", revid);
if (RC || WL) {
button.insertBefore($("span.mw-changeslist-line-inner", $(this)));
} else if (PH || UC) {
button.prependTo($(this));
}
}
} //function diffButton
function patrolButton() {
let revid = $(this).attr("data-mw-revid");
if (revid) {
let button = $('<span/>');
button.addClass("ip-button ip-unpatrolled")
.text(conf.patrol_button)
.attr("title", conf.patrol_button_hint + revid)
.data("revid", revid)
.on("click", patrolClick);
if (RC || WL) {
button.insertAfter($("span.mw-changeslist-separator", $(this)).eq(1));
} else if (PH || UC) {
button.insertAfter($("span.mw-changeslist-separator", $(this)).eq(0));
}
}
} //function patrolButton
//------------------------------------ mw.API calls
//mw.API patrol call
function patrol(revid) {
console.log("(inline-patrol) patrolling revid ", revid);
let mwPromise = new mw.Api().postWithToken('patrol', {
'action': 'patrol',
'format': 'json',
'revid': revid
});
return mwPromise;
}
//mw.API diff call
function get_diff(revid) {
console.log("(inline-patrol) getting diff for ", revid);
let mwPromise = new mw.Api().get({
'action': 'compare',
'format': 'json',
'uselang': 'content',
'maxage': '7200',
'smaxage': '7200', //cache results for 2h
'difftype': conf.diff_type, //inline, table, unified
'torelative': 'prev',
'fromrev': revid
});
return mwPromise;
} //get_diff
//mw.API call for unpatrolled edits on Page History and User Contribution pages
function get_unpatrolled() {
let rcKey, rcValue;
if (UC) {
rcKey = "rcuser";
rcValue = mw.config.get("wgRelevantUserName");
}
if (PH) {
rcKey = "rctitle";
rcValue = mw.config.get("wgPageName");
}
if (UC || PH) {
let unpatrolledRevisionsPromise = new mw.Api().get({
"action": "query",
"format": "json",
"list": "recentchanges",
"rcprop": "ids|patrolled",
"rcshow": "unpatrolled",
"rclimit": "123",
[rcKey]: rcValue //mdn: computed property names
});
unpatrolledRevisionsPromise.done(function(jsondata) {
$.each(jsondata.query.recentchanges, function(i, rc) {
$("li[data-mw-revid=" + rc.revid + "]").addClass("mw-changeslist-reviewstatus-unpatrolled"); //same as Recent Changes <li's>
});
mw.hook('userjs.inline_patrol.get_patrol_data').fire("done");
});
unpatrolledRevisionsPromise.fail(function(jsondata) {
mw.hook('userjs.inline_patrol.get_patrol_data').fire("failed");
});
}
} //function get_unpatrolled
//------------------------------------ Button click handlers
//[patrol] button click handler
function patrolClick(event) {
let button = $(this);
const revid = button.data("revid");
const mwPromise = patrol(revid);
mwPromise.done((data) => {
mw.notify(conf.patrol_successful + revid, {
'type': 'success'
});
statsUpd('patrol');
button
.removeClass("ip-unpatrolled").addClass("ip-patrolled")
.off(); //no clicks
});
mwPromise.fail((data) => {
mw.notify(conf.patrol_error + revid, {
'type': 'error'
});
button
.removeClass("ip-unpatrolled").addClass("ip-error")
.off(); //no clicks
});
event.stopPropagation();
} //function patrolClick
async function diffClick(event) {
let button = $(this);
let parent = button.closest("li"); // Safely gets the <li> element
// 1. UI styling for selected line (Original Logic)
if (RC || WL) {
let nsclass = parent.attr('class').match(/mw-changeslist-ns\d+-[^\s]+/);
$("li[data-mw-revid]").css({
"border-right": "none"
});
if (nsclass) {
$(`li.${nsclass[0]}`).css({
"border-right": "5px solid red",
"border-radius": "4px"
});
}
}
let status = button.data("status");
// 2. EXISTING BEHAVIOR: Hiding open diffs
if (status === "shown") {
let list = parent;
if (event.shiftKey && (RC || WL)) {
let match = parent.attr('class').match(/mw-changeslist-ns\d+-[^\s]+/);
if (match) list = $(`.${match[0]}`); // hide same title
statsUpd('hidesame');
} else if (event.ctrlKey && (RC || WL)) {
list = $('.mw-changeslist-line'); // hide all
statsUpd('hideall');
} else if ((event.shiftKey || event.ctrlKey) && (PH || UC)) {
list = $("li[data-mw-revid]"); // hide all on PH/UC pages
statsUpd('hideall');
}
statsUpd('hide');
list.each(function() {
$(this).children("div.ip-diff-div").hide();
let btn = $(this).find("span.ip-diff").eq(0);
if (btn.data("status") === "shown") {
btn.text(conf.diff_button + "↓").data("status", "hidden");
}
});
event.stopPropagation();
return;
}
// 3. NEW BEHAVIOR: Smart Expansion of Next 10
if (event.shiftKey || event.ctrlKey) {
let allRows = $("li[data-mw-revid]");
let targetRows = allRows; // Default for Ctrl
if (event.shiftKey && (RC || WL)) {
let nsclass = parent.attr('class').match(/mw-changeslist-ns\d+-[^\s]+/);
if (nsclass) {
targetRows = $(`li.${nsclass[0]}`); // Restrict to same title
}
}
let currentIndex = targetRows.index(parent);
let nextTenRows = targetRows.slice(currentIndex, currentIndex + 10);
for (let i = 0; i < nextTenRows.length; i++) {
let row = $(nextTenRows[i]);
let btn = row.find(".ip-diff").eq(0);
await processDiff(btn, row);
}
} else {
// Normal click without modifiers
await processDiff(button, parent);
}
event.stopPropagation();
}
$(document).on("click", ".ip-diff", diffClick);
// Call on_ready at the end
on_ready();
});
5vcgomrqi3flq3i1txgp74r25lah2i3
739886
739885
2026-04-30T16:17:58Z
Ponor
47975
patrol button click delegation
739886
javascript
text/javascript
/*
Inline [diff] for revisions in RECENT CHANGES, WATCHLIST, PAGE HISTORY and USER CONTRIBUTIONS.
Adds [patrol] buttons for unpatrolled edits for users with sysop or patrol rights.
Inspired by Bradv's Expand Diffs, Writ Keeper's Common History, and Ivi1o4's Unpatrolled User Contributions scripts.
Author:Ponor (2024-06-10)
Documentation: [[User:Ponor/inline-diff-inline-patrol]]
Style sheet: [[User:Ponor/inline-diff-inline-patrol.css]]
*/
$(document).ready(function() {
//user groups
const patroller = mw.config.get("wgUserGroups").includes("patroller");
const sysop = mw.config.get("wgUserGroups").includes("sysop");
//which page are we on?
const RC = mw.config.get("wgCanonicalSpecialPageName") == "Recentchanges";
const UC = mw.config.get("wgCanonicalSpecialPageName") == "Contributions" || mw.config.get("wgCanonicalSpecialPageName") == "IPContributions";
const WL = mw.config.get("wgCanonicalSpecialPageName") == "Watchlist";
const PH = mw.config.get("wgAction") == "history";
const WIKI = mw.config.get('wgDBname');
const PAGE = (RC && "RC") || (UC && "UC") || (WL && "WL") || (PH && "PH");
if (window.inline_patrol_running ||
//!(patroller || sysop) ||
!(RC || UC || PH || WL)
) {
return;
} //get out of here
//------------------------------------ Get and set styles
//diff_styles: mw.loader.load('mediawiki.diff.styles'),
mw.loader.load('https://www.mediawiki.org/w/load.php?modules=mediawiki.diff.styles&only=styles', 'text/css');
mw.loader.load('https://meta.wikimedia.org/wiki/User:Ponor/inline-diff-inline-patrol.css?action=raw&ctype=text/css', 'text/css');
let conf = {
patrol_button: "patrol",
patrol_button_hint: "mark as patrolled: revision #",
patrol_successful: "Patrolling revision #", //pop-up notification
patrol_error: "Error patrolling revision #", //pop-up notification
patrol_disabled: false, //disable [patrol] buttons
diff_button: "diff",
diff_button_hint: "show diff",
diff_type: "table", //inline, table
max_diff_height: "40em",
watchlist_timestamp_disabled: false,
newer_older_navigation_disabled: false,
}; //conf
//------------------------------------ Helper functions
function statsUpd(s, clear = false) {
const newStats = clear ? 0 : 1 + statsGet(s);
mw.storage.set("inlineDiffInlinePatrol-" + s, newStats, 2 * 24 * 3600);
}
function statsGet(s) {
return Number(mw.storage.get("inlineDiffInlinePatrol-" + s)) || 0;
}
function proper_nesting(m, p1, p2, p3, link_class, prefix = '') {
let link = p2;
let s_start = p1;
if (p2.search(/^[^⥅⥆⥴⥳]+?⥆/) >= 0) {
s_start = s_start + "⥆";
link = "⥅" + link;
}
if (p2.search(/^[^⥅⥆⥴⥳]+?⥳/) >= 0) {
s_start = s_start + "⥳";
link = "⥴" + link;
}
let s_end = p3;
if (p2.search(/⥅[^⥅⥆⥴⥳]+$/) >= 0) {
s_end = "⥅" + s_end;
link = link + "⥆";
}
if (p2.search(/⥴[^⥅⥆⥴⥳]+$/) >= 0) {
s_end = "⥴" + s_end;
link = link + "⥳";
}
return s_start + '<a class="' + link_class + '"target="_blank" href="/wiki/' + prefix + p2.replaceAll(/[⥅⥆⥴⥳]/g, '').replaceAll(' ', '_') + '">' + link + '</a>' + s_end;
}
function linked_wl(m, p1, p2, p3) {
return proper_nesting(m, p1, p2, p3, "ip-diff-wl", "");
}
function linked_tl(m, p1, p2, p3) {
return proper_nesting(m, p1, p2, p3, "ip-diff-tl", "Template:");
}
function linked_url(m) {
return '<a class="ip-diff-url" target="_blank" href="' + m.replaceAll(/[⥅⥆⥴⥳]/g, '') + '">' + m + '</a>';
}
async function processDiff(button, parent) {
let status = button.data("status");
if (status === "shown") {
return; // Already open, do nothing
} else if (status === "hidden") {
parent.children("div.ip-diff-div").eq(0).show();
button.text(conf.diff_button + "↑").data("status", "shown");
return; // Unhidden, no API call needed
}
statsUpd('diff');
statsUpd(PAGE);
try {
const revid = button.data("revid");
const data = await get_diff(revid);
let diff_data = data.compare["*"];
if (conf.diff_type == 'table') {
let del_tag = '<del class="diffchange diffchange-inline">';
let ins_tag = '<ins class="diffchange diffchange-inline">';
diff_data = diff_data
.replaceAll(del_tag, '⥴').replaceAll('</del>', '⥳')
.replaceAll(ins_tag, '⥅').replaceAll('</ins>', '⥆')
.replaceAll('<', '¦<')
.replaceAll(/https?:\/\/[^\|\}\]¦\s]+/g, linked_url)
.replaceAll('¦<', '<')
.replaceAll(/(\[\[ *[⥅⥆⥴⥳]? *)([^\]\|]+?)( *[⥅⥆⥴⥳]? *[\]\|])/g, linked_wl)
.replaceAll(/(?<!\{)(\{\{(?![#{]) *[⥅⥆⥴⥳]? *)([^\}\|\n<]+?)( *[⥅⥆⥴⥳]? *[\}\|\n<])/g, linked_tl)
.replaceAll("⥅⥆", "").replaceAll("⥴⥳", "")
.replaceAll('⥴', del_tag).replaceAll('⥳', '</del>')
.replaceAll('⥅', ins_tag).replaceAll('⥆', '</ins>');
}
let diff_table = `<table class="diff">\n<colgroup>\n <col class="diff-marker">\n <col class="diff-content">\n <col class="diff-marker">\n <col class="diff-content">\n</colgroup>\n<tbody>\n${diff_data}\n</tbody>\n</table>`;
let diff_div = $('<div/>');
let diff_div_top = $("<div/>").addClass("ip-diff-topdiv").data("revid", revid);
let diff_div_table = $("<div/>").addClass("ip-diff-table").html(diff_table);
diff_div
.addClass("ip-diff-div")
.css({
"max-height": conf.max_diff_height,
"overflow-y": "auto"
})
.append(diff_div_top)
.append(diff_div_table)
.on("dblclick", function() {
$(this).hide();
button.data("status", "hidden").text(conf.diff_button + "↓");
statsUpd('dblhide');
});
parent.append(diff_div);
button.data("status", "shown").text(conf.diff_button + "↑");
mw.hook('userjs.inline_patrol.fetch_diff').fire("fetched", diff_div);
} catch (err) {
console.error("(inline-patrol) Error fetching diff:", err);
button.text("Error");
}
}
//------------------------------------ MAIN
function on_ready() {
//the script should not be loaded twice
window.inline_patrol_running = true;
if (window.ip_configuration) {
for (const c in conf) {
conf[c] = window.ip_configuration[c] || conf[c];
}
}
//Finally
function initialize() {
if (!conf.patrol_disabled && (patroller || sysop)) {
$("li[data-mw-revid].mw-changeslist-reviewstatus-unpatrolled").each(patrolButton);
}
$("li[data-mw-revid]").each(diffButton);
if (statsGet('patrol') > 0) {
mw.track('stats.mediawiki_gadget_inlineDiffInlinePatrol_patrol_total', statsGet('patrol'), {
wiki: WIKI
});
statsUpd('patrol', true);
}
if (statsGet('diff') > 0) {
mw.track('stats.mediawiki_gadget_inlineDiffInlinePatrol_diff_total', statsGet('diff'), {
wiki: WIKI
});
statsUpd('diff', true);
}
for (const pg of ['RC', 'WL', 'PH', 'UC']) {
if (statsGet(pg) > 0) {
mw.track('stats.mediawiki_gadget_inlineDiffInlinePatrol_pages_total', statsGet(pg), {
page: pg
});
statsUpd(pg, true);
}
}
for (const ft of ['timestamp', 'navigate', 'hide', 'dblhide', 'hidesame', 'hideall']) {
if (statsGet(ft) > 0) {
mw.track('stats.mediawiki_gadget_inlineDiffInlinePatrol_features_total', statsGet(ft), {
feature: ft
});
statsUpd(ft, true)
}
}
} //function initialize
//Recent Changes and Watchlist
if (RC || WL) {
initialize();
//mw.hook because RC page likes to update itself
mw.hook("wikipage.content").add(function(el) {
if (el.hasClass('mw-changeslist')) {
initialize();
}
});
}
if ((WL || RC) && !conf.newer_older_navigation_disabled) {
mw.loader.load('https://test.wikipedia.org/wiki/User:Ponor/inline-diff-newerolder-navigation.js?action=raw&ctype=text/javascript', 'text/javascript');
console.log('loading watchlist_timestamp');
}
if ((WL || RC) && !conf.watchlist_timestamp_disabled) {
mw.loader.load('https://test.wikipedia.org/wiki/User:Ponor/inline-diff-watchlist-timestamp.js?action=raw&ctype=text/javascript', 'text/javascript');
console.log('loading watchlist_timestamp');
}
if (PH && !conf.watchlist_timestamp_disabled) {
mw.loader.load('https://test.wikipedia.org/wiki/User:Ponor/inline-diff-watchedpage-timestamp.js?action=raw&ctype=text/javascript', 'text/javascript');
console.log('loading watchedpage_timestamp');
}
//User Contributions and Page History
if (UC || PH) {
get_unpatrolled();
mw.hook('userjs.inline_patrol.get_patrol_data').add(function(status) {
initialize();
});
}
} //function on_ready
//------------------------------------ Buttons
function diffButton() {
let revid = $(this).attr("data-mw-revid");
//console.log(revid)
if (revid) {
let button = $('<span/>');
button.addClass("ip-button ip-diff")
.text(conf.diff_button + "↓")
.attr("title", conf.diff_button_hint)
.data("revid", revid);
if (RC || WL) {
button.insertBefore($("span.mw-changeslist-line-inner", $(this)));
} else if (PH || UC) {
button.prependTo($(this));
}
}
} //function diffButton
function patrolButton() {
let revid = $(this).attr("data-mw-revid");
if (revid) {
let button = $('<span/>');
button.addClass("ip-button ip-unpatrolled")
.text(conf.patrol_button)
.attr("title", conf.patrol_button_hint + revid)
.data("revid", revid);
if (RC || WL) {
button.insertAfter($("span.mw-changeslist-separator", $(this)).eq(1));
} else if (PH || UC) {
button.insertAfter($("span.mw-changeslist-separator", $(this)).eq(0));
}
}
} //function patrolButton
//------------------------------------ mw.API calls
//mw.API patrol call
function patrol(revid) {
console.log("(inline-patrol) patrolling revid ", revid);
let mwPromise = new mw.Api().postWithToken('patrol', {
'action': 'patrol',
'format': 'json',
'revid': revid
});
return mwPromise;
}
//mw.API diff call
function get_diff(revid) {
console.log("(inline-patrol) getting diff for ", revid);
let mwPromise = new mw.Api().get({
'action': 'compare',
'format': 'json',
'uselang': 'content',
'maxage': '7200',
'smaxage': '7200', //cache results for 2h
'difftype': conf.diff_type, //inline, table, unified
'torelative': 'prev',
'fromrev': revid
});
return mwPromise;
} //get_diff
//mw.API call for unpatrolled edits on Page History and User Contribution pages
function get_unpatrolled() {
let rcKey, rcValue;
if (UC) {
rcKey = "rcuser";
rcValue = mw.config.get("wgRelevantUserName");
}
if (PH) {
rcKey = "rctitle";
rcValue = mw.config.get("wgPageName");
}
if (UC || PH) {
let unpatrolledRevisionsPromise = new mw.Api().get({
"action": "query",
"format": "json",
"list": "recentchanges",
"rcprop": "ids|patrolled",
"rcshow": "unpatrolled",
"rclimit": "123",
[rcKey]: rcValue //mdn: computed property names
});
unpatrolledRevisionsPromise.done(function(jsondata) {
$.each(jsondata.query.recentchanges, function(i, rc) {
$("li[data-mw-revid=" + rc.revid + "]").addClass("mw-changeslist-reviewstatus-unpatrolled"); //same as Recent Changes <li's>
});
mw.hook('userjs.inline_patrol.get_patrol_data').fire("done");
});
unpatrolledRevisionsPromise.fail(function(jsondata) {
mw.hook('userjs.inline_patrol.get_patrol_data').fire("failed");
});
}
} //function get_unpatrolled
//------------------------------------ Button click handlers
//[patrol] button click handler
function patrolClick(event) {
let button = $(this);
if (button.data("patrolling")) return;
button.data("patrolling", true);
const revid = button.data("revid");
const mwPromise = patrol(revid);
mwPromise.done((data) => {
mw.notify(conf.patrol_successful + revid, {
'type': 'success'
});
statsUpd('patrol');
button
.removeClass("ip-unpatrolled").addClass("ip-patrolled");
});
mwPromise.fail((data) => {
mw.notify(conf.patrol_error + revid, {
'type': 'error'
});
button
.removeClass("ip-unpatrolled").addClass("ip-error");
button.data("patrolling", false);
});
event.stopPropagation();
} //function patrolClick
async function diffClick(event) {
let button = $(this);
let parent = button.closest("li"); // Safely gets the <li> element
// 1. UI styling for selected line (Original Logic)
if (RC || WL) {
let nsclass = parent.attr('class').match(/mw-changeslist-ns\d+-[^\s]+/);
$("li[data-mw-revid]").css({
"border-right": "none"
});
if (nsclass) {
$(`li.${nsclass[0]}`).css({
"border-right": "5px solid red",
"border-radius": "4px"
});
}
}
let status = button.data("status");
// 2. EXISTING BEHAVIOR: Hiding open diffs
if (status === "shown") {
let list = parent;
if (event.shiftKey && (RC || WL)) {
let match = parent.attr('class').match(/mw-changeslist-ns\d+-[^\s]+/);
if (match) list = $(`.${match[0]}`); // hide same title
statsUpd('hidesame');
} else if (event.ctrlKey && (RC || WL)) {
list = $('.mw-changeslist-line'); // hide all
statsUpd('hideall');
} else if ((event.shiftKey || event.ctrlKey) && (PH || UC)) {
list = $("li[data-mw-revid]"); // hide all on PH/UC pages
statsUpd('hideall');
}
statsUpd('hide');
list.each(function() {
$(this).children("div.ip-diff-div").hide();
let btn = $(this).find("span.ip-diff").eq(0);
if (btn.data("status") === "shown") {
btn.text(conf.diff_button + "↓").data("status", "hidden");
}
});
event.stopPropagation();
return;
}
// 3. NEW BEHAVIOR: Smart Expansion of Next 10
if (event.shiftKey || event.ctrlKey) {
let allRows = $("li[data-mw-revid]");
let targetRows = allRows; // Default for Ctrl
if (event.shiftKey && (RC || WL)) {
let nsclass = parent.attr('class').match(/mw-changeslist-ns\d+-[^\s]+/);
if (nsclass) {
targetRows = $(`li.${nsclass[0]}`); // Restrict to same title
}
}
let currentIndex = targetRows.index(parent);
let nextTenRows = targetRows.slice(currentIndex, currentIndex + 10);
for (let i = 0; i < nextTenRows.length; i++) {
let row = $(nextTenRows[i]);
let btn = row.find(".ip-diff").eq(0);
await processDiff(btn, row);
}
} else {
// Normal click without modifiers
await processDiff(button, parent);
}
event.stopPropagation();
}
$(document).on("click", ".ip-diff", diffClick);
$(document).on("click", ".ip-unpatrolled", patrolClick);
// Call on_ready at the end
on_ready();
});
b1t29amjas8e17hmdc8055ub2b02x2y
User talk:NovemBot
3
160473
739934
739266
2026-05-01T08:52:01Z
Novem Linguae
49714
Notification: speedy deletion nomination of [[:NovemTest110]].
739934
wikitext
text/x-wiki
[[File:Information.svg|25px|alt=Information icon]] Hello, I'm [[User:Novem Linguae|Novem Linguae]]. I wanted to let you know that I reverted one of [[Special:Contributions/NovemBot|your recent contributions]] because it did not appear constructive. If you would like to experiment, please use the [[Wikipedia:Sandbox|sandbox]]. If you think I made a mistake, or if you have any questions, you can leave me a message on [[User_talk:Novem Linguae|my talk page]]. Thanks. <!-- Template:Huggle/warn-1 --><!-- This is a variant of: --><!-- Template:uw-vandalism1 -->–[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 14:55, 24 April 2026 (UTC)
{{subst:db-a10-notice|NovemTest110|nowelcome=|article=Test|{{{key2}}}={{{value2}}}|{{{key3}}}={{{value3}}}}}<!-- Template:Db-csd-notice-custom --> –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 08:52, 1 May 2026 (UTC)
02imrj5vmgdqf89u8vkefl4b0yf3qkf
739945
739934
2026-05-01T09:46:07Z
Novem Linguae
49714
Notification: speedy deletion nomination of [[:NovemTest110]].
739945
wikitext
text/x-wiki
[[File:Information.svg|25px|alt=Information icon]] Hello, I'm [[User:Novem Linguae|Novem Linguae]]. I wanted to let you know that I reverted one of [[Special:Contributions/NovemBot|your recent contributions]] because it did not appear constructive. If you would like to experiment, please use the [[Wikipedia:Sandbox|sandbox]]. If you think I made a mistake, or if you have any questions, you can leave me a message on [[User_talk:Novem Linguae|my talk page]]. Thanks. <!-- Template:Huggle/warn-1 --><!-- This is a variant of: --><!-- Template:uw-vandalism1 -->–[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 14:55, 24 April 2026 (UTC)
{{subst:db-a10-notice|NovemTest110|nowelcome=|article=Test|{{{key2}}}={{{value2}}}|{{{key3}}}={{{value3}}}}}<!-- Template:Db-csd-notice-custom --> –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 08:52, 1 May 2026 (UTC)
==[[Wikipedia:Criteria for speedy deletion|Speedy deletion]] nomination of [[:NovemTest110]]==
[[File:Information icon4.svg|48px|left|alt=|link=]]
Thank you for your interest in [[Kurds]] and [[Kurdistan]]. Due to past disruption, a [[WP:GS|special set of rules]] apply to anyone editing in this topic area. Specifically, only [[Wikipedia:User access levels#Extended confirmed users|extended confirmed]] editors are allowed to [[WP:ARBECR|make direct edits and page creations]] to this topic area. Since you currently do not meet the requirements to contribute to this topic area, the page you created has been nominated for speedy deletion under [[WP:G5|G5]].
If you think this page does not fall under the general sanctions described, you may '''contest the nomination''' by [[:NovemTest110|visiting the page]] and clicking the button labelled "Contest this speedy deletion". This will give you the opportunity to explain why you believe the page should not be deleted. However, be aware that once a page is tagged for speedy deletion, it may be deleted without delay. Please do not remove the speedy deletion tag from the page yourself, but do not hesitate to add information in line with [[Wikipedia:List of policies|Wikipedia's policies and guidelines]]. Additionally you may wish to read about the topic restrictions [[WP:GS/KURD|here]] and [[WP:CT/KURD|here]].<!-- Template:Db-gs-notice --><!-- Template:Db-csd-notice-custom --> –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 09:46, 1 May 2026 (UTC)
ts3xd1tlr6yb5zzm3pwnunn8wbzvnec
739960
739945
2026-05-01T10:56:15Z
Novem Linguae
49714
Notification: speedy deletion nomination of [[:NovemTest110]].
739960
wikitext
text/x-wiki
[[File:Information.svg|25px|alt=Information icon]] Hello, I'm [[User:Novem Linguae|Novem Linguae]]. I wanted to let you know that I reverted one of [[Special:Contributions/NovemBot|your recent contributions]] because it did not appear constructive. If you would like to experiment, please use the [[Wikipedia:Sandbox|sandbox]]. If you think I made a mistake, or if you have any questions, you can leave me a message on [[User_talk:Novem Linguae|my talk page]]. Thanks. <!-- Template:Huggle/warn-1 --><!-- This is a variant of: --><!-- Template:uw-vandalism1 -->–[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 14:55, 24 April 2026 (UTC)
{{subst:db-a10-notice|NovemTest110|nowelcome=|article=Test|{{{key2}}}={{{value2}}}|{{{key3}}}={{{value3}}}}}<!-- Template:Db-csd-notice-custom --> –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 08:52, 1 May 2026 (UTC)
==[[Wikipedia:Criteria for speedy deletion|Speedy deletion]] nomination of [[:NovemTest110]]==
[[File:Information icon4.svg|48px|left|alt=|link=]]
Thank you for your interest in [[Kurds]] and [[Kurdistan]]. Due to past disruption, a [[WP:GS|special set of rules]] apply to anyone editing in this topic area. Specifically, only [[Wikipedia:User access levels#Extended confirmed users|extended confirmed]] editors are allowed to [[WP:ARBECR|make direct edits and page creations]] to this topic area. Since you currently do not meet the requirements to contribute to this topic area, the page you created has been nominated for speedy deletion under [[WP:G5|G5]].
If you think this page does not fall under the general sanctions described, you may '''contest the nomination''' by [[:NovemTest110|visiting the page]] and clicking the button labelled "Contest this speedy deletion". This will give you the opportunity to explain why you believe the page should not be deleted. However, be aware that once a page is tagged for speedy deletion, it may be deleted without delay. Please do not remove the speedy deletion tag from the page yourself, but do not hesitate to add information in line with [[Wikipedia:List of policies|Wikipedia's policies and guidelines]]. Additionally you may wish to read about the topic restrictions [[WP:GS/KURD|here]] and [[WP:CT/KURD|here]].<!-- Template:Db-gs-notice --><!-- Template:Db-csd-notice-custom --> –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 09:46, 1 May 2026 (UTC)
==[[Wikipedia:Criteria for speedy deletion|Speedy deletion]] nomination of [[:NovemTest110]]==
[[File:Information icon4.svg|48px|left|alt=|link=]]
Thank you for your interest in [[Kurds]] and [[Kurdistan]]. Due to past disruption, a [[WP:GS|special set of rules]] apply to anyone editing in this topic area. Specifically, only [[Wikipedia:User access levels#Extended confirmed users|extended confirmed]] editors are allowed to [[WP:ARBECR|make direct edits and page creations]] to this topic area. Since you currently do not meet the requirements to contribute to this topic area, the page you created has been nominated for speedy deletion under [[WP:G5|G5]].
If you think this page does not fall under the general sanctions described, you may '''contest the nomination''' by [[:NovemTest110|visiting the page]] and clicking the button labelled "Contest this speedy deletion". This will give you the opportunity to explain why you believe the page should not be deleted. However, be aware that once a page is tagged for speedy deletion, it may be deleted without delay. Please do not remove the speedy deletion tag from the page yourself, but do not hesitate to add information in line with [[Wikipedia:List of policies|Wikipedia's policies and guidelines]]. Additionally you may wish to read about the topic restrictions [[WP:GS/KURD|here]] and [[WP:CT/KURD|here]].<!-- Template:Db-gs-notice --><!-- Template:Db-csd-notice-custom --> –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 10:56, 1 May 2026 (UTC)
9blzn9svzp5yewycxwb9yp16uc2njlv
739964
739960
2026-05-01T10:58:14Z
Novem Linguae
49714
Notification: speedy deletion nomination of [[:NovemTest110]].
739964
wikitext
text/x-wiki
[[File:Information.svg|25px|alt=Information icon]] Hello, I'm [[User:Novem Linguae|Novem Linguae]]. I wanted to let you know that I reverted one of [[Special:Contributions/NovemBot|your recent contributions]] because it did not appear constructive. If you would like to experiment, please use the [[Wikipedia:Sandbox|sandbox]]. If you think I made a mistake, or if you have any questions, you can leave me a message on [[User_talk:Novem Linguae|my talk page]]. Thanks. <!-- Template:Huggle/warn-1 --><!-- This is a variant of: --><!-- Template:uw-vandalism1 -->–[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 14:55, 24 April 2026 (UTC)
{{subst:db-a10-notice|NovemTest110|nowelcome=|article=Test|{{{key2}}}={{{value2}}}|{{{key3}}}={{{value3}}}}}<!-- Template:Db-csd-notice-custom --> –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 08:52, 1 May 2026 (UTC)
==[[Wikipedia:Criteria for speedy deletion|Speedy deletion]] nomination of [[:NovemTest110]]==
[[File:Information icon4.svg|48px|left|alt=|link=]]
Thank you for your interest in [[Kurds]] and [[Kurdistan]]. Due to past disruption, a [[WP:GS|special set of rules]] apply to anyone editing in this topic area. Specifically, only [[Wikipedia:User access levels#Extended confirmed users|extended confirmed]] editors are allowed to [[WP:ARBECR|make direct edits and page creations]] to this topic area. Since you currently do not meet the requirements to contribute to this topic area, the page you created has been nominated for speedy deletion under [[WP:G5|G5]].
If you think this page does not fall under the general sanctions described, you may '''contest the nomination''' by [[:NovemTest110|visiting the page]] and clicking the button labelled "Contest this speedy deletion". This will give you the opportunity to explain why you believe the page should not be deleted. However, be aware that once a page is tagged for speedy deletion, it may be deleted without delay. Please do not remove the speedy deletion tag from the page yourself, but do not hesitate to add information in line with [[Wikipedia:List of policies|Wikipedia's policies and guidelines]]. Additionally you may wish to read about the topic restrictions [[WP:GS/KURD|here]] and [[WP:CT/KURD|here]].<!-- Template:Db-gs-notice --><!-- Template:Db-csd-notice-custom --> –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 09:46, 1 May 2026 (UTC)
==[[Wikipedia:Criteria for speedy deletion|Speedy deletion]] nomination of [[:NovemTest110]]==
[[File:Information icon4.svg|48px|left|alt=|link=]]
Thank you for your interest in [[Kurds]] and [[Kurdistan]]. Due to past disruption, a [[WP:GS|special set of rules]] apply to anyone editing in this topic area. Specifically, only [[Wikipedia:User access levels#Extended confirmed users|extended confirmed]] editors are allowed to [[WP:ARBECR|make direct edits and page creations]] to this topic area. Since you currently do not meet the requirements to contribute to this topic area, the page you created has been nominated for speedy deletion under [[WP:G5|G5]].
If you think this page does not fall under the general sanctions described, you may '''contest the nomination''' by [[:NovemTest110|visiting the page]] and clicking the button labelled "Contest this speedy deletion". This will give you the opportunity to explain why you believe the page should not be deleted. However, be aware that once a page is tagged for speedy deletion, it may be deleted without delay. Please do not remove the speedy deletion tag from the page yourself, but do not hesitate to add information in line with [[Wikipedia:List of policies|Wikipedia's policies and guidelines]]. Additionally you may wish to read about the topic restrictions [[WP:GS/KURD|here]] and [[WP:CT/KURD|here]].<!-- Template:Db-gs-notice --><!-- Template:Db-csd-notice-custom --> –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 10:56, 1 May 2026 (UTC)
==[[Wikipedia:Criteria for speedy deletion|Speedy deletion]] nomination of [[:NovemTest110]]==
[[File:Information icon4.svg|48px|left|alt=|link=]]
Thank you for your interest in [[Kurds]] and [[Kurdistan]]. Due to past disruption, a [[WP:GS|special set of rules]] apply to anyone editing in this topic area. Specifically, only [[Wikipedia:User access levels#Extended confirmed users|extended confirmed]] editors are allowed to [[WP:ARBECR|make direct edits and page creations]] to this topic area. Since you currently do not meet the requirements to contribute to this topic area, the page you created has been nominated for speedy deletion under [[WP:G5|G5]].
If you think this page does not fall under the general sanctions described, you may '''contest the nomination''' by [[:NovemTest110|visiting the page]] and clicking the button labelled "Contest this speedy deletion". This will give you the opportunity to explain why you believe the page should not be deleted. However, be aware that once a page is tagged for speedy deletion, it may be deleted without delay. Please do not remove the speedy deletion tag from the page yourself, but do not hesitate to add information in line with [[Wikipedia:List of policies|Wikipedia's policies and guidelines]]. Additionally you may wish to read about the topic restrictions [[WP:GS/KURD|here]] and [[WP:CT/KURD|here]].<!-- Template:Db-gs-notice --><!-- Template:Db-csd-notice-custom --> –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 10:58, 1 May 2026 (UTC)
8kfoedbebqysfznbbayi66hfob7uy78
William Harvey
0
164988
739900
649734
2026-05-01T00:04:58Z
InternetArchiveBot
34092
Rescuing 1 sources and tagging 0 as dead.) #IABot (v2.0.9.5
739900
wikitext
text/x-wiki
{{Infotaula persona
| sepultura = Hempstead
}}
'''William Harvey ''' ([[Folkestone]], [[1 d'abril]] de [[1578]] – [[Roehampton]], [[3 de juny]] de [[1657]]),<ref>{{Ref-llibre|cognom=Asimov|nom=Isaac|títol=Enciclopedia biográfica de ciencia y tecnología : la vida y la obra de 1197 grandes científicos desde la antigüedad hasta nuestros dias|edició=Nueva edición revisada|llengua=castellà|data=1973| editorial=Ediciones de la Revista de Occidente|lloc=Madrid|pàgines=92|isbn=8429270043|capítol=Harvey, William}}</ref> va ser un [[metge]] anglès que va ser el primer a descriure correctament i en detall el [[sistema circulatori]] i les propietats de la [[sang]] bombejada al cos pel [[cor]]. Tanmateix, el metge nascut a [[Damasc]] [[Ibn an-Nafís]] ([[1213]] - [[1288]]) va ser el primer a descriure al [[Caire]] el [[1242]], el procés de circulació de la sang, i en particular la [[circulació pulmonar]], en el cos humà.<ref name="Nature200601112">{{ref-publicació|títol=Islam and Science: The Islamic world |publicació= [[Nature]] |any=2006 |mes=11 |volum=444 |exemplar=7115 |pàgines=22-25 |url= http://dx.doi.org/10.1038/444020a |doi=10.1038/444020a }}</ref>
== Biografia ==
Harvey va néixer a [[Folkestone]] on va aprendre [[llatí]], estudià posteriorment durant 5 anys al King's School de [[Canterbury]] i després al [[Caius College]] de [[Cambridge]].
Després de graduar-se com ''Bachelor of Arts'' al Caius College el 1597, va fer el viatge per Europa, pràctica habitual entre els homes cultes de l'època, i va estar a França, Alemanya i Itàlia on entrà a la Universitat de [[Pàdua]] el 1598. Harvey durant els anys d'estudi es relacionà amb [[Hieronymus Fabricius|Fabricius]] i llegí l'obra d'aquest ''[[De Venarum Ostiolis]]''. Harvey es va doctorar en medicina, a Pàdua, el 1602, i tornà a Anglaterra on, el mateix any, obtingué el doctorat de medicina per Cambridge. S'establí a Londres.i exercí a l'hospital de Saint Bartolomew quasi fins al final de la seva vida.
Va ser nomenat Metge Extraordinari ('Physician Extraordinary') pel rei James I el 1618, es creu que també va ser metge d'aristòcrates com per exemple [[Francis Bacon|Lord Chancellor Bacon]]. El 1628 publicà a Frankfurt el seu tractat de circulació de la sang ''[[De Motu Cordis]]'' que va ser mal rebut per alguns metges.<ref>{{ref-llibre |autor=Sir D'Arcy Power |títol=William Harvey |llengua=anglès |data=1847 |pàgines=74 |url= https://archive.org/details/williamharvey00powe/page/n7/mode/2up editorial=T. Fisher Unwin}}</ref> El 1630, per encàrrec del rei James I va acompanyar el duc de Lennox a Europa i va visitar Pàdua, França i Espanya. Tornà a Anglaterra el 1632 i el nou rei Charles I el va nomenar ''Physician in Ordinary''. Durant la [[guerra civil anglesa]] mantingué continuà al costat de la monarquia. Es va retirar de la professió el 1645. Morí a [[Roehampton]] i va ser enterrat a [[Hempstead]], [[Essex]].
== "De Motu Cordis" (conegut també com "Sobre el moviment del cor i la sang") ==
''De Motu Cordis'' és un llibre dedicat a [[Carles V del Sacre Imperi Romanogermànic]], publicat el [[1628]] que consta de 72 pàgines en 17 capítols. Conté una clara i detallada descripció del funcionament del cor i el moviment circular de la sang pel cos. Com que tenia una simple lent i no pas un [[microscopi]] no va ser capaç d'aconseguir-ne imatges adequades. Harvey va fer els seus experiments principalment en animals. Remarca el fet que els dos [[Ventricle cardíac|ventricles cardíacs]] es mouen junts gairebé simultàniament i no independentment com deien els seus predecessors. En el capítol 8 entrà en conflicte amb la creença de [[Galè]] sobre que el fetge és l'origen de la [[sang venosa]] (amb suport experimental).<ref>{{ref-llibre |cognom=Power |nom=D'Arcy |títol=William Harvey: Masters of Medicine |pàgines=193 | editorial=T. Fisher Unwin |any=1897 |isbn=978-1-4179-6578-6 |llengua=anglès}}</ref>
== Exercitationes de generatione animalium ==
L'altra obra important de Harvey va ser ''Exercitationes de generatione animalium'' (de la generació d'animals), publicada el [[1651]]. Hi havia estat treballant des de feia molts anys, però potser mai l'hauria acabat sense l'ànim del seu amic George Ent.<ref>{{cite ODNB |id=12531 |title=William Harvey |first=Roger |last=French |url= http://www.oxforddnb.com/view/article/12531 |access-date=1 gener 2014 |freearticle=yes}}</ref> El llibre comença amb una descripció del desenvolupament de l'ou de gallina. La major part és teòrica, tractant les teories d'Aristòtil i el treball dels metges que van seguir Galeno fins a Fabricius. Finalment s’ocupa de l'embriogènesi en animals vivípars, especialment de les cerves i les femelles. El tractament és generalment aristotèlic i limitat per l'ús d'una lent d'augment simple.
== Referències ==
{{Referències}}
== Bibliografia ==
* {{ref-llibre | autor = Butterfield, Herbert | títol = The Origins of Modern Science | any = 1957 | editorial = The Free Press | lloc=Nova York| edició = revised }}
* {{ref-llibre | autor = Gregory, Andrew | títol = Harvey's Heart, The Discovery of Blood Circulation | editorial = Icon Books | lloc = Cambridge, England | any = 2001 }}
* {{ref-llibre | autor = Munk, William | enllaçautor = William Munk | títol = The Roll of the Royal College of Physicians of London, Vol. I | any = 1878 | lloc = Londres | edició = 2nd | pàgines = 124 – 146 | url = http://www.rcplondon.ac.uk/heritage/munksroll/munk_details.asp?ID=5069 }} {{Webarchive|url=https://web.archive.org/web/20090615145945/http://www.rcplondon.ac.uk/heritage/munksroll/munk_details.asp?ID=5069 |date=2009-06-15 }}
* {{ref-llibre | autor = Harvey, William | títol = On the Motion of the Heart and Blood in Animals | editorial = George Bell and Sons |lloc=Londres| any = 1889 | url = http://books.google.cat/?id=ewAIAAAAIAAJ&printsec=frontcover&dq=william+harvey }}
* {{ref-llibre | autor = Rapson, Helen | títol = The Circulation of the Blood | any = 1982 | editorial = Frederick Muller | lloc =Londres}}
* {{ref-llibre | autor= Harvey, William |coautors=Translated by Kenneth J. Franklin. Introduction by Dr. Andrew Wear| títol= The Circulation of the Blood and Other Writings | editorial = Everyman: Orion Publishing Group | lloc=Londres| any= 1993 | isbn=0-460-87362-8}}
* {{ref-llibre | autor= Willis, Robert (translator) | títol=The Works of William Harvey | any= 1847 | editorial= Sydenham Society| lloc=Londres| url= http://www.archive.org/details/workswilliamhar00harvgoog}}
* {{ref-llibre | autor = Harris, Paul | títol = William Harvey, Folkestone's Most Famous Son | any = 2007 | editorial = Lilburne Press | lloc = Folkestone }}
* {{ref-llibre | autor = Royal Society of Medicine (Great Britain) | títol = Portraits of Dr. William Harvey | any = 1913 | editorial = Humphrey Milford, Oxford University Press |lloc=Londres| url = http://www.archive.org/details/portraitsofdrwil00royarich}}
* {{ref-llibre | autor = Kearney, Hugh | títol = Science and Change 1500 - 1700 | any = 1971 | editorial = McGraw-Hill | lloc =Nova York}}
* {{ref-llibre | autor = Singer, Charles | títol = A History of Biology | any = 1959 |lloc=Londres| editorial = Abelard-Schuman | edició = third, revised }}
* {{ref-llibre | autor = Mitchell, Silas Weir | títol = Some Memoranda in Regard to William Harvey, M.D. | any = 1907}}
== Vegeu també ==
* [[Amato Lusitano]] – metge portuguès del {{segle|XVI}} que també es considera descobridor de la circulació de la sang
* [[Miguel Servet]] – que va proposa la circulació de la sang sense proves experimentals
== Enllaços externs ==
{{Projectes germans}}
* [http://www.fordham.edu/halsall/mod/1628harvey-blood.html William Harvey: "On The Motion Of The Heart And Blood In Animals", 1628] {{Webarchive|url=https://web.archive.org/web/20090914040350/http://www.fordham.edu/halsall/mod/1628harvey-blood.html |date=2009-09-14 }}
* [http://www.accessexcellence.org/RC/AB/BC/William_Harvey.html William Harvey info from the (US) National Health Museum]
* [http://www.wardsbookofdays.com/1april.htm The life and work of William Harvey @ ''Ward's Book of Days'']
* [https://web.archive.org/web/20050305102926/http://www.geocities.com/Heartland/6575/library/harveybo.htm The Harvey Genealogist: The Harvey Book: PART ONE] (menciona William Harvey i diversos ancestres i parents)
{{Autoritat}}
{{ORDENA:Harvey, William}}
[[Categoria:Metges anglesos]]
[[Categoria:Biòlegs anglesos]]
[[Categoria:Persones de Kent]]
[[Categoria:Alumnes de la Universitat de Pàdua]]
[[Categoria:Alumnes del Gonville and Caius College]]
[[Categoria:Morts a Anglaterra]]
== Legacy ==
[[Fitxer:Stamp_of_USSR_2004.jpg|thumb|209x209px| William Harvey en un segell postal [[Unió de Repúbliques Socialistes Soviètiques|soviètic]] de 1957]]
Harvey's whalebone demonstration rod, tipped with silver, resides in the silver room of the museum of the Royal College of Physicians. He used it to point to objects during his lectures.<ref>{{Ref-web|url=http://munksroll.rcplondon.ac.uk/Biography/Details/5069|títol=William Harvey|cognom=Munk|nom=William|editor=Royal Society of Physicians, Lives of the Fellows, Volume I}}</ref>
Harvey's whalebone demonstration rod, tipped with silver, resides in the silver room of the museum of the [[:en:Royal_College_of_Physicians|Royal College of Physicians]]. He used it to point to objects during his lectures.<ref>{{cite web |last=Munk |first=William |title=William Harvey |url=http://munksroll.rcplondon.ac.uk/Biography/Details/5069 |publisher=Royal Society of Physicians, Lives of the Fellows, Volume I |page=124 |access-date=2025-03-17 |archive-date=2017-04-02 |archive-url=https://web.archive.org/web/20170402141124/http://munksroll.rcplondon.ac.uk/Biography/Details/5069 |url-status=dead }}</ref>
Diversos edificis i institucions mèdiques porten el nom de Harvey o commemoran d'una altra manera. La Societat Harveiana d'Edimburg va ser fundada el 1782 pel doctor Andrew Duncan . La Societat celebra un Festival anual en honor a Harvey on el President de la Societat pronuncia l'Oració Harveiana, seguida d'un sopar formal. La seu del Festival alterna entre el Royal College of Physicians of Edinburgh i el Royal College of Surgeons of Edinburgh . La Harveian Society of London és una societat mèdica fundada el 1831 amb seu a The Medical Society of London, Chandos Street, a Cavendish Square. <ref name=":SocietyWebPage">{{Ref-web|url=http://www.harveiansocietyoflondon.btck.co.uk|títol=The Harveian Society of London - Home|obra=www.harveiansocietyoflondon.btck.co.uk|llengua=en|consulta=21 April 2018}}</ref> <ref name=": Geraint2010">{{Ref-publicació|cognom=James|nom=D. Geraint|data=August 2010|publicació=Journal of Medical Biography|volum=18|exemplar=3|pàgines=126|doi=10.1258/jmb.2009.009087|issn=1758-1087|pmid=20798408}}</ref> El Royal College of Physicians de Londres fa una conferència anual establerta per William Harvey el 1656 anomenada Harveian Oration . <ref>{{Ref-publicació|doi=10.1136/bmj.2.4007.783|pmc=2087562|any=1937|cognom=Hurst|nom=A.|publicació=BMJ|volum=2|pàgines=783–9|pmid=20780984|exemplar=4007}}</ref> L' Harvey Club de Londres es va fundar al Canadà el 1919 i té la seu a la [[Universitat d'Ontario Occidental|Universitat de Western Ontario]] . <ref>{{Ref-llibre|títol=A century of medicine at Western: a centennial history of the Faculty of Medicine, University of Western Ontario|cognom=Barr|nom=Murray Llewellyn|data=1977|editorial=University of Western Ontario|isbn=0919534007|lloc=London|oclc=4045914|url=https://archive.org/details/centuryofmedicin0000barr|pàgines=110}}</ref> La Harvey Society, fundada el 1905, té la seu a la ciutat de Nova York i acull una sèrie de conferències anuals sobre els avenços recents de les ciències biomèdiques. <ref>{{Ref-publicació|url=http://www.harveysociety.org/history/Harvey-Science-25th-Anniversary.pdf|cognom=Cole, M.D., Rufus|publicació=Science|data=20 June 1930|volum=LXXI|exemplar=1851|pàgines=617–627|doi=10.1126/science.71.1851.617|pmid=17839933|bibcode=1930Sci....71..617C}}</ref> La sala de conferències principal de l' Escola de Medicina Clínica de la Universitat de Cambridge porta el nom de William Harvey, que va ser un antic alumne de l'institut. <ref>{{Ref-web|títol=Which Medical Schools Make You The Best Doctor In The World?|url=https://www.crimsoneducation.org/us/blog/which-med-schools-make-you-the-best-doctor|obra=Crimson}}</ref> El William Harvey Research Institute de Barts i la London School of Medicine and Dentistry és un centre de recerca centrat en farmacologia bioquímica, malalties ortopèdiques, endocrinologia, genòmica, farmacologia clínica i medicina translacional i terapèutica. <ref>{{Ref-web|títol=WHRI Barts and the London|url=https://www.qmul.ac.uk/whri/|obra=Queen Mary University of London}}</ref> L'hospital William Harvey d' [[Ashford (Kent)|Ashford, Kent]] porta el seu nom. La ciutat natal de Harvey, [[Folkestone|Folkestone, Kent]] també té una estàtua d'ell. <ref>{{Ref-web|títol=William Harvey Statue|url=https://www.bshs.org.uk/travel-guide/william-harvey-statue|obra=BSHS Travel Guide|data=22 January 2014}}</ref>
hec13ulq0gb8yhdbddh3h07j2o7tzog
Category:Test1
14
166870
739911
664786
2026-05-01T06:42:13Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: добавление шаблона номинации на удаление
739911
wikitext
text/x-wiki
<noinclude>{{Обсуждаемая категория|2026-05-01}}</noinclude>
{{Category redirect|Category:Test 2}}
a7t8pmt4tin4hta2ob6kua9e47oyzp9
739914
739911
2026-05-01T06:45:37Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: добавление шаблона номинации на удаление
739914
wikitext
text/x-wiki
<noinclude>{{Обсуждаемая категория|2026-05-01}}
{{Обсуждаемая категория|2026-05-01}}
</noinclude>
{{Category redirect|Category:Test 2}}
k2hsgbajroiuqdgdpotdqu202tz03lr
739917
739914
2026-05-01T06:48:06Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: добавление шаблона номинации на удаление
739917
wikitext
text/x-wiki
<noinclude>{{Обсуждаемая категория|2026-05-01}}
{{Обсуждаемая категория|2026-05-01}}
{{Обсуждаемая категория|2026-05-01}}
</noinclude>
{{Category redirect|Category:Test 2}}
e2ba51x63p00rn7mz64w7pz68bh2we7
739923
739917
2026-05-01T07:09:23Z
Solidest
54422
739923
wikitext
text/x-wiki
{{Category redirect|Category:Test 2}}
e3b7wftecs0wt3ae7epox54csqsams4
739928
739923
2026-05-01T07:10:00Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: добавление шаблона номинации на удаление
739928
wikitext
text/x-wiki
<noinclude>{{Обсуждаемая категория|2026-05-01}}</noinclude>
{{Category redirect|Category:Test 2}}
a7t8pmt4tin4hta2ob6kua9e47oyzp9
Category:Test3
14
166873
739919
664805
2026-05-01T06:48:44Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: добавление шаблона номинации на удаление
739919
wikitext
text/x-wiki
<noinclude>{{Обсуждаемая категория|2026-05-01}}</noinclude>
{{Category redirect|Category:Test 3}}
1bfkiktcaygil27o8y0356eern1qecb
Theatre of Pain
0
167845
739953
690573
2026-05-01T10:17:12Z
GiovanniPen
65549
Bot: Automated text replacement (-asd +melo)
739953
wikitext
text/x-wiki
{{Info/Álbum
|nome = Theatre of Pain
|tipo = estúdio
|artista = [[Mötley Crüe]]
|capa =
|lançado = [[21 de junho]] de [[1985]]
|gravado = Cherokee Studios em [[Hollywood]], [[Califórnia]]
|gênero = [[Heavy metal]], [[glam metal]]
|duração = 44:10
|gravadora = [[Elektra Records|Elektra]]
|produtor = [[Tom Werman]]
|último_álbum = ''[[Shout at the Devil]]''<br />(1983)
|próximo_álbum = ''[[Girls, Girls, Girls]]''<br />(1987)
}}
'''''Theatre of Pain''''' é o terceiro [[álbum de estúdio]] da banda de [[heavy metal]] [[Mötley Crüe]], foi lançado em 21 de junho de 1985.
Contém os hits "[[Smokin' in the Boys Room]]" e "[[Home Sweet Home]]". O álbum alcançou o número 6 nas tabelas norte-americanas e o número 36 no Reino Unido. Obteve quádrupla platina, certificada pela [[Recording Industry Association of America|RIAA]] em 1995.<ref>{{citar web|url=http://www.riaa.com/goldandplatinumdata.php?resultpage=1&table=SEARCH_RESULTS&action=&title=Theatre%20of%20Pain&artist=&format=&debutLP=&category=&sex=&releaseDate=&requestNo=&type=&level=&label=&company=&certificationDate=&awardDescription=&catalogNo=&aSex=&rec_id=&charField=&gold=&platinum=&multiPlat=&level2=&certDate=&album=&id=&after=&before=&startMonth=1&endMonth=1&startYear=1958&endYear=2009&sort=Artist&perPage=25|título=RIAA Gold & Platinum database-''Theatre of Pain''|acessodata=6 de março de 2009}}</ref>
== Faixas ==
Todas as letras por [[Nikki Sixx]] exceto "Smokin' in the Boys Room", escrita por [[Cub Koda]].
* "City Boy Blues" (Sixx, Mars, Neil) - 4:10
* "Smokin' in the Boys' Room" (cover de [[Brownsville Station]]) (Koda, Lutz) - 3:27
* "Louder Than Hell" (Sixx) - 2:32
* "Keep Your Eye on the Money" (Sixx) - 4:40
* "Home Sweet Home" (Sixx, Neil, Lee) - 3:59
* "Tonight (We Need a Lover)" (Sixx, Neil) - 3:37
* "Use It or Lose It" (Sixx, Mars, Neil, Lee) - 2:39
* "Save Our Souls" (Sixx, Neil) - 4:13
* "Raise Your Hands to Rock" (Sixx) - 2:48
* "Fight For Your Rights" (Sixx, Mars) - 3:50
=== Reedição 2003 ===
* "Home Sweet Home" (Demo Version) - 4:24
* "Smokin' in the Boys' Room" (Alternate Guitar Solo-Rough Mix) - 3:34
* "City Boy Blues" (Demo Version) - 4:28
* "Home Sweet Home" (Instrumental Rough Mix) - 2:58
* "Keep Your Eye on the Money" (Demo Version) - 3:49
* "Tommy's Drum Piece from Cherokee Studios" - 3:16
* "Home Sweet Home" (Music Video)
== Paradas ==
'''Album''' - ''[[Billboard]]'' (América do Norte)
{| class="wikitable"
!align="left" valign="top"|Ano
!align="left" valign="top"|Parada
!align="left" valign="top"|Posição
|-
|align="left" valign="top"|1986
|align="left" valign="top"|[[Billboard 200]]
|align="left" valign="top"|6<ref>{{citar web|url=http://www.billboard.com/bbcom/retrieve_chart_history.do?model.chartFormatGroupName=Albums&model.vnuArtistId=5252&model.vnuAlbumId=327684|título=''Bilboard'' album chart history-Mötley Crüe|acessodata=6 de março de 2009}}</ref>
|-
|}
{{Referências}}
{{Mötley Crüe}}
{{Esboço-álbum|Mötley Crüe}}
{{Portal3|Música}}
{{DEFAULTSORT:Theatre Pain}}
[[Categoria:Álbuns de 1985]]
[[Categoria:Álbuns de Mötley Crüe]]
[[Categoria:Álbuns de glam metal]]
[[Categoria:Álbuns de heavy metal]]
[[Categoria:Álbuns da Elektra Records]]
== Pessoal ==
=== Mötley Crüe ===
* [[Vince Neil]] – vocal principal e de apoio, gaita melo a d g g f
* [[Mick Mars]] – todas as guitarras elétricas, acústicas e slide, backing vocals a b c d e f
*
*
pu9t2vxc7bnygu3y19khagbqn0l3m4i
Mainspace
0
171251
739940
739655
2026-05-01T09:29:17Z
Novem Linguae
49714
Requesting speedy deletion ([[WP:CSD#G5|CSD G5]]).
739940
wikitext
text/x-wiki
{{db-gs|code=kurd|help=off}}
<!-- Please do not remove or change this AfD message until the discussion has been closed. -->
{{AfDM|page=Mainspace (29th nomination)|year=2026|month=April|day=28|substed=yes|origtag=afdx|help=off|outcome=merging|target=NovemsRedirect}}
<!-- End of AfD message, feel free to edit beyond this point -->
Test
g4qr9tcq4jy1routk5ojhwj51enc2dk
NovemTest110
0
174827
739933
738731
2026-05-01T08:52:00Z
Novem Linguae
49714
Requesting speedy deletion ([[WP:CSD#A10|CSD A10]]).
739933
wikitext
text/x-wiki
{{db-a10|article=Test|help=off}}
<!-- Please do not remove or change this AfD message until the discussion has been closed. -->
{{AfDM|page=NovemTest110 (13th nomination)|year=2026|month=April|day=18|substed=yes|origtag=afdx|help=off|outcome=merging|target=564654654}}
<!-- End of AfD message, feel free to edit beyond this point -->
test
bnwuz5kis3j7ffb2886jekl6ps0s6ib
739936
739933
2026-05-01T08:53:17Z
Novem Linguae
49714
Replaced content with "test"
739936
wikitext
text/x-wiki
test
jrwjerxiekdtj9k82lg930wpkr6tq6r
739944
739936
2026-05-01T09:40:21Z
Novem Linguae
49714
Requesting speedy deletion ([[WP:CSD#G5|CSD G5]]).
739944
wikitext
text/x-wiki
{{db-gs|code=kurd|help=off}}
test
230m7pperlghk6tjuvqrlbmcsq9e92q
739947
739944
2026-05-01T09:46:19Z
Novem Linguae
49714
739947
wikitext
text/x-wiki
test
jrwjerxiekdtj9k82lg930wpkr6tq6r
739959
739947
2026-05-01T10:56:14Z
Novem Linguae
49714
Requesting speedy deletion ([[WP:CSD#G5|CSD G5]]).
739959
wikitext
text/x-wiki
{{db-gs|code=kurd|help=off}}
test
230m7pperlghk6tjuvqrlbmcsq9e92q
739963
739959
2026-05-01T10:58:13Z
Novem Linguae
49714
Requesting speedy deletion ([[WP:CSD#G5|CSD G5]]).
739963
wikitext
text/x-wiki
{{db-gs|code=kurd|help=off}}
{{db-gs|code=kurd|help=off}}
test
3uxgyl0ucxs17k2huq26ii27q8e7lvu
739966
739963
2026-05-01T10:58:27Z
Novem Linguae
49714
Replaced content with "test"
739966
wikitext
text/x-wiki
test
jrwjerxiekdtj9k82lg930wpkr6tq6r
739967
739966
2026-05-01T10:58:43Z
Novem Linguae
49714
Requesting speedy deletion ([[WP:CSD#G5|CSD G5]]).
739967
wikitext
text/x-wiki
{{db-gs|code=kurd|help=off}}
test
230m7pperlghk6tjuvqrlbmcsq9e92q
User:Solidest/remover-core.js
2
174846
739909
739550
2026-05-01T06:38:18Z
Solidest
54422
739909
javascript
text/javascript
/**
* Remover — ядро (core).
* Загружается лениво при первом клике по пункту меню.
* Ожидает, что window.RemoverState уже задан remover-loader.js.
*
* Архитектура: единый реестр OPERATIONS описывает все операции (КБУ, КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС, Защита, Запрос, Снятие, кат-варианты).
* Логика выполнения сосредоточена в универсальных обработчиках.
* Экспортирует: window.RemoverCore.handleMenuClick(item, event)
*/
(function () {
'use strict';
var state = window.RemoverState;
if (!state) { console.error('RemoverCore: window.RemoverState не задан.'); return; }
var mwCfg = state.mwCfg;
var cfg = applyCoreConfigDefaults(state.cfg || {});
var isCategory = state.isCategory;
var isVector22 = state.isVector22;
var scriptLink = cfg.scriptLink;
var settingsOptionName = state.settingsOptionName || 'userjs-remover-settings';
var settingsVersion = 1;
var settingsMenuMeta = collectSettingsMenuMeta();
var settingsArticleItemLabels = settingsMenuMeta.articleLabels;
var settingsCategoryItemLabels = settingsMenuMeta.categoryLabels;
var settingsItemLabelById = settingsMenuMeta.idToLabel;
var settingsItemLabelByNorm = settingsMenuMeta.labelByNorm;
var settingsItemLabelOrder = settingsMenuMeta.labelOrder;
var settingsDefaults = getDefaultSettings();
var MENU_TITLE_PRESET_CACTIONS = '__remover_portlet_cactions__';
var MENU_TITLE_PRESET_PAGE = '__remover_portlet_page__';
var MENU_TITLE_PRESET_TOOLS = '__remover_portlet_tools__';
var initialSettings = normalizeRemoverSettings(readSettingsOptionState(state.settings || {}));
var setAlert = ('setAlert' in state) ? !!state.setAlert : initialSettings.notifyAuthor;
var setSubscribe = ('setSubscribe' in state) ? !!state.setSubscribe : initialSettings.subscribeTopic;
var signatureSeparator = ('signatureSeparator' in state && typeof state.signatureSeparator === 'string')
? state.signatureSeparator.trim()
: initialSettings.signatureSeparator;
initialSettings.notifyAuthor = setAlert;
initialSettings.subscribeTopic = setSubscribe;
initialSettings.signatureSeparator = signatureSeparator;
state.cfg = cfg;
state.settings = clonePlainObject(initialSettings);
state.setAlert = setAlert;
state.setSubscribe = setSubscribe;
// ─── Константы ──────────────────────────────────────────────────────────
var MONTHS_NOM = ['Январь','Февраль','Март','Апрель','Май','Июнь','Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'];
var MONTHS_GEN = ['января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря'];
var MONTHS_NOM_LOWER = MONTHS_NOM.map(function (m) { return m.toLowerCase(); });
var T_OPEN = '{' + '{';
var T_CLOSE = '}' + '}';
var KBU_CRITERIA_PAGE = 'Википедия:Критерии быстрого удаления';
var RE_ESCAPE = /[.*+?^${}()|[\]\\]/g;
var RE_KU_ON_PAGE = /\{\{\s*(?:к\s*удалению|ку)\s*(?:\||\}\})/i;
var RE_KPM_ON_PAGE = /\{\{\s*(?:к\s*переименованию|кпм|rename)\s*(?:\||\}\})/i;
var RE_KBU_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?(?:db\s*-[^|}\s]+|уд\s*-[^|}\s]+|к\s*быстрому\s*удалению|к\s*отсроченному\s*удалению|deleteslow|ds|hang\s*-?\s*on)\s*(?:\||\}\})/i;
var RE_KUL_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?к\s*улучшению\s*(?:\||\}\})/i;
var KBU_PATTERN_STR = 'db\\s*-[^|}\\s]+|уд\\s*-[^|}\\s]+|к\\s*быстрому\\s*удалению|к\\s*отсроченному\\s*удалению|deleteslow|ds';
var RE_KBU_PATTERNS = new RegExp('(?:' + KBU_PATTERN_STR + ')', 'i');
var KUL_PATTERN_STR = 'к\\s*улучшению';
var RE_KUL_PATTERN = new RegExp(KUL_PATTERN_STR, 'i');
var HANGON_PATTERN_STR = 'hang\\s*-?\\s*on';
var RE_HANGON = new RegExp(HANGON_PATTERN_STR, 'i');
var RE_DATE_ISO = /^(\d{4})-(\d{2})-(\d{2})$/;
var RE_DATE_RUSSIAN = /^(\d{1,2})\s+([\u0430-\u044f\u0410-\u042f\u0451\u0401.]+)\s+(\d{2}|\d{4})$/;
var RE_DATE_DASH = /^(\d{1,4})\s*-\s*(\d{1,2})\s*-\s*(\d{1,4})$/;
var RE_DATE_DOT = /^(\d{1,2})\s*\.\s*(\d{1,2})\s*\.\s*(\d{2}|\d{4})$/;
var RE_DATE_SLASH = /^(\d{1,4})\s*\/\s*(\d{1,2})\s*\/\s*(\d{1,4})$/;
var RE_NOINCLUDE = /(\s*)<noinclude>([\s\S]*?)<\/noinclude>/i;
var RE_TEMPLATE_NS = /^шаблон\s*:\s*/i;
// ─── Глобальные переменные сессии ────────────────────────────────────────
var isError = false;
var logStatusSeq = 0;
var resizeObservers = [];
var modalLayoutSyncHandlers = [];
var tplAliasCache = {};
// ─── Стили ───────────────────────────────────────────────────────────────
var stStyles = cfg.modalStyles;
var tk = {
cBase: 'var(--color-base, #202122)',
cSub: 'var(--color-subtle, #72777d)',
cSubM: 'var(--color-subtle, #54595d)',
cInv: 'var(--color-inverted-fixed, #fff)',
cProg: 'var(--color-progressive, #3366cc)',
cProgH: 'var(--color-progressive--hover, #2a4b8d)',
cDang: 'var(--color-destructive, #d73333)',
cDis: 'var(--color-disabled, var(--color-subtle, #72777d))',
bgBase: 'var(--background-color-base, #fff)',
bgNSub: 'var(--background-color-neutral-subtle, #f8f9fa)',
bgN: 'var(--background-color-neutral, #eaecf0)',
bgDis: 'var(--background-color-disabled, var(--background-color-neutral, #eaecf0))',
bgProg: 'var(--background-color-progressive, #3366cc)',
bgProgH:'var(--background-color-progressive--hover, #2a4d8f)',
bgSucc: 'var(--background-color-success, #14866d)',
bgSuccH:'var(--background-color-success--hover, #0f6d57)',
bSub: 'var(--border-color-subtle, #a2a9b1)',
bSubS: 'var(--border-color-subtle, #ddd)',
bDis: 'var(--border-color-disabled, var(--border-color-subtle, #a2a9b1))',
bProg: 'var(--border-color-progressive, #3366cc)',
bProgH: 'var(--border-color-progressive--hover, #2a4d8f)',
bSucc: 'var(--border-color-success, #14866d)',
bSuccH: 'var(--border-color-success--hover, #0f6d57)'
};
var sz = {
taH: '180px',
taMinH: '100px',
taMinW: '180px',
mobileBp: 720,
modalRatio: 0.4,
modalMinWide: 420,
modalDefaultWide: 720,
viewportGap: 24,
touchDesktopGap: 120
};
var btnBase = 'border-radius:4px;padding:8px 16px;cursor:pointer;font-size:14px;font-family:inherit;transition:background .1s;';
var neutralVis = 'background:' + tk.bgNSub + ';border:1px solid ' + tk.bSub + ';color:' + tk.cBase + ';border-radius:4px;';
var stCancel = neutralVis + btnBase;
var stSubmit = 'background:' + tk.bgProg + ';color:' + tk.cInv + ';border:1px solid ' + tk.bProg + ';' + btnBase;
var stReload = 'background:' + tk.bgSucc + ';color:' + tk.cInv + ';border:1px solid ' + tk.bSucc + ';' + btnBase;
var stInputBox = 'flex:1;padding:6px;box-sizing:border-box;min-width:0;border:1px solid ' + tk.bSub + ';background:' + tk.bgBase + ';color:inherit;border-radius:2px;';
var stInputFull= 'width:100%;padding:6px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:2px;background:' + tk.bgBase + ';color:inherit;margin-bottom:10px;';
var stRow = 'display:flex;margin-bottom:6px;';
var stToolBtn = neutralVis + 'padding:4px 8px;margin-left:4px;cursor:pointer;font-size:12px;';
var stRemoveBtn= neutralVis + 'display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;padding:0;width:32px;height:32px;margin-left:4px;cursor:pointer;font-size:12px;line-height:1;';
var stFooterWrap = 'display:flex;align-items:center;gap:8px;flex-wrap:wrap;';
var stFooterChecks = 'display:flex;flex-direction:column;gap:4px;margin-right:auto;flex:1 1 220px;min-width:0;';
var stFooterCheckLabel = 'display:inline-flex;align-items:flex-start;gap:6px;font-size:14px;line-height:1.4;max-width:100%;';
var stFooterActions = 'display:flex;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;justify-content:flex-end;margin-left:auto;';
var stHeaderIconBtn = 'margin:0 0 0 auto;width:32px;min-width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:4px;background:' + tk.bgNSub + ';color:' + tk.cSubM + ';cursor:pointer;font-size:16px;line-height:1;';
var multiNominationGap = '6px';
var RESIZE_CLASS = 'rm-resizable';
// ═══════════════════════════════════════════════════════════════════════════
// РЕЕСТР ОПЕРАЦИЙ
// Каждая запись описывает одну кнопку меню. Поля:
// id — идентификатор (совпадает с item.id из loader)
// handler — имя метода-обработчика в объекте handlers
// handlerArg — аргумент, передаваемый в handler (опционально)
// ═══════════════════════════════════════════════════════════════════════════
var OPERATIONS = [
// ── Статьи ──────────────────────────────────────────────────────────
{
id: 'fRm',
label: 'КБУ',
handler: 'showKbu',
// Параметры номинации: заполняются при submit
nomination: {
pageTitle: function (pg) { return normTitle(pg); },
// шаблон встраивается в статью, номинационная страница отсутствует
inArticle: true
}
},
{
id: 'tRm',
label: 'КУ',
handler: 'showNomination',
nomination: {
comment: 'к удалению',
template: 'к удалению',
navTemplate: 'КУ',
nomPage: function (date) { return 'Википедия:К удалению/' + date; },
supportsMulti: true,
supportsTransfer: true,
// шаблон встраивается в статью через <noinclude>
inArticle: true,
articleTpl: function (tplpar, date) { return 'к удалению|' + date + (tplpar ? '|' + tplpar : ''); }
}
},
{
id: 'rnm',
label: 'КПМ',
handler: 'showNomination',
nomination: {
comment: 'к переименованию',
template: 'к переименованию',
navTemplate: 'КПМ',
nomPage: function (date) { return 'Википедия:К переименованию/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к переименованию|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'rename',
firstId: 'rmRenameFirst', inputClass: 'rmRenameInput',
firstPh: 'Новое название',
addBtnId: 'rmAddRename', addBtnLabel: 'Добавить вариант',
containerId: 'rmRenameContainer', addPh: 'Дополнительный вариант',
maxRows: 2, maxMsg: 'Максимум 3 варианта переименования.'
}
}
},
{
id: 'imp',
label: 'КУЛ',
handler: 'showNomination',
nomination: {
comment: 'к срочному улучшению',
template: 'к улучшению',
navTemplate: 'КУЛ',
nomPage: function (date) { return 'Википедия:К улучшению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к улучшению|' + date + (tplpar ? '|' + tplpar : ''); }
}
},
{
id: 'merge',
label: 'КОБ',
handler: 'showNomination',
nomination: {
comment: 'к объединению с другой',
template: 'к объединению',
navTemplate: 'КОБ',
nomPage: function (date) { return 'Википедия:К объединению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к объединению|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'merge',
firstId: 'rmMergeFirst', inputClass: 'rmMergeInput',
firstPh: 'Объединить с…',
addBtnId: 'rmAddMerge', addBtnLabel: '+',
containerId: 'rmMergeContainer', addPh: 'Дополнительная статья',
maxRows: 10, maxMsg: 'Максимум 11 статей для объединения.'
}
}
},
{
id: 'split',
label: 'КРАЗД',
handler: 'showNomination',
nomination: {
comment: 'к разделению',
template: 'к разделению',
navTemplate: 'КР',
nomPage: function (date) { return 'Википедия:К разделению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к разделению|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'split',
firstId: 'rmSplitFirst', inputClass: 'rmSplitInput',
firstPh: 'Разделить на…',
addBtnId: 'rmAddSplit', addBtnLabel: '+',
containerId: 'rmSplitContainer', addPh: 'Дополнительная статья'
}
}
},
{
id: 'recov',
label: 'ВУС',
handler: 'showNomination',
nomination: {
comment: '',
template: 'к восстановлению',
navTemplate: 'ВУС',
nomPage: function (date) { return 'Википедия:К восстановлению/' + date; },
inArticle: false // шаблон не ставится в (удалённую) статью
}
},
{
id: 'close',
label: 'Снятие',
handler: 'showArticleClose'
},
// ── Запросы ─────────────────────────────────────────────────────────
{
id: 'protect',
label: 'Защита',
handler: 'showReport',
reportMode: 'protect'
},
{
id: 'request',
label: 'Запрос',
handler: 'showReport',
reportMode: 'request'
},
// ── Категории ────────────────────────────────────────────────────────
{
id: 'cat-fRm',
label: 'КБУ',
handler: 'showKbu',
forCategory: true
},
{
id: 'cat-discuss',
label: 'Обсудить',
handler: 'showCatNomination',
catType: 'discuss'
},
{
id: 'cat-delete',
label: 'Удалить',
handler: 'showCatNomination',
catType: 'deletion'
},
{
id: 'cat-rename',
label: 'Переименовать',
handler: 'showCatNomination',
catType: 'rename'
},
{
id: 'cat-merge',
label: 'Объединить',
handler: 'showCatNomination',
catType: 'merge'
},
{
id: 'cat-done',
label: 'Снятие',
handler: 'showCatClose'
}
];
// Быстрый поиск по id
var OPERATIONS_MAP = {};
OPERATIONS.forEach(function (op) { OPERATIONS_MAP[op.id] = op; });
// ─── Тексты переноса (КБУ → КУ) ─────────────────────────────────────────
var transferTexts = {
kbu: { notice: 'Перенесено с быстрого удаления.', hint: 'Шаблоны КБУ и Hangon будут сняты.' },
kul: { notice: 'Перенесено с КУЛ.', hint: 'Шаблоны КУЛ будут сняты.' },
both: { notice: 'Перенесено с быстрого удаления и КУЛ.', hint: 'Шаблоны КБУ, КУЛ и Hangon будут сняты.' }
};
// ═══════════════════════════════════════════════════════════════════════════
// УТИЛИТЫ
// ═══════════════════════════════════════════════════════════════════════════
function escapeRegExp(s) { return s.replace(RE_ESCAPE, '\\$&'); }
function escapeHtml(s) {
return String(s || '')
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
function joinHtml(parts) { return parts.join(''); }
function ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
function padTwo(n) { return n < 10 ? '0' + n : String(n); }
function expandTwoDigitYear(value) {
return 2000 + parseInt(value, 10);
}
function monthToNumber(name) {
var lower = name.toLowerCase().replace(/\.$/, '');
var idx = MONTHS_GEN.indexOf(lower);
if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower);
if (idx === -1 && lower.length >= 3) {
for (var i = 0; i < MONTHS_GEN.length; i++) {
if (MONTHS_GEN[i].indexOf(lower) === 0 || MONTHS_NOM_LOWER[i].indexOf(lower) === 0) return i + 1;
}
}
return idx + 1;
}
function makeStandardDate(yearValue, monthValue, dayValue) {
var yearText = String(yearValue || '').trim();
var year = yearText.length === 2 ? expandTwoDigitYear(yearText) : parseInt(yearText, 10);
var month = parseInt(monthValue, 10);
var day = parseInt(dayValue, 10);
var maxDay;
if ((yearText.length !== 2 && yearText.length !== 4) || isNaN(year) || isNaN(month) || isNaN(day) || year < 1 || month < 1 || month > 12 || day < 1) return null;
maxDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
if (day > maxDay) return null;
return year + '-' + padTwo(month) + '-' + padTwo(day);
}
function normalizeIsoDate(value) {
var m = String(value || '').trim().match(RE_DATE_ISO);
return m ? makeStandardDate(m[1], m[2], m[3]) : null;
}
function normalizeTemplateName(name) {
return (name || '').toLowerCase().replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
}
function getDate(dateString) {
var d = dateString ? new Date(dateString) : new Date();
var iso = d.getUTCFullYear() + '-' + padTwo(d.getUTCMonth() + 1) + '-' + padTwo(d.getUTCDate());
var rus = d.getUTCDate() + ' ' + MONTHS_GEN[d.getUTCMonth()] + ' ' + d.getUTCFullYear();
return [iso, rus];
}
function convertToStandardDate(dateStr) {
var value = String(dateStr || '').replace(/\s+/g, ' ').trim();
var m;
var mo;
var normalized;
m = value.match(RE_DATE_ISO);
if (m) return normalizeIsoDate(value) || '';
m = value.match(RE_DATE_RUSSIAN);
if (m) {
mo = monthToNumber(m[2]);
normalized = mo ? makeStandardDate(m[3], mo, m[1]) : null;
return normalized || '';
}
m = value.match(RE_DATE_DASH);
if (m) {
normalized = makeStandardDate(m[1], m[2], m[3]);
return normalized || '';
}
m = value.match(RE_DATE_DOT);
if (m) {
normalized = makeStandardDate(m[3], m[2], m[1]);
return normalized || '';
}
m = value.match(RE_DATE_SLASH);
if (m) {
normalized = makeStandardDate(m[1], m[2], m[3]);
return normalized || '';
}
return value;
}
function getTalkPage(pageName) {
var match = /([^:]*:)?(.*)/.exec(pageName);
if (match[1]) {
var ns = mwCfg.wgNamespaceIds[match[1].slice(0, -1).toLowerCase().replace(/ /g, '_')];
if (ns !== undefined) return mwCfg.wgFormattedNamespaces[ns | 1] + ':' + match[2];
}
return 'Обсуждение:' + pageName;
}
function normTitle(s) { return (s || '').replace(/_/g, ' '); }
function stripCatPrefix(s) { return (s || '').replace(/^Категория:\s*/i, ''); }
function normalizeCategoryPageName(value) {
var title = normTitle(value).trim();
var nsMatch, nsKey, ns;
if (!title) return '';
nsMatch = title.match(/^([^:]+):(.+)$/);
if (nsMatch) {
nsKey = nsMatch[1].toLowerCase().replace(/ /g, '_');
ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[nsKey];
if (ns === 14) return (mwCfg.wgFormattedNamespaces[14] || 'Категория') + ':' + nsMatch[2].trim();
return title;
}
return (mwCfg.wgFormattedNamespaces[14] || 'Категория') + ':' + title;
}
function makeSummary(text) { return scriptLink + ': ' + text; }
function appendNominationSignature(text) {
var body = String(text || '');
return body + (signatureSeparator ? ' ' + signatureSeparator : '') + ' ~~' + '~~';
}
function extractDisplayedText(s) {
return (s || '').replace(/\[\[:?(?:[^|\]]+\|)?(.+?)\]\]/g, '$1');
}
function collectInputValues(selector) {
return $(selector).map(function () { return $(this).val().trim(); }).get().filter(Boolean);
}
function applyCoreConfigDefaults(config) {
var defaults = {
scriptLink: '[[Участник:Solidest/Remover|Remover]]',
fastRemoveReasons: {
general: [
['уд-бессвязно', 'О1 Бессвязный текст'],
['уд-тест', 'О2 Тестовая страница'],
['уд-ванд', 'О3 Вандальная страница'],
['уд-повторно', 'О4 Уже удалялось'],
['уд-автор', 'О5 По просьбе автора'],
['уд-обс', 'О6 Ненужная подстраница'],
['уд-переим', 'О7 Для переименования'],
['уд-дубль', 'О8 Дубликат'],
['уд-реклама', 'О9 Реклама или спам'],
['db-badtalk', 'О10 Нецелевая СО'],
['уд-копивио', 'О11 Нарушение АП']
],
articles: [
['подст:ds', 'ds Отсроченное пусто или коротко', 'С'],
['уд-пусто', 'С1 Пусто или коротко'],
['уд-иностр', 'С2 Не на русском'],
['уд-ссылки', 'С3 Лишь ссылки'],
['уд-нз', 'С5 Явно незначимо'],
['уд-бям', 'С7 Создано нейросетью']
],
redirects: [
['уд-в никуда', 'П1 Перенапр. в никуда'],
['db-redirspace', 'П2 Межпростр. перенапр.'],
['уд-опечатка', 'П3 Перенапр. с опечаткой'],
['уд-падеж', 'П4 Не именительный падеж'],
['уд-смысл', 'П5 Неверное перенапр.'],
['db-redirtalk', 'П6 Перенапр. на СО']
],
files: [
['db-duplicate', 'Ф1 Копия файла'],
['db-badimage', 'Ф2 Повреждённый файл'],
['подст:nld', 'Ф3 Нет данных о лицензии'],
['подст:nsd', 'Ф3 Нет данных о источнике'],
['подст:nad', 'Ф3 Нет данных о авторе'],
['подст:dd', 'Ф3 Сомнительные данные файла'],
['подст:ofud', 'Ф4 Неиспользуемый КДИ'],
['подст:dfud', 'Ф5 Нет КДИ'],
['db-badfairuse', 'Ф6 Неоправданное КДИ'],
['подст:rfu', 'Ф7 Заменяемый КДИ'],
['NCT', 'Ф8 Есть на Складе'],
['подст:Nothost', 'Ф9 Файл — ВП:НЕХОСТИНГ']
],
categories: [
['уд-пусткат', 'К1 Пустая категория'],
['db-templatecat', 'К1.2 Разобранная служебная кат.'],
['уд-перекат', 'К2 Переименованная кат.']
],
users: [
['уд-владелец', 'У1 По желанию владельца'],
['уд-анон', 'У2 Устаревшая СО анонима'],
['уд-несущ', 'У3 Несуществующий участник'],
['уд-нецелевое', 'У4 Нецелевое использ. ЛП'],
['уд-неактив', 'У5 Подстраница неактивного']
],
special: [
['db', 'Особый случай']
]
},
fastRemoveCriteriaAnchors: {
'подст:ds': 'С1',
deleteslow: 'С1',
ds: 'С1'
},
requiredParamTemplates: {
'уд-переим': 'страницу, которую нужно переименовать',
'уд-дубль': 'страницу-дубликат',
'уд-копивио': 'URL источника нарушения АП',
'db-duplicate': 'имя файла-оригинала',
'подст:rfu': 'имя заменяемого файла',
'NCT': 'имя файла на Викискладе',
'уд-перекат': 'новое название категории',
'db': '!причину удаления'
},
categoryTemplates: {
discuss: 'Обсуждаемая категория|обсуждаемая категория|Acat|acat|ОКТО|окто|Категория к обсуждению|категория к обсуждению',
rename: 'Категория к переименованию|категория к переименованию|Anacat|anacat',
merge: 'Категория к объединению|категория к объединению|Amergecat|amergecat|Cfm|cfm',
discussed: 'Обсуждавшаяся категория|обсуждавшаяся категория|Обсуждалась|обсуждалась|Обсуждалось|обсуждалось'
},
modalStyles: {
border: '1px solid var(--border-color-progressive, #3366bb)',
background: 'var(--background-color-base, #f8f9fa)',
borderRadius: '6px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
headerColor: 'var(--color-progressive, #3366bb)'
}
};
config.scriptLink = config.scriptLink || defaults.scriptLink;
config.fastRemoveReasons = $.extend({}, defaults.fastRemoveReasons, config.fastRemoveReasons || {});
config.fastRemoveCriteriaAnchors = $.extend({}, defaults.fastRemoveCriteriaAnchors, config.fastRemoveCriteriaAnchors || {});
config.requiredParamTemplates = $.extend({}, defaults.requiredParamTemplates, config.requiredParamTemplates || {});
config.categoryTemplates = $.extend({}, defaults.categoryTemplates, config.categoryTemplates || {});
config.modalStyles = $.extend({}, defaults.modalStyles, config.modalStyles || {});
return config;
}
function clonePlainObject(obj) {
return JSON.parse(JSON.stringify(obj || {}));
}
function normalizeMenuLabel(value) {
return String(value || '').replace(/\s+/g, ' ').trim().toLowerCase();
}
function readSettingsOptionState(fallback) {
var base = clonePlainObject(fallback || {});
var stored;
var raw = null;
if (!mw.user || !mw.user.options || typeof mw.user.options.get !== 'function') return base;
stored = mw.user.options.get(settingsOptionName);
if (typeof stored === 'string' && stored.trim()) {
try {
raw = JSON.parse(stored);
} catch (e) {
console.warn('RemoverCore: не удалось прочитать сохранённые настройки полностью, будут использованы данные loader.', e);
}
}
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return base;
return $.extend({}, raw, base, ('quickPhrases' in raw) ? { quickPhrases: raw.quickPhrases } : {});
}
function normalizeQuickPhraseValue(value) {
return String(value == null ? '' : value).replace(/\r\n?/g, '\n').trim();
}
function normalizeQuickPhrasesList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
var normalized = [];
(source || []).forEach(function (value) {
var phrase = normalizeQuickPhraseValue(value);
if (phrase && normalized.indexOf(phrase) === -1) normalized.push(phrase);
});
return normalized;
}
function collectSettingsMenuMeta() {
var labels = [];
var articleLabels = [];
var categoryLabels = [];
var idToLabel = {};
var labelByNorm = {};
var labelOrder = {};
function collect(items, targetLabels) {
items.forEach(function (item) {
var id;
var label;
var normLabel;
if (!item || item.type === 'separator') return;
id = String(item.id || '').trim();
label = String(item.label || '').trim();
normLabel = normalizeMenuLabel(label);
if (id && label && !(id in idToLabel)) idToLabel[id] = label;
if (label && targetLabels.indexOf(label) === -1) targetLabels.push(label);
if (normLabel && !(normLabel in labelByNorm)) {
labelByNorm[normLabel] = label;
labelOrder[label] = labels.length;
labels.push(label);
}
});
}
collect(cfg.articleMenuItems, articleLabels);
collect(cfg.categoryMenuItems, categoryLabels);
return {
labels: labels,
articleLabels: articleLabels,
categoryLabels: categoryLabels,
idToLabel: idToLabel,
labelByNorm: labelByNorm,
labelOrder: labelOrder
};
}
function buildSettingsMenuItemsHint() {
var parts = [];
if (settingsArticleItemLabels.length) {
parts.push('<div class="rmSettingsHintRow"><span class="rmSettingsHintBadge">Статьи</span>' + escapeHtml(settingsArticleItemLabels.join(', ')) + '.</div>');
}
if (settingsCategoryItemLabels.length) {
parts.push('<div class="rmSettingsHintRow"><span class="rmSettingsHintBadge">Категории</span>' + escapeHtml(settingsCategoryItemLabels.join(', ')) + '.</div>');
}
return parts.length ? '<div class="rmSettingsHintList">' + parts.join('') + '</div>' : '';
}
function getDefaultSettings() {
return {
version: settingsVersion,
excludedNamespaces: normalizeNumberList(cfg.excludedNamespaces, []),
notifyAuthor: !!cfg.defaultNotifyAuthor,
subscribeTopic: !!cfg.defaultSubscribeTopic,
menuTitle: (typeof cfg.menuTitle === 'string' && cfg.menuTitle.trim()) ? cfg.menuTitle.trim() : 'Remover',
disabledItems: [],
quickPhrases: normalizeQuickPhrasesList(cfg.quickPhrases, []),
showMenuIcons: !!cfg.showMenuIcons,
signatureSeparator: (typeof cfg.signatureSeparator === 'string') ? cfg.signatureSeparator.trim() : ''
};
}
function normalizeNumberList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
return source
.map(function (value) { return parseInt(value, 10); })
.filter(function (value, index, arr) { return !isNaN(value) && arr.indexOf(value) === index; })
.sort(function (a, b) { return a - b; });
}
function normalizeDisabledItemValue(value) {
var token = String(value || '').trim();
if (!token) return null;
if (settingsItemLabelById[token]) return settingsItemLabelById[token];
return settingsItemLabelByNorm[normalizeMenuLabel(token)] || null;
}
function compareSettingsMenuLabels(a, b) {
var ai = Object.prototype.hasOwnProperty.call(settingsItemLabelOrder, a) ? settingsItemLabelOrder[a] : Number.MAX_SAFE_INTEGER;
var bi = Object.prototype.hasOwnProperty.call(settingsItemLabelOrder, b) ? settingsItemLabelOrder[b] : Number.MAX_SAFE_INTEGER;
if (ai !== bi) return ai - bi;
return a.localeCompare(b, 'ru');
}
function normalizeDisabledItemsList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
return source
.map(normalizeDisabledItemValue)
.filter(function (value, index, arr) { return value && arr.indexOf(value) === index; })
.sort(compareSettingsMenuLabels);
}
function normalizeMenuTitleSetting(value, fallback) {
var menuTitle = String(value || '').trim();
if (!menuTitle) return fallback;
if (menuTitle === MENU_TITLE_PRESET_CACTIONS || /^(#)?p-cactions$/i.test(menuTitle)) return MENU_TITLE_PRESET_CACTIONS;
if (menuTitle === MENU_TITLE_PRESET_PAGE || /^(#)?p-page$/i.test(menuTitle) || /^(#)?p-actions$/i.test(menuTitle)) return MENU_TITLE_PRESET_PAGE;
if (menuTitle === MENU_TITLE_PRESET_TOOLS || /^(#)?p-tb$/i.test(menuTitle)) return MENU_TITLE_PRESET_TOOLS;
return menuTitle;
}
function normalizeRemoverSettings(raw) {
var defaults = clonePlainObject(settingsDefaults);
var source = (raw && typeof raw === 'object') ? raw : {};
return {
version: settingsVersion,
excludedNamespaces: normalizeNumberList(source.excludedNamespaces, defaults.excludedNamespaces || []),
notifyAuthor: ('notifyAuthor' in source) ? !!source.notifyAuthor : !!defaults.notifyAuthor,
subscribeTopic: ('subscribeTopic' in source) ? !!source.subscribeTopic : !!defaults.subscribeTopic,
menuTitle: normalizeMenuTitleSetting(
(typeof source.menuTitle === 'string' && source.menuTitle.trim()) ? source.menuTitle : '',
typeof defaults.menuTitle === 'string' && defaults.menuTitle.trim() ? defaults.menuTitle.trim() : 'Remover'
),
disabledItems: normalizeDisabledItemsList(source.disabledItems, defaults.disabledItems || []),
quickPhrases: normalizeQuickPhrasesList(source.quickPhrases, defaults.quickPhrases || []),
showMenuIcons: ('showMenuIcons' in source) ? !!source.showMenuIcons : !!defaults.showMenuIcons,
signatureSeparator: (typeof source.signatureSeparator === 'string')
? source.signatureSeparator.trim()
: (typeof defaults.signatureSeparator === 'string' ? defaults.signatureSeparator.trim() : '')
};
}
function areRemoverSettingsEqual(a, b) {
return JSON.stringify(normalizeRemoverSettings(a)) === JSON.stringify(normalizeRemoverSettings(b));
}
function updateStoredSettingsState(settings, skipUserOptionsSync) {
var normalized = normalizeRemoverSettings(settings);
state.settings = clonePlainObject(normalized);
setAlert = normalized.notifyAuthor;
setSubscribe = normalized.subscribeTopic;
signatureSeparator = normalized.signatureSeparator;
state.setAlert = setAlert;
state.setSubscribe = setSubscribe;
if (!skipUserOptionsSync && mw.user && mw.user.options && typeof mw.user.options.set === 'function') {
mw.user.options.set(settingsOptionName, JSON.stringify(normalized));
}
return normalized;
}
function splitSettingsInput(value) {
return String(value || '')
.split(/[\s,;]+/)
.map(function (part) { return part.trim(); })
.filter(Boolean);
}
function splitSettingsListInput(value) {
return String(value || '')
.split(/[\n,;]+/)
.map(function (part) { return part.trim(); })
.filter(Boolean);
}
function parseNamespaceInput(value) {
var tokens = splitSettingsInput(value);
var invalid = [];
var values = [];
tokens.forEach(function (token) {
var parsed = parseInt(token, 10);
if (String(parsed) !== token) invalid.push(token);
else if (values.indexOf(parsed) === -1) values.push(parsed);
});
values.sort(function (a, b) { return a - b; });
return { values: values, invalid: invalid };
}
function parseDisabledItemsInput(value) {
var tokens = splitSettingsListInput(value);
var invalid = [];
var values = [];
tokens.forEach(function (token) {
var normalized = normalizeDisabledItemValue(token);
if (!normalized) invalid.push(token);
else if (values.indexOf(normalized) === -1) values.push(normalized);
});
values.sort(compareSettingsMenuLabels);
return { values: values, invalid: invalid };
}
function formatPagesWithAnd(names, prefix) {
var p = prefix || ':';
var links = (names || []).map(function (n) { return '[[' + p + n + ']]'; });
if (!links.length) return '';
if (links.length === 1) return links[0];
return links.slice(0, -1).join(', ') + ' и ' + links[links.length - 1];
}
function formatCatLink(name) { return '[[:Категория:' + name + ']]'; }
function formatMergeStatus(status) {
return { already_exists: 'уже был', updated: 'дополнен', created: 'создан' }[status] || status;
}
function applyGeneratedText($el, generated) {
var cur = $el.val();
var prev = $el.data('rmGenerated') || '';
if (!prev || cur.indexOf(prev) === 0) {
$el.val(generated + cur.slice(prev.length));
} else {
$el.val(generated + (cur ? '\n' + cur : ''));
}
$el.data('rmGenerated', generated);
}
function getCurrentQuickPhrases() {
return normalizeQuickPhrasesList(
state.settings && state.settings.quickPhrases,
settingsDefaults.quickPhrases || []
);
}
function insertTextIntoTextarea($el, text) {
var el = $el && $el[0];
var value;
var start;
var end;
var updatedValue;
var caretPos;
if (!el) return;
text = String(text || '');
if (!text) return;
value = $el.val() || '';
start = typeof el.selectionStart === 'number' ? el.selectionStart : value.length;
end = typeof el.selectionEnd === 'number' ? el.selectionEnd : start;
updatedValue = value.slice(0, start) + text + value.slice(end);
caretPos = start + text.length;
$el.val(updatedValue).trigger('input').trigger('change').focus();
if (typeof el.setSelectionRange === 'function') el.setSelectionRange(caretPos, caretPos);
}
function buildQuickPhrasesPanelHtml(textareaId) {
var phrases = getCurrentQuickPhrases();
if (!phrases.length) return '';
return joinHtml([
'<div class="rmQuickPhrasesPanel ', RESIZE_CLASS, '" data-rm-target="', textareaId, '">',
phrases.map(function (phrase) {
return joinHtml([
'<button type="button" class="rmQuickPhraseActionBtn" data-rm-target="', textareaId,
'" data-rm-phrase="', escapeHtml(phrase), '">',
escapeHtml(phrase),
'</button>'
]);
}).join(''),
'</div>'
]);
}
function getMultiNominationCommentText(commentsByArticle, articleTitle) {
var key = normTitle(articleTitle);
if (!commentsByArticle || !Object.prototype.hasOwnProperty.call(commentsByArticle, key)) return '';
return normalizeQuickPhraseValue(commentsByArticle[key]);
}
function buildMultiNominationText(articles, bodyText, commentsByArticle, options) {
var opts = options || {};
var list = Array.isArray(articles) ? articles.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasArticleComments = false;
var headingLevel = Math.max(2, parseInt(opts.headingLevel, 10) || 3);
var headingMarks = new Array(headingLevel + 1).join('=');
var articleSections = list.map(function (a) {
var comment = getMultiNominationCommentText(commentsByArticle, a);
if (comment) hasArticleComments = true;
return '\n' + headingMarks + ' [[:' + a + ']] ' + headingMarks + '\n' + (comment ? appendNominationSignature(comment) + '\n' : '');
}).join('');
var commonSectionText = body
? appendNominationSignature(body)
: (hasArticleComments ? '' : appendNominationSignature(''));
return articleSections + '\n' + headingMarks + ' По всем ' + headingMarks + '\n' + commonSectionText;
}
function buildMultiNominationListText(pages, bodyText, commentsByPage) {
var list = Array.isArray(pages) ? pages.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasPageComments = false;
var pageLines = list.map(function (pageName) {
var comment = getMultiNominationCommentText(commentsByPage, pageName);
if (comment) hasPageComments = true;
return '* [[:' + pageName + ']]' + (comment ? '\n*: ' + appendNominationSignature(comment) : '');
}).join('\n');
var commonText = body
? appendNominationSignature(body)
: (hasPageComments ? '' : appendNominationSignature(''));
return pageLines + (pageLines && commonText ? '\n' : '') + commonText;
}
function collectMultiNominationComments(normalizePageName) {
var comments = {};
var normalize = typeof normalizePageName === 'function' ? normalizePageName : normTitle;
$('.rmMultiArticleBlock').each(function () {
var $block = $(this);
var article = normalize(($block.find('.rmArticleInput').val() || '').trim());
var comment = normalizeQuickPhraseValue($block.find('.rmArticleCommentInput').val());
if (!article) return;
comments[article] = comment;
});
return comments;
}
function getNominationPublishText(job) {
if (job && job.isMulti) return String(job.msg || '');
return appendNominationSignature(job && job.msg);
}
function getNominationConflictRule(job) {
if (!job || job.mode !== 'nominate') return null;
if (job.opId === 'tRm' || job.opId === 'mRm') {
return {
id: 'ku',
label: 'КУ',
namePattern: '(?:к\\s*удалению|ку)',
detect: function (articleText) {
var match = String(articleText || '').match(/\{\{\s*((?:к\s*удалению|ку))\s*(?:\||\}\})/i);
if (!match) return null;
var templateName = ucfirst(String(match[1] || '').replace(/\s+/g, ' ').trim());
return {
label: 'КУ',
templateName: templateName || 'КУ',
templateDisplay: '{{' + (templateName || 'КУ') + '}}'
};
}
};
}
return null;
}
function detectNominationConflict(articleText, job) {
var rule = getNominationConflictRule(job);
if (!rule || typeof rule.detect !== 'function') return null;
return rule.detect(articleText);
}
function getConflictDecisionForPage(job, pageName) {
var decisions = job && job.conflictDecisions;
var key = normTitle(pageName);
return decisions && decisions[key] ? decisions[key] : null;
}
function getCategoryMergeRe() {
return new RegExp('\\{\\{\\s*(?:' + cfg.categoryTemplates.merge + ')\\s*\\|\\s*([^\\}]+)\\}\\}', 'i');
}
function eachSequential(targets, iteratee, done) {
var i = 0;
(function next(err) {
if (err || i >= targets.length) { done(err || null); return; }
iteratee(targets[i++], next);
}(null));
}
function normalizeSectionForLink(sectionTitle) {
return (sectionTitle || '').trim()
.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, function (_, target, label) {
var v = (label || target || '').trim();
return v.charAt(0) === ':' ? v.slice(1) : v;
})
.replace(/''+/g, '').replace(/\s+/g, ' ').trim();
}
function getViewportWidth() {
return Math.floor(Math.max(
(document.documentElement && document.documentElement.clientWidth) || 0,
(typeof window.innerWidth === 'number' && window.innerWidth) || 0,
$(window).width() || 0
));
}
function getVisualViewportWidth() {
var widths = [];
if (window.visualViewport && typeof window.visualViewport.width === 'number' && window.visualViewport.width > 0) widths.push(window.visualViewport.width);
if (window.screen && window.screen.width > 0) widths.push(window.screen.width);
return widths.length ? Math.floor(Math.min.apply(Math, widths)) : getViewportWidth();
}
function isTouchModalDevice() {
return !!(
(window.matchMedia && window.matchMedia('(pointer: coarse)').matches) ||
('ontouchstart' in window) ||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 0) ||
(navigator.msMaxTouchPoints && navigator.msMaxTouchPoints > 0)
);
}
function getModalLayout() {
var minWidth = parseInt(sz.taMinW, 10) || 180;
var layoutWidth = getViewportWidth();
var visualWidth = getVisualViewportWidth();
var contentWidth = Math.max(minWidth, Math.floor($('#content').width() || $('#content').innerWidth() || $(window).width() || minWidth));
var isMobile = layoutWidth <= sz.mobileBp;
var isTouchDesktop = !isMobile &&
isTouchModalDevice() &&
visualWidth > 0 &&
visualWidth <= sz.mobileBp &&
layoutWidth >= visualWidth + sz.touchDesktopGap;
var useFullWidth = isMobile || isTouchDesktop;
var maxOuterWidth;
var defaultOuterWidth;
var desktopWidth;
if (isMobile) maxOuterWidth = Math.max(minWidth, (visualWidth || contentWidth) - sz.viewportGap);
else if (isTouchDesktop) maxOuterWidth = Math.max(minWidth, contentWidth - 32);
else maxOuterWidth = Math.max(minWidth, Math.min(contentWidth, (visualWidth ? visualWidth - sz.viewportGap : contentWidth)));
desktopWidth = Math.max(minWidth, Math.floor(contentWidth * sz.modalRatio));
if (useFullWidth || desktopWidth < sz.modalMinWide) defaultOuterWidth = contentWidth;
else if (desktopWidth < sz.modalDefaultWide) defaultOuterWidth = sz.modalDefaultWide;
else defaultOuterWidth = desktopWidth;
return {
minWidth: minWidth,
isMobile: isMobile,
isTouchDesktop: isTouchDesktop,
useFullWidth: useFullWidth,
shouldCenter: useFullWidth || mwCfg.skin === 'minerva',
maxOuterWidth: maxOuterWidth,
defaultOuterWidth: Math.min(defaultOuterWidth, maxOuterWidth)
};
}
function getDefaultResizableWidth(frameWidth) {
var layout = getModalLayout();
return Math.max(layout.minWidth, layout.defaultOuterWidth - Math.floor(frameWidth || 0));
}
function getBoxFrameWidth($el) {
function px(prop) {
var n = parseFloat($el.css(prop));
return isNaN(n) ? 0 : n;
}
return px('padding-left') + px('padding-right') + px('border-left-width') + px('border-right-width');
}
// ═══════════════════════════════════════════════════════════════════════════
// API
// ═══════════════════════════════════════════════════════════════════════════
function getApiUrl() {
return (mw.util && typeof mw.util.wikiScript === 'function') ? mw.util.wikiScript('api') : '/w/api.php';
}
function getCsrfTokenValue() {
return (mw.user && mw.user.tokens && typeof mw.user.tokens.get === 'function')
? mw.user.tokens.get('csrfToken')
: null;
}
function storeCsrfToken(token) {
if (!token || !mw.user || !mw.user.tokens || typeof mw.user.tokens.set !== 'function') return;
mw.user.tokens.set({ csrfToken: token });
}
function isValidCsrfToken(token) {
return typeof token === 'string' && !!token && token !== '+\\';
}
function fetchCsrfToken(forceRefresh, callback) {
var cachedToken = getCsrfTokenValue();
if (!forceRefresh && isValidCsrfToken(cachedToken)) {
callback(cachedToken);
return;
}
$.ajax({
url: getApiUrl(),
method: 'GET',
dataType: 'json',
data: { action: 'query', meta: 'tokens', type: 'csrf', format: 'json' }
})
.done(function (data) {
var token = data && data.query && data.query.tokens && data.query.tokens.csrftoken;
if (isValidCsrfToken(token)) {
storeCsrfToken(token);
callback(token);
return;
}
callback(null);
})
.fail(function () {
callback(null);
});
}
function apiReq(params, mode, callback) {
var isWrite = mode === 'edit' || mode === 'discussiontoolssubscribe' || mode === 'options';
function sendRequest(retryWithFreshToken) {
var reqParams = $.extend({}, params, { format: 'json', action: mode });
if (!isWrite) {
$.ajax({ url: getApiUrl(), method: 'GET', data: reqParams, dataType: 'json' })
.done(function (data) { if (callback) callback(data); })
.fail(function (jqXHR, status) {
console.error('Ошибка API: ' + status);
if (callback) callback({ error: { code: 'network', info: status } });
});
return;
}
fetchCsrfToken(!!retryWithFreshToken, function (token) {
if (!isValidCsrfToken(token)) {
if (callback) callback({ error: { code: 'badtoken', info: 'Не удалось получить CSRF-токен.' } });
return;
}
reqParams.token = token;
$.ajax({ url: getApiUrl(), method: 'POST', data: reqParams, dataType: 'json' })
.done(function (data) {
var err = data && data.error;
var isBadToken = err && (err.code === 'badtoken' || /invalid csrf token/i.test(String(err.info || '')));
if (isBadToken && !retryWithFreshToken) {
sendRequest(true);
return;
}
if (callback) callback(data);
})
.fail(function (jqXHR, status) {
console.error('Ошибка API: ' + status);
if (callback) callback({ error: { code: 'network', info: status } });
});
});
}
sendRequest(false);
}
function saveSettingsToServer(settings, callback) {
var normalized = normalizeRemoverSettings(settings);
if (!mwCfg.wgUserName) {
callback({ code: 'notloggedin', info: 'Сохранять настройки можно только после входа в учётную запись.' });
return;
}
apiReq({ optionname: settingsOptionName, optionvalue: JSON.stringify(normalized) }, 'options', function (resp) {
if (resp && resp.options === 'success') {
callback(null, updateStoredSettingsState(normalized));
return;
}
callback((resp && resp.error) || { code: 'options_failed', info: 'Не удалось сохранить настройки.' });
});
}
function resetSettingsOnServer(callback) {
if (!mwCfg.wgUserName) {
callback({ code: 'notloggedin', info: 'Сбрасывать настройки можно только после входа в учётную запись.' });
return;
}
apiReq({ change: settingsOptionName }, 'options', function (resp) {
if (resp && resp.options === 'success') {
callback(null, updateStoredSettingsState(settingsDefaults, true));
return;
}
callback((resp && resp.error) || { code: 'options_failed', info: 'Не удалось сбросить настройки.' });
});
}
function getFirstQueryPage(data) {
var pages = data && data.query && data.query.pages;
if (!pages) return null;
return pages[Object.keys(pages)[0]] || null;
}
function extractRevisionContent(rev) {
if (!rev) return null;
if (typeof rev['*'] === 'string') return rev['*'];
if (rev.slots && rev.slots.main) {
if (typeof rev.slots.main['*'] === 'string') return rev.slots.main['*'];
if (typeof rev.slots.main.content === 'string') return rev.slots.main.content;
}
return null;
}
function makeReadError(apiError, fallbackCode, fallbackInfo) {
var err = apiError || {};
return {
code: err.code || fallbackCode || 'read_failed',
info: err.info || fallbackInfo || 'Не удалось получить содержимое.'
};
}
function getTextWithTimestamp(pageName, callback) {
apiReq({ prop: 'revisions', rvprop: 'content|timestamp', rvslots: 'main', titles: pageName }, 'query', function (data) {
var content;
var page;
if (data && data.error) {
callback(null, null, data.error);
return;
}
if (!data || !data.query || !data.query.pages) {
callback(null, null, { code: 'read_failed', info: 'Некорректный ответ API при чтении страницы.' });
return;
}
page = getFirstQueryPage(data);
if (!page) {
callback(null, null, { code: 'read_failed', info: 'API не вернул данные страницы.' });
return;
}
if (page.invalid !== undefined) {
callback(null, null, { code: 'invalidtitle', info: page.invalidreason || 'Некорректное название страницы.' });
return;
}
if (page.missing !== undefined) {
callback(null, null, null);
return;
}
if (!page.revisions || !page.revisions.length) {
callback(null, null, { code: 'read_failed', info: 'API не вернул ревизии страницы.' });
return;
}
content = extractRevisionContent(page.revisions[0]);
if (content === null) {
callback(null, null, { code: 'content_missing', info: 'API не вернул текст страницы.' });
return;
}
callback(content, page.revisions[0].timestamp || null, null);
});
}
function getText(pageName, callback) {
getTextWithTimestamp(pageName, function (text, baseTimestamp, err) { callback(text, err); });
}
function editPageContent(pageTitle, options, buildFn, callback) {
var opts = options || {};
var maxRetries = Math.max(0, parseInt(opts.editConflictRetries, 10) || 1);
(function attempt(retry) {
getTextWithTimestamp(pageTitle, function (sourceText, baseTimestamp, readErr) {
if (readErr) {
callback(makeReadError(readErr, opts.readErrorCode || 'read_failed', 'Не удалось получить содержимое страницы «' + pageTitle + '».'));
return;
}
if (sourceText === null) {
callback({ code: opts.readErrorCode || 'read_failed', info: opts.readError || 'Страница «' + pageTitle + '» не существует.' });
return;
}
var done = (function () {
var called = false;
return function (result) {
if (called) return;
called = true;
if (!result || result.error) { callback((result && result.error) || { code: 'build_failed', info: 'Не удалось подготовить правку.' }, result && result.meta || null); return; }
if (result.skip) { callback(null, result.meta || null); return; }
if (typeof result.text !== 'string') { callback({ code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
var ep = { title: pageTitle, text: result.text, summary: result.summary || opts.summary || '' };
if (opts.watchlist) ep.watchlist = opts.watchlist;
if (opts.assertuser) ep.assertuser = opts.assertuser;
if (opts.createonly) ep.createonly = opts.createonly;
if (opts.useBaseTimestamp !== false && baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) {
var err = resp && resp.error ? resp.error : null;
if (err && err.code === 'editconflict' && retry < maxRetries) { attempt(retry + 1); return; }
callback(err, result.meta || null);
});
};
}());
var maybe = buildFn(sourceText, done);
if (maybe !== undefined) done(maybe);
});
}(0));
}
// ═══════════════════════════════════════════════════════════════════════════
// ШАБЛОНЫ: удаление и вставка
// ═══════════════════════════════════════════════════════════════════════════
function findBalancedTemplateEnd(text, start) {
var depth = 0;
var i = start;
var len = text.length;
while (i < len - 1) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') {
depth++;
i += 2;
continue;
}
if (text.charAt(i) === '}' && text.charAt(i + 1) === '}') {
depth--;
i += 2;
if (depth === 0) return i;
continue;
}
i++;
}
return -1;
}
function splitTemplateTopLevelParts(innerText) {
var parts = [];
var start = 0;
var templateDepth = 0;
var linkDepth = 0;
var i = 0;
var text = String(innerText || '');
while (i < text.length) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') {
templateDepth++;
i += 2;
continue;
}
if (text.charAt(i) === '}' && text.charAt(i + 1) === '}' && templateDepth > 0) {
templateDepth--;
i += 2;
continue;
}
if (text.charAt(i) === '[' && text.charAt(i + 1) === '[') {
linkDepth++;
i += 2;
continue;
}
if (text.charAt(i) === ']' && text.charAt(i + 1) === ']' && linkDepth > 0) {
linkDepth--;
i += 2;
continue;
}
if (text.charAt(i) === '|' && templateDepth === 0 && linkDepth === 0) {
parts.push(text.slice(start, i));
start = i + 1;
}
i++;
}
parts.push(text.slice(start));
return parts;
}
function getTemplateMatchAt(text, start, nameRe) {
var end = findBalancedTemplateEnd(text, start);
var parts;
var rawName;
var name;
if (end < 0) return null;
parts = splitTemplateTopLevelParts(text.slice(start + 2, end - 2));
rawName = String(parts.shift() || '').trim();
name = rawName.replace(/^(?:subst|подст)\s*:\s*/i, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
if (!nameRe.test(name)) return null;
return {
start: start,
end: end,
text: text.slice(start, end),
name: rawName,
params: parts.map(function (part) { return part.trim(); })
};
}
function findTemplateByPattern(text, namePattern) {
var source = String(text || '');
var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i');
var i = 0;
var end;
var match;
while (i < source.length - 1) {
if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') {
match = getTemplateMatchAt(source, i, nameRe);
if (match) return match;
end = findBalancedTemplateEnd(source, i);
if (end > i) { i = end; continue; }
}
i++;
}
return null;
}
function getTemplateRemovalRange(text, match) {
var start = match.start;
var end = match.end;
var before = text.slice(0, start).match(/<noinclude>\s*$/i);
var after;
if (before) {
after = text.slice(end).match(/^\s*<\/noinclude>\s*\n?/i);
if (after) {
return { start: before.index, end: end + after[0].length };
}
}
after = text.slice(end).match(/^[ \t]*(?:\r?\n)?/);
if (after) end += after[0].length;
return { start: start, end: end };
}
function stripTemplatesByPattern(text, namePattern) {
var source = String(text || '');
var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i');
var ranges = [];
var out = [];
var pos = 0;
var i = 0;
var end;
var match;
var range;
while (i < source.length - 1) {
if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') {
match = getTemplateMatchAt(source, i, nameRe);
if (match) {
range = getTemplateRemovalRange(source, match);
ranges.push(range);
i = Math.max(match.end, range.end);
continue;
}
end = findBalancedTemplateEnd(source, i);
if (end > i) { i = end; continue; }
}
i++;
}
if (!ranges.length) return { text: source, removed: false };
ranges.forEach(function (r) {
if (r.start < pos) r.start = pos;
out.push(source.slice(pos, r.start));
pos = r.end;
});
out.push(source.slice(pos));
return { text: out.join(''), removed: true };
}
function removeTemplatesByAliases(text, aliases) {
var seen = {}, patterns = [];
aliases.forEach(function (alias) {
var tpl = alias.replace(RE_TEMPLATE_NS, '').trim();
var key = tpl.toLowerCase();
if (!tpl || seen[key]) return;
seen[key] = true;
patterns.push(escapeRegExp(tpl).replace(/\\ /g, '[ _]+'));
});
if (!patterns.length) return { text: text, removed: false };
return stripTemplatesByPattern(text, '(?:' + patterns.join('|') + ')');
}
function removeTransferTemplatesLocal(articleText, transferMode) {
var result = { text: articleText, removedKbu: false, removedKul: false, removedHangon: false };
if (transferMode === 'none') return result;
if (transferMode === 'kbu' || transferMode === 'both') {
var kbu = stripTemplatesByPattern(result.text, '(?:' + KBU_PATTERN_STR + ')');
result.text = kbu.text; result.removedKbu = kbu.removed;
}
if (transferMode === 'kul' || transferMode === 'both') {
var kul = stripTemplatesByPattern(result.text, KUL_PATTERN_STR);
result.text = kul.text; result.removedKul = kul.removed;
}
var hangon = stripTemplatesByPattern(result.text, HANGON_PATTERN_STR);
result.text = hangon.text; result.removedHangon = hangon.removed;
return result;
}
function removeTransferTemplatesWithApiFallback(pageName, articleText, transferMode, localResult, callback) {
var needKbu = (transferMode === 'kbu' || transferMode === 'both') && !localResult.removedKbu;
var needKul = (transferMode === 'kul' || transferMode === 'both') && !localResult.removedKul;
var needHangon = transferMode !== 'none' && !localResult.removedHangon;
if (!needKbu && !needKul && !needHangon) { callback(localResult); return; }
var titleMap = {};
if (needKbu) ['Шаблон:К быстрому удалению','Шаблон:К отсроченному удалению','Шаблон:Deleteslow','Шаблон:Ds'].forEach(function (t) { titleMap[t] = true; });
if (needKul) titleMap['Шаблон:К улучшению'] = true;
if (needHangon) { titleMap['Шаблон:Hangon'] = true; titleMap['Шаблон:Hang-on'] = true; }
apiReq({ prop: 'templates', titles: pageName, tllimit: 'max' }, 'query', function (data) {
var page = getFirstQueryPage(data);
if (page && page.templates) {
page.templates.forEach(function (tpl) {
var norm = normalizeTemplateName(tpl.title);
if ((needKbu && (RE_KBU_PATTERNS.test(norm) || norm === 'к быстрому удалению' || norm === 'к отсроченному удалению' || norm === 'deleteslow' || norm === 'ds')) ||
(needKul && RE_KUL_PATTERN.test(norm)) ||
(needHangon && RE_HANGON.test(norm))) {
titleMap[tpl.title] = true;
}
});
}
var titles = Object.keys(titleMap);
if (!titles.length) { callback(localResult); return; }
function normalizeAliasKey(title) { return (title || '').replace(/_/g, ' ').toLowerCase().trim(); }
function collectAndApplyAliases() {
var allAliases = [];
titles.forEach(function (title) { allAliases = allAliases.concat(tplAliasCache[title] || [title]); });
var dedup = {}, aliases = [];
allAliases.forEach(function (alias) {
var key = normalizeAliasKey(alias);
if (!key || dedup[key]) return;
dedup[key] = true; aliases.push(alias);
});
var updated = $.extend({}, localResult);
var r = removeTemplatesByAliases(updated.text, aliases);
updated.text = r.text;
if (r.removed) {
if (needKbu) updated.removedKbu = true;
if (needKul) updated.removedKul = true;
if (needHangon) updated.removedHangon = true;
}
callback(updated);
}
var missingTitles = titles.filter(function (t) { return !tplAliasCache[t]; });
if (!missingTitles.length) { collectAndApplyAliases(); return; }
(function resolveChunk(offset) {
if (offset >= missingTitles.length) { collectAndApplyAliases(); return; }
var chunk = missingTitles.slice(offset, offset + 20);
var chunkByKey = {};
chunk.forEach(function (t) { chunkByKey[normalizeAliasKey(t)] = t; });
apiReq({ prop: 'redirects', rdlimit: 'max', titles: chunk.join('|') }, 'query', function (resp) {
var pages = resp && resp.query && resp.query.pages ? resp.query.pages : {};
Object.keys(pages).forEach(function (pid) {
var p = pages[pid];
if (!p || !p.title) return;
var sourceTitle = chunkByKey[normalizeAliasKey(p.title)];
if (!sourceTitle) return;
var found = [p.title].concat((p.redirects || []).map(function (r) { return r.title; }));
var seen = {}, unique = [];
found.forEach(function (t) { var k = normalizeAliasKey(t); if (!k || seen[k]) return; seen[k] = true; unique.push(t); });
tplAliasCache[sourceTitle] = unique.length ? unique : [sourceTitle];
});
chunk.forEach(function (t) { if (!tplAliasCache[t]) tplAliasCache[t] = [t]; });
resolveChunk(offset + 20);
});
}(0));
});
}
// ─── Вставка шаблонов ────────────────────────────────────────────────────
function findInsertPositionAfterProjectTemplates(text) {
var pos = 0, len = text.length;
while (pos < len) {
var wsMatch = text.slice(pos).match(/^[\t ]*\n/);
if (wsMatch) { pos += wsMatch[0].length; continue; }
if (text.charAt(pos) !== '{' || text.charAt(pos + 1) !== '{') break;
var afterOpen = text.slice(pos + 2);
var nameMatch = afterOpen.match(/^[\s]*([\s\S]*?)[\s]*(?:\||\}\})/);
var templateEnd;
if (!nameMatch) break;
var tplName = nameMatch[1].toLowerCase().replace(/\s+/g, ' ').trim();
if (!/^статья проекта(\s|$)/.test(tplName) && tplName !== 'блок проектов статьи') break;
templateEnd = findBalancedTemplateEnd(text, pos);
if (templateEnd < 0) break;
pos = templateEnd;
if (pos < len && text.charAt(pos) === '\n') pos++;
}
return pos;
}
function insertTplOnTalkPage(text, tplText, sep) {
var s = (sep === undefined) ? '\n' : sep;
var insertPos = findInsertPositionAfterProjectTemplates(text);
if (insertPos === 0) return tplText + (text.length ? s + text.replace(/^\n+/, '') : '');
var before = text.slice(0, insertPos).replace(/\n+$/, '');
var after = text.slice(insertPos).replace(/^\n+/, '');
return before + '\n' + tplText + (after.length ? s + after : '');
}
function wrapInNoinclude(text, templateText) {
var match = text.match(RE_NOINCLUDE);
if (match) {
// Если перед noinclude есть непробельный контент — вставляем новый noinclude сверху
var before = text.slice(0, text.indexOf(match[0]));
if (/\S/.test(before)) {
return '<noinclude>' + templateText + '</noinclude>\n' + text;
}
var content = match[2];
if (content.length > 0 && content.charAt(content.length - 1) !== '\n') content += '\n';
return text.replace(match[0], match[1] + '<noinclude>' + content + templateText + '\n</noinclude>');
}
return '<noinclude>' + templateText + '</noinclude>\n' + text;
}
function upsertRetTemplateOnTalkPage(text, dateIso, sectionTitle) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var tplRe = /\{\{\s*(?:subst\s*:\s*)?(?:оставлено)\s*([^}]*)\}\}/i;
var tplMatch = source.match(tplRe);
function buildTpl(dateValue, sectionValue) {
var tpl = 'оставлено|' + dateValue;
if (sectionValue) tpl += '|l1=' + sectionValue;
return T_OPEN + tpl + T_CLOSE;
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, buildTpl(dateIso, normalizedSection), '\n'), status: 'created' };
}
var parts = tplMatch[1].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); });
var stdDate = convertToStandardDate(dateIso);
if (existingDates.indexOf(stdDate) !== -1) return { text: source, status: 'already_present' };
var nextIdx = existingDates.length + 1;
var suffix = '|' + dateIso + (normalizedSection ? '|l' + nextIdx + '=' + normalizedSection : '');
return {
text: source.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}\s*$/, suffix + '}}'); }),
status: 'updated'
};
}
function buildConditionalRetTemplateText(dateIso, sectionTitle, reasonText, deadlineText, sectionIndex) {
var normalizedSection = String(sectionTitle || '').trim();
var normalizedReason = normalizeQuickPhraseValue(reasonText);
var normalizedDeadline = String(deadlineText || '').trim();
var index = parseInt(sectionIndex, 10);
var tpl = 'условно оставлено|' + dateIso;
if (isNaN(index) || index < 1) index = 1;
if (normalizedSection) tpl += '|l' + index + '=' + normalizedSection;
if (normalizedReason) tpl += '|пояснение=' + normalizedReason;
if (normalizedDeadline) tpl += '|срок=' + normalizedDeadline;
return T_OPEN + tpl + T_CLOSE;
}
function upsertConditionalRetTemplateOnTalkPage(text, dateIso, sectionTitle, reasonText, deadlineText) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var normalizedReason = normalizeQuickPhraseValue(reasonText);
var normalizedDeadline = String(deadlineText || '').trim();
var tplRe = /\{\{\s*(?:subst\s*:\s*)?(?:условно\s*оставлено)\s*([^}]*)\}\}/i;
var tplMatch = source.match(tplRe);
if (!tplMatch) {
return {
text: insertTplOnTalkPage(source, buildConditionalRetTemplateText(dateIso, normalizedSection, normalizedReason, normalizedDeadline, 1), '\n'),
status: 'created'
};
}
var parts = tplMatch[1].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); });
var stdDate = convertToStandardDate(dateIso);
if (existingDates.indexOf(stdDate) !== -1) return { text: source, status: 'already_present' };
var nextIdx = existingDates.length + 1;
var suffix = '|' + dateIso;
if (normalizedSection) suffix += '|l' + nextIdx + '=' + normalizedSection;
if (normalizedReason) suffix += '|пояснение=' + normalizedReason;
if (normalizedDeadline) suffix += '|срок=' + normalizedDeadline;
return {
text: source.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}\s*$/, suffix + '}}'); }),
status: 'updated'
};
}
// ═══════════════════════════════════════════════════════════════════════════
// ПАЙПЛАЙН НОМИНАЦИИ
// ═══════════════════════════════════════════════════════════════════════════
function runNominationPipeline(steps) {
var s = steps;
var ctx = { templateMeta: null, nominationInfo: null };
var stages = [
{
name: 'шаблон',
fn: function (next) {
s.templateStep(function (err, meta) { ctx.templateMeta = meta || null; next(err); });
}
},
{
name: 'номинация',
pendingText: 'Публикуется номинация...',
successText: 'Номинация опубликована.',
errorText: 'Публикация номинации.',
fn: function (next) {
s.nominationStep(function (err, info) { ctx.nominationInfo = info || null; next(err); });
}
},
{
name: 'подписка',
shouldRun: function () {
var info = ctx.nominationInfo;
return !!(setSubscribe && info && info.pageTitle && info.sectionTitle);
},
fn: function (next) {
subscribeToTopic(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle, function () { next(); });
}
},
{
name: 'оповещение',
shouldRun: function () { return !!(setAlert && !s.skipNotify); },
fn: function (next) { s.notifyStep(ctx.nominationInfo, next); }
}
];
(function run(i) {
if (i >= stages.length) { if (typeof s.onSuccess === 'function') s.onSuccess(ctx); return; }
var stage = stages[i];
if (typeof stage.shouldRun === 'function' && !stage.shouldRun()) { run(i + 1); return; }
var statusId = stage.pendingText
? logStatus(stage.pendingText, null, { pending: true, trackError: false })
: null;
try {
stage.fn(function (err) {
if (err) {
if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), err, { statusId: statusId });
if (typeof s.onFailure === 'function') s.onFailure(stage.name, err, ctx);
else markSubmitError();
return;
}
if (statusId && stage.successText) logStatus(stage.successText, null, { statusId: statusId, trackError: false });
run(i + 1);
});
} catch (ex) {
var exErr = { code: 'exception', info: (ex && ex.message) ? ex.message : String(ex) };
if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), exErr, { statusId: statusId });
if (typeof s.onFailure === 'function') s.onFailure(stage.name, exErr, ctx);
else markSubmitError();
}
}(0));
}
// ─── Публикация номинации ────────────────────────────────────────────────
function publishNomination(opts, callback) {
var cb = callback || function () {};
function doPublish() {
apiReq({
title: opts.pageTitle,
section: 'new',
sectiontitle: opts.sectionTitle,
summary: opts.summary,
text: opts.text,
assertuser: mwCfg.wgUserName
}, 'edit', function (resp) {
cb(resp && resp.error ? resp.error : null);
});
}
if (opts.sectionTitle) {
if (!opts.navTemplate) { doPublish(); return; }
apiReq({ title: opts.pageTitle, createonly: '1', text: T_OPEN + opts.navTemplate + '-Навигация' + T_CLOSE + '\n', summary: makeSummary('автоматическая шапка'), assertuser: mwCfg.wgUserName },
'edit', function (resp) {
if (resp && resp.error && resp.error.code !== 'articleexists') { cb(resp.error); return; }
doPublish();
});
return;
}
// Вставка в существующую страницу
editPageContent(opts.pageTitle, { summary: opts.summary, readError: opts.readErrorMessage || 'Не удалось получить содержимое.' },
function (pageText) { return opts.buildText ? opts.buildText(pageText) : null; },
function (err) { cb(err || null); }
);
}
// ─── Оповещение авторов ──────────────────────────────────────────────────
function notifyAuthor(pg, options, callback) {
var opts = options || {};
var cb = callback || function () {};
var actionText = (typeof opts.actionText === 'string') ? opts.actionText : '';
var discussionPage = (typeof opts.discussionPage === 'string') ? opts.discussionPage : '';
var discussionSection = normalizeSectionForLink((typeof opts.discussionSection === 'string') ? opts.discussionSection : '');
var includeProposed = (typeof opts.includeProposedPrefix === 'boolean') ? opts.includeProposedPrefix : true;
var actionPhrase = ((includeProposed ? 'предложена ' : '') + actionText).trim() || 'изменена';
var discussionText = discussionPage ? 'Обсуждение — на странице [[' + discussionPage + (discussionSection ? '#' + discussionSection : '') + ']]. ' : '';
apiReq({ prop: 'revisions', rvprop: 'user', rvdir: 'newer', titles: pg }, 'query', function (queryResp) {
var page = getFirstQueryPage(queryResp);
if (!page) { cb({ code: 'network', info: 'Network error' }); return; }
if (page.missing !== undefined) { cb({ code: 'missing', info: 'Page missing.' }); return; }
if (!page.revisions || !page.revisions.length) { cb({ code: 'no_revisions', info: 'No revisions.' }); return; }
var rv = page.revisions[0];
if ('anon' in rv || rv.userhidden || !rv.user || rv.user === mwCfg.wgUserName) { cb(null); return; }
apiReq({
title: 'User talk:' + rv.user, section: 'new',
sectiontitle: 'Remover: [[:' + pg + ']]',
summary: opts.summary || makeSummary('уведомление автора'),
text: 'Страница [[:' + pg + ']], созданная вами, ' + actionPhrase + '. ' +
discussionText +
'~~' + '~~<br><small>Это автоматическое уведомление, сгенерированное ' + scriptLink + '.</small>',
assertuser: mwCfg.wgUserName
}, 'edit', function (editResp) { cb(editResp && editResp.error ? editResp.error : null); });
});
}
function notifyAuthorsForPages(pages, notifyOptions, callback) {
var cb = callback || function () {};
var opts = notifyOptions || {};
var list = [];
(pages || []).forEach(function (p) { var t = normTitle(p); if (t && list.indexOf(t) === -1) list.push(t); });
if (!list.length) { cb(); return; }
eachSequential(list, function (pg, next) {
var pageLink = buildQuotedStatusPageLink(pg);
var statusId = logStatus('Отправляется уведомление создателю страницы ' + pageLink + '...', null, { pending: true, trackError: false });
notifyAuthor(pg, opts, function (err) {
logStatus(err ? 'Уведомление создателя страницы ' + pageLink + '.' : 'Создатель страницы ' + pageLink + ' уведомлён.', err,
{ statusId: statusId, trackError: opts.trackError !== false });
next();
});
}, cb);
}
// ─── Подписка на раздел ──────────────────────────────────────────────────
function subscribeToTopic(pageTitle, sectionTitle, callback) {
var cb = callback || function () {};
if (!setSubscribe || !sectionTitle) { cb(); return; }
var statusId = logStatus('Оформляется подписка на раздел...', null, { pending: true, trackError: false });
var targetFrag = normalizeSectionForLink(sectionTitle).toLowerCase();
function finish(err, st) {
if (err) { logStatus('Не удалось подписаться на раздел.', err, { statusId: statusId, trackError: false }); cb(); return; }
logStatus(st === 'subscribed' ? 'Оформлена подписка на раздел.' : 'Раздел для подписки не найден.', null, { statusId: statusId, trackError: false });
cb();
}
function trySubscribe(attemptsLeft) {
apiReq({ page: pageTitle, prop: 'threaditemshtml', excludesignatures: true }, 'discussiontoolspageinfo', function (data) {
var items = (data && data.discussiontoolspageinfo && data.discussiontoolspageinfo.threaditemshtml) || null;
if (!items || !items.length) {
if (attemptsLeft > 0) { setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000); return; }
finish(null, 'not_found'); return;
}
var commentname = null;
for (var ti = items.length - 1; ti >= 0; ti--) {
var t = items[ti];
if (t.type === 'heading') {
var htext = (t.headingText || t.html || '').replace(/<[^>]+>/g, '').trim();
if (htext.toLowerCase() === targetFrag || normalizeSectionForLink(htext).replace(/_/g, ' ').toLowerCase() === targetFrag) {
commentname = t.name; break;
}
}
}
if (!commentname) {
if (attemptsLeft > 0) setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000);
else finish(null, 'not_found');
return;
}
apiReq({ page: pageTitle, commentname: commentname, subscribe: '1' }, 'discussiontoolssubscribe', function (res) {
finish(res && res.error ? res.error : null, 'subscribed');
});
});
}
setTimeout(function () { trySubscribe(2); }, 1500);
}
// ═══════════════════════════════════════════════════════════════════════════
// UI: модальные окна
// ═══════════════════════════════════════════════════════════════════════════
function syncModalLayout() {
var syncFn = $('#removerModal').data('rmSyncLayout');
if (typeof syncFn === 'function') syncFn();
}
function clearModalLayoutSyncHandlers() {
modalLayoutSyncHandlers = [];
$('#removerModal').removeData('rmSyncLayout');
}
function registerModalLayoutSync(handler) {
if (typeof handler !== 'function') return;
if (modalLayoutSyncHandlers.indexOf(handler) === -1) modalLayoutSyncHandlers.push(handler);
$('#removerModal').data('rmSyncLayout', function () {
modalLayoutSyncHandlers.slice().forEach(function (fn) {
if (typeof fn === 'function') fn();
});
});
}
function registerResizeObserver(observer) {
if (observer) resizeObservers.push(observer);
}
function resetModalObservers() {
resizeObservers.forEach(function (observer) {
if (observer && typeof observer.disconnect === 'function') observer.disconnect();
});
resizeObservers = [];
clearModalLayoutSyncHandlers();
$(window).off('resize.removerModal');
$(window).off('.rmTaResize');
}
function closeModal() {
resetModalObservers();
$(window).off('keydown.remover');
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
}
function ensureModalStyles() {
if (document.getElementById('removerModalDynamicStyles')) return;
var progH = 'background:' + tk.bgProgH + '!important;border-color:' + tk.bProgH + '!important;color:' + tk.cInv + '!important;';
var neutH = 'background:' + tk.bgN + '!important;border-color:' + tk.bSub + '!important;color:inherit!important;';
var succH = 'background:' + tk.bgSuccH+ '!important;border-color:' + tk.bSuccH + '!important;color:' + tk.cInv + '!important;';
var pillBg = 'linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%)';
var pillShadow = '0 1px 0 rgba(255,255,255,.08) inset,0 1px 2px rgba(0,0,0,.18)';
var activePillShadow = '0 1px 0 rgba(255,255,255,.14) inset,0 2px 6px rgba(51,102,204,.24)';
var css = [
'#removerModal,#removerModal *{-moz-text-size-adjust:none!important;-webkit-text-size-adjust:100%!important;text-size-adjust:100%!important}',
'#removerModal{color:inherit}',
'#removerModal input::placeholder,#removerModal textarea::placeholder{color:var(--color-subtle,currentColor);opacity:.7}',
'#removerModal input[type="checkbox"],#removerModal input[type="radio"]{appearance:auto;-webkit-appearance:auto;-moz-appearance:auto;accent-color:auto}',
'#removerModal input[type="checkbox"]{outline:none!important;box-shadow:none!important}',
'#removerModal button{transition:background-color .12s ease,border-color .12s ease,color .12s ease,box-shadow .12s ease,filter .12s ease,transform .06s ease}',
'#removerModal .rmToggleBtn{background:' + tk.bgNSub + '!important;border-color:' + tk.bSub + '!important;color:inherit!important}',
'#removerModal .rmToggleBtn.is-active{background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important}',
'#removerModal button:not(:disabled):hover{filter:brightness(.97)}',
'#removerModal button:not(:disabled):active{transform:translateY(1px)}',
'#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):hover,#removerModal .rmToggleBtn.is-active:hover{' + progH + 'filter:none}',
'#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):active,#removerModal .rmToggleBtn.is-active:active{' + progH + 'filter:brightness(.92)!important}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError){background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important;outline:none!important;box-shadow:0 0 0 6px rgba(51,102,204,.13),0 1px 2px rgba(0,0,0,.08)!important}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):hover{' + progH + 'box-shadow:0 0 0 7px rgba(51,102,204,.16),0 1px 2px rgba(0,0,0,.1)!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):active{' + progH + 'box-shadow:0 0 0 5px rgba(51,102,204,.14),0 1px 2px rgba(0,0,0,.08)!important;filter:brightness(.92)!important}',
'#removerModal .rmArticleCommentToggle.is-active{background:#bfc4ca!important;border-color:#8f98a3!important;color:#202122!important}',
'#removerModal .rmArticleCommentToggle.is-active:hover{background:#b4bac1!important;border-color:#848e99!important;color:#202122!important;filter:none}',
'#removerModal .rmArticleCommentToggle.is-active:active{background:#a9b0b8!important;border-color:#79838f!important;color:#202122!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitError:not(:disabled):hover{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitError:not(:disabled):active{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:brightness(.88)!important}',
'#removerModal #removerReload:not(:disabled):hover{' + succH + 'filter:none}',
'#removerModal #removerReload:not(:disabled):active{' + succH + 'filter:brightness(.92)!important}',
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):hover,#removerModal .rmToggleBtn:not(.is-active):hover{' + neutH + 'filter:none}',
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):active{' + neutH + 'filter:brightness(.92)!important}',
'#removerModal a.removerModalLink{color:' + tk.cProg + ';text-decoration:none;border-bottom:0;box-shadow:none;word-break:break-word;overflow-wrap:anywhere}',
'#removerModal a.removerModalLink:hover,#removerModal a.removerModalLink:focus{color:' + tk.cProgH + ';text-decoration:underline}',
'#removerModal #removerModalSubtitle{font-size:12px!important;line-height:1.35!important;font-weight:400!important;color:' + tk.cSubM + '!important;max-width:100%;overflow-wrap:anywhere;word-break:break-word}',
'#removerModal #removerModalSubtitle a{font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important}',
'#removerModal a.rmButtonLikeLink{color:' + tk.cBase + '!important;text-decoration:none!important;transform:none!important;transition:background-color .12s ease,border-color .12s ease,color .12s ease,filter .12s ease,transform .06s ease!important}',
'#removerModal a.rmButtonLikeLink:hover{' + neutH + 'text-decoration:none!important;transform:none!important}',
'#removerModal a.rmButtonLikeLink:focus{text-decoration:none!important}',
'#removerModal a.rmButtonLikeLink:focus:not(:focus-visible){outline:none!important}',
'#removerModal a.rmButtonLikeLink:focus-visible{outline:2px solid ' + tk.bProg + '!important;outline-offset:2px;text-decoration:none!important}',
'#removerModal a.rmButtonLikeLink:hover:active{' + neutH + 'filter:brightness(.92)!important;transform:translateY(1px)!important;text-decoration:none!important}',
'#removerModal .rmInfoBox{margin:0 0 10px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgNSub + '}',
'#removerModal .rmActionList{display:flex;flex-direction:column;gap:6px}',
'#removerModal .rmActionItem{display:block;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgBase + ';cursor:pointer;transition:background-color .12s ease,transform .06s ease}',
'#removerModal .rmActionItem:hover{background:' + tk.bgNSub + '}',
'#removerModal .rmActionItem:active{transform:translateY(1px)}',
'#removerModal .rmActionMain{display:flex;align-items:center}',
'#removerModal .rmActionMain input[type="radio"]{margin-right:8px}',
'#removerModal .rmActionMeta{display:block;margin-left:24px;margin-top:2px;color:' + tk.cSubM + ';font-size:12px;line-height:1.35}',
'#removerModal .rmConflictLead{margin:0 0 10px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}',
'#removerModal .rmConflictList{display:flex;flex-direction:column;gap:10px}',
'#removerModal .rmConflictCard{padding:12px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}',
'#removerModal .rmConflictCard.is-skip{background:' + tk.bgNSub + ';border-color:' + tk.bSubS + '}',
'#removerModal .rmConflictTitle{font-size:14px;font-weight:700;line-height:1.4;color:' + tk.cBase + ';word-break:break-word;overflow-wrap:anywhere}',
'#removerModal .rmConflictMeta{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmConflictGroup{margin-top:10px}',
'#removerModal .rmConflictGroupTitle{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmConflictButtons{display:flex;flex-wrap:wrap;gap:6px}',
'#removerModal .rmConflictChoice{padding:5px 10px}',
'#removerModal .rmConflictChoice.is-disabled,#removerModal .rmConflictButtons.is-disabled .rmConflictChoice{opacity:.55;cursor:not-allowed;pointer-events:none}',
'#removerModal .rmConflictHint{margin-top:8px;font-size:11px;line-height:1.45;color:' + tk.cSub + '}',
'#removerModal #rmSettingsForm{display:flex;flex-direction:column;gap:14px}',
'#removerModal .rmSettingsLead{margin:-2px 0 2px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}',
'#removerModal .rmSettingsSection{margin:0;padding:14px 16px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.7) inset}',
'#removerModal .rmSettingsSectionHeader{margin:0 0 12px}',
'#removerModal .rmSettingsSectionTitle{font-size:14px;font-weight:700;line-height:1.35;color:' + tk.cBase + '}',
'#removerModal .rmSettingsSectionDescription{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmSettingsField{display:block;margin:0 0 10px;padding:12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}',
'#removerModal .rmSettingsField:last-child{margin-bottom:0}',
'#removerModal .rmSettingsFieldLabel{display:block;font-size:13px;font-weight:700;line-height:1.35;margin:0 0 8px;color:' + tk.cBase + '}',
'#removerModal .rmSettingsFieldControl{display:block;min-width:0}',
'#removerModal .rmSettingsFieldControl input{margin-bottom:0!important}',
'#removerModal .rmSettingsFieldControl input[type="text"]{min-height:38px;border-radius:6px}',
'#removerModal .rmSettingsFieldHint{margin-top:8px;font-size:12px;line-height:1.55;color:' + tk.cSubM + ';overflow-wrap:anywhere}',
'#removerModal .rmSettingsChecks{display:flex;flex-direction:column;gap:8px}',
'#removerModal .rmSettingsCheck{display:inline-flex;align-items:flex-start;gap:8px;font-size:14px;line-height:1.45;color:' + tk.cBase + '}',
'#removerModal .rmSettingsCheck input[type="checkbox"]{margin:3px 0 0;flex-shrink:0}',
'#removerModal .rmSettingsMenuPresetWrap{margin-top:10px}',
'#removerModal .rmSettingsMenuPresetLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmSegmentedBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmSettingsMenuPresetBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmSegmentedBtn,#removerModal .rmSettingsMenuPresetBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}',
'#removerModal .rmSegmentedBtn.is-active,#removerModal .rmSettingsMenuPresetBtn.is-active{background:' + tk.bgProg + ';border-color:' + tk.bProg + ';color:' + tk.cInv + ';box-shadow:' + activePillShadow + '}',
'#removerModal.rmModalSettings{border:1px solid ' + tk.bSub + '!important;background:' + tk.bgBase + '!important;border-radius:12px!important;box-shadow:0 14px 32px rgba(0,0,0,.08),0 1px 0 rgba(255,255,255,.78) inset!important}',
'#removerModal.rmModalSettings #removerModalHeaderBar{margin-bottom:14px;padding-bottom:10px;border-bottom:2px solid ' + tk.bSub + '}',
'#removerModal.rmModalSettings #removerModalSubtitle{margin:-2px 0 12px!important;color:' + tk.cSubM + '!important}',
'#removerModal.rmModalSettings #rmSettingsForm{gap:18px}',
'#removerModal.rmModalSettings .rmSettingsLead{margin:0;padding:0 2px;color:' + tk.cSubM + '}',
'#removerModal.rmModalSettings .rmSettingsSection{padding:16px 18px;border:1px solid ' + tk.bSub + ';border-radius:12px;background:linear-gradient(180deg,' + tk.bgNSub + ' 0%,' + tk.bgBase + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.82) inset,0 8px 18px rgba(0,0,0,.035)}',
'#removerModal.rmModalSettings .rmSettingsSectionHeader{margin:0 0 6px;padding-bottom:0;border-bottom:0}',
'#removerModal.rmModalSettings .rmSettingsSectionTitle{font-size:16px;line-height:1.3}',
'#removerModal.rmModalSettings .rmSettingsSectionDescription{margin-top:5px;max-width:none}',
'#removerModal.rmModalSettings .rmSettingsField{margin:14px 0 0;padding:12px 0 0;border:0;border-top:1px solid ' + tk.bSubS + ';border-radius:0;background:transparent;box-shadow:none}',
'#removerModal.rmModalSettings .rmSettingsField:first-child{margin-top:0;padding-top:0;border-top:0}',
'#removerModal.rmModalSettings .rmSettingsFieldLabel{margin:0 0 6px}',
'#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]{min-height:40px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}',
'#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]:focus{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.16);outline:none}',
'#removerModal.rmModalSettings .rmSettingsFieldHint{margin-top:6px;max-width:none}',
'#removerModal.rmModalSettings .rmSettingsChecks{gap:10px}',
'#removerModal.rmModalSettings .rmSettingsCheck{padding:4px 0}',
'#removerModal.rmModalSettings .rmSettingsMenuPresetWrap{margin-top:12px;padding-top:10px;border-top:1px dashed ' + tk.bSubS + '}',
'#removerModal.rmModalSettings .rmSettingsHintList{width:100%;max-width:100%;box-sizing:border-box;margin-top:10px;padding:10px 12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + '}',
'#removerModal.rmModalSettings .rmSettingsHintRow{line-height:1.55}',
'#removerModal.rmModalSettings .rmSettingsHintBadge{background:' + tk.bgNSub + '}',
'#removerModal.rmModalSettings .rmQuickPhraseEditor{padding-top:2px}',
'#removerModal.rmModalSettings .rmQuickPhraseMeta{min-height:18px}',
'#removerModal.rmModalSettings #rmSettingsSignaturePreviewCode{display:inline-block;padding:2px 8px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}',
'#removerModal.rmModalSettings #rmFooterActionButtons{position:relative;flex:0 0 auto!important;display:flex!important;align-items:center!important;justify-content:flex-end!important;gap:6px!important;margin-left:auto!important;max-width:100%!important}',
'#removerModal.rmModalSettings #rmSettingsActionButtonsRow{display:flex;align-items:center;justify-content:flex-end;gap:6px;flex-wrap:nowrap}',
'#removerModal.rmModalSettings #rmSettingsUnsavedHint{display:none;position:absolute;top:100%;right:0;width:auto;box-sizing:border-box;margin:4px 0 0;color:' + tk.cSubM + ';opacity:.78;font-size:12px;line-height:1.35;text-align:right;white-space:nowrap}',
'#removerModal .rmProtectControlGroup{margin-top:12px;padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-sizing:border-box}',
'#removerModal .rmProtectControlLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmProtectControlGroup .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}',
'#removerModal .rmProtectControlGroup .rmSegmentedBtn{padding:4px 10px;font-size:12px}',
'#removerModal .rmTransferPanel{margin-top:10px;padding:0;border:0;background:transparent;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmTransferGrid{display:grid;grid-template-columns:max-content max-content;column-gap:10px;row-gap:6px;align-items:start;justify-content:start}',
'#removerModal .rmTransferGrid .rmSegmentedBar{justify-self:start}',
'#removerModal .rmTransferHintRow{grid-column:1 / -1;min-height:0}',
'#removerModal .rmTransferPanel .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}',
'#removerModal .rmTransferPanel .rmSegmentedBtn{padding:6px 14px;font-size:12px;font-weight:700}',
'#removerModal #rmTransferModeGroup{gap:0}',
'#removerModal #rmTransferModeGroup .rmSegmentedBtn:first-child{border-top-right-radius:0;border-bottom-right-radius:0}',
'#removerModal #rmTransferModeGroup .rmSegmentedBtn + .rmSegmentedBtn{margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}',
'#removerModal.rmCompactContent .rmArticleRow{flex-wrap:wrap!important;gap:6px}',
'#removerModal.rmCompactContent .rmArticleRow .rmArticleInput{flex:1 1 100%!important;width:100%!important}',
'#removerModal.rmCompactContent .rmArticleRow .rmArticleCommentToggle,#removerModal.rmCompactContent .rmArticleRow .rmAddArticle,#removerModal.rmCompactContent .rmArticleRow .rmRemoveInput{margin-left:0!important}',
'#removerModal.rmCompactContent .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(1){order:1}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(2){order:2}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(3){order:3}',
'#removerModal.rmCompactContent #rmTransferModeGroup{flex-direction:column;align-items:flex-start;gap:6px}',
'#removerModal.rmCompactContent #rmTransferModeGroup .rmSegmentedBtn{margin-left:0!important;border-radius:999px!important}',
'#removerModal.rmCompactContent .rmTransferHintRow{grid-column:auto}',
'#removerModal #rmProtectTextBlock{margin-top:14px}',
'#removerModal #rmSettingsMenuTitle:disabled{background:' + tk.bgDis + '!important;border-color:' + tk.bDis + '!important;color:' + tk.cDis + '!important;-webkit-text-fill-color:' + tk.cDis + ';opacity:1;cursor:not-allowed;box-shadow:none!important}',
'#removerModal .rmSettingsHintList{display:flex;flex-direction:column;gap:4px;margin-top:8px}',
'#removerModal .rmSettingsHintRow{font-size:12px;line-height:1.5;color:' + tk.cSubM + '}',
'#removerModal .rmSettingsHintBadge{display:inline-block;margin-right:6px;padding:1px 6px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';font-size:11px;font-weight:700;color:' + tk.cSubM + ';vertical-align:baseline}',
'#removerModal .rmQuickPhraseEditor{display:flex;flex-direction:column;gap:10px}',
'#removerModal .rmQuickPhraseList,#removerModal .rmQuickPhrasesPanel{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-start}',
'#removerModal .rmQuickPhraseChip{position:relative;display:inline-flex;align-items:center;gap:4px;max-width:100%;padding:2px 4px 2px 10px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';box-shadow:' + pillShadow + ';transition:border-color .12s,box-shadow .12s,opacity .12s;overflow:visible}',
'#removerModal .rmQuickPhraseChip.is-editing{opacity:.42;border-style:dashed}',
'#removerModal .rmQuickPhraseChip.is-dragging{opacity:.65}',
'#removerModal .rmQuickPhraseChip.is-drop-before::before,#removerModal .rmQuickPhraseChip.is-drop-after::after{content:"";position:absolute;top:50%;width:3px;height:24px;border-radius:999px;background:' + tk.cProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.08)}',
'#removerModal .rmQuickPhraseChip.is-drop-before::before{left:-4px;transform:translate(-50%,-50%)}',
'#removerModal .rmQuickPhraseChip.is-drop-after::after{right:-4px;transform:translate(50%,-50%)}',
'#removerModal .rmQuickPhraseEditBtn{max-width:100%;padding:3px 0;border:0;background:transparent;color:' + tk.cBase + ';font-size:13px;line-height:1.35;cursor:pointer;text-align:left;white-space:normal}',
'#removerModal .rmQuickPhraseRemoveBtn{width:24px;height:24px;padding:0;border:0;border-radius:999px;background:transparent;color:' + tk.cSubM + ';font-size:18px;line-height:1;cursor:pointer;flex-shrink:0}',
'#removerModal .rmQuickPhraseRemoveBtn:hover{background:' + tk.bgN + ';color:' + tk.cBase + '}',
'#removerModal #rmSettingsQuickPhraseInput.is-editing{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.12)}',
'#removerModal .rmQuickPhraseMeta{font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmQuickPhraseEmpty{padding:2px 0;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmQuickPhrasesPanel{margin-top:8px}',
'#removerModal .rmQuickPhraseActionBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}',
'#removerModal .rmQuickPhraseActionBtn:hover{border-color:' + tk.bProg + ';color:' + tk.cProg + '}',
'@media (max-width:' + sz.mobileBp + 'px){',
'#removerModal button{white-space:normal!important}',
'#removerModal #rmFooterButtons{align-items:flex-start!important}',
'#removerModal #rmFooterCheckboxes,#removerModal #rmFooterActionButtons{width:100%!important;max-width:100%!important;margin-left:0!important}',
'#removerModal .rmSettingsSection{padding:12px 13px}',
'#removerModal .rmSettingsField{padding:10px}',
'#removerModal.rmModalSettings #rmSettingsUnsavedHint{max-width:100%;margin:4px 0 0;text-align:right;white-space:normal}',
'#removerModal .rmTransferPanel{padding:0}',
'#removerModal .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}',
'#removerModal .rmTransferHintRow{grid-column:auto}',
'#removerModal .rmQuickPhraseChip{max-width:100%}',
'}'
].join('');
var style = document.createElement('style');
style.id = 'removerModalDynamicStyles';
style.textContent = css;
document.head.appendChild(style);
}
function applyV2022Layout($modal, explicitWidth) {
if (!isVector22) return;
var css = { 'max-width': '100%', 'box-sizing': 'border-box', 'overflow-wrap': 'anywhere' };
if (typeof explicitWidth === 'number') css['max-width'] = explicitWidth + 'px';
$modal.css(css);
}
function getPageUrl(pageTitle) {
return (mw.util && typeof mw.util.getUrl === 'function')
? mw.util.getUrl(pageTitle)
: '/wiki/' + encodeURIComponent((pageTitle || '').replace(/ /g, '_'));
}
function getPageUrlWithFragment(pageTitle, fragment) {
var url = getPageUrl(pageTitle);
var frag = normalizeSectionForLink(fragment);
if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_'));
return url;
}
function buildStatusPageLink(pageName) {
var title = normTitle(pageName);
return '<a href="' + escapeHtml(getPageUrl(title)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(title) + '</a>';
}
function buildQuotedStatusPageLink(pageName) {
return '«' + buildStatusPageLink(pageName) + '»';
}
function buildHeaderIconButtonHtml(id, title, label, text) {
return joinHtml([
'<button id="', id, '" type="button" title="', escapeHtml(title), '" aria-label="', escapeHtml(label || title), '" ',
'style="', stHeaderIconBtn, '">', text || '', '</button>'
]);
}
function createModal(opts) {
if (typeof opts === 'string') opts = { title: opts };
var layout = getModalLayout();
resetModalObservers();
ensureModalStyles();
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
var subtitleHtml = '';
var subtitleStyle = 'margin:-4px 0 8px;font-size:12px!important;color:' + tk.cSubM + ';line-height:1.35!important;font-weight:400!important;';
var subtitleLinkStyle = 'font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important;';
if (opts.subtitleHtml) {
subtitleHtml = joinHtml([
'<div id="removerModalSubtitle" style="', subtitleStyle, '">',
opts.subtitleHtml,
'</div>'
]);
} else if (opts.subtitlePage) {
subtitleHtml = joinHtml([
'<div id="removerModalSubtitle" style="', subtitleStyle, '">',
opts.subtitleLabel || 'Текущий день',
': <a href="', getPageUrl(opts.subtitlePage), '" target="_blank" rel="noopener noreferrer" class="removerModalLink" style="', subtitleLinkStyle, '">',
normTitle(opts.subtitlePage),
'</a></div>'
]);
}
var settingsButtonHtml = opts.showSettingsButton === false ? '' :
buildHeaderIconButtonHtml('removerSettingsTrigger', 'Конфигурация', 'Конфигурация', '⚙');
var display = opts.inline ? 'inline-block' : 'block';
var modalMargin = opts.inline ? '1em 0' : (layout.shouldCenter ? '1em auto' : '1em 0');
var inlineLayoutStyle = opts.inline ? ';justify-self:start;align-self:start;width:fit-content;' : '';
var modalStyle = joinHtml([
'position:relative;padding:1.5em;margin:', modalMargin, ';display:', display, ';',
'border:', stStyles.border, ';background:', stStyles.background,
';border-radius:', stStyles.borderRadius, ';box-shadow:', stStyles.boxShadow,
';max-width:100%;box-sizing:border-box;overflow-wrap:anywhere;', inlineLayoutStyle
]);
var headerStyle = 'display:flex;align-items:center;gap:10px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid ' + tk.bSubS + ';';
var titleStyle = 'color:' + stStyles.headerColor + ';margin:0;padding:0;border:0;display:block;font-size:1.3em;font-weight:400;line-height:1.25;flex:1 1 auto;min-width:0;';
$('#content').prepend(joinHtml([
'<div id="removerModal" style="', modalStyle, '">',
'<div id="removerModalHeaderBar" style="', headerStyle, '">',
'<h1 id="removerModalTitle" style="', titleStyle, '"><span id="removerModalTitleText">', opts.title, '</span></h1>',
settingsButtonHtml,
'</div>',
subtitleHtml,
'<div id="removerModalContent"></div>',
'<div id="removerModalFooter" style="margin-top:15px;"></div>',
'</div>'
]));
var $modal = $('#removerModal');
if (opts.width === 'compact') $modal.css({ width: layout.defaultOuterWidth + 'px', 'max-width': '100%', 'box-sizing': 'border-box' });
else applyV2022Layout($modal);
$('#removerSettingsTrigger').off('click').on('click', function () {
openSettings();
});
}
function buildFooterCheckboxHtml(name, checked, label) {
return joinHtml([
'<label style="', stFooterCheckLabel, '">',
'<input name="', name, '" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ',
checked ? 'checked' : '',
'>',
label,
'</label>'
]);
}
function buildFooterActionsHtml(buttonsHtml) {
return '<div id="rmFooterActionButtons" style="' + stFooterActions + '">' + buttonsHtml + '</div>';
}
function renderModalFooter(mode, options) {
var opts = options || {};
$('#removerModalFooter').css('width', '');
if (mode === 'submit') {
var showCb = opts.showCheckbox !== false;
var showSub = opts.showSubscribe === true;
var ns = mwCfg.wgNamespaceNumber;
var notifyLabel = ns === 0 ? 'Оповестить создателя статьи'
: (ns === 10 || ns === 11) ? 'Оповестить создателя шаблона'
: (ns === 14 || ns === 15) ? 'Оповестить создателя категории'
: 'Оповестить создателя страницы';
var cbInlineHtml = '';
if (showSub || showCb) {
cbInlineHtml = joinHtml([
'<div id="rmFooterCheckboxes" style="', stFooterChecks, '">',
showSub ? buildFooterCheckboxHtml('rmSubscribe', setSubscribe, 'Подписаться на номинацию') : '',
showCb ? buildFooterCheckboxHtml('rmUAlert', setAlert, notifyLabel) : '',
'</div>'
]);
}
$('#removerModalFooter').html(joinHtml([
'<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:', cbInlineHtml ? 'space-between' : 'flex-end', ';">',
cbInlineHtml,
buildFooterActionsHtml(joinHtml([
'<button id="removerCancel" style="', stCancel, '">Отмена</button>',
'<button id="removerSubmit" style="', stSubmit, '">', opts.submitText || 'ОК', '</button>'
])),
'</div>'
]));
$('#removerCancel').click(function () { closeModal(); });
$('#removerSubmit').data('rmSubmitInProgress', false).click(function () {
if ($(this).data('rmSubmitInProgress')) return;
$(this).removeClass('rmSubmitError').css({ background: '', 'border-color': '', color: '' });
isError = false;
if (!opts.preserveLogOnSubmit) {
$('#rmLogBox').empty();
logStatusSeq = 0;
}
if (showCb) { setAlert = $('[name="rmUAlert"]').is(':checked'); state.setAlert = setAlert; }
if (showSub) { setSubscribe = $('[name="rmSubscribe"]').is(':checked'); state.setSubscribe = setSubscribe; }
$(this).data('rmSubmitInProgress', true).prop('disabled', true);
var submitResult;
try { submitResult = opts.onSubmit(); } catch (ex) { unlockModalSubmit(); throw ex; }
if (submitResult === false) unlockModalSubmit();
});
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.ctrlKey && e.keyCode === 13) $('#removerSubmit').click();
});
} else if (mode === 'reload') {
var newBtns = buildFooterActionsHtml(joinHtml([
'<button id="removerCancel" style="', stCancel, '">Закрыть</button>',
'<button id="removerReload" style="', stReload, '">', opts.reloadText || 'Обновить страницу', '</button>'
]));
$('#rmFooterCheckboxes').remove();
var $btns = $('#rmFooterButtons');
if ($btns.length) {
$btns.css({ 'justify-content': 'flex-end' }).html(newBtns);
} else {
$('#removerModalFooter').append(joinHtml([
'<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:flex-end;">',
newBtns,
'</div>'
]));
}
$('#removerCancel').click(function () { closeModal(); });
$('#removerReload').click(function () { location.reload(); });
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.keyCode === 27) $('#removerCancel').click();
if (e.ctrlKey && e.keyCode === 13) $('#removerReload').click();
});
} else { // 'close'
$('#removerModalFooter').html(joinHtml([
'<div style="display:flex;justify-content:flex-end;align-items:center;">',
'<button id="removerCancel" style="', stCancel, 'margin-right:0;">', opts.closeText || 'Закрыть', '</button>',
'</div>'
]));
$('#removerCancel').click(function () { closeModal(); });
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.keyCode === 27) $('#removerCancel').click();
});
}
}
function unlockModalSubmit() {
$('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false);
}
function markSubmitError() {
isError = true;
var errColor = '#d73333';
$('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false)
.addClass('rmSubmitError').css({ background: errColor, 'border-color': errColor, color: '#fff' });
}
// ─── UI: статус и ссылки ─────────────────────────────────────────────────
function startProcessing() {
if ($('#rmLogBox').length) return;
$('#removerModal').append(
'<div id="rmLogBox" style="margin-top:12px;padding-top:10px;border-top:1px solid ' + tk.bSubS + ';line-height:1.5;overflow-wrap:anywhere;word-break:break-word;box-sizing:border-box;"></div>'
);
syncLinkWidths();
}
function logStatus(message, error, opts) {
var o = opts || {};
if (o.trackError !== false && error && error.code) isError = true;
var $box = $('#rmLogBox');
if (!$box.length) { startProcessing(); $box = $('#rmLogBox'); }
var statusId = o.statusId || ('rm-status-' + (++logStatusSeq));
var $row = $box.find('[data-rm-status-id="' + statusId + '"]');
if (!$row.length) {
$row = $('<div data-rm-status-id="' + statusId + '" style="margin-top:4px;line-height:1.4;"></div>');
$box.append($row);
}
var html;
if (error) {
var errText = error.code
? '<span class="error"><small>' + escapeHtml(formatLogErrorCode(error.code)) + ': ' + escapeHtml(String(error.info || '')) + '</small></span>'
: escapeHtml(String(error));
html = '<span style="color:' + tk.cDang + ';margin-right:4px;">✕</span>' + message + ' — ' + errText;
} else if (o.pending) {
html = '<span style="color:' + tk.cSubM + ';">' + message + '</span>';
} else {
html = '<span style="color:' + tk.bgSucc + ';margin-right:4px;">✓</span><span style="color:' + tk.cSubM + ';">' + message + '</span>';
}
$row.html(html);
return statusId;
}
function formatLogErrorCode(code) {
var value = String(code || '');
return value.toLowerCase() === 'error' ? 'Ошибка' : value;
}
function logPageEdit(pageName, error, opts) {
logStatus('Правка страницы ' + buildQuotedStatusPageLink(pageName) + '.', error, opts);
}
function syncLinkWidths() {
var $box = $('#rmLogBox');
if (!$box.length) return;
var taW = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0;
$box.css({ width: taW ? taW + 'px' : '', 'max-width': '100%' });
}
function appendNominationLink(pageTitle, sectionTitle) {
if (!pageTitle) return;
var url = getPageUrl(pageTitle);
var frag = normalizeSectionForLink(sectionTitle);
if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_'));
var label = normTitle(frag ? pageTitle + '#' + frag : pageTitle);
var $target = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent');
$target.append(
'<div style="margin-top:4px;line-height:1.4;word-break:break-word;overflow-wrap:anywhere;display:flex;align-items:baseline;gap:5px;">' +
'<span style="color:' + tk.bgSucc + ';font-size:14px;flex-shrink:0;">✓</span>' +
'<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a></div>'
);
}
// ─── UI-строители ────────────────────────────────────────────────────────
function buildInfoBoxHtml(mainText, detailsText, isErr) {
var cls = isErr ? ' class="error"' : '';
return joinHtml([
'<div class="rmInfoBox">',
'<p', cls, ' style="margin:0', detailsText ? ' 0 6px' : '', ';">', mainText, '</p>',
detailsText ? '<p style="margin:0;color:' + tk.cSubM + ';">' + detailsText + '</p>' : '',
'</div>'
]);
}
function buildActionsHtml(actions, inputName, listId) {
var actionItemsHtml = actions.map(function (a, i) {
var meta = a.description || (a.talkNotice ? 'С добавлением {{' + (a.talkTemplate || a.resultTemplate || 'шаблон') + '}} на СО.' : '');
var tagHtml = a.tag
? '<span style="display:inline-block;font-size:13px;font-weight:600;padding:2px 7px;border-radius:3px;background:' + tk.bgN + ';color:' + tk.cSubM + ';margin-right:8px;white-space:nowrap;vertical-align:middle;">' + escapeHtml(a.tag) + '</span>'
: '';
return joinHtml([
'<label class="rmActionItem">',
'<span class="rmActionMain">',
'<input type="radio" name="', inputName, '" value="', a.id, '" ', i === 0 ? 'checked' : '', '>',
tagHtml,
'<span>', a.label, '</span>',
'</span>',
meta ? '<span class="rmActionMeta">' + meta + '</span>' : '',
'</label>'
]);
}).join('');
return joinHtml([
'<div style="margin:0 0 8px;color:', tk.cSubM, ';font-size:13px;">Обнаружены открытые номинации:</div>',
'<div', listId ? ' id="' + listId + '"' : '', ' class="rmActionList">',
actionItemsHtml,
'</div>'
]);
}
function buildNestedCommentFieldsHtml(opts) {
var options = opts || {};
var wrapId = options.wrapId || '';
var textareaId = options.textareaId || '';
var textareaClass = options.textareaClass ? ' ' + options.textareaClass : '';
var textareaStyleExtra = options.textareaStyleExtra || '';
var wrapStyleExtra = options.wrapStyleExtra || '';
var placeholder = options.placeholder || 'Комментарий (необязательно)';
var beforeHtml = options.beforeHtml || '';
var marginTop = options.marginTop || '6px';
var minHeight = parseInt(options.minHeight, 10) || 90;
var isEmbedded = !!options.embedded;
var wrapClass = isEmbedded ? '' : (' class="' + RESIZE_CLASS + '"');
var wrapStyle = 'display:none;margin-top:' + marginTop + ';max-width:100%;box-sizing:border-box;';
if (isEmbedded) {
wrapStyle += 'padding:0;border:0;background:transparent;';
} else {
wrapStyle += 'padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:6px;background:' + tk.bgNSub + ';';
}
wrapStyle += wrapStyleExtra;
return joinHtml([
'<div id="', wrapId, '"', wrapClass, ' style="', wrapStyle, '">',
beforeHtml,
'<textarea id="', textareaId, '" class="rmNestedCommentInput', textareaClass,
'" placeholder="', escapeHtml(placeholder), '" style="', stInputFull,
'min-height:', minHeight, 'px;resize:both;margin-bottom:6px;', textareaStyleExtra, '"></textarea>',
buildQuickPhrasesPanelHtml(textareaId),
'</div>'
]);
}
function buildConditionalRetFieldsHtml() {
return buildNestedCommentFieldsHtml({
wrapId: 'rmCloseConditionalWrap',
textareaId: 'rmCloseConditionalReason',
placeholder: 'Условие / пояснение (необязательно)',
marginTop: '8px',
minHeight: 90,
beforeHtml: '<input id="rmCloseConditionalDeadline" type="text" placeholder="Срок доработки: 2026-05-31" style="' + stInputFull + 'margin-bottom:6px;">'
});
}
function buildAddArticleButtonHtml(options) {
var opts = options || {};
var title = opts.addTitle || 'Добавить статью';
return '<button type="button" class="rmAddArticle" title="' + escapeHtml(title) + '" aria-label="' + escapeHtml(title) + '" style="' + stRemoveBtn + '">+</button>';
}
function buildSquareAddButtonHtml(id, title, className) {
var idAttr = id ? ' id="' + id + '"' : '';
var clsAttr = className ? ' class="' + className + '"' : '';
var label = title || 'Добавить';
return '<button' + idAttr + ' type="button"' + clsAttr + ' title="' + escapeHtml(label) + '" aria-label="' + escapeHtml(label) + '" style="' + stRemoveBtn + '">+</button>';
}
function buildMultiArticleButtonsHtml(commentWrapId, commentId, options) {
var opts = options || {};
var commentBtnStyle = stToolBtn + (opts.showComment ? '' : 'display:none;');
if (opts.showAdd) return buildAddArticleButtonHtml(opts);
return joinHtml([
'<button type="button" class="rmToggleBtn rmArticleCommentToggle" data-rm-comment-wrap="', commentWrapId,
'" data-rm-comment-textarea="', commentId, '" aria-expanded="false" style="', commentBtnStyle, '">Комментарий</button>',
'<button type="button" class="rmRemoveInput" style="', stRemoveBtn, '" title="', escapeHtml(opts.removeTitle || 'Удалить статью'), '">−</button>'
]);
}
function buildMultiArticleRowHtml(index, options) {
var opts = options || {};
var articleId = 'rmArticle' + index;
var commentWrapId = 'rmArticleCommentWrap' + index;
var commentId = 'rmArticleComment' + index;
var articleValue = opts.articleValue ? ' value="' + escapeHtml(opts.articleValue) + '"' : '';
var inputPlaceholder = opts.inputPlaceholder || 'Статья';
var commentPlaceholder = opts.commentPlaceholder || 'Комментарий только для этой статьи (необязательно)';
var articleRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
var blockStyle = 'max-width:100%;box-sizing:border-box;';
var buttonsHtml = buildMultiArticleButtonsHtml(commentWrapId, commentId, {
showAdd: !!opts.showAdd,
showComment: !!opts.showComment,
addTitle: opts.addTitle,
removeTitle: opts.removeTitle
});
return joinHtml([
'<div class="rmMultiArticleBlock ', RESIZE_CLASS, '" style="', blockStyle, '">',
'<div', opts.rowId ? ' id="' + opts.rowId + '"' : '', ' class="rmArticleRow" style="', articleRowStyle, '">',
'<input id="', articleId, '" type="text" placeholder="', escapeHtml(inputPlaceholder), '" class="rmArticleInput" style="', stInputBox, '"', articleValue, '>',
buttonsHtml,
'</div>',
buildNestedCommentFieldsHtml({
wrapId: commentWrapId,
textareaId: commentId,
textareaClass: 'rmArticleCommentInput',
placeholder: commentPlaceholder,
marginTop: '4px',
minHeight: 90,
embedded: true,
wrapStyleExtra: 'padding:0 0 0 12px;background:transparent;',
textareaStyleExtra: 'border-color:' + tk.bSubS + ';border-radius:4px;background:' + tk.bgBase + ';'
}),
'</div>'
]);
}
function showInfoAndClose(mainText, detailsText, isErr) {
$('#removerModalContent').html(buildInfoBoxHtml(mainText, detailsText || '', isErr || false));
renderModalFooter('close');
}
function getSelectedAction(inputName, actionMap) {
var id = $('[name="' + inputName + '"]:checked').val();
var sel = actionMap[id];
if (!sel) alert('Выберите действие.');
return sel || null;
}
function prependTemplateToNoinclude(text, templateText) {
var source = String(text || '');
var tpl = String(templateText || '').trim();
if (!tpl) return source;
var match = source.match(RE_NOINCLUDE);
if (match) {
var before = source.slice(0, source.indexOf(match[0]));
if (/\S/.test(before)) return '<noinclude>' + tpl + '</noinclude>\n' + source;
var content = String(match[2] || '').replace(/^\n+/, '');
return source.replace(match[0], match[1] + '<noinclude>' + tpl + (content ? '\n' + content : '') + '\n</noinclude>');
}
return '<noinclude>' + tpl + '</noinclude>\n' + source;
}
function buildGeneratedNominationTemplateText(job, pg) {
var tplStr = '';
if (!job) return '';
if (job.opId === 'fRm') {
tplStr = job.kbuTemplate || '';
if (job.kbuAddInfo) tplStr += '|1=' + job.kbuAddInfo;
if (job.kbuComment) tplStr += '|' + (job.kbuAddInfo ? '2' : '1') + '=' + job.kbuComment;
return tplStr ? (T_OPEN + tplStr + T_CLOSE) : '';
}
if (typeof job.articleTpl !== 'function') return '';
tplStr = job.articleTpl(job.tplpar, job.date[0]);
if (job.opId === 'merge' && job.tplpar) {
tplStr = (job.op.nomination.articleTpl)(
('|' + job.tplpar + '|').replace('|' + pg + '|', '|').slice(1, -1),
job.date[0]
);
}
return tplStr ? (T_OPEN + tplStr + T_CLOSE) : '';
}
function applyConflictTemplateResolution(articleText, job, pg, decision) {
var rule = getNominationConflictRule(job);
var generatedTemplate = buildGeneratedNominationTemplateText(job, pg);
var source = String(articleText || '');
if (!generatedTemplate || !decision) return source;
if (decision.templateAction === 'overwrite') {
var cleaned = rule ? stripTemplatesByPattern(source, rule.namePattern).text : source;
return prependTemplateToNoinclude(cleaned, generatedTemplate);
}
if (decision.templateAction === 'prepend') return prependTemplateToNoinclude(source, generatedTemplate);
return source;
}
function inspectMultiNominationConflicts(job, callback) {
var cb = callback || function () {};
var pages = (job && job.multiArticles) ? job.multiArticles.slice() : [];
var conflicts = [];
var statusId = logStatus('Проверяются статьи на наличие уже установленных шаблонов...', null, { pending: true, trackError: false });
if (!pages.length) {
logStatus('Проверка завершена: конфликтов не найдено.', null, { statusId: statusId, trackError: false });
cb(null, conflicts);
return;
}
eachSequential(pages, function (pg, next) {
getText(pg, function (articleText, readErr) {
var conflict;
if (readErr) { next(makeReadError(readErr, 'read_failed', 'Не удалось проверить страницу «' + pg + '».')); return; }
if (articleText === null) { next({ code: 'read_failed', info: 'Страница «' + pg + '» не существует.' }); return; }
conflict = detectNominationConflict(articleText, job);
if (conflict) {
conflicts.push($.extend({ pageName: pg }, conflict));
logStatus('В статье ' + buildQuotedStatusPageLink(pg) + ' обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.', null, { trackError: false });
}
next();
});
}, function (err) {
if (err) {
logStatus('Проверка статей.', err, { statusId: statusId });
cb(err);
return;
}
logStatus(
conflicts.length
? 'Проверка завершена: найдены статьи с уже установленными шаблонами.'
: 'Проверка завершена: конфликтов не найдено.',
null,
{ statusId: statusId, trackError: false }
);
cb(null, conflicts);
});
}
function buildNominationConflictResolutionHtml(conflicts) {
return '<div class="rmInfoBox"><p style="margin:0 0 6px;">Найдены статьи, где шаблон уже стоит. Для каждой конфликтующей страницы выберите, что делать со статьёй и с шаблоном.</p>' +
'<p style="margin:0;color:' + tk.cSubM + ';font-size:12px;line-height:1.45;">По умолчанию такие статьи исключаются из новой номинации, а существующий шаблон остаётся без изменений.</p></div>' +
'<div class="rmConflictLead">После выбора нажмите «Продолжить номинирование».</div>' +
'<div id="rmConflictList" class="rmConflictList">' +
conflicts.map(function (conflict, index) {
var pageLink = buildStatusPageLink(conflict.pageName);
return '<div class="rmConflictCard" data-rm-conflict-index="' + index + '">' +
'<input type="hidden" class="rmConflictPageAction" value="skip">' +
'<input type="hidden" class="rmConflictTemplateAction" value="keep">' +
'<div class="rmConflictTitle">' + pageLink + '</div>' +
'<div class="rmConflictMeta">Обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.</div>' +
'<div class="rmConflictGroup">' +
'<div class="rmConflictGroupTitle">Действие со статьёй</div>' +
'<div class="rmConflictButtons" data-rm-conflict-group="page">' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="page" data-rm-choice="skip" aria-pressed="true">Убрать из номинации</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="page" data-rm-choice="keep" aria-pressed="false">Оставить в номинации</button>' +
'</div></div>' +
'<div class="rmConflictGroup">' +
'<div class="rmConflictGroupTitle">Действие с шаблоном</div>' +
'<div class="rmConflictButtons" data-rm-conflict-group="template">' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="template" data-rm-choice="keep" aria-pressed="true">Оставить как есть</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="overwrite" aria-pressed="false">Новая дата</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="prepend" aria-pressed="false">Второй сверху</button>' +
'</div>' +
'<div class="rmConflictHint">Если статья исключается из номинации, действие с шаблоном не применяется.</div>' +
'</div></div>';
}).join('') +
'</div>';
}
function updateNominationConflictCardState($card) {
var pageAction = $card.find('.rmConflictPageAction').val() || 'skip';
var disableTemplate = pageAction !== 'keep';
var $templateButtons = $card.find('[data-rm-choice-type="template"]');
$card.toggleClass('is-skip', disableTemplate);
$card.find('[data-rm-conflict-group="template"]').toggleClass('is-disabled', disableTemplate);
$templateButtons.prop('disabled', disableTemplate).toggleClass('is-disabled', disableTemplate);
}
function bindNominationConflictResolutionUi() {
var $content = $('#removerModalContent');
function setChoice($card, type, value) {
var inputClass = type === 'page' ? '.rmConflictPageAction' : '.rmConflictTemplateAction';
$card.find(inputClass).val(value);
$card.find('[data-rm-choice-type="' + type + '"]').each(function () {
var isActive = $(this).data('rmChoice') === value;
$(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false');
});
updateNominationConflictCardState($card);
}
$content.off('.rmConflictResolution').on('click.rmConflictResolution', '[data-rm-choice-type]', function () {
var $btn = $(this);
var $card = $btn.closest('.rmConflictCard');
if ($btn.prop('disabled')) return;
setChoice($card, $btn.data('rmChoiceType'), $btn.data('rmChoice'));
});
$('#rmConflictList .rmConflictCard').each(function () { updateNominationConflictCardState($(this)); });
}
function collectNominationConflictResolution(conflicts) {
var decisions = {};
(conflicts || []).forEach(function (conflict, index) {
var $card = $('#rmConflictList .rmConflictCard[data-rm-conflict-index="' + index + '"]');
decisions[normTitle(conflict.pageName)] = {
pageAction: $card.find('.rmConflictPageAction').val() || 'skip',
templateAction: $card.find('.rmConflictTemplateAction').val() || 'keep'
};
});
return decisions;
}
function applyNominationConflictResolutionToJob(job, decisions) {
var resultArticles;
var headerText;
if (!job || !job.isMulti) {
job.conflictDecisions = decisions || {};
return { value: job };
}
resultArticles = (job.multiArticles || []).filter(function (pageName) {
var decision = decisions && decisions[normTitle(pageName)];
return !decision || decision.pageAction !== 'skip';
});
if (!resultArticles.length) return { error: 'После исключения конфликтующих статей в номинации не осталось ни одной страницы.' };
job.conflictDecisions = decisions || {};
job.multiArticles = resultArticles.slice();
job.pages = resultArticles.slice().reverse();
headerText = String(job.multiHeaderText || '').trim();
job.section = headerText || ('[[:' + resultArticles[0] + ']]');
job.sectionNW = job.section.replace(/\[\[:/g, '').replace(/]]/g, '');
job.msg = job.multiNominationFormat === 'list'
? buildMultiNominationListText(resultArticles, job.multiNominationBody, job.multiArticleComments)
: buildMultiNominationText(resultArticles, job.multiNominationBody, job.multiArticleComments);
job.summary = makeSummary('номинация [[' + job.nomPage + '#' + job.sectionNW + ']]');
return { value: job };
}
function showNominationConflictResolution(job, conflicts, onContinue) {
resetModalObservers();
$('#removerModalContent').html(buildNominationConflictResolutionHtml(conflicts));
bindNominationConflictResolutionUi();
syncLinkWidths();
renderModalFooter('submit', {
submitText: 'Продолжить номинирование',
showSubscribe: true,
preserveLogOnSubmit: true,
onSubmit: function () {
var decisions = collectNominationConflictResolution(conflicts);
var applied = applyNominationConflictResolutionToJob(job, decisions);
if (applied.error) {
alert(applied.error);
return false;
}
if (typeof onContinue === 'function') onContinue(applied.value);
return true;
}
});
}
function bindTouchTextareaGrip($ta, sync, getMaxWidth, options) {
var opts = options || {};
var minWidth = parseInt(opts.minWidth, 10) || parseInt(sz.taMinW, 10) || 180;
var minHeight = parseInt(opts.minHeight, 10) || parseInt(sz.taMinH, 10) || 100;
var allowWidthResize = opts.allowWidth !== false;
var syncFn = typeof sync === 'function' ? sync : function () {};
var getMaxWidthFn = typeof getMaxWidth === 'function' ? getMaxWidth : function () { return $ta.outerWidth() || minWidth; };
var usePointerEvents = typeof window.PointerEvent === 'function';
var dragState = { active: false, startX: 0, startY: 0, startWidth: 0, startHeight: 0 };
var gripStyle = 'height:20px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-top:0;border-radius:0 0 4px 4px;background:' + tk.bgNSub + ';display:flex;align-items:center;justify-content:center;cursor:ns-resize;touch-action:none;user-select:none;-webkit-user-select:none;';
if (opts.gripMarginBottom) gripStyle += 'margin-bottom:' + opts.gripMarginBottom + ';';
var $grip = $('<div data-rm-textarea-grip="1" style="' + gripStyle + '"><span style="display:block;width:42px;height:4px;border-radius:999px;background:' + tk.bSub + ';opacity:.9;"></span></div>');
function getCoord(evt, key) {
var e = evt.originalEvent || evt;
if (e.touches && e.touches.length) return e.touches[0][key];
if (e.changedTouches && e.changedTouches.length) return e.changedTouches[0][key];
return e[key];
}
function stopDrag() {
dragState.active = false;
$(window).off('.rmTaResize');
}
function onDragMove(evt) {
var clientX;
var clientY;
if (!dragState.active) return;
clientX = getCoord(evt, 'clientX');
clientY = getCoord(evt, 'clientY');
if (typeof clientY !== 'number') return;
if (allowWidthResize && typeof clientX === 'number') $ta.css('width', Math.max(minWidth, Math.min(getMaxWidthFn(), dragState.startWidth + (clientX - dragState.startX))) + 'px');
$ta.css('height', Math.max(minHeight, dragState.startHeight + (clientY - dragState.startY)) + 'px');
syncFn();
if (evt.preventDefault) evt.preventDefault();
}
function startDrag(evt) {
var clientY = getCoord(evt, 'clientY');
if (typeof clientY !== 'number') return;
dragState.active = true;
dragState.startX = getCoord(evt, 'clientX') || 0;
dragState.startY = clientY;
dragState.startWidth = $ta.outerWidth();
dragState.startHeight = $ta.outerHeight();
if (evt.preventDefault) evt.preventDefault();
$(window).off('.rmTaResize');
if (usePointerEvents) {
$(window).on('pointermove.rmTaResize', onDragMove).on('pointerup.rmTaResize pointercancel.rmTaResize', stopDrag);
} else {
$(window).on('touchmove.rmTaResize mousemove.rmTaResize', onDragMove).on('touchend.rmTaResize touchcancel.rmTaResize mouseup.rmTaResize', stopDrag);
}
}
$ta.css({ 'border-bottom-left-radius': '0', 'border-bottom-right-radius': '0' });
$ta.next('[data-rm-textarea-grip]').remove();
$ta.after($grip);
if (usePointerEvents) $grip.on('pointerdown.rmTaGrip', startDrag);
else $grip.on('touchstart.rmTaGrip mousedown.rmTaGrip', startDrag);
}
function applyModalContentWidth($modal, contentWidth, options) {
var opts = options || {};
var layout = getModalLayout();
var modalFrame = getBoxFrameWidth($modal);
var safeContentWidth = Math.max(layout.minWidth, Math.min(contentWidth, layout.maxOuterWidth - modalFrame));
var modalWidth = safeContentWidth + modalFrame;
var initialContentW = parseFloat($modal.data('rmInitialContentW')) || 0;
$modal.css({
width: modalWidth + 'px',
'max-width': layout.maxOuterWidth + 'px',
'box-sizing': 'border-box',
'margin-left': layout.shouldCenter ? 'auto' : '0',
'margin-right': layout.shouldCenter ? 'auto' : '0'
}).toggleClass('rmCompactContent', safeContentWidth < 520);
$('.' + RESIZE_CLASS).css({
width: safeContentWidth + 'px',
'max-width': '100%',
'box-sizing': 'border-box'
});
$('#rmMsg,#nominationReason,#rmReportText').each(function () {
var $textarea = $(this);
var textareaId = this.id;
if (!$textarea.length) return;
$textarea.css('width', safeContentWidth + 'px');
$textarea.next('[data-rm-textarea-grip]').css('width', safeContentWidth + 'px');
$('.rmQuickPhrasesPanel[data-rm-target="' + textareaId + '"]').css('width', safeContentWidth + 'px');
});
$('.rmNestedCommentInput').each(function () {
var $textarea = $(this);
var $wrap = $textarea.parent();
var containerFrame = parseFloat($textarea.data('rmNestedContainerFrame')) || 0;
var wrapFrame = parseFloat($textarea.data('rmNestedWrapFrame')) || 0;
var safeMinWidth = parseFloat($textarea.data('rmNestedMinWidth')) || 0;
var wrapOuterWidth;
var textareaWidth;
if (!$wrap.length || !$wrap.is(':visible')) return;
wrapOuterWidth = Math.max(1, safeContentWidth - containerFrame);
textareaWidth = Math.max(1, wrapOuterWidth - wrapFrame);
$wrap.css({
width: wrapOuterWidth + 'px',
'max-width': '100%',
'box-sizing': 'border-box'
});
$textarea.css({
width: textareaWidth + 'px',
'min-width': Math.min(safeMinWidth || textareaWidth, textareaWidth) + 'px'
});
$textarea.next('[data-rm-textarea-grip]').css('width', textareaWidth + 'px');
$('.rmQuickPhrasesPanel[data-rm-target="' + this.id + '"]').css('width', textareaWidth + 'px');
});
syncLinkWidths();
if (isVector22) {
var $content = $('#content');
if ($content.length && modalWidth > initialContentW) $content.css({ 'min-width': modalWidth + 'px' });
else if ($content.length) $content.css({ 'min-width': '' });
} else {
$('#content').css({ 'min-width': '' });
}
if (!opts.skipStore) $modal.data('rmContentWidth', safeContentWidth);
return safeContentWidth;
}
function setupNestedResizableTextarea(textareaId, wrapId, minWidth, minHeight) {
var $ta = $('#' + textareaId);
var $wrap = $('#' + wrapId);
var $modal = $('#removerModal');
var $container = $wrap.parent();
var layout = getModalLayout();
var safeMinWidth = parseInt(minWidth, 10) || 280;
var safeMinHeight = parseInt(minHeight, 10) || 90;
var initialWidth;
var modalFrame;
var containerFrame;
var wrapFrame;
function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; }
function getMaxTextareaWidth(currentLayout) {
return Math.max(1, currentLayout.maxOuterWidth - modalFrame - containerFrame - wrapFrame);
}
function getEffectiveMinWidth(currentLayout) {
return Math.min(safeMinWidth, getMaxTextareaWidth(currentLayout));
}
function sync() {
var currentLayout = getModalLayout();
var maxTextareaWidth = getMaxTextareaWidth(currentLayout);
var textareaWidth = $ta.outerWidth();
var contentWidth;
if (!$wrap.is(':visible')) return;
if (textareaWidth > maxTextareaWidth) {
$ta.css('width', maxTextareaWidth + 'px');
textareaWidth = $ta.outerWidth();
}
contentWidth = Math.min(currentLayout.maxOuterWidth - modalFrame, textareaWidth + wrapFrame + containerFrame);
applyModalContentWidth($modal, contentWidth);
}
if (!$ta.length || !$wrap.length || !$modal.length) return;
modalFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width');
containerFrame = $container.length
? px($container, 'padding-left') + px($container, 'padding-right') + px($container, 'border-left-width') + px($container, 'border-right-width')
: 0;
wrapFrame = px($wrap, 'padding-left') + px($wrap, 'padding-right') + px($wrap, 'border-left-width') + px($wrap, 'border-right-width');
initialWidth = Math.min(
Math.max(getEffectiveMinWidth(layout), getDefaultResizableWidth(modalFrame + containerFrame + wrapFrame)),
getMaxTextareaWidth(layout)
);
$ta.css({
width: initialWidth + 'px',
'min-width': getEffectiveMinWidth(layout) + 'px',
'min-height': safeMinHeight + 'px',
'box-sizing': 'border-box',
resize: layout.useFullWidth ? 'none' : 'both',
'border-bottom-left-radius': '',
'border-bottom-right-radius': ''
});
$ta.data('rmNestedContainerFrame', containerFrame);
$ta.data('rmNestedWrapFrame', wrapFrame);
$ta.data('rmNestedMinWidth', safeMinWidth);
$ta.next('[data-rm-textarea-grip]').remove();
if (layout.useFullWidth) {
bindTouchTextareaGrip($ta, sync, function () {
return getMaxTextareaWidth(getModalLayout());
}, {
minWidth: getEffectiveMinWidth(layout),
minHeight: safeMinHeight
});
}
registerModalLayoutSync(sync);
$(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
if (typeof ResizeObserver === 'function') {
var observer = new ResizeObserver(sync);
observer.observe($ta[0]);
registerResizeObserver(observer);
}
sync();
}
function setupResizableModal(textareaId) {
var $ta = $('#' + textareaId);
var $modal = $('#removerModal');
var layout = getModalLayout();
function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; }
applyV2022Layout($modal);
$modal.css({ display: 'block', 'margin-left': layout.shouldCenter ? 'auto' : '0', 'margin-right': layout.shouldCenter ? 'auto' : '0' });
var isBorderBox = ($modal.css('box-sizing') || '').toLowerCase() === 'border-box';
var hFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width');
var initialContentW = isVector22 ? ($('#content').outerWidth() || 0) : 0;
var minWidth = layout.minWidth;
$modal.data('rmInitialContentW', initialContentW);
$ta.css({ width: getDefaultResizableWidth(hFrame) + 'px', height: sz.taH, padding: '8px', 'box-sizing': 'border-box',
border: '1px solid ' + tk.bSub, 'border-radius': '2px', background: tk.bgBase,
color: 'inherit', resize: layout.useFullWidth ? 'none' : 'both', 'min-height': sz.taMinH, 'min-width': sz.taMinW });
$(window).off('.rmTaResize');
function sync() {
var currentLayout = getModalLayout();
var maxTextareaWidth = Math.max(minWidth, currentLayout.maxOuterWidth - Math.floor(hFrame));
var w = $ta.outerWidth();
if (w > maxTextareaWidth) {
$ta.css('width', maxTextareaWidth + 'px');
w = $ta.outerWidth();
}
applyModalContentWidth($modal, isBorderBox ? w : Math.min(currentLayout.maxOuterWidth - hFrame, w));
}
if (layout.useFullWidth) bindTouchTextareaGrip($ta, sync, function () {
return Math.max(minWidth, getModalLayout().maxOuterWidth - Math.floor(hFrame));
});
else {
$ta.css({ 'border-bottom-left-radius': '', 'border-bottom-right-radius': '' });
$ta.next('[data-rm-textarea-grip]').remove();
}
registerModalLayoutSync(sync);
$(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
if (typeof ResizeObserver === 'function') {
var observer = new ResizeObserver(sync);
observer.observe($ta[0]);
registerResizeObserver(observer);
}
sync();
}
function addInputRow(opts) {
var w = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0;
$('#' + opts.containerId).append(joinHtml([
'<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, w ? 'width:' + w + 'px;' : '', '">',
'<input type="text" class="', opts.inputClass || 'variantInput', '" placeholder="', opts.placeholder || '', '" style="', stInputBox, '">',
'<button type="button" class="rmRemoveInput" style="', stRemoveBtn, '" title="Удалить">−</button>',
'</div>'
]));
syncModalLayout();
}
$(document).off('click.rmRemoveInput').on('click.rmRemoveInput', '.rmRemoveInput', function () {
$(this).closest('.rmInputRow').remove();
syncModalLayout();
});
$(document).off('click.rmQuickPhraseInsert').on('click.rmQuickPhraseInsert', '.rmQuickPhraseActionBtn', function (e) {
var targetId;
var phrase;
e.preventDefault();
targetId = $(this).data('rmTarget');
phrase = $(this).attr('data-rm-phrase') || '';
if (!targetId) return;
insertTextIntoTextarea($('#' + targetId), phrase);
});
function buildMultiInputHtml(c) {
var addLabel = c.addBtnLabel || '+ Добавить';
return joinHtml([
'<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="', c.firstId, '" type="text" class="', c.inputClass || 'variantInput', '" placeholder="', c.firstPh || '', '" style="', stInputBox, '">',
buildSquareAddButtonHtml(c.addBtnId, addLabel.replace(/^\+\s*/, '') || 'Добавить'),
'</div>',
'<div id="', c.containerId, '"></div>'
]);
}
function wireMultiInput(c) {
$('#' + c.addBtnId).click(function () {
var count = $('#' + c.containerId + ' .rmInputRow').length;
if (typeof c.maxRows === 'number' && count >= c.maxRows) { alert(c.maxMsg || 'Достигнут лимит.'); return; }
addInputRow({ containerId: c.containerId, placeholder: c.addPh, inputClass: c.inputClass });
});
}
function buildSettingsFieldHtml(label, controlHtml, helpText, options) {
var opts = options || {};
var helpHtml = helpText
? '<div class="rmSettingsFieldHint">' + helpText + '</div>'
: '';
var labelHtml = opts.forId
? '<label class="rmSettingsFieldLabel" for="' + opts.forId + '">' + label + '</label>'
: '<div class="rmSettingsFieldLabel">' + label + '</div>';
return joinHtml([
'<div class="rmSettingsField">',
labelHtml,
'<div class="rmSettingsFieldControl">', controlHtml, '</div>',
helpHtml,
'</div>'
]);
}
function buildSettingsSectionHtml(title, bodyHtml, helpText, options) {
var opts = options || {};
var headerHtml = '';
var description = [];
if (opts.titleNote) description.push(opts.titleNote);
if (helpText) description.push(helpText);
if (title || description.length) {
headerHtml = joinHtml([
'<div class="rmSettingsSectionHeader">',
title ? '<div class="rmSettingsSectionTitle">' + title + '</div>' : '',
description.length ? '<div class="rmSettingsSectionDescription">' + description.join(' ') + '</div>' : '',
'</div>'
]);
}
return joinHtml([
'<div class="rmSettingsSection">',
headerHtml,
bodyHtml,
'</div>'
]);
}
function buildSettingsSimpleCheckboxHtml(id, text) {
return joinHtml([
'<label class="rmSettingsCheck">',
'<input id="', id, '" type="checkbox">',
'<span>', text, '</span>',
'</label>'
]);
}
function buildQuickPhrasesSettingsEditorHtml() {
return joinHtml([
'<div id="rmSettingsQuickPhrasesEditor" class="rmQuickPhraseEditor">',
'<div id="rmSettingsQuickPhrasesList" class="rmQuickPhraseList"></div>',
'<input id="rmSettingsQuickPhraseInput" type="text" autocomplete="off" style="', stInputFull, 'margin-bottom:0;">',
'<div id="rmSettingsQuickPhraseMeta" class="rmQuickPhraseMeta"></div>',
'</div>'
]);
}
function getQuickPhraseEditor() {
return $('#rmSettingsQuickPhrasesEditor');
}
function getQuickPhraseEditorState() {
var $editor = getQuickPhraseEditor();
var phrases = normalizeQuickPhrasesList($editor.data('rmQuickPhrases'), []);
var editingIndex = parseInt($editor.data('rmQuickPhraseEditingIndex'), 10);
if (isNaN(editingIndex)) editingIndex = -1;
return { editor: $editor, phrases: phrases, editingIndex: editingIndex };
}
function setQuickPhraseEditorState(phrases, editingIndex) {
var $editor = getQuickPhraseEditor();
var normalized = normalizeQuickPhrasesList(phrases, []);
var safeEditingIndex = parseInt(editingIndex, 10);
if (isNaN(safeEditingIndex) || safeEditingIndex < 0 || safeEditingIndex >= normalized.length) safeEditingIndex = -1;
if (!$editor.length) return;
$editor.data('rmQuickPhrases', normalized);
$editor.data('rmQuickPhraseEditingIndex', safeEditingIndex);
renderQuickPhraseEditor();
}
function clearQuickPhraseDropState() {
var $editor = getQuickPhraseEditor();
$editor.removeData('rmQuickPhraseDragIndex');
$editor.removeData('rmQuickPhraseDropIndex');
$editor.removeData('rmQuickPhraseDropAfter');
$editor.find('.rmQuickPhraseChip').removeClass('is-dragging is-drop-before is-drop-after');
}
function renderQuickPhraseEditor() {
var state = getQuickPhraseEditorState();
var $list = $('#rmSettingsQuickPhrasesList');
var $input = $('#rmSettingsQuickPhraseInput');
var $meta = $('#rmSettingsQuickPhraseMeta');
if (!state.editor.length || !$list.length || !$input.length) return;
if (state.phrases.length) {
$list.html(state.phrases.map(function (phrase, index) {
var chipClass = 'rmQuickPhraseChip' + (index === state.editingIndex ? ' is-editing' : '');
return joinHtml([
'<div class="', chipClass, '" draggable="true" data-rm-quick-index="', index, '">',
'<button type="button" class="rmQuickPhraseEditBtn" title="Редактировать фразу">', escapeHtml(phrase), '</button>',
'<button type="button" class="rmQuickPhraseRemoveBtn" title="Удалить фразу" aria-label="Удалить фразу">×</button>',
'</div>'
]);
}).join(''));
} else {
$list.html('<div class="rmQuickPhraseEmpty">Фразы пока не добавлены.</div>');
}
$input
.attr('placeholder', state.editingIndex >= 0 ? 'Изменить значение...' : 'Добавить значение...')
.toggleClass('is-editing', state.editingIndex >= 0);
$meta
.text('')
.hide();
}
function notifyQuickPhraseEditorChanged() {
var $editor = getQuickPhraseEditor();
if ($editor.length) $editor.trigger('rmQuickPhrasesChanged');
}
function startQuickPhraseEdit(index) {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
if (index < 0 || index >= state.phrases.length || !$input.length) return;
state.editor.data('rmQuickPhraseEditingIndex', index);
$input.val(state.phrases[index]);
renderQuickPhraseEditor();
$input.trigger('focus');
if ($input[0] && typeof $input[0].select === 'function') $input[0].select();
}
function cancelQuickPhraseEdit() {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
state.editor.data('rmQuickPhraseEditingIndex', -1);
if ($input.length) $input.val('').removeClass('is-editing');
renderQuickPhraseEditor();
}
function saveQuickPhraseInput() {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
var value = normalizeQuickPhraseValue($input.val());
var next = [];
if (!$input.length || !value) return false;
if (state.editingIndex >= 0) {
state.phrases.forEach(function (phrase, index) {
if (index === state.editingIndex) {
next.push(value);
return;
}
if (phrase !== value && next.indexOf(phrase) === -1) next.push(phrase);
});
} else {
next = state.phrases.slice();
if (next.indexOf(value) === -1) next.push(value);
}
state.editor.data('rmQuickPhrases', normalizeQuickPhrasesList(next, []));
state.editor.data('rmQuickPhraseEditingIndex', -1);
$input.val('').removeClass('is-editing');
clearQuickPhraseDropState();
renderQuickPhraseEditor();
notifyQuickPhraseEditorChanged();
return true;
}
function removeQuickPhrase(index) {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
if (index < 0 || index >= state.phrases.length) return;
state.phrases.splice(index, 1);
state.editor.data('rmQuickPhrases', state.phrases);
if (state.editingIndex === index) {
state.editor.data('rmQuickPhraseEditingIndex', -1);
if ($input.length) $input.val('').removeClass('is-editing');
} else if (state.editingIndex > index) {
state.editor.data('rmQuickPhraseEditingIndex', state.editingIndex - 1);
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
notifyQuickPhraseEditorChanged();
}
function reorderQuickPhrases(phrases, fromIndex, toIndex, placeAfter) {
var result = phrases.slice();
var insertIndex = toIndex + (placeAfter ? 1 : 0);
var item;
if (fromIndex < 0 || fromIndex >= result.length || toIndex < 0 || toIndex >= result.length) return result;
item = result.splice(fromIndex, 1)[0];
if (fromIndex < insertIndex) insertIndex--;
result.splice(insertIndex, 0, item);
return result;
}
function getQuickPhraseDropPointer(evt) {
var originalEvent = evt && (evt.originalEvent || evt);
if (!originalEvent) return null;
if (typeof originalEvent.clientX !== 'number' || typeof originalEvent.clientY !== 'number') return null;
return { x: originalEvent.clientX, y: originalEvent.clientY };
}
function getQuickPhraseDropTarget($list, pointer, dragIndex) {
var candidates = [];
var rowCandidates;
var minRowDistance = Infinity;
var bestBoundary = null;
var bestBoundaryDistance = Infinity;
if (!$list || !$list.length || !pointer) return null;
$list.children('.rmQuickPhraseChip').each(function () {
var index = parseInt($(this).attr('data-rm-quick-index'), 10);
var rect;
var rowDistance;
if (isNaN(index) || index === dragIndex) return;
rect = this.getBoundingClientRect();
if (!rect.width || !rect.height) return;
rowDistance = pointer.y < rect.top ? (rect.top - pointer.y) : (pointer.y > rect.bottom ? (pointer.y - rect.bottom) : 0);
candidates.push({
node: this,
index: index,
left: rect.left,
right: rect.right,
top: rect.top,
bottom: rect.bottom,
midX: rect.left + rect.width / 2,
rowDistance: rowDistance
});
if (rowDistance < minRowDistance) minRowDistance = rowDistance;
});
if (!candidates.length) return null;
rowCandidates = candidates
.filter(function (candidate) { return candidate.rowDistance === minRowDistance; })
.sort(function (a, b) {
if (a.left !== b.left) return a.left - b.left;
return a.index - b.index;
});
if (!rowCandidates.length) return null;
if (pointer.x <= rowCandidates[0].left) {
return { index: rowCandidates[0].index, placeAfter: false, node: rowCandidates[0].node };
}
if (pointer.x >= rowCandidates[rowCandidates.length - 1].right) {
return {
index: rowCandidates[rowCandidates.length - 1].index,
placeAfter: true,
node: rowCandidates[rowCandidates.length - 1].node
};
}
for (var i = 0; i < rowCandidates.length; i++) {
var candidate = rowCandidates[i];
if (pointer.x >= candidate.left && pointer.x <= candidate.right) {
return {
index: candidate.index,
placeAfter: pointer.x > candidate.midX,
node: candidate.node
};
}
}
rowCandidates.forEach(function (candidate) {
var leftDistance = Math.abs(pointer.x - candidate.left);
var rightDistance = Math.abs(pointer.x - candidate.right);
if (leftDistance < bestBoundaryDistance) {
bestBoundaryDistance = leftDistance;
bestBoundary = { index: candidate.index, placeAfter: false, node: candidate.node };
}
if (rightDistance < bestBoundaryDistance) {
bestBoundaryDistance = rightDistance;
bestBoundary = { index: candidate.index, placeAfter: true, node: candidate.node };
}
});
return bestBoundary;
}
function bindQuickPhrasesEditor() {
var $editor = getQuickPhraseEditor();
var $list = $('#rmSettingsQuickPhrasesList');
var $input = $('#rmSettingsQuickPhraseInput');
function updateQuickPhraseDropTarget(evt) {
var dragIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10);
var pointer = getQuickPhraseDropPointer(evt);
var target;
if (isNaN(dragIndex) || !pointer) return null;
target = getQuickPhraseDropTarget($list, pointer, dragIndex);
if (!target) return null;
$editor.data('rmQuickPhraseDropIndex', target.index);
$editor.data('rmQuickPhraseDropAfter', !!target.placeAfter);
$editor.find('.rmQuickPhraseChip').removeClass('is-drop-before is-drop-after');
$(target.node).addClass(target.placeAfter ? 'is-drop-after' : 'is-drop-before');
return target;
}
function applyQuickPhraseDrop() {
var state = getQuickPhraseEditorState();
var fromIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10);
var toIndex = parseInt($editor.data('rmQuickPhraseDropIndex'), 10);
var placeAfter = $editor.data('rmQuickPhraseDropAfter') === true;
var changed = false;
if (!isNaN(fromIndex) && !isNaN(toIndex) && fromIndex !== toIndex) {
state.editor.data('rmQuickPhrases', reorderQuickPhrases(state.phrases, fromIndex, toIndex, placeAfter));
if (state.editingIndex === fromIndex) state.editor.data('rmQuickPhraseEditingIndex', -1);
changed = true;
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
if (changed) notifyQuickPhraseEditorChanged();
}
if (!$editor.length || !$list.length || !$input.length) return;
$editor.off('.rmQuickPhraseEditor');
$list.off('.rmQuickPhraseEditor');
$input.off('.rmQuickPhraseEditor');
$input.on('keydown.rmQuickPhraseEditor', function (e) {
if (e.key === 'Enter' || e.keyCode === 13) {
e.preventDefault();
saveQuickPhraseInput();
} else if (e.key === 'Escape' || e.keyCode === 27) {
e.preventDefault();
cancelQuickPhraseEdit();
}
});
$editor
.on('click.rmQuickPhraseEditor', '.rmQuickPhraseEditBtn', function () {
var index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10);
startQuickPhraseEdit(index);
})
.on('click.rmQuickPhraseEditor', '.rmQuickPhraseRemoveBtn', function (e) {
var index;
e.preventDefault();
e.stopPropagation();
index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10);
removeQuickPhrase(index);
})
.on('dragstart.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
var index = parseInt($(this).attr('data-rm-quick-index'), 10);
if (isNaN(index)) return;
$editor.data('rmQuickPhraseDragIndex', index);
$(this).addClass('is-dragging');
if (e.originalEvent && e.originalEvent.dataTransfer) {
e.originalEvent.dataTransfer.effectAllowed = 'move';
e.originalEvent.dataTransfer.setData('text/plain', String(index));
}
})
.on('dragover.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
if ($editor.data('rmQuickPhraseDragIndex') === undefined) return;
e.preventDefault();
updateQuickPhraseDropTarget(e);
if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move';
})
.on('drop.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
e.preventDefault();
updateQuickPhraseDropTarget(e);
applyQuickPhraseDrop();
})
.on('dragend.rmQuickPhraseEditor', '.rmQuickPhraseChip', function () {
clearQuickPhraseDropState();
});
$list
.on('dragover.rmQuickPhraseEditor', function (e) {
if ($editor.data('rmQuickPhraseDragIndex') === undefined) return;
e.preventDefault();
updateQuickPhraseDropTarget(e);
if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move';
})
.on('drop.rmQuickPhraseEditor', function (e) {
e.preventDefault();
updateQuickPhraseDropTarget(e);
applyQuickPhraseDrop();
});
renderQuickPhraseEditor();
}
function collectQuickPhraseValues() {
return getQuickPhraseEditorState().phrases;
}
function collectQuickPhraseValuesSnapshot() {
var state = getQuickPhraseEditorState();
var value = normalizeQuickPhraseValue($('#rmSettingsQuickPhraseInput').val());
var next = state.phrases.slice();
if (!value) return next;
if (state.editingIndex >= 0) {
next[state.editingIndex] = value;
} else if (next.indexOf(value) === -1) {
next.push(value);
}
return normalizeQuickPhrasesList(next, []);
}
function isMenuTitlePresetValue(value) {
return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS;
}
function isMenuTitlePresetOnlySkin() {
return mwCfg.skin === 'minerva' || mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
}
function getDefaultMenuTitlePreset() {
return mwCfg.skin === 'minerva' ? MENU_TITLE_PRESET_TOOLS : MENU_TITLE_PRESET_CACTIONS;
}
function shouldPreserveStoredMenuTitleOnSave() {
return mwCfg.skin === 'minerva';
}
function isAvailableMenuTitlePresetValue(value) {
return getMenuTitlePresetOptions().some(function (option) {
return option.value === value;
});
}
function getMenuTitlePresetOptions() {
if (mwCfg.skin === 'minerva') {
return [
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Ещё' }
];
}
if (isVector22) {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты/Действия' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты/Основное' }
];
}
if (mwCfg.skin === 'timeless') {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты для страниц' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Вики-инструменты' }
];
}
if (mwCfg.skin === 'monobook') {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Верхняя панель' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' }
];
}
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: mwCfg.skin === 'vector' ? 'Ещё' : 'Ещё / Действия' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' }
];
}
function getMenuTitlePresetHintText() {
var base = (mwCfg.skin === 'vector' || isVector22)
? 'Можно либо изменить заголовок отдельного меню Remover, либо перенести все пункты в одно из существующих стандартных меню.'
: 'На этом скине Remover использует существующие стандартные меню; отдельное меню с собственным заголовком не создаётся.';
if (isVector22) base += ' В Vector 2022 кнопки «Действия» и «Основное» находятся внутри общего меню «Инструменты».';
else if (mwCfg.skin === 'vector') base += ' В Vector отдельный заголовок создаёт собственное меню рядом с «Ещё».';
else if (mwCfg.skin === 'minerva') base += ' В Minerva Neue пункты Remover показываются в меню «Ещё».';
else if (mwCfg.skin === 'timeless') base += ' В Timeless доступны только стандартные варианты: «Инструменты для страниц», «Страница» и «Вики-инструменты».';
else if (mwCfg.skin === 'monobook') base += ' В MonoBook доступны только два варианта: поместить пункты в «Инструменты» или вывести их в верхнюю панель. Собственный заголовок меню здесь не используется.';
return base;
}
function getSignatureSeparatorPreviewText(value) {
var separator = String(value || '').trim();
return separator ? (separator + ' ' + '~~' + '~~') : ('~~' + '~~');
}
function updateSignatureSeparatorPreview(value) {
var previewValue = (typeof value === 'string') ? value : ($('#rmSettingsSignatureSeparator').val() || '');
var $code = $('#rmSettingsSignaturePreviewCode');
if (!$code.length) return;
$code.text(getSignatureSeparatorPreviewText(previewValue));
}
function bindSignatureSeparatorPreview() {
var $input = $('#rmSettingsSignatureSeparator');
if (!$input.length) return;
$input.off('.rmSignaturePreview').on('input.rmSignaturePreview change.rmSignaturePreview', function () {
updateSignatureSeparatorPreview($(this).val());
});
updateSignatureSeparatorPreview($input.val());
}
function buildMenuTitlePresetButtonsHtml() {
return joinHtml([
'<div class="rmSettingsMenuPresetWrap">',
'<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>',
'<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">',
getMenuTitlePresetOptions().map(function (option) {
return joinHtml([
'<button type="button" class="rmSettingsMenuPresetBtn" data-rm-menu-preset="',
option.value,
'" aria-pressed="false">',
escapeHtml(option.label),
'</button>'
]);
}).join(''),
'</div>',
'</div>'
]);
}
function applyMenuTitlePresetControls(presetValue) {
var preset = isMenuTitlePresetValue(presetValue) ? presetValue : '';
var $bar = $('#rmSettingsMenuPresetBar');
var $input = $('#rmSettingsMenuTitle');
var forcePresetOnly = isMenuTitlePresetOnlySkin();
if (!$bar.length || !$input.length) return;
$bar.data('rmPreset', preset);
$bar.find('.rmSettingsMenuPresetBtn').each(function () {
var isActive = $(this).data('rmMenuPreset') === preset;
$(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false');
});
$input.prop('disabled', forcePresetOnly || !!preset);
}
function bindMenuTitlePresetControls() {
var $bar = $('#rmSettingsMenuPresetBar');
var $input = $('#rmSettingsMenuTitle');
if (!$bar.length || !$input.length) return;
$input.off('.rmSettingsMenuPreset').on('input.rmSettingsMenuPreset', function () {
if (!$bar.data('rmPreset')) $input.data('rmCustomValue', $input.val());
});
$bar.off('.rmSettingsMenuPreset').on('click.rmSettingsMenuPreset', '.rmSettingsMenuPresetBtn', function () {
var preset = $(this).data('rmMenuPreset');
var currentPreset = $bar.data('rmPreset') || '';
if (currentPreset === preset) {
if (isMenuTitlePresetOnlySkin()) return;
applyMenuTitlePresetControls('');
$input.val($input.data('rmCustomValue') || '');
$input.trigger('focus');
return;
}
$input.data('rmCustomValue', $input.val());
applyMenuTitlePresetControls(preset);
});
}
function fillSettingsFormValues(settings) {
var data = normalizeRemoverSettings(settings);
var forcePresetOnly = isMenuTitlePresetOnlySkin();
var menuTitleValue = data.menuTitle || '';
var storedMenuTitleValue = menuTitleValue;
if (forcePresetOnly && !isAvailableMenuTitlePresetValue(menuTitleValue)) menuTitleValue = getDefaultMenuTitlePreset();
var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue);
$('#rmSettingsForm').data('rmStoredMenuTitle', storedMenuTitleValue || '');
$('#rmSettingsNotifyAuthor').prop('checked', !!data.notifyAuthor);
$('#rmSettingsSubscribeTopic').prop('checked', !!data.subscribeTopic);
$('#rmSettingsShowMenuIcons').prop('checked', !!data.showMenuIcons);
$('#rmSettingsMenuTitle').val(customMenuTitle || '').data('rmCustomValue', customMenuTitle || '');
$('#rmSettingsSignatureSeparator').val(data.signatureSeparator || '');
$('#rmSettingsExcludedNamespaces').val((data.excludedNamespaces || []).join(', '));
$('#rmSettingsDisabledItems').val((data.disabledItems || []).join(', '));
setQuickPhraseEditorState(data.quickPhrases || [], -1);
$('#rmSettingsQuickPhraseInput').val('').removeClass('is-editing');
clearQuickPhraseDropState();
applyMenuTitlePresetControls(menuTitleValue);
updateSignatureSeparatorPreview(data.signatureSeparator || '');
}
function collectSettingsFormValues(options) {
var opts = options || {};
var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val());
var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val());
var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset');
var forcePresetOnly = isMenuTitlePresetOnlySkin();
var storedMenuTitle = $('#rmSettingsForm').data('rmStoredMenuTitle');
if (namespaces.invalid.length) {
return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' };
}
if (disabledItems.invalid.length) {
return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' };
}
if (!opts.skipQuickPhraseCommit) saveQuickPhraseInput();
return {
value: normalizeRemoverSettings({
notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'),
subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'),
showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'),
menuTitle: forcePresetOnly
? (shouldPreserveStoredMenuTitleOnSave() && typeof storedMenuTitle === 'string'
? storedMenuTitle
: (isAvailableMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : getDefaultMenuTitlePreset()))
: (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()),
signatureSeparator: $('#rmSettingsSignatureSeparator').val(),
excludedNamespaces: namespaces.values,
disabledItems: disabledItems.values,
quickPhrases: opts.skipQuickPhraseCommit ? collectQuickPhraseValuesSnapshot() : collectQuickPhraseValues()
})
};
}
function updateSettingsSubmitReadyState(baselineSettings) {
var collected = collectSettingsFormValues({ skipQuickPhraseCommit: true });
var hasChanges = !!(collected.error || (collected.value && !areRemoverSettingsEqual(collected.value, baselineSettings)));
$('#removerSubmit').toggleClass('rmSubmitReady', hasChanges && !$('#removerSubmit').hasClass('rmSubmitError'));
$('#rmSettingsUnsavedHint').css('display', hasChanges ? 'inline-block' : 'none');
}
function bindSettingsSubmitReadyState(baselineSettings) {
var update = function () {
setTimeout(function () { updateSettingsSubmitReadyState(baselineSettings); }, 0);
};
$('#rmSettingsForm').off('.rmSettingsReady').on('input.rmSettingsReady change.rmSettingsReady', 'input, textarea, select', update);
$('#removerModalContent').off('click.rmSettingsReady keyup.rmSettingsReady').on(
'click.rmSettingsReady keyup.rmSettingsReady',
'.rmSettingsMenuPresetBtn,.rmQuickPhraseEditBtn,.rmQuickPhraseRemoveBtn,.rmQuickPhraseChip,#rmSettingsQuickPhraseInput',
update
);
$('#rmSettingsQuickPhrasesEditor').off('rmQuickPhrasesChanged.rmSettingsReady').on('rmQuickPhrasesChanged.rmSettingsReady', update);
updateSettingsSubmitReadyState(baselineSettings);
}
function buildSettingsFormHtml(menuLabelsHint) {
var menuFields =
buildSettingsFieldHtml('Заголовок отдельного меню',
'<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(),
getMenuTitlePresetHintText(), { forId: 'rmSettingsMenuTitle' }) +
buildSettingsFieldHtml('Визуальное оформление меню',
'<div class="rmSettingsChecks">' + buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи в пунктах меню') + '</div>');
var messageFields =
buildSettingsFieldHtml('Префикс перед подписью',
'<input id="rmSettingsSignatureSeparator" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Добавляется перед подписью в публикуемых сообщениях.<span style="display:block;margin-top:6px;">Предпросмотр: <code id="rmSettingsSignaturePreviewCode"></code>.</span>',
{ forId: 'rmSettingsSignatureSeparator' }) +
buildSettingsFieldHtml('Часто используемые фразы', buildQuickPhrasesSettingsEditorHtml(),
'Enter для добавления. Порядок элементов изменяется перетаскиванием.', { forId: 'rmSettingsQuickPhraseInput' });
var defaultFields = '<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') +
buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') + '</div>';
var disableFields =
buildSettingsFieldHtml('Скрыть пункты меню',
'<input id="rmSettingsDisabledItems" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Названия пунктов через запятую, например <code>КБУ, КУЛ, КОБ</code>.' + menuLabelsHint,
{ forId: 'rmSettingsDisabledItems' }) +
buildSettingsFieldHtml('Не показывать в пространствах имён',
'<input id="rmSettingsExcludedNamespaces" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Номера пространств имён через запятую, например <code>2, 10, 828</code>. См. <a href="' + getPageUrl('Википедия:Пространства имён') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Пространства имён</a>.',
{ forId: 'rmSettingsExcludedNamespaces' });
return joinHtml([
'<div id="rmSettingsForm" style="max-width:100%;">',
'<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>',
buildSettingsSectionHtml('Меню', menuFields, 'Настройки внешнего вида и состава меню Remover.'),
buildSettingsSectionHtml('Оформление сообщений', messageFields, 'Настройки оформления публикуемых сообщений в номинациях.'),
buildSettingsSectionHtml('Опции по умолчанию', defaultFields, 'Регулирует изначальное состояние галочек.'),
buildSettingsSectionHtml('Отключение', disableFields, 'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'),
'</div>'
]);
}
function buildSettingsFooterLeftHtml() {
return joinHtml([
'<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;margin-right:auto;">',
'<button id="rmSettingsResetFooter" type="button" title="Удаляет сохранённые настройки Remover из вашего профиля MediaWiki и возвращает значения по умолчанию." style="', stCancel, '">Сбросить все настройки</button>',
'<a id="rmSettingsReportIssue" href="', getPageUrl('Обсуждение участника:Solidest/Remover'), '" target="_blank" rel="noopener noreferrer" ',
'title="Сообщить о проблеме или предложить улучшение" aria-label="Сообщить о проблеме или предложить улучшение" ',
'class="removerModalLink rmButtonLikeLink" style="', stCancel, 'display:inline-flex;align-items:center;justify-content:center;text-align:center;text-decoration:none;box-sizing:border-box;max-width:100%;line-height:1.2;word-break:normal;overflow-wrap:normal;">Обратная связь</a>',
'</div>'
]);
}
function openSettings() {
var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults);
var menuLabelsHint = buildSettingsMenuItemsHint();
var $previousModal = $('#removerModal').length ? $('#removerModal').detach() : $();
var previousLayoutSyncHandlers = modalLayoutSyncHandlers.slice();
function restorePreviousModal() {
closeModal();
if ($previousModal.length) {
$('#content').prepend($previousModal);
modalLayoutSyncHandlers = previousLayoutSyncHandlers.slice();
if (modalLayoutSyncHandlers.length) $(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
syncModalLayout();
syncLinkWidths();
}
}
createModal({
title: 'Конфигурация',
width: 'compact',
showSettingsButton: false
});
$('#removerModal').addClass('rmModalSettings');
$('#removerModalHeaderBar').append(buildHeaderIconButtonHtml('rmSettingsBack', 'Назад', 'Назад', '←'));
$('#rmSettingsBack').on('click', restorePreviousModal);
$('#removerModalContent').html(buildSettingsFormHtml(menuLabelsHint));
fillSettingsFormValues(currentSettings);
bindMenuTitlePresetControls();
bindSignatureSeparatorPreview();
bindQuickPhrasesEditor();
renderModalFooter('submit', {
showCheckbox: false,
submitText: 'Сохранить',
onSubmit: function () {
var collected = collectSettingsFormValues();
var shouldReset;
var saveFn;
if (collected.error) {
alert(collected.error);
return false;
}
shouldReset = areRemoverSettingsEqual(collected.value, settingsDefaults);
saveFn = shouldReset
? function (callback) { resetSettingsOnServer(callback); }
: function (callback) { saveSettingsToServer(collected.value, callback); };
saveFn(function (err) {
if (err) {
alert('Не удалось ' + (shouldReset ? 'сбросить' : 'сохранить') + ' настройки: ' + (err.info || err.code || 'неизвестная ошибка') + '.');
unlockModalSubmit();
return;
}
location.reload();
});
}
});
var $settingsActions = $('#rmFooterActionButtons');
$settingsActions.wrapInner('<div id="rmSettingsActionButtonsRow"></div>');
$settingsActions.append('<span id="rmSettingsUnsavedHint" role="status" aria-live="polite">Есть несохранённые изменения</span>');
bindSettingsSubmitReadyState(currentSettings);
$('#rmFooterButtons').css('justify-content', 'space-between').prepend(buildSettingsFooterLeftHtml());
$('#rmSettingsResetFooter').on('click', function () {
fillSettingsFormValues(settingsDefaults);
updateSettingsSubmitReadyState(currentSettings);
$('#removerSubmit').trigger('focus');
});
}
// ─── Завершение обработки ────────────────────────────────────────────────
function finalizeSuccess(nominationInfo, usePageReload) {
if (isError) {
var $box = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent');
$box.append('<p class="error">При выполнении скрипта произошли ошибки.</p>');
markSubmitError();
return;
}
renderModalFooter('reload');
if (nominationInfo && nominationInfo.pageTitle) {
appendNominationLink(nominationInfo.pageTitle, nominationInfo.sectionTitle);
}
if (!usePageReload && !nominationInfo) location.reload();
}
function finalizeFastRemoval(notifiedPages, summary) {
if (isError || !setAlert || !notifiedPages || !notifiedPages.length) {
finalizeSuccess(null, false);
return;
}
notifyAuthorsForPages(notifiedPages, {
summary: summary,
actionText: 'к быстрому удалению'
}, function () {
finalizeSuccess(null, false);
});
}
// ─── Общий runner ────────────────────────────────────────────────────────
/**
* Универсальный запуск полного пайплайна номинации.
* @param {Object} o
* templateStep — функция (next) → обработка шаблонов на статьях
* nominationStep — функция (done) → публикация номинации, done(err, {pageTitle, sectionTitle})
* notifyStep — функция (nominationInfo, next)
* skipNotify — boolean
* skipLink — boolean, не показывать ссылку на номинацию
*/
function runFlow(o) {
runNominationPipeline({
templateStep: o.templateStep,
nominationStep: o.nominationStep,
notifyStep: o.notifyStep || function (info, next) { next(); },
skipNotify: o.skipNotify,
onSuccess: function (ctx) {
if (isError) { markSubmitError(); return; }
renderModalFooter('reload');
if (!o.skipLink && ctx.nominationInfo && ctx.nominationInfo.pageTitle) {
appendNominationLink(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle);
}
},
onFailure: function () { markSubmitError(); }
});
}
// ═══════════════════════════════════════════════════════════════════════════
// ЯДРО: обработка статей (apply template + nomination page)
// ═══════════════════════════════════════════════════════════════════════════
/**
* Применяет шаблон к одной статье/категории.
* Понимает режим inArticle (вставка через <noinclude>),
* режим closeAction (снятие шаблона + запись на СО),
* режим cleanupAction (снятие КБУ/КУЛ).
*
* @param {string} pg — название страницы
* @param {Object} job — параметры задания (см. buildJob)
* @param {function} callback(err, meta)
*/
function applyTemplateToPage(pg, job, callback) {
var mode = job.mode;
// ── Снятие КБУ/КУЛ ──────────────────────────────────────────────────
if (mode === 'cleanup') {
var tm = job.transferMode || 'none';
if (tm === 'none') { callback({ code: 'error', info: 'Не выбран режим снятия шаблонов.' }); return; }
editPageContent(pg, { summary: job.summary, watchlist: 'nochange', readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var local = removeTransferTemplatesLocal(article, tm);
removeTransferTemplatesWithApiFallback(pg, local.text, tm, local, function (updated) {
if (updated.text === article) { done({ error: { code: 'error', info: 'Шаблоны для снятия не найдены.' } }); return; }
done({ text: updated.text });
});
}, function (err) { callback(err); });
return;
}
// ── Подведение итогов по КУ/КПМ (снятие + итог на СО) ───────────────
if (mode === 'denom') {
getTextWithTimestamp(pg, function (article, baseTimestamp, readErr) {
if (readErr) { callback(makeReadError(readErr, 'read_failed', 'Не удалось получить содержимое страницы «' + pg + '».')); return; }
if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; }
if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; }
var tplPattern = job.sourceTemplate.split('|').map(function (alias) {
return escapeRegExp(alias.trim()).replace(/\s+/g, '[ _]*');
}).join('|');
var tpl = findTemplateByPattern(article, tplPattern);
if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; }
var normalizedTplDate = convertToStandardDate(tpl.params[0]);
var tplExtra = tpl.params.slice(1).join('|').trim();
if (!RE_DATE_ISO.test(normalizedTplDate)) {
callback({ code: 'error', info: 'Не удалось распознать дату в шаблоне: «' + (tpl.params[0] || '') + '».' });
return;
}
var date = getDate(normalizedTplDate);
var nomPlace = 'ВП:' + job.sourceTemplate.replace(/\|.*/, '') + '/' + date[1];
var retTalkSection = '';
var sectionNW, tplpar, newTalkTpl;
if (job.closeType === 'doneRnm') { sectionNW = job.oldTitle + ' → ' + pg; tplpar = job.oldTitle + '|' + pg; }
if (job.closeType === 'noRnm') { sectionNW = pg + ' → ' + tplExtra; tplpar = pg + '|' + tplExtra; }
if (job.closeType === 'ret' || job.closeType === 'retConditional') {
retTalkSection = tplExtra;
sectionNW = retTalkSection || pg;
tplpar = retTalkSection ? ('l1=' + retTalkSection) : '';
}
var editSummary = makeSummary('номинация [[' + (nomPlace ? nomPlace + '#' : '') + sectionNW + ']] — ' + job.resultTemplate);
var talkTitle = getTalkPage(pg);
newTalkTpl = (job.closeType === 'retConditional')
? buildConditionalRetTemplateText(date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline, 1)
: (T_OPEN + job.resultTemplate + '|' + date[0] + '|' + tplpar + T_CLOSE);
getTextWithTimestamp(talkTitle, function (talkText, talkTimestamp, talkReadErr) {
if (talkReadErr) { callback(makeReadError(talkReadErr, 'talk_read_failed', 'Не удалось получить содержимое СО страницы «' + pg + '».')); return; }
var sourceTalkText = talkText || '';
var talkResult = (job.closeType === 'ret')
? upsertRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection)
: (job.closeType === 'retConditional')
? upsertConditionalRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline)
: { text: insertTplOnTalkPage(sourceTalkText, newTalkTpl, '\n'), status: 'created' };
function saveArticle() {
var cleaned = stripTemplatesByPattern(article, tplPattern).text;
var ep = { title: pg, text: cleaned, summary: editSummary, watchlist: 'nochange', assertuser: mwCfg.wgUserName };
if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (t) {
var editErr = t && t.error ? t.error : null;
callback(editErr, editErr ? null : {
discussionPage: nomPlace,
discussionSection: sectionNW,
summary: editSummary
});
});
}
if (talkResult.text === sourceTalkText) { saveArticle(); return; }
var talkEp = { title: talkTitle, text: talkResult.text, summary: editSummary };
if (talkTimestamp) talkEp.basetimestamp = talkTimestamp;
apiReq(talkEp, 'edit', function (talkResp) {
if (talkResp && talkResp.error) { callback({ code: 'talk_failed', info: 'Не удалось записать итог на СО: ' + talkResp.error.info }); return; }
saveArticle();
});
});
});
return;
}
// ── Обычная номинация: вставка шаблона в статью ─────────────────────
// mode === 'nominate'
var isKu = job.opId === 'tRm' || job.opId === 'mRm';
editPageContent(pg, { summary: job.summary, readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var hasExistingKu = isKu && RE_KU_ON_PAGE.test(article);
var conflictDecision = getConflictDecisionForPage(job, pg);
function buildResult(finalText) {
var generatedTpl = buildGeneratedNominationTemplateText(job, pg);
return { text: generatedTpl ? wrapInNoinclude(finalText, generatedTpl) : finalText };
}
function finishConflictResolution(sourceText) {
var resolvedText;
var pageLink = buildQuotedStatusPageLink(pg);
if (conflictDecision.templateAction === 'keep') {
if (sourceText !== article) {
return {
text: sourceText,
meta: { successMessage: 'Шаблон на странице ' + pageLink + ' оставлен без изменений; шаблоны переноса сняты.' }
};
}
return { skip: true, meta: { successMessage: 'Шаблон на странице ' + pageLink + ' оставлен без изменений.' } };
}
resolvedText = applyConflictTemplateResolution(sourceText, job, pg, conflictDecision);
return {
text: resolvedText,
meta: {
successMessage: conflictDecision.templateAction === 'overwrite'
? 'Шаблон КУ на странице ' + pageLink + ' перезаписан новой датой.'
: 'Новый шаблон КУ добавлен сверху на странице ' + pageLink + '.'
}
};
}
if (hasExistingKu && (!conflictDecision || conflictDecision.pageAction !== 'keep')) {
return { error: { code: 'error', info: 'На странице уже стоит шаблон КУ.' } };
}
if (hasExistingKu && conflictDecision && conflictDecision.pageAction === 'keep') {
if (job.transferMode && job.transferMode !== 'none') {
var localConflict = removeTransferTemplatesLocal(article, job.transferMode);
removeTransferTemplatesWithApiFallback(pg, localConflict.text, job.transferMode, localConflict, function (updated) {
done(finishConflictResolution(updated.text));
});
return;
}
return finishConflictResolution(article);
}
if (isKu && job.transferMode && job.transferMode !== 'none') {
var local = removeTransferTemplatesLocal(article, job.transferMode);
removeTransferTemplatesWithApiFallback(pg, local.text, job.transferMode, local, function (updated) { done(buildResult(updated.text)); });
return;
}
return buildResult(article);
},
function (err) { callback(err); }
);
}
/**
* Обрабатывает список страниц последовательно.
* @param {string[]} pages
* @param {Object} job
* @param {function} onDone(notifiedPages, err, pageMeta)
*/
function processPageList(pages, job, onDone) {
var notifiedPages = [];
var pageMeta = {};
eachSequential(pages.slice().reverse(), function (pg, nextPage) {
var pageLink = buildQuotedStatusPageLink(pg);
var statusId = logStatus('Обрабатывается страница ' + pageLink + '...', null, { pending: true, trackError: false });
applyTemplateToPage(pg, job, function (err, meta) {
var normPg = normTitle(pg);
var isClose = job.mode === 'cleanup' || job.mode === 'denom';
if (!isClose) {
if (!err && meta && meta.successMessage) logStatus(meta.successMessage, null, { statusId: statusId, trackError: false });
else logPageEdit(pg, err, { statusId: statusId });
} else {
if (err) { logStatus('Завершение по странице ' + pageLink, err, { statusId: statusId }); }
else {
logStatus('Шаблон снят со страницы ' + pageLink + '.', null, { statusId: statusId, trackError: false });
if (job.mode === 'denom') logStatus('Шаблон установлен на СО страницы ' + pageLink + '.', null, { trackError: false });
}
}
if (!err) {
notifiedPages.push(pg);
if (meta) pageMeta[normPg] = meta;
}
nextPage(err || null);
});
}, function (err) { onDone(notifiedPages, err, pageMeta); });
}
// ═══════════════════════════════════════════════════════════════════════════
// ПОСТРОЕНИЕ JOB из формы
// ═══════════════════════════════════════════════════════════════════════════
/**
* Строит объект job из данных формы для операции номинации (tRm, rnm, imp, merge, split, recov).
* @param {Object} op — запись из OPERATIONS
* @param {string} pg — целевая страница (уже разрешённая)
* @param {boolean} isMulti — режим мультиноминации
* @returns {Object|false} — job или false при ошибке ввода
*/
function buildNominationJob(op, pg, isMulti) {
var nom = op.nomination;
var date = getDate();
var msg = normalizeQuickPhraseValue($('#rmMsg').val());
var rawMsg = msg;
var opId = isMulti ? 'mRm' : op.id;
var tplpar = '';
var section, sectionNW, extraPages, multiArticles = [];
var multiHeaderText = '';
var multiArticleComments = {};
// Вычислить section и tplpar в зависимости от типа дополнительного ввода
if (nom.extraInput) {
var ei = nom.extraInput;
if (ei.type === 'rename') {
var rn = collectInputValues('.rmRenameInput');
if (!rn.length) { alert('Укажите новое название.'); return false; }
tplpar = rn[0] + (rn.length > 1 ? '||' + rn.slice(1).join('|') : '');
section = '[[:' + pg + ']] → ' + rn.map(function (n) { return '[[:' + n + ']]'; }).join(', ');
} else if (ei.type === 'merge') {
var mn = collectInputValues('.rmMergeInput');
if (!mn.length) { alert('Укажите статью для объединения.'); return false; }
tplpar = pg + '|' + mn.join('|');
extraPages = mn;
section = formatPagesWithAnd([pg].concat(mn));
} else if (ei.type === 'split') {
var sn = collectInputValues('.rmSplitInput');
if (!sn.length) { alert('Укажите статьи для разделения.'); return false; }
tplpar = formatPagesWithAnd(sn);
section = '[[:' + pg + ']] → ' + tplpar;
}
}
if (isMulti) {
var ttl = $('#rmHeader').val() || '';
var articles = collectInputValues('.rmArticleInput');
var multiFormat = $('.rmArticleMultiFormatBtn.is-active').data('rmMultiFormat') || 'sections';
multiArticleComments = collectMultiNominationComments();
multiHeaderText = ttl;
multiArticles = articles.slice();
section = ttl;
msg = multiFormat === 'list'
? buildMultiNominationListText(articles, rawMsg, multiArticleComments)
: buildMultiNominationText(articles, rawMsg, multiArticleComments);
}
if (!section) section = '[[:' + pg + ']]';
sectionNW = section.replace(/\[\[:/g, '').replace(/]]/g, '');
var nomPageDate = date[1];
var nomPage = nom.nomPage(nomPageDate);
var summary = makeSummary('номинация [[' + nomPage + '#' + sectionNW + ']]');
return {
mode: 'nominate',
opId: opId,
op: op,
date: date,
tplpar: tplpar,
articleTpl: nom.articleTpl || function () { return ''; },
inArticle: nom.inArticle !== false,
transferMode: (nom.supportsTransfer ? getTransferModeFromButtons() : 'none'),
summary: summary,
msg: msg,
nomPage: nomPage,
navTemplate: nom.navTemplate,
section: section,
sectionNW: sectionNW,
comment: nom.comment || '',
extraPages: extraPages || [],
isMulti: !!isMulti,
multiHeaderText: multiHeaderText,
multiNominationBody: rawMsg,
multiArticleComments: multiArticleComments,
multiNominationFormat: multiFormat || 'sections',
multiArticles: multiArticles,
pages: isMulti ? multiArticles.slice().reverse() : ([pg].concat(extraPages || []))
};
}
function getTransferModeFromButtons() {
var kbu = $('#rmTransferBtnKbu').hasClass('is-active');
var kul = $('#rmTransferBtnKul').hasClass('is-active');
if (kbu && kul) return 'both';
if (kbu) return 'kbu';
if (kul) return 'kul';
return 'none';
}
function buildKbuFormHtml(reasons) {
return joinHtml([
'<select id="rmSel" style="', stInputFull, '">',
reasons.map(function (r, i) { return '<option value="' + i + '">' + r[1] + '</option>'; }).join(''),
'</select>',
'<input id="fiRm" type="hidden" style="', stInputFull, '">',
'<input id="fiRmComment" type="text" placeholder="Комментарий (необязательно)" style="', stInputFull, '">',
buildQuickPhrasesPanelHtml('fiRmComment')
]);
}
function buildNominationMultiHeaderHtml(pg, options) {
var opts = options || {};
var multiHeaderRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
return joinHtml([
'<div id="rmMultiHeader" class="', RESIZE_CLASS, '" style="display:none;margin-bottom:6px;">',
'<div class="rmArticleRow rmNominationHeaderRow" style="', multiHeaderRowStyle, '"><input id="rmHeader" type="text" placeholder="Заголовок номинации" style="', stInputBox, '">',
buildAddArticleButtonHtml(opts), '</div>',
'</div>',
'<div id="rmArticlesContainer" style="display:flex;flex-direction:column;gap:', multiNominationGap, ';margin-bottom:', multiNominationGap, ';">',
buildMultiArticleRowHtml(0, $.extend({}, opts, { rowId: 'rmFirstArticle', articleValue: pg, showAdd: true })),
'</div>'
]);
}
function buildTransferBoxHtml() {
return joinHtml([
'<div id="rmTransferBox" class="', RESIZE_CLASS, ' rmTransferPanel" style="width:100%;box-sizing:border-box;"><div class="rmTransferGrid">',
'<div id="rmTransferModeSingle" class="rmSegmentedBar"><button type="button" id="rmTransferBtnNone" class="rmSegmentedBtn rmToggleBtn is-active" aria-pressed="true">Обычная номинация</button></div>',
'<div id="rmTransferModeGroup" class="rmSegmentedBar">',
'<button type="button" id="rmTransferBtnKbu" class="rmSegmentedBtn rmToggleBtn" aria-pressed="false" title="Шаблоны db-*, уд-*, КОУ, Hangon.">Снять КБУ</button>',
'<button type="button" id="rmTransferBtnKul" class="rmSegmentedBtn rmToggleBtn" aria-pressed="false" title="Шаблон «К улучшению».">Снять КУЛ</button>',
'</div>',
'<div class="rmTransferHintRow"><div id="rmTransferHint" style="display:none;font-size:12px;line-height:1.35;color:', tk.cSubM, ';"></div></div>',
'</div></div>'
]);
}
function buildMultiNominationFormatSwitchHtml(wrapId, buttonClass) {
return joinHtml([
'<div id="', wrapId, '" class="', RESIZE_CLASS, ' rmProtectControlGroup" style="display:none;">',
'<div class="rmProtectControlLabel">Формат обсуждения</div>',
'<div class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn ', buttonClass, ' is-active" data-rm-multi-format="sections" aria-pressed="true">Подразделами</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn ', buttonClass, '" data-rm-multi-format="list" aria-pressed="false">Списком</button>',
'</div>',
'</div>'
]);
}
function buildNominationFormHtml(nom, pg, multiMode) {
return joinHtml([
multiMode ? buildNominationMultiHeaderHtml(pg) : '',
multiMode ? buildMultiNominationFormatSwitchHtml('rmArticleMultiFormatWrap', 'rmArticleMultiFormatBtn') : '',
nom.extraInput ? buildMultiInputHtml(nom.extraInput) : '',
'<textarea id="rmMsg" placeholder="Текст номинации без подписи"></textarea>',
buildQuickPhrasesPanelHtml('rmMsg'),
nom.supportsTransfer ? buildTransferBoxHtml() : ''
]);
}
function buildCategoryNominationFormHtml(variantConfig, multiMode, pageName) {
return joinHtml([
multiMode ? buildNominationMultiHeaderHtml(pageName, {
inputPlaceholder: 'Категория',
addTitle: 'Добавить категорию',
removeTitle: 'Удалить категорию',
commentPlaceholder: 'Комментарий только для этой категории (необязательно)'
}) : '',
multiMode ? buildMultiNominationFormatSwitchHtml('rmCategoryMultiFormatWrap', 'rmCategoryMultiFormatBtn') : '',
variantConfig ? buildMultiInputHtml(variantConfig) : '',
'<textarea id="nominationReason" placeholder="Текст номинации без подписи"></textarea>',
buildQuickPhrasesPanelHtml('nominationReason')
]);
}
function ensureMultiPageCommentTextareaResizer(textareaId, wrapId) {
var $textarea = $('#' + textareaId);
if (!$textarea.length || $textarea.data('rmNestedResizerReady')) return;
setupNestedResizableTextarea(textareaId, wrapId, parseInt(sz.taMinW, 10) || 180, 90);
$textarea.data('rmNestedResizerReady', true);
}
function setMultiPageCommentExpanded($btn, expanded) {
var wrapId = $btn.data('rmCommentWrap');
var textareaId = $btn.data('rmCommentTextarea');
var $wrap = $('#' + wrapId);
if (!$btn.length || !$wrap.length) return;
$btn.attr('aria-expanded', expanded ? 'true' : 'false')
.toggleClass('is-active', expanded)
.text(expanded ? 'Скрыть комментарий' : 'Комментарий');
$wrap.toggle(expanded);
if (expanded) ensureMultiPageCommentTextareaResizer(textareaId, wrapId);
}
function getMultiPageCommentTargets($block) {
var $textarea = $block.find('.rmArticleCommentInput').first();
var $wrap = $textarea.parent();
return {
wrapId: $wrap.attr('id') || '',
textareaId: $textarea.attr('id') || ''
};
}
function setMultiPageRowControls($block, showAdd, showComment, options) {
var opts = options || {};
var $row = $block.find('.rmArticleRow').first();
var ids = getMultiPageCommentTargets($block);
var $commentBtn = $row.find('.rmArticleCommentToggle');
if (!$row.length) return;
if (showAdd) {
if ($commentBtn.length) setMultiPageCommentExpanded($commentBtn, false);
if (ids.wrapId) $('#' + ids.wrapId).hide();
$row.find('.rmArticleCommentToggle,.rmRemoveInput').remove();
if (!$row.find('.rmAddArticle').length) $row.append(buildAddArticleButtonHtml(opts));
return;
}
$row.find('.rmAddArticle').remove();
if (!$row.find('.rmArticleCommentToggle').length) {
$row.append(buildMultiArticleButtonsHtml(ids.wrapId, ids.textareaId, $.extend({}, opts, { showComment: showComment })));
return;
}
$row.find('.rmArticleCommentToggle').toggle(showComment);
}
function setupMultiPageNominationUi(options) {
var opts = options || {};
var containerSelector = opts.containerSelector || '#rmArticlesContainer';
var pageCounter = parseInt(opts.nextIndex, 10) || 1;
var wasMultiModeExpanded = false;
function restoreEmptySinglePageInput() {
var $pageInput = $(containerSelector + ' .rmArticleInput').first();
if (!$pageInput.length || String($pageInput.val() || '').trim()) return;
$pageInput.val(opts.defaultPage || '');
}
function updateMultiMode() {
var $blocks = $(containerSelector + ' .rmMultiArticleBlock');
var hasExtra = $blocks.length > 1;
$('#rmMultiHeader').toggle(hasExtra);
if (opts.multiOnlySelector) $(opts.multiOnlySelector).toggle(hasExtra);
if (!hasExtra && wasMultiModeExpanded) restoreEmptySinglePageInput();
$blocks.each(function (index) {
setMultiPageRowControls($(this), !hasExtra && index === 0, hasExtra, opts);
});
wasMultiModeExpanded = hasExtra;
syncModalLayout();
}
$(document).off('click.rmMultiPageAdd').on('click.rmMultiPageAdd', '.rmAddArticle', function () {
$(containerSelector).append(buildMultiArticleRowHtml(pageCounter++, opts));
updateMultiMode();
});
$(document).off('click.rmMultiPageComment').on('click.rmMultiPageComment', '.rmArticleCommentToggle', function () {
var $btn = $(this);
if (!$btn.is(':visible')) return;
setMultiPageCommentExpanded($btn, $btn.attr('aria-expanded') !== 'true');
syncModalLayout();
});
$(document).off('click.rmMultiPageRemove').on('click.rmMultiPageRemove', '.rmArticleRow .rmRemoveInput', function () {
$(this).closest('.rmMultiArticleBlock').remove();
updateMultiMode();
});
updateMultiMode();
return {
update: updateMultiMode,
isMulti: function () { return $(containerSelector + ' .rmMultiArticleBlock').length > 1; }
};
}
function bindMultiNominationFormatSwitch(rootSelector, buttonSelector) {
$(rootSelector).off('click.rmMultiFormat', buttonSelector).on('click.rmMultiFormat', buttonSelector, function () {
var $btn = $(this);
$(buttonSelector).removeClass('is-active').attr('aria-pressed', 'false');
$btn.addClass('is-active').attr('aria-pressed', 'true');
});
}
function buildProtectAddButtonHtml() {
return buildSquareAddButtonHtml('', 'Добавить страницу', 'rmProtectAddPage');
}
function buildProtectPageRowHtml(id, pageName, isFirstRow) {
return joinHtml([
'<div', isFirstRow ? ' id="rmProtectFirstRow"' : '', ' class="rmProtectPageRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="', id, '" type="text" placeholder="Страница" class="rmProtectPageInput" style="', stInputBox, '"',
pageName ? ' value="' + escapeHtml(pageName) + '"' : '', '>',
isFirstRow
? buildProtectAddButtonHtml()
: '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>',
'</div>'
]);
}
function buildReportFormHtml(ctx, isZka) {
var reportTextPlaceholder = (isZka ? 'Введите текст запроса.' : 'Текст запроса (можно дополнить или изменить).') + ' Подпись будет добавлена автоматически.';
if (isZka) {
return joinHtml([
'<input id="rmReportHeader" type="text" placeholder="Тема/заголовок" style="', stInputFull, '" value="', escapeHtml(ctx.pageLink), '">',
'<textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>',
buildQuickPhrasesPanelHtml('rmReportText')
]);
}
return joinHtml([
'<div id="rmProtectModeBtns" class="', RESIZE_CLASS, '" style="margin-bottom:14px;"><div class="rmSegmentedBar">',
'<button id="rmProtectModeInstall" type="button" class="rmSegmentedBtn rmProtectModeBtn is-active" aria-pressed="true">🛡️ Установить защиту</button>',
'<button id="rmProtectModeRemove" type="button" class="rmSegmentedBtn rmProtectModeBtn" aria-pressed="false">📛 Снять защиту</button>',
'</div></div>',
'<div id="rmProtectMultiWrap" class="', RESIZE_CLASS, '">',
'<div id="rmProtectHeaderWrap" style="display:none;margin-bottom:6px;"><div class="rmProtectHeaderRow" style="', stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'), '"><input id="rmProtectHeader" type="text" placeholder="Заголовок (для нескольких страниц)" style="', stInputBox, '">', buildProtectAddButtonHtml(), '</div></div>',
buildProtectPageRowHtml('rmProtectPage0', ctx.pageName, true),
'<div id="rmProtectPagesContainer"></div>',
'</div>',
'<div id="rmProtectLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmProtectLevels" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>',
'</div></div>',
'<div id="rmProtectReasonsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Причины</div><div id="rmProtectReasons" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="война правок">война правок</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="неконсенсусные изменения">неконсенсусные изменения</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="вандализм">вандализм</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="популярная статья">популярная статья</button>',
'</div></div>',
'<div id="rmRemoveLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup" style="display:none;"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmRemoveLevels" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>',
'</div></div>',
'<div id="rmProtectTextBlock" class="', RESIZE_CLASS, '"><textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>',
buildQuickPhrasesPanelHtml('rmReportText'), '</div>'
]);
}
// ═══════════════════════════════════════════════════════════════════════════
// ОБРАБОТЧИКИ ОПЕРАЦИЙ
// ═══════════════════════════════════════════════════════════════════════════
var handlers = {
// ── КБУ ─────────────────────────────────────────────────────────────
showKbu: function (op) {
var forCategory = !!(op && op.forCategory);
var reasons = getFastRemoveReasons();
createModal({
title: 'Быстрое удаление',
width: 'compact',
subtitleHtml: '<span id="rmKbuCriteriaLinkWrap"></span>'
});
$('#removerModalContent').html(buildKbuFormHtml(reasons));
function updateKbuReasonControls() {
var reason = reasons[$('#rmSel').val()] || reasons[0];
var paramCfg = reason ? cfg.requiredParamTemplates[reason[0]] : null;
var showComment = true;
$('#rmKbuCriteriaLinkWrap').html(buildFastRemoveCriteriaLinkHtml(reason));
if (paramCfg) {
var noComment = paramCfg.charAt(0) === '!';
$('#fiRm').attr({ type: 'text', placeholder: 'Укажите ' + (noComment ? paramCfg.substring(1) : paramCfg) }).show();
showComment = !noComment;
} else {
$('#fiRm').attr('type', 'hidden').hide();
}
$('#fiRmComment').toggle(showComment);
$('.rmQuickPhrasesPanel[data-rm-target="fiRmComment"]').toggle(showComment);
}
$('#rmSel').change(updateKbuReasonControls);
$('#rmSel').trigger('change');
renderModalFooter('submit', {
submitText: 'Номинировать',
onSubmit: function () {
var idx = $('#rmSel').val();
var addInfo = $('#fiRm').val();
var comment = $('#fiRmComment').val();
startProcessing();
if (forCategory) {
var tpl = reasons[idx][0];
var categorySummary = makeSummary('номинация категории на быстрое удаление');
if (addInfo) tpl += '|1=' + addInfo;
if (comment) tpl += '|' + (addInfo ? '2' : '1') + '=' + comment;
editPageContent(mwCfg.wgPageName, { summary: categorySummary, readError: 'Не удалось получить содержимое.' },
function (text) { return { text: wrapInNoinclude(text, T_OPEN + tpl + T_CLOSE) }; },
function (err) {
if (err) {
unlockModalSubmit();
logStatus('Ошибка записи.', err);
} else {
logStatus('Страница номинирована к быстрому удалению.', null, { trackError: false });
finalizeFastRemoval([normTitle(mwCfg.wgPageName)], categorySummary);
}
});
} else {
var job = {
mode: 'nominate', opId: 'fRm',
kbuTemplate: reasons[idx][0], kbuAddInfo: addInfo, kbuComment: comment,
summary: makeSummary('номинация к [[ВП:КБУ|быстрому удалению]]'),
inArticle: true
};
processPageList([normTitle(mwCfg.wgPageName)], job, function (notifiedPages) {
finalizeFastRemoval(notifiedPages, job.summary);
});
}
return true;
}
});
},
// ── Универсальная номинация (КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС) ────────
showNomination: function (op) {
var nom = op.nomination;
var pg = normTitle(mwCfg.wgPageName);
var date = getDate()[1];
var nomPage = nom.nomPage(date);
var multiMode = nom.supportsMulti;
function updateTransferUi() {
var mode = getTransferModeFromButtons();
var isNone = mode === 'none';
var isKbu = mode === 'kbu' || mode === 'both';
var isKul = mode === 'kul' || mode === 'both';
$('#rmTransferBtnNone').toggleClass('is-active', isNone).attr('aria-pressed', isNone ? 'true' : 'false');
$('#rmTransferBtnKbu').toggleClass('is-active', isKbu).attr('aria-pressed', isKbu ? 'true' : 'false');
$('#rmTransferBtnKul').toggleClass('is-active', isKul).attr('aria-pressed', isKul ? 'true' : 'false');
var t = transferTexts[mode];
if (t) { $('#rmTransferHint').text(t.hint).show(); } else { $('#rmTransferHint').hide().text(''); }
applyGeneratedText($('#rmMsg'), t && t.notice ? t.notice + '\n' : '');
}
createModal({
title: 'Номинация: ' + nom.template,
subtitlePage: nomPage,
subtitleLabel: 'Текущий день'
});
$('#removerModalContent').html(buildNominationFormHtml(nom, pg, multiMode));
setupResizableModal('rmMsg');
// Логика переноса
if (nom.supportsTransfer) {
$(document).off('click.rmTransfer').on('click.rmTransfer', '#rmTransferBtnNone,#rmTransferBtnKbu,#rmTransferBtnKul', function () {
if (this.id === 'rmTransferBtnNone') {
$('#rmTransferBtnKbu,#rmTransferBtnKul').removeClass('is-active');
$('#rmTransferBtnNone').addClass('is-active');
} else {
$(this).toggleClass('is-active');
var anyOn = $('#rmTransferBtnKbu').hasClass('is-active') || $('#rmTransferBtnKul').hasClass('is-active');
$('#rmTransferBtnNone').toggleClass('is-active', !anyOn);
}
updateTransferUi();
});
updateTransferUi();
}
// Многостраничный режим
if (multiMode) {
setupMultiPageNominationUi({ defaultPage: pg, multiOnlySelector: '#rmArticleMultiFormatWrap' });
bindMultiNominationFormatSwitch('#removerModalContent', '.rmArticleMultiFormatBtn');
}
if (nom.extraInput) wireMultiInput(nom.extraInput);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var isMulti = multiMode && $('#rmArticlesContainer .rmMultiArticleBlock').length > 1;
var inputVal = !isMulti ? normTitle($('#rmArticlesContainer .rmArticleInput').first().val() || '') : '';
var changed = inputVal && inputVal !== pg;
function executeJob(job) {
startProcessing();
runFlow({
templateStep: function (next) {
if (!job.inArticle) { next(); return; }
processPageList(job.pages, job, function (notifiedPages, err) {
job._notifiedPages = notifiedPages;
next(err);
});
},
nominationStep: function (done) {
publishNomination({
pageTitle: job.nomPage,
navTemplate: job.navTemplate,
sectionTitle: job.section,
summary: job.summary,
text: getNominationPublishText(job)
}, function (err) { done(err, { pageTitle: job.nomPage, sectionTitle: job.section }); });
},
notifyStep: function (nominationInfo, next) {
var pages = job._notifiedPages || [];
if (!setAlert || !pages.length) { next(); return; }
notifyAuthorsForPages(pages, {
summary: job.summary,
actionText: job.comment,
discussionPage: nominationInfo && nominationInfo.pageTitle,
discussionSection: nominationInfo && nominationInfo.sectionTitle
}, next);
},
skipLink: op.id === 'fRm'
});
}
function run(targetPg) {
var job = buildNominationJob(op, targetPg, isMulti);
if (!job) { unlockModalSubmit(); return; }
if (job.isMulti && job.inArticle && getNominationConflictRule(job)) {
startProcessing();
inspectMultiNominationConflicts(job, function (err, conflicts) {
if (err) { markSubmitError(); return; }
if (!conflicts.length) { executeJob(job); return; }
showNominationConflictResolution(job, conflicts, function (resolvedJob) {
executeJob(resolvedJob);
});
});
return;
}
executeJob(job);
}
if (changed) {
apiReq({ prop: 'info', titles: inputVal }, 'query', function (data) {
if (data && data.error) {
unlockModalSubmit();
alert('Не удалось проверить страницу из-за ошибки API. Попробуйте ещё раз.');
return;
}
var page = getFirstQueryPage(data);
if (!page) {
unlockModalSubmit();
alert('Не удалось проверить страницу из-за временной ошибки. Попробуйте ещё раз.');
return;
}
if (page.missing !== undefined) {
unlockModalSubmit();
alert('Страница «' + inputVal + '» не существует.');
return;
}
run(normTitle(page.title || inputVal));
});
} else {
run(pg);
}
return true;
}
});
},
// ── Снятие номинации (статья) ────────────────────────────────────────
showArticleClose: function () {
showCloseActionsModal({
inputName: 'rmCloseAction',
listId: 'rmCloseActions',
emptyText: 'Не найдено подходящих шаблонов для закрытия.',
emptyDetails: 'Проверяются: КУ, КПМ, КБУ, КУЛ.',
getActions: function (articleText) {
var actions = [];
if (RE_KU_ON_PAGE.test(articleText)) actions.push({ id: 'ret', tag: 'КУ', label: 'Оставлено', mode: 'denom', closeType: 'ret', resultTemplate: 'оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{оставлено}} на СО.', comment: 'оставлена', talkNotice: true });
if (RE_KU_ON_PAGE.test(articleText)) actions.push({ id: 'retConditional', tag: 'КУ', label: 'Условно оставлено', mode: 'denom', closeType: 'retConditional', resultTemplate: 'условно оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{условно оставлено}} на СО.', comment: 'условно оставлена', talkNotice: true, needsConditionalFields: true });
if (RE_KPM_ON_PAGE.test(articleText)) actions.push({ id: 'doneRnm', tag: 'КПМ', label: 'Переименовано', mode: 'denom', closeType: 'doneRnm', resultTemplate: 'переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{переименовано}} на СО.', comment: 'переименована', talkNotice: true, needsOldTitle: true });
if (RE_KPM_ON_PAGE.test(articleText)) actions.push({ id: 'noRnm', tag: 'КПМ', label: 'Не переименовано', mode: 'denom', closeType: 'noRnm', resultTemplate: 'не переименовано',sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{не переименовано}} на СО.', comment: 'не переименована',talkNotice: true });
var hasKbu = RE_KBU_ON_PAGE.test(articleText);
var hasKul = RE_KUL_ON_PAGE.test(articleText);
if (hasKbu && hasKul) actions.push({ id: 'cleanup-both', tag: 'КБУ и КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'both', cleanupLabel: 'КБУ и КУЛ', description: 'Снимает шаблоны КБУ/КУЛ и Hangon.' });
if (hasKbu) actions.push({ id: 'cleanup-kbu', tag: 'КБУ', label: 'Снятие', mode: 'cleanup', transferMode: 'kbu', cleanupLabel: 'КБУ', description: 'Снимает шаблоны КБУ и Hangon.' });
if (hasKul) actions.push({ id: 'cleanup-kul', tag: 'КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'kul', cleanupLabel: 'КУЛ', description: 'Снимает шаблон КУЛ.' });
return actions;
},
afterRender: function (actions) {
var hasDoneRnm = actions.some(function (a) { return a.id === 'doneRnm'; });
var hasConditionalRet = actions.some(function (a) { return a.id === 'retConditional'; });
if (hasDoneRnm) {
$('#rmCloseActions input[value="doneRnm"]').closest('.rmActionItem').append(
'<div id="rmCloseOldTitleWrap" style="display:none;margin-top:6px;"><input id="rmCloseOldTitle" type="text" placeholder="Старое название" style="' + stInputFull + '"></div>'
);
}
if (hasConditionalRet) {
$('#rmCloseActions input[value="retConditional"]').closest('.rmActionItem').append(buildConditionalRetFieldsHtml());
}
},
afterFooterRender: function (_, actionMap) {
function ensureConditionalTextareaResizer() {
var $textarea = $('#rmCloseConditionalReason');
if (!$textarea.length || $textarea.data('rmConditionalResizerReady')) return;
setupNestedResizableTextarea('rmCloseConditionalReason', 'rmCloseConditionalWrap', 280, 90);
$textarea.data('rmConditionalResizerReady', true);
}
function updateUi() {
var sel = actionMap[$('[name="rmCloseAction"]:checked').val()];
$('#rmCloseOldTitleWrap').toggle(!!(sel && sel.needsOldTitle));
$('#rmCloseConditionalWrap').toggle(!!(sel && sel.needsConditionalFields));
if (sel && sel.needsConditionalFields) ensureConditionalTextareaResizer();
var disableNotify = !!(sel && sel.mode === 'cleanup' && sel.transferMode === 'kbu');
var $cb = $('[name="rmUAlert"]');
var $cbLabel = $('[name="rmUAlert"]').closest('label');
if ($cb.length) $cb.prop('disabled', disableNotify);
if ($cbLabel.length) $cbLabel.css({
visibility: disableNotify ? 'hidden' : 'visible',
pointerEvents: disableNotify ? 'none' : ''
});
syncModalLayout();
}
$(document).off('change.rmCloseAction').on('change.rmCloseAction', '[name="rmCloseAction"]', updateUi);
updateUi();
},
onSubmit: function (sel, pageName) {
var job;
if (sel.mode === 'denom') {
var oldTitle = sel.needsOldTitle ? ($('#rmCloseOldTitle').val() || '').trim() : '';
var conditionalReason = sel.needsConditionalFields ? normalizeQuickPhraseValue($('#rmCloseConditionalReason').val()) : '';
var conditionalDeadline = sel.needsConditionalFields ? String($('#rmCloseConditionalDeadline').val() || '').trim() : '';
if (sel.needsOldTitle && !oldTitle) { alert('Укажите старое название.'); return false; }
if (conditionalDeadline && normalizeIsoDate(conditionalDeadline) !== conditionalDeadline) {
alert('Укажите срок в формате YYYY-MM-DD, например 2026-05-31.');
return false;
}
job = {
mode: 'denom',
closeType: sel.closeType,
resultTemplate: sel.resultTemplate,
sourceTemplate: sel.sourceTemplate,
oldTitle: oldTitle,
conditionalReason: conditionalReason,
conditionalDeadline: conditionalDeadline,
notifyActionText: sel.comment,
skipNotify: false
};
} else {
job = {
mode: 'cleanup',
transferMode: sel.transferMode,
summary: makeSummary('снятие шаблонов ' + sel.cleanupLabel),
notifyActionText: (sel.transferMode === 'kul' || sel.transferMode === 'both')
? 'больше не номинирована к срочному улучшению'
: '',
skipNotify: !(sel.transferMode === 'kul' || sel.transferMode === 'both')
};
}
processPageList([pageName], job, function (notifiedPages, err, pageMeta) {
function finishClose() {
if (isError) { markSubmitError(); }
else { renderModalFooter('reload'); }
}
if (isError || err || job.skipNotify || !setAlert || !notifiedPages.length) { finishClose(); return; }
var meta = (pageMeta && pageMeta[normTitle(pageName)]) || {};
notifyAuthorsForPages(notifiedPages, {
summary: meta.summary || job.summary,
actionText: job.notifyActionText,
discussionPage: meta.discussionPage,
discussionSection: meta.discussionSection,
includeProposedPrefix: false
}, finishClose);
});
return true;
}
});
},
// ── ОБКАТ: номинация категории ───────────────────────────────────────
showCatNomination: function (op) {
var catType = op.catType;
var titles = { discuss: 'Номинация: обсуждение', deletion: 'Номинация: к удалению', rename: 'Номинация: к переименованию', merge: 'Номинация: к объединению' };
var now = new Date();
var catDiscPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[now.getUTCMonth()] + '_' + now.getUTCFullYear();
var pageName = normalizeCategoryPageName(mwCfg.wgPageName);
var multiMode = catType === 'deletion';
createModal({ title: titles[catType], subtitlePage: catDiscPage, subtitleLabel: 'Текущий месяц' });
var variantCfgs = {
rename: { firstId: 'firstRenameInput', addBtnId: 'addRenameVariant', containerId: 'renameVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Новое название без префикса Категория:', addPh: 'Дополнительный вариант названия' },
merge: { firstId: 'firstMergeInput', addBtnId: 'addMergeVariant', containerId: 'mergeVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Категория для объединения без префикса Категория:', addPh: 'Дополнительный вариант объединения' }
};
var vCfg = variantCfgs[catType];
$('#removerModalContent').html(buildCategoryNominationFormHtml(vCfg, multiMode, pageName));
setupResizableModal('nominationReason');
if (multiMode) {
setupMultiPageNominationUi({
defaultPage: pageName,
inputPlaceholder: 'Категория',
addTitle: 'Добавить категорию',
removeTitle: 'Удалить категорию',
commentPlaceholder: 'Комментарий только для этой категории (необязательно)',
multiOnlySelector: '#rmCategoryMultiFormatWrap'
});
bindMultiNominationFormatSwitch('#removerModalContent', '.rmCategoryMultiFormatBtn');
}
if (vCfg) wireMultiInput(vCfg);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var reason = normalizeQuickPhraseValue($('#nominationReason').val());
var targetPages = multiMode ? collectCategoryPageInputValues('.rmArticleInput') : [pageName];
var isMulti = multiMode && targetPages.length > 1;
var multiFormat = $('.rmCategoryMultiFormatBtn.is-active').data('rmMultiFormat') || 'sections';
var commentsByCategory = isMulti ? collectMultiNominationComments(normalizeCategoryPageName) : {};
var discussionTarget = isMulti ? targetPages : targetPages[0];
var discussionReason = isMulti
? (multiFormat === 'list'
? buildMultiNominationListText(targetPages, reason, commentsByCategory)
: buildMultiNominationText(targetPages, reason, commentsByCategory, { headingLevel: 4 }))
: reason;
var discussionOptions = isMulti ? { headerText: $('#rmHeader').val(), reasonIsPrepared: true } : null;
var notifiedPages = [];
if (!reason) { alert('Пожалуйста, укажите причину/тему.'); return false; }
if (!targetPages.length) { alert('Укажите категорию.'); return false; }
var mainName = null, additionalNames = [];
if (vCfg) {
mainName = $('#' + vCfg.firstId).val().trim();
if (!mainName) { alert('Укажите ' + (catType === 'rename' ? 'новое название' : 'категорию для объединения') + '.'); return false; }
additionalNames = collectInputValues('#' + vCfg.containerId + ' .variantInput');
}
startProcessing();
runFlow({
templateStep: function (next) {
addTemplatesToCategories(targetPages, catType, mainName, additionalNames, function (err, processedPages) {
notifiedPages = processedPages || [];
next(err);
});
},
nominationStep: function (done) {
createCategoryDiscussion(discussionTarget, discussionReason, catType, function (err, nominationInfo) { done(err, nominationInfo || null); }, mainName, additionalNames, discussionOptions);
},
notifyStep: function (nominationInfo, next) {
if (!setAlert || !nominationInfo) { next(); return; }
var section = normalizeSectionForLink(nominationInfo.sectionTitle || '');
notifyAuthorsForPages(notifiedPages.length ? notifiedPages : targetPages, {
summary: makeSummary('номинация [[' + nominationInfo.pageTitle + (section ? '#' + section : '') + ']]'),
actionText: { discuss: 'к обсуждению', deletion: 'к удалению', rename: 'к переименованию', merge: 'к объединению' }[catType] || 'к обсуждению',
discussionPage: nominationInfo.pageTitle,
discussionSection: nominationInfo.sectionTitle
}, next);
}
});
return true;
}
});
},
// ── Снятие номинации (категория) ─────────────────────────────────────
showCatClose: function () {
showCloseActionsModal({
inputName: 'rmCategoryCloseAction',
showCheckbox: false,
emptyText: 'Не найдено подходящих шаблонов для завершения.',
emptyDetails: 'Проверяются ОБКАТ и КУ.',
getActions: function (catText) {
var allObkat = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|');
var actions = [];
if (new RegExp('\\{\\{\\s*(?:' + allObkat + ')\\s*(?:\\||\\}\\})', 'i').test(catText)) {
actions.push({ id: 'cat-obkat-done', tag: 'ОБКАТ', label: 'Завершено', mode: 'obkat', talkTemplate: 'Обсуждавшаяся категория', description: 'Снимает шаблон ОБКАТ, добавляет {{Обсуждавшаяся категория}} на СО.', talkNotice: true });
}
if (RE_KU_ON_PAGE.test(catText)) {
actions.push({ id: 'cat-ku-cleanup', tag: 'КУ', label: 'Снятие', mode: 'cleanup', description: 'Снимает шаблон КУ без записи на СО.' });
}
return actions;
},
onSubmit: function (sel, pageName) {
if (sel.mode === 'obkat') markCategoryDiscussionAsDone(pageName);
if (sel.mode === 'cleanup') removeKuFromCategory(pageName);
return true;
}
});
},
// ── Защита / Запрос к администраторам ───────────────────────────────
showReport: function (op) {
var mode = op.reportMode || 'protect';
var ctx = getReporterContext(mode);
var isZka = mode === 'request';
var protectMode = 'install';
var pageCounter = 1;
function buildProtectText(pm) {
if (pm === 'remove') {
var removeLevels = [];
$('#rmRemoveLevels .rmToggleBtn.is-active').each(function () { removeLevels.push($(this).data('label')); });
return removeLevels.length ? 'Просьба снять ' + removeLevels.join(' и/или ') + '.' : '';
}
var levels = [], reasons = [];
$('#rmProtectLevels .rmToggleBtn.is-active').each(function () { levels.push($(this).data('label')); });
$('#rmProtectReasons .rmToggleBtn.is-active').each(function () { reasons.push($(this).data('label')); });
if (!levels.length && !reasons.length) return '';
var text = 'Просьба установить';
if (levels.length) text += ' ' + levels.join(' и/или ');
if (reasons.length) text += ' по причине: ' + reasons.join(', ');
return text + '.';
}
function applyProtectMode(m) {
protectMode = m;
var isInstall = m === 'install';
$('#rmProtectModeInstall').toggleClass('is-active', isInstall).attr('aria-pressed', isInstall ? 'true' : 'false');
$('#rmProtectModeRemove').toggleClass('is-active', !isInstall).attr('aria-pressed', !isInstall ? 'true' : 'false');
$('#removerModalTitleText').text(isInstall ? 'Запрос на защиту страницы' : 'Запрос на снятие защиты');
var linkPage = isInstall ? 'Википедия:Установка защиты' : 'Википедия:Снятие защиты';
$('#rmProtectLinkWrap').html('<a href="' + getPageUrl(linkPage) + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + escapeHtml(linkPage) + '</a>');
$('#rmProtectLevelsWrap,#rmProtectReasonsWrap').toggle(isInstall);
$('#rmRemoveLevelsWrap').toggle(!isInstall);
$('#rmProtectLevels .rmProtectOptBtn,#rmProtectReasons .rmProtectOptBtn,#rmRemoveLevels .rmProtectOptBtn').removeClass('is-active');
$('#rmReportText').val('').removeData('rmGenerated');
}
function updateProtectMultiUi() {
var $rows = $('#rmProtectMultiWrap .rmProtectPageRow');
var hasExtra = $rows.length > 1;
if (!$rows.length) {
$('#rmProtectPagesContainer').append(buildProtectPageRowHtml('rmProtectPage' + pageCounter++, ctx.pageName, true));
$rows = $('#rmProtectMultiWrap .rmProtectPageRow');
hasExtra = false;
}
$('#rmProtectHeaderWrap').toggle(hasExtra);
if (!hasExtra) $('#rmProtectHeader').val('');
$rows.each(function () {
var $row = $(this);
$row.find('.rmProtectAddPage,.rmRemoveInput').remove();
$row.append(hasExtra
? '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>'
: buildProtectAddButtonHtml()
);
});
syncModalLayout();
}
createModal({
title: isZka ? 'Запрос к администраторам' : 'Запрос на защиту страницы',
width: 'compact',
subtitleHtml: isZka
? '<a href="' + getPageUrl('Википедия:Запросы к администраторам') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Запросы к администраторам</a>' +
' · <a href="' + getPageUrl('Википедия:Запросы к администраторам/Быстрые') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Быстрые</a>'
: '<span id="rmProtectLinkWrap"></span>'
});
$('#removerModalContent').html(buildReportFormHtml(ctx, isZka));
if (!isZka) {
$('#removerModalContent')
.on('click', '#rmProtectModeInstall', function () { applyProtectMode('install'); })
.on('click', '#rmProtectModeRemove', function () { applyProtectMode('remove'); })
.on('click', '.rmProtectOptBtn', function () {
$(this).toggleClass('is-active');
if (protectMode === 'install') {
var $levels = $('#rmProtectLevels .rmProtectOptBtn.is-active');
if ($(this).closest('#rmProtectReasons').length && $(this).hasClass('is-active') && $levels.length === 0) {
$('#rmProtectLevels .rmProtectOptBtn').first().addClass('is-active');
}
if ($(this).closest('#rmProtectLevels').length && !$(this).hasClass('is-active') && $levels.length === 0) {
$('#rmProtectReasons .rmProtectOptBtn').removeClass('is-active');
}
}
applyGeneratedText($('#rmReportText'), buildProtectText(protectMode));
});
$(document)
.off('click.rmProtectAdd').on('click.rmProtectAdd', '.rmProtectAddPage', function () {
var id = 'rmProtectPage' + pageCounter++;
$('#rmProtectPagesContainer').append(buildProtectPageRowHtml(id, '', false));
updateProtectMultiUi();
})
.off('click.rmProtectRemove').on('click.rmProtectRemove', '.rmProtectPageRow .rmRemoveInput', function () {
$(this).closest('.rmProtectPageRow').remove(); updateProtectMultiUi();
});
applyProtectMode('install');
}
setupResizableModal('rmReportText');
renderModalFooter('submit', {
showCheckbox: false,
showSubscribe: true,
submitText: 'Отправить',
onSubmit: function () { doReport(ctx, false, protectMode); return true; }
});
if (isZka) {
$('<button id="rmReportFast" style="' + stCancel + '">Быстрый запрос</button>').insertBefore('#removerSubmit');
$('#rmReportFast').click(function () {
if ($('#removerSubmit').data('rmSubmitInProgress')) return;
$('#removerSubmit').data('rmSubmitInProgress', true).prop('disabled', true);
$('#rmReportFast').prop('disabled', true);
doReport(ctx, true, protectMode);
});
}
}
};
// ═══════════════════════════════════════════════════════════════════════════
// ВСПОМОГАТЕЛЬНЫЕ: КБУ, закрытие, категории
// ═══════════════════════════════════════════════════════════════════════════
function getFastRemoveCriteriaAnchorFromConfig(templateName) {
var anchors = cfg.fastRemoveCriteriaAnchors || {};
var template = String(templateName || '').trim();
var lower = template.toLowerCase();
var key;
if (!template) return '';
if (Object.prototype.hasOwnProperty.call(anchors, template)) return anchors[template];
for (key in anchors) {
if (Object.prototype.hasOwnProperty.call(anchors, key) && String(key).toLowerCase() === lower) {
return anchors[key];
}
}
return '';
}
function getFastRemoveCriteriaAnchor(reason) {
var configured = reason ? getFastRemoveCriteriaAnchorFromConfig(reason[0]) : '';
var template, label, m;
if (configured) return configured;
template = String(reason && reason[0] || '');
label = String(reason && reason[1] || '');
if (/^(?:подст\s*:\s*)?(?:deleteslow|ds)$/i.test(template) || /^\s*ds\b/i.test(label)) return 'С1';
m = label.match(/^\s*([А-ЯЁA-Z]{1,3}\d+(?:\.\d+)?)/);
return m ? m[1] : '';
}
function buildFastRemoveCriteriaLinkHtml(reason) {
var anchor = getFastRemoveCriteriaAnchor(reason);
var label = KBU_CRITERIA_PAGE + (anchor ? '#' + anchor : '');
var url = getPageUrlWithFragment(KBU_CRITERIA_PAGE, anchor);
return '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(label) + '</a>';
}
function getFastRemoveReasons() {
var reasons = cfg.fastRemoveReasons;
var prefix = (mwCfg.wgIsRedirect ? 'ОП' : 'О') + ({ 0: 'С', 2: 'У', 3: 'У', 6: 'Ф', 14: 'К' }[mwCfg.wgNamespaceNumber] || '');
var all = [];
if (isCategory && reasons.categories) all = all.concat(reasons.categories);
['general','articles','redirects','files','users','special'].forEach(function (k) { if (reasons[k]) all = all.concat(reasons[k]); });
if (!isCategory && reasons.categories) all = all.concat(reasons.categories);
return all.filter(function (r) { return prefix.indexOf(r[2] !== undefined ? r[2] : r[1].charAt(0)) >= 0; });
}
function showCloseActionsModal(opts) {
createModal({ title: 'Снятие шаблонов номинаций', inline: true });
$('#removerModalContent').html('<p style="margin:0;">Определение доступных действий...</p>');
var pageName = normTitle(mwCfg.wgPageName);
getText(pageName, function (pageText, readErr) {
if (readErr) { showInfoAndClose('Не удалось прочитать содержимое страницы.', readErr.info || readErr.code || '', true); return; }
if (pageText === null) { showInfoAndClose('Не удалось прочитать содержимое страницы.', '', true); return; }
var actions = opts.getActions(pageText);
if (!actions.length) { showInfoAndClose(opts.emptyText, opts.emptyDetails || ''); return; }
var actionMap = actions.reduce(function (m, a) { m[a.id] = a; return m; }, {});
$('#removerModalContent').html(buildActionsHtml(actions, opts.inputName, opts.listId));
if (opts.afterRender) opts.afterRender(actions, actionMap, pageName, pageText);
renderModalFooter('submit', {
showCheckbox: opts.showCheckbox,
submitText: 'Выполнить',
onSubmit: function () {
var sel = getSelectedAction(opts.inputName, actionMap);
if (!sel) return false;
startProcessing();
return opts.onSubmit(sel, pageName, pageText, actionMap) !== false;
}
});
if (opts.afterFooterRender) opts.afterFooterRender(actions, actionMap, pageName, pageText);
});
}
function runPageEditWithStatus(opts) {
var o = opts || {};
var statusId = logStatus(o.pendingText, null, { pending: true, trackError: false });
editPageContent(o.pageName, o.editOptions, o.buildFn, function (err, meta) {
if (err) { logStatus(o.errorText, err, { statusId: statusId }); unlockModalSubmit(); return; }
logStatus(o.successText, null, { statusId: statusId, trackError: false });
if (o.onSuccess) o.onSuccess(meta || null);
});
}
function removeKuFromCategory(pageName) {
runPageEditWithStatus({
pageName: pageName,
pendingText: 'Снимается шаблон КУ...',
errorText: 'Снятие шаблона КУ.',
successText: 'Шаблон КУ снят.',
editOptions: { summary: makeSummary('снятие шаблона КУ'), watchlist: 'nochange', readError: 'Страница не существует.', readErrorCode: 'read_failed' },
buildFn: function (text) {
var r = stripTemplatesByPattern(text, '(?:к\\s*удалению|ку)');
if (!r.removed) return { error: { code: 'no_changes', info: 'Шаблон КУ не найден.' } };
return { text: r.text.replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n') };
},
onSuccess: function () {
logStatus('Шаблон на СО не устанавливался.', null, { trackError: false });
renderModalFooter('reload');
}
});
}
// ── Категории: добавление шаблона ────────────────────────────────────────
function addTemplateToCategory(pageName, type, mainName, additionalNames, callback) {
var cb = callback || function () {};
var cfgByType = {
discuss: { action: 'обсуждение', template: 'Обсуждаемая категория' },
deletion: { action: 'удаление', template: 'Обсуждаемая категория' },
rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true },
merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true }
};
var typeCfg = cfgByType[type];
if (!typeCfg) { cb({ code: 'error', info: 'Неизвестный тип номинации.' }); return; }
var dateStr = getDate()[0];
var parts = [dateStr];
if (typeCfg.needsMain) {
if (!mainName) { cb({ code: 'error', info: 'Не указано основное название.' }); return; }
parts.push(mainName);
if (additionalNames && additionalNames.length) Array.prototype.push.apply(parts, additionalNames);
}
var tplText = T_OPEN + typeCfg.template + '|' + parts.join('|') + T_CLOSE;
editPageContent(pageName, { summary: makeSummary('добавление шаблона номинации на ' + typeCfg.action), readError: 'Не удалось получить содержимое.' },
function (text) { return { text: wrapInNoinclude(text, tplText) }; },
function (err) {
logPageEdit(pageName, err);
if (err) { cb(err); return; }
if (type === 'merge') {
addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, function () { cb(null); });
return;
}
cb(null);
}
);
}
function collectCategoryPageInputValues(selector) {
var pages = [];
$(selector).each(function () {
var title = normalizeCategoryPageName($(this).val() || '');
if (title && pages.indexOf(title) === -1) pages.push(title);
});
return pages;
}
function addTemplatesToCategories(pages, type, mainName, additionalNames, callback) {
var cb = callback || function () {};
var processedPages = [];
eachSequential(pages || [], function (pageName, next) {
addTemplateToCategory(pageName, type, mainName, additionalNames, function (err) {
if (!err) processedPages.push(pageName);
next(err || null);
});
}, function (err) { cb(err, processedPages); });
}
function addMergeTemplatesToTargets(sourcePage, mainName, additionalNames, dateStr, callback) {
var cb = callback || function () {};
var currentCatName = normTitle(stripCatPrefix(sourcePage));
var targets = [mainName].concat(additionalNames || []);
if (!targets.length) { cb(); return; }
eachSequential(targets, function (target, next) {
var targetPage = 'Категория:' + target;
addMergeTemplateToTargetCategory(targetPage, currentCatName, dateStr, function (success, status) {
var url = getPageUrl(targetPage);
var linkHtml = '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(targetPage) + '</a>';
if (success) {
var extra = (status === 'already_exists' || status === 'updated') ? ' (' + formatMergeStatus(status) + ')' : '';
logStatus('Шаблон добавлен в ' + linkHtml + extra + '.', null, { trackError: false });
} else {
logStatus('Ошибка при добавлении шаблона в ' + linkHtml + '.', { code: 'merge_target_failed', info: status }, { trackError: false });
}
next();
});
}, cb);
}
function addMergeTemplateToTargetCategory(targetPageName, sourceCatName, dateStr, callback) {
editPageContent(targetPageName, { summary: makeSummary('добавление шаблона объединения'), readError: 'Не удалось получить содержимое' },
function (text) {
var existing = text.match(getCategoryMergeRe());
if (existing) {
var cats = existing[1].split('|').slice(1).map(function (p) { return p.trim(); }).filter(function (p) { return p.indexOf('=') === -1 && p.length > 0; });
var norm = sourceCatName.replace(/\s+/g, ' ').trim();
if (cats.some(function (c) { return c.replace(/\s+/g, ' ').trim() === norm; })) { return { skip: true, meta: { status: 'already_exists' } }; }
return {
text: text.replace(existing[0], function () { return existing[0].replace(/\}\}\s*$/, '|' + sourceCatName + '}}'); }),
summary: makeSummary('дополнение шаблона объединения [[:Категория:' + sourceCatName + ']]'),
meta: { status: 'updated' }
};
}
return { text: wrapInNoinclude(text, T_OPEN + 'Категория к объединению|' + dateStr + '|' + sourceCatName + T_CLOSE), meta: { status: 'created' } };
},
function (err, meta) { callback(!err, err ? err.info : ((meta && meta.status) || 'updated')); }
);
}
// ── Категории: обсуждение ────────────────────────────────────────────────
function buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, options) {
var opts = options || {};
var pages = Array.isArray(pageName) ? pageName : null;
var titleText;
if (pages && pages.length) {
titleText = String(opts.headerText || '').trim() || (formatPagesWithAnd(pages, ':') + (type === 'deletion' ? ' → удалить' : ''));
return '=== ' + titleText + ' ===\n';
}
var title = '=== [[:' + pageName + ']]';
if (type === 'rename' || type === 'merge') {
title += (type === 'rename' ? ' → ' : ' объединить с ') + formatCatLink(mainName);
if (additionalNames && additionalNames.length) {
var conj = type === 'rename' ? ' или ' : ' и ';
var head = additionalNames.slice(0, -1).map(formatCatLink).join(', ');
title += (additionalNames.length > 1 ? ', ' + head : '') + conj + formatCatLink(additionalNames[additionalNames.length - 1]);
}
} else if (type === 'deletion') {
title += ' → удалить';
}
return title + ' ===\n';
}
function createCategoryDiscussion(pageName, reason, type, callback, mainName, additionalNames, options) {
var cb = callback || function () {};
var opts = options || {};
var now = new Date();
var m = now.getUTCMonth(), year = now.getUTCFullYear(), day = now.getUTCDate();
var dateHeader = '== ' + day + ' ' + MONTHS_GEN[m] + ' ' + year + ' ==\n';
var discPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[m] + '_' + year;
var discTitle = buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, opts);
var discText = discTitle + (opts.reasonIsPrepared ? String(reason || '') : appendNominationSignature(reason)) + '\n';
var sectionTitle = discTitle.replace(/^===\s*/, '').replace(/\s*===\s*$/, '').trim();
var summaryTarget = Array.isArray(pageName) ? formatPagesWithAnd(pageName, ':') : '[[:' + pageName + ']]';
var summaryText = 'добавление обсуждения для ' + (Array.isArray(pageName) ? 'категорий ' : 'категории ') + summaryTarget;
publishNomination({
pageTitle: discPage,
readErrorMessage: 'Не удалось получить содержимое страницы обсуждения.',
summary: makeSummary(summaryText),
buildText: function (text) {
var todayIdx = text.indexOf(dateHeader.trim());
if (todayIdx !== -1) {
var endIdx = text.indexOf('\n== ', todayIdx + dateHeader.length);
if (endIdx === -1) endIdx = text.length;
var before = text.slice(0, endIdx);
return { text: (before.endsWith('\n\n') ? before.slice(0, -1) : before) + '\n' + discText + text.slice(endIdx) };
}
var searchStr = T_OPEN + 'ОБК-Навигация' + T_CLOSE;
var obkIdx = text.indexOf(searchStr);
if (obkIdx === -1) return { error: { code: 'insert_failed', info: 'Не удалось найти место для вставки.' } };
return { text: text.slice(0, obkIdx + searchStr.length) + '\n\n' + dateHeader + discText + text.slice(obkIdx + searchStr.length).replace(/^\n+/, '\n') };
}
}, function (err) {
if (err) { cb(err); return; }
cb(null, { pageTitle: discPage, sectionTitle: sectionTitle });
});
}
// ── Категории: завершение ОБКАТ ───────────────────────────────────────────
function markCategoryDiscussionAsDone(pageName) {
runPageEditWithStatus({
pageName: pageName,
pendingText: 'Снимается шаблон обсуждения...',
errorText: 'Снятие шаблона обсуждения.',
successText: 'Шаблон обсуждения снят.',
editOptions: { summary: makeSummary('обсуждение категории завершено'), readError: 'Не удалось получить содержимое.' },
buildFn: function (text) {
var allTpls = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|');
var patterns = [
new RegExp('<noinclude>\\s*\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}\\s*</noinclude>', 'i'),
new RegExp('\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}', 'i')
];
var match = null;
for (var i = 0; i < patterns.length; i++) { match = text.match(patterns[i]); if (match) break; }
if (!match) return { error: { code: 'no_changes', info: 'Шаблон обсуждаемой категории не найден.' } };
return {
text: text.replace(match[0], '').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n'),
meta: { tplDate: convertToStandardDate(match[2].split('|')[0].trim()) }
};
},
onSuccess: function (meta) {
var talkStatusId = logStatus('Обновляется шаблон на СО категории...', null, { pending: true, trackError: false });
updateCategoryTalkPage(pageName, meta.tplDate, function (talkErr, info) {
if (talkErr) { logStatus('Установка шаблона на СО.', talkErr, { statusId: talkStatusId }); unlockModalSubmit(); return; }
logStatus(
(info && (info.status === 'already_present' || info.status === 'no_changes')) ? 'Шаблон на СО уже установлен.' : 'Шаблон установлен на СО.',
null, { statusId: talkStatusId, trackError: false }
);
renderModalFooter('reload');
});
}
});
}
function updateCategoryTalkPage(categoryName, templateDate, callback) {
var cb = callback || function () {};
var talkPage = getTalkPage(categoryName);
var newTpl = T_OPEN + 'Обсуждавшаяся категория|' + templateDate + T_CLOSE;
getTextWithTimestamp(talkPage, function (text, baseTimestamp, readErr) {
if (readErr) { cb(makeReadError(readErr, 'talk_read_failed', 'Не удалось получить содержимое СО категории.')); return; }
if (text === null) {
apiReq({ title: talkPage, text: newTpl + '\n\n', summary: makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate), createonly: true },
'edit', function (resp) {
if (resp && resp.error) {
if (resp.error.code === 'articleexists') setTimeout(function () { updateCategoryTalkPage(categoryName, templateDate, cb); }, 1000);
else cb(resp.error);
} else cb(null, { status: 'created' });
});
return;
}
var discussedRe = new RegExp('\\{\\{\\s*(' + cfg.categoryTemplates.discussed + ')([^\\}]*?)\\s*\\}\\}', 'i');
var tplMatch = text.match(discussedRe);
var newText = text;
if (tplMatch) {
var existingDates = tplMatch[2].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
if (existingDates.indexOf(templateDate) !== -1) { cb(null, { status: 'already_present' }); return; }
newText = text.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}$/, '|' + templateDate + '}}'); });
} else {
newText = insertTplOnTalkPage(text, newTpl);
}
if (newText === text) { cb(null, { status: 'no_changes' }); return; }
var ep = {
title: talkPage,
text: newText,
summary: tplMatch
? makeSummary('обновление шаблона [[ш:Обсуждавшаяся категория]], добавлена дата ' + templateDate)
: makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate)
};
if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) { cb(resp && resp.error ? resp.error : null, resp && !resp.error ? { status: tplMatch ? 'updated' : 'created' } : null); });
});
}
// ── Быстрое объединение (Ctrl+клик КОБ) ─────────────────────────────────
function buildQuickMergeHtml(tplDate, targets, currentCatName) {
return joinHtml([
'<p>Найден шаблон с датой <strong>', escapeHtml(tplDate), '</strong>. Категории для объединения:</p>',
'<pre style="background:', tk.bgBase, ';color:', tk.cBase, ';padding:10px;border:1px solid ', tk.bSubS, ';border-radius:4px;margin-bottom:10px;">',
targets.map(function (c) { return '• ' + escapeHtml(c); }).join('\n'),
'</pre>',
'<p><strong>Текущая категория:</strong> ', escapeHtml(currentCatName), '</p>',
'<p style="color:', tk.cSub, ';">Шаблон будет добавлен во все указанные выше категории.</p>'
]);
}
function showQuickMergeModal() {
getText(mwCfg.wgPageName, function (text, readErr) {
if (readErr) { alert('Не удалось получить содержимое: ' + (readErr.info || readErr.code || 'ошибка API') + '.'); return; }
if (!text) { alert('Не удалось получить содержимое.'); return; }
var mergeRe = getCategoryMergeRe();
var match = text.match(mergeRe);
if (!match) { alert('В текущей категории не найден шаблон "Категория к объединению".'); return; }
var params = match[1].split('|').map(function (p) { return p.trim(); });
var tplDate = params[0];
var targets = params.slice(1);
if (!targets.length) { alert('В шаблоне не найдены целевые категории.'); return; }
createModal({ title: 'Быстрое добавление шаблона объединения' });
var currentCatName = normTitle(stripCatPrefix(mwCfg.wgPageName));
$('#removerModalContent').html(buildQuickMergeHtml(tplDate, targets, currentCatName));
renderModalFooter('submit', {
showCheckbox: false,
submitText: 'Добавить шаблоны',
onSubmit: function () {
startProcessing();
$('#removerSubmit').prop('disabled', true);
eachSequential(targets, function (target, next) {
addMergeTemplateToTargetCategory('Категория:' + target, currentCatName, tplDate, function (success, status) {
if (success) logStatus('Категория [[:Категория:' + target + ']] (' + formatMergeStatus(status) + ').', null, { trackError: false });
else logStatus('Ошибка [[:Категория:' + target + ']].', { code: 'merge_target_failed', info: status }, { trackError: false });
next();
});
}, function () {
if (isError) markSubmitError(); else renderModalFooter('close');
});
return true;
}
});
});
}
// ── ЗКА/Защита: публикация ───────────────────────────────────────────────
function getReporterContext(mode) {
var rawPage = mwCfg.wgPageName;
var pageName = normTitle(rawPage)
.replace(/(Special|Служебная):(Contributions|Вклад)\//i, 'User:')
.replace(/(User talk:|Обсуждение участни(ка|цы):)/i, 'User:');
var isUserRelated = /user|contrib|участни|вклад/i.test(rawPage);
var displayName = normTitle(rawPage)
.replace(/(Special|Служебная):(Вклад|Contributions)\//i, '')
.replace(/(User talk:|Обсуждение участни(ка|цы):)/i, '')
.replace(/(user|участни(к|ца)):/i, '');
var pageLink = '[[' + pageName + (isUserRelated ? '|' + displayName + ']]' : ']]');
var reportPage = mode === 'request' ? 'Википедия:Запросы к администраторам' : 'Википедия:Установка защиты';
return { pageName: pageName, pageLink: pageLink, displayName: displayName, reportPage: reportPage };
}
function doReport(ctx, fast, protectMode) {
var header = $('#rmReportHeader').val() || ctx.pageLink;
var text = $('#rmReportText').val() || '';
var isZka = ctx.reportPage === 'Википедия:Запросы к администраторам';
var isRemoveProtect = !isZka && protectMode === 'remove';
startProcessing();
var targetPage, editParams, sectionForLink;
if (fast) {
targetPage = 'Википедия:Запросы к администраторам/Быстрые';
sectionForLink = null;
editParams = {
appendtext: '\n\n' + T_OPEN + 'sub' + 'st:t:preload/ЗКАБ/subst|\n | участник = ' + ctx.displayName +
'| страница = | пояснение = ' + text + T_CLOSE + '\n',
summary: makeSummary('новый запрос [[Special:Contributions/' + ctx.displayName + ']]')
};
} else if (isZka) {
targetPage = ctx.reportPage;
sectionForLink = extractDisplayedText(header);
var isIpFull = /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/.test(ctx.displayName);
editParams = {
section: 'new',
sectiontitle: header,
text: '* ' + T_OPEN + 'userlinks|' + ctx.displayName + (isIpFull ? '|ip=1' : '') + T_CLOSE + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
} else {
targetPage = isRemoveProtect ? 'Википедия:Снятие защиты' : 'Википедия:Установка защиты';
var pages = collectInputValues('.rmProtectPageInput');
if (!pages.length) pages = [ctx.pageName];
var sectionTitle, pageLines;
if (pages.length === 1) {
sectionTitle = '[[' + pages[0] + ']]';
pageLines = '* ' + T_OPEN + 'pagelinks-protect|' + pages[0] + T_CLOSE;
} else {
sectionTitle = ($('#rmProtectHeader').val() || '').trim() || pages.map(function (p) { return '[[' + p + ']]'; }).join(', ');
pageLines = pages.map(function (p) { return '* ' + T_OPEN + 'pagelinks-protect|' + p + T_CLOSE; }).join('\n');
}
sectionForLink = extractDisplayedText(sectionTitle);
editParams = {
section: 'new',
sectiontitle: sectionTitle,
text: pageLines + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
}
var statusId = logStatus('Публикуется запрос на «' + targetPage + '»...', null, { pending: true, trackError: false });
apiReq($.extend({ title: targetPage }, editParams), 'edit', function (resp) {
if (resp && resp.error) {
logStatus('Публикация запроса на «' + targetPage + '».', resp.error, { statusId: statusId });
markSubmitError();
if (isZka) $('#rmReportFast').prop('disabled', false);
return;
}
logStatus('Запрос опубликован.', null, { statusId: statusId, trackError: false });
appendNominationLink(targetPage, sectionForLink);
if (sectionForLink) subscribeToTopic(targetPage, sectionForLink);
renderModalFooter('reload');
});
}
// ═══════════════════════════════════════════════════════════════════════════
// ДИСПЕТЧЕР
// ═══════════════════════════════════════════════════════════════════════════
function handleMenuClick(item, event) {
isError = false;
var op = OPERATIONS_MAP[item.id];
// Специальный случай: КОБ категории с Ctrl — быстрое добавление шаблона
if (item.id === 'cat-merge' && event && event.ctrlKey) {
showQuickMergeModal();
return;
}
if (!op) {
console.error('RemoverCore: неизвестная операция', item.id);
return;
}
var handlerFn = handlers[op.handler];
if (typeof handlerFn !== 'function') {
console.error('RemoverCore: обработчик не найден', op.handler);
return;
}
handlerFn(op, event);
}
// ─── Экспорт ─────────────────────────────────────────────────────────────
window.RemoverCore = { handleMenuClick: handleMenuClick };
}());
6ywtcsvrdhpx13g1d5h3p4ob6qnoitl
739910
739909
2026-05-01T06:41:26Z
Solidest
54422
739910
javascript
text/javascript
/**
* Remover — ядро (core).
* Загружается лениво при первом клике по пункту меню.
* Ожидает, что window.RemoverState уже задан remover-loader.js.
*
* Архитектура: единый реестр OPERATIONS описывает все операции (КБУ, КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС, Защита, Запрос, Снятие, кат-варианты).
* Логика выполнения сосредоточена в универсальных обработчиках.
* Экспортирует: window.RemoverCore.handleMenuClick(item, event)
*/
(function () {
'use strict';
var state = window.RemoverState;
if (!state) { console.error('RemoverCore: window.RemoverState не задан.'); return; }
var mwCfg = state.mwCfg;
var cfg = applyCoreConfigDefaults(state.cfg || {});
var isCategory = state.isCategory;
var isVector22 = state.isVector22;
var scriptLink = cfg.scriptLink;
var settingsOptionName = state.settingsOptionName || 'userjs-remover-settings';
var settingsVersion = 1;
var settingsMenuMeta = collectSettingsMenuMeta();
var settingsArticleItemLabels = settingsMenuMeta.articleLabels;
var settingsCategoryItemLabels = settingsMenuMeta.categoryLabels;
var settingsItemLabelById = settingsMenuMeta.idToLabel;
var settingsItemLabelByNorm = settingsMenuMeta.labelByNorm;
var settingsItemLabelOrder = settingsMenuMeta.labelOrder;
var settingsDefaults = getDefaultSettings();
var MENU_TITLE_PRESET_CACTIONS = '__remover_portlet_cactions__';
var MENU_TITLE_PRESET_PAGE = '__remover_portlet_page__';
var MENU_TITLE_PRESET_TOOLS = '__remover_portlet_tools__';
var initialSettings = normalizeRemoverSettings(readSettingsOptionState(state.settings || {}));
var setAlert = ('setAlert' in state) ? !!state.setAlert : initialSettings.notifyAuthor;
var setSubscribe = ('setSubscribe' in state) ? !!state.setSubscribe : initialSettings.subscribeTopic;
var signatureSeparator = ('signatureSeparator' in state && typeof state.signatureSeparator === 'string')
? state.signatureSeparator.trim()
: initialSettings.signatureSeparator;
initialSettings.notifyAuthor = setAlert;
initialSettings.subscribeTopic = setSubscribe;
initialSettings.signatureSeparator = signatureSeparator;
state.cfg = cfg;
state.settings = clonePlainObject(initialSettings);
state.setAlert = setAlert;
state.setSubscribe = setSubscribe;
// ─── Константы ──────────────────────────────────────────────────────────
var MONTHS_NOM = ['Январь','Февраль','Март','Апрель','Май','Июнь','Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'];
var MONTHS_GEN = ['января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря'];
var MONTHS_NOM_LOWER = MONTHS_NOM.map(function (m) { return m.toLowerCase(); });
var T_OPEN = '{' + '{';
var T_CLOSE = '}' + '}';
var KBU_CRITERIA_PAGE = 'Википедия:Критерии быстрого удаления';
var RE_ESCAPE = /[.*+?^${}()|[\]\\]/g;
var RE_KU_ON_PAGE = /\{\{\s*(?:к\s*удалению|ку)\s*(?:\||\}\})/i;
var RE_KPM_ON_PAGE = /\{\{\s*(?:к\s*переименованию|кпм|rename)\s*(?:\||\}\})/i;
var RE_KBU_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?(?:db\s*-[^|}\s]+|уд\s*-[^|}\s]+|к\s*быстрому\s*удалению|к\s*отсроченному\s*удалению|deleteslow|ds|hang\s*-?\s*on)\s*(?:\||\}\})/i;
var RE_KUL_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?к\s*улучшению\s*(?:\||\}\})/i;
var KBU_PATTERN_STR = 'db\\s*-[^|}\\s]+|уд\\s*-[^|}\\s]+|к\\s*быстрому\\s*удалению|к\\s*отсроченному\\s*удалению|deleteslow|ds';
var RE_KBU_PATTERNS = new RegExp('(?:' + KBU_PATTERN_STR + ')', 'i');
var KUL_PATTERN_STR = 'к\\s*улучшению';
var RE_KUL_PATTERN = new RegExp(KUL_PATTERN_STR, 'i');
var HANGON_PATTERN_STR = 'hang\\s*-?\\s*on';
var RE_HANGON = new RegExp(HANGON_PATTERN_STR, 'i');
var RE_DATE_ISO = /^(\d{4})-(\d{2})-(\d{2})$/;
var RE_DATE_RUSSIAN = /^(\d{1,2})\s+([\u0430-\u044f\u0410-\u042f\u0451\u0401.]+)\s+(\d{2}|\d{4})$/;
var RE_DATE_DASH = /^(\d{1,4})\s*-\s*(\d{1,2})\s*-\s*(\d{1,4})$/;
var RE_DATE_DOT = /^(\d{1,2})\s*\.\s*(\d{1,2})\s*\.\s*(\d{2}|\d{4})$/;
var RE_DATE_SLASH = /^(\d{1,4})\s*\/\s*(\d{1,2})\s*\/\s*(\d{1,4})$/;
var RE_NOINCLUDE = /(\s*)<noinclude>([\s\S]*?)<\/noinclude>/i;
var RE_TEMPLATE_NS = /^шаблон\s*:\s*/i;
// ─── Глобальные переменные сессии ────────────────────────────────────────
var isError = false;
var logStatusSeq = 0;
var resizeObservers = [];
var modalLayoutSyncHandlers = [];
var tplAliasCache = {};
// ─── Стили ───────────────────────────────────────────────────────────────
var stStyles = cfg.modalStyles;
var tk = {
cBase: 'var(--color-base, #202122)',
cSub: 'var(--color-subtle, #72777d)',
cSubM: 'var(--color-subtle, #54595d)',
cInv: 'var(--color-inverted-fixed, #fff)',
cProg: 'var(--color-progressive, #3366cc)',
cProgH: 'var(--color-progressive--hover, #2a4b8d)',
cDang: 'var(--color-destructive, #d73333)',
cDis: 'var(--color-disabled, var(--color-subtle, #72777d))',
bgBase: 'var(--background-color-base, #fff)',
bgNSub: 'var(--background-color-neutral-subtle, #f8f9fa)',
bgN: 'var(--background-color-neutral, #eaecf0)',
bgDis: 'var(--background-color-disabled, var(--background-color-neutral, #eaecf0))',
bgProg: 'var(--background-color-progressive, #3366cc)',
bgProgH:'var(--background-color-progressive--hover, #2a4d8f)',
bgSucc: 'var(--background-color-success, #14866d)',
bgSuccH:'var(--background-color-success--hover, #0f6d57)',
bSub: 'var(--border-color-subtle, #a2a9b1)',
bSubS: 'var(--border-color-subtle, #ddd)',
bDis: 'var(--border-color-disabled, var(--border-color-subtle, #a2a9b1))',
bProg: 'var(--border-color-progressive, #3366cc)',
bProgH: 'var(--border-color-progressive--hover, #2a4d8f)',
bSucc: 'var(--border-color-success, #14866d)',
bSuccH: 'var(--border-color-success--hover, #0f6d57)'
};
var sz = {
taH: '180px',
taMinH: '100px',
taMinW: '180px',
mobileBp: 720,
modalRatio: 0.4,
modalMinWide: 420,
modalDefaultWide: 720,
viewportGap: 24,
touchDesktopGap: 120
};
var btnBase = 'border-radius:4px;padding:8px 16px;cursor:pointer;font-size:14px;font-family:inherit;transition:background .1s;';
var neutralVis = 'background:' + tk.bgNSub + ';border:1px solid ' + tk.bSub + ';color:' + tk.cBase + ';border-radius:4px;';
var stCancel = neutralVis + btnBase;
var stSubmit = 'background:' + tk.bgProg + ';color:' + tk.cInv + ';border:1px solid ' + tk.bProg + ';' + btnBase;
var stReload = 'background:' + tk.bgSucc + ';color:' + tk.cInv + ';border:1px solid ' + tk.bSucc + ';' + btnBase;
var stInputBox = 'flex:1;padding:6px;box-sizing:border-box;min-width:0;border:1px solid ' + tk.bSub + ';background:' + tk.bgBase + ';color:inherit;border-radius:2px;';
var stInputFull= 'width:100%;padding:6px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:2px;background:' + tk.bgBase + ';color:inherit;margin-bottom:10px;';
var stRow = 'display:flex;margin-bottom:6px;';
var stToolBtn = neutralVis + 'padding:4px 8px;margin-left:4px;cursor:pointer;font-size:12px;';
var stRemoveBtn= neutralVis + 'display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;padding:0;width:32px;height:32px;margin-left:4px;cursor:pointer;font-size:12px;line-height:1;';
var stFooterWrap = 'display:flex;align-items:center;gap:8px;flex-wrap:wrap;';
var stFooterChecks = 'display:flex;flex-direction:column;gap:4px;margin-right:auto;flex:1 1 220px;min-width:0;';
var stFooterCheckLabel = 'display:inline-flex;align-items:flex-start;gap:6px;font-size:14px;line-height:1.4;max-width:100%;';
var stFooterActions = 'display:flex;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;justify-content:flex-end;margin-left:auto;';
var stHeaderIconBtn = 'margin:0 0 0 auto;width:32px;min-width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:4px;background:' + tk.bgNSub + ';color:' + tk.cSubM + ';cursor:pointer;font-size:16px;line-height:1;';
var multiNominationGap = '6px';
var RESIZE_CLASS = 'rm-resizable';
// ═══════════════════════════════════════════════════════════════════════════
// РЕЕСТР ОПЕРАЦИЙ
// Каждая запись описывает одну кнопку меню. Поля:
// id — идентификатор (совпадает с item.id из loader)
// handler — имя метода-обработчика в объекте handlers
// handlerArg — аргумент, передаваемый в handler (опционально)
// ═══════════════════════════════════════════════════════════════════════════
var OPERATIONS = [
// ── Статьи ──────────────────────────────────────────────────────────
{
id: 'fRm',
label: 'КБУ',
handler: 'showKbu',
// Параметры номинации: заполняются при submit
nomination: {
pageTitle: function (pg) { return normTitle(pg); },
// шаблон встраивается в статью, номинационная страница отсутствует
inArticle: true
}
},
{
id: 'tRm',
label: 'КУ',
handler: 'showNomination',
nomination: {
comment: 'к удалению',
template: 'к удалению',
navTemplate: 'КУ',
nomPage: function (date) { return 'Википедия:К удалению/' + date; },
supportsMulti: true,
supportsTransfer: true,
// шаблон встраивается в статью через <noinclude>
inArticle: true,
articleTpl: function (tplpar, date) { return 'к удалению|' + date + (tplpar ? '|' + tplpar : ''); }
}
},
{
id: 'rnm',
label: 'КПМ',
handler: 'showNomination',
nomination: {
comment: 'к переименованию',
template: 'к переименованию',
navTemplate: 'КПМ',
nomPage: function (date) { return 'Википедия:К переименованию/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к переименованию|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'rename',
firstId: 'rmRenameFirst', inputClass: 'rmRenameInput',
firstPh: 'Новое название',
addBtnId: 'rmAddRename', addBtnLabel: 'Добавить вариант',
containerId: 'rmRenameContainer', addPh: 'Дополнительный вариант',
maxRows: 2, maxMsg: 'Максимум 3 варианта переименования.'
}
}
},
{
id: 'imp',
label: 'КУЛ',
handler: 'showNomination',
nomination: {
comment: 'к срочному улучшению',
template: 'к улучшению',
navTemplate: 'КУЛ',
nomPage: function (date) { return 'Википедия:К улучшению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к улучшению|' + date + (tplpar ? '|' + tplpar : ''); }
}
},
{
id: 'merge',
label: 'КОБ',
handler: 'showNomination',
nomination: {
comment: 'к объединению с другой',
template: 'к объединению',
navTemplate: 'КОБ',
nomPage: function (date) { return 'Википедия:К объединению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к объединению|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'merge',
firstId: 'rmMergeFirst', inputClass: 'rmMergeInput',
firstPh: 'Объединить с…',
addBtnId: 'rmAddMerge', addBtnLabel: '+',
containerId: 'rmMergeContainer', addPh: 'Дополнительная статья',
maxRows: 10, maxMsg: 'Максимум 11 статей для объединения.'
}
}
},
{
id: 'split',
label: 'КРАЗД',
handler: 'showNomination',
nomination: {
comment: 'к разделению',
template: 'к разделению',
navTemplate: 'КР',
nomPage: function (date) { return 'Википедия:К разделению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к разделению|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'split',
firstId: 'rmSplitFirst', inputClass: 'rmSplitInput',
firstPh: 'Разделить на…',
addBtnId: 'rmAddSplit', addBtnLabel: '+',
containerId: 'rmSplitContainer', addPh: 'Дополнительная статья'
}
}
},
{
id: 'recov',
label: 'ВУС',
handler: 'showNomination',
nomination: {
comment: '',
template: 'к восстановлению',
navTemplate: 'ВУС',
nomPage: function (date) { return 'Википедия:К восстановлению/' + date; },
inArticle: false // шаблон не ставится в (удалённую) статью
}
},
{
id: 'close',
label: 'Снятие',
handler: 'showArticleClose'
},
// ── Запросы ─────────────────────────────────────────────────────────
{
id: 'protect',
label: 'Защита',
handler: 'showReport',
reportMode: 'protect'
},
{
id: 'request',
label: 'Запрос',
handler: 'showReport',
reportMode: 'request'
},
// ── Категории ────────────────────────────────────────────────────────
{
id: 'cat-fRm',
label: 'КБУ',
handler: 'showKbu',
forCategory: true
},
{
id: 'cat-discuss',
label: 'Обсудить',
handler: 'showCatNomination',
catType: 'discuss'
},
{
id: 'cat-delete',
label: 'Удалить',
handler: 'showCatNomination',
catType: 'deletion'
},
{
id: 'cat-rename',
label: 'Переименовать',
handler: 'showCatNomination',
catType: 'rename'
},
{
id: 'cat-merge',
label: 'Объединить',
handler: 'showCatNomination',
catType: 'merge'
},
{
id: 'cat-done',
label: 'Снятие',
handler: 'showCatClose'
}
];
// Быстрый поиск по id
var OPERATIONS_MAP = {};
OPERATIONS.forEach(function (op) { OPERATIONS_MAP[op.id] = op; });
// ─── Тексты переноса (КБУ → КУ) ─────────────────────────────────────────
var transferTexts = {
kbu: { notice: 'Перенесено с быстрого удаления.', hint: 'Шаблоны КБУ и Hangon будут сняты.' },
kul: { notice: 'Перенесено с КУЛ.', hint: 'Шаблоны КУЛ будут сняты.' },
both: { notice: 'Перенесено с быстрого удаления и КУЛ.', hint: 'Шаблоны КБУ, КУЛ и Hangon будут сняты.' }
};
// ═══════════════════════════════════════════════════════════════════════════
// УТИЛИТЫ
// ═══════════════════════════════════════════════════════════════════════════
function escapeRegExp(s) { return s.replace(RE_ESCAPE, '\\$&'); }
function escapeHtml(s) {
return String(s || '')
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
function joinHtml(parts) { return parts.join(''); }
function ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
function padTwo(n) { return n < 10 ? '0' + n : String(n); }
function expandTwoDigitYear(value) {
return 2000 + parseInt(value, 10);
}
function monthToNumber(name) {
var lower = name.toLowerCase().replace(/\.$/, '');
var idx = MONTHS_GEN.indexOf(lower);
if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower);
if (idx === -1 && lower.length >= 3) {
for (var i = 0; i < MONTHS_GEN.length; i++) {
if (MONTHS_GEN[i].indexOf(lower) === 0 || MONTHS_NOM_LOWER[i].indexOf(lower) === 0) return i + 1;
}
}
return idx + 1;
}
function makeStandardDate(yearValue, monthValue, dayValue) {
var yearText = String(yearValue || '').trim();
var year = yearText.length === 2 ? expandTwoDigitYear(yearText) : parseInt(yearText, 10);
var month = parseInt(monthValue, 10);
var day = parseInt(dayValue, 10);
var maxDay;
if ((yearText.length !== 2 && yearText.length !== 4) || isNaN(year) || isNaN(month) || isNaN(day) || year < 1 || month < 1 || month > 12 || day < 1) return null;
maxDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
if (day > maxDay) return null;
return year + '-' + padTwo(month) + '-' + padTwo(day);
}
function normalizeIsoDate(value) {
var m = String(value || '').trim().match(RE_DATE_ISO);
return m ? makeStandardDate(m[1], m[2], m[3]) : null;
}
function normalizeTemplateName(name) {
return (name || '').toLowerCase().replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
}
function getDate(dateString) {
var d = dateString ? new Date(dateString) : new Date();
var iso = d.getUTCFullYear() + '-' + padTwo(d.getUTCMonth() + 1) + '-' + padTwo(d.getUTCDate());
var rus = d.getUTCDate() + ' ' + MONTHS_GEN[d.getUTCMonth()] + ' ' + d.getUTCFullYear();
return [iso, rus];
}
function convertToStandardDate(dateStr) {
var value = String(dateStr || '').replace(/\s+/g, ' ').trim();
var m;
var mo;
var normalized;
m = value.match(RE_DATE_ISO);
if (m) return normalizeIsoDate(value) || '';
m = value.match(RE_DATE_RUSSIAN);
if (m) {
mo = monthToNumber(m[2]);
normalized = mo ? makeStandardDate(m[3], mo, m[1]) : null;
return normalized || '';
}
m = value.match(RE_DATE_DASH);
if (m) {
normalized = makeStandardDate(m[1], m[2], m[3]);
return normalized || '';
}
m = value.match(RE_DATE_DOT);
if (m) {
normalized = makeStandardDate(m[3], m[2], m[1]);
return normalized || '';
}
m = value.match(RE_DATE_SLASH);
if (m) {
normalized = makeStandardDate(m[1], m[2], m[3]);
return normalized || '';
}
return value;
}
function getTalkPage(pageName) {
var match = /([^:]*:)?(.*)/.exec(pageName);
if (match[1]) {
var ns = mwCfg.wgNamespaceIds[match[1].slice(0, -1).toLowerCase().replace(/ /g, '_')];
if (ns !== undefined) return mwCfg.wgFormattedNamespaces[ns | 1] + ':' + match[2];
}
return 'Обсуждение:' + pageName;
}
function normTitle(s) { return (s || '').replace(/_/g, ' '); }
function stripCatPrefix(s) { return (s || '').replace(/^Категория:\s*/i, ''); }
function normalizeCategoryPageName(value) {
var title = normTitle(value).trim();
var nsMatch, nsKey, ns;
if (!title) return '';
nsMatch = title.match(/^([^:]+):(.+)$/);
if (nsMatch) {
nsKey = nsMatch[1].toLowerCase().replace(/ /g, '_');
ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[nsKey];
if (ns === 14) return (mwCfg.wgFormattedNamespaces[14] || 'Категория') + ':' + nsMatch[2].trim();
return title;
}
return (mwCfg.wgFormattedNamespaces[14] || 'Категория') + ':' + title;
}
function makeSummary(text) { return scriptLink + ': ' + text; }
function appendNominationSignature(text) {
var body = String(text || '');
return body + (signatureSeparator ? ' ' + signatureSeparator : '') + ' ~~' + '~~';
}
function extractDisplayedText(s) {
return (s || '').replace(/\[\[:?(?:[^|\]]+\|)?(.+?)\]\]/g, '$1');
}
function collectInputValues(selector) {
return $(selector).map(function () { return $(this).val().trim(); }).get().filter(Boolean);
}
function applyCoreConfigDefaults(config) {
var defaults = {
scriptLink: '[[Участник:Solidest/Remover|Remover]]',
fastRemoveReasons: {
general: [
['уд-бессвязно', 'О1 Бессвязный текст'],
['уд-тест', 'О2 Тестовая страница'],
['уд-ванд', 'О3 Вандальная страница'],
['уд-повторно', 'О4 Уже удалялось'],
['уд-автор', 'О5 По просьбе автора'],
['уд-обс', 'О6 Ненужная подстраница'],
['уд-переим', 'О7 Для переименования'],
['уд-дубль', 'О8 Дубликат'],
['уд-реклама', 'О9 Реклама или спам'],
['db-badtalk', 'О10 Нецелевая СО'],
['уд-копивио', 'О11 Нарушение АП']
],
articles: [
['подст:ds', 'ds Отсроченное пусто или коротко', 'С'],
['уд-пусто', 'С1 Пусто или коротко'],
['уд-иностр', 'С2 Не на русском'],
['уд-ссылки', 'С3 Лишь ссылки'],
['уд-нз', 'С5 Явно незначимо'],
['уд-бям', 'С7 Создано нейросетью']
],
redirects: [
['уд-в никуда', 'П1 Перенапр. в никуда'],
['db-redirspace', 'П2 Межпростр. перенапр.'],
['уд-опечатка', 'П3 Перенапр. с опечаткой'],
['уд-падеж', 'П4 Не именительный падеж'],
['уд-смысл', 'П5 Неверное перенапр.'],
['db-redirtalk', 'П6 Перенапр. на СО']
],
files: [
['db-duplicate', 'Ф1 Копия файла'],
['db-badimage', 'Ф2 Повреждённый файл'],
['подст:nld', 'Ф3 Нет данных о лицензии'],
['подст:nsd', 'Ф3 Нет данных о источнике'],
['подст:nad', 'Ф3 Нет данных о авторе'],
['подст:dd', 'Ф3 Сомнительные данные файла'],
['подст:ofud', 'Ф4 Неиспользуемый КДИ'],
['подст:dfud', 'Ф5 Нет КДИ'],
['db-badfairuse', 'Ф6 Неоправданное КДИ'],
['подст:rfu', 'Ф7 Заменяемый КДИ'],
['NCT', 'Ф8 Есть на Складе'],
['подст:Nothost', 'Ф9 Файл — ВП:НЕХОСТИНГ']
],
categories: [
['уд-пусткат', 'К1 Пустая категория'],
['db-templatecat', 'К1.2 Разобранная служебная кат.'],
['уд-перекат', 'К2 Переименованная кат.']
],
users: [
['уд-владелец', 'У1 По желанию владельца'],
['уд-анон', 'У2 Устаревшая СО анонима'],
['уд-несущ', 'У3 Несуществующий участник'],
['уд-нецелевое', 'У4 Нецелевое использ. ЛП'],
['уд-неактив', 'У5 Подстраница неактивного']
],
special: [
['db', 'Особый случай']
]
},
fastRemoveCriteriaAnchors: {
'подст:ds': 'С1',
deleteslow: 'С1',
ds: 'С1'
},
requiredParamTemplates: {
'уд-переим': 'страницу, которую нужно переименовать',
'уд-дубль': 'страницу-дубликат',
'уд-копивио': 'URL источника нарушения АП',
'db-duplicate': 'имя файла-оригинала',
'подст:rfu': 'имя заменяемого файла',
'NCT': 'имя файла на Викискладе',
'уд-перекат': 'новое название категории',
'db': '!причину удаления'
},
categoryTemplates: {
discuss: 'Обсуждаемая категория|обсуждаемая категория|Acat|acat|ОКТО|окто|Категория к обсуждению|категория к обсуждению',
rename: 'Категория к переименованию|категория к переименованию|Anacat|anacat',
merge: 'Категория к объединению|категория к объединению|Amergecat|amergecat|Cfm|cfm',
discussed: 'Обсуждавшаяся категория|обсуждавшаяся категория|Обсуждалась|обсуждалась|Обсуждалось|обсуждалось'
},
modalStyles: {
border: '1px solid var(--border-color-progressive, #3366bb)',
background: 'var(--background-color-base, #f8f9fa)',
borderRadius: '6px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
headerColor: 'var(--color-progressive, #3366bb)'
}
};
config.scriptLink = config.scriptLink || defaults.scriptLink;
config.fastRemoveReasons = $.extend({}, defaults.fastRemoveReasons, config.fastRemoveReasons || {});
config.fastRemoveCriteriaAnchors = $.extend({}, defaults.fastRemoveCriteriaAnchors, config.fastRemoveCriteriaAnchors || {});
config.requiredParamTemplates = $.extend({}, defaults.requiredParamTemplates, config.requiredParamTemplates || {});
config.categoryTemplates = $.extend({}, defaults.categoryTemplates, config.categoryTemplates || {});
config.modalStyles = $.extend({}, defaults.modalStyles, config.modalStyles || {});
return config;
}
function clonePlainObject(obj) {
return JSON.parse(JSON.stringify(obj || {}));
}
function normalizeMenuLabel(value) {
return String(value || '').replace(/\s+/g, ' ').trim().toLowerCase();
}
function readSettingsOptionState(fallback) {
var base = clonePlainObject(fallback || {});
var stored;
var raw = null;
if (!mw.user || !mw.user.options || typeof mw.user.options.get !== 'function') return base;
stored = mw.user.options.get(settingsOptionName);
if (typeof stored === 'string' && stored.trim()) {
try {
raw = JSON.parse(stored);
} catch (e) {
console.warn('RemoverCore: не удалось прочитать сохранённые настройки полностью, будут использованы данные loader.', e);
}
}
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return base;
return $.extend({}, raw, base, ('quickPhrases' in raw) ? { quickPhrases: raw.quickPhrases } : {});
}
function normalizeQuickPhraseValue(value) {
return String(value == null ? '' : value).replace(/\r\n?/g, '\n').trim();
}
function normalizeQuickPhrasesList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
var normalized = [];
(source || []).forEach(function (value) {
var phrase = normalizeQuickPhraseValue(value);
if (phrase && normalized.indexOf(phrase) === -1) normalized.push(phrase);
});
return normalized;
}
function collectSettingsMenuMeta() {
var labels = [];
var articleLabels = [];
var categoryLabels = [];
var idToLabel = {};
var labelByNorm = {};
var labelOrder = {};
function collect(items, targetLabels) {
items.forEach(function (item) {
var id;
var label;
var normLabel;
if (!item || item.type === 'separator') return;
id = String(item.id || '').trim();
label = String(item.label || '').trim();
normLabel = normalizeMenuLabel(label);
if (id && label && !(id in idToLabel)) idToLabel[id] = label;
if (label && targetLabels.indexOf(label) === -1) targetLabels.push(label);
if (normLabel && !(normLabel in labelByNorm)) {
labelByNorm[normLabel] = label;
labelOrder[label] = labels.length;
labels.push(label);
}
});
}
collect(cfg.articleMenuItems, articleLabels);
collect(cfg.categoryMenuItems, categoryLabels);
return {
labels: labels,
articleLabels: articleLabels,
categoryLabels: categoryLabels,
idToLabel: idToLabel,
labelByNorm: labelByNorm,
labelOrder: labelOrder
};
}
function buildSettingsMenuItemsHint() {
var parts = [];
if (settingsArticleItemLabels.length) {
parts.push('<div class="rmSettingsHintRow"><span class="rmSettingsHintBadge">Статьи</span>' + escapeHtml(settingsArticleItemLabels.join(', ')) + '.</div>');
}
if (settingsCategoryItemLabels.length) {
parts.push('<div class="rmSettingsHintRow"><span class="rmSettingsHintBadge">Категории</span>' + escapeHtml(settingsCategoryItemLabels.join(', ')) + '.</div>');
}
return parts.length ? '<div class="rmSettingsHintList">' + parts.join('') + '</div>' : '';
}
function getDefaultSettings() {
return {
version: settingsVersion,
excludedNamespaces: normalizeNumberList(cfg.excludedNamespaces, []),
notifyAuthor: !!cfg.defaultNotifyAuthor,
subscribeTopic: !!cfg.defaultSubscribeTopic,
menuTitle: (typeof cfg.menuTitle === 'string' && cfg.menuTitle.trim()) ? cfg.menuTitle.trim() : 'Remover',
disabledItems: [],
quickPhrases: normalizeQuickPhrasesList(cfg.quickPhrases, []),
showMenuIcons: !!cfg.showMenuIcons,
signatureSeparator: (typeof cfg.signatureSeparator === 'string') ? cfg.signatureSeparator.trim() : ''
};
}
function normalizeNumberList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
return source
.map(function (value) { return parseInt(value, 10); })
.filter(function (value, index, arr) { return !isNaN(value) && arr.indexOf(value) === index; })
.sort(function (a, b) { return a - b; });
}
function normalizeDisabledItemValue(value) {
var token = String(value || '').trim();
if (!token) return null;
if (settingsItemLabelById[token]) return settingsItemLabelById[token];
return settingsItemLabelByNorm[normalizeMenuLabel(token)] || null;
}
function compareSettingsMenuLabels(a, b) {
var ai = Object.prototype.hasOwnProperty.call(settingsItemLabelOrder, a) ? settingsItemLabelOrder[a] : Number.MAX_SAFE_INTEGER;
var bi = Object.prototype.hasOwnProperty.call(settingsItemLabelOrder, b) ? settingsItemLabelOrder[b] : Number.MAX_SAFE_INTEGER;
if (ai !== bi) return ai - bi;
return a.localeCompare(b, 'ru');
}
function normalizeDisabledItemsList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
return source
.map(normalizeDisabledItemValue)
.filter(function (value, index, arr) { return value && arr.indexOf(value) === index; })
.sort(compareSettingsMenuLabels);
}
function normalizeMenuTitleSetting(value, fallback) {
var menuTitle = String(value || '').trim();
if (!menuTitle) return fallback;
if (menuTitle === MENU_TITLE_PRESET_CACTIONS || /^(#)?p-cactions$/i.test(menuTitle)) return MENU_TITLE_PRESET_CACTIONS;
if (menuTitle === MENU_TITLE_PRESET_PAGE || /^(#)?p-page$/i.test(menuTitle) || /^(#)?p-actions$/i.test(menuTitle)) return MENU_TITLE_PRESET_PAGE;
if (menuTitle === MENU_TITLE_PRESET_TOOLS || /^(#)?p-tb$/i.test(menuTitle)) return MENU_TITLE_PRESET_TOOLS;
return menuTitle;
}
function normalizeRemoverSettings(raw) {
var defaults = clonePlainObject(settingsDefaults);
var source = (raw && typeof raw === 'object') ? raw : {};
return {
version: settingsVersion,
excludedNamespaces: normalizeNumberList(source.excludedNamespaces, defaults.excludedNamespaces || []),
notifyAuthor: ('notifyAuthor' in source) ? !!source.notifyAuthor : !!defaults.notifyAuthor,
subscribeTopic: ('subscribeTopic' in source) ? !!source.subscribeTopic : !!defaults.subscribeTopic,
menuTitle: normalizeMenuTitleSetting(
(typeof source.menuTitle === 'string' && source.menuTitle.trim()) ? source.menuTitle : '',
typeof defaults.menuTitle === 'string' && defaults.menuTitle.trim() ? defaults.menuTitle.trim() : 'Remover'
),
disabledItems: normalizeDisabledItemsList(source.disabledItems, defaults.disabledItems || []),
quickPhrases: normalizeQuickPhrasesList(source.quickPhrases, defaults.quickPhrases || []),
showMenuIcons: ('showMenuIcons' in source) ? !!source.showMenuIcons : !!defaults.showMenuIcons,
signatureSeparator: (typeof source.signatureSeparator === 'string')
? source.signatureSeparator.trim()
: (typeof defaults.signatureSeparator === 'string' ? defaults.signatureSeparator.trim() : '')
};
}
function areRemoverSettingsEqual(a, b) {
return JSON.stringify(normalizeRemoverSettings(a)) === JSON.stringify(normalizeRemoverSettings(b));
}
function updateStoredSettingsState(settings, skipUserOptionsSync) {
var normalized = normalizeRemoverSettings(settings);
state.settings = clonePlainObject(normalized);
setAlert = normalized.notifyAuthor;
setSubscribe = normalized.subscribeTopic;
signatureSeparator = normalized.signatureSeparator;
state.setAlert = setAlert;
state.setSubscribe = setSubscribe;
if (!skipUserOptionsSync && mw.user && mw.user.options && typeof mw.user.options.set === 'function') {
mw.user.options.set(settingsOptionName, JSON.stringify(normalized));
}
return normalized;
}
function splitSettingsInput(value) {
return String(value || '')
.split(/[\s,;]+/)
.map(function (part) { return part.trim(); })
.filter(Boolean);
}
function splitSettingsListInput(value) {
return String(value || '')
.split(/[\n,;]+/)
.map(function (part) { return part.trim(); })
.filter(Boolean);
}
function parseNamespaceInput(value) {
var tokens = splitSettingsInput(value);
var invalid = [];
var values = [];
tokens.forEach(function (token) {
var parsed = parseInt(token, 10);
if (String(parsed) !== token) invalid.push(token);
else if (values.indexOf(parsed) === -1) values.push(parsed);
});
values.sort(function (a, b) { return a - b; });
return { values: values, invalid: invalid };
}
function parseDisabledItemsInput(value) {
var tokens = splitSettingsListInput(value);
var invalid = [];
var values = [];
tokens.forEach(function (token) {
var normalized = normalizeDisabledItemValue(token);
if (!normalized) invalid.push(token);
else if (values.indexOf(normalized) === -1) values.push(normalized);
});
values.sort(compareSettingsMenuLabels);
return { values: values, invalid: invalid };
}
function formatPagesWithAnd(names, prefix) {
var p = prefix || ':';
var links = (names || []).map(function (n) { return '[[' + p + n + ']]'; });
if (!links.length) return '';
if (links.length === 1) return links[0];
return links.slice(0, -1).join(', ') + ' и ' + links[links.length - 1];
}
function formatCatLink(name) { return '[[:Категория:' + name + ']]'; }
function formatMergeStatus(status) {
return { already_exists: 'уже был', updated: 'дополнен', created: 'создан' }[status] || status;
}
function applyGeneratedText($el, generated) {
var cur = $el.val();
var prev = $el.data('rmGenerated') || '';
if (!prev || cur.indexOf(prev) === 0) {
$el.val(generated + cur.slice(prev.length));
} else {
$el.val(generated + (cur ? '\n' + cur : ''));
}
$el.data('rmGenerated', generated);
}
function getCurrentQuickPhrases() {
return normalizeQuickPhrasesList(
state.settings && state.settings.quickPhrases,
settingsDefaults.quickPhrases || []
);
}
function insertTextIntoTextarea($el, text) {
var el = $el && $el[0];
var value;
var start;
var end;
var updatedValue;
var caretPos;
if (!el) return;
text = String(text || '');
if (!text) return;
value = $el.val() || '';
start = typeof el.selectionStart === 'number' ? el.selectionStart : value.length;
end = typeof el.selectionEnd === 'number' ? el.selectionEnd : start;
updatedValue = value.slice(0, start) + text + value.slice(end);
caretPos = start + text.length;
$el.val(updatedValue).trigger('input').trigger('change').focus();
if (typeof el.setSelectionRange === 'function') el.setSelectionRange(caretPos, caretPos);
}
function buildQuickPhrasesPanelHtml(textareaId) {
var phrases = getCurrentQuickPhrases();
if (!phrases.length) return '';
return joinHtml([
'<div class="rmQuickPhrasesPanel ', RESIZE_CLASS, '" data-rm-target="', textareaId, '">',
phrases.map(function (phrase) {
return joinHtml([
'<button type="button" class="rmQuickPhraseActionBtn" data-rm-target="', textareaId,
'" data-rm-phrase="', escapeHtml(phrase), '">',
escapeHtml(phrase),
'</button>'
]);
}).join(''),
'</div>'
]);
}
function getMultiNominationCommentText(commentsByArticle, articleTitle) {
var key = normTitle(articleTitle);
if (!commentsByArticle || !Object.prototype.hasOwnProperty.call(commentsByArticle, key)) return '';
return normalizeQuickPhraseValue(commentsByArticle[key]);
}
function buildMultiNominationText(articles, bodyText, commentsByArticle, options) {
var opts = options || {};
var list = Array.isArray(articles) ? articles.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasArticleComments = false;
var headingLevel = Math.max(2, parseInt(opts.headingLevel, 10) || 3);
var headingMarks = new Array(headingLevel + 1).join('=');
var articleSections = list.map(function (a) {
var comment = getMultiNominationCommentText(commentsByArticle, a);
if (comment) hasArticleComments = true;
return '\n' + headingMarks + ' [[:' + a + ']] ' + headingMarks + '\n' + (comment ? appendNominationSignature(comment) + '\n' : '');
}).join('');
var commonSectionText = body
? appendNominationSignature(body)
: (hasArticleComments ? '' : appendNominationSignature(''));
return articleSections + '\n' + headingMarks + ' По всем ' + headingMarks + '\n' + commonSectionText;
}
function buildMultiNominationListText(pages, bodyText, commentsByPage) {
var list = Array.isArray(pages) ? pages.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasPageComments = false;
var pageLines = list.map(function (pageName) {
var comment = getMultiNominationCommentText(commentsByPage, pageName);
if (comment) hasPageComments = true;
return '* [[:' + pageName + ']]' + (comment ? '\n*: ' + appendNominationSignature(comment) : '');
}).join('\n');
var commonText = body
? appendNominationSignature(body)
: (hasPageComments ? '' : appendNominationSignature(''));
return pageLines + (pageLines && commonText ? '\n' : '') + commonText;
}
function collectMultiNominationComments(normalizePageName) {
var comments = {};
var normalize = typeof normalizePageName === 'function' ? normalizePageName : normTitle;
$('.rmMultiArticleBlock').each(function () {
var $block = $(this);
var article = normalize(($block.find('.rmArticleInput').val() || '').trim());
var comment = normalizeQuickPhraseValue($block.find('.rmArticleCommentInput').val());
if (!article) return;
comments[article] = comment;
});
return comments;
}
function getNominationPublishText(job) {
if (job && job.isMulti) return String(job.msg || '');
return appendNominationSignature(job && job.msg);
}
function getNominationConflictRule(job) {
if (!job || job.mode !== 'nominate') return null;
if (job.opId === 'tRm' || job.opId === 'mRm') {
return {
id: 'ku',
label: 'КУ',
namePattern: '(?:к\\s*удалению|ку)',
detect: function (articleText) {
var match = String(articleText || '').match(/\{\{\s*((?:к\s*удалению|ку))\s*(?:\||\}\})/i);
if (!match) return null;
var templateName = ucfirst(String(match[1] || '').replace(/\s+/g, ' ').trim());
return {
label: 'КУ',
templateName: templateName || 'КУ',
templateDisplay: '{{' + (templateName || 'КУ') + '}}'
};
}
};
}
return null;
}
function detectNominationConflict(articleText, job) {
var rule = getNominationConflictRule(job);
if (!rule || typeof rule.detect !== 'function') return null;
return rule.detect(articleText);
}
function getConflictDecisionForPage(job, pageName) {
var decisions = job && job.conflictDecisions;
var key = normTitle(pageName);
return decisions && decisions[key] ? decisions[key] : null;
}
function getCategoryMergeRe() {
return new RegExp('\\{\\{\\s*(?:' + cfg.categoryTemplates.merge + ')\\s*\\|\\s*([^\\}]+)\\}\\}', 'i');
}
function eachSequential(targets, iteratee, done) {
var i = 0;
(function next(err) {
if (err || i >= targets.length) { done(err || null); return; }
iteratee(targets[i++], next);
}(null));
}
function normalizeSectionForLink(sectionTitle) {
return (sectionTitle || '').trim()
.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, function (_, target, label) {
var v = (label || target || '').trim();
return v.charAt(0) === ':' ? v.slice(1) : v;
})
.replace(/''+/g, '').replace(/\s+/g, ' ').trim();
}
function getViewportWidth() {
return Math.floor(Math.max(
(document.documentElement && document.documentElement.clientWidth) || 0,
(typeof window.innerWidth === 'number' && window.innerWidth) || 0,
$(window).width() || 0
));
}
function getVisualViewportWidth() {
var widths = [];
if (window.visualViewport && typeof window.visualViewport.width === 'number' && window.visualViewport.width > 0) widths.push(window.visualViewport.width);
if (window.screen && window.screen.width > 0) widths.push(window.screen.width);
return widths.length ? Math.floor(Math.min.apply(Math, widths)) : getViewportWidth();
}
function isTouchModalDevice() {
return !!(
(window.matchMedia && window.matchMedia('(pointer: coarse)').matches) ||
('ontouchstart' in window) ||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 0) ||
(navigator.msMaxTouchPoints && navigator.msMaxTouchPoints > 0)
);
}
function getModalLayout() {
var minWidth = parseInt(sz.taMinW, 10) || 180;
var layoutWidth = getViewportWidth();
var visualWidth = getVisualViewportWidth();
var contentWidth = Math.max(minWidth, Math.floor($('#content').width() || $('#content').innerWidth() || $(window).width() || minWidth));
var isMobile = layoutWidth <= sz.mobileBp;
var isTouchDesktop = !isMobile &&
isTouchModalDevice() &&
visualWidth > 0 &&
visualWidth <= sz.mobileBp &&
layoutWidth >= visualWidth + sz.touchDesktopGap;
var useFullWidth = isMobile || isTouchDesktop;
var maxOuterWidth;
var defaultOuterWidth;
var desktopWidth;
if (isMobile) maxOuterWidth = Math.max(minWidth, (visualWidth || contentWidth) - sz.viewportGap);
else if (isTouchDesktop) maxOuterWidth = Math.max(minWidth, contentWidth - 32);
else maxOuterWidth = Math.max(minWidth, Math.min(contentWidth, (visualWidth ? visualWidth - sz.viewportGap : contentWidth)));
desktopWidth = Math.max(minWidth, Math.floor(contentWidth * sz.modalRatio));
if (useFullWidth || desktopWidth < sz.modalMinWide) defaultOuterWidth = contentWidth;
else if (desktopWidth < sz.modalDefaultWide) defaultOuterWidth = sz.modalDefaultWide;
else defaultOuterWidth = desktopWidth;
return {
minWidth: minWidth,
isMobile: isMobile,
isTouchDesktop: isTouchDesktop,
useFullWidth: useFullWidth,
shouldCenter: useFullWidth || mwCfg.skin === 'minerva',
maxOuterWidth: maxOuterWidth,
defaultOuterWidth: Math.min(defaultOuterWidth, maxOuterWidth)
};
}
function getDefaultResizableWidth(frameWidth) {
var layout = getModalLayout();
return Math.max(layout.minWidth, layout.defaultOuterWidth - Math.floor(frameWidth || 0));
}
function getBoxFrameWidth($el) {
function px(prop) {
var n = parseFloat($el.css(prop));
return isNaN(n) ? 0 : n;
}
return px('padding-left') + px('padding-right') + px('border-left-width') + px('border-right-width');
}
// ═══════════════════════════════════════════════════════════════════════════
// API
// ═══════════════════════════════════════════════════════════════════════════
function getApiUrl() {
return (mw.util && typeof mw.util.wikiScript === 'function') ? mw.util.wikiScript('api') : '/w/api.php';
}
function getCsrfTokenValue() {
return (mw.user && mw.user.tokens && typeof mw.user.tokens.get === 'function')
? mw.user.tokens.get('csrfToken')
: null;
}
function storeCsrfToken(token) {
if (!token || !mw.user || !mw.user.tokens || typeof mw.user.tokens.set !== 'function') return;
mw.user.tokens.set({ csrfToken: token });
}
function isValidCsrfToken(token) {
return typeof token === 'string' && !!token && token !== '+\\';
}
function fetchCsrfToken(forceRefresh, callback) {
var cachedToken = getCsrfTokenValue();
if (!forceRefresh && isValidCsrfToken(cachedToken)) {
callback(cachedToken);
return;
}
$.ajax({
url: getApiUrl(),
method: 'GET',
dataType: 'json',
data: { action: 'query', meta: 'tokens', type: 'csrf', format: 'json' }
})
.done(function (data) {
var token = data && data.query && data.query.tokens && data.query.tokens.csrftoken;
if (isValidCsrfToken(token)) {
storeCsrfToken(token);
callback(token);
return;
}
callback(null);
})
.fail(function () {
callback(null);
});
}
function apiReq(params, mode, callback) {
var isWrite = mode === 'edit' || mode === 'discussiontoolssubscribe' || mode === 'options';
function sendRequest(retryWithFreshToken) {
var reqParams = $.extend({}, params, { format: 'json', action: mode });
if (!isWrite) {
$.ajax({ url: getApiUrl(), method: 'GET', data: reqParams, dataType: 'json' })
.done(function (data) { if (callback) callback(data); })
.fail(function (jqXHR, status) {
console.error('Ошибка API: ' + status);
if (callback) callback({ error: { code: 'network', info: status } });
});
return;
}
fetchCsrfToken(!!retryWithFreshToken, function (token) {
if (!isValidCsrfToken(token)) {
if (callback) callback({ error: { code: 'badtoken', info: 'Не удалось получить CSRF-токен.' } });
return;
}
reqParams.token = token;
$.ajax({ url: getApiUrl(), method: 'POST', data: reqParams, dataType: 'json' })
.done(function (data) {
var err = data && data.error;
var isBadToken = err && (err.code === 'badtoken' || /invalid csrf token/i.test(String(err.info || '')));
if (isBadToken && !retryWithFreshToken) {
sendRequest(true);
return;
}
if (callback) callback(data);
})
.fail(function (jqXHR, status) {
console.error('Ошибка API: ' + status);
if (callback) callback({ error: { code: 'network', info: status } });
});
});
}
sendRequest(false);
}
function saveSettingsToServer(settings, callback) {
var normalized = normalizeRemoverSettings(settings);
if (!mwCfg.wgUserName) {
callback({ code: 'notloggedin', info: 'Сохранять настройки можно только после входа в учётную запись.' });
return;
}
apiReq({ optionname: settingsOptionName, optionvalue: JSON.stringify(normalized) }, 'options', function (resp) {
if (resp && resp.options === 'success') {
callback(null, updateStoredSettingsState(normalized));
return;
}
callback((resp && resp.error) || { code: 'options_failed', info: 'Не удалось сохранить настройки.' });
});
}
function resetSettingsOnServer(callback) {
if (!mwCfg.wgUserName) {
callback({ code: 'notloggedin', info: 'Сбрасывать настройки можно только после входа в учётную запись.' });
return;
}
apiReq({ change: settingsOptionName }, 'options', function (resp) {
if (resp && resp.options === 'success') {
callback(null, updateStoredSettingsState(settingsDefaults, true));
return;
}
callback((resp && resp.error) || { code: 'options_failed', info: 'Не удалось сбросить настройки.' });
});
}
function getFirstQueryPage(data) {
var pages = data && data.query && data.query.pages;
if (!pages) return null;
return pages[Object.keys(pages)[0]] || null;
}
function extractRevisionContent(rev) {
if (!rev) return null;
if (typeof rev['*'] === 'string') return rev['*'];
if (rev.slots && rev.slots.main) {
if (typeof rev.slots.main['*'] === 'string') return rev.slots.main['*'];
if (typeof rev.slots.main.content === 'string') return rev.slots.main.content;
}
return null;
}
function makeReadError(apiError, fallbackCode, fallbackInfo) {
var err = apiError || {};
return {
code: err.code || fallbackCode || 'read_failed',
info: err.info || fallbackInfo || 'Не удалось получить содержимое.'
};
}
function getTextWithTimestamp(pageName, callback) {
apiReq({ prop: 'revisions', rvprop: 'content|timestamp', rvslots: 'main', titles: pageName }, 'query', function (data) {
var content;
var page;
if (data && data.error) {
callback(null, null, data.error);
return;
}
if (!data || !data.query || !data.query.pages) {
callback(null, null, { code: 'read_failed', info: 'Некорректный ответ API при чтении страницы.' });
return;
}
page = getFirstQueryPage(data);
if (!page) {
callback(null, null, { code: 'read_failed', info: 'API не вернул данные страницы.' });
return;
}
if (page.invalid !== undefined) {
callback(null, null, { code: 'invalidtitle', info: page.invalidreason || 'Некорректное название страницы.' });
return;
}
if (page.missing !== undefined) {
callback(null, null, null);
return;
}
if (!page.revisions || !page.revisions.length) {
callback(null, null, { code: 'read_failed', info: 'API не вернул ревизии страницы.' });
return;
}
content = extractRevisionContent(page.revisions[0]);
if (content === null) {
callback(null, null, { code: 'content_missing', info: 'API не вернул текст страницы.' });
return;
}
callback(content, page.revisions[0].timestamp || null, null);
});
}
function getText(pageName, callback) {
getTextWithTimestamp(pageName, function (text, baseTimestamp, err) { callback(text, err); });
}
function editPageContent(pageTitle, options, buildFn, callback) {
var opts = options || {};
var maxRetries = Math.max(0, parseInt(opts.editConflictRetries, 10) || 1);
(function attempt(retry) {
getTextWithTimestamp(pageTitle, function (sourceText, baseTimestamp, readErr) {
if (readErr) {
callback(makeReadError(readErr, opts.readErrorCode || 'read_failed', 'Не удалось получить содержимое страницы «' + pageTitle + '».'));
return;
}
if (sourceText === null) {
callback({ code: opts.readErrorCode || 'read_failed', info: opts.readError || 'Страница «' + pageTitle + '» не существует.' });
return;
}
var done = (function () {
var called = false;
return function (result) {
if (called) return;
called = true;
if (!result || result.error) { callback((result && result.error) || { code: 'build_failed', info: 'Не удалось подготовить правку.' }, result && result.meta || null); return; }
if (result.skip) { callback(null, result.meta || null); return; }
if (typeof result.text !== 'string') { callback({ code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
var ep = { title: pageTitle, text: result.text, summary: result.summary || opts.summary || '' };
if (opts.watchlist) ep.watchlist = opts.watchlist;
if (opts.assertuser) ep.assertuser = opts.assertuser;
if (opts.createonly) ep.createonly = opts.createonly;
if (opts.useBaseTimestamp !== false && baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) {
var err = resp && resp.error ? resp.error : null;
if (err && err.code === 'editconflict' && retry < maxRetries) { attempt(retry + 1); return; }
callback(err, result.meta || null);
});
};
}());
var maybe = buildFn(sourceText, done);
if (maybe !== undefined) done(maybe);
});
}(0));
}
// ═══════════════════════════════════════════════════════════════════════════
// ШАБЛОНЫ: удаление и вставка
// ═══════════════════════════════════════════════════════════════════════════
function findBalancedTemplateEnd(text, start) {
var depth = 0;
var i = start;
var len = text.length;
while (i < len - 1) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') {
depth++;
i += 2;
continue;
}
if (text.charAt(i) === '}' && text.charAt(i + 1) === '}') {
depth--;
i += 2;
if (depth === 0) return i;
continue;
}
i++;
}
return -1;
}
function splitTemplateTopLevelParts(innerText) {
var parts = [];
var start = 0;
var templateDepth = 0;
var linkDepth = 0;
var i = 0;
var text = String(innerText || '');
while (i < text.length) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') {
templateDepth++;
i += 2;
continue;
}
if (text.charAt(i) === '}' && text.charAt(i + 1) === '}' && templateDepth > 0) {
templateDepth--;
i += 2;
continue;
}
if (text.charAt(i) === '[' && text.charAt(i + 1) === '[') {
linkDepth++;
i += 2;
continue;
}
if (text.charAt(i) === ']' && text.charAt(i + 1) === ']' && linkDepth > 0) {
linkDepth--;
i += 2;
continue;
}
if (text.charAt(i) === '|' && templateDepth === 0 && linkDepth === 0) {
parts.push(text.slice(start, i));
start = i + 1;
}
i++;
}
parts.push(text.slice(start));
return parts;
}
function getTemplateMatchAt(text, start, nameRe) {
var end = findBalancedTemplateEnd(text, start);
var parts;
var rawName;
var name;
if (end < 0) return null;
parts = splitTemplateTopLevelParts(text.slice(start + 2, end - 2));
rawName = String(parts.shift() || '').trim();
name = rawName.replace(/^(?:subst|подст)\s*:\s*/i, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
if (!nameRe.test(name)) return null;
return {
start: start,
end: end,
text: text.slice(start, end),
name: rawName,
params: parts.map(function (part) { return part.trim(); })
};
}
function findTemplateByPattern(text, namePattern) {
var source = String(text || '');
var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i');
var i = 0;
var end;
var match;
while (i < source.length - 1) {
if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') {
match = getTemplateMatchAt(source, i, nameRe);
if (match) return match;
end = findBalancedTemplateEnd(source, i);
if (end > i) { i = end; continue; }
}
i++;
}
return null;
}
function getTemplateRemovalRange(text, match) {
var start = match.start;
var end = match.end;
var before = text.slice(0, start).match(/<noinclude>\s*$/i);
var after;
if (before) {
after = text.slice(end).match(/^\s*<\/noinclude>\s*\n?/i);
if (after) {
return { start: before.index, end: end + after[0].length };
}
}
after = text.slice(end).match(/^[ \t]*(?:\r?\n)?/);
if (after) end += after[0].length;
return { start: start, end: end };
}
function stripTemplatesByPattern(text, namePattern) {
var source = String(text || '');
var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i');
var ranges = [];
var out = [];
var pos = 0;
var i = 0;
var end;
var match;
var range;
while (i < source.length - 1) {
if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') {
match = getTemplateMatchAt(source, i, nameRe);
if (match) {
range = getTemplateRemovalRange(source, match);
ranges.push(range);
i = Math.max(match.end, range.end);
continue;
}
end = findBalancedTemplateEnd(source, i);
if (end > i) { i = end; continue; }
}
i++;
}
if (!ranges.length) return { text: source, removed: false };
ranges.forEach(function (r) {
if (r.start < pos) r.start = pos;
out.push(source.slice(pos, r.start));
pos = r.end;
});
out.push(source.slice(pos));
return { text: out.join(''), removed: true };
}
function removeTemplatesByAliases(text, aliases) {
var seen = {}, patterns = [];
aliases.forEach(function (alias) {
var tpl = alias.replace(RE_TEMPLATE_NS, '').trim();
var key = tpl.toLowerCase();
if (!tpl || seen[key]) return;
seen[key] = true;
patterns.push(escapeRegExp(tpl).replace(/\\ /g, '[ _]+'));
});
if (!patterns.length) return { text: text, removed: false };
return stripTemplatesByPattern(text, '(?:' + patterns.join('|') + ')');
}
function removeTransferTemplatesLocal(articleText, transferMode) {
var result = { text: articleText, removedKbu: false, removedKul: false, removedHangon: false };
if (transferMode === 'none') return result;
if (transferMode === 'kbu' || transferMode === 'both') {
var kbu = stripTemplatesByPattern(result.text, '(?:' + KBU_PATTERN_STR + ')');
result.text = kbu.text; result.removedKbu = kbu.removed;
}
if (transferMode === 'kul' || transferMode === 'both') {
var kul = stripTemplatesByPattern(result.text, KUL_PATTERN_STR);
result.text = kul.text; result.removedKul = kul.removed;
}
var hangon = stripTemplatesByPattern(result.text, HANGON_PATTERN_STR);
result.text = hangon.text; result.removedHangon = hangon.removed;
return result;
}
function removeTransferTemplatesWithApiFallback(pageName, articleText, transferMode, localResult, callback) {
var needKbu = (transferMode === 'kbu' || transferMode === 'both') && !localResult.removedKbu;
var needKul = (transferMode === 'kul' || transferMode === 'both') && !localResult.removedKul;
var needHangon = transferMode !== 'none' && !localResult.removedHangon;
if (!needKbu && !needKul && !needHangon) { callback(localResult); return; }
var titleMap = {};
if (needKbu) ['Шаблон:К быстрому удалению','Шаблон:К отсроченному удалению','Шаблон:Deleteslow','Шаблон:Ds'].forEach(function (t) { titleMap[t] = true; });
if (needKul) titleMap['Шаблон:К улучшению'] = true;
if (needHangon) { titleMap['Шаблон:Hangon'] = true; titleMap['Шаблон:Hang-on'] = true; }
apiReq({ prop: 'templates', titles: pageName, tllimit: 'max' }, 'query', function (data) {
var page = getFirstQueryPage(data);
if (page && page.templates) {
page.templates.forEach(function (tpl) {
var norm = normalizeTemplateName(tpl.title);
if ((needKbu && (RE_KBU_PATTERNS.test(norm) || norm === 'к быстрому удалению' || norm === 'к отсроченному удалению' || norm === 'deleteslow' || norm === 'ds')) ||
(needKul && RE_KUL_PATTERN.test(norm)) ||
(needHangon && RE_HANGON.test(norm))) {
titleMap[tpl.title] = true;
}
});
}
var titles = Object.keys(titleMap);
if (!titles.length) { callback(localResult); return; }
function normalizeAliasKey(title) { return (title || '').replace(/_/g, ' ').toLowerCase().trim(); }
function collectAndApplyAliases() {
var allAliases = [];
titles.forEach(function (title) { allAliases = allAliases.concat(tplAliasCache[title] || [title]); });
var dedup = {}, aliases = [];
allAliases.forEach(function (alias) {
var key = normalizeAliasKey(alias);
if (!key || dedup[key]) return;
dedup[key] = true; aliases.push(alias);
});
var updated = $.extend({}, localResult);
var r = removeTemplatesByAliases(updated.text, aliases);
updated.text = r.text;
if (r.removed) {
if (needKbu) updated.removedKbu = true;
if (needKul) updated.removedKul = true;
if (needHangon) updated.removedHangon = true;
}
callback(updated);
}
var missingTitles = titles.filter(function (t) { return !tplAliasCache[t]; });
if (!missingTitles.length) { collectAndApplyAliases(); return; }
(function resolveChunk(offset) {
if (offset >= missingTitles.length) { collectAndApplyAliases(); return; }
var chunk = missingTitles.slice(offset, offset + 20);
var chunkByKey = {};
chunk.forEach(function (t) { chunkByKey[normalizeAliasKey(t)] = t; });
apiReq({ prop: 'redirects', rdlimit: 'max', titles: chunk.join('|') }, 'query', function (resp) {
var pages = resp && resp.query && resp.query.pages ? resp.query.pages : {};
Object.keys(pages).forEach(function (pid) {
var p = pages[pid];
if (!p || !p.title) return;
var sourceTitle = chunkByKey[normalizeAliasKey(p.title)];
if (!sourceTitle) return;
var found = [p.title].concat((p.redirects || []).map(function (r) { return r.title; }));
var seen = {}, unique = [];
found.forEach(function (t) { var k = normalizeAliasKey(t); if (!k || seen[k]) return; seen[k] = true; unique.push(t); });
tplAliasCache[sourceTitle] = unique.length ? unique : [sourceTitle];
});
chunk.forEach(function (t) { if (!tplAliasCache[t]) tplAliasCache[t] = [t]; });
resolveChunk(offset + 20);
});
}(0));
});
}
// ─── Вставка шаблонов ────────────────────────────────────────────────────
function findInsertPositionAfterProjectTemplates(text) {
var pos = 0, len = text.length;
while (pos < len) {
var wsMatch = text.slice(pos).match(/^[\t ]*\n/);
if (wsMatch) { pos += wsMatch[0].length; continue; }
if (text.charAt(pos) !== '{' || text.charAt(pos + 1) !== '{') break;
var afterOpen = text.slice(pos + 2);
var nameMatch = afterOpen.match(/^[\s]*([\s\S]*?)[\s]*(?:\||\}\})/);
var templateEnd;
if (!nameMatch) break;
var tplName = nameMatch[1].toLowerCase().replace(/\s+/g, ' ').trim();
if (!/^статья проекта(\s|$)/.test(tplName) && tplName !== 'блок проектов статьи') break;
templateEnd = findBalancedTemplateEnd(text, pos);
if (templateEnd < 0) break;
pos = templateEnd;
if (pos < len && text.charAt(pos) === '\n') pos++;
}
return pos;
}
function insertTplOnTalkPage(text, tplText, sep) {
var s = (sep === undefined) ? '\n' : sep;
var insertPos = findInsertPositionAfterProjectTemplates(text);
if (insertPos === 0) return tplText + (text.length ? s + text.replace(/^\n+/, '') : '');
var before = text.slice(0, insertPos).replace(/\n+$/, '');
var after = text.slice(insertPos).replace(/^\n+/, '');
return before + '\n' + tplText + (after.length ? s + after : '');
}
function wrapInNoinclude(text, templateText) {
var match = text.match(RE_NOINCLUDE);
if (match) {
// Если перед noinclude есть непробельный контент — вставляем новый noinclude сверху
var before = text.slice(0, text.indexOf(match[0]));
if (/\S/.test(before)) {
return '<noinclude>' + templateText + '</noinclude>\n' + text;
}
var content = match[2];
if (content.length > 0 && content.charAt(content.length - 1) !== '\n') content += '\n';
return text.replace(match[0], match[1] + '<noinclude>' + content + templateText + '\n</noinclude>');
}
return '<noinclude>' + templateText + '</noinclude>\n' + text;
}
function upsertRetTemplateOnTalkPage(text, dateIso, sectionTitle) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var tplRe = /\{\{\s*(?:subst\s*:\s*)?(?:оставлено)\s*([^}]*)\}\}/i;
var tplMatch = source.match(tplRe);
function buildTpl(dateValue, sectionValue) {
var tpl = 'оставлено|' + dateValue;
if (sectionValue) tpl += '|l1=' + sectionValue;
return T_OPEN + tpl + T_CLOSE;
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, buildTpl(dateIso, normalizedSection), '\n'), status: 'created' };
}
var parts = tplMatch[1].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); });
var stdDate = convertToStandardDate(dateIso);
if (existingDates.indexOf(stdDate) !== -1) return { text: source, status: 'already_present' };
var nextIdx = existingDates.length + 1;
var suffix = '|' + dateIso + (normalizedSection ? '|l' + nextIdx + '=' + normalizedSection : '');
return {
text: source.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}\s*$/, suffix + '}}'); }),
status: 'updated'
};
}
function buildConditionalRetTemplateText(dateIso, sectionTitle, reasonText, deadlineText, sectionIndex) {
var normalizedSection = String(sectionTitle || '').trim();
var normalizedReason = normalizeQuickPhraseValue(reasonText);
var normalizedDeadline = String(deadlineText || '').trim();
var index = parseInt(sectionIndex, 10);
var tpl = 'условно оставлено|' + dateIso;
if (isNaN(index) || index < 1) index = 1;
if (normalizedSection) tpl += '|l' + index + '=' + normalizedSection;
if (normalizedReason) tpl += '|пояснение=' + normalizedReason;
if (normalizedDeadline) tpl += '|срок=' + normalizedDeadline;
return T_OPEN + tpl + T_CLOSE;
}
function upsertConditionalRetTemplateOnTalkPage(text, dateIso, sectionTitle, reasonText, deadlineText) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var normalizedReason = normalizeQuickPhraseValue(reasonText);
var normalizedDeadline = String(deadlineText || '').trim();
var tplRe = /\{\{\s*(?:subst\s*:\s*)?(?:условно\s*оставлено)\s*([^}]*)\}\}/i;
var tplMatch = source.match(tplRe);
if (!tplMatch) {
return {
text: insertTplOnTalkPage(source, buildConditionalRetTemplateText(dateIso, normalizedSection, normalizedReason, normalizedDeadline, 1), '\n'),
status: 'created'
};
}
var parts = tplMatch[1].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); });
var stdDate = convertToStandardDate(dateIso);
if (existingDates.indexOf(stdDate) !== -1) return { text: source, status: 'already_present' };
var nextIdx = existingDates.length + 1;
var suffix = '|' + dateIso;
if (normalizedSection) suffix += '|l' + nextIdx + '=' + normalizedSection;
if (normalizedReason) suffix += '|пояснение=' + normalizedReason;
if (normalizedDeadline) suffix += '|срок=' + normalizedDeadline;
return {
text: source.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}\s*$/, suffix + '}}'); }),
status: 'updated'
};
}
// ═══════════════════════════════════════════════════════════════════════════
// ПАЙПЛАЙН НОМИНАЦИИ
// ═══════════════════════════════════════════════════════════════════════════
function runNominationPipeline(steps) {
var s = steps;
var ctx = { templateMeta: null, nominationInfo: null };
var stages = [
{
name: 'шаблон',
fn: function (next) {
s.templateStep(function (err, meta) { ctx.templateMeta = meta || null; next(err); });
}
},
{
name: 'номинация',
pendingText: 'Публикуется номинация...',
successText: 'Номинация опубликована.',
errorText: 'Публикация номинации.',
fn: function (next) {
s.nominationStep(function (err, info) { ctx.nominationInfo = info || null; next(err); });
}
},
{
name: 'подписка',
shouldRun: function () {
var info = ctx.nominationInfo;
return !!(setSubscribe && info && info.pageTitle && info.sectionTitle);
},
fn: function (next) {
subscribeToTopic(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle, function () { next(); });
}
},
{
name: 'оповещение',
shouldRun: function () { return !!(setAlert && !s.skipNotify); },
fn: function (next) { s.notifyStep(ctx.nominationInfo, next); }
}
];
(function run(i) {
if (i >= stages.length) { if (typeof s.onSuccess === 'function') s.onSuccess(ctx); return; }
var stage = stages[i];
if (typeof stage.shouldRun === 'function' && !stage.shouldRun()) { run(i + 1); return; }
var statusId = stage.pendingText
? logStatus(stage.pendingText, null, { pending: true, trackError: false })
: null;
try {
stage.fn(function (err) {
if (err) {
if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), err, { statusId: statusId });
if (typeof s.onFailure === 'function') s.onFailure(stage.name, err, ctx);
else markSubmitError();
return;
}
if (statusId && stage.successText) logStatus(stage.successText, null, { statusId: statusId, trackError: false });
run(i + 1);
});
} catch (ex) {
var exErr = { code: 'exception', info: (ex && ex.message) ? ex.message : String(ex) };
if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), exErr, { statusId: statusId });
if (typeof s.onFailure === 'function') s.onFailure(stage.name, exErr, ctx);
else markSubmitError();
}
}(0));
}
// ─── Публикация номинации ────────────────────────────────────────────────
function publishNomination(opts, callback) {
var cb = callback || function () {};
function doPublish() {
apiReq({
title: opts.pageTitle,
section: 'new',
sectiontitle: opts.sectionTitle,
summary: opts.summary,
text: opts.text,
assertuser: mwCfg.wgUserName
}, 'edit', function (resp) {
cb(resp && resp.error ? resp.error : null);
});
}
if (opts.sectionTitle) {
if (!opts.navTemplate) { doPublish(); return; }
apiReq({ title: opts.pageTitle, createonly: '1', text: T_OPEN + opts.navTemplate + '-Навигация' + T_CLOSE + '\n', summary: makeSummary('автоматическая шапка'), assertuser: mwCfg.wgUserName },
'edit', function (resp) {
if (resp && resp.error && resp.error.code !== 'articleexists') { cb(resp.error); return; }
doPublish();
});
return;
}
// Вставка в существующую страницу
editPageContent(opts.pageTitle, { summary: opts.summary, readError: opts.readErrorMessage || 'Не удалось получить содержимое.' },
function (pageText) { return opts.buildText ? opts.buildText(pageText) : null; },
function (err) { cb(err || null); }
);
}
// ─── Оповещение авторов ──────────────────────────────────────────────────
function notifyAuthor(pg, options, callback) {
var opts = options || {};
var cb = callback || function () {};
var actionText = (typeof opts.actionText === 'string') ? opts.actionText : '';
var discussionPage = (typeof opts.discussionPage === 'string') ? opts.discussionPage : '';
var discussionSection = normalizeSectionForLink((typeof opts.discussionSection === 'string') ? opts.discussionSection : '');
var includeProposed = (typeof opts.includeProposedPrefix === 'boolean') ? opts.includeProposedPrefix : true;
var actionPhrase = ((includeProposed ? 'предложена ' : '') + actionText).trim() || 'изменена';
var discussionText = discussionPage ? 'Обсуждение — на странице [[' + discussionPage + (discussionSection ? '#' + discussionSection : '') + ']]. ' : '';
apiReq({ prop: 'revisions', rvprop: 'user', rvdir: 'newer', titles: pg }, 'query', function (queryResp) {
var page = getFirstQueryPage(queryResp);
if (!page) { cb({ code: 'network', info: 'Network error' }); return; }
if (page.missing !== undefined) { cb({ code: 'missing', info: 'Page missing.' }); return; }
if (!page.revisions || !page.revisions.length) { cb({ code: 'no_revisions', info: 'No revisions.' }); return; }
var rv = page.revisions[0];
if ('anon' in rv || rv.userhidden || !rv.user || rv.user === mwCfg.wgUserName) { cb(null); return; }
apiReq({
title: 'User talk:' + rv.user, section: 'new',
sectiontitle: 'Remover: [[:' + pg + ']]',
summary: opts.summary || makeSummary('уведомление автора'),
text: 'Страница [[:' + pg + ']], созданная вами, ' + actionPhrase + '. ' +
discussionText +
'~~' + '~~<br><small>Это автоматическое уведомление, сгенерированное ' + scriptLink + '.</small>',
assertuser: mwCfg.wgUserName
}, 'edit', function (editResp) { cb(editResp && editResp.error ? editResp.error : null); });
});
}
function notifyAuthorsForPages(pages, notifyOptions, callback) {
var cb = callback || function () {};
var opts = notifyOptions || {};
var list = [];
(pages || []).forEach(function (p) { var t = normTitle(p); if (t && list.indexOf(t) === -1) list.push(t); });
if (!list.length) { cb(); return; }
eachSequential(list, function (pg, next) {
var pageLink = buildQuotedStatusPageLink(pg);
var statusId = logStatus('Отправляется уведомление создателю страницы ' + pageLink + '...', null, { pending: true, trackError: false });
notifyAuthor(pg, opts, function (err) {
logStatus(err ? 'Уведомление создателя страницы ' + pageLink + '.' : 'Создатель страницы ' + pageLink + ' уведомлён.', err,
{ statusId: statusId, trackError: opts.trackError !== false });
next();
});
}, cb);
}
// ─── Подписка на раздел ──────────────────────────────────────────────────
function subscribeToTopic(pageTitle, sectionTitle, callback) {
var cb = callback || function () {};
if (!setSubscribe || !sectionTitle) { cb(); return; }
var statusId = logStatus('Оформляется подписка на раздел...', null, { pending: true, trackError: false });
var targetFrag = normalizeSectionForLink(sectionTitle).toLowerCase();
function finish(err, st) {
if (err) { logStatus('Не удалось подписаться на раздел.', err, { statusId: statusId, trackError: false }); cb(); return; }
logStatus(st === 'subscribed' ? 'Оформлена подписка на раздел.' : 'Раздел для подписки не найден.', null, { statusId: statusId, trackError: false });
cb();
}
function trySubscribe(attemptsLeft) {
apiReq({ page: pageTitle, prop: 'threaditemshtml', excludesignatures: true }, 'discussiontoolspageinfo', function (data) {
var items = (data && data.discussiontoolspageinfo && data.discussiontoolspageinfo.threaditemshtml) || null;
if (!items || !items.length) {
if (attemptsLeft > 0) { setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000); return; }
finish(null, 'not_found'); return;
}
var commentname = null;
for (var ti = items.length - 1; ti >= 0; ti--) {
var t = items[ti];
if (t.type === 'heading') {
var htext = (t.headingText || t.html || '').replace(/<[^>]+>/g, '').trim();
if (htext.toLowerCase() === targetFrag || normalizeSectionForLink(htext).replace(/_/g, ' ').toLowerCase() === targetFrag) {
commentname = t.name; break;
}
}
}
if (!commentname) {
if (attemptsLeft > 0) setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000);
else finish(null, 'not_found');
return;
}
apiReq({ page: pageTitle, commentname: commentname, subscribe: '1' }, 'discussiontoolssubscribe', function (res) {
finish(res && res.error ? res.error : null, 'subscribed');
});
});
}
setTimeout(function () { trySubscribe(2); }, 1500);
}
// ═══════════════════════════════════════════════════════════════════════════
// UI: модальные окна
// ═══════════════════════════════════════════════════════════════════════════
function syncModalLayout() {
var syncFn = $('#removerModal').data('rmSyncLayout');
if (typeof syncFn === 'function') syncFn();
}
function clearModalLayoutSyncHandlers() {
modalLayoutSyncHandlers = [];
$('#removerModal').removeData('rmSyncLayout');
}
function registerModalLayoutSync(handler) {
if (typeof handler !== 'function') return;
if (modalLayoutSyncHandlers.indexOf(handler) === -1) modalLayoutSyncHandlers.push(handler);
$('#removerModal').data('rmSyncLayout', function () {
modalLayoutSyncHandlers.slice().forEach(function (fn) {
if (typeof fn === 'function') fn();
});
});
}
function registerResizeObserver(observer) {
if (observer) resizeObservers.push(observer);
}
function resetModalObservers() {
resizeObservers.forEach(function (observer) {
if (observer && typeof observer.disconnect === 'function') observer.disconnect();
});
resizeObservers = [];
clearModalLayoutSyncHandlers();
$(window).off('resize.removerModal');
$(window).off('.rmTaResize');
}
function closeModal() {
resetModalObservers();
$(window).off('keydown.remover');
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
}
function ensureModalStyles() {
if (document.getElementById('removerModalDynamicStyles')) return;
var progH = 'background:' + tk.bgProgH + '!important;border-color:' + tk.bProgH + '!important;color:' + tk.cInv + '!important;';
var neutH = 'background:' + tk.bgN + '!important;border-color:' + tk.bSub + '!important;color:inherit!important;';
var succH = 'background:' + tk.bgSuccH+ '!important;border-color:' + tk.bSuccH + '!important;color:' + tk.cInv + '!important;';
var pillBg = 'linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%)';
var pillShadow = '0 1px 0 rgba(255,255,255,.08) inset,0 1px 2px rgba(0,0,0,.18)';
var activePillShadow = '0 1px 0 rgba(255,255,255,.14) inset,0 2px 6px rgba(51,102,204,.24)';
var css = [
'#removerModal,#removerModal *{-moz-text-size-adjust:none!important;-webkit-text-size-adjust:100%!important;text-size-adjust:100%!important}',
'#removerModal{color:inherit}',
'#removerModal input::placeholder,#removerModal textarea::placeholder{color:var(--color-subtle,currentColor);opacity:.7}',
'#removerModal input[type="checkbox"],#removerModal input[type="radio"]{appearance:auto;-webkit-appearance:auto;-moz-appearance:auto;accent-color:auto}',
'#removerModal input[type="checkbox"]{outline:none!important;box-shadow:none!important}',
'#removerModal button{transition:background-color .12s ease,border-color .12s ease,color .12s ease,box-shadow .12s ease,filter .12s ease,transform .06s ease}',
'#removerModal .rmToggleBtn{background:' + tk.bgNSub + '!important;border-color:' + tk.bSub + '!important;color:inherit!important}',
'#removerModal .rmToggleBtn.is-active{background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important}',
'#removerModal button:not(:disabled):hover{filter:brightness(.97)}',
'#removerModal button:not(:disabled):active{transform:translateY(1px)}',
'#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):hover,#removerModal .rmToggleBtn.is-active:hover{' + progH + 'filter:none}',
'#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):active,#removerModal .rmToggleBtn.is-active:active{' + progH + 'filter:brightness(.92)!important}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError){background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important;outline:none!important;box-shadow:0 0 0 6px rgba(51,102,204,.13),0 1px 2px rgba(0,0,0,.08)!important}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):hover{' + progH + 'box-shadow:0 0 0 7px rgba(51,102,204,.16),0 1px 2px rgba(0,0,0,.1)!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):active{' + progH + 'box-shadow:0 0 0 5px rgba(51,102,204,.14),0 1px 2px rgba(0,0,0,.08)!important;filter:brightness(.92)!important}',
'#removerModal .rmArticleCommentToggle.is-active{background:#bfc4ca!important;border-color:#8f98a3!important;color:#202122!important}',
'#removerModal .rmArticleCommentToggle.is-active:hover{background:#b4bac1!important;border-color:#848e99!important;color:#202122!important;filter:none}',
'#removerModal .rmArticleCommentToggle.is-active:active{background:#a9b0b8!important;border-color:#79838f!important;color:#202122!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitError:not(:disabled):hover{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitError:not(:disabled):active{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:brightness(.88)!important}',
'#removerModal #removerReload:not(:disabled):hover{' + succH + 'filter:none}',
'#removerModal #removerReload:not(:disabled):active{' + succH + 'filter:brightness(.92)!important}',
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):hover,#removerModal .rmToggleBtn:not(.is-active):hover{' + neutH + 'filter:none}',
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):active{' + neutH + 'filter:brightness(.92)!important}',
'#removerModal a.removerModalLink{color:' + tk.cProg + ';text-decoration:none;border-bottom:0;box-shadow:none;word-break:break-word;overflow-wrap:anywhere}',
'#removerModal a.removerModalLink:hover,#removerModal a.removerModalLink:focus{color:' + tk.cProgH + ';text-decoration:underline}',
'#removerModal #removerModalSubtitle{font-size:12px!important;line-height:1.35!important;font-weight:400!important;color:' + tk.cSubM + '!important;max-width:100%;overflow-wrap:anywhere;word-break:break-word}',
'#removerModal #removerModalSubtitle a{font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important}',
'#removerModal a.rmButtonLikeLink{color:' + tk.cBase + '!important;text-decoration:none!important;transform:none!important;transition:background-color .12s ease,border-color .12s ease,color .12s ease,filter .12s ease,transform .06s ease!important}',
'#removerModal a.rmButtonLikeLink:hover{' + neutH + 'text-decoration:none!important;transform:none!important}',
'#removerModal a.rmButtonLikeLink:focus{text-decoration:none!important}',
'#removerModal a.rmButtonLikeLink:focus:not(:focus-visible){outline:none!important}',
'#removerModal a.rmButtonLikeLink:focus-visible{outline:2px solid ' + tk.bProg + '!important;outline-offset:2px;text-decoration:none!important}',
'#removerModal a.rmButtonLikeLink:hover:active{' + neutH + 'filter:brightness(.92)!important;transform:translateY(1px)!important;text-decoration:none!important}',
'#removerModal .rmInfoBox{margin:0 0 10px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgNSub + '}',
'#removerModal .rmActionList{display:flex;flex-direction:column;gap:6px}',
'#removerModal .rmActionItem{display:block;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgBase + ';cursor:pointer;transition:background-color .12s ease,transform .06s ease}',
'#removerModal .rmActionItem:hover{background:' + tk.bgNSub + '}',
'#removerModal .rmActionItem:active{transform:translateY(1px)}',
'#removerModal .rmActionMain{display:flex;align-items:center}',
'#removerModal .rmActionMain input[type="radio"]{margin-right:8px}',
'#removerModal .rmActionMeta{display:block;margin-left:24px;margin-top:2px;color:' + tk.cSubM + ';font-size:12px;line-height:1.35}',
'#removerModal .rmConflictLead{margin:0 0 10px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}',
'#removerModal .rmConflictList{display:flex;flex-direction:column;gap:10px}',
'#removerModal .rmConflictCard{padding:12px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}',
'#removerModal .rmConflictCard.is-skip{background:' + tk.bgNSub + ';border-color:' + tk.bSubS + '}',
'#removerModal .rmConflictTitle{font-size:14px;font-weight:700;line-height:1.4;color:' + tk.cBase + ';word-break:break-word;overflow-wrap:anywhere}',
'#removerModal .rmConflictMeta{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmConflictGroup{margin-top:10px}',
'#removerModal .rmConflictGroupTitle{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmConflictButtons{display:flex;flex-wrap:wrap;gap:6px}',
'#removerModal .rmConflictChoice{padding:5px 10px}',
'#removerModal .rmConflictChoice.is-disabled,#removerModal .rmConflictButtons.is-disabled .rmConflictChoice{opacity:.55;cursor:not-allowed;pointer-events:none}',
'#removerModal .rmConflictHint{margin-top:8px;font-size:11px;line-height:1.45;color:' + tk.cSub + '}',
'#removerModal #rmSettingsForm{display:flex;flex-direction:column;gap:14px}',
'#removerModal .rmSettingsLead{margin:-2px 0 2px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}',
'#removerModal .rmSettingsSection{margin:0;padding:14px 16px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.7) inset}',
'#removerModal .rmSettingsSectionHeader{margin:0 0 12px}',
'#removerModal .rmSettingsSectionTitle{font-size:14px;font-weight:700;line-height:1.35;color:' + tk.cBase + '}',
'#removerModal .rmSettingsSectionDescription{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmSettingsField{display:block;margin:0 0 10px;padding:12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}',
'#removerModal .rmSettingsField:last-child{margin-bottom:0}',
'#removerModal .rmSettingsFieldLabel{display:block;font-size:13px;font-weight:700;line-height:1.35;margin:0 0 8px;color:' + tk.cBase + '}',
'#removerModal .rmSettingsFieldControl{display:block;min-width:0}',
'#removerModal .rmSettingsFieldControl input{margin-bottom:0!important}',
'#removerModal .rmSettingsFieldControl input[type="text"]{min-height:38px;border-radius:6px}',
'#removerModal .rmSettingsFieldHint{margin-top:8px;font-size:12px;line-height:1.55;color:' + tk.cSubM + ';overflow-wrap:anywhere}',
'#removerModal .rmSettingsChecks{display:flex;flex-direction:column;gap:8px}',
'#removerModal .rmSettingsCheck{display:inline-flex;align-items:flex-start;gap:8px;font-size:14px;line-height:1.45;color:' + tk.cBase + '}',
'#removerModal .rmSettingsCheck input[type="checkbox"]{margin:3px 0 0;flex-shrink:0}',
'#removerModal .rmSettingsMenuPresetWrap{margin-top:10px}',
'#removerModal .rmSettingsMenuPresetLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmSegmentedBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmSettingsMenuPresetBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmSegmentedBtn,#removerModal .rmSettingsMenuPresetBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}',
'#removerModal .rmSegmentedBtn.is-active,#removerModal .rmSettingsMenuPresetBtn.is-active{background:' + tk.bgProg + ';border-color:' + tk.bProg + ';color:' + tk.cInv + ';box-shadow:' + activePillShadow + '}',
'#removerModal.rmModalSettings{border:1px solid ' + tk.bSub + '!important;background:' + tk.bgBase + '!important;border-radius:12px!important;box-shadow:0 14px 32px rgba(0,0,0,.08),0 1px 0 rgba(255,255,255,.78) inset!important}',
'#removerModal.rmModalSettings #removerModalHeaderBar{margin-bottom:14px;padding-bottom:10px;border-bottom:2px solid ' + tk.bSub + '}',
'#removerModal.rmModalSettings #removerModalSubtitle{margin:-2px 0 12px!important;color:' + tk.cSubM + '!important}',
'#removerModal.rmModalSettings #rmSettingsForm{gap:18px}',
'#removerModal.rmModalSettings .rmSettingsLead{margin:0;padding:0 2px;color:' + tk.cSubM + '}',
'#removerModal.rmModalSettings .rmSettingsSection{padding:16px 18px;border:1px solid ' + tk.bSub + ';border-radius:12px;background:linear-gradient(180deg,' + tk.bgNSub + ' 0%,' + tk.bgBase + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.82) inset,0 8px 18px rgba(0,0,0,.035)}',
'#removerModal.rmModalSettings .rmSettingsSectionHeader{margin:0 0 6px;padding-bottom:0;border-bottom:0}',
'#removerModal.rmModalSettings .rmSettingsSectionTitle{font-size:16px;line-height:1.3}',
'#removerModal.rmModalSettings .rmSettingsSectionDescription{margin-top:5px;max-width:none}',
'#removerModal.rmModalSettings .rmSettingsField{margin:14px 0 0;padding:12px 0 0;border:0;border-top:1px solid ' + tk.bSubS + ';border-radius:0;background:transparent;box-shadow:none}',
'#removerModal.rmModalSettings .rmSettingsField:first-child{margin-top:0;padding-top:0;border-top:0}',
'#removerModal.rmModalSettings .rmSettingsFieldLabel{margin:0 0 6px}',
'#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]{min-height:40px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}',
'#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]:focus{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.16);outline:none}',
'#removerModal.rmModalSettings .rmSettingsFieldHint{margin-top:6px;max-width:none}',
'#removerModal.rmModalSettings .rmSettingsChecks{gap:10px}',
'#removerModal.rmModalSettings .rmSettingsCheck{padding:4px 0}',
'#removerModal.rmModalSettings .rmSettingsMenuPresetWrap{margin-top:12px;padding-top:10px;border-top:1px dashed ' + tk.bSubS + '}',
'#removerModal.rmModalSettings .rmSettingsHintList{width:100%;max-width:100%;box-sizing:border-box;margin-top:10px;padding:10px 12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + '}',
'#removerModal.rmModalSettings .rmSettingsHintRow{line-height:1.55}',
'#removerModal.rmModalSettings .rmSettingsHintBadge{background:' + tk.bgNSub + '}',
'#removerModal.rmModalSettings .rmQuickPhraseEditor{padding-top:2px}',
'#removerModal.rmModalSettings .rmQuickPhraseMeta{min-height:18px}',
'#removerModal.rmModalSettings #rmSettingsSignaturePreviewCode{display:inline-block;padding:2px 8px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}',
'#removerModal.rmModalSettings #rmFooterActionButtons{position:relative;flex:0 0 auto!important;display:flex!important;align-items:center!important;justify-content:flex-end!important;gap:6px!important;margin-left:auto!important;max-width:100%!important}',
'#removerModal.rmModalSettings #rmSettingsActionButtonsRow{display:flex;align-items:center;justify-content:flex-end;gap:6px;flex-wrap:nowrap}',
'#removerModal.rmModalSettings #rmSettingsUnsavedHint{display:none;position:absolute;top:100%;right:0;width:auto;box-sizing:border-box;margin:4px 0 0;color:' + tk.cSubM + ';opacity:.78;font-size:12px;line-height:1.35;text-align:right;white-space:nowrap}',
'#removerModal .rmProtectControlGroup{margin-top:12px;padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-sizing:border-box}',
'#removerModal .rmProtectControlLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmProtectControlGroup .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}',
'#removerModal .rmProtectControlGroup .rmSegmentedBtn{padding:4px 10px;font-size:12px}',
'#removerModal .rmTransferPanel{margin-top:10px;padding:0;border:0;background:transparent;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmTransferGrid{display:grid;grid-template-columns:max-content max-content;column-gap:10px;row-gap:6px;align-items:start;justify-content:start}',
'#removerModal .rmTransferGrid .rmSegmentedBar{justify-self:start}',
'#removerModal .rmTransferHintRow{grid-column:1 / -1;min-height:0}',
'#removerModal .rmTransferPanel .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}',
'#removerModal .rmTransferPanel .rmSegmentedBtn{padding:6px 14px;font-size:12px;font-weight:700}',
'#removerModal #rmTransferModeGroup{gap:0}',
'#removerModal #rmTransferModeGroup .rmSegmentedBtn:first-child{border-top-right-radius:0;border-bottom-right-radius:0}',
'#removerModal #rmTransferModeGroup .rmSegmentedBtn + .rmSegmentedBtn{margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}',
'#removerModal.rmCompactContent .rmArticleRow{flex-wrap:wrap!important;gap:6px}',
'#removerModal.rmCompactContent .rmArticleRow .rmArticleInput{flex:1 1 100%!important;width:100%!important}',
'#removerModal.rmCompactContent .rmArticleRow .rmArticleCommentToggle,#removerModal.rmCompactContent .rmArticleRow .rmAddArticle,#removerModal.rmCompactContent .rmArticleRow .rmRemoveInput{margin-left:0!important}',
'#removerModal.rmCompactContent .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(1){order:1}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(2){order:2}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(3){order:3}',
'#removerModal.rmCompactContent #rmTransferModeGroup{flex-direction:column;align-items:flex-start;gap:6px}',
'#removerModal.rmCompactContent #rmTransferModeGroup .rmSegmentedBtn{margin-left:0!important;border-radius:999px!important}',
'#removerModal.rmCompactContent .rmTransferHintRow{grid-column:auto}',
'#removerModal #rmProtectTextBlock{margin-top:14px}',
'#removerModal #rmSettingsMenuTitle:disabled{background:' + tk.bgDis + '!important;border-color:' + tk.bDis + '!important;color:' + tk.cDis + '!important;-webkit-text-fill-color:' + tk.cDis + ';opacity:1;cursor:not-allowed;box-shadow:none!important}',
'#removerModal .rmSettingsHintList{display:flex;flex-direction:column;gap:4px;margin-top:8px}',
'#removerModal .rmSettingsHintRow{font-size:12px;line-height:1.5;color:' + tk.cSubM + '}',
'#removerModal .rmSettingsHintBadge{display:inline-block;margin-right:6px;padding:1px 6px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';font-size:11px;font-weight:700;color:' + tk.cSubM + ';vertical-align:baseline}',
'#removerModal .rmQuickPhraseEditor{display:flex;flex-direction:column;gap:10px}',
'#removerModal .rmQuickPhraseList,#removerModal .rmQuickPhrasesPanel{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-start}',
'#removerModal .rmQuickPhraseChip{position:relative;display:inline-flex;align-items:center;gap:4px;max-width:100%;padding:2px 4px 2px 10px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';box-shadow:' + pillShadow + ';transition:border-color .12s,box-shadow .12s,opacity .12s;overflow:visible}',
'#removerModal .rmQuickPhraseChip.is-editing{opacity:.42;border-style:dashed}',
'#removerModal .rmQuickPhraseChip.is-dragging{opacity:.65}',
'#removerModal .rmQuickPhraseChip.is-drop-before::before,#removerModal .rmQuickPhraseChip.is-drop-after::after{content:"";position:absolute;top:50%;width:3px;height:24px;border-radius:999px;background:' + tk.cProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.08)}',
'#removerModal .rmQuickPhraseChip.is-drop-before::before{left:-4px;transform:translate(-50%,-50%)}',
'#removerModal .rmQuickPhraseChip.is-drop-after::after{right:-4px;transform:translate(50%,-50%)}',
'#removerModal .rmQuickPhraseEditBtn{max-width:100%;padding:3px 0;border:0;background:transparent;color:' + tk.cBase + ';font-size:13px;line-height:1.35;cursor:pointer;text-align:left;white-space:normal}',
'#removerModal .rmQuickPhraseRemoveBtn{width:24px;height:24px;padding:0;border:0;border-radius:999px;background:transparent;color:' + tk.cSubM + ';font-size:18px;line-height:1;cursor:pointer;flex-shrink:0}',
'#removerModal .rmQuickPhraseRemoveBtn:hover{background:' + tk.bgN + ';color:' + tk.cBase + '}',
'#removerModal #rmSettingsQuickPhraseInput.is-editing{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.12)}',
'#removerModal .rmQuickPhraseMeta{font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmQuickPhraseEmpty{padding:2px 0;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmQuickPhrasesPanel{margin-top:8px}',
'#removerModal .rmQuickPhraseActionBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}',
'#removerModal .rmQuickPhraseActionBtn:hover{border-color:' + tk.bProg + ';color:' + tk.cProg + '}',
'@media (max-width:' + sz.mobileBp + 'px){',
'#removerModal button{white-space:normal!important}',
'#removerModal #rmFooterButtons{align-items:flex-start!important}',
'#removerModal #rmFooterCheckboxes,#removerModal #rmFooterActionButtons{width:100%!important;max-width:100%!important;margin-left:0!important}',
'#removerModal .rmSettingsSection{padding:12px 13px}',
'#removerModal .rmSettingsField{padding:10px}',
'#removerModal.rmModalSettings #rmSettingsUnsavedHint{max-width:100%;margin:4px 0 0;text-align:right;white-space:normal}',
'#removerModal .rmTransferPanel{padding:0}',
'#removerModal .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}',
'#removerModal .rmTransferHintRow{grid-column:auto}',
'#removerModal .rmQuickPhraseChip{max-width:100%}',
'}'
].join('');
var style = document.createElement('style');
style.id = 'removerModalDynamicStyles';
style.textContent = css;
document.head.appendChild(style);
}
function applyV2022Layout($modal, explicitWidth) {
if (!isVector22) return;
var css = { 'max-width': '100%', 'box-sizing': 'border-box', 'overflow-wrap': 'anywhere' };
if (typeof explicitWidth === 'number') css['max-width'] = explicitWidth + 'px';
$modal.css(css);
}
function getPageUrl(pageTitle) {
return (mw.util && typeof mw.util.getUrl === 'function')
? mw.util.getUrl(pageTitle)
: '/wiki/' + encodeURIComponent((pageTitle || '').replace(/ /g, '_'));
}
function getPageUrlWithFragment(pageTitle, fragment) {
var url = getPageUrl(pageTitle);
var frag = normalizeSectionForLink(fragment);
if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_'));
return url;
}
function buildStatusPageLink(pageName) {
var title = normTitle(pageName);
return '<a href="' + escapeHtml(getPageUrl(title)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(title) + '</a>';
}
function buildQuotedStatusPageLink(pageName) {
return '«' + buildStatusPageLink(pageName) + '»';
}
function buildHeaderIconButtonHtml(id, title, label, text) {
return joinHtml([
'<button id="', id, '" type="button" title="', escapeHtml(title), '" aria-label="', escapeHtml(label || title), '" ',
'style="', stHeaderIconBtn, '">', text || '', '</button>'
]);
}
function createModal(opts) {
if (typeof opts === 'string') opts = { title: opts };
var layout = getModalLayout();
resetModalObservers();
ensureModalStyles();
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
var subtitleHtml = '';
var subtitleStyle = 'margin:-4px 0 8px;font-size:12px!important;color:' + tk.cSubM + ';line-height:1.35!important;font-weight:400!important;';
var subtitleLinkStyle = 'font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important;';
if (opts.subtitleHtml) {
subtitleHtml = joinHtml([
'<div id="removerModalSubtitle" style="', subtitleStyle, '">',
opts.subtitleHtml,
'</div>'
]);
} else if (opts.subtitlePage) {
subtitleHtml = joinHtml([
'<div id="removerModalSubtitle" style="', subtitleStyle, '">',
opts.subtitleLabel || 'Текущий день',
': <a href="', getPageUrl(opts.subtitlePage), '" target="_blank" rel="noopener noreferrer" class="removerModalLink" style="', subtitleLinkStyle, '">',
normTitle(opts.subtitlePage),
'</a></div>'
]);
}
var settingsButtonHtml = opts.showSettingsButton === false ? '' :
buildHeaderIconButtonHtml('removerSettingsTrigger', 'Конфигурация', 'Конфигурация', '⚙');
var display = opts.inline ? 'inline-block' : 'block';
var modalMargin = opts.inline ? '1em 0' : (layout.shouldCenter ? '1em auto' : '1em 0');
var inlineLayoutStyle = opts.inline ? ';justify-self:start;align-self:start;width:fit-content;' : '';
var modalStyle = joinHtml([
'position:relative;padding:1.5em;margin:', modalMargin, ';display:', display, ';',
'border:', stStyles.border, ';background:', stStyles.background,
';border-radius:', stStyles.borderRadius, ';box-shadow:', stStyles.boxShadow,
';max-width:100%;box-sizing:border-box;overflow-wrap:anywhere;', inlineLayoutStyle
]);
var headerStyle = 'display:flex;align-items:center;gap:10px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid ' + tk.bSubS + ';';
var titleStyle = 'color:' + stStyles.headerColor + ';margin:0;padding:0;border:0;display:block;font-size:1.3em;font-weight:400;line-height:1.25;flex:1 1 auto;min-width:0;';
$('#content').prepend(joinHtml([
'<div id="removerModal" style="', modalStyle, '">',
'<div id="removerModalHeaderBar" style="', headerStyle, '">',
'<h1 id="removerModalTitle" style="', titleStyle, '"><span id="removerModalTitleText">', opts.title, '</span></h1>',
settingsButtonHtml,
'</div>',
subtitleHtml,
'<div id="removerModalContent"></div>',
'<div id="removerModalFooter" style="margin-top:15px;"></div>',
'</div>'
]));
var $modal = $('#removerModal');
if (opts.width === 'compact') $modal.css({ width: layout.defaultOuterWidth + 'px', 'max-width': '100%', 'box-sizing': 'border-box' });
else applyV2022Layout($modal);
$('#removerSettingsTrigger').off('click').on('click', function () {
openSettings();
});
}
function buildFooterCheckboxHtml(name, checked, label) {
return joinHtml([
'<label style="', stFooterCheckLabel, '">',
'<input name="', name, '" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ',
checked ? 'checked' : '',
'>',
label,
'</label>'
]);
}
function buildFooterActionsHtml(buttonsHtml) {
return '<div id="rmFooterActionButtons" style="' + stFooterActions + '">' + buttonsHtml + '</div>';
}
function renderModalFooter(mode, options) {
var opts = options || {};
$('#removerModalFooter').css('width', '');
if (mode === 'submit') {
var showCb = opts.showCheckbox !== false;
var showSub = opts.showSubscribe === true;
var ns = mwCfg.wgNamespaceNumber;
var notifyLabel = ns === 0 ? 'Оповестить создателя статьи'
: (ns === 10 || ns === 11) ? 'Оповестить создателя шаблона'
: (ns === 14 || ns === 15) ? 'Оповестить создателя категории'
: 'Оповестить создателя страницы';
var cbInlineHtml = '';
if (showSub || showCb) {
cbInlineHtml = joinHtml([
'<div id="rmFooterCheckboxes" style="', stFooterChecks, '">',
showSub ? buildFooterCheckboxHtml('rmSubscribe', setSubscribe, 'Подписаться на номинацию') : '',
showCb ? buildFooterCheckboxHtml('rmUAlert', setAlert, notifyLabel) : '',
'</div>'
]);
}
$('#removerModalFooter').html(joinHtml([
'<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:', cbInlineHtml ? 'space-between' : 'flex-end', ';">',
cbInlineHtml,
buildFooterActionsHtml(joinHtml([
'<button id="removerCancel" style="', stCancel, '">Отмена</button>',
'<button id="removerSubmit" style="', stSubmit, '">', opts.submitText || 'ОК', '</button>'
])),
'</div>'
]));
$('#removerCancel').click(function () { closeModal(); });
$('#removerSubmit').data('rmSubmitInProgress', false).click(function () {
if ($(this).data('rmSubmitInProgress')) return;
$(this).removeClass('rmSubmitError').css({ background: '', 'border-color': '', color: '' });
isError = false;
if (!opts.preserveLogOnSubmit) {
$('#rmLogBox').empty();
logStatusSeq = 0;
}
if (showCb) { setAlert = $('[name="rmUAlert"]').is(':checked'); state.setAlert = setAlert; }
if (showSub) { setSubscribe = $('[name="rmSubscribe"]').is(':checked'); state.setSubscribe = setSubscribe; }
$(this).data('rmSubmitInProgress', true).prop('disabled', true);
var submitResult;
try { submitResult = opts.onSubmit(); } catch (ex) { unlockModalSubmit(); throw ex; }
if (submitResult === false) unlockModalSubmit();
});
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.ctrlKey && e.keyCode === 13) $('#removerSubmit').click();
});
} else if (mode === 'reload') {
var newBtns = buildFooterActionsHtml(joinHtml([
'<button id="removerCancel" style="', stCancel, '">Закрыть</button>',
'<button id="removerReload" style="', stReload, '">', opts.reloadText || 'Обновить страницу', '</button>'
]));
$('#rmFooterCheckboxes').remove();
var $btns = $('#rmFooterButtons');
if ($btns.length) {
$btns.css({ 'justify-content': 'flex-end' }).html(newBtns);
} else {
$('#removerModalFooter').append(joinHtml([
'<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:flex-end;">',
newBtns,
'</div>'
]));
}
$('#removerCancel').click(function () { closeModal(); });
$('#removerReload').click(function () { location.reload(); });
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.keyCode === 27) $('#removerCancel').click();
if (e.ctrlKey && e.keyCode === 13) $('#removerReload').click();
});
} else { // 'close'
$('#removerModalFooter').html(joinHtml([
'<div style="display:flex;justify-content:flex-end;align-items:center;">',
'<button id="removerCancel" style="', stCancel, 'margin-right:0;">', opts.closeText || 'Закрыть', '</button>',
'</div>'
]));
$('#removerCancel').click(function () { closeModal(); });
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.keyCode === 27) $('#removerCancel').click();
});
}
}
function unlockModalSubmit() {
$('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false);
}
function markSubmitError() {
isError = true;
var errColor = '#d73333';
$('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false)
.addClass('rmSubmitError').css({ background: errColor, 'border-color': errColor, color: '#fff' });
}
// ─── UI: статус и ссылки ─────────────────────────────────────────────────
function startProcessing() {
if ($('#rmLogBox').length) return;
$('#removerModal').append(
'<div id="rmLogBox" style="margin-top:12px;padding-top:10px;border-top:1px solid ' + tk.bSubS + ';line-height:1.5;overflow-wrap:anywhere;word-break:break-word;box-sizing:border-box;"></div>'
);
syncLinkWidths();
}
function logStatus(message, error, opts) {
var o = opts || {};
if (o.trackError !== false && error && error.code) isError = true;
var $box = $('#rmLogBox');
if (!$box.length) { startProcessing(); $box = $('#rmLogBox'); }
var statusId = o.statusId || ('rm-status-' + (++logStatusSeq));
var $row = $box.find('[data-rm-status-id="' + statusId + '"]');
if (!$row.length) {
$row = $('<div data-rm-status-id="' + statusId + '" style="margin-top:4px;line-height:1.4;"></div>');
$box.append($row);
}
var html;
if (error) {
var errText = error.code
? '<span class="error"><small>' + escapeHtml(formatLogErrorCode(error.code)) + ': ' + escapeHtml(String(error.info || '')) + '</small></span>'
: escapeHtml(String(error));
html = '<span style="color:' + tk.cDang + ';margin-right:4px;">✕</span>' + message + ' — ' + errText;
} else if (o.pending) {
html = '<span style="color:' + tk.cSubM + ';">' + message + '</span>';
} else {
html = '<span style="color:' + tk.bgSucc + ';margin-right:4px;">✓</span><span style="color:' + tk.cSubM + ';">' + message + '</span>';
}
$row.html(html);
return statusId;
}
function formatLogErrorCode(code) {
var value = String(code || '');
return value.toLowerCase() === 'error' ? 'Ошибка' : value;
}
function logPageEdit(pageName, error, opts) {
logStatus('Правка страницы ' + buildQuotedStatusPageLink(pageName) + '.', error, opts);
}
function syncLinkWidths() {
var $box = $('#rmLogBox');
if (!$box.length) return;
var taW = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0;
$box.css({ width: taW ? taW + 'px' : '', 'max-width': '100%' });
}
function appendNominationLink(pageTitle, sectionTitle) {
if (!pageTitle) return;
var url = getPageUrl(pageTitle);
var frag = normalizeSectionForLink(sectionTitle);
if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_'));
var label = normTitle(frag ? pageTitle + '#' + frag : pageTitle);
var $target = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent');
$target.append(
'<div style="margin-top:4px;line-height:1.4;word-break:break-word;overflow-wrap:anywhere;display:flex;align-items:baseline;gap:5px;">' +
'<span style="color:' + tk.bgSucc + ';font-size:14px;flex-shrink:0;">✓</span>' +
'<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a></div>'
);
}
// ─── UI-строители ────────────────────────────────────────────────────────
function buildInfoBoxHtml(mainText, detailsText, isErr) {
var cls = isErr ? ' class="error"' : '';
return joinHtml([
'<div class="rmInfoBox">',
'<p', cls, ' style="margin:0', detailsText ? ' 0 6px' : '', ';">', mainText, '</p>',
detailsText ? '<p style="margin:0;color:' + tk.cSubM + ';">' + detailsText + '</p>' : '',
'</div>'
]);
}
function buildActionsHtml(actions, inputName, listId) {
var actionItemsHtml = actions.map(function (a, i) {
var meta = a.description || (a.talkNotice ? 'С добавлением {{' + (a.talkTemplate || a.resultTemplate || 'шаблон') + '}} на СО.' : '');
var tagHtml = a.tag
? '<span style="display:inline-block;font-size:13px;font-weight:600;padding:2px 7px;border-radius:3px;background:' + tk.bgN + ';color:' + tk.cSubM + ';margin-right:8px;white-space:nowrap;vertical-align:middle;">' + escapeHtml(a.tag) + '</span>'
: '';
return joinHtml([
'<label class="rmActionItem">',
'<span class="rmActionMain">',
'<input type="radio" name="', inputName, '" value="', a.id, '" ', i === 0 ? 'checked' : '', '>',
tagHtml,
'<span>', a.label, '</span>',
'</span>',
meta ? '<span class="rmActionMeta">' + meta + '</span>' : '',
'</label>'
]);
}).join('');
return joinHtml([
'<div style="margin:0 0 8px;color:', tk.cSubM, ';font-size:13px;">Обнаружены открытые номинации:</div>',
'<div', listId ? ' id="' + listId + '"' : '', ' class="rmActionList">',
actionItemsHtml,
'</div>'
]);
}
function buildNestedCommentFieldsHtml(opts) {
var options = opts || {};
var wrapId = options.wrapId || '';
var textareaId = options.textareaId || '';
var textareaClass = options.textareaClass ? ' ' + options.textareaClass : '';
var textareaStyleExtra = options.textareaStyleExtra || '';
var wrapStyleExtra = options.wrapStyleExtra || '';
var placeholder = options.placeholder || 'Комментарий (необязательно)';
var beforeHtml = options.beforeHtml || '';
var marginTop = options.marginTop || '6px';
var minHeight = parseInt(options.minHeight, 10) || 90;
var isEmbedded = !!options.embedded;
var wrapClass = isEmbedded ? '' : (' class="' + RESIZE_CLASS + '"');
var wrapStyle = 'display:none;margin-top:' + marginTop + ';max-width:100%;box-sizing:border-box;';
if (isEmbedded) {
wrapStyle += 'padding:0;border:0;background:transparent;';
} else {
wrapStyle += 'padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:6px;background:' + tk.bgNSub + ';';
}
wrapStyle += wrapStyleExtra;
return joinHtml([
'<div id="', wrapId, '"', wrapClass, ' style="', wrapStyle, '">',
beforeHtml,
'<textarea id="', textareaId, '" class="rmNestedCommentInput', textareaClass,
'" placeholder="', escapeHtml(placeholder), '" style="', stInputFull,
'min-height:', minHeight, 'px;resize:both;margin-bottom:6px;', textareaStyleExtra, '"></textarea>',
buildQuickPhrasesPanelHtml(textareaId),
'</div>'
]);
}
function buildConditionalRetFieldsHtml() {
return buildNestedCommentFieldsHtml({
wrapId: 'rmCloseConditionalWrap',
textareaId: 'rmCloseConditionalReason',
placeholder: 'Условие / пояснение (необязательно)',
marginTop: '8px',
minHeight: 90,
beforeHtml: '<input id="rmCloseConditionalDeadline" type="text" placeholder="Срок доработки: 2026-05-31" style="' + stInputFull + 'margin-bottom:6px;">'
});
}
function buildAddArticleButtonHtml(options) {
var opts = options || {};
var title = opts.addTitle || 'Добавить статью';
return '<button type="button" class="rmAddArticle" title="' + escapeHtml(title) + '" aria-label="' + escapeHtml(title) + '" style="' + stRemoveBtn + '">+</button>';
}
function buildSquareAddButtonHtml(id, title, className) {
var idAttr = id ? ' id="' + id + '"' : '';
var clsAttr = className ? ' class="' + className + '"' : '';
var label = title || 'Добавить';
return '<button' + idAttr + ' type="button"' + clsAttr + ' title="' + escapeHtml(label) + '" aria-label="' + escapeHtml(label) + '" style="' + stRemoveBtn + '">+</button>';
}
function buildMultiArticleButtonsHtml(commentWrapId, commentId, options) {
var opts = options || {};
var commentBtnStyle = stToolBtn + (opts.showComment ? '' : 'display:none;');
if (opts.showAdd) return buildAddArticleButtonHtml(opts);
return joinHtml([
'<button type="button" class="rmToggleBtn rmArticleCommentToggle" data-rm-comment-wrap="', commentWrapId,
'" data-rm-comment-textarea="', commentId, '" aria-expanded="false" style="', commentBtnStyle, '">Комментарий</button>',
'<button type="button" class="rmRemoveInput" style="', stRemoveBtn, '" title="', escapeHtml(opts.removeTitle || 'Удалить статью'), '">−</button>'
]);
}
function buildMultiArticleRowHtml(index, options) {
var opts = options || {};
var articleId = 'rmArticle' + index;
var commentWrapId = 'rmArticleCommentWrap' + index;
var commentId = 'rmArticleComment' + index;
var articleValue = opts.articleValue ? ' value="' + escapeHtml(opts.articleValue) + '"' : '';
var inputPlaceholder = opts.inputPlaceholder || 'Статья';
var commentPlaceholder = opts.commentPlaceholder || 'Комментарий только для этой статьи (необязательно)';
var articleRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
var blockStyle = 'max-width:100%;box-sizing:border-box;';
var buttonsHtml = buildMultiArticleButtonsHtml(commentWrapId, commentId, {
showAdd: !!opts.showAdd,
showComment: !!opts.showComment,
addTitle: opts.addTitle,
removeTitle: opts.removeTitle
});
return joinHtml([
'<div class="rmMultiArticleBlock ', RESIZE_CLASS, '" style="', blockStyle, '">',
'<div', opts.rowId ? ' id="' + opts.rowId + '"' : '', ' class="rmArticleRow" style="', articleRowStyle, '">',
'<input id="', articleId, '" type="text" placeholder="', escapeHtml(inputPlaceholder), '" class="rmArticleInput" style="', stInputBox, '"', articleValue, '>',
buttonsHtml,
'</div>',
buildNestedCommentFieldsHtml({
wrapId: commentWrapId,
textareaId: commentId,
textareaClass: 'rmArticleCommentInput',
placeholder: commentPlaceholder,
marginTop: '4px',
minHeight: 90,
embedded: true,
wrapStyleExtra: 'padding:0 0 0 12px;background:transparent;',
textareaStyleExtra: 'border-color:' + tk.bSubS + ';border-radius:4px;background:' + tk.bgBase + ';'
}),
'</div>'
]);
}
function showInfoAndClose(mainText, detailsText, isErr) {
$('#removerModalContent').html(buildInfoBoxHtml(mainText, detailsText || '', isErr || false));
renderModalFooter('close');
}
function getSelectedAction(inputName, actionMap) {
var id = $('[name="' + inputName + '"]:checked').val();
var sel = actionMap[id];
if (!sel) alert('Выберите действие.');
return sel || null;
}
function prependTemplateToNoinclude(text, templateText) {
var source = String(text || '');
var tpl = String(templateText || '').trim();
if (!tpl) return source;
var match = source.match(RE_NOINCLUDE);
if (match) {
var before = source.slice(0, source.indexOf(match[0]));
if (/\S/.test(before)) return '<noinclude>' + tpl + '</noinclude>\n' + source;
var content = String(match[2] || '').replace(/^\n+/, '');
return source.replace(match[0], match[1] + '<noinclude>' + tpl + (content ? '\n' + content : '') + '\n</noinclude>');
}
return '<noinclude>' + tpl + '</noinclude>\n' + source;
}
function buildGeneratedNominationTemplateText(job, pg) {
var tplStr = '';
if (!job) return '';
if (job.opId === 'fRm') {
tplStr = job.kbuTemplate || '';
if (job.kbuAddInfo) tplStr += '|1=' + job.kbuAddInfo;
if (job.kbuComment) tplStr += '|' + (job.kbuAddInfo ? '2' : '1') + '=' + job.kbuComment;
return tplStr ? (T_OPEN + tplStr + T_CLOSE) : '';
}
if (typeof job.articleTpl !== 'function') return '';
tplStr = job.articleTpl(job.tplpar, job.date[0]);
if (job.opId === 'merge' && job.tplpar) {
tplStr = (job.op.nomination.articleTpl)(
('|' + job.tplpar + '|').replace('|' + pg + '|', '|').slice(1, -1),
job.date[0]
);
}
return tplStr ? (T_OPEN + tplStr + T_CLOSE) : '';
}
function applyConflictTemplateResolution(articleText, job, pg, decision) {
var rule = getNominationConflictRule(job);
var generatedTemplate = buildGeneratedNominationTemplateText(job, pg);
var source = String(articleText || '');
if (!generatedTemplate || !decision) return source;
if (decision.templateAction === 'overwrite') {
var cleaned = rule ? stripTemplatesByPattern(source, rule.namePattern).text : source;
return prependTemplateToNoinclude(cleaned, generatedTemplate);
}
if (decision.templateAction === 'prepend') return prependTemplateToNoinclude(source, generatedTemplate);
return source;
}
function inspectMultiNominationConflicts(job, callback) {
var cb = callback || function () {};
var pages = (job && job.multiArticles) ? job.multiArticles.slice() : [];
var conflicts = [];
var statusId = logStatus('Проверяются статьи на наличие уже установленных шаблонов...', null, { pending: true, trackError: false });
if (!pages.length) {
logStatus('Проверка завершена: конфликтов не найдено.', null, { statusId: statusId, trackError: false });
cb(null, conflicts);
return;
}
eachSequential(pages, function (pg, next) {
getText(pg, function (articleText, readErr) {
var conflict;
if (readErr) { next(makeReadError(readErr, 'read_failed', 'Не удалось проверить страницу «' + pg + '».')); return; }
if (articleText === null) { next({ code: 'read_failed', info: 'Страница «' + pg + '» не существует.' }); return; }
conflict = detectNominationConflict(articleText, job);
if (conflict) {
conflicts.push($.extend({ pageName: pg }, conflict));
logStatus('В статье ' + buildQuotedStatusPageLink(pg) + ' обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.', null, { trackError: false });
}
next();
});
}, function (err) {
if (err) {
logStatus('Проверка статей.', err, { statusId: statusId });
cb(err);
return;
}
logStatus(
conflicts.length
? 'Проверка завершена: найдены статьи с уже установленными шаблонами.'
: 'Проверка завершена: конфликтов не найдено.',
null,
{ statusId: statusId, trackError: false }
);
cb(null, conflicts);
});
}
function buildNominationConflictResolutionHtml(conflicts) {
return '<div class="rmInfoBox"><p style="margin:0 0 6px;">Найдены статьи, где шаблон уже стоит. Для каждой конфликтующей страницы выберите, что делать со статьёй и с шаблоном.</p>' +
'<p style="margin:0;color:' + tk.cSubM + ';font-size:12px;line-height:1.45;">По умолчанию такие статьи исключаются из новой номинации, а существующий шаблон остаётся без изменений.</p></div>' +
'<div class="rmConflictLead">После выбора нажмите «Продолжить номинирование».</div>' +
'<div id="rmConflictList" class="rmConflictList">' +
conflicts.map(function (conflict, index) {
var pageLink = buildStatusPageLink(conflict.pageName);
return '<div class="rmConflictCard" data-rm-conflict-index="' + index + '">' +
'<input type="hidden" class="rmConflictPageAction" value="skip">' +
'<input type="hidden" class="rmConflictTemplateAction" value="keep">' +
'<div class="rmConflictTitle">' + pageLink + '</div>' +
'<div class="rmConflictMeta">Обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.</div>' +
'<div class="rmConflictGroup">' +
'<div class="rmConflictGroupTitle">Действие со статьёй</div>' +
'<div class="rmConflictButtons" data-rm-conflict-group="page">' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="page" data-rm-choice="skip" aria-pressed="true">Убрать из номинации</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="page" data-rm-choice="keep" aria-pressed="false">Оставить в номинации</button>' +
'</div></div>' +
'<div class="rmConflictGroup">' +
'<div class="rmConflictGroupTitle">Действие с шаблоном</div>' +
'<div class="rmConflictButtons" data-rm-conflict-group="template">' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="template" data-rm-choice="keep" aria-pressed="true">Оставить как есть</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="overwrite" aria-pressed="false">Новая дата</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="prepend" aria-pressed="false">Второй сверху</button>' +
'</div>' +
'<div class="rmConflictHint">Если статья исключается из номинации, действие с шаблоном не применяется.</div>' +
'</div></div>';
}).join('') +
'</div>';
}
function updateNominationConflictCardState($card) {
var pageAction = $card.find('.rmConflictPageAction').val() || 'skip';
var disableTemplate = pageAction !== 'keep';
var $templateButtons = $card.find('[data-rm-choice-type="template"]');
$card.toggleClass('is-skip', disableTemplate);
$card.find('[data-rm-conflict-group="template"]').toggleClass('is-disabled', disableTemplate);
$templateButtons.prop('disabled', disableTemplate).toggleClass('is-disabled', disableTemplate);
}
function bindNominationConflictResolutionUi() {
var $content = $('#removerModalContent');
function setChoice($card, type, value) {
var inputClass = type === 'page' ? '.rmConflictPageAction' : '.rmConflictTemplateAction';
$card.find(inputClass).val(value);
$card.find('[data-rm-choice-type="' + type + '"]').each(function () {
var isActive = $(this).data('rmChoice') === value;
$(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false');
});
updateNominationConflictCardState($card);
}
$content.off('.rmConflictResolution').on('click.rmConflictResolution', '[data-rm-choice-type]', function () {
var $btn = $(this);
var $card = $btn.closest('.rmConflictCard');
if ($btn.prop('disabled')) return;
setChoice($card, $btn.data('rmChoiceType'), $btn.data('rmChoice'));
});
$('#rmConflictList .rmConflictCard').each(function () { updateNominationConflictCardState($(this)); });
}
function collectNominationConflictResolution(conflicts) {
var decisions = {};
(conflicts || []).forEach(function (conflict, index) {
var $card = $('#rmConflictList .rmConflictCard[data-rm-conflict-index="' + index + '"]');
decisions[normTitle(conflict.pageName)] = {
pageAction: $card.find('.rmConflictPageAction').val() || 'skip',
templateAction: $card.find('.rmConflictTemplateAction').val() || 'keep'
};
});
return decisions;
}
function applyNominationConflictResolutionToJob(job, decisions) {
var resultArticles;
var headerText;
if (!job || !job.isMulti) {
job.conflictDecisions = decisions || {};
return { value: job };
}
resultArticles = (job.multiArticles || []).filter(function (pageName) {
var decision = decisions && decisions[normTitle(pageName)];
return !decision || decision.pageAction !== 'skip';
});
if (!resultArticles.length) return { error: 'После исключения конфликтующих статей в номинации не осталось ни одной страницы.' };
job.conflictDecisions = decisions || {};
job.multiArticles = resultArticles.slice();
job.pages = resultArticles.slice().reverse();
headerText = String(job.multiHeaderText || '').trim();
job.section = headerText || ('[[:' + resultArticles[0] + ']]');
job.sectionNW = job.section.replace(/\[\[:/g, '').replace(/]]/g, '');
job.msg = job.multiNominationFormat === 'list'
? buildMultiNominationListText(resultArticles, job.multiNominationBody, job.multiArticleComments)
: buildMultiNominationText(resultArticles, job.multiNominationBody, job.multiArticleComments);
job.summary = makeSummary('номинация [[' + job.nomPage + '#' + job.sectionNW + ']]');
return { value: job };
}
function showNominationConflictResolution(job, conflicts, onContinue) {
resetModalObservers();
$('#removerModalContent').html(buildNominationConflictResolutionHtml(conflicts));
bindNominationConflictResolutionUi();
syncLinkWidths();
renderModalFooter('submit', {
submitText: 'Продолжить номинирование',
showSubscribe: true,
preserveLogOnSubmit: true,
onSubmit: function () {
var decisions = collectNominationConflictResolution(conflicts);
var applied = applyNominationConflictResolutionToJob(job, decisions);
if (applied.error) {
alert(applied.error);
return false;
}
if (typeof onContinue === 'function') onContinue(applied.value);
return true;
}
});
}
function bindTouchTextareaGrip($ta, sync, getMaxWidth, options) {
var opts = options || {};
var minWidth = parseInt(opts.minWidth, 10) || parseInt(sz.taMinW, 10) || 180;
var minHeight = parseInt(opts.minHeight, 10) || parseInt(sz.taMinH, 10) || 100;
var allowWidthResize = opts.allowWidth !== false;
var syncFn = typeof sync === 'function' ? sync : function () {};
var getMaxWidthFn = typeof getMaxWidth === 'function' ? getMaxWidth : function () { return $ta.outerWidth() || minWidth; };
var usePointerEvents = typeof window.PointerEvent === 'function';
var dragState = { active: false, startX: 0, startY: 0, startWidth: 0, startHeight: 0 };
var gripStyle = 'height:20px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-top:0;border-radius:0 0 4px 4px;background:' + tk.bgNSub + ';display:flex;align-items:center;justify-content:center;cursor:ns-resize;touch-action:none;user-select:none;-webkit-user-select:none;';
if (opts.gripMarginBottom) gripStyle += 'margin-bottom:' + opts.gripMarginBottom + ';';
var $grip = $('<div data-rm-textarea-grip="1" style="' + gripStyle + '"><span style="display:block;width:42px;height:4px;border-radius:999px;background:' + tk.bSub + ';opacity:.9;"></span></div>');
function getCoord(evt, key) {
var e = evt.originalEvent || evt;
if (e.touches && e.touches.length) return e.touches[0][key];
if (e.changedTouches && e.changedTouches.length) return e.changedTouches[0][key];
return e[key];
}
function stopDrag() {
dragState.active = false;
$(window).off('.rmTaResize');
}
function onDragMove(evt) {
var clientX;
var clientY;
if (!dragState.active) return;
clientX = getCoord(evt, 'clientX');
clientY = getCoord(evt, 'clientY');
if (typeof clientY !== 'number') return;
if (allowWidthResize && typeof clientX === 'number') $ta.css('width', Math.max(minWidth, Math.min(getMaxWidthFn(), dragState.startWidth + (clientX - dragState.startX))) + 'px');
$ta.css('height', Math.max(minHeight, dragState.startHeight + (clientY - dragState.startY)) + 'px');
syncFn();
if (evt.preventDefault) evt.preventDefault();
}
function startDrag(evt) {
var clientY = getCoord(evt, 'clientY');
if (typeof clientY !== 'number') return;
dragState.active = true;
dragState.startX = getCoord(evt, 'clientX') || 0;
dragState.startY = clientY;
dragState.startWidth = $ta.outerWidth();
dragState.startHeight = $ta.outerHeight();
if (evt.preventDefault) evt.preventDefault();
$(window).off('.rmTaResize');
if (usePointerEvents) {
$(window).on('pointermove.rmTaResize', onDragMove).on('pointerup.rmTaResize pointercancel.rmTaResize', stopDrag);
} else {
$(window).on('touchmove.rmTaResize mousemove.rmTaResize', onDragMove).on('touchend.rmTaResize touchcancel.rmTaResize mouseup.rmTaResize', stopDrag);
}
}
$ta.css({ 'border-bottom-left-radius': '0', 'border-bottom-right-radius': '0' });
$ta.next('[data-rm-textarea-grip]').remove();
$ta.after($grip);
if (usePointerEvents) $grip.on('pointerdown.rmTaGrip', startDrag);
else $grip.on('touchstart.rmTaGrip mousedown.rmTaGrip', startDrag);
}
function applyModalContentWidth($modal, contentWidth, options) {
var opts = options || {};
var layout = getModalLayout();
var modalFrame = getBoxFrameWidth($modal);
var safeContentWidth = Math.max(layout.minWidth, Math.min(contentWidth, layout.maxOuterWidth - modalFrame));
var modalWidth = safeContentWidth + modalFrame;
var initialContentW = parseFloat($modal.data('rmInitialContentW')) || 0;
$modal.css({
width: modalWidth + 'px',
'max-width': layout.maxOuterWidth + 'px',
'box-sizing': 'border-box',
'margin-left': layout.shouldCenter ? 'auto' : '0',
'margin-right': layout.shouldCenter ? 'auto' : '0'
}).toggleClass('rmCompactContent', safeContentWidth < 520);
$('.' + RESIZE_CLASS).css({
width: safeContentWidth + 'px',
'max-width': '100%',
'box-sizing': 'border-box'
});
$('#rmMsg,#nominationReason,#rmReportText').each(function () {
var $textarea = $(this);
var textareaId = this.id;
if (!$textarea.length) return;
$textarea.css('width', safeContentWidth + 'px');
$textarea.next('[data-rm-textarea-grip]').css('width', safeContentWidth + 'px');
$('.rmQuickPhrasesPanel[data-rm-target="' + textareaId + '"]').css('width', safeContentWidth + 'px');
});
$('.rmNestedCommentInput').each(function () {
var $textarea = $(this);
var $wrap = $textarea.parent();
var containerFrame = parseFloat($textarea.data('rmNestedContainerFrame')) || 0;
var wrapFrame = parseFloat($textarea.data('rmNestedWrapFrame')) || 0;
var safeMinWidth = parseFloat($textarea.data('rmNestedMinWidth')) || 0;
var wrapOuterWidth;
var textareaWidth;
if (!$wrap.length || !$wrap.is(':visible')) return;
wrapOuterWidth = Math.max(1, safeContentWidth - containerFrame);
textareaWidth = Math.max(1, wrapOuterWidth - wrapFrame);
$wrap.css({
width: wrapOuterWidth + 'px',
'max-width': '100%',
'box-sizing': 'border-box'
});
$textarea.css({
width: textareaWidth + 'px',
'min-width': Math.min(safeMinWidth || textareaWidth, textareaWidth) + 'px'
});
$textarea.next('[data-rm-textarea-grip]').css('width', textareaWidth + 'px');
$('.rmQuickPhrasesPanel[data-rm-target="' + this.id + '"]').css('width', textareaWidth + 'px');
});
syncLinkWidths();
if (isVector22) {
var $content = $('#content');
if ($content.length && modalWidth > initialContentW) $content.css({ 'min-width': modalWidth + 'px' });
else if ($content.length) $content.css({ 'min-width': '' });
} else {
$('#content').css({ 'min-width': '' });
}
if (!opts.skipStore) $modal.data('rmContentWidth', safeContentWidth);
return safeContentWidth;
}
function setupNestedResizableTextarea(textareaId, wrapId, minWidth, minHeight) {
var $ta = $('#' + textareaId);
var $wrap = $('#' + wrapId);
var $modal = $('#removerModal');
var $container = $wrap.parent();
var layout = getModalLayout();
var safeMinWidth = parseInt(minWidth, 10) || 280;
var safeMinHeight = parseInt(minHeight, 10) || 90;
var initialWidth;
var modalFrame;
var containerFrame;
var wrapFrame;
function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; }
function getMaxTextareaWidth(currentLayout) {
return Math.max(1, currentLayout.maxOuterWidth - modalFrame - containerFrame - wrapFrame);
}
function getEffectiveMinWidth(currentLayout) {
return Math.min(safeMinWidth, getMaxTextareaWidth(currentLayout));
}
function sync() {
var currentLayout = getModalLayout();
var maxTextareaWidth = getMaxTextareaWidth(currentLayout);
var textareaWidth = $ta.outerWidth();
var contentWidth;
if (!$wrap.is(':visible')) return;
if (textareaWidth > maxTextareaWidth) {
$ta.css('width', maxTextareaWidth + 'px');
textareaWidth = $ta.outerWidth();
}
contentWidth = Math.min(currentLayout.maxOuterWidth - modalFrame, textareaWidth + wrapFrame + containerFrame);
applyModalContentWidth($modal, contentWidth);
}
if (!$ta.length || !$wrap.length || !$modal.length) return;
modalFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width');
containerFrame = $container.length
? px($container, 'padding-left') + px($container, 'padding-right') + px($container, 'border-left-width') + px($container, 'border-right-width')
: 0;
wrapFrame = px($wrap, 'padding-left') + px($wrap, 'padding-right') + px($wrap, 'border-left-width') + px($wrap, 'border-right-width');
initialWidth = Math.min(
Math.max(getEffectiveMinWidth(layout), getDefaultResizableWidth(modalFrame + containerFrame + wrapFrame)),
getMaxTextareaWidth(layout)
);
$ta.css({
width: initialWidth + 'px',
'min-width': getEffectiveMinWidth(layout) + 'px',
'min-height': safeMinHeight + 'px',
'box-sizing': 'border-box',
resize: layout.useFullWidth ? 'none' : 'both',
'border-bottom-left-radius': '',
'border-bottom-right-radius': ''
});
$ta.data('rmNestedContainerFrame', containerFrame);
$ta.data('rmNestedWrapFrame', wrapFrame);
$ta.data('rmNestedMinWidth', safeMinWidth);
$ta.next('[data-rm-textarea-grip]').remove();
if (layout.useFullWidth) {
bindTouchTextareaGrip($ta, sync, function () {
return getMaxTextareaWidth(getModalLayout());
}, {
minWidth: getEffectiveMinWidth(layout),
minHeight: safeMinHeight
});
}
registerModalLayoutSync(sync);
$(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
if (typeof ResizeObserver === 'function') {
var observer = new ResizeObserver(sync);
observer.observe($ta[0]);
registerResizeObserver(observer);
}
sync();
}
function setupResizableModal(textareaId) {
var $ta = $('#' + textareaId);
var $modal = $('#removerModal');
var layout = getModalLayout();
function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; }
applyV2022Layout($modal);
$modal.css({ display: 'block', 'margin-left': layout.shouldCenter ? 'auto' : '0', 'margin-right': layout.shouldCenter ? 'auto' : '0' });
var isBorderBox = ($modal.css('box-sizing') || '').toLowerCase() === 'border-box';
var hFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width');
var initialContentW = isVector22 ? ($('#content').outerWidth() || 0) : 0;
var minWidth = layout.minWidth;
$modal.data('rmInitialContentW', initialContentW);
$ta.css({ width: getDefaultResizableWidth(hFrame) + 'px', height: sz.taH, padding: '8px', 'box-sizing': 'border-box',
border: '1px solid ' + tk.bSub, 'border-radius': '2px', background: tk.bgBase,
color: 'inherit', resize: layout.useFullWidth ? 'none' : 'both', 'min-height': sz.taMinH, 'min-width': sz.taMinW });
$(window).off('.rmTaResize');
function sync() {
var currentLayout = getModalLayout();
var maxTextareaWidth = Math.max(minWidth, currentLayout.maxOuterWidth - Math.floor(hFrame));
var w = $ta.outerWidth();
if (w > maxTextareaWidth) {
$ta.css('width', maxTextareaWidth + 'px');
w = $ta.outerWidth();
}
applyModalContentWidth($modal, isBorderBox ? w : Math.min(currentLayout.maxOuterWidth - hFrame, w));
}
if (layout.useFullWidth) bindTouchTextareaGrip($ta, sync, function () {
return Math.max(minWidth, getModalLayout().maxOuterWidth - Math.floor(hFrame));
});
else {
$ta.css({ 'border-bottom-left-radius': '', 'border-bottom-right-radius': '' });
$ta.next('[data-rm-textarea-grip]').remove();
}
registerModalLayoutSync(sync);
$(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
if (typeof ResizeObserver === 'function') {
var observer = new ResizeObserver(sync);
observer.observe($ta[0]);
registerResizeObserver(observer);
}
sync();
}
function addInputRow(opts) {
var w = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0;
$('#' + opts.containerId).append(joinHtml([
'<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, w ? 'width:' + w + 'px;' : '', '">',
'<input type="text" class="', opts.inputClass || 'variantInput', '" placeholder="', opts.placeholder || '', '" style="', stInputBox, '">',
'<button type="button" class="rmRemoveInput" style="', stRemoveBtn, '" title="Удалить">−</button>',
'</div>'
]));
syncModalLayout();
}
$(document).off('click.rmRemoveInput').on('click.rmRemoveInput', '.rmRemoveInput', function () {
$(this).closest('.rmInputRow').remove();
syncModalLayout();
});
$(document).off('click.rmQuickPhraseInsert').on('click.rmQuickPhraseInsert', '.rmQuickPhraseActionBtn', function (e) {
var targetId;
var phrase;
e.preventDefault();
targetId = $(this).data('rmTarget');
phrase = $(this).attr('data-rm-phrase') || '';
if (!targetId) return;
insertTextIntoTextarea($('#' + targetId), phrase);
});
function buildMultiInputHtml(c) {
var addLabel = c.addBtnLabel || '+ Добавить';
return joinHtml([
'<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="', c.firstId, '" type="text" class="', c.inputClass || 'variantInput', '" placeholder="', c.firstPh || '', '" style="', stInputBox, '">',
buildSquareAddButtonHtml(c.addBtnId, addLabel.replace(/^\+\s*/, '') || 'Добавить'),
'</div>',
'<div id="', c.containerId, '"></div>'
]);
}
function wireMultiInput(c) {
$('#' + c.addBtnId).click(function () {
var count = $('#' + c.containerId + ' .rmInputRow').length;
if (typeof c.maxRows === 'number' && count >= c.maxRows) { alert(c.maxMsg || 'Достигнут лимит.'); return; }
addInputRow({ containerId: c.containerId, placeholder: c.addPh, inputClass: c.inputClass });
});
}
function buildSettingsFieldHtml(label, controlHtml, helpText, options) {
var opts = options || {};
var helpHtml = helpText
? '<div class="rmSettingsFieldHint">' + helpText + '</div>'
: '';
var labelHtml = opts.forId
? '<label class="rmSettingsFieldLabel" for="' + opts.forId + '">' + label + '</label>'
: '<div class="rmSettingsFieldLabel">' + label + '</div>';
return joinHtml([
'<div class="rmSettingsField">',
labelHtml,
'<div class="rmSettingsFieldControl">', controlHtml, '</div>',
helpHtml,
'</div>'
]);
}
function buildSettingsSectionHtml(title, bodyHtml, helpText, options) {
var opts = options || {};
var headerHtml = '';
var description = [];
if (opts.titleNote) description.push(opts.titleNote);
if (helpText) description.push(helpText);
if (title || description.length) {
headerHtml = joinHtml([
'<div class="rmSettingsSectionHeader">',
title ? '<div class="rmSettingsSectionTitle">' + title + '</div>' : '',
description.length ? '<div class="rmSettingsSectionDescription">' + description.join(' ') + '</div>' : '',
'</div>'
]);
}
return joinHtml([
'<div class="rmSettingsSection">',
headerHtml,
bodyHtml,
'</div>'
]);
}
function buildSettingsSimpleCheckboxHtml(id, text) {
return joinHtml([
'<label class="rmSettingsCheck">',
'<input id="', id, '" type="checkbox">',
'<span>', text, '</span>',
'</label>'
]);
}
function buildQuickPhrasesSettingsEditorHtml() {
return joinHtml([
'<div id="rmSettingsQuickPhrasesEditor" class="rmQuickPhraseEditor">',
'<div id="rmSettingsQuickPhrasesList" class="rmQuickPhraseList"></div>',
'<input id="rmSettingsQuickPhraseInput" type="text" autocomplete="off" style="', stInputFull, 'margin-bottom:0;">',
'<div id="rmSettingsQuickPhraseMeta" class="rmQuickPhraseMeta"></div>',
'</div>'
]);
}
function getQuickPhraseEditor() {
return $('#rmSettingsQuickPhrasesEditor');
}
function getQuickPhraseEditorState() {
var $editor = getQuickPhraseEditor();
var phrases = normalizeQuickPhrasesList($editor.data('rmQuickPhrases'), []);
var editingIndex = parseInt($editor.data('rmQuickPhraseEditingIndex'), 10);
if (isNaN(editingIndex)) editingIndex = -1;
return { editor: $editor, phrases: phrases, editingIndex: editingIndex };
}
function setQuickPhraseEditorState(phrases, editingIndex) {
var $editor = getQuickPhraseEditor();
var normalized = normalizeQuickPhrasesList(phrases, []);
var safeEditingIndex = parseInt(editingIndex, 10);
if (isNaN(safeEditingIndex) || safeEditingIndex < 0 || safeEditingIndex >= normalized.length) safeEditingIndex = -1;
if (!$editor.length) return;
$editor.data('rmQuickPhrases', normalized);
$editor.data('rmQuickPhraseEditingIndex', safeEditingIndex);
renderQuickPhraseEditor();
}
function clearQuickPhraseDropState() {
var $editor = getQuickPhraseEditor();
$editor.removeData('rmQuickPhraseDragIndex');
$editor.removeData('rmQuickPhraseDropIndex');
$editor.removeData('rmQuickPhraseDropAfter');
$editor.find('.rmQuickPhraseChip').removeClass('is-dragging is-drop-before is-drop-after');
}
function renderQuickPhraseEditor() {
var state = getQuickPhraseEditorState();
var $list = $('#rmSettingsQuickPhrasesList');
var $input = $('#rmSettingsQuickPhraseInput');
var $meta = $('#rmSettingsQuickPhraseMeta');
if (!state.editor.length || !$list.length || !$input.length) return;
if (state.phrases.length) {
$list.html(state.phrases.map(function (phrase, index) {
var chipClass = 'rmQuickPhraseChip' + (index === state.editingIndex ? ' is-editing' : '');
return joinHtml([
'<div class="', chipClass, '" draggable="true" data-rm-quick-index="', index, '">',
'<button type="button" class="rmQuickPhraseEditBtn" title="Редактировать фразу">', escapeHtml(phrase), '</button>',
'<button type="button" class="rmQuickPhraseRemoveBtn" title="Удалить фразу" aria-label="Удалить фразу">×</button>',
'</div>'
]);
}).join(''));
} else {
$list.html('<div class="rmQuickPhraseEmpty">Фразы пока не добавлены.</div>');
}
$input
.attr('placeholder', state.editingIndex >= 0 ? 'Изменить значение...' : 'Добавить значение...')
.toggleClass('is-editing', state.editingIndex >= 0);
$meta
.text('')
.hide();
}
function notifyQuickPhraseEditorChanged() {
var $editor = getQuickPhraseEditor();
if ($editor.length) $editor.trigger('rmQuickPhrasesChanged');
}
function startQuickPhraseEdit(index) {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
if (index < 0 || index >= state.phrases.length || !$input.length) return;
state.editor.data('rmQuickPhraseEditingIndex', index);
$input.val(state.phrases[index]);
renderQuickPhraseEditor();
$input.trigger('focus');
if ($input[0] && typeof $input[0].select === 'function') $input[0].select();
}
function cancelQuickPhraseEdit() {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
state.editor.data('rmQuickPhraseEditingIndex', -1);
if ($input.length) $input.val('').removeClass('is-editing');
renderQuickPhraseEditor();
}
function saveQuickPhraseInput() {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
var value = normalizeQuickPhraseValue($input.val());
var next = [];
if (!$input.length || !value) return false;
if (state.editingIndex >= 0) {
state.phrases.forEach(function (phrase, index) {
if (index === state.editingIndex) {
next.push(value);
return;
}
if (phrase !== value && next.indexOf(phrase) === -1) next.push(phrase);
});
} else {
next = state.phrases.slice();
if (next.indexOf(value) === -1) next.push(value);
}
state.editor.data('rmQuickPhrases', normalizeQuickPhrasesList(next, []));
state.editor.data('rmQuickPhraseEditingIndex', -1);
$input.val('').removeClass('is-editing');
clearQuickPhraseDropState();
renderQuickPhraseEditor();
notifyQuickPhraseEditorChanged();
return true;
}
function removeQuickPhrase(index) {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
if (index < 0 || index >= state.phrases.length) return;
state.phrases.splice(index, 1);
state.editor.data('rmQuickPhrases', state.phrases);
if (state.editingIndex === index) {
state.editor.data('rmQuickPhraseEditingIndex', -1);
if ($input.length) $input.val('').removeClass('is-editing');
} else if (state.editingIndex > index) {
state.editor.data('rmQuickPhraseEditingIndex', state.editingIndex - 1);
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
notifyQuickPhraseEditorChanged();
}
function reorderQuickPhrases(phrases, fromIndex, toIndex, placeAfter) {
var result = phrases.slice();
var insertIndex = toIndex + (placeAfter ? 1 : 0);
var item;
if (fromIndex < 0 || fromIndex >= result.length || toIndex < 0 || toIndex >= result.length) return result;
item = result.splice(fromIndex, 1)[0];
if (fromIndex < insertIndex) insertIndex--;
result.splice(insertIndex, 0, item);
return result;
}
function getQuickPhraseDropPointer(evt) {
var originalEvent = evt && (evt.originalEvent || evt);
if (!originalEvent) return null;
if (typeof originalEvent.clientX !== 'number' || typeof originalEvent.clientY !== 'number') return null;
return { x: originalEvent.clientX, y: originalEvent.clientY };
}
function getQuickPhraseDropTarget($list, pointer, dragIndex) {
var candidates = [];
var rowCandidates;
var minRowDistance = Infinity;
var bestBoundary = null;
var bestBoundaryDistance = Infinity;
if (!$list || !$list.length || !pointer) return null;
$list.children('.rmQuickPhraseChip').each(function () {
var index = parseInt($(this).attr('data-rm-quick-index'), 10);
var rect;
var rowDistance;
if (isNaN(index) || index === dragIndex) return;
rect = this.getBoundingClientRect();
if (!rect.width || !rect.height) return;
rowDistance = pointer.y < rect.top ? (rect.top - pointer.y) : (pointer.y > rect.bottom ? (pointer.y - rect.bottom) : 0);
candidates.push({
node: this,
index: index,
left: rect.left,
right: rect.right,
top: rect.top,
bottom: rect.bottom,
midX: rect.left + rect.width / 2,
rowDistance: rowDistance
});
if (rowDistance < minRowDistance) minRowDistance = rowDistance;
});
if (!candidates.length) return null;
rowCandidates = candidates
.filter(function (candidate) { return candidate.rowDistance === minRowDistance; })
.sort(function (a, b) {
if (a.left !== b.left) return a.left - b.left;
return a.index - b.index;
});
if (!rowCandidates.length) return null;
if (pointer.x <= rowCandidates[0].left) {
return { index: rowCandidates[0].index, placeAfter: false, node: rowCandidates[0].node };
}
if (pointer.x >= rowCandidates[rowCandidates.length - 1].right) {
return {
index: rowCandidates[rowCandidates.length - 1].index,
placeAfter: true,
node: rowCandidates[rowCandidates.length - 1].node
};
}
for (var i = 0; i < rowCandidates.length; i++) {
var candidate = rowCandidates[i];
if (pointer.x >= candidate.left && pointer.x <= candidate.right) {
return {
index: candidate.index,
placeAfter: pointer.x > candidate.midX,
node: candidate.node
};
}
}
rowCandidates.forEach(function (candidate) {
var leftDistance = Math.abs(pointer.x - candidate.left);
var rightDistance = Math.abs(pointer.x - candidate.right);
if (leftDistance < bestBoundaryDistance) {
bestBoundaryDistance = leftDistance;
bestBoundary = { index: candidate.index, placeAfter: false, node: candidate.node };
}
if (rightDistance < bestBoundaryDistance) {
bestBoundaryDistance = rightDistance;
bestBoundary = { index: candidate.index, placeAfter: true, node: candidate.node };
}
});
return bestBoundary;
}
function bindQuickPhrasesEditor() {
var $editor = getQuickPhraseEditor();
var $list = $('#rmSettingsQuickPhrasesList');
var $input = $('#rmSettingsQuickPhraseInput');
function updateQuickPhraseDropTarget(evt) {
var dragIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10);
var pointer = getQuickPhraseDropPointer(evt);
var target;
if (isNaN(dragIndex) || !pointer) return null;
target = getQuickPhraseDropTarget($list, pointer, dragIndex);
if (!target) return null;
$editor.data('rmQuickPhraseDropIndex', target.index);
$editor.data('rmQuickPhraseDropAfter', !!target.placeAfter);
$editor.find('.rmQuickPhraseChip').removeClass('is-drop-before is-drop-after');
$(target.node).addClass(target.placeAfter ? 'is-drop-after' : 'is-drop-before');
return target;
}
function applyQuickPhraseDrop() {
var state = getQuickPhraseEditorState();
var fromIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10);
var toIndex = parseInt($editor.data('rmQuickPhraseDropIndex'), 10);
var placeAfter = $editor.data('rmQuickPhraseDropAfter') === true;
var changed = false;
if (!isNaN(fromIndex) && !isNaN(toIndex) && fromIndex !== toIndex) {
state.editor.data('rmQuickPhrases', reorderQuickPhrases(state.phrases, fromIndex, toIndex, placeAfter));
if (state.editingIndex === fromIndex) state.editor.data('rmQuickPhraseEditingIndex', -1);
changed = true;
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
if (changed) notifyQuickPhraseEditorChanged();
}
if (!$editor.length || !$list.length || !$input.length) return;
$editor.off('.rmQuickPhraseEditor');
$list.off('.rmQuickPhraseEditor');
$input.off('.rmQuickPhraseEditor');
$input.on('keydown.rmQuickPhraseEditor', function (e) {
if (e.key === 'Enter' || e.keyCode === 13) {
e.preventDefault();
saveQuickPhraseInput();
} else if (e.key === 'Escape' || e.keyCode === 27) {
e.preventDefault();
cancelQuickPhraseEdit();
}
});
$editor
.on('click.rmQuickPhraseEditor', '.rmQuickPhraseEditBtn', function () {
var index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10);
startQuickPhraseEdit(index);
})
.on('click.rmQuickPhraseEditor', '.rmQuickPhraseRemoveBtn', function (e) {
var index;
e.preventDefault();
e.stopPropagation();
index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10);
removeQuickPhrase(index);
})
.on('dragstart.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
var index = parseInt($(this).attr('data-rm-quick-index'), 10);
if (isNaN(index)) return;
$editor.data('rmQuickPhraseDragIndex', index);
$(this).addClass('is-dragging');
if (e.originalEvent && e.originalEvent.dataTransfer) {
e.originalEvent.dataTransfer.effectAllowed = 'move';
e.originalEvent.dataTransfer.setData('text/plain', String(index));
}
})
.on('dragover.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
if ($editor.data('rmQuickPhraseDragIndex') === undefined) return;
e.preventDefault();
updateQuickPhraseDropTarget(e);
if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move';
})
.on('drop.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
e.preventDefault();
updateQuickPhraseDropTarget(e);
applyQuickPhraseDrop();
})
.on('dragend.rmQuickPhraseEditor', '.rmQuickPhraseChip', function () {
clearQuickPhraseDropState();
});
$list
.on('dragover.rmQuickPhraseEditor', function (e) {
if ($editor.data('rmQuickPhraseDragIndex') === undefined) return;
e.preventDefault();
updateQuickPhraseDropTarget(e);
if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move';
})
.on('drop.rmQuickPhraseEditor', function (e) {
e.preventDefault();
updateQuickPhraseDropTarget(e);
applyQuickPhraseDrop();
});
renderQuickPhraseEditor();
}
function collectQuickPhraseValues() {
return getQuickPhraseEditorState().phrases;
}
function collectQuickPhraseValuesSnapshot() {
var state = getQuickPhraseEditorState();
var value = normalizeQuickPhraseValue($('#rmSettingsQuickPhraseInput').val());
var next = state.phrases.slice();
if (!value) return next;
if (state.editingIndex >= 0) {
next[state.editingIndex] = value;
} else if (next.indexOf(value) === -1) {
next.push(value);
}
return normalizeQuickPhrasesList(next, []);
}
function isMenuTitlePresetValue(value) {
return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS;
}
function isMenuTitlePresetOnlySkin() {
return mwCfg.skin === 'minerva' || mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
}
function getDefaultMenuTitlePreset() {
return mwCfg.skin === 'minerva' ? MENU_TITLE_PRESET_TOOLS : MENU_TITLE_PRESET_CACTIONS;
}
function shouldPreserveStoredMenuTitleOnSave() {
return mwCfg.skin === 'minerva';
}
function isAvailableMenuTitlePresetValue(value) {
return getMenuTitlePresetOptions().some(function (option) {
return option.value === value;
});
}
function getMenuTitlePresetOptions() {
if (mwCfg.skin === 'minerva') {
return [
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Ещё' }
];
}
if (isVector22) {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты/Действия' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты/Основное' }
];
}
if (mwCfg.skin === 'timeless') {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты для страниц' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Вики-инструменты' }
];
}
if (mwCfg.skin === 'monobook') {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Верхняя панель' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' }
];
}
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: mwCfg.skin === 'vector' ? 'Ещё' : 'Ещё / Действия' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' }
];
}
function getMenuTitlePresetHintText() {
var base = (mwCfg.skin === 'vector' || isVector22)
? 'Можно либо изменить заголовок отдельного меню Remover, либо перенести все пункты в одно из существующих стандартных меню.'
: 'На этом скине Remover использует существующие стандартные меню; отдельное меню с собственным заголовком не создаётся.';
if (isVector22) base += ' В Vector 2022 кнопки «Действия» и «Основное» находятся внутри общего меню «Инструменты».';
else if (mwCfg.skin === 'vector') base += ' В Vector отдельный заголовок создаёт собственное меню рядом с «Ещё».';
else if (mwCfg.skin === 'minerva') base += ' В Minerva Neue пункты Remover показываются в меню «Ещё».';
else if (mwCfg.skin === 'timeless') base += ' В Timeless доступны только стандартные варианты: «Инструменты для страниц», «Страница» и «Вики-инструменты».';
else if (mwCfg.skin === 'monobook') base += ' В MonoBook доступны только два варианта: поместить пункты в «Инструменты» или вывести их в верхнюю панель. Собственный заголовок меню здесь не используется.';
return base;
}
function getSignatureSeparatorPreviewText(value) {
var separator = String(value || '').trim();
return separator ? (separator + ' ' + '~~' + '~~') : ('~~' + '~~');
}
function updateSignatureSeparatorPreview(value) {
var previewValue = (typeof value === 'string') ? value : ($('#rmSettingsSignatureSeparator').val() || '');
var $code = $('#rmSettingsSignaturePreviewCode');
if (!$code.length) return;
$code.text(getSignatureSeparatorPreviewText(previewValue));
}
function bindSignatureSeparatorPreview() {
var $input = $('#rmSettingsSignatureSeparator');
if (!$input.length) return;
$input.off('.rmSignaturePreview').on('input.rmSignaturePreview change.rmSignaturePreview', function () {
updateSignatureSeparatorPreview($(this).val());
});
updateSignatureSeparatorPreview($input.val());
}
function buildMenuTitlePresetButtonsHtml() {
return joinHtml([
'<div class="rmSettingsMenuPresetWrap">',
'<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>',
'<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">',
getMenuTitlePresetOptions().map(function (option) {
return joinHtml([
'<button type="button" class="rmSettingsMenuPresetBtn" data-rm-menu-preset="',
option.value,
'" aria-pressed="false">',
escapeHtml(option.label),
'</button>'
]);
}).join(''),
'</div>',
'</div>'
]);
}
function applyMenuTitlePresetControls(presetValue) {
var preset = isMenuTitlePresetValue(presetValue) ? presetValue : '';
var $bar = $('#rmSettingsMenuPresetBar');
var $input = $('#rmSettingsMenuTitle');
var forcePresetOnly = isMenuTitlePresetOnlySkin();
if (!$bar.length || !$input.length) return;
$bar.data('rmPreset', preset);
$bar.find('.rmSettingsMenuPresetBtn').each(function () {
var isActive = $(this).data('rmMenuPreset') === preset;
$(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false');
});
$input.prop('disabled', forcePresetOnly || !!preset);
}
function bindMenuTitlePresetControls() {
var $bar = $('#rmSettingsMenuPresetBar');
var $input = $('#rmSettingsMenuTitle');
if (!$bar.length || !$input.length) return;
$input.off('.rmSettingsMenuPreset').on('input.rmSettingsMenuPreset', function () {
if (!$bar.data('rmPreset')) $input.data('rmCustomValue', $input.val());
});
$bar.off('.rmSettingsMenuPreset').on('click.rmSettingsMenuPreset', '.rmSettingsMenuPresetBtn', function () {
var preset = $(this).data('rmMenuPreset');
var currentPreset = $bar.data('rmPreset') || '';
if (currentPreset === preset) {
if (isMenuTitlePresetOnlySkin()) return;
applyMenuTitlePresetControls('');
$input.val($input.data('rmCustomValue') || '');
$input.trigger('focus');
return;
}
$input.data('rmCustomValue', $input.val());
applyMenuTitlePresetControls(preset);
});
}
function fillSettingsFormValues(settings) {
var data = normalizeRemoverSettings(settings);
var forcePresetOnly = isMenuTitlePresetOnlySkin();
var menuTitleValue = data.menuTitle || '';
var storedMenuTitleValue = menuTitleValue;
if (forcePresetOnly && !isAvailableMenuTitlePresetValue(menuTitleValue)) menuTitleValue = getDefaultMenuTitlePreset();
var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue);
$('#rmSettingsForm').data('rmStoredMenuTitle', storedMenuTitleValue || '');
$('#rmSettingsNotifyAuthor').prop('checked', !!data.notifyAuthor);
$('#rmSettingsSubscribeTopic').prop('checked', !!data.subscribeTopic);
$('#rmSettingsShowMenuIcons').prop('checked', !!data.showMenuIcons);
$('#rmSettingsMenuTitle').val(customMenuTitle || '').data('rmCustomValue', customMenuTitle || '');
$('#rmSettingsSignatureSeparator').val(data.signatureSeparator || '');
$('#rmSettingsExcludedNamespaces').val((data.excludedNamespaces || []).join(', '));
$('#rmSettingsDisabledItems').val((data.disabledItems || []).join(', '));
setQuickPhraseEditorState(data.quickPhrases || [], -1);
$('#rmSettingsQuickPhraseInput').val('').removeClass('is-editing');
clearQuickPhraseDropState();
applyMenuTitlePresetControls(menuTitleValue);
updateSignatureSeparatorPreview(data.signatureSeparator || '');
}
function collectSettingsFormValues(options) {
var opts = options || {};
var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val());
var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val());
var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset');
var forcePresetOnly = isMenuTitlePresetOnlySkin();
var storedMenuTitle = $('#rmSettingsForm').data('rmStoredMenuTitle');
if (namespaces.invalid.length) {
return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' };
}
if (disabledItems.invalid.length) {
return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' };
}
if (!opts.skipQuickPhraseCommit) saveQuickPhraseInput();
return {
value: normalizeRemoverSettings({
notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'),
subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'),
showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'),
menuTitle: forcePresetOnly
? (shouldPreserveStoredMenuTitleOnSave() && typeof storedMenuTitle === 'string'
? storedMenuTitle
: (isAvailableMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : getDefaultMenuTitlePreset()))
: (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()),
signatureSeparator: $('#rmSettingsSignatureSeparator').val(),
excludedNamespaces: namespaces.values,
disabledItems: disabledItems.values,
quickPhrases: opts.skipQuickPhraseCommit ? collectQuickPhraseValuesSnapshot() : collectQuickPhraseValues()
})
};
}
function updateSettingsSubmitReadyState(baselineSettings) {
var collected = collectSettingsFormValues({ skipQuickPhraseCommit: true });
var hasChanges = !!(collected.error || (collected.value && !areRemoverSettingsEqual(collected.value, baselineSettings)));
$('#removerSubmit').toggleClass('rmSubmitReady', hasChanges && !$('#removerSubmit').hasClass('rmSubmitError'));
$('#rmSettingsUnsavedHint').css('display', hasChanges ? 'inline-block' : 'none');
}
function bindSettingsSubmitReadyState(baselineSettings) {
var update = function () {
setTimeout(function () { updateSettingsSubmitReadyState(baselineSettings); }, 0);
};
$('#rmSettingsForm').off('.rmSettingsReady').on('input.rmSettingsReady change.rmSettingsReady', 'input, textarea, select', update);
$('#removerModalContent').off('click.rmSettingsReady keyup.rmSettingsReady').on(
'click.rmSettingsReady keyup.rmSettingsReady',
'.rmSettingsMenuPresetBtn,.rmQuickPhraseEditBtn,.rmQuickPhraseRemoveBtn,.rmQuickPhraseChip,#rmSettingsQuickPhraseInput',
update
);
$('#rmSettingsQuickPhrasesEditor').off('rmQuickPhrasesChanged.rmSettingsReady').on('rmQuickPhrasesChanged.rmSettingsReady', update);
updateSettingsSubmitReadyState(baselineSettings);
}
function buildSettingsFormHtml(menuLabelsHint) {
var menuFields =
buildSettingsFieldHtml('Заголовок отдельного меню',
'<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(),
getMenuTitlePresetHintText(), { forId: 'rmSettingsMenuTitle' }) +
buildSettingsFieldHtml('Визуальное оформление меню',
'<div class="rmSettingsChecks">' + buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи в пунктах меню') + '</div>');
var messageFields =
buildSettingsFieldHtml('Префикс перед подписью',
'<input id="rmSettingsSignatureSeparator" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Добавляется перед подписью в публикуемых сообщениях.<span style="display:block;margin-top:6px;">Предпросмотр: <code id="rmSettingsSignaturePreviewCode"></code>.</span>',
{ forId: 'rmSettingsSignatureSeparator' }) +
buildSettingsFieldHtml('Часто используемые фразы', buildQuickPhrasesSettingsEditorHtml(),
'Enter для добавления. Порядок элементов изменяется перетаскиванием.', { forId: 'rmSettingsQuickPhraseInput' });
var defaultFields = '<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') +
buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') + '</div>';
var disableFields =
buildSettingsFieldHtml('Скрыть пункты меню',
'<input id="rmSettingsDisabledItems" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Названия пунктов через запятую, например <code>КБУ, КУЛ, КОБ</code>.' + menuLabelsHint,
{ forId: 'rmSettingsDisabledItems' }) +
buildSettingsFieldHtml('Не показывать в пространствах имён',
'<input id="rmSettingsExcludedNamespaces" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Номера пространств имён через запятую, например <code>2, 10, 828</code>. См. <a href="' + getPageUrl('Википедия:Пространства имён') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Пространства имён</a>.',
{ forId: 'rmSettingsExcludedNamespaces' });
return joinHtml([
'<div id="rmSettingsForm" style="max-width:100%;">',
'<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>',
buildSettingsSectionHtml('Меню', menuFields, 'Настройки внешнего вида и состава меню Remover.'),
buildSettingsSectionHtml('Оформление сообщений', messageFields, 'Настройки оформления публикуемых сообщений в номинациях.'),
buildSettingsSectionHtml('Опции по умолчанию', defaultFields, 'Регулирует изначальное состояние галочек.'),
buildSettingsSectionHtml('Отключение', disableFields, 'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'),
'</div>'
]);
}
function buildSettingsFooterLeftHtml() {
return joinHtml([
'<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;margin-right:auto;">',
'<button id="rmSettingsResetFooter" type="button" title="Удаляет сохранённые настройки Remover из вашего профиля MediaWiki и возвращает значения по умолчанию." style="', stCancel, '">Сбросить все настройки</button>',
'<a id="rmSettingsReportIssue" href="', getPageUrl('Обсуждение участника:Solidest/Remover'), '" target="_blank" rel="noopener noreferrer" ',
'title="Сообщить о проблеме или предложить улучшение" aria-label="Сообщить о проблеме или предложить улучшение" ',
'class="removerModalLink rmButtonLikeLink" style="', stCancel, 'display:inline-flex;align-items:center;justify-content:center;text-align:center;text-decoration:none;box-sizing:border-box;max-width:100%;line-height:1.2;word-break:normal;overflow-wrap:normal;">Обратная связь</a>',
'</div>'
]);
}
function openSettings() {
var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults);
var menuLabelsHint = buildSettingsMenuItemsHint();
var $previousModal = $('#removerModal').length ? $('#removerModal').detach() : $();
var previousLayoutSyncHandlers = modalLayoutSyncHandlers.slice();
function restorePreviousModal() {
closeModal();
if ($previousModal.length) {
$('#content').prepend($previousModal);
modalLayoutSyncHandlers = previousLayoutSyncHandlers.slice();
if (modalLayoutSyncHandlers.length) $(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
syncModalLayout();
syncLinkWidths();
}
}
createModal({
title: 'Конфигурация',
width: 'compact',
showSettingsButton: false
});
$('#removerModal').addClass('rmModalSettings');
$('#removerModalHeaderBar').append(buildHeaderIconButtonHtml('rmSettingsBack', 'Назад', 'Назад', '←'));
$('#rmSettingsBack').on('click', restorePreviousModal);
$('#removerModalContent').html(buildSettingsFormHtml(menuLabelsHint));
fillSettingsFormValues(currentSettings);
bindMenuTitlePresetControls();
bindSignatureSeparatorPreview();
bindQuickPhrasesEditor();
renderModalFooter('submit', {
showCheckbox: false,
submitText: 'Сохранить',
onSubmit: function () {
var collected = collectSettingsFormValues();
var shouldReset;
var saveFn;
if (collected.error) {
alert(collected.error);
return false;
}
shouldReset = areRemoverSettingsEqual(collected.value, settingsDefaults);
saveFn = shouldReset
? function (callback) { resetSettingsOnServer(callback); }
: function (callback) { saveSettingsToServer(collected.value, callback); };
saveFn(function (err) {
if (err) {
alert('Не удалось ' + (shouldReset ? 'сбросить' : 'сохранить') + ' настройки: ' + (err.info || err.code || 'неизвестная ошибка') + '.');
unlockModalSubmit();
return;
}
location.reload();
});
}
});
var $settingsActions = $('#rmFooterActionButtons');
$settingsActions.wrapInner('<div id="rmSettingsActionButtonsRow"></div>');
$settingsActions.append('<span id="rmSettingsUnsavedHint" role="status" aria-live="polite">Есть несохранённые изменения</span>');
bindSettingsSubmitReadyState(currentSettings);
$('#rmFooterButtons').css('justify-content', 'space-between').prepend(buildSettingsFooterLeftHtml());
$('#rmSettingsResetFooter').on('click', function () {
fillSettingsFormValues(settingsDefaults);
updateSettingsSubmitReadyState(currentSettings);
$('#removerSubmit').trigger('focus');
});
}
// ─── Завершение обработки ────────────────────────────────────────────────
function finalizeSuccess(nominationInfo, usePageReload) {
if (isError) {
var $box = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent');
$box.append('<p class="error">При выполнении скрипта произошли ошибки.</p>');
markSubmitError();
return;
}
renderModalFooter('reload');
if (nominationInfo && nominationInfo.pageTitle) {
appendNominationLink(nominationInfo.pageTitle, nominationInfo.sectionTitle);
}
if (!usePageReload && !nominationInfo) location.reload();
}
function finalizeFastRemoval(notifiedPages, summary) {
if (isError || !setAlert || !notifiedPages || !notifiedPages.length) {
finalizeSuccess(null, false);
return;
}
notifyAuthorsForPages(notifiedPages, {
summary: summary,
actionText: 'к быстрому удалению'
}, function () {
finalizeSuccess(null, false);
});
}
// ─── Общий runner ────────────────────────────────────────────────────────
/**
* Универсальный запуск полного пайплайна номинации.
* @param {Object} o
* templateStep — функция (next) → обработка шаблонов на статьях
* nominationStep — функция (done) → публикация номинации, done(err, {pageTitle, sectionTitle})
* notifyStep — функция (nominationInfo, next)
* skipNotify — boolean
* skipLink — boolean, не показывать ссылку на номинацию
*/
function runFlow(o) {
runNominationPipeline({
templateStep: o.templateStep,
nominationStep: o.nominationStep,
notifyStep: o.notifyStep || function (info, next) { next(); },
skipNotify: o.skipNotify,
onSuccess: function (ctx) {
if (isError) { markSubmitError(); return; }
renderModalFooter('reload');
if (!o.skipLink && ctx.nominationInfo && ctx.nominationInfo.pageTitle) {
appendNominationLink(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle);
}
},
onFailure: function () { markSubmitError(); }
});
}
// ═══════════════════════════════════════════════════════════════════════════
// ЯДРО: обработка статей (apply template + nomination page)
// ═══════════════════════════════════════════════════════════════════════════
/**
* Применяет шаблон к одной статье/категории.
* Понимает режим inArticle (вставка через <noinclude>),
* режим closeAction (снятие шаблона + запись на СО),
* режим cleanupAction (снятие КБУ/КУЛ).
*
* @param {string} pg — название страницы
* @param {Object} job — параметры задания (см. buildJob)
* @param {function} callback(err, meta)
*/
function applyTemplateToPage(pg, job, callback) {
var mode = job.mode;
// ── Снятие КБУ/КУЛ ──────────────────────────────────────────────────
if (mode === 'cleanup') {
var tm = job.transferMode || 'none';
if (tm === 'none') { callback({ code: 'error', info: 'Не выбран режим снятия шаблонов.' }); return; }
editPageContent(pg, { summary: job.summary, watchlist: 'nochange', readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var local = removeTransferTemplatesLocal(article, tm);
removeTransferTemplatesWithApiFallback(pg, local.text, tm, local, function (updated) {
if (updated.text === article) { done({ error: { code: 'error', info: 'Шаблоны для снятия не найдены.' } }); return; }
done({ text: updated.text });
});
}, function (err) { callback(err); });
return;
}
// ── Подведение итогов по КУ/КПМ (снятие + итог на СО) ───────────────
if (mode === 'denom') {
getTextWithTimestamp(pg, function (article, baseTimestamp, readErr) {
if (readErr) { callback(makeReadError(readErr, 'read_failed', 'Не удалось получить содержимое страницы «' + pg + '».')); return; }
if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; }
if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; }
var tplPattern = job.sourceTemplate.split('|').map(function (alias) {
return escapeRegExp(alias.trim()).replace(/\s+/g, '[ _]*');
}).join('|');
var tpl = findTemplateByPattern(article, tplPattern);
if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; }
var normalizedTplDate = convertToStandardDate(tpl.params[0]);
var tplExtra = tpl.params.slice(1).join('|').trim();
if (!RE_DATE_ISO.test(normalizedTplDate)) {
callback({ code: 'error', info: 'Не удалось распознать дату в шаблоне: «' + (tpl.params[0] || '') + '».' });
return;
}
var date = getDate(normalizedTplDate);
var nomPlace = 'ВП:' + job.sourceTemplate.replace(/\|.*/, '') + '/' + date[1];
var retTalkSection = '';
var sectionNW, tplpar, newTalkTpl;
if (job.closeType === 'doneRnm') { sectionNW = job.oldTitle + ' → ' + pg; tplpar = job.oldTitle + '|' + pg; }
if (job.closeType === 'noRnm') { sectionNW = pg + ' → ' + tplExtra; tplpar = pg + '|' + tplExtra; }
if (job.closeType === 'ret' || job.closeType === 'retConditional') {
retTalkSection = tplExtra;
sectionNW = retTalkSection || pg;
tplpar = retTalkSection ? ('l1=' + retTalkSection) : '';
}
var editSummary = makeSummary('номинация [[' + (nomPlace ? nomPlace + '#' : '') + sectionNW + ']] — ' + job.resultTemplate);
var talkTitle = getTalkPage(pg);
newTalkTpl = (job.closeType === 'retConditional')
? buildConditionalRetTemplateText(date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline, 1)
: (T_OPEN + job.resultTemplate + '|' + date[0] + '|' + tplpar + T_CLOSE);
getTextWithTimestamp(talkTitle, function (talkText, talkTimestamp, talkReadErr) {
if (talkReadErr) { callback(makeReadError(talkReadErr, 'talk_read_failed', 'Не удалось получить содержимое СО страницы «' + pg + '».')); return; }
var sourceTalkText = talkText || '';
var talkResult = (job.closeType === 'ret')
? upsertRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection)
: (job.closeType === 'retConditional')
? upsertConditionalRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline)
: { text: insertTplOnTalkPage(sourceTalkText, newTalkTpl, '\n'), status: 'created' };
function saveArticle() {
var cleaned = stripTemplatesByPattern(article, tplPattern).text;
var ep = { title: pg, text: cleaned, summary: editSummary, watchlist: 'nochange', assertuser: mwCfg.wgUserName };
if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (t) {
var editErr = t && t.error ? t.error : null;
callback(editErr, editErr ? null : {
discussionPage: nomPlace,
discussionSection: sectionNW,
summary: editSummary
});
});
}
if (talkResult.text === sourceTalkText) { saveArticle(); return; }
var talkEp = { title: talkTitle, text: talkResult.text, summary: editSummary };
if (talkTimestamp) talkEp.basetimestamp = talkTimestamp;
apiReq(talkEp, 'edit', function (talkResp) {
if (talkResp && talkResp.error) { callback({ code: 'talk_failed', info: 'Не удалось записать итог на СО: ' + talkResp.error.info }); return; }
saveArticle();
});
});
});
return;
}
// ── Обычная номинация: вставка шаблона в статью ─────────────────────
// mode === 'nominate'
var isKu = job.opId === 'tRm' || job.opId === 'mRm';
editPageContent(pg, { summary: job.summary, readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var hasExistingKu = isKu && RE_KU_ON_PAGE.test(article);
var conflictDecision = getConflictDecisionForPage(job, pg);
function buildResult(finalText) {
var generatedTpl = buildGeneratedNominationTemplateText(job, pg);
return { text: generatedTpl ? wrapInNoinclude(finalText, generatedTpl) : finalText };
}
function finishConflictResolution(sourceText) {
var resolvedText;
var pageLink = buildQuotedStatusPageLink(pg);
if (conflictDecision.templateAction === 'keep') {
if (sourceText !== article) {
return {
text: sourceText,
meta: { successMessage: 'Шаблон на странице ' + pageLink + ' оставлен без изменений; шаблоны переноса сняты.' }
};
}
return { skip: true, meta: { successMessage: 'Шаблон на странице ' + pageLink + ' оставлен без изменений.' } };
}
resolvedText = applyConflictTemplateResolution(sourceText, job, pg, conflictDecision);
return {
text: resolvedText,
meta: {
successMessage: conflictDecision.templateAction === 'overwrite'
? 'Шаблон КУ на странице ' + pageLink + ' перезаписан новой датой.'
: 'Новый шаблон КУ добавлен сверху на странице ' + pageLink + '.'
}
};
}
if (hasExistingKu && (!conflictDecision || conflictDecision.pageAction !== 'keep')) {
return { error: { code: 'error', info: 'На странице уже стоит шаблон КУ.' } };
}
if (hasExistingKu && conflictDecision && conflictDecision.pageAction === 'keep') {
if (job.transferMode && job.transferMode !== 'none') {
var localConflict = removeTransferTemplatesLocal(article, job.transferMode);
removeTransferTemplatesWithApiFallback(pg, localConflict.text, job.transferMode, localConflict, function (updated) {
done(finishConflictResolution(updated.text));
});
return;
}
return finishConflictResolution(article);
}
if (isKu && job.transferMode && job.transferMode !== 'none') {
var local = removeTransferTemplatesLocal(article, job.transferMode);
removeTransferTemplatesWithApiFallback(pg, local.text, job.transferMode, local, function (updated) { done(buildResult(updated.text)); });
return;
}
return buildResult(article);
},
function (err) { callback(err); }
);
}
/**
* Обрабатывает список страниц последовательно.
* @param {string[]} pages
* @param {Object} job
* @param {function} onDone(notifiedPages, err, pageMeta)
*/
function processPageList(pages, job, onDone) {
var notifiedPages = [];
var pageMeta = {};
eachSequential(pages.slice().reverse(), function (pg, nextPage) {
var pageLink = buildQuotedStatusPageLink(pg);
var statusId = logStatus('Обрабатывается страница ' + pageLink + '...', null, { pending: true, trackError: false });
applyTemplateToPage(pg, job, function (err, meta) {
var normPg = normTitle(pg);
var isClose = job.mode === 'cleanup' || job.mode === 'denom';
if (!isClose) {
if (!err && meta && meta.successMessage) logStatus(meta.successMessage, null, { statusId: statusId, trackError: false });
else logPageEdit(pg, err, { statusId: statusId });
} else {
if (err) { logStatus('Завершение по странице ' + pageLink, err, { statusId: statusId }); }
else {
logStatus('Шаблон снят со страницы ' + pageLink + '.', null, { statusId: statusId, trackError: false });
if (job.mode === 'denom') logStatus('Шаблон установлен на СО страницы ' + pageLink + '.', null, { trackError: false });
}
}
if (!err) {
notifiedPages.push(pg);
if (meta) pageMeta[normPg] = meta;
}
nextPage(err || null);
});
}, function (err) { onDone(notifiedPages, err, pageMeta); });
}
// ═══════════════════════════════════════════════════════════════════════════
// ПОСТРОЕНИЕ JOB из формы
// ═══════════════════════════════════════════════════════════════════════════
/**
* Строит объект job из данных формы для операции номинации (tRm, rnm, imp, merge, split, recov).
* @param {Object} op — запись из OPERATIONS
* @param {string} pg — целевая страница (уже разрешённая)
* @param {boolean} isMulti — режим мультиноминации
* @returns {Object|false} — job или false при ошибке ввода
*/
function buildNominationJob(op, pg, isMulti) {
var nom = op.nomination;
var date = getDate();
var msg = normalizeQuickPhraseValue($('#rmMsg').val());
var rawMsg = msg;
var opId = isMulti ? 'mRm' : op.id;
var tplpar = '';
var section, sectionNW, extraPages, multiArticles = [];
var multiHeaderText = '';
var multiArticleComments = {};
// Вычислить section и tplpar в зависимости от типа дополнительного ввода
if (nom.extraInput) {
var ei = nom.extraInput;
if (ei.type === 'rename') {
var rn = collectInputValues('.rmRenameInput');
if (!rn.length) { alert('Укажите новое название.'); return false; }
tplpar = rn[0] + (rn.length > 1 ? '||' + rn.slice(1).join('|') : '');
section = '[[:' + pg + ']] → ' + rn.map(function (n) { return '[[:' + n + ']]'; }).join(', ');
} else if (ei.type === 'merge') {
var mn = collectInputValues('.rmMergeInput');
if (!mn.length) { alert('Укажите статью для объединения.'); return false; }
tplpar = pg + '|' + mn.join('|');
extraPages = mn;
section = formatPagesWithAnd([pg].concat(mn));
} else if (ei.type === 'split') {
var sn = collectInputValues('.rmSplitInput');
if (!sn.length) { alert('Укажите статьи для разделения.'); return false; }
tplpar = formatPagesWithAnd(sn);
section = '[[:' + pg + ']] → ' + tplpar;
}
}
if (isMulti) {
var ttl = $('#rmHeader').val() || '';
var articles = collectInputValues('.rmArticleInput');
var multiFormat = $('.rmArticleMultiFormatBtn.is-active').data('rmMultiFormat') || 'sections';
multiArticleComments = collectMultiNominationComments();
multiHeaderText = ttl;
multiArticles = articles.slice();
section = ttl;
msg = multiFormat === 'list'
? buildMultiNominationListText(articles, rawMsg, multiArticleComments)
: buildMultiNominationText(articles, rawMsg, multiArticleComments);
}
if (!section) section = '[[:' + pg + ']]';
sectionNW = section.replace(/\[\[:/g, '').replace(/]]/g, '');
var nomPageDate = date[1];
var nomPage = nom.nomPage(nomPageDate);
var summary = makeSummary('номинация [[' + nomPage + '#' + sectionNW + ']]');
return {
mode: 'nominate',
opId: opId,
op: op,
date: date,
tplpar: tplpar,
articleTpl: nom.articleTpl || function () { return ''; },
inArticle: nom.inArticle !== false,
transferMode: (nom.supportsTransfer ? getTransferModeFromButtons() : 'none'),
summary: summary,
msg: msg,
nomPage: nomPage,
navTemplate: nom.navTemplate,
section: section,
sectionNW: sectionNW,
comment: nom.comment || '',
extraPages: extraPages || [],
isMulti: !!isMulti,
multiHeaderText: multiHeaderText,
multiNominationBody: rawMsg,
multiArticleComments: multiArticleComments,
multiNominationFormat: multiFormat || 'sections',
multiArticles: multiArticles,
pages: isMulti ? multiArticles.slice().reverse() : ([pg].concat(extraPages || []))
};
}
function getTransferModeFromButtons() {
var kbu = $('#rmTransferBtnKbu').hasClass('is-active');
var kul = $('#rmTransferBtnKul').hasClass('is-active');
if (kbu && kul) return 'both';
if (kbu) return 'kbu';
if (kul) return 'kul';
return 'none';
}
function buildKbuFormHtml(reasons) {
return joinHtml([
'<select id="rmSel" style="', stInputFull, '">',
reasons.map(function (r, i) { return '<option value="' + i + '">' + r[1] + '</option>'; }).join(''),
'</select>',
'<input id="fiRm" type="hidden" style="', stInputFull, '">',
'<input id="fiRmComment" type="text" placeholder="Комментарий (необязательно)" style="', stInputFull, '">',
buildQuickPhrasesPanelHtml('fiRmComment')
]);
}
function buildNominationMultiHeaderHtml(pg, options) {
var opts = options || {};
var multiHeaderRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
return joinHtml([
'<div id="rmMultiHeader" class="', RESIZE_CLASS, '" style="display:none;margin-bottom:6px;">',
'<div class="rmArticleRow rmNominationHeaderRow" style="', multiHeaderRowStyle, '"><input id="rmHeader" type="text" placeholder="Заголовок номинации" style="', stInputBox, '">',
buildAddArticleButtonHtml(opts), '</div>',
'</div>',
'<div id="rmArticlesContainer" style="display:flex;flex-direction:column;gap:', multiNominationGap, ';margin-bottom:', multiNominationGap, ';">',
buildMultiArticleRowHtml(0, $.extend({}, opts, { rowId: 'rmFirstArticle', articleValue: pg, showAdd: true })),
'</div>'
]);
}
function buildTransferBoxHtml() {
return joinHtml([
'<div id="rmTransferBox" class="', RESIZE_CLASS, ' rmTransferPanel" style="width:100%;box-sizing:border-box;"><div class="rmTransferGrid">',
'<div id="rmTransferModeSingle" class="rmSegmentedBar"><button type="button" id="rmTransferBtnNone" class="rmSegmentedBtn rmToggleBtn is-active" aria-pressed="true">Обычная номинация</button></div>',
'<div id="rmTransferModeGroup" class="rmSegmentedBar">',
'<button type="button" id="rmTransferBtnKbu" class="rmSegmentedBtn rmToggleBtn" aria-pressed="false" title="Шаблоны db-*, уд-*, КОУ, Hangon.">Снять КБУ</button>',
'<button type="button" id="rmTransferBtnKul" class="rmSegmentedBtn rmToggleBtn" aria-pressed="false" title="Шаблон «К улучшению».">Снять КУЛ</button>',
'</div>',
'<div class="rmTransferHintRow"><div id="rmTransferHint" style="display:none;font-size:12px;line-height:1.35;color:', tk.cSubM, ';"></div></div>',
'</div></div>'
]);
}
function buildMultiNominationFormatSwitchHtml(wrapId, buttonClass) {
return joinHtml([
'<div id="', wrapId, '" class="', RESIZE_CLASS, '" style="display:none;margin-top:8px;margin-bottom:10px;">',
'<div class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn ', buttonClass, ' is-active" data-rm-multi-format="sections" aria-pressed="true">Оформить подразделами</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn ', buttonClass, '" data-rm-multi-format="list" aria-pressed="false">Оформить списком</button>',
'</div>',
'</div>'
]);
}
function buildNominationFormHtml(nom, pg, multiMode) {
return joinHtml([
multiMode ? buildNominationMultiHeaderHtml(pg) : '',
nom.extraInput ? buildMultiInputHtml(nom.extraInput) : '',
'<textarea id="rmMsg" placeholder="Текст номинации без подписи"></textarea>',
buildQuickPhrasesPanelHtml('rmMsg'),
nom.supportsTransfer ? buildTransferBoxHtml() : '',
multiMode ? buildMultiNominationFormatSwitchHtml('rmArticleMultiFormatWrap', 'rmArticleMultiFormatBtn') : ''
]);
}
function buildCategoryNominationFormHtml(variantConfig, multiMode, pageName) {
return joinHtml([
multiMode ? buildNominationMultiHeaderHtml(pageName, {
inputPlaceholder: 'Категория',
addTitle: 'Добавить категорию',
removeTitle: 'Удалить категорию',
commentPlaceholder: 'Комментарий только для этой категории (необязательно)'
}) : '',
variantConfig ? buildMultiInputHtml(variantConfig) : '',
'<textarea id="nominationReason" placeholder="Текст номинации без подписи"></textarea>',
buildQuickPhrasesPanelHtml('nominationReason'),
multiMode ? buildMultiNominationFormatSwitchHtml('rmCategoryMultiFormatWrap', 'rmCategoryMultiFormatBtn') : ''
]);
}
function ensureMultiPageCommentTextareaResizer(textareaId, wrapId) {
var $textarea = $('#' + textareaId);
if (!$textarea.length || $textarea.data('rmNestedResizerReady')) return;
setupNestedResizableTextarea(textareaId, wrapId, parseInt(sz.taMinW, 10) || 180, 90);
$textarea.data('rmNestedResizerReady', true);
}
function setMultiPageCommentExpanded($btn, expanded) {
var wrapId = $btn.data('rmCommentWrap');
var textareaId = $btn.data('rmCommentTextarea');
var $wrap = $('#' + wrapId);
if (!$btn.length || !$wrap.length) return;
$btn.attr('aria-expanded', expanded ? 'true' : 'false')
.toggleClass('is-active', expanded)
.text(expanded ? 'Скрыть комментарий' : 'Комментарий');
$wrap.toggle(expanded);
if (expanded) ensureMultiPageCommentTextareaResizer(textareaId, wrapId);
}
function getMultiPageCommentTargets($block) {
var $textarea = $block.find('.rmArticleCommentInput').first();
var $wrap = $textarea.parent();
return {
wrapId: $wrap.attr('id') || '',
textareaId: $textarea.attr('id') || ''
};
}
function setMultiPageRowControls($block, showAdd, showComment, options) {
var opts = options || {};
var $row = $block.find('.rmArticleRow').first();
var ids = getMultiPageCommentTargets($block);
var $commentBtn = $row.find('.rmArticleCommentToggle');
if (!$row.length) return;
if (showAdd) {
if ($commentBtn.length) setMultiPageCommentExpanded($commentBtn, false);
if (ids.wrapId) $('#' + ids.wrapId).hide();
$row.find('.rmArticleCommentToggle,.rmRemoveInput').remove();
if (!$row.find('.rmAddArticle').length) $row.append(buildAddArticleButtonHtml(opts));
return;
}
$row.find('.rmAddArticle').remove();
if (!$row.find('.rmArticleCommentToggle').length) {
$row.append(buildMultiArticleButtonsHtml(ids.wrapId, ids.textareaId, $.extend({}, opts, { showComment: showComment })));
return;
}
$row.find('.rmArticleCommentToggle').toggle(showComment);
}
function setupMultiPageNominationUi(options) {
var opts = options || {};
var containerSelector = opts.containerSelector || '#rmArticlesContainer';
var pageCounter = parseInt(opts.nextIndex, 10) || 1;
var wasMultiModeExpanded = false;
function restoreEmptySinglePageInput() {
var $pageInput = $(containerSelector + ' .rmArticleInput').first();
if (!$pageInput.length || String($pageInput.val() || '').trim()) return;
$pageInput.val(opts.defaultPage || '');
}
function updateMultiMode() {
var $blocks = $(containerSelector + ' .rmMultiArticleBlock');
var hasExtra = $blocks.length > 1;
$('#rmMultiHeader').toggle(hasExtra);
if (opts.multiOnlySelector) $(opts.multiOnlySelector).toggle(hasExtra);
if (!hasExtra && wasMultiModeExpanded) restoreEmptySinglePageInput();
$blocks.each(function (index) {
setMultiPageRowControls($(this), !hasExtra && index === 0, hasExtra, opts);
});
wasMultiModeExpanded = hasExtra;
syncModalLayout();
}
$(document).off('click.rmMultiPageAdd').on('click.rmMultiPageAdd', '.rmAddArticle', function () {
$(containerSelector).append(buildMultiArticleRowHtml(pageCounter++, opts));
updateMultiMode();
});
$(document).off('click.rmMultiPageComment').on('click.rmMultiPageComment', '.rmArticleCommentToggle', function () {
var $btn = $(this);
if (!$btn.is(':visible')) return;
setMultiPageCommentExpanded($btn, $btn.attr('aria-expanded') !== 'true');
syncModalLayout();
});
$(document).off('click.rmMultiPageRemove').on('click.rmMultiPageRemove', '.rmArticleRow .rmRemoveInput', function () {
$(this).closest('.rmMultiArticleBlock').remove();
updateMultiMode();
});
updateMultiMode();
return {
update: updateMultiMode,
isMulti: function () { return $(containerSelector + ' .rmMultiArticleBlock').length > 1; }
};
}
function bindMultiNominationFormatSwitch(rootSelector, buttonSelector) {
$(rootSelector).off('click.rmMultiFormat', buttonSelector).on('click.rmMultiFormat', buttonSelector, function () {
var $btn = $(this);
$(buttonSelector).removeClass('is-active').attr('aria-pressed', 'false');
$btn.addClass('is-active').attr('aria-pressed', 'true');
});
}
function buildProtectAddButtonHtml() {
return buildSquareAddButtonHtml('', 'Добавить страницу', 'rmProtectAddPage');
}
function buildProtectPageRowHtml(id, pageName, isFirstRow) {
return joinHtml([
'<div', isFirstRow ? ' id="rmProtectFirstRow"' : '', ' class="rmProtectPageRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="', id, '" type="text" placeholder="Страница" class="rmProtectPageInput" style="', stInputBox, '"',
pageName ? ' value="' + escapeHtml(pageName) + '"' : '', '>',
isFirstRow
? buildProtectAddButtonHtml()
: '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>',
'</div>'
]);
}
function buildReportFormHtml(ctx, isZka) {
var reportTextPlaceholder = (isZka ? 'Введите текст запроса.' : 'Текст запроса (можно дополнить или изменить).') + ' Подпись будет добавлена автоматически.';
if (isZka) {
return joinHtml([
'<input id="rmReportHeader" type="text" placeholder="Тема/заголовок" style="', stInputFull, '" value="', escapeHtml(ctx.pageLink), '">',
'<textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>',
buildQuickPhrasesPanelHtml('rmReportText')
]);
}
return joinHtml([
'<div id="rmProtectModeBtns" class="', RESIZE_CLASS, '" style="margin-bottom:14px;"><div class="rmSegmentedBar">',
'<button id="rmProtectModeInstall" type="button" class="rmSegmentedBtn rmProtectModeBtn is-active" aria-pressed="true">🛡️ Установить защиту</button>',
'<button id="rmProtectModeRemove" type="button" class="rmSegmentedBtn rmProtectModeBtn" aria-pressed="false">📛 Снять защиту</button>',
'</div></div>',
'<div id="rmProtectMultiWrap" class="', RESIZE_CLASS, '">',
'<div id="rmProtectHeaderWrap" style="display:none;margin-bottom:6px;"><div class="rmProtectHeaderRow" style="', stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'), '"><input id="rmProtectHeader" type="text" placeholder="Заголовок (для нескольких страниц)" style="', stInputBox, '">', buildProtectAddButtonHtml(), '</div></div>',
buildProtectPageRowHtml('rmProtectPage0', ctx.pageName, true),
'<div id="rmProtectPagesContainer"></div>',
'</div>',
'<div id="rmProtectLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmProtectLevels" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>',
'</div></div>',
'<div id="rmProtectReasonsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Причины</div><div id="rmProtectReasons" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="война правок">война правок</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="неконсенсусные изменения">неконсенсусные изменения</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="вандализм">вандализм</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="популярная статья">популярная статья</button>',
'</div></div>',
'<div id="rmRemoveLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup" style="display:none;"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmRemoveLevels" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>',
'</div></div>',
'<div id="rmProtectTextBlock" class="', RESIZE_CLASS, '"><textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>',
buildQuickPhrasesPanelHtml('rmReportText'), '</div>'
]);
}
// ═══════════════════════════════════════════════════════════════════════════
// ОБРАБОТЧИКИ ОПЕРАЦИЙ
// ═══════════════════════════════════════════════════════════════════════════
var handlers = {
// ── КБУ ─────────────────────────────────────────────────────────────
showKbu: function (op) {
var forCategory = !!(op && op.forCategory);
var reasons = getFastRemoveReasons();
createModal({
title: 'Быстрое удаление',
width: 'compact',
subtitleHtml: '<span id="rmKbuCriteriaLinkWrap"></span>'
});
$('#removerModalContent').html(buildKbuFormHtml(reasons));
function updateKbuReasonControls() {
var reason = reasons[$('#rmSel').val()] || reasons[0];
var paramCfg = reason ? cfg.requiredParamTemplates[reason[0]] : null;
var showComment = true;
$('#rmKbuCriteriaLinkWrap').html(buildFastRemoveCriteriaLinkHtml(reason));
if (paramCfg) {
var noComment = paramCfg.charAt(0) === '!';
$('#fiRm').attr({ type: 'text', placeholder: 'Укажите ' + (noComment ? paramCfg.substring(1) : paramCfg) }).show();
showComment = !noComment;
} else {
$('#fiRm').attr('type', 'hidden').hide();
}
$('#fiRmComment').toggle(showComment);
$('.rmQuickPhrasesPanel[data-rm-target="fiRmComment"]').toggle(showComment);
}
$('#rmSel').change(updateKbuReasonControls);
$('#rmSel').trigger('change');
renderModalFooter('submit', {
submitText: 'Номинировать',
onSubmit: function () {
var idx = $('#rmSel').val();
var addInfo = $('#fiRm').val();
var comment = $('#fiRmComment').val();
startProcessing();
if (forCategory) {
var tpl = reasons[idx][0];
var categorySummary = makeSummary('номинация категории на быстрое удаление');
if (addInfo) tpl += '|1=' + addInfo;
if (comment) tpl += '|' + (addInfo ? '2' : '1') + '=' + comment;
editPageContent(mwCfg.wgPageName, { summary: categorySummary, readError: 'Не удалось получить содержимое.' },
function (text) { return { text: wrapInNoinclude(text, T_OPEN + tpl + T_CLOSE) }; },
function (err) {
if (err) {
unlockModalSubmit();
logStatus('Ошибка записи.', err);
} else {
logStatus('Страница номинирована к быстрому удалению.', null, { trackError: false });
finalizeFastRemoval([normTitle(mwCfg.wgPageName)], categorySummary);
}
});
} else {
var job = {
mode: 'nominate', opId: 'fRm',
kbuTemplate: reasons[idx][0], kbuAddInfo: addInfo, kbuComment: comment,
summary: makeSummary('номинация к [[ВП:КБУ|быстрому удалению]]'),
inArticle: true
};
processPageList([normTitle(mwCfg.wgPageName)], job, function (notifiedPages) {
finalizeFastRemoval(notifiedPages, job.summary);
});
}
return true;
}
});
},
// ── Универсальная номинация (КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС) ────────
showNomination: function (op) {
var nom = op.nomination;
var pg = normTitle(mwCfg.wgPageName);
var date = getDate()[1];
var nomPage = nom.nomPage(date);
var multiMode = nom.supportsMulti;
function updateTransferUi() {
var mode = getTransferModeFromButtons();
var isNone = mode === 'none';
var isKbu = mode === 'kbu' || mode === 'both';
var isKul = mode === 'kul' || mode === 'both';
$('#rmTransferBtnNone').toggleClass('is-active', isNone).attr('aria-pressed', isNone ? 'true' : 'false');
$('#rmTransferBtnKbu').toggleClass('is-active', isKbu).attr('aria-pressed', isKbu ? 'true' : 'false');
$('#rmTransferBtnKul').toggleClass('is-active', isKul).attr('aria-pressed', isKul ? 'true' : 'false');
var t = transferTexts[mode];
if (t) { $('#rmTransferHint').text(t.hint).show(); } else { $('#rmTransferHint').hide().text(''); }
applyGeneratedText($('#rmMsg'), t && t.notice ? t.notice + '\n' : '');
}
createModal({
title: 'Номинация: ' + nom.template,
subtitlePage: nomPage,
subtitleLabel: 'Текущий день'
});
$('#removerModalContent').html(buildNominationFormHtml(nom, pg, multiMode));
setupResizableModal('rmMsg');
// Логика переноса
if (nom.supportsTransfer) {
$(document).off('click.rmTransfer').on('click.rmTransfer', '#rmTransferBtnNone,#rmTransferBtnKbu,#rmTransferBtnKul', function () {
if (this.id === 'rmTransferBtnNone') {
$('#rmTransferBtnKbu,#rmTransferBtnKul').removeClass('is-active');
$('#rmTransferBtnNone').addClass('is-active');
} else {
$(this).toggleClass('is-active');
var anyOn = $('#rmTransferBtnKbu').hasClass('is-active') || $('#rmTransferBtnKul').hasClass('is-active');
$('#rmTransferBtnNone').toggleClass('is-active', !anyOn);
}
updateTransferUi();
});
updateTransferUi();
}
// Многостраничный режим
if (multiMode) {
setupMultiPageNominationUi({ defaultPage: pg, multiOnlySelector: '#rmArticleMultiFormatWrap' });
bindMultiNominationFormatSwitch('#removerModalContent', '.rmArticleMultiFormatBtn');
}
if (nom.extraInput) wireMultiInput(nom.extraInput);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var isMulti = multiMode && $('#rmArticlesContainer .rmMultiArticleBlock').length > 1;
var inputVal = !isMulti ? normTitle($('#rmArticlesContainer .rmArticleInput').first().val() || '') : '';
var changed = inputVal && inputVal !== pg;
function executeJob(job) {
startProcessing();
runFlow({
templateStep: function (next) {
if (!job.inArticle) { next(); return; }
processPageList(job.pages, job, function (notifiedPages, err) {
job._notifiedPages = notifiedPages;
next(err);
});
},
nominationStep: function (done) {
publishNomination({
pageTitle: job.nomPage,
navTemplate: job.navTemplate,
sectionTitle: job.section,
summary: job.summary,
text: getNominationPublishText(job)
}, function (err) { done(err, { pageTitle: job.nomPage, sectionTitle: job.section }); });
},
notifyStep: function (nominationInfo, next) {
var pages = job._notifiedPages || [];
if (!setAlert || !pages.length) { next(); return; }
notifyAuthorsForPages(pages, {
summary: job.summary,
actionText: job.comment,
discussionPage: nominationInfo && nominationInfo.pageTitle,
discussionSection: nominationInfo && nominationInfo.sectionTitle
}, next);
},
skipLink: op.id === 'fRm'
});
}
function run(targetPg) {
var job = buildNominationJob(op, targetPg, isMulti);
if (!job) { unlockModalSubmit(); return; }
if (job.isMulti && job.inArticle && getNominationConflictRule(job)) {
startProcessing();
inspectMultiNominationConflicts(job, function (err, conflicts) {
if (err) { markSubmitError(); return; }
if (!conflicts.length) { executeJob(job); return; }
showNominationConflictResolution(job, conflicts, function (resolvedJob) {
executeJob(resolvedJob);
});
});
return;
}
executeJob(job);
}
if (changed) {
apiReq({ prop: 'info', titles: inputVal }, 'query', function (data) {
if (data && data.error) {
unlockModalSubmit();
alert('Не удалось проверить страницу из-за ошибки API. Попробуйте ещё раз.');
return;
}
var page = getFirstQueryPage(data);
if (!page) {
unlockModalSubmit();
alert('Не удалось проверить страницу из-за временной ошибки. Попробуйте ещё раз.');
return;
}
if (page.missing !== undefined) {
unlockModalSubmit();
alert('Страница «' + inputVal + '» не существует.');
return;
}
run(normTitle(page.title || inputVal));
});
} else {
run(pg);
}
return true;
}
});
},
// ── Снятие номинации (статья) ────────────────────────────────────────
showArticleClose: function () {
showCloseActionsModal({
inputName: 'rmCloseAction',
listId: 'rmCloseActions',
emptyText: 'Не найдено подходящих шаблонов для закрытия.',
emptyDetails: 'Проверяются: КУ, КПМ, КБУ, КУЛ.',
getActions: function (articleText) {
var actions = [];
if (RE_KU_ON_PAGE.test(articleText)) actions.push({ id: 'ret', tag: 'КУ', label: 'Оставлено', mode: 'denom', closeType: 'ret', resultTemplate: 'оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{оставлено}} на СО.', comment: 'оставлена', talkNotice: true });
if (RE_KU_ON_PAGE.test(articleText)) actions.push({ id: 'retConditional', tag: 'КУ', label: 'Условно оставлено', mode: 'denom', closeType: 'retConditional', resultTemplate: 'условно оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{условно оставлено}} на СО.', comment: 'условно оставлена', talkNotice: true, needsConditionalFields: true });
if (RE_KPM_ON_PAGE.test(articleText)) actions.push({ id: 'doneRnm', tag: 'КПМ', label: 'Переименовано', mode: 'denom', closeType: 'doneRnm', resultTemplate: 'переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{переименовано}} на СО.', comment: 'переименована', talkNotice: true, needsOldTitle: true });
if (RE_KPM_ON_PAGE.test(articleText)) actions.push({ id: 'noRnm', tag: 'КПМ', label: 'Не переименовано', mode: 'denom', closeType: 'noRnm', resultTemplate: 'не переименовано',sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{не переименовано}} на СО.', comment: 'не переименована',talkNotice: true });
var hasKbu = RE_KBU_ON_PAGE.test(articleText);
var hasKul = RE_KUL_ON_PAGE.test(articleText);
if (hasKbu && hasKul) actions.push({ id: 'cleanup-both', tag: 'КБУ и КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'both', cleanupLabel: 'КБУ и КУЛ', description: 'Снимает шаблоны КБУ/КУЛ и Hangon.' });
if (hasKbu) actions.push({ id: 'cleanup-kbu', tag: 'КБУ', label: 'Снятие', mode: 'cleanup', transferMode: 'kbu', cleanupLabel: 'КБУ', description: 'Снимает шаблоны КБУ и Hangon.' });
if (hasKul) actions.push({ id: 'cleanup-kul', tag: 'КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'kul', cleanupLabel: 'КУЛ', description: 'Снимает шаблон КУЛ.' });
return actions;
},
afterRender: function (actions) {
var hasDoneRnm = actions.some(function (a) { return a.id === 'doneRnm'; });
var hasConditionalRet = actions.some(function (a) { return a.id === 'retConditional'; });
if (hasDoneRnm) {
$('#rmCloseActions input[value="doneRnm"]').closest('.rmActionItem').append(
'<div id="rmCloseOldTitleWrap" style="display:none;margin-top:6px;"><input id="rmCloseOldTitle" type="text" placeholder="Старое название" style="' + stInputFull + '"></div>'
);
}
if (hasConditionalRet) {
$('#rmCloseActions input[value="retConditional"]').closest('.rmActionItem').append(buildConditionalRetFieldsHtml());
}
},
afterFooterRender: function (_, actionMap) {
function ensureConditionalTextareaResizer() {
var $textarea = $('#rmCloseConditionalReason');
if (!$textarea.length || $textarea.data('rmConditionalResizerReady')) return;
setupNestedResizableTextarea('rmCloseConditionalReason', 'rmCloseConditionalWrap', 280, 90);
$textarea.data('rmConditionalResizerReady', true);
}
function updateUi() {
var sel = actionMap[$('[name="rmCloseAction"]:checked').val()];
$('#rmCloseOldTitleWrap').toggle(!!(sel && sel.needsOldTitle));
$('#rmCloseConditionalWrap').toggle(!!(sel && sel.needsConditionalFields));
if (sel && sel.needsConditionalFields) ensureConditionalTextareaResizer();
var disableNotify = !!(sel && sel.mode === 'cleanup' && sel.transferMode === 'kbu');
var $cb = $('[name="rmUAlert"]');
var $cbLabel = $('[name="rmUAlert"]').closest('label');
if ($cb.length) $cb.prop('disabled', disableNotify);
if ($cbLabel.length) $cbLabel.css({
visibility: disableNotify ? 'hidden' : 'visible',
pointerEvents: disableNotify ? 'none' : ''
});
syncModalLayout();
}
$(document).off('change.rmCloseAction').on('change.rmCloseAction', '[name="rmCloseAction"]', updateUi);
updateUi();
},
onSubmit: function (sel, pageName) {
var job;
if (sel.mode === 'denom') {
var oldTitle = sel.needsOldTitle ? ($('#rmCloseOldTitle').val() || '').trim() : '';
var conditionalReason = sel.needsConditionalFields ? normalizeQuickPhraseValue($('#rmCloseConditionalReason').val()) : '';
var conditionalDeadline = sel.needsConditionalFields ? String($('#rmCloseConditionalDeadline').val() || '').trim() : '';
if (sel.needsOldTitle && !oldTitle) { alert('Укажите старое название.'); return false; }
if (conditionalDeadline && normalizeIsoDate(conditionalDeadline) !== conditionalDeadline) {
alert('Укажите срок в формате YYYY-MM-DD, например 2026-05-31.');
return false;
}
job = {
mode: 'denom',
closeType: sel.closeType,
resultTemplate: sel.resultTemplate,
sourceTemplate: sel.sourceTemplate,
oldTitle: oldTitle,
conditionalReason: conditionalReason,
conditionalDeadline: conditionalDeadline,
notifyActionText: sel.comment,
skipNotify: false
};
} else {
job = {
mode: 'cleanup',
transferMode: sel.transferMode,
summary: makeSummary('снятие шаблонов ' + sel.cleanupLabel),
notifyActionText: (sel.transferMode === 'kul' || sel.transferMode === 'both')
? 'больше не номинирована к срочному улучшению'
: '',
skipNotify: !(sel.transferMode === 'kul' || sel.transferMode === 'both')
};
}
processPageList([pageName], job, function (notifiedPages, err, pageMeta) {
function finishClose() {
if (isError) { markSubmitError(); }
else { renderModalFooter('reload'); }
}
if (isError || err || job.skipNotify || !setAlert || !notifiedPages.length) { finishClose(); return; }
var meta = (pageMeta && pageMeta[normTitle(pageName)]) || {};
notifyAuthorsForPages(notifiedPages, {
summary: meta.summary || job.summary,
actionText: job.notifyActionText,
discussionPage: meta.discussionPage,
discussionSection: meta.discussionSection,
includeProposedPrefix: false
}, finishClose);
});
return true;
}
});
},
// ── ОБКАТ: номинация категории ───────────────────────────────────────
showCatNomination: function (op) {
var catType = op.catType;
var titles = { discuss: 'Номинация: обсуждение', deletion: 'Номинация: к удалению', rename: 'Номинация: к переименованию', merge: 'Номинация: к объединению' };
var now = new Date();
var catDiscPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[now.getUTCMonth()] + '_' + now.getUTCFullYear();
var pageName = normalizeCategoryPageName(mwCfg.wgPageName);
var multiMode = catType === 'deletion';
createModal({ title: titles[catType], subtitlePage: catDiscPage, subtitleLabel: 'Текущий месяц' });
var variantCfgs = {
rename: { firstId: 'firstRenameInput', addBtnId: 'addRenameVariant', containerId: 'renameVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Новое название без префикса Категория:', addPh: 'Дополнительный вариант названия' },
merge: { firstId: 'firstMergeInput', addBtnId: 'addMergeVariant', containerId: 'mergeVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Категория для объединения без префикса Категория:', addPh: 'Дополнительный вариант объединения' }
};
var vCfg = variantCfgs[catType];
$('#removerModalContent').html(buildCategoryNominationFormHtml(vCfg, multiMode, pageName));
setupResizableModal('nominationReason');
if (multiMode) {
setupMultiPageNominationUi({
defaultPage: pageName,
inputPlaceholder: 'Категория',
addTitle: 'Добавить категорию',
removeTitle: 'Удалить категорию',
commentPlaceholder: 'Комментарий только для этой категории (необязательно)',
multiOnlySelector: '#rmCategoryMultiFormatWrap'
});
bindMultiNominationFormatSwitch('#removerModalContent', '.rmCategoryMultiFormatBtn');
}
if (vCfg) wireMultiInput(vCfg);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var reason = normalizeQuickPhraseValue($('#nominationReason').val());
var targetPages = multiMode ? collectCategoryPageInputValues('.rmArticleInput') : [pageName];
var isMulti = multiMode && targetPages.length > 1;
var multiFormat = $('.rmCategoryMultiFormatBtn.is-active').data('rmMultiFormat') || 'sections';
var commentsByCategory = isMulti ? collectMultiNominationComments(normalizeCategoryPageName) : {};
var discussionTarget = isMulti ? targetPages : targetPages[0];
var discussionReason = isMulti
? (multiFormat === 'list'
? buildMultiNominationListText(targetPages, reason, commentsByCategory)
: buildMultiNominationText(targetPages, reason, commentsByCategory, { headingLevel: 4 }))
: reason;
var discussionOptions = isMulti ? { headerText: $('#rmHeader').val(), reasonIsPrepared: true } : null;
var notifiedPages = [];
if (!reason) { alert('Пожалуйста, укажите причину/тему.'); return false; }
if (!targetPages.length) { alert('Укажите категорию.'); return false; }
var mainName = null, additionalNames = [];
if (vCfg) {
mainName = $('#' + vCfg.firstId).val().trim();
if (!mainName) { alert('Укажите ' + (catType === 'rename' ? 'новое название' : 'категорию для объединения') + '.'); return false; }
additionalNames = collectInputValues('#' + vCfg.containerId + ' .variantInput');
}
startProcessing();
runFlow({
templateStep: function (next) {
addTemplatesToCategories(targetPages, catType, mainName, additionalNames, function (err, processedPages) {
notifiedPages = processedPages || [];
next(err);
});
},
nominationStep: function (done) {
createCategoryDiscussion(discussionTarget, discussionReason, catType, function (err, nominationInfo) { done(err, nominationInfo || null); }, mainName, additionalNames, discussionOptions);
},
notifyStep: function (nominationInfo, next) {
if (!setAlert || !nominationInfo) { next(); return; }
var section = normalizeSectionForLink(nominationInfo.sectionTitle || '');
notifyAuthorsForPages(notifiedPages.length ? notifiedPages : targetPages, {
summary: makeSummary('номинация [[' + nominationInfo.pageTitle + (section ? '#' + section : '') + ']]'),
actionText: { discuss: 'к обсуждению', deletion: 'к удалению', rename: 'к переименованию', merge: 'к объединению' }[catType] || 'к обсуждению',
discussionPage: nominationInfo.pageTitle,
discussionSection: nominationInfo.sectionTitle
}, next);
}
});
return true;
}
});
},
// ── Снятие номинации (категория) ─────────────────────────────────────
showCatClose: function () {
showCloseActionsModal({
inputName: 'rmCategoryCloseAction',
showCheckbox: false,
emptyText: 'Не найдено подходящих шаблонов для завершения.',
emptyDetails: 'Проверяются ОБКАТ и КУ.',
getActions: function (catText) {
var allObkat = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|');
var actions = [];
if (new RegExp('\\{\\{\\s*(?:' + allObkat + ')\\s*(?:\\||\\}\\})', 'i').test(catText)) {
actions.push({ id: 'cat-obkat-done', tag: 'ОБКАТ', label: 'Завершено', mode: 'obkat', talkTemplate: 'Обсуждавшаяся категория', description: 'Снимает шаблон ОБКАТ, добавляет {{Обсуждавшаяся категория}} на СО.', talkNotice: true });
}
if (RE_KU_ON_PAGE.test(catText)) {
actions.push({ id: 'cat-ku-cleanup', tag: 'КУ', label: 'Снятие', mode: 'cleanup', description: 'Снимает шаблон КУ без записи на СО.' });
}
return actions;
},
onSubmit: function (sel, pageName) {
if (sel.mode === 'obkat') markCategoryDiscussionAsDone(pageName);
if (sel.mode === 'cleanup') removeKuFromCategory(pageName);
return true;
}
});
},
// ── Защита / Запрос к администраторам ───────────────────────────────
showReport: function (op) {
var mode = op.reportMode || 'protect';
var ctx = getReporterContext(mode);
var isZka = mode === 'request';
var protectMode = 'install';
var pageCounter = 1;
function buildProtectText(pm) {
if (pm === 'remove') {
var removeLevels = [];
$('#rmRemoveLevels .rmToggleBtn.is-active').each(function () { removeLevels.push($(this).data('label')); });
return removeLevels.length ? 'Просьба снять ' + removeLevels.join(' и/или ') + '.' : '';
}
var levels = [], reasons = [];
$('#rmProtectLevels .rmToggleBtn.is-active').each(function () { levels.push($(this).data('label')); });
$('#rmProtectReasons .rmToggleBtn.is-active').each(function () { reasons.push($(this).data('label')); });
if (!levels.length && !reasons.length) return '';
var text = 'Просьба установить';
if (levels.length) text += ' ' + levels.join(' и/или ');
if (reasons.length) text += ' по причине: ' + reasons.join(', ');
return text + '.';
}
function applyProtectMode(m) {
protectMode = m;
var isInstall = m === 'install';
$('#rmProtectModeInstall').toggleClass('is-active', isInstall).attr('aria-pressed', isInstall ? 'true' : 'false');
$('#rmProtectModeRemove').toggleClass('is-active', !isInstall).attr('aria-pressed', !isInstall ? 'true' : 'false');
$('#removerModalTitleText').text(isInstall ? 'Запрос на защиту страницы' : 'Запрос на снятие защиты');
var linkPage = isInstall ? 'Википедия:Установка защиты' : 'Википедия:Снятие защиты';
$('#rmProtectLinkWrap').html('<a href="' + getPageUrl(linkPage) + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + escapeHtml(linkPage) + '</a>');
$('#rmProtectLevelsWrap,#rmProtectReasonsWrap').toggle(isInstall);
$('#rmRemoveLevelsWrap').toggle(!isInstall);
$('#rmProtectLevels .rmProtectOptBtn,#rmProtectReasons .rmProtectOptBtn,#rmRemoveLevels .rmProtectOptBtn').removeClass('is-active');
$('#rmReportText').val('').removeData('rmGenerated');
}
function updateProtectMultiUi() {
var $rows = $('#rmProtectMultiWrap .rmProtectPageRow');
var hasExtra = $rows.length > 1;
if (!$rows.length) {
$('#rmProtectPagesContainer').append(buildProtectPageRowHtml('rmProtectPage' + pageCounter++, ctx.pageName, true));
$rows = $('#rmProtectMultiWrap .rmProtectPageRow');
hasExtra = false;
}
$('#rmProtectHeaderWrap').toggle(hasExtra);
if (!hasExtra) $('#rmProtectHeader').val('');
$rows.each(function () {
var $row = $(this);
$row.find('.rmProtectAddPage,.rmRemoveInput').remove();
$row.append(hasExtra
? '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>'
: buildProtectAddButtonHtml()
);
});
syncModalLayout();
}
createModal({
title: isZka ? 'Запрос к администраторам' : 'Запрос на защиту страницы',
width: 'compact',
subtitleHtml: isZka
? '<a href="' + getPageUrl('Википедия:Запросы к администраторам') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Запросы к администраторам</a>' +
' · <a href="' + getPageUrl('Википедия:Запросы к администраторам/Быстрые') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Быстрые</a>'
: '<span id="rmProtectLinkWrap"></span>'
});
$('#removerModalContent').html(buildReportFormHtml(ctx, isZka));
if (!isZka) {
$('#removerModalContent')
.on('click', '#rmProtectModeInstall', function () { applyProtectMode('install'); })
.on('click', '#rmProtectModeRemove', function () { applyProtectMode('remove'); })
.on('click', '.rmProtectOptBtn', function () {
$(this).toggleClass('is-active');
if (protectMode === 'install') {
var $levels = $('#rmProtectLevels .rmProtectOptBtn.is-active');
if ($(this).closest('#rmProtectReasons').length && $(this).hasClass('is-active') && $levels.length === 0) {
$('#rmProtectLevels .rmProtectOptBtn').first().addClass('is-active');
}
if ($(this).closest('#rmProtectLevels').length && !$(this).hasClass('is-active') && $levels.length === 0) {
$('#rmProtectReasons .rmProtectOptBtn').removeClass('is-active');
}
}
applyGeneratedText($('#rmReportText'), buildProtectText(protectMode));
});
$(document)
.off('click.rmProtectAdd').on('click.rmProtectAdd', '.rmProtectAddPage', function () {
var id = 'rmProtectPage' + pageCounter++;
$('#rmProtectPagesContainer').append(buildProtectPageRowHtml(id, '', false));
updateProtectMultiUi();
})
.off('click.rmProtectRemove').on('click.rmProtectRemove', '.rmProtectPageRow .rmRemoveInput', function () {
$(this).closest('.rmProtectPageRow').remove(); updateProtectMultiUi();
});
applyProtectMode('install');
}
setupResizableModal('rmReportText');
renderModalFooter('submit', {
showCheckbox: false,
showSubscribe: true,
submitText: 'Отправить',
onSubmit: function () { doReport(ctx, false, protectMode); return true; }
});
if (isZka) {
$('<button id="rmReportFast" style="' + stCancel + '">Быстрый запрос</button>').insertBefore('#removerSubmit');
$('#rmReportFast').click(function () {
if ($('#removerSubmit').data('rmSubmitInProgress')) return;
$('#removerSubmit').data('rmSubmitInProgress', true).prop('disabled', true);
$('#rmReportFast').prop('disabled', true);
doReport(ctx, true, protectMode);
});
}
}
};
// ═══════════════════════════════════════════════════════════════════════════
// ВСПОМОГАТЕЛЬНЫЕ: КБУ, закрытие, категории
// ═══════════════════════════════════════════════════════════════════════════
function getFastRemoveCriteriaAnchorFromConfig(templateName) {
var anchors = cfg.fastRemoveCriteriaAnchors || {};
var template = String(templateName || '').trim();
var lower = template.toLowerCase();
var key;
if (!template) return '';
if (Object.prototype.hasOwnProperty.call(anchors, template)) return anchors[template];
for (key in anchors) {
if (Object.prototype.hasOwnProperty.call(anchors, key) && String(key).toLowerCase() === lower) {
return anchors[key];
}
}
return '';
}
function getFastRemoveCriteriaAnchor(reason) {
var configured = reason ? getFastRemoveCriteriaAnchorFromConfig(reason[0]) : '';
var template, label, m;
if (configured) return configured;
template = String(reason && reason[0] || '');
label = String(reason && reason[1] || '');
if (/^(?:подст\s*:\s*)?(?:deleteslow|ds)$/i.test(template) || /^\s*ds\b/i.test(label)) return 'С1';
m = label.match(/^\s*([А-ЯЁA-Z]{1,3}\d+(?:\.\d+)?)/);
return m ? m[1] : '';
}
function buildFastRemoveCriteriaLinkHtml(reason) {
var anchor = getFastRemoveCriteriaAnchor(reason);
var label = KBU_CRITERIA_PAGE + (anchor ? '#' + anchor : '');
var url = getPageUrlWithFragment(KBU_CRITERIA_PAGE, anchor);
return '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(label) + '</a>';
}
function getFastRemoveReasons() {
var reasons = cfg.fastRemoveReasons;
var prefix = (mwCfg.wgIsRedirect ? 'ОП' : 'О') + ({ 0: 'С', 2: 'У', 3: 'У', 6: 'Ф', 14: 'К' }[mwCfg.wgNamespaceNumber] || '');
var all = [];
if (isCategory && reasons.categories) all = all.concat(reasons.categories);
['general','articles','redirects','files','users','special'].forEach(function (k) { if (reasons[k]) all = all.concat(reasons[k]); });
if (!isCategory && reasons.categories) all = all.concat(reasons.categories);
return all.filter(function (r) { return prefix.indexOf(r[2] !== undefined ? r[2] : r[1].charAt(0)) >= 0; });
}
function showCloseActionsModal(opts) {
createModal({ title: 'Снятие шаблонов номинаций', inline: true });
$('#removerModalContent').html('<p style="margin:0;">Определение доступных действий...</p>');
var pageName = normTitle(mwCfg.wgPageName);
getText(pageName, function (pageText, readErr) {
if (readErr) { showInfoAndClose('Не удалось прочитать содержимое страницы.', readErr.info || readErr.code || '', true); return; }
if (pageText === null) { showInfoAndClose('Не удалось прочитать содержимое страницы.', '', true); return; }
var actions = opts.getActions(pageText);
if (!actions.length) { showInfoAndClose(opts.emptyText, opts.emptyDetails || ''); return; }
var actionMap = actions.reduce(function (m, a) { m[a.id] = a; return m; }, {});
$('#removerModalContent').html(buildActionsHtml(actions, opts.inputName, opts.listId));
if (opts.afterRender) opts.afterRender(actions, actionMap, pageName, pageText);
renderModalFooter('submit', {
showCheckbox: opts.showCheckbox,
submitText: 'Выполнить',
onSubmit: function () {
var sel = getSelectedAction(opts.inputName, actionMap);
if (!sel) return false;
startProcessing();
return opts.onSubmit(sel, pageName, pageText, actionMap) !== false;
}
});
if (opts.afterFooterRender) opts.afterFooterRender(actions, actionMap, pageName, pageText);
});
}
function runPageEditWithStatus(opts) {
var o = opts || {};
var statusId = logStatus(o.pendingText, null, { pending: true, trackError: false });
editPageContent(o.pageName, o.editOptions, o.buildFn, function (err, meta) {
if (err) { logStatus(o.errorText, err, { statusId: statusId }); unlockModalSubmit(); return; }
logStatus(o.successText, null, { statusId: statusId, trackError: false });
if (o.onSuccess) o.onSuccess(meta || null);
});
}
function removeKuFromCategory(pageName) {
runPageEditWithStatus({
pageName: pageName,
pendingText: 'Снимается шаблон КУ...',
errorText: 'Снятие шаблона КУ.',
successText: 'Шаблон КУ снят.',
editOptions: { summary: makeSummary('снятие шаблона КУ'), watchlist: 'nochange', readError: 'Страница не существует.', readErrorCode: 'read_failed' },
buildFn: function (text) {
var r = stripTemplatesByPattern(text, '(?:к\\s*удалению|ку)');
if (!r.removed) return { error: { code: 'no_changes', info: 'Шаблон КУ не найден.' } };
return { text: r.text.replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n') };
},
onSuccess: function () {
logStatus('Шаблон на СО не устанавливался.', null, { trackError: false });
renderModalFooter('reload');
}
});
}
// ── Категории: добавление шаблона ────────────────────────────────────────
function addTemplateToCategory(pageName, type, mainName, additionalNames, callback) {
var cb = callback || function () {};
var cfgByType = {
discuss: { action: 'обсуждение', template: 'Обсуждаемая категория' },
deletion: { action: 'удаление', template: 'Обсуждаемая категория' },
rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true },
merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true }
};
var typeCfg = cfgByType[type];
if (!typeCfg) { cb({ code: 'error', info: 'Неизвестный тип номинации.' }); return; }
var dateStr = getDate()[0];
var parts = [dateStr];
if (typeCfg.needsMain) {
if (!mainName) { cb({ code: 'error', info: 'Не указано основное название.' }); return; }
parts.push(mainName);
if (additionalNames && additionalNames.length) Array.prototype.push.apply(parts, additionalNames);
}
var tplText = T_OPEN + typeCfg.template + '|' + parts.join('|') + T_CLOSE;
editPageContent(pageName, { summary: makeSummary('добавление шаблона номинации на ' + typeCfg.action), readError: 'Не удалось получить содержимое.' },
function (text) { return { text: wrapInNoinclude(text, tplText) }; },
function (err) {
logPageEdit(pageName, err);
if (err) { cb(err); return; }
if (type === 'merge') {
addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, function () { cb(null); });
return;
}
cb(null);
}
);
}
function collectCategoryPageInputValues(selector) {
var pages = [];
$(selector).each(function () {
var title = normalizeCategoryPageName($(this).val() || '');
if (title && pages.indexOf(title) === -1) pages.push(title);
});
return pages;
}
function addTemplatesToCategories(pages, type, mainName, additionalNames, callback) {
var cb = callback || function () {};
var processedPages = [];
eachSequential(pages || [], function (pageName, next) {
addTemplateToCategory(pageName, type, mainName, additionalNames, function (err) {
if (!err) processedPages.push(pageName);
next(err || null);
});
}, function (err) { cb(err, processedPages); });
}
function addMergeTemplatesToTargets(sourcePage, mainName, additionalNames, dateStr, callback) {
var cb = callback || function () {};
var currentCatName = normTitle(stripCatPrefix(sourcePage));
var targets = [mainName].concat(additionalNames || []);
if (!targets.length) { cb(); return; }
eachSequential(targets, function (target, next) {
var targetPage = 'Категория:' + target;
addMergeTemplateToTargetCategory(targetPage, currentCatName, dateStr, function (success, status) {
var url = getPageUrl(targetPage);
var linkHtml = '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(targetPage) + '</a>';
if (success) {
var extra = (status === 'already_exists' || status === 'updated') ? ' (' + formatMergeStatus(status) + ')' : '';
logStatus('Шаблон добавлен в ' + linkHtml + extra + '.', null, { trackError: false });
} else {
logStatus('Ошибка при добавлении шаблона в ' + linkHtml + '.', { code: 'merge_target_failed', info: status }, { trackError: false });
}
next();
});
}, cb);
}
function addMergeTemplateToTargetCategory(targetPageName, sourceCatName, dateStr, callback) {
editPageContent(targetPageName, { summary: makeSummary('добавление шаблона объединения'), readError: 'Не удалось получить содержимое' },
function (text) {
var existing = text.match(getCategoryMergeRe());
if (existing) {
var cats = existing[1].split('|').slice(1).map(function (p) { return p.trim(); }).filter(function (p) { return p.indexOf('=') === -1 && p.length > 0; });
var norm = sourceCatName.replace(/\s+/g, ' ').trim();
if (cats.some(function (c) { return c.replace(/\s+/g, ' ').trim() === norm; })) { return { skip: true, meta: { status: 'already_exists' } }; }
return {
text: text.replace(existing[0], function () { return existing[0].replace(/\}\}\s*$/, '|' + sourceCatName + '}}'); }),
summary: makeSummary('дополнение шаблона объединения [[:Категория:' + sourceCatName + ']]'),
meta: { status: 'updated' }
};
}
return { text: wrapInNoinclude(text, T_OPEN + 'Категория к объединению|' + dateStr + '|' + sourceCatName + T_CLOSE), meta: { status: 'created' } };
},
function (err, meta) { callback(!err, err ? err.info : ((meta && meta.status) || 'updated')); }
);
}
// ── Категории: обсуждение ────────────────────────────────────────────────
function buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, options) {
var opts = options || {};
var pages = Array.isArray(pageName) ? pageName : null;
var titleText;
if (pages && pages.length) {
titleText = String(opts.headerText || '').trim() || (formatPagesWithAnd(pages, ':') + (type === 'deletion' ? ' → удалить' : ''));
return '=== ' + titleText + ' ===\n';
}
var title = '=== [[:' + pageName + ']]';
if (type === 'rename' || type === 'merge') {
title += (type === 'rename' ? ' → ' : ' объединить с ') + formatCatLink(mainName);
if (additionalNames && additionalNames.length) {
var conj = type === 'rename' ? ' или ' : ' и ';
var head = additionalNames.slice(0, -1).map(formatCatLink).join(', ');
title += (additionalNames.length > 1 ? ', ' + head : '') + conj + formatCatLink(additionalNames[additionalNames.length - 1]);
}
} else if (type === 'deletion') {
title += ' → удалить';
}
return title + ' ===\n';
}
function createCategoryDiscussion(pageName, reason, type, callback, mainName, additionalNames, options) {
var cb = callback || function () {};
var opts = options || {};
var now = new Date();
var m = now.getUTCMonth(), year = now.getUTCFullYear(), day = now.getUTCDate();
var dateHeader = '== ' + day + ' ' + MONTHS_GEN[m] + ' ' + year + ' ==\n';
var discPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[m] + '_' + year;
var discTitle = buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, opts);
var discText = discTitle + (opts.reasonIsPrepared ? String(reason || '') : appendNominationSignature(reason)) + '\n';
var sectionTitle = discTitle.replace(/^===\s*/, '').replace(/\s*===\s*$/, '').trim();
var summaryTarget = Array.isArray(pageName) ? formatPagesWithAnd(pageName, ':') : '[[:' + pageName + ']]';
var summaryText = 'добавление обсуждения для ' + (Array.isArray(pageName) ? 'категорий ' : 'категории ') + summaryTarget;
publishNomination({
pageTitle: discPage,
readErrorMessage: 'Не удалось получить содержимое страницы обсуждения.',
summary: makeSummary(summaryText),
buildText: function (text) {
var todayIdx = text.indexOf(dateHeader.trim());
if (todayIdx !== -1) {
var endIdx = text.indexOf('\n== ', todayIdx + dateHeader.length);
if (endIdx === -1) endIdx = text.length;
var before = text.slice(0, endIdx);
return { text: (before.endsWith('\n\n') ? before.slice(0, -1) : before) + '\n' + discText + text.slice(endIdx) };
}
var searchStr = T_OPEN + 'ОБК-Навигация' + T_CLOSE;
var obkIdx = text.indexOf(searchStr);
if (obkIdx === -1) return { error: { code: 'insert_failed', info: 'Не удалось найти место для вставки.' } };
return { text: text.slice(0, obkIdx + searchStr.length) + '\n\n' + dateHeader + discText + text.slice(obkIdx + searchStr.length).replace(/^\n+/, '\n') };
}
}, function (err) {
if (err) { cb(err); return; }
cb(null, { pageTitle: discPage, sectionTitle: sectionTitle });
});
}
// ── Категории: завершение ОБКАТ ───────────────────────────────────────────
function markCategoryDiscussionAsDone(pageName) {
runPageEditWithStatus({
pageName: pageName,
pendingText: 'Снимается шаблон обсуждения...',
errorText: 'Снятие шаблона обсуждения.',
successText: 'Шаблон обсуждения снят.',
editOptions: { summary: makeSummary('обсуждение категории завершено'), readError: 'Не удалось получить содержимое.' },
buildFn: function (text) {
var allTpls = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|');
var patterns = [
new RegExp('<noinclude>\\s*\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}\\s*</noinclude>', 'i'),
new RegExp('\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}', 'i')
];
var match = null;
for (var i = 0; i < patterns.length; i++) { match = text.match(patterns[i]); if (match) break; }
if (!match) return { error: { code: 'no_changes', info: 'Шаблон обсуждаемой категории не найден.' } };
return {
text: text.replace(match[0], '').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n'),
meta: { tplDate: convertToStandardDate(match[2].split('|')[0].trim()) }
};
},
onSuccess: function (meta) {
var talkStatusId = logStatus('Обновляется шаблон на СО категории...', null, { pending: true, trackError: false });
updateCategoryTalkPage(pageName, meta.tplDate, function (talkErr, info) {
if (talkErr) { logStatus('Установка шаблона на СО.', talkErr, { statusId: talkStatusId }); unlockModalSubmit(); return; }
logStatus(
(info && (info.status === 'already_present' || info.status === 'no_changes')) ? 'Шаблон на СО уже установлен.' : 'Шаблон установлен на СО.',
null, { statusId: talkStatusId, trackError: false }
);
renderModalFooter('reload');
});
}
});
}
function updateCategoryTalkPage(categoryName, templateDate, callback) {
var cb = callback || function () {};
var talkPage = getTalkPage(categoryName);
var newTpl = T_OPEN + 'Обсуждавшаяся категория|' + templateDate + T_CLOSE;
getTextWithTimestamp(talkPage, function (text, baseTimestamp, readErr) {
if (readErr) { cb(makeReadError(readErr, 'talk_read_failed', 'Не удалось получить содержимое СО категории.')); return; }
if (text === null) {
apiReq({ title: talkPage, text: newTpl + '\n\n', summary: makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate), createonly: true },
'edit', function (resp) {
if (resp && resp.error) {
if (resp.error.code === 'articleexists') setTimeout(function () { updateCategoryTalkPage(categoryName, templateDate, cb); }, 1000);
else cb(resp.error);
} else cb(null, { status: 'created' });
});
return;
}
var discussedRe = new RegExp('\\{\\{\\s*(' + cfg.categoryTemplates.discussed + ')([^\\}]*?)\\s*\\}\\}', 'i');
var tplMatch = text.match(discussedRe);
var newText = text;
if (tplMatch) {
var existingDates = tplMatch[2].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
if (existingDates.indexOf(templateDate) !== -1) { cb(null, { status: 'already_present' }); return; }
newText = text.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}$/, '|' + templateDate + '}}'); });
} else {
newText = insertTplOnTalkPage(text, newTpl);
}
if (newText === text) { cb(null, { status: 'no_changes' }); return; }
var ep = {
title: talkPage,
text: newText,
summary: tplMatch
? makeSummary('обновление шаблона [[ш:Обсуждавшаяся категория]], добавлена дата ' + templateDate)
: makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate)
};
if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) { cb(resp && resp.error ? resp.error : null, resp && !resp.error ? { status: tplMatch ? 'updated' : 'created' } : null); });
});
}
// ── Быстрое объединение (Ctrl+клик КОБ) ─────────────────────────────────
function buildQuickMergeHtml(tplDate, targets, currentCatName) {
return joinHtml([
'<p>Найден шаблон с датой <strong>', escapeHtml(tplDate), '</strong>. Категории для объединения:</p>',
'<pre style="background:', tk.bgBase, ';color:', tk.cBase, ';padding:10px;border:1px solid ', tk.bSubS, ';border-radius:4px;margin-bottom:10px;">',
targets.map(function (c) { return '• ' + escapeHtml(c); }).join('\n'),
'</pre>',
'<p><strong>Текущая категория:</strong> ', escapeHtml(currentCatName), '</p>',
'<p style="color:', tk.cSub, ';">Шаблон будет добавлен во все указанные выше категории.</p>'
]);
}
function showQuickMergeModal() {
getText(mwCfg.wgPageName, function (text, readErr) {
if (readErr) { alert('Не удалось получить содержимое: ' + (readErr.info || readErr.code || 'ошибка API') + '.'); return; }
if (!text) { alert('Не удалось получить содержимое.'); return; }
var mergeRe = getCategoryMergeRe();
var match = text.match(mergeRe);
if (!match) { alert('В текущей категории не найден шаблон "Категория к объединению".'); return; }
var params = match[1].split('|').map(function (p) { return p.trim(); });
var tplDate = params[0];
var targets = params.slice(1);
if (!targets.length) { alert('В шаблоне не найдены целевые категории.'); return; }
createModal({ title: 'Быстрое добавление шаблона объединения' });
var currentCatName = normTitle(stripCatPrefix(mwCfg.wgPageName));
$('#removerModalContent').html(buildQuickMergeHtml(tplDate, targets, currentCatName));
renderModalFooter('submit', {
showCheckbox: false,
submitText: 'Добавить шаблоны',
onSubmit: function () {
startProcessing();
$('#removerSubmit').prop('disabled', true);
eachSequential(targets, function (target, next) {
addMergeTemplateToTargetCategory('Категория:' + target, currentCatName, tplDate, function (success, status) {
if (success) logStatus('Категория [[:Категория:' + target + ']] (' + formatMergeStatus(status) + ').', null, { trackError: false });
else logStatus('Ошибка [[:Категория:' + target + ']].', { code: 'merge_target_failed', info: status }, { trackError: false });
next();
});
}, function () {
if (isError) markSubmitError(); else renderModalFooter('close');
});
return true;
}
});
});
}
// ── ЗКА/Защита: публикация ───────────────────────────────────────────────
function getReporterContext(mode) {
var rawPage = mwCfg.wgPageName;
var pageName = normTitle(rawPage)
.replace(/(Special|Служебная):(Contributions|Вклад)\//i, 'User:')
.replace(/(User talk:|Обсуждение участни(ка|цы):)/i, 'User:');
var isUserRelated = /user|contrib|участни|вклад/i.test(rawPage);
var displayName = normTitle(rawPage)
.replace(/(Special|Служебная):(Вклад|Contributions)\//i, '')
.replace(/(User talk:|Обсуждение участни(ка|цы):)/i, '')
.replace(/(user|участни(к|ца)):/i, '');
var pageLink = '[[' + pageName + (isUserRelated ? '|' + displayName + ']]' : ']]');
var reportPage = mode === 'request' ? 'Википедия:Запросы к администраторам' : 'Википедия:Установка защиты';
return { pageName: pageName, pageLink: pageLink, displayName: displayName, reportPage: reportPage };
}
function doReport(ctx, fast, protectMode) {
var header = $('#rmReportHeader').val() || ctx.pageLink;
var text = $('#rmReportText').val() || '';
var isZka = ctx.reportPage === 'Википедия:Запросы к администраторам';
var isRemoveProtect = !isZka && protectMode === 'remove';
startProcessing();
var targetPage, editParams, sectionForLink;
if (fast) {
targetPage = 'Википедия:Запросы к администраторам/Быстрые';
sectionForLink = null;
editParams = {
appendtext: '\n\n' + T_OPEN + 'sub' + 'st:t:preload/ЗКАБ/subst|\n | участник = ' + ctx.displayName +
'| страница = | пояснение = ' + text + T_CLOSE + '\n',
summary: makeSummary('новый запрос [[Special:Contributions/' + ctx.displayName + ']]')
};
} else if (isZka) {
targetPage = ctx.reportPage;
sectionForLink = extractDisplayedText(header);
var isIpFull = /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/.test(ctx.displayName);
editParams = {
section: 'new',
sectiontitle: header,
text: '* ' + T_OPEN + 'userlinks|' + ctx.displayName + (isIpFull ? '|ip=1' : '') + T_CLOSE + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
} else {
targetPage = isRemoveProtect ? 'Википедия:Снятие защиты' : 'Википедия:Установка защиты';
var pages = collectInputValues('.rmProtectPageInput');
if (!pages.length) pages = [ctx.pageName];
var sectionTitle, pageLines;
if (pages.length === 1) {
sectionTitle = '[[' + pages[0] + ']]';
pageLines = '* ' + T_OPEN + 'pagelinks-protect|' + pages[0] + T_CLOSE;
} else {
sectionTitle = ($('#rmProtectHeader').val() || '').trim() || pages.map(function (p) { return '[[' + p + ']]'; }).join(', ');
pageLines = pages.map(function (p) { return '* ' + T_OPEN + 'pagelinks-protect|' + p + T_CLOSE; }).join('\n');
}
sectionForLink = extractDisplayedText(sectionTitle);
editParams = {
section: 'new',
sectiontitle: sectionTitle,
text: pageLines + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
}
var statusId = logStatus('Публикуется запрос на «' + targetPage + '»...', null, { pending: true, trackError: false });
apiReq($.extend({ title: targetPage }, editParams), 'edit', function (resp) {
if (resp && resp.error) {
logStatus('Публикация запроса на «' + targetPage + '».', resp.error, { statusId: statusId });
markSubmitError();
if (isZka) $('#rmReportFast').prop('disabled', false);
return;
}
logStatus('Запрос опубликован.', null, { statusId: statusId, trackError: false });
appendNominationLink(targetPage, sectionForLink);
if (sectionForLink) subscribeToTopic(targetPage, sectionForLink);
renderModalFooter('reload');
});
}
// ═══════════════════════════════════════════════════════════════════════════
// ДИСПЕТЧЕР
// ═══════════════════════════════════════════════════════════════════════════
function handleMenuClick(item, event) {
isError = false;
var op = OPERATIONS_MAP[item.id];
// Специальный случай: КОБ категории с Ctrl — быстрое добавление шаблона
if (item.id === 'cat-merge' && event && event.ctrlKey) {
showQuickMergeModal();
return;
}
if (!op) {
console.error('RemoverCore: неизвестная операция', item.id);
return;
}
var handlerFn = handlers[op.handler];
if (typeof handlerFn !== 'function') {
console.error('RemoverCore: обработчик не найден', op.handler);
return;
}
handlerFn(op, event);
}
// ─── Экспорт ─────────────────────────────────────────────────────────────
window.RemoverCore = { handleMenuClick: handleMenuClick };
}());
ro80mrw0wgbeb568uxg0txnt0f4cau9
739913
739910
2026-05-01T06:45:33Z
Solidest
54422
739913
javascript
text/javascript
/**
* Remover — ядро (core).
* Загружается лениво при первом клике по пункту меню.
* Ожидает, что window.RemoverState уже задан remover-loader.js.
*
* Архитектура: единый реестр OPERATIONS описывает все операции (КБУ, КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС, Защита, Запрос, Снятие, кат-варианты).
* Логика выполнения сосредоточена в универсальных обработчиках.
* Экспортирует: window.RemoverCore.handleMenuClick(item, event)
*/
(function () {
'use strict';
var state = window.RemoverState;
if (!state) { console.error('RemoverCore: window.RemoverState не задан.'); return; }
var mwCfg = state.mwCfg;
var cfg = applyCoreConfigDefaults(state.cfg || {});
var isCategory = state.isCategory;
var isVector22 = state.isVector22;
var scriptLink = cfg.scriptLink;
var settingsOptionName = state.settingsOptionName || 'userjs-remover-settings';
var settingsVersion = 1;
var settingsMenuMeta = collectSettingsMenuMeta();
var settingsArticleItemLabels = settingsMenuMeta.articleLabels;
var settingsCategoryItemLabels = settingsMenuMeta.categoryLabels;
var settingsItemLabelById = settingsMenuMeta.idToLabel;
var settingsItemLabelByNorm = settingsMenuMeta.labelByNorm;
var settingsItemLabelOrder = settingsMenuMeta.labelOrder;
var settingsDefaults = getDefaultSettings();
var MENU_TITLE_PRESET_CACTIONS = '__remover_portlet_cactions__';
var MENU_TITLE_PRESET_PAGE = '__remover_portlet_page__';
var MENU_TITLE_PRESET_TOOLS = '__remover_portlet_tools__';
var initialSettings = normalizeRemoverSettings(readSettingsOptionState(state.settings || {}));
var setAlert = ('setAlert' in state) ? !!state.setAlert : initialSettings.notifyAuthor;
var setSubscribe = ('setSubscribe' in state) ? !!state.setSubscribe : initialSettings.subscribeTopic;
var signatureSeparator = ('signatureSeparator' in state && typeof state.signatureSeparator === 'string')
? state.signatureSeparator.trim()
: initialSettings.signatureSeparator;
initialSettings.notifyAuthor = setAlert;
initialSettings.subscribeTopic = setSubscribe;
initialSettings.signatureSeparator = signatureSeparator;
state.cfg = cfg;
state.settings = clonePlainObject(initialSettings);
state.setAlert = setAlert;
state.setSubscribe = setSubscribe;
// ─── Константы ──────────────────────────────────────────────────────────
var MONTHS_NOM = ['Январь','Февраль','Март','Апрель','Май','Июнь','Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'];
var MONTHS_GEN = ['января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря'];
var MONTHS_NOM_LOWER = MONTHS_NOM.map(function (m) { return m.toLowerCase(); });
var T_OPEN = '{' + '{';
var T_CLOSE = '}' + '}';
var KBU_CRITERIA_PAGE = 'Википедия:Критерии быстрого удаления';
var RE_ESCAPE = /[.*+?^${}()|[\]\\]/g;
var RE_KU_ON_PAGE = /\{\{\s*(?:к\s*удалению|ку)\s*(?:\||\}\})/i;
var RE_KPM_ON_PAGE = /\{\{\s*(?:к\s*переименованию|кпм|rename)\s*(?:\||\}\})/i;
var RE_KBU_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?(?:db\s*-[^|}\s]+|уд\s*-[^|}\s]+|к\s*быстрому\s*удалению|к\s*отсроченному\s*удалению|deleteslow|ds|hang\s*-?\s*on)\s*(?:\||\}\})/i;
var RE_KUL_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?к\s*улучшению\s*(?:\||\}\})/i;
var KBU_PATTERN_STR = 'db\\s*-[^|}\\s]+|уд\\s*-[^|}\\s]+|к\\s*быстрому\\s*удалению|к\\s*отсроченному\\s*удалению|deleteslow|ds';
var RE_KBU_PATTERNS = new RegExp('(?:' + KBU_PATTERN_STR + ')', 'i');
var KUL_PATTERN_STR = 'к\\s*улучшению';
var RE_KUL_PATTERN = new RegExp(KUL_PATTERN_STR, 'i');
var HANGON_PATTERN_STR = 'hang\\s*-?\\s*on';
var RE_HANGON = new RegExp(HANGON_PATTERN_STR, 'i');
var RE_DATE_ISO = /^(\d{4})-(\d{2})-(\d{2})$/;
var RE_DATE_RUSSIAN = /^(\d{1,2})\s+([\u0430-\u044f\u0410-\u042f\u0451\u0401.]+)\s+(\d{2}|\d{4})$/;
var RE_DATE_DASH = /^(\d{1,4})\s*-\s*(\d{1,2})\s*-\s*(\d{1,4})$/;
var RE_DATE_DOT = /^(\d{1,2})\s*\.\s*(\d{1,2})\s*\.\s*(\d{2}|\d{4})$/;
var RE_DATE_SLASH = /^(\d{1,4})\s*\/\s*(\d{1,2})\s*\/\s*(\d{1,4})$/;
var RE_NOINCLUDE = /(\s*)<noinclude>([\s\S]*?)<\/noinclude>/i;
var RE_TEMPLATE_NS = /^шаблон\s*:\s*/i;
// ─── Глобальные переменные сессии ────────────────────────────────────────
var isError = false;
var logStatusSeq = 0;
var resizeObservers = [];
var modalLayoutSyncHandlers = [];
var tplAliasCache = {};
// ─── Стили ───────────────────────────────────────────────────────────────
var stStyles = cfg.modalStyles;
var tk = {
cBase: 'var(--color-base, #202122)',
cSub: 'var(--color-subtle, #72777d)',
cSubM: 'var(--color-subtle, #54595d)',
cInv: 'var(--color-inverted-fixed, #fff)',
cProg: 'var(--color-progressive, #3366cc)',
cProgH: 'var(--color-progressive--hover, #2a4b8d)',
cDang: 'var(--color-destructive, #d73333)',
cDis: 'var(--color-disabled, var(--color-subtle, #72777d))',
bgBase: 'var(--background-color-base, #fff)',
bgNSub: 'var(--background-color-neutral-subtle, #f8f9fa)',
bgN: 'var(--background-color-neutral, #eaecf0)',
bgDis: 'var(--background-color-disabled, var(--background-color-neutral, #eaecf0))',
bgProg: 'var(--background-color-progressive, #3366cc)',
bgProgH:'var(--background-color-progressive--hover, #2a4d8f)',
bgSucc: 'var(--background-color-success, #14866d)',
bgSuccH:'var(--background-color-success--hover, #0f6d57)',
bSub: 'var(--border-color-subtle, #a2a9b1)',
bSubS: 'var(--border-color-subtle, #ddd)',
bDis: 'var(--border-color-disabled, var(--border-color-subtle, #a2a9b1))',
bProg: 'var(--border-color-progressive, #3366cc)',
bProgH: 'var(--border-color-progressive--hover, #2a4d8f)',
bSucc: 'var(--border-color-success, #14866d)',
bSuccH: 'var(--border-color-success--hover, #0f6d57)'
};
var sz = {
taH: '180px',
taMinH: '100px',
taMinW: '180px',
mobileBp: 720,
modalRatio: 0.4,
modalMinWide: 420,
modalDefaultWide: 720,
viewportGap: 24,
touchDesktopGap: 120
};
var btnBase = 'border-radius:4px;padding:8px 16px;cursor:pointer;font-size:14px;font-family:inherit;transition:background .1s;';
var neutralVis = 'background:' + tk.bgNSub + ';border:1px solid ' + tk.bSub + ';color:' + tk.cBase + ';border-radius:4px;';
var stCancel = neutralVis + btnBase;
var stSubmit = 'background:' + tk.bgProg + ';color:' + tk.cInv + ';border:1px solid ' + tk.bProg + ';' + btnBase;
var stReload = 'background:' + tk.bgSucc + ';color:' + tk.cInv + ';border:1px solid ' + tk.bSucc + ';' + btnBase;
var stInputBox = 'flex:1;padding:6px;box-sizing:border-box;min-width:0;border:1px solid ' + tk.bSub + ';background:' + tk.bgBase + ';color:inherit;border-radius:2px;';
var stInputFull= 'width:100%;padding:6px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:2px;background:' + tk.bgBase + ';color:inherit;margin-bottom:10px;';
var stRow = 'display:flex;margin-bottom:6px;';
var stToolBtn = neutralVis + 'padding:4px 8px;margin-left:4px;cursor:pointer;font-size:12px;';
var stRemoveBtn= neutralVis + 'display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;padding:0;width:32px;height:32px;margin-left:4px;cursor:pointer;font-size:12px;line-height:1;';
var stFooterWrap = 'display:flex;align-items:center;gap:8px;flex-wrap:wrap;';
var stFooterChecks = 'display:flex;flex-direction:column;gap:4px;margin-right:auto;flex:1 1 220px;min-width:0;';
var stFooterCheckLabel = 'display:inline-flex;align-items:flex-start;gap:6px;font-size:14px;line-height:1.4;max-width:100%;';
var stFooterActions = 'display:flex;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;justify-content:flex-end;margin-left:auto;';
var stHeaderIconBtn = 'margin:0 0 0 auto;width:32px;min-width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:4px;background:' + tk.bgNSub + ';color:' + tk.cSubM + ';cursor:pointer;font-size:16px;line-height:1;';
var multiNominationGap = '6px';
var RESIZE_CLASS = 'rm-resizable';
// ═══════════════════════════════════════════════════════════════════════════
// РЕЕСТР ОПЕРАЦИЙ
// Каждая запись описывает одну кнопку меню. Поля:
// id — идентификатор (совпадает с item.id из loader)
// handler — имя метода-обработчика в объекте handlers
// handlerArg — аргумент, передаваемый в handler (опционально)
// ═══════════════════════════════════════════════════════════════════════════
var OPERATIONS = [
// ── Статьи ──────────────────────────────────────────────────────────
{
id: 'fRm',
label: 'КБУ',
handler: 'showKbu',
// Параметры номинации: заполняются при submit
nomination: {
pageTitle: function (pg) { return normTitle(pg); },
// шаблон встраивается в статью, номинационная страница отсутствует
inArticle: true
}
},
{
id: 'tRm',
label: 'КУ',
handler: 'showNomination',
nomination: {
comment: 'к удалению',
template: 'к удалению',
navTemplate: 'КУ',
nomPage: function (date) { return 'Википедия:К удалению/' + date; },
supportsMulti: true,
supportsTransfer: true,
// шаблон встраивается в статью через <noinclude>
inArticle: true,
articleTpl: function (tplpar, date) { return 'к удалению|' + date + (tplpar ? '|' + tplpar : ''); }
}
},
{
id: 'rnm',
label: 'КПМ',
handler: 'showNomination',
nomination: {
comment: 'к переименованию',
template: 'к переименованию',
navTemplate: 'КПМ',
nomPage: function (date) { return 'Википедия:К переименованию/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к переименованию|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'rename',
firstId: 'rmRenameFirst', inputClass: 'rmRenameInput',
firstPh: 'Новое название',
addBtnId: 'rmAddRename', addBtnLabel: 'Добавить вариант',
containerId: 'rmRenameContainer', addPh: 'Дополнительный вариант',
maxRows: 2, maxMsg: 'Максимум 3 варианта переименования.'
}
}
},
{
id: 'imp',
label: 'КУЛ',
handler: 'showNomination',
nomination: {
comment: 'к срочному улучшению',
template: 'к улучшению',
navTemplate: 'КУЛ',
nomPage: function (date) { return 'Википедия:К улучшению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к улучшению|' + date + (tplpar ? '|' + tplpar : ''); }
}
},
{
id: 'merge',
label: 'КОБ',
handler: 'showNomination',
nomination: {
comment: 'к объединению с другой',
template: 'к объединению',
navTemplate: 'КОБ',
nomPage: function (date) { return 'Википедия:К объединению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к объединению|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'merge',
firstId: 'rmMergeFirst', inputClass: 'rmMergeInput',
firstPh: 'Объединить с…',
addBtnId: 'rmAddMerge', addBtnLabel: '+',
containerId: 'rmMergeContainer', addPh: 'Дополнительная статья',
maxRows: 10, maxMsg: 'Максимум 11 статей для объединения.'
}
}
},
{
id: 'split',
label: 'КРАЗД',
handler: 'showNomination',
nomination: {
comment: 'к разделению',
template: 'к разделению',
navTemplate: 'КР',
nomPage: function (date) { return 'Википедия:К разделению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к разделению|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'split',
firstId: 'rmSplitFirst', inputClass: 'rmSplitInput',
firstPh: 'Разделить на…',
addBtnId: 'rmAddSplit', addBtnLabel: '+',
containerId: 'rmSplitContainer', addPh: 'Дополнительная статья'
}
}
},
{
id: 'recov',
label: 'ВУС',
handler: 'showNomination',
nomination: {
comment: '',
template: 'к восстановлению',
navTemplate: 'ВУС',
nomPage: function (date) { return 'Википедия:К восстановлению/' + date; },
inArticle: false // шаблон не ставится в (удалённую) статью
}
},
{
id: 'close',
label: 'Снятие',
handler: 'showArticleClose'
},
// ── Запросы ─────────────────────────────────────────────────────────
{
id: 'protect',
label: 'Защита',
handler: 'showReport',
reportMode: 'protect'
},
{
id: 'request',
label: 'Запрос',
handler: 'showReport',
reportMode: 'request'
},
// ── Категории ────────────────────────────────────────────────────────
{
id: 'cat-fRm',
label: 'КБУ',
handler: 'showKbu',
forCategory: true
},
{
id: 'cat-discuss',
label: 'Обсудить',
handler: 'showCatNomination',
catType: 'discuss'
},
{
id: 'cat-delete',
label: 'Удалить',
handler: 'showCatNomination',
catType: 'deletion'
},
{
id: 'cat-rename',
label: 'Переименовать',
handler: 'showCatNomination',
catType: 'rename'
},
{
id: 'cat-merge',
label: 'Объединить',
handler: 'showCatNomination',
catType: 'merge'
},
{
id: 'cat-done',
label: 'Снятие',
handler: 'showCatClose'
}
];
// Быстрый поиск по id
var OPERATIONS_MAP = {};
OPERATIONS.forEach(function (op) { OPERATIONS_MAP[op.id] = op; });
// ─── Тексты переноса (КБУ → КУ) ─────────────────────────────────────────
var transferTexts = {
kbu: { notice: 'Перенесено с быстрого удаления.', hint: 'Шаблоны КБУ и Hangon будут сняты.' },
kul: { notice: 'Перенесено с КУЛ.', hint: 'Шаблоны КУЛ будут сняты.' },
both: { notice: 'Перенесено с быстрого удаления и КУЛ.', hint: 'Шаблоны КБУ, КУЛ и Hangon будут сняты.' }
};
// ═══════════════════════════════════════════════════════════════════════════
// УТИЛИТЫ
// ═══════════════════════════════════════════════════════════════════════════
function escapeRegExp(s) { return s.replace(RE_ESCAPE, '\\$&'); }
function escapeHtml(s) {
return String(s || '')
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
function joinHtml(parts) { return parts.join(''); }
function ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
function padTwo(n) { return n < 10 ? '0' + n : String(n); }
function expandTwoDigitYear(value) {
return 2000 + parseInt(value, 10);
}
function monthToNumber(name) {
var lower = name.toLowerCase().replace(/\.$/, '');
var idx = MONTHS_GEN.indexOf(lower);
if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower);
if (idx === -1 && lower.length >= 3) {
for (var i = 0; i < MONTHS_GEN.length; i++) {
if (MONTHS_GEN[i].indexOf(lower) === 0 || MONTHS_NOM_LOWER[i].indexOf(lower) === 0) return i + 1;
}
}
return idx + 1;
}
function makeStandardDate(yearValue, monthValue, dayValue) {
var yearText = String(yearValue || '').trim();
var year = yearText.length === 2 ? expandTwoDigitYear(yearText) : parseInt(yearText, 10);
var month = parseInt(monthValue, 10);
var day = parseInt(dayValue, 10);
var maxDay;
if ((yearText.length !== 2 && yearText.length !== 4) || isNaN(year) || isNaN(month) || isNaN(day) || year < 1 || month < 1 || month > 12 || day < 1) return null;
maxDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
if (day > maxDay) return null;
return year + '-' + padTwo(month) + '-' + padTwo(day);
}
function normalizeIsoDate(value) {
var m = String(value || '').trim().match(RE_DATE_ISO);
return m ? makeStandardDate(m[1], m[2], m[3]) : null;
}
function normalizeTemplateName(name) {
return (name || '').toLowerCase().replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
}
function getDate(dateString) {
var d = dateString ? new Date(dateString) : new Date();
var iso = d.getUTCFullYear() + '-' + padTwo(d.getUTCMonth() + 1) + '-' + padTwo(d.getUTCDate());
var rus = d.getUTCDate() + ' ' + MONTHS_GEN[d.getUTCMonth()] + ' ' + d.getUTCFullYear();
return [iso, rus];
}
function convertToStandardDate(dateStr) {
var value = String(dateStr || '').replace(/\s+/g, ' ').trim();
var m;
var mo;
var normalized;
m = value.match(RE_DATE_ISO);
if (m) return normalizeIsoDate(value) || '';
m = value.match(RE_DATE_RUSSIAN);
if (m) {
mo = monthToNumber(m[2]);
normalized = mo ? makeStandardDate(m[3], mo, m[1]) : null;
return normalized || '';
}
m = value.match(RE_DATE_DASH);
if (m) {
normalized = makeStandardDate(m[1], m[2], m[3]);
return normalized || '';
}
m = value.match(RE_DATE_DOT);
if (m) {
normalized = makeStandardDate(m[3], m[2], m[1]);
return normalized || '';
}
m = value.match(RE_DATE_SLASH);
if (m) {
normalized = makeStandardDate(m[1], m[2], m[3]);
return normalized || '';
}
return value;
}
function getTalkPage(pageName) {
var match = /([^:]*:)?(.*)/.exec(pageName);
if (match[1]) {
var ns = mwCfg.wgNamespaceIds[match[1].slice(0, -1).toLowerCase().replace(/ /g, '_')];
if (ns !== undefined) return mwCfg.wgFormattedNamespaces[ns | 1] + ':' + match[2];
}
return 'Обсуждение:' + pageName;
}
function normTitle(s) { return (s || '').replace(/_/g, ' '); }
function stripCatPrefix(s) { return (s || '').replace(/^Категория:\s*/i, ''); }
function normalizeCategoryPageName(value) {
var title = normTitle(value).trim();
var nsMatch, nsKey, ns;
if (!title) return '';
nsMatch = title.match(/^([^:]+):(.+)$/);
if (nsMatch) {
nsKey = nsMatch[1].toLowerCase().replace(/ /g, '_');
ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[nsKey];
if (ns === 14) return (mwCfg.wgFormattedNamespaces[14] || 'Категория') + ':' + nsMatch[2].trim();
return title;
}
return (mwCfg.wgFormattedNamespaces[14] || 'Категория') + ':' + title;
}
function makeSummary(text) { return scriptLink + ': ' + text; }
function appendNominationSignature(text) {
var body = String(text || '');
return body + (signatureSeparator ? ' ' + signatureSeparator : '') + ' ~~' + '~~';
}
function extractDisplayedText(s) {
return (s || '').replace(/\[\[:?(?:[^|\]]+\|)?(.+?)\]\]/g, '$1');
}
function collectInputValues(selector) {
return $(selector).map(function () { return $(this).val().trim(); }).get().filter(Boolean);
}
function applyCoreConfigDefaults(config) {
var defaults = {
scriptLink: '[[Участник:Solidest/Remover|Remover]]',
fastRemoveReasons: {
general: [
['уд-бессвязно', 'О1 Бессвязный текст'],
['уд-тест', 'О2 Тестовая страница'],
['уд-ванд', 'О3 Вандальная страница'],
['уд-повторно', 'О4 Уже удалялось'],
['уд-автор', 'О5 По просьбе автора'],
['уд-обс', 'О6 Ненужная подстраница'],
['уд-переим', 'О7 Для переименования'],
['уд-дубль', 'О8 Дубликат'],
['уд-реклама', 'О9 Реклама или спам'],
['db-badtalk', 'О10 Нецелевая СО'],
['уд-копивио', 'О11 Нарушение АП']
],
articles: [
['подст:ds', 'ds Отсроченное пусто или коротко', 'С'],
['уд-пусто', 'С1 Пусто или коротко'],
['уд-иностр', 'С2 Не на русском'],
['уд-ссылки', 'С3 Лишь ссылки'],
['уд-нз', 'С5 Явно незначимо'],
['уд-бям', 'С7 Создано нейросетью']
],
redirects: [
['уд-в никуда', 'П1 Перенапр. в никуда'],
['db-redirspace', 'П2 Межпростр. перенапр.'],
['уд-опечатка', 'П3 Перенапр. с опечаткой'],
['уд-падеж', 'П4 Не именительный падеж'],
['уд-смысл', 'П5 Неверное перенапр.'],
['db-redirtalk', 'П6 Перенапр. на СО']
],
files: [
['db-duplicate', 'Ф1 Копия файла'],
['db-badimage', 'Ф2 Повреждённый файл'],
['подст:nld', 'Ф3 Нет данных о лицензии'],
['подст:nsd', 'Ф3 Нет данных о источнике'],
['подст:nad', 'Ф3 Нет данных о авторе'],
['подст:dd', 'Ф3 Сомнительные данные файла'],
['подст:ofud', 'Ф4 Неиспользуемый КДИ'],
['подст:dfud', 'Ф5 Нет КДИ'],
['db-badfairuse', 'Ф6 Неоправданное КДИ'],
['подст:rfu', 'Ф7 Заменяемый КДИ'],
['NCT', 'Ф8 Есть на Складе'],
['подст:Nothost', 'Ф9 Файл — ВП:НЕХОСТИНГ']
],
categories: [
['уд-пусткат', 'К1 Пустая категория'],
['db-templatecat', 'К1.2 Разобранная служебная кат.'],
['уд-перекат', 'К2 Переименованная кат.']
],
users: [
['уд-владелец', 'У1 По желанию владельца'],
['уд-анон', 'У2 Устаревшая СО анонима'],
['уд-несущ', 'У3 Несуществующий участник'],
['уд-нецелевое', 'У4 Нецелевое использ. ЛП'],
['уд-неактив', 'У5 Подстраница неактивного']
],
special: [
['db', 'Особый случай']
]
},
fastRemoveCriteriaAnchors: {
'подст:ds': 'С1',
deleteslow: 'С1',
ds: 'С1'
},
requiredParamTemplates: {
'уд-переим': 'страницу, которую нужно переименовать',
'уд-дубль': 'страницу-дубликат',
'уд-копивио': 'URL источника нарушения АП',
'db-duplicate': 'имя файла-оригинала',
'подст:rfu': 'имя заменяемого файла',
'NCT': 'имя файла на Викискладе',
'уд-перекат': 'новое название категории',
'db': '!причину удаления'
},
categoryTemplates: {
discuss: 'Обсуждаемая категория|обсуждаемая категория|Acat|acat|ОКТО|окто|Категория к обсуждению|категория к обсуждению',
rename: 'Категория к переименованию|категория к переименованию|Anacat|anacat',
merge: 'Категория к объединению|категория к объединению|Amergecat|amergecat|Cfm|cfm',
discussed: 'Обсуждавшаяся категория|обсуждавшаяся категория|Обсуждалась|обсуждалась|Обсуждалось|обсуждалось'
},
modalStyles: {
border: '1px solid var(--border-color-progressive, #3366bb)',
background: 'var(--background-color-base, #f8f9fa)',
borderRadius: '6px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
headerColor: 'var(--color-progressive, #3366bb)'
}
};
config.scriptLink = config.scriptLink || defaults.scriptLink;
config.fastRemoveReasons = $.extend({}, defaults.fastRemoveReasons, config.fastRemoveReasons || {});
config.fastRemoveCriteriaAnchors = $.extend({}, defaults.fastRemoveCriteriaAnchors, config.fastRemoveCriteriaAnchors || {});
config.requiredParamTemplates = $.extend({}, defaults.requiredParamTemplates, config.requiredParamTemplates || {});
config.categoryTemplates = $.extend({}, defaults.categoryTemplates, config.categoryTemplates || {});
config.modalStyles = $.extend({}, defaults.modalStyles, config.modalStyles || {});
return config;
}
function clonePlainObject(obj) {
return JSON.parse(JSON.stringify(obj || {}));
}
function normalizeMenuLabel(value) {
return String(value || '').replace(/\s+/g, ' ').trim().toLowerCase();
}
function readSettingsOptionState(fallback) {
var base = clonePlainObject(fallback || {});
var stored;
var raw = null;
if (!mw.user || !mw.user.options || typeof mw.user.options.get !== 'function') return base;
stored = mw.user.options.get(settingsOptionName);
if (typeof stored === 'string' && stored.trim()) {
try {
raw = JSON.parse(stored);
} catch (e) {
console.warn('RemoverCore: не удалось прочитать сохранённые настройки полностью, будут использованы данные loader.', e);
}
}
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return base;
return $.extend({}, raw, base, ('quickPhrases' in raw) ? { quickPhrases: raw.quickPhrases } : {});
}
function normalizeQuickPhraseValue(value) {
return String(value == null ? '' : value).replace(/\r\n?/g, '\n').trim();
}
function normalizeQuickPhrasesList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
var normalized = [];
(source || []).forEach(function (value) {
var phrase = normalizeQuickPhraseValue(value);
if (phrase && normalized.indexOf(phrase) === -1) normalized.push(phrase);
});
return normalized;
}
function collectSettingsMenuMeta() {
var labels = [];
var articleLabels = [];
var categoryLabels = [];
var idToLabel = {};
var labelByNorm = {};
var labelOrder = {};
function collect(items, targetLabels) {
items.forEach(function (item) {
var id;
var label;
var normLabel;
if (!item || item.type === 'separator') return;
id = String(item.id || '').trim();
label = String(item.label || '').trim();
normLabel = normalizeMenuLabel(label);
if (id && label && !(id in idToLabel)) idToLabel[id] = label;
if (label && targetLabels.indexOf(label) === -1) targetLabels.push(label);
if (normLabel && !(normLabel in labelByNorm)) {
labelByNorm[normLabel] = label;
labelOrder[label] = labels.length;
labels.push(label);
}
});
}
collect(cfg.articleMenuItems, articleLabels);
collect(cfg.categoryMenuItems, categoryLabels);
return {
labels: labels,
articleLabels: articleLabels,
categoryLabels: categoryLabels,
idToLabel: idToLabel,
labelByNorm: labelByNorm,
labelOrder: labelOrder
};
}
function buildSettingsMenuItemsHint() {
var parts = [];
if (settingsArticleItemLabels.length) {
parts.push('<div class="rmSettingsHintRow"><span class="rmSettingsHintBadge">Статьи</span>' + escapeHtml(settingsArticleItemLabels.join(', ')) + '.</div>');
}
if (settingsCategoryItemLabels.length) {
parts.push('<div class="rmSettingsHintRow"><span class="rmSettingsHintBadge">Категории</span>' + escapeHtml(settingsCategoryItemLabels.join(', ')) + '.</div>');
}
return parts.length ? '<div class="rmSettingsHintList">' + parts.join('') + '</div>' : '';
}
function getDefaultSettings() {
return {
version: settingsVersion,
excludedNamespaces: normalizeNumberList(cfg.excludedNamespaces, []),
notifyAuthor: !!cfg.defaultNotifyAuthor,
subscribeTopic: !!cfg.defaultSubscribeTopic,
menuTitle: (typeof cfg.menuTitle === 'string' && cfg.menuTitle.trim()) ? cfg.menuTitle.trim() : 'Remover',
disabledItems: [],
quickPhrases: normalizeQuickPhrasesList(cfg.quickPhrases, []),
showMenuIcons: !!cfg.showMenuIcons,
signatureSeparator: (typeof cfg.signatureSeparator === 'string') ? cfg.signatureSeparator.trim() : ''
};
}
function normalizeNumberList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
return source
.map(function (value) { return parseInt(value, 10); })
.filter(function (value, index, arr) { return !isNaN(value) && arr.indexOf(value) === index; })
.sort(function (a, b) { return a - b; });
}
function normalizeDisabledItemValue(value) {
var token = String(value || '').trim();
if (!token) return null;
if (settingsItemLabelById[token]) return settingsItemLabelById[token];
return settingsItemLabelByNorm[normalizeMenuLabel(token)] || null;
}
function compareSettingsMenuLabels(a, b) {
var ai = Object.prototype.hasOwnProperty.call(settingsItemLabelOrder, a) ? settingsItemLabelOrder[a] : Number.MAX_SAFE_INTEGER;
var bi = Object.prototype.hasOwnProperty.call(settingsItemLabelOrder, b) ? settingsItemLabelOrder[b] : Number.MAX_SAFE_INTEGER;
if (ai !== bi) return ai - bi;
return a.localeCompare(b, 'ru');
}
function normalizeDisabledItemsList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
return source
.map(normalizeDisabledItemValue)
.filter(function (value, index, arr) { return value && arr.indexOf(value) === index; })
.sort(compareSettingsMenuLabels);
}
function normalizeMenuTitleSetting(value, fallback) {
var menuTitle = String(value || '').trim();
if (!menuTitle) return fallback;
if (menuTitle === MENU_TITLE_PRESET_CACTIONS || /^(#)?p-cactions$/i.test(menuTitle)) return MENU_TITLE_PRESET_CACTIONS;
if (menuTitle === MENU_TITLE_PRESET_PAGE || /^(#)?p-page$/i.test(menuTitle) || /^(#)?p-actions$/i.test(menuTitle)) return MENU_TITLE_PRESET_PAGE;
if (menuTitle === MENU_TITLE_PRESET_TOOLS || /^(#)?p-tb$/i.test(menuTitle)) return MENU_TITLE_PRESET_TOOLS;
return menuTitle;
}
function normalizeRemoverSettings(raw) {
var defaults = clonePlainObject(settingsDefaults);
var source = (raw && typeof raw === 'object') ? raw : {};
return {
version: settingsVersion,
excludedNamespaces: normalizeNumberList(source.excludedNamespaces, defaults.excludedNamespaces || []),
notifyAuthor: ('notifyAuthor' in source) ? !!source.notifyAuthor : !!defaults.notifyAuthor,
subscribeTopic: ('subscribeTopic' in source) ? !!source.subscribeTopic : !!defaults.subscribeTopic,
menuTitle: normalizeMenuTitleSetting(
(typeof source.menuTitle === 'string' && source.menuTitle.trim()) ? source.menuTitle : '',
typeof defaults.menuTitle === 'string' && defaults.menuTitle.trim() ? defaults.menuTitle.trim() : 'Remover'
),
disabledItems: normalizeDisabledItemsList(source.disabledItems, defaults.disabledItems || []),
quickPhrases: normalizeQuickPhrasesList(source.quickPhrases, defaults.quickPhrases || []),
showMenuIcons: ('showMenuIcons' in source) ? !!source.showMenuIcons : !!defaults.showMenuIcons,
signatureSeparator: (typeof source.signatureSeparator === 'string')
? source.signatureSeparator.trim()
: (typeof defaults.signatureSeparator === 'string' ? defaults.signatureSeparator.trim() : '')
};
}
function areRemoverSettingsEqual(a, b) {
return JSON.stringify(normalizeRemoverSettings(a)) === JSON.stringify(normalizeRemoverSettings(b));
}
function updateStoredSettingsState(settings, skipUserOptionsSync) {
var normalized = normalizeRemoverSettings(settings);
state.settings = clonePlainObject(normalized);
setAlert = normalized.notifyAuthor;
setSubscribe = normalized.subscribeTopic;
signatureSeparator = normalized.signatureSeparator;
state.setAlert = setAlert;
state.setSubscribe = setSubscribe;
if (!skipUserOptionsSync && mw.user && mw.user.options && typeof mw.user.options.set === 'function') {
mw.user.options.set(settingsOptionName, JSON.stringify(normalized));
}
return normalized;
}
function splitSettingsInput(value) {
return String(value || '')
.split(/[\s,;]+/)
.map(function (part) { return part.trim(); })
.filter(Boolean);
}
function splitSettingsListInput(value) {
return String(value || '')
.split(/[\n,;]+/)
.map(function (part) { return part.trim(); })
.filter(Boolean);
}
function parseNamespaceInput(value) {
var tokens = splitSettingsInput(value);
var invalid = [];
var values = [];
tokens.forEach(function (token) {
var parsed = parseInt(token, 10);
if (String(parsed) !== token) invalid.push(token);
else if (values.indexOf(parsed) === -1) values.push(parsed);
});
values.sort(function (a, b) { return a - b; });
return { values: values, invalid: invalid };
}
function parseDisabledItemsInput(value) {
var tokens = splitSettingsListInput(value);
var invalid = [];
var values = [];
tokens.forEach(function (token) {
var normalized = normalizeDisabledItemValue(token);
if (!normalized) invalid.push(token);
else if (values.indexOf(normalized) === -1) values.push(normalized);
});
values.sort(compareSettingsMenuLabels);
return { values: values, invalid: invalid };
}
function formatPagesWithAnd(names, prefix) {
var p = prefix || ':';
var links = (names || []).map(function (n) { return '[[' + p + n + ']]'; });
if (!links.length) return '';
if (links.length === 1) return links[0];
return links.slice(0, -1).join(', ') + ' и ' + links[links.length - 1];
}
function formatCatLink(name) { return '[[:Категория:' + name + ']]'; }
function formatMergeStatus(status) {
return { already_exists: 'уже был', updated: 'дополнен', created: 'создан' }[status] || status;
}
function applyGeneratedText($el, generated) {
var cur = $el.val();
var prev = $el.data('rmGenerated') || '';
if (!prev || cur.indexOf(prev) === 0) {
$el.val(generated + cur.slice(prev.length));
} else {
$el.val(generated + (cur ? '\n' + cur : ''));
}
$el.data('rmGenerated', generated);
}
function getCurrentQuickPhrases() {
return normalizeQuickPhrasesList(
state.settings && state.settings.quickPhrases,
settingsDefaults.quickPhrases || []
);
}
function insertTextIntoTextarea($el, text) {
var el = $el && $el[0];
var value;
var start;
var end;
var updatedValue;
var caretPos;
if (!el) return;
text = String(text || '');
if (!text) return;
value = $el.val() || '';
start = typeof el.selectionStart === 'number' ? el.selectionStart : value.length;
end = typeof el.selectionEnd === 'number' ? el.selectionEnd : start;
updatedValue = value.slice(0, start) + text + value.slice(end);
caretPos = start + text.length;
$el.val(updatedValue).trigger('input').trigger('change').focus();
if (typeof el.setSelectionRange === 'function') el.setSelectionRange(caretPos, caretPos);
}
function buildQuickPhrasesPanelHtml(textareaId) {
var phrases = getCurrentQuickPhrases();
if (!phrases.length) return '';
return joinHtml([
'<div class="rmQuickPhrasesPanel ', RESIZE_CLASS, '" data-rm-target="', textareaId, '">',
phrases.map(function (phrase) {
return joinHtml([
'<button type="button" class="rmQuickPhraseActionBtn" data-rm-target="', textareaId,
'" data-rm-phrase="', escapeHtml(phrase), '">',
escapeHtml(phrase),
'</button>'
]);
}).join(''),
'</div>'
]);
}
function getMultiNominationCommentText(commentsByArticle, articleTitle) {
var key = normTitle(articleTitle);
if (!commentsByArticle || !Object.prototype.hasOwnProperty.call(commentsByArticle, key)) return '';
return normalizeQuickPhraseValue(commentsByArticle[key]);
}
function buildMultiNominationText(articles, bodyText, commentsByArticle, options) {
var opts = options || {};
var list = Array.isArray(articles) ? articles.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasArticleComments = false;
var headingLevel = Math.max(2, parseInt(opts.headingLevel, 10) || 3);
var headingMarks = new Array(headingLevel + 1).join('=');
var articleSections = list.map(function (a) {
var comment = getMultiNominationCommentText(commentsByArticle, a);
if (comment) hasArticleComments = true;
return '\n' + headingMarks + ' [[:' + a + ']] ' + headingMarks + '\n' + (comment ? appendNominationSignature(comment) + '\n' : '');
}).join('');
var commonSectionText = body
? appendNominationSignature(body)
: (hasArticleComments ? '' : appendNominationSignature(''));
return articleSections + '\n' + headingMarks + ' По всем ' + headingMarks + '\n' + commonSectionText;
}
function buildMultiNominationListText(pages, bodyText, commentsByPage) {
var list = Array.isArray(pages) ? pages.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasPageComments = false;
var pageLines = list.map(function (pageName) {
var comment = getMultiNominationCommentText(commentsByPage, pageName);
if (comment) hasPageComments = true;
return '* [[:' + pageName + ']]' + (comment ? '\n*: ' + appendNominationSignature(comment) : '');
}).join('\n');
var commonText = body
? appendNominationSignature(body)
: (hasPageComments ? '' : appendNominationSignature(''));
return pageLines + (pageLines && commonText ? '\n' : '') + commonText;
}
function collectMultiNominationComments(normalizePageName) {
var comments = {};
var normalize = typeof normalizePageName === 'function' ? normalizePageName : normTitle;
$('.rmMultiArticleBlock').each(function () {
var $block = $(this);
var article = normalize(($block.find('.rmArticleInput').val() || '').trim());
var comment = normalizeQuickPhraseValue($block.find('.rmArticleCommentInput').val());
if (!article) return;
comments[article] = comment;
});
return comments;
}
function getNominationPublishText(job) {
if (job && job.isMulti) return String(job.msg || '');
return appendNominationSignature(job && job.msg);
}
function getNominationConflictRule(job) {
if (!job || job.mode !== 'nominate') return null;
if (job.opId === 'tRm' || job.opId === 'mRm') {
return {
id: 'ku',
label: 'КУ',
namePattern: '(?:к\\s*удалению|ку)',
detect: function (articleText) {
var match = String(articleText || '').match(/\{\{\s*((?:к\s*удалению|ку))\s*(?:\||\}\})/i);
if (!match) return null;
var templateName = ucfirst(String(match[1] || '').replace(/\s+/g, ' ').trim());
return {
label: 'КУ',
templateName: templateName || 'КУ',
templateDisplay: '{{' + (templateName || 'КУ') + '}}'
};
}
};
}
return null;
}
function detectNominationConflict(articleText, job) {
var rule = getNominationConflictRule(job);
if (!rule || typeof rule.detect !== 'function') return null;
return rule.detect(articleText);
}
function getConflictDecisionForPage(job, pageName) {
var decisions = job && job.conflictDecisions;
var key = normTitle(pageName);
return decisions && decisions[key] ? decisions[key] : null;
}
function getCategoryMergeRe() {
return new RegExp('\\{\\{\\s*(?:' + cfg.categoryTemplates.merge + ')\\s*\\|\\s*([^\\}]+)\\}\\}', 'i');
}
function eachSequential(targets, iteratee, done) {
var i = 0;
(function next(err) {
if (err || i >= targets.length) { done(err || null); return; }
iteratee(targets[i++], next);
}(null));
}
function normalizeSectionForLink(sectionTitle) {
return (sectionTitle || '').trim()
.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, function (_, target, label) {
var v = (label || target || '').trim();
return v.charAt(0) === ':' ? v.slice(1) : v;
})
.replace(/''+/g, '').replace(/\s+/g, ' ').trim();
}
function getViewportWidth() {
return Math.floor(Math.max(
(document.documentElement && document.documentElement.clientWidth) || 0,
(typeof window.innerWidth === 'number' && window.innerWidth) || 0,
$(window).width() || 0
));
}
function getVisualViewportWidth() {
var widths = [];
if (window.visualViewport && typeof window.visualViewport.width === 'number' && window.visualViewport.width > 0) widths.push(window.visualViewport.width);
if (window.screen && window.screen.width > 0) widths.push(window.screen.width);
return widths.length ? Math.floor(Math.min.apply(Math, widths)) : getViewportWidth();
}
function isTouchModalDevice() {
return !!(
(window.matchMedia && window.matchMedia('(pointer: coarse)').matches) ||
('ontouchstart' in window) ||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 0) ||
(navigator.msMaxTouchPoints && navigator.msMaxTouchPoints > 0)
);
}
function getModalLayout() {
var minWidth = parseInt(sz.taMinW, 10) || 180;
var layoutWidth = getViewportWidth();
var visualWidth = getVisualViewportWidth();
var contentWidth = Math.max(minWidth, Math.floor($('#content').width() || $('#content').innerWidth() || $(window).width() || minWidth));
var isMobile = layoutWidth <= sz.mobileBp;
var isTouchDesktop = !isMobile &&
isTouchModalDevice() &&
visualWidth > 0 &&
visualWidth <= sz.mobileBp &&
layoutWidth >= visualWidth + sz.touchDesktopGap;
var useFullWidth = isMobile || isTouchDesktop;
var maxOuterWidth;
var defaultOuterWidth;
var desktopWidth;
if (isMobile) maxOuterWidth = Math.max(minWidth, (visualWidth || contentWidth) - sz.viewportGap);
else if (isTouchDesktop) maxOuterWidth = Math.max(minWidth, contentWidth - 32);
else maxOuterWidth = Math.max(minWidth, Math.min(contentWidth, (visualWidth ? visualWidth - sz.viewportGap : contentWidth)));
desktopWidth = Math.max(minWidth, Math.floor(contentWidth * sz.modalRatio));
if (useFullWidth || desktopWidth < sz.modalMinWide) defaultOuterWidth = contentWidth;
else if (desktopWidth < sz.modalDefaultWide) defaultOuterWidth = sz.modalDefaultWide;
else defaultOuterWidth = desktopWidth;
return {
minWidth: minWidth,
isMobile: isMobile,
isTouchDesktop: isTouchDesktop,
useFullWidth: useFullWidth,
shouldCenter: useFullWidth || mwCfg.skin === 'minerva',
maxOuterWidth: maxOuterWidth,
defaultOuterWidth: Math.min(defaultOuterWidth, maxOuterWidth)
};
}
function getDefaultResizableWidth(frameWidth) {
var layout = getModalLayout();
return Math.max(layout.minWidth, layout.defaultOuterWidth - Math.floor(frameWidth || 0));
}
function getBoxFrameWidth($el) {
function px(prop) {
var n = parseFloat($el.css(prop));
return isNaN(n) ? 0 : n;
}
return px('padding-left') + px('padding-right') + px('border-left-width') + px('border-right-width');
}
// ═══════════════════════════════════════════════════════════════════════════
// API
// ═══════════════════════════════════════════════════════════════════════════
function getApiUrl() {
return (mw.util && typeof mw.util.wikiScript === 'function') ? mw.util.wikiScript('api') : '/w/api.php';
}
function getCsrfTokenValue() {
return (mw.user && mw.user.tokens && typeof mw.user.tokens.get === 'function')
? mw.user.tokens.get('csrfToken')
: null;
}
function storeCsrfToken(token) {
if (!token || !mw.user || !mw.user.tokens || typeof mw.user.tokens.set !== 'function') return;
mw.user.tokens.set({ csrfToken: token });
}
function isValidCsrfToken(token) {
return typeof token === 'string' && !!token && token !== '+\\';
}
function fetchCsrfToken(forceRefresh, callback) {
var cachedToken = getCsrfTokenValue();
if (!forceRefresh && isValidCsrfToken(cachedToken)) {
callback(cachedToken);
return;
}
$.ajax({
url: getApiUrl(),
method: 'GET',
dataType: 'json',
data: { action: 'query', meta: 'tokens', type: 'csrf', format: 'json' }
})
.done(function (data) {
var token = data && data.query && data.query.tokens && data.query.tokens.csrftoken;
if (isValidCsrfToken(token)) {
storeCsrfToken(token);
callback(token);
return;
}
callback(null);
})
.fail(function () {
callback(null);
});
}
function apiReq(params, mode, callback) {
var isWrite = mode === 'edit' || mode === 'discussiontoolssubscribe' || mode === 'options';
function sendRequest(retryWithFreshToken) {
var reqParams = $.extend({}, params, { format: 'json', action: mode });
if (!isWrite) {
$.ajax({ url: getApiUrl(), method: 'GET', data: reqParams, dataType: 'json' })
.done(function (data) { if (callback) callback(data); })
.fail(function (jqXHR, status) {
console.error('Ошибка API: ' + status);
if (callback) callback({ error: { code: 'network', info: status } });
});
return;
}
fetchCsrfToken(!!retryWithFreshToken, function (token) {
if (!isValidCsrfToken(token)) {
if (callback) callback({ error: { code: 'badtoken', info: 'Не удалось получить CSRF-токен.' } });
return;
}
reqParams.token = token;
$.ajax({ url: getApiUrl(), method: 'POST', data: reqParams, dataType: 'json' })
.done(function (data) {
var err = data && data.error;
var isBadToken = err && (err.code === 'badtoken' || /invalid csrf token/i.test(String(err.info || '')));
if (isBadToken && !retryWithFreshToken) {
sendRequest(true);
return;
}
if (callback) callback(data);
})
.fail(function (jqXHR, status) {
console.error('Ошибка API: ' + status);
if (callback) callback({ error: { code: 'network', info: status } });
});
});
}
sendRequest(false);
}
function saveSettingsToServer(settings, callback) {
var normalized = normalizeRemoverSettings(settings);
if (!mwCfg.wgUserName) {
callback({ code: 'notloggedin', info: 'Сохранять настройки можно только после входа в учётную запись.' });
return;
}
apiReq({ optionname: settingsOptionName, optionvalue: JSON.stringify(normalized) }, 'options', function (resp) {
if (resp && resp.options === 'success') {
callback(null, updateStoredSettingsState(normalized));
return;
}
callback((resp && resp.error) || { code: 'options_failed', info: 'Не удалось сохранить настройки.' });
});
}
function resetSettingsOnServer(callback) {
if (!mwCfg.wgUserName) {
callback({ code: 'notloggedin', info: 'Сбрасывать настройки можно только после входа в учётную запись.' });
return;
}
apiReq({ change: settingsOptionName }, 'options', function (resp) {
if (resp && resp.options === 'success') {
callback(null, updateStoredSettingsState(settingsDefaults, true));
return;
}
callback((resp && resp.error) || { code: 'options_failed', info: 'Не удалось сбросить настройки.' });
});
}
function getFirstQueryPage(data) {
var pages = data && data.query && data.query.pages;
if (!pages) return null;
return pages[Object.keys(pages)[0]] || null;
}
function extractRevisionContent(rev) {
if (!rev) return null;
if (typeof rev['*'] === 'string') return rev['*'];
if (rev.slots && rev.slots.main) {
if (typeof rev.slots.main['*'] === 'string') return rev.slots.main['*'];
if (typeof rev.slots.main.content === 'string') return rev.slots.main.content;
}
return null;
}
function makeReadError(apiError, fallbackCode, fallbackInfo) {
var err = apiError || {};
return {
code: err.code || fallbackCode || 'read_failed',
info: err.info || fallbackInfo || 'Не удалось получить содержимое.'
};
}
function getTextWithTimestamp(pageName, callback) {
apiReq({ prop: 'revisions', rvprop: 'content|timestamp', rvslots: 'main', titles: pageName }, 'query', function (data) {
var content;
var page;
if (data && data.error) {
callback(null, null, data.error);
return;
}
if (!data || !data.query || !data.query.pages) {
callback(null, null, { code: 'read_failed', info: 'Некорректный ответ API при чтении страницы.' });
return;
}
page = getFirstQueryPage(data);
if (!page) {
callback(null, null, { code: 'read_failed', info: 'API не вернул данные страницы.' });
return;
}
if (page.invalid !== undefined) {
callback(null, null, { code: 'invalidtitle', info: page.invalidreason || 'Некорректное название страницы.' });
return;
}
if (page.missing !== undefined) {
callback(null, null, null);
return;
}
if (!page.revisions || !page.revisions.length) {
callback(null, null, { code: 'read_failed', info: 'API не вернул ревизии страницы.' });
return;
}
content = extractRevisionContent(page.revisions[0]);
if (content === null) {
callback(null, null, { code: 'content_missing', info: 'API не вернул текст страницы.' });
return;
}
callback(content, page.revisions[0].timestamp || null, null);
});
}
function getText(pageName, callback) {
getTextWithTimestamp(pageName, function (text, baseTimestamp, err) { callback(text, err); });
}
function editPageContent(pageTitle, options, buildFn, callback) {
var opts = options || {};
var maxRetries = Math.max(0, parseInt(opts.editConflictRetries, 10) || 1);
(function attempt(retry) {
getTextWithTimestamp(pageTitle, function (sourceText, baseTimestamp, readErr) {
if (readErr) {
callback(makeReadError(readErr, opts.readErrorCode || 'read_failed', 'Не удалось получить содержимое страницы «' + pageTitle + '».'));
return;
}
if (sourceText === null) {
callback({ code: opts.readErrorCode || 'read_failed', info: opts.readError || 'Страница «' + pageTitle + '» не существует.' });
return;
}
var done = (function () {
var called = false;
return function (result) {
if (called) return;
called = true;
if (!result || result.error) { callback((result && result.error) || { code: 'build_failed', info: 'Не удалось подготовить правку.' }, result && result.meta || null); return; }
if (result.skip) { callback(null, result.meta || null); return; }
if (typeof result.text !== 'string') { callback({ code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
var ep = { title: pageTitle, text: result.text, summary: result.summary || opts.summary || '' };
if (opts.watchlist) ep.watchlist = opts.watchlist;
if (opts.assertuser) ep.assertuser = opts.assertuser;
if (opts.createonly) ep.createonly = opts.createonly;
if (opts.useBaseTimestamp !== false && baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) {
var err = resp && resp.error ? resp.error : null;
if (err && err.code === 'editconflict' && retry < maxRetries) { attempt(retry + 1); return; }
callback(err, result.meta || null);
});
};
}());
var maybe = buildFn(sourceText, done);
if (maybe !== undefined) done(maybe);
});
}(0));
}
// ═══════════════════════════════════════════════════════════════════════════
// ШАБЛОНЫ: удаление и вставка
// ═══════════════════════════════════════════════════════════════════════════
function findBalancedTemplateEnd(text, start) {
var depth = 0;
var i = start;
var len = text.length;
while (i < len - 1) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') {
depth++;
i += 2;
continue;
}
if (text.charAt(i) === '}' && text.charAt(i + 1) === '}') {
depth--;
i += 2;
if (depth === 0) return i;
continue;
}
i++;
}
return -1;
}
function splitTemplateTopLevelParts(innerText) {
var parts = [];
var start = 0;
var templateDepth = 0;
var linkDepth = 0;
var i = 0;
var text = String(innerText || '');
while (i < text.length) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') {
templateDepth++;
i += 2;
continue;
}
if (text.charAt(i) === '}' && text.charAt(i + 1) === '}' && templateDepth > 0) {
templateDepth--;
i += 2;
continue;
}
if (text.charAt(i) === '[' && text.charAt(i + 1) === '[') {
linkDepth++;
i += 2;
continue;
}
if (text.charAt(i) === ']' && text.charAt(i + 1) === ']' && linkDepth > 0) {
linkDepth--;
i += 2;
continue;
}
if (text.charAt(i) === '|' && templateDepth === 0 && linkDepth === 0) {
parts.push(text.slice(start, i));
start = i + 1;
}
i++;
}
parts.push(text.slice(start));
return parts;
}
function getTemplateMatchAt(text, start, nameRe) {
var end = findBalancedTemplateEnd(text, start);
var parts;
var rawName;
var name;
if (end < 0) return null;
parts = splitTemplateTopLevelParts(text.slice(start + 2, end - 2));
rawName = String(parts.shift() || '').trim();
name = rawName.replace(/^(?:subst|подст)\s*:\s*/i, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
if (!nameRe.test(name)) return null;
return {
start: start,
end: end,
text: text.slice(start, end),
name: rawName,
params: parts.map(function (part) { return part.trim(); })
};
}
function findTemplateByPattern(text, namePattern) {
var source = String(text || '');
var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i');
var i = 0;
var end;
var match;
while (i < source.length - 1) {
if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') {
match = getTemplateMatchAt(source, i, nameRe);
if (match) return match;
end = findBalancedTemplateEnd(source, i);
if (end > i) { i = end; continue; }
}
i++;
}
return null;
}
function getTemplateRemovalRange(text, match) {
var start = match.start;
var end = match.end;
var before = text.slice(0, start).match(/<noinclude>\s*$/i);
var after;
if (before) {
after = text.slice(end).match(/^\s*<\/noinclude>\s*\n?/i);
if (after) {
return { start: before.index, end: end + after[0].length };
}
}
after = text.slice(end).match(/^[ \t]*(?:\r?\n)?/);
if (after) end += after[0].length;
return { start: start, end: end };
}
function stripTemplatesByPattern(text, namePattern) {
var source = String(text || '');
var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i');
var ranges = [];
var out = [];
var pos = 0;
var i = 0;
var end;
var match;
var range;
while (i < source.length - 1) {
if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') {
match = getTemplateMatchAt(source, i, nameRe);
if (match) {
range = getTemplateRemovalRange(source, match);
ranges.push(range);
i = Math.max(match.end, range.end);
continue;
}
end = findBalancedTemplateEnd(source, i);
if (end > i) { i = end; continue; }
}
i++;
}
if (!ranges.length) return { text: source, removed: false };
ranges.forEach(function (r) {
if (r.start < pos) r.start = pos;
out.push(source.slice(pos, r.start));
pos = r.end;
});
out.push(source.slice(pos));
return { text: out.join(''), removed: true };
}
function removeTemplatesByAliases(text, aliases) {
var seen = {}, patterns = [];
aliases.forEach(function (alias) {
var tpl = alias.replace(RE_TEMPLATE_NS, '').trim();
var key = tpl.toLowerCase();
if (!tpl || seen[key]) return;
seen[key] = true;
patterns.push(escapeRegExp(tpl).replace(/\\ /g, '[ _]+'));
});
if (!patterns.length) return { text: text, removed: false };
return stripTemplatesByPattern(text, '(?:' + patterns.join('|') + ')');
}
function removeTransferTemplatesLocal(articleText, transferMode) {
var result = { text: articleText, removedKbu: false, removedKul: false, removedHangon: false };
if (transferMode === 'none') return result;
if (transferMode === 'kbu' || transferMode === 'both') {
var kbu = stripTemplatesByPattern(result.text, '(?:' + KBU_PATTERN_STR + ')');
result.text = kbu.text; result.removedKbu = kbu.removed;
}
if (transferMode === 'kul' || transferMode === 'both') {
var kul = stripTemplatesByPattern(result.text, KUL_PATTERN_STR);
result.text = kul.text; result.removedKul = kul.removed;
}
var hangon = stripTemplatesByPattern(result.text, HANGON_PATTERN_STR);
result.text = hangon.text; result.removedHangon = hangon.removed;
return result;
}
function removeTransferTemplatesWithApiFallback(pageName, articleText, transferMode, localResult, callback) {
var needKbu = (transferMode === 'kbu' || transferMode === 'both') && !localResult.removedKbu;
var needKul = (transferMode === 'kul' || transferMode === 'both') && !localResult.removedKul;
var needHangon = transferMode !== 'none' && !localResult.removedHangon;
if (!needKbu && !needKul && !needHangon) { callback(localResult); return; }
var titleMap = {};
if (needKbu) ['Шаблон:К быстрому удалению','Шаблон:К отсроченному удалению','Шаблон:Deleteslow','Шаблон:Ds'].forEach(function (t) { titleMap[t] = true; });
if (needKul) titleMap['Шаблон:К улучшению'] = true;
if (needHangon) { titleMap['Шаблон:Hangon'] = true; titleMap['Шаблон:Hang-on'] = true; }
apiReq({ prop: 'templates', titles: pageName, tllimit: 'max' }, 'query', function (data) {
var page = getFirstQueryPage(data);
if (page && page.templates) {
page.templates.forEach(function (tpl) {
var norm = normalizeTemplateName(tpl.title);
if ((needKbu && (RE_KBU_PATTERNS.test(norm) || norm === 'к быстрому удалению' || norm === 'к отсроченному удалению' || norm === 'deleteslow' || norm === 'ds')) ||
(needKul && RE_KUL_PATTERN.test(norm)) ||
(needHangon && RE_HANGON.test(norm))) {
titleMap[tpl.title] = true;
}
});
}
var titles = Object.keys(titleMap);
if (!titles.length) { callback(localResult); return; }
function normalizeAliasKey(title) { return (title || '').replace(/_/g, ' ').toLowerCase().trim(); }
function collectAndApplyAliases() {
var allAliases = [];
titles.forEach(function (title) { allAliases = allAliases.concat(tplAliasCache[title] || [title]); });
var dedup = {}, aliases = [];
allAliases.forEach(function (alias) {
var key = normalizeAliasKey(alias);
if (!key || dedup[key]) return;
dedup[key] = true; aliases.push(alias);
});
var updated = $.extend({}, localResult);
var r = removeTemplatesByAliases(updated.text, aliases);
updated.text = r.text;
if (r.removed) {
if (needKbu) updated.removedKbu = true;
if (needKul) updated.removedKul = true;
if (needHangon) updated.removedHangon = true;
}
callback(updated);
}
var missingTitles = titles.filter(function (t) { return !tplAliasCache[t]; });
if (!missingTitles.length) { collectAndApplyAliases(); return; }
(function resolveChunk(offset) {
if (offset >= missingTitles.length) { collectAndApplyAliases(); return; }
var chunk = missingTitles.slice(offset, offset + 20);
var chunkByKey = {};
chunk.forEach(function (t) { chunkByKey[normalizeAliasKey(t)] = t; });
apiReq({ prop: 'redirects', rdlimit: 'max', titles: chunk.join('|') }, 'query', function (resp) {
var pages = resp && resp.query && resp.query.pages ? resp.query.pages : {};
Object.keys(pages).forEach(function (pid) {
var p = pages[pid];
if (!p || !p.title) return;
var sourceTitle = chunkByKey[normalizeAliasKey(p.title)];
if (!sourceTitle) return;
var found = [p.title].concat((p.redirects || []).map(function (r) { return r.title; }));
var seen = {}, unique = [];
found.forEach(function (t) { var k = normalizeAliasKey(t); if (!k || seen[k]) return; seen[k] = true; unique.push(t); });
tplAliasCache[sourceTitle] = unique.length ? unique : [sourceTitle];
});
chunk.forEach(function (t) { if (!tplAliasCache[t]) tplAliasCache[t] = [t]; });
resolveChunk(offset + 20);
});
}(0));
});
}
// ─── Вставка шаблонов ────────────────────────────────────────────────────
function findInsertPositionAfterProjectTemplates(text) {
var pos = 0, len = text.length;
while (pos < len) {
var wsMatch = text.slice(pos).match(/^[\t ]*\n/);
if (wsMatch) { pos += wsMatch[0].length; continue; }
if (text.charAt(pos) !== '{' || text.charAt(pos + 1) !== '{') break;
var afterOpen = text.slice(pos + 2);
var nameMatch = afterOpen.match(/^[\s]*([\s\S]*?)[\s]*(?:\||\}\})/);
var templateEnd;
if (!nameMatch) break;
var tplName = nameMatch[1].toLowerCase().replace(/\s+/g, ' ').trim();
if (!/^статья проекта(\s|$)/.test(tplName) && tplName !== 'блок проектов статьи') break;
templateEnd = findBalancedTemplateEnd(text, pos);
if (templateEnd < 0) break;
pos = templateEnd;
if (pos < len && text.charAt(pos) === '\n') pos++;
}
return pos;
}
function insertTplOnTalkPage(text, tplText, sep) {
var s = (sep === undefined) ? '\n' : sep;
var insertPos = findInsertPositionAfterProjectTemplates(text);
if (insertPos === 0) return tplText + (text.length ? s + text.replace(/^\n+/, '') : '');
var before = text.slice(0, insertPos).replace(/\n+$/, '');
var after = text.slice(insertPos).replace(/^\n+/, '');
return before + '\n' + tplText + (after.length ? s + after : '');
}
function wrapInNoinclude(text, templateText) {
var match = text.match(RE_NOINCLUDE);
if (match) {
// Если перед noinclude есть непробельный контент — вставляем новый noinclude сверху
var before = text.slice(0, text.indexOf(match[0]));
if (/\S/.test(before)) {
return '<noinclude>' + templateText + '</noinclude>\n' + text;
}
var content = match[2];
if (content.length > 0 && content.charAt(content.length - 1) !== '\n') content += '\n';
return text.replace(match[0], match[1] + '<noinclude>' + content + templateText + '\n</noinclude>');
}
return '<noinclude>' + templateText + '</noinclude>\n' + text;
}
function upsertRetTemplateOnTalkPage(text, dateIso, sectionTitle) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var tplRe = /\{\{\s*(?:subst\s*:\s*)?(?:оставлено)\s*([^}]*)\}\}/i;
var tplMatch = source.match(tplRe);
function buildTpl(dateValue, sectionValue) {
var tpl = 'оставлено|' + dateValue;
if (sectionValue) tpl += '|l1=' + sectionValue;
return T_OPEN + tpl + T_CLOSE;
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, buildTpl(dateIso, normalizedSection), '\n'), status: 'created' };
}
var parts = tplMatch[1].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); });
var stdDate = convertToStandardDate(dateIso);
if (existingDates.indexOf(stdDate) !== -1) return { text: source, status: 'already_present' };
var nextIdx = existingDates.length + 1;
var suffix = '|' + dateIso + (normalizedSection ? '|l' + nextIdx + '=' + normalizedSection : '');
return {
text: source.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}\s*$/, suffix + '}}'); }),
status: 'updated'
};
}
function buildConditionalRetTemplateText(dateIso, sectionTitle, reasonText, deadlineText, sectionIndex) {
var normalizedSection = String(sectionTitle || '').trim();
var normalizedReason = normalizeQuickPhraseValue(reasonText);
var normalizedDeadline = String(deadlineText || '').trim();
var index = parseInt(sectionIndex, 10);
var tpl = 'условно оставлено|' + dateIso;
if (isNaN(index) || index < 1) index = 1;
if (normalizedSection) tpl += '|l' + index + '=' + normalizedSection;
if (normalizedReason) tpl += '|пояснение=' + normalizedReason;
if (normalizedDeadline) tpl += '|срок=' + normalizedDeadline;
return T_OPEN + tpl + T_CLOSE;
}
function upsertConditionalRetTemplateOnTalkPage(text, dateIso, sectionTitle, reasonText, deadlineText) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var normalizedReason = normalizeQuickPhraseValue(reasonText);
var normalizedDeadline = String(deadlineText || '').trim();
var tplRe = /\{\{\s*(?:subst\s*:\s*)?(?:условно\s*оставлено)\s*([^}]*)\}\}/i;
var tplMatch = source.match(tplRe);
if (!tplMatch) {
return {
text: insertTplOnTalkPage(source, buildConditionalRetTemplateText(dateIso, normalizedSection, normalizedReason, normalizedDeadline, 1), '\n'),
status: 'created'
};
}
var parts = tplMatch[1].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); });
var stdDate = convertToStandardDate(dateIso);
if (existingDates.indexOf(stdDate) !== -1) return { text: source, status: 'already_present' };
var nextIdx = existingDates.length + 1;
var suffix = '|' + dateIso;
if (normalizedSection) suffix += '|l' + nextIdx + '=' + normalizedSection;
if (normalizedReason) suffix += '|пояснение=' + normalizedReason;
if (normalizedDeadline) suffix += '|срок=' + normalizedDeadline;
return {
text: source.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}\s*$/, suffix + '}}'); }),
status: 'updated'
};
}
// ═══════════════════════════════════════════════════════════════════════════
// ПАЙПЛАЙН НОМИНАЦИИ
// ═══════════════════════════════════════════════════════════════════════════
function runNominationPipeline(steps) {
var s = steps;
var ctx = { templateMeta: null, nominationInfo: null };
var stages = [
{
name: 'шаблон',
fn: function (next) {
s.templateStep(function (err, meta) { ctx.templateMeta = meta || null; next(err); });
}
},
{
name: 'номинация',
pendingText: 'Публикуется номинация...',
successText: 'Номинация опубликована.',
errorText: 'Публикация номинации.',
fn: function (next) {
s.nominationStep(function (err, info) { ctx.nominationInfo = info || null; next(err); });
}
},
{
name: 'подписка',
shouldRun: function () {
var info = ctx.nominationInfo;
return !!(setSubscribe && info && info.pageTitle && info.sectionTitle);
},
fn: function (next) {
subscribeToTopic(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle, function () { next(); });
}
},
{
name: 'оповещение',
shouldRun: function () { return !!(setAlert && !s.skipNotify); },
fn: function (next) { s.notifyStep(ctx.nominationInfo, next); }
}
];
(function run(i) {
if (i >= stages.length) { if (typeof s.onSuccess === 'function') s.onSuccess(ctx); return; }
var stage = stages[i];
if (typeof stage.shouldRun === 'function' && !stage.shouldRun()) { run(i + 1); return; }
var statusId = stage.pendingText
? logStatus(stage.pendingText, null, { pending: true, trackError: false })
: null;
try {
stage.fn(function (err) {
if (err) {
if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), err, { statusId: statusId });
if (typeof s.onFailure === 'function') s.onFailure(stage.name, err, ctx);
else markSubmitError();
return;
}
if (statusId && stage.successText) logStatus(stage.successText, null, { statusId: statusId, trackError: false });
run(i + 1);
});
} catch (ex) {
var exErr = { code: 'exception', info: (ex && ex.message) ? ex.message : String(ex) };
if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), exErr, { statusId: statusId });
if (typeof s.onFailure === 'function') s.onFailure(stage.name, exErr, ctx);
else markSubmitError();
}
}(0));
}
// ─── Публикация номинации ────────────────────────────────────────────────
function publishNomination(opts, callback) {
var cb = callback || function () {};
function doPublish() {
apiReq({
title: opts.pageTitle,
section: 'new',
sectiontitle: opts.sectionTitle,
summary: opts.summary,
text: opts.text,
assertuser: mwCfg.wgUserName
}, 'edit', function (resp) {
cb(resp && resp.error ? resp.error : null);
});
}
if (opts.sectionTitle) {
if (!opts.navTemplate) { doPublish(); return; }
apiReq({ title: opts.pageTitle, createonly: '1', text: T_OPEN + opts.navTemplate + '-Навигация' + T_CLOSE + '\n', summary: makeSummary('автоматическая шапка'), assertuser: mwCfg.wgUserName },
'edit', function (resp) {
if (resp && resp.error && resp.error.code !== 'articleexists') { cb(resp.error); return; }
doPublish();
});
return;
}
// Вставка в существующую страницу
if (opts.createText !== undefined) {
getTextWithTimestamp(opts.pageTitle, function (pageText, baseTimestamp, readErr) {
var result;
var ep;
if (readErr) { cb(makeReadError(readErr, 'read_failed', opts.readErrorMessage || 'Не удалось получить содержимое.')); return; }
result = pageText === null
? (typeof opts.createText === 'function' ? opts.createText() : opts.createText)
: (opts.buildText ? opts.buildText(pageText) : null);
if (typeof result === 'string') result = { text: result };
if (!result || result.error) { cb((result && result.error) || { code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
if (result.skip) { cb(null); return; }
if (typeof result.text !== 'string') { cb({ code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
ep = {
title: opts.pageTitle,
text: result.text,
summary: result.summary || opts.summary,
assertuser: mwCfg.wgUserName
};
if (pageText === null) ep.createonly = true;
else if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) { cb(resp && resp.error ? resp.error : null); });
});
return;
}
editPageContent(opts.pageTitle, { summary: opts.summary, readError: opts.readErrorMessage || 'Не удалось получить содержимое.' },
function (pageText) { return opts.buildText ? opts.buildText(pageText) : null; },
function (err) { cb(err || null); }
);
}
// ─── Оповещение авторов ──────────────────────────────────────────────────
function notifyAuthor(pg, options, callback) {
var opts = options || {};
var cb = callback || function () {};
var actionText = (typeof opts.actionText === 'string') ? opts.actionText : '';
var discussionPage = (typeof opts.discussionPage === 'string') ? opts.discussionPage : '';
var discussionSection = normalizeSectionForLink((typeof opts.discussionSection === 'string') ? opts.discussionSection : '');
var includeProposed = (typeof opts.includeProposedPrefix === 'boolean') ? opts.includeProposedPrefix : true;
var actionPhrase = ((includeProposed ? 'предложена ' : '') + actionText).trim() || 'изменена';
var discussionText = discussionPage ? 'Обсуждение — на странице [[' + discussionPage + (discussionSection ? '#' + discussionSection : '') + ']]. ' : '';
apiReq({ prop: 'revisions', rvprop: 'user', rvdir: 'newer', titles: pg }, 'query', function (queryResp) {
var page = getFirstQueryPage(queryResp);
if (!page) { cb({ code: 'network', info: 'Network error' }); return; }
if (page.missing !== undefined) { cb({ code: 'missing', info: 'Page missing.' }); return; }
if (!page.revisions || !page.revisions.length) { cb({ code: 'no_revisions', info: 'No revisions.' }); return; }
var rv = page.revisions[0];
if ('anon' in rv || rv.userhidden || !rv.user || rv.user === mwCfg.wgUserName) { cb(null); return; }
apiReq({
title: 'User talk:' + rv.user, section: 'new',
sectiontitle: 'Remover: [[:' + pg + ']]',
summary: opts.summary || makeSummary('уведомление автора'),
text: 'Страница [[:' + pg + ']], созданная вами, ' + actionPhrase + '. ' +
discussionText +
'~~' + '~~<br><small>Это автоматическое уведомление, сгенерированное ' + scriptLink + '.</small>',
assertuser: mwCfg.wgUserName
}, 'edit', function (editResp) { cb(editResp && editResp.error ? editResp.error : null); });
});
}
function notifyAuthorsForPages(pages, notifyOptions, callback) {
var cb = callback || function () {};
var opts = notifyOptions || {};
var list = [];
(pages || []).forEach(function (p) { var t = normTitle(p); if (t && list.indexOf(t) === -1) list.push(t); });
if (!list.length) { cb(); return; }
eachSequential(list, function (pg, next) {
var pageLink = buildQuotedStatusPageLink(pg);
var statusId = logStatus('Отправляется уведомление создателю страницы ' + pageLink + '...', null, { pending: true, trackError: false });
notifyAuthor(pg, opts, function (err) {
logStatus(err ? 'Уведомление создателя страницы ' + pageLink + '.' : 'Создатель страницы ' + pageLink + ' уведомлён.', err,
{ statusId: statusId, trackError: opts.trackError !== false });
next();
});
}, cb);
}
// ─── Подписка на раздел ──────────────────────────────────────────────────
function subscribeToTopic(pageTitle, sectionTitle, callback) {
var cb = callback || function () {};
if (!setSubscribe || !sectionTitle) { cb(); return; }
var statusId = logStatus('Оформляется подписка на раздел...', null, { pending: true, trackError: false });
var targetFrag = normalizeSectionForLink(sectionTitle).toLowerCase();
function finish(err, st) {
if (err) { logStatus('Не удалось подписаться на раздел.', err, { statusId: statusId, trackError: false }); cb(); return; }
logStatus(st === 'subscribed' ? 'Оформлена подписка на раздел.' : 'Раздел для подписки не найден.', null, { statusId: statusId, trackError: false });
cb();
}
function trySubscribe(attemptsLeft) {
apiReq({ page: pageTitle, prop: 'threaditemshtml', excludesignatures: true }, 'discussiontoolspageinfo', function (data) {
var items = (data && data.discussiontoolspageinfo && data.discussiontoolspageinfo.threaditemshtml) || null;
if (!items || !items.length) {
if (attemptsLeft > 0) { setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000); return; }
finish(null, 'not_found'); return;
}
var commentname = null;
for (var ti = items.length - 1; ti >= 0; ti--) {
var t = items[ti];
if (t.type === 'heading') {
var htext = (t.headingText || t.html || '').replace(/<[^>]+>/g, '').trim();
if (htext.toLowerCase() === targetFrag || normalizeSectionForLink(htext).replace(/_/g, ' ').toLowerCase() === targetFrag) {
commentname = t.name; break;
}
}
}
if (!commentname) {
if (attemptsLeft > 0) setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000);
else finish(null, 'not_found');
return;
}
apiReq({ page: pageTitle, commentname: commentname, subscribe: '1' }, 'discussiontoolssubscribe', function (res) {
finish(res && res.error ? res.error : null, 'subscribed');
});
});
}
setTimeout(function () { trySubscribe(2); }, 1500);
}
// ═══════════════════════════════════════════════════════════════════════════
// UI: модальные окна
// ═══════════════════════════════════════════════════════════════════════════
function syncModalLayout() {
var syncFn = $('#removerModal').data('rmSyncLayout');
if (typeof syncFn === 'function') syncFn();
}
function clearModalLayoutSyncHandlers() {
modalLayoutSyncHandlers = [];
$('#removerModal').removeData('rmSyncLayout');
}
function registerModalLayoutSync(handler) {
if (typeof handler !== 'function') return;
if (modalLayoutSyncHandlers.indexOf(handler) === -1) modalLayoutSyncHandlers.push(handler);
$('#removerModal').data('rmSyncLayout', function () {
modalLayoutSyncHandlers.slice().forEach(function (fn) {
if (typeof fn === 'function') fn();
});
});
}
function registerResizeObserver(observer) {
if (observer) resizeObservers.push(observer);
}
function resetModalObservers() {
resizeObservers.forEach(function (observer) {
if (observer && typeof observer.disconnect === 'function') observer.disconnect();
});
resizeObservers = [];
clearModalLayoutSyncHandlers();
$(window).off('resize.removerModal');
$(window).off('.rmTaResize');
}
function closeModal() {
resetModalObservers();
$(window).off('keydown.remover');
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
}
function ensureModalStyles() {
if (document.getElementById('removerModalDynamicStyles')) return;
var progH = 'background:' + tk.bgProgH + '!important;border-color:' + tk.bProgH + '!important;color:' + tk.cInv + '!important;';
var neutH = 'background:' + tk.bgN + '!important;border-color:' + tk.bSub + '!important;color:inherit!important;';
var succH = 'background:' + tk.bgSuccH+ '!important;border-color:' + tk.bSuccH + '!important;color:' + tk.cInv + '!important;';
var pillBg = 'linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%)';
var pillShadow = '0 1px 0 rgba(255,255,255,.08) inset,0 1px 2px rgba(0,0,0,.18)';
var activePillShadow = '0 1px 0 rgba(255,255,255,.14) inset,0 2px 6px rgba(51,102,204,.24)';
var css = [
'#removerModal,#removerModal *{-moz-text-size-adjust:none!important;-webkit-text-size-adjust:100%!important;text-size-adjust:100%!important}',
'#removerModal{color:inherit}',
'#removerModal input::placeholder,#removerModal textarea::placeholder{color:var(--color-subtle,currentColor);opacity:.7}',
'#removerModal input[type="checkbox"],#removerModal input[type="radio"]{appearance:auto;-webkit-appearance:auto;-moz-appearance:auto;accent-color:auto}',
'#removerModal input[type="checkbox"]{outline:none!important;box-shadow:none!important}',
'#removerModal button{transition:background-color .12s ease,border-color .12s ease,color .12s ease,box-shadow .12s ease,filter .12s ease,transform .06s ease}',
'#removerModal .rmToggleBtn{background:' + tk.bgNSub + '!important;border-color:' + tk.bSub + '!important;color:inherit!important}',
'#removerModal .rmToggleBtn.is-active{background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important}',
'#removerModal button:not(:disabled):hover{filter:brightness(.97)}',
'#removerModal button:not(:disabled):active{transform:translateY(1px)}',
'#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):hover,#removerModal .rmToggleBtn.is-active:hover{' + progH + 'filter:none}',
'#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):active,#removerModal .rmToggleBtn.is-active:active{' + progH + 'filter:brightness(.92)!important}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError){background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important;outline:none!important;box-shadow:0 0 0 6px rgba(51,102,204,.13),0 1px 2px rgba(0,0,0,.08)!important}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):hover{' + progH + 'box-shadow:0 0 0 7px rgba(51,102,204,.16),0 1px 2px rgba(0,0,0,.1)!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):active{' + progH + 'box-shadow:0 0 0 5px rgba(51,102,204,.14),0 1px 2px rgba(0,0,0,.08)!important;filter:brightness(.92)!important}',
'#removerModal .rmArticleCommentToggle.is-active{background:#bfc4ca!important;border-color:#8f98a3!important;color:#202122!important}',
'#removerModal .rmArticleCommentToggle.is-active:hover{background:#b4bac1!important;border-color:#848e99!important;color:#202122!important;filter:none}',
'#removerModal .rmArticleCommentToggle.is-active:active{background:#a9b0b8!important;border-color:#79838f!important;color:#202122!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitError:not(:disabled):hover{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitError:not(:disabled):active{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:brightness(.88)!important}',
'#removerModal #removerReload:not(:disabled):hover{' + succH + 'filter:none}',
'#removerModal #removerReload:not(:disabled):active{' + succH + 'filter:brightness(.92)!important}',
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):hover,#removerModal .rmToggleBtn:not(.is-active):hover{' + neutH + 'filter:none}',
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):active{' + neutH + 'filter:brightness(.92)!important}',
'#removerModal a.removerModalLink{color:' + tk.cProg + ';text-decoration:none;border-bottom:0;box-shadow:none;word-break:break-word;overflow-wrap:anywhere}',
'#removerModal a.removerModalLink:hover,#removerModal a.removerModalLink:focus{color:' + tk.cProgH + ';text-decoration:underline}',
'#removerModal #removerModalSubtitle{font-size:12px!important;line-height:1.35!important;font-weight:400!important;color:' + tk.cSubM + '!important;max-width:100%;overflow-wrap:anywhere;word-break:break-word}',
'#removerModal #removerModalSubtitle a{font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important}',
'#removerModal a.rmButtonLikeLink{color:' + tk.cBase + '!important;text-decoration:none!important;transform:none!important;transition:background-color .12s ease,border-color .12s ease,color .12s ease,filter .12s ease,transform .06s ease!important}',
'#removerModal a.rmButtonLikeLink:hover{' + neutH + 'text-decoration:none!important;transform:none!important}',
'#removerModal a.rmButtonLikeLink:focus{text-decoration:none!important}',
'#removerModal a.rmButtonLikeLink:focus:not(:focus-visible){outline:none!important}',
'#removerModal a.rmButtonLikeLink:focus-visible{outline:2px solid ' + tk.bProg + '!important;outline-offset:2px;text-decoration:none!important}',
'#removerModal a.rmButtonLikeLink:hover:active{' + neutH + 'filter:brightness(.92)!important;transform:translateY(1px)!important;text-decoration:none!important}',
'#removerModal .rmInfoBox{margin:0 0 10px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgNSub + '}',
'#removerModal .rmActionList{display:flex;flex-direction:column;gap:6px}',
'#removerModal .rmActionItem{display:block;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgBase + ';cursor:pointer;transition:background-color .12s ease,transform .06s ease}',
'#removerModal .rmActionItem:hover{background:' + tk.bgNSub + '}',
'#removerModal .rmActionItem:active{transform:translateY(1px)}',
'#removerModal .rmActionMain{display:flex;align-items:center}',
'#removerModal .rmActionMain input[type="radio"]{margin-right:8px}',
'#removerModal .rmActionMeta{display:block;margin-left:24px;margin-top:2px;color:' + tk.cSubM + ';font-size:12px;line-height:1.35}',
'#removerModal .rmConflictLead{margin:0 0 10px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}',
'#removerModal .rmConflictList{display:flex;flex-direction:column;gap:10px}',
'#removerModal .rmConflictCard{padding:12px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}',
'#removerModal .rmConflictCard.is-skip{background:' + tk.bgNSub + ';border-color:' + tk.bSubS + '}',
'#removerModal .rmConflictTitle{font-size:14px;font-weight:700;line-height:1.4;color:' + tk.cBase + ';word-break:break-word;overflow-wrap:anywhere}',
'#removerModal .rmConflictMeta{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmConflictGroup{margin-top:10px}',
'#removerModal .rmConflictGroupTitle{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmConflictButtons{display:flex;flex-wrap:wrap;gap:6px}',
'#removerModal .rmConflictChoice{padding:5px 10px}',
'#removerModal .rmConflictChoice.is-disabled,#removerModal .rmConflictButtons.is-disabled .rmConflictChoice{opacity:.55;cursor:not-allowed;pointer-events:none}',
'#removerModal .rmConflictHint{margin-top:8px;font-size:11px;line-height:1.45;color:' + tk.cSub + '}',
'#removerModal #rmSettingsForm{display:flex;flex-direction:column;gap:14px}',
'#removerModal .rmSettingsLead{margin:-2px 0 2px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}',
'#removerModal .rmSettingsSection{margin:0;padding:14px 16px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.7) inset}',
'#removerModal .rmSettingsSectionHeader{margin:0 0 12px}',
'#removerModal .rmSettingsSectionTitle{font-size:14px;font-weight:700;line-height:1.35;color:' + tk.cBase + '}',
'#removerModal .rmSettingsSectionDescription{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmSettingsField{display:block;margin:0 0 10px;padding:12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}',
'#removerModal .rmSettingsField:last-child{margin-bottom:0}',
'#removerModal .rmSettingsFieldLabel{display:block;font-size:13px;font-weight:700;line-height:1.35;margin:0 0 8px;color:' + tk.cBase + '}',
'#removerModal .rmSettingsFieldControl{display:block;min-width:0}',
'#removerModal .rmSettingsFieldControl input{margin-bottom:0!important}',
'#removerModal .rmSettingsFieldControl input[type="text"]{min-height:38px;border-radius:6px}',
'#removerModal .rmSettingsFieldHint{margin-top:8px;font-size:12px;line-height:1.55;color:' + tk.cSubM + ';overflow-wrap:anywhere}',
'#removerModal .rmSettingsChecks{display:flex;flex-direction:column;gap:8px}',
'#removerModal .rmSettingsCheck{display:inline-flex;align-items:flex-start;gap:8px;font-size:14px;line-height:1.45;color:' + tk.cBase + '}',
'#removerModal .rmSettingsCheck input[type="checkbox"]{margin:3px 0 0;flex-shrink:0}',
'#removerModal .rmSettingsMenuPresetWrap{margin-top:10px}',
'#removerModal .rmSettingsMenuPresetLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmSegmentedBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmSettingsMenuPresetBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmSegmentedBtn,#removerModal .rmSettingsMenuPresetBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}',
'#removerModal .rmSegmentedBtn.is-active,#removerModal .rmSettingsMenuPresetBtn.is-active{background:' + tk.bgProg + ';border-color:' + tk.bProg + ';color:' + tk.cInv + ';box-shadow:' + activePillShadow + '}',
'#removerModal.rmModalSettings{border:1px solid ' + tk.bSub + '!important;background:' + tk.bgBase + '!important;border-radius:12px!important;box-shadow:0 14px 32px rgba(0,0,0,.08),0 1px 0 rgba(255,255,255,.78) inset!important}',
'#removerModal.rmModalSettings #removerModalHeaderBar{margin-bottom:14px;padding-bottom:10px;border-bottom:2px solid ' + tk.bSub + '}',
'#removerModal.rmModalSettings #removerModalSubtitle{margin:-2px 0 12px!important;color:' + tk.cSubM + '!important}',
'#removerModal.rmModalSettings #rmSettingsForm{gap:18px}',
'#removerModal.rmModalSettings .rmSettingsLead{margin:0;padding:0 2px;color:' + tk.cSubM + '}',
'#removerModal.rmModalSettings .rmSettingsSection{padding:16px 18px;border:1px solid ' + tk.bSub + ';border-radius:12px;background:linear-gradient(180deg,' + tk.bgNSub + ' 0%,' + tk.bgBase + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.82) inset,0 8px 18px rgba(0,0,0,.035)}',
'#removerModal.rmModalSettings .rmSettingsSectionHeader{margin:0 0 6px;padding-bottom:0;border-bottom:0}',
'#removerModal.rmModalSettings .rmSettingsSectionTitle{font-size:16px;line-height:1.3}',
'#removerModal.rmModalSettings .rmSettingsSectionDescription{margin-top:5px;max-width:none}',
'#removerModal.rmModalSettings .rmSettingsField{margin:14px 0 0;padding:12px 0 0;border:0;border-top:1px solid ' + tk.bSubS + ';border-radius:0;background:transparent;box-shadow:none}',
'#removerModal.rmModalSettings .rmSettingsField:first-child{margin-top:0;padding-top:0;border-top:0}',
'#removerModal.rmModalSettings .rmSettingsFieldLabel{margin:0 0 6px}',
'#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]{min-height:40px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}',
'#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]:focus{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.16);outline:none}',
'#removerModal.rmModalSettings .rmSettingsFieldHint{margin-top:6px;max-width:none}',
'#removerModal.rmModalSettings .rmSettingsChecks{gap:10px}',
'#removerModal.rmModalSettings .rmSettingsCheck{padding:4px 0}',
'#removerModal.rmModalSettings .rmSettingsMenuPresetWrap{margin-top:12px;padding-top:10px;border-top:1px dashed ' + tk.bSubS + '}',
'#removerModal.rmModalSettings .rmSettingsHintList{width:100%;max-width:100%;box-sizing:border-box;margin-top:10px;padding:10px 12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + '}',
'#removerModal.rmModalSettings .rmSettingsHintRow{line-height:1.55}',
'#removerModal.rmModalSettings .rmSettingsHintBadge{background:' + tk.bgNSub + '}',
'#removerModal.rmModalSettings .rmQuickPhraseEditor{padding-top:2px}',
'#removerModal.rmModalSettings .rmQuickPhraseMeta{min-height:18px}',
'#removerModal.rmModalSettings #rmSettingsSignaturePreviewCode{display:inline-block;padding:2px 8px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}',
'#removerModal.rmModalSettings #rmFooterActionButtons{position:relative;flex:0 0 auto!important;display:flex!important;align-items:center!important;justify-content:flex-end!important;gap:6px!important;margin-left:auto!important;max-width:100%!important}',
'#removerModal.rmModalSettings #rmSettingsActionButtonsRow{display:flex;align-items:center;justify-content:flex-end;gap:6px;flex-wrap:nowrap}',
'#removerModal.rmModalSettings #rmSettingsUnsavedHint{display:none;position:absolute;top:100%;right:0;width:auto;box-sizing:border-box;margin:4px 0 0;color:' + tk.cSubM + ';opacity:.78;font-size:12px;line-height:1.35;text-align:right;white-space:nowrap}',
'#removerModal .rmProtectControlGroup{margin-top:12px;padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-sizing:border-box}',
'#removerModal .rmProtectControlLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmProtectControlGroup .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}',
'#removerModal .rmProtectControlGroup .rmSegmentedBtn{padding:4px 10px;font-size:12px}',
'#removerModal .rmTransferPanel{margin-top:10px;padding:0;border:0;background:transparent;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmTransferGrid{display:grid;grid-template-columns:max-content max-content;column-gap:10px;row-gap:6px;align-items:start;justify-content:start}',
'#removerModal .rmTransferGrid .rmSegmentedBar{justify-self:start}',
'#removerModal .rmTransferHintRow{grid-column:1 / -1;min-height:0}',
'#removerModal .rmTransferPanel .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}',
'#removerModal .rmTransferPanel .rmSegmentedBtn{padding:6px 14px;font-size:12px;font-weight:700}',
'#removerModal #rmTransferModeGroup{gap:0}',
'#removerModal #rmTransferModeGroup .rmSegmentedBtn:first-child{border-top-right-radius:0;border-bottom-right-radius:0}',
'#removerModal #rmTransferModeGroup .rmSegmentedBtn + .rmSegmentedBtn{margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}',
'#removerModal.rmCompactContent .rmArticleRow{flex-wrap:wrap!important;gap:6px}',
'#removerModal.rmCompactContent .rmArticleRow .rmArticleInput{flex:1 1 100%!important;width:100%!important}',
'#removerModal.rmCompactContent .rmArticleRow .rmArticleCommentToggle,#removerModal.rmCompactContent .rmArticleRow .rmAddArticle,#removerModal.rmCompactContent .rmArticleRow .rmRemoveInput{margin-left:0!important}',
'#removerModal.rmCompactContent .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(1){order:1}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(2){order:2}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(3){order:3}',
'#removerModal.rmCompactContent #rmTransferModeGroup{flex-direction:column;align-items:flex-start;gap:6px}',
'#removerModal.rmCompactContent #rmTransferModeGroup .rmSegmentedBtn{margin-left:0!important;border-radius:999px!important}',
'#removerModal.rmCompactContent .rmTransferHintRow{grid-column:auto}',
'#removerModal #rmProtectTextBlock{margin-top:14px}',
'#removerModal #rmSettingsMenuTitle:disabled{background:' + tk.bgDis + '!important;border-color:' + tk.bDis + '!important;color:' + tk.cDis + '!important;-webkit-text-fill-color:' + tk.cDis + ';opacity:1;cursor:not-allowed;box-shadow:none!important}',
'#removerModal .rmSettingsHintList{display:flex;flex-direction:column;gap:4px;margin-top:8px}',
'#removerModal .rmSettingsHintRow{font-size:12px;line-height:1.5;color:' + tk.cSubM + '}',
'#removerModal .rmSettingsHintBadge{display:inline-block;margin-right:6px;padding:1px 6px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';font-size:11px;font-weight:700;color:' + tk.cSubM + ';vertical-align:baseline}',
'#removerModal .rmQuickPhraseEditor{display:flex;flex-direction:column;gap:10px}',
'#removerModal .rmQuickPhraseList,#removerModal .rmQuickPhrasesPanel{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-start}',
'#removerModal .rmQuickPhraseChip{position:relative;display:inline-flex;align-items:center;gap:4px;max-width:100%;padding:2px 4px 2px 10px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';box-shadow:' + pillShadow + ';transition:border-color .12s,box-shadow .12s,opacity .12s;overflow:visible}',
'#removerModal .rmQuickPhraseChip.is-editing{opacity:.42;border-style:dashed}',
'#removerModal .rmQuickPhraseChip.is-dragging{opacity:.65}',
'#removerModal .rmQuickPhraseChip.is-drop-before::before,#removerModal .rmQuickPhraseChip.is-drop-after::after{content:"";position:absolute;top:50%;width:3px;height:24px;border-radius:999px;background:' + tk.cProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.08)}',
'#removerModal .rmQuickPhraseChip.is-drop-before::before{left:-4px;transform:translate(-50%,-50%)}',
'#removerModal .rmQuickPhraseChip.is-drop-after::after{right:-4px;transform:translate(50%,-50%)}',
'#removerModal .rmQuickPhraseEditBtn{max-width:100%;padding:3px 0;border:0;background:transparent;color:' + tk.cBase + ';font-size:13px;line-height:1.35;cursor:pointer;text-align:left;white-space:normal}',
'#removerModal .rmQuickPhraseRemoveBtn{width:24px;height:24px;padding:0;border:0;border-radius:999px;background:transparent;color:' + tk.cSubM + ';font-size:18px;line-height:1;cursor:pointer;flex-shrink:0}',
'#removerModal .rmQuickPhraseRemoveBtn:hover{background:' + tk.bgN + ';color:' + tk.cBase + '}',
'#removerModal #rmSettingsQuickPhraseInput.is-editing{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.12)}',
'#removerModal .rmQuickPhraseMeta{font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmQuickPhraseEmpty{padding:2px 0;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmQuickPhrasesPanel{margin-top:8px}',
'#removerModal .rmQuickPhraseActionBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}',
'#removerModal .rmQuickPhraseActionBtn:hover{border-color:' + tk.bProg + ';color:' + tk.cProg + '}',
'@media (max-width:' + sz.mobileBp + 'px){',
'#removerModal button{white-space:normal!important}',
'#removerModal #rmFooterButtons{align-items:flex-start!important}',
'#removerModal #rmFooterCheckboxes,#removerModal #rmFooterActionButtons{width:100%!important;max-width:100%!important;margin-left:0!important}',
'#removerModal .rmSettingsSection{padding:12px 13px}',
'#removerModal .rmSettingsField{padding:10px}',
'#removerModal.rmModalSettings #rmSettingsUnsavedHint{max-width:100%;margin:4px 0 0;text-align:right;white-space:normal}',
'#removerModal .rmTransferPanel{padding:0}',
'#removerModal .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}',
'#removerModal .rmTransferHintRow{grid-column:auto}',
'#removerModal .rmQuickPhraseChip{max-width:100%}',
'}'
].join('');
var style = document.createElement('style');
style.id = 'removerModalDynamicStyles';
style.textContent = css;
document.head.appendChild(style);
}
function applyV2022Layout($modal, explicitWidth) {
if (!isVector22) return;
var css = { 'max-width': '100%', 'box-sizing': 'border-box', 'overflow-wrap': 'anywhere' };
if (typeof explicitWidth === 'number') css['max-width'] = explicitWidth + 'px';
$modal.css(css);
}
function getPageUrl(pageTitle) {
return (mw.util && typeof mw.util.getUrl === 'function')
? mw.util.getUrl(pageTitle)
: '/wiki/' + encodeURIComponent((pageTitle || '').replace(/ /g, '_'));
}
function getPageUrlWithFragment(pageTitle, fragment) {
var url = getPageUrl(pageTitle);
var frag = normalizeSectionForLink(fragment);
if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_'));
return url;
}
function buildStatusPageLink(pageName) {
var title = normTitle(pageName);
return '<a href="' + escapeHtml(getPageUrl(title)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(title) + '</a>';
}
function buildQuotedStatusPageLink(pageName) {
return '«' + buildStatusPageLink(pageName) + '»';
}
function buildHeaderIconButtonHtml(id, title, label, text) {
return joinHtml([
'<button id="', id, '" type="button" title="', escapeHtml(title), '" aria-label="', escapeHtml(label || title), '" ',
'style="', stHeaderIconBtn, '">', text || '', '</button>'
]);
}
function createModal(opts) {
if (typeof opts === 'string') opts = { title: opts };
var layout = getModalLayout();
resetModalObservers();
ensureModalStyles();
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
var subtitleHtml = '';
var subtitleStyle = 'margin:-4px 0 8px;font-size:12px!important;color:' + tk.cSubM + ';line-height:1.35!important;font-weight:400!important;';
var subtitleLinkStyle = 'font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important;';
if (opts.subtitleHtml) {
subtitleHtml = joinHtml([
'<div id="removerModalSubtitle" style="', subtitleStyle, '">',
opts.subtitleHtml,
'</div>'
]);
} else if (opts.subtitlePage) {
subtitleHtml = joinHtml([
'<div id="removerModalSubtitle" style="', subtitleStyle, '">',
opts.subtitleLabel || 'Текущий день',
': <a href="', getPageUrl(opts.subtitlePage), '" target="_blank" rel="noopener noreferrer" class="removerModalLink" style="', subtitleLinkStyle, '">',
normTitle(opts.subtitlePage),
'</a></div>'
]);
}
var settingsButtonHtml = opts.showSettingsButton === false ? '' :
buildHeaderIconButtonHtml('removerSettingsTrigger', 'Конфигурация', 'Конфигурация', '⚙');
var display = opts.inline ? 'inline-block' : 'block';
var modalMargin = opts.inline ? '1em 0' : (layout.shouldCenter ? '1em auto' : '1em 0');
var inlineLayoutStyle = opts.inline ? ';justify-self:start;align-self:start;width:fit-content;' : '';
var modalStyle = joinHtml([
'position:relative;padding:1.5em;margin:', modalMargin, ';display:', display, ';',
'border:', stStyles.border, ';background:', stStyles.background,
';border-radius:', stStyles.borderRadius, ';box-shadow:', stStyles.boxShadow,
';max-width:100%;box-sizing:border-box;overflow-wrap:anywhere;', inlineLayoutStyle
]);
var headerStyle = 'display:flex;align-items:center;gap:10px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid ' + tk.bSubS + ';';
var titleStyle = 'color:' + stStyles.headerColor + ';margin:0;padding:0;border:0;display:block;font-size:1.3em;font-weight:400;line-height:1.25;flex:1 1 auto;min-width:0;';
$('#content').prepend(joinHtml([
'<div id="removerModal" style="', modalStyle, '">',
'<div id="removerModalHeaderBar" style="', headerStyle, '">',
'<h1 id="removerModalTitle" style="', titleStyle, '"><span id="removerModalTitleText">', opts.title, '</span></h1>',
settingsButtonHtml,
'</div>',
subtitleHtml,
'<div id="removerModalContent"></div>',
'<div id="removerModalFooter" style="margin-top:15px;"></div>',
'</div>'
]));
var $modal = $('#removerModal');
if (opts.width === 'compact') $modal.css({ width: layout.defaultOuterWidth + 'px', 'max-width': '100%', 'box-sizing': 'border-box' });
else applyV2022Layout($modal);
$('#removerSettingsTrigger').off('click').on('click', function () {
openSettings();
});
}
function buildFooterCheckboxHtml(name, checked, label) {
return joinHtml([
'<label style="', stFooterCheckLabel, '">',
'<input name="', name, '" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ',
checked ? 'checked' : '',
'>',
label,
'</label>'
]);
}
function buildFooterActionsHtml(buttonsHtml) {
return '<div id="rmFooterActionButtons" style="' + stFooterActions + '">' + buttonsHtml + '</div>';
}
function renderModalFooter(mode, options) {
var opts = options || {};
$('#removerModalFooter').css('width', '');
if (mode === 'submit') {
var showCb = opts.showCheckbox !== false;
var showSub = opts.showSubscribe === true;
var ns = mwCfg.wgNamespaceNumber;
var notifyLabel = ns === 0 ? 'Оповестить создателя статьи'
: (ns === 10 || ns === 11) ? 'Оповестить создателя шаблона'
: (ns === 14 || ns === 15) ? 'Оповестить создателя категории'
: 'Оповестить создателя страницы';
var cbInlineHtml = '';
if (showSub || showCb) {
cbInlineHtml = joinHtml([
'<div id="rmFooterCheckboxes" style="', stFooterChecks, '">',
showSub ? buildFooterCheckboxHtml('rmSubscribe', setSubscribe, 'Подписаться на номинацию') : '',
showCb ? buildFooterCheckboxHtml('rmUAlert', setAlert, notifyLabel) : '',
'</div>'
]);
}
$('#removerModalFooter').html(joinHtml([
'<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:', cbInlineHtml ? 'space-between' : 'flex-end', ';">',
cbInlineHtml,
buildFooterActionsHtml(joinHtml([
'<button id="removerCancel" style="', stCancel, '">Отмена</button>',
'<button id="removerSubmit" style="', stSubmit, '">', opts.submitText || 'ОК', '</button>'
])),
'</div>'
]));
$('#removerCancel').click(function () { closeModal(); });
$('#removerSubmit').data('rmSubmitInProgress', false).click(function () {
if ($(this).data('rmSubmitInProgress')) return;
$(this).removeClass('rmSubmitError').css({ background: '', 'border-color': '', color: '' });
isError = false;
if (!opts.preserveLogOnSubmit) {
$('#rmLogBox').empty();
logStatusSeq = 0;
}
if (showCb) { setAlert = $('[name="rmUAlert"]').is(':checked'); state.setAlert = setAlert; }
if (showSub) { setSubscribe = $('[name="rmSubscribe"]').is(':checked'); state.setSubscribe = setSubscribe; }
$(this).data('rmSubmitInProgress', true).prop('disabled', true);
var submitResult;
try { submitResult = opts.onSubmit(); } catch (ex) { unlockModalSubmit(); throw ex; }
if (submitResult === false) unlockModalSubmit();
});
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.ctrlKey && e.keyCode === 13) $('#removerSubmit').click();
});
} else if (mode === 'reload') {
var newBtns = buildFooterActionsHtml(joinHtml([
'<button id="removerCancel" style="', stCancel, '">Закрыть</button>',
'<button id="removerReload" style="', stReload, '">', opts.reloadText || 'Обновить страницу', '</button>'
]));
$('#rmFooterCheckboxes').remove();
var $btns = $('#rmFooterButtons');
if ($btns.length) {
$btns.css({ 'justify-content': 'flex-end' }).html(newBtns);
} else {
$('#removerModalFooter').append(joinHtml([
'<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:flex-end;">',
newBtns,
'</div>'
]));
}
$('#removerCancel').click(function () { closeModal(); });
$('#removerReload').click(function () { location.reload(); });
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.keyCode === 27) $('#removerCancel').click();
if (e.ctrlKey && e.keyCode === 13) $('#removerReload').click();
});
} else { // 'close'
$('#removerModalFooter').html(joinHtml([
'<div style="display:flex;justify-content:flex-end;align-items:center;">',
'<button id="removerCancel" style="', stCancel, 'margin-right:0;">', opts.closeText || 'Закрыть', '</button>',
'</div>'
]));
$('#removerCancel').click(function () { closeModal(); });
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.keyCode === 27) $('#removerCancel').click();
});
}
}
function unlockModalSubmit() {
$('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false);
}
function markSubmitError() {
isError = true;
var errColor = '#d73333';
$('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false)
.addClass('rmSubmitError').css({ background: errColor, 'border-color': errColor, color: '#fff' });
}
// ─── UI: статус и ссылки ─────────────────────────────────────────────────
function startProcessing() {
if ($('#rmLogBox').length) return;
$('#removerModal').append(
'<div id="rmLogBox" style="margin-top:12px;padding-top:10px;border-top:1px solid ' + tk.bSubS + ';line-height:1.5;overflow-wrap:anywhere;word-break:break-word;box-sizing:border-box;"></div>'
);
syncLinkWidths();
}
function logStatus(message, error, opts) {
var o = opts || {};
if (o.trackError !== false && error && error.code) isError = true;
var $box = $('#rmLogBox');
if (!$box.length) { startProcessing(); $box = $('#rmLogBox'); }
var statusId = o.statusId || ('rm-status-' + (++logStatusSeq));
var $row = $box.find('[data-rm-status-id="' + statusId + '"]');
if (!$row.length) {
$row = $('<div data-rm-status-id="' + statusId + '" style="margin-top:4px;line-height:1.4;"></div>');
$box.append($row);
}
var html;
if (error) {
var errText = error.code
? '<span class="error"><small>' + escapeHtml(formatLogErrorCode(error.code)) + ': ' + escapeHtml(String(error.info || '')) + '</small></span>'
: escapeHtml(String(error));
html = '<span style="color:' + tk.cDang + ';margin-right:4px;">✕</span>' + message + ' — ' + errText;
} else if (o.pending) {
html = '<span style="color:' + tk.cSubM + ';">' + message + '</span>';
} else {
html = '<span style="color:' + tk.bgSucc + ';margin-right:4px;">✓</span><span style="color:' + tk.cSubM + ';">' + message + '</span>';
}
$row.html(html);
return statusId;
}
function formatLogErrorCode(code) {
var value = String(code || '');
return value.toLowerCase() === 'error' ? 'Ошибка' : value;
}
function logPageEdit(pageName, error, opts) {
logStatus('Правка страницы ' + buildQuotedStatusPageLink(pageName) + '.', error, opts);
}
function syncLinkWidths() {
var $box = $('#rmLogBox');
if (!$box.length) return;
var taW = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0;
$box.css({ width: taW ? taW + 'px' : '', 'max-width': '100%' });
}
function appendNominationLink(pageTitle, sectionTitle) {
if (!pageTitle) return;
var url = getPageUrl(pageTitle);
var frag = normalizeSectionForLink(sectionTitle);
if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_'));
var label = normTitle(frag ? pageTitle + '#' + frag : pageTitle);
var $target = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent');
$target.append(
'<div style="margin-top:4px;line-height:1.4;word-break:break-word;overflow-wrap:anywhere;display:flex;align-items:baseline;gap:5px;">' +
'<span style="color:' + tk.bgSucc + ';font-size:14px;flex-shrink:0;">✓</span>' +
'<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a></div>'
);
}
// ─── UI-строители ────────────────────────────────────────────────────────
function buildInfoBoxHtml(mainText, detailsText, isErr) {
var cls = isErr ? ' class="error"' : '';
return joinHtml([
'<div class="rmInfoBox">',
'<p', cls, ' style="margin:0', detailsText ? ' 0 6px' : '', ';">', mainText, '</p>',
detailsText ? '<p style="margin:0;color:' + tk.cSubM + ';">' + detailsText + '</p>' : '',
'</div>'
]);
}
function buildActionsHtml(actions, inputName, listId) {
var actionItemsHtml = actions.map(function (a, i) {
var meta = a.description || (a.talkNotice ? 'С добавлением {{' + (a.talkTemplate || a.resultTemplate || 'шаблон') + '}} на СО.' : '');
var tagHtml = a.tag
? '<span style="display:inline-block;font-size:13px;font-weight:600;padding:2px 7px;border-radius:3px;background:' + tk.bgN + ';color:' + tk.cSubM + ';margin-right:8px;white-space:nowrap;vertical-align:middle;">' + escapeHtml(a.tag) + '</span>'
: '';
return joinHtml([
'<label class="rmActionItem">',
'<span class="rmActionMain">',
'<input type="radio" name="', inputName, '" value="', a.id, '" ', i === 0 ? 'checked' : '', '>',
tagHtml,
'<span>', a.label, '</span>',
'</span>',
meta ? '<span class="rmActionMeta">' + meta + '</span>' : '',
'</label>'
]);
}).join('');
return joinHtml([
'<div style="margin:0 0 8px;color:', tk.cSubM, ';font-size:13px;">Обнаружены открытые номинации:</div>',
'<div', listId ? ' id="' + listId + '"' : '', ' class="rmActionList">',
actionItemsHtml,
'</div>'
]);
}
function buildNestedCommentFieldsHtml(opts) {
var options = opts || {};
var wrapId = options.wrapId || '';
var textareaId = options.textareaId || '';
var textareaClass = options.textareaClass ? ' ' + options.textareaClass : '';
var textareaStyleExtra = options.textareaStyleExtra || '';
var wrapStyleExtra = options.wrapStyleExtra || '';
var placeholder = options.placeholder || 'Комментарий (необязательно)';
var beforeHtml = options.beforeHtml || '';
var marginTop = options.marginTop || '6px';
var minHeight = parseInt(options.minHeight, 10) || 90;
var isEmbedded = !!options.embedded;
var wrapClass = isEmbedded ? '' : (' class="' + RESIZE_CLASS + '"');
var wrapStyle = 'display:none;margin-top:' + marginTop + ';max-width:100%;box-sizing:border-box;';
if (isEmbedded) {
wrapStyle += 'padding:0;border:0;background:transparent;';
} else {
wrapStyle += 'padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:6px;background:' + tk.bgNSub + ';';
}
wrapStyle += wrapStyleExtra;
return joinHtml([
'<div id="', wrapId, '"', wrapClass, ' style="', wrapStyle, '">',
beforeHtml,
'<textarea id="', textareaId, '" class="rmNestedCommentInput', textareaClass,
'" placeholder="', escapeHtml(placeholder), '" style="', stInputFull,
'min-height:', minHeight, 'px;resize:both;margin-bottom:6px;', textareaStyleExtra, '"></textarea>',
buildQuickPhrasesPanelHtml(textareaId),
'</div>'
]);
}
function buildConditionalRetFieldsHtml() {
return buildNestedCommentFieldsHtml({
wrapId: 'rmCloseConditionalWrap',
textareaId: 'rmCloseConditionalReason',
placeholder: 'Условие / пояснение (необязательно)',
marginTop: '8px',
minHeight: 90,
beforeHtml: '<input id="rmCloseConditionalDeadline" type="text" placeholder="Срок доработки: 2026-05-31" style="' + stInputFull + 'margin-bottom:6px;">'
});
}
function buildAddArticleButtonHtml(options) {
var opts = options || {};
var title = opts.addTitle || 'Добавить статью';
return '<button type="button" class="rmAddArticle" title="' + escapeHtml(title) + '" aria-label="' + escapeHtml(title) + '" style="' + stRemoveBtn + '">+</button>';
}
function buildSquareAddButtonHtml(id, title, className) {
var idAttr = id ? ' id="' + id + '"' : '';
var clsAttr = className ? ' class="' + className + '"' : '';
var label = title || 'Добавить';
return '<button' + idAttr + ' type="button"' + clsAttr + ' title="' + escapeHtml(label) + '" aria-label="' + escapeHtml(label) + '" style="' + stRemoveBtn + '">+</button>';
}
function buildMultiArticleButtonsHtml(commentWrapId, commentId, options) {
var opts = options || {};
var commentBtnStyle = stToolBtn + (opts.showComment ? '' : 'display:none;');
if (opts.showAdd) return buildAddArticleButtonHtml(opts);
return joinHtml([
'<button type="button" class="rmToggleBtn rmArticleCommentToggle" data-rm-comment-wrap="', commentWrapId,
'" data-rm-comment-textarea="', commentId, '" aria-expanded="false" style="', commentBtnStyle, '">Комментарий</button>',
'<button type="button" class="rmRemoveInput" style="', stRemoveBtn, '" title="', escapeHtml(opts.removeTitle || 'Удалить статью'), '">−</button>'
]);
}
function buildMultiArticleRowHtml(index, options) {
var opts = options || {};
var articleId = 'rmArticle' + index;
var commentWrapId = 'rmArticleCommentWrap' + index;
var commentId = 'rmArticleComment' + index;
var articleValue = opts.articleValue ? ' value="' + escapeHtml(opts.articleValue) + '"' : '';
var inputPlaceholder = opts.inputPlaceholder || 'Статья';
var commentPlaceholder = opts.commentPlaceholder || 'Комментарий только для этой статьи (необязательно)';
var articleRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
var blockStyle = 'max-width:100%;box-sizing:border-box;';
var buttonsHtml = buildMultiArticleButtonsHtml(commentWrapId, commentId, {
showAdd: !!opts.showAdd,
showComment: !!opts.showComment,
addTitle: opts.addTitle,
removeTitle: opts.removeTitle
});
return joinHtml([
'<div class="rmMultiArticleBlock ', RESIZE_CLASS, '" style="', blockStyle, '">',
'<div', opts.rowId ? ' id="' + opts.rowId + '"' : '', ' class="rmArticleRow" style="', articleRowStyle, '">',
'<input id="', articleId, '" type="text" placeholder="', escapeHtml(inputPlaceholder), '" class="rmArticleInput" style="', stInputBox, '"', articleValue, '>',
buttonsHtml,
'</div>',
buildNestedCommentFieldsHtml({
wrapId: commentWrapId,
textareaId: commentId,
textareaClass: 'rmArticleCommentInput',
placeholder: commentPlaceholder,
marginTop: '4px',
minHeight: 90,
embedded: true,
wrapStyleExtra: 'padding:0 0 0 12px;background:transparent;',
textareaStyleExtra: 'border-color:' + tk.bSubS + ';border-radius:4px;background:' + tk.bgBase + ';'
}),
'</div>'
]);
}
function showInfoAndClose(mainText, detailsText, isErr) {
$('#removerModalContent').html(buildInfoBoxHtml(mainText, detailsText || '', isErr || false));
renderModalFooter('close');
}
function getSelectedAction(inputName, actionMap) {
var id = $('[name="' + inputName + '"]:checked').val();
var sel = actionMap[id];
if (!sel) alert('Выберите действие.');
return sel || null;
}
function prependTemplateToNoinclude(text, templateText) {
var source = String(text || '');
var tpl = String(templateText || '').trim();
if (!tpl) return source;
var match = source.match(RE_NOINCLUDE);
if (match) {
var before = source.slice(0, source.indexOf(match[0]));
if (/\S/.test(before)) return '<noinclude>' + tpl + '</noinclude>\n' + source;
var content = String(match[2] || '').replace(/^\n+/, '');
return source.replace(match[0], match[1] + '<noinclude>' + tpl + (content ? '\n' + content : '') + '\n</noinclude>');
}
return '<noinclude>' + tpl + '</noinclude>\n' + source;
}
function buildGeneratedNominationTemplateText(job, pg) {
var tplStr = '';
if (!job) return '';
if (job.opId === 'fRm') {
tplStr = job.kbuTemplate || '';
if (job.kbuAddInfo) tplStr += '|1=' + job.kbuAddInfo;
if (job.kbuComment) tplStr += '|' + (job.kbuAddInfo ? '2' : '1') + '=' + job.kbuComment;
return tplStr ? (T_OPEN + tplStr + T_CLOSE) : '';
}
if (typeof job.articleTpl !== 'function') return '';
tplStr = job.articleTpl(job.tplpar, job.date[0]);
if (job.opId === 'merge' && job.tplpar) {
tplStr = (job.op.nomination.articleTpl)(
('|' + job.tplpar + '|').replace('|' + pg + '|', '|').slice(1, -1),
job.date[0]
);
}
return tplStr ? (T_OPEN + tplStr + T_CLOSE) : '';
}
function applyConflictTemplateResolution(articleText, job, pg, decision) {
var rule = getNominationConflictRule(job);
var generatedTemplate = buildGeneratedNominationTemplateText(job, pg);
var source = String(articleText || '');
if (!generatedTemplate || !decision) return source;
if (decision.templateAction === 'overwrite') {
var cleaned = rule ? stripTemplatesByPattern(source, rule.namePattern).text : source;
return prependTemplateToNoinclude(cleaned, generatedTemplate);
}
if (decision.templateAction === 'prepend') return prependTemplateToNoinclude(source, generatedTemplate);
return source;
}
function inspectMultiNominationConflicts(job, callback) {
var cb = callback || function () {};
var pages = (job && job.multiArticles) ? job.multiArticles.slice() : [];
var conflicts = [];
var statusId = logStatus('Проверяются статьи на наличие уже установленных шаблонов...', null, { pending: true, trackError: false });
if (!pages.length) {
logStatus('Проверка завершена: конфликтов не найдено.', null, { statusId: statusId, trackError: false });
cb(null, conflicts);
return;
}
eachSequential(pages, function (pg, next) {
getText(pg, function (articleText, readErr) {
var conflict;
if (readErr) { next(makeReadError(readErr, 'read_failed', 'Не удалось проверить страницу «' + pg + '».')); return; }
if (articleText === null) { next({ code: 'read_failed', info: 'Страница «' + pg + '» не существует.' }); return; }
conflict = detectNominationConflict(articleText, job);
if (conflict) {
conflicts.push($.extend({ pageName: pg }, conflict));
logStatus('В статье ' + buildQuotedStatusPageLink(pg) + ' обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.', null, { trackError: false });
}
next();
});
}, function (err) {
if (err) {
logStatus('Проверка статей.', err, { statusId: statusId });
cb(err);
return;
}
logStatus(
conflicts.length
? 'Проверка завершена: найдены статьи с уже установленными шаблонами.'
: 'Проверка завершена: конфликтов не найдено.',
null,
{ statusId: statusId, trackError: false }
);
cb(null, conflicts);
});
}
function buildNominationConflictResolutionHtml(conflicts) {
return '<div class="rmInfoBox"><p style="margin:0 0 6px;">Найдены статьи, где шаблон уже стоит. Для каждой конфликтующей страницы выберите, что делать со статьёй и с шаблоном.</p>' +
'<p style="margin:0;color:' + tk.cSubM + ';font-size:12px;line-height:1.45;">По умолчанию такие статьи исключаются из новой номинации, а существующий шаблон остаётся без изменений.</p></div>' +
'<div class="rmConflictLead">После выбора нажмите «Продолжить номинирование».</div>' +
'<div id="rmConflictList" class="rmConflictList">' +
conflicts.map(function (conflict, index) {
var pageLink = buildStatusPageLink(conflict.pageName);
return '<div class="rmConflictCard" data-rm-conflict-index="' + index + '">' +
'<input type="hidden" class="rmConflictPageAction" value="skip">' +
'<input type="hidden" class="rmConflictTemplateAction" value="keep">' +
'<div class="rmConflictTitle">' + pageLink + '</div>' +
'<div class="rmConflictMeta">Обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.</div>' +
'<div class="rmConflictGroup">' +
'<div class="rmConflictGroupTitle">Действие со статьёй</div>' +
'<div class="rmConflictButtons" data-rm-conflict-group="page">' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="page" data-rm-choice="skip" aria-pressed="true">Убрать из номинации</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="page" data-rm-choice="keep" aria-pressed="false">Оставить в номинации</button>' +
'</div></div>' +
'<div class="rmConflictGroup">' +
'<div class="rmConflictGroupTitle">Действие с шаблоном</div>' +
'<div class="rmConflictButtons" data-rm-conflict-group="template">' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="template" data-rm-choice="keep" aria-pressed="true">Оставить как есть</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="overwrite" aria-pressed="false">Новая дата</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="prepend" aria-pressed="false">Второй сверху</button>' +
'</div>' +
'<div class="rmConflictHint">Если статья исключается из номинации, действие с шаблоном не применяется.</div>' +
'</div></div>';
}).join('') +
'</div>';
}
function updateNominationConflictCardState($card) {
var pageAction = $card.find('.rmConflictPageAction').val() || 'skip';
var disableTemplate = pageAction !== 'keep';
var $templateButtons = $card.find('[data-rm-choice-type="template"]');
$card.toggleClass('is-skip', disableTemplate);
$card.find('[data-rm-conflict-group="template"]').toggleClass('is-disabled', disableTemplate);
$templateButtons.prop('disabled', disableTemplate).toggleClass('is-disabled', disableTemplate);
}
function bindNominationConflictResolutionUi() {
var $content = $('#removerModalContent');
function setChoice($card, type, value) {
var inputClass = type === 'page' ? '.rmConflictPageAction' : '.rmConflictTemplateAction';
$card.find(inputClass).val(value);
$card.find('[data-rm-choice-type="' + type + '"]').each(function () {
var isActive = $(this).data('rmChoice') === value;
$(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false');
});
updateNominationConflictCardState($card);
}
$content.off('.rmConflictResolution').on('click.rmConflictResolution', '[data-rm-choice-type]', function () {
var $btn = $(this);
var $card = $btn.closest('.rmConflictCard');
if ($btn.prop('disabled')) return;
setChoice($card, $btn.data('rmChoiceType'), $btn.data('rmChoice'));
});
$('#rmConflictList .rmConflictCard').each(function () { updateNominationConflictCardState($(this)); });
}
function collectNominationConflictResolution(conflicts) {
var decisions = {};
(conflicts || []).forEach(function (conflict, index) {
var $card = $('#rmConflictList .rmConflictCard[data-rm-conflict-index="' + index + '"]');
decisions[normTitle(conflict.pageName)] = {
pageAction: $card.find('.rmConflictPageAction').val() || 'skip',
templateAction: $card.find('.rmConflictTemplateAction').val() || 'keep'
};
});
return decisions;
}
function applyNominationConflictResolutionToJob(job, decisions) {
var resultArticles;
var headerText;
if (!job || !job.isMulti) {
job.conflictDecisions = decisions || {};
return { value: job };
}
resultArticles = (job.multiArticles || []).filter(function (pageName) {
var decision = decisions && decisions[normTitle(pageName)];
return !decision || decision.pageAction !== 'skip';
});
if (!resultArticles.length) return { error: 'После исключения конфликтующих статей в номинации не осталось ни одной страницы.' };
job.conflictDecisions = decisions || {};
job.multiArticles = resultArticles.slice();
job.pages = resultArticles.slice().reverse();
headerText = String(job.multiHeaderText || '').trim();
job.section = headerText || ('[[:' + resultArticles[0] + ']]');
job.sectionNW = job.section.replace(/\[\[:/g, '').replace(/]]/g, '');
job.msg = job.multiNominationFormat === 'list'
? buildMultiNominationListText(resultArticles, job.multiNominationBody, job.multiArticleComments)
: buildMultiNominationText(resultArticles, job.multiNominationBody, job.multiArticleComments);
job.summary = makeSummary('номинация [[' + job.nomPage + '#' + job.sectionNW + ']]');
return { value: job };
}
function showNominationConflictResolution(job, conflicts, onContinue) {
resetModalObservers();
$('#removerModalContent').html(buildNominationConflictResolutionHtml(conflicts));
bindNominationConflictResolutionUi();
syncLinkWidths();
renderModalFooter('submit', {
submitText: 'Продолжить номинирование',
showSubscribe: true,
preserveLogOnSubmit: true,
onSubmit: function () {
var decisions = collectNominationConflictResolution(conflicts);
var applied = applyNominationConflictResolutionToJob(job, decisions);
if (applied.error) {
alert(applied.error);
return false;
}
if (typeof onContinue === 'function') onContinue(applied.value);
return true;
}
});
}
function bindTouchTextareaGrip($ta, sync, getMaxWidth, options) {
var opts = options || {};
var minWidth = parseInt(opts.minWidth, 10) || parseInt(sz.taMinW, 10) || 180;
var minHeight = parseInt(opts.minHeight, 10) || parseInt(sz.taMinH, 10) || 100;
var allowWidthResize = opts.allowWidth !== false;
var syncFn = typeof sync === 'function' ? sync : function () {};
var getMaxWidthFn = typeof getMaxWidth === 'function' ? getMaxWidth : function () { return $ta.outerWidth() || minWidth; };
var usePointerEvents = typeof window.PointerEvent === 'function';
var dragState = { active: false, startX: 0, startY: 0, startWidth: 0, startHeight: 0 };
var gripStyle = 'height:20px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-top:0;border-radius:0 0 4px 4px;background:' + tk.bgNSub + ';display:flex;align-items:center;justify-content:center;cursor:ns-resize;touch-action:none;user-select:none;-webkit-user-select:none;';
if (opts.gripMarginBottom) gripStyle += 'margin-bottom:' + opts.gripMarginBottom + ';';
var $grip = $('<div data-rm-textarea-grip="1" style="' + gripStyle + '"><span style="display:block;width:42px;height:4px;border-radius:999px;background:' + tk.bSub + ';opacity:.9;"></span></div>');
function getCoord(evt, key) {
var e = evt.originalEvent || evt;
if (e.touches && e.touches.length) return e.touches[0][key];
if (e.changedTouches && e.changedTouches.length) return e.changedTouches[0][key];
return e[key];
}
function stopDrag() {
dragState.active = false;
$(window).off('.rmTaResize');
}
function onDragMove(evt) {
var clientX;
var clientY;
if (!dragState.active) return;
clientX = getCoord(evt, 'clientX');
clientY = getCoord(evt, 'clientY');
if (typeof clientY !== 'number') return;
if (allowWidthResize && typeof clientX === 'number') $ta.css('width', Math.max(minWidth, Math.min(getMaxWidthFn(), dragState.startWidth + (clientX - dragState.startX))) + 'px');
$ta.css('height', Math.max(minHeight, dragState.startHeight + (clientY - dragState.startY)) + 'px');
syncFn();
if (evt.preventDefault) evt.preventDefault();
}
function startDrag(evt) {
var clientY = getCoord(evt, 'clientY');
if (typeof clientY !== 'number') return;
dragState.active = true;
dragState.startX = getCoord(evt, 'clientX') || 0;
dragState.startY = clientY;
dragState.startWidth = $ta.outerWidth();
dragState.startHeight = $ta.outerHeight();
if (evt.preventDefault) evt.preventDefault();
$(window).off('.rmTaResize');
if (usePointerEvents) {
$(window).on('pointermove.rmTaResize', onDragMove).on('pointerup.rmTaResize pointercancel.rmTaResize', stopDrag);
} else {
$(window).on('touchmove.rmTaResize mousemove.rmTaResize', onDragMove).on('touchend.rmTaResize touchcancel.rmTaResize mouseup.rmTaResize', stopDrag);
}
}
$ta.css({ 'border-bottom-left-radius': '0', 'border-bottom-right-radius': '0' });
$ta.next('[data-rm-textarea-grip]').remove();
$ta.after($grip);
if (usePointerEvents) $grip.on('pointerdown.rmTaGrip', startDrag);
else $grip.on('touchstart.rmTaGrip mousedown.rmTaGrip', startDrag);
}
function applyModalContentWidth($modal, contentWidth, options) {
var opts = options || {};
var layout = getModalLayout();
var modalFrame = getBoxFrameWidth($modal);
var safeContentWidth = Math.max(layout.minWidth, Math.min(contentWidth, layout.maxOuterWidth - modalFrame));
var modalWidth = safeContentWidth + modalFrame;
var initialContentW = parseFloat($modal.data('rmInitialContentW')) || 0;
$modal.css({
width: modalWidth + 'px',
'max-width': layout.maxOuterWidth + 'px',
'box-sizing': 'border-box',
'margin-left': layout.shouldCenter ? 'auto' : '0',
'margin-right': layout.shouldCenter ? 'auto' : '0'
}).toggleClass('rmCompactContent', safeContentWidth < 520);
$('.' + RESIZE_CLASS).css({
width: safeContentWidth + 'px',
'max-width': '100%',
'box-sizing': 'border-box'
});
$('#rmMsg,#nominationReason,#rmReportText').each(function () {
var $textarea = $(this);
var textareaId = this.id;
if (!$textarea.length) return;
$textarea.css('width', safeContentWidth + 'px');
$textarea.next('[data-rm-textarea-grip]').css('width', safeContentWidth + 'px');
$('.rmQuickPhrasesPanel[data-rm-target="' + textareaId + '"]').css('width', safeContentWidth + 'px');
});
$('.rmNestedCommentInput').each(function () {
var $textarea = $(this);
var $wrap = $textarea.parent();
var containerFrame = parseFloat($textarea.data('rmNestedContainerFrame')) || 0;
var wrapFrame = parseFloat($textarea.data('rmNestedWrapFrame')) || 0;
var safeMinWidth = parseFloat($textarea.data('rmNestedMinWidth')) || 0;
var wrapOuterWidth;
var textareaWidth;
if (!$wrap.length || !$wrap.is(':visible')) return;
wrapOuterWidth = Math.max(1, safeContentWidth - containerFrame);
textareaWidth = Math.max(1, wrapOuterWidth - wrapFrame);
$wrap.css({
width: wrapOuterWidth + 'px',
'max-width': '100%',
'box-sizing': 'border-box'
});
$textarea.css({
width: textareaWidth + 'px',
'min-width': Math.min(safeMinWidth || textareaWidth, textareaWidth) + 'px'
});
$textarea.next('[data-rm-textarea-grip]').css('width', textareaWidth + 'px');
$('.rmQuickPhrasesPanel[data-rm-target="' + this.id + '"]').css('width', textareaWidth + 'px');
});
syncLinkWidths();
if (isVector22) {
var $content = $('#content');
if ($content.length && modalWidth > initialContentW) $content.css({ 'min-width': modalWidth + 'px' });
else if ($content.length) $content.css({ 'min-width': '' });
} else {
$('#content').css({ 'min-width': '' });
}
if (!opts.skipStore) $modal.data('rmContentWidth', safeContentWidth);
return safeContentWidth;
}
function setupNestedResizableTextarea(textareaId, wrapId, minWidth, minHeight) {
var $ta = $('#' + textareaId);
var $wrap = $('#' + wrapId);
var $modal = $('#removerModal');
var $container = $wrap.parent();
var layout = getModalLayout();
var safeMinWidth = parseInt(minWidth, 10) || 280;
var safeMinHeight = parseInt(minHeight, 10) || 90;
var initialWidth;
var modalFrame;
var containerFrame;
var wrapFrame;
function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; }
function getMaxTextareaWidth(currentLayout) {
return Math.max(1, currentLayout.maxOuterWidth - modalFrame - containerFrame - wrapFrame);
}
function getEffectiveMinWidth(currentLayout) {
return Math.min(safeMinWidth, getMaxTextareaWidth(currentLayout));
}
function sync() {
var currentLayout = getModalLayout();
var maxTextareaWidth = getMaxTextareaWidth(currentLayout);
var textareaWidth = $ta.outerWidth();
var contentWidth;
if (!$wrap.is(':visible')) return;
if (textareaWidth > maxTextareaWidth) {
$ta.css('width', maxTextareaWidth + 'px');
textareaWidth = $ta.outerWidth();
}
contentWidth = Math.min(currentLayout.maxOuterWidth - modalFrame, textareaWidth + wrapFrame + containerFrame);
applyModalContentWidth($modal, contentWidth);
}
if (!$ta.length || !$wrap.length || !$modal.length) return;
modalFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width');
containerFrame = $container.length
? px($container, 'padding-left') + px($container, 'padding-right') + px($container, 'border-left-width') + px($container, 'border-right-width')
: 0;
wrapFrame = px($wrap, 'padding-left') + px($wrap, 'padding-right') + px($wrap, 'border-left-width') + px($wrap, 'border-right-width');
initialWidth = Math.min(
Math.max(getEffectiveMinWidth(layout), getDefaultResizableWidth(modalFrame + containerFrame + wrapFrame)),
getMaxTextareaWidth(layout)
);
$ta.css({
width: initialWidth + 'px',
'min-width': getEffectiveMinWidth(layout) + 'px',
'min-height': safeMinHeight + 'px',
'box-sizing': 'border-box',
resize: layout.useFullWidth ? 'none' : 'both',
'border-bottom-left-radius': '',
'border-bottom-right-radius': ''
});
$ta.data('rmNestedContainerFrame', containerFrame);
$ta.data('rmNestedWrapFrame', wrapFrame);
$ta.data('rmNestedMinWidth', safeMinWidth);
$ta.next('[data-rm-textarea-grip]').remove();
if (layout.useFullWidth) {
bindTouchTextareaGrip($ta, sync, function () {
return getMaxTextareaWidth(getModalLayout());
}, {
minWidth: getEffectiveMinWidth(layout),
minHeight: safeMinHeight
});
}
registerModalLayoutSync(sync);
$(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
if (typeof ResizeObserver === 'function') {
var observer = new ResizeObserver(sync);
observer.observe($ta[0]);
registerResizeObserver(observer);
}
sync();
}
function setupResizableModal(textareaId) {
var $ta = $('#' + textareaId);
var $modal = $('#removerModal');
var layout = getModalLayout();
function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; }
applyV2022Layout($modal);
$modal.css({ display: 'block', 'margin-left': layout.shouldCenter ? 'auto' : '0', 'margin-right': layout.shouldCenter ? 'auto' : '0' });
var isBorderBox = ($modal.css('box-sizing') || '').toLowerCase() === 'border-box';
var hFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width');
var initialContentW = isVector22 ? ($('#content').outerWidth() || 0) : 0;
var minWidth = layout.minWidth;
$modal.data('rmInitialContentW', initialContentW);
$ta.css({ width: getDefaultResizableWidth(hFrame) + 'px', height: sz.taH, padding: '8px', 'box-sizing': 'border-box',
border: '1px solid ' + tk.bSub, 'border-radius': '2px', background: tk.bgBase,
color: 'inherit', resize: layout.useFullWidth ? 'none' : 'both', 'min-height': sz.taMinH, 'min-width': sz.taMinW });
$(window).off('.rmTaResize');
function sync() {
var currentLayout = getModalLayout();
var maxTextareaWidth = Math.max(minWidth, currentLayout.maxOuterWidth - Math.floor(hFrame));
var w = $ta.outerWidth();
if (w > maxTextareaWidth) {
$ta.css('width', maxTextareaWidth + 'px');
w = $ta.outerWidth();
}
applyModalContentWidth($modal, isBorderBox ? w : Math.min(currentLayout.maxOuterWidth - hFrame, w));
}
if (layout.useFullWidth) bindTouchTextareaGrip($ta, sync, function () {
return Math.max(minWidth, getModalLayout().maxOuterWidth - Math.floor(hFrame));
});
else {
$ta.css({ 'border-bottom-left-radius': '', 'border-bottom-right-radius': '' });
$ta.next('[data-rm-textarea-grip]').remove();
}
registerModalLayoutSync(sync);
$(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
if (typeof ResizeObserver === 'function') {
var observer = new ResizeObserver(sync);
observer.observe($ta[0]);
registerResizeObserver(observer);
}
sync();
}
function addInputRow(opts) {
var w = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0;
$('#' + opts.containerId).append(joinHtml([
'<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, w ? 'width:' + w + 'px;' : '', '">',
'<input type="text" class="', opts.inputClass || 'variantInput', '" placeholder="', opts.placeholder || '', '" style="', stInputBox, '">',
'<button type="button" class="rmRemoveInput" style="', stRemoveBtn, '" title="Удалить">−</button>',
'</div>'
]));
syncModalLayout();
}
$(document).off('click.rmRemoveInput').on('click.rmRemoveInput', '.rmRemoveInput', function () {
$(this).closest('.rmInputRow').remove();
syncModalLayout();
});
$(document).off('click.rmQuickPhraseInsert').on('click.rmQuickPhraseInsert', '.rmQuickPhraseActionBtn', function (e) {
var targetId;
var phrase;
e.preventDefault();
targetId = $(this).data('rmTarget');
phrase = $(this).attr('data-rm-phrase') || '';
if (!targetId) return;
insertTextIntoTextarea($('#' + targetId), phrase);
});
function buildMultiInputHtml(c) {
var addLabel = c.addBtnLabel || '+ Добавить';
return joinHtml([
'<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="', c.firstId, '" type="text" class="', c.inputClass || 'variantInput', '" placeholder="', c.firstPh || '', '" style="', stInputBox, '">',
buildSquareAddButtonHtml(c.addBtnId, addLabel.replace(/^\+\s*/, '') || 'Добавить'),
'</div>',
'<div id="', c.containerId, '"></div>'
]);
}
function wireMultiInput(c) {
$('#' + c.addBtnId).click(function () {
var count = $('#' + c.containerId + ' .rmInputRow').length;
if (typeof c.maxRows === 'number' && count >= c.maxRows) { alert(c.maxMsg || 'Достигнут лимит.'); return; }
addInputRow({ containerId: c.containerId, placeholder: c.addPh, inputClass: c.inputClass });
});
}
function buildSettingsFieldHtml(label, controlHtml, helpText, options) {
var opts = options || {};
var helpHtml = helpText
? '<div class="rmSettingsFieldHint">' + helpText + '</div>'
: '';
var labelHtml = opts.forId
? '<label class="rmSettingsFieldLabel" for="' + opts.forId + '">' + label + '</label>'
: '<div class="rmSettingsFieldLabel">' + label + '</div>';
return joinHtml([
'<div class="rmSettingsField">',
labelHtml,
'<div class="rmSettingsFieldControl">', controlHtml, '</div>',
helpHtml,
'</div>'
]);
}
function buildSettingsSectionHtml(title, bodyHtml, helpText, options) {
var opts = options || {};
var headerHtml = '';
var description = [];
if (opts.titleNote) description.push(opts.titleNote);
if (helpText) description.push(helpText);
if (title || description.length) {
headerHtml = joinHtml([
'<div class="rmSettingsSectionHeader">',
title ? '<div class="rmSettingsSectionTitle">' + title + '</div>' : '',
description.length ? '<div class="rmSettingsSectionDescription">' + description.join(' ') + '</div>' : '',
'</div>'
]);
}
return joinHtml([
'<div class="rmSettingsSection">',
headerHtml,
bodyHtml,
'</div>'
]);
}
function buildSettingsSimpleCheckboxHtml(id, text) {
return joinHtml([
'<label class="rmSettingsCheck">',
'<input id="', id, '" type="checkbox">',
'<span>', text, '</span>',
'</label>'
]);
}
function buildQuickPhrasesSettingsEditorHtml() {
return joinHtml([
'<div id="rmSettingsQuickPhrasesEditor" class="rmQuickPhraseEditor">',
'<div id="rmSettingsQuickPhrasesList" class="rmQuickPhraseList"></div>',
'<input id="rmSettingsQuickPhraseInput" type="text" autocomplete="off" style="', stInputFull, 'margin-bottom:0;">',
'<div id="rmSettingsQuickPhraseMeta" class="rmQuickPhraseMeta"></div>',
'</div>'
]);
}
function getQuickPhraseEditor() {
return $('#rmSettingsQuickPhrasesEditor');
}
function getQuickPhraseEditorState() {
var $editor = getQuickPhraseEditor();
var phrases = normalizeQuickPhrasesList($editor.data('rmQuickPhrases'), []);
var editingIndex = parseInt($editor.data('rmQuickPhraseEditingIndex'), 10);
if (isNaN(editingIndex)) editingIndex = -1;
return { editor: $editor, phrases: phrases, editingIndex: editingIndex };
}
function setQuickPhraseEditorState(phrases, editingIndex) {
var $editor = getQuickPhraseEditor();
var normalized = normalizeQuickPhrasesList(phrases, []);
var safeEditingIndex = parseInt(editingIndex, 10);
if (isNaN(safeEditingIndex) || safeEditingIndex < 0 || safeEditingIndex >= normalized.length) safeEditingIndex = -1;
if (!$editor.length) return;
$editor.data('rmQuickPhrases', normalized);
$editor.data('rmQuickPhraseEditingIndex', safeEditingIndex);
renderQuickPhraseEditor();
}
function clearQuickPhraseDropState() {
var $editor = getQuickPhraseEditor();
$editor.removeData('rmQuickPhraseDragIndex');
$editor.removeData('rmQuickPhraseDropIndex');
$editor.removeData('rmQuickPhraseDropAfter');
$editor.find('.rmQuickPhraseChip').removeClass('is-dragging is-drop-before is-drop-after');
}
function renderQuickPhraseEditor() {
var state = getQuickPhraseEditorState();
var $list = $('#rmSettingsQuickPhrasesList');
var $input = $('#rmSettingsQuickPhraseInput');
var $meta = $('#rmSettingsQuickPhraseMeta');
if (!state.editor.length || !$list.length || !$input.length) return;
if (state.phrases.length) {
$list.html(state.phrases.map(function (phrase, index) {
var chipClass = 'rmQuickPhraseChip' + (index === state.editingIndex ? ' is-editing' : '');
return joinHtml([
'<div class="', chipClass, '" draggable="true" data-rm-quick-index="', index, '">',
'<button type="button" class="rmQuickPhraseEditBtn" title="Редактировать фразу">', escapeHtml(phrase), '</button>',
'<button type="button" class="rmQuickPhraseRemoveBtn" title="Удалить фразу" aria-label="Удалить фразу">×</button>',
'</div>'
]);
}).join(''));
} else {
$list.html('<div class="rmQuickPhraseEmpty">Фразы пока не добавлены.</div>');
}
$input
.attr('placeholder', state.editingIndex >= 0 ? 'Изменить значение...' : 'Добавить значение...')
.toggleClass('is-editing', state.editingIndex >= 0);
$meta
.text('')
.hide();
}
function notifyQuickPhraseEditorChanged() {
var $editor = getQuickPhraseEditor();
if ($editor.length) $editor.trigger('rmQuickPhrasesChanged');
}
function startQuickPhraseEdit(index) {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
if (index < 0 || index >= state.phrases.length || !$input.length) return;
state.editor.data('rmQuickPhraseEditingIndex', index);
$input.val(state.phrases[index]);
renderQuickPhraseEditor();
$input.trigger('focus');
if ($input[0] && typeof $input[0].select === 'function') $input[0].select();
}
function cancelQuickPhraseEdit() {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
state.editor.data('rmQuickPhraseEditingIndex', -1);
if ($input.length) $input.val('').removeClass('is-editing');
renderQuickPhraseEditor();
}
function saveQuickPhraseInput() {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
var value = normalizeQuickPhraseValue($input.val());
var next = [];
if (!$input.length || !value) return false;
if (state.editingIndex >= 0) {
state.phrases.forEach(function (phrase, index) {
if (index === state.editingIndex) {
next.push(value);
return;
}
if (phrase !== value && next.indexOf(phrase) === -1) next.push(phrase);
});
} else {
next = state.phrases.slice();
if (next.indexOf(value) === -1) next.push(value);
}
state.editor.data('rmQuickPhrases', normalizeQuickPhrasesList(next, []));
state.editor.data('rmQuickPhraseEditingIndex', -1);
$input.val('').removeClass('is-editing');
clearQuickPhraseDropState();
renderQuickPhraseEditor();
notifyQuickPhraseEditorChanged();
return true;
}
function removeQuickPhrase(index) {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
if (index < 0 || index >= state.phrases.length) return;
state.phrases.splice(index, 1);
state.editor.data('rmQuickPhrases', state.phrases);
if (state.editingIndex === index) {
state.editor.data('rmQuickPhraseEditingIndex', -1);
if ($input.length) $input.val('').removeClass('is-editing');
} else if (state.editingIndex > index) {
state.editor.data('rmQuickPhraseEditingIndex', state.editingIndex - 1);
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
notifyQuickPhraseEditorChanged();
}
function reorderQuickPhrases(phrases, fromIndex, toIndex, placeAfter) {
var result = phrases.slice();
var insertIndex = toIndex + (placeAfter ? 1 : 0);
var item;
if (fromIndex < 0 || fromIndex >= result.length || toIndex < 0 || toIndex >= result.length) return result;
item = result.splice(fromIndex, 1)[0];
if (fromIndex < insertIndex) insertIndex--;
result.splice(insertIndex, 0, item);
return result;
}
function getQuickPhraseDropPointer(evt) {
var originalEvent = evt && (evt.originalEvent || evt);
if (!originalEvent) return null;
if (typeof originalEvent.clientX !== 'number' || typeof originalEvent.clientY !== 'number') return null;
return { x: originalEvent.clientX, y: originalEvent.clientY };
}
function getQuickPhraseDropTarget($list, pointer, dragIndex) {
var candidates = [];
var rowCandidates;
var minRowDistance = Infinity;
var bestBoundary = null;
var bestBoundaryDistance = Infinity;
if (!$list || !$list.length || !pointer) return null;
$list.children('.rmQuickPhraseChip').each(function () {
var index = parseInt($(this).attr('data-rm-quick-index'), 10);
var rect;
var rowDistance;
if (isNaN(index) || index === dragIndex) return;
rect = this.getBoundingClientRect();
if (!rect.width || !rect.height) return;
rowDistance = pointer.y < rect.top ? (rect.top - pointer.y) : (pointer.y > rect.bottom ? (pointer.y - rect.bottom) : 0);
candidates.push({
node: this,
index: index,
left: rect.left,
right: rect.right,
top: rect.top,
bottom: rect.bottom,
midX: rect.left + rect.width / 2,
rowDistance: rowDistance
});
if (rowDistance < minRowDistance) minRowDistance = rowDistance;
});
if (!candidates.length) return null;
rowCandidates = candidates
.filter(function (candidate) { return candidate.rowDistance === minRowDistance; })
.sort(function (a, b) {
if (a.left !== b.left) return a.left - b.left;
return a.index - b.index;
});
if (!rowCandidates.length) return null;
if (pointer.x <= rowCandidates[0].left) {
return { index: rowCandidates[0].index, placeAfter: false, node: rowCandidates[0].node };
}
if (pointer.x >= rowCandidates[rowCandidates.length - 1].right) {
return {
index: rowCandidates[rowCandidates.length - 1].index,
placeAfter: true,
node: rowCandidates[rowCandidates.length - 1].node
};
}
for (var i = 0; i < rowCandidates.length; i++) {
var candidate = rowCandidates[i];
if (pointer.x >= candidate.left && pointer.x <= candidate.right) {
return {
index: candidate.index,
placeAfter: pointer.x > candidate.midX,
node: candidate.node
};
}
}
rowCandidates.forEach(function (candidate) {
var leftDistance = Math.abs(pointer.x - candidate.left);
var rightDistance = Math.abs(pointer.x - candidate.right);
if (leftDistance < bestBoundaryDistance) {
bestBoundaryDistance = leftDistance;
bestBoundary = { index: candidate.index, placeAfter: false, node: candidate.node };
}
if (rightDistance < bestBoundaryDistance) {
bestBoundaryDistance = rightDistance;
bestBoundary = { index: candidate.index, placeAfter: true, node: candidate.node };
}
});
return bestBoundary;
}
function bindQuickPhrasesEditor() {
var $editor = getQuickPhraseEditor();
var $list = $('#rmSettingsQuickPhrasesList');
var $input = $('#rmSettingsQuickPhraseInput');
function updateQuickPhraseDropTarget(evt) {
var dragIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10);
var pointer = getQuickPhraseDropPointer(evt);
var target;
if (isNaN(dragIndex) || !pointer) return null;
target = getQuickPhraseDropTarget($list, pointer, dragIndex);
if (!target) return null;
$editor.data('rmQuickPhraseDropIndex', target.index);
$editor.data('rmQuickPhraseDropAfter', !!target.placeAfter);
$editor.find('.rmQuickPhraseChip').removeClass('is-drop-before is-drop-after');
$(target.node).addClass(target.placeAfter ? 'is-drop-after' : 'is-drop-before');
return target;
}
function applyQuickPhraseDrop() {
var state = getQuickPhraseEditorState();
var fromIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10);
var toIndex = parseInt($editor.data('rmQuickPhraseDropIndex'), 10);
var placeAfter = $editor.data('rmQuickPhraseDropAfter') === true;
var changed = false;
if (!isNaN(fromIndex) && !isNaN(toIndex) && fromIndex !== toIndex) {
state.editor.data('rmQuickPhrases', reorderQuickPhrases(state.phrases, fromIndex, toIndex, placeAfter));
if (state.editingIndex === fromIndex) state.editor.data('rmQuickPhraseEditingIndex', -1);
changed = true;
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
if (changed) notifyQuickPhraseEditorChanged();
}
if (!$editor.length || !$list.length || !$input.length) return;
$editor.off('.rmQuickPhraseEditor');
$list.off('.rmQuickPhraseEditor');
$input.off('.rmQuickPhraseEditor');
$input.on('keydown.rmQuickPhraseEditor', function (e) {
if (e.key === 'Enter' || e.keyCode === 13) {
e.preventDefault();
saveQuickPhraseInput();
} else if (e.key === 'Escape' || e.keyCode === 27) {
e.preventDefault();
cancelQuickPhraseEdit();
}
});
$editor
.on('click.rmQuickPhraseEditor', '.rmQuickPhraseEditBtn', function () {
var index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10);
startQuickPhraseEdit(index);
})
.on('click.rmQuickPhraseEditor', '.rmQuickPhraseRemoveBtn', function (e) {
var index;
e.preventDefault();
e.stopPropagation();
index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10);
removeQuickPhrase(index);
})
.on('dragstart.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
var index = parseInt($(this).attr('data-rm-quick-index'), 10);
if (isNaN(index)) return;
$editor.data('rmQuickPhraseDragIndex', index);
$(this).addClass('is-dragging');
if (e.originalEvent && e.originalEvent.dataTransfer) {
e.originalEvent.dataTransfer.effectAllowed = 'move';
e.originalEvent.dataTransfer.setData('text/plain', String(index));
}
})
.on('dragover.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
if ($editor.data('rmQuickPhraseDragIndex') === undefined) return;
e.preventDefault();
updateQuickPhraseDropTarget(e);
if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move';
})
.on('drop.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
e.preventDefault();
updateQuickPhraseDropTarget(e);
applyQuickPhraseDrop();
})
.on('dragend.rmQuickPhraseEditor', '.rmQuickPhraseChip', function () {
clearQuickPhraseDropState();
});
$list
.on('dragover.rmQuickPhraseEditor', function (e) {
if ($editor.data('rmQuickPhraseDragIndex') === undefined) return;
e.preventDefault();
updateQuickPhraseDropTarget(e);
if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move';
})
.on('drop.rmQuickPhraseEditor', function (e) {
e.preventDefault();
updateQuickPhraseDropTarget(e);
applyQuickPhraseDrop();
});
renderQuickPhraseEditor();
}
function collectQuickPhraseValues() {
return getQuickPhraseEditorState().phrases;
}
function collectQuickPhraseValuesSnapshot() {
var state = getQuickPhraseEditorState();
var value = normalizeQuickPhraseValue($('#rmSettingsQuickPhraseInput').val());
var next = state.phrases.slice();
if (!value) return next;
if (state.editingIndex >= 0) {
next[state.editingIndex] = value;
} else if (next.indexOf(value) === -1) {
next.push(value);
}
return normalizeQuickPhrasesList(next, []);
}
function isMenuTitlePresetValue(value) {
return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS;
}
function isMenuTitlePresetOnlySkin() {
return mwCfg.skin === 'minerva' || mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
}
function getDefaultMenuTitlePreset() {
return mwCfg.skin === 'minerva' ? MENU_TITLE_PRESET_TOOLS : MENU_TITLE_PRESET_CACTIONS;
}
function shouldPreserveStoredMenuTitleOnSave() {
return mwCfg.skin === 'minerva';
}
function isAvailableMenuTitlePresetValue(value) {
return getMenuTitlePresetOptions().some(function (option) {
return option.value === value;
});
}
function getMenuTitlePresetOptions() {
if (mwCfg.skin === 'minerva') {
return [
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Ещё' }
];
}
if (isVector22) {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты/Действия' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты/Основное' }
];
}
if (mwCfg.skin === 'timeless') {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты для страниц' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Вики-инструменты' }
];
}
if (mwCfg.skin === 'monobook') {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Верхняя панель' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' }
];
}
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: mwCfg.skin === 'vector' ? 'Ещё' : 'Ещё / Действия' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' }
];
}
function getMenuTitlePresetHintText() {
var base = (mwCfg.skin === 'vector' || isVector22)
? 'Можно либо изменить заголовок отдельного меню Remover, либо перенести все пункты в одно из существующих стандартных меню.'
: 'На этом скине Remover использует существующие стандартные меню; отдельное меню с собственным заголовком не создаётся.';
if (isVector22) base += ' В Vector 2022 кнопки «Действия» и «Основное» находятся внутри общего меню «Инструменты».';
else if (mwCfg.skin === 'vector') base += ' В Vector отдельный заголовок создаёт собственное меню рядом с «Ещё».';
else if (mwCfg.skin === 'minerva') base += ' В Minerva Neue пункты Remover показываются в меню «Ещё».';
else if (mwCfg.skin === 'timeless') base += ' В Timeless доступны только стандартные варианты: «Инструменты для страниц», «Страница» и «Вики-инструменты».';
else if (mwCfg.skin === 'monobook') base += ' В MonoBook доступны только два варианта: поместить пункты в «Инструменты» или вывести их в верхнюю панель. Собственный заголовок меню здесь не используется.';
return base;
}
function getSignatureSeparatorPreviewText(value) {
var separator = String(value || '').trim();
return separator ? (separator + ' ' + '~~' + '~~') : ('~~' + '~~');
}
function updateSignatureSeparatorPreview(value) {
var previewValue = (typeof value === 'string') ? value : ($('#rmSettingsSignatureSeparator').val() || '');
var $code = $('#rmSettingsSignaturePreviewCode');
if (!$code.length) return;
$code.text(getSignatureSeparatorPreviewText(previewValue));
}
function bindSignatureSeparatorPreview() {
var $input = $('#rmSettingsSignatureSeparator');
if (!$input.length) return;
$input.off('.rmSignaturePreview').on('input.rmSignaturePreview change.rmSignaturePreview', function () {
updateSignatureSeparatorPreview($(this).val());
});
updateSignatureSeparatorPreview($input.val());
}
function buildMenuTitlePresetButtonsHtml() {
return joinHtml([
'<div class="rmSettingsMenuPresetWrap">',
'<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>',
'<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">',
getMenuTitlePresetOptions().map(function (option) {
return joinHtml([
'<button type="button" class="rmSettingsMenuPresetBtn" data-rm-menu-preset="',
option.value,
'" aria-pressed="false">',
escapeHtml(option.label),
'</button>'
]);
}).join(''),
'</div>',
'</div>'
]);
}
function applyMenuTitlePresetControls(presetValue) {
var preset = isMenuTitlePresetValue(presetValue) ? presetValue : '';
var $bar = $('#rmSettingsMenuPresetBar');
var $input = $('#rmSettingsMenuTitle');
var forcePresetOnly = isMenuTitlePresetOnlySkin();
if (!$bar.length || !$input.length) return;
$bar.data('rmPreset', preset);
$bar.find('.rmSettingsMenuPresetBtn').each(function () {
var isActive = $(this).data('rmMenuPreset') === preset;
$(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false');
});
$input.prop('disabled', forcePresetOnly || !!preset);
}
function bindMenuTitlePresetControls() {
var $bar = $('#rmSettingsMenuPresetBar');
var $input = $('#rmSettingsMenuTitle');
if (!$bar.length || !$input.length) return;
$input.off('.rmSettingsMenuPreset').on('input.rmSettingsMenuPreset', function () {
if (!$bar.data('rmPreset')) $input.data('rmCustomValue', $input.val());
});
$bar.off('.rmSettingsMenuPreset').on('click.rmSettingsMenuPreset', '.rmSettingsMenuPresetBtn', function () {
var preset = $(this).data('rmMenuPreset');
var currentPreset = $bar.data('rmPreset') || '';
if (currentPreset === preset) {
if (isMenuTitlePresetOnlySkin()) return;
applyMenuTitlePresetControls('');
$input.val($input.data('rmCustomValue') || '');
$input.trigger('focus');
return;
}
$input.data('rmCustomValue', $input.val());
applyMenuTitlePresetControls(preset);
});
}
function fillSettingsFormValues(settings) {
var data = normalizeRemoverSettings(settings);
var forcePresetOnly = isMenuTitlePresetOnlySkin();
var menuTitleValue = data.menuTitle || '';
var storedMenuTitleValue = menuTitleValue;
if (forcePresetOnly && !isAvailableMenuTitlePresetValue(menuTitleValue)) menuTitleValue = getDefaultMenuTitlePreset();
var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue);
$('#rmSettingsForm').data('rmStoredMenuTitle', storedMenuTitleValue || '');
$('#rmSettingsNotifyAuthor').prop('checked', !!data.notifyAuthor);
$('#rmSettingsSubscribeTopic').prop('checked', !!data.subscribeTopic);
$('#rmSettingsShowMenuIcons').prop('checked', !!data.showMenuIcons);
$('#rmSettingsMenuTitle').val(customMenuTitle || '').data('rmCustomValue', customMenuTitle || '');
$('#rmSettingsSignatureSeparator').val(data.signatureSeparator || '');
$('#rmSettingsExcludedNamespaces').val((data.excludedNamespaces || []).join(', '));
$('#rmSettingsDisabledItems').val((data.disabledItems || []).join(', '));
setQuickPhraseEditorState(data.quickPhrases || [], -1);
$('#rmSettingsQuickPhraseInput').val('').removeClass('is-editing');
clearQuickPhraseDropState();
applyMenuTitlePresetControls(menuTitleValue);
updateSignatureSeparatorPreview(data.signatureSeparator || '');
}
function collectSettingsFormValues(options) {
var opts = options || {};
var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val());
var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val());
var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset');
var forcePresetOnly = isMenuTitlePresetOnlySkin();
var storedMenuTitle = $('#rmSettingsForm').data('rmStoredMenuTitle');
if (namespaces.invalid.length) {
return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' };
}
if (disabledItems.invalid.length) {
return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' };
}
if (!opts.skipQuickPhraseCommit) saveQuickPhraseInput();
return {
value: normalizeRemoverSettings({
notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'),
subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'),
showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'),
menuTitle: forcePresetOnly
? (shouldPreserveStoredMenuTitleOnSave() && typeof storedMenuTitle === 'string'
? storedMenuTitle
: (isAvailableMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : getDefaultMenuTitlePreset()))
: (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()),
signatureSeparator: $('#rmSettingsSignatureSeparator').val(),
excludedNamespaces: namespaces.values,
disabledItems: disabledItems.values,
quickPhrases: opts.skipQuickPhraseCommit ? collectQuickPhraseValuesSnapshot() : collectQuickPhraseValues()
})
};
}
function updateSettingsSubmitReadyState(baselineSettings) {
var collected = collectSettingsFormValues({ skipQuickPhraseCommit: true });
var hasChanges = !!(collected.error || (collected.value && !areRemoverSettingsEqual(collected.value, baselineSettings)));
$('#removerSubmit').toggleClass('rmSubmitReady', hasChanges && !$('#removerSubmit').hasClass('rmSubmitError'));
$('#rmSettingsUnsavedHint').css('display', hasChanges ? 'inline-block' : 'none');
}
function bindSettingsSubmitReadyState(baselineSettings) {
var update = function () {
setTimeout(function () { updateSettingsSubmitReadyState(baselineSettings); }, 0);
};
$('#rmSettingsForm').off('.rmSettingsReady').on('input.rmSettingsReady change.rmSettingsReady', 'input, textarea, select', update);
$('#removerModalContent').off('click.rmSettingsReady keyup.rmSettingsReady').on(
'click.rmSettingsReady keyup.rmSettingsReady',
'.rmSettingsMenuPresetBtn,.rmQuickPhraseEditBtn,.rmQuickPhraseRemoveBtn,.rmQuickPhraseChip,#rmSettingsQuickPhraseInput',
update
);
$('#rmSettingsQuickPhrasesEditor').off('rmQuickPhrasesChanged.rmSettingsReady').on('rmQuickPhrasesChanged.rmSettingsReady', update);
updateSettingsSubmitReadyState(baselineSettings);
}
function buildSettingsFormHtml(menuLabelsHint) {
var menuFields =
buildSettingsFieldHtml('Заголовок отдельного меню',
'<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(),
getMenuTitlePresetHintText(), { forId: 'rmSettingsMenuTitle' }) +
buildSettingsFieldHtml('Визуальное оформление меню',
'<div class="rmSettingsChecks">' + buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи в пунктах меню') + '</div>');
var messageFields =
buildSettingsFieldHtml('Префикс перед подписью',
'<input id="rmSettingsSignatureSeparator" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Добавляется перед подписью в публикуемых сообщениях.<span style="display:block;margin-top:6px;">Предпросмотр: <code id="rmSettingsSignaturePreviewCode"></code>.</span>',
{ forId: 'rmSettingsSignatureSeparator' }) +
buildSettingsFieldHtml('Часто используемые фразы', buildQuickPhrasesSettingsEditorHtml(),
'Enter для добавления. Порядок элементов изменяется перетаскиванием.', { forId: 'rmSettingsQuickPhraseInput' });
var defaultFields = '<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') +
buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') + '</div>';
var disableFields =
buildSettingsFieldHtml('Скрыть пункты меню',
'<input id="rmSettingsDisabledItems" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Названия пунктов через запятую, например <code>КБУ, КУЛ, КОБ</code>.' + menuLabelsHint,
{ forId: 'rmSettingsDisabledItems' }) +
buildSettingsFieldHtml('Не показывать в пространствах имён',
'<input id="rmSettingsExcludedNamespaces" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Номера пространств имён через запятую, например <code>2, 10, 828</code>. См. <a href="' + getPageUrl('Википедия:Пространства имён') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Пространства имён</a>.',
{ forId: 'rmSettingsExcludedNamespaces' });
return joinHtml([
'<div id="rmSettingsForm" style="max-width:100%;">',
'<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>',
buildSettingsSectionHtml('Меню', menuFields, 'Настройки внешнего вида и состава меню Remover.'),
buildSettingsSectionHtml('Оформление сообщений', messageFields, 'Настройки оформления публикуемых сообщений в номинациях.'),
buildSettingsSectionHtml('Опции по умолчанию', defaultFields, 'Регулирует изначальное состояние галочек.'),
buildSettingsSectionHtml('Отключение', disableFields, 'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'),
'</div>'
]);
}
function buildSettingsFooterLeftHtml() {
return joinHtml([
'<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;margin-right:auto;">',
'<button id="rmSettingsResetFooter" type="button" title="Удаляет сохранённые настройки Remover из вашего профиля MediaWiki и возвращает значения по умолчанию." style="', stCancel, '">Сбросить все настройки</button>',
'<a id="rmSettingsReportIssue" href="', getPageUrl('Обсуждение участника:Solidest/Remover'), '" target="_blank" rel="noopener noreferrer" ',
'title="Сообщить о проблеме или предложить улучшение" aria-label="Сообщить о проблеме или предложить улучшение" ',
'class="removerModalLink rmButtonLikeLink" style="', stCancel, 'display:inline-flex;align-items:center;justify-content:center;text-align:center;text-decoration:none;box-sizing:border-box;max-width:100%;line-height:1.2;word-break:normal;overflow-wrap:normal;">Обратная связь</a>',
'</div>'
]);
}
function openSettings() {
var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults);
var menuLabelsHint = buildSettingsMenuItemsHint();
var $previousModal = $('#removerModal').length ? $('#removerModal').detach() : $();
var previousLayoutSyncHandlers = modalLayoutSyncHandlers.slice();
function restorePreviousModal() {
closeModal();
if ($previousModal.length) {
$('#content').prepend($previousModal);
modalLayoutSyncHandlers = previousLayoutSyncHandlers.slice();
if (modalLayoutSyncHandlers.length) $(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
syncModalLayout();
syncLinkWidths();
}
}
createModal({
title: 'Конфигурация',
width: 'compact',
showSettingsButton: false
});
$('#removerModal').addClass('rmModalSettings');
$('#removerModalHeaderBar').append(buildHeaderIconButtonHtml('rmSettingsBack', 'Назад', 'Назад', '←'));
$('#rmSettingsBack').on('click', restorePreviousModal);
$('#removerModalContent').html(buildSettingsFormHtml(menuLabelsHint));
fillSettingsFormValues(currentSettings);
bindMenuTitlePresetControls();
bindSignatureSeparatorPreview();
bindQuickPhrasesEditor();
renderModalFooter('submit', {
showCheckbox: false,
submitText: 'Сохранить',
onSubmit: function () {
var collected = collectSettingsFormValues();
var shouldReset;
var saveFn;
if (collected.error) {
alert(collected.error);
return false;
}
shouldReset = areRemoverSettingsEqual(collected.value, settingsDefaults);
saveFn = shouldReset
? function (callback) { resetSettingsOnServer(callback); }
: function (callback) { saveSettingsToServer(collected.value, callback); };
saveFn(function (err) {
if (err) {
alert('Не удалось ' + (shouldReset ? 'сбросить' : 'сохранить') + ' настройки: ' + (err.info || err.code || 'неизвестная ошибка') + '.');
unlockModalSubmit();
return;
}
location.reload();
});
}
});
var $settingsActions = $('#rmFooterActionButtons');
$settingsActions.wrapInner('<div id="rmSettingsActionButtonsRow"></div>');
$settingsActions.append('<span id="rmSettingsUnsavedHint" role="status" aria-live="polite">Есть несохранённые изменения</span>');
bindSettingsSubmitReadyState(currentSettings);
$('#rmFooterButtons').css('justify-content', 'space-between').prepend(buildSettingsFooterLeftHtml());
$('#rmSettingsResetFooter').on('click', function () {
fillSettingsFormValues(settingsDefaults);
updateSettingsSubmitReadyState(currentSettings);
$('#removerSubmit').trigger('focus');
});
}
// ─── Завершение обработки ────────────────────────────────────────────────
function finalizeSuccess(nominationInfo, usePageReload) {
if (isError) {
var $box = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent');
$box.append('<p class="error">При выполнении скрипта произошли ошибки.</p>');
markSubmitError();
return;
}
renderModalFooter('reload');
if (nominationInfo && nominationInfo.pageTitle) {
appendNominationLink(nominationInfo.pageTitle, nominationInfo.sectionTitle);
}
if (!usePageReload && !nominationInfo) location.reload();
}
function finalizeFastRemoval(notifiedPages, summary) {
if (isError || !setAlert || !notifiedPages || !notifiedPages.length) {
finalizeSuccess(null, false);
return;
}
notifyAuthorsForPages(notifiedPages, {
summary: summary,
actionText: 'к быстрому удалению'
}, function () {
finalizeSuccess(null, false);
});
}
// ─── Общий runner ────────────────────────────────────────────────────────
/**
* Универсальный запуск полного пайплайна номинации.
* @param {Object} o
* templateStep — функция (next) → обработка шаблонов на статьях
* nominationStep — функция (done) → публикация номинации, done(err, {pageTitle, sectionTitle})
* notifyStep — функция (nominationInfo, next)
* skipNotify — boolean
* skipLink — boolean, не показывать ссылку на номинацию
*/
function runFlow(o) {
runNominationPipeline({
templateStep: o.templateStep,
nominationStep: o.nominationStep,
notifyStep: o.notifyStep || function (info, next) { next(); },
skipNotify: o.skipNotify,
onSuccess: function (ctx) {
if (isError) { markSubmitError(); return; }
renderModalFooter('reload');
if (!o.skipLink && ctx.nominationInfo && ctx.nominationInfo.pageTitle) {
appendNominationLink(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle);
}
},
onFailure: function () { markSubmitError(); }
});
}
// ═══════════════════════════════════════════════════════════════════════════
// ЯДРО: обработка статей (apply template + nomination page)
// ═══════════════════════════════════════════════════════════════════════════
/**
* Применяет шаблон к одной статье/категории.
* Понимает режим inArticle (вставка через <noinclude>),
* режим closeAction (снятие шаблона + запись на СО),
* режим cleanupAction (снятие КБУ/КУЛ).
*
* @param {string} pg — название страницы
* @param {Object} job — параметры задания (см. buildJob)
* @param {function} callback(err, meta)
*/
function applyTemplateToPage(pg, job, callback) {
var mode = job.mode;
// ── Снятие КБУ/КУЛ ──────────────────────────────────────────────────
if (mode === 'cleanup') {
var tm = job.transferMode || 'none';
if (tm === 'none') { callback({ code: 'error', info: 'Не выбран режим снятия шаблонов.' }); return; }
editPageContent(pg, { summary: job.summary, watchlist: 'nochange', readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var local = removeTransferTemplatesLocal(article, tm);
removeTransferTemplatesWithApiFallback(pg, local.text, tm, local, function (updated) {
if (updated.text === article) { done({ error: { code: 'error', info: 'Шаблоны для снятия не найдены.' } }); return; }
done({ text: updated.text });
});
}, function (err) { callback(err); });
return;
}
// ── Подведение итогов по КУ/КПМ (снятие + итог на СО) ───────────────
if (mode === 'denom') {
getTextWithTimestamp(pg, function (article, baseTimestamp, readErr) {
if (readErr) { callback(makeReadError(readErr, 'read_failed', 'Не удалось получить содержимое страницы «' + pg + '».')); return; }
if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; }
if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; }
var tplPattern = job.sourceTemplate.split('|').map(function (alias) {
return escapeRegExp(alias.trim()).replace(/\s+/g, '[ _]*');
}).join('|');
var tpl = findTemplateByPattern(article, tplPattern);
if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; }
var normalizedTplDate = convertToStandardDate(tpl.params[0]);
var tplExtra = tpl.params.slice(1).join('|').trim();
if (!RE_DATE_ISO.test(normalizedTplDate)) {
callback({ code: 'error', info: 'Не удалось распознать дату в шаблоне: «' + (tpl.params[0] || '') + '».' });
return;
}
var date = getDate(normalizedTplDate);
var nomPlace = 'ВП:' + job.sourceTemplate.replace(/\|.*/, '') + '/' + date[1];
var retTalkSection = '';
var sectionNW, tplpar, newTalkTpl;
if (job.closeType === 'doneRnm') { sectionNW = job.oldTitle + ' → ' + pg; tplpar = job.oldTitle + '|' + pg; }
if (job.closeType === 'noRnm') { sectionNW = pg + ' → ' + tplExtra; tplpar = pg + '|' + tplExtra; }
if (job.closeType === 'ret' || job.closeType === 'retConditional') {
retTalkSection = tplExtra;
sectionNW = retTalkSection || pg;
tplpar = retTalkSection ? ('l1=' + retTalkSection) : '';
}
var editSummary = makeSummary('номинация [[' + (nomPlace ? nomPlace + '#' : '') + sectionNW + ']] — ' + job.resultTemplate);
var talkTitle = getTalkPage(pg);
newTalkTpl = (job.closeType === 'retConditional')
? buildConditionalRetTemplateText(date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline, 1)
: (T_OPEN + job.resultTemplate + '|' + date[0] + '|' + tplpar + T_CLOSE);
getTextWithTimestamp(talkTitle, function (talkText, talkTimestamp, talkReadErr) {
if (talkReadErr) { callback(makeReadError(talkReadErr, 'talk_read_failed', 'Не удалось получить содержимое СО страницы «' + pg + '».')); return; }
var sourceTalkText = talkText || '';
var talkResult = (job.closeType === 'ret')
? upsertRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection)
: (job.closeType === 'retConditional')
? upsertConditionalRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline)
: { text: insertTplOnTalkPage(sourceTalkText, newTalkTpl, '\n'), status: 'created' };
function saveArticle() {
var cleaned = stripTemplatesByPattern(article, tplPattern).text;
var ep = { title: pg, text: cleaned, summary: editSummary, watchlist: 'nochange', assertuser: mwCfg.wgUserName };
if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (t) {
var editErr = t && t.error ? t.error : null;
callback(editErr, editErr ? null : {
discussionPage: nomPlace,
discussionSection: sectionNW,
summary: editSummary
});
});
}
if (talkResult.text === sourceTalkText) { saveArticle(); return; }
var talkEp = { title: talkTitle, text: talkResult.text, summary: editSummary };
if (talkTimestamp) talkEp.basetimestamp = talkTimestamp;
apiReq(talkEp, 'edit', function (talkResp) {
if (talkResp && talkResp.error) { callback({ code: 'talk_failed', info: 'Не удалось записать итог на СО: ' + talkResp.error.info }); return; }
saveArticle();
});
});
});
return;
}
// ── Обычная номинация: вставка шаблона в статью ─────────────────────
// mode === 'nominate'
var isKu = job.opId === 'tRm' || job.opId === 'mRm';
editPageContent(pg, { summary: job.summary, readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var hasExistingKu = isKu && RE_KU_ON_PAGE.test(article);
var conflictDecision = getConflictDecisionForPage(job, pg);
function buildResult(finalText) {
var generatedTpl = buildGeneratedNominationTemplateText(job, pg);
return { text: generatedTpl ? wrapInNoinclude(finalText, generatedTpl) : finalText };
}
function finishConflictResolution(sourceText) {
var resolvedText;
var pageLink = buildQuotedStatusPageLink(pg);
if (conflictDecision.templateAction === 'keep') {
if (sourceText !== article) {
return {
text: sourceText,
meta: { successMessage: 'Шаблон на странице ' + pageLink + ' оставлен без изменений; шаблоны переноса сняты.' }
};
}
return { skip: true, meta: { successMessage: 'Шаблон на странице ' + pageLink + ' оставлен без изменений.' } };
}
resolvedText = applyConflictTemplateResolution(sourceText, job, pg, conflictDecision);
return {
text: resolvedText,
meta: {
successMessage: conflictDecision.templateAction === 'overwrite'
? 'Шаблон КУ на странице ' + pageLink + ' перезаписан новой датой.'
: 'Новый шаблон КУ добавлен сверху на странице ' + pageLink + '.'
}
};
}
if (hasExistingKu && (!conflictDecision || conflictDecision.pageAction !== 'keep')) {
return { error: { code: 'error', info: 'На странице уже стоит шаблон КУ.' } };
}
if (hasExistingKu && conflictDecision && conflictDecision.pageAction === 'keep') {
if (job.transferMode && job.transferMode !== 'none') {
var localConflict = removeTransferTemplatesLocal(article, job.transferMode);
removeTransferTemplatesWithApiFallback(pg, localConflict.text, job.transferMode, localConflict, function (updated) {
done(finishConflictResolution(updated.text));
});
return;
}
return finishConflictResolution(article);
}
if (isKu && job.transferMode && job.transferMode !== 'none') {
var local = removeTransferTemplatesLocal(article, job.transferMode);
removeTransferTemplatesWithApiFallback(pg, local.text, job.transferMode, local, function (updated) { done(buildResult(updated.text)); });
return;
}
return buildResult(article);
},
function (err) { callback(err); }
);
}
/**
* Обрабатывает список страниц последовательно.
* @param {string[]} pages
* @param {Object} job
* @param {function} onDone(notifiedPages, err, pageMeta)
*/
function processPageList(pages, job, onDone) {
var notifiedPages = [];
var pageMeta = {};
eachSequential(pages.slice().reverse(), function (pg, nextPage) {
var pageLink = buildQuotedStatusPageLink(pg);
var statusId = logStatus('Обрабатывается страница ' + pageLink + '...', null, { pending: true, trackError: false });
applyTemplateToPage(pg, job, function (err, meta) {
var normPg = normTitle(pg);
var isClose = job.mode === 'cleanup' || job.mode === 'denom';
if (!isClose) {
if (!err && meta && meta.successMessage) logStatus(meta.successMessage, null, { statusId: statusId, trackError: false });
else logPageEdit(pg, err, { statusId: statusId });
} else {
if (err) { logStatus('Завершение по странице ' + pageLink, err, { statusId: statusId }); }
else {
logStatus('Шаблон снят со страницы ' + pageLink + '.', null, { statusId: statusId, trackError: false });
if (job.mode === 'denom') logStatus('Шаблон установлен на СО страницы ' + pageLink + '.', null, { trackError: false });
}
}
if (!err) {
notifiedPages.push(pg);
if (meta) pageMeta[normPg] = meta;
}
nextPage(err || null);
});
}, function (err) { onDone(notifiedPages, err, pageMeta); });
}
// ═══════════════════════════════════════════════════════════════════════════
// ПОСТРОЕНИЕ JOB из формы
// ═══════════════════════════════════════════════════════════════════════════
/**
* Строит объект job из данных формы для операции номинации (tRm, rnm, imp, merge, split, recov).
* @param {Object} op — запись из OPERATIONS
* @param {string} pg — целевая страница (уже разрешённая)
* @param {boolean} isMulti — режим мультиноминации
* @returns {Object|false} — job или false при ошибке ввода
*/
function buildNominationJob(op, pg, isMulti) {
var nom = op.nomination;
var date = getDate();
var msg = normalizeQuickPhraseValue($('#rmMsg').val());
var rawMsg = msg;
var opId = isMulti ? 'mRm' : op.id;
var tplpar = '';
var section, sectionNW, extraPages, multiArticles = [];
var multiHeaderText = '';
var multiArticleComments = {};
// Вычислить section и tplpar в зависимости от типа дополнительного ввода
if (nom.extraInput) {
var ei = nom.extraInput;
if (ei.type === 'rename') {
var rn = collectInputValues('.rmRenameInput');
if (!rn.length) { alert('Укажите новое название.'); return false; }
tplpar = rn[0] + (rn.length > 1 ? '||' + rn.slice(1).join('|') : '');
section = '[[:' + pg + ']] → ' + rn.map(function (n) { return '[[:' + n + ']]'; }).join(', ');
} else if (ei.type === 'merge') {
var mn = collectInputValues('.rmMergeInput');
if (!mn.length) { alert('Укажите статью для объединения.'); return false; }
tplpar = pg + '|' + mn.join('|');
extraPages = mn;
section = formatPagesWithAnd([pg].concat(mn));
} else if (ei.type === 'split') {
var sn = collectInputValues('.rmSplitInput');
if (!sn.length) { alert('Укажите статьи для разделения.'); return false; }
tplpar = formatPagesWithAnd(sn);
section = '[[:' + pg + ']] → ' + tplpar;
}
}
if (isMulti) {
var ttl = $('#rmHeader').val() || '';
var articles = collectInputValues('.rmArticleInput');
var multiFormat = $('.rmArticleMultiFormatBtn.is-active').data('rmMultiFormat') || 'sections';
multiArticleComments = collectMultiNominationComments();
multiHeaderText = ttl;
multiArticles = articles.slice();
section = ttl;
msg = multiFormat === 'list'
? buildMultiNominationListText(articles, rawMsg, multiArticleComments)
: buildMultiNominationText(articles, rawMsg, multiArticleComments);
}
if (!section) section = '[[:' + pg + ']]';
sectionNW = section.replace(/\[\[:/g, '').replace(/]]/g, '');
var nomPageDate = date[1];
var nomPage = nom.nomPage(nomPageDate);
var summary = makeSummary('номинация [[' + nomPage + '#' + sectionNW + ']]');
return {
mode: 'nominate',
opId: opId,
op: op,
date: date,
tplpar: tplpar,
articleTpl: nom.articleTpl || function () { return ''; },
inArticle: nom.inArticle !== false,
transferMode: (nom.supportsTransfer ? getTransferModeFromButtons() : 'none'),
summary: summary,
msg: msg,
nomPage: nomPage,
navTemplate: nom.navTemplate,
section: section,
sectionNW: sectionNW,
comment: nom.comment || '',
extraPages: extraPages || [],
isMulti: !!isMulti,
multiHeaderText: multiHeaderText,
multiNominationBody: rawMsg,
multiArticleComments: multiArticleComments,
multiNominationFormat: multiFormat || 'sections',
multiArticles: multiArticles,
pages: isMulti ? multiArticles.slice().reverse() : ([pg].concat(extraPages || []))
};
}
function getTransferModeFromButtons() {
var kbu = $('#rmTransferBtnKbu').hasClass('is-active');
var kul = $('#rmTransferBtnKul').hasClass('is-active');
if (kbu && kul) return 'both';
if (kbu) return 'kbu';
if (kul) return 'kul';
return 'none';
}
function buildKbuFormHtml(reasons) {
return joinHtml([
'<select id="rmSel" style="', stInputFull, '">',
reasons.map(function (r, i) { return '<option value="' + i + '">' + r[1] + '</option>'; }).join(''),
'</select>',
'<input id="fiRm" type="hidden" style="', stInputFull, '">',
'<input id="fiRmComment" type="text" placeholder="Комментарий (необязательно)" style="', stInputFull, '">',
buildQuickPhrasesPanelHtml('fiRmComment')
]);
}
function buildNominationMultiHeaderHtml(pg, options) {
var opts = options || {};
var multiHeaderRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
return joinHtml([
'<div id="rmMultiHeader" class="', RESIZE_CLASS, '" style="display:none;margin-bottom:6px;">',
'<div class="rmArticleRow rmNominationHeaderRow" style="', multiHeaderRowStyle, '"><input id="rmHeader" type="text" placeholder="Заголовок номинации" style="', stInputBox, '">',
buildAddArticleButtonHtml(opts), '</div>',
'</div>',
'<div id="rmArticlesContainer" style="display:flex;flex-direction:column;gap:', multiNominationGap, ';margin-bottom:', multiNominationGap, ';">',
buildMultiArticleRowHtml(0, $.extend({}, opts, { rowId: 'rmFirstArticle', articleValue: pg, showAdd: true })),
'</div>'
]);
}
function buildTransferBoxHtml() {
return joinHtml([
'<div id="rmTransferBox" class="', RESIZE_CLASS, ' rmTransferPanel" style="width:100%;box-sizing:border-box;"><div class="rmTransferGrid">',
'<div id="rmTransferModeSingle" class="rmSegmentedBar"><button type="button" id="rmTransferBtnNone" class="rmSegmentedBtn rmToggleBtn is-active" aria-pressed="true">Обычная номинация</button></div>',
'<div id="rmTransferModeGroup" class="rmSegmentedBar">',
'<button type="button" id="rmTransferBtnKbu" class="rmSegmentedBtn rmToggleBtn" aria-pressed="false" title="Шаблоны db-*, уд-*, КОУ, Hangon.">Снять КБУ</button>',
'<button type="button" id="rmTransferBtnKul" class="rmSegmentedBtn rmToggleBtn" aria-pressed="false" title="Шаблон «К улучшению».">Снять КУЛ</button>',
'</div>',
'<div class="rmTransferHintRow"><div id="rmTransferHint" style="display:none;font-size:12px;line-height:1.35;color:', tk.cSubM, ';"></div></div>',
'</div></div>'
]);
}
function buildMultiNominationFormatSwitchHtml(wrapId, buttonClass) {
return joinHtml([
'<div id="', wrapId, '" class="', RESIZE_CLASS, '" style="display:none;margin-top:8px;margin-bottom:10px;">',
'<div class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn ', buttonClass, ' is-active" data-rm-multi-format="sections" aria-pressed="true">Оформить подразделами</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn ', buttonClass, '" data-rm-multi-format="list" aria-pressed="false">Оформить списком</button>',
'</div>',
'</div>'
]);
}
function buildNominationFormHtml(nom, pg, multiMode) {
return joinHtml([
multiMode ? buildNominationMultiHeaderHtml(pg) : '',
nom.extraInput ? buildMultiInputHtml(nom.extraInput) : '',
'<textarea id="rmMsg" placeholder="Текст номинации без подписи"></textarea>',
buildQuickPhrasesPanelHtml('rmMsg'),
nom.supportsTransfer ? buildTransferBoxHtml() : '',
multiMode ? buildMultiNominationFormatSwitchHtml('rmArticleMultiFormatWrap', 'rmArticleMultiFormatBtn') : ''
]);
}
function buildCategoryNominationFormHtml(variantConfig, multiMode, pageName) {
return joinHtml([
multiMode ? buildNominationMultiHeaderHtml(pageName, {
inputPlaceholder: 'Категория',
addTitle: 'Добавить категорию',
removeTitle: 'Удалить категорию',
commentPlaceholder: 'Комментарий только для этой категории (необязательно)'
}) : '',
variantConfig ? buildMultiInputHtml(variantConfig) : '',
'<textarea id="nominationReason" placeholder="Текст номинации без подписи"></textarea>',
buildQuickPhrasesPanelHtml('nominationReason'),
multiMode ? buildMultiNominationFormatSwitchHtml('rmCategoryMultiFormatWrap', 'rmCategoryMultiFormatBtn') : ''
]);
}
function ensureMultiPageCommentTextareaResizer(textareaId, wrapId) {
var $textarea = $('#' + textareaId);
if (!$textarea.length || $textarea.data('rmNestedResizerReady')) return;
setupNestedResizableTextarea(textareaId, wrapId, parseInt(sz.taMinW, 10) || 180, 90);
$textarea.data('rmNestedResizerReady', true);
}
function setMultiPageCommentExpanded($btn, expanded) {
var wrapId = $btn.data('rmCommentWrap');
var textareaId = $btn.data('rmCommentTextarea');
var $wrap = $('#' + wrapId);
if (!$btn.length || !$wrap.length) return;
$btn.attr('aria-expanded', expanded ? 'true' : 'false')
.toggleClass('is-active', expanded)
.text(expanded ? 'Скрыть комментарий' : 'Комментарий');
$wrap.toggle(expanded);
if (expanded) ensureMultiPageCommentTextareaResizer(textareaId, wrapId);
}
function getMultiPageCommentTargets($block) {
var $textarea = $block.find('.rmArticleCommentInput').first();
var $wrap = $textarea.parent();
return {
wrapId: $wrap.attr('id') || '',
textareaId: $textarea.attr('id') || ''
};
}
function setMultiPageRowControls($block, showAdd, showComment, options) {
var opts = options || {};
var $row = $block.find('.rmArticleRow').first();
var ids = getMultiPageCommentTargets($block);
var $commentBtn = $row.find('.rmArticleCommentToggle');
if (!$row.length) return;
if (showAdd) {
if ($commentBtn.length) setMultiPageCommentExpanded($commentBtn, false);
if (ids.wrapId) $('#' + ids.wrapId).hide();
$row.find('.rmArticleCommentToggle,.rmRemoveInput').remove();
if (!$row.find('.rmAddArticle').length) $row.append(buildAddArticleButtonHtml(opts));
return;
}
$row.find('.rmAddArticle').remove();
if (!$row.find('.rmArticleCommentToggle').length) {
$row.append(buildMultiArticleButtonsHtml(ids.wrapId, ids.textareaId, $.extend({}, opts, { showComment: showComment })));
return;
}
$row.find('.rmArticleCommentToggle').toggle(showComment);
}
function setupMultiPageNominationUi(options) {
var opts = options || {};
var containerSelector = opts.containerSelector || '#rmArticlesContainer';
var pageCounter = parseInt(opts.nextIndex, 10) || 1;
var wasMultiModeExpanded = false;
function restoreEmptySinglePageInput() {
var $pageInput = $(containerSelector + ' .rmArticleInput').first();
if (!$pageInput.length || String($pageInput.val() || '').trim()) return;
$pageInput.val(opts.defaultPage || '');
}
function updateMultiMode() {
var $blocks = $(containerSelector + ' .rmMultiArticleBlock');
var hasExtra = $blocks.length > 1;
$('#rmMultiHeader').toggle(hasExtra);
if (opts.multiOnlySelector) $(opts.multiOnlySelector).toggle(hasExtra);
if (!hasExtra && wasMultiModeExpanded) restoreEmptySinglePageInput();
$blocks.each(function (index) {
setMultiPageRowControls($(this), !hasExtra && index === 0, hasExtra, opts);
});
wasMultiModeExpanded = hasExtra;
syncModalLayout();
}
$(document).off('click.rmMultiPageAdd').on('click.rmMultiPageAdd', '.rmAddArticle', function () {
$(containerSelector).append(buildMultiArticleRowHtml(pageCounter++, opts));
updateMultiMode();
});
$(document).off('click.rmMultiPageComment').on('click.rmMultiPageComment', '.rmArticleCommentToggle', function () {
var $btn = $(this);
if (!$btn.is(':visible')) return;
setMultiPageCommentExpanded($btn, $btn.attr('aria-expanded') !== 'true');
syncModalLayout();
});
$(document).off('click.rmMultiPageRemove').on('click.rmMultiPageRemove', '.rmArticleRow .rmRemoveInput', function () {
$(this).closest('.rmMultiArticleBlock').remove();
updateMultiMode();
});
updateMultiMode();
return {
update: updateMultiMode,
isMulti: function () { return $(containerSelector + ' .rmMultiArticleBlock').length > 1; }
};
}
function bindMultiNominationFormatSwitch(rootSelector, buttonSelector) {
$(rootSelector).off('click.rmMultiFormat', buttonSelector).on('click.rmMultiFormat', buttonSelector, function () {
var $btn = $(this);
$(buttonSelector).removeClass('is-active').attr('aria-pressed', 'false');
$btn.addClass('is-active').attr('aria-pressed', 'true');
});
}
function buildProtectAddButtonHtml() {
return buildSquareAddButtonHtml('', 'Добавить страницу', 'rmProtectAddPage');
}
function buildProtectPageRowHtml(id, pageName, isFirstRow) {
return joinHtml([
'<div', isFirstRow ? ' id="rmProtectFirstRow"' : '', ' class="rmProtectPageRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="', id, '" type="text" placeholder="Страница" class="rmProtectPageInput" style="', stInputBox, '"',
pageName ? ' value="' + escapeHtml(pageName) + '"' : '', '>',
isFirstRow
? buildProtectAddButtonHtml()
: '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>',
'</div>'
]);
}
function buildReportFormHtml(ctx, isZka) {
var reportTextPlaceholder = (isZka ? 'Введите текст запроса.' : 'Текст запроса (можно дополнить или изменить).') + ' Подпись будет добавлена автоматически.';
if (isZka) {
return joinHtml([
'<input id="rmReportHeader" type="text" placeholder="Тема/заголовок" style="', stInputFull, '" value="', escapeHtml(ctx.pageLink), '">',
'<textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>',
buildQuickPhrasesPanelHtml('rmReportText')
]);
}
return joinHtml([
'<div id="rmProtectModeBtns" class="', RESIZE_CLASS, '" style="margin-bottom:14px;"><div class="rmSegmentedBar">',
'<button id="rmProtectModeInstall" type="button" class="rmSegmentedBtn rmProtectModeBtn is-active" aria-pressed="true">🛡️ Установить защиту</button>',
'<button id="rmProtectModeRemove" type="button" class="rmSegmentedBtn rmProtectModeBtn" aria-pressed="false">📛 Снять защиту</button>',
'</div></div>',
'<div id="rmProtectMultiWrap" class="', RESIZE_CLASS, '">',
'<div id="rmProtectHeaderWrap" style="display:none;margin-bottom:6px;"><div class="rmProtectHeaderRow" style="', stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'), '"><input id="rmProtectHeader" type="text" placeholder="Заголовок (для нескольких страниц)" style="', stInputBox, '">', buildProtectAddButtonHtml(), '</div></div>',
buildProtectPageRowHtml('rmProtectPage0', ctx.pageName, true),
'<div id="rmProtectPagesContainer"></div>',
'</div>',
'<div id="rmProtectLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmProtectLevels" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>',
'</div></div>',
'<div id="rmProtectReasonsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Причины</div><div id="rmProtectReasons" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="война правок">война правок</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="неконсенсусные изменения">неконсенсусные изменения</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="вандализм">вандализм</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="популярная статья">популярная статья</button>',
'</div></div>',
'<div id="rmRemoveLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup" style="display:none;"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmRemoveLevels" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>',
'</div></div>',
'<div id="rmProtectTextBlock" class="', RESIZE_CLASS, '"><textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>',
buildQuickPhrasesPanelHtml('rmReportText'), '</div>'
]);
}
// ═══════════════════════════════════════════════════════════════════════════
// ОБРАБОТЧИКИ ОПЕРАЦИЙ
// ═══════════════════════════════════════════════════════════════════════════
var handlers = {
// ── КБУ ─────────────────────────────────────────────────────────────
showKbu: function (op) {
var forCategory = !!(op && op.forCategory);
var reasons = getFastRemoveReasons();
createModal({
title: 'Быстрое удаление',
width: 'compact',
subtitleHtml: '<span id="rmKbuCriteriaLinkWrap"></span>'
});
$('#removerModalContent').html(buildKbuFormHtml(reasons));
function updateKbuReasonControls() {
var reason = reasons[$('#rmSel').val()] || reasons[0];
var paramCfg = reason ? cfg.requiredParamTemplates[reason[0]] : null;
var showComment = true;
$('#rmKbuCriteriaLinkWrap').html(buildFastRemoveCriteriaLinkHtml(reason));
if (paramCfg) {
var noComment = paramCfg.charAt(0) === '!';
$('#fiRm').attr({ type: 'text', placeholder: 'Укажите ' + (noComment ? paramCfg.substring(1) : paramCfg) }).show();
showComment = !noComment;
} else {
$('#fiRm').attr('type', 'hidden').hide();
}
$('#fiRmComment').toggle(showComment);
$('.rmQuickPhrasesPanel[data-rm-target="fiRmComment"]').toggle(showComment);
}
$('#rmSel').change(updateKbuReasonControls);
$('#rmSel').trigger('change');
renderModalFooter('submit', {
submitText: 'Номинировать',
onSubmit: function () {
var idx = $('#rmSel').val();
var addInfo = $('#fiRm').val();
var comment = $('#fiRmComment').val();
startProcessing();
if (forCategory) {
var tpl = reasons[idx][0];
var categorySummary = makeSummary('номинация категории на быстрое удаление');
if (addInfo) tpl += '|1=' + addInfo;
if (comment) tpl += '|' + (addInfo ? '2' : '1') + '=' + comment;
editPageContent(mwCfg.wgPageName, { summary: categorySummary, readError: 'Не удалось получить содержимое.' },
function (text) { return { text: wrapInNoinclude(text, T_OPEN + tpl + T_CLOSE) }; },
function (err) {
if (err) {
unlockModalSubmit();
logStatus('Ошибка записи.', err);
} else {
logStatus('Страница номинирована к быстрому удалению.', null, { trackError: false });
finalizeFastRemoval([normTitle(mwCfg.wgPageName)], categorySummary);
}
});
} else {
var job = {
mode: 'nominate', opId: 'fRm',
kbuTemplate: reasons[idx][0], kbuAddInfo: addInfo, kbuComment: comment,
summary: makeSummary('номинация к [[ВП:КБУ|быстрому удалению]]'),
inArticle: true
};
processPageList([normTitle(mwCfg.wgPageName)], job, function (notifiedPages) {
finalizeFastRemoval(notifiedPages, job.summary);
});
}
return true;
}
});
},
// ── Универсальная номинация (КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС) ────────
showNomination: function (op) {
var nom = op.nomination;
var pg = normTitle(mwCfg.wgPageName);
var date = getDate()[1];
var nomPage = nom.nomPage(date);
var multiMode = nom.supportsMulti;
function updateTransferUi() {
var mode = getTransferModeFromButtons();
var isNone = mode === 'none';
var isKbu = mode === 'kbu' || mode === 'both';
var isKul = mode === 'kul' || mode === 'both';
$('#rmTransferBtnNone').toggleClass('is-active', isNone).attr('aria-pressed', isNone ? 'true' : 'false');
$('#rmTransferBtnKbu').toggleClass('is-active', isKbu).attr('aria-pressed', isKbu ? 'true' : 'false');
$('#rmTransferBtnKul').toggleClass('is-active', isKul).attr('aria-pressed', isKul ? 'true' : 'false');
var t = transferTexts[mode];
if (t) { $('#rmTransferHint').text(t.hint).show(); } else { $('#rmTransferHint').hide().text(''); }
applyGeneratedText($('#rmMsg'), t && t.notice ? t.notice + '\n' : '');
}
createModal({
title: 'Номинация: ' + nom.template,
subtitlePage: nomPage,
subtitleLabel: 'Текущий день'
});
$('#removerModalContent').html(buildNominationFormHtml(nom, pg, multiMode));
setupResizableModal('rmMsg');
// Логика переноса
if (nom.supportsTransfer) {
$(document).off('click.rmTransfer').on('click.rmTransfer', '#rmTransferBtnNone,#rmTransferBtnKbu,#rmTransferBtnKul', function () {
if (this.id === 'rmTransferBtnNone') {
$('#rmTransferBtnKbu,#rmTransferBtnKul').removeClass('is-active');
$('#rmTransferBtnNone').addClass('is-active');
} else {
$(this).toggleClass('is-active');
var anyOn = $('#rmTransferBtnKbu').hasClass('is-active') || $('#rmTransferBtnKul').hasClass('is-active');
$('#rmTransferBtnNone').toggleClass('is-active', !anyOn);
}
updateTransferUi();
});
updateTransferUi();
}
// Многостраничный режим
if (multiMode) {
setupMultiPageNominationUi({ defaultPage: pg, multiOnlySelector: '#rmArticleMultiFormatWrap' });
bindMultiNominationFormatSwitch('#removerModalContent', '.rmArticleMultiFormatBtn');
}
if (nom.extraInput) wireMultiInput(nom.extraInput);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var isMulti = multiMode && $('#rmArticlesContainer .rmMultiArticleBlock').length > 1;
var inputVal = !isMulti ? normTitle($('#rmArticlesContainer .rmArticleInput').first().val() || '') : '';
var changed = inputVal && inputVal !== pg;
function executeJob(job) {
startProcessing();
runFlow({
templateStep: function (next) {
if (!job.inArticle) { next(); return; }
processPageList(job.pages, job, function (notifiedPages, err) {
job._notifiedPages = notifiedPages;
next(err);
});
},
nominationStep: function (done) {
publishNomination({
pageTitle: job.nomPage,
navTemplate: job.navTemplate,
sectionTitle: job.section,
summary: job.summary,
text: getNominationPublishText(job)
}, function (err) { done(err, { pageTitle: job.nomPage, sectionTitle: job.section }); });
},
notifyStep: function (nominationInfo, next) {
var pages = job._notifiedPages || [];
if (!setAlert || !pages.length) { next(); return; }
notifyAuthorsForPages(pages, {
summary: job.summary,
actionText: job.comment,
discussionPage: nominationInfo && nominationInfo.pageTitle,
discussionSection: nominationInfo && nominationInfo.sectionTitle
}, next);
},
skipLink: op.id === 'fRm'
});
}
function run(targetPg) {
var job = buildNominationJob(op, targetPg, isMulti);
if (!job) { unlockModalSubmit(); return; }
if (job.isMulti && job.inArticle && getNominationConflictRule(job)) {
startProcessing();
inspectMultiNominationConflicts(job, function (err, conflicts) {
if (err) { markSubmitError(); return; }
if (!conflicts.length) { executeJob(job); return; }
showNominationConflictResolution(job, conflicts, function (resolvedJob) {
executeJob(resolvedJob);
});
});
return;
}
executeJob(job);
}
if (changed) {
apiReq({ prop: 'info', titles: inputVal }, 'query', function (data) {
if (data && data.error) {
unlockModalSubmit();
alert('Не удалось проверить страницу из-за ошибки API. Попробуйте ещё раз.');
return;
}
var page = getFirstQueryPage(data);
if (!page) {
unlockModalSubmit();
alert('Не удалось проверить страницу из-за временной ошибки. Попробуйте ещё раз.');
return;
}
if (page.missing !== undefined) {
unlockModalSubmit();
alert('Страница «' + inputVal + '» не существует.');
return;
}
run(normTitle(page.title || inputVal));
});
} else {
run(pg);
}
return true;
}
});
},
// ── Снятие номинации (статья) ────────────────────────────────────────
showArticleClose: function () {
showCloseActionsModal({
inputName: 'rmCloseAction',
listId: 'rmCloseActions',
emptyText: 'Не найдено подходящих шаблонов для закрытия.',
emptyDetails: 'Проверяются: КУ, КПМ, КБУ, КУЛ.',
getActions: function (articleText) {
var actions = [];
if (RE_KU_ON_PAGE.test(articleText)) actions.push({ id: 'ret', tag: 'КУ', label: 'Оставлено', mode: 'denom', closeType: 'ret', resultTemplate: 'оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{оставлено}} на СО.', comment: 'оставлена', talkNotice: true });
if (RE_KU_ON_PAGE.test(articleText)) actions.push({ id: 'retConditional', tag: 'КУ', label: 'Условно оставлено', mode: 'denom', closeType: 'retConditional', resultTemplate: 'условно оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{условно оставлено}} на СО.', comment: 'условно оставлена', talkNotice: true, needsConditionalFields: true });
if (RE_KPM_ON_PAGE.test(articleText)) actions.push({ id: 'doneRnm', tag: 'КПМ', label: 'Переименовано', mode: 'denom', closeType: 'doneRnm', resultTemplate: 'переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{переименовано}} на СО.', comment: 'переименована', talkNotice: true, needsOldTitle: true });
if (RE_KPM_ON_PAGE.test(articleText)) actions.push({ id: 'noRnm', tag: 'КПМ', label: 'Не переименовано', mode: 'denom', closeType: 'noRnm', resultTemplate: 'не переименовано',sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{не переименовано}} на СО.', comment: 'не переименована',talkNotice: true });
var hasKbu = RE_KBU_ON_PAGE.test(articleText);
var hasKul = RE_KUL_ON_PAGE.test(articleText);
if (hasKbu && hasKul) actions.push({ id: 'cleanup-both', tag: 'КБУ и КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'both', cleanupLabel: 'КБУ и КУЛ', description: 'Снимает шаблоны КБУ/КУЛ и Hangon.' });
if (hasKbu) actions.push({ id: 'cleanup-kbu', tag: 'КБУ', label: 'Снятие', mode: 'cleanup', transferMode: 'kbu', cleanupLabel: 'КБУ', description: 'Снимает шаблоны КБУ и Hangon.' });
if (hasKul) actions.push({ id: 'cleanup-kul', tag: 'КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'kul', cleanupLabel: 'КУЛ', description: 'Снимает шаблон КУЛ.' });
return actions;
},
afterRender: function (actions) {
var hasDoneRnm = actions.some(function (a) { return a.id === 'doneRnm'; });
var hasConditionalRet = actions.some(function (a) { return a.id === 'retConditional'; });
if (hasDoneRnm) {
$('#rmCloseActions input[value="doneRnm"]').closest('.rmActionItem').append(
'<div id="rmCloseOldTitleWrap" style="display:none;margin-top:6px;"><input id="rmCloseOldTitle" type="text" placeholder="Старое название" style="' + stInputFull + '"></div>'
);
}
if (hasConditionalRet) {
$('#rmCloseActions input[value="retConditional"]').closest('.rmActionItem').append(buildConditionalRetFieldsHtml());
}
},
afterFooterRender: function (_, actionMap) {
function ensureConditionalTextareaResizer() {
var $textarea = $('#rmCloseConditionalReason');
if (!$textarea.length || $textarea.data('rmConditionalResizerReady')) return;
setupNestedResizableTextarea('rmCloseConditionalReason', 'rmCloseConditionalWrap', 280, 90);
$textarea.data('rmConditionalResizerReady', true);
}
function updateUi() {
var sel = actionMap[$('[name="rmCloseAction"]:checked').val()];
$('#rmCloseOldTitleWrap').toggle(!!(sel && sel.needsOldTitle));
$('#rmCloseConditionalWrap').toggle(!!(sel && sel.needsConditionalFields));
if (sel && sel.needsConditionalFields) ensureConditionalTextareaResizer();
var disableNotify = !!(sel && sel.mode === 'cleanup' && sel.transferMode === 'kbu');
var $cb = $('[name="rmUAlert"]');
var $cbLabel = $('[name="rmUAlert"]').closest('label');
if ($cb.length) $cb.prop('disabled', disableNotify);
if ($cbLabel.length) $cbLabel.css({
visibility: disableNotify ? 'hidden' : 'visible',
pointerEvents: disableNotify ? 'none' : ''
});
syncModalLayout();
}
$(document).off('change.rmCloseAction').on('change.rmCloseAction', '[name="rmCloseAction"]', updateUi);
updateUi();
},
onSubmit: function (sel, pageName) {
var job;
if (sel.mode === 'denom') {
var oldTitle = sel.needsOldTitle ? ($('#rmCloseOldTitle').val() || '').trim() : '';
var conditionalReason = sel.needsConditionalFields ? normalizeQuickPhraseValue($('#rmCloseConditionalReason').val()) : '';
var conditionalDeadline = sel.needsConditionalFields ? String($('#rmCloseConditionalDeadline').val() || '').trim() : '';
if (sel.needsOldTitle && !oldTitle) { alert('Укажите старое название.'); return false; }
if (conditionalDeadline && normalizeIsoDate(conditionalDeadline) !== conditionalDeadline) {
alert('Укажите срок в формате YYYY-MM-DD, например 2026-05-31.');
return false;
}
job = {
mode: 'denom',
closeType: sel.closeType,
resultTemplate: sel.resultTemplate,
sourceTemplate: sel.sourceTemplate,
oldTitle: oldTitle,
conditionalReason: conditionalReason,
conditionalDeadline: conditionalDeadline,
notifyActionText: sel.comment,
skipNotify: false
};
} else {
job = {
mode: 'cleanup',
transferMode: sel.transferMode,
summary: makeSummary('снятие шаблонов ' + sel.cleanupLabel),
notifyActionText: (sel.transferMode === 'kul' || sel.transferMode === 'both')
? 'больше не номинирована к срочному улучшению'
: '',
skipNotify: !(sel.transferMode === 'kul' || sel.transferMode === 'both')
};
}
processPageList([pageName], job, function (notifiedPages, err, pageMeta) {
function finishClose() {
if (isError) { markSubmitError(); }
else { renderModalFooter('reload'); }
}
if (isError || err || job.skipNotify || !setAlert || !notifiedPages.length) { finishClose(); return; }
var meta = (pageMeta && pageMeta[normTitle(pageName)]) || {};
notifyAuthorsForPages(notifiedPages, {
summary: meta.summary || job.summary,
actionText: job.notifyActionText,
discussionPage: meta.discussionPage,
discussionSection: meta.discussionSection,
includeProposedPrefix: false
}, finishClose);
});
return true;
}
});
},
// ── ОБКАТ: номинация категории ───────────────────────────────────────
showCatNomination: function (op) {
var catType = op.catType;
var titles = { discuss: 'Номинация: обсуждение', deletion: 'Номинация: к удалению', rename: 'Номинация: к переименованию', merge: 'Номинация: к объединению' };
var now = new Date();
var catDiscPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[now.getUTCMonth()] + '_' + now.getUTCFullYear();
var pageName = normalizeCategoryPageName(mwCfg.wgPageName);
var multiMode = catType === 'deletion';
createModal({ title: titles[catType], subtitlePage: catDiscPage, subtitleLabel: 'Текущий месяц' });
var variantCfgs = {
rename: { firstId: 'firstRenameInput', addBtnId: 'addRenameVariant', containerId: 'renameVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Новое название без префикса Категория:', addPh: 'Дополнительный вариант названия' },
merge: { firstId: 'firstMergeInput', addBtnId: 'addMergeVariant', containerId: 'mergeVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Категория для объединения без префикса Категория:', addPh: 'Дополнительный вариант объединения' }
};
var vCfg = variantCfgs[catType];
$('#removerModalContent').html(buildCategoryNominationFormHtml(vCfg, multiMode, pageName));
setupResizableModal('nominationReason');
if (multiMode) {
setupMultiPageNominationUi({
defaultPage: pageName,
inputPlaceholder: 'Категория',
addTitle: 'Добавить категорию',
removeTitle: 'Удалить категорию',
commentPlaceholder: 'Комментарий только для этой категории (необязательно)',
multiOnlySelector: '#rmCategoryMultiFormatWrap'
});
bindMultiNominationFormatSwitch('#removerModalContent', '.rmCategoryMultiFormatBtn');
}
if (vCfg) wireMultiInput(vCfg);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var reason = normalizeQuickPhraseValue($('#nominationReason').val());
var targetPages = multiMode ? collectCategoryPageInputValues('.rmArticleInput') : [pageName];
var isMulti = multiMode && targetPages.length > 1;
var multiFormat = $('.rmCategoryMultiFormatBtn.is-active').data('rmMultiFormat') || 'sections';
var commentsByCategory = isMulti ? collectMultiNominationComments(normalizeCategoryPageName) : {};
var discussionTarget = isMulti ? targetPages : targetPages[0];
var discussionReason = isMulti
? (multiFormat === 'list'
? buildMultiNominationListText(targetPages, reason, commentsByCategory)
: buildMultiNominationText(targetPages, reason, commentsByCategory, { headingLevel: 4 }))
: reason;
var discussionOptions = isMulti ? { headerText: $('#rmHeader').val(), reasonIsPrepared: true } : null;
var notifiedPages = [];
if (!reason) { alert('Пожалуйста, укажите причину/тему.'); return false; }
if (!targetPages.length) { alert('Укажите категорию.'); return false; }
var mainName = null, additionalNames = [];
if (vCfg) {
mainName = $('#' + vCfg.firstId).val().trim();
if (!mainName) { alert('Укажите ' + (catType === 'rename' ? 'новое название' : 'категорию для объединения') + '.'); return false; }
additionalNames = collectInputValues('#' + vCfg.containerId + ' .variantInput');
}
startProcessing();
runFlow({
templateStep: function (next) {
addTemplatesToCategories(targetPages, catType, mainName, additionalNames, function (err, processedPages) {
notifiedPages = processedPages || [];
next(err);
});
},
nominationStep: function (done) {
createCategoryDiscussion(discussionTarget, discussionReason, catType, function (err, nominationInfo) { done(err, nominationInfo || null); }, mainName, additionalNames, discussionOptions);
},
notifyStep: function (nominationInfo, next) {
if (!setAlert || !nominationInfo) { next(); return; }
var section = normalizeSectionForLink(nominationInfo.sectionTitle || '');
notifyAuthorsForPages(notifiedPages.length ? notifiedPages : targetPages, {
summary: makeSummary('номинация [[' + nominationInfo.pageTitle + (section ? '#' + section : '') + ']]'),
actionText: { discuss: 'к обсуждению', deletion: 'к удалению', rename: 'к переименованию', merge: 'к объединению' }[catType] || 'к обсуждению',
discussionPage: nominationInfo.pageTitle,
discussionSection: nominationInfo.sectionTitle
}, next);
}
});
return true;
}
});
},
// ── Снятие номинации (категория) ─────────────────────────────────────
showCatClose: function () {
showCloseActionsModal({
inputName: 'rmCategoryCloseAction',
showCheckbox: false,
emptyText: 'Не найдено подходящих шаблонов для завершения.',
emptyDetails: 'Проверяются ОБКАТ и КУ.',
getActions: function (catText) {
var allObkat = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|');
var actions = [];
if (new RegExp('\\{\\{\\s*(?:' + allObkat + ')\\s*(?:\\||\\}\\})', 'i').test(catText)) {
actions.push({ id: 'cat-obkat-done', tag: 'ОБКАТ', label: 'Завершено', mode: 'obkat', talkTemplate: 'Обсуждавшаяся категория', description: 'Снимает шаблон ОБКАТ, добавляет {{Обсуждавшаяся категория}} на СО.', talkNotice: true });
}
if (RE_KU_ON_PAGE.test(catText)) {
actions.push({ id: 'cat-ku-cleanup', tag: 'КУ', label: 'Снятие', mode: 'cleanup', description: 'Снимает шаблон КУ без записи на СО.' });
}
return actions;
},
onSubmit: function (sel, pageName) {
if (sel.mode === 'obkat') markCategoryDiscussionAsDone(pageName);
if (sel.mode === 'cleanup') removeKuFromCategory(pageName);
return true;
}
});
},
// ── Защита / Запрос к администраторам ───────────────────────────────
showReport: function (op) {
var mode = op.reportMode || 'protect';
var ctx = getReporterContext(mode);
var isZka = mode === 'request';
var protectMode = 'install';
var pageCounter = 1;
function buildProtectText(pm) {
if (pm === 'remove') {
var removeLevels = [];
$('#rmRemoveLevels .rmToggleBtn.is-active').each(function () { removeLevels.push($(this).data('label')); });
return removeLevels.length ? 'Просьба снять ' + removeLevels.join(' и/или ') + '.' : '';
}
var levels = [], reasons = [];
$('#rmProtectLevels .rmToggleBtn.is-active').each(function () { levels.push($(this).data('label')); });
$('#rmProtectReasons .rmToggleBtn.is-active').each(function () { reasons.push($(this).data('label')); });
if (!levels.length && !reasons.length) return '';
var text = 'Просьба установить';
if (levels.length) text += ' ' + levels.join(' и/или ');
if (reasons.length) text += ' по причине: ' + reasons.join(', ');
return text + '.';
}
function applyProtectMode(m) {
protectMode = m;
var isInstall = m === 'install';
$('#rmProtectModeInstall').toggleClass('is-active', isInstall).attr('aria-pressed', isInstall ? 'true' : 'false');
$('#rmProtectModeRemove').toggleClass('is-active', !isInstall).attr('aria-pressed', !isInstall ? 'true' : 'false');
$('#removerModalTitleText').text(isInstall ? 'Запрос на защиту страницы' : 'Запрос на снятие защиты');
var linkPage = isInstall ? 'Википедия:Установка защиты' : 'Википедия:Снятие защиты';
$('#rmProtectLinkWrap').html('<a href="' + getPageUrl(linkPage) + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + escapeHtml(linkPage) + '</a>');
$('#rmProtectLevelsWrap,#rmProtectReasonsWrap').toggle(isInstall);
$('#rmRemoveLevelsWrap').toggle(!isInstall);
$('#rmProtectLevels .rmProtectOptBtn,#rmProtectReasons .rmProtectOptBtn,#rmRemoveLevels .rmProtectOptBtn').removeClass('is-active');
$('#rmReportText').val('').removeData('rmGenerated');
}
function updateProtectMultiUi() {
var $rows = $('#rmProtectMultiWrap .rmProtectPageRow');
var hasExtra = $rows.length > 1;
if (!$rows.length) {
$('#rmProtectPagesContainer').append(buildProtectPageRowHtml('rmProtectPage' + pageCounter++, ctx.pageName, true));
$rows = $('#rmProtectMultiWrap .rmProtectPageRow');
hasExtra = false;
}
$('#rmProtectHeaderWrap').toggle(hasExtra);
if (!hasExtra) $('#rmProtectHeader').val('');
$rows.each(function () {
var $row = $(this);
$row.find('.rmProtectAddPage,.rmRemoveInput').remove();
$row.append(hasExtra
? '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>'
: buildProtectAddButtonHtml()
);
});
syncModalLayout();
}
createModal({
title: isZka ? 'Запрос к администраторам' : 'Запрос на защиту страницы',
width: 'compact',
subtitleHtml: isZka
? '<a href="' + getPageUrl('Википедия:Запросы к администраторам') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Запросы к администраторам</a>' +
' · <a href="' + getPageUrl('Википедия:Запросы к администраторам/Быстрые') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Быстрые</a>'
: '<span id="rmProtectLinkWrap"></span>'
});
$('#removerModalContent').html(buildReportFormHtml(ctx, isZka));
if (!isZka) {
$('#removerModalContent')
.on('click', '#rmProtectModeInstall', function () { applyProtectMode('install'); })
.on('click', '#rmProtectModeRemove', function () { applyProtectMode('remove'); })
.on('click', '.rmProtectOptBtn', function () {
$(this).toggleClass('is-active');
if (protectMode === 'install') {
var $levels = $('#rmProtectLevels .rmProtectOptBtn.is-active');
if ($(this).closest('#rmProtectReasons').length && $(this).hasClass('is-active') && $levels.length === 0) {
$('#rmProtectLevels .rmProtectOptBtn').first().addClass('is-active');
}
if ($(this).closest('#rmProtectLevels').length && !$(this).hasClass('is-active') && $levels.length === 0) {
$('#rmProtectReasons .rmProtectOptBtn').removeClass('is-active');
}
}
applyGeneratedText($('#rmReportText'), buildProtectText(protectMode));
});
$(document)
.off('click.rmProtectAdd').on('click.rmProtectAdd', '.rmProtectAddPage', function () {
var id = 'rmProtectPage' + pageCounter++;
$('#rmProtectPagesContainer').append(buildProtectPageRowHtml(id, '', false));
updateProtectMultiUi();
})
.off('click.rmProtectRemove').on('click.rmProtectRemove', '.rmProtectPageRow .rmRemoveInput', function () {
$(this).closest('.rmProtectPageRow').remove(); updateProtectMultiUi();
});
applyProtectMode('install');
}
setupResizableModal('rmReportText');
renderModalFooter('submit', {
showCheckbox: false,
showSubscribe: true,
submitText: 'Отправить',
onSubmit: function () { doReport(ctx, false, protectMode); return true; }
});
if (isZka) {
$('<button id="rmReportFast" style="' + stCancel + '">Быстрый запрос</button>').insertBefore('#removerSubmit');
$('#rmReportFast').click(function () {
if ($('#removerSubmit').data('rmSubmitInProgress')) return;
$('#removerSubmit').data('rmSubmitInProgress', true).prop('disabled', true);
$('#rmReportFast').prop('disabled', true);
doReport(ctx, true, protectMode);
});
}
}
};
// ═══════════════════════════════════════════════════════════════════════════
// ВСПОМОГАТЕЛЬНЫЕ: КБУ, закрытие, категории
// ═══════════════════════════════════════════════════════════════════════════
function getFastRemoveCriteriaAnchorFromConfig(templateName) {
var anchors = cfg.fastRemoveCriteriaAnchors || {};
var template = String(templateName || '').trim();
var lower = template.toLowerCase();
var key;
if (!template) return '';
if (Object.prototype.hasOwnProperty.call(anchors, template)) return anchors[template];
for (key in anchors) {
if (Object.prototype.hasOwnProperty.call(anchors, key) && String(key).toLowerCase() === lower) {
return anchors[key];
}
}
return '';
}
function getFastRemoveCriteriaAnchor(reason) {
var configured = reason ? getFastRemoveCriteriaAnchorFromConfig(reason[0]) : '';
var template, label, m;
if (configured) return configured;
template = String(reason && reason[0] || '');
label = String(reason && reason[1] || '');
if (/^(?:подст\s*:\s*)?(?:deleteslow|ds)$/i.test(template) || /^\s*ds\b/i.test(label)) return 'С1';
m = label.match(/^\s*([А-ЯЁA-Z]{1,3}\d+(?:\.\d+)?)/);
return m ? m[1] : '';
}
function buildFastRemoveCriteriaLinkHtml(reason) {
var anchor = getFastRemoveCriteriaAnchor(reason);
var label = KBU_CRITERIA_PAGE + (anchor ? '#' + anchor : '');
var url = getPageUrlWithFragment(KBU_CRITERIA_PAGE, anchor);
return '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(label) + '</a>';
}
function getFastRemoveReasons() {
var reasons = cfg.fastRemoveReasons;
var prefix = (mwCfg.wgIsRedirect ? 'ОП' : 'О') + ({ 0: 'С', 2: 'У', 3: 'У', 6: 'Ф', 14: 'К' }[mwCfg.wgNamespaceNumber] || '');
var all = [];
if (isCategory && reasons.categories) all = all.concat(reasons.categories);
['general','articles','redirects','files','users','special'].forEach(function (k) { if (reasons[k]) all = all.concat(reasons[k]); });
if (!isCategory && reasons.categories) all = all.concat(reasons.categories);
return all.filter(function (r) { return prefix.indexOf(r[2] !== undefined ? r[2] : r[1].charAt(0)) >= 0; });
}
function showCloseActionsModal(opts) {
createModal({ title: 'Снятие шаблонов номинаций', inline: true });
$('#removerModalContent').html('<p style="margin:0;">Определение доступных действий...</p>');
var pageName = normTitle(mwCfg.wgPageName);
getText(pageName, function (pageText, readErr) {
if (readErr) { showInfoAndClose('Не удалось прочитать содержимое страницы.', readErr.info || readErr.code || '', true); return; }
if (pageText === null) { showInfoAndClose('Не удалось прочитать содержимое страницы.', '', true); return; }
var actions = opts.getActions(pageText);
if (!actions.length) { showInfoAndClose(opts.emptyText, opts.emptyDetails || ''); return; }
var actionMap = actions.reduce(function (m, a) { m[a.id] = a; return m; }, {});
$('#removerModalContent').html(buildActionsHtml(actions, opts.inputName, opts.listId));
if (opts.afterRender) opts.afterRender(actions, actionMap, pageName, pageText);
renderModalFooter('submit', {
showCheckbox: opts.showCheckbox,
submitText: 'Выполнить',
onSubmit: function () {
var sel = getSelectedAction(opts.inputName, actionMap);
if (!sel) return false;
startProcessing();
return opts.onSubmit(sel, pageName, pageText, actionMap) !== false;
}
});
if (opts.afterFooterRender) opts.afterFooterRender(actions, actionMap, pageName, pageText);
});
}
function runPageEditWithStatus(opts) {
var o = opts || {};
var statusId = logStatus(o.pendingText, null, { pending: true, trackError: false });
editPageContent(o.pageName, o.editOptions, o.buildFn, function (err, meta) {
if (err) { logStatus(o.errorText, err, { statusId: statusId }); unlockModalSubmit(); return; }
logStatus(o.successText, null, { statusId: statusId, trackError: false });
if (o.onSuccess) o.onSuccess(meta || null);
});
}
function removeKuFromCategory(pageName) {
runPageEditWithStatus({
pageName: pageName,
pendingText: 'Снимается шаблон КУ...',
errorText: 'Снятие шаблона КУ.',
successText: 'Шаблон КУ снят.',
editOptions: { summary: makeSummary('снятие шаблона КУ'), watchlist: 'nochange', readError: 'Страница не существует.', readErrorCode: 'read_failed' },
buildFn: function (text) {
var r = stripTemplatesByPattern(text, '(?:к\\s*удалению|ку)');
if (!r.removed) return { error: { code: 'no_changes', info: 'Шаблон КУ не найден.' } };
return { text: r.text.replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n') };
},
onSuccess: function () {
logStatus('Шаблон на СО не устанавливался.', null, { trackError: false });
renderModalFooter('reload');
}
});
}
// ── Категории: добавление шаблона ────────────────────────────────────────
function addTemplateToCategory(pageName, type, mainName, additionalNames, callback) {
var cb = callback || function () {};
var cfgByType = {
discuss: { action: 'обсуждение', template: 'Обсуждаемая категория' },
deletion: { action: 'удаление', template: 'Обсуждаемая категория' },
rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true },
merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true }
};
var typeCfg = cfgByType[type];
if (!typeCfg) { cb({ code: 'error', info: 'Неизвестный тип номинации.' }); return; }
var dateStr = getDate()[0];
var parts = [dateStr];
if (typeCfg.needsMain) {
if (!mainName) { cb({ code: 'error', info: 'Не указано основное название.' }); return; }
parts.push(mainName);
if (additionalNames && additionalNames.length) Array.prototype.push.apply(parts, additionalNames);
}
var tplText = T_OPEN + typeCfg.template + '|' + parts.join('|') + T_CLOSE;
editPageContent(pageName, { summary: makeSummary('добавление шаблона номинации на ' + typeCfg.action), readError: 'Не удалось получить содержимое.' },
function (text) { return { text: wrapInNoinclude(text, tplText) }; },
function (err) {
logPageEdit(pageName, err);
if (err) { cb(err); return; }
if (type === 'merge') {
addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, function () { cb(null); });
return;
}
cb(null);
}
);
}
function collectCategoryPageInputValues(selector) {
var pages = [];
$(selector).each(function () {
var title = normalizeCategoryPageName($(this).val() || '');
if (title && pages.indexOf(title) === -1) pages.push(title);
});
return pages;
}
function addTemplatesToCategories(pages, type, mainName, additionalNames, callback) {
var cb = callback || function () {};
var processedPages = [];
eachSequential(pages || [], function (pageName, next) {
addTemplateToCategory(pageName, type, mainName, additionalNames, function (err) {
if (!err) processedPages.push(pageName);
next(err || null);
});
}, function (err) { cb(err, processedPages); });
}
function addMergeTemplatesToTargets(sourcePage, mainName, additionalNames, dateStr, callback) {
var cb = callback || function () {};
var currentCatName = normTitle(stripCatPrefix(sourcePage));
var targets = [mainName].concat(additionalNames || []);
if (!targets.length) { cb(); return; }
eachSequential(targets, function (target, next) {
var targetPage = 'Категория:' + target;
addMergeTemplateToTargetCategory(targetPage, currentCatName, dateStr, function (success, status) {
var url = getPageUrl(targetPage);
var linkHtml = '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(targetPage) + '</a>';
if (success) {
var extra = (status === 'already_exists' || status === 'updated') ? ' (' + formatMergeStatus(status) + ')' : '';
logStatus('Шаблон добавлен в ' + linkHtml + extra + '.', null, { trackError: false });
} else {
logStatus('Ошибка при добавлении шаблона в ' + linkHtml + '.', { code: 'merge_target_failed', info: status }, { trackError: false });
}
next();
});
}, cb);
}
function addMergeTemplateToTargetCategory(targetPageName, sourceCatName, dateStr, callback) {
editPageContent(targetPageName, { summary: makeSummary('добавление шаблона объединения'), readError: 'Не удалось получить содержимое' },
function (text) {
var existing = text.match(getCategoryMergeRe());
if (existing) {
var cats = existing[1].split('|').slice(1).map(function (p) { return p.trim(); }).filter(function (p) { return p.indexOf('=') === -1 && p.length > 0; });
var norm = sourceCatName.replace(/\s+/g, ' ').trim();
if (cats.some(function (c) { return c.replace(/\s+/g, ' ').trim() === norm; })) { return { skip: true, meta: { status: 'already_exists' } }; }
return {
text: text.replace(existing[0], function () { return existing[0].replace(/\}\}\s*$/, '|' + sourceCatName + '}}'); }),
summary: makeSummary('дополнение шаблона объединения [[:Категория:' + sourceCatName + ']]'),
meta: { status: 'updated' }
};
}
return { text: wrapInNoinclude(text, T_OPEN + 'Категория к объединению|' + dateStr + '|' + sourceCatName + T_CLOSE), meta: { status: 'created' } };
},
function (err, meta) { callback(!err, err ? err.info : ((meta && meta.status) || 'updated')); }
);
}
// ── Категории: обсуждение ────────────────────────────────────────────────
function buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, options) {
var opts = options || {};
var pages = Array.isArray(pageName) ? pageName : null;
var titleText;
if (pages && pages.length) {
titleText = String(opts.headerText || '').trim() || (formatPagesWithAnd(pages, ':') + (type === 'deletion' ? ' → удалить' : ''));
return '=== ' + titleText + ' ===\n';
}
var title = '=== [[:' + pageName + ']]';
if (type === 'rename' || type === 'merge') {
title += (type === 'rename' ? ' → ' : ' объединить с ') + formatCatLink(mainName);
if (additionalNames && additionalNames.length) {
var conj = type === 'rename' ? ' или ' : ' и ';
var head = additionalNames.slice(0, -1).map(formatCatLink).join(', ');
title += (additionalNames.length > 1 ? ', ' + head : '') + conj + formatCatLink(additionalNames[additionalNames.length - 1]);
}
} else if (type === 'deletion') {
title += ' → удалить';
}
return title + ' ===\n';
}
function createCategoryDiscussion(pageName, reason, type, callback, mainName, additionalNames, options) {
var cb = callback || function () {};
var opts = options || {};
var now = new Date();
var m = now.getUTCMonth(), year = now.getUTCFullYear(), day = now.getUTCDate();
var dateHeader = '== ' + day + ' ' + MONTHS_GEN[m] + ' ' + year + ' ==\n';
var discPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[m] + '_' + year;
var discTitle = buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, opts);
var discText = discTitle + (opts.reasonIsPrepared ? String(reason || '') : appendNominationSignature(reason)) + '\n';
var sectionTitle = discTitle.replace(/^===\s*/, '').replace(/\s*===\s*$/, '').trim();
var summaryTarget = Array.isArray(pageName) ? formatPagesWithAnd(pageName, ':') : '[[:' + pageName + ']]';
var summaryText = 'добавление обсуждения для ' + (Array.isArray(pageName) ? 'категорий ' : 'категории ') + summaryTarget;
publishNomination({
pageTitle: discPage,
readErrorMessage: 'Не удалось получить содержимое страницы обсуждения.',
summary: makeSummary(summaryText),
createText: function () {
return T_OPEN + 'ОБК-Навигация' + T_CLOSE + '\n\n' + dateHeader + discText;
},
buildText: function (text) {
var todayIdx = text.indexOf(dateHeader.trim());
if (todayIdx !== -1) {
var endIdx = text.indexOf('\n== ', todayIdx + dateHeader.length);
if (endIdx === -1) endIdx = text.length;
var before = text.slice(0, endIdx);
return { text: (before.endsWith('\n\n') ? before.slice(0, -1) : before) + '\n' + discText + text.slice(endIdx) };
}
var searchStr = T_OPEN + 'ОБК-Навигация' + T_CLOSE;
var obkIdx = text.indexOf(searchStr);
if (obkIdx === -1) return { error: { code: 'insert_failed', info: 'Не удалось найти место для вставки.' } };
return { text: text.slice(0, obkIdx + searchStr.length) + '\n\n' + dateHeader + discText + text.slice(obkIdx + searchStr.length).replace(/^\n+/, '\n') };
}
}, function (err) {
if (err) { cb(err); return; }
cb(null, { pageTitle: discPage, sectionTitle: sectionTitle });
});
}
// ── Категории: завершение ОБКАТ ───────────────────────────────────────────
function markCategoryDiscussionAsDone(pageName) {
runPageEditWithStatus({
pageName: pageName,
pendingText: 'Снимается шаблон обсуждения...',
errorText: 'Снятие шаблона обсуждения.',
successText: 'Шаблон обсуждения снят.',
editOptions: { summary: makeSummary('обсуждение категории завершено'), readError: 'Не удалось получить содержимое.' },
buildFn: function (text) {
var allTpls = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|');
var patterns = [
new RegExp('<noinclude>\\s*\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}\\s*</noinclude>', 'i'),
new RegExp('\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}', 'i')
];
var match = null;
for (var i = 0; i < patterns.length; i++) { match = text.match(patterns[i]); if (match) break; }
if (!match) return { error: { code: 'no_changes', info: 'Шаблон обсуждаемой категории не найден.' } };
return {
text: text.replace(match[0], '').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n'),
meta: { tplDate: convertToStandardDate(match[2].split('|')[0].trim()) }
};
},
onSuccess: function (meta) {
var talkStatusId = logStatus('Обновляется шаблон на СО категории...', null, { pending: true, trackError: false });
updateCategoryTalkPage(pageName, meta.tplDate, function (talkErr, info) {
if (talkErr) { logStatus('Установка шаблона на СО.', talkErr, { statusId: talkStatusId }); unlockModalSubmit(); return; }
logStatus(
(info && (info.status === 'already_present' || info.status === 'no_changes')) ? 'Шаблон на СО уже установлен.' : 'Шаблон установлен на СО.',
null, { statusId: talkStatusId, trackError: false }
);
renderModalFooter('reload');
});
}
});
}
function updateCategoryTalkPage(categoryName, templateDate, callback) {
var cb = callback || function () {};
var talkPage = getTalkPage(categoryName);
var newTpl = T_OPEN + 'Обсуждавшаяся категория|' + templateDate + T_CLOSE;
getTextWithTimestamp(talkPage, function (text, baseTimestamp, readErr) {
if (readErr) { cb(makeReadError(readErr, 'talk_read_failed', 'Не удалось получить содержимое СО категории.')); return; }
if (text === null) {
apiReq({ title: talkPage, text: newTpl + '\n\n', summary: makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate), createonly: true },
'edit', function (resp) {
if (resp && resp.error) {
if (resp.error.code === 'articleexists') setTimeout(function () { updateCategoryTalkPage(categoryName, templateDate, cb); }, 1000);
else cb(resp.error);
} else cb(null, { status: 'created' });
});
return;
}
var discussedRe = new RegExp('\\{\\{\\s*(' + cfg.categoryTemplates.discussed + ')([^\\}]*?)\\s*\\}\\}', 'i');
var tplMatch = text.match(discussedRe);
var newText = text;
if (tplMatch) {
var existingDates = tplMatch[2].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
if (existingDates.indexOf(templateDate) !== -1) { cb(null, { status: 'already_present' }); return; }
newText = text.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}$/, '|' + templateDate + '}}'); });
} else {
newText = insertTplOnTalkPage(text, newTpl);
}
if (newText === text) { cb(null, { status: 'no_changes' }); return; }
var ep = {
title: talkPage,
text: newText,
summary: tplMatch
? makeSummary('обновление шаблона [[ш:Обсуждавшаяся категория]], добавлена дата ' + templateDate)
: makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate)
};
if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) { cb(resp && resp.error ? resp.error : null, resp && !resp.error ? { status: tplMatch ? 'updated' : 'created' } : null); });
});
}
// ── Быстрое объединение (Ctrl+клик КОБ) ─────────────────────────────────
function buildQuickMergeHtml(tplDate, targets, currentCatName) {
return joinHtml([
'<p>Найден шаблон с датой <strong>', escapeHtml(tplDate), '</strong>. Категории для объединения:</p>',
'<pre style="background:', tk.bgBase, ';color:', tk.cBase, ';padding:10px;border:1px solid ', tk.bSubS, ';border-radius:4px;margin-bottom:10px;">',
targets.map(function (c) { return '• ' + escapeHtml(c); }).join('\n'),
'</pre>',
'<p><strong>Текущая категория:</strong> ', escapeHtml(currentCatName), '</p>',
'<p style="color:', tk.cSub, ';">Шаблон будет добавлен во все указанные выше категории.</p>'
]);
}
function showQuickMergeModal() {
getText(mwCfg.wgPageName, function (text, readErr) {
if (readErr) { alert('Не удалось получить содержимое: ' + (readErr.info || readErr.code || 'ошибка API') + '.'); return; }
if (!text) { alert('Не удалось получить содержимое.'); return; }
var mergeRe = getCategoryMergeRe();
var match = text.match(mergeRe);
if (!match) { alert('В текущей категории не найден шаблон "Категория к объединению".'); return; }
var params = match[1].split('|').map(function (p) { return p.trim(); });
var tplDate = params[0];
var targets = params.slice(1);
if (!targets.length) { alert('В шаблоне не найдены целевые категории.'); return; }
createModal({ title: 'Быстрое добавление шаблона объединения' });
var currentCatName = normTitle(stripCatPrefix(mwCfg.wgPageName));
$('#removerModalContent').html(buildQuickMergeHtml(tplDate, targets, currentCatName));
renderModalFooter('submit', {
showCheckbox: false,
submitText: 'Добавить шаблоны',
onSubmit: function () {
startProcessing();
$('#removerSubmit').prop('disabled', true);
eachSequential(targets, function (target, next) {
addMergeTemplateToTargetCategory('Категория:' + target, currentCatName, tplDate, function (success, status) {
if (success) logStatus('Категория [[:Категория:' + target + ']] (' + formatMergeStatus(status) + ').', null, { trackError: false });
else logStatus('Ошибка [[:Категория:' + target + ']].', { code: 'merge_target_failed', info: status }, { trackError: false });
next();
});
}, function () {
if (isError) markSubmitError(); else renderModalFooter('close');
});
return true;
}
});
});
}
// ── ЗКА/Защита: публикация ───────────────────────────────────────────────
function getReporterContext(mode) {
var rawPage = mwCfg.wgPageName;
var pageName = normTitle(rawPage)
.replace(/(Special|Служебная):(Contributions|Вклад)\//i, 'User:')
.replace(/(User talk:|Обсуждение участни(ка|цы):)/i, 'User:');
var isUserRelated = /user|contrib|участни|вклад/i.test(rawPage);
var displayName = normTitle(rawPage)
.replace(/(Special|Служебная):(Вклад|Contributions)\//i, '')
.replace(/(User talk:|Обсуждение участни(ка|цы):)/i, '')
.replace(/(user|участни(к|ца)):/i, '');
var pageLink = '[[' + pageName + (isUserRelated ? '|' + displayName + ']]' : ']]');
var reportPage = mode === 'request' ? 'Википедия:Запросы к администраторам' : 'Википедия:Установка защиты';
return { pageName: pageName, pageLink: pageLink, displayName: displayName, reportPage: reportPage };
}
function doReport(ctx, fast, protectMode) {
var header = $('#rmReportHeader').val() || ctx.pageLink;
var text = $('#rmReportText').val() || '';
var isZka = ctx.reportPage === 'Википедия:Запросы к администраторам';
var isRemoveProtect = !isZka && protectMode === 'remove';
startProcessing();
var targetPage, editParams, sectionForLink;
if (fast) {
targetPage = 'Википедия:Запросы к администраторам/Быстрые';
sectionForLink = null;
editParams = {
appendtext: '\n\n' + T_OPEN + 'sub' + 'st:t:preload/ЗКАБ/subst|\n | участник = ' + ctx.displayName +
'| страница = | пояснение = ' + text + T_CLOSE + '\n',
summary: makeSummary('новый запрос [[Special:Contributions/' + ctx.displayName + ']]')
};
} else if (isZka) {
targetPage = ctx.reportPage;
sectionForLink = extractDisplayedText(header);
var isIpFull = /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/.test(ctx.displayName);
editParams = {
section: 'new',
sectiontitle: header,
text: '* ' + T_OPEN + 'userlinks|' + ctx.displayName + (isIpFull ? '|ip=1' : '') + T_CLOSE + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
} else {
targetPage = isRemoveProtect ? 'Википедия:Снятие защиты' : 'Википедия:Установка защиты';
var pages = collectInputValues('.rmProtectPageInput');
if (!pages.length) pages = [ctx.pageName];
var sectionTitle, pageLines;
if (pages.length === 1) {
sectionTitle = '[[' + pages[0] + ']]';
pageLines = '* ' + T_OPEN + 'pagelinks-protect|' + pages[0] + T_CLOSE;
} else {
sectionTitle = ($('#rmProtectHeader').val() || '').trim() || pages.map(function (p) { return '[[' + p + ']]'; }).join(', ');
pageLines = pages.map(function (p) { return '* ' + T_OPEN + 'pagelinks-protect|' + p + T_CLOSE; }).join('\n');
}
sectionForLink = extractDisplayedText(sectionTitle);
editParams = {
section: 'new',
sectiontitle: sectionTitle,
text: pageLines + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
}
var statusId = logStatus('Публикуется запрос на «' + targetPage + '»...', null, { pending: true, trackError: false });
apiReq($.extend({ title: targetPage }, editParams), 'edit', function (resp) {
if (resp && resp.error) {
logStatus('Публикация запроса на «' + targetPage + '».', resp.error, { statusId: statusId });
markSubmitError();
if (isZka) $('#rmReportFast').prop('disabled', false);
return;
}
logStatus('Запрос опубликован.', null, { statusId: statusId, trackError: false });
appendNominationLink(targetPage, sectionForLink);
if (sectionForLink) subscribeToTopic(targetPage, sectionForLink);
renderModalFooter('reload');
});
}
// ═══════════════════════════════════════════════════════════════════════════
// ДИСПЕТЧЕР
// ═══════════════════════════════════════════════════════════════════════════
function handleMenuClick(item, event) {
isError = false;
var op = OPERATIONS_MAP[item.id];
// Специальный случай: КОБ категории с Ctrl — быстрое добавление шаблона
if (item.id === 'cat-merge' && event && event.ctrlKey) {
showQuickMergeModal();
return;
}
if (!op) {
console.error('RemoverCore: неизвестная операция', item.id);
return;
}
var handlerFn = handlers[op.handler];
if (typeof handlerFn !== 'function') {
console.error('RemoverCore: обработчик не найден', op.handler);
return;
}
handlerFn(op, event);
}
// ─── Экспорт ─────────────────────────────────────────────────────────────
window.RemoverCore = { handleMenuClick: handleMenuClick };
}());
51mp0xsm0o44ppr7prw6d2928yx2p5j
739916
739913
2026-05-01T06:48:02Z
Solidest
54422
739916
javascript
text/javascript
/**
* Remover — ядро (core).
* Загружается лениво при первом клике по пункту меню.
* Ожидает, что window.RemoverState уже задан remover-loader.js.
*
* Архитектура: единый реестр OPERATIONS описывает все операции (КБУ, КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС, Защита, Запрос, Снятие, кат-варианты).
* Логика выполнения сосредоточена в универсальных обработчиках.
* Экспортирует: window.RemoverCore.handleMenuClick(item, event)
*/
(function () {
'use strict';
var state = window.RemoverState;
if (!state) { console.error('RemoverCore: window.RemoverState не задан.'); return; }
var mwCfg = state.mwCfg;
var cfg = applyCoreConfigDefaults(state.cfg || {});
var isCategory = state.isCategory;
var isVector22 = state.isVector22;
var scriptLink = cfg.scriptLink;
var settingsOptionName = state.settingsOptionName || 'userjs-remover-settings';
var settingsVersion = 1;
var settingsMenuMeta = collectSettingsMenuMeta();
var settingsArticleItemLabels = settingsMenuMeta.articleLabels;
var settingsCategoryItemLabels = settingsMenuMeta.categoryLabels;
var settingsItemLabelById = settingsMenuMeta.idToLabel;
var settingsItemLabelByNorm = settingsMenuMeta.labelByNorm;
var settingsItemLabelOrder = settingsMenuMeta.labelOrder;
var settingsDefaults = getDefaultSettings();
var MENU_TITLE_PRESET_CACTIONS = '__remover_portlet_cactions__';
var MENU_TITLE_PRESET_PAGE = '__remover_portlet_page__';
var MENU_TITLE_PRESET_TOOLS = '__remover_portlet_tools__';
var initialSettings = normalizeRemoverSettings(readSettingsOptionState(state.settings || {}));
var setAlert = ('setAlert' in state) ? !!state.setAlert : initialSettings.notifyAuthor;
var setSubscribe = ('setSubscribe' in state) ? !!state.setSubscribe : initialSettings.subscribeTopic;
var signatureSeparator = ('signatureSeparator' in state && typeof state.signatureSeparator === 'string')
? state.signatureSeparator.trim()
: initialSettings.signatureSeparator;
initialSettings.notifyAuthor = setAlert;
initialSettings.subscribeTopic = setSubscribe;
initialSettings.signatureSeparator = signatureSeparator;
state.cfg = cfg;
state.settings = clonePlainObject(initialSettings);
state.setAlert = setAlert;
state.setSubscribe = setSubscribe;
// ─── Константы ──────────────────────────────────────────────────────────
var MONTHS_NOM = ['Январь','Февраль','Март','Апрель','Май','Июнь','Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'];
var MONTHS_GEN = ['января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря'];
var MONTHS_NOM_LOWER = MONTHS_NOM.map(function (m) { return m.toLowerCase(); });
var T_OPEN = '{' + '{';
var T_CLOSE = '}' + '}';
var KBU_CRITERIA_PAGE = 'Википедия:Критерии быстрого удаления';
var RE_ESCAPE = /[.*+?^${}()|[\]\\]/g;
var RE_KU_ON_PAGE = /\{\{\s*(?:к\s*удалению|ку)\s*(?:\||\}\})/i;
var RE_KPM_ON_PAGE = /\{\{\s*(?:к\s*переименованию|кпм|rename)\s*(?:\||\}\})/i;
var RE_KBU_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?(?:db\s*-[^|}\s]+|уд\s*-[^|}\s]+|к\s*быстрому\s*удалению|к\s*отсроченному\s*удалению|deleteslow|ds|hang\s*-?\s*on)\s*(?:\||\}\})/i;
var RE_KUL_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?к\s*улучшению\s*(?:\||\}\})/i;
var KBU_PATTERN_STR = 'db\\s*-[^|}\\s]+|уд\\s*-[^|}\\s]+|к\\s*быстрому\\s*удалению|к\\s*отсроченному\\s*удалению|deleteslow|ds';
var RE_KBU_PATTERNS = new RegExp('(?:' + KBU_PATTERN_STR + ')', 'i');
var KUL_PATTERN_STR = 'к\\s*улучшению';
var RE_KUL_PATTERN = new RegExp(KUL_PATTERN_STR, 'i');
var HANGON_PATTERN_STR = 'hang\\s*-?\\s*on';
var RE_HANGON = new RegExp(HANGON_PATTERN_STR, 'i');
var RE_DATE_ISO = /^(\d{4})-(\d{2})-(\d{2})$/;
var RE_DATE_RUSSIAN = /^(\d{1,2})\s+([\u0430-\u044f\u0410-\u042f\u0451\u0401.]+)\s+(\d{2}|\d{4})$/;
var RE_DATE_DASH = /^(\d{1,4})\s*-\s*(\d{1,2})\s*-\s*(\d{1,4})$/;
var RE_DATE_DOT = /^(\d{1,2})\s*\.\s*(\d{1,2})\s*\.\s*(\d{2}|\d{4})$/;
var RE_DATE_SLASH = /^(\d{1,4})\s*\/\s*(\d{1,2})\s*\/\s*(\d{1,4})$/;
var RE_NOINCLUDE = /(\s*)<noinclude>([\s\S]*?)<\/noinclude>/i;
var RE_TEMPLATE_NS = /^шаблон\s*:\s*/i;
// ─── Глобальные переменные сессии ────────────────────────────────────────
var isError = false;
var logStatusSeq = 0;
var resizeObservers = [];
var modalLayoutSyncHandlers = [];
var tplAliasCache = {};
// ─── Стили ───────────────────────────────────────────────────────────────
var stStyles = cfg.modalStyles;
var tk = {
cBase: 'var(--color-base, #202122)',
cSub: 'var(--color-subtle, #72777d)',
cSubM: 'var(--color-subtle, #54595d)',
cInv: 'var(--color-inverted-fixed, #fff)',
cProg: 'var(--color-progressive, #3366cc)',
cProgH: 'var(--color-progressive--hover, #2a4b8d)',
cDang: 'var(--color-destructive, #d73333)',
cDis: 'var(--color-disabled, var(--color-subtle, #72777d))',
bgBase: 'var(--background-color-base, #fff)',
bgNSub: 'var(--background-color-neutral-subtle, #f8f9fa)',
bgN: 'var(--background-color-neutral, #eaecf0)',
bgDis: 'var(--background-color-disabled, var(--background-color-neutral, #eaecf0))',
bgProg: 'var(--background-color-progressive, #3366cc)',
bgProgH:'var(--background-color-progressive--hover, #2a4d8f)',
bgSucc: 'var(--background-color-success, #14866d)',
bgSuccH:'var(--background-color-success--hover, #0f6d57)',
bSub: 'var(--border-color-subtle, #a2a9b1)',
bSubS: 'var(--border-color-subtle, #ddd)',
bDis: 'var(--border-color-disabled, var(--border-color-subtle, #a2a9b1))',
bProg: 'var(--border-color-progressive, #3366cc)',
bProgH: 'var(--border-color-progressive--hover, #2a4d8f)',
bSucc: 'var(--border-color-success, #14866d)',
bSuccH: 'var(--border-color-success--hover, #0f6d57)'
};
var sz = {
taH: '180px',
taMinH: '100px',
taMinW: '180px',
mobileBp: 720,
modalRatio: 0.4,
modalMinWide: 420,
modalDefaultWide: 720,
viewportGap: 24,
touchDesktopGap: 120
};
var btnBase = 'border-radius:4px;padding:8px 16px;cursor:pointer;font-size:14px;font-family:inherit;transition:background .1s;';
var neutralVis = 'background:' + tk.bgNSub + ';border:1px solid ' + tk.bSub + ';color:' + tk.cBase + ';border-radius:4px;';
var stCancel = neutralVis + btnBase;
var stSubmit = 'background:' + tk.bgProg + ';color:' + tk.cInv + ';border:1px solid ' + tk.bProg + ';' + btnBase;
var stReload = 'background:' + tk.bgSucc + ';color:' + tk.cInv + ';border:1px solid ' + tk.bSucc + ';' + btnBase;
var stInputBox = 'flex:1;padding:6px;box-sizing:border-box;min-width:0;border:1px solid ' + tk.bSub + ';background:' + tk.bgBase + ';color:inherit;border-radius:2px;';
var stInputFull= 'width:100%;padding:6px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:2px;background:' + tk.bgBase + ';color:inherit;margin-bottom:10px;';
var stRow = 'display:flex;margin-bottom:6px;';
var stToolBtn = neutralVis + 'padding:4px 8px;margin-left:4px;cursor:pointer;font-size:12px;';
var stRemoveBtn= neutralVis + 'display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;padding:0;width:32px;height:32px;margin-left:4px;cursor:pointer;font-size:12px;line-height:1;';
var stFooterWrap = 'display:flex;align-items:center;gap:8px;flex-wrap:wrap;';
var stFooterChecks = 'display:flex;flex-direction:column;gap:4px;margin-right:auto;flex:1 1 220px;min-width:0;';
var stFooterCheckLabel = 'display:inline-flex;align-items:flex-start;gap:6px;font-size:14px;line-height:1.4;max-width:100%;';
var stFooterActions = 'display:flex;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;justify-content:flex-end;margin-left:auto;';
var stHeaderIconBtn = 'margin:0 0 0 auto;width:32px;min-width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:4px;background:' + tk.bgNSub + ';color:' + tk.cSubM + ';cursor:pointer;font-size:16px;line-height:1;';
var multiNominationGap = '6px';
var RESIZE_CLASS = 'rm-resizable';
// ═══════════════════════════════════════════════════════════════════════════
// РЕЕСТР ОПЕРАЦИЙ
// Каждая запись описывает одну кнопку меню. Поля:
// id — идентификатор (совпадает с item.id из loader)
// handler — имя метода-обработчика в объекте handlers
// handlerArg — аргумент, передаваемый в handler (опционально)
// ═══════════════════════════════════════════════════════════════════════════
var OPERATIONS = [
// ── Статьи ──────────────────────────────────────────────────────────
{
id: 'fRm',
label: 'КБУ',
handler: 'showKbu',
// Параметры номинации: заполняются при submit
nomination: {
pageTitle: function (pg) { return normTitle(pg); },
// шаблон встраивается в статью, номинационная страница отсутствует
inArticle: true
}
},
{
id: 'tRm',
label: 'КУ',
handler: 'showNomination',
nomination: {
comment: 'к удалению',
template: 'к удалению',
navTemplate: 'КУ',
nomPage: function (date) { return 'Википедия:К удалению/' + date; },
supportsMulti: true,
supportsTransfer: true,
// шаблон встраивается в статью через <noinclude>
inArticle: true,
articleTpl: function (tplpar, date) { return 'к удалению|' + date + (tplpar ? '|' + tplpar : ''); }
}
},
{
id: 'rnm',
label: 'КПМ',
handler: 'showNomination',
nomination: {
comment: 'к переименованию',
template: 'к переименованию',
navTemplate: 'КПМ',
nomPage: function (date) { return 'Википедия:К переименованию/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к переименованию|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'rename',
firstId: 'rmRenameFirst', inputClass: 'rmRenameInput',
firstPh: 'Новое название',
addBtnId: 'rmAddRename', addBtnLabel: 'Добавить вариант',
containerId: 'rmRenameContainer', addPh: 'Дополнительный вариант',
maxRows: 2, maxMsg: 'Максимум 3 варианта переименования.'
}
}
},
{
id: 'imp',
label: 'КУЛ',
handler: 'showNomination',
nomination: {
comment: 'к срочному улучшению',
template: 'к улучшению',
navTemplate: 'КУЛ',
nomPage: function (date) { return 'Википедия:К улучшению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к улучшению|' + date + (tplpar ? '|' + tplpar : ''); }
}
},
{
id: 'merge',
label: 'КОБ',
handler: 'showNomination',
nomination: {
comment: 'к объединению с другой',
template: 'к объединению',
navTemplate: 'КОБ',
nomPage: function (date) { return 'Википедия:К объединению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к объединению|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'merge',
firstId: 'rmMergeFirst', inputClass: 'rmMergeInput',
firstPh: 'Объединить с…',
addBtnId: 'rmAddMerge', addBtnLabel: '+',
containerId: 'rmMergeContainer', addPh: 'Дополнительная статья',
maxRows: 10, maxMsg: 'Максимум 11 статей для объединения.'
}
}
},
{
id: 'split',
label: 'КРАЗД',
handler: 'showNomination',
nomination: {
comment: 'к разделению',
template: 'к разделению',
navTemplate: 'КР',
nomPage: function (date) { return 'Википедия:К разделению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к разделению|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'split',
firstId: 'rmSplitFirst', inputClass: 'rmSplitInput',
firstPh: 'Разделить на…',
addBtnId: 'rmAddSplit', addBtnLabel: '+',
containerId: 'rmSplitContainer', addPh: 'Дополнительная статья'
}
}
},
{
id: 'recov',
label: 'ВУС',
handler: 'showNomination',
nomination: {
comment: '',
template: 'к восстановлению',
navTemplate: 'ВУС',
nomPage: function (date) { return 'Википедия:К восстановлению/' + date; },
inArticle: false // шаблон не ставится в (удалённую) статью
}
},
{
id: 'close',
label: 'Снятие',
handler: 'showArticleClose'
},
// ── Запросы ─────────────────────────────────────────────────────────
{
id: 'protect',
label: 'Защита',
handler: 'showReport',
reportMode: 'protect'
},
{
id: 'request',
label: 'Запрос',
handler: 'showReport',
reportMode: 'request'
},
// ── Категории ────────────────────────────────────────────────────────
{
id: 'cat-fRm',
label: 'КБУ',
handler: 'showKbu',
forCategory: true
},
{
id: 'cat-discuss',
label: 'Обсудить',
handler: 'showCatNomination',
catType: 'discuss'
},
{
id: 'cat-delete',
label: 'Удалить',
handler: 'showCatNomination',
catType: 'deletion'
},
{
id: 'cat-rename',
label: 'Переименовать',
handler: 'showCatNomination',
catType: 'rename'
},
{
id: 'cat-merge',
label: 'Объединить',
handler: 'showCatNomination',
catType: 'merge'
},
{
id: 'cat-done',
label: 'Снятие',
handler: 'showCatClose'
}
];
// Быстрый поиск по id
var OPERATIONS_MAP = {};
OPERATIONS.forEach(function (op) { OPERATIONS_MAP[op.id] = op; });
// ─── Тексты переноса (КБУ → КУ) ─────────────────────────────────────────
var transferTexts = {
kbu: { notice: 'Перенесено с быстрого удаления.', hint: 'Шаблоны КБУ и Hangon будут сняты.' },
kul: { notice: 'Перенесено с КУЛ.', hint: 'Шаблоны КУЛ будут сняты.' },
both: { notice: 'Перенесено с быстрого удаления и КУЛ.', hint: 'Шаблоны КБУ, КУЛ и Hangon будут сняты.' }
};
// ═══════════════════════════════════════════════════════════════════════════
// УТИЛИТЫ
// ═══════════════════════════════════════════════════════════════════════════
function escapeRegExp(s) { return s.replace(RE_ESCAPE, '\\$&'); }
function escapeHtml(s) {
return String(s || '')
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
function joinHtml(parts) { return parts.join(''); }
function ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
function padTwo(n) { return n < 10 ? '0' + n : String(n); }
function expandTwoDigitYear(value) {
return 2000 + parseInt(value, 10);
}
function monthToNumber(name) {
var lower = name.toLowerCase().replace(/\.$/, '');
var idx = MONTHS_GEN.indexOf(lower);
if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower);
if (idx === -1 && lower.length >= 3) {
for (var i = 0; i < MONTHS_GEN.length; i++) {
if (MONTHS_GEN[i].indexOf(lower) === 0 || MONTHS_NOM_LOWER[i].indexOf(lower) === 0) return i + 1;
}
}
return idx + 1;
}
function makeStandardDate(yearValue, monthValue, dayValue) {
var yearText = String(yearValue || '').trim();
var year = yearText.length === 2 ? expandTwoDigitYear(yearText) : parseInt(yearText, 10);
var month = parseInt(monthValue, 10);
var day = parseInt(dayValue, 10);
var maxDay;
if ((yearText.length !== 2 && yearText.length !== 4) || isNaN(year) || isNaN(month) || isNaN(day) || year < 1 || month < 1 || month > 12 || day < 1) return null;
maxDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
if (day > maxDay) return null;
return year + '-' + padTwo(month) + '-' + padTwo(day);
}
function normalizeIsoDate(value) {
var m = String(value || '').trim().match(RE_DATE_ISO);
return m ? makeStandardDate(m[1], m[2], m[3]) : null;
}
function normalizeTemplateName(name) {
return (name || '').toLowerCase().replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
}
function getDate(dateString) {
var d = dateString ? new Date(dateString) : new Date();
var iso = d.getUTCFullYear() + '-' + padTwo(d.getUTCMonth() + 1) + '-' + padTwo(d.getUTCDate());
var rus = d.getUTCDate() + ' ' + MONTHS_GEN[d.getUTCMonth()] + ' ' + d.getUTCFullYear();
return [iso, rus];
}
function convertToStandardDate(dateStr) {
var value = String(dateStr || '').replace(/\s+/g, ' ').trim();
var m;
var mo;
var normalized;
m = value.match(RE_DATE_ISO);
if (m) return normalizeIsoDate(value) || '';
m = value.match(RE_DATE_RUSSIAN);
if (m) {
mo = monthToNumber(m[2]);
normalized = mo ? makeStandardDate(m[3], mo, m[1]) : null;
return normalized || '';
}
m = value.match(RE_DATE_DASH);
if (m) {
normalized = makeStandardDate(m[1], m[2], m[3]);
return normalized || '';
}
m = value.match(RE_DATE_DOT);
if (m) {
normalized = makeStandardDate(m[3], m[2], m[1]);
return normalized || '';
}
m = value.match(RE_DATE_SLASH);
if (m) {
normalized = makeStandardDate(m[1], m[2], m[3]);
return normalized || '';
}
return value;
}
function getTalkPage(pageName) {
var match = /([^:]*:)?(.*)/.exec(pageName);
if (match[1]) {
var ns = mwCfg.wgNamespaceIds[match[1].slice(0, -1).toLowerCase().replace(/ /g, '_')];
if (ns !== undefined) return mwCfg.wgFormattedNamespaces[ns | 1] + ':' + match[2];
}
return 'Обсуждение:' + pageName;
}
function normTitle(s) { return (s || '').replace(/_/g, ' '); }
function stripCatPrefix(s) { return (s || '').replace(/^Категория:\s*/i, ''); }
function normalizeCategoryPageName(value) {
var title = normTitle(value).trim();
var nsMatch, nsKey, ns;
if (!title) return '';
nsMatch = title.match(/^([^:]+):(.+)$/);
if (nsMatch) {
nsKey = nsMatch[1].toLowerCase().replace(/ /g, '_');
ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[nsKey];
if (ns === 14) return (mwCfg.wgFormattedNamespaces[14] || 'Категория') + ':' + nsMatch[2].trim();
return title;
}
return (mwCfg.wgFormattedNamespaces[14] || 'Категория') + ':' + title;
}
function makeSummary(text) { return scriptLink + ': ' + text; }
function appendNominationSignature(text) {
var body = String(text || '');
return body + (signatureSeparator ? ' ' + signatureSeparator : '') + ' ~~' + '~~';
}
function extractDisplayedText(s) {
return (s || '').replace(/\[\[:?(?:[^|\]]+\|)?(.+?)\]\]/g, '$1');
}
function collectInputValues(selector) {
return $(selector).map(function () { return $(this).val().trim(); }).get().filter(Boolean);
}
function applyCoreConfigDefaults(config) {
var defaults = {
scriptLink: '[[Участник:Solidest/Remover|Remover]]',
fastRemoveReasons: {
general: [
['уд-бессвязно', 'О1 Бессвязный текст'],
['уд-тест', 'О2 Тестовая страница'],
['уд-ванд', 'О3 Вандальная страница'],
['уд-повторно', 'О4 Уже удалялось'],
['уд-автор', 'О5 По просьбе автора'],
['уд-обс', 'О6 Ненужная подстраница'],
['уд-переим', 'О7 Для переименования'],
['уд-дубль', 'О8 Дубликат'],
['уд-реклама', 'О9 Реклама или спам'],
['db-badtalk', 'О10 Нецелевая СО'],
['уд-копивио', 'О11 Нарушение АП']
],
articles: [
['подст:ds', 'ds Отсроченное пусто или коротко', 'С'],
['уд-пусто', 'С1 Пусто или коротко'],
['уд-иностр', 'С2 Не на русском'],
['уд-ссылки', 'С3 Лишь ссылки'],
['уд-нз', 'С5 Явно незначимо'],
['уд-бям', 'С7 Создано нейросетью']
],
redirects: [
['уд-в никуда', 'П1 Перенапр. в никуда'],
['db-redirspace', 'П2 Межпростр. перенапр.'],
['уд-опечатка', 'П3 Перенапр. с опечаткой'],
['уд-падеж', 'П4 Не именительный падеж'],
['уд-смысл', 'П5 Неверное перенапр.'],
['db-redirtalk', 'П6 Перенапр. на СО']
],
files: [
['db-duplicate', 'Ф1 Копия файла'],
['db-badimage', 'Ф2 Повреждённый файл'],
['подст:nld', 'Ф3 Нет данных о лицензии'],
['подст:nsd', 'Ф3 Нет данных о источнике'],
['подст:nad', 'Ф3 Нет данных о авторе'],
['подст:dd', 'Ф3 Сомнительные данные файла'],
['подст:ofud', 'Ф4 Неиспользуемый КДИ'],
['подст:dfud', 'Ф5 Нет КДИ'],
['db-badfairuse', 'Ф6 Неоправданное КДИ'],
['подст:rfu', 'Ф7 Заменяемый КДИ'],
['NCT', 'Ф8 Есть на Складе'],
['подст:Nothost', 'Ф9 Файл — ВП:НЕХОСТИНГ']
],
categories: [
['уд-пусткат', 'К1 Пустая категория'],
['db-templatecat', 'К1.2 Разобранная служебная кат.'],
['уд-перекат', 'К2 Переименованная кат.']
],
users: [
['уд-владелец', 'У1 По желанию владельца'],
['уд-анон', 'У2 Устаревшая СО анонима'],
['уд-несущ', 'У3 Несуществующий участник'],
['уд-нецелевое', 'У4 Нецелевое использ. ЛП'],
['уд-неактив', 'У5 Подстраница неактивного']
],
special: [
['db', 'Особый случай']
]
},
fastRemoveCriteriaAnchors: {
'подст:ds': 'С1',
deleteslow: 'С1',
ds: 'С1'
},
requiredParamTemplates: {
'уд-переим': 'страницу, которую нужно переименовать',
'уд-дубль': 'страницу-дубликат',
'уд-копивио': 'URL источника нарушения АП',
'db-duplicate': 'имя файла-оригинала',
'подст:rfu': 'имя заменяемого файла',
'NCT': 'имя файла на Викискладе',
'уд-перекат': 'новое название категории',
'db': '!причину удаления'
},
categoryTemplates: {
discuss: 'Обсуждаемая категория|обсуждаемая категория|Acat|acat|ОКТО|окто|Категория к обсуждению|категория к обсуждению',
rename: 'Категория к переименованию|категория к переименованию|Anacat|anacat',
merge: 'Категория к объединению|категория к объединению|Amergecat|amergecat|Cfm|cfm',
discussed: 'Обсуждавшаяся категория|обсуждавшаяся категория|Обсуждалась|обсуждалась|Обсуждалось|обсуждалось'
},
modalStyles: {
border: '1px solid var(--border-color-progressive, #3366bb)',
background: 'var(--background-color-base, #f8f9fa)',
borderRadius: '6px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
headerColor: 'var(--color-progressive, #3366bb)'
}
};
config.scriptLink = config.scriptLink || defaults.scriptLink;
config.fastRemoveReasons = $.extend({}, defaults.fastRemoveReasons, config.fastRemoveReasons || {});
config.fastRemoveCriteriaAnchors = $.extend({}, defaults.fastRemoveCriteriaAnchors, config.fastRemoveCriteriaAnchors || {});
config.requiredParamTemplates = $.extend({}, defaults.requiredParamTemplates, config.requiredParamTemplates || {});
config.categoryTemplates = $.extend({}, defaults.categoryTemplates, config.categoryTemplates || {});
config.modalStyles = $.extend({}, defaults.modalStyles, config.modalStyles || {});
return config;
}
function clonePlainObject(obj) {
return JSON.parse(JSON.stringify(obj || {}));
}
function normalizeMenuLabel(value) {
return String(value || '').replace(/\s+/g, ' ').trim().toLowerCase();
}
function readSettingsOptionState(fallback) {
var base = clonePlainObject(fallback || {});
var stored;
var raw = null;
if (!mw.user || !mw.user.options || typeof mw.user.options.get !== 'function') return base;
stored = mw.user.options.get(settingsOptionName);
if (typeof stored === 'string' && stored.trim()) {
try {
raw = JSON.parse(stored);
} catch (e) {
console.warn('RemoverCore: не удалось прочитать сохранённые настройки полностью, будут использованы данные loader.', e);
}
}
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return base;
return $.extend({}, raw, base, ('quickPhrases' in raw) ? { quickPhrases: raw.quickPhrases } : {});
}
function normalizeQuickPhraseValue(value) {
return String(value == null ? '' : value).replace(/\r\n?/g, '\n').trim();
}
function normalizeQuickPhrasesList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
var normalized = [];
(source || []).forEach(function (value) {
var phrase = normalizeQuickPhraseValue(value);
if (phrase && normalized.indexOf(phrase) === -1) normalized.push(phrase);
});
return normalized;
}
function collectSettingsMenuMeta() {
var labels = [];
var articleLabels = [];
var categoryLabels = [];
var idToLabel = {};
var labelByNorm = {};
var labelOrder = {};
function collect(items, targetLabels) {
items.forEach(function (item) {
var id;
var label;
var normLabel;
if (!item || item.type === 'separator') return;
id = String(item.id || '').trim();
label = String(item.label || '').trim();
normLabel = normalizeMenuLabel(label);
if (id && label && !(id in idToLabel)) idToLabel[id] = label;
if (label && targetLabels.indexOf(label) === -1) targetLabels.push(label);
if (normLabel && !(normLabel in labelByNorm)) {
labelByNorm[normLabel] = label;
labelOrder[label] = labels.length;
labels.push(label);
}
});
}
collect(cfg.articleMenuItems, articleLabels);
collect(cfg.categoryMenuItems, categoryLabels);
return {
labels: labels,
articleLabels: articleLabels,
categoryLabels: categoryLabels,
idToLabel: idToLabel,
labelByNorm: labelByNorm,
labelOrder: labelOrder
};
}
function buildSettingsMenuItemsHint() {
var parts = [];
if (settingsArticleItemLabels.length) {
parts.push('<div class="rmSettingsHintRow"><span class="rmSettingsHintBadge">Статьи</span>' + escapeHtml(settingsArticleItemLabels.join(', ')) + '.</div>');
}
if (settingsCategoryItemLabels.length) {
parts.push('<div class="rmSettingsHintRow"><span class="rmSettingsHintBadge">Категории</span>' + escapeHtml(settingsCategoryItemLabels.join(', ')) + '.</div>');
}
return parts.length ? '<div class="rmSettingsHintList">' + parts.join('') + '</div>' : '';
}
function getDefaultSettings() {
return {
version: settingsVersion,
excludedNamespaces: normalizeNumberList(cfg.excludedNamespaces, []),
notifyAuthor: !!cfg.defaultNotifyAuthor,
subscribeTopic: !!cfg.defaultSubscribeTopic,
menuTitle: (typeof cfg.menuTitle === 'string' && cfg.menuTitle.trim()) ? cfg.menuTitle.trim() : 'Remover',
disabledItems: [],
quickPhrases: normalizeQuickPhrasesList(cfg.quickPhrases, []),
showMenuIcons: !!cfg.showMenuIcons,
signatureSeparator: (typeof cfg.signatureSeparator === 'string') ? cfg.signatureSeparator.trim() : ''
};
}
function normalizeNumberList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
return source
.map(function (value) { return parseInt(value, 10); })
.filter(function (value, index, arr) { return !isNaN(value) && arr.indexOf(value) === index; })
.sort(function (a, b) { return a - b; });
}
function normalizeDisabledItemValue(value) {
var token = String(value || '').trim();
if (!token) return null;
if (settingsItemLabelById[token]) return settingsItemLabelById[token];
return settingsItemLabelByNorm[normalizeMenuLabel(token)] || null;
}
function compareSettingsMenuLabels(a, b) {
var ai = Object.prototype.hasOwnProperty.call(settingsItemLabelOrder, a) ? settingsItemLabelOrder[a] : Number.MAX_SAFE_INTEGER;
var bi = Object.prototype.hasOwnProperty.call(settingsItemLabelOrder, b) ? settingsItemLabelOrder[b] : Number.MAX_SAFE_INTEGER;
if (ai !== bi) return ai - bi;
return a.localeCompare(b, 'ru');
}
function normalizeDisabledItemsList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
return source
.map(normalizeDisabledItemValue)
.filter(function (value, index, arr) { return value && arr.indexOf(value) === index; })
.sort(compareSettingsMenuLabels);
}
function normalizeMenuTitleSetting(value, fallback) {
var menuTitle = String(value || '').trim();
if (!menuTitle) return fallback;
if (menuTitle === MENU_TITLE_PRESET_CACTIONS || /^(#)?p-cactions$/i.test(menuTitle)) return MENU_TITLE_PRESET_CACTIONS;
if (menuTitle === MENU_TITLE_PRESET_PAGE || /^(#)?p-page$/i.test(menuTitle) || /^(#)?p-actions$/i.test(menuTitle)) return MENU_TITLE_PRESET_PAGE;
if (menuTitle === MENU_TITLE_PRESET_TOOLS || /^(#)?p-tb$/i.test(menuTitle)) return MENU_TITLE_PRESET_TOOLS;
return menuTitle;
}
function normalizeRemoverSettings(raw) {
var defaults = clonePlainObject(settingsDefaults);
var source = (raw && typeof raw === 'object') ? raw : {};
return {
version: settingsVersion,
excludedNamespaces: normalizeNumberList(source.excludedNamespaces, defaults.excludedNamespaces || []),
notifyAuthor: ('notifyAuthor' in source) ? !!source.notifyAuthor : !!defaults.notifyAuthor,
subscribeTopic: ('subscribeTopic' in source) ? !!source.subscribeTopic : !!defaults.subscribeTopic,
menuTitle: normalizeMenuTitleSetting(
(typeof source.menuTitle === 'string' && source.menuTitle.trim()) ? source.menuTitle : '',
typeof defaults.menuTitle === 'string' && defaults.menuTitle.trim() ? defaults.menuTitle.trim() : 'Remover'
),
disabledItems: normalizeDisabledItemsList(source.disabledItems, defaults.disabledItems || []),
quickPhrases: normalizeQuickPhrasesList(source.quickPhrases, defaults.quickPhrases || []),
showMenuIcons: ('showMenuIcons' in source) ? !!source.showMenuIcons : !!defaults.showMenuIcons,
signatureSeparator: (typeof source.signatureSeparator === 'string')
? source.signatureSeparator.trim()
: (typeof defaults.signatureSeparator === 'string' ? defaults.signatureSeparator.trim() : '')
};
}
function areRemoverSettingsEqual(a, b) {
return JSON.stringify(normalizeRemoverSettings(a)) === JSON.stringify(normalizeRemoverSettings(b));
}
function updateStoredSettingsState(settings, skipUserOptionsSync) {
var normalized = normalizeRemoverSettings(settings);
state.settings = clonePlainObject(normalized);
setAlert = normalized.notifyAuthor;
setSubscribe = normalized.subscribeTopic;
signatureSeparator = normalized.signatureSeparator;
state.setAlert = setAlert;
state.setSubscribe = setSubscribe;
if (!skipUserOptionsSync && mw.user && mw.user.options && typeof mw.user.options.set === 'function') {
mw.user.options.set(settingsOptionName, JSON.stringify(normalized));
}
return normalized;
}
function splitSettingsInput(value) {
return String(value || '')
.split(/[\s,;]+/)
.map(function (part) { return part.trim(); })
.filter(Boolean);
}
function splitSettingsListInput(value) {
return String(value || '')
.split(/[\n,;]+/)
.map(function (part) { return part.trim(); })
.filter(Boolean);
}
function parseNamespaceInput(value) {
var tokens = splitSettingsInput(value);
var invalid = [];
var values = [];
tokens.forEach(function (token) {
var parsed = parseInt(token, 10);
if (String(parsed) !== token) invalid.push(token);
else if (values.indexOf(parsed) === -1) values.push(parsed);
});
values.sort(function (a, b) { return a - b; });
return { values: values, invalid: invalid };
}
function parseDisabledItemsInput(value) {
var tokens = splitSettingsListInput(value);
var invalid = [];
var values = [];
tokens.forEach(function (token) {
var normalized = normalizeDisabledItemValue(token);
if (!normalized) invalid.push(token);
else if (values.indexOf(normalized) === -1) values.push(normalized);
});
values.sort(compareSettingsMenuLabels);
return { values: values, invalid: invalid };
}
function formatPagesWithAnd(names, prefix) {
var p = prefix || ':';
var links = (names || []).map(function (n) { return '[[' + p + n + ']]'; });
if (!links.length) return '';
if (links.length === 1) return links[0];
return links.slice(0, -1).join(', ') + ' и ' + links[links.length - 1];
}
function formatCatLink(name) { return '[[:Категория:' + name + ']]'; }
function formatMergeStatus(status) {
return { already_exists: 'уже был', updated: 'дополнен', created: 'создан' }[status] || status;
}
function applyGeneratedText($el, generated) {
var cur = $el.val();
var prev = $el.data('rmGenerated') || '';
if (!prev || cur.indexOf(prev) === 0) {
$el.val(generated + cur.slice(prev.length));
} else {
$el.val(generated + (cur ? '\n' + cur : ''));
}
$el.data('rmGenerated', generated);
}
function getCurrentQuickPhrases() {
return normalizeQuickPhrasesList(
state.settings && state.settings.quickPhrases,
settingsDefaults.quickPhrases || []
);
}
function insertTextIntoTextarea($el, text) {
var el = $el && $el[0];
var value;
var start;
var end;
var updatedValue;
var caretPos;
if (!el) return;
text = String(text || '');
if (!text) return;
value = $el.val() || '';
start = typeof el.selectionStart === 'number' ? el.selectionStart : value.length;
end = typeof el.selectionEnd === 'number' ? el.selectionEnd : start;
updatedValue = value.slice(0, start) + text + value.slice(end);
caretPos = start + text.length;
$el.val(updatedValue).trigger('input').trigger('change').focus();
if (typeof el.setSelectionRange === 'function') el.setSelectionRange(caretPos, caretPos);
}
function buildQuickPhrasesPanelHtml(textareaId) {
var phrases = getCurrentQuickPhrases();
if (!phrases.length) return '';
return joinHtml([
'<div class="rmQuickPhrasesPanel ', RESIZE_CLASS, '" data-rm-target="', textareaId, '">',
phrases.map(function (phrase) {
return joinHtml([
'<button type="button" class="rmQuickPhraseActionBtn" data-rm-target="', textareaId,
'" data-rm-phrase="', escapeHtml(phrase), '">',
escapeHtml(phrase),
'</button>'
]);
}).join(''),
'</div>'
]);
}
function getMultiNominationCommentText(commentsByArticle, articleTitle) {
var key = normTitle(articleTitle);
if (!commentsByArticle || !Object.prototype.hasOwnProperty.call(commentsByArticle, key)) return '';
return normalizeQuickPhraseValue(commentsByArticle[key]);
}
function buildMultiNominationText(articles, bodyText, commentsByArticle, options) {
var opts = options || {};
var list = Array.isArray(articles) ? articles.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasArticleComments = false;
var headingLevel = Math.max(2, parseInt(opts.headingLevel, 10) || 3);
var headingMarks = new Array(headingLevel + 1).join('=');
var articleSections = list.map(function (a) {
var comment = getMultiNominationCommentText(commentsByArticle, a);
if (comment) hasArticleComments = true;
return '\n' + headingMarks + ' [[:' + a + ']] ' + headingMarks + '\n' + (comment ? appendNominationSignature(comment) + '\n' : '');
}).join('');
var commonSectionText = body
? appendNominationSignature(body)
: (hasArticleComments ? '' : appendNominationSignature(''));
return articleSections + '\n' + headingMarks + ' По всем ' + headingMarks + '\n' + commonSectionText;
}
function buildMultiNominationListText(pages, bodyText, commentsByPage) {
var list = Array.isArray(pages) ? pages.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasPageComments = false;
var pageLines = list.map(function (pageName) {
var comment = getMultiNominationCommentText(commentsByPage, pageName);
if (comment) hasPageComments = true;
return '* [[:' + pageName + ']]' + (comment ? '\n*: ' + appendNominationSignature(comment) : '');
}).join('\n');
var commonText = body
? appendNominationSignature(body)
: (hasPageComments ? '' : appendNominationSignature(''));
return pageLines + (pageLines && commonText ? '\n' : '') + commonText;
}
function collectMultiNominationComments(normalizePageName) {
var comments = {};
var normalize = typeof normalizePageName === 'function' ? normalizePageName : normTitle;
$('.rmMultiArticleBlock').each(function () {
var $block = $(this);
var article = normalize(($block.find('.rmArticleInput').val() || '').trim());
var comment = normalizeQuickPhraseValue($block.find('.rmArticleCommentInput').val());
if (!article) return;
comments[article] = comment;
});
return comments;
}
function getNominationPublishText(job) {
if (job && job.isMulti) return String(job.msg || '');
return appendNominationSignature(job && job.msg);
}
function getNominationConflictRule(job) {
if (!job || job.mode !== 'nominate') return null;
if (job.opId === 'tRm' || job.opId === 'mRm') {
return {
id: 'ku',
label: 'КУ',
namePattern: '(?:к\\s*удалению|ку)',
detect: function (articleText) {
var match = String(articleText || '').match(/\{\{\s*((?:к\s*удалению|ку))\s*(?:\||\}\})/i);
if (!match) return null;
var templateName = ucfirst(String(match[1] || '').replace(/\s+/g, ' ').trim());
return {
label: 'КУ',
templateName: templateName || 'КУ',
templateDisplay: '{{' + (templateName || 'КУ') + '}}'
};
}
};
}
return null;
}
function detectNominationConflict(articleText, job) {
var rule = getNominationConflictRule(job);
if (!rule || typeof rule.detect !== 'function') return null;
return rule.detect(articleText);
}
function getConflictDecisionForPage(job, pageName) {
var decisions = job && job.conflictDecisions;
var key = normTitle(pageName);
return decisions && decisions[key] ? decisions[key] : null;
}
function getCategoryMergeRe() {
return new RegExp('\\{\\{\\s*(?:' + cfg.categoryTemplates.merge + ')\\s*\\|\\s*([^\\}]+)\\}\\}', 'i');
}
function eachSequential(targets, iteratee, done) {
var i = 0;
(function next(err) {
if (err || i >= targets.length) { done(err || null); return; }
iteratee(targets[i++], next);
}(null));
}
function normalizeSectionForLink(sectionTitle) {
return (sectionTitle || '').trim()
.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, function (_, target, label) {
var v = (label || target || '').trim();
return v.charAt(0) === ':' ? v.slice(1) : v;
})
.replace(/''+/g, '').replace(/\s+/g, ' ').trim();
}
function getViewportWidth() {
return Math.floor(Math.max(
(document.documentElement && document.documentElement.clientWidth) || 0,
(typeof window.innerWidth === 'number' && window.innerWidth) || 0,
$(window).width() || 0
));
}
function getVisualViewportWidth() {
var widths = [];
if (window.visualViewport && typeof window.visualViewport.width === 'number' && window.visualViewport.width > 0) widths.push(window.visualViewport.width);
if (window.screen && window.screen.width > 0) widths.push(window.screen.width);
return widths.length ? Math.floor(Math.min.apply(Math, widths)) : getViewportWidth();
}
function isTouchModalDevice() {
return !!(
(window.matchMedia && window.matchMedia('(pointer: coarse)').matches) ||
('ontouchstart' in window) ||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 0) ||
(navigator.msMaxTouchPoints && navigator.msMaxTouchPoints > 0)
);
}
function getModalLayout() {
var minWidth = parseInt(sz.taMinW, 10) || 180;
var layoutWidth = getViewportWidth();
var visualWidth = getVisualViewportWidth();
var contentWidth = Math.max(minWidth, Math.floor($('#content').width() || $('#content').innerWidth() || $(window).width() || minWidth));
var isMobile = layoutWidth <= sz.mobileBp;
var isTouchDesktop = !isMobile &&
isTouchModalDevice() &&
visualWidth > 0 &&
visualWidth <= sz.mobileBp &&
layoutWidth >= visualWidth + sz.touchDesktopGap;
var useFullWidth = isMobile || isTouchDesktop;
var maxOuterWidth;
var defaultOuterWidth;
var desktopWidth;
if (isMobile) maxOuterWidth = Math.max(minWidth, (visualWidth || contentWidth) - sz.viewportGap);
else if (isTouchDesktop) maxOuterWidth = Math.max(minWidth, contentWidth - 32);
else maxOuterWidth = Math.max(minWidth, Math.min(contentWidth, (visualWidth ? visualWidth - sz.viewportGap : contentWidth)));
desktopWidth = Math.max(minWidth, Math.floor(contentWidth * sz.modalRatio));
if (useFullWidth || desktopWidth < sz.modalMinWide) defaultOuterWidth = contentWidth;
else if (desktopWidth < sz.modalDefaultWide) defaultOuterWidth = sz.modalDefaultWide;
else defaultOuterWidth = desktopWidth;
return {
minWidth: minWidth,
isMobile: isMobile,
isTouchDesktop: isTouchDesktop,
useFullWidth: useFullWidth,
shouldCenter: useFullWidth || mwCfg.skin === 'minerva',
maxOuterWidth: maxOuterWidth,
defaultOuterWidth: Math.min(defaultOuterWidth, maxOuterWidth)
};
}
function getDefaultResizableWidth(frameWidth) {
var layout = getModalLayout();
return Math.max(layout.minWidth, layout.defaultOuterWidth - Math.floor(frameWidth || 0));
}
function getBoxFrameWidth($el) {
function px(prop) {
var n = parseFloat($el.css(prop));
return isNaN(n) ? 0 : n;
}
return px('padding-left') + px('padding-right') + px('border-left-width') + px('border-right-width');
}
// ═══════════════════════════════════════════════════════════════════════════
// API
// ═══════════════════════════════════════════════════════════════════════════
function getApiUrl() {
return (mw.util && typeof mw.util.wikiScript === 'function') ? mw.util.wikiScript('api') : '/w/api.php';
}
function getCsrfTokenValue() {
return (mw.user && mw.user.tokens && typeof mw.user.tokens.get === 'function')
? mw.user.tokens.get('csrfToken')
: null;
}
function storeCsrfToken(token) {
if (!token || !mw.user || !mw.user.tokens || typeof mw.user.tokens.set !== 'function') return;
mw.user.tokens.set({ csrfToken: token });
}
function isValidCsrfToken(token) {
return typeof token === 'string' && !!token && token !== '+\\';
}
function fetchCsrfToken(forceRefresh, callback) {
var cachedToken = getCsrfTokenValue();
if (!forceRefresh && isValidCsrfToken(cachedToken)) {
callback(cachedToken);
return;
}
$.ajax({
url: getApiUrl(),
method: 'GET',
dataType: 'json',
data: { action: 'query', meta: 'tokens', type: 'csrf', format: 'json' }
})
.done(function (data) {
var token = data && data.query && data.query.tokens && data.query.tokens.csrftoken;
if (isValidCsrfToken(token)) {
storeCsrfToken(token);
callback(token);
return;
}
callback(null);
})
.fail(function () {
callback(null);
});
}
function apiReq(params, mode, callback) {
var isWrite = mode === 'edit' || mode === 'discussiontoolssubscribe' || mode === 'options';
function sendRequest(retryWithFreshToken) {
var reqParams = $.extend({}, params, { format: 'json', action: mode });
if (!isWrite) {
$.ajax({ url: getApiUrl(), method: 'GET', data: reqParams, dataType: 'json' })
.done(function (data) { if (callback) callback(data); })
.fail(function (jqXHR, status) {
console.error('Ошибка API: ' + status);
if (callback) callback({ error: { code: 'network', info: status } });
});
return;
}
fetchCsrfToken(!!retryWithFreshToken, function (token) {
if (!isValidCsrfToken(token)) {
if (callback) callback({ error: { code: 'badtoken', info: 'Не удалось получить CSRF-токен.' } });
return;
}
reqParams.token = token;
$.ajax({ url: getApiUrl(), method: 'POST', data: reqParams, dataType: 'json' })
.done(function (data) {
var err = data && data.error;
var isBadToken = err && (err.code === 'badtoken' || /invalid csrf token/i.test(String(err.info || '')));
if (isBadToken && !retryWithFreshToken) {
sendRequest(true);
return;
}
if (callback) callback(data);
})
.fail(function (jqXHR, status) {
console.error('Ошибка API: ' + status);
if (callback) callback({ error: { code: 'network', info: status } });
});
});
}
sendRequest(false);
}
function saveSettingsToServer(settings, callback) {
var normalized = normalizeRemoverSettings(settings);
if (!mwCfg.wgUserName) {
callback({ code: 'notloggedin', info: 'Сохранять настройки можно только после входа в учётную запись.' });
return;
}
apiReq({ optionname: settingsOptionName, optionvalue: JSON.stringify(normalized) }, 'options', function (resp) {
if (resp && resp.options === 'success') {
callback(null, updateStoredSettingsState(normalized));
return;
}
callback((resp && resp.error) || { code: 'options_failed', info: 'Не удалось сохранить настройки.' });
});
}
function resetSettingsOnServer(callback) {
if (!mwCfg.wgUserName) {
callback({ code: 'notloggedin', info: 'Сбрасывать настройки можно только после входа в учётную запись.' });
return;
}
apiReq({ change: settingsOptionName }, 'options', function (resp) {
if (resp && resp.options === 'success') {
callback(null, updateStoredSettingsState(settingsDefaults, true));
return;
}
callback((resp && resp.error) || { code: 'options_failed', info: 'Не удалось сбросить настройки.' });
});
}
function getFirstQueryPage(data) {
var pages = data && data.query && data.query.pages;
if (!pages) return null;
return pages[Object.keys(pages)[0]] || null;
}
function extractRevisionContent(rev) {
if (!rev) return null;
if (typeof rev['*'] === 'string') return rev['*'];
if (rev.slots && rev.slots.main) {
if (typeof rev.slots.main['*'] === 'string') return rev.slots.main['*'];
if (typeof rev.slots.main.content === 'string') return rev.slots.main.content;
}
return null;
}
function makeReadError(apiError, fallbackCode, fallbackInfo) {
var err = apiError || {};
return {
code: err.code || fallbackCode || 'read_failed',
info: err.info || fallbackInfo || 'Не удалось получить содержимое.'
};
}
function getTextWithTimestamp(pageName, callback) {
apiReq({ prop: 'revisions', rvprop: 'content|timestamp', rvslots: 'main', titles: pageName }, 'query', function (data) {
var content;
var page;
if (data && data.error) {
callback(null, null, data.error);
return;
}
if (!data || !data.query || !data.query.pages) {
callback(null, null, { code: 'read_failed', info: 'Некорректный ответ API при чтении страницы.' });
return;
}
page = getFirstQueryPage(data);
if (!page) {
callback(null, null, { code: 'read_failed', info: 'API не вернул данные страницы.' });
return;
}
if (page.invalid !== undefined) {
callback(null, null, { code: 'invalidtitle', info: page.invalidreason || 'Некорректное название страницы.' });
return;
}
if (page.missing !== undefined) {
callback(null, null, null);
return;
}
if (!page.revisions || !page.revisions.length) {
callback(null, null, { code: 'read_failed', info: 'API не вернул ревизии страницы.' });
return;
}
content = extractRevisionContent(page.revisions[0]);
if (content === null) {
callback(null, null, { code: 'content_missing', info: 'API не вернул текст страницы.' });
return;
}
callback(content, page.revisions[0].timestamp || null, null);
});
}
function getText(pageName, callback) {
getTextWithTimestamp(pageName, function (text, baseTimestamp, err) { callback(text, err); });
}
function editPageContent(pageTitle, options, buildFn, callback) {
var opts = options || {};
var maxRetries = Math.max(0, parseInt(opts.editConflictRetries, 10) || 1);
(function attempt(retry) {
getTextWithTimestamp(pageTitle, function (sourceText, baseTimestamp, readErr) {
if (readErr) {
callback(makeReadError(readErr, opts.readErrorCode || 'read_failed', 'Не удалось получить содержимое страницы «' + pageTitle + '».'));
return;
}
if (sourceText === null) {
callback({ code: opts.readErrorCode || 'read_failed', info: opts.readError || 'Страница «' + pageTitle + '» не существует.' });
return;
}
var done = (function () {
var called = false;
return function (result) {
if (called) return;
called = true;
if (!result || result.error) { callback((result && result.error) || { code: 'build_failed', info: 'Не удалось подготовить правку.' }, result && result.meta || null); return; }
if (result.skip) { callback(null, result.meta || null); return; }
if (typeof result.text !== 'string') { callback({ code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
var ep = { title: pageTitle, text: result.text, summary: result.summary || opts.summary || '' };
if (opts.watchlist) ep.watchlist = opts.watchlist;
if (opts.assertuser) ep.assertuser = opts.assertuser;
if (opts.createonly) ep.createonly = opts.createonly;
if (opts.useBaseTimestamp !== false && baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) {
var err = resp && resp.error ? resp.error : null;
if (err && err.code === 'editconflict' && retry < maxRetries) { attempt(retry + 1); return; }
callback(err, result.meta || null);
});
};
}());
var maybe = buildFn(sourceText, done);
if (maybe !== undefined) done(maybe);
});
}(0));
}
// ═══════════════════════════════════════════════════════════════════════════
// ШАБЛОНЫ: удаление и вставка
// ═══════════════════════════════════════════════════════════════════════════
function findBalancedTemplateEnd(text, start) {
var depth = 0;
var i = start;
var len = text.length;
while (i < len - 1) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') {
depth++;
i += 2;
continue;
}
if (text.charAt(i) === '}' && text.charAt(i + 1) === '}') {
depth--;
i += 2;
if (depth === 0) return i;
continue;
}
i++;
}
return -1;
}
function splitTemplateTopLevelParts(innerText) {
var parts = [];
var start = 0;
var templateDepth = 0;
var linkDepth = 0;
var i = 0;
var text = String(innerText || '');
while (i < text.length) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') {
templateDepth++;
i += 2;
continue;
}
if (text.charAt(i) === '}' && text.charAt(i + 1) === '}' && templateDepth > 0) {
templateDepth--;
i += 2;
continue;
}
if (text.charAt(i) === '[' && text.charAt(i + 1) === '[') {
linkDepth++;
i += 2;
continue;
}
if (text.charAt(i) === ']' && text.charAt(i + 1) === ']' && linkDepth > 0) {
linkDepth--;
i += 2;
continue;
}
if (text.charAt(i) === '|' && templateDepth === 0 && linkDepth === 0) {
parts.push(text.slice(start, i));
start = i + 1;
}
i++;
}
parts.push(text.slice(start));
return parts;
}
function getTemplateMatchAt(text, start, nameRe) {
var end = findBalancedTemplateEnd(text, start);
var parts;
var rawName;
var name;
if (end < 0) return null;
parts = splitTemplateTopLevelParts(text.slice(start + 2, end - 2));
rawName = String(parts.shift() || '').trim();
name = rawName.replace(/^(?:subst|подст)\s*:\s*/i, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
if (!nameRe.test(name)) return null;
return {
start: start,
end: end,
text: text.slice(start, end),
name: rawName,
params: parts.map(function (part) { return part.trim(); })
};
}
function findTemplateByPattern(text, namePattern) {
var source = String(text || '');
var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i');
var i = 0;
var end;
var match;
while (i < source.length - 1) {
if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') {
match = getTemplateMatchAt(source, i, nameRe);
if (match) return match;
end = findBalancedTemplateEnd(source, i);
if (end > i) { i = end; continue; }
}
i++;
}
return null;
}
function hasTemplateWithDateByPattern(text, namePattern, dateIso) {
var source = String(text || '');
var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i');
var targetDate = convertToStandardDate(dateIso);
var i = 0;
var end;
var match;
var templateDate;
if (!targetDate) return false;
while (i < source.length - 1) {
if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') {
match = getTemplateMatchAt(source, i, nameRe);
if (match) {
templateDate = convertToStandardDate(String(match.params[0] || '').replace(/^\s*1\s*=\s*/, ''));
if (templateDate === targetDate) return true;
i = match.end;
continue;
}
end = findBalancedTemplateEnd(source, i);
if (end > i) { i = end; continue; }
}
i++;
}
return false;
}
function getTemplateRemovalRange(text, match) {
var start = match.start;
var end = match.end;
var before = text.slice(0, start).match(/<noinclude>\s*$/i);
var after;
if (before) {
after = text.slice(end).match(/^\s*<\/noinclude>\s*\n?/i);
if (after) {
return { start: before.index, end: end + after[0].length };
}
}
after = text.slice(end).match(/^[ \t]*(?:\r?\n)?/);
if (after) end += after[0].length;
return { start: start, end: end };
}
function stripTemplatesByPattern(text, namePattern) {
var source = String(text || '');
var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i');
var ranges = [];
var out = [];
var pos = 0;
var i = 0;
var end;
var match;
var range;
while (i < source.length - 1) {
if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') {
match = getTemplateMatchAt(source, i, nameRe);
if (match) {
range = getTemplateRemovalRange(source, match);
ranges.push(range);
i = Math.max(match.end, range.end);
continue;
}
end = findBalancedTemplateEnd(source, i);
if (end > i) { i = end; continue; }
}
i++;
}
if (!ranges.length) return { text: source, removed: false };
ranges.forEach(function (r) {
if (r.start < pos) r.start = pos;
out.push(source.slice(pos, r.start));
pos = r.end;
});
out.push(source.slice(pos));
return { text: out.join(''), removed: true };
}
function removeTemplatesByAliases(text, aliases) {
var seen = {}, patterns = [];
aliases.forEach(function (alias) {
var tpl = alias.replace(RE_TEMPLATE_NS, '').trim();
var key = tpl.toLowerCase();
if (!tpl || seen[key]) return;
seen[key] = true;
patterns.push(escapeRegExp(tpl).replace(/\\ /g, '[ _]+'));
});
if (!patterns.length) return { text: text, removed: false };
return stripTemplatesByPattern(text, '(?:' + patterns.join('|') + ')');
}
function removeTransferTemplatesLocal(articleText, transferMode) {
var result = { text: articleText, removedKbu: false, removedKul: false, removedHangon: false };
if (transferMode === 'none') return result;
if (transferMode === 'kbu' || transferMode === 'both') {
var kbu = stripTemplatesByPattern(result.text, '(?:' + KBU_PATTERN_STR + ')');
result.text = kbu.text; result.removedKbu = kbu.removed;
}
if (transferMode === 'kul' || transferMode === 'both') {
var kul = stripTemplatesByPattern(result.text, KUL_PATTERN_STR);
result.text = kul.text; result.removedKul = kul.removed;
}
var hangon = stripTemplatesByPattern(result.text, HANGON_PATTERN_STR);
result.text = hangon.text; result.removedHangon = hangon.removed;
return result;
}
function removeTransferTemplatesWithApiFallback(pageName, articleText, transferMode, localResult, callback) {
var needKbu = (transferMode === 'kbu' || transferMode === 'both') && !localResult.removedKbu;
var needKul = (transferMode === 'kul' || transferMode === 'both') && !localResult.removedKul;
var needHangon = transferMode !== 'none' && !localResult.removedHangon;
if (!needKbu && !needKul && !needHangon) { callback(localResult); return; }
var titleMap = {};
if (needKbu) ['Шаблон:К быстрому удалению','Шаблон:К отсроченному удалению','Шаблон:Deleteslow','Шаблон:Ds'].forEach(function (t) { titleMap[t] = true; });
if (needKul) titleMap['Шаблон:К улучшению'] = true;
if (needHangon) { titleMap['Шаблон:Hangon'] = true; titleMap['Шаблон:Hang-on'] = true; }
apiReq({ prop: 'templates', titles: pageName, tllimit: 'max' }, 'query', function (data) {
var page = getFirstQueryPage(data);
if (page && page.templates) {
page.templates.forEach(function (tpl) {
var norm = normalizeTemplateName(tpl.title);
if ((needKbu && (RE_KBU_PATTERNS.test(norm) || norm === 'к быстрому удалению' || norm === 'к отсроченному удалению' || norm === 'deleteslow' || norm === 'ds')) ||
(needKul && RE_KUL_PATTERN.test(norm)) ||
(needHangon && RE_HANGON.test(norm))) {
titleMap[tpl.title] = true;
}
});
}
var titles = Object.keys(titleMap);
if (!titles.length) { callback(localResult); return; }
function normalizeAliasKey(title) { return (title || '').replace(/_/g, ' ').toLowerCase().trim(); }
function collectAndApplyAliases() {
var allAliases = [];
titles.forEach(function (title) { allAliases = allAliases.concat(tplAliasCache[title] || [title]); });
var dedup = {}, aliases = [];
allAliases.forEach(function (alias) {
var key = normalizeAliasKey(alias);
if (!key || dedup[key]) return;
dedup[key] = true; aliases.push(alias);
});
var updated = $.extend({}, localResult);
var r = removeTemplatesByAliases(updated.text, aliases);
updated.text = r.text;
if (r.removed) {
if (needKbu) updated.removedKbu = true;
if (needKul) updated.removedKul = true;
if (needHangon) updated.removedHangon = true;
}
callback(updated);
}
var missingTitles = titles.filter(function (t) { return !tplAliasCache[t]; });
if (!missingTitles.length) { collectAndApplyAliases(); return; }
(function resolveChunk(offset) {
if (offset >= missingTitles.length) { collectAndApplyAliases(); return; }
var chunk = missingTitles.slice(offset, offset + 20);
var chunkByKey = {};
chunk.forEach(function (t) { chunkByKey[normalizeAliasKey(t)] = t; });
apiReq({ prop: 'redirects', rdlimit: 'max', titles: chunk.join('|') }, 'query', function (resp) {
var pages = resp && resp.query && resp.query.pages ? resp.query.pages : {};
Object.keys(pages).forEach(function (pid) {
var p = pages[pid];
if (!p || !p.title) return;
var sourceTitle = chunkByKey[normalizeAliasKey(p.title)];
if (!sourceTitle) return;
var found = [p.title].concat((p.redirects || []).map(function (r) { return r.title; }));
var seen = {}, unique = [];
found.forEach(function (t) { var k = normalizeAliasKey(t); if (!k || seen[k]) return; seen[k] = true; unique.push(t); });
tplAliasCache[sourceTitle] = unique.length ? unique : [sourceTitle];
});
chunk.forEach(function (t) { if (!tplAliasCache[t]) tplAliasCache[t] = [t]; });
resolveChunk(offset + 20);
});
}(0));
});
}
// ─── Вставка шаблонов ────────────────────────────────────────────────────
function findInsertPositionAfterProjectTemplates(text) {
var pos = 0, len = text.length;
while (pos < len) {
var wsMatch = text.slice(pos).match(/^[\t ]*\n/);
if (wsMatch) { pos += wsMatch[0].length; continue; }
if (text.charAt(pos) !== '{' || text.charAt(pos + 1) !== '{') break;
var afterOpen = text.slice(pos + 2);
var nameMatch = afterOpen.match(/^[\s]*([\s\S]*?)[\s]*(?:\||\}\})/);
var templateEnd;
if (!nameMatch) break;
var tplName = nameMatch[1].toLowerCase().replace(/\s+/g, ' ').trim();
if (!/^статья проекта(\s|$)/.test(tplName) && tplName !== 'блок проектов статьи') break;
templateEnd = findBalancedTemplateEnd(text, pos);
if (templateEnd < 0) break;
pos = templateEnd;
if (pos < len && text.charAt(pos) === '\n') pos++;
}
return pos;
}
function insertTplOnTalkPage(text, tplText, sep) {
var s = (sep === undefined) ? '\n' : sep;
var insertPos = findInsertPositionAfterProjectTemplates(text);
if (insertPos === 0) return tplText + (text.length ? s + text.replace(/^\n+/, '') : '');
var before = text.slice(0, insertPos).replace(/\n+$/, '');
var after = text.slice(insertPos).replace(/^\n+/, '');
return before + '\n' + tplText + (after.length ? s + after : '');
}
function wrapInNoinclude(text, templateText) {
var match = text.match(RE_NOINCLUDE);
if (match) {
// Если перед noinclude есть непробельный контент — вставляем новый noinclude сверху
var before = text.slice(0, text.indexOf(match[0]));
if (/\S/.test(before)) {
return '<noinclude>' + templateText + '</noinclude>\n' + text;
}
var content = match[2];
if (content.length > 0 && content.charAt(content.length - 1) !== '\n') content += '\n';
return text.replace(match[0], match[1] + '<noinclude>' + content + templateText + '\n</noinclude>');
}
return '<noinclude>' + templateText + '</noinclude>\n' + text;
}
function upsertRetTemplateOnTalkPage(text, dateIso, sectionTitle) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var tplRe = /\{\{\s*(?:subst\s*:\s*)?(?:оставлено)\s*([^}]*)\}\}/i;
var tplMatch = source.match(tplRe);
function buildTpl(dateValue, sectionValue) {
var tpl = 'оставлено|' + dateValue;
if (sectionValue) tpl += '|l1=' + sectionValue;
return T_OPEN + tpl + T_CLOSE;
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, buildTpl(dateIso, normalizedSection), '\n'), status: 'created' };
}
var parts = tplMatch[1].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); });
var stdDate = convertToStandardDate(dateIso);
if (existingDates.indexOf(stdDate) !== -1) return { text: source, status: 'already_present' };
var nextIdx = existingDates.length + 1;
var suffix = '|' + dateIso + (normalizedSection ? '|l' + nextIdx + '=' + normalizedSection : '');
return {
text: source.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}\s*$/, suffix + '}}'); }),
status: 'updated'
};
}
function buildConditionalRetTemplateText(dateIso, sectionTitle, reasonText, deadlineText, sectionIndex) {
var normalizedSection = String(sectionTitle || '').trim();
var normalizedReason = normalizeQuickPhraseValue(reasonText);
var normalizedDeadline = String(deadlineText || '').trim();
var index = parseInt(sectionIndex, 10);
var tpl = 'условно оставлено|' + dateIso;
if (isNaN(index) || index < 1) index = 1;
if (normalizedSection) tpl += '|l' + index + '=' + normalizedSection;
if (normalizedReason) tpl += '|пояснение=' + normalizedReason;
if (normalizedDeadline) tpl += '|срок=' + normalizedDeadline;
return T_OPEN + tpl + T_CLOSE;
}
function upsertConditionalRetTemplateOnTalkPage(text, dateIso, sectionTitle, reasonText, deadlineText) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var normalizedReason = normalizeQuickPhraseValue(reasonText);
var normalizedDeadline = String(deadlineText || '').trim();
var tplRe = /\{\{\s*(?:subst\s*:\s*)?(?:условно\s*оставлено)\s*([^}]*)\}\}/i;
var tplMatch = source.match(tplRe);
if (!tplMatch) {
return {
text: insertTplOnTalkPage(source, buildConditionalRetTemplateText(dateIso, normalizedSection, normalizedReason, normalizedDeadline, 1), '\n'),
status: 'created'
};
}
var parts = tplMatch[1].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); });
var stdDate = convertToStandardDate(dateIso);
if (existingDates.indexOf(stdDate) !== -1) return { text: source, status: 'already_present' };
var nextIdx = existingDates.length + 1;
var suffix = '|' + dateIso;
if (normalizedSection) suffix += '|l' + nextIdx + '=' + normalizedSection;
if (normalizedReason) suffix += '|пояснение=' + normalizedReason;
if (normalizedDeadline) suffix += '|срок=' + normalizedDeadline;
return {
text: source.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}\s*$/, suffix + '}}'); }),
status: 'updated'
};
}
// ═══════════════════════════════════════════════════════════════════════════
// ПАЙПЛАЙН НОМИНАЦИИ
// ═══════════════════════════════════════════════════════════════════════════
function runNominationPipeline(steps) {
var s = steps;
var ctx = { templateMeta: null, nominationInfo: null };
var stages = [
{
name: 'шаблон',
fn: function (next) {
s.templateStep(function (err, meta) { ctx.templateMeta = meta || null; next(err); });
}
},
{
name: 'номинация',
pendingText: 'Публикуется номинация...',
successText: 'Номинация опубликована.',
errorText: 'Публикация номинации.',
fn: function (next) {
s.nominationStep(function (err, info) { ctx.nominationInfo = info || null; next(err); });
}
},
{
name: 'подписка',
shouldRun: function () {
var info = ctx.nominationInfo;
return !!(setSubscribe && info && info.pageTitle && info.sectionTitle);
},
fn: function (next) {
subscribeToTopic(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle, function () { next(); });
}
},
{
name: 'оповещение',
shouldRun: function () { return !!(setAlert && !s.skipNotify); },
fn: function (next) { s.notifyStep(ctx.nominationInfo, next); }
}
];
(function run(i) {
if (i >= stages.length) { if (typeof s.onSuccess === 'function') s.onSuccess(ctx); return; }
var stage = stages[i];
if (typeof stage.shouldRun === 'function' && !stage.shouldRun()) { run(i + 1); return; }
var statusId = stage.pendingText
? logStatus(stage.pendingText, null, { pending: true, trackError: false })
: null;
try {
stage.fn(function (err) {
if (err) {
if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), err, { statusId: statusId });
if (typeof s.onFailure === 'function') s.onFailure(stage.name, err, ctx);
else markSubmitError();
return;
}
if (statusId && stage.successText) logStatus(stage.successText, null, { statusId: statusId, trackError: false });
run(i + 1);
});
} catch (ex) {
var exErr = { code: 'exception', info: (ex && ex.message) ? ex.message : String(ex) };
if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), exErr, { statusId: statusId });
if (typeof s.onFailure === 'function') s.onFailure(stage.name, exErr, ctx);
else markSubmitError();
}
}(0));
}
// ─── Публикация номинации ────────────────────────────────────────────────
function publishNomination(opts, callback) {
var cb = callback || function () {};
function doPublish() {
apiReq({
title: opts.pageTitle,
section: 'new',
sectiontitle: opts.sectionTitle,
summary: opts.summary,
text: opts.text,
assertuser: mwCfg.wgUserName
}, 'edit', function (resp) {
cb(resp && resp.error ? resp.error : null);
});
}
if (opts.sectionTitle) {
if (!opts.navTemplate) { doPublish(); return; }
apiReq({ title: opts.pageTitle, createonly: '1', text: T_OPEN + opts.navTemplate + '-Навигация' + T_CLOSE + '\n', summary: makeSummary('автоматическая шапка'), assertuser: mwCfg.wgUserName },
'edit', function (resp) {
if (resp && resp.error && resp.error.code !== 'articleexists') { cb(resp.error); return; }
doPublish();
});
return;
}
// Вставка в существующую страницу
if (opts.createText !== undefined) {
getTextWithTimestamp(opts.pageTitle, function (pageText, baseTimestamp, readErr) {
var result;
var ep;
if (readErr) { cb(makeReadError(readErr, 'read_failed', opts.readErrorMessage || 'Не удалось получить содержимое.')); return; }
result = pageText === null
? (typeof opts.createText === 'function' ? opts.createText() : opts.createText)
: (opts.buildText ? opts.buildText(pageText) : null);
if (typeof result === 'string') result = { text: result };
if (!result || result.error) { cb((result && result.error) || { code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
if (result.skip) { cb(null); return; }
if (typeof result.text !== 'string') { cb({ code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
ep = {
title: opts.pageTitle,
text: result.text,
summary: result.summary || opts.summary,
assertuser: mwCfg.wgUserName
};
if (pageText === null) ep.createonly = true;
else if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) { cb(resp && resp.error ? resp.error : null); });
});
return;
}
editPageContent(opts.pageTitle, { summary: opts.summary, readError: opts.readErrorMessage || 'Не удалось получить содержимое.' },
function (pageText) { return opts.buildText ? opts.buildText(pageText) : null; },
function (err) { cb(err || null); }
);
}
// ─── Оповещение авторов ──────────────────────────────────────────────────
function notifyAuthor(pg, options, callback) {
var opts = options || {};
var cb = callback || function () {};
var actionText = (typeof opts.actionText === 'string') ? opts.actionText : '';
var discussionPage = (typeof opts.discussionPage === 'string') ? opts.discussionPage : '';
var discussionSection = normalizeSectionForLink((typeof opts.discussionSection === 'string') ? opts.discussionSection : '');
var includeProposed = (typeof opts.includeProposedPrefix === 'boolean') ? opts.includeProposedPrefix : true;
var actionPhrase = ((includeProposed ? 'предложена ' : '') + actionText).trim() || 'изменена';
var discussionText = discussionPage ? 'Обсуждение — на странице [[' + discussionPage + (discussionSection ? '#' + discussionSection : '') + ']]. ' : '';
apiReq({ prop: 'revisions', rvprop: 'user', rvdir: 'newer', titles: pg }, 'query', function (queryResp) {
var page = getFirstQueryPage(queryResp);
if (!page) { cb({ code: 'network', info: 'Network error' }); return; }
if (page.missing !== undefined) { cb({ code: 'missing', info: 'Page missing.' }); return; }
if (!page.revisions || !page.revisions.length) { cb({ code: 'no_revisions', info: 'No revisions.' }); return; }
var rv = page.revisions[0];
if ('anon' in rv || rv.userhidden || !rv.user || rv.user === mwCfg.wgUserName) { cb(null); return; }
apiReq({
title: 'User talk:' + rv.user, section: 'new',
sectiontitle: 'Remover: [[:' + pg + ']]',
summary: opts.summary || makeSummary('уведомление автора'),
text: 'Страница [[:' + pg + ']], созданная вами, ' + actionPhrase + '. ' +
discussionText +
'~~' + '~~<br><small>Это автоматическое уведомление, сгенерированное ' + scriptLink + '.</small>',
assertuser: mwCfg.wgUserName
}, 'edit', function (editResp) { cb(editResp && editResp.error ? editResp.error : null); });
});
}
function notifyAuthorsForPages(pages, notifyOptions, callback) {
var cb = callback || function () {};
var opts = notifyOptions || {};
var list = [];
(pages || []).forEach(function (p) { var t = normTitle(p); if (t && list.indexOf(t) === -1) list.push(t); });
if (!list.length) { cb(); return; }
eachSequential(list, function (pg, next) {
var pageLink = buildQuotedStatusPageLink(pg);
var statusId = logStatus('Отправляется уведомление создателю страницы ' + pageLink + '...', null, { pending: true, trackError: false });
notifyAuthor(pg, opts, function (err) {
logStatus(err ? 'Уведомление создателя страницы ' + pageLink + '.' : 'Создатель страницы ' + pageLink + ' уведомлён.', err,
{ statusId: statusId, trackError: opts.trackError !== false });
next();
});
}, cb);
}
// ─── Подписка на раздел ──────────────────────────────────────────────────
function subscribeToTopic(pageTitle, sectionTitle, callback) {
var cb = callback || function () {};
if (!setSubscribe || !sectionTitle) { cb(); return; }
var statusId = logStatus('Оформляется подписка на раздел...', null, { pending: true, trackError: false });
var targetFrag = normalizeSectionForLink(sectionTitle).toLowerCase();
function finish(err, st) {
if (err) { logStatus('Не удалось подписаться на раздел.', err, { statusId: statusId, trackError: false }); cb(); return; }
logStatus(st === 'subscribed' ? 'Оформлена подписка на раздел.' : 'Раздел для подписки не найден.', null, { statusId: statusId, trackError: false });
cb();
}
function trySubscribe(attemptsLeft) {
apiReq({ page: pageTitle, prop: 'threaditemshtml', excludesignatures: true }, 'discussiontoolspageinfo', function (data) {
var items = (data && data.discussiontoolspageinfo && data.discussiontoolspageinfo.threaditemshtml) || null;
if (!items || !items.length) {
if (attemptsLeft > 0) { setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000); return; }
finish(null, 'not_found'); return;
}
var commentname = null;
for (var ti = items.length - 1; ti >= 0; ti--) {
var t = items[ti];
if (t.type === 'heading') {
var htext = (t.headingText || t.html || '').replace(/<[^>]+>/g, '').trim();
if (htext.toLowerCase() === targetFrag || normalizeSectionForLink(htext).replace(/_/g, ' ').toLowerCase() === targetFrag) {
commentname = t.name; break;
}
}
}
if (!commentname) {
if (attemptsLeft > 0) setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000);
else finish(null, 'not_found');
return;
}
apiReq({ page: pageTitle, commentname: commentname, subscribe: '1' }, 'discussiontoolssubscribe', function (res) {
finish(res && res.error ? res.error : null, 'subscribed');
});
});
}
setTimeout(function () { trySubscribe(2); }, 1500);
}
// ═══════════════════════════════════════════════════════════════════════════
// UI: модальные окна
// ═══════════════════════════════════════════════════════════════════════════
function syncModalLayout() {
var syncFn = $('#removerModal').data('rmSyncLayout');
if (typeof syncFn === 'function') syncFn();
}
function clearModalLayoutSyncHandlers() {
modalLayoutSyncHandlers = [];
$('#removerModal').removeData('rmSyncLayout');
}
function registerModalLayoutSync(handler) {
if (typeof handler !== 'function') return;
if (modalLayoutSyncHandlers.indexOf(handler) === -1) modalLayoutSyncHandlers.push(handler);
$('#removerModal').data('rmSyncLayout', function () {
modalLayoutSyncHandlers.slice().forEach(function (fn) {
if (typeof fn === 'function') fn();
});
});
}
function registerResizeObserver(observer) {
if (observer) resizeObservers.push(observer);
}
function resetModalObservers() {
resizeObservers.forEach(function (observer) {
if (observer && typeof observer.disconnect === 'function') observer.disconnect();
});
resizeObservers = [];
clearModalLayoutSyncHandlers();
$(window).off('resize.removerModal');
$(window).off('.rmTaResize');
}
function closeModal() {
resetModalObservers();
$(window).off('keydown.remover');
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
}
function ensureModalStyles() {
if (document.getElementById('removerModalDynamicStyles')) return;
var progH = 'background:' + tk.bgProgH + '!important;border-color:' + tk.bProgH + '!important;color:' + tk.cInv + '!important;';
var neutH = 'background:' + tk.bgN + '!important;border-color:' + tk.bSub + '!important;color:inherit!important;';
var succH = 'background:' + tk.bgSuccH+ '!important;border-color:' + tk.bSuccH + '!important;color:' + tk.cInv + '!important;';
var pillBg = 'linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%)';
var pillShadow = '0 1px 0 rgba(255,255,255,.08) inset,0 1px 2px rgba(0,0,0,.18)';
var activePillShadow = '0 1px 0 rgba(255,255,255,.14) inset,0 2px 6px rgba(51,102,204,.24)';
var css = [
'#removerModal,#removerModal *{-moz-text-size-adjust:none!important;-webkit-text-size-adjust:100%!important;text-size-adjust:100%!important}',
'#removerModal{color:inherit}',
'#removerModal input::placeholder,#removerModal textarea::placeholder{color:var(--color-subtle,currentColor);opacity:.7}',
'#removerModal input[type="checkbox"],#removerModal input[type="radio"]{appearance:auto;-webkit-appearance:auto;-moz-appearance:auto;accent-color:auto}',
'#removerModal input[type="checkbox"]{outline:none!important;box-shadow:none!important}',
'#removerModal button{transition:background-color .12s ease,border-color .12s ease,color .12s ease,box-shadow .12s ease,filter .12s ease,transform .06s ease}',
'#removerModal .rmToggleBtn{background:' + tk.bgNSub + '!important;border-color:' + tk.bSub + '!important;color:inherit!important}',
'#removerModal .rmToggleBtn.is-active{background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important}',
'#removerModal button:not(:disabled):hover{filter:brightness(.97)}',
'#removerModal button:not(:disabled):active{transform:translateY(1px)}',
'#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):hover,#removerModal .rmToggleBtn.is-active:hover{' + progH + 'filter:none}',
'#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):active,#removerModal .rmToggleBtn.is-active:active{' + progH + 'filter:brightness(.92)!important}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError){background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important;outline:none!important;box-shadow:0 0 0 6px rgba(51,102,204,.13),0 1px 2px rgba(0,0,0,.08)!important}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):hover{' + progH + 'box-shadow:0 0 0 7px rgba(51,102,204,.16),0 1px 2px rgba(0,0,0,.1)!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):active{' + progH + 'box-shadow:0 0 0 5px rgba(51,102,204,.14),0 1px 2px rgba(0,0,0,.08)!important;filter:brightness(.92)!important}',
'#removerModal .rmArticleCommentToggle.is-active{background:#bfc4ca!important;border-color:#8f98a3!important;color:#202122!important}',
'#removerModal .rmArticleCommentToggle.is-active:hover{background:#b4bac1!important;border-color:#848e99!important;color:#202122!important;filter:none}',
'#removerModal .rmArticleCommentToggle.is-active:active{background:#a9b0b8!important;border-color:#79838f!important;color:#202122!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitError:not(:disabled):hover{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitError:not(:disabled):active{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:brightness(.88)!important}',
'#removerModal #removerReload:not(:disabled):hover{' + succH + 'filter:none}',
'#removerModal #removerReload:not(:disabled):active{' + succH + 'filter:brightness(.92)!important}',
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):hover,#removerModal .rmToggleBtn:not(.is-active):hover{' + neutH + 'filter:none}',
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):active{' + neutH + 'filter:brightness(.92)!important}',
'#removerModal a.removerModalLink{color:' + tk.cProg + ';text-decoration:none;border-bottom:0;box-shadow:none;word-break:break-word;overflow-wrap:anywhere}',
'#removerModal a.removerModalLink:hover,#removerModal a.removerModalLink:focus{color:' + tk.cProgH + ';text-decoration:underline}',
'#removerModal #removerModalSubtitle{font-size:12px!important;line-height:1.35!important;font-weight:400!important;color:' + tk.cSubM + '!important;max-width:100%;overflow-wrap:anywhere;word-break:break-word}',
'#removerModal #removerModalSubtitle a{font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important}',
'#removerModal a.rmButtonLikeLink{color:' + tk.cBase + '!important;text-decoration:none!important;transform:none!important;transition:background-color .12s ease,border-color .12s ease,color .12s ease,filter .12s ease,transform .06s ease!important}',
'#removerModal a.rmButtonLikeLink:hover{' + neutH + 'text-decoration:none!important;transform:none!important}',
'#removerModal a.rmButtonLikeLink:focus{text-decoration:none!important}',
'#removerModal a.rmButtonLikeLink:focus:not(:focus-visible){outline:none!important}',
'#removerModal a.rmButtonLikeLink:focus-visible{outline:2px solid ' + tk.bProg + '!important;outline-offset:2px;text-decoration:none!important}',
'#removerModal a.rmButtonLikeLink:hover:active{' + neutH + 'filter:brightness(.92)!important;transform:translateY(1px)!important;text-decoration:none!important}',
'#removerModal .rmInfoBox{margin:0 0 10px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgNSub + '}',
'#removerModal .rmActionList{display:flex;flex-direction:column;gap:6px}',
'#removerModal .rmActionItem{display:block;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgBase + ';cursor:pointer;transition:background-color .12s ease,transform .06s ease}',
'#removerModal .rmActionItem:hover{background:' + tk.bgNSub + '}',
'#removerModal .rmActionItem:active{transform:translateY(1px)}',
'#removerModal .rmActionMain{display:flex;align-items:center}',
'#removerModal .rmActionMain input[type="radio"]{margin-right:8px}',
'#removerModal .rmActionMeta{display:block;margin-left:24px;margin-top:2px;color:' + tk.cSubM + ';font-size:12px;line-height:1.35}',
'#removerModal .rmConflictLead{margin:0 0 10px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}',
'#removerModal .rmConflictList{display:flex;flex-direction:column;gap:10px}',
'#removerModal .rmConflictCard{padding:12px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}',
'#removerModal .rmConflictCard.is-skip{background:' + tk.bgNSub + ';border-color:' + tk.bSubS + '}',
'#removerModal .rmConflictTitle{font-size:14px;font-weight:700;line-height:1.4;color:' + tk.cBase + ';word-break:break-word;overflow-wrap:anywhere}',
'#removerModal .rmConflictMeta{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmConflictGroup{margin-top:10px}',
'#removerModal .rmConflictGroupTitle{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmConflictButtons{display:flex;flex-wrap:wrap;gap:6px}',
'#removerModal .rmConflictChoice{padding:5px 10px}',
'#removerModal .rmConflictChoice.is-disabled,#removerModal .rmConflictButtons.is-disabled .rmConflictChoice{opacity:.55;cursor:not-allowed;pointer-events:none}',
'#removerModal .rmConflictHint{margin-top:8px;font-size:11px;line-height:1.45;color:' + tk.cSub + '}',
'#removerModal #rmSettingsForm{display:flex;flex-direction:column;gap:14px}',
'#removerModal .rmSettingsLead{margin:-2px 0 2px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}',
'#removerModal .rmSettingsSection{margin:0;padding:14px 16px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.7) inset}',
'#removerModal .rmSettingsSectionHeader{margin:0 0 12px}',
'#removerModal .rmSettingsSectionTitle{font-size:14px;font-weight:700;line-height:1.35;color:' + tk.cBase + '}',
'#removerModal .rmSettingsSectionDescription{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmSettingsField{display:block;margin:0 0 10px;padding:12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}',
'#removerModal .rmSettingsField:last-child{margin-bottom:0}',
'#removerModal .rmSettingsFieldLabel{display:block;font-size:13px;font-weight:700;line-height:1.35;margin:0 0 8px;color:' + tk.cBase + '}',
'#removerModal .rmSettingsFieldControl{display:block;min-width:0}',
'#removerModal .rmSettingsFieldControl input{margin-bottom:0!important}',
'#removerModal .rmSettingsFieldControl input[type="text"]{min-height:38px;border-radius:6px}',
'#removerModal .rmSettingsFieldHint{margin-top:8px;font-size:12px;line-height:1.55;color:' + tk.cSubM + ';overflow-wrap:anywhere}',
'#removerModal .rmSettingsChecks{display:flex;flex-direction:column;gap:8px}',
'#removerModal .rmSettingsCheck{display:inline-flex;align-items:flex-start;gap:8px;font-size:14px;line-height:1.45;color:' + tk.cBase + '}',
'#removerModal .rmSettingsCheck input[type="checkbox"]{margin:3px 0 0;flex-shrink:0}',
'#removerModal .rmSettingsMenuPresetWrap{margin-top:10px}',
'#removerModal .rmSettingsMenuPresetLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmSegmentedBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmSettingsMenuPresetBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmSegmentedBtn,#removerModal .rmSettingsMenuPresetBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}',
'#removerModal .rmSegmentedBtn.is-active,#removerModal .rmSettingsMenuPresetBtn.is-active{background:' + tk.bgProg + ';border-color:' + tk.bProg + ';color:' + tk.cInv + ';box-shadow:' + activePillShadow + '}',
'#removerModal.rmModalSettings{border:1px solid ' + tk.bSub + '!important;background:' + tk.bgBase + '!important;border-radius:12px!important;box-shadow:0 14px 32px rgba(0,0,0,.08),0 1px 0 rgba(255,255,255,.78) inset!important}',
'#removerModal.rmModalSettings #removerModalHeaderBar{margin-bottom:14px;padding-bottom:10px;border-bottom:2px solid ' + tk.bSub + '}',
'#removerModal.rmModalSettings #removerModalSubtitle{margin:-2px 0 12px!important;color:' + tk.cSubM + '!important}',
'#removerModal.rmModalSettings #rmSettingsForm{gap:18px}',
'#removerModal.rmModalSettings .rmSettingsLead{margin:0;padding:0 2px;color:' + tk.cSubM + '}',
'#removerModal.rmModalSettings .rmSettingsSection{padding:16px 18px;border:1px solid ' + tk.bSub + ';border-radius:12px;background:linear-gradient(180deg,' + tk.bgNSub + ' 0%,' + tk.bgBase + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.82) inset,0 8px 18px rgba(0,0,0,.035)}',
'#removerModal.rmModalSettings .rmSettingsSectionHeader{margin:0 0 6px;padding-bottom:0;border-bottom:0}',
'#removerModal.rmModalSettings .rmSettingsSectionTitle{font-size:16px;line-height:1.3}',
'#removerModal.rmModalSettings .rmSettingsSectionDescription{margin-top:5px;max-width:none}',
'#removerModal.rmModalSettings .rmSettingsField{margin:14px 0 0;padding:12px 0 0;border:0;border-top:1px solid ' + tk.bSubS + ';border-radius:0;background:transparent;box-shadow:none}',
'#removerModal.rmModalSettings .rmSettingsField:first-child{margin-top:0;padding-top:0;border-top:0}',
'#removerModal.rmModalSettings .rmSettingsFieldLabel{margin:0 0 6px}',
'#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]{min-height:40px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}',
'#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]:focus{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.16);outline:none}',
'#removerModal.rmModalSettings .rmSettingsFieldHint{margin-top:6px;max-width:none}',
'#removerModal.rmModalSettings .rmSettingsChecks{gap:10px}',
'#removerModal.rmModalSettings .rmSettingsCheck{padding:4px 0}',
'#removerModal.rmModalSettings .rmSettingsMenuPresetWrap{margin-top:12px;padding-top:10px;border-top:1px dashed ' + tk.bSubS + '}',
'#removerModal.rmModalSettings .rmSettingsHintList{width:100%;max-width:100%;box-sizing:border-box;margin-top:10px;padding:10px 12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + '}',
'#removerModal.rmModalSettings .rmSettingsHintRow{line-height:1.55}',
'#removerModal.rmModalSettings .rmSettingsHintBadge{background:' + tk.bgNSub + '}',
'#removerModal.rmModalSettings .rmQuickPhraseEditor{padding-top:2px}',
'#removerModal.rmModalSettings .rmQuickPhraseMeta{min-height:18px}',
'#removerModal.rmModalSettings #rmSettingsSignaturePreviewCode{display:inline-block;padding:2px 8px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}',
'#removerModal.rmModalSettings #rmFooterActionButtons{position:relative;flex:0 0 auto!important;display:flex!important;align-items:center!important;justify-content:flex-end!important;gap:6px!important;margin-left:auto!important;max-width:100%!important}',
'#removerModal.rmModalSettings #rmSettingsActionButtonsRow{display:flex;align-items:center;justify-content:flex-end;gap:6px;flex-wrap:nowrap}',
'#removerModal.rmModalSettings #rmSettingsUnsavedHint{display:none;position:absolute;top:100%;right:0;width:auto;box-sizing:border-box;margin:4px 0 0;color:' + tk.cSubM + ';opacity:.78;font-size:12px;line-height:1.35;text-align:right;white-space:nowrap}',
'#removerModal .rmProtectControlGroup{margin-top:12px;padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-sizing:border-box}',
'#removerModal .rmProtectControlLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmProtectControlGroup .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}',
'#removerModal .rmProtectControlGroup .rmSegmentedBtn{padding:4px 10px;font-size:12px}',
'#removerModal .rmTransferPanel{margin-top:10px;padding:0;border:0;background:transparent;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmTransferGrid{display:grid;grid-template-columns:max-content max-content;column-gap:10px;row-gap:6px;align-items:start;justify-content:start}',
'#removerModal .rmTransferGrid .rmSegmentedBar{justify-self:start}',
'#removerModal .rmTransferHintRow{grid-column:1 / -1;min-height:0}',
'#removerModal .rmTransferPanel .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}',
'#removerModal .rmTransferPanel .rmSegmentedBtn{padding:6px 14px;font-size:12px;font-weight:700}',
'#removerModal #rmTransferModeGroup{gap:0}',
'#removerModal #rmTransferModeGroup .rmSegmentedBtn:first-child{border-top-right-radius:0;border-bottom-right-radius:0}',
'#removerModal #rmTransferModeGroup .rmSegmentedBtn + .rmSegmentedBtn{margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}',
'#removerModal.rmCompactContent .rmArticleRow{flex-wrap:wrap!important;gap:6px}',
'#removerModal.rmCompactContent .rmArticleRow .rmArticleInput{flex:1 1 100%!important;width:100%!important}',
'#removerModal.rmCompactContent .rmArticleRow .rmArticleCommentToggle,#removerModal.rmCompactContent .rmArticleRow .rmAddArticle,#removerModal.rmCompactContent .rmArticleRow .rmRemoveInput{margin-left:0!important}',
'#removerModal.rmCompactContent .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(1){order:1}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(2){order:2}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(3){order:3}',
'#removerModal.rmCompactContent #rmTransferModeGroup{flex-direction:column;align-items:flex-start;gap:6px}',
'#removerModal.rmCompactContent #rmTransferModeGroup .rmSegmentedBtn{margin-left:0!important;border-radius:999px!important}',
'#removerModal.rmCompactContent .rmTransferHintRow{grid-column:auto}',
'#removerModal #rmProtectTextBlock{margin-top:14px}',
'#removerModal #rmSettingsMenuTitle:disabled{background:' + tk.bgDis + '!important;border-color:' + tk.bDis + '!important;color:' + tk.cDis + '!important;-webkit-text-fill-color:' + tk.cDis + ';opacity:1;cursor:not-allowed;box-shadow:none!important}',
'#removerModal .rmSettingsHintList{display:flex;flex-direction:column;gap:4px;margin-top:8px}',
'#removerModal .rmSettingsHintRow{font-size:12px;line-height:1.5;color:' + tk.cSubM + '}',
'#removerModal .rmSettingsHintBadge{display:inline-block;margin-right:6px;padding:1px 6px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';font-size:11px;font-weight:700;color:' + tk.cSubM + ';vertical-align:baseline}',
'#removerModal .rmQuickPhraseEditor{display:flex;flex-direction:column;gap:10px}',
'#removerModal .rmQuickPhraseList,#removerModal .rmQuickPhrasesPanel{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-start}',
'#removerModal .rmQuickPhraseChip{position:relative;display:inline-flex;align-items:center;gap:4px;max-width:100%;padding:2px 4px 2px 10px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';box-shadow:' + pillShadow + ';transition:border-color .12s,box-shadow .12s,opacity .12s;overflow:visible}',
'#removerModal .rmQuickPhraseChip.is-editing{opacity:.42;border-style:dashed}',
'#removerModal .rmQuickPhraseChip.is-dragging{opacity:.65}',
'#removerModal .rmQuickPhraseChip.is-drop-before::before,#removerModal .rmQuickPhraseChip.is-drop-after::after{content:"";position:absolute;top:50%;width:3px;height:24px;border-radius:999px;background:' + tk.cProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.08)}',
'#removerModal .rmQuickPhraseChip.is-drop-before::before{left:-4px;transform:translate(-50%,-50%)}',
'#removerModal .rmQuickPhraseChip.is-drop-after::after{right:-4px;transform:translate(50%,-50%)}',
'#removerModal .rmQuickPhraseEditBtn{max-width:100%;padding:3px 0;border:0;background:transparent;color:' + tk.cBase + ';font-size:13px;line-height:1.35;cursor:pointer;text-align:left;white-space:normal}',
'#removerModal .rmQuickPhraseRemoveBtn{width:24px;height:24px;padding:0;border:0;border-radius:999px;background:transparent;color:' + tk.cSubM + ';font-size:18px;line-height:1;cursor:pointer;flex-shrink:0}',
'#removerModal .rmQuickPhraseRemoveBtn:hover{background:' + tk.bgN + ';color:' + tk.cBase + '}',
'#removerModal #rmSettingsQuickPhraseInput.is-editing{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.12)}',
'#removerModal .rmQuickPhraseMeta{font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmQuickPhraseEmpty{padding:2px 0;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmQuickPhrasesPanel{margin-top:8px}',
'#removerModal .rmQuickPhraseActionBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}',
'#removerModal .rmQuickPhraseActionBtn:hover{border-color:' + tk.bProg + ';color:' + tk.cProg + '}',
'@media (max-width:' + sz.mobileBp + 'px){',
'#removerModal button{white-space:normal!important}',
'#removerModal #rmFooterButtons{align-items:flex-start!important}',
'#removerModal #rmFooterCheckboxes,#removerModal #rmFooterActionButtons{width:100%!important;max-width:100%!important;margin-left:0!important}',
'#removerModal .rmSettingsSection{padding:12px 13px}',
'#removerModal .rmSettingsField{padding:10px}',
'#removerModal.rmModalSettings #rmSettingsUnsavedHint{max-width:100%;margin:4px 0 0;text-align:right;white-space:normal}',
'#removerModal .rmTransferPanel{padding:0}',
'#removerModal .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}',
'#removerModal .rmTransferHintRow{grid-column:auto}',
'#removerModal .rmQuickPhraseChip{max-width:100%}',
'}'
].join('');
var style = document.createElement('style');
style.id = 'removerModalDynamicStyles';
style.textContent = css;
document.head.appendChild(style);
}
function applyV2022Layout($modal, explicitWidth) {
if (!isVector22) return;
var css = { 'max-width': '100%', 'box-sizing': 'border-box', 'overflow-wrap': 'anywhere' };
if (typeof explicitWidth === 'number') css['max-width'] = explicitWidth + 'px';
$modal.css(css);
}
function getPageUrl(pageTitle) {
return (mw.util && typeof mw.util.getUrl === 'function')
? mw.util.getUrl(pageTitle)
: '/wiki/' + encodeURIComponent((pageTitle || '').replace(/ /g, '_'));
}
function getPageUrlWithFragment(pageTitle, fragment) {
var url = getPageUrl(pageTitle);
var frag = normalizeSectionForLink(fragment);
if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_'));
return url;
}
function buildStatusPageLink(pageName) {
var title = normTitle(pageName);
return '<a href="' + escapeHtml(getPageUrl(title)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(title) + '</a>';
}
function buildQuotedStatusPageLink(pageName) {
return '«' + buildStatusPageLink(pageName) + '»';
}
function buildHeaderIconButtonHtml(id, title, label, text) {
return joinHtml([
'<button id="', id, '" type="button" title="', escapeHtml(title), '" aria-label="', escapeHtml(label || title), '" ',
'style="', stHeaderIconBtn, '">', text || '', '</button>'
]);
}
function createModal(opts) {
if (typeof opts === 'string') opts = { title: opts };
var layout = getModalLayout();
resetModalObservers();
ensureModalStyles();
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
var subtitleHtml = '';
var subtitleStyle = 'margin:-4px 0 8px;font-size:12px!important;color:' + tk.cSubM + ';line-height:1.35!important;font-weight:400!important;';
var subtitleLinkStyle = 'font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important;';
if (opts.subtitleHtml) {
subtitleHtml = joinHtml([
'<div id="removerModalSubtitle" style="', subtitleStyle, '">',
opts.subtitleHtml,
'</div>'
]);
} else if (opts.subtitlePage) {
subtitleHtml = joinHtml([
'<div id="removerModalSubtitle" style="', subtitleStyle, '">',
opts.subtitleLabel || 'Текущий день',
': <a href="', getPageUrl(opts.subtitlePage), '" target="_blank" rel="noopener noreferrer" class="removerModalLink" style="', subtitleLinkStyle, '">',
normTitle(opts.subtitlePage),
'</a></div>'
]);
}
var settingsButtonHtml = opts.showSettingsButton === false ? '' :
buildHeaderIconButtonHtml('removerSettingsTrigger', 'Конфигурация', 'Конфигурация', '⚙');
var display = opts.inline ? 'inline-block' : 'block';
var modalMargin = opts.inline ? '1em 0' : (layout.shouldCenter ? '1em auto' : '1em 0');
var inlineLayoutStyle = opts.inline ? ';justify-self:start;align-self:start;width:fit-content;' : '';
var modalStyle = joinHtml([
'position:relative;padding:1.5em;margin:', modalMargin, ';display:', display, ';',
'border:', stStyles.border, ';background:', stStyles.background,
';border-radius:', stStyles.borderRadius, ';box-shadow:', stStyles.boxShadow,
';max-width:100%;box-sizing:border-box;overflow-wrap:anywhere;', inlineLayoutStyle
]);
var headerStyle = 'display:flex;align-items:center;gap:10px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid ' + tk.bSubS + ';';
var titleStyle = 'color:' + stStyles.headerColor + ';margin:0;padding:0;border:0;display:block;font-size:1.3em;font-weight:400;line-height:1.25;flex:1 1 auto;min-width:0;';
$('#content').prepend(joinHtml([
'<div id="removerModal" style="', modalStyle, '">',
'<div id="removerModalHeaderBar" style="', headerStyle, '">',
'<h1 id="removerModalTitle" style="', titleStyle, '"><span id="removerModalTitleText">', opts.title, '</span></h1>',
settingsButtonHtml,
'</div>',
subtitleHtml,
'<div id="removerModalContent"></div>',
'<div id="removerModalFooter" style="margin-top:15px;"></div>',
'</div>'
]));
var $modal = $('#removerModal');
if (opts.width === 'compact') $modal.css({ width: layout.defaultOuterWidth + 'px', 'max-width': '100%', 'box-sizing': 'border-box' });
else applyV2022Layout($modal);
$('#removerSettingsTrigger').off('click').on('click', function () {
openSettings();
});
}
function buildFooterCheckboxHtml(name, checked, label) {
return joinHtml([
'<label style="', stFooterCheckLabel, '">',
'<input name="', name, '" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ',
checked ? 'checked' : '',
'>',
label,
'</label>'
]);
}
function buildFooterActionsHtml(buttonsHtml) {
return '<div id="rmFooterActionButtons" style="' + stFooterActions + '">' + buttonsHtml + '</div>';
}
function renderModalFooter(mode, options) {
var opts = options || {};
$('#removerModalFooter').css('width', '');
if (mode === 'submit') {
var showCb = opts.showCheckbox !== false;
var showSub = opts.showSubscribe === true;
var ns = mwCfg.wgNamespaceNumber;
var notifyLabel = ns === 0 ? 'Оповестить создателя статьи'
: (ns === 10 || ns === 11) ? 'Оповестить создателя шаблона'
: (ns === 14 || ns === 15) ? 'Оповестить создателя категории'
: 'Оповестить создателя страницы';
var cbInlineHtml = '';
if (showSub || showCb) {
cbInlineHtml = joinHtml([
'<div id="rmFooterCheckboxes" style="', stFooterChecks, '">',
showSub ? buildFooterCheckboxHtml('rmSubscribe', setSubscribe, 'Подписаться на номинацию') : '',
showCb ? buildFooterCheckboxHtml('rmUAlert', setAlert, notifyLabel) : '',
'</div>'
]);
}
$('#removerModalFooter').html(joinHtml([
'<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:', cbInlineHtml ? 'space-between' : 'flex-end', ';">',
cbInlineHtml,
buildFooterActionsHtml(joinHtml([
'<button id="removerCancel" style="', stCancel, '">Отмена</button>',
'<button id="removerSubmit" style="', stSubmit, '">', opts.submitText || 'ОК', '</button>'
])),
'</div>'
]));
$('#removerCancel').click(function () { closeModal(); });
$('#removerSubmit').data('rmSubmitInProgress', false).click(function () {
if ($(this).data('rmSubmitInProgress')) return;
$(this).removeClass('rmSubmitError').css({ background: '', 'border-color': '', color: '' });
isError = false;
if (!opts.preserveLogOnSubmit) {
$('#rmLogBox').empty();
logStatusSeq = 0;
}
if (showCb) { setAlert = $('[name="rmUAlert"]').is(':checked'); state.setAlert = setAlert; }
if (showSub) { setSubscribe = $('[name="rmSubscribe"]').is(':checked'); state.setSubscribe = setSubscribe; }
$(this).data('rmSubmitInProgress', true).prop('disabled', true);
var submitResult;
try { submitResult = opts.onSubmit(); } catch (ex) { unlockModalSubmit(); throw ex; }
if (submitResult === false) unlockModalSubmit();
});
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.ctrlKey && e.keyCode === 13) $('#removerSubmit').click();
});
} else if (mode === 'reload') {
var newBtns = buildFooterActionsHtml(joinHtml([
'<button id="removerCancel" style="', stCancel, '">Закрыть</button>',
'<button id="removerReload" style="', stReload, '">', opts.reloadText || 'Обновить страницу', '</button>'
]));
$('#rmFooterCheckboxes').remove();
var $btns = $('#rmFooterButtons');
if ($btns.length) {
$btns.css({ 'justify-content': 'flex-end' }).html(newBtns);
} else {
$('#removerModalFooter').append(joinHtml([
'<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:flex-end;">',
newBtns,
'</div>'
]));
}
$('#removerCancel').click(function () { closeModal(); });
$('#removerReload').click(function () { location.reload(); });
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.keyCode === 27) $('#removerCancel').click();
if (e.ctrlKey && e.keyCode === 13) $('#removerReload').click();
});
} else { // 'close'
$('#removerModalFooter').html(joinHtml([
'<div style="display:flex;justify-content:flex-end;align-items:center;">',
'<button id="removerCancel" style="', stCancel, 'margin-right:0;">', opts.closeText || 'Закрыть', '</button>',
'</div>'
]));
$('#removerCancel').click(function () { closeModal(); });
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.keyCode === 27) $('#removerCancel').click();
});
}
}
function unlockModalSubmit() {
$('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false);
}
function markSubmitError() {
isError = true;
var errColor = '#d73333';
$('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false)
.addClass('rmSubmitError').css({ background: errColor, 'border-color': errColor, color: '#fff' });
}
// ─── UI: статус и ссылки ─────────────────────────────────────────────────
function startProcessing() {
if ($('#rmLogBox').length) return;
$('#removerModal').append(
'<div id="rmLogBox" style="margin-top:12px;padding-top:10px;border-top:1px solid ' + tk.bSubS + ';line-height:1.5;overflow-wrap:anywhere;word-break:break-word;box-sizing:border-box;"></div>'
);
syncLinkWidths();
}
function logStatus(message, error, opts) {
var o = opts || {};
if (o.trackError !== false && error && error.code) isError = true;
var $box = $('#rmLogBox');
if (!$box.length) { startProcessing(); $box = $('#rmLogBox'); }
var statusId = o.statusId || ('rm-status-' + (++logStatusSeq));
var $row = $box.find('[data-rm-status-id="' + statusId + '"]');
if (!$row.length) {
$row = $('<div data-rm-status-id="' + statusId + '" style="margin-top:4px;line-height:1.4;"></div>');
$box.append($row);
}
var html;
if (error) {
var errText = error.code
? '<span class="error"><small>' + escapeHtml(formatLogErrorCode(error.code)) + ': ' + escapeHtml(String(error.info || '')) + '</small></span>'
: escapeHtml(String(error));
html = '<span style="color:' + tk.cDang + ';margin-right:4px;">✕</span>' + message + ' — ' + errText;
} else if (o.pending) {
html = '<span style="color:' + tk.cSubM + ';">' + message + '</span>';
} else {
html = '<span style="color:' + tk.bgSucc + ';margin-right:4px;">✓</span><span style="color:' + tk.cSubM + ';">' + message + '</span>';
}
$row.html(html);
return statusId;
}
function formatLogErrorCode(code) {
var value = String(code || '');
return value.toLowerCase() === 'error' ? 'Ошибка' : value;
}
function logPageEdit(pageName, error, opts) {
logStatus('Правка страницы ' + buildQuotedStatusPageLink(pageName) + '.', error, opts);
}
function syncLinkWidths() {
var $box = $('#rmLogBox');
if (!$box.length) return;
var taW = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0;
$box.css({ width: taW ? taW + 'px' : '', 'max-width': '100%' });
}
function appendNominationLink(pageTitle, sectionTitle) {
if (!pageTitle) return;
var url = getPageUrl(pageTitle);
var frag = normalizeSectionForLink(sectionTitle);
if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_'));
var label = normTitle(frag ? pageTitle + '#' + frag : pageTitle);
var $target = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent');
$target.append(
'<div style="margin-top:4px;line-height:1.4;word-break:break-word;overflow-wrap:anywhere;display:flex;align-items:baseline;gap:5px;">' +
'<span style="color:' + tk.bgSucc + ';font-size:14px;flex-shrink:0;">✓</span>' +
'<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a></div>'
);
}
// ─── UI-строители ────────────────────────────────────────────────────────
function buildInfoBoxHtml(mainText, detailsText, isErr) {
var cls = isErr ? ' class="error"' : '';
return joinHtml([
'<div class="rmInfoBox">',
'<p', cls, ' style="margin:0', detailsText ? ' 0 6px' : '', ';">', mainText, '</p>',
detailsText ? '<p style="margin:0;color:' + tk.cSubM + ';">' + detailsText + '</p>' : '',
'</div>'
]);
}
function buildActionsHtml(actions, inputName, listId) {
var actionItemsHtml = actions.map(function (a, i) {
var meta = a.description || (a.talkNotice ? 'С добавлением {{' + (a.talkTemplate || a.resultTemplate || 'шаблон') + '}} на СО.' : '');
var tagHtml = a.tag
? '<span style="display:inline-block;font-size:13px;font-weight:600;padding:2px 7px;border-radius:3px;background:' + tk.bgN + ';color:' + tk.cSubM + ';margin-right:8px;white-space:nowrap;vertical-align:middle;">' + escapeHtml(a.tag) + '</span>'
: '';
return joinHtml([
'<label class="rmActionItem">',
'<span class="rmActionMain">',
'<input type="radio" name="', inputName, '" value="', a.id, '" ', i === 0 ? 'checked' : '', '>',
tagHtml,
'<span>', a.label, '</span>',
'</span>',
meta ? '<span class="rmActionMeta">' + meta + '</span>' : '',
'</label>'
]);
}).join('');
return joinHtml([
'<div style="margin:0 0 8px;color:', tk.cSubM, ';font-size:13px;">Обнаружены открытые номинации:</div>',
'<div', listId ? ' id="' + listId + '"' : '', ' class="rmActionList">',
actionItemsHtml,
'</div>'
]);
}
function buildNestedCommentFieldsHtml(opts) {
var options = opts || {};
var wrapId = options.wrapId || '';
var textareaId = options.textareaId || '';
var textareaClass = options.textareaClass ? ' ' + options.textareaClass : '';
var textareaStyleExtra = options.textareaStyleExtra || '';
var wrapStyleExtra = options.wrapStyleExtra || '';
var placeholder = options.placeholder || 'Комментарий (необязательно)';
var beforeHtml = options.beforeHtml || '';
var marginTop = options.marginTop || '6px';
var minHeight = parseInt(options.minHeight, 10) || 90;
var isEmbedded = !!options.embedded;
var wrapClass = isEmbedded ? '' : (' class="' + RESIZE_CLASS + '"');
var wrapStyle = 'display:none;margin-top:' + marginTop + ';max-width:100%;box-sizing:border-box;';
if (isEmbedded) {
wrapStyle += 'padding:0;border:0;background:transparent;';
} else {
wrapStyle += 'padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:6px;background:' + tk.bgNSub + ';';
}
wrapStyle += wrapStyleExtra;
return joinHtml([
'<div id="', wrapId, '"', wrapClass, ' style="', wrapStyle, '">',
beforeHtml,
'<textarea id="', textareaId, '" class="rmNestedCommentInput', textareaClass,
'" placeholder="', escapeHtml(placeholder), '" style="', stInputFull,
'min-height:', minHeight, 'px;resize:both;margin-bottom:6px;', textareaStyleExtra, '"></textarea>',
buildQuickPhrasesPanelHtml(textareaId),
'</div>'
]);
}
function buildConditionalRetFieldsHtml() {
return buildNestedCommentFieldsHtml({
wrapId: 'rmCloseConditionalWrap',
textareaId: 'rmCloseConditionalReason',
placeholder: 'Условие / пояснение (необязательно)',
marginTop: '8px',
minHeight: 90,
beforeHtml: '<input id="rmCloseConditionalDeadline" type="text" placeholder="Срок доработки: 2026-05-31" style="' + stInputFull + 'margin-bottom:6px;">'
});
}
function buildAddArticleButtonHtml(options) {
var opts = options || {};
var title = opts.addTitle || 'Добавить статью';
return '<button type="button" class="rmAddArticle" title="' + escapeHtml(title) + '" aria-label="' + escapeHtml(title) + '" style="' + stRemoveBtn + '">+</button>';
}
function buildSquareAddButtonHtml(id, title, className) {
var idAttr = id ? ' id="' + id + '"' : '';
var clsAttr = className ? ' class="' + className + '"' : '';
var label = title || 'Добавить';
return '<button' + idAttr + ' type="button"' + clsAttr + ' title="' + escapeHtml(label) + '" aria-label="' + escapeHtml(label) + '" style="' + stRemoveBtn + '">+</button>';
}
function buildMultiArticleButtonsHtml(commentWrapId, commentId, options) {
var opts = options || {};
var commentBtnStyle = stToolBtn + (opts.showComment ? '' : 'display:none;');
if (opts.showAdd) return buildAddArticleButtonHtml(opts);
return joinHtml([
'<button type="button" class="rmToggleBtn rmArticleCommentToggle" data-rm-comment-wrap="', commentWrapId,
'" data-rm-comment-textarea="', commentId, '" aria-expanded="false" style="', commentBtnStyle, '">Комментарий</button>',
'<button type="button" class="rmRemoveInput" style="', stRemoveBtn, '" title="', escapeHtml(opts.removeTitle || 'Удалить статью'), '">−</button>'
]);
}
function buildMultiArticleRowHtml(index, options) {
var opts = options || {};
var articleId = 'rmArticle' + index;
var commentWrapId = 'rmArticleCommentWrap' + index;
var commentId = 'rmArticleComment' + index;
var articleValue = opts.articleValue ? ' value="' + escapeHtml(opts.articleValue) + '"' : '';
var inputPlaceholder = opts.inputPlaceholder || 'Статья';
var commentPlaceholder = opts.commentPlaceholder || 'Комментарий только для этой статьи (необязательно)';
var articleRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
var blockStyle = 'max-width:100%;box-sizing:border-box;';
var buttonsHtml = buildMultiArticleButtonsHtml(commentWrapId, commentId, {
showAdd: !!opts.showAdd,
showComment: !!opts.showComment,
addTitle: opts.addTitle,
removeTitle: opts.removeTitle
});
return joinHtml([
'<div class="rmMultiArticleBlock ', RESIZE_CLASS, '" style="', blockStyle, '">',
'<div', opts.rowId ? ' id="' + opts.rowId + '"' : '', ' class="rmArticleRow" style="', articleRowStyle, '">',
'<input id="', articleId, '" type="text" placeholder="', escapeHtml(inputPlaceholder), '" class="rmArticleInput" style="', stInputBox, '"', articleValue, '>',
buttonsHtml,
'</div>',
buildNestedCommentFieldsHtml({
wrapId: commentWrapId,
textareaId: commentId,
textareaClass: 'rmArticleCommentInput',
placeholder: commentPlaceholder,
marginTop: '4px',
minHeight: 90,
embedded: true,
wrapStyleExtra: 'padding:0 0 0 12px;background:transparent;',
textareaStyleExtra: 'border-color:' + tk.bSubS + ';border-radius:4px;background:' + tk.bgBase + ';'
}),
'</div>'
]);
}
function showInfoAndClose(mainText, detailsText, isErr) {
$('#removerModalContent').html(buildInfoBoxHtml(mainText, detailsText || '', isErr || false));
renderModalFooter('close');
}
function getSelectedAction(inputName, actionMap) {
var id = $('[name="' + inputName + '"]:checked').val();
var sel = actionMap[id];
if (!sel) alert('Выберите действие.');
return sel || null;
}
function prependTemplateToNoinclude(text, templateText) {
var source = String(text || '');
var tpl = String(templateText || '').trim();
if (!tpl) return source;
var match = source.match(RE_NOINCLUDE);
if (match) {
var before = source.slice(0, source.indexOf(match[0]));
if (/\S/.test(before)) return '<noinclude>' + tpl + '</noinclude>\n' + source;
var content = String(match[2] || '').replace(/^\n+/, '');
return source.replace(match[0], match[1] + '<noinclude>' + tpl + (content ? '\n' + content : '') + '\n</noinclude>');
}
return '<noinclude>' + tpl + '</noinclude>\n' + source;
}
function buildGeneratedNominationTemplateText(job, pg) {
var tplStr = '';
if (!job) return '';
if (job.opId === 'fRm') {
tplStr = job.kbuTemplate || '';
if (job.kbuAddInfo) tplStr += '|1=' + job.kbuAddInfo;
if (job.kbuComment) tplStr += '|' + (job.kbuAddInfo ? '2' : '1') + '=' + job.kbuComment;
return tplStr ? (T_OPEN + tplStr + T_CLOSE) : '';
}
if (typeof job.articleTpl !== 'function') return '';
tplStr = job.articleTpl(job.tplpar, job.date[0]);
if (job.opId === 'merge' && job.tplpar) {
tplStr = (job.op.nomination.articleTpl)(
('|' + job.tplpar + '|').replace('|' + pg + '|', '|').slice(1, -1),
job.date[0]
);
}
return tplStr ? (T_OPEN + tplStr + T_CLOSE) : '';
}
function applyConflictTemplateResolution(articleText, job, pg, decision) {
var rule = getNominationConflictRule(job);
var generatedTemplate = buildGeneratedNominationTemplateText(job, pg);
var source = String(articleText || '');
if (!generatedTemplate || !decision) return source;
if (decision.templateAction === 'overwrite') {
var cleaned = rule ? stripTemplatesByPattern(source, rule.namePattern).text : source;
return prependTemplateToNoinclude(cleaned, generatedTemplate);
}
if (decision.templateAction === 'prepend') return prependTemplateToNoinclude(source, generatedTemplate);
return source;
}
function inspectMultiNominationConflicts(job, callback) {
var cb = callback || function () {};
var pages = (job && job.multiArticles) ? job.multiArticles.slice() : [];
var conflicts = [];
var statusId = logStatus('Проверяются статьи на наличие уже установленных шаблонов...', null, { pending: true, trackError: false });
if (!pages.length) {
logStatus('Проверка завершена: конфликтов не найдено.', null, { statusId: statusId, trackError: false });
cb(null, conflicts);
return;
}
eachSequential(pages, function (pg, next) {
getText(pg, function (articleText, readErr) {
var conflict;
if (readErr) { next(makeReadError(readErr, 'read_failed', 'Не удалось проверить страницу «' + pg + '».')); return; }
if (articleText === null) { next({ code: 'read_failed', info: 'Страница «' + pg + '» не существует.' }); return; }
conflict = detectNominationConflict(articleText, job);
if (conflict) {
conflicts.push($.extend({ pageName: pg }, conflict));
logStatus('В статье ' + buildQuotedStatusPageLink(pg) + ' обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.', null, { trackError: false });
}
next();
});
}, function (err) {
if (err) {
logStatus('Проверка статей.', err, { statusId: statusId });
cb(err);
return;
}
logStatus(
conflicts.length
? 'Проверка завершена: найдены статьи с уже установленными шаблонами.'
: 'Проверка завершена: конфликтов не найдено.',
null,
{ statusId: statusId, trackError: false }
);
cb(null, conflicts);
});
}
function buildNominationConflictResolutionHtml(conflicts) {
return '<div class="rmInfoBox"><p style="margin:0 0 6px;">Найдены статьи, где шаблон уже стоит. Для каждой конфликтующей страницы выберите, что делать со статьёй и с шаблоном.</p>' +
'<p style="margin:0;color:' + tk.cSubM + ';font-size:12px;line-height:1.45;">По умолчанию такие статьи исключаются из новой номинации, а существующий шаблон остаётся без изменений.</p></div>' +
'<div class="rmConflictLead">После выбора нажмите «Продолжить номинирование».</div>' +
'<div id="rmConflictList" class="rmConflictList">' +
conflicts.map(function (conflict, index) {
var pageLink = buildStatusPageLink(conflict.pageName);
return '<div class="rmConflictCard" data-rm-conflict-index="' + index + '">' +
'<input type="hidden" class="rmConflictPageAction" value="skip">' +
'<input type="hidden" class="rmConflictTemplateAction" value="keep">' +
'<div class="rmConflictTitle">' + pageLink + '</div>' +
'<div class="rmConflictMeta">Обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.</div>' +
'<div class="rmConflictGroup">' +
'<div class="rmConflictGroupTitle">Действие со статьёй</div>' +
'<div class="rmConflictButtons" data-rm-conflict-group="page">' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="page" data-rm-choice="skip" aria-pressed="true">Убрать из номинации</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="page" data-rm-choice="keep" aria-pressed="false">Оставить в номинации</button>' +
'</div></div>' +
'<div class="rmConflictGroup">' +
'<div class="rmConflictGroupTitle">Действие с шаблоном</div>' +
'<div class="rmConflictButtons" data-rm-conflict-group="template">' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="template" data-rm-choice="keep" aria-pressed="true">Оставить как есть</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="overwrite" aria-pressed="false">Новая дата</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="prepend" aria-pressed="false">Второй сверху</button>' +
'</div>' +
'<div class="rmConflictHint">Если статья исключается из номинации, действие с шаблоном не применяется.</div>' +
'</div></div>';
}).join('') +
'</div>';
}
function updateNominationConflictCardState($card) {
var pageAction = $card.find('.rmConflictPageAction').val() || 'skip';
var disableTemplate = pageAction !== 'keep';
var $templateButtons = $card.find('[data-rm-choice-type="template"]');
$card.toggleClass('is-skip', disableTemplate);
$card.find('[data-rm-conflict-group="template"]').toggleClass('is-disabled', disableTemplate);
$templateButtons.prop('disabled', disableTemplate).toggleClass('is-disabled', disableTemplate);
}
function bindNominationConflictResolutionUi() {
var $content = $('#removerModalContent');
function setChoice($card, type, value) {
var inputClass = type === 'page' ? '.rmConflictPageAction' : '.rmConflictTemplateAction';
$card.find(inputClass).val(value);
$card.find('[data-rm-choice-type="' + type + '"]').each(function () {
var isActive = $(this).data('rmChoice') === value;
$(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false');
});
updateNominationConflictCardState($card);
}
$content.off('.rmConflictResolution').on('click.rmConflictResolution', '[data-rm-choice-type]', function () {
var $btn = $(this);
var $card = $btn.closest('.rmConflictCard');
if ($btn.prop('disabled')) return;
setChoice($card, $btn.data('rmChoiceType'), $btn.data('rmChoice'));
});
$('#rmConflictList .rmConflictCard').each(function () { updateNominationConflictCardState($(this)); });
}
function collectNominationConflictResolution(conflicts) {
var decisions = {};
(conflicts || []).forEach(function (conflict, index) {
var $card = $('#rmConflictList .rmConflictCard[data-rm-conflict-index="' + index + '"]');
decisions[normTitle(conflict.pageName)] = {
pageAction: $card.find('.rmConflictPageAction').val() || 'skip',
templateAction: $card.find('.rmConflictTemplateAction').val() || 'keep'
};
});
return decisions;
}
function applyNominationConflictResolutionToJob(job, decisions) {
var resultArticles;
var headerText;
if (!job || !job.isMulti) {
job.conflictDecisions = decisions || {};
return { value: job };
}
resultArticles = (job.multiArticles || []).filter(function (pageName) {
var decision = decisions && decisions[normTitle(pageName)];
return !decision || decision.pageAction !== 'skip';
});
if (!resultArticles.length) return { error: 'После исключения конфликтующих статей в номинации не осталось ни одной страницы.' };
job.conflictDecisions = decisions || {};
job.multiArticles = resultArticles.slice();
job.pages = resultArticles.slice().reverse();
headerText = String(job.multiHeaderText || '').trim();
job.section = headerText || ('[[:' + resultArticles[0] + ']]');
job.sectionNW = job.section.replace(/\[\[:/g, '').replace(/]]/g, '');
job.msg = job.multiNominationFormat === 'list'
? buildMultiNominationListText(resultArticles, job.multiNominationBody, job.multiArticleComments)
: buildMultiNominationText(resultArticles, job.multiNominationBody, job.multiArticleComments);
job.summary = makeSummary('номинация [[' + job.nomPage + '#' + job.sectionNW + ']]');
return { value: job };
}
function showNominationConflictResolution(job, conflicts, onContinue) {
resetModalObservers();
$('#removerModalContent').html(buildNominationConflictResolutionHtml(conflicts));
bindNominationConflictResolutionUi();
syncLinkWidths();
renderModalFooter('submit', {
submitText: 'Продолжить номинирование',
showSubscribe: true,
preserveLogOnSubmit: true,
onSubmit: function () {
var decisions = collectNominationConflictResolution(conflicts);
var applied = applyNominationConflictResolutionToJob(job, decisions);
if (applied.error) {
alert(applied.error);
return false;
}
if (typeof onContinue === 'function') onContinue(applied.value);
return true;
}
});
}
function bindTouchTextareaGrip($ta, sync, getMaxWidth, options) {
var opts = options || {};
var minWidth = parseInt(opts.minWidth, 10) || parseInt(sz.taMinW, 10) || 180;
var minHeight = parseInt(opts.minHeight, 10) || parseInt(sz.taMinH, 10) || 100;
var allowWidthResize = opts.allowWidth !== false;
var syncFn = typeof sync === 'function' ? sync : function () {};
var getMaxWidthFn = typeof getMaxWidth === 'function' ? getMaxWidth : function () { return $ta.outerWidth() || minWidth; };
var usePointerEvents = typeof window.PointerEvent === 'function';
var dragState = { active: false, startX: 0, startY: 0, startWidth: 0, startHeight: 0 };
var gripStyle = 'height:20px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-top:0;border-radius:0 0 4px 4px;background:' + tk.bgNSub + ';display:flex;align-items:center;justify-content:center;cursor:ns-resize;touch-action:none;user-select:none;-webkit-user-select:none;';
if (opts.gripMarginBottom) gripStyle += 'margin-bottom:' + opts.gripMarginBottom + ';';
var $grip = $('<div data-rm-textarea-grip="1" style="' + gripStyle + '"><span style="display:block;width:42px;height:4px;border-radius:999px;background:' + tk.bSub + ';opacity:.9;"></span></div>');
function getCoord(evt, key) {
var e = evt.originalEvent || evt;
if (e.touches && e.touches.length) return e.touches[0][key];
if (e.changedTouches && e.changedTouches.length) return e.changedTouches[0][key];
return e[key];
}
function stopDrag() {
dragState.active = false;
$(window).off('.rmTaResize');
}
function onDragMove(evt) {
var clientX;
var clientY;
if (!dragState.active) return;
clientX = getCoord(evt, 'clientX');
clientY = getCoord(evt, 'clientY');
if (typeof clientY !== 'number') return;
if (allowWidthResize && typeof clientX === 'number') $ta.css('width', Math.max(minWidth, Math.min(getMaxWidthFn(), dragState.startWidth + (clientX - dragState.startX))) + 'px');
$ta.css('height', Math.max(minHeight, dragState.startHeight + (clientY - dragState.startY)) + 'px');
syncFn();
if (evt.preventDefault) evt.preventDefault();
}
function startDrag(evt) {
var clientY = getCoord(evt, 'clientY');
if (typeof clientY !== 'number') return;
dragState.active = true;
dragState.startX = getCoord(evt, 'clientX') || 0;
dragState.startY = clientY;
dragState.startWidth = $ta.outerWidth();
dragState.startHeight = $ta.outerHeight();
if (evt.preventDefault) evt.preventDefault();
$(window).off('.rmTaResize');
if (usePointerEvents) {
$(window).on('pointermove.rmTaResize', onDragMove).on('pointerup.rmTaResize pointercancel.rmTaResize', stopDrag);
} else {
$(window).on('touchmove.rmTaResize mousemove.rmTaResize', onDragMove).on('touchend.rmTaResize touchcancel.rmTaResize mouseup.rmTaResize', stopDrag);
}
}
$ta.css({ 'border-bottom-left-radius': '0', 'border-bottom-right-radius': '0' });
$ta.next('[data-rm-textarea-grip]').remove();
$ta.after($grip);
if (usePointerEvents) $grip.on('pointerdown.rmTaGrip', startDrag);
else $grip.on('touchstart.rmTaGrip mousedown.rmTaGrip', startDrag);
}
function applyModalContentWidth($modal, contentWidth, options) {
var opts = options || {};
var layout = getModalLayout();
var modalFrame = getBoxFrameWidth($modal);
var safeContentWidth = Math.max(layout.minWidth, Math.min(contentWidth, layout.maxOuterWidth - modalFrame));
var modalWidth = safeContentWidth + modalFrame;
var initialContentW = parseFloat($modal.data('rmInitialContentW')) || 0;
$modal.css({
width: modalWidth + 'px',
'max-width': layout.maxOuterWidth + 'px',
'box-sizing': 'border-box',
'margin-left': layout.shouldCenter ? 'auto' : '0',
'margin-right': layout.shouldCenter ? 'auto' : '0'
}).toggleClass('rmCompactContent', safeContentWidth < 520);
$('.' + RESIZE_CLASS).css({
width: safeContentWidth + 'px',
'max-width': '100%',
'box-sizing': 'border-box'
});
$('#rmMsg,#nominationReason,#rmReportText').each(function () {
var $textarea = $(this);
var textareaId = this.id;
if (!$textarea.length) return;
$textarea.css('width', safeContentWidth + 'px');
$textarea.next('[data-rm-textarea-grip]').css('width', safeContentWidth + 'px');
$('.rmQuickPhrasesPanel[data-rm-target="' + textareaId + '"]').css('width', safeContentWidth + 'px');
});
$('.rmNestedCommentInput').each(function () {
var $textarea = $(this);
var $wrap = $textarea.parent();
var containerFrame = parseFloat($textarea.data('rmNestedContainerFrame')) || 0;
var wrapFrame = parseFloat($textarea.data('rmNestedWrapFrame')) || 0;
var safeMinWidth = parseFloat($textarea.data('rmNestedMinWidth')) || 0;
var wrapOuterWidth;
var textareaWidth;
if (!$wrap.length || !$wrap.is(':visible')) return;
wrapOuterWidth = Math.max(1, safeContentWidth - containerFrame);
textareaWidth = Math.max(1, wrapOuterWidth - wrapFrame);
$wrap.css({
width: wrapOuterWidth + 'px',
'max-width': '100%',
'box-sizing': 'border-box'
});
$textarea.css({
width: textareaWidth + 'px',
'min-width': Math.min(safeMinWidth || textareaWidth, textareaWidth) + 'px'
});
$textarea.next('[data-rm-textarea-grip]').css('width', textareaWidth + 'px');
$('.rmQuickPhrasesPanel[data-rm-target="' + this.id + '"]').css('width', textareaWidth + 'px');
});
syncLinkWidths();
if (isVector22) {
var $content = $('#content');
if ($content.length && modalWidth > initialContentW) $content.css({ 'min-width': modalWidth + 'px' });
else if ($content.length) $content.css({ 'min-width': '' });
} else {
$('#content').css({ 'min-width': '' });
}
if (!opts.skipStore) $modal.data('rmContentWidth', safeContentWidth);
return safeContentWidth;
}
function setupNestedResizableTextarea(textareaId, wrapId, minWidth, minHeight) {
var $ta = $('#' + textareaId);
var $wrap = $('#' + wrapId);
var $modal = $('#removerModal');
var $container = $wrap.parent();
var layout = getModalLayout();
var safeMinWidth = parseInt(minWidth, 10) || 280;
var safeMinHeight = parseInt(minHeight, 10) || 90;
var initialWidth;
var modalFrame;
var containerFrame;
var wrapFrame;
function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; }
function getMaxTextareaWidth(currentLayout) {
return Math.max(1, currentLayout.maxOuterWidth - modalFrame - containerFrame - wrapFrame);
}
function getEffectiveMinWidth(currentLayout) {
return Math.min(safeMinWidth, getMaxTextareaWidth(currentLayout));
}
function sync() {
var currentLayout = getModalLayout();
var maxTextareaWidth = getMaxTextareaWidth(currentLayout);
var textareaWidth = $ta.outerWidth();
var contentWidth;
if (!$wrap.is(':visible')) return;
if (textareaWidth > maxTextareaWidth) {
$ta.css('width', maxTextareaWidth + 'px');
textareaWidth = $ta.outerWidth();
}
contentWidth = Math.min(currentLayout.maxOuterWidth - modalFrame, textareaWidth + wrapFrame + containerFrame);
applyModalContentWidth($modal, contentWidth);
}
if (!$ta.length || !$wrap.length || !$modal.length) return;
modalFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width');
containerFrame = $container.length
? px($container, 'padding-left') + px($container, 'padding-right') + px($container, 'border-left-width') + px($container, 'border-right-width')
: 0;
wrapFrame = px($wrap, 'padding-left') + px($wrap, 'padding-right') + px($wrap, 'border-left-width') + px($wrap, 'border-right-width');
initialWidth = Math.min(
Math.max(getEffectiveMinWidth(layout), getDefaultResizableWidth(modalFrame + containerFrame + wrapFrame)),
getMaxTextareaWidth(layout)
);
$ta.css({
width: initialWidth + 'px',
'min-width': getEffectiveMinWidth(layout) + 'px',
'min-height': safeMinHeight + 'px',
'box-sizing': 'border-box',
resize: layout.useFullWidth ? 'none' : 'both',
'border-bottom-left-radius': '',
'border-bottom-right-radius': ''
});
$ta.data('rmNestedContainerFrame', containerFrame);
$ta.data('rmNestedWrapFrame', wrapFrame);
$ta.data('rmNestedMinWidth', safeMinWidth);
$ta.next('[data-rm-textarea-grip]').remove();
if (layout.useFullWidth) {
bindTouchTextareaGrip($ta, sync, function () {
return getMaxTextareaWidth(getModalLayout());
}, {
minWidth: getEffectiveMinWidth(layout),
minHeight: safeMinHeight
});
}
registerModalLayoutSync(sync);
$(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
if (typeof ResizeObserver === 'function') {
var observer = new ResizeObserver(sync);
observer.observe($ta[0]);
registerResizeObserver(observer);
}
sync();
}
function setupResizableModal(textareaId) {
var $ta = $('#' + textareaId);
var $modal = $('#removerModal');
var layout = getModalLayout();
function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; }
applyV2022Layout($modal);
$modal.css({ display: 'block', 'margin-left': layout.shouldCenter ? 'auto' : '0', 'margin-right': layout.shouldCenter ? 'auto' : '0' });
var isBorderBox = ($modal.css('box-sizing') || '').toLowerCase() === 'border-box';
var hFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width');
var initialContentW = isVector22 ? ($('#content').outerWidth() || 0) : 0;
var minWidth = layout.minWidth;
$modal.data('rmInitialContentW', initialContentW);
$ta.css({ width: getDefaultResizableWidth(hFrame) + 'px', height: sz.taH, padding: '8px', 'box-sizing': 'border-box',
border: '1px solid ' + tk.bSub, 'border-radius': '2px', background: tk.bgBase,
color: 'inherit', resize: layout.useFullWidth ? 'none' : 'both', 'min-height': sz.taMinH, 'min-width': sz.taMinW });
$(window).off('.rmTaResize');
function sync() {
var currentLayout = getModalLayout();
var maxTextareaWidth = Math.max(minWidth, currentLayout.maxOuterWidth - Math.floor(hFrame));
var w = $ta.outerWidth();
if (w > maxTextareaWidth) {
$ta.css('width', maxTextareaWidth + 'px');
w = $ta.outerWidth();
}
applyModalContentWidth($modal, isBorderBox ? w : Math.min(currentLayout.maxOuterWidth - hFrame, w));
}
if (layout.useFullWidth) bindTouchTextareaGrip($ta, sync, function () {
return Math.max(minWidth, getModalLayout().maxOuterWidth - Math.floor(hFrame));
});
else {
$ta.css({ 'border-bottom-left-radius': '', 'border-bottom-right-radius': '' });
$ta.next('[data-rm-textarea-grip]').remove();
}
registerModalLayoutSync(sync);
$(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
if (typeof ResizeObserver === 'function') {
var observer = new ResizeObserver(sync);
observer.observe($ta[0]);
registerResizeObserver(observer);
}
sync();
}
function addInputRow(opts) {
var w = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0;
$('#' + opts.containerId).append(joinHtml([
'<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, w ? 'width:' + w + 'px;' : '', '">',
'<input type="text" class="', opts.inputClass || 'variantInput', '" placeholder="', opts.placeholder || '', '" style="', stInputBox, '">',
'<button type="button" class="rmRemoveInput" style="', stRemoveBtn, '" title="Удалить">−</button>',
'</div>'
]));
syncModalLayout();
}
$(document).off('click.rmRemoveInput').on('click.rmRemoveInput', '.rmRemoveInput', function () {
$(this).closest('.rmInputRow').remove();
syncModalLayout();
});
$(document).off('click.rmQuickPhraseInsert').on('click.rmQuickPhraseInsert', '.rmQuickPhraseActionBtn', function (e) {
var targetId;
var phrase;
e.preventDefault();
targetId = $(this).data('rmTarget');
phrase = $(this).attr('data-rm-phrase') || '';
if (!targetId) return;
insertTextIntoTextarea($('#' + targetId), phrase);
});
function buildMultiInputHtml(c) {
var addLabel = c.addBtnLabel || '+ Добавить';
return joinHtml([
'<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="', c.firstId, '" type="text" class="', c.inputClass || 'variantInput', '" placeholder="', c.firstPh || '', '" style="', stInputBox, '">',
buildSquareAddButtonHtml(c.addBtnId, addLabel.replace(/^\+\s*/, '') || 'Добавить'),
'</div>',
'<div id="', c.containerId, '"></div>'
]);
}
function wireMultiInput(c) {
$('#' + c.addBtnId).click(function () {
var count = $('#' + c.containerId + ' .rmInputRow').length;
if (typeof c.maxRows === 'number' && count >= c.maxRows) { alert(c.maxMsg || 'Достигнут лимит.'); return; }
addInputRow({ containerId: c.containerId, placeholder: c.addPh, inputClass: c.inputClass });
});
}
function buildSettingsFieldHtml(label, controlHtml, helpText, options) {
var opts = options || {};
var helpHtml = helpText
? '<div class="rmSettingsFieldHint">' + helpText + '</div>'
: '';
var labelHtml = opts.forId
? '<label class="rmSettingsFieldLabel" for="' + opts.forId + '">' + label + '</label>'
: '<div class="rmSettingsFieldLabel">' + label + '</div>';
return joinHtml([
'<div class="rmSettingsField">',
labelHtml,
'<div class="rmSettingsFieldControl">', controlHtml, '</div>',
helpHtml,
'</div>'
]);
}
function buildSettingsSectionHtml(title, bodyHtml, helpText, options) {
var opts = options || {};
var headerHtml = '';
var description = [];
if (opts.titleNote) description.push(opts.titleNote);
if (helpText) description.push(helpText);
if (title || description.length) {
headerHtml = joinHtml([
'<div class="rmSettingsSectionHeader">',
title ? '<div class="rmSettingsSectionTitle">' + title + '</div>' : '',
description.length ? '<div class="rmSettingsSectionDescription">' + description.join(' ') + '</div>' : '',
'</div>'
]);
}
return joinHtml([
'<div class="rmSettingsSection">',
headerHtml,
bodyHtml,
'</div>'
]);
}
function buildSettingsSimpleCheckboxHtml(id, text) {
return joinHtml([
'<label class="rmSettingsCheck">',
'<input id="', id, '" type="checkbox">',
'<span>', text, '</span>',
'</label>'
]);
}
function buildQuickPhrasesSettingsEditorHtml() {
return joinHtml([
'<div id="rmSettingsQuickPhrasesEditor" class="rmQuickPhraseEditor">',
'<div id="rmSettingsQuickPhrasesList" class="rmQuickPhraseList"></div>',
'<input id="rmSettingsQuickPhraseInput" type="text" autocomplete="off" style="', stInputFull, 'margin-bottom:0;">',
'<div id="rmSettingsQuickPhraseMeta" class="rmQuickPhraseMeta"></div>',
'</div>'
]);
}
function getQuickPhraseEditor() {
return $('#rmSettingsQuickPhrasesEditor');
}
function getQuickPhraseEditorState() {
var $editor = getQuickPhraseEditor();
var phrases = normalizeQuickPhrasesList($editor.data('rmQuickPhrases'), []);
var editingIndex = parseInt($editor.data('rmQuickPhraseEditingIndex'), 10);
if (isNaN(editingIndex)) editingIndex = -1;
return { editor: $editor, phrases: phrases, editingIndex: editingIndex };
}
function setQuickPhraseEditorState(phrases, editingIndex) {
var $editor = getQuickPhraseEditor();
var normalized = normalizeQuickPhrasesList(phrases, []);
var safeEditingIndex = parseInt(editingIndex, 10);
if (isNaN(safeEditingIndex) || safeEditingIndex < 0 || safeEditingIndex >= normalized.length) safeEditingIndex = -1;
if (!$editor.length) return;
$editor.data('rmQuickPhrases', normalized);
$editor.data('rmQuickPhraseEditingIndex', safeEditingIndex);
renderQuickPhraseEditor();
}
function clearQuickPhraseDropState() {
var $editor = getQuickPhraseEditor();
$editor.removeData('rmQuickPhraseDragIndex');
$editor.removeData('rmQuickPhraseDropIndex');
$editor.removeData('rmQuickPhraseDropAfter');
$editor.find('.rmQuickPhraseChip').removeClass('is-dragging is-drop-before is-drop-after');
}
function renderQuickPhraseEditor() {
var state = getQuickPhraseEditorState();
var $list = $('#rmSettingsQuickPhrasesList');
var $input = $('#rmSettingsQuickPhraseInput');
var $meta = $('#rmSettingsQuickPhraseMeta');
if (!state.editor.length || !$list.length || !$input.length) return;
if (state.phrases.length) {
$list.html(state.phrases.map(function (phrase, index) {
var chipClass = 'rmQuickPhraseChip' + (index === state.editingIndex ? ' is-editing' : '');
return joinHtml([
'<div class="', chipClass, '" draggable="true" data-rm-quick-index="', index, '">',
'<button type="button" class="rmQuickPhraseEditBtn" title="Редактировать фразу">', escapeHtml(phrase), '</button>',
'<button type="button" class="rmQuickPhraseRemoveBtn" title="Удалить фразу" aria-label="Удалить фразу">×</button>',
'</div>'
]);
}).join(''));
} else {
$list.html('<div class="rmQuickPhraseEmpty">Фразы пока не добавлены.</div>');
}
$input
.attr('placeholder', state.editingIndex >= 0 ? 'Изменить значение...' : 'Добавить значение...')
.toggleClass('is-editing', state.editingIndex >= 0);
$meta
.text('')
.hide();
}
function notifyQuickPhraseEditorChanged() {
var $editor = getQuickPhraseEditor();
if ($editor.length) $editor.trigger('rmQuickPhrasesChanged');
}
function startQuickPhraseEdit(index) {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
if (index < 0 || index >= state.phrases.length || !$input.length) return;
state.editor.data('rmQuickPhraseEditingIndex', index);
$input.val(state.phrases[index]);
renderQuickPhraseEditor();
$input.trigger('focus');
if ($input[0] && typeof $input[0].select === 'function') $input[0].select();
}
function cancelQuickPhraseEdit() {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
state.editor.data('rmQuickPhraseEditingIndex', -1);
if ($input.length) $input.val('').removeClass('is-editing');
renderQuickPhraseEditor();
}
function saveQuickPhraseInput() {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
var value = normalizeQuickPhraseValue($input.val());
var next = [];
if (!$input.length || !value) return false;
if (state.editingIndex >= 0) {
state.phrases.forEach(function (phrase, index) {
if (index === state.editingIndex) {
next.push(value);
return;
}
if (phrase !== value && next.indexOf(phrase) === -1) next.push(phrase);
});
} else {
next = state.phrases.slice();
if (next.indexOf(value) === -1) next.push(value);
}
state.editor.data('rmQuickPhrases', normalizeQuickPhrasesList(next, []));
state.editor.data('rmQuickPhraseEditingIndex', -1);
$input.val('').removeClass('is-editing');
clearQuickPhraseDropState();
renderQuickPhraseEditor();
notifyQuickPhraseEditorChanged();
return true;
}
function removeQuickPhrase(index) {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
if (index < 0 || index >= state.phrases.length) return;
state.phrases.splice(index, 1);
state.editor.data('rmQuickPhrases', state.phrases);
if (state.editingIndex === index) {
state.editor.data('rmQuickPhraseEditingIndex', -1);
if ($input.length) $input.val('').removeClass('is-editing');
} else if (state.editingIndex > index) {
state.editor.data('rmQuickPhraseEditingIndex', state.editingIndex - 1);
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
notifyQuickPhraseEditorChanged();
}
function reorderQuickPhrases(phrases, fromIndex, toIndex, placeAfter) {
var result = phrases.slice();
var insertIndex = toIndex + (placeAfter ? 1 : 0);
var item;
if (fromIndex < 0 || fromIndex >= result.length || toIndex < 0 || toIndex >= result.length) return result;
item = result.splice(fromIndex, 1)[0];
if (fromIndex < insertIndex) insertIndex--;
result.splice(insertIndex, 0, item);
return result;
}
function getQuickPhraseDropPointer(evt) {
var originalEvent = evt && (evt.originalEvent || evt);
if (!originalEvent) return null;
if (typeof originalEvent.clientX !== 'number' || typeof originalEvent.clientY !== 'number') return null;
return { x: originalEvent.clientX, y: originalEvent.clientY };
}
function getQuickPhraseDropTarget($list, pointer, dragIndex) {
var candidates = [];
var rowCandidates;
var minRowDistance = Infinity;
var bestBoundary = null;
var bestBoundaryDistance = Infinity;
if (!$list || !$list.length || !pointer) return null;
$list.children('.rmQuickPhraseChip').each(function () {
var index = parseInt($(this).attr('data-rm-quick-index'), 10);
var rect;
var rowDistance;
if (isNaN(index) || index === dragIndex) return;
rect = this.getBoundingClientRect();
if (!rect.width || !rect.height) return;
rowDistance = pointer.y < rect.top ? (rect.top - pointer.y) : (pointer.y > rect.bottom ? (pointer.y - rect.bottom) : 0);
candidates.push({
node: this,
index: index,
left: rect.left,
right: rect.right,
top: rect.top,
bottom: rect.bottom,
midX: rect.left + rect.width / 2,
rowDistance: rowDistance
});
if (rowDistance < minRowDistance) minRowDistance = rowDistance;
});
if (!candidates.length) return null;
rowCandidates = candidates
.filter(function (candidate) { return candidate.rowDistance === minRowDistance; })
.sort(function (a, b) {
if (a.left !== b.left) return a.left - b.left;
return a.index - b.index;
});
if (!rowCandidates.length) return null;
if (pointer.x <= rowCandidates[0].left) {
return { index: rowCandidates[0].index, placeAfter: false, node: rowCandidates[0].node };
}
if (pointer.x >= rowCandidates[rowCandidates.length - 1].right) {
return {
index: rowCandidates[rowCandidates.length - 1].index,
placeAfter: true,
node: rowCandidates[rowCandidates.length - 1].node
};
}
for (var i = 0; i < rowCandidates.length; i++) {
var candidate = rowCandidates[i];
if (pointer.x >= candidate.left && pointer.x <= candidate.right) {
return {
index: candidate.index,
placeAfter: pointer.x > candidate.midX,
node: candidate.node
};
}
}
rowCandidates.forEach(function (candidate) {
var leftDistance = Math.abs(pointer.x - candidate.left);
var rightDistance = Math.abs(pointer.x - candidate.right);
if (leftDistance < bestBoundaryDistance) {
bestBoundaryDistance = leftDistance;
bestBoundary = { index: candidate.index, placeAfter: false, node: candidate.node };
}
if (rightDistance < bestBoundaryDistance) {
bestBoundaryDistance = rightDistance;
bestBoundary = { index: candidate.index, placeAfter: true, node: candidate.node };
}
});
return bestBoundary;
}
function bindQuickPhrasesEditor() {
var $editor = getQuickPhraseEditor();
var $list = $('#rmSettingsQuickPhrasesList');
var $input = $('#rmSettingsQuickPhraseInput');
function updateQuickPhraseDropTarget(evt) {
var dragIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10);
var pointer = getQuickPhraseDropPointer(evt);
var target;
if (isNaN(dragIndex) || !pointer) return null;
target = getQuickPhraseDropTarget($list, pointer, dragIndex);
if (!target) return null;
$editor.data('rmQuickPhraseDropIndex', target.index);
$editor.data('rmQuickPhraseDropAfter', !!target.placeAfter);
$editor.find('.rmQuickPhraseChip').removeClass('is-drop-before is-drop-after');
$(target.node).addClass(target.placeAfter ? 'is-drop-after' : 'is-drop-before');
return target;
}
function applyQuickPhraseDrop() {
var state = getQuickPhraseEditorState();
var fromIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10);
var toIndex = parseInt($editor.data('rmQuickPhraseDropIndex'), 10);
var placeAfter = $editor.data('rmQuickPhraseDropAfter') === true;
var changed = false;
if (!isNaN(fromIndex) && !isNaN(toIndex) && fromIndex !== toIndex) {
state.editor.data('rmQuickPhrases', reorderQuickPhrases(state.phrases, fromIndex, toIndex, placeAfter));
if (state.editingIndex === fromIndex) state.editor.data('rmQuickPhraseEditingIndex', -1);
changed = true;
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
if (changed) notifyQuickPhraseEditorChanged();
}
if (!$editor.length || !$list.length || !$input.length) return;
$editor.off('.rmQuickPhraseEditor');
$list.off('.rmQuickPhraseEditor');
$input.off('.rmQuickPhraseEditor');
$input.on('keydown.rmQuickPhraseEditor', function (e) {
if (e.key === 'Enter' || e.keyCode === 13) {
e.preventDefault();
saveQuickPhraseInput();
} else if (e.key === 'Escape' || e.keyCode === 27) {
e.preventDefault();
cancelQuickPhraseEdit();
}
});
$editor
.on('click.rmQuickPhraseEditor', '.rmQuickPhraseEditBtn', function () {
var index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10);
startQuickPhraseEdit(index);
})
.on('click.rmQuickPhraseEditor', '.rmQuickPhraseRemoveBtn', function (e) {
var index;
e.preventDefault();
e.stopPropagation();
index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10);
removeQuickPhrase(index);
})
.on('dragstart.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
var index = parseInt($(this).attr('data-rm-quick-index'), 10);
if (isNaN(index)) return;
$editor.data('rmQuickPhraseDragIndex', index);
$(this).addClass('is-dragging');
if (e.originalEvent && e.originalEvent.dataTransfer) {
e.originalEvent.dataTransfer.effectAllowed = 'move';
e.originalEvent.dataTransfer.setData('text/plain', String(index));
}
})
.on('dragover.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
if ($editor.data('rmQuickPhraseDragIndex') === undefined) return;
e.preventDefault();
updateQuickPhraseDropTarget(e);
if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move';
})
.on('drop.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
e.preventDefault();
updateQuickPhraseDropTarget(e);
applyQuickPhraseDrop();
})
.on('dragend.rmQuickPhraseEditor', '.rmQuickPhraseChip', function () {
clearQuickPhraseDropState();
});
$list
.on('dragover.rmQuickPhraseEditor', function (e) {
if ($editor.data('rmQuickPhraseDragIndex') === undefined) return;
e.preventDefault();
updateQuickPhraseDropTarget(e);
if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move';
})
.on('drop.rmQuickPhraseEditor', function (e) {
e.preventDefault();
updateQuickPhraseDropTarget(e);
applyQuickPhraseDrop();
});
renderQuickPhraseEditor();
}
function collectQuickPhraseValues() {
return getQuickPhraseEditorState().phrases;
}
function collectQuickPhraseValuesSnapshot() {
var state = getQuickPhraseEditorState();
var value = normalizeQuickPhraseValue($('#rmSettingsQuickPhraseInput').val());
var next = state.phrases.slice();
if (!value) return next;
if (state.editingIndex >= 0) {
next[state.editingIndex] = value;
} else if (next.indexOf(value) === -1) {
next.push(value);
}
return normalizeQuickPhrasesList(next, []);
}
function isMenuTitlePresetValue(value) {
return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS;
}
function isMenuTitlePresetOnlySkin() {
return mwCfg.skin === 'minerva' || mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
}
function getDefaultMenuTitlePreset() {
return mwCfg.skin === 'minerva' ? MENU_TITLE_PRESET_TOOLS : MENU_TITLE_PRESET_CACTIONS;
}
function shouldPreserveStoredMenuTitleOnSave() {
return mwCfg.skin === 'minerva';
}
function isAvailableMenuTitlePresetValue(value) {
return getMenuTitlePresetOptions().some(function (option) {
return option.value === value;
});
}
function getMenuTitlePresetOptions() {
if (mwCfg.skin === 'minerva') {
return [
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Ещё' }
];
}
if (isVector22) {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты/Действия' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты/Основное' }
];
}
if (mwCfg.skin === 'timeless') {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты для страниц' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Вики-инструменты' }
];
}
if (mwCfg.skin === 'monobook') {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Верхняя панель' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' }
];
}
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: mwCfg.skin === 'vector' ? 'Ещё' : 'Ещё / Действия' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' }
];
}
function getMenuTitlePresetHintText() {
var base = (mwCfg.skin === 'vector' || isVector22)
? 'Можно либо изменить заголовок отдельного меню Remover, либо перенести все пункты в одно из существующих стандартных меню.'
: 'На этом скине Remover использует существующие стандартные меню; отдельное меню с собственным заголовком не создаётся.';
if (isVector22) base += ' В Vector 2022 кнопки «Действия» и «Основное» находятся внутри общего меню «Инструменты».';
else if (mwCfg.skin === 'vector') base += ' В Vector отдельный заголовок создаёт собственное меню рядом с «Ещё».';
else if (mwCfg.skin === 'minerva') base += ' В Minerva Neue пункты Remover показываются в меню «Ещё».';
else if (mwCfg.skin === 'timeless') base += ' В Timeless доступны только стандартные варианты: «Инструменты для страниц», «Страница» и «Вики-инструменты».';
else if (mwCfg.skin === 'monobook') base += ' В MonoBook доступны только два варианта: поместить пункты в «Инструменты» или вывести их в верхнюю панель. Собственный заголовок меню здесь не используется.';
return base;
}
function getSignatureSeparatorPreviewText(value) {
var separator = String(value || '').trim();
return separator ? (separator + ' ' + '~~' + '~~') : ('~~' + '~~');
}
function updateSignatureSeparatorPreview(value) {
var previewValue = (typeof value === 'string') ? value : ($('#rmSettingsSignatureSeparator').val() || '');
var $code = $('#rmSettingsSignaturePreviewCode');
if (!$code.length) return;
$code.text(getSignatureSeparatorPreviewText(previewValue));
}
function bindSignatureSeparatorPreview() {
var $input = $('#rmSettingsSignatureSeparator');
if (!$input.length) return;
$input.off('.rmSignaturePreview').on('input.rmSignaturePreview change.rmSignaturePreview', function () {
updateSignatureSeparatorPreview($(this).val());
});
updateSignatureSeparatorPreview($input.val());
}
function buildMenuTitlePresetButtonsHtml() {
return joinHtml([
'<div class="rmSettingsMenuPresetWrap">',
'<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>',
'<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">',
getMenuTitlePresetOptions().map(function (option) {
return joinHtml([
'<button type="button" class="rmSettingsMenuPresetBtn" data-rm-menu-preset="',
option.value,
'" aria-pressed="false">',
escapeHtml(option.label),
'</button>'
]);
}).join(''),
'</div>',
'</div>'
]);
}
function applyMenuTitlePresetControls(presetValue) {
var preset = isMenuTitlePresetValue(presetValue) ? presetValue : '';
var $bar = $('#rmSettingsMenuPresetBar');
var $input = $('#rmSettingsMenuTitle');
var forcePresetOnly = isMenuTitlePresetOnlySkin();
if (!$bar.length || !$input.length) return;
$bar.data('rmPreset', preset);
$bar.find('.rmSettingsMenuPresetBtn').each(function () {
var isActive = $(this).data('rmMenuPreset') === preset;
$(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false');
});
$input.prop('disabled', forcePresetOnly || !!preset);
}
function bindMenuTitlePresetControls() {
var $bar = $('#rmSettingsMenuPresetBar');
var $input = $('#rmSettingsMenuTitle');
if (!$bar.length || !$input.length) return;
$input.off('.rmSettingsMenuPreset').on('input.rmSettingsMenuPreset', function () {
if (!$bar.data('rmPreset')) $input.data('rmCustomValue', $input.val());
});
$bar.off('.rmSettingsMenuPreset').on('click.rmSettingsMenuPreset', '.rmSettingsMenuPresetBtn', function () {
var preset = $(this).data('rmMenuPreset');
var currentPreset = $bar.data('rmPreset') || '';
if (currentPreset === preset) {
if (isMenuTitlePresetOnlySkin()) return;
applyMenuTitlePresetControls('');
$input.val($input.data('rmCustomValue') || '');
$input.trigger('focus');
return;
}
$input.data('rmCustomValue', $input.val());
applyMenuTitlePresetControls(preset);
});
}
function fillSettingsFormValues(settings) {
var data = normalizeRemoverSettings(settings);
var forcePresetOnly = isMenuTitlePresetOnlySkin();
var menuTitleValue = data.menuTitle || '';
var storedMenuTitleValue = menuTitleValue;
if (forcePresetOnly && !isAvailableMenuTitlePresetValue(menuTitleValue)) menuTitleValue = getDefaultMenuTitlePreset();
var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue);
$('#rmSettingsForm').data('rmStoredMenuTitle', storedMenuTitleValue || '');
$('#rmSettingsNotifyAuthor').prop('checked', !!data.notifyAuthor);
$('#rmSettingsSubscribeTopic').prop('checked', !!data.subscribeTopic);
$('#rmSettingsShowMenuIcons').prop('checked', !!data.showMenuIcons);
$('#rmSettingsMenuTitle').val(customMenuTitle || '').data('rmCustomValue', customMenuTitle || '');
$('#rmSettingsSignatureSeparator').val(data.signatureSeparator || '');
$('#rmSettingsExcludedNamespaces').val((data.excludedNamespaces || []).join(', '));
$('#rmSettingsDisabledItems').val((data.disabledItems || []).join(', '));
setQuickPhraseEditorState(data.quickPhrases || [], -1);
$('#rmSettingsQuickPhraseInput').val('').removeClass('is-editing');
clearQuickPhraseDropState();
applyMenuTitlePresetControls(menuTitleValue);
updateSignatureSeparatorPreview(data.signatureSeparator || '');
}
function collectSettingsFormValues(options) {
var opts = options || {};
var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val());
var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val());
var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset');
var forcePresetOnly = isMenuTitlePresetOnlySkin();
var storedMenuTitle = $('#rmSettingsForm').data('rmStoredMenuTitle');
if (namespaces.invalid.length) {
return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' };
}
if (disabledItems.invalid.length) {
return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' };
}
if (!opts.skipQuickPhraseCommit) saveQuickPhraseInput();
return {
value: normalizeRemoverSettings({
notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'),
subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'),
showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'),
menuTitle: forcePresetOnly
? (shouldPreserveStoredMenuTitleOnSave() && typeof storedMenuTitle === 'string'
? storedMenuTitle
: (isAvailableMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : getDefaultMenuTitlePreset()))
: (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()),
signatureSeparator: $('#rmSettingsSignatureSeparator').val(),
excludedNamespaces: namespaces.values,
disabledItems: disabledItems.values,
quickPhrases: opts.skipQuickPhraseCommit ? collectQuickPhraseValuesSnapshot() : collectQuickPhraseValues()
})
};
}
function updateSettingsSubmitReadyState(baselineSettings) {
var collected = collectSettingsFormValues({ skipQuickPhraseCommit: true });
var hasChanges = !!(collected.error || (collected.value && !areRemoverSettingsEqual(collected.value, baselineSettings)));
$('#removerSubmit').toggleClass('rmSubmitReady', hasChanges && !$('#removerSubmit').hasClass('rmSubmitError'));
$('#rmSettingsUnsavedHint').css('display', hasChanges ? 'inline-block' : 'none');
}
function bindSettingsSubmitReadyState(baselineSettings) {
var update = function () {
setTimeout(function () { updateSettingsSubmitReadyState(baselineSettings); }, 0);
};
$('#rmSettingsForm').off('.rmSettingsReady').on('input.rmSettingsReady change.rmSettingsReady', 'input, textarea, select', update);
$('#removerModalContent').off('click.rmSettingsReady keyup.rmSettingsReady').on(
'click.rmSettingsReady keyup.rmSettingsReady',
'.rmSettingsMenuPresetBtn,.rmQuickPhraseEditBtn,.rmQuickPhraseRemoveBtn,.rmQuickPhraseChip,#rmSettingsQuickPhraseInput',
update
);
$('#rmSettingsQuickPhrasesEditor').off('rmQuickPhrasesChanged.rmSettingsReady').on('rmQuickPhrasesChanged.rmSettingsReady', update);
updateSettingsSubmitReadyState(baselineSettings);
}
function buildSettingsFormHtml(menuLabelsHint) {
var menuFields =
buildSettingsFieldHtml('Заголовок отдельного меню',
'<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(),
getMenuTitlePresetHintText(), { forId: 'rmSettingsMenuTitle' }) +
buildSettingsFieldHtml('Визуальное оформление меню',
'<div class="rmSettingsChecks">' + buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи в пунктах меню') + '</div>');
var messageFields =
buildSettingsFieldHtml('Префикс перед подписью',
'<input id="rmSettingsSignatureSeparator" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Добавляется перед подписью в публикуемых сообщениях.<span style="display:block;margin-top:6px;">Предпросмотр: <code id="rmSettingsSignaturePreviewCode"></code>.</span>',
{ forId: 'rmSettingsSignatureSeparator' }) +
buildSettingsFieldHtml('Часто используемые фразы', buildQuickPhrasesSettingsEditorHtml(),
'Enter для добавления. Порядок элементов изменяется перетаскиванием.', { forId: 'rmSettingsQuickPhraseInput' });
var defaultFields = '<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') +
buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') + '</div>';
var disableFields =
buildSettingsFieldHtml('Скрыть пункты меню',
'<input id="rmSettingsDisabledItems" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Названия пунктов через запятую, например <code>КБУ, КУЛ, КОБ</code>.' + menuLabelsHint,
{ forId: 'rmSettingsDisabledItems' }) +
buildSettingsFieldHtml('Не показывать в пространствах имён',
'<input id="rmSettingsExcludedNamespaces" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Номера пространств имён через запятую, например <code>2, 10, 828</code>. См. <a href="' + getPageUrl('Википедия:Пространства имён') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Пространства имён</a>.',
{ forId: 'rmSettingsExcludedNamespaces' });
return joinHtml([
'<div id="rmSettingsForm" style="max-width:100%;">',
'<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>',
buildSettingsSectionHtml('Меню', menuFields, 'Настройки внешнего вида и состава меню Remover.'),
buildSettingsSectionHtml('Оформление сообщений', messageFields, 'Настройки оформления публикуемых сообщений в номинациях.'),
buildSettingsSectionHtml('Опции по умолчанию', defaultFields, 'Регулирует изначальное состояние галочек.'),
buildSettingsSectionHtml('Отключение', disableFields, 'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'),
'</div>'
]);
}
function buildSettingsFooterLeftHtml() {
return joinHtml([
'<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;margin-right:auto;">',
'<button id="rmSettingsResetFooter" type="button" title="Удаляет сохранённые настройки Remover из вашего профиля MediaWiki и возвращает значения по умолчанию." style="', stCancel, '">Сбросить все настройки</button>',
'<a id="rmSettingsReportIssue" href="', getPageUrl('Обсуждение участника:Solidest/Remover'), '" target="_blank" rel="noopener noreferrer" ',
'title="Сообщить о проблеме или предложить улучшение" aria-label="Сообщить о проблеме или предложить улучшение" ',
'class="removerModalLink rmButtonLikeLink" style="', stCancel, 'display:inline-flex;align-items:center;justify-content:center;text-align:center;text-decoration:none;box-sizing:border-box;max-width:100%;line-height:1.2;word-break:normal;overflow-wrap:normal;">Обратная связь</a>',
'</div>'
]);
}
function openSettings() {
var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults);
var menuLabelsHint = buildSettingsMenuItemsHint();
var $previousModal = $('#removerModal').length ? $('#removerModal').detach() : $();
var previousLayoutSyncHandlers = modalLayoutSyncHandlers.slice();
function restorePreviousModal() {
closeModal();
if ($previousModal.length) {
$('#content').prepend($previousModal);
modalLayoutSyncHandlers = previousLayoutSyncHandlers.slice();
if (modalLayoutSyncHandlers.length) $(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
syncModalLayout();
syncLinkWidths();
}
}
createModal({
title: 'Конфигурация',
width: 'compact',
showSettingsButton: false
});
$('#removerModal').addClass('rmModalSettings');
$('#removerModalHeaderBar').append(buildHeaderIconButtonHtml('rmSettingsBack', 'Назад', 'Назад', '←'));
$('#rmSettingsBack').on('click', restorePreviousModal);
$('#removerModalContent').html(buildSettingsFormHtml(menuLabelsHint));
fillSettingsFormValues(currentSettings);
bindMenuTitlePresetControls();
bindSignatureSeparatorPreview();
bindQuickPhrasesEditor();
renderModalFooter('submit', {
showCheckbox: false,
submitText: 'Сохранить',
onSubmit: function () {
var collected = collectSettingsFormValues();
var shouldReset;
var saveFn;
if (collected.error) {
alert(collected.error);
return false;
}
shouldReset = areRemoverSettingsEqual(collected.value, settingsDefaults);
saveFn = shouldReset
? function (callback) { resetSettingsOnServer(callback); }
: function (callback) { saveSettingsToServer(collected.value, callback); };
saveFn(function (err) {
if (err) {
alert('Не удалось ' + (shouldReset ? 'сбросить' : 'сохранить') + ' настройки: ' + (err.info || err.code || 'неизвестная ошибка') + '.');
unlockModalSubmit();
return;
}
location.reload();
});
}
});
var $settingsActions = $('#rmFooterActionButtons');
$settingsActions.wrapInner('<div id="rmSettingsActionButtonsRow"></div>');
$settingsActions.append('<span id="rmSettingsUnsavedHint" role="status" aria-live="polite">Есть несохранённые изменения</span>');
bindSettingsSubmitReadyState(currentSettings);
$('#rmFooterButtons').css('justify-content', 'space-between').prepend(buildSettingsFooterLeftHtml());
$('#rmSettingsResetFooter').on('click', function () {
fillSettingsFormValues(settingsDefaults);
updateSettingsSubmitReadyState(currentSettings);
$('#removerSubmit').trigger('focus');
});
}
// ─── Завершение обработки ────────────────────────────────────────────────
function finalizeSuccess(nominationInfo, usePageReload) {
if (isError) {
var $box = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent');
$box.append('<p class="error">При выполнении скрипта произошли ошибки.</p>');
markSubmitError();
return;
}
renderModalFooter('reload');
if (nominationInfo && nominationInfo.pageTitle) {
appendNominationLink(nominationInfo.pageTitle, nominationInfo.sectionTitle);
}
if (!usePageReload && !nominationInfo) location.reload();
}
function finalizeFastRemoval(notifiedPages, summary) {
if (isError || !setAlert || !notifiedPages || !notifiedPages.length) {
finalizeSuccess(null, false);
return;
}
notifyAuthorsForPages(notifiedPages, {
summary: summary,
actionText: 'к быстрому удалению'
}, function () {
finalizeSuccess(null, false);
});
}
// ─── Общий runner ────────────────────────────────────────────────────────
/**
* Универсальный запуск полного пайплайна номинации.
* @param {Object} o
* templateStep — функция (next) → обработка шаблонов на статьях
* nominationStep — функция (done) → публикация номинации, done(err, {pageTitle, sectionTitle})
* notifyStep — функция (nominationInfo, next)
* skipNotify — boolean
* skipLink — boolean, не показывать ссылку на номинацию
*/
function runFlow(o) {
runNominationPipeline({
templateStep: o.templateStep,
nominationStep: o.nominationStep,
notifyStep: o.notifyStep || function (info, next) { next(); },
skipNotify: o.skipNotify,
onSuccess: function (ctx) {
if (isError) { markSubmitError(); return; }
renderModalFooter('reload');
if (!o.skipLink && ctx.nominationInfo && ctx.nominationInfo.pageTitle) {
appendNominationLink(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle);
}
},
onFailure: function () { markSubmitError(); }
});
}
// ═══════════════════════════════════════════════════════════════════════════
// ЯДРО: обработка статей (apply template + nomination page)
// ═══════════════════════════════════════════════════════════════════════════
/**
* Применяет шаблон к одной статье/категории.
* Понимает режим inArticle (вставка через <noinclude>),
* режим closeAction (снятие шаблона + запись на СО),
* режим cleanupAction (снятие КБУ/КУЛ).
*
* @param {string} pg — название страницы
* @param {Object} job — параметры задания (см. buildJob)
* @param {function} callback(err, meta)
*/
function applyTemplateToPage(pg, job, callback) {
var mode = job.mode;
// ── Снятие КБУ/КУЛ ──────────────────────────────────────────────────
if (mode === 'cleanup') {
var tm = job.transferMode || 'none';
if (tm === 'none') { callback({ code: 'error', info: 'Не выбран режим снятия шаблонов.' }); return; }
editPageContent(pg, { summary: job.summary, watchlist: 'nochange', readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var local = removeTransferTemplatesLocal(article, tm);
removeTransferTemplatesWithApiFallback(pg, local.text, tm, local, function (updated) {
if (updated.text === article) { done({ error: { code: 'error', info: 'Шаблоны для снятия не найдены.' } }); return; }
done({ text: updated.text });
});
}, function (err) { callback(err); });
return;
}
// ── Подведение итогов по КУ/КПМ (снятие + итог на СО) ───────────────
if (mode === 'denom') {
getTextWithTimestamp(pg, function (article, baseTimestamp, readErr) {
if (readErr) { callback(makeReadError(readErr, 'read_failed', 'Не удалось получить содержимое страницы «' + pg + '».')); return; }
if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; }
if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; }
var tplPattern = job.sourceTemplate.split('|').map(function (alias) {
return escapeRegExp(alias.trim()).replace(/\s+/g, '[ _]*');
}).join('|');
var tpl = findTemplateByPattern(article, tplPattern);
if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; }
var normalizedTplDate = convertToStandardDate(tpl.params[0]);
var tplExtra = tpl.params.slice(1).join('|').trim();
if (!RE_DATE_ISO.test(normalizedTplDate)) {
callback({ code: 'error', info: 'Не удалось распознать дату в шаблоне: «' + (tpl.params[0] || '') + '».' });
return;
}
var date = getDate(normalizedTplDate);
var nomPlace = 'ВП:' + job.sourceTemplate.replace(/\|.*/, '') + '/' + date[1];
var retTalkSection = '';
var sectionNW, tplpar, newTalkTpl;
if (job.closeType === 'doneRnm') { sectionNW = job.oldTitle + ' → ' + pg; tplpar = job.oldTitle + '|' + pg; }
if (job.closeType === 'noRnm') { sectionNW = pg + ' → ' + tplExtra; tplpar = pg + '|' + tplExtra; }
if (job.closeType === 'ret' || job.closeType === 'retConditional') {
retTalkSection = tplExtra;
sectionNW = retTalkSection || pg;
tplpar = retTalkSection ? ('l1=' + retTalkSection) : '';
}
var editSummary = makeSummary('номинация [[' + (nomPlace ? nomPlace + '#' : '') + sectionNW + ']] — ' + job.resultTemplate);
var talkTitle = getTalkPage(pg);
newTalkTpl = (job.closeType === 'retConditional')
? buildConditionalRetTemplateText(date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline, 1)
: (T_OPEN + job.resultTemplate + '|' + date[0] + '|' + tplpar + T_CLOSE);
getTextWithTimestamp(talkTitle, function (talkText, talkTimestamp, talkReadErr) {
if (talkReadErr) { callback(makeReadError(talkReadErr, 'talk_read_failed', 'Не удалось получить содержимое СО страницы «' + pg + '».')); return; }
var sourceTalkText = talkText || '';
var talkResult = (job.closeType === 'ret')
? upsertRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection)
: (job.closeType === 'retConditional')
? upsertConditionalRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline)
: { text: insertTplOnTalkPage(sourceTalkText, newTalkTpl, '\n'), status: 'created' };
function saveArticle() {
var cleaned = stripTemplatesByPattern(article, tplPattern).text;
var ep = { title: pg, text: cleaned, summary: editSummary, watchlist: 'nochange', assertuser: mwCfg.wgUserName };
if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (t) {
var editErr = t && t.error ? t.error : null;
callback(editErr, editErr ? null : {
discussionPage: nomPlace,
discussionSection: sectionNW,
summary: editSummary
});
});
}
if (talkResult.text === sourceTalkText) { saveArticle(); return; }
var talkEp = { title: talkTitle, text: talkResult.text, summary: editSummary };
if (talkTimestamp) talkEp.basetimestamp = talkTimestamp;
apiReq(talkEp, 'edit', function (talkResp) {
if (talkResp && talkResp.error) { callback({ code: 'talk_failed', info: 'Не удалось записать итог на СО: ' + talkResp.error.info }); return; }
saveArticle();
});
});
});
return;
}
// ── Обычная номинация: вставка шаблона в статью ─────────────────────
// mode === 'nominate'
var isKu = job.opId === 'tRm' || job.opId === 'mRm';
editPageContent(pg, { summary: job.summary, readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var hasExistingKu = isKu && RE_KU_ON_PAGE.test(article);
var conflictDecision = getConflictDecisionForPage(job, pg);
function buildResult(finalText) {
var generatedTpl = buildGeneratedNominationTemplateText(job, pg);
return { text: generatedTpl ? wrapInNoinclude(finalText, generatedTpl) : finalText };
}
function finishConflictResolution(sourceText) {
var resolvedText;
var pageLink = buildQuotedStatusPageLink(pg);
if (conflictDecision.templateAction === 'keep') {
if (sourceText !== article) {
return {
text: sourceText,
meta: { successMessage: 'Шаблон на странице ' + pageLink + ' оставлен без изменений; шаблоны переноса сняты.' }
};
}
return { skip: true, meta: { successMessage: 'Шаблон на странице ' + pageLink + ' оставлен без изменений.' } };
}
resolvedText = applyConflictTemplateResolution(sourceText, job, pg, conflictDecision);
return {
text: resolvedText,
meta: {
successMessage: conflictDecision.templateAction === 'overwrite'
? 'Шаблон КУ на странице ' + pageLink + ' перезаписан новой датой.'
: 'Новый шаблон КУ добавлен сверху на странице ' + pageLink + '.'
}
};
}
if (hasExistingKu && (!conflictDecision || conflictDecision.pageAction !== 'keep')) {
return { error: { code: 'error', info: 'На странице уже стоит шаблон КУ.' } };
}
if (hasExistingKu && conflictDecision && conflictDecision.pageAction === 'keep') {
if (job.transferMode && job.transferMode !== 'none') {
var localConflict = removeTransferTemplatesLocal(article, job.transferMode);
removeTransferTemplatesWithApiFallback(pg, localConflict.text, job.transferMode, localConflict, function (updated) {
done(finishConflictResolution(updated.text));
});
return;
}
return finishConflictResolution(article);
}
if (isKu && job.transferMode && job.transferMode !== 'none') {
var local = removeTransferTemplatesLocal(article, job.transferMode);
removeTransferTemplatesWithApiFallback(pg, local.text, job.transferMode, local, function (updated) { done(buildResult(updated.text)); });
return;
}
return buildResult(article);
},
function (err) { callback(err); }
);
}
/**
* Обрабатывает список страниц последовательно.
* @param {string[]} pages
* @param {Object} job
* @param {function} onDone(notifiedPages, err, pageMeta)
*/
function processPageList(pages, job, onDone) {
var notifiedPages = [];
var pageMeta = {};
eachSequential(pages.slice().reverse(), function (pg, nextPage) {
var pageLink = buildQuotedStatusPageLink(pg);
var statusId = logStatus('Обрабатывается страница ' + pageLink + '...', null, { pending: true, trackError: false });
applyTemplateToPage(pg, job, function (err, meta) {
var normPg = normTitle(pg);
var isClose = job.mode === 'cleanup' || job.mode === 'denom';
if (!isClose) {
if (!err && meta && meta.successMessage) logStatus(meta.successMessage, null, { statusId: statusId, trackError: false });
else logPageEdit(pg, err, { statusId: statusId });
} else {
if (err) { logStatus('Завершение по странице ' + pageLink, err, { statusId: statusId }); }
else {
logStatus('Шаблон снят со страницы ' + pageLink + '.', null, { statusId: statusId, trackError: false });
if (job.mode === 'denom') logStatus('Шаблон установлен на СО страницы ' + pageLink + '.', null, { trackError: false });
}
}
if (!err) {
notifiedPages.push(pg);
if (meta) pageMeta[normPg] = meta;
}
nextPage(err || null);
});
}, function (err) { onDone(notifiedPages, err, pageMeta); });
}
// ═══════════════════════════════════════════════════════════════════════════
// ПОСТРОЕНИЕ JOB из формы
// ═══════════════════════════════════════════════════════════════════════════
/**
* Строит объект job из данных формы для операции номинации (tRm, rnm, imp, merge, split, recov).
* @param {Object} op — запись из OPERATIONS
* @param {string} pg — целевая страница (уже разрешённая)
* @param {boolean} isMulti — режим мультиноминации
* @returns {Object|false} — job или false при ошибке ввода
*/
function buildNominationJob(op, pg, isMulti) {
var nom = op.nomination;
var date = getDate();
var msg = normalizeQuickPhraseValue($('#rmMsg').val());
var rawMsg = msg;
var opId = isMulti ? 'mRm' : op.id;
var tplpar = '';
var section, sectionNW, extraPages, multiArticles = [];
var multiHeaderText = '';
var multiArticleComments = {};
// Вычислить section и tplpar в зависимости от типа дополнительного ввода
if (nom.extraInput) {
var ei = nom.extraInput;
if (ei.type === 'rename') {
var rn = collectInputValues('.rmRenameInput');
if (!rn.length) { alert('Укажите новое название.'); return false; }
tplpar = rn[0] + (rn.length > 1 ? '||' + rn.slice(1).join('|') : '');
section = '[[:' + pg + ']] → ' + rn.map(function (n) { return '[[:' + n + ']]'; }).join(', ');
} else if (ei.type === 'merge') {
var mn = collectInputValues('.rmMergeInput');
if (!mn.length) { alert('Укажите статью для объединения.'); return false; }
tplpar = pg + '|' + mn.join('|');
extraPages = mn;
section = formatPagesWithAnd([pg].concat(mn));
} else if (ei.type === 'split') {
var sn = collectInputValues('.rmSplitInput');
if (!sn.length) { alert('Укажите статьи для разделения.'); return false; }
tplpar = formatPagesWithAnd(sn);
section = '[[:' + pg + ']] → ' + tplpar;
}
}
if (isMulti) {
var ttl = $('#rmHeader').val() || '';
var articles = collectInputValues('.rmArticleInput');
var multiFormat = $('.rmArticleMultiFormatBtn.is-active').data('rmMultiFormat') || 'sections';
multiArticleComments = collectMultiNominationComments();
multiHeaderText = ttl;
multiArticles = articles.slice();
section = ttl;
msg = multiFormat === 'list'
? buildMultiNominationListText(articles, rawMsg, multiArticleComments)
: buildMultiNominationText(articles, rawMsg, multiArticleComments);
}
if (!section) section = '[[:' + pg + ']]';
sectionNW = section.replace(/\[\[:/g, '').replace(/]]/g, '');
var nomPageDate = date[1];
var nomPage = nom.nomPage(nomPageDate);
var summary = makeSummary('номинация [[' + nomPage + '#' + sectionNW + ']]');
return {
mode: 'nominate',
opId: opId,
op: op,
date: date,
tplpar: tplpar,
articleTpl: nom.articleTpl || function () { return ''; },
inArticle: nom.inArticle !== false,
transferMode: (nom.supportsTransfer ? getTransferModeFromButtons() : 'none'),
summary: summary,
msg: msg,
nomPage: nomPage,
navTemplate: nom.navTemplate,
section: section,
sectionNW: sectionNW,
comment: nom.comment || '',
extraPages: extraPages || [],
isMulti: !!isMulti,
multiHeaderText: multiHeaderText,
multiNominationBody: rawMsg,
multiArticleComments: multiArticleComments,
multiNominationFormat: multiFormat || 'sections',
multiArticles: multiArticles,
pages: isMulti ? multiArticles.slice().reverse() : ([pg].concat(extraPages || []))
};
}
function getTransferModeFromButtons() {
var kbu = $('#rmTransferBtnKbu').hasClass('is-active');
var kul = $('#rmTransferBtnKul').hasClass('is-active');
if (kbu && kul) return 'both';
if (kbu) return 'kbu';
if (kul) return 'kul';
return 'none';
}
function buildKbuFormHtml(reasons) {
return joinHtml([
'<select id="rmSel" style="', stInputFull, '">',
reasons.map(function (r, i) { return '<option value="' + i + '">' + r[1] + '</option>'; }).join(''),
'</select>',
'<input id="fiRm" type="hidden" style="', stInputFull, '">',
'<input id="fiRmComment" type="text" placeholder="Комментарий (необязательно)" style="', stInputFull, '">',
buildQuickPhrasesPanelHtml('fiRmComment')
]);
}
function buildNominationMultiHeaderHtml(pg, options) {
var opts = options || {};
var multiHeaderRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
return joinHtml([
'<div id="rmMultiHeader" class="', RESIZE_CLASS, '" style="display:none;margin-bottom:6px;">',
'<div class="rmArticleRow rmNominationHeaderRow" style="', multiHeaderRowStyle, '"><input id="rmHeader" type="text" placeholder="Заголовок номинации" style="', stInputBox, '">',
buildAddArticleButtonHtml(opts), '</div>',
'</div>',
'<div id="rmArticlesContainer" style="display:flex;flex-direction:column;gap:', multiNominationGap, ';margin-bottom:', multiNominationGap, ';">',
buildMultiArticleRowHtml(0, $.extend({}, opts, { rowId: 'rmFirstArticle', articleValue: pg, showAdd: true })),
'</div>'
]);
}
function buildTransferBoxHtml() {
return joinHtml([
'<div id="rmTransferBox" class="', RESIZE_CLASS, ' rmTransferPanel" style="width:100%;box-sizing:border-box;"><div class="rmTransferGrid">',
'<div id="rmTransferModeSingle" class="rmSegmentedBar"><button type="button" id="rmTransferBtnNone" class="rmSegmentedBtn rmToggleBtn is-active" aria-pressed="true">Обычная номинация</button></div>',
'<div id="rmTransferModeGroup" class="rmSegmentedBar">',
'<button type="button" id="rmTransferBtnKbu" class="rmSegmentedBtn rmToggleBtn" aria-pressed="false" title="Шаблоны db-*, уд-*, КОУ, Hangon.">Снять КБУ</button>',
'<button type="button" id="rmTransferBtnKul" class="rmSegmentedBtn rmToggleBtn" aria-pressed="false" title="Шаблон «К улучшению».">Снять КУЛ</button>',
'</div>',
'<div class="rmTransferHintRow"><div id="rmTransferHint" style="display:none;font-size:12px;line-height:1.35;color:', tk.cSubM, ';"></div></div>',
'</div></div>'
]);
}
function buildMultiNominationFormatSwitchHtml(wrapId, buttonClass) {
return joinHtml([
'<div id="', wrapId, '" class="', RESIZE_CLASS, '" style="display:none;margin-top:8px;margin-bottom:10px;">',
'<div class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn ', buttonClass, ' is-active" data-rm-multi-format="sections" aria-pressed="true">Оформить подразделами</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn ', buttonClass, '" data-rm-multi-format="list" aria-pressed="false">Оформить списком</button>',
'</div>',
'</div>'
]);
}
function buildNominationFormHtml(nom, pg, multiMode) {
return joinHtml([
multiMode ? buildNominationMultiHeaderHtml(pg) : '',
nom.extraInput ? buildMultiInputHtml(nom.extraInput) : '',
'<textarea id="rmMsg" placeholder="Текст номинации без подписи"></textarea>',
buildQuickPhrasesPanelHtml('rmMsg'),
nom.supportsTransfer ? buildTransferBoxHtml() : '',
multiMode ? buildMultiNominationFormatSwitchHtml('rmArticleMultiFormatWrap', 'rmArticleMultiFormatBtn') : ''
]);
}
function buildCategoryNominationFormHtml(variantConfig, multiMode, pageName) {
return joinHtml([
multiMode ? buildNominationMultiHeaderHtml(pageName, {
inputPlaceholder: 'Категория',
addTitle: 'Добавить категорию',
removeTitle: 'Удалить категорию',
commentPlaceholder: 'Комментарий только для этой категории (необязательно)'
}) : '',
variantConfig ? buildMultiInputHtml(variantConfig) : '',
'<textarea id="nominationReason" placeholder="Текст номинации без подписи"></textarea>',
buildQuickPhrasesPanelHtml('nominationReason'),
multiMode ? buildMultiNominationFormatSwitchHtml('rmCategoryMultiFormatWrap', 'rmCategoryMultiFormatBtn') : ''
]);
}
function ensureMultiPageCommentTextareaResizer(textareaId, wrapId) {
var $textarea = $('#' + textareaId);
if (!$textarea.length || $textarea.data('rmNestedResizerReady')) return;
setupNestedResizableTextarea(textareaId, wrapId, parseInt(sz.taMinW, 10) || 180, 90);
$textarea.data('rmNestedResizerReady', true);
}
function setMultiPageCommentExpanded($btn, expanded) {
var wrapId = $btn.data('rmCommentWrap');
var textareaId = $btn.data('rmCommentTextarea');
var $wrap = $('#' + wrapId);
if (!$btn.length || !$wrap.length) return;
$btn.attr('aria-expanded', expanded ? 'true' : 'false')
.toggleClass('is-active', expanded)
.text(expanded ? 'Скрыть комментарий' : 'Комментарий');
$wrap.toggle(expanded);
if (expanded) ensureMultiPageCommentTextareaResizer(textareaId, wrapId);
}
function getMultiPageCommentTargets($block) {
var $textarea = $block.find('.rmArticleCommentInput').first();
var $wrap = $textarea.parent();
return {
wrapId: $wrap.attr('id') || '',
textareaId: $textarea.attr('id') || ''
};
}
function setMultiPageRowControls($block, showAdd, showComment, options) {
var opts = options || {};
var $row = $block.find('.rmArticleRow').first();
var ids = getMultiPageCommentTargets($block);
var $commentBtn = $row.find('.rmArticleCommentToggle');
if (!$row.length) return;
if (showAdd) {
if ($commentBtn.length) setMultiPageCommentExpanded($commentBtn, false);
if (ids.wrapId) $('#' + ids.wrapId).hide();
$row.find('.rmArticleCommentToggle,.rmRemoveInput').remove();
if (!$row.find('.rmAddArticle').length) $row.append(buildAddArticleButtonHtml(opts));
return;
}
$row.find('.rmAddArticle').remove();
if (!$row.find('.rmArticleCommentToggle').length) {
$row.append(buildMultiArticleButtonsHtml(ids.wrapId, ids.textareaId, $.extend({}, opts, { showComment: showComment })));
return;
}
$row.find('.rmArticleCommentToggle').toggle(showComment);
}
function setupMultiPageNominationUi(options) {
var opts = options || {};
var containerSelector = opts.containerSelector || '#rmArticlesContainer';
var pageCounter = parseInt(opts.nextIndex, 10) || 1;
var wasMultiModeExpanded = false;
function restoreEmptySinglePageInput() {
var $pageInput = $(containerSelector + ' .rmArticleInput').first();
if (!$pageInput.length || String($pageInput.val() || '').trim()) return;
$pageInput.val(opts.defaultPage || '');
}
function updateMultiMode() {
var $blocks = $(containerSelector + ' .rmMultiArticleBlock');
var hasExtra = $blocks.length > 1;
$('#rmMultiHeader').toggle(hasExtra);
if (opts.multiOnlySelector) $(opts.multiOnlySelector).toggle(hasExtra);
if (!hasExtra && wasMultiModeExpanded) restoreEmptySinglePageInput();
$blocks.each(function (index) {
setMultiPageRowControls($(this), !hasExtra && index === 0, hasExtra, opts);
});
wasMultiModeExpanded = hasExtra;
syncModalLayout();
}
$(document).off('click.rmMultiPageAdd').on('click.rmMultiPageAdd', '.rmAddArticle', function () {
$(containerSelector).append(buildMultiArticleRowHtml(pageCounter++, opts));
updateMultiMode();
});
$(document).off('click.rmMultiPageComment').on('click.rmMultiPageComment', '.rmArticleCommentToggle', function () {
var $btn = $(this);
if (!$btn.is(':visible')) return;
setMultiPageCommentExpanded($btn, $btn.attr('aria-expanded') !== 'true');
syncModalLayout();
});
$(document).off('click.rmMultiPageRemove').on('click.rmMultiPageRemove', '.rmArticleRow .rmRemoveInput', function () {
$(this).closest('.rmMultiArticleBlock').remove();
updateMultiMode();
});
updateMultiMode();
return {
update: updateMultiMode,
isMulti: function () { return $(containerSelector + ' .rmMultiArticleBlock').length > 1; }
};
}
function bindMultiNominationFormatSwitch(rootSelector, buttonSelector) {
$(rootSelector).off('click.rmMultiFormat', buttonSelector).on('click.rmMultiFormat', buttonSelector, function () {
var $btn = $(this);
$(buttonSelector).removeClass('is-active').attr('aria-pressed', 'false');
$btn.addClass('is-active').attr('aria-pressed', 'true');
});
}
function buildProtectAddButtonHtml() {
return buildSquareAddButtonHtml('', 'Добавить страницу', 'rmProtectAddPage');
}
function buildProtectPageRowHtml(id, pageName, isFirstRow) {
return joinHtml([
'<div', isFirstRow ? ' id="rmProtectFirstRow"' : '', ' class="rmProtectPageRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="', id, '" type="text" placeholder="Страница" class="rmProtectPageInput" style="', stInputBox, '"',
pageName ? ' value="' + escapeHtml(pageName) + '"' : '', '>',
isFirstRow
? buildProtectAddButtonHtml()
: '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>',
'</div>'
]);
}
function buildReportFormHtml(ctx, isZka) {
var reportTextPlaceholder = (isZka ? 'Введите текст запроса.' : 'Текст запроса (можно дополнить или изменить).') + ' Подпись будет добавлена автоматически.';
if (isZka) {
return joinHtml([
'<input id="rmReportHeader" type="text" placeholder="Тема/заголовок" style="', stInputFull, '" value="', escapeHtml(ctx.pageLink), '">',
'<textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>',
buildQuickPhrasesPanelHtml('rmReportText')
]);
}
return joinHtml([
'<div id="rmProtectModeBtns" class="', RESIZE_CLASS, '" style="margin-bottom:14px;"><div class="rmSegmentedBar">',
'<button id="rmProtectModeInstall" type="button" class="rmSegmentedBtn rmProtectModeBtn is-active" aria-pressed="true">🛡️ Установить защиту</button>',
'<button id="rmProtectModeRemove" type="button" class="rmSegmentedBtn rmProtectModeBtn" aria-pressed="false">📛 Снять защиту</button>',
'</div></div>',
'<div id="rmProtectMultiWrap" class="', RESIZE_CLASS, '">',
'<div id="rmProtectHeaderWrap" style="display:none;margin-bottom:6px;"><div class="rmProtectHeaderRow" style="', stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'), '"><input id="rmProtectHeader" type="text" placeholder="Заголовок (для нескольких страниц)" style="', stInputBox, '">', buildProtectAddButtonHtml(), '</div></div>',
buildProtectPageRowHtml('rmProtectPage0', ctx.pageName, true),
'<div id="rmProtectPagesContainer"></div>',
'</div>',
'<div id="rmProtectLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmProtectLevels" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>',
'</div></div>',
'<div id="rmProtectReasonsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Причины</div><div id="rmProtectReasons" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="война правок">война правок</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="неконсенсусные изменения">неконсенсусные изменения</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="вандализм">вандализм</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="популярная статья">популярная статья</button>',
'</div></div>',
'<div id="rmRemoveLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup" style="display:none;"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmRemoveLevels" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>',
'</div></div>',
'<div id="rmProtectTextBlock" class="', RESIZE_CLASS, '"><textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>',
buildQuickPhrasesPanelHtml('rmReportText'), '</div>'
]);
}
// ═══════════════════════════════════════════════════════════════════════════
// ОБРАБОТЧИКИ ОПЕРАЦИЙ
// ═══════════════════════════════════════════════════════════════════════════
var handlers = {
// ── КБУ ─────────────────────────────────────────────────────────────
showKbu: function (op) {
var forCategory = !!(op && op.forCategory);
var reasons = getFastRemoveReasons();
createModal({
title: 'Быстрое удаление',
width: 'compact',
subtitleHtml: '<span id="rmKbuCriteriaLinkWrap"></span>'
});
$('#removerModalContent').html(buildKbuFormHtml(reasons));
function updateKbuReasonControls() {
var reason = reasons[$('#rmSel').val()] || reasons[0];
var paramCfg = reason ? cfg.requiredParamTemplates[reason[0]] : null;
var showComment = true;
$('#rmKbuCriteriaLinkWrap').html(buildFastRemoveCriteriaLinkHtml(reason));
if (paramCfg) {
var noComment = paramCfg.charAt(0) === '!';
$('#fiRm').attr({ type: 'text', placeholder: 'Укажите ' + (noComment ? paramCfg.substring(1) : paramCfg) }).show();
showComment = !noComment;
} else {
$('#fiRm').attr('type', 'hidden').hide();
}
$('#fiRmComment').toggle(showComment);
$('.rmQuickPhrasesPanel[data-rm-target="fiRmComment"]').toggle(showComment);
}
$('#rmSel').change(updateKbuReasonControls);
$('#rmSel').trigger('change');
renderModalFooter('submit', {
submitText: 'Номинировать',
onSubmit: function () {
var idx = $('#rmSel').val();
var addInfo = $('#fiRm').val();
var comment = $('#fiRmComment').val();
startProcessing();
if (forCategory) {
var tpl = reasons[idx][0];
var categorySummary = makeSummary('номинация категории на быстрое удаление');
if (addInfo) tpl += '|1=' + addInfo;
if (comment) tpl += '|' + (addInfo ? '2' : '1') + '=' + comment;
editPageContent(mwCfg.wgPageName, { summary: categorySummary, readError: 'Не удалось получить содержимое.' },
function (text) { return { text: wrapInNoinclude(text, T_OPEN + tpl + T_CLOSE) }; },
function (err) {
if (err) {
unlockModalSubmit();
logStatus('Ошибка записи.', err);
} else {
logStatus('Страница номинирована к быстрому удалению.', null, { trackError: false });
finalizeFastRemoval([normTitle(mwCfg.wgPageName)], categorySummary);
}
});
} else {
var job = {
mode: 'nominate', opId: 'fRm',
kbuTemplate: reasons[idx][0], kbuAddInfo: addInfo, kbuComment: comment,
summary: makeSummary('номинация к [[ВП:КБУ|быстрому удалению]]'),
inArticle: true
};
processPageList([normTitle(mwCfg.wgPageName)], job, function (notifiedPages) {
finalizeFastRemoval(notifiedPages, job.summary);
});
}
return true;
}
});
},
// ── Универсальная номинация (КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС) ────────
showNomination: function (op) {
var nom = op.nomination;
var pg = normTitle(mwCfg.wgPageName);
var date = getDate()[1];
var nomPage = nom.nomPage(date);
var multiMode = nom.supportsMulti;
function updateTransferUi() {
var mode = getTransferModeFromButtons();
var isNone = mode === 'none';
var isKbu = mode === 'kbu' || mode === 'both';
var isKul = mode === 'kul' || mode === 'both';
$('#rmTransferBtnNone').toggleClass('is-active', isNone).attr('aria-pressed', isNone ? 'true' : 'false');
$('#rmTransferBtnKbu').toggleClass('is-active', isKbu).attr('aria-pressed', isKbu ? 'true' : 'false');
$('#rmTransferBtnKul').toggleClass('is-active', isKul).attr('aria-pressed', isKul ? 'true' : 'false');
var t = transferTexts[mode];
if (t) { $('#rmTransferHint').text(t.hint).show(); } else { $('#rmTransferHint').hide().text(''); }
applyGeneratedText($('#rmMsg'), t && t.notice ? t.notice + '\n' : '');
}
createModal({
title: 'Номинация: ' + nom.template,
subtitlePage: nomPage,
subtitleLabel: 'Текущий день'
});
$('#removerModalContent').html(buildNominationFormHtml(nom, pg, multiMode));
setupResizableModal('rmMsg');
// Логика переноса
if (nom.supportsTransfer) {
$(document).off('click.rmTransfer').on('click.rmTransfer', '#rmTransferBtnNone,#rmTransferBtnKbu,#rmTransferBtnKul', function () {
if (this.id === 'rmTransferBtnNone') {
$('#rmTransferBtnKbu,#rmTransferBtnKul').removeClass('is-active');
$('#rmTransferBtnNone').addClass('is-active');
} else {
$(this).toggleClass('is-active');
var anyOn = $('#rmTransferBtnKbu').hasClass('is-active') || $('#rmTransferBtnKul').hasClass('is-active');
$('#rmTransferBtnNone').toggleClass('is-active', !anyOn);
}
updateTransferUi();
});
updateTransferUi();
}
// Многостраничный режим
if (multiMode) {
setupMultiPageNominationUi({ defaultPage: pg, multiOnlySelector: '#rmArticleMultiFormatWrap' });
bindMultiNominationFormatSwitch('#removerModalContent', '.rmArticleMultiFormatBtn');
}
if (nom.extraInput) wireMultiInput(nom.extraInput);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var isMulti = multiMode && $('#rmArticlesContainer .rmMultiArticleBlock').length > 1;
var inputVal = !isMulti ? normTitle($('#rmArticlesContainer .rmArticleInput').first().val() || '') : '';
var changed = inputVal && inputVal !== pg;
function executeJob(job) {
startProcessing();
runFlow({
templateStep: function (next) {
if (!job.inArticle) { next(); return; }
processPageList(job.pages, job, function (notifiedPages, err) {
job._notifiedPages = notifiedPages;
next(err);
});
},
nominationStep: function (done) {
publishNomination({
pageTitle: job.nomPage,
navTemplate: job.navTemplate,
sectionTitle: job.section,
summary: job.summary,
text: getNominationPublishText(job)
}, function (err) { done(err, { pageTitle: job.nomPage, sectionTitle: job.section }); });
},
notifyStep: function (nominationInfo, next) {
var pages = job._notifiedPages || [];
if (!setAlert || !pages.length) { next(); return; }
notifyAuthorsForPages(pages, {
summary: job.summary,
actionText: job.comment,
discussionPage: nominationInfo && nominationInfo.pageTitle,
discussionSection: nominationInfo && nominationInfo.sectionTitle
}, next);
},
skipLink: op.id === 'fRm'
});
}
function run(targetPg) {
var job = buildNominationJob(op, targetPg, isMulti);
if (!job) { unlockModalSubmit(); return; }
if (job.isMulti && job.inArticle && getNominationConflictRule(job)) {
startProcessing();
inspectMultiNominationConflicts(job, function (err, conflicts) {
if (err) { markSubmitError(); return; }
if (!conflicts.length) { executeJob(job); return; }
showNominationConflictResolution(job, conflicts, function (resolvedJob) {
executeJob(resolvedJob);
});
});
return;
}
executeJob(job);
}
if (changed) {
apiReq({ prop: 'info', titles: inputVal }, 'query', function (data) {
if (data && data.error) {
unlockModalSubmit();
alert('Не удалось проверить страницу из-за ошибки API. Попробуйте ещё раз.');
return;
}
var page = getFirstQueryPage(data);
if (!page) {
unlockModalSubmit();
alert('Не удалось проверить страницу из-за временной ошибки. Попробуйте ещё раз.');
return;
}
if (page.missing !== undefined) {
unlockModalSubmit();
alert('Страница «' + inputVal + '» не существует.');
return;
}
run(normTitle(page.title || inputVal));
});
} else {
run(pg);
}
return true;
}
});
},
// ── Снятие номинации (статья) ────────────────────────────────────────
showArticleClose: function () {
showCloseActionsModal({
inputName: 'rmCloseAction',
listId: 'rmCloseActions',
emptyText: 'Не найдено подходящих шаблонов для закрытия.',
emptyDetails: 'Проверяются: КУ, КПМ, КБУ, КУЛ.',
getActions: function (articleText) {
var actions = [];
if (RE_KU_ON_PAGE.test(articleText)) actions.push({ id: 'ret', tag: 'КУ', label: 'Оставлено', mode: 'denom', closeType: 'ret', resultTemplate: 'оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{оставлено}} на СО.', comment: 'оставлена', talkNotice: true });
if (RE_KU_ON_PAGE.test(articleText)) actions.push({ id: 'retConditional', tag: 'КУ', label: 'Условно оставлено', mode: 'denom', closeType: 'retConditional', resultTemplate: 'условно оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{условно оставлено}} на СО.', comment: 'условно оставлена', talkNotice: true, needsConditionalFields: true });
if (RE_KPM_ON_PAGE.test(articleText)) actions.push({ id: 'doneRnm', tag: 'КПМ', label: 'Переименовано', mode: 'denom', closeType: 'doneRnm', resultTemplate: 'переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{переименовано}} на СО.', comment: 'переименована', talkNotice: true, needsOldTitle: true });
if (RE_KPM_ON_PAGE.test(articleText)) actions.push({ id: 'noRnm', tag: 'КПМ', label: 'Не переименовано', mode: 'denom', closeType: 'noRnm', resultTemplate: 'не переименовано',sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{не переименовано}} на СО.', comment: 'не переименована',talkNotice: true });
var hasKbu = RE_KBU_ON_PAGE.test(articleText);
var hasKul = RE_KUL_ON_PAGE.test(articleText);
if (hasKbu && hasKul) actions.push({ id: 'cleanup-both', tag: 'КБУ и КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'both', cleanupLabel: 'КБУ и КУЛ', description: 'Снимает шаблоны КБУ/КУЛ и Hangon.' });
if (hasKbu) actions.push({ id: 'cleanup-kbu', tag: 'КБУ', label: 'Снятие', mode: 'cleanup', transferMode: 'kbu', cleanupLabel: 'КБУ', description: 'Снимает шаблоны КБУ и Hangon.' });
if (hasKul) actions.push({ id: 'cleanup-kul', tag: 'КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'kul', cleanupLabel: 'КУЛ', description: 'Снимает шаблон КУЛ.' });
return actions;
},
afterRender: function (actions) {
var hasDoneRnm = actions.some(function (a) { return a.id === 'doneRnm'; });
var hasConditionalRet = actions.some(function (a) { return a.id === 'retConditional'; });
if (hasDoneRnm) {
$('#rmCloseActions input[value="doneRnm"]').closest('.rmActionItem').append(
'<div id="rmCloseOldTitleWrap" style="display:none;margin-top:6px;"><input id="rmCloseOldTitle" type="text" placeholder="Старое название" style="' + stInputFull + '"></div>'
);
}
if (hasConditionalRet) {
$('#rmCloseActions input[value="retConditional"]').closest('.rmActionItem').append(buildConditionalRetFieldsHtml());
}
},
afterFooterRender: function (_, actionMap) {
function ensureConditionalTextareaResizer() {
var $textarea = $('#rmCloseConditionalReason');
if (!$textarea.length || $textarea.data('rmConditionalResizerReady')) return;
setupNestedResizableTextarea('rmCloseConditionalReason', 'rmCloseConditionalWrap', 280, 90);
$textarea.data('rmConditionalResizerReady', true);
}
function updateUi() {
var sel = actionMap[$('[name="rmCloseAction"]:checked').val()];
$('#rmCloseOldTitleWrap').toggle(!!(sel && sel.needsOldTitle));
$('#rmCloseConditionalWrap').toggle(!!(sel && sel.needsConditionalFields));
if (sel && sel.needsConditionalFields) ensureConditionalTextareaResizer();
var disableNotify = !!(sel && sel.mode === 'cleanup' && sel.transferMode === 'kbu');
var $cb = $('[name="rmUAlert"]');
var $cbLabel = $('[name="rmUAlert"]').closest('label');
if ($cb.length) $cb.prop('disabled', disableNotify);
if ($cbLabel.length) $cbLabel.css({
visibility: disableNotify ? 'hidden' : 'visible',
pointerEvents: disableNotify ? 'none' : ''
});
syncModalLayout();
}
$(document).off('change.rmCloseAction').on('change.rmCloseAction', '[name="rmCloseAction"]', updateUi);
updateUi();
},
onSubmit: function (sel, pageName) {
var job;
if (sel.mode === 'denom') {
var oldTitle = sel.needsOldTitle ? ($('#rmCloseOldTitle').val() || '').trim() : '';
var conditionalReason = sel.needsConditionalFields ? normalizeQuickPhraseValue($('#rmCloseConditionalReason').val()) : '';
var conditionalDeadline = sel.needsConditionalFields ? String($('#rmCloseConditionalDeadline').val() || '').trim() : '';
if (sel.needsOldTitle && !oldTitle) { alert('Укажите старое название.'); return false; }
if (conditionalDeadline && normalizeIsoDate(conditionalDeadline) !== conditionalDeadline) {
alert('Укажите срок в формате YYYY-MM-DD, например 2026-05-31.');
return false;
}
job = {
mode: 'denom',
closeType: sel.closeType,
resultTemplate: sel.resultTemplate,
sourceTemplate: sel.sourceTemplate,
oldTitle: oldTitle,
conditionalReason: conditionalReason,
conditionalDeadline: conditionalDeadline,
notifyActionText: sel.comment,
skipNotify: false
};
} else {
job = {
mode: 'cleanup',
transferMode: sel.transferMode,
summary: makeSummary('снятие шаблонов ' + sel.cleanupLabel),
notifyActionText: (sel.transferMode === 'kul' || sel.transferMode === 'both')
? 'больше не номинирована к срочному улучшению'
: '',
skipNotify: !(sel.transferMode === 'kul' || sel.transferMode === 'both')
};
}
processPageList([pageName], job, function (notifiedPages, err, pageMeta) {
function finishClose() {
if (isError) { markSubmitError(); }
else { renderModalFooter('reload'); }
}
if (isError || err || job.skipNotify || !setAlert || !notifiedPages.length) { finishClose(); return; }
var meta = (pageMeta && pageMeta[normTitle(pageName)]) || {};
notifyAuthorsForPages(notifiedPages, {
summary: meta.summary || job.summary,
actionText: job.notifyActionText,
discussionPage: meta.discussionPage,
discussionSection: meta.discussionSection,
includeProposedPrefix: false
}, finishClose);
});
return true;
}
});
},
// ── ОБКАТ: номинация категории ───────────────────────────────────────
showCatNomination: function (op) {
var catType = op.catType;
var titles = { discuss: 'Номинация: обсуждение', deletion: 'Номинация: к удалению', rename: 'Номинация: к переименованию', merge: 'Номинация: к объединению' };
var now = new Date();
var catDiscPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[now.getUTCMonth()] + '_' + now.getUTCFullYear();
var pageName = normalizeCategoryPageName(mwCfg.wgPageName);
var multiMode = catType === 'deletion';
createModal({ title: titles[catType], subtitlePage: catDiscPage, subtitleLabel: 'Текущий месяц' });
var variantCfgs = {
rename: { firstId: 'firstRenameInput', addBtnId: 'addRenameVariant', containerId: 'renameVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Новое название без префикса Категория:', addPh: 'Дополнительный вариант названия' },
merge: { firstId: 'firstMergeInput', addBtnId: 'addMergeVariant', containerId: 'mergeVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Категория для объединения без префикса Категория:', addPh: 'Дополнительный вариант объединения' }
};
var vCfg = variantCfgs[catType];
$('#removerModalContent').html(buildCategoryNominationFormHtml(vCfg, multiMode, pageName));
setupResizableModal('nominationReason');
if (multiMode) {
setupMultiPageNominationUi({
defaultPage: pageName,
inputPlaceholder: 'Категория',
addTitle: 'Добавить категорию',
removeTitle: 'Удалить категорию',
commentPlaceholder: 'Комментарий только для этой категории (необязательно)',
multiOnlySelector: '#rmCategoryMultiFormatWrap'
});
bindMultiNominationFormatSwitch('#removerModalContent', '.rmCategoryMultiFormatBtn');
}
if (vCfg) wireMultiInput(vCfg);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var reason = normalizeQuickPhraseValue($('#nominationReason').val());
var targetPages = multiMode ? collectCategoryPageInputValues('.rmArticleInput') : [pageName];
var isMulti = multiMode && targetPages.length > 1;
var multiFormat = $('.rmCategoryMultiFormatBtn.is-active').data('rmMultiFormat') || 'sections';
var commentsByCategory = isMulti ? collectMultiNominationComments(normalizeCategoryPageName) : {};
var discussionTarget = isMulti ? targetPages : targetPages[0];
var discussionReason = isMulti
? (multiFormat === 'list'
? buildMultiNominationListText(targetPages, reason, commentsByCategory)
: buildMultiNominationText(targetPages, reason, commentsByCategory, { headingLevel: 4 }))
: reason;
var discussionOptions = isMulti ? { headerText: $('#rmHeader').val(), reasonIsPrepared: true } : null;
var notifiedPages = [];
if (!reason) { alert('Пожалуйста, укажите причину/тему.'); return false; }
if (!targetPages.length) { alert('Укажите категорию.'); return false; }
var mainName = null, additionalNames = [];
if (vCfg) {
mainName = $('#' + vCfg.firstId).val().trim();
if (!mainName) { alert('Укажите ' + (catType === 'rename' ? 'новое название' : 'категорию для объединения') + '.'); return false; }
additionalNames = collectInputValues('#' + vCfg.containerId + ' .variantInput');
}
startProcessing();
runFlow({
templateStep: function (next) {
addTemplatesToCategories(targetPages, catType, mainName, additionalNames, function (err, processedPages) {
notifiedPages = processedPages || [];
next(err);
});
},
nominationStep: function (done) {
createCategoryDiscussion(discussionTarget, discussionReason, catType, function (err, nominationInfo) { done(err, nominationInfo || null); }, mainName, additionalNames, discussionOptions);
},
notifyStep: function (nominationInfo, next) {
if (!setAlert || !nominationInfo) { next(); return; }
var section = normalizeSectionForLink(nominationInfo.sectionTitle || '');
notifyAuthorsForPages(notifiedPages.length ? notifiedPages : targetPages, {
summary: makeSummary('номинация [[' + nominationInfo.pageTitle + (section ? '#' + section : '') + ']]'),
actionText: { discuss: 'к обсуждению', deletion: 'к удалению', rename: 'к переименованию', merge: 'к объединению' }[catType] || 'к обсуждению',
discussionPage: nominationInfo.pageTitle,
discussionSection: nominationInfo.sectionTitle
}, next);
}
});
return true;
}
});
},
// ── Снятие номинации (категория) ─────────────────────────────────────
showCatClose: function () {
showCloseActionsModal({
inputName: 'rmCategoryCloseAction',
showCheckbox: false,
emptyText: 'Не найдено подходящих шаблонов для завершения.',
emptyDetails: 'Проверяются ОБКАТ и КУ.',
getActions: function (catText) {
var allObkat = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|');
var actions = [];
if (new RegExp('\\{\\{\\s*(?:' + allObkat + ')\\s*(?:\\||\\}\\})', 'i').test(catText)) {
actions.push({ id: 'cat-obkat-done', tag: 'ОБКАТ', label: 'Завершено', mode: 'obkat', talkTemplate: 'Обсуждавшаяся категория', description: 'Снимает шаблон ОБКАТ, добавляет {{Обсуждавшаяся категория}} на СО.', talkNotice: true });
}
if (RE_KU_ON_PAGE.test(catText)) {
actions.push({ id: 'cat-ku-cleanup', tag: 'КУ', label: 'Снятие', mode: 'cleanup', description: 'Снимает шаблон КУ без записи на СО.' });
}
return actions;
},
onSubmit: function (sel, pageName) {
if (sel.mode === 'obkat') markCategoryDiscussionAsDone(pageName);
if (sel.mode === 'cleanup') removeKuFromCategory(pageName);
return true;
}
});
},
// ── Защита / Запрос к администраторам ───────────────────────────────
showReport: function (op) {
var mode = op.reportMode || 'protect';
var ctx = getReporterContext(mode);
var isZka = mode === 'request';
var protectMode = 'install';
var pageCounter = 1;
function buildProtectText(pm) {
if (pm === 'remove') {
var removeLevels = [];
$('#rmRemoveLevels .rmToggleBtn.is-active').each(function () { removeLevels.push($(this).data('label')); });
return removeLevels.length ? 'Просьба снять ' + removeLevels.join(' и/или ') + '.' : '';
}
var levels = [], reasons = [];
$('#rmProtectLevels .rmToggleBtn.is-active').each(function () { levels.push($(this).data('label')); });
$('#rmProtectReasons .rmToggleBtn.is-active').each(function () { reasons.push($(this).data('label')); });
if (!levels.length && !reasons.length) return '';
var text = 'Просьба установить';
if (levels.length) text += ' ' + levels.join(' и/или ');
if (reasons.length) text += ' по причине: ' + reasons.join(', ');
return text + '.';
}
function applyProtectMode(m) {
protectMode = m;
var isInstall = m === 'install';
$('#rmProtectModeInstall').toggleClass('is-active', isInstall).attr('aria-pressed', isInstall ? 'true' : 'false');
$('#rmProtectModeRemove').toggleClass('is-active', !isInstall).attr('aria-pressed', !isInstall ? 'true' : 'false');
$('#removerModalTitleText').text(isInstall ? 'Запрос на защиту страницы' : 'Запрос на снятие защиты');
var linkPage = isInstall ? 'Википедия:Установка защиты' : 'Википедия:Снятие защиты';
$('#rmProtectLinkWrap').html('<a href="' + getPageUrl(linkPage) + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + escapeHtml(linkPage) + '</a>');
$('#rmProtectLevelsWrap,#rmProtectReasonsWrap').toggle(isInstall);
$('#rmRemoveLevelsWrap').toggle(!isInstall);
$('#rmProtectLevels .rmProtectOptBtn,#rmProtectReasons .rmProtectOptBtn,#rmRemoveLevels .rmProtectOptBtn').removeClass('is-active');
$('#rmReportText').val('').removeData('rmGenerated');
}
function updateProtectMultiUi() {
var $rows = $('#rmProtectMultiWrap .rmProtectPageRow');
var hasExtra = $rows.length > 1;
if (!$rows.length) {
$('#rmProtectPagesContainer').append(buildProtectPageRowHtml('rmProtectPage' + pageCounter++, ctx.pageName, true));
$rows = $('#rmProtectMultiWrap .rmProtectPageRow');
hasExtra = false;
}
$('#rmProtectHeaderWrap').toggle(hasExtra);
if (!hasExtra) $('#rmProtectHeader').val('');
$rows.each(function () {
var $row = $(this);
$row.find('.rmProtectAddPage,.rmRemoveInput').remove();
$row.append(hasExtra
? '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>'
: buildProtectAddButtonHtml()
);
});
syncModalLayout();
}
createModal({
title: isZka ? 'Запрос к администраторам' : 'Запрос на защиту страницы',
width: 'compact',
subtitleHtml: isZka
? '<a href="' + getPageUrl('Википедия:Запросы к администраторам') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Запросы к администраторам</a>' +
' · <a href="' + getPageUrl('Википедия:Запросы к администраторам/Быстрые') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Быстрые</a>'
: '<span id="rmProtectLinkWrap"></span>'
});
$('#removerModalContent').html(buildReportFormHtml(ctx, isZka));
if (!isZka) {
$('#removerModalContent')
.on('click', '#rmProtectModeInstall', function () { applyProtectMode('install'); })
.on('click', '#rmProtectModeRemove', function () { applyProtectMode('remove'); })
.on('click', '.rmProtectOptBtn', function () {
$(this).toggleClass('is-active');
if (protectMode === 'install') {
var $levels = $('#rmProtectLevels .rmProtectOptBtn.is-active');
if ($(this).closest('#rmProtectReasons').length && $(this).hasClass('is-active') && $levels.length === 0) {
$('#rmProtectLevels .rmProtectOptBtn').first().addClass('is-active');
}
if ($(this).closest('#rmProtectLevels').length && !$(this).hasClass('is-active') && $levels.length === 0) {
$('#rmProtectReasons .rmProtectOptBtn').removeClass('is-active');
}
}
applyGeneratedText($('#rmReportText'), buildProtectText(protectMode));
});
$(document)
.off('click.rmProtectAdd').on('click.rmProtectAdd', '.rmProtectAddPage', function () {
var id = 'rmProtectPage' + pageCounter++;
$('#rmProtectPagesContainer').append(buildProtectPageRowHtml(id, '', false));
updateProtectMultiUi();
})
.off('click.rmProtectRemove').on('click.rmProtectRemove', '.rmProtectPageRow .rmRemoveInput', function () {
$(this).closest('.rmProtectPageRow').remove(); updateProtectMultiUi();
});
applyProtectMode('install');
}
setupResizableModal('rmReportText');
renderModalFooter('submit', {
showCheckbox: false,
showSubscribe: true,
submitText: 'Отправить',
onSubmit: function () { doReport(ctx, false, protectMode); return true; }
});
if (isZka) {
$('<button id="rmReportFast" style="' + stCancel + '">Быстрый запрос</button>').insertBefore('#removerSubmit');
$('#rmReportFast').click(function () {
if ($('#removerSubmit').data('rmSubmitInProgress')) return;
$('#removerSubmit').data('rmSubmitInProgress', true).prop('disabled', true);
$('#rmReportFast').prop('disabled', true);
doReport(ctx, true, protectMode);
});
}
}
};
// ═══════════════════════════════════════════════════════════════════════════
// ВСПОМОГАТЕЛЬНЫЕ: КБУ, закрытие, категории
// ═══════════════════════════════════════════════════════════════════════════
function getFastRemoveCriteriaAnchorFromConfig(templateName) {
var anchors = cfg.fastRemoveCriteriaAnchors || {};
var template = String(templateName || '').trim();
var lower = template.toLowerCase();
var key;
if (!template) return '';
if (Object.prototype.hasOwnProperty.call(anchors, template)) return anchors[template];
for (key in anchors) {
if (Object.prototype.hasOwnProperty.call(anchors, key) && String(key).toLowerCase() === lower) {
return anchors[key];
}
}
return '';
}
function getFastRemoveCriteriaAnchor(reason) {
var configured = reason ? getFastRemoveCriteriaAnchorFromConfig(reason[0]) : '';
var template, label, m;
if (configured) return configured;
template = String(reason && reason[0] || '');
label = String(reason && reason[1] || '');
if (/^(?:подст\s*:\s*)?(?:deleteslow|ds)$/i.test(template) || /^\s*ds\b/i.test(label)) return 'С1';
m = label.match(/^\s*([А-ЯЁA-Z]{1,3}\d+(?:\.\d+)?)/);
return m ? m[1] : '';
}
function buildFastRemoveCriteriaLinkHtml(reason) {
var anchor = getFastRemoveCriteriaAnchor(reason);
var label = KBU_CRITERIA_PAGE + (anchor ? '#' + anchor : '');
var url = getPageUrlWithFragment(KBU_CRITERIA_PAGE, anchor);
return '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(label) + '</a>';
}
function getFastRemoveReasons() {
var reasons = cfg.fastRemoveReasons;
var prefix = (mwCfg.wgIsRedirect ? 'ОП' : 'О') + ({ 0: 'С', 2: 'У', 3: 'У', 6: 'Ф', 14: 'К' }[mwCfg.wgNamespaceNumber] || '');
var all = [];
if (isCategory && reasons.categories) all = all.concat(reasons.categories);
['general','articles','redirects','files','users','special'].forEach(function (k) { if (reasons[k]) all = all.concat(reasons[k]); });
if (!isCategory && reasons.categories) all = all.concat(reasons.categories);
return all.filter(function (r) { return prefix.indexOf(r[2] !== undefined ? r[2] : r[1].charAt(0)) >= 0; });
}
function showCloseActionsModal(opts) {
createModal({ title: 'Снятие шаблонов номинаций', inline: true });
$('#removerModalContent').html('<p style="margin:0;">Определение доступных действий...</p>');
var pageName = normTitle(mwCfg.wgPageName);
getText(pageName, function (pageText, readErr) {
if (readErr) { showInfoAndClose('Не удалось прочитать содержимое страницы.', readErr.info || readErr.code || '', true); return; }
if (pageText === null) { showInfoAndClose('Не удалось прочитать содержимое страницы.', '', true); return; }
var actions = opts.getActions(pageText);
if (!actions.length) { showInfoAndClose(opts.emptyText, opts.emptyDetails || ''); return; }
var actionMap = actions.reduce(function (m, a) { m[a.id] = a; return m; }, {});
$('#removerModalContent').html(buildActionsHtml(actions, opts.inputName, opts.listId));
if (opts.afterRender) opts.afterRender(actions, actionMap, pageName, pageText);
renderModalFooter('submit', {
showCheckbox: opts.showCheckbox,
submitText: 'Выполнить',
onSubmit: function () {
var sel = getSelectedAction(opts.inputName, actionMap);
if (!sel) return false;
startProcessing();
return opts.onSubmit(sel, pageName, pageText, actionMap) !== false;
}
});
if (opts.afterFooterRender) opts.afterFooterRender(actions, actionMap, pageName, pageText);
});
}
function runPageEditWithStatus(opts) {
var o = opts || {};
var statusId = logStatus(o.pendingText, null, { pending: true, trackError: false });
editPageContent(o.pageName, o.editOptions, o.buildFn, function (err, meta) {
if (err) { logStatus(o.errorText, err, { statusId: statusId }); unlockModalSubmit(); return; }
logStatus(o.successText, null, { statusId: statusId, trackError: false });
if (o.onSuccess) o.onSuccess(meta || null);
});
}
function removeKuFromCategory(pageName) {
runPageEditWithStatus({
pageName: pageName,
pendingText: 'Снимается шаблон КУ...',
errorText: 'Снятие шаблона КУ.',
successText: 'Шаблон КУ снят.',
editOptions: { summary: makeSummary('снятие шаблона КУ'), watchlist: 'nochange', readError: 'Страница не существует.', readErrorCode: 'read_failed' },
buildFn: function (text) {
var r = stripTemplatesByPattern(text, '(?:к\\s*удалению|ку)');
if (!r.removed) return { error: { code: 'no_changes', info: 'Шаблон КУ не найден.' } };
return { text: r.text.replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n') };
},
onSuccess: function () {
logStatus('Шаблон на СО не устанавливался.', null, { trackError: false });
renderModalFooter('reload');
}
});
}
// ── Категории: добавление шаблона ────────────────────────────────────────
function addTemplateToCategory(pageName, type, mainName, additionalNames, callback) {
var cb = callback || function () {};
var cfgByType = {
discuss: { action: 'обсуждение', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss },
deletion: { action: 'удаление', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss },
rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true, aliases: cfg.categoryTemplates.rename },
merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true, aliases: cfg.categoryTemplates.merge }
};
var typeCfg = cfgByType[type];
if (!typeCfg) { cb({ code: 'error', info: 'Неизвестный тип номинации.' }); return; }
var dateStr = getDate()[0];
var parts = [dateStr];
if (typeCfg.needsMain) {
if (!mainName) { cb({ code: 'error', info: 'Не указано основное название.' }); return; }
parts.push(mainName);
if (additionalNames && additionalNames.length) Array.prototype.push.apply(parts, additionalNames);
}
var tplText = T_OPEN + typeCfg.template + '|' + parts.join('|') + T_CLOSE;
editPageContent(pageName, { summary: makeSummary('добавление шаблона номинации на ' + typeCfg.action), readError: 'Не удалось получить содержимое.' },
function (text) {
if (hasTemplateWithDateByPattern(text, typeCfg.aliases, dateStr)) {
return { skip: true, meta: { status: 'already_present' } };
}
return { text: wrapInNoinclude(text, tplText) };
},
function (err, meta) {
if (!err && meta && meta.status === 'already_present') {
logStatus('На странице ' + buildQuotedStatusPageLink(pageName) + ' уже есть шаблон номинации с датой ' + dateStr + '.', null, { trackError: false });
} else {
logPageEdit(pageName, err);
}
if (err) { cb(err); return; }
if (type === 'merge') {
addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, function () { cb(null); });
return;
}
cb(null);
}
);
}
function collectCategoryPageInputValues(selector) {
var pages = [];
$(selector).each(function () {
var title = normalizeCategoryPageName($(this).val() || '');
if (title && pages.indexOf(title) === -1) pages.push(title);
});
return pages;
}
function addTemplatesToCategories(pages, type, mainName, additionalNames, callback) {
var cb = callback || function () {};
var processedPages = [];
eachSequential(pages || [], function (pageName, next) {
addTemplateToCategory(pageName, type, mainName, additionalNames, function (err) {
if (!err) processedPages.push(pageName);
next(err || null);
});
}, function (err) { cb(err, processedPages); });
}
function addMergeTemplatesToTargets(sourcePage, mainName, additionalNames, dateStr, callback) {
var cb = callback || function () {};
var currentCatName = normTitle(stripCatPrefix(sourcePage));
var targets = [mainName].concat(additionalNames || []);
if (!targets.length) { cb(); return; }
eachSequential(targets, function (target, next) {
var targetPage = 'Категория:' + target;
addMergeTemplateToTargetCategory(targetPage, currentCatName, dateStr, function (success, status) {
var url = getPageUrl(targetPage);
var linkHtml = '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(targetPage) + '</a>';
if (success) {
var extra = (status === 'already_exists' || status === 'updated') ? ' (' + formatMergeStatus(status) + ')' : '';
logStatus('Шаблон добавлен в ' + linkHtml + extra + '.', null, { trackError: false });
} else {
logStatus('Ошибка при добавлении шаблона в ' + linkHtml + '.', { code: 'merge_target_failed', info: status }, { trackError: false });
}
next();
});
}, cb);
}
function addMergeTemplateToTargetCategory(targetPageName, sourceCatName, dateStr, callback) {
editPageContent(targetPageName, { summary: makeSummary('добавление шаблона объединения'), readError: 'Не удалось получить содержимое' },
function (text) {
var existing = text.match(getCategoryMergeRe());
if (existing) {
var cats = existing[1].split('|').slice(1).map(function (p) { return p.trim(); }).filter(function (p) { return p.indexOf('=') === -1 && p.length > 0; });
var norm = sourceCatName.replace(/\s+/g, ' ').trim();
if (cats.some(function (c) { return c.replace(/\s+/g, ' ').trim() === norm; })) { return { skip: true, meta: { status: 'already_exists' } }; }
return {
text: text.replace(existing[0], function () { return existing[0].replace(/\}\}\s*$/, '|' + sourceCatName + '}}'); }),
summary: makeSummary('дополнение шаблона объединения [[:Категория:' + sourceCatName + ']]'),
meta: { status: 'updated' }
};
}
return { text: wrapInNoinclude(text, T_OPEN + 'Категория к объединению|' + dateStr + '|' + sourceCatName + T_CLOSE), meta: { status: 'created' } };
},
function (err, meta) { callback(!err, err ? err.info : ((meta && meta.status) || 'updated')); }
);
}
// ── Категории: обсуждение ────────────────────────────────────────────────
function buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, options) {
var opts = options || {};
var pages = Array.isArray(pageName) ? pageName : null;
var titleText;
if (pages && pages.length) {
titleText = String(opts.headerText || '').trim() || (formatPagesWithAnd(pages, ':') + (type === 'deletion' ? ' → удалить' : ''));
return '=== ' + titleText + ' ===\n';
}
var title = '=== [[:' + pageName + ']]';
if (type === 'rename' || type === 'merge') {
title += (type === 'rename' ? ' → ' : ' объединить с ') + formatCatLink(mainName);
if (additionalNames && additionalNames.length) {
var conj = type === 'rename' ? ' или ' : ' и ';
var head = additionalNames.slice(0, -1).map(formatCatLink).join(', ');
title += (additionalNames.length > 1 ? ', ' + head : '') + conj + formatCatLink(additionalNames[additionalNames.length - 1]);
}
} else if (type === 'deletion') {
title += ' → удалить';
}
return title + ' ===\n';
}
function createCategoryDiscussion(pageName, reason, type, callback, mainName, additionalNames, options) {
var cb = callback || function () {};
var opts = options || {};
var now = new Date();
var m = now.getUTCMonth(), year = now.getUTCFullYear(), day = now.getUTCDate();
var dateHeader = '== ' + day + ' ' + MONTHS_GEN[m] + ' ' + year + ' ==\n';
var discPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[m] + '_' + year;
var discTitle = buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, opts);
var discText = discTitle + (opts.reasonIsPrepared ? String(reason || '') : appendNominationSignature(reason)) + '\n';
var sectionTitle = discTitle.replace(/^===\s*/, '').replace(/\s*===\s*$/, '').trim();
var summaryTarget = Array.isArray(pageName) ? formatPagesWithAnd(pageName, ':') : '[[:' + pageName + ']]';
var summaryText = 'добавление обсуждения для ' + (Array.isArray(pageName) ? 'категорий ' : 'категории ') + summaryTarget;
publishNomination({
pageTitle: discPage,
readErrorMessage: 'Не удалось получить содержимое страницы обсуждения.',
summary: makeSummary(summaryText),
createText: function () {
return T_OPEN + 'ОБК-Навигация' + T_CLOSE + '\n\n' + dateHeader + discText;
},
buildText: function (text) {
var todayIdx = text.indexOf(dateHeader.trim());
if (todayIdx !== -1) {
var endIdx = text.indexOf('\n== ', todayIdx + dateHeader.length);
if (endIdx === -1) endIdx = text.length;
var before = text.slice(0, endIdx);
return { text: (before.endsWith('\n\n') ? before.slice(0, -1) : before) + '\n' + discText + text.slice(endIdx) };
}
var searchStr = T_OPEN + 'ОБК-Навигация' + T_CLOSE;
var obkIdx = text.indexOf(searchStr);
if (obkIdx === -1) return { error: { code: 'insert_failed', info: 'Не удалось найти место для вставки.' } };
return { text: text.slice(0, obkIdx + searchStr.length) + '\n\n' + dateHeader + discText + text.slice(obkIdx + searchStr.length).replace(/^\n+/, '\n') };
}
}, function (err) {
if (err) { cb(err); return; }
cb(null, { pageTitle: discPage, sectionTitle: sectionTitle });
});
}
// ── Категории: завершение ОБКАТ ───────────────────────────────────────────
function markCategoryDiscussionAsDone(pageName) {
runPageEditWithStatus({
pageName: pageName,
pendingText: 'Снимается шаблон обсуждения...',
errorText: 'Снятие шаблона обсуждения.',
successText: 'Шаблон обсуждения снят.',
editOptions: { summary: makeSummary('обсуждение категории завершено'), readError: 'Не удалось получить содержимое.' },
buildFn: function (text) {
var allTpls = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|');
var patterns = [
new RegExp('<noinclude>\\s*\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}\\s*</noinclude>', 'i'),
new RegExp('\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}', 'i')
];
var match = null;
for (var i = 0; i < patterns.length; i++) { match = text.match(patterns[i]); if (match) break; }
if (!match) return { error: { code: 'no_changes', info: 'Шаблон обсуждаемой категории не найден.' } };
return {
text: text.replace(match[0], '').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n'),
meta: { tplDate: convertToStandardDate(match[2].split('|')[0].trim()) }
};
},
onSuccess: function (meta) {
var talkStatusId = logStatus('Обновляется шаблон на СО категории...', null, { pending: true, trackError: false });
updateCategoryTalkPage(pageName, meta.tplDate, function (talkErr, info) {
if (talkErr) { logStatus('Установка шаблона на СО.', talkErr, { statusId: talkStatusId }); unlockModalSubmit(); return; }
logStatus(
(info && (info.status === 'already_present' || info.status === 'no_changes')) ? 'Шаблон на СО уже установлен.' : 'Шаблон установлен на СО.',
null, { statusId: talkStatusId, trackError: false }
);
renderModalFooter('reload');
});
}
});
}
function updateCategoryTalkPage(categoryName, templateDate, callback) {
var cb = callback || function () {};
var talkPage = getTalkPage(categoryName);
var newTpl = T_OPEN + 'Обсуждавшаяся категория|' + templateDate + T_CLOSE;
getTextWithTimestamp(talkPage, function (text, baseTimestamp, readErr) {
if (readErr) { cb(makeReadError(readErr, 'talk_read_failed', 'Не удалось получить содержимое СО категории.')); return; }
if (text === null) {
apiReq({ title: talkPage, text: newTpl + '\n\n', summary: makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate), createonly: true },
'edit', function (resp) {
if (resp && resp.error) {
if (resp.error.code === 'articleexists') setTimeout(function () { updateCategoryTalkPage(categoryName, templateDate, cb); }, 1000);
else cb(resp.error);
} else cb(null, { status: 'created' });
});
return;
}
var discussedRe = new RegExp('\\{\\{\\s*(' + cfg.categoryTemplates.discussed + ')([^\\}]*?)\\s*\\}\\}', 'i');
var tplMatch = text.match(discussedRe);
var newText = text;
if (tplMatch) {
var existingDates = tplMatch[2].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
if (existingDates.indexOf(templateDate) !== -1) { cb(null, { status: 'already_present' }); return; }
newText = text.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}$/, '|' + templateDate + '}}'); });
} else {
newText = insertTplOnTalkPage(text, newTpl);
}
if (newText === text) { cb(null, { status: 'no_changes' }); return; }
var ep = {
title: talkPage,
text: newText,
summary: tplMatch
? makeSummary('обновление шаблона [[ш:Обсуждавшаяся категория]], добавлена дата ' + templateDate)
: makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate)
};
if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) { cb(resp && resp.error ? resp.error : null, resp && !resp.error ? { status: tplMatch ? 'updated' : 'created' } : null); });
});
}
// ── Быстрое объединение (Ctrl+клик КОБ) ─────────────────────────────────
function buildQuickMergeHtml(tplDate, targets, currentCatName) {
return joinHtml([
'<p>Найден шаблон с датой <strong>', escapeHtml(tplDate), '</strong>. Категории для объединения:</p>',
'<pre style="background:', tk.bgBase, ';color:', tk.cBase, ';padding:10px;border:1px solid ', tk.bSubS, ';border-radius:4px;margin-bottom:10px;">',
targets.map(function (c) { return '• ' + escapeHtml(c); }).join('\n'),
'</pre>',
'<p><strong>Текущая категория:</strong> ', escapeHtml(currentCatName), '</p>',
'<p style="color:', tk.cSub, ';">Шаблон будет добавлен во все указанные выше категории.</p>'
]);
}
function showQuickMergeModal() {
getText(mwCfg.wgPageName, function (text, readErr) {
if (readErr) { alert('Не удалось получить содержимое: ' + (readErr.info || readErr.code || 'ошибка API') + '.'); return; }
if (!text) { alert('Не удалось получить содержимое.'); return; }
var mergeRe = getCategoryMergeRe();
var match = text.match(mergeRe);
if (!match) { alert('В текущей категории не найден шаблон "Категория к объединению".'); return; }
var params = match[1].split('|').map(function (p) { return p.trim(); });
var tplDate = params[0];
var targets = params.slice(1);
if (!targets.length) { alert('В шаблоне не найдены целевые категории.'); return; }
createModal({ title: 'Быстрое добавление шаблона объединения' });
var currentCatName = normTitle(stripCatPrefix(mwCfg.wgPageName));
$('#removerModalContent').html(buildQuickMergeHtml(tplDate, targets, currentCatName));
renderModalFooter('submit', {
showCheckbox: false,
submitText: 'Добавить шаблоны',
onSubmit: function () {
startProcessing();
$('#removerSubmit').prop('disabled', true);
eachSequential(targets, function (target, next) {
addMergeTemplateToTargetCategory('Категория:' + target, currentCatName, tplDate, function (success, status) {
if (success) logStatus('Категория [[:Категория:' + target + ']] (' + formatMergeStatus(status) + ').', null, { trackError: false });
else logStatus('Ошибка [[:Категория:' + target + ']].', { code: 'merge_target_failed', info: status }, { trackError: false });
next();
});
}, function () {
if (isError) markSubmitError(); else renderModalFooter('close');
});
return true;
}
});
});
}
// ── ЗКА/Защита: публикация ───────────────────────────────────────────────
function getReporterContext(mode) {
var rawPage = mwCfg.wgPageName;
var pageName = normTitle(rawPage)
.replace(/(Special|Служебная):(Contributions|Вклад)\//i, 'User:')
.replace(/(User talk:|Обсуждение участни(ка|цы):)/i, 'User:');
var isUserRelated = /user|contrib|участни|вклад/i.test(rawPage);
var displayName = normTitle(rawPage)
.replace(/(Special|Служебная):(Вклад|Contributions)\//i, '')
.replace(/(User talk:|Обсуждение участни(ка|цы):)/i, '')
.replace(/(user|участни(к|ца)):/i, '');
var pageLink = '[[' + pageName + (isUserRelated ? '|' + displayName + ']]' : ']]');
var reportPage = mode === 'request' ? 'Википедия:Запросы к администраторам' : 'Википедия:Установка защиты';
return { pageName: pageName, pageLink: pageLink, displayName: displayName, reportPage: reportPage };
}
function doReport(ctx, fast, protectMode) {
var header = $('#rmReportHeader').val() || ctx.pageLink;
var text = $('#rmReportText').val() || '';
var isZka = ctx.reportPage === 'Википедия:Запросы к администраторам';
var isRemoveProtect = !isZka && protectMode === 'remove';
startProcessing();
var targetPage, editParams, sectionForLink;
if (fast) {
targetPage = 'Википедия:Запросы к администраторам/Быстрые';
sectionForLink = null;
editParams = {
appendtext: '\n\n' + T_OPEN + 'sub' + 'st:t:preload/ЗКАБ/subst|\n | участник = ' + ctx.displayName +
'| страница = | пояснение = ' + text + T_CLOSE + '\n',
summary: makeSummary('новый запрос [[Special:Contributions/' + ctx.displayName + ']]')
};
} else if (isZka) {
targetPage = ctx.reportPage;
sectionForLink = extractDisplayedText(header);
var isIpFull = /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/.test(ctx.displayName);
editParams = {
section: 'new',
sectiontitle: header,
text: '* ' + T_OPEN + 'userlinks|' + ctx.displayName + (isIpFull ? '|ip=1' : '') + T_CLOSE + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
} else {
targetPage = isRemoveProtect ? 'Википедия:Снятие защиты' : 'Википедия:Установка защиты';
var pages = collectInputValues('.rmProtectPageInput');
if (!pages.length) pages = [ctx.pageName];
var sectionTitle, pageLines;
if (pages.length === 1) {
sectionTitle = '[[' + pages[0] + ']]';
pageLines = '* ' + T_OPEN + 'pagelinks-protect|' + pages[0] + T_CLOSE;
} else {
sectionTitle = ($('#rmProtectHeader').val() || '').trim() || pages.map(function (p) { return '[[' + p + ']]'; }).join(', ');
pageLines = pages.map(function (p) { return '* ' + T_OPEN + 'pagelinks-protect|' + p + T_CLOSE; }).join('\n');
}
sectionForLink = extractDisplayedText(sectionTitle);
editParams = {
section: 'new',
sectiontitle: sectionTitle,
text: pageLines + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
}
var statusId = logStatus('Публикуется запрос на «' + targetPage + '»...', null, { pending: true, trackError: false });
apiReq($.extend({ title: targetPage }, editParams), 'edit', function (resp) {
if (resp && resp.error) {
logStatus('Публикация запроса на «' + targetPage + '».', resp.error, { statusId: statusId });
markSubmitError();
if (isZka) $('#rmReportFast').prop('disabled', false);
return;
}
logStatus('Запрос опубликован.', null, { statusId: statusId, trackError: false });
appendNominationLink(targetPage, sectionForLink);
if (sectionForLink) subscribeToTopic(targetPage, sectionForLink);
renderModalFooter('reload');
});
}
// ═══════════════════════════════════════════════════════════════════════════
// ДИСПЕТЧЕР
// ═══════════════════════════════════════════════════════════════════════════
function handleMenuClick(item, event) {
isError = false;
var op = OPERATIONS_MAP[item.id];
// Специальный случай: КОБ категории с Ctrl — быстрое добавление шаблона
if (item.id === 'cat-merge' && event && event.ctrlKey) {
showQuickMergeModal();
return;
}
if (!op) {
console.error('RemoverCore: неизвестная операция', item.id);
return;
}
var handlerFn = handlers[op.handler];
if (typeof handlerFn !== 'function') {
console.error('RemoverCore: обработчик не найден', op.handler);
return;
}
handlerFn(op, event);
}
// ─── Экспорт ─────────────────────────────────────────────────────────────
window.RemoverCore = { handleMenuClick: handleMenuClick };
}());
hw0tjcqe2uv8fglhny225g0im7qxtbb
739922
739916
2026-05-01T07:08:55Z
Solidest
54422
739922
javascript
text/javascript
/**
* Remover — ядро (core).
* Загружается лениво при первом клике по пункту меню.
* Ожидает, что window.RemoverState уже задан remover-loader.js.
*
* Архитектура: единый реестр OPERATIONS описывает все операции (КБУ, КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС, Защита, Запрос, Снятие, кат-варианты).
* Логика выполнения сосредоточена в универсальных обработчиках.
* Экспортирует: window.RemoverCore.handleMenuClick(item, event)
*/
(function () {
'use strict';
var state = window.RemoverState;
if (!state) { console.error('RemoverCore: window.RemoverState не задан.'); return; }
var mwCfg = state.mwCfg;
var cfg = applyCoreConfigDefaults(state.cfg || {});
var isCategory = state.isCategory;
var isVector22 = state.isVector22;
var scriptLink = cfg.scriptLink;
var settingsOptionName = state.settingsOptionName || 'userjs-remover-settings';
var settingsVersion = 1;
var settingsMenuMeta = collectSettingsMenuMeta();
var settingsArticleItemLabels = settingsMenuMeta.articleLabels;
var settingsCategoryItemLabels = settingsMenuMeta.categoryLabels;
var settingsItemLabelById = settingsMenuMeta.idToLabel;
var settingsItemLabelByNorm = settingsMenuMeta.labelByNorm;
var settingsItemLabelOrder = settingsMenuMeta.labelOrder;
var settingsDefaults = getDefaultSettings();
var MENU_TITLE_PRESET_CACTIONS = '__remover_portlet_cactions__';
var MENU_TITLE_PRESET_PAGE = '__remover_portlet_page__';
var MENU_TITLE_PRESET_TOOLS = '__remover_portlet_tools__';
var initialSettings = normalizeRemoverSettings(readSettingsOptionState(state.settings || {}));
var setAlert = ('setAlert' in state) ? !!state.setAlert : initialSettings.notifyAuthor;
var setSubscribe = ('setSubscribe' in state) ? !!state.setSubscribe : initialSettings.subscribeTopic;
var signatureSeparator = ('signatureSeparator' in state && typeof state.signatureSeparator === 'string')
? state.signatureSeparator.trim()
: initialSettings.signatureSeparator;
initialSettings.notifyAuthor = setAlert;
initialSettings.subscribeTopic = setSubscribe;
initialSettings.signatureSeparator = signatureSeparator;
state.cfg = cfg;
state.settings = clonePlainObject(initialSettings);
state.setAlert = setAlert;
state.setSubscribe = setSubscribe;
// ─── Константы ──────────────────────────────────────────────────────────
var MONTHS_NOM = ['Январь','Февраль','Март','Апрель','Май','Июнь','Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'];
var MONTHS_GEN = ['января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря'];
var MONTHS_NOM_LOWER = MONTHS_NOM.map(function (m) { return m.toLowerCase(); });
var T_OPEN = '{' + '{';
var T_CLOSE = '}' + '}';
var KBU_CRITERIA_PAGE = 'Википедия:Критерии быстрого удаления';
var RE_ESCAPE = /[.*+?^${}()|[\]\\]/g;
var RE_KU_ON_PAGE = /\{\{\s*(?:к\s*удалению|ку)\s*(?:\||\}\})/i;
var RE_KPM_ON_PAGE = /\{\{\s*(?:к\s*переименованию|кпм|rename)\s*(?:\||\}\})/i;
var RE_KBU_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?(?:db\s*-[^|}\s]+|уд\s*-[^|}\s]+|к\s*быстрому\s*удалению|к\s*отсроченному\s*удалению|deleteslow|ds|hang\s*-?\s*on)\s*(?:\||\}\})/i;
var RE_KUL_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?к\s*улучшению\s*(?:\||\}\})/i;
var KBU_PATTERN_STR = 'db\\s*-[^|}\\s]+|уд\\s*-[^|}\\s]+|к\\s*быстрому\\s*удалению|к\\s*отсроченному\\s*удалению|deleteslow|ds';
var RE_KBU_PATTERNS = new RegExp('(?:' + KBU_PATTERN_STR + ')', 'i');
var KUL_PATTERN_STR = 'к\\s*улучшению';
var RE_KUL_PATTERN = new RegExp(KUL_PATTERN_STR, 'i');
var HANGON_PATTERN_STR = 'hang\\s*-?\\s*on';
var RE_HANGON = new RegExp(HANGON_PATTERN_STR, 'i');
var RE_DATE_ISO = /^(\d{4})-(\d{2})-(\d{2})$/;
var RE_DATE_RUSSIAN = /^(\d{1,2})\s+([\u0430-\u044f\u0410-\u042f\u0451\u0401.]+)\s+(\d{2}|\d{4})$/;
var RE_DATE_DASH = /^(\d{1,4})\s*-\s*(\d{1,2})\s*-\s*(\d{1,4})$/;
var RE_DATE_DOT = /^(\d{1,2})\s*\.\s*(\d{1,2})\s*\.\s*(\d{2}|\d{4})$/;
var RE_DATE_SLASH = /^(\d{1,4})\s*\/\s*(\d{1,2})\s*\/\s*(\d{1,4})$/;
var RE_NOINCLUDE = /(\s*)<noinclude>([\s\S]*?)<\/noinclude>/i;
var RE_TEMPLATE_NS = /^шаблон\s*:\s*/i;
// ─── Глобальные переменные сессии ────────────────────────────────────────
var isError = false;
var logStatusSeq = 0;
var resizeObservers = [];
var modalLayoutSyncHandlers = [];
var tplAliasCache = {};
// ─── Стили ───────────────────────────────────────────────────────────────
var stStyles = cfg.modalStyles;
var tk = {
cBase: 'var(--color-base, #202122)',
cSub: 'var(--color-subtle, #72777d)',
cSubM: 'var(--color-subtle, #54595d)',
cInv: 'var(--color-inverted-fixed, #fff)',
cProg: 'var(--color-progressive, #3366cc)',
cProgH: 'var(--color-progressive--hover, #2a4b8d)',
cDang: 'var(--color-destructive, #d73333)',
cDis: 'var(--color-disabled, var(--color-subtle, #72777d))',
bgBase: 'var(--background-color-base, #fff)',
bgNSub: 'var(--background-color-neutral-subtle, #f8f9fa)',
bgN: 'var(--background-color-neutral, #eaecf0)',
bgDis: 'var(--background-color-disabled, var(--background-color-neutral, #eaecf0))',
bgProg: 'var(--background-color-progressive, #3366cc)',
bgProgH:'var(--background-color-progressive--hover, #2a4d8f)',
bgSucc: 'var(--background-color-success, #14866d)',
bgSuccH:'var(--background-color-success--hover, #0f6d57)',
bSub: 'var(--border-color-subtle, #a2a9b1)',
bSubS: 'var(--border-color-subtle, #ddd)',
bDis: 'var(--border-color-disabled, var(--border-color-subtle, #a2a9b1))',
bProg: 'var(--border-color-progressive, #3366cc)',
bProgH: 'var(--border-color-progressive--hover, #2a4d8f)',
bSucc: 'var(--border-color-success, #14866d)',
bSuccH: 'var(--border-color-success--hover, #0f6d57)'
};
var sz = {
taH: '180px',
taMinH: '100px',
taMinW: '180px',
mobileBp: 720,
modalRatio: 0.4,
modalMinWide: 420,
modalDefaultWide: 720,
viewportGap: 24,
touchDesktopGap: 120
};
var btnBase = 'border-radius:4px;padding:8px 16px;cursor:pointer;font-size:14px;font-family:inherit;transition:background .1s;';
var neutralVis = 'background:' + tk.bgNSub + ';border:1px solid ' + tk.bSub + ';color:' + tk.cBase + ';border-radius:4px;';
var stCancel = neutralVis + btnBase;
var stSubmit = 'background:' + tk.bgProg + ';color:' + tk.cInv + ';border:1px solid ' + tk.bProg + ';' + btnBase;
var stReload = 'background:' + tk.bgSucc + ';color:' + tk.cInv + ';border:1px solid ' + tk.bSucc + ';' + btnBase;
var stInputBox = 'flex:1;padding:6px;box-sizing:border-box;min-width:0;border:1px solid ' + tk.bSub + ';background:' + tk.bgBase + ';color:inherit;border-radius:2px;';
var stInputFull= 'width:100%;padding:6px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:2px;background:' + tk.bgBase + ';color:inherit;margin-bottom:10px;';
var stRow = 'display:flex;margin-bottom:6px;';
var stToolBtn = neutralVis + 'padding:4px 8px;margin-left:4px;cursor:pointer;font-size:12px;';
var stRemoveBtn= neutralVis + 'display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;padding:0;width:32px;height:32px;margin-left:4px;cursor:pointer;font-size:12px;line-height:1;';
var stFooterWrap = 'display:flex;align-items:center;gap:8px;flex-wrap:wrap;';
var stFooterChecks = 'display:flex;flex-direction:column;gap:4px;margin-right:auto;flex:1 1 220px;min-width:0;';
var stFooterCheckLabel = 'display:inline-flex;align-items:flex-start;gap:6px;font-size:14px;line-height:1.4;max-width:100%;';
var stFooterActions = 'display:flex;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;justify-content:flex-end;margin-left:auto;';
var stHeaderIconBtn = 'margin:0 0 0 auto;width:32px;min-width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:4px;background:' + tk.bgNSub + ';color:' + tk.cSubM + ';cursor:pointer;font-size:16px;line-height:1;';
var multiNominationGap = '6px';
var RESIZE_CLASS = 'rm-resizable';
// ═══════════════════════════════════════════════════════════════════════════
// РЕЕСТР ОПЕРАЦИЙ
// Каждая запись описывает одну кнопку меню. Поля:
// id — идентификатор (совпадает с item.id из loader)
// handler — имя метода-обработчика в объекте handlers
// handlerArg — аргумент, передаваемый в handler (опционально)
// ═══════════════════════════════════════════════════════════════════════════
var OPERATIONS = [
// ── Статьи ──────────────────────────────────────────────────────────
{
id: 'fRm',
label: 'КБУ',
handler: 'showKbu',
// Параметры номинации: заполняются при submit
nomination: {
pageTitle: function (pg) { return normTitle(pg); },
// шаблон встраивается в статью, номинационная страница отсутствует
inArticle: true
}
},
{
id: 'tRm',
label: 'КУ',
handler: 'showNomination',
nomination: {
comment: 'к удалению',
template: 'к удалению',
navTemplate: 'КУ',
nomPage: function (date) { return 'Википедия:К удалению/' + date; },
supportsMulti: true,
supportsTransfer: true,
// шаблон встраивается в статью через <noinclude>
inArticle: true,
articleTpl: function (tplpar, date) { return 'к удалению|' + date + (tplpar ? '|' + tplpar : ''); }
}
},
{
id: 'rnm',
label: 'КПМ',
handler: 'showNomination',
nomination: {
comment: 'к переименованию',
template: 'к переименованию',
navTemplate: 'КПМ',
nomPage: function (date) { return 'Википедия:К переименованию/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к переименованию|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'rename',
firstId: 'rmRenameFirst', inputClass: 'rmRenameInput',
firstPh: 'Новое название',
addBtnId: 'rmAddRename', addBtnLabel: 'Добавить вариант',
containerId: 'rmRenameContainer', addPh: 'Дополнительный вариант',
maxRows: 2, maxMsg: 'Максимум 3 варианта переименования.'
}
}
},
{
id: 'imp',
label: 'КУЛ',
handler: 'showNomination',
nomination: {
comment: 'к срочному улучшению',
template: 'к улучшению',
navTemplate: 'КУЛ',
nomPage: function (date) { return 'Википедия:К улучшению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к улучшению|' + date + (tplpar ? '|' + tplpar : ''); }
}
},
{
id: 'merge',
label: 'КОБ',
handler: 'showNomination',
nomination: {
comment: 'к объединению с другой',
template: 'к объединению',
navTemplate: 'КОБ',
nomPage: function (date) { return 'Википедия:К объединению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к объединению|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'merge',
firstId: 'rmMergeFirst', inputClass: 'rmMergeInput',
firstPh: 'Объединить с…',
addBtnId: 'rmAddMerge', addBtnLabel: '+',
containerId: 'rmMergeContainer', addPh: 'Дополнительная статья',
maxRows: 10, maxMsg: 'Максимум 11 статей для объединения.'
}
}
},
{
id: 'split',
label: 'КРАЗД',
handler: 'showNomination',
nomination: {
comment: 'к разделению',
template: 'к разделению',
navTemplate: 'КР',
nomPage: function (date) { return 'Википедия:К разделению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к разделению|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'split',
firstId: 'rmSplitFirst', inputClass: 'rmSplitInput',
firstPh: 'Разделить на…',
addBtnId: 'rmAddSplit', addBtnLabel: '+',
containerId: 'rmSplitContainer', addPh: 'Дополнительная статья'
}
}
},
{
id: 'recov',
label: 'ВУС',
handler: 'showNomination',
nomination: {
comment: '',
template: 'к восстановлению',
navTemplate: 'ВУС',
nomPage: function (date) { return 'Википедия:К восстановлению/' + date; },
inArticle: false // шаблон не ставится в (удалённую) статью
}
},
{
id: 'close',
label: 'Снятие',
handler: 'showArticleClose'
},
// ── Запросы ─────────────────────────────────────────────────────────
{
id: 'protect',
label: 'Защита',
handler: 'showReport',
reportMode: 'protect'
},
{
id: 'request',
label: 'Запрос',
handler: 'showReport',
reportMode: 'request'
},
// ── Категории ────────────────────────────────────────────────────────
{
id: 'cat-fRm',
label: 'КБУ',
handler: 'showKbu',
forCategory: true
},
{
id: 'cat-discuss',
label: 'Обсудить',
handler: 'showCatNomination',
catType: 'discuss'
},
{
id: 'cat-delete',
label: 'Удалить',
handler: 'showCatNomination',
catType: 'deletion'
},
{
id: 'cat-rename',
label: 'Переименовать',
handler: 'showCatNomination',
catType: 'rename'
},
{
id: 'cat-merge',
label: 'Объединить',
handler: 'showCatNomination',
catType: 'merge'
},
{
id: 'cat-done',
label: 'Снятие',
handler: 'showCatClose'
}
];
// Быстрый поиск по id
var OPERATIONS_MAP = {};
OPERATIONS.forEach(function (op) { OPERATIONS_MAP[op.id] = op; });
// ─── Тексты переноса (КБУ → КУ) ─────────────────────────────────────────
var transferTexts = {
kbu: { notice: 'Перенесено с быстрого удаления.', hint: 'Шаблоны КБУ и Hangon будут сняты.' },
kul: { notice: 'Перенесено с КУЛ.', hint: 'Шаблоны КУЛ будут сняты.' },
both: { notice: 'Перенесено с быстрого удаления и КУЛ.', hint: 'Шаблоны КБУ, КУЛ и Hangon будут сняты.' }
};
// ═══════════════════════════════════════════════════════════════════════════
// УТИЛИТЫ
// ═══════════════════════════════════════════════════════════════════════════
function escapeRegExp(s) { return s.replace(RE_ESCAPE, '\\$&'); }
function escapeHtml(s) {
return String(s || '')
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
function joinHtml(parts) { return parts.join(''); }
function ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
function padTwo(n) { return n < 10 ? '0' + n : String(n); }
function expandTwoDigitYear(value) {
return 2000 + parseInt(value, 10);
}
function monthToNumber(name) {
var lower = name.toLowerCase().replace(/\.$/, '');
var idx = MONTHS_GEN.indexOf(lower);
if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower);
if (idx === -1 && lower.length >= 3) {
for (var i = 0; i < MONTHS_GEN.length; i++) {
if (MONTHS_GEN[i].indexOf(lower) === 0 || MONTHS_NOM_LOWER[i].indexOf(lower) === 0) return i + 1;
}
}
return idx + 1;
}
function makeStandardDate(yearValue, monthValue, dayValue) {
var yearText = String(yearValue || '').trim();
var year = yearText.length === 2 ? expandTwoDigitYear(yearText) : parseInt(yearText, 10);
var month = parseInt(monthValue, 10);
var day = parseInt(dayValue, 10);
var maxDay;
if ((yearText.length !== 2 && yearText.length !== 4) || isNaN(year) || isNaN(month) || isNaN(day) || year < 1 || month < 1 || month > 12 || day < 1) return null;
maxDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
if (day > maxDay) return null;
return year + '-' + padTwo(month) + '-' + padTwo(day);
}
function normalizeIsoDate(value) {
var m = String(value || '').trim().match(RE_DATE_ISO);
return m ? makeStandardDate(m[1], m[2], m[3]) : null;
}
function normalizeTemplateName(name) {
return (name || '').toLowerCase().replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
}
function getDate(dateString) {
var d = dateString ? new Date(dateString) : new Date();
var iso = d.getUTCFullYear() + '-' + padTwo(d.getUTCMonth() + 1) + '-' + padTwo(d.getUTCDate());
var rus = d.getUTCDate() + ' ' + MONTHS_GEN[d.getUTCMonth()] + ' ' + d.getUTCFullYear();
return [iso, rus];
}
function convertToStandardDate(dateStr) {
var value = String(dateStr || '').replace(/\s+/g, ' ').trim();
var m;
var mo;
var normalized;
m = value.match(RE_DATE_ISO);
if (m) return normalizeIsoDate(value) || '';
m = value.match(RE_DATE_RUSSIAN);
if (m) {
mo = monthToNumber(m[2]);
normalized = mo ? makeStandardDate(m[3], mo, m[1]) : null;
return normalized || '';
}
m = value.match(RE_DATE_DASH);
if (m) {
normalized = makeStandardDate(m[1], m[2], m[3]);
return normalized || '';
}
m = value.match(RE_DATE_DOT);
if (m) {
normalized = makeStandardDate(m[3], m[2], m[1]);
return normalized || '';
}
m = value.match(RE_DATE_SLASH);
if (m) {
normalized = makeStandardDate(m[1], m[2], m[3]);
return normalized || '';
}
return value;
}
function getTalkPage(pageName) {
var match = /([^:]*:)?(.*)/.exec(pageName);
if (match[1]) {
var ns = mwCfg.wgNamespaceIds[match[1].slice(0, -1).toLowerCase().replace(/ /g, '_')];
if (ns !== undefined) return mwCfg.wgFormattedNamespaces[ns | 1] + ':' + match[2];
}
return 'Обсуждение:' + pageName;
}
function normTitle(s) { return (s || '').replace(/_/g, ' '); }
function stripCatPrefix(s) { return (s || '').replace(/^(?:Категория|Category):\s*/i, ''); }
function getCategoryNamespaceLabel() {
return mwCfg.wgFormattedNamespaces[14] || 'Категория';
}
function normalizeCategoryPageName(value) {
var title = normTitle(value).trim();
var nsMatch, nsKey, ns;
if (!title) return '';
nsMatch = title.match(/^([^:]+):(.+)$/);
if (nsMatch) {
nsKey = nsMatch[1].toLowerCase().replace(/ /g, '_');
ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[nsKey];
if (ns === 14 || nsKey === 'category' || nsKey === 'категория') return getCategoryNamespaceLabel() + ':' + nsMatch[2].trim();
return getCategoryNamespaceLabel() + ':' + title;
}
return getCategoryNamespaceLabel() + ':' + title;
}
function makeSummary(text) { return scriptLink + ': ' + text; }
function appendNominationSignature(text) {
var body = String(text || '');
return body + (signatureSeparator ? ' ' + signatureSeparator : '') + ' ~~' + '~~';
}
function extractDisplayedText(s) {
return (s || '').replace(/\[\[:?(?:[^|\]]+\|)?(.+?)\]\]/g, '$1');
}
function collectInputValues(selector) {
return $(selector).map(function () { return $(this).val().trim(); }).get().filter(Boolean);
}
function applyCoreConfigDefaults(config) {
var defaults = {
scriptLink: '[[Участник:Solidest/Remover|Remover]]',
fastRemoveReasons: {
general: [
['уд-бессвязно', 'О1 Бессвязный текст'],
['уд-тест', 'О2 Тестовая страница'],
['уд-ванд', 'О3 Вандальная страница'],
['уд-повторно', 'О4 Уже удалялось'],
['уд-автор', 'О5 По просьбе автора'],
['уд-обс', 'О6 Ненужная подстраница'],
['уд-переим', 'О7 Для переименования'],
['уд-дубль', 'О8 Дубликат'],
['уд-реклама', 'О9 Реклама или спам'],
['db-badtalk', 'О10 Нецелевая СО'],
['уд-копивио', 'О11 Нарушение АП']
],
articles: [
['подст:ds', 'ds Отсроченное пусто или коротко', 'С'],
['уд-пусто', 'С1 Пусто или коротко'],
['уд-иностр', 'С2 Не на русском'],
['уд-ссылки', 'С3 Лишь ссылки'],
['уд-нз', 'С5 Явно незначимо'],
['уд-бям', 'С7 Создано нейросетью']
],
redirects: [
['уд-в никуда', 'П1 Перенапр. в никуда'],
['db-redirspace', 'П2 Межпростр. перенапр.'],
['уд-опечатка', 'П3 Перенапр. с опечаткой'],
['уд-падеж', 'П4 Не именительный падеж'],
['уд-смысл', 'П5 Неверное перенапр.'],
['db-redirtalk', 'П6 Перенапр. на СО']
],
files: [
['db-duplicate', 'Ф1 Копия файла'],
['db-badimage', 'Ф2 Повреждённый файл'],
['подст:nld', 'Ф3 Нет данных о лицензии'],
['подст:nsd', 'Ф3 Нет данных о источнике'],
['подст:nad', 'Ф3 Нет данных о авторе'],
['подст:dd', 'Ф3 Сомнительные данные файла'],
['подст:ofud', 'Ф4 Неиспользуемый КДИ'],
['подст:dfud', 'Ф5 Нет КДИ'],
['db-badfairuse', 'Ф6 Неоправданное КДИ'],
['подст:rfu', 'Ф7 Заменяемый КДИ'],
['NCT', 'Ф8 Есть на Складе'],
['подст:Nothost', 'Ф9 Файл — ВП:НЕХОСТИНГ']
],
categories: [
['уд-пусткат', 'К1 Пустая категория'],
['db-templatecat', 'К1.2 Разобранная служебная кат.'],
['уд-перекат', 'К2 Переименованная кат.']
],
users: [
['уд-владелец', 'У1 По желанию владельца'],
['уд-анон', 'У2 Устаревшая СО анонима'],
['уд-несущ', 'У3 Несуществующий участник'],
['уд-нецелевое', 'У4 Нецелевое использ. ЛП'],
['уд-неактив', 'У5 Подстраница неактивного']
],
special: [
['db', 'Особый случай']
]
},
fastRemoveCriteriaAnchors: {
'подст:ds': 'С1',
deleteslow: 'С1',
ds: 'С1'
},
requiredParamTemplates: {
'уд-переим': 'страницу, которую нужно переименовать',
'уд-дубль': 'страницу-дубликат',
'уд-копивио': 'URL источника нарушения АП',
'db-duplicate': 'имя файла-оригинала',
'подст:rfu': 'имя заменяемого файла',
'NCT': 'имя файла на Викискладе',
'уд-перекат': 'новое название категории',
'db': '!причину удаления'
},
categoryTemplates: {
discuss: 'Обсуждаемая категория|обсуждаемая категория|Acat|acat|ОКТО|окто|Категория к обсуждению|категория к обсуждению',
rename: 'Категория к переименованию|категория к переименованию|Anacat|anacat',
merge: 'Категория к объединению|категория к объединению|Amergecat|amergecat|Cfm|cfm',
discussed: 'Обсуждавшаяся категория|обсуждавшаяся категория|Обсуждалась|обсуждалась|Обсуждалось|обсуждалось'
},
modalStyles: {
border: '1px solid var(--border-color-progressive, #3366bb)',
background: 'var(--background-color-base, #f8f9fa)',
borderRadius: '6px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
headerColor: 'var(--color-progressive, #3366bb)'
}
};
config.scriptLink = config.scriptLink || defaults.scriptLink;
config.fastRemoveReasons = $.extend({}, defaults.fastRemoveReasons, config.fastRemoveReasons || {});
config.fastRemoveCriteriaAnchors = $.extend({}, defaults.fastRemoveCriteriaAnchors, config.fastRemoveCriteriaAnchors || {});
config.requiredParamTemplates = $.extend({}, defaults.requiredParamTemplates, config.requiredParamTemplates || {});
config.categoryTemplates = $.extend({}, defaults.categoryTemplates, config.categoryTemplates || {});
config.modalStyles = $.extend({}, defaults.modalStyles, config.modalStyles || {});
return config;
}
function clonePlainObject(obj) {
return JSON.parse(JSON.stringify(obj || {}));
}
function normalizeMenuLabel(value) {
return String(value || '').replace(/\s+/g, ' ').trim().toLowerCase();
}
function readSettingsOptionState(fallback) {
var base = clonePlainObject(fallback || {});
var stored;
var raw = null;
if (!mw.user || !mw.user.options || typeof mw.user.options.get !== 'function') return base;
stored = mw.user.options.get(settingsOptionName);
if (typeof stored === 'string' && stored.trim()) {
try {
raw = JSON.parse(stored);
} catch (e) {
console.warn('RemoverCore: не удалось прочитать сохранённые настройки полностью, будут использованы данные loader.', e);
}
}
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return base;
return $.extend({}, raw, base, ('quickPhrases' in raw) ? { quickPhrases: raw.quickPhrases } : {});
}
function normalizeQuickPhraseValue(value) {
return String(value == null ? '' : value).replace(/\r\n?/g, '\n').trim();
}
function normalizeQuickPhrasesList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
var normalized = [];
(source || []).forEach(function (value) {
var phrase = normalizeQuickPhraseValue(value);
if (phrase && normalized.indexOf(phrase) === -1) normalized.push(phrase);
});
return normalized;
}
function collectSettingsMenuMeta() {
var labels = [];
var articleLabels = [];
var categoryLabels = [];
var idToLabel = {};
var labelByNorm = {};
var labelOrder = {};
function collect(items, targetLabels) {
items.forEach(function (item) {
var id;
var label;
var normLabel;
if (!item || item.type === 'separator') return;
id = String(item.id || '').trim();
label = String(item.label || '').trim();
normLabel = normalizeMenuLabel(label);
if (id && label && !(id in idToLabel)) idToLabel[id] = label;
if (label && targetLabels.indexOf(label) === -1) targetLabels.push(label);
if (normLabel && !(normLabel in labelByNorm)) {
labelByNorm[normLabel] = label;
labelOrder[label] = labels.length;
labels.push(label);
}
});
}
collect(cfg.articleMenuItems, articleLabels);
collect(cfg.categoryMenuItems, categoryLabels);
return {
labels: labels,
articleLabels: articleLabels,
categoryLabels: categoryLabels,
idToLabel: idToLabel,
labelByNorm: labelByNorm,
labelOrder: labelOrder
};
}
function buildSettingsMenuItemsHint() {
var parts = [];
if (settingsArticleItemLabels.length) {
parts.push('<div class="rmSettingsHintRow"><span class="rmSettingsHintBadge">Статьи</span>' + escapeHtml(settingsArticleItemLabels.join(', ')) + '.</div>');
}
if (settingsCategoryItemLabels.length) {
parts.push('<div class="rmSettingsHintRow"><span class="rmSettingsHintBadge">Категории</span>' + escapeHtml(settingsCategoryItemLabels.join(', ')) + '.</div>');
}
return parts.length ? '<div class="rmSettingsHintList">' + parts.join('') + '</div>' : '';
}
function getDefaultSettings() {
return {
version: settingsVersion,
excludedNamespaces: normalizeNumberList(cfg.excludedNamespaces, []),
notifyAuthor: !!cfg.defaultNotifyAuthor,
subscribeTopic: !!cfg.defaultSubscribeTopic,
menuTitle: (typeof cfg.menuTitle === 'string' && cfg.menuTitle.trim()) ? cfg.menuTitle.trim() : 'Remover',
disabledItems: [],
quickPhrases: normalizeQuickPhrasesList(cfg.quickPhrases, []),
showMenuIcons: !!cfg.showMenuIcons,
signatureSeparator: (typeof cfg.signatureSeparator === 'string') ? cfg.signatureSeparator.trim() : ''
};
}
function normalizeNumberList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
return source
.map(function (value) { return parseInt(value, 10); })
.filter(function (value, index, arr) { return !isNaN(value) && arr.indexOf(value) === index; })
.sort(function (a, b) { return a - b; });
}
function normalizeDisabledItemValue(value) {
var token = String(value || '').trim();
if (!token) return null;
if (settingsItemLabelById[token]) return settingsItemLabelById[token];
return settingsItemLabelByNorm[normalizeMenuLabel(token)] || null;
}
function compareSettingsMenuLabels(a, b) {
var ai = Object.prototype.hasOwnProperty.call(settingsItemLabelOrder, a) ? settingsItemLabelOrder[a] : Number.MAX_SAFE_INTEGER;
var bi = Object.prototype.hasOwnProperty.call(settingsItemLabelOrder, b) ? settingsItemLabelOrder[b] : Number.MAX_SAFE_INTEGER;
if (ai !== bi) return ai - bi;
return a.localeCompare(b, 'ru');
}
function normalizeDisabledItemsList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
return source
.map(normalizeDisabledItemValue)
.filter(function (value, index, arr) { return value && arr.indexOf(value) === index; })
.sort(compareSettingsMenuLabels);
}
function normalizeMenuTitleSetting(value, fallback) {
var menuTitle = String(value || '').trim();
if (!menuTitle) return fallback;
if (menuTitle === MENU_TITLE_PRESET_CACTIONS || /^(#)?p-cactions$/i.test(menuTitle)) return MENU_TITLE_PRESET_CACTIONS;
if (menuTitle === MENU_TITLE_PRESET_PAGE || /^(#)?p-page$/i.test(menuTitle) || /^(#)?p-actions$/i.test(menuTitle)) return MENU_TITLE_PRESET_PAGE;
if (menuTitle === MENU_TITLE_PRESET_TOOLS || /^(#)?p-tb$/i.test(menuTitle)) return MENU_TITLE_PRESET_TOOLS;
return menuTitle;
}
function normalizeRemoverSettings(raw) {
var defaults = clonePlainObject(settingsDefaults);
var source = (raw && typeof raw === 'object') ? raw : {};
return {
version: settingsVersion,
excludedNamespaces: normalizeNumberList(source.excludedNamespaces, defaults.excludedNamespaces || []),
notifyAuthor: ('notifyAuthor' in source) ? !!source.notifyAuthor : !!defaults.notifyAuthor,
subscribeTopic: ('subscribeTopic' in source) ? !!source.subscribeTopic : !!defaults.subscribeTopic,
menuTitle: normalizeMenuTitleSetting(
(typeof source.menuTitle === 'string' && source.menuTitle.trim()) ? source.menuTitle : '',
typeof defaults.menuTitle === 'string' && defaults.menuTitle.trim() ? defaults.menuTitle.trim() : 'Remover'
),
disabledItems: normalizeDisabledItemsList(source.disabledItems, defaults.disabledItems || []),
quickPhrases: normalizeQuickPhrasesList(source.quickPhrases, defaults.quickPhrases || []),
showMenuIcons: ('showMenuIcons' in source) ? !!source.showMenuIcons : !!defaults.showMenuIcons,
signatureSeparator: (typeof source.signatureSeparator === 'string')
? source.signatureSeparator.trim()
: (typeof defaults.signatureSeparator === 'string' ? defaults.signatureSeparator.trim() : '')
};
}
function areRemoverSettingsEqual(a, b) {
return JSON.stringify(normalizeRemoverSettings(a)) === JSON.stringify(normalizeRemoverSettings(b));
}
function updateStoredSettingsState(settings, skipUserOptionsSync) {
var normalized = normalizeRemoverSettings(settings);
state.settings = clonePlainObject(normalized);
setAlert = normalized.notifyAuthor;
setSubscribe = normalized.subscribeTopic;
signatureSeparator = normalized.signatureSeparator;
state.setAlert = setAlert;
state.setSubscribe = setSubscribe;
if (!skipUserOptionsSync && mw.user && mw.user.options && typeof mw.user.options.set === 'function') {
mw.user.options.set(settingsOptionName, JSON.stringify(normalized));
}
return normalized;
}
function splitSettingsInput(value) {
return String(value || '')
.split(/[\s,;]+/)
.map(function (part) { return part.trim(); })
.filter(Boolean);
}
function splitSettingsListInput(value) {
return String(value || '')
.split(/[\n,;]+/)
.map(function (part) { return part.trim(); })
.filter(Boolean);
}
function parseNamespaceInput(value) {
var tokens = splitSettingsInput(value);
var invalid = [];
var values = [];
tokens.forEach(function (token) {
var parsed = parseInt(token, 10);
if (String(parsed) !== token) invalid.push(token);
else if (values.indexOf(parsed) === -1) values.push(parsed);
});
values.sort(function (a, b) { return a - b; });
return { values: values, invalid: invalid };
}
function parseDisabledItemsInput(value) {
var tokens = splitSettingsListInput(value);
var invalid = [];
var values = [];
tokens.forEach(function (token) {
var normalized = normalizeDisabledItemValue(token);
if (!normalized) invalid.push(token);
else if (values.indexOf(normalized) === -1) values.push(normalized);
});
values.sort(compareSettingsMenuLabels);
return { values: values, invalid: invalid };
}
function formatPagesWithAnd(names, prefix) {
var p = prefix || ':';
var links = (names || []).map(function (n) { return '[[' + p + n + ']]'; });
if (!links.length) return '';
if (links.length === 1) return links[0];
return links.slice(0, -1).join(', ') + ' и ' + links[links.length - 1];
}
function formatCatLink(name) { return '[[:Категория:' + name + ']]'; }
function formatMergeStatus(status) {
return { already_exists: 'уже был', updated: 'дополнен', created: 'создан' }[status] || status;
}
function applyGeneratedText($el, generated) {
var cur = $el.val();
var prev = $el.data('rmGenerated') || '';
if (!prev || cur.indexOf(prev) === 0) {
$el.val(generated + cur.slice(prev.length));
} else {
$el.val(generated + (cur ? '\n' + cur : ''));
}
$el.data('rmGenerated', generated);
}
function getCurrentQuickPhrases() {
return normalizeQuickPhrasesList(
state.settings && state.settings.quickPhrases,
settingsDefaults.quickPhrases || []
);
}
function insertTextIntoTextarea($el, text) {
var el = $el && $el[0];
var value;
var start;
var end;
var updatedValue;
var caretPos;
if (!el) return;
text = String(text || '');
if (!text) return;
value = $el.val() || '';
start = typeof el.selectionStart === 'number' ? el.selectionStart : value.length;
end = typeof el.selectionEnd === 'number' ? el.selectionEnd : start;
updatedValue = value.slice(0, start) + text + value.slice(end);
caretPos = start + text.length;
$el.val(updatedValue).trigger('input').trigger('change').focus();
if (typeof el.setSelectionRange === 'function') el.setSelectionRange(caretPos, caretPos);
}
function buildQuickPhrasesPanelHtml(textareaId) {
var phrases = getCurrentQuickPhrases();
if (!phrases.length) return '';
return joinHtml([
'<div class="rmQuickPhrasesPanel ', RESIZE_CLASS, '" data-rm-target="', textareaId, '">',
phrases.map(function (phrase) {
return joinHtml([
'<button type="button" class="rmQuickPhraseActionBtn" data-rm-target="', textareaId,
'" data-rm-phrase="', escapeHtml(phrase), '">',
escapeHtml(phrase),
'</button>'
]);
}).join(''),
'</div>'
]);
}
function getMultiNominationCommentText(commentsByPage, pageTitle) {
var key = normTitle(pageTitle);
if (!commentsByPage || !Object.prototype.hasOwnProperty.call(commentsByPage, key)) return '';
return normalizeQuickPhraseValue(commentsByPage[key]);
}
function buildMultiNominationText(pages, bodyText, commentsByPage, options) {
var opts = options || {};
var list = Array.isArray(pages) ? pages.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasPageComments = false;
var headingLevel = Math.max(2, parseInt(opts.headingLevel, 10) || 3);
var headingMarks = new Array(headingLevel + 1).join('=');
var pageSections = list.map(function (pageName) {
var comment = getMultiNominationCommentText(commentsByPage, pageName);
if (comment) hasPageComments = true;
return '\n' + headingMarks + ' [[:' + pageName + ']] ' + headingMarks + '\n' + (comment ? appendNominationSignature(comment) + '\n' : '');
}).join('');
var commonSectionText = body
? appendNominationSignature(body)
: (hasPageComments ? '' : appendNominationSignature(''));
return pageSections + '\n' + headingMarks + ' По всем ' + headingMarks + '\n' + commonSectionText;
}
function buildMultiNominationListText(pages, bodyText, commentsByPage) {
var list = Array.isArray(pages) ? pages.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasPageComments = false;
var pageLines = list.map(function (pageName) {
var comment = getMultiNominationCommentText(commentsByPage, pageName);
if (comment) hasPageComments = true;
return '* [[:' + pageName + ']]' + (comment ? '\n' + appendNominationSignature(comment).split('\n').map(function (line) { return '*: ' + line; }).join('\n') : '');
}).join('\n');
var commonText = body
? appendNominationSignature(body)
: (hasPageComments ? '' : appendNominationSignature(''));
return pageLines + (pageLines && commonText ? '\n' : '') + commonText;
}
function collectMultiNominationComments(normalizePageName) {
var comments = {};
var normalize = typeof normalizePageName === 'function' ? normalizePageName : normTitle;
$('.rmMultiPageBlock').each(function () {
var $block = $(this);
var pageName = normalize(($block.find('.rmMultiPageInput').val() || '').trim());
var comment = normalizeQuickPhraseValue($block.find('.rmMultiPageCommentInput').val());
if (!pageName) return;
comments[pageName] = comment;
});
return comments;
}
function getNominationPublishText(job) {
if (job && job.isMulti) return String(job.msg || '');
return appendNominationSignature(job && job.msg);
}
function getNominationConflictRule(job) {
if (!job || job.mode !== 'nominate') return null;
if (job.opId === 'tRm' || job.opId === 'mRm') {
return {
id: 'ku',
label: 'КУ',
namePattern: '(?:к\\s*удалению|ку)',
detect: function (articleText) {
var match = String(articleText || '').match(/\{\{\s*((?:к\s*удалению|ку))\s*(?:\||\}\})/i);
if (!match) return null;
var templateName = ucfirst(String(match[1] || '').replace(/\s+/g, ' ').trim());
return {
label: 'КУ',
templateName: templateName || 'КУ',
templateDisplay: '{{' + (templateName || 'КУ') + '}}'
};
}
};
}
return null;
}
function detectNominationConflict(articleText, job) {
var rule = getNominationConflictRule(job);
if (!rule || typeof rule.detect !== 'function') return null;
return rule.detect(articleText);
}
function getConflictDecisionForPage(job, pageName) {
var decisions = job && job.conflictDecisions;
var key = normTitle(pageName);
return decisions && decisions[key] ? decisions[key] : null;
}
function getCategoryMergeRe() {
return new RegExp('\\{\\{\\s*(?:' + cfg.categoryTemplates.merge + ')\\s*\\|\\s*([^\\}]+)\\}\\}', 'i');
}
function eachSequential(targets, iteratee, done) {
var i = 0;
(function next(err) {
if (err || i >= targets.length) { done(err || null); return; }
iteratee(targets[i++], next);
}(null));
}
function normalizeSectionForLink(sectionTitle) {
return (sectionTitle || '').trim()
.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, function (_, target, label) {
var v = (label || target || '').trim();
return v.charAt(0) === ':' ? v.slice(1) : v;
})
.replace(/''+/g, '').replace(/\s+/g, ' ').trim();
}
function getViewportWidth() {
return Math.floor(Math.max(
(document.documentElement && document.documentElement.clientWidth) || 0,
(typeof window.innerWidth === 'number' && window.innerWidth) || 0,
$(window).width() || 0
));
}
function getVisualViewportWidth() {
var widths = [];
if (window.visualViewport && typeof window.visualViewport.width === 'number' && window.visualViewport.width > 0) widths.push(window.visualViewport.width);
if (window.screen && window.screen.width > 0) widths.push(window.screen.width);
return widths.length ? Math.floor(Math.min.apply(Math, widths)) : getViewportWidth();
}
function isTouchModalDevice() {
return !!(
(window.matchMedia && window.matchMedia('(pointer: coarse)').matches) ||
('ontouchstart' in window) ||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 0) ||
(navigator.msMaxTouchPoints && navigator.msMaxTouchPoints > 0)
);
}
function getModalLayout() {
var minWidth = parseInt(sz.taMinW, 10) || 180;
var layoutWidth = getViewportWidth();
var visualWidth = getVisualViewportWidth();
var contentWidth = Math.max(minWidth, Math.floor($('#content').width() || $('#content').innerWidth() || $(window).width() || minWidth));
var isMobile = layoutWidth <= sz.mobileBp;
var isTouchDesktop = !isMobile &&
isTouchModalDevice() &&
visualWidth > 0 &&
visualWidth <= sz.mobileBp &&
layoutWidth >= visualWidth + sz.touchDesktopGap;
var useFullWidth = isMobile || isTouchDesktop;
var maxOuterWidth;
var defaultOuterWidth;
var desktopWidth;
if (isMobile) maxOuterWidth = Math.max(minWidth, (visualWidth || contentWidth) - sz.viewportGap);
else if (isTouchDesktop) maxOuterWidth = Math.max(minWidth, contentWidth - 32);
else maxOuterWidth = Math.max(minWidth, Math.min(contentWidth, (visualWidth ? visualWidth - sz.viewportGap : contentWidth)));
desktopWidth = Math.max(minWidth, Math.floor(contentWidth * sz.modalRatio));
if (useFullWidth || desktopWidth < sz.modalMinWide) defaultOuterWidth = contentWidth;
else if (desktopWidth < sz.modalDefaultWide) defaultOuterWidth = sz.modalDefaultWide;
else defaultOuterWidth = desktopWidth;
return {
minWidth: minWidth,
isMobile: isMobile,
isTouchDesktop: isTouchDesktop,
useFullWidth: useFullWidth,
shouldCenter: useFullWidth || mwCfg.skin === 'minerva',
maxOuterWidth: maxOuterWidth,
defaultOuterWidth: Math.min(defaultOuterWidth, maxOuterWidth)
};
}
function getDefaultResizableWidth(frameWidth) {
var layout = getModalLayout();
return Math.max(layout.minWidth, layout.defaultOuterWidth - Math.floor(frameWidth || 0));
}
function getBoxFrameWidth($el) {
function px(prop) {
var n = parseFloat($el.css(prop));
return isNaN(n) ? 0 : n;
}
return px('padding-left') + px('padding-right') + px('border-left-width') + px('border-right-width');
}
// ═══════════════════════════════════════════════════════════════════════════
// API
// ═══════════════════════════════════════════════════════════════════════════
function getApiUrl() {
return (mw.util && typeof mw.util.wikiScript === 'function') ? mw.util.wikiScript('api') : '/w/api.php';
}
function getCsrfTokenValue() {
return (mw.user && mw.user.tokens && typeof mw.user.tokens.get === 'function')
? mw.user.tokens.get('csrfToken')
: null;
}
function storeCsrfToken(token) {
if (!token || !mw.user || !mw.user.tokens || typeof mw.user.tokens.set !== 'function') return;
mw.user.tokens.set({ csrfToken: token });
}
function isValidCsrfToken(token) {
return typeof token === 'string' && !!token && token !== '+\\';
}
function fetchCsrfToken(forceRefresh, callback) {
var cachedToken = getCsrfTokenValue();
if (!forceRefresh && isValidCsrfToken(cachedToken)) {
callback(cachedToken);
return;
}
$.ajax({
url: getApiUrl(),
method: 'GET',
dataType: 'json',
data: { action: 'query', meta: 'tokens', type: 'csrf', format: 'json' }
})
.done(function (data) {
var token = data && data.query && data.query.tokens && data.query.tokens.csrftoken;
if (isValidCsrfToken(token)) {
storeCsrfToken(token);
callback(token);
return;
}
callback(null);
})
.fail(function () {
callback(null);
});
}
function apiReq(params, mode, callback) {
var isWrite = mode === 'edit' || mode === 'discussiontoolssubscribe' || mode === 'options';
function sendRequest(retryWithFreshToken) {
var reqParams = $.extend({}, params, { format: 'json', action: mode });
if (!isWrite) {
$.ajax({ url: getApiUrl(), method: 'GET', data: reqParams, dataType: 'json' })
.done(function (data) { if (callback) callback(data); })
.fail(function (jqXHR, status) {
console.error('Ошибка API: ' + status);
if (callback) callback({ error: { code: 'network', info: status } });
});
return;
}
fetchCsrfToken(!!retryWithFreshToken, function (token) {
if (!isValidCsrfToken(token)) {
if (callback) callback({ error: { code: 'badtoken', info: 'Не удалось получить CSRF-токен.' } });
return;
}
reqParams.token = token;
$.ajax({ url: getApiUrl(), method: 'POST', data: reqParams, dataType: 'json' })
.done(function (data) {
var err = data && data.error;
var isBadToken = err && (err.code === 'badtoken' || /invalid csrf token/i.test(String(err.info || '')));
if (isBadToken && !retryWithFreshToken) {
sendRequest(true);
return;
}
if (callback) callback(data);
})
.fail(function (jqXHR, status) {
console.error('Ошибка API: ' + status);
if (callback) callback({ error: { code: 'network', info: status } });
});
});
}
sendRequest(false);
}
function saveSettingsToServer(settings, callback) {
var normalized = normalizeRemoverSettings(settings);
if (!mwCfg.wgUserName) {
callback({ code: 'notloggedin', info: 'Сохранять настройки можно только после входа в учётную запись.' });
return;
}
apiReq({ optionname: settingsOptionName, optionvalue: JSON.stringify(normalized) }, 'options', function (resp) {
if (resp && resp.options === 'success') {
callback(null, updateStoredSettingsState(normalized));
return;
}
callback((resp && resp.error) || { code: 'options_failed', info: 'Не удалось сохранить настройки.' });
});
}
function resetSettingsOnServer(callback) {
if (!mwCfg.wgUserName) {
callback({ code: 'notloggedin', info: 'Сбрасывать настройки можно только после входа в учётную запись.' });
return;
}
apiReq({ change: settingsOptionName }, 'options', function (resp) {
if (resp && resp.options === 'success') {
callback(null, updateStoredSettingsState(settingsDefaults, true));
return;
}
callback((resp && resp.error) || { code: 'options_failed', info: 'Не удалось сбросить настройки.' });
});
}
function getFirstQueryPage(data) {
var pages = data && data.query && data.query.pages;
if (!pages) return null;
return pages[Object.keys(pages)[0]] || null;
}
function extractRevisionContent(rev) {
if (!rev) return null;
if (typeof rev['*'] === 'string') return rev['*'];
if (rev.slots && rev.slots.main) {
if (typeof rev.slots.main['*'] === 'string') return rev.slots.main['*'];
if (typeof rev.slots.main.content === 'string') return rev.slots.main.content;
}
return null;
}
function makeReadError(apiError, fallbackCode, fallbackInfo) {
var err = apiError || {};
return {
code: err.code || fallbackCode || 'read_failed',
info: err.info || fallbackInfo || 'Не удалось получить содержимое.'
};
}
function getTextWithTimestamp(pageName, callback) {
apiReq({ prop: 'revisions', rvprop: 'content|timestamp', rvslots: 'main', titles: pageName }, 'query', function (data) {
var content;
var page;
if (data && data.error) {
callback(null, null, data.error);
return;
}
if (!data || !data.query || !data.query.pages) {
callback(null, null, { code: 'read_failed', info: 'Некорректный ответ API при чтении страницы.' });
return;
}
page = getFirstQueryPage(data);
if (!page) {
callback(null, null, { code: 'read_failed', info: 'API не вернул данные страницы.' });
return;
}
if (page.invalid !== undefined) {
callback(null, null, { code: 'invalidtitle', info: page.invalidreason || 'Некорректное название страницы.' });
return;
}
if (page.missing !== undefined) {
callback(null, null, null);
return;
}
if (!page.revisions || !page.revisions.length) {
callback(null, null, { code: 'read_failed', info: 'API не вернул ревизии страницы.' });
return;
}
content = extractRevisionContent(page.revisions[0]);
if (content === null) {
callback(null, null, { code: 'content_missing', info: 'API не вернул текст страницы.' });
return;
}
callback(content, page.revisions[0].timestamp || null, null);
});
}
function getText(pageName, callback) {
getTextWithTimestamp(pageName, function (text, baseTimestamp, err) { callback(text, err); });
}
function editPageContent(pageTitle, options, buildFn, callback) {
var opts = options || {};
var maxRetries = Math.max(0, parseInt(opts.editConflictRetries, 10) || 1);
(function attempt(retry) {
getTextWithTimestamp(pageTitle, function (sourceText, baseTimestamp, readErr) {
if (readErr) {
callback(makeReadError(readErr, opts.readErrorCode || 'read_failed', 'Не удалось получить содержимое страницы «' + pageTitle + '».'));
return;
}
if (sourceText === null) {
callback({ code: opts.readErrorCode || 'read_failed', info: opts.readError || 'Страница «' + pageTitle + '» не существует.' });
return;
}
var done = (function () {
var called = false;
return function (result) {
if (called) return;
called = true;
if (!result || result.error) { callback((result && result.error) || { code: 'build_failed', info: 'Не удалось подготовить правку.' }, result && result.meta || null); return; }
if (result.skip) { callback(null, result.meta || null); return; }
if (typeof result.text !== 'string') { callback({ code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
var ep = { title: pageTitle, text: result.text, summary: result.summary || opts.summary || '' };
if (opts.watchlist) ep.watchlist = opts.watchlist;
if (opts.assertuser) ep.assertuser = opts.assertuser;
if (opts.createonly) ep.createonly = opts.createonly;
if (opts.useBaseTimestamp !== false && baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) {
var err = resp && resp.error ? resp.error : null;
if (err && err.code === 'editconflict' && retry < maxRetries) { attempt(retry + 1); return; }
callback(err, result.meta || null);
});
};
}());
var maybe = buildFn(sourceText, done);
if (maybe !== undefined) done(maybe);
});
}(0));
}
// ═══════════════════════════════════════════════════════════════════════════
// ШАБЛОНЫ: удаление и вставка
// ═══════════════════════════════════════════════════════════════════════════
function findBalancedTemplateEnd(text, start) {
var depth = 0;
var i = start;
var len = text.length;
while (i < len - 1) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') {
depth++;
i += 2;
continue;
}
if (text.charAt(i) === '}' && text.charAt(i + 1) === '}') {
depth--;
i += 2;
if (depth === 0) return i;
continue;
}
i++;
}
return -1;
}
function splitTemplateTopLevelParts(innerText) {
var parts = [];
var start = 0;
var templateDepth = 0;
var linkDepth = 0;
var i = 0;
var text = String(innerText || '');
while (i < text.length) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') {
templateDepth++;
i += 2;
continue;
}
if (text.charAt(i) === '}' && text.charAt(i + 1) === '}' && templateDepth > 0) {
templateDepth--;
i += 2;
continue;
}
if (text.charAt(i) === '[' && text.charAt(i + 1) === '[') {
linkDepth++;
i += 2;
continue;
}
if (text.charAt(i) === ']' && text.charAt(i + 1) === ']' && linkDepth > 0) {
linkDepth--;
i += 2;
continue;
}
if (text.charAt(i) === '|' && templateDepth === 0 && linkDepth === 0) {
parts.push(text.slice(start, i));
start = i + 1;
}
i++;
}
parts.push(text.slice(start));
return parts;
}
function getTemplateMatchAt(text, start, nameRe) {
var end = findBalancedTemplateEnd(text, start);
var parts;
var rawName;
var name;
if (end < 0) return null;
parts = splitTemplateTopLevelParts(text.slice(start + 2, end - 2));
rawName = String(parts.shift() || '').trim();
name = rawName.replace(/^(?:subst|подст)\s*:\s*/i, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
if (!nameRe.test(name)) return null;
return {
start: start,
end: end,
text: text.slice(start, end),
name: rawName,
params: parts.map(function (part) { return part.trim(); })
};
}
function findTemplateByPattern(text, namePattern) {
var source = String(text || '');
var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i');
var i = 0;
var end;
var match;
while (i < source.length - 1) {
if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') {
match = getTemplateMatchAt(source, i, nameRe);
if (match) return match;
end = findBalancedTemplateEnd(source, i);
if (end > i) { i = end; continue; }
}
i++;
}
return null;
}
function hasTemplateWithDateByPattern(text, namePattern, dateIso) {
var source = String(text || '');
var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i');
var targetDate = convertToStandardDate(dateIso);
var i = 0;
var end;
var match;
var templateDate;
if (!targetDate) return false;
while (i < source.length - 1) {
if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') {
match = getTemplateMatchAt(source, i, nameRe);
if (match) {
templateDate = convertToStandardDate(String(match.params[0] || '').replace(/^\s*1\s*=\s*/, ''));
if (templateDate === targetDate) return true;
i = match.end;
continue;
}
end = findBalancedTemplateEnd(source, i);
if (end > i) { i = end; continue; }
}
i++;
}
return false;
}
function getTemplateRemovalRange(text, match) {
var start = match.start;
var end = match.end;
var before = text.slice(0, start).match(/<noinclude>\s*$/i);
var after;
if (before) {
after = text.slice(end).match(/^\s*<\/noinclude>\s*\n?/i);
if (after) {
return { start: before.index, end: end + after[0].length };
}
}
after = text.slice(end).match(/^[ \t]*(?:\r?\n)?/);
if (after) end += after[0].length;
return { start: start, end: end };
}
function stripTemplatesByPattern(text, namePattern) {
var source = String(text || '');
var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i');
var ranges = [];
var out = [];
var pos = 0;
var i = 0;
var end;
var match;
var range;
while (i < source.length - 1) {
if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') {
match = getTemplateMatchAt(source, i, nameRe);
if (match) {
range = getTemplateRemovalRange(source, match);
ranges.push(range);
i = Math.max(match.end, range.end);
continue;
}
end = findBalancedTemplateEnd(source, i);
if (end > i) { i = end; continue; }
}
i++;
}
if (!ranges.length) return { text: source, removed: false };
ranges.forEach(function (r) {
if (r.start < pos) r.start = pos;
out.push(source.slice(pos, r.start));
pos = r.end;
});
out.push(source.slice(pos));
return { text: out.join(''), removed: true };
}
function removeTemplatesByAliases(text, aliases) {
var seen = {}, patterns = [];
aliases.forEach(function (alias) {
var tpl = alias.replace(RE_TEMPLATE_NS, '').trim();
var key = tpl.toLowerCase();
if (!tpl || seen[key]) return;
seen[key] = true;
patterns.push(escapeRegExp(tpl).replace(/\\ /g, '[ _]+'));
});
if (!patterns.length) return { text: text, removed: false };
return stripTemplatesByPattern(text, '(?:' + patterns.join('|') + ')');
}
function removeTransferTemplatesLocal(articleText, transferMode) {
var result = { text: articleText, removedKbu: false, removedKul: false, removedHangon: false };
if (transferMode === 'none') return result;
if (transferMode === 'kbu' || transferMode === 'both') {
var kbu = stripTemplatesByPattern(result.text, '(?:' + KBU_PATTERN_STR + ')');
result.text = kbu.text; result.removedKbu = kbu.removed;
}
if (transferMode === 'kul' || transferMode === 'both') {
var kul = stripTemplatesByPattern(result.text, KUL_PATTERN_STR);
result.text = kul.text; result.removedKul = kul.removed;
}
var hangon = stripTemplatesByPattern(result.text, HANGON_PATTERN_STR);
result.text = hangon.text; result.removedHangon = hangon.removed;
return result;
}
function removeTransferTemplatesWithApiFallback(pageName, articleText, transferMode, localResult, callback) {
var needKbu = (transferMode === 'kbu' || transferMode === 'both') && !localResult.removedKbu;
var needKul = (transferMode === 'kul' || transferMode === 'both') && !localResult.removedKul;
var needHangon = transferMode !== 'none' && !localResult.removedHangon;
if (!needKbu && !needKul && !needHangon) { callback(localResult); return; }
var titleMap = {};
if (needKbu) ['Шаблон:К быстрому удалению','Шаблон:К отсроченному удалению','Шаблон:Deleteslow','Шаблон:Ds'].forEach(function (t) { titleMap[t] = true; });
if (needKul) titleMap['Шаблон:К улучшению'] = true;
if (needHangon) { titleMap['Шаблон:Hangon'] = true; titleMap['Шаблон:Hang-on'] = true; }
apiReq({ prop: 'templates', titles: pageName, tllimit: 'max' }, 'query', function (data) {
var page = getFirstQueryPage(data);
if (page && page.templates) {
page.templates.forEach(function (tpl) {
var norm = normalizeTemplateName(tpl.title);
if ((needKbu && (RE_KBU_PATTERNS.test(norm) || norm === 'к быстрому удалению' || norm === 'к отсроченному удалению' || norm === 'deleteslow' || norm === 'ds')) ||
(needKul && RE_KUL_PATTERN.test(norm)) ||
(needHangon && RE_HANGON.test(norm))) {
titleMap[tpl.title] = true;
}
});
}
var titles = Object.keys(titleMap);
if (!titles.length) { callback(localResult); return; }
function normalizeAliasKey(title) { return (title || '').replace(/_/g, ' ').toLowerCase().trim(); }
function collectAndApplyAliases() {
var allAliases = [];
titles.forEach(function (title) { allAliases = allAliases.concat(tplAliasCache[title] || [title]); });
var dedup = {}, aliases = [];
allAliases.forEach(function (alias) {
var key = normalizeAliasKey(alias);
if (!key || dedup[key]) return;
dedup[key] = true; aliases.push(alias);
});
var updated = $.extend({}, localResult);
var r = removeTemplatesByAliases(updated.text, aliases);
updated.text = r.text;
if (r.removed) {
if (needKbu) updated.removedKbu = true;
if (needKul) updated.removedKul = true;
if (needHangon) updated.removedHangon = true;
}
callback(updated);
}
var missingTitles = titles.filter(function (t) { return !tplAliasCache[t]; });
if (!missingTitles.length) { collectAndApplyAliases(); return; }
(function resolveChunk(offset) {
if (offset >= missingTitles.length) { collectAndApplyAliases(); return; }
var chunk = missingTitles.slice(offset, offset + 20);
var chunkByKey = {};
chunk.forEach(function (t) { chunkByKey[normalizeAliasKey(t)] = t; });
apiReq({ prop: 'redirects', rdlimit: 'max', titles: chunk.join('|') }, 'query', function (resp) {
var pages = resp && resp.query && resp.query.pages ? resp.query.pages : {};
Object.keys(pages).forEach(function (pid) {
var p = pages[pid];
if (!p || !p.title) return;
var sourceTitle = chunkByKey[normalizeAliasKey(p.title)];
if (!sourceTitle) return;
var found = [p.title].concat((p.redirects || []).map(function (r) { return r.title; }));
var seen = {}, unique = [];
found.forEach(function (t) { var k = normalizeAliasKey(t); if (!k || seen[k]) return; seen[k] = true; unique.push(t); });
tplAliasCache[sourceTitle] = unique.length ? unique : [sourceTitle];
});
chunk.forEach(function (t) { if (!tplAliasCache[t]) tplAliasCache[t] = [t]; });
resolveChunk(offset + 20);
});
}(0));
});
}
// ─── Вставка шаблонов ────────────────────────────────────────────────────
function findInsertPositionAfterProjectTemplates(text) {
var pos = 0, len = text.length;
while (pos < len) {
var wsMatch = text.slice(pos).match(/^[\t ]*\n/);
if (wsMatch) { pos += wsMatch[0].length; continue; }
if (text.charAt(pos) !== '{' || text.charAt(pos + 1) !== '{') break;
var afterOpen = text.slice(pos + 2);
var nameMatch = afterOpen.match(/^[\s]*([\s\S]*?)[\s]*(?:\||\}\})/);
var templateEnd;
if (!nameMatch) break;
var tplName = nameMatch[1].toLowerCase().replace(/\s+/g, ' ').trim();
if (!/^статья проекта(\s|$)/.test(tplName) && tplName !== 'блок проектов статьи') break;
templateEnd = findBalancedTemplateEnd(text, pos);
if (templateEnd < 0) break;
pos = templateEnd;
if (pos < len && text.charAt(pos) === '\n') pos++;
}
return pos;
}
function insertTplOnTalkPage(text, tplText, sep) {
var s = (sep === undefined) ? '\n' : sep;
var insertPos = findInsertPositionAfterProjectTemplates(text);
if (insertPos === 0) return tplText + (text.length ? s + text.replace(/^\n+/, '') : '');
var before = text.slice(0, insertPos).replace(/\n+$/, '');
var after = text.slice(insertPos).replace(/^\n+/, '');
return before + '\n' + tplText + (after.length ? s + after : '');
}
function wrapInNoinclude(text, templateText) {
var match = text.match(RE_NOINCLUDE);
if (match) {
// Если перед noinclude есть непробельный контент — вставляем новый noinclude сверху
var before = text.slice(0, text.indexOf(match[0]));
if (/\S/.test(before)) {
return '<noinclude>' + templateText + '</noinclude>\n' + text;
}
var content = match[2];
if (content.length > 0 && content.charAt(content.length - 1) !== '\n') content += '\n';
return text.replace(match[0], match[1] + '<noinclude>' + content + templateText + '\n</noinclude>');
}
return '<noinclude>' + templateText + '</noinclude>\n' + text;
}
function upsertRetTemplateOnTalkPage(text, dateIso, sectionTitle) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var tplRe = /\{\{\s*(?:subst\s*:\s*)?(?:оставлено)\s*([^}]*)\}\}/i;
var tplMatch = source.match(tplRe);
function buildTpl(dateValue, sectionValue) {
var tpl = 'оставлено|' + dateValue;
if (sectionValue) tpl += '|l1=' + sectionValue;
return T_OPEN + tpl + T_CLOSE;
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, buildTpl(dateIso, normalizedSection), '\n'), status: 'created' };
}
var parts = tplMatch[1].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); });
var stdDate = convertToStandardDate(dateIso);
if (existingDates.indexOf(stdDate) !== -1) return { text: source, status: 'already_present' };
var nextIdx = existingDates.length + 1;
var suffix = '|' + dateIso + (normalizedSection ? '|l' + nextIdx + '=' + normalizedSection : '');
return {
text: source.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}\s*$/, suffix + '}}'); }),
status: 'updated'
};
}
function buildConditionalRetTemplateText(dateIso, sectionTitle, reasonText, deadlineText, sectionIndex) {
var normalizedSection = String(sectionTitle || '').trim();
var normalizedReason = normalizeQuickPhraseValue(reasonText);
var normalizedDeadline = String(deadlineText || '').trim();
var index = parseInt(sectionIndex, 10);
var tpl = 'условно оставлено|' + dateIso;
if (isNaN(index) || index < 1) index = 1;
if (normalizedSection) tpl += '|l' + index + '=' + normalizedSection;
if (normalizedReason) tpl += '|пояснение=' + normalizedReason;
if (normalizedDeadline) tpl += '|срок=' + normalizedDeadline;
return T_OPEN + tpl + T_CLOSE;
}
function upsertConditionalRetTemplateOnTalkPage(text, dateIso, sectionTitle, reasonText, deadlineText) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var normalizedReason = normalizeQuickPhraseValue(reasonText);
var normalizedDeadline = String(deadlineText || '').trim();
var tplRe = /\{\{\s*(?:subst\s*:\s*)?(?:условно\s*оставлено)\s*([^}]*)\}\}/i;
var tplMatch = source.match(tplRe);
if (!tplMatch) {
return {
text: insertTplOnTalkPage(source, buildConditionalRetTemplateText(dateIso, normalizedSection, normalizedReason, normalizedDeadline, 1), '\n'),
status: 'created'
};
}
var parts = tplMatch[1].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); });
var stdDate = convertToStandardDate(dateIso);
if (existingDates.indexOf(stdDate) !== -1) return { text: source, status: 'already_present' };
var nextIdx = existingDates.length + 1;
var suffix = '|' + dateIso;
if (normalizedSection) suffix += '|l' + nextIdx + '=' + normalizedSection;
if (normalizedReason) suffix += '|пояснение=' + normalizedReason;
if (normalizedDeadline) suffix += '|срок=' + normalizedDeadline;
return {
text: source.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}\s*$/, suffix + '}}'); }),
status: 'updated'
};
}
// ═══════════════════════════════════════════════════════════════════════════
// ПАЙПЛАЙН НОМИНАЦИИ
// ═══════════════════════════════════════════════════════════════════════════
function runNominationPipeline(steps) {
var s = steps;
var ctx = { templateMeta: null, nominationInfo: null };
var stages = [
{
name: 'шаблон',
fn: function (next) {
s.templateStep(function (err, meta) { ctx.templateMeta = meta || null; next(err); });
}
},
{
name: 'номинация',
pendingText: 'Публикуется номинация...',
successText: 'Номинация опубликована.',
errorText: 'Публикация номинации.',
fn: function (next) {
s.nominationStep(function (err, info) { ctx.nominationInfo = info || null; next(err); });
}
},
{
name: 'подписка',
shouldRun: function () {
var info = ctx.nominationInfo;
return !!(setSubscribe && info && info.pageTitle && info.sectionTitle);
},
fn: function (next) {
subscribeToTopic(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle, function () { next(); });
}
},
{
name: 'оповещение',
shouldRun: function () { return !!(setAlert && !s.skipNotify); },
fn: function (next) { s.notifyStep(ctx.nominationInfo, next); }
}
];
(function run(i) {
if (i >= stages.length) { if (typeof s.onSuccess === 'function') s.onSuccess(ctx); return; }
var stage = stages[i];
if (typeof stage.shouldRun === 'function' && !stage.shouldRun()) { run(i + 1); return; }
var statusId = stage.pendingText
? logStatus(stage.pendingText, null, { pending: true, trackError: false })
: null;
try {
stage.fn(function (err) {
if (err) {
if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), err, { statusId: statusId });
if (typeof s.onFailure === 'function') s.onFailure(stage.name, err, ctx);
else markSubmitError();
return;
}
if (statusId && stage.successText) logStatus(stage.successText, null, { statusId: statusId, trackError: false });
run(i + 1);
});
} catch (ex) {
var exErr = { code: 'exception', info: (ex && ex.message) ? ex.message : String(ex) };
if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), exErr, { statusId: statusId });
if (typeof s.onFailure === 'function') s.onFailure(stage.name, exErr, ctx);
else markSubmitError();
}
}(0));
}
// ─── Публикация номинации ────────────────────────────────────────────────
function publishNomination(opts, callback) {
var cb = callback || function () {};
var maxRetries = Math.max(0, parseInt(opts.editConflictRetries, 10) || 1);
function doPublish() {
apiReq({
title: opts.pageTitle,
section: 'new',
sectiontitle: opts.sectionTitle,
summary: opts.summary,
text: opts.text,
assertuser: mwCfg.wgUserName
}, 'edit', function (resp) {
cb(resp && resp.error ? resp.error : null);
});
}
if (opts.sectionTitle) {
if (!opts.navTemplate) { doPublish(); return; }
apiReq({ title: opts.pageTitle, createonly: '1', text: T_OPEN + opts.navTemplate + '-Навигация' + T_CLOSE + '\n', summary: makeSummary('автоматическая шапка'), assertuser: mwCfg.wgUserName },
'edit', function (resp) {
if (resp && resp.error && resp.error.code !== 'articleexists') { cb(resp.error); return; }
doPublish();
});
return;
}
// Вставка в существующую страницу
if (opts.createText !== undefined) {
(function attempt(retry) {
getTextWithTimestamp(opts.pageTitle, function (pageText, baseTimestamp, readErr) {
var result;
var ep;
if (readErr) { cb(makeReadError(readErr, 'read_failed', opts.readErrorMessage || 'Не удалось получить содержимое.')); return; }
result = pageText === null
? (typeof opts.createText === 'function' ? opts.createText() : opts.createText)
: (opts.buildText ? opts.buildText(pageText) : null);
if (typeof result === 'string') result = { text: result };
if (!result || result.error) { cb((result && result.error) || { code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
if (result.skip) { cb(null); return; }
if (typeof result.text !== 'string') { cb({ code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
ep = {
title: opts.pageTitle,
text: result.text,
summary: result.summary || opts.summary,
assertuser: mwCfg.wgUserName
};
if (pageText === null) ep.createonly = true;
else if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) {
var err = resp && resp.error ? resp.error : null;
if (err && (err.code === 'editconflict' || err.code === 'articleexists') && retry < maxRetries) {
attempt(retry + 1);
return;
}
cb(err);
});
});
}(0));
return;
}
editPageContent(opts.pageTitle, { summary: opts.summary, readError: opts.readErrorMessage || 'Не удалось получить содержимое.' },
function (pageText) { return opts.buildText ? opts.buildText(pageText) : null; },
function (err) { cb(err || null); }
);
}
// ─── Оповещение авторов ──────────────────────────────────────────────────
function notifyAuthor(pg, options, callback) {
var opts = options || {};
var cb = callback || function () {};
var actionText = (typeof opts.actionText === 'string') ? opts.actionText : '';
var discussionPage = (typeof opts.discussionPage === 'string') ? opts.discussionPage : '';
var discussionSection = normalizeSectionForLink((typeof opts.discussionSection === 'string') ? opts.discussionSection : '');
var includeProposed = (typeof opts.includeProposedPrefix === 'boolean') ? opts.includeProposedPrefix : true;
var actionPhrase = ((includeProposed ? 'предложена ' : '') + actionText).trim() || 'изменена';
var discussionText = discussionPage ? 'Обсуждение — на странице [[' + discussionPage + (discussionSection ? '#' + discussionSection : '') + ']]. ' : '';
apiReq({ prop: 'revisions', rvprop: 'user', rvdir: 'newer', titles: pg }, 'query', function (queryResp) {
var page = getFirstQueryPage(queryResp);
if (!page) { cb({ code: 'network', info: 'Network error' }); return; }
if (page.missing !== undefined) { cb({ code: 'missing', info: 'Page missing.' }); return; }
if (!page.revisions || !page.revisions.length) { cb({ code: 'no_revisions', info: 'No revisions.' }); return; }
var rv = page.revisions[0];
if ('anon' in rv || rv.userhidden || !rv.user || rv.user === mwCfg.wgUserName) { cb(null); return; }
apiReq({
title: 'User talk:' + rv.user, section: 'new',
sectiontitle: 'Remover: [[:' + pg + ']]',
summary: opts.summary || makeSummary('уведомление автора'),
text: 'Страница [[:' + pg + ']], созданная вами, ' + actionPhrase + '. ' +
discussionText +
'~~' + '~~<br><small>Это автоматическое уведомление, сгенерированное ' + scriptLink + '.</small>',
assertuser: mwCfg.wgUserName
}, 'edit', function (editResp) { cb(editResp && editResp.error ? editResp.error : null); });
});
}
function notifyAuthorsForPages(pages, notifyOptions, callback) {
var cb = callback || function () {};
var opts = notifyOptions || {};
var list = [];
(pages || []).forEach(function (p) { var t = normTitle(p); if (t && list.indexOf(t) === -1) list.push(t); });
if (!list.length) { cb(); return; }
eachSequential(list, function (pg, next) {
var pageLink = buildQuotedStatusPageLink(pg);
var statusId = logStatus('Отправляется уведомление создателю страницы ' + pageLink + '...', null, { pending: true, trackError: false });
notifyAuthor(pg, opts, function (err) {
logStatus(err ? 'Уведомление создателя страницы ' + pageLink + '.' : 'Создатель страницы ' + pageLink + ' уведомлён.', err,
{ statusId: statusId, trackError: opts.trackError !== false });
next();
});
}, cb);
}
// ─── Подписка на раздел ──────────────────────────────────────────────────
function subscribeToTopic(pageTitle, sectionTitle, callback) {
var cb = callback || function () {};
if (!setSubscribe || !sectionTitle) { cb(); return; }
var statusId = logStatus('Оформляется подписка на раздел...', null, { pending: true, trackError: false });
var targetFrag = normalizeSectionForLink(sectionTitle).toLowerCase();
function finish(err, st) {
if (err) { logStatus('Не удалось подписаться на раздел.', err, { statusId: statusId, trackError: false }); cb(); return; }
logStatus(st === 'subscribed' ? 'Оформлена подписка на раздел.' : 'Раздел для подписки не найден.', null, { statusId: statusId, trackError: false });
cb();
}
function trySubscribe(attemptsLeft) {
apiReq({ page: pageTitle, prop: 'threaditemshtml', excludesignatures: true }, 'discussiontoolspageinfo', function (data) {
var items = (data && data.discussiontoolspageinfo && data.discussiontoolspageinfo.threaditemshtml) || null;
if (!items || !items.length) {
if (attemptsLeft > 0) { setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000); return; }
finish(null, 'not_found'); return;
}
var commentname = null;
for (var ti = items.length - 1; ti >= 0; ti--) {
var t = items[ti];
if (t.type === 'heading') {
var htext = (t.headingText || t.html || '').replace(/<[^>]+>/g, '').trim();
if (htext.toLowerCase() === targetFrag || normalizeSectionForLink(htext).replace(/_/g, ' ').toLowerCase() === targetFrag) {
commentname = t.name; break;
}
}
}
if (!commentname) {
if (attemptsLeft > 0) setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000);
else finish(null, 'not_found');
return;
}
apiReq({ page: pageTitle, commentname: commentname, subscribe: '1' }, 'discussiontoolssubscribe', function (res) {
finish(res && res.error ? res.error : null, 'subscribed');
});
});
}
setTimeout(function () { trySubscribe(2); }, 1500);
}
// ═══════════════════════════════════════════════════════════════════════════
// UI: модальные окна
// ═══════════════════════════════════════════════════════════════════════════
function syncModalLayout() {
var syncFn = $('#removerModal').data('rmSyncLayout');
if (typeof syncFn === 'function') syncFn();
}
function clearModalLayoutSyncHandlers() {
modalLayoutSyncHandlers = [];
$('#removerModal').removeData('rmSyncLayout');
}
function registerModalLayoutSync(handler) {
if (typeof handler !== 'function') return;
if (modalLayoutSyncHandlers.indexOf(handler) === -1) modalLayoutSyncHandlers.push(handler);
$('#removerModal').data('rmSyncLayout', function () {
modalLayoutSyncHandlers.slice().forEach(function (fn) {
if (typeof fn === 'function') fn();
});
});
}
function registerResizeObserver(observer) {
if (observer) resizeObservers.push(observer);
}
function resetModalObservers() {
resizeObservers.forEach(function (observer) {
if (observer && typeof observer.disconnect === 'function') observer.disconnect();
});
resizeObservers = [];
clearModalLayoutSyncHandlers();
$(window).off('resize.removerModal');
$(window).off('.rmTaResize');
}
function closeModal() {
resetModalObservers();
$(window).off('keydown.remover');
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
}
function ensureModalStyles() {
if (document.getElementById('removerModalDynamicStyles')) return;
var progH = 'background:' + tk.bgProgH + '!important;border-color:' + tk.bProgH + '!important;color:' + tk.cInv + '!important;';
var neutH = 'background:' + tk.bgN + '!important;border-color:' + tk.bSub + '!important;color:inherit!important;';
var succH = 'background:' + tk.bgSuccH+ '!important;border-color:' + tk.bSuccH + '!important;color:' + tk.cInv + '!important;';
var pillBg = 'linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%)';
var pillShadow = '0 1px 0 rgba(255,255,255,.08) inset,0 1px 2px rgba(0,0,0,.18)';
var activePillShadow = '0 1px 0 rgba(255,255,255,.14) inset,0 2px 6px rgba(51,102,204,.24)';
var css = [
'#removerModal,#removerModal *{-moz-text-size-adjust:none!important;-webkit-text-size-adjust:100%!important;text-size-adjust:100%!important}',
'#removerModal{color:inherit}',
'#removerModal input::placeholder,#removerModal textarea::placeholder{color:var(--color-subtle,currentColor);opacity:.7}',
'#removerModal input[type="checkbox"],#removerModal input[type="radio"]{appearance:auto;-webkit-appearance:auto;-moz-appearance:auto;accent-color:auto}',
'#removerModal input[type="checkbox"]{outline:none!important;box-shadow:none!important}',
'#removerModal button{transition:background-color .12s ease,border-color .12s ease,color .12s ease,box-shadow .12s ease,filter .12s ease,transform .06s ease}',
'#removerModal .rmToggleBtn{background:' + tk.bgNSub + '!important;border-color:' + tk.bSub + '!important;color:inherit!important}',
'#removerModal .rmToggleBtn.is-active{background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important}',
'#removerModal button:not(:disabled):hover{filter:brightness(.97)}',
'#removerModal button:not(:disabled):active{transform:translateY(1px)}',
'#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):hover,#removerModal .rmToggleBtn.is-active:hover{' + progH + 'filter:none}',
'#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):active,#removerModal .rmToggleBtn.is-active:active{' + progH + 'filter:brightness(.92)!important}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError){background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important;outline:none!important;box-shadow:0 0 0 6px rgba(51,102,204,.13),0 1px 2px rgba(0,0,0,.08)!important}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):hover{' + progH + 'box-shadow:0 0 0 7px rgba(51,102,204,.16),0 1px 2px rgba(0,0,0,.1)!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):active{' + progH + 'box-shadow:0 0 0 5px rgba(51,102,204,.14),0 1px 2px rgba(0,0,0,.08)!important;filter:brightness(.92)!important}',
'#removerModal .rmMultiPageCommentToggle.is-active{background:#bfc4ca!important;border-color:#8f98a3!important;color:#202122!important}',
'#removerModal .rmMultiPageCommentToggle.is-active:hover{background:#b4bac1!important;border-color:#848e99!important;color:#202122!important;filter:none}',
'#removerModal .rmMultiPageCommentToggle.is-active:active{background:#a9b0b8!important;border-color:#79838f!important;color:#202122!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitError:not(:disabled):hover{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitError:not(:disabled):active{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:brightness(.88)!important}',
'#removerModal #removerReload:not(:disabled):hover{' + succH + 'filter:none}',
'#removerModal #removerReload:not(:disabled):active{' + succH + 'filter:brightness(.92)!important}',
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):hover,#removerModal .rmToggleBtn:not(.is-active):hover{' + neutH + 'filter:none}',
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):active{' + neutH + 'filter:brightness(.92)!important}',
'#removerModal a.removerModalLink{color:' + tk.cProg + ';text-decoration:none;border-bottom:0;box-shadow:none;word-break:break-word;overflow-wrap:anywhere}',
'#removerModal a.removerModalLink:hover,#removerModal a.removerModalLink:focus{color:' + tk.cProgH + ';text-decoration:underline}',
'#removerModal #removerModalSubtitle{font-size:12px!important;line-height:1.35!important;font-weight:400!important;color:' + tk.cSubM + '!important;max-width:100%;overflow-wrap:anywhere;word-break:break-word}',
'#removerModal #removerModalSubtitle a{font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important}',
'#removerModal a.rmButtonLikeLink{color:' + tk.cBase + '!important;text-decoration:none!important;transform:none!important;transition:background-color .12s ease,border-color .12s ease,color .12s ease,filter .12s ease,transform .06s ease!important}',
'#removerModal a.rmButtonLikeLink:hover{' + neutH + 'text-decoration:none!important;transform:none!important}',
'#removerModal a.rmButtonLikeLink:focus{text-decoration:none!important}',
'#removerModal a.rmButtonLikeLink:focus:not(:focus-visible){outline:none!important}',
'#removerModal a.rmButtonLikeLink:focus-visible{outline:2px solid ' + tk.bProg + '!important;outline-offset:2px;text-decoration:none!important}',
'#removerModal a.rmButtonLikeLink:hover:active{' + neutH + 'filter:brightness(.92)!important;transform:translateY(1px)!important;text-decoration:none!important}',
'#removerModal .rmInfoBox{margin:0 0 10px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgNSub + '}',
'#removerModal .rmActionList{display:flex;flex-direction:column;gap:6px}',
'#removerModal .rmActionItem{display:block;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgBase + ';cursor:pointer;transition:background-color .12s ease,transform .06s ease}',
'#removerModal .rmActionItem:hover{background:' + tk.bgNSub + '}',
'#removerModal .rmActionItem:active{transform:translateY(1px)}',
'#removerModal .rmActionMain{display:flex;align-items:center}',
'#removerModal .rmActionMain input[type="radio"]{margin-right:8px}',
'#removerModal .rmActionMeta{display:block;margin-left:24px;margin-top:2px;color:' + tk.cSubM + ';font-size:12px;line-height:1.35}',
'#removerModal .rmConflictLead{margin:0 0 10px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}',
'#removerModal .rmConflictList{display:flex;flex-direction:column;gap:10px}',
'#removerModal .rmConflictCard{padding:12px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}',
'#removerModal .rmConflictCard.is-skip{background:' + tk.bgNSub + ';border-color:' + tk.bSubS + '}',
'#removerModal .rmConflictTitle{font-size:14px;font-weight:700;line-height:1.4;color:' + tk.cBase + ';word-break:break-word;overflow-wrap:anywhere}',
'#removerModal .rmConflictMeta{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmConflictGroup{margin-top:10px}',
'#removerModal .rmConflictGroupTitle{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmConflictButtons{display:flex;flex-wrap:wrap;gap:6px}',
'#removerModal .rmConflictChoice{padding:5px 10px}',
'#removerModal .rmConflictChoice.is-disabled,#removerModal .rmConflictButtons.is-disabled .rmConflictChoice{opacity:.55;cursor:not-allowed;pointer-events:none}',
'#removerModal .rmConflictHint{margin-top:8px;font-size:11px;line-height:1.45;color:' + tk.cSub + '}',
'#removerModal #rmSettingsForm{display:flex;flex-direction:column;gap:14px}',
'#removerModal .rmSettingsLead{margin:-2px 0 2px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}',
'#removerModal .rmSettingsSection{margin:0;padding:14px 16px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.7) inset}',
'#removerModal .rmSettingsSectionHeader{margin:0 0 12px}',
'#removerModal .rmSettingsSectionTitle{font-size:14px;font-weight:700;line-height:1.35;color:' + tk.cBase + '}',
'#removerModal .rmSettingsSectionDescription{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmSettingsField{display:block;margin:0 0 10px;padding:12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}',
'#removerModal .rmSettingsField:last-child{margin-bottom:0}',
'#removerModal .rmSettingsFieldLabel{display:block;font-size:13px;font-weight:700;line-height:1.35;margin:0 0 8px;color:' + tk.cBase + '}',
'#removerModal .rmSettingsFieldControl{display:block;min-width:0}',
'#removerModal .rmSettingsFieldControl input{margin-bottom:0!important}',
'#removerModal .rmSettingsFieldControl input[type="text"]{min-height:38px;border-radius:6px}',
'#removerModal .rmSettingsFieldHint{margin-top:8px;font-size:12px;line-height:1.55;color:' + tk.cSubM + ';overflow-wrap:anywhere}',
'#removerModal .rmSettingsChecks{display:flex;flex-direction:column;gap:8px}',
'#removerModal .rmSettingsCheck{display:inline-flex;align-items:flex-start;gap:8px;font-size:14px;line-height:1.45;color:' + tk.cBase + '}',
'#removerModal .rmSettingsCheck input[type="checkbox"]{margin:3px 0 0;flex-shrink:0}',
'#removerModal .rmSettingsMenuPresetWrap{margin-top:10px}',
'#removerModal .rmSettingsMenuPresetLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmSegmentedBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmSettingsMenuPresetBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmSegmentedBtn,#removerModal .rmSettingsMenuPresetBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}',
'#removerModal .rmSegmentedBtn.is-active,#removerModal .rmSettingsMenuPresetBtn.is-active{background:' + tk.bgProg + ';border-color:' + tk.bProg + ';color:' + tk.cInv + ';box-shadow:' + activePillShadow + '}',
'#removerModal.rmModalSettings{border:1px solid ' + tk.bSub + '!important;background:' + tk.bgBase + '!important;border-radius:12px!important;box-shadow:0 14px 32px rgba(0,0,0,.08),0 1px 0 rgba(255,255,255,.78) inset!important}',
'#removerModal.rmModalSettings #removerModalHeaderBar{margin-bottom:14px;padding-bottom:10px;border-bottom:2px solid ' + tk.bSub + '}',
'#removerModal.rmModalSettings #removerModalSubtitle{margin:-2px 0 12px!important;color:' + tk.cSubM + '!important}',
'#removerModal.rmModalSettings #rmSettingsForm{gap:18px}',
'#removerModal.rmModalSettings .rmSettingsLead{margin:0;padding:0 2px;color:' + tk.cSubM + '}',
'#removerModal.rmModalSettings .rmSettingsSection{padding:16px 18px;border:1px solid ' + tk.bSub + ';border-radius:12px;background:linear-gradient(180deg,' + tk.bgNSub + ' 0%,' + tk.bgBase + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.82) inset,0 8px 18px rgba(0,0,0,.035)}',
'#removerModal.rmModalSettings .rmSettingsSectionHeader{margin:0 0 6px;padding-bottom:0;border-bottom:0}',
'#removerModal.rmModalSettings .rmSettingsSectionTitle{font-size:16px;line-height:1.3}',
'#removerModal.rmModalSettings .rmSettingsSectionDescription{margin-top:5px;max-width:none}',
'#removerModal.rmModalSettings .rmSettingsField{margin:14px 0 0;padding:12px 0 0;border:0;border-top:1px solid ' + tk.bSubS + ';border-radius:0;background:transparent;box-shadow:none}',
'#removerModal.rmModalSettings .rmSettingsField:first-child{margin-top:0;padding-top:0;border-top:0}',
'#removerModal.rmModalSettings .rmSettingsFieldLabel{margin:0 0 6px}',
'#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]{min-height:40px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}',
'#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]:focus{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.16);outline:none}',
'#removerModal.rmModalSettings .rmSettingsFieldHint{margin-top:6px;max-width:none}',
'#removerModal.rmModalSettings .rmSettingsChecks{gap:10px}',
'#removerModal.rmModalSettings .rmSettingsCheck{padding:4px 0}',
'#removerModal.rmModalSettings .rmSettingsMenuPresetWrap{margin-top:12px;padding-top:10px;border-top:1px dashed ' + tk.bSubS + '}',
'#removerModal.rmModalSettings .rmSettingsHintList{width:100%;max-width:100%;box-sizing:border-box;margin-top:10px;padding:10px 12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + '}',
'#removerModal.rmModalSettings .rmSettingsHintRow{line-height:1.55}',
'#removerModal.rmModalSettings .rmSettingsHintBadge{background:' + tk.bgNSub + '}',
'#removerModal.rmModalSettings .rmQuickPhraseEditor{padding-top:2px}',
'#removerModal.rmModalSettings .rmQuickPhraseMeta{min-height:18px}',
'#removerModal.rmModalSettings #rmSettingsSignaturePreviewCode{display:inline-block;padding:2px 8px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}',
'#removerModal.rmModalSettings #rmFooterActionButtons{position:relative;flex:0 0 auto!important;display:flex!important;align-items:center!important;justify-content:flex-end!important;gap:6px!important;margin-left:auto!important;max-width:100%!important}',
'#removerModal.rmModalSettings #rmSettingsActionButtonsRow{display:flex;align-items:center;justify-content:flex-end;gap:6px;flex-wrap:nowrap}',
'#removerModal.rmModalSettings #rmSettingsUnsavedHint{display:none;position:absolute;top:100%;right:0;width:auto;box-sizing:border-box;margin:4px 0 0;color:' + tk.cSubM + ';opacity:.78;font-size:12px;line-height:1.35;text-align:right;white-space:nowrap}',
'#removerModal .rmProtectControlGroup{margin-top:12px;padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-sizing:border-box}',
'#removerModal .rmProtectControlLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmProtectControlGroup .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}',
'#removerModal .rmProtectControlGroup .rmSegmentedBtn{padding:4px 10px;font-size:12px}',
'#removerModal .rmTransferPanel{margin-top:10px;padding:0;border:0;background:transparent;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmTransferGrid{display:grid;grid-template-columns:max-content max-content;column-gap:10px;row-gap:6px;align-items:start;justify-content:start}',
'#removerModal .rmTransferGrid .rmSegmentedBar{justify-self:start}',
'#removerModal .rmTransferHintRow{grid-column:1 / -1;min-height:0}',
'#removerModal .rmTransferPanel .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}',
'#removerModal .rmTransferPanel .rmSegmentedBtn{padding:6px 14px;font-size:12px;font-weight:700}',
'#removerModal #rmTransferModeGroup{gap:0}',
'#removerModal #rmTransferModeGroup .rmSegmentedBtn:first-child{border-top-right-radius:0;border-bottom-right-radius:0}',
'#removerModal #rmTransferModeGroup .rmSegmentedBtn + .rmSegmentedBtn{margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}',
'#removerModal.rmCompactContent .rmMultiPageRow{flex-wrap:wrap!important;gap:6px}',
'#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageInput{flex:1 1 100%!important;width:100%!important}',
'#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageCommentToggle,#removerModal.rmCompactContent .rmMultiPageRow .rmAddMultiPage,#removerModal.rmCompactContent .rmMultiPageRow .rmRemoveInput{margin-left:0!important}',
'#removerModal.rmCompactContent .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(1){order:1}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(2){order:2}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(3){order:3}',
'#removerModal.rmCompactContent #rmTransferModeGroup{flex-direction:column;align-items:flex-start;gap:6px}',
'#removerModal.rmCompactContent #rmTransferModeGroup .rmSegmentedBtn{margin-left:0!important;border-radius:999px!important}',
'#removerModal.rmCompactContent .rmTransferHintRow{grid-column:auto}',
'#removerModal #rmProtectTextBlock{margin-top:14px}',
'#removerModal #rmSettingsMenuTitle:disabled{background:' + tk.bgDis + '!important;border-color:' + tk.bDis + '!important;color:' + tk.cDis + '!important;-webkit-text-fill-color:' + tk.cDis + ';opacity:1;cursor:not-allowed;box-shadow:none!important}',
'#removerModal .rmSettingsHintList{display:flex;flex-direction:column;gap:4px;margin-top:8px}',
'#removerModal .rmSettingsHintRow{font-size:12px;line-height:1.5;color:' + tk.cSubM + '}',
'#removerModal .rmSettingsHintBadge{display:inline-block;margin-right:6px;padding:1px 6px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';font-size:11px;font-weight:700;color:' + tk.cSubM + ';vertical-align:baseline}',
'#removerModal .rmQuickPhraseEditor{display:flex;flex-direction:column;gap:10px}',
'#removerModal .rmQuickPhraseList,#removerModal .rmQuickPhrasesPanel{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-start}',
'#removerModal .rmQuickPhraseChip{position:relative;display:inline-flex;align-items:center;gap:4px;max-width:100%;padding:2px 4px 2px 10px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';box-shadow:' + pillShadow + ';transition:border-color .12s,box-shadow .12s,opacity .12s;overflow:visible}',
'#removerModal .rmQuickPhraseChip.is-editing{opacity:.42;border-style:dashed}',
'#removerModal .rmQuickPhraseChip.is-dragging{opacity:.65}',
'#removerModal .rmQuickPhraseChip.is-drop-before::before,#removerModal .rmQuickPhraseChip.is-drop-after::after{content:"";position:absolute;top:50%;width:3px;height:24px;border-radius:999px;background:' + tk.cProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.08)}',
'#removerModal .rmQuickPhraseChip.is-drop-before::before{left:-4px;transform:translate(-50%,-50%)}',
'#removerModal .rmQuickPhraseChip.is-drop-after::after{right:-4px;transform:translate(50%,-50%)}',
'#removerModal .rmQuickPhraseEditBtn{max-width:100%;padding:3px 0;border:0;background:transparent;color:' + tk.cBase + ';font-size:13px;line-height:1.35;cursor:pointer;text-align:left;white-space:normal}',
'#removerModal .rmQuickPhraseRemoveBtn{width:24px;height:24px;padding:0;border:0;border-radius:999px;background:transparent;color:' + tk.cSubM + ';font-size:18px;line-height:1;cursor:pointer;flex-shrink:0}',
'#removerModal .rmQuickPhraseRemoveBtn:hover{background:' + tk.bgN + ';color:' + tk.cBase + '}',
'#removerModal #rmSettingsQuickPhraseInput.is-editing{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.12)}',
'#removerModal .rmQuickPhraseMeta{font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmQuickPhraseEmpty{padding:2px 0;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmQuickPhrasesPanel{margin-top:8px}',
'#removerModal .rmQuickPhraseActionBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}',
'#removerModal .rmQuickPhraseActionBtn:hover{border-color:' + tk.bProg + ';color:' + tk.cProg + '}',
'@media (max-width:' + sz.mobileBp + 'px){',
'#removerModal button{white-space:normal!important}',
'#removerModal #rmFooterButtons{align-items:flex-start!important}',
'#removerModal #rmFooterCheckboxes,#removerModal #rmFooterActionButtons{width:100%!important;max-width:100%!important;margin-left:0!important}',
'#removerModal .rmSettingsSection{padding:12px 13px}',
'#removerModal .rmSettingsField{padding:10px}',
'#removerModal.rmModalSettings #rmSettingsUnsavedHint{max-width:100%;margin:4px 0 0;text-align:right;white-space:normal}',
'#removerModal .rmTransferPanel{padding:0}',
'#removerModal .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}',
'#removerModal .rmTransferHintRow{grid-column:auto}',
'#removerModal .rmQuickPhraseChip{max-width:100%}',
'}'
].join('');
var style = document.createElement('style');
style.id = 'removerModalDynamicStyles';
style.textContent = css;
document.head.appendChild(style);
}
function applyV2022Layout($modal, explicitWidth) {
if (!isVector22) return;
var css = { 'max-width': '100%', 'box-sizing': 'border-box', 'overflow-wrap': 'anywhere' };
if (typeof explicitWidth === 'number') css['max-width'] = explicitWidth + 'px';
$modal.css(css);
}
function getPageUrl(pageTitle) {
return (mw.util && typeof mw.util.getUrl === 'function')
? mw.util.getUrl(pageTitle)
: '/wiki/' + encodeURIComponent((pageTitle || '').replace(/ /g, '_'));
}
function getPageUrlWithFragment(pageTitle, fragment) {
var url = getPageUrl(pageTitle);
var frag = normalizeSectionForLink(fragment);
if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_'));
return url;
}
function buildStatusPageLink(pageName) {
var title = normTitle(pageName);
return '<a href="' + escapeHtml(getPageUrl(title)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(title) + '</a>';
}
function buildQuotedStatusPageLink(pageName) {
return '«' + buildStatusPageLink(pageName) + '»';
}
function buildHeaderIconButtonHtml(id, title, label, text) {
return joinHtml([
'<button id="', id, '" type="button" title="', escapeHtml(title), '" aria-label="', escapeHtml(label || title), '" ',
'style="', stHeaderIconBtn, '">', text || '', '</button>'
]);
}
function createModal(opts) {
if (typeof opts === 'string') opts = { title: opts };
var layout = getModalLayout();
resetModalObservers();
ensureModalStyles();
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
var subtitleHtml = '';
var subtitleStyle = 'margin:-4px 0 8px;font-size:12px!important;color:' + tk.cSubM + ';line-height:1.35!important;font-weight:400!important;';
var subtitleLinkStyle = 'font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important;';
if (opts.subtitleHtml) {
subtitleHtml = joinHtml([
'<div id="removerModalSubtitle" style="', subtitleStyle, '">',
opts.subtitleHtml,
'</div>'
]);
} else if (opts.subtitlePage) {
subtitleHtml = joinHtml([
'<div id="removerModalSubtitle" style="', subtitleStyle, '">',
opts.subtitleLabel || 'Текущий день',
': <a href="', getPageUrl(opts.subtitlePage), '" target="_blank" rel="noopener noreferrer" class="removerModalLink" style="', subtitleLinkStyle, '">',
normTitle(opts.subtitlePage),
'</a></div>'
]);
}
var settingsButtonHtml = opts.showSettingsButton === false ? '' :
buildHeaderIconButtonHtml('removerSettingsTrigger', 'Конфигурация', 'Конфигурация', '⚙');
var display = opts.inline ? 'inline-block' : 'block';
var modalMargin = opts.inline ? '1em 0' : (layout.shouldCenter ? '1em auto' : '1em 0');
var inlineLayoutStyle = opts.inline ? ';justify-self:start;align-self:start;width:fit-content;' : '';
var modalStyle = joinHtml([
'position:relative;padding:1.5em;margin:', modalMargin, ';display:', display, ';',
'border:', stStyles.border, ';background:', stStyles.background,
';border-radius:', stStyles.borderRadius, ';box-shadow:', stStyles.boxShadow,
';max-width:100%;box-sizing:border-box;overflow-wrap:anywhere;', inlineLayoutStyle
]);
var headerStyle = 'display:flex;align-items:center;gap:10px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid ' + tk.bSubS + ';';
var titleStyle = 'color:' + stStyles.headerColor + ';margin:0;padding:0;border:0;display:block;font-size:1.3em;font-weight:400;line-height:1.25;flex:1 1 auto;min-width:0;';
$('#content').prepend(joinHtml([
'<div id="removerModal" style="', modalStyle, '">',
'<div id="removerModalHeaderBar" style="', headerStyle, '">',
'<h1 id="removerModalTitle" style="', titleStyle, '"><span id="removerModalTitleText">', opts.title, '</span></h1>',
settingsButtonHtml,
'</div>',
subtitleHtml,
'<div id="removerModalContent"></div>',
'<div id="removerModalFooter" style="margin-top:15px;"></div>',
'</div>'
]));
var $modal = $('#removerModal');
if (opts.width === 'compact') $modal.css({ width: layout.defaultOuterWidth + 'px', 'max-width': '100%', 'box-sizing': 'border-box' });
else applyV2022Layout($modal);
$('#removerSettingsTrigger').off('click').on('click', function () {
openSettings();
});
}
function buildFooterCheckboxHtml(name, checked, label) {
return joinHtml([
'<label style="', stFooterCheckLabel, '">',
'<input name="', name, '" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ',
checked ? 'checked' : '',
'>',
label,
'</label>'
]);
}
function buildFooterActionsHtml(buttonsHtml) {
return '<div id="rmFooterActionButtons" style="' + stFooterActions + '">' + buttonsHtml + '</div>';
}
function renderModalFooter(mode, options) {
var opts = options || {};
$('#removerModalFooter').css('width', '');
if (mode === 'submit') {
var showCb = opts.showCheckbox !== false;
var showSub = opts.showSubscribe === true;
var ns = mwCfg.wgNamespaceNumber;
var notifyLabel = ns === 0 ? 'Оповестить создателя статьи'
: (ns === 10 || ns === 11) ? 'Оповестить создателя шаблона'
: (ns === 14 || ns === 15) ? 'Оповестить создателя категории'
: 'Оповестить создателя страницы';
var cbInlineHtml = '';
if (showSub || showCb) {
cbInlineHtml = joinHtml([
'<div id="rmFooterCheckboxes" style="', stFooterChecks, '">',
showSub ? buildFooterCheckboxHtml('rmSubscribe', setSubscribe, 'Подписаться на номинацию') : '',
showCb ? buildFooterCheckboxHtml('rmUAlert', setAlert, notifyLabel) : '',
'</div>'
]);
}
$('#removerModalFooter').html(joinHtml([
'<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:', cbInlineHtml ? 'space-between' : 'flex-end', ';">',
cbInlineHtml,
buildFooterActionsHtml(joinHtml([
'<button id="removerCancel" style="', stCancel, '">Отмена</button>',
'<button id="removerSubmit" style="', stSubmit, '">', opts.submitText || 'ОК', '</button>'
])),
'</div>'
]));
$('#removerCancel').click(function () { closeModal(); });
$('#removerSubmit').data('rmSubmitInProgress', false).click(function () {
if ($(this).data('rmSubmitInProgress')) return;
$(this).removeClass('rmSubmitError').css({ background: '', 'border-color': '', color: '' });
isError = false;
if (!opts.preserveLogOnSubmit) {
$('#rmLogBox').empty();
logStatusSeq = 0;
}
if (showCb) { setAlert = $('[name="rmUAlert"]').is(':checked'); state.setAlert = setAlert; }
if (showSub) { setSubscribe = $('[name="rmSubscribe"]').is(':checked'); state.setSubscribe = setSubscribe; }
$(this).data('rmSubmitInProgress', true).prop('disabled', true);
var submitResult;
try { submitResult = opts.onSubmit(); } catch (ex) { unlockModalSubmit(); throw ex; }
if (submitResult === false) unlockModalSubmit();
});
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.ctrlKey && e.keyCode === 13) $('#removerSubmit').click();
});
} else if (mode === 'reload') {
var newBtns = buildFooterActionsHtml(joinHtml([
'<button id="removerCancel" style="', stCancel, '">Закрыть</button>',
'<button id="removerReload" style="', stReload, '">', opts.reloadText || 'Обновить страницу', '</button>'
]));
$('#rmFooterCheckboxes').remove();
var $btns = $('#rmFooterButtons');
if ($btns.length) {
$btns.css({ 'justify-content': 'flex-end' }).html(newBtns);
} else {
$('#removerModalFooter').append(joinHtml([
'<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:flex-end;">',
newBtns,
'</div>'
]));
}
$('#removerCancel').click(function () { closeModal(); });
$('#removerReload').click(function () { location.reload(); });
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.keyCode === 27) $('#removerCancel').click();
if (e.ctrlKey && e.keyCode === 13) $('#removerReload').click();
});
} else { // 'close'
$('#removerModalFooter').html(joinHtml([
'<div style="display:flex;justify-content:flex-end;align-items:center;">',
'<button id="removerCancel" style="', stCancel, 'margin-right:0;">', opts.closeText || 'Закрыть', '</button>',
'</div>'
]));
$('#removerCancel').click(function () { closeModal(); });
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.keyCode === 27) $('#removerCancel').click();
});
}
}
function unlockModalSubmit() {
$('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false);
}
function markSubmitError() {
isError = true;
var errColor = '#d73333';
$('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false)
.addClass('rmSubmitError').css({ background: errColor, 'border-color': errColor, color: '#fff' });
}
// ─── UI: статус и ссылки ─────────────────────────────────────────────────
function startProcessing() {
if ($('#rmLogBox').length) return;
$('#removerModal').append(
'<div id="rmLogBox" style="margin-top:12px;padding-top:10px;border-top:1px solid ' + tk.bSubS + ';line-height:1.5;overflow-wrap:anywhere;word-break:break-word;box-sizing:border-box;"></div>'
);
syncLinkWidths();
}
function logStatus(message, error, opts) {
var o = opts || {};
if (o.trackError !== false && error && error.code) isError = true;
var $box = $('#rmLogBox');
if (!$box.length) { startProcessing(); $box = $('#rmLogBox'); }
var statusId = o.statusId || ('rm-status-' + (++logStatusSeq));
var $row = $box.find('[data-rm-status-id="' + statusId + '"]');
if (!$row.length) {
$row = $('<div data-rm-status-id="' + statusId + '" style="margin-top:4px;line-height:1.4;"></div>');
$box.append($row);
}
var html;
if (error) {
var errText = error.code
? '<span class="error"><small>' + escapeHtml(formatLogErrorCode(error.code)) + ': ' + escapeHtml(String(error.info || '')) + '</small></span>'
: escapeHtml(String(error));
html = '<span style="color:' + tk.cDang + ';margin-right:4px;">✕</span>' + message + ' — ' + errText;
} else if (o.pending) {
html = '<span style="color:' + tk.cSubM + ';">' + message + '</span>';
} else {
html = '<span style="color:' + tk.bgSucc + ';margin-right:4px;">✓</span><span style="color:' + tk.cSubM + ';">' + message + '</span>';
}
$row.html(html);
return statusId;
}
function formatLogErrorCode(code) {
var value = String(code || '');
return value.toLowerCase() === 'error' ? 'Ошибка' : value;
}
function logPageEdit(pageName, error, opts) {
logStatus('Правка страницы ' + buildQuotedStatusPageLink(pageName) + '.', error, opts);
}
function syncLinkWidths() {
var $box = $('#rmLogBox');
if (!$box.length) return;
var taW = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0;
$box.css({ width: taW ? taW + 'px' : '', 'max-width': '100%' });
}
function appendNominationLink(pageTitle, sectionTitle) {
if (!pageTitle) return;
var url = getPageUrl(pageTitle);
var frag = normalizeSectionForLink(sectionTitle);
if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_'));
var label = normTitle(frag ? pageTitle + '#' + frag : pageTitle);
var $target = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent');
$target.append(
'<div style="margin-top:4px;line-height:1.4;word-break:break-word;overflow-wrap:anywhere;display:flex;align-items:baseline;gap:5px;">' +
'<span style="color:' + tk.bgSucc + ';font-size:14px;flex-shrink:0;">✓</span>' +
'<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a></div>'
);
}
// ─── UI-строители ────────────────────────────────────────────────────────
function buildInfoBoxHtml(mainText, detailsText, isErr) {
var cls = isErr ? ' class="error"' : '';
return joinHtml([
'<div class="rmInfoBox">',
'<p', cls, ' style="margin:0', detailsText ? ' 0 6px' : '', ';">', mainText, '</p>',
detailsText ? '<p style="margin:0;color:' + tk.cSubM + ';">' + detailsText + '</p>' : '',
'</div>'
]);
}
function buildActionsHtml(actions, inputName, listId) {
var actionItemsHtml = actions.map(function (a, i) {
var meta = a.description || (a.talkNotice ? 'С добавлением {{' + (a.talkTemplate || a.resultTemplate || 'шаблон') + '}} на СО.' : '');
var tagHtml = a.tag
? '<span style="display:inline-block;font-size:13px;font-weight:600;padding:2px 7px;border-radius:3px;background:' + tk.bgN + ';color:' + tk.cSubM + ';margin-right:8px;white-space:nowrap;vertical-align:middle;">' + escapeHtml(a.tag) + '</span>'
: '';
return joinHtml([
'<label class="rmActionItem">',
'<span class="rmActionMain">',
'<input type="radio" name="', inputName, '" value="', a.id, '" ', i === 0 ? 'checked' : '', '>',
tagHtml,
'<span>', a.label, '</span>',
'</span>',
meta ? '<span class="rmActionMeta">' + meta + '</span>' : '',
'</label>'
]);
}).join('');
return joinHtml([
'<div style="margin:0 0 8px;color:', tk.cSubM, ';font-size:13px;">Обнаружены открытые номинации:</div>',
'<div', listId ? ' id="' + listId + '"' : '', ' class="rmActionList">',
actionItemsHtml,
'</div>'
]);
}
function buildNestedCommentFieldsHtml(opts) {
var options = opts || {};
var wrapId = options.wrapId || '';
var textareaId = options.textareaId || '';
var textareaClass = options.textareaClass ? ' ' + options.textareaClass : '';
var textareaStyleExtra = options.textareaStyleExtra || '';
var wrapStyleExtra = options.wrapStyleExtra || '';
var placeholder = options.placeholder || 'Комментарий (необязательно)';
var beforeHtml = options.beforeHtml || '';
var marginTop = options.marginTop || '6px';
var minHeight = parseInt(options.minHeight, 10) || 90;
var isEmbedded = !!options.embedded;
var wrapClass = isEmbedded ? '' : (' class="' + RESIZE_CLASS + '"');
var wrapStyle = 'display:none;margin-top:' + marginTop + ';max-width:100%;box-sizing:border-box;';
if (isEmbedded) {
wrapStyle += 'padding:0;border:0;background:transparent;';
} else {
wrapStyle += 'padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:6px;background:' + tk.bgNSub + ';';
}
wrapStyle += wrapStyleExtra;
return joinHtml([
'<div id="', wrapId, '"', wrapClass, ' style="', wrapStyle, '">',
beforeHtml,
'<textarea id="', textareaId, '" class="rmNestedCommentInput', textareaClass,
'" placeholder="', escapeHtml(placeholder), '" style="', stInputFull,
'min-height:', minHeight, 'px;resize:both;margin-bottom:6px;', textareaStyleExtra, '"></textarea>',
buildQuickPhrasesPanelHtml(textareaId),
'</div>'
]);
}
function buildConditionalRetFieldsHtml() {
return buildNestedCommentFieldsHtml({
wrapId: 'rmCloseConditionalWrap',
textareaId: 'rmCloseConditionalReason',
placeholder: 'Условие / пояснение (необязательно)',
marginTop: '8px',
minHeight: 90,
beforeHtml: '<input id="rmCloseConditionalDeadline" type="text" placeholder="Срок доработки: 2026-05-31" style="' + stInputFull + 'margin-bottom:6px;">'
});
}
function buildAddMultiPageButtonHtml(options) {
var opts = options || {};
var title = opts.addTitle || 'Добавить страницу';
return buildSquareAddButtonHtml('', title, 'rmAddMultiPage');
}
function buildSquareAddButtonHtml(id, title, className) {
var idAttr = id ? ' id="' + id + '"' : '';
var clsAttr = className ? ' class="' + className + '"' : '';
var label = title || 'Добавить';
return '<button' + idAttr + ' type="button"' + clsAttr + ' title="' + escapeHtml(label) + '" aria-label="' + escapeHtml(label) + '" style="' + stRemoveBtn + '">+</button>';
}
function buildMultiPageButtonsHtml(commentWrapId, commentId, options) {
var opts = options || {};
var commentBtnStyle = stToolBtn + (opts.showComment ? '' : 'display:none;');
if (opts.showAdd) return buildAddMultiPageButtonHtml(opts);
return joinHtml([
'<button type="button" class="rmToggleBtn rmMultiPageCommentToggle" data-rm-comment-wrap="', commentWrapId,
'" data-rm-comment-textarea="', commentId, '" aria-expanded="false" style="', commentBtnStyle, '">Комментарий</button>',
'<button type="button" class="rmRemoveInput" style="', stRemoveBtn, '" title="', escapeHtml(opts.removeTitle || 'Удалить страницу'), '">−</button>'
]);
}
function buildMultiPageRowHtml(index, options) {
var opts = options || {};
var pageInputId = 'rmMultiPage' + index;
var commentWrapId = 'rmMultiPageCommentWrap' + index;
var commentId = 'rmMultiPageComment' + index;
var pageValue = opts.pageValue || '';
var pageValueAttr = pageValue ? ' value="' + escapeHtml(pageValue) + '"' : '';
var inputPlaceholder = opts.inputPlaceholder || 'Страница';
var commentPlaceholder = opts.commentPlaceholder || 'Комментарий только для этой страницы (необязательно)';
var pageRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
var blockStyle = 'max-width:100%;box-sizing:border-box;';
var buttonsHtml = buildMultiPageButtonsHtml(commentWrapId, commentId, {
showAdd: !!opts.showAdd,
showComment: !!opts.showComment,
addTitle: opts.addTitle,
removeTitle: opts.removeTitle
});
return joinHtml([
'<div class="rmMultiPageBlock ', RESIZE_CLASS, '" style="', blockStyle, '">',
'<div', opts.rowId ? ' id="' + opts.rowId + '"' : '', ' class="rmMultiPageRow" style="', pageRowStyle, '">',
'<input id="', pageInputId, '" type="text" placeholder="', escapeHtml(inputPlaceholder), '" class="rmMultiPageInput" style="', stInputBox, '"', pageValueAttr, '>',
buttonsHtml,
'</div>',
buildNestedCommentFieldsHtml({
wrapId: commentWrapId,
textareaId: commentId,
textareaClass: 'rmMultiPageCommentInput',
placeholder: commentPlaceholder,
marginTop: '4px',
minHeight: 90,
embedded: true,
wrapStyleExtra: 'padding:0 0 0 12px;background:transparent;',
textareaStyleExtra: 'border-color:' + tk.bSubS + ';border-radius:4px;background:' + tk.bgBase + ';'
}),
'</div>'
]);
}
function showInfoAndClose(mainText, detailsText, isErr) {
$('#removerModalContent').html(buildInfoBoxHtml(mainText, detailsText || '', isErr || false));
renderModalFooter('close');
}
function getSelectedAction(inputName, actionMap) {
var id = $('[name="' + inputName + '"]:checked').val();
var sel = actionMap[id];
if (!sel) alert('Выберите действие.');
return sel || null;
}
function prependTemplateToNoinclude(text, templateText) {
var source = String(text || '');
var tpl = String(templateText || '').trim();
if (!tpl) return source;
var match = source.match(RE_NOINCLUDE);
if (match) {
var before = source.slice(0, source.indexOf(match[0]));
if (/\S/.test(before)) return '<noinclude>' + tpl + '</noinclude>\n' + source;
var content = String(match[2] || '').replace(/^\n+/, '');
return source.replace(match[0], match[1] + '<noinclude>' + tpl + (content ? '\n' + content : '') + '\n</noinclude>');
}
return '<noinclude>' + tpl + '</noinclude>\n' + source;
}
function buildGeneratedNominationTemplateText(job, pg) {
var tplStr = '';
if (!job) return '';
if (job.opId === 'fRm') {
tplStr = job.kbuTemplate || '';
if (job.kbuAddInfo) tplStr += '|1=' + job.kbuAddInfo;
if (job.kbuComment) tplStr += '|' + (job.kbuAddInfo ? '2' : '1') + '=' + job.kbuComment;
return tplStr ? (T_OPEN + tplStr + T_CLOSE) : '';
}
if (typeof job.articleTpl !== 'function') return '';
tplStr = job.articleTpl(job.tplpar, job.date[0]);
if (job.opId === 'merge' && job.tplpar) {
tplStr = (job.op.nomination.articleTpl)(
('|' + job.tplpar + '|').replace('|' + pg + '|', '|').slice(1, -1),
job.date[0]
);
}
return tplStr ? (T_OPEN + tplStr + T_CLOSE) : '';
}
function applyConflictTemplateResolution(articleText, job, pg, decision) {
var rule = getNominationConflictRule(job);
var generatedTemplate = buildGeneratedNominationTemplateText(job, pg);
var source = String(articleText || '');
if (!generatedTemplate || !decision) return source;
if (decision.templateAction === 'overwrite') {
var cleaned = rule ? stripTemplatesByPattern(source, rule.namePattern).text : source;
return prependTemplateToNoinclude(cleaned, generatedTemplate);
}
if (decision.templateAction === 'prepend') return prependTemplateToNoinclude(source, generatedTemplate);
return source;
}
function inspectMultiNominationConflicts(job, callback) {
var cb = callback || function () {};
var pages = (job && job.multiArticles) ? job.multiArticles.slice() : [];
var conflicts = [];
var statusId = logStatus('Проверяются статьи на наличие уже установленных шаблонов...', null, { pending: true, trackError: false });
if (!pages.length) {
logStatus('Проверка завершена: конфликтов не найдено.', null, { statusId: statusId, trackError: false });
cb(null, conflicts);
return;
}
eachSequential(pages, function (pg, next) {
getText(pg, function (articleText, readErr) {
var conflict;
if (readErr) { next(makeReadError(readErr, 'read_failed', 'Не удалось проверить страницу «' + pg + '».')); return; }
if (articleText === null) { next({ code: 'read_failed', info: 'Страница «' + pg + '» не существует.' }); return; }
conflict = detectNominationConflict(articleText, job);
if (conflict) {
conflicts.push($.extend({ pageName: pg }, conflict));
logStatus('В статье ' + buildQuotedStatusPageLink(pg) + ' обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.', null, { trackError: false });
}
next();
});
}, function (err) {
if (err) {
logStatus('Проверка статей.', err, { statusId: statusId });
cb(err);
return;
}
logStatus(
conflicts.length
? 'Проверка завершена: найдены статьи с уже установленными шаблонами.'
: 'Проверка завершена: конфликтов не найдено.',
null,
{ statusId: statusId, trackError: false }
);
cb(null, conflicts);
});
}
function buildNominationConflictResolutionHtml(conflicts) {
return '<div class="rmInfoBox"><p style="margin:0 0 6px;">Найдены статьи, где шаблон уже стоит. Для каждой конфликтующей страницы выберите, что делать со статьёй и с шаблоном.</p>' +
'<p style="margin:0;color:' + tk.cSubM + ';font-size:12px;line-height:1.45;">По умолчанию такие статьи исключаются из новой номинации, а существующий шаблон остаётся без изменений.</p></div>' +
'<div class="rmConflictLead">После выбора нажмите «Продолжить номинирование».</div>' +
'<div id="rmConflictList" class="rmConflictList">' +
conflicts.map(function (conflict, index) {
var pageLink = buildStatusPageLink(conflict.pageName);
return '<div class="rmConflictCard" data-rm-conflict-index="' + index + '">' +
'<input type="hidden" class="rmConflictPageAction" value="skip">' +
'<input type="hidden" class="rmConflictTemplateAction" value="keep">' +
'<div class="rmConflictTitle">' + pageLink + '</div>' +
'<div class="rmConflictMeta">Обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.</div>' +
'<div class="rmConflictGroup">' +
'<div class="rmConflictGroupTitle">Действие со статьёй</div>' +
'<div class="rmConflictButtons" data-rm-conflict-group="page">' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="page" data-rm-choice="skip" aria-pressed="true">Убрать из номинации</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="page" data-rm-choice="keep" aria-pressed="false">Оставить в номинации</button>' +
'</div></div>' +
'<div class="rmConflictGroup">' +
'<div class="rmConflictGroupTitle">Действие с шаблоном</div>' +
'<div class="rmConflictButtons" data-rm-conflict-group="template">' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="template" data-rm-choice="keep" aria-pressed="true">Оставить как есть</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="overwrite" aria-pressed="false">Новая дата</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="prepend" aria-pressed="false">Второй сверху</button>' +
'</div>' +
'<div class="rmConflictHint">Если статья исключается из номинации, действие с шаблоном не применяется.</div>' +
'</div></div>';
}).join('') +
'</div>';
}
function updateNominationConflictCardState($card) {
var pageAction = $card.find('.rmConflictPageAction').val() || 'skip';
var disableTemplate = pageAction !== 'keep';
var $templateButtons = $card.find('[data-rm-choice-type="template"]');
$card.toggleClass('is-skip', disableTemplate);
$card.find('[data-rm-conflict-group="template"]').toggleClass('is-disabled', disableTemplate);
$templateButtons.prop('disabled', disableTemplate).toggleClass('is-disabled', disableTemplate);
}
function bindNominationConflictResolutionUi() {
var $content = $('#removerModalContent');
function setChoice($card, type, value) {
var inputClass = type === 'page' ? '.rmConflictPageAction' : '.rmConflictTemplateAction';
$card.find(inputClass).val(value);
$card.find('[data-rm-choice-type="' + type + '"]').each(function () {
var isActive = $(this).data('rmChoice') === value;
$(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false');
});
updateNominationConflictCardState($card);
}
$content.off('.rmConflictResolution').on('click.rmConflictResolution', '[data-rm-choice-type]', function () {
var $btn = $(this);
var $card = $btn.closest('.rmConflictCard');
if ($btn.prop('disabled')) return;
setChoice($card, $btn.data('rmChoiceType'), $btn.data('rmChoice'));
});
$('#rmConflictList .rmConflictCard').each(function () { updateNominationConflictCardState($(this)); });
}
function collectNominationConflictResolution(conflicts) {
var decisions = {};
(conflicts || []).forEach(function (conflict, index) {
var $card = $('#rmConflictList .rmConflictCard[data-rm-conflict-index="' + index + '"]');
decisions[normTitle(conflict.pageName)] = {
pageAction: $card.find('.rmConflictPageAction').val() || 'skip',
templateAction: $card.find('.rmConflictTemplateAction').val() || 'keep'
};
});
return decisions;
}
function applyNominationConflictResolutionToJob(job, decisions) {
var resultArticles;
var headerText;
if (!job || !job.isMulti) {
job.conflictDecisions = decisions || {};
return { value: job };
}
resultArticles = (job.multiArticles || []).filter(function (pageName) {
var decision = decisions && decisions[normTitle(pageName)];
return !decision || decision.pageAction !== 'skip';
});
if (!resultArticles.length) return { error: 'После исключения конфликтующих статей в номинации не осталось ни одной страницы.' };
job.conflictDecisions = decisions || {};
job.multiArticles = resultArticles.slice();
job.pages = resultArticles.slice().reverse();
headerText = String(job.multiHeaderText || '').trim();
job.section = headerText || ('[[:' + resultArticles[0] + ']]');
job.sectionNW = job.section.replace(/\[\[:/g, '').replace(/]]/g, '');
job.msg = job.multiNominationFormat === 'list'
? buildMultiNominationListText(resultArticles, job.multiNominationBody, job.multiArticleComments)
: buildMultiNominationText(resultArticles, job.multiNominationBody, job.multiArticleComments);
job.summary = makeSummary('номинация [[' + job.nomPage + '#' + job.sectionNW + ']]');
return { value: job };
}
function showNominationConflictResolution(job, conflicts, onContinue) {
resetModalObservers();
$('#removerModalContent').html(buildNominationConflictResolutionHtml(conflicts));
bindNominationConflictResolutionUi();
syncLinkWidths();
renderModalFooter('submit', {
submitText: 'Продолжить номинирование',
showSubscribe: true,
preserveLogOnSubmit: true,
onSubmit: function () {
var decisions = collectNominationConflictResolution(conflicts);
var applied = applyNominationConflictResolutionToJob(job, decisions);
if (applied.error) {
alert(applied.error);
return false;
}
if (typeof onContinue === 'function') onContinue(applied.value);
return true;
}
});
}
function bindTouchTextareaGrip($ta, sync, getMaxWidth, options) {
var opts = options || {};
var minWidth = parseInt(opts.minWidth, 10) || parseInt(sz.taMinW, 10) || 180;
var minHeight = parseInt(opts.minHeight, 10) || parseInt(sz.taMinH, 10) || 100;
var allowWidthResize = opts.allowWidth !== false;
var syncFn = typeof sync === 'function' ? sync : function () {};
var getMaxWidthFn = typeof getMaxWidth === 'function' ? getMaxWidth : function () { return $ta.outerWidth() || minWidth; };
var usePointerEvents = typeof window.PointerEvent === 'function';
var dragState = { active: false, startX: 0, startY: 0, startWidth: 0, startHeight: 0 };
var gripStyle = 'height:20px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-top:0;border-radius:0 0 4px 4px;background:' + tk.bgNSub + ';display:flex;align-items:center;justify-content:center;cursor:ns-resize;touch-action:none;user-select:none;-webkit-user-select:none;';
if (opts.gripMarginBottom) gripStyle += 'margin-bottom:' + opts.gripMarginBottom + ';';
var $grip = $('<div data-rm-textarea-grip="1" style="' + gripStyle + '"><span style="display:block;width:42px;height:4px;border-radius:999px;background:' + tk.bSub + ';opacity:.9;"></span></div>');
function getCoord(evt, key) {
var e = evt.originalEvent || evt;
if (e.touches && e.touches.length) return e.touches[0][key];
if (e.changedTouches && e.changedTouches.length) return e.changedTouches[0][key];
return e[key];
}
function stopDrag() {
dragState.active = false;
$(window).off('.rmTaResize');
}
function onDragMove(evt) {
var clientX;
var clientY;
if (!dragState.active) return;
clientX = getCoord(evt, 'clientX');
clientY = getCoord(evt, 'clientY');
if (typeof clientY !== 'number') return;
if (allowWidthResize && typeof clientX === 'number') $ta.css('width', Math.max(minWidth, Math.min(getMaxWidthFn(), dragState.startWidth + (clientX - dragState.startX))) + 'px');
$ta.css('height', Math.max(minHeight, dragState.startHeight + (clientY - dragState.startY)) + 'px');
syncFn();
if (evt.preventDefault) evt.preventDefault();
}
function startDrag(evt) {
var clientY = getCoord(evt, 'clientY');
if (typeof clientY !== 'number') return;
dragState.active = true;
dragState.startX = getCoord(evt, 'clientX') || 0;
dragState.startY = clientY;
dragState.startWidth = $ta.outerWidth();
dragState.startHeight = $ta.outerHeight();
if (evt.preventDefault) evt.preventDefault();
$(window).off('.rmTaResize');
if (usePointerEvents) {
$(window).on('pointermove.rmTaResize', onDragMove).on('pointerup.rmTaResize pointercancel.rmTaResize', stopDrag);
} else {
$(window).on('touchmove.rmTaResize mousemove.rmTaResize', onDragMove).on('touchend.rmTaResize touchcancel.rmTaResize mouseup.rmTaResize', stopDrag);
}
}
$ta.css({ 'border-bottom-left-radius': '0', 'border-bottom-right-radius': '0' });
$ta.next('[data-rm-textarea-grip]').remove();
$ta.after($grip);
if (usePointerEvents) $grip.on('pointerdown.rmTaGrip', startDrag);
else $grip.on('touchstart.rmTaGrip mousedown.rmTaGrip', startDrag);
}
function applyModalContentWidth($modal, contentWidth, options) {
var opts = options || {};
var layout = getModalLayout();
var modalFrame = getBoxFrameWidth($modal);
var safeContentWidth = Math.max(layout.minWidth, Math.min(contentWidth, layout.maxOuterWidth - modalFrame));
var modalWidth = safeContentWidth + modalFrame;
var initialContentW = parseFloat($modal.data('rmInitialContentW')) || 0;
$modal.css({
width: modalWidth + 'px',
'max-width': layout.maxOuterWidth + 'px',
'box-sizing': 'border-box',
'margin-left': layout.shouldCenter ? 'auto' : '0',
'margin-right': layout.shouldCenter ? 'auto' : '0'
}).toggleClass('rmCompactContent', safeContentWidth < 520);
$('.' + RESIZE_CLASS).css({
width: safeContentWidth + 'px',
'max-width': '100%',
'box-sizing': 'border-box'
});
$('#rmMsg,#nominationReason,#rmReportText').each(function () {
var $textarea = $(this);
var textareaId = this.id;
if (!$textarea.length) return;
$textarea.css('width', safeContentWidth + 'px');
$textarea.next('[data-rm-textarea-grip]').css('width', safeContentWidth + 'px');
$('.rmQuickPhrasesPanel[data-rm-target="' + textareaId + '"]').css('width', safeContentWidth + 'px');
});
$('.rmNestedCommentInput').each(function () {
var $textarea = $(this);
var $wrap = $textarea.parent();
var containerFrame = parseFloat($textarea.data('rmNestedContainerFrame')) || 0;
var wrapFrame = parseFloat($textarea.data('rmNestedWrapFrame')) || 0;
var safeMinWidth = parseFloat($textarea.data('rmNestedMinWidth')) || 0;
var wrapOuterWidth;
var textareaWidth;
if (!$wrap.length || !$wrap.is(':visible')) return;
wrapOuterWidth = Math.max(1, safeContentWidth - containerFrame);
textareaWidth = Math.max(1, wrapOuterWidth - wrapFrame);
$wrap.css({
width: wrapOuterWidth + 'px',
'max-width': '100%',
'box-sizing': 'border-box'
});
$textarea.css({
width: textareaWidth + 'px',
'min-width': Math.min(safeMinWidth || textareaWidth, textareaWidth) + 'px'
});
$textarea.next('[data-rm-textarea-grip]').css('width', textareaWidth + 'px');
$('.rmQuickPhrasesPanel[data-rm-target="' + this.id + '"]').css('width', textareaWidth + 'px');
});
syncLinkWidths();
if (isVector22) {
var $content = $('#content');
if ($content.length && modalWidth > initialContentW) $content.css({ 'min-width': modalWidth + 'px' });
else if ($content.length) $content.css({ 'min-width': '' });
} else {
$('#content').css({ 'min-width': '' });
}
if (!opts.skipStore) $modal.data('rmContentWidth', safeContentWidth);
return safeContentWidth;
}
function setupNestedResizableTextarea(textareaId, wrapId, minWidth, minHeight) {
var $ta = $('#' + textareaId);
var $wrap = $('#' + wrapId);
var $modal = $('#removerModal');
var $container = $wrap.parent();
var layout = getModalLayout();
var safeMinWidth = parseInt(minWidth, 10) || 280;
var safeMinHeight = parseInt(minHeight, 10) || 90;
var initialWidth;
var modalFrame;
var containerFrame;
var wrapFrame;
function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; }
function getMaxTextareaWidth(currentLayout) {
return Math.max(1, currentLayout.maxOuterWidth - modalFrame - containerFrame - wrapFrame);
}
function getEffectiveMinWidth(currentLayout) {
return Math.min(safeMinWidth, getMaxTextareaWidth(currentLayout));
}
function sync() {
var currentLayout = getModalLayout();
var maxTextareaWidth = getMaxTextareaWidth(currentLayout);
var textareaWidth = $ta.outerWidth();
var contentWidth;
if (!$wrap.is(':visible')) return;
if (textareaWidth > maxTextareaWidth) {
$ta.css('width', maxTextareaWidth + 'px');
textareaWidth = $ta.outerWidth();
}
contentWidth = Math.min(currentLayout.maxOuterWidth - modalFrame, textareaWidth + wrapFrame + containerFrame);
applyModalContentWidth($modal, contentWidth);
}
if (!$ta.length || !$wrap.length || !$modal.length) return;
modalFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width');
containerFrame = $container.length
? px($container, 'padding-left') + px($container, 'padding-right') + px($container, 'border-left-width') + px($container, 'border-right-width')
: 0;
wrapFrame = px($wrap, 'padding-left') + px($wrap, 'padding-right') + px($wrap, 'border-left-width') + px($wrap, 'border-right-width');
initialWidth = Math.min(
Math.max(getEffectiveMinWidth(layout), getDefaultResizableWidth(modalFrame + containerFrame + wrapFrame)),
getMaxTextareaWidth(layout)
);
$ta.css({
width: initialWidth + 'px',
'min-width': getEffectiveMinWidth(layout) + 'px',
'min-height': safeMinHeight + 'px',
'box-sizing': 'border-box',
resize: layout.useFullWidth ? 'none' : 'both',
'border-bottom-left-radius': '',
'border-bottom-right-radius': ''
});
$ta.data('rmNestedContainerFrame', containerFrame);
$ta.data('rmNestedWrapFrame', wrapFrame);
$ta.data('rmNestedMinWidth', safeMinWidth);
$ta.next('[data-rm-textarea-grip]').remove();
if (layout.useFullWidth) {
bindTouchTextareaGrip($ta, sync, function () {
return getMaxTextareaWidth(getModalLayout());
}, {
minWidth: getEffectiveMinWidth(layout),
minHeight: safeMinHeight
});
}
registerModalLayoutSync(sync);
$(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
if (typeof ResizeObserver === 'function') {
var observer = new ResizeObserver(sync);
observer.observe($ta[0]);
registerResizeObserver(observer);
}
sync();
}
function setupResizableModal(textareaId) {
var $ta = $('#' + textareaId);
var $modal = $('#removerModal');
var layout = getModalLayout();
function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; }
applyV2022Layout($modal);
$modal.css({ display: 'block', 'margin-left': layout.shouldCenter ? 'auto' : '0', 'margin-right': layout.shouldCenter ? 'auto' : '0' });
var isBorderBox = ($modal.css('box-sizing') || '').toLowerCase() === 'border-box';
var hFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width');
var initialContentW = isVector22 ? ($('#content').outerWidth() || 0) : 0;
var minWidth = layout.minWidth;
$modal.data('rmInitialContentW', initialContentW);
$ta.css({ width: getDefaultResizableWidth(hFrame) + 'px', height: sz.taH, padding: '8px', 'box-sizing': 'border-box',
border: '1px solid ' + tk.bSub, 'border-radius': '2px', background: tk.bgBase,
color: 'inherit', resize: layout.useFullWidth ? 'none' : 'both', 'min-height': sz.taMinH, 'min-width': sz.taMinW });
$(window).off('.rmTaResize');
function sync() {
var currentLayout = getModalLayout();
var maxTextareaWidth = Math.max(minWidth, currentLayout.maxOuterWidth - Math.floor(hFrame));
var w = $ta.outerWidth();
if (w > maxTextareaWidth) {
$ta.css('width', maxTextareaWidth + 'px');
w = $ta.outerWidth();
}
applyModalContentWidth($modal, isBorderBox ? w : Math.min(currentLayout.maxOuterWidth - hFrame, w));
}
if (layout.useFullWidth) bindTouchTextareaGrip($ta, sync, function () {
return Math.max(minWidth, getModalLayout().maxOuterWidth - Math.floor(hFrame));
});
else {
$ta.css({ 'border-bottom-left-radius': '', 'border-bottom-right-radius': '' });
$ta.next('[data-rm-textarea-grip]').remove();
}
registerModalLayoutSync(sync);
$(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
if (typeof ResizeObserver === 'function') {
var observer = new ResizeObserver(sync);
observer.observe($ta[0]);
registerResizeObserver(observer);
}
sync();
}
function addInputRow(opts) {
var w = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0;
$('#' + opts.containerId).append(joinHtml([
'<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, w ? 'width:' + w + 'px;' : '', '">',
'<input type="text" class="', opts.inputClass || 'variantInput', '" placeholder="', opts.placeholder || '', '" style="', stInputBox, '">',
'<button type="button" class="rmRemoveInput" style="', stRemoveBtn, '" title="Удалить">−</button>',
'</div>'
]));
syncModalLayout();
}
$(document).off('click.rmRemoveInput').on('click.rmRemoveInput', '.rmRemoveInput', function () {
$(this).closest('.rmInputRow').remove();
syncModalLayout();
});
$(document).off('click.rmQuickPhraseInsert').on('click.rmQuickPhraseInsert', '.rmQuickPhraseActionBtn', function (e) {
var targetId;
var phrase;
e.preventDefault();
targetId = $(this).data('rmTarget');
phrase = $(this).attr('data-rm-phrase') || '';
if (!targetId) return;
insertTextIntoTextarea($('#' + targetId), phrase);
});
function buildMultiInputHtml(c) {
var addLabel = c.addBtnLabel || '+ Добавить';
return joinHtml([
'<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="', c.firstId, '" type="text" class="', c.inputClass || 'variantInput', '" placeholder="', c.firstPh || '', '" style="', stInputBox, '">',
buildSquareAddButtonHtml(c.addBtnId, addLabel.replace(/^\+\s*/, '') || 'Добавить'),
'</div>',
'<div id="', c.containerId, '"></div>'
]);
}
function wireMultiInput(c) {
$('#' + c.addBtnId).click(function () {
var count = $('#' + c.containerId + ' .rmInputRow').length;
if (typeof c.maxRows === 'number' && count >= c.maxRows) { alert(c.maxMsg || 'Достигнут лимит.'); return; }
addInputRow({ containerId: c.containerId, placeholder: c.addPh, inputClass: c.inputClass });
});
}
function buildSettingsFieldHtml(label, controlHtml, helpText, options) {
var opts = options || {};
var helpHtml = helpText
? '<div class="rmSettingsFieldHint">' + helpText + '</div>'
: '';
var labelHtml = opts.forId
? '<label class="rmSettingsFieldLabel" for="' + opts.forId + '">' + label + '</label>'
: '<div class="rmSettingsFieldLabel">' + label + '</div>';
return joinHtml([
'<div class="rmSettingsField">',
labelHtml,
'<div class="rmSettingsFieldControl">', controlHtml, '</div>',
helpHtml,
'</div>'
]);
}
function buildSettingsSectionHtml(title, bodyHtml, helpText, options) {
var opts = options || {};
var headerHtml = '';
var description = [];
if (opts.titleNote) description.push(opts.titleNote);
if (helpText) description.push(helpText);
if (title || description.length) {
headerHtml = joinHtml([
'<div class="rmSettingsSectionHeader">',
title ? '<div class="rmSettingsSectionTitle">' + title + '</div>' : '',
description.length ? '<div class="rmSettingsSectionDescription">' + description.join(' ') + '</div>' : '',
'</div>'
]);
}
return joinHtml([
'<div class="rmSettingsSection">',
headerHtml,
bodyHtml,
'</div>'
]);
}
function buildSettingsSimpleCheckboxHtml(id, text) {
return joinHtml([
'<label class="rmSettingsCheck">',
'<input id="', id, '" type="checkbox">',
'<span>', text, '</span>',
'</label>'
]);
}
function buildQuickPhrasesSettingsEditorHtml() {
return joinHtml([
'<div id="rmSettingsQuickPhrasesEditor" class="rmQuickPhraseEditor">',
'<div id="rmSettingsQuickPhrasesList" class="rmQuickPhraseList"></div>',
'<input id="rmSettingsQuickPhraseInput" type="text" autocomplete="off" style="', stInputFull, 'margin-bottom:0;">',
'<div id="rmSettingsQuickPhraseMeta" class="rmQuickPhraseMeta"></div>',
'</div>'
]);
}
function getQuickPhraseEditor() {
return $('#rmSettingsQuickPhrasesEditor');
}
function getQuickPhraseEditorState() {
var $editor = getQuickPhraseEditor();
var phrases = normalizeQuickPhrasesList($editor.data('rmQuickPhrases'), []);
var editingIndex = parseInt($editor.data('rmQuickPhraseEditingIndex'), 10);
if (isNaN(editingIndex)) editingIndex = -1;
return { editor: $editor, phrases: phrases, editingIndex: editingIndex };
}
function setQuickPhraseEditorState(phrases, editingIndex) {
var $editor = getQuickPhraseEditor();
var normalized = normalizeQuickPhrasesList(phrases, []);
var safeEditingIndex = parseInt(editingIndex, 10);
if (isNaN(safeEditingIndex) || safeEditingIndex < 0 || safeEditingIndex >= normalized.length) safeEditingIndex = -1;
if (!$editor.length) return;
$editor.data('rmQuickPhrases', normalized);
$editor.data('rmQuickPhraseEditingIndex', safeEditingIndex);
renderQuickPhraseEditor();
}
function clearQuickPhraseDropState() {
var $editor = getQuickPhraseEditor();
$editor.removeData('rmQuickPhraseDragIndex');
$editor.removeData('rmQuickPhraseDropIndex');
$editor.removeData('rmQuickPhraseDropAfter');
$editor.find('.rmQuickPhraseChip').removeClass('is-dragging is-drop-before is-drop-after');
}
function renderQuickPhraseEditor() {
var state = getQuickPhraseEditorState();
var $list = $('#rmSettingsQuickPhrasesList');
var $input = $('#rmSettingsQuickPhraseInput');
var $meta = $('#rmSettingsQuickPhraseMeta');
if (!state.editor.length || !$list.length || !$input.length) return;
if (state.phrases.length) {
$list.html(state.phrases.map(function (phrase, index) {
var chipClass = 'rmQuickPhraseChip' + (index === state.editingIndex ? ' is-editing' : '');
return joinHtml([
'<div class="', chipClass, '" draggable="true" data-rm-quick-index="', index, '">',
'<button type="button" class="rmQuickPhraseEditBtn" title="Редактировать фразу">', escapeHtml(phrase), '</button>',
'<button type="button" class="rmQuickPhraseRemoveBtn" title="Удалить фразу" aria-label="Удалить фразу">×</button>',
'</div>'
]);
}).join(''));
} else {
$list.html('<div class="rmQuickPhraseEmpty">Фразы пока не добавлены.</div>');
}
$input
.attr('placeholder', state.editingIndex >= 0 ? 'Изменить значение...' : 'Добавить значение...')
.toggleClass('is-editing', state.editingIndex >= 0);
$meta
.text('')
.hide();
}
function notifyQuickPhraseEditorChanged() {
var $editor = getQuickPhraseEditor();
if ($editor.length) $editor.trigger('rmQuickPhrasesChanged');
}
function startQuickPhraseEdit(index) {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
if (index < 0 || index >= state.phrases.length || !$input.length) return;
state.editor.data('rmQuickPhraseEditingIndex', index);
$input.val(state.phrases[index]);
renderQuickPhraseEditor();
$input.trigger('focus');
if ($input[0] && typeof $input[0].select === 'function') $input[0].select();
}
function cancelQuickPhraseEdit() {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
state.editor.data('rmQuickPhraseEditingIndex', -1);
if ($input.length) $input.val('').removeClass('is-editing');
renderQuickPhraseEditor();
}
function saveQuickPhraseInput() {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
var value = normalizeQuickPhraseValue($input.val());
var next = [];
if (!$input.length || !value) return false;
if (state.editingIndex >= 0) {
state.phrases.forEach(function (phrase, index) {
if (index === state.editingIndex) {
next.push(value);
return;
}
if (phrase !== value && next.indexOf(phrase) === -1) next.push(phrase);
});
} else {
next = state.phrases.slice();
if (next.indexOf(value) === -1) next.push(value);
}
state.editor.data('rmQuickPhrases', normalizeQuickPhrasesList(next, []));
state.editor.data('rmQuickPhraseEditingIndex', -1);
$input.val('').removeClass('is-editing');
clearQuickPhraseDropState();
renderQuickPhraseEditor();
notifyQuickPhraseEditorChanged();
return true;
}
function removeQuickPhrase(index) {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
if (index < 0 || index >= state.phrases.length) return;
state.phrases.splice(index, 1);
state.editor.data('rmQuickPhrases', state.phrases);
if (state.editingIndex === index) {
state.editor.data('rmQuickPhraseEditingIndex', -1);
if ($input.length) $input.val('').removeClass('is-editing');
} else if (state.editingIndex > index) {
state.editor.data('rmQuickPhraseEditingIndex', state.editingIndex - 1);
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
notifyQuickPhraseEditorChanged();
}
function reorderQuickPhrases(phrases, fromIndex, toIndex, placeAfter) {
var result = phrases.slice();
var insertIndex = toIndex + (placeAfter ? 1 : 0);
var item;
if (fromIndex < 0 || fromIndex >= result.length || toIndex < 0 || toIndex >= result.length) return result;
item = result.splice(fromIndex, 1)[0];
if (fromIndex < insertIndex) insertIndex--;
result.splice(insertIndex, 0, item);
return result;
}
function getQuickPhraseDropPointer(evt) {
var originalEvent = evt && (evt.originalEvent || evt);
if (!originalEvent) return null;
if (typeof originalEvent.clientX !== 'number' || typeof originalEvent.clientY !== 'number') return null;
return { x: originalEvent.clientX, y: originalEvent.clientY };
}
function getQuickPhraseDropTarget($list, pointer, dragIndex) {
var candidates = [];
var rowCandidates;
var minRowDistance = Infinity;
var bestBoundary = null;
var bestBoundaryDistance = Infinity;
if (!$list || !$list.length || !pointer) return null;
$list.children('.rmQuickPhraseChip').each(function () {
var index = parseInt($(this).attr('data-rm-quick-index'), 10);
var rect;
var rowDistance;
if (isNaN(index) || index === dragIndex) return;
rect = this.getBoundingClientRect();
if (!rect.width || !rect.height) return;
rowDistance = pointer.y < rect.top ? (rect.top - pointer.y) : (pointer.y > rect.bottom ? (pointer.y - rect.bottom) : 0);
candidates.push({
node: this,
index: index,
left: rect.left,
right: rect.right,
top: rect.top,
bottom: rect.bottom,
midX: rect.left + rect.width / 2,
rowDistance: rowDistance
});
if (rowDistance < minRowDistance) minRowDistance = rowDistance;
});
if (!candidates.length) return null;
rowCandidates = candidates
.filter(function (candidate) { return candidate.rowDistance === minRowDistance; })
.sort(function (a, b) {
if (a.left !== b.left) return a.left - b.left;
return a.index - b.index;
});
if (!rowCandidates.length) return null;
if (pointer.x <= rowCandidates[0].left) {
return { index: rowCandidates[0].index, placeAfter: false, node: rowCandidates[0].node };
}
if (pointer.x >= rowCandidates[rowCandidates.length - 1].right) {
return {
index: rowCandidates[rowCandidates.length - 1].index,
placeAfter: true,
node: rowCandidates[rowCandidates.length - 1].node
};
}
for (var i = 0; i < rowCandidates.length; i++) {
var candidate = rowCandidates[i];
if (pointer.x >= candidate.left && pointer.x <= candidate.right) {
return {
index: candidate.index,
placeAfter: pointer.x > candidate.midX,
node: candidate.node
};
}
}
rowCandidates.forEach(function (candidate) {
var leftDistance = Math.abs(pointer.x - candidate.left);
var rightDistance = Math.abs(pointer.x - candidate.right);
if (leftDistance < bestBoundaryDistance) {
bestBoundaryDistance = leftDistance;
bestBoundary = { index: candidate.index, placeAfter: false, node: candidate.node };
}
if (rightDistance < bestBoundaryDistance) {
bestBoundaryDistance = rightDistance;
bestBoundary = { index: candidate.index, placeAfter: true, node: candidate.node };
}
});
return bestBoundary;
}
function bindQuickPhrasesEditor() {
var $editor = getQuickPhraseEditor();
var $list = $('#rmSettingsQuickPhrasesList');
var $input = $('#rmSettingsQuickPhraseInput');
function updateQuickPhraseDropTarget(evt) {
var dragIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10);
var pointer = getQuickPhraseDropPointer(evt);
var target;
if (isNaN(dragIndex) || !pointer) return null;
target = getQuickPhraseDropTarget($list, pointer, dragIndex);
if (!target) return null;
$editor.data('rmQuickPhraseDropIndex', target.index);
$editor.data('rmQuickPhraseDropAfter', !!target.placeAfter);
$editor.find('.rmQuickPhraseChip').removeClass('is-drop-before is-drop-after');
$(target.node).addClass(target.placeAfter ? 'is-drop-after' : 'is-drop-before');
return target;
}
function applyQuickPhraseDrop() {
var state = getQuickPhraseEditorState();
var fromIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10);
var toIndex = parseInt($editor.data('rmQuickPhraseDropIndex'), 10);
var placeAfter = $editor.data('rmQuickPhraseDropAfter') === true;
var changed = false;
if (!isNaN(fromIndex) && !isNaN(toIndex) && fromIndex !== toIndex) {
state.editor.data('rmQuickPhrases', reorderQuickPhrases(state.phrases, fromIndex, toIndex, placeAfter));
if (state.editingIndex === fromIndex) state.editor.data('rmQuickPhraseEditingIndex', -1);
changed = true;
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
if (changed) notifyQuickPhraseEditorChanged();
}
if (!$editor.length || !$list.length || !$input.length) return;
$editor.off('.rmQuickPhraseEditor');
$list.off('.rmQuickPhraseEditor');
$input.off('.rmQuickPhraseEditor');
$input.on('keydown.rmQuickPhraseEditor', function (e) {
if (e.key === 'Enter' || e.keyCode === 13) {
e.preventDefault();
saveQuickPhraseInput();
} else if (e.key === 'Escape' || e.keyCode === 27) {
e.preventDefault();
cancelQuickPhraseEdit();
}
});
$editor
.on('click.rmQuickPhraseEditor', '.rmQuickPhraseEditBtn', function () {
var index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10);
startQuickPhraseEdit(index);
})
.on('click.rmQuickPhraseEditor', '.rmQuickPhraseRemoveBtn', function (e) {
var index;
e.preventDefault();
e.stopPropagation();
index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10);
removeQuickPhrase(index);
})
.on('dragstart.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
var index = parseInt($(this).attr('data-rm-quick-index'), 10);
if (isNaN(index)) return;
$editor.data('rmQuickPhraseDragIndex', index);
$(this).addClass('is-dragging');
if (e.originalEvent && e.originalEvent.dataTransfer) {
e.originalEvent.dataTransfer.effectAllowed = 'move';
e.originalEvent.dataTransfer.setData('text/plain', String(index));
}
})
.on('dragover.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
if ($editor.data('rmQuickPhraseDragIndex') === undefined) return;
e.preventDefault();
updateQuickPhraseDropTarget(e);
if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move';
})
.on('drop.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
e.preventDefault();
updateQuickPhraseDropTarget(e);
applyQuickPhraseDrop();
})
.on('dragend.rmQuickPhraseEditor', '.rmQuickPhraseChip', function () {
clearQuickPhraseDropState();
});
$list
.on('dragover.rmQuickPhraseEditor', function (e) {
if ($editor.data('rmQuickPhraseDragIndex') === undefined) return;
e.preventDefault();
updateQuickPhraseDropTarget(e);
if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move';
})
.on('drop.rmQuickPhraseEditor', function (e) {
e.preventDefault();
updateQuickPhraseDropTarget(e);
applyQuickPhraseDrop();
});
renderQuickPhraseEditor();
}
function collectQuickPhraseValues() {
return getQuickPhraseEditorState().phrases;
}
function collectQuickPhraseValuesSnapshot() {
var state = getQuickPhraseEditorState();
var value = normalizeQuickPhraseValue($('#rmSettingsQuickPhraseInput').val());
var next = state.phrases.slice();
if (!value) return next;
if (state.editingIndex >= 0) {
next[state.editingIndex] = value;
} else if (next.indexOf(value) === -1) {
next.push(value);
}
return normalizeQuickPhrasesList(next, []);
}
function isMenuTitlePresetValue(value) {
return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS;
}
function isMenuTitlePresetOnlySkin() {
return mwCfg.skin === 'minerva' || mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
}
function getDefaultMenuTitlePreset() {
return mwCfg.skin === 'minerva' ? MENU_TITLE_PRESET_TOOLS : MENU_TITLE_PRESET_CACTIONS;
}
function shouldPreserveStoredMenuTitleOnSave() {
return mwCfg.skin === 'minerva';
}
function isAvailableMenuTitlePresetValue(value) {
return getMenuTitlePresetOptions().some(function (option) {
return option.value === value;
});
}
function getMenuTitlePresetOptions() {
if (mwCfg.skin === 'minerva') {
return [
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Ещё' }
];
}
if (isVector22) {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты/Действия' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты/Основное' }
];
}
if (mwCfg.skin === 'timeless') {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты для страниц' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Вики-инструменты' }
];
}
if (mwCfg.skin === 'monobook') {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Верхняя панель' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' }
];
}
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: mwCfg.skin === 'vector' ? 'Ещё' : 'Ещё / Действия' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' }
];
}
function getMenuTitlePresetHintText() {
var base = (mwCfg.skin === 'vector' || isVector22)
? 'Можно либо изменить заголовок отдельного меню Remover, либо перенести все пункты в одно из существующих стандартных меню.'
: 'На этом скине Remover использует существующие стандартные меню; отдельное меню с собственным заголовком не создаётся.';
if (isVector22) base += ' В Vector 2022 кнопки «Действия» и «Основное» находятся внутри общего меню «Инструменты».';
else if (mwCfg.skin === 'vector') base += ' В Vector отдельный заголовок создаёт собственное меню рядом с «Ещё».';
else if (mwCfg.skin === 'minerva') base += ' В Minerva Neue пункты Remover показываются в меню «Ещё».';
else if (mwCfg.skin === 'timeless') base += ' В Timeless доступны только стандартные варианты: «Инструменты для страниц», «Страница» и «Вики-инструменты».';
else if (mwCfg.skin === 'monobook') base += ' В MonoBook доступны только два варианта: поместить пункты в «Инструменты» или вывести их в верхнюю панель. Собственный заголовок меню здесь не используется.';
return base;
}
function getSignatureSeparatorPreviewText(value) {
var separator = String(value || '').trim();
return separator ? (separator + ' ' + '~~' + '~~') : ('~~' + '~~');
}
function updateSignatureSeparatorPreview(value) {
var previewValue = (typeof value === 'string') ? value : ($('#rmSettingsSignatureSeparator').val() || '');
var $code = $('#rmSettingsSignaturePreviewCode');
if (!$code.length) return;
$code.text(getSignatureSeparatorPreviewText(previewValue));
}
function bindSignatureSeparatorPreview() {
var $input = $('#rmSettingsSignatureSeparator');
if (!$input.length) return;
$input.off('.rmSignaturePreview').on('input.rmSignaturePreview change.rmSignaturePreview', function () {
updateSignatureSeparatorPreview($(this).val());
});
updateSignatureSeparatorPreview($input.val());
}
function buildMenuTitlePresetButtonsHtml() {
return joinHtml([
'<div class="rmSettingsMenuPresetWrap">',
'<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>',
'<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">',
getMenuTitlePresetOptions().map(function (option) {
return joinHtml([
'<button type="button" class="rmSettingsMenuPresetBtn" data-rm-menu-preset="',
option.value,
'" aria-pressed="false">',
escapeHtml(option.label),
'</button>'
]);
}).join(''),
'</div>',
'</div>'
]);
}
function applyMenuTitlePresetControls(presetValue) {
var preset = isMenuTitlePresetValue(presetValue) ? presetValue : '';
var $bar = $('#rmSettingsMenuPresetBar');
var $input = $('#rmSettingsMenuTitle');
var forcePresetOnly = isMenuTitlePresetOnlySkin();
if (!$bar.length || !$input.length) return;
$bar.data('rmPreset', preset);
$bar.find('.rmSettingsMenuPresetBtn').each(function () {
var isActive = $(this).data('rmMenuPreset') === preset;
$(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false');
});
$input.prop('disabled', forcePresetOnly || !!preset);
}
function bindMenuTitlePresetControls() {
var $bar = $('#rmSettingsMenuPresetBar');
var $input = $('#rmSettingsMenuTitle');
if (!$bar.length || !$input.length) return;
$input.off('.rmSettingsMenuPreset').on('input.rmSettingsMenuPreset', function () {
if (!$bar.data('rmPreset')) $input.data('rmCustomValue', $input.val());
});
$bar.off('.rmSettingsMenuPreset').on('click.rmSettingsMenuPreset', '.rmSettingsMenuPresetBtn', function () {
var preset = $(this).data('rmMenuPreset');
var currentPreset = $bar.data('rmPreset') || '';
if (currentPreset === preset) {
if (isMenuTitlePresetOnlySkin()) return;
applyMenuTitlePresetControls('');
$input.val($input.data('rmCustomValue') || '');
$input.trigger('focus');
return;
}
$input.data('rmCustomValue', $input.val());
applyMenuTitlePresetControls(preset);
});
}
function fillSettingsFormValues(settings) {
var data = normalizeRemoverSettings(settings);
var forcePresetOnly = isMenuTitlePresetOnlySkin();
var menuTitleValue = data.menuTitle || '';
var storedMenuTitleValue = menuTitleValue;
if (forcePresetOnly && !isAvailableMenuTitlePresetValue(menuTitleValue)) menuTitleValue = getDefaultMenuTitlePreset();
var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue);
$('#rmSettingsForm').data('rmStoredMenuTitle', storedMenuTitleValue || '');
$('#rmSettingsNotifyAuthor').prop('checked', !!data.notifyAuthor);
$('#rmSettingsSubscribeTopic').prop('checked', !!data.subscribeTopic);
$('#rmSettingsShowMenuIcons').prop('checked', !!data.showMenuIcons);
$('#rmSettingsMenuTitle').val(customMenuTitle || '').data('rmCustomValue', customMenuTitle || '');
$('#rmSettingsSignatureSeparator').val(data.signatureSeparator || '');
$('#rmSettingsExcludedNamespaces').val((data.excludedNamespaces || []).join(', '));
$('#rmSettingsDisabledItems').val((data.disabledItems || []).join(', '));
setQuickPhraseEditorState(data.quickPhrases || [], -1);
$('#rmSettingsQuickPhraseInput').val('').removeClass('is-editing');
clearQuickPhraseDropState();
applyMenuTitlePresetControls(menuTitleValue);
updateSignatureSeparatorPreview(data.signatureSeparator || '');
}
function collectSettingsFormValues(options) {
var opts = options || {};
var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val());
var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val());
var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset');
var forcePresetOnly = isMenuTitlePresetOnlySkin();
var storedMenuTitle = $('#rmSettingsForm').data('rmStoredMenuTitle');
if (namespaces.invalid.length) {
return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' };
}
if (disabledItems.invalid.length) {
return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' };
}
if (!opts.skipQuickPhraseCommit) saveQuickPhraseInput();
return {
value: normalizeRemoverSettings({
notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'),
subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'),
showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'),
menuTitle: forcePresetOnly
? (shouldPreserveStoredMenuTitleOnSave() && typeof storedMenuTitle === 'string'
? storedMenuTitle
: (isAvailableMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : getDefaultMenuTitlePreset()))
: (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()),
signatureSeparator: $('#rmSettingsSignatureSeparator').val(),
excludedNamespaces: namespaces.values,
disabledItems: disabledItems.values,
quickPhrases: opts.skipQuickPhraseCommit ? collectQuickPhraseValuesSnapshot() : collectQuickPhraseValues()
})
};
}
function updateSettingsSubmitReadyState(baselineSettings) {
var collected = collectSettingsFormValues({ skipQuickPhraseCommit: true });
var hasChanges = !!(collected.error || (collected.value && !areRemoverSettingsEqual(collected.value, baselineSettings)));
$('#removerSubmit').toggleClass('rmSubmitReady', hasChanges && !$('#removerSubmit').hasClass('rmSubmitError'));
$('#rmSettingsUnsavedHint').css('display', hasChanges ? 'inline-block' : 'none');
}
function bindSettingsSubmitReadyState(baselineSettings) {
var update = function () {
setTimeout(function () { updateSettingsSubmitReadyState(baselineSettings); }, 0);
};
$('#rmSettingsForm').off('.rmSettingsReady').on('input.rmSettingsReady change.rmSettingsReady', 'input, textarea, select', update);
$('#removerModalContent').off('click.rmSettingsReady keyup.rmSettingsReady').on(
'click.rmSettingsReady keyup.rmSettingsReady',
'.rmSettingsMenuPresetBtn,.rmQuickPhraseEditBtn,.rmQuickPhraseRemoveBtn,.rmQuickPhraseChip,#rmSettingsQuickPhraseInput',
update
);
$('#rmSettingsQuickPhrasesEditor').off('rmQuickPhrasesChanged.rmSettingsReady').on('rmQuickPhrasesChanged.rmSettingsReady', update);
updateSettingsSubmitReadyState(baselineSettings);
}
function buildSettingsFormHtml(menuLabelsHint) {
var menuFields =
buildSettingsFieldHtml('Заголовок отдельного меню',
'<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(),
getMenuTitlePresetHintText(), { forId: 'rmSettingsMenuTitle' }) +
buildSettingsFieldHtml('Визуальное оформление меню',
'<div class="rmSettingsChecks">' + buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи в пунктах меню') + '</div>');
var messageFields =
buildSettingsFieldHtml('Префикс перед подписью',
'<input id="rmSettingsSignatureSeparator" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Добавляется перед подписью в публикуемых сообщениях.<span style="display:block;margin-top:6px;">Предпросмотр: <code id="rmSettingsSignaturePreviewCode"></code>.</span>',
{ forId: 'rmSettingsSignatureSeparator' }) +
buildSettingsFieldHtml('Часто используемые фразы', buildQuickPhrasesSettingsEditorHtml(),
'Enter для добавления. Порядок элементов изменяется перетаскиванием.', { forId: 'rmSettingsQuickPhraseInput' });
var defaultFields = '<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') +
buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') + '</div>';
var disableFields =
buildSettingsFieldHtml('Скрыть пункты меню',
'<input id="rmSettingsDisabledItems" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Названия пунктов через запятую, например <code>КБУ, КУЛ, КОБ</code>.' + menuLabelsHint,
{ forId: 'rmSettingsDisabledItems' }) +
buildSettingsFieldHtml('Не показывать в пространствах имён',
'<input id="rmSettingsExcludedNamespaces" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Номера пространств имён через запятую, например <code>2, 10, 828</code>. См. <a href="' + getPageUrl('Википедия:Пространства имён') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Пространства имён</a>.',
{ forId: 'rmSettingsExcludedNamespaces' });
return joinHtml([
'<div id="rmSettingsForm" style="max-width:100%;">',
'<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>',
buildSettingsSectionHtml('Меню', menuFields, 'Настройки внешнего вида и состава меню Remover.'),
buildSettingsSectionHtml('Оформление сообщений', messageFields, 'Настройки оформления публикуемых сообщений в номинациях.'),
buildSettingsSectionHtml('Опции по умолчанию', defaultFields, 'Регулирует изначальное состояние галочек.'),
buildSettingsSectionHtml('Отключение', disableFields, 'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'),
'</div>'
]);
}
function buildSettingsFooterLeftHtml() {
return joinHtml([
'<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;margin-right:auto;">',
'<button id="rmSettingsResetFooter" type="button" title="Удаляет сохранённые настройки Remover из вашего профиля MediaWiki и возвращает значения по умолчанию." style="', stCancel, '">Сбросить все настройки</button>',
'<a id="rmSettingsReportIssue" href="', getPageUrl('Обсуждение участника:Solidest/Remover'), '" target="_blank" rel="noopener noreferrer" ',
'title="Сообщить о проблеме или предложить улучшение" aria-label="Сообщить о проблеме или предложить улучшение" ',
'class="removerModalLink rmButtonLikeLink" style="', stCancel, 'display:inline-flex;align-items:center;justify-content:center;text-align:center;text-decoration:none;box-sizing:border-box;max-width:100%;line-height:1.2;word-break:normal;overflow-wrap:normal;">Обратная связь</a>',
'</div>'
]);
}
function openSettings() {
var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults);
var menuLabelsHint = buildSettingsMenuItemsHint();
var $previousModal = $('#removerModal').length ? $('#removerModal').detach() : $();
var previousLayoutSyncHandlers = modalLayoutSyncHandlers.slice();
function restorePreviousModal() {
closeModal();
if ($previousModal.length) {
$('#content').prepend($previousModal);
modalLayoutSyncHandlers = previousLayoutSyncHandlers.slice();
if (modalLayoutSyncHandlers.length) $(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
syncModalLayout();
syncLinkWidths();
}
}
createModal({
title: 'Конфигурация',
width: 'compact',
showSettingsButton: false
});
$('#removerModal').addClass('rmModalSettings');
$('#removerModalHeaderBar').append(buildHeaderIconButtonHtml('rmSettingsBack', 'Назад', 'Назад', '←'));
$('#rmSettingsBack').on('click', restorePreviousModal);
$('#removerModalContent').html(buildSettingsFormHtml(menuLabelsHint));
fillSettingsFormValues(currentSettings);
bindMenuTitlePresetControls();
bindSignatureSeparatorPreview();
bindQuickPhrasesEditor();
renderModalFooter('submit', {
showCheckbox: false,
submitText: 'Сохранить',
onSubmit: function () {
var collected = collectSettingsFormValues();
var shouldReset;
var saveFn;
if (collected.error) {
alert(collected.error);
return false;
}
shouldReset = areRemoverSettingsEqual(collected.value, settingsDefaults);
saveFn = shouldReset
? function (callback) { resetSettingsOnServer(callback); }
: function (callback) { saveSettingsToServer(collected.value, callback); };
saveFn(function (err) {
if (err) {
alert('Не удалось ' + (shouldReset ? 'сбросить' : 'сохранить') + ' настройки: ' + (err.info || err.code || 'неизвестная ошибка') + '.');
unlockModalSubmit();
return;
}
location.reload();
});
}
});
var $settingsActions = $('#rmFooterActionButtons');
$settingsActions.wrapInner('<div id="rmSettingsActionButtonsRow"></div>');
$settingsActions.append('<span id="rmSettingsUnsavedHint" role="status" aria-live="polite">Есть несохранённые изменения</span>');
bindSettingsSubmitReadyState(currentSettings);
$('#rmFooterButtons').css('justify-content', 'space-between').prepend(buildSettingsFooterLeftHtml());
$('#rmSettingsResetFooter').on('click', function () {
fillSettingsFormValues(settingsDefaults);
updateSettingsSubmitReadyState(currentSettings);
$('#removerSubmit').trigger('focus');
});
}
// ─── Завершение обработки ────────────────────────────────────────────────
function finalizeSuccess(nominationInfo, usePageReload) {
if (isError) {
var $box = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent');
$box.append('<p class="error">При выполнении скрипта произошли ошибки.</p>');
markSubmitError();
return;
}
renderModalFooter('reload');
if (nominationInfo && nominationInfo.pageTitle) {
appendNominationLink(nominationInfo.pageTitle, nominationInfo.sectionTitle);
}
if (!usePageReload && !nominationInfo) location.reload();
}
function finalizeFastRemoval(notifiedPages, summary) {
if (isError || !setAlert || !notifiedPages || !notifiedPages.length) {
finalizeSuccess(null, false);
return;
}
notifyAuthorsForPages(notifiedPages, {
summary: summary,
actionText: 'к быстрому удалению'
}, function () {
finalizeSuccess(null, false);
});
}
// ─── Общий runner ────────────────────────────────────────────────────────
/**
* Универсальный запуск полного пайплайна номинации.
* @param {Object} o
* templateStep — функция (next) → обработка шаблонов на статьях
* nominationStep — функция (done) → публикация номинации, done(err, {pageTitle, sectionTitle})
* notifyStep — функция (nominationInfo, next)
* skipNotify — boolean
* skipLink — boolean, не показывать ссылку на номинацию
*/
function runFlow(o) {
runNominationPipeline({
templateStep: o.templateStep,
nominationStep: o.nominationStep,
notifyStep: o.notifyStep || function (info, next) { next(); },
skipNotify: o.skipNotify,
onSuccess: function (ctx) {
if (isError) { markSubmitError(); return; }
renderModalFooter('reload');
if (!o.skipLink && ctx.nominationInfo && ctx.nominationInfo.pageTitle) {
appendNominationLink(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle);
}
},
onFailure: function () { markSubmitError(); }
});
}
// ═══════════════════════════════════════════════════════════════════════════
// ЯДРО: обработка статей (apply template + nomination page)
// ═══════════════════════════════════════════════════════════════════════════
/**
* Применяет шаблон к одной статье/категории.
* Понимает режим inArticle (вставка через <noinclude>),
* режим closeAction (снятие шаблона + запись на СО),
* режим cleanupAction (снятие КБУ/КУЛ).
*
* @param {string} pg — название страницы
* @param {Object} job — параметры задания (см. buildJob)
* @param {function} callback(err, meta)
*/
function applyTemplateToPage(pg, job, callback) {
var mode = job.mode;
// ── Снятие КБУ/КУЛ ──────────────────────────────────────────────────
if (mode === 'cleanup') {
var tm = job.transferMode || 'none';
if (tm === 'none') { callback({ code: 'error', info: 'Не выбран режим снятия шаблонов.' }); return; }
editPageContent(pg, { summary: job.summary, watchlist: 'nochange', readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var local = removeTransferTemplatesLocal(article, tm);
removeTransferTemplatesWithApiFallback(pg, local.text, tm, local, function (updated) {
if (updated.text === article) { done({ error: { code: 'error', info: 'Шаблоны для снятия не найдены.' } }); return; }
done({ text: updated.text });
});
}, function (err) { callback(err); });
return;
}
// ── Подведение итогов по КУ/КПМ (снятие + итог на СО) ───────────────
if (mode === 'denom') {
getTextWithTimestamp(pg, function (article, baseTimestamp, readErr) {
if (readErr) { callback(makeReadError(readErr, 'read_failed', 'Не удалось получить содержимое страницы «' + pg + '».')); return; }
if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; }
if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; }
var tplPattern = job.sourceTemplate.split('|').map(function (alias) {
return escapeRegExp(alias.trim()).replace(/\s+/g, '[ _]*');
}).join('|');
var tpl = findTemplateByPattern(article, tplPattern);
if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; }
var normalizedTplDate = convertToStandardDate(tpl.params[0]);
var tplExtra = tpl.params.slice(1).join('|').trim();
if (!RE_DATE_ISO.test(normalizedTplDate)) {
callback({ code: 'error', info: 'Не удалось распознать дату в шаблоне: «' + (tpl.params[0] || '') + '».' });
return;
}
var date = getDate(normalizedTplDate);
var nomPlace = 'ВП:' + job.sourceTemplate.replace(/\|.*/, '') + '/' + date[1];
var retTalkSection = '';
var sectionNW, tplpar, newTalkTpl;
if (job.closeType === 'doneRnm') { sectionNW = job.oldTitle + ' → ' + pg; tplpar = job.oldTitle + '|' + pg; }
if (job.closeType === 'noRnm') { sectionNW = pg + ' → ' + tplExtra; tplpar = pg + '|' + tplExtra; }
if (job.closeType === 'ret' || job.closeType === 'retConditional') {
retTalkSection = tplExtra;
sectionNW = retTalkSection || pg;
tplpar = retTalkSection ? ('l1=' + retTalkSection) : '';
}
var editSummary = makeSummary('номинация [[' + (nomPlace ? nomPlace + '#' : '') + sectionNW + ']] — ' + job.resultTemplate);
var talkTitle = getTalkPage(pg);
newTalkTpl = (job.closeType === 'retConditional')
? buildConditionalRetTemplateText(date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline, 1)
: (T_OPEN + job.resultTemplate + '|' + date[0] + '|' + tplpar + T_CLOSE);
getTextWithTimestamp(talkTitle, function (talkText, talkTimestamp, talkReadErr) {
if (talkReadErr) { callback(makeReadError(talkReadErr, 'talk_read_failed', 'Не удалось получить содержимое СО страницы «' + pg + '».')); return; }
var sourceTalkText = talkText || '';
var talkResult = (job.closeType === 'ret')
? upsertRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection)
: (job.closeType === 'retConditional')
? upsertConditionalRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline)
: { text: insertTplOnTalkPage(sourceTalkText, newTalkTpl, '\n'), status: 'created' };
function saveArticle() {
var cleaned = stripTemplatesByPattern(article, tplPattern).text;
var ep = { title: pg, text: cleaned, summary: editSummary, watchlist: 'nochange', assertuser: mwCfg.wgUserName };
if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (t) {
var editErr = t && t.error ? t.error : null;
callback(editErr, editErr ? null : {
discussionPage: nomPlace,
discussionSection: sectionNW,
summary: editSummary
});
});
}
if (talkResult.text === sourceTalkText) { saveArticle(); return; }
var talkEp = { title: talkTitle, text: talkResult.text, summary: editSummary };
if (talkTimestamp) talkEp.basetimestamp = talkTimestamp;
apiReq(talkEp, 'edit', function (talkResp) {
if (talkResp && talkResp.error) { callback({ code: 'talk_failed', info: 'Не удалось записать итог на СО: ' + talkResp.error.info }); return; }
saveArticle();
});
});
});
return;
}
// ── Обычная номинация: вставка шаблона в статью ─────────────────────
// mode === 'nominate'
var isKu = job.opId === 'tRm' || job.opId === 'mRm';
editPageContent(pg, { summary: job.summary, readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var hasExistingKu = isKu && RE_KU_ON_PAGE.test(article);
var conflictDecision = getConflictDecisionForPage(job, pg);
function buildResult(finalText) {
var generatedTpl = buildGeneratedNominationTemplateText(job, pg);
return { text: generatedTpl ? wrapInNoinclude(finalText, generatedTpl) : finalText };
}
function finishConflictResolution(sourceText) {
var resolvedText;
var pageLink = buildQuotedStatusPageLink(pg);
if (conflictDecision.templateAction === 'keep') {
if (sourceText !== article) {
return {
text: sourceText,
meta: { successMessage: 'Шаблон на странице ' + pageLink + ' оставлен без изменений; шаблоны переноса сняты.' }
};
}
return { skip: true, meta: { successMessage: 'Шаблон на странице ' + pageLink + ' оставлен без изменений.' } };
}
resolvedText = applyConflictTemplateResolution(sourceText, job, pg, conflictDecision);
return {
text: resolvedText,
meta: {
successMessage: conflictDecision.templateAction === 'overwrite'
? 'Шаблон КУ на странице ' + pageLink + ' перезаписан новой датой.'
: 'Новый шаблон КУ добавлен сверху на странице ' + pageLink + '.'
}
};
}
if (hasExistingKu && (!conflictDecision || conflictDecision.pageAction !== 'keep')) {
return { error: { code: 'error', info: 'На странице уже стоит шаблон КУ.' } };
}
if (hasExistingKu && conflictDecision && conflictDecision.pageAction === 'keep') {
if (job.transferMode && job.transferMode !== 'none') {
var localConflict = removeTransferTemplatesLocal(article, job.transferMode);
removeTransferTemplatesWithApiFallback(pg, localConflict.text, job.transferMode, localConflict, function (updated) {
done(finishConflictResolution(updated.text));
});
return;
}
return finishConflictResolution(article);
}
if (isKu && job.transferMode && job.transferMode !== 'none') {
var local = removeTransferTemplatesLocal(article, job.transferMode);
removeTransferTemplatesWithApiFallback(pg, local.text, job.transferMode, local, function (updated) { done(buildResult(updated.text)); });
return;
}
return buildResult(article);
},
function (err) { callback(err); }
);
}
/**
* Обрабатывает список страниц последовательно.
* @param {string[]} pages
* @param {Object} job
* @param {function} onDone(notifiedPages, err, pageMeta)
*/
function processPageList(pages, job, onDone) {
var notifiedPages = [];
var pageMeta = {};
eachSequential(pages.slice().reverse(), function (pg, nextPage) {
var pageLink = buildQuotedStatusPageLink(pg);
var statusId = logStatus('Обрабатывается страница ' + pageLink + '...', null, { pending: true, trackError: false });
applyTemplateToPage(pg, job, function (err, meta) {
var normPg = normTitle(pg);
var isClose = job.mode === 'cleanup' || job.mode === 'denom';
if (!isClose) {
if (!err && meta && meta.successMessage) logStatus(meta.successMessage, null, { statusId: statusId, trackError: false });
else logPageEdit(pg, err, { statusId: statusId });
} else {
if (err) { logStatus('Завершение по странице ' + pageLink, err, { statusId: statusId }); }
else {
logStatus('Шаблон снят со страницы ' + pageLink + '.', null, { statusId: statusId, trackError: false });
if (job.mode === 'denom') logStatus('Шаблон установлен на СО страницы ' + pageLink + '.', null, { trackError: false });
}
}
if (!err) {
notifiedPages.push(pg);
if (meta) pageMeta[normPg] = meta;
}
nextPage(err || null);
});
}, function (err) { onDone(notifiedPages, err, pageMeta); });
}
// ═══════════════════════════════════════════════════════════════════════════
// ПОСТРОЕНИЕ JOB из формы
// ═══════════════════════════════════════════════════════════════════════════
/**
* Строит объект job из данных формы для операции номинации (tRm, rnm, imp, merge, split, recov).
* @param {Object} op — запись из OPERATIONS
* @param {string} pg — целевая страница (уже разрешённая)
* @param {boolean} isMulti — режим мультиноминации
* @returns {Object|false} — job или false при ошибке ввода
*/
function buildNominationJob(op, pg, isMulti) {
var nom = op.nomination;
var date = getDate();
var msg = normalizeQuickPhraseValue($('#rmMsg').val());
var rawMsg = msg;
var opId = isMulti ? 'mRm' : op.id;
var tplpar = '';
var section, sectionNW, extraPages, multiArticles = [];
var multiHeaderText = '';
var multiArticleComments = {};
// Вычислить section и tplpar в зависимости от типа дополнительного ввода
if (nom.extraInput) {
var ei = nom.extraInput;
if (ei.type === 'rename') {
var rn = collectInputValues('.rmRenameInput');
if (!rn.length) { alert('Укажите новое название.'); return false; }
tplpar = rn[0] + (rn.length > 1 ? '||' + rn.slice(1).join('|') : '');
section = '[[:' + pg + ']] → ' + rn.map(function (n) { return '[[:' + n + ']]'; }).join(', ');
} else if (ei.type === 'merge') {
var mn = collectInputValues('.rmMergeInput');
if (!mn.length) { alert('Укажите статью для объединения.'); return false; }
tplpar = pg + '|' + mn.join('|');
extraPages = mn;
section = formatPagesWithAnd([pg].concat(mn));
} else if (ei.type === 'split') {
var sn = collectInputValues('.rmSplitInput');
if (!sn.length) { alert('Укажите статьи для разделения.'); return false; }
tplpar = formatPagesWithAnd(sn);
section = '[[:' + pg + ']] → ' + tplpar;
}
}
if (isMulti) {
var ttl = $('#rmHeader').val() || '';
var articles = collectInputValues('.rmMultiPageInput');
var multiFormat = $('.rmArticleMultiFormatBtn.is-active').data('rmMultiFormat') || 'sections';
multiArticleComments = collectMultiNominationComments();
multiHeaderText = ttl;
multiArticles = articles.slice();
section = ttl;
msg = multiFormat === 'list'
? buildMultiNominationListText(articles, rawMsg, multiArticleComments)
: buildMultiNominationText(articles, rawMsg, multiArticleComments);
}
if (!section) section = '[[:' + pg + ']]';
sectionNW = section.replace(/\[\[:/g, '').replace(/]]/g, '');
var nomPageDate = date[1];
var nomPage = nom.nomPage(nomPageDate);
var summary = makeSummary('номинация [[' + nomPage + '#' + sectionNW + ']]');
return {
mode: 'nominate',
opId: opId,
op: op,
date: date,
tplpar: tplpar,
articleTpl: nom.articleTpl || function () { return ''; },
inArticle: nom.inArticle !== false,
transferMode: (nom.supportsTransfer ? getTransferModeFromButtons() : 'none'),
summary: summary,
msg: msg,
nomPage: nomPage,
navTemplate: nom.navTemplate,
section: section,
sectionNW: sectionNW,
comment: nom.comment || '',
extraPages: extraPages || [],
isMulti: !!isMulti,
multiHeaderText: multiHeaderText,
multiNominationBody: rawMsg,
multiArticleComments: multiArticleComments,
multiNominationFormat: multiFormat || 'sections',
multiArticles: multiArticles,
pages: isMulti ? multiArticles.slice().reverse() : ([pg].concat(extraPages || []))
};
}
function getTransferModeFromButtons() {
var kbu = $('#rmTransferBtnKbu').hasClass('is-active');
var kul = $('#rmTransferBtnKul').hasClass('is-active');
if (kbu && kul) return 'both';
if (kbu) return 'kbu';
if (kul) return 'kul';
return 'none';
}
function buildKbuFormHtml(reasons) {
return joinHtml([
'<select id="rmSel" style="', stInputFull, '">',
reasons.map(function (r, i) { return '<option value="' + i + '">' + r[1] + '</option>'; }).join(''),
'</select>',
'<input id="fiRm" type="hidden" style="', stInputFull, '">',
'<input id="fiRmComment" type="text" placeholder="Комментарий (необязательно)" style="', stInputFull, '">',
buildQuickPhrasesPanelHtml('fiRmComment')
]);
}
function buildNominationMultiHeaderHtml(pg, options) {
var opts = options || {};
var multiHeaderRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
return joinHtml([
'<div id="rmMultiHeader" class="', RESIZE_CLASS, '" style="display:none;margin-bottom:6px;">',
'<div class="rmMultiPageRow rmNominationHeaderRow" style="', multiHeaderRowStyle, '"><input id="rmHeader" type="text" placeholder="Заголовок номинации" style="', stInputBox, '">',
buildAddMultiPageButtonHtml(opts), '</div>',
'</div>',
'<div id="rmMultiPagesContainer" style="display:flex;flex-direction:column;gap:', multiNominationGap, ';margin-bottom:', multiNominationGap, ';">',
buildMultiPageRowHtml(0, $.extend({}, opts, { rowId: 'rmFirstMultiPage', pageValue: pg, showAdd: true })),
'</div>'
]);
}
function buildTransferBoxHtml() {
return joinHtml([
'<div id="rmTransferBox" class="', RESIZE_CLASS, ' rmTransferPanel" style="width:100%;box-sizing:border-box;"><div class="rmTransferGrid">',
'<div id="rmTransferModeSingle" class="rmSegmentedBar"><button type="button" id="rmTransferBtnNone" class="rmSegmentedBtn rmToggleBtn is-active" aria-pressed="true">Обычная номинация</button></div>',
'<div id="rmTransferModeGroup" class="rmSegmentedBar">',
'<button type="button" id="rmTransferBtnKbu" class="rmSegmentedBtn rmToggleBtn" aria-pressed="false" title="Шаблоны db-*, уд-*, КОУ, Hangon.">Снять КБУ</button>',
'<button type="button" id="rmTransferBtnKul" class="rmSegmentedBtn rmToggleBtn" aria-pressed="false" title="Шаблон «К улучшению».">Снять КУЛ</button>',
'</div>',
'<div class="rmTransferHintRow"><div id="rmTransferHint" style="display:none;font-size:12px;line-height:1.35;color:', tk.cSubM, ';"></div></div>',
'</div></div>'
]);
}
function buildMultiNominationFormatSwitchHtml(wrapId, buttonClass) {
return joinHtml([
'<div id="', wrapId, '" class="', RESIZE_CLASS, '" style="display:none;margin-top:8px;margin-bottom:10px;">',
'<div class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn ', buttonClass, ' is-active" data-rm-multi-format="sections" aria-pressed="true">Оформить подразделами</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn ', buttonClass, '" data-rm-multi-format="list" aria-pressed="false">Оформить списком</button>',
'</div>',
'</div>'
]);
}
function buildNominationFormHtml(nom, pg, multiMode) {
return joinHtml([
multiMode ? buildNominationMultiHeaderHtml(pg, {
inputPlaceholder: 'Статья',
addTitle: 'Добавить статью',
removeTitle: 'Удалить статью',
commentPlaceholder: 'Комментарий только для этой статьи (необязательно)'
}) : '',
nom.extraInput ? buildMultiInputHtml(nom.extraInput) : '',
'<textarea id="rmMsg" placeholder="Текст номинации без подписи"></textarea>',
buildQuickPhrasesPanelHtml('rmMsg'),
nom.supportsTransfer ? buildTransferBoxHtml() : '',
multiMode ? buildMultiNominationFormatSwitchHtml('rmArticleMultiFormatWrap', 'rmArticleMultiFormatBtn') : ''
]);
}
function buildCategoryNominationFormHtml(variantConfig, multiMode, pageName) {
return joinHtml([
multiMode ? buildNominationMultiHeaderHtml(pageName, {
inputPlaceholder: 'Категория',
addTitle: 'Добавить категорию',
removeTitle: 'Удалить категорию',
commentPlaceholder: 'Комментарий только для этой категории (необязательно)'
}) : '',
variantConfig ? buildMultiInputHtml(variantConfig) : '',
'<textarea id="nominationReason" placeholder="Текст номинации без подписи"></textarea>',
buildQuickPhrasesPanelHtml('nominationReason'),
multiMode ? buildMultiNominationFormatSwitchHtml('rmCategoryMultiFormatWrap', 'rmCategoryMultiFormatBtn') : ''
]);
}
function ensureMultiPageCommentTextareaResizer(textareaId, wrapId) {
var $textarea = $('#' + textareaId);
if (!$textarea.length || $textarea.data('rmNestedResizerReady')) return;
setupNestedResizableTextarea(textareaId, wrapId, parseInt(sz.taMinW, 10) || 180, 90);
$textarea.data('rmNestedResizerReady', true);
}
function setMultiPageCommentExpanded($btn, expanded) {
var wrapId = $btn.data('rmCommentWrap');
var textareaId = $btn.data('rmCommentTextarea');
var $wrap = $('#' + wrapId);
if (!$btn.length || !$wrap.length) return;
$btn.attr('aria-expanded', expanded ? 'true' : 'false')
.toggleClass('is-active', expanded)
.text(expanded ? 'Скрыть комментарий' : 'Комментарий');
$wrap.toggle(expanded);
if (expanded) ensureMultiPageCommentTextareaResizer(textareaId, wrapId);
}
function getMultiPageCommentTargets($block) {
var $textarea = $block.find('.rmMultiPageCommentInput').first();
var $wrap = $textarea.parent();
return {
wrapId: $wrap.attr('id') || '',
textareaId: $textarea.attr('id') || ''
};
}
function setMultiPageRowControls($block, showAdd, showComment, options) {
var opts = options || {};
var $row = $block.find('.rmMultiPageRow').first();
var ids = getMultiPageCommentTargets($block);
var $commentBtn = $row.find('.rmMultiPageCommentToggle');
if (!$row.length) return;
if (showAdd) {
if ($commentBtn.length) setMultiPageCommentExpanded($commentBtn, false);
if (ids.wrapId) $('#' + ids.wrapId).hide();
$row.find('.rmMultiPageCommentToggle,.rmRemoveInput').remove();
if (!$row.find('.rmAddMultiPage').length) $row.append(buildAddMultiPageButtonHtml(opts));
return;
}
$row.find('.rmAddMultiPage').remove();
if (!$row.find('.rmMultiPageCommentToggle').length) {
$row.append(buildMultiPageButtonsHtml(ids.wrapId, ids.textareaId, $.extend({}, opts, { showComment: showComment })));
return;
}
$row.find('.rmMultiPageCommentToggle').toggle(showComment);
}
function setupMultiPageNominationUi(options) {
var opts = options || {};
var containerSelector = opts.containerSelector || '#rmMultiPagesContainer';
var pageCounter = parseInt(opts.nextIndex, 10) || 1;
var wasMultiModeExpanded = false;
function restoreEmptySinglePageInput() {
var $pageInput = $(containerSelector + ' .rmMultiPageInput').first();
if (!$pageInput.length || String($pageInput.val() || '').trim()) return;
$pageInput.val(opts.defaultPage || '');
}
function updateMultiMode() {
var $blocks = $(containerSelector + ' .rmMultiPageBlock');
var hasExtra = $blocks.length > 1;
$('#rmMultiHeader').toggle(hasExtra);
if (opts.multiOnlySelector) $(opts.multiOnlySelector).toggle(hasExtra);
if (!hasExtra && wasMultiModeExpanded) restoreEmptySinglePageInput();
$blocks.each(function (index) {
setMultiPageRowControls($(this), !hasExtra && index === 0, hasExtra, opts);
});
wasMultiModeExpanded = hasExtra;
syncModalLayout();
}
$(document).off('click.rmMultiPageAdd').on('click.rmMultiPageAdd', '.rmAddMultiPage', function () {
$(containerSelector).append(buildMultiPageRowHtml(pageCounter++, opts));
updateMultiMode();
});
$(document).off('click.rmMultiPageComment').on('click.rmMultiPageComment', '.rmMultiPageCommentToggle', function () {
var $btn = $(this);
if (!$btn.is(':visible')) return;
setMultiPageCommentExpanded($btn, $btn.attr('aria-expanded') !== 'true');
syncModalLayout();
});
$(document).off('click.rmMultiPageRemove').on('click.rmMultiPageRemove', '.rmMultiPageRow .rmRemoveInput', function () {
$(this).closest('.rmMultiPageBlock').remove();
updateMultiMode();
});
updateMultiMode();
return {
update: updateMultiMode,
isMulti: function () { return $(containerSelector + ' .rmMultiPageBlock').length > 1; }
};
}
function bindMultiNominationFormatSwitch(rootSelector, buttonSelector) {
var $root = $(rootSelector);
$root.off('click.rmMultiFormat').on('click.rmMultiFormat', buttonSelector, function () {
var $btn = $(this);
$root.find(buttonSelector).removeClass('is-active').attr('aria-pressed', 'false');
$btn.addClass('is-active').attr('aria-pressed', 'true');
});
}
function buildProtectAddButtonHtml() {
return buildSquareAddButtonHtml('', 'Добавить страницу', 'rmProtectAddPage');
}
function buildProtectPageRowHtml(id, pageName, isFirstRow) {
return joinHtml([
'<div', isFirstRow ? ' id="rmProtectFirstRow"' : '', ' class="rmProtectPageRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="', id, '" type="text" placeholder="Страница" class="rmProtectPageInput" style="', stInputBox, '"',
pageName ? ' value="' + escapeHtml(pageName) + '"' : '', '>',
isFirstRow
? buildProtectAddButtonHtml()
: '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>',
'</div>'
]);
}
function buildReportFormHtml(ctx, isZka) {
var reportTextPlaceholder = (isZka ? 'Введите текст запроса.' : 'Текст запроса (можно дополнить или изменить).') + ' Подпись будет добавлена автоматически.';
if (isZka) {
return joinHtml([
'<input id="rmReportHeader" type="text" placeholder="Тема/заголовок" style="', stInputFull, '" value="', escapeHtml(ctx.pageLink), '">',
'<textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>',
buildQuickPhrasesPanelHtml('rmReportText')
]);
}
return joinHtml([
'<div id="rmProtectModeBtns" class="', RESIZE_CLASS, '" style="margin-bottom:14px;"><div class="rmSegmentedBar">',
'<button id="rmProtectModeInstall" type="button" class="rmSegmentedBtn rmProtectModeBtn is-active" aria-pressed="true">🛡️ Установить защиту</button>',
'<button id="rmProtectModeRemove" type="button" class="rmSegmentedBtn rmProtectModeBtn" aria-pressed="false">📛 Снять защиту</button>',
'</div></div>',
'<div id="rmProtectMultiWrap" class="', RESIZE_CLASS, '">',
'<div id="rmProtectHeaderWrap" style="display:none;margin-bottom:6px;"><div class="rmProtectHeaderRow" style="', stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'), '"><input id="rmProtectHeader" type="text" placeholder="Заголовок (для нескольких страниц)" style="', stInputBox, '">', buildProtectAddButtonHtml(), '</div></div>',
buildProtectPageRowHtml('rmProtectPage0', ctx.pageName, true),
'<div id="rmProtectPagesContainer"></div>',
'</div>',
'<div id="rmProtectLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmProtectLevels" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>',
'</div></div>',
'<div id="rmProtectReasonsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Причины</div><div id="rmProtectReasons" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="война правок">война правок</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="неконсенсусные изменения">неконсенсусные изменения</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="вандализм">вандализм</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="популярная статья">популярная статья</button>',
'</div></div>',
'<div id="rmRemoveLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup" style="display:none;"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmRemoveLevels" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>',
'</div></div>',
'<div id="rmProtectTextBlock" class="', RESIZE_CLASS, '"><textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>',
buildQuickPhrasesPanelHtml('rmReportText'), '</div>'
]);
}
// ═══════════════════════════════════════════════════════════════════════════
// ОБРАБОТЧИКИ ОПЕРАЦИЙ
// ═══════════════════════════════════════════════════════════════════════════
var handlers = {
// ── КБУ ─────────────────────────────────────────────────────────────
showKbu: function (op) {
var forCategory = !!(op && op.forCategory);
var reasons = getFastRemoveReasons();
createModal({
title: 'Быстрое удаление',
width: 'compact',
subtitleHtml: '<span id="rmKbuCriteriaLinkWrap"></span>'
});
$('#removerModalContent').html(buildKbuFormHtml(reasons));
function updateKbuReasonControls() {
var reason = reasons[$('#rmSel').val()] || reasons[0];
var paramCfg = reason ? cfg.requiredParamTemplates[reason[0]] : null;
var showComment = true;
$('#rmKbuCriteriaLinkWrap').html(buildFastRemoveCriteriaLinkHtml(reason));
if (paramCfg) {
var noComment = paramCfg.charAt(0) === '!';
$('#fiRm').attr({ type: 'text', placeholder: 'Укажите ' + (noComment ? paramCfg.substring(1) : paramCfg) }).show();
showComment = !noComment;
} else {
$('#fiRm').attr('type', 'hidden').hide();
}
$('#fiRmComment').toggle(showComment);
$('.rmQuickPhrasesPanel[data-rm-target="fiRmComment"]').toggle(showComment);
}
$('#rmSel').change(updateKbuReasonControls);
$('#rmSel').trigger('change');
renderModalFooter('submit', {
submitText: 'Номинировать',
onSubmit: function () {
var idx = $('#rmSel').val();
var addInfo = $('#fiRm').val();
var comment = $('#fiRmComment').val();
startProcessing();
if (forCategory) {
var tpl = reasons[idx][0];
var categorySummary = makeSummary('номинация категории на быстрое удаление');
if (addInfo) tpl += '|1=' + addInfo;
if (comment) tpl += '|' + (addInfo ? '2' : '1') + '=' + comment;
editPageContent(mwCfg.wgPageName, { summary: categorySummary, readError: 'Не удалось получить содержимое.' },
function (text) { return { text: wrapInNoinclude(text, T_OPEN + tpl + T_CLOSE) }; },
function (err) {
if (err) {
unlockModalSubmit();
logStatus('Ошибка записи.', err);
} else {
logStatus('Страница номинирована к быстрому удалению.', null, { trackError: false });
finalizeFastRemoval([normTitle(mwCfg.wgPageName)], categorySummary);
}
});
} else {
var job = {
mode: 'nominate', opId: 'fRm',
kbuTemplate: reasons[idx][0], kbuAddInfo: addInfo, kbuComment: comment,
summary: makeSummary('номинация к [[ВП:КБУ|быстрому удалению]]'),
inArticle: true
};
processPageList([normTitle(mwCfg.wgPageName)], job, function (notifiedPages) {
finalizeFastRemoval(notifiedPages, job.summary);
});
}
return true;
}
});
},
// ── Универсальная номинация (КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС) ────────
showNomination: function (op) {
var nom = op.nomination;
var pg = normTitle(mwCfg.wgPageName);
var date = getDate()[1];
var nomPage = nom.nomPage(date);
var multiMode = nom.supportsMulti;
function updateTransferUi() {
var mode = getTransferModeFromButtons();
var isNone = mode === 'none';
var isKbu = mode === 'kbu' || mode === 'both';
var isKul = mode === 'kul' || mode === 'both';
$('#rmTransferBtnNone').toggleClass('is-active', isNone).attr('aria-pressed', isNone ? 'true' : 'false');
$('#rmTransferBtnKbu').toggleClass('is-active', isKbu).attr('aria-pressed', isKbu ? 'true' : 'false');
$('#rmTransferBtnKul').toggleClass('is-active', isKul).attr('aria-pressed', isKul ? 'true' : 'false');
var t = transferTexts[mode];
if (t) { $('#rmTransferHint').text(t.hint).show(); } else { $('#rmTransferHint').hide().text(''); }
applyGeneratedText($('#rmMsg'), t && t.notice ? t.notice + '\n' : '');
}
createModal({
title: 'Номинация: ' + nom.template,
subtitlePage: nomPage,
subtitleLabel: 'Текущий день'
});
$('#removerModalContent').html(buildNominationFormHtml(nom, pg, multiMode));
setupResizableModal('rmMsg');
// Логика переноса
if (nom.supportsTransfer) {
$(document).off('click.rmTransfer').on('click.rmTransfer', '#rmTransferBtnNone,#rmTransferBtnKbu,#rmTransferBtnKul', function () {
if (this.id === 'rmTransferBtnNone') {
$('#rmTransferBtnKbu,#rmTransferBtnKul').removeClass('is-active');
$('#rmTransferBtnNone').addClass('is-active');
} else {
$(this).toggleClass('is-active');
var anyOn = $('#rmTransferBtnKbu').hasClass('is-active') || $('#rmTransferBtnKul').hasClass('is-active');
$('#rmTransferBtnNone').toggleClass('is-active', !anyOn);
}
updateTransferUi();
});
updateTransferUi();
}
// Многостраничный режим
if (multiMode) {
setupMultiPageNominationUi({ defaultPage: pg, multiOnlySelector: '#rmArticleMultiFormatWrap' });
bindMultiNominationFormatSwitch('#removerModalContent', '.rmArticleMultiFormatBtn');
}
if (nom.extraInput) wireMultiInput(nom.extraInput);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var isMulti = multiMode && $('#rmMultiPagesContainer .rmMultiPageBlock').length > 1;
var inputVal = !isMulti ? normTitle($('#rmMultiPagesContainer .rmMultiPageInput').first().val() || '') : '';
var changed = inputVal && inputVal !== pg;
function executeJob(job) {
startProcessing();
runFlow({
templateStep: function (next) {
if (!job.inArticle) { next(); return; }
processPageList(job.pages, job, function (notifiedPages, err) {
job._notifiedPages = notifiedPages;
next(err);
});
},
nominationStep: function (done) {
publishNomination({
pageTitle: job.nomPage,
navTemplate: job.navTemplate,
sectionTitle: job.section,
summary: job.summary,
text: getNominationPublishText(job)
}, function (err) { done(err, { pageTitle: job.nomPage, sectionTitle: job.section }); });
},
notifyStep: function (nominationInfo, next) {
var pages = job._notifiedPages || [];
if (!setAlert || !pages.length) { next(); return; }
notifyAuthorsForPages(pages, {
summary: job.summary,
actionText: job.comment,
discussionPage: nominationInfo && nominationInfo.pageTitle,
discussionSection: nominationInfo && nominationInfo.sectionTitle
}, next);
},
skipLink: op.id === 'fRm'
});
}
function run(targetPg) {
var job = buildNominationJob(op, targetPg, isMulti);
if (!job) { unlockModalSubmit(); return; }
if (job.isMulti && job.inArticle && getNominationConflictRule(job)) {
startProcessing();
inspectMultiNominationConflicts(job, function (err, conflicts) {
if (err) { markSubmitError(); return; }
if (!conflicts.length) { executeJob(job); return; }
showNominationConflictResolution(job, conflicts, function (resolvedJob) {
executeJob(resolvedJob);
});
});
return;
}
executeJob(job);
}
if (changed) {
apiReq({ prop: 'info', titles: inputVal }, 'query', function (data) {
if (data && data.error) {
unlockModalSubmit();
alert('Не удалось проверить страницу из-за ошибки API. Попробуйте ещё раз.');
return;
}
var page = getFirstQueryPage(data);
if (!page) {
unlockModalSubmit();
alert('Не удалось проверить страницу из-за временной ошибки. Попробуйте ещё раз.');
return;
}
if (page.missing !== undefined) {
unlockModalSubmit();
alert('Страница «' + inputVal + '» не существует.');
return;
}
run(normTitle(page.title || inputVal));
});
} else {
run(pg);
}
return true;
}
});
},
// ── Снятие номинации (статья) ────────────────────────────────────────
showArticleClose: function () {
showCloseActionsModal({
inputName: 'rmCloseAction',
listId: 'rmCloseActions',
emptyText: 'Не найдено подходящих шаблонов для закрытия.',
emptyDetails: 'Проверяются: КУ, КПМ, КБУ, КУЛ.',
getActions: function (articleText) {
var actions = [];
if (RE_KU_ON_PAGE.test(articleText)) actions.push({ id: 'ret', tag: 'КУ', label: 'Оставлено', mode: 'denom', closeType: 'ret', resultTemplate: 'оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{оставлено}} на СО.', comment: 'оставлена', talkNotice: true });
if (RE_KU_ON_PAGE.test(articleText)) actions.push({ id: 'retConditional', tag: 'КУ', label: 'Условно оставлено', mode: 'denom', closeType: 'retConditional', resultTemplate: 'условно оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{условно оставлено}} на СО.', comment: 'условно оставлена', talkNotice: true, needsConditionalFields: true });
if (RE_KPM_ON_PAGE.test(articleText)) actions.push({ id: 'doneRnm', tag: 'КПМ', label: 'Переименовано', mode: 'denom', closeType: 'doneRnm', resultTemplate: 'переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{переименовано}} на СО.', comment: 'переименована', talkNotice: true, needsOldTitle: true });
if (RE_KPM_ON_PAGE.test(articleText)) actions.push({ id: 'noRnm', tag: 'КПМ', label: 'Не переименовано', mode: 'denom', closeType: 'noRnm', resultTemplate: 'не переименовано',sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{не переименовано}} на СО.', comment: 'не переименована',talkNotice: true });
var hasKbu = RE_KBU_ON_PAGE.test(articleText);
var hasKul = RE_KUL_ON_PAGE.test(articleText);
if (hasKbu && hasKul) actions.push({ id: 'cleanup-both', tag: 'КБУ и КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'both', cleanupLabel: 'КБУ и КУЛ', description: 'Снимает шаблоны КБУ/КУЛ и Hangon.' });
if (hasKbu) actions.push({ id: 'cleanup-kbu', tag: 'КБУ', label: 'Снятие', mode: 'cleanup', transferMode: 'kbu', cleanupLabel: 'КБУ', description: 'Снимает шаблоны КБУ и Hangon.' });
if (hasKul) actions.push({ id: 'cleanup-kul', tag: 'КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'kul', cleanupLabel: 'КУЛ', description: 'Снимает шаблон КУЛ.' });
return actions;
},
afterRender: function (actions) {
var hasDoneRnm = actions.some(function (a) { return a.id === 'doneRnm'; });
var hasConditionalRet = actions.some(function (a) { return a.id === 'retConditional'; });
if (hasDoneRnm) {
$('#rmCloseActions input[value="doneRnm"]').closest('.rmActionItem').append(
'<div id="rmCloseOldTitleWrap" style="display:none;margin-top:6px;"><input id="rmCloseOldTitle" type="text" placeholder="Старое название" style="' + stInputFull + '"></div>'
);
}
if (hasConditionalRet) {
$('#rmCloseActions input[value="retConditional"]').closest('.rmActionItem').append(buildConditionalRetFieldsHtml());
}
},
afterFooterRender: function (_, actionMap) {
function ensureConditionalTextareaResizer() {
var $textarea = $('#rmCloseConditionalReason');
if (!$textarea.length || $textarea.data('rmConditionalResizerReady')) return;
setupNestedResizableTextarea('rmCloseConditionalReason', 'rmCloseConditionalWrap', 280, 90);
$textarea.data('rmConditionalResizerReady', true);
}
function updateUi() {
var sel = actionMap[$('[name="rmCloseAction"]:checked').val()];
$('#rmCloseOldTitleWrap').toggle(!!(sel && sel.needsOldTitle));
$('#rmCloseConditionalWrap').toggle(!!(sel && sel.needsConditionalFields));
if (sel && sel.needsConditionalFields) ensureConditionalTextareaResizer();
var disableNotify = !!(sel && sel.mode === 'cleanup' && sel.transferMode === 'kbu');
var $cb = $('[name="rmUAlert"]');
var $cbLabel = $('[name="rmUAlert"]').closest('label');
if ($cb.length) $cb.prop('disabled', disableNotify);
if ($cbLabel.length) $cbLabel.css({
visibility: disableNotify ? 'hidden' : 'visible',
pointerEvents: disableNotify ? 'none' : ''
});
syncModalLayout();
}
$(document).off('change.rmCloseAction').on('change.rmCloseAction', '[name="rmCloseAction"]', updateUi);
updateUi();
},
onSubmit: function (sel, pageName) {
var job;
if (sel.mode === 'denom') {
var oldTitle = sel.needsOldTitle ? ($('#rmCloseOldTitle').val() || '').trim() : '';
var conditionalReason = sel.needsConditionalFields ? normalizeQuickPhraseValue($('#rmCloseConditionalReason').val()) : '';
var conditionalDeadline = sel.needsConditionalFields ? String($('#rmCloseConditionalDeadline').val() || '').trim() : '';
if (sel.needsOldTitle && !oldTitle) { alert('Укажите старое название.'); return false; }
if (conditionalDeadline && normalizeIsoDate(conditionalDeadline) !== conditionalDeadline) {
alert('Укажите срок в формате YYYY-MM-DD, например 2026-05-31.');
return false;
}
job = {
mode: 'denom',
closeType: sel.closeType,
resultTemplate: sel.resultTemplate,
sourceTemplate: sel.sourceTemplate,
oldTitle: oldTitle,
conditionalReason: conditionalReason,
conditionalDeadline: conditionalDeadline,
notifyActionText: sel.comment,
skipNotify: false
};
} else {
job = {
mode: 'cleanup',
transferMode: sel.transferMode,
summary: makeSummary('снятие шаблонов ' + sel.cleanupLabel),
notifyActionText: (sel.transferMode === 'kul' || sel.transferMode === 'both')
? 'больше не номинирована к срочному улучшению'
: '',
skipNotify: !(sel.transferMode === 'kul' || sel.transferMode === 'both')
};
}
processPageList([pageName], job, function (notifiedPages, err, pageMeta) {
function finishClose() {
if (isError) { markSubmitError(); }
else { renderModalFooter('reload'); }
}
if (isError || err || job.skipNotify || !setAlert || !notifiedPages.length) { finishClose(); return; }
var meta = (pageMeta && pageMeta[normTitle(pageName)]) || {};
notifyAuthorsForPages(notifiedPages, {
summary: meta.summary || job.summary,
actionText: job.notifyActionText,
discussionPage: meta.discussionPage,
discussionSection: meta.discussionSection,
includeProposedPrefix: false
}, finishClose);
});
return true;
}
});
},
// ── ОБКАТ: номинация категории ───────────────────────────────────────
showCatNomination: function (op) {
var catType = op.catType;
var titles = { discuss: 'Номинация: обсуждение', deletion: 'Номинация: к удалению', rename: 'Номинация: к переименованию', merge: 'Номинация: к объединению' };
var now = new Date();
var catDiscPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[now.getUTCMonth()] + '_' + now.getUTCFullYear();
var pageName = normalizeCategoryPageName(mwCfg.wgPageName);
var multiMode = catType === 'deletion';
createModal({ title: titles[catType], subtitlePage: catDiscPage, subtitleLabel: 'Текущий месяц' });
var variantCfgs = {
rename: { firstId: 'firstRenameInput', addBtnId: 'addRenameVariant', containerId: 'renameVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Новое название без префикса Категория:', addPh: 'Дополнительный вариант названия' },
merge: { firstId: 'firstMergeInput', addBtnId: 'addMergeVariant', containerId: 'mergeVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Категория для объединения без префикса Категория:', addPh: 'Дополнительный вариант объединения' }
};
var vCfg = variantCfgs[catType];
$('#removerModalContent').html(buildCategoryNominationFormHtml(vCfg, multiMode, pageName));
setupResizableModal('nominationReason');
if (multiMode) {
setupMultiPageNominationUi({
defaultPage: pageName,
inputPlaceholder: 'Категория',
addTitle: 'Добавить категорию',
removeTitle: 'Удалить категорию',
commentPlaceholder: 'Комментарий только для этой категории (необязательно)',
multiOnlySelector: '#rmCategoryMultiFormatWrap'
});
bindMultiNominationFormatSwitch('#removerModalContent', '.rmCategoryMultiFormatBtn');
}
if (vCfg) wireMultiInput(vCfg);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var reason = normalizeQuickPhraseValue($('#nominationReason').val());
var targetPages = multiMode ? collectCategoryPageInputValues('.rmMultiPageInput') : [pageName];
var isMulti = multiMode && targetPages.length > 1;
var multiFormat = $('.rmCategoryMultiFormatBtn.is-active').data('rmMultiFormat') || 'sections';
var commentsByCategory = isMulti ? collectMultiNominationComments(normalizeCategoryPageName) : {};
var discussionTarget = isMulti ? targetPages : targetPages[0];
var discussionReason = isMulti
? (multiFormat === 'list'
? buildMultiNominationListText(targetPages, reason, commentsByCategory)
: buildMultiNominationText(targetPages, reason, commentsByCategory, { headingLevel: 4 }))
: reason;
var discussionOptions = isMulti ? { headerText: $('#rmHeader').val(), reasonIsPrepared: true } : null;
var notifiedPages = [];
if (!reason) { alert('Пожалуйста, укажите причину/тему.'); return false; }
if (!targetPages.length) { alert('Укажите категорию.'); return false; }
var mainName = null, additionalNames = [];
if (vCfg) {
mainName = $('#' + vCfg.firstId).val().trim();
if (!mainName) { alert('Укажите ' + (catType === 'rename' ? 'новое название' : 'категорию для объединения') + '.'); return false; }
additionalNames = collectInputValues('#' + vCfg.containerId + ' .variantInput');
}
startProcessing();
runFlow({
templateStep: function (next) {
addTemplatesToCategories(targetPages, catType, mainName, additionalNames, function (err, processedPages) {
notifiedPages = processedPages || [];
next(err);
});
},
nominationStep: function (done) {
createCategoryDiscussion(discussionTarget, discussionReason, catType, function (err, nominationInfo) { done(err, nominationInfo || null); }, mainName, additionalNames, discussionOptions);
},
notifyStep: function (nominationInfo, next) {
if (!setAlert || !nominationInfo) { next(); return; }
var section = normalizeSectionForLink(nominationInfo.sectionTitle || '');
notifyAuthorsForPages(notifiedPages.length ? notifiedPages : targetPages, {
summary: makeSummary('номинация [[' + nominationInfo.pageTitle + (section ? '#' + section : '') + ']]'),
actionText: { discuss: 'к обсуждению', deletion: 'к удалению', rename: 'к переименованию', merge: 'к объединению' }[catType] || 'к обсуждению',
discussionPage: nominationInfo.pageTitle,
discussionSection: nominationInfo.sectionTitle
}, next);
}
});
return true;
}
});
},
// ── Снятие номинации (категория) ─────────────────────────────────────
showCatClose: function () {
showCloseActionsModal({
inputName: 'rmCategoryCloseAction',
showCheckbox: false,
emptyText: 'Не найдено подходящих шаблонов для завершения.',
emptyDetails: 'Проверяются ОБКАТ и КУ.',
getActions: function (catText) {
var allObkat = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|');
var actions = [];
if (new RegExp('\\{\\{\\s*(?:' + allObkat + ')\\s*(?:\\||\\}\\})', 'i').test(catText)) {
actions.push({ id: 'cat-obkat-done', tag: 'ОБКАТ', label: 'Завершено', mode: 'obkat', talkTemplate: 'Обсуждавшаяся категория', description: 'Снимает шаблон ОБКАТ, добавляет {{Обсуждавшаяся категория}} на СО.', talkNotice: true });
}
if (RE_KU_ON_PAGE.test(catText)) {
actions.push({ id: 'cat-ku-cleanup', tag: 'КУ', label: 'Снятие', mode: 'cleanup', description: 'Снимает шаблон КУ без записи на СО.' });
}
return actions;
},
onSubmit: function (sel, pageName) {
if (sel.mode === 'obkat') markCategoryDiscussionAsDone(pageName);
if (sel.mode === 'cleanup') removeKuFromCategory(pageName);
return true;
}
});
},
// ── Защита / Запрос к администраторам ───────────────────────────────
showReport: function (op) {
var mode = op.reportMode || 'protect';
var ctx = getReporterContext(mode);
var isZka = mode === 'request';
var protectMode = 'install';
var pageCounter = 1;
function buildProtectText(pm) {
if (pm === 'remove') {
var removeLevels = [];
$('#rmRemoveLevels .rmToggleBtn.is-active').each(function () { removeLevels.push($(this).data('label')); });
return removeLevels.length ? 'Просьба снять ' + removeLevels.join(' и/или ') + '.' : '';
}
var levels = [], reasons = [];
$('#rmProtectLevels .rmToggleBtn.is-active').each(function () { levels.push($(this).data('label')); });
$('#rmProtectReasons .rmToggleBtn.is-active').each(function () { reasons.push($(this).data('label')); });
if (!levels.length && !reasons.length) return '';
var text = 'Просьба установить';
if (levels.length) text += ' ' + levels.join(' и/или ');
if (reasons.length) text += ' по причине: ' + reasons.join(', ');
return text + '.';
}
function applyProtectMode(m) {
protectMode = m;
var isInstall = m === 'install';
$('#rmProtectModeInstall').toggleClass('is-active', isInstall).attr('aria-pressed', isInstall ? 'true' : 'false');
$('#rmProtectModeRemove').toggleClass('is-active', !isInstall).attr('aria-pressed', !isInstall ? 'true' : 'false');
$('#removerModalTitleText').text(isInstall ? 'Запрос на защиту страницы' : 'Запрос на снятие защиты');
var linkPage = isInstall ? 'Википедия:Установка защиты' : 'Википедия:Снятие защиты';
$('#rmProtectLinkWrap').html('<a href="' + getPageUrl(linkPage) + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + escapeHtml(linkPage) + '</a>');
$('#rmProtectLevelsWrap,#rmProtectReasonsWrap').toggle(isInstall);
$('#rmRemoveLevelsWrap').toggle(!isInstall);
$('#rmProtectLevels .rmProtectOptBtn,#rmProtectReasons .rmProtectOptBtn,#rmRemoveLevels .rmProtectOptBtn').removeClass('is-active');
$('#rmReportText').val('').removeData('rmGenerated');
}
function updateProtectMultiUi() {
var $rows = $('#rmProtectMultiWrap .rmProtectPageRow');
var hasExtra = $rows.length > 1;
if (!$rows.length) {
$('#rmProtectPagesContainer').append(buildProtectPageRowHtml('rmProtectPage' + pageCounter++, ctx.pageName, true));
$rows = $('#rmProtectMultiWrap .rmProtectPageRow');
hasExtra = false;
}
$('#rmProtectHeaderWrap').toggle(hasExtra);
if (!hasExtra) $('#rmProtectHeader').val('');
$rows.each(function () {
var $row = $(this);
$row.find('.rmProtectAddPage,.rmRemoveInput').remove();
$row.append(hasExtra
? '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>'
: buildProtectAddButtonHtml()
);
});
syncModalLayout();
}
createModal({
title: isZka ? 'Запрос к администраторам' : 'Запрос на защиту страницы',
width: 'compact',
subtitleHtml: isZka
? '<a href="' + getPageUrl('Википедия:Запросы к администраторам') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Запросы к администраторам</a>' +
' · <a href="' + getPageUrl('Википедия:Запросы к администраторам/Быстрые') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Быстрые</a>'
: '<span id="rmProtectLinkWrap"></span>'
});
$('#removerModalContent').html(buildReportFormHtml(ctx, isZka));
if (!isZka) {
$('#removerModalContent')
.on('click', '#rmProtectModeInstall', function () { applyProtectMode('install'); })
.on('click', '#rmProtectModeRemove', function () { applyProtectMode('remove'); })
.on('click', '.rmProtectOptBtn', function () {
$(this).toggleClass('is-active');
if (protectMode === 'install') {
var $levels = $('#rmProtectLevels .rmProtectOptBtn.is-active');
if ($(this).closest('#rmProtectReasons').length && $(this).hasClass('is-active') && $levels.length === 0) {
$('#rmProtectLevels .rmProtectOptBtn').first().addClass('is-active');
}
if ($(this).closest('#rmProtectLevels').length && !$(this).hasClass('is-active') && $levels.length === 0) {
$('#rmProtectReasons .rmProtectOptBtn').removeClass('is-active');
}
}
applyGeneratedText($('#rmReportText'), buildProtectText(protectMode));
});
$(document)
.off('click.rmProtectAdd').on('click.rmProtectAdd', '.rmProtectAddPage', function () {
var id = 'rmProtectPage' + pageCounter++;
$('#rmProtectPagesContainer').append(buildProtectPageRowHtml(id, '', false));
updateProtectMultiUi();
})
.off('click.rmProtectRemove').on('click.rmProtectRemove', '.rmProtectPageRow .rmRemoveInput', function () {
$(this).closest('.rmProtectPageRow').remove(); updateProtectMultiUi();
});
applyProtectMode('install');
}
setupResizableModal('rmReportText');
renderModalFooter('submit', {
showCheckbox: false,
showSubscribe: true,
submitText: 'Отправить',
onSubmit: function () { doReport(ctx, false, protectMode); return true; }
});
if (isZka) {
$('<button id="rmReportFast" style="' + stCancel + '">Быстрый запрос</button>').insertBefore('#removerSubmit');
$('#rmReportFast').click(function () {
if ($('#removerSubmit').data('rmSubmitInProgress')) return;
$('#removerSubmit').data('rmSubmitInProgress', true).prop('disabled', true);
$('#rmReportFast').prop('disabled', true);
doReport(ctx, true, protectMode);
});
}
}
};
// ═══════════════════════════════════════════════════════════════════════════
// ВСПОМОГАТЕЛЬНЫЕ: КБУ, закрытие, категории
// ═══════════════════════════════════════════════════════════════════════════
function getFastRemoveCriteriaAnchorFromConfig(templateName) {
var anchors = cfg.fastRemoveCriteriaAnchors || {};
var template = String(templateName || '').trim();
var lower = template.toLowerCase();
var key;
if (!template) return '';
if (Object.prototype.hasOwnProperty.call(anchors, template)) return anchors[template];
for (key in anchors) {
if (Object.prototype.hasOwnProperty.call(anchors, key) && String(key).toLowerCase() === lower) {
return anchors[key];
}
}
return '';
}
function getFastRemoveCriteriaAnchor(reason) {
var configured = reason ? getFastRemoveCriteriaAnchorFromConfig(reason[0]) : '';
var template, label, m;
if (configured) return configured;
template = String(reason && reason[0] || '');
label = String(reason && reason[1] || '');
if (/^(?:подст\s*:\s*)?(?:deleteslow|ds)$/i.test(template) || /^\s*ds\b/i.test(label)) return 'С1';
m = label.match(/^\s*([А-ЯЁA-Z]{1,3}\d+(?:\.\d+)?)/);
return m ? m[1] : '';
}
function buildFastRemoveCriteriaLinkHtml(reason) {
var anchor = getFastRemoveCriteriaAnchor(reason);
var label = KBU_CRITERIA_PAGE + (anchor ? '#' + anchor : '');
var url = getPageUrlWithFragment(KBU_CRITERIA_PAGE, anchor);
return '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(label) + '</a>';
}
function getFastRemoveReasons() {
var reasons = cfg.fastRemoveReasons;
var prefix = (mwCfg.wgIsRedirect ? 'ОП' : 'О') + ({ 0: 'С', 2: 'У', 3: 'У', 6: 'Ф', 14: 'К' }[mwCfg.wgNamespaceNumber] || '');
var all = [];
if (isCategory && reasons.categories) all = all.concat(reasons.categories);
['general','articles','redirects','files','users','special'].forEach(function (k) { if (reasons[k]) all = all.concat(reasons[k]); });
if (!isCategory && reasons.categories) all = all.concat(reasons.categories);
return all.filter(function (r) { return prefix.indexOf(r[2] !== undefined ? r[2] : r[1].charAt(0)) >= 0; });
}
function showCloseActionsModal(opts) {
createModal({ title: 'Снятие шаблонов номинаций', inline: true });
$('#removerModalContent').html('<p style="margin:0;">Определение доступных действий...</p>');
var pageName = normTitle(mwCfg.wgPageName);
getText(pageName, function (pageText, readErr) {
if (readErr) { showInfoAndClose('Не удалось прочитать содержимое страницы.', readErr.info || readErr.code || '', true); return; }
if (pageText === null) { showInfoAndClose('Не удалось прочитать содержимое страницы.', '', true); return; }
var actions = opts.getActions(pageText);
if (!actions.length) { showInfoAndClose(opts.emptyText, opts.emptyDetails || ''); return; }
var actionMap = actions.reduce(function (m, a) { m[a.id] = a; return m; }, {});
$('#removerModalContent').html(buildActionsHtml(actions, opts.inputName, opts.listId));
if (opts.afterRender) opts.afterRender(actions, actionMap, pageName, pageText);
renderModalFooter('submit', {
showCheckbox: opts.showCheckbox,
submitText: 'Выполнить',
onSubmit: function () {
var sel = getSelectedAction(opts.inputName, actionMap);
if (!sel) return false;
startProcessing();
return opts.onSubmit(sel, pageName, pageText, actionMap) !== false;
}
});
if (opts.afterFooterRender) opts.afterFooterRender(actions, actionMap, pageName, pageText);
});
}
function runPageEditWithStatus(opts) {
var o = opts || {};
var statusId = logStatus(o.pendingText, null, { pending: true, trackError: false });
editPageContent(o.pageName, o.editOptions, o.buildFn, function (err, meta) {
if (err) { logStatus(o.errorText, err, { statusId: statusId }); unlockModalSubmit(); return; }
logStatus(o.successText, null, { statusId: statusId, trackError: false });
if (o.onSuccess) o.onSuccess(meta || null);
});
}
function removeKuFromCategory(pageName) {
runPageEditWithStatus({
pageName: pageName,
pendingText: 'Снимается шаблон КУ...',
errorText: 'Снятие шаблона КУ.',
successText: 'Шаблон КУ снят.',
editOptions: { summary: makeSummary('снятие шаблона КУ'), watchlist: 'nochange', readError: 'Страница не существует.', readErrorCode: 'read_failed' },
buildFn: function (text) {
var r = stripTemplatesByPattern(text, '(?:к\\s*удалению|ку)');
if (!r.removed) return { error: { code: 'no_changes', info: 'Шаблон КУ не найден.' } };
return { text: r.text.replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n') };
},
onSuccess: function () {
logStatus('Шаблон на СО не устанавливался.', null, { trackError: false });
renderModalFooter('reload');
}
});
}
// ── Категории: добавление шаблона ────────────────────────────────────────
function addTemplateToCategory(pageName, type, mainName, additionalNames, callback) {
var cb = callback || function () {};
var cfgByType = {
discuss: { action: 'обсуждение', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss },
deletion: { action: 'удаление', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss },
rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true, aliases: cfg.categoryTemplates.rename },
merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true, aliases: cfg.categoryTemplates.merge }
};
var typeCfg = cfgByType[type];
if (!typeCfg) { cb({ code: 'error', info: 'Неизвестный тип номинации.' }); return; }
var dateStr = getDate()[0];
var parts = [dateStr];
if (typeCfg.needsMain) {
if (!mainName) { cb({ code: 'error', info: 'Не указано основное название.' }); return; }
parts.push(mainName);
if (additionalNames && additionalNames.length) Array.prototype.push.apply(parts, additionalNames);
}
var tplText = T_OPEN + typeCfg.template + '|' + parts.join('|') + T_CLOSE;
editPageContent(pageName, { summary: makeSummary('добавление шаблона номинации на ' + typeCfg.action), readError: 'Не удалось получить содержимое.' },
function (text) {
if (hasTemplateWithDateByPattern(text, typeCfg.aliases, dateStr)) {
return { skip: true, meta: { status: 'already_present' } };
}
return { text: wrapInNoinclude(text, tplText) };
},
function (err, meta) {
if (!err && meta && meta.status === 'already_present') {
logStatus('На странице ' + buildQuotedStatusPageLink(pageName) + ' уже есть шаблон номинации с датой ' + dateStr + '.', null, { trackError: false });
} else {
logPageEdit(pageName, err);
}
if (err) { cb(err); return; }
if (type === 'merge') {
addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, function () { cb(null); });
return;
}
cb(null);
}
);
}
function collectCategoryPageInputValues(selector) {
var pages = [];
$(selector).each(function () {
var title = normalizeCategoryPageName($(this).val() || '');
if (title && pages.indexOf(title) === -1) pages.push(title);
});
return pages;
}
function addTemplatesToCategories(pages, type, mainName, additionalNames, callback) {
var cb = callback || function () {};
var processedPages = [];
eachSequential(pages || [], function (pageName, next) {
addTemplateToCategory(pageName, type, mainName, additionalNames, function (err) {
if (!err) processedPages.push(pageName);
next(err || null);
});
}, function (err) { cb(err, processedPages); });
}
function addMergeTemplatesToTargets(sourcePage, mainName, additionalNames, dateStr, callback) {
var cb = callback || function () {};
var currentCatName = normTitle(stripCatPrefix(sourcePage));
var targets = [mainName].concat(additionalNames || []);
if (!targets.length) { cb(); return; }
eachSequential(targets, function (target, next) {
var targetPage = 'Категория:' + target;
addMergeTemplateToTargetCategory(targetPage, currentCatName, dateStr, function (success, status) {
var url = getPageUrl(targetPage);
var linkHtml = '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(targetPage) + '</a>';
if (success) {
var extra = (status === 'already_exists' || status === 'updated') ? ' (' + formatMergeStatus(status) + ')' : '';
logStatus('Шаблон добавлен в ' + linkHtml + extra + '.', null, { trackError: false });
} else {
logStatus('Ошибка при добавлении шаблона в ' + linkHtml + '.', { code: 'merge_target_failed', info: status }, { trackError: false });
}
next();
});
}, cb);
}
function addMergeTemplateToTargetCategory(targetPageName, sourceCatName, dateStr, callback) {
editPageContent(targetPageName, { summary: makeSummary('добавление шаблона объединения'), readError: 'Не удалось получить содержимое' },
function (text) {
var existing = text.match(getCategoryMergeRe());
if (existing) {
var cats = existing[1].split('|').slice(1).map(function (p) { return p.trim(); }).filter(function (p) { return p.indexOf('=') === -1 && p.length > 0; });
var norm = sourceCatName.replace(/\s+/g, ' ').trim();
if (cats.some(function (c) { return c.replace(/\s+/g, ' ').trim() === norm; })) { return { skip: true, meta: { status: 'already_exists' } }; }
return {
text: text.replace(existing[0], function () { return existing[0].replace(/\}\}\s*$/, '|' + sourceCatName + '}}'); }),
summary: makeSummary('дополнение шаблона объединения [[:Категория:' + sourceCatName + ']]'),
meta: { status: 'updated' }
};
}
return { text: wrapInNoinclude(text, T_OPEN + 'Категория к объединению|' + dateStr + '|' + sourceCatName + T_CLOSE), meta: { status: 'created' } };
},
function (err, meta) { callback(!err, err ? err.info : ((meta && meta.status) || 'updated')); }
);
}
// ── Категории: обсуждение ────────────────────────────────────────────────
function buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, options) {
var opts = options || {};
var pages = Array.isArray(pageName) ? pageName : null;
var titleText;
if (pages && pages.length) {
titleText = String(opts.headerText || '').trim() || (formatPagesWithAnd(pages, ':') + (type === 'deletion' ? ' → удалить' : ''));
return '=== ' + titleText + ' ===\n';
}
var title = '=== [[:' + pageName + ']]';
if (type === 'rename' || type === 'merge') {
title += (type === 'rename' ? ' → ' : ' объединить с ') + formatCatLink(mainName);
if (additionalNames && additionalNames.length) {
var conj = type === 'rename' ? ' или ' : ' и ';
var head = additionalNames.slice(0, -1).map(formatCatLink).join(', ');
title += (additionalNames.length > 1 ? ', ' + head : '') + conj + formatCatLink(additionalNames[additionalNames.length - 1]);
}
} else if (type === 'deletion') {
title += ' → удалить';
}
return title + ' ===\n';
}
function createCategoryDiscussion(pageName, reason, type, callback, mainName, additionalNames, options) {
var cb = callback || function () {};
var opts = options || {};
var now = new Date();
var m = now.getUTCMonth(), year = now.getUTCFullYear(), day = now.getUTCDate();
var dateHeader = '== ' + day + ' ' + MONTHS_GEN[m] + ' ' + year + ' ==\n';
var discPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[m] + '_' + year;
var discTitle = buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, opts);
var discText = discTitle + (opts.reasonIsPrepared ? String(reason || '') : appendNominationSignature(reason)) + '\n';
var sectionTitle = discTitle.replace(/^===\s*/, '').replace(/\s*===\s*$/, '').trim();
var summaryTarget = Array.isArray(pageName) ? formatPagesWithAnd(pageName, ':') : '[[:' + pageName + ']]';
var summaryText = 'добавление обсуждения для ' + (Array.isArray(pageName) ? 'категорий ' : 'категории ') + summaryTarget;
publishNomination({
pageTitle: discPage,
readErrorMessage: 'Не удалось получить содержимое страницы обсуждения.',
summary: makeSummary(summaryText),
createText: function () {
return T_OPEN + 'ОБК-Навигация' + T_CLOSE + '\n\n' + dateHeader + discText;
},
buildText: function (text) {
var todayIdx = text.indexOf(dateHeader.trim());
if (todayIdx !== -1) {
var endIdx = text.indexOf('\n== ', todayIdx + dateHeader.length);
if (endIdx === -1) endIdx = text.length;
var before = text.slice(0, endIdx);
return { text: (before.endsWith('\n\n') ? before.slice(0, -1) : before) + '\n' + discText + text.slice(endIdx) };
}
var searchStr = T_OPEN + 'ОБК-Навигация' + T_CLOSE;
var obkIdx = text.indexOf(searchStr);
if (obkIdx === -1) return { error: { code: 'insert_failed', info: 'Не удалось найти место для вставки.' } };
return { text: text.slice(0, obkIdx + searchStr.length) + '\n\n' + dateHeader + discText + text.slice(obkIdx + searchStr.length).replace(/^\n+/, '\n') };
}
}, function (err) {
if (err) { cb(err); return; }
cb(null, { pageTitle: discPage, sectionTitle: sectionTitle });
});
}
// ── Категории: завершение ОБКАТ ───────────────────────────────────────────
function markCategoryDiscussionAsDone(pageName) {
runPageEditWithStatus({
pageName: pageName,
pendingText: 'Снимается шаблон обсуждения...',
errorText: 'Снятие шаблона обсуждения.',
successText: 'Шаблон обсуждения снят.',
editOptions: { summary: makeSummary('обсуждение категории завершено'), readError: 'Не удалось получить содержимое.' },
buildFn: function (text) {
var allTpls = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|');
var patterns = [
new RegExp('<noinclude>\\s*\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}\\s*</noinclude>', 'i'),
new RegExp('\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}', 'i')
];
var match = null;
for (var i = 0; i < patterns.length; i++) { match = text.match(patterns[i]); if (match) break; }
if (!match) return { error: { code: 'no_changes', info: 'Шаблон обсуждаемой категории не найден.' } };
return {
text: text.replace(match[0], '').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n'),
meta: { tplDate: convertToStandardDate(match[2].split('|')[0].trim()) }
};
},
onSuccess: function (meta) {
var talkStatusId = logStatus('Обновляется шаблон на СО категории...', null, { pending: true, trackError: false });
updateCategoryTalkPage(pageName, meta.tplDate, function (talkErr, info) {
if (talkErr) { logStatus('Установка шаблона на СО.', talkErr, { statusId: talkStatusId }); unlockModalSubmit(); return; }
logStatus(
(info && (info.status === 'already_present' || info.status === 'no_changes')) ? 'Шаблон на СО уже установлен.' : 'Шаблон установлен на СО.',
null, { statusId: talkStatusId, trackError: false }
);
renderModalFooter('reload');
});
}
});
}
function updateCategoryTalkPage(categoryName, templateDate, callback) {
var cb = callback || function () {};
var talkPage = getTalkPage(categoryName);
var newTpl = T_OPEN + 'Обсуждавшаяся категория|' + templateDate + T_CLOSE;
getTextWithTimestamp(talkPage, function (text, baseTimestamp, readErr) {
if (readErr) { cb(makeReadError(readErr, 'talk_read_failed', 'Не удалось получить содержимое СО категории.')); return; }
if (text === null) {
apiReq({ title: talkPage, text: newTpl + '\n\n', summary: makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate), createonly: true },
'edit', function (resp) {
if (resp && resp.error) {
if (resp.error.code === 'articleexists') setTimeout(function () { updateCategoryTalkPage(categoryName, templateDate, cb); }, 1000);
else cb(resp.error);
} else cb(null, { status: 'created' });
});
return;
}
var discussedRe = new RegExp('\\{\\{\\s*(' + cfg.categoryTemplates.discussed + ')([^\\}]*?)\\s*\\}\\}', 'i');
var tplMatch = text.match(discussedRe);
var newText = text;
if (tplMatch) {
var existingDates = tplMatch[2].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
if (existingDates.indexOf(templateDate) !== -1) { cb(null, { status: 'already_present' }); return; }
newText = text.replace(tplMatch[0], function () { return tplMatch[0].replace(/\s*\}\}$/, '|' + templateDate + '}}'); });
} else {
newText = insertTplOnTalkPage(text, newTpl);
}
if (newText === text) { cb(null, { status: 'no_changes' }); return; }
var ep = {
title: talkPage,
text: newText,
summary: tplMatch
? makeSummary('обновление шаблона [[ш:Обсуждавшаяся категория]], добавлена дата ' + templateDate)
: makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate)
};
if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) { cb(resp && resp.error ? resp.error : null, resp && !resp.error ? { status: tplMatch ? 'updated' : 'created' } : null); });
});
}
// ── Быстрое объединение (Ctrl+клик КОБ) ─────────────────────────────────
function buildQuickMergeHtml(tplDate, targets, currentCatName) {
return joinHtml([
'<p>Найден шаблон с датой <strong>', escapeHtml(tplDate), '</strong>. Категории для объединения:</p>',
'<pre style="background:', tk.bgBase, ';color:', tk.cBase, ';padding:10px;border:1px solid ', tk.bSubS, ';border-radius:4px;margin-bottom:10px;">',
targets.map(function (c) { return '• ' + escapeHtml(c); }).join('\n'),
'</pre>',
'<p><strong>Текущая категория:</strong> ', escapeHtml(currentCatName), '</p>',
'<p style="color:', tk.cSub, ';">Шаблон будет добавлен во все указанные выше категории.</p>'
]);
}
function showQuickMergeModal() {
getText(mwCfg.wgPageName, function (text, readErr) {
if (readErr) { alert('Не удалось получить содержимое: ' + (readErr.info || readErr.code || 'ошибка API') + '.'); return; }
if (!text) { alert('Не удалось получить содержимое.'); return; }
var mergeRe = getCategoryMergeRe();
var match = text.match(mergeRe);
if (!match) { alert('В текущей категории не найден шаблон "Категория к объединению".'); return; }
var params = match[1].split('|').map(function (p) { return p.trim(); });
var tplDate = params[0];
var targets = params.slice(1);
if (!targets.length) { alert('В шаблоне не найдены целевые категории.'); return; }
createModal({ title: 'Быстрое добавление шаблона объединения' });
var currentCatName = normTitle(stripCatPrefix(mwCfg.wgPageName));
$('#removerModalContent').html(buildQuickMergeHtml(tplDate, targets, currentCatName));
renderModalFooter('submit', {
showCheckbox: false,
submitText: 'Добавить шаблоны',
onSubmit: function () {
startProcessing();
$('#removerSubmit').prop('disabled', true);
eachSequential(targets, function (target, next) {
addMergeTemplateToTargetCategory('Категория:' + target, currentCatName, tplDate, function (success, status) {
if (success) logStatus('Категория [[:Категория:' + target + ']] (' + formatMergeStatus(status) + ').', null, { trackError: false });
else logStatus('Ошибка [[:Категория:' + target + ']].', { code: 'merge_target_failed', info: status }, { trackError: false });
next();
});
}, function () {
if (isError) markSubmitError(); else renderModalFooter('close');
});
return true;
}
});
});
}
// ── ЗКА/Защита: публикация ───────────────────────────────────────────────
function getReporterContext(mode) {
var rawPage = mwCfg.wgPageName;
var pageName = normTitle(rawPage)
.replace(/(Special|Служебная):(Contributions|Вклад)\//i, 'User:')
.replace(/(User talk:|Обсуждение участни(ка|цы):)/i, 'User:');
var isUserRelated = /user|contrib|участни|вклад/i.test(rawPage);
var displayName = normTitle(rawPage)
.replace(/(Special|Служебная):(Вклад|Contributions)\//i, '')
.replace(/(User talk:|Обсуждение участни(ка|цы):)/i, '')
.replace(/(user|участни(к|ца)):/i, '');
var pageLink = '[[' + pageName + (isUserRelated ? '|' + displayName + ']]' : ']]');
var reportPage = mode === 'request' ? 'Википедия:Запросы к администраторам' : 'Википедия:Установка защиты';
return { pageName: pageName, pageLink: pageLink, displayName: displayName, reportPage: reportPage };
}
function doReport(ctx, fast, protectMode) {
var header = $('#rmReportHeader').val() || ctx.pageLink;
var text = $('#rmReportText').val() || '';
var isZka = ctx.reportPage === 'Википедия:Запросы к администраторам';
var isRemoveProtect = !isZka && protectMode === 'remove';
startProcessing();
var targetPage, editParams, sectionForLink;
if (fast) {
targetPage = 'Википедия:Запросы к администраторам/Быстрые';
sectionForLink = null;
editParams = {
appendtext: '\n\n' + T_OPEN + 'sub' + 'st:t:preload/ЗКАБ/subst|\n | участник = ' + ctx.displayName +
'| страница = | пояснение = ' + text + T_CLOSE + '\n',
summary: makeSummary('новый запрос [[Special:Contributions/' + ctx.displayName + ']]')
};
} else if (isZka) {
targetPage = ctx.reportPage;
sectionForLink = extractDisplayedText(header);
var isIpFull = /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/.test(ctx.displayName);
editParams = {
section: 'new',
sectiontitle: header,
text: '* ' + T_OPEN + 'userlinks|' + ctx.displayName + (isIpFull ? '|ip=1' : '') + T_CLOSE + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
} else {
targetPage = isRemoveProtect ? 'Википедия:Снятие защиты' : 'Википедия:Установка защиты';
var pages = collectInputValues('.rmProtectPageInput');
if (!pages.length) pages = [ctx.pageName];
var sectionTitle, pageLines;
if (pages.length === 1) {
sectionTitle = '[[' + pages[0] + ']]';
pageLines = '* ' + T_OPEN + 'pagelinks-protect|' + pages[0] + T_CLOSE;
} else {
sectionTitle = ($('#rmProtectHeader').val() || '').trim() || pages.map(function (p) { return '[[' + p + ']]'; }).join(', ');
pageLines = pages.map(function (p) { return '* ' + T_OPEN + 'pagelinks-protect|' + p + T_CLOSE; }).join('\n');
}
sectionForLink = extractDisplayedText(sectionTitle);
editParams = {
section: 'new',
sectiontitle: sectionTitle,
text: pageLines + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
}
var statusId = logStatus('Публикуется запрос на «' + targetPage + '»...', null, { pending: true, trackError: false });
apiReq($.extend({ title: targetPage }, editParams), 'edit', function (resp) {
if (resp && resp.error) {
logStatus('Публикация запроса на «' + targetPage + '».', resp.error, { statusId: statusId });
markSubmitError();
if (isZka) $('#rmReportFast').prop('disabled', false);
return;
}
logStatus('Запрос опубликован.', null, { statusId: statusId, trackError: false });
appendNominationLink(targetPage, sectionForLink);
if (sectionForLink) subscribeToTopic(targetPage, sectionForLink);
renderModalFooter('reload');
});
}
// ═══════════════════════════════════════════════════════════════════════════
// ДИСПЕТЧЕР
// ═══════════════════════════════════════════════════════════════════════════
function handleMenuClick(item, event) {
isError = false;
var op = OPERATIONS_MAP[item.id];
// Специальный случай: КОБ категории с Ctrl — быстрое добавление шаблона
if (item.id === 'cat-merge' && event && event.ctrlKey) {
showQuickMergeModal();
return;
}
if (!op) {
console.error('RemoverCore: неизвестная операция', item.id);
return;
}
var handlerFn = handlers[op.handler];
if (typeof handlerFn !== 'function') {
console.error('RemoverCore: обработчик не найден', op.handler);
return;
}
handlerFn(op, event);
}
// ─── Экспорт ─────────────────────────────────────────────────────────────
window.RemoverCore = { handleMenuClick: handleMenuClick };
}());
hjgszr3j4g4okvur4rug5x5limtsptq
Narcistic Personality Disorder (song)
0
175019
739896
739382
2026-04-30T23:50:41Z
Cryptocurrency777
73698
739896
wikitext
text/x-wiki
'''Narcisstic Personality Disorder''' is a 2023 song by Odetari as the last single on XIII sorrows
It has xemnas from Kingdom hearts on the cover
that's all guess
2gm7xn2x9mt290gnseaqusl7sly5nrf
User:Sam Sailor/test.js/comrade-lib.js
2
175022
739954
739857
2026-05-01T10:24:10Z
Sam Sailor
26820
Test
739954
javascript
text/javascript
//<nowiki>
/* global mw, $, ComradeLib */
/**
* Comrade-Lib
* Helper functions
*/
mw.util.addCSS(`
.ambox-Orphan { display: block !important; }
.comrade-container {
box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px;
padding: 10px 15px; margin: 10px 0; display: flex;
flex-direction: column; align-items: flex-start; gap: 8px;
border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5;
}
.comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.comrade-content {
display: flex; flex-direction: column;
gap: 5px; width: 100%;
}
.comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; }
.comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; }
.comrade-info, .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; }
.comrade-container button:not(.comrade-dismiss) {
background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px;
padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px;
}
.comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; }
.comrade-dismiss {
background: transparent; border: none; color: #d33; font-weight: bold;
cursor: pointer; font-size: 0.9em;
}
.comrade-dismiss:hover { text-decoration: underline; }
.comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; }
`);
window.ComradeLib = window.ComradeLib || {};
window.ComradeLib.api = new mw.Api();
window.ComradeLib.appendDismiss = function($el) {
$('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
};
// Category audit
window.ComradeLib.arrayChunk = function(arr, size) {
const result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
};
window.ComradeLib.fetchOresData = async function(revIds) {
const scores = {};
const BATCH = 1;
for (let i = 0; i < revIds.length; i += BATCH) {
const rid = revIds[i];
try {
const response = await fetch('https://api.wikimedia.org/service/lw/inference/v1/models/enwiki-articlequality:predict', {
method: 'POST',
body: JSON.stringify({
rev_id: parseInt(rid, 10)
})
});
if (!response.ok) {
console.warn(`Comrade: Lift Wing ${response.status} for rev ${rid}`);
continue;
}
const data = await response.json();
const prediction = data?.enwiki?.scores?.[String(rid)]?.articlequality?.score?.prediction ?? data?.output?.prediction ?? null;
if (prediction) scores[String(rid)] = prediction;
} catch (e) {
console.warn(`Comrade: Lift Wing fetch error for rev ${rid}`, e);
}
}
return scores;
};
window.ComradeLib.performCategoryAudit = async function() {
const api = new mw.Api();
const $links = $('#mw-pages li a');
const titles = $links.map((i, el) => $(el).attr('title')).get().filter(t => t && !t.startsWith('Talk:'));
if (!titles.length) return;
const $btn = $('#comrade-audit-btn');
$btn.prop('disabled', true).text('Auditing...');
const chunks = ComradeLib.arrayChunk(titles, 25);
const results = {};
try {
for (const chunk of chunks) {
const talkTitles = chunk.map(t => 'Talk:' + t);
const res = await api.get({
action: 'query',
titles: chunk.concat(talkTitles).join('|'),
prop: 'revisions',
rvprop: 'content|ids',
rvslots: 'main',
formatversion: 2
});
const pages = res.query.pages;
const getContent = pg => {
if (!pg || pg.missing || !pg.revisions) return '';
const rev = pg.revisions[0];
return rev.slots?.main?.content ?? rev.content ?? '';
};
const revIds = [];
const titleToRevId = {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
if (mainPg?.revisions?.[0]?.revid) {
const rid = String(mainPg.revisions[0].revid);
revIds.push(rid);
titleToRevId[title] = rid;
}
});
const oresScores = revIds.length ? await ComradeLib.fetchOresData(revIds) : {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
const talkPg = pages.find(p => p.title.toLowerCase() === ('talk:' + title).toLowerCase() && !p.missing);
const talkContent = getContent(talkPg);
const mainContent = getContent(mainPg);
const classMatch = talkContent.match(/\|\s*class\s*=\s*([^|{}\n\r]+)/i);
const rawClass = classMatch ? classMatch[1].trim() : '';
const talkClass = rawClass ? rawClass.charAt(0).toUpperCase() + rawClass.slice(1).toLowerCase() : 'Unrated';
const hasStubTags = /\{\{[^}]*stub[^}]*\}\}/i.test(mainContent);
const rid = titleToRevId[title];
const predicted = rid ? (oresScores[rid] ?? null) : null;
results[title] = {
talkClass,
predicted,
hasStubTags
};
});
}
Object.entries(results).forEach(([title, {
talkClass,
predicted,
hasStubTags
}]) => {
ComradeLib.renderCategoryAuditUI(title, talkClass, predicted, hasStubTags);
});
$btn.text('Audit complete').fadeOut(2000);
} catch (error) {
console.error('Comrade: Audit error', error);
$btn.text('Audit failed').prop('disabled', false);
}
};
window.ComradeLib.renderCategoryAuditUI = function(title, talkClass, predicted, hasStub) {
const $link = $(`#mw-pages li a[title="${title}"]`);
if (!$link.length) return;
let label = '';
let style = 'font-size: 0.85em; margin-left: 8px; font-weight: normal;';
if (talkClass !== 'Stub' && talkClass !== 'Unrated' && hasStub) {
label = `${talkClass} (stub tag present) : ORES: ${predicted || '?'}`;
style += ' color: #f0ad4e;';
} else if (talkClass === 'Stub' && predicted && predicted !== 'Stub') {
label = `Stub : ${predicted}`;
style += ' color: #27ae60;';
} else if (predicted && predicted !== talkClass) {
label = `${talkClass} : ${predicted}`;
style += ' color: #36c;';
}
if (label) {
$link.parent().find('.comrade-audit-label').remove();
const $span = $('<span>').addClass('comrade-audit-label').attr('style', style).text(` [${label}]`);
$link.after($span);
}
};
// Category audit END
// Deorphanizer
// performDeorphan
window.ComradeLib.performDeorphan = async function() {
try {
await ComradeLib.api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]';
text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, '');
text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|');
text = ComradeLib.replaceMI(text);
text = text.replace(/\n{3,}/g, '\n\n').trim();
return {
text,
summary,
minor: true
};
});
mw.notify('Orphan tag removed!', {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to remove orphan tag.', {
type: 'error'
});
}
};
// replaceMI (Multiple Issues)
window.ComradeLib.replaceMI = function(text) {
const miRegex = /\{\{Multiple[ _]issues\s*\|/gi;
let result = '';
let lastIndex = 0;
let match;
while ((match = miRegex.exec(text)) !== null) {
const start = match.index;
result += text.slice(lastIndex, start);
let depth = 0;
let i = start;
while (i < text.length) {
if (text[i] === '{' && text[i + 1] === '{') {
depth++;
i += 2;
} else if (text[i] === '}' && text[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
const end = i;
const full = text.slice(start, end);
const pipeIdx = full.indexOf('|');
const inner = full.slice(pipeIdx + 1, full.length - 2).trim();
const topLevel = ComradeLib.extractTopLevelTemplates(inner);
if (topLevel.length === 0) {
result += '';
} else if (topLevel.length === 1) {
result += topLevel[0].trim();
} else {
result += full;
}
lastIndex = end;
}
result += text.slice(lastIndex);
return result;
};
// extractTopLevelTemplates
window.ComradeLib.extractTopLevelTemplates = function(inner) {
const templates = [];
let i = 0;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
let depth = 0;
const start = i;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
depth++;
i += 2;
} else if (inner[i] === '}' && inner[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
templates.push(inner.slice(start, i));
} else {
i++;
}
}
return templates;
};
// Quality suite
// getOresPrediction
window.ComradeLib.getOresPrediction = async function() {
const pageName = mw.config.get('wgPageName');
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'ids',
formatversion: 2
});
const page = pageData.query.pages[0];
const revId = page?.revisions?.[0]?.revid;
if (!revId) return null;
const scores = await ComradeLib.fetchOresData([String(revId)]);
const pred = scores[String(revId)];
if (!pred) return null;
return pred.charAt(0).toUpperCase() + pred.slice(1);
} catch (e) {
console.warn("Comrade: getOresPrediction failed to fetch via LiftWing", e);
return null;
}
};
// checkQualityConsistency
window.ComradeLib.checkQualityConsistency = async function() {
const oresClass = ComradeLib.getOresPrediction();
if (!oresClass || oresClass === 'Stub') return;
const talkTitle = 'Talk:' + mw.config.get('wgPageName');
try {
const [talkRes, pageRes] = await Promise.all([
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkTitle,
rvprop: 'content',
formatversion: 2
}),
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
})
]);
const talkPage = talkRes.query.pages[0];
const mainWikitext = pageRes.query.pages[0].revisions[0].content;
const hasStubTag = /\{\{[^}|]*stub[^}]*\}\}/i.test(mainWikitext);
let talkClass = null;
if (!talkPage.missing && talkPage.revisions) {
const classMatch = talkPage.revisions[0].content.match(/\|class\s*=\s*([^|}\s]+)/i);
if (classMatch) {
const rawClass = classMatch[1].toLowerCase();
if (rawClass === 'ga') talkClass = 'Ga';
else if (rawClass === 'fa') talkClass = 'Fa';
else talkClass = rawClass.charAt(0).toUpperCase() + rawClass.slice(1);
}
}
const isStubRated = talkClass === 'Stub';
const isStartRated = talkClass === 'Start';
const needsPromotionNudge = (isStubRated && (oresClass === 'Start' || oresClass === 'C')) || (isStartRated && oresClass === 'C');
const needsHighQualNudge = (oresClass !== talkClass) && !needsPromotionNudge;
const lingeringStub = (talkClass && !isStubRated && hasStubTag);
if (talkPage.missing || needsHighQualNudge || needsPromotionNudge || lingeringStub) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $content = $('<div>').append($('<strong>').text('Quality:'), " ");
let message = `ORES suggests <b>${oresClass}</b>. `;
if (talkPage.missing) {
message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `;
const $btnRater = $('<button>').text('Open Rater').on('click', () => $('#ca-rater, #t-rater').find('a')[0].click());
$content.append(message, $btnRater);
} else if (needsHighQualNudge) {
message = `ORES predicts <b>${oresClass}</b>. Please assess manually. `;
const $btnOkay = $('<button>').text('OK').on('click', () => {
window.location.hash = "#content";
mw.notify("Review the criteria for " + oresClass + "-class.");
$container.find('.comrade-dismiss').click();
});
$content.append(message, $btnOkay);
} else if (lingeringStub && !needsPromotionNudge) {
message = `Article is ${talkClass}-class, but stub tags remain. `;
const $btnRemove = $('<button>').text('Remove stub tags').on('click', () => ComradeLib.performStubRemoval());
$content.append(message, $btnRemove);
} else if (needsPromotionNudge) {
const $btnTarget = $('<button>').text(`Promote to ${oresClass}`).on('click', () => ComradeLib.performPromotion(oresClass));
$content.append(message, $btnTarget);
if (isStubRated && oresClass === 'C') {
const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => ComradeLib.performPromotion('Start'));
$content.append("\u00A0or\u00A0", $btnStart);
}
}
$container.append($('<div>').addClass('comrade-header').append($content));
$('#content').prepend($container);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.error("Comrade quality check failed", e);
}
};
// performStubRemoval
window.ComradeLib.performStubRemoval = async function() {
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
});
let wikitext = pageData.query.pages[0].revisions[0].content;
// 'g' flag is appropriate here for replacement
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const newWikitext = wikitext.replace(stubRegex, '').trim();
if (wikitext === newWikitext) {
mw.notify("No stub tags found to remove.");
return;
}
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: mw.config.get("wgPageName"),
text: newWikitext,
summary: `Removing lingering stub tags per ORES/Talk assessment`,
nocreate: true
});
location.reload();
} catch (err) {
console.error("Comrade failed to remove stub tags:", err);
mw.notify("Error removing stub tags.", {
type: 'error'
});
}
};
// performPromotion
window.ComradeLib.performPromotion = async function(newClass) {
const classLinks = {
'Start': '[[WP:STARTCLASS|Start-Class]]',
'C': '[[WP:CCLASS|C-Class]]',
'B': '[[WP:BCLASS|B-Class]]'
};
const linkedClass = classLinks[newClass] || newClass;
const pageName = mw.config.get('wgPageName');
const talkName = 'Talk:' + pageName;
try {
const talkData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkName,
rvprop: 'content',
formatversion: 2
});
let talkWikitext = talkData.query.pages[0].revisions[0].content;
const newTalkWikitext = talkWikitext.replace(/(\|class\s*=\s*)([^|}\s]+)/i, `$1${newClass}`);
if (talkWikitext !== newTalkWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: talkName,
text: newTalkWikitext,
summary: `Promoting article to ${linkedClass} per review and [[WP:ORES]]`
});
}
const artData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'content',
formatversion: 2
});
let artWikitext = artData.query.pages[0].revisions[0].content;
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const cleanArtWikitext = artWikitext.replace(stubRegex, '');
if (artWikitext !== cleanArtWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: pageName,
text: cleanArtWikitext,
summary: `Removing stub tags; article promoted to ${linkedClass}`,
nocreate: true
});
}
location.reload();
} catch (e) {
console.error("Promotion failed:", e);
mw.notify("Promotion failed during multi-step edit.", {
type: 'error'
});
}
};
// Quality suite END
// Redirects
// checkAndRenderRedirect
window.ComradeLib.checkAndRenderRedirect = async function(redirTitle, targetTitle, rCategory, labelText, customWikitext) {
const res = await ComradeLib.api.get({
action: 'query',
titles: redirTitle,
formatversion: 2
});
if (res.query.pages[0].missing) {
const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-');
const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Creating redirect to [[${targetTitle}]] (${labelText.toLowerCase()})`;
const $btn = $('<button>').text('Create redirect').on('click', () => ComradeLib.createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
ComradeLib.appendDismiss($container);
}
};
// createModernRedirect
window.ComradeLib.createModernRedirect = async function(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) {
const api = ComradeLib.api;
const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Creating redirect to [[${targetTitle}]]`;
try {
await api.postWithEditToken({
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
});
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
} catch (e) {
mw.notify("Failed to create redirect.", {
type: 'error'
});
}
};
// Domain name
window.ComradeLib.checkDomainRedirect = async function(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
try {
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
});
const res = await fetch(url).then(r => r.json());
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
} catch (e) {
console.warn("Comrade: Wikidata API call failed.");
}
}
if (!domainCandidate) {
domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href");
}
if (domainCandidate) {
try {
const url = new URL(domainCandidate);
let domain = url.hostname.replace(/^www\d*\./, "");
if (domain.includes(".") && url.pathname === "/") {
const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate);
try {
const response = await fetch(citationURL);
if (response.ok) {
await ComradeLib.checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {
console.warn("Comrade: Citation API check failed.");
}
}
} catch (e) {
console.warn("Comrade: Error parsing URL object.");
}
}
};
// Long name
window.ComradeLib.getLongNameFromWikitext = function(wikitext) {
const firstLine = wikitext.split('\n').find(l => l.includes("'''"));
if (!firstLine) return null;
const boldMatch = firstLine.match(/'''(.+?)'''/);
if (!boldMatch) return null;
return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim();
};
// Expanded normalization
window.ComradeLib.getNormalizedTitles = function(currentTitle) {
const maps = {
all: {
'æ': 'ae',
'Æ': 'Ae',
'œ': 'oe',
'Œ': 'Oe',
'ij': 'ij',
'IJ': 'Ij',
'ß': 'ss',
'ẞ': 'SS',
'ð': 'd',
'Ð': 'D',
'ø': 'o',
'Ø': 'O',
'ł': 'l',
'Ł': 'L',
'þ': 'th',
'Þ': 'Th',
'ŋ': 'ng',
'Ŋ': 'Ng'
}
};
let cleanAscii = currentTitle.split('').map((char, index) => {
let rep = maps.all[char];
if (rep) {
return (index > 0 && rep.length > 1) ? rep.toLowerCase() : rep;
}
return char;
}).join('');
cleanAscii = cleanAscii.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
return {
ascii: (cleanAscii.toLowerCase() !== currentTitle.toLowerCase()) ? cleanAscii : null
};
};
// Redirects END
// SD
window.ComradeLib.checkShortDescDates = async function(wikitext, entity) {
const sdMatch = wikitext.match(/\{\{[Ss]hort description\|([^|{}]+)(?:\|[^}]*)?\}\}/);
if (!sdMatch) return;
const currentSD = sdMatch[1].trim();
const dateRegex = /\((?:born |died |fl\. )?\d{3,4}(?:–\d{3,4}|s)?\)/i;
const hasDate = dateRegex.test(currentSD);
if (hasDate && currentSD.length > 15) return;
const getYear = (val) => {
if (!val || !val.time) return null;
const yearMatch = val.time.match(/[+-](\d{4,})/);
if (!yearMatch) return null;
let year = parseInt(yearMatch[1]);
if (val.time.startsWith('-')) return year + " BC";
if (val.precision === 8) return year + "s";
return year;
};
let dateStr = "";
const bYear = getYear(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value);
const dYear = getYear(entity.claims?.P570?.[0]?.mainsnak?.datavalue?.value);
if (bYear && dYear) dateStr = `(${bYear}–${dYear})`;
else if (bYear) dateStr = `(born ${bYear})`;
else if (dYear) dateStr = `(died ${dYear})`;
else {
const flYear = getYear(entity.claims?.P1317?.[0]?.mainsnak?.datavalue?.value);
if (flYear) dateStr = `(fl. ${flYear})`;
}
if (!dateStr) return;
let finalSD = currentSD;
const words = currentSD.split(' ');
if (words.length === 1 && !hasDate) {
const occupationId = entity.claims?.P106?.[0]?.mainsnak?.datavalue?.value?.id;
if (occupationId) {
const occRes = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${occupationId}&props=labels&languages=en&format=json&origin=*`).then(r => r.json());
const occLabel = occRes.entities[occupationId]?.labels?.en?.value;
if (occLabel) {
finalSD = `${occLabel.charAt(0).toLowerCase() + occLabel.slice(1)} ${dateStr}`;
}
} else {
finalSD = `${currentSD} ${dateStr}`;
}
} else if (!hasDate) {
finalSD = `${currentSD} ${dateStr}`;
}
if (finalSD !== currentSD) {
ComradeLib.renderSDNudge(currentSD, finalSD);
}
};
window.ComradeLib.renderSDNudge = function(oldSD, newSD) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Short Description:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text('Dates missing: '), $('<code>').text(newSD));
const $btn = $('<button>').text('Update').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
content = content.replace(/(\{\{[Ss]hort description\|)[^|}]+/, `$1${newSD}`);
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Updating short description: +dates/role per [[WP:SDFORMAT]]`,
nocreate: true
});
mw.notify('Short description updated!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update SD.', {
type: 'error'
});
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
//</nowiki>
kj86kq7qfgg33o5nx21jbrtzfnp0xw8
739955
739954
2026-05-01T10:33:20Z
Sam Sailor
26820
Test
739955
javascript
text/javascript
//<nowiki>
/* global mw, $, ComradeLib */
/**
* Comrade-Lib
* Helper functions
*/
mw.util.addCSS(`
.ambox-Orphan { display: block !important; }
.comrade-container {
box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px;
padding: 10px 15px; margin: 10px 0; display: flex;
flex-direction: column; align-items: flex-start; gap: 8px;
border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5;
}
.comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.comrade-content {
display: flex; flex-direction: column;
gap: 5px; width: 100%;
}
.comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; }
.comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; }
.comrade-info, .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; }
.comrade-container button:not(.comrade-dismiss) {
background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px;
padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px;
}
.comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; }
.comrade-dismiss {
background: transparent; border: none; color: #d33; font-weight: bold;
cursor: pointer; font-size: 0.9em;
}
.comrade-dismiss:hover { text-decoration: underline; }
.comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; }
`);
window.ComradeLib = window.ComradeLib || {};
window.ComradeLib.api = new mw.Api();
window.ComradeLib.appendDismiss = function($el) {
$('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
};
// Category audit
window.ComradeLib.arrayChunk = function(arr, size) {
const result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
};
window.ComradeLib.fetchOresData = async function(revIds) {
const scores = {};
const BATCH = 1;
for (let i = 0; i < revIds.length; i += BATCH) {
const rid = revIds[i];
try {
const response = await fetch('https://api.wikimedia.org/service/lw/inference/v1/models/enwiki-articlequality:predict', {
method: 'POST',
body: JSON.stringify({
rev_id: parseInt(rid, 10)
})
});
if (!response.ok) {
console.warn(`Comrade: Lift Wing ${response.status} for rev ${rid}`);
continue;
}
const data = await response.json();
const prediction = data?.enwiki?.scores?.[String(rid)]?.articlequality?.score?.prediction ?? data?.output?.prediction ?? null;
if (prediction) scores[String(rid)] = prediction;
} catch (e) {
console.warn(`Comrade: Lift Wing fetch error for rev ${rid}`, e);
}
}
return scores;
};
window.ComradeLib.performCategoryAudit = async function() {
const api = new mw.Api();
const $links = $('#mw-pages li a');
const titles = $links.map((i, el) => $(el).attr('title')).get().filter(t => t && !t.startsWith('Talk:'));
if (!titles.length) return;
const $btn = $('#comrade-audit-btn');
$btn.prop('disabled', true).text('Auditing...');
const chunks = ComradeLib.arrayChunk(titles, 25);
const results = {};
try {
for (const chunk of chunks) {
const talkTitles = chunk.map(t => 'Talk:' + t);
const res = await api.get({
action: 'query',
titles: chunk.concat(talkTitles).join('|'),
prop: 'revisions',
rvprop: 'content|ids',
rvslots: 'main',
formatversion: 2
});
const pages = res.query.pages;
const getContent = pg => {
if (!pg || pg.missing || !pg.revisions) return '';
const rev = pg.revisions[0];
return rev.slots?.main?.content ?? rev.content ?? '';
};
const revIds = [];
const titleToRevId = {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
if (mainPg?.revisions?.[0]?.revid) {
const rid = String(mainPg.revisions[0].revid);
revIds.push(rid);
titleToRevId[title] = rid;
}
});
const oresScores = revIds.length ? await ComradeLib.fetchOresData(revIds) : {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
const talkPg = pages.find(p => p.title.toLowerCase() === ('talk:' + title).toLowerCase() && !p.missing);
const talkContent = getContent(talkPg);
const mainContent = getContent(mainPg);
const classMatch = talkContent.match(/\|\s*class\s*=\s*([^|{}\n\r]+)/i);
const rawClass = classMatch ? classMatch[1].trim() : '';
const talkClass = rawClass ? rawClass.charAt(0).toUpperCase() + rawClass.slice(1).toLowerCase() : 'Unrated';
const hasStubTags = /\{\{[^}]*stub[^}]*\}\}/i.test(mainContent);
const rid = titleToRevId[title];
const predicted = rid ? (oresScores[rid] ?? null) : null;
results[title] = {
talkClass,
predicted,
hasStubTags
};
});
}
Object.entries(results).forEach(([title, {
talkClass,
predicted,
hasStubTags
}]) => {
ComradeLib.renderCategoryAuditUI(title, talkClass, predicted, hasStubTags);
});
$btn.text('Audit complete').fadeOut(2000);
} catch (error) {
console.error('Comrade: Audit error', error);
$btn.text('Audit failed').prop('disabled', false);
}
};
window.ComradeLib.renderCategoryAuditUI = function(title, talkClass, predicted, hasStub) {
const $link = $(`#mw-pages li a[title="${title}"]`);
if (!$link.length) return;
let label = '';
let style = 'font-size: 0.85em; margin-left: 8px; font-weight: normal;';
if (talkClass !== 'Stub' && talkClass !== 'Unrated' && hasStub) {
label = `${talkClass} (stub tag present) : ORES: ${predicted || '?'}`;
style += ' color: #f0ad4e;';
} else if (talkClass === 'Stub' && predicted && predicted !== 'Stub') {
label = `Stub : ${predicted}`;
style += ' color: #27ae60;';
} else if (predicted && predicted !== talkClass) {
label = `${talkClass} : ${predicted}`;
style += ' color: #36c;';
}
if (label) {
$link.parent().find('.comrade-audit-label').remove();
const $span = $('<span>').addClass('comrade-audit-label').attr('style', style).text(` [${label}]`);
$link.after($span);
}
};
// Category audit END
// Deorphanizer
// performDeorphan
window.ComradeLib.performDeorphan = async function() {
try {
await ComradeLib.api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]';
text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, '');
text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|');
text = ComradeLib.replaceMI(text);
text = text.replace(/\n{3,}/g, '\n\n').trim();
return {
text,
summary,
minor: true
};
});
mw.notify('Orphan tag removed!', {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to remove orphan tag.', {
type: 'error'
});
}
};
// replaceMI (Multiple Issues)
window.ComradeLib.replaceMI = function(text) {
const miRegex = /\{\{Multiple[ _]issues\s*\|/gi;
let result = '';
let lastIndex = 0;
let match;
while ((match = miRegex.exec(text)) !== null) {
const start = match.index;
result += text.slice(lastIndex, start);
let depth = 0;
let i = start;
while (i < text.length) {
if (text[i] === '{' && text[i + 1] === '{') {
depth++;
i += 2;
} else if (text[i] === '}' && text[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
const end = i;
const full = text.slice(start, end);
const pipeIdx = full.indexOf('|');
const inner = full.slice(pipeIdx + 1, full.length - 2).trim();
const topLevel = ComradeLib.extractTopLevelTemplates(inner);
if (topLevel.length === 0) {
result += '';
} else if (topLevel.length === 1) {
result += topLevel[0].trim();
} else {
result += full;
}
lastIndex = end;
}
result += text.slice(lastIndex);
return result;
};
// extractTopLevelTemplates
window.ComradeLib.extractTopLevelTemplates = function(inner) {
const templates = [];
let i = 0;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
let depth = 0;
const start = i;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
depth++;
i += 2;
} else if (inner[i] === '}' && inner[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
templates.push(inner.slice(start, i));
} else {
i++;
}
}
return templates;
};
// Quality suite
// getOresPrediction
window.ComradeLib.getOresPrediction = async function() {
const pageName = mw.config.get('wgPageName');
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'ids',
formatversion: 2
});
const page = pageData.query.pages[0];
const revId = page?.revisions?.[0]?.revid;
if (!revId) return null;
const scores = await ComradeLib.fetchOresData([String(revId)]);
const pred = scores[String(revId)];
if (!pred) return null;
return pred.charAt(0).toUpperCase() + pred.slice(1);
} catch (e) {
console.warn("Comrade: getOresPrediction failed to fetch via LiftWing", e);
return null;
}
};
// checkQualityConsistency
window.ComradeLib.checkQualityConsistency = async function() {
const oresClass = await ComradeLib.getOresPrediction();
if (!oresClass || oresClass === 'Stub') return;
const talkTitle = 'Talk:' + mw.config.get('wgPageName');
try {
const [talkRes, pageRes] = await Promise.all([
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkTitle,
rvprop: 'content',
formatversion: 2
}),
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
})
]);
const talkPage = talkRes.query.pages[0];
const mainWikitext = pageRes.query.pages[0].revisions[0].content;
const hasStubTag = /\{\{[^}|]*stub[^}]*\}\}/i.test(mainWikitext);
let talkClass = null;
if (!talkPage.missing && talkPage.revisions) {
const classMatch = talkPage.revisions[0].content.match(/\|class\s*=\s*([^|}\s]+)/i);
if (classMatch) {
const rawClass = classMatch[1].toLowerCase();
if (rawClass === 'ga') talkClass = 'Ga';
else if (rawClass === 'fa') talkClass = 'Fa';
else talkClass = rawClass.charAt(0).toUpperCase() + rawClass.slice(1);
}
}
const isStubRated = talkClass === 'Stub';
const isStartRated = talkClass === 'Start';
const needsPromotionNudge = (isStubRated && (oresClass === 'Start' || oresClass === 'C')) || (isStartRated && oresClass === 'C');
const needsHighQualNudge = (oresClass !== talkClass) && !needsPromotionNudge;
const lingeringStub = (talkClass && !isStubRated && hasStubTag);
if (talkPage.missing || needsHighQualNudge || needsPromotionNudge || lingeringStub) {
const $container = $('<div>').addClass('comrade-container');
const $content = $('<div>').append($('<strong>').text('Quality:'), " ");
let message = `ORES suggests <b>${oresClass}</b>. `;
if (talkPage.missing) {
message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `;
const $btnRater = $('<button>').text('Open Rater').on('click', () => $('#ca-rater, #t-rater').find('a')[0].click());
$content.append(message, $btnRater);
} else if (needsHighQualNudge) {
if (oresClass === talkClass) {
return;
}
message = `ORES predicts <b>${oresClass}</b>. Please assess manually. `;
const $btnOkay = $('<button>').text('OK').on('click', () => {
window.location.hash = "#content";
mw.notify("Review the criteria for " + oresClass + "-class.");
$container.find('.comrade-dismiss').click();
});
$content.append(message, $btnOkay);
} else if (lingeringStub && !needsPromotionNudge) {
message = `Article is ${talkClass}-class, but stub tags remain. `;
const $btnRemove = $('<button>').text('Remove stub tags').on('click', () => ComradeLib.performStubRemoval());
$content.append(message, $btnRemove);
} else if (needsPromotionNudge) {
const $btnTarget = $('<button>').text(`Promote to ${oresClass}`).on('click', () => ComradeLib.performPromotion(oresClass));
$content.append(message, $btnTarget);
if (isStubRated && oresClass === 'C') {
const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => ComradeLib.performPromotion('Start'));
$content.append("\u00A0or\u00A0", $btnStart);
}
}
$container.append($('<div>').addClass('comrade-header').append($content));
$('#content').prepend($container);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.error("Comrade quality check failed", e);
}
};
// performStubRemoval
window.ComradeLib.performStubRemoval = async function() {
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
});
let wikitext = pageData.query.pages[0].revisions[0].content;
// 'g' flag is appropriate here for replacement
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const newWikitext = wikitext.replace(stubRegex, '').trim();
if (wikitext === newWikitext) {
mw.notify("No stub tags found to remove.");
return;
}
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: mw.config.get("wgPageName"),
text: newWikitext,
summary: `Removing lingering stub tags per ORES/Talk assessment`,
nocreate: true
});
location.reload();
} catch (err) {
console.error("Comrade failed to remove stub tags:", err);
mw.notify("Error removing stub tags.", {
type: 'error'
});
}
};
// performPromotion
window.ComradeLib.performPromotion = async function(newClass) {
const classLinks = {
'Start': '[[WP:STARTCLASS|Start-Class]]',
'C': '[[WP:CCLASS|C-Class]]',
'B': '[[WP:BCLASS|B-Class]]'
};
const linkedClass = classLinks[newClass] || newClass;
const pageName = mw.config.get('wgPageName');
const talkName = 'Talk:' + pageName;
try {
const talkData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkName,
rvprop: 'content',
formatversion: 2
});
let talkWikitext = talkData.query.pages[0].revisions[0].content;
const newTalkWikitext = talkWikitext.replace(/(\|class\s*=\s*)([^|}\s]+)/i, `$1${newClass}`);
if (talkWikitext !== newTalkWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: talkName,
text: newTalkWikitext,
summary: `Promoting article to ${linkedClass} per review and [[WP:ORES]]`
});
}
const artData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'content',
formatversion: 2
});
let artWikitext = artData.query.pages[0].revisions[0].content;
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const cleanArtWikitext = artWikitext.replace(stubRegex, '');
if (artWikitext !== cleanArtWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: pageName,
text: cleanArtWikitext,
summary: `Removing stub tags; article promoted to ${linkedClass}`,
nocreate: true
});
}
location.reload();
} catch (e) {
console.error("Promotion failed:", e);
mw.notify("Promotion failed during multi-step edit.", {
type: 'error'
});
}
};
// Quality suite END
// Redirects
// checkAndRenderRedirect
window.ComradeLib.checkAndRenderRedirect = async function(redirTitle, targetTitle, rCategory, labelText, customWikitext) {
const res = await ComradeLib.api.get({
action: 'query',
titles: redirTitle,
formatversion: 2
});
if (res.query.pages[0].missing) {
const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-');
const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Creating redirect to [[${targetTitle}]] (${labelText.toLowerCase()})`;
const $btn = $('<button>').text('Create redirect').on('click', () => ComradeLib.createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
ComradeLib.appendDismiss($container);
}
};
// createModernRedirect
window.ComradeLib.createModernRedirect = async function(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) {
const api = ComradeLib.api;
const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Creating redirect to [[${targetTitle}]]`;
try {
await api.postWithEditToken({
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
});
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
} catch (e) {
mw.notify("Failed to create redirect.", {
type: 'error'
});
}
};
// Domain name
window.ComradeLib.checkDomainRedirect = async function(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
try {
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
});
const res = await fetch(url).then(r => r.json());
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
} catch (e) {
console.warn("Comrade: Wikidata API call failed.");
}
}
if (!domainCandidate) {
domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href");
}
if (domainCandidate) {
try {
const url = new URL(domainCandidate);
let domain = url.hostname.replace(/^www\d*\./, "");
if (domain.includes(".") && url.pathname === "/") {
const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate);
try {
const response = await fetch(citationURL);
if (response.ok) {
await ComradeLib.checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {
console.warn("Comrade: Citation API check failed.");
}
}
} catch (e) {
console.warn("Comrade: Error parsing URL object.");
}
}
};
// Long name
window.ComradeLib.getLongNameFromWikitext = function(wikitext) {
const firstLine = wikitext.split('\n').find(l => l.includes("'''"));
if (!firstLine) return null;
const boldMatch = firstLine.match(/'''(.+?)'''/);
if (!boldMatch) return null;
return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim();
};
// Expanded normalization
window.ComradeLib.getNormalizedTitles = function(currentTitle) {
const maps = {
all: {
'æ': 'ae',
'Æ': 'Ae',
'œ': 'oe',
'Œ': 'Oe',
'ij': 'ij',
'IJ': 'Ij',
'ß': 'ss',
'ẞ': 'SS',
'ð': 'd',
'Ð': 'D',
'ø': 'o',
'Ø': 'O',
'ł': 'l',
'Ł': 'L',
'þ': 'th',
'Þ': 'Th',
'ŋ': 'ng',
'Ŋ': 'Ng'
}
};
let cleanAscii = currentTitle.split('').map((char, index) => {
let rep = maps.all[char];
if (rep) {
return (index > 0 && rep.length > 1) ? rep.toLowerCase() : rep;
}
return char;
}).join('');
cleanAscii = cleanAscii.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
return {
ascii: (cleanAscii.toLowerCase() !== currentTitle.toLowerCase()) ? cleanAscii : null
};
};
// Redirects END
// SD
window.ComradeLib.checkShortDescDates = async function(wikitext, entity) {
const sdMatch = wikitext.match(/\{\{[Ss]hort description\|([^|{}]+)(?:\|[^}]*)?\}\}/);
if (!sdMatch) return;
const currentSD = sdMatch[1].trim();
const dateRegex = /\((?:born |died |fl\. )?\d{3,4}(?:–\d{3,4}|s)?\)/i;
const hasDate = dateRegex.test(currentSD);
if (hasDate && currentSD.length > 15) return;
const getYear = (val) => {
if (!val || !val.time) return null;
const yearMatch = val.time.match(/[+-](\d{4,})/);
if (!yearMatch) return null;
let year = parseInt(yearMatch[1]);
if (val.time.startsWith('-')) return year + " BC";
if (val.precision === 8) return year + "s";
return year;
};
let dateStr = "";
const bYear = getYear(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value);
const dYear = getYear(entity.claims?.P570?.[0]?.mainsnak?.datavalue?.value);
if (bYear && dYear) dateStr = `(${bYear}–${dYear})`;
else if (bYear) dateStr = `(born ${bYear})`;
else if (dYear) dateStr = `(died ${dYear})`;
else {
const flYear = getYear(entity.claims?.P1317?.[0]?.mainsnak?.datavalue?.value);
if (flYear) dateStr = `(fl. ${flYear})`;
}
if (!dateStr) return;
let finalSD = currentSD;
const words = currentSD.split(' ');
if (words.length === 1 && !hasDate) {
const occupationId = entity.claims?.P106?.[0]?.mainsnak?.datavalue?.value?.id;
if (occupationId) {
const occRes = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${occupationId}&props=labels&languages=en&format=json&origin=*`).then(r => r.json());
const occLabel = occRes.entities[occupationId]?.labels?.en?.value;
if (occLabel) {
finalSD = `${occLabel.charAt(0).toLowerCase() + occLabel.slice(1)} ${dateStr}`;
}
} else {
finalSD = `${currentSD} ${dateStr}`;
}
} else if (!hasDate) {
finalSD = `${currentSD} ${dateStr}`;
}
if (finalSD !== currentSD) {
ComradeLib.renderSDNudge(currentSD, finalSD);
}
};
window.ComradeLib.renderSDNudge = function(oldSD, newSD) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Short Description:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text('Dates missing: '), $('<code>').text(newSD));
const $btn = $('<button>').text('Update').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
content = content.replace(/(\{\{[Ss]hort description\|)[^|}]+/, `$1${newSD}`);
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Updating short description: +dates/role per [[WP:SDFORMAT]]`,
nocreate: true
});
mw.notify('Short description updated!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update SD.', {
type: 'error'
});
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
//</nowiki>
7vb44585lfbula5zh2c3kv8xjzb1lbw
739956
739955
2026-05-01T10:41:25Z
Sam Sailor
26820
Test
739956
javascript
text/javascript
//<nowiki>
/* global mw, $, ComradeLib */
/**
* Comrade-Lib
* Helper functions
*/
mw.util.addCSS(`
.ambox-Orphan { display: block !important; }
.comrade-container {
box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px;
padding: 10px 15px; margin: 10px 0; display: flex;
flex-direction: column; align-items: flex-start; gap: 8px;
border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5;
}
.comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.comrade-content {
display: flex; flex-direction: column;
gap: 5px; width: 100%;
}
.comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; }
.comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; }
.comrade-info, .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; }
.comrade-container button:not(.comrade-dismiss) {
background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px;
padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px;
}
.comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; }
.comrade-dismiss {
background: transparent; border: none; color: #d33; font-weight: bold;
cursor: pointer; font-size: 0.9em;
}
.comrade-dismiss:hover { text-decoration: underline; }
.comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; }
`);
window.ComradeLib = window.ComradeLib || {};
window.ComradeLib.api = new mw.Api();
window.ComradeLib.appendDismiss = function($el) {
$('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
};
// Category audit
window.ComradeLib.arrayChunk = function(arr, size) {
const result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
};
window.ComradeLib.fetchOresData = async function(revIds) {
const scores = {};
const BATCH = 1;
for (let i = 0; i < revIds.length; i += BATCH) {
const rid = revIds[i];
try {
const response = await fetch('https://api.wikimedia.org/service/lw/inference/v1/models/enwiki-articlequality:predict', {
method: 'POST',
body: JSON.stringify({
rev_id: parseInt(rid, 10)
})
});
if (!response.ok) {
console.warn(`Comrade: Lift Wing ${response.status} for rev ${rid}`);
continue;
}
const data = await response.json();
const prediction = data?.enwiki?.scores?.[String(rid)]?.articlequality?.score?.prediction ?? data?.output?.prediction ?? null;
if (prediction) scores[String(rid)] = prediction;
} catch (e) {
console.warn(`Comrade: Lift Wing fetch error for rev ${rid}`, e);
}
}
return scores;
};
window.ComradeLib.performCategoryAudit = async function() {
const api = new mw.Api();
const $links = $('#mw-pages li a');
const titles = $links.map((i, el) => $(el).attr('title')).get().filter(t => t && !t.startsWith('Talk:'));
if (!titles.length) return;
const $btn = $('#comrade-audit-btn');
$btn.prop('disabled', true).text('Auditing...');
const chunks = ComradeLib.arrayChunk(titles, 25);
const results = {};
try {
for (const chunk of chunks) {
const talkTitles = chunk.map(t => 'Talk:' + t);
const res = await api.get({
action: 'query',
titles: chunk.concat(talkTitles).join('|'),
prop: 'revisions',
rvprop: 'content|ids',
rvslots: 'main',
formatversion: 2
});
const pages = res.query.pages;
const getContent = pg => {
if (!pg || pg.missing || !pg.revisions) return '';
const rev = pg.revisions[0];
return rev.slots?.main?.content ?? rev.content ?? '';
};
const revIds = [];
const titleToRevId = {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
if (mainPg?.revisions?.[0]?.revid) {
const rid = String(mainPg.revisions[0].revid);
revIds.push(rid);
titleToRevId[title] = rid;
}
});
const oresScores = revIds.length ? await ComradeLib.fetchOresData(revIds) : {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
const talkPg = pages.find(p => p.title.toLowerCase() === ('talk:' + title).toLowerCase() && !p.missing);
const talkContent = getContent(talkPg);
const mainContent = getContent(mainPg);
const classMatch = talkContent.match(/\|\s*class\s*=\s*([^|{}\n\r]+)/i);
const rawClass = classMatch ? classMatch[1].trim() : '';
const talkClass = rawClass ? rawClass.charAt(0).toUpperCase() + rawClass.slice(1).toLowerCase() : 'Unrated';
const hasStubTags = /\{\{[^}]*stub[^}]*\}\}/i.test(mainContent);
const rid = titleToRevId[title];
const predicted = rid ? (oresScores[rid] ?? null) : null;
results[title] = {
talkClass,
predicted,
hasStubTags
};
});
}
Object.entries(results).forEach(([title, {
talkClass,
predicted,
hasStubTags
}]) => {
ComradeLib.renderCategoryAuditUI(title, talkClass, predicted, hasStubTags);
});
$btn.text('Audit complete').fadeOut(2000);
} catch (error) {
console.error('Comrade: Audit error', error);
$btn.text('Audit failed').prop('disabled', false);
}
};
window.ComradeLib.renderCategoryAuditUI = function(title, talkClass, predicted, hasStub) {
const $link = $(`#mw-pages li a[title="${title}"]`);
if (!$link.length) return;
let label = '';
let style = 'font-size: 0.85em; margin-left: 8px; font-weight: normal;';
if (talkClass !== 'Stub' && talkClass !== 'Unrated' && hasStub) {
label = `${talkClass} (stub tag present) : ORES: ${predicted || '?'}`;
style += ' color: #f0ad4e;';
} else if (talkClass === 'Stub' && predicted && predicted !== 'Stub') {
label = `Stub : ${predicted}`;
style += ' color: #27ae60;';
} else if (predicted && predicted !== talkClass) {
label = `${talkClass} : ${predicted}`;
style += ' color: #36c;';
}
if (label) {
$link.parent().find('.comrade-audit-label').remove();
const $span = $('<span>').addClass('comrade-audit-label').attr('style', style).text(` [${label}]`);
$link.after($span);
}
};
// Category audit END
// Deorphanizer
// performDeorphan
window.ComradeLib.performDeorphan = async function() {
try {
await ComradeLib.api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]';
text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, '');
text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|');
text = ComradeLib.replaceMI(text);
text = text.replace(/\n{3,}/g, '\n\n').trim();
return {
text,
summary,
minor: true
};
});
mw.notify('Orphan tag removed!', {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to remove orphan tag.', {
type: 'error'
});
}
};
// replaceMI (Multiple Issues)
window.ComradeLib.replaceMI = function(text) {
const miRegex = /\{\{Multiple[ _]issues\s*\|/gi;
let result = '';
let lastIndex = 0;
let match;
while ((match = miRegex.exec(text)) !== null) {
const start = match.index;
result += text.slice(lastIndex, start);
let depth = 0;
let i = start;
while (i < text.length) {
if (text[i] === '{' && text[i + 1] === '{') {
depth++;
i += 2;
} else if (text[i] === '}' && text[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
const end = i;
const full = text.slice(start, end);
const pipeIdx = full.indexOf('|');
const inner = full.slice(pipeIdx + 1, full.length - 2).trim();
const topLevel = ComradeLib.extractTopLevelTemplates(inner);
if (topLevel.length === 0) {
result += '';
} else if (topLevel.length === 1) {
result += topLevel[0].trim();
} else {
result += full;
}
lastIndex = end;
}
result += text.slice(lastIndex);
return result;
};
// extractTopLevelTemplates
window.ComradeLib.extractTopLevelTemplates = function(inner) {
const templates = [];
let i = 0;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
let depth = 0;
const start = i;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
depth++;
i += 2;
} else if (inner[i] === '}' && inner[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
templates.push(inner.slice(start, i));
} else {
i++;
}
}
return templates;
};
// Quality suite
// getOresPrediction
window.ComradeLib.getOresPrediction = async function() {
const pageName = mw.config.get('wgPageName');
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'ids',
formatversion: 2
});
const page = pageData.query.pages[0];
const revId = page?.revisions?.[0]?.revid;
if (!revId) return null;
const scores = await ComradeLib.fetchOresData([String(revId)]);
const pred = scores[String(revId)];
if (!pred) return null;
return pred.charAt(0).toUpperCase() + pred.slice(1);
} catch (e) {
console.warn("Comrade: getOresPrediction failed to fetch via LiftWing", e);
return null;
}
};
// checkQualityConsistency
window.ComradeLib.checkQualityConsistency = async function() {
const oresClass = await ComradeLib.getOresPrediction();
if (!oresClass || oresClass === 'Stub') return;
const talkTitle = 'Talk:' + mw.config.get('wgPageName');
try {
const [talkRes, pageRes] = await Promise.all([
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkTitle,
rvprop: 'content',
formatversion: 2
}),
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
})
]);
const talkPage = talkRes.query.pages[0];
const mainWikitext = pageRes.query.pages[0].revisions[0].content;
const hasStubTag = /\{\{[^}|]*stub[^}]*\}\}/i.test(mainWikitext);
let talkClass = null;
if (!talkPage.missing && talkPage.revisions) {
const classMatch = talkPage.revisions[0].content.match(/\|class\s*=\s*([^|}\s]+)/i);
if (classMatch) {
const rawClass = classMatch[1].toLowerCase();
if (rawClass === 'ga') talkClass = 'Ga';
else if (rawClass === 'fa') talkClass = 'Fa';
else talkClass = rawClass.charAt(0).toUpperCase() + rawClass.slice(1);
}
}
if (oresClass === talkClass) return;
const isStubRated = talkClass === 'Stub';
const isStartRated = talkClass === 'Start';
const needsPromotionNudge = (isStubRated && (oresClass === 'Start' || oresClass === 'C')) || (isStartRated && oresClass === 'C');
const needsHighQualNudge = (oresClass !== talkClass) && !needsPromotionNudge;
const lingeringStub = (talkClass && !isStubRated && hasStubTag);
if (talkPage.missing || needsHighQualNudge || needsPromotionNudge || lingeringStub) {
const $container = $('<div>').addClass('comrade-container comrade-info');
const $content = $('<div>').append($('<strong>').text('Quality:'), " ");
let message = `ORES suggests <b>${oresClass}</b>. `;
if (talkPage.missing) {
message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `;
const $btnRater = $('<button>').text('Open Rater').on('click', () => $('#ca-rater, #t-rater').find('a')[0].click());
$content.append(message, $btnRater);
} else if (needsHighQualNudge) {
message = `ORES predicts <b>${oresClass}</b>. Please assess manually. `;
const $btnOkay = $('<button>').text('OK').on('click', () => {
window.location.hash = "#content";
mw.notify("Review the criteria for " + oresClass + "-class.");
$container.find('.comrade-dismiss').click();
});
$content.append(message, $btnOkay);
} else if (lingeringStub && !needsPromotionNudge) {
message = `Article is ${talkClass}-class, but stub tags remain. `;
const $btnRemove = $('<button>').text('Remove stub tags').on('click', () => ComradeLib.performStubRemoval());
$content.append(message, $btnRemove);
} else if (needsPromotionNudge) {
const $btnTarget = $('<button>').text(`Promote to ${oresClass}`).on('click', () => ComradeLib.performPromotion(oresClass));
$content.append(message, $btnTarget);
if (isStubRated && oresClass === 'C') {
const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => ComradeLib.performPromotion('Start'));
$content.append("\u00A0or\u00A0", $btnStart);
}
}
$container.append($('<div>').addClass('comrade-header').append($content));
$('#content').prepend($container);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.error("Comrade quality check failed", e);
}
};
// performStubRemoval
window.ComradeLib.performStubRemoval = async function() {
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
});
let wikitext = pageData.query.pages[0].revisions[0].content;
// 'g' flag is appropriate here for replacement
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const newWikitext = wikitext.replace(stubRegex, '').trim();
if (wikitext === newWikitext) {
mw.notify("No stub tags found to remove.");
return;
}
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: mw.config.get("wgPageName"),
text: newWikitext,
summary: `Removing lingering stub tags per ORES/Talk assessment`,
nocreate: true
});
location.reload();
} catch (err) {
console.error("Comrade failed to remove stub tags:", err);
mw.notify("Error removing stub tags.", {
type: 'error'
});
}
};
// performPromotion
window.ComradeLib.performPromotion = async function(newClass) {
const classLinks = {
'Start': '[[WP:STARTCLASS|Start-Class]]',
'C': '[[WP:CCLASS|C-Class]]',
'B': '[[WP:BCLASS|B-Class]]'
};
const linkedClass = classLinks[newClass] || newClass;
const pageName = mw.config.get('wgPageName');
const talkName = 'Talk:' + pageName;
try {
const talkData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkName,
rvprop: 'content',
formatversion: 2
});
let talkWikitext = talkData.query.pages[0].revisions[0].content;
const newTalkWikitext = talkWikitext.replace(/(\|class\s*=\s*)([^|}\s]+)/i, `$1${newClass}`);
if (talkWikitext !== newTalkWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: talkName,
text: newTalkWikitext,
summary: `Promoting article to ${linkedClass} per review and [[WP:ORES]]`
});
}
const artData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'content',
formatversion: 2
});
let artWikitext = artData.query.pages[0].revisions[0].content;
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const cleanArtWikitext = artWikitext.replace(stubRegex, '');
if (artWikitext !== cleanArtWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: pageName,
text: cleanArtWikitext,
summary: `Removing stub tags; article promoted to ${linkedClass}`,
nocreate: true
});
}
location.reload();
} catch (e) {
console.error("Promotion failed:", e);
mw.notify("Promotion failed during multi-step edit.", {
type: 'error'
});
}
};
// Quality suite END
// Redirects
// checkAndRenderRedirect
window.ComradeLib.checkAndRenderRedirect = async function(redirTitle, targetTitle, rCategory, labelText, customWikitext) {
const res = await ComradeLib.api.get({
action: 'query',
titles: redirTitle,
formatversion: 2
});
if (res.query.pages[0].missing) {
const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-');
const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Creating redirect to [[${targetTitle}]] (${labelText.toLowerCase()})`;
const $btn = $('<button>').text('Create redirect').on('click', () => ComradeLib.createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
ComradeLib.appendDismiss($container);
}
};
// createModernRedirect
window.ComradeLib.createModernRedirect = async function(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) {
const api = ComradeLib.api;
const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Creating redirect to [[${targetTitle}]]`;
try {
await api.postWithEditToken({
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
});
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
} catch (e) {
mw.notify("Failed to create redirect.", {
type: 'error'
});
}
};
// Domain name
window.ComradeLib.checkDomainRedirect = async function(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
try {
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
});
const res = await fetch(url).then(r => r.json());
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
} catch (e) {
console.warn("Comrade: Wikidata API call failed.");
}
}
if (!domainCandidate) {
domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href");
}
if (domainCandidate) {
try {
const url = new URL(domainCandidate);
let domain = url.hostname.replace(/^www\d*\./, "");
if (domain.includes(".") && url.pathname === "/") {
const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate);
try {
const response = await fetch(citationURL);
if (response.ok) {
await ComradeLib.checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {
console.warn("Comrade: Citation API check failed.");
}
}
} catch (e) {
console.warn("Comrade: Error parsing URL object.");
}
}
};
// Long name
window.ComradeLib.getLongNameFromWikitext = function(wikitext) {
const firstLine = wikitext.split('\n').find(l => l.includes("'''"));
if (!firstLine) return null;
const boldMatch = firstLine.match(/'''(.+?)'''/);
if (!boldMatch) return null;
return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim();
};
// Expanded normalization
window.ComradeLib.getNormalizedTitles = function(currentTitle) {
const maps = {
all: {
'æ': 'ae',
'Æ': 'Ae',
'œ': 'oe',
'Œ': 'Oe',
'ij': 'ij',
'IJ': 'Ij',
'ß': 'ss',
'ẞ': 'SS',
'ð': 'd',
'Ð': 'D',
'ø': 'o',
'Ø': 'O',
'ł': 'l',
'Ł': 'L',
'þ': 'th',
'Þ': 'Th',
'ŋ': 'ng',
'Ŋ': 'Ng'
}
};
let cleanAscii = currentTitle.split('').map((char, index) => {
let rep = maps.all[char];
if (rep) {
return (index > 0 && rep.length > 1) ? rep.toLowerCase() : rep;
}
return char;
}).join('');
cleanAscii = cleanAscii.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
return {
ascii: (cleanAscii.toLowerCase() !== currentTitle.toLowerCase()) ? cleanAscii : null
};
};
// Redirects END
// SD
window.ComradeLib.checkShortDescDates = async function(wikitext, entity) {
const sdMatch = wikitext.match(/\{\{[Ss]hort description\|([^|{}]+)(?:\|[^}]*)?\}\}/);
if (!sdMatch) return;
const currentSD = sdMatch[1].trim();
const dateRegex = /\((?:born |died |fl\. )?\d{3,4}(?:–\d{3,4}|s)?\)/i;
const hasDate = dateRegex.test(currentSD);
if (hasDate && currentSD.length > 15) return;
const getYear = (val) => {
if (!val || !val.time) return null;
const yearMatch = val.time.match(/[+-](\d{4,})/);
if (!yearMatch) return null;
let year = parseInt(yearMatch[1]);
if (val.time.startsWith('-')) return year + " BC";
if (val.precision === 8) return year + "s";
return year;
};
let dateStr = "";
const bYear = getYear(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value);
const dYear = getYear(entity.claims?.P570?.[0]?.mainsnak?.datavalue?.value);
if (bYear && dYear) dateStr = `(${bYear}–${dYear})`;
else if (bYear) dateStr = `(born ${bYear})`;
else if (dYear) dateStr = `(died ${dYear})`;
else {
const flYear = getYear(entity.claims?.P1317?.[0]?.mainsnak?.datavalue?.value);
if (flYear) dateStr = `(fl. ${flYear})`;
}
if (!dateStr) return;
let finalSD = currentSD;
const words = currentSD.split(' ');
if (words.length === 1 && !hasDate) {
const occupationId = entity.claims?.P106?.[0]?.mainsnak?.datavalue?.value?.id;
if (occupationId) {
const occRes = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${occupationId}&props=labels&languages=en&format=json&origin=*`).then(r => r.json());
const occLabel = occRes.entities[occupationId]?.labels?.en?.value;
if (occLabel) {
finalSD = `${occLabel.charAt(0).toLowerCase() + occLabel.slice(1)} ${dateStr}`;
}
} else {
finalSD = `${currentSD} ${dateStr}`;
}
} else if (!hasDate) {
finalSD = `${currentSD} ${dateStr}`;
}
if (finalSD !== currentSD) {
ComradeLib.renderSDNudge(currentSD, finalSD);
}
};
window.ComradeLib.renderSDNudge = function(oldSD, newSD) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Short Description:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text('Dates missing: '), $('<code>').text(newSD));
const $btn = $('<button>').text('Update').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
content = content.replace(/(\{\{[Ss]hort description\|)[^|}]+/, `$1${newSD}`);
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Updating short description: +dates/role per [[WP:SDFORMAT]]`,
nocreate: true
});
mw.notify('Short description updated!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update SD.', {
type: 'error'
});
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
//</nowiki>
a9gcb7gax4v6n25fczle6qapefbx21w
739969
739956
2026-05-01T11:18:11Z
Sam Sailor
26820
Test
739969
javascript
text/javascript
//<nowiki>
/* global mw, $, ComradeLib */
/**
* Comrade-Lib
* Helper functions
*/
mw.util.addCSS(`
.ambox-Orphan { display: block !important; }
.comrade-container {
box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px;
padding: 10px 15px; margin: 10px 0; display: flex;
flex-direction: column; align-items: flex-start; gap: 8px;
border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5;
}
.comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.comrade-content {
display: flex; flex-direction: column;
gap: 5px; width: 100%;
}
.comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; }
.comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; }
.comrade-info, .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; }
.comrade-container button:not(.comrade-dismiss) {
background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px;
padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px;
}
.comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; }
.comrade-dismiss {
background: transparent; border: none; color: #d33; font-weight: bold;
cursor: pointer; font-size: 0.9em;
}
.comrade-dismiss:hover { text-decoration: underline; }
.comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; }
`);
window.ComradeLib = window.ComradeLib || {};
window.ComradeLib.api = new mw.Api();
window.ComradeLib.appendDismiss = function($el) {
$('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
};
// Category audit
window.ComradeLib.arrayChunk = function(arr, size) {
const result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
};
window.ComradeLib.fetchOresData = async function(revIds) {
const scores = {};
const BATCH = 1;
for (let i = 0; i < revIds.length; i += BATCH) {
const rid = revIds[i];
try {
const response = await fetch('https://api.wikimedia.org/service/lw/inference/v1/models/enwiki-articlequality:predict', {
method: 'POST',
body: JSON.stringify({
rev_id: parseInt(rid, 10)
})
});
if (!response.ok) {
console.warn(`Comrade: Lift Wing ${response.status} for rev ${rid}`);
continue;
}
const data = await response.json();
const prediction = data?.enwiki?.scores?.[String(rid)]?.articlequality?.score?.prediction ?? data?.output?.prediction ?? null;
if (prediction) scores[String(rid)] = prediction;
} catch (e) {
console.warn(`Comrade: Lift Wing fetch error for rev ${rid}`, e);
}
}
return scores;
};
window.ComradeLib.performCategoryAudit = async function() {
const api = new mw.Api();
const $links = $('#mw-pages li a');
const titles = $links.map((i, el) => $(el).attr('title')).get().filter(t => t && !t.startsWith('Talk:'));
if (!titles.length) return;
const $btn = $('#comrade-audit-btn');
$btn.prop('disabled', true).text('Auditing...');
const chunks = ComradeLib.arrayChunk(titles, 25);
const results = {};
try {
for (const chunk of chunks) {
const talkTitles = chunk.map(t => 'Talk:' + t);
const res = await api.get({
action: 'query',
titles: chunk.concat(talkTitles).join('|'),
prop: 'revisions',
rvprop: 'content|ids',
rvslots: 'main',
formatversion: 2
});
const pages = res.query.pages;
const getContent = pg => {
if (!pg || pg.missing || !pg.revisions) return '';
const rev = pg.revisions[0];
return rev.slots?.main?.content ?? rev.content ?? '';
};
const revIds = [];
const titleToRevId = {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
if (mainPg?.revisions?.[0]?.revid) {
const rid = String(mainPg.revisions[0].revid);
revIds.push(rid);
titleToRevId[title] = rid;
}
});
const oresScores = revIds.length ? await ComradeLib.fetchOresData(revIds) : {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
const talkPg = pages.find(p => p.title.toLowerCase() === ('talk:' + title).toLowerCase() && !p.missing);
const talkContent = getContent(talkPg);
const mainContent = getContent(mainPg);
const classMatch = talkContent.match(/\|\s*class\s*=\s*([^|{}\n\r]+)/i);
const rawClass = classMatch ? classMatch[1].trim() : '';
const talkClass = rawClass ? rawClass.charAt(0).toUpperCase() + rawClass.slice(1).toLowerCase() : 'Unrated';
const hasStubTags = /\{\{[^}]*stub[^}]*\}\}/i.test(mainContent);
const rid = titleToRevId[title];
const predicted = rid ? (oresScores[rid] ?? null) : null;
results[title] = {
talkClass,
predicted,
hasStubTags
};
});
}
Object.entries(results).forEach(([title, {
talkClass,
predicted,
hasStubTags
}]) => {
ComradeLib.renderCategoryAuditUI(title, talkClass, predicted, hasStubTags);
});
$btn.text('Audit complete').fadeOut(2000);
} catch (error) {
console.error('Comrade: Audit error', error);
$btn.text('Audit failed').prop('disabled', false);
}
};
window.ComradeLib.renderCategoryAuditUI = function(title, talkClass, predicted, hasStub) {
const $link = $(`#mw-pages li a[title="${title}"]`);
if (!$link.length) return;
let label = '';
let style = 'font-size: 0.85em; margin-left: 8px; font-weight: normal;';
if (talkClass !== 'Stub' && talkClass !== 'Unrated' && hasStub) {
label = `${talkClass} (stub tag present) : ORES: ${predicted || '?'}`;
style += ' color: #f0ad4e;';
} else if (talkClass === 'Stub' && predicted && predicted !== 'Stub') {
label = `Stub : ${predicted}`;
style += ' color: #27ae60;';
} else if (predicted && predicted !== talkClass) {
label = `${talkClass} : ${predicted}`;
style += ' color: #36c;';
}
if (label) {
$link.parent().find('.comrade-audit-label').remove();
const $span = $('<span>').addClass('comrade-audit-label').attr('style', style).text(` [${label}]`);
$link.after($span);
}
};
// Category audit END
// Deorphanizer
// performDeorphan
window.ComradeLib.performDeorphan = async function() {
try {
await ComradeLib.api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]';
text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, '');
text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|');
text = ComradeLib.replaceMI(text);
text = text.replace(/\n{3,}/g, '\n\n').trim();
return {
text,
summary,
minor: true
};
});
mw.notify('Orphan tag removed!', {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to remove orphan tag.', {
type: 'error'
});
}
};
// replaceMI (Multiple Issues)
window.ComradeLib.replaceMI = function(text) {
const miRegex = /\{\{Multiple[ _]issues\s*\|/gi;
let result = '';
let lastIndex = 0;
let match;
while ((match = miRegex.exec(text)) !== null) {
const start = match.index;
result += text.slice(lastIndex, start);
let depth = 0;
let i = start;
while (i < text.length) {
if (text[i] === '{' && text[i + 1] === '{') {
depth++;
i += 2;
} else if (text[i] === '}' && text[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
const end = i;
const full = text.slice(start, end);
const pipeIdx = full.indexOf('|');
const inner = full.slice(pipeIdx + 1, full.length - 2).trim();
const topLevel = ComradeLib.extractTopLevelTemplates(inner);
if (topLevel.length === 0) {
result += '';
} else if (topLevel.length === 1) {
result += topLevel[0].trim();
} else {
result += full;
}
lastIndex = end;
}
result += text.slice(lastIndex);
return result;
};
// extractTopLevelTemplates
window.ComradeLib.extractTopLevelTemplates = function(inner) {
const templates = [];
let i = 0;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
let depth = 0;
const start = i;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
depth++;
i += 2;
} else if (inner[i] === '}' && inner[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
templates.push(inner.slice(start, i));
} else {
i++;
}
}
return templates;
};
// Quality suite
// getOresPrediction
window.ComradeLib.getOresPrediction = async function() {
const pageName = mw.config.get('wgPageName');
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'ids',
formatversion: 2
});
const page = pageData.query.pages[0];
const revId = page?.revisions?.[0]?.revid;
if (!revId) return null;
const scores = await ComradeLib.fetchOresData([String(revId)]);
const pred = scores[String(revId)];
if (!pred) return null;
return pred.charAt(0).toUpperCase() + pred.slice(1);
} catch (e) {
console.warn("Comrade: getOresPrediction failed to fetch via LiftWing", e);
return null;
}
};
// checkQualityConsistency
window.ComradeLib.checkQualityConsistency = async function() {
const oresClass = await ComradeLib.getOresPrediction();
if (!oresClass || oresClass === 'Stub') return;
const talkTitle = 'Talk:' + mw.config.get('wgPageName');
try {
const [talkRes, pageRes] = await Promise.all([
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkTitle,
rvprop: 'content',
formatversion: 2
}),
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
})
]);
const talkPage = talkRes.query.pages[0];
const mainWikitext = pageRes.query.pages[0].revisions[0].content;
const hasStubTag = /\{\{[^}|]*stub[^}]*\}\}/i.test(mainWikitext);
let talkClass = null;
if (!talkPage.missing && talkPage.revisions) {
const classMatch = talkPage.revisions[0].content.match(/\|class\s*=\s*([^|}\s]+)/i);
if (classMatch) {
const rawClass = classMatch[1].toLowerCase();
if (rawClass === 'ga') talkClass = 'Ga';
else if (rawClass === 'fa') talkClass = 'Fa';
else talkClass = rawClass.charAt(0).toUpperCase() + rawClass.slice(1);
}
}
console.log(oresClass, talkClass);
if (oresClass.toLowerCase() === talkClass?.toLowerCase()) return;
const isStubRated = talkClass === 'Stub';
const isStartRated = talkClass === 'Start';
const needsPromotionNudge = (isStubRated && (oresClass === 'Start' || oresClass === 'C')) || (isStartRated && oresClass === 'C');
const needsHighQualNudge = (oresClass !== talkClass) && !needsPromotionNudge;
const lingeringStub = (talkClass && !isStubRated && hasStubTag);
if (talkPage.missing || needsHighQualNudge || needsPromotionNudge || lingeringStub) {
const $container = $('<div>').addClass('comrade-container comrade-info');
const $content = $('<div>').append($('<strong>').text('Quality:'), " ");
let message = `ORES suggests <b>${oresClass}</b>. `;
if (talkPage.missing) {
message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `;
const $btnRater = $('<button>').text('Open Rater').on('click', () => $('#ca-rater, #t-rater').find('a')[0].click());
$content.append(message, $btnRater);
} else if (needsHighQualNudge) {
message = `ORES predicts <b>${oresClass}</b>. Please assess manually. `;
const $btnOkay = $('<button>').text('OK').on('click', () => {
window.location.hash = "#content";
mw.notify("Review the criteria for " + oresClass + "-class.");
$container.find('.comrade-dismiss').click();
});
$content.append(message, $btnOkay);
} else if (lingeringStub && !needsPromotionNudge) {
message = `Article is ${talkClass}-class, but stub tags remain. `;
const $btnRemove = $('<button>').text('Remove stub tags').on('click', () => ComradeLib.performStubRemoval());
$content.append(message, $btnRemove);
} else if (needsPromotionNudge) {
const $btnTarget = $('<button>').text(`Promote to ${oresClass}`).on('click', () => ComradeLib.performPromotion(oresClass));
$content.append(message, $btnTarget);
if (isStubRated && oresClass === 'C') {
const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => ComradeLib.performPromotion('Start'));
$content.append("\u00A0or\u00A0", $btnStart);
}
}
$container.append($('<div>').addClass('comrade-header').append($content));
$('#content').prepend($container);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.error("Comrade quality check failed", e);
}
};
// performStubRemoval
window.ComradeLib.performStubRemoval = async function() {
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
});
let wikitext = pageData.query.pages[0].revisions[0].content;
// 'g' flag is appropriate here for replacement
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const newWikitext = wikitext.replace(stubRegex, '').trim();
if (wikitext === newWikitext) {
mw.notify("No stub tags found to remove.");
return;
}
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: mw.config.get("wgPageName"),
text: newWikitext,
summary: `Removing lingering stub tags per ORES/Talk assessment`,
nocreate: true
});
location.reload();
} catch (err) {
console.error("Comrade failed to remove stub tags:", err);
mw.notify("Error removing stub tags.", {
type: 'error'
});
}
};
// performPromotion
window.ComradeLib.performPromotion = async function(newClass) {
const classLinks = {
'Start': '[[WP:STARTCLASS|Start-Class]]',
'C': '[[WP:CCLASS|C-Class]]',
'B': '[[WP:BCLASS|B-Class]]'
};
const linkedClass = classLinks[newClass] || newClass;
const pageName = mw.config.get('wgPageName');
const talkName = 'Talk:' + pageName;
try {
const talkData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkName,
rvprop: 'content',
formatversion: 2
});
let talkWikitext = talkData.query.pages[0].revisions[0].content;
const newTalkWikitext = talkWikitext.replace(/(\|class\s*=\s*)([^|}\s]+)/i, `$1${newClass}`);
if (talkWikitext !== newTalkWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: talkName,
text: newTalkWikitext,
summary: `Promoting article to ${linkedClass} per review and [[WP:ORES]]`
});
}
const artData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'content',
formatversion: 2
});
let artWikitext = artData.query.pages[0].revisions[0].content;
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const cleanArtWikitext = artWikitext.replace(stubRegex, '');
if (artWikitext !== cleanArtWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: pageName,
text: cleanArtWikitext,
summary: `Removing stub tags; article promoted to ${linkedClass}`,
nocreate: true
});
}
location.reload();
} catch (e) {
console.error("Promotion failed:", e);
mw.notify("Promotion failed during multi-step edit.", {
type: 'error'
});
}
};
// Quality suite END
// Redirects
// checkAndRenderRedirect
window.ComradeLib.checkAndRenderRedirect = async function(redirTitle, targetTitle, rCategory, labelText, customWikitext) {
const res = await ComradeLib.api.get({
action: 'query',
titles: redirTitle,
formatversion: 2
});
if (res.query.pages[0].missing) {
const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-');
const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Creating redirect to [[${targetTitle}]] (${labelText.toLowerCase()})`;
const $btn = $('<button>').text('Create redirect').on('click', () => ComradeLib.createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
ComradeLib.appendDismiss($container);
}
};
// createModernRedirect
window.ComradeLib.createModernRedirect = async function(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) {
const api = ComradeLib.api;
const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Creating redirect to [[${targetTitle}]]`;
try {
await api.postWithEditToken({
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
});
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
} catch (e) {
mw.notify("Failed to create redirect.", {
type: 'error'
});
}
};
// Domain name
window.ComradeLib.checkDomainRedirect = async function(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
try {
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
});
const res = await fetch(url).then(r => r.json());
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
} catch (e) {
console.warn("Comrade: Wikidata API call failed.");
}
}
if (!domainCandidate) {
domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href");
}
if (domainCandidate) {
try {
const url = new URL(domainCandidate);
let domain = url.hostname.replace(/^www\d*\./, "");
if (domain.includes(".") && url.pathname === "/") {
const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate);
try {
const response = await fetch(citationURL);
if (response.ok) {
await ComradeLib.checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {
console.warn("Comrade: Citation API check failed.");
}
}
} catch (e) {
console.warn("Comrade: Error parsing URL object.");
}
}
};
// Long name
window.ComradeLib.getLongNameFromWikitext = function(wikitext) {
const firstLine = wikitext.split('\n').find(l => l.includes("'''"));
if (!firstLine) return null;
const boldMatch = firstLine.match(/'''(.+?)'''/);
if (!boldMatch) return null;
return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim();
};
// Expanded normalization
window.ComradeLib.getNormalizedTitles = function(currentTitle) {
const maps = {
all: {
'æ': 'ae',
'Æ': 'Ae',
'œ': 'oe',
'Œ': 'Oe',
'ij': 'ij',
'IJ': 'Ij',
'ß': 'ss',
'ẞ': 'SS',
'ð': 'd',
'Ð': 'D',
'ø': 'o',
'Ø': 'O',
'ł': 'l',
'Ł': 'L',
'þ': 'th',
'Þ': 'Th',
'ŋ': 'ng',
'Ŋ': 'Ng'
}
};
let cleanAscii = currentTitle.split('').map((char, index) => {
let rep = maps.all[char];
if (rep) {
return (index > 0 && rep.length > 1) ? rep.toLowerCase() : rep;
}
return char;
}).join('');
cleanAscii = cleanAscii.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
return {
ascii: (cleanAscii.toLowerCase() !== currentTitle.toLowerCase()) ? cleanAscii : null
};
};
// Redirects END
// SD
window.ComradeLib.checkShortDescDates = async function(wikitext, entity) {
const sdMatch = wikitext.match(/\{\{[Ss]hort description\|([^|{}]+)(?:\|[^}]*)?\}\}/);
if (!sdMatch) return;
const currentSD = sdMatch[1].trim();
const dateRegex = /\((?:born |died |fl\. )?\d{3,4}(?:–\d{3,4}|s)?\)/i;
const hasDate = dateRegex.test(currentSD);
if (hasDate && currentSD.length > 15) return;
const getYear = (val) => {
if (!val || !val.time) return null;
const yearMatch = val.time.match(/[+-](\d{4,})/);
if (!yearMatch) return null;
let year = parseInt(yearMatch[1]);
if (val.time.startsWith('-')) return year + " BC";
if (val.precision === 8) return year + "s";
return year;
};
let dateStr = "";
const bYear = getYear(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value);
const dYear = getYear(entity.claims?.P570?.[0]?.mainsnak?.datavalue?.value);
if (bYear && dYear) dateStr = `(${bYear}–${dYear})`;
else if (bYear) dateStr = `(born ${bYear})`;
else if (dYear) dateStr = `(died ${dYear})`;
else {
const flYear = getYear(entity.claims?.P1317?.[0]?.mainsnak?.datavalue?.value);
if (flYear) dateStr = `(fl. ${flYear})`;
}
if (!dateStr) return;
let finalSD = currentSD;
const words = currentSD.split(' ');
if (words.length === 1 && !hasDate) {
const occupationId = entity.claims?.P106?.[0]?.mainsnak?.datavalue?.value?.id;
if (occupationId) {
const occRes = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${occupationId}&props=labels&languages=en&format=json&origin=*`).then(r => r.json());
const occLabel = occRes.entities[occupationId]?.labels?.en?.value;
if (occLabel) {
finalSD = `${occLabel.charAt(0).toLowerCase() + occLabel.slice(1)} ${dateStr}`;
}
} else {
finalSD = `${currentSD} ${dateStr}`;
}
} else if (!hasDate) {
finalSD = `${currentSD} ${dateStr}`;
}
if (finalSD !== currentSD) {
ComradeLib.renderSDNudge(currentSD, finalSD);
}
};
window.ComradeLib.renderSDNudge = function(oldSD, newSD) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Short Description:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text('Dates missing: '), $('<code>').text(newSD));
const $btn = $('<button>').text('Update').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
content = content.replace(/(\{\{[Ss]hort description\|)[^|}]+/, `$1${newSD}`);
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Updating short description: +dates/role per [[WP:SDFORMAT]]`,
nocreate: true
});
mw.notify('Short description updated!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update SD.', {
type: 'error'
});
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
//</nowiki>
c83w5ymw8elunisshjb7z0qy5fr3thg
Template:Subst test 1
10
175047
739880
739757
2026-04-30T12:14:25Z
Cewbot
33876
Protected "[[Template:Subst test 1]]": protect test ([Edit=Allow only administrators] (indefinite) [Move=Allow only administrators] (indefinite))
739757
wikitext
text/x-wiki
* {{if empty}}{{if empty|1}}{{if empty||2}}
* {{{{{|subst:}}}if empty}}{{{{{|subst:}}}if empty|1}}{{{{{|subst:}}}if empty||2}}
* {{{{{|safesubst:}}}if empty}}{{{{{|safesubst:}}}if empty|1}}{{{{{|safesubst:}}}if empty||2}}
e7pzssxiu1lbye5wqbms5czyz42xhka
Kkw
0
175065
739881
2026-04-30T12:47:10Z
~2026-26243-04
73766
Created page with "• Very huge losses • End of Abbasid Rule in Tabaristan"
739881
wikitext
text/x-wiki
• Very huge losses
• End of Abbasid Rule in Tabaristan
qcb8iybzixaeii6afxefgn4kbws22cn
Odetari
0
175070
739897
2026-04-30T23:53:10Z
Cryptocurrency777
73698
Redirected page to [[Narcistic Personality Disorder (song)]]
739897
wikitext
text/x-wiki
#REDIRECT [[Narcistic Personality Disorder (song)]]
nef5nlrz8i44ign6w3ujpxk7n6h7yn9
DaniDV
0
175071
739898
2026-04-30T23:57:40Z
Cryptocurrency777
73698
Created page with " ''{{Infobox musical artist}}'' Darkphobia, a producer who made ''Decay'', duh {{Did you mean/box}} {{Welcome-copyright}}"
739898
wikitext
text/x-wiki
''{{Infobox musical artist}}''
Darkphobia, a producer who made ''Decay'', duh
{{Did you mean/box}}
{{Welcome-copyright}}
eh8vpuyp4iyhlzbnbfg0xgyhwl7kfip
739899
739898
2026-04-30T23:59:03Z
Cryptocurrency777
73698
739899
wikitext
text/x-wiki
''{{InfoboxTest|DaniDV|alias=Darkphobia|years active=2024+|agent=-}}''
Darkphobia, a producer who made ''Decay'', duh
{{Did you mean/box}}
{{Welcome-copyright}}
d419h788va8krnl419pdgk20afrspd3
Tourmaline
0
175073
739904
2026-05-01T02:03:03Z
Cryptocurrency777
73698
Created page with "'''Tourmaline''' is a group of silicate mineral that comes in many colors, but is mainly known for the watermelon color variant. Dravite, also called brown tourmaline, is a sodium magnesium variant. Some variants are magnetic. {{Infobox musical artist|Name=Tourmaline|image=File:Tourmaline-121240.jpg}} {| class="wikitable" |+ The 42 minerals in the group (endmember formulas) recognized by the [[International Mineralogical Association]] |- ! Species Name ! Ideal Endmemb..."
739904
wikitext
text/x-wiki
'''Tourmaline''' is a group of silicate mineral that comes in many colors, but is mainly known for the watermelon color variant. Dravite, also called brown tourmaline, is a sodium magnesium variant. Some variants are magnetic.
{{Infobox musical artist|Name=Tourmaline|image=File:Tourmaline-121240.jpg}}
{| class="wikitable"
|+ The 42 minerals in the group (endmember formulas) recognized by the [[International Mineralogical Association]]
|-
! Species Name
! Ideal Endmember Formula
! IMA Number
! Symbol
|-
| [[Adachiite]]
| CaFe<sup>2+</sup><sub>3</sub>Al<sub>6</sub>(Si<sub>5</sub>AlO<sub>18</sub>)(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>OH
| 2012-101
| Adc
|-
| [[Alumino-oxy-rossmanite]]
| ▢Al<sub>3</sub>Al<sub>6</sub>(Si<sub>5</sub>AlO<sub>18</sub>)(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2020-008
| Aorsm
|-
| [[Bosiite]]
| NaFe<sup>3+</sup><sub>3</sub>(Al<sub>4</sub>Mg<sub>2</sub>)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2014-094
| Bos
|-
| [[Celleriite]]
| ▢(Mn<sup>2+</sup><sub>2</sub>Al)Al<sub>6</sub>(Si<sub>6</sub>O<sub>18</sub>)(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>(OH)
| 2019-089
| Cll
|-
| [[Chromium-dravite]]
| NaMg<sub>3</sub>Cr<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>OH
| 1982-055
| Cdrv
|-
| [[Chromo-alumino-povondraite]]
| NaCr<sub>3</sub>(Al<sub>4</sub>Mg<sub>2</sub>)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2013-089
| Capov
|-
| [[Darrellhenryite]]
| NaLiAl<sub>2</sub>Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2012-026
| Dhry
|-
| [[#Chemical composition|Dravite]]
| NaMg<sub>3</sub>Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>OH
| - 1884 -
| Drv
|-
| [[Dutrowite]]
| Na(Fe<sub>2.5</sub>Ti<sub>0.5</sub>)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2019-082
| Dtw
|-
| [[Elbaite]]
|Na(Li<sub>1.5</sub>,Al<sub>1.5</sub>)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>OH
| - 1913 -
| Elb
|-
| [[Ertlite]]
|NaAl<sub>3</sub>Al<sub>6</sub>(Si<sub>4</sub>B<sub>2</sub>O<sub>18</sub>)(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2023-086
| Etl
|-
| [[Ferro-bosiite]]
| NaFe<sup>3+</sup><sub>3</sub>(Al<sub>4</sub>Fe<sup>2+</sup><sub>2</sub>)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2022-069
| Fbos
|-
| [[Feruvite]]
| CaFe<sup>2+</sup><sub>3</sub>(MgAl<sub>5</sub>)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>OH
| 1987-057
| Fer
|-
| [[Fluor-buergerite]]
| NaFe<sup>3+</sup><sub>3</sub>Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>O<sub>3</sub>F
| 1965-005
| Fbu
|-
| [[Fluor-dravite]]
| NaMg<sub>3</sub>Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>F
| 2009-089
| Fdrv
|-
| [[Fluor-elbaite]]
| Na(Li<sub>1.5</sub>,Al<sub>1.5</sub>)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>F
| 2011-071
| Felb
|-
| [[Fluor-liddicoatite]]
| Ca(Li<sub>2</sub>,Al)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>F
| 1976-041
| Fld
|-
| [[Fluor-rossmanite]]
| ▢(LiAl<sub>2</sub>)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>F
| 2023-111
| Frsm
|-
| [[Fluor-schorl]]
| NaFe<sup>2+</sup><sub>3</sub>Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>F
| 2010-067
| Fsrl
|-
| [[Fluor-tsilaisite]]
| NaMn<sup>2+</sup><sub>3</sub>Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>F
| 2012-044
| Ftl
|-
| [[Fluor-uvite]]
| CaMg<sub>3</sub>(Al<sub>5</sub>Mg)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>F
| - 1930 -
| Fluvt
|-
| [[Foitite]]
| ▢(Fe<sup>2+</sup><sub>2</sub>Al)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>OH
| 1992-034
| Foi
|-
| [[Liddicoatite]]
| Ca(Li<sub>2</sub>,Al)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>(OH)
| 2025-047
| Ld
|-
| [[Lucchesiite]]
| Ca(Fe<sup>2+</sup>)<sub>3</sub>Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2015-043
| Lcc
|-
| [[Magnesio-dutrowite]]
| Na(Mg<sub>2.5</sub>Ti<sub>0.5</sub>)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2023-015
| Mdtw
|-
| [[Magnesio-foitite]]
| ▢(Mg<sub>2</sub>Al)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>OH
| 1998-037
| Mfoi
|-
| [[Magnesio-lucchesite]]
| Ca(Mg<sub>3</sub>Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2019-025
| Mlcc
|-
| [[Maruyamaite]]
| K(MgAl<sub>2</sub>)(Al<sub>5</sub>Mg)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2013-123
| Mry
|-
| [[#Chemical composition|Olenite]]
| NaAl<sub>3</sub>Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>O<sub>3</sub>OH
| 1985-006
| Ole
|-
| [[Oxy-chromium-dravite]]
| NaCr<sub>3</sub>(Mg<sub>2</sub>Cr<sub>4</sub>)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2011-097
| Ocdrv
|-
| [[Oxy-dravite]]
| Na(Al<sub>2</sub>Mg)(Al<sub>5</sub>Mg)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2012-004
| Odrv
|-
| [[Oxy-foitite]]
| ▢(Fe<sup>2+</sup>Al<sub>2</sub>)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2016-069
| Ofoi
|-
| [[Oxy-schorl]]
| Na(Fe<sup>2+</sup><sub>2</sub>Al)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2011-011
| Osrl
|-
| [[Oxy-vanadium-dravite]]
| NaV<sub>3</sub>(V<sub>4</sub>Mg<sub>2</sub>)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 1999-050
| Ovdrv
|-
| [[Povondraite]]
| NaFe<sup>3+</sup><sub>3</sub>(Fe<sup>3+</sup><sub>4</sub>Mg<sub>2</sub>)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 1979
| Pov
|-
| [[Princivalleite]]
| Na(Mn<sub>2</sub>Al)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2020-056
| Pva
|-
| [[Rossmanite]]
| ▢(LiAl<sub>2</sub>)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>OH
| 1996-018
| Rsm
|-
| [[Schorl]]
| NaFe<sup>2+</sup><sub>3</sub>Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>OH
| - 1505 -
| Srl
|-
| [[Tsilaisite]]
| NaMn<sup>2+</sup><sub>3</sub>Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>OH
| 2011-047
| Tsl
|-
| [[Uvite]]
| CaMg<sub>3</sub>(Al<sub>5</sub>Mg)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>OH
| 2000-030
| Uvt
|-
| [[Vanadio-oxy-chromium-dravite]]
| NaV<sub>3</sub>(Cr<sub>4</sub>Mg<sub>2</sub>)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2012-034
| Vocdrv
|-
| [[Vanadio-oxy-dravite]]
| NaV<sub>3</sub>(Al<sub>4</sub>Mg<sub>2</sub>)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2012-074
| Vodrv
|}
[[File:Sad-face.png|thumb]]
3vmcxwljkg0big5evqwap8mxkw9j124
739905
739904
2026-05-01T02:04:08Z
Cryptocurrency777
73698
739905
wikitext
text/x-wiki
'''Tourmaline''' is a group of silicate mineral that comes in many colors, but is mainly known for the watermelon color variant. Dravite, also called brown tourmaline, is a sodium magnesium variant. Some variants are magnetic.
{{Infobox musical artist|Name=Tourmaline|image=File:Tourmaline-121240.jpg}}
{| class="wikitable"
|+ The 42 minerals in the group (endmember formulas) recognized by the [[International Mineralogical Association]]
|-
! Species Name
! Ideal Endmember Formula
! IMA Number
! Symbol
|-
| [[Adachiite]]
| CaFe<sup>2+</sup><sub>3</sub>Al<sub>6</sub>(Si<sub>5</sub>AlO<sub>18</sub>)(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>OH
| 2012-101
| Adc
|-
| [[Alumino-oxy-rossmanite]]
| ▢Al<sub>3</sub>Al<sub>6</sub>(Si<sub>5</sub>AlO<sub>18</sub>)(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2020-008
| Aorsm
|-
| [[Bosiite]]
| NaFe<sup>3+</sup><sub>3</sub>(Al<sub>4</sub>Mg<sub>2</sub>)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2014-094
| Bos
|-
| [[Celleriite]]
| ▢(Mn<sup>2+</sup><sub>2</sub>Al)Al<sub>6</sub>(Si<sub>6</sub>O<sub>18</sub>)(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>(OH)
| 2019-089
| Cll
|-
| [[Chromium-dravite]]
| NaMg<sub>3</sub>Cr<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>OH
| 1982-055
| Cdrv
|-
| [[Chromo-alumino-povondraite]]
| NaCr<sub>3</sub>(Al<sub>4</sub>Mg<sub>2</sub>)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2013-089
| Capov
|-
| [[Darrellhenryite]]
| NaLiAl<sub>2</sub>Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2012-026
| Dhry
|-
| [[#Chemical composition|Dravite]]
| NaMg<sub>3</sub>Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>OH
| - 1884 -
| Drv
|-
| [[Dutrowite]]
| Na(Fe<sub>2.5</sub>Ti<sub>0.5</sub>)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2019-082
| Dtw
|-
| [[Elbaite]]
|Na(Li<sub>1.5</sub>,Al<sub>1.5</sub>)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>OH
| - 1913 -
| Elb
|-
| [[Ertlite]]
|NaAl<sub>3</sub>Al<sub>6</sub>(Si<sub>4</sub>B<sub>2</sub>O<sub>18</sub>)(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2023-086
| Etl
|-
| [[Ferro-bosiite]]
| NaFe<sup>3+</sup><sub>3</sub>(Al<sub>4</sub>Fe<sup>2+</sup><sub>2</sub>)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2022-069
| Fbos
|-
| [[Feruvite]]
| CaFe<sup>2+</sup><sub>3</sub>(MgAl<sub>5</sub>)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>OH
| 1987-057
| Fer
|-
| [[Fluor-buergerite]]
| NaFe<sup>3+</sup><sub>3</sub>Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>O<sub>3</sub>F
| 1965-005
| Fbu
|-
| [[Fluor-dravite]]
| NaMg<sub>3</sub>Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>F
| 2009-089
| Fdrv
|-
| [[Fluor-elbaite]]
| Na(Li<sub>1.5</sub>,Al<sub>1.5</sub>)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>F
| 2011-071
| Felb
|-
| [[Fluor-liddicoatite]]
| Ca(Li<sub>2</sub>,Al)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>F
| 1976-041
| Fld
|-
| [[Fluor-rossmanite]]
| ▢(LiAl<sub>2</sub>)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>F
| 2023-111
| Frsm
|-
| [[Fluor-schorl]]
| NaFe<sup>2+</sup><sub>3</sub>Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>F
| 2010-067
| Fsrl
|-
| [[Fluor-tsilaisite]]
| NaMn<sup>2+</sup><sub>3</sub>Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>F
| 2012-044
| Ftl
|-
| [[Fluor-uvite]]
| CaMg<sub>3</sub>(Al<sub>5</sub>Mg)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>F
| - 1930 -
| Fluvt
|-
| [[Foitite]]
| ▢(Fe<sup>2+</sup><sub>2</sub>Al)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>OH
| 1992-034
| Foi
|-
| [[Liddicoatite]]
| Ca(Li<sub>2</sub>,Al)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>(OH)
| 2025-047
| Ld
|-
| [[Lucchesiite]]
| Ca(Fe<sup>2+</sup>)<sub>3</sub>Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2015-043
| Lcc
|-
| [[Magnesio-dutrowite]]
| Na(Mg<sub>2.5</sub>Ti<sub>0.5</sub>)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2023-015
| Mdtw
|-
| [[Magnesio-foitite]]
| ▢(Mg<sub>2</sub>Al)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>OH
| 1998-037
| Mfoi
|-
| [[Magnesio-lucchesite]]
| Ca(Mg<sub>3</sub>Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2019-025
| Mlcc
|-
| [[Maruyamaite]]
| K(MgAl<sub>2</sub>)(Al<sub>5</sub>Mg)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2013-123
| Mry
|-
| [[#Chemical composition|Olenite]]
| NaAl<sub>3</sub>Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>O<sub>3</sub>OH
| 1985-006
| Ole
|-
| [[Oxy-chromium-dravite]]
| NaCr<sub>3</sub>(Mg<sub>2</sub>Cr<sub>4</sub>)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2011-097
| Ocdrv
|-
| [[Oxy-dravite]]
| Na(Al<sub>2</sub>Mg)(Al<sub>5</sub>Mg)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2012-004
| Odrv
|-
| [[Oxy-foitite]]
| ▢(Fe<sup>2+</sup>Al<sub>2</sub>)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2016-069
| Ofoi
|-
| [[Oxy-schorl]]
| Na(Fe<sup>2+</sup><sub>2</sub>Al)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2011-011
| Osrl
|-
| [[Oxy-vanadium-dravite]]
| NaV<sub>3</sub>(V<sub>4</sub>Mg<sub>2</sub>)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 1999-050
| Ovdrv
|-
| [[Povondraite]]
| NaFe<sup>3+</sup><sub>3</sub>(Fe<sup>3+</sup><sub>4</sub>Mg<sub>2</sub>)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 1979
| Pov
|-
| [[Princivalleite]]
| Na(Mn<sub>2</sub>Al)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2020-056
| Pva
|-
| [[Rossmanite]]
| ▢(LiAl<sub>2</sub>)Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>OH
| 1996-018
| Rsm
|-
| [[Schorl]]
| NaFe<sup>2+</sup><sub>3</sub>Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>OH
| - 1505 -
| Srl
|-
| [[Tsilaisite]]
| NaMn<sup>2+</sup><sub>3</sub>Al<sub>6</sub>Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>OH
| 2011-047
| Tsl
|-
| [[Uvite]]
| CaMg<sub>3</sub>(Al<sub>5</sub>Mg)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>OH
| 2000-030
| Uvt
|-
| [[Vanadio-oxy-chromium-dravite]]
| NaV<sub>3</sub>(Cr<sub>4</sub>Mg<sub>2</sub>)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2012-034
| Vocdrv
|-
| [[Vanadio-oxy-dravite]]
| NaV<sub>3</sub>(Al<sub>4</sub>Mg<sub>2</sub>)Si<sub>6</sub>O<sub>18</sub>(BO<sub>3</sub>)<sub>3</sub>(OH)<sub>3</sub>O
| 2012-074
| Vodrv
|}
[[File:Sad-face.png|thumb]]
[[Category:Rocks]]
09i0z4hojbgtlawsfwkwq7b9hoeiou3
Watermelon Rock
0
175074
739906
2026-05-01T02:05:29Z
Cryptocurrency777
73698
Redirected page to [[Tourmaline]]
739906
wikitext
text/x-wiki
#REDIRECT [[Tourmaline]]
1w7rl86polkcutj0lptukbm4z89nvqk
Википедия:Обсуждение категорий/Май 2026
0
175075
739920
2026-05-01T06:48:45Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: добавление обсуждения для категорий [[:Category:Test1]], [[:Category:Test2]] и [[:Category:Test3]]
739920
wikitext
text/x-wiki
{{ОБК-Навигация}}
== 1 мая 2026 ==
=== Test ===
==== [[:Category:Test1]] ====
test1 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 06:48, 1 May 2026 (UTC)
==== [[:Category:Test2]] ====
test2 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 06:48, 1 May 2026 (UTC)
==== [[:Category:Test3]] ====
==== По всем ====
test0 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 06:48, 1 May 2026 (UTC)
6p4g1xf0ctmkhtc2jikla0jfhynt1f3
739921
739920
2026-05-01T06:49:56Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: добавление обсуждения для категорий [[:Category:Test1]], [[:Category:Test2]] и [[:Category:Test3]]
739921
wikitext
text/x-wiki
{{ОБК-Навигация}}
== 1 мая 2026 ==
=== Test ===
==== [[:Category:Test1]] ====
test1 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 06:48, 1 May 2026 (UTC)
==== [[:Category:Test2]] ====
test2 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 06:48, 1 May 2026 (UTC)
==== [[:Category:Test3]] ====
==== По всем ====
test0 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 06:48, 1 May 2026 (UTC)
=== Test2 ===
* [[:Category:Test1]]
*: new1 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 06:49, 1 May 2026 (UTC)
* [[:Category:Test2]]
* [[:Category:Test3]]
*: new3 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 06:49, 1 May 2026 (UTC)
new4 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 06:49, 1 May 2026 (UTC)
denv90ltzgwncingg8y13cd525fq822
739930
739921
2026-05-01T07:10:01Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: добавление обсуждения для категорий [[:Category:Test1]] и [[:Category:Test2]]
739930
wikitext
text/x-wiki
{{ОБК-Навигация}}
== 1 мая 2026 ==
=== Test ===
==== [[:Category:Test1]] ====
test1 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 06:48, 1 May 2026 (UTC)
==== [[:Category:Test2]] ====
test2 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 06:48, 1 May 2026 (UTC)
==== [[:Category:Test3]] ====
==== По всем ====
test0 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 06:48, 1 May 2026 (UTC)
=== Test2 ===
* [[:Category:Test1]]
*: new1 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 06:49, 1 May 2026 (UTC)
* [[:Category:Test2]]
* [[:Category:Test3]]
*: new3 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 06:49, 1 May 2026 (UTC)
new4 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 06:49, 1 May 2026 (UTC)
=== Test3 ===
==== [[:Category:Test1]] ====
test1 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 07:10, 1 May 2026 (UTC)
==== [[:Category:Test2]] ====
==== По всем ====
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 07:10, 1 May 2026 (UTC)
jlz9zdej59usrz0p109lsa2gb20wlud
Category talk:Test2
15
175076
739925
2026-05-01T07:09:30Z
Solidest
54422
[[Участник:Solidest/Remover|Remover]]: добавление шаблона [[ш:Обсуждавшаяся категория]] с датой 2026-05-01
739925
wikitext
text/x-wiki
{{Обсуждавшаяся категория|2026-05-01}}
qd9vgrlnswt4qn7q4ghsdrbjnz7urpw
Melo
0
175077
739951
2026-05-01T10:11:41Z
GiovanniPen
65549
Created page with "asd"
739951
wikitext
text/x-wiki
asd
s5op7ycvjd36f7zhqh7velbpukh6l5c