Some checks failed
Deploy Application / deploy (push) Has been cancelled
314 lines
10 KiB
JavaScript
314 lines
10 KiB
JavaScript
/**
|
|
* 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();
|
|
});
|
|
})();
|
|
|