PageSourceSearch

https://volleybretagne.fr/media/templates/site/lbvb/js/template.js?87c2d1

js volleybretagne.fr collected 2026-10-02 00:02:21 UTC 12,692 bytes, 334 lines download raw bytes

1/**
2 * LBVB template behaviours.
3 *
4 * Preloader, sticky header,
5 * off-canvas menu, mega-menu keyboard/touch support, header search and
6 * scroll-to-top. Vanilla JS, no jQuery.
7 *
8 * @copyright  (C) 2026 Ligue de Bretagne de Volley-Ball
9 * @license    GNU General Public License version 2 or later
10 */
11(function () {
12    'use strict';
13
14    var doc = document;
15    var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
16
17    function on(el, ev, fn) {
18        if (el) { el.addEventListener(ev, fn); }
19    }
20
21    /* -------------------------------------------------- Preloader */
22
23    function hidePreloader() {
24        var pre = doc.querySelector('.sp-pre-loader');
25        if (!pre) { return; }
26        pre.classList.add('is-hidden');
27        window.setTimeout(function () {
28            if (pre.parentNode) { pre.parentNode.removeChild(pre); }
29        }, reduceMotion ? 0 : 400);
30    }
31
32    window.addEventListener('load', hidePreloader);
33    // Safety net: never let a stalled asset trap the page behind the loader.
34    window.setTimeout(hidePreloader, 5000);
35
36    /* -------------------------------------------------- Sticky header */
37
38    (function stickyHeader() {
39        var header = doc.getElementById('sp-header');
40        if (!header || !doc.body.classList.contains('has-sticky-header')) { return; }
41
42        // `header-sticky` is what template.css turns into position:fixed. It must only
43        // be applied once the page has scrolled past the header, otherwise the header
44        // is lifted out of flow on load and overlaps the first section.
45        var offset  = header.offsetTop;
46        var ticking = false;
47        var spacer  = null;
48
49        function ensureSpacer(h) {
50            if (!spacer) {
51                spacer = doc.createElement('div');
52                spacer.className = 'sp-header-spacer';
53                header.parentNode.insertBefore(spacer, header.nextSibling);
54            }
55            spacer.style.height = h + 'px';
56        }
57
58        function update() {
59            var stuck = window.pageYOffset > offset + 10;
60
61            if (stuck && !header.classList.contains('header-sticky')) {
62                // Reserve the space the header is about to vacate, so nothing jumps.
63                ensureSpacer(header.getBoundingClientRect().height);
64                header.classList.add('header-sticky');
65            } else if (!stuck && header.classList.contains('header-sticky')) {
66                header.classList.remove('header-sticky');
67                if (spacer) { spacer.style.height = '0px'; }
68            }
69            ticking = false;
70        }
71
72        window.addEventListener('scroll', function () {
73            if (!ticking) {
74                window.requestAnimationFrame(update);
75                ticking = true;
76            }
77        }, { passive: true });
78
79        update();
80    }());
81
82    /* -------------------------------------------------- Off-canvas menu */
83
84    (function offcanvas() {
85        var toggler = doc.getElementById('offcanvas-toggler');
86        var menu    = doc.getElementById('offcanvas-menu') || doc.querySelector('.offcanvas-menu');
87        var overlay = doc.querySelector('.offcanvas-overlay');
88        var closeEl = doc.querySelector('.close-offcanvas');
89
90        if (!toggler || !menu) { return; }
91
92        var lastFocused = null;
93
94        function open(e) {
95            if (e) { e.preventDefault(); }
96            lastFocused = doc.activeElement;
97            doc.body.classList.add('offcanvas-active');
98            menu.classList.add('active');
99            if (overlay) { overlay.hidden = false; }
100            toggler.setAttribute('aria-expanded', 'true');
101            var focusable = menu.querySelector('a, button, input');
102            if (focusable) { focusable.focus(); }
103        }
104
105        function close(e) {
106            if (e) { e.preventDefault(); }
107            doc.body.classList.remove('offcanvas-active');
108            menu.classList.remove('active');
109            if (overlay) { overlay.hidden = true; }
110            toggler.setAttribute('aria-expanded', 'false');
111            if (lastFocused) { lastFocused.focus(); }
112        }
113
114        on(toggler, 'click', function (e) {
115            if (doc.body.classList.contains('offcanvas-active')) { close(e); } else { open(e); }
116        });
117        on(overlay, 'click', close);
118        on(closeEl, 'click', close);
119
120        on(doc, 'keydown', function (e) {
121            if (e.key === 'Escape' && doc.body.classList.contains('offcanvas-active')) { close(); }
122        });
123
124        // Expand/collapse sub-levels inside the off-canvas menu.
125        Array.prototype.forEach.call(menu.querySelectorAll('.offcanvas-menu-toggler'), function (btn) {
126            on(btn, 'click', function (e) {
127                e.preventDefault();
128                var target = doc.querySelector(btn.getAttribute('data-bs-target'));
129                if (!target) { return; }
130                var isOpen = target.classList.toggle('show');
131                btn.classList.toggle('collapsed', !isOpen);
132                btn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
133            });
134        });
135    }());
136
137    /* -------------------------------------------------- Mega menu */
138
139    (function megamenu() {
140        var items = doc.querySelectorAll('.sp-megamenu-parent .sp-has-child');
141        if (!items.length) { return; }
142
143        var isTouch = window.matchMedia('(hover: none)').matches;
144
145        Array.prototype.forEach.call(items, function (li) {
146            var link = li.querySelector(':scope > a');
147            var panel = li.querySelector(':scope > .sp-dropdown');
148            if (!link || !panel) { return; }
149
150            link.setAttribute('aria-haspopup', 'true');
151            link.setAttribute('aria-expanded', 'false');
152
153            function setOpen(state) {
154                li.classList.toggle('menu-justify', false);
155                li.classList.toggle('is-open', state);
156                link.setAttribute('aria-expanded', state ? 'true' : 'false');
157            }
158
159            // Keyboard: open on focus within, close on blur out.
160            on(li, 'focusin', function () { setOpen(true); });
161            on(li, 'focusout', function () {
162                window.setTimeout(function () {
163                    if (!li.contains(doc.activeElement)) { setOpen(false); }
164                }, 0);
165            });
166
167            // Touch: first tap opens, second follows the link.
168            if (isTouch) {
169                on(link, 'click', function (e) {
170                    if (!li.classList.contains('is-open')) {
171                        e.preventDefault();
172                        setOpen(true);
173                    }
174                });
175            }
176
177            on(doc, 'keydown', function (e) {
178                if (e.key === 'Escape' && li.classList.contains('is-open')) {
179                    setOpen(false);
180                    link.focus();
181                }
182            });
183        });
184
185        // Close any open panel when clicking elsewhere.
186        on(doc, 'click', function (e) {
187            Array.prototype.forEach.call(items, function (li) {
188                if (!li.contains(e.target)) {
189                    li.classList.remove('is-open');
190                    var a = li.querySelector(':scope > a');
191                    if (a) { a.setAttribute('aria-expanded', 'false'); }
192                }
193            });
194        });
195    }());
196
197    /* -------------------------------------------------- Header search */
198
199    (function headerSearch() {
200        var toggler = doc.querySelector('.header-search-toggler');
201        var panel   = doc.getElementById('sp-header-search');
202        if (!toggler || !panel) { return; }
203
204        on(toggler, 'click', function () {
205            var open = panel.hidden;
206            panel.hidden = !open;
207            toggler.setAttribute('aria-expanded', open ? 'true' : 'false');
208            if (open) {
209                var field = panel.querySelector('input[type="search"], input[type="text"]');
210                if (field) { field.focus(); }
211            }
212        });
213    }());
214
215    /* -------------------------------------------------- Article actions */
216
217    (function articleActions() {
218        var copyBtn  = doc.querySelector('.js-copy-link');
219        var printBtn = doc.querySelector('.js-print');
220
221        function toast(msg) {
222            var el = doc.createElement('div');
223            el.className = 'lbvb-toast';
224            el.setAttribute('role', 'status');
225            el.textContent = msg;
226            doc.body.appendChild(el);
227            // next frame so the transition runs
228            window.requestAnimationFrame(function () { el.classList.add('is-visible'); });
229            window.setTimeout(function () {
230                el.classList.remove('is-visible');
231                window.setTimeout(function () { el.remove(); }, 250);
232            }, 2000);
233        }
234
235        on(copyBtn, 'click', function () {
236            var url = copyBtn.getAttribute('data-url') || window.location.href;
237            var done = function () {
238                copyBtn.classList.add('is-done');
239                toast(copyBtn.getAttribute('data-done') || 'OK');
240                window.setTimeout(function () { copyBtn.classList.remove('is-done'); }, 2000);
241            };
242
243            if (navigator.clipboard && window.isSecureContext) {
244                navigator.clipboard.writeText(url).then(done).catch(function () { legacy(url, done); });
245            } else {
246                legacy(url, done);
247            }
248        });
249
250        function legacy(text, cb) {
251            var ta = doc.createElement('textarea');
252            ta.value = text;
253            ta.setAttribute('readonly', '');
254            ta.style.cssText = 'position:absolute;left:-9999px';
255            doc.body.appendChild(ta);
256            ta.select();
257            try { doc.execCommand('copy'); cb(); } catch (e) { /* nothing to do */ }
258            ta.remove();
259        }
260
261        on(printBtn, 'click', function () { window.print(); });
262    }());
263
264    /* -------------------------------------------------- Bottom banner offset */
265
266    (function bottomBannerOffset() {
267        // Consent banners dock at the bottom of the viewport and overlap the
268        // back-to-top button. Flag the body and publish the banner height so
269        // CSS can lift the button clear of it.
270        function measure() {
271            var banner = null;
272            var candidates = doc.querySelectorAll('body > div, body > section, body > aside');
273
274            Array.prototype.forEach.call(candidates, function (el) {
275                if (banner) { return; }
276                var cs = window.getComputedStyle(el);
277                if (cs.position !== 'fixed' || cs.display === 'none' || cs.visibility === 'hidden') { return; }
278                var r = el.getBoundingClientRect();
279                // Docked to the bottom, spanning most of the width, and not tall.
280                if (r.height > 0 && r.width > window.innerWidth * 0.6 &&
281                    Math.abs(r.bottom - window.innerHeight) < 4 && r.height < window.innerHeight * 0.6) {
282                    banner = el;
283                }
284            });
285
286            if (banner) {
287                doc.body.classList.add('has-bottom-banner');
288                doc.documentElement.style.setProperty(
289                    '--lbvb-bottom-banner',
290                    Math.round(banner.getBoundingClientRect().height) + 'px'
291                );
292            } else {
293                doc.body.classList.remove('has-bottom-banner');
294                doc.documentElement.style.removeProperty('--lbvb-bottom-banner');
295            }
296        }
297
298        measure();
299        window.setTimeout(measure, 1200);   // banners often inject late
300        window.addEventListener('resize', measure, { passive: true });
301
302        if (window.MutationObserver) {
303            new MutationObserver(measure).observe(doc.body, { childList: true, subtree: false });
304        }
305    }());
306
307    /* -------------------------------------------------- Scroll to top */
308
309    (function scrollUp() {
310        var btn = doc.querySelector('.sp-scroll-up');
311        if (!btn) { return; }
312
313        var ticking = false;
314
315        function update() {
316            btn.style.display = window.pageYOffset > 300 ? 'flex' : 'none';
317            ticking = false;
318        }
319
320        window.addEventListener('scroll', function () {
321            if (!ticking) {
322                window.requestAnimationFrame(update);
323                ticking = true;
324            }
325        }, { passive: true });
326
327        on(btn, 'click', function (e) {
328            e.preventDefault();
329            window.scrollTo({ top: 0, behavior: reduceMotion ? 'auto' : 'smooth' });
330        });
331
332        update();
333    }());
334}());

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.