custom masonry on page archhives projets

This commit is contained in:
2026-10-26 21:02:33 +01:00
parent bf8f265077
commit f4961e9679
4 changed files with 120 additions and 8 deletions
@@ -69,6 +69,7 @@ gsap.registerPlugin(ScrollTrigger);
initHomeColumnScrollSync();
initFsLightbox();
initPageFull();
initMasonry();
// initVues()
// titles.js traite les titres hors écran par lots, après la première
@@ -351,6 +352,102 @@ gsap.registerPlugin(ScrollTrigger);
window.refreshFsLightbox();
}
// __ __ _ _ _
// | \/ | ___ _ __ ___| |__ ___ _ __ ___ ___| |_| |
// | |\/| |/ _ \| '_ \ / __| '_ \ / _ \ '__/ _ \/ __| __| |
// | | | | (_) | | | | (__| | | | __/ | | __/\__ \ |_|_|
// |_| |_|\___/|_| |_|\___|_| |_|\___|_| \___||___/\__(_)
//
// Masonry maison pour la grille des projets (.view-content de
// views-archives-projets) : CSS Grid + grid-auto-rows fin + span N par
// item. Pas de librairie ; l'ordre gauche→droite de la vue est préservé
// (auto-placement sparse, dense inutile : les trous résiduels font moins
// d'une rangée virtuelle). Sans JS (ou avant le premier pack), la grille
// CSS actuelle reste le fallback : des trous, mais rien de cassé.
function initMasonry(){
const containers = document.querySelectorAll('.views-archives-projets .view-content');
if (!containers.length) return;
// Granularité d'une rangée virtuelle : assez fine pour que les trous
// résiduels (≤ BASE) soient invisibles, assez grosse pour des spans
// raisonnables.
const BASE = 8; // px
const pack = (container) => {
const items = container.querySelectorAll(':scope > .views-row');
if (!items.length) return;
// Gouttière verticale : row-gap de la grille (0 ici — la gouttière
// vit dans le margin-bottom des items) ; le pas de quantification
// du span est donc BASE et non BASE + gouttière.
const rowGap = parseFloat(getComputedStyle(container).rowGap) || 0;
// Dense : sans lui, le placement sparse ne recule jamais — un item
// haut qui ne rentre pas dans un trou le saute, et les suivants ne
// peuvent plus y revenir (trous visibles sous la première rangée).
// Dense renvoie chaque item au premier trou où il tient, quitte à
// inverser localement l'ordre visuel — comportement standard d'un
// masonry.
container.style.gridAutoFlow = 'row dense';
// Les images portent width/height (ratio réservé au layout) et
// align-items: start (SCSS) garantit qu'un item garde sa hauteur de
// contenu, spans appliqués ou non. On mesure donc SANS reset des
// spans : pas de re-layout complet de la grille à chaque pack —
// chaque pack se résume à lire N hauteurs et n'écrire que les spans
// qui changent (sinon chaque image chargée pendant le scroll
// déclenchait deux layouts forcés des 110 items, d'où le lag).
items.forEach((item) => {
const cs = getComputedStyle(item);
const h = item.offsetHeight
+ (parseFloat(cs.marginTop) || 0)
+ (parseFloat(cs.marginBottom) || 0);
const span = Math.max(1, Math.ceil((h + rowGap) / (BASE + rowGap)));
if (item._masonrySpan !== span) {
item._masonrySpan = span;
item.style.gridRowEnd = `span ${span}`;
}
});
container.style.gridAutoRows = `${BASE}px`;
};
const schedule = (container) => {
// Une seule passe par frame, même si dix images lazy arrivent coup
// sur coup.
if (container._masonryRaf) return;
container._masonryRaf = requestAnimationFrame(() => {
container._masonryRaf = null;
pack(container);
});
};
containers.forEach((container) => {
// Toute variation de largeur (seuil mobile : 4→2 colonnes, zoom,
// rotation) repasse par un pack complet.
new ResizeObserver(() => schedule(container)).observe(container);
// Le contenu d'une carte peut changer de hauteur APRÈS un pack :
// troncature des h2 par titles.js (data-overflow-lines), chargement
// des images, polices… Or la views-row, étirée sur son span, ne
// change PAS de taille quand son contenu rétrécit : le span reste
// surdimensionné (l'espace mort dans la row) et aucun déclencheur
// extérieur ne le voit. On observe donc directement les articles.
const contentRO = new ResizeObserver(() => schedule(container));
container.querySelectorAll(':scope > .views-row > article').forEach((a) => contentRO.observe(a));
// Images lazy : chaque chargement change la hauteur d'un item.
container.addEventListener('load', () => schedule(container), true);
// La police variable (Epilogue) charge après le layout initial, et
// titles.js retouche les h2 par lots : les deux changent des hauteurs.
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(() => schedule(container));
}
document.addEventListener('leshed:titles-ready', () => schedule(container), { once: true });
schedule(container);
});
}
// function initVues(){
// console.log('initVues');