MediaWiki:Common.js: Difference between revisions
Tag: Undo |
No edit summary |
||
| Line 289: | Line 289: | ||
var _headingObserver = null; | var _headingObserver = null; | ||
var _tocActiveId = null; | |||
// NEW: moved out of attachHeadingObserver's own local scope so they | |||
// can be reused both by the scroll observer AND by a direct trigger | |||
// for search-result clicks — was previously only reachable from | |||
// inside attachHeadingObserver's own IntersectionObserver callback. | |||
// _tocActiveId (was _activeId, local to attachHeadingObserver) is | |||
// module-level now for the same reason — both callers need to share | |||
// the same "what's currently active" state, not track it separately. | |||
function getTocLink(id) { | |||
var toc = document.querySelector('.vector-toc'); | |||
if (!toc) return null; | |||
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 clearActiveToc() { | |||
var toc = document.querySelector('.vector-toc'); | |||
if (!toc) return; | |||
var ACTIVE_COLOR = '#f57c00'; | |||
toc.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) 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 setActiveToc(id) { | |||
var toc = document.querySelector('.vector-toc'); | |||
if (!toc) return; | |||
var ACTIVE_COLOR = '#f57c00'; | |||
if (_tocActiveId === id) return; | |||
_tocActiveId = id; | |||
clearActiveToc(); | |||
if (!id) return; | |||
var li = getTocLi(id); | |||
if (!li) return; | |||
li.classList.add('vector-toc-list-item-active'); | |||
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'); }); | |||
} | |||
} | |||
var anc = li.parentNode; | |||
while (anc && anc !== toc) { | |||
if (anc.classList) anc.classList.remove('vector-toc-list-item-collapsed'); | |||
if (anc.tagName === 'UL' || anc.tagName === 'LI') anc.style.removeProperty('display'); | |||
anc = anc.parentNode; | |||
} | |||
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; | |||
} | |||
} | |||
} | |||
// NEW: given any element in the article content, finds the nearest | |||
// heading (with an id) that precedes it in document order, using | |||
// compareDocumentPosition — robust regardless of nesting depth. | |||
// Verified against a realistic multi-section fragment, including | |||
// the "before any heading" and "right after a chapter heading, no | |||
// sub-heading yet" edge cases, before wiring this in. | |||
function findNearestHeadingId(target) { | |||
var content = document.querySelector('.mw-parser-output'); | |||
if (!content) return null; | |||
var headings = content.querySelectorAll('h1[id],h2[id],h3[id],h4[id],h5[id],h6[id]'); | |||
var nearest = null; | |||
for (var i = 0; i < headings.length; i++) { | |||
var h = headings[i]; | |||
var pos = h.compareDocumentPosition(target); | |||
if (pos & Node.DOCUMENT_POSITION_FOLLOWING) { | |||
nearest = h.id; | |||
} else { | |||
break; | |||
} | |||
} | |||
return nearest; | |||
} | |||
// NEW: exposed so the search-highlight IIFE (below) can directly | |||
// trigger the TOC to expand and highlight the right section right | |||
// after a search-result scroll lands — rather than relying solely on | |||
// the IntersectionObserver to catch up on its own timing. | |||
window.grHighlightTocForElement = function (el) { | |||
if (!el) return; | |||
var id = findNearestHeadingId(el); | |||
if (id) setActiveToc(id); | |||
}; | |||
function attachHeadingObserver() { | function attachHeadingObserver() { | ||
| Line 294: | Line 394: | ||
if (_headingObserver) return; | if (_headingObserver) return; | ||
if (!window.IntersectionObserver) return; | if (!window.IntersectionObserver) return; | ||
var content = document.querySelector('.mw-parser-output'); | var content = document.querySelector('.mw-parser-output'); | ||
if (!content) return; | if (!content) return; | ||
| Line 302: | Line 400: | ||
var toc = document.querySelector('.vector-toc'); | var toc = document.querySelector('.vector-toc'); | ||
if (!toc) return; | if (!toc) return; | ||
var _visible = new Set(); | var _visible = new Set(); | ||
_headingObserver = new IntersectionObserver(function (entries) { | _headingObserver = new IntersectionObserver(function (entries) { | ||
| Line 362: | Line 415: | ||
headings.forEach(function (h) { var y = h.getBoundingClientRect().top; if (y < 0 && y > bestY) { bestY = y; topId = h.id; } }); | headings.forEach(function (h) { var y = h.getBoundingClientRect().top; if (y < 0 && y > bestY) { bestY = y; topId = h.id; } }); | ||
} | } | ||
setActiveToc(topId || null); | |||
}, { rootMargin: '-60px 0px -65% 0px', threshold: 0 }); | }, { rootMargin: '-60px 0px -65% 0px', threshold: 0 }); | ||
headings.forEach(function (h) { _headingObserver.observe(h); }); | headings.forEach(function (h) { _headingObserver.observe(h); }); | ||
| Line 501: | Line 554: | ||
var isAnon = !userName || (window.mw && mw.config.get('wgUserId') === null); | var isAnon = !userName || (window.mw && mw.config.get('wgUserId') === null); | ||
var authEl = null; | var authEl = null; | ||
if (isAnon) { | if (isAnon) { | ||
| Line 587: | Line 639: | ||
}); | }); | ||
menu.querySelector('#gr-header-logout').addEventListener('click', function (e) { | menu.querySelector('#gr-header-logout').addEventListener('click', function (e) { | ||
e.preventDefault(); | e.preventDefault(); | ||
| Line 673: | Line 724: | ||
var after = document.createTextNode(orig.slice(idx + hlText.length)); | var after = document.createTextNode(orig.slice(idx + hlText.length)); | ||
parent.insertBefore(before, spanEl); parent.insertBefore(mark, spanEl); parent.insertBefore(after, spanEl); parent.removeChild(spanEl); | parent.insertBefore(before, spanEl); parent.insertBefore(mark, spanEl); parent.insertBefore(after, spanEl); parent.removeChild(spanEl); | ||
setTimeout(function () { mark.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 100); | setTimeout(function () { mark.scrollIntoView({ behavior: 'smooth', block: 'center' }); if (window.grHighlightTocForElement) window.grHighlightTocForElement(mark); }, 100); | ||
found = true; | found = true; | ||
} | } | ||
| Line 692: | Line 743: | ||
p.insertBefore(document.createTextNode(txt.slice(idx2 + mark2.textContent.length)), node); | p.insertBefore(document.createTextNode(txt.slice(idx2 + mark2.textContent.length)), node); | ||
p.removeChild(node); | p.removeChild(node); | ||
setTimeout(function () { mark2.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 100); | setTimeout(function () { mark2.scrollIntoView({ behavior: 'smooth', block: 'center' }); if (window.grHighlightTocForElement) window.grHighlightTocForElement(mark2); }, 100); | ||
found = true; | found = true; | ||
} | } | ||
| Line 715: | Line 766: | ||
/* ── Search result highlight ──────────────────────────────────── */ | /* ── Search result highlight ──────────────────────────────────── */ | ||
(function () { | (function () { | ||
| Line 794: | Line 838: | ||
first.classList.add('gr-search-hl-pulse'); | first.classList.add('gr-search-hl-pulse'); | ||
setTimeout(function () { first.classList.remove('gr-search-hl-pulse'); }, 2000); | setTimeout(function () { first.classList.remove('gr-search-hl-pulse'); }, 2000); | ||
// NEW: directly trigger the TOC to expand and highlight the | |||
// right section, rather than relying solely on the scroll | |||
// observer to catch up on its own. This is the actual fix — | |||
// confirmed the helper against a realistic multi-section | |||
// fragment before wiring it in here. | |||
if (window.grHighlightTocForElement) window.grHighlightTocForElement(first); | |||
} | } | ||
| Line 911: | Line 961: | ||
hlEls[current].classList.add('gr-search-hl-current'); | hlEls[current].classList.add('gr-search-hl-current'); | ||
hlEls[current].scrollIntoView({ behavior: 'smooth', block: 'center' }); | hlEls[current].scrollIntoView({ behavior: 'smooth', block: 'center' }); | ||
// NEW: same TOC sync on Prev/Next navigation within the | |||
// results, not just the initial jump. | |||
if (window.grHighlightTocForElement) window.grHighlightTocForElement(hlEls[current]); | |||
} | } | ||