User:NguoiDungKhongDinhDanh/QuickDiff.js
Appearance
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.
/**
* QuickDiff - quickly view any diff link.
* Fandom article: /dev.fandom.com/wiki/QuickDiff
*
* For attribution: [[:wikia:dev:MediaWiki:QuickDiff/code.js]]
* Originally written by [email protected].
**/
/* jshint maxerr: 9999, undef: true, unused: true, quotmark: single */
/* globals jQuery, mediaWiki, window, document, localStorage, location */
(function($, mw) {
'use strict';
/ double-run protection
if (window.quickDiffLoaded) {
return;
}
window.quickDiffLoaded = true;
var lang = mw.config.get('wgUserLanguage');
var i18n = function() {
var msgname = arguments[0];
var msgs = window.quickDiffi18n;
if (lang === 'qqx' || lang === 'version') return '\u29FC' + msgname + '\u29FD';
var msg = msgs[lang !== 'qqq' ? lang : 'en'] && msgs[lang][msgname] || msgs.en[msgname];
for (let i = 1; i < arguments.length; i++) {
msg = msg.replace(
new RegExp('\\$' + i, 'g'),
arguments[i]
);
}
return msg;
};
var normalize = function(string) {
return string ? string.replace(/_/g, ' ').toLowerCase() : '';
};
var modal;
var special = {};
function isElementOrChildFrontmost(element) {
var pos = element.getBoundingClientRect();
var frontmostElement = document.elementFromPoint(pos.left, pos.top);
return element.contains(frontmostElement);
}
/ "Special:Diff/12345" and "Special:ComparePages" link detection
function initSpecialPageStrings() {
special.diffDefault = mw.util.getUrl('Special:Diff/');
special.compareDefault = mw.util.getUrl('Special:ComparePages');
var wiki = mw.config.get('wgDBname');
var storageKeyDiff = 'QuickDiff-specialdiff_' + wiki;
var storageKeyCompare = 'QuickDiff-specialcompare_' + wiki;
var storageKeyDiffText = 'QuickDiff-specialdiff-text_' + wiki;
var storageKeyCompareText = 'QuickDiff-specialcompare-text_' + wiki;
try {
special.diff = localStorage.getItem(storageKeyDiff);
special.compare = localStorage.getItem(storageKeyCompare);
special.diffText = localStorage.getItem(storageKeyDiffText);
special.compareText = localStorage.getItem(storageKeyCompareText);
} catch (ignore) {}
if (special.diff && special.compare && special.diffText && special.compareText) {
/ using stored values - no need for api request
return;
}
$.getJSON(mw.util.wikiScript('api'), {
action: 'parse',
text: '<span class="diff">[[{{#special:Diff/}}]]</span><span class="compare">[[{{#special:ComparePages}}]]</span>',
prop: 'text',
disablelimitreport: true,
contentmodel: 'wikitext',
format: 'json'
}).done(function(data) {
var $parsed = $(data.parse.text['*']);
special.diff = $parsed.find('.diff > a').attr('href');
special.compare = $parsed.find('.compare > a').attr('href');
special.diffText = $parsed.find('.diff > a').text().slice(0, -1);
special.compareText = $parsed.find('.compare > a').text();
try {
localStorage.setItem(storageKeyDiff, special.diff);
localStorage.setItem(storageKeyCompare, special.compare);
localStorage.setItem(storageKeyDiffText, special.diffText);
localStorage.setItem(storageKeyCompareText, special.compareText);
} catch (ignore) {}
});
}
/ support for patrolling edits directly from modal
/ ideally this wouldn't be needed and we'd rely on MediaWiki's own handler,
/ but that's run only on document ready and isn't easily reusable
function initAjaxPatrolHandler() {
var $spinner = mw.libs.QDmodal.getSpinner();
$spinner.css({
'--qdmodal-spinner-size': '1em',
verticalAlign: 'middle'
});
mw.hook('quickdiff.ready').add(function(modal) {
var links = {};
var type = ['patrol', 'rollback', 'thank', 'undo'];
var selector = [
'.patrollink[data-mw="interface"] > a',
'.mw-rollback-link > a',
'.mw-thanks-thank-link',
'.mw-diff-undo > a'
];
for (let i = 0; i < type.length; i++) {
links[type[i]] = modal.$element.find(selector[i]);
}
type.forEach(function(element) {
links[element].on('click', function(event) {
event.preventDefault();
if ($(this).is('[disabled]')) {
return;
}
var es;
switch (element) {
case 'rollback':
case 'undo':
try {
es = window.prompt('Edit summary (optional):').trim();
} catch (e) {
return;
}
break;
case 'thank':
if (!window.confirm('Are you sure you want to thank this edit?')) {
return;
}
break;
}
links[element].find('.qdmodal-spinner-container').remove().end()
.attr('disabled', '').append(' ', $spinner.clone());
var $spinners = links[element].find('.qdmodal-spinner-container');
var p = function(para) {
return mw.util.getParamValue(para, event.target.href);
};
var l = function(r) {
return '[[Special:Diff/' + r + '|' + r + ']]';
};
var token = '';
var param = {};
switch (element) {
case 'patrol':
param = {
action: 'patrol',
rcid: p('rcid')
};
break;
case 'rollback':
param = {
action: 'rollback',
title: p('title'),
user: p('from')
};
if (es) param.summary = es;
break;
case 'thank':
token = 'csrf';
param = {
action: 'thank',
rev: +event.target.getAttribute('data-revision-id'),
source: 'diff'
};
break;
case 'undo':
token = 'csrf';
param = {
action: 'edit',
title: p('title'),
minor: true,
undo: p('undo'),
undoafter: p('undoafter'),
basetimestamp: new Date().toISOString()
};
if (es) {
param.summary = es;
} else if ($('#quickdiff-modal .diff-multi').length) {
param.summary = 'Undo revisions from ' + l(p('undo')) + ' to ' + l(p('undoafter'));
}
break;
}
mw.loader.using('mediawiki.api').done(function () {
(new mw.Api()).postWithToken(!token ? param.action : token, param).done(function() {
$spinners.removeAttr('style').text('✓').parent().wrap('<s>');
}).fail(function(error, response) {
$spinners.attr(
'title', response.error.info
).removeAttr('style').text('✗').parent().removeAttr('disabled');
});
});
});
});
});
}
function getDiffTitle($diff) {
var prevTitle = $diff.find('#mw-diff-otitle1 a').attr('title');
var currTitle = $diff.find('#mw-diff-ntitle1 a').attr('title');
if (prevTitle && prevTitle !== currTitle) {
return i18n('differences-multipage', prevTitle, currTitle);
}
return i18n('differences', currTitle);
}
function addDiffActions() {
var prevTitle = modal.$content.find('#mw-diff-otitle1 a').attr('title');
var currTitle = modal.$content.find('#mw-diff-ntitle1 a').attr('title');
/ collect action links (edit, undo, rollback, patrol) from the diff
var $actions = modal.$content.find('.diff-ntitle').find(
'.mw-diff-edit, .mw-diff-undo, .mw-rollback-link, .patrollink, .mw-diff-tool'
).clone();
/ remove text nodes (the brackets around each link)
$actions.contents().filter(function(ignore, element) {
return element.nodeType === 3;
}).remove();
$actions.find('a')
.addClass('qdmodal-button')
.attr('target', '_blank');
/ if diff is for one page, add a page history action
if (prevTitle === currTitle) {
$actions = $actions.add(
$('<a>').attr({
class: 'qdmodal-button',
href: mw.util.getUrl(currTitle, {action: 'history'}),
target: '_blank'
}).text(i18n('history'))
);
}
modal.$footer.append($actions);
}
function loadDiff(url) {
modal.show({
loading: true,
title: !modal.visible && i18n('loading')
});
/ add 'action=render' and 'diffonly' params to save some bytes on each request
url.extend({
action: 'render',
diffonly: '1'
});
/ pass through 'bot' param for rollback links if it's in use on the current page
if (mw.util.getParamValue('bot')) {
url.extend({
bot: '1'
});
}
$.when(
$.get(url.getRelativePath()),
mw.loader.load(['mediawiki.diff', 'mediawiki.diff.styles']) / [[:phab:T309441]]
).always(function(response) {
delete url.query.action;
delete url.query.diffonly;
delete url.query.bot;
var data = {
url: url,
buttons: [
{
text: i18n('link'),
href: url.toString(),
attr: {
'data-disable-quickdiff': ''
}
}
],
content: mw.html.escape(i18n('error', url.toString()))
};
var $diff;
if (typeof response[0] === 'string') {
var $content = $(response[0]);
$diff = $content.filter('table.diff, #mw-rev-deleted-no-diff');
if (!$diff.length) {
/ $content is a complete page - see if a diff can be found
/ needed for diffs from special pages as they ignore action=render URL parameter
$diff = $content.find('table.diff');
}
}
if (!$diff || $diff.length === 0) {
/ default content is error msg
return modal.show(data);
}
data.content = $diff;
data.hook = 'quickdiff.ready';
data.onBeforeShow = addDiffActions;
data.title = getDiffTitle($diff);
/ if a diff, fire the standard MW hook
if ($diff.is('table.diff[data-mw="interface"]')) {
mw.hook('wikipage.diff').fire($diff);
}
modal.show(data);
});
}
function keydownHandler(event) {
/ only handle key presses if QuickDiff is frontmost
if (!isElementOrChildFrontmost(modal.$container[0])) {
return;
}
if (event.key === 'ArrowLeft') {
modal.$content.find('#differences-prevlink').trigger('click');
} else if (event.key === 'ArrowRight') {
modal.$content.find('#differences-nextlink').trigger('click');
}
}
function linkClickHandler(event) {
/ ignore clicks with modifier keys to avoid overriding browser features
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
return;
}
/ ignore click if link has "data-disable-quickdiff" attribute set
if (event.currentTarget.dataset.disableQuickdiff !== undefined) {
return;
}
var url = event.currentTarget.href;
var loc = location.href;
try {
url = new mw.Uri(url);
loc = new mw.Uri(loc);
} catch (ignore) {
/ quit if url couldn't be parsed
/ it wouldn't be a link QuickDiff could handle anyway
return;
}
/ cross-domain requests not supported
if (url.host !== loc.host) {
return;
}
/ If we're at a diff and clicked on a link with the same href, it is probably a JS one.
if (urlWithoutFragment(url) === urlWithoutFragment(loc)) {
return;
}
/ Ignore safemode links.
if (url.query.safemode) {
return;
}
/ no fragment check is to ensure section links/collapsible trigger links on diff pages are ignored
/ Handled by the third condition of this function.
var hasDiffParam = 'diff' in url.query; / && url.fragment === undefined;
var isSpecialDiffLink = (
url.path.indexOf(special.diff) === 0 ||
url.path.indexOf(special.diffDefault) === 0 ||
normalize(url.query.title).startsWith(normalize(special.diffText))
);
var isSpecialCompareLink = (
url.path.indexOf(special.compare) === 0 ||
url.path.indexOf(special.compareDefault) === 0 ||
normalize(url.query.title) === normalize(special.compareText)
);
if (hasDiffParam || isSpecialDiffLink || isSpecialCompareLink) {
event.preventDefault();
loadDiff(url);
}
}
function urlWithoutFragment(urlobject) {
delete urlobject.fragment;
return urlobject.toString();
}
function init() {
var $body = $(document.body);
modal = new mw.libs.QDmodal('quickdiff-modal');
/ full screen modal
var css = ['#quickdiff-modal {\n\theight: 100%;\n\twidth: 100%;\n}'];
/ always show modal footer for UI consistency
css.push('#quickdiff-modal > footer {\n\tdisplay: flex;\n}');
/ hide square brackets around rollback link in footer
css.push('#quickdiff-modal > footer .mw-rollback-link::before,\n#quickdiff-modal > footer .mw-rollback-link::after {\n\tcontent: none;\n}');
css.push(
`#quickdiff-modal > footer > :is(.mw-diff-edit, .mw-diff-tool, .mw-diff-undo)::before,
#quickdiff-modal > footer > :is(.mw-diff-edit, .mw-diff-tool, .mw-diff-undo)::after {
content: none;
}`
);
/ On the other hand, keep them in the case MediaWiki doesn't load.
css.push('.mw-rollback-link::before {\n\tcontent: \'[\';\n}');
css.push('.mw-rollback-link::after {\n\tcontent: \']\';\n}');
/ Prevent text-decoration
css.push('#quickdiff-modal > footer .qdmodal-button[href]:hover {\n\ttext-decoration: none;\n}');
mw.util.addCSS(css.join('\n'));
/ attach to body for compatibility with ajax-loaded content
/ also, one attached event handler is better than hundreds!
$body.on('click.quickdiff', 'a[href]', linkClickHandler);
/ listen for left/right arrow keys, to move between prev/next diff
$body.on('keydown.quickdiff', keydownHandler);
initSpecialPageStrings();
initAjaxPatrolHandler();
}
function initDependencies() {
var fullurl = function(str) {
var ctype = str.match(/\.js$/) && 'javascript' || (str.match(/\.css$/) && 'css' || '');
return 'https://meta.wikimedia.org/w/index.php?title=' + str + '&action=raw' + (ctype && '&ctype=text/' + ctype || '');
};
var i18nMsgs = new $.Deferred();
var waitFor = [
i18nMsgs,
mw.loader.using(['mediawiki.Uri', 'mediawiki.util'])
];
window.quickDiffi18n = JSON.parse(localStorage.getItem('QuickDiff-i18n'));
if (!(mw.libs.QDmodal && mw.libs.QDmodal.version >= 20201108)) {
waitFor.push(
$.ajax({
url: fullurl('User:NguoiDungKhongDinhDanh/QDmodal.js'),
dataType: 'script',
cache: true
})
);
}
if (
(
!(
window.quickDiffi18n &&
typeof window.quickDiffi18n.version === 'number' &&
window.quickDiffi18n.version <= 1664488998
) || (
window.quickDiffi18n &&
typeof window.quickDiffi18n.version === 'undefined'
)
) && new mw.Uri(location.href).host.match(
/(mediawiki|wik(i([mp]edia|books|quote|source|voyage|news|versity|data)|tionary)).org$/
)
) {
waitFor.push(
(new mw.ForeignApi('https://meta.wikimedia.org/w/api.php')).get({
action: 'query',
prop: ['revisions'],
titles: ['User:NguoiDungKhongDinhDanh/QuickDiff.js/i18n.json'],
rvprop: ['content'],
rvslots: ['main'],
rvlimit: 1,
format: 'json',
formatversion: 2
}).done(function(response) {
window.quickDiffi18n = JSON.parse(response.query.pages[0].revisions[0].slots.main.content);
localStorage.setItem('QuickDiff-i18n', JSON.stringify(window.quickDiffi18n));
i18nMsgs.resolve();
}).fail(function(error, response) {
mw.notify('Cannot initialize QuickDiff. API error: ' + response.error.info, {
type: 'warn',
title: 'QuickDiff: ' + error,
autoHide: true,
autoHideSeconds: 10
});
})
);
} else {
localStorage.setItem('QuickDiff-i18n',
JSON.stringify({
en: {
differences: 'Differences: $1',
'differences-multipage': 'Differences between "$1" and "$2"',
error: 'Something went wrong while getting the page at "$1".',
history: 'history',
link: 'open link',
loading: 'Loading…'
}
})
);
i18nMsgs.resolve();
}
/* mw.hook('dev.i18n').add(function(i18njs) {
i18njs.loadMessages('QuickDiff').done(function(i18nData) {
i18n = i18nData.msg;
i18nMsgs.resolve();
});
}); */
$.when.apply($, waitFor).done(init);
}
initDependencies();
})(jQuery, mediaWiki);