PageSourceSearch

https://greenberetfoundation.org/wp-content/themes/greenberetfound…/js/video-modal-fallback.js?ver=2026061701

js greenberetfoundation.org collected 2026-10-02 02:40:34 UTC 11,977 bytes, 371 lines download raw bytes

1/**
2 * GBF video modal — single, framework-independent handler.
3 *
4 * Owns every YouTube trigger on the site (podcast cards, partner videos,
5 * webflow .play lightboxes, anything with a data-video attribute). It opens
6 * and closes the #videoModal WITHOUT depending on Bootstrap's modal plugin so
7 * behaviour is identical on local and production, and it ALWAYS clears the
8 * iframe src on close so audio/video never keeps playing in the background.
9 *
10 * Trigger clicks are caught in the capture phase and stopped there, which also
11 * disables Bootstrap's competing data-toggle="modal" data-api auto-open.
12 */
13(function() {
14    if (window.GBFVideoModalHandlerLoaded) {
15        return;
16    }
17
18    window.GBFVideoModalHandlerLoaded = true;
19    // Legacy flags other scripts checked, kept so nothing re-binds.
20    window.GBFVideoModalFallbackEnhancedLoaded = true;
21    window.GBFVideoModalFallbackLoaded = true;
22
23    var triggerSelector = '.partner-video, .jedburghpodcast_vid, .gbf-video-trigger, .play, .playcontainer, .playcontainersub, [data-video], [data-video-id], [data-video-url]';
24    var containerSelector = '.playcontainer, .playcontainersub, .play, .gbf-video-trigger, .partner-video, .jedburghpodcast_vid';
25
26    function hasInteractiveParent(node) {
27        return !!(node && node.closest && node.closest('a, button'));
28    }
29
30    function getAttributeVideo(node) {
31        if (!node || !node.getAttribute) {
32            return '';
33        }
34
35        return node.getAttribute('data-video') ||
36            node.getAttribute('data-video-id') ||
37            node.getAttribute('data-video-url') ||
38            '';
39    }
40
41    function getYouTubeId(value) {
42        var rawValue = String(value || '').trim();
43        var srcMatch;
44        var parsedUrl;
45        var queryId;
46        var pathMatch;
47        var fallbackMatch;
48
49        if (!rawValue) {
50            return '';
51        }
52
53        if (/^[A-Za-z0-9_-]{11}$/.test(rawValue)) {
54            return rawValue;
55        }
56
57        srcMatch = rawValue.match(/\bsrc=["']([^"']+)["']/i);
58        if (srcMatch) {
59            return getYouTubeId(srcMatch[1]);
60        }
61
62        try {
63            parsedUrl = new URL(rawValue, window.location.href);
64            queryId = parsedUrl.searchParams.get('v');
65
66            if (queryId && /^[A-Za-z0-9_-]{11}$/.test(queryId)) {
67                return queryId;
68            }
69
70            pathMatch = parsedUrl.pathname.match(/(?:embed|shorts|live)\/([A-Za-z0-9_-]{11})/);
71
72            if (pathMatch) {
73                return pathMatch[1];
74            }
75
76            if (parsedUrl.hostname.indexOf('youtu.be') !== -1) {
77                pathMatch = parsedUrl.pathname.replace(/^\//, '').match(/^([A-Za-z0-9_-]{11})/);
78
79                if (pathMatch) {
80                    return pathMatch[1];
81                }
82            }
83        } catch (error) {
84            // Fall through to regex extraction for encoded Embedly/Webflow payloads.
85        }
86
87        fallbackMatch = rawValue.match(/(?:v=|embed\/|embed%2F|shorts\/|shorts%2F|live\/|live%2F|youtu\.be\/|youtu\.be%2F)([A-Za-z0-9_-]{11})/i);
88
89        if (fallbackMatch) {
90            return fallbackMatch[1];
91        }
92
93        if ((rawValue.indexOf('youtube') !== -1 || rawValue.indexOf('youtu.be') !== -1) && rawValue.match(/[A-Za-z0-9_-]{11}/)) {
94            return rawValue.match(/[A-Za-z0-9_-]{11}/)[0];
95        }
96
97        return '';
98    }
99
100    function getLegacyPlayVideo(trigger) {
101        var scope = trigger;
102        var jsonNode;
103        var lightboxData;
104        var firstItem;
105
106        if (!scope) {
107            return '';
108        }
109
110        jsonNode = scope.querySelector ? scope.querySelector('.w-json') : null;
111
112        if (!jsonNode && scope.closest) {
113            scope = scope.closest(containerSelector);
114            jsonNode = scope && scope.querySelector ? scope.querySelector('.w-json') : null;
115        }
116
117        if (!jsonNode) {
118            return '';
119        }
120
121        try {
122            lightboxData = JSON.parse(jsonNode.textContent || '{}');
123            firstItem = lightboxData.items && lightboxData.items[0];
124
125            return firstItem ? (firstItem.originalUrl || firstItem.url || firstItem.html || firstItem.thumbnailUrl || '') : '';
126        } catch (error) {
127            return '';
128        }
129    }
130
131    function getTriggerVideoValue(trigger) {
132        var videoValue = getAttributeVideo(trigger);
133        var parentWithVideo;
134        var childWithVideo;
135
136        if (videoValue) {
137            return videoValue;
138        }
139
140        parentWithVideo = trigger && trigger.closest ? trigger.closest('[data-vi
140deo], [data-video-id], [data-video-url]') : null;
141        videoValue = getAttributeVideo(parentWithVideo);
142
143        if (videoValue) {
144            return videoValue;
145        }
146
147        childWithVideo = trigger && trigger.querySelector ? trigger.querySelector('[data-video], [data-video-id], [data-video-url]') : null;
148        videoValue = getAttributeVideo(childWithVideo);
149
150        if (videoValue) {
151            return videoValue;
152        }
153
154        return getLegacyPlayVideo(trigger);
155    }
156
157    function ensureBackdrop() {
158        var backdrop;
159
160        if (document.querySelector('.modal-backdrop.gbf-video-backdrop')) {
161            return;
162        }
163
164        backdrop = document.createElement('div');
165        backdrop.className = 'modal-backdrop fade show gbf-video-backdrop';
166        document.body.appendChild(backdrop);
167    }
168
169    function clearBackdrops() {
170        document.querySelectorAll('.modal-backdrop.gbf-video-backdrop').forEach(function(backdrop) {
171            if (backdrop.parentNode) {
172                backdrop.parentNode.removeChild(backdrop);
173            }
174        });
175    }
176
177    function ensureVideoModal() {
178        var modal = document.getElementById('videoModal');
179        var modalBody;
180        var iframe;
181
182        if (!modal) {
183            modal = document.createElement('div');
184            modal.className = 'modal fade video-modal';
185            modal.id = 'videoModal';
186            modal.tabIndex = -1;
187            modal.setAttribute('aria-hidden', 'true');
188            modal.innerHTML =
189                '<button type="button" class="close" data-dismiss="modal" aria-label="Close">' +
190                    '<span aria-hidden="true">&times;</span>' +
191                '</button>' +
192                '<div class="modal-dialog modal-dialog-centered modal-xl">' +
193                    '<div class="modal-content">' +
194                        '<div class="modal-body"></div>' +
195                    '</div>' +
196                '</div>';
197
198            document.body.appendChild(modal);
199        }
200
201        iframe = modal.querySelector('iframe');
202
203        if (!iframe) {
204            modalBody = modal.querySelector('.modal-body') || modal;
205            iframe = document.createElement('iframe');
206            iframe.width = '560';
207            iframe.height = '315';
208            iframe.title = 'YouTube video player';
209            iframe.frameBorder = '0';
210            iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share';
211            iframe.referrerPolicy = 'strict-origin-when-cross-origin';
212            iframe.allowFullscreen = true;
213            modalBody.appendChild(iframe);
214        }
215
216        return modal;
217    }
218
219    function isModalOpen() {
220        var modal = document.getElementById('videoModal');
221        return !!(modal && modal.classList.contains('show'));
222    }
223
224    function openVideoModal(videoValue) {
225        var videoId = getYouTubeId(videoValue);
226        var modal;
227        var iframe;
228
229        if (!videoId) {
230            return false;
231        }
232
233        modal = ensureVideoModal();
234        iframe = modal.querySelector('iframe');
235
236        if (iframe) {
237            iframe.setAttribute('src', 'https://www.youtube.com/embed/' + encodeURIComponent(videoId) + '?autoplay=1&rel=0');
238        }
239
240        modal.style.display = 'block';
241        modal.removeAttribute('aria-hidden');
242        modal.setAttribute('aria-modal', 'true');
243        modal.classList.add('show');
244        document.body.classList.add('modal-open');
245        ensureBackdrop();
246
247        return true;
248    }
249
250    function closeVideoModal() {
251        var modal = document.getElementById('videoModal');
252        var iframe;
253
254        if (!modal) {
255            return;
256        }
257
258        modal.classList.remove('show');
259        modal.style.display = 'none';
260        modal.setAttribute('aria-hidden', 'true');
261        modal.removeAttribute('aria-modal');
262        document.body.classList.remove('modal-open');
263
264        // Always stop playback by tearing down the iframe src.
265        iframe = modal.querySelector('iframe');
266        if (iframe) {
267            iframe.setAttribute('src', '');
268        }
269
270        clearBackdrops();
271    }
272
273    function handleTrigger(event) {
274        var trigger = event.target.closest(triggerSelector);
275        var videoValue;
276
277        if (!trigger) {
278            return;
279        }
280
281        videoValue = getTriggerVideoValue(trigger);
282
283        if (!getYouTubeId(videoValue)) {
284            return;
285        }
286
287        // Capture-phase stop: prevents Bootstrap's data-toggle data-api (and any
288        // other delegated handler) from also acting on this same click.
289        event.preventDefault();
290        event.stopPropagation();
291        openVideoModal(videoValue);
292    }
293
294    // Open — capture phase so we win over Bootstrap's bubble-phase data-api.
295    document.addEventListener('click', handleTrigger, true);
296
297    document.addEventListener('keydown', function(event) {
298        var trigger;
299        var videoValue;
300
301        if (event.key !== 'Enter' && event.key !== ' ') {
302            return;
303        }
304
305        trigger = event.target.closest(triggerSelector);
306
307        // Native <a>/<button> triggers already emit a click on Enter/Space, which
308        // the click handler above catches. Only handle non-interactive triggers
309        // (e.g. div.play with role="button") here to avoid double-opening.
310        if (!trigger || trigger.matches('a, button')) {
311            return;
312        }
313
314        videoValue = getTriggerVideoValue(trigger);
315
316        if (!getYouTubeId(videoValue)) {
317            return;
318        }
319
320        event.preventDefault();
321        openVideoModal(videoValue);
322    }, true);
323
324    // Close — close button, [data-dismiss], the dark backdrop, or a click on the
325    // modal container itself (the area outside the centered dialog).
326    document.addEventListener('click', function(event) {
327        var modal = document.getElementById('videoModal');
328
329        if (!modal || !isModalOpen()) {
330            return;
331        }
332
333        var isDismiss = !!event.target.closest('#videoModal .close, #videoModal [data-dismiss="modal"], .modal-backdrop.gbf-video-backdrop');
334        var isOutside = event.target === modal; // clicked the container, not the dialog
335
336        if (!isDismiss && !isOutside) {
337            return;
338        }
339
340        event.preventDefault();
341        closeVideoModal();
342    });
343
344    // Close on Escape.
345    document.addEventListener('keydown', function(event) {
346        if ((event.key === 'Escape' || event.key === 'Esc') && isModalOpen()) {
347            closeVideoModal();
348        }
349    });
350
351    // If anything still drives the modal through Bootstrap, clean up on hide too.
352    document.addEventListener('hidden.bs.modal', function(event) {
353        if (event.target && event.target.id === 'videoModal') {
354            closeVideoModal();
355        }
356    });
357
358    document.addEventListener('DOMContentLoaded', function() {
359        document.querySelectorAll('.gbf-video-trigger, .play, .playcontainer, .playcontainersub').forEach(function(trigger) {
360            if (!getYouTubeId(getTriggerVideoValue(trigger)) || hasInteractiveParent(trigger)) {
361                return;
362            }
363
364            trigger.setAttribute('role', 'button');
365
366            if (!trigger.hasAttribute('tabindex')) {
367                trigger.setAttribute('tabindex', '0');
368            }
369        });
370    });
371})();

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.