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