image zooming
This commit is contained in:
427
script.js
427
script.js
@@ -3,6 +3,267 @@
|
|||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'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 =
|
||||||
|
'<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
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Gallery Thumbnails
|
||||||
|
// ============================================================
|
||||||
function initGalleryThumbs() {
|
function initGalleryThumbs() {
|
||||||
var imgs = document.querySelectorAll('div.luxtools-gallery img[data-thumb-src]');
|
var imgs = document.querySelectorAll('div.luxtools-gallery img[data-thumb-src]');
|
||||||
if (!imgs || !imgs.length) return;
|
if (!imgs || !imgs.length) return;
|
||||||
@@ -43,161 +304,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureLightbox() {
|
// ============================================================
|
||||||
var existing = document.querySelector('.luxtools-lightbox');
|
// Open Service (file:// links)
|
||||||
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 =
|
|
||||||
'<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(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();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getServiceUrl(el) {
|
function getServiceUrl(el) {
|
||||||
var url = el.getAttribute('data-service-url') || '';
|
var url = el.getAttribute('data-service-url') || '';
|
||||||
url = (url || '').trim();
|
url = (url || '').trim();
|
||||||
@@ -444,6 +553,9 @@
|
|||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Click Handlers
|
||||||
|
// ============================================================
|
||||||
function findOpenElement(target) {
|
function findOpenElement(target) {
|
||||||
var el = target;
|
var el = target;
|
||||||
while (el && el !== document) {
|
while (el && el !== document) {
|
||||||
@@ -469,7 +581,7 @@
|
|||||||
var gallery = galleryItem.closest ? galleryItem.closest('div.luxtools-gallery[data-luxtools-gallery="1"]') : null;
|
var gallery = galleryItem.closest ? galleryItem.closest('div.luxtools-gallery[data-luxtools-gallery="1"]') : null;
|
||||||
if (gallery) {
|
if (gallery) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
openLightboxFor(gallery, galleryItem);
|
Lightbox.open(gallery, galleryItem);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -508,6 +620,9 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Initialize
|
||||||
|
// ============================================================
|
||||||
document.addEventListener('click', onClick, false);
|
document.addEventListener('click', onClick, false);
|
||||||
document.addEventListener('DOMContentLoaded', initGalleryThumbs, false);
|
document.addEventListener('DOMContentLoaded', initGalleryThumbs, false);
|
||||||
document.addEventListener('DOMContentLoaded', initScratchpads, false);
|
document.addEventListener('DOMContentLoaded', initScratchpads, false);
|
||||||
|
|||||||
@@ -291,6 +291,8 @@ html.luxtools-noscroll body {
|
|||||||
*/
|
*/
|
||||||
max-width: calc(100vw - 1.6em);
|
max-width: calc(100vw - 1.6em);
|
||||||
max-height: calc(100vh - 1.6em);
|
max-height: calc(100vh - 1.6em);
|
||||||
|
transform-origin: center center;
|
||||||
|
transition: transform 0.1s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.luxtools-lightbox .luxtools-lightbox-caption {
|
.luxtools-lightbox .luxtools-lightbox-caption {
|
||||||
|
|||||||
Reference in New Issue
Block a user