vultus-core 0.5.1 → 0.5.3
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 +96 -10
- 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/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/kinds.d.ts +20 -15
- package/dist/kinds.d.ts.map +1 -1
- package/dist/kinds.js +46 -20
- package/dist/kinds.js.map +1 -1
- package/dist/parseApiError.d.ts +6 -0
- package/dist/parseApiError.d.ts.map +1 -1
- package/dist/parseApiError.js +7 -1
- package/dist/parseApiError.js.map +1 -1
- package/dist/tree.d.ts +38 -9
- package/dist/tree.d.ts.map +1 -1
- package/dist/tree.js +91 -21
- package/dist/tree.js.map +1 -1
- package/dist/{useTreeField.d.ts → useManyOfTreeField.d.ts} +10 -9
- package/dist/useManyOfTreeField.d.ts.map +1 -0
- package/dist/{useTreeField.js → useManyOfTreeField.js} +9 -8
- package/dist/useManyOfTreeField.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 +1 -0
- package/dist/useStoredField.d.ts.map +1 -1
- package/dist/useStoredField.js +3 -3
- package/dist/useStoredField.js.map +1 -1
- package/package.json +3 -2
- package/src/field.ts +25 -5
- package/src/index.ts +9 -4
- package/src/kinds.ts +46 -22
- package/src/parseApiError.ts +7 -1
- package/src/tree.ts +105 -20
- package/src/{useTreeField.ts → useManyOfTreeField.ts} +14 -13
- package/src/useOneOfTreeField.ts +182 -0
- package/src/useStoredField.ts +6 -4
- package/dist/useTreeField.d.ts.map +0 -1
- package/dist/useTreeField.js.map +0 -1
package/src/tree.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { OneOfChoice, OneOfChoiceControls } from './choices.js';
|
|
2
|
+
import { getVerdict } from './decision.js';
|
|
2
3
|
import { normalizeText } from './normalizeText.js';
|
|
3
4
|
|
|
4
5
|
/** A node of a tree field (Praeceptor's categories): a choice with children. */
|
|
5
6
|
export interface TreeNode<T extends string = string> extends OneOfChoice<T> {
|
|
6
7
|
children?: TreeNode<T>[];
|
|
8
|
+
/** Whether the one-of tree may choose this node (default true); a folder when false. Ignored by the many-of tree. */
|
|
9
|
+
choosable?: boolean;
|
|
7
10
|
}
|
|
8
11
|
|
|
9
12
|
/** A node as the widgets get it: the resolved choice, its children likewise, and whether it shows checked. */
|
|
@@ -16,12 +19,20 @@ export interface TreeNodeControls<T extends string = string> extends OneOfChoice
|
|
|
16
19
|
/**
|
|
17
20
|
* What a tree field's value lists, as antd TreeSelect's `showCheckedStrategy` and
|
|
18
21
|
* `checkStrictly` do. In the three cascading selections a node stands for its subtree, and
|
|
19
|
-
* a node all of whose children are covered is covered too
|
|
20
|
-
* covered nodes the
|
|
22
|
+
* a node all of whose children are covered is covered too — disabled nodes aside, as antd
|
|
23
|
+
* conducts its checks (see `cascade`); they differ only in which of the covered nodes the
|
|
24
|
+
* value names:
|
|
21
25
|
* - `'topmost'` (`SHOW_PARENT`, the default): the covered nodes whose parent is not covered;
|
|
22
26
|
* - `'all'` (`SHOW_ALL`): every covered node;
|
|
23
27
|
* - `'leaves'` (`SHOW_CHILD`): the covered leaves.
|
|
24
28
|
*
|
|
29
|
+
* Where a disabled node breaks the cascade, the value also names what the others would not
|
|
30
|
+
* cover: `'topmost'` a covered disabled node under a covered parent, and a covered node under
|
|
31
|
+
* a covered disabled parent (as antd TreeSelect's SHOW_PARENT does); `'leaves'` a covered node
|
|
32
|
+
* with children that is disabled or has no enabled child (stricter than antd's SHOW_CHILD,
|
|
33
|
+
* which drops a checked disabled parent whose enabled children are all checked, and a parent
|
|
34
|
+
* whose only children are disabled and checked — so the value would no longer cover them).
|
|
35
|
+
*
|
|
25
36
|
* `'independent'` (`checkStrictly`, moxb's `strictSelect`): each node is chosen on its own.
|
|
26
37
|
*/
|
|
27
38
|
export type TreeSelection = 'topmost' | 'all' | 'leaves' | 'independent';
|
|
@@ -58,14 +69,33 @@ function unknownValues<T extends string>(order: TreeNode<T>[], value: T[]): T[]
|
|
|
58
69
|
return [...new Set(value.filter((v) => !known.has(v)))];
|
|
59
70
|
}
|
|
60
71
|
|
|
61
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Whether a node can be checked: its `enabled` / `disabled` resolved as resolveChoice does (a
|
|
74
|
+
* Decision takes no context, so this needs none). Works on a definition's nodes (TreeNode,
|
|
75
|
+
* Decisions) and on the widgets' node controls (TreeNodeControls, `enabled: boolean`) alike.
|
|
76
|
+
*/
|
|
77
|
+
function nodeEnabled(node: Pick<TreeNode, 'enabled' | 'disabled'>): boolean {
|
|
78
|
+
if (node.enabled !== undefined) return getVerdict(node.enabled, true);
|
|
79
|
+
return !getVerdict(node.disabled, false);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The nodes a value covers in the cascading selections, by antd's check conduction (rc-tree's
|
|
84
|
+
* conductCheck), so the hook and the widgets agree: the value's nodes; the enabled children of
|
|
85
|
+
* a covered enabled node, and so on down (a disabled node is never covered through a checked
|
|
86
|
+
* ancestor, nor is its subtree through it); and an enabled node all of whose enabled children
|
|
87
|
+
* are covered, if it has any (a parent whose children are all disabled, or a disabled parent,
|
|
88
|
+
* is never covered through its children). A disabled node in the value covers itself only.
|
|
89
|
+
*/
|
|
62
90
|
function cascade<T extends string>(nodes: TreeNode<T>[], value: T[]): Set<T> {
|
|
63
91
|
const listed = new Set(value);
|
|
64
92
|
const covered = new Set<T>();
|
|
65
93
|
const walk = (node: TreeNode<T>, inherited: boolean): boolean => {
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const
|
|
94
|
+
const enabled = nodeEnabled(node);
|
|
95
|
+
const self = listed.has(node.value) || (inherited && enabled);
|
|
96
|
+
const children = (node.children ?? []).map((child) => ({ enabled: nodeEnabled(child), covered: walk(child, self && enabled) }));
|
|
97
|
+
const counted = children.filter((child) => child.enabled);
|
|
98
|
+
const isCovered = self || (enabled && counted.length > 0 && counted.every((child) => child.covered));
|
|
69
99
|
if (isCovered) covered.add(node.value);
|
|
70
100
|
return isCovered;
|
|
71
101
|
};
|
|
@@ -73,14 +103,29 @@ function cascade<T extends string>(nodes: TreeNode<T>[], value: T[]): Set<T> {
|
|
|
73
103
|
return covered;
|
|
74
104
|
}
|
|
75
105
|
|
|
76
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* The covered nodes the value names under `selection` (see TreeSelection), in pre-order, by
|
|
108
|
+
* rc-tree-select's formatStrategyValues (for `'leaves'`, a stricter form of it), so that the
|
|
109
|
+
* named nodes cover exactly what `covered` does (`cascade` of them gives it back) also where
|
|
110
|
+
* disabled nodes break the cascade:
|
|
111
|
+
* - `'topmost'`: a covered node unless its parent is covered and neither of the two is
|
|
112
|
+
* disabled (only then does the parent cover it);
|
|
113
|
+
* - `'leaves'`: the covered leaves, and a covered node with children that is disabled, has
|
|
114
|
+
* no enabled child, or has an enabled child not covered (its children would not cover it);
|
|
115
|
+
* - `'all'`: every covered node.
|
|
116
|
+
*/
|
|
77
117
|
function namedNodes<T extends string>(order: TreeNode<T>[], parent: Map<T, T>, covered: Set<T>, selection: TreeSelection): T[] {
|
|
118
|
+
const enabled = new Map(order.map((node) => [node.value, nodeEnabled(node)]));
|
|
78
119
|
const named = order.filter((node) => {
|
|
79
120
|
if (!covered.has(node.value)) return false;
|
|
80
|
-
if (selection === 'leaves') return !hasChildren(node);
|
|
81
121
|
if (selection === 'topmost') {
|
|
82
122
|
const up = parent.get(node.value);
|
|
83
|
-
return up === undefined || !covered.has(up);
|
|
123
|
+
return up === undefined || !covered.has(up) || !enabled.get(node.value) || !enabled.get(up);
|
|
124
|
+
}
|
|
125
|
+
if (selection === 'leaves') {
|
|
126
|
+
if (!hasChildren(node) || !enabled.get(node.value)) return true;
|
|
127
|
+
const counted = node.children!.filter((child) => enabled.get(child.value));
|
|
128
|
+
return counted.length === 0 || !counted.every((child) => covered.has(child.value));
|
|
84
129
|
}
|
|
85
130
|
return true;
|
|
86
131
|
});
|
|
@@ -105,19 +150,23 @@ export function normalizeTreeValue<T extends string>(nodes: TreeNode<T>[], value
|
|
|
105
150
|
/**
|
|
106
151
|
* Every node a tree field's value stands for, in pre-order (what an item filed under some
|
|
107
152
|
* node matches against): in the cascading selections the covered nodes (the value's nodes,
|
|
108
|
-
* their descendants, and every node all of whose children are
|
|
109
|
-
* `'independent'` the value's nodes themselves. Values that
|
|
110
|
-
* normalizeTreeValue.
|
|
153
|
+
* their enabled descendants, and every enabled node all of whose enabled children are
|
|
154
|
+
* covered; see `cascade`); with `'independent'` the value's nodes themselves. Values that
|
|
155
|
+
* name no node follow, as in normalizeTreeValue. A node disabled when the value was stored
|
|
156
|
+
* and enabled now is covered from then on (with `'topmost'`, a stored parent covers it).
|
|
111
157
|
*/
|
|
112
158
|
export function coveredNodes<T extends string>(nodes: TreeNode<T>[], value: T[], selection: TreeSelection = 'topmost'): T[] {
|
|
113
159
|
return normalizeTreeValue(nodes, value, selection === 'independent' ? 'independent' : 'all');
|
|
114
160
|
}
|
|
115
161
|
|
|
116
162
|
/**
|
|
117
|
-
* The value after flipping `node`, normalized
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
163
|
+
* The value after flipping `node`, normalized, as antd's Tree flips a checkbox. In the
|
|
164
|
+
* cascading selections a covered node is removed with the descendants it covers through
|
|
165
|
+
* enabled nodes and with its enabled ancestors up to the first disabled one (they no longer
|
|
166
|
+
* have all their enabled children); an uncovered one is added, covering its subtree as
|
|
167
|
+
* `cascade` says. With `'independent'` the node alone is added or removed. A `node` that is
|
|
168
|
+
* not one of the nodes changes nothing. Whether `node` itself may be flipped is the caller's
|
|
169
|
+
* to decide (useManyOfTreeField refuses a disabled one).
|
|
121
170
|
*/
|
|
122
171
|
export function toggleTreeValue<T extends string>(nodes: TreeNode<T>[], value: T[], node: T, selection: TreeSelection): T[] {
|
|
123
172
|
const { order, parent } = indexTree(nodes);
|
|
@@ -127,12 +176,16 @@ export function toggleTreeValue<T extends string>(nodes: TreeNode<T>[], value: T
|
|
|
127
176
|
return normalizeTreeValue(nodes, value.includes(node) ? value.filter((v) => v !== node) : [...value, node], selection);
|
|
128
177
|
}
|
|
129
178
|
const covered = cascade(nodes, value);
|
|
130
|
-
const subtree = indexTree([target]).order.map((n) => n.value);
|
|
131
179
|
if (covered.has(node)) {
|
|
132
|
-
|
|
133
|
-
|
|
180
|
+
const uncover = (n: TreeNode<T>) => {
|
|
181
|
+
covered.delete(n.value);
|
|
182
|
+
for (const child of n.children ?? []) if (nodeEnabled(child)) uncover(child);
|
|
183
|
+
};
|
|
184
|
+
uncover(target);
|
|
185
|
+
const byValue = new Map(order.map((n) => [n.value, n]));
|
|
186
|
+
for (let up = parent.get(node); up !== undefined && nodeEnabled(byValue.get(up)!); up = parent.get(up)) covered.delete(up);
|
|
134
187
|
} else {
|
|
135
|
-
|
|
188
|
+
covered.add(node);
|
|
136
189
|
}
|
|
137
190
|
return normalizeTreeValue(nodes, [...covered, ...unknownValues(order, value)], selection);
|
|
138
191
|
}
|
|
@@ -173,3 +226,35 @@ export function filterTree<N extends { label?: string; value: string; children?:
|
|
|
173
226
|
});
|
|
174
227
|
return keep(nodes, false);
|
|
175
228
|
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Which nodes the one-of tree field may choose: `'any'` node, or only `'leaves'` (a node with
|
|
232
|
+
* no children).
|
|
233
|
+
*/
|
|
234
|
+
export type TreeChoose = 'any' | 'leaves';
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Whether the one-of tree field may choose `node` under `choose` (see TreeChoose): choosable
|
|
238
|
+
* (its `choosable` is not `false`), enabled (as nodeEnabled resolves it), and, under `'leaves'`,
|
|
239
|
+
* has no children.
|
|
240
|
+
*/
|
|
241
|
+
export function canChooseNode(node: TreeNode, choose: TreeChoose): boolean {
|
|
242
|
+
if (node.choosable === false) return false;
|
|
243
|
+
if (!nodeEnabled(node)) return false;
|
|
244
|
+
return choose !== 'leaves' || !hasChildren(node);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** The values canChooseNode accepts, in tree pre-order. */
|
|
248
|
+
export function choosableValues<T extends string>(nodes: TreeNode<T>[], choose: TreeChoose): T[] {
|
|
249
|
+
const { order } = indexTree(nodes);
|
|
250
|
+
return order.filter((node) => canChooseNode(node, choose)).map((node) => node.value);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** The values from the root down to `value` inclusive, in tree order; `[]` when no node has it. */
|
|
254
|
+
export function treePath<T extends string>(nodes: TreeNode<T>[], value: T): T[] {
|
|
255
|
+
const { order, parent } = indexTree(nodes);
|
|
256
|
+
if (!order.some((node) => node.value === value)) return [];
|
|
257
|
+
const path: T[] = [value];
|
|
258
|
+
for (let up = parent.get(value); up !== undefined; up = parent.get(up)) path.unshift(up);
|
|
259
|
+
return path;
|
|
260
|
+
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from './tree.js';
|
|
9
9
|
import type { ErrorRoutesRegistry, ValueOrFn } from './types.js';
|
|
10
10
|
|
|
11
|
-
export type
|
|
11
|
+
export type ManyOfTreeOwnOptions<T extends string = string, Ctx = any> = {
|
|
12
12
|
/** The roots, each with its children; static or derived (a function, of the field context if it declares a parameter). */
|
|
13
13
|
nodes: ValueOrFn<TreeNode<T>[], Ctx>;
|
|
14
14
|
/** What the value lists; default `'topmost'` (see TreeSelection). */
|
|
@@ -17,10 +17,10 @@ export type TreeOwnOptions<T extends string = string, Ctx = any> = {
|
|
|
17
17
|
expand?: TreeExpand;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
export type
|
|
21
|
-
FieldOptions<T[], T[], RouteId, Ctx> &
|
|
20
|
+
export type ManyOfTreeFieldOptions<T extends string = string, RouteId extends string = string, Ctx = any> =
|
|
21
|
+
FieldOptions<T[], T[], RouteId, Ctx> & ManyOfTreeOwnOptions<T, Ctx>;
|
|
22
22
|
|
|
23
|
-
export interface
|
|
23
|
+
export interface ManyOfTreeFieldControls<T extends string = string> extends FieldControls<T[], T[]> {
|
|
24
24
|
/** The roots as the widgets get them, each with its children. */
|
|
25
25
|
nodes: TreeNodeControls<T>[];
|
|
26
26
|
selection: TreeSelection;
|
|
@@ -32,8 +32,8 @@ export interface TreeFieldControls<T extends string = string> extends FieldContr
|
|
|
32
32
|
toggle(value: T): void;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
/** The implicit initial value for a tree field: `initialValue` resolved, else the empty list. */
|
|
36
|
-
export function
|
|
35
|
+
/** The implicit initial value for a many-of tree field: `initialValue` resolved, else the empty list. */
|
|
36
|
+
export function manyOfTreeInitialValue<T extends string>(
|
|
37
37
|
opts: { initialValue?: ValueOrFn<T[]> },
|
|
38
38
|
ctx: FieldCtx,
|
|
39
39
|
id: string,
|
|
@@ -47,24 +47,25 @@ function sameList<T>(a: T[], b: T[]): boolean {
|
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* A field whose value is a set of tree nodes (Praeceptor's category filter), with antd
|
|
50
|
-
* TreeSelect's semantics: in the cascading selections a checked node stands for its subtree
|
|
51
|
-
*
|
|
50
|
+
* TreeSelect's semantics: in the cascading selections a checked node stands for its subtree
|
|
51
|
+
* (its enabled part: disabled nodes follow antd's check conduction, so the widgets show what
|
|
52
|
+
* the value covers), and `selection` says which nodes the value lists (see TreeSelection). The value is shown
|
|
52
53
|
* and requested normalized (normalizeTreeValue): in tree pre-order, values that name no
|
|
53
54
|
* node (a stored value naming a removed node) kept at the end. `setValue` does not police
|
|
54
55
|
* disabled nodes (the widgets disable them); `toggle` refuses a disabled or unknown node with
|
|
55
56
|
* a warning, as useManyOfField does.
|
|
56
57
|
*/
|
|
57
|
-
export function
|
|
58
|
-
opts:
|
|
58
|
+
export function useManyOfTreeField<T extends string = string, RouteId extends string = string, Ctx = any>(
|
|
59
|
+
opts: ManyOfTreeFieldOptions<T, RouteId, Ctx>,
|
|
59
60
|
registry?: ErrorRoutesRegistry<RouteId>,
|
|
60
|
-
):
|
|
61
|
+
): ManyOfTreeFieldControls<T> {
|
|
61
62
|
const ctx = useFieldContext();
|
|
62
63
|
const nodes = resolveIn<TreeNode<T>[]>(opts.nodes, [], ctx, `Field '${opts.id}': option 'nodes'`);
|
|
63
64
|
const selection = opts.selection ?? 'topmost';
|
|
64
65
|
|
|
65
66
|
const inside = !opts.onChange && !opts.commit;
|
|
66
|
-
const initialValue = () =>
|
|
67
|
-
const field = useFieldCore<T[], T[], RouteId, Ctx>('
|
|
67
|
+
const initialValue = () => manyOfTreeInitialValue<T>(opts, ctx, opts.id);
|
|
68
|
+
const field = useFieldCore<T[], T[], RouteId, Ctx>('manyOfTree', inside ? { ...opts, initialValue } : opts, registry);
|
|
68
69
|
|
|
69
70
|
// The same list object while it is already normalized, so the value is stable across renders.
|
|
70
71
|
const normalized = normalizeTreeValue(nodes, field.value, selection);
|
|
@@ -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
|
@@ -56,7 +56,9 @@ function showsDefault(arg: ArgDefinition<any>, stored: unknown, shown: () => unk
|
|
|
56
56
|
*/
|
|
57
57
|
export interface StoredField<D extends FieldDefinition = FieldDefinition> {
|
|
58
58
|
/** The definition in its outside form, for the kind's hook (`useManyOfField(stored.options)`). */
|
|
59
|
-
options: D extends any
|
|
59
|
+
options: D extends any
|
|
60
|
+
? Omit<D, 'initialValue'> & { value: any; onChange(value: any): void; hrefFor(value: any): string | undefined }
|
|
61
|
+
: never;
|
|
60
62
|
isDefault: boolean;
|
|
61
63
|
/** Removes the field's key from the store, so it shows its default again. */
|
|
62
64
|
reset(method?: UpdateMethod): void;
|
|
@@ -96,7 +98,7 @@ export function useStoredField<D extends FieldDefinition>(definition: D): Stored
|
|
|
96
98
|
: fresh;
|
|
97
99
|
argRef.current = arg;
|
|
98
100
|
|
|
99
|
-
const { value: stored, set, reset } = useArg(arg);
|
|
101
|
+
const { value: stored, set, reset, hrefFor } = useArg(arg);
|
|
100
102
|
// Called on the store, not passed detached: a store written as a class uses `this`.
|
|
101
103
|
const subscribe = useCallback((listener: () => void) => store.subscribe(listener), [store]);
|
|
102
104
|
const read = useCallback(() => store.read(definition.id), [store, definition.id]);
|
|
@@ -119,8 +121,8 @@ export function useStoredField<D extends FieldDefinition>(definition: D): Stored
|
|
|
119
121
|
const onChange = useCallback((next: unknown) => set(next, 'replace'), [set]);
|
|
120
122
|
const options = useMemo(() => {
|
|
121
123
|
const { initialValue: _inside, ...rest } = definition;
|
|
122
|
-
return { ...rest, value, onChange } as StoredField<D>['options'];
|
|
123
|
-
}, [definition, value, onChange]);
|
|
124
|
+
return { ...rest, value, onChange, hrefFor } as StoredField<D>['options'];
|
|
125
|
+
}, [definition, value, onChange, hrefFor]);
|
|
124
126
|
|
|
125
127
|
return { options, isDefault: showsDefault(arg, stored, () => value), reset };
|
|
126
128
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useTreeField.d.ts","sourceRoot":"","sources":["../src/useTreeField.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAEjF,OAAO,EAAmB,KAAK,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAEnE,OAAO,EAEL,KAAK,UAAU,EAAE,KAAK,QAAQ,EAAE,KAAK,gBAAgB,EAAE,KAAK,aAAa,EAC1E,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEjE,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,GAAG,GAAG,GAAG,IAAI;IACjE,0HAA0H;IAC1H,KAAK,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;IACrC,qEAAqE;IACrE,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,6EAA6E;IAC7E,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,OAAO,SAAS,MAAM,GAAG,MAAM,EAAE,GAAG,GAAG,GAAG,IAChG,YAAY,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,CAAC,GAAG,cAAc,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AAEhE,MAAM,WAAW,iBAAiB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,aAAa,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;IAC3F,iEAAiE;IACjE,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7B,SAAS,EAAE,aAAa,CAAC;IACzB,yDAAyD;IACzD,YAAY,EAAE,CAAC,EAAE,CAAC;IAClB,gCAAgC;IAChC,OAAO,EAAE,GAAG,CAAC,CAAC,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC;IACrC,2IAA2I;IAC3I,MAAM,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;CACxB;AAED,iGAAiG;AACjG,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,EAC/C,IAAI,EAAE;IAAE,YAAY,CAAC,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,CAAA;CAAE,EACvC,GAAG,EAAE,QAAQ,EACb,EAAE,EAAE,MAAM,GACT,CAAC,EAAE,CAEL;AAMD;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,OAAO,SAAS,MAAM,GAAG,MAAM,EAAE,GAAG,GAAG,GAAG,EAChG,IAAI,EAAE,gBAAgB,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,EACvC,QAAQ,CAAC,EAAE,mBAAmB,CAAC,OAAO,CAAC,GACtC,iBAAiB,CAAC,CAAC,CAAC,CAgDtB"}
|
package/dist/useTreeField.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useTreeField.js","sourceRoot":"","sources":["../src/useTreeField.ts"],"names":[],"mappings":"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"}
|