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.
// This script led to [[:phab:T308364]] being filed.
/* jshint esversion: 10, maxerr: 9999, unused: true, quotmark: single */

$(() => {
	const spn = (mw.config.get('wgCanonicalSpecialPageName') || '').toLowerCase();
	const lc = str => str.toLowerCase();
	const switches = function (cases, def) {
		for (const i in cases) {
			if (i.toLowerCase() === spn) {
				return cases[i];
			}
		}
		return def;
	};

	// Main functions
	const linktoggle = function () {
		window.ajaxloaderloading = !window.ajaxloaderloading;
	};
	const spinner = function () {
		const $l = $('<div>').attr('class', 'ajaxloader-loader');

		for (let i = 0; i < 12; i++) {
			$l.append(
				$('<div>')
					.attr('class', 'ajaxloader-loader-items')
					.css({
						animation: 'ajaxloader-loader 1.2s linear infinite',
						transform: `rotate(${i * 30}deg)`,
						'animation-delay': Math.round((-1.1 + i * 0.1) * 10) / 10 + 's'
					})
			);
		}

		return $('<div>')
			.attr({
				class: 'ajaxloader-loader-wrapper',
				id: 'ajaxloader-loader'
			})
			.append($l);
	};
	const init = function (object) {
		const {links, linkswrapper, linkextend, main, parent, preload, preupdate} = object;
		const updateWrapper =
			object.update !== 'unwrapped' ?
				(linkswrapper || links) &&
				function (content) {
					if (preupdate) {
						preupdate();
					}
					$(linkswrapper || links).each(function (index) {
						$(this).replaceWith($(linkswrapper || links, content)[index]);
					});
				}
			:	function (content) {
					let $links = [
						$(parent || '#mw-content-text').clone(),
						$(parent || '#mw-content-text', content).clone()
					];

					$links = $links.map($el => {
						$el.find('*').not('a').remove();
						$el.html(
							$el
								.html()
								.trim()
								.replace(/\s*title=".*?(?<!\\)"(\s*)/g, '$1')
								.replace(/.*?(.+)\s*\1.*?/, '$1')
						);
						return $el;
					});

					$(links).each(function () {
						$(this).prop(
							'outerHTML',
							$(this)
								.prop('outerHTML')
								.replace(/\s*title=".*?"(\s*)/g, '$1')
						);
					});

					for (const _ of [0, 1]) {
						$('#mw-content-text').html((_, oldHtml) => oldHtml.replace($links[0].html(), $links[1].html()));
					}
				};

		if (!links) {
			console.warn('No links.');
			return;
		}

		const update = function (response) {
			updateWrapper(response);
			$(main).replaceWith($(main, response));
			if (preload) {
				preload();
			}
			mw.hook('wikipage.content').fire($(main));
		};

		const initialHtml = document.documentElement.outerHTML;
		// handle navigating between history items pushed by this script
		window.addEventListener('popstate', event => {
			update(event?.state?.response ?? initialHtml);
		});

		$('body').on('click', links, function (e) {
			e.preventDefault();
			if (window.ajaxloaderloading) {
				return;
			}

			linktoggle();
			const $spinner = spinner();
			$(main).hide().after($spinner);

			const link = new URL($(this).prop('href'));
			if (linkextend) {
				Object.entries(linkextend).forEach(([key, value]) => {
					link.searchParams.set(key, value);
				});
			}

			$.ajax({
				url: link.toString(),
				success: function (response) {
					$spinner.remove();
					linktoggle();
					update(response);
					window.history.___pushState({response: response}, '', link); // add new URL to history/urlbar
				},
				error: function () {
					$spinner.remove();
					linktoggle();
					$(main).show();
					mw.notify('Cannot get pages.', {title: 'AjaxLoader', type: 'warn'});
				}
			});
		});
	};
	const includes = function (array, element = spn) {
		array = array instanceof Array ? array : [array];
		return array.some(e => e.toLowerCase() === element.toLowerCase());
	};
	const join = function (links, parent, separator) {
		const s = [];
		for (const l of links) {
			s.push(`${parent}${separator || ' > a'}${l}`);
		}
		return s.join(', ');
	};
	const load = function (server, title) {
		mw.loader.load(`//${server}/w/index.php?title=${title}&action=raw&ctype=text/javascript`);
	};

	// CSS
	// For attribution: //loading.io/css (CC0)
	const css = `
		.ajaxloader-loader-wrapper {
			display: block;
			margin: 3em 0;
			text-align: center;
		}
		.ajaxloader-loader {
			display: inline-block;
			position: relative;
			width: 80px;
			height: 80px;
		}
		.ajaxloader-loader .ajaxloader-loader-items {
			transform-origin: 40px 40px;
		}
		.ajaxloader-loader .ajaxloader-loader-items::after {
			content: ' ';
			display: block;
			position: absolute;
			top: 4px;
			left: 36px;
			width: 6px;
			height: 18px;
			border-radius: 20%;
			background: #000000;
		}
		@keyframes ajaxloader-loader {
			0% {
				opacity: 1;
			}
			100% {
				opacity: 0;
			}
		}
	`;
	mw.loader.addStyleTag(css, document.head.children[0]); // easily overridden

	// Data
	const data = [
		{
			name: 'Navlinks inside mw-pager-navigation-bar ([[:phab:T308364]])',
			check: function () {
				return document.getElementsByClassName('mw-pager-navigation-bar').length > 0;
			},
			links: join(
				['.mw-lastlink', '.mw-firstlink', '.mw-prevlink', '.mw-nextlink', '.mw-numlink'],
				'.mw-pager-navigation-bar'
			),
			linkswrapper: '.mw-pager-navigation-bar',
			main:
				$('.mw-pager-body').length ? '.mw-pager-body' : (
					switches(
						{
							Log: '.mw-logevent-loglines',
							'': '#pagehistory',
							AbuseLog: '#mw-content-text > form',
							Search: '.mw-search-results-container',
							WhatLinksHere: '#mw-whatlinkshere-list',
							GlobalUsage: '#mw-globalusage-result'
						},
						'#mw-content-text ul, #mw-content-text ol'
					)
				),
			preload: function () {
				if (includes('NewImages')) {
					$('#mw-content-text > ul').before($('.mw-pager-navigation-bar').clone());
				}
			},
			preupdate: function () {
				if (includes('NewImages')) {
					$($('.mw-pager-navigation-bar')[0]).remove();
				}
			}
		},
		{
			name: 'Special pages with -nav classes',
			check: function () {
				return includes(['AllPages', 'PrefixIndex']);
			},
			links: '.mw-' + spn + '-nav > a',
			linkswrapper: '.mw-' + spn + '-nav',
			main: '.mw-' + spn + '-body'
		},
		{
			name: 'Abuse filter history',
			check: function () {
				return spn === lc('AbuseFilter') && $('.mw-abusefilter-history-buttons').length > 0;
			},
			links: '.mw-abusefilter-history-buttons a.oo-ui-buttonElement-button',
			linkswrapper: '.mw-abusefilter-history-buttons',
			main: '#mw-content-text > table.wikitable',
			preload: function () {
				$('#mw-content-text > table.wikitable').before($('.mw-abusefilter-history-buttons').clone());
			},
			preupdate: function () {
				$($('.mw-abusefilter-history-buttons')[0]).remove();
			}
		},
		{
			name: 'Modernized special pages with button-like navlinks',
			check: function () {
				return includes([
					'AllMessages',
					'ProtectedPages',
					'BlockList',
					'AutoblockList',
					'AbuseFilter',
					'ListFiles'
				]);
			},
			links: '.TablePager_nav a',
			linkswrapper: '.TablePager_nav',
			main: switches({
				AllMessages: '#mw-allmessagestable',
				ProtectedPages: '.mw-protectedpages',
				BlockList: '.mw-blocklist',
				AutoblockList: '.mw-blocklist',
				AbuseFilter: '.mw-datatable',
				ListFiles: '.mw-datatable'
			})
		},
		{
			name: 'Contributions',
			check: function () {
				return includes(['Contributions']);
			},
			links: '.mw-pager-navigation-bar > a',
			linkswrapper: '.mw-pager-navigation-bar',
			main: '.mw-pager-body'
		},
		{
			name: 'SearchTranslations',
			check: function () {
				return includes(['SearchTranslations']);
			},
			links: '.tux-pagination-links > a',
			linkswrapper: '.tux-pagination-links',
			main: '.results'
		},
		{
			name: 'Categories',
			check: function () {
				return mw.config.get('wgNamespaceNumber') === 14;
			},
			exempt: function () {
				load('en.wikipedia.org', 'User:NguoiDungKhongDinhDanh/AjaxCat.js');
			}
		},
		{
			name: 'Translatable pages',
			check: function () {
				return $('ul.mw-pt-languages-list').length;
			},
			links: '.mw-pt-languages-list a.mw-pt-progress',
			linkswrapper: '.mw-pt-languages-list',
			main: '#bodyContent'
		},
		{
			name: 'Files',
			check: function () {
				return mw.config.get('wgNamespaceNumber') === 6;
			},
			links: join(
				['.mw-lastlink', '.mw-firstlink', '.mw-prevlink', '.mw-nextlink', '.mw-numlink'],
				'.mw-pager-navigation-bar'
			),
			linkswrapper: '.mw-pager-navigation-bar',
			main: '.filehistory'
		}
	];

	// Init.
	for (const i of data) {
		if (i.check()) {
			console.log(i.name);
			if (!i.exempt) {
				if (i.preload) {
					i.preload();
				}
				init(i);
			} else {
				i.exempt();
			}
			break;
		}
	}
});