/** * Product Card Lightbox * Full-screen image viewer with zoom/pan support * Reuses patterns from product detail page */ (function() { 'use strict'; // State let currentZoom = 1; let maxZoom = 3; let minZoom = 1; let panX = 0, panY = 0; let isDragging = false; let startX = 0, startY = 0; let currentImageIndex = 0; let allImages = []; let initialPinchDistance = 0; let initialZoom = 1; let zoomLevelTimeout = null; // DOM elements let lightbox, lightboxImg, zoomLevel, thumbsContainer; let lightboxInitialized = false; // Lightbox HTML const lightboxHTML = `
`; /** * Initialize lightbox (create DOM once) */ function initLightbox() { if (lightboxInitialized) return; document.body.insertAdjacentHTML('beforeend', lightboxHTML); lightbox = document.getElementById('productLightbox'); lightboxImg = document.getElementById('lightboxImg'); zoomLevel = document.getElementById('lightboxZoomLevel'); thumbsContainer = document.getElementById('lightboxThumbs'); // Event listeners lightbox.querySelector('.lightbox-close').addEventListener('click', closeLightbox); lightbox.querySelector('.lightbox-prev').addEventListener('click', () => switchImage(currentImageIndex - 1)); lightbox.querySelector('.lightbox-next').addEventListener('click', () => switchImage(currentImageIndex + 1)); lightbox.querySelector('.lightbox-zoom-in').addEventListener('click', () => zoom(0.5)); lightbox.querySelector('.lightbox-zoom-out').addEventListener('click', () => zoom(-0.5)); lightbox.querySelector('.lightbox-reset').addEventListener('click', resetZoom); // Close on background click lightbox.addEventListener('click', (e) => { if (e.target === lightbox || e.target.classList.contains('lightbox-image-wrapper')) { closeLightbox(); } }); // Keyboard document.addEventListener('keydown', (e) => { if (!lightbox.classList.contains('active')) return; if (e.key === 'Escape') closeLightbox(); if (e.key === 'ArrowLeft') switchImage(currentImageIndex - 1); if (e.key === 'ArrowRight') switchImage(currentImageIndex + 1); }); // Mouse wheel zoom lightbox.addEventListener('wheel', (e) => { if (!lightbox.classList.contains('active')) return; e.preventDefault(); zoom(e.deltaY > 0 ? -0.3 : 0.3); }, { passive: false }); // Drag for panning lightboxImg.addEventListener('mousedown', startDrag); document.addEventListener('mousemove', doDrag); document.addEventListener('mouseup', endDrag); // Touch support lightbox.addEventListener('touchstart', handleTouchStart, { passive: true }); lightbox.addEventListener('touchmove', handleTouchMove, { passive: false }); lightbox.addEventListener('touchend', handleTouchEnd); // Double-tap to zoom let lastTap = 0; lightboxImg.addEventListener('touchend', (e) => { const now = Date.now(); if (now - lastTap < 300) { e.preventDefault(); if (currentZoom > 1) { resetZoom(); } else { zoom(1); } } lastTap = now; }); lightboxInitialized = true; } /** * Open lightbox with images */ function openLightbox(images, startIndex = 0) { initLightbox(); allImages = images; currentImageIndex = startIndex; // Reset zoom resetZoom(); // Load first image loadImage(currentImageIndex); // Build thumbnails buildThumbnails(); // Show/hide nav buttons updateNavButtons(); // Show lightbox lightbox.classList.add('active'); document.body.style.overflow = 'hidden'; // Show instructions briefly showInstructions(); } /** * Close lightbox */ function closeLightbox() { if (!lightbox) return; lightbox.classList.remove('active'); document.body.style.overflow = ''; resetZoom(); } /** * Load image at index */ function loadImage(index) { if (index < 0 || index >= allImages.length) return; currentImageIndex = index; lightboxImg.src = allImages[index].src; // Update thumbnails thumbsContainer.querySelectorAll('.lightbox-thumb').forEach((thumb, i) => { thumb.classList.toggle('active', i === index); }); updateNavButtons(); } /** * Switch to image (with circular wrap) */ function switchImage(index) { if (index < 0) index = allImages.length - 1; if (index >= allImages.length) index = 0; resetZoom(); loadImage(index); } /** * Build thumbnail strip */ function buildThumbnails() { thumbsContainer.innerHTML = ''; if (allImages.length <= 1) { thumbsContainer.style.display = 'none'; return; } thumbsContainer.style.display = 'flex'; allImages.forEach((img, i) => { const thumb = document.createElement('img'); thumb.className = 'lightbox-thumb' + (i === currentImageIndex ? ' active' : ''); thumb.src = img.src; thumb.alt = 'Thumbnail ' + (i + 1); thumb.addEventListener('click', (e) => { e.stopPropagation(); switchImage(i); }); thumbsContainer.appendChild(thumb); }); } /** * Update nav button visibility */ function updateNavButtons() { const hasMultiple = allImages.length > 1; lightbox.querySelector('.lightbox-prev').style.display = hasMultiple ? 'flex' : 'none'; lightbox.querySelector('.lightbox-next').style.display = hasMultiple ? 'flex' : 'none'; } /** * Zoom function */ function zoom(delta) { const oldZoom = currentZoom; currentZoom = Math.max(minZoom, Math.min(maxZoom, currentZoom + delta)); if (currentZoom !== oldZoom) { constrainPan(); applyTransform(); showZoomLevel(); updateZoomButtons(); } } /** * Reset zoom and pan */ function resetZoom() { currentZoom = 1; panX = 0; panY = 0; applyTransform(); updateZoomButtons(); } /** * Apply transform to image */ function applyTransform() { if (!lightboxImg) return; lightboxImg.style.transform = `scale(${currentZoom}) translate(${panX}px, ${panY}px)`; } /** * Constrain pan to keep image visible */ function constrainPan() { if (currentZoom <= 1) { panX = 0; panY = 0; return; } // Get viewport and image dimensions const wrapper = lightbox.querySelector('.lightbox-image-wrapper'); if (!wrapper) return; const wrapperRect = wrapper.getBoundingClientRect(); const imgRect = lightboxImg.getBoundingClientRect(); // Calculate max pan based on zoom level const scaledWidth = lightboxImg.naturalWidth * currentZoom; const scaledHeight = lightboxImg.naturalHeight * currentZoom; const maxPanX = Math.max(0, (scaledWidth - wrapperRect.width) / 2 / currentZoom); const maxPanY = Math.max(0, (scaledHeight - wrapperRect.height) / 2 / currentZoom); panX = Math.max(-maxPanX, Math.min(maxPanX, panX)); panY = Math.max(-maxPanY, Math.min(maxPanY, panY)); } /** * Show zoom level indicator */ function showZoomLevel() { zoomLevel.textContent = Math.round(currentZoom * 100) + '%'; zoomLevel.classList.add('visible'); clearTimeout(zoomLevelTimeout); zoomLevelTimeout = setTimeout(() => zoomLevel.classList.remove('visible'), 1500); } /** * Update zoom button states */ function updateZoomButtons() { if (!lightbox) return; const zoomIn = lightbox.querySelector('.lightbox-zoom-in'); const zoomOut = lightbox.querySelector('.lightbox-zoom-out'); if (zoomIn) zoomIn.disabled = currentZoom >= maxZoom; if (zoomOut) zoomOut.disabled = currentZoom <= minZoom; } /** * Start drag (for panning) */ function startDrag(e) { if (currentZoom <= 1) return; isDragging = true; startX = e.clientX - panX * currentZoom; startY = e.clientY - panY * currentZoom; lightboxImg.classList.add('dragging'); e.preventDefault(); } /** * Do drag (for panning) */ function doDrag(e) { if (!isDragging) return; e.preventDefault(); panX = (e.clientX - startX) / currentZoom; panY = (e.clientY - startY) / currentZoom; constrainPan(); applyTransform(); } /** * End drag */ function endDrag() { if (!isDragging) return; isDragging = false; if (lightboxImg) lightboxImg.classList.remove('dragging'); } /** * Handle touch start (for pinch-to-zoom) */ function handleTouchStart(e) { if (e.touches.length === 2) { // Pinch start initialPinchDistance = Math.hypot( e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY ); initialZoom = currentZoom; } else if (e.touches.length === 1 && currentZoom > 1) { // Pan start isDragging = true; startX = e.touches[0].clientX - panX * currentZoom; startY = e.touches[0].clientY - panY * currentZoom; } } /** * Handle touch move (for pinch-to-zoom and pan) */ function handleTouchMove(e) { if (e.touches.length === 2) { // Pinch zoom e.preventDefault(); const dist = Math.hypot( e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY ); currentZoom = Math.max(minZoom, Math.min(maxZoom, initialZoom * (dist / initialPinchDistance))); constrainPan(); applyTransform(); showZoomLevel(); updateZoomButtons(); } else if (e.touches.length === 1 && isDragging && currentZoom > 1) { // Pan e.preventDefault(); panX = (e.touches[0].clientX - startX) / currentZoom; panY = (e.touches[0].clientY - startY) / currentZoom; constrainPan(); applyTransform(); } } /** * Handle touch end */ function handleTouchEnd() { isDragging = false; initialPinchDistance = 0; } /** * Show instructions briefly */ function showInstructions() { const instr = lightbox.querySelector('.lightbox-instructions'); if (!instr) return; // Only show once per session if (sessionStorage.getItem('lightbox-instructions-shown')) return; sessionStorage.setItem('lightbox-instructions-shown', 'true'); instr.classList.add('visible'); setTimeout(() => instr.classList.remove('visible'), 4000); } // Expose globally window.ProductLightbox = { open: openLightbox, close: closeLightbox }; // Handle zoom button clicks on product cards document.addEventListener('click', (e) => { const btn = e.target.closest('.btn-zoom-image'); if (btn) { e.preventDefault(); e.stopPropagation(); const imagesData = btn.dataset.images; if (imagesData) { try { const images = JSON.parse(imagesData.replace(/"/g, '"')); if (images && images.length > 0) { openLightbox(images, 0); } } catch (err) { console.warn('Failed to parse images data for lightbox', err); } } } }); })();