(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 += `
`;
}
// Show EXIF fields if available
if (Object.keys(exifData).length > 0) {
sidebarHTML += `
Camera
`;
if (exifData.camera) {
sidebarHTML += `
${exifData.camera}
`;
}
if (exifData.lens) {
sidebarHTML += `
`;
}
if (exifData.focalLength) {
sidebarHTML += `
`;
}
if (exifData.aperture) {
sidebarHTML += `
`;
}
if (exifData.shutterSpeed) {
sidebarHTML += `
`;
}
if (exifData.iso) {
sidebarHTML += `
`;
}
if (exifData.dateTaken) {
sidebarHTML += `
`;
}
}
// 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,
};
})();