MediaWiki:Common.js

Revision as of 22:38, 28 July 2026 by Digiwayen (talk | contribs) (Light-theme JS rewrite)

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 );
            } );
        }
    } );

}() );