@samline/forms 2.7.2 → 2.9.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 (58) hide show
  1. package/CHANGELOG.md +316 -0
  2. package/README.md +119 -119
  3. package/dist/browser/global.d.ts +50 -16
  4. package/dist/browser/global.global.js +12 -6188
  5. package/dist/browser/global.global.js.map +1 -1
  6. package/dist/browser/index.cjs +340 -84
  7. package/dist/browser/index.cjs.map +1 -1
  8. package/dist/browser/index.d.cts +1 -1
  9. package/dist/browser/index.d.ts +1 -1
  10. package/dist/browser/index.js +1 -1
  11. package/dist/{chunk-MMHX34WB.js → chunk-6MA5TF6R.js} +341 -85
  12. package/dist/chunk-6MA5TF6R.js.map +1 -0
  13. package/dist/index.cjs +340 -84
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +2 -2
  16. package/dist/index.d.ts +2 -2
  17. package/dist/index.js +1 -1
  18. package/dist/{registry-GzopYPCR.d.ts → registry-CPe_lf8-.d.cts} +50 -4
  19. package/dist/{registry-GzopYPCR.d.cts → registry-CPe_lf8-.d.ts} +50 -4
  20. package/package.json +39 -8
  21. package/dist/chunk-MMHX34WB.js.map +0 -1
  22. package/docs/api/add-cleanup.md +0 -52
  23. package/docs/api/append.md +0 -96
  24. package/docs/api/auto-submit.md +0 -91
  25. package/docs/api/clear-errors.md +0 -69
  26. package/docs/api/destroy.md +0 -80
  27. package/docs/api/disable-auto-submit.md +0 -65
  28. package/docs/api/element.md +0 -78
  29. package/docs/api/form.md +0 -156
  30. package/docs/api/format.md +0 -270
  31. package/docs/api/get-data.md +0 -90
  32. package/docs/api/get-field.md +0 -87
  33. package/docs/api/get-state.md +0 -111
  34. package/docs/api/get-value.md +0 -114
  35. package/docs/api/index.md +0 -55
  36. package/docs/api/observe.md +0 -126
  37. package/docs/api/on-submit.md +0 -94
  38. package/docs/api/parse-form-data.md +0 -94
  39. package/docs/api/prefill.md +0 -89
  40. package/docs/api/regex.md +0 -155
  41. package/docs/api/reset.md +0 -61
  42. package/docs/api/revalidate.md +0 -52
  43. package/docs/api/set-errors.md +0 -97
  44. package/docs/api/set-value.md +0 -120
  45. package/docs/api/subscribe.md +0 -107
  46. package/docs/api/unwatch.md +0 -74
  47. package/docs/api/validate-field-value.md +0 -144
  48. package/docs/api/validate-values.md +0 -103
  49. package/docs/api/validate.md +0 -102
  50. package/docs/api/watch.md +0 -117
  51. package/docs/browser.md +0 -156
  52. package/docs/css-styling.md +0 -191
  53. package/docs/getting-started.md +0 -222
  54. package/docs/options.md +0 -326
  55. package/docs/recipes.md +0 -531
  56. package/docs/typescript.md +0 -615
  57. package/docs/vanilla.md +0 -12
  58. /package/{LICENSE → LICENSE.md} +0 -0
package/dist/index.cjs CHANGED
@@ -41,7 +41,7 @@ module.exports = __toCommonJS(src_exports);
41
41
 
42
42
  // src/api/append.ts
43
43
  var createAppend = (state2, helpers) => ({ tag, content, class: className, atStart = false }) => {
44
- if (!state2.element) return null;
44
+ if (!state2.element || state2.isDestroyed) return null;
45
45
  helpers.clearFieldCache();
46
46
  if (className) {
47
47
  const classSelector = className.trim().split(/\s+/)[0];
@@ -77,6 +77,7 @@ var createAddCleanup = (state2, _helpers) => (cleanup) => {
77
77
 
78
78
  // src/api/auto-submit.ts
79
79
  var createAutoSubmit = (state2, helpers) => (next = true) => {
80
+ if (state2.isDestroyed) return state2.api;
80
81
  state2.autoSubmitEnabled = next !== false;
81
82
  if (!state2.autoSubmitEnabled && state2.autoSubmitTimer) {
82
83
  clearTimeout(state2.autoSubmitTimer);
@@ -89,6 +90,7 @@ var createAutoSubmit = (state2, helpers) => (next = true) => {
89
90
 
90
91
  // src/api/clear-errors.ts
91
92
  var createClearErrors = (state2, helpers) => (fields) => {
93
+ if (state2.isDestroyed) return state2.api;
92
94
  if (!fields) {
93
95
  state2.manualErrors = {};
94
96
  helpers.syncVisualState();
@@ -215,6 +217,7 @@ var isFormatChar = (char) => {
215
217
  return true;
216
218
  };
217
219
  var restoreCursor = (field, position) => {
220
+ if (field.ownerDocument.activeElement !== field) return;
218
221
  const max = field.value.length;
219
222
  const safe = clamp(position, 0, max);
220
223
  const apply = () => {
@@ -517,6 +520,7 @@ var createDestroy = (state2, _helpers) => () => {
517
520
  }
518
521
  }
519
522
  state2.watchedFields.clear();
523
+ state2.groupWatchers.clear();
520
524
  state2.subscribers.clear();
521
525
  state2.submitHandlers.clear();
522
526
  state2.fieldCache.clear();
@@ -609,7 +613,7 @@ var mergeErrors = (left, right) => {
609
613
  var createGetState = (state2, helpers) => () => {
610
614
  const values = helpers.getValues();
611
615
  const errors = cloneErrors(helpers.getMergedErrors());
612
- const filledFields = helpers.getTrackedFieldNames().filter((name) => {
616
+ const filledFields = Object.keys(values).filter((name) => {
613
617
  const value = values[name];
614
618
  if (Array.isArray(value)) return value.length > 0;
615
619
  if (typeof value === "string") return value.trim().length > 0;
@@ -744,9 +748,9 @@ var writeFieldValue = (fields, value) => {
744
748
  continue;
745
749
  }
746
750
  if (field instanceof HTMLSelectElement && field.multiple) {
747
- const selectedValues = normalizedArray ?? [normalizedValue];
751
+ const selectedValues2 = normalizedArray ?? [normalizedValue];
748
752
  for (const option of field.options) {
749
- option.selected = selectedValues.includes(option.value);
753
+ option.selected = selectedValues2.includes(option.value);
750
754
  }
751
755
  continue;
752
756
  }
@@ -781,7 +785,7 @@ var submitForm = (form2) => {
781
785
  var createGetValue = (state2, helpers) => (name) => readFieldValue(helpers.getFieldsByName(name));
782
786
 
783
787
  // src/api/observe.ts
784
- var createObserve = (state2, helpers) => (field, callback) => {
788
+ var observeField = (state2, field, callback) => {
785
789
  const callbacks = state2.watchedFields.get(field) ?? /* @__PURE__ */ new Set();
786
790
  callbacks.add(callback);
787
791
  state2.watchedFields.set(field, callbacks);
@@ -796,47 +800,72 @@ var createObserve = (state2, helpers) => (field, callback) => {
796
800
  }
797
801
  };
798
802
  };
803
+ var observeFields = (state2, fields, callback) => {
804
+ const names = Array.from(new Set(fields));
805
+ if (names.length === 0) return () => void 0;
806
+ const entry = { fields: names, callback };
807
+ state2.groupWatchers.add(entry);
808
+ if (state2.element) {
809
+ const values = Object.fromEntries(names.map((name) => [name, state2.api.getValue(name)]));
810
+ callback(values, null, state2.element, state2.api.getState());
811
+ }
812
+ return () => {
813
+ state2.groupWatchers.delete(entry);
814
+ };
815
+ };
816
+ var createObserve = (state2, _helpers) => {
817
+ function observe(field, callback) {
818
+ if (state2.isDestroyed) return () => void 0;
819
+ return Array.isArray(field) ? observeFields(state2, field, callback) : observeField(state2, field, callback);
820
+ }
821
+ return observe;
822
+ };
799
823
 
800
824
  // src/api/on-submit.ts
801
825
  var createOnSubmit = (state2, _helpers) => (callback, preventDefault = true) => {
826
+ if (state2.isDestroyed) return state2.api;
802
827
  state2.submitHandlers.add({ callback, preventDefault });
803
828
  return state2.api;
804
829
  };
805
830
 
806
831
  // src/api/prefill.ts
807
- var createPrefill = (state2, _helpers) => (fieldName) => {
808
- if (!state2.element) return state2.api;
832
+ var createPrefill = (state2, helpers) => (fieldName) => {
833
+ if (!state2.element || state2.isDestroyed) return state2.api;
809
834
  const params = new URLSearchParams(window.location.search);
810
- params.forEach((value, key) => {
811
- if (fieldName && fieldName !== key) return;
812
- state2.api.setValue(key, value);
813
- });
835
+ const keys = Array.from(new Set(params.keys())).filter(
836
+ (key) => !fieldName || fieldName === key
837
+ );
838
+ if (keys.length === 0) return state2.api;
839
+ state2.autoSubmitSuspended = true;
840
+ try {
841
+ for (const key of keys) {
842
+ const values = params.getAll(key);
843
+ state2.api.setValue(key, values.length > 1 ? values : values[0]);
844
+ }
845
+ } finally {
846
+ state2.autoSubmitSuspended = false;
847
+ }
848
+ helpers.scheduleAutoSubmit();
814
849
  return state2.api;
815
850
  };
816
851
 
817
852
  // src/api/reset.ts
818
853
  var createReset = (state2, helpers) => () => {
819
- if (!state2.element) return state2.api;
820
- state2.element.reset();
821
- resetFormattedFields(state2);
822
- state2.manualErrors = {};
823
- state2.validationErrors = {};
824
- state2.validationGroupErrors = {};
825
- state2.validationElementErrors.clear();
826
- clearAttributes(state2.element, [
827
- state2.attributes.error,
828
- state2.attributes.filled,
829
- "aria-invalid"
830
- ]);
831
- helpers.syncVisualState();
832
- helpers.notifySubscribers();
854
+ if (!state2.element || state2.isDestroyed) return state2.api;
855
+ state2.isResetting = true;
856
+ try {
857
+ state2.element.reset();
858
+ } finally {
859
+ state2.isResetting = false;
860
+ }
861
+ helpers.applyReset();
833
862
  return state2.api;
834
863
  };
835
864
 
836
865
  // src/api/validate.ts
837
866
  var createValidate = (state2, helpers) => (fields) => {
867
+ if (state2.isDestroyed) return { isValid: true, errors: {} };
838
868
  state2.isValidated = true;
839
- helpers.syncVisualState(fields);
840
869
  return helpers.validateNames(fields);
841
870
  };
842
871
 
@@ -845,6 +874,7 @@ var createRevalidate = createValidate;
845
874
 
846
875
  // src/api/set-errors.ts
847
876
  var createSetErrors = (state2, helpers) => (fields) => {
877
+ if (state2.isDestroyed) return state2.api;
848
878
  if (Array.isArray(fields)) {
849
879
  state2.manualErrors = {
850
880
  ...state2.manualErrors,
@@ -861,8 +891,78 @@ var createSetErrors = (state2, helpers) => (fields) => {
861
891
  return state2.api;
862
892
  };
863
893
 
894
+ // src/api/set-options.ts
895
+ var isGroup = (entry) => typeof entry === "object" && entry !== null && Array.isArray(entry.options);
896
+ var createOption = (entry) => {
897
+ const option = document.createElement("option");
898
+ if (typeof entry === "object") {
899
+ option.value = String(entry.value);
900
+ option.textContent = entry.label ?? String(entry.value);
901
+ option.disabled = Boolean(entry.disabled);
902
+ if (entry.selected) option.defaultSelected = true;
903
+ } else {
904
+ option.value = String(entry);
905
+ option.textContent = String(entry);
906
+ }
907
+ return option;
908
+ };
909
+ var buildNodes = (options) => {
910
+ const nodes = [];
911
+ let hasExplicitSelection = false;
912
+ const track = (option) => {
913
+ if (option.defaultSelected) hasExplicitSelection = true;
914
+ return option;
915
+ };
916
+ for (const entry of options) {
917
+ if (entry === null || entry === void 0) continue;
918
+ if (isGroup(entry)) {
919
+ const group = document.createElement("optgroup");
920
+ group.label = entry.label;
921
+ group.disabled = Boolean(entry.disabled);
922
+ for (const child of entry.options) {
923
+ if (child === null || child === void 0) continue;
924
+ group.appendChild(track(createOption(child)));
925
+ }
926
+ nodes.push(group);
927
+ continue;
928
+ }
929
+ nodes.push(track(createOption(entry)));
930
+ }
931
+ return { nodes, hasExplicitSelection };
932
+ };
933
+ var selectedValues = (select) => Array.from(select.selectedOptions, (option) => option.value);
934
+ var restoreSelection = (select, previous) => {
935
+ if (previous.length === 0) return;
936
+ const options = Array.from(select.options);
937
+ if (select.multiple) {
938
+ for (const option of options) option.selected = previous.includes(option.value);
939
+ return;
940
+ }
941
+ const match = options.find((option) => option.value === previous[0]);
942
+ if (match) match.selected = true;
943
+ };
944
+ var sameValues = (left, right) => left.length === right.length && left.every((value, index) => value === right[index]);
945
+ var createSetOptions = (state2, helpers) => (name, options, config = {}) => {
946
+ if (!state2.element || state2.isDestroyed) return state2.api;
947
+ const selects = helpers.getFieldsByName(name).filter((field) => field instanceof HTMLSelectElement);
948
+ if (selects.length === 0) return state2.api;
949
+ const keepValue = config.keepValue ?? true;
950
+ const changed = [];
951
+ for (const select of selects) {
952
+ const previous = selectedValues(select);
953
+ const { nodes, hasExplicitSelection } = buildNodes(options);
954
+ select.replaceChildren(...nodes);
955
+ if (keepValue && !hasExplicitSelection) restoreSelection(select, previous);
956
+ if (!sameValues(previous, selectedValues(select))) changed.push(select);
957
+ }
958
+ helpers.commitOwnMutations(selects);
959
+ changed[0]?.dispatchEvent(new Event("input", { bubbles: true }));
960
+ return state2.api;
961
+ };
962
+
864
963
  // src/api/set-value.ts
865
964
  var createSetValue = (state2, helpers) => (name, value) => {
965
+ if (state2.isDestroyed) return state2.api;
866
966
  const fields = helpers.getFieldsByName(name);
867
967
  if (fields.length === 0) return state2.api;
868
968
  writeFieldValue(fields, value);
@@ -874,6 +974,7 @@ var createSetValue = (state2, helpers) => (name, value) => {
874
974
 
875
975
  // src/api/subscribe.ts
876
976
  var createSubscribe = (state2, _helpers) => (listener) => {
977
+ if (state2.isDestroyed) return () => void 0;
877
978
  state2.subscribers.add(listener);
878
979
  listener(state2.api.getState());
879
980
  return () => {
@@ -882,28 +983,46 @@ var createSubscribe = (state2, _helpers) => (listener) => {
882
983
  };
883
984
 
884
985
  // src/api/unwatch.ts
885
- var createUnwatch = (state2, _helpers) => (field, callback) => {
886
- if (!field) {
887
- state2.watchedFields.clear();
888
- return state2.api;
889
- }
890
- if (!callback) {
891
- state2.watchedFields.delete(field);
986
+ var sameFields = (left, right) => left.length === right.size && left.every((name) => right.has(name));
987
+ var createUnwatch = (state2, _helpers) => {
988
+ function unwatch(field, callback) {
989
+ if (field === void 0 || field === "") {
990
+ state2.watchedFields.clear();
991
+ state2.groupWatchers.clear();
992
+ return state2.api;
993
+ }
994
+ if (Array.isArray(field)) {
995
+ const names = new Set(field);
996
+ for (const entry of Array.from(state2.groupWatchers)) {
997
+ if (!sameFields(entry.fields, names)) continue;
998
+ if (callback && entry.callback !== callback) continue;
999
+ state2.groupWatchers.delete(entry);
1000
+ }
1001
+ return state2.api;
1002
+ }
1003
+ if (!callback) {
1004
+ state2.watchedFields.delete(field);
1005
+ return state2.api;
1006
+ }
1007
+ const callbacks = state2.watchedFields.get(field);
1008
+ callbacks?.delete(callback);
1009
+ if (callbacks && callbacks.size === 0) {
1010
+ state2.watchedFields.delete(field);
1011
+ }
892
1012
  return state2.api;
893
1013
  }
894
- const callbacks = state2.watchedFields.get(field);
895
- callbacks?.delete(callback);
896
- if (callbacks && callbacks.size === 0) {
897
- state2.watchedFields.delete(field);
898
- }
899
- return state2.api;
1014
+ return unwatch;
900
1015
  };
901
1016
 
902
1017
  // src/api/watch.ts
903
- var createWatch = (state2, helpers) => (field, callback) => {
1018
+ var createWatch = (state2, helpers) => {
904
1019
  const observe = createObserve(state2, helpers);
905
- observe(field, callback);
906
- return state2.api;
1020
+ function watch(field, callback) {
1021
+ if (Array.isArray(field)) observe(field, callback);
1022
+ else observe(field, callback);
1023
+ return state2.api;
1024
+ }
1025
+ return watch;
907
1026
  };
908
1027
 
909
1028
  // src/core/validation.ts
@@ -1071,6 +1190,7 @@ var createFormController = (target, options = {}) => {
1071
1190
  attributes,
1072
1191
  validators: normalizedOptions.validators ?? {},
1073
1192
  watchedFields: /* @__PURE__ */ new Map(),
1193
+ groupWatchers: /* @__PURE__ */ new Set(),
1074
1194
  subscribers: /* @__PURE__ */ new Set(),
1075
1195
  submitHandlers: /* @__PURE__ */ new Set(),
1076
1196
  fieldCache: /* @__PURE__ */ new Map(),
@@ -1084,6 +1204,9 @@ var createFormController = (target, options = {}) => {
1084
1204
  submitCount: 0,
1085
1205
  pendingSubmitCount: 0,
1086
1206
  autoSubmitTimer: null,
1207
+ isAutoSubmitting: false,
1208
+ autoSubmitSuspended: false,
1209
+ isResetting: false,
1087
1210
  isDestroyed: false,
1088
1211
  listeners: [],
1089
1212
  mutationObserver: null,
@@ -1128,12 +1251,20 @@ var createFormController = (target, options = {}) => {
1128
1251
  state2.fieldCache.set(name, fields);
1129
1252
  return fields;
1130
1253
  };
1131
- const getTrackedFieldNames = () => {
1132
- if (!state2.element) return Object.keys(state2.validators);
1133
- const names = new Set(Object.keys(state2.validators));
1254
+ const indexFieldsByName = () => {
1255
+ const index = /* @__PURE__ */ new Map();
1256
+ if (!state2.element) return index;
1134
1257
  for (const field of getNamedFields(state2.element)) {
1135
- names.add(field.name);
1258
+ const bucket = index.get(field.name);
1259
+ if (bucket) bucket.push(field);
1260
+ else index.set(field.name, [field]);
1136
1261
  }
1262
+ return index;
1263
+ };
1264
+ const getTrackedFieldNames = (index = indexFieldsByName()) => {
1265
+ if (!state2.element) return Object.keys(state2.validators);
1266
+ const names = new Set(Object.keys(state2.validators));
1267
+ for (const name of index.keys()) names.add(name);
1137
1268
  for (const [canonical, entry] of state2.formattedFields) {
1138
1269
  names.add(canonical);
1139
1270
  if (entry.displayName && entry.displayName !== canonical) {
@@ -1144,8 +1275,9 @@ var createFormController = (target, options = {}) => {
1144
1275
  };
1145
1276
  const getValues = () => {
1146
1277
  const values = {};
1147
- for (const name of getTrackedFieldNames()) {
1148
- values[name] = readFieldValue(getFieldsByName(name));
1278
+ const index = indexFieldsByName();
1279
+ for (const name of getTrackedFieldNames(index)) {
1280
+ values[name] = readFieldValue(index.get(name) ?? []);
1149
1281
  }
1150
1282
  return values;
1151
1283
  };
@@ -1229,7 +1361,7 @@ var createFormController = (target, options = {}) => {
1229
1361
  }
1230
1362
  return affected;
1231
1363
  };
1232
- const emitFieldWatchers = (name) => {
1364
+ const emitSingleWatchers = (name) => {
1233
1365
  if (!state2.element || !state2.api) return;
1234
1366
  const callbacks = state2.watchedFields.get(name);
1235
1367
  if (!callbacks || callbacks.size === 0) return;
@@ -1238,29 +1370,85 @@ var createFormController = (target, options = {}) => {
1238
1370
  const snapshot = state2.api.getState();
1239
1371
  callbacks.forEach((callback) => callback(value, fieldElement, state2.element, snapshot));
1240
1372
  };
1373
+ const emitGroupWatchers = (names) => {
1374
+ if (!state2.element || !state2.api || state2.groupWatchers.size === 0) return;
1375
+ let snapshot;
1376
+ for (const entry of Array.from(state2.groupWatchers)) {
1377
+ const changed = names ? names.find((name) => entry.fields.includes(name)) : null;
1378
+ if (changed === void 0) continue;
1379
+ snapshot ?? (snapshot = state2.api.getState());
1380
+ const values = Object.fromEntries(
1381
+ entry.fields.map((field) => [field, state2.api.getValue(field)])
1382
+ );
1383
+ entry.callback(values, changed, state2.element, snapshot);
1384
+ }
1385
+ };
1386
+ const emitFieldWatchers = (name) => {
1387
+ emitSingleWatchers(name);
1388
+ emitGroupWatchers([name]);
1389
+ };
1390
+ const runAutoSubmit = () => {
1391
+ state2.autoSubmitTimer = null;
1392
+ if (!state2.element || state2.isDestroyed || !state2.autoSubmitEnabled) return;
1393
+ state2.isAutoSubmitting = true;
1394
+ try {
1395
+ submitForm(state2.element);
1396
+ } finally {
1397
+ state2.isAutoSubmitting = false;
1398
+ }
1399
+ };
1241
1400
  const scheduleAutoSubmit = () => {
1242
- if (!state2.element || !state2.autoSubmitEnabled) return;
1401
+ if (!state2.element || !state2.autoSubmitEnabled || state2.autoSubmitSuspended) return;
1243
1402
  if (state2.autoSubmitTimer) clearTimeout(state2.autoSubmitTimer);
1403
+ state2.autoSubmitTimer = null;
1244
1404
  if (state2.autoSubmitDebounce > 0) {
1245
- state2.autoSubmitTimer = setTimeout(
1246
- () => submitForm(state2.element),
1247
- state2.autoSubmitDebounce
1248
- );
1405
+ state2.autoSubmitTimer = setTimeout(runAutoSubmit, state2.autoSubmitDebounce);
1249
1406
  return;
1250
1407
  }
1251
- submitForm(state2.element);
1408
+ runAutoSubmit();
1409
+ };
1410
+ const selectSnapshots = /* @__PURE__ */ new WeakMap();
1411
+ const readSelectSnapshot = (select) => Array.from(select.selectedOptions, (option) => option.value).join("\0");
1412
+ const snapshotSelect = (select) => {
1413
+ selectSnapshots.set(select, readSelectSnapshot(select));
1414
+ };
1415
+ const snapshotAllSelects = () => {
1416
+ if (!state2.element) return;
1417
+ for (const field of getNamedFields(state2.element)) {
1418
+ if (field instanceof HTMLSelectElement) snapshotSelect(field);
1419
+ }
1420
+ };
1421
+ const emitAllFieldWatchers = () => {
1422
+ for (const name of Array.from(state2.watchedFields.keys())) emitSingleWatchers(name);
1423
+ emitGroupWatchers(null);
1424
+ };
1425
+ const applyReset = () => {
1426
+ if (!state2.element || state2.isDestroyed) return;
1427
+ clearFieldCache();
1428
+ resetFormattedFields(state2);
1429
+ state2.manualErrors = {};
1430
+ state2.validationErrors = {};
1431
+ state2.validationGroupErrors = {};
1432
+ state2.validationElementErrors.clear();
1433
+ clearAttributes(state2.element, [attributes.error, attributes.filled, "aria-invalid"]);
1434
+ syncVisualState();
1435
+ snapshotAllSelects();
1436
+ emitAllFieldWatchers();
1437
+ notifySubscribers();
1252
1438
  };
1253
1439
  const helpers = {
1254
1440
  notifySubscribers,
1255
1441
  clearFieldCache,
1256
1442
  getFieldsByName,
1257
- getTrackedFieldNames,
1443
+ getTrackedFieldNames: () => getTrackedFieldNames(),
1258
1444
  getValues,
1259
1445
  getMergedErrors,
1260
1446
  syncVisualState,
1261
1447
  validateNames,
1262
1448
  emitFieldWatchers,
1263
- scheduleAutoSubmit
1449
+ scheduleAutoSubmit,
1450
+ applyReset,
1451
+ commitOwnMutations: (selects) => commitOwnMutations(selects)
1264
1452
  };
1265
1453
  const handleDelegatedEvent = (event) => {
1266
1454
  const target2 = event.target;
@@ -1268,6 +1456,7 @@ var createFormController = (target, options = {}) => {
1268
1456
  const name = target2.name;
1269
1457
  if (!name) return;
1270
1458
  clearFieldCache();
1459
+ if (target2 instanceof HTMLSelectElement) snapshotSelect(target2);
1271
1460
  const canonical = resolveCanonicalForName(state2, name) ?? name;
1272
1461
  const displayName = resolveDisplayNameForName(state2, name);
1273
1462
  if (normalizedOptions.clearManualErrorsOnChange) {
@@ -1278,8 +1467,9 @@ var createFormController = (target, options = {}) => {
1278
1467
  if (state2.isValidated && affectedValidationNames.some((field) => state2.validators[field] !== void 0)) {
1279
1468
  validateNames(affectedValidationNames);
1280
1469
  }
1281
- emitFieldWatchers(canonical);
1282
- if (canonical !== name) emitFieldWatchers(name);
1470
+ emitSingleWatchers(canonical);
1471
+ if (canonical !== name) emitSingleWatchers(name);
1472
+ emitGroupWatchers(canonical !== name ? [canonical, name] : [canonical]);
1283
1473
  notifySubscribers();
1284
1474
  scheduleAutoSubmit();
1285
1475
  };
@@ -1289,6 +1479,23 @@ var createFormController = (target, options = {}) => {
1289
1479
  if (target2.form !== state2.element || state2.element?.contains(target2)) return;
1290
1480
  handleDelegatedEvent(event);
1291
1481
  };
1482
+ const isFocusable = (candidate) => {
1483
+ if (!candidate.isConnected || candidate.disabled || candidate.hidden) return false;
1484
+ return !(candidate instanceof HTMLInputElement && candidate.type === "hidden");
1485
+ };
1486
+ const focusFirstInvalidField = (errors) => {
1487
+ const candidates = [];
1488
+ for (const name of Object.keys(errors)) {
1489
+ const fields = getFieldsByName(resolveDisplayNameForName(state2, name)).filter(isFocusable);
1490
+ const elementErrors = state2.validationElementErrors.get(name);
1491
+ const field = fields.find((candidate) => elementErrors?.has(candidate)) ?? fields[0];
1492
+ if (field) candidates.push(field);
1493
+ }
1494
+ candidates.sort(
1495
+ (left, right) => left.compareDocumentPosition(right) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1
1496
+ );
1497
+ candidates[0]?.focus();
1498
+ };
1292
1499
  const handleSubmitEvent = (event) => {
1293
1500
  if (!state2.element || state2.isDestroyed || !state2.api) return;
1294
1501
  if (normalizedOptions.clearErrorsOnSubmit) state2.manualErrors = {};
@@ -1299,23 +1506,7 @@ var createFormController = (target, options = {}) => {
1299
1506
  state2.submitCount += 1;
1300
1507
  notifySubscribers();
1301
1508
  if (!validation.isValid) {
1302
- for (const name of Object.keys(validation.errors)) {
1303
- const displayName = resolveDisplayNameForName(state2, name);
1304
- const fields = getFieldsByName(displayName);
1305
- const elementErrors = state2.validationElementErrors.get(name);
1306
- const field = fields.find((candidate2) => {
1307
- if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
1308
- if (candidate2 instanceof HTMLInputElement && candidate2.type === "hidden") return false;
1309
- return elementErrors?.has(candidate2);
1310
- }) ?? fields.find((candidate2) => {
1311
- if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
1312
- return !(candidate2 instanceof HTMLInputElement && candidate2.type === "hidden");
1313
- });
1314
- if (field) {
1315
- field.focus();
1316
- break;
1317
- }
1318
- }
1509
+ if (!state2.isAutoSubmitting) focusFirstInvalidField(validation.errors);
1319
1510
  return;
1320
1511
  }
1321
1512
  const candidate = event;
@@ -1328,7 +1519,13 @@ var createFormController = (target, options = {}) => {
1328
1519
  try {
1329
1520
  for (const handler of handlers) {
1330
1521
  const result = handler.callback(state2.element, data, formData, snapshot);
1331
- if (result && typeof result.then === "function") pending.push(Promise.resolve(result));
1522
+ if (result && typeof result.then === "function") {
1523
+ pending.push(
1524
+ Promise.resolve(result).catch((error) => {
1525
+ console.error("[forms] submit handler failed", error);
1526
+ })
1527
+ );
1528
+ }
1332
1529
  }
1333
1530
  } catch (error) {
1334
1531
  synchronousError = error;
@@ -1344,15 +1541,71 @@ var createFormController = (target, options = {}) => {
1344
1541
  }
1345
1542
  if (hasSynchronousError) throw synchronousError;
1346
1543
  };
1544
+ const handleResetEvent = (event) => {
1545
+ if (state2.isResetting) return;
1546
+ const timer = setTimeout(() => {
1547
+ state2.cleanups.delete(cancel);
1548
+ if (!event.defaultPrevented) applyReset();
1549
+ }, 0);
1550
+ const cancel = () => clearTimeout(timer);
1551
+ state2.cleanups.add(cancel);
1552
+ };
1553
+ const touchesFields = (node) => node instanceof Element && (isFieldElement(node) || node.querySelector("input, select, textarea") !== null);
1554
+ const isStructural = (record) => record.type === "attributes" || Array.from(record.addedNodes).some(touchesFields) || Array.from(record.removedNodes).some(touchesFields);
1555
+ const isOptionNode = (node) => node instanceof HTMLOptionElement || node instanceof HTMLOptGroupElement;
1556
+ const optionListOwner = (record) => {
1557
+ if (record.type !== "childList") return null;
1558
+ const touchesOptions = Array.from(record.addedNodes).some(isOptionNode) || Array.from(record.removedNodes).some(isOptionNode);
1559
+ if (!touchesOptions) return null;
1560
+ const target2 = record.target;
1561
+ if (target2 instanceof HTMLSelectElement) return target2;
1562
+ if (target2 instanceof HTMLOptGroupElement && target2.parentElement instanceof HTMLSelectElement) {
1563
+ return target2.parentElement;
1564
+ }
1565
+ return null;
1566
+ };
1567
+ const handleMutations = (records) => {
1568
+ if (state2.isDestroyed) return;
1569
+ const structural = records.some(isStructural);
1570
+ const changedNames = /* @__PURE__ */ new Set();
1571
+ for (const record of records) {
1572
+ const select = optionListOwner(record);
1573
+ if (!select || !select.name) continue;
1574
+ const next = readSelectSnapshot(select);
1575
+ if (selectSnapshots.get(select) !== next) changedNames.add(select.name);
1576
+ selectSnapshots.set(select, next);
1577
+ }
1578
+ if (!structural && changedNames.size === 0) return;
1579
+ clearFieldCache();
1580
+ if (structural) {
1581
+ snapshotAllSelects();
1582
+ if (state2.isValidated) validateNames();
1583
+ else syncVisualState();
1584
+ } else {
1585
+ const affected = /* @__PURE__ */ new Set();
1586
+ for (const name of changedNames) {
1587
+ for (const field of getAffectedValidationNames(name)) affected.add(field);
1588
+ }
1589
+ const names = Array.from(affected);
1590
+ syncVisualState(Array.from(changedNames));
1591
+ if (state2.isValidated && names.some((field) => state2.validators[field] !== void 0)) {
1592
+ validateNames(names);
1593
+ }
1594
+ }
1595
+ for (const name of changedNames) emitFieldWatchers(name);
1596
+ notifySubscribers();
1597
+ };
1598
+ const commitOwnMutations = (selects) => {
1599
+ selects.forEach(snapshotSelect);
1600
+ const pending = state2.mutationObserver?.takeRecords() ?? [];
1601
+ const foreign = pending.filter(
1602
+ (record) => !selects.some((select) => select.contains(record.target))
1603
+ );
1604
+ if (foreign.length > 0) handleMutations(foreign);
1605
+ };
1347
1606
  const startMutationObserver = () => {
1348
1607
  if (!state2.element || typeof MutationObserver === "undefined") return;
1349
- state2.mutationObserver = new MutationObserver(() => {
1350
- clearFieldCache();
1351
- if (state2.isValidated) {
1352
- validateNames();
1353
- } else syncVisualState();
1354
- notifySubscribers();
1355
- });
1608
+ state2.mutationObserver = new MutationObserver(handleMutations);
1356
1609
  state2.mutationObserver.observe(state2.element, {
1357
1610
  childList: true,
1358
1611
  subtree: true,
@@ -1381,6 +1634,7 @@ var createFormController = (target, options = {}) => {
1381
1634
  setErrors: createSetErrors(state2, helpers),
1382
1635
  clearErrors: createClearErrors(state2, helpers),
1383
1636
  setValue: createSetValue(state2, helpers),
1637
+ setOptions: createSetOptions(state2, helpers),
1384
1638
  getValue: createGetValue(state2, helpers),
1385
1639
  getField: createGetField(state2, helpers),
1386
1640
  validate: createValidate(state2, helpers),
@@ -1399,6 +1653,8 @@ var createFormController = (target, options = {}) => {
1399
1653
  addListener2(state2.element, "input", handleDelegatedEvent);
1400
1654
  addListener2(document, "input", handleExternalDelegatedEvent);
1401
1655
  addListener2(state2.element, "submit", handleSubmitEvent);
1656
+ addListener2(state2.element, "reset", handleResetEvent);
1657
+ snapshotAllSelects();
1402
1658
  startMutationObserver();
1403
1659
  }
1404
1660
  if (normalizedOptions.autoSubmit) {