MediaWiki:Gadget-readableRC-core.js

This is an old revision of this page, as edited by Alex (talk | contribs) at 17:15, 17 October 2024. The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

After saving, you may need to bypass your browser's cache to see the changes. For further information, see Wikipedia:Bypass your cache.

  • In most Windows and Linux browsers: Hold down Ctrl and press F5.
  • In Safari: Hold down ⇧ Shift and click the Reload button.
  • In Chrome and Firefox for Mac: Hold down both ⌘ Cmd+⇧ Shift and press R.
// <nowiki>
// Formats the rows on Special:RecentChanges where all the information runs together
// into three columns (page, diff/byte change, and user links) to make it more readable
//
// @author Iiii_I_I_I

;(function ($, mw) {
	function runReadableRC($content) {
		if (!$content.hasClass('mw-changeslist')) {
			return;
		}

		$content.addClass('gadget-rc-enabled');

		let rows = document.querySelectorAll(
			'.mw-changeslist-src-mw-edit,' +
			'.mw-changeslist-src-mw-log,' +
			'.mw-changeslist-src-mw-new'
		);

		for (let row of rows) {
			// hover text on timestamp column
			addRelativeTime(row);

			// nested rows
			if (row.classList.contains('mw-rcfilters-ui-highlights-enhanced-nested')) {
				if (row.classList.contains('mw-changeslist-edit')) {
					cleanNestedEdit(row);
				} else {
					cleanNestedLog(row);
				}
			}
			// top-level rows
			else {
				// grouped row
				if (row.classList.contains('mw-rcfilters-ui-highlights-enhanced-toplevel')) {
					let parent = row.closest('.mw-changeslist-line');

					if (parent.classList.contains('mw-changeslist-edit')) {
						cleanGroupedEdit(row);
					} else {
						cleanGroupedLog(row);
					}
				}
				// single row
				else {
					if (row.classList.contains('mw-changeslist-edit')) {
						cleanSingleEdit(row);
					} else {
						cleanSingleLog(row);
					}
				}
			}
		}

		// fires every time readableRC runs when new edits come in
		mw.hook('ext.gadget.readableRC').fire();
	}

	function cleanNestedLog(row) {
		row.classList.add('gadget-rc-row', 'gadget-rc-log', 'gadget-rc-nested');

		// FIRST COLUMN: log timestamp
		row.querySelector('.mw-enhanced-rc-time').classList.add('gadget-rc-col-1');

		// SECOND COLUMN: placeholder with separator dots
		row.querySelector('.mw-changeslist-separator').classList.add('gadget-rc-col-2');

		// THIRD COLUMN: user info
		let col3 = document.createElement('span');

		col3.classList.add('gadget-rc-col-3');
		col3.append(
			get('.mw-changeslist-log-entry', row),
			get('.mw-tag-markers', row)
		);
		row.querySelector('.gadget-rc-col-2').after(col3);
		cleanUserLinks(row);
	}

	function cleanNestedEdit(row) {
		row.classList.add('gadget-rc-row', 'gadget-rc-edit', 'gadget-rc-nested');

		// FIRST COLUMN: revision link
		row.querySelector('.mw-enhanced-rc-time').classList.add('gadget-rc-col-1');

		// THIRD COLUMN: user info
		let col3 = document.createElement('span');

		col3.classList.add('gadget-rc-col-3');
		col3.append(
			get('.mw-userlink', row),
			get('.mw-usertoollinks', row),
			get('.mw-enhanced-rc-nested > .history-deleted', row), // (username removed)
			get('.comment', row),
			get('.mw-pager-tools', row),
			get('.mw-tag-markers', row)
		);

		// SECOND COLUMN: diff text
		let parent = row.querySelector('td.mw-enhanced-rc-nested');
		let col1 = row.querySelector('.gadget-rc-col-1');

		// detach first column so remaining elements all go in diff column
		parent.removeChild(col1);

		// wrap elements together inside column 2
		let col2 = document.createElement('span');

		col2.classList.add('gadget-rc-col-2');
		while (parent.firstChild) {
			col2.append(parent.firstChild);
		}

		// put everything back together
		parent.append(col1);
		parent.append(col2);
		parent.append(col3);

		cleanUserLinks(row);
	}

	function cleanGroupedLog(row) {
		row.classList.add('gadget-rc-row', 'gadget-rc-log', 'gadget-rc-grouped');

		// FIRST COLUMN: log name
		row.querySelector('.mw-rc-unwatched').classList.add('gadget-rc-col-1');

		// SECOND COLUMN: placeholder with separator dots
		row.querySelector('.mw-changeslist-separator').classList.add('gadget-rc-col-2');

		// THIRD COLUMN: user info
		row.querySelector('.changedby').classList.add('gadget-rc-col-3');

		// remove square brackets from grouped usernames; can't use remove()
		// since there might be other text in the same node, eg. "(4×)]"
		let users = row.querySelector('.gadget-rc-col-3').childNodes;

		users[0].textContent = users[0].textContent.slice(1); // [
		users[users.length - 1].textContent = users[users.length - 1].textContent.slice(0, -1); // ]

		// remove empty text nodes - convert live NodeList to array
		let children = [...row.querySelector('.mw-changeslist-line-inner').childNodes];

		for (let child of children) {
			if (child.nodeType === Node.TEXT_NODE) {
				child.remove();
			}
		}
	}

	function cleanGroupedEdit(row) {
		row.classList.add('gadget-rc-row', 'gadget-rc-edit', 'gadget-rc-grouped');

		// FIRST COLUMN: page name
		row.querySelector('.mw-title').classList.add('gadget-rc-col-1');

		// SECOND COLUMN: diff text
		let col2 = document.createElement('span');

		col2.classList.add('gadget-rc-col-2');
		col2.append(
			get('.mw-changeslist-links', row),
			get('.mw-diff-bytes', row)
		);
		row.querySelector('.gadget-rc-col-1').after(col2);

		// "x changes" -> "diff"
		if (row.querySelector('.mw-changeslist-groupdiff')) {
			row.querySelector('.mw-changeslist-groupdiff').textContent = 'diff';
		}
		// new pages have a text node instead of a link
		else {
			row.querySelector('.mw-changeslist-links span:first-child').textContent = 'diff';
		}

		// "history" -> "hist"
		if (row.querySelector('.mw-changeslist-history')) {
			row.querySelector('.mw-changeslist-history').textContent = 'hist';
		}
		// nonexistent pages (redirect-suppressed move or deleted) have a text node instead of a link
		// @todo check for new classname/structure; no example rn
		else {
			// let newHist = row.querySelector('.mw-changeslist-line-inner').childNodes[4].nodeValue.replace('history', 'hist');
			// row.querySelector('.mw-changeslist-line-inner').childNodes[4].nodeValue = newHist;
		}

		// THIRD COLUMN: user info
		row.querySelector('.changedby').classList.add('gadget-rc-col-3');

		// remove square brackets from grouped usernames; cannot simply use remove()
		// since there might be other text in the same node, eg. "(4×)]"
		let users = row.querySelector('.gadget-rc-col-3').childNodes;

		users[0].textContent = users[0].textContent.slice(1); // [
		users[users.length - 1].textContent = users[users.length - 1].textContent.slice(0, -1); // ]

		// remove empty text nodes - convert live NodeList to array
		let children = [...row.querySelector('.mw-changeslist-line-inner').childNodes];

		for (let child of children) {
			if (child.nodeType === Node.TEXT_NODE) {
				child.remove();
			}
		}
	}

	function cleanSingleLog(row) {
		row.classList.add('gadget-rc-row', 'gadget-rc-log');

		// FIRST COLUMN: log name
		row.querySelector('.mw-changeslist-line-inner-logLink').classList.add('gadget-rc-col-1');

		// SECOND COLUMN: placeholder with separator dots
		row.querySelector('.mw-changeslist-line-inner-separatorAfterLinks').classList.add('gadget-rc-col-2');

		// THIRD COLUMN: user info
		let col3 = document.createElement('span');

		col3.classList.add('gadget-rc-col-3');
		col3.append(
			get('.mw-changeslist-line-inner-logEntry', row),
			get('.mw-changeslist-line-inner-tags', row),
			get('.mw-changeslist-line-inner-watchingUsers', row)
		);
		row.querySelector('.gadget-rc-col-2').after(col3);
		cleanUserLinks(row);
	}

	function cleanSingleEdit(row) {
		row.classList.add('gadget-rc-row', 'gadget-rc-edit');

		// FIRST COLUMN: page name
		row.querySelector('.mw-changeslist-line-inner-articleLink').classList.add('gadget-rc-col-1');

		// SECOND COLUMN: diff text
		let col2 = document.createElement('span');

		col2.classList.add('gadget-rc-col-2');
		col2.append(
			get('.mw-changeslist-line-inner-historyLink', row),
			get('.mw-changeslist-line-inner-characterDiff', row)
		);
		row.querySelector('.gadget-rc-col-1').after(col2);

		// THIRD COLUMN: user info
		let col3 = document.createElement('span');

		col3.classList.add('gadget-rc-col-3');
		col3.append(
			get('.mw-changeslist-line-inner-userLink', row),
			get('.mw-changeslist-line-inner-userTalkLink', row),
			get('.mw-changeslist-line-inner-comment', row),
			get('.mw-changeslist-line-inner-rollback', row),
			get('.mw-changeslist-line-inner-tags', row),
			get('.mw-changeslist-line-inner-watchingUsers', row)
		);
		col2.after(col3);
		cleanUserLinks(row);
	}

	function cleanUserLinks(row) {
		// if username has been revdeled (shows "(username removed)"), it has no links
		if (row.querySelector('.history-deleted')) return;

		// replace links with first letter of each link
		let links = row.querySelectorAll('.mw-usertoollinks a');

		for (let link of links) {
			link.textContent = link.textContent.slice(0, 1);
		}

		// rollback link doesn't exist if page creation or user does not have the right
		if (row.querySelector('.mw-rollback-link')) {
			row.querySelector('.mw-rollback-link a').textContent = 'rollback';
		}
	}

	// add relative time (eg. "25m ago") to timestamp column as hover text
	function addRelativeTime(row) {
		// if row is a single row
		let timestamp = row.getAttribute('data-mw-ts');

		// if row is a grouped row
		if (timestamp === null) {
			timestamp = row.closest('table.mw-enhanced-rc').getAttribute('data-mw-ts');
		}

		// convert mw timestamp (eg. 20240906020749) to Date (equal to 2024-09-06, 02:07:49 UTC)
		let {y, m, d, h, min, s} = timestamp.match(/(?<y>\d{4})(?<m>\d{2})(?<d>\d{2})(?<h>\d{2})(?<min>\d{2})(?<s>\d{2})/).groups;
		let timestampObj = new Date(`${y}-${m}-${d}T${h}:${min}:${s}.000Z`);

		// get time difference, then format into hours and minutes
		let minsAgo = Math.floor((new Date() - timestampObj) / 1000 / 60);
		let timeAgo;

		if (minsAgo === 0) {
			timeAgo = 'Now!';
		} else if (minsAgo < 60) {
			timeAgo = minsAgo + 'm ago';
		} else {
			timeAgo = Math.floor(minsAgo / 60) + 'h ' + minsAgo % 60 + 'm ago';
		}

		get('.mw-enhanced-rc', row).setAttribute('title', timeAgo);
	}

	// return element if it exists; if not, fail silently (unlike querySelector, which returns null)
	function get(selector, scope = document) {
		let element = scope.querySelector(selector);
		return (element) ? element : '';
	}

	function init() {
		mw.hook('structuredChangeFilters.ui.initialized').add(function () {
			// initial load
			runReadableRC($('.mw-changeslist'));

			// page refreshed with new edits / "Live updates" on
			mw.hook('wikipage.content').add(runReadableRC);
		});
	}

	$(init);
})(jQuery, mediaWiki);

// </nowiki>