MediaWiki:Common.js: Difference between revisions

No edit summary
No edit summary
Line 129: Line 129:


   function tagTextNodes() {
   function tagTextNodes() {
     // Collect all roots to walk: main content + Vector 2022 TOC sidebar
     // Walk only .mw-parser-output — the main article content.
     var roots = [];
    //
    // We deliberately EXCLUDE .vector-toc (the sidebar TOC):
    // Vector 2022 has its own MutationObserver on the TOC that rebuilds
    // list items when text nodes change. If we transliterate TOC text nodes,
    // Vector re-reads the changed text, tries to match it to a heading anchor,
    // fails (because anchors use the original Devanagari), and renders
    // "unknown title" for those entries. Keeping the TOC in Devanagari
     // preserves correct anchor navigation regardless of script selection.
     var content = document.querySelector( '.mw-parser-output' );
     var content = document.querySelector( '.mw-parser-output' );
     if ( content ) roots.push( content );
     if ( !content ) return;


    // Vector 2022 sidebar TOC lives outside .mw-parser-output
     var walker = document.createTreeWalker( content, NodeFilter.SHOW_TEXT );
     var vtoc = document.querySelector( '.vector-toc' );
     var nodes  = [];
     if ( vtoc ) roots.push( vtoc );
    while ( walker.nextNode() ) nodes.push( walker.currentNode );


     // Legacy inline TOC (#toc) is inside .mw-parser-output on older skins —
     nodes.forEach( function ( node ) {
    // the data-deva guard prevents double-wrapping if it was already covered.
      var p = node.parentNode;
    // Only add it as a separate root when there is no .mw-parser-output.
      if ( !p ) return;
    if ( !content ) {
      if ( p.hasAttribute && p.hasAttribute( 'data-deva' ) ) return;
      var ltoc = document.querySelector( '#toc' );
      if ( p.closest ) {
      if ( ltoc ) roots.push( ltoc );
        if ( p.closest( '.gr-controls' )   ||
    }
            p.closest( '.mw-editsection' ) ) return;
 
      }
    roots.forEach( function ( root ) {
      var orig = node.textContent;
      var walker = document.createTreeWalker( root, NodeFilter.SHOW_TEXT );
      if ( !orig.trim() ) return;
      var nodes  = [];
      var span = document.createElement( 'span' );
      while ( walker.nextNode() ) nodes.push( walker.currentNode );
      span.setAttribute( 'data-deva', orig );
 
      span.textContent = orig;
      nodes.forEach( function ( node ) {
      p.replaceChild( span, node );
        var p = node.parentNode;
      translatableSpans.push( span );
        if ( !p ) return;
        if ( p.hasAttribute && p.hasAttribute( 'data-deva' ) ) return;
        if ( p.closest ) {
          if ( p.closest( '.gr-script-bar' ) ||  // legacy bar (harmless guard)
              p.closest( '.gr-controls' )   || // toolbar controls
              p.closest( '.mw-editsection' ) ) return;
        }
        var orig = node.textContent;
        if ( !orig.trim() ) return;
        var span = document.createElement( 'span' );
        span.setAttribute( 'data-deva', orig );
        span.textContent = orig;
        p.replaceChild( span, node );
        translatableSpans.push( span );
      } );
     } );
     } );
   }
   }
Line 177: Line 169:
       if ( !span.parentNode ) return; // detached node
       if ( !span.parentNode ) return; // detached node
       var orig = span.getAttribute( 'data-deva' );
       var orig = span.getAttribute( 'data-deva' );
      if ( !orig ) return;
       span.textContent = ( script === 'deva' )
       span.textContent = ( script === 'deva' )
         ? orig
         ? orig
Line 185: Line 178:
   // ── TOC active-item highlight + lazy TOC tagging ───────────────
   // ── TOC active-item highlight + lazy TOC tagging ───────────────
   // ── TOC active-item highlight + lazy TOC tagging ───────────────
   // ── TOC active-item highlight + lazy TOC tagging ───────────────
  // ── TOC active-item highlight ───────────────────────────────────
  // Watches class changes on TOC list items and colours the active
  // link orange. We do NOT transliterate TOC text nodes — doing so
  // triggers Vector's own TOC observer which rebuilds items using the
  // new text as anchor text, failing to match the original heading
  // IDs and showing "unknown title".
   function watchTocActive() {
   function watchTocActive() {
     var toc = document.querySelector( '.vector-toc' );
     var toc = document.querySelector( '.vector-toc' );
     if ( !toc || toc._grObserved ) return;
     if ( !toc || toc._grObserved ) return;
     toc._grObserved = true;
     toc._grObserved = true;
    if ( !window.MutationObserver ) return;


    var _tagging = false;
     function attachHighlight( li ) {
     function tagAndApplyToc() {
       observer.observe( li, { attributes: true, attributeFilter: [ 'class' ] } );
       if ( _tagging ) return;
      _tagging = true;
      var walker = document.createTreeWalker( toc, NodeFilter.SHOW_TEXT );
      var nodes  = [];
      while ( walker.nextNode() ) nodes.push( walker.currentNode );
      var newSpans = [];
      nodes.forEach( function ( node ) {
        var p = node.parentNode;
        if ( !p ) return;
        if ( p.hasAttribute && p.hasAttribute( 'data-deva' ) ) return;
        var orig = node.textContent;
        if ( !orig.trim() ) return;
        var span = document.createElement( 'span' );
        span.setAttribute( 'data-deva', orig );
        span.textContent = orig;
        p.replaceChild( span, node );
        translatableSpans.push( span );
        newSpans.push( span );
      } );
      if ( currentScript !== 'deva' && newSpans.length ) {
        newSpans.forEach( function ( span ) {
          span.textContent = transliterateText( span.getAttribute( 'data-deva' ), currentScript );
        } );
      }
      _tagging = false;
     }
     }


     // Tag immediately in case TOC is already in the DOM
     var observer = new MutationObserver( function ( mutations ) {
     tagAndApplyToc();
      mutations.forEach( function ( m ) {
        // New list items added (lazy render) → attach highlight to them
        if ( m.type === 'childList' ) {
          m.addedNodes.forEach( function ( n ) {
            if ( n.nodeType !== 1 ) return;
            if ( n.classList.contains( 'vector-toc-list-item' ) ) {
              attachHighlight( n );
            }
            n.querySelectorAll && n.querySelectorAll( '.vector-toc-list-item' )
              .forEach( attachHighlight );
          } );
          return;
        }
        // Class change → scroll active item into view if needed.
        // Colour is handled entirely by CSS — no inline style manipulation
        // which could conflict with Vector's own stylesheet.
        if ( m.attributeName !== 'class' ) return;
        var li = m.target;
        if ( !li.classList.contains( 'vector-toc-list-item-active' ) ) return;
        var container = document.querySelector( '.vector-sticky-pinned-container' );
        if ( !container ) return;
        var liRect  = li.getBoundingClientRect();
        var cRect  = container.getBoundingClientRect();
        var isAbove = liRect.top    < cRect.top    + 8;
        var isBelow = liRect.bottom > cRect.bottom - 8;
        if ( isAbove || isBelow ) {
          li.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
        }
      } );
     } );


     if ( window.MutationObserver ) {
     // Observe existing list items for class changes
      // Separate observer just for active-item highlight — attribute changes only,
    toc.querySelectorAll( '.vector-toc-list-item' ).forEach( attachHighlight );
      // never fires from our own DOM writes, safe to run forever
    // Watch for new items (lazy TOC population)
      var attrObserver = new MutationObserver( function ( mutations ) {
    observer.observe( toc, { childList: true, subtree: true } );
        mutations.forEach( function ( m ) {
          if ( m.attributeName !== 'class' ) return;
          var li  = m.target;
          var link = li.querySelector( ':scope > .vector-toc-link' );
          if ( !link ) return;
          if ( li.classList.contains( 'vector-toc-list-item-active' ) ) {
            link.style.color      = '#f57c00';
            link.style.fontWeight = '700';
          } else {
            link.style.color      = '';
            link.style.fontWeight = '';
          }
        } );
      } );
      toc.querySelectorAll( '.vector-toc-list-item' ).forEach( function ( li ) {
        attrObserver.observe( li, { attributes: true, attributeFilter: [ 'class' ] } );
      } );


      // Separate observer for lazy TOC population — watches childList but
    // On initial load: scroll to already-active item (colour handled by CSS)
      // ignores mutations caused by our own span insertions (those have
    setTimeout( function () {
      // addedNodes that are <span data-deva="…"> elements we just created)
      var active = toc.querySelector( '.vector-toc-list-item-active' );
      var childObserver = new MutationObserver( function ( mutations ) {
      if ( active ) active.scrollIntoView({ block: 'nearest', behavior: 'auto' });
        var hasExternalAddition = mutations.some( function ( m ) {
    }, 400 );
          return Array.prototype.some.call( m.addedNodes, function ( n ) {
            return !( n.nodeType === 1 && n.hasAttribute( 'data-deva' ) );
          } );
        } );
        if ( !hasExternalAddition ) return;
        tagAndApplyToc();
        // Attach highlight observer to any newly added list items
        toc.querySelectorAll( '.vector-toc-list-item' ).forEach( function ( li ) {
          attrObserver.observe( li, { attributes: true, attributeFilter: [ 'class' ] } );
        } );
      } );
      childObserver.observe( toc, { childList: true, subtree: true } );
    }
   }
   }
   // ── Init ──────────────────────────────────────────────────────
   // ── Init ──────────────────────────────────────────────────────