import { Logger } from '../../core/logger.js'; import { EventEmitter } from './EventEmitter.js'; export class ImageUploader extends EventEmitter { constructor(config, state) { super(); this.config = config; this.state = state; this.uploadAreas = new Map(); this.activeUploads = new Map(); } async init() { const elements = document.querySelectorAll('[data-image-upload]'); for (const element of elements) { await this.initializeUploadArea(element); } Logger.info(`[ImageUploader] Initialized ${elements.length} upload areas`); } async initializeUploadArea(element) { const config = this.parseElementConfig(element); const uploadArea = new UploadArea(element, config, this); this.uploadAreas.set(element, uploadArea); await uploadArea.init(); } parseElementConfig(element) { return { multiple: element.hasAttribute('data-multiple'), maxFiles: parseInt(element.dataset.maxFiles) || 10, accept: element.dataset.accept || this.config.allowedTypes.join(','), maxSize: parseInt(element.dataset.maxSize) || this.config.maxFileSize, preview: element.hasAttribute('data-preview'), ...this.config }; } async uploadFile(file, config, progressCallback) { const uploadId = this.generateUploadId(); try { // Validate file this.validateFile(file, config); // Create form data const formData = new FormData(); formData.append('image', file); // Add CSRF token if available const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || document.querySelector('input[name="_token"]')?.value; if (csrfToken) { formData.append('_token', csrfToken); } // Create XMLHttpRequest for progress tracking const xhr = new XMLHttpRequest(); // Track active upload this.activeUploads.set(uploadId, { xhr, file }); // Setup progress tracking xhr.upload.addEventListener('progress', (event) => { if (event.lengthComputable) { const progress = Math.round((event.loaded / event.total) * 100); progressCallback?.(progress); this.emit('upload:progress', { uploadId, file, progress }); } }); // Create upload promise const uploadPromise = new Promise((resolve, reject) => { xhr.onload = () => { this.activeUploads.delete(uploadId); if (xhr.status >= 200 && xhr.status < 300) { try { const response = JSON.parse(xhr.responseText); resolve(response); } catch (e) { reject(new Error('Invalid JSON response')); } } else { try { const errorResponse = JSON.parse(xhr.responseText); reject(new Error(errorResponse.message || `Upload failed: ${xhr.status}`)); } catch (e) { reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`)); } } }; xhr.onerror = () => { this.activeUploads.delete(uploadId); reject(new Error('Network error during upload')); }; xhr.onabort = () => { this.activeUploads.delete(uploadId); reject(new Error('Upload cancelled')); }; }); // Start upload xhr.open('POST', config.uploadEndpoint); xhr.send(formData); const result = await uploadPromise; this.emit('upload:success', result); return result; } catch (error) { this.activeUploads.delete(uploadId); this.emit('upload:error', error); throw error; } } validateFile(file, config) { // Check file type if (!config.allowedTypes.includes(file.type)) { throw new Error(`File type ${file.type} is not allowed`); } // Check file size if (file.size > config.maxSize) { const maxSizeMB = Math.round(config.maxSize / (1024 * 1024)); throw new Error(`File size exceeds ${maxSizeMB}MB limit`); } // Check if it's actually an image if (!file.type.startsWith('image/')) { throw new Error('File is not an image'); } } cancelUpload(uploadId) { const upload = this.activeUploads.get(uploadId); if (upload) { upload.xhr.abort(); this.activeUploads.delete(uploadId); this.emit('upload:cancelled', { uploadId, file: upload.file }); } } generateUploadId() { return 'upload_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); } destroy() { // Cancel all active uploads for (const [uploadId] of this.activeUploads) { this.cancelUpload(uploadId); } // Destroy all upload areas for (const [element, uploadArea] of this.uploadAreas) { uploadArea.destroy(); } this.uploadAreas.clear(); this.removeAllListeners(); } } class UploadArea { constructor(element, config, uploader) { this.element = element; this.config = config; this.uploader = uploader; this.fileInput = null; this.previewContainer = null; this.uploadQueue = []; this.isDragOver = false; } async init() { this.createHTML(); this.setupEventListeners(); this.setupDragAndDrop(); } createHTML() { this.element.classList.add('image-upload-area'); this.element.innerHTML = `
Supports ${this.config.allowedTypes.map(type => type.split('/')[1].toUpperCase()).join(', ')} up to ${Math.round(this.config.maxSize / (1024 * 1024))}MB