User:NguoiDungKhongDinhDanh/QuickDiff.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)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: 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 OneTwoThreeFall@dev.fandom.com.
**/
/* 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);