Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
753 lines
24 KiB
JavaScript
753 lines
24 KiB
JavaScript
import { Logger } from '../../core/logger.js';
|
|
import { FormValidator } from './FormValidator.js';
|
|
import { FormState } from './FormState.js';
|
|
|
|
export class FormHandler {
|
|
constructor(form, options = {}) {
|
|
this.form = form;
|
|
this.options = {
|
|
validateOnSubmit: true,
|
|
validateOnBlur: false,
|
|
validateOnInput: false,
|
|
showInlineErrors: true,
|
|
preventSubmitOnError: true,
|
|
submitMethod: 'POST',
|
|
ajaxSubmit: true,
|
|
// Autosave options
|
|
enableAutosave: options.enableAutosave ?? form.hasAttribute('data-autosave'),
|
|
autosaveInterval: options.autosaveInterval || 30000, // 30 seconds
|
|
autosaveRetentionPeriod: options.autosaveRetentionPeriod || 24 * 60 * 60 * 1000, // 24 hours
|
|
autosaveStorageKey: options.autosaveStorageKey || null,
|
|
autosaveStoragePrefix: options.autosaveStoragePrefix || 'form_draft_',
|
|
autosaveVisualFeedback: options.autosaveVisualFeedback ?? true,
|
|
autosaveExcludeFields: options.autosaveExcludeFields || [
|
|
'input[type="password"]',
|
|
'input[type="hidden"][name="_token"]',
|
|
'input[type="hidden"][name="_form_id"]',
|
|
'input[name*="password"]',
|
|
'input[name*="confirm"]',
|
|
'input[name*="honeypot"]'
|
|
],
|
|
autosaveImmediateFields: options.autosaveImmediateFields || [
|
|
'textarea',
|
|
'input[type="email"]',
|
|
'input[type="text"]',
|
|
'select'
|
|
],
|
|
...options
|
|
};
|
|
|
|
this.validator = FormValidator.create(form);
|
|
this.state = FormState.create(form);
|
|
this.isSubmitting = false;
|
|
|
|
// Autosave state
|
|
this.autosaveTimer = null;
|
|
this.lastAutosaveTime = null;
|
|
this.autosaveStorageKey = null;
|
|
|
|
this.init();
|
|
}
|
|
|
|
static create(form, options = {}) {
|
|
return new FormHandler(form, options);
|
|
}
|
|
|
|
init() {
|
|
this.bindEvents();
|
|
this.setupErrorDisplay();
|
|
|
|
// Initialize autosave if enabled
|
|
if (this.options.enableAutosave) {
|
|
this.initAutosave();
|
|
}
|
|
|
|
// Mark form as enhanced
|
|
this.form.setAttribute('data-enhanced', 'true');
|
|
|
|
Logger.info(`[FormHandler] Initialized for form: ${this.form.id || 'unnamed'}`);
|
|
}
|
|
|
|
bindEvents() {
|
|
// Submit event
|
|
this.form.addEventListener('submit', (e) => this.handleSubmit(e));
|
|
|
|
// Field validation events
|
|
if (this.options.validateOnBlur || this.options.validateOnInput) {
|
|
const fields = this.form.querySelectorAll('input, textarea, select');
|
|
|
|
fields.forEach(field => {
|
|
if (this.options.validateOnBlur) {
|
|
field.addEventListener('blur', () => this.validateSingleField(field));
|
|
}
|
|
|
|
if (this.options.validateOnInput) {
|
|
field.addEventListener('input', () => this.validateSingleField(field));
|
|
}
|
|
});
|
|
}
|
|
|
|
// Autosave events
|
|
if (this.options.enableAutosave) {
|
|
this.form.addEventListener('input', (e) => this.onAutosaveFieldChange(e));
|
|
this.form.addEventListener('change', (e) => this.onAutosaveFieldChange(e));
|
|
|
|
// Save on page unload
|
|
window.addEventListener('beforeunload', () => {
|
|
if (this.state.isDirty()) {
|
|
this.saveAutosaveDraft();
|
|
}
|
|
});
|
|
|
|
// Handle page visibility changes
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.hidden && this.state.isDirty()) {
|
|
this.saveAutosaveDraft();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
async handleSubmit(event) {
|
|
if (this.isSubmitting) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
// Validate if enabled
|
|
if (this.options.validateOnSubmit) {
|
|
const isValid = this.validator.validate();
|
|
|
|
if (!isValid) {
|
|
event.preventDefault();
|
|
this.displayErrors();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// AJAX submission
|
|
if (this.options.ajaxSubmit) {
|
|
event.preventDefault();
|
|
await this.submitViaAjax();
|
|
}
|
|
|
|
// If not AJAX, let the form submit naturally
|
|
}
|
|
|
|
async submitViaAjax() {
|
|
try {
|
|
this.setSubmitState(true);
|
|
this.clearErrors();
|
|
|
|
const formData = new FormData(this.form);
|
|
const url = this.form.action || window.location.href;
|
|
const method = this.form.method || this.options.submitMethod;
|
|
|
|
const response = await fetch(url, {
|
|
method: method.toUpperCase(),
|
|
body: formData,
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
}
|
|
});
|
|
|
|
const data = await this.parseResponse(response);
|
|
|
|
if (response.ok) {
|
|
this.handleSuccess(data);
|
|
} else {
|
|
this.handleError(data);
|
|
}
|
|
|
|
} catch (error) {
|
|
Logger.error('[FormHandler] Submit error:', error);
|
|
this.handleError({
|
|
message: 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.',
|
|
errors: {}
|
|
});
|
|
} finally {
|
|
this.setSubmitState(false);
|
|
}
|
|
}
|
|
|
|
async parseResponse(response) {
|
|
const contentType = response.headers.get('content-type');
|
|
|
|
if (contentType?.includes('application/json')) {
|
|
return await response.json();
|
|
}
|
|
|
|
const text = await response.text();
|
|
|
|
// Try to parse as JSON, fallback to text
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch {
|
|
return { message: text };
|
|
}
|
|
}
|
|
|
|
handleSuccess(data) {
|
|
Logger.info('[FormHandler] Form submitted successfully');
|
|
|
|
// Clear autosave draft on successful submission
|
|
if (this.options.enableAutosave) {
|
|
this.clearAutosaveDraft();
|
|
}
|
|
|
|
// Clear form if configured
|
|
if (data.clearForm !== false) {
|
|
this.form.reset();
|
|
this.state.reset();
|
|
}
|
|
|
|
this.showMessage(data.message || 'Formular erfolgreich gesendet!', 'success');
|
|
|
|
// Call success callback
|
|
this.triggerEvent('form:success', { data });
|
|
}
|
|
|
|
handleError(data) {
|
|
Logger.warn('[FormHandler] Form submission error:', data);
|
|
|
|
// Handle field-specific errors
|
|
if (data.errors && typeof data.errors === 'object') {
|
|
for (const [fieldName, errorMessage] of Object.entries(data.errors)) {
|
|
this.validator.errors.set(fieldName, errorMessage);
|
|
}
|
|
this.displayErrors();
|
|
}
|
|
|
|
// Show general error message
|
|
this.showMessage(data.message || 'Ein Fehler ist aufgetreten.', 'error');
|
|
|
|
// Call error callback
|
|
this.triggerEvent('form:error', { data });
|
|
}
|
|
|
|
validateSingleField(field) {
|
|
this.validator.errors.delete(field.name);
|
|
this.validator.validateField(field);
|
|
|
|
this.displayFieldError(field);
|
|
}
|
|
|
|
displayErrors() {
|
|
if (!this.options.showInlineErrors) return;
|
|
|
|
const errors = this.validator.getErrors();
|
|
|
|
for (const [fieldName, message] of Object.entries(errors)) {
|
|
const field = this.form.querySelector(`[name="${fieldName}"]`);
|
|
if (field) {
|
|
this.displayFieldError(field, message);
|
|
}
|
|
}
|
|
}
|
|
|
|
displayFieldError(field, message = null) {
|
|
const errorMessage = message || this.validator.getFieldError(field.name);
|
|
const errorElement = this.getOrCreateErrorElement(field);
|
|
|
|
if (errorMessage) {
|
|
errorElement.textContent = errorMessage;
|
|
errorElement.style.display = 'block';
|
|
field.classList.add('error');
|
|
field.setAttribute('aria-invalid', 'true');
|
|
field.setAttribute('aria-describedby', errorElement.id);
|
|
} else {
|
|
errorElement.textContent = '';
|
|
errorElement.style.display = 'none';
|
|
field.classList.remove('error');
|
|
field.removeAttribute('aria-invalid');
|
|
field.removeAttribute('aria-describedby');
|
|
}
|
|
}
|
|
|
|
getOrCreateErrorElement(field) {
|
|
const errorId = `error-${field.name}`;
|
|
let errorElement = document.getElementById(errorId);
|
|
|
|
if (!errorElement) {
|
|
errorElement = document.createElement('div');
|
|
errorElement.id = errorId;
|
|
errorElement.className = 'form-error';
|
|
errorElement.setAttribute('role', 'alert');
|
|
errorElement.style.display = 'none';
|
|
|
|
// Insert after field or field container
|
|
const container = field.closest('.form-group') || field.parentElement;
|
|
container.appendChild(errorElement);
|
|
}
|
|
|
|
return errorElement;
|
|
}
|
|
|
|
setupErrorDisplay() {
|
|
// Add CSS for error states if not present
|
|
if (!document.getElementById('form-handler-styles')) {
|
|
const styles = document.createElement('style');
|
|
styles.id = 'form-handler-styles';
|
|
styles.textContent = `
|
|
.form-error {
|
|
color: #dc2626;
|
|
font-size: 0.875rem;
|
|
margin-top: 0.25rem;
|
|
}
|
|
input.error, textarea.error, select.error {
|
|
border-color: #dc2626;
|
|
box-shadow: 0 0 0 1px #dc2626;
|
|
}
|
|
.form-message {
|
|
padding: 0.75rem;
|
|
border-radius: 0.375rem;
|
|
margin: 1rem 0;
|
|
}
|
|
.form-message.success {
|
|
background-color: #dcfce7;
|
|
color: #166534;
|
|
border: 1px solid #bbf7d0;
|
|
}
|
|
.form-message.error {
|
|
background-color: #fef2f2;
|
|
color: #dc2626;
|
|
border: 1px solid #fecaca;
|
|
}
|
|
`;
|
|
document.head.appendChild(styles);
|
|
}
|
|
}
|
|
|
|
showMessage(message, type = 'info') {
|
|
let messageContainer = this.form.querySelector('.form-messages');
|
|
|
|
if (!messageContainer) {
|
|
messageContainer = document.createElement('div');
|
|
messageContainer.className = 'form-messages';
|
|
this.form.prepend(messageContainer);
|
|
}
|
|
|
|
const messageElement = document.createElement('div');
|
|
messageElement.className = `form-message ${type}`;
|
|
messageElement.textContent = message;
|
|
messageElement.setAttribute('role', type === 'error' ? 'alert' : 'status');
|
|
|
|
messageContainer.innerHTML = '';
|
|
messageContainer.appendChild(messageElement);
|
|
|
|
// Auto-hide success messages after 5 seconds
|
|
if (type === 'success') {
|
|
setTimeout(() => {
|
|
if (messageElement.parentElement) {
|
|
messageElement.remove();
|
|
}
|
|
}, 5000);
|
|
}
|
|
}
|
|
|
|
clearErrors() {
|
|
this.validator.clearErrors();
|
|
|
|
// Clear visual error states
|
|
const errorFields = this.form.querySelectorAll('.error');
|
|
errorFields.forEach(field => {
|
|
field.classList.remove('error');
|
|
field.removeAttribute('aria-invalid');
|
|
field.removeAttribute('aria-describedby');
|
|
});
|
|
|
|
// Hide error messages
|
|
const errorElements = this.form.querySelectorAll('.form-error');
|
|
errorElements.forEach(element => {
|
|
element.style.display = 'none';
|
|
element.textContent = '';
|
|
});
|
|
|
|
// Clear general messages
|
|
const messageContainer = this.form.querySelector('.form-messages');
|
|
if (messageContainer) {
|
|
messageContainer.innerHTML = '';
|
|
}
|
|
}
|
|
|
|
setSubmitState(isSubmitting) {
|
|
this.isSubmitting = isSubmitting;
|
|
|
|
const submitButtons = this.form.querySelectorAll('button[type="submit"], input[type="submit"]');
|
|
|
|
submitButtons.forEach(button => {
|
|
button.disabled = isSubmitting;
|
|
|
|
if (isSubmitting) {
|
|
button.setAttribute('data-original-text', button.textContent);
|
|
button.textContent = 'Wird gesendet...';
|
|
} else {
|
|
const originalText = button.getAttribute('data-original-text');
|
|
if (originalText) {
|
|
button.textContent = originalText;
|
|
button.removeAttribute('data-original-text');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
triggerEvent(eventName, detail = {}) {
|
|
const event = new CustomEvent(eventName, {
|
|
detail,
|
|
bubbles: true,
|
|
cancelable: true
|
|
});
|
|
|
|
this.form.dispatchEvent(event);
|
|
}
|
|
|
|
/**
|
|
* Initialize autosave functionality
|
|
*/
|
|
initAutosave() {
|
|
// Generate storage key
|
|
this.autosaveStorageKey = this.options.autosaveStorageKey ||
|
|
this.options.autosaveStoragePrefix + this.generateFormId();
|
|
|
|
// Restore draft on init
|
|
this.restoreAutosaveDraft();
|
|
|
|
// Start periodic autosave
|
|
this.startAutosave();
|
|
|
|
Logger.info('[FormHandler] Autosave initialized', { storageKey: this.autosaveStorageKey });
|
|
}
|
|
|
|
/**
|
|
* Generate unique form identifier
|
|
*/
|
|
generateFormId() {
|
|
const formId = this.form.id ||
|
|
this.form.getAttribute('data-form-id') ||
|
|
this.form.querySelector('input[name="_form_id"]')?.value ||
|
|
'form_' + Date.now();
|
|
|
|
return formId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
}
|
|
|
|
/**
|
|
* Handle field change for autosave
|
|
*/
|
|
onAutosaveFieldChange(event) {
|
|
const field = event.target;
|
|
|
|
// Skip excluded fields
|
|
if (this.isAutosaveFieldExcluded(field)) {
|
|
return;
|
|
}
|
|
|
|
// Save immediately for important fields
|
|
if (this.isAutosaveImmediateField(field)) {
|
|
this.saveAutosaveDraft();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if field should be excluded from autosave
|
|
*/
|
|
isAutosaveFieldExcluded(field) {
|
|
return this.options.autosaveExcludeFields.some(selector =>
|
|
field.matches(selector)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if field should trigger immediate save
|
|
*/
|
|
isAutosaveImmediateField(field) {
|
|
return this.options.autosaveImmediateFields.some(selector =>
|
|
field.matches(selector)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Start periodic autosave
|
|
*/
|
|
startAutosave() {
|
|
if (this.autosaveTimer) {
|
|
return;
|
|
}
|
|
|
|
this.autosaveTimer = setInterval(() => {
|
|
if (this.state.isDirty()) {
|
|
this.saveAutosaveDraft();
|
|
}
|
|
}, this.options.autosaveInterval);
|
|
|
|
Logger.debug('[FormHandler] Autosave started', { interval: this.options.autosaveInterval });
|
|
}
|
|
|
|
/**
|
|
* Stop periodic autosave
|
|
*/
|
|
stopAutosave() {
|
|
if (this.autosaveTimer) {
|
|
clearInterval(this.autosaveTimer);
|
|
this.autosaveTimer = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save form data as draft
|
|
*/
|
|
saveAutosaveDraft() {
|
|
if (!this.form || !this.state.isDirty()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const formData = this.extractAutosaveFormData();
|
|
|
|
const draft = {
|
|
data: formData,
|
|
timestamp: Date.now(),
|
|
formId: this.generateFormId(),
|
|
url: window.location.href,
|
|
version: '1.0'
|
|
};
|
|
|
|
localStorage.setItem(this.autosaveStorageKey, JSON.stringify(draft));
|
|
|
|
this.lastAutosaveTime = new Date();
|
|
|
|
Logger.debug('[FormHandler] Draft saved', {
|
|
fields: Object.keys(formData).length,
|
|
storageKey: this.autosaveStorageKey
|
|
});
|
|
|
|
if (this.options.autosaveVisualFeedback) {
|
|
this.showAutosaveStatus('Draft saved', 'success', 1500);
|
|
}
|
|
|
|
// Trigger event
|
|
this.triggerEvent('form:autosave', { draft });
|
|
} catch (error) {
|
|
Logger.error('[FormHandler] Failed to save draft', error);
|
|
|
|
if (this.options.autosaveVisualFeedback) {
|
|
this.showAutosaveStatus('Failed to save draft', 'error', 3000);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Restore draft data to form
|
|
*/
|
|
restoreAutosaveDraft() {
|
|
try {
|
|
const draftJson = localStorage.getItem(this.autosaveStorageKey);
|
|
|
|
if (!draftJson) {
|
|
return;
|
|
}
|
|
|
|
const draft = JSON.parse(draftJson);
|
|
|
|
// Check if draft is expired
|
|
const age = Date.now() - draft.timestamp;
|
|
if (age > this.options.autosaveRetentionPeriod) {
|
|
localStorage.removeItem(this.autosaveStorageKey);
|
|
return;
|
|
}
|
|
|
|
// Restore form data
|
|
this.restoreAutosaveFormData(draft.data);
|
|
|
|
Logger.info('[FormHandler] Draft restored', {
|
|
age: Math.floor(age / 1000) + 's',
|
|
fields: Object.keys(draft.data).length
|
|
});
|
|
|
|
if (this.options.autosaveVisualFeedback) {
|
|
const ageText = this.formatDuration(age);
|
|
this.showAutosaveStatus(`Draft restored from ${ageText} ago`, 'info', 4000);
|
|
}
|
|
|
|
// Trigger event
|
|
this.triggerEvent('form:autosave-restored', { draft });
|
|
} catch (error) {
|
|
Logger.error('[FormHandler] Failed to restore draft', error);
|
|
localStorage.removeItem(this.autosaveStorageKey);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract form data for autosave (excluding sensitive fields)
|
|
*/
|
|
extractAutosaveFormData() {
|
|
const fields = this.getFormFields();
|
|
const data = {};
|
|
|
|
fields.forEach(field => {
|
|
if (!this.isAutosaveFieldExcluded(field)) {
|
|
const key = field.name || field.id;
|
|
const value = this.getAutosaveFieldValue(field);
|
|
|
|
if (key && value !== null && value !== undefined && value !== '') {
|
|
data[key] = value;
|
|
}
|
|
}
|
|
});
|
|
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Restore data to form fields
|
|
*/
|
|
restoreAutosaveFormData(data) {
|
|
let restoredCount = 0;
|
|
|
|
Object.entries(data).forEach(([key, value]) => {
|
|
const field = this.form.querySelector(`[name="${key}"], #${key}`);
|
|
|
|
if (field && !this.isAutosaveFieldExcluded(field)) {
|
|
this.setAutosaveFieldValue(field, value);
|
|
restoredCount++;
|
|
}
|
|
});
|
|
|
|
// Update form state
|
|
this.state.captureInitialValues();
|
|
|
|
return restoredCount;
|
|
}
|
|
|
|
/**
|
|
* Get form fields
|
|
*/
|
|
getFormFields() {
|
|
return Array.from(this.form.querySelectorAll(
|
|
'input:not([type="submit"]):not([type="button"]):not([type="reset"]), ' +
|
|
'textarea, select'
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Get field value for autosave
|
|
*/
|
|
getAutosaveFieldValue(field) {
|
|
switch (field.type) {
|
|
case 'checkbox':
|
|
return field.checked;
|
|
case 'radio':
|
|
return field.checked ? field.value : null;
|
|
case 'file':
|
|
return null; // Don't save file inputs
|
|
default:
|
|
return field.value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set field value for autosave
|
|
*/
|
|
setAutosaveFieldValue(field, value) {
|
|
switch (field.type) {
|
|
case 'checkbox':
|
|
field.checked = Boolean(value);
|
|
break;
|
|
case 'radio':
|
|
field.checked = (field.value === value);
|
|
break;
|
|
case 'file':
|
|
// Can't restore file inputs
|
|
break;
|
|
default:
|
|
field.value = value;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear autosave draft
|
|
*/
|
|
clearAutosaveDraft() {
|
|
if (this.autosaveStorageKey) {
|
|
localStorage.removeItem(this.autosaveStorageKey);
|
|
}
|
|
Logger.debug('[FormHandler] Draft cleared');
|
|
}
|
|
|
|
/**
|
|
* Format duration for human reading
|
|
*/
|
|
formatDuration(ms) {
|
|
const seconds = Math.floor(ms / 1000);
|
|
const minutes = Math.floor(seconds / 60);
|
|
const hours = Math.floor(minutes / 60);
|
|
|
|
if (hours > 0) {
|
|
return `${hours}h ${minutes % 60}m`;
|
|
} else if (minutes > 0) {
|
|
return `${minutes}m`;
|
|
} else {
|
|
return `${seconds}s`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show autosave status message
|
|
*/
|
|
showAutosaveStatus(message, type = 'info', duration = 3000) {
|
|
// Create or update status element
|
|
let statusEl = document.getElementById('form-autosave-status');
|
|
|
|
if (!statusEl) {
|
|
statusEl = document.createElement('div');
|
|
statusEl.id = 'form-autosave-status';
|
|
statusEl.style.cssText = `
|
|
position: fixed;
|
|
bottom: 20px;
|
|
right: 20px;
|
|
padding: 8px 12px;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
z-index: 9999;
|
|
max-width: 250px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
transition: opacity 0.3s ease;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
`;
|
|
document.body.appendChild(statusEl);
|
|
}
|
|
|
|
statusEl.textContent = message;
|
|
|
|
const styles = {
|
|
info: 'background: #e3f2fd; color: #1565c0; border: 1px solid #bbdefb;',
|
|
success: 'background: #e8f5e8; color: #2e7d32; border: 1px solid #c8e6c9;',
|
|
error: 'background: #ffebee; color: #c62828; border: 1px solid #ffcdd2;'
|
|
};
|
|
|
|
statusEl.style.cssText += styles[type] || styles.info;
|
|
statusEl.style.opacity = '1';
|
|
|
|
setTimeout(() => {
|
|
if (statusEl) {
|
|
statusEl.style.opacity = '0';
|
|
setTimeout(() => {
|
|
if (statusEl && statusEl.parentNode) {
|
|
statusEl.parentNode.removeChild(statusEl);
|
|
}
|
|
}, 300);
|
|
}
|
|
}, duration);
|
|
}
|
|
|
|
destroy() {
|
|
// Stop autosave
|
|
if (this.options.enableAutosave) {
|
|
this.stopAutosave();
|
|
}
|
|
|
|
// Remove event listeners and clean up
|
|
this.form.removeAttribute('data-enhanced');
|
|
Logger.info('[FormHandler] Destroyed');
|
|
}
|
|
} |