Files
org_web/output/assets/scripts/script.js

176 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// COPY BUTTON:
document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll("pre.src").forEach(function (block) {
const button = document.createElement("button");
button.innerText = "Copy";
button.className = "copy-btn";
// Append button inside <pre>
block.appendChild(button);
button.addEventListener("click", function () {
const text = block.innerText.replace(button.innerText, ""); // exclude button text
navigator.clipboard.writeText(text.trim()).then(() => {
button.innerText = "Copied!";
setTimeout(() => (button.innerText = "Copy"), 1500);
});
});
});
});
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll('a.footref[href^="#fn"]').forEach(ref => {
const sup = ref.closest("sup") || ref;
// idempotent: don't insert twice
if (sup.nextElementSibling && sup.nextElementSibling.classList?.contains("footnote-sidenote")) return;
const targetId = ref.getAttribute("href").replace(/^#/, ""); // works for fn.2 or fn2
const anchor = document.getElementById(targetId);
if (!anchor) return;
const footdef = anchor.closest(".footdef") || anchor.parentElement;
if (!footdef) return;
// 1) Prefer leaf paragraphs to avoid div+p duplication
let paras = footdef.querySelectorAll("p.footpara");
if (!paras.length) {
// fallback: any .footpara elements that don't contain another .footpara
paras = footdef.querySelectorAll(".footpara:not(:has(.footpara))");
}
// 2) Build HTML, de-duplicating by text content
let parts = [];
if (paras.length) {
const seen = new Set();
parts = Array.from(paras).map(p => {
const txt = p.textContent.trim().replace(/\s+/g, " ");
if (seen.has(txt)) return "";
seen.add(txt);
return p.innerHTML.trim();
}).filter(Boolean);
}
// 3) Fallback: clean full block if no paras found
if (!parts.length) {
const clone = footdef.cloneNode(true);
clone.querySelectorAll("sup.footnum, a[role='doc-backlink']").forEach(n => n.remove());
parts = [clone.innerHTML.trim()];
}
// 4) Insert the sidenote
const sn = document.createElement("span");
sn.className = "sidenote footnote-sidenote";
sn.setAttribute("data-fn", (ref.textContent || "").trim());
sn.innerHTML = parts.join(" ");
sup.insertAdjacentElement("afterend", sn);
});
});
(function(){
const root = document.documentElement;
const storageKey = "theme";
const saved = localStorage.getItem(storageKey);
if (saved === "dark" || saved === "light") {
root.setAttribute("data-theme", saved);
}
const btn = document.getElementById("theme-toggle");
if (!btn) return;
btn.addEventListener("click", () => {
const current = root.getAttribute("data-theme");
const next = current === "dark" ? "light" : "dark";
// If no current (auto), assume were toggling to dark first
const target = current ? next : "dark";
root.setAttribute("data-theme", target);
localStorage.setItem(storageKey, target);
});
})();
document.addEventListener("DOMContentLoaded", () => {
const toc = document.querySelector("#text-table-of-contents");
if (!toc) { console.warn("No #text-table-of-contents found"); return; }
const links = toc.querySelectorAll('a[href^="#"]');
if (!links.length) { console.warn("No ToC links found"); return; }
// Map: id -> link
const linkById = new Map();
links.forEach(a => {
const id = decodeURIComponent(a.getAttribute("href").slice(1));
const el = document.getElementById(id);
if (el) linkById.set(id, a);
});
if (!linkById.size) { console.warn("No matching headings with IDs"); return; }
// Headings to observe (h2h4 usually)
const headings = [...document.querySelectorAll("h2[id], h3[id], h4[id]")]
.filter(h => linkById.has(h.id));
// Helper to mark active
const setActive = (id) => {
links.forEach(a => {
const active = a.getAttribute("href") === `#${id}`;
a.classList.toggle("is-active", active);
if (active) a.setAttribute("aria-current", "true");
else a.removeAttribute("aria-current");
});
};
// Calculate sticky header offset in px
const headerOffsetPx = 6.5 * parseFloat(getComputedStyle(document.documentElement).fontSize);
// Track visible headings (id -> distance from top)
const visible = new Map();
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const id = entry.target.id;
if (entry.isIntersecting) {
// How far from the top (after header offset)
const dist = entry.target.getBoundingClientRect().top - headerOffsetPx;
visible.set(id, dist);
} else {
visible.delete(id);
}
});
if (visible.size) {
// Choose the heading closest to the top (>= -headerOffset)
const topMost = [...visible.entries()]
.sort((a,b) => Math.abs(a[1]) - Math.abs(b[1]))[0][0];
setActive(topMost);
// console.log("Active:", topMost, visible);
}
}, {
root: null, // track relative to viewport
rootMargin: `-${headerOffsetPx}px 0px -70% 0px`,
threshold: [0, 0.01, 0.1] // fire as soon as it enters
});
headings.forEach(h => observer.observe(h));
// Initial highlight (in case you load midpage)
let bestId = null, bestDist = Infinity;
headings.forEach(h => {
const top = h.getBoundingClientRect().top - headerOffsetPx;
const dist = top < 0 ? Math.abs(top) : top + 1e6;
if (dist < bestDist) { bestDist = dist; bestId = h.id; }
});
if (bestId) setActive(bestId);
// Optional: smooth-scroll ToC clicks
toc.addEventListener("click", (e) => {
const a = e.target.closest('a[href^="#"]');
if (!a) return;
const id = decodeURIComponent(a.hash.slice(1));
const el = document.getElementById(id);
if (!el) return;
e.preventDefault();
el.scrollIntoView({ behavior: "smooth", block: "start" });
el.setAttribute("tabindex", "-1");
el.focus({ preventScroll: true });
history.pushState(null, "", `#${id}`);
});
});