@samline/forms 2.3.4 → 2.6.0

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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +8 -7
  3. package/dist/browser/global.d.ts +10 -16
  4. package/dist/browser/global.global.js +328 -51
  5. package/dist/browser/global.global.js.map +1 -1
  6. package/dist/browser/index.cjs +1309 -0
  7. package/dist/browser/index.cjs.map +1 -0
  8. package/dist/browser/index.d.cts +14 -0
  9. package/dist/browser/index.d.ts +14 -0
  10. package/dist/browser/index.js +23 -0
  11. package/dist/browser/index.js.map +1 -0
  12. package/dist/chunk-L4GRKSQ5.js +1270 -0
  13. package/dist/chunk-L4GRKSQ5.js.map +1 -0
  14. package/dist/index.cjs +143 -25
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +4 -158
  17. package/dist/index.d.ts +4 -158
  18. package/dist/index.js +7 -1141
  19. package/dist/index.js.map +1 -1
  20. package/dist/registry-BpqJetAt.d.cts +148 -0
  21. package/dist/registry-BpqJetAt.d.ts +148 -0
  22. package/docs/api/append.md +96 -0
  23. package/docs/api/auto-submit.md +91 -0
  24. package/docs/api/clear-errors.md +69 -0
  25. package/docs/api/destroy.md +65 -0
  26. package/docs/api/disable-auto-submit.md +65 -0
  27. package/docs/api/element.md +78 -0
  28. package/docs/api/form.md +154 -0
  29. package/docs/api/format.md +270 -0
  30. package/docs/api/get-data.md +90 -0
  31. package/docs/api/get-field.md +87 -0
  32. package/docs/api/get-state.md +95 -0
  33. package/docs/api/get-value.md +114 -0
  34. package/docs/api/index.md +54 -0
  35. package/docs/api/observe.md +126 -0
  36. package/docs/api/on-submit.md +90 -0
  37. package/docs/api/parse-form-data.md +94 -0
  38. package/docs/api/prefill.md +89 -0
  39. package/docs/api/regex.md +155 -0
  40. package/docs/api/reset.md +60 -0
  41. package/docs/api/revalidate.md +50 -0
  42. package/docs/api/set-errors.md +97 -0
  43. package/docs/api/set-value.md +120 -0
  44. package/docs/api/subscribe.md +91 -0
  45. package/docs/api/unwatch.md +74 -0
  46. package/docs/api/validate-field-value.md +108 -0
  47. package/docs/api/validate-values.md +100 -0
  48. package/docs/api/validate.md +100 -0
  49. package/docs/api/watch.md +117 -0
  50. package/docs/browser.md +156 -0
  51. package/docs/css-styling.md +187 -0
  52. package/docs/getting-started.md +217 -0
  53. package/docs/options.md +259 -0
  54. package/docs/recipes.md +477 -0
  55. package/docs/typescript.md +576 -0
  56. package/docs/vanilla.md +12 -0
  57. package/package.json +60 -16
@@ -0,0 +1,1270 @@
1
+ // src/core/serialize.ts
2
+ var isEmptyFile = (value) => value instanceof File && value.size === 0 && value.name === "";
3
+ var appendValue = (data, key, value) => {
4
+ if (!Object.prototype.hasOwnProperty.call(data, key)) {
5
+ Object.defineProperty(data, key, {
6
+ value,
7
+ writable: true,
8
+ enumerable: true,
9
+ configurable: true
10
+ });
11
+ return;
12
+ }
13
+ const current = data[key];
14
+ if (Array.isArray(current)) {
15
+ current.push(value);
16
+ return;
17
+ }
18
+ data[key] = [current, value];
19
+ };
20
+ var parseFormData = (formElement, submitter) => {
21
+ const raw = submitter ? new FormData(formElement, submitter) : new FormData(formElement);
22
+ const formData = new FormData();
23
+ const data = {};
24
+ raw.forEach((value, key) => {
25
+ if (isEmptyFile(value)) return;
26
+ formData.append(key, value);
27
+ appendValue(data, key, value);
28
+ });
29
+ return { data, formData };
30
+ };
31
+
32
+ // src/core/validation.ts
33
+ var resolveRule = (rule) => {
34
+ if (rule === void 0) {
35
+ return { value: void 0, message: void 0 };
36
+ }
37
+ if (typeof rule === "object" && rule !== null && "value" in rule) {
38
+ return { value: rule.value, message: rule.message };
39
+ }
40
+ return { value: rule, message: void 0 };
41
+ };
42
+ var hasValue = (value) => {
43
+ if (Array.isArray(value)) return value.length > 0;
44
+ if (typeof value === "string") return value.trim().length > 0;
45
+ return value !== void 0;
46
+ };
47
+ var getValueLength = (value) => {
48
+ if (Array.isArray(value)) return value.length;
49
+ if (typeof value === "string") return value.length;
50
+ return 0;
51
+ };
52
+ var toPatternTarget = (value) => {
53
+ if (typeof value === "string") return value;
54
+ if (Array.isArray(value)) {
55
+ return value.map((entry) => typeof entry === "string" ? entry : entry.name).join(",");
56
+ }
57
+ return "";
58
+ };
59
+ var valuesAreEqual = (left, right) => {
60
+ if (Array.isArray(left) && Array.isArray(right)) {
61
+ return left.length === right.length && left.every((entry, index) => entry === right[index]);
62
+ }
63
+ return left === right;
64
+ };
65
+ var validateFieldValue = (field, value, rules, values) => {
66
+ const errors = [];
67
+ const context = { field, value, values };
68
+ const required = resolveRule(rules.required);
69
+ const minLength = resolveRule(rules.minLength);
70
+ const maxLength = resolveRule(rules.maxLength);
71
+ const pattern = resolveRule(rules.pattern);
72
+ const sameAs = resolveRule(rules.sameAs);
73
+ if (required.value && !hasValue(value)) {
74
+ errors.push(required.message ?? "This field is required.");
75
+ }
76
+ if (minLength.value !== void 0 && getValueLength(value) < minLength.value) {
77
+ errors.push(minLength.message ?? `Minimum length is ${minLength.value}.`);
78
+ }
79
+ if (maxLength.value !== void 0 && getValueLength(value) > maxLength.value) {
80
+ errors.push(maxLength.message ?? `Maximum length is ${maxLength.value}.`);
81
+ }
82
+ if (pattern.value && hasValue(value)) {
83
+ pattern.value.lastIndex = 0;
84
+ const matches = pattern.value.test(toPatternTarget(value));
85
+ pattern.value.lastIndex = 0;
86
+ if (!matches) {
87
+ errors.push(pattern.message ?? "Value does not match the required pattern.");
88
+ }
89
+ }
90
+ if (sameAs.value !== void 0) {
91
+ const otherValue = values[sameAs.value];
92
+ if (hasValue(value) && hasValue(otherValue) && !valuesAreEqual(value, otherValue)) {
93
+ errors.push(sameAs.message ?? `Value must match ${sameAs.value}.`);
94
+ }
95
+ }
96
+ const custom = rules.validate ? Array.isArray(rules.validate) ? rules.validate : [rules.validate] : [];
97
+ for (const validator of custom) {
98
+ const result = validator(context);
99
+ if (typeof result === "string" && result.length > 0) errors.push(result);
100
+ if (result === false) errors.push("Validation failed.");
101
+ }
102
+ return errors;
103
+ };
104
+ var validateValues = (values, schema) => {
105
+ const errors = {};
106
+ for (const [field, rules] of Object.entries(schema)) {
107
+ const messages = validateFieldValue(field, values[field], rules, values);
108
+ if (messages.length > 0) errors[field] = messages;
109
+ }
110
+ return {
111
+ isValid: Object.keys(errors).length === 0,
112
+ errors
113
+ };
114
+ };
115
+
116
+ // src/api/append.ts
117
+ var createAppend = (state2, helpers) => ({ tag, content, class: className, atStart = false }) => {
118
+ if (!state2.element) return null;
119
+ helpers.clearFieldCache();
120
+ if (className) {
121
+ const classSelector = className.trim().split(/\s+/)[0];
122
+ const existing = classSelector ? state2.element.getElementsByClassName(classSelector)[0] : null;
123
+ existing?.remove();
124
+ }
125
+ const node = document.createElement(tag);
126
+ if (className) node.className = className;
127
+ node.innerHTML = content;
128
+ if (atStart && state2.element.firstChild) {
129
+ state2.element.insertBefore(node, state2.element.firstChild);
130
+ } else {
131
+ state2.element.appendChild(node);
132
+ }
133
+ return node;
134
+ };
135
+
136
+ // src/api/auto-submit.ts
137
+ var createAutoSubmit = (state2, helpers) => (next = true) => {
138
+ state2.autoSubmitEnabled = next !== false;
139
+ if (!state2.autoSubmitEnabled && state2.autoSubmitTimer) {
140
+ clearTimeout(state2.autoSubmitTimer);
141
+ state2.autoSubmitTimer = null;
142
+ }
143
+ state2.autoSubmitDebounce = typeof next === "object" && next ? next.debounce ?? 0 : 0;
144
+ helpers.notifySubscribers();
145
+ return state2.api;
146
+ };
147
+
148
+ // src/api/clear-errors.ts
149
+ var createClearErrors = (state2, helpers) => (fields) => {
150
+ if (!fields) {
151
+ state2.manualErrors = {};
152
+ helpers.syncVisualState();
153
+ helpers.notifySubscribers();
154
+ return state2.api;
155
+ }
156
+ for (const field of fields) {
157
+ delete state2.manualErrors[field];
158
+ }
159
+ helpers.syncVisualState(fields);
160
+ helpers.notifySubscribers();
161
+ return state2.api;
162
+ };
163
+
164
+ // src/core/format-helpers.ts
165
+ var FORMATTER_RAW_ATTRIBUTE = "data-formatter-raw-for";
166
+ var cssEscape = (value) => {
167
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
168
+ return CSS.escape(value);
169
+ }
170
+ return value.replace(/([!"#$%&'()*+,./:;<=>?@\[\\\]^`{|}~])/g, "\\$1");
171
+ };
172
+ var findVisibleField = (form2, displayName) => {
173
+ const node = form2.querySelector(
174
+ `[name="${cssEscape(displayName)}"]:not([type="hidden"])`
175
+ );
176
+ if (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) {
177
+ return node;
178
+ }
179
+ return null;
180
+ };
181
+ var findHiddenMirror = (form2, fieldName) => {
182
+ const byAttr = form2.querySelector(
183
+ `[${FORMATTER_RAW_ATTRIBUTE}="${cssEscape(fieldName)}"]`
184
+ );
185
+ if (byAttr) return byAttr;
186
+ const byName = form2.querySelector(
187
+ `input[type="hidden"][name="${cssEscape(fieldName)}"]`
188
+ );
189
+ if (byName) return byName;
190
+ return null;
191
+ };
192
+ var ensureHiddenMirror = (form2, fieldName) => {
193
+ const existing = findHiddenMirror(form2, fieldName);
194
+ if (existing) return existing;
195
+ const input = document.createElement("input");
196
+ input.type = "hidden";
197
+ input.name = fieldName;
198
+ input.setAttribute("aria-hidden", "true");
199
+ input.tabIndex = -1;
200
+ input.setAttribute(FORMATTER_RAW_ATTRIBUTE, fieldName);
201
+ form2.appendChild(input);
202
+ return input;
203
+ };
204
+ var renameVisibleField = (visible, displayName) => {
205
+ if (visible.name === displayName) return;
206
+ visible.name = displayName;
207
+ };
208
+ var restoreVisibleName = (visible, originalName) => {
209
+ visible.name = originalName;
210
+ };
211
+ var applyFormattedValue = (visible, mirror, formatted, raw, inputType) => {
212
+ const rawInput = visible.value;
213
+ const caret = clamp(
214
+ visible.selectionStart ?? rawInput.length,
215
+ 0,
216
+ rawInput.length
217
+ );
218
+ if (mirror && mirror.value !== raw) mirror.value = raw;
219
+ if (rawInput === formatted) {
220
+ const newCaret2 = pullCaretPastLeadingDelimiter(caret, formatted, inputType);
221
+ if (newCaret2 !== caret) restoreCursor(visible, newCaret2);
222
+ return newCaret2;
223
+ }
224
+ visible.value = formatted;
225
+ const newCaret = pullCaretPastLeadingDelimiter(
226
+ computeCursorPosition(caret, rawInput, formatted),
227
+ formatted,
228
+ inputType
229
+ );
230
+ restoreCursor(visible, newCaret);
231
+ return newCaret;
232
+ };
233
+ var pullCaretPastLeadingDelimiter = (caret, value, inputType) => {
234
+ if (!inputType || !inputType.startsWith("delete")) return caret;
235
+ let cursor = caret;
236
+ while (cursor > 0 && isFormatChar(value[cursor - 1])) {
237
+ cursor -= 1;
238
+ }
239
+ return cursor;
240
+ };
241
+ var clamp = (value, min, max) => Math.max(min, Math.min(max, value));
242
+ var computeCursorPosition = (caretPos, rawInput, newValue) => {
243
+ if (newValue.length === 0) return 0;
244
+ if (caretPos >= rawInput.length) return newValue.length;
245
+ const targetNonFormat = countNonFormat(rawInput.slice(0, caretPos));
246
+ if (targetNonFormat === 0) {
247
+ let i = 0;
248
+ while (i < newValue.length && isFormatChar(newValue[i])) i += 1;
249
+ return i;
250
+ }
251
+ let seen = 0;
252
+ for (let i = 0; i < newValue.length; i += 1) {
253
+ if (!isFormatChar(newValue[i])) {
254
+ seen += 1;
255
+ if (seen === targetNonFormat) {
256
+ return Math.min(i + 1, newValue.length);
257
+ }
258
+ }
259
+ }
260
+ return newValue.length;
261
+ };
262
+ var countNonFormat = (value) => {
263
+ let count = 0;
264
+ for (const char of value) {
265
+ if (!isFormatChar(char)) count += 1;
266
+ }
267
+ return count;
268
+ };
269
+ var isFormatChar = (char) => {
270
+ if (!char) return false;
271
+ if (char >= "0" && char <= "9") return false;
272
+ if (char >= "a" && char <= "z" || char >= "A" && char <= "Z") return false;
273
+ return true;
274
+ };
275
+ var restoreCursor = (field, position) => {
276
+ const max = field.value.length;
277
+ const safe = clamp(position, 0, max);
278
+ const apply = () => {
279
+ try {
280
+ field.setSelectionRange(safe, safe);
281
+ } catch {
282
+ }
283
+ };
284
+ if (typeof navigator !== "undefined" && /Android/i.test(navigator.userAgent)) {
285
+ if (typeof requestAnimationFrame === "function") {
286
+ requestAnimationFrame(apply);
287
+ } else {
288
+ setTimeout(apply, 1);
289
+ }
290
+ return;
291
+ }
292
+ apply();
293
+ };
294
+
295
+ // src/core/formatter-loader.ts
296
+ var state = null;
297
+ var MISSING_MESSAGE = "[samline/forms] The `format()` and `formatAll()` methods require the @samline/formatter package. Install it with: npm i @samline/formatter (or pnpm/bun/yarn equivalent).";
298
+ var warned = false;
299
+ var warnOnce = () => {
300
+ if (warned) return;
301
+ warned = true;
302
+ console.error(MISSING_MESSAGE);
303
+ };
304
+ var loadFormatter = () => {
305
+ if (state && state.status === "resolved") {
306
+ if (!state.module) warnOnce();
307
+ return Promise.resolve(state.module);
308
+ }
309
+ if (state && state.status === "pending") {
310
+ return state.promise;
311
+ }
312
+ const promise = (async () => {
313
+ try {
314
+ const mod = await import("@samline/formatter");
315
+ state = { status: "resolved", module: mod };
316
+ return mod;
317
+ } catch {
318
+ warnOnce();
319
+ state = { status: "resolved", module: null };
320
+ return null;
321
+ }
322
+ })();
323
+ state = { status: "pending", promise };
324
+ return promise;
325
+ };
326
+
327
+ // src/api/format.ts
328
+ var UNBOUND = null;
329
+ var registry = /* @__PURE__ */ new WeakMap();
330
+ var getRegistry = (state2) => {
331
+ let bucket = registry.get(state2);
332
+ if (!bucket) {
333
+ bucket = /* @__PURE__ */ new Map();
334
+ registry.set(state2, bucket);
335
+ }
336
+ return bucket;
337
+ };
338
+ var resolveFieldNames = (config) => Array.isArray(config.field) ? config.field : [config.field];
339
+ var addListener = (state2, target, type, handler, useCapture) => {
340
+ if (useCapture) {
341
+ target.addEventListener(type, handler, { capture: true });
342
+ } else {
343
+ target.addEventListener(type, handler);
344
+ }
345
+ const entry = { element: target, type, handler };
346
+ if (useCapture !== void 0) entry.capture = useCapture;
347
+ state2.listeners.push(entry);
348
+ };
349
+ var readInputType = (event) => {
350
+ const candidate = event;
351
+ return typeof candidate.inputType === "string" ? candidate.inputType : void 0;
352
+ };
353
+ var buildAutoInterpretedOptions = (formatOptions) => ({
354
+ ...formatOptions ?? {},
355
+ interpretInputAs: "auto"
356
+ });
357
+ var shouldForceAutoInterpretation = (formatOptions) => formatOptions?.interpretInputAs === void 0;
358
+ var buildHandler = (visible, mirror, formatFn, formatType, formatOptions) => {
359
+ const handler = (event) => {
360
+ if (!visible.isConnected || !mirror.isConnected) return;
361
+ const isVisible = event.target === visible;
362
+ const isMirror = event.target === mirror;
363
+ if (!isVisible && !isMirror) return;
364
+ const rawInput = isMirror ? mirror.value : visible.value;
365
+ const inputType = readInputType(event);
366
+ const sourceOptions = isMirror && shouldForceAutoInterpretation(formatOptions) ? buildAutoInterpretedOptions(formatOptions) : formatOptions;
367
+ const { formatted, raw } = formatFn(rawInput, formatType, sourceOptions);
368
+ if (!formatted && !raw) {
369
+ visible.value = "";
370
+ if (mirror.value !== "") mirror.value = "";
371
+ return;
372
+ }
373
+ applyFormattedValue(visible, mirror, formatted, raw, inputType);
374
+ };
375
+ return handler;
376
+ };
377
+ var applyFormat = async (state2, helpers, config) => {
378
+ if (!state2.element || !state2.api) return;
379
+ if (Array.isArray(config.field) && config.displayField) {
380
+ console.error(
381
+ "[samline/forms] `displayField` can only be used when `field` is a string."
382
+ );
383
+ return;
384
+ }
385
+ const formatType = config.type;
386
+ const formatOptions = config.options;
387
+ const fieldNames = resolveFieldNames(config);
388
+ const bucket = getRegistry(state2);
389
+ const phase1Entries = [];
390
+ for (const fieldName of fieldNames) {
391
+ const displayName = config.displayField ?? `${fieldName}_displayed`;
392
+ const canonicalFields = helpers.getFieldsByName(fieldName);
393
+ const candidates = canonicalFields.filter(
394
+ (f) => f instanceof HTMLInputElement && f.type !== "hidden" || f instanceof HTMLTextAreaElement
395
+ );
396
+ let visible = candidates[0] ?? null;
397
+ if (!visible) {
398
+ visible = findVisibleField(state2.element, displayName);
399
+ }
400
+ if (!visible) continue;
401
+ const existing = bucket.get(fieldName);
402
+ let mirror = findHiddenMirror(state2.element, fieldName);
403
+ const mirrorIsOwned = !mirror;
404
+ if (!mirror) mirror = ensureHiddenMirror(state2.element, fieldName);
405
+ renameVisibleField(visible, displayName);
406
+ if (existing) {
407
+ existing.displayName = displayName;
408
+ existing.mirrorIsOwned = mirrorIsOwned;
409
+ state2.formattedFields.set(fieldName, {
410
+ config,
411
+ displayName,
412
+ mirrorIsOwned
413
+ });
414
+ phase1Entries.push(existing);
415
+ } else {
416
+ const entry = {
417
+ canonicalName: fieldName,
418
+ displayName,
419
+ visible,
420
+ mirror,
421
+ mirrorIsOwned,
422
+ defaultVisible: null,
423
+ defaultRaw: null,
424
+ handler: UNBOUND
425
+ };
426
+ bucket.set(fieldName, entry);
427
+ state2.formattedFields.set(fieldName, {
428
+ config,
429
+ displayName,
430
+ mirrorIsOwned
431
+ });
432
+ phase1Entries.push(entry);
433
+ }
434
+ }
435
+ const formatter = await loadFormatter();
436
+ if (state2.isDestroyed) {
437
+ rollbackPhase1(phase1Entries, bucket, state2.formattedFields);
438
+ return;
439
+ }
440
+ if (!formatter) {
441
+ rollbackPhase1(phase1Entries, bucket, state2.formattedFields);
442
+ return;
443
+ }
444
+ for (const entry of phase1Entries) {
445
+ if (entry.handler !== UNBOUND) {
446
+ const current = entry.visible.value;
447
+ if (current !== "") {
448
+ const { formatted, raw } = formatter.format(current, formatType, formatOptions);
449
+ applyFormattedValue(entry.visible, entry.mirror, formatted, raw);
450
+ }
451
+ continue;
452
+ }
453
+ const handler = buildHandler(
454
+ entry.visible,
455
+ entry.mirror,
456
+ formatter.format,
457
+ formatType,
458
+ formatOptions
459
+ );
460
+ addListener(state2, state2.element, "input", handler, true);
461
+ entry.handler = handler;
462
+ const initial = entry.visible.value;
463
+ if (initial !== "") {
464
+ const initialOptions = shouldForceAutoInterpretation(formatOptions) ? buildAutoInterpretedOptions(formatOptions) : formatOptions;
465
+ const { formatted, raw } = formatter.format(initial, formatType, initialOptions);
466
+ applyFormattedValue(entry.visible, entry.mirror, formatted, raw);
467
+ } else if (entry.mirrorIsOwned && entry.mirror.value !== "") {
468
+ entry.mirror.value = "";
469
+ }
470
+ entry.defaultVisible = entry.visible.value;
471
+ entry.defaultRaw = entry.mirror.value;
472
+ }
473
+ };
474
+ var resetFormattedFields = (state2) => {
475
+ const bucket = registry.get(state2);
476
+ if (!bucket) return;
477
+ for (const entry of bucket.values()) {
478
+ if (entry.defaultVisible !== null) entry.visible.value = entry.defaultVisible;
479
+ if (entry.defaultRaw !== null) entry.mirror.value = entry.defaultRaw;
480
+ }
481
+ };
482
+ var rollbackPhase1 = (entries, bucket, registry2) => {
483
+ for (const entry of entries) {
484
+ if (entry.visible.isConnected) {
485
+ restoreVisibleName(entry.visible, entry.canonicalName);
486
+ }
487
+ if (entry.mirrorIsOwned && entry.mirror.isConnected) {
488
+ entry.mirror.remove();
489
+ }
490
+ bucket.delete(entry.canonicalName);
491
+ registry2.delete(entry.canonicalName);
492
+ }
493
+ };
494
+ var createFormat = (state2, helpers) => (config) => {
495
+ if (state2.isDestroyed) return state2.api;
496
+ if (!state2.element) return state2.api;
497
+ void applyFormat(state2, helpers, config);
498
+ return state2.api;
499
+ };
500
+ var createFormatAll = createFormat;
501
+ var resolveCanonicalForName = (state2, name) => {
502
+ const bucket = registry.get(state2);
503
+ if (!bucket) return null;
504
+ for (const [canonical, entry] of bucket) {
505
+ if (canonical === name || entry.displayName === name) return canonical;
506
+ }
507
+ return null;
508
+ };
509
+ var resolveDisplayNameForName = (state2, name) => {
510
+ const bucket = registry.get(state2);
511
+ if (!bucket) return name;
512
+ for (const [canonical, entry] of bucket) {
513
+ if (canonical === name) return entry.displayName;
514
+ if (entry.displayName === name) return entry.displayName;
515
+ }
516
+ return name;
517
+ };
518
+ var cleanupFormatRegistry = (state2) => {
519
+ if (!state2.element) {
520
+ registry.delete(state2);
521
+ return;
522
+ }
523
+ const bucket = registry.get(state2);
524
+ if (!bucket) {
525
+ state2.element.querySelectorAll(`[${FORMATTER_RAW_ATTRIBUTE}]`).forEach((node) => node.remove());
526
+ return;
527
+ }
528
+ for (const [fieldName, entry] of bucket) {
529
+ if (entry.handler !== UNBOUND) {
530
+ state2.element.removeEventListener("input", entry.handler, true);
531
+ }
532
+ if (entry.visible.isConnected) {
533
+ restoreVisibleName(entry.visible, fieldName);
534
+ }
535
+ if (entry.mirrorIsOwned && entry.mirror.isConnected) {
536
+ entry.mirror.remove();
537
+ }
538
+ bucket.delete(fieldName);
539
+ state2.formattedFields.delete(fieldName);
540
+ }
541
+ state2.element.querySelectorAll(`[${FORMATTER_RAW_ATTRIBUTE}]`).forEach((node) => node.remove());
542
+ };
543
+
544
+ // src/api/destroy.ts
545
+ var createDestroy = (state2, _helpers) => () => {
546
+ if (state2.isDestroyed) return;
547
+ state2.isDestroyed = true;
548
+ for (const listener of state2.listeners) {
549
+ const options = listener.capture ? { capture: true } : void 0;
550
+ if (options) {
551
+ listener.element.removeEventListener(listener.type, listener.handler, options);
552
+ } else {
553
+ listener.element.removeEventListener(listener.type, listener.handler);
554
+ }
555
+ }
556
+ state2.listeners.length = 0;
557
+ if (state2.autoSubmitTimer) {
558
+ clearTimeout(state2.autoSubmitTimer);
559
+ state2.autoSubmitTimer = null;
560
+ }
561
+ state2.mutationObserver?.disconnect();
562
+ state2.mutationObserver = null;
563
+ state2.watchedFields.clear();
564
+ state2.subscribers.clear();
565
+ state2.submitHandlers.clear();
566
+ state2.fieldCache.clear();
567
+ state2.manualErrors = {};
568
+ state2.validationErrors = {};
569
+ cleanupFormatRegistry(state2);
570
+ };
571
+
572
+ // src/api/disable-auto-submit.ts
573
+ var createDisableAutoSubmit = (state2, helpers) => () => {
574
+ state2.autoSubmitEnabled = false;
575
+ if (state2.autoSubmitTimer) {
576
+ clearTimeout(state2.autoSubmitTimer);
577
+ state2.autoSubmitTimer = null;
578
+ }
579
+ helpers.notifySubscribers();
580
+ return state2.api;
581
+ };
582
+
583
+ // src/api/get-data.ts
584
+ var createGetData = (_state, _helpers) => () => {
585
+ if (!_state.element) return { data: {}, formData: new FormData() };
586
+ return parseFormData(_state.element);
587
+ };
588
+
589
+ // src/api/get-field.ts
590
+ var createGetField = (state2, helpers) => (name) => {
591
+ const fields = helpers.getFieldsByName(name);
592
+ if (fields.length === 0) return null;
593
+ return fields.length === 1 ? fields[0] : fields;
594
+ };
595
+
596
+ // src/core/state.ts
597
+ var createEmptyFormState = () => ({
598
+ values: {},
599
+ errors: {},
600
+ filledFields: [],
601
+ isValid: true,
602
+ isValidated: false,
603
+ autoSubmit: false,
604
+ submitCount: 0
605
+ });
606
+ var cloneErrors = (errors) => Object.fromEntries(
607
+ Object.entries(errors).map(([field, messages]) => [field, [...messages]])
608
+ );
609
+ var mergeErrors = (left, right) => {
610
+ const merged = { ...left };
611
+ for (const [field, messages] of Object.entries(right)) {
612
+ merged[field] = merged[field] ? [...merged[field], ...messages] : [...messages];
613
+ }
614
+ return merged;
615
+ };
616
+
617
+ // src/api/get-state.ts
618
+ var createGetState = (state2, helpers) => () => {
619
+ const values = helpers.getValues();
620
+ const errors = cloneErrors(helpers.getMergedErrors());
621
+ const filledFields = helpers.getTrackedFieldNames().filter((name) => {
622
+ const value = values[name];
623
+ if (Array.isArray(value)) return value.length > 0;
624
+ if (typeof value === "string") return value.trim().length > 0;
625
+ return value !== void 0;
626
+ });
627
+ return {
628
+ ...createEmptyFormState(),
629
+ values,
630
+ errors,
631
+ filledFields,
632
+ isValid: Object.keys(errors).length === 0,
633
+ isValidated: state2.isValidated,
634
+ autoSubmit: state2.autoSubmitEnabled,
635
+ submitCount: state2.submitCount
636
+ };
637
+ };
638
+
639
+ // src/core/dom.ts
640
+ var isFormFieldElement = (value) => value instanceof HTMLInputElement || value instanceof HTMLSelectElement || value instanceof HTMLTextAreaElement;
641
+ var isArraySyntax = (field) => typeof field.name === "string" && field.name.endsWith("[]");
642
+ var resolveFormElement = (target) => {
643
+ if (!target) return null;
644
+ if (target instanceof HTMLFormElement) return target;
645
+ if (typeof target === "string") {
646
+ const node = document.getElementById(target);
647
+ return node instanceof HTMLFormElement ? node : null;
648
+ }
649
+ if ("current" in target) {
650
+ return target.current instanceof HTMLFormElement ? target.current : null;
651
+ }
652
+ return null;
653
+ };
654
+ var getNamedFields = (form2) => Array.from(form2.elements).filter(
655
+ (element) => isFormFieldElement(element) && Boolean(element.name)
656
+ );
657
+ var queryNamedFields = (form2, name) => getNamedFields(form2).filter((field) => field.name === name);
658
+ var isFieldElement = (value) => isFormFieldElement(value);
659
+ var isFieldFilled = (field) => {
660
+ if (field instanceof HTMLInputElement) {
661
+ if (field.type === "checkbox" || field.type === "radio") return field.checked;
662
+ if (field.type === "file") return Boolean(field.files && field.files.length > 0);
663
+ return field.value.trim() !== "";
664
+ }
665
+ return field.value !== "";
666
+ };
667
+ var applyBooleanAttribute = (field, attribute, enabled) => {
668
+ if (enabled) {
669
+ field.setAttribute(attribute, "");
670
+ } else {
671
+ field.removeAttribute(attribute);
672
+ }
673
+ };
674
+ var readFieldValue = (fields) => {
675
+ if (fields.length === 0) return void 0;
676
+ const first = fields[0];
677
+ if (!first) return void 0;
678
+ if (isArraySyntax(first) && fields.length > 1) {
679
+ if (first instanceof HTMLSelectElement || first instanceof HTMLTextAreaElement) {
680
+ return fields.map((f) => f.value);
681
+ }
682
+ if (first instanceof HTMLInputElement) {
683
+ if (first.type === "radio") {
684
+ const checked = fields.find(
685
+ (f) => f instanceof HTMLInputElement && f.checked
686
+ );
687
+ return checked?.value ?? "";
688
+ }
689
+ if (first.type === "checkbox") {
690
+ return fields.filter(
691
+ (f) => f instanceof HTMLInputElement && f.checked
692
+ ).map((f) => f.value);
693
+ }
694
+ if (first.type === "file") {
695
+ return fields.flatMap(
696
+ (f) => f instanceof HTMLInputElement && f.files ? Array.from(f.files) : []
697
+ );
698
+ }
699
+ }
700
+ return fields.map((f) => f.value);
701
+ }
702
+ if (first instanceof HTMLSelectElement && first.multiple) {
703
+ return Array.from(first.selectedOptions, (option) => option.value);
704
+ }
705
+ if (first instanceof HTMLSelectElement || first instanceof HTMLTextAreaElement) {
706
+ return first.value;
707
+ }
708
+ if (first instanceof HTMLInputElement) {
709
+ if (first.type === "radio") {
710
+ const checked = fields.find(
711
+ (f) => f instanceof HTMLInputElement && f.checked
712
+ );
713
+ return checked?.value ?? "";
714
+ }
715
+ if (first.type === "checkbox") {
716
+ const values = fields.filter(
717
+ (f) => f instanceof HTMLInputElement && f.checked
718
+ ).map((f) => f.value);
719
+ return values.length > 1 ? values : values[0] ?? "";
720
+ }
721
+ if (first.type === "file") {
722
+ return first.files ? Array.from(first.files) : [];
723
+ }
724
+ }
725
+ return first.value;
726
+ };
727
+ var writeFieldValue = (fields, value) => {
728
+ const normalizedArray = Array.isArray(value) ? value.map((item) => String(item)) : null;
729
+ const normalizedValue = value === void 0 || value === null ? "" : String(value);
730
+ const distributeArray = fields.length > 0 && fields[0] !== void 0 && isArraySyntax(fields[0]) && normalizedArray !== null;
731
+ for (let index = 0; index < fields.length; index++) {
732
+ const field = fields[index];
733
+ if (!field) continue;
734
+ if (field instanceof HTMLInputElement) {
735
+ if (field.type === "checkbox") {
736
+ field.checked = normalizedArray ? normalizedArray.includes(field.value) : normalizedValue === field.value;
737
+ continue;
738
+ }
739
+ if (field.type === "radio") {
740
+ field.checked = field.value === normalizedValue;
741
+ continue;
742
+ }
743
+ if (field.type === "file") {
744
+ if (Array.isArray(value) && value.length === 0) field.value = "";
745
+ continue;
746
+ }
747
+ if (distributeArray && normalizedArray) {
748
+ field.value = normalizedArray[index] ?? "";
749
+ continue;
750
+ }
751
+ field.value = normalizedValue;
752
+ continue;
753
+ }
754
+ if (field instanceof HTMLSelectElement && field.multiple) {
755
+ const selectedValues = normalizedArray ?? [normalizedValue];
756
+ for (const option of field.options) {
757
+ option.selected = selectedValues.includes(option.value);
758
+ }
759
+ continue;
760
+ }
761
+ if (distributeArray && normalizedArray) {
762
+ field.value = normalizedArray[index] ?? "";
763
+ continue;
764
+ }
765
+ field.value = normalizedValue;
766
+ }
767
+ };
768
+ var clearAttributes = (form2, attributes) => {
769
+ for (const attribute of attributes) {
770
+ getNamedFields(form2).forEach((field) => field.removeAttribute(attribute));
771
+ }
772
+ };
773
+ var submitForm = (form2) => {
774
+ if (typeof form2.requestSubmit === "function") {
775
+ form2.requestSubmit();
776
+ return;
777
+ }
778
+ const submitButton = form2.querySelector(
779
+ 'button[type="submit"], input[type="submit"]'
780
+ );
781
+ if (submitButton) {
782
+ submitButton.click();
783
+ return;
784
+ }
785
+ form2.submit();
786
+ };
787
+
788
+ // src/api/get-value.ts
789
+ var createGetValue = (state2, helpers) => (name) => readFieldValue(helpers.getFieldsByName(name));
790
+
791
+ // src/api/observe.ts
792
+ var createObserve = (state2, helpers) => (field, callback) => {
793
+ const callbacks = state2.watchedFields.get(field) ?? /* @__PURE__ */ new Set();
794
+ callbacks.add(callback);
795
+ state2.watchedFields.set(field, callbacks);
796
+ if (state2.element) {
797
+ callback(state2.api.getValue(field), state2.api.getField(field), state2.element, state2.api.getState());
798
+ }
799
+ return () => {
800
+ const current = state2.watchedFields.get(field);
801
+ current?.delete(callback);
802
+ if (current && current.size === 0) {
803
+ state2.watchedFields.delete(field);
804
+ }
805
+ };
806
+ };
807
+
808
+ // src/api/on-submit.ts
809
+ var createOnSubmit = (state2, _helpers) => (callback, preventDefault = true) => {
810
+ state2.submitHandlers.add({ callback, preventDefault });
811
+ return state2.api;
812
+ };
813
+
814
+ // src/api/prefill.ts
815
+ var createPrefill = (state2, _helpers) => (fieldName) => {
816
+ if (!state2.element) return state2.api;
817
+ const params = new URLSearchParams(window.location.search);
818
+ params.forEach((value, key) => {
819
+ if (fieldName && fieldName !== key) return;
820
+ state2.api.setValue(key, value);
821
+ });
822
+ return state2.api;
823
+ };
824
+
825
+ // src/api/reset.ts
826
+ var createReset = (state2, helpers) => () => {
827
+ if (!state2.element) return state2.api;
828
+ state2.element.reset();
829
+ resetFormattedFields(state2);
830
+ state2.manualErrors = {};
831
+ state2.validationErrors = {};
832
+ clearAttributes(state2.element, [
833
+ state2.attributes.error,
834
+ state2.attributes.filled,
835
+ "aria-invalid"
836
+ ]);
837
+ if (state2.isValidated) helpers.syncVisualState();
838
+ helpers.notifySubscribers();
839
+ return state2.api;
840
+ };
841
+
842
+ // src/api/validate.ts
843
+ var createValidate = (state2, helpers) => (fields) => {
844
+ state2.isValidated = true;
845
+ helpers.syncVisualState(fields);
846
+ return helpers.validateNames(fields);
847
+ };
848
+
849
+ // src/api/revalidate.ts
850
+ var createRevalidate = createValidate;
851
+
852
+ // src/api/set-errors.ts
853
+ var createSetErrors = (state2, helpers) => (fields) => {
854
+ if (Array.isArray(fields)) {
855
+ state2.manualErrors = {
856
+ ...state2.manualErrors,
857
+ ...Object.fromEntries(fields.map((field) => [field, ["Invalid value."]]))
858
+ };
859
+ } else {
860
+ state2.manualErrors = {
861
+ ...state2.manualErrors,
862
+ ...cloneErrors(fields)
863
+ };
864
+ }
865
+ helpers.syncVisualState(Array.isArray(fields) ? fields : Object.keys(fields));
866
+ helpers.notifySubscribers();
867
+ return state2.api;
868
+ };
869
+
870
+ // src/api/set-value.ts
871
+ var createSetValue = (state2, helpers) => (name, value) => {
872
+ const fields = helpers.getFieldsByName(name);
873
+ if (fields.length === 0) return state2.api;
874
+ writeFieldValue(fields, value);
875
+ const firstField = fields[0];
876
+ if (!firstField) return state2.api;
877
+ firstField.dispatchEvent(new Event("input", { bubbles: true }));
878
+ return state2.api;
879
+ };
880
+
881
+ // src/api/subscribe.ts
882
+ var createSubscribe = (state2, _helpers) => (listener) => {
883
+ state2.subscribers.add(listener);
884
+ listener(state2.api.getState());
885
+ return () => {
886
+ state2.subscribers.delete(listener);
887
+ };
888
+ };
889
+
890
+ // src/api/unwatch.ts
891
+ var createUnwatch = (state2, _helpers) => (field, callback) => {
892
+ if (!field) {
893
+ state2.watchedFields.clear();
894
+ return state2.api;
895
+ }
896
+ if (!callback) {
897
+ state2.watchedFields.delete(field);
898
+ return state2.api;
899
+ }
900
+ const callbacks = state2.watchedFields.get(field);
901
+ callbacks?.delete(callback);
902
+ if (callbacks && callbacks.size === 0) {
903
+ state2.watchedFields.delete(field);
904
+ }
905
+ return state2.api;
906
+ };
907
+
908
+ // src/api/watch.ts
909
+ var createWatch = (state2, helpers) => (field, callback) => {
910
+ const observe = createObserve(state2, helpers);
911
+ observe(field, callback);
912
+ return state2.api;
913
+ };
914
+
915
+ // src/core/controller.ts
916
+ var DEFAULT_ATTRIBUTES = {
917
+ filled: "css-filled",
918
+ error: "css-error"
919
+ };
920
+ var DEFAULT_OPTIONS = {
921
+ autoValidate: true,
922
+ clearErrorsOnSubmit: true,
923
+ clearManualErrorsOnChange: true
924
+ };
925
+ var createFormController = (target, options = {}) => {
926
+ const element = resolveFormElement(target);
927
+ const attributes = { ...DEFAULT_ATTRIBUTES, ...options.attributes };
928
+ const normalizedOptions = {
929
+ ...DEFAULT_OPTIONS,
930
+ ...options,
931
+ attributes
932
+ };
933
+ const state2 = {
934
+ element,
935
+ options: normalizedOptions,
936
+ attributes,
937
+ validators: normalizedOptions.validators ?? {},
938
+ watchedFields: /* @__PURE__ */ new Map(),
939
+ subscribers: /* @__PURE__ */ new Set(),
940
+ submitHandlers: /* @__PURE__ */ new Set(),
941
+ fieldCache: /* @__PURE__ */ new Map(),
942
+ manualErrors: {},
943
+ validationErrors: {},
944
+ isValidated: Boolean(normalizedOptions.autoValidate),
945
+ autoSubmitEnabled: false,
946
+ autoSubmitDebounce: 0,
947
+ submitCount: 0,
948
+ autoSubmitTimer: null,
949
+ isDestroyed: false,
950
+ listeners: [],
951
+ mutationObserver: null,
952
+ formattedFields: /* @__PURE__ */ new Map(),
953
+ api: null
954
+ };
955
+ const validationDependents = /* @__PURE__ */ new Map();
956
+ for (const [field, rules] of Object.entries(state2.validators)) {
957
+ const sameAs = rules.sameAs;
958
+ if (sameAs === void 0) continue;
959
+ const source = typeof sameAs === "string" ? sameAs : sameAs.value;
960
+ const dependents = validationDependents.get(source) ?? /* @__PURE__ */ new Set();
961
+ dependents.add(field);
962
+ validationDependents.set(source, dependents);
963
+ }
964
+ const notifySubscribers = () => {
965
+ if (!state2.api) return;
966
+ const snapshot = state2.api.getState();
967
+ state2.subscribers.forEach((listener) => listener(snapshot));
968
+ };
969
+ const addListener2 = (node, type, handler, capture) => {
970
+ if (capture) {
971
+ node.addEventListener(type, handler, { capture: true });
972
+ } else {
973
+ node.addEventListener(type, handler);
974
+ }
975
+ const entry = { element: node, type, handler };
976
+ if (capture !== void 0) entry.capture = capture;
977
+ state2.listeners.push(entry);
978
+ };
979
+ const clearFieldCache = () => state2.fieldCache.clear();
980
+ const getFieldsByName = (name) => {
981
+ if (!state2.element) return [];
982
+ const fields = queryNamedFields(state2.element, name);
983
+ state2.fieldCache.set(name, fields);
984
+ return fields;
985
+ };
986
+ const getTrackedFieldNames = () => {
987
+ if (!state2.element) return Object.keys(state2.validators);
988
+ const names = new Set(Object.keys(state2.validators));
989
+ for (const field of getNamedFields(state2.element)) {
990
+ names.add(field.name);
991
+ }
992
+ for (const [canonical, entry] of state2.formattedFields) {
993
+ names.add(canonical);
994
+ if (entry.displayName && entry.displayName !== canonical) {
995
+ names.add(entry.displayName);
996
+ }
997
+ }
998
+ return Array.from(names);
999
+ };
1000
+ const getValues = () => {
1001
+ const values = {};
1002
+ for (const name of getTrackedFieldNames()) {
1003
+ values[name] = readFieldValue(getFieldsByName(name));
1004
+ }
1005
+ return values;
1006
+ };
1007
+ const getMergedErrors = () => mergeErrors(state2.validationErrors, state2.manualErrors);
1008
+ const syncVisualState = (names) => {
1009
+ if (!state2.element) return;
1010
+ const sourceNames = names ?? getTrackedFieldNames();
1011
+ const errors = getMergedErrors();
1012
+ for (const name of sourceNames) {
1013
+ const displayName = resolveDisplayNameForName(state2, name);
1014
+ const canonical = resolveCanonicalForName(state2, name) ?? name;
1015
+ const fields = getFieldsByName(displayName);
1016
+ const hasError = Boolean(errors[canonical]?.length);
1017
+ for (const field of fields) {
1018
+ applyBooleanAttribute(field, attributes.filled, isFieldFilled(field));
1019
+ applyBooleanAttribute(field, attributes.error, hasError);
1020
+ if (hasError) field.setAttribute("aria-invalid", "true");
1021
+ else field.removeAttribute("aria-invalid");
1022
+ }
1023
+ }
1024
+ };
1025
+ void readFieldValue;
1026
+ void writeFieldValue;
1027
+ const validateNames = (names) => {
1028
+ const targetNames = names ?? Object.keys(state2.validators);
1029
+ const values = getValues();
1030
+ const nextValidationErrors = names ? cloneErrors(state2.validationErrors) : {};
1031
+ for (const name of targetNames) {
1032
+ const rules = state2.validators[name];
1033
+ if (!rules) {
1034
+ delete nextValidationErrors[name];
1035
+ continue;
1036
+ }
1037
+ const messages = validateFieldValue(name, values[name], rules, values);
1038
+ if (messages.length > 0) nextValidationErrors[name] = messages;
1039
+ else delete nextValidationErrors[name];
1040
+ }
1041
+ state2.validationErrors = nextValidationErrors;
1042
+ syncVisualState(names);
1043
+ return {
1044
+ isValid: Object.keys(getMergedErrors()).length === 0,
1045
+ errors: cloneErrors(getMergedErrors())
1046
+ };
1047
+ };
1048
+ const getAffectedValidationNames = (name) => {
1049
+ const affected = [];
1050
+ const pending = [name];
1051
+ const visited = /* @__PURE__ */ new Set();
1052
+ for (let index = 0; index < pending.length; index += 1) {
1053
+ const current = pending[index];
1054
+ if (visited.has(current)) continue;
1055
+ visited.add(current);
1056
+ affected.push(current);
1057
+ for (const dependent of validationDependents.get(current) ?? []) {
1058
+ if (!visited.has(dependent)) pending.push(dependent);
1059
+ }
1060
+ }
1061
+ return affected;
1062
+ };
1063
+ const emitFieldWatchers = (name) => {
1064
+ if (!state2.element || !state2.api) return;
1065
+ const callbacks = state2.watchedFields.get(name);
1066
+ if (!callbacks || callbacks.size === 0) return;
1067
+ const value = state2.api.getValue(name);
1068
+ const fieldElement = state2.api.getField(name);
1069
+ const snapshot = state2.api.getState();
1070
+ callbacks.forEach((callback) => callback(value, fieldElement, state2.element, snapshot));
1071
+ };
1072
+ const scheduleAutoSubmit = () => {
1073
+ if (!state2.element || !state2.autoSubmitEnabled) return;
1074
+ if (state2.autoSubmitTimer) clearTimeout(state2.autoSubmitTimer);
1075
+ if (state2.autoSubmitDebounce > 0) {
1076
+ state2.autoSubmitTimer = setTimeout(
1077
+ () => submitForm(state2.element),
1078
+ state2.autoSubmitDebounce
1079
+ );
1080
+ return;
1081
+ }
1082
+ submitForm(state2.element);
1083
+ };
1084
+ const helpers = {
1085
+ notifySubscribers,
1086
+ clearFieldCache,
1087
+ getFieldsByName,
1088
+ getTrackedFieldNames,
1089
+ getValues,
1090
+ getMergedErrors,
1091
+ syncVisualState,
1092
+ validateNames,
1093
+ emitFieldWatchers,
1094
+ scheduleAutoSubmit
1095
+ };
1096
+ const handleDelegatedEvent = (event) => {
1097
+ const target2 = event.target;
1098
+ if (!(target2 instanceof Element) || !isFieldElement(target2)) return;
1099
+ const name = target2.name;
1100
+ if (!name) return;
1101
+ clearFieldCache();
1102
+ const canonical = resolveCanonicalForName(state2, name) ?? name;
1103
+ const displayName = resolveDisplayNameForName(state2, name);
1104
+ if (normalizedOptions.clearManualErrorsOnChange) {
1105
+ delete state2.manualErrors[canonical];
1106
+ }
1107
+ syncVisualState([displayName]);
1108
+ const affectedValidationNames = getAffectedValidationNames(canonical);
1109
+ if (state2.isValidated && affectedValidationNames.some((field) => state2.validators[field] !== void 0)) {
1110
+ validateNames(affectedValidationNames);
1111
+ }
1112
+ emitFieldWatchers(canonical);
1113
+ if (canonical !== name) emitFieldWatchers(name);
1114
+ notifySubscribers();
1115
+ scheduleAutoSubmit();
1116
+ };
1117
+ const handleExternalDelegatedEvent = (event) => {
1118
+ const target2 = event.target;
1119
+ if (!(target2 instanceof Element) || !isFieldElement(target2)) return;
1120
+ if (target2.form !== state2.element || state2.element?.contains(target2)) return;
1121
+ handleDelegatedEvent(event);
1122
+ };
1123
+ const handleSubmitEvent = (event) => {
1124
+ if (!state2.element || state2.isDestroyed || !state2.api) return;
1125
+ if (normalizedOptions.clearErrorsOnSubmit) state2.manualErrors = {};
1126
+ const validation = state2.api.validate();
1127
+ const handlers = Array.from(state2.submitHandlers);
1128
+ const shouldPrevent = validation.isValid ? handlers.some((handler) => handler.preventDefault) : true;
1129
+ if (shouldPrevent) event.preventDefault();
1130
+ state2.submitCount += 1;
1131
+ notifySubscribers();
1132
+ if (!validation.isValid) {
1133
+ for (const name of Object.keys(validation.errors)) {
1134
+ const displayName = resolveDisplayNameForName(state2, name);
1135
+ const field = getFieldsByName(displayName).find((candidate2) => {
1136
+ if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
1137
+ return !(candidate2 instanceof HTMLInputElement && candidate2.type === "hidden");
1138
+ });
1139
+ if (field) {
1140
+ field.focus();
1141
+ break;
1142
+ }
1143
+ }
1144
+ return;
1145
+ }
1146
+ const candidate = event;
1147
+ const submitter = candidate.submitter instanceof HTMLElement ? candidate.submitter : null;
1148
+ const { data, formData } = parseFormData(state2.element, submitter);
1149
+ const snapshot = state2.api.getState();
1150
+ handlers.forEach(
1151
+ (handler) => handler.callback(state2.element, data, formData, snapshot)
1152
+ );
1153
+ };
1154
+ const startMutationObserver = () => {
1155
+ if (!state2.element || typeof MutationObserver === "undefined") return;
1156
+ state2.mutationObserver = new MutationObserver(() => {
1157
+ clearFieldCache();
1158
+ if (state2.isValidated) {
1159
+ syncVisualState();
1160
+ validateNames();
1161
+ }
1162
+ notifySubscribers();
1163
+ });
1164
+ state2.mutationObserver.observe(state2.element, {
1165
+ childList: true,
1166
+ subtree: true,
1167
+ attributes: true,
1168
+ attributeFilter: ["name", "type"]
1169
+ });
1170
+ };
1171
+ const api = {
1172
+ get element() {
1173
+ return state2.element;
1174
+ },
1175
+ get f() {
1176
+ return state2.element;
1177
+ },
1178
+ get options() {
1179
+ return state2.options;
1180
+ },
1181
+ onSubmit: createOnSubmit(state2, helpers),
1182
+ watch: createWatch(state2, helpers),
1183
+ observe: createObserve(state2, helpers),
1184
+ unwatch: createUnwatch(state2, helpers),
1185
+ subscribe: createSubscribe(state2, helpers),
1186
+ prefill: createPrefill(state2, helpers),
1187
+ append: createAppend(state2, helpers),
1188
+ setErrors: createSetErrors(state2, helpers),
1189
+ clearErrors: createClearErrors(state2, helpers),
1190
+ setValue: createSetValue(state2, helpers),
1191
+ getValue: createGetValue(state2, helpers),
1192
+ getField: createGetField(state2, helpers),
1193
+ validate: createValidate(state2, helpers),
1194
+ revalidate: createRevalidate(state2, helpers),
1195
+ reset: createReset(state2, helpers),
1196
+ autoSubmit: createAutoSubmit(state2, helpers),
1197
+ disableAutoSubmit: createDisableAutoSubmit(state2, helpers),
1198
+ getData: createGetData(state2, helpers),
1199
+ getState: createGetState(state2, helpers),
1200
+ format: createFormat(state2, helpers),
1201
+ formatAll: createFormatAll(state2, helpers),
1202
+ destroy: createDestroy(state2, helpers)
1203
+ };
1204
+ state2.api = api;
1205
+ if (state2.element) {
1206
+ addListener2(state2.element, "input", handleDelegatedEvent);
1207
+ addListener2(document, "input", handleExternalDelegatedEvent);
1208
+ addListener2(state2.element, "submit", handleSubmitEvent);
1209
+ startMutationObserver();
1210
+ }
1211
+ if (normalizedOptions.autoSubmit) {
1212
+ api.autoSubmit(normalizedOptions.autoSubmit);
1213
+ }
1214
+ if (normalizedOptions.formats) {
1215
+ for (const config of Object.values(normalizedOptions.formats)) {
1216
+ api.format(config);
1217
+ }
1218
+ }
1219
+ if (state2.isValidated) {
1220
+ syncVisualState();
1221
+ validateNames();
1222
+ }
1223
+ return api;
1224
+ };
1225
+
1226
+ // src/api/form.ts
1227
+ var form = (target, options = {}) => createFormController(target, options);
1228
+
1229
+ // src/browser/registry.ts
1230
+ var available = {};
1231
+ var newForm = (input) => {
1232
+ const { id, options } = input;
1233
+ if (!id) {
1234
+ console.error("Form ID is required");
1235
+ return;
1236
+ }
1237
+ available[id]?.destroy();
1238
+ const controller = form(id, { ...options });
1239
+ available[id] = controller;
1240
+ return controller;
1241
+ };
1242
+ var destroyForm = (id) => {
1243
+ if (!id) {
1244
+ console.error("Form ID is required");
1245
+ return;
1246
+ }
1247
+ const controller = available[id];
1248
+ if (controller) {
1249
+ controller.destroy();
1250
+ delete available[id];
1251
+ } else {
1252
+ console.warn(`Form with ID ${id} not found`);
1253
+ }
1254
+ };
1255
+ var browser = {
1256
+ form,
1257
+ newForm,
1258
+ destroyForm,
1259
+ available
1260
+ };
1261
+
1262
+ export {
1263
+ parseFormData,
1264
+ validateFieldValue,
1265
+ validateValues,
1266
+ createFormController,
1267
+ form,
1268
+ browser
1269
+ };
1270
+ //# sourceMappingURL=chunk-L4GRKSQ5.js.map