MediaWiki:Common.js: Difference between revisions
No edit summary Tags: Manual revert Reverted |
Undo revision 5755 by Chandrashekars (talk) Tag: Undo |
||
| Line 1,242: | Line 1,242: | ||
/* ── Expose storeQueryForLink for readerToolbar to call ── */ | /* ── Expose storeQueryForLink for readerToolbar to call ── */ | ||
window.grStoreSearchHL = storeQueryForLink; | window.grStoreSearchHL = storeQueryForLink; | ||
}() ); | |||
/* ═══════════════════════════════════════════════════════════════ | |||
Mobile addon — paste at bottom of MediaWiki:Common.js | |||
Only runs on Minerva (mobile) skin | |||
═══════════════════════════════════════════════════════════════ */ | |||
( function () { | |||
if ( !document.body.classList.contains( 'skin-minerva' ) ) return; | |||
/* ── 1. CSS injected into <head> — beats all other stylesheets ── */ | |||
function injectCSS() { | |||
if ( document.getElementById( 'gr-mob-css' ) ) return; | |||
var s = document.createElement( 'style' ); | |||
s.id = 'gr-mob-css'; | |||
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;}' + | |||
'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;}' + | |||
'.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;}' + | |||
/* Logo row: icon + title wrap cleanly */ | |||
'.branding-box a{display:flex!important;align-items:center!important;' + | |||
'text-decoration:none!important;max-width:calc(100vw - 80px)!important;}' + | |||
'.branding-box a::before{content:"";display:block;width:30px;height:30px;flex-shrink:0;' + | |||
'background:url("/favicon.png") center/contain no-repeat;' + | |||
'margin-right:8px;}' + | |||
'.branding-box a span{color:#fff!important;font-size:16px!important;font-weight:700!important;' + | |||
'font-family:system-ui,sans-serif!important;line-height:1.2!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 label{color:#fff!important;}' + | |||
/* Hide Page/Discussion tabs + icon toolbar */ | |||
'.minerva-tabs,.mw-portlet-associated-pages,.page-actions-menu,' + | |||
'#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;}' + | |||
/* Drawer footer: hide About/Disclaimers */ | |||
'.mw-footer.minerva-footer,.footer-places,.footer-info,' + | |||
'.minerva-footer-logo,#footer-places-about,' + | |||
'#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;}' + | |||
/* Sections expanded */ | |||
'.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-10{display:block!important;visibility:visible!important;}' + | |||
'.collapsible-block{display:block!important;}' + | |||
'.section-heading .indicator,.collapsible-heading .indicator{display: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;' + | |||
'gap:12px!important;width:100%!important;}' + | |||
'.gr-home-card{width:100%!important;max-width:100%!important;' + | |||
'min-width:unset!important;box-sizing:border-box!important;flex:none!important;}' + | |||
'.gr-home-toggle{flex-wrap:wrap!important;}' + | |||
/* Content */ | |||
'.mw-parser-output{font-size:18px!important;line-height:1.8!important;}' + | |||
'.mw-parser-output h2,.mw-parser-output h3{width:100%!important;}' + | |||
'.bhashyam-block{margin-left:8px!important;}' + | |||
'#footer,.mw-footer,.catlinks,#catlinks{display:none!important;}' + | |||
/* TOC overlay */ | |||
'.gr-mob-toc-panel{position:fixed!important;top:0!important;left:0!important;' + | |||
'bottom:0!important;width:82vw!important;max-width:340px!important;' + | |||
'background:#fff!important;z-index:10400!important;' + | |||
'box-shadow:4px 0 28px rgba(0,0,0,0.22)!important;overflow-y:auto!important;' + | |||
'padding:0 0 40px!important;transform:translateX(-110%)!important;' + | |||
'transition:transform 0.26s cubic-bezier(0.4,0,0.2,1)!important;display:block!important;}' + | |||
'.gr-mob-toc-panel.open{transform:translateX(0)!important;}' + | |||
'.gr-mob-toc-backdrop{display:none!important;position:fixed!important;inset:0!important;' + | |||
'background:rgba(0,0,0,0.4)!important;z-index:10399!important;}' + | |||
'.gr-mob-toc-backdrop.open{display:block!important;}' + | |||
'.gr-mob-toc-header{position:sticky!important;top:0!important;background:#fff!important;' + | |||
'display:flex!important;align-items:center!important;justify-content:space-between!important;' + | |||
'padding:16px 16px 12px!important;border-bottom:1px solid #f0ebe6!important;z-index:1!important;}' + | |||
'.gr-mob-toc-title{font-size:13px!important;font-weight:700!important;text-transform:uppercase!important;' + | |||
'letter-spacing:0.08em!important;color:#b5451b!important;font-family:system-ui,sans-serif!important;}' + | |||
'.gr-mob-toc-close{background:none!important;border:none!important;' + | |||
'font-size:22px!important;color:#999!important;cursor:pointer!important;padding:4px 8px!important;}' + | |||
'.gr-mob-toc-body{padding:12px 16px!important;}' + | |||
'.gr-mob-toc-body a{display:block!important;font-size:16px!important;line-height:1.6!important;' + | |||
'color:#2c1810!important;text-decoration:none!important;padding:8px 0!important;' + | |||
'border-bottom:1px solid #f5f0ed!important;}'; | |||
document.head.appendChild( s ); | |||
} | |||
/* ── 2. Expand hidden sections ── */ | |||
function expandSections() { | |||
document.querySelectorAll( '[class*="mf-section-"], .collapsible-block' ) | |||
.forEach( function ( el ) { | |||
el.removeAttribute( 'hidden' ); | |||
el.style.setProperty( 'display', 'block', 'important' ); | |||
el.style.setProperty( 'visibility', 'visible', 'important' ); | |||
el.removeAttribute( 'aria-hidden' ); | |||
} ); | |||
document.querySelectorAll( '.section-heading, .collapsible-heading' ) | |||
.forEach( function ( el ) { | |||
el.setAttribute( 'aria-expanded', 'true' ); | |||
el.style.setProperty( 'pointer-events', 'none', 'important' ); | |||
} ); | |||
} | |||
/* Persistent observer — fight MF re-collapsing */ | |||
function watchSections() { | |||
var t = null; | |||
var obs = new MutationObserver( function ( ms ) { | |||
if ( ms.some( function(m){ return m.attributeName === 'hidden'; } ) ) { | |||
clearTimeout(t); t = setTimeout( expandSections, 30 ); | |||
} | |||
} ); | |||
var root = document.querySelector( '#mw-content-text' ) || document.body; | |||
obs.observe( root, { subtree:true, attributes:true, attributeFilter:['hidden','aria-hidden'] } ); | |||
} | |||
/* ── 3. Body padding observer ── */ | |||
function watchBodyPadding() { | |||
new MutationObserver( function () { | |||
if ( document.body.style.paddingTop && document.body.style.paddingTop !== '0px' ) { | |||
document.body.style.paddingTop = ''; | |||
} | |||
} ).observe( document.body, { attributes:true, attributeFilter:['style'] } ); | |||
} | |||
/* ── 4. Inject custom links into drawer ── */ | |||
function injectMenuLinks() { | |||
/* Already injected */ | |||
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' ); | |||
if ( !navDrawer ) return; | |||
var wrap = document.createElement( 'div' ); | |||
wrap.id = 'gr-mob-menu-items'; | |||
wrap.style.cssText = 'width:100%;background:#fff;margin-top:8px;'; | |||
var itemStyle = 'display:flex;align-items:center;gap:14px;padding:15px 20px;' + | |||
'font-size:16px;color:#2c1810;text-decoration:none;' + | |||
'font-family:system-ui,sans-serif;border-bottom:1px solid #f0ebe6;background:#fff;'; | |||
function makeItem( href, emoji, label ) { | |||
var a = document.createElement( 'a' ); | |||
a.href = href; | |||
a.style.cssText = itemStyle; | |||
a.innerHTML = | |||
'<span>' + label + '</span>'; | |||
return a; | |||
} | |||
wrap.appendChild( makeItem( '/Main_Page', '', 'Home' ) ); | |||
wrap.appendChild( makeItem( '/My_wiki:Help', '', 'Help' ) ); | |||
wrap.appendChild( makeItem( '/My_wiki:About', '', 'About' ) ); | |||
var userName = window.mw ? mw.config.get( 'wgUserName' ) : null; | |||
if ( userName ) { | |||
var la = document.querySelector( 'a[href*="action=logout"]' ); | |||
wrap.appendChild( makeItem( | |||
la ? la.href : '/index.php?title=Special:UserLogout', '', 'Log out' | |||
) ); | |||
} else { | |||
wrap.appendChild( makeItem( '/index.php?title=Special:UserLogin', '', 'Log in' ) ); | |||
} | |||
/* 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' ); | |||
if ( pageLeft ) { | |||
/* Clear default items and insert ours */ | |||
while ( pageLeft.firstChild ) pageLeft.removeChild( pageLeft.firstChild ); | |||
pageLeft.style.removeProperty( 'display' ); /* un-hide it */ | |||
pageLeft.appendChild( wrap ); | |||
} else { | |||
/* Fallback */ | |||
navDrawer.appendChild( wrap ); | |||
} | |||
/* Hide footer portlets */ | |||
document.querySelectorAll( | |||
'.mw-footer.minerva-footer,.footer-places,.footer-info,' + | |||
'.minerva-footer-logo,#footer-places-about,' + | |||
'#footer-places-disclaimers,#footer-places-privacy' | |||
).forEach( function(el) { el.style.setProperty('display','none','important'); } ); | |||
} | |||
/* ── 5. Mobile TOC overlay ── */ | |||
var _tocDone = false; | |||
function initToc() { | |||
if ( _tocDone ) return; | |||
var tocList = document.querySelector( '.vector-toc-contents, .vector-toc .vector-toc-list' ); | |||
if ( !tocList ) return; | |||
if ( !tocList.querySelector( 'li' ) ) return; | |||
_tocDone = true; | |||
var bd = document.createElement( 'div' ); | |||
bd.className = 'gr-mob-toc-backdrop'; | |||
document.body.appendChild( bd ); | |||
var panel = document.createElement( 'div' ); | |||
panel.className = 'gr-mob-toc-panel'; | |||
var hdr = document.createElement( 'div' ); | |||
hdr.className = 'gr-mob-toc-header'; | |||
var ttl = document.createElement( 'div' ); | |||
ttl.className = 'gr-mob-toc-title'; | |||
ttl.textContent = 'विषयसूची'; | |||
var cls = document.createElement( 'button' ); | |||
cls.className = 'gr-mob-toc-close'; | |||
cls.textContent = '✕'; | |||
hdr.appendChild( ttl ); hdr.appendChild( cls ); | |||
panel.appendChild( hdr ); | |||
var body = document.createElement( 'div' ); | |||
body.className = 'gr-mob-toc-body'; | |||
body.appendChild( tocList.cloneNode( true ) ); | |||
panel.appendChild( body ); | |||
document.body.appendChild( panel ); | |||
var btn = document.createElement( 'button' ); | |||
btn.id = 'gr-mob-toc-btn'; | |||
btn.innerHTML = '☰ Contents'; | |||
btn.style.cssText = 'position:fixed;bottom:148px;left:16px;z-index:9100;' + | |||
'background:#fff;border:1.5px solid #b5451b;border-radius:24px;' + | |||
'padding:10px 16px;font-size:15px;font-family:system-ui,sans-serif;' + | |||
'color:#b5451b;font-weight:600;box-shadow:0 3px 14px rgba(0,0,0,0.15);cursor:pointer;'; | |||
document.body.appendChild( btn ); | |||
function open() { panel.classList.add('open'); bd.classList.add('open'); document.body.style.overflow='hidden'; } | |||
function close() { panel.classList.remove('open'); bd.classList.remove('open'); document.body.style.overflow=''; } | |||
btn.onclick = open; cls.onclick = close; bd.onclick = close; | |||
body.querySelectorAll('a').forEach(function(a){ a.onclick = close; }); | |||
} | |||
/* ── Boot ── */ | |||
injectCSS(); | |||
watchBodyPadding(); | |||
function boot() { | |||
expandSections(); | |||
watchSections(); | |||
injectMenuLinks(); | |||
[100, 400, 900, 1800].forEach(function(ms){ setTimeout(expandSections, ms); }); | |||
setTimeout(initToc, 700); | |||
} | |||
if ( document.readyState === 'loading' ) { | |||
document.addEventListener( 'DOMContentLoaded', boot ); | |||
} else { | |||
boot(); | |||
} | |||
if ( window.mw ) { | |||
mw.hook( 'wikipage.content' ).add(function() { | |||
setTimeout(function(){ expandSections(); injectMenuLinks(); initToc(); }, 300); | |||
}); | |||
} | |||
}() ); | |||
/** | |||
* 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 | |||
*/ | |||
( function () { | |||
'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 () { | |||
/* Wait one tick so Minerva's own DOMReady handlers run first */ | |||
setTimeout( function () { | |||
var blocks = document.querySelectorAll( '.collapsible-block, .toggle-list' ); | |||
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 ) { | |||
try { el.remove(); } catch(e) {} | |||
} | |||
} ); | |||
}, 0 ); | |||
} ); | |||
/* ══════════════════════════════════════════════════════════════ | |||
* 2. MAIN PAGE: keep #gr-home-toggle below the reader bar | |||
* ══════════════════════════════════════════════════════════════ */ | |||
if ( mw.config.get( 'wgPageName' ) !== 'Main_Page' ) return; | |||
mw.loader.using( 'mediawiki.util' ).done( function () { | |||
$( function () { | |||
applyHomeToggleOffset(); | |||
/* Re-apply when bar height might change (orientation change, etc.) */ | |||
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, 800 ); | |||
} ); | |||
} ); | |||
function applyHomeToggleOffset() { | |||
var bar = document.getElementById( 'gr-static-bar' ); | |||
if ( !bar ) return; | |||
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 ); | |||
/* #gr-home is the outermost container */ | |||
var homeEl = document.getElementById( 'gr-home' ); | |||
/* #gr-home-toggle is the tab switcher row */ | |||
var toggleEl = document.getElementById( 'gr-home-toggle' ); | |||
/* Also target the mw-parser-output first child as a fallback */ | |||
var firstChild = document.querySelector( | |||
'#mw-content-text .mw-parser-output > .gr-home, ' + | |||
'#mw-content-text .mw-parser-output > *:first-child' | |||
); | |||
/* 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 ) { | |||
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' ); | |||
if ( contentText ) { | |||
/* Only add extra padding if we're on mobile */ | |||
var isMob = window.innerWidth < 768 || !!document.getElementById( 'mw-mf-viewport' ); | |||
if ( isMob ) { | |||
/* Check if the toggle is actually obscured */ | |||
if ( toggleEl ) { | |||
var toggleRect = toggleEl.getBoundingClientRect(); | |||
/* If toggle top < barBottom, the bar is covering it */ | |||
if ( toggleRect.top < barBottom ) { | |||
var currentPT = parseInt( window.getComputedStyle( document.body ).paddingTop, 10 ) || 0; | |||
var needed = currentPT + ( barBottom - toggleRect.top ) + 4; | |||
document.body.style.paddingTop = needed + 'px'; | |||
} | |||
} | |||
} | |||
} | |||
} | |||
/* ══════════════════════════════════════════════════════════════ | |||
* 3. WIRE THE GRANTHA / AUTHOR TAB TOGGLE | |||
* In case the toggle script hasn't loaded yet or failed | |||
* ══════════════════════════════════════════════════════════════ */ | |||
$( function () { | |||
var $toggle = $( '#gr-home-toggle' ); | |||
var $viewG = $( '#gr-view-grantha' ); | |||
var $viewA = $( '#gr-view-author' ); | |||
var $btnG = $( '#gr-toggle-grantha' ); | |||
var $btnA = $( '#gr-toggle-author' ); | |||
if ( !$toggle.length || !$viewG.length || !$viewA.length ) return; | |||
/* Only wire if not already wired */ | |||
if ( $toggle.data( 'gr-wired' ) ) return; | |||
$toggle.data( 'gr-wired', true ); | |||
function showView( which ) { | |||
if ( which === 'grantha' ) { | |||
$viewG.show(); $viewA.hide(); | |||
$btnG.addClass( 'gr-toggle-active' ); | |||
$btnA.removeClass( 'gr-toggle-active' ); | |||
} else { | |||
$viewA.show(); $viewG.hide(); | |||
$btnA.addClass( 'gr-toggle-active' ); | |||
$btnG.removeClass( 'gr-toggle-active' ); | |||
} | |||
try { localStorage.setItem( 'grantha_home_tab', which ); } catch(e){} | |||
} | |||
$btnG.on( 'click keydown', function(e){ | |||
if ( e.type === 'keydown' && e.key !== 'Enter' && e.key !== ' ' ) return; | |||
showView( 'grantha' ); | |||
}); | |||
$btnA.on( 'click keydown', function(e){ | |||
if ( e.type === 'keydown' && e.key !== 'Enter' && e.key !== ' ' ) return; | |||
showView( 'author' ); | |||
}); | |||
/* Restore last-used tab */ | |||
try { | |||
var saved = localStorage.getItem( 'grantha_home_tab' ); | |||
if ( saved === 'author' ) showView( 'author' ); | |||
else showView( 'grantha' ); | |||
} catch(e) { showView( 'grantha' ); } | |||
} ); | |||
}() ); | }() ); | ||
Revision as of 12:23, 21 May 2026
/* MediaWiki:Common.js — grantha.io (v7)
*
* Changes vs v6:
* 1. BUG FIX: Main IIFE was never properly closed — About link block was
* pasted inside it without the closing }() ); which broke the module
* boundary and killed the by-Author toggle. Each block is now its
* own self-contained IIFE.
* 2. TOC: "Beginning" link removed.
* 3. TOC: Heading label → "विषयसूची".
* 4. TOC: मूल / उल्लेख nav links injected at top of TOC panel.
* 5. TOC: All subsections expanded by default.
* 6. TOC: Only the active item is bold; all others are normal weight.
* 7. About link URL derived from wgArticlePath — works under any
* sub-path install (e.g. /My_wiki/). Points to "My_wiki:About".
* 8. TOC customisations skipped on Main_Page and About pages.
* 9. Uses Vector's native TOC with IntersectionObserver-based active highlight.
*/
( function () {
var LS_SCRIPT_KEY = 'grantha_reader_script';
var currentScript = 'deva';
// ── IAST transliteration ────────────────────────────────────────
function devanagariToIAST( text ) {
var CONSONANTS = {
'क':'k','ख':'kh','ग':'g','घ':'gh','ङ':'ṅ',
'च':'c','छ':'ch','ज':'j','झ':'jh','ञ':'ñ',
'ट':'ṭ','ठ':'ṭh','ड':'ḍ','ढ':'ḍh','ण':'ṇ',
'त':'t','थ':'th','द':'d','ध':'dh','न':'n',
'प':'p','फ':'ph','ब':'b','भ':'bh','म':'m',
'य':'y','र':'r','ल':'l','ळ':'ḷ','व':'v',
'श':'ś','ष':'ṣ','स':'s','ह':'h'
};
var DIACRITICS = {
'ा':'ā','ि':'i','ी':'ī','ु':'u','ू':'ū',
'ृ':'ṛ','ॄ':'ṝ','े':'e','ै':'ai','ो':'o','ौ':'au'
};
var VOWELS = {
'अ':'a','आ':'ā','इ':'i','ई':'ī','उ':'u','ऊ':'ū',
'ऋ':'ṛ','ॠ':'ṝ','ए':'e','ऐ':'ai','ओ':'o','औ':'au','ऽ':"'"
};
var MISC = {
'ं':'ṃ','ः':'ḥ','ँ':'m̐','ॐ':'oṃ',
'०':'0','१':'1','२':'2','३':'3','४':'4',
'५':'5','६':'6','७':'7','८':'8','९':'9'
};
var HALANTA = '्';
var chars = Array.from( text );
var result = '';
var i = 0;
while ( i < chars.length ) {
var ch = chars[ i ];
var next = chars[ i + 1 ];
if ( CONSONANTS[ ch ] ) {
var base = CONSONANTS[ ch ];
if ( next === HALANTA ) { result += base; i += 2; }
else if ( DIACRITICS[ next ] ) { result += base + DIACRITICS[ next ]; i += 2; }
else if ( next === 'ं' || next === 'ः' ) { result += base + 'a' + MISC[ next ]; i += 2; }
else { result += base + 'a'; i++; }
} else if ( VOWELS[ ch ] ) { result += VOWELS[ ch ]; i++; }
else if ( DIACRITICS[ ch ] ) { result += DIACRITICS[ ch ]; i++; }
else if ( MISC[ ch ] ) { result += MISC[ ch ]; i++; }
else { result += ch; i++; }
}
return result;
}
// ── Character maps for Kannada / Tamil ─────────────────────────
var SCRIPT_MAP = {
kn: {
'अ':'ಅ','आ':'ಆ','इ':'ಇ','ई':'ಈ','उ':'ಉ','ऊ':'ಊ','ऋ':'ಋ',
'ए':'ಏ','ऐ':'ಐ','ओ':'ಓ','औ':'ಔ','ऽ':'ಽ',
'क':'ಕ','ख':'ಖ','ग':'ಗ','घ':'ಘ','ङ':'ಙ',
'च':'ಚ','छ':'ಛ','ज':'ಜ','झ':'ಝ','ञ':'ಞ',
'ट':'ಟ','ठ':'ಠ','ड':'ಡ','ढ':'ಢ','ण':'ಣ',
'त':'ತ','थ':'ಥ','द':'ದ','ध':'ಧ','न':'ನ',
'प':'ಪ','फ':'ಫ','ब':'ಬ','भ':'ಭ','म':'ಮ',
'य':'ಯ','र':'ರ','ल':'ಲ','व':'ವ',
'श':'ಶ','ष':'ಷ','स':'ಸ','ह':'ಹ',
'ा':'ಾ','ि':'ಿ','ी':'ೀ','ु':'ು','ू':'ೂ',
'ृ':'ೃ','े':'ೇ','ै':'ೈ','ो':'ೋ','ौ':'ೌ',
'ं':'ಂ','ः':'ಃ','्':'್',
'०':'೦','१':'೧','२':'೨','३':'೩','४':'೪',
'५':'೫','६':'೬','७':'೭','८':'೮','९':'೯'
},
ta: {
'अ':'அ','आ':'ஆ','इ':'இ','ई':'ஈ','उ':'உ','ऊ':'ஊ',
'ऋ':'ரு','ॠ':'ரூ',
'ए':'ஏ','ऐ':'ஐ','ओ':'ஓ','औ':'ஔ',
'क':'க','ख':'க','ग':'க','घ':'க','ङ':'ங',
'च':'ச','छ':'ச','ज':'ஜ','झ':'ஜ','ञ':'ஞ',
'ट':'ட','ठ':'ட','ड':'ட','ढ':'ட','ண':'ண',
'त':'த','थ':'த','द':'த','ध':'த','न':'ந',
'प':'ப','फ':'ப','ब':'ப','भ':'ப','म':'ம',
'य':'ய','र':'ர','ல':'ல','ळ':'ழ','व':'வ',
'श':'ஶ','ष':'ஷ','स':'ஸ','ह':'ஹ',
'ा':'ா','ि':'ி','ी':'ீ','ு':'ு','ू':'ூ',
'ृ':'ு','ॄ':'ூ',
'े':'ே','ை':'ை','ो':'ோ','ौ':'ௌ',
'ं':'ம்','ः':':','ँ':'ம்','्':'்','ॐ':'ௐ','ऽ':'ௗ',
'०':'0','१':'1','२':'2','३':'3','४':'4',
'५':'5','६':'6','७':'7','८':'8','९':'9'
}
};
var PRE = [
[ /ङ्क/g, 'ंक' ], [ /ङ्ख/g, 'ंख' ], [ /ङ्ग/g, 'ंग' ], [ /ङ्घ/g, 'ंघ' ],
[ /ञ्च/g, 'ंच' ], [ /ञ्ज/g, 'ंज' ], [ /ण्ट/g, 'ंट' ], [ /ण्ड/g, 'ंड' ],
[ /न्त/g, 'ंत' ], [ /न्द/g, 'ंद' ], [ /म्ब/g, 'ंब' ], [ /म्भ/g, 'ंभ' ]
];
function transliterateText( text, script ) {
if ( script === 'en' ) return devanagariToIAST( text );
var map = SCRIPT_MAP[ script ];
if ( !map ) return text;
var t = text;
PRE.forEach( function ( p ) { t = t.replace( p[ 0 ], p[ 1 ] ); } );
return Array.from( t ).map( function ( ch ) {
return map[ ch ] !== undefined ? map[ ch ] : ch;
} ).join( '' );
}
// ── Tag all transliteratable text nodes once per page ───────────
var translatableSpans = [];
function tagTextNodes() {
var content = document.querySelector( '.mw-parser-output' );
if ( content ) {
var walker = document.createTreeWalker( content, NodeFilter.SHOW_TEXT );
var nodes = [];
while ( walker.nextNode() ) nodes.push( walker.currentNode );
nodes.forEach( function ( node ) {
var p = node.parentNode;
if ( !p ) return;
if ( p.hasAttribute && p.hasAttribute( 'data-deva' ) ) return;
if ( p.closest ) {
if ( p.closest( '.gr-controls' ) ) return;
if ( p.closest( '.mw-editsection' ) ) return;
// #gr-toc-doc-nav buttons manage their own data-deva spans in makeBtn
}
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 );
} );
}
document.querySelectorAll( '.vector-toc .vector-toc-text' ).forEach( function ( span ) {
if ( span.hasAttribute( 'data-deva' ) ) return;
var orig = span.textContent;
if ( !orig.trim() ) return;
span.setAttribute( 'data-deva', orig );
translatableSpans.push( span );
} );
}
// ── Apply a script to all tagged spans ─────────────────────────
function applyScript( script ) {
currentScript = script;
translatableSpans.forEach( function ( span ) {
if ( !span.parentNode ) return;
var orig = span.getAttribute( 'data-deva' );
if ( !orig ) return;
span.textContent = ( script === 'deva' )
? orig
: transliterateText( orig, script );
} );
}
// ── Pages where sidebar TOC should not be modified ──────────────
function _isNoTocPage() {
var pn = ( window.mw && mw.config && mw.config.get( 'wgPageName' ) ) || '';
return pn === 'Main_Page' || /^[A-Za-z0-9_]+:About$/.test( pn );
}
// ── TOC: rename "Contents" → "विषयसूची" ────────────────────────
function renameTocTitle() {
if ( _isNoTocPage() ) return;
var toc = document.querySelector('.vector-toc');
if ( !toc ) return;
var titleEl =
toc.querySelector('.vector-toc-title') ||
toc.querySelector('.vector-pinnable-header-label');
if ( !titleEl ) return;
var LABEL = 'विषयसूची';
/* If already inserted, just refresh text */
var span = titleEl.querySelector('.gr-toc-title');
if ( !span ) {
titleEl.innerHTML = '';
span = document.createElement('span');
span.className = 'gr-toc-title';
span.setAttribute('data-deva', LABEL);
titleEl.appendChild(span);
translatableSpans.push(span); // uses your internal array
}
span.textContent =
currentScript === 'deva'
? LABEL
: transliterateText(LABEL, currentScript);
}
// ── TOC: Remove the "Beginning" / top-of-page link ─────────────
function removeTocBeginning() {
if ( _isNoTocPage() ) return;
var toc = document.querySelector( '.vector-toc' );
if ( !toc ) return;
// Try the dedicated id first (Vector 2022)
var el = toc.querySelector( '#vector-toc-beginning' );
if ( !el ) {
// Fallback: first list-item whose link has no # anchor = the "Beginning" entry
var items = toc.querySelectorAll( '.vector-toc-list-item' );
for ( var i = 0; i < items.length; i++ ) {
var a = items[ i ].querySelector( 'a' );
if ( a ) {
var href = a.getAttribute( 'href' ) || '';
if ( href.indexOf( '#' ) === -1 ) { el = items[ i ]; break; }
}
}
}
if ( el && el.parentNode ) el.parentNode.removeChild( el );
}
// ── TOC: Expand all collapsed subsections on load ───────────────
function expandTocSections() {
if ( _isNoTocPage() ) return;
var toc = document.querySelector( '.vector-toc' );
if ( !toc ) return;
toc.querySelectorAll( '.vector-toc-list-item-collapsed' ).forEach( function ( li ) {
li.classList.remove( 'vector-toc-list-item-collapsed' );
} );
}
// ── TOC: Inject मूल / उल्लेख nav links ─────────────────────────
function injectTocDocNav() {
if ( _isNoTocPage() ) return;
var toc = document.querySelector( '.vector-toc' );
if ( !toc ) return;
if ( document.getElementById( 'gr-toc-doc-nav' ) ) return;
var artPath = ( window.mw && mw.config && mw.config.get( 'wgArticlePath' ) ) || '/wiki/$1';
var pageTitle = ( window.mw && mw.config && mw.config.get( 'wgPageName' ) ) || '';
var teekaPage = document.querySelector( '.gr-teeka-page' );
var primarySlug = teekaPage ? ( teekaPage.getAttribute( 'data-primary' ) || '' ) : '';
var docSlug = teekaPage ? ( teekaPage.getAttribute( 'data-slug' ) || '' ) : '';
if ( !primarySlug ) {
primarySlug = pageTitle.split( '/' )[ 0 ];
docSlug = primarySlug;
}
if ( !primarySlug ) return;
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( primarySlug + '/Ullekha' );
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 moolaPageUrl = wikiUrl( primarySlug + '/Moola' );
var showMoolaPage = !teekaPage && hasMoolaPage;
var showMoolaBack = !!teekaPage;
var showUllekha = hasUllekhaPage || !!teekaPage;
if ( !showMoolaPage && !showMoolaBack && !showUllekha ) return;
var nav = document.createElement( 'div' );
nav.id = 'gr-toc-doc-nav';
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 ) {
var a = document.createElement( 'a' );
a.href = href;
a.setAttribute( 'class', 'toc-main-link-item');
var lspan = document.createElement( 'span' );
lspan.setAttribute( 'data-deva', label );
lspan.textContent = ( currentScript && currentScript !== 'deva' )
? transliterateText( label, currentScript )
: label;
translatableSpans.push( lspan );
a.appendChild( lspan );
a.addEventListener( 'mouseover', function () { this.style.opacity = '0.72'; } );
a.addEventListener( 'mouseout', function () { this.style.opacity = '1'; } );
return a;
}
if ( showMoolaPage ) nav.appendChild( makeBtn( moolaPageUrl, 'मूलम्' ) );
if ( showMoolaBack ) nav.appendChild( makeBtn( moolaUrl, 'मूल' ) );
if ( showUllekha ) nav.appendChild( makeBtn( ullekhaUrl, 'उल्लेख') );
var tocContents = toc.querySelector( '.vector-toc-contents' );
if ( tocContents ) toc.insertBefore( nav, tocContents );
else toc.appendChild( nav );
}
// ── TOC active-item highlight ────────────────────────────────────
function watchTocActive() {
if ( _isNoTocPage() ) return;
var toc = document.querySelector( '.vector-toc' );
if ( !toc ) return;
if ( !window.MutationObserver ) return;
if ( toc._grObserved ) {
toc.querySelectorAll( '.vector-toc-list-item' ).forEach( attachHighlight );
return;
}
toc._grObserved = true;
var ACTIVE_COLOR = '#f57c00';
function setActive( li, on ) {
var link = li.querySelector( '.vector-toc-link' ) || li.querySelector( 'a' );
if ( !link ) return;
var hasActiveChild = !!li.querySelector(
'.vector-toc-list-item .vector-toc-list-item-active'
);
var shouldHighlight = on && !hasActiveChild;
if ( shouldHighlight ) {
link.style.setProperty( 'color', ACTIVE_COLOR, 'important' );
link.style.setProperty( 'font-weight', '700', 'important' );
link.querySelectorAll( '*' ).forEach( function ( el ) {
el.style.setProperty( 'color', ACTIVE_COLOR, 'important' );
el.style.setProperty( 'font-weight', '700', 'important' );
} );
} else {
link.style.removeProperty( 'color' );
link.style.setProperty( 'font-weight', '400', 'important' );
link.querySelectorAll( '*' ).forEach( function ( el ) {
el.style.removeProperty( 'color' );
el.style.setProperty( 'font-weight', '400', 'important' );
} );
}
}
function normaliseAll() {
toc.querySelectorAll( '.vector-toc-list-item' ).forEach( function ( li ) {
setActive( li, li.classList.contains( 'vector-toc-list-item-active' ) );
} );
}
function attachHighlight( li ) {
if ( li._grHighlightAttached ) return;
li._grHighlightAttached = true;
liObs.observe( li, { attributes: true, attributeFilter: [ 'class' ] } );
setActive( li, li.classList.contains( 'vector-toc-list-item-active' ) );
}
var liObs = new MutationObserver( function ( mutations ) {
mutations.forEach( function ( m ) {
if ( m.attributeName !== 'class' ) return;
var li = m.target;
var active = li.classList.contains( 'vector-toc-list-item-active' );
setActive( li, active );
if ( active ) {
var anc = li.parentNode;
while ( anc && anc !== document.body ) {
if ( anc.classList &&
anc.classList.contains( 'vector-toc-list-item' ) &&
anc.classList.contains( 'vector-toc-list-item-collapsed' ) ) {
anc.classList.remove( 'vector-toc-list-item-collapsed' );
}
anc = anc.parentNode;
}
var container = document.querySelector( '.vector-sticky-pinned-container' );
if ( container ) {
var vis = container.offsetHeight > 0 &&
container.offsetParent !== null &&
window.getComputedStyle( container ).display !== 'none' &&
window.getComputedStyle( container ).visibility !== 'hidden';
if ( vis ) {
var lr = li.getBoundingClientRect();
var cr = container.getBoundingClientRect();
if ( lr.top < cr.top + 4 || lr.bottom > cr.bottom - 4 ) {
var sh = null, node = li.parentNode;
while ( node && node !== document.body ) {
var ov = window.getComputedStyle( node ).overflowY;
if ( ( ov === 'auto' || ov === 'scroll' ) &&
node.scrollHeight > node.clientHeight ) { sh = node; break; }
node = node.parentNode;
}
if ( !sh && container.scrollHeight > container.clientHeight ) sh = container;
if ( sh ) {
var hr = sh.getBoundingClientRect();
var top = lr.top - hr.top + sh.scrollTop;
sh.scrollTop = Math.max( 0, top - sh.clientHeight / 2 + li.offsetHeight / 2 );
}
}
}
}
}
} );
} );
var structObs = new MutationObserver( function ( mutations ) {
mutations.forEach( function ( m ) {
if ( m.type !== 'childList' ) return;
m.addedNodes.forEach( function ( n ) {
if ( n.nodeType !== 1 ) return;
if ( n.classList && n.classList.contains( 'vector-toc-list-item' ) ) attachHighlight( n );
if ( n.querySelectorAll ) n.querySelectorAll( '.vector-toc-list-item' ).forEach( attachHighlight );
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 );
translatableSpans.push( span );
} );
} );
} );
} );
toc.querySelectorAll( '.vector-toc-list-item' ).forEach( attachHighlight );
structObs.observe( toc, { childList: true, subtree: true } );
setTimeout( function () {
normaliseAll();
var active = toc.querySelector( '.vector-toc-list-item-active' );
if ( active ) setActive( active, true );
}, 300 );
}
// ── 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() {
if ( _isNoTocPage() ) return;
var toc = document.querySelector( '.vector-toc' );
if ( !toc ) return;
removeTocBeginning();
renameTocTitle();
expandTocSections();
injectTocDocNav();
attachHeadingObserver();
}
// ── 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;
function attachHeadingObserver() {
if ( _isNoTocPage() ) return;
if ( _headingObserver ) return; // only attach once
if ( !window.IntersectionObserver ) {
watchTocActive(); // fallback for old browsers
return;
}
var ACTIVE_COLOR = '#f57c00';
var _activeId = null;
// Collect all headings that have an id (Vector gives them ids)
var content = document.querySelector( '.mw-parser-output' );
if ( !content ) return;
var headings = Array.from(
content.querySelectorAll( 'h1[id],h2[id],h3[id],h4[id],h5[id],h6[id]' )
);
if ( !headings.length ) return;
var toc = document.querySelector( '.vector-toc' );
if ( !toc ) return;
function getTocLink( id ) {
// Vector renders TOC links as href="#id"
return toc.querySelector( 'a[href="#' + CSS.escape( id ) + '"]' );
}
function getTocLi( id ) {
var a = getTocLink( id );
return a ? a.closest( '.vector-toc-list-item' ) : null;
}
function clearActive() {
toc.querySelectorAll( '.vector-toc-list-item-active' ).forEach( function ( li ) {
li.classList.remove( 'vector-toc-list-item-active' );
var lnk = li.querySelector( '.vector-toc-link' );
if ( !lnk ) return;
lnk.style.removeProperty( 'color' );
lnk.style.setProperty( 'font-weight', '400', 'important' );
lnk.querySelectorAll( '*' ).forEach( function ( el ) {
el.style.removeProperty( 'color' );
el.style.setProperty( 'font-weight', '400', 'important' );
} );
} );
}
function setActive( id ) {
if ( _activeId === id ) return;
_activeId = id;
clearActive();
if ( !id ) return;
var li = getTocLi( id );
if ( !li ) return;
li.classList.add( 'vector-toc-list-item-active' );
// Highlight only the innermost active item
var hasActiveChild = !!li.querySelector(
'.vector-toc-list-item .vector-toc-list-item-active'
);
if ( !hasActiveChild ) {
var lnk = li.querySelector( '.vector-toc-link' );
if ( lnk ) {
lnk.style.setProperty( 'color', ACTIVE_COLOR, 'important' );
lnk.style.setProperty( 'font-weight', '700', 'important' );
lnk.querySelectorAll( '*' ).forEach( function ( el ) {
el.style.setProperty( 'color', ACTIVE_COLOR, 'important' );
el.style.setProperty( 'font-weight', '700', 'important' );
} );
}
}
// Expand collapsed ancestors
var anc = li.parentNode;
while ( anc && anc !== toc ) {
if ( anc.classList && anc.classList.contains( 'vector-toc-list-item-collapsed' ) ) {
anc.classList.remove( 'vector-toc-list-item-collapsed' );
}
anc = anc.parentNode;
}
// Scroll TOC item into view within sidebar
var sticky = document.querySelector( '.vector-sticky-pinned-container' );
var scrollEl = sticky || toc;
if ( scrollEl.scrollHeight > scrollEl.clientHeight ) {
var lr = li.getBoundingClientRect();
var cr = scrollEl.getBoundingClientRect();
if ( lr.top < cr.top + 8 || lr.bottom > cr.bottom - 8 ) {
scrollEl.scrollTop += lr.top - cr.top - scrollEl.clientHeight / 2 + li.offsetHeight / 2;
}
}
}
// Track which headings are visible
var _visible = new Set();
_headingObserver = new IntersectionObserver( function ( entries ) {
entries.forEach( function ( entry ) {
if ( entry.isIntersecting ) {
_visible.add( entry.target.id );
} else {
_visible.delete( entry.target.id );
}
} );
// Pick the topmost visible heading
var topId = null;
var topY = Infinity;
_visible.forEach( function ( id ) {
var el = document.getElementById( id );
if ( el ) {
var y = el.getBoundingClientRect().top;
if ( y >= 0 && y < topY ) { topY = y; topId = id; }
}
} );
// Nothing visible: find the last heading scrolled past (above viewport)
if ( !topId ) {
var bestY = -Infinity;
headings.forEach( function ( h ) {
var y = h.getBoundingClientRect().top;
if ( y < 0 && y > bestY ) { bestY = y; topId = h.id; }
} );
}
setActive( topId || null );
}, {
// Fire when heading enters/leaves the top 30% of the viewport
rootMargin: '-60px 0px -65% 0px',
threshold: 0
} );
headings.forEach( function ( h ) { _headingObserver.observe( h ); } );
}
// ── Init ────────────────────────────────────────────────────────
function init() {
var HIDE_IDS = [
'vector-appearance',
'vector-appearance-pinned-container',
'vector-appearance-unpinned-container'
];
function removeHiddenEls() {
HIDE_IDS.forEach( function ( id ) {
var el = document.getElementById( id );
if ( el && el.parentNode ) el.parentNode.removeChild( el );
} );
var pt = document.getElementById( 'vector-page-tools' ) ||
document.querySelector( '.vector-page-tools-pinned-container' );
if ( pt ) {
pt.querySelectorAll( '[aria-controls="vector-appearance"]' )
.forEach( function ( el ) { if ( el.parentNode ) el.parentNode.removeChild( el ); } );
}
}
removeHiddenEls();
( function detectTeekaMode() {
var tp = document.querySelector( '.gr-teeka-page' );
if ( !tp ) return;
var primary = tp.getAttribute( 'data-primary' ) || '';
var artPath = ( window.mw && mw.config.get( 'wgArticlePath' ) ) || '/wiki/$1';
var mainUrl = artPath.replace( '$1', primary );
var refParam = window.location.search.match( /[?&]ref=([01])/ );
if ( refParam ) {
document.body.classList.add( refParam[1] === '1' ? 'gr-ref-mode' : 'gr-standalone' );
return;
}
var ref = document.referrer || '';
document.body.classList.add(
( ref && primary && ref.indexOf( mainUrl ) !== -1 ) ? 'gr-ref-mode' : 'gr-standalone'
);
}() );
if ( window.MutationObserver ) {
var hideObs = new MutationObserver( function ( mutations ) {
var dirty = false;
mutations.forEach( function ( m ) { if ( m.addedNodes.length ) dirty = true; } );
if ( dirty ) removeHiddenEls();
} );
hideObs.observe( document.body, { childList: true, subtree: false } );
setTimeout( function () { hideObs.disconnect(); }, 6000 );
}
var content = document.querySelector( '.mw-parser-output' );
var alreadyTagged = content && content.querySelector( '[data-deva]' );
if ( !alreadyTagged ) {
translatableSpans = [];
tagTextNodes();
} else {
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 );
} );
}
var saved = ( function () {
try { return localStorage.getItem( LS_SCRIPT_KEY ); } catch ( e ) { return null; }
}() );
if ( saved && saved !== 'deva' ) { applyScript( saved ); }
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 );
}
// ── React to toolbar script-change events ──────────────────────
window.addEventListener( 'gr-script-change', function ( e ) {
var script = e && e.detail && e.detail.script;
if ( script ) applyScript( script );
} );
// ── 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 ) {
var container = e && e.detail && e.detail.container;
if ( !container ) return;
var walker = document.createTreeWalker( container, NodeFilter.SHOW_TEXT );
var nodes = [];
while ( walker.nextNode() ) nodes.push( walker.currentNode );
nodes.forEach( function ( node ) {
var p = node.parentNode;
if ( !p || ( p.hasAttribute && p.hasAttribute( 'data-deva' ) ) ) return;
var orig = node.textContent;
if ( !orig.trim() ) return;
var span = document.createElement( 'span' );
span.setAttribute( 'data-deva', orig );
span.textContent = currentScript !== 'deva' ? transliterateText( orig, currentScript ) : orig;
p.replaceChild( span, node );
translatableSpans.push( span );
} );
} );
// ── React to script changes from OTHER tabs (BroadcastChannel) ──
try {
var _grBC = new BroadcastChannel( 'gr-script' );
_grBC.onmessage = function ( e ) {
var script = e && e.data && e.data.script;
if ( script ) {
currentScript = script;
var sel = document.querySelector( '.gr-script-sel' );
if ( sel ) sel.value = script;
applyScript( script );
}
};
} catch ( e ) {}
// ── MediaWiki SPA-style navigation ──────────────────────────────
if ( window.mw ) {
mw.hook( 'wikipage.content' ).add( function () {
setTimeout( function () {
var content = document.querySelector( '.mw-parser-output' );
var alreadyTagged = content && content.querySelector( '[data-deva]' );
if ( !alreadyTagged ) {
translatableSpans = [];
tagTextNodes();
} else {
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 );
} );
}
if ( currentScript !== 'deva' ) applyScript( currentScript );
setupToc();
}, 150 );
} );
}
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', init );
} else {
init();
}
}() ); /* ← end of main IIFE */
// ── Inject "Help" and "About" links into the header ─────────────────
( function () {
function wikiHref( title ) {
if ( window.mw && mw.util && mw.util.getUrl ) return mw.util.getUrl( title );
var ap = ( window.mw && mw.config && mw.config.get( 'wgArticlePath' ) ) || '/wiki/$1';
return ap.replace( '$1', title );
}
var linkStyle = [
'color:rgba(255,255,255,0.88)', 'font-size:0.88em',
'font-family:system-ui,sans-serif', 'font-weight:500',
'text-decoration:none', 'padding:4px 10px', 'border-radius:4px',
'margin-right:4px', 'transition:color 0.15s,background 0.15s',
'white-space:nowrap',
].join( ';' );
function makeHeaderLink( id, href, label ) {
var a = document.createElement( 'a' );
a.id = id; a.href = href; a.textContent = label;
a.style.cssText = linkStyle;
a.addEventListener( 'mouseover', function () {
this.style.color = '#fff'; this.style.background = 'rgba(255,255,255,0.12)';
} );
a.addEventListener( 'mouseout', function () {
this.style.color = 'rgba(255,255,255,0.88)'; this.style.background = 'transparent';
} );
return a;
}
function injectHeaderLinks() {
if ( document.getElementById( 'gr-about-link' ) ) return;
var headerEnd = document.querySelector( '.vector-header-end' ) ||
document.querySelector( '#vector-user-links' ) ||
document.querySelector( '.mw-header' );
if ( !headerEnd ) return;
var helpLink = makeHeaderLink( 'gr-help-link', wikiHref( 'My_wiki:Help' ), 'Help' );
var aboutLink = makeHeaderLink( 'gr-about-link', wikiHref( 'My_wiki:About' ), 'About' );
var ul = document.querySelector( '.vector-user-links' ) ||
document.querySelector( '#pt-userpage' );
if ( ul && ul.parentNode === headerEnd ) {
headerEnd.insertBefore( aboutLink, ul );
headerEnd.insertBefore( helpLink, aboutLink );
} else {
headerEnd.appendChild( helpLink );
headerEnd.appendChild( aboutLink );
}
}
if ( document.readyState === 'loading' ) document.addEventListener( 'DOMContentLoaded', injectHeaderLinks );
else injectHeaderLinks();
}() );
// ── Main page: by-Grantha / by-Author toggle ──────────────────────
( function () {
function grHomeView( v ) {
var gView = document.getElementById( 'gr-view-grantha' );
var aView = document.getElementById( 'gr-view-author' );
var gBtn = document.getElementById( 'gr-toggle-grantha' );
var aBtn = document.getElementById( 'gr-toggle-author' );
if ( !gView || !aView || !gBtn || !aBtn ) return;
gView.style.display = ( v === 'grantha' ) ? '' : 'none';
aView.style.display = ( v === 'author' ) ? '' : 'none';
gBtn.className = 'gr-toggle-btn' + ( v === 'grantha' ? ' gr-toggle-active' : '' );
aBtn.className = 'gr-toggle-btn' + ( v === 'author' ? ' gr-toggle-active' : '' );
try { localStorage.setItem( 'gr_home_view', v ); } catch ( e ) {}
}
function initHomeToggle() {
var gBtn = document.getElementById( 'gr-toggle-grantha' );
var aBtn = document.getElementById( 'gr-toggle-author' );
if ( !gBtn || !aBtn ) return;
gBtn.addEventListener( 'click', function () { grHomeView( 'grantha' ); } );
aBtn.addEventListener( 'click', function () { grHomeView( 'author' ); } );
[ gBtn, aBtn ].forEach( function ( btn ) {
btn.addEventListener( 'keydown', function ( e ) {
if ( e.key === 'Enter' || e.key === ' ' ) btn.click();
} );
} );
var saved;
try { saved = localStorage.getItem( 'gr_home_view' ); } catch ( e ) {}
if ( saved === 'author' ) grHomeView( 'author' );
}
if ( document.readyState === 'loading' ) document.addEventListener( 'DOMContentLoaded', initHomeToggle );
else initHomeToggle();
}() );
// ── Ullekha reference link handler ─────────────────────────────────
( function () {
function highlightOnArrival() {
var search = window.location.search;
if ( !search ) return;
var m = search.match( /[?&]hlUllekha=([^&]+)/ );
if ( !m ) return;
var needle;
try { needle = decodeURIComponent( m[ 1 ] ); } catch ( e ) { 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() {
var content = document.querySelector( '.mw-parser-output' );
if ( !content ) return;
var snippet = needle.slice( 0, 40 );
var found = false;
// Search data-deva spans first (post-tagTextNodes state)
var spans = content.querySelectorAll( '[data-deva]' );
for ( var i = 0; i < spans.length && !found; i++ ) {
var spanEl = spans[ i ];
var orig = spanEl.getAttribute( 'data-deva' ) || '';
if ( orig.indexOf( snippet ) === -1 ) continue;
// Replace the span with: text-before + <mark> + text-after
var idx = orig.indexOf( snippet );
var hlText = orig.slice( idx, Math.min( idx + needle.length, orig.length ) );
var mark = document.createElement( 'mark' );
mark.className = 'gr-ullekha-highlight';
mark.style.cssText = 'background:#fff176;border-radius:2px;padding:0 2px;';
mark.textContent = hlText;
var parent = spanEl.parentNode;
if ( !parent ) continue;
var before = document.createTextNode( orig.slice( 0, idx ) );
var after = document.createTextNode( orig.slice( idx + hlText.length ) );
parent.insertBefore( before, spanEl );
parent.insertBefore( mark, spanEl );
parent.insertBefore( after, spanEl );
parent.removeChild( spanEl );
setTimeout( function () {
mark.scrollIntoView( { behavior: 'smooth', block: 'center' } );
}, 100 );
found = true;
}
// Fallback: raw text nodes (before tagTextNodes runs)
if ( !found ) {
var walker = document.createTreeWalker( content, NodeFilter.SHOW_TEXT );
while ( walker.nextNode() && !found ) {
var node = walker.currentNode;
var txt = node.textContent || '';
if ( txt.indexOf( snippet ) === -1 ) continue;
var idx2 = txt.indexOf( snippet );
var mark2 = document.createElement( 'mark' );
mark2.className = 'gr-ullekha-highlight';
mark2.style.cssText = 'background:#fff176;border-radius:2px;padding:0 2px;';
mark2.textContent = txt.slice( idx2, Math.min( idx2 + needle.length, txt.length ) );
var p = node.parentNode;
p.insertBefore( document.createTextNode( txt.slice( 0, idx2 ) ), node );
p.insertBefore( mark2, node );
p.insertBefore( document.createTextNode( txt.slice( idx2 + mark2.textContent.length ) ), node );
p.removeChild( node );
setTimeout( function () {
mark2.scrollIntoView( { behavior: 'smooth', block: 'center' } );
}, 100 );
found = true;
}
}
}
// Delay to let tagTextNodes() and MW rendering finish
setTimeout( doHighlight, 600 );
}
function wireUllekhaLinks() {
document.querySelectorAll( '.gr-ullekha-ref-link' ).forEach( function ( wrap ) {
var anchor = wrap.getAttribute( 'data-anchor' ) || '';
var hl = wrap.getAttribute( 'data-hl' ) || '';
var a = wrap.querySelector( 'a' );
if ( !a ) return;
var base = a.href.split( '#' )[ 0 ];
var encoded = encodeURIComponent( hl );
a.href = base + ( hl ? '?hlUllekha=' + encoded : '' )
+ ( anchor ? '#' + anchor : '' );
} );
}
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', function () {
highlightOnArrival();
wireUllekhaLinks();
} );
} else {
highlightOnArrival();
wireUllekhaLinks();
}
}() );/* ── Search result highlight — highlight query term on arrival ──
* 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 () {
/* ── Step 1: When leaving via a search result link,
store the query in sessionStorage ── */
function storeQueryForLink( url, query ) {
try {
/* Store just the pathname so protocol/host differences don't break matching */
var a = document.createElement( 'a' );
a.href = url;
sessionStorage.setItem( 'gr_search_hl', JSON.stringify({
query: query,
pathname: a.pathname // e.g. "/Brahmasutra"
}) );
} catch(e) {}
}
/* ── Step 2: On page load, check if we arrived from a search result ── */
function applyHighlight() {
var stored;
try {
stored = JSON.parse( sessionStorage.getItem( 'gr_search_hl' ) || 'null' );
} catch(e) { return; }
if ( !stored || !stored.query ) return;
/* Check pathname matches — lenient comparison */
var currentPath = window.location.pathname;
var storedPath = stored.pathname || '';
/* Normalize: remove trailing slash, decode */
function normPath(p) { return decodeURIComponent(p).replace(/\/+$/, ''); }
if ( storedPath && normPath(storedPath) !== normPath(currentPath) ) {
/* Different page — clear and bail */
try { sessionStorage.removeItem( 'gr_search_hl' ); } catch(e) {}
return;
}
var query = stored.query.trim();
if ( !query ) return;
/* Clear so refreshing doesn't re-highlight */
try { sessionStorage.removeItem( 'gr_search_hl' ); } catch(e) {}
/* Wait for content + Common.js tagTextNodes to finish rendering */
var delays = [400, 900, 1500];
delays.forEach( function(ms) {
setTimeout( function () {
/* Only run if not already highlighted */
if ( !document.querySelector( '.gr-search-hl' ) ) {
highlightText( query );
}
}, ms );
} );
}
/* ── Core: walk text nodes and wrap matches ── */
function highlightText( query ) {
var content = document.querySelector( '#mw-content-text .mw-parser-output' );
if ( !content ) return;
/* Normalise query — strip quotes, split on spaces */
var raw = query.replace( /^"|"$/g, '' ).trim();
if ( !raw ) return;
/* Build regex — escape special chars, match whole query first,
fall back to individual words */
var patterns = [];
/* Full phrase */
patterns.push( escapeRegex( raw ) );
/* Individual words (min 2 chars) */
raw.split( /\s+/ ).forEach( function(w) {
if ( w.length >= 2 ) patterns.push( escapeRegex( w ) );
} );
var matched = false;
for ( var pi = 0; pi < patterns.length; pi++ ) {
var re;
try { re = new RegExp( '(' + patterns[pi] + ')', 'gi' ); }
catch(e) { continue; }
var count = wrapMatches( content, re );
if ( count > 0 ) { matched = true; break; }
}
if ( !matched ) return;
/* Scroll to first highlight */
var first = document.querySelector( '.gr-search-hl' );
if ( first ) {
first.scrollIntoView({ behavior: 'smooth', block: 'center' });
/* Pulse animation */
first.classList.add( 'gr-search-hl-pulse' );
setTimeout( function() {
first.classList.remove( 'gr-search-hl-pulse' );
}, 2000 );
}
/* Show dismiss button */
showDismissBar( query );
}
function escapeRegex( s ) {
return s.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
}
function wrapMatches( root, re ) {
var count = 0;
var walker = document.createTreeWalker(
root, NodeFilter.SHOW_TEXT, {
acceptNode: function( node ) {
/* Skip inside script, style, our own highlights */
var p = node.parentElement;
if ( !p ) return NodeFilter.FILTER_REJECT;
var tag = p.tagName.toUpperCase();
if ( tag === 'SCRIPT' || tag === 'STYLE' || tag === 'NOSCRIPT' ) return NodeFilter.FILTER_REJECT;
if ( p.classList.contains( 'gr-search-hl' ) ) return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
}
}, false
);
var nodes = [];
var node;
while ( ( node = walker.nextNode() ) ) nodes.push( node );
nodes.forEach( function( textNode ) {
var val = textNode.nodeValue;
if ( !re.test( val ) ) return;
re.lastIndex = 0;
var frag = document.createDocumentFragment();
var last = 0;
var m;
while ( ( m = re.exec( val ) ) !== null ) {
if ( m.index > last ) {
frag.appendChild( document.createTextNode( val.slice( last, m.index ) ) );
}
var span = document.createElement( 'span' );
span.className = 'gr-search-hl';
span.textContent = m[0];
frag.appendChild( span );
last = m.index + m[0].length;
count++;
}
if ( last < val.length ) {
frag.appendChild( document.createTextNode( val.slice( last ) ) );
}
textNode.parentNode.replaceChild( frag, textNode );
} );
return count;
}
/* ── Dismiss bar ── */
function showDismissBar( query ) {
var isMob = window.innerWidth < 768;
var bar = document.createElement( 'div' );
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 ) {
bar.style.cssText = [
'position:fixed',
'top:calc(var(--gr-header-height,56px) + var(--gr-toc-top,52px) + 8px)',
'left:50%', 'transform:translateX(-50%)',
'z-index:10200',
'background:#b5451b', 'color:#fff',
'padding:8px 14px',
'border-radius:24px',
'display:flex', 'align-items:center', 'gap:8px',
'font-family:system-ui,sans-serif', 'font-size:13px',
'box-shadow:0 3px 12px rgba(0,0,0,0.25)',
'white-space:nowrap',
'max-width:calc(100vw - 32px)'
].join(';');
} else {
bar.style.cssText = [
'position:fixed', 'bottom:0', 'left:0', 'right:0', 'z-index:10200',
'background:#b5451b', 'color:#fff', 'padding:10px 16px',
'display:flex', 'align-items:center', 'justify-content:space-between',
'font-family:system-ui,sans-serif', 'font-size:14px',
'box-shadow:0 -2px 8px rgba(0,0,0,0.2)'
].join(';');
}
var count = document.querySelectorAll( '.gr-search-hl' ).length;
if ( isMob ) {
/* Mobile: compact pill — just count + prev/next + dismiss */
bar.innerHTML =
'<span style="flex-shrink:0">🔍 ' + count + '</span>' +
'<button id="gr-hl-prev" style="background:rgba(255,255,255,0.2);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-next" style="background:rgba(255,255,255,0.2);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 {
/* Desktop: full bar */
var nav = document.createElement( 'div' );
nav.style.cssText = 'display:flex;align-items:center;gap:12px;';
nav.innerHTML =
'<span>🔍 <strong>' + escHtml(query) + '</strong> — ' + count + ' match' + (count===1?'':'es') + '</span>' +
'<button id="gr-hl-prev" style="background:rgba(255,255,255,0.2);border:none;color:#fff;padding:4px 10px;border-radius:4px;cursor:pointer;font-size:13px;min-height:32px;">↑ Prev</button>' +
'<button id="gr-hl-next" style="background:rgba(255,255,255,0.2);border:none;color:#fff;padding:4px 10px;border-radius:4px;cursor:pointer;font-size:13px;min-height:32px;">↓ Next</button>';
bar.appendChild( nav );
}
var dismiss = isMob
? bar.querySelector( '#gr-hl-dismiss' )
: ( function() {
var b = document.createElement( 'button' );
b.textContent = '✕ Clear';
b.id = 'gr-hl-dismiss';
b.style.cssText = 'background:rgba(255,255,255,0.15);border:none;color:#fff;padding:4px 12px;border-radius:4px;cursor:pointer;font-size:13px;min-height:32px;';
bar.appendChild( b );
return b;
}() );
dismiss.onclick = function () {
clearHighlights();
bar.remove();
};
document.body.appendChild( bar );
/* Prev / Next navigation */
var hlEls = Array.from( document.querySelectorAll( '.gr-search-hl' ) );
var currentIdx = 0;
function goTo( idx ) {
hlEls.forEach( function(el) { el.classList.remove( 'gr-search-hl-current' ); } );
currentIdx = ( idx + hlEls.length ) % hlEls.length;
var el = hlEls[ currentIdx ];
el.classList.add( 'gr-search-hl-current' );
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
var nextBtn = document.getElementById( 'gr-hl-next' );
var prevBtn = document.getElementById( 'gr-hl-prev' );
if ( nextBtn ) nextBtn.onclick = function() { goTo( currentIdx + 1 ); };
if ( prevBtn ) prevBtn.onclick = function() { goTo( currentIdx - 1 ); };
}
function clearHighlights() {
document.querySelectorAll( '.gr-search-hl' ).forEach( function( span ) {
var parent = span.parentNode;
while ( span.firstChild ) parent.insertBefore( span.firstChild, span );
parent.removeChild( span );
} );
}
function escHtml( s ) {
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
}
/* ── Inject CSS for highlights ── */
function injectHighlightCSS() {
if ( document.getElementById( 'gr-hl-css' ) ) return;
var s = document.createElement( 'style' );
s.id = 'gr-hl-css';
s.textContent = [
'.gr-search-hl{',
' background:#fff176;color:#1a1a1a;',
' border-radius:2px;padding:0 1px;',
' box-shadow:0 0 0 1px rgba(181,69,27,0.25);',
'}',
'.gr-search-hl-current{',
' background:#ffb300!important;',
' box-shadow:0 0 0 2px #b5451b!important;',
'}',
'@keyframes gr-hl-pulse{',
' 0%{background:#ffb300;}',
' 50%{background:#fff176;}',
' 100%{background:#fff176;}',
'}',
'.gr-search-hl-pulse{animation:gr-hl-pulse 1.2s ease 2;}',
].join('');
document.head.appendChild( s );
}
/* ── Boot ── */
injectHighlightCSS();
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', applyHighlight );
} else {
applyHighlight();
}
/* ── Expose storeQueryForLink for readerToolbar to call ── */
window.grStoreSearchHL = storeQueryForLink;
}() );
/* ═══════════════════════════════════════════════════════════════
Mobile addon — paste at bottom of MediaWiki:Common.js
Only runs on Minerva (mobile) skin
═══════════════════════════════════════════════════════════════ */
( function () {
if ( !document.body.classList.contains( 'skin-minerva' ) ) return;
/* ── 1. CSS injected into <head> — beats all other stylesheets ── */
function injectCSS() {
if ( document.getElementById( 'gr-mob-css' ) ) return;
var s = document.createElement( 'style' );
s.id = 'gr-mob-css';
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;}' +
'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;}' +
'.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;}' +
/* Logo row: icon + title wrap cleanly */
'.branding-box a{display:flex!important;align-items:center!important;' +
'text-decoration:none!important;max-width:calc(100vw - 80px)!important;}' +
'.branding-box a::before{content:"";display:block;width:30px;height:30px;flex-shrink:0;' +
'background:url("/favicon.png") center/contain no-repeat;' +
'margin-right:8px;}' +
'.branding-box a span{color:#fff!important;font-size:16px!important;font-weight:700!important;' +
'font-family:system-ui,sans-serif!important;line-height:1.2!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 label{color:#fff!important;}' +
/* Hide Page/Discussion tabs + icon toolbar */
'.minerva-tabs,.mw-portlet-associated-pages,.page-actions-menu,' +
'#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;}' +
/* Drawer footer: hide About/Disclaimers */
'.mw-footer.minerva-footer,.footer-places,.footer-info,' +
'.minerva-footer-logo,#footer-places-about,' +
'#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;}' +
/* Sections expanded */
'.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-10{display:block!important;visibility:visible!important;}' +
'.collapsible-block{display:block!important;}' +
'.section-heading .indicator,.collapsible-heading .indicator{display: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;' +
'gap:12px!important;width:100%!important;}' +
'.gr-home-card{width:100%!important;max-width:100%!important;' +
'min-width:unset!important;box-sizing:border-box!important;flex:none!important;}' +
'.gr-home-toggle{flex-wrap:wrap!important;}' +
/* Content */
'.mw-parser-output{font-size:18px!important;line-height:1.8!important;}' +
'.mw-parser-output h2,.mw-parser-output h3{width:100%!important;}' +
'.bhashyam-block{margin-left:8px!important;}' +
'#footer,.mw-footer,.catlinks,#catlinks{display:none!important;}' +
/* TOC overlay */
'.gr-mob-toc-panel{position:fixed!important;top:0!important;left:0!important;' +
'bottom:0!important;width:82vw!important;max-width:340px!important;' +
'background:#fff!important;z-index:10400!important;' +
'box-shadow:4px 0 28px rgba(0,0,0,0.22)!important;overflow-y:auto!important;' +
'padding:0 0 40px!important;transform:translateX(-110%)!important;' +
'transition:transform 0.26s cubic-bezier(0.4,0,0.2,1)!important;display:block!important;}' +
'.gr-mob-toc-panel.open{transform:translateX(0)!important;}' +
'.gr-mob-toc-backdrop{display:none!important;position:fixed!important;inset:0!important;' +
'background:rgba(0,0,0,0.4)!important;z-index:10399!important;}' +
'.gr-mob-toc-backdrop.open{display:block!important;}' +
'.gr-mob-toc-header{position:sticky!important;top:0!important;background:#fff!important;' +
'display:flex!important;align-items:center!important;justify-content:space-between!important;' +
'padding:16px 16px 12px!important;border-bottom:1px solid #f0ebe6!important;z-index:1!important;}' +
'.gr-mob-toc-title{font-size:13px!important;font-weight:700!important;text-transform:uppercase!important;' +
'letter-spacing:0.08em!important;color:#b5451b!important;font-family:system-ui,sans-serif!important;}' +
'.gr-mob-toc-close{background:none!important;border:none!important;' +
'font-size:22px!important;color:#999!important;cursor:pointer!important;padding:4px 8px!important;}' +
'.gr-mob-toc-body{padding:12px 16px!important;}' +
'.gr-mob-toc-body a{display:block!important;font-size:16px!important;line-height:1.6!important;' +
'color:#2c1810!important;text-decoration:none!important;padding:8px 0!important;' +
'border-bottom:1px solid #f5f0ed!important;}';
document.head.appendChild( s );
}
/* ── 2. Expand hidden sections ── */
function expandSections() {
document.querySelectorAll( '[class*="mf-section-"], .collapsible-block' )
.forEach( function ( el ) {
el.removeAttribute( 'hidden' );
el.style.setProperty( 'display', 'block', 'important' );
el.style.setProperty( 'visibility', 'visible', 'important' );
el.removeAttribute( 'aria-hidden' );
} );
document.querySelectorAll( '.section-heading, .collapsible-heading' )
.forEach( function ( el ) {
el.setAttribute( 'aria-expanded', 'true' );
el.style.setProperty( 'pointer-events', 'none', 'important' );
} );
}
/* Persistent observer — fight MF re-collapsing */
function watchSections() {
var t = null;
var obs = new MutationObserver( function ( ms ) {
if ( ms.some( function(m){ return m.attributeName === 'hidden'; } ) ) {
clearTimeout(t); t = setTimeout( expandSections, 30 );
}
} );
var root = document.querySelector( '#mw-content-text' ) || document.body;
obs.observe( root, { subtree:true, attributes:true, attributeFilter:['hidden','aria-hidden'] } );
}
/* ── 3. Body padding observer ── */
function watchBodyPadding() {
new MutationObserver( function () {
if ( document.body.style.paddingTop && document.body.style.paddingTop !== '0px' ) {
document.body.style.paddingTop = '';
}
} ).observe( document.body, { attributes:true, attributeFilter:['style'] } );
}
/* ── 4. Inject custom links into drawer ── */
function injectMenuLinks() {
/* Already injected */
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' );
if ( !navDrawer ) return;
var wrap = document.createElement( 'div' );
wrap.id = 'gr-mob-menu-items';
wrap.style.cssText = 'width:100%;background:#fff;margin-top:8px;';
var itemStyle = 'display:flex;align-items:center;gap:14px;padding:15px 20px;' +
'font-size:16px;color:#2c1810;text-decoration:none;' +
'font-family:system-ui,sans-serif;border-bottom:1px solid #f0ebe6;background:#fff;';
function makeItem( href, emoji, label ) {
var a = document.createElement( 'a' );
a.href = href;
a.style.cssText = itemStyle;
a.innerHTML =
'<span>' + label + '</span>';
return a;
}
wrap.appendChild( makeItem( '/Main_Page', '', 'Home' ) );
wrap.appendChild( makeItem( '/My_wiki:Help', '', 'Help' ) );
wrap.appendChild( makeItem( '/My_wiki:About', '', 'About' ) );
var userName = window.mw ? mw.config.get( 'wgUserName' ) : null;
if ( userName ) {
var la = document.querySelector( 'a[href*="action=logout"]' );
wrap.appendChild( makeItem(
la ? la.href : '/index.php?title=Special:UserLogout', '', 'Log out'
) );
} else {
wrap.appendChild( makeItem( '/index.php?title=Special:UserLogin', '', 'Log in' ) );
}
/* 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' );
if ( pageLeft ) {
/* Clear default items and insert ours */
while ( pageLeft.firstChild ) pageLeft.removeChild( pageLeft.firstChild );
pageLeft.style.removeProperty( 'display' ); /* un-hide it */
pageLeft.appendChild( wrap );
} else {
/* Fallback */
navDrawer.appendChild( wrap );
}
/* Hide footer portlets */
document.querySelectorAll(
'.mw-footer.minerva-footer,.footer-places,.footer-info,' +
'.minerva-footer-logo,#footer-places-about,' +
'#footer-places-disclaimers,#footer-places-privacy'
).forEach( function(el) { el.style.setProperty('display','none','important'); } );
}
/* ── 5. Mobile TOC overlay ── */
var _tocDone = false;
function initToc() {
if ( _tocDone ) return;
var tocList = document.querySelector( '.vector-toc-contents, .vector-toc .vector-toc-list' );
if ( !tocList ) return;
if ( !tocList.querySelector( 'li' ) ) return;
_tocDone = true;
var bd = document.createElement( 'div' );
bd.className = 'gr-mob-toc-backdrop';
document.body.appendChild( bd );
var panel = document.createElement( 'div' );
panel.className = 'gr-mob-toc-panel';
var hdr = document.createElement( 'div' );
hdr.className = 'gr-mob-toc-header';
var ttl = document.createElement( 'div' );
ttl.className = 'gr-mob-toc-title';
ttl.textContent = 'विषयसूची';
var cls = document.createElement( 'button' );
cls.className = 'gr-mob-toc-close';
cls.textContent = '✕';
hdr.appendChild( ttl ); hdr.appendChild( cls );
panel.appendChild( hdr );
var body = document.createElement( 'div' );
body.className = 'gr-mob-toc-body';
body.appendChild( tocList.cloneNode( true ) );
panel.appendChild( body );
document.body.appendChild( panel );
var btn = document.createElement( 'button' );
btn.id = 'gr-mob-toc-btn';
btn.innerHTML = '☰ Contents';
btn.style.cssText = 'position:fixed;bottom:148px;left:16px;z-index:9100;' +
'background:#fff;border:1.5px solid #b5451b;border-radius:24px;' +
'padding:10px 16px;font-size:15px;font-family:system-ui,sans-serif;' +
'color:#b5451b;font-weight:600;box-shadow:0 3px 14px rgba(0,0,0,0.15);cursor:pointer;';
document.body.appendChild( btn );
function open() { panel.classList.add('open'); bd.classList.add('open'); document.body.style.overflow='hidden'; }
function close() { panel.classList.remove('open'); bd.classList.remove('open'); document.body.style.overflow=''; }
btn.onclick = open; cls.onclick = close; bd.onclick = close;
body.querySelectorAll('a').forEach(function(a){ a.onclick = close; });
}
/* ── Boot ── */
injectCSS();
watchBodyPadding();
function boot() {
expandSections();
watchSections();
injectMenuLinks();
[100, 400, 900, 1800].forEach(function(ms){ setTimeout(expandSections, ms); });
setTimeout(initToc, 700);
}
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', boot );
} else {
boot();
}
if ( window.mw ) {
mw.hook( 'wikipage.content' ).add(function() {
setTimeout(function(){ expandSections(); injectMenuLinks(); initToc(); }, 300);
});
}
}() );
/**
* 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
*/
( function () {
'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 () {
/* Wait one tick so Minerva's own DOMReady handlers run first */
setTimeout( function () {
var blocks = document.querySelectorAll( '.collapsible-block, .toggle-list' );
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 ) {
try { el.remove(); } catch(e) {}
}
} );
}, 0 );
} );
/* ══════════════════════════════════════════════════════════════
* 2. MAIN PAGE: keep #gr-home-toggle below the reader bar
* ══════════════════════════════════════════════════════════════ */
if ( mw.config.get( 'wgPageName' ) !== 'Main_Page' ) return;
mw.loader.using( 'mediawiki.util' ).done( function () {
$( function () {
applyHomeToggleOffset();
/* Re-apply when bar height might change (orientation change, etc.) */
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, 800 );
} );
} );
function applyHomeToggleOffset() {
var bar = document.getElementById( 'gr-static-bar' );
if ( !bar ) return;
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 );
/* #gr-home is the outermost container */
var homeEl = document.getElementById( 'gr-home' );
/* #gr-home-toggle is the tab switcher row */
var toggleEl = document.getElementById( 'gr-home-toggle' );
/* Also target the mw-parser-output first child as a fallback */
var firstChild = document.querySelector(
'#mw-content-text .mw-parser-output > .gr-home, ' +
'#mw-content-text .mw-parser-output > *:first-child'
);
/* 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 ) {
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' );
if ( contentText ) {
/* Only add extra padding if we're on mobile */
var isMob = window.innerWidth < 768 || !!document.getElementById( 'mw-mf-viewport' );
if ( isMob ) {
/* Check if the toggle is actually obscured */
if ( toggleEl ) {
var toggleRect = toggleEl.getBoundingClientRect();
/* If toggle top < barBottom, the bar is covering it */
if ( toggleRect.top < barBottom ) {
var currentPT = parseInt( window.getComputedStyle( document.body ).paddingTop, 10 ) || 0;
var needed = currentPT + ( barBottom - toggleRect.top ) + 4;
document.body.style.paddingTop = needed + 'px';
}
}
}
}
}
/* ══════════════════════════════════════════════════════════════
* 3. WIRE THE GRANTHA / AUTHOR TAB TOGGLE
* In case the toggle script hasn't loaded yet or failed
* ══════════════════════════════════════════════════════════════ */
$( function () {
var $toggle = $( '#gr-home-toggle' );
var $viewG = $( '#gr-view-grantha' );
var $viewA = $( '#gr-view-author' );
var $btnG = $( '#gr-toggle-grantha' );
var $btnA = $( '#gr-toggle-author' );
if ( !$toggle.length || !$viewG.length || !$viewA.length ) return;
/* Only wire if not already wired */
if ( $toggle.data( 'gr-wired' ) ) return;
$toggle.data( 'gr-wired', true );
function showView( which ) {
if ( which === 'grantha' ) {
$viewG.show(); $viewA.hide();
$btnG.addClass( 'gr-toggle-active' );
$btnA.removeClass( 'gr-toggle-active' );
} else {
$viewA.show(); $viewG.hide();
$btnA.addClass( 'gr-toggle-active' );
$btnG.removeClass( 'gr-toggle-active' );
}
try { localStorage.setItem( 'grantha_home_tab', which ); } catch(e){}
}
$btnG.on( 'click keydown', function(e){
if ( e.type === 'keydown' && e.key !== 'Enter' && e.key !== ' ' ) return;
showView( 'grantha' );
});
$btnA.on( 'click keydown', function(e){
if ( e.type === 'keydown' && e.key !== 'Enter' && e.key !== ' ' ) return;
showView( 'author' );
});
/* Restore last-used tab */
try {
var saved = localStorage.getItem( 'grantha_home_tab' );
if ( saved === 'author' ) showView( 'author' );
else showView( 'grantha' );
} catch(e) { showView( 'grantha' ); }
} );
}() );