124 lines
4.7 KiB
JavaScript
124 lines
4.7 KiB
JavaScript
/* sidenotes.js — Collision avoidance and hover linking for sidenotes.
|
|
*
|
|
* HTML structure produced by Filters/Sidenotes.hs:
|
|
* <sup class="sidenote-ref" id="snref-N"><a href="#sn-N">N</a></sup>
|
|
* <span class="sidenote" id="sn-N">…</span>
|
|
*
|
|
* #markdownBody must be position: relative (layout.css guarantees this).
|
|
* The .sidenote spans are position: absolute; left: calc(100% + 2.5rem).
|
|
* Without an explicit top they sit at their "hypothetical static position",
|
|
* which is fine for isolated notes but causes overlaps when notes are close.
|
|
*
|
|
* This script:
|
|
* 1. Anchors each sidenote's top to its reference's offsetTop so the
|
|
* alignment is explicit and stable across all browsers.
|
|
* 2. Walks notes top-to-bottom and pushes each one below the previous
|
|
* if they would overlap (with a small gap between them).
|
|
* 3. Wires bidirectional hover highlights between each ref↔sidenote pair.
|
|
*/
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
const GAP = 12; /* minimum px gap between successive sidenotes */
|
|
|
|
function positionSidenotes() {
|
|
const body = document.getElementById('markdownBody');
|
|
if (!body) return;
|
|
|
|
const notes = Array.from(body.querySelectorAll('.sidenote'));
|
|
if (notes.length === 0) return;
|
|
|
|
/* Only run on wide viewports where sidenotes are visible. */
|
|
if (getComputedStyle(notes[0]).display === 'none') return;
|
|
|
|
let prevBottom = 0;
|
|
|
|
notes.forEach(function (sn) {
|
|
/* Reset any prior JS-applied top so offsetTop reads correctly. */
|
|
sn.style.top = '';
|
|
|
|
const id = sn.id; /* "sn-N" */
|
|
const refId = 'snref-' + id.slice(3); /* "snref-N" */
|
|
const ref = document.getElementById(refId);
|
|
|
|
/* Preferred top: align with the reference superscript. */
|
|
const preferred = ref ? ref.offsetTop : sn.offsetTop;
|
|
const top = Math.max(preferred, prevBottom + GAP);
|
|
|
|
sn.style.top = top + 'px';
|
|
prevBottom = top + sn.offsetHeight;
|
|
});
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Hover + click wiring */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/* At most one sidenote is "focused" (click-sticky) at a time. */
|
|
let focusedPair = null;
|
|
|
|
function wireHover(ref, sn) {
|
|
let hovering = false;
|
|
let focused = false;
|
|
|
|
function update() {
|
|
const active = focused || hovering;
|
|
ref.classList.toggle('is-active', active);
|
|
sn.classList.toggle('is-active', active);
|
|
}
|
|
|
|
function unfocus() { focused = false; update(); }
|
|
|
|
ref.addEventListener('mouseenter', function () { hovering = true; update(); });
|
|
ref.addEventListener('mouseleave', function () { hovering = false; update(); });
|
|
sn.addEventListener('mouseenter', function () { hovering = true; update(); });
|
|
sn.addEventListener('mouseleave', function () { hovering = false; update(); });
|
|
|
|
/* Click on the superscript link: sticky focus on wide viewports,
|
|
normal anchor scroll on narrow viewports (sidenote hidden). */
|
|
const link = ref.querySelector('a');
|
|
if (link) {
|
|
link.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
if (getComputedStyle(sn).display === 'none') return; /* narrow: no sidenote to focus */
|
|
if (focused) {
|
|
focused = false;
|
|
focusedPair = null;
|
|
} else {
|
|
if (focusedPair) focusedPair.unfocus();
|
|
focused = true;
|
|
focusedPair = { ref: ref, sn: sn, unfocus: unfocus };
|
|
}
|
|
update();
|
|
});
|
|
}
|
|
}
|
|
|
|
/* Click anywhere outside a focused pair dismisses it. */
|
|
document.addEventListener('click', function (e) {
|
|
if (focusedPair &&
|
|
!focusedPair.ref.contains(e.target) &&
|
|
!focusedPair.sn.contains(e.target)) {
|
|
focusedPair.unfocus();
|
|
focusedPair = null;
|
|
}
|
|
});
|
|
|
|
function init() {
|
|
const body = document.getElementById('markdownBody');
|
|
if (!body) return;
|
|
|
|
body.querySelectorAll('.sidenote').forEach(function (sn) {
|
|
const refId = 'snref-' + sn.id.slice(3);
|
|
const ref = document.getElementById(refId);
|
|
if (ref) wireHover(ref, sn);
|
|
});
|
|
|
|
positionSidenotes();
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
window.addEventListener('resize', positionSidenotes);
|
|
}());
|