410 lines
16 KiB
JavaScript
410 lines
16 KiB
JavaScript
// import { createApp } from 'vue'
|
|
import '../scss/main.scss'
|
|
|
|
import "splitting/dist/splitting.css";
|
|
import "splitting/dist/splitting-cells.css";
|
|
import { gsap, Power1 } from 'gsap';
|
|
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
|
import Lenis from 'lenis';
|
|
import 'fslightbox';
|
|
|
|
gsap.registerPlugin(ScrollTrigger);
|
|
|
|
// import Content from './vuejs/Content.vue'
|
|
|
|
// import REST from './api/rest-axios'
|
|
|
|
// import LocomotiveScroll from 'locomotive-scroll';
|
|
// const scroll = new LocomotiveScroll();
|
|
|
|
|
|
// /**
|
|
// * @file
|
|
// * https://www.drupal.org/docs/drupal-apis/javascript-api/javascript-api-overview
|
|
// */
|
|
// (function (Drupal) {
|
|
|
|
// 'use strict';
|
|
|
|
// Drupal.behaviors.leshed = {
|
|
// attach: function (context, settings) {
|
|
// console.log('It works!');
|
|
// }
|
|
// };
|
|
|
|
// } (Drupal));
|
|
|
|
(function (Drupal, drupalSettings) {
|
|
const LeShedTheme = function () {
|
|
const _is_front = drupalSettings.path.isFront
|
|
console.log('drupalSettings', drupalSettings)
|
|
|
|
const scroller = document.querySelector('main[role="main"]');
|
|
const gsap_mm = gsap.matchMedia();
|
|
console.log('gsap_mm', gsap_mm);
|
|
|
|
// Instance Lenis partagée : initPageFull en a besoin pour resynchroniser la
|
|
// plage de scroll après avoir gonflé le spacer (voir ensureScrollForTitle).
|
|
let lenis = null;
|
|
|
|
// ___ _ _
|
|
// |_ _|_ _ (_) |_
|
|
// | || ' \| | _|
|
|
// |___|_||_|_|\__|
|
|
function init () {
|
|
console.log('LeShedTheme init()');
|
|
console.log('deploy test to delete');
|
|
|
|
initBurgerMenu();
|
|
initSmoothScroll();
|
|
initHomeColumnScrollSync();
|
|
initFsLightbox();
|
|
initPageFull();
|
|
// initVues()
|
|
}
|
|
|
|
|
|
function onWindowResize() {
|
|
_cardsForms.forEach(({card, forme, formepos}) => {
|
|
// console.log(`card`, card, 'forme', forme, 'formepos', formepos);
|
|
forme.style.top = `${card.offsetTop + formepos.top}px`;
|
|
forme.style.left = `${card.offsetLeft + formepos.left}px`;
|
|
})
|
|
}
|
|
|
|
function initBurgerMenu() {
|
|
let header_right = document.getElementById('burger-btn');
|
|
header_right.parentElement.addEventListener('click', function(e){
|
|
// console.log('click header_right', this);
|
|
this.firstElementChild.toggleAttribute('opened');
|
|
this.firstElementChild.nextElementSibling.toggleAttribute('opened');
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Scroll lissé (Lenis) sur le conteneur `main` — et non la fenêtre, car
|
|
* c'est `main[role="main"]` qui porte l'overflow. Lenis est branché sur le
|
|
* ticker GSAP (une seule boucle RAF) et pousse ses updates à ScrollTrigger
|
|
* pour que la synchro des colonnes reste calée sur le scroll lissé.
|
|
*/
|
|
function initSmoothScroll() {
|
|
const scroller = document.querySelector('main[role="main"]');
|
|
const content = scroller && scroller.querySelector('.layout-content');
|
|
if (!scroller || !content) return;
|
|
|
|
lenis = new Lenis({
|
|
wrapper: scroller, // conteneur scrollable custom
|
|
content: content, // contenu réel (le 1er enfant est l'ancre skip-link)
|
|
duration: 1.2,
|
|
smoothWheel: true,
|
|
});
|
|
|
|
lenis.on('scroll', ScrollTrigger.update);
|
|
gsap.ticker.add((time) => lenis.raf(time * 1000));
|
|
gsap.ticker.lagSmoothing(0);
|
|
|
|
// Le recalcul post-load (lenis.resize + ScrollTrigger.refresh) est
|
|
// centralisé dans initPageFull pour garantir l'ordre : gonflage du spacer
|
|
// → lenis.resize → refresh. Le faire ici (avant le gonflage) donnerait à
|
|
// Lenis une plage de scroll périmée sur les pages courtes (cf. node/586).
|
|
|
|
return lenis;
|
|
}
|
|
|
|
/**
|
|
* Synchronise le scroll des deux colonnes du layout 50/50 : la colonne la
|
|
* plus courte est ralentie (translateY décroissant) pour que son bas soit
|
|
* atteint en même temps que celui de la colonne la plus haute, qui défile
|
|
* nativement (flux normal, pas de pin). Le scroll réel a lieu dans
|
|
* `main[role="main"]`, pas dans la fenêtre : GSAP utilise donc ce conteneur
|
|
* comme `scroller`.
|
|
*/
|
|
function initHomeColumnScrollSync() {
|
|
console.log('initHomeColumnScrollSync');
|
|
|
|
const firstColumn = document.querySelector('.layout--twocol-section--50-50 .layout__region--first');
|
|
const secondColumn = document.querySelector('.layout--twocol-section--50-50 .layout__region--second');
|
|
console.log(scroller, firstColumn, secondColumn);
|
|
|
|
if (!scroller || !firstColumn || !secondColumn) return;
|
|
|
|
|
|
gsap_mm.add('(min-width: 768px)', () => {
|
|
[firstColumn, secondColumn].forEach((column) => {
|
|
gsap.to(column, {
|
|
// Distance à parcourir pour que le bas de la colonne arrive en
|
|
// bas du viewport en même temps que la colonne la plus haute.
|
|
y: () => {
|
|
const visibleHeight = scroller.clientHeight;
|
|
const scrollRange = scroller.scrollHeight - scroller.clientHeight;
|
|
const columnRevealRange = Math.max(0, column.offsetHeight - visibleHeight);
|
|
return scrollRange - columnRevealRange;
|
|
},
|
|
ease: 'none',
|
|
scrollTrigger: {
|
|
scroller: scroller,
|
|
start: 0,
|
|
end: 'max', // scroll maximum du conteneur (robuste au resize)
|
|
// Valeur numérique = easing : l'animation met ~0.6s à rattraper la
|
|
// position de scroll au lieu de la suivre au pixel près → lisse les
|
|
// à-coups de la colonne lente. Monter la valeur = plus d'inertie.
|
|
scrub: true,
|
|
invalidateOnRefresh: true,
|
|
},
|
|
});
|
|
});
|
|
|
|
// Nettoyage automatique des tweens/triggers quand on repasse sous 768px.
|
|
return () => {};
|
|
});
|
|
}
|
|
|
|
function initPageFull(){
|
|
console.log('initPageFull');
|
|
|
|
|
|
const headerScaleTarget = 0.4;
|
|
|
|
const cols = document.querySelector('article.node-type-projet.view-mode-full .cols, article.node-type-evenement.view-mode-full .cols');
|
|
const leftColumn = cols && cols.querySelector('.col.left');
|
|
const rightColumn = cols && cols.querySelector('.col.right .field_images');
|
|
const spacer = leftColumn && leftColumn.querySelector('.spacer');
|
|
// Hauteur CSS de base du spacer (5em), mesurée AVANT tout gonflage JS.
|
|
const spacerBaseHeight = spacer ? (parseFloat(getComputedStyle(spacer).height) || 0) : 0;
|
|
|
|
const pageHeader = document.querySelector('\
|
|
article.node-type-projet.view-mode-full .cols>.col.left>header,\
|
|
article.node-type-evenement.view-mode-full .cols>.col.left>header\
|
|
');
|
|
console.log('pageHeader', pageHeader);
|
|
|
|
if (!pageHeader && !(cols && leftColumn)) return;
|
|
|
|
// Un seul contexte de timing pour TOUT le comportement de la page full
|
|
// (scale-down du titre + gel de la colonne gauche + contexte sticky des
|
|
// images). Le double rAF attend la hauteur réelle du titre après Splitting ;
|
|
// layoutSync() dimensionne les colonnes AVANT la création des ScrollTriggers.
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
gsap_mm.add('(min-width: 768px)', () => {
|
|
|
|
const viewport = () => scroller.clientHeight;
|
|
|
|
// Amplitude du scale-down du titre, en px de scroll.
|
|
const titleEnd = () => pageHeader ? pageHeader.offsetHeight * (1 - headerScaleTarget) : 0;
|
|
|
|
// Portion du spacer ajoutée par le JS (hors hauteur CSS de base 5em).
|
|
const spacerAdded = () => spacer
|
|
? Math.max(0, (parseFloat(getComputedStyle(spacer).height) || 0) - spacerBaseHeight)
|
|
: 0;
|
|
// Hauteur du contenu RÉEL de la colonne gauche (sans le gonflage JS).
|
|
const leftContentHeight = () => leftColumn ? leftColumn.offsetHeight - spacerAdded() : 0;
|
|
|
|
// Hauteur en flux de la pile d'images (les .field-item sont sticky,
|
|
// donc toujours dans le flux) et hauteur de la dernière image.
|
|
const imagesFlow = () => {
|
|
if (!rightColumn) return 0;
|
|
let sum = 0;
|
|
rightColumn.querySelectorAll('.field-item').forEach(item => { sum += item.offsetHeight; });
|
|
return sum;
|
|
};
|
|
const lastImageHeight = () => {
|
|
if (!rightColumn) return 0;
|
|
const last = rightColumn.querySelector('.field-item:last-child');
|
|
return last ? last.offsetHeight : 0;
|
|
};
|
|
|
|
// Spacer gauche : garantit SEULEMENT assez de scroll pour dérouler le
|
|
// scale-down du titre quand le contenu est trop court (peu/pas d'image
|
|
// + texte bref → node/586). Minimal, pour NE PAS sur-scroller (sinon en
|
|
// fin de scroll tout est parti en haut → node/572).
|
|
const ensureScrollForTitle = () => {
|
|
if (!spacer || !pageHeader) return;
|
|
const end = titleEnd();
|
|
const scrollRange = scroller.scrollHeight - scroller.clientHeight;
|
|
if (scrollRange < end) {
|
|
const add = Math.max(0, end + viewport() - leftContentHeight());
|
|
spacer.style.height = (spacerBaseHeight + add) + 'px';
|
|
void scroller.offsetHeight;
|
|
}
|
|
};
|
|
|
|
// Contexte sticky de la colonne droite : le `:after` étend .field_images
|
|
// sous la dernière image. Hauteur = MAX de deux besoins (aucun ne
|
|
// référence scrollRange → pas de boucle de rétroaction) :
|
|
// - `leftH - imagesFlow` : couvrir tout le scroll d'une gauche haute
|
|
// (texte long) pour que la pile reste figée jusqu'au bout (node/573) ;
|
|
// - `viewport - dernièreImage` : le « rab » de scroll pour que la
|
|
// DERNIÈRE image atteigne le haut de la pile et y termine le scroll.
|
|
// Sans lui, sur une gauche courte + images, le scroll s'arrête à
|
|
// `imagesFlow - viewport` alors que la dernière image n'atteint le
|
|
// haut qu'à `imagesFlow - dernièreImage` → elle n'y arrive jamais.
|
|
const adjustStickyContext = () => {
|
|
if (!rightColumn) return;
|
|
const extent = Math.max(
|
|
0,
|
|
leftColumn.offsetHeight - imagesFlow(),
|
|
viewport() - lastImageHeight()
|
|
);
|
|
rightColumn.style.setProperty('--sticky-extent', extent + 'px');
|
|
};
|
|
|
|
// Dimensionne les colonnes (spacer gauche puis :after droit, dans cet
|
|
// ordre car l'extent lit leftColumn.offsetHeight), puis resynchronise
|
|
// Lenis sur la nouvelle plage de scroll.
|
|
const layoutSync = () => {
|
|
ensureScrollForTitle();
|
|
adjustStickyContext();
|
|
void scroller.offsetHeight; // force le reflow
|
|
if (lenis) lenis.resize(); // Lenis voit la nouvelle hauteur
|
|
};
|
|
|
|
// Seuil de scroll (px) où la colonne gauche se fige : MAX de
|
|
// - le débordement du contenu réel (bas du texte au bas du viewport) ;
|
|
// - titleEnd (fin du scale-down).
|
|
// Le max sur titleEnd est essentiel : pendant que le titre rétrécit,
|
|
// l'en-tête est épinglé en haut et le contenu remonte pour suivre son
|
|
// bord bas ; un gel démarrant avant la fin du scale-down pousserait
|
|
// l'en-tête vers le bas → « le titre diminue en descendant » (576, 584).
|
|
const leftFreezeStart = () => Math.max(0, leftContentHeight() - viewport(), titleEnd());
|
|
|
|
// 1) Colonnes dimensionnées AVANT la création des ScrollTriggers.
|
|
layoutSync();
|
|
|
|
// 2) SCALE-DOWN DU TITRE
|
|
if (pageHeader) {
|
|
gsap.to(pageHeader, {
|
|
scale: headerScaleTarget,
|
|
y: titleEnd, // compense la réduction pour garder le bord haut fixe
|
|
ease: 'none',
|
|
scrollTrigger: {
|
|
scroller: scroller, start: 0, end: titleEnd, scrub: true, invalidateOnRefresh: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
// 3) GEL DE LA COLONNE GAUCHE
|
|
// On NE pin PAS (le pin sort l'élément du flux et casse le flex .cols).
|
|
// On translate : défilement natif jusqu'à `leftFreezeStart`, puis
|
|
// contre-translation 1:1 du scroll → elle se fige en restant un
|
|
// flex-item normal. `.cols` est en `align-items: flex-start`.
|
|
if (cols && leftColumn) {
|
|
gsap.fromTo(leftColumn,
|
|
{ y: 0 },
|
|
{
|
|
y: () => (scroller.scrollHeight - scroller.clientHeight) - leftFreezeStart(),
|
|
ease: 'none',
|
|
scrollTrigger: {
|
|
scroller: scroller, start: leftFreezeStart, end: 'max', scrub: true, invalidateOnRefresh: true,
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
// 4) Recalage quand les hauteurs changent (images/police) : dans
|
|
// l'ordre layoutSync (→ lenis.resize) puis refresh des ScrollTriggers.
|
|
const recalc = () => { layoutSync(); ScrollTrigger.refresh(); };
|
|
const imgs = rightColumn ? Array.from(rightColumn.querySelectorAll('img')) : [];
|
|
imgs.forEach(img => { if (!img.complete) img.addEventListener('load', recalc); });
|
|
window.addEventListener('load', recalc);
|
|
// La police Epilogue peut charger après `load` et changer la hauteur /
|
|
// le nombre de lignes du titre → titleEnd faux (node/584 : titre coupé,
|
|
// parfois sur 3 lignes). On recale quand les polices sont prêtes.
|
|
if (document.fonts && document.fonts.ready) document.fonts.ready.then(recalc);
|
|
|
|
// 5) Nettoyage au repassage < 768px.
|
|
return () => {
|
|
imgs.forEach(img => img.removeEventListener('load', recalc));
|
|
window.removeEventListener('load', recalc);
|
|
if (spacer) spacer.style.height = '';
|
|
if (rightColumn) rightColumn.style.removeProperty('--sticky-extent');
|
|
};
|
|
});
|
|
});
|
|
});
|
|
|
|
}
|
|
|
|
|
|
function initFsLightbox(){
|
|
console.log('initFsLightbox');
|
|
// console.log(window.FsLightbox);
|
|
let imgs_links = document.querySelectorAll('article.node-type-projet.view-mode-full .field_images.multiple .field-item a');
|
|
for (const link of imgs_links) {
|
|
link.setAttribute("data-fslightbox", "gallery")
|
|
}
|
|
// window.FsLightbox
|
|
window.refreshFsLightbox();
|
|
}
|
|
|
|
// function initVues(){
|
|
// console.log('initVues');
|
|
|
|
// // initVueContent();
|
|
|
|
|
|
// }
|
|
|
|
// function initVueContent(){
|
|
// createApp(Content).mount('main[role="main"]');
|
|
|
|
// // processEtapeLinks();
|
|
// }
|
|
|
|
|
|
// function onClickEtapeLink(e){
|
|
// e.preventDefault();
|
|
|
|
// let a = e.currentTarget;
|
|
// let nid = a.dataset.nodeNid;
|
|
// console.log(nid);
|
|
|
|
// getNodeData(nid);
|
|
|
|
// return null;
|
|
// }
|
|
|
|
|
|
// function processEtapeLinks(){
|
|
// let etape_link_fields = document.querySelectorAll('#etapes-liste div.views-field-title');
|
|
// etape_link_fields.forEach((field, index) => {
|
|
// let nid = null;
|
|
// let classList = field.classList;
|
|
// classList.forEach((classe) => {
|
|
// let reg = /data-node-(\d+)/;
|
|
// let result = classe.match(reg);
|
|
// if (result) {
|
|
// nid = result[1];
|
|
// console.log(nid);
|
|
// }
|
|
// })
|
|
|
|
// if (nid) {
|
|
// let a = field.querySelector('a');
|
|
// a.setAttribute('data-node-nid', nid);
|
|
// a.addEventListener('click', onClickEtapeLink);
|
|
// }
|
|
|
|
|
|
// })
|
|
// }
|
|
|
|
// function getNodeData(nid){
|
|
// const params = {
|
|
// }
|
|
// REST.get(`/node/${nid}?_format=json`, params)
|
|
// .then((data) => {
|
|
// console.log('user REST getUser data', data)
|
|
// })
|
|
// .catch(error => {
|
|
// console.warn('Issue with getNodedata', error)
|
|
// Promise.reject(error)
|
|
// })
|
|
// }
|
|
|
|
init()
|
|
} // end LeShedTheme()
|
|
|
|
LeShedTheme()
|
|
})(Drupal, drupalSettings)
|