PageSourceSearch

https://www.bbbs.org/wp-content/themes/BBBSA-2025/assets/js/logo-r…&new_bbbs=1&ver=1791489781&new_bbbs=1

js bbbs.org collected 2026-10-09 23:28:17 UTC 9,818 bytes, 260 lines download raw bytes

1(function() {
2    'use strict';
3
4    var SPEED_PX_PER_SEC = { slow: 20, medium: 40, fast: 80 };
5    var RESUME_DELAY_AFTER_POINTER = 1200; // ms — covers mobile momentum settle
6    var RESUME_DELAY_AFTER_HOVER_OR_FOCUS = 250;
7
8    function initLogoRow(section) {
9        var track     = section.querySelector('.logo-row-section__track');
10        var logos     = section.querySelector('.logo-row-section__logos');
11        var fadeLeft  = section.querySelector('.logo-row-section__fade--left');
12        var fadeRight = section.querySelector('.logo-row-section__fade--right');
13
14        if (!track || !logos) return;
15
16        var autoplay      = section.getAttribute('data-autoplay') === 'true';
17        var speedKey       = section.getAttribute('data-autoplay-speed') || 'medium';
18        var reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
19
20        if (!SPEED_PX_PER_SEC[speedKey]) speedKey = 'medium';
21
22        var originalContentWidth = 0;
23
24        // ---- Scroll state check ----
25
26        function checkScrollState() {
27            var trackWidth  = track.offsetWidth;
28            var logosWidth  = logos.scrollWidth;
29            var scrollLeft  = track.scrollLeft;
30            var needsScroll = logosWidth > trackWidth + 2;
31
32            section.classList.toggle('has-scroll', needsScroll);
33
34            // When autoplaying, fade visibility is handled by CSS (.is-autoplaying).
35            if (section.classList.contains('is-autoplaying')) return;
36
37            if (needsScroll) {
38                var maxScroll = logosWidth - trackWidth;
39                fadeLeft.classList.toggle('is-visible',  scrollLeft > 5);
40                fadeRight.classList.toggle('is-visible', scrollLeft < maxScroll - 5);
41            } else {
42                fadeLeft.classList.remove('is-visible');
43                fadeRight.classList.remove('is-visible');
44            }
45        }
46
47        // ---- rAF autoplay loop ----
48
49        var rafId = null;
50        var lastTs = 0;
51        var isPaused = false;
52        var resumeTimer = null;
53
54        function autoplayStep(ts) {
55            if (!lastTs) lastTs = ts;
56            var deltaMs = ts - lastTs;
57            lastTs = ts;
58
59            if (!isPaused) {
60                var pxPerMs = SPEED_PX_PER_SEC[speedKey] / 1000;
61                track.scrollLeft += pxPerMs * deltaMs;
62
63                // Seamless loop: once we've scrolled past the original content width,
64                // rewind by that width. The cloned second half makes this invisible.
65                if (track.scrollLeft >= originalContentWidth) {
66                    track.scrollLeft -= originalContentWidth;
67                }
68            }
69
70            rafId = requestAnimationFrame(autoplayStep);
71        }
72
73        function startAutoplay() {
74            if (rafId !== null) return;
75            lastTs = 0;
76            rafId = requestAnimationFrame(autoplayStep);
77        }
78
79        function stopAutoplay() {
80            if (rafId !== null) cancelAnimationFrame(rafId);
81            rafId = null;
82            lastTs = 0;
83        }
84
85        function pauseAutoplay() {
86            isPaused = true;
87            clearTimeout(resumeTimer);
88        }
89
90        function resumeAutoplay(delay) {
91            clearTimeout(resumeTimer);
92            resumeTimer = setTimeout(function() {
93                isPaused = false;
94                lastTs = 0; // reset timestamp so delta on next frame isn't a huge jump
95            }, delay);
96        }
97
98        // ---- Drag wiring ----
99
100        var isDragging = false;
101        var didDrag = false; // true if pointer moved beyond click threshold
102        var dragStartX = 0;
103        var dragStartScrollLeft = 0;
104        var capturedPointerId = null;
105        var DRAG_THRESHOLD = 5; // px — movement below this counts as a click, not a drag
106
107        track.addEventListener('pointerdown', function(e) {
108            if (!section.classList.contains('has-scroll')) return;
109            // Custom drag only for mouse/pen; touch keeps native momentum scroll
110            if (e.pointerType !== 'mouse' && e.pointerType !== 'pen') return;
111
112            // Prevent native image/link drag so our custom drag works over logos
113            e.preventDefault();
114
115            isDragging = true;
116            didDrag = false;
117            dragStartX = e.clientX;
118            dragStartScrollLeft = track.scrollLeft;
119            capturedPointerId = e.pointerId;
120            try { track.setPointerCapture(e.pointerId); } catch (_) {}
121            section.classList.add('is-dragging');
122        });
123
124        track.addEventListener('pointermove', function(e) {
125            if (!isDragging) return;
126            if (!didDrag && Math.abs(e.clientX - dragStartX) > DRAG_THRESHOLD) {
127                didDrag = true;
128            }
129            track.scrollLeft = dragStartScrollLeft - (e.clientX - dragStartX);
130        });
131
132        function endDrag() {
133            if (!isDragging) return;
134            isDragging = false;
135            if (capturedPointerId !== null) {
136                try { track.releasePointerCapture(capturedPointerId); } catch (_) {}
137                capturedPointerId = null;
138            }
139            section.classList.remove('is-dragging');
140        }
141
142        track.addEventListener('pointerup', endDrag);
143        track.addEventListener('pointercancel', endDrag);
144
145        // Block link navigation when the pointer was dragged, allow on simple clicks.
146        // Also: since preventDefault on pointerdown suppresses the native click event,
147        // synthesize a click for simple taps (no drag) so logo links still work.
148        track.addEventListener('pointerup', function(e) {
149            if (e.pointerType !== 'mouse' && e.pointerType !== 'pen') return;
150            if (didDrag) return; // real drag — do nothing, link stays blocked
151
152            // Simple click — find the nearest <a> under the pointer and navigate
153            var target = document.elementFromPoint(e.clientX, e.clientY);
154            if (!target) return;
155            var link = target.closest('.logo-row-section__logo-link');
156            if (link) {
157                link.click();
158            }
159        });
160
161        // ---- Scroll & resize listeners ----
162
163        track.addEventListener('scroll', checkScrollState, { passive: true });
164
165        var resizeTimer;
166        window.addEventListener('resize', function() {
167            clearTimeout(resizeTimer);
168            resizeTimer = setTimeout(function() {
169                checkScrollState();
170                // If layout now fits without scroll, halt autoplay.
171                // NOTE: We do NOT remove existing clones on resize — accepted limitation for MVP.
172                if (!section.classList.contains('has-scroll')) {
173                    stopAutoplay();
174                    section.classList.remove('is-autoplaying');
175                }
176            }, 150);
177        }, { passive: true });
178
179        // ---- Init after images load ----
180
181        function init() {
182            checkScrollState();
183
184            var hasScroll = section.classList.contains('has-scroll');
185
186            if (hasScroll && autoplay && !reducedMotion) {
187                // Clone every <li> for seamless loop
188                var items = logos.querySelectorAll('.logo-row-section__logo-item');
189                items.forEach(function(item) {
190                    var clone = item.cloneNode(true);
191                    clone.setAttribute('data-logo-clone', 'true');
192                    clone.setAttribute('aria-hidden', 'true');
193                    var links = clone.querySelectorAll('a');
194                    links.forEach(function(a) { a.setAttribute('tabindex', '-1'); });
195                    logos.appendChild(clone);
196                });
197
198                originalContentWidth = logos.scrollWidth / 2;
199                section.classList.add('is-autoplaying');
200                startAutoplay();
201
202                // ---- Pause/resume wiring (only when autoplay is active) ----
203
204                track.addEventListener('pointerdown', function() {
205                    pauseAutoplay();
206                });
207
208                track.addEventListener('pointerenter', function(e) {
209                    if (e.pointerType === 'mouse') {
210                        pauseAutoplay();
211                    }
212                });
213
214                section.addEventListener('focusin', function() {
215                    pauseAutoplay();
216                });
217
218                track.addEventListener('pointerup', function() {
219                    resumeAutoplay(RESUME_DELAY_AFTER_POINTER);
220                });
221
222                track.addEventListener('pointercancel', function() {
223                    resumeAutoplay(RESUME_DELAY_AFTER_POINTER);
224                });
225
226                track.addEventListener('pointerleave', function(e) {
227                    if (e.pointerType === 'mouse') {
228                        resumeAutoplay(RESUME_DELAY_AFTER_HOVER_OR_FOCUS);
229                    }
230                });
231
232                section.addEventListener('focusout', function() {
233                    resumeAutoplay(RESUME_DELAY_AFTER_HOVER_OR_FOCUS);
234                });
235            }
236        }
237
238        var images = logos.querySelectorAll('img');
239        var loaded = 0;
240        if (images.length === 0) {
241            init();
242        } else {
243            images.forEach(function(img) {
244                if (img.complete) {
245                    loaded++;
246                    if (loaded === images.length) init();
247                } else {
248                    img.addEventListener('load', function() {
249                        loaded++;
250                        if (loaded === images.length) init();
251                    });
252                }
253            });
254        }
255    }
256
257    document.addEventListener('DOMContentLoaded', function() {
258        document.querySelectorAll('.logo-row-section').forEach(initLogoRow);
259    });
260})();

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.