summaryrefslogtreecommitdiff
path: root/public/footnotes.js
blob: 57ded98eeed03cb94159ff3e507eb69079c73ac2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
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();
});