MediaWiki:Common.js: Difference between revisions

No edit summary
No edit summary
Line 440: Line 440:
       if ( active ) setActive( active, true );
       if ( active ) setActive( active, true );
     }, 300 );
     }, 300 );
  }
  // ── Custom TOC builder (replaces Vector heading-based TOC) ─────────
  // Reads span.gr-toc-anchor elements which have clean IDs set by the
  // importer — avoids MediaWiki's URL-encoded heading anchors entirely.
  // Mirrors the ref site's data-attribute approach: TOC entries come
  // from data-title / data-level on the anchor spans, not from headings.
  var _tocBuilt = false;
  function buildCustomToc() {
    if ( _isNoTocPage() ) return;
    var toc = document.querySelector( '.vector-toc' );
    if ( !toc ) return;
    // Collect all anchor spans in document order
    var anchors = Array.from(
      document.querySelectorAll( '.mw-parser-output .gr-toc-anchor' )
    );
    if ( !anchors.length ) {
      // No gr-toc-anchor spans — fall back to standard TOC customisations
      removeTocBeginning();
      renameTocTitle();
      expandTocSections();
      injectTocDocNav();
      watchTocActive();
      return;
    }
    // Build TOC data: [{id, title, level}]
    var entries = anchors.map( function ( span ) {
      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)
    function buildList( items ) {
      var ul = document.createElement( 'ul' );
      ul.className = 'vector-toc-list';
      var i = 0;
      while ( i < items.length ) {
        var item = items[ i ];
        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' );
        a.className = 'vector-toc-link';
        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)
        var children = [];
        var j = i + 1;
        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
    var contents = toc.querySelector( '.vector-toc-contents' );
    if ( !contents ) {
      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
      contents.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 ) {
          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' );
          } );
        }
      } );
      if ( !id ) return;
      // Find matching li
      var activeLi = contents.querySelector( '[data-toc-id="' + id + '"]' );
      if ( !activeLi ) return;
      activeLi.classList.add( 'vector-toc-list-item-active' );
      // Only highlight if it is the innermost active item (no active child)
      var hasActiveChild = !!activeLi.querySelector(
        '.vector-toc-list-item .vector-toc-list-item-active'
      );
      if ( !hasActiveChild ) {
        var lnk = activeLi.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' );
          } );
        }
      }
      // Expand collapsed ancestors
      var anc = activeLi.parentNode;
      while ( anc && anc !== contents ) {
        if ( anc.classList && anc.classList.contains( 'vector-toc-list-item-collapsed' ) ) {
          anc.classList.remove( 'vector-toc-list-item-collapsed' );
        }
        anc = anc.parentNode;
      }
      // Scroll TOC entry into view
      var container = document.querySelector( '.vector-sticky-pinned-container' );
      if ( container ) {
        var lr = activeLi.getBoundingClientRect();
        var cr = container.getBoundingClientRect();
        if ( lr.top < cr.top + 4 || lr.bottom > cr.bottom - 4 ) {
          container.scrollTop += lr.top - cr.top - container.clientHeight / 2;
        }
      }
    }
    // Track which anchors are visible
    var _visibleIds = new Set();
    var observer = new IntersectionObserver( function ( entries ) {
      entries.forEach( function ( entry ) {
        var id = entry.target.id;
        if ( entry.isIntersecting ) {
          _visibleIds.add( id );
        } else {
          _visibleIds.delete( id );
        }
      } );
      // Find the topmost visible anchor
      var topId = null;
      var topY  = Infinity;
      _visibleIds.forEach( function ( id ) {
        var el = document.getElementById( id );
        if ( el ) {
          var y = el.getBoundingClientRect().top;
          if ( y < topY ) { topY = y; topId = id; }
        }
      } );
      // If nothing visible (scrolled past), find the last anchor above viewport
      if ( !topId ) {
        var best = null, bestBottom = -Infinity;
        anchors.forEach( function ( span ) {
          var r = span.getBoundingClientRect();
          if ( r.bottom < 80 && r.bottom > bestBottom ) {
            bestBottom = r.bottom;
            best = span.id;
          }
        } );
        topId = best;
      }
      setTocActive( topId );
    }, { rootMargin: '-60px 0px -70% 0px', threshold: 0 } );
    anchors.forEach( function ( span ) { observer.observe( span ); } );
   }
   }


   // ── Run all TOC customisations ───────────────────────────────────
   // ── Run all TOC customisations ───────────────────────────────────
   function setupToc() {
   function setupToc() {
     removeTocBeginning();
     if ( _tocBuilt ) {
    renameTocTitle();
      // TOC already built — just re-run label/nav parts
    expandTocSections();
      renameTocTitle();
    injectTocDocNav();
      injectTocDocNav();
    watchTocActive();
      return;
    }
    // Try custom TOC first; falls back to Vector customisations if no anchors
    buildCustomToc();
    if ( !_tocBuilt ) {
      removeTocBeginning();
      renameTocTitle();
      expandTocSections();
      injectTocDocNav();
      watchTocActive();
    }
   }
   }