1// dynamic-modal-for-mosaic.js â v1.1 2 3async function getVideoThumbnail(url) { 4 const fallback = '/_resources/_images/video-embed-placeholder.jpg'; 5 try { 6 const ytMatch = url.match(/(?:youtube\.com\/(?:embed\/|watch\?v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/); 7 if (ytMatch) return `https://img.youtube.com/vi/${ytMatch[1]}/maxresdefault.jpg`; 8 9 const vimeoMatch = url.match(/(?:player\.)?vimeo\.com\/((?:video\/)?\d+(\?h=[a-zA-Z0-9]+)?)/); 10 if (vimeoMatch) { 11 const res = await fetch(`https://noembed.com/embed?url=https://vimeo.com/${vimeoMatch[1]}`); 12 if (!res.ok) return fallback; 13 const data = await res.json(); 14 return data.thumbnail_url || fallback; 15 } 16 return fallback; 17 } catch { 18 return fallback; 19 } 20} 21// Remove any previously-created modal instances. 22(function removeExistingModals() { 23 document.querySelectorAll('#mycmodal').forEach(modal => modal.remove()); 24})(); 25 26// Replace each .video-embed iframe with a .video-thumb thumbnail. 27// Stores the embed URL on data-video-src so the modal can build 28// a fresh iframe on demand â no page-level iframes, no history pollution. 29async function initVideoBlocks() { 30 for (const iframe of document.querySelectorAll('iframe.video-embed')) { 31 try { 32 const src = iframe.src; 33 const thumbUrl = await getVideoThumbnail(src); 34 35 const thumb = document.createElement('div'); 36 thumb.className = 'video-thumb'; 37 38 const img = document.createElement('img'); 39 img.src = thumbUrl; 40 img.alt = 'Play video'; 41 img.className = 'cmodal-trigger'; 42 img.dataset.videoSrc = src; 43 44 thumb.appendChild(img); 45 iframe.parentNode.replaceChild(thumb, iframe); 46 } catch (err) { 47 console.error('Thumbnail error:', err); 48 } 49 } 50} 51 52document.addEventListener('DOMContentLoaded', async function () { 53 await initVideoBlocks(); 54 55 let cmodal, cmodalGraphic, cmodalCaption, cmodalControls, cmodalContent, closeButton; 56 let currentImageIndex = 0; 57 let currentImageList = []; 58 59 function setupModal() { 60 document.querySelectorAll('#mycmodal').forEach(modal => modal.remove()); 61 cmodal = document.createElement('div'); 62 cmodal.id = 'mycmodal'; 63 cmodal.className = 'cmodal'; 64 65 cmodalContent = document.createElement('div'); 66 cmodalContent.className = 'cmodal-content'; 67 68 closeButton = document.createElement('span'); 69 closeButton.className = 'Button close'; 70 closeButton.onclick = closeModal; 71 72 cmodalGraphic = document.createElement('div'); 73 cmodalGraphic.id = 'modalGraphic'; 74 75 cmodalCaption = document.createElement('div'); 76 cmodalCaption.id = 'cmodalCaption'; 77 78 cmodalControls = document.createElement('div'); 79 cmodalControls.className = 'modalControls'; 80 81 const prevButton = document.createElement('a'); 82 prevButton.className = 'previous'; 83 prevButton.onclick = function () { changeImage(-1); }; 84 85 const nextButton = document.createElement('a'); 86 nextButton.className = 'next'; 87 nextButton.onclick = function () { changeImage(1); }; 88 89 cmodalControls.appendChild(prevButton); 90 cmodalControls.appendChild(nextButton); 91 92 cmodalContent.appendChild(closeButton); 93 cmodalContent.appendChild(cmodalGraphic); 94 cmodalContent.appendChild(cmodalCaption); 95 96 cmodal.appendChild(cmodalContent); 97 cmodal.appendChild(cmodalControls); 98 document.body.appendChild(cmodal); 99 100 cmodal.addEventListener('click', function (e) { 101 if (e.target === cmodal || e.target === cmodalContent) closeModal(); 102 }); 103 } 104 105 function attachImageListeners() { 106 currentImageList = Array.from(document.querySelectorAll('.cmodal-trigger')).filter( 107 el => !el.closest('#mycmodal') 108 ); 109 110 currentImageList.forEach(function (el) { 111 if (el.dataset.cmodalBound) return; 112 el.dataset.cmodalBound = '1'; 113 el.addEventListener('click', function (e) { 114 e.preventDefault(); 115 e.stopPropagation(); 116 openModal(currentImageList.indexOf(el)); 117 }); 118 }); 119 } 120 121 function openModal(index) { 122 document.body.style.overflow = 'hidden'; 123 setupModal(); 124 currentImageIndex = index; 125 updateModal(); 126 cmodal.style.display = 'flex'; 127 } 128 129 function updateModal() { 130 if (!currentImageList.length) return; 131 const el = currentImageList[currentImageIndex]; 132 133 cmodalGraphic.innerHTML = ''; 134 135 if (el.dataset.videoSrc) { 136 // Video: build a fresh iframe â destroyed on close, no history pollution. 137 const videoBlock = document.createElement('div'); 138 videoBlock.className = 'video-block'; 139 const iframe = document.createElement('iframe'); 140 iframe.className = 'video-embed'; 141 iframe.style.cssText = 'display:block; width:100%; height:100%; min-height:315px; border:0;'; 142 const src = el.dataset.videoSrc; 143 iframe.src = src + (src.includes('?') ? '&' : '?') + 'auto
143play=1'; 144 iframe.setAttribute('allowfullscreen', 'allowfullscreen'); 145 iframe.setAttribute('allow', 'autoplay'); 146 videoBlock.appendChild(iframe); 147 cmodalGraphic.appendChild(videoBlock); 148 } else { 149 // Image: copy graphic-content from the grid item. 150 const graphicEl = el.closest('.grid-item')?.querySelector('.graphic-content'); 151 cmodalGraphic.innerHTML = graphicEl ? graphicEl.innerHTML : ''; 152 } 153 154 const captionEl = el.closest('.grid-item')?.querySelector('.caption'); 155 cmodalCaption.innerHTML = captionEl ? captionEl.innerHTML : ''; 156 } 157 158 function changeImage(step) { 159 currentImageIndex = Math.max(0, Math.min(currentImageList.length - 1, currentImageIndex + step)); 160 updateModal(); 161 } 162 163 function closeModal() { 164 if (!cmodal) return; 165 cmodalGraphic.innerHTML = ''; // destroys iframe, stops video playback 166 cmodal.style.display = 'none'; 167 document.body.style.overflow = 'auto'; 168 } 169 170 document.addEventListener('keyup', function (e) { 171 if (!cmodal || cmodal.style.display !== 'flex') return; 172 if (e.key === 'Escape') closeModal(); 173 if (e.key === 'ArrowLeft') changeImage(-1); 174 if (e.key === 'ArrowRight') changeImage(1); 175 }); 176 177 const observer = new MutationObserver(attachImageListeners); 178 observer.observe(document.body, { childList: true, subtree: true }); 179 180 attachImageListeners(); 181});
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.