Jump to content

User:Nardog/MoveHistory-core.js

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Nardog (talk | contribs) at 08:21, 25 March 2023 (copy interwikis correctly). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
(function moveHistoryCore() {
	if (window.moveHistoryDialog) {
		window.moveHistoryDialog.open();
		return;
	}
	mw.loader.addStyleTag('.movehistory .oo-ui-window-body{padding:0 1em 1em} .movehistory .wikitable{margin-bottom:0;width:100%}');
	mw.loader.using([
		'mediawiki.api', 'mediawiki.util', 'mediawiki.Title', 'oojs-ui-windows',
		'oojs-ui-widgets', 'mediawiki.widgets', 'mediawiki.widgets.DateInputWidget',
		'oojs-ui.styles.icons-interactions', 'mediawiki.interface.helpers.styles',
		'jquery.tablesorter'
	], () => {
		let dialog;
		let nowiki = s => s.replace(
			/["&'<=>\[\]{|}]|:(?=\/\/)|_(?=_)|~(?=~~)/g,
			m => '&#' + m.codePointAt(0) + ';'
		);
		let articlePath = mw.config.get('wgArticlePath').replace(/\$1.*/, '');
		let getPath = (pathname, search, hash) => {
			let s = '';
			if (pathname && pathname.startsWith(articlePath)) {
				s = decodeURIComponent(pathname.slice(articlePath.length));
			} else if (search) {
				let title = mw.util.getParamValue('title', search);
				if (title) s = title;
			}
			if (hash) s += mw.util.percentDecodeFragment(hash);
			return s.replace(/_/g, ' ');
		};
		let api = new mw.Api({
			ajax: { headers: { 'Api-User-Agent': 'MoveHistory (https://en.wikipedia.org/wiki/User:Nardog/MoveHistory)' } }
		});
		let arrow = document.dir === 'rtl' ? ' ← ' : ' → ';
		class MoveHistorySearch {
			constructor(page, dir, since, until) {
				this.$status = $('<p>');
				this.$trail = $('<p>');
				this.$tbody = $('<tbody>');
				this.$table = $('<table>').addClass('wikitable').append(
					$('<tr>').append(
						$('<th>').text('Date'),
						$('<th>').text('From'),
						$('<th>').text('To'),
						$('<th>').text('Performer'),
						$('<th>').text('Comment')
					).wrap('<thead>').parent(),
					this.$tbody
				);
				dialog.$results.empty().append(
					this.$status, '<hr>', this.$trail, this.$table
				);
				this.page = page;
				this.ascending = dir === 'newer';
				let sinceTs = (since || '2005-06-25') + 'T00:00:00Z';
				let untilTs;
				if (until) untilTs = until + 'T23:59:59Z';
				this.params = {
					action: 'query',
					titles: page,
					prop: 'revisions',
					rvstart: this.ascending ? sinceTs : untilTs,
					rvend: this.ascending ? untilTs : sinceTs,
					rvdir: dir,
					rvprop: 'sha1|timestamp|user|comment',
					rvlimit: 'max',
					formatversion: 2
				};
				this.revCount = 0;
				this.candidates = [];
				this.titles = {};
				this.noRedirLinks = new WeakSet();
				this.moves = [];
				this.i = 0;
				this.loadRevs();
			}
			setBusy(busy) {
				MoveHistoryDialog.static.escapable = !busy;
				let actions = dialog.actions.get();
				actions[2].setDisabled(busy);
				actions[3].setDisabled(this.complete || busy);
				actions[4].toggle(!(busy || !this.moves.length));
			}
			setStatus(text) {
				this.$status.text(text);
				dialog.updateSize();
			}
			loadRevs() {
				if (this.complete) {
					this.loadMoves();
					return;
				}
				this.setBusy(true);
				this.hasNew = false;
				if (this.$sortable) {
					this.$sortable.replaceWith(this.$table);
					this.$sortable = null;
				}
				this.setStatus(`Loading history${this.revCount ? (this.ascending ? ' after ' + this.lastDate : ' before ' + this.firstDate) : ''}...`);
				api.get(this.params).always((response, errorObj) => {
					let errorMsg = ((errorObj || {}).error || {}).info;
					if (!response || typeof response === 'string' || errorMsg) {
						this.setStatus('Error retrieving revisions' + (errorMsg ? ': ' + errorMsg : ''));
						this.finish(true);
						return;
					}
					let revs = ((((response || {}).query || {}).pages || [])[0] || {}).revisions;
					if (revs) this.processRevs(revs);
					this.params.rvcontinue = ((response || {}).continue || {}).rvcontinue;
					if (!this.params.rvcontinue) {
						this.complete = response.batchcomplete;
					}
					this.loadMoves();
				});
			}
			processRevs(revs) {
				this.revCount += revs.length;
				if (!this.ascending) revs.reverse();
				revs.forEach(rev => {
					let comp = this.lastRev;
					this.lastRev = rev;
					if (!rev.comment || !rev.user || !rev.sha1 ||
						!comp || comp.sha1 !== rev.sha1
					) {
						return;
					}
					let matches = rev.comment.match(/\[\[:?([^\]]+)\]\].+?\[\[:?([^\]]+)\]\]/);
					if (matches) rev.matches = matches.slice(1);
					this.candidates.push(rev);
				});
				if (!this.ascending || !this.firstDate) {
					this.firstDate = revs[0].timestamp;
				}
				if (this.ascending || !this.lastDate) {
					this.lastDate = this.lastRev.timestamp;
				}
			}
			loadMoves() {
				let rev = this.candidates[this.ascending ? 'shift' : 'pop']();
				if (!rev) {
					this.finish();
					return;
				}
				this.setStatus(`Seeing if there was a move at ${rev.timestamp}...`);
				let date = Date.parse(rev.timestamp) / 1000;
				api.get({
					action: 'query',
					list: 'logevents',
					letype: 'move',
					lestart: date + 60,
					leend: date,
					leprop: 'details|title|user|parsedcomment',
					lelimit: 'max',
					formatversion: 2
				}).always((response, errorObj) => {
					let errorMsg = ((errorObj || {}).error || {}).info;
					if (!response || typeof response === 'string' || errorMsg) {
						this.setStatus('Error retrieving moves' + (errorMsg ? ': ' + errorMsg : ''));
						this.finish(true);
						return;
					}
					(((response || {}).query || {}).logevents || []).reverse().some(le => {
						if (le.user !== rev.user || !rev.comment.includes(le.title)) return;
						let target = ((le || {}).params || {}).target_title;
						if (!target || !rev.comment.includes(target) ||
							rev.matches &&
							[le.title, target].some(s => !rev.matches.includes(s))
						) {
							return;
						}
						this.addRow({
							date: rev.timestamp,
							offset: new Date(Date.parse(rev.timestamp) + 1000)
								.toISOString().slice(0, -5).replace(/\D/g, ''),
							from: le.title,
							to: target,
							user: le.user,
							comment: $.parseHTML(le.parsedcomment)
						});
						return true;
					});
					this.loadMoves();
				});
			}
			addRow(move) {
				if (!this.moves.length) {
					this.lastName = this.ascending ? move.from : move.to;
					this.$trail.append(this.makeLink(this.lastName));
				}
				if (this.ascending) {
					if (this.lastName !== move.from) {
						this.$trail.append(arrow + '?' + arrow, this.makeLink(move.from));
					}
					this.$trail.append(arrow, this.makeLink(move.to));
					this.lastName = move.to;
				} else {
					if (this.lastName !== move.to) {
						this.$trail.prepend(this.makeLink(move.to), arrow + '?' + arrow);
					}
					this.$trail.prepend(this.makeLink(move.from), arrow);
					this.lastName = move.from;
				}
				$('<tr>').append(
					$('<td>').append(
						$('<a>').attr({
							href: mw.util.getUrl(this.page, {
								action: 'history',
								offset: move.offset
							}),
							title: 'See history up to this move',
							target: '_blank'
						}).text(move.date)
					),
					$('<td>').append(this.makeLink(move.from)),
					$('<td>').append(this.makeLink(move.to)),
					$('<td>').append(
						this.makeLink('User:' + move.user, move.user, true),
						' ',
						$('<span>').addClass('mw-changeslist-links').append(
							$('<span>').append(this.makeLink('User talk:' + move.user, 'talk', true)),
							$('<span>').append(this.makeLink('Special:Contributions/' + move.user, 'contribs', true))
						)
					),
					$('<td>').append($(move.comment).clone().attr('target', '_blank'))
				).appendTo(this.$tbody);
				dialog.updateSize();
				this.moves.push(move);
				this.hasNew = true;
			}
			makeLink(title, text, allowRedirect) {
				let obj;
				if (this.titles.hasOwnProperty(title)) {
					obj = this.titles[title];
				} else {
					obj = { links: [] };
					this.titles[title] = obj;
					if (title === this.page) {
						obj.classes = ['mw-selflink', 'selflink'];
						obj.processed = true;
					}
				}
				let params = obj.red && { action: 'edit', redlink: 1 } ||
					!allowRedirect && obj.redirect && { redirect: 'no' };
				let $link = $('<a>').attr({
					href: mw.util.getUrl(obj.canonical || title, params),
					title: obj.canonical || title,
					target: '_blank'
				}).addClass(obj.classes).text(text || title);
				if (!allowRedirect && !obj.processed) {
					this.noRedirLinks.add($link[0]);
				}
				if (!obj.processed) obj.links.push($link[0]);
				return $link;
			}
			finish(isError) {
				let count = this.moves.length;
				if (!isError) {
					this.setStatus(`Found ${count} move${count === 1 ? '' : 's'} in ${this.revCount.toLocaleString()} revisions${this.revCount ? ` from ${this.firstDate} to ${this.lastDate}` : ''}.${this.complete ? '' : ' Click Continue to inspect more revisions.'}`);
				}
				if (!this.complete) {
					if (++this.i >= 4 || this.hasNew || isError) {
						this.i = 0;
					} else {
						this.loadRevs();
						return;
					}
				}
				this.setBusy();
				if (!count) return;
				this.queryTitles(
					Object.entries(this.titles)
						.filter(([k, v]) => !v.processed).map(([k]) => k)
				);
			}
			queryTitles(titles = []) {
				if (!titles.length) {
					this.$sortable = this.$table.clone().addClass('sortable').tablesorter();
					this.$table.replaceWith(this.$sortable);
					dialog.updateSize();
					return;
				}
				let curTitles = titles.slice(0, 50);
				curTitles.forEach(title => {
					this.titles[title].processed = true;
				});
				api.post({
					action: 'query',
					titles: curTitles,
					prop: 'info',
					inprop: 'linkclasses',
					formatversion: 2
				}).always(response => {
					let query = response && response.query;
					if (!query) {
						this.queryTitles();
						return;
					}
					(query.normalized || []).forEach(entry => {
						if (!this.titles.hasOwnProperty(entry.from)) return;
						let obj = this.titles[entry.from];
						obj.canonical = entry.to;
						this.titles[entry.to] = obj;
					});
					(query.pages || []).forEach(page => {
						if (!this.titles.hasOwnProperty(page.title)) return;
						let obj = this.titles[page.title];
						let classes = page.linkclasses || [];
						if (page.missing && !page.known) {
							classes.push('new');
							obj.red = true;
						} else if (classes.includes('mw-redirect')) {
							obj.redirect = true;
						}
						if (classes.length) obj.classes = classes;
					});
					curTitles.forEach(title => {
						let obj = this.titles[title];
						let $links = $(obj.links).addClass(obj.classes);
						$links.attr('href', i => mw.util.getUrl(
							obj.canonical || title,
							obj.red && { action: 'edit', redlink: 1 } ||
							obj.redirect && this.noRedirLinks.has($links[i]) &&
							{ redirect: 'no' }
						));
						if (obj.canonical) $links.attr('title', obj.canonical);
						delete obj.links;
					});
					dialog.updateSize();
					this.queryTitles(titles.slice(50));
				});
			}
			copyResults() {
				let text = this.$trail.contents().get().map(n => (
					n.tagName === 'A' ? `[[:${n.textContent}]]` : n.textContent
				)).join('') + `
{| class="wikitable sortable plainlinks"
! Date
! From
! To
! Performer
! Comment
${this.moves.map(move => `|-
| [{{fullurl:${this.page}|action=history&offset=${move.offset}}} ${move.date}]
| ${this.titles[move.from] && this.titles[move.from].redirect ? `[{{fullurl:${move.from}|redirect=no}} ${move.from}]` : `[[:${move.from}]]`}
| ${this.titles[move.to] && this.titles[move.to].redirect ? `[{{fullurl:${move.to}|redirect=no}} ${move.to}]` : `[[:${move.to}]]`}
| [[User:${move.user}|${move.user}]] ([[User talk:${move.user}|talk]] &#124; [[Special:Contributions/${move.user}|contribs]])
|${move.comment.length ? ' ' + move.comment.map(n => (
	n.tagName === 'A'
		? n.classList.contains('extiw')
			? `[[${n.title}|${nowiki(n.textContent)}]]`
			: `[[:${getPath(n.pathname, n.search, n.hash)}|${nowiki(n.textContent)}]]`
		: nowiki(n.textContent)
)).join('') : ''}
`).join('')}|}`;
				navigator.clipboard.writeText(text).then(() => {
					mw.notify('Copied');
				}, () => {
					let $textarea = $('<textarea>').attr({
						readonly: '',
						style: 'position:fixed;top:-100%'
					}).val(text).appendTo(document.body);
					$textarea[0].select();
					document.execCommand('copy');
					$textarea.remove();
					mw.notify('Probably copied');
				});
			}
		}
		function MoveHistoryDialog(config) {
			MoveHistoryDialog.parent.call(this, config);
			this.$element.addClass('movehistory');
		}
		OO.inheritClass(MoveHistoryDialog, OO.ui.ProcessDialog);
		MoveHistoryDialog.static.name = 'moveHistoryDialog';
		MoveHistoryDialog.static.title = 'Move history';
		MoveHistoryDialog.static.size = 'small';
		MoveHistoryDialog.static.actions = [
			{
				modes: 'config',
				flags: ['safe', 'close']
			},
			{
				action: 'search',
				label: 'Search',
				modes: 'config',
				flags: ['primary', 'progressive'],
				disabled: true
			},
			{
				action: 'goBack',
				modes: 'results',
				flags: ['safe', 'back']
			},
			{
				action: 'continue',
				label: 'Continue',
				modes: 'results',
				flags: ['primary', 'progressive'],
				disabled: true
			},
			{
				action: 'copy',
				label: 'Copy results as wikitext'
			}
		];
		MoveHistoryDialog.prototype.initialize = function () {
			MoveHistoryDialog.parent.prototype.initialize.apply(this, arguments);
			let updateButton = () => {
				let invalid = ['pageInput', 'sinceInput', 'untilInput']
					.some(n => this[n].hasFlag('invalid'));
				this.actions.get()[1].setDisabled(invalid);
			};
			this.pageInput = new mw.widgets.TitleInputWidget({
				$overlay: this.$overlay,
				api: api,
				excludeDynamicNamespaces: true,
				required: true,
				showMissing: false
			}).on('flag', updateButton);
			let rt = mw.Title.newFromText(mw.config.get('wgRelevantPageName'));
			if (rt && rt.namespace >= 0) {
				this.pageInput.setValue(rt.toText());
			}
			this.directionInput = new OO.ui.RadioSelectInputWidget({
				options: [
					{ data: 'newer', label: 'Oldest first' },
					{ data: 'older', label: 'Newest first' }
				]
			});
			this.sinceInput = new mw.widgets.DateInputWidget({
				$overlay: this.$overlay,
				displayFormat: 'YYYY-MM-DD'
			}).on('flag', updateButton);
			this.untilInput = new mw.widgets.DateInputWidget({
				$overlay: this.$overlay,
				displayFormat: 'YYYY-MM-DD',
				mustBeAfter: '2005-06-24'
			}).on('change', () => {
				let m = this.untilInput.getMoment();
				this.sinceInput.mustBeBefore = m.isValid()
					? m.add(1, 'days')
					: null;
				this.sinceInput.emit('change');
			}).on('flag', updateButton);
			this.form = new OO.ui.FormLayout({
				items: [
					new OO.ui.FieldLayout(this.pageInput, {
						label: 'Page:',
						align: 'top'
					}),
					new OO.ui.FieldLayout(this.directionInput, {
						label: 'Direction:',
						align: 'top'
					}),
					new OO.ui.FieldLayout(this.sinceInput, {
						label: 'Since:',
						align: 'top'
					}),
					new OO.ui.FieldLayout(this.untilInput, {
						label: 'Until:',
						align: 'top'
					})
				],
				content: [$('<input>').attr({ type: 'submit', hidden: '' })]
			}).on('submit', () => {
				if (!this.actions.get()[1].isDisabled()) {
					this.executeAction('search');
				}
			});
			this.$results = $('<div>');
			this.$body.append(this.form.$element);
		};
		MoveHistoryDialog.prototype.getSetupProcess = function (data) {
			return MoveHistoryDialog.super.prototype.getSetupProcess.call(this, data)
				.next(function () {
					this.pageInput.emit('change');
					this.actions.setMode('config');
				}, this);
		};
		MoveHistoryDialog.prototype.getReadyProcess = function (data) {
			return MoveHistoryDialog.super.prototype.getReadyProcess.call(this, data)
				.next(function () {
					this.pageInput.focus();
				}, this);
		};
		MoveHistoryDialog.prototype.getActionProcess = function (action) {
			if (action === 'search') {
				this.actions.setMode('results');
				let config = [
					this.pageInput.getValue(),
					this.directionInput.getValue(),
					this.sinceInput.getValue(),
					this.untilInput.getValue()
				];
				if (!this.config || config.some((v, i) => v !== this.config[i])) {
					this.config = config;
					this.search = new MoveHistorySearch(...config);
				} else {
					this.actions.get()[3].setDisabled(this.search.complete);
					this.actions.get()[4].toggle(!!this.search.moves.length);
				}
				this.form.toggle(false).$element.after(this.$results);
				this.setSize('larger');
			} else if (action === 'continue') {
				this.search.loadRevs();
			} else if (action === 'copy') {
				this.search.copyResults();
			} else {
				this.actions.setMode('config');
				this.$results.detach();
				this.form.toggle(true);
				this.setSize('small');
			}
			return MoveHistoryDialog.super.prototype.getActionProcess.call(this, action);
		};
		dialog = new MoveHistoryDialog();
		window.moveHistoryDialog = dialog;
		let winMan = new OO.ui.WindowManager();
		winMan.addWindows([dialog]);
		winMan.$element.appendTo(document.body);
		dialog.open();
	});
})();