formstand 0.8.0 → 0.10.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 +511 -1
- package/README.md +19 -14
- package/dist/index.cjs +662 -250
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +166 -55
- package/dist/index.d.ts +166 -55
- package/dist/index.js +658 -253
- package/dist/index.js.map +1 -1
- package/package.json +11 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createStore } from 'zustand/vanilla';
|
|
2
|
+
import { devtools } from 'zustand/middleware';
|
|
2
3
|
import { z } from 'zod';
|
|
3
|
-
import { useRef, useEffect, createContext, useMemo, useCallback,
|
|
4
|
-
import { jsxs,
|
|
4
|
+
import { useRef, useEffect, createContext, useMemo, useCallback, useContext, useId, useState } from 'react';
|
|
5
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
5
6
|
import { useStore } from 'zustand/react';
|
|
6
7
|
import { useShallow } from 'zustand/react/shallow';
|
|
7
8
|
|
|
@@ -17,6 +18,20 @@ var moveFromTo = (from, to) => (n) => {
|
|
|
17
18
|
return n;
|
|
18
19
|
};
|
|
19
20
|
var swapIndices = (a, b) => (n) => n === a ? b : n === b ? a : n;
|
|
21
|
+
var invertMapper = (mapper, oldLength, newLength) => {
|
|
22
|
+
const oldByNew = new Map(
|
|
23
|
+
Array.from({ length: oldLength }, (_, oldIndex) => oldIndex).flatMap(
|
|
24
|
+
(oldIndex) => {
|
|
25
|
+
const newIndex = mapper(oldIndex);
|
|
26
|
+
return newIndex === null ? [] : [[newIndex, oldIndex]];
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
);
|
|
30
|
+
return Array.from(
|
|
31
|
+
{ length: newLength },
|
|
32
|
+
(_, newIndex) => oldByNew.get(newIndex) ?? -1
|
|
33
|
+
);
|
|
34
|
+
};
|
|
20
35
|
var indexAndTail = (rest) => {
|
|
21
36
|
const dotIdx = rest.indexOf(".");
|
|
22
37
|
const idxStr = dotIdx === -1 ? rest : rest.slice(0, dotIdx);
|
|
@@ -40,6 +55,53 @@ var reKeyByArrayPath = (map, basePath, mapper) => {
|
|
|
40
55
|
);
|
|
41
56
|
};
|
|
42
57
|
|
|
58
|
+
// src/core/arrayOpLog.ts
|
|
59
|
+
var OP_LOG_LIMIT = 16;
|
|
60
|
+
var MAX_LOG_PATHS = 256;
|
|
61
|
+
var composeOps = (first, second) => ({
|
|
62
|
+
from: first.from,
|
|
63
|
+
to: second.to,
|
|
64
|
+
mapping: second.mapping.map(
|
|
65
|
+
(mid) => mid === -1 ? -1 : first.mapping[mid] ?? -1
|
|
66
|
+
)
|
|
67
|
+
});
|
|
68
|
+
var logs = /* @__PURE__ */ new WeakMap();
|
|
69
|
+
var boundOps = (ops) => {
|
|
70
|
+
const [head, second] = ops;
|
|
71
|
+
if (ops.length <= OP_LOG_LIMIT || head === void 0 || second === void 0) {
|
|
72
|
+
return ops;
|
|
73
|
+
}
|
|
74
|
+
return head.to === second.from ? [composeOps(head, second), ...ops.slice(2)] : ops.slice(1);
|
|
75
|
+
};
|
|
76
|
+
var recordArrayOp = (store, path, record) => {
|
|
77
|
+
const existing = logs.get(store);
|
|
78
|
+
const byPath = existing ?? /* @__PURE__ */ new Map();
|
|
79
|
+
if (existing === void 0) logs.set(store, byPath);
|
|
80
|
+
const bounded = boundOps([...byPath.get(path) ?? [], record]);
|
|
81
|
+
byPath.delete(path);
|
|
82
|
+
byPath.set(path, bounded);
|
|
83
|
+
if (byPath.size > MAX_LOG_PATHS) {
|
|
84
|
+
const oldest = byPath.keys().next().value;
|
|
85
|
+
if (oldest !== void 0) byPath.delete(oldest);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
var arrayOpsFor = (store, path) => logs.get(store)?.get(path) ?? [];
|
|
89
|
+
var clearArrayOps = (store, path) => {
|
|
90
|
+
logs.get(store)?.delete(path);
|
|
91
|
+
};
|
|
92
|
+
var clearArrayOpsUnder = (store, path, keepSelf = false) => {
|
|
93
|
+
const byPath = logs.get(store);
|
|
94
|
+
if (byPath === void 0) return;
|
|
95
|
+
if (path === "") {
|
|
96
|
+
byPath.clear();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const related = [...byPath.keys()].filter(
|
|
100
|
+
(p) => p === path && !keepSelf || p.startsWith(`${path}.`)
|
|
101
|
+
);
|
|
102
|
+
related.forEach((p) => byPath.delete(p));
|
|
103
|
+
};
|
|
104
|
+
|
|
43
105
|
// src/core/equality.ts
|
|
44
106
|
var isPlainObject = (v) => {
|
|
45
107
|
if (typeof v !== "object" || v === null) return false;
|
|
@@ -85,7 +147,18 @@ var isIndexSegment = (s) => {
|
|
|
85
147
|
const n = Number(s);
|
|
86
148
|
return Number.isInteger(n) && n >= 0 && String(n) === s;
|
|
87
149
|
};
|
|
88
|
-
var
|
|
150
|
+
var PARSE_CACHE_MAX = 4096;
|
|
151
|
+
var parseCache = /* @__PURE__ */ new Map();
|
|
152
|
+
var parsePath = (path) => {
|
|
153
|
+
const cached = parseCache.get(path);
|
|
154
|
+
if (cached !== void 0) return cached;
|
|
155
|
+
const segments = Object.freeze(
|
|
156
|
+
path === "" ? [] : path.split(".").map((s) => isIndexSegment(s) ? Number(s) : s)
|
|
157
|
+
);
|
|
158
|
+
if (parseCache.size >= PARSE_CACHE_MAX) parseCache.clear();
|
|
159
|
+
parseCache.set(path, segments);
|
|
160
|
+
return segments;
|
|
161
|
+
};
|
|
89
162
|
var isPlainRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
90
163
|
var readSegment = (obj, segment) => {
|
|
91
164
|
if (obj == null) return void 0;
|
|
@@ -98,8 +171,9 @@ var getAtPath = (obj, path) => parsePath(path).reduce(readSegment, obj);
|
|
|
98
171
|
var slotAtPath = (obj, path) => {
|
|
99
172
|
const walked = parsePath(path).reduce(
|
|
100
173
|
(acc, segment) => {
|
|
101
|
-
const
|
|
102
|
-
|
|
174
|
+
const container = acc.current;
|
|
175
|
+
const addressable = typeof segment === "number" ? Array.isArray(container) ? segment < container.length : isPlainRecord(container) : isPlainRecord(container);
|
|
176
|
+
return !acc.ok || !addressable ? { ok: false, current: void 0 } : { ok: true, current: readSegment(container, segment) };
|
|
103
177
|
},
|
|
104
178
|
{ ok: true, current: obj }
|
|
105
179
|
);
|
|
@@ -163,7 +237,7 @@ var expandIssue = (issue, prefix) => {
|
|
|
163
237
|
return [{ path: issuePathToFormPath(at), message: issue.message }];
|
|
164
238
|
};
|
|
165
239
|
var flattenIssues = (issues) => issues.flatMap((issue) => expandIssue(issue, [])).reduce((acc, { path, message }) => {
|
|
166
|
-
const existing = acc[path] ?? [];
|
|
240
|
+
const existing = (Object.hasOwn(acc, path) ? acc[path] : void 0) ?? [];
|
|
167
241
|
return existing.includes(message) ? acc : { ...acc, [path]: [...existing, message] };
|
|
168
242
|
}, {});
|
|
169
243
|
var validateSync = (schema, values) => {
|
|
@@ -263,7 +337,7 @@ var releaseAncestorEntries = (map, path) => {
|
|
|
263
337
|
return kept.length === entries.length ? map : Object.fromEntries(kept);
|
|
264
338
|
};
|
|
265
339
|
var dirtyPathsOf = (values, initial, prefix) => {
|
|
266
|
-
if (
|
|
340
|
+
if (Object.is(values, initial)) return [];
|
|
267
341
|
if (isPlainObject(values) && isPlainObject(initial)) {
|
|
268
342
|
const keys = /* @__PURE__ */ new Set([...Object.keys(values), ...Object.keys(initial)]);
|
|
269
343
|
const childPaths = [...keys].flatMap(
|
|
@@ -273,9 +347,11 @@ var dirtyPathsOf = (values, initial, prefix) => {
|
|
|
273
347
|
prefix === "" ? k : `${prefix}.${k}`
|
|
274
348
|
)
|
|
275
349
|
);
|
|
276
|
-
|
|
350
|
+
if (childPaths.length > 0) return childPaths;
|
|
351
|
+
const sameKeySet = Object.keys(values).length === Object.keys(initial).length && Object.keys(values).every((k) => Object.hasOwn(initial, k));
|
|
352
|
+
return sameKeySet ? [] : [prefix];
|
|
277
353
|
}
|
|
278
|
-
return [prefix];
|
|
354
|
+
return valuesEqual(values, initial) ? [] : [prefix];
|
|
279
355
|
};
|
|
280
356
|
var FORM_SEQ_KEY = "__form__";
|
|
281
357
|
var SUBMIT_SEQ_KEY = "__submit__";
|
|
@@ -294,7 +370,9 @@ var reuseErrorRefs = (next, prev) => {
|
|
|
294
370
|
};
|
|
295
371
|
var scopeSettledResult = (result, path, viaSubschema) => result.kind !== "invalid" ? emptyErrors : viaSubschema ? prefixErrorKeys(result.errors, path) : path === "" ? result.errors : pickScope(result.errors, [path]);
|
|
296
372
|
var mergeErrorChannels = (schema, server) => {
|
|
297
|
-
const extra = Object.entries(server).filter(
|
|
373
|
+
const extra = Object.entries(server).filter(
|
|
374
|
+
([k]) => !Object.hasOwn(schema, k)
|
|
375
|
+
);
|
|
298
376
|
return extra.length === 0 ? schema : { ...schema, ...Object.fromEntries(extra) };
|
|
299
377
|
};
|
|
300
378
|
var errorChannels = (state, patch) => {
|
|
@@ -337,13 +415,27 @@ var createForm = (schema, options) => {
|
|
|
337
415
|
mode: initialMode,
|
|
338
416
|
reValidateMode: initialReValidateMode
|
|
339
417
|
};
|
|
340
|
-
const store = createStore(() => initial)
|
|
418
|
+
const store = options.devtools === void 0 || options.devtools === false ? createStore(() => initial) : createStore()(
|
|
419
|
+
devtools(() => initial, {
|
|
420
|
+
name: typeof options.devtools === "string" ? options.devtools : "formstand",
|
|
421
|
+
// Non-production only, matching zustand's own devtools default:
|
|
422
|
+
// opting in during development must NOT stream every keystroke to
|
|
423
|
+
// an end user who happens to have the extension installed, where
|
|
424
|
+
// a module-singleton form's unbounded time-travel history is a
|
|
425
|
+
// real memory + serialization cost. Guarded for environments
|
|
426
|
+
// without process (a bundler defines NODE_ENV in real dev).
|
|
427
|
+
enabled: typeof process !== "undefined" && process.env?.["NODE_ENV"] !== "production"
|
|
428
|
+
})
|
|
429
|
+
);
|
|
341
430
|
const commitFullPass = (state, next) => errorChannels(state, {
|
|
342
431
|
schemaErrors: reuseErrorRefs(next, state.schemaErrors)
|
|
343
432
|
});
|
|
344
433
|
const validate = () => {
|
|
434
|
+
if (asyncScopes.has("")) {
|
|
435
|
+
return { kind: "pending", promise: validateAsyncOnForm() };
|
|
436
|
+
}
|
|
345
437
|
try {
|
|
346
|
-
const result =
|
|
438
|
+
const result = validateSyncLatched("", schema, store.getState().values);
|
|
347
439
|
store.setState((state) => ({
|
|
348
440
|
...state,
|
|
349
441
|
...commitFullPass(
|
|
@@ -360,33 +452,65 @@ var createForm = (schema, options) => {
|
|
|
360
452
|
}
|
|
361
453
|
};
|
|
362
454
|
const validateFields = (paths) => {
|
|
363
|
-
if (paths.length === 0) return
|
|
455
|
+
if (paths.length === 0) return { kind: "valid" };
|
|
364
456
|
if (paths.some((p) => p === "")) {
|
|
365
457
|
const result = validate();
|
|
366
|
-
return result.kind === "pending" ? result.promise.then(
|
|
458
|
+
return result.kind === "pending" ? { kind: "pending", promise: result.promise.then(settledFormResult) } : settledFormResult(result);
|
|
459
|
+
}
|
|
460
|
+
const values = store.getState().values;
|
|
461
|
+
const scopes = paths.map((path) => ({
|
|
462
|
+
path,
|
|
463
|
+
scope: fieldScopeFor(path, values)
|
|
464
|
+
}));
|
|
465
|
+
const allViaSubschema = scopes.every(
|
|
466
|
+
({ scope }) => scope.kind === "skip" || scope.kind === "parse" && scope.viaSubschema
|
|
467
|
+
);
|
|
468
|
+
const anyLatched = allViaSubschema ? scopes.some(
|
|
469
|
+
({ path, scope }) => scope.kind === "parse" && asyncScopes.has(path)
|
|
470
|
+
) : asyncScopes.has("");
|
|
471
|
+
if (anyLatched) {
|
|
472
|
+
return { kind: "pending", promise: validateFieldsAsync(paths) };
|
|
367
473
|
}
|
|
368
474
|
try {
|
|
369
|
-
|
|
475
|
+
if (allViaSubschema) {
|
|
476
|
+
const merged = scopes.reduce(
|
|
477
|
+
(acc, { path, scope }) => scope.kind === "skip" ? acc : {
|
|
478
|
+
...acc,
|
|
479
|
+
...scopeSettledResult(
|
|
480
|
+
validateSyncLatched(path, scope.schema, scope.value),
|
|
481
|
+
path,
|
|
482
|
+
true
|
|
483
|
+
)
|
|
484
|
+
},
|
|
485
|
+
{}
|
|
486
|
+
);
|
|
487
|
+
store.setState((state) => ({
|
|
488
|
+
...state,
|
|
489
|
+
...commitScopedErrors(state, merged, paths)
|
|
490
|
+
}));
|
|
491
|
+
return settledFields(merged);
|
|
492
|
+
}
|
|
493
|
+
const result = validateSyncLatched("", schema, values);
|
|
370
494
|
const fullErrors = result.kind === "invalid" ? result.errors : emptyErrors;
|
|
495
|
+
const requestedErrors = pickScope(fullErrors, paths);
|
|
371
496
|
store.setState((state) => ({
|
|
372
497
|
...state,
|
|
373
|
-
...
|
|
374
|
-
schemaErrors: reuseErrorRefs(
|
|
375
|
-
mergeScopedErrors(
|
|
376
|
-
state.schemaErrors,
|
|
377
|
-
pickScope(fullErrors, paths),
|
|
378
|
-
paths
|
|
379
|
-
),
|
|
380
|
-
state.schemaErrors
|
|
381
|
-
),
|
|
382
|
-
// A validation explicitly targeting these paths supersedes any
|
|
383
|
-
// server verdict on them.
|
|
384
|
-
serverErrors: omitScope(state.serverErrors, paths)
|
|
385
|
-
})
|
|
498
|
+
...commitScopedErrors(state, requestedErrors, paths)
|
|
386
499
|
}));
|
|
387
|
-
return
|
|
500
|
+
return settledFields(requestedErrors);
|
|
501
|
+
} catch (e) {
|
|
502
|
+
if (isAsyncRequiredError(e)) {
|
|
503
|
+
return { kind: "pending", promise: validateFieldsAsync(paths) };
|
|
504
|
+
}
|
|
505
|
+
throw e;
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
const asyncScopes = /* @__PURE__ */ new Set();
|
|
509
|
+
const validateSyncLatched = (key, scopeSchema, value) => {
|
|
510
|
+
try {
|
|
511
|
+
return validateSync(scopeSchema, value);
|
|
388
512
|
} catch (e) {
|
|
389
|
-
if (isAsyncRequiredError(e))
|
|
513
|
+
if (isAsyncRequiredError(e)) asyncScopes.add(key);
|
|
390
514
|
throw e;
|
|
391
515
|
}
|
|
392
516
|
};
|
|
@@ -416,15 +540,6 @@ var createForm = (schema, options) => {
|
|
|
416
540
|
const slot = slotAtPath(values, path);
|
|
417
541
|
return slot.exists ? { kind: "parse", schema: sub, value: slot.value, viaSubschema: true } : { kind: "skip" };
|
|
418
542
|
};
|
|
419
|
-
const scopedFieldErrors = (path) => {
|
|
420
|
-
const values = store.getState().values;
|
|
421
|
-
const scope = fieldScopeFor(path, values);
|
|
422
|
-
return scope.kind === "skip" ? emptyErrors : scopeSettledResult(
|
|
423
|
-
validateSync(scope.schema, scope.value),
|
|
424
|
-
path,
|
|
425
|
-
scope.viaSubschema
|
|
426
|
-
);
|
|
427
|
-
};
|
|
428
543
|
const scopedFieldErrorsAsync = async (path, values) => {
|
|
429
544
|
const scope = fieldScopeFor(path, values);
|
|
430
545
|
return scope.kind === "skip" ? emptyErrors : scopeSettledResult(
|
|
@@ -437,17 +552,32 @@ var createForm = (schema, options) => {
|
|
|
437
552
|
const messages = Object.values(scoped).flat();
|
|
438
553
|
return messages.length === 0 ? { kind: "valid" } : { kind: "invalid", errors: messages };
|
|
439
554
|
};
|
|
440
|
-
const
|
|
555
|
+
const settledFields = (scoped) => Object.keys(scoped).length === 0 ? { kind: "valid" } : { kind: "invalid", errors: scoped };
|
|
556
|
+
const settledFormResult = (result) => settledFields(result.kind === "invalid" ? result.errors : emptyErrors);
|
|
557
|
+
const commitScopedErrors = (state, scoped, paths) => errorChannels(state, {
|
|
441
558
|
schemaErrors: reuseErrorRefs(
|
|
442
|
-
mergeScopedErrors(state.schemaErrors, scoped,
|
|
559
|
+
mergeScopedErrors(state.schemaErrors, scoped, paths),
|
|
443
560
|
state.schemaErrors
|
|
444
561
|
),
|
|
445
|
-
serverErrors: omitScope(state.serverErrors,
|
|
562
|
+
serverErrors: omitScope(state.serverErrors, paths)
|
|
446
563
|
});
|
|
564
|
+
const commitFieldErrors = (state, scoped, path) => path === "" ? commitFullPass(state, scoped) : commitScopedErrors(state, scoped, [path]);
|
|
447
565
|
const validateField = (path) => {
|
|
448
566
|
warnMissingSchemaPath("validateField", path);
|
|
567
|
+
const scope = fieldScopeFor(path, store.getState().values);
|
|
568
|
+
if (scope.kind === "parse" && asyncScopes.has(scope.viaSubschema ? path : "")) {
|
|
569
|
+
return { kind: "pending", promise: validateFieldAsync(path) };
|
|
570
|
+
}
|
|
449
571
|
try {
|
|
450
|
-
const scoped =
|
|
572
|
+
const scoped = scope.kind === "skip" ? emptyErrors : scopeSettledResult(
|
|
573
|
+
validateSyncLatched(
|
|
574
|
+
scope.viaSubschema ? path : "",
|
|
575
|
+
scope.schema,
|
|
576
|
+
scope.value
|
|
577
|
+
),
|
|
578
|
+
path,
|
|
579
|
+
scope.viaSubschema
|
|
580
|
+
);
|
|
451
581
|
store.setState((state) => ({
|
|
452
582
|
...state,
|
|
453
583
|
...commitFieldErrors(state, scoped, path)
|
|
@@ -500,10 +630,9 @@ var createForm = (schema, options) => {
|
|
|
500
630
|
return result;
|
|
501
631
|
};
|
|
502
632
|
const validateFieldsAsync = async (paths) => {
|
|
503
|
-
if (paths.length === 0) return
|
|
633
|
+
if (paths.length === 0) return { kind: "valid" };
|
|
504
634
|
if (paths.some((p) => p === "")) {
|
|
505
|
-
|
|
506
|
-
return result2.kind === "valid";
|
|
635
|
+
return settledFormResult(await validateAsyncOnForm());
|
|
507
636
|
}
|
|
508
637
|
const passes = new Map(paths.map((p) => [p, claimPass(p)]));
|
|
509
638
|
const valuesAtStart = store.getState().values;
|
|
@@ -516,9 +645,8 @@ var createForm = (schema, options) => {
|
|
|
516
645
|
}));
|
|
517
646
|
const result = await validateAsync(schema, valuesAtStart);
|
|
518
647
|
const fullErrors = result.kind === "invalid" ? result.errors : emptyErrors;
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
);
|
|
648
|
+
const requestedErrors = pickScope(fullErrors, paths);
|
|
649
|
+
const requested = settledFields(requestedErrors);
|
|
522
650
|
const live = store.getState();
|
|
523
651
|
const stillCurrent = paths.filter(
|
|
524
652
|
(p) => sequences.get(p) === passes.get(p)
|
|
@@ -533,27 +661,21 @@ var createForm = (schema, options) => {
|
|
|
533
661
|
)
|
|
534
662
|
}));
|
|
535
663
|
}
|
|
536
|
-
return
|
|
664
|
+
return requested;
|
|
537
665
|
}
|
|
538
666
|
store.setState((state) => ({
|
|
539
667
|
...state,
|
|
540
|
-
...
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
stillCurrent
|
|
546
|
-
),
|
|
547
|
-
state.schemaErrors
|
|
548
|
-
),
|
|
549
|
-
serverErrors: omitScope(state.serverErrors, stillCurrent)
|
|
550
|
-
}),
|
|
668
|
+
...commitScopedErrors(
|
|
669
|
+
state,
|
|
670
|
+
pickScope(fullErrors, stillCurrent),
|
|
671
|
+
stillCurrent
|
|
672
|
+
),
|
|
551
673
|
isValidating: stillCurrent.reduce(
|
|
552
674
|
(acc, p) => omitKey(acc, p),
|
|
553
675
|
state.isValidating
|
|
554
676
|
)
|
|
555
677
|
}));
|
|
556
|
-
return
|
|
678
|
+
return requested;
|
|
557
679
|
};
|
|
558
680
|
const validateFieldAsync = async (path) => {
|
|
559
681
|
warnMissingSchemaPath("validateFieldAsync", path);
|
|
@@ -603,12 +725,21 @@ var createForm = (schema, options) => {
|
|
|
603
725
|
);
|
|
604
726
|
return;
|
|
605
727
|
}
|
|
728
|
+
const arr = Array.isArray(current) ? current : [];
|
|
729
|
+
const next = nextArray(arr);
|
|
730
|
+
const written = setAtPath(store.getState().values, path, next);
|
|
731
|
+
clearArrayOpsUnder(store, path, true);
|
|
732
|
+
if (Array.isArray(current) && getAtPath(written, path) === next) {
|
|
733
|
+
recordArrayOp(store, path, {
|
|
734
|
+
from: current,
|
|
735
|
+
to: next,
|
|
736
|
+
mapping: invertMapper(mapper, current.length, next.length)
|
|
737
|
+
});
|
|
738
|
+
}
|
|
606
739
|
store.setState((state) => {
|
|
607
|
-
const live = getAtPath(state.values, path);
|
|
608
|
-
const arr = Array.isArray(live) ? live : [];
|
|
609
740
|
return {
|
|
610
741
|
...state,
|
|
611
|
-
values:
|
|
742
|
+
values: written,
|
|
612
743
|
...errorChannels(state, {
|
|
613
744
|
schemaErrors: reKeyByArrayPath(state.schemaErrors, path, mapper),
|
|
614
745
|
// Server entries on rows follow their rows through the index
|
|
@@ -749,24 +880,30 @@ var createForm = (schema, options) => {
|
|
|
749
880
|
}
|
|
750
881
|
});
|
|
751
882
|
},
|
|
752
|
-
setValue: (path, value) =>
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
883
|
+
setValue: (path, value) => {
|
|
884
|
+
clearArrayOpsUnder(store, path);
|
|
885
|
+
store.setState((state) => ({
|
|
886
|
+
...state,
|
|
887
|
+
values: setAtPath(state.values, path, value),
|
|
888
|
+
...errorChannels(state, {
|
|
889
|
+
serverErrors: releaseSpine(state.serverErrors, path)
|
|
890
|
+
})
|
|
891
|
+
}));
|
|
892
|
+
},
|
|
893
|
+
setValues: (next) => {
|
|
894
|
+
clearArrayOpsUnder(store, "");
|
|
895
|
+
store.setState((state) => ({
|
|
896
|
+
...state,
|
|
897
|
+
values: next,
|
|
898
|
+
...errorChannels(state, {
|
|
899
|
+
serverErrors: releaseChangedSlices(
|
|
900
|
+
state.serverErrors,
|
|
901
|
+
state.values,
|
|
902
|
+
next
|
|
903
|
+
)
|
|
904
|
+
})
|
|
905
|
+
}));
|
|
906
|
+
},
|
|
770
907
|
setTouched: (path, touched = true) => store.setState((state) => ({
|
|
771
908
|
...state,
|
|
772
909
|
touched: { ...state.touched, [path]: touched }
|
|
@@ -797,25 +934,32 @@ var createForm = (schema, options) => {
|
|
|
797
934
|
})
|
|
798
935
|
};
|
|
799
936
|
}),
|
|
800
|
-
updateState: (updater) =>
|
|
801
|
-
const
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
937
|
+
updateState: (updater) => {
|
|
938
|
+
const valuesBefore = store.getState().values;
|
|
939
|
+
store.setState((state) => {
|
|
940
|
+
const patch = updater(state);
|
|
941
|
+
if (Object.keys(patch).length === 0) return state;
|
|
942
|
+
if (patch.errors !== void 0 && patch.errors !== state.errors) {
|
|
943
|
+
console.warn(
|
|
944
|
+
"[formstand] `errors` is derived from schemaErrors/serverErrors \u2014 patch those channels instead; the direct `errors` patch is ignored."
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
const next = { ...state, ...patch };
|
|
948
|
+
return {
|
|
949
|
+
...next,
|
|
950
|
+
...errorChannels(state, {
|
|
951
|
+
schemaErrors: next.schemaErrors,
|
|
952
|
+
serverErrors: next.serverErrors
|
|
953
|
+
})
|
|
954
|
+
};
|
|
955
|
+
});
|
|
956
|
+
if (store.getState().values !== valuesBefore) {
|
|
957
|
+
clearArrayOpsUnder(store, "");
|
|
807
958
|
}
|
|
808
|
-
|
|
809
|
-
return {
|
|
810
|
-
...next,
|
|
811
|
-
...errorChannels(state, {
|
|
812
|
-
schemaErrors: next.schemaErrors,
|
|
813
|
-
serverErrors: next.serverErrors
|
|
814
|
-
})
|
|
815
|
-
};
|
|
816
|
-
}),
|
|
959
|
+
},
|
|
817
960
|
adoptValues: (values) => {
|
|
818
961
|
sequences.clear();
|
|
962
|
+
clearArrayOpsUnder(store, "");
|
|
819
963
|
store.setState((state) => ({
|
|
820
964
|
...state,
|
|
821
965
|
values,
|
|
@@ -833,6 +977,7 @@ var createForm = (schema, options) => {
|
|
|
833
977
|
},
|
|
834
978
|
reset: (nextInitial, resetOptions) => {
|
|
835
979
|
sequences.clear();
|
|
980
|
+
clearArrayOpsUnder(store, "");
|
|
836
981
|
store.setState((state) => {
|
|
837
982
|
const init = nextInitial === void 0 ? state.initialValues : isPlainObject(state.initialValues) && isPlainObject(nextInitial) ? {
|
|
838
983
|
...state.initialValues,
|
|
@@ -854,23 +999,33 @@ var createForm = (schema, options) => {
|
|
|
854
999
|
};
|
|
855
1000
|
});
|
|
856
1001
|
},
|
|
857
|
-
resetField: (path) =>
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
1002
|
+
resetField: (path) => {
|
|
1003
|
+
const initialSlot = slotAtPath(
|
|
1004
|
+
store.getState().initialValues,
|
|
1005
|
+
path
|
|
1006
|
+
);
|
|
1007
|
+
if (!initialSlot.exists) {
|
|
1008
|
+
console.warn(
|
|
1009
|
+
`[formstand] resetField("${path}"): path has no initial value (e.g. an appended array row); value left unchanged, field state cleared.`
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
if (initialSlot.exists) {
|
|
1013
|
+
clearArrayOpsUnder(store, path);
|
|
1014
|
+
}
|
|
1015
|
+
store.setState((state) => ({
|
|
1016
|
+
...state,
|
|
1017
|
+
values: initialSlot.exists ? setAtPath(state.values, path, initialSlot.value) : state.values,
|
|
1018
|
+
// A runtime "" path scopes the whole form (the imperative surface is
|
|
1019
|
+
// string-typed even though FieldPath excludes ""). Server entries
|
|
1020
|
+
// follow the setValue release contract (releaseSpine).
|
|
1021
|
+
...errorChannels(state, {
|
|
1022
|
+
schemaErrors: path === "" ? emptyErrors : omitScope(state.schemaErrors, [path]),
|
|
1023
|
+
serverErrors: releaseSpine(state.serverErrors, path)
|
|
1024
|
+
}),
|
|
1025
|
+
touched: path === "" ? emptyBools : omitScope(state.touched, [path]),
|
|
1026
|
+
isValidating: path === "" ? emptyBools : omitScope(state.isValidating, [path])
|
|
1027
|
+
}));
|
|
1028
|
+
},
|
|
874
1029
|
validate,
|
|
875
1030
|
validateField,
|
|
876
1031
|
validateFields,
|
|
@@ -924,29 +1079,32 @@ var createForm = (schema, options) => {
|
|
|
924
1079
|
return dirtyPathsOf(state.values, state.initialValues, "");
|
|
925
1080
|
},
|
|
926
1081
|
snapshot: () => store.getState(),
|
|
927
|
-
restore: (snap) =>
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1082
|
+
restore: (snap) => {
|
|
1083
|
+
clearArrayOpsUnder(store, "");
|
|
1084
|
+
store.setState(() => {
|
|
1085
|
+
const schemaErrors = snap.schemaErrors ?? emptyErrors;
|
|
1086
|
+
const serverErrors = snap.serverErrors ?? emptyErrors;
|
|
1087
|
+
return {
|
|
1088
|
+
...snap,
|
|
1089
|
+
schemaErrors,
|
|
1090
|
+
serverErrors,
|
|
1091
|
+
errors: reuseErrorRefs(
|
|
1092
|
+
mergeErrorChannels(schemaErrors, serverErrors),
|
|
1093
|
+
snap.errors ?? emptyErrors
|
|
1094
|
+
),
|
|
1095
|
+
// In-flight state is owned by live passes, never by snapshots: a
|
|
1096
|
+
// snapshotted flag has no pass left to clear it (the pass that set
|
|
1097
|
+
// it cleared its ORIGINAL entry when it settled), so restoring one
|
|
1098
|
+
// would stick it forever. isSubmitting follows the same rule — a
|
|
1099
|
+
// mid-submit snapshot restored later must not disable submit
|
|
1100
|
+
// buttons forever, so it reflects the LIVE submit count instead of
|
|
1101
|
+
// the snapshotted flag (a live pass's finally still resets it).
|
|
1102
|
+
isValidating: emptyBools,
|
|
1103
|
+
isValidatingForm: false,
|
|
1104
|
+
isSubmitting: inFlight.count > 0
|
|
1105
|
+
};
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
950
1108
|
});
|
|
951
1109
|
};
|
|
952
1110
|
|
|
@@ -966,6 +1124,74 @@ var shouldValidateOn = (trigger, mode, reValidateMode, submitAttempted, touched
|
|
|
966
1124
|
return true;
|
|
967
1125
|
}
|
|
968
1126
|
};
|
|
1127
|
+
|
|
1128
|
+
// src/core/persist.ts
|
|
1129
|
+
var defaultStorage = () => {
|
|
1130
|
+
try {
|
|
1131
|
+
return typeof globalThis.localStorage === "undefined" ? null : globalThis.localStorage;
|
|
1132
|
+
} catch {
|
|
1133
|
+
return null;
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
var persistForm = (form, options) => {
|
|
1137
|
+
const storage = options.storage ?? defaultStorage();
|
|
1138
|
+
const debounceMs = options.debounceMs ?? 300;
|
|
1139
|
+
const apply = options.apply ?? "adopt";
|
|
1140
|
+
const applyDraft = (values) => apply === "restore" ? form.setValues(values) : form.adoptValues(values);
|
|
1141
|
+
const restore = () => {
|
|
1142
|
+
try {
|
|
1143
|
+
const raw = storage === null ? null : storage.getItem(options.key);
|
|
1144
|
+
if (raw === null) return false;
|
|
1145
|
+
applyDraft(JSON.parse(raw));
|
|
1146
|
+
return true;
|
|
1147
|
+
} catch {
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
const writeDraft = (values) => {
|
|
1152
|
+
try {
|
|
1153
|
+
storage?.setItem(options.key, JSON.stringify(values));
|
|
1154
|
+
} catch {
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
const timerRef = {
|
|
1158
|
+
current: null
|
|
1159
|
+
};
|
|
1160
|
+
const cancelPending = () => {
|
|
1161
|
+
if (timerRef.current !== null) {
|
|
1162
|
+
clearTimeout(timerRef.current);
|
|
1163
|
+
timerRef.current = null;
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
if (apply !== "manual") {
|
|
1167
|
+
restore();
|
|
1168
|
+
}
|
|
1169
|
+
const unsubscribe = form.watchValues((values) => {
|
|
1170
|
+
cancelPending();
|
|
1171
|
+
if (debounceMs === 0) {
|
|
1172
|
+
writeDraft(values);
|
|
1173
|
+
} else {
|
|
1174
|
+
timerRef.current = setTimeout(() => {
|
|
1175
|
+
timerRef.current = null;
|
|
1176
|
+
writeDraft(values);
|
|
1177
|
+
}, debounceMs);
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
return Object.freeze({
|
|
1181
|
+
restore,
|
|
1182
|
+
clear: () => {
|
|
1183
|
+
cancelPending();
|
|
1184
|
+
try {
|
|
1185
|
+
storage?.removeItem(options.key);
|
|
1186
|
+
} catch {
|
|
1187
|
+
}
|
|
1188
|
+
},
|
|
1189
|
+
dispose: () => {
|
|
1190
|
+
cancelPending();
|
|
1191
|
+
unsubscribe();
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
1194
|
+
};
|
|
969
1195
|
var useForm = (schema, options) => {
|
|
970
1196
|
const formRef = useRef(null);
|
|
971
1197
|
if (formRef.current === null) {
|
|
@@ -1048,15 +1274,7 @@ function useField(form, pathArg, options) {
|
|
|
1048
1274
|
}, debounceMs);
|
|
1049
1275
|
return;
|
|
1050
1276
|
}
|
|
1051
|
-
|
|
1052
|
-
form.validateField(path);
|
|
1053
|
-
} catch (e) {
|
|
1054
|
-
if (isAsyncRequiredError(e)) {
|
|
1055
|
-
void form.validateFieldAsync(path);
|
|
1056
|
-
return;
|
|
1057
|
-
}
|
|
1058
|
-
throw e;
|
|
1059
|
-
}
|
|
1277
|
+
form.validateField(path);
|
|
1060
1278
|
}, [form, path, debounceMs]);
|
|
1061
1279
|
const setValue = useCallback(
|
|
1062
1280
|
(value) => {
|
|
@@ -1128,12 +1346,22 @@ function useField(form, pathArg, options) {
|
|
|
1128
1346
|
[slice, emptyValue, setValue, setTouched, setError, clearError, validate, validateAsync2, onBlur]
|
|
1129
1347
|
);
|
|
1130
1348
|
}
|
|
1131
|
-
|
|
1349
|
+
|
|
1350
|
+
// src/react/useVariantField.ts
|
|
1351
|
+
function useVariantField(form, unionPath, field) {
|
|
1352
|
+
return useField(
|
|
1353
|
+
form,
|
|
1354
|
+
`${unionPath}.${field}`
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1132
1357
|
var EMPTY_ITEMS = [];
|
|
1358
|
+
var mintNumbers = (needsMint) => new Map(
|
|
1359
|
+
needsMint.flatMap((mint, i) => mint ? [i] : []).map((position, k) => [position, k + 1])
|
|
1360
|
+
);
|
|
1133
1361
|
var reconcileIds = (prev, nextItems) => {
|
|
1134
1362
|
if (prev.items === nextItems) return prev;
|
|
1135
1363
|
if (prev.items.length === nextItems.length && prev.items.every((item, i) => Object.is(item, nextItems[i]))) {
|
|
1136
|
-
return prev;
|
|
1364
|
+
return { ...prev, items: nextItems };
|
|
1137
1365
|
}
|
|
1138
1366
|
const buckets = prev.items.reduce((acc, item, i) => {
|
|
1139
1367
|
const bucket = acc.get(item);
|
|
@@ -1155,15 +1383,77 @@ var reconcileIds = (prev, nextItems) => {
|
|
|
1155
1383
|
})
|
|
1156
1384
|
);
|
|
1157
1385
|
const reused = matchedIds.map((id, i) => id ?? leftoverAt.get(i) ?? null);
|
|
1158
|
-
const
|
|
1159
|
-
const count = acc[acc.length - 1] ?? 0;
|
|
1160
|
-
return [...acc, id === null ? count + 1 : count];
|
|
1161
|
-
}, []);
|
|
1162
|
-
const minted = mintCounts[mintCounts.length - 1] ?? 0;
|
|
1386
|
+
const mints = mintNumbers(reused.map((id) => id === null));
|
|
1163
1387
|
const ids = reused.map(
|
|
1164
|
-
(id, i) => id ??
|
|
1388
|
+
(id, i) => id ?? `${prev.prefix}${prev.counter + (mints.get(i) ?? 0)}`
|
|
1165
1389
|
);
|
|
1166
|
-
return {
|
|
1390
|
+
return {
|
|
1391
|
+
items: nextItems,
|
|
1392
|
+
ids,
|
|
1393
|
+
counter: prev.counter + mints.size,
|
|
1394
|
+
prefix: prev.prefix
|
|
1395
|
+
};
|
|
1396
|
+
};
|
|
1397
|
+
var mappedIdState = (prev, mapping, liveItems) => {
|
|
1398
|
+
if (liveItems.length !== mapping.length) return null;
|
|
1399
|
+
const aligned = mapping.every(
|
|
1400
|
+
(src, i) => src === -1 ? true : prev.ids[src] !== void 0 && Object.is(liveItems[i], prev.items[src])
|
|
1401
|
+
);
|
|
1402
|
+
if (!aligned) return null;
|
|
1403
|
+
const mints = mintNumbers(mapping.map((src) => src === -1));
|
|
1404
|
+
const ids = mapping.map(
|
|
1405
|
+
(src, i) => src === -1 ? `${prev.prefix}${prev.counter + (mints.get(i) ?? 0)}` : prev.ids[src]
|
|
1406
|
+
);
|
|
1407
|
+
return {
|
|
1408
|
+
items: liveItems,
|
|
1409
|
+
ids,
|
|
1410
|
+
counter: prev.counter + mints.size,
|
|
1411
|
+
prefix: prev.prefix
|
|
1412
|
+
};
|
|
1413
|
+
};
|
|
1414
|
+
var MAX_ID_PATHS = 4096;
|
|
1415
|
+
var sharedIdEntries = /* @__PURE__ */ new WeakMap();
|
|
1416
|
+
var sharedIdEntry = (store, path) => {
|
|
1417
|
+
const existingStore = sharedIdEntries.get(store);
|
|
1418
|
+
const forStore = existingStore ?? { byPath: /* @__PURE__ */ new Map(), seq: 1 };
|
|
1419
|
+
if (existingStore === void 0) sharedIdEntries.set(store, forStore);
|
|
1420
|
+
const existing = forStore.byPath.get(path);
|
|
1421
|
+
if (existing !== void 0) {
|
|
1422
|
+
forStore.byPath.delete(path);
|
|
1423
|
+
forStore.byPath.set(path, existing);
|
|
1424
|
+
return existing;
|
|
1425
|
+
}
|
|
1426
|
+
const created = {
|
|
1427
|
+
state: {
|
|
1428
|
+
items: EMPTY_ITEMS,
|
|
1429
|
+
ids: [],
|
|
1430
|
+
counter: 0,
|
|
1431
|
+
prefix: `__zfa_${forStore.seq}_`
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
forStore.seq += 1;
|
|
1435
|
+
forStore.byPath.set(path, created);
|
|
1436
|
+
if (forStore.byPath.size > MAX_ID_PATHS) {
|
|
1437
|
+
const oldest = forStore.byPath.keys().next().value;
|
|
1438
|
+
if (oldest !== void 0) forStore.byPath.delete(oldest);
|
|
1439
|
+
}
|
|
1440
|
+
return created;
|
|
1441
|
+
};
|
|
1442
|
+
var walkOps = (start, items, ops) => {
|
|
1443
|
+
const walked = ops.reduce((acc, op) => {
|
|
1444
|
+
const base = acc.items === op.from ? acc : reconcileIds(acc, op.from);
|
|
1445
|
+
return mappedIdState(base, op.mapping, op.to) ?? base;
|
|
1446
|
+
}, start);
|
|
1447
|
+
return walked.items === items ? walked : reconcileIds(walked, items);
|
|
1448
|
+
};
|
|
1449
|
+
var deriveIds = (store, path, items) => {
|
|
1450
|
+
const entry = sharedIdEntry(store, path);
|
|
1451
|
+
if (entry.state.items === items) return entry.state;
|
|
1452
|
+
const ops = arrayOpsFor(store, path);
|
|
1453
|
+
if (ops.length > 0) clearArrayOps(store, path);
|
|
1454
|
+
const next = walkOps(entry.state, items, ops);
|
|
1455
|
+
entry.state = next;
|
|
1456
|
+
return next;
|
|
1167
1457
|
};
|
|
1168
1458
|
function useFieldArray(form, pathArg) {
|
|
1169
1459
|
const path = useStore(
|
|
@@ -1182,13 +1472,7 @@ function useFieldArray(form, pathArg) {
|
|
|
1182
1472
|
);
|
|
1183
1473
|
const items = slice.items;
|
|
1184
1474
|
const error = slice.error;
|
|
1185
|
-
const
|
|
1186
|
-
const base = idEntry.form === form && idEntry.path === path ? idEntry.state : { ...EMPTY_ID_STATE, counter: idEntry.state.counter };
|
|
1187
|
-
const nextIdState = reconcileIds(base, items);
|
|
1188
|
-
if (idEntry.form !== form || idEntry.path !== path || idEntry.state !== nextIdState) {
|
|
1189
|
-
setIdEntry({ form, path, state: nextIdState });
|
|
1190
|
-
}
|
|
1191
|
-
const ids = nextIdState.ids;
|
|
1475
|
+
const ids = deriveIds(form.store, path, items).ids;
|
|
1192
1476
|
const push = useCallback(
|
|
1193
1477
|
(item) => form.arrayPush(path, item),
|
|
1194
1478
|
[form, path]
|
|
@@ -1269,6 +1553,11 @@ var capitalize = (name) => name.length === 0 ? name : name.charAt(0).toUpperCase
|
|
|
1269
1553
|
var createFormHooks = (form, name) => {
|
|
1270
1554
|
const structural = form;
|
|
1271
1555
|
const useBoundField = (path, options) => useField(structural, path, options);
|
|
1556
|
+
const useBoundVariantField = (unionPath, field) => useVariantField(
|
|
1557
|
+
structural,
|
|
1558
|
+
unionPath,
|
|
1559
|
+
field
|
|
1560
|
+
);
|
|
1272
1561
|
const useBoundFieldArray = (path) => useFieldArray(structural, path);
|
|
1273
1562
|
const useBoundSelector = (selector) => useFormSelector(structural, selector);
|
|
1274
1563
|
const useBoundSelectorShallow = (selector) => useFormSelectorShallow(structural, selector);
|
|
@@ -1280,6 +1569,7 @@ var createFormHooks = (form, name) => {
|
|
|
1280
1569
|
const prefix = capitalize(name ?? "");
|
|
1281
1570
|
return {
|
|
1282
1571
|
[`use${prefix}Field`]: useBoundField,
|
|
1572
|
+
[`use${prefix}VariantField`]: useBoundVariantField,
|
|
1283
1573
|
[`use${prefix}FieldArray`]: useBoundFieldArray,
|
|
1284
1574
|
[`use${prefix}Selector`]: useBoundSelector,
|
|
1285
1575
|
[`use${prefix}SelectorShallow`]: useBoundSelectorShallow,
|
|
@@ -1292,7 +1582,8 @@ var createFormHooks = (form, name) => {
|
|
|
1292
1582
|
};
|
|
1293
1583
|
|
|
1294
1584
|
// src/react/inputProps.ts
|
|
1295
|
-
var
|
|
1585
|
+
var hasFieldError = (error) => error !== void 0 && error.length > 0;
|
|
1586
|
+
var ariaInvalid = (field) => hasFieldError(field.error) ? true : void 0;
|
|
1296
1587
|
var numberToInputText = (value) => value === void 0 || value === null || Number.isNaN(value) ? "" : String(value);
|
|
1297
1588
|
var parseNumberText = (text) => {
|
|
1298
1589
|
if (text.trim() === "") return { kind: "empty" };
|
|
@@ -1324,6 +1615,48 @@ var numberInputProps = (field) => ({
|
|
|
1324
1615
|
},
|
|
1325
1616
|
onBlur: field.onBlur
|
|
1326
1617
|
});
|
|
1618
|
+
var dateToInputText = (value) => {
|
|
1619
|
+
if (value === void 0 || value === null || Number.isNaN(value.getTime())) {
|
|
1620
|
+
return "";
|
|
1621
|
+
}
|
|
1622
|
+
const pad = (part) => String(part).padStart(2, "0");
|
|
1623
|
+
return `${String(value.getFullYear()).padStart(4, "0")}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`;
|
|
1624
|
+
};
|
|
1625
|
+
var parseDateText = (text) => {
|
|
1626
|
+
if (text.trim() === "") return { kind: "empty" };
|
|
1627
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
|
|
1628
|
+
if (match === null) return { kind: "invalid" };
|
|
1629
|
+
const [, year, month, day] = match;
|
|
1630
|
+
const y = Number(year);
|
|
1631
|
+
const date = new Date(y, Number(month) - 1, Number(day));
|
|
1632
|
+
if (y < 100) date.setFullYear(y);
|
|
1633
|
+
return date.getFullYear() === y && date.getMonth() === Number(month) - 1 && date.getDate() === Number(day) ? { kind: "date", value: date } : { kind: "invalid" };
|
|
1634
|
+
};
|
|
1635
|
+
var dateInputProps = (field) => ({
|
|
1636
|
+
type: "date",
|
|
1637
|
+
name: field.path,
|
|
1638
|
+
value: dateToInputText(field.value),
|
|
1639
|
+
"aria-invalid": ariaInvalid(field),
|
|
1640
|
+
onChange: (e) => {
|
|
1641
|
+
const parsed = parseDateText(e.target.value);
|
|
1642
|
+
if (parsed.kind !== "date") {
|
|
1643
|
+
field.setValue(field.emptyValue);
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
const current = field.value;
|
|
1647
|
+
const next = current instanceof Date && !Number.isNaN(current.getTime()) ? new Date(
|
|
1648
|
+
parsed.value.getFullYear(),
|
|
1649
|
+
parsed.value.getMonth(),
|
|
1650
|
+
parsed.value.getDate(),
|
|
1651
|
+
current.getHours(),
|
|
1652
|
+
current.getMinutes(),
|
|
1653
|
+
current.getSeconds(),
|
|
1654
|
+
current.getMilliseconds()
|
|
1655
|
+
) : parsed.value;
|
|
1656
|
+
field.setValue(next);
|
|
1657
|
+
},
|
|
1658
|
+
onBlur: field.onBlur
|
|
1659
|
+
});
|
|
1327
1660
|
var checkboxProps = (field) => ({
|
|
1328
1661
|
type: "checkbox",
|
|
1329
1662
|
name: field.path,
|
|
@@ -1344,11 +1677,37 @@ var selectProps = (field) => ({
|
|
|
1344
1677
|
},
|
|
1345
1678
|
onBlur: field.onBlur
|
|
1346
1679
|
});
|
|
1347
|
-
var
|
|
1680
|
+
var useFieldA11y = (error) => {
|
|
1681
|
+
const id = useId();
|
|
1682
|
+
const errorId = `${id}-error`;
|
|
1683
|
+
return {
|
|
1684
|
+
id,
|
|
1685
|
+
errorId,
|
|
1686
|
+
describedBy: hasFieldError(error) ? errorId : void 0
|
|
1687
|
+
};
|
|
1688
|
+
};
|
|
1348
1689
|
var ErrorText = ({
|
|
1349
1690
|
id,
|
|
1350
1691
|
error
|
|
1351
|
-
}) =>
|
|
1692
|
+
}) => hasFieldError(error) ? /* @__PURE__ */ jsx("span", { className: "zf-error", id, role: "alert", children: error?.[0] }) : null;
|
|
1693
|
+
var FieldShell = ({
|
|
1694
|
+
id,
|
|
1695
|
+
errorId,
|
|
1696
|
+
label,
|
|
1697
|
+
error,
|
|
1698
|
+
wrapLabel,
|
|
1699
|
+
children
|
|
1700
|
+
}) => /* @__PURE__ */ jsxs("div", { className: "zf-field", children: [
|
|
1701
|
+
wrapLabel === true ? /* @__PURE__ */ jsxs("label", { htmlFor: id, className: "zf-label", children: [
|
|
1702
|
+
children,
|
|
1703
|
+
" ",
|
|
1704
|
+
label
|
|
1705
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1706
|
+
label !== void 0 ? /* @__PURE__ */ jsx("label", { htmlFor: id, className: "zf-label", children: label }) : null,
|
|
1707
|
+
children
|
|
1708
|
+
] }),
|
|
1709
|
+
/* @__PURE__ */ jsx(ErrorText, { id: errorId, error })
|
|
1710
|
+
] });
|
|
1352
1711
|
var TextField = ({
|
|
1353
1712
|
form,
|
|
1354
1713
|
path,
|
|
@@ -1358,25 +1717,60 @@ var TextField = ({
|
|
|
1358
1717
|
autoComplete,
|
|
1359
1718
|
ref
|
|
1360
1719
|
}) => {
|
|
1361
|
-
const id = useId();
|
|
1362
|
-
const errorId = `${id}-error`;
|
|
1363
1720
|
const field = useField(form, path);
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1721
|
+
const a11y = useFieldA11y(field.error);
|
|
1722
|
+
return /* @__PURE__ */ jsx(
|
|
1723
|
+
FieldShell,
|
|
1724
|
+
{
|
|
1725
|
+
id: a11y.id,
|
|
1726
|
+
errorId: a11y.errorId,
|
|
1727
|
+
label,
|
|
1728
|
+
error: field.error,
|
|
1729
|
+
children: /* @__PURE__ */ jsx(
|
|
1730
|
+
"input",
|
|
1731
|
+
{
|
|
1732
|
+
id: a11y.id,
|
|
1733
|
+
ref,
|
|
1734
|
+
...textInputProps(field),
|
|
1735
|
+
type,
|
|
1736
|
+
placeholder,
|
|
1737
|
+
autoComplete,
|
|
1738
|
+
"aria-describedby": a11y.describedBy
|
|
1739
|
+
}
|
|
1740
|
+
)
|
|
1741
|
+
}
|
|
1742
|
+
);
|
|
1743
|
+
};
|
|
1744
|
+
var DateField = ({
|
|
1745
|
+
form,
|
|
1746
|
+
path,
|
|
1747
|
+
label,
|
|
1748
|
+
min,
|
|
1749
|
+
max,
|
|
1750
|
+
ref
|
|
1751
|
+
}) => {
|
|
1752
|
+
const field = useField(form, path);
|
|
1753
|
+
const a11y = useFieldA11y(field.error);
|
|
1754
|
+
return /* @__PURE__ */ jsx(
|
|
1755
|
+
FieldShell,
|
|
1756
|
+
{
|
|
1757
|
+
id: a11y.id,
|
|
1758
|
+
errorId: a11y.errorId,
|
|
1759
|
+
label,
|
|
1760
|
+
error: field.error,
|
|
1761
|
+
children: /* @__PURE__ */ jsx(
|
|
1762
|
+
"input",
|
|
1763
|
+
{
|
|
1764
|
+
id: a11y.id,
|
|
1765
|
+
ref,
|
|
1766
|
+
...dateInputProps(field),
|
|
1767
|
+
min,
|
|
1768
|
+
max,
|
|
1769
|
+
"aria-describedby": a11y.describedBy
|
|
1770
|
+
}
|
|
1771
|
+
)
|
|
1772
|
+
}
|
|
1773
|
+
);
|
|
1380
1774
|
};
|
|
1381
1775
|
var IDLE_EDIT = { raw: null, pushed: void 0 };
|
|
1382
1776
|
var useNumberInput = (field) => {
|
|
@@ -1390,7 +1784,7 @@ var useNumberInput = (field) => {
|
|
|
1390
1784
|
name: field.path,
|
|
1391
1785
|
value: raw ?? numberToInputText(field.value),
|
|
1392
1786
|
inputMode: "decimal",
|
|
1393
|
-
"aria-invalid":
|
|
1787
|
+
"aria-invalid": hasFieldError(field.error) ? true : void 0,
|
|
1394
1788
|
onChange: (e) => {
|
|
1395
1789
|
const text = e.target.value;
|
|
1396
1790
|
const parsed = parseNumberText(text);
|
|
@@ -1423,25 +1817,29 @@ var NumberField = ({
|
|
|
1423
1817
|
placeholder,
|
|
1424
1818
|
ref
|
|
1425
1819
|
}) => {
|
|
1426
|
-
const id = useId();
|
|
1427
|
-
const errorId = `${id}-error`;
|
|
1428
1820
|
const field = useField(form, path);
|
|
1429
1821
|
const binding = useNumberInput(field);
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1822
|
+
const a11y = useFieldA11y(field.error);
|
|
1823
|
+
return /* @__PURE__ */ jsx(
|
|
1824
|
+
FieldShell,
|
|
1825
|
+
{
|
|
1826
|
+
id: a11y.id,
|
|
1827
|
+
errorId: a11y.errorId,
|
|
1828
|
+
label,
|
|
1829
|
+
error: field.error,
|
|
1830
|
+
children: /* @__PURE__ */ jsx(
|
|
1831
|
+
"input",
|
|
1832
|
+
{
|
|
1833
|
+
id: a11y.id,
|
|
1834
|
+
ref,
|
|
1835
|
+
type: "text",
|
|
1836
|
+
...binding,
|
|
1837
|
+
placeholder,
|
|
1838
|
+
"aria-describedby": a11y.describedBy
|
|
1839
|
+
}
|
|
1840
|
+
)
|
|
1841
|
+
}
|
|
1842
|
+
);
|
|
1445
1843
|
};
|
|
1446
1844
|
var CheckboxField = ({
|
|
1447
1845
|
form,
|
|
@@ -1449,25 +1847,27 @@ var CheckboxField = ({
|
|
|
1449
1847
|
label,
|
|
1450
1848
|
ref
|
|
1451
1849
|
}) => {
|
|
1452
|
-
const id = useId();
|
|
1453
|
-
const errorId = `${id}-error`;
|
|
1454
1850
|
const field = useField(form, path);
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1851
|
+
const a11y = useFieldA11y(field.error);
|
|
1852
|
+
return /* @__PURE__ */ jsx(
|
|
1853
|
+
FieldShell,
|
|
1854
|
+
{
|
|
1855
|
+
id: a11y.id,
|
|
1856
|
+
errorId: a11y.errorId,
|
|
1857
|
+
label,
|
|
1858
|
+
error: field.error,
|
|
1859
|
+
wrapLabel: true,
|
|
1860
|
+
children: /* @__PURE__ */ jsx(
|
|
1458
1861
|
"input",
|
|
1459
1862
|
{
|
|
1460
|
-
id,
|
|
1863
|
+
id: a11y.id,
|
|
1461
1864
|
ref,
|
|
1462
1865
|
...checkboxProps(field),
|
|
1463
|
-
"aria-describedby":
|
|
1866
|
+
"aria-describedby": a11y.describedBy
|
|
1464
1867
|
}
|
|
1465
|
-
)
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
] }),
|
|
1469
|
-
/* @__PURE__ */ jsx(ErrorText, { id: errorId, error: field.error })
|
|
1470
|
-
] });
|
|
1868
|
+
)
|
|
1869
|
+
}
|
|
1870
|
+
);
|
|
1471
1871
|
};
|
|
1472
1872
|
var SelectField = ({
|
|
1473
1873
|
form,
|
|
@@ -1477,43 +1877,48 @@ var SelectField = ({
|
|
|
1477
1877
|
placeholder,
|
|
1478
1878
|
ref
|
|
1479
1879
|
}) => {
|
|
1480
|
-
const id = useId();
|
|
1481
|
-
const errorId = `${id}-error`;
|
|
1482
1880
|
const field = useField(form, path);
|
|
1881
|
+
const a11y = useFieldA11y(field.error);
|
|
1882
|
+
const select = selectProps(field);
|
|
1483
1883
|
const clearable = field.emptyValue === null;
|
|
1484
|
-
const
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1884
|
+
const hasExplicitEmptyOption = options.some((opt) => opt.value === "");
|
|
1885
|
+
const showEmptyOption = !hasExplicitEmptyOption && (clearable || select.value === "" || placeholder !== void 0);
|
|
1886
|
+
return /* @__PURE__ */ jsx(
|
|
1887
|
+
FieldShell,
|
|
1888
|
+
{
|
|
1889
|
+
id: a11y.id,
|
|
1890
|
+
errorId: a11y.errorId,
|
|
1891
|
+
label,
|
|
1892
|
+
error: field.error,
|
|
1893
|
+
children: /* @__PURE__ */ jsxs(
|
|
1894
|
+
"select",
|
|
1895
|
+
{
|
|
1896
|
+
id: a11y.id,
|
|
1897
|
+
ref,
|
|
1898
|
+
...select,
|
|
1899
|
+
"aria-describedby": a11y.describedBy,
|
|
1900
|
+
children: [
|
|
1901
|
+
showEmptyOption ? /* @__PURE__ */ jsx("option", { value: "", disabled: !clearable, children: placeholder ?? "" }) : null,
|
|
1902
|
+
options.map((opt, index) => (
|
|
1903
|
+
// Index-qualified: option lists may legitimately repeat a value
|
|
1904
|
+
// (e.g. differently-labelled aliases), and bare value keys would
|
|
1905
|
+
// collide.
|
|
1906
|
+
/* @__PURE__ */ jsx("option", { value: opt.value, children: opt.label }, `${index}-${opt.value}`)
|
|
1907
|
+
))
|
|
1908
|
+
]
|
|
1909
|
+
}
|
|
1910
|
+
)
|
|
1911
|
+
}
|
|
1912
|
+
);
|
|
1507
1913
|
};
|
|
1508
1914
|
|
|
1509
1915
|
// src/react/focusError.ts
|
|
1510
|
-
var
|
|
1916
|
+
var namedControls = (scope) => [
|
|
1511
1917
|
...scope.querySelectorAll(
|
|
1512
1918
|
"input[name], select[name], textarea[name]"
|
|
1513
1919
|
)
|
|
1514
|
-
]
|
|
1515
|
-
|
|
1516
|
-
);
|
|
1920
|
+
];
|
|
1921
|
+
var isFocusCandidate = (el) => !el.matches(':disabled, [type="hidden"]') && el.closest("dialog:not([open])") === null;
|
|
1517
1922
|
var holdsFocus = (el) => {
|
|
1518
1923
|
const rootNode = el.getRootNode();
|
|
1519
1924
|
const active = rootNode instanceof Document || rootNode instanceof ShadowRoot ? rootNode.activeElement : document.activeElement;
|
|
@@ -1533,19 +1938,19 @@ var focusFirstError = (errors, root) => {
|
|
|
1533
1938
|
(k) => k !== "" && (errors[k]?.length ?? 0) > 0
|
|
1534
1939
|
);
|
|
1535
1940
|
const hasRootError = (errors[""]?.length ?? 0) > 0;
|
|
1536
|
-
const controls =
|
|
1537
|
-
const fieldMatches = controls.filter(
|
|
1538
|
-
(el) => nameMatchesAny(el, erroredPaths)
|
|
1539
|
-
);
|
|
1941
|
+
const controls = namedControls(scope);
|
|
1942
|
+
const fieldMatches = controls.filter((el) => nameMatchesAny(el, erroredPaths)).filter(isFocusCandidate);
|
|
1540
1943
|
const rootFallbackApplies = hasRootError && (root !== void 0 || document.querySelectorAll("form").length <= 1);
|
|
1541
|
-
const candidates = fieldMatches.length > 0 ? fieldMatches : rootFallbackApplies ? controls : [];
|
|
1944
|
+
const candidates = fieldMatches.length > 0 ? fieldMatches : rootFallbackApplies ? controls.filter(isFocusCandidate) : [];
|
|
1542
1945
|
return focusFirstOf(candidates);
|
|
1543
1946
|
};
|
|
1544
1947
|
var focusField = (path, root) => {
|
|
1545
|
-
const controls =
|
|
1546
|
-
return path === "" ? (root !== void 0 || document.querySelectorAll("form").length <= 1) && focusFirstOf(controls
|
|
1948
|
+
const controls = namedControls(root ?? document);
|
|
1949
|
+
return path === "" ? (root !== void 0 || document.querySelectorAll("form").length <= 1) && focusFirstOf(controls.filter(isFocusCandidate)) : focusFirstOf(
|
|
1950
|
+
controls.filter((el) => nameMatchesAny(el, [path])).filter(isFocusCandidate)
|
|
1951
|
+
);
|
|
1547
1952
|
};
|
|
1548
1953
|
|
|
1549
|
-
export { CheckboxField, NumberField, SelectField, TextField, checkboxProps, createForm, createFormContext, createFormHooks, emptyValueForSchema, flattenIssues, focusField, focusFirstError, getAtPath, isAsyncRequiredError, numberInputProps, numberToInputText, parseNumberText, parsePath, selectProps, setAtPath, shouldValidateOn, textInputProps, useField, useFieldArray, useForm, useFormError, useFormSelector, useFormSelectorShallow, useIsDirty, useIsSubmitting, useIsValid, useSubmitCount, validateAsync, validateSync };
|
|
1954
|
+
export { CheckboxField, DateField, NumberField, SelectField, TextField, checkboxProps, createForm, createFormContext, createFormHooks, dateInputProps, dateToInputText, emptyValueForSchema, flattenIssues, focusField, focusFirstError, getAtPath, hasFieldError, isAsyncRequiredError, numberInputProps, numberToInputText, parseDateText, parseNumberText, parsePath, persistForm, selectProps, setAtPath, shouldValidateOn, textInputProps, useField, useFieldArray, useForm, useFormError, useFormSelector, useFormSelectorShallow, useIsDirty, useIsSubmitting, useIsValid, useSubmitCount, useVariantField, validateAsync, validateSync };
|
|
1550
1955
|
//# sourceMappingURL=index.js.map
|
|
1551
1956
|
//# sourceMappingURL=index.js.map
|