/** * Admin Forms JavaScript Enhancements * * Provides: * - Drag & drop handlers for upload zones * - File preview generation (images) * - Auto-resize textareas (JSON fields) * - Keyboard shortcuts (Ctrl+S for save) * - Toast notification system * - Form auto-save (debounced) */ (function() { 'use strict'; /** * Initialize drag & drop for upload zones */ function initDragAndDrop() { const uploadZones = document.querySelectorAll('[data-live-dropzone]'); uploadZones.forEach(zone => { const fileInput = zone.querySelector('input[type="file"]'); if (!fileInput) return; // Prevent default drag behaviors ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { zone.addEventListener(eventName, preventDefaults, false); document.body.addEventListener(eventName, preventDefaults, false); }); // Highlight drop zone when item is dragged over it ['dragenter', 'dragover'].forEach(eventName => { zone.addEventListener(eventName, () => { zone.classList.add('admin-upload-zone--dragover'); }, false); }); ['dragleave', 'drop'].forEach(eventName => { zone.addEventListener(eventName, () => { zone.classList.remove('admin-upload-zone--dragover'); }, false); }); // Handle dropped files zone.addEventListener('drop', (e) => { const dt = e.dataTransfer; const files = dt.files; if (files.length > 0) { fileInput.files = files; // Trigger change event to notify LiveComponent fileInput.dispatchEvent(new Event('change', { bubbles: true })); } }, false); // Click on zone to trigger file input zone.addEventListener('click', () => { if (fileInput) { fileInput.click(); } }, false); }); } function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); } /** * Generate file preview for images */ function initFilePreview() { const fileInputs = document.querySelectorAll('input[type="file"][data-live-upload]'); fileInputs.forEach(input => { input.addEventListener('change', (e) => { const file = e.target.files[0]; if (!file || !file.type.startsWith('image/')) return; const reader = new FileReader(); reader.onload = (event) => { // Find preview container const component = input.closest('[data-live-component]'); if (!component) return; const previewContainer = component.querySelector('.admin-file-preview'); if (!previewContainer) return; const previewImage = previewContainer.querySelector('.admin-file-preview__image'); if (previewImage) { previewImage.src = event.target.result; } else { // Create preview image if it doesn't exist const img = document.createElement('img'); img.src = event.target.result; img.className = 'admin-file-preview__image'; img.alt = 'Preview'; previewContainer.insertBefore(img, previewContainer.firstChild); } }; reader.readAsDataURL(file); }); }); } /** * Auto-resize textareas (especially for JSON fields) */ function initAutoResizeTextareas() { const textareas = document.querySelectorAll('.admin-textarea--code'); textareas.forEach(textarea => { // Set initial height textarea.style.height = 'auto'; textarea.style.height = textarea.scrollHeight + 'px'; // Auto-resize on input textarea.addEventListener('input', () => { textarea.style.height = 'auto'; textarea.style.height = textarea.scrollHeight + 'px'; }); // Auto-resize on paste textarea.addEventListener('paste', () => { setTimeout(() => { textarea.style.height = 'auto'; textarea.style.height = textarea.scrollHeight + 'px'; }, 10); }); }); } /** * Keyboard shortcuts */ function initKeyboardShortcuts() { document.addEventListener('keydown', (e) => { // Ctrl+S or Cmd+S: Save draft if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); const saveButton = document.querySelector('[data-live-action="autoSave"]'); if (saveButton) { saveButton.click(); } } // Escape: Cancel/Close if (e.key === 'Escape') { const cancelButton = document.querySelector('a[href*="/admin/"]'); if (cancelButton && cancelButton.textContent.trim().toLowerCase() === 'cancel') { cancelButton.click(); } } }); } /** * Toast notification system */ const Toast = { show(message, type = 'info', duration = 5000) { const toast = document.createElement('div'); toast.className = `admin-toast admin-toast--${type}`; const messageEl = document.createElement('p'); messageEl.className = 'admin-toast__message'; messageEl.textContent = message; toast.appendChild(messageEl); const closeBtn = document.createElement('button'); closeBtn.className = 'admin-toast__close'; closeBtn.innerHTML = '×'; closeBtn.setAttribute('aria-label', 'Close'); closeBtn.addEventListener('click', () => this.hide(toast)); toast.appendChild(closeBtn); document.body.appendChild(toast); // Auto-hide after duration if (duration > 0) { setTimeout(() => this.hide(toast), duration); } return toast; }, hide(toast) { if (toast && toast.parentNode) { toast.style.animation = 'admin-toast-slide-out 0.3s ease-out'; setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }, 300); } }, success(message, duration = 5000) { return this.show(message, 'success', duration); }, error(message, duration = 7000) { return this.show(message, 'error', duration); }, info(message, duration = 5000) { return this.show(message, 'info', duration); }, warning(message, duration = 6000) { return this.show(message, 'warning', duration); } }; // Listen for LiveComponent events and show toasts document.addEventListener('livecomponent:upload:success', (e) => { const detail = e.detail || {}; Toast.success(detail.message || 'File uploaded successfully!'); // Optional redirect after upload if (detail.redirect_url) { setTimeout(() => { window.location.href = detail.redirect_url; }, 1500); } }); document.addEventListener('livecomponent:upload:error', (e) => { const error = e.detail?.error || 'Upload failed'; Toast.error(error); }); document.addEventListener('livecomponent:draft:saved', (e) => { Toast.info('Draft saved'); }); // Handle form submission with redirect document.addEventListener('livecomponent:form:submitted', (e) => { const detail = e.detail || {}; if (detail.success && detail.redirect_url) { Toast.success(detail.message || 'Content created successfully!'); // Redirect after a short delay to show the toast setTimeout(() => { window.location.href = detail.redirect_url; }, 1000); } else { Toast.error(detail.message || 'Failed to submit form'); } }); /** * Form auto-save (debounced) */ function initAutoSave() { const forms = document.querySelectorAll('[data-live-component]'); forms.forEach(form => { const inputs = form.querySelectorAll('input, textarea, select'); let autoSaveTimer = null; const AUTO_SAVE_DELAY = 30000; // 30 seconds inputs.forEach(input => { input.addEventListener('input', () => { // Clear existing timer if (autoSaveTimer) { clearTimeout(autoSaveTimer); } // Set new timer autoSaveTimer = setTimeout(() => { const saveButton = form.querySelector('[data-live-action="autoSave"]'); if (saveButton) { saveButton.click(); } }, AUTO_SAVE_DELAY); }); }); }); } /** * Initialize all enhancements when DOM is ready */ function init() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); return; } initDragAndDrop(); initFilePreview(); initAutoResizeTextareas(); initKeyboardShortcuts(); initAutoSave(); // Expose Toast globally for use in other scripts window.AdminToast = Toast; } // Initialize immediately if DOM is already loaded init(); // Also initialize when LiveComponents are loaded dynamically document.addEventListener('livecomponent:loaded', () => { initDragAndDrop(); initFilePreview(); initAutoResizeTextareas(); }); })();