205 lines
8.9 KiB
JavaScript
205 lines
8.9 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
const UI = (window.PQueue && window.PQueue.UI) || (window.PQueue = (window.PQueue || {}), window.PQueue.UI = {}, window.PQueue.UI);
|
|
|
|
UI.ensureGallery = function ensureGallery() {
|
|
if (UI.galleryOverlay) return;
|
|
const overlay = UI.el("div", { class: "pqueue-gallery-overlay" });
|
|
const box = UI.el("div", { class: "pqueue-gallery" });
|
|
const closeBtn = UI.button({ icon: "ti ti-x", variant: "ghost", subtle: true, title: "Close" });
|
|
closeBtn.addEventListener("click", UI.closeGallery);
|
|
const img = UI.el("img", { class: "pqueue-gallery__img" });
|
|
const counter = UI.el("div", { class: "pqueue-gallery__counter" });
|
|
const prev = UI.button({ icon: "ti ti-chevron-left", variant: "ghost", subtle: true, title: "Previous" });
|
|
prev.addEventListener("click", () => UI.galleryShow(UI.galleryState.index - 1));
|
|
const next = UI.button({ icon: "ti ti-chevron-right", variant: "ghost", subtle: true, title: "Next" });
|
|
next.addEventListener("click", () => UI.galleryShow(UI.galleryState.index + 1));
|
|
|
|
box.appendChild(closeBtn);
|
|
box.appendChild(img);
|
|
box.appendChild(counter);
|
|
box.appendChild(prev);
|
|
box.appendChild(next);
|
|
overlay.appendChild(box);
|
|
|
|
overlay.addEventListener("click", (e) => {
|
|
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;
|
|
UI.galleryCounter = counter;
|
|
UI.galleryState = { images: [], index: 0, pid: null };
|
|
};
|
|
|
|
UI.openGallery = function openGallery(images, startIndex = 0, pid = null) {
|
|
UI.ensureGallery();
|
|
UI.galleryState.images = images || [];
|
|
UI.galleryState.pid = pid;
|
|
UI.galleryState.index = Math.max(0, Math.min(startIndex, UI.galleryState.images.length - 1));
|
|
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) {
|
|
const images = UI.galleryState.images;
|
|
if (!images.length) return;
|
|
UI.galleryState.index = (index + images.length) % images.length;
|
|
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) {
|
|
const handler = (e) => {
|
|
if (!UI.galleryOverlay || UI.galleryOverlay.style.display !== "flex") return;
|
|
if (e.key === "Escape") UI.closeGallery();
|
|
else if (e.key === "ArrowRight") UI.galleryShow(UI.galleryState.index + 1);
|
|
else if (e.key === "ArrowLeft") UI.galleryShow(UI.galleryState.index - 1);
|
|
};
|
|
if (enabled) {
|
|
UI.galleryKeyHandler = handler;
|
|
document.addEventListener("keydown", handler);
|
|
} else if (UI.galleryKeyHandler) {
|
|
document.removeEventListener("keydown", UI.galleryKeyHandler);
|
|
UI.galleryKeyHandler = null;
|
|
}
|
|
};
|
|
})();
|
|
|
|
|
|
|
|
|