PageSourceSearch

https://systematic-paris-region.org/wp-content/themes/kn/js/kn_scripts.js

js systematic-paris-region.org collected 2026-10-01 11:28:37 UTC 35,299 bytes, 825 lines download raw bytes

1/******************************************************************************************************/
2/* Animations GSAP du site */
3/******************************************************************************************************/
4let smoother;
5
6window.addEventListener("load", () => {
7    smoother = ScrollSmoother.create({
8        smooth: 1,
9        effects: true,
10        smoothTouch: 0.1,
11    });
12});
13
14document.addEventListener("DOMContentLoaded", function () {
15    // Gestion des ancres
16    document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
17        anchor.addEventListener("click", function (e) {
18            // Ne pas intercepter les clics dans les formulaires Gravity Forms
19            if (this.closest('.gform_wrapper') || this.closest('.gform_page')) return;
20            // Ne pas intercepter les déclencheurs de modale
21            if (this.hasAttribute('data-modal-open')) return;
22
23            e.preventDefault();
24            const targetId = this.getAttribute("href");
25
26            if (targetId !== "#") {
27                const target = document.querySelector(targetId);
28                if (target) {
29                    smoother.scrollTo(target, true, "top top+=250");
30                }
31            }
32        });
33    });
34});
35
36document.addEventListener("DOMContentLoaded", function () {
37    const adminBar = document.getElementById("wpadminbar");
38    if (adminBar) {
39        document.body.insertBefore(adminBar, document.body.firstChild);
40    }
41});
42
43/******************************************************************************************************/
44/* Filtres événements */
45/*******************************************************************************************************/
46
47document.addEventListener("DOMContentLoaded", () => {
48    console.log('test ultime filtres v1.2');
49
50    //////////////////////////////////////////////////////////////////////////////////////////////////////////////
51    // Toggle des filtres collapsibles (Thématiques, etc.)
52    document.querySelectorAll('.filter--collapsible .filter__toggle').forEach((btn) => {
53        btn.addEventListener('click', () => {
54            const wrapper = btn.closest('.filter--collapsible');
55            if (!wrapper) return;
56            const isCollapsed = wrapper.classList.toggle('is-collapsed');
57            btn.setAttribute('aria-expanded', isCollapsed ? 'false' : 'true');
58
59            // Ouvrir/fermer le panneau change la hauteur des filtres. Le pin sticky a été
60            // calculé sur l'ancienne hauteur : sans recalcul, les filtres dépassent sur le
61            // footer. On rafraîchit ScrollTrigger une fois la transition max-height terminée.
62            const panel = wrapper.querySelector('.filter__panel');
63            if (panel && typeof ScrollTrigger !== 'undefined') {
64                let done = false;
65                const refresh = () => { if (done) return; done = true; ScrollTrigger.refresh(); };
66                panel.addEventListener('transitionend', function te(e) {
67                    if (e.propertyName === 'max-height') {
68                        panel.removeEventListener('transitionend', te);
69                        refresh();
70                    }
71                });
72                setTimeout(refresh, 450); // fallback si transitionend ne se déclenche pas
73            }
74        });
75    });
76
77    //////////////////////////////////////////////////////////////////////////////////////////////////////////////
78    // Formulaire de recherche par filtres
79    const initialEvents = document.querySelector(".initial_events");
80    const initialEventsBackup = initialEvents ? initialEvents.innerHTML : '';
81    const formFilter = document.getElementById('filter_event');
82    const resultsContainer = document.getElementById("event_post_results");
83    // Filtres des thématiques
84    const filtersThematique = document.querySelectorAll('.choices_thematiques .input_thematique');
85    // Filtres des localisations
86    const filtersLocalisation = document.querySelectorAll('.choices_localisation .input_localisation');
87    // Filtres des organisateurs
88    const filtersOrganisateur = document.querySelectorAll('.choices_organisateur .input_organisateur');
89    // Filtres type_news (actualités)
90    const filtersTypeNews = document.querySelectorAll('.choices_type_news .input_type_news');
91    const eventsToGo = document.getElementById("togo_events");
92    const eventsPast = document.getElementById("past_events");
93    const formSearch = document.getElementById('searchform_event');
94
95    function scrollToEventResults() {
96        const target = document.querySelector('.container.event');
97        if (!target) return;
98        const headerH = (document.querySelector('.header')?.offsetHeight ?? 100) + 20;
99        if (smoother) {
100            smoother.scrollTo(target, true, 'top top+=' + headerH);
101        } else {
102            const top = target.getBoundingClientRect().top + window.scrollY - headerH;
103            window.scrollTo({ top, behavior: 'smooth' });
104        }
105    }
106
107    function fetchFilteredEvents(pagedOverride) {
108        let allFiltersCheck = [];
109
110        filtersThematique.forEach((filterThematiqueCheck) => {
111            // Vérifier quelle thématique est choisie (cochée) et récupérer son id 
112            if (filterThematiqueCheck.checked) {
113                allFiltersCheck.push(filterThematiqueCheck.id);
114            }
115        });
116
117        const dateStart = document.getElementById('date_start').value;
118        const dateEnd = document.getElementById('date_end').value;
119
120        let allLocalisation = [];
121
122        filtersLocalisation.forEach((localisation) => {
123            if (localisation.checked) {
124                allLocalisation.push(localisation.value);
125            }
126        });
127
128        let allOrganisateur = [];
129
130        filtersOrganisateur.forEach((orga) => {
131            if (orga.checked) {
132                allOrganisateur.push(orga.value);
133            }
134        });
135
136        let allTypeNews = [];
137
138        filtersTypeNews.forEach((tn) => {
139            if (tn.checked) {
140                allTypeNews.push(tn.value);
141            }
142        });
143
144        // Recherche par mot-clé (intégrée aux filtres)
145        const searchInput = document.querySelector('#searchform_event input[name="s"]');
146        const searchValue = searchInput ? searchInput.value.trim() : '';
147
148        // Rassembler toute la data
149        const data = new FormData();
150        // L'envoyer à la requête AJAX
151        data.append('action', 'filter_events')
152        // Post type cible (events par défaut, "appel-a-projets" si présent dans le form)
153        const postTypeInput = document.querySelector('#filter_event input[name="post_type_filter"]');
154        data.append('post_type_filter', postTypeInput ? postTypeInput.value : 'evenement');
155        // Mot-clé de recherche (filtre WP_Query `s`)
156        if (searchValue) data.append('search', searchValue);
157        // Boucler sur tous les id des thématiques choisies
158        allFiltersCheck.forEach((valueFilter) => {
159            data.append('choiceThematique[]', valueFilter);
160        })
161        data.append('dateStart', dateStart);
162        data.append('dateEnd', dateEnd);
163        // Boucler sur tous les id des localisations choisies
164        allLocalisation.forEach((valueLoc) => {
165            data.append('choiceLocalisation[]', valueLoc);
166        });
167        // Boucler sur tous les organisateurs choisis
168        allOrganisateur.forEach((valueOrga) => {
169            data.append('choiceOrganisateur[]', valueOrga);
170        });
171        // Boucler sur les types d'actualité choisis
172        allTypeNews.forEach((valueTn) => {
173            data.append('choiceTypeNews[]', valueTn);
174        });
175        // Pagination : par défaut 1 (nouvelle recherche), sinon la page demandée
176        data.append('paged', pagedOverride ? Math.max(1, parseInt(pagedOverride, 10) || 1) : 1);
177        // Ajouter les valeurs des checkbox "événements à venir" et "événements passés"
178        // (absentes sur le template Actualités → on envoie false par défaut)
179        data.append('eventsToGo', eventsToGo ? eventsToGo.checked : false);
180        data.append('eventsPast', eventsPast ? eventsPast.checked : false);
181
182        // Loader (min. 500 ms de visibilité pour ne pas clignoter sur les réponses rapides)
183        const loaderStart = performance.now();
184        showFilterLoader(true);
185
186        fetch("/wp-admin/admin-ajax.php", {
187            method: 'POST',
188            body: data,
189        })
190        .then(response => response.json())
191        .then(response => {
192            if (response.success) {
193                document.getElementById("event_post_results").innerHTML = response.data.html;
194                initialEvents.innerHTML = '';
195
196                if (window.innerWidth >= 992) {
197                    ScrollTrigger.refresh();
198                }
199
200                scrollToEventResults();
201            }
202        })
203        .catch(() => {})
204        .finally(() => {
205            const elapsed = performance.now() - loaderStart;
206            const remaining = Math.max(0, 500 - elapsed);
207            setTimeout(() => showFilterLoader(false), remaining);
208        });
209    };
210
211    // ── Loader visuel pendant la requête AJAX de filtrage ──
212    function showFilterLoader(show) {
213        let loader = document.getElementById('kn-filter-loader');
214        if (!loader) {
215            loader = document.createElement('div');
216            loader.id = 'kn-filter-loader';
217            loader.setAttribute('aria-hidden', 'true');
218            loader.setAttribute('role', 'status');
219            loader.innerHTML = '<div class="kn-filter-loader__spinner" aria-label="Chargement..."></div>';
220            // Cible : le conteneur de résultats (visible dès qu'un f
220iltre a été appliqué),
221            // sinon le bloc .cpt qui l'englobe.
222            const anchor = document.getElementById('event_post_results')
223                || document.querySelector('.cpt')
224                || document.querySelector('.container.event')
225                || document.body;
226            const cs = window.getComputedStyle(anchor);
227            if (cs.position === 'static') anchor.style.position = 'relative';
228            anchor.appendChild(loader);
229        }
230        loader.classList.toggle('is-active', !!show);
231        loader.setAttribute('aria-hidden', show ? 'false' : 'true');
232    }
233    
234    // Toggle "événements à venir" / "appels en cours"
235    if (eventsToGo) {
236        eventsToGo.addEventListener("change", () => {
237            if (eventsToGo.checked) {
238                if (eventsPast) {
239                    eventsPast.checked = false;
240                    eventsPast.classList.remove("active");
241                }
242                eventsToGo.classList.add("active");
243                fetchFilteredEvents();
244            } else {
245                eventsToGo.classList.remove("active");
246                resultsContainer.innerHTML = '';
247                initialEvents.innerHTML = initialEventsBackup;
248            }
249        });
250    }
251
252    //////////////////////////////////////////////////////////////////////////////////////////////////////////////
253    // Toggle "événements passés" / "appels clôturés"
254
255    if (eventsPast) {
256        eventsPast.addEventListener("change", () => {
257            if (eventsPast.checked) {
258                if (eventsToGo) {
259                    eventsToGo.checked = false;
260                    eventsToGo.classList.remove("active");
261                }
262                eventsPast.classList.add("active");
263                fetchFilteredEvents();
264            } else {
265                eventsPast.classList.remove("active");
266                resultsContainer.innerHTML = '';
267                initialEvents.innerHTML = initialEventsBackup;
268            }
269        });
270    }
271
272    if (formFilter) {
273
274        formFilter.addEventListener("submit", function (e) {
275            e.preventDefault();
276            fetchFilteredEvents();
277        });
278
279        // Auto-submit à chaque changement d'un filtre (checkbox / date / select) —
280        // s'applique aux actualités, événements et appels-à-projets qui partagent ce formulaire.
281        const debounce = (fn, delay) => {
282            let t;
283            return function () { clearTimeout(t); t = setTimeout(fn, delay); };
284        };
285        const autoSubmit = debounce(() => fetchFilteredEvents(), 100);
286        formFilter.addEventListener('change', function (e) {
287            if (!e.target.matches('input, select')) return;
288            autoSubmit();
289        });
290    }
291
292    // Pagination AJAX : intercepte les clics sur les liens WP-PageNavi rendus dans le container résultats
293    // (sans ça, les liens pointent vers /wp-admin/admin-ajax.php?paged=N → page blanche)
294    if (resultsContainer) {
295        resultsContainer.addEventListener('click', function (e) {
296            const link = e.target.closest('a');
297            if (!link) return;
298            if (!link.closest('.wp-pagenavi, .pagination, nav.navigation')) return;
299            e.preventDefault();
300            // Extrait le paged depuis l'URL du lien (querystring ?paged=N ou /page/N/)
301            let paged = 1;
302            try {
303                const url = new URL(link.href, window.location.origin);
304                const p = url.searchParams.get('paged') || url.searchParams.get('page');
305                if (p) paged = parseInt(p, 10) || 1;
306                if (paged === 1) {
307                    const m = url.pathname.match(/\/page\/(\d+)/);
308                    if (m) paged = parseInt(m[1], 10) || 1;
309                }
310            } catch (err) {}
311            fetchFilteredEvents(paged);
312        });
313    }
314
315    //////////////////////////////////////////////////////////////////////////////////////////////////////////////
316    // Formulaire de recherche par mots-clés → combine avec les autres filtres
317
318    if (formSearch) {
319        formSearch.addEventListener("submit", function (e) {
320            e.preventDefault();
321            fetchFilteredEvents();
322        });
323    }
324
325    //////////////////////////////////////////////////////////////////////////////////////////////////////////////
326    // Réinitialisation des filtres et de la recherche
327    const resetBtn = document.getElementById("reset_search");
328
329    if (resetBtn) {
330        resetBtn.addEventListener("click", function () {
331
332            // Reset form search
333            if (formSearch) {
334                formSearch.querySelector('input[name="s"]').value = "";
335            }
336
337            // Reset checkbox filters thématiques / localisation / organisateur / type_news
338            document.querySelectorAll('.input_thematique, .input_localisation, .input_organisateur, .input_type_news').forEach(input => {
339                input.checked = false;
340            });
341
342            // Reset date pickers
343            document.getElementById("date_start").value = "";
344            document.getElementById("date_end").value = "";
345
346            // Reset checkbox events togo/pasts (absentes sur le template Actualités)
347            if (eventsToGo) {
348                eventsToGo.checked = true;
349                eventsToGo.classList.add("active");
350            }
351            if (eventsPast) {
352                eventsPast.checked = false;
353                eventsPast.classList.remove("active");
354            }
355
356            // Recharger les événements initiaux
357            resultsContainer.innerHTML = '';
358            initialEvents.innerHTML = initialEventsBackup;
359        });
360    }
361});
362
363/******************************************************************************************************/
364/* Sticky des filtres */
365/*******************************************************************************************************/
366
367
368if (window.innerWidth >= 992) {
369    window.addEventListener("load", () => {
370        const filters = document.querySelector(".wrapper_filter .filters");
371        const container = document.querySelector('.container.event');
372
373        if (filters && container) {
374            // Marge entre le bas des filtres et le bas du viewport (en px)
375            const bottomMargin = 20;
376
377            // Si les filtres sont plus hauts que le viewport, on revient à un pin top (sinon plus rien n'est visible)
378            // Sinon on pin de telle sorte que le BAS des filtres soit collé au bas du viewport
379            // Offset sous le header fixe (px)
380            const topOffset = 115;
381            const buildStart = () => {
382                const filtersHeight = filters.offsetHeight;
383                const vh            = window.innerHeight;
384                // Cas normal : les filtres tiennent sous le header → on les épingle EN HAUT.
385                if (filtersHeight + topOffset + bottomMargin <= vh) {
386                    return `top top+=${topOffset}`;
387                }
388                // Filtres plus hauts que l'espace dispo (ex. thématiques ouvertes) →
389                // on aligne leur BAS sur le bas du viewport pour garder le bas visible.
390                return `top top+=${vh - filtersHeight - bottomMargin}`;
391            };
392
393            ScrollTrigger.create({
394                trigger: ".wrapper_filter",
395                start: buildStart,
396                // Borné à 0 : si les filtres sont aussi hauts que la colonne de contenu, pas de
397                // pin (sinon ils débordent sur le footer).
398                end: () => `+=${Math.max(0, container.offsetHeight - filters.offsetHeight)}`,
399                pin: filters,
400                pinSpacing: false,
401                scroller: ScrollSmoother.get()?.wrapper() ?? window,
402                invalidateOnRefresh: true,
403                onEnter: () => filters.classList.add("is-pinned"),
404                onLeaveBack: () => filters.classList.remove("is-pinned"),
405                onLeave: () => filters.classList.remove("is-pinned"),
406            });
407        }
408    });
409};
410
411/******************************************************************************************************/
412/* Animation galerie */
413/*******************************************************************************************************/
414
415// document.addEventListener("DOMContentLoaded", () => {
416//     const logos = gsap.utils.toArray(".galerie_logo");
417//     if (!logos.length) return;
418
419//     const setters = logos.map((el) => gsap.quickSetter(el, "y", "px"));
420
421//     const mm = gsap.matchMedia();
422//     mm.add(
423//         {
424//             isMobile: "(max-width: 768px)",
425//             isDesktop: "(min-width: 769px)",
426//         },
427//         (ctx) => {
428//             const amplitude = ctx.conditions.isMobile ? 30 : 60;
429
430//             const dirs = logos.map((_, i) => {
431//                 const col = i % 4;
432//                 return (col === 0 || col === 2) ? -amplitude : amplitude;
433//             });
434
435//             ScrollTrigger.create({
436//                 trigger: ".galerie_logos",
437//                 start: "center bottom",
438//                 end: "bottom top",
439//                 scrub: 0.2,
440//                 onUpdate: (self) => {
441//                     const p = self.progress;
442//                     for (let i = 0; i < logos.length; i++) setters[i](dirs[i] * p);
443//                 },
444//             });
445
446//             return () => setters.forEach((s) => s(0));
447//         }
448//     );
449// });
450
451/******************************************************************************************************/
452/* Animation chiffres clés 2cols */
453/******************************************************************************************************/
454
455window.addEventListener("DOMContentLoaded", () => {
456    const els = document.querySelectorAll(".top .two_cols_chiffre_valeur");
457    if (!els.length) return;
458
459    els.forEach((el) => {
460        const raw = el.textContent.trim();
461        const hasDecimal = raw.includes(",");
462        const decimalCount = hasDecimal ? raw.split(",")[1].length : 0;
463        const end = parseFloat(raw.replace(",", "."));
464        const setText = gsap.quickSetter(el, "textContent");
465
466        const obj = { v: 0 };
467        setText("0");
468
469        gsap.to(obj, {
470            v: end,
471            duration: 1,
472            ease: "power2.out",
473            scrollTrigger: {
474                trigger: el,
475                start: "top bottom",
476                once: true,
477            },
478            onUpdate: () => {
479                const factor = Math.pow(10, decimalCount);
480                const val = Math.round(obj.v * factor) / factor;
481                const formatted = val.toFixed(decimalCount).replace(".", ",");
482                setText(formatted.replace(/\B(?=(\d{3})+(?!\d))/g, " "));
483            },
484        });
485    });
486});
487
488
489/******************************************************************************************************/
490/* Slider splidejs */
491/******************************************************************************************************/
492
493document.addEventListener("DOMContentLoaded", function () {
494    // console.log('Test v1.1 slider');
495    const sliders = document.querySelectorAll(".wrapper_slider");
496
497    // console.log("teeeest");
498    sliders.forEach((slider) => {
499        const perPage = slider.dataset.perPage;
500        const perMove = slider.dataset.perMove;
501
502        let config = {
503            type: "loop",
504            perPage: Number(perPage),
505            perMove: Number(perMove),
506            autoplay: true,
507            gap: '1em', // Attention si cette valeur est changé -> à changer aussi dans le css cpt.css teaser__item
508            speed: 800,
509            pauseOnHover: false,
510            pagination: false,
511            arrows: true,
512            breakpoints: {
513                768: {
514                    perPage: 1,
515                },
516                992: {
517                    perPage: 2,
518                },
519                1200: {
520                    perPage: Number(perPage),
521                }
522            },
523        };
524
525        const splideInstance = new Splide(slider, config).mount();
526        slider.style.setProperty('--splide-per-page', perPage);
527
528        // Lien entre les onglets AAP et le slider parent (filtre par zone géographique)
529        const aapWrapper = slider.closest('.aap-zone-filter');
530        if (aapWrapper && !aapWrapper.dataset.splideLinked) {
531            aapWrapper.dataset.splideLinked = '1';
532            aapWrapper._splide       = splideInstance;
533            aapWrapper._splideConfig = config;
534            aapWrapper._splideEl     = slider;
535            // Mémorise toutes les slides d'origine pour pouvoir restaurer dynamiquement
536            aapWrapper._allSlides = Array.from(slider.querySelectorAll('.splide__list > .splide__slide')).map((s) => s.cloneNode(true));
537        }
538    });
539
540    // ── Onglets AAP : multi-toggle par zone géographique ──────────────────────
541    document.querySelectorAll('.aap-zone-filter').forEach((wrapper) => {
542        const tabs = wrapper.querySelectorAll('.aap-zone-tab');
543
544        function rebuildSlider(activeZones) {
545            if (!wrapper._splideEl || !wrapper._allSlides) return;
546
547            const list = wrapper._splideEl.querySelector('.splide__list');
548            if (!list) return;
549
550            const showAll  = activeZones.size === 0;
551            const filtered = wrapper._allSlides.filter((s) => {
552                const z = s.getAttribute('data-zone');
553                return showAll || activeZones.has(z);
554            });
555
556            // Détruire l'instance Splide actuelle si elle existe
557            if (wrapper._splide && typeof wrapper._splide.destroy === 'function') {
558                wrapper._splide.destroy(true);
559            }
560
561            // Reconstruire la liste avec uniquement les slides filtrées (clones)
562            list.innerHTML = '';
563            const empty = wrapper.querySelector('.aap-zone-empty');
564            if (filtered.length === 0) {
565                // Aucun résultat : afficher le message vide
566                if (empty) empty.hidden = false;
567                if (wrapper._splideEl) wrapper._splideEl.style.display = 'none';
568                wrapper._splide = null;
569                return;
570            }
571            if (empty) empty.hidden = true;
572            if (wrapper._splideEl) wrapper._splideEl.style.display = '';
573            filtered.forEach((s) => list.appendChild(s.cloneNode(true)));
574
575            // Re-monter Splide + restaurer le CSS variable --splide-per-page
576            // (destroy(true) supprime les styles inline de Splide, dont notre variable)
577            wrapper._splide = new Splide(wrapper._splideEl, wrapper._splideConfig).mount();
578            const desiredPerPage = wrapper._splideConfig.perPage || 3;
579            wrapper._splideEl.style.setProperty('--splide-per-page', desiredPerPage);
580        }
581
582        function applyFilter() {
583            const activeZones = new Set(
584                Array.from(tabs)
585                    .filter((t) => t.classList.contains('is-active'))
586                    .map((t) => t.dataset.zone)
587            );
588            wrapper.setAttribute('data-active-zones', Array.from(activeZones).join(' '));
589            rebuildSlider(activeZones);
590        }
591
592        tabs.forEach((tab) => {
593            tab.addEventListener('click', () => {
594                const currentlyActive = tab.classList.contains('is-active');
595                const activeCount     = wrapper.querySelectorAll('.aap-zone-tab.is-active').length;
596
597                // Comportement exclusif :
598                //   - Si l'onglet cliqué est le SEUL actif → on réactive tout (reset)
599                //   - Sinon → seul l'onglet cliqué reste actif (filtre exclusif)
600                const isResetClick = currentlyActive && activeCount === 1;
601
602                tabs.forEach((t) => {
603                    const active = isResetClick ? true : (t === tab);
604                    t.classList.toggle('is-active', active);
605                    t.setAttribute('aria-pressed', active ? 'true' : 'false');
606                });
607                applyFilter();
608            });
609        });
610    });
611});
612
613// Slider spécifique pour le carrousel
614// document.addEventListener("DOMContentLoaded", function () {
615//     const carrouselSliders = document.querySelectorAll(".carrousel__list");
616
617//     carrouselSliders.forEach((slider_carrousel) => {
618//         const perPage = slider_carrousel.dataset.perPage;
619//         const perMove = slider_carrousel.dataset.perMove;
620//         new Splide(slider_carrousel, {
621//             type: "loop",
622//             perPage: Number(perPage),
623//             perMove: Number(perMove),
624//             drag: "free",
625//             focus: "center",
626//             pagination: false,
627//             arrows: false,
628//             breakpoints: {
629//                 992: {
630//                     perPage: 3,
631//                 },
632//             },
633//             autoScroll: {
634//                 speed: 2,
635//                 pauseOnHover: false,
636//                 pauseOnFocus: false,
637//                 breakpoints: {
638//                     992: {
639//                         speed: 1,
640//                     },
641//                 },
642//             },
643//         }).mount(window.splide.Extensions);
644//     });
645// });
646
647/******************************************************************************************************/
648/* FAQ form */
649/******************************************************************************************************/
650
651document.addEventListener("DOMContentLoaded", function () {
652    const buttonsFaq = document.querySelectorAll(".faq-button");
653
654    buttonsFaq.forEach((bouton) => {
655        bouton.addEventListener("click", function (event) {
656            event.stopPropagation();
657            buttonsFaq.forEach((otherBouton) => {
658                otherBouton.classList.toggle("active", otherBouton === bouton);
659            });
660        });
661    });
662
663    document.addEventListener("click", function (event) {
664        if (!event.target.closest(".faq-button")) {
665            buttonsFaq.forEach((btn) => btn.classList.remove("active"));
666        }
667    });
668});
669
670/******************************************************************************************************/
671/* Accordéons */
672/******************************************************************************************************/
673
674document.addEventListener("DOMContentLoaded", function () {
675    const buttons = document.querySelectorAll(".accordeon:not(.open) .items .accordeon-item");
676
677    buttons.forEach((bouton) => {
678        bouton.addEventListener("click", function (event) {
679            event.stopPropagation();
680            buttons.forEach((otherBouton) => {
681                otherBouton.classList.toggle("active", otherBouton === bouton);
682            });
683        });
684    });
685
686    document.addEventListener("click", function (event) {
687        if (!event.target.closest(".accordeon:not(.open) .items .accordeon-item")) {
688            buttons.forEach((btn) => btn.classList.remove("active"));
689        }
690    });
691});
692
693/******************************************************************************************************/
694/* Onglets */
695/******************************************************************************************************/
696
697document.addEventListener("DOMContentLoaded", function () {
698    const ongletsSection = document.querySelectorAll(".onglets .bloc_tab.tab");
699    
700    ongletsSection.forEach((section) => {
701        const tabButtons = section.querySelectorAll(".tab-button");
702        const tabItems = section.querySelectorAll(".tab-item");
703
704        tabButtons.forEach((button) => {
705            button.addEventListener("click", function () {
706                const targetId = this.getAttribute("data-target");
707                const targetItem = document.getElementById(targetId);
708
709                tabButtons.forEach((btn) => btn.classList.remove("active"));
710                tabItems.forEach((item) => item.classList.remove("active"));
711
712                this.classList.add("active");
713                if (targetItem) {
714                    targetItem.classList.add("active");
715                }
716            });
717        });
718    });
719});
720
721/******************************************************************************************************/
722/* Ajouter une class cta à tous les p parents ayant pour enfant un bouton */
723/* et ajouter une class youtube au p parent d'une iframe youtube */
724/******************************************************************************************************/
725
726document.addEventListener("DOMContentLoaded", function () {
727    [].forEach.call(document.querySelectorAll("[class^=bouton]"), function (el) {
728        if (el.parentNode.nodeName == "P") {
729            el.parentNode.classList.add("cta");
730            el.parentNode.classList.add("cta--p");
731        }
732    });
733
734    [].forEach.call(
735        document.querySelectorAll('iframe[src*="youtube"]'),
736        function (el) {
737            if (el.parentNode.nodeName == "P") {
738                el.parentNode.classList.add("youtube");
739            }
740        }
741    );
742
743    function nl2br(str, replaceMode, isXhtml) {
744        var breakTag = (isXhtml) ? '<br />' : '<br>';
745        var replaceStr = (replaceMode) ? '$1' + breakTag : '$1' + breakTag + '$2';
746        return (str + '').replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, replaceStr);
747    }
748});
749
750/* Placer la timeline form adhésion en bas à gauche du form */
751
752document.addEventListener("DOMContentLoaded", function () {
753    // Libellés ACF (titre + sous-titre) capturés au 1er rendu : 1 entrée par ligne du répéteur
754    let customStepLabels = null;
755
756    function moveSteps() {
757        const form = document.querySelector(".form > .container");
758        const timeline = document.querySelector(".form .infos_company .timeline");
759        const formSteps = document.querySelector(".form__contener .gform_wrapper .gf_page_steps");
760
761        if (!(form && form.classList.contains('title_right') && timeline && formSteps)) return;
762
763        // Capture unique des libellés rendus par form.php, AVANT de vider la timeline.
764        // Une entrée par .etape (donc par ligne du répéteur) : chaîne vide si le titre n'est pas renseigné.
765        if (customStepLabels === null) {
766            customStepLabels = Array.from(timeline.querySelectorAll('.etape')).map((etape) => {
767                const label = etape.querySelector('.gf_step_label');
768                return label ? label.innerHTML.trim() : '';
769            });
770        }
771
772        // Déplacer les étapes natives de Gravity Forms dans la timeline
773        timeline.innerHTML = '';
774        timeline.appendChild(formSteps);
775
776        // Appliquer titre + sous-titre uniquement quand les champs ACF sont renseignés
777        // (1re ligne du répéteur = étape 1, 2e ligne = étape 2, etc.)
778        formSteps.querySelectorAll('.gf_step').forEach((step, index) => {
779            const label = step.querySelector('.gf_step_label');
780            const customLabel = customStepLabels[index];
781            if (label && customLabel) {
782                label.innerHTML = customLabel;
783            }
784        });
785    }
786
787    // Au chargement initial
788    moveSteps();
789
790    // Après chaque navigation de page GF (AJAX)
791    jQuery(document).on('gform_post_render', function () {
792        moveSteps();
793    });
794});
795//////////////////////////////////////////////////////////////////////////////////////////////////////////////
796// Modale newsletter (footer) — ouverture / fermeture
797document.addEventListener('DOMContentLoaded', () => {
798    const openModal = (modal) => {
799        if (!modal) return;
800        modal.classList.add('is-open');
801        modal.setAttribute('aria-hidden', 'false');
802        document.body.classList.add('modal-open');
803        const focusable = modal.querySelector('input, textarea, button:not(.modal__close)');
804        if (focusable) setTimeout(() => focusable.focus(), 50);
805    };
806    const closeModal = (modal) => {
807        if (!modal) return;
808        modal.classList.remove('is-open');
809        modal.setAttribute('aria-hidden', 'true');
810        document.body.classList.remove('modal-open');
811    };
812    document.querySelectorAll('[data-modal-open]').forEach((btn) => {
813        btn.addEventListener('click', (e) => {
814            e.preventDefault();
815            openModal(document.getElementById(btn.dataset.modalOpen));
816        });
817    });
818    document.querySelectorAll('[data-modal-close]').forEach((el) => {
819        el.addEventListener('click', () => closeModal(el.closest('.modal')));
820    });
821    document.addEventListener('keydown', (e) => {
822        if (e.key !== 'Escape') return;
823        document.querySelectorAll('.modal.is-open').forEach(closeModal);
824    });
825});

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.