diff options
Diffstat (limited to 'public/footnotes.js')
| -rw-r--r-- | public/footnotes.js | 99 |
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(); +}); |
