diff --git a/script.js b/script.js
index 4f12933..660120d 100644
--- a/script.js
+++ b/script.js
@@ -3,6 +3,267 @@
(function () {
'use strict';
+ // ============================================================
+ // Lightbox Module
+ // ============================================================
+ var 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 =
+ '
' +
+ '';
+
+ 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
+ };
+ })();
+
+ // ============================================================
+ // Gallery Thumbnails
+ // ============================================================
function initGalleryThumbs() {
var imgs = document.querySelectorAll('div.luxtools-gallery img[data-thumb-src]');
if (!imgs || !imgs.length) return;
@@ -43,161 +304,9 @@
}
}
- function ensureLightbox() {
- var existing = document.querySelector('.luxtools-lightbox');
- if (existing) return existing;
-
- var root = document.createElement('div');
- root.className = 'luxtools-lightbox';
- root.setAttribute('role', 'dialog');
- root.setAttribute('aria-modal', 'true');
- root.setAttribute('aria-hidden', 'true');
-
- root.innerHTML =
- '' +
- '';
-
- document.body.appendChild(root);
- return root;
- }
-
- function getGalleryItems(galleryEl) {
- var links = galleryEl.querySelectorAll('a.luxtools-gallery-item[data-luxtools-full]');
- var 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 });
- });
- return items;
- }
-
- function openLightboxFor(galleryEl, startEl) {
- var items = getGalleryItems(galleryEl);
- if (!items.length) return;
-
- var index = 0;
- for (var i = 0; i < items.length; i++) {
- if (items[i].el === startEl) { index = i; break; }
- }
-
- var lb = ensureLightbox();
- var img = lb.querySelector('img.luxtools-lightbox-img');
- var cap = lb.querySelector('.luxtools-lightbox-caption');
-
- var pushedHistory = false;
- var closingFromPopstate = false;
-
- function clamp(n) {
- if (n < 0) return items.length - 1;
- if (n >= items.length) return 0;
- return n;
- }
-
- function render() {
- var it = items[index];
- img.src = it.full;
- img.setAttribute('data-luxtools-index', String(index));
- if (cap) cap.textContent = (it.name || '').trim();
- }
-
- function close() {
- 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) {}
- // Clear src to stop downloads on close.
- img.src = '';
- document.removeEventListener('keydown', onKeyDown, true);
- window.removeEventListener('popstate', onPopState, true);
-
- // If we opened by pushing a state, pop it when closing via UI.
- if (pushedHistory && !closingFromPopstate) {
- try {
- if (window.history && window.history.state && window.history.state.luxtoolsLightbox === 1) {
- window.history.back();
- }
- } catch (e) {
- // ignore
- }
- }
- }
-
- function next() {
- index = clamp(index + 1);
- render();
- }
-
- function prev() {
- index = clamp(index - 1);
- render();
- }
-
- function onKeyDown(e) {
- if (!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.classList.contains('is-open')) return;
- closingFromPopstate = true;
- try { close(); } finally { closingFromPopstate = false; }
- }
-
- lb.onclick = function (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; }
-
- // Click outside the image closes (but don't interfere with controls).
- if (t.closest && t.closest('button.luxtools-lightbox-zone')) return;
- if (t.closest && t.closest('img.luxtools-lightbox-img')) return;
- e.preventDefault();
- close();
- };
-
- // Open
- 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) {}
- document.addEventListener('keydown', onKeyDown, true);
- window.addEventListener('popstate', onPopState, true);
-
- // Allow closing via browser back button.
- try {
- if (window.history && window.history.pushState) {
- window.history.pushState({ luxtoolsLightbox: 1 }, '', window.location.href);
- pushedHistory = true;
- }
- } catch (e) {
- // ignore
- }
- render();
- }
-
+ // ============================================================
+ // Open Service (file:// links)
+ // ============================================================
function getServiceUrl(el) {
var url = el.getAttribute('data-service-url') || '';
url = (url || '').trim();
@@ -444,6 +553,9 @@
}, true);
}
+ // ============================================================
+ // Click Handlers
+ // ============================================================
function findOpenElement(target) {
var el = target;
while (el && el !== document) {
@@ -469,7 +581,7 @@
var gallery = galleryItem.closest ? galleryItem.closest('div.luxtools-gallery[data-luxtools-gallery="1"]') : null;
if (gallery) {
event.preventDefault();
- openLightboxFor(gallery, galleryItem);
+ Lightbox.open(gallery, galleryItem);
return;
}
}
@@ -508,6 +620,9 @@
});
}
+ // ============================================================
+ // Initialize
+ // ============================================================
document.addEventListener('click', onClick, false);
document.addEventListener('DOMContentLoaded', initGalleryThumbs, false);
document.addEventListener('DOMContentLoaded', initScratchpads, false);
diff --git a/style.css b/style.css
index 88592cd..e3f8d7b 100644
--- a/style.css
+++ b/style.css
@@ -291,6 +291,8 @@ html.luxtools-noscroll body {
*/
max-width: calc(100vw - 1.6em);
max-height: calc(100vh - 1.6em);
+ transform-origin: center center;
+ transition: transform 0.1s ease-out;
}
.luxtools-lightbox .luxtools-lightbox-caption {