initiales bauen mit mistral
This commit is contained in:
1 parent
7e2e8bdf59
commit
c74bca1e13
22 files changed
+5642
No files matched your search
@@ -0,0 +1,432 @@
|
||||
/** =============================================================================
|
||||
* 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
|
||||
};
|
||||
Reference in new issue
Block a user