Files
drupal-erable/web/themes/erabletheme/js/erabletheme.js

1102 lines
45 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é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 `<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));