MediaWiki:Common.js: Difference between revisions
No edit summary |
No edit summary |
||
| Line 276: | Line 276: | ||
translatableSpans.push(lspan); | translatableSpans.push(lspan); | ||
a.appendChild(lspan); | a.appendChild(lspan); | ||
return a; | return a; | ||
} | } | ||
| Line 307: | Line 305: | ||
return a ? a.closest('.vector-toc-list-item') : null; | return a ? a.closest('.vector-toc-list-item') : null; | ||
} | } | ||
// Highlighting is class-only (styled in Common.css §6): | |||
// .gr-toc-current — the one entry being read, on the <a> itself | |||
// .gr-toc-in-path — its parent sections, marked on the <li> | |||
// Previously this set inline colours on the active item AND on every | |||
// element inside it, so a section with sub-sections lit up whole. | |||
function clearActiveToc() { | function clearActiveToc() { | ||
var toc = document.querySelector('.vector-toc'); | var toc = document.querySelector('.vector-toc'); | ||
if (!toc) return; | if (!toc) return; | ||
toc.querySelectorAll('.gr-toc-current').forEach(function (el) { el.classList.remove('gr-toc-current'); }); | |||
toc.querySelectorAll('. | toc.querySelectorAll('.gr-toc-in-path').forEach(function (el) { el.classList.remove('gr-toc-in-path'); }); | ||
} | } | ||
function setActiveToc(id) { | function setActiveToc(id) { | ||
var toc = document.querySelector('.vector-toc'); | var toc = document.querySelector('.vector-toc'); | ||
if (!toc) return; | if (!toc) return; | ||
if (_tocActiveId === id) return; | if (_tocActiveId === id) return; | ||
_tocActiveId = id; | _tocActiveId = id; | ||
| Line 330: | Line 325: | ||
var li = getTocLi(id); | var li = getTocLi(id); | ||
if (!li) return; | if (!li) return; | ||
// The entry itself — the link only, so sub-entries stay as they are. | |||
var | var lnk = li.querySelector(':scope > .vector-toc-link') || li.querySelector('.vector-toc-link'); | ||
if (lnk) lnk.classList.add('gr-toc-current'); | |||
// Its parent sections get the quieter "you are in here" treatment, | |||
// and any collapsed ancestor is opened so the entry is visible. | |||
var anc = li.parentNode; | var anc = li.parentNode; | ||
while (anc && anc !== toc) { | while (anc && anc !== toc) { | ||
if (anc.classList) anc.classList.remove('vector-toc-list-item-collapsed'); | if (anc.classList) { | ||
anc.classList.remove('vector-toc-list-item-collapsed'); | |||
if (anc.classList.contains('vector-toc-list-item')) anc.classList.add('gr-toc-in-path'); | |||
} | |||
if (anc.tagName === 'UL' || anc.tagName === 'LI') anc.style.removeProperty('display'); | if (anc.tagName === 'UL' || anc.tagName === 'LI') anc.style.removeProperty('display'); | ||
anc = anc.parentNode; | anc = anc.parentNode; | ||
| Line 387: | Line 380: | ||
if (!el) return; | if (!el) return; | ||
var id = findNearestHeadingId(el); | var id = findNearestHeadingId(el); | ||
if (id) setActiveToc(id); | if (id) { _tocLockUntil = Date.now() + 700; setActiveToc(id); } | ||
}; | }; | ||
// ── Which section am I reading? ──────────────────────────────── | |||
// Was an IntersectionObserver with a shrunken root band. Two faults | |||
// came out of that, both reported as "the highlight is wrong": | |||
// • right after jumping to a heading, that heading sits ABOVE the | |||
// band, so nothing was "visible" and the fallback picked the | |||
// PREVIOUS heading — clicking a sub-section highlighted its parent; | |||
// • once every heading was outside the band, no intersection ever | |||
// changed again, so the observer stopped firing and the highlight | |||
// froze wherever it was. | |||
// This measures instead: the active section is the last heading above | |||
// a reading line just under the sticky toolbar. It updates on every | |||
// scroll frame and can't go stale. | |||
var _tocHeadings = []; | |||
var _tocTops = []; | |||
var _tocMeasuredHeight = 0; | |||
var _tocLockUntil = 0; | |||
function measureHeadings() { | |||
var y = window.pageYOffset || document.documentElement.scrollTop || 0; | |||
_tocTops = _tocHeadings.map(function (h) { return h.getBoundingClientRect().top + y; }); | |||
_tocMeasuredHeight = document.documentElement.scrollHeight; | |||
} | |||
function readingLine() { | |||
var y = window.pageYOffset || document.documentElement.scrollTop || 0; | |||
var bar = document.getElementById('gr-static-bar'); | |||
var barBottom = bar ? Math.max(bar.getBoundingClientRect().bottom, 0) : 0; | |||
return y + barBottom + 16; | |||
} | |||
var _lockRecheck = null; | |||
function updateActiveFromScroll() { | |||
if (!_tocHeadings.length) return; | |||
if (Date.now() < _tocLockUntil) { // a click just chose one | |||
// Re-check once the lock lifts, in case this was the last | |||
// scroll event — otherwise the highlight would sit on the | |||
// clicked entry even after the reader has moved on. | |||
if (!_lockRecheck) { | |||
_lockRecheck = setTimeout(function () { _lockRecheck = null; updateActiveFromScroll(); }, _tocLockUntil - Date.now() + 30); | |||
} | |||
return; | |||
} | |||
// Interleaving a commentary or changing the font size moves things; | |||
// re-measure when the page's height has changed. | |||
if (document.documentElement.scrollHeight !== _tocMeasuredHeight) measureHeadings(); | |||
var line = readingLine(), lo = 0, hi = _tocTops.length - 1, idx = 0; | |||
while (lo <= hi) { // last heading at or above the line | |||
var mid = (lo + hi) >> 1; | |||
if (_tocTops[mid] <= line + 1) { idx = mid; lo = mid + 1; } else { hi = mid - 1; } | |||
} | |||
setActiveToc(_tocHeadings[idx].id); | |||
} | |||
function attachHeadingObserver() { | function attachHeadingObserver() { | ||
if (_isNoTocPage()) return; | if (_isNoTocPage()) return; | ||
if (_headingObserver) return; | if (_headingObserver) return; | ||
var content = document.querySelector('.mw-parser-output'); | var content = document.querySelector('.mw-parser-output'); | ||
if (!content) return; | if (!content) return; | ||
_tocHeadings = Array.prototype.slice.call(content.querySelectorAll('h1[id],h2[id],h3[id],h4[id],h5[id],h6[id]')); | |||
if (! | if (!_tocHeadings.length) return; | ||
if (!document.querySelector('.vector-toc')) return; | |||
measureHeadings(); | |||
// Time-based throttle rather than a requestAnimationFrame latch: if | |||
// one frame callback is ever skipped (background tab, a jump during | |||
// load) a latch stays stuck and the highlight stops updating for | |||
// good — which is one of the ways this went stale before. | |||
var lastRun = 0, trailing = null; | |||
function onScroll() { | |||
var now = Date.now(); | |||
if (now - lastRun >= 80) { lastRun = now; updateActiveFromScroll(); } | |||
else if (!trailing) { | |||
trailing = setTimeout(function () { trailing = null; lastRun = Date.now(); updateActiveFromScroll(); }, 80); | |||
} | } | ||
} | |||
}, { | window.addEventListener('scroll', onScroll, { passive: true }); | ||
window.addEventListener('resize', function () { measureHeadings(); onScroll(); }, { passive: true }); | |||
if (document.fonts && document.fonts.ready && document.fonts.ready.then) { | |||
document.fonts.ready.then(function () { measureHeadings(); onScroll(); }); | |||
} | |||
_headingObserver = { onScroll: onScroll }; // marks "already wired" | |||
updateActiveFromScroll(); | |||
} | |||
// Clicking an entry highlights it straight away, rather than waiting | |||
// for the scroll observer to catch up after the jump. | |||
function attachTocClickHandler(toc) { | |||
if (toc.getAttribute('data-gr-click-wired')) return; | |||
toc.setAttribute('data-gr-click-wired', '1'); | |||
toc.addEventListener('click', function (e) { | |||
var a = e.target.closest ? e.target.closest('.vector-toc-link') : null; | |||
if (!a) return; | |||
var href = a.getAttribute('href') || ''; | |||
if (href.charAt(0) !== '#') return; | |||
var id; | |||
try { id = decodeURIComponent(href.slice(1)); } catch (err) { id = href.slice(1); } | |||
if (id) { _tocLockUntil = Date.now() + 700; setActiveToc(id); } | |||
}); | |||
} | } | ||
| Line 428: | Line 488: | ||
expandTocSections(); | expandTocSections(); | ||
injectTocDocNav(); | injectTocDocNav(); | ||
attachTocClickHandler(toc); | |||
attachHeadingObserver(); | attachHeadingObserver(); | ||
} | } | ||