/ src / scripts / toc.ts
toc.ts
 1  const tocLinks = document.querySelectorAll<HTMLAnchorElement>(".toc-li a");
 2  const headings = document.querySelectorAll<HTMLElement>(
 3  	"#_top, article h1, article h2, article h3, article h4",
 4  );
 5  const tocMap = new Map<Element, HTMLElement>();
 6  
 7  // Map TOC links to their corresponding headings
 8  for (const link of tocLinks) {
 9  	const id = link.href.split("#")[1];
10  	const heading = document.getElementById(id);
11  	if (heading) tocMap.set(heading, link as HTMLElement);
12  }
13  
14  function checkVisibility(el: HTMLElement) {
15  	const rect = el.getBoundingClientRect();
16  	const viewHeight = Math.max(
17  		document.documentElement.clientHeight,
18  		window.innerHeight,
19  	);
20  	return !(rect.bottom < 0 || rect.top - viewHeight >= 0);
21  }
22  
23  const observer = new IntersectionObserver(
24  	() => {
25  		let visible: HTMLElement | null = null;
26  
27  		for (const heading of headings) {
28  			// check if this specific heading is visible on the screen
29  			const isVisible = checkVisibility(heading);
30  
31  			const link = tocMap.get(heading);
32  
33  			if (!isVisible) {
34  				continue;
35  			}
36  
37  			if (link) link.classList.add("active");
38  
39  			if (!visible) {
40  				visible = heading;
41  			}
42  
43  			break;
44  		}
45  
46  		if (visible) {
47  			for (const key of tocMap.keys()) {
48  				if (key !== visible) {
49  					const link = tocMap.get(key);
50  					if (link) link.classList.remove("active");
51  				}
52  			}
53  		}
54  	},
55  	{ threshold: 0, root: null, rootMargin: "0px" },
56  );
57  
58  // Observe all headings
59  for (const heading of headings) {
60  	observer.observe(heading);
61  }