MediaWiki:Common.js: Difference between revisions

Undo revision 5755 by Chandrashekars (talk)
Tag: Undo
No edit summary
Line 91: Line 91:
       'क':'க','ख':'க','ग':'க','घ':'க','ङ':'ங',
       'क':'க','ख':'க','ग':'க','घ':'க','ङ':'ங',
       'च':'ச','छ':'ச','ज':'ஜ','झ':'ஜ','ञ':'ஞ',
       'च':'ச','छ':'ச','ज':'ஜ','झ':'ஜ','ञ':'ஞ',
       'ट':'ட','ठ':'ட','ड':'ட','ढ':'ட','':'ண',
       'ट':'ட','ठ':'ட','ड':'ட','ढ':'ட','':'ண',
       'त':'த','थ':'த','द':'த','ध':'த','न':'ந',
       'त':'த','थ':'த','द':'த','ध':'த','न':'ந',
       'प':'ப','फ':'ப','ब':'ப','भ':'ப','म':'ம',
       'प':'ப','फ':'ப','ब':'ப','भ':'ப','म':'ம',
       'य':'ய','र':'ர','':'ல','ळ':'ழ','व':'வ',
       'य':'ய','र':'ர','':'ல','ळ':'ழ','व':'வ',
       'श':'ஶ','ष':'ஷ','स':'ஸ','ह':'ஹ',
       'श':'ஶ','ष':'ஷ','स':'ஸ','ह':'ஹ',
       'ा':'ா','ि':'ி','ी':'ீ','':'ு','ू':'ூ',
       'ा':'ா','ि':'ி','ी':'ீ','':'ு','ू':'ூ',
       'ृ':'ு','ॄ':'ூ',
       'ृ':'ு','ॄ':'ூ',
       'े':'ே','':'ை','ो':'ோ','ौ':'ௌ',
       'े':'ே','':'ை','ो':'ோ','ौ':'ௌ',
       'ं':'ம்','ः':':','ँ':'ம்','्':'்','ॐ':'ௐ','ऽ':'ௗ',
       'ं':'ம்','ः':':','ँ':'ம்','्':'்','ॐ':'ௐ','ऽ':'ௗ',
       '०':'0','१':'1','२':'2','३':'3','४':'4',
       '०':'0','१':'1','२':'2','३':'3','४':'4',
Line 139: Line 139:
           if ( p.closest( '.gr-controls' )    ) return;
           if ( p.closest( '.gr-controls' )    ) return;
           if ( p.closest( '.mw-editsection' ) ) return;
           if ( p.closest( '.mw-editsection' ) ) return;
          // #gr-toc-doc-nav buttons manage their own data-deva spans in makeBtn
         }
         }
         var orig = node.textContent;
         var orig = node.textContent;
Line 194: Line 193:
   var LABEL = 'विषयसूची';
   var LABEL = 'विषयसूची';


  /* If already inserted, just refresh text */
   var span = titleEl.querySelector('.gr-toc-title');
   var span = titleEl.querySelector('.gr-toc-title');


Line 205: Line 203:


     titleEl.appendChild(span);
     titleEl.appendChild(span);
     translatableSpans.push(span);   // uses your internal array
     translatableSpans.push(span);
   }
   }


Line 218: Line 216:
     var toc = document.querySelector( '.vector-toc' );
     var toc = document.querySelector( '.vector-toc' );
     if ( !toc ) return;
     if ( !toc ) return;
    // Try the dedicated id first (Vector 2022)
     var el = toc.querySelector( '#vector-toc-beginning' );
     var el = toc.querySelector( '#vector-toc-beginning' );
     if ( !el ) {
     if ( !el ) {
      // Fallback: first list-item whose link has no # anchor = the "Beginning" entry
       var items = toc.querySelectorAll( '.vector-toc-list-item' );
       var items = toc.querySelectorAll( '.vector-toc-list-item' );
       for ( var i = 0; i < items.length; i++ ) {
       for ( var i = 0; i < items.length; i++ ) {
Line 288: Line 284:
     nav.setAttribute( 'class', 'toc-main-links');
     nav.setAttribute( 'class', 'toc-main-links');


    // ── CHANGE: makeBtn now wraps label in a data-deva span so
    // transliteration (script switching) applies to button text.
     function makeBtn( href, label ) {
     function makeBtn( href, label ) {
       var a = document.createElement( 'a' );
       var a = document.createElement( 'a' );
Line 448: Line 442:


   // ── TOC: Use Vector's native TOC, add highlight + expand + rename ──────
   // ── TOC: Use Vector's native TOC, add highlight + expand + rename ──────
  // Drops the custom TOC builder entirely. Vector builds the correct TOC
  // from page headings (== Chapter ==, === Section ===). We just:
  //  1. Rename "Contents" → "विषयसूची"
  //  2. Expand all collapsed sections
  //  3. Remove the "Beginning" top link
  //  4. Inject मूल/उल्लेख nav
  //  5. Add reliable IntersectionObserver active-highlight on headings
   function setupToc() {
   function setupToc() {
     if ( _isNoTocPage() ) return;
     if ( _isNoTocPage() ) return;
Line 469: Line 455:


   // ── IntersectionObserver on actual heading elements ──────────────
   // ── IntersectionObserver on actual heading elements ──────────────
  // Watches the real == Heading == elements in the page body.
  // Much more reliable than watching gr-toc-anchor spans.
   var _headingObserver = null;
   var _headingObserver = null;


   function attachHeadingObserver() {
   function attachHeadingObserver() {
     if ( _isNoTocPage() ) return;
     if ( _isNoTocPage() ) return;
     if ( _headingObserver ) return;   // only attach once
     if ( _headingObserver ) return;
     if ( !window.IntersectionObserver ) {
     if ( !window.IntersectionObserver ) {
       watchTocActive();               // fallback for old browsers
       return; /* IntersectionObserver not available — skip active highlight */
      return;
     }
     }


Line 484: Line 467:
     var _activeId    = null;
     var _activeId    = null;


    // Collect all headings that have an id (Vector gives them ids)
     var content  = document.querySelector( '.mw-parser-output' );
     var content  = document.querySelector( '.mw-parser-output' );
     if ( !content ) return;
     if ( !content ) return;
Line 496: Line 478:


     function getTocLink( id ) {
     function getTocLink( id ) {
      // Vector renders TOC links as href="#id"
       return toc.querySelector( 'a[href="#' + CSS.escape( id ) + '"]' );
       return toc.querySelector( 'a[href="#' + CSS.escape( id ) + '"]' );
     }
     }
Line 506: Line 487:


     function clearActive() {
     function clearActive() {
       toc.querySelectorAll( '.vector-toc-list-item-active' ).forEach( function ( li ) {
      /* Clear ALL toc items — prevents multiple items staying highlighted simultaneously */
       toc.querySelectorAll( '.vector-toc-list-item' ).forEach( function ( li ) {
         li.classList.remove( 'vector-toc-list-item-active' );
         li.classList.remove( 'vector-toc-list-item-active' );
         var lnk = li.querySelector( '.vector-toc-link' );
         var lnk = li.querySelector( '.vector-toc-link' );
Line 530: Line 512:
       li.classList.add( 'vector-toc-list-item-active' );
       li.classList.add( 'vector-toc-list-item-active' );


      // Highlight only the innermost active item
       var hasActiveChild = !!li.querySelector(
       var hasActiveChild = !!li.querySelector(
         '.vector-toc-list-item .vector-toc-list-item-active'
         '.vector-toc-list-item .vector-toc-list-item-active'
Line 546: Line 527:
       }
       }


       // Expand collapsed ancestors
       // Expand ALL ancestor sections — remove collapsed, force display
       var anc = li.parentNode;
       var anc = li.parentNode;
       while ( anc && anc !== toc ) {
       while ( anc && anc !== toc ) {
         if ( anc.classList && anc.classList.contains( 'vector-toc-list-item-collapsed' ) ) {
         if ( anc.classList ) {
           anc.classList.remove( 'vector-toc-list-item-collapsed' );
           anc.classList.remove( 'vector-toc-list-item-collapsed' );
        }
        if ( anc.tagName === 'UL' || anc.tagName === 'LI' ) {
          anc.style.removeProperty( 'display' );
         }
         }
         anc = anc.parentNode;
         anc = anc.parentNode;
Line 567: Line 551:
     }
     }


    // Track which headings are visible
     var _visible = new Set();
     var _visible = new Set();


Line 579: Line 562:
       } );
       } );


      // Pick the topmost visible heading
       var topId = null;
       var topId = null;
       var topY  = Infinity;
       var topY  = Infinity;
Line 590: Line 572:
       } );
       } );


      // Nothing visible: find the last heading scrolled past (above viewport)
       if ( !topId ) {
       if ( !topId ) {
         var bestY = -Infinity;
         var bestY = -Infinity;
Line 601: Line 582:
       setActive( topId || null );
       setActive( topId || null );
     }, {
     }, {
      // Fire when heading enters/leaves the top 30% of the viewport
       rootMargin: '-60px 0px -65% 0px',
       rootMargin: '-60px 0px -65% 0px',
       threshold: 0
       threshold: 0
Line 677: Line 657:
     else { currentScript = 'deva'; }
     else { currentScript = 'deva'; }


    // Vector 2022 defers TOC render — retry at 300ms and 800ms
    // Use a single short delay so Vector has rendered the TOC
     setTimeout( setupToc, 200 );
     setTimeout( setupToc, 200 );
   }
   }
Line 689: Line 667:


   // ── React to gr-new-content (siteNav panel rendered new items) ──
   // ── React to gr-new-content (siteNav panel rendered new items) ──
  // Tag any new text nodes added by the documents panel
   window.addEventListener( 'gr-new-content', function ( e ) {
   window.addEventListener( 'gr-new-content', function ( e ) {
     var container = e && e.detail && e.detail.container;
     var container = e && e.detail && e.detail.container;
Line 859: Line 836:
     if ( !needle || needle.length < 4 ) return;
     if ( !needle || needle.length < 4 ) return;


    // Wait for tagTextNodes() to finish wrapping text in data-deva spans,
    // then search those spans rather than raw text nodes.
     function doHighlight() {
     function doHighlight() {
       var content = document.querySelector( '.mw-parser-output' );
       var content = document.querySelector( '.mw-parser-output' );
Line 868: Line 843:
       var found  = false;
       var found  = false;


      // Search data-deva spans first (post-tagTextNodes state)
       var spans = content.querySelectorAll( '[data-deva]' );
       var spans = content.querySelectorAll( '[data-deva]' );
       for ( var i = 0; i < spans.length && !found; i++ ) {
       for ( var i = 0; i < spans.length && !found; i++ ) {
Line 875: Line 849:
         if ( orig.indexOf( snippet ) === -1 ) continue;
         if ( orig.indexOf( snippet ) === -1 ) continue;


        // Replace the span with: text-before + <mark> + text-after
         var idx    = orig.indexOf( snippet );
         var idx    = orig.indexOf( snippet );
         var hlText = orig.slice( idx, Math.min( idx + needle.length, orig.length ) );
         var hlText = orig.slice( idx, Math.min( idx + needle.length, orig.length ) );
Line 899: Line 872:
       }
       }


      // Fallback: raw text nodes (before tagTextNodes runs)
       if ( !found ) {
       if ( !found ) {
         var walker = document.createTreeWalker( content, NodeFilter.SHOW_TEXT );
         var walker = document.createTreeWalker( content, NodeFilter.SHOW_TEXT );
Line 924: Line 896:
     }
     }


    // Delay to let tagTextNodes() and MW rendering finish
     setTimeout( doHighlight, 600 );
     setTimeout( doHighlight, 600 );
   }
   }
Line 951: Line 922:
     wireUllekhaLinks();
     wireUllekhaLinks();
   }
   }
}() );/* ── Search result highlight — highlight query term on arrival ──
}() );/* ── Search result highlight ──────────────────────────────────── */
* When user clicks a search result, we pass the query in the URL
* hash as #gr-search:QUERY or read it from sessionStorage.
* On page load we find all matching text nodes and wrap them.
* ─────────────────────────────────────────────────────────────── */
( function () {
( function () {


  /* ── Step 1: When leaving via a search result link,
    store the query in sessionStorage ── */
   function storeQueryForLink( url, query ) {
   function storeQueryForLink( url, query ) {
     try {
     try {
      /* Store just the pathname so protocol/host differences don't break matching */
       var a = document.createElement( 'a' );
       var a = document.createElement( 'a' );
       a.href = url;
       a.href = url;
       sessionStorage.setItem( 'gr_search_hl', JSON.stringify({
       sessionStorage.setItem( 'gr_search_hl', JSON.stringify({
         query:    query,
         query:    query,
         pathname: a.pathname   // e.g. "/Brahmasutra"
         pathname: a.pathname
       }) );
       }) );
     } catch(e) {}
     } catch(e) {}
   }
   }


  /* ── Step 2: On page load, check if we arrived from a search result ── */
   function applyHighlight() {
   function applyHighlight() {
     var stored;
     var stored;
Line 980: Line 943:
     if ( !stored || !stored.query ) return;
     if ( !stored || !stored.query ) return;


    /* Check pathname matches — lenient comparison */
     var currentPath = window.location.pathname;
     var currentPath = window.location.pathname;
     var storedPath  = stored.pathname || '';
     var storedPath  = stored.pathname || '';
    /* Normalize: remove trailing slash, decode */
     function normPath(p) { return decodeURIComponent(p).replace(/\/+$/, ''); }
     function normPath(p) { return decodeURIComponent(p).replace(/\/+$/, ''); }
     if ( storedPath && normPath(storedPath) !== normPath(currentPath) ) {
     if ( storedPath && normPath(storedPath) !== normPath(currentPath) ) {
      /* Different page — clear and bail */
       try { sessionStorage.removeItem( 'gr_search_hl' ); } catch(e) {}
       try { sessionStorage.removeItem( 'gr_search_hl' ); } catch(e) {}
       return;
       return;
Line 994: Line 954:
     if ( !query ) return;
     if ( !query ) return;


    /* Clear so refreshing doesn't re-highlight */
     try { sessionStorage.removeItem( 'gr_search_hl' ); } catch(e) {}
     try { sessionStorage.removeItem( 'gr_search_hl' ); } catch(e) {}


    /* Wait for content + Common.js tagTextNodes to finish rendering */
     var delays = [400, 900, 1500];
     var delays = [400, 900, 1500];
     delays.forEach( function(ms) {
     delays.forEach( function(ms) {
       setTimeout( function () {
       setTimeout( function () {
        /* Only run if not already highlighted */
         if ( !document.querySelector( '.gr-search-hl' ) ) {
         if ( !document.querySelector( '.gr-search-hl' ) ) {
           highlightText( query );
           highlightText( query );
Line 1,009: Line 966:
   }
   }


  /* ── Core: walk text nodes and wrap matches ── */
   function highlightText( query ) {
   function highlightText( query ) {
     var content = document.querySelector( '#mw-content-text .mw-parser-output' );
     var content = document.querySelector( '#mw-content-text .mw-parser-output' );
     if ( !content ) return;
     if ( !content ) return;


    /* Normalise query — strip quotes, split on spaces */
     var raw = query.replace( /^"|"$/g, '' ).trim();
     var raw = query.replace( /^"|"$/g, '' ).trim();
     if ( !raw ) return;
     if ( !raw ) return;


    /* Build regex — escape special chars, match whole query first,
      fall back to individual words */
     var patterns = [];
     var patterns = [];
    /* Full phrase */
     patterns.push( escapeRegex( raw ) );
     patterns.push( escapeRegex( raw ) );
    /* Individual words (min 2 chars) */
     raw.split( /\s+/ ).forEach( function(w) {
     raw.split( /\s+/ ).forEach( function(w) {
       if ( w.length >= 2 ) patterns.push( escapeRegex( w ) );
       if ( w.length >= 2 ) patterns.push( escapeRegex( w ) );
Line 1,040: Line 991:
     if ( !matched ) return;
     if ( !matched ) return;


    /* Scroll to first highlight */
     var first = document.querySelector( '.gr-search-hl' );
     var first = document.querySelector( '.gr-search-hl' );
     if ( first ) {
     if ( first ) {
       first.scrollIntoView({ behavior: 'smooth', block: 'center' });
       first.scrollIntoView({ behavior: 'smooth', block: 'center' });
      /* Pulse animation */
       first.classList.add( 'gr-search-hl-pulse' );
       first.classList.add( 'gr-search-hl-pulse' );
       setTimeout( function() {
       setTimeout( function() {
Line 1,051: Line 1,000:
     }
     }


    /* Show dismiss button */
     showDismissBar( query );
     showDismissBar( query );
   }
   }
Line 1,064: Line 1,012:
       root, NodeFilter.SHOW_TEXT, {
       root, NodeFilter.SHOW_TEXT, {
         acceptNode: function( node ) {
         acceptNode: function( node ) {
          /* Skip inside script, style, our own highlights */
           var p = node.parentElement;
           var p = node.parentElement;
           if ( !p ) return NodeFilter.FILTER_REJECT;
           if ( !p ) return NodeFilter.FILTER_REJECT;
Line 1,107: Line 1,054:
   }
   }


  /* ── Dismiss bar ── */
   function showDismissBar( query ) {
   function showDismissBar( query ) {
     var isMob = window.innerWidth < 768;
     var isMob = window.innerWidth < 768;
Line 1,113: Line 1,059:
     bar.id = 'gr-hl-bar';
     bar.id = 'gr-hl-bar';


    /* On mobile: float as a pill near top to avoid bottom bar conflicts.
      On desktop: sit at bottom. */
     if ( isMob ) {
     if ( isMob ) {
       bar.style.cssText = [
       bar.style.cssText = [
Line 1,143: Line 1,087:


     if ( isMob ) {
     if ( isMob ) {
      /* Mobile: compact pill — just count + prev/next + dismiss */
       bar.innerHTML =
       bar.innerHTML =
         '<span style="flex-shrink:0">🔍 ' + count + '</span>' +
         '<span style="flex-shrink:0">🔍 ' + count + '</span>' +
Line 1,150: Line 1,093:
         '<button id="gr-hl-dismiss" style="background:rgba(255,255,255,0.15);border:none;color:#fff;min-width:36px;height:36px;border-radius:50%;cursor:pointer;font-size:16px;display:flex;align-items:center;justify-content:center;">✕</button>';
         '<button id="gr-hl-dismiss" style="background:rgba(255,255,255,0.15);border:none;color:#fff;min-width:36px;height:36px;border-radius:50%;cursor:pointer;font-size:16px;display:flex;align-items:center;justify-content:center;">✕</button>';
     } else {
     } else {
      /* Desktop: full bar */
       var nav = document.createElement( 'div' );
       var nav = document.createElement( 'div' );
       nav.style.cssText = 'display:flex;align-items:center;gap:12px;';
       nav.style.cssText = 'display:flex;align-items:center;gap:12px;';
Line 1,178: Line 1,120:
     document.body.appendChild( bar );
     document.body.appendChild( bar );


    /* Prev / Next navigation */
     var hlEls = Array.from( document.querySelectorAll( '.gr-search-hl' ) );
     var hlEls = Array.from( document.querySelectorAll( '.gr-search-hl' ) );
     var currentIdx = 0;
     var currentIdx = 0;
Line 1,207: Line 1,148:
   }
   }


  /* ── Inject CSS for highlights ── */
   function injectHighlightCSS() {
   function injectHighlightCSS() {
     if ( document.getElementById( 'gr-hl-css' ) ) return;
     if ( document.getElementById( 'gr-hl-css' ) ) return;
Line 1,232: Line 1,172:
   }
   }


  /* ── Boot ── */
   injectHighlightCSS();
   injectHighlightCSS();
   if ( document.readyState === 'loading' ) {
   if ( document.readyState === 'loading' ) {
Line 1,240: Line 1,179:
   }
   }


  /* ── Expose storeQueryForLink for readerToolbar to call ── */
   window.grStoreSearchHL = storeQueryForLink;
   window.grStoreSearchHL = storeQueryForLink;


Line 1,251: Line 1,189:
   if ( !document.body.classList.contains( 'skin-minerva' ) ) return;
   if ( !document.body.classList.contains( 'skin-minerva' ) ) return;


   /* ── 1. CSS injected into <head> — beats all other stylesheets ── */
   /* ── 1. CSS injected into <head> ── */
   function injectCSS() {
   function injectCSS() {
     if ( document.getElementById( 'gr-mob-css' ) ) return;
     if ( document.getElementById( 'gr-mob-css' ) ) return;
Line 1,257: Line 1,195:
     s.id = 'gr-mob-css';
     s.id = 'gr-mob-css';
     s.textContent =
     s.textContent =
      /* Body padding — readerToolbar sets this, we clear it */
       'body,#mw-mf-viewport,#mw-mf-page-center{padding-top:0!important;margin-top:0!important;}' +
       'body,#mw-mf-viewport,#mw-mf-page-center{padding-top:0!important;margin-top:0!important;}' +
       'html,body,#mw-mf-viewport,#mw-mf-page-center{overflow-x:hidden!important;max-width:100vw!important;}' +
       'html,body,#mw-mf-viewport,#mw-mf-page-center{overflow-x:hidden!important;max-width:100vw!important;}' +
      /* Header orange + sticky */
       'header.header-container{background:#b5451b!important;position:sticky!important;top:0!important;z-index:300!important;}' +
       'header.header-container{background:#b5451b!important;position:sticky!important;top:0!important;z-index:300!important;}' +
       '.minerva-header{background:#b5451b!important;min-height:54px!important;}' +
       '.minerva-header{background:#b5451b!important;min-height:54px!important;}' +
      /* Header: keep only hamburger + branding, hide search */
       '.minerva-header .search-toggle,.minerva-header .minerva-user-notifications{display:none!important;}' +
       '.minerva-header .search-toggle,.minerva-header .minerva-user-notifications{display:none!important;}' +
      /* Logo row: icon + title wrap cleanly */
       '.branding-box a{display:flex!important;align-items:center!important;' +
       '.branding-box a{display:flex!important;align-items:center!important;' +
         'text-decoration:none!important;max-width:calc(100vw - 80px)!important;}' +
         'text-decoration:none!important;max-width:calc(100vw - 80px)!important;}' +
Line 1,277: Line 1,208:
         'font-family:system-ui,sans-serif!important;line-height:1.2!important;' +
         'font-family:system-ui,sans-serif!important;line-height:1.2!important;' +
         'flex:1 1 auto!important;min-width:0!important;}' +
         'flex:1 1 auto!important;min-width:0!important;}' +
      /* Header icons white */
       '.minerva-header svg path,.minerva-header svg rect,.minerva-header svg circle{fill:#fff!important;}' +
       '.minerva-header svg path,.minerva-header svg rect,.minerva-header svg circle{fill:#fff!important;}' +
       '.minerva-header label{color:#fff!important;}' +
       '.minerva-header label{color:#fff!important;}' +
      /* Hide Page/Discussion tabs + icon toolbar */
       '.minerva-tabs,.mw-portlet-associated-pages,.page-actions-menu,' +
       '.minerva-tabs,.mw-portlet-associated-pages,.page-actions-menu,' +
         '#page-secondary-actions,.last-modified-bar,.minerva-anon-talk-link{display:none!important;}' +
         '#page-secondary-actions,.last-modified-bar,.minerva-anon-talk-link{display:none!important;}' +
      /* Drawer: hide default items, show ours */
      /* #mw-mf-page-left is kept — we replace its content in JS */ '' +
       '#gr-mob-menu-items{display:block!important;}' +
       '#gr-mob-menu-items{display:block!important;}' +
      /* Drawer footer: hide About/Disclaimers */
       '.mw-footer.minerva-footer,.footer-places,.footer-info,' +
       '.mw-footer.minerva-footer,.footer-places,.footer-info,' +
         '.minerva-footer-logo,#footer-places-about,' +
         '.minerva-footer-logo,#footer-places-about,' +
         '#footer-places-disclaimers,#footer-places-privacy{display:none!important;}' +
         '#footer-places-disclaimers,#footer-places-privacy{display:none!important;}' +
      /* ReaderToolbar below header */
       '#gr-static-bar{position:sticky!important;top:54px!important;z-index:200!important;}' +
       '#gr-static-bar{position:sticky!important;top:54px!important;z-index:200!important;}' +
      /* Sections expanded */
       '.mf-section-0,.mf-section-1,.mf-section-2,.mf-section-3,.mf-section-4,' +
       '.mf-section-0,.mf-section-1,.mf-section-2,.mf-section-3,.mf-section-4,' +
         '.mf-section-5,.mf-section-6,.mf-section-7,.mf-section-8,.mf-section-9,' +
         '.mf-section-5,.mf-section-6,.mf-section-7,.mf-section-8,.mf-section-9,' +
Line 1,305: Line 1,223:
       '.section-heading .indicator,.collapsible-heading .indicator{display:none!important;}' +
       '.section-heading .indicator,.collapsible-heading .indicator{display:none!important;}' +
       '.section-heading,.collapsible-heading{pointer-events:none!important;}' +
       '.section-heading,.collapsible-heading{pointer-events:none!important;}' +
      /* Home grid single column */
       '.gr-home-grid{flex-direction:column!important;flex-wrap:nowrap!important;' +
       '.gr-home-grid{flex-direction:column!important;flex-wrap:nowrap!important;' +
         'gap:12px!important;width:100%!important;}' +
         'gap:12px!important;width:100%!important;}' +
Line 1,312: Line 1,228:
         'min-width:unset!important;box-sizing:border-box!important;flex:none!important;}' +
         'min-width:unset!important;box-sizing:border-box!important;flex:none!important;}' +
       '.gr-home-toggle{flex-wrap:wrap!important;}' +
       '.gr-home-toggle{flex-wrap:wrap!important;}' +
      /* Content */
       '.mw-parser-output{font-size:18px!important;line-height:1.8!important;}' +
       '.mw-parser-output{font-size:18px!important;line-height:1.8!important;}' +
       '.mw-parser-output h2,.mw-parser-output h3{width:100%!important;}' +
       '.mw-parser-output h2,.mw-parser-output h3{width:100%!important;}' +
       '.bhashyam-block{margin-left:8px!important;}' +
       '.bhashyam-block{margin-left:8px!important;}' +
       '#footer,.mw-footer,.catlinks,#catlinks{display:none!important;}' +
       '#footer,.mw-footer,.catlinks,#catlinks{display:none!important;}' +
      /* TOC overlay */
       '.gr-mob-toc-panel{position:fixed!important;top:0!important;left:0!important;' +
       '.gr-mob-toc-panel{position:fixed!important;top:0!important;left:0!important;' +
         'bottom:0!important;width:82vw!important;max-width:340px!important;' +
         'bottom:0!important;width:82vw!important;max-width:340px!important;' +
Line 1,360: Line 1,272:
   }
   }


  /* Persistent observer — fight MF re-collapsing */
   function watchSections() {
   function watchSections() {
     var t = null;
     var t = null;
Line 1,383: Line 1,294:
   /* ── 4. Inject custom links into drawer ── */
   /* ── 4. Inject custom links into drawer ── */
   function injectMenuLinks() {
   function injectMenuLinks() {
    /* Already injected */
     if ( document.getElementById( 'gr-mob-menu-items' ) ) return;
     if ( document.getElementById( 'gr-mob-menu-items' ) ) return;


    /* The drawer container is .navigation-drawer (the <nav> element)
      Our links go AFTER the #mw-mf-page-left div (which we hide via CSS) */
     var navDrawer = document.querySelector( '.navigation-drawer' );
     var navDrawer = document.querySelector( '.navigation-drawer' );
     if ( !navDrawer ) return;
     if ( !navDrawer ) return;
Line 1,422: Line 1,330:
     }
     }


    /* Replace contents of #mw-mf-page-left — it's inside the
      toggle-list mechanism so it only shows when drawer is open */
     var pageLeft = document.getElementById( 'mw-mf-page-left' );
     var pageLeft = document.getElementById( 'mw-mf-page-left' );
     if ( pageLeft ) {
     if ( pageLeft ) {
      /* Clear default items and insert ours */
       while ( pageLeft.firstChild ) pageLeft.removeChild( pageLeft.firstChild );
       while ( pageLeft.firstChild ) pageLeft.removeChild( pageLeft.firstChild );
       pageLeft.style.removeProperty( 'display' ); /* un-hide it */
       pageLeft.style.removeProperty( 'display' );
       pageLeft.appendChild( wrap );
       pageLeft.appendChild( wrap );
     } else {
     } else {
      /* Fallback */
       navDrawer.appendChild( wrap );
       navDrawer.appendChild( wrap );
     }
     }


    /* Hide footer portlets */
     document.querySelectorAll(
     document.querySelectorAll(
       '.mw-footer.minerva-footer,.footer-places,.footer-info,' +
       '.mw-footer.minerva-footer,.footer-places,.footer-info,' +
Line 1,490: Line 1,393:
     btn.onclick = open; cls.onclick = close; bd.onclick = close;
     btn.onclick = open; cls.onclick = close; bd.onclick = close;
     body.querySelectorAll('a').forEach(function(a){ a.onclick = close; });
     body.querySelectorAll('a').forEach(function(a){ a.onclick = close; });
  }
  /* ── 6. Moolam / Ullekha links below page title (mobile only) ── */
  function injectMoolaUllekhaLinks() {
    if ( document.getElementById( 'gr-mob-doc-nav' ) ) return;
    var pageName = ( window.mw && mw.config && mw.config.get( 'wgPageName' ) ) || '';
    if ( pageName === 'Main_Page' || !pageName ) return;
    function wikiUrl( slug ) {
      if ( window.mw && mw.util && mw.util.getUrl ) return mw.util.getUrl( slug );
      var ap = ( window.mw && mw.config && mw.config.get( 'wgArticlePath' ) ) || '/wiki/$1';
      return ap.replace( '$1', encodeURIComponent( slug ).replace( /%2F/g, '/' ) );
    }
    var teekaPage      = document.querySelector( '.gr-teeka-page' );
    var primarySlug    = teekaPage
      ? ( teekaPage.getAttribute( 'data-primary' ) || pageName.split('/')[0] )
      : pageName.split('/')[0];
    var docTitleEl    = document.querySelector( '.gr-doc-title' );
    var hasMoolaPage  = docTitleEl && docTitleEl.getAttribute( 'data-has-moola' )  === '1';
    var hasUllekhaPage = docTitleEl && docTitleEl.getAttribute( 'data-has-ullekha' ) === '1';
    var showMoolam  = !!teekaPage || hasMoolaPage;
    var showUllekha = hasUllekhaPage || !!teekaPage;
    if ( !showMoolam && !showUllekha ) return;
    var nav = document.createElement( 'div' );
    nav.id = 'gr-mob-doc-nav';
    nav.style.cssText = 'display:flex;gap:10px;padding:10px 16px 8px;' +
      'background:#fdf8f5;border-bottom:1px solid #f0e0d6;font-family:system-ui,sans-serif;';
    function makeLink( href, label ) {
      var a = document.createElement( 'a' );
      a.href = href; a.textContent = label;
      a.style.cssText = 'display:inline-flex;align-items:center;padding:5px 16px;' +
        'border-radius:20px;background:#fff;border:1.5px solid #e8cfc4;' +
        'color:#b5451b;font-size:14px;font-weight:600;text-decoration:none;';
      return a;
    }
    if ( teekaPage )        nav.appendChild( makeLink( wikiUrl( primarySlug ),              'मूल' ) );
    else if ( hasMoolaPage ) nav.appendChild( makeLink( wikiUrl( primarySlug + '/Moola' ),  'मूलम्' ) );
    if ( showUllekha )      nav.appendChild( makeLink( wikiUrl( primarySlug + '/Ullekha' ), 'उल्लेख' ) );
    /* Insert below the page h1 */
    var h1 = document.getElementById( 'firstHeading' ) ||
            document.querySelector( '.page-heading, h1.firstHeading, .mw-first-heading' );
    if ( h1 && h1.parentNode ) {
      h1.parentNode.insertBefore( nav, h1.nextSibling );
    } else {
      var ct = document.getElementById( 'mw-content-text' );
      if ( ct ) ct.insertBefore( nav, ct.firstChild );
    }
   }
   }


Line 1,500: Line 1,456:
     watchSections();
     watchSections();
     injectMenuLinks();
     injectMenuLinks();
    injectMoolaUllekhaLinks();
     [100, 400, 900, 1800].forEach(function(ms){ setTimeout(expandSections, ms); });
     [100, 400, 900, 1800].forEach(function(ms){ setTimeout(expandSections, ms); });
     setTimeout(initToc, 700);
     setTimeout(initToc, 700);
Line 1,512: Line 1,469:
   if ( window.mw ) {
   if ( window.mw ) {
     mw.hook( 'wikipage.content' ).add(function() {
     mw.hook( 'wikipage.content' ).add(function() {
       setTimeout(function(){ expandSections(); injectMenuLinks(); initToc(); }, 300);
       setTimeout(function(){
        expandSections(); injectMenuLinks(); injectMoolaUllekhaLinks(); initToc();
      }, 300);
     });
     });
   }
   }
Line 1,519: Line 1,478:
/**
/**
  * grantha-mobile-fixes.js
  * grantha-mobile-fixes.js
* Add this to MediaWiki:Common.js (or load as a separate gadget)
*
* Fixes:
*  1. Patches Minerva's ToggleList to guard against null parentNode
*    (prevents "Cannot read properties of null" crash on Main_Page)
*  2. Keeps #gr-home-toggle visible below #gr-static-bar on mobile
*  3. Wires the grantha/author tab toggle if it hasn't been wired yet
  */
  */


Line 1,531: Line 1,483:
   'use strict';
   'use strict';


  /* ══════════════════════════════════════════════════════════════
  * 1. TOGGLELIST NULL GUARD
  * Minerva's initMobile.js runs ToggleList.init() which calls
  * el.parentNode on every .collapsible-block it finds.
  * On Main_Page, some of these are dynamically injected and may
  * not yet be in the live DOM when ToggleList runs.
  * We patch by removing any element that has no parentNode
  * before ToggleList.init() fires.
  * ══════════════════════════════════════════════════════════════ */
   mw.hook( 'wikipage.content' ).add( function () {
   mw.hook( 'wikipage.content' ).add( function () {
    /* Wait one tick so Minerva's own DOMReady handlers run first */
     setTimeout( function () {
     setTimeout( function () {
       var blocks = document.querySelectorAll( '.collapsible-block, .toggle-list' );
       var blocks = document.querySelectorAll( '.collapsible-block, .toggle-list' );
       Array.prototype.forEach.call( blocks, function ( el ) {
       Array.prototype.forEach.call( blocks, function ( el ) {
        /* If the element has no parentNode it's detached — remove it
          before ToggleList tries to call parentNode on it          */
         if ( !el.parentNode ) {
         if ( !el.parentNode ) {
           try { el.remove(); } catch(e) {}
           try { el.remove(); } catch(e) {}
Line 1,553: Line 1,493:
     }, 0 );
     }, 0 );
   } );
   } );
  /* ══════════════════════════════════════════════════════════════
  * 2. MAIN PAGE: keep #gr-home-toggle below the reader bar
  * ══════════════════════════════════════════════════════════════ */


   if ( mw.config.get( 'wgPageName' ) !== 'Main_Page' ) return;
   if ( mw.config.get( 'wgPageName' ) !== 'Main_Page' ) return;
Line 1,563: Line 1,499:
     $( function () {
     $( function () {
       applyHomeToggleOffset();
       applyHomeToggleOffset();
      /* Re-apply when bar height might change (orientation change, etc.) */
       window.addEventListener( 'resize', applyHomeToggleOffset, { passive: true } );
       window.addEventListener( 'resize', applyHomeToggleOffset, { passive: true } );
      /* Re-apply after a short delay in case the bar hasn't rendered yet */
       setTimeout( applyHomeToggleOffset, 300 );
       setTimeout( applyHomeToggleOffset, 300 );
       setTimeout( applyHomeToggleOffset, 800 );
       setTimeout( applyHomeToggleOffset, 800 );
Line 1,578: Line 1,510:


     var barRect = bar.getBoundingClientRect();
     var barRect = bar.getBoundingClientRect();
    /* barRect.bottom = distance from viewport top to bar bottom.
      On load (before any scroll) this equals the bar's actual top offset
      plus its height. After scrolling (Minerva header gone) it's just
      the bar height since bar.top → 0.                                  */
     var barBottom = Math.round( barRect.bottom );
     var barBottom = Math.round( barRect.bottom );


    /* #gr-home is the outermost container */
     var homeEl = document.getElementById( 'gr-home' );
     var homeEl = document.getElementById( 'gr-home' );
    /* #gr-home-toggle is the tab switcher row */
     var toggleEl = document.getElementById( 'gr-home-toggle' );
     var toggleEl = document.getElementById( 'gr-home-toggle' );
    /* Also target the mw-parser-output first child as a fallback */
     var firstChild = document.querySelector(
     var firstChild = document.querySelector(
       '#mw-content-text .mw-parser-output > .gr-home, ' +
       '#mw-content-text .mw-parser-output > .gr-home, ' +
Line 1,594: Line 1,519:
     );
     );


    /* scroll-margin-top: so that if someone scrolls-to-top the toggle
      is visible below the bar (not behind it)                        */
     [ homeEl, toggleEl, firstChild ].forEach( function ( el ) {
     [ homeEl, toggleEl, firstChild ].forEach( function ( el ) {
       if ( el ) el.style.scrollMarginTop = ( barBottom + 4 ) + 'px';
       if ( el ) el.style.scrollMarginTop = ( barBottom + 4 ) + 'px';
     } );
     } );


    /* The body already has padding-top set by readerToolbar.js.
      But on Main_Page the toggle might STILL be hidden if the
      Minerva header is taller. Force the content area's top
      padding to be at least barBottom.                                */
     var contentText = document.getElementById( 'mw-content-text' );
     var contentText = document.getElementById( 'mw-content-text' );
     if ( contentText ) {
     if ( contentText ) {
      /* Only add extra padding if we're on mobile */
       var isMob = window.innerWidth < 768 || !!document.getElementById( 'mw-mf-viewport' );
       var isMob = window.innerWidth < 768 || !!document.getElementById( 'mw-mf-viewport' );
       if ( isMob ) {
       if ( isMob ) {
        /* Check if the toggle is actually obscured */
         if ( toggleEl ) {
         if ( toggleEl ) {
           var toggleRect = toggleEl.getBoundingClientRect();
           var toggleRect = toggleEl.getBoundingClientRect();
          /* If toggle top < barBottom, the bar is covering it */
           if ( toggleRect.top < barBottom ) {
           if ( toggleRect.top < barBottom ) {
             var currentPT = parseInt( window.getComputedStyle( document.body ).paddingTop, 10 ) || 0;
             var currentPT = parseInt( window.getComputedStyle( document.body ).paddingTop, 10 ) || 0;
Line 1,623: Line 1,539:
   }
   }


  /* ══════════════════════════════════════════════════════════════
  * 3. WIRE THE GRANTHA / AUTHOR TAB TOGGLE
  * In case the toggle script hasn't loaded yet or failed
  * ══════════════════════════════════════════════════════════════ */
   $( function () {
   $( function () {
     var $toggle  = $( '#gr-home-toggle' );
     var $toggle  = $( '#gr-home-toggle' );
Line 1,635: Line 1,547:


     if ( !$toggle.length || !$viewG.length || !$viewA.length ) return;
     if ( !$toggle.length || !$viewG.length || !$viewA.length ) return;
    /* Only wire if not already wired */
     if ( $toggle.data( 'gr-wired' ) ) return;
     if ( $toggle.data( 'gr-wired' ) ) return;
     $toggle.data( 'gr-wired', true );
     $toggle.data( 'gr-wired', true );
Line 1,661: Line 1,572:
     });
     });


    /* Restore last-used tab */
     try {
     try {
       var saved = localStorage.getItem( 'grantha_home_tab' );
       var saved = localStorage.getItem( 'grantha_home_tab' );