Spaces:
Running on Zero
Running on Zero
File size: 5,262 Bytes
619344d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | /* viewer.js
=========
Light-box / zoom / keyboard logic for the result grid.
Expects:
• the page to contain thumbnails inside elements with class="item"
and data-idx attributes that match the order of
window.GRID_DATA.sources & window.GRID_DATA.captions
• the CSS rules from viewer.css (same selectors as before)
*/
const INITIAL_SCALE_WHEN_FITS = 1.5;
/* ----------------------------------------------------------
1. Data from Python-generated <script> snippet
---------------------------------------------------------- */
const {
sources,
captions
} = window.GRID_DATA;
/* ----------------------------------------------------------
2. Build / cache overlay DOM elements
---------------------------------------------------------- */
const overlay = document.createElement('div');
overlay.id = 'overlay';
overlay.innerHTML = `
<div id="container"></div>
<div id="caption"></div>
`;
document.body.appendChild(overlay);
const container = overlay.querySelector('#container');
const captionEl = overlay.querySelector('#caption');
/* ----------------------------------------------------------
3. Runtime state
---------------------------------------------------------- */
let idx = 0;
let isZoomed = false;
let canZoom = false; // false for videos
let media = null; // <img> | <video>
let baseWidth = 0; // natural width for wheel-zoom
let scale = 1;
/* ----------------------------------------------------------
4. Helpers
---------------------------------------------------------- */
const mediaTag = src =>
src.toLowerCase().endsWith('.mp4') ?
`<video src="${src}" controls autoplay></video>` :
`<img src="${src}" alt="preview">`;
/* Center or pin content when zoomed */
function adjustAlignment() {
if (!isZoomed) {
container.style.alignSelf = '';
overlay.classList.remove('scrollY');
return;
}
const hOverflow = overlay.scrollWidth > overlay.clientWidth;
const vOverflow = overlay.scrollHeight > overlay.clientHeight;
container.style.alignSelf = hOverflow ? 'flex-start' : '';
overlay.classList.toggle('scrollY', vOverflow);
}
/* ----------------------------------------------------------
5. Open / close / zoom
---------------------------------------------------------- */
function show(i) {
idx = (i + sources.length) % sources.length;
container.innerHTML = mediaTag(sources[idx]);
captionEl.textContent = captions[idx];
overlay.style.display = 'flex';
document.body.style.overflow = 'hidden'; // lock background scroll
media = container.firstElementChild;
canZoom = media.tagName === 'IMG';
isZoomed = false;
scale = 1;
media.style.width = '';
media.classList.remove('zoomed');
overlay.classList.remove('scrollY');
container.style.alignSelf = '';
media.onclick = canZoom ? toggleZoom : null;
adjustAlignment();
}
function hide() {
overlay.style.display = 'none'; // make it invisible
document.body.style.overflow = '';
isZoomed = false;
/* container will be overwritten on next show() call */
}
function toggleZoom() {
if (!canZoom) return; // ignore for videos
isZoomed = !isZoomed;
media.classList.toggle('zoomed', isZoomed);
if (isZoomed) {
baseWidth = media.naturalWidth;
media.style.width = (baseWidth * INITIAL_SCALE_WHEN_FITS) + 'px';
overlay.scrollTop = overlay.scrollLeft = 0;
} else {
media.style.width = '';
}
adjustAlignment();
}
/* ----------------------------------------------------------
6. Event wiring
---------------------------------------------------------- */
/* Thumbnail clicks – activate only when the media itself is clicked */
document.querySelector('.grid').addEventListener('click', e => {
if (!e.target.matches('img, video')) return; // ignore textarea, padding, …
const idx = +e.target.closest('.item').dataset.idx; // ascend to the card
show(idx);
});
/* Keyboard */
document.addEventListener('keydown', e => {
if (overlay.style.display !== 'flex') return;
/* Esc — exit zoom first, then close overlay */
if (e.key === 'Escape') {
e.preventDefault(); // ← stop “Stop loading” in Chrome/Firefox
if (isZoomed) toggleZoom();
else hide();
return;
}
/* Enter toggles zoom for images */
if (e.key === 'Enter' && canZoom) {
toggleZoom();
return;
}
/* Arrow navigation disabled while zoomed */
if (isZoomed) return;
if (e.key === 'ArrowRight') show(idx + 1);
else if (e.key === 'ArrowLeft') show(idx - 1);
});
/* Click backdrop to close */
overlay.addEventListener('click', e => {
if (e.target === overlay) hide();
});
/* Ctrl + wheel zoom for images only */
overlay.addEventListener('wheel', e => {
if (!canZoom || !e.ctrlKey) return;
if (!isZoomed) toggleZoom(); // auto-enter zoom
e.preventDefault();
scale += (e.deltaY < 0 ? 0.1 : -0.1);
scale = Math.max(0.2, Math.min(5, scale));
media.style.width = (baseWidth * scale) + 'px';
adjustAlignment();
}, {
passive: false
});
/* Keep centering correct on resize / browser zoom */
window.addEventListener('resize', adjustAlignment);
|