Skip to content
quarry Artifact [9b266d8f61]
Login

Artifact [9b266d8f61]

Artifact 9b266d8f614d9538ff347054e1e04d07d5e31562a25bc8ff2454c7c3cf82ac5e:


// 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;
    }
  });
})();