arborist/docs/_source/_static/zoom.js
russell@unturf.com 2a37c6ec51
docs: click-to-zoom lightbox for diagrams (scroll-zoom + drag-pan)
Graphviz renders SVG as <object>, which swallows clicks and can't be
lightboxed. Vendored, dependency-free zoom.js converts each graphviz
<object> to a clickable <img> (crisp vector) and adds a fullscreen
overlay: click to open, scroll to zoom toward the cursor, drag to pan,
Esc / dbl-click / background-click to close. Works for any img.zoomable,
so future charts get it free. Degrades gracefully (no JS -> inline image
still renders). Wired via html_css_files / html_js_files.
2026-05-21 15:26:39 -04:00

112 lines
3.5 KiB
JavaScript

/* Click-to-zoom lightbox for docs diagrams. Dependency-free.
*
* Sphinx renders graphviz SVG as <object>, which swallows clicks and
* can't be lightboxed. We replace each with an <img> (crisp vector,
* clickable), then a click opens a fullscreen overlay with
* scroll-to-zoom + drag-to-pan so dense diagrams can be read in detail.
* Esc / double-click / background-click closes. Degrades gracefully:
* if JS is off, the inline image still renders. */
(function () {
function ready(fn) {
if (document.readyState !== "loading") fn();
else document.addEventListener("DOMContentLoaded", fn);
}
ready(function () {
// 1. graphviz <object> SVG -> clickable <img>
document
.querySelectorAll('.graphviz object[type="image/svg+xml"]')
.forEach(function (obj) {
var img = document.createElement("img");
img.src = obj.getAttribute("data");
img.className = "zoomable";
img.alt = "diagram — click to zoom";
obj.parentNode.replaceChild(img, obj);
});
// 2. overlay scaffold
var ov = document.createElement("div");
ov.id = "arb-zoom-overlay";
var big = document.createElement("img");
ov.appendChild(big);
var hint = document.createElement("div");
hint.id = "arb-zoom-hint";
hint.textContent = "scroll = zoom · drag = pan · Esc / dbl-click = close";
document.body.appendChild(ov);
document.body.appendChild(hint);
var scale = 1, tx = 0, ty = 0;
var dragging = false, moved = false, sx = 0, sy = 0;
function apply() {
big.style.transform =
"translate(" + tx + "px," + ty + "px) scale(" + scale + ")";
}
function open(src) {
ov.classList.add("open");
big.onload = function () {
var s =
Math.min(
window.innerWidth / big.naturalWidth,
window.innerHeight / big.naturalHeight
) * 0.95;
scale = s > 0 ? s : 1;
tx = (window.innerWidth - big.naturalWidth * scale) / 2;
ty = (window.innerHeight - big.naturalHeight * scale) / 2;
apply();
};
big.src = src;
}
function close() {
ov.classList.remove("open");
big.src = "";
}
document.body.addEventListener("click", function (e) {
var t = e.target;
if (t && t.classList && t.classList.contains("zoomable")) open(t.src);
});
ov.addEventListener(
"wheel",
function (e) {
e.preventDefault();
var f = e.deltaY < 0 ? 1.15 : 1 / 1.15;
// zoom toward the cursor
tx = e.clientX - (e.clientX - tx) * f;
ty = e.clientY - (e.clientY - ty) * f;
scale *= f;
apply();
},
{ passive: false }
);
ov.addEventListener("mousedown", function (e) {
dragging = true;
moved = false;
sx = e.clientX - tx;
sy = e.clientY - ty;
ov.classList.add("grabbing");
});
window.addEventListener("mousemove", function (e) {
if (!dragging) return;
moved = true;
tx = e.clientX - sx;
ty = e.clientY - sy;
apply();
});
window.addEventListener("mouseup", function () {
dragging = false;
ov.classList.remove("grabbing");
});
// background click (no drag) closes; dbl-click + Esc close
ov.addEventListener("click", function (e) {
if (e.target === ov && !moved) close();
});
ov.addEventListener("dblclick", close);
document.addEventListener("keydown", function (e) {
if (e.key === "Escape") close();
});
});
})();