MediaWiki:Common.js: Difference between revisions

No edit summary
No edit summary
Line 1: Line 1:
/* MediaWiki:Common.js — grantha.io  (v6)
/* MediaWiki:Common.js — grantha.io  (v7)
  *
  *
  * Changes vs v5:
  * Changes vs v6:
  *  1. TOC active highlight fix: Vector 2022 sets the active class on the
  *  1. BUG FIX: The main IIFE was never properly closed in v6 — the "About"
  *    <li.vector-toc-list-item>, but Common.js wraps all text nodes inside
*    link injection block was pasted inside it without the closing }() );
  *     .vector-toc-text spans in <span data-deva="">.  The CSS colour rule
*    This broke the JS module boundary and caused the by-Author toggle on
  *    was targeting .vector-toc-list-item-active > a which never matched
*    the main page to fail (the grHomeView IIFE ran in a corrupted scope).
  *    because the <a> sits deeper and its text is now inside a <span>.
  *    Fix: About link injection is now its own self-contained IIFE, cleanly
  *    Fix: MutationObserver watches the <li> for class changes and directly
*    separated from the transliteration/TOC IIFE.
  *    applies/removes the orange colour via inline style on the .vector-toc-link
  * 2. TOC: Remove the auto-generated "Beginning" link (Vector 2022 always
  *    ancestor, bypassing the CSS specificity war entirely.
  *    inserts a first TOC entry that links to the page top; on Sanskrit
  *  2. All other behaviour identical to v5.
  *    content pages this is unnecessary and clutters the TOC).
*  3. TOC: Add "मूल" (Moola) and "उल्लेख" (Ullekha) nav links below the
  *    document title area — Moola links back to the primary bhashya page
  *    (on teeka/vyakhya pages), Ullekha links to the references index page
*    for the document. Both use the existing data-primary / doc-slug
  *    attributes already present in the page HTML.
  *  4. All other behaviour identical to v6.
  */
  */


Line 87: Line 93:
       'क':'க','ख':'க','ग':'க','घ':'க','ङ':'ங',
       'क':'க','ख':'க','ग':'க','घ':'க','ङ':'ங',
       'च':'ச','छ':'ச','ज':'ஜ','झ':'ஜ','ञ':'ஞ',
       'च':'ச','छ':'ச','ज':'ஜ','झ':'ஜ','ञ':'ஞ',
       'ट':'ட','ठ':'ட','ड':'ட','ढ':'ட','':'ண',
       'ट':'ட','ठ':'ட','ड':'ட','ढ':'ட','':'ண',
       'त':'த','थ':'த','द':'த','ध':'த','न':'ந',
       'त':'த','थ':'த','द':'த','ध':'த','न':'ந',
       'प':'ப','फ':'ப','ब':'ப','भ':'ப','म':'ம',
       'प':'ப','फ':'ப','ब':'ப','भ':'ப','म':'ம',
Line 166: Line 172:
         : transliterateText( orig, script );
         : transliterateText( orig, script );
     } );
     } );
  }
  // ── TOC: Remove "Beginning" link ────────────────────────────────
  // Vector 2022 always inserts a first TOC item linking to the page top
  // (labelled "Beginning" in English UI). On Sanskrit content pages this
  // is redundant — remove it so the TOC starts at the first real heading.
  function removeTocBeginning() {
    var toc = document.querySelector( '.vector-toc' );
    if ( !toc ) return;
    /* The "Beginning" entry lives in a <li> that either:
    *  (a) has class vector-toc-list-item AND contains an <a> whose href
    *      ends in the page title with no fragment (i.e. no "#" anchor), or
    *  (b) is the very first <li> inside .vector-toc-contents whose link
    *      has no hash fragment at all (it scrolls to the very top).
    * We target it by its unique id (#vector-toc-beginning) which Vector
    * 2022 reliably sets, with a fallback to the first fragment-free item.
    */
    var beginning = toc.querySelector( '#vector-toc-beginning' );
    if ( !beginning ) {
      /* Fallback: first <li> whose anchor has no "#" in href */
      var items = toc.querySelectorAll( '.vector-toc-list-item' );
      for ( var i = 0; i < items.length; i++ ) {
        var a = items[ i ].querySelector( 'a' );
        if ( a && a.href.indexOf( '#' ) === -1 ) {
          beginning = items[ i ];
          break;
        }
      }
    }
    if ( beginning && beginning.parentNode ) {
      beginning.parentNode.removeChild( beginning );
    }
  }
  // ── TOC: Inject Moola / Ullekha nav links ───────────────────────
  // Adds two navigation buttons above the TOC list:
  //  मूल  → the primary bhashya page this teeka belongs to
  //          (read from .gr-teeka-page[data-primary] when present,
  //          otherwise derived from the current page path)
  //  उल्लेख → the /Ullekha sub-page for this document,
  //          which lists all pramana references cited in it.
  //
  // The nav bar is injected once and is guarded against duplicate runs.
  function injectTocDocNav() {
    var toc = document.querySelector( '.vector-toc' );
    if ( !toc ) return;
    if ( document.getElementById( 'gr-toc-doc-nav' ) ) return;
    /* ── Determine slugs ─────────────────────────────────────────── */
    var artPath = ( window.mw && mw.config && mw.config.get( 'wgArticlePath' ) ) || '/wiki/$1';
    var pageTitle = ( window.mw && mw.config && mw.config.get( 'wgPageName' ) ) || '';
    /* For teeka/vyakhya pages the imported HTML contains:
    *  <div class="gr-teeka-page" data-primary="Brahmasutra" data-slug="Nyayasudha">
    * Use that when available. */
    var teekaPage  = document.querySelector( '.gr-teeka-page' );
    var primarySlug = teekaPage ? ( teekaPage.getAttribute( 'data-primary' ) || '' ) : '';
    var docSlug    = teekaPage ? ( teekaPage.getAttribute( 'data-slug' )    || '' ) : '';
    /* If not a teeka page, derive slugs from the page title.
    * Page titles look like "Brahmasutra" or "Brahmasutra/Part1". */
    if ( !primarySlug ) {
      /* Top-level doc pages: the slug IS the page (or its root) */
      primarySlug = pageTitle.split( '/' )[ 0 ];
      docSlug    = primarySlug;
    }
    if ( !primarySlug ) return;  /* nothing to link to */
    /* ── Build URLs ──────────────────────────────────────────────── */
    function wikiUrl( slug ) {
      if ( window.mw && mw.util && mw.util.getUrl ) {
        return mw.util.getUrl( slug );
      }
      return artPath.replace( '$1', encodeURIComponent( slug ).replace( /%2F/g, '/' ) );
    }
    var moolaUrl  = wikiUrl( primarySlug );
    var ullekhaUrl = wikiUrl( ( teekaPage ? ( primarySlug + '/Vyakhya/' + docSlug ) : primarySlug ) + '/Ullekha' );
    /* ── Don't show Moola link if we ARE the moola page ─────────── */
    var isMoola = !teekaPage && ( pageTitle === primarySlug || pageTitle.indexOf( primarySlug + '/Part' ) === 0 );
    /* ── Build the nav bar ───────────────────────────────────────── */
    var nav = document.createElement( 'div' );
    nav.id = 'gr-toc-doc-nav';
    var navStyle = [
      'display:flex',
      'gap:6px',
      'padding:6px 8px 4px',
      'border-bottom:1px solid var(--border-color-base,#a2a9b1)',
      'margin-bottom:4px',
      'flex-wrap:wrap',
    ].join( ';' );
    nav.setAttribute( 'style', navStyle );
    var btnBase = [
      'display:inline-block',
      'padding:2px 8px',
      'border-radius:3px',
      'font-size:0.82em',
      'font-weight:500',
      'text-decoration:none',
      'border:1px solid currentColor',
      'line-height:1.6',
      'white-space:nowrap',
    ].join( ';' );
    function makeBtn( href, label, colorVar, bgVar ) {
      var a = document.createElement( 'a' );
      a.href = href;
      a.textContent = label;
      a.setAttribute( 'style',
        btnBase + ';'
        + 'color:' + colorVar + ';'
        + 'background:' + bgVar + ';'
      );
      a.setAttribute( 'data-deva-skip', '1' );  /* don't transliterate these */
      a.addEventListener( 'mouseover', function () {
        this.style.opacity = '0.78';
      } );
      a.addEventListener( 'mouseout', function () {
        this.style.opacity = '1';
      } );
      return a;
    }
    if ( !isMoola ) {
      nav.appendChild( makeBtn( moolaUrl,  'मूल',    '#b04b00', '#fff3e0' ) );
    }
    nav.appendChild(  makeBtn( ullekhaUrl, 'उल्लेख', '#1a6496', '#e8f4fc' ) );
    /* Insert the nav bar at the very top of the TOC, before the heading */
    var tocInner = toc.querySelector( '.vector-toc-contents' ) || toc.firstElementChild || toc;
    toc.insertBefore( nav, tocInner );
   }
   }


   // ── TOC active-item highlight ────────────────────────────────────
   // ── TOC active-item highlight ────────────────────────────────────
   // FIX: Instead of relying on CSS :active selectors (which fail because
   // FIX (v6): Instead of relying on CSS :active selectors (which fail because
   // Common.js wraps text nodes in <span data-deva>, making > a or > .link
   // Common.js wraps text nodes in <span data-deva>, making > a or > .link
   // selectors not match the coloured text), we use a MutationObserver to
   // selectors not match the coloured text), we use a MutationObserver to
Line 199: Line 341:
         link.style.setProperty( 'color',      ACTIVE_COLOR,  'important' );
         link.style.setProperty( 'color',      ACTIVE_COLOR,  'important' );
         link.style.setProperty( 'font-weight', ACTIVE_WEIGHT, 'important' );
         link.style.setProperty( 'font-weight', ACTIVE_WEIGHT, 'important' );
        /* Colour ALL descendant elements — covers both:
        *  • data-deva <span> wrappers (Devanagari mode, textContent replaced)
        *  • .vector-toc-text / .vector-toc-numb spans (all scripts)
        * After applyScript() runs for non-Deva scripts, textContent is a
        * plain text node so querySelectorAll('span') still finds the
        * structural spans (.vector-toc-text etc.) which need colouring. */
         link.querySelectorAll( '*' ).forEach( function ( el ) {
         link.querySelectorAll( '*' ).forEach( function ( el ) {
           el.style.setProperty( 'color', ACTIVE_COLOR, 'important' );
           el.style.setProperty( 'color', ACTIVE_COLOR, 'important' );
Line 221: Line 357:
       li._grHighlightAttached = true;
       li._grHighlightAttached = true;
       liObserver.observe( li, { attributes: true, attributeFilter: [ 'class' ] } );
       liObserver.observe( li, { attributes: true, attributeFilter: [ 'class' ] } );
      /* Apply immediately if already active on attachment */
       if ( li.classList.contains( 'vector-toc-list-item-active' ) ) {
       if ( li.classList.contains( 'vector-toc-list-item-active' ) ) {
         setLinkActive( li, true );
         setLinkActive( li, true );
Line 234: Line 369:
         setLinkActive( li, isActive );
         setLinkActive( li, isActive );


        /* 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 ) {
           /* ── Auto-expand parent section in TOC ────────────────────────
           /* ── Auto-expand parent section in TOC ────────────────────────
           * Vector collapses child sections under a parent li. When a child
           * Vector collapses child sections under a parent li. When a child
           * becomes active we must expand its parent by removing the
           * becomes active we must expand its parent by removing the
           * vector-toc-list-item-collapsed class, mirroring what Vector's
           * vector-toc-list-item-collapsed class. */
          * own JS does on click. Walk up to find collapsed ancestors. */
           var ancestor = li.parentNode;
           var ancestor = li.parentNode;
           while ( ancestor && ancestor !== document.body ) {
           while ( ancestor && ancestor !== document.body ) {
Line 256: Line 385:


           /* ── Scroll active item into view within the TOC ──────────────
           /* ── Scroll active item into view within the TOC ──────────────
           * Use getBoundingClientRect for both the item and the scroll
           * Use getBoundingClientRect so measurement is always in viewport
          * container so the measurement is always in viewport coords —
           * coords — avoids offsetTop-relative-to-offsetParent mismatch. */
           * avoids the offsetTop-relative-to-offsetParent mismatch. */
           var container = document.querySelector( '.vector-sticky-pinned-container' );
           var container = document.querySelector( '.vector-sticky-pinned-container' );
           if ( container ) {
           if ( container ) {
Line 268: Line 396:
               var liRect = li.getBoundingClientRect();
               var liRect = li.getBoundingClientRect();
               var cRect  = container.getBoundingClientRect();
               var cRect  = container.getBoundingClientRect();
              /* Only scroll if the item is outside the visible container area */
               if ( liRect.top < cRect.top + 4 || liRect.bottom > cRect.bottom - 4 ) {
               if ( liRect.top < cRect.top + 4 || liRect.bottom > cRect.bottom - 4 ) {
                /* Walk up to find the actual scrollable ancestor */
                 var scrollHost = null;
                 var scrollHost = null;
                 var node = li.parentNode;
                 var node = li.parentNode;
Line 282: Line 408:
                   node = node.parentNode;
                   node = node.parentNode;
                 }
                 }
                /* Fallback: use container itself if no scrollable ancestor found */
                 if ( !scrollHost && container.scrollHeight > container.clientHeight ) {
                 if ( !scrollHost && container.scrollHeight > container.clientHeight ) {
                   scrollHost = container;
                   scrollHost = container;
                 }
                 }
                 if ( scrollHost ) {
                 if ( scrollHost ) {
                  /* Convert li position to scrollHost-relative using rects */
                   var hostRect = scrollHost.getBoundingClientRect();
                   var hostRect = scrollHost.getBoundingClientRect();
                   var currentScroll = scrollHost.scrollTop;
                   var currentScroll = scrollHost.scrollTop;
Line 306: Line 430:
         m.addedNodes.forEach( function ( n ) {
         m.addedNodes.forEach( function ( n ) {
           if ( n.nodeType !== 1 ) return;
           if ( n.nodeType !== 1 ) return;
          /* Attach highlight observer to newly added list items */
           if ( n.classList && n.classList.contains( 'vector-toc-list-item' ) ) {
           if ( n.classList && n.classList.contains( 'vector-toc-list-item' ) ) {
             attachHighlight( n );
             attachHighlight( n );
Line 314: Line 437:
           }
           }


          /* Tag any new .vector-toc-text spans for transliteration */
           var newSpans = [];
           var newSpans = [];
           if ( n.classList && n.classList.contains( 'vector-toc-text' ) ) newSpans.push( n );
           if ( n.classList && n.classList.contains( 'vector-toc-text' ) ) newSpans.push( n );
Line 337: Line 459:
     structObserver.observe( toc, { childList: true, subtree: true } );
     structObserver.observe( toc, { childList: true, subtree: true } );


    /* 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' );
Line 352: Line 469:
   // ── Init ────────────────────────────────────────────────────────
   // ── Init ────────────────────────────────────────────────────────
   function init() {
   function init() {
    /* Remove appearance panel + watchlist overflow.
    * Vector injects some of these elements via its own JS after DOMContentLoaded,
    * so we use a MutationObserver to catch them whenever they appear. */
    /* Remove appearance panel elements by their stable IDs only.
    * We deliberately avoid class-based selectors like
    * .mw-portlet-vector-user-menu-overflow because on some Vector versions
    * that class name is shared with the user-menu portlet (which contains
    * the login/logout/preferences links) and removing it hides the profile
    * dropdown.  ID-based removal is safe and precise. */
     var HIDE_IDS = [
     var HIDE_IDS = [
       'vector-appearance',
       'vector-appearance',
Line 371: Line 479:
         if ( el && el.parentNode ) el.parentNode.removeChild( el );
         if ( el && el.parentNode ) el.parentNode.removeChild( el );
       } );
       } );
      /* Also remove the appearance toggle button by its aria-controls attribute.
      * Scope the search to page-tools area only — never touch the header user-links. */
       var pageTools = document.getElementById( 'vector-page-tools' ) ||
       var pageTools = document.getElementById( 'vector-page-tools' ) ||
                       document.querySelector( '.vector-page-tools-pinned-container' );
                       document.querySelector( '.vector-page-tools-pinned-container' );
Line 384: Line 490:
     removeHiddenEls();
     removeHiddenEls();


     /* ── Teeka view-mode detection ────────────────────────────────────────
     /* ── Teeka view-mode detection ──────────────────────────────── */
    * Teeka pages carry <div class="gr-teeka-page" data-primary="X" data-slug="Y">
    *
    * Two modes:
    *  gr-standalone  — user opened the teeka URL directly, or ?ref=0
    *                    → plain reading view, no coloured containers
    *  gr-ref-mode    — user navigated here from the main doc, or ?ref=1
    *                    → coloured teeka-block containers (the default styled view)
    *
    * Detection order: ?ref= query param > document.referrer > default (standalone)
    * ─────────────────────────────────────────────────────────────────── */
     ( function detectTeekaMode() {
     ( function detectTeekaMode() {
       var teekaPage = document.querySelector( '.gr-teeka-page' );
       var teekaPage = document.querySelector( '.gr-teeka-page' );
       if ( !teekaPage ) return; // not a teeka page — nothing to do
       if ( !teekaPage ) return;


       var primary = teekaPage.getAttribute( 'data-primary' ) || '';
       var primary = teekaPage.getAttribute( 'data-primary' ) || '';
Line 403: Line 499:
       var mainUrl = artPath.replace( '$1', primary );
       var mainUrl = artPath.replace( '$1', primary );


      // 1. Query param override: ?ref=1 or ?ref=0
       var qs = window.location.search;
       var qs = window.location.search;
       var refParam = qs.match( /[?&]ref=([01])/ );
       var refParam = qs.match( /[?&]ref=([01])/ );
Line 411: Line 506:
       }
       }


      // 2. Referrer check — did we come from the main doc?
       var ref = document.referrer || '';
       var ref = document.referrer || '';
       var fromMain = ref && primary && ref.indexOf( mainUrl ) !== -1;
       var fromMain = ref && primary && ref.indexOf( mainUrl ) !== -1;
Line 425: Line 519:
         if ( needsClean ) removeHiddenEls();
         if ( needsClean ) removeHiddenEls();
       } );
       } );
      /* Watch only direct children of body — NOT subtree.
      * Vector appends the appearance panel as a direct child of body.
      * Using subtree:true would fire on every inner DOM change (including
      * Vector setting active classes on TOC items) and interfere with the
      * liObserver that handles active highlight colouring. */
       hideObserver.observe( document.body, { childList: true, subtree: false } );
       hideObserver.observe( document.body, { childList: true, subtree: false } );
      /* Stop observing after 6s — Vector will have finished by then */
       setTimeout( function () { hideObserver.disconnect(); }, 6000 );
       setTimeout( function () { hideObserver.disconnect(); }, 6000 );
     }
     }
Line 458: Line 546:
     }
     }


    /* TOC setup — remove Beginning link, inject doc-nav, start active watcher.
    * Retry at 300ms and 800ms because Vector 2022 renders the TOC after
    * DOMContentLoaded via its own JS. */
    removeTocBeginning();
    injectTocDocNav();
     watchTocActive();
     watchTocActive();
     /* Retry for Vector 2022 TOC which renders after DOMContentLoaded */
     setTimeout( function () {
    setTimeout( watchTocActive, 300 );
      removeTocBeginning();
     setTimeout( watchTocActive, 800 );
      injectTocDocNav();
      watchTocActive();
    }, 300 );
     setTimeout( function () {
      removeTocBeginning();
      injectTocDocNav();
      watchTocActive();
    }, 800 );
   }
   }


Line 488: Line 588:
         }
         }
         if ( currentScript !== 'deva' ) applyScript( currentScript );
         if ( currentScript !== 'deva' ) applyScript( currentScript );
        removeTocBeginning();
        injectTocDocNav();
         watchTocActive();
         watchTocActive();
       }, 150 );
       }, 150 );
Line 498: Line 600:
     init();
     init();
   }
   }
   /* ── Inject "About" link into the header navigation ──────────────────
 
  * Adds a clean "About" link into the Vector 2022 header, positioned
}() );   /* ← end of main transliteration / TOC IIFE */
  * after the site title/logo area. Uses the existing header link style. */
 
  document.addEventListener( 'DOMContentLoaded', function () {
 
    /* Only inject once */
// ── Inject "About" link into the header navigation ─────────────────
// This is its own IIFE — completely separate from the main IIFE above.
// (v6 bug: it was incorrectly nested inside the main IIFE without a
//  closing }() ); which broke the module boundary and killed the
//  by-Author toggle on the main page.)
( function () {
  function injectAboutLink() {
     if ( document.getElementById( 'gr-about-link' ) ) return;
     if ( document.getElementById( 'gr-about-link' ) ) return;
 
    /* Target: the end of the header start container */
     var headerEnd = document.querySelector( '.vector-header-end' ) ||
     var headerEnd = document.querySelector( '.vector-header-end' ) ||
                     document.querySelector( '#vector-user-links' ) ||
                     document.querySelector( '#vector-user-links' ) ||
                     document.querySelector( '.mw-header' );
                     document.querySelector( '.mw-header' );
     if ( !headerEnd ) return;
     if ( !headerEnd ) return;
 
     var aboutLink = document.createElement( 'a' );
     var aboutLink = document.createElement( 'a' );
     aboutLink.id        = 'gr-about-link';
     aboutLink.id        = 'gr-about-link';
Line 528: Line 635:
       'transition: color 0.15s, background 0.15s',
       'transition: color 0.15s, background 0.15s',
       'white-space: nowrap',
       'white-space: nowrap',
     ].join(';');
     ].join( ';' );
     aboutLink.addEventListener( 'mouseover', function () {
     aboutLink.addEventListener( 'mouseover', function () {
       this.style.color = '#fff';
       this.style.color = '#fff';
Line 537: Line 644:
       this.style.background = 'transparent';
       this.style.background = 'transparent';
     } );
     } );
 
    /* Insert before the user-links block so it sits left of the username */
     var userLinks = document.querySelector( '.vector-user-links' ) ||
     var userLinks = document.querySelector( '.vector-user-links' ) ||
                     document.querySelector( '#pt-userpage' );
                     document.querySelector( '#pt-userpage' );
Line 548: Line 654:
   }
   }


  if ( document.readyState === 'loading' ) {
    document.addEventListener( 'DOMContentLoaded', injectAboutLink );
  } else {
    injectAboutLink();
  }
}() );
}() );




// ── Main page: by-Grantha / by-Author toggle ──────────────────
// ── Main page: by-Grantha / by-Author toggle ──────────────────────
( function () {
( function () {
   function grHomeView( v ) {
   function grHomeView( v ) {