/* Event listener for scrolling and changing the active label on the TOC */ document.addEventListener("DOMContentLoaded", () => { const toc = document.querySelector("#text-table-of-contents"); if (!toc) { console.warn("No #text-table-of-contents found"); return; } const links = toc.querySelectorAll('a[href^="#"]'); // '^=' is a starts with operator. // Intro matches if (!links.length) { console.warn("No ToC links found"); return; } // Map: id -> link const linkById = new Map(); links.forEach(a => { const id = decodeURIComponent(a.getAttribute("href").slice(1)); const el = document.getElementById(id); if (el) linkById.set(id, a); }); if (!linkById.size) { console.warn("No matching headings with IDs"); return; } // Headings to observe (h2–h4 usually) const headings = [...document.querySelectorAll("h2[id], h3[id], h4[id]")] .filter(h => linkById.has(h.id)); // Helper to mark active const setActive = (id) => { links.forEach(a => { const active = a.getAttribute("href") === `#${id}`; a.classList.toggle("is-active", active); if (active) a.setAttribute("aria-current", "true"); else a.removeAttribute("aria-current"); }); }; // Calculate sticky header offset in px const headerOffsetPx = 6.5 * parseFloat(getComputedStyle(document.documentElement).fontSize); // Track visible headings (id -> distance from top) const visible = new Map(); const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { const id = entry.target.id; if (entry.isIntersecting) { // How far from the top (after header offset) const dist = entry.target.getBoundingClientRect().top - headerOffsetPx; visible.set(id, dist); } else { visible.delete(id); } }); if (visible.size) { // Choose the heading closest to the top (>= -headerOffset) const topMost = [...visible.entries()] .sort((a,b) => Math.abs(a[1]) - Math.abs(b[1]))[0][0]; setActive(topMost); // console.log("Active:", topMost, visible); } }, { root: null, // track relative to viewport rootMargin: `-${headerOffsetPx}px 0px -70% 0px`, threshold: [0, 0.01, 0.1] // fire as soon as it enters }); headings.forEach(h => observer.observe(h)); // Initial highlight (in case load mid‑page) let bestId = null, bestDist = Infinity; headings.forEach(h => { const top = h.getBoundingClientRect().top - headerOffsetPx; const dist = top < 0 ? Math.abs(top) : top + 1e6; if (dist < bestDist) { bestDist = dist; bestId = h.id; } }); if (bestId) setActive(bestId); // smooth-scroll ToC clicks toc.addEventListener("click", (e) => { const a = e.target.closest('a[href^="#"]'); if (!a) return; const id = decodeURIComponent(a.hash.slice(1)); const el = document.getElementById(id); if (!el) return; e.preventDefault(); el.scrollIntoView({ behavior: "smooth", block: "start" }); el.setAttribute("tabindex", "-1"); el.focus({ preventScroll: true }); history.pushState(null, "", `#${id}`); }); });