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.