ultimate-jekyll-manager 0.0.119 → 0.0.121
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +102 -2
- package/README.md +171 -2
- package/TODO.md +10 -2
- package/_backup/form-manager.backup.js +1020 -0
- package/dist/assets/js/libs/auth/pages.js +64 -136
- package/dist/assets/js/libs/form-manager.js +643 -775
- package/dist/assets/js/pages/account/sections/api-keys.js +37 -52
- package/dist/assets/js/pages/account/sections/connections.js +37 -46
- package/dist/assets/js/pages/account/sections/delete.js +46 -66
- package/dist/assets/js/pages/account/sections/profile.js +37 -56
- package/dist/assets/js/pages/account/sections/security.js +100 -126
- package/dist/assets/js/pages/admin/notifications/new/index.js +72 -157
- package/dist/assets/js/pages/blog/index.js +29 -51
- package/dist/assets/js/pages/contact/index.js +110 -144
- package/dist/assets/js/pages/download/index.js +38 -86
- package/dist/assets/js/pages/oauth2/index.js +17 -17
- package/dist/assets/js/pages/payment/checkout/index.js +23 -36
- package/dist/assets/js/pages/test/libraries/form-manager/index.js +194 -0
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/auth/signin.html +2 -2
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/auth/signup.html +2 -2
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/contact.html +10 -37
- package/dist/defaults/dist/pages/test/libraries/form-manager.html +181 -0
- package/dist/gulp/tasks/serve.js +18 -0
- package/dist/lib/logger.js +1 -1
- package/firebase-debug.log +420 -0
- package/package.json +11 -7
- package/.playwright-mcp/page-2025-10-22T19-11-27-666Z.png +0 -0
- package/.playwright-mcp/page-2025-10-22T19-11-57-357Z.png +0 -0
|
@@ -0,0 +1,1020 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form Manager Library
|
|
3
|
+
* A comprehensive form management system that handles state, validation, submission, and UI updates
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class FormManager extends EventTarget {
|
|
7
|
+
constructor(selector, options = {}) {
|
|
8
|
+
super();
|
|
9
|
+
|
|
10
|
+
// Store whether initialState was explicitly provided
|
|
11
|
+
this.hasCustomInitialState = options.hasOwnProperty('initialState');
|
|
12
|
+
|
|
13
|
+
// Configuration with defaults
|
|
14
|
+
this.config = {
|
|
15
|
+
autoDisable: true, // Auto disable/enable form controls
|
|
16
|
+
showSpinner: true, // Show spinner on submit buttons
|
|
17
|
+
validateOnSubmit: true, // Validate before submission
|
|
18
|
+
allowMultipleSubmissions: true, // Allow multiple submissions
|
|
19
|
+
resetOnSuccess: false, // Reset form after successful submission
|
|
20
|
+
spinnerHTML: '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>', // Spinner HTML
|
|
21
|
+
submitButtonLoadingText: 'Processing...',
|
|
22
|
+
submitButtonSuccessText: null, // Text to show on button after successful submission (when allowMultipleSubmissions: false)
|
|
23
|
+
fieldErrorClass: 'is-invalid',
|
|
24
|
+
fieldSuccessClass: 'is-valid',
|
|
25
|
+
initialState: 'loading', // Initial state: loading, ready, submitting, submitted
|
|
26
|
+
toastPosition: 'top-center', // Toast position: top-center, top-end, bottom-center, bottom-end, middle-center
|
|
27
|
+
toastDuration: 5000, // Toast duration in milliseconds
|
|
28
|
+
...options
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Get form element
|
|
32
|
+
this.form = typeof selector === 'string' ? document.querySelector(selector) : selector;
|
|
33
|
+
if (!this.form) {
|
|
34
|
+
throw new Error(`Form not found: ${selector}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// State management
|
|
38
|
+
this.state = {
|
|
39
|
+
status: 'loading', // loading, ready, submitting, submitted
|
|
40
|
+
isValid: true,
|
|
41
|
+
errors: {},
|
|
42
|
+
data: {},
|
|
43
|
+
isDirty: false
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Store original button states
|
|
47
|
+
this.originalButtonStates = new Map();
|
|
48
|
+
|
|
49
|
+
// Initialize
|
|
50
|
+
this.init();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Initialize the form manager
|
|
55
|
+
*/
|
|
56
|
+
init() {
|
|
57
|
+
// Store original button states BEFORE setting loading state
|
|
58
|
+
this.form.querySelectorAll('button').forEach(button => {
|
|
59
|
+
const state = {
|
|
60
|
+
innerHTML: button.innerHTML,
|
|
61
|
+
disabled: button.disabled
|
|
62
|
+
};
|
|
63
|
+
this.originalButtonStates.set(button, state);
|
|
64
|
+
|
|
65
|
+
/* @dev-only:start */
|
|
66
|
+
{
|
|
67
|
+
console.log(`[FormManager] Storing button "${button.textContent.trim()}": disabled=${state.disabled}`);
|
|
68
|
+
}
|
|
69
|
+
/* @dev-only:end */
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Track which submit button was clicked
|
|
73
|
+
this.clickedSubmitButton = null;
|
|
74
|
+
|
|
75
|
+
// Store if this is the first initialization
|
|
76
|
+
this.isInitializing = true;
|
|
77
|
+
|
|
78
|
+
// Set initial state
|
|
79
|
+
this.setFormState(this.config.initialState);
|
|
80
|
+
|
|
81
|
+
// Attach event listeners
|
|
82
|
+
this.attachEventListeners();
|
|
83
|
+
|
|
84
|
+
// Handle page show event (when navigating back from OAuth redirect or other navigation)
|
|
85
|
+
window.addEventListener('pageshow', (event) => {
|
|
86
|
+
// Check if page was restored from cache (persisted)
|
|
87
|
+
|
|
88
|
+
// Log
|
|
89
|
+
console.log('[FormManager] pageshow event', event);
|
|
90
|
+
|
|
91
|
+
// Quit if not persisted
|
|
92
|
+
if (!event.persisted) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Log
|
|
97
|
+
console.log('[FormManager] Page restored from cache, resetting form to ready state');
|
|
98
|
+
|
|
99
|
+
// Reset form to ready state if it was in submitting state
|
|
100
|
+
if (this.state.status === 'submitting') {
|
|
101
|
+
this.setFormState('ready');
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Only auto-transition to ready if initialState wasn't explicitly set
|
|
106
|
+
// (meaning it's using the default 'loading' value)
|
|
107
|
+
if (!this.hasCustomInitialState) {
|
|
108
|
+
// Set ready state when DOM is ready
|
|
109
|
+
if (document.readyState === 'loading') {
|
|
110
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
111
|
+
this.isInitializing = false;
|
|
112
|
+
this.setFormState('ready');
|
|
113
|
+
});
|
|
114
|
+
} else {
|
|
115
|
+
// Use setTimeout to ensure any parent initialization completes
|
|
116
|
+
setTimeout(() => {
|
|
117
|
+
this.isInitializing = false;
|
|
118
|
+
this.setFormState('ready');
|
|
119
|
+
}, 0);
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
// If custom initial state was provided, we're done initializing
|
|
123
|
+
this.isInitializing = false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Attach all event listeners
|
|
129
|
+
*/
|
|
130
|
+
attachEventListeners() {
|
|
131
|
+
// Form submit
|
|
132
|
+
this.form.addEventListener('submit', (e) => this.handleSubmit(e));
|
|
133
|
+
|
|
134
|
+
// Unified input changes - capture all user interactions
|
|
135
|
+
const inputEvents = ['keyup', 'change', 'paste', 'cut'];
|
|
136
|
+
const changeHandler = (e) => this.handleChange(e);
|
|
137
|
+
|
|
138
|
+
// Attach to all form inputs
|
|
139
|
+
this.form.querySelectorAll('input, select, textarea').forEach(element => {
|
|
140
|
+
inputEvents.forEach(eventType => {
|
|
141
|
+
element.addEventListener(eventType, changeHandler);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Button clicks (for non-submit buttons)
|
|
146
|
+
this.form.querySelectorAll('button[type="button"]').forEach(button => {
|
|
147
|
+
button.addEventListener('click', (e) => this.handleButtonClick(e));
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Track submit button clicks
|
|
151
|
+
this.form.querySelectorAll('button[type="submit"]').forEach(button => {
|
|
152
|
+
button.addEventListener('click', (e) => {
|
|
153
|
+
this.clickedSubmitButton = e.currentTarget;
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Handle form submission
|
|
160
|
+
*/
|
|
161
|
+
async handleSubmit(e) {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
|
|
164
|
+
// Check if already submitting
|
|
165
|
+
if (this.state.status === 'submitting' && !this.config.allowMultipleSubmissions) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Clear any existing errors
|
|
170
|
+
this.clearErrors();
|
|
171
|
+
|
|
172
|
+
// Collect form data
|
|
173
|
+
const formData = this.collectFormData();
|
|
174
|
+
|
|
175
|
+
// Build the submit event detail
|
|
176
|
+
const submitEvent = new CustomEvent('submit', {
|
|
177
|
+
detail: {
|
|
178
|
+
data: formData,
|
|
179
|
+
form: this.form,
|
|
180
|
+
submitButton: this.clickedSubmitButton
|
|
181
|
+
},
|
|
182
|
+
cancelable: true
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
/* @dev-only:start */
|
|
186
|
+
{
|
|
187
|
+
console.log(`[FormManager] Submit event triggered on ${this.form.id}`, formData, submitEvent.detail.submitButton);
|
|
188
|
+
}
|
|
189
|
+
/* @dev-only:end */
|
|
190
|
+
|
|
191
|
+
// Validate if enabled
|
|
192
|
+
console.log('-----1');
|
|
193
|
+
if (this.config.validateOnSubmit) {
|
|
194
|
+
console.log('-----2');
|
|
195
|
+
const validation = this.validate(formData);
|
|
196
|
+
if (!validation.isValid) {
|
|
197
|
+
console.log('-----3');
|
|
198
|
+
this.showErrors(validation.errors);
|
|
199
|
+
// Show a summary notification for validation errors
|
|
200
|
+
const errorCount = Object.keys(validation.errors).length;
|
|
201
|
+
const message = errorCount === 1
|
|
202
|
+
? 'Please correct the error below'
|
|
203
|
+
: `Please correct the ${errorCount} errors below`;
|
|
204
|
+
this.showNotification(message, 'danger');
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Set submitting state
|
|
210
|
+
this.setFormState('submitting');
|
|
211
|
+
|
|
212
|
+
// Emit submit event with the clicked submit button
|
|
213
|
+
this.dispatchEvent(submitEvent);
|
|
214
|
+
|
|
215
|
+
// Reset clicked button after dispatching event
|
|
216
|
+
this.clickedSubmitButton = null;
|
|
217
|
+
|
|
218
|
+
// If event was not cancelled, handle default submission
|
|
219
|
+
if (!submitEvent.defaultPrevented) {
|
|
220
|
+
try {
|
|
221
|
+
// Default submission (can be overridden by listening to submit event)
|
|
222
|
+
await this.defaultSubmitHandler(formData);
|
|
223
|
+
|
|
224
|
+
// Success - set state based on allowMultipleSubmissions
|
|
225
|
+
if (this.config.allowMultipleSubmissions) {
|
|
226
|
+
this.setFormState('ready');
|
|
227
|
+
} else {
|
|
228
|
+
this.setFormState('submitted');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this.dispatchEvent(new CustomEvent('success', {
|
|
232
|
+
detail: { data: formData }
|
|
233
|
+
}));
|
|
234
|
+
|
|
235
|
+
// Reset form if configured
|
|
236
|
+
if (this.config.resetOnSuccess) {
|
|
237
|
+
this.reset();
|
|
238
|
+
}
|
|
239
|
+
} catch (error) {
|
|
240
|
+
// Error - always go back to ready state
|
|
241
|
+
this.setFormState('ready');
|
|
242
|
+
this.showError(error.message);
|
|
243
|
+
this.dispatchEvent(new CustomEvent('error', {
|
|
244
|
+
detail: { error, data: formData }
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Default submit handler (can be overridden)
|
|
252
|
+
*/
|
|
253
|
+
async defaultSubmitHandler(data) {
|
|
254
|
+
// Default implementation - just log
|
|
255
|
+
console.log('Form submitted with data:', data);
|
|
256
|
+
// Simulate async operation
|
|
257
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Handle input changes with debouncing
|
|
262
|
+
*/
|
|
263
|
+
handleChange(e) {
|
|
264
|
+
const field = e.target;
|
|
265
|
+
|
|
266
|
+
// Clear field error immediately when user starts typing
|
|
267
|
+
if (field.classList.contains(this.config.fieldErrorClass)) {
|
|
268
|
+
this.clearFieldError(field);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Clear any existing timeout for this field
|
|
272
|
+
if (this.changeTimeouts) {
|
|
273
|
+
clearTimeout(this.changeTimeouts.get(field));
|
|
274
|
+
} else {
|
|
275
|
+
this.changeTimeouts = new Map();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Set a new timeout to capture the final value
|
|
279
|
+
this.changeTimeouts.set(field, setTimeout(() => {
|
|
280
|
+
this.state.isDirty = true;
|
|
281
|
+
|
|
282
|
+
// Collect all form data after the change
|
|
283
|
+
const data = this.collectFormData();
|
|
284
|
+
|
|
285
|
+
// Get the specific field value
|
|
286
|
+
const fieldValue = this.getFieldValue(field);
|
|
287
|
+
|
|
288
|
+
// Emit the unified change event
|
|
289
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
290
|
+
detail: {
|
|
291
|
+
field: field,
|
|
292
|
+
fieldName: field.name,
|
|
293
|
+
fieldValue: fieldValue,
|
|
294
|
+
data: data,
|
|
295
|
+
event: e
|
|
296
|
+
}
|
|
297
|
+
}));
|
|
298
|
+
|
|
299
|
+
// Clean up the timeout reference
|
|
300
|
+
this.changeTimeouts.delete(field);
|
|
301
|
+
}, 100)); // 100ms delay to ensure we capture the final value
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Handle non-submit button clicks
|
|
306
|
+
*/
|
|
307
|
+
handleButtonClick(e) {
|
|
308
|
+
const button = e.currentTarget;
|
|
309
|
+
const action = button.getAttribute('data-action') || button.id;
|
|
310
|
+
|
|
311
|
+
this.dispatchEvent(new CustomEvent('button', {
|
|
312
|
+
detail: {
|
|
313
|
+
button,
|
|
314
|
+
action,
|
|
315
|
+
data: this.collectFormData()
|
|
316
|
+
}
|
|
317
|
+
}));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Set form state and update UI accordingly
|
|
322
|
+
*/
|
|
323
|
+
setFormState(status) {
|
|
324
|
+
const previousStatus = this.state.status;
|
|
325
|
+
this.state.status = status;
|
|
326
|
+
|
|
327
|
+
/* @dev-only:start */
|
|
328
|
+
{
|
|
329
|
+
console.log(`[FormManager] ${this.form.id || 'form'}: ${previousStatus} --> ${status}`);
|
|
330
|
+
}
|
|
331
|
+
/* @dev-only:end */
|
|
332
|
+
|
|
333
|
+
// Update form data attribute
|
|
334
|
+
this.form.setAttribute('data-form-state', status);
|
|
335
|
+
|
|
336
|
+
switch (status) {
|
|
337
|
+
case 'loading':
|
|
338
|
+
this.disableForm();
|
|
339
|
+
break;
|
|
340
|
+
|
|
341
|
+
case 'ready':
|
|
342
|
+
this.enableForm();
|
|
343
|
+
this.hideSubmittingState();
|
|
344
|
+
break;
|
|
345
|
+
|
|
346
|
+
case 'submitting':
|
|
347
|
+
this.disableForm();
|
|
348
|
+
this.showSubmittingState();
|
|
349
|
+
break;
|
|
350
|
+
|
|
351
|
+
case 'submitted':
|
|
352
|
+
// Keep form disabled after submission by default
|
|
353
|
+
if (!this.config.resetOnSuccess) {
|
|
354
|
+
this.disableForm();
|
|
355
|
+
}
|
|
356
|
+
this.hideSubmittingState();
|
|
357
|
+
// Update button text if submitButtonSuccessText is configured
|
|
358
|
+
if (this.config.submitButtonSuccessText && !this.config.allowMultipleSubmissions) {
|
|
359
|
+
this.showSuccessButtonText();
|
|
360
|
+
}
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Emit state change event
|
|
365
|
+
this.dispatchEvent(new CustomEvent('statechange', {
|
|
366
|
+
detail: {
|
|
367
|
+
status,
|
|
368
|
+
previousStatus
|
|
369
|
+
}
|
|
370
|
+
}));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Disable all form controls
|
|
375
|
+
*/
|
|
376
|
+
disableForm() {
|
|
377
|
+
if (!this.config.autoDisable) return;
|
|
378
|
+
|
|
379
|
+
// Disable all inputs, selects, textareas, and buttons
|
|
380
|
+
this.form.querySelectorAll('input, select, textarea, button').forEach(element => {
|
|
381
|
+
element.disabled = true;
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Enable all form controls
|
|
387
|
+
*/
|
|
388
|
+
enableForm() {
|
|
389
|
+
if (!this.config.autoDisable) return;
|
|
390
|
+
|
|
391
|
+
/* @dev-only:start */
|
|
392
|
+
{
|
|
393
|
+
const count = this.form.querySelectorAll('input, select, textarea, button').length;
|
|
394
|
+
console.log(`[FormManager] Enabling ${count} controls in ${this.form.id || 'form'}`);
|
|
395
|
+
}
|
|
396
|
+
/* @dev-only:end */
|
|
397
|
+
|
|
398
|
+
// Enable all inputs, selects, textareas, and buttons
|
|
399
|
+
this.form.querySelectorAll('input, select, textarea, button').forEach(element => {
|
|
400
|
+
// Only enable if it wasn't originally disabled
|
|
401
|
+
const originalState = this.originalButtonStates.get(element);
|
|
402
|
+
|
|
403
|
+
// Always enable submit buttons regardless of original state
|
|
404
|
+
const isSubmitButton = element.type === 'submit';
|
|
405
|
+
|
|
406
|
+
/* @dev-only:start */
|
|
407
|
+
{
|
|
408
|
+
if (element.tagName === 'BUTTON') {
|
|
409
|
+
const willEnable = isSubmitButton || !originalState || !originalState.disabled;
|
|
410
|
+
console.log(`[FormManager] Button "${element.textContent.trim()}": originally ${originalState?.disabled ? 'disabled' : 'enabled'} --> ${willEnable ? 'enabling' : 'keeping disabled'}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/* @dev-only:end */
|
|
414
|
+
|
|
415
|
+
if (isSubmitButton || !originalState || !originalState.disabled) {
|
|
416
|
+
element.disabled = false;
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// Focus the field with autofocus attribute if it exists
|
|
421
|
+
const autofocusField = this.form.querySelector('[autofocus]');
|
|
422
|
+
if (autofocusField && !autofocusField.disabled) {
|
|
423
|
+
autofocusField.focus();
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Show submitting state on buttons
|
|
429
|
+
*/
|
|
430
|
+
showSubmittingState() {
|
|
431
|
+
if (!this.config.showSpinner) return;
|
|
432
|
+
|
|
433
|
+
// Update submit buttons
|
|
434
|
+
this.form.querySelectorAll('button[type="submit"]').forEach(button => {
|
|
435
|
+
const originalState = this.originalButtonStates.get(button);
|
|
436
|
+
if (originalState) {
|
|
437
|
+
button.innerHTML = this.config.spinnerHTML + this.config.submitButtonLoadingText;
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Hide submitting state on buttons
|
|
444
|
+
*/
|
|
445
|
+
hideSubmittingState() {
|
|
446
|
+
// Restore original button content
|
|
447
|
+
this.form.querySelectorAll('button[type="submit"]').forEach(button => {
|
|
448
|
+
const originalState = this.originalButtonStates.get(button);
|
|
449
|
+
if (originalState) {
|
|
450
|
+
button.innerHTML = originalState.innerHTML;
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Show success button text after successful submission
|
|
457
|
+
*/
|
|
458
|
+
showSuccessButtonText() {
|
|
459
|
+
if (!this.config.submitButtonSuccessText) return;
|
|
460
|
+
|
|
461
|
+
// Update submit buttons with success text
|
|
462
|
+
this.form.querySelectorAll('button[type="submit"]').forEach(button => {
|
|
463
|
+
// Find the button-text span if it exists
|
|
464
|
+
const buttonTextSpan = button.querySelector('.button-text');
|
|
465
|
+
if (buttonTextSpan) {
|
|
466
|
+
buttonTextSpan.textContent = this.config.submitButtonSuccessText;
|
|
467
|
+
} else {
|
|
468
|
+
// If no button-text span, update the entire button content
|
|
469
|
+
button.textContent = this.config.submitButtonSuccessText;
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Set nested property value using dot notation
|
|
476
|
+
*/
|
|
477
|
+
setNestedValue(obj, path, value) {
|
|
478
|
+
// Check if path contains dots
|
|
479
|
+
if (!path.includes('.')) {
|
|
480
|
+
obj[path] = value;
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const keys = path.split('.');
|
|
485
|
+
const lastKey = keys.pop();
|
|
486
|
+
|
|
487
|
+
// Create nested structure if it doesn't exist
|
|
488
|
+
let current = obj;
|
|
489
|
+
for (const key of keys) {
|
|
490
|
+
if (!current[key] || typeof current[key] !== 'object') {
|
|
491
|
+
current[key] = {};
|
|
492
|
+
}
|
|
493
|
+
current = current[key];
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Set the value
|
|
497
|
+
current[lastKey] = value;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Get nested property value using dot notation
|
|
502
|
+
*/
|
|
503
|
+
getNestedValue(obj, path) {
|
|
504
|
+
// Check if path contains dots
|
|
505
|
+
if (!path.includes('.')) {
|
|
506
|
+
return obj[path];
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const keys = path.split('.');
|
|
510
|
+
let current = obj;
|
|
511
|
+
|
|
512
|
+
for (const key of keys) {
|
|
513
|
+
if (current == null || typeof current !== 'object') {
|
|
514
|
+
return undefined;
|
|
515
|
+
}
|
|
516
|
+
current = current[key];
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return current;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Collect all form data
|
|
524
|
+
*/
|
|
525
|
+
collectFormData() {
|
|
526
|
+
const formData = new FormData(this.form);
|
|
527
|
+
const data = {};
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
// Convert FormData to plain object with support for dot notation
|
|
531
|
+
for (const [key, value] of formData.entries()) {
|
|
532
|
+
// Handle both nested and flat structure
|
|
533
|
+
const existingValue = this.getNestedValue(data, key);
|
|
534
|
+
if (existingValue !== undefined) {
|
|
535
|
+
// Handle multiple values
|
|
536
|
+
if (Array.isArray(existingValue)) {
|
|
537
|
+
existingValue.push(value);
|
|
538
|
+
} else {
|
|
539
|
+
this.setNestedValue(data, key, [existingValue, value]);
|
|
540
|
+
}
|
|
541
|
+
} else {
|
|
542
|
+
this.setNestedValue(data, key, value);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Handle checkboxes that might not be in FormData when unchecked
|
|
547
|
+
this.form.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
|
|
548
|
+
const name = checkbox.name;
|
|
549
|
+
if (!checkbox.checked) {
|
|
550
|
+
// Check if value exists
|
|
551
|
+
const value = this.getNestedValue(data, name);
|
|
552
|
+
if (value === undefined) {
|
|
553
|
+
// For single checkboxes, set to false
|
|
554
|
+
if (!this.form.querySelectorAll(`input[type="checkbox"][name="${name}"]`)[1]) {
|
|
555
|
+
this.setNestedValue(data, name, false);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// Handle radio buttons
|
|
562
|
+
this.form.querySelectorAll('input[type="radio"]:checked').forEach(radio => {
|
|
563
|
+
this.setNestedValue(data, radio.name, radio.value);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
this.state.data = data;
|
|
567
|
+
return data;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Get field value by element or name
|
|
572
|
+
*/
|
|
573
|
+
getFieldValue(fieldOrName) {
|
|
574
|
+
const field = typeof fieldOrName === 'string'
|
|
575
|
+
? this.form.querySelector(`[name="${fieldOrName}"]`)
|
|
576
|
+
: fieldOrName;
|
|
577
|
+
|
|
578
|
+
if (!field) return undefined;
|
|
579
|
+
|
|
580
|
+
if (field.type === 'checkbox') {
|
|
581
|
+
// For multiple checkboxes with same name, return array of checked values
|
|
582
|
+
const checkboxes = this.form.querySelectorAll(`input[type="checkbox"][name="${field.name}"]`);
|
|
583
|
+
if (checkboxes.length > 1) {
|
|
584
|
+
return Array.from(checkboxes)
|
|
585
|
+
.filter(cb => cb.checked)
|
|
586
|
+
.map(cb => cb.value);
|
|
587
|
+
}
|
|
588
|
+
return field.checked;
|
|
589
|
+
} else if (field.type === 'radio') {
|
|
590
|
+
return this.form.querySelector(`input[name="${field.name}"]:checked`)?.value;
|
|
591
|
+
} else {
|
|
592
|
+
return field.value;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Validate form data
|
|
598
|
+
*/
|
|
599
|
+
validate(data) {
|
|
600
|
+
const errors = {};
|
|
601
|
+
let isValid = true;
|
|
602
|
+
|
|
603
|
+
// Log
|
|
604
|
+
/* @dev-only:start */
|
|
605
|
+
{
|
|
606
|
+
console.log(`[FormManager] Validating form ${this.form.id || 'form'}`, data);
|
|
607
|
+
}
|
|
608
|
+
/* @dev-only:end */
|
|
609
|
+
|
|
610
|
+
// Check required fields
|
|
611
|
+
this.form.querySelectorAll('[required]').forEach(field => {
|
|
612
|
+
const value = this.getNestedValue(data, field.name);
|
|
613
|
+
|
|
614
|
+
if (!value || (typeof value === 'string' && !value.trim())) {
|
|
615
|
+
errors[field.name] = `${this.getFieldLabel(field)} is required`;
|
|
616
|
+
isValid = false;
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// Check email fields
|
|
621
|
+
this.form.querySelectorAll('input[type="email"]').forEach(field => {
|
|
622
|
+
const value = this.getNestedValue(data, field.name);
|
|
623
|
+
|
|
624
|
+
if (value && !this.isValidEmail(value)) {
|
|
625
|
+
errors[field.name] = 'Please enter a valid email address';
|
|
626
|
+
isValid = false;
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// Check pattern validation
|
|
631
|
+
this.form.querySelectorAll('[pattern]').forEach(field => {
|
|
632
|
+
const value = this.getNestedValue(data, field.name);
|
|
633
|
+
|
|
634
|
+
const pattern = new RegExp(field.pattern);
|
|
635
|
+
if (value && !pattern.test(value)) {
|
|
636
|
+
errors[field.name] = field.title || 'Invalid format';
|
|
637
|
+
isValid = false;
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
// Check minlength validation
|
|
642
|
+
this.form.querySelectorAll('[minlength]').forEach(field => {
|
|
643
|
+
const value = this.getNestedValue(data, field.name);
|
|
644
|
+
|
|
645
|
+
const minLength = parseInt(field.minLength);
|
|
646
|
+
if (value && value.length < minLength) {
|
|
647
|
+
errors[field.name] = `${this.getFieldLabel(field)} must be at least ${minLength} characters`;
|
|
648
|
+
isValid = false;
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// Check maxlength validation
|
|
653
|
+
this.form.querySelectorAll('[maxlength]').forEach(field => {
|
|
654
|
+
const value = this.getNestedValue(data, field.name);
|
|
655
|
+
|
|
656
|
+
const maxLength = parseInt(field.maxLength);
|
|
657
|
+
if (value && value.length > maxLength) {
|
|
658
|
+
errors[field.name] = `${this.getFieldLabel(field)} must be no more than ${maxLength} characters`;
|
|
659
|
+
isValid = false;
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
// Check min validation for number inputs
|
|
664
|
+
this.form.querySelectorAll('input[type="number"][min]').forEach(field => {
|
|
665
|
+
const value = this.getNestedValue(data, field.name);
|
|
666
|
+
|
|
667
|
+
const min = parseFloat(field.min);
|
|
668
|
+
const numValue = parseFloat(value);
|
|
669
|
+
if (!isNaN(numValue) && numValue < min) {
|
|
670
|
+
errors[field.name] = `${this.getFieldLabel(field)} must be at least ${min}`;
|
|
671
|
+
isValid = false;
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
// Check max validation for number inputs
|
|
676
|
+
this.form.querySelectorAll('input[type="number"][max]').forEach(field => {
|
|
677
|
+
const value = this.getNestedValue(data, field.name);
|
|
678
|
+
|
|
679
|
+
const max = parseFloat(field.max);
|
|
680
|
+
const numValue = parseFloat(value);
|
|
681
|
+
if (!isNaN(numValue) && numValue > max) {
|
|
682
|
+
errors[field.name] = `${this.getFieldLabel(field)} must be no more than ${max}`;
|
|
683
|
+
isValid = false;
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
// Custom validation
|
|
688
|
+
const customValidation = new CustomEvent('validate', {
|
|
689
|
+
detail: { data, errors },
|
|
690
|
+
cancelable: true
|
|
691
|
+
});
|
|
692
|
+
this.dispatchEvent(customValidation);
|
|
693
|
+
|
|
694
|
+
// Update isValid if custom validation added errors
|
|
695
|
+
this.state.isValid = isValid;
|
|
696
|
+
this.state.errors = errors;
|
|
697
|
+
|
|
698
|
+
// Log
|
|
699
|
+
/* @dev-only:start */
|
|
700
|
+
{
|
|
701
|
+
console.log(`[FormManager] Validation result for ${this.form.id || 'form'}`, { isValid, errors });
|
|
702
|
+
}
|
|
703
|
+
/* @dev-only:end */
|
|
704
|
+
|
|
705
|
+
return { isValid, errors };
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Get field label
|
|
710
|
+
*/
|
|
711
|
+
getFieldLabel(field) {
|
|
712
|
+
// Try to find associated label
|
|
713
|
+
const label = this.form.querySelector(`label[for="${field.id}"]`);
|
|
714
|
+
if (label) {
|
|
715
|
+
return label.textContent.replace('*', '').trim();
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Fallback to name attribute
|
|
719
|
+
return field.name.replace(/[_-]/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Email validation helper
|
|
724
|
+
*/
|
|
725
|
+
isValidEmail(email) {
|
|
726
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Show errors
|
|
731
|
+
*/
|
|
732
|
+
showErrors(errors) {
|
|
733
|
+
// Clear previous errors
|
|
734
|
+
this.clearErrors();
|
|
735
|
+
|
|
736
|
+
let firstErrorField = null;
|
|
737
|
+
|
|
738
|
+
// Show field-specific errors
|
|
739
|
+
Object.entries(errors).forEach(([fieldName, error]) => {
|
|
740
|
+
const field = this.form.querySelector(`[name="${fieldName}"]`);
|
|
741
|
+
if (field) {
|
|
742
|
+
field.classList.add(this.config.fieldErrorClass);
|
|
743
|
+
|
|
744
|
+
// Track first error field for focus
|
|
745
|
+
if (!firstErrorField) {
|
|
746
|
+
firstErrorField = field;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Show error message
|
|
750
|
+
const errorElement = document.createElement('div');
|
|
751
|
+
errorElement.className = 'invalid-feedback';
|
|
752
|
+
errorElement.textContent = error;
|
|
753
|
+
|
|
754
|
+
// Insert after field or field group
|
|
755
|
+
const insertAfter = field.closest('.input-group') || field;
|
|
756
|
+
insertAfter.parentNode.insertBefore(errorElement, insertAfter.nextSibling);
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
// Focus the first field with an error
|
|
761
|
+
if (firstErrorField) {
|
|
762
|
+
firstErrorField.focus();
|
|
763
|
+
|
|
764
|
+
// Scroll into view if needed
|
|
765
|
+
firstErrorField.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// If there are errors that couldn't be attached to fields, show them in a notification
|
|
769
|
+
const unattachedErrors = Object.entries(errors).filter(([fieldName]) => {
|
|
770
|
+
return !this.form.querySelector(`[name="${fieldName}"]`);
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
if (unattachedErrors.length > 0) {
|
|
774
|
+
const errorMessage = unattachedErrors.map(([_, error]) => error).join(', ');
|
|
775
|
+
this.showNotification(errorMessage, 'danger');
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Show notification (toast-style)
|
|
781
|
+
*/
|
|
782
|
+
showNotification(message, type = 'info') {
|
|
783
|
+
const $notification = document.createElement('div');
|
|
784
|
+
$notification.className = `alert alert-${type} alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-5 animation-slide-down`;
|
|
785
|
+
$notification.style.zIndex = '9999';
|
|
786
|
+
$notification.innerHTML = `
|
|
787
|
+
${message}
|
|
788
|
+
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
789
|
+
`;
|
|
790
|
+
|
|
791
|
+
document.body.appendChild($notification);
|
|
792
|
+
|
|
793
|
+
setTimeout(() => {
|
|
794
|
+
$notification.remove();
|
|
795
|
+
}, 5000);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Show single error message
|
|
800
|
+
*/
|
|
801
|
+
showError(messageOrError) {
|
|
802
|
+
// Handle Error objects and strings
|
|
803
|
+
let message;
|
|
804
|
+
|
|
805
|
+
if (messageOrError instanceof Error) {
|
|
806
|
+
message = messageOrError.message;
|
|
807
|
+
console.error('FormManager Error:', messageOrError);
|
|
808
|
+
} else {
|
|
809
|
+
message = messageOrError;
|
|
810
|
+
console.error('FormManager Error:', message);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Always use notification system
|
|
814
|
+
this.showNotification(message, 'danger');
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Clear all errors
|
|
819
|
+
*/
|
|
820
|
+
clearErrors() {
|
|
821
|
+
// Remove field error classes
|
|
822
|
+
this.form.querySelectorAll(`.${this.config.fieldErrorClass}`).forEach(field => {
|
|
823
|
+
field.classList.remove(this.config.fieldErrorClass);
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
// Remove error messages
|
|
827
|
+
this.form.querySelectorAll('.invalid-feedback').forEach(el => el.remove());
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Clear error for a specific field
|
|
832
|
+
*/
|
|
833
|
+
clearFieldError(field) {
|
|
834
|
+
// Remove error class from field
|
|
835
|
+
field.classList.remove(this.config.fieldErrorClass);
|
|
836
|
+
|
|
837
|
+
// Remove error message for this field
|
|
838
|
+
const errorElement = field.parentElement.querySelector('.invalid-feedback') ||
|
|
839
|
+
field.closest('.input-group')?.parentElement.querySelector('.invalid-feedback');
|
|
840
|
+
if (errorElement) {
|
|
841
|
+
errorElement.remove();
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Remove field from state errors
|
|
845
|
+
if (this.state.errors && field.name) {
|
|
846
|
+
delete this.state.errors[field.name];
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Show success message
|
|
852
|
+
*/
|
|
853
|
+
showSuccess(message) {
|
|
854
|
+
// Always use notification system
|
|
855
|
+
this.showNotification(message, 'success');
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Reset form
|
|
860
|
+
*/
|
|
861
|
+
reset() {
|
|
862
|
+
this.form.reset();
|
|
863
|
+
this.state.isDirty = false;
|
|
864
|
+
this.state.data = {};
|
|
865
|
+
this.state.errors = {};
|
|
866
|
+
this.clearErrors();
|
|
867
|
+
this.setFormState('ready');
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Set field value programmatically
|
|
872
|
+
*/
|
|
873
|
+
setFieldValue(fieldName, value) {
|
|
874
|
+
const field = this.form.querySelector(`[name="${fieldName}"]`);
|
|
875
|
+
if (!field) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (field.type === 'checkbox') {
|
|
880
|
+
field.checked = !!value;
|
|
881
|
+
} else if (field.type === 'radio') {
|
|
882
|
+
const radio = this.form.querySelector(`[name="${fieldName}"][value="${value}"]`);
|
|
883
|
+
if (radio) {
|
|
884
|
+
radio.checked = true;
|
|
885
|
+
}
|
|
886
|
+
} else {
|
|
887
|
+
field.value = value;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Update internal state data with nested field support
|
|
891
|
+
this.setNestedValue(this.state.data, fieldName, value);
|
|
892
|
+
|
|
893
|
+
// Trigger change event
|
|
894
|
+
field.dispatchEvent(new Event('change', { bubbles: true }));
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Get field value from state data
|
|
899
|
+
*/
|
|
900
|
+
getValue(fieldName) {
|
|
901
|
+
return this.getNestedValue(this.state.data, fieldName);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Disable specific field
|
|
906
|
+
*/
|
|
907
|
+
disableField(fieldName) {
|
|
908
|
+
const field = this.form.querySelector(`[name="${fieldName}"]`);
|
|
909
|
+
if (field) field.disabled = true;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Enable specific field
|
|
914
|
+
*/
|
|
915
|
+
enableField(fieldName) {
|
|
916
|
+
const field = this.form.querySelector(`[name="${fieldName}"]`);
|
|
917
|
+
if (field) field.disabled = false;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Get current form data
|
|
922
|
+
*/
|
|
923
|
+
getData() {
|
|
924
|
+
return this.collectFormData();
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Flatten nested object to dot notation
|
|
929
|
+
*/
|
|
930
|
+
flattenObject(obj, prefix = '') {
|
|
931
|
+
const flattened = {};
|
|
932
|
+
|
|
933
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
934
|
+
const newKey = prefix ? `${prefix}.${key}` : key;
|
|
935
|
+
|
|
936
|
+
if (value && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)) {
|
|
937
|
+
// Recursively flatten nested objects
|
|
938
|
+
Object.assign(flattened, this.flattenObject(value, newKey));
|
|
939
|
+
} else {
|
|
940
|
+
flattened[newKey] = value;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
return flattened;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Set form values programmatically
|
|
949
|
+
* @param {Object} values - Object with field names as keys and values to set (supports nested objects)
|
|
950
|
+
*/
|
|
951
|
+
setValues(values) {
|
|
952
|
+
if (!values || typeof values !== 'object') return;
|
|
953
|
+
|
|
954
|
+
// Flatten nested objects to dot notation
|
|
955
|
+
const flatValues = this.flattenObject(values);
|
|
956
|
+
|
|
957
|
+
Object.entries(flatValues).forEach(([name, value]) => {
|
|
958
|
+
// Find form elements by name or id
|
|
959
|
+
const element = this.form.querySelector(`[name="${name}"]`) ||
|
|
960
|
+
this.form.querySelector(`#${name}`) ||
|
|
961
|
+
this.form.querySelector(`#${name}-input`) ||
|
|
962
|
+
this.form.querySelector(`#${name}-select`);
|
|
963
|
+
|
|
964
|
+
if (!element) return;
|
|
965
|
+
|
|
966
|
+
// Handle different element types
|
|
967
|
+
if (element.type === 'checkbox') {
|
|
968
|
+
element.checked = !!value;
|
|
969
|
+
} else if (element.type === 'radio') {
|
|
970
|
+
// For radio buttons, find the one with matching value
|
|
971
|
+
const radioGroup = this.form.querySelectorAll(`[name="${name}"]`);
|
|
972
|
+
radioGroup.forEach(radio => {
|
|
973
|
+
radio.checked = radio.value === value;
|
|
974
|
+
});
|
|
975
|
+
} else if (element.tagName === 'SELECT') {
|
|
976
|
+
// For select elements, set the value
|
|
977
|
+
element.value = value;
|
|
978
|
+
// If value doesn't exist in options, try to find by text
|
|
979
|
+
if (!element.value && value) {
|
|
980
|
+
const option = Array.from(element.options).find(opt =>
|
|
981
|
+
opt.text.toLowerCase() === value.toLowerCase()
|
|
982
|
+
);
|
|
983
|
+
if (option) element.value = option.value;
|
|
984
|
+
}
|
|
985
|
+
} else {
|
|
986
|
+
// For text inputs, textareas, etc.
|
|
987
|
+
element.value = value || '';
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Trigger change event to update form state
|
|
991
|
+
const event = new Event('change', { bubbles: true });
|
|
992
|
+
element.dispatchEvent(event);
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
// Update form state
|
|
996
|
+
this.state.data = this.collectFormData();
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Get form state
|
|
1001
|
+
*/
|
|
1002
|
+
getState() {
|
|
1003
|
+
return { ...this.state };
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Check if form is dirty
|
|
1008
|
+
*/
|
|
1009
|
+
isDirty() {
|
|
1010
|
+
return this.state.isDirty;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Check if form is valid
|
|
1015
|
+
*/
|
|
1016
|
+
isValid() {
|
|
1017
|
+
const validation = this.validate(this.collectFormData());
|
|
1018
|
+
return validation.isValid;
|
|
1019
|
+
}
|
|
1020
|
+
}
|