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