/** * FileUploadWidget - Pre-built UI Component for File Uploads in LiveComponents * * Provides a complete upload interface with: * - Drag & Drop zone * - File list with thumbnails * - Progress bars * - File validation feedback * - Remove/cancel capabilities * * @package Framework\LiveComponents */ import { ComponentFileUploader } from './ComponentFileUploader.js'; /** * FileUploadWidget - Ready-to-use Upload UI */ export class FileUploadWidget { constructor(containerElement, options = {}) { this.container = containerElement; this.componentElement = containerElement.closest('[data-live-id]'); if (!this.componentElement) { throw new Error('FileUploadWidget must be used inside a LiveComponent'); } // Widget options this.options = { maxFileSize: options.maxFileSize || 10 * 1024 * 1024, allowedMimeTypes: options.allowedMimeTypes || [], allowedExtensions: options.allowedExtensions || [], maxFiles: options.maxFiles || 10, showPreviews: options.showPreviews !== false, showProgress: options.showProgress !== false, showFileList: options.showFileList !== false, autoUpload: options.autoUpload !== false, multiple: options.multiple !== false, dropZoneText: options.dropZoneText || 'Drag & drop files here or click to browse', browseButtonText: options.browseButtonText || 'Browse Files', uploadButtonText: options.uploadButtonText || 'Upload All', ...options }; this.files = new Map(); // Map this.buildUI(); this.initializeUploader(); } /** * Build the widget UI */ buildUI() { // Create widget structure this.container.innerHTML = `

${this.options.dropZoneText}

0 ? `accept="${this.options.allowedMimeTypes.join(',')}"` : ''} style="display: none;" />
${this.options.showFileList ? ` ` : ''} ${this.options.showProgress ? ` ` : ''}
`; // Cache DOM elements this.dropZone = this.container.querySelector('[data-dropzone]'); this.fileInput = this.container.querySelector('[data-file-input]'); this.browseButton = this.container.querySelector('[data-browse-button]'); this.fileList = this.container.querySelector('[data-file-list]'); this.fileItems = this.container.querySelector('[data-file-items]'); this.overallProgress = this.container.querySelector('[data-overall-progress]'); this.progressFill = this.container.querySelector('[data-progress-fill]'); this.progressText = this.container.querySelector('[data-progress-text]'); this.uploadAllButton = this.container.querySelector('[data-upload-all]'); this.clearAllButton = this.container.querySelector('[data-clear-all]'); // Bind UI events this.browseButton?.addEventListener('click', () => this.fileInput.click()); this.uploadAllButton?.addEventListener('click', () => this.uploader.uploadAll()); this.clearAllButton?.addEventListener('click', () => this.clearAll()); } /** * Initialize the uploader */ initializeUploader() { this.uploader = new ComponentFileUploader(this.componentElement, { ...this.options, dropZone: this.dropZone, fileInput: this.fileInput, onFileAdded: (data) => this.handleFileAdded(data), onFileRemoved: (data) => this.handleFileRemoved(data), onUploadStart: (data) => this.handleUploadStart(data), onUploadProgress: (data) => this.handleUploadProgress(data), onUploadComplete: (data) => this.handleUploadComplete(data), onUploadError: (data) => this.handleUploadError(data), onAllUploadsComplete: (data) => this.handleAllUploadsComplete(data) }); } /** * Handle file added */ handleFileAdded({ fileId, file, progress }) { if (!this.options.showFileList) return; // Show file list if (this.fileList) { this.fileList.style.display = 'block'; } // Create file UI element const fileElement = this.createFileElement(fileId, file, progress); this.fileItems.appendChild(fileElement); this.files.set(fileId, fileElement); // Update file count this.updateFileCount(); } /** * Handle file removed */ handleFileRemoved({ fileId }) { const fileElement = this.files.get(fileId); if (fileElement) { fileElement.remove(); this.files.delete(fileId); } // Update file count this.updateFileCount(); // Hide file list if empty if (this.files.size === 0 && this.fileList) { this.fileList.style.display = 'none'; } } /** * Handle upload start */ handleUploadStart({ fileId }) { this.updateFileStatus(fileId, 'uploading'); // Show overall progress if (this.overallProgress) { this.overallProgress.style.display = 'block'; } } /** * Handle upload progress */ handleUploadProgress({ fileId, percentage, uploadedBytes, totalBytes, uploadSpeed, remainingTime }) { // Update file progress bar const fileElement = this.files.get(fileId); if (fileElement) { const progressBar = fileElement.querySelector('.file-progress-fill'); const progressText = fileElement.querySelector('.file-progress-text'); if (progressBar) { progressBar.style.width = `${percentage}%`; } if (progressText) { progressText.textContent = `${percentage}%`; } } // Update overall progress const stats = this.uploader.getStats(); if (this.progressFill) { this.progressFill.style.width = `${stats.overallProgress}%`; } if (this.progressText) { this.progressText.textContent = `${stats.overallProgress}%`; } } /** * Handle upload complete */ handleUploadComplete({ fileId, response }) { this.updateFileStatus(fileId, 'complete'); } /** * Handle upload error */ handleUploadError({ fileId, error }) { this.updateFileStatus(fileId, 'error', error); } /** * Handle all uploads complete */ handleAllUploadsComplete({ totalFiles, successCount, errorCount }) { // Hide overall progress after a delay setTimeout(() => { if (this.overallProgress) { this.overallProgress.style.display = 'none'; } }, 2000); } /** * Create file UI element */ createFileElement(fileId, file, progress) { const div = document.createElement('div'); div.className = 'file-item'; div.dataset.fileId = fileId; const statusClass = progress.error ? 'error' : 'pending'; div.innerHTML = `
${this.options.showPreviews && file.type.startsWith('image/') ? `${file.name}` : this.getFileIconSvg(file.type) }
${this.truncateFileName(file.name, 40)}
${this.formatBytes(file.size)} ${progress.error || 'Pending'}
${this.options.showProgress ? ` ` : ''} ${progress.error ? `
${progress.error}
` : ''}
`; // Bind remove button const removeButton = div.querySelector('[data-remove-file]'); removeButton.addEventListener('click', () => this.uploader.removeFile(fileId)); return div; } /** * Update file status in UI */ updateFileStatus(fileId, status, errorMessage = null) { const fileElement = this.files.get(fileId); if (!fileElement) return; const statusElement = fileElement.querySelector('.file-status'); const progressElement = fileElement.querySelector('.file-progress'); const errorElement = fileElement.querySelector('.file-error'); // Update status text and class if (statusElement) { statusElement.dataset.status = status; const statusText = { pending: 'Pending', uploading: 'Uploading...', complete: 'Complete', error: 'Error' }[status] || status; statusElement.textContent = statusText; } // Show/hide progress if (progressElement) { progressElement.style.display = status === 'uploading' ? 'flex' : 'none'; } // Handle errors if (status === 'error' && errorMessage) { if (errorElement) { errorElement.textContent = errorMessage; errorElement.style.display = 'block'; } } // Add completion/error visual feedback fileElement.classList.remove('file-uploading', 'file-complete', 'file-error'); if (status === 'uploading') fileElement.classList.add('file-uploading'); if (status === 'complete') fileElement.classList.add('file-complete'); if (status === 'error') fileElement.classList.add('file-error'); } /** * Update file count in header */ updateFileCount() { const header = this.container.querySelector('.file-list-header h4'); if (header) { header.textContent = `Files (${this.files.size})`; } } /** * Clear all files */ clearAll() { this.uploader.clearAll(); this.fileItems.innerHTML = ''; this.files.clear(); if (this.fileList) { this.fileList.style.display = 'none'; } this.updateFileCount(); } /** * Helper: Format bytes */ formatBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]; } /** * Helper: Truncate file name */ truncateFileName(name, maxLength) { if (name.length <= maxLength) return name; const extension = name.split('.').pop(); const baseName = name.substring(0, name.length - extension.length - 1); const truncated = baseName.substring(0, maxLength - extension.length - 4); return `${truncated}...${extension}`; } /** * Helper: Get file icon SVG */ getFileIconSvg(mimeType) { // Document icon return ` `; } /** * Cleanup */ destroy() { this.uploader.destroy(); this.container.innerHTML = ''; this.files.clear(); } } export default FileUploadWidget;