1/* viewer.js adapter for the AsymFlow project page. 2 The light-box / zoom logic below is copied from lakonlab/ui/media_viewer/viewer.js. 3 Only the thumbnail source discovery and click delegation are adapted for this page. 4*/ 5 6const lightboxItems = Array.from(document.querySelectorAll('.teaser-card, .figure')) 7 .map((item, idx) => { 8 const media = item.querySelector('img'); 9 const caption = item.querySelector('.caption'); 10 if (!media) return null; 11 item.classList.add('item'); 12 item.dataset.idx = idx; 13 return { 14 source: media.currentSrc || media.src, 15 caption: caption ? caption.textContent.trim() : media.alt, 16 }; 17 }) 18 .filter(Boolean); 19 20const viewerData = { 21 sources: lightboxItems.map(item => item.source), 22 captions: lightboxItems.map(item => item.caption), 23}; 24 25const INITIAL_SCALE_WHEN_FITS = 1.5; 26 27const { 28 sources, 29 captions 30} = viewerData; 31 32const overlay = document.createElement('div'); 33overlay.id = 'overlay'; 34overlay.innerHTML = ` 35 <div id="container"></div> 36 <div id="caption"></div> 37`; 38document.body.appendChild(overlay); 39 40const container = overlay.querySelector('#container'); 41const captionEl = overlay.querySelector('#caption'); 42 43let idx = 0; 44let isZoomed = false; 45let canZoom = false; // false for videos 46let media = null; // <img> | <video> 47let baseWidth = 0; // natural width for wheel-zoom 48let scale = 1; 49let zoomModifierDown = false; 50 51const mediaTag = src => 52 src.toLowerCase().endsWith('.mp4') ? 53 `<video src="${src}" controls autoplay></video>` : 54 `<img src="${src}" alt="preview">`; 55 56/* Center or pin content when zoomed */ 57function adjustAlignment() { 58 if (!isZoomed) { 59 container.style.alignSelf = ''; 60 overlay.classList.remove('scrollY'); 61 return; 62 } 63 const hOverflow = overlay.scrollWidth > overlay.clientWidth; 64 const vOverflow = overlay.scrollHeight > overlay.clientHeight; 65 66 container.style.alignSelf = hOverflow ? 'flex-start' : ''; 67 overlay.classList.toggle('scrollY', vOverflow); 68} 69 70function show(i) { 71 idx = (i + sources.length) % sources.length; 72 73 container.innerHTML = mediaTag(sources[idx]); 74 captionEl.textContent = captions[idx]; 75 if (window.MathJax && window.MathJax.typesetPromise) { 76 window.MathJax.typesetPromise([captionEl]); 77 } 78 overlay.style.display = 'flex'; 79 document.body.style.overflow = 'hidden'; // lock background scroll 80 81 media = container.firstElementChild; 82 canZoom = media.tagName === 'IMG'; 83 isZoomed = false; 84 scale = 1; 85 86 media.style.width = ''; 87 media.classList.remove('zoomed'); 88 overlay.classList.remove('scrollY'); 89 container.style.alignSelf = ''; 90 91 media.onclick = canZoom ? toggleZoom : null; 92 adjustAlignment(); 93} 94 95function hide() { 96 overlay.style.display = 'none'; // make it invisible 97 document.body.style.overflow = ''; 98 isZoomed = false; 99 /* container will be overwritten on next show() call */ 100} 101 102function toggleZoom() { 103 if (!canZoom) return; // ignore for videos 104 105 isZoomed = !isZoomed; 106 media.classList.toggle('zoomed', isZoomed); 107 108 if (isZoomed) { 109 enterZoom(media.naturalWidth, INITIAL_SCALE_WHEN_FITS); 110 } else { 111 media.style.width = ''; 112 scale = 1; 113 } 114 adjustAlignment(); 115} 116 117function enterZoom(width, initialScale) { 118 baseWidth = width; 119 scale = initialScale; 120 media.style.width = (baseWidth * scale) + 'px'; 121 requestAnimationFrame(centerZoomedImage); 122} 123 124function centerZoomedImage() { 125 const rect = media.getBoundingClientRect(); 126 const viewportCenterX = overlay.clientWidth / 2; 127 const viewportCenterY = overlay.clientHeight / 2; 128 129 overlay.scrollLeft += rect.left + rect.width / 2 - viewportCenterX; 130 overlay.scrollTop += rect.top + rect.height / 2 - viewportCenterY; 131} 132 133function setZoomScale(nextScale) { 134 const rect = media.getBoundingClientRect(); 135 const viewportCenterX = overlay.clientWidth / 2; 136 const viewportCenterY = overlay.clientHeight / 2; 137 const ratioX = rect.width > 0 ? (viewportCenterX - rect.left) / rect.width : 0.5; 138 const ratioY = rect.height > 0 ? (viewportCenterY - rect.top) / rect.height : 0.5; 139 140 scale = Math.max(0.2, Math.min(5, nextScale)); 141 media.style.width = (baseWidth * scale) + 'px'; 142 143 requestAnimationFrame(() => { 144 const nextRect = media.getBoundingClientRect(); 145 overlay.scrollLeft += nextRect.left + ratioX * nextRect.width - viewportCenterX; 146 overlay.scrollTop += nextRect.top + ratioY * nextRect.height - viewportCenterY; 147 adjustAlignment(); 148 }); 149} 150 151/* Thumbnail clicks - activate only when a page image itself is clicked */ 152document.addEventListener('click', e => { 153 if (!e.target.matches('.teaser-card img, .figure img')) return; 154 const idx = +e.target.closest('.item').dataset.idx; 155 show(idx); 156}); 157 158/* Keyboard */ 159document.addEventListener('keydown', e => { 160 if (e.key === 'Control' || e.key === 'Meta') { 161 zoomModifierDown = true; 162 } 163 164 if (overlay.style.display !== 'flex') return; 165 166 /* Esc - exit zoom first, then close overlay */ 167 if (e.key === 'Escape') { 168 e.preventDefault(); // stop "Stop loading" in Chrome/Firefox 169 if (isZoomed) toggleZoom(); 170 else hide(); 171 return; 172 } 173 174 /* Enter toggles zoom for images */ 175 if (e.key === 'Enter' && canZoom) { 176 toggleZoom(); 177 return; 178 } 179
180 /* Arrow navigation disabled while zoomed */ 181 if (isZoomed) return; 182 if (e.key === 'ArrowRight') show(idx + 1); 183 else if (e.key === 'ArrowLeft') show(idx - 1); 184}); 185 186document.addEventListener('keyup', e => { 187 if (e.key === 'Control' || e.key === 'Meta') { 188 zoomModifierDown = false; 189 } 190}); 191 192window.addEventListener('blur', () => { 193 zoomModifierDown = false; 194}); 195 196/* Click backdrop to close */ 197overlay.addEventListener('click', e => { 198 if (e.target === overlay) hide(); 199}); 200 201function handleCtrlWheel(e) { 202 if (overlay.style.display !== 'flex' || !canZoom) return; 203 if (!(e.ctrlKey || e.metaKey || zoomModifierDown)) return; 204 205 e.preventDefault(); 206 e.stopPropagation(); 207 if (typeof e.stopImmediatePropagation === 'function') { 208 e.stopImmediatePropagation(); 209 } 210 211 if (!isZoomed) { 212 const renderedWidth = media.getBoundingClientRect().width; 213 isZoomed = true; 214 media.classList.add('zoomed'); 215 enterZoom(renderedWidth, 1); 216 adjustAlignment(); 217 return; 218 } 219 setZoomScale(scale + (e.deltaY < 0 ? 0.1 : -0.1)); 220} 221 222/* Ctrl/Cmd + wheel zoom for images only. 223 Capture-phase handling prevents browser/page zoom from consuming the gesture. */ 224window.addEventListener('wheel', handleCtrlWheel, { 225 capture: true, 226 passive: false 227}); 228 229window.addEventListener('gesturestart', e => { 230 if (overlay.style.display !== 'flex') return; 231 e.preventDefault(); 232}, {passive: false}); 233 234window.addEventListener('gesturechange', e => { 235 if (overlay.style.display !== 'flex') return; 236 e.preventDefault(); 237}, {passive: false}); 238 239/* Keep centering correct on resize / browser zoom */ 240window.addEventListener('resize', adjustAlignment);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.