MediaWiki:Common.js: Difference between revisions
Appearance
ZelocoreCMS JS enhancements — initial deploy |
Light-theme JS rewrite |
||
| Line 1: | Line 1: | ||
/* ============================================================ | /* ============================================================ | ||
ZelocoreCMS Wiki — MediaWiki:Common.js | ZelocoreCMS Wiki — MediaWiki:Common.js | ||
Light-theme compatible enhancements | |||
============================================================ */ | ============================================================ */ | ||
/* global mw, $ */ | |||
( function () { | ( function () { | ||
'use strict'; | '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 | |||
function ready( fn ) { | |||
if ( document.readyState !== 'loading' ) { fn(); } else { document.addEventListener( 'DOMContentLoaded', fn ); } | |||
} | } | ||
/* ── | /* ── 1. Reading Progress Bar ─────────────────────────── */ | ||
function | function initProgressBar() { | ||
var | var bar = document.createElement( 'div' ); | ||
bar.id = 'zc-reading-progress'; | |||
Object.assign( bar.style, { | |||
position: 'fixed', top: '0', left: '0', height: '3px', width: '0%', | |||
function | background: '#3366cc', zIndex: '9999', transition: 'width 0.1s linear', | ||
pointerEvents: 'none' | |||
var | } ); | ||
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 } ); | |||
} | } | ||
function | /* ── 2. Back-to-Top Button ───────────────────────────── */ | ||
var | 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. | /* ── 3. Keyboard Shortcut: / or Ctrl+K focuses search ── */ | ||
function | function initSearchShortcut() { | ||
var input = qs( '#searchInput, .cdx-text-input__input, input[name="search"]' ); | var input = qs( '#searchInput, .cdx-text-input__input, input[name="search"]' ); | ||
if ( !input ) return; | if ( !input ) { return; } | ||
document.addEventListener( 'keydown', function ( e ) { | |||
var active = document.activeElement; | |||
document.addEventListener( 'keydown', function( e ) { | var isEditing = active && ( active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.isContentEditable ); | ||
if ( ( e.ctrlKey || e.metaKey ) && e.key === 'k' ) { | if ( ( ( e.ctrlKey || e.metaKey ) && e.key === 'k' ) || ( e.key === '/' && !isEditing ) ) { | ||
e.preventDefault(); | e.preventDefault(); | ||
input.focus(); | input.focus(); | ||
input.select(); | input.select(); | ||
} | } | ||
} ); | } ); | ||
} | } | ||
/* ── 4 | /* ── 4. Code Block Copy Button ───────────────────────── */ | ||
function addCopyButtons() { | |||
qsa( 'pre, .mw-highlight' ).forEach( function ( block ) { | |||
if ( qs( '.zc-copy-btn', block ) ) { return; } | |||
function | |||
qsa( 'pre, .mw-highlight' ).forEach( function( block ) { | |||
if ( qs( '.zc-copy-btn', block ) ) return; | |||
var btn = document.createElement( 'button' ); | var btn = document.createElement( 'button' ); | ||
btn.className | btn.className = 'zc-copy-btn'; | ||
btn.textContent = ' | btn.textContent = 'Copy'; | ||
btn.style | 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'; | ||
btn.addEventListener( 'click', function() { | |||
var text = block.textContent.replace( / | |||
navigator.clipboard.writeText( text ).then( function() { | |||
btn.textContent | |||
btn.style.color | |||
btn.style.borderColor = ' | |||
setTimeout( function() { | |||
btn.textContent | |||
btn.style.color | |||
btn.style.borderColor = ' | |||
}, 2000 ); | }, 2000 ); | ||
}); | } ); | ||
}); | } ); | ||
block.style.position = 'relative'; | block.style.position = 'relative'; | ||
block.appendChild( btn ); | 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 | /* ── 6. Smooth anchor scrolling ─────────────────────── */ | ||
function initSmoothScroll() { | function initSmoothScroll() { | ||
qsa( 'a[href^="#"]' ).forEach( function( a ) { | qsa( 'a[href^="#"]' ).forEach( function ( a ) { | ||
a.addEventListener( 'click', function( e ) { | a.addEventListener( 'click', function ( e ) { | ||
var | var id = decodeURIComponent( this.hash.slice( 1 ) ); | ||
if ( !target ) return; | var target = document.getElementById( id ); | ||
if ( !target ) { return; } | |||
e.preventDefault(); | e.preventDefault(); | ||
window.scrollTo( { | window.scrollTo( { | ||
top: | top: target.getBoundingClientRect().top + window.scrollY - 70, | ||
behavior: 'smooth' | behavior: 'smooth' | ||
} ); | } ); | ||
}); | } ); | ||
}); | } ); | ||
} | } | ||
/* ── 7. TOC | /* ── 7. TOC scroll-spy (highlight current heading) ──── */ | ||
function initTOCSpy() { | function initTOCSpy() { | ||
var headings = qsa( '.mw-body-content h2, .mw-body-content h3' ); | var headings = qsa( '.mw-body-content h2, .mw-body-content h3' ); | ||
var | var tocLinks = qsa( '#toc a, .toc a' ); | ||
if ( !headings.length || ! | if ( !headings.length || !tocLinks.length ) { return; } | ||
var ticking = false; | |||
window.addEventListener( 'scroll', | 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 ) { | |||
link.style. | var isCurrent = link.getAttribute( 'href' ) === current; | ||
link.style. | link.style.fontWeight = isCurrent ? 'bold' : ''; | ||
} | link.style.color = isCurrent ? '#202122' : ''; | ||
}); | } ); | ||
}, | ticking = false; | ||
} ); | |||
}, { passive: true } ); | |||
} | } | ||
/* ── 8. | /* ── 8. Table row highlight ─────────────────────────── */ | ||
function | function initTableHighlight() { | ||
qsa( '. | 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. | /* ── 9. Auto external link icon ─────────────────────── */ | ||
function | function initExternalLinks() { | ||
qsa( '. | 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. | /* ── 10. Sidebar active link highlight ──────────────── */ | ||
function | function initSidebarHighlight() { | ||
var | 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 () { | |||
/* ── Bootstrap | |||
ready( function() { | |||
initProgressBar(); | initProgressBar(); | ||
initBackToTop(); | initBackToTop(); | ||
initSearchShortcut(); | |||
initSmoothScroll(); | initSmoothScroll(); | ||
initTOCSpy(); | initTOCSpy(); | ||
initSidebarHighlight(); | |||
initScrollReveal(); | |||
initTableHighlight(); | |||
initExternalLinks(); | |||
setTimeout( addCopyButtons, 400 ); | |||
// Re-run after VisualEditor deactivates | |||
if ( mw && mw.hook ) { | |||
mw.hook( 've.deactivate' ).add( function () { | |||
// Re-run | setTimeout( addCopyButtons, 600 ); | ||
mw.hook( 've.deactivate' ).add( function() { | } ); | ||
} | |||
} ); | |||
}); | |||
}() ); | }() ); | ||
Revision as of 22:38, 28 July 2026
/* ============================================================
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 );
} );
}
} );
}() );