Files
luxtools-plugin/js/lightbox.js
2026-01-15 20:24:16 +01:00

266 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* global window, document */
(function () {
'use strict';
var Luxtools = window.Luxtools || (window.Luxtools = {});
// ============================================================
// Lightbox Module
// ============================================================
Luxtools.Lightbox = (function () {
var lb = null;
var img = null;
var cap = null;
var items = [];
var index = 0;
// Zoom/pan state
var scale = 1;
var panX = 0;
var panY = 0;
var minScale = 1;
var maxScale = 5;
var isPanning = false;
var panStartX = 0;
var panStartY = 0;
// History state
var pushedHistory = false;
var closingFromPopstate = false;
function ensureElement() {
if (lb) return;
lb = document.createElement('div');
lb.className = 'luxtools-lightbox';
lb.setAttribute('role', 'dialog');
lb.setAttribute('aria-modal', 'true');
lb.setAttribute('aria-hidden', 'true');
lb.innerHTML =
'<div class="luxtools-lightbox-backdrop" data-luxtools-action="close"></div>' +
'<div class="luxtools-lightbox-stage">' +
'<button type="button" class="luxtools-lightbox-close" data-luxtools-action="close" aria-label="Close">×</button>' +
'<button type="button" class="luxtools-lightbox-zone luxtools-lightbox-zone-prev" data-luxtools-action="prev" aria-label="Previous"></button>' +
'<button type="button" class="luxtools-lightbox-zone luxtools-lightbox-zone-next" data-luxtools-action="next" aria-label="Next"></button>' +
'<div class="luxtools-lightbox-media">' +
'<img class="luxtools-lightbox-img" alt="" />' +
'</div>' +
'<div class="luxtools-lightbox-caption"></div>' +
'</div>';
document.body.appendChild(lb);
img = lb.querySelector('img.luxtools-lightbox-img');
cap = lb.querySelector('.luxtools-lightbox-caption');
lb.addEventListener('click', onClick);
}
function clampIndex(n) {
if (n < 0) return items.length - 1;
if (n >= items.length) return 0;
return n;
}
function applyTransform() {
if (scale <= 1 && panX === 0 && panY === 0) {
img.style.transform = '';
} else {
img.style.transform = 'scale(' + scale + ') translate(' + panX + 'px, ' + panY + 'px)';
}
img.style.cursor = scale > 1 ? 'grab' : '';
}
function resetZoom() {
scale = 1;
panX = 0;
panY = 0;
applyTransform();
}
function render() {
var it = items[index];
img.src = it.full;
img.setAttribute('data-luxtools-index', String(index));
if (cap) cap.textContent = (it.name || '').trim();
resetZoom();
}
function next() {
index = clampIndex(index + 1);
render();
}
function prev() {
index = clampIndex(index - 1);
render();
}
// Event handlers
function onWheel(e) {
e.preventDefault();
var delta = e.deltaY > 0 ? -0.15 : 0.15;
scale = Math.max(minScale, Math.min(maxScale, scale + delta));
if (scale <= 1) { panX = 0; panY = 0; }
applyTransform();
}
function onDblClick(e) {
e.preventDefault();
if (scale > 1) {
scale = 1;
panX = 0;
panY = 0;
} else {
scale = 2.5;
}
applyTransform();
}
function onMouseDown(e) {
if (scale > 1 && e.button === 0) {
isPanning = true;
panStartX = e.clientX - panX * scale;
panStartY = e.clientY - panY * scale;
img.style.cursor = 'grabbing';
e.preventDefault();
}
}
function onMouseMove(e) {
if (isPanning && scale > 1) {
panX = (e.clientX - panStartX) / scale;
panY = (e.clientY - panStartY) / scale;
applyTransform();
img.style.cursor = 'grabbing';
}
}
function onMouseUp() {
isPanning = false;
img.style.cursor = scale > 1 ? 'grab' : '';
}
function onKeyDown(e) {
if (!lb || !lb.classList.contains('is-open')) return;
var key = e.key || '';
if (key === 'Escape') {
e.preventDefault();
close();
} else if (key === 'ArrowRight') {
e.preventDefault();
next();
} else if (key === 'ArrowLeft') {
e.preventDefault();
prev();
}
}
function onPopState() {
if (!lb || !lb.classList.contains('is-open')) return;
closingFromPopstate = true;
try { close(); } finally { closingFromPopstate = false; }
}
function onClick(e) {
var t = e.target;
if (!t || !t.getAttribute) return;
var action = t.getAttribute('data-luxtools-action') || '';
if (action === 'close') { e.preventDefault(); close(); return; }
if (action === 'next') { e.preventDefault(); next(); return; }
if (action === 'prev') { e.preventDefault(); prev(); return; }
if (t.closest && t.closest('button.luxtools-lightbox-zone')) return;
if (t.closest && t.closest('img.luxtools-lightbox-img')) return;
e.preventDefault();
close();
}
function attachListeners() {
document.addEventListener('keydown', onKeyDown, true);
window.addEventListener('popstate', onPopState, true);
img.addEventListener('wheel', onWheel, { passive: false });
img.addEventListener('dblclick', onDblClick);
img.addEventListener('mousedown', onMouseDown);
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
}
function detachListeners() {
document.removeEventListener('keydown', onKeyDown, true);
window.removeEventListener('popstate', onPopState, true);
img.removeEventListener('wheel', onWheel);
img.removeEventListener('dblclick', onDblClick);
img.removeEventListener('mousedown', onMouseDown);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
}
function open(galleryEl, startEl) {
var links = galleryEl.querySelectorAll('a.luxtools-gallery-item[data-luxtools-full]');
items = [];
links.forEach(function (a) {
var full = a.getAttribute('data-luxtools-full') || a.getAttribute('href') || '';
var name = a.getAttribute('data-luxtools-name') || a.getAttribute('title') || '';
if (!full) return;
items.push({ el: a, full: full, name: name });
});
if (!items.length) return;
index = 0;
for (var i = 0; i < items.length; i++) {
if (items[i].el === startEl) { index = i; break; }
}
ensureElement();
pushedHistory = false;
closingFromPopstate = false;
lb.classList.add('is-open');
lb.setAttribute('aria-hidden', 'false');
try { document.documentElement.classList.add('luxtools-noscroll'); } catch (e) {}
try { document.body.style.overflow = 'hidden'; } catch (e) {}
attachListeners();
try {
if (window.history && window.history.pushState) {
window.history.pushState({ luxtoolsLightbox: 1 }, '', window.location.href);
pushedHistory = true;
}
} catch (e) {}
render();
}
function close() {
if (!lb) return;
lb.classList.remove('is-open');
lb.setAttribute('aria-hidden', 'true');
try { document.documentElement.classList.remove('luxtools-noscroll'); } catch (e) {}
try { document.body.style.overflow = ''; } catch (e) {}
img.src = '';
resetZoom();
detachListeners();
if (pushedHistory && !closingFromPopstate) {
try {
if (window.history && window.history.state && window.history.state.luxtoolsLightbox === 1) {
window.history.back();
}
} catch (e) {}
}
items = [];
}
return {
open: open,
close: close
};
})();
})();