← back to Jill Website
public/js/gallery.js
283 lines
/**
* Responsive Image Gallery Component
* Features: Lazy loading, lightbox, keyboard navigation, touch gestures
*/
class ImageGallery {
constructor(galleryElement) {
this.gallery = galleryElement;
this.images = Array.from(this.gallery.querySelectorAll('.gallery-item'));
this.currentIndex = 0;
this.lightbox = null;
this.lightboxImage = null;
this.lightboxCaption = null;
this.init();
}
init() {
this.setupLazyLoading();
this.setupLightbox();
this.setupImageClickHandlers();
}
/**
* Setup Intersection Observer for lazy loading images
*/
setupLazyLoading() {
if (!('IntersectionObserver' in window)) {
// Fallback: load all images immediately if IntersectionObserver not supported
this.images.forEach(item => this.loadImage(item));
return;
}
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
observer.unobserve(entry.target);
}
});
}, {
rootMargin: '50px 0px', // Start loading 50px before entering viewport
threshold: 0.01
});
this.images.forEach(item => {
imageObserver.observe(item);
});
}
/**
* Load image from data-src attribute
*/
loadImage(imageItem) {
const img = imageItem.querySelector('img');
const imgSrc = img.getAttribute('data-src');
if (!imgSrc || img.src === imgSrc) return;
// Show loading state
imageItem.classList.add('loading');
const tempImg = new Image();
tempImg.onload = () => {
img.src = imgSrc;
img.classList.add('loaded');
imageItem.classList.remove('loading');
imageItem.classList.add('loaded');
};
tempImg.onerror = () => {
imageItem.classList.remove('loading');
imageItem.classList.add('error');
console.error('Failed to load image:', imgSrc);
};
tempImg.src = imgSrc;
}
/**
* Create and setup lightbox modal
*/
setupLightbox() {
// Create lightbox HTML structure
this.lightbox = document.createElement('div');
this.lightbox.className = 'gallery-lightbox';
this.lightbox.innerHTML = `
<button class="lightbox-close" aria-label="Close lightbox">
<i class="fas fa-times"></i>
</button>
<button class="lightbox-prev" aria-label="Previous image">
<i class="fas fa-chevron-left"></i>
</button>
<button class="lightbox-next" aria-label="Next image">
<i class="fas fa-chevron-right"></i>
</button>
<div class="lightbox-content">
<img class="lightbox-image" src="" alt="">
<div class="lightbox-caption"></div>
<div class="lightbox-counter"></div>
</div>
`;
document.body.appendChild(this.lightbox);
// Cache lightbox elements
this.lightboxImage = this.lightbox.querySelector('.lightbox-image');
this.lightboxCaption = this.lightbox.querySelector('.lightbox-caption');
this.lightboxCounter = this.lightbox.querySelector('.lightbox-counter');
// Setup event listeners
this.lightbox.querySelector('.lightbox-close').addEventListener('click', () => this.closeLightbox());
this.lightbox.querySelector('.lightbox-prev').addEventListener('click', () => this.showPrevious());
this.lightbox.querySelector('.lightbox-next').addEventListener('click', () => this.showNext());
// Close on background click
this.lightbox.addEventListener('click', (e) => {
if (e.target === this.lightbox) {
this.closeLightbox();
}
});
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (!this.lightbox.classList.contains('active')) return;
switch(e.key) {
case 'Escape':
this.closeLightbox();
break;
case 'ArrowLeft':
this.showPrevious();
break;
case 'ArrowRight':
this.showNext();
break;
}
});
// Touch gestures for mobile
this.setupTouchGestures();
}
/**
* Setup touch gestures for mobile swipe navigation
*/
setupTouchGestures() {
let touchStartX = 0;
let touchEndX = 0;
this.lightbox.addEventListener('touchstart', (e) => {
touchStartX = e.changedTouches[0].screenX;
}, { passive: true });
this.lightbox.addEventListener('touchend', (e) => {
touchEndX = e.changedTouches[0].screenX;
this.handleSwipe(touchStartX, touchEndX);
}, { passive: true });
}
/**
* Handle swipe gestures
*/
handleSwipe(startX, endX) {
const swipeThreshold = 50;
const diff = startX - endX;
if (Math.abs(diff) > swipeThreshold) {
if (diff > 0) {
// Swipe left - show next
this.showNext();
} else {
// Swipe right - show previous
this.showPrevious();
}
}
}
/**
* Setup click handlers on gallery images
*/
setupImageClickHandlers() {
this.images.forEach((item, index) => {
item.addEventListener('click', () => {
this.openLightbox(index);
});
// Make items keyboard accessible
item.setAttribute('tabindex', '0');
item.setAttribute('role', 'button');
// Add keyboard support
item.addEventListener('keypress', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.openLightbox(index);
}
});
});
}
/**
* Open lightbox at specific image index
*/
openLightbox(index) {
this.currentIndex = index;
this.showImage(index);
this.lightbox.classList.add('active');
document.body.style.overflow = 'hidden'; // Prevent background scrolling
}
/**
* Close lightbox
*/
closeLightbox() {
this.lightbox.classList.remove('active');
document.body.style.overflow = ''; // Restore scrolling
}
/**
* Show image at specific index
*/
showImage(index) {
const item = this.images[index];
const img = item.querySelector('img');
const imgSrc = img.src || img.getAttribute('data-src');
const imgAlt = img.getAttribute('alt') || '';
const caption = item.getAttribute('data-caption') || imgAlt;
// Update lightbox image
this.lightboxImage.src = imgSrc;
this.lightboxImage.alt = imgAlt;
// Update caption
this.lightboxCaption.textContent = caption;
// Update counter
this.lightboxCounter.textContent = `${index + 1} / ${this.images.length}`;
// Update navigation button states
this.updateNavigationButtons();
}
/**
* Show previous image
*/
showPrevious() {
this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length;
this.showImage(this.currentIndex);
}
/**
* Show next image
*/
showNext() {
this.currentIndex = (this.currentIndex + 1) % this.images.length;
this.showImage(this.currentIndex);
}
/**
* Update navigation button states (disable if at start/end)
*/
updateNavigationButtons() {
const prevBtn = this.lightbox.querySelector('.lightbox-prev');
const nextBtn = this.lightbox.querySelector('.lightbox-next');
// Always enable for circular navigation
prevBtn.disabled = false;
nextBtn.disabled = false;
}
}
/**
* Initialize all galleries on page load
*/
document.addEventListener('DOMContentLoaded', () => {
const galleries = document.querySelectorAll('.image-gallery');
galleries.forEach(gallery => {
new ImageGallery(gallery);
});
console.log('🖼️ Image Gallery Component Initialized');
});