MediaWiki:Common.js: Difference between revisions

No edit summary
No edit summary
Line 1: Line 1:
/* MediaWiki:Common.js — grantha.io
/* MediaWiki:Common.js — grantha.io (v6)
  *
  *
  * Responsibilities:
  * Changes vs v5:
  *  1. Transliteration engine (Devanāgarī → IAST / Kannada / Tamil).
  *  1. TOC active highlight fix: Vector 2022 sets the active class on the
  * 2. Tag all transliteratable text nodes once per page load.
  *     <li.vector-toc-list-item>, but Common.js wraps all text nodes inside
  * 3. Apply the script stored in localStorage('grantha_reader_script')
  *     .vector-toc-text spans in <span data-deva="…">.  The CSS colour rule
  *    so every page opens in the user's preferred script automatically.
  *    was targeting .vector-toc-list-item-active > a which never matched
*  4. Listen for the 'gr-script-change' CustomEvent dispatched by the
  *    because the <a> sits deeper and its text is now inside a <span>.
  *    ReaderToolbar dropdown and re-apply the new script immediately.
  *     Fix: MutationObserver watches the <li> for class changes and directly
  * 5. TOC active-item highlight via MutationObserver (unchanged).
  *     applies/removes the orange colour via inline style on the .vector-toc-link
  *
  *     ancestor, bypassing the CSS specificity war entirely.
* The script-switcher bar/buttons previously built here are removed —
  *  2. All other behaviour identical to v5.
  * the ReaderToolbar extension now owns the dropdown UI.
  *
  * localStorage key shared with ReaderToolbar: 'grantha_reader_script'
  */
  */


Line 125: Line 122:


   function tagTextNodes() {
   function tagTextNodes() {
    // ── Main article content ──────────────────────────────────────
     var content = document.querySelector( '.mw-parser-output' );
     var content = document.querySelector( '.mw-parser-output' );
     if ( content ) {
     if ( content ) {
Line 137: Line 133:
         if ( p.hasAttribute && p.hasAttribute( 'data-deva' ) ) return;
         if ( p.hasAttribute && p.hasAttribute( 'data-deva' ) ) return;
         if ( p.closest ) {
         if ( p.closest ) {
           if ( p.closest( '.gr-controls' )   ||
           if ( p.closest( '.gr-controls' ) || p.closest( '.mw-editsection' ) ) return;
              p.closest( '.mw-editsection' ) ) return;
         }
         }
         var orig = node.textContent;
         var orig = node.textContent;
Line 150: Line 145:
     }
     }


     // ── Sidebar TOC (.vector-toc-text spans) ─────────────────────
     // Tag .vector-toc-text spans for TOC transliteration
    // We target the .vector-toc-text spans that Vector itself renders,
    // rather than walking raw text nodes. Mutating textContent of an
    // existing child span is a characterData mutation — Vector's own
    // MutationObserver only watches for childList changes on the <li>,
    // so it will NOT fire, and the "unknown title" bug is avoided.
     document.querySelectorAll( '.vector-toc .vector-toc-text' ).forEach( function ( span ) {
     document.querySelectorAll( '.vector-toc .vector-toc-text' ).forEach( function ( span ) {
       if ( span.hasAttribute( 'data-deva' ) ) return; // already tagged
       if ( span.hasAttribute( 'data-deva' ) ) return;
       var orig = span.textContent;
       var orig = span.textContent;
       if ( !orig.trim() ) return;
       if ( !orig.trim() ) return;
Line 169: Line 159:
     currentScript = script;
     currentScript = script;
     translatableSpans.forEach( function ( span ) {
     translatableSpans.forEach( function ( span ) {
       if ( !span.parentNode ) return; // detached node
       if ( !span.parentNode ) return;
       var orig = span.getAttribute( 'data-deva' );
       var orig = span.getAttribute( 'data-deva' );
       if ( !orig ) return;
       if ( !orig ) return;
Line 239: Line 229:
         setLinkActive( li, isActive );
         setLinkActive( li, isActive );


         /* Scroll active item into view if it's off screen */
         /* Scroll active item into view within the TOC container —
        * but ONLY when the TOC sidebar is actually visible and expanded.
        * If the TOC is collapsed or hidden, scrollIntoView scrolls the
        * whole page instead of just the TOC, which hijacks the reading
        * position. */
         if ( isActive ) {
         if ( isActive ) {
           var container = document.querySelector( '.vector-sticky-pinned-container' );
           var container = document.querySelector( '.vector-sticky-pinned-container' );
           if ( container ) {
           if ( container ) {
             var liRect = li.getBoundingClientRect();
            /* Check TOC is visible: the container must have nonzero height
            var cRect   = container.getBoundingClientRect();
            * and must not be hidden by the Vector "pinned/unpinned" toggle */
            if ( liRect.top < cRect.top + 8 || liRect.bottom > cRect.bottom - 8 ) {
             var tocVisible = container.offsetHeight > 0 &&
              li.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
                            container.offsetParent !== null &&
                            window.getComputedStyle( container ).display !== 'none' &&
                            window.getComputedStyle( container ).visibility !== 'hidden';
            if ( tocVisible ) {
              var liRect = li.getBoundingClientRect();
              var cRect = container.getBoundingClientRect();
              if ( liRect.top < cRect.top + 8 || liRect.bottom > cRect.bottom - 8 ) {
                /* Scroll only inside the TOC container, not the page */
                var scrollParent = container.querySelector( '.vector-toc' ) || container;
                var offset = liRect.top - cRect.top;
                if ( offset < 8 || offset > cRect.height - 48 ) {
                  scrollParent.scrollTop += offset - ( cRect.height / 2 );
                }
              }
             }
             }
           }
           }
Line 289: Line 296:
     structObserver.observe( toc, { childList: true, subtree: true } );
     structObserver.observe( toc, { childList: true, subtree: true } );


     /* On initial load, scroll to the already-active item */
     /* On initial load, colour the already-active item.
    * scrollIntoView is intentionally skipped here — calling it while the
    * TOC sidebar might be collapsed causes the PAGE to scroll to the element
    * rather than scrolling within the TOC container.  The liObserver handles
    * scrolling the TOC as the user scrolls the page content. */
     setTimeout( function () {
     setTimeout( function () {
       var active = toc.querySelector( '.vector-toc-list-item-active' );
       var active = toc.querySelector( '.vector-toc-list-item-active' );
       if ( active ) {
       if ( active ) {
         setLinkActive( active, true );
         setLinkActive( active, true );
        active.scrollIntoView({ block: 'nearest', behavior: 'auto' });
       }
       }
     }, 300 );
     }, 300 );
Line 366: Line 376:


}() );
}() );
// ── Main page: by-Grantha / by-Author toggle ──────────────────
// ── Main page: by-Grantha / by-Author toggle ──────────────────
// Runs only on Main_Page. Attaches click handlers to the two
// <div role="button"> toggles emitted by make_main_page().
// Using delegated event attachment here (not onclick= in wikitext)
// because MediaWiki strips onclick= attributes for security.
( function () {
( function () {
   function grHomeView( v ) {
   function grHomeView( v ) {
     var gView = document.getElementById( 'gr-view-grantha' );
     var gView = document.getElementById( 'gr-view-grantha' );
     var aView = document.getElementById( 'gr-view-author' );
     var aView = document.getElementById( 'gr-view-author' );
     var gBtn   = document.getElementById( 'gr-toggle-grantha' );
     var gBtn = document.getElementById( 'gr-toggle-grantha' );
     var aBtn   = document.getElementById( 'gr-toggle-author' );
     var aBtn = document.getElementById( 'gr-toggle-author' );
     if ( !gView || !aView || !gBtn || !aBtn ) return;
     if ( !gView || !aView || !gBtn || !aBtn ) return;
 
     gView.style.display = ( v === 'grantha' ) ? '' : 'none';
     gView.style.display = ( v === 'grantha' ) ? '' : 'none';
     aView.style.display = ( v === 'author'  ) ? '' : 'none';
     aView.style.display = ( v === 'author'  ) ? '' : 'none';
Line 385: Line 393:
     try { localStorage.setItem( 'gr_home_view', v ); } catch ( e ) {}
     try { localStorage.setItem( 'gr_home_view', v ); } catch ( e ) {}
   }
   }
 
   function initHomeToggle() {
   function initHomeToggle() {
     var gBtn = document.getElementById( 'gr-toggle-grantha' );
     var gBtn = document.getElementById( 'gr-toggle-grantha' );
     var aBtn = document.getElementById( 'gr-toggle-author' );
     var aBtn = document.getElementById( 'gr-toggle-author' );
     if ( !gBtn || !aBtn ) return; // not on Main_Page
     if ( !gBtn || !aBtn ) return;
 
     gBtn.addEventListener( 'click', function () { grHomeView( 'grantha' ); } );
     gBtn.addEventListener( 'click', function () { grHomeView( 'grantha' ); } );
     aBtn.addEventListener( 'click', function () { grHomeView( 'author' );  } );
     aBtn.addEventListener( 'click', function () { grHomeView( 'author' );  } );
    // Keyboard support
     [ gBtn, aBtn ].forEach( function ( btn ) {
     [ gBtn, aBtn ].forEach( function ( btn ) {
       btn.addEventListener( 'keydown', function ( e ) {
       btn.addEventListener( 'keydown', function ( e ) {
Line 399: Line 406:
       } );
       } );
     } );
     } );
 
    // Restore persisted preference
     var saved;
     var saved;
     try { saved = localStorage.getItem( 'gr_home_view' ); } catch ( e ) {}
     try { saved = localStorage.getItem( 'gr_home_view' ); } catch ( e ) {}
     if ( saved === 'author' ) grHomeView( 'author' );
     if ( saved === 'author' ) grHomeView( 'author' );
   }
   }
 
   if ( document.readyState === 'loading' ) {
   if ( document.readyState === 'loading' ) {
     document.addEventListener( 'DOMContentLoaded', initHomeToggle );
     document.addEventListener( 'DOMContentLoaded', initHomeToggle );