PageSourceSearch

https://www.lothianbuses.com/wp-content/plugins/lothian-safer-edin…thian-safer-edinburgh-carousel.js?ver=1.0.2

js lothianbuses.com collected 2026-10-02 03:06:44 UTC 9,076 bytes, 259 lines download raw bytes

1(function() {
2    'use strict';
3
4    const containers = document.querySelectorAll('.lothian-safer-edinburgh-carousel-posts-container');
5    if (containers.length === 0) return;
6
7    const CONFIG = {
8        scrollSpeed: 0.5,
9        pauseOnHover: true,
10    };
11
12    let lightboxOpen = false;
13    let lightboxTriggerEl = null;
14    let lightboxSourceCarousel = null;
15
16    // ===== LIGHTBOX FUNCTIONALITY (shared across all carousels) =====
17    function createLightbox() {
18
19        const modal = document.createElement('div');
20        modal.className = 'lse-carousel-lightbox';
21        modal.innerHTML = `
22            <div class="lse-carousel-lightbox-overlay"></div>
23            <div class="lse-carousel-lightbox-content" role="dialog" aria-modal="true" aria-label="Image preview">
24                <button class="lse-carousel-lightbox-close" aria-label="Close lightbox">
25                    <span aria-hidden="true">&times;</span>
26                </button>
27                <img class="lse-carousel-lightbox-image" src="" alt="" />
28                <div class="lse-carousel-lightbox-title"></div>
29            </div>
30        `;
31        document.body.appendChild(modal);
32        return modal;
33
34    }
35
36    const lightbox = createLightbox();
37    const lightboxImage = lightbox.querySelector('.lse-carousel-lightbox-image');
38    const lightboxTitle = lightbox.querySelector('.lse-carousel-lightbox-title');
39    const closeBtn = lightbox.querySelector('.lse-carousel-lightbox-close');
40    const overlay = lightbox.querySelector('.lse-carousel-lightbox-overlay');
41
42    let lightboxLoadToken = 0;
43
44    function openLightbox(url, title, triggerEl) {
45
46        const token = ++lightboxLoadToken; // guards against rapid successive clicks
47
48        lightboxTriggerEl = triggerEl || null;
49        lightboxSourceCarousel = triggerEl ? triggerEl.closest('.lothian-safer-edinburgh-carousel-posts-container') : null;
50
51        lightboxImage.classList.add('lse-carousel-lightbox-image-loading');
52        lightboxTitle.textContent = title || '';
53        lightbox.classList.add('lse-carousel-lightbox-active');
54        document.body.style.overflow = 'hidden';
55        lightboxOpen = true;
56
57        closeBtn.focus();
58
59        const preload = new Image();
60
61        preload.onload = () => {
62            if (token !== lightboxLoadToken) return; // a newer image was requested while this one was loading
63            lightboxImage.src = url;
64            lightboxImage.alt = title || '';
65            lightboxImage.classList.remove('lse-carousel-lightbox-image-loading');
66        };
67
68        preload.src = url;
69
70    }
71
72    // Trap focus within the lightbox when it's open
73    lightbox.addEventListener('keydown', (e) => {
74
75        if (e.key !== 'Tab' || !lightboxOpen) return;
76
77        // The close button is the only focusable element in this modal,
78        // so Tab and Shift+Tab both just keep focus pinned there.
79        e.preventDefault();
80        closeBtn.focus();
81
82    });
83
84    function closeLightbox() {
85
86        lightbox.classList.remove('lse-carousel-lightbox-active');
87        document.body.style.overflow = '';
88        lightboxOpen = false;
89
90        if (lightboxTriggerEl) {
91            lightboxTriggerEl.focus();
92            lightboxTriggerEl = null;
93        }
94
95        if (lightboxSourceCarousel) {
96            lightboxSourceCarousel.dispatchEvent(new CustomEvent('lse-carousel-lightbox-closed'));
97            lightboxSourceCarousel = null;
98        }
99
100    }
101
102    closeBtn.addEventListener('click', closeLightbox);
103    overlay.addEventListener('click', closeLightbox);
104
105    document.addEventListener('keydown', (e) => {
106        if (
107            e.key === 'Escape'
108            &&
109            lightbox.classList.contains('lse-carousel-lightbox-active')
110        ) {
111            closeLightbox();
112        }
113    });
114
115    // ===== PER-CAROUSEL INITIALISATION =====
116    function initCarousel(container) {
117
118        const items = container.querySelectorAll('.lothian-safer-edinburgh-carousel-post-card');
119        if (items.length === 0) return;
120
121        let isAutoPlaying = true;
122        let animationFrameId = null;
123
124        const totalItems = items.length;
125        const itemsPerSet = totalItems / 2;
126
127        // Cached layout measurements — recalculated on resize rather than every frame
128        let itemWidthWithGap = 0;
129        let resetPoint = 0;
130
131        function measure() {
132            const itemWidth = items[0].offsetWidth;
133            const gap = parseFloat(window.getComputedStyle(container).gap) || 0;
134            itemWidthWithGap = itemWidth + gap;
135            resetPoint = itemWidthWithGap * itemsPerSet;
136        }
137
138        measure();
139        window.addEventListener('resize', measure);
140
141        // ===== IMAGE BUTTON HANDLERS (scoped to this container) =====
142        // Native <button> handles Enter/Space activation automatically — click covers both.
143        container.querySelectorAll('.lothian-safer-edinburgh-carousel-post-image-btn').forEach((btn) => {
144
145            btn.addEventListener('click', () => {
146                const url = btn.dataset.lightboxUrl;
147                if (url) {
148                    openLightbox(url, btn.dataset.lightboxTitle, btn);
149                }
150            });
151
152            // Auto-scroll can carry the carousel on before Tab reaches this
153            // button, leaving the focused item out of view — bring it back
154            // into the visible area whenever it receives keyboard focus
155            btn.addEventListener('focus', () => {
156                btn.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
157            });
158
159        });
160
161        // ===== AUTO-SCROLL =====
162        function autoScroll() {
163
164            if (
165                isAutoPlaying
166                &&
167                !lightboxOpen
168            ) {
169
170                container.scrollLeft += CONFIG.scrollSpeed;
171
172                if (container.scrollLeft >= resetPoint - 10) {
173                    container.scrollLeft = 0;
174                }
175
176            }
177
178            animationFrameId = requestAnimationFrame(autoScroll);
179        }
180
181        // ===== PAUSE ON HOVER / KEYBOARD FOCUS =====
182        if (CONFIG.pauseOnHover) {
183            container.addEventListener('mouseenter', () => { isAutoPlaying = false; });
184            container.addEventListener('mouseleave', () => { isAutoPlaying = true; });
185        }
186
187        // Pause for as long as focus is anywhere inside the carousel —
188        // covers the Tab-to-reach-a-button flow, not just arrow-key scrolling
189        container.addEventListener('focusin', () => { isAutoPlaying = false; });
190
191        container.addEventListener('focusout', (e) => {
192
193            if (!container.contains(e.relatedTarget)) {
194                isAutoPlaying = true;
195            }
196
197        });
198
199        container.addEventListener('lse-carousel-lightbox-closed', () => {
200            isAutoPlaying = true;
201        });
202
203        // ===== KEYBOARD NAVIGATION (scoped to this carousel via bubbling) =====
204        // Only real (non-duplicate) buttons are focusable — see tabindex="-1"
205        // on duplicate items in the PHP template
206        const focusableButtons = Array.from(
207            container.querySelectorAll('.lothian-safer-edinburgh-carousel-post-image-btn:not([tabindex="-1"])')
208        );
209
210        container.addEventListener('keydown', (e) => {
211
212            if (lightboxOpen) return;
213
214            if (
215                e.key === 'ArrowLeft'
216                ||
217                e.key === 'ArrowRight'
218            ) {
219
220                const currentIndex = focusableButtons.indexOf(document.activeElement);
221
222                // If focus isn't on one of this carousel's buttons, let the
223                // key do its default thing rather than guessing an index
224                if (currentIndex === -1) return;
225
226                e.preventDefault();
227
228                let nextIndex;
229                if (e.key === 'ArrowLeft') {
230                    nextIndex = Math.max( 0, currentIndex - 1 );
231                } else {
232                    nextIndex = Math.min( focusableButtons.length - 1, currentIndex + 1 );
233                }
234
235                // .focus() triggers the existing 'focus' listener above,
236                // which handles scrolling the item into view
237                focusableButtons[ nextIndex ].focus();
238
239            }
240
241        });
242
243        autoScroll();
244
245        // Exposed so a future caller (e.g. if a page builder ever tears
246        // this container down dynamically) can stop the animation loop
247        // and remove the resize listener cleanly.
248        return function destroy() {
249            if (animationFrameId !== null) {
250                cancelAnimationFrame(animationFrameId);
251            }
252            window.removeEventListener('resize', measure);
253        };
254
255    }
256
257    const carouselInstances = Array.from(containers).map(initCarousel);
258
259})();

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.