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.
112 lines
3.5 KiB
JavaScript
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();
|
|
});
|
|
});
|
|
})();
|