diff --git a/_includes/components/widgets/toc.njk b/_includes/components/widgets/toc.njk index 9470e02..de6e280 100644 --- a/_includes/components/widgets/toc.njk +++ b/_includes/components/widgets/toc.njk @@ -1,19 +1,27 @@ -{# Table of Contents Widget (for articles with headings) #} -{% if toc and toc.length %} - -
-

Contents

- +{# Table of Contents Widget — client-side Alpine.js heading scanner with scroll spy #} +{# Only renders on Articles/Notes with 3+ headings (h2-h4) #} +{% if title or (not (bookmarkOf or bookmark_of or likeOf or like_of or repostOf or repost_of)) %} +
+
+

Contents

+ +
- {% endif %} diff --git a/_includes/layouts/base.njk b/_includes/layouts/base.njk index 492a41f..6ba6999 100644 --- a/_includes/layouts/base.njk +++ b/_includes/layouts/base.njk @@ -79,6 +79,7 @@ + diff --git a/js/toc-scanner.js b/js/toc-scanner.js new file mode 100644 index 0000000..1b0f904 --- /dev/null +++ b/js/toc-scanner.js @@ -0,0 +1,46 @@ +/** + * Alpine.js TOC scanner component. + * Scans .e-content for h2/h3/h4 headings with IDs, + * builds a table of contents, and highlights the + * current section via IntersectionObserver scroll spy. + */ +document.addEventListener("alpine:init", () => { + Alpine.data("tocScanner", () => ({ + items: [], + _observer: null, + + init() { + const content = document.querySelector(".e-content"); + if (!content) return; + + const headings = content.querySelectorAll("h2[id], h3[id], h4[id]"); + if (headings.length < 3) return; + + this.items = Array.from(headings).map((h) => ({ + id: h.id, + text: h.textContent.replace(/^#\s*/, "").trim(), + level: parseInt(h.tagName[1]), + active: false, + })); + + this._observer = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + if (entry.isIntersecting) { + this.items.forEach((item) => { + item.active = item.id === entry.target.id; + }); + } + } + }, + { rootMargin: "0px 0px -70% 0px" }, + ); + + headings.forEach((h) => this._observer.observe(h)); + }, + + destroy() { + if (this._observer) this._observer.disconnect(); + }, + })); +});