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