PageSourceSearch

https://otis.edu/_resources/js/dynamic-modal-for-mosaic.js

js otis.edu collected 2026-10-01 09:08:10 UTC 6,211 bytes, 181 lines download raw bytes

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.