extroonie.com/assets/js/footnotes.js
Extroonie 4a6f6b2062
Some checks are pending
Check site / build (push) Waiting to run
feat: initial commit
2026-09-19 08:37:03 -07:00

81 lines
2.9 KiB
JavaScript

(() => {
const references = document.querySelectorAll('.footnote-ref');
if (!references.length) return;
const preview = document.createElement('span');
preview.id = 'footnote-preview';
preview.className = 'footnote-preview';
preview.setAttribute('role', 'note');
preview.hidden = true;
document.body.append(preview);
let active;
let timer;
function close() {
clearTimeout(timer);
if (active && preview.contains(document.activeElement)) active.focus({ preventScroll: true });
active?.removeAttribute('aria-controls');
active = null;
preview.hidden = true;
}
function scheduleClose() {
clearTimeout(timer);
timer = setTimeout(() => {
if (!preview.contains(document.activeElement) && document.activeElement !== active) close();
}, 180);
}
function show(reference) {
if (active === reference) {
clearTimeout(timer);
return;
}
close();
const note = document.getElementById(decodeURIComponent(reference.hash.slice(1)));
if (!note) return;
const content = note.cloneNode(true);
content.querySelectorAll('.footnote-backref').forEach((link) => link.remove());
content.querySelectorAll('[id]').forEach((element) => element.removeAttribute('id'));
preview.replaceChildren(...content.childNodes);
active = reference;
reference.setAttribute('aria-controls', preview.id);
preview.setAttribute('aria-label', `Footnote ${reference.textContent}`);
reference.closest('sup').after(preview);
preview.hidden = false;
const rect = reference.getBoundingClientRect();
const gap = 8;
const inset = 16;
const left = Math.max(inset, Math.min(rect.left, innerWidth - preview.offsetWidth - inset));
const below = rect.bottom + gap;
const top = below + preview.offsetHeight <= innerHeight - inset ? below : rect.top - preview.offsetHeight - gap;
preview.style.left = `${left}px`;
preview.style.top = `${Math.max(inset, top)}px`;
}
for (const reference of references) {
reference.closest('sup').tabIndex = -1;
const note = document.getElementById(decodeURIComponent(reference.hash.slice(1)));
if (note) note.tabIndex = -1;
reference.addEventListener('pointerenter', (event) => {
if (event.pointerType === 'mouse') show(reference);
});
reference.addEventListener('pointerleave', scheduleClose);
reference.addEventListener('focus', () => {
if (reference.matches(':focus-visible')) show(reference);
});
reference.addEventListener('blur', scheduleClose);
reference.addEventListener('click', close);
}
preview.addEventListener('pointerenter', () => clearTimeout(timer));
preview.addEventListener('pointerleave', scheduleClose);
preview.addEventListener('focusin', () => clearTimeout(timer));
preview.addEventListener('focusout', scheduleClose);
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') close();
});
window.addEventListener('resize', close);
window.addEventListener(
'scroll',
() => {
if (!preview.contains(document.activeElement)) close();
},
{ passive: true },
);
})();