Spaces:
Sleeping
Sleeping
| // 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(); | |
| } | |
| })(); |