From 08123c70bf5a97b1731344086ccda733d4edfaa9 Mon Sep 17 00:00:00 2001 From: tomoya ishida Date: Tue, 18 Nov 2025 20:38:05 +0900 Subject: [PATCH 2/2] Stable calculation of active toc (#1456) Fix this unstable active table-of-contents link calculation shown in this video: Clicking "Plugin types", active toc link will be "Option Parsing" or "File Parsing", depend on previous scroll position. https://github.com/user-attachments/assets/bfd5c92a-9f11-4c9b-9267-86ae55607541 IntersectionObserver only notifies changed intersections. We need to track which heading tag currently intersects with viewport. (I use Set in this pull request) Use the top-most intersecting heading to make user-clicked toc links match with active toc links. --------- Co-authored-by: Sutou Kouhei --- lib/rdoc/generator/template/aliki/js/aliki.js | 77 +++++++++++-------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/lib/rdoc/generator/template/aliki/js/aliki.js b/lib/rdoc/generator/template/aliki/js/aliki.js index 599dc4a15d..5fcc3ac317 100644 --- a/lib/rdoc/generator/template/aliki/js/aliki.js +++ b/lib/rdoc/generator/template/aliki/js/aliki.js @@ -209,54 +209,65 @@ function generateToc() { function hookTocActiveHighlighting() { var tocLinks = document.querySelectorAll('.toc-link'); - if (tocLinks.length === 0) return; + var targetHeadings = []; + tocLinks.forEach(function(link) { + var targetId = link.getAttribute('data-target'); + var heading = document.getElementById(targetId); + if (heading) { + targetHeadings.push(heading); + } + }); + + if (targetHeadings.length === 0) return; var observerOptions = { root: null, - rootMargin: '-20% 0px -35% 0px', + rootMargin: '0% 0px -35% 0px', threshold: 0 }; - var activeLink = null; + var intersectingHeadings = new Set(); + function update() { + var firstIntersectingHeading = targetHeadings.find(function(heading) { + return intersectingHeadings.has(heading); + }); + if (!firstIntersectingHeading) return; + var correspondingLink = document.querySelector('.toc-link[data-target="' + firstIntersectingHeading.id + '"]'); + if (!correspondingLink) return; + + // Remove active class from all links + tocLinks.forEach(function(link) { + link.classList.remove('active'); + }); + + // Add active class to current link + correspondingLink.classList.add('active'); + + // Scroll link into view if needed + var tocNav = document.querySelector('#toc-nav'); + if (tocNav) { + var linkRect = correspondingLink.getBoundingClientRect(); + var navRect = tocNav.getBoundingClientRect(); + if (linkRect.top < navRect.top || linkRect.bottom > navRect.bottom) { + correspondingLink.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + } + } var observer = new IntersectionObserver(function(entries) { entries.forEach(function(entry) { if (entry.isIntersecting) { - var id = entry.target.id; - var correspondingLink = document.querySelector('.toc-link[data-target="' + id + '"]'); - - if (correspondingLink) { - // Remove active class from all links - tocLinks.forEach(function(link) { - link.classList.remove('active'); - }); - - // Add active class to current link - correspondingLink.classList.add('active'); - activeLink = correspondingLink; - - // Scroll link into view if needed - var tocNav = document.querySelector('#toc-nav'); - if (tocNav) { - var linkRect = correspondingLink.getBoundingClientRect(); - var navRect = tocNav.getBoundingClientRect(); - - if (linkRect.top < navRect.top || linkRect.bottom > navRect.bottom) { - correspondingLink.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); - } - } - } + intersectingHeadings.add(entry.target); + } else { + intersectingHeadings.delete(entry.target); } }); + update(); }, observerOptions); // Observe all headings that have corresponding TOC links - tocLinks.forEach(function(link) { - var targetId = link.getAttribute('data-target'); - var targetHeading = document.getElementById(targetId); - if (targetHeading) { - observer.observe(targetHeading); - } + targetHeadings.forEach(function(heading) { + observer.observe(heading); }); // Smooth scroll when clicking TOC links