MediaWiki:Common.js: Difference between revisions
No edit summary |
No edit summary |
||
| Line 14: | Line 14: | ||
* sub-path install (e.g. /My_wiki/). Points to "My_wiki:About". | * sub-path install (e.g. /My_wiki/). Points to "My_wiki:About". | ||
* 8. TOC customisations skipped on Main_Page and About pages. | * 8. TOC customisations skipped on Main_Page and About pages. | ||
* 9. | * 9. Uses Vector's native TOC with IntersectionObserver-based active highlight. | ||
*/ | */ | ||
| Line 448: | Line 447: | ||
} | } | ||
// ── | // ── 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 | function setupToc() { | ||
if ( _isNoTocPage() ) return; | if ( _isNoTocPage() ) return; | ||
var toc = document.querySelector( '.vector-toc' ); | var toc = document.querySelector( '.vector-toc' ); | ||
if ( !toc ) return; | 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. | |||
watchTocActive(); | var _headingObserver = null; | ||
function attachHeadingObserver() { | |||
if ( _isNoTocPage() ) return; | |||
if ( _headingObserver ) return; // only attach once | |||
if ( !window.IntersectionObserver ) { | |||
watchTocActive(); // fallback for old browsers | |||
return; | return; | ||
} | } | ||
var ACTIVE_COLOR = '#f57c00'; | |||
var | 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' ); | li.classList.remove( 'vector-toc-list-item-active' ); | ||
var lnk = li.querySelector( '.vector-toc-link' ); | var lnk = li.querySelector( '.vector-toc-link' ); | ||
if ( lnk ) | 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; | if ( !id ) return; | ||
var li = getTocLi( id ); | |||
if ( !li ) return; | |||
if ( ! | |||
li.classList.add( 'vector-toc-list-item-active' ); | |||
// | // Highlight only the innermost active item | ||
var hasActiveChild = !! | var hasActiveChild = !!li.querySelector( | ||
'.vector-toc-list-item .vector-toc-list-item-active' | '.vector-toc-list-item .vector-toc-list-item-active' | ||
); | ); | ||
if ( !hasActiveChild ) { | if ( !hasActiveChild ) { | ||
var lnk = | var lnk = li.querySelector( '.vector-toc-link' ); | ||
if ( lnk ) { | if ( lnk ) { | ||
lnk.style.setProperty( 'color', ACTIVE_COLOR, 'important' ); | lnk.style.setProperty( 'color', ACTIVE_COLOR, 'important' ); | ||
| Line 600: | Line 547: | ||
// Expand collapsed ancestors | // Expand collapsed ancestors | ||
var anc = | var anc = li.parentNode; | ||
while ( anc && anc !== | while ( anc && anc !== toc ) { | ||
if ( anc.classList && anc.classList.contains( 'vector-toc-list-item-collapsed' ) ) { | if ( anc.classList && anc.classList.contains( 'vector-toc-list-item-collapsed' ) ) { | ||
anc.classList.remove( 'vector-toc-list-item-collapsed' ); | anc.classList.remove( 'vector-toc-list-item-collapsed' ); | ||
| Line 608: | Line 555: | ||
} | } | ||
// Scroll TOC | // Scroll TOC item into view within sidebar | ||
var | var sticky = document.querySelector( '.vector-sticky-pinned-container' ); | ||
if ( | var scrollEl = sticky || toc; | ||
var lr = | if ( scrollEl.scrollHeight > scrollEl.clientHeight ) { | ||
var cr = | var lr = li.getBoundingClientRect(); | ||
if ( lr.top < cr.top + | 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 | // Track which headings are visible | ||
var | var _visible = new Set(); | ||
_headingObserver = new IntersectionObserver( function ( entries ) { | |||
entries.forEach( function ( entry ) { | entries.forEach( function ( entry ) { | ||
if ( entry.isIntersecting ) { | if ( entry.isIntersecting ) { | ||
_visible.add( entry.target.id ); | |||
} else { | } else { | ||
_visible.delete( entry.target.id ); | |||
} | } | ||
} ); | } ); | ||
// | // Pick the topmost visible heading | ||
var topId = null; | var topId = null; | ||
var topY = Infinity; | var topY = Infinity; | ||
_visible.forEach( function ( id ) { | |||
var el = document.getElementById( id ); | var el = document.getElementById( id ); | ||
if ( el ) { | if ( el ) { | ||
var y = el.getBoundingClientRect().top; | var y = el.getBoundingClientRect().top; | ||
if ( y < topY ) { topY = y; topId = id; } | if ( y >= 0 && y < topY ) { topY = y; topId = id; } | ||
} | } | ||
} ); | } ); | ||
// | // Nothing visible: find the last heading scrolled past (above viewport) | ||
if ( !topId ) { | if ( !topId ) { | ||
var | var bestY = -Infinity; | ||
headings.forEach( function ( h ) { | |||
var | var y = h.getBoundingClientRect().top; | ||
if ( | if ( y < 0 && y > bestY ) { bestY = y; topId = h.id; } | ||
} ); | } ); | ||
} | } | ||
setActive( topId || null ); | |||
}, { rootMargin: '-60px 0px - | }, { | ||
// 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 ); } ); | |||
} | } | ||
| Line 778: | Line 678: | ||
// Vector 2022 defers TOC render — retry at 300ms and 800ms | // Vector 2022 defers TOC render — retry at 300ms and 800ms | ||
// Use a single short delay so Vector has rendered the TOC | |||
setTimeout( setupToc, 200 ); | |||
setTimeout( setupToc, | |||
} | } | ||
| Line 842: | Line 741: | ||
} | } | ||
if ( currentScript !== 'deva' ) applyScript( currentScript ); | if ( currentScript !== 'deva' ) applyScript( currentScript ); | ||
setupToc(); | |||
}, 150 ); | }, 150 ); | ||
} ); | } ); | ||