chore: sync upstream — add toc-scanner.js and data-fetch.js
New files from rmdes/indiekit-eleventy-theme: - js/toc-scanner.js: Alpine.js TOC scanner with IntersectionObserver scroll spy - lib/data-fetch.js: shared fetch helper with 10s timeout and watch-mode cache extension Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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) { this._hideWrapper(); return; }
|
||||
|
||||
const headings = content.querySelectorAll("h2[id], h3[id], h4[id]");
|
||||
if (headings.length < 3) { this._hideWrapper(); 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));
|
||||
},
|
||||
|
||||
_hideWrapper() {
|
||||
const wrapper = this.$root.closest(".widget-collapsible");
|
||||
if (wrapper) wrapper.style.display = "none";
|
||||
},
|
||||
|
||||
destroy() {
|
||||
if (this._observer) this._observer.disconnect();
|
||||
},
|
||||
}));
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Shared data-fetching helper for _data files.
|
||||
*
|
||||
* Wraps @11ty/eleventy-fetch with two protections:
|
||||
* 1. Hard timeout — 10-second AbortController ceiling on every request
|
||||
* 2. Watch-mode cache extension — uses "4h" TTL during watch/serve,
|
||||
* keeping the original (shorter) TTL only for production builds
|
||||
*
|
||||
* Usage:
|
||||
* import { cachedFetch } from "../lib/data-fetch.js";
|
||||
* const data = await cachedFetch(url, { duration: "15m", type: "json" });
|
||||
*/
|
||||
|
||||
import EleventyFetch from "@11ty/eleventy-fetch";
|
||||
|
||||
const FETCH_TIMEOUT_MS = 10_000; // 10 seconds
|
||||
|
||||
// In watch/serve mode, extend cache to avoid re-fetching on every rebuild.
|
||||
// Production builds use the caller's original TTL for fresh data.
|
||||
const isWatchMode = process.env.ELEVENTY_RUN_MODE !== "build";
|
||||
const WATCH_MODE_DURATION = "4h";
|
||||
|
||||
/**
|
||||
* Fetch with timeout and watch-mode cache extension.
|
||||
*
|
||||
* @param {string} url - URL to fetch
|
||||
* @param {object} options - EleventyFetch options (duration, type, fetchOptions, etc.)
|
||||
* @returns {Promise<any>} Parsed response
|
||||
*/
|
||||
export async function cachedFetch(url, options = {}) {
|
||||
// Extend cache in watch mode
|
||||
const duration = isWatchMode ? WATCH_MODE_DURATION : (options.duration || "15m");
|
||||
|
||||
// Create abort controller for hard timeout
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const fetchOptions = {
|
||||
...options.fetchOptions,
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
const result = await EleventyFetch(url, {
|
||||
...options,
|
||||
duration,
|
||||
fetchOptions,
|
||||
});
|
||||
|
||||
return result;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user