Files
drupal-erable/web/themes/erabletheme/js/erabletheme.js
Valentin Le Moign 5adc79ac4b Refonte décors home + texte vert lisible
- home: chapo agrandi, flower-1 collée à gauche, oiseau droite retiré
- oiseau projets lauréats -> bird-3 (plus petit, repositionné)
- frise verte parallax au niveau de la carte projets
- suppression décors footer + système page-decor (code mort inclus)
- /projets: texte à gauche de la carte agrandi en desktop
- texte vert #33ffc4 -> #0c8461 (sidebars, contenus projets, etc.)
- icône dropdown sidebar au même vert que le texte
2026-07-08 15:29:55 +02:00

1061 lines
44 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @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 <main>, donc on mesure sa hauteur et on l'expose via
// une CSS variable pour que <main> 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 <a>.
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/<slug>.
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(' ;', '&nbsp;;');
content.innerHTML = content.innerHTML.replaceAll(' :', '&nbsp;:');
content.innerHTML = content.innerHTML.replaceAll(' ?', '&nbsp;?');
content.innerHTML = content.innerHTML.replaceAll(' !', '&nbsp;!');
content.innerHTML = content.innerHTML.replaceAll('« ', '«&nbsp;');
content.innerHTML = content.innerHTML.replaceAll(' »', '&nbsp;»');
content.innerHTML = content.innerHTML.replaceAll('(« ', '(«&nbsp;');
content.innerHTML = content.innerHTML.replaceAll(' »)', '&nbsp;»)');
});
//
// 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é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écor home-above : flower-1 (gauche), au-dessus des cards, son bas
// débordant de `overlap` px sur le haut des cards. offsetVw décale
// horizontalement : négatif = déborde à gauche du viewport, si bien
// que la fleur semble « sortir de sous » une card. offsetVwDesktop
// (optionnel) surcharge offsetVw à partir de $breakpoint_desktop :
// ici un léger décalage à droite du bouquet en desktop uniquement.
// Pas de parallax.
//
const HOME_ABOVE_CONFIGS = [
{ side: 'left', file: 'drawings/flower-1.png', widthVw: 15, overlap: 40, offsetVw: -3, offsetVwDesktop: 0 },
];
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;width:auto;height:auto;pointer-events:none;z-index:0;';
layoutContainer.appendChild(img);
homeAboveState.push({ img, side: cfg.side, widthVw: cfg.widthVw, overlap: cfg.overlap, offsetVw: cfg.offsetVw, offsetVwDesktop: cfg.offsetVwDesktop, 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écalage horizontal responsive : offsetVwDesktop surcharge
// offsetVw à partir de 1080px (léger recentrage du bouquet).
const isDesktop = window.innerWidth >= 1080;
const offsetVw = (isDesktop && entry.offsetVwDesktop != null)
? entry.offsetVwDesktop : entry.offsetVw;
entry.img.style[entry.side] = offsetVw + 'vw';
}
}
//
// Décors home-projet, ancrés au bloc .map-projets :
// - bird-3 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-3.png', widthVw: 11, 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-3 remontée de 0.75× sa hauteur au-dessus du bloc,
// puis redescendue de 20px (toutes vues).
const top = blockTopAbs - heightPx * 0.75 + 20;
if (window.innerWidth < 1080) {
// tablette : collée au bord gauche (léger décalage vers la
// droite par rapport à l'ancien -3vw).
img.style.left = '0';
} else {
// desktop : moins de débord à gauche du bloc (0.25 au lieu
// de 0.5) → l'oiseau est décalé vers la droite.
img.style.left = (blockLeftAbs - widthPx * 0.25 - 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);
}
}
// Frise verte fluo à gauche, au niveau de la carte des projets
// lauréats. Injectée comme .bgImg dans #background (donc animée par
// Rellax comme les autres frises), dimensionnée en vh comme elles
// (largeur auto → pas d'effet « zoomé »), positionnée au centre
// vertical du bloc .map-projets. Doit être appelée AVANT initRellax.
function setupMapDecor() {
if (!bgRoot) return;
bgRoot.querySelectorAll('.map-decor').forEach(n => n.remove());
const block = document.querySelector('.map-projets');
if (!block) return;
const bgRect = bgRoot.getBoundingClientRect();
const bgTopAbs = bgRect.top + window.scrollY;
const bRect = block.getBoundingClientRect();
const blockCenterAbs = bRect.top + window.scrollY + bRect.height / 2;
const img = document.createElement('img');
// .bgImg → capté par Rellax ; .decor-left → collé à left:0 ;
// .map-decor → sélecteur de nettoyage.
img.className = 'bgImg decor-left map-decor';
img.setAttribute('data-rellax-speed', '-3');
img.setAttribute('aria-hidden', 'true');
img.setAttribute('alt', '');
img.src = '/themes/erabletheme/assets/new-bg-shapes/left-3.png';
// height en vh (comme les frises) ; top centré sur la carte.
img.style.cssText = 'position:absolute;left:0;width:auto;height:32vh;'
+ 'pointer-events:none;top:' + (blockCenterAbs - bgTopAbs - (0.16 * window.innerHeight)) + 'px;';
bgRoot.appendChild(img);
}
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();
setupMapDecor();
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();
setupMapDecor();
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 `<sup style="font-size: 0.7rem; vertical-align: top; font-weight: bold;">${footnoteNumber++}</sup>`;
});
});
// 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 = `<strong>${footnote.number}.</strong> ${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));