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.
/* ============================================================
ZelocoreCMS Wiki — MediaWiki:Common.js
Light-theme compatible enhancements
============================================================ */
/* global mw, $ */
( function () {
'use strict';
var qs = function ( s, c ) { return ( c || document ).querySelector( s ); };
var qsa = function ( s, c ) { return Array.prototype.slice.call( ( c || document ).querySelectorAll( s ) ); };
function ready( fn ) {
if ( document.readyState !== 'loading' ) { fn(); } else { document.addEventListener( 'DOMContentLoaded', fn ); }
}
/* ── 1. Reading Progress Bar ─────────────────────────── */
function initProgressBar() {
var bar = document.createElement( 'div' );
bar.id = 'zc-reading-progress';
Object.assign( bar.style, {
position: 'fixed', top: '0', left: '0', height: '3px', width: '0%',
background: '#3366cc', zIndex: '9999', transition: 'width 0.1s linear',
pointerEvents: 'none'
} );
document.body.appendChild( bar );
window.addEventListener( 'scroll', function () {
var doc = document.documentElement;
var pct = ( window.scrollY / ( doc.scrollHeight - window.innerHeight ) ) * 100;
bar.style.width = Math.min( pct, 100 ) + '%';
}, { passive: true } );
}
/* ── 2. Back-to-Top Button ───────────────────────────── */
function initBackToTop() {
var btn = document.createElement( 'button' );
btn.id = 'zc-back-to-top';
btn.textContent = '↑';
btn.title = 'Back to top';
Object.assign( btn.style, {
position: 'fixed', bottom: '24px', right: '24px',
width: '40px', height: '40px', borderRadius: '50%',
background: '#3366cc', border: 'none', color: '#fff',
fontSize: '1.1rem', fontWeight: '700', cursor: 'pointer',
opacity: '0', transition: 'opacity 0.25s ease, transform 0.25s ease',
boxShadow: '0 2px 8px rgba(0,0,0,0.25)', zIndex: '998', transform: 'scale(0.8)'
} );
document.body.appendChild( btn );
window.addEventListener( 'scroll', function () {
var show = window.scrollY > 400;
btn.style.opacity = show ? '1' : '0';
btn.style.transform = show ? 'scale(1)' : 'scale(0.8)';
}, { passive: true } );
btn.addEventListener( 'click', function () {
window.scrollTo( { top: 0, behavior: 'smooth' } );
} );
}
/* ── 3. Keyboard Shortcut: / or Ctrl+K focuses search ── */
function initSearchShortcut() {
var input = qs( '#searchInput, .cdx-text-input__input, input[name="search"]' );
if ( !input ) { return; }
document.addEventListener( 'keydown', function ( e ) {
var active = document.activeElement;
var isEditing = active && ( active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.isContentEditable );
if ( ( ( e.ctrlKey || e.metaKey ) && e.key === 'k' ) || ( e.key === '/' && !isEditing ) ) {
e.preventDefault();
input.focus();
input.select();
}
} );
}
/* ── 4. Code Block Copy Button ───────────────────────── */
function addCopyButtons() {
qsa( 'pre, .mw-highlight' ).forEach( function ( block ) {
if ( qs( '.zc-copy-btn', block ) ) { return; }
var btn = document.createElement( 'button' );
btn.className = 'zc-copy-btn';
btn.textContent = 'Copy';
Object.assign( btn.style, {
position: 'absolute', top: '6px', right: '6px',
background: '#f8f9fa', border: '1px solid #a2a9b1',
color: '#54595d', fontSize: '0.75rem', padding: '2px 10px',
borderRadius: '2px', cursor: 'pointer', fontFamily: 'inherit'
} );
btn.addEventListener( 'click', function () {
var text = block.textContent.replace( /^Copy$|^✓ Copied$/m, '' ).trim();
navigator.clipboard.writeText( text ).then( function () {
btn.textContent = '✓ Copied';
btn.style.color = '#14866d';
btn.style.borderColor = '#14866d';
setTimeout( function () {
btn.textContent = 'Copy';
btn.style.color = '#54595d';
btn.style.borderColor = '#a2a9b1';
}, 2000 );
} );
} );
block.style.position = 'relative';
block.appendChild( btn );
} );
}
/* ── 5. Scroll-reveal for elements ──────────────────── */
function initScrollReveal() {
if ( !window.IntersectionObserver ) { return; }
var obs = new IntersectionObserver( function ( entries ) {
entries.forEach( function ( entry ) {
if ( entry.isIntersecting ) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
obs.unobserve( entry.target );
}
} );
}, { threshold: 0.08 } );
qsa( '.mp-portal-cell' ).forEach( function ( el ) {
el.style.opacity = '0';
el.style.transform = 'translateY(12px)';
el.style.transition = 'opacity 0.4s ease, transform 0.4s ease';
obs.observe( el );
} );
}
/* ── 6. Smooth anchor scrolling ─────────────────────── */
function initSmoothScroll() {
qsa( 'a[href^="#"]' ).forEach( function ( a ) {
a.addEventListener( 'click', function ( e ) {
var id = decodeURIComponent( this.hash.slice( 1 ) );
var target = document.getElementById( id );
if ( !target ) { return; }
e.preventDefault();
window.scrollTo( {
top: target.getBoundingClientRect().top + window.scrollY - 70,
behavior: 'smooth'
} );
} );
} );
}
/* ── 7. TOC scroll-spy (highlight current heading) ──── */
function initTOCSpy() {
var headings = qsa( '.mw-body-content h2, .mw-body-content h3' );
var tocLinks = qsa( '#toc a, .toc a' );
if ( !headings.length || !tocLinks.length ) { return; }
var ticking = false;
window.addEventListener( 'scroll', function () {
if ( ticking ) { return; }
ticking = true;
requestAnimationFrame( function () {
var scrollY = window.scrollY + 80;
var current = '';
headings.forEach( function ( h ) {
if ( h.getBoundingClientRect().top + window.scrollY <= scrollY ) {
current = '#' + h.id;
}
} );
tocLinks.forEach( function ( link ) {
var isCurrent = link.getAttribute( 'href' ) === current;
link.style.fontWeight = isCurrent ? 'bold' : '';
link.style.color = isCurrent ? '#202122' : '';
} );
ticking = false;
} );
}, { passive: true } );
}
/* ── 8. Table row highlight ─────────────────────────── */
function initTableHighlight() {
qsa( '.wikitable tbody tr' ).forEach( function ( row ) {
row.addEventListener( 'mouseenter', function () {
qsa( 'td', this ).forEach( function ( td ) {
td.style.background = '#eaf3fb';
} );
} );
row.addEventListener( 'mouseleave', function () {
qsa( 'td', this ).forEach( function ( td ) {
td.style.background = '';
} );
} );
} );
}
/* ── 9. Auto external link icon ─────────────────────── */
function initExternalLinks() {
qsa( '.mw-body-content a.external' ).forEach( function ( a ) {
if ( !qs( '.ext-icon', a ) ) {
var icon = document.createElement( 'span' );
icon.className = 'ext-icon';
icon.setAttribute( 'aria-hidden', 'true' );
icon.textContent = ' ↗';
icon.style.cssText = 'font-size:0.75em; color:#72777d; margin-left:1px;';
a.appendChild( icon );
}
} );
}
/* ── 10. Sidebar active link highlight ──────────────── */
function initSidebarHighlight() {
var currentPath = window.location.pathname;
qsa( '#mw-panel a, .vector-menu-content-list a' ).forEach( function ( a ) {
if ( a.pathname && a.pathname !== '/' && currentPath === a.pathname ) {
a.style.fontWeight = 'bold';
a.style.color = '#202122';
}
} );
}
/* ── Bootstrap ──────────────────────────────────────── */
ready( function () {
initProgressBar();
initBackToTop();
initSearchShortcut();
initSmoothScroll();
initTOCSpy();
initSidebarHighlight();
initScrollReveal();
initTableHighlight();
initExternalLinks();
setTimeout( addCopyButtons, 400 );
// Re-run after VisualEditor deactivates
if ( mw && mw.hook ) {
mw.hook( 've.deactivate' ).add( function () {
setTimeout( addCopyButtons, 600 );
} );
}
} );
}() );