Dash-math_Database / assets /toc_scrollspy.js
Mochi0622's picture
initial deploy
e66cfb4
Raw
History Blame Contribute Delete
3 kB
// 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();
}
})();