MediaWiki:Common.js: Difference between revisions
No edit summary |
No edit summary |
||
| Line 440: | Line 440: | ||
if ( active ) setActive( active, true ); | if ( active ) setActive( active, true ); | ||
}, 300 ); | }, 300 ); | ||
} | |||
// ── Custom TOC builder (replaces Vector heading-based TOC) ───────── | |||
// Reads span.gr-toc-anchor elements which have clean IDs set by the | |||
// importer — avoids MediaWiki's URL-encoded heading anchors entirely. | |||
// Mirrors the ref site's data-attribute approach: TOC entries come | |||
// from data-title / data-level on the anchor spans, not from headings. | |||
var _tocBuilt = false; | |||
function buildCustomToc() { | |||
if ( _isNoTocPage() ) return; | |||
var toc = document.querySelector( '.vector-toc' ); | |||
if ( !toc ) return; | |||
// Collect all anchor spans in document order | |||
var anchors = Array.from( | |||
document.querySelectorAll( '.mw-parser-output .gr-toc-anchor' ) | |||
); | |||
if ( !anchors.length ) { | |||
// No gr-toc-anchor spans — fall back to standard TOC customisations | |||
removeTocBeginning(); | |||
renameTocTitle(); | |||
expandTocSections(); | |||
injectTocDocNav(); | |||
watchTocActive(); | |||
return; | |||
} | |||
// Build TOC data: [{id, title, level}] | |||
var entries = anchors.map( function ( span ) { | |||
return { | |||
id: span.id, | |||
title: span.getAttribute( 'data-title' ) || span.id, | |||
level: parseInt( span.getAttribute( 'data-level' ) || '1', 10 ), | |||
el: span, | |||
}; | |||
} ); | |||
// Build the <ul> tree (3 levels: adhyaya, pada, adhikarana) | |||
function buildList( items ) { | |||
var ul = document.createElement( 'ul' ); | |||
ul.className = 'vector-toc-list'; | |||
var i = 0; | |||
while ( i < items.length ) { | |||
var item = items[ i ]; | |||
var li = document.createElement( 'li' ); | |||
li.className = 'vector-toc-list-item vector-toc-level-' + item.level; | |||
li.setAttribute( 'data-toc-id', item.id ); | |||
var a = document.createElement( 'a' ); | |||
a.className = 'vector-toc-link'; | |||
a.href = '#' + item.id; | |||
var textSpan = document.createElement( 'span' ); | |||
textSpan.className = 'vector-toc-text'; | |||
textSpan.setAttribute( 'data-deva', item.title ); | |||
textSpan.textContent = currentScript !== 'deva' | |||
? transliterateText( item.title, currentScript ) | |||
: item.title; | |||
translatableSpans.push( textSpan ); | |||
a.appendChild( textSpan ); | |||
li.appendChild( a ); | |||
// Collect children (higher level numbers) | |||
var children = []; | |||
var j = i + 1; | |||
while ( j < items.length && items[ j ].level > item.level ) { | |||
children.push( items[ j ] ); | |||
j++; | |||
} | |||
if ( children.length ) { | |||
var childUl = buildList( children ); | |||
li.appendChild( childUl ); | |||
li.classList.add( 'vector-toc-list-item-with-children' ); | |||
// Start collapsed for level 2+ if many children | |||
if ( item.level >= 2 && children.length > 4 ) { | |||
li.classList.add( 'vector-toc-list-item-collapsed' ); | |||
} | |||
} | |||
ul.appendChild( li ); | |||
i = j; | |||
} | |||
return ul; | |||
} | |||
// Inject into Vector TOC | |||
var contents = toc.querySelector( '.vector-toc-contents' ); | |||
if ( !contents ) { | |||
contents = document.createElement( 'div' ); | |||
contents.className = 'vector-toc-contents'; | |||
toc.appendChild( contents ); | |||
} | |||
contents.innerHTML = ''; | |||
var listUl = buildList( entries ); | |||
listUl.id = 'mw-panel-toc-list'; | |||
contents.appendChild( listUl ); | |||
_tocBuilt = true; | |||
// Rename title | |||
renameTocTitle(); | |||
injectTocDocNav(); | |||
// ── IntersectionObserver for active highlighting ────────────── | |||
if ( !window.IntersectionObserver ) return; | |||
var ACTIVE_COLOR = '#f57c00'; | |||
var _activeId = null; | |||
function setTocActive( id ) { | |||
if ( _activeId === id ) return; | |||
_activeId = id; | |||
// Clear all active classes and inline styles | |||
contents.querySelectorAll( '.vector-toc-list-item' ).forEach( function ( li ) { | |||
li.classList.remove( 'vector-toc-list-item-active' ); | |||
var lnk = li.querySelector( '.vector-toc-link' ); | |||
if ( lnk ) { | |||
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' ); | |||
} ); | |||
} | |||
} ); | |||
if ( !id ) return; | |||
// Find matching li | |||
var activeLi = contents.querySelector( '[data-toc-id="' + id + '"]' ); | |||
if ( !activeLi ) return; | |||
activeLi.classList.add( 'vector-toc-list-item-active' ); | |||
// Only highlight if it is the innermost active item (no active child) | |||
var hasActiveChild = !!activeLi.querySelector( | |||
'.vector-toc-list-item .vector-toc-list-item-active' | |||
); | |||
if ( !hasActiveChild ) { | |||
var lnk = activeLi.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 = activeLi.parentNode; | |||
while ( anc && anc !== contents ) { | |||
if ( anc.classList && anc.classList.contains( 'vector-toc-list-item-collapsed' ) ) { | |||
anc.classList.remove( 'vector-toc-list-item-collapsed' ); | |||
} | |||
anc = anc.parentNode; | |||
} | |||
// Scroll TOC entry into view | |||
var container = document.querySelector( '.vector-sticky-pinned-container' ); | |||
if ( container ) { | |||
var lr = activeLi.getBoundingClientRect(); | |||
var cr = container.getBoundingClientRect(); | |||
if ( lr.top < cr.top + 4 || lr.bottom > cr.bottom - 4 ) { | |||
container.scrollTop += lr.top - cr.top - container.clientHeight / 2; | |||
} | |||
} | |||
} | |||
// Track which anchors are visible | |||
var _visibleIds = new Set(); | |||
var observer = new IntersectionObserver( function ( entries ) { | |||
entries.forEach( function ( entry ) { | |||
var id = entry.target.id; | |||
if ( entry.isIntersecting ) { | |||
_visibleIds.add( id ); | |||
} else { | |||
_visibleIds.delete( id ); | |||
} | |||
} ); | |||
// Find the topmost visible anchor | |||
var topId = null; | |||
var topY = Infinity; | |||
_visibleIds.forEach( function ( id ) { | |||
var el = document.getElementById( id ); | |||
if ( el ) { | |||
var y = el.getBoundingClientRect().top; | |||
if ( y < topY ) { topY = y; topId = id; } | |||
} | |||
} ); | |||
// If nothing visible (scrolled past), find the last anchor above viewport | |||
if ( !topId ) { | |||
var best = null, bestBottom = -Infinity; | |||
anchors.forEach( function ( span ) { | |||
var r = span.getBoundingClientRect(); | |||
if ( r.bottom < 80 && r.bottom > bestBottom ) { | |||
bestBottom = r.bottom; | |||
best = span.id; | |||
} | |||
} ); | |||
topId = best; | |||
} | |||
setTocActive( topId ); | |||
}, { rootMargin: '-60px 0px -70% 0px', threshold: 0 } ); | |||
anchors.forEach( function ( span ) { observer.observe( span ); } ); | |||
} | } | ||
// ── Run all TOC customisations ─────────────────────────────────── | // ── Run all TOC customisations ─────────────────────────────────── | ||
function setupToc() { | function setupToc() { | ||
removeTocBeginning(); | if ( _tocBuilt ) { | ||
// TOC already built — just re-run label/nav parts | |||
renameTocTitle(); | |||
injectTocDocNav(); | |||
return; | |||
} | |||
// Try custom TOC first; falls back to Vector customisations if no anchors | |||
buildCustomToc(); | |||
if ( !_tocBuilt ) { | |||
removeTocBeginning(); | |||
renameTocTitle(); | |||
expandTocSections(); | |||
injectTocDocNav(); | |||
watchTocActive(); | |||
} | |||
} | } | ||