diff --git a/lib/rdoc/generator/template/aliki/js/aliki.js b/lib/rdoc/generator/template/aliki/js/aliki.js index 2855cabb75..6d1605a10e 100644 --- a/lib/rdoc/generator/template/aliki/js/aliki.js +++ b/lib/rdoc/generator/template/aliki/js/aliki.js @@ -305,28 +305,25 @@ function hookTocActiveHighlighting() { if (targetHeadings.length === 0) return; - const observerOptions = { - root: null, - rootMargin: '0% 0px -35% 0px', - threshold: 0 - }; - - const intersectingHeadings = new Set(); + let activeLink = null; const update = () => { - const firstIntersectingHeading = targetHeadings.find((heading) => { - return intersectingHeadings.has(heading); - }); - if (!firstIntersectingHeading) return; - const correspondingLink = document.querySelector(`.toc-link[data-target="${firstIntersectingHeading.id}"]`); - if (!correspondingLink) return; - - // Remove active class from all links - tocLinks.forEach((link) => { - link.classList.remove('active'); + // Prefer the first heading in the upper 65% of the viewport. With no + // heading there, such as deep inside a long method list, the current + // section is the last heading above the viewport. + const viewportLimit = window.innerHeight * 0.65; + const currentHeading = targetHeadings.find((heading) => { + const rect = heading.getBoundingClientRect(); + return rect.bottom > 0 && rect.top < viewportLimit; + }) || targetHeadings.findLast((heading) => { + return heading.getBoundingClientRect().top < 0; }); + if (!currentHeading) return; + const correspondingLink = document.querySelector(`.toc-link[data-target="${currentHeading.id}"]`); + if (!correspondingLink || correspondingLink === activeLink) return; - // Add active class to current link + activeLink?.classList.remove('active'); correspondingLink.classList.add('active'); + activeLink = correspondingLink; // Scroll link into view if needed const tocNav = document.querySelector('#toc-nav'); @@ -339,21 +336,20 @@ function hookTocActiveHighlighting() { } } }; - const observer = new IntersectionObserver((entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - intersectingHeadings.add(entry.target); - } else { - intersectingHeadings.delete(entry.target); - } + + let updateScheduled = false; + const scheduleUpdate = () => { + if (updateScheduled) return; + updateScheduled = true; + requestAnimationFrame(() => { + updateScheduled = false; + update(); }); - update(); - }, observerOptions); + }; + window.addEventListener('scroll', scheduleUpdate, { passive: true }); + window.addEventListener('resize', scheduleUpdate); - // Observe all headings that have corresponding TOC links - targetHeadings.forEach((heading) => { - observer.observe(heading); - }); + update(); } function setHeadingScrollHandler(heading, link) {