433 lines
14 KiB
JavaScript
433 lines
14 KiB
JavaScript
/** =============================================================================
|
|
* Formeleon - Multi-tenant Form Generator
|
|
* Author: Björn Runden <br@runden.net>
|
|
* 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 = `
|
|
<div class="form-field border p-3 mb-3 rounded">
|
|
<div class="form-field-header d-flex justify-content-between mb-2">
|
|
<h5>Field ${fieldCount}</h5>
|
|
<div class="form-field-actions">
|
|
<button type="button" class="btn btn-sm btn-outline-danger remove-field">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="row g-3">
|
|
<div class="col-md-3">
|
|
<label class="form-label">Field Name</label>
|
|
<input type="text" class="form-control" name="fields[${fieldCount}][name]" placeholder="field_name">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Type</label>
|
|
<select class="form-select field-type-select" name="fields[${fieldCount}][type]">
|
|
<option value="text">Text</option>
|
|
<option value="email">Email</option>
|
|
<option value="textarea">Textarea</option>
|
|
<option value="select">Select</option>
|
|
<option value="checkbox">Checkbox</option>
|
|
<option value="radio">Radio</option>
|
|
<option value="number">Number</option>
|
|
<option value="date">Date</option>
|
|
<option value="file">File</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Label</label>
|
|
<input type="text" class="form-control" name="fields[${fieldCount}][label]" placeholder="Field Label">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="form-check form-switch mt-4">
|
|
<input class="form-check-input" type="checkbox" name="fields[${fieldCount}][required]" value="true">
|
|
<label class="form-check-label">Required</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row mt-2">
|
|
<div class="col-12">
|
|
<label class="form-label">Options (for select/radio/checkbox)</label>
|
|
<input type="text" class="form-control" name="fields[${fieldCount}][options]" placeholder="option1,option2,option3">
|
|
<div class="form-text">Comma-separated list of options</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
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 = `
|
|
<div class="field-options row mt-2">
|
|
<div class="col-12">
|
|
<label class="form-label">Options</label>
|
|
<textarea class="form-control" name="fields[][options]" placeholder="option1\noption2\noption3"></textarea>
|
|
<div class="form-text">One option per line</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
fieldDiv.append(optionsHtml);
|
|
break;
|
|
|
|
case 'textarea':
|
|
const textareaHtml = `
|
|
<div class="field-options row mt-2">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Rows</label>
|
|
<input type="number" class="form-control" name="fields[][rows]" value="3">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Placeholder</label>
|
|
<input type="text" class="form-control" name="fields[][placeholder]" placeholder="Enter text here...">
|
|
</div>
|
|
</div>
|
|
`;
|
|
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('<span class="spinner-border spinner-border-sm ms-2" role="status" aria-hidden="true"></span>');
|
|
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 = `
|
|
<div id="${toastId}" class="toast align-items-center text-white bg-${type} border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
|
<div class="d-flex">
|
|
<div class="toast-body">${message}</div>
|
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Create toast container if it doesn't exist
|
|
let container = $('.toast-container');
|
|
if (container.length === 0) {
|
|
container = $('<div class="toast-container position-fixed bottom-0 end-0 p-3"></div>');
|
|
$('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
|
|
};
|