sourcehypertextpubliccosmeticssidenotes.js

// I wrote this at half past 11 at night.
// I am so, so sorry.
// Interpretation and commenting has been left as an exercise to future me.

// Converts inline notes to sidenotes for a page.
// notes: The CSS selector of the element where the notes are located.
// sidebars: The CSS selector of the sidebar(s) to place the sidenotes in.
// minWidth: The minimum width for sidenotes to be added to a page.
function laySidenotes(notes, sidebars, minWidth = 800) {
	const writingMode = getComputedStyle(document.body)["writing-mode"];
	const startEnd =
		writingMode == "vertical-rl"
			? ["right", "left"]
			: writingMode == "vertical-lr"
			? ["left", "right"]
			: ["top", "bottom"];

	const rect = el => ({
		start: el.getBoundingClientRect()[startEnd[0]],
		end: el.getBoundingClientRect()[startEnd[1]]
	});

	function lastChildBlockSize(sidebar) {
		if (sidebar.lastElementChild) {
			return rect(sidebar.lastElementChild).end;
		} else {
			return rect(sidebar).start;
		}
	}

	for (s of $$(sidebars)) {
		s.innerHTML = "";
	}

	for (s of $$(notes)) {
		s.id = s.id.replace("xx-body-", "");
	}

	if ($$(notes)?.[0]?.style?.display == "none") {
		$$(notes).forEach(note => {
			note.style.display = note.dataset?.originalDisplay || "flex";
		});
	}

	if (window.innerWidth > minWidth) {
		$$(notes).forEach((note, index) => {
			let sidenote;
			const slug = note.id.replace("sn-body-", "");

			if ($$(sidebars).length == 1) {
				$(sidebars).innerHTML += note.outerHTML;
				sidenote = $(sidebars).lastElementChild;
			} else {
				let roomiest;
				$$(sidebars).forEach(bar => {
					if (
						roomiest === undefined ||
						lastChildBlockSize(bar) < lastChildBlockSize(roomiest)
					) {
						roomiest = bar;
					}
				});
				roomiest.innerHTML += note.outerHTML;
				sidenote = roomiest.lastElementChild;
			}

			note.dataset.originalDisplay = getComputedStyle(note).display;
			note.style.display = "none";
			note.id = `xx-body-${note.id}`;

			let lastNote;
			if (sidenote.previousElementSibling) {
				lastNote = rect(sidenote.previousElementSibling).end;
			} else {
				switch (startEnd[0]) {
					case "left":
						lastNote = -1 * window.scrollX;
						break;
					case "right":
						lastNote = window.scrollX;
						break;
					default:
						lastNote = -1 * window.scrollY;
						break;
				}
			}

			let headerHeight = 0;
			if (!sidenote.previousElementSibling) {
				headerHeight =
					rect(sidenote.parentElement).start -
					rect(document.body).start;
			}

			sidenote.style.marginBlockStart =
				(
					rect($(`#sn-ref-${slug}`)).start -
					lastNote -
					headerHeight
				).toString() + "px";

			if (parseFloat(sidenote.style.marginBlockStart) < 0) {
				sidenote.style.marginBlockStart = 0;
			}

			if (rect(sidenote).end - rect(sidenote).start > 200) {
				sidenote.style.marginBlockStart =
					(
						parseFloat(sidenote.style.marginBlockStart) -
						(rect(sidenote).end - rect(sidenote).start) / 6
					).toString() + "px";

				if (parseFloat(sidenote.style.marginBlockStart) < 0) {
					sidenote.style.marginBlockStart = 0;
				}
			}
		});
	}
}

// Same parameters as laySidenotes. Used to set up the above function via listeners.
function listenForSidenotes(notes, sidebars, minWidth = 800) {
	window.on("load", () => {
		laySidenotes(notes, sidebars, minWidth);
	});
	window.on("resize", () => {
		laySidenotes(notes, sidebars, minWidth);
	});
	/* Don't forget summaries opening and closing! We install a mutation observer to keep track of that.*/
	const observer = new MutationObserver(() => {
		laySidenotes(notes, sidebars, minWidth);
	});
	$$("details").forEach(el => {
		observer.observe(el, {
			attributes: true,
			childList: true,
			subtree: true,
			characterData: true
		});
	});
}

documentReady(() => {
	if (!$("html").classList.contains("override-sidenotes")) {
		if (document.body.getAttribute("data-sidenote-width")) {
			listenForSidenotes(
				"main aside.note, div.articles aside.note",
				".sidenotes, #sidenotes",
				parseInt(document.body.getAttribute("data-sidenote-width"))
			);
		} else {
			listenForSidenotes(
				"main aside.note, div.articles aside.note",
				".sidenotes, #sidenotes",
				800
			);
		}
	}
});