| | |
| | | } |
| | | |
| | | let form = this.getForm(e.target); |
| | | //Autosave |
| | | if (!form || !form.options.cache) return; |
| | | this.updateItem(field.dataset.field, this.getFieldValue(e.target), form); |
| | | } |
| | | |
| | |
| | | window.debouncer.cancel(`form:${form.id}:validate:${fieldName}`); |
| | | this.validateField(e.target); |
| | | |
| | | if (form.options.cache) { |
| | | this.updateItem(fieldName, this.getFieldValue(e.target), form); |
| | | } |
| | | this.updateItem(fieldName, this.getFieldValue(e.target), form); |
| | | } |
| | | |
| | | handleInput(e){ |
| | |
| | | e.preventDefault(); |
| | | const storedData = await this.store.get(form.id); |
| | | |
| | | this.notify('form-submit', { |
| | | config: form, |
| | | data: storedData?.changes || {} |
| | | }); |
| | | if (form.options.cache) { |
| | | this.notify('form-submit', { |
| | | config: form, |
| | | data: storedData.changes |
| | | }); |
| | | } else { |
| | | this.notify('form-submit', { |
| | | config: form, |
| | | data: this.changes.get(form.id)?.changes??{}, |
| | | }); |
| | | } |
| | | |
| | | } |
| | | |
| | | if (form.options.showSummary) { |
| | |
| | | let changes = this.changes.get(form.id); |
| | | changes.changes[name] = value; |
| | | this.changes.set(form.id, changes); |
| | | this.scheduleBackup(); |
| | | if (form.options.cache) { |
| | | this.scheduleBackup(); |
| | | } |
| | | } |
| | | |
| | | scheduleBackup() { |
| | |
| | | } |
| | | } |
| | | |
| | | handleFormSuccess(form, data) { |
| | | // Clear previous errors |
| | | form.querySelectorAll('.error-message').forEach(el => el.remove()); |
| | | form.querySelectorAll('.field-error').forEach(el => |
| | | el.classList.remove('field-error') |
| | | ); |
| | | |
| | | // Add success class to form |
| | | form.classList.add('form-success'); |
| | | |
| | | // Show success message if provided |
| | | if (data.message) { |
| | | const success = document.createElement('div'); |
| | | success.className = 'form-success-message success-message'; |
| | | success.textContent = data.message; |
| | | form.insertBefore(success, form.firstChild); |
| | | |
| | | const icon = window.getIcon?.('check-circle'); |
| | | if (icon) { |
| | | icon.classList.add('success-icon'); |
| | | success.prepend(icon); |
| | | } |
| | | } |
| | | |
| | | // If there's a title/description (for registration success) |
| | | if (data.title || data.description) { |
| | | const successBox = document.createElement('div'); |
| | | successBox.className = 'success-box'; |
| | | |
| | | if (data.title) { |
| | | const title = document.createElement('h3'); |
| | | title.textContent = data.title; |
| | | successBox.appendChild(title); |
| | | } |
| | | |
| | | if (data.description) { |
| | | const descriptions = Array.isArray(data.description) |
| | | ? data.description |
| | | : [data.description]; |
| | | |
| | | descriptions.forEach(desc => { |
| | | const p = document.createElement('p'); |
| | | p.textContent = desc; |
| | | successBox.appendChild(p); |
| | | }); |
| | | } |
| | | |
| | | form.insertBefore(successBox, form.firstChild); |
| | | } |
| | | |
| | | // DELETE CACHED FORM DATA ON SUCCESS |
| | | if (form.dataset.formId) { |
| | | this.store.delete(form.dataset.formId).catch(err => { |
| | | console.warn('Failed to clear form cache:', err); |
| | | }); |
| | | |
| | | // Clear form config dirty state |
| | | const formConfig = this.forms.get(form.dataset.formId); |
| | | if (formConfig) { |
| | | formConfig.isDirty = false; |
| | | formConfig.lastSaved = Date.now(); |
| | | formConfig.data = {}; // Clear cached data |
| | | } |
| | | } |
| | | |
| | | // Announce success for accessibility |
| | | if (window.jvbA11y) { |
| | | window.jvbA11y.announce(data.message || 'Form submitted successfully'); |
| | | } |
| | | |
| | | // Trigger custom event |
| | | form.dispatchEvent(new CustomEvent('jvb-form-success', { |
| | | detail: data |
| | | })); |
| | | } |
| | | |
| | | handleFormError(form, data) { |
| | | // Clear all previous errors |
| | | form.querySelectorAll('.error-message').forEach(el => el.remove()); |
| | | form.querySelectorAll('.field-error, .has-error').forEach(el => { |
| | | el.classList.remove('field-error', 'has-error'); |
| | | }); |
| | | |
| | | // Clear validation states using existing method |
| | | form.querySelectorAll('.field').forEach(fieldWrapper => { |
| | | this.clearValidation(fieldWrapper); |
| | | }); |
| | | |
| | | // Handle field-specific errors |
| | | if (data.field) { |
| | | const fieldWrapper = form.querySelector(`[data-field="${data.field}"]`); |
| | | if (fieldWrapper) { |
| | | // Use existing showError method for consistency |
| | | this.showError(fieldWrapper, data.message); |
| | | |
| | | // Mark as touched so validation persists |
| | | this.touchedFields.add(data.field); |
| | | |
| | | // Scroll to error |
| | | fieldWrapper.scrollIntoView({ behavior: 'smooth', block: 'center' }); |
| | | |
| | | // Focus the input for better UX |
| | | const input = fieldWrapper.querySelector('input, textarea, select'); |
| | | if (input) { |
| | | input.focus(); |
| | | } |
| | | } |
| | | } else { |
| | | // General form error (not field-specific) |
| | | const error = document.createElement('div'); |
| | | error.className = 'form-error error-message'; |
| | | error.textContent = data.message; |
| | | |
| | | // Add icon for consistency |
| | | const icon = window.getIcon?.('close-circle'); |
| | | if (icon) { |
| | | icon.classList.add('error-icon'); |
| | | error.prepend(icon); |
| | | } |
| | | |
| | | form.insertBefore(error, form.firstChild); |
| | | |
| | | // Scroll to top to show the error |
| | | form.scrollIntoView({ behavior: 'smooth', block: 'start' }); |
| | | } |
| | | |
| | | // Announce error for accessibility |
| | | if (window.jvbA11y) { |
| | | const announcement = data.field |
| | | ? `Error in ${data.field}: ${data.message}` |
| | | : `Form error: ${data.message}`; |
| | | window.jvbA11y.announce(announcement); |
| | | } |
| | | |
| | | // Trigger custom event |
| | | form.dispatchEvent(new CustomEvent('jvb-form-error', { |
| | | detail: data |
| | | })); |
| | | } |
| | | |
| | | /********************************************************************** |
| | | STATUS |
| | | **********************************************************************/ |
| | |
| | | this.inputs.set(config.id, config); |
| | | } |
| | | /********************************************************************** |
| | | Subscription |
| | | **********************************************************************/ |
| | | subscribe(callback) { |
| | | this.subscribers.add(callback); |
| | | return () => this.subscribers.delete(callback); |
| | | } |
| | | |
| | | notify(event, data) { |
| | | this.subscribers.forEach(cb => { |
| | | try { |
| | | cb(event, data); |
| | | } catch (e) { |
| | | console.error('HandleSelection subscriber error:', e); |
| | | } |
| | | }); |
| | | } |
| | | /********************************************************************** |
| | | Cleanup |
| | | **********************************************************************/ |
| | | destroy() { |