/** ============================================================================= * Formeleon - Multi-tenant Form Generator * Author: Björn Runden * Script: main.js * Description: Main JavaScript for Formeleon admin interface. * Handles common functionality, AJAX requests, and UI interactions. * ============================================================================= */ // Wait for DOM to be ready $(document).ready(function() { // Initialize tooltips $('[data-bs-toggle="tooltip"]').tooltip(); // Initialize popovers $('[data-bs-toggle="popover"]').popover(); // Handle auto-dismiss alerts setTimeout(function() { $('.alert').alert('close'); }, 5000); // Form field management (for form builder) initFormBuilder(); // JSON editor initialization initJsonEditor(); // Search functionality initSearch(); // Confirmation for delete actions initDeleteConfirmation(); }); /** * Initialize form builder functionality */ function initFormBuilder() { // Add field button $(document).on('click', '.add-field', function(e) { e.preventDefault(); const container = $(this).data('container'); addFieldToContainer(container); }); // Remove field button $(document).on('click', '.remove-field', function(e) { e.preventDefault(); $(this).closest('.form-field').remove(); }); // Field type change handler $(document).on('change', '.field-type-select', function() { const fieldDiv = $(this).closest('.form-field'); const fieldType = $(this).val(); updateFieldOptions(fieldDiv, fieldType); }); } /** * Add a new field to the form builder * @param {string} container - CSS selector for the container */ function addFieldToContainer(container) { const containerEl = $(container); const fieldCount = containerEl.find('.form-field').length + 1; const fieldHtml = `
Field ${fieldCount}
Comma-separated list of options
`; containerEl.append(fieldHtml); } /** * Update field options based on field type * @param {jQuery} fieldDiv - The field div element * @param {string} fieldType - The selected field type */ function updateFieldOptions(fieldDiv, fieldType) { const optionsGroup = fieldDiv.find('.field-options'); // Remove existing options optionsGroup.remove(); // Add type-specific options switch(fieldType) { case 'select': case 'radio': case 'checkbox': const optionsHtml = `
One option per line
`; fieldDiv.append(optionsHtml); break; case 'textarea': const textareaHtml = `
`; fieldDiv.append(textareaHtml); break; default: // No additional options for basic field types break; } } /** * Initialize JSON editor for form structure */ function initJsonEditor() { // Pretty print JSON in textarea $('.json-editor').each(function() { const textarea = $(this); try { const json = JSON.parse(textarea.val()); textarea.val(JSON.stringify(json, null, 2)); } catch (e) { // Not valid JSON, leave as is } }); // Format JSON button $(document).on('click', '.format-json', function(e) { e.preventDefault(); const target = $(this).data('target'); const textarea = $(target); try { const json = JSON.parse(textarea.val()); textarea.val(JSON.stringify(json, null, 2)); } catch (e) { alert('Invalid JSON: ' + e.message); } }); // Minify JSON button $(document).on('click', '.minify-json', function(e) { e.preventDefault(); const target = $(this).data('target'); const textarea = $(target); try { const json = JSON.parse(textarea.val()); textarea.val(JSON.stringify(json)); } catch (e) { alert('Invalid JSON: ' + e.message); } }); } /** * Initialize search functionality */ function initSearch() { // Table search $('#searchInput').on('input', function() { const searchTerm = $(this).val().toLowerCase(); const table = $(this).closest('.card').find('table tbody tr'); table.each(function() { const text = $(this).text().toLowerCase(); const visible = text.includes(searchTerm); $(this).toggle(visible); }); }); } /** * Initialize delete confirmation dialogs */ function initDeleteConfirmation() { // Already handled inline with onsubmit="return confirm(...)" // This is a fallback for more complex confirmations $(document).on('click', '.confirm-delete', function(e) { e.preventDefault(); const message = $(this).data('confirm-message') || 'Are you sure you want to delete this item?'; const form = $(this).closest('form'); if (confirm(message)) { form.submit(); } }); } /** * AJAX helper function * @param {string} url - URL to request * @param {string} method - HTTP method (GET, POST, PUT, DELETE) * @param {object} data - Data to send * @param {function} success - Success callback * @param {function} error - Error callback */ function ajaxRequest(url, method, data, success, error) { $.ajax({ url: url, method: method, data: data, contentType: 'application/json', dataType: 'json', headers: { 'X-CSRFToken': getCsrfToken() }, success: function(response) { if (typeof success === 'function') { success(response); } }, error: function(xhr, status, err) { if (typeof error === 'function') { error(xhr, status, err); } else { alert('Error: ' + (xhr.responseJSON?.message || err)); } } }); } /** * Get CSRF token from meta tag or cookie * @return {string} CSRF token */ function getCsrfToken() { return $('meta[name="csrf-token"]').attr('content') || getCookie('csrftoken') || getCookie('csrf_token'); } /** * Get cookie by name * @param {string} name - Cookie name * @return {string} Cookie value or null */ function getCookie(name) { let cookieValue = null; if (document.cookie && document.cookie !== '') { const cookies = document.cookie.split(';'); for (let i = 0; i < cookies.length; i++) { const cookie = cookies[i].trim(); if (cookie.substring(0, name.length + 1) === (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break; } } } return cookieValue; } /** * Show loading spinner * @param {string|jQuery} element - Element or selector to add spinner to */ function showLoading(element) { const el = $(element); el.append(''); el.attr('disabled', true); } /** * Hide loading spinner * @param {string|jQuery} element - Element or selector to remove spinner from */ function hideLoading(element) { const el = $(element); el.find('.spinner-border').remove(); el.removeAttr('disabled'); } /** * Format date for display * @param {string} dateString - ISO date string * @return {string} Formatted date */ function formatDate(dateString) { if (!dateString) return 'N/A'; const date = new Date(dateString); return date.toLocaleString(); } /** * Format bytes for display * @param {number} bytes - Number of bytes * @return {string} Formatted size */ function formatBytes(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } /** * Generate a random ID * @param {number} length - Length of ID (default: 8) * @return {string} Random ID */ function generateId(length = 8) { return Math.random().toString(36).substring(2, 2 + length); } /** * Copy text to clipboard * @param {string} text - Text to copy * @param {function} callback - Optional callback */ function copyToClipboard(text, callback) { navigator.clipboard.writeText(text).then(function() { if (typeof callback === 'function') { callback(true); } }).catch(function(err) { if (typeof callback === 'function') { callback(false, err); } }); } /** * Show toast notification * @param {string} message - Message to display * @param {string} type - Type (success, error, warning, info) */ function showToast(message, type = 'info') { const toastId = 'toast-' + Date.now(); const toastHtml = ` `; // Create toast container if it doesn't exist let container = $('.toast-container'); if (container.length === 0) { container = $('
'); $('body').append(container); } container.append(toastHtml); const toastElement = $('#' + toastId); const toast = new bootstrap.Toast(toastElement[0], { autohide: true, delay: 3000 }); toast.show(); // Remove toast after it's hidden toastElement.on('hidden.bs.toast', function() { toastElement.remove(); }); } // Export functions for use in other scripts window.Formeleon = { ajaxRequest: ajaxRequest, showLoading: showLoading, hideLoading: hideLoading, formatDate: formatDate, formatBytes: formatBytes, generateId: generateId, copyToClipboard: copyToClipboard, showToast: showToast, getCsrfToken: getCsrfToken };