import { Logger } from '../../core/logger.js'; import { EventEmitter } from './EventEmitter.js'; export class ImageGallery extends EventEmitter { constructor(config, state) { super(); this.config = config; this.state = state; this.galleries = new Map(); this.currentImages = new Map(); this.currentPage = 1; this.hasMore = true; this.isLoading = false; this.searchTerm = ''; this.sortField = 'ulid'; this.sortDirection = 'desc'; } async init() { const elements = document.querySelectorAll('[data-image-gallery]'); for (const element of elements) { await this.initializeGallery(element); } Logger.info(`[ImageGallery] Initialized ${elements.length} galleries`); } async initializeGallery(element) { const config = this.parseElementConfig(element); const gallery = new Gallery(element, config, this); this.galleries.set(element, gallery); await gallery.init(); } parseElementConfig(element) { return { pageSize: parseInt(element.dataset.pageSize) || 20, columns: parseInt(element.dataset.columns) || 4, showPagination: element.hasAttribute('data-pagination'), showSearch: element.hasAttribute('data-search'), showSort: element.hasAttribute('data-sort'), allowDelete: element.hasAttribute('data-allow-delete'), allowEdit: element.hasAttribute('data-allow-edit'), selectable: element.hasAttribute('data-selectable'), ...this.config }; } async loadImages(page = 1, search = '', sort = 'ulid', direction = 'desc') { if (this.isLoading) return; this.isLoading = true; this.emit('gallery:loading', true); try { const params = new URLSearchParams({ page: page.toString(), limit: this.config.pageSize?.toString() || '20', sort: sort, direction: direction }); if (search) { params.append('search', search); } const response = await fetch(`${this.config.listEndpoint}?${params}`, { headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); this.currentPage = page; this.hasMore = data.meta.has_more; this.searchTerm = search; this.sortField = sort; this.sortDirection = direction; // Store images for this gallery instance const galleryKey = `${search}_${sort}_${direction}`; if (page === 1) { this.currentImages.set(galleryKey, data.data); } else { const existing = this.currentImages.get(galleryKey) || []; this.currentImages.set(galleryKey, [...existing, ...data.data]); } this.emit('gallery:loaded', { images: this.currentImages.get(galleryKey), meta: data.meta, page, hasMore: this.hasMore }); return data; } catch (error) { Logger.error('[ImageGallery] Failed to load images:', error); this.emit('gallery:error', error); throw error; } finally { this.isLoading = false; this.emit('gallery:loading', false); } } async deleteImage(imageId) { try { const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || document.querySelector('input[name="_token"]')?.value; const response = await fetch(`${this.config.uploadEndpoint}/${imageId}`, { method: 'DELETE', headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-TOKEN': csrfToken || '' } }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || `HTTP ${response.status}`); } // Remove from current images for (const [key, images] of this.currentImages) { const filtered = images.filter(img => img.ulid !== imageId); this.currentImages.set(key, filtered); } this.emit('image:delete', { imageId }); return true; } catch (error) { Logger.error('[ImageGallery] Failed to delete image:', error); this.emit('gallery:error', error); throw error; } } async refresh() { await this.loadImages(1, this.searchTerm, this.sortField, this.sortDirection); } destroy() { for (const [element, gallery] of this.galleries) { gallery.destroy(); } this.galleries.clear(); this.currentImages.clear(); this.removeAllListeners(); } } class Gallery { constructor(element, config, manager) { this.element = element; this.config = config; this.manager = manager; this.gridContainer = null; this.searchInput = null; this.sortSelect = null; this.loadMoreButton = null; this.currentImages = []; this.selectedImages = new Set(); } async init() { this.createHTML(); this.setupEventListeners(); await this.loadInitialImages(); } createHTML() { this.element.classList.add('image-gallery'); this.element.innerHTML = ` ${this.config.showSearch ? this.createSearchHTML() : ''} ${this.config.showSort ? this.createSortHTML() : ''} ${this.config.showPagination ? this.createPaginationHTML() : ''} `; this.gridContainer = this.element.querySelector('.gallery__grid'); this.searchInput = this.element.querySelector('.gallery__search'); this.sortSelect = this.element.querySelector('.gallery__sort'); this.loadMoreButton = this.element.querySelector('.gallery__load-more'); } createSearchHTML() { return ` `; } createSortHTML() { return ` `; } createPaginationHTML() { return ` `; } setupEventListeners() { // Search functionality if (this.searchInput) { let searchTimeout; this.searchInput.addEventListener('input', (e) => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { this.handleSearch(e.target.value); }, 300); }); const clearButton = this.element.querySelector('.gallery__search-clear'); if (clearButton) { clearButton.addEventListener('click', () => { this.searchInput.value = ''; this.handleSearch(''); }); this.searchInput.addEventListener('input', (e) => { clearButton.style.display = e.target.value ? 'block' : 'none'; }); } } // Sort functionality if (this.sortSelect) { this.sortSelect.addEventListener('change', (e) => { const [field, direction] = e.target.value.split(':'); this.handleSort(field, direction); }); } // Load more functionality if (this.loadMoreButton) { this.loadMoreButton.addEventListener('click', () => { this.loadMore(); }); } // Manager event listeners this.manager.on('gallery:loaded', (data) => { this.renderImages(data.images); this.updatePagination(data.hasMore); }); this.manager.on('gallery:loading', (isLoading) => { this.toggleLoading(isLoading); }); this.manager.on('gallery:error', (error) => { this.showError(error.message); }); // Intersection Observer for infinite scroll if (this.config.showPagination) { this.setupInfiniteScroll(); } } setupInfiniteScroll() { const observer = new IntersectionObserver((entries) => { const [entry] = entries; if (entry.isIntersecting && this.manager.hasMore && !this.manager.isLoading) { this.loadMore(); } }, { rootMargin: '100px' }); // Observe the load more button if (this.loadMoreButton) { observer.observe(this.loadMoreButton); } } async loadInitialImages() { await this.manager.loadImages(1); } async handleSearch(term) { await this.manager.loadImages(1, term, this.manager.sortField, this.manager.sortDirection); } async handleSort(field, direction) { await this.manager.loadImages(1, this.manager.searchTerm, field, direction); } async loadMore() { if (this.manager.hasMore && !this.manager.isLoading) { await this.manager.loadImages( this.manager.currentPage + 1, this.manager.searchTerm, this.manager.sortField, this.manager.sortDirection ); } } renderImages(images) { this.currentImages = images; if (images.length === 0) { this.showEmptyState(); return; } this.hideEmptyState(); this.gridContainer.innerHTML = ''; images.forEach(image => { const imageElement = this.createImageElement(image); this.gridContainer.appendChild(imageElement); }); } createImageElement(image) { const imageItem = document.createElement('div'); imageItem.className = 'gallery__item'; imageItem.dataset.imageId = image.ulid; const aspectRatio = image.dimensions.width / image.dimensions.height; const gridSpan = aspectRatio > 1.5 ? 2 : 1; // Wide images span 2 columns imageItem.style.gridColumn = `span ${Math.min(gridSpan, this.config.columns)}`; imageItem.innerHTML = ` `; this.setupImageEventListeners(imageItem, image); return imageItem; } setupImageEventListeners(imageItem, image) { // View button const viewBtn = imageItem.querySelector('.view-btn'); if (viewBtn) { viewBtn.addEventListener('click', (e) => { e.stopPropagation(); this.manager.emit('image:view', image); }); } // Edit button const editBtn = imageItem.querySelector('.edit-btn'); if (editBtn) { editBtn.addEventListener('click', (e) => { e.stopPropagation(); this.manager.emit('image:edit', image); }); } // Delete button const deleteBtn = imageItem.querySelector('.delete-btn'); if (deleteBtn) { deleteBtn.addEventListener('click', async (e) => { e.stopPropagation(); if (confirm(`Delete "${image.filename}"?`)) { try { await this.manager.deleteImage(image.ulid); imageItem.remove(); } catch (error) { alert('Failed to delete image: ' + error.message); } } }); } // Select button const selectBtn = imageItem.querySelector('.select-btn'); if (selectBtn) { selectBtn.addEventListener('click', (e) => { e.stopPropagation(); this.toggleSelection(imageItem, image); }); } // Double-click to view imageItem.addEventListener('dblclick', () => { this.manager.emit('image:view', image); }); } toggleSelection(imageItem, image) { if (this.selectedImages.has(image.ulid)) { this.selectedImages.delete(image.ulid); imageItem.classList.remove('gallery__item--selected'); } else { this.selectedImages.add(image.ulid); imageItem.classList.add('gallery__item--selected'); } this.manager.emit('image:select', { image, selected: this.selectedImages.has(image.ulid), selectedCount: this.selectedImages.size }); } updatePagination(hasMore) { if (this.loadMoreButton) { this.loadMoreButton.style.display = hasMore ? 'block' : 'none'; } } toggleLoading(isLoading) { const loadingElement = this.element.querySelector('.gallery__loading'); if (loadingElement) { loadingElement.style.display = isLoading ? 'block' : 'none'; } if (this.loadMoreButton) { this.loadMoreButton.disabled = isLoading; this.loadMoreButton.textContent = isLoading ? 'Loading...' : 'Load More Images'; } } showEmptyState() { const emptyElement = this.element.querySelector('.gallery__empty'); if (emptyElement) { emptyElement.style.display = 'block'; } this.gridContainer.style.display = 'none'; } hideEmptyState() { const emptyElement = this.element.querySelector('.gallery__empty'); if (emptyElement) { emptyElement.style.display = 'none'; } this.gridContainer.style.display = 'grid'; } showError(message) { // Create or update error message let errorElement = this.element.querySelector('.gallery__error'); if (!errorElement) { errorElement = document.createElement('div'); errorElement.className = 'gallery__error'; this.element.appendChild(errorElement); } errorElement.innerHTML = `
⚠️ ${message}
`; // Auto-remove after 5 seconds setTimeout(() => { if (errorElement.parentNode) { errorElement.parentNode.removeChild(errorElement); } }, 5000); // Close button errorElement.querySelector('.error-close').addEventListener('click', () => { if (errorElement.parentNode) { errorElement.parentNode.removeChild(errorElement); } }); } destroy() { this.element.innerHTML = ''; this.element.classList.remove('image-gallery'); } }