cedaei.com/themes/paperesque/assets/js/floatingFootnotes.js

124 lines
4.7 KiB
JavaScript

import { docReady, onWindowResize } from "./utils.js";
import { ResizeObserver } from '@juggle/resize-observer';
const ARTICLE_CONTENT_SELECTOR = "article#main";
const FOOTNOTE_SECTION_SELECTOR = "section.footnotes[role=doc-endnotes]";
const FLOATING_FOOTNOTE_MIN_WIDTH = 1260;
// Computes an offset such that setting `top` on elemToAlign will put it
// in vertical alignment with targetAlignment.
function computeOffsetForAlignment(elemToAlign, targetAlignment) {
const offsetParentTop = elemToAlign.offsetParent.getBoundingClientRect().top;
// Distance between the top of the offset parent and the top of the target alignment
return targetAlignment.getBoundingClientRect().top - offsetParentTop;
}
function setFootnoteOffsets(footnotes) {
// Keep track of the bottom of the last element, because we don't want to
// overlap footnotes.
let bottomOfLastElem = 0;
Array.prototype.forEach.call(footnotes, function (footnote, i) {
// In theory, don't need to escape this because IDs can't contain
// quotes, in practice, not sure. ¯\_(ツ)_/¯
// Get the thing that refers to the footnote
const intextLink = document.querySelector("a.footnote-ref[href='#" + footnote.id + "']");
// Find its "content parent"; nearest paragraph or list item or
// whatever. We use this for alignment because it looks much cleaner.
// If it doesn't, your paragraphs are too long :P
// Fallback - use the same height as the link.
const verticalAlignmentTarget = intextLink.closest('p,li') || intextLink;
let offset = computeOffsetForAlignment(footnote, verticalAlignmentTarget);
if (offset < bottomOfLastElem) {
offset = bottomOfLastElem;
}
// computedStyle values are always in pixels, but have the suffix 'px'.
// offsetHeight doesn't include margins, but we want it to use them so
// we retain the style / visual fidelity when all the footnotes are
// crammed together.
bottomOfLastElem =
offset +
footnote.offsetHeight +
parseInt(window.getComputedStyle(footnote).marginBottom) +
parseInt(window.getComputedStyle(footnote).marginTop);
footnote.style.top = offset + 'px';
footnote.style.position = 'absolute';
});
}
function clearFootnoteOffsets(footnotes) {
// Reset all
Array.prototype.forEach.call(footnotes, function (fn, i) {
fn.style.top = null;
fn.style.position = null;
});
}
// contract: this is idempotent; i.e. it won't wreck anything if you call it
// with the same value over and over again. Though maybe it'll wreck performance
// lol.
function updateFootnoteFloat(shouldFloat) {
const footnoteSection = document.querySelector(FOOTNOTE_SECTION_SELECTOR);
const footnotes = footnoteSection.querySelectorAll(
"li[role=doc-endnote]");
if (shouldFloat) {
// Do this first because we need styles applied before doing other
// calculations
footnoteSection.classList.add('floating-footnotes');
setFootnoteOffsets(footnotes);
subscribeToUpdates();
} else {
unsubscribeFromUpdates();
clearFootnoteOffsets(footnotes);
footnoteSection.classList.remove('floating-footnotes');
}
}
function subscribeToUpdates() {
const article = document.querySelector(ARTICLE_CONTENT_SELECTOR);
// Watch for dimension changes on the thing that holds all the footnotes so
// we can reposition as required
resizeObserver.observe(article);
}
function unsubscribeFromUpdates() {
resizeObserver.disconnect();
}
const notifySizeChange = function() {
// Default state, not expanded.
let bigEnough = false;
return function () {
// Pixel width at which this looks good
let nowBigEnough = window.innerWidth >= FLOATING_FOOTNOTE_MIN_WIDTH;
if (nowBigEnough !== bigEnough) {
updateFootnoteFloat(nowBigEnough);
bigEnough = nowBigEnough;
}
};
}();
const resizeObserver = new ResizeObserver((_entries, observer) => {
// By virtue of the fact that we're subscribed, we know this is true.
updateFootnoteFloat(true);
});
export default function enableFloatingFootnotes() {
docReady(() => {
const footnoteSection = document.querySelector(FOOTNOTE_SECTION_SELECTOR);
const article = document.querySelector(ARTICLE_CONTENT_SELECTOR);
const allowFloatingFootnotes = article && !article.classList.contains('no-floating-footnotes');
// only set it all up if there's actually a footnote section and
// we haven't explicitly disabled floating footnotes.
if (footnoteSection && allowFloatingFootnotes) {
onWindowResize(notifySizeChange);
}
});
}