/** * File Upload Widget * * Handles file uploads (primarily images) with drag-and-drop support, * preview, delete, and scheduling functionality. * * @module FileUploadWidget */ (function() { 'use strict'; // Ensure LEDMatrixWidgets registry exists if (typeof window.LEDMatrixWidgets === 'undefined') { console.error('[FileUploadWidget] LEDMatrixWidgets registry not found. Load registry.js first.'); return; } /** * Register the file-upload widget */ window.LEDMatrixWidgets.register('file-upload', { name: 'File Upload Widget', version: '1.0.0', /** * Render the file upload widget * Note: This widget is currently server-side rendered via Jinja2 template. * This registration ensures the handlers are available globally. * Future enhancement: Full client-side rendering support. */ render: function(container, config, value, options) { // For now, widgets are server-side rendered // This function is a placeholder for future client-side rendering console.log('[FileUploadWidget] Render called (server-side rendered)'); }, /** * Get current value from widget * @param {string} fieldId - Field ID * @returns {Array} Array of uploaded files */ getValue: function(fieldId) { return window.getCurrentImages ? window.getCurrentImages(fieldId) : []; }, /** * Set value in widget * @param {string} fieldId - Field ID * @param {Array} images - Array of image objects */ setValue: function(fieldId, images) { if (window.updateImageList) { window.updateImageList(fieldId, images); } }, handlers: { // Handlers are attached to window for backwards compatibility } }); // ===== File Upload Handlers (Backwards Compatible) ===== // These functions are called from the server-rendered template /** * Handle file drop event * @param {Event} event - Drop event * @param {string} fieldId - Field ID */ window.handleFileDrop = function(event, fieldId) { event.preventDefault(); const files = event.dataTransfer.files; if (files.length === 0) return; // Route to single-file handler if this is a string file-upload widget const fileInput = document.getElementById(`${fieldId}_file_input`); if (fileInput && fileInput.dataset.uploadEndpoint && fileInput.dataset.uploadEndpoint.trim() !== '') { window.handleSingleFileUpload(fieldId, files[0]); } else { window.handleFiles(fieldId, Array.from(files)); } }; /** * Handle file select event * @param {Event} event - Change event * @param {string} fieldId - Field ID */ window.handleFileSelect = function(event, fieldId) { const files = event.target.files; if (files.length > 0) { window.handleFiles(fieldId, Array.from(files)); } }; /** * Handle single-file select for string file-upload widgets (e.g. credentials.json) * @param {Event} event - Change event * @param {string} fieldId - Field ID */ window.handleSingleFileSelect = function(event, fieldId) { const files = event.target.files; if (files.length > 0) { window.handleSingleFileUpload(fieldId, files[0]); } }; /** * Upload a single file for string file-upload widgets * Reads upload config from data attributes on the file input element. * @param {string} fieldId - Field ID * @param {File} file - File to upload */ window.handleSingleFileUpload = async function(fieldId, file) { const fileInput = document.getElementById(`${fieldId}_file_input`); if (!fileInput) return; const uploadEndpoint = fileInput.dataset.uploadEndpoint; const targetFilename = fileInput.dataset.targetFilename || 'file.json'; const maxSizeMB = parseFloat(fileInput.dataset.maxSizeMb || '1'); const allowedExtensions = (fileInput.dataset.allowedExtensions || '.json') .split(',').map(e => e.trim().toLowerCase()); const statusDiv = document.getElementById(`${fieldId}_upload_status`); const notifyFn = window.showNotification || console.log; // Guard: endpoint must be configured if (!uploadEndpoint) { notifyFn('No upload endpoint configured for this field', 'error'); return; } // Validate extension const fileExt = '.' + file.name.split('.').pop().toLowerCase(); if (!allowedExtensions.includes(fileExt)) { notifyFn(`File must be one of: ${allowedExtensions.join(', ')}`, 'error'); return; } // Validate size if (file.size > maxSizeMB * 1024 * 1024) { notifyFn(`File exceeds ${maxSizeMB}MB limit`, 'error'); return; } if (statusDiv) { statusDiv.className = 'mt-2 text-xs text-gray-500'; statusDiv.textContent = ''; const spinner = document.createElement('i'); spinner.className = 'fas fa-spinner fa-spin mr-1'; statusDiv.appendChild(spinner); statusDiv.appendChild(document.createTextNode('Uploading...')); } const formData = new FormData(); formData.append('file', file); try { const response = await fetch(uploadEndpoint, { method: 'POST', body: formData }); if (!response.ok) { const body = await response.text(); throw new Error(`Server error ${response.status}: ${body}`); } const data = await response.json(); if (data.status === 'success') { if (statusDiv) { statusDiv.className = 'mt-2 text-xs text-green-600'; statusDiv.textContent = ''; const icon = document.createElement('i'); icon.className = 'fas fa-check-circle mr-1'; statusDiv.appendChild(icon); statusDiv.appendChild(document.createTextNode(`Uploaded: ${targetFilename}`)); } // Update hidden input with the target filename const hiddenInput = document.getElementById(fieldId); if (hiddenInput) hiddenInput.value = targetFilename; notifyFn(`${targetFilename} uploaded successfully`, 'success'); } else { if (statusDiv) { statusDiv.className = 'mt-2 text-xs text-red-600'; statusDiv.textContent = ''; const icon = document.createElement('i'); icon.className = 'fas fa-exclamation-circle mr-1'; statusDiv.appendChild(icon); statusDiv.appendChild(document.createTextNode(`Upload failed: ${data.message}`)); } notifyFn(`Upload failed: ${data.message}`, 'error'); } } catch (error) { if (statusDiv) { statusDiv.className = 'mt-2 text-xs text-red-600'; statusDiv.textContent = ''; const icon = document.createElement('i'); icon.className = 'fas fa-exclamation-circle mr-1'; statusDiv.appendChild(icon); statusDiv.appendChild(document.createTextNode(`Upload error: ${error.message}`)); } notifyFn(`Upload error: ${error.message}`, 'error'); } finally { if (fileInput) fileInput.value = ''; } }; /** * Handle multiple files upload * @param {string} fieldId - Field ID * @param {Array} files - Files to upload */ window.handleFiles = async function(fieldId, files) { const uploadConfig = window.getUploadConfig ? window.getUploadConfig(fieldId) : {}; const pluginId = uploadConfig.plugin_id || window.currentPluginConfig?.pluginId || 'static-image'; const maxFiles = uploadConfig.max_files || 10; const maxSizeMB = uploadConfig.max_size_mb || 5; const fileType = uploadConfig.file_type || 'image'; const customUploadEndpoint = uploadConfig.endpoint || '/api/v3/plugins/assets/upload'; // Get allowed types from config, with fallback const allowedTypes = uploadConfig.allowed_types || ['image/png', 'image/jpeg', 'image/jpg', 'image/bmp', 'image/gif']; // Get current files list const currentFiles = window.getCurrentImages ? window.getCurrentImages(fieldId) : []; // Validate file types and sizes first, build validFiles const validFiles = []; for (const file of files) { if (file.size > maxSizeMB * 1024 * 1024) { const notifyFn = window.showNotification || console.error; notifyFn(`File ${file.name} exceeds ${maxSizeMB}MB limit`, 'error'); continue; } if (fileType === 'json') { // Validate JSON files if (!file.name.toLowerCase().endsWith('.json')) { const notifyFn = window.showNotification || console.error; notifyFn(`File ${file.name} must be a JSON file (.json)`, 'error'); continue; } } else { // Validate image files using allowedTypes from config if (!allowedTypes.includes(file.type)) { const notifyFn = window.showNotification || console.error; notifyFn(`File ${file.name} is not a valid image type`, 'error'); continue; } } validFiles.push(file); } // Check max files AFTER building validFiles if (currentFiles.length + validFiles.length > maxFiles) { const notifyFn = window.showNotification || console.error; notifyFn(`Maximum ${maxFiles} files allowed. You have ${currentFiles.length} and tried to add ${validFiles.length}.`, 'error'); return; } if (validFiles.length === 0) { return; } // Show upload progress if (window.showUploadProgress) { window.showUploadProgress(fieldId, validFiles.length); } // Upload files const formData = new FormData(); if (fileType !== 'json') { formData.append('plugin_id', pluginId); } validFiles.forEach(file => formData.append('files', file)); try { const response = await fetch(customUploadEndpoint, { method: 'POST', body: formData }); const data = await response.json(); if (data.status === 'success') { // Add uploaded files to current list const currentFiles = window.getCurrentImages ? window.getCurrentImages(fieldId) : []; const newFiles = [...currentFiles, ...(data.uploaded_files || data.data?.files || [])]; if (window.updateImageList) { window.updateImageList(fieldId, newFiles); } const notifyFn = window.showNotification || console.log; notifyFn(`Successfully uploaded ${data.uploaded_files?.length || data.data?.files?.length || 0} ${fileType === 'json' ? 'file(s)' : 'image(s)'}`, 'success'); } else { const notifyFn = window.showNotification || console.error; notifyFn(`Upload failed: ${data.message}`, 'error'); } } catch (error) { console.error('Upload error:', error); const notifyFn = window.showNotification || console.error; notifyFn(`Upload error: ${error.message}`, 'error'); } finally { if (window.hideUploadProgress) { window.hideUploadProgress(fieldId); } // Clear file input const fileInput = document.getElementById(`${fieldId}_file_input`); if (fileInput) { fileInput.value = ''; } } }; /** * Delete uploaded image * @param {string} fieldId - Field ID * @param {string} imageId - Image ID * @param {string} pluginId - Plugin ID */ window.deleteUploadedImage = async function(fieldId, imageId, pluginId) { return window.deleteUploadedFile(fieldId, imageId, pluginId, 'image', null); }; /** * Delete uploaded file (generic) * @param {string} fieldId - Field ID * @param {string} fileId - File ID * @param {string} pluginId - Plugin ID * @param {string} fileType - File type ('image' or 'json') * @param {string|null} customDeleteEndpoint - Custom delete endpoint */ window.deleteUploadedFile = async function(fieldId, fileId, pluginId, fileType, customDeleteEndpoint) { const fileTypeLabel = fileType === 'json' ? 'file' : 'image'; if (!confirm(`Are you sure you want to delete this ${fileTypeLabel}?`)) { return; } try { const deleteEndpoint = customDeleteEndpoint || (fileType === 'json' ? '/api/v3/plugins/of-the-day/json/delete' : '/api/v3/plugins/assets/delete'); const requestBody = fileType === 'json' ? { file_id: fileId } : { plugin_id: pluginId, image_id: fileId }; const response = await fetch(deleteEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); const data = await response.json(); if (data.status === 'success') { // Remove from current list - normalize types for comparison const currentFiles = window.getCurrentImages ? window.getCurrentImages(fieldId) : []; const fileIdStr = String(fileId); const newFiles = currentFiles.filter(file => { const fileIdValue = String(file.id || file.category_name || ''); return fileIdValue !== fileIdStr; }); if (window.updateImageList) { window.updateImageList(fieldId, newFiles); } const notifyFn = window.showNotification || console.log; notifyFn(`${fileType === 'json' ? 'File' : 'Image'} deleted successfully`, 'success'); } else { const notifyFn = window.showNotification || console.error; notifyFn(`Delete failed: ${data.message}`, 'error'); } } catch (error) { console.error('Delete error:', error); const notifyFn = window.showNotification || console.error; notifyFn(`Delete error: ${error.message}`, 'error'); } }; /** * Get upload configuration from schema * @param {string} fieldId - Field ID * @returns {Object} Upload configuration */ window.getUploadConfig = function(fieldId) { // Extract config from schema const schema = window.currentPluginConfig?.schema; if (!schema || !schema.properties) return {}; // Find the property that matches this fieldId // FieldId is like "image_config_images" for "image_config.images" const key = fieldId.replace(/_/g, '.'); const keys = key.split('.'); let prop = schema.properties; for (const k of keys) { if (prop && prop[k]) { prop = prop[k]; if (prop.properties && prop.type === 'object') { prop = prop.properties; } else if (prop.type === 'array' && prop['x-widget'] === 'file-upload') { break; } else { break; } } } // If we found an array with x-widget, get its config if (prop && prop.type === 'array' && prop['x-widget'] === 'file-upload') { return prop['x-upload-config'] || {}; } // Try to find nested images array if (schema.properties && schema.properties.image_config && schema.properties.image_config.properties && schema.properties.image_config.properties.images) { const imagesProp = schema.properties.image_config.properties.images; if (imagesProp['x-widget'] === 'file-upload') { return imagesProp['x-upload-config'] || {}; } } return {}; }; /** * Get current images from hidden input * @param {string} fieldId - Field ID * @returns {Array} Array of image objects */ window.getCurrentImages = function(fieldId) { const hiddenInput = document.getElementById(`${fieldId}_images_data`); if (hiddenInput && hiddenInput.value) { try { return JSON.parse(hiddenInput.value); } catch (e) { console.error('Error parsing images data:', e); } } return []; }; /** * Update image list display and hidden input * Uses DOM creation to prevent XSS and preserves open schedule editors * @param {string} fieldId - Field ID * @param {Array} images - Array of image objects */ window.updateImageList = function(fieldId, images) { const hiddenInput = document.getElementById(`${fieldId}_images_data`); if (hiddenInput) { hiddenInput.value = JSON.stringify(images); } // Update the display const imageList = document.getElementById(`${fieldId}_image_list`); if (!imageList) return; const uploadConfig = window.getUploadConfig(fieldId); const pluginId = uploadConfig.plugin_id || window.currentPluginConfig?.pluginId || 'static-image'; // Detect which schedule is currently open (if any) const openScheduleId = (() => { const existingItems = imageList.querySelectorAll('[id^="img_"]'); for (const item of existingItems) { const scheduleDiv = item.querySelector('[id^="schedule_"]'); if (scheduleDiv && !scheduleDiv.classList.contains('hidden')) { // Extract the ID from schedule_ const match = scheduleDiv.id.match(/^schedule_(.+)$/); if (match) { return match[1]; } } } return null; })(); // Preserve open schedule content if it exists const preservedScheduleContent = openScheduleId ? (() => { const scheduleDiv = document.getElementById(`schedule_${openScheduleId}`); return scheduleDiv ? scheduleDiv.innerHTML : null; })() : null; // Clear and rebuild using DOM creation imageList.innerHTML = ''; images.forEach((img, idx) => { const imgId = img.id || idx; const sanitizedId = String(imgId).replace(/[^a-zA-Z0-9_-]/g, '_'); const imgSchedule = img.schedule || {}; const hasSchedule = imgSchedule.enabled && imgSchedule.mode && imgSchedule.mode !== 'always'; const scheduleSummary = hasSchedule ? (window.getScheduleSummary ? window.getScheduleSummary(imgSchedule) : 'Scheduled') : 'Always shown'; // Create container div const container = document.createElement('div'); container.id = `img_${sanitizedId}`; container.className = 'bg-gray-50 p-3 rounded-lg border border-gray-200'; // Create main content div const mainDiv = document.createElement('div'); mainDiv.className = 'flex items-center justify-between mb-2'; // Create left section with image and info const leftSection = document.createElement('div'); leftSection.className = 'flex items-center space-x-3 flex-1'; // Create image element const imgEl = document.createElement('img'); const imgPath = String(img.path || '').replace(/^\/+/, ''); imgEl.src = '/' + imgPath; imgEl.alt = String(img.filename || ''); imgEl.className = 'w-16 h-16 object-cover rounded'; imgEl.addEventListener('error', function() { this.style.display = 'none'; if (this.nextElementSibling) { this.nextElementSibling.style.display = 'block'; } }); // Create placeholder div for broken images const placeholderDiv = document.createElement('div'); placeholderDiv.style.display = 'none'; placeholderDiv.className = 'w-16 h-16 bg-gray-200 rounded flex items-center justify-center'; const placeholderIcon = document.createElement('i'); placeholderIcon.className = 'fas fa-image text-gray-400'; placeholderDiv.appendChild(placeholderIcon); // Create info div const infoDiv = document.createElement('div'); infoDiv.className = 'flex-1 min-w-0'; // Filename const filenameP = document.createElement('p'); filenameP.className = 'text-sm font-medium text-gray-900 truncate'; filenameP.textContent = img.original_filename || img.filename || 'Image'; // Size and date const sizeDateP = document.createElement('p'); sizeDateP.className = 'text-xs text-gray-500'; const fileSize = window.formatFileSize ? window.formatFileSize(img.size || 0) : (Math.round((img.size || 0) / 1024) + ' KB'); const uploadedDate = window.formatDate ? window.formatDate(img.uploaded_at) : (img.uploaded_at || ''); sizeDateP.textContent = `${fileSize} • ${uploadedDate}`; // Schedule summary const scheduleP = document.createElement('p'); scheduleP.className = 'text-xs text-blue-600 mt-1'; const clockIcon = document.createElement('i'); clockIcon.className = 'fas fa-clock mr-1'; scheduleP.appendChild(clockIcon); scheduleP.appendChild(document.createTextNode(scheduleSummary)); infoDiv.appendChild(filenameP); infoDiv.appendChild(sizeDateP); infoDiv.appendChild(scheduleP); leftSection.appendChild(imgEl); leftSection.appendChild(placeholderDiv); leftSection.appendChild(infoDiv); // Create right section with buttons const rightSection = document.createElement('div'); rightSection.className = 'flex items-center space-x-2 ml-4'; // Schedule button const scheduleBtn = document.createElement('button'); scheduleBtn.type = 'button'; scheduleBtn.className = 'text-blue-600 hover:text-blue-800 p-2'; scheduleBtn.title = 'Schedule this image'; scheduleBtn.dataset.fieldId = fieldId; scheduleBtn.dataset.imageId = String(imgId); scheduleBtn.dataset.imageIdx = String(idx); scheduleBtn.addEventListener('click', function() { window.openImageSchedule(this.dataset.fieldId, this.dataset.imageId, parseInt(this.dataset.imageIdx, 10)); }); const scheduleIcon = document.createElement('i'); scheduleIcon.className = 'fas fa-calendar-alt'; scheduleBtn.appendChild(scheduleIcon); // Delete button const deleteBtn = document.createElement('button'); deleteBtn.type = 'button'; deleteBtn.className = 'text-red-600 hover:text-red-800 p-2'; deleteBtn.title = 'Delete image'; deleteBtn.dataset.fieldId = fieldId; deleteBtn.dataset.imageId = String(imgId); deleteBtn.dataset.pluginId = pluginId; deleteBtn.addEventListener('click', function() { window.deleteUploadedImage(this.dataset.fieldId, this.dataset.imageId, this.dataset.pluginId); }); const deleteIcon = document.createElement('i'); deleteIcon.className = 'fas fa-trash'; deleteBtn.appendChild(deleteIcon); rightSection.appendChild(scheduleBtn); rightSection.appendChild(deleteBtn); mainDiv.appendChild(leftSection); mainDiv.appendChild(rightSection); // Create schedule container const scheduleContainer = document.createElement('div'); scheduleContainer.id = `schedule_${sanitizedId}`; scheduleContainer.className = 'hidden mt-3 pt-3 border-t border-gray-300'; // Restore preserved schedule content if this is the open one if (openScheduleId === sanitizedId && preservedScheduleContent) { scheduleContainer.innerHTML = preservedScheduleContent; scheduleContainer.classList.remove('hidden'); } container.appendChild(mainDiv); container.appendChild(scheduleContainer); imageList.appendChild(container); }); }; /** * Show upload progress * @param {string} fieldId - Field ID * @param {number} totalFiles - Total number of files */ window.showUploadProgress = function(fieldId, totalFiles) { const dropZone = document.getElementById(`${fieldId}_drop_zone`); if (dropZone) { dropZone.innerHTML = `

Uploading ${totalFiles} file(s)...

`; dropZone.style.pointerEvents = 'none'; } }; /** * Hide upload progress and restore drop zone * @param {string} fieldId - Field ID */ window.hideUploadProgress = function(fieldId) { const uploadConfig = window.getUploadConfig(fieldId); const maxFiles = uploadConfig.max_files || 10; const maxSizeMB = uploadConfig.max_size_mb || 5; const allowedTypes = uploadConfig.allowed_types || ['image/png', 'image/jpeg', 'image/bmp', 'image/gif']; // Generate user-friendly extension list from allowedTypes const extensionMap = { 'image/png': 'PNG', 'image/jpeg': 'JPG', 'image/jpg': 'JPG', 'image/bmp': 'BMP', 'image/gif': 'GIF', 'image/webp': 'WEBP' }; const extensions = allowedTypes .map(type => extensionMap[type] || type.split('/')[1]?.toUpperCase() || type) .filter((ext, idx, arr) => arr.indexOf(ext) === idx) // Remove duplicates .join(', '); const extensionText = extensions || 'PNG, JPG, GIF, BMP'; const dropZone = document.getElementById(`${fieldId}_drop_zone`); if (dropZone) { dropZone.innerHTML = `

Drag and drop images here or click to browse

Max ${maxFiles} files, ${maxSizeMB}MB each (${extensionText})

`; dropZone.style.pointerEvents = 'auto'; } }; /** * Format file size * @param {number} bytes - File size in bytes * @returns {string} Formatted file size */ window.formatFileSize = function(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; }; /** * Format date string * @param {string} dateString - Date string * @returns {string} Formatted date */ window.formatDate = function(dateString) { if (!dateString) return 'Unknown date'; try { const date = new Date(dateString); return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } catch (e) { return dateString; } }; /** * Get schedule summary text * @param {Object} schedule - Schedule object * @returns {string} Schedule summary */ window.getScheduleSummary = function(schedule) { if (!schedule || !schedule.enabled || schedule.mode === 'always') { return 'Always shown'; } if (schedule.mode === 'time_range') { return `${schedule.start_time || '08:00'} - ${schedule.end_time || '18:00'} (daily)`; } if (schedule.mode === 'per_day' && schedule.days) { const enabledDays = Object.entries(schedule.days) .filter(([day, config]) => config && config.enabled) .map(([day]) => day.charAt(0).toUpperCase() + day.slice(1, 3)); if (enabledDays.length === 0) { return 'Never shown'; } return enabledDays.join(', ') + ' only'; } return 'Scheduled'; }; /** * Open image schedule editor * @param {string} fieldId - Field ID * @param {string|number} imageId - Image ID * @param {number} imageIdx - Image index */ window.openImageSchedule = function(fieldId, imageId, imageIdx) { const currentImages = window.getCurrentImages(fieldId); const image = currentImages[imageIdx]; if (!image) return; // Sanitize imageId to match updateImageList's sanitization const sanitizedId = (imageId || imageIdx).toString().replace(/[^a-zA-Z0-9_-]/g, '_'); const scheduleContainer = document.getElementById(`schedule_${sanitizedId}`); if (!scheduleContainer) return; // Toggle visibility const isVisible = !scheduleContainer.classList.contains('hidden'); if (isVisible) { scheduleContainer.classList.add('hidden'); return; } scheduleContainer.classList.remove('hidden'); const schedule = image.schedule || { enabled: false, mode: 'always', start_time: '08:00', end_time: '18:00', days: {} }; // Escape HTML helper const escapeHtml = (text) => { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }; // Use sanitizedId for all ID references in the schedule HTML // Use data attributes instead of inline handlers to prevent JS injection scheduleContainer.innerHTML = `

Schedule Settings

When enabled, this image will only display during scheduled times

${['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'].map(day => { const dayConfig = (schedule.days && schedule.days[day]) || { enabled: true, start_time: '08:00', end_time: '18:00' }; return `
`; }).join('')}
`; // Attach event listeners using data attributes (prevents JS injection) const enabledCheckbox = document.getElementById(`schedule_enabled_${sanitizedId}`); if (enabledCheckbox) { enabledCheckbox.addEventListener('change', function() { const fieldId = this.dataset.fieldId; const imageId = this.dataset.imageId; const imageIdx = parseInt(this.dataset.imageIdx, 10); window.toggleImageScheduleEnabled(fieldId, imageId, imageIdx); }); } const modeSelect = document.getElementById(`schedule_mode_${sanitizedId}`); if (modeSelect) { modeSelect.addEventListener('change', function() { const fieldId = this.dataset.fieldId; const imageId = this.dataset.imageId; const imageIdx = parseInt(this.dataset.imageIdx, 10); window.updateImageScheduleMode(fieldId, imageId, imageIdx); }); } const startInput = document.getElementById(`schedule_start_${sanitizedId}`); if (startInput) { startInput.addEventListener('change', function() { const fieldId = this.dataset.fieldId; const imageId = this.dataset.imageId; const imageIdx = parseInt(this.dataset.imageIdx, 10); window.updateImageScheduleTime(fieldId, imageId, imageIdx); }); } const endInput = document.getElementById(`schedule_end_${sanitizedId}`); if (endInput) { endInput.addEventListener('change', function() { const fieldId = this.dataset.fieldId; const imageId = this.dataset.imageId; const imageIdx = parseInt(this.dataset.imageIdx, 10); window.updateImageScheduleTime(fieldId, imageId, imageIdx); }); } // Attach listeners for per-day inputs ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'].forEach(day => { const dayCheckbox = document.getElementById(`day_${day}_${sanitizedId}`); if (dayCheckbox) { dayCheckbox.addEventListener('change', function() { const fieldId = this.dataset.fieldId; const imageId = this.dataset.imageId; const imageIdx = parseInt(this.dataset.imageIdx, 10); const day = this.dataset.day; window.updateImageScheduleDay(fieldId, imageId, imageIdx, day); }); } const dayStartInput = document.getElementById(`day_${day}_start_${sanitizedId}`); if (dayStartInput) { dayStartInput.addEventListener('change', function() { const fieldId = this.dataset.fieldId; const imageId = this.dataset.imageId; const imageIdx = parseInt(this.dataset.imageIdx, 10); const day = this.dataset.day; window.updateImageScheduleDay(fieldId, imageId, imageIdx, day); }); } const dayEndInput = document.getElementById(`day_${day}_end_${sanitizedId}`); if (dayEndInput) { dayEndInput.addEventListener('change', function() { const fieldId = this.dataset.fieldId; const imageId = this.dataset.imageId; const imageIdx = parseInt(this.dataset.imageIdx, 10); const day = this.dataset.day; window.updateImageScheduleDay(fieldId, imageId, imageIdx, day); }); } }); }; /** * Toggle image schedule enabled state */ window.toggleImageScheduleEnabled = function(fieldId, imageId, imageIdx) { const currentImages = window.getCurrentImages(fieldId); const image = currentImages[imageIdx]; if (!image) return; // Sanitize imageId for DOM lookup const sanitizedId = String(imageId).replace(/[^a-zA-Z0-9_-]/g, '_'); const checkbox = document.getElementById(`schedule_enabled_${sanitizedId}`); const enabled = checkbox ? checkbox.checked : false; if (!image.schedule) { image.schedule = { enabled: false, mode: 'always', start_time: '08:00', end_time: '18:00', days: {} }; } image.schedule.enabled = enabled; const optionsDiv = document.getElementById(`schedule_options_${sanitizedId}`); if (optionsDiv) { optionsDiv.style.display = enabled ? 'block' : 'none'; } if (window.updateImageList) { window.updateImageList(fieldId, currentImages); } }; /** * Update image schedule mode */ window.updateImageScheduleMode = function(fieldId, imageId, imageIdx) { const currentImages = window.getCurrentImages(fieldId); const image = currentImages[imageIdx]; if (!image) return; // Sanitize imageId for DOM lookup const sanitizedId = String(imageId).replace(/[^a-zA-Z0-9_-]/g, '_'); if (!image.schedule) { image.schedule = { enabled: true, mode: 'always', start_time: '08:00', end_time: '18:00', days: {} }; } const modeSelect = document.getElementById(`schedule_mode_${sanitizedId}`); const mode = modeSelect ? modeSelect.value : 'always'; image.schedule.mode = mode; const timeRangeDiv = document.getElementById(`time_range_${sanitizedId}`); const perDayDiv = document.getElementById(`per_day_${sanitizedId}`); if (timeRangeDiv) timeRangeDiv.style.display = mode === 'time_range' ? 'grid' : 'none'; if (perDayDiv) perDayDiv.style.display = mode === 'per_day' ? 'block' : 'none'; if (window.updateImageList) { window.updateImageList(fieldId, currentImages); } }; /** * Update image schedule time */ window.updateImageScheduleTime = function(fieldId, imageId, imageIdx) { const currentImages = window.getCurrentImages(fieldId); const image = currentImages[imageIdx]; if (!image) return; // Sanitize imageId for DOM lookup const sanitizedId = String(imageId).replace(/[^a-zA-Z0-9_-]/g, '_'); if (!image.schedule) { image.schedule = { enabled: true, mode: 'time_range', start_time: '08:00', end_time: '18:00' }; } const startInput = document.getElementById(`schedule_start_${sanitizedId}`); const endInput = document.getElementById(`schedule_end_${sanitizedId}`); if (startInput) image.schedule.start_time = startInput.value || '08:00'; if (endInput) image.schedule.end_time = endInput.value || '18:00'; if (window.updateImageList) { window.updateImageList(fieldId, currentImages); } }; /** * Update image schedule day */ window.updateImageScheduleDay = function(fieldId, imageId, imageIdx, day) { const currentImages = window.getCurrentImages(fieldId); const image = currentImages[imageIdx]; if (!image) return; // Sanitize imageId for DOM lookup const sanitizedId = String(imageId).replace(/[^a-zA-Z0-9_-]/g, '_'); if (!image.schedule) { image.schedule = { enabled: true, mode: 'per_day', days: {} }; } if (!image.schedule.days) { image.schedule.days = {}; } const checkbox = document.getElementById(`day_${day}_${sanitizedId}`); const startInput = document.getElementById(`day_${day}_start_${sanitizedId}`); const endInput = document.getElementById(`day_${day}_end_${sanitizedId}`); const enabled = checkbox ? checkbox.checked : true; if (!image.schedule.days[day]) { image.schedule.days[day] = { enabled: true, start_time: '08:00', end_time: '18:00' }; } image.schedule.days[day].enabled = enabled; if (startInput) image.schedule.days[day].start_time = startInput.value || '08:00'; if (endInput) image.schedule.days[day].end_time = endInput.value || '18:00'; const dayTimesDiv = document.getElementById(`day_times_${day}_${sanitizedId}`); if (dayTimesDiv) { dayTimesDiv.style.display = enabled ? 'grid' : 'none'; } if (startInput) startInput.disabled = !enabled; if (endInput) endInput.disabled = !enabled; if (window.updateImageList) { window.updateImageList(fieldId, currentImages); } }; console.log('[FileUploadWidget] File upload widget registered'); })();