User:Nardog/dark-mode-toggle.js

/**

* Enables or disables the dark-mode gadget.

*

* Authors: User:SD0001, User:Nardog

*/

// '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.get('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',

});

}

var isOn = mw.loader.getState('ext.gadget.dark-mode') === 'ready';

var broadcastChannel = new BroadcastChannel('gadget-dark-mode');

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() {

mw.hook('vector.page_title_scroll').remove(vectorStickyCallback);

if (document.getElementById('pt-darkmode-sticky-header')) return;

makePortletLink('p-personal-sticky-header', 'pt-darkmode-sticky-header', '#pt-watchlist-sticky-header');

}

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();

});

}

function togglePortlets() {

var labelSelector;

switch (mw.config.get('skin')) {

case 'vector':

case 'vector-2022':

case 'minerva':

labelSelector = '#pt-darkmode span:not(:empty), #pt-darkmode-sticky-header span:not(:empty)';

break;

default:

labelSelector = '#pt-darkmode a';

}

$(labelSelector).text(getMsg('label'));

$('#pt-darkmode a, #pt-darkmode-sticky-header a')

.attr('title', getMsg('tooltip'));

}

function actuallyToggleDarkMode() {

// Modify the element on the page to include/exclude dark-mode styles

// We can't use mw.loader as it doesn't work both ways (see talk page)

var scriptPath = mw.util.wikiScript('load');

var $gadgetsLink = $('link[rel="stylesheet"][href^="' + scriptPath + '?"][href*="ext.gadget."]');

if ($gadgetsLink.length) {

var url = new URL($gadgetsLink.prop('href'));

var modules = url.searchParams.get('modules');

if (isOn) {

modules += ',dark-mode';

} else {

if (modules === 'ext.gadget.dark-mode') {

// dark-mode is the only module in this link

$gadgetsLink.remove();

return;

}

modules = 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

}

url.searchParams.set('modules', modules);

$gadgetsLink.prop('href', url.pathname + url.search);

} else {

// No gadget-containing styles are enabled

$('').attr({

rel: 'stylesheet',

href: scriptPath + '?lang=' + mw.config.get('wgUserLanguage') +

'&modules=ext.gadget.dark-mode&only=styles&skin=' + mw.config.get('skin')

}).appendTo(document.head);

}

}

function savePreference() {

new mw.Api().saveOption('gadget-dark-mode', isOn ? '1' : '0');

}

function savePreferenceLocally() {

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 systemSchemeNow = matchMedia('(prefers-color-scheme: dark)').matches;

var systemSchemeLast = mw.storage.get('dark-mode-system-scheme') === '1';

if (systemSchemeNow !== systemSchemeLast) {

if (systemSchemeNow !== isOn) {

toggleMode();

}

mw.requestIdleCallback(function () {

mw.storage.set('dark-mode-system-scheme', systemSchemeNow ? '1' : '0');

});

}

}

$.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

var storageState = mw.storage.session.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);

}

}

});