Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3e8258286 |
@@ -0,0 +1,209 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const vm = require('node:vm');
|
||||
const path = require('node:path');
|
||||
const { escapeHtml } = require('../../web/js/html-escape.js');
|
||||
|
||||
function fixture(count = 2000) {
|
||||
const elements = new Map();
|
||||
function element() {
|
||||
return {
|
||||
dataset: {}, children: [], listeners: {}, classList: { add() {}, remove() {} },
|
||||
set innerHTML(html) { this.html = html; this.children = [{ html }]; },
|
||||
get innerHTML() { return this.html || ''; },
|
||||
insertAdjacentHTML(position, html) { this.children.push({ html }); },
|
||||
addEventListener(type, handler) { (this.listeners[type] ||= []).push(handler); },
|
||||
querySelectorAll() { return []; },
|
||||
remove() {},
|
||||
};
|
||||
}
|
||||
const document = {
|
||||
addEventListener() {},
|
||||
getElementById(id) { if (!elements.has(id)) elements.set(id, element()); return elements.get(id); },
|
||||
createElement: element,
|
||||
};
|
||||
const instances = [];
|
||||
const timers = new Map();
|
||||
let timerId = 0;
|
||||
class Viewer {
|
||||
constructor(holder, options) {
|
||||
this.element = holder; this.options = { fullscreen: true, ...options };
|
||||
this.index = options.initialViewIndex; this.isShown = true;
|
||||
this.length = (holder.innerHTML.match(/<img /g) || []).length;
|
||||
instances.push(this);
|
||||
}
|
||||
requestFullscreen() { this.fullscreenRequested = true; }
|
||||
exitFullscreen() { this.fullscreenExited = true; }
|
||||
show() { this.shown = true; }
|
||||
view(index) { this.index = index; }
|
||||
clearEnforceFocus() { this.focusCleared = true; }
|
||||
destroy() { this.destroyed = true; }
|
||||
}
|
||||
const context = vm.createContext({ document, window: {}, Viewer, escapeHtml,
|
||||
console, setTimeout(callback) { timers.set(++timerId, callback); return timerId; },
|
||||
clearTimeout(id) { timers.delete(id); },
|
||||
ApiClient: { resolveUrl: value => value, STALE: Symbol() },
|
||||
ListState: { nextPageState: (state, next) => ({ ...state, ...next }) },
|
||||
});
|
||||
const source = fs.readFileSync(path.join(__dirname, '../../web/js/gallery.js'), 'utf8');
|
||||
const Manager = vm.runInContext(source + '\nGalleryManager;', context);
|
||||
const manager = Object.create(Manager.prototype);
|
||||
Object.assign(manager, { images: Array.from({ length: count }, (_, i) => ({
|
||||
filename: `${i}.png`, url: `/original/${i}.png`, thumbnail_url: `/thumb/${i}.jpg`,
|
||||
relative_path: `${i}.png`, size: 100, modified_time: 1,
|
||||
})), viewMode: 'grid', viewer: null, currentPage: 1, limit: 100, total: count,
|
||||
getSettings: () => ({ showImageInfo: true }), });
|
||||
return { manager, document, instances, timers };
|
||||
}
|
||||
|
||||
for (const mode of ['grid', 'list']) {
|
||||
test(`${mode} append retains existing cards and adds only the new page`, () => {
|
||||
const { manager, document, timers } = fixture(100);
|
||||
manager.viewMode = mode;
|
||||
manager.renderGallery();
|
||||
const container = document.getElementById(mode === 'grid' ? 'galleryGrid' : 'galleryList');
|
||||
assert.equal(timers.size, 0);
|
||||
const first = container.children[0];
|
||||
manager.images.push({ ...manager.images[0], filename: 'new.png' });
|
||||
manager.renderGallery({ from: 100 });
|
||||
assert.equal(container.children[0], first);
|
||||
assert.equal(container.children.length, 2);
|
||||
assert.match(container.children[1].html, /new.png/);
|
||||
assert.doesNotMatch(container.children[1].html, /alt="0.png"/);
|
||||
assert.match(container.children[1].html, /data-image-index="100"/);
|
||||
});
|
||||
}
|
||||
|
||||
test('loadImages passes the previous length for append, and zero for replacement', async () => {
|
||||
const { manager } = fixture(200);
|
||||
const offsets = [];
|
||||
Object.assign(manager, { imagesRequest: async () => ({ total: 500, images: [manager.images[0]] }),
|
||||
renderGallery: options => offsets.push(options.from), updateStats() {},
|
||||
updatePagination() {}, maybeLoadMore() {}, showLoading() {}, });
|
||||
await manager.loadImages(3, { append: true });
|
||||
await manager.loadImages(1);
|
||||
assert.deepEqual(offsets, [200, 0]);
|
||||
assert.equal(manager.images.length, 1);
|
||||
});
|
||||
|
||||
test('viewer opening is bounded and selects the clicked original', () => {
|
||||
const { manager, instances } = fixture();
|
||||
manager.openViewer(1500);
|
||||
const viewer = instances[0];
|
||||
assert.ok(viewer.element.innerHTML.match(/<img /g).length <= 100);
|
||||
assert.match(viewer.element.innerHTML, /data-original="\/original\/1500.png"/);
|
||||
const urls = [...viewer.element.innerHTML.matchAll(/data-original="([^"]+)"/g)].map(m => m[1]);
|
||||
assert.equal(urls[viewer.options.initialViewIndex], '/original/1500.png');
|
||||
assert.equal(viewer.shown, true);
|
||||
});
|
||||
|
||||
test('previous/next cross window edges and wrap only at gallery edges', () => {
|
||||
const { manager, instances } = fixture();
|
||||
manager.openViewer(1500);
|
||||
const first = manager.viewer;
|
||||
first.index = 99;
|
||||
const lastUrl = [...first.element.innerHTML.matchAll(/data-original="\/original\/(\d+).png"/g)].at(-1);
|
||||
first.next(true);
|
||||
const next = manager.viewer;
|
||||
const urls = [...next.element.innerHTML.matchAll(/data-original="\/original\/(\d+).png"/g)];
|
||||
assert.equal(Number(urls[next.index][1]), Number(lastUrl[1]) + 1);
|
||||
manager.openViewer(0);
|
||||
manager.viewer.prev(true);
|
||||
const wrapped = manager.viewer;
|
||||
const wrappedUrls = [...wrapped.element.innerHTML.matchAll(/data-original="\/original\/(\d+).png"/g)];
|
||||
assert.equal(Number(wrappedUrls[wrapped.index][1]), 1999);
|
||||
assert.ok(instances.length >= 3);
|
||||
});
|
||||
|
||||
test('delegated card clicks bind once, open appended indices, and preserve video dispatch', () => {
|
||||
const { manager, document } = fixture();
|
||||
const opened = [];
|
||||
manager.openViewer = index => opened.push(index);
|
||||
manager.showVideoModal = (...args) => opened.push(args);
|
||||
manager.addFallbackClickHandlers();
|
||||
manager.addFallbackClickHandlers();
|
||||
const grid = document.getElementById('galleryGrid');
|
||||
assert.equal(grid.listeners.click.length, 1);
|
||||
const item = { dataset: { imageIndex: '1500' }, querySelector: () => ({ dataset: {
|
||||
original: '/original/1500.png', caption: 'caption', isVideo: 'false',
|
||||
} }) };
|
||||
const click = () => grid.listeners.click[0]({ target: { closest: () => item }, preventDefault() {}, stopPropagation() {} });
|
||||
click();
|
||||
item.querySelector = () => ({ dataset: { original: '/video.mp4', caption: 'video', isVideo: 'true' } });
|
||||
click();
|
||||
assert.deepEqual(opened, [1500, ['/video.mp4', 'video', 1500]]);
|
||||
});
|
||||
|
||||
test('navigation inside the window reuses the viewer and honors non-looping edges', () => {
|
||||
const { manager, instances } = fixture();
|
||||
manager.openViewer(1500);
|
||||
const viewer = manager.viewer;
|
||||
viewer.next(true);
|
||||
assert.equal(manager.viewer, viewer);
|
||||
assert.equal(viewer.index, 51);
|
||||
viewer.prev(true);
|
||||
assert.equal(viewer.index, 50);
|
||||
manager.openViewer(0);
|
||||
const edge = manager.viewer;
|
||||
edge.prev(false);
|
||||
assert.equal(manager.viewer, edge);
|
||||
assert.equal(edge.index, 0);
|
||||
assert.equal(instances.length, 2);
|
||||
});
|
||||
|
||||
test('append leaves an open viewer intact; a full render destroys it', () => {
|
||||
const { manager } = fixture();
|
||||
manager.openViewer(1500);
|
||||
const viewer = manager.viewer;
|
||||
manager.renderGallery({ from: 1900 });
|
||||
assert.equal(manager.viewer, viewer);
|
||||
manager.renderGallery();
|
||||
assert.equal(viewer.destroyed, true);
|
||||
assert.equal(manager.viewer, null);
|
||||
});
|
||||
|
||||
test('each viewed image updates sidebar metadata without waiting for DOM mutations', () => {
|
||||
const { manager, document } = fixture();
|
||||
const urls = [];
|
||||
document.querySelector = () => ({});
|
||||
const sidebar = document.getElementById('metadata-sidebar');
|
||||
sidebar.querySelector = () => ({});
|
||||
manager.loadImageMetadata = url => urls.push(url);
|
||||
manager.openViewer(1500);
|
||||
manager.viewer.options.viewed({ detail: { originalImage: { dataset: { original: '/original/1500.png' } } } });
|
||||
manager.viewer.options.viewed({ detail: { originalImage: { dataset: { original: '/original/1501.png' } } } });
|
||||
assert.deepEqual(urls, ['/original/1500.png', '/original/1501.png']);
|
||||
});
|
||||
|
||||
test('slideshow advances across windows without preloading every original and stops on hide', () => {
|
||||
const { manager, timers } = fixture();
|
||||
manager.openViewer(1500);
|
||||
let prevented = false;
|
||||
manager.viewer.options.play({ preventDefault() { prevented = true; } });
|
||||
assert.equal(prevented, true);
|
||||
assert.equal(manager.viewer.fullscreenRequested, true);
|
||||
assert.equal(timers.size, 1);
|
||||
manager.viewer.index = 99;
|
||||
const tick = timers.values().next().value;
|
||||
timers.clear();
|
||||
tick();
|
||||
const viewer = manager.viewer;
|
||||
const urls = [...viewer.element.innerHTML.matchAll(/data-original="([^"]+)"/g)];
|
||||
assert.equal(urls[viewer.index][1], '/original/1550.png');
|
||||
assert.equal(timers.size, 1);
|
||||
viewer.options.hide();
|
||||
assert.equal(viewer.fullscreenExited, true);
|
||||
assert.equal(timers.size, 0);
|
||||
});
|
||||
|
||||
test('destroying a viewer removes its focus trap and slideshow timer', () => {
|
||||
const { manager, timers } = fixture();
|
||||
manager.openViewer(1500);
|
||||
const viewer = manager.viewer;
|
||||
viewer.options.play({ preventDefault() {} });
|
||||
manager.destroyViewer();
|
||||
assert.equal(viewer.focusCleared, true);
|
||||
assert.equal(viewer.destroyed, true);
|
||||
assert.equal(timers.size, 0);
|
||||
});
|
||||
+110
-99
@@ -357,9 +357,10 @@
|
||||
url: ApiClient.resolveUrl(image.url),
|
||||
thumbnail_url: ApiClient.resolveUrl(image.thumbnail_url),
|
||||
}));
|
||||
const from = append ? this.images.length : 0;
|
||||
this.images = append ? this.images.concat(images) : images;
|
||||
this.updateStats();
|
||||
this.renderGallery();
|
||||
this.renderGallery({ from });
|
||||
this.updatePagination();
|
||||
document.getElementById('loadingStatus').textContent = 'Loaded';
|
||||
this.maybeLoadMore(); // a short page may not fill the viewport
|
||||
@@ -445,7 +446,8 @@
|
||||
this.renderGallery();
|
||||
}
|
||||
|
||||
renderGallery() {
|
||||
renderGallery({ from = 0 } = {}) {
|
||||
if (from === 0) this.destroyViewer();
|
||||
if (this.images.length === 0) {
|
||||
this.showError('No images found in the output folder');
|
||||
return;
|
||||
@@ -455,19 +457,15 @@
|
||||
document.getElementById('errorState').classList.add('hidden');
|
||||
|
||||
if (this.viewMode === 'grid') {
|
||||
this.renderGridView();
|
||||
this.renderGridView({ from });
|
||||
} else {
|
||||
this.renderListView();
|
||||
this.renderListView({ from });
|
||||
}
|
||||
|
||||
// Initialize ViewerJS after rendering
|
||||
setTimeout(() => this.initializeViewer(), 100);
|
||||
|
||||
// Add simple fallback click handlers
|
||||
this.addFallbackClickHandlers();
|
||||
}
|
||||
|
||||
renderGridView() {
|
||||
renderGridView({ from = 0 } = {}) {
|
||||
const grid = document.getElementById('galleryGrid');
|
||||
const list = document.getElementById('galleryList');
|
||||
const showInfo = this.getSettings().showImageInfo;
|
||||
@@ -475,7 +473,8 @@
|
||||
list.classList.add('hidden');
|
||||
grid.classList.remove('hidden');
|
||||
|
||||
grid.innerHTML = this.images.map((image, index) => {
|
||||
const html = this.images.slice(from).map((image, offset) => {
|
||||
const index = from + offset;
|
||||
// Use thumbnail for display if available, fallback to original
|
||||
const displayUrl = image.thumbnail_url || image.url;
|
||||
const hasThumb = !!image.thumbnail_url;
|
||||
@@ -483,7 +482,7 @@
|
||||
const mediaType = image.media_type || 'image';
|
||||
|
||||
return `
|
||||
<div class="image-item bg-pm-surface rounded-pm-md overflow-hidden border border-pm hover:border-pm cursor-pointer group">
|
||||
<div data-image-index="${index}" class="image-item bg-pm-surface rounded-pm-md overflow-hidden border border-pm hover:border-pm cursor-pointer group">
|
||||
<div class="aspect-square bg-pm-primary overflow-hidden relative">
|
||||
<img src="${escapeHtml(displayUrl)}"
|
||||
alt="${escapeHtml(image.filename)}"
|
||||
@@ -525,9 +524,11 @@
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
if (from === 0) grid.innerHTML = html;
|
||||
else grid.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
renderListView() {
|
||||
renderListView({ from = 0 } = {}) {
|
||||
const grid = document.getElementById('galleryGrid');
|
||||
const list = document.getElementById('galleryList');
|
||||
const showInfo = this.getSettings().showImageInfo;
|
||||
@@ -535,7 +536,8 @@
|
||||
grid.classList.add('hidden');
|
||||
list.classList.remove('hidden');
|
||||
|
||||
list.innerHTML = this.images.map((image, index) => {
|
||||
const html = this.images.slice(from).map((image, offset) => {
|
||||
const index = from + offset;
|
||||
// Use thumbnail for display if available, fallback to original
|
||||
const displayUrl = image.thumbnail_url || image.url;
|
||||
const hasThumb = !!image.thumbnail_url;
|
||||
@@ -543,7 +545,7 @@
|
||||
const mediaType = image.media_type || 'image';
|
||||
|
||||
return `
|
||||
<div class="image-item bg-pm-surface rounded-pm-md border border-pm hover:border-pm cursor-pointer group flex items-center p-4">
|
||||
<div data-image-index="${index}" class="image-item bg-pm-surface rounded-pm-md border border-pm hover:border-pm cursor-pointer group flex items-center p-4">
|
||||
<div class="w-16 h-16 bg-pm-primary rounded-pm-md overflow-hidden flex-shrink-0 mr-4 relative">
|
||||
<img src="${escapeHtml(displayUrl)}"
|
||||
alt="${escapeHtml(image.filename)}"
|
||||
@@ -576,26 +578,35 @@
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
if (from === 0) list.innerHTML = html;
|
||||
else list.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
initializeViewer() {
|
||||
// Destroy existing viewer if it exists
|
||||
destroyViewer({ keepSlideshow = false } = {}) {
|
||||
if (!keepSlideshow) this.stopViewerSlideshow();
|
||||
if (this.viewer) {
|
||||
this.viewer.clearEnforceFocus();
|
||||
this.viewer.destroy();
|
||||
}
|
||||
this.viewer = null;
|
||||
document.getElementById('metadata-sidebar')?.remove();
|
||||
}
|
||||
|
||||
const container = this.viewMode === 'grid' ? document.getElementById('galleryGrid') : document.getElementById('galleryList');
|
||||
|
||||
// Find all images in the container
|
||||
const images = container.querySelectorAll('img[data-original]');
|
||||
|
||||
if (images.length === 0) {
|
||||
console.warn('No images found for viewer initialization');
|
||||
return;
|
||||
}
|
||||
openViewer(index, { keepSlideshow = false } = {}) {
|
||||
if (!Number.isInteger(index) || index < 0 || index >= this.images.length) return;
|
||||
this.destroyViewer({ keepSlideshow });
|
||||
const size = 100;
|
||||
const start = Math.max(0, Math.min(index - size / 2, this.images.length - size));
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = this.images.slice(start, start + size).map(image =>
|
||||
`<img src="${escapeHtml(image.thumbnail_url || image.url)}"
|
||||
data-original="${escapeHtml(image.url)}"
|
||||
alt="${escapeHtml(image.filename)}">`
|
||||
).join('');
|
||||
|
||||
this.viewer = new Viewer(container, {
|
||||
inline: false,
|
||||
initialViewIndex: index - start,
|
||||
button: true,
|
||||
backdrop: true,
|
||||
navbar: true,
|
||||
@@ -624,14 +635,20 @@
|
||||
// Add metadata sidebar when ViewerJS opens
|
||||
// Use the original image URL from data-original attribute
|
||||
const originalImg = event.detail.originalImage;
|
||||
setTimeout(() => this.addMetadataSidebar(originalImg), 100);
|
||||
this.addMetadataSidebar(originalImg);
|
||||
},
|
||||
show: function() {
|
||||
play: (event) => {
|
||||
event.preventDefault();
|
||||
if (this.slideshowPlaying) this.stopViewerSlideshow();
|
||||
else {
|
||||
this.slideshowPlaying = true;
|
||||
if (this.viewer.options.fullscreen) this.viewer.requestFullscreen();
|
||||
this.scheduleViewerSlide();
|
||||
}
|
||||
},
|
||||
shown: function() {
|
||||
},
|
||||
hide: function() {
|
||||
// Remove sidebar when viewer closes
|
||||
hide: () => {
|
||||
this.viewer?.clearEnforceFocus();
|
||||
this.stopViewerSlideshow();
|
||||
const sidebar = document.getElementById('metadata-sidebar');
|
||||
if (sidebar) {
|
||||
sidebar.remove();
|
||||
@@ -639,12 +656,50 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Don't add click handlers here - let ViewerJS handle them
|
||||
const viewer = this.viewer;
|
||||
const navigate = (direction, loop = false) => {
|
||||
if (viewer !== this.viewer || viewer.hiding || viewer.played) return viewer;
|
||||
let target = start + viewer.index + direction;
|
||||
if (loop) target = (target + this.images.length) % this.images.length;
|
||||
else target = Math.max(0, Math.min(target, this.images.length - 1));
|
||||
if (target >= start && target < start + viewer.length) {
|
||||
viewer.view(target - start);
|
||||
} else {
|
||||
this.openViewer(target, { keepSlideshow: true });
|
||||
}
|
||||
return this.viewer;
|
||||
};
|
||||
viewer.prev = (loop) => navigate(-1, loop);
|
||||
viewer.next = (loop) => navigate(1, loop);
|
||||
viewer.show();
|
||||
}
|
||||
|
||||
stopViewerSlideshow() {
|
||||
clearTimeout(this.slideshowTimer);
|
||||
this.slideshowTimer = null;
|
||||
if (this.slideshowPlaying) this.viewer?.exitFullscreen();
|
||||
this.slideshowPlaying = false;
|
||||
}
|
||||
|
||||
scheduleViewerSlide() {
|
||||
// Native Viewer.js slideshows preload every original and loop within the filmstrip.
|
||||
this.slideshowTimer = setTimeout(() => {
|
||||
this.slideshowTimer = null;
|
||||
if (!this.slideshowPlaying || !this.viewer) return;
|
||||
this.viewer.next(true);
|
||||
if (this.slideshowPlaying) this.scheduleViewerSlide();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
addMetadataSidebar(originalImage) {
|
||||
const viewerContainer = document.querySelector('.viewer-container');
|
||||
if (!viewerContainer || document.getElementById('metadata-sidebar')) return;
|
||||
if (!viewerContainer) return;
|
||||
const existing = document.getElementById('metadata-sidebar');
|
||||
if (existing) {
|
||||
this.loadImageMetadata(originalImage.dataset.original || originalImage.src,
|
||||
existing.querySelector('#metadata-content'));
|
||||
return;
|
||||
}
|
||||
|
||||
const sidebar = document.createElement('div');
|
||||
sidebar.id = 'metadata-sidebar';
|
||||
@@ -690,79 +745,35 @@
|
||||
// Load metadata for the current image (ALWAYS use original for metadata)
|
||||
const originalImageUrl = originalImage.dataset.original || originalImage.src;
|
||||
this.loadImageMetadata(originalImageUrl, sidebar.querySelector('#metadata-content'));
|
||||
|
||||
// Listen for ViewerJS view changes to update metadata
|
||||
this.setupViewerMetadataUpdates(sidebar);
|
||||
}
|
||||
|
||||
setupViewerMetadataUpdates(sidebar) {
|
||||
// Listen for ViewerJS navigation events
|
||||
const viewerContainer = document.querySelector('.viewer-container');
|
||||
if (!viewerContainer) return;
|
||||
|
||||
// Override ViewerJS navigation to update metadata
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
|
||||
const img = mutation.target;
|
||||
if (img.tagName === 'IMG' && img.closest('.viewer-canvas')) {
|
||||
// Find the corresponding original image
|
||||
const imageUrl = img.src;
|
||||
this.loadImageMetadata(imageUrl, sidebar.querySelector('#metadata-content'));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const canvas = viewerContainer.querySelector('.viewer-canvas');
|
||||
if (canvas) {
|
||||
observer.observe(canvas, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['src']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
addFallbackClickHandlers() {
|
||||
// Add simple click handlers that work even if ViewerJS fails
|
||||
const container = this.viewMode === 'grid' ? document.getElementById('galleryGrid') : document.getElementById('galleryList');
|
||||
const imageItems = container.querySelectorAll('.image-item');
|
||||
|
||||
imageItems.forEach((item, index) => {
|
||||
item.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
for (const id of ['galleryGrid', 'galleryList']) {
|
||||
const container = document.getElementById(id);
|
||||
if (container.dataset.mediaClicksBound) continue;
|
||||
container.dataset.mediaClicksBound = '1';
|
||||
container.addEventListener('click', (event) => {
|
||||
const item = event.target.closest('.image-item');
|
||||
if (!item) return;
|
||||
const img = item.querySelector('img[data-original]');
|
||||
if (img) {
|
||||
// ALWAYS use the original media URL for viewing
|
||||
const originalMediaUrl = img.dataset.original;
|
||||
const caption = img.dataset.caption;
|
||||
const isVideo = img.dataset.isVideo === 'true';
|
||||
const mediaType = img.dataset.mediaType || 'image';
|
||||
|
||||
|
||||
if (isVideo) {
|
||||
// For videos, use our custom video modal
|
||||
this.showVideoModal(originalMediaUrl, caption, index);
|
||||
} else {
|
||||
// For images, try ViewerJS first, fallback to simple modal
|
||||
if (this.viewer) {
|
||||
try {
|
||||
this.viewer.view(index);
|
||||
} catch (e) {
|
||||
console.warn('ViewerJS failed, using fallback:', e);
|
||||
this.showSimpleImageModal(originalMediaUrl, caption);
|
||||
}
|
||||
} else {
|
||||
this.showSimpleImageModal(originalMediaUrl, caption);
|
||||
}
|
||||
if (!img) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const index = Number(item.dataset.imageIndex);
|
||||
const { original, caption, isVideo } = img.dataset;
|
||||
if (isVideo === 'true') {
|
||||
this.showVideoModal(original, caption, index);
|
||||
} else {
|
||||
try {
|
||||
this.openViewer(index);
|
||||
} catch (error) {
|
||||
console.warn('ViewerJS failed, using fallback:', error);
|
||||
this.destroyViewer();
|
||||
this.showSimpleImageModal(original, caption);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async showSimpleImageModal(imageUrl, caption) {
|
||||
|
||||
Reference in New Issue
Block a user