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