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-open-graph";
|
||||
@import "components/swiper";
|
||||
@import "components/lightbox";
|
||||
@import "components/slider-before-after";
|
||||
@import "components/dl-table";
|
||||
@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 { 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();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue