/** * EasyLightbox - A simple, lightweight lightbox for images */ (function() { // Default options const defaultOptions = { selector: '.lightbox-img, #flowDiagram', // Images that should trigger lightbox captionAttribute: 'data-caption', // Attribute to retrieve caption from zoomable: true, // Whether to enable zoom controls maxZoom: 300, // Maximum zoom percentage minZoom: 100, // Minimum zoom percentage closeOnEsc: true, // Close on escape key closeOnOutsideClick: true // Close when clicking outside image }; // Create global object window.EasyLightbox = { options: { ...defaultOptions }, // Initialize with custom options init: function(customOptions = {}) { // Merge default options with custom options this.options = { ...defaultOptions, ...customOptions }; // Create lightbox container if it doesn't exist this._createLightbox(); // Initialize listeners for all matching elements this._initImageListeners(); return this; }, // Create the lightbox HTML structure if it doesn't exist _createLightbox: function() { // Check if lightbox already exists if (document.getElementById("imageLightbox")) { return; } // Create lightbox container const lightbox = document.createElement("div"); lightbox.id = "imageLightbox"; lightbox.className = "lightbox"; // Create lightbox content with simplified HTML lightbox.innerHTML = ` `; // Add lightbox CSS link if not already present if (!document.getElementById("lightbox-styles")) { const link = document.createElement("link"); link.id = "lightbox-styles"; link.rel = "stylesheet"; link.href = "/css/lightbox.css"; document.head.appendChild(link); } // Add to document document.body.appendChild(lightbox); // Cache DOM elements this.elements = { lightbox: lightbox, lightboxImg: document.getElementById("lightboxImg"), lightboxCaption: document.getElementById("lightboxCaption"), lightboxClose: document.getElementById("lightboxClose"), zoomSlider: document.getElementById("zoomSlider"), zoomValue: document.getElementById("zoomValue") }; // Initialize event handlers inside the lightbox this._initLightboxHandlers(); }, // Initialize listeners for images that should open the lightbox _initImageListeners: function() { const images = document.querySelectorAll(this.options.selector); const self = this; images.forEach(img => { // *** FIX: Skip the actual lightbox image itself *** if (img.id === "lightboxImg") return; // Skip if already initialized if (img.dataset.lightboxInitialized) return; img.dataset.lightboxInitialized = "true"; img.style.cursor = "pointer"; img.addEventListener("click", function() { let caption = this.getAttribute(self.options.captionAttribute); if (this.id === "flowDiagram" || !caption) { caption = "Basic POW Flow Diagram"; } self.open(this, caption); }); }); // Special handling for flowDiagram if not caught by selector const flowDiagram = document.getElementById("flowDiagram"); if (flowDiagram && !flowDiagram.dataset.lightboxInitialized) { flowDiagram.dataset.lightboxInitialized = "true"; flowDiagram.style.cursor = "pointer"; flowDiagram.addEventListener("click", function() { self.open(this, "Basic POW Flow Diagram"); }); } }, // Initialize lightbox event handlers for zooming, closing, etc. _initLightboxHandlers: function() { const self = this; const elements = this.elements; let isDragging = false; let startX, startY, startPanX, startPanY; let panX = 0, panY = 0; // Add zoom slider handler if (this.options.zoomable && elements.zoomSlider) { elements.zoomSlider.addEventListener("input", function() { const value = this.value; elements.zoomValue.textContent = value + "%"; updateTransform(); }); } // Add close button handler if (elements.lightboxClose) { elements.lightboxClose.addEventListener("click", function(e) { e.preventDefault(); e.stopPropagation(); self.close(); }); } // Add outside click handler if (this.options.closeOnOutsideClick) { elements.lightbox.addEventListener("click", function(e) { if (e.target === elements.lightbox) { self.close(); } }); } // Add escape key handler if (this.options.closeOnEsc) { document.addEventListener("keydown", function(e) { if (e.key === "Escape" && elements.lightbox.classList.contains("active")) { self.close(); } }); } // Add drag handlers for panning when zoomed if (elements.lightboxImg) { elements.lightboxImg.addEventListener("mousedown", startDrag); elements.lightboxImg.addEventListener("touchstart", startDrag); } function startDrag(e) { // Only allow dragging when zoomed in if (!self.options.zoomable || parseInt(elements.zoomSlider.value) <= 100) return; e.preventDefault(); if (e.type === "touchstart") { startX = e.touches[0].clientX; startY = e.touches[0].clientY; } else { startX = e.clientX; startY = e.clientY; } startPanX = panX; startPanY = panY; isDragging = true; elements.lightboxImg.classList.add("grabbing"); document.addEventListener("mousemove", doDrag); document.addEventListener("touchmove", doDrag); document.addEventListener("mouseup", stopDrag); document.addEventListener("touchend", stopDrag); document.addEventListener("mouseleave", stopDrag); } function doDrag(e) { if (!isDragging) return; // Prevent default scroll/zoom behavior on touch devices e.preventDefault(); let clientX, clientY; if (e.type === "touchmove") { // Ensure there's a touch point if (e.touches.length === 0) return; clientX = e.touches[0].clientX; clientY = e.touches[0].clientY; } else { clientX = e.clientX; clientY = e.clientY; } const deltaX = clientX - startX; const deltaY = clientY - startY; panX = startPanX + deltaX; panY = startPanY + deltaY; // Apply the transform immediately for live dragging updateTransform(); } function stopDrag() { if (!isDragging) return; isDragging = false; elements.lightboxImg.classList.remove("grabbing"); document.removeEventListener("mousemove", doDrag); document.removeEventListener("touchmove", doDrag); document.removeEventListener("mouseup", stopDrag); document.removeEventListener("touchend", stopDrag); document.removeEventListener("mouseleave", stopDrag); } function updateTransform() { if (!self.options.zoomable) return; const scale = parseInt(elements.zoomSlider.value) / 100; elements.lightboxImg.style.transform = `scale(${scale}) translate(${ panX / scale }px, ${panY / scale}px)`; } // Prevent scrolling on mobile when interacting with the lightbox const isMobile = window.matchMedia( "(max-width: 768px), (max-width: 1024px) and (orientation: landscape)" ).matches; if (isMobile && elements.lightboxImg) { elements.lightboxImg.addEventListener("touchmove", function(e) { if (e.touches.length > 1) { e.preventDefault(); } }); } }, // Open the lightbox with a specific image open: async function(imageElement, caption) { if (!imageElement || !this.elements) return; const elements = this.elements; let panX = 0, panY = 0; // Remove any previous SVG if (elements.lightboxImg && elements.lightboxImg.parentNode) { elements.lightboxImg.style.display = ''; const prevSvg = elements.lightboxImg.parentNode.querySelector('svg.injected-svg'); if (prevSvg) prevSvg.remove(); } const src = imageElement.src || imageElement.getAttribute("data-fullsize") || ""; const isSVG = src.toLowerCase().endsWith('.svg'); // Helper for zoom slider value function getZoom() { return elements.zoomSlider ? parseInt(elements.zoomSlider.value) / 100 : 1; } // Helper to update SVG transform function updateSVGTransform(svg, svgPanX, svgPanY, scale) { svg.style.transform = `scale(${scale}) translate(${svgPanX/scale}px, ${svgPanY/scale}px)`; } if (isSVG) { elements.lightboxImg.style.display = 'none'; try { const resp = await fetch(src); let svgText = await resp.text(); const tempDiv = document.createElement('div'); tempDiv.innerHTML = svgText; const svg = tempDiv.querySelector('svg'); if (svg) { svg.classList.add('injected-svg'); svg.style.transformOrigin = 'center center'; svg.style.maxWidth = '100%'; svg.style.maxHeight = '100%'; svg.style.display = 'block'; svg.style.cursor = 'grab'; svg.style.userSelect = 'none'; svg.removeAttribute('width'); svg.removeAttribute('height'); elements.lightboxImg.parentNode.appendChild(svg); // Set default zoom to 1.0x (100%) if (elements.zoomSlider) { elements.zoomSlider.value = 100; elements.zoomValue.textContent = '100%'; } let svgPanX = 0, svgPanY = 0; let isDragging = false, startX, startY, startPanX = 0, startPanY = 0; let currentScale = getZoom(); updateSVGTransform(svg, svgPanX, svgPanY, currentScale); // Drag logic for SVG svg.addEventListener('mousedown', startDrag); svg.addEventListener('touchstart', startDrag); function startDrag(e) { e.preventDefault(); isDragging = true; svg.classList.add('grabbing'); if (e.type === 'touchstart') { startX = e.touches[0].clientX; startY = e.touches[0].clientY; } else { startX = e.clientX; startY = e.clientY; } startPanX = svgPanX; startPanY = svgPanY; document.addEventListener('mousemove', doDrag); document.addEventListener('touchmove', doDrag); document.addEventListener('mouseup', stopDrag); document.addEventListener('touchend', stopDrag); document.addEventListener('mouseleave', stopDrag); } function doDrag(e) { if (!isDragging) return; e.preventDefault(); let clientX, clientY; if (e.type === 'touchmove') { if (e.touches.length === 0) return; clientX = e.touches[0].clientX; clientY = e.touches[0].clientY; } else { clientX = e.clientX; clientY = e.clientY; } const deltaX = clientX - startX; const deltaY = clientY - startY; svgPanX = startPanX + deltaX; svgPanY = startPanY + deltaY; updateSVGTransform(svg, svgPanX, svgPanY, getZoom()); } function stopDrag() { if (!isDragging) return; isDragging = false; svg.classList.remove('grabbing'); document.removeEventListener('mousemove', doDrag); document.removeEventListener('touchmove', doDrag); document.removeEventListener('mouseup', stopDrag); document.removeEventListener('touchend', stopDrag); document.removeEventListener('mouseleave', stopDrag); } // Zoom slider controls SVG scale if (elements.zoomSlider) { elements.zoomSlider.oninput = function() { currentScale = getZoom(); elements.zoomValue.textContent = Math.round(currentScale * 100) + '%'; updateSVGTransform(svg, svgPanX, svgPanY, currentScale); }; } } } catch (e) { elements.lightboxImg.style.display = ''; } } else { elements.lightboxImg.src = src; elements.lightboxImg.style.display = ''; elements.lightboxImg.style.transform = "scale(1) translate(0px, 0px)"; if (this.options.zoomable && elements.zoomSlider) { elements.zoomSlider.value = 100; elements.zoomValue.textContent = "100%"; } } const captionText = caption || imageElement.getAttribute(this.options.captionAttribute) || imageElement.alt || imageElement.getAttribute("title") || ""; elements.lightboxCaption.textContent = captionText; elements.lightbox.classList.add("active"); document.body.style.overflow = "hidden"; }, // Close the lightbox close: function() { if (!this.elements) return; this.elements.lightbox.classList.remove("active"); document.body.style.overflow = ""; } }; // Auto-initialize on load if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", function() { window.EasyLightbox.init(); }); } else { window.EasyLightbox.init(); } })();