User:Ponor/inline-diff-inline-patrol.js
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/*
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() {
"use strict";
//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";
const WL = mw.config.get("wgCanonicalSpecialPageName") == "Watchlist";
const PH = mw.config.get("wgAction") == "history"; //Page History
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
diff_button: "diff",
diff_button_hint: "show diff",
diff_type: "table", //inline, table
max_diff_height: "40em",
}; //conf
//------------------------------------ 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 (patroller || sysop) {
$("li[data-mw-revid].mw-changeslist-reviewstatus-unpatrolled").each(patrolButton);
}
$("li[data-mw-revid]").each(diffButton);
} //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();
}
});
}
//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)
.on("click", diffClick);
//button.insertAfter($(this).find("span.mw-changeslist-separator").eq(0));
//button.prependTo($(this));
if (RC || WL) {
//button.prependTo($("span.mw-changeslist-line-inner", $(this)));
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) {
//console.log(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);
//button.insertAfter($(this).find("span.mw-changeslist-separator").eq(0));
//button.insertAfter($(this).find(".mw-changeslist-date").eq(0));
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_patrolled
//------------------------------------ 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) => { //arrow function does not create its context, "this" is from outer context
mw.notify(conf.patrol_successful + revid, {'type':'success'});
button
.removeClass("ip-unpatrolled").addClass("ip-patrolled")
.off(); //no clicks
});
mwPromise.fail((data) => { //arrow function does not create its context, "this" is from outer context
mw.notify(conf.patrol_error + revid, {'type':'error'});
button
.removeClass("ip-unpatrolled").addClass("ip-error")
.off(); //no clicks
});
event.stopPropagation();
} //function patrolClick
//[diff] button click handler
function diffClick(event) {
let button = $(this);
let parent = button.parent();
if (button.data("status") == "shown") {
parent.children("div.ip-diff-div").eq(0).hide();
button.text(conf.diff_button + "↓")
.data("status", "hidden");
return;
} else if (button.data("status") == "hidden") {
parent.children("div.ip-diff-div").eq(0).show();
button.text(conf.diff_button + "↑")
.data("status", "shown");
return;
}
function diff_promise_done(data) {
let diff_data = data.compare["*"];
//linkify
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;
} //proper_nesting
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>';}
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('<', '¦<') //<ref comes as <ref in diffs, use ¦ as stop char
.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/>');
diff_div
.addClass("ip-diff-div")
.css({"max-height": conf.max_diff_height, "overflow-y": "auto"})
.html(diff_table)
.on("dblclick", function() {
$(this).hide();
button.data("status", "hidden");
});
parent.append(diff_div);
button.data("status", "shown")
.text(conf.diff_button + "↑");
} //diff_promise_done
const revid = button.data().revid;
const mwPromise = get_diff(revid);
mwPromise.done(diff_promise_done);
event.stopPropagation();
}
on_ready();
});