MediaWiki:Common.js: Difference between revisions

No edit summary
No edit summary
Line 14: Line 14:
  *    sub-path install (e.g. /My_wiki/). Points to "My_wiki:About".
  *    sub-path install (e.g. /My_wiki/). Points to "My_wiki:About".
  *  8. TOC customisations skipped on Main_Page and About pages.
  *  8. TOC customisations skipped on Main_Page and About pages.
  *  9. BUG FIX: setupToc now waits for gr-toc-anchor spans before building;
  *  9. Uses Vector's native TOC with IntersectionObserver-based active highlight.
*    MW hook guard prevents rebuild once TOC is already built.
  */
  */


Line 448: Line 447:
   }
   }


   // ── Custom TOC builder (replaces Vector heading-based TOC) ─────────
   // ── TOC: Use Vector's native TOC, add highlight + expand + rename ──────
   // Reads span.gr-toc-anchor elements which have clean IDs set by the
  // Drops the custom TOC builder entirely. Vector builds the correct TOC
   // importer — avoids MediaWiki's URL-encoded heading anchors entirely.
  // from page headings (== Chapter ==, === Section ===). We just:
   // Mirrors the ref site's data-attribute approach: TOC entries come
   //   1. Rename "Contents" → "विषयसूची"
   // from data-title / data-level on the anchor spans, not from headings.
   //   2. Expand all collapsed sections
  var _tocBuilt = false;
   //   3. Remove the "Beginning" top link
   //   4. Inject मूल/उल्लेख nav
  //   5. Add reliable IntersectionObserver active-highlight on headings


   function buildCustomToc() {
   function setupToc() {
     if ( _isNoTocPage() ) return;
     if ( _isNoTocPage() ) return;
     var toc = document.querySelector( '.vector-toc' );
     var toc = document.querySelector( '.vector-toc' );
     if ( !toc ) return;
     if ( !toc ) return;


     // Collect all anchor spans in document order
     removeTocBeginning();
     var anchors = Array.from(
     renameTocTitle();
      document.querySelectorAll( '.mw-parser-output .gr-toc-anchor' )
    expandTocSections();
     );
     injectTocDocNav();
     if ( !anchors.length ) {
     attachHeadingObserver();
      // No gr-toc-anchor spans — fall back to standard TOC customisations
  }
      removeTocBeginning();
 
      renameTocTitle();
  // ── IntersectionObserver on actual heading elements ──────────────
      expandTocSections();
  // Watches the real == Heading == elements in the page body.
      injectTocDocNav();
  // Much more reliable than watching gr-toc-anchor spans.
       watchTocActive();
  var _headingObserver = null;
 
  function attachHeadingObserver() {
    if ( _isNoTocPage() ) return;
    if ( _headingObserver ) return;   // only attach once
    if ( !window.IntersectionObserver ) {
       watchTocActive();               // fallback for old browsers
       return;
       return;
     }
     }


    // Build TOC data: [{id, title, level}]
     var ACTIVE_COLOR = '#f57c00';
     var entries = anchors.map( function ( span ) {
     var _activeId    = null;
      return {
        id:    span.id,
        title: span.getAttribute( 'data-title' ) || span.id,
        level: parseInt( span.getAttribute( 'data-level' ) || '1', 10 ),
        el:    span,
      };
     } );


     // Build the <ul> tree (3 levels: adhyaya, pada, adhikarana)
     // Collect all headings that have an id (Vector gives them ids)
     function buildList( items ) {
     var content  = document.querySelector( '.mw-parser-output' );
      var ul = document.createElement( 'ul' );
    if ( !content ) return;
      ul.className = 'vector-toc-list';
    var headings = Array.from(
      var i = 0;
      content.querySelectorAll( 'h1[id],h2[id],h3[id],h4[id],h5[id],h6[id]' )
      while ( i < items.length ) {
    );
        var item = items[ i ];
    if ( !headings.length ) return;
        var li = document.createElement( 'li' );
        li.className = 'vector-toc-list-item vector-toc-level-' + item.level;
        li.setAttribute( 'data-toc-id', item.id );


        var a = document.createElement( 'a' );
    var toc = document.querySelector( '.vector-toc' );
        a.className = 'vector-toc-link';
    if ( !toc ) return;
        a.href = '#' + item.id;
 
        var textSpan = document.createElement( 'span' );
        textSpan.className = 'vector-toc-text';
        textSpan.setAttribute( 'data-deva', item.title );
        textSpan.textContent = currentScript !== 'deva'
          ? transliterateText( item.title, currentScript )
          : item.title;
        translatableSpans.push( textSpan );
 
        a.appendChild( textSpan );
        li.appendChild( a );


        // Collect children (higher level numbers)
    function getTocLink( id ) {
        var children = [];
      // Vector renders TOC links as href="#id"
        var j = i + 1;
      return toc.querySelector( 'a[href="#' + CSS.escape( id ) + '"]' );
        while ( j < items.length && items[ j ].level > item.level ) {
          children.push( items[ j ] );
          j++;
        }
        if ( children.length ) {
          var childUl = buildList( children );
          li.appendChild( childUl );
          li.classList.add( 'vector-toc-list-item-with-children' );
          // Start collapsed for level 2+ if many children
          if ( item.level >= 2 && children.length > 4 ) {
            li.classList.add( 'vector-toc-list-item-collapsed' );
          }
        }
        ul.appendChild( li );
        i = j;
      }
      return ul;
     }
     }


     // Inject into Vector TOC
     function getTocLi( id ) {
    var contents = toc.querySelector( '.vector-toc-contents' );
       var a = getTocLink( id );
    if ( !contents ) {
       return a ? a.closest( '.vector-toc-list-item' ) : null;
       contents = document.createElement( 'div' );
       contents.className = 'vector-toc-contents';
      toc.appendChild( contents );
     }
     }
    contents.innerHTML = '';
    var listUl = buildList( entries );
    listUl.id = 'mw-panel-toc-list';
    contents.appendChild( listUl );
    _tocBuilt = true;
    // Rename title
    renameTocTitle();
    injectTocDocNav();
    // ── IntersectionObserver for active highlighting ──────────────
    if ( !window.IntersectionObserver ) return;
    var ACTIVE_COLOR  = '#f57c00';
    var _activeId    = null;
    function setTocActive( id ) {
      if ( _activeId === id ) return;
      _activeId = id;


      // Clear all active classes and inline styles
    function clearActive() {
       contents.querySelectorAll( '.vector-toc-list-item' ).forEach( function ( li ) {
       toc.querySelectorAll( '.vector-toc-list-item-active' ).forEach( function ( li ) {
         li.classList.remove( 'vector-toc-list-item-active' );
         li.classList.remove( 'vector-toc-list-item-active' );
         var lnk = li.querySelector( '.vector-toc-link' );
         var lnk = li.querySelector( '.vector-toc-link' );
         if ( lnk ) {
         if ( !lnk ) return;
          lnk.style.removeProperty( 'color' );
        lnk.style.removeProperty( 'color' );
          lnk.style.setProperty( 'font-weight', '400', 'important' );
        lnk.style.setProperty( 'font-weight', '400', 'important' );
          lnk.querySelectorAll( '*' ).forEach( function ( el ) {
        lnk.querySelectorAll( '*' ).forEach( function ( el ) {
            el.style.removeProperty( 'color' );
          el.style.removeProperty( 'color' );
            el.style.setProperty( 'font-weight', '400', 'important' );
          el.style.setProperty( 'font-weight', '400', 'important' );
          } );
        } );
        }
       } );
       } );
    }


    function setActive( id ) {
      if ( _activeId === id ) return;
      _activeId = id;
      clearActive();
       if ( !id ) return;
       if ( !id ) return;


       // Find matching li
       var li = getTocLi( id );
      var activeLi = contents.querySelector( '[data-toc-id="' + id + '"]' );
       if ( !li ) return;
       if ( !activeLi ) return;


       activeLi.classList.add( 'vector-toc-list-item-active' );
       li.classList.add( 'vector-toc-list-item-active' );


       // Only highlight if it is the innermost active item (no active child)
       // Highlight only the innermost active item
       var hasActiveChild = !!activeLi.querySelector(
       var hasActiveChild = !!li.querySelector(
         '.vector-toc-list-item .vector-toc-list-item-active'
         '.vector-toc-list-item .vector-toc-list-item-active'
       );
       );
       if ( !hasActiveChild ) {
       if ( !hasActiveChild ) {
         var lnk = activeLi.querySelector( '.vector-toc-link' );
         var lnk = li.querySelector( '.vector-toc-link' );
         if ( lnk ) {
         if ( lnk ) {
           lnk.style.setProperty( 'color',      ACTIVE_COLOR, 'important' );
           lnk.style.setProperty( 'color',      ACTIVE_COLOR, 'important' );
Line 600: Line 547:


       // Expand collapsed ancestors
       // Expand collapsed ancestors
       var anc = activeLi.parentNode;
       var anc = li.parentNode;
       while ( anc && anc !== contents ) {
       while ( anc && anc !== toc ) {
         if ( anc.classList && anc.classList.contains( 'vector-toc-list-item-collapsed' ) ) {
         if ( anc.classList && anc.classList.contains( 'vector-toc-list-item-collapsed' ) ) {
           anc.classList.remove( 'vector-toc-list-item-collapsed' );
           anc.classList.remove( 'vector-toc-list-item-collapsed' );
Line 608: Line 555:
       }
       }


       // Scroll TOC entry into view
       // Scroll TOC item into view within sidebar
       var container = document.querySelector( '.vector-sticky-pinned-container' );
       var sticky = document.querySelector( '.vector-sticky-pinned-container' );
       if ( container ) {
      var scrollEl = sticky || toc;
         var lr = activeLi.getBoundingClientRect();
       if ( scrollEl.scrollHeight > scrollEl.clientHeight ) {
         var cr = container.getBoundingClientRect();
         var lr = li.getBoundingClientRect();
         if ( lr.top < cr.top + 4 || lr.bottom > cr.bottom - 4 ) {
         var cr = scrollEl.getBoundingClientRect();
           container.scrollTop += lr.top - cr.top - container.clientHeight / 2;
         if ( lr.top < cr.top + 8 || lr.bottom > cr.bottom - 8 ) {
           scrollEl.scrollTop += lr.top - cr.top - scrollEl.clientHeight / 2 + li.offsetHeight / 2;
         }
         }
       }
       }
     }
     }


     // Track which anchors are visible
     // Track which headings are visible
     var _visibleIds = new Set();
     var _visible = new Set();


     var observer = new IntersectionObserver( function ( entries ) {
     _headingObserver = new IntersectionObserver( function ( entries ) {
       entries.forEach( function ( entry ) {
       entries.forEach( function ( entry ) {
        var id = entry.target.id;
         if ( entry.isIntersecting ) {
         if ( entry.isIntersecting ) {
           _visibleIds.add( id );
           _visible.add( entry.target.id );
         } else {
         } else {
           _visibleIds.delete( id );
           _visible.delete( entry.target.id );
         }
         }
       } );
       } );


       // Find the topmost visible anchor
       // Pick the topmost visible heading
       var topId = null;
       var topId = null;
       var topY  = Infinity;
       var topY  = Infinity;
       _visibleIds.forEach( function ( id ) {
       _visible.forEach( function ( id ) {
         var el = document.getElementById( id );
         var el = document.getElementById( id );
         if ( el ) {
         if ( el ) {
           var y = el.getBoundingClientRect().top;
           var y = el.getBoundingClientRect().top;
           if ( y < topY ) { topY = y; topId = id; }
           if ( y >= 0 && y < topY ) { topY = y; topId = id; }
         }
         }
       } );
       } );


       // If nothing visible (scrolled past), find the last anchor above viewport
       // Nothing visible: find the last heading scrolled past (above viewport)
       if ( !topId ) {
       if ( !topId ) {
         var best = null, bestBottom = -Infinity;
         var bestY = -Infinity;
         anchors.forEach( function ( span ) {
         headings.forEach( function ( h ) {
           var r = span.getBoundingClientRect();
           var y = h.getBoundingClientRect().top;
           if ( r.bottom < 80 && r.bottom > bestBottom ) {
           if ( y < 0 && y > bestY ) { bestY = y; topId = h.id; }
            bestBottom = r.bottom;
            best = span.id;
          }
         } );
         } );
        topId = best;
       }
       }


       setTocActive( topId );
       setActive( topId || null );
     }, { rootMargin: '-60px 0px -70% 0px', threshold: 0 } );
     }, {
 
      // Fire when heading enters/leaves the top 30% of the viewport
     anchors.forEach( function ( span ) { observer.observe( span ); } );
      rootMargin: '-60px 0px -65% 0px',
 
      threshold: 0
    // ── Guard: re-inject our TOC if Vector wipes it ───────────────
     } );
    // Vector 2022 can replace contents.innerHTML after our build.
    // Watch for that and immediately restore our list.
    if ( window.MutationObserver ) {
      var _reinjecting = false;
      var tocGuard = new MutationObserver( function () {
        if ( _reinjecting ) return;
        // If our list is gone, put it back
        if ( !contents.querySelector( '#mw-panel-toc-list' ) ) {
          _reinjecting = true;
          contents.innerHTML = '';
          var freshUl = buildList( entries );
          freshUl.id = 'mw-panel-toc-list';
          contents.appendChild( freshUl );
          renameTocTitle();
          _reinjecting = false;
        }
      } );
      tocGuard.observe( contents, { childList: true, subtree: false } );
    }
  }


  // ── Run all TOC customisations ───────────────────────────────────
    headings.forEach( function ( h ) { _headingObserver.observe( h ); } );
  // FIX v8: Wait for gr-toc-anchor spans before building; once built never rebuild.
  function setupToc() {
    if ( _tocBuilt ) {
      // TOC already built — just re-run label/nav parts, never rebuild
      renameTocTitle();
      injectTocDocNav();
      return;
    }
    // Only build if gr-toc-anchor spans are already in the DOM
    var anchors = document.querySelectorAll( '.mw-parser-output .gr-toc-anchor' );
    if ( !anchors.length ) {
      // Anchors not yet in DOM — skip this call, retry will catch it
      return;
    }
    // Try custom TOC first; falls back to Vector customisations if no anchors
    buildCustomToc();
    if ( !_tocBuilt ) {
      removeTocBeginning();
      renameTocTitle();
      expandTocSections();
      injectTocDocNav();
      watchTocActive();
    }
   }
   }


Line 778: Line 678:


     // Vector 2022 defers TOC render — retry at 300ms and 800ms
     // Vector 2022 defers TOC render — retry at 300ms and 800ms
     setupToc();
     // Use a single short delay so Vector has rendered the TOC
    setTimeout( setupToc, 300 );
     setTimeout( setupToc, 200 );
     setTimeout( setupToc, 800 );
   }
   }


Line 842: Line 741:
         }
         }
         if ( currentScript !== 'deva' ) applyScript( currentScript );
         if ( currentScript !== 'deva' ) applyScript( currentScript );
         // FIX v8: Only call setupToc if not already built
         setupToc();
        if ( !_tocBuilt ) setupToc();
       }, 150 );
       }, 150 );
     } );
     } );