MediaWiki:Common.js: Difference between revisions

No edit summary
No edit summary
Line 122: Line 122:


   // ── Tag all transliteratable text nodes once per page ───────────
   // ── Tag all transliteratable text nodes once per page ───────────
  // Wraps each text node in a <span data-deva="original"> so that
  // 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() {
     // Walk only .mw-parser-output — the main article content.
     // ── Main article content ──────────────────────────────────────
    //
    // 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 ) return;
     if ( content ) {
      var walker = document.createTreeWalker( content, NodeFilter.SHOW_TEXT );
      var nodes  = [];
      while ( walker.nextNode() ) nodes.push( walker.currentNode );


    var walker = document.createTreeWalker( content, NodeFilter.SHOW_TEXT );
      nodes.forEach( function ( node ) {
    var nodes  = [];
        var p = node.parentNode;
    while ( walker.nextNode() ) nodes.push( walker.currentNode );
        if ( !p ) return;
        if ( p.hasAttribute && p.hasAttribute( 'data-deva' ) ) return;
        if ( p.closest ) {
          if ( p.closest( '.gr-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 );
      } );
    }


     nodes.forEach( function ( node ) {
     // ── Sidebar TOC (.vector-toc-text spans) ─────────────────────
      var p = node.parentNode;
    // We target the .vector-toc-text spans that Vector itself renders,
      if ( !p ) return;
    // rather than walking raw text nodes. Mutating textContent of an
      if ( p.hasAttribute && p.hasAttribute( 'data-deva' ) ) return;
    // existing child span is a characterData mutation — Vector's own
      if ( p.closest ) {
    // MutationObserver only watches for childList changes on the <li>,
        if ( p.closest( '.gr-controls' )    ||
    // so it will NOT fire, and the "unknown title" bug is avoided.
            p.closest( '.mw-editsection' ) ) return;
    document.querySelectorAll( '.vector-toc .vector-toc-text' ).forEach( function ( span ) {
      }
      if ( span.hasAttribute( 'data-deva' ) ) return; // already tagged
       var orig = node.textContent;
       var orig = span.textContent;
       if ( !orig.trim() ) return;
       if ( !orig.trim() ) return;
      var span = document.createElement( 'span' );
       span.setAttribute( 'data-deva', orig );
       span.setAttribute( 'data-deva', orig );
      span.textContent = orig;
      p.replaceChild( span, node );
       translatableSpans.push( span );
       translatableSpans.push( span );
     } );
     } );
Line 176: Line 178:
   }
   }


  // ── TOC active-item highlight + lazy TOC tagging ───────────────
  // ── TOC active-item highlight + lazy TOC tagging ───────────────
   // ── TOC active-item highlight ───────────────────────────────────
   // ── 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' );
Line 196: Line 191:
     var observer = new MutationObserver( function ( mutations ) {
     var observer = new MutationObserver( function ( mutations ) {
       mutations.forEach( function ( m ) {
       mutations.forEach( function ( m ) {
         // New list items added (lazy render) → attach highlight to them
         // New list items added (lazy render) → attach highlight + tag for transliteration
         if ( m.type === 'childList' ) {
         if ( m.type === 'childList' ) {
           m.addedNodes.forEach( function ( n ) {
           m.addedNodes.forEach( function ( n ) {
Line 205: Line 200:
             n.querySelectorAll && n.querySelectorAll( '.vector-toc-list-item' )
             n.querySelectorAll && n.querySelectorAll( '.vector-toc-list-item' )
               .forEach( attachHighlight );
               .forEach( attachHighlight );
            // Tag any newly revealed .vector-toc-text spans for transliteration
            var newTextSpans = [];
            if ( n.classList && n.classList.contains( 'vector-toc-text' ) ) {
              newTextSpans.push( n );
            }
            if ( n.querySelectorAll ) {
              n.querySelectorAll( '.vector-toc-text' ).forEach( function ( s ) {
                newTextSpans.push( s );
              } );
            }
            newTextSpans.forEach( function ( span ) {
              if ( span.hasAttribute( 'data-deva' ) ) return;
              var orig = span.textContent;
              if ( !orig.trim() ) return;
              span.setAttribute( 'data-deva', orig );
              if ( currentScript !== 'deva' ) {
                span.textContent = transliterateText( orig, currentScript );
              }
              translatableSpans.push( span );
            } );
           } );
           } );
           return;
           return;
         }
         }
         // Class change → scroll active item into view if needed.
         // 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;
         if ( m.attributeName !== 'class' ) return;
         var li = m.target;
         var li = m.target;
Line 231: Line 246:
     observer.observe( toc, { childList: true, subtree: true } );
     observer.observe( toc, { childList: true, subtree: true } );


     // On initial load: scroll to already-active item (colour handled by CSS)
     // On initial load: scroll to already-active item
     setTimeout( function () {
     setTimeout( function () {
       var active = toc.querySelector( '.vector-toc-list-item-active' );
       var active = toc.querySelector( '.vector-toc-list-item-active' );
Line 237: Line 252:
     }, 400 );
     }, 400 );
   }
   }
   // ── Init ──────────────────────────────────────────────────────
   // ── Init ──────────────────────────────────────────────────────
   function init() {
   function init() {
Line 244: Line 260:
       translatableSpans = [];
       translatableSpans = [];
       tagTextNodes();
       tagTextNodes();
    } else {
      // Content already tagged — still tag TOC spans if not yet done
      document.querySelectorAll( '.vector-toc .vector-toc-text:not([data-deva])' ).forEach( function ( span ) {
        var orig = span.textContent;
        if ( !orig.trim() ) return;
        span.setAttribute( 'data-deva', orig );
        translatableSpans.push( span );
      } );
     }
     }


Line 256: Line 280:
     }
     }


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


   // ── React to toolbar dropdown changes on the same page ─────────
   // ── React to toolbar dropdown changes on the same page ─────────
  // The ReaderToolbar dispatches 'gr-script-change' when the user
  // picks a new script.  We just call applyScript() — no need to
  // touch localStorage again (the toolbar already wrote it).
   window.addEventListener( 'gr-script-change', function ( e ) {
   window.addEventListener( 'gr-script-change', function ( e ) {
     var script = e && e.detail && e.detail.script;
     var script = e && e.detail && e.detail.script;
Line 278: Line 298:
           translatableSpans = [];
           translatableSpans = [];
           tagTextNodes();
           tagTextNodes();
        } else {
          // Tag any untagged TOC spans after navigation
          document.querySelectorAll( '.vector-toc .vector-toc-text:not([data-deva])' ).forEach( function ( span ) {
            var orig = span.textContent;
            if ( !orig.trim() ) return;
            span.setAttribute( 'data-deva', orig );
            translatableSpans.push( span );
          } );
         }
         }
         // Re-apply current script to newly loaded content
         // Re-apply current script to newly loaded content