Skip to content

Commit 5b079b8

Browse files
committed
Stable calculation of active toc
IntersectionObserver only notifies changed intersections. We need to track which heading tag currently intersects with viewport. Use the top-most intersecting heading to make user-clicked toc links match with active toc links.
1 parent 70995a4 commit 5b079b8

File tree

1 file changed

+45
-33
lines changed
  • lib/rdoc/generator/template/aliki/js

1 file changed

+45
-33
lines changed

lib/rdoc/generator/template/aliki/js/aliki.js

Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -209,54 +209,66 @@ function generateToc() {
209209

210210
function hookTocActiveHighlighting() {
211211
var tocLinks = document.querySelectorAll('.toc-link');
212-
if (tocLinks.length === 0) return;
212+
var targetHeadings = [];
213+
tocLinks.forEach(function(link) {
214+
var targetId = link.getAttribute('data-target');
215+
var heading = document.getElementById(targetId);
216+
if (heading) {
217+
targetHeadings.push(heading);
218+
}
219+
});
220+
221+
if (targetHeadings.length === 0) return;
213222

214223
var observerOptions = {
215224
root: null,
216-
rootMargin: '-20% 0px -35% 0px',
225+
rootMargin: '0% 0px -35% 0px',
217226
threshold: 0
218227
};
219228

220-
var activeLink = null;
229+
var intersectingHeadings = new Set();
230+
function update() {
231+
var firstIntersectingHeading = targetHeadings.find(function(heading) {
232+
return intersectingHeadings.has(heading);
233+
});
234+
if (!firstIntersectingHeading) return;
235+
var correspondingLink = document.querySelector('.toc-link[data-target="' + firstIntersectingHeading.id + '"]');
236+
if (!correspondingLink) return;
237+
238+
// Remove active class from all links
239+
tocLinks.forEach(function(link) {
240+
link.classList.remove('active');
241+
});
242+
243+
// Add active class to current link
244+
correspondingLink.classList.add('active');
245+
activeLink = correspondingLink;
246+
247+
// Scroll link into view if needed
248+
var tocNav = document.querySelector('#toc-nav');
249+
if (tocNav) {
250+
var linkRect = correspondingLink.getBoundingClientRect();
251+
var navRect = tocNav.getBoundingClientRect();
221252

253+
if (linkRect.top < navRect.top || linkRect.bottom > navRect.bottom) {
254+
correspondingLink.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
255+
}
256+
}
257+
}
222258
var observer = new IntersectionObserver(function(entries) {
223259
entries.forEach(function(entry) {
224260
if (entry.isIntersecting) {
225-
var id = entry.target.id;
226-
var correspondingLink = document.querySelector('.toc-link[data-target="' + id + '"]');
227-
228-
if (correspondingLink) {
229-
// Remove active class from all links
230-
tocLinks.forEach(function(link) {
231-
link.classList.remove('active');
232-
});
233-
234-
// Add active class to current link
235-
correspondingLink.classList.add('active');
236-
activeLink = correspondingLink;
237-
238-
// Scroll link into view if needed
239-
var tocNav = document.querySelector('#toc-nav');
240-
if (tocNav) {
241-
var linkRect = correspondingLink.getBoundingClientRect();
242-
var navRect = tocNav.getBoundingClientRect();
243-
244-
if (linkRect.top < navRect.top || linkRect.bottom > navRect.bottom) {
245-
correspondingLink.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
246-
}
247-
}
248-
}
261+
intersectingHeadings.add(entry.target);
262+
} else {
263+
intersectingHeadings.delete(entry.target);
249264
}
250265
});
266+
update();
251267
}, observerOptions);
252268

253269
// Observe all headings that have corresponding TOC links
254-
tocLinks.forEach(function(link) {
255-
var targetId = link.getAttribute('data-target');
256-
var targetHeading = document.getElementById(targetId);
257-
if (targetHeading) {
258-
observer.observe(targetHeading);
259-
}
270+
targetHeadings.forEach(function(heading) {
271+
observer.observe(heading);
260272
});
261273

262274
// Smooth scroll when clicking TOC links

0 commit comments

Comments
 (0)