MediaWiki:Common.js
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.
/* EDFM: small, purely presentational enhancements. None are required
* for the site to function -- all degrade gracefully with JS off -- and
* none are achievable in pure CSS, which is why they exist here instead
* of in MediaWiki:Common.css. Kept deliberately tiny: no framework, no
* build step, vanilla DOM APIs only.
*/
( function () {
'use strict';
/* 1. Eyebrow placement (see Template:Article header, .edfm-eyebrow in
* Common.css). MediaWiki always renders the real #firstHeading (the
* page's H1) before the article body in the DOM -- there is no
* template or CSS hook that lets wikitext content appear earlier than
* that. The eyebrow is authored as ordinary wikitext at the top of the
* article; this just moves that already-rendered, already-accessible
* element to sit visually above the title instead of below it. If this
* script doesn't run for any reason, the eyebrow still renders --
* directly under the title rather than above it -- so nothing breaks,
* this only affects placement.
*/
function placeEyebrow() {
var eyebrow = document.querySelector( '.edfm-eyebrow' );
var heading = document.getElementById( 'firstHeading' );
if ( eyebrow && heading && heading.parentNode ) {
heading.parentNode.insertBefore( eyebrow, heading );
}
}
/* 2. "You are here" sidebar highlighting. MediaWiki's default sidebar
* has no built-in current-page indication -- this compares each
* sidebar link's URL to the current page and flags the match so
* Common.css can style it (see .edfm-nav-current). Wrapped in try/catch
* per-link only so one malformed href can't stop the rest from being
* checked.
*/
function highlightCurrentNavLink() {
var links = document.querySelectorAll( '#mw-panel .vector-menu-content-list a[href]' );
/* Compare pathname *and* query string, not just pathname. Most
* content pages use clean /wiki/Page_Name URLs where pathname alone
* is unambiguous, but action views (edit, history, diff, page
* info, ...) all route through the same /index.php pathname and
* are only distinguished by their query string (?action=edit vs
* ?action=info etc.) -- comparing pathname only would wrongly
* highlight every sidebar link that happens to also point at
* /index.php, e.g. "Page information" while actually viewing the
* edit-source screen.
*/
var here = window.location.pathname + window.location.search;
for ( var i = 0; i < links.length; i++ ) {
var a = links[ i ];
try {
var target = new URL( a.href, window.location.href );
if ( target.pathname + target.search === here ) {
a.parentNode.classList.add( 'edfm-nav-current' );
}
} catch ( e ) {
/* malformed href on this one link -- skip it, not fatal */
}
}
}
/* 3. Jump-nav label shortening (2026-08-08, Engineer article layout
* pass). MediaWiki's native table of contents always mirrors a
* section's real ==Heading== text exactly -- there's no template or
* markup mechanism for a heading to carry a separate "short nav
* label". Article headings stay full and descriptive (e.g. "How to
* Receive an Invitation") since that's what reads well in the article
* body; this only shortens the *jump-nav tile* text afterwards, so the
* dense tile grid doesn't force long headings to wrap across two
* lines. Keyed by exact heading text, shared across all Engineer
* articles (same section names throughout, see Template:New engineer
* page) rather than being specific to any one page. Anything not in
* this list is left exactly as MediaWiki rendered it -- this only ever
* shortens, never invents nav text for a heading it doesn't recognise.
*/
var TOC_SHORT_LABELS = {
'How to Discover This Engineer': 'Discovery',
'How to Receive an Invitation': 'Invitation',
'How to Unlock This Engineer': 'Unlock',
'Engineer Unlock Chain': 'Unlock Chain',
'Available Engineering': 'Engineering',
'Experimental Effects': 'Experimentals',
'Increasing Engineer Reputation': 'Reputation',
'Recommended Blueprint to Pin': 'Blueprint Pinning'
};
function shortenTocLabels() {
var toctexts = document.querySelectorAll( '#toc .toctext, .toc .toctext' );
for ( var i = 0; i < toctexts.length; i++ ) {
var el = toctexts[ i ];
var full = el.textContent.trim();
if ( Object.prototype.hasOwnProperty.call( TOC_SHORT_LABELS, full ) ) {
el.textContent = TOC_SHORT_LABELS[ full ];
}
}
}
/* 4. Desktop top-bar clocks. RuneScape.wiki has a compact utility strip
* in the personal-toolbar tier; EDFM only needs time readouts there.
* UTC is synced once from MediaWiki's API curtimestamp (server-side UTC)
* and then advanced locally each second; local time uses the visitor's
* browser timezone. If the API request fails, the UTC clock falls back to
* the browser clock in UTC rather than leaving a blank control.
*/
function initTopBarClocks() {
var personal = document.getElementById( 'p-personal' );
if ( !personal || document.querySelector( '.edfm-topbar-clocks' ) ) {
return;
}
var clocks = document.createElement( 'div' );
clocks.className = 'edfm-topbar-clocks';
clocks.setAttribute( 'aria-label', 'Current time' );
var utcClock = document.createElement( 'span' );
utcClock.className = 'edfm-topbar-clock edfm-topbar-clock-utc';
utcClock.innerHTML = '<span class="edfm-topbar-clock-label">UTC</span> <time>--:--</time>';
var localClock = document.createElement( 'span' );
localClock.className = 'edfm-topbar-clock edfm-topbar-clock-local';
localClock.innerHTML = '<span class="edfm-topbar-clock-label">Local</span> <time>--:--</time>';
clocks.appendChild( utcClock );
clocks.appendChild( localClock );
personal.insertBefore( clocks, personal.firstChild );
var utcOffset = 0;
var utcTime = utcClock.querySelector( 'time' );
var localTime = localClock.querySelector( 'time' );
function twoDigits( number ) {
return String( number ).padStart( 2, '0' );
}
function render() {
var localNow = new Date();
var utcNow = new Date( Date.now() + utcOffset );
var utcText = twoDigits( utcNow.getUTCHours() ) + ':' + twoDigits( utcNow.getUTCMinutes() );
var localText = twoDigits( localNow.getHours() ) + ':' + twoDigits( localNow.getMinutes() );
utcTime.textContent = utcText;
utcTime.setAttribute( 'datetime', utcNow.toISOString() );
utcTime.setAttribute( 'title', 'Server time (UTC): ' + utcNow.toUTCString() );
localTime.textContent = localText;
localTime.setAttribute( 'datetime', localNow.toISOString() );
localTime.setAttribute( 'title', 'Your local time: ' + localNow.toLocaleString() );
}
function syncServerTime() {
if ( !window.fetch || !window.mw || !mw.util ) {
render();
return;
}
var apiUrl = mw.util.wikiScript( 'api' ) + '?action=query&format=json&curtimestamp=1';
fetch( apiUrl, { credentials: 'same-origin' } )
.then( function ( response ) { return response.json(); } )
.then( function ( data ) {
if ( data && data.curtimestamp ) {
utcOffset = Date.parse( data.curtimestamp ) - Date.now();
}
render();
} )
.catch( render );
}
syncServerTime();
window.setInterval( render, 1000 );
}
/* 5. Mobile app shell. Vector legacy was built for desktop; this adds a
* small progressive-enhancement shell on phones: hamburger drawer using
* the real #mw-panel links, a search button that reveals/focuses the
* existing MediaWiki search input, ESC/scrim close behavior, and ARIA
* state updates. With JS off the CSS fallback remains the existing stacked
* Vector mobile layout.
*/
function initMobileChrome() {
var navigation = document.getElementById( 'mw-navigation' );
var panel = document.getElementById( 'mw-panel' );
var searchInput = document.getElementById( 'searchInput' );
if ( !navigation || !panel || document.querySelector( '.edfm-mobile-appbar' ) ) {
return;
}
var appbar = document.createElement( 'div' );
appbar.className = 'edfm-mobile-appbar';
var menuButton = document.createElement( 'button' );
menuButton.type = 'button';
menuButton.className = 'edfm-mobile-menu-button';
menuButton.setAttribute( 'aria-label', 'Open navigation menu' );
menuButton.setAttribute( 'aria-controls', 'mw-panel' );
menuButton.setAttribute( 'aria-expanded', 'false' );
var brand = document.createElement( 'a' );
brand.className = 'edfm-mobile-brand';
brand.href = mw && mw.util ? mw.util.getUrl( 'Main Page' ) : '/wiki/Main_Page';
brand.textContent = 'Field Manual';
var searchButton = document.createElement( 'button' );
searchButton.type = 'button';
searchButton.className = 'edfm-mobile-search-button';
searchButton.setAttribute( 'aria-label', 'Search the Field Manual' );
searchButton.setAttribute( 'aria-expanded', 'false' );
appbar.appendChild( menuButton );
appbar.appendChild( brand );
appbar.appendChild( searchButton );
navigation.insertBefore( appbar, navigation.firstChild );
var closeButton = document.createElement( 'button' );
closeButton.type = 'button';
closeButton.className = 'edfm-mobile-close-button';
closeButton.setAttribute( 'aria-label', 'Close navigation menu' );
panel.insertBefore( closeButton, panel.firstChild );
var scrim = document.createElement( 'button' );
scrim.type = 'button';
scrim.className = 'edfm-mobile-scrim';
scrim.setAttribute( 'aria-label', 'Close navigation menu' );
document.body.appendChild( scrim );
function setMenuOpen( open ) {
document.body.classList.toggle( 'edfm-mobile-menu-open', open );
menuButton.setAttribute( 'aria-expanded', open ? 'true' : 'false' );
}
function setSearchOpen( open ) {
document.body.classList.toggle( 'edfm-mobile-search-open', open );
searchButton.setAttribute( 'aria-expanded', open ? 'true' : 'false' );
if ( open && searchInput ) {
window.setTimeout( function () { searchInput.focus(); }, 0 );
}
}
menuButton.addEventListener( 'click', function () { setMenuOpen( true ); } );
closeButton.addEventListener( 'click', function () { setMenuOpen( false ); } );
scrim.addEventListener( 'click', function () { setMenuOpen( false ); } );
searchButton.addEventListener( 'click', function () {
setSearchOpen( !document.body.classList.contains( 'edfm-mobile-search-open' ) );
} );
panel.addEventListener( 'click', function ( event ) {
if ( event.target && event.target.closest && event.target.closest( 'a' ) ) {
setMenuOpen( false );
}
} );
document.addEventListener( 'keydown', function ( event ) {
if ( event.key === 'Escape' ) {
setMenuOpen( false );
setSearchOpen( false );
}
} );
}
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', function () {
placeEyebrow();
highlightCurrentNavLink();
shortenTocLabels();
initTopBarClocks();
initMobileChrome();
} );
} else {
placeEyebrow();
highlightCurrentNavLink();
shortenTocLabels();
initTopBarClocks();
initMobileChrome();
}
}() );