@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
@@ -1,4 +1,4 @@
1
- import { u as FormsApi, v as FormsAvailable, F as FormTarget, a as FormControllerOptions, b as FormController, N as NewFormInput } from '../registry-GzopYPCR.cjs';
1
+ import { v as FormsApi, w as FormsAvailable, F as FormTarget, a as FormControllerOptions, b as FormController, N as NewFormInput } from '../registry-CPe_lf8-.cjs';
2
2
 
3
3
  declare const Forms: FormsApi;
4
4
  declare const form: (target: FormTarget, options?: FormControllerOptions) => FormController;
@@ -1,4 +1,4 @@
1
- import { u as FormsApi, v as FormsAvailable, F as FormTarget, a as FormControllerOptions, b as FormController, N as NewFormInput } from '../registry-GzopYPCR.js';
1
+ import { v as FormsApi, w as FormsAvailable, F as FormTarget, a as FormControllerOptions, b as FormController, N as NewFormInput } from '../registry-CPe_lf8-.js';
2
2
 
3
3
  declare const Forms: FormsApi;
4
4
  declare const form: (target: FormTarget, options?: FormControllerOptions) => FormController;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  browser
3
- } from "../chunk-MMHX34WB.js";
3
+ } from "../chunk-6MA5TF6R.js";
4
4
 
5
5
  // src/browser/global.ts
6
6
  var Forms = browser;
@@ -172,7 +172,7 @@ var validateValues = (values, schema) => {
172
172
 
173
173
  // src/api/append.ts
174
174
  var createAppend = (state2, helpers) => ({ tag, content, class: className, atStart = false }) => {
175
- if (!state2.element) return null;
175
+ if (!state2.element || state2.isDestroyed) return null;
176
176
  helpers.clearFieldCache();
177
177
  if (className) {
178
178
  const classSelector = className.trim().split(/\s+/)[0];
@@ -208,6 +208,7 @@ var createAddCleanup = (state2, _helpers) => (cleanup) => {
208
208
 
209
209
  // src/api/auto-submit.ts
210
210
  var createAutoSubmit = (state2, helpers) => (next = true) => {
211
+ if (state2.isDestroyed) return state2.api;
211
212
  state2.autoSubmitEnabled = next !== false;
212
213
  if (!state2.autoSubmitEnabled && state2.autoSubmitTimer) {
213
214
  clearTimeout(state2.autoSubmitTimer);
@@ -220,6 +221,7 @@ var createAutoSubmit = (state2, helpers) => (next = true) => {
220
221
 
221
222
  // src/api/clear-errors.ts
222
223
  var createClearErrors = (state2, helpers) => (fields) => {
224
+ if (state2.isDestroyed) return state2.api;
223
225
  if (!fields) {
224
226
  state2.manualErrors = {};
225
227
  helpers.syncVisualState();
@@ -346,6 +348,7 @@ var isFormatChar = (char) => {
346
348
  return true;
347
349
  };
348
350
  var restoreCursor = (field, position) => {
351
+ if (field.ownerDocument.activeElement !== field) return;
349
352
  const max = field.value.length;
350
353
  const safe = clamp(position, 0, max);
351
354
  const apply = () => {
@@ -648,6 +651,7 @@ var createDestroy = (state2, _helpers) => () => {
648
651
  }
649
652
  }
650
653
  state2.watchedFields.clear();
654
+ state2.groupWatchers.clear();
651
655
  state2.subscribers.clear();
652
656
  state2.submitHandlers.clear();
653
657
  state2.fieldCache.clear();
@@ -709,7 +713,7 @@ var mergeErrors = (left, right) => {
709
713
  var createGetState = (state2, helpers) => () => {
710
714
  const values = helpers.getValues();
711
715
  const errors = cloneErrors(helpers.getMergedErrors());
712
- const filledFields = helpers.getTrackedFieldNames().filter((name) => {
716
+ const filledFields = Object.keys(values).filter((name) => {
713
717
  const value = values[name];
714
718
  if (Array.isArray(value)) return value.length > 0;
715
719
  if (typeof value === "string") return value.trim().length > 0;
@@ -844,9 +848,9 @@ var writeFieldValue = (fields, value) => {
844
848
  continue;
845
849
  }
846
850
  if (field instanceof HTMLSelectElement && field.multiple) {
847
- const selectedValues = normalizedArray ?? [normalizedValue];
851
+ const selectedValues2 = normalizedArray ?? [normalizedValue];
848
852
  for (const option of field.options) {
849
- option.selected = selectedValues.includes(option.value);
853
+ option.selected = selectedValues2.includes(option.value);
850
854
  }
851
855
  continue;
852
856
  }
@@ -881,7 +885,7 @@ var submitForm = (form2) => {
881
885
  var createGetValue = (state2, helpers) => (name) => readFieldValue(helpers.getFieldsByName(name));
882
886
 
883
887
  // src/api/observe.ts
884
- var createObserve = (state2, helpers) => (field, callback) => {
888
+ var observeField = (state2, field, callback) => {
885
889
  const callbacks = state2.watchedFields.get(field) ?? /* @__PURE__ */ new Set();
886
890
  callbacks.add(callback);
887
891
  state2.watchedFields.set(field, callbacks);
@@ -896,47 +900,72 @@ var createObserve = (state2, helpers) => (field, callback) => {
896
900
  }
897
901
  };
898
902
  };
903
+ var observeFields = (state2, fields, callback) => {
904
+ const names = Array.from(new Set(fields));
905
+ if (names.length === 0) return () => void 0;
906
+ const entry = { fields: names, callback };
907
+ state2.groupWatchers.add(entry);
908
+ if (state2.element) {
909
+ const values = Object.fromEntries(names.map((name) => [name, state2.api.getValue(name)]));
910
+ callback(values, null, state2.element, state2.api.getState());
911
+ }
912
+ return () => {
913
+ state2.groupWatchers.delete(entry);
914
+ };
915
+ };
916
+ var createObserve = (state2, _helpers) => {
917
+ function observe(field, callback) {
918
+ if (state2.isDestroyed) return () => void 0;
919
+ return Array.isArray(field) ? observeFields(state2, field, callback) : observeField(state2, field, callback);
920
+ }
921
+ return observe;
922
+ };
899
923
 
900
924
  // src/api/on-submit.ts
901
925
  var createOnSubmit = (state2, _helpers) => (callback, preventDefault = true) => {
926
+ if (state2.isDestroyed) return state2.api;
902
927
  state2.submitHandlers.add({ callback, preventDefault });
903
928
  return state2.api;
904
929
  };
905
930
 
906
931
  // src/api/prefill.ts
907
- var createPrefill = (state2, _helpers) => (fieldName) => {
908
- if (!state2.element) return state2.api;
932
+ var createPrefill = (state2, helpers) => (fieldName) => {
933
+ if (!state2.element || state2.isDestroyed) return state2.api;
909
934
  const params = new URLSearchParams(window.location.search);
910
- params.forEach((value, key) => {
911
- if (fieldName && fieldName !== key) return;
912
- state2.api.setValue(key, value);
913
- });
935
+ const keys = Array.from(new Set(params.keys())).filter(
936
+ (key) => !fieldName || fieldName === key
937
+ );
938
+ if (keys.length === 0) return state2.api;
939
+ state2.autoSubmitSuspended = true;
940
+ try {
941
+ for (const key of keys) {
942
+ const values = params.getAll(key);
943
+ state2.api.setValue(key, values.length > 1 ? values : values[0]);
944
+ }
945
+ } finally {
946
+ state2.autoSubmitSuspended = false;
947
+ }
948
+ helpers.scheduleAutoSubmit();
914
949
  return state2.api;
915
950
  };
916
951
 
917
952
  // src/api/reset.ts
918
953
  var createReset = (state2, helpers) => () => {
919
- if (!state2.element) return state2.api;
920
- state2.element.reset();
921
- resetFormattedFields(state2);
922
- state2.manualErrors = {};
923
- state2.validationErrors = {};
924
- state2.validationGroupErrors = {};
925
- state2.validationElementErrors.clear();
926
- clearAttributes(state2.element, [
927
- state2.attributes.error,
928
- state2.attributes.filled,
929
- "aria-invalid"
930
- ]);
931
- helpers.syncVisualState();
932
- helpers.notifySubscribers();
954
+ if (!state2.element || state2.isDestroyed) return state2.api;
955
+ state2.isResetting = true;
956
+ try {
957
+ state2.element.reset();
958
+ } finally {
959
+ state2.isResetting = false;
960
+ }
961
+ helpers.applyReset();
933
962
  return state2.api;
934
963
  };
935
964
 
936
965
  // src/api/validate.ts
937
966
  var createValidate = (state2, helpers) => (fields) => {
967
+ if (state2.isDestroyed) return { isValid: true, errors: {} };
938
968
  state2.isValidated = true;
939
- helpers.syncVisualState(fields);
940
969
  return helpers.validateNames(fields);
941
970
  };
942
971
 
@@ -945,6 +974,7 @@ var createRevalidate = createValidate;
945
974
 
946
975
  // src/api/set-errors.ts
947
976
  var createSetErrors = (state2, helpers) => (fields) => {
977
+ if (state2.isDestroyed) return state2.api;
948
978
  if (Array.isArray(fields)) {
949
979
  state2.manualErrors = {
950
980
  ...state2.manualErrors,
@@ -961,8 +991,78 @@ var createSetErrors = (state2, helpers) => (fields) => {
961
991
  return state2.api;
962
992
  };
963
993
 
994
+ // src/api/set-options.ts
995
+ var isGroup = (entry) => typeof entry === "object" && entry !== null && Array.isArray(entry.options);
996
+ var createOption = (entry) => {
997
+ const option = document.createElement("option");
998
+ if (typeof entry === "object") {
999
+ option.value = String(entry.value);
1000
+ option.textContent = entry.label ?? String(entry.value);
1001
+ option.disabled = Boolean(entry.disabled);
1002
+ if (entry.selected) option.defaultSelected = true;
1003
+ } else {
1004
+ option.value = String(entry);
1005
+ option.textContent = String(entry);
1006
+ }
1007
+ return option;
1008
+ };
1009
+ var buildNodes = (options) => {
1010
+ const nodes = [];
1011
+ let hasExplicitSelection = false;
1012
+ const track = (option) => {
1013
+ if (option.defaultSelected) hasExplicitSelection = true;
1014
+ return option;
1015
+ };
1016
+ for (const entry of options) {
1017
+ if (entry === null || entry === void 0) continue;
1018
+ if (isGroup(entry)) {
1019
+ const group = document.createElement("optgroup");
1020
+ group.label = entry.label;
1021
+ group.disabled = Boolean(entry.disabled);
1022
+ for (const child of entry.options) {
1023
+ if (child === null || child === void 0) continue;
1024
+ group.appendChild(track(createOption(child)));
1025
+ }
1026
+ nodes.push(group);
1027
+ continue;
1028
+ }
1029
+ nodes.push(track(createOption(entry)));
1030
+ }
1031
+ return { nodes, hasExplicitSelection };
1032
+ };
1033
+ var selectedValues = (select) => Array.from(select.selectedOptions, (option) => option.value);
1034
+ var restoreSelection = (select, previous) => {
1035
+ if (previous.length === 0) return;
1036
+ const options = Array.from(select.options);
1037
+ if (select.multiple) {
1038
+ for (const option of options) option.selected = previous.includes(option.value);
1039
+ return;
1040
+ }
1041
+ const match = options.find((option) => option.value === previous[0]);
1042
+ if (match) match.selected = true;
1043
+ };
1044
+ var sameValues = (left, right) => left.length === right.length && left.every((value, index) => value === right[index]);
1045
+ var createSetOptions = (state2, helpers) => (name, options, config = {}) => {
1046
+ if (!state2.element || state2.isDestroyed) return state2.api;
1047
+ const selects = helpers.getFieldsByName(name).filter((field) => field instanceof HTMLSelectElement);
1048
+ if (selects.length === 0) return state2.api;
1049
+ const keepValue = config.keepValue ?? true;
1050
+ const changed = [];
1051
+ for (const select of selects) {
1052
+ const previous = selectedValues(select);
1053
+ const { nodes, hasExplicitSelection } = buildNodes(options);
1054
+ select.replaceChildren(...nodes);
1055
+ if (keepValue && !hasExplicitSelection) restoreSelection(select, previous);
1056
+ if (!sameValues(previous, selectedValues(select))) changed.push(select);
1057
+ }
1058
+ helpers.commitOwnMutations(selects);
1059
+ changed[0]?.dispatchEvent(new Event("input", { bubbles: true }));
1060
+ return state2.api;
1061
+ };
1062
+
964
1063
  // src/api/set-value.ts
965
1064
  var createSetValue = (state2, helpers) => (name, value) => {
1065
+ if (state2.isDestroyed) return state2.api;
966
1066
  const fields = helpers.getFieldsByName(name);
967
1067
  if (fields.length === 0) return state2.api;
968
1068
  writeFieldValue(fields, value);
@@ -974,6 +1074,7 @@ var createSetValue = (state2, helpers) => (name, value) => {
974
1074
 
975
1075
  // src/api/subscribe.ts
976
1076
  var createSubscribe = (state2, _helpers) => (listener) => {
1077
+ if (state2.isDestroyed) return () => void 0;
977
1078
  state2.subscribers.add(listener);
978
1079
  listener(state2.api.getState());
979
1080
  return () => {
@@ -982,28 +1083,46 @@ var createSubscribe = (state2, _helpers) => (listener) => {
982
1083
  };
983
1084
 
984
1085
  // src/api/unwatch.ts
985
- var createUnwatch = (state2, _helpers) => (field, callback) => {
986
- if (!field) {
987
- state2.watchedFields.clear();
988
- return state2.api;
989
- }
990
- if (!callback) {
991
- state2.watchedFields.delete(field);
1086
+ var sameFields = (left, right) => left.length === right.size && left.every((name) => right.has(name));
1087
+ var createUnwatch = (state2, _helpers) => {
1088
+ function unwatch(field, callback) {
1089
+ if (field === void 0 || field === "") {
1090
+ state2.watchedFields.clear();
1091
+ state2.groupWatchers.clear();
1092
+ return state2.api;
1093
+ }
1094
+ if (Array.isArray(field)) {
1095
+ const names = new Set(field);
1096
+ for (const entry of Array.from(state2.groupWatchers)) {
1097
+ if (!sameFields(entry.fields, names)) continue;
1098
+ if (callback && entry.callback !== callback) continue;
1099
+ state2.groupWatchers.delete(entry);
1100
+ }
1101
+ return state2.api;
1102
+ }
1103
+ if (!callback) {
1104
+ state2.watchedFields.delete(field);
1105
+ return state2.api;
1106
+ }
1107
+ const callbacks = state2.watchedFields.get(field);
1108
+ callbacks?.delete(callback);
1109
+ if (callbacks && callbacks.size === 0) {
1110
+ state2.watchedFields.delete(field);
1111
+ }
992
1112
  return state2.api;
993
1113
  }
994
- const callbacks = state2.watchedFields.get(field);
995
- callbacks?.delete(callback);
996
- if (callbacks && callbacks.size === 0) {
997
- state2.watchedFields.delete(field);
998
- }
999
- return state2.api;
1114
+ return unwatch;
1000
1115
  };
1001
1116
 
1002
1117
  // src/api/watch.ts
1003
- var createWatch = (state2, helpers) => (field, callback) => {
1118
+ var createWatch = (state2, helpers) => {
1004
1119
  const observe = createObserve(state2, helpers);
1005
- observe(field, callback);
1006
- return state2.api;
1120
+ function watch(field, callback) {
1121
+ if (Array.isArray(field)) observe(field, callback);
1122
+ else observe(field, callback);
1123
+ return state2.api;
1124
+ }
1125
+ return watch;
1007
1126
  };
1008
1127
 
1009
1128
  // src/core/controller.ts
@@ -1030,6 +1149,7 @@ var createFormController = (target, options = {}) => {
1030
1149
  attributes,
1031
1150
  validators: normalizedOptions.validators ?? {},
1032
1151
  watchedFields: /* @__PURE__ */ new Map(),
1152
+ groupWatchers: /* @__PURE__ */ new Set(),
1033
1153
  subscribers: /* @__PURE__ */ new Set(),
1034
1154
  submitHandlers: /* @__PURE__ */ new Set(),
1035
1155
  fieldCache: /* @__PURE__ */ new Map(),
@@ -1043,6 +1163,9 @@ var createFormController = (target, options = {}) => {
1043
1163
  submitCount: 0,
1044
1164
  pendingSubmitCount: 0,
1045
1165
  autoSubmitTimer: null,
1166
+ isAutoSubmitting: false,
1167
+ autoSubmitSuspended: false,
1168
+ isResetting: false,
1046
1169
  isDestroyed: false,
1047
1170
  listeners: [],
1048
1171
  mutationObserver: null,
@@ -1087,12 +1210,20 @@ var createFormController = (target, options = {}) => {
1087
1210
  state2.fieldCache.set(name, fields);
1088
1211
  return fields;
1089
1212
  };
1090
- const getTrackedFieldNames = () => {
1091
- if (!state2.element) return Object.keys(state2.validators);
1092
- const names = new Set(Object.keys(state2.validators));
1213
+ const indexFieldsByName = () => {
1214
+ const index = /* @__PURE__ */ new Map();
1215
+ if (!state2.element) return index;
1093
1216
  for (const field of getNamedFields(state2.element)) {
1094
- names.add(field.name);
1217
+ const bucket = index.get(field.name);
1218
+ if (bucket) bucket.push(field);
1219
+ else index.set(field.name, [field]);
1095
1220
  }
1221
+ return index;
1222
+ };
1223
+ const getTrackedFieldNames = (index = indexFieldsByName()) => {
1224
+ if (!state2.element) return Object.keys(state2.validators);
1225
+ const names = new Set(Object.keys(state2.validators));
1226
+ for (const name of index.keys()) names.add(name);
1096
1227
  for (const [canonical, entry] of state2.formattedFields) {
1097
1228
  names.add(canonical);
1098
1229
  if (entry.displayName && entry.displayName !== canonical) {
@@ -1103,8 +1234,9 @@ var createFormController = (target, options = {}) => {
1103
1234
  };
1104
1235
  const getValues = () => {
1105
1236
  const values = {};
1106
- for (const name of getTrackedFieldNames()) {
1107
- values[name] = readFieldValue(getFieldsByName(name));
1237
+ const index = indexFieldsByName();
1238
+ for (const name of getTrackedFieldNames(index)) {
1239
+ values[name] = readFieldValue(index.get(name) ?? []);
1108
1240
  }
1109
1241
  return values;
1110
1242
  };
@@ -1188,7 +1320,7 @@ var createFormController = (target, options = {}) => {
1188
1320
  }
1189
1321
  return affected;
1190
1322
  };
1191
- const emitFieldWatchers = (name) => {
1323
+ const emitSingleWatchers = (name) => {
1192
1324
  if (!state2.element || !state2.api) return;
1193
1325
  const callbacks = state2.watchedFields.get(name);
1194
1326
  if (!callbacks || callbacks.size === 0) return;
@@ -1197,29 +1329,85 @@ var createFormController = (target, options = {}) => {
1197
1329
  const snapshot = state2.api.getState();
1198
1330
  callbacks.forEach((callback) => callback(value, fieldElement, state2.element, snapshot));
1199
1331
  };
1332
+ const emitGroupWatchers = (names) => {
1333
+ if (!state2.element || !state2.api || state2.groupWatchers.size === 0) return;
1334
+ let snapshot;
1335
+ for (const entry of Array.from(state2.groupWatchers)) {
1336
+ const changed = names ? names.find((name) => entry.fields.includes(name)) : null;
1337
+ if (changed === void 0) continue;
1338
+ snapshot ?? (snapshot = state2.api.getState());
1339
+ const values = Object.fromEntries(
1340
+ entry.fields.map((field) => [field, state2.api.getValue(field)])
1341
+ );
1342
+ entry.callback(values, changed, state2.element, snapshot);
1343
+ }
1344
+ };
1345
+ const emitFieldWatchers = (name) => {
1346
+ emitSingleWatchers(name);
1347
+ emitGroupWatchers([name]);
1348
+ };
1349
+ const runAutoSubmit = () => {
1350
+ state2.autoSubmitTimer = null;
1351
+ if (!state2.element || state2.isDestroyed || !state2.autoSubmitEnabled) return;
1352
+ state2.isAutoSubmitting = true;
1353
+ try {
1354
+ submitForm(state2.element);
1355
+ } finally {
1356
+ state2.isAutoSubmitting = false;
1357
+ }
1358
+ };
1200
1359
  const scheduleAutoSubmit = () => {
1201
- if (!state2.element || !state2.autoSubmitEnabled) return;
1360
+ if (!state2.element || !state2.autoSubmitEnabled || state2.autoSubmitSuspended) return;
1202
1361
  if (state2.autoSubmitTimer) clearTimeout(state2.autoSubmitTimer);
1362
+ state2.autoSubmitTimer = null;
1203
1363
  if (state2.autoSubmitDebounce > 0) {
1204
- state2.autoSubmitTimer = setTimeout(
1205
- () => submitForm(state2.element),
1206
- state2.autoSubmitDebounce
1207
- );
1364
+ state2.autoSubmitTimer = setTimeout(runAutoSubmit, state2.autoSubmitDebounce);
1208
1365
  return;
1209
1366
  }
1210
- submitForm(state2.element);
1367
+ runAutoSubmit();
1368
+ };
1369
+ const selectSnapshots = /* @__PURE__ */ new WeakMap();
1370
+ const readSelectSnapshot = (select) => Array.from(select.selectedOptions, (option) => option.value).join("\0");
1371
+ const snapshotSelect = (select) => {
1372
+ selectSnapshots.set(select, readSelectSnapshot(select));
1373
+ };
1374
+ const snapshotAllSelects = () => {
1375
+ if (!state2.element) return;
1376
+ for (const field of getNamedFields(state2.element)) {
1377
+ if (field instanceof HTMLSelectElement) snapshotSelect(field);
1378
+ }
1379
+ };
1380
+ const emitAllFieldWatchers = () => {
1381
+ for (const name of Array.from(state2.watchedFields.keys())) emitSingleWatchers(name);
1382
+ emitGroupWatchers(null);
1383
+ };
1384
+ const applyReset = () => {
1385
+ if (!state2.element || state2.isDestroyed) return;
1386
+ clearFieldCache();
1387
+ resetFormattedFields(state2);
1388
+ state2.manualErrors = {};
1389
+ state2.validationErrors = {};
1390
+ state2.validationGroupErrors = {};
1391
+ state2.validationElementErrors.clear();
1392
+ clearAttributes(state2.element, [attributes.error, attributes.filled, "aria-invalid"]);
1393
+ syncVisualState();
1394
+ snapshotAllSelects();
1395
+ emitAllFieldWatchers();
1396
+ notifySubscribers();
1211
1397
  };
1212
1398
  const helpers = {
1213
1399
  notifySubscribers,
1214
1400
  clearFieldCache,
1215
1401
  getFieldsByName,
1216
- getTrackedFieldNames,
1402
+ getTrackedFieldNames: () => getTrackedFieldNames(),
1217
1403
  getValues,
1218
1404
  getMergedErrors,
1219
1405
  syncVisualState,
1220
1406
  validateNames,
1221
1407
  emitFieldWatchers,
1222
- scheduleAutoSubmit
1408
+ scheduleAutoSubmit,
1409
+ applyReset,
1410
+ commitOwnMutations: (selects) => commitOwnMutations(selects)
1223
1411
  };
1224
1412
  const handleDelegatedEvent = (event) => {
1225
1413
  const target2 = event.target;
@@ -1227,6 +1415,7 @@ var createFormController = (target, options = {}) => {
1227
1415
  const name = target2.name;
1228
1416
  if (!name) return;
1229
1417
  clearFieldCache();
1418
+ if (target2 instanceof HTMLSelectElement) snapshotSelect(target2);
1230
1419
  const canonical = resolveCanonicalForName(state2, name) ?? name;
1231
1420
  const displayName = resolveDisplayNameForName(state2, name);
1232
1421
  if (normalizedOptions.clearManualErrorsOnChange) {
@@ -1237,8 +1426,9 @@ var createFormController = (target, options = {}) => {
1237
1426
  if (state2.isValidated && affectedValidationNames.some((field) => state2.validators[field] !== void 0)) {
1238
1427
  validateNames(affectedValidationNames);
1239
1428
  }
1240
- emitFieldWatchers(canonical);
1241
- if (canonical !== name) emitFieldWatchers(name);
1429
+ emitSingleWatchers(canonical);
1430
+ if (canonical !== name) emitSingleWatchers(name);
1431
+ emitGroupWatchers(canonical !== name ? [canonical, name] : [canonical]);
1242
1432
  notifySubscribers();
1243
1433
  scheduleAutoSubmit();
1244
1434
  };
@@ -1248,6 +1438,23 @@ var createFormController = (target, options = {}) => {
1248
1438
  if (target2.form !== state2.element || state2.element?.contains(target2)) return;
1249
1439
  handleDelegatedEvent(event);
1250
1440
  };
1441
+ const isFocusable = (candidate) => {
1442
+ if (!candidate.isConnected || candidate.disabled || candidate.hidden) return false;
1443
+ return !(candidate instanceof HTMLInputElement && candidate.type === "hidden");
1444
+ };
1445
+ const focusFirstInvalidField = (errors) => {
1446
+ const candidates = [];
1447
+ for (const name of Object.keys(errors)) {
1448
+ const fields = getFieldsByName(resolveDisplayNameForName(state2, name)).filter(isFocusable);
1449
+ const elementErrors = state2.validationElementErrors.get(name);
1450
+ const field = fields.find((candidate) => elementErrors?.has(candidate)) ?? fields[0];
1451
+ if (field) candidates.push(field);
1452
+ }
1453
+ candidates.sort(
1454
+ (left, right) => left.compareDocumentPosition(right) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1
1455
+ );
1456
+ candidates[0]?.focus();
1457
+ };
1251
1458
  const handleSubmitEvent = (event) => {
1252
1459
  if (!state2.element || state2.isDestroyed || !state2.api) return;
1253
1460
  if (normalizedOptions.clearErrorsOnSubmit) state2.manualErrors = {};
@@ -1258,23 +1465,7 @@ var createFormController = (target, options = {}) => {
1258
1465
  state2.submitCount += 1;
1259
1466
  notifySubscribers();
1260
1467
  if (!validation.isValid) {
1261
- for (const name of Object.keys(validation.errors)) {
1262
- const displayName = resolveDisplayNameForName(state2, name);
1263
- const fields = getFieldsByName(displayName);
1264
- const elementErrors = state2.validationElementErrors.get(name);
1265
- const field = fields.find((candidate2) => {
1266
- if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
1267
- if (candidate2 instanceof HTMLInputElement && candidate2.type === "hidden") return false;
1268
- return elementErrors?.has(candidate2);
1269
- }) ?? fields.find((candidate2) => {
1270
- if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
1271
- return !(candidate2 instanceof HTMLInputElement && candidate2.type === "hidden");
1272
- });
1273
- if (field) {
1274
- field.focus();
1275
- break;
1276
- }
1277
- }
1468
+ if (!state2.isAutoSubmitting) focusFirstInvalidField(validation.errors);
1278
1469
  return;
1279
1470
  }
1280
1471
  const candidate = event;
@@ -1287,7 +1478,13 @@ var createFormController = (target, options = {}) => {
1287
1478
  try {
1288
1479
  for (const handler of handlers) {
1289
1480
  const result = handler.callback(state2.element, data, formData, snapshot);
1290
- if (result && typeof result.then === "function") pending.push(Promise.resolve(result));
1481
+ if (result && typeof result.then === "function") {
1482
+ pending.push(
1483
+ Promise.resolve(result).catch((error) => {
1484
+ console.error("[forms] submit handler failed", error);
1485
+ })
1486
+ );
1487
+ }
1291
1488
  }
1292
1489
  } catch (error) {
1293
1490
  synchronousError = error;
@@ -1303,15 +1500,71 @@ var createFormController = (target, options = {}) => {
1303
1500
  }
1304
1501
  if (hasSynchronousError) throw synchronousError;
1305
1502
  };
1503
+ const handleResetEvent = (event) => {
1504
+ if (state2.isResetting) return;
1505
+ const timer = setTimeout(() => {
1506
+ state2.cleanups.delete(cancel);
1507
+ if (!event.defaultPrevented) applyReset();
1508
+ }, 0);
1509
+ const cancel = () => clearTimeout(timer);
1510
+ state2.cleanups.add(cancel);
1511
+ };
1512
+ const touchesFields = (node) => node instanceof Element && (isFieldElement(node) || node.querySelector("input, select, textarea") !== null);
1513
+ const isStructural = (record) => record.type === "attributes" || Array.from(record.addedNodes).some(touchesFields) || Array.from(record.removedNodes).some(touchesFields);
1514
+ const isOptionNode = (node) => node instanceof HTMLOptionElement || node instanceof HTMLOptGroupElement;
1515
+ const optionListOwner = (record) => {
1516
+ if (record.type !== "childList") return null;
1517
+ const touchesOptions = Array.from(record.addedNodes).some(isOptionNode) || Array.from(record.removedNodes).some(isOptionNode);
1518
+ if (!touchesOptions) return null;
1519
+ const target2 = record.target;
1520
+ if (target2 instanceof HTMLSelectElement) return target2;
1521
+ if (target2 instanceof HTMLOptGroupElement && target2.parentElement instanceof HTMLSelectElement) {
1522
+ return target2.parentElement;
1523
+ }
1524
+ return null;
1525
+ };
1526
+ const handleMutations = (records) => {
1527
+ if (state2.isDestroyed) return;
1528
+ const structural = records.some(isStructural);
1529
+ const changedNames = /* @__PURE__ */ new Set();
1530
+ for (const record of records) {
1531
+ const select = optionListOwner(record);
1532
+ if (!select || !select.name) continue;
1533
+ const next = readSelectSnapshot(select);
1534
+ if (selectSnapshots.get(select) !== next) changedNames.add(select.name);
1535
+ selectSnapshots.set(select, next);
1536
+ }
1537
+ if (!structural && changedNames.size === 0) return;
1538
+ clearFieldCache();
1539
+ if (structural) {
1540
+ snapshotAllSelects();
1541
+ if (state2.isValidated) validateNames();
1542
+ else syncVisualState();
1543
+ } else {
1544
+ const affected = /* @__PURE__ */ new Set();
1545
+ for (const name of changedNames) {
1546
+ for (const field of getAffectedValidationNames(name)) affected.add(field);
1547
+ }
1548
+ const names = Array.from(affected);
1549
+ syncVisualState(Array.from(changedNames));
1550
+ if (state2.isValidated && names.some((field) => state2.validators[field] !== void 0)) {
1551
+ validateNames(names);
1552
+ }
1553
+ }
1554
+ for (const name of changedNames) emitFieldWatchers(name);
1555
+ notifySubscribers();
1556
+ };
1557
+ const commitOwnMutations = (selects) => {
1558
+ selects.forEach(snapshotSelect);
1559
+ const pending = state2.mutationObserver?.takeRecords() ?? [];
1560
+ const foreign = pending.filter(
1561
+ (record) => !selects.some((select) => select.contains(record.target))
1562
+ );
1563
+ if (foreign.length > 0) handleMutations(foreign);
1564
+ };
1306
1565
  const startMutationObserver = () => {
1307
1566
  if (!state2.element || typeof MutationObserver === "undefined") return;
1308
- state2.mutationObserver = new MutationObserver(() => {
1309
- clearFieldCache();
1310
- if (state2.isValidated) {
1311
- validateNames();
1312
- } else syncVisualState();
1313
- notifySubscribers();
1314
- });
1567
+ state2.mutationObserver = new MutationObserver(handleMutations);
1315
1568
  state2.mutationObserver.observe(state2.element, {
1316
1569
  childList: true,
1317
1570
  subtree: true,
@@ -1340,6 +1593,7 @@ var createFormController = (target, options = {}) => {
1340
1593
  setErrors: createSetErrors(state2, helpers),
1341
1594
  clearErrors: createClearErrors(state2, helpers),
1342
1595
  setValue: createSetValue(state2, helpers),
1596
+ setOptions: createSetOptions(state2, helpers),
1343
1597
  getValue: createGetValue(state2, helpers),
1344
1598
  getField: createGetField(state2, helpers),
1345
1599
  validate: createValidate(state2, helpers),
@@ -1358,6 +1612,8 @@ var createFormController = (target, options = {}) => {
1358
1612
  addListener2(state2.element, "input", handleDelegatedEvent);
1359
1613
  addListener2(document, "input", handleExternalDelegatedEvent);
1360
1614
  addListener2(state2.element, "submit", handleSubmitEvent);
1615
+ addListener2(state2.element, "reset", handleResetEvent);
1616
+ snapshotAllSelects();
1361
1617
  startMutationObserver();
1362
1618
  }
1363
1619
  if (normalizedOptions.autoSubmit) {
@@ -1417,4 +1673,4 @@ export {
1417
1673
  form,
1418
1674
  browser
1419
1675
  };
1420
- //# sourceMappingURL=chunk-MMHX34WB.js.map
1676
+ //# sourceMappingURL=chunk-6MA5TF6R.js.map