User:Nardog/MoveHistoryClassic-core.js

Source: Wikipedia, the free encyclopedia.
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.
mw.loader.using([
	'mediawiki.api', 'mediawiki.util', 'oojs-ui-core', 'mediawiki.widgets',
	'jquery.tablesorter', 'oojs-ui.styles.icons-interactions',
	'mediawiki.interface.helpers.styles'
], function moveHistoryCore() {
	mw.util.addCSS('.movehistory .oo-ui-radioOptionWidget{display:inline-table} .movehistory .oo-ui-radioOptionWidget{margin-right:8px} .movehistory .mw-widget-dateInputWidget{width:8em} .movehistory > table{margin-top:0}');
	let $div = $('<div>').addClass('movehistory');
	let $status = $('<div>').addClass('small'), $trail = $('<p>'), $table, $tbody;
	
	let button = new OO.ui.ButtonInputWidget({
		label: 'Search',
		flags: ['primary', 'progressive'],
		type: 'submit'
	});
	let directionInput = new OO.ui.RadioSelectInputWidget({
		options: [
			{ data: 'newer', label: 'Oldest first' },
			{ data: 'older', label: 'Newest first' }
		]
	});
	let sinceInput = new mw.widgets.DateInputWidget({
		placeholderLabel: 'Since',
		displayFormat: 'YYYY-MM-DD'
	});
	let untilInput = new mw.widgets.DateInputWidget({
		placeholderLabel: 'Until',
		displayFormat: 'YYYY-MM-DD'
	});
	let form = new OO.ui.FormLayout({
		items: [
			button,
			directionInput,
			new OO.ui.HorizontalLayout({ items: [sinceInput, untilInput] })
		],
		classes: ['oo-ui-horizontalLayout']
	});
	
	let inputs = [button, directionInput, sinceInput, untilInput];
	let setDisabledAll = disabled => {
		inputs.forEach(input => {
			input.setDisabled(disabled);
		});
	};
	
	let currentName = mw.config.get('wgPageName').replace(/_/g, ' ');
	let api, rvcontinue, ascending, isComplete, lastDate;
	let modified, prevDir, prevSince, prevUntil;
	let loadRevs = () => {
		if (modified) {
			reset();
		} else if (isComplete) {
			loadMoves();
			return;
		}
		setDisabledAll(true);
		let dir = directionInput.getValue();
		prevDir = dir;
		ascending = dir === 'newer';
		let since = sinceInput.getValue();
		prevSince = since;
		since = (since || '2005-06-25') + 'T00:00:00Z';
		let until = untilInput.getValue();
		prevUntil = until;
		until = until ? until + 'T23:59:59Z' : undefined;
		$status.text(`Loading history${lastDate ? (ascending ? ' after ' : ' before ') + lastDate : ''}...`);
		if (!api) api = new mw.Api({
			ajax: { headers: { 'Api-User-Agent': 'MoveHistory (https://en.wikipedia.org/wiki/User:Nardog/MoveHistory)' } }
		});
		api.get({
			action: 'query',
			titles: currentName,
			prop: 'revisions',
			rvstart: ascending ? since : until,
			rvend: ascending ? until : since,
			rvdir: dir,
			rvprop: 'sha1|timestamp|user|comment',
			rvlimit: 'max',
			rvcontinue: rvcontinue,
			formatversion: 2
		}).always((response, errorObj) => {
			let errorMsg = ((errorObj || {}).error || {}).info;
			if (!response || typeof response === 'string' || errorMsg) {
				button.setDisabled(false);
				$status.text('Error retrieving revisions' + (errorMsg ? ': ' + errorMsg : ''));
				return;
			}
			processRevs(((((response || {}).query || {}).pages || [])[0] || {}).revisions || []);
			rvcontinue = ((response || {}).continue || {}).rvcontinue;
			if (!rvcontinue) isComplete = response.batchcomplete;
			loadMoves();
		});
	};
	
	let lastRev, candidates = [], revCount = 0;
	let processRevs = revs => {
		revCount += revs.length;
		if (lastRev) revs.unshift(lastRev);
		let increment = ascending ? -1 : 1;
		revs.forEach((rev, i) => {
			if (!(rev.comment && rev.user && rev.sha1)) return;
			let comp = revs[i + increment];
			if (!comp || comp.sha1 !== rev.sha1) return;
			let matches = rev.comment.match(/\[\[:?([^\]]+)\]\].+?\[\[:?([^\]]+)\]\]/);
			if (matches) rev.matches = matches.slice(1);
			candidates.push(rev);
		});
		lastRev = revs.pop();
		lastDate = lastRev.timestamp;
	};
	
	let titles = new Set(), processed = new Set(), missing = new Set();
	let loadMoves = () => {
		let rev = candidates.shift();
		let titlesArg = [...titles].filter(t => !processed.has(t)).join('|') || undefined;
		if (!rev && !titlesArg) {
			finish();
			return;
		}
		
		let args = {
			action: 'query',
			titles: titlesArg,
			formatversion: 2
		};
		if (rev) {
			$status.text(`Seeing if there was a move at ${rev.timestamp}...`);
			let date = Date.parse(rev.timestamp) / 1000;
			Object.assign(args, {
				list: 'logevents',
				letype: 'move',
				lestart: date + 60,
				leend: date,
				leprop: 'details|title|user|parsedcomment',
				lelimit: 'max'
			});
		}
		api.get(args).always((response, errorObj) => {
			let errorMsg = ((errorObj || {}).error || {}).info;
			if (!response || typeof response === 'string' || errorMsg) {
				button.setDisabled(false);
				$status.text('Error retrieving moves' + (errorMsg ? ': ' + errorMsg : ''));
				return;
			}
			(((response || {}).query || {}).pages || []).forEach(page => {
				processed.add(page.title);
				if (page.missing) missing.add(page.title);
			});
			(((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)) return;
				if (rev.matches && [le.title, target].some(s => !rev.matches.includes(s))) return;
				addRow(rev.timestamp, le.title, target, le.user, le.parsedcomment);
				return true;
			});
			loadMoves();
		});
	};
	
	let arrow = document.dir === 'rtl' ? ' ← ' : ' → ', lastName, count = 0;
	let addRow = (date, from, to, user, comment) => {
		if (!count) {
			lastName = ascending ? from : to;
			$trail.append(makeLink(lastName));
		}
		if (ascending) {
			if (lastName !== from) $trail.append(' ... ', makeLink(from));
			$trail.append(arrow, makeLink(to));
			lastName = to;
		} else {
			if (lastName !== to) $trail.prepend(makeLink(to), ' ... ');
			$trail.prepend(makeLink(from), arrow);
			lastName = from;
		}
		
		if (!$table) {
			$tbody = $('<tbody>');
			$table = $('<table>').addClass('wikitable sortable').append(
				$('<tr>').append(
					$('<th>').text('Date'),
					$('<th>').text('From'),
					$('<th>').text('To'),
					$('<th>').text('Performer'),
					$('<th>').text('Comment')
				).wrap('<thead>').parent(),
				$tbody
			);
		}
		
		$('<tr>').append(
			$('<td>').append(
				$('<a>', {
					href: mw.util.getUrl(currentName, { action: 'history', offset: Number(date.replace(/\D/g, '')) + 1 }),
					text: date
				})
			),
			$('<td>').append(makeLink(from)),
			$('<td>').append(makeLink(to)),
			$('<td>').append(
				$('<a>', {
					href: mw.util.getUrl('User:' + user),
					text: user
				}),
				' ',
				$('<span>').addClass('mw-changeslist-links').append(
					$('<a>', {
						href: mw.util.getUrl('User talk:' + user),
						text: 'talk',
					}).wrap('<span>').parent(),
					$('<a>', {
						href: mw.util.getUrl('Special:Contributions/' + user),
						text: 'contribs'
					}).wrap('<span>').parent()
				)
			),
			$('<td>').append(comment)
		).appendTo($tbody);
		$table.insertAfter($trail);
		count++;
	};
	
	let blueLinks = {};
	let makeLink = name => {
		if (name === currentName) {
			return name;
		} else if (missing.has(name)) {
			return $('<a>', {
				class: 'new',
				href: mw.util.getUrl(name, { action: 'edit', redlink: 1 }),
				text: name
			});
		} else {
			let $link = $('<a>', {
				href: mw.util.getUrl(name, { redirect: 'no' }),
				text: name
			});
			if (blueLinks[name]) {
				blueLinks[name] = blueLinks[name].add($link);
			} else {
				blueLinks[name] = $link;
			}
			titles.add(name);
			return $link;
		}
	};
	
	let i = 0, lastCount = 0, prevLabel, prevDisabled;
	let finish = () => {
		missing.forEach(name => {
			if (!blueLinks[name]) return;
			blueLinks[name].attr({
				class: 'new',
				href: mw.util.getUrl(name, { action: 'edit', redlink: 1 })
			});
			delete blueLinks[name];
		});
		
		setDisabledAll(false);
		$status.text(`Found ${count} move${count === 1 ? '' : 's'} in ${revCount.toLocaleString()} revisions${isComplete ? '' : (ascending ? ' until ' : ' since ') + lastDate}${count ? ':' : ''}`);
		if (isComplete) {
			button.setLabel(count ? 'No more results' : 'No results').setDisabled(true);
		} else {
			if (++i >= 4 || count - lastCount) {
				button.setLabel('Continue');
				i = 0;
			} else {
				loadRevs();
				return;
			}
			lastCount = count;
		}
		prevLabel = button.getLabel();
		prevDisabled = button.isDisabled();
		
		if ($table) {
			if ($table.hasClass('jquery-tablesorter'))
				$table.remove().insertAfter($trail);
			$table.tablesorter();
		}
	};
	
	let reset = () => {
		rvcontinue = undefined;
		isComplete = false;
		lastRev = undefined;
		lastDate = undefined;
		candidates = [];
		revCount = 0;
		count = 0;
		lastCount = 0;
		$trail.empty();
		if ($table) $table.detach().find('tr:not(:first-child)').remove();
		modified = false;
	};
	
	let updateForm = () => {
		let since = sinceInput.getValue(), until = untilInput.getValue();
		let invalid = since && until && since > until;
		sinceInput.setValidityFlag(!invalid);
		invalid = invalid || until && until < '2005-06-25';
		untilInput.setValidityFlag(!invalid);
		if (!prevLabel) {
			button.setDisabled(invalid);
			return;
		}
		if (directionInput.getValue() === prevDir &&
			since === prevSince && until === prevUntil
		) {
			button.setLabel(prevLabel).setDisabled(invalid || prevDisabled);
			modified = false;
		} else {
			button.setLabel('Search').setDisabled(invalid);
			modified = true;
		}
	};
	directionInput.on('change', updateForm);
	sinceInput.on('change', updateForm);
	untilInput.on('change', updateForm);
	
	button.on('click', loadRevs);
	$div.append(form.$element, $status, $trail);
	$('<h2>').text('Move history').prependTo('#mw-content-text').after($div);
});