// refpop.js — the reference preview card. First-party, dependency-free.
//
// One shared popover (#refpop) serves every reference on the page. The
// rules, in the order they matter:
// 1. A card is shown only when ITS content is ready: never stale, never
// an empty shell, never a spinner in a card.
// 2. Fetching starts on pointerenter/focus (hover intent), so by the
// time the open delay has passed the content is usually there. If it
// is slower the card appears late; if the pointer has left by then
// it does not appear at all.
// 3. Fetched card HTML is cached per ref for the page's lifetime.
// 4. The shared element is emptied before it is positioned, and filled
// only for the CURRENT ref: a response whose ref is no longer
// current is dropped.
// 5. Keyboard focus and Escape work.
(function () {
"use strict";
var pop = document.getElementById("refpop");
var repo = document.body.dataset.refRepo;
if (!pop || !repo) return;
var DELAY = 300, GRACE = 200;
var ANCHOR = "--qrefpop";
var cache = {}; // key -> card html
var inflight = {}; // key -> promise of card html
var cur = null; // the anchor the pointer or focus is on (the current ref)
var openFor = null; // the anchor the visible card belongs to
var openTimer = 0, hideTimer = 0;
var hovers = matchMedia("(hover: hover)");
function keyOf(a) { return repo + "|" + a.dataset.ref; }
function load(a) {
var k = keyOf(a);
if (cache[k] !== undefined) return Promise.resolve(cache[k]);
if (inflight[k]) return inflight[k];
var u = "/r/" + encodeURIComponent(repo) + "/ref.frag?fmt=html&ref=" + encodeURIComponent(a.dataset.ref);
inflight[k] = fetch(u, { credentials: "same-origin" }).then(function (r) {
if (!r.ok) throw new Error(String(r.status));
return r.text();
}).then(function (html) {
delete inflight[k];
cache[k] = html;
return html;
}, function (e) { delete inflight[k]; throw e; });
return inflight[k];
}
function clear() {
if (openFor) openFor.style.anchorName = "";
openFor = null;
try { if (pop.matches(":popover-open")) pop.hidePopover(); } catch (e) {}
pop.replaceChildren();
}
function show(a, html) {
clear(); // never leave the previous card's content in place
pop.innerHTML = html;
openFor = a;
if (CSS.supports("anchor-name", ANCHOR)) {
a.style.anchorName = ANCHOR;
} else {
var b = a.getBoundingClientRect();
pop.style.left = Math.max(4, Math.min(b.left, innerWidth - 340)) + "px";
pop.style.top = (b.bottom + 6) + "px";
}
try { pop.showPopover(); } catch (e) { clear(); }
}
// intent: the pointer or focus is on a. Fetch now, show after the delay
// if a is still the current ref and its content has arrived.
function intent(a, delay) {
if (cur === a) return;
cur = a;
clearTimeout(openTimer);
clearTimeout(hideTimer);
if (openFor && openFor !== a) clear(); // a different ref: drop the old card at once
var p = load(a);
openTimer = setTimeout(function () {
p.then(function (html) {
if (cur !== a) return; // left, or moved to another ref: out of date
show(a, html);
}, function () {});
}, delay);
}
function leave() {
cur = null;
clearTimeout(openTimer);
clearTimeout(hideTimer);
hideTimer = setTimeout(clear, GRACE); // time to move into the card itself
}
function anchorOf(t) {
return t && t.closest ? t.closest("a[data-ref]") : null;
}
document.addEventListener("mouseover", function (ev) {
if (!hovers.matches) return;
var a = anchorOf(ev.target);
if (a) intent(a, DELAY);
});
document.addEventListener("mouseout", function (ev) {
if (!hovers.matches) return;
var a = anchorOf(ev.target);
if (!a || (ev.relatedTarget && a.contains(ev.relatedTarget))) return;
if (ev.relatedTarget && pop.contains(ev.relatedTarget)) return;
leave();
});
document.addEventListener("focusin", function (ev) {
var a = anchorOf(ev.target);
if (a) intent(a, 0);
});
document.addEventListener("focusout", function (ev) {
if (anchorOf(ev.target)) leave();
});
// The card itself keeps itself open while the pointer is on it.
pop.addEventListener("mouseover", function () { clearTimeout(hideTimer); });
pop.addEventListener("mouseout", function (ev) {
if (!ev.relatedTarget || !pop.contains(ev.relatedTarget)) leave();
});
// Where hovering is not possible a tap opens the card instead of the link.
document.addEventListener("click", function (ev) {
if (hovers.matches) return;
var a = anchorOf(ev.target);
if (!a) return;
ev.preventDefault();
intent(a, 0);
});
document.addEventListener("keydown", function (ev) {
if (ev.key !== "Escape") return;
cur = null;
clearTimeout(openTimer);
clear();
});
// The native popover also closes on light dismiss; keep our state honest.
pop.addEventListener("toggle", function (ev) {
if (ev.newState === "closed") {
if (openFor) openFor.style.anchorName = "";
openFor = null;
}
});
})();