MediaWiki:Common.js: Difference between revisions

No edit summary
No edit summary
Line 122: Line 122:
   // Wraps each text node in a <span data-deva="original"> so that
   // Wraps each text node in a <span data-deva="original"> so that
   // applyScript() can flip the content without touching innerHTML.
   // applyScript() can flip the content without touching innerHTML.
  // Covers both .mw-parser-output AND the TOC sidebar so that script
  // switching is truly global across content and navigation.
   var translatableSpans = [];
   var translatableSpans = [];


   function tagTextNodes() {
   function tagTextNodes() {
    // Collect all roots to walk: main content + Vector 2022 TOC sidebar
    var roots = [];
     var content = document.querySelector( '.mw-parser-output' );
     var content = document.querySelector( '.mw-parser-output' );
     if ( !content ) return;
     if ( content ) roots.push( content );


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


     nodes.forEach( function ( node ) {
     // Legacy inline TOC (#toc) is inside .mw-parser-output on older skins —
      var p = node.parentNode;
    // the data-deva guard prevents double-wrapping if it was already covered.
      if ( !p ) return;
    // Only add it as a separate root when there is no .mw-parser-output.
      if ( p.hasAttribute && p.hasAttribute( 'data-deva' ) ) return;
    if ( !content ) {
      if ( p.closest ) {
      var ltoc = document.querySelector( '#toc' );
        if ( p.closest( '.gr-script-bar' ) ||  // legacy bar (harmless guard)
      if ( ltoc ) roots.push( ltoc );
            p.closest( '.gr-controls' )  ||  // toolbar controls
    }
            p.closest( '.vector-toc' )    ||
 
            p.closest( '#toc' )          ||
    roots.forEach( function ( root ) {
            p.closest( '.mw-editsection' ) ) return;
      var walker = document.createTreeWalker( root, NodeFilter.SHOW_TEXT );
      }
      var nodes  = [];
      var orig = node.textContent;
      while ( walker.nextNode() ) nodes.push( walker.currentNode );
      if ( !orig.trim() ) return;
 
      var span = document.createElement( 'span' );
      nodes.forEach( function ( node ) {
      span.setAttribute( 'data-deva', orig );
        var p = node.parentNode;
      span.textContent = orig;
        if ( !p ) return;
      p.replaceChild( span, node );
        if ( p.hasAttribute && p.hasAttribute( 'data-deva' ) ) return;
      translatableSpans.push( span );
        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 165: Line 181:
   }
   }


   // ── TOC active-item highlight ───────────────────────────────────
   // ── TOC active-item highlight + lazy TOC tagging ───────────────
   // MutationObserver watches Vector 2022 TOC class changes and colours
   // MutationObserver watches Vector 2022 TOC class changes and colours
   // the active link orange.  This block is not modified.
   // the active link orange.  This block is not modified for the highlight
  // logic, but we additionally tag any TOC text nodes that were not yet
  // in the DOM when tagTextNodes() ran at boot (Vector 2022 renders the
  // TOC lazily on first scroll).
   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;
    // Tag any TOC text nodes that appeared after initial tagTextNodes() run
    function tagAndApplyToc() {
      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 );
      } );
      // Apply current script to any newly tagged spans
      if ( currentScript !== 'deva' && newSpans.length ) {
        newSpans.forEach( function ( span ) {
          span.textContent = transliterateText( span.getAttribute( 'data-deva' ), currentScript );
        } );
      }
    }
    // Tag immediately (catches TOC if it was already rendered)
    tagAndApplyToc();


     var observer = new MutationObserver( function ( mutations ) {
     var observer = new MutationObserver( function ( mutations ) {
       mutations.forEach( function ( m ) {
       mutations.forEach( function ( m ) {
         if ( m.attributeName !== 'class' ) return;
        // Tag any new text nodes added to the TOC (lazy render)
        var li  = m.target;
        if ( m.addedNodes && m.addedNodes.length ) {
        var link = li.querySelector( ':scope > .vector-toc-link' );
          tagAndApplyToc();
        if ( !link ) return;
        }
        if ( li.classList.contains( 'vector-toc-list-item-active' ) ) {
        // Highlight active item
          link.style.color      = '#f57c00';
         if ( m.attributeName === 'class' ) {
          link.style.fontWeight = '700';
          var li  = m.target;
        } else {
          var link = li.querySelector( ':scope > .vector-toc-link' );
          link.style.color      = '';
          if ( !link ) return;
          link.style.fontWeight = '';
          if ( li.classList.contains( 'vector-toc-list-item-active' ) ) {
            link.style.color      = '#f57c00';
            link.style.fontWeight = '700';
          } else {
            link.style.color      = '';
            link.style.fontWeight = '';
          }
         }
         }
       } );
       } );
Line 192: Line 247:
       observer.observe( li, { attributes: true, attributeFilter: [ 'class' ] } );
       observer.observe( li, { attributes: true, attributeFilter: [ 'class' ] } );
     } );
     } );
    // Also watch for new list items being added (TOC populated lazily)
    observer.observe( toc, { childList: true, subtree: true } );
   }
   }


   // ── Init ────────────────────────────────────────────────────────
   // ── Init ────────────────────────────────────────────────────────
   function init() {
   function init() {
     var content     = document.querySelector( '.mw-parser-output' );
     var content       = document.querySelector( '.mw-parser-output' );
     var alreadyTagged = content && content.querySelector( '[data-deva]' );
     var alreadyTagged = content && content.querySelector( '[data-deva]' );
     if ( !alreadyTagged ) {
     if ( !alreadyTagged ) {
Line 213: Line 271:
     }
     }


    // watchTocActive handles both highlight colouring AND lazy TOC tagging
     watchTocActive();
     watchTocActive();
   }
   }