427 lines
No EOL
15 KiB
JavaScript
427 lines
No EOL
15 KiB
JavaScript
/**
|
|
* 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 = `
|
|
<div class="lightbox-content">
|
|
<div class="lightbox-close" id="lightboxClose">X</div>
|
|
<div class="lightbox-img-container">
|
|
<img class="lightbox-img" id="lightboxImg" src="" alt="Enlarged image" draggable="false">
|
|
</div>
|
|
<div class="lightbox-caption" id="lightboxCaption"></div>
|
|
${
|
|
this.options.zoomable
|
|
? `
|
|
<div class="zoom-controls">
|
|
<span class="zoom-label">Zoom:</span>
|
|
<input type="range" min="${this.options.minZoom}" max="${this.options.maxZoom}" value="100" class="zoom-slider" id="zoomSlider">
|
|
<span class="zoom-value" id="zoomValue">100%</span>
|
|
</div>`
|
|
: ""
|
|
}
|
|
</div>
|
|
`;
|
|
|
|
// 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();
|
|
}
|
|
})(); |