MediaWiki:Common.js: Difference between revisions

No edit summary
No edit summary
Line 276: Line 276:
             translatableSpans.push(lspan);
             translatableSpans.push(lspan);
             a.appendChild(lspan);
             a.appendChild(lspan);
            a.addEventListener('mouseover', function () { this.style.opacity = '0.72'; });
            a.addEventListener('mouseout', function () { this.style.opacity = '1'; });
             return a;
             return a;
         }
         }
Line 307: Line 305:
         return a ? a.closest('.vector-toc-list-item') : null;
         return a ? a.closest('.vector-toc-list-item') : null;
     }
     }
    // Highlighting is class-only (styled in Common.css §6):
    //  .gr-toc-current  — the one entry being read, on the <a> itself
    //  .gr-toc-in-path  — its parent sections, marked on the <li>
    // Previously this set inline colours on the active item AND on every
    // element inside it, so a section with sub-sections lit up whole.
     function clearActiveToc() {
     function clearActiveToc() {
         var toc = document.querySelector('.vector-toc');
         var toc = document.querySelector('.vector-toc');
         if (!toc) return;
         if (!toc) return;
        var ACTIVE_COLOR = '#f57c00';
         toc.querySelectorAll('.gr-toc-current').forEach(function (el) { el.classList.remove('gr-toc-current'); });
         toc.querySelectorAll('.vector-toc-list-item').forEach(function (li) {
        toc.querySelectorAll('.gr-toc-in-path').forEach(function (el) { el.classList.remove('gr-toc-in-path'); });
            li.classList.remove('vector-toc-list-item-active');
            var lnk = li.querySelector('.vector-toc-link');
            if (!lnk) return;
            lnk.style.removeProperty('color');
            lnk.style.setProperty('font-weight', '400', 'important');
            lnk.querySelectorAll('*').forEach(function (el) { el.style.removeProperty('color'); el.style.setProperty('font-weight', '400', 'important'); });
        });
     }
     }
     function setActiveToc(id) {
     function setActiveToc(id) {
         var toc = document.querySelector('.vector-toc');
         var toc = document.querySelector('.vector-toc');
         if (!toc) return;
         if (!toc) return;
        var ACTIVE_COLOR = '#f57c00';
         if (_tocActiveId === id) return;
         if (_tocActiveId === id) return;
         _tocActiveId = id;
         _tocActiveId = id;
Line 330: Line 325:
         var li = getTocLi(id);
         var li = getTocLi(id);
         if (!li) return;
         if (!li) return;
         li.classList.add('vector-toc-list-item-active');
         // The entry itself — the link only, so sub-entries stay as they are.
         var hasActiveChild = !!li.querySelector('.vector-toc-list-item .vector-toc-list-item-active');
         var lnk = li.querySelector(':scope > .vector-toc-link') || li.querySelector('.vector-toc-link');
        if (!hasActiveChild) {
        if (lnk) lnk.classList.add('gr-toc-current');
            var lnk = li.querySelector('.vector-toc-link');
        // Its parent sections get the quieter "you are in here" treatment,
            if (lnk) {
         // and any collapsed ancestor is opened so the entry is visible.
                lnk.style.setProperty('color', ACTIVE_COLOR, 'important');
                lnk.style.setProperty('font-weight', '700', 'important');
                lnk.querySelectorAll('*').forEach(function (el) { el.style.setProperty('color', ACTIVE_COLOR, 'important'); el.style.setProperty('font-weight', '700', 'important'); });
            }
         }
         var anc = li.parentNode;
         var anc = li.parentNode;
         while (anc && anc !== toc) {
         while (anc && anc !== toc) {
             if (anc.classList) anc.classList.remove('vector-toc-list-item-collapsed');
             if (anc.classList) {
                anc.classList.remove('vector-toc-list-item-collapsed');
                if (anc.classList.contains('vector-toc-list-item')) anc.classList.add('gr-toc-in-path');
            }
             if (anc.tagName === 'UL' || anc.tagName === 'LI') anc.style.removeProperty('display');
             if (anc.tagName === 'UL' || anc.tagName === 'LI') anc.style.removeProperty('display');
             anc = anc.parentNode;
             anc = anc.parentNode;
Line 387: Line 380:
         if (!el) return;
         if (!el) return;
         var id = findNearestHeadingId(el);
         var id = findNearestHeadingId(el);
         if (id) setActiveToc(id);
         if (id) { _tocLockUntil = Date.now() + 700; setActiveToc(id); }
     };
     };
    // ── Which section am I reading? ────────────────────────────────
    // Was an IntersectionObserver with a shrunken root band. Two faults
    // came out of that, both reported as "the highlight is wrong":
    //  • right after jumping to a heading, that heading sits ABOVE the
    //    band, so nothing was "visible" and the fallback picked the
    //    PREVIOUS heading — clicking a sub-section highlighted its parent;
    //  • once every heading was outside the band, no intersection ever
    //    changed again, so the observer stopped firing and the highlight
    //    froze wherever it was.
    // This measures instead: the active section is the last heading above
    // a reading line just under the sticky toolbar. It updates on every
    // scroll frame and can't go stale.
    var _tocHeadings = [];
    var _tocTops = [];
    var _tocMeasuredHeight = 0;
    var _tocLockUntil = 0;
    function measureHeadings() {
        var y = window.pageYOffset || document.documentElement.scrollTop || 0;
        _tocTops = _tocHeadings.map(function (h) { return h.getBoundingClientRect().top + y; });
        _tocMeasuredHeight = document.documentElement.scrollHeight;
    }
    function readingLine() {
        var y = window.pageYOffset || document.documentElement.scrollTop || 0;
        var bar = document.getElementById('gr-static-bar');
        var barBottom = bar ? Math.max(bar.getBoundingClientRect().bottom, 0) : 0;
        return y + barBottom + 16;
    }
    var _lockRecheck = null;
    function updateActiveFromScroll() {
        if (!_tocHeadings.length) return;
        if (Date.now() < _tocLockUntil) {                // a click just chose one
            // Re-check once the lock lifts, in case this was the last
            // scroll event — otherwise the highlight would sit on the
            // clicked entry even after the reader has moved on.
            if (!_lockRecheck) {
                _lockRecheck = setTimeout(function () { _lockRecheck = null; updateActiveFromScroll(); }, _tocLockUntil - Date.now() + 30);
            }
            return;
        }
        // Interleaving a commentary or changing the font size moves things;
        // re-measure when the page's height has changed.
        if (document.documentElement.scrollHeight !== _tocMeasuredHeight) measureHeadings();
        var line = readingLine(), lo = 0, hi = _tocTops.length - 1, idx = 0;
        while (lo <= hi) {                                // last heading at or above the line
            var mid = (lo + hi) >> 1;
            if (_tocTops[mid] <= line + 1) { idx = mid; lo = mid + 1; } else { hi = mid - 1; }
        }
        setActiveToc(_tocHeadings[idx].id);
    }


     function attachHeadingObserver() {
     function attachHeadingObserver() {
         if (_isNoTocPage()) return;
         if (_isNoTocPage()) return;
         if (_headingObserver) return;
         if (_headingObserver) return;
        if (!window.IntersectionObserver) return;
         var content = document.querySelector('.mw-parser-output');
         var content = document.querySelector('.mw-parser-output');
         if (!content) return;
         if (!content) return;
         var headings = Array.from(content.querySelectorAll('h1[id],h2[id],h3[id],h4[id],h5[id],h6[id]'));
         _tocHeadings = Array.prototype.slice.call(content.querySelectorAll('h1[id],h2[id],h3[id],h4[id],h5[id],h6[id]'));
         if (!headings.length) return;
         if (!_tocHeadings.length) return;
         var toc = document.querySelector('.vector-toc');
         if (!document.querySelector('.vector-toc')) return;
        if (!toc) return;
         measureHeadings();
         var _visible = new Set();
         // Time-based throttle rather than a requestAnimationFrame latch: if
         _headingObserver = new IntersectionObserver(function (entries) {
        // one frame callback is ever skipped (background tab, a jump during
            entries.forEach(function (entry) {
        // load) a latch stays stuck and the highlight stops updating for
                if (entry.isIntersecting) _visible.add(entry.target.id);
        // good — which is one of the ways this went stale before.
                else _visible.delete(entry.target.id);
        var lastRun = 0, trailing = null;
            });
        function onScroll() {
            var topId = null, topY = Infinity;
            var now = Date.now();
            _visible.forEach(function (id) {
            if (now - lastRun >= 80) { lastRun = now; updateActiveFromScroll(); }
                var el = document.getElementById(id);
             else if (!trailing) {
                if (el) { var y = el.getBoundingClientRect().top; if (y >= 0 && y < topY) { topY = y; topId = id; } }
                 trailing = setTimeout(function () { trailing = null; lastRun = Date.now(); updateActiveFromScroll(); }, 80);
             });
            if (!topId) {
                 var bestY = -Infinity;
                headings.forEach(function (h) { var y = h.getBoundingClientRect().top; if (y < 0 && y > bestY) { bestY = y; topId = h.id; } });
             }
             }
             setActiveToc(topId || null);
        }
         }, { rootMargin: '-60px 0px -65% 0px', threshold: 0 });
        window.addEventListener('scroll', onScroll, { passive: true });
        headings.forEach(function (h) { _headingObserver.observe(h); });
        window.addEventListener('resize', function () { measureHeadings(); onScroll(); }, { passive: true });
        if (document.fonts && document.fonts.ready && document.fonts.ready.then) {
             document.fonts.ready.then(function () { measureHeadings(); onScroll(); });
         }
        _headingObserver = { onScroll: onScroll };      // marks "already wired"
        updateActiveFromScroll();
    }
 
    // Clicking an entry highlights it straight away, rather than waiting
    // for the scroll observer to catch up after the jump.
    function attachTocClickHandler(toc) {
        if (toc.getAttribute('data-gr-click-wired')) return;
        toc.setAttribute('data-gr-click-wired', '1');
        toc.addEventListener('click', function (e) {
            var a = e.target.closest ? e.target.closest('.vector-toc-link') : null;
            if (!a) return;
            var href = a.getAttribute('href') || '';
            if (href.charAt(0) !== '#') return;
            var id;
            try { id = decodeURIComponent(href.slice(1)); } catch (err) { id = href.slice(1); }
            if (id) { _tocLockUntil = Date.now() + 700; setActiveToc(id); }
        });
     }
     }


Line 428: Line 488:
         expandTocSections();
         expandTocSections();
         injectTocDocNav();
         injectTocDocNav();
        attachTocClickHandler(toc);
         attachHeadingObserver();
         attachHeadingObserver();
     }
     }