MediaWiki:Common.js: Difference between revisions

Undo revision 7268 by Vaishnavi (talk)
Tag: Undo
No edit summary
Line 289: Line 289:


     var _headingObserver = null;
     var _headingObserver = null;
    var _tocActiveId = null;
    // NEW: moved out of attachHeadingObserver's own local scope so they
    // can be reused both by the scroll observer AND by a direct trigger
    // for search-result clicks — was previously only reachable from
    // inside attachHeadingObserver's own IntersectionObserver callback.
    // _tocActiveId (was _activeId, local to attachHeadingObserver) is
    // module-level now for the same reason — both callers need to share
    // the same "what's currently active" state, not track it separately.
    function getTocLink(id) {
        var toc = document.querySelector('.vector-toc');
        if (!toc) return null;
        return toc.querySelector('a[href="#' + CSS.escape(id) + '"]');
    }
    function getTocLi(id) {
        var a = getTocLink(id);
        return a ? a.closest('.vector-toc-list-item') : null;
    }
    function clearActiveToc() {
        var toc = document.querySelector('.vector-toc');
        if (!toc) return;
        var ACTIVE_COLOR = '#f57c00';
        toc.querySelectorAll('.vector-toc-list-item').forEach(function (li) {
            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) {
        var toc = document.querySelector('.vector-toc');
        if (!toc) return;
        var ACTIVE_COLOR = '#f57c00';
        if (_tocActiveId === id) return;
        _tocActiveId = id;
        clearActiveToc();
        if (!id) return;
        var li = getTocLi(id);
        if (!li) return;
        li.classList.add('vector-toc-list-item-active');
        var hasActiveChild = !!li.querySelector('.vector-toc-list-item .vector-toc-list-item-active');
        if (!hasActiveChild) {
            var lnk = li.querySelector('.vector-toc-link');
            if (lnk) {
                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;
        while (anc && anc !== toc) {
            if (anc.classList) anc.classList.remove('vector-toc-list-item-collapsed');
            if (anc.tagName === 'UL' || anc.tagName === 'LI') anc.style.removeProperty('display');
            anc = anc.parentNode;
        }
        var sticky = document.querySelector('.vector-sticky-pinned-container');
        var scrollEl = sticky || toc;
        if (scrollEl.scrollHeight > scrollEl.clientHeight) {
            var lr = li.getBoundingClientRect();
            var cr = scrollEl.getBoundingClientRect();
            if (lr.top < cr.top + 8 || lr.bottom > cr.bottom - 8) {
                scrollEl.scrollTop += lr.top - cr.top - scrollEl.clientHeight / 2 + li.offsetHeight / 2;
            }
        }
    }
    // NEW: given any element in the article content, finds the nearest
    // heading (with an id) that precedes it in document order, using
    // compareDocumentPosition — robust regardless of nesting depth.
    // Verified against a realistic multi-section fragment, including
    // the "before any heading" and "right after a chapter heading, no
    // sub-heading yet" edge cases, before wiring this in.
    function findNearestHeadingId(target) {
        var content = document.querySelector('.mw-parser-output');
        if (!content) return null;
        var headings = content.querySelectorAll('h1[id],h2[id],h3[id],h4[id],h5[id],h6[id]');
        var nearest = null;
        for (var i = 0; i < headings.length; i++) {
            var h = headings[i];
            var pos = h.compareDocumentPosition(target);
            if (pos & Node.DOCUMENT_POSITION_FOLLOWING) {
                nearest = h.id;
            } else {
                break;
            }
        }
        return nearest;
    }
    // NEW: exposed so the search-highlight IIFE (below) can directly
    // trigger the TOC to expand and highlight the right section right
    // after a search-result scroll lands — rather than relying solely on
    // the IntersectionObserver to catch up on its own timing.
    window.grHighlightTocForElement = function (el) {
        if (!el) return;
        var id = findNearestHeadingId(el);
        if (id) setActiveToc(id);
    };


     function attachHeadingObserver() {
     function attachHeadingObserver() {
Line 294: Line 394:
         if (_headingObserver) return;
         if (_headingObserver) return;
         if (!window.IntersectionObserver) return;
         if (!window.IntersectionObserver) return;
        var ACTIVE_COLOR = '#f57c00';
        var _activeId = null;
         var content = document.querySelector('.mw-parser-output');
         var content = document.querySelector('.mw-parser-output');
         if (!content) return;
         if (!content) return;
Line 302: Line 400:
         var toc = document.querySelector('.vector-toc');
         var toc = document.querySelector('.vector-toc');
         if (!toc) return;
         if (!toc) return;
        function getTocLink(id) { return toc.querySelector('a[href="#' + CSS.escape(id) + '"]'); }
        function getTocLi(id) { var a = getTocLink(id); return a ? a.closest('.vector-toc-list-item') : null; }
        function clearActive() {
            toc.querySelectorAll('.vector-toc-list-item').forEach(function (li) {
                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 setActive(id) {
            if (_activeId === id) return;
            _activeId = id;
            clearActive();
            if (!id) return;
            var li = getTocLi(id);
            if (!li) return;
            li.classList.add('vector-toc-list-item-active');
            var hasActiveChild = !!li.querySelector('.vector-toc-list-item .vector-toc-list-item-active');
            if (!hasActiveChild) {
                var lnk = li.querySelector('.vector-toc-link');
                if (lnk) {
                    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;
            while (anc && anc !== toc) {
                if (anc.classList) anc.classList.remove('vector-toc-list-item-collapsed');
                if (anc.tagName === 'UL' || anc.tagName === 'LI') anc.style.removeProperty('display');
                anc = anc.parentNode;
            }
            var sticky = document.querySelector('.vector-sticky-pinned-container');
            var scrollEl = sticky || toc;
            if (scrollEl.scrollHeight > scrollEl.clientHeight) {
                var lr = li.getBoundingClientRect();
                var cr = scrollEl.getBoundingClientRect();
                if (lr.top < cr.top + 8 || lr.bottom > cr.bottom - 8) {
                    scrollEl.scrollTop += lr.top - cr.top - scrollEl.clientHeight / 2 + li.offsetHeight / 2;
                }
            }
        }
         var _visible = new Set();
         var _visible = new Set();
         _headingObserver = new IntersectionObserver(function (entries) {
         _headingObserver = new IntersectionObserver(function (entries) {
Line 362: Line 415:
                 headings.forEach(function (h) { var y = h.getBoundingClientRect().top; if (y < 0 && y > bestY) { bestY = y; topId = h.id; } });
                 headings.forEach(function (h) { var y = h.getBoundingClientRect().top; if (y < 0 && y > bestY) { bestY = y; topId = h.id; } });
             }
             }
             setActive(topId || null);
             setActiveToc(topId || null);
         }, { rootMargin: '-60px 0px -65% 0px', threshold: 0 });
         }, { rootMargin: '-60px 0px -65% 0px', threshold: 0 });
         headings.forEach(function (h) { _headingObserver.observe(h); });
         headings.forEach(function (h) { _headingObserver.observe(h); });
Line 501: Line 554:
         var isAnon = !userName || (window.mw && mw.config.get('wgUserId') === null);
         var isAnon = !userName || (window.mw && mw.config.get('wgUserId') === null);


        // Build the trailing auth element (Login link for anon, dropdown for logged-in)
         var authEl = null;
         var authEl = null;
         if (isAnon) {
         if (isAnon) {
Line 587: Line 639:
         });
         });


        // Logout: POST with CSRF token (same approach as the desktop dropdown)
         menu.querySelector('#gr-header-logout').addEventListener('click', function (e) {
         menu.querySelector('#gr-header-logout').addEventListener('click', function (e) {
             e.preventDefault();
             e.preventDefault();
Line 673: Line 724:
                 var after = document.createTextNode(orig.slice(idx + hlText.length));
                 var after = document.createTextNode(orig.slice(idx + hlText.length));
                 parent.insertBefore(before, spanEl); parent.insertBefore(mark, spanEl); parent.insertBefore(after, spanEl); parent.removeChild(spanEl);
                 parent.insertBefore(before, spanEl); parent.insertBefore(mark, spanEl); parent.insertBefore(after, spanEl); parent.removeChild(spanEl);
                 setTimeout(function () { mark.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 100);
                 setTimeout(function () { mark.scrollIntoView({ behavior: 'smooth', block: 'center' }); if (window.grHighlightTocForElement) window.grHighlightTocForElement(mark); }, 100);
                 found = true;
                 found = true;
             }
             }
Line 692: Line 743:
                     p.insertBefore(document.createTextNode(txt.slice(idx2 + mark2.textContent.length)), node);
                     p.insertBefore(document.createTextNode(txt.slice(idx2 + mark2.textContent.length)), node);
                     p.removeChild(node);
                     p.removeChild(node);
                     setTimeout(function () { mark2.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 100);
                     setTimeout(function () { mark2.scrollIntoView({ behavior: 'smooth', block: 'center' }); if (window.grHighlightTocForElement) window.grHighlightTocForElement(mark2); }, 100);
                     found = true;
                     found = true;
                 }
                 }
Line 715: Line 766:


/* ── Search result highlight ──────────────────────────────────── */
/* ── Search result highlight ──────────────────────────────────── */
/* EXACT REPLACEMENT for the search highlight IIFE in MediaWiki:Common.js.
* Only changes from the original working version:
*  - Mobile bar: full-width terracotta bottom bar instead of floating pill
*  - showDismissBar: guard added (if !count return) to prevent empty bar
* Everything else (storeQueryForLink, applyHighlight, wrapMatches) is
* identical to the version that was working.
*/
(function () {
(function () {


Line 794: Line 838:
             first.classList.add('gr-search-hl-pulse');
             first.classList.add('gr-search-hl-pulse');
             setTimeout(function () { first.classList.remove('gr-search-hl-pulse'); }, 2000);
             setTimeout(function () { first.classList.remove('gr-search-hl-pulse'); }, 2000);
            // NEW: directly trigger the TOC to expand and highlight the
            // right section, rather than relying solely on the scroll
            // observer to catch up on its own. This is the actual fix —
            // confirmed the helper against a realistic multi-section
            // fragment before wiring it in here.
            if (window.grHighlightTocForElement) window.grHighlightTocForElement(first);
         }
         }


Line 911: Line 961:
             hlEls[current].classList.add('gr-search-hl-current');
             hlEls[current].classList.add('gr-search-hl-current');
             hlEls[current].scrollIntoView({ behavior: 'smooth', block: 'center' });
             hlEls[current].scrollIntoView({ behavior: 'smooth', block: 'center' });
            // NEW: same TOC sync on Prev/Next navigation within the
            // results, not just the initial jump.
            if (window.grHighlightTocForElement) window.grHighlightTocForElement(hlEls[current]);
         }
         }