MediaWiki:Common.js: Difference between revisions

From LinuxizeNet
(Site JS: viewport patch, code copy buttons, theme toggle)
 
(Correct viewport comment: $wgVectorResponsive does not affect Vector legacy)
Line 13: Line 13:


/* ---------------------------------------------------------------
/* ---------------------------------------------------------------
* 1. Viewport
* 1. Viewport — PERMANENT. Do not delete.
*
* Vector legacy emits <meta name="viewport" content="width=1000">,
* Vector legacy emits <meta name="viewport" content="width=1000">,
* which makes phones render a zoomed-out desktop page and prevents
* which makes phones render a zoomed-out desktop page and prevents
* every max-width media query from ever matching.
* 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
* Runs at parse time rather than on DOM ready, to minimise the
* reflow flash. Delete this block after setting $wgVectorResponsive.
* reflow flash.
* ------------------------------------------------------------- */
* ------------------------------------------------------------- */
var viewport = document.querySelector( 'meta[name="viewport"]' );
var viewport = document.querySelector( 'meta[name="viewport"]' );

Revision as of 05:50, 8 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() {
		var host = document.querySelector( '#p-personal .vector-menu-content-list' );

		if ( !host ) {
			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();
		} );
	}
}() );