Files
tsuki/assets/js/toc-active.js
T
tiennm99 e19bb67c88 feat: visual layer with vi typography, dark mode, view transitions (Phase 6)
- 9 CSS files (tokens, reset, typography, layout, components, home,
  archive, toc, view-transitions) bundled via resources.Concat
- 3 ES module JS files (theme-toggle, code-copy, toc-active); toc-active
  loaded only on long posts
- @view-transition: navigation auto for cross-page morph (CSS-only)
- TOC partial gated by WordCount>400 and Params.toc!=false; sticky on
  wide viewports, framed block on narrow
- render-heading.html adds cosmetic anchor links
- Hugo asset pipeline: resources.Concat | minify | fingerprint (no
  SCSS, no PostCSS, no Node)
- i18n/vi.yml extended with prev/next/posts/archiveEmpty/toggleTheme
- Bundle sizes: CSS 3.3 KB gz (budget 15), JS 0.8 KB gz (budget 8)

Config: github-ascii heading IDs, :contentbasename permalink token for
clean ASCII URLs, pagination.pagerSize migration (Hugo 0.128+).
2026-05-07 21:27:46 +07:00

33 lines
1.1 KiB
JavaScript

const toc = document.querySelector(".toc");
if (toc) {
const links = new Map();
for (const a of toc.querySelectorAll('a[href^="#"]')) {
const id = decodeURIComponent(a.getAttribute("href").slice(1));
if (id) links.set(id, a);
}
const headings = document.querySelectorAll(".post-content :is(h2, h3, h4)[id]");
if (headings.length && links.size) {
const visible = new Set();
const setActive = (id) => {
for (const a of toc.querySelectorAll('a[aria-current="true"]')) a.removeAttribute("aria-current");
const link = links.get(id);
if (link) link.setAttribute("aria-current", "true");
};
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) visible.add(entry.target.id);
else visible.delete(entry.target.id);
}
if (visible.size) {
for (const h of headings) {
if (visible.has(h.id)) { setActive(h.id); break; }
}
}
}, { rootMargin: "-15% 0px -70% 0px", threshold: 0 });
for (const h of headings) observer.observe(h);
}
}