@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.
- package/CHANGELOG.md +316 -0
- package/README.md +119 -119
- package/dist/browser/global.d.ts +50 -16
- package/dist/browser/global.global.js +12 -6188
- package/dist/browser/global.global.js.map +1 -1
- package/dist/browser/index.cjs +340 -84
- package/dist/browser/index.cjs.map +1 -1
- package/dist/browser/index.d.cts +1 -1
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/{chunk-MMHX34WB.js → chunk-6MA5TF6R.js} +341 -85
- package/dist/chunk-6MA5TF6R.js.map +1 -0
- package/dist/index.cjs +340 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/{registry-GzopYPCR.d.ts → registry-CPe_lf8-.d.cts} +50 -4
- package/dist/{registry-GzopYPCR.d.cts → registry-CPe_lf8-.d.ts} +50 -4
- package/package.json +39 -8
- package/dist/chunk-MMHX34WB.js.map +0 -1
- package/docs/api/add-cleanup.md +0 -52
- package/docs/api/append.md +0 -96
- package/docs/api/auto-submit.md +0 -91
- package/docs/api/clear-errors.md +0 -69
- package/docs/api/destroy.md +0 -80
- package/docs/api/disable-auto-submit.md +0 -65
- package/docs/api/element.md +0 -78
- package/docs/api/form.md +0 -156
- package/docs/api/format.md +0 -270
- package/docs/api/get-data.md +0 -90
- package/docs/api/get-field.md +0 -87
- package/docs/api/get-state.md +0 -111
- package/docs/api/get-value.md +0 -114
- package/docs/api/index.md +0 -55
- package/docs/api/observe.md +0 -126
- package/docs/api/on-submit.md +0 -94
- package/docs/api/parse-form-data.md +0 -94
- package/docs/api/prefill.md +0 -89
- package/docs/api/regex.md +0 -155
- package/docs/api/reset.md +0 -61
- package/docs/api/revalidate.md +0 -52
- package/docs/api/set-errors.md +0 -97
- package/docs/api/set-value.md +0 -120
- package/docs/api/subscribe.md +0 -107
- package/docs/api/unwatch.md +0 -74
- package/docs/api/validate-field-value.md +0 -144
- package/docs/api/validate-values.md +0 -103
- package/docs/api/validate.md +0 -102
- package/docs/api/watch.md +0 -117
- package/docs/browser.md +0 -156
- package/docs/css-styling.md +0 -191
- package/docs/getting-started.md +0 -222
- package/docs/options.md +0 -326
- package/docs/recipes.md +0 -531
- package/docs/typescript.md +0 -615
- package/docs/vanilla.md +0 -12
- /package/{LICENSE → LICENSE.md} +0 -0
package/dist/browser/index.cjs
CHANGED
|
@@ -40,7 +40,7 @@ module.exports = __toCommonJS(browser_exports);
|
|
|
40
40
|
|
|
41
41
|
// src/api/append.ts
|
|
42
42
|
var createAppend = (state2, helpers) => ({ tag, content, class: className, atStart = false }) => {
|
|
43
|
-
if (!state2.element) return null;
|
|
43
|
+
if (!state2.element || state2.isDestroyed) return null;
|
|
44
44
|
helpers.clearFieldCache();
|
|
45
45
|
if (className) {
|
|
46
46
|
const classSelector = className.trim().split(/\s+/)[0];
|
|
@@ -76,6 +76,7 @@ var createAddCleanup = (state2, _helpers) => (cleanup) => {
|
|
|
76
76
|
|
|
77
77
|
// src/api/auto-submit.ts
|
|
78
78
|
var createAutoSubmit = (state2, helpers) => (next = true) => {
|
|
79
|
+
if (state2.isDestroyed) return state2.api;
|
|
79
80
|
state2.autoSubmitEnabled = next !== false;
|
|
80
81
|
if (!state2.autoSubmitEnabled && state2.autoSubmitTimer) {
|
|
81
82
|
clearTimeout(state2.autoSubmitTimer);
|
|
@@ -88,6 +89,7 @@ var createAutoSubmit = (state2, helpers) => (next = true) => {
|
|
|
88
89
|
|
|
89
90
|
// src/api/clear-errors.ts
|
|
90
91
|
var createClearErrors = (state2, helpers) => (fields) => {
|
|
92
|
+
if (state2.isDestroyed) return state2.api;
|
|
91
93
|
if (!fields) {
|
|
92
94
|
state2.manualErrors = {};
|
|
93
95
|
helpers.syncVisualState();
|
|
@@ -214,6 +216,7 @@ var isFormatChar = (char) => {
|
|
|
214
216
|
return true;
|
|
215
217
|
};
|
|
216
218
|
var restoreCursor = (field, position) => {
|
|
219
|
+
if (field.ownerDocument.activeElement !== field) return;
|
|
217
220
|
const max = field.value.length;
|
|
218
221
|
const safe = clamp(position, 0, max);
|
|
219
222
|
const apply = () => {
|
|
@@ -516,6 +519,7 @@ var createDestroy = (state2, _helpers) => () => {
|
|
|
516
519
|
}
|
|
517
520
|
}
|
|
518
521
|
state2.watchedFields.clear();
|
|
522
|
+
state2.groupWatchers.clear();
|
|
519
523
|
state2.subscribers.clear();
|
|
520
524
|
state2.submitHandlers.clear();
|
|
521
525
|
state2.fieldCache.clear();
|
|
@@ -608,7 +612,7 @@ var mergeErrors = (left, right) => {
|
|
|
608
612
|
var createGetState = (state2, helpers) => () => {
|
|
609
613
|
const values = helpers.getValues();
|
|
610
614
|
const errors = cloneErrors(helpers.getMergedErrors());
|
|
611
|
-
const filledFields =
|
|
615
|
+
const filledFields = Object.keys(values).filter((name) => {
|
|
612
616
|
const value = values[name];
|
|
613
617
|
if (Array.isArray(value)) return value.length > 0;
|
|
614
618
|
if (typeof value === "string") return value.trim().length > 0;
|
|
@@ -743,9 +747,9 @@ var writeFieldValue = (fields, value) => {
|
|
|
743
747
|
continue;
|
|
744
748
|
}
|
|
745
749
|
if (field instanceof HTMLSelectElement && field.multiple) {
|
|
746
|
-
const
|
|
750
|
+
const selectedValues2 = normalizedArray ?? [normalizedValue];
|
|
747
751
|
for (const option of field.options) {
|
|
748
|
-
option.selected =
|
|
752
|
+
option.selected = selectedValues2.includes(option.value);
|
|
749
753
|
}
|
|
750
754
|
continue;
|
|
751
755
|
}
|
|
@@ -780,7 +784,7 @@ var submitForm = (form3) => {
|
|
|
780
784
|
var createGetValue = (state2, helpers) => (name) => readFieldValue(helpers.getFieldsByName(name));
|
|
781
785
|
|
|
782
786
|
// src/api/observe.ts
|
|
783
|
-
var
|
|
787
|
+
var observeField = (state2, field, callback) => {
|
|
784
788
|
const callbacks = state2.watchedFields.get(field) ?? /* @__PURE__ */ new Set();
|
|
785
789
|
callbacks.add(callback);
|
|
786
790
|
state2.watchedFields.set(field, callbacks);
|
|
@@ -795,47 +799,72 @@ var createObserve = (state2, helpers) => (field, callback) => {
|
|
|
795
799
|
}
|
|
796
800
|
};
|
|
797
801
|
};
|
|
802
|
+
var observeFields = (state2, fields, callback) => {
|
|
803
|
+
const names = Array.from(new Set(fields));
|
|
804
|
+
if (names.length === 0) return () => void 0;
|
|
805
|
+
const entry = { fields: names, callback };
|
|
806
|
+
state2.groupWatchers.add(entry);
|
|
807
|
+
if (state2.element) {
|
|
808
|
+
const values = Object.fromEntries(names.map((name) => [name, state2.api.getValue(name)]));
|
|
809
|
+
callback(values, null, state2.element, state2.api.getState());
|
|
810
|
+
}
|
|
811
|
+
return () => {
|
|
812
|
+
state2.groupWatchers.delete(entry);
|
|
813
|
+
};
|
|
814
|
+
};
|
|
815
|
+
var createObserve = (state2, _helpers) => {
|
|
816
|
+
function observe(field, callback) {
|
|
817
|
+
if (state2.isDestroyed) return () => void 0;
|
|
818
|
+
return Array.isArray(field) ? observeFields(state2, field, callback) : observeField(state2, field, callback);
|
|
819
|
+
}
|
|
820
|
+
return observe;
|
|
821
|
+
};
|
|
798
822
|
|
|
799
823
|
// src/api/on-submit.ts
|
|
800
824
|
var createOnSubmit = (state2, _helpers) => (callback, preventDefault = true) => {
|
|
825
|
+
if (state2.isDestroyed) return state2.api;
|
|
801
826
|
state2.submitHandlers.add({ callback, preventDefault });
|
|
802
827
|
return state2.api;
|
|
803
828
|
};
|
|
804
829
|
|
|
805
830
|
// src/api/prefill.ts
|
|
806
|
-
var createPrefill = (state2,
|
|
807
|
-
if (!state2.element) return state2.api;
|
|
831
|
+
var createPrefill = (state2, helpers) => (fieldName) => {
|
|
832
|
+
if (!state2.element || state2.isDestroyed) return state2.api;
|
|
808
833
|
const params = new URLSearchParams(window.location.search);
|
|
809
|
-
params.
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
834
|
+
const keys = Array.from(new Set(params.keys())).filter(
|
|
835
|
+
(key) => !fieldName || fieldName === key
|
|
836
|
+
);
|
|
837
|
+
if (keys.length === 0) return state2.api;
|
|
838
|
+
state2.autoSubmitSuspended = true;
|
|
839
|
+
try {
|
|
840
|
+
for (const key of keys) {
|
|
841
|
+
const values = params.getAll(key);
|
|
842
|
+
state2.api.setValue(key, values.length > 1 ? values : values[0]);
|
|
843
|
+
}
|
|
844
|
+
} finally {
|
|
845
|
+
state2.autoSubmitSuspended = false;
|
|
846
|
+
}
|
|
847
|
+
helpers.scheduleAutoSubmit();
|
|
813
848
|
return state2.api;
|
|
814
849
|
};
|
|
815
850
|
|
|
816
851
|
// src/api/reset.ts
|
|
817
852
|
var createReset = (state2, helpers) => () => {
|
|
818
|
-
if (!state2.element) return state2.api;
|
|
819
|
-
state2.
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
state2.attributes.error,
|
|
827
|
-
state2.attributes.filled,
|
|
828
|
-
"aria-invalid"
|
|
829
|
-
]);
|
|
830
|
-
helpers.syncVisualState();
|
|
831
|
-
helpers.notifySubscribers();
|
|
853
|
+
if (!state2.element || state2.isDestroyed) return state2.api;
|
|
854
|
+
state2.isResetting = true;
|
|
855
|
+
try {
|
|
856
|
+
state2.element.reset();
|
|
857
|
+
} finally {
|
|
858
|
+
state2.isResetting = false;
|
|
859
|
+
}
|
|
860
|
+
helpers.applyReset();
|
|
832
861
|
return state2.api;
|
|
833
862
|
};
|
|
834
863
|
|
|
835
864
|
// src/api/validate.ts
|
|
836
865
|
var createValidate = (state2, helpers) => (fields) => {
|
|
866
|
+
if (state2.isDestroyed) return { isValid: true, errors: {} };
|
|
837
867
|
state2.isValidated = true;
|
|
838
|
-
helpers.syncVisualState(fields);
|
|
839
868
|
return helpers.validateNames(fields);
|
|
840
869
|
};
|
|
841
870
|
|
|
@@ -844,6 +873,7 @@ var createRevalidate = createValidate;
|
|
|
844
873
|
|
|
845
874
|
// src/api/set-errors.ts
|
|
846
875
|
var createSetErrors = (state2, helpers) => (fields) => {
|
|
876
|
+
if (state2.isDestroyed) return state2.api;
|
|
847
877
|
if (Array.isArray(fields)) {
|
|
848
878
|
state2.manualErrors = {
|
|
849
879
|
...state2.manualErrors,
|
|
@@ -860,8 +890,78 @@ var createSetErrors = (state2, helpers) => (fields) => {
|
|
|
860
890
|
return state2.api;
|
|
861
891
|
};
|
|
862
892
|
|
|
893
|
+
// src/api/set-options.ts
|
|
894
|
+
var isGroup = (entry) => typeof entry === "object" && entry !== null && Array.isArray(entry.options);
|
|
895
|
+
var createOption = (entry) => {
|
|
896
|
+
const option = document.createElement("option");
|
|
897
|
+
if (typeof entry === "object") {
|
|
898
|
+
option.value = String(entry.value);
|
|
899
|
+
option.textContent = entry.label ?? String(entry.value);
|
|
900
|
+
option.disabled = Boolean(entry.disabled);
|
|
901
|
+
if (entry.selected) option.defaultSelected = true;
|
|
902
|
+
} else {
|
|
903
|
+
option.value = String(entry);
|
|
904
|
+
option.textContent = String(entry);
|
|
905
|
+
}
|
|
906
|
+
return option;
|
|
907
|
+
};
|
|
908
|
+
var buildNodes = (options) => {
|
|
909
|
+
const nodes = [];
|
|
910
|
+
let hasExplicitSelection = false;
|
|
911
|
+
const track = (option) => {
|
|
912
|
+
if (option.defaultSelected) hasExplicitSelection = true;
|
|
913
|
+
return option;
|
|
914
|
+
};
|
|
915
|
+
for (const entry of options) {
|
|
916
|
+
if (entry === null || entry === void 0) continue;
|
|
917
|
+
if (isGroup(entry)) {
|
|
918
|
+
const group = document.createElement("optgroup");
|
|
919
|
+
group.label = entry.label;
|
|
920
|
+
group.disabled = Boolean(entry.disabled);
|
|
921
|
+
for (const child of entry.options) {
|
|
922
|
+
if (child === null || child === void 0) continue;
|
|
923
|
+
group.appendChild(track(createOption(child)));
|
|
924
|
+
}
|
|
925
|
+
nodes.push(group);
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
nodes.push(track(createOption(entry)));
|
|
929
|
+
}
|
|
930
|
+
return { nodes, hasExplicitSelection };
|
|
931
|
+
};
|
|
932
|
+
var selectedValues = (select) => Array.from(select.selectedOptions, (option) => option.value);
|
|
933
|
+
var restoreSelection = (select, previous) => {
|
|
934
|
+
if (previous.length === 0) return;
|
|
935
|
+
const options = Array.from(select.options);
|
|
936
|
+
if (select.multiple) {
|
|
937
|
+
for (const option of options) option.selected = previous.includes(option.value);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
const match = options.find((option) => option.value === previous[0]);
|
|
941
|
+
if (match) match.selected = true;
|
|
942
|
+
};
|
|
943
|
+
var sameValues = (left, right) => left.length === right.length && left.every((value, index) => value === right[index]);
|
|
944
|
+
var createSetOptions = (state2, helpers) => (name, options, config = {}) => {
|
|
945
|
+
if (!state2.element || state2.isDestroyed) return state2.api;
|
|
946
|
+
const selects = helpers.getFieldsByName(name).filter((field) => field instanceof HTMLSelectElement);
|
|
947
|
+
if (selects.length === 0) return state2.api;
|
|
948
|
+
const keepValue = config.keepValue ?? true;
|
|
949
|
+
const changed = [];
|
|
950
|
+
for (const select of selects) {
|
|
951
|
+
const previous = selectedValues(select);
|
|
952
|
+
const { nodes, hasExplicitSelection } = buildNodes(options);
|
|
953
|
+
select.replaceChildren(...nodes);
|
|
954
|
+
if (keepValue && !hasExplicitSelection) restoreSelection(select, previous);
|
|
955
|
+
if (!sameValues(previous, selectedValues(select))) changed.push(select);
|
|
956
|
+
}
|
|
957
|
+
helpers.commitOwnMutations(selects);
|
|
958
|
+
changed[0]?.dispatchEvent(new Event("input", { bubbles: true }));
|
|
959
|
+
return state2.api;
|
|
960
|
+
};
|
|
961
|
+
|
|
863
962
|
// src/api/set-value.ts
|
|
864
963
|
var createSetValue = (state2, helpers) => (name, value) => {
|
|
964
|
+
if (state2.isDestroyed) return state2.api;
|
|
865
965
|
const fields = helpers.getFieldsByName(name);
|
|
866
966
|
if (fields.length === 0) return state2.api;
|
|
867
967
|
writeFieldValue(fields, value);
|
|
@@ -873,6 +973,7 @@ var createSetValue = (state2, helpers) => (name, value) => {
|
|
|
873
973
|
|
|
874
974
|
// src/api/subscribe.ts
|
|
875
975
|
var createSubscribe = (state2, _helpers) => (listener) => {
|
|
976
|
+
if (state2.isDestroyed) return () => void 0;
|
|
876
977
|
state2.subscribers.add(listener);
|
|
877
978
|
listener(state2.api.getState());
|
|
878
979
|
return () => {
|
|
@@ -881,28 +982,46 @@ var createSubscribe = (state2, _helpers) => (listener) => {
|
|
|
881
982
|
};
|
|
882
983
|
|
|
883
984
|
// src/api/unwatch.ts
|
|
884
|
-
var
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
985
|
+
var sameFields = (left, right) => left.length === right.size && left.every((name) => right.has(name));
|
|
986
|
+
var createUnwatch = (state2, _helpers) => {
|
|
987
|
+
function unwatch(field, callback) {
|
|
988
|
+
if (field === void 0 || field === "") {
|
|
989
|
+
state2.watchedFields.clear();
|
|
990
|
+
state2.groupWatchers.clear();
|
|
991
|
+
return state2.api;
|
|
992
|
+
}
|
|
993
|
+
if (Array.isArray(field)) {
|
|
994
|
+
const names = new Set(field);
|
|
995
|
+
for (const entry of Array.from(state2.groupWatchers)) {
|
|
996
|
+
if (!sameFields(entry.fields, names)) continue;
|
|
997
|
+
if (callback && entry.callback !== callback) continue;
|
|
998
|
+
state2.groupWatchers.delete(entry);
|
|
999
|
+
}
|
|
1000
|
+
return state2.api;
|
|
1001
|
+
}
|
|
1002
|
+
if (!callback) {
|
|
1003
|
+
state2.watchedFields.delete(field);
|
|
1004
|
+
return state2.api;
|
|
1005
|
+
}
|
|
1006
|
+
const callbacks = state2.watchedFields.get(field);
|
|
1007
|
+
callbacks?.delete(callback);
|
|
1008
|
+
if (callbacks && callbacks.size === 0) {
|
|
1009
|
+
state2.watchedFields.delete(field);
|
|
1010
|
+
}
|
|
891
1011
|
return state2.api;
|
|
892
1012
|
}
|
|
893
|
-
|
|
894
|
-
callbacks?.delete(callback);
|
|
895
|
-
if (callbacks && callbacks.size === 0) {
|
|
896
|
-
state2.watchedFields.delete(field);
|
|
897
|
-
}
|
|
898
|
-
return state2.api;
|
|
1013
|
+
return unwatch;
|
|
899
1014
|
};
|
|
900
1015
|
|
|
901
1016
|
// src/api/watch.ts
|
|
902
|
-
var createWatch = (state2, helpers) =>
|
|
1017
|
+
var createWatch = (state2, helpers) => {
|
|
903
1018
|
const observe = createObserve(state2, helpers);
|
|
904
|
-
|
|
905
|
-
|
|
1019
|
+
function watch(field, callback) {
|
|
1020
|
+
if (Array.isArray(field)) observe(field, callback);
|
|
1021
|
+
else observe(field, callback);
|
|
1022
|
+
return state2.api;
|
|
1023
|
+
}
|
|
1024
|
+
return watch;
|
|
906
1025
|
};
|
|
907
1026
|
|
|
908
1027
|
// src/core/validation.ts
|
|
@@ -1055,6 +1174,7 @@ var createFormController = (target, options = {}) => {
|
|
|
1055
1174
|
attributes,
|
|
1056
1175
|
validators: normalizedOptions.validators ?? {},
|
|
1057
1176
|
watchedFields: /* @__PURE__ */ new Map(),
|
|
1177
|
+
groupWatchers: /* @__PURE__ */ new Set(),
|
|
1058
1178
|
subscribers: /* @__PURE__ */ new Set(),
|
|
1059
1179
|
submitHandlers: /* @__PURE__ */ new Set(),
|
|
1060
1180
|
fieldCache: /* @__PURE__ */ new Map(),
|
|
@@ -1068,6 +1188,9 @@ var createFormController = (target, options = {}) => {
|
|
|
1068
1188
|
submitCount: 0,
|
|
1069
1189
|
pendingSubmitCount: 0,
|
|
1070
1190
|
autoSubmitTimer: null,
|
|
1191
|
+
isAutoSubmitting: false,
|
|
1192
|
+
autoSubmitSuspended: false,
|
|
1193
|
+
isResetting: false,
|
|
1071
1194
|
isDestroyed: false,
|
|
1072
1195
|
listeners: [],
|
|
1073
1196
|
mutationObserver: null,
|
|
@@ -1112,12 +1235,20 @@ var createFormController = (target, options = {}) => {
|
|
|
1112
1235
|
state2.fieldCache.set(name, fields);
|
|
1113
1236
|
return fields;
|
|
1114
1237
|
};
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1117
|
-
|
|
1238
|
+
const indexFieldsByName = () => {
|
|
1239
|
+
const index = /* @__PURE__ */ new Map();
|
|
1240
|
+
if (!state2.element) return index;
|
|
1118
1241
|
for (const field of getNamedFields(state2.element)) {
|
|
1119
|
-
|
|
1242
|
+
const bucket = index.get(field.name);
|
|
1243
|
+
if (bucket) bucket.push(field);
|
|
1244
|
+
else index.set(field.name, [field]);
|
|
1120
1245
|
}
|
|
1246
|
+
return index;
|
|
1247
|
+
};
|
|
1248
|
+
const getTrackedFieldNames = (index = indexFieldsByName()) => {
|
|
1249
|
+
if (!state2.element) return Object.keys(state2.validators);
|
|
1250
|
+
const names = new Set(Object.keys(state2.validators));
|
|
1251
|
+
for (const name of index.keys()) names.add(name);
|
|
1121
1252
|
for (const [canonical, entry] of state2.formattedFields) {
|
|
1122
1253
|
names.add(canonical);
|
|
1123
1254
|
if (entry.displayName && entry.displayName !== canonical) {
|
|
@@ -1128,8 +1259,9 @@ var createFormController = (target, options = {}) => {
|
|
|
1128
1259
|
};
|
|
1129
1260
|
const getValues = () => {
|
|
1130
1261
|
const values = {};
|
|
1131
|
-
|
|
1132
|
-
|
|
1262
|
+
const index = indexFieldsByName();
|
|
1263
|
+
for (const name of getTrackedFieldNames(index)) {
|
|
1264
|
+
values[name] = readFieldValue(index.get(name) ?? []);
|
|
1133
1265
|
}
|
|
1134
1266
|
return values;
|
|
1135
1267
|
};
|
|
@@ -1213,7 +1345,7 @@ var createFormController = (target, options = {}) => {
|
|
|
1213
1345
|
}
|
|
1214
1346
|
return affected;
|
|
1215
1347
|
};
|
|
1216
|
-
const
|
|
1348
|
+
const emitSingleWatchers = (name) => {
|
|
1217
1349
|
if (!state2.element || !state2.api) return;
|
|
1218
1350
|
const callbacks = state2.watchedFields.get(name);
|
|
1219
1351
|
if (!callbacks || callbacks.size === 0) return;
|
|
@@ -1222,29 +1354,85 @@ var createFormController = (target, options = {}) => {
|
|
|
1222
1354
|
const snapshot = state2.api.getState();
|
|
1223
1355
|
callbacks.forEach((callback) => callback(value, fieldElement, state2.element, snapshot));
|
|
1224
1356
|
};
|
|
1357
|
+
const emitGroupWatchers = (names) => {
|
|
1358
|
+
if (!state2.element || !state2.api || state2.groupWatchers.size === 0) return;
|
|
1359
|
+
let snapshot;
|
|
1360
|
+
for (const entry of Array.from(state2.groupWatchers)) {
|
|
1361
|
+
const changed = names ? names.find((name) => entry.fields.includes(name)) : null;
|
|
1362
|
+
if (changed === void 0) continue;
|
|
1363
|
+
snapshot ?? (snapshot = state2.api.getState());
|
|
1364
|
+
const values = Object.fromEntries(
|
|
1365
|
+
entry.fields.map((field) => [field, state2.api.getValue(field)])
|
|
1366
|
+
);
|
|
1367
|
+
entry.callback(values, changed, state2.element, snapshot);
|
|
1368
|
+
}
|
|
1369
|
+
};
|
|
1370
|
+
const emitFieldWatchers = (name) => {
|
|
1371
|
+
emitSingleWatchers(name);
|
|
1372
|
+
emitGroupWatchers([name]);
|
|
1373
|
+
};
|
|
1374
|
+
const runAutoSubmit = () => {
|
|
1375
|
+
state2.autoSubmitTimer = null;
|
|
1376
|
+
if (!state2.element || state2.isDestroyed || !state2.autoSubmitEnabled) return;
|
|
1377
|
+
state2.isAutoSubmitting = true;
|
|
1378
|
+
try {
|
|
1379
|
+
submitForm(state2.element);
|
|
1380
|
+
} finally {
|
|
1381
|
+
state2.isAutoSubmitting = false;
|
|
1382
|
+
}
|
|
1383
|
+
};
|
|
1225
1384
|
const scheduleAutoSubmit = () => {
|
|
1226
|
-
if (!state2.element || !state2.autoSubmitEnabled) return;
|
|
1385
|
+
if (!state2.element || !state2.autoSubmitEnabled || state2.autoSubmitSuspended) return;
|
|
1227
1386
|
if (state2.autoSubmitTimer) clearTimeout(state2.autoSubmitTimer);
|
|
1387
|
+
state2.autoSubmitTimer = null;
|
|
1228
1388
|
if (state2.autoSubmitDebounce > 0) {
|
|
1229
|
-
state2.autoSubmitTimer = setTimeout(
|
|
1230
|
-
() => submitForm(state2.element),
|
|
1231
|
-
state2.autoSubmitDebounce
|
|
1232
|
-
);
|
|
1389
|
+
state2.autoSubmitTimer = setTimeout(runAutoSubmit, state2.autoSubmitDebounce);
|
|
1233
1390
|
return;
|
|
1234
1391
|
}
|
|
1235
|
-
|
|
1392
|
+
runAutoSubmit();
|
|
1393
|
+
};
|
|
1394
|
+
const selectSnapshots = /* @__PURE__ */ new WeakMap();
|
|
1395
|
+
const readSelectSnapshot = (select) => Array.from(select.selectedOptions, (option) => option.value).join("\0");
|
|
1396
|
+
const snapshotSelect = (select) => {
|
|
1397
|
+
selectSnapshots.set(select, readSelectSnapshot(select));
|
|
1398
|
+
};
|
|
1399
|
+
const snapshotAllSelects = () => {
|
|
1400
|
+
if (!state2.element) return;
|
|
1401
|
+
for (const field of getNamedFields(state2.element)) {
|
|
1402
|
+
if (field instanceof HTMLSelectElement) snapshotSelect(field);
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
const emitAllFieldWatchers = () => {
|
|
1406
|
+
for (const name of Array.from(state2.watchedFields.keys())) emitSingleWatchers(name);
|
|
1407
|
+
emitGroupWatchers(null);
|
|
1408
|
+
};
|
|
1409
|
+
const applyReset = () => {
|
|
1410
|
+
if (!state2.element || state2.isDestroyed) return;
|
|
1411
|
+
clearFieldCache();
|
|
1412
|
+
resetFormattedFields(state2);
|
|
1413
|
+
state2.manualErrors = {};
|
|
1414
|
+
state2.validationErrors = {};
|
|
1415
|
+
state2.validationGroupErrors = {};
|
|
1416
|
+
state2.validationElementErrors.clear();
|
|
1417
|
+
clearAttributes(state2.element, [attributes.error, attributes.filled, "aria-invalid"]);
|
|
1418
|
+
syncVisualState();
|
|
1419
|
+
snapshotAllSelects();
|
|
1420
|
+
emitAllFieldWatchers();
|
|
1421
|
+
notifySubscribers();
|
|
1236
1422
|
};
|
|
1237
1423
|
const helpers = {
|
|
1238
1424
|
notifySubscribers,
|
|
1239
1425
|
clearFieldCache,
|
|
1240
1426
|
getFieldsByName,
|
|
1241
|
-
getTrackedFieldNames,
|
|
1427
|
+
getTrackedFieldNames: () => getTrackedFieldNames(),
|
|
1242
1428
|
getValues,
|
|
1243
1429
|
getMergedErrors,
|
|
1244
1430
|
syncVisualState,
|
|
1245
1431
|
validateNames,
|
|
1246
1432
|
emitFieldWatchers,
|
|
1247
|
-
scheduleAutoSubmit
|
|
1433
|
+
scheduleAutoSubmit,
|
|
1434
|
+
applyReset,
|
|
1435
|
+
commitOwnMutations: (selects) => commitOwnMutations(selects)
|
|
1248
1436
|
};
|
|
1249
1437
|
const handleDelegatedEvent = (event) => {
|
|
1250
1438
|
const target2 = event.target;
|
|
@@ -1252,6 +1440,7 @@ var createFormController = (target, options = {}) => {
|
|
|
1252
1440
|
const name = target2.name;
|
|
1253
1441
|
if (!name) return;
|
|
1254
1442
|
clearFieldCache();
|
|
1443
|
+
if (target2 instanceof HTMLSelectElement) snapshotSelect(target2);
|
|
1255
1444
|
const canonical = resolveCanonicalForName(state2, name) ?? name;
|
|
1256
1445
|
const displayName = resolveDisplayNameForName(state2, name);
|
|
1257
1446
|
if (normalizedOptions.clearManualErrorsOnChange) {
|
|
@@ -1262,8 +1451,9 @@ var createFormController = (target, options = {}) => {
|
|
|
1262
1451
|
if (state2.isValidated && affectedValidationNames.some((field) => state2.validators[field] !== void 0)) {
|
|
1263
1452
|
validateNames(affectedValidationNames);
|
|
1264
1453
|
}
|
|
1265
|
-
|
|
1266
|
-
if (canonical !== name)
|
|
1454
|
+
emitSingleWatchers(canonical);
|
|
1455
|
+
if (canonical !== name) emitSingleWatchers(name);
|
|
1456
|
+
emitGroupWatchers(canonical !== name ? [canonical, name] : [canonical]);
|
|
1267
1457
|
notifySubscribers();
|
|
1268
1458
|
scheduleAutoSubmit();
|
|
1269
1459
|
};
|
|
@@ -1273,6 +1463,23 @@ var createFormController = (target, options = {}) => {
|
|
|
1273
1463
|
if (target2.form !== state2.element || state2.element?.contains(target2)) return;
|
|
1274
1464
|
handleDelegatedEvent(event);
|
|
1275
1465
|
};
|
|
1466
|
+
const isFocusable = (candidate) => {
|
|
1467
|
+
if (!candidate.isConnected || candidate.disabled || candidate.hidden) return false;
|
|
1468
|
+
return !(candidate instanceof HTMLInputElement && candidate.type === "hidden");
|
|
1469
|
+
};
|
|
1470
|
+
const focusFirstInvalidField = (errors) => {
|
|
1471
|
+
const candidates = [];
|
|
1472
|
+
for (const name of Object.keys(errors)) {
|
|
1473
|
+
const fields = getFieldsByName(resolveDisplayNameForName(state2, name)).filter(isFocusable);
|
|
1474
|
+
const elementErrors = state2.validationElementErrors.get(name);
|
|
1475
|
+
const field = fields.find((candidate) => elementErrors?.has(candidate)) ?? fields[0];
|
|
1476
|
+
if (field) candidates.push(field);
|
|
1477
|
+
}
|
|
1478
|
+
candidates.sort(
|
|
1479
|
+
(left, right) => left.compareDocumentPosition(right) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1
|
|
1480
|
+
);
|
|
1481
|
+
candidates[0]?.focus();
|
|
1482
|
+
};
|
|
1276
1483
|
const handleSubmitEvent = (event) => {
|
|
1277
1484
|
if (!state2.element || state2.isDestroyed || !state2.api) return;
|
|
1278
1485
|
if (normalizedOptions.clearErrorsOnSubmit) state2.manualErrors = {};
|
|
@@ -1283,23 +1490,7 @@ var createFormController = (target, options = {}) => {
|
|
|
1283
1490
|
state2.submitCount += 1;
|
|
1284
1491
|
notifySubscribers();
|
|
1285
1492
|
if (!validation.isValid) {
|
|
1286
|
-
|
|
1287
|
-
const displayName = resolveDisplayNameForName(state2, name);
|
|
1288
|
-
const fields = getFieldsByName(displayName);
|
|
1289
|
-
const elementErrors = state2.validationElementErrors.get(name);
|
|
1290
|
-
const field = fields.find((candidate2) => {
|
|
1291
|
-
if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
|
|
1292
|
-
if (candidate2 instanceof HTMLInputElement && candidate2.type === "hidden") return false;
|
|
1293
|
-
return elementErrors?.has(candidate2);
|
|
1294
|
-
}) ?? fields.find((candidate2) => {
|
|
1295
|
-
if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
|
|
1296
|
-
return !(candidate2 instanceof HTMLInputElement && candidate2.type === "hidden");
|
|
1297
|
-
});
|
|
1298
|
-
if (field) {
|
|
1299
|
-
field.focus();
|
|
1300
|
-
break;
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1493
|
+
if (!state2.isAutoSubmitting) focusFirstInvalidField(validation.errors);
|
|
1303
1494
|
return;
|
|
1304
1495
|
}
|
|
1305
1496
|
const candidate = event;
|
|
@@ -1312,7 +1503,13 @@ var createFormController = (target, options = {}) => {
|
|
|
1312
1503
|
try {
|
|
1313
1504
|
for (const handler of handlers) {
|
|
1314
1505
|
const result = handler.callback(state2.element, data, formData, snapshot);
|
|
1315
|
-
if (result && typeof result.then === "function")
|
|
1506
|
+
if (result && typeof result.then === "function") {
|
|
1507
|
+
pending.push(
|
|
1508
|
+
Promise.resolve(result).catch((error) => {
|
|
1509
|
+
console.error("[forms] submit handler failed", error);
|
|
1510
|
+
})
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1316
1513
|
}
|
|
1317
1514
|
} catch (error) {
|
|
1318
1515
|
synchronousError = error;
|
|
@@ -1328,15 +1525,71 @@ var createFormController = (target, options = {}) => {
|
|
|
1328
1525
|
}
|
|
1329
1526
|
if (hasSynchronousError) throw synchronousError;
|
|
1330
1527
|
};
|
|
1528
|
+
const handleResetEvent = (event) => {
|
|
1529
|
+
if (state2.isResetting) return;
|
|
1530
|
+
const timer = setTimeout(() => {
|
|
1531
|
+
state2.cleanups.delete(cancel);
|
|
1532
|
+
if (!event.defaultPrevented) applyReset();
|
|
1533
|
+
}, 0);
|
|
1534
|
+
const cancel = () => clearTimeout(timer);
|
|
1535
|
+
state2.cleanups.add(cancel);
|
|
1536
|
+
};
|
|
1537
|
+
const touchesFields = (node) => node instanceof Element && (isFieldElement(node) || node.querySelector("input, select, textarea") !== null);
|
|
1538
|
+
const isStructural = (record) => record.type === "attributes" || Array.from(record.addedNodes).some(touchesFields) || Array.from(record.removedNodes).some(touchesFields);
|
|
1539
|
+
const isOptionNode = (node) => node instanceof HTMLOptionElement || node instanceof HTMLOptGroupElement;
|
|
1540
|
+
const optionListOwner = (record) => {
|
|
1541
|
+
if (record.type !== "childList") return null;
|
|
1542
|
+
const touchesOptions = Array.from(record.addedNodes).some(isOptionNode) || Array.from(record.removedNodes).some(isOptionNode);
|
|
1543
|
+
if (!touchesOptions) return null;
|
|
1544
|
+
const target2 = record.target;
|
|
1545
|
+
if (target2 instanceof HTMLSelectElement) return target2;
|
|
1546
|
+
if (target2 instanceof HTMLOptGroupElement && target2.parentElement instanceof HTMLSelectElement) {
|
|
1547
|
+
return target2.parentElement;
|
|
1548
|
+
}
|
|
1549
|
+
return null;
|
|
1550
|
+
};
|
|
1551
|
+
const handleMutations = (records) => {
|
|
1552
|
+
if (state2.isDestroyed) return;
|
|
1553
|
+
const structural = records.some(isStructural);
|
|
1554
|
+
const changedNames = /* @__PURE__ */ new Set();
|
|
1555
|
+
for (const record of records) {
|
|
1556
|
+
const select = optionListOwner(record);
|
|
1557
|
+
if (!select || !select.name) continue;
|
|
1558
|
+
const next = readSelectSnapshot(select);
|
|
1559
|
+
if (selectSnapshots.get(select) !== next) changedNames.add(select.name);
|
|
1560
|
+
selectSnapshots.set(select, next);
|
|
1561
|
+
}
|
|
1562
|
+
if (!structural && changedNames.size === 0) return;
|
|
1563
|
+
clearFieldCache();
|
|
1564
|
+
if (structural) {
|
|
1565
|
+
snapshotAllSelects();
|
|
1566
|
+
if (state2.isValidated) validateNames();
|
|
1567
|
+
else syncVisualState();
|
|
1568
|
+
} else {
|
|
1569
|
+
const affected = /* @__PURE__ */ new Set();
|
|
1570
|
+
for (const name of changedNames) {
|
|
1571
|
+
for (const field of getAffectedValidationNames(name)) affected.add(field);
|
|
1572
|
+
}
|
|
1573
|
+
const names = Array.from(affected);
|
|
1574
|
+
syncVisualState(Array.from(changedNames));
|
|
1575
|
+
if (state2.isValidated && names.some((field) => state2.validators[field] !== void 0)) {
|
|
1576
|
+
validateNames(names);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
for (const name of changedNames) emitFieldWatchers(name);
|
|
1580
|
+
notifySubscribers();
|
|
1581
|
+
};
|
|
1582
|
+
const commitOwnMutations = (selects) => {
|
|
1583
|
+
selects.forEach(snapshotSelect);
|
|
1584
|
+
const pending = state2.mutationObserver?.takeRecords() ?? [];
|
|
1585
|
+
const foreign = pending.filter(
|
|
1586
|
+
(record) => !selects.some((select) => select.contains(record.target))
|
|
1587
|
+
);
|
|
1588
|
+
if (foreign.length > 0) handleMutations(foreign);
|
|
1589
|
+
};
|
|
1331
1590
|
const startMutationObserver = () => {
|
|
1332
1591
|
if (!state2.element || typeof MutationObserver === "undefined") return;
|
|
1333
|
-
state2.mutationObserver = new MutationObserver(
|
|
1334
|
-
clearFieldCache();
|
|
1335
|
-
if (state2.isValidated) {
|
|
1336
|
-
validateNames();
|
|
1337
|
-
} else syncVisualState();
|
|
1338
|
-
notifySubscribers();
|
|
1339
|
-
});
|
|
1592
|
+
state2.mutationObserver = new MutationObserver(handleMutations);
|
|
1340
1593
|
state2.mutationObserver.observe(state2.element, {
|
|
1341
1594
|
childList: true,
|
|
1342
1595
|
subtree: true,
|
|
@@ -1365,6 +1618,7 @@ var createFormController = (target, options = {}) => {
|
|
|
1365
1618
|
setErrors: createSetErrors(state2, helpers),
|
|
1366
1619
|
clearErrors: createClearErrors(state2, helpers),
|
|
1367
1620
|
setValue: createSetValue(state2, helpers),
|
|
1621
|
+
setOptions: createSetOptions(state2, helpers),
|
|
1368
1622
|
getValue: createGetValue(state2, helpers),
|
|
1369
1623
|
getField: createGetField(state2, helpers),
|
|
1370
1624
|
validate: createValidate(state2, helpers),
|
|
@@ -1383,6 +1637,8 @@ var createFormController = (target, options = {}) => {
|
|
|
1383
1637
|
addListener2(state2.element, "input", handleDelegatedEvent);
|
|
1384
1638
|
addListener2(document, "input", handleExternalDelegatedEvent);
|
|
1385
1639
|
addListener2(state2.element, "submit", handleSubmitEvent);
|
|
1640
|
+
addListener2(state2.element, "reset", handleResetEvent);
|
|
1641
|
+
snapshotAllSelects();
|
|
1386
1642
|
startMutationObserver();
|
|
1387
1643
|
}
|
|
1388
1644
|
if (normalizedOptions.autoSubmit) {
|