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)); }); } });