388 lines
13 KiB
JavaScript
388 lines
13 KiB
JavaScript
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 = `
|
|
<span class="jg-close">×</span>
|
|
<img class="jg-modal-img" src="">
|
|
<div class="jg-controls">
|
|
<span class="jg-prev">❮</span>
|
|
<span class="jg-next">❯</span>
|
|
</div>
|
|
`;
|
|
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`;
|
|
});
|
|
}
|
|
}
|
|
|