MediaWiki:Common.js: Difference between revisions

From LinuxizeNet
(Correct viewport comment: $wgVectorResponsive does not affect Vector legacy)
(Mount the theme toggle in Timeless's header (#personal-extra))
Line 82: Line 82:


function buildThemeToggle() {
function buildThemeToggle() {
var host = document.querySelector( '#p-personal .vector-menu-content-list' );
/* Mount points differ per skin, in preference order:
*  - Timeless: #personal-extra is the small icon strip in the header,
*    always visible. #p-personal's list is inside the user dropdown,
*    so a toggle there would be hidden behind a click.
*  - Vector legacy: the personal links row.
* Until this existed, the toggle simply never rendered under Timeless
* and dark mode was reachable only via prefers-color-scheme. */
var host = document.querySelector( '#personal-extra ul' ) ||
document.querySelector( '#p-personal .vector-menu-content-list' ) ||
document.querySelector( '#p-personal ul' );


if ( !host ) {
if ( !host || host.querySelector( '.lz-theme-toggle' ) ) {
return;
return;
}
}

Revision as of 20:28, 9 August 2026

/**
 * MediaWiki:Common.js — wiki.linuxize.net
 *
 * Three jobs, in priority order:
 *   1. Patch the viewport meta so the responsive CSS in Common.css can fire.
 *      This is a workaround. The correct fix is $wgVectorResponsive = true;
 *      in LocalSettings.php — delete section 1 once that is set.
 *   2. Add a language chip + copy button to every SyntaxHighlight block.
 *   3. Add a light/dark toggle that persists and matches the CSS tokens.
 */
( function () {
	'use strict';

	/* ---------------------------------------------------------------
	 * 1. Viewport — PERMANENT. Do not delete.
	 *
	 * Vector legacy emits <meta name="viewport" content="width=1000">,
	 * which makes phones render a zoomed-out desktop page and prevents
	 * every max-width media query from ever matching.
	 *
	 * $wgVectorResponsive does NOT fix this. Tested 2026-08-08 on
	 * MediaWiki 1.41.5 with Vector 1.0.0: with the setting enabled and
	 * placed after wfLoadSkin( 'Vector' ), ?useskin=vector still returns
	 * width=1000, while vector-2022, timeless, monobook and minerva all
	 * return width=device-width. The config does not affect the legacy
	 * skin in this version.
	 *
	 * So this patch is the only thing making the site usable on a phone
	 * while the default skin is Vector legacy. It can only be removed by
	 * switching the default skin — and that also means reworking the
	 * chrome selectors in Common.css section 2 and 3, which target the
	 * legacy DOM.
	 *
	 * Runs at parse time rather than on DOM ready, to minimise the
	 * reflow flash.
	 * ------------------------------------------------------------- */
	var viewport = document.querySelector( 'meta[name="viewport"]' );

	if ( viewport ) {
		viewport.setAttribute( 'content', 'width=device-width, initial-scale=1' );
	} else {
		viewport = document.createElement( 'meta' );
		viewport.name = 'viewport';
		viewport.content = 'width=device-width, initial-scale=1';
		document.head.appendChild( viewport );
	}

	/* ---------------------------------------------------------------
	 * 2. Theme
	 * Applied before first paint so there is no light flash on a dark
	 * device. 'auto' stores nothing and lets prefers-color-scheme win.
	 * ------------------------------------------------------------- */
	var STORAGE_KEY = 'lz-theme';

	function readTheme() {
		try {
			return window.localStorage.getItem( STORAGE_KEY );
		} catch ( e ) {
			return null;
		}
	}

	function applyTheme( theme ) {
		if ( theme === 'dark' || theme === 'light' ) {
			document.documentElement.setAttribute( 'data-theme', theme );
		} else {
			document.documentElement.removeAttribute( 'data-theme' );
		}
	}

	applyTheme( readTheme() );

	function currentTheme() {
		var stamped = document.documentElement.getAttribute( 'data-theme' );

		if ( stamped ) {
			return stamped;
		}

		return window.matchMedia( '(prefers-color-scheme: dark)' ).matches ? 'dark' : 'light';
	}

	function buildThemeToggle() {
		/* Mount points differ per skin, in preference order:
		 *   - Timeless: #personal-extra is the small icon strip in the header,
		 *     always visible. #p-personal's list is inside the user dropdown,
		 *     so a toggle there would be hidden behind a click.
		 *   - Vector legacy: the personal links row.
		 * Until this existed, the toggle simply never rendered under Timeless
		 * and dark mode was reachable only via prefers-color-scheme. */
		var host = document.querySelector( '#personal-extra ul' ) ||
			document.querySelector( '#p-personal .vector-menu-content-list' ) ||
			document.querySelector( '#p-personal ul' );

		if ( !host || host.querySelector( '.lz-theme-toggle' ) ) {
			return;
		}

		var li = document.createElement( 'li' );
		var btn = document.createElement( 'button' );

		btn.type = 'button';
		btn.className = 'lz-theme-toggle';
		btn.setAttribute( 'aria-live', 'polite' );

		function label() {
			btn.textContent = currentTheme() === 'dark' ? 'light' : 'dark';
			btn.setAttribute( 'aria-label', 'Switch to ' + btn.textContent + ' theme' );
		}

		label();

		btn.addEventListener( 'click', function () {
			var next = currentTheme() === 'dark' ? 'light' : 'dark';

			applyTheme( next );

			try {
				window.localStorage.setItem( STORAGE_KEY, next );
			} catch ( e ) {
				/* private browsing — the choice just will not persist */
			}

			label();
		} );

		li.appendChild( btn );
		host.appendChild( li );
	}

	/* ---------------------------------------------------------------
	 * 3. Code block chrome
	 * SyntaxHighlight emits:
	 *   <div class="mw-highlight mw-highlight-lang-bash"><pre>…</pre></div>
	 * so the language is recoverable from the class name.
	 * ------------------------------------------------------------- */
	function buildCodeChrome() {
		var blocks = document.querySelectorAll( '#mw-content-text .mw-highlight' );

		Array.prototype.forEach.call( blocks, function ( block ) {
			if ( block.querySelector( '.lz-chead' ) ) {
				return;
			}

			var pre = block.querySelector( 'pre' );

			if ( !pre ) {
				return;
			}

			var match = /mw-highlight-lang-([a-z0-9+#-]+)/i.exec( block.className );
			var head = document.createElement( 'div' );
			var lang = document.createElement( 'span' );
			var copy = document.createElement( 'button' );

			head.className = 'lz-chead';

			lang.className = 'lz-lang';
			lang.textContent = match ? match[ 1 ] : 'text';

			copy.type = 'button';
			copy.className = 'lz-copy';
			copy.textContent = 'Copy';

			copy.addEventListener( 'click', function () {
				var text = pre.innerText;

				function done() {
					copy.textContent = 'Copied';
					copy.classList.add( 'lz-done' );
					window.setTimeout( function () {
						copy.textContent = 'Copy';
						copy.classList.remove( 'lz-done' );
					}, 1600 );
				}

				if ( navigator.clipboard && window.isSecureContext ) {
					navigator.clipboard.writeText( text ).then( done, function () {
						copy.textContent = 'Failed';
					} );
					return;
				}

				// Fallback for non-secure contexts.
				var ta = document.createElement( 'textarea' );

				ta.value = text;
				ta.setAttribute( 'readonly', '' );
				ta.style.position = 'absolute';
				ta.style.left = '-9999px';
				document.body.appendChild( ta );
				ta.select();

				try {
					document.execCommand( 'copy' );
					done();
				} catch ( e ) {
					copy.textContent = 'Failed';
				}

				document.body.removeChild( ta );
			} );

			head.appendChild( lang );
			head.appendChild( copy );
			block.insertBefore( head, pre );
		} );
	}

	/* ------------------------------------------------------------- */
	if ( window.mw && mw.hook ) {
		mw.hook( 'wikipage.content' ).add( buildCodeChrome );
		$( buildThemeToggle );
	} else {
		document.addEventListener( 'DOMContentLoaded', function () {
			buildCodeChrome();
			buildThemeToggle();
		} );
	}
}() );