class Gallery {
|
constructor(modal, config = {}) {
|
this.container = config.container
|
? document.querySelector(config.container)
|
: document.querySelector('main');
|
|
this.gallerySelector = config.gallerySelector || 'img[data-small]';
|
|
this.modal = new window.jvbModal(modal, {
|
onOpen: false,
|
onSave: false,
|
onClose: () => this.closeGallery(false)
|
});
|
|
this.modalElement = (typeof modal === 'string')
|
? document.querySelector(modal)
|
: modal;
|
|
if (!this.modalElement) return;
|
|
this.a11y = window.jvbA11y;
|
this.index = 0;
|
this.items = [];
|
|
this.swipe = {
|
touchStart: null,
|
touchEnd: null,
|
minSwipe: 50,
|
};
|
|
this.initElements();
|
this.initListeners();
|
}
|
|
initElements() {
|
this.prevBtn = this.modalElement.querySelector('.prev');
|
this.nextBtn = this.modalElement.querySelector('.next');
|
this.image = this.modalElement.querySelector('.image');
|
this.counter = this.modalElement.querySelector('.counter');
|
}
|
|
initListeners() {
|
// Delegate click handling to container
|
this.container.addEventListener('click', this.handleClick.bind(this));
|
|
// Modal-specific listeners added only when open
|
this.boundKeyHandler = this.handleKeys.bind(this);
|
this.boundTouchStart = this.handleTouchStart.bind(this);
|
this.boundTouchMove = this.handleTouchMove.bind(this);
|
this.boundTouchEnd = this.handleTouchEnd.bind(this);
|
}
|
|
handleClick(e) {
|
// Open gallery when clicking images
|
const img = e.target.closest(this.gallerySelector);
|
if (img && !this.modal.isOpen) {
|
e.preventDefault();
|
this.openGallery(img);
|
return;
|
}
|
|
// Navigation within gallery
|
if (this.modal.isOpen) {
|
if (e.target.closest('.next')) {
|
this.navigate(1);
|
} else if (e.target.closest('.prev')) {
|
this.navigate(-1);
|
}
|
}
|
}
|
|
handleKeys(e) {
|
if (!this.modal.isOpen) return;
|
|
switch (e.key) {
|
case 'ArrowLeft':
|
e.preventDefault();
|
this.navigate(-1);
|
break;
|
case 'ArrowRight':
|
e.preventDefault();
|
this.navigate(1);
|
break;
|
}
|
}
|
|
handleTouchStart(e) {
|
if (!this.modal.isOpen) return;
|
this.swipe.touchStart = e.touches[0].clientX;
|
}
|
|
handleTouchMove(e) {
|
if (!this.modal.isOpen) return;
|
this.swipe.touchEnd = e.touches[0].clientX;
|
}
|
|
handleTouchEnd(e) {
|
if (!this.modal.isOpen || !this.swipe.touchStart || !this.swipe.touchEnd) return;
|
|
const distance = this.swipe.touchStart - this.swipe.touchEnd;
|
|
if (Math.abs(distance) > this.swipe.minSwipe) {
|
this.navigate(distance > 0 ? 1 : -1);
|
}
|
|
this.swipe.touchStart = null;
|
this.swipe.touchEnd = null;
|
}
|
|
buildGalleryItems() {
|
return Array.from(this.container.querySelectorAll(this.gallerySelector))
|
.map((img, index) => ({
|
id: img.dataset.id || index,
|
small: img.dataset.small || img.src,
|
medium: img.dataset.medium || img.src,
|
full: img.dataset.full || img.src,
|
alt: img.alt || '',
|
element: img
|
}));
|
}
|
|
openGallery(clickedImg) {
|
// Build fresh gallery items
|
this.items = this.buildGalleryItems();
|
|
// Find clicked image index
|
this.index = this.items.findIndex(item =>
|
item.element === clickedImg
|
);
|
|
if (this.index === -1) this.index = 0;
|
|
// Attach modal-specific listeners
|
document.addEventListener('keydown', this.boundKeyHandler);
|
document.addEventListener('touchstart', this.boundTouchStart, { passive: true });
|
document.addEventListener('touchmove', this.boundTouchMove, { passive: true });
|
document.addEventListener('touchend', this.boundTouchEnd, { passive: true });
|
|
this.modal.handleOpen();
|
this.updateDisplay();
|
this.preloadAdjacent();
|
|
this.a11y.announce(
|
`Gallery opened. Image ${this.index + 1} of ${this.items.length}. Use arrow keys to navigate.`
|
);
|
}
|
|
closeGallery(useModal = true) {
|
// Remove modal-specific listeners
|
document.removeEventListener('keydown', this.boundKeyHandler);
|
document.removeEventListener('touchstart', this.boundTouchStart);
|
document.removeEventListener('touchmove', this.boundTouchMove);
|
document.removeEventListener('touchend', this.boundTouchEnd);
|
|
if (useModal) {
|
this.modal.handleClose();
|
}
|
}
|
|
navigate(direction) {
|
let newIndex = this.index + direction;
|
|
// Wrap around
|
if (newIndex < 0) {
|
newIndex = this.items.length - 1;
|
} else if (newIndex >= this.items.length) {
|
newIndex = 0;
|
} else if (this.items.length - newIndex === 3) {
|
this.notify('load-more');
|
}
|
|
this.index = newIndex;
|
this.updateDisplay();
|
this.preloadAdjacent();
|
|
this.a11y.announce(`Image ${this.index + 1} of ${this.items.length}`);
|
}
|
|
updateDisplay() {
|
const item = this.items[this.index];
|
if (!item) return;
|
|
// Use medium/full based on viewport
|
const src = window.innerWidth < 1000
|
? (item.medium || item.src)
|
: (item.full || item.src);
|
|
this.image.src = src;
|
this.image.alt = item.alt;
|
this.counter.textContent = `${this.index + 1} / ${this.items.length}`;
|
|
// Update button states
|
this.prevBtn.classList.toggle('disabled', this.items.length <= 1);
|
this.nextBtn.classList.toggle('disabled', this.items.length <= 1);
|
}
|
|
preloadAdjacent() {
|
[-1, 1].forEach(offset => {
|
const index = this.index + offset;
|
if (index >= 0 && index < this.items.length) {
|
const item = this.items[index];
|
const img = new Image();
|
img.src = window.innerWidth < 1000
|
? (item.medium || item.src)
|
: (item.full || item.src);
|
}
|
});
|
}
|
|
cleanup() {
|
this.container.removeEventListener('click', this.handleClick);
|
this.closeGallery(false);
|
}
|
}
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
let galleries = document.querySelectorAll('dialog.gallery');
|
if (galleries.length > 0) {
|
window.galleries = new Map();
|
|
galleries.forEach(gallery => {
|
let id = gallery.id;
|
window.galleries.set(id, new Gallery(gallery));
|
});
|
}
|
});
|