MediaWiki:Common.js: Difference between revisions

No edit summary
No edit summary
Line 178: Line 178:
   }
   }


   // ── TOC active-item highlight ───────────────────────────────────
   // ── TOC active-item highlight ────────────────────────────────────
  // FIX: Instead of relying on CSS :active selectors (which fail because
  // Common.js wraps text nodes in <span data-deva>, making > a or > .link
  // selectors not match the coloured text), we use a MutationObserver to
  // watch each <li> for class changes and imperatively apply/remove the
  // orange colour via inline style on the .vector-toc-link inside it.
  // This is immune to DOM depth and span nesting.
   function watchTocActive() {
   function watchTocActive() {
     var toc = document.querySelector( '.vector-toc' );
     var toc = document.querySelector( '.vector-toc' );
     if ( !toc || toc._grObserved ) return;
     if ( !toc ) return;
    if ( !window.MutationObserver ) return;
    /* Guard only the structObserver — attach it once per toc element.
      Per-<li> attachment is already guarded by _grHighlightAttached,
      so calling watchTocActive() multiple times is safe and picks up
      any <li> items that weren't in the DOM on the first call. */
    if ( toc._grObserved ) {
      /* Re-scan for any <li> items that arrived after first call */
      toc.querySelectorAll( '.vector-toc-list-item' ).forEach( attachHighlight );
      return;
    }
     toc._grObserved = true;
     toc._grObserved = true;
     if ( !window.MutationObserver ) return;
 
     var ACTIVE_COLOR  = '#f57c00';
    var ACTIVE_WEIGHT = '700';
 
    function setLinkActive( li, active ) {
      var link = li.querySelector( '.vector-toc-link' ) || li.querySelector( 'a' );
      if ( !link ) return;
      if ( active ) {
        link.style.setProperty( 'color',      ACTIVE_COLOR,  'important' );
        link.style.setProperty( 'font-weight', ACTIVE_WEIGHT, 'important' );
        /* Also colour nested spans (e.g. data-deva spans from transliteration) */
        link.querySelectorAll( 'span' ).forEach( function ( s ) {
          s.style.setProperty( 'color', ACTIVE_COLOR, 'important' );
        } );
      } else {
        link.style.removeProperty( 'color' );
        link.style.removeProperty( 'font-weight' );
        link.querySelectorAll( 'span' ).forEach( function ( s ) {
          s.style.removeProperty( 'color' );
        } );
      }
    }


     function attachHighlight( li ) {
     function attachHighlight( li ) {
       observer.observe( li, { attributes: true, attributeFilter: [ 'class' ] } );
       if ( li._grHighlightAttached ) return;
      li._grHighlightAttached = true;
      liObserver.observe( li, { attributes: true, attributeFilter: [ 'class' ] } );
      /* Apply immediately if already active on attachment */
      if ( li.classList.contains( 'vector-toc-list-item-active' ) ) {
        setLinkActive( li, true );
      }
     }
     }


     var observer = new MutationObserver( function ( mutations ) {
     var liObserver = new MutationObserver( function ( mutations ) {
       mutations.forEach( function ( m ) {
       mutations.forEach( function ( m ) {
         // New list items added (lazy render) → attach highlight + tag for transliteration
         if ( m.attributeName !== 'class' ) return;
         if ( m.type === 'childList' ) {
         var li = m.target;
           m.addedNodes.forEach( function ( n ) {
        var isActive = li.classList.contains( 'vector-toc-list-item-active' );
             if ( n.nodeType !== 1 ) return;
        setLinkActive( li, isActive );
             if ( n.classList.contains( 'vector-toc-list-item' ) ) {
 
               attachHighlight( n );
        /* Scroll active item into view if it's off screen */
        if ( isActive ) {
           var container = document.querySelector( '.vector-sticky-pinned-container' );
          if ( container ) {
             var liRect  = li.getBoundingClientRect();
            var cRect  = container.getBoundingClientRect();
             if ( liRect.top < cRect.top + 8 || liRect.bottom > cRect.bottom - 8 ) {
               li.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
             }
             }
            n.querySelectorAll && n.querySelectorAll( '.vector-toc-list-item' )
          }
              .forEach( attachHighlight );
        }
      } );
    } );


             // Tag any newly revealed .vector-toc-text spans for transliteration
    var structObserver = new MutationObserver( function ( mutations ) {
            var newTextSpans = [];
      mutations.forEach( function ( m ) {
            if ( n.classList && n.classList.contains( 'vector-toc-text' ) ) {
        if ( m.type !== 'childList' ) return;
               newTextSpans.push( n );
        m.addedNodes.forEach( function ( n ) {
          if ( n.nodeType !== 1 ) return;
          /* Attach highlight observer to newly added list items */
          if ( n.classList && n.classList.contains( 'vector-toc-list-item' ) ) {
            attachHighlight( n );
          }
          if ( n.querySelectorAll ) {
             n.querySelectorAll( '.vector-toc-list-item' ).forEach( attachHighlight );
          }
 
          /* Tag any new .vector-toc-text spans for transliteration */
          var newSpans = [];
          if ( n.classList && n.classList.contains( 'vector-toc-text' ) ) newSpans.push( n );
          if ( n.querySelectorAll ) {
            n.querySelectorAll( '.vector-toc-text' ).forEach( function ( s ) { newSpans.push( s ); } );
          }
          newSpans.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 );
             }
             }
             if ( n.querySelectorAll ) {
             translatableSpans.push( span );
              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;
         } );
        }
 
        // Class change → scroll active item into view if needed.
        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' });
        }
       } );
       } );
     } );
     } );


    // Observe existing list items for class changes
     toc.querySelectorAll( '.vector-toc-list-item' ).forEach( attachHighlight );
     toc.querySelectorAll( '.vector-toc-list-item' ).forEach( attachHighlight );
     // Watch for new items (lazy TOC population)
     structObserver.observe( toc, { childList: true, subtree: true } );
    observer.observe( toc, { childList: true, subtree: true } );


     // On initial load: scroll to already-active item
     /* On initial load, scroll to the 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' );
       if ( active ) active.scrollIntoView({ block: 'nearest', behavior: 'auto' });
       if ( active ) {
     }, 400 );
        setLinkActive( active, true );
        active.scrollIntoView({ block: 'nearest', behavior: 'auto' });
      }
     }, 300 );
   }
   }


   // ── Init ──────────────────────────────────────────────────────
   // ── Init ────────────────────────────────────────────────────────
   function init() {
   function init() {
     var content      = document.querySelector( '.mw-parser-output' );
     var content      = document.querySelector( '.mw-parser-output' );
Line 261: Line 307:
       tagTextNodes();
       tagTextNodes();
     } else {
     } 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 ) {
       document.querySelectorAll( '.vector-toc .vector-toc-text:not([data-deva])' ).forEach( function ( span ) {
         var orig = span.textContent;
         var orig = span.textContent;
Line 270: Line 315:
     }
     }


    // Apply the globally persisted script preference immediately
     var saved = ( function () {
     var saved = ( function () {
       try { return localStorage.getItem( LS_SCRIPT_KEY ); } catch ( e ) { return null; }
       try { return localStorage.getItem( LS_SCRIPT_KEY ); } catch ( e ) { return null; }
Line 281: Line 325:


     watchTocActive();
     watchTocActive();
    /* Retry for Vector 2022 TOC which renders after DOMContentLoaded */
    setTimeout( watchTocActive, 300 );
    setTimeout( watchTocActive, 800 );
   }
   }


   // ── React to toolbar dropdown changes on the same page ─────────
   // ── React to toolbar dropdown changes ──────────────────────────
   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 289: Line 336:
   } );
   } );


   // ── MediaWiki hook (SPA-style navigation support) ───────────────
   // ── MediaWiki hook (SPA-style navigation) ───────────────────────
   if ( window.mw ) {
   if ( window.mw ) {
     mw.hook( 'wikipage.content' ).add( function () {
     mw.hook( 'wikipage.content' ).add( function () {
       setTimeout( function () {
       setTimeout( function () {
         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 299: Line 346:
           tagTextNodes();
           tagTextNodes();
         } else {
         } else {
          // Tag any untagged TOC spans after navigation
           document.querySelectorAll( '.vector-toc .vector-toc-text:not([data-deva])' ).forEach( function ( span ) {
           document.querySelectorAll( '.vector-toc .vector-toc-text:not([data-deva])' ).forEach( function ( span ) {
             var orig = span.textContent;
             var orig = span.textContent;
Line 307: Line 353:
           } );
           } );
         }
         }
        // Re-apply current script to newly loaded content
         if ( currentScript !== 'deva' ) applyScript( currentScript );
         if ( currentScript !== 'deva' ) {
          applyScript( currentScript );
        }
         watchTocActive();
         watchTocActive();
       }, 150 );
       }, 150 );
Line 316: Line 359:
   }
   }


  // ── Fallback for non-MW environments ───────────────────────────
   if ( document.readyState === 'loading' ) {
   if ( document.readyState === 'loading' ) {
     document.addEventListener( 'DOMContentLoaded', init );
     document.addEventListener( 'DOMContentLoaded', init );