Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

MediaWiki:Citizen.js

MediaWiki interface page
Revision as of 12:34, 12 August 2026 by Robo (talk | contribs) (Bot: careui phase 1 — stop loading webfonts from JS (now @font-face in Common.css); fix copy-button closure bug where every button copied the last code block - Claude)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
 * OHC Network Wiki — Citizen skin site JS (core mechanism, no Gadgets ext).
 * Features: copy-code, search hotkey, back-to-top.
 * Guarded with window flags so the matching ohc-* gadgets (if enabled later
 * via MediaWiki:Gadgets-definition) no-op instead of double-applying.
 *
 * Fonts are NOT loaded here any more. They are declared as @font-face in
 * MediaWiki:Common.css (self-hosted variable Figtree / Geist Mono), so the
 * browser discovers them while parsing CSS instead of after a script round
 * trip — the old mw.loader.load() approach guaranteed a flash of fallback
 * text, and in practice the webfonts were often not applied at all.
 */
( function () {
	'use strict';


	/* 1. Copy-code -------------------------------------------- */
	function copyText( text, btn ) {
		var reset = function () {
			btn.textContent = 'Copy';
			btn.classList.remove( 'ohc-copied' );
		};
		var done = function () {
			btn.textContent = 'Copied';
			btn.classList.add( 'ohc-copied' );
			setTimeout( reset, 1600 );
		};
		var fail = function () {
			btn.textContent = 'Failed';
			setTimeout( reset, 1600 );
		};
		function fallbackCopy() {
			var ta = document.createElement( 'textarea' );
			ta.value = text;
			ta.style.cssText = 'position:fixed;left:-9999px;top:0;';
			document.body.appendChild( ta );
			ta.select();
			var ok = false;
			try { ok = document.execCommand( 'copy' ); } catch ( e ) {}
			document.body.removeChild( ta );
			if ( ok ) { done(); } else { fail(); }
		}
		if ( navigator.clipboard && navigator.clipboard.writeText ) {
			navigator.clipboard.writeText( text ).then( done, fallbackCopy );
		} else {
			fallbackCopy();
		}
	}

	function addCopyButtons( container ) {
		var pres = container.querySelectorAll( 'pre' );
		// `let`, not `var`: with `var` the click handlers all closed over the
		// final loop values, so every Copy button copied the last code block
		// on the page.
		for ( let i = 0; i < pres.length; i++ ) {
			const pre = pres[ i ];
			if ( pre.classList.contains( 'ohc-copy-code' ) ) {
				continue;
			}
			const btn = document.createElement( 'button' );
			btn.type = 'button';
			btn.className = 'ohc-copy-btn';
			btn.textContent = 'Copy';
			btn.setAttribute( 'aria-label', 'Copy code to clipboard' );
			btn.addEventListener( 'click', function () {
				copyText( pre.textContent.replace( /\n$/, '' ), btn );
			} );
			pre.classList.add( 'ohc-copy-code' );
			pre.appendChild( btn );
		}
	}

	/* 2. Search hotkey ---------------------------------------- */
	window.ohcSearchHotkeyLoaded = window.ohcSearchHotkeyLoaded || false;
	function bindSearchHotkey() {
		if ( window.ohcSearchHotkeyLoaded ) { return; }
		window.ohcSearchHotkeyLoaded = true;
		document.addEventListener( 'keydown', function ( e ) {
			if ( e.key !== '/' && e.key !== '?' ) { return; }
			var t = e.target;
			if ( t && ( t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable ) ) {
				return;
			}
			e.preventDefault();
			var input =
				document.querySelector( '#searchInput' ) ||
				document.querySelector( '.citizen-search input[type="search"]' ) ||
				document.querySelector( 'input[type="search"]' );
			if ( input ) {
				input.focus();
				if ( input.select ) { input.select(); }
			}
		} );
	}

	/* 3. Back to top ------------------------------------------ */
	window.ohcBackToTopLoaded = window.ohcBackToTopLoaded || false;
	function initBackToTop() {
		if ( window.ohcBackToTopLoaded || document.querySelector( '.ohc-to-top' ) ) { return; }
		window.ohcBackToTopLoaded = true;
		var btn = document.createElement( 'button' );
		btn.type = 'button';
		btn.className = 'ohc-to-top';
		btn.setAttribute( 'aria-label', 'Back to top' );
		btn.innerHTML = '&uarr;';
		btn.addEventListener( 'click', function () {
			window.scrollTo( { top: 0, behavior: 'smooth' } );
		} );
		document.body.appendChild( btn );
		var toggle = function () {
			if ( window.scrollY > 600 ) {
				btn.classList.add( 'ohc-to-top--show' );
			} else {
				btn.classList.remove( 'ohc-to-top--show' );
			}
		};
		toggle();
		window.addEventListener( 'scroll', toggle, { passive: true } );
	}

	addCopyButtons( document );
	mw.hook( 'wikipage.content' ).add( addCopyButtons );
	bindSearchHotkey();
	initBackToTop();
}() );