feat: lightbox desktop pour images et galeries
- Lightbox custom avec Swiper (pas de nouvelle dépendance) - Déclenché sur les figures éligibles uniquement (exclut figures dans <a> 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 <noreply@anthropic.com>
This commit is contained in:
parent
9f2b85087d
commit
3d9a0421b3
4 changed files with 246 additions and 0 deletions
81
assets/css/components/_lightbox.scss
Normal file
81
assets/css/components/_lightbox.scss
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
@import "components/card-block-small";
|
@import "components/card-block-small";
|
||||||
@import "components/card-open-graph";
|
@import "components/card-open-graph";
|
||||||
@import "components/swiper";
|
@import "components/swiper";
|
||||||
|
@import "components/lightbox";
|
||||||
@import "components/slider-before-after";
|
@import "components/slider-before-after";
|
||||||
@import "components/dl-table";
|
@import "components/dl-table";
|
||||||
@import "components/bottom-bar";
|
@import "components/bottom-bar";
|
||||||
|
|
|
||||||
162
assets/js/lightbox.js
Normal file
162
assets/js/lightbox.js
Normal file
|
|
@ -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 = `<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><rect y="22.3249" width="31.1111" height="2.22222" transform="rotate(-45 0 22.3249)"/><rect x="1.80078" width="31.1111" height="2.22222" transform="rotate(45 1.80078 0)"/></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 = `
|
||||||
|
<button id="lightbox-close" aria-label="Fermer">${CLOSE_SVG}</button>
|
||||||
|
<div id="lightbox-swiper" class="swiper">
|
||||||
|
<div class="swiper-wrapper"></div>
|
||||||
|
<div class="swiper-button-prev"></div>
|
||||||
|
<div class="swiper-button-next"></div>
|
||||||
|
<div class="swiper-pagination"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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 = `
|
||||||
|
<figure>
|
||||||
|
<img src="${src}" alt="${alt}">
|
||||||
|
${caption ? `<figcaption>${caption}</figcaption>` : ''}
|
||||||
|
</figure>
|
||||||
|
`;
|
||||||
|
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 <a>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ import { initSliderBeforeAfter} from './sliderBeforeAfter.js';
|
||||||
import { navInvestigation } from './investigation.js';
|
import { navInvestigation } from './investigation.js';
|
||||||
import { progressBar, scrollBack} from './bottom-bar.js';
|
import { progressBar, scrollBack} from './bottom-bar.js';
|
||||||
import { initSort } from './sort.js';
|
import { initSort } from './sort.js';
|
||||||
|
import { initLightbox } from './lightbox.js';
|
||||||
|
|
||||||
const responsiveMedium = 1080;
|
const responsiveMedium = 1080;
|
||||||
const responsiveSmall = 768;
|
const responsiveSmall = 768;
|
||||||
|
|
@ -27,6 +28,7 @@ window.onload = async function () {
|
||||||
playVideo();
|
playVideo();
|
||||||
initDropdowns(responsiveSmall, responsiveSmallX);
|
initDropdowns(responsiveSmall, responsiveSmallX);
|
||||||
initSwipers();
|
initSwipers();
|
||||||
|
initLightbox();
|
||||||
|
|
||||||
progressBar();
|
progressBar();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue