// components/file-upload-component.js - File upload and management import { Utils } from '../utils.js'; import { StateManager } from '../services/state-manager.js'; import { ApiService } from '../services/api-service.js'; import { TranslationService } from '../services/translation-service.js'; export const FileUploadComponent = { elements: { uploadFileBtn: null, uploadFileOverlay: null, fileDropZone: null, fileInput: null, doneFileUploadBtn: null, closeFileUploadBtn: null, fileListHtml: null }, constants: { FILE_SIZE_LIMIT: 10 * 1024 * 1024, // 10 MB TOTAL_FILE_SIZE_LIMIT: 30 * 1024 * 1024, // 30 MB MAX_FILE_NAME_LENGTH: 50, ALLOWED_TYPES: ['.pdf', '.txt', '.docx', '.jpg', '.jpeg', '.png'] }, icons: { upload: ` `, spinner: ` `, check: ` `, trash: ` ` }, /** * Initialize the file upload component */ init() { this.elements.uploadFileBtn = document.getElementById('upload-file-btn'); this.elements.uploadFileOverlay = document.getElementById('upload-file-overlay'); this.elements.fileDropZone = document.getElementById('file-drop-zone'); this.elements.fileInput = document.getElementById('file-input'); this.elements.doneFileUploadBtn = document.getElementById('done-file-upload'); this.elements.closeFileUploadBtn = document.getElementById('close-file-upload-btn'); this.elements.fileListHtml = document.getElementById('file-list'); this.attachOutsideClickListener(); this.attachEventListeners(); this.renderFiles(); }, attachOutsideClickListener() { this.elements.uploadFileOverlay.addEventListener('click', (e) => { // Check if click is on the overlay itself (not its children) if (e.target === this.elements.uploadFileOverlay) { this.closeOverlay(); } }); }, /** * Attach event listeners */ attachEventListeners() { this.elements.uploadFileBtn.addEventListener('click', (e) => this.openOverlay(e)); this.elements.fileDropZone.addEventListener('click', () => this.elements.fileInput.click()); this.elements.closeFileUploadBtn.addEventListener('click', () => this.closeOverlay()); this.elements.doneFileUploadBtn.addEventListener('click', () => this.closeOverlay()); // Prevent the browser from opening a dropped file ['dragover', 'drop'].forEach(eventName => { this.elements.fileDropZone.addEventListener(eventName, (e) => e.preventDefault()); }); this.elements.fileDropZone.addEventListener('dragover', () => { this.elements.fileDropZone.classList.add('active'); }); // File drop logic this.elements.fileDropZone.addEventListener('drop', (e) => { this.elements.fileDropZone.classList.remove('active'); const addedFiles = Array.from(e.dataTransfer.files); this.handleFileAddition(addedFiles); }); // File browsing logic this.elements.fileInput.addEventListener('change', (e) => { const addedFiles = Array.from(e.target.files); this.handleFileAddition(addedFiles); }); }, /** * Open the upload overlay */ openOverlay(e) { e.preventDefault(); this.elements.uploadFileOverlay.style.display = ''; }, /** * Close the upload overlay */ closeOverlay() { this.elements.uploadFileOverlay.style.display = 'none'; }, /** * Handle file addition (drop or browse) * @param {Array} newFiles - Array of new files */ async handleFileAddition(newFiles) { const isProcessingSuccessful = this.processFiles(newFiles); if (!isProcessingSuccessful) { return; } newFiles.forEach(file => StateManager.addFile(file)); // Upload files newFiles.forEach(async (file) => { file.state = 'uploading'; this.renderFiles(); const isUploadSuccessful = await ApiService.uploadFile(file); file.state = isUploadSuccessful ? 'uploaded' : 'ready'; this.renderFiles(); }); this.renderFiles(); }, /** * Validate files before adding * @param {Array} newFiles - Array of files to validate * @returns {boolean} Whether files are valid */ processFiles(newFiles) { // Check file types const unallowedFiles = newFiles.filter((file) => !this.constants.ALLOWED_TYPES.some(ext => file.name.endsWith(ext)) ); if (unallowedFiles.length > 0) { newFiles.forEach((file) => Utils.removeFileFromInput(this.elements.fileInput, file)); showSnackbar(translations[StateManager.currentLang]["error_file_format"], "error"); return false; } // Check individual file size const largeFiles = newFiles.filter((file) => file.size > this.constants.FILE_SIZE_LIMIT); if (largeFiles.length > 0) { newFiles.forEach((file) => Utils.removeFileFromInput(this.elements.fileInput, file)); showSnackbar(translations[StateManager.currentLang]["error_file_size"], "error"); return false; } // Check total file size const totalFileSize = [...newFiles, ...StateManager.getFiles()].reduce((sum, file) => sum + file.size, 0); if (totalFileSize > this.constants.TOTAL_FILE_SIZE_LIMIT) { newFiles.forEach((file) => Utils.removeFileFromInput(this.elements.fileInput, file)); showSnackbar(translations[StateManager.currentLang]["error_total_file_size"], "error"); return false; } // Check file name length const filesWithLongName = newFiles.filter((file) => file.name.length > this.constants.MAX_FILE_NAME_LENGTH); if (filesWithLongName.length > 0) { newFiles.forEach((file) => Utils.removeFileFromInput(this.elements.fileInput, file)); showSnackbar(translations[StateManager.currentLang]["error_file_name_length"], "error"); return false; } return true; }, /** * Render the file list */ renderFiles() { this.elements.fileListHtml.innerHTML = ''; const sessionFiles = StateManager.getFiles(); if (sessionFiles.length === 0) { const noFileMessage = document.createElement('div'); noFileMessage.classList.add('no-file'); noFileMessage.dataset.i18n = "no_files"; this.elements.fileListHtml.appendChild(noFileMessage); TranslationService.applyTranslation(); return; } sessionFiles.forEach((f) => { const fileItem = document.createElement('div'); fileItem.classList.add('file-item'); fileItem.textContent = f.name; const fileActions = document.createElement('div'); fileActions.classList.add('file-actions'); const uploadButton = this.createUploadButton(f); const deleteButton = this.createDeleteButton(f); fileActions.appendChild(uploadButton); fileActions.appendChild(deleteButton); fileItem.appendChild(fileActions); this.elements.fileListHtml.appendChild(fileItem); }); TranslationService.applyTranslation(); }, /** * Create upload button for a file * @param {File} file - File object * @returns {HTMLButtonElement} Upload button */ createUploadButton(file) { const uploadButton = document.createElement('button'); if (file.state === 'uploaded') { uploadButton.innerHTML = this.icons.check + ``; uploadButton.classList.add('disabled-button'); uploadButton.disabled = true; } else if (file.state === 'uploading') { uploadButton.innerHTML = this.icons.spinner + ``; uploadButton.classList.add('disabled-button'); uploadButton.disabled = true; } else if (file.state === 'ready') { uploadButton.innerHTML = this.icons.upload + ``; uploadButton.classList.add('ok-button'); uploadButton.addEventListener('click', async () => { file.state = 'uploading'; this.renderFiles(); const isUploadSuccessful = await ApiService.uploadFile(file); file.state = isUploadSuccessful ? 'uploaded' : 'ready'; this.renderFiles(); }); } return uploadButton; }, /** * Create delete button for a file * @param {File} file - File object * @returns {HTMLButtonElement} Delete button */ createDeleteButton(file) { const deleteButton = document.createElement('button'); deleteButton.innerHTML = this.icons.trash + ``; deleteButton.classList.add('no-button'); deleteButton.addEventListener('click', async () => { // No need to send a request to the server if the file was not uploaded const isDeletionSuccessful = file.state === 'uploaded' ? await ApiService.deleteFile(file) : true; if (isDeletionSuccessful) { Utils.removeFileFromInput(this.elements.fileInput, file); StateManager.removeFile(file); this.renderFiles(); } }); return deleteButton; } };