/** * @file * erabletheme behaviors. */ (function ($, Drupal) { 'use strict'; Drupal.behaviors.erabletheme = { attach: function (context, settings) { // // Bloc en-tête hors colonne (.page-header-outside) : positionné en // absolute dans
, donc on mesure sa hauteur et on l'expose via // une CSS variable pour que
réserve la bonne place en padding-top. // const header_outside = document.querySelector('.page-header-outside'); const main_el = header_outside ? header_outside.closest('main') : null; if (header_outside && main_el && !main_el.dataset.headerOutsideMeasured) { const updateHeaderOutsideHeight = () => { const h = header_outside.getBoundingClientRect().height; main_el.style.setProperty('--page-header-outside-h', h + 'px'); }; updateHeaderOutsideHeight(); window.addEventListener('resize', updateHeaderOutsideHeight); main_el.dataset.headerOutsideMeasured = 'true'; } // // Sidebar projets : marquer .is-active sur le lien de la page courante // (la vue ne pose pas cette classe automatiquement, contrairement aux // blocs menus Drupal). // const projetsSidebar = document.querySelector('#block-erabletheme-views-block-projets-block-1'); if (projetsSidebar && !projetsSidebar.dataset.activeMarked) { const currentPath = window.location.pathname.replace(/\/$/, ''); projetsSidebar.querySelectorAll('a[href]').forEach(a => { if (a.getAttribute('href').replace(/\/$/, '') === currentPath) { a.classList.add('is-active'); } }); projetsSidebar.dataset.activeMarked = 'true'; } // // Pages taxonomy (filtres par tag de projet) : on rend les sous-titres // des cards cliquables (le titre est déjà lié via le Twig). // document.querySelectorAll('.taxonomy_page').forEach(card => { if (card.dataset.subtitleLinked) return; const titleLink = card.querySelector('.projet_label a'); const subtitle = card.querySelector('.sous_titre'); if (titleLink && subtitle && !subtitle.querySelector('a')) { const href = titleLink.getAttribute('href'); // On enveloppe les enfants du sous-titre dans un . const a = document.createElement('a'); a.setAttribute('href', href); while (subtitle.firstChild) a.appendChild(subtitle.firstChild); subtitle.appendChild(a); } card.dataset.subtitleLinked = 'true'; }); // // Home intro : sortir le .more-link (rendu profond par smart_trim) // de .intro_body pour le placer directement sous .intro_main. // Ainsi le séparateur vertical s'arrête en bas du texte, pas du bouton, // et le bouton est centré par rapport à .home_introduction. // const intro = document.querySelector('.home_introduction'); if (intro && !intro.dataset.moreLinkMoved) { const moreLink = intro.querySelector('.intro_body .more-link'); if (moreLink) { intro.appendChild(moreLink); intro.dataset.moreLinkMoved = 'true'; } } // // Carrousel // const isTaxonomyPage = document.querySelector('.taxonomy_page') ? true : false; if ($('.slick-container') && !isTaxonomyPage) { let slickEl = $('.slick-container').find('div').eq(3); $(slickEl).slick({ centerMode: true, slidesToShow: 3, dots: true, appendDots: $('#carousel_dots'), responsive: [ { breakpoint: 760, settings: { slidesToShow: 1 } }, ], }); } if ($('.diapo') && !isTaxonomyPage) { let slickEl = $('.diapo > div > div');$(slickEl).slick({ dots: true, appendDots: $('#carousel_dots'), }); } // // Toggle du menu // const hamburgerBtn = document.getElementById("hamburger"); const hamburgerIcon = document.querySelector(".burger-icon"); const menu = hamburgerBtn.nextElementSibling; const menuItems = menu.children; const opacityDelay = 50; let inTransition = false; function toggleMenuItems(action) { let delay = opacityDelay; for (let i = 0; i < menuItems.length; i++) { setTimeout(() => { if (action === 'show') { menuItems[i].classList.add('visible'); } else if (action === 'hide') { menuItems[menuItems.length - i - 1].classList.remove('visible'); } }, delay); delay += opacityDelay; } } // Toggle menu visibility on hamburger click hamburgerBtn.addEventListener("click", function(event) { event.stopPropagation(); if (!inTransition) { inTransition = true; hamburgerIcon.classList.toggle('open'); if(menu.classList.contains('active')) { setTimeout(() => { menu.style.display = "none"; }, 700); toggleMenuItems('hide'); } else { menu.style.display = "flex"; toggleMenuItems('show'); } setTimeout(() => { menu.classList.toggle("active"); }, 1); setTimeout(() => { inTransition = false; }, 700); } }); // Close menu when clicking outside of the menu document.addEventListener("click", function(event) { const isHamburgerClicked = event.target === hamburgerBtn; const isMenuClicked = event.target === menu || event.target.parentElement === menu; if (!isMenuClicked && !isHamburgerClicked && !inTransition) { inTransition = true; hamburgerIcon.classList.remove('open'); menu.classList.remove("active"); toggleMenuItems('hide'); setTimeout(() => { menu.style.display = "none"; inTransition = false; }, 700); } }); // // Programme page text fade in when it enters viewport // const url = window.location.pathname; if (url.endsWith("le-programme-erable") || url.endsWith("le-programme-erable/")) { const textElements = document.querySelectorAll('.fullpage_content p, .fullpage_content h3'); for (let element of textElements) element.classList.add('faded'); const fadeInOnScroll = (entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('fade-in'); // Add your fade-in class observer.unobserve(entry.target); // Stop observing once it's visible } }); }; // Create an Intersection Observer const observer = new IntersectionObserver(fadeInOnScroll, { threshold: 0.1 // Adjust as needed (0.1 means 10% of the element is visible) }); // Attach the observer to each element textElements.forEach(element => { observer.observe(element); }); } // // Fixed links // let fluoButtons; if (document.querySelector('.actu_full')) { fluoButtons = document.querySelectorAll('.liens_fixed > div > div:nth-of-type(2):not(.visually-hidden) > div, .file_fixed > div > .visually-hidden + div > div'); } else if (document.querySelector('.offre-content')) { fluoButtons = document.querySelectorAll('.liens_fixed > div, .file_fixed > div > div'); } else if (document.querySelector('.projets')) { fluoButtons = document.querySelectorAll('.file_fixed > div > div, .liens > div > div > div'); } else { fluoButtons = document.querySelectorAll('.liens_fixed > div > div:not(.visually-hidden), .file_fixed > div > div, .liens > div > div'); } let footer = document.querySelector('footer'); function positionFluoLinks(scrollTop) { for (let i = fluoButtons.length; i > 0 ; i--) { if (window.innerWidth > 1080) { const layoutContainerEl = document.querySelector('.layout-container'); let pageScrollableHeight = layoutContainerEl.offsetHeight; let prevButtonBottom = fluoButtons[i] ? parseInt(fluoButtons[i].style.bottom) : 0; let prevButtonHeight = fluoButtons[i] ? fluoButtons[i].offsetHeight : 0; fluoButtons[i - 1].style.position = 'fixed'; fluoButtons[i - 1].style.left = document.querySelector('.projets') ? '76vw' : '75vw'; /* console.log(` pageScrollableHeight: ${pageScrollableHeight}, footer.offsetHeight: ${footer.offsetHeight}, window.innerHeight: ${window.innerHeight}, scrollTop: ${scrollTop} `); */ if (pageScrollableHeight - footer.offsetHeight < window.innerHeight + scrollTop && i === fluoButtons.length) { let margin = document.querySelector('.projets') ? 15 : 30; fluoButtons[i - 1].style.bottom = `${window.innerHeight - footer.offsetTop + scrollTop + margin}px`; } else { let margin = document.querySelector('.projets') ? 0 : 15; fluoButtons[i - 1].style.bottom = i === fluoButtons.length ? '30px' : `${prevButtonBottom + prevButtonHeight + margin}px`; } } else { fluoButtons[i - 1].style.position = 'relative'; fluoButtons[i - 1].style.left = '0'; fluoButtons[i - 1].style.bottom = '0'; } } } if (document.querySelector('.fullpage')) { let liens = document.querySelectorAll('.liens_fixed a, .file_fixed a, .content_partenaires a'); for (let lien of liens) { lien.setAttribute('target', '_blank'); } positionFluoLinks(); document.body.addEventListener('scroll', (e) => { positionFluoLinks(e.target.scrollTop); }); window.addEventListener('resize', () => { positionFluoLinks(); }); } // Changer le texte des liens des ressources if (document.querySelector('.ressources')) { let liens = document.querySelectorAll('.file a'); for (let lien of liens) { lien.innerHTML = 'Télécharger le document'; lien.setAttribute('href', lien.getAttribute('href').replace('/index.php', '')); lien.setAttribute('target', '_blank'); } // si la ressource n'a pas de date afficher le lien plus haute // et la ressource en premier let ressources = document.querySelectorAll('.views-row'); let container = document.querySelector('.views-row:first-of-type').parentElement; for (let ressource of ressources) { if (!ressource.querySelector('time')) { let link = ressource.querySelector('.file'); let linkContainer = link.parentElement.parentElement; linkContainer.style.gridRow = '2'; linkContainer.style.marginTop = '30px'; } const rows = Array.from(container.querySelectorAll('.views-row')); const undated = []; const dated = []; rows.forEach(row => { if (row.querySelector('time')) { dated.push(row); } else { undated.push(row); } }); // Clear container container.innerHTML = ''; // Append undated rows first, then dated [...undated, ...dated].forEach(row => { container.appendChild(row); }); } } // // Ajouter les logos réseaux sociaux dans le menu togglable // let socials = document.querySelector('.social-media-links--platforms'); let socialsClone = socials.cloneNode(true); socialsClone.id = 'socials-in-menu-wrapper'; let menuContainer = document.querySelector('#hamburger + ul'); let socialsContainer = document.createElement('li'); socialsContainer?.append(socialsClone); menuContainer?.append(socialsContainer); // // Retirer paragraphes vides fullpage // et ajouter de la marge pour les images en portrait // let currentPage; if (document.querySelector('.fullpage:not(.actus)')) currentPage = 'fullpage'; else if (document.querySelector('.carousel_container')) currentPage = 'home'; if (currentPage === 'fullpage') { let paragraphs = document.querySelectorAll('p'); for (let paragraph of paragraphs) { if (!/[a-zA-Z]/.test(paragraph.innerText)) { paragraph.remove(); } } let imgs = document.querySelectorAll('.fullpage img'); let isActus = document.querySelector('.retour_actus'); if (isActus) { for (let img of imgs) { if (img.height > img.width) { img.parentElement.style.padding = '0 12vw'; } } } } // // all meetup card is a link // let meetup = document.querySelector('.meetup_list'); if (meetup) { let meetupCards= document.querySelectorAll('.meetup_list > .views-row'); for (let card of meetupCards) { let meetupLink = card.querySelector('.offre-title'); meetupLink = meetupLink.getAttribute('href'); let ficheLink = document.createElement('a'); ficheLink.setAttribute('href', meetupLink); let cardArticle = card.querySelector('article'); ficheLink.append(cardArticle); card.append(ficheLink); } } // // Aside menu toggle // const sideMenuTitle = document.querySelector('#block-erabletheme-leprogramme-2 h2') || document.querySelector('#block-erabletheme-views-block-projets-block-1 h2'); if (sideMenuTitle) { sideMenuTitle.addEventListener('click', () => { const sideMenu = document.querySelector('#block-erabletheme-leprogramme-2 > ul') || document.querySelector('#block-erabletheme-views-block-projets-block-1 .projets_list'); const titleArrow = sideMenuTitle.querySelector('div'); if (!sideMenu.classList.contains('closed')) { sideMenu.classList.add('closed'); titleArrow.classList.add('closed'); } else { sideMenu.classList.remove('closed'); titleArrow.classList.remove('closed'); } }); } // // set active project in aside project menu // const asideProjectMenu = document.querySelector('#block-erabletheme-views-block-projets-block-1 .projets_list'); if (asideProjectMenu) { // Le titre du projet n'est plus rendu dans .projet_full depuis la // refonte (il a été remonté dans .page-header-outside, hors colonne // blanche). On va chercher le titre courant à sa nouvelle place. // Garde sur null + chaîne vide pour éviter l'exception qui plantait // tout le behavior attach (et donc Rellax) sur /projets/. const headerOutside = document.querySelector('.layout-content .page-header-outside h2'); const contentPageTitle = (headerOutside?.innerText || '').trim().toLowerCase(); if (contentPageTitle) { for (let item of asideProjectMenu.children) { const itemTitle = (item.innerText || '').trim().toLowerCase(); if (itemTitle === contentPageTitle) { item.querySelector('a')?.classList.add('is-active'); } } } } // // set non breaking spaces // document.querySelectorAll('p, li, h1, h2, h3, h4, h5, h6').forEach((content) => { content.innerHTML = content.innerHTML.replaceAll(' ;', ' ;'); content.innerHTML = content.innerHTML.replaceAll(' :', ' :'); content.innerHTML = content.innerHTML.replaceAll(' ?', ' ?'); content.innerHTML = content.innerHTML.replaceAll(' !', ' !'); content.innerHTML = content.innerHTML.replaceAll('« ', '« '); content.innerHTML = content.innerHTML.replaceAll(' »', ' »'); content.innerHTML = content.innerHTML.replaceAll('(« ', '(« '); content.innerHTML = content.innerHTML.replaceAll(' »)', ' »)'); }); // // index projets random shape // if (document.querySelector('.projets_list')) { const projets = document.querySelectorAll('.projets_list > .views-row'); const shapesAmount = 6; for (let i = 0; i < projets.length; i++) { const randomShape = Math.floor(Math.random() * shapesAmount) + 1; if (projets[i].querySelector('img')) { const img = projets[i].querySelector('a'); img.style.maskImage = `url('/themes/erabletheme/assets/shapes/shape_${randomShape}.svg')`; } } } // // Décors mid-left (col-decor) : PNG empilés au pied gauche // extérieur d'une colonne. Position absolue dans .layout-container, // bord droit du PNG calé sur bord gauche colonne via // translateX(-100%). Tailles + gaps proportionnels à colH (bornés // min/max). Affichage conditionnel : seuils 50%/30% colH. // - .fullpage:not(.large-container) : pile de 2 (mid-left-1+2). // - .map-projets (home) : 1 seul (mid-left-1) via singleOnly. // Parallax custom (sans Rellax) plus bas. // Visibilité responsive : _background.scss. // const layoutContainer = document.querySelector('.layout-container'); const colDecorState = []; // { img1, img2, col } function clamp(min, val, max) { return Math.max(min, Math.min(val, max)); } // Coefficient appliqué aux décors home (oiseaux + fleurs) hors // desktop : on grossit les images pour qu'elles restent lisibles // sur écrans étroits (le viewport rétrécit mais les éléments // décoratifs en vw deviennent visuellement trop petits). function decorWidthScale() { const w = window.innerWidth; if (w < 760) return 1.8; // mobile if (w < 1080) return 1.5; // tablette return 1; // desktop } function colDecorMetrics(colH) { return { gapBottom: clamp(120, 0.08 * colH, 320), gapBetween: clamp(60, 0.03 * colH, 180), h2: clamp(180, 0.14 * colH, 480), h1: clamp(220, 0.17 * colH, 580), }; } function createColDecorImg(idx) { const img = document.createElement('img'); img.className = `col-decor col-decor-${idx}`; img.setAttribute('aria-hidden', 'true'); img.setAttribute('alt', ''); img.src = `/themes/erabletheme/assets/new-bg-shapes/mid-left-${idx}.png`; img.style.cssText = 'position:absolute;width:auto;pointer-events:none;z-index:0;transform:translateX(-100%);'; return img; } function setupColDecor() { if (!layoutContainer) return; layoutContainer.querySelectorAll('.col-decor').forEach(n => n.remove()); colDecorState.length = 0; // Colonnes .fullpage internes : pile de 2 PNG (mid-left-1 et -2). document.querySelectorAll('.fullpage:not(.large-container)').forEach(col => { const img1 = createColDecorImg(1); const img2 = createColDecorImg(2); layoutContainer.appendChild(img1); layoutContainer.appendChild(img2); colDecorState.push({ img1, img2, col }); }); // Bloc .map-projets (home) : un seul PNG via singleOnly. document.querySelectorAll('.map-projets').forEach(col => { const img1 = createColDecorImg(1); const img2 = createColDecorImg(2); layoutContainer.appendChild(img1); layoutContainer.appendChild(img2); colDecorState.push({ img1, img2, col, singleOnly: true }); }); positionColDecor(); } function positionColDecor() { if (!colDecorState.length) return; const lcRect = layoutContainer.getBoundingClientRect(); const lcTopAbs = lcRect.top + window.scrollY; const lcLeftAbs = lcRect.left + window.scrollX; for (const entry of colDecorState) { const { img1, img2, col, singleOnly } = entry; const r = col.getBoundingClientRect(); const colH = r.height; const m = colDecorMetrics(colH); const stack1H = m.gapBottom + m.h2 + m.gapBetween + m.h1; // singleOnly : un seul motif (img1) si la place le permet // (= gapBottom + h1 sous 50% colH). const showBoth = !singleOnly && stack1H <= colH * 0.5; const showOne = !showBoth && (m.gapBottom + m.h2) <= colH * 0.3; const showSingle = singleOnly && (m.gapBottom + m.h1) <= colH * 0.5; entry.visible1 = showBoth || showSingle; entry.visible2 = !singleOnly && (showBoth || showOne); img1.style.display = entry.visible1 ? 'block' : 'none'; img2.style.display = entry.visible2 ? 'block' : 'none'; if (!entry.visible1 && !entry.visible2) continue; const colBottomAbs = r.bottom + window.scrollY; const leftRel = (r.left + window.scrollX) - lcLeftAbs; if (singleOnly) { const top1 = colBottomAbs - m.gapBottom - m.h1; img1.style.top = (top1 - lcTopAbs) + 'px'; img1.style.left = leftRel + 'px'; img1.style.height = m.h1 + 'px'; entry.scrollRef1 = top1 + m.h1 / 2 - window.innerHeight / 2; } else { const top2 = colBottomAbs - m.gapBottom - m.h2; img2.style.top = (top2 - lcTopAbs) + 'px'; img2.style.left = leftRel + 'px'; img2.style.height = m.h2 + 'px'; entry.scrollRef2 = top2 + m.h2 / 2 - window.innerHeight / 2; if (showBoth) { const top1 = top2 - m.gapBetween - m.h1; img1.style.top = (top1 - lcTopAbs) + 'px'; img1.style.left = leftRel + 'px'; img1.style.height = m.h1 + 'px'; entry.scrollRef1 = top1 + m.h1 / 2 - window.innerHeight / 2; } } } applyColDecorParallax(); } // Parallax custom (sans Rellax) : remontée subtile, amplitude bornée, // proportionnelle au scroll relatif à la position d'ancrage. À 0.15 // de vitesse, pour 100px de scroll au-delà de la référence, le motif // monte de 15px ; clamp à ±30px évite tout débordement. const COL_DECOR_PARALLAX_SPEED = 0.15; const COL_DECOR_PARALLAX_AMP = 30; let colDecorRafPending = false; function applyColDecorParallax() { const scrollY = window.scrollY || document.body.scrollTop || 0; for (const { img1, img2, visible1, visible2, scrollRef1, scrollRef2 } of colDecorState) { if (visible2 && scrollRef2 != null) { const dY = clamp(-COL_DECOR_PARALLAX_AMP, -(scrollY - scrollRef2) * COL_DECOR_PARALLAX_SPEED, COL_DECOR_PARALLAX_AMP); img2.style.transform = `translate3d(0, ${dY}px, 0) translateX(-100%)`; } if (visible1 && scrollRef1 != null) { const dY = clamp(-COL_DECOR_PARALLAX_AMP, -(scrollY - scrollRef1) * COL_DECOR_PARALLAX_SPEED, COL_DECOR_PARALLAX_AMP); img1.style.transform = `translate3d(0, ${dY}px, 0) translateX(-100%)`; } } } function onColDecorScroll() { if (colDecorRafPending) return; colDecorRafPending = true; requestAnimationFrame(() => { colDecorRafPending = false; applyColDecorParallax(); applyHomeDiapoParallax(); }); } if (!document.body.dataset.colDecorScrollWired) { document.body.addEventListener('scroll', onColDecorScroll, { passive: true }); document.body.dataset.colDecorScrollWired = 'true'; } // // Décor aléatoire en haut à droite de chaque .fullpage. Tirage // parmi drawings/ (excluant flower-2 + bird-3 déjà au footer). // Bas de l'image overlap le haut de l'ancre (20%, sauf flower-3 // à 40% car tige fine). Mobile : ancre = .page-header-outside. // Largeur par breakpoint. // const PAGE_DECOR_POOL = ['bird-1.png', 'bird-2.png', 'flower-1.png', 'flower-3.png']; const pageDecorState = []; function pageDecorWidthVw() { const w = window.innerWidth; if (w < 1080) return 22; // mobile + tablette return 10; // desktop } function setupPageDecor() { if (!layoutContainer) return; layoutContainer.querySelectorAll('.page-decor').forEach(n => n.remove()); pageDecorState.length = 0; document.querySelectorAll('.fullpage').forEach(col => { // Exclure les .fullpage situées dans une sidebar (vue projets // imbriquée). On ne décore que les colonnes de contenu. if (col.closest('aside')) return; // Tirage déterministe par colonne : on stocke l'index sur le // dataset pour qu'un resize ne change pas l'image affichée. let idx = col.dataset.pageDecorIdx; if (idx == null) { idx = Math.floor(Math.random() * PAGE_DECOR_POOL.length); col.dataset.pageDecorIdx = String(idx); } else { idx = parseInt(idx, 10); } const img = document.createElement('img'); img.className = 'page-decor'; img.setAttribute('aria-hidden', 'true'); img.setAttribute('alt', ''); img.src = `/themes/erabletheme/assets/drawings/${PAGE_DECOR_POOL[idx]}`; img.style.cssText = 'position:absolute;width:auto;height:auto;pointer-events:none;z-index:0;'; layoutContainer.appendChild(img); pageDecorState.push({ img, col }); }); positionPageDecor(); } function positionPageDecor() { if (!pageDecorState.length) return; const lcRect = layoutContainer.getBoundingClientRect(); const lcTopAbs = lcRect.top + window.scrollY; const lcLeftAbs = lcRect.left + window.scrollX; const widthVw = pageDecorWidthVw(); const widthPx = (widthVw / 100) * window.innerWidth; const isMobile = window.innerWidth < 760; for (const entry of pageDecorState) { const img = entry.img; if (!img.naturalWidth) { img.addEventListener('load', positionPageDecor, { once: true }); continue; } const heightPx = widthPx * (img.naturalHeight / img.naturalWidth); // Mobile : ancre = .page-header-outside (titre/retour) si présent, // sinon la colonne elle-même. let anchorEl = entry.col; if (isMobile) { const layoutContent = entry.col.closest('.layout-content'); const phOutside = layoutContent && layoutContent.querySelector('.page-header-outside'); if (phOutside) anchorEl = phOutside; } const r = anchorEl.getBoundingClientRect(); const anchorTopAbs = r.top + window.scrollY; const anchorRightAbs = r.right + window.scrollX; // flower-3 a une tige fine en haut → on la pousse plus dans // l'ancre (40%) pour qu'elle ne soit pas perdue dans la marge. const overlapRatio = img.src.includes('flower-3') ? 0.4 : 0.2; const top = anchorTopAbs + heightPx * overlapRatio - heightPx; const left = anchorRightAbs - widthPx * 0.7; // 30% dépasse à droite img.style.width = widthPx + 'px'; img.style.top = (top - lcTopAbs) + 'px'; img.style.left = (left - lcLeftAbs) + 'px'; } } // // Décors home-diapo-{right,left} : ancrés aux bords droit/gauche // du viewport, leur haut dépassant au-dessus du haut des cards // du carousel home (overlapTop px). Parallax custom. // const HOME_DIAPO_CONFIGS = [ { side: 'right', file: 'home-diapo-right.png', widthVw: 30, overlapTop: 160 }, { side: 'left', file: 'home-diapo-left.png', widthVw: 28, overlapTop: 80 }, ]; const homeDiapoState = []; // Ancre cible : .slick-list (cards visibles), avec fallbacks. // .carousel_container inclut les dots → bas trop bas, à éviter. function getCarouselCardsBox() { return document.querySelector('.carousel_container .slick-list') || document.querySelector('.carousel_container .slick-container') || document.querySelector('.carousel_container'); } function setupHomeDiapoDecor() { if (!layoutContainer) return; layoutContainer.querySelectorAll('.home-diapo-decor').forEach(n => n.remove()); homeDiapoState.length = 0; const cardsBox = getCarouselCardsBox(); if (!cardsBox) return; for (const cfg of HOME_DIAPO_CONFIGS) { const img = document.createElement('img'); img.className = `home-diapo-decor home-diapo-decor-${cfg.side}`; img.setAttribute('aria-hidden', 'true'); img.setAttribute('alt', ''); img.src = `/themes/erabletheme/assets/new-bg-shapes/${cfg.file}`; img.style.cssText = `position:absolute;${cfg.side}:0;width:auto;height:auto;pointer-events:none;z-index:0;`; layoutContainer.appendChild(img); homeDiapoState.push({ img, widthVw: cfg.widthVw, overlapTop: cfg.overlapTop, cardsBox }); } positionHomeDiapoDecor(); } function positionHomeDiapoDecor() { if (!homeDiapoState.length) return; const lcRect = layoutContainer.getBoundingClientRect(); const lcTopAbs = lcRect.top + window.scrollY; const scale = decorWidthScale(); for (const entry of homeDiapoState) { const widthPx = (entry.widthVw / 100) * window.innerWidth * scale; const cRect = entry.cardsBox.getBoundingClientRect(); const cardsTopAbs = cRect.top + window.scrollY; // Top = haut des cards - overlapTop : le haut de l'image // dépasse de cette valeur au-dessus du haut des cards. const top = cardsTopAbs - entry.overlapTop; entry.img.style.width = widthPx + 'px'; entry.img.style.top = (top - lcTopAbs) + 'px'; entry.scrollRef = top - window.innerHeight / 2; } applyHomeDiapoParallax(); } function applyHomeDiapoParallax() { if (!homeDiapoState.length) return; const scrollY = window.scrollY || document.body.scrollTop || 0; for (const { img, scrollRef } of homeDiapoState) { if (scrollRef == null) continue; const dY = clamp(-COL_DECOR_PARALLAX_AMP, -(scrollY - scrollRef) * COL_DECOR_PARALLAX_SPEED, COL_DECOR_PARALLAX_AMP); img.style.transform = `translate3d(0, ${dY}px, 0)`; } } // // Décors home-above : flower-1 (gauche) et bird-1 (droite), // au-dessus des cards, leur bas débordant de `overlap` px sur le // haut des cards. offsetVw : décalage horizontal vers l'intérieur. // Pas de parallax. // const HOME_ABOVE_CONFIGS = [ { side: 'left', file: 'drawings/flower-1.png', widthVw: 15, overlap: 40, offsetVw: 8 }, { side: 'right', file: 'drawings/bird-1.png', widthVw: 15, overlap: 40, offsetVw: 4 }, ]; const homeAboveState = []; function setupHomeAboveDecor() { if (!layoutContainer) return; layoutContainer.querySelectorAll('.home-above-decor').forEach(n => n.remove()); homeAboveState.length = 0; const cardsBox = getCarouselCardsBox(); if (!cardsBox) return; for (const cfg of HOME_ABOVE_CONFIGS) { const img = document.createElement('img'); img.className = `home-above-decor home-above-decor-${cfg.side}`; img.setAttribute('aria-hidden', 'true'); img.setAttribute('alt', ''); img.src = `/themes/erabletheme/assets/${cfg.file}`; img.style.cssText = `position:absolute;${cfg.side}:${cfg.offsetVw}vw;width:auto;height:auto;pointer-events:none;z-index:0;`; layoutContainer.appendChild(img); homeAboveState.push({ img, widthVw: cfg.widthVw, overlap: cfg.overlap, cardsBox }); } positionHomeAboveDecor(); } function positionHomeAboveDecor() { if (!homeAboveState.length) return; const lcRect = layoutContainer.getBoundingClientRect(); const lcTopAbs = lcRect.top + window.scrollY; const lcHeight = lcRect.height; const scale = decorWidthScale(); for (const entry of homeAboveState) { const widthPx = (entry.widthVw / 100) * window.innerWidth * scale; const cRect = entry.cardsBox.getBoundingClientRect(); const cardsTopAbs = cRect.top + window.scrollY; // Le BAS de l'image est à `overlap` px sous le haut des cards : // l'image est essentiellement au-dessus du carousel, débord en bas. const imgBottom = cardsTopAbs + entry.overlap; const bottomFromLcBottom = (lcTopAbs + lcHeight) - imgBottom; entry.img.style.width = widthPx + 'px'; entry.img.style.bottom = bottomFromLcBottom + 'px'; } } // // Décors home-projet, ancrés au bloc .map-projets : // - bird-2 en haut à gauche, débordant à gauche du bloc en // desktop, ou collé à -3vw du viewport en mobile/tablette. // - flower-3 collée au bord droit du viewport (tronquée), centre // vertical à 1/3 depuis le haut du bloc. // Pas de parallax. // const HOME_PROJET_CONFIGS = [ { file: 'drawings/bird-2.png', widthVw: 15, anchor: 'block-left-top' }, { file: 'drawings/flower-3.png', widthVw: 7, anchor: 'viewport-right' }, ]; const homeProjetState = []; function setupHomeProjetDecor() { if (!layoutContainer) return; layoutContainer.querySelectorAll('.home-projet-decor').forEach(n => n.remove()); homeProjetState.length = 0; const block = document.querySelector('.map-projets'); if (!block) return; for (const cfg of HOME_PROJET_CONFIGS) { const img = document.createElement('img'); img.className = `home-projet-decor home-projet-decor-${cfg.anchor}`; img.setAttribute('aria-hidden', 'true'); img.setAttribute('alt', ''); img.src = `/themes/erabletheme/assets/${cfg.file}`; img.style.cssText = 'position:absolute;width:auto;height:auto;pointer-events:none;z-index:0;'; layoutContainer.appendChild(img); homeProjetState.push({ img, anchor: cfg.anchor, widthVw: cfg.widthVw, block }); } positionHomeProjetDecor(); } function positionHomeProjetDecor() { if (!homeProjetState.length) return; const lcRect = layoutContainer.getBoundingClientRect(); const lcTopAbs = lcRect.top + window.scrollY; const lcLeftAbs = lcRect.left + window.scrollX; const scale = decorWidthScale(); for (const entry of homeProjetState) { const img = entry.img; if (!img.naturalWidth) { img.addEventListener('load', positionHomeProjetDecor, { once: true }); continue; } const widthPx = (entry.widthVw / 100) * window.innerWidth * scale; const heightPx = widthPx * (img.naturalHeight / img.naturalWidth); const bRect = entry.block.getBoundingClientRect(); const blockTopAbs = bRect.top + window.scrollY; const blockLeftAbs = bRect.left + window.scrollX; const blockHeightPx = bRect.height; img.style.width = widthPx + 'px'; if (entry.anchor === 'block-left-top') { // bird-2 remontée de 0.6× sa hauteur au-dessus du bloc. const top = blockTopAbs - heightPx * 0.6; if (window.innerWidth < 1080) { img.style.left = '-3vw'; } else { img.style.left = (blockLeftAbs - widthPx * 0.5 - lcLeftAbs) + 'px'; } img.style.top = (top - lcTopAbs) + 'px'; img.style.right = ''; } else if (entry.anchor === 'viewport-right') { // flower-3 : tronquée par le bord droit du viewport. const top = blockTopAbs + blockHeightPx / 3 - heightPx / 2; img.style.right = '-20px'; img.style.left = ''; img.style.top = (top - lcTopAbs) + 'px'; } } } // // Décors latéraux parallax v2. // Cloner le .decor-tile autant de fois que nécessaire pour couvrir // la hauteur de .layout-container, puis instancier Rellax sur tous // les .bgImg (originaux + clones). Au resize, on détruit/recompute. // const bgRoot = document.getElementById('background'); let rellax = null; function setupBackgroundTiles() { if (!bgRoot) return; const original = bgRoot.querySelector('.decor-tile:not([data-tile-clone])'); if (!original) return; // Nettoyage des clones précédents (cas resize). bgRoot.querySelectorAll('.decor-tile[data-tile-clone]').forEach(n => n.remove()); const layout = document.querySelector('.layout-container'); const pageHeight = layout ? layout.offsetHeight : window.innerHeight; const tileHeight = original.getBoundingClientRect().height; if (tileHeight <= 0) return; const tileCount = Math.max(1, Math.ceil(pageHeight / tileHeight)); for (let i = 1; i < tileCount; i++) { const clone = original.cloneNode(true); clone.dataset.tileClone = 'true'; clone.dataset.tileIndex = String(i); // .decor-once : formes pensées pour le haut du 1er tile // (coupées en haut) — on les enlève des clones. clone.querySelectorAll('.decor-once').forEach(n => n.remove()); bgRoot.appendChild(clone); } } function initRellax() { if (rellax) { try { rellax.destroy(); } catch (e) { /* noop */ } } rellax = new Rellax('.bgImg', { wrapper: 'body' }); } // Init initial : frises latérales (basées sur layout-container, // mesure stable même sans images chargées) + Rellax. Les col-decor // sont injectés et positionnés UNIQUEMENT au window.load — leur // position dépend de getBoundingClientRect() de .fullpage qui n'est // fiable qu'une fois toutes les images du contenu chargées (sinon // Rellax cache un blockTop périmé et le motif dérive vers le bas // sur les pages longues). setupBackgroundTiles(); initRellax(); window.addEventListener('load', () => { setupBackgroundTiles(); setupColDecor(); setupHomeDiapoDecor(); setupHomeAboveDecor(); setupHomeProjetDecor(); setupPageDecor(); initRellax(); }); // Resize debouncé : recalcule le nombre de tiles + tous les décors, // repart Rellax propre. let bgResizeTimer = null; window.addEventListener('resize', () => { clearTimeout(bgResizeTimer); bgResizeTimer = setTimeout(() => { setupBackgroundTiles(); setupColDecor(); setupHomeDiapoDecor(); setupHomeAboveDecor(); setupHomeProjetDecor(); setupPageDecor(); initRellax(); }, 200); }); // footnotes if (document.querySelector('.retour_projets')) { const textContent = document.querySelector('.text-content'); if (textContent) { const paragraphs = textContent.querySelectorAll('p'); const footnotes = []; let footnoteNumber = 1; // Process each paragraph to find and replace [...] with superscripts paragraphs.forEach(p => { p.innerHTML = p.innerHTML.replace(/\[(.*?)\]/g, (match, content) => { footnotes.push({ number: footnoteNumber, text: content }); return `${footnoteNumber++}`; }); }); // Create footnotes div if there are any footnotes if (footnotes.length > 0) { const fullpageContent = document.querySelector('.fullpage_content'); if (fullpageContent) { const footnotesDiv = document.createElement('div'); footnotesDiv.className = 'footnotes'; footnotes.forEach(footnote => { const footnoteItem = document.createElement('p'); footnoteItem.innerHTML = `${footnote.number}. ${footnote.text}`; footnoteItem.style.fontSize = '0.8rem'; footnotesDiv.appendChild(footnoteItem); }); fullpageContent.appendChild(footnotesDiv); } } } } } } let firstMap = null; Drupal.behaviors.customLeafletInteraction = { attach: function (context, settings) { $(context).on('leafletMapInit', function (e, settings, map, mapid, markers) { if (document.querySelector('.projet_full')) { // map on the projects page let customIcon = L.icon({ iconUrl: '/themes/erabletheme/assets/leaflet-point.svg', iconSize: [18, 18], iconAnchor: [9, 9], popupAnchor: [0, -9] }); let initiatedMap = document.querySelector(`#${mapid}`); const currentLibelle = initiatedMap.parentElement.nextElementSibling?.innerText; if (currentLibelle) { initiatedMap.parentElement.nextElementSibling.remove(); } if (document.querySelectorAll('.leaflet-container').length <= 1) { firstMap = map; let bounds = L.latLngBounds(); map.eachLayer(function (layer) { if (layer instanceof L.Marker && !layer._popup) { layer.bindPopup(currentLibelle, { closeButton: false, className: 'popup' }) .setIcon(customIcon) .on('mouseover', function () { this.openPopup(); }) .on('mouseout', function () { this.closePopup(); }); bounds.extend(layer.getLatLng()); map.fitBounds(bounds, { padding: [30, 30], maxZoom: 18 }); } }); } else { // map on the projects index document.querySelector(`#${mapid}`).parentElement.parentElement.parentElement.remove(); const firstMarker = Object.values(markers)[0]; let newMarker = L.marker([firstMarker._latlng.lat, firstMarker._latlng.lng], { icon: customIcon }) .addTo(firstMap) .bindPopup(currentLibelle, { closeButton: false, className: 'popup' }) .on('mouseover', function () { this.openPopup(); }) .on('mouseout', function () { this.closePopup(); }); firstMap.fitBounds( firstMap.getBounds().extend(newMarker.getLatLng()), { padding: [30, 30], maxZoom: 18 } ); } } else { // // on projects pages, replace leaflet icons with erable leafs // const currentMap = document.querySelector('.leaflet-container'); const leafletIcons = document.querySelectorAll('.leaflet-marker-pane img'); for (let icon of leafletIcons) { icon.setAttribute('src', '/themes/erabletheme/assets/leaflet-point.svg'); } // // set hover on leaflet marker and zones // const leafletPlaces = document.querySelectorAll('.leaflet-marker-pane img, .leaflet-overlay-pane path'); const libelles = document.querySelectorAll('.libelles-carte > div > div > div'); for (let i = 0; i < leafletPlaces.length; i++) { const carte = document.querySelector('.carte'); leafletPlaces[i].addEventListener('mouseenter', () => { let div = document.createElement('div'); div.setAttribute('id', 'leaflet-popup'); div.style.zIndex = '100'; const x = leafletPlaces[i].getBoundingClientRect().left + leafletPlaces[i].getBoundingClientRect().width / 2; const y = leafletPlaces[i].getBoundingClientRect().top; setTimeout(() => { div.style.opacity = 1; const divHeight = div.getBoundingClientRect().height; const divWidth = div.getBoundingClientRect().width; setTimeout(() => { div.style.left = `${x - divWidth / 2}px`; div.style.top = `${y - divHeight}px`; }, 10); }, 10); div.innerText = libelles[i].innerText; carte.appendChild(div); }); leafletPlaces[i].addEventListener('mouseleave', () => { let divs = document.querySelectorAll('#leaflet-popup'); for (let div of divs) { div.style.opacity = 0; setTimeout(() => { div.remove(); }, 300); } }); } } }); } } } (jQuery, Drupal));