summaryrefslogtreecommitdiff
path: root/public/footnotes.js
diff options
context:
space:
mode:
Diffstat (limited to 'public/footnotes.js')
-rw-r--r--public/footnotes.js99
1 files changed, 99 insertions, 0 deletions
diff --git a/public/footnotes.js b/public/footnotes.js
new file mode 100644
index 0000000..57ded98
--- /dev/null
+++ b/public/footnotes.js
@@ -0,0 +1,99 @@
+document.documentElement.classList.add("js");
+
+const SIDENOTES = {
+ inside: true, // true: notes inside the frame, right of the post. false: outside, in the margin
+ insideShare: 0.21, // share of the post area given to the notes when inside (0.3 = 70/30)
+ border: false, // border around each note
+ // readingWidth: "70ch", // max width of the post text, null = no cap
+ readingWidth: null, // max width of the post text, null = no cap
+};
+
+document.addEventListener("DOMContentLoaded", () => {
+ const area = document.querySelector("[data-scroll-area]");
+ const article = area?.querySelector("article");
+ if (!area || !article) return;
+
+ article.style.maxWidth = SIDENOTES.readingWidth ?? "none";
+ if (SIDENOTES.inside)
+ article.style.marginRight = `${SIDENOTES.insideShare * 100}%`;
+
+ const refs = [...document.querySelectorAll("a.footnote-ref")];
+ if (refs.length === 0) return;
+
+ const panel = document.createElement("div");
+ panel.className = "sidenotes" + (SIDENOTES.border ? " bordered" : "");
+ document.body.append(panel);
+
+ const items = refs.map((ref) => {
+ ref.addEventListener("click", (e) => e.preventDefault());
+
+ const note = document.getElementById(ref.getAttribute("href").slice(1));
+ const body = document.createElement("div");
+ body.append(...note.cloneNode(true).childNodes);
+ body.querySelector(".footnote-backref")?.remove();
+
+ const num = document.createElement("sup");
+ num.className = "sidenote-num";
+ num.textContent = ref.textContent;
+ (body.querySelector("p") ?? body).prepend(num);
+
+ const el = document.createElement("div");
+ el.className = "sidenote";
+ el.append(body);
+ panel.append(el);
+ return { ref, el };
+ });
+
+ const gap = 6;
+ let queued = false;
+
+ const layout = () => {
+ queued = false;
+ const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
+ const top0 = area.getBoundingClientRect().top + area.clientTop;
+
+ let left, right;
+ if (SIDENOTES.inside) {
+ const r = area.getBoundingClientRect();
+ left = article.getBoundingClientRect().right + rem;
+ right =
+ r.left +
+ area.clientLeft +
+ area.clientWidth -
+ parseFloat(getComputedStyle(area).paddingRight);
+ } else {
+ left = area.closest("main").getBoundingClientRect().right + rem;
+ right = window.innerWidth - rem;
+ }
+
+ Object.assign(panel.style, {
+ left: `${left}px`,
+ top: `${top0}px`,
+ width: `${Math.max(0, right - left)}px`,
+ height: `${area.clientHeight}px`,
+ });
+
+ let bottom = -Infinity;
+ for (const { ref, el } of items) {
+ const top = Math.max(
+ ref.getBoundingClientRect().top - top0,
+ bottom + gap,
+ );
+ el.style.top = `${top}px`;
+ bottom = top + el.offsetHeight;
+ }
+ };
+
+ const schedule = () => {
+ if (!queued) {
+ queued = true;
+ requestAnimationFrame(layout);
+ }
+ };
+
+ area.addEventListener("scroll", schedule, { passive: true });
+ window.addEventListener("resize", schedule);
+ new ResizeObserver(schedule).observe(area);
+ document.fonts?.ready.then(schedule);
+ layout();
+});