PageSourceSearch

https://www2.gov.bm/js/article-interop.djh9ms5ydr.js

js www2.gov.bm collected 2026-09-29 03:21:36 UTC 23,650 bytes, 639 lines download raw bytes

1/**
2 * Editorial detail interop -- progress bar, share, lightbox, back-to-top, gallery.
3 */
4window.articleInterop = (function () {
5    let _lightboxImages = [];
6    let _lightboxIndex = 0;
7    let _savedOverflow = '';
8    let _progressStartElement = null;
9    let _progressEndElement = null;
10    let _progressChrome = null;
11    let _progressBar = null;
12    let _progressFrameId = null;
13    let _progressUpdateScheduled = false;
14    let _progressScrollHandler = null;
15    let _progressResizeHandler = null;
16    let _progressResizeObserver = null;
17    let _editorialDetailObserver = null;
18    let _carouselAnimationFrames = new WeakMap();
19    let _carouselAnimatingTracks = new WeakSet();
20
21    function getEditorialDetailOptions(options) {
22        options = options || {};
23
24        return {
25            progressRootSelector: options.progressRootSelector || '[data-editorial-detail-root]',
26            progressSelector: options.progressSelector || '[data-editorial-progress]',
27            chromeSelector: options.chromeSelector || '[data-editorial-detail-chrome]'
28        };
29    }
30
31    function stopEditorialDetailObserver() {
32        if (_editorialDetailObserver) {
33            _editorialDetailObserver.disconnect();
34            _editorialDetailObserver = null;
35        }
36    }
37
38    function observeEditorialDetailWhenPresent(options) {
39        if (_editorialDetailObserver || typeof MutationObserver !== 'function') {
40            return;
41        }
42
43        var detailOptions = getEditorialDetailOptions(options);
44
45        var target = document.body || document.documentElement;
46        if (!(target instanceof HTMLElement)) {
47            return;
48        }
49
50        _editorialDetailObserver = new MutationObserver(function () {
51            var progressRoot = document.querySelector(detailOptions.progressRootSelector);
52            var progress = document.querySelector(detailOptions.progressSelector);
53            if (progressRoot instanceof HTMLElement && progress instanceof HTMLElement) {
54                initEditorialDetail(detailOptions);
55            }
56        });
57        _editorialDetailObserver.observe(target, { childList: true, subtree: true });
58    }
59
60    function destroyProgressBar() {
61        stopEditorialDetailObserver();
62
63        if (_progressScrollHandler) {
64            window.removeEventListener('scroll', _progressScrollHandler);
65            _progressScrollHandler = null;
66        }
67
68        if (_progressResizeHandler) {
69            window.removeEventListener('resize', _progressResizeHandler);
70            _progressResizeHandler = null;
71        }
72
73        if (_progressResizeObserver) {
74            _progressResizeObserver.disconnect();
75            _progressResizeObserver = null;
76        }
77
78        if (_progressFrameId !== null) {
79            cancelAnimationFrame(_progressFrameId);
80            _progressFrameId = null;
81        }
82
83        if (_progressBar instanceof HTMLElement && _progressBar.isConnected) {
84            _progressBar.style.removeProperty('--article-progress-scale');
85        }
86
87        _progressStartElement = null;
88        _progressEndElement = null;
89        _progressChrome = null;
90        _progressBar = null;
91        _progressUpdateScheduled = false;
92    }
93
94    function updateProgressBar() {
95        _progressUpdateScheduled = false;
96        _progressFrameId = null;
97
98        if (
99            !(_progressStartElement instanceof HTMLElement) ||
100            !(_progressEndElement instanceof HTMLElement) ||
101            !(_progressBar instanceof HTMLElement) ||
102            !_progressStartElement.isConnected ||
103            !_progressEndElement.isConnected ||
104            !_progressBar.isConnected
105        ) {
106            destroyProgressBar();
107            return;
108        }
109
110        var startRect = _progressStartElement.getBoundingClientRect();
111        var endRect = _progressEndElement.getBoundingClientRect();
112        var start = startRect.top + window.scrollY;
113        var end = endRect.bottom + window.scrollY;
114
115        // Tie progress to the visual reading line beneath the sticky chrome.
116        var anchor = window.scrollY;
117        if (_progressChrome instanceof HTMLElement && _progressChrome.isConnected) {
118            anchor += _progressChrome.getBoundingClientRect().bottom;
119        }
120
121        var startScrollY = start - anchor;
122        var endScrollY = end - window.innerHeight;
123
124        if (endScrollY <= startScrollY) {
125            var immediateProgress = window.scrollY >= endScrollY ? 1 : 0;
126            _progressBar.style.setProperty('--article-progress-scale', immediateProgress.toString());
127            return;
128        }
129
130        var progress = Math.min(Math.max((window.scrollY - startScrollY) / (endScrollY - startScrollY), 0), 1);
131        _progressBar.style.setProperty('--article-progress-scale', progress.toString());
132    }
133
134    function scheduleProgressBarUpdate() {
135        if (_progressUpdateScheduled) {
136            return;
137        }
138
139        _progressUpdateScheduled = true;
140        _progressFrameId = requestAnimationFrame(updateProgressBar);
141    }
142
143    // ── Reading progress bar ──────────────────────────────────────────
144    function initProgressBar(startSelector, endSelector, progressSelector, chromeSelector) {
145        destroyProgressBar();
146
147        var start = document.querySelector(startSelector);
148        var end = document.querySelector(endSelector);
149        var progress = document.querySelector(progressSelector);
150        var chrome = chromeSelector ? document.querySelector(chromeSelector) : null;
151
152        if (
153            !(start instanceof HTMLElement) ||
154            !(end instanceof HTMLElement) ||
155            !(progress instanceof HTMLElement)
156        ) {
157            return;
158        }
159
160        _progressStartElement = start;
161        _progressEndElement = end;
162        _progressChrome = chrome instanceof HTMLElement ? chrome : null;
163        _progressBar = progress;
164        _progressScrollHandler = scheduleProgressBarUpdate;
165        _progressResizeHandler = scheduleProgressBarUpdate;
166
167        window.addEventListener('scroll', _progressScrollHandler, { passive: true });
168        window.addEventListener('resize', _progressResizeHandler, { passive: true });
169
170        if (typeof ResizeObserver === 'function') {
171            _progressResizeObserver = new ResizeObserver(scheduleProgressBarUpdate);
172            _progressResizeObserver.observe(_progressStartElement);
173            if (_progressEndElement !== _progressStartElement) {
174                _progressResizeObserver.observe(_progressEndElement);
175            }
176            if (_progressChrome) {
177                _progressResizeObserver.observe(_progressChrome);
178            }
179        }
180
181        scheduleProgressBarUpdate();
182    }
183
184    function initEditorialDetail(options) {
185        var detailOptions = getEditorialDetailOptions(options);
186        var progressRoot = document.querySelector(detailOptions.progressRootSelector);
187        var progress = document.querySelector(detailOptions.progressSelector);
188
189        if (progressRoot instanceof HTMLElement && progress instanceof HTMLElement) {
190            stopEditorialDetailObserver();
191            var rootSelector = detailOptions.progressRootSelector;
192            initProgressBar(rootSelector, rootSelector, detailOptions.progressSelector, detailOptions.chromeSelector);
193        } else {
194            destroyProgressBar();
195            observeEditorialDetailWhenPresent(detailOptions);
196        }
197
198        initCarousel('#article-carousel');
199        initInlineCarousels();
200    }
201
202    function initArticlePage() {
203        initEditorialDetail();
204    }
205
206    function initInlineCarousels() {
207        document.querySelectorAll('.article-carousel[data-inline-carousel]').forEach(function (carousel) {
208            if (carousel.id) initCarousel('#' + carousel.id);
209        });
210    }
211
212    // ── Copy article link ─────────────────────────────────────────────
213    async function copyLink(button) {
214        var url = window.location.href;
215        var success = false;
216
217        try {
218            if (window.clipboardInterop && typeof window.clipboardInterop.copyText === 'function') {
219                success = await window.clipboardInterop.copyText(url);
220            } else if (navigator.clipboard && navigator.clipboard.writeText) {
221                await navigator.clipboard.writeText(url);
222                success = true;
223            } else {
224                // Fallback for older browsers
225                var ta = document.createElement('textarea');
226                ta.value = url;
227                ta.style.position = 'fixed';
228                ta.style.left = '-9999px';
229                document.body.appendChild(ta);
230                ta.select();
231                success = document.execCommand('copy');
232                document.body.removeChild(ta);
233            }
234        } catch (_) {
235            success = false;
236        }
237
238        if (success && button) {
239            var original = button.getAttribute('aria-label');
240            button.setAttribute('aria-label', 'Copied page link');
241            button.classList.add('share-btn-copied');
242            setTimeout(function () {
243                button.setAttribute('aria-label', original || '');
244                button.classList.remove('share-btn-copied');
245            }, 2000);
246        }
247    }
248
249    // ── Native share ──────────────────────────────────────────────────
250    async function nativeShare(title, text) {
251        if (!navigator.share) return;
252        try {
253            await navigator.share({ title: title, text: text, url: window.location.href });
254        } catch (_) {
255            // User cancelled -- silently ignore
256        }
257    }
258
259    function canNativeShare() {
260        return !!navigator.share;
261    }
262
263    // ── Lightbox ──────────────────────────────────────────────────────
264    var _previousFocus = null;
265    var _focusTrapHandler = null;
266    var _touchStartHandler = null;
267    var _touchEndHandler = null;
268    var _backdropClickHandler = null;
269
270    function openLightbox(images, startIndex) {
271        _lightboxImages = typeof images === 'string' ? JSON.parse(images) : images;
272        _lightboxIndex = startIndex || 0;
273        var overlay = document.getElementById('article-lightbox');
274        if (!overlay) return;
275
276        _savedOverflow = document.body.style.overflow;
277        document.body.style.overflow = 'hidden';
278        overlay.removeAttribute('hidden');
279        overlay.style.display = '';
280        _updateLightbox();
281
282        _previousFocus = document.activeElement;
283        overlay.focus();
284
285        // Focus trap
286        _focusTrapHandler = function (e) {
287            if (e.key !== 'Tab') return;
288            var focusable = overlay.querySelectorAll('button, [tabindex]');
289            if (focusable.length === 0) return;
290            var first = focusable[0];
291            var last = focusable[focusable.length - 1];
292            if (e.shiftKey && document.activeElement === first) {
293                e.preventDefault();
294                last.focus();
295            } else if (!e.shiftKey && document.activeElement === last) {
296                e.preventDefault();
297                first.focus();
298            }
299        };
300        overlay.addEventListener('keydown', _focusTrapHandler);
301
302        // Swipe support
303        var touchStartX = 0;
304        _touchStartHandler = function (e) { touchStartX = e.changedTouches[0].screenX; };
305        _touchEndHandler = function (e) {
306            var diff = e.changedTouches[0].screenX - touchStartX;
307            if (Math.abs(diff) > 50) {
308                if (diff > 0) lightboxPrev();
309                else lightboxNext();
310            }
311        };
312        overlay.addEventListener('touchstart', _touchStartHandler, { passive: true });
313        overlay.addEventListener('touchend', _touchEndHandler, { passive: true });
314
315        // Close when clicking the dark backdrop (not nav buttons, close button, or image)
316        _backdropClickHandler = function (e) {
317            if (e.target === overlay) closeLightbox();
318        };
319        overlay.addEventListener('click', _backdropClickHandler);
320    }
321
322    function closeLightbox() {
323        var overlay = document.getElementById('article-lightbox');
324        if (overlay) {
325            if (_focusTrapHandler) overlay.removeEventListener('keydown', _focusTrapHandler);
326            if (_touchStartHandler) overlay.removeEventListener('touchstart', _touchStartHandler);
327            if (_touchEndHandler) overlay.removeEventListener('touchend', _touchEndHandler);
328            if (_backdropClickHandler) overlay.removeEventListener('click', _backdropClickHandler);
329        }
330
331        if (_previousFocus) _previousFocus.focus();
332
333        if (!overlay) return;
334
335        document.body.style.overflow = _savedOverflow;
336        overlay.setAttribute('hidden', '');
337        _lightboxImages = [];
338        _lightboxIndex = 0;
339        _previousFocus = null;
340        _focusTrapHandler = null;
341        _touchStartHandler = null;
342        _touchEndHandler = null;
343        _backdropClickHandler = null;
344    }
345
346    function lightboxPrev() {
347        if (_lightboxImages.length === 0) return;
348        _lightboxIndex = (_lightboxIndex - 1 + _lightboxImages.length) % _lightboxImages.length;
349        _updateLightbox();
350    }
351
352    function lightboxNext() {
353        if (_lightboxImages.length === 0) return;
354        _lightboxIndex = (_lightboxIndex + 1) % _lightboxImages.length;
355        _updateLightbox();
356    }
357
358    function _updateLightbox() {
359        var overlay = document.getElementById('article-lightbox');
360        if (!overlay || _lightboxImages.length === 0) return;
361
362        var current = _lightboxImages[_lightboxIndex];
363        var img = overlay.querySelector('.article-lightbox img');
364        if (img) {
365            img.src = current.url;
366            img.alt = current.alt || '';
367        }
368
369        var counter = overlay.querySelector('.article-lightbox-counter');
370        if (counter) {
371            counter.textContent = (_lightboxIndex + 1) + ' / ' + _lightboxImages.length;
372        }
373
374        var caption = overlay.querySelector('.article-lightbox-caption');
375        if (caption) {
376            caption.textContent = current.caption || '';
377        }
378    }
379
380    function initLightboxKeyboard() {
381        document.addEventListener('keydown', function (e) {
382            var overlay = document.getElementById('article-lightbox');
383            if (!overlay || overlay.hasAttribute('hidden')) return;
384
385            if (e.key === 'Escape') {
386                closeLightbox();
387            } else if (e.key === 'ArrowLeft') {
388                lightboxPrev();
389            } else if (e.key === 'ArrowRight') {
390                lightboxNext();
391            }
392        });
393    }
394
395    // ── Back to top ───────────────────────────────────────────────────
396    function initBackToTop(buttonSelector) {
397        var btn = document.querySelector(buttonSelector);
398        if (!btn) return;
399
400        btn.style.display = 'none';
401        var threshold = window.innerHeight * 2;
402
403        window.addEventListener('scroll', function () {
404            if (window.scrollY > threshold) {
405                btn.style.display = '';
406            } else {
407                btn.style.display = 'none';
408            }
409        }, { passive: true });
410    }
411
412    function getCarouselElements(carouselSelector) {
413        var carousel = document.querySelector(carouselSelector);
414        if (!(carousel instanceof HTMLElement)) return null;
415
416        var track = carousel.querySelector('.article-carousel-track');
417        if (!(track instanceof HTMLElement)) return null;
418
419        var slides = Array.from(track.querySelectorAll('.article-carou
419sel-slide'))
420            .filter(function (slide) { return slide instanceof HTMLElement; });
421
422        if (slides.length === 0) return null;
423
424        return { carousel: carousel, track: track, slides: slides };
425    }
426
427    function getCarouselClampedIndex(slides, index) {
428        return Math.min(Math.max(index, 0), slides.length - 1);
429    }
430
431    function getClosestCarouselIndex(track, slides) {
432        var currentLeft = track.scrollLeft;
433        var closestIndex = 0;
434        var smallestDelta = Number.POSITIVE_INFINITY;
435
436        slides.forEach(function (slide, index) {
437            var delta = Math.abs(currentLeft - slide.offsetLeft);
438            if (delta < smallestDelta) {
439                smallestDelta = delta;
440                closestIndex = index;
441            }
442        });
443
444        return closestIndex;
445    }
446
447    function syncCarouselUi(carousel, track, slides) {
448        var idx = getClosestCarouselIndex(track, slides);
449        var counter = carousel.querySelector('.article-carousel-counter');
450        var gallery = carousel.closest('.article-gallery');
451        var thumbs = gallery ? gallery.querySelectorAll('.article-gallery-thumb') : [];
452
453        if (counter) {
454            counter.textContent = (idx + 1) + ' / ' + slides.length;
455        }
456
457        var captionEl = gallery ? gallery.querySelector('.article-carousel-caption') : null;
458        if (captionEl && slides[idx]) {
459            var captionText = slides[idx].getAttribute('data-caption') || '';
460            captionEl.textContent = captionText;
461            captionEl.style.display = captionText ? '' : 'none';
462        }
463
464        thumbs.forEach(function (thumb, thumbIndex) {
465            thumb.classList.toggle('is-active', thumbIndex === idx);
466        });
467    }
468
469    function stopCarouselAnimation(track) {
470        var frameId = _carouselAnimationFrames.get(track);
471        if (frameId !== undefined) {
472            cancelAnimationFrame(frameId);
473            _carouselAnimationFrames.delete(track);
474        }
475
476        _carouselAnimatingTracks.delete(track);
477        track.style.scrollSnapType = '';
478        track.style.scrollBehavior = '';
479    }
480
481    function setCarouselIndex(carousel, track, slides, index) {
482        stopCarouselAnimation(track);
483
484        var clampedIndex = getCarouselClampedIndex(slides, index);
485        track.style.scrollBehavior = 'auto';
486        track.scrollLeft = slides[clampedIndex].offsetLeft;
487        track.style.scrollBehavior = '';
488
489        syncCarouselUi(carousel, track, slides);
490    }
491
492    function animateCarouselToIndex(carousel, track, slides, index) {
493        var clampedIndex = getCarouselClampedIndex(slides, index);
494        var startLeft = track.scrollLeft;
495        var targetLeft = slides[clampedIndex].offsetLeft;
496
497        if (Math.abs(targetLeft - startLeft) < 1) {
498            setCarouselIndex(carousel, track, slides, clampedIndex);
499            return;
500        }
501
502        stopCarouselAnimation(track);
503
504        _carouselAnimatingTracks.add(track);
505        track.style.scrollSnapType = 'none';
506        track.style.scrollBehavior = 'auto';
507
508        var startTime = performance.now();
509        var duration = 260;
510
511        function step(timestamp) {
512            var progress = Math.min((timestamp - startTime) / duration, 1);
513            var eased = 1 - Math.pow(1 - progress, 3);
514            track.scrollLeft = startLeft + ((targetLeft - startLeft) * eased);
515
516            if (progress < 1) {
517                _carouselAnimationFrames.set(track, requestAnimationFrame(step));
518                return;
519            }
520
521            _carouselAnimationFrames.delete(track);
522            _carouselAnimatingTracks.delete(track);
523            track.scrollLeft = targetLeft;
524            track.style.scrollSnapType = '';
525            track.style.scrollBehavior = '';
526            syncCarouselUi(carousel, track, slides);
527        }
528
529        _carouselAnimationFrames.set(track, requestAnimationFrame(step));
530    }
531
532    // ── Gallery carousel (scroll-snap) ───────────────────────────────
533    function initCarousel(carouselSelector) {
534        var elements = getCarouselElements(carouselSelector);
535        if (!elements) return;
536
537        var carousel = elements.carousel;
538        if (carousel.dataset.articleCarouselInitialized === 'true') return;
539
540        carousel.dataset.articleCarouselInitialized = 'true';
541
542        var track = elements.track;
543        var slides = elements.slides;
544        var prevBtn = carousel.querySelector('.article-carousel-prev');
545        var nextBtn = carousel.querySelector('.article-carousel-next');
546        var slideCount = slides.length;
547
548        function updateUI() {
549            syncCarouselUi(carousel, track, slides);
550        }
551
552        track.addEventListener('scrollend', function () {
553            if (_carouselAnimatingTracks.has(track)) {
554                return;
555            }
556
557            updateUI();
558        });
559
560        var scrollTimer;
561        track.addEventListener('scroll', function () {
562            if (_carouselAnimatingTracks.has(track)) {
563                return;
564            }
565
566            clearTimeout(scrollTimer);
567            scrollTimer = setTimeout(updateUI, 100);
568        }, { passive: true });
569
570        if (prevBtn) {
571            prevBtn.addEventListener('click', function () {
572                var currentIndex = getClosestCarouselIndex(track, slides);
573                if (currentIndex === 0) {
574                    setCarouselIndex(carousel, track, slides, slideCount - 1);
575                } else {
576                    animateCarouselToIndex(carousel, track, slides, currentIndex - 1);
577                }
578            });
579        }
580
581        if (nextBtn) {
582            nextBtn.addEventListener('click', function () {
583                var currentIndex = getClosestCarouselIndex(track, slides);
584                if (currentIndex >= slideCount - 1) {
585                    setCarouselIndex(carousel, track, slides, 0);
586                } else {
587                    animateCarouselToIndex(carousel, track, slides, currentIndex + 1);
588                }
589            });
590        }
591
592        updateUI();
593    }
594
595    function carouselGoTo(carouselSelector, index) {
596        var elements = getCarouselElements(carouselSelector);
597        if (!elements) return;
598
599        animateCarouselToIndex(elements.carousel, elements.track, elements.slides, index);
600    }
601
602    return {
603        initEditorialDetail: initEditorialDetail,
604        initArticlePage: initArticlePage,
605        initProgressBar: initProgressBar,
606        destroyProgressBar: destroyProgressBar,
607        copyLink: copyLink,
608        nativeShare: nativeShare,
609        canNativeShare: canNativeShare,
610        openLightbox: openLightbox,
611        closeLightbox: closeLightbox,
612        lightboxPrev: lightboxPrev,
613        lightboxNext: lightboxNext,
614        _updateLightbox: _updateLightbox,
615        initLightboxKeyboard: initLightboxKeyboard,
616        initBackToTop: initBackToTop,
617        initCarousel: initCarousel,
618        initInlineCarousels: initInlineCarousels,
619        carouselGoTo: carouselGoTo
620    };
621})();
622
623window.addEventListener('pagehide', function () {
624    window.articleInterop.destroyProgressBar();
625});
626
627function initEditorialDetailWhenPresent() {
628    window.articleInterop.initEditorialDetail();
629}
630
631window.Blazor?.addEventListener?.('enhancedload', initEditorialDetailWhenPresent);
632window.Blazor?.addEventListener?.('enhancednavigationstart', function () {
633    window.articleInterop.destroyProgressBar();
634});
635
636document.addEventListener('DOMContentLoaded', function () {
637    initEditorialDetailWhenPresent();
638    window.articleInterop.initLightboxKeyboard();
639});

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.