website-htmx-rustelo-code/crates/pages_htmx/templates/partials/image-zoom.j2

114 lines
3.9 KiB
Text
Raw Normal View History

2026-07-10 03:44:13 +01:00
{# Global in-page lightbox. Click to zoom, full-screen modal overlay.
Handles two media kinds via event delegation:
• <picture> images → shown as <img> (browser-picked AVIF/WebP, currentSrc)
• .content-graph-mini-preview → its inline ego-network <svg>, cloned & enlarged
Boost-proof: htmx navigation can drop the overlay node, so styles + overlay are
recreated ON DEMAND (looked up by id, rebuilt if missing) rather than cached in
closure refs. Listeners bind to `document` exactly once (document survives htmx
swaps), guarded by a window flag so re-running after a swap never double-binds. #}
<script>
(function () {
var STYLE_ID = 'oz-style';
var OVERLAY_ID = 'oz-overlay';
function ensureStyle() {
if (document.getElementById(STYLE_ID)) return;
var s = document.createElement('style');
s.id = STYLE_ID;
s.textContent = ''
+ '.oz-overlay{position:fixed;inset:0;z-index:2147483000;display:none;'
+ 'align-items:center;justify-content:center;background:rgba(8,11,16,.92);'
+ 'padding:2rem;cursor:zoom-out;opacity:0;transition:opacity .15s ease}'
+ '.oz-overlay.oz-open{display:flex;opacity:1}'
+ '.oz-overlay img.oz-media{max-width:100%;max-height:100%;width:auto;height:auto;'
+ 'border-radius:.5rem;box-shadow:0 10px 40px rgba(0,0,0,.5);cursor:default}'
+ '.oz-overlay svg.oz-media{width:min(90vw,90vh);height:min(90vw,90vh);'
+ 'max-width:100%;max-height:100%;cursor:default}'
+ '.oz-close{position:absolute;top:1rem;right:1.25rem;font-size:2.25rem;'
+ 'line-height:1;color:#fff;background:none;border:0;cursor:pointer;opacity:.85}'
+ '.oz-close:hover{opacity:1}'
+ 'picture img{cursor:zoom-in}';
(document.head || document.documentElement).appendChild(s);
}
function ensureOverlay() {
var o = document.getElementById(OVERLAY_ID);
if (o) return o;
o = document.createElement('div');
o.id = OVERLAY_ID;
o.className = 'oz-overlay';
o.setAttribute('role', 'dialog');
o.setAttribute('aria-modal', 'true');
o.innerHTML = '<button class="oz-close" type="button" aria-label="Close">×</button>';
document.body.appendChild(o);
return o;
}
function setMedia(node) {
ensureStyle();
var o = ensureOverlay();
var old = o.querySelector('.oz-media');
if (old) old.remove();
node.classList.add('oz-media');
o.appendChild(node);
o.classList.add('oz-open');
document.body.style.overflow = 'hidden';
}
function openImg(src, alt) {
var im = new Image();
im.src = src;
im.alt = alt || '';
setMedia(im);
}
function openGraph(svgEl) {
var c = svgEl.cloneNode(true);
// Overlay is always dark — force the on-dark palette so labels/edges stay
// legible even when the page itself is in light mode.
c.style.setProperty('--cg-label', '#d1d5db');
c.style.setProperty('--cg-edge', '#4b5563');
c.style.setProperty('--cg-node', '#9ca3af');
setMedia(c);
}
function closeImg() {
var o = document.getElementById(OVERLAY_ID);
if (o) o.classList.remove('oz-open');
document.body.style.overflow = '';
}
if (!window.__ozBound) {
window.__ozBound = true;
document.addEventListener('click', function (e) {
var t = e.target;
if (!t || !t.closest) return;
var img = t.closest('picture img');
if (img) {
e.preventDefault();
openImg(img.currentSrc || img.src, img.alt);
return;
}
var mini = t.closest('.content-graph-mini-preview');
if (mini) {
var svg = mini.querySelector('svg');
if (svg) {
e.preventDefault();
openGraph(svg);
return;
}
}
var ov = t.closest('#' + OVERLAY_ID);
if (ov && (t.id === OVERLAY_ID || t.closest('.oz-close'))) closeImg();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' || e.key === 'Esc') closeImg();
});
}
})();
</script>