(function() { 'use strict'; // Internal state let currentPhotoIndex = 0; let photosData = []; let isOpen = false; let exifData = {}; let options = { swipeEnabled: true, keyboardEnabled: true, extractEXIF: true, onOpen: null, onClose: null, }; // Lightbox DOM elements (created on first init) let lightboxModal = null; let lightboxImage = null; let lightboxCaption = null; let lightboxSidebar = null; /** * Initialize lightbox for a photo collection * @param {string} containerSelector - CSS selector for photo container * @param {Array} photos - Array of photo objects: {src, alt, caption, location} * @param {Object} opts - Options: {swipeEnabled, keyboardEnabled, extractEXIF, onOpen, onClose} * @returns {Object} - {openLightbox, closeLightbox} */ function initLightbox(containerSelector, photos, opts = {}) { photosData = photos; options = { ...options, ...opts }; // Merge options with defaults const container = document.querySelector(containerSelector); if (!container) { console.error('Photo container not found:', containerSelector); return { openLightbox: () => {}, closeLightbox: () => {} }; } // Attach click listeners to all figures in the container const figures = container.querySelectorAll('figure[data-photo-index]'); figures.forEach((figure) => { figure.addEventListener('click', () => { const index = parseInt(figure.dataset.photoIndex, 10); openLightbox(index); }); }); // Return public API return { openLightbox, closeLightbox, }; } /** * Open lightbox to a specific photo * @param {number} photoIndex - 0-based index into photosData array */ function openLightbox(photoIndex) { if (photoIndex < 0 || photoIndex >= photosData.length) { console.warn('Invalid photo index:', photoIndex); return; } currentPhotoIndex = photoIndex; isOpen = true; // Create lightbox DOM if it doesn't exist if (!lightboxModal) { createLightboxDOM(); } // Populate lightbox with current photo updateLightboxContent(); // Show modal lightboxModal.classList.add('visible'); document.body.style.overflow = 'hidden'; // Extract EXIF if enabled if (options.extractEXIF) { extractEXIFForCurrentPhoto(); } // Attach event listeners attachEventListeners(); // Callback if (options.onOpen) { options.onOpen(photoIndex); } } /** * Close lightbox */ function closeLightbox() { if (!isOpen || !lightboxModal) return; isOpen = false; lightboxModal.classList.remove('visible'); document.body.style.overflow = ''; // Detach event listeners detachEventListeners(); // Callback if (options.onClose) { options.onClose(); } } /** * Create lightbox HTML structure and inject into DOM */ function createLightboxDOM() { lightboxModal = document.createElement('div'); lightboxModal.className = 'photo-lightbox'; lightboxModal.innerHTML = `
`; document.body.appendChild(lightboxModal); // Cache element references lightboxImage = lightboxModal.querySelector('.photo-lightbox-image'); lightboxCaption = lightboxModal.querySelector('.photo-lightbox-caption'); lightboxSidebar = lightboxModal.querySelector('.photo-lightbox-sidebar-content'); // Attach close button and backdrop click lightboxModal.querySelector('.photo-lightbox-close').addEventListener('click', closeLightbox); lightboxModal.querySelector('.photo-lightbox-backdrop').addEventListener('click', closeLightbox); // Attach navigation buttons lightboxModal.querySelector('.photo-lightbox-prev').addEventListener('click', () => navigate(-1)); lightboxModal.querySelector('.photo-lightbox-next').addEventListener('click', () => navigate(1)); } /** * Update lightbox content with current photo */ function updateLightboxContent() { const photo = photosData[currentPhotoIndex]; if (!photo) return; lightboxImage.src = photo.src; lightboxImage.alt = photo.alt || ''; // Caption if (photo.caption) { lightboxCaption.textContent = photo.caption; lightboxCaption.style.display = 'block'; } else { lightboxCaption.style.display = 'none'; } // Update prev/next button states const prevBtn = lightboxModal.querySelector('.photo-lightbox-prev'); const nextBtn = lightboxModal.querySelector('.photo-lightbox-next'); prevBtn.disabled = currentPhotoIndex === 0; nextBtn.disabled = currentPhotoIndex === photosData.length - 1; } /** * Navigate to previous or next photo * @param {number} direction - -1 for prev, 1 for next */ function navigate(direction) { const newIndex = currentPhotoIndex + direction; if (newIndex >= 0 && newIndex < photosData.length) { currentPhotoIndex = newIndex; updateLightboxContent(); // Reset EXIF data and re-extract if enabled exifData = {}; if (options.extractEXIF) { extractEXIFForCurrentPhoto(); } } } /** * Extract EXIF data from current photo image element */ function extractEXIFForCurrentPhoto() { extractEXIF(lightboxImage).then((data) => { exifData = data; updateSidebarContent(); }); } /** * Update sidebar with EXIF and location data */ function updateSidebarContent() { const photo = photosData[currentPhotoIndex]; let sidebarHTML = ''; // Always show location if provided if (photo.location) { sidebarHTML += `
Location
${photo.location}
`; } // Show EXIF fields if available if (Object.keys(exifData).length > 0) { sidebarHTML += `
Camera
`; if (exifData.camera) { sidebarHTML += `
${exifData.camera}
`; } if (exifData.lens) { sidebarHTML += `
Lens
${exifData.lens}
`; } if (exifData.focalLength) { sidebarHTML += `
Focal Length
${exifData.focalLength}
`; } if (exifData.aperture) { sidebarHTML += `
Aperture
${exifData.aperture}
`; } if (exifData.shutterSpeed) { sidebarHTML += `
Shutter Speed
${exifData.shutterSpeed}
`; } if (exifData.iso) { sidebarHTML += `
ISO
${exifData.iso}
`; } if (exifData.dateTaken) { sidebarHTML += `
Date Taken
${exifData.dateTaken}
`; } } // Show or hide sidebar based on whether there's content const sidebarContainer = lightboxModal.querySelector('.photo-lightbox-sidebar'); if (sidebarHTML) { lightboxSidebar.innerHTML = sidebarHTML; sidebarContainer.classList.add('visible'); } else { sidebarContainer.classList.remove('visible'); } } /** * Extract EXIF data from an image element using exif-js library * @param {HTMLImageElement} imageElement - Image to read EXIF from * @returns {Promise} - Resolves to {camera, lens, iso, aperture, shutterSpeed, focalLength, dateTaken} */ function extractEXIF(imageElement) { return new Promise((resolve) => { // If exif-js is not loaded, return empty object if (typeof EXIF === 'undefined') { console.warn('exif-js library not loaded'); resolve({}); return; } EXIF.getData(imageElement, function() { const data = { camera: EXIF.getTag(this, 'Model'), lens: EXIF.getTag(this, 'LensModel'), iso: EXIF.getTag(this, 'ISOSpeedRatings'), aperture: formatAperture(EXIF.getTag(this, 'FNumber')), shutterSpeed: formatShutterSpeed(EXIF.getTag(this, 'ExposureTime')), focalLength: formatFocalLength(EXIF.getTag(this, 'FocalLength')), dateTaken: EXIF.getTag(this, 'DateTime'), }; // Filter out undefined values Object.keys(data).forEach(key => { if (data[key] === undefined) { delete data[key]; } }); resolve(data); }); }); } /** * Format aperture value (f-number) */ function formatAperture(value) { if (!value) return null; if (typeof value === 'object' && value.numerator !== undefined) { return `f/${(value.numerator / value.denominator).toFixed(1)}`; } return `f/${value}`; } /** * Format shutter speed (exposure time) */ function formatShutterSpeed(value) { if (!value) return null; if (typeof value === 'object' && value.numerator !== undefined) { const speed = value.numerator / value.denominator; if (speed >= 1) { return `${speed.toFixed(1)}s`; } return `1/${Math.round(1 / speed)}`; } return value; } /** * Format focal length */ function formatFocalLength(value) { if (!value) return null; if (typeof value === 'object' && value.numerator !== undefined) { return `${(value.numerator / value.denominator).toFixed(0)}mm`; } return `${value}mm`; } /** * Attach event listeners (swipe, keyboard, etc.) */ function attachEventListeners() { if (options.swipeEnabled) { attachSwipeListeners(); } if (options.keyboardEnabled) { attachKeyboardListeners(); } } /** * Detach event listeners to prevent memory leaks */ function detachEventListeners() { if (options.swipeEnabled) { detachSwipeListeners(); } if (options.keyboardEnabled) { detachKeyboardListeners(); } } /** * Touch swipe event listeners */ let swipeStartX = 0; let swipeStartY = 0; function onTouchStart(e) { swipeStartX = e.touches[0].clientX; swipeStartY = e.touches[0].clientY; } function onTouchEnd(e) { const swipeEndX = e.changedTouches[0].clientX; const swipeEndY = e.changedTouches[0].clientY; const diffX = swipeEndX - swipeStartX; const diffY = swipeEndY - swipeStartY; // Only consider horizontal swipe if delta-x > delta-y if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) { if (diffX > 0) { // Swiped right: go to previous navigate(-1); } else { // Swiped left: go to next navigate(1); } } } function attachSwipeListeners() { lightboxModal.addEventListener('touchstart', onTouchStart, false); lightboxModal.addEventListener('touchend', onTouchEnd, false); } function detachSwipeListeners() { lightboxModal.removeEventListener('touchstart', onTouchStart, false); lightboxModal.removeEventListener('touchend', onTouchEnd, false); } /** * Keyboard event listeners */ function onKeyDown(e) { if (!isOpen) return; switch (e.key) { case 'ArrowLeft': navigate(-1); e.preventDefault(); break; case 'ArrowRight': navigate(1); e.preventDefault(); break; case 'Escape': closeLightbox(); e.preventDefault(); break; default: break; } } function attachKeyboardListeners() { document.addEventListener('keydown', onKeyDown); } function detachKeyboardListeners() { document.removeEventListener('keydown', onKeyDown); } // Expose public API window.PhotoUtils = { initLightbox, extractEXIF, openLightbox, closeLightbox, }; })();