// assets/toc_scrollspy.js console.log("[toc_scrollspy] loaded (v3-scroll-sync)"); (function () { let ticking = false; function getTocState() { const root = document.getElementById("toc-root"); if (!root) return null; const offset = parseInt(root.dataset.tocOffset || "80", 10); const items = Array.from(root.querySelectorAll(".toc-item")); if (!items.length) return null; const pairs = items .map((item) => { const targetId = item.dataset.target; if (!targetId) return null; const el = document.getElementById(targetId); if (!el) return null; return { item, el, id: targetId }; }) .filter(Boolean); if (!pairs.length) return null; return { root, offset, pairs }; } function keepItemVisible(root, item) { if (!root || !item) return; const rootTop = root.scrollTop; const rootBottom = rootTop + root.clientHeight; const itemTop = item.offsetTop; const itemBottom = itemTop + item.offsetHeight; if (itemTop < rootTop + 8) { root.scrollTo({ top: Math.max(0, itemTop - 8), behavior: "smooth", }); } else if (itemBottom > rootBottom - 8) { root.scrollTo({ top: itemBottom - root.clientHeight + 8, behavior: "smooth", }); } } function updateActiveToc() { const state = getTocState(); if (!state) return; const { root, offset, pairs } = state; // 找目前最接近視窗頂端 offset 的 section let active = pairs[0]; let bestDist = Infinity; for (const pair of pairs) { const rect = pair.el.getBoundingClientRect(); const dist = Math.abs(rect.top - offset - 8); // 優先選在 offset 上方或接近 offset 的 section if (rect.top - offset <= 20 && dist < bestDist) { bestDist = dist; active = pair; } } // 如果上面都沒找到,就退回第一個可見 section if (!active) { for (const pair of pairs) { const rect = pair.el.getBoundingClientRect(); if (rect.top >= offset) { active = pair; break; } } } pairs.forEach(({ item }) => item.classList.remove("toc-active")); if (active) { active.item.classList.add("toc-active"); keepItemVisible(root, active.item); } } function requestUpdate() { if (ticking) return; ticking = true; window.requestAnimationFrame(() => { updateActiveToc(); ticking = false; }); } function start() { requestUpdate(); window.addEventListener("scroll", requestUpdate, { passive: true }); window.addEventListener("resize", requestUpdate, { passive: true }); const mo = new MutationObserver(() => { requestUpdate(); }); mo.observe(document.body, { childList: true, subtree: true }); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", start); } else { start(); } })();