124 lines
4.7 KiB
JavaScript
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);
|
||
|
}
|
||
|
});
|
||
|
}
|