User:Nardog/MoveHistory-core.js

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' ], function moveHistoryCore {	mw.loader.addStyleTag('.movehistory .oo-ui-window-body{padding:1em;display:flex;justify-content:center} .movehistory form{flex-grow:1} .movehistory .wikitable{width:100%;margin-top:0;white-space:nowrap} .movehistory-date, .movehistory-title{text-align:center} .movehistory-title, .movehistory-comment{white-space:normal;word-break:break-word} .movehistory-comment{vertical-align:top;max-width:16em}');	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 = $(' ').appendTo(dialog.$results.empty);			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.start; }		start { this.setBusy(true); dialog.actions.setMode('searching'); this.i = 0; this.paused = false; this.doNext; }		doNext { if (!this.paused && this.candidates.length) { this.loadMoves; } else if (!this.paused && !this.complete && this.i < 4) { this.loadRevs; } else { this.finish; }		}		loadRevs { this.i++; this.setStatus(`Loading history${				this.revCount					? this.ascending						? ' after ' + this.lastDate						: ' before ' + this.firstDate					: ''			}...`); api.get(this.params).always((response, error) => {				let errorMsg = ((error || {}).error || {}).info;				if (!response || typeof response === 'string' || errorMsg) {					this.finish('Error loading revisions' + (errorMsg ? ': ' + errorMsg : ''));					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.doNext;			}); }		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']; 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, error) => {				let errorMsg = ((error || {}).error || {}).info;				if (!response || typeof response === 'string' || errorMsg) {					this.finish('Error loading moves' + (errorMsg ? ': ' + errorMsg : ''));					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.addMove({						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.doNext;			}); }		addMove(move) { if (!this.moves.length) { this.lastName = this.ascending ? move.from : move.to; this.$trail = $(' ').append(this.makeLink(this.lastName)); this.$tbody = $(' '); this.$table = $(' ').addClass('wikitable').append(					$(' ').append( $(' ').append(							$(' ').attr('rowspan', 2).text('Date'),							$(' ').text(this.ascending ? 'From' : 'To'),							$(' ').attr('rowspan', 2).text('Performer'),							$(' ').attr('rowspan', 2).text('Comment')						), $(' ').append(							$(' ').text(this.ascending ? 'To' : 'From')						) ),					this.$tbody				); this.$status.after(' ', this.$trail, this.$table); }			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; }			this.$tbody.append(				$(' ').append( $(' ').attr({ class: 'movehistory-date', rowspan: 2 }).append(						$('').attr({ href: mw.util.getUrl(this.page, {								action: 'history',								offset: move.offset							}), title: 'See history up to this move', target: '_blank' }).append(move.date.slice(0, 10), ' ', move.date.slice(10))					), $(' ').addClass('movehistory-title').append(						this.makeLink(this.ascending ? move.from : move.to)					), $(' ').attr({ class: 'movehistory-user', rowspan: 2 }).append(						this.makeLink('User:' + move.user, move.user, true),						' ',						$(' ').addClass('mw-changeslist-links').append( $(' ').append(this.makeLink('User talk:' + move.user, 'talk', true)), $(' ').append(this.makeLink('Special:Contributions/' + move.user, 'contribs', true)) )					),					$(' ').attr({ class: 'movehistory-comment', rowspan: 2 }).append(						$(move.comment).clone.attr('target', '_blank')					) ),				$(' ').append( $(' ').addClass('movehistory-title').append(						this.makeLink(this.ascending ? move.to : move.from)					) )			);			dialog.setSize('large'); this.moves.push(move); }		finish(error) { let count = this.moves.length; let complete = this.complete && !this.candidates.length; this.setStatus(error || `Found ${count} move${count === 1 ?  : 's'} in ${				this.revCount.toLocaleString('en')			} revisions${				this.revCount ? ` from ${this.firstDate} to ${this.lastDate}` : 			}.${				complete ? '' : ' Click Continue to inspect more revisions.'			}`); this.setBusy; this.mode = complete ? count ? 'found' : 'notFound' : count ? 'pausedFound' : 'paused'; dialog.actions.setMode(this.mode); if (!count) return; this.queryTitles(				Object.entries(this.titles)					.filter(([k, v]) => !v.processed).map(([k]) => k)			); }		setBusy(busy) { MoveHistoryDialog.static.escapable = !busy; dialog.$navigation.toggleClass('oo-ui-pendingElement-pending', !!busy); }		setStatus(text) { this.$status.text(text); dialog.updateSize; console.log(text); }		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 = $('').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; }		queryTitles(titles) { if (!titles.length) 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',				inlinkcontext: this.page,				formatversion: 2			}, {				headers: { 'Promise-Non-Write-API-Action': 1 }			}).always(response => {				let query = response && response.query;				if (!query) 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; });				this.queryTitles(titles.slice(50));			}); }		copyResults { let text = this.$trail.contents.get.map(n => ( n.tagName === 'A' ? `${n.textContent}` : n.textContent )).join('') + ` {| class="wikitable plainlinks" style="white-space: nowrap;" ! rowspan="2" | Date ! ${this.ascending ? 'From' : 'To'} ! rowspan="2" | Performer ! rowspan="2" | Comment ! ${this.ascending ? 'To' : 'From'} ${this.moves.map(move => `|-	this.titles[this.ascending ? move.from : move.to] && this.titles[this.ascending ? move.from : move.to].redirect		? `[ ${this.ascending ? move.from : move.to}]`		: `${this.ascending ? move.from : move.to}` }	move.comment.length ? 'style="vertical-align: top; white-space: normal;" | ' + move.comment.map(n => (		n.tagName === 'A' ? `${nowiki(n.textContent)}` : nowiki(n.textContent)	)).join() : '|' }	this.titles[this.ascending ? move.to : move.from] && this.titles[this.ascending ? move.to : move.from].redirect		? `[ ${this.ascending ? move.to : move.from}]`		: `${this.ascending ? move.to : move.from}` } `).join()}|}`; let $textarea = $(' ').attr({				readonly: '',				style: 'position:fixed;top:-100%'			}).val(text).appendTo(document.body); $textarea[0].select; let copied; try { copied = document.execCommand('copy'); } catch (e) {} $textarea.remove; if (copied) { mw.notify('Copied'); } else { mw.notify('Copy failed', { type: 'error' }); }		}	}	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: ['paused', 'pausedFound', 'found', 'notFound'], flags: ['safe', 'back'] },		{			action: 'continue', label: 'Continue', modes: ['paused', 'pausedFound'], flags: ['primary', 'progressive'] },		{			action: 'pause', label: 'Pause', modes: 'searching', flags: ['primary', 'destructive'] },		{			action: 'copy', modes: ['pausedFound', 'found'], label: 'Copy results as wikitext' }	];	MoveHistoryDialog.prototype.initialize = function { MoveHistoryDialog.parent.prototype.initialize.apply(this, arguments); let rt = mw.Title.newFromText(mw.config.get('wgRelevantPageName')); this.pageInput = new mw.widgets.TitleInputWidget({			$overlay: this.$overlay,			api: api,			excludeDynamicNamespaces: true,			required: true,			showMissing: false,			value: rt && rt.namespace >= 0 ? rt.toText : ''		}).connect(this, { change: 'updateButton' }); 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'		}).connect(this, { 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') : undefined;			this.sinceInput.setValidityFlag;		}).connect(this, { 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' })			]		}).connect(this, { submit: ['executeAction', 'search'] }); this.$results = $(' '); this.form.$element .append($(' ').attr({ type: 'submit', hidden: '' })) .appendTo(this.$body); };	MoveHistoryDialog.prototype.updateButton = function { this.actions.setAbilities({ search: this.canSearch }); };	MoveHistoryDialog.prototype.canSearch = function { return !!this.pageInput.getValue && ['sinceInput', 'untilInput'].every(n => !this[n].hasFlag('invalid')); };	MoveHistoryDialog.prototype.getSetupProcess = function (data) { return MoveHistoryDialog.super.prototype.getSetupProcess.call(this, data).next(function {			this.updateButton;			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') { if (!this.canSearch) return; let config = [ this.pageInput.getMWTitle.toText, 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.setMode(this.search.mode); }			this.form.toggle(false).$element.after(this.$results); this.setSize(this.search.moves.length ? 'large' : 'medium'); } else if (action === 'continue') { this.search.start; } else if (action === 'pause') { this.search.paused = true; } 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(OO.ui.getTeleportTarget); dialog.open; });
 * rowspan="2" style="text-align: center;" | [ ${move.date.slice(0, 10)} ${move.date.slice(10)}]
 * style="text-align: center;" | ${
 * rowspan="2" | ${move.user} (talk &#124; contribs)
 * rowspan="2" ${
 * style="text-align: center;" | ${
 * style="text-align: center;" | ${