@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/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 =
|
|
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
|
|
751
|
+
const selectedValues2 = normalizedArray ?? [normalizedValue];
|
|
748
752
|
for (const option of field.options) {
|
|
749
|
-
option.selected =
|
|
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
|
|
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,
|
|
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.
|
|
811
|
-
|
|
812
|
-
|
|
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.
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|
-
|
|
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) =>
|
|
1018
|
+
var createWatch = (state2, helpers) => {
|
|
904
1019
|
const observe = createObserve(state2, helpers);
|
|
905
|
-
|
|
906
|
-
|
|
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
|
|
1132
|
-
|
|
1133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1148
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1282
|
-
if (canonical !== 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
|
-
|
|
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")
|
|
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) {
|