fix: DockerSecretsResolver - don't normalize absolute paths like /var/www/html/...
Some checks failed
Deploy Application / deploy (push) Has been cancelled
Some checks failed
Deploy Application / deploy (push) Has been cancelled
This commit is contained in:
313
resources/js/admin-forms.js
Normal file
313
resources/js/admin-forms.js
Normal file
@@ -0,0 +1,313 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user