vultus-core 0.5.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +176 -4
- package/dist/FieldContext.d.ts +37 -1
- package/dist/FieldContext.d.ts.map +1 -1
- package/dist/FieldContext.js +68 -11
- package/dist/FieldContext.js.map +1 -1
- package/dist/field.d.ts +12 -1
- package/dist/field.d.ts.map +1 -1
- package/dist/field.js +6 -1
- package/dist/field.js.map +1 -1
- package/dist/fieldGroupValues.d.ts +63 -0
- package/dist/fieldGroupValues.d.ts.map +1 -0
- package/dist/fieldGroupValues.js +119 -0
- package/dist/fieldGroupValues.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/kindHooks.d.ts +23 -0
- package/dist/kindHooks.d.ts.map +1 -0
- package/dist/kindHooks.js +38 -0
- package/dist/kindHooks.js.map +1 -0
- package/dist/kinds.d.ts +19 -14
- package/dist/kinds.d.ts.map +1 -1
- package/dist/kinds.js +40 -14
- package/dist/kinds.js.map +1 -1
- package/dist/tree.d.ts +17 -0
- package/dist/tree.d.ts.map +1 -1
- package/dist/tree.js +27 -0
- package/dist/tree.js.map +1 -1
- package/dist/useArg.d.ts +6 -0
- package/dist/useArg.d.ts.map +1 -1
- package/dist/useArg.js +6 -2
- package/dist/useArg.js.map +1 -1
- package/dist/useFieldGroup.d.ts +80 -0
- package/dist/useFieldGroup.d.ts.map +1 -0
- package/dist/useFieldGroup.js +266 -0
- package/dist/useFieldGroup.js.map +1 -0
- package/dist/useOneOfTreeField.d.ts +79 -0
- package/dist/useOneOfTreeField.d.ts.map +1 -0
- package/dist/useOneOfTreeField.js +117 -0
- package/dist/useOneOfTreeField.js.map +1 -0
- package/dist/useStoredField.d.ts +9 -0
- package/dist/useStoredField.d.ts.map +1 -1
- package/dist/useStoredField.js +7 -5
- package/dist/useStoredField.js.map +1 -1
- package/package.json +1 -1
- package/src/FieldContext.tsx +73 -11
- package/src/field.ts +25 -5
- package/src/fieldGroupValues.ts +169 -0
- package/src/index.ts +7 -1
- package/src/kindHooks.ts +52 -0
- package/src/kinds.ts +38 -14
- package/src/tree.ts +34 -0
- package/src/useArg.ts +6 -2
- package/src/useFieldGroup.ts +346 -0
- package/src/useOneOfTreeField.ts +182 -0
- package/src/useStoredField.ts +10 -6
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { useFieldCore, type FieldControls, type FieldOptions } from './field.js';
|
|
2
|
+
import { resolveChoice, type OneOfChoiceControls } from './choices.js';
|
|
3
|
+
import { useFieldContext, type FieldCtx } from './FieldContext.js';
|
|
4
|
+
import { resolveIn } from './resolve.js';
|
|
5
|
+
import {
|
|
6
|
+
canChooseNode, choosableValues, expandedTreeKeys, treePath,
|
|
7
|
+
type TreeChoose, type TreeExpand, type TreeNode,
|
|
8
|
+
} from './tree.js';
|
|
9
|
+
import type { ErrorRoutesRegistry, ValueOrFn } from './types.js';
|
|
10
|
+
|
|
11
|
+
export type OneOfTreeOwnOptions<T extends string = string, Ctx = any> = {
|
|
12
|
+
/** The roots, each with its children; static or derived (a function, of the field context if it declares a parameter). */
|
|
13
|
+
nodes: ValueOrFn<TreeNode<T>[], Ctx>;
|
|
14
|
+
/** Which nodes may be chosen; default 'any' (see TreeChoose). */
|
|
15
|
+
choose?: TreeChoose;
|
|
16
|
+
/** Whether the value may be empty (''), nothing chosen; default false (as oneOf, never empty). */
|
|
17
|
+
optional?: boolean;
|
|
18
|
+
/** Which nodes a widget opens at first; default 'selected' (the value's ancestors; see TreeExpand). */
|
|
19
|
+
expand?: TreeExpand;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type OneOfTreeFieldOptions<T extends string = string, RouteId extends string = string, Ctx = any> =
|
|
23
|
+
FieldOptions<T | '', T | '', RouteId, Ctx> & OneOfTreeOwnOptions<T, Ctx>;
|
|
24
|
+
|
|
25
|
+
/** A tree node as the one-of tree's widgets get it: the resolved choice, its children likewise, and whether it may be chosen, is the value, or an ancestor of it. */
|
|
26
|
+
export interface OneOfTreeNodeControls<T extends string = string> extends OneOfChoiceControls<T> {
|
|
27
|
+
children: OneOfTreeNodeControls<T>[] | undefined;
|
|
28
|
+
/** Whether the field may choose this node under `choose` (see canChooseNode). */
|
|
29
|
+
choosable: boolean;
|
|
30
|
+
/** It is the value. */
|
|
31
|
+
current: boolean;
|
|
32
|
+
/** An ancestor of the value (not the value itself). */
|
|
33
|
+
onPath: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface OneOfTreeFieldControls<T extends string = string> extends FieldControls<T | '', T | ''> {
|
|
37
|
+
/** The roots as the widgets get them, each with its children. */
|
|
38
|
+
nodes: OneOfTreeNodeControls<T>[];
|
|
39
|
+
/** Every node, by its value. */
|
|
40
|
+
byValue: Map<T, OneOfTreeNodeControls<T>>;
|
|
41
|
+
/** The chosen node, or undefined (the value is empty, or names no node). */
|
|
42
|
+
current: OneOfTreeNodeControls<T> | undefined;
|
|
43
|
+
/** The nodes from the root down to `current`, inclusive; empty when there is none. */
|
|
44
|
+
path: OneOfTreeNodeControls<T>[];
|
|
45
|
+
/**
|
|
46
|
+
* The choosable node just before `current` in tree pre-order — by `current`'s own position,
|
|
47
|
+
* not its position among the choosable nodes, so a `current` that cannot itself be chosen
|
|
48
|
+
* (a value naming a folder, or a node a context change disabled) still gets its true
|
|
49
|
+
* neighbours. Undefined at the start, and with no `current`.
|
|
50
|
+
*/
|
|
51
|
+
previous: OneOfTreeNodeControls<T> | undefined;
|
|
52
|
+
/** The choosable node just after `current` in tree pre-order, by the same rule; with no `current`, the first choosable node. */
|
|
53
|
+
next: OneOfTreeNodeControls<T> | undefined;
|
|
54
|
+
optional: boolean;
|
|
55
|
+
choose: TreeChoose;
|
|
56
|
+
/** The nodes a widget opens at first (from `expand`). */
|
|
57
|
+
expandedKeys: T[];
|
|
58
|
+
/** Requests ''; refused (with a warning) unless `optional`, as `setValue('')` would be. */
|
|
59
|
+
clear(): void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The implicit initial value for a one-of tree field: `initialValue` resolved through the field
|
|
64
|
+
* context, else '' when `optional`, else the first node `canChooseNode` accepts in tree
|
|
65
|
+
* pre-order, else '' (no node can be chosen; as oneOf's undefined choice).
|
|
66
|
+
*/
|
|
67
|
+
export function oneOfTreeInitialValue<T extends string>(
|
|
68
|
+
opts: { initialValue?: ValueOrFn<T | ''>; choose?: TreeChoose; optional?: boolean },
|
|
69
|
+
ctx: FieldCtx,
|
|
70
|
+
id: string,
|
|
71
|
+
nodes: TreeNode<T>[],
|
|
72
|
+
): T | '' {
|
|
73
|
+
const resolved = resolveIn<T | '' | undefined>(opts.initialValue, undefined, ctx, `Field '${id}': option 'initialValue'`);
|
|
74
|
+
if (resolved !== undefined) return resolved;
|
|
75
|
+
if (opts.optional) return '';
|
|
76
|
+
return choosableValues(nodes, opts.choose ?? 'any')[0] ?? '';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A field whose value is a single tree node, or nothing (Praeceptor's category picker, a
|
|
81
|
+
* table of contents): `choosable` (a per-node flag, default true; a folder) and the field's
|
|
82
|
+
* `choose: 'any' | 'leaves'` together say which nodes `setValue` accepts (canChooseNode);
|
|
83
|
+
* `optional` says whether '' (nothing chosen) is one of them too. `setValue` refuses, with a
|
|
84
|
+
* warning (as useOneOfField refuses a disabled choice): a value naming no node, a node
|
|
85
|
+
* `canChooseNode` rejects, and '' unless `optional`. `clear()` requests ''. The field's current
|
|
86
|
+
* state is the chosen node's description, unless `valueDescriptions` words it differently (as
|
|
87
|
+
* oneOf). `previous` / `next` are the choosable nodes just before and after `current`'s own
|
|
88
|
+
* position in tree pre-order (so a `current` that cannot itself be chosen still gets its true
|
|
89
|
+
* neighbours, skipping only the nodes between them that cannot be chosen); with no `current`,
|
|
90
|
+
* `previous` is undefined and `next` is the first choosable node. `expandedKeys` (from `expand`,
|
|
91
|
+
* default `'selected'`) are the value's ancestors,
|
|
92
|
+
* so a table of contents opens where the reader is. Inside the field the value starts at
|
|
93
|
+
* `oneOfTreeInitialValue`.
|
|
94
|
+
*/
|
|
95
|
+
export function useOneOfTreeField<T extends string = string, RouteId extends string = string, Ctx = any>(
|
|
96
|
+
opts: OneOfTreeFieldOptions<T, RouteId, Ctx>,
|
|
97
|
+
registry?: ErrorRoutesRegistry<RouteId>,
|
|
98
|
+
): OneOfTreeFieldControls<T> {
|
|
99
|
+
const ctx = useFieldContext();
|
|
100
|
+
const nodes = resolveIn<TreeNode<T>[]>(opts.nodes, [], ctx, `Field '${opts.id}': option 'nodes'`);
|
|
101
|
+
const choose = opts.choose ?? 'any';
|
|
102
|
+
const optional = opts.optional ?? false;
|
|
103
|
+
|
|
104
|
+
const inside = !opts.onChange && !opts.commit;
|
|
105
|
+
const initialValue = () => oneOfTreeInitialValue<T>(opts, ctx, opts.id, nodes);
|
|
106
|
+
const field = useFieldCore<T | '', T | '', RouteId, Ctx>('oneOfTree', inside ? { ...opts, initialValue } : opts, registry);
|
|
107
|
+
|
|
108
|
+
const valuePath = field.value ? treePath(nodes, field.value) : [];
|
|
109
|
+
const ancestors = new Set(valuePath);
|
|
110
|
+
const byValue = new Map<T, OneOfTreeNodeControls<T>>();
|
|
111
|
+
const control = (node: TreeNode<T>): OneOfTreeNodeControls<T> => {
|
|
112
|
+
const controls: OneOfTreeNodeControls<T> = {
|
|
113
|
+
...resolveChoice(opts.id, node),
|
|
114
|
+
children: undefined,
|
|
115
|
+
choosable: canChooseNode(node, choose),
|
|
116
|
+
current: node.value === field.value,
|
|
117
|
+
onPath: ancestors.has(node.value) && node.value !== field.value,
|
|
118
|
+
};
|
|
119
|
+
byValue.set(node.value, controls);
|
|
120
|
+
if (node.children) controls.children = node.children.map(control);
|
|
121
|
+
return controls;
|
|
122
|
+
};
|
|
123
|
+
const nodeControls = nodes.map(control);
|
|
124
|
+
|
|
125
|
+
const current = field.value ? byValue.get(field.value) : undefined;
|
|
126
|
+
const pathControls = valuePath.map((v) => byValue.get(v)!);
|
|
127
|
+
|
|
128
|
+
// byValue's insertion order is the full tree's pre-order (control() visits a node before its
|
|
129
|
+
// children), so it doubles as "every node, choosable or not" for finding current's neighbours.
|
|
130
|
+
const fullOrder = [...byValue.keys()];
|
|
131
|
+
const nearestChoosable = (from: number, step: 1 | -1): OneOfTreeNodeControls<T> | undefined => {
|
|
132
|
+
for (let i = from; i >= 0 && i < fullOrder.length; i += step) {
|
|
133
|
+
const node = byValue.get(fullOrder[i])!;
|
|
134
|
+
if (node.choosable) return node;
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
};
|
|
138
|
+
const index = current ? fullOrder.indexOf(current.value) : -1;
|
|
139
|
+
const previous = nearestChoosable(index - 1, -1);
|
|
140
|
+
const next = nearestChoosable(index + 1, 1);
|
|
141
|
+
|
|
142
|
+
const setValue = (requested: T | '') => {
|
|
143
|
+
if (requested === '') {
|
|
144
|
+
if (!optional) {
|
|
145
|
+
console.warn(`Field '${opts.id}': '' is refused — the field is not optional`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
field.setValue('');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const node = byValue.get(requested);
|
|
152
|
+
if (!node) {
|
|
153
|
+
console.warn(`Field '${opts.id}': '${requested}' is not one of its nodes`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (!node.choosable) {
|
|
157
|
+
if (!node.enabled) {
|
|
158
|
+
console.warn(`Field '${opts.id}': node '${requested}' is disabled — ${node.whyDisabled ?? '(no reason)'}`);
|
|
159
|
+
} else {
|
|
160
|
+
console.warn(`Field '${opts.id}': node '${requested}' cannot be chosen`);
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
field.setValue(requested);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
...field,
|
|
169
|
+
valueDescription: field.valueDescription ?? current?.description,
|
|
170
|
+
setValue,
|
|
171
|
+
nodes: nodeControls,
|
|
172
|
+
byValue,
|
|
173
|
+
current,
|
|
174
|
+
path: pathControls,
|
|
175
|
+
previous,
|
|
176
|
+
next,
|
|
177
|
+
optional,
|
|
178
|
+
choose,
|
|
179
|
+
expandedKeys: expandedTreeKeys(nodes, field.value ? [field.value] : [], opts.expand ?? 'selected'),
|
|
180
|
+
clear: () => setValue(''),
|
|
181
|
+
};
|
|
182
|
+
}
|
package/src/useStoredField.ts
CHANGED
|
@@ -32,9 +32,11 @@ function warnOnConsole(message: string): void {
|
|
|
32
32
|
/**
|
|
33
33
|
* The same value element by element: a list in the same order, a range with the same ends, a
|
|
34
34
|
* scalar. Stricter than the value types' `isEqual` (lists as sets), because the order a field
|
|
35
|
-
* shows (choice order, tree order) matters to the identity kept below.
|
|
35
|
+
* shows (choice order, tree order) matters to the identity kept below. Exported for the field
|
|
36
|
+
* group (useFieldGroup), which keeps its values' identity by the same rule; not part of the
|
|
37
|
+
* package's API.
|
|
36
38
|
*/
|
|
37
|
-
function sameValue(a: unknown, b: unknown): boolean {
|
|
39
|
+
export function sameValue(a: unknown, b: unknown): boolean {
|
|
38
40
|
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
39
41
|
return a === b;
|
|
40
42
|
}
|
|
@@ -56,7 +58,9 @@ function showsDefault(arg: ArgDefinition<any>, stored: unknown, shown: () => unk
|
|
|
56
58
|
*/
|
|
57
59
|
export interface StoredField<D extends FieldDefinition = FieldDefinition> {
|
|
58
60
|
/** The definition in its outside form, for the kind's hook (`useManyOfField(stored.options)`). */
|
|
59
|
-
options: D extends any
|
|
61
|
+
options: D extends any
|
|
62
|
+
? Omit<D, 'initialValue'> & { value: any; onChange(value: any): void; hrefFor(value: any): string | undefined }
|
|
63
|
+
: never;
|
|
60
64
|
isDefault: boolean;
|
|
61
65
|
/** Removes the field's key from the store, so it shows its default again. */
|
|
62
66
|
reset(method?: UpdateMethod): void;
|
|
@@ -96,7 +100,7 @@ export function useStoredField<D extends FieldDefinition>(definition: D): Stored
|
|
|
96
100
|
: fresh;
|
|
97
101
|
argRef.current = arg;
|
|
98
102
|
|
|
99
|
-
const { value: stored, set, reset } = useArg(arg);
|
|
103
|
+
const { value: stored, set, reset, hrefFor } = useArg(arg);
|
|
100
104
|
// Called on the store, not passed detached: a store written as a class uses `this`.
|
|
101
105
|
const subscribe = useCallback((listener: () => void) => store.subscribe(listener), [store]);
|
|
102
106
|
const read = useCallback(() => store.read(definition.id), [store, definition.id]);
|
|
@@ -119,8 +123,8 @@ export function useStoredField<D extends FieldDefinition>(definition: D): Stored
|
|
|
119
123
|
const onChange = useCallback((next: unknown) => set(next, 'replace'), [set]);
|
|
120
124
|
const options = useMemo(() => {
|
|
121
125
|
const { initialValue: _inside, ...rest } = definition;
|
|
122
|
-
return { ...rest, value, onChange } as StoredField<D>['options'];
|
|
123
|
-
}, [definition, value, onChange]);
|
|
126
|
+
return { ...rest, value, onChange, hrefFor } as StoredField<D>['options'];
|
|
127
|
+
}, [definition, value, onChange, hrefFor]);
|
|
124
128
|
|
|
125
129
|
return { options, isDefault: showsDefault(arg, stored, () => value), reset };
|
|
126
130
|
}
|