User:Nardog/dark-mode-toggle.js

This is an old revision of this page, as edited by Nardog (talk | contribs) at 22:17, 18 February 2023. 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.
/**
 * Enables or disables the dark-mode gadget.
 *
 * Authors: [[User:SD0001]], [[User:Nardog]]
 */
(function () {
	// 'Dark mode' and 'Light mode' messages must match the ::before content in
	// [[MediaWiki:Gadget-dark-mode-toggle-pagestyles.css]] and [[MediaWiki:Gadget-dark-mode.css]], respectively.
	// Don't overwrite existing messages, if already set on a foreign wiki prior to loading this file
	if (!mw.messages.exists('darkmode-turn-on-label')) {
		mw.messages.set({
			'darkmode-turn-on-label': 'Dark mode',
			'darkmode-turn-on-tooltip': 'Turn dark mode on',
			'darkmode-turn-off-label': 'Light mode',
			'darkmode-turn-off-tooltip': 'Turn dark mode off',
		});
	}

	// Prevent double execution
	if (mw.config.get('dark-mode-toggle-loaded')) return;
	mw.config.set('dark-mode-toggle-loaded', true);

	var moduleState = mw.loader.getState('ext.gadget.dark-mode');
	var isOn = moduleState === 'ready';
	var isLocal = !!moduleState;
	var broadcastChannel = new BroadcastChannel('gadget-dark-mode');
	var linkElement;

	function setThemeColor() {
		// Update the theme-color used by some browsers for coloration of the tab headers and surrounding UI
		$('meta[name="theme-color"]').attr('content', isOn ? '#000000' : '#eaecf0');
	}

	function setHtmlClass() {
		// CSS class for externally styling elements in dark mode via TemplateStyles (or CSS from other gadgets or common.css)
		// A brief flash of the original styles will occur, so this is only suitable for style changes for which flashes are tolerable.
		// For others, update Gadget-dark-mode.css directly which is loaded without FOUCs
		$(document.documentElement).toggleClass('client-dark-mode', isOn);
	}

	function vectorStickyCallback() {
		makePortletLink('p-personal-sticky-header', 'pt-darkmode-sticky-header', '#pt-watchlist-sticky-header');
		mw.hook('vector.page_title_scroll').remove(vectorStickyCallback);
	}

	function addPortlets() {
		makePortletLink('p-personal', 'pt-darkmode', '#pt-watchlist');

		if (mw.config.get('skin') === 'vector-2022') {
			mw.hook('vector.page_title_scroll').add(vectorStickyCallback);
		}
	}

	function getMsg(suffix) {
		var key = 'darkmode-turn-' + (isOn ? 'off' : 'on') + '-' + suffix;
		return mw.msg(key);
	}

	function makePortletLink(portletId, portletLinkId, nextnode) {
		var label = getMsg('label');
		var tooltip = getMsg('tooltip');
		$(mw.util.addPortletLink(portletId, '#', label, portletLinkId, tooltip, '', nextnode))
			.children().on('click', function (e) {
				e.preventDefault();
				toggleMode();
			});

		// Work around lack of icon in Vector 2022
		if (mw.config.get('skin') === 'vector-2022') {
			$('#' + portletId + ' span:only-child')
				.before($('<span>').addClass('mw-ui-icon mw-ui-icon-vector-gadget-pt-darkmode'));
		}
	}

	function togglePortlets() {
		var labelSelector;
		switch (mw.config.get('skin')) {
			case 'vector':
			case 'vector-2022':
			case 'minerva':
				labelSelector = '#pt-darkmode span:not(.mw-ui-icon), #pt-darkmode-sticky-header span:not(.mw-ui-icon)';
				break;
			default:
				labelSelector = '#pt-darkmode a';
		}
		$(labelSelector).text(getMsg('label'));
		$('#pt-darkmode a, #pt-darkmode-sticky-header a')
			.attr('title', getMsg('tooltip'));
	}

	function actuallyToggleDarkMode() {
		if (linkElement) {
			linkElement.disabled = !isOn;
			return;
		}

		if (isOn) {
			linkElement = $('<link>').attr({
				rel: 'stylesheet',
				href: (isLocal ? '' : '//en.wikipedia.org') + '/w/load.php?modules=ext.gadget.dark-mode&only=styles'
			}).appendTo(document.head)[0];
		} else if (isLocal) {
			// Modify the <link> element on the page to remove dark-mode styles
			var scriptPath = mw.util.wikiScript('load');
			var $gadgetsLink = $('link[rel="stylesheet"][href^="' + scriptPath + '?"][href*="ext.gadget."]');
			if (!$gadgetsLink.length) return;
			var uri = new mw.Uri($gadgetsLink.prop('href'));
			if (uri.query.modules === 'ext.gadget.dark-mode') {
				// dark-mode is the only module in this link
				$gadgetsLink.remove();
			} else {
				uri.query.modules = uri.query.modules
					.replace('ext.gadget.dark-mode,', 'ext.gadget.') // dark-mode is first in the gadget list
					.replace(/,dark-mode(,|$)/, '$1'); // dark-mode is in middle or end of the list
				$gadgetsLink.prop('href', uri.getRelativePath());
			}
		}
	}

	function savePreference() {
		if (isLocal) {
			new mw.Api().saveOption('gadget-dark-mode', isOn ? '1' : '0');
		} else {
			mw.requestIdleCallback(function () {
				if (isOn) {
					// Expire in 90 days
					mw.storage.set('dark-mode-toggled', '1', 7776000);
				} else {
					mw.storage.remove('dark-mode-toggled');
				}
			});
		}

		if (window.wpDarkModeAutoToggle &&
			matchMedia('(prefers-color-scheme: dark)').matches !== isOn
		) {
			mw.requestIdleCallback(function () {
				// Expire in 90 days
				mw.storage.set('dark-mode-system-scheme-override', isOn ? '1' : '0', 7776000);
			});
		}
	}

	function savePreferenceLocally() {
		if (isLocal) {
			mw.user.options.set('gadget-dark-mode', Number(isOn));

			// In case the user navigates to another page too quickly
			mw.storage.session.set('dark-mode-toggled', isOn ? '1' : '0');
		}
	}

	function notifyOtherTabs() {
		// Broadcast state change to other tabs
		broadcastChannel.postMessage(isOn);
	}

	function toggleMode(offline) {
		isOn = !isOn;
		if (!offline) {
			savePreference();
			notifyOtherTabs();
		}
		setHtmlClass();
		setThemeColor();
		savePreferenceLocally();
		togglePortlets();
		actuallyToggleDarkMode();
	}

	function toggleBasedOnSystemColourScheme() {
		var prefersDark = matchMedia('(prefers-color-scheme: dark)').matches;
		var preferenceOverride = mw.storage.get('dark-mode-system-scheme-override');
		if (preferenceOverride) {
			prefersDark = preferenceOverride === '1';
		}
		if (prefersDark === isOn) {
			if (preferenceOverride) {
				mw.requestIdleCallback(function () {
					mw.storage.remove('dark-mode-system-scheme-override');
				});
			}
		} else {
			toggleMode();
		}
	}

	if (!isLocal) {
		mw.loader.load('//en.wikipedia.org/w/load.php?modules=ext.gadget.dark-mode-toggle-pagestyles&only=styles', 'text/css');
	}

	$.when($.ready, mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.Uri', 'mediawiki.storage'])).then(function () {
		setHtmlClass();
		setThemeColor();
		addPortlets();

		// Recover state if the navigation was too quick or if loaded as a user script
		var storageState = isLocal ? mw.storage.session.get('dark-mode-toggled') : mw.storage.get('dark-mode-toggled');
		if (storageState && Number(storageState) !== Number(isOn)) {
			toggleMode(true);
		}

		// Listen to dark mode state change made on other tabs
		broadcastChannel.onmessage = function (msg) {
			if (msg.data !== isOn) {
				toggleMode(true);
			}
		};

		if (window.wpDarkModeAutoToggle) {
			toggleBasedOnSystemColourScheme();

			// If system colour scheme changes while user is viewing, toggle immediately
			var mediaQuery = matchMedia('(prefers-color-scheme: dark)');
			if (mediaQuery.addEventListener) {
				mediaQuery.addEventListener('change', toggleBasedOnSystemColourScheme);
			} else if (mediaQuery.addListener) { // Safari 13 and older
				mediaQuery.addListener(toggleBasedOnSystemColourScheme);
			}
		}
	});
}());