export default class JustifiedGallery { constructor(container, options = {}) { this.container = container; this.options = { margin: options.margin || 5, columns: options.columns || 3, breakpoints: options.breakpoints || { 768: 2, 480: 1 }, wrapLinks: options.wrapLinks || false, // active l'encapsulation linkAttribute: options.linkAttribute || 'src', // attribut à utiliser pour href justifyLastRow: options.justifyLastRow || false, }; this.currentIndex = 0; this.isOpenModal = false; this.init(); window.addEventListener('resize', () => this.layoutGallery()); } init() { this.images = this.getImages(); // Encapsule les images si nécessaire if (this.options.wrapLinks) { this.images.forEach(img => this.wrapImage(img)); this.images = this.getImages(); // rafraîchit la liste } // S'assure que toutes les images sont chargées const promises = this.images.map(img => this.waitForImageLoad(img)); Promise.all(promises).then(() => { this.layoutGallery(); this.setupModal(); }); this.container.addEventListener('keydown', e => { e.preventDefault(); if (e.key === 'ArrowRight') { e.target.nextSibling.nextSibling.focus() } if (e.key === "ArrowLeft") { e.target.previousSibling.previousSibling.focus() } if (e.key === "ArrowDown") { const images = this.getImages(); let target = e.target; for (let i = 0; i < this.options.columns; i++) { if (Math.ceil(images.length / this.options.columns) <= i) { // On récupère le dernier élément si on dépasse la dernière ligne target = images[images.length - 1].parentNode; break; } // Utiliser nextElementSibling pour éviter les text nodes if (target.nextElementSibling) { target = target.nextElementSibling; } } if (target) { target.focus(); // Scroll pour voir la ligne où se trouve l'image focus target.scrollIntoView({ behavior: 'smooth', block: 'center' }); } } if (e.key === "ArrowUp") { let target = e.target for (let i = 0; this.options.columns > i; i++) { if (this.getImages().length < i) { target = this.getImages()[0]; break; } target = target.previousSibling.previousSibling } if (target) { target.focus(); // Scroll pour voir la ligne où se trouve l'image focus target.scrollIntoView({ behavior: 'smooth', block: 'center' }); } } if (e.key === "Enter") { e.target.click() } }) } wrapImage(img) { const href = img.getAttribute(this.options.linkAttribute) || img.src; const a = document.createElement('a'); a.href = href; a.style.display = 'inline-block'; img.parentNode.insertBefore(a, img); a.appendChild(img); } getImages() { return Array.from(this.container.querySelectorAll('img')); } waitForImageLoad(img) { return new Promise(resolve => { if (img.complete) resolve(); else { img.onload = () => resolve(); img.onerror = () => resolve(); } }); } getColumns() { const width = this.container.clientWidth; let cols = this.options.columns; for (let bp in this.options.breakpoints) { if (width <= bp) cols = this.options.breakpoints[bp]; } return cols; } layoutGallery() { const containerWidth = this.container.clientWidth; const margin = this.options.margin; const cols = this.getColumns(); let rowImages = []; let totalRatio = 0; this.images.forEach((img, index) => { const ratio = img.naturalWidth / img.naturalHeight; if (this.options.wrapLinks) { rowImages.push({ img: img.parentNode, ratio }); } else { rowImages.push({ img, ratio }); } totalRatio += ratio; const isLastRow = index === this.images.length - 1; if (rowImages.length === cols || isLastRow) { let rowHeight; if (isLastRow && rowImages.length < cols && !this.options.justifyLastRow) { // Ne pas étirer la dernière ligne const avgRatio = totalRatio / rowImages.length; rowHeight = Math.floor((containerWidth / cols) / avgRatio); } else { // Étire normalement pour remplir la ligne rowHeight = Math.floor((containerWidth - margin * (rowImages.length - 1)) / totalRatio); } this.scaleRow(rowImages, rowHeight, margin); rowImages = []; totalRatio = 0; } }); } scaleRow(row, rowHeight, margin) { row.forEach((item, index) => { const width = rowHeight * item.ratio; if (this.options.wrapLinks) { item.img.style.overflow = 'hidden'; item.img.firstChild.style.width = '100%'; } item.img.style.width = `${width}px`; item.img.style.height = `${rowHeight}px`; item.img.style.marginRight = index < row.length - 1 ? `${margin}px` : '0'; item.img.style.marginBottom = `${margin}px`; item.img.style.cursor = 'pointer'; }); } setupModal() { // Création du modal this.modal = document.createElement('div'); this.modal.classList.add('jg-modal'); this.modal.innerHTML = ` ×
`; document.body.appendChild(this.modal); // Références this.modalImg = this.modal.querySelector('.jg-modal-img'); this.closeBtn = this.modal.querySelector('.jg-close'); this.prevBtn = this.modal.querySelector('.jg-prev'); this.nextBtn = this.modal.querySelector('.jg-next'); // Événements this.images.forEach((img, idx) => { img.addEventListener('click', e => { e.preventDefault(); // empêche le clic sur le lien this.openModal(idx); }); img.parentNode.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); this.openModal(idx); } }) }); this.closeBtn.addEventListener('click', () => this.closeModal()); this.prevBtn.addEventListener('click', () => this.prevImage()); this.nextBtn.addEventListener('click', () => this.nextImage()); this.modal.addEventListener('click', e => { if (e.target === this.modal) this.closeModal(); }); document.addEventListener('keydown', e => { if (this.modal.style.display === 'flex') { if (e.key === 'ArrowLeft') this.prevImage(); if (e.key === 'ArrowRight') this.nextImage(); if (e.key === 'Escape') this.closeModal(); } }); // Variables pour drag + zoom this.modalImg.currentScale = 1; let isDragging = false; let startX, startY; let currentTranslateX = 0, currentTranslateY = 0; // Drag start this.modalImg.addEventListener('mousedown', e => { if (e.button !== 0) return; isDragging = true; startX = e.clientX; startY = e.clientY; this.modalImg.style.cursor = 'grabbing'; e.preventDefault(); }); // Drag move this.modal.addEventListener('mousemove', e => { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; startX = e.clientX; startY = e.clientY; currentTranslateX += dx; currentTranslateY += dy; this.modalImg.style.transform = `translate(${currentTranslateX}px, ${currentTranslateY}px) scale(${this.modalImg.currentScale})`; }); // Drag end this.modal.addEventListener('mouseup', () => { isDragging = false; this.modalImg.style.cursor = 'grab'; }); this.modal.addEventListener('mouseleave', () => { isDragging = false; this.modalImg.style.cursor = 'grab'; }); // Scroll pour zoom this.modalImg.addEventListener('wheel', e => { e.preventDefault(); const scaleAmount = e.deltaY > 0 ? -0.1 : 0.1; this.modalImg.currentScale = Math.min(Math.max(this.modalImg.currentScale + scaleAmount, 0.5), 3); // limites min/max this.modalImg.style.transform = `translate(${currentTranslateX}px, ${currentTranslateY}px) scale(${this.modalImg.currentScale})`; }); } openModal(index) { this.isOpenModal = true; this.currentIndex = index; this.modal.style.display = 'flex'; this.modalImg.src = this.images[this.currentIndex].src; document.body.style.overflow = 'hidden'; } closeModal() { this.isOpenModal = false; this.modal.style.display = 'none'; document.body.style.overflow = 'scroll'; // Reset zoom et position this.modalImg.currentScale = 1; this.modalImg.style.transform = `translate(0px, 0px) scale(1)`; currentTranslateX = 0; currentTranslateY = 0; } prevImage() { this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length; this.modalImg.src = this.images[this.currentIndex].src; } nextImage() { this.currentIndex = (this.currentIndex + 1) % this.images.length; this.modalImg.src = this.images[this.currentIndex].src; } } class Gallery { constructor(container, options = {}) { this.container = container; this.options = { margin: options.margin || 5, columns: options.columns || 4, // nombre de colonnes par défaut breakpoints: options.breakpoints || { 768: 2, 480: 1 }, // largeur => colonnes }; this.init(); window.addEventListener('resize', () => this.layoutGallery()); } init() { this.images = this.getImages(); // On attend que toutes les images soient chargées pour avoir leurs dimensions naturelles const promises = this.images.map(img => this.waitForImageLoad(img)); Promise.all(promises).then(() => this.layoutGallery()); } getImages() { return Array.from(this.container.querySelectorAll('img')); } waitForImageLoad(img) { return new Promise(resolve => { if (img.complete) resolve(); else { img.onload = () => resolve(); img.onerror = () => resolve(); } }); } calculateRatio(img) { return img.naturalWidth / img.naturalHeight; } getColumns() { const width = this.container.clientWidth; let cols = this.options.columns; // Vérifie les breakpoints pour adapter le nombre de colonnes for (let bp in this.options.breakpoints) { if (width <= bp) { cols = this.options.breakpoints[bp]; } } return cols; } layoutGallery() { const containerWidth = this.container.clientWidth; const margin = this.options.margin; const cols = this.getColumns(); const rowHeight = (containerWidth - margin * (cols - 1)) / cols; // largeur d'une "colonne" comme base let rowImages = []; this.images.forEach((img, index) => { const ratio = this.calculateRatio(img); rowImages.push({ img, ratio }); if (rowImages.length === cols || index === this.images.length - 1) { this.scaleRow(rowImages, rowHeight, margin); rowImages = []; } }); } scaleRow(row, rowHeight, margin) { row.forEach((item, index) => { const width = rowHeight * item.ratio; item.img.style.width = `${width}px`; item.img.style.height = `${rowHeight}px`; item.img.style.marginRight = index < row.length - 1 ? `${margin}px` : '0'; item.img.style.marginBottom = `${margin}px`; }); } }