MediaWiki:Common.js: Difference between revisions
No edit summary |
No edit summary |
||
| Line 1: | Line 1: | ||
/* MediaWiki:Common.js — grantha.io ( | /* MediaWiki:Common.js — grantha.io (v7) | ||
* | * | ||
* Changes vs | * Changes vs v6: | ||
* 1. | * 1. BUG FIX: The main IIFE was never properly closed in v6 — the "About" | ||
* | * link injection block was pasted inside it without the closing }() ); | ||
* | * This broke the JS module boundary and caused the by-Author toggle on | ||
* | * the main page to fail (the grHomeView IIFE ran in a corrupted scope). | ||
* | * Fix: About link injection is now its own self-contained IIFE, cleanly | ||
* | * separated from the transliteration/TOC IIFE. | ||
* | * 2. TOC: Remove the auto-generated "Beginning" link (Vector 2022 always | ||
* | * inserts a first TOC entry that links to the page top; on Sanskrit | ||
* | * content pages this is unnecessary and clutters the TOC). | ||
* 3. TOC: Add "मूल" (Moola) and "उल्लेख" (Ullekha) nav links below the | |||
* document title area — Moola links back to the primary bhashya page | |||
* (on teeka/vyakhya pages), Ullekha links to the references index page | |||
* for the document. Both use the existing data-primary / doc-slug | |||
* attributes already present in the page HTML. | |||
* 4. All other behaviour identical to v6. | |||
*/ | */ | ||
| Line 87: | Line 93: | ||
'क':'க','ख':'க','ग':'க','घ':'க','ङ':'ங', | 'क':'க','ख':'க','ग':'க','घ':'க','ङ':'ங', | ||
'च':'ச','छ':'ச','ज':'ஜ','झ':'ஜ','ञ':'ஞ', | 'च':'ச','छ':'ச','ज':'ஜ','झ':'ஜ','ञ':'ஞ', | ||
'ट':'ட','ठ':'ட','ड':'ட','ढ':'ட',' | 'ट':'ட','ठ':'ட','ड':'ட','ढ':'ட','ண':'ண', | ||
'त':'த','थ':'த','द':'த','ध':'த','न':'ந', | 'त':'த','थ':'த','द':'த','ध':'த','न':'ந', | ||
'प':'ப','फ':'ப','ब':'ப','भ':'ப','म':'ம', | 'प':'ப','फ':'ப','ब':'ப','भ':'ப','म':'ம', | ||
| Line 166: | Line 172: | ||
: transliterateText( orig, script ); | : transliterateText( orig, script ); | ||
} ); | } ); | ||
} | |||
// ── TOC: Remove "Beginning" link ──────────────────────────────── | |||
// Vector 2022 always inserts a first TOC item linking to the page top | |||
// (labelled "Beginning" in English UI). On Sanskrit content pages this | |||
// is redundant — remove it so the TOC starts at the first real heading. | |||
function removeTocBeginning() { | |||
var toc = document.querySelector( '.vector-toc' ); | |||
if ( !toc ) return; | |||
/* The "Beginning" entry lives in a <li> that either: | |||
* (a) has class vector-toc-list-item AND contains an <a> whose href | |||
* ends in the page title with no fragment (i.e. no "#" anchor), or | |||
* (b) is the very first <li> inside .vector-toc-contents whose link | |||
* has no hash fragment at all (it scrolls to the very top). | |||
* We target it by its unique id (#vector-toc-beginning) which Vector | |||
* 2022 reliably sets, with a fallback to the first fragment-free item. | |||
*/ | |||
var beginning = toc.querySelector( '#vector-toc-beginning' ); | |||
if ( !beginning ) { | |||
/* Fallback: first <li> whose anchor has no "#" in href */ | |||
var items = toc.querySelectorAll( '.vector-toc-list-item' ); | |||
for ( var i = 0; i < items.length; i++ ) { | |||
var a = items[ i ].querySelector( 'a' ); | |||
if ( a && a.href.indexOf( '#' ) === -1 ) { | |||
beginning = items[ i ]; | |||
break; | |||
} | |||
} | |||
} | |||
if ( beginning && beginning.parentNode ) { | |||
beginning.parentNode.removeChild( beginning ); | |||
} | |||
} | |||
// ── TOC: Inject Moola / Ullekha nav links ─────────────────────── | |||
// Adds two navigation buttons above the TOC list: | |||
// मूल → the primary bhashya page this teeka belongs to | |||
// (read from .gr-teeka-page[data-primary] when present, | |||
// otherwise derived from the current page path) | |||
// उल्लेख → the /Ullekha sub-page for this document, | |||
// which lists all pramana references cited in it. | |||
// | |||
// The nav bar is injected once and is guarded against duplicate runs. | |||
function injectTocDocNav() { | |||
var toc = document.querySelector( '.vector-toc' ); | |||
if ( !toc ) return; | |||
if ( document.getElementById( 'gr-toc-doc-nav' ) ) return; | |||
/* ── Determine slugs ─────────────────────────────────────────── */ | |||
var artPath = ( window.mw && mw.config && mw.config.get( 'wgArticlePath' ) ) || '/wiki/$1'; | |||
var pageTitle = ( window.mw && mw.config && mw.config.get( 'wgPageName' ) ) || ''; | |||
/* For teeka/vyakhya pages the imported HTML contains: | |||
* <div class="gr-teeka-page" data-primary="Brahmasutra" data-slug="Nyayasudha"> | |||
* Use that when available. */ | |||
var teekaPage = document.querySelector( '.gr-teeka-page' ); | |||
var primarySlug = teekaPage ? ( teekaPage.getAttribute( 'data-primary' ) || '' ) : ''; | |||
var docSlug = teekaPage ? ( teekaPage.getAttribute( 'data-slug' ) || '' ) : ''; | |||
/* If not a teeka page, derive slugs from the page title. | |||
* Page titles look like "Brahmasutra" or "Brahmasutra/Part1". */ | |||
if ( !primarySlug ) { | |||
/* Top-level doc pages: the slug IS the page (or its root) */ | |||
primarySlug = pageTitle.split( '/' )[ 0 ]; | |||
docSlug = primarySlug; | |||
} | |||
if ( !primarySlug ) return; /* nothing to link to */ | |||
/* ── Build URLs ──────────────────────────────────────────────── */ | |||
function wikiUrl( slug ) { | |||
if ( window.mw && mw.util && mw.util.getUrl ) { | |||
return mw.util.getUrl( slug ); | |||
} | |||
return artPath.replace( '$1', encodeURIComponent( slug ).replace( /%2F/g, '/' ) ); | |||
} | |||
var moolaUrl = wikiUrl( primarySlug ); | |||
var ullekhaUrl = wikiUrl( ( teekaPage ? ( primarySlug + '/Vyakhya/' + docSlug ) : primarySlug ) + '/Ullekha' ); | |||
/* ── Don't show Moola link if we ARE the moola page ─────────── */ | |||
var isMoola = !teekaPage && ( pageTitle === primarySlug || pageTitle.indexOf( primarySlug + '/Part' ) === 0 ); | |||
/* ── Build the nav bar ───────────────────────────────────────── */ | |||
var nav = document.createElement( 'div' ); | |||
nav.id = 'gr-toc-doc-nav'; | |||
var navStyle = [ | |||
'display:flex', | |||
'gap:6px', | |||
'padding:6px 8px 4px', | |||
'border-bottom:1px solid var(--border-color-base,#a2a9b1)', | |||
'margin-bottom:4px', | |||
'flex-wrap:wrap', | |||
].join( ';' ); | |||
nav.setAttribute( 'style', navStyle ); | |||
var btnBase = [ | |||
'display:inline-block', | |||
'padding:2px 8px', | |||
'border-radius:3px', | |||
'font-size:0.82em', | |||
'font-weight:500', | |||
'text-decoration:none', | |||
'border:1px solid currentColor', | |||
'line-height:1.6', | |||
'white-space:nowrap', | |||
].join( ';' ); | |||
function makeBtn( href, label, colorVar, bgVar ) { | |||
var a = document.createElement( 'a' ); | |||
a.href = href; | |||
a.textContent = label; | |||
a.setAttribute( 'style', | |||
btnBase + ';' | |||
+ 'color:' + colorVar + ';' | |||
+ 'background:' + bgVar + ';' | |||
); | |||
a.setAttribute( 'data-deva-skip', '1' ); /* don't transliterate these */ | |||
a.addEventListener( 'mouseover', function () { | |||
this.style.opacity = '0.78'; | |||
} ); | |||
a.addEventListener( 'mouseout', function () { | |||
this.style.opacity = '1'; | |||
} ); | |||
return a; | |||
} | |||
if ( !isMoola ) { | |||
nav.appendChild( makeBtn( moolaUrl, 'मूल', '#b04b00', '#fff3e0' ) ); | |||
} | |||
nav.appendChild( makeBtn( ullekhaUrl, 'उल्लेख', '#1a6496', '#e8f4fc' ) ); | |||
/* Insert the nav bar at the very top of the TOC, before the heading */ | |||
var tocInner = toc.querySelector( '.vector-toc-contents' ) || toc.firstElementChild || toc; | |||
toc.insertBefore( nav, tocInner ); | |||
} | } | ||
// ── TOC active-item highlight ──────────────────────────────────── | // ── TOC active-item highlight ──────────────────────────────────── | ||
// FIX: Instead of relying on CSS :active selectors (which fail because | // FIX (v6): Instead of relying on CSS :active selectors (which fail because | ||
// Common.js wraps text nodes in <span data-deva>, making > a or > .link | // Common.js wraps text nodes in <span data-deva>, making > a or > .link | ||
// selectors not match the coloured text), we use a MutationObserver to | // selectors not match the coloured text), we use a MutationObserver to | ||
| Line 199: | Line 341: | ||
link.style.setProperty( 'color', ACTIVE_COLOR, 'important' ); | link.style.setProperty( 'color', ACTIVE_COLOR, 'important' ); | ||
link.style.setProperty( 'font-weight', ACTIVE_WEIGHT, 'important' ); | link.style.setProperty( 'font-weight', ACTIVE_WEIGHT, 'important' ); | ||
link.querySelectorAll( '*' ).forEach( function ( el ) { | link.querySelectorAll( '*' ).forEach( function ( el ) { | ||
el.style.setProperty( 'color', ACTIVE_COLOR, 'important' ); | el.style.setProperty( 'color', ACTIVE_COLOR, 'important' ); | ||
| Line 221: | Line 357: | ||
li._grHighlightAttached = true; | li._grHighlightAttached = true; | ||
liObserver.observe( li, { attributes: true, attributeFilter: [ 'class' ] } ); | liObserver.observe( li, { attributes: true, attributeFilter: [ 'class' ] } ); | ||
if ( li.classList.contains( 'vector-toc-list-item-active' ) ) { | if ( li.classList.contains( 'vector-toc-list-item-active' ) ) { | ||
setLinkActive( li, true ); | setLinkActive( li, true ); | ||
| Line 234: | Line 369: | ||
setLinkActive( li, isActive ); | setLinkActive( li, isActive ); | ||
if ( isActive ) { | if ( isActive ) { | ||
/* ── Auto-expand parent section in TOC ──────────────────────── | /* ── Auto-expand parent section in TOC ──────────────────────── | ||
* Vector collapses child sections under a parent li. When a child | * Vector collapses child sections under a parent li. When a child | ||
* becomes active we must expand its parent by removing the | * becomes active we must expand its parent by removing the | ||
* vector-toc-list-item-collapsed class | * vector-toc-list-item-collapsed class. */ | ||
var ancestor = li.parentNode; | var ancestor = li.parentNode; | ||
while ( ancestor && ancestor !== document.body ) { | while ( ancestor && ancestor !== document.body ) { | ||
| Line 256: | Line 385: | ||
/* ── Scroll active item into view within the TOC ────────────── | /* ── Scroll active item into view within the TOC ────────────── | ||
* Use getBoundingClientRect | * Use getBoundingClientRect so measurement is always in viewport | ||
* coords — avoids offsetTop-relative-to-offsetParent mismatch. */ | |||
* avoids | |||
var container = document.querySelector( '.vector-sticky-pinned-container' ); | var container = document.querySelector( '.vector-sticky-pinned-container' ); | ||
if ( container ) { | if ( container ) { | ||
| Line 268: | Line 396: | ||
var liRect = li.getBoundingClientRect(); | var liRect = li.getBoundingClientRect(); | ||
var cRect = container.getBoundingClientRect(); | var cRect = container.getBoundingClientRect(); | ||
if ( liRect.top < cRect.top + 4 || liRect.bottom > cRect.bottom - 4 ) { | if ( liRect.top < cRect.top + 4 || liRect.bottom > cRect.bottom - 4 ) { | ||
var scrollHost = null; | var scrollHost = null; | ||
var node = li.parentNode; | var node = li.parentNode; | ||
| Line 282: | Line 408: | ||
node = node.parentNode; | node = node.parentNode; | ||
} | } | ||
if ( !scrollHost && container.scrollHeight > container.clientHeight ) { | if ( !scrollHost && container.scrollHeight > container.clientHeight ) { | ||
scrollHost = container; | scrollHost = container; | ||
} | } | ||
if ( scrollHost ) { | if ( scrollHost ) { | ||
var hostRect = scrollHost.getBoundingClientRect(); | var hostRect = scrollHost.getBoundingClientRect(); | ||
var currentScroll = scrollHost.scrollTop; | var currentScroll = scrollHost.scrollTop; | ||
| Line 306: | Line 430: | ||
m.addedNodes.forEach( function ( n ) { | m.addedNodes.forEach( function ( n ) { | ||
if ( n.nodeType !== 1 ) return; | if ( n.nodeType !== 1 ) return; | ||
if ( n.classList && n.classList.contains( 'vector-toc-list-item' ) ) { | if ( n.classList && n.classList.contains( 'vector-toc-list-item' ) ) { | ||
attachHighlight( n ); | attachHighlight( n ); | ||
| Line 314: | Line 437: | ||
} | } | ||
var newSpans = []; | var newSpans = []; | ||
if ( n.classList && n.classList.contains( 'vector-toc-text' ) ) newSpans.push( n ); | if ( n.classList && n.classList.contains( 'vector-toc-text' ) ) newSpans.push( n ); | ||
| Line 337: | Line 459: | ||
structObserver.observe( toc, { childList: true, subtree: true } ); | structObserver.observe( toc, { childList: true, subtree: true } ); | ||
setTimeout( function () { | setTimeout( function () { | ||
var active = toc.querySelector( '.vector-toc-list-item-active' ); | var active = toc.querySelector( '.vector-toc-list-item-active' ); | ||
| Line 352: | Line 469: | ||
// ── Init ──────────────────────────────────────────────────────── | // ── Init ──────────────────────────────────────────────────────── | ||
function init() { | function init() { | ||
var HIDE_IDS = [ | var HIDE_IDS = [ | ||
'vector-appearance', | 'vector-appearance', | ||
| Line 371: | Line 479: | ||
if ( el && el.parentNode ) el.parentNode.removeChild( el ); | if ( el && el.parentNode ) el.parentNode.removeChild( el ); | ||
} ); | } ); | ||
var pageTools = document.getElementById( 'vector-page-tools' ) || | var pageTools = document.getElementById( 'vector-page-tools' ) || | ||
document.querySelector( '.vector-page-tools-pinned-container' ); | document.querySelector( '.vector-page-tools-pinned-container' ); | ||
| Line 384: | Line 490: | ||
removeHiddenEls(); | removeHiddenEls(); | ||
/* ── Teeka view-mode detection | /* ── Teeka view-mode detection ──────────────────────────────── */ | ||
( function detectTeekaMode() { | ( function detectTeekaMode() { | ||
var teekaPage = document.querySelector( '.gr-teeka-page' ); | var teekaPage = document.querySelector( '.gr-teeka-page' ); | ||
if ( !teekaPage ) return; | if ( !teekaPage ) return; | ||
var primary = teekaPage.getAttribute( 'data-primary' ) || ''; | var primary = teekaPage.getAttribute( 'data-primary' ) || ''; | ||
| Line 403: | Line 499: | ||
var mainUrl = artPath.replace( '$1', primary ); | var mainUrl = artPath.replace( '$1', primary ); | ||
var qs = window.location.search; | var qs = window.location.search; | ||
var refParam = qs.match( /[?&]ref=([01])/ ); | var refParam = qs.match( /[?&]ref=([01])/ ); | ||
| Line 411: | Line 506: | ||
} | } | ||
var ref = document.referrer || ''; | var ref = document.referrer || ''; | ||
var fromMain = ref && primary && ref.indexOf( mainUrl ) !== -1; | var fromMain = ref && primary && ref.indexOf( mainUrl ) !== -1; | ||
| Line 425: | Line 519: | ||
if ( needsClean ) removeHiddenEls(); | if ( needsClean ) removeHiddenEls(); | ||
} ); | } ); | ||
hideObserver.observe( document.body, { childList: true, subtree: false } ); | hideObserver.observe( document.body, { childList: true, subtree: false } ); | ||
setTimeout( function () { hideObserver.disconnect(); }, 6000 ); | setTimeout( function () { hideObserver.disconnect(); }, 6000 ); | ||
} | } | ||
| Line 458: | Line 546: | ||
} | } | ||
/* TOC setup — remove Beginning link, inject doc-nav, start active watcher. | |||
* Retry at 300ms and 800ms because Vector 2022 renders the TOC after | |||
* DOMContentLoaded via its own JS. */ | |||
removeTocBeginning(); | |||
injectTocDocNav(); | |||
watchTocActive(); | watchTocActive(); | ||
setTimeout( function () { | |||
removeTocBeginning(); | |||
setTimeout( watchTocActive, 800 ); | injectTocDocNav(); | ||
watchTocActive(); | |||
}, 300 ); | |||
setTimeout( function () { | |||
removeTocBeginning(); | |||
injectTocDocNav(); | |||
watchTocActive(); | |||
}, 800 ); | |||
} | } | ||
| Line 488: | Line 588: | ||
} | } | ||
if ( currentScript !== 'deva' ) applyScript( currentScript ); | if ( currentScript !== 'deva' ) applyScript( currentScript ); | ||
removeTocBeginning(); | |||
injectTocDocNav(); | |||
watchTocActive(); | watchTocActive(); | ||
}, 150 ); | }, 150 ); | ||
| Line 498: | Line 600: | ||
init(); | init(); | ||
} | } | ||
/* ── Inject "About" link into the header navigation | |||
}() ); /* ← end of main transliteration / TOC IIFE */ | |||
// ── Inject "About" link into the header navigation ───────────────── | |||
// This is its own IIFE — completely separate from the main IIFE above. | |||
// (v6 bug: it was incorrectly nested inside the main IIFE without a | |||
// closing }() ); which broke the module boundary and killed the | |||
// by-Author toggle on the main page.) | |||
( function () { | |||
function injectAboutLink() { | |||
if ( document.getElementById( 'gr-about-link' ) ) return; | if ( document.getElementById( 'gr-about-link' ) ) return; | ||
var headerEnd = document.querySelector( '.vector-header-end' ) || | var headerEnd = document.querySelector( '.vector-header-end' ) || | ||
document.querySelector( '#vector-user-links' ) || | document.querySelector( '#vector-user-links' ) || | ||
document.querySelector( '.mw-header' ); | document.querySelector( '.mw-header' ); | ||
if ( !headerEnd ) return; | if ( !headerEnd ) return; | ||
var aboutLink = document.createElement( 'a' ); | var aboutLink = document.createElement( 'a' ); | ||
aboutLink.id = 'gr-about-link'; | aboutLink.id = 'gr-about-link'; | ||
| Line 528: | Line 635: | ||
'transition: color 0.15s, background 0.15s', | 'transition: color 0.15s, background 0.15s', | ||
'white-space: nowrap', | 'white-space: nowrap', | ||
].join(';'); | ].join( ';' ); | ||
aboutLink.addEventListener( 'mouseover', function () { | aboutLink.addEventListener( 'mouseover', function () { | ||
this.style.color = '#fff'; | this.style.color = '#fff'; | ||
| Line 537: | Line 644: | ||
this.style.background = 'transparent'; | this.style.background = 'transparent'; | ||
} ); | } ); | ||
var userLinks = document.querySelector( '.vector-user-links' ) || | var userLinks = document.querySelector( '.vector-user-links' ) || | ||
document.querySelector( '#pt-userpage' ); | document.querySelector( '#pt-userpage' ); | ||
| Line 548: | Line 654: | ||
} | } | ||
if ( document.readyState === 'loading' ) { | |||
document.addEventListener( 'DOMContentLoaded', injectAboutLink ); | |||
} else { | |||
injectAboutLink(); | |||
} | |||
}() ); | }() ); | ||
// ── Main page: by-Grantha / by-Author toggle | // ── Main page: by-Grantha / by-Author toggle ────────────────────── | ||
( function () { | ( function () { | ||
function grHomeView( v ) { | function grHomeView( v ) { | ||