Files
formeleon/static/js/main.js
T

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
};