UI: Enhance image gallery with zoom, pan, accessibility, and focus management
This commit is contained in:
@@ -27,6 +27,114 @@
|
||||
if (e.target === overlay) UI.closeGallery();
|
||||
});
|
||||
|
||||
// Accessibility and focus management
|
||||
try {
|
||||
overlay.setAttribute("role", "dialog");
|
||||
overlay.setAttribute("aria-modal", "true");
|
||||
overlay.tabIndex = -1;
|
||||
} catch (err) { /* noop */ }
|
||||
|
||||
// Zoom/Pan state and helpers
|
||||
UI.galleryZoom = { scale: 1, x: 0, y: 0, dragging: false, lastX: 0, lastY: 0 };
|
||||
UI.updateGalleryTransform = function updateGalleryTransform() {
|
||||
try {
|
||||
const z = UI.galleryZoom || { scale: 1, x: 0, y: 0, dragging: false };
|
||||
img.style.transform = `translate(${z.x}px, ${z.y}px) scale(${z.scale})`;
|
||||
img.style.cursor = z.scale > 1 ? (z.dragging ? "grabbing" : "grab") : "default";
|
||||
img.style.userSelect = "none";
|
||||
img.style.willChange = "transform";
|
||||
img.draggable = false;
|
||||
} catch (err) { /* noop */ }
|
||||
};
|
||||
UI.resetGalleryTransform = function resetGalleryTransform() {
|
||||
UI.galleryZoom = { scale: 1, x: 0, y: 0, dragging: false, lastX: 0, lastY: 0 };
|
||||
UI.updateGalleryTransform();
|
||||
};
|
||||
|
||||
// Double-click to toggle zoom (2x default)
|
||||
img.addEventListener("dblclick", (e) => {
|
||||
try {
|
||||
e.preventDefault();
|
||||
const z = UI.galleryZoom || (UI.galleryZoom = { scale: 1, x: 0, y: 0, dragging: false, lastX: 0, lastY: 0 });
|
||||
if (z.scale === 1) {
|
||||
z.scale = 2;
|
||||
} else {
|
||||
z.scale = 1; z.x = 0; z.y = 0;
|
||||
}
|
||||
UI.updateGalleryTransform();
|
||||
} catch (err) { /* noop */ }
|
||||
});
|
||||
|
||||
// Wheel for fine zoom (1x - 5x)
|
||||
const onWheel = (e) => {
|
||||
try {
|
||||
e.preventDefault();
|
||||
const z = UI.galleryZoom || (UI.galleryZoom = { scale: 1, x: 0, y: 0, dragging: false, lastX: 0, lastY: 0 });
|
||||
const step = e.deltaY > 0 ? -0.2 : 0.2;
|
||||
const next = Math.max(1, Math.min(5, Number(z.scale || 1) + step));
|
||||
z.scale = Number(next.toFixed(2));
|
||||
if (z.scale === 1) { z.x = 0; z.y = 0; }
|
||||
UI.updateGalleryTransform();
|
||||
} catch (err) { /* noop */ }
|
||||
};
|
||||
img.addEventListener("wheel", onWheel, { passive: false });
|
||||
|
||||
// Drag to pan when zoomed
|
||||
img.addEventListener("mousedown", (e) => {
|
||||
try {
|
||||
if (!UI.galleryZoom || UI.galleryZoom.scale <= 1) return;
|
||||
e.preventDefault();
|
||||
UI.galleryZoom.dragging = true;
|
||||
UI.galleryZoom.lastX = e.clientX;
|
||||
UI.galleryZoom.lastY = e.clientY;
|
||||
UI.updateGalleryTransform();
|
||||
} catch (err) { /* noop */ }
|
||||
});
|
||||
const onMove = (e) => {
|
||||
try {
|
||||
const z = UI.galleryZoom;
|
||||
if (!z || !z.dragging) return;
|
||||
const dx = e.clientX - z.lastX;
|
||||
const dy = e.clientY - z.lastY;
|
||||
z.x += dx; z.y += dy; z.lastX = e.clientX; z.lastY = e.clientY;
|
||||
UI.updateGalleryTransform();
|
||||
} catch (err) { /* noop */ }
|
||||
};
|
||||
const onUp = () => {
|
||||
try {
|
||||
if (!UI.galleryZoom) return;
|
||||
UI.galleryZoom.dragging = false;
|
||||
UI.updateGalleryTransform();
|
||||
} catch (err) { /* noop */ }
|
||||
};
|
||||
window.addEventListener("mousemove", onMove);
|
||||
window.addEventListener("mouseup", onUp);
|
||||
|
||||
// Focus trap within gallery controls
|
||||
UI.bindGalleryFocusTrap = function bindGalleryFocusTrap(enabled) {
|
||||
const trapHandler = (e) => {
|
||||
try {
|
||||
if (!UI.galleryOverlay || UI.galleryOverlay.style.display !== "flex") return;
|
||||
if (e.key !== "Tab") return;
|
||||
const focusables = [closeBtn, prev, next].filter(Boolean);
|
||||
if (!focusables.length) return;
|
||||
const current = document.activeElement;
|
||||
const idx = Math.max(0, focusables.indexOf(current));
|
||||
e.preventDefault();
|
||||
const dir = e.shiftKey ? -1 : 1;
|
||||
const nextIdx = (idx + dir + focusables.length) % focusables.length;
|
||||
focusables[nextIdx].focus();
|
||||
} catch (err) { /* noop */ }
|
||||
};
|
||||
if (enabled) {
|
||||
UI.galleryFocusTrapHandler = trapHandler;
|
||||
overlay.addEventListener("keydown", trapHandler);
|
||||
} else if (UI.galleryFocusTrapHandler) {
|
||||
overlay.removeEventListener("keydown", UI.galleryFocusTrapHandler);
|
||||
UI.galleryFocusTrapHandler = null;
|
||||
}
|
||||
};
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
UI.galleryOverlay = overlay;
|
||||
UI.galleryImg = img;
|
||||
@@ -42,11 +150,26 @@
|
||||
UI.galleryOverlay.style.display = "flex";
|
||||
UI.galleryShow(UI.galleryState.index);
|
||||
UI.bindGalleryKeys(true);
|
||||
UI.bindGalleryFocusTrap(true);
|
||||
try {
|
||||
UI._galleryPrevFocus = document.activeElement || null;
|
||||
// Focus the close button initially
|
||||
const btn = UI.galleryOverlay.querySelector(".pqueue-button");
|
||||
if (btn && typeof btn.focus === "function") btn.focus();
|
||||
else UI.galleryOverlay.focus();
|
||||
} catch (err) { /* noop */ }
|
||||
};
|
||||
|
||||
UI.closeGallery = function closeGallery() {
|
||||
if (UI.galleryOverlay) UI.galleryOverlay.style.display = "none";
|
||||
UI.bindGalleryKeys(false);
|
||||
UI.bindGalleryFocusTrap(false);
|
||||
try { UI.resetGalleryTransform && UI.resetGalleryTransform(); } catch (err) { /* noop */ }
|
||||
try {
|
||||
if (UI._galleryPrevFocus && document.body.contains(UI._galleryPrevFocus)) {
|
||||
UI._galleryPrevFocus.focus();
|
||||
}
|
||||
} catch (err) { /* noop */ }
|
||||
};
|
||||
|
||||
UI.galleryShow = function galleryShow(index) {
|
||||
@@ -56,6 +179,7 @@
|
||||
const desc = images[UI.galleryState.index];
|
||||
UI.galleryImg.src = UI.buildPreviewUrl(desc, UI.galleryState.pid).href;
|
||||
UI.galleryCounter.textContent = `${UI.galleryState.index + 1} / ${images.length}`;
|
||||
try { UI.resetGalleryTransform && UI.resetGalleryTransform(); } catch (err) { /* noop */ }
|
||||
};
|
||||
|
||||
UI.bindGalleryKeys = function bindGalleryKeys(enabled) {
|
||||
|
||||
Reference in New Issue
Block a user