From 3d9a0421b381c44c9c695c288e55cc194b8608ca Mon Sep 17 00:00:00 2001 From: isUnknown Date: Fri, 27 Feb 2026 15:57:35 +0100 Subject: [PATCH] feat: lightbox desktop pour images et galeries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Lightbox custom avec Swiper (pas de nouvelle dépendance) - Déclenché sur les figures éligibles uniquement (exclut figures dans et dans les cards avec .link-block) - Galeries : ouvre toutes les slides au bon index - Fermeture : bouton ✕, clic overlay, Echap - Captions : figcaption, p.caption (dans figure ou sibling) - Cursor zoom-in desktop uniquement via figure[data-lightbox] Co-Authored-By: Claude Sonnet 4.6 --- assets/css/components/_lightbox.scss | 81 ++++++++++++++ assets/css/style.scss | 1 + assets/js/lightbox.js | 162 +++++++++++++++++++++++++++ assets/js/script.js | 2 + 4 files changed, 246 insertions(+) create mode 100644 assets/css/components/_lightbox.scss create mode 100644 assets/js/lightbox.js diff --git a/assets/css/components/_lightbox.scss b/assets/css/components/_lightbox.scss new file mode 100644 index 0000000..d4dcb5b --- /dev/null +++ b/assets/css/components/_lightbox.scss @@ -0,0 +1,81 @@ +#lightbox { + position: fixed; + inset: 0; + z-index: calc(var(--z-header) + 10); + background-color: rgba(0, 0, 0, 0.92); + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + visibility: hidden; + pointer-events: none; + transition: opacity 0.3s ease, visibility 0.3s ease; + + body.lightbox-open & { + opacity: 1; + visibility: visible; + pointer-events: auto; + } +} + +#lightbox-close { + position: absolute; + top: var(--padding-body); + right: var(--padding-body); + background: none; + border: none; + color: var(--color-txt); + cursor: pointer; + padding: 8px; + z-index: 1; + line-height: 0; + + svg { + width: 30px; + fill: var(--color-txt); + transition: fill 0.2s ease; + } + + &:hover svg { + fill: var(--grey-400); + } +} + +#lightbox-swiper { + width: 90vw; + + .swiper-slide { + display: flex; + align-items: center; + justify-content: center; + + figure { + display: flex; + flex-direction: column; + align-items: center; + cursor: default; + + img { + max-width: 90vw; + max-height: calc(90dvh - 100px); + width: auto; + height: auto; + object-fit: contain; + } + + figcaption { + margin-top: calc(var(--spacing) * 0.5); + color: var(--color-txt-light); + font-size: var(--fs-small); + text-align: center; + } + } + } +} + +// Cursor zoom-in sur les figures éligibles — desktop uniquement +@media (min-width: 1080px) { + figure[data-lightbox] { + cursor: zoom-in; + } +} diff --git a/assets/css/style.scss b/assets/css/style.scss index 974c0d9..3a46dd1 100644 --- a/assets/css/style.scss +++ b/assets/css/style.scss @@ -27,6 +27,7 @@ @import "components/card-block-small"; @import "components/card-open-graph"; @import "components/swiper"; +@import "components/lightbox"; @import "components/slider-before-after"; @import "components/dl-table"; @import "components/bottom-bar"; diff --git a/assets/js/lightbox.js b/assets/js/lightbox.js new file mode 100644 index 0000000..eed3f3e --- /dev/null +++ b/assets/js/lightbox.js @@ -0,0 +1,162 @@ +import Swiper from 'https://cdn.jsdelivr.net/npm/swiper@12/swiper-bundle.min.mjs'; + +const DESKTOP = window.matchMedia('(min-width: 1080px)'); + +// Inline du close.svg (assets/icons/close.svg) +const CLOSE_SVG = ``; + +let lightboxEl = null; +let lightboxSwiper = null; + +function createLightboxDOM() { + const el = document.createElement('div'); + el.id = 'lightbox'; + el.setAttribute('aria-modal', 'true'); + el.setAttribute('role', 'dialog'); + el.setAttribute('aria-label', 'Image agrandie'); + el.setAttribute('inert', ''); + + el.innerHTML = ` + + + `; + + document.body.appendChild(el); + + el.querySelector('#lightbox-close').addEventListener('click', closeLightbox); + + // Fermeture au clic sur le fond (pas sur le swiper) + el.addEventListener('click', (e) => { + if (e.target === el) closeLightbox(); + }); + + return el; +} + +function ensureLightboxDOM() { + if (!lightboxEl) { + lightboxEl = createLightboxDOM(); + } +} + +function getImageData(figure) { + const img = figure.querySelector('img'); + + // 1. figcaption dans la figure + // 2. p.caption dans la figure (horizontal-gallery) + // 3. p.caption sibling de la figure (covers : resource, news-item, impact…) + const captionEl = + figure.querySelector('figcaption') || + figure.querySelector('p.caption') || + (figure.nextElementSibling?.matches('p.caption') ? figure.nextElementSibling : null); + + return { + src: img?.src || '', + alt: img?.alt || '', + caption: captionEl?.innerHTML || '', + }; +} + +function buildSlides(images) { + const wrapper = lightboxEl.querySelector('.swiper-wrapper'); + wrapper.innerHTML = ''; + + images.forEach(({ src, alt, caption }) => { + const slide = document.createElement('div'); + slide.className = 'swiper-slide'; + slide.innerHTML = ` +
+ ${alt} + ${caption ? `
${caption}
` : ''} +
+ `; + wrapper.appendChild(slide); + }); +} + +function openLightbox(images, startIndex = 0) { + ensureLightboxDOM(); + buildSlides(images); + + const swiperEl = lightboxEl.querySelector('#lightbox-swiper'); + + // Détruire l'instance précédente si elle existe + if (swiperEl.swiper) { + swiperEl.swiper.destroy(true, true); + } + + lightboxSwiper = new Swiper(swiperEl, { + slidesPerView: 1, + initialSlide: startIndex, + speed: 400, + watchOverflow: true, + keyboard: { enabled: true }, + navigation: { + nextEl: swiperEl.querySelector('.swiper-button-next'), + prevEl: swiperEl.querySelector('.swiper-button-prev'), + }, + pagination: { + el: swiperEl.querySelector('.swiper-pagination'), + clickable: true, + }, + a11y: { + prevSlideMessage: 'Image précédente', + nextSlideMessage: 'Image suivante', + }, + }); + + lightboxEl.removeAttribute('inert'); + document.body.classList.add('lightbox-open'); + lightboxEl.querySelector('#lightbox-close').focus(); +} + +function closeLightbox() { + document.body.classList.remove('lightbox-open'); + lightboxEl.setAttribute('inert', ''); +} + +function isEligible(figure) { + // Exclure les figures dans un
+ if (figure.closest('a')) return false; + // Exclure si le parent direct contient un .link-block (card navigable) + if (figure.parentElement.querySelector(':scope > .link-block')) return false; + return true; +} + +export function initLightbox() { + // Fermeture à la touche Echap + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && document.body.classList.contains('lightbox-open')) { + closeLightbox(); + } + }); + + document.querySelectorAll('figure').forEach((figure) => { + if (!isEligible(figure)) return; + + // Marquer les figures éligibles (pour le cursor CSS) + figure.setAttribute('data-lightbox', ''); + + figure.addEventListener('click', () => { + if (!DESKTOP.matches) return; + + const slide = figure.closest('.swiper-slide'); + + if (slide) { + // Galerie : ouvrir toutes les images du swiper parent + const swiperEl = slide.closest('.swiper'); + const allFigures = [...swiperEl.querySelectorAll('.swiper-slide > figure')]; + const startIndex = allFigures.indexOf(figure); + openLightbox(allFigures.map(getImageData), startIndex); + } else { + // Image standalone + openLightbox([getImageData(figure)], 0); + } + }); + }); +} diff --git a/assets/js/script.js b/assets/js/script.js index a568a39..31198a9 100644 --- a/assets/js/script.js +++ b/assets/js/script.js @@ -9,6 +9,7 @@ import { initSliderBeforeAfter} from './sliderBeforeAfter.js'; import { navInvestigation } from './investigation.js'; import { progressBar, scrollBack} from './bottom-bar.js'; import { initSort } from './sort.js'; +import { initLightbox } from './lightbox.js'; const responsiveMedium = 1080; const responsiveSmall = 768; @@ -27,6 +28,7 @@ window.onload = async function () { playVideo(); initDropdowns(responsiveSmall, responsiveSmallX); initSwipers(); + initLightbox(); progressBar();