@rainbow-oh/yee-c 0.6.4 → 0.8.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/dist/cjs/Button/style/index.less +5 -4
- package/dist/cjs/Drawer/drawer.js +1 -1
- package/dist/cjs/Drawer/interface.d.ts +1 -1
- package/dist/cjs/Drawer/style/index.less +1 -0
- package/dist/cjs/Form/form-store.d.ts +2 -1
- package/dist/cjs/Form/form-store.js +32 -17
- package/dist/cjs/List/demo/virtual.d.ts +3 -0
- package/dist/cjs/List/demo/virtual.js +53 -0
- package/dist/cjs/List/hooks/useListKeyboard.d.ts +28 -0
- package/dist/cjs/List/hooks/useListKeyboard.js +188 -0
- package/dist/cjs/List/interface.d.ts +29 -2
- package/dist/cjs/List/list.js +141 -21
- package/dist/cjs/List/style/index.less +24 -0
- package/dist/cjs/Select/demo/columns.d.ts +3 -0
- package/dist/cjs/Select/demo/columns.js +62 -0
- package/dist/cjs/Select/hooks/useSelectKeyboard.d.ts +13 -1
- package/dist/cjs/Select/hooks/useSelectKeyboard.js +81 -10
- package/dist/cjs/Select/interface.d.ts +30 -3
- package/dist/cjs/Select/option.js +4 -3
- package/dist/cjs/Select/options.js +90 -8
- package/dist/cjs/Select/select.js +32 -8
- package/dist/cjs/Select/style/index.less +14 -0
- package/dist/cjs/Selector/interface.d.ts +4 -0
- package/dist/cjs/Selector/selector.js +5 -3
- package/dist/cjs/Table/hooks/useColumns.js +1 -1
- package/dist/cjs/Table/interface.d.ts +1 -1
- package/dist/cjs/Table/style/index.less +35 -11
- package/dist/cjs/Table/table.js +57 -7
- package/dist/cjs/Table/tbody/cell.js +1 -1
- package/dist/cjs/Table/util/header.d.ts +3 -1
- package/dist/cjs/Table/util/header.js +30 -3
- package/dist/cjs/Tree/demo/draggable.js +2 -0
- package/dist/cjs/Tree/interface.d.ts +46 -23
- package/dist/cjs/Tree/tree-node.d.ts +1 -1
- package/dist/cjs/Tree/tree-node.js +30 -23
- package/dist/cjs/Tree/tree.d.ts +13 -12
- package/dist/cjs/Tree/tree.js +452 -227
- package/dist/cjs/Tree/utils/tree-tools.d.ts +43 -10
- package/dist/cjs/Tree/utils/tree-tools.js +129 -91
- package/dist/cjs/locale/context.d.ts +5 -4
- package/dist/cjs/locale/context.js +15 -8
- package/dist/cjs/locale/interface.d.ts +20 -0
- package/dist/cjs/locale/util.d.ts +3 -3
- package/dist/cjs/style/color.css +3 -0
- package/dist/cjs/style/size.css +2 -1
- package/dist/cjs/style/themes.css +1 -1
- package/dist/cjs/style/variables.css +6 -2
- package/dist/cjs/utils/isEqual.js +5 -7
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/Button/style/index.less +5 -4
- package/dist/esm/Drawer/drawer.js +2 -1
- package/dist/esm/Drawer/interface.d.ts +1 -1
- package/dist/esm/Drawer/style/index.less +1 -0
- package/dist/esm/Form/form-store.d.ts +2 -1
- package/dist/esm/Form/form-store.js +40 -19
- package/dist/esm/List/demo/virtual.d.ts +3 -0
- package/dist/esm/List/demo/virtual.js +22 -0
- package/dist/esm/List/hooks/useListKeyboard.d.ts +28 -0
- package/dist/esm/List/hooks/useListKeyboard.js +173 -0
- package/dist/esm/List/interface.d.ts +29 -2
- package/dist/esm/List/list.js +171 -14
- package/dist/esm/List/style/index.less +24 -0
- package/dist/esm/Select/demo/columns.d.ts +3 -0
- package/dist/esm/Select/demo/columns.js +36 -0
- package/dist/esm/Select/hooks/useSelectKeyboard.d.ts +13 -1
- package/dist/esm/Select/hooks/useSelectKeyboard.js +91 -14
- package/dist/esm/Select/interface.d.ts +30 -3
- package/dist/esm/Select/option.js +6 -2
- package/dist/esm/Select/options.js +114 -32
- package/dist/esm/Select/select.js +40 -11
- package/dist/esm/Select/style/index.less +14 -0
- package/dist/esm/Selector/interface.d.ts +4 -0
- package/dist/esm/Selector/selector.js +10 -4
- package/dist/esm/Table/hooks/useColumns.js +1 -1
- package/dist/esm/Table/interface.d.ts +1 -1
- package/dist/esm/Table/style/index.less +35 -11
- package/dist/esm/Table/table.js +54 -2
- package/dist/esm/Table/tbody/cell.js +1 -1
- package/dist/esm/Table/util/header.d.ts +3 -1
- package/dist/esm/Table/util/header.js +30 -2
- package/dist/esm/Tree/demo/draggable.js +2 -0
- package/dist/esm/Tree/interface.d.ts +46 -23
- package/dist/esm/Tree/tree-node.d.ts +1 -1
- package/dist/esm/Tree/tree-node.js +26 -31
- package/dist/esm/Tree/tree.d.ts +13 -12
- package/dist/esm/Tree/tree.js +572 -163
- package/dist/esm/Tree/utils/tree-tools.d.ts +43 -10
- package/dist/esm/Tree/utils/tree-tools.js +219 -104
- package/dist/esm/locale/context.d.ts +5 -4
- package/dist/esm/locale/context.js +25 -9
- package/dist/esm/locale/interface.d.ts +20 -0
- package/dist/esm/locale/util.d.ts +3 -3
- package/dist/esm/style/color.css +3 -0
- package/dist/esm/style/size.css +2 -1
- package/dist/esm/style/themes.css +1 -1
- package/dist/esm/style/variables.css +6 -2
- package/dist/esm/utils/isEqual.js +5 -7
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/umd/yee-c.min.css +4 -4
- package/dist/umd/yee-c.min.js +1 -1
- package/package.json +1 -1
- package/src/Button/style/index.less +5 -4
- package/src/Drawer/drawer.tsx +1 -1
- package/src/Drawer/interface.ts +1 -0
- package/src/Drawer/style/index.less +1 -0
- package/src/Form/form-store.ts +42 -15
- package/src/List/demo/virtual.tsx +20 -0
- package/src/List/hooks/useListKeyboard.ts +235 -0
- package/src/List/index.en-US.md +6 -0
- package/src/List/index.zh-CN.md +6 -0
- package/src/List/interface.ts +29 -2
- package/src/List/list.tsx +157 -19
- package/src/List/style/index.less +24 -0
- package/src/Select/demo/columns.tsx +35 -0
- package/src/Select/hooks/useSelectKeyboard.ts +112 -15
- package/src/Select/index.en-US.md +4 -0
- package/src/Select/index.zh-CN.md +4 -0
- package/src/Select/interface.ts +31 -4
- package/src/Select/option.tsx +9 -3
- package/src/Select/options.tsx +119 -26
- package/src/Select/select.tsx +41 -8
- package/src/Select/style/index.less +14 -0
- package/src/Selector/interface.ts +4 -0
- package/src/Selector/selector.tsx +8 -3
- package/src/Table/hooks/useColumns.ts +1 -1
- package/src/Table/interface.ts +1 -1
- package/src/Table/style/index.less +35 -11
- package/src/Table/table.tsx +60 -1
- package/src/Table/tbody/cell.tsx +1 -1
- package/src/Table/util/header.ts +48 -2
- package/src/Tree/demo/draggable.tsx +2 -0
- package/src/Tree/interface.ts +46 -23
- package/src/Tree/tree-node.tsx +26 -29
- package/src/Tree/tree.tsx +573 -274
- package/src/Tree/utils/tree-tools.ts +231 -125
- package/src/locale/_README.md +22 -13
- package/src/locale/context.tsx +33 -16
- package/src/locale/interface.ts +24 -0
- package/src/locale/util.ts +6 -6
- package/src/style/color.css +3 -0
- package/src/style/size.css +2 -1
- package/src/utils/isEqual.ts +5 -7
- package/src/version.ts +1 -1
|
@@ -6,6 +6,17 @@ export interface MoveInfo {
|
|
|
6
6
|
dragKey: string | number;
|
|
7
7
|
dropKey: string | number;
|
|
8
8
|
position: DragPosition;
|
|
9
|
+
/**
|
|
10
|
+
* Full key path from root to the dragged node (inclusive).
|
|
11
|
+
* When provided, enables precise matching even with non-unique keys.
|
|
12
|
+
* Example: ['root', 'parent', 'child']
|
|
13
|
+
*/
|
|
14
|
+
dragPath?: Array<string | number>;
|
|
15
|
+
/**
|
|
16
|
+
* Full key path from root to the drop target node (inclusive).
|
|
17
|
+
* When provided, enables precise matching even with non-unique keys.
|
|
18
|
+
*/
|
|
19
|
+
dropPath?: Array<string | number>;
|
|
9
20
|
}
|
|
10
21
|
|
|
11
22
|
/**
|
|
@@ -16,6 +27,10 @@ export interface MoveInfo {
|
|
|
16
27
|
* nodes are reused by reference (data may carry non-cloneable values like
|
|
17
28
|
* React elements / functions in `icon`).
|
|
18
29
|
*
|
|
30
|
+
* When `dragPath` / `dropPath` are provided in `info`, matching is done by
|
|
31
|
+
* walking the exact path (supports non-unique keys). Otherwise falls back to
|
|
32
|
+
* first-match by key (backward compatible).
|
|
33
|
+
*
|
|
19
34
|
* Returns a brand-new root array; returns the input unchanged if dragKey is
|
|
20
35
|
* not found or drag and drop resolve to the same location.
|
|
21
36
|
*/
|
|
@@ -27,29 +42,66 @@ export function moveTreeNode<T extends Record<string, unknown>>(
|
|
|
27
42
|
if (!tree || (Array.isArray(tree) && tree.length === 0)) return [];
|
|
28
43
|
|
|
29
44
|
const { key: K, children: C } = fieldNames;
|
|
30
|
-
const { dragKey, dropKey, position } = info;
|
|
45
|
+
const { dragKey, dropKey, position, dragPath, dropPath } = info;
|
|
31
46
|
const root = Array.isArray(tree) ? tree : [tree];
|
|
32
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Match a node: when a path is provided, match by comparing node key against
|
|
50
|
+
* the expected key at the current depth; otherwise match by key equality.
|
|
51
|
+
*/
|
|
52
|
+
const matchNode = (
|
|
53
|
+
node: T,
|
|
54
|
+
targetKey: string | number,
|
|
55
|
+
path: Array<string | number> | undefined,
|
|
56
|
+
depth: number,
|
|
57
|
+
): boolean => {
|
|
58
|
+
if (path) {
|
|
59
|
+
return String(node[K]) === String(path[depth]);
|
|
60
|
+
}
|
|
61
|
+
return node[K] === targetKey;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const isTargetDepth = (
|
|
65
|
+
path: Array<string | number> | undefined,
|
|
66
|
+
depth: number,
|
|
67
|
+
): boolean => {
|
|
68
|
+
if (!path) return true; // no path constraint, match at any depth
|
|
69
|
+
return depth === path.length - 1;
|
|
70
|
+
};
|
|
71
|
+
|
|
33
72
|
// Phase 1: detach the drag node subtree (immutably).
|
|
34
|
-
// Returns the rebuilt array (without the drag node) plus the detached node.
|
|
35
73
|
const detach = (
|
|
36
74
|
nodes: T[],
|
|
75
|
+
depth: number,
|
|
37
76
|
): { found: T | null; nodes: T[] } => {
|
|
38
77
|
let found: T | null = null;
|
|
39
78
|
const next: T[] = [];
|
|
40
79
|
for (const n of nodes) {
|
|
41
|
-
if (
|
|
42
|
-
found
|
|
80
|
+
if (
|
|
81
|
+
!found &&
|
|
82
|
+
matchNode(n, dragKey, dragPath, depth) &&
|
|
83
|
+
isTargetDepth(dragPath, depth)
|
|
84
|
+
) {
|
|
85
|
+
found = n;
|
|
43
86
|
continue;
|
|
44
87
|
}
|
|
45
88
|
const kids = n[C] as T[] | undefined;
|
|
46
|
-
if (
|
|
47
|
-
|
|
89
|
+
if (
|
|
90
|
+
!found &&
|
|
91
|
+
dragPath &&
|
|
92
|
+
Array.isArray(kids) &&
|
|
93
|
+
!matchNode(n, dragKey, dragPath, depth)
|
|
94
|
+
) {
|
|
95
|
+
// A path lets us skip branches that cannot contain the drag node.
|
|
96
|
+
next.push(n);
|
|
97
|
+
} else if (!found && Array.isArray(kids)) {
|
|
98
|
+
// On the drag path (or no path constraint), recurse
|
|
99
|
+
const r = detach(kids, depth + 1);
|
|
48
100
|
if (r.found) {
|
|
49
101
|
found = r.found;
|
|
50
102
|
next.push({ ...n, [C]: r.nodes });
|
|
51
103
|
} else {
|
|
52
|
-
next.push(n);
|
|
104
|
+
next.push(n);
|
|
53
105
|
}
|
|
54
106
|
} else {
|
|
55
107
|
next.push(n);
|
|
@@ -58,38 +110,43 @@ export function moveTreeNode<T extends Record<string, unknown>>(
|
|
|
58
110
|
return { found, nodes: next };
|
|
59
111
|
};
|
|
60
112
|
|
|
61
|
-
const { found: dragNode, nodes: withoutDrag } = detach(root);
|
|
62
|
-
if (!dragNode) return root;
|
|
113
|
+
const { found: dragNode, nodes: withoutDrag } = detach(root, 0);
|
|
114
|
+
if (!dragNode) return root;
|
|
63
115
|
|
|
64
|
-
// Phase 2: insert per position
|
|
65
|
-
|
|
66
|
-
// follow the matched subtree must still be copied (by reference) or they'd be lost.
|
|
67
|
-
const insert = (nodes: T[]): { inserted: boolean; nodes: T[] } => {
|
|
116
|
+
// Phase 2: insert per position.
|
|
117
|
+
const insert = (nodes: T[], depth: number): { inserted: boolean; nodes: T[] } => {
|
|
68
118
|
const next: T[] = [];
|
|
69
119
|
let inserted = false;
|
|
70
120
|
for (const n of nodes) {
|
|
71
|
-
|
|
121
|
+
const isTarget =
|
|
122
|
+
!inserted &&
|
|
123
|
+
matchNode(n, dropKey, dropPath, depth) &&
|
|
124
|
+
isTargetDepth(dropPath, depth);
|
|
125
|
+
|
|
126
|
+
if (isTarget && position === 'inside') {
|
|
72
127
|
const oldKids = (Array.isArray(n[C]) ? n[C] : []) as T[];
|
|
73
128
|
next.push({ ...n, [C]: [...oldKids, dragNode] });
|
|
74
129
|
inserted = true;
|
|
75
130
|
continue;
|
|
76
131
|
}
|
|
77
|
-
if (
|
|
132
|
+
if (isTarget && position === 'before') {
|
|
78
133
|
next.push(dragNode, n);
|
|
79
134
|
inserted = true;
|
|
80
135
|
continue;
|
|
81
136
|
}
|
|
82
|
-
if (
|
|
137
|
+
if (isTarget && position === 'after') {
|
|
83
138
|
next.push(n, dragNode);
|
|
84
139
|
inserted = true;
|
|
85
140
|
continue;
|
|
86
141
|
}
|
|
87
142
|
const kids = n[C] as T[] | undefined;
|
|
88
143
|
if (inserted || !Array.isArray(kids)) {
|
|
89
|
-
|
|
144
|
+
next.push(n);
|
|
145
|
+
} else if (dropPath && !matchNode(n, dropKey, dropPath, depth)) {
|
|
146
|
+
// Not on the drop path at this level, skip recursion
|
|
90
147
|
next.push(n);
|
|
91
148
|
} else {
|
|
92
|
-
const r = insert(kids);
|
|
149
|
+
const r = insert(kids, depth + 1);
|
|
93
150
|
if (r.inserted) {
|
|
94
151
|
next.push({ ...n, [C]: r.nodes });
|
|
95
152
|
inserted = true;
|
|
@@ -101,34 +158,20 @@ export function moveTreeNode<T extends Record<string, unknown>>(
|
|
|
101
158
|
return { inserted, nodes: next };
|
|
102
159
|
};
|
|
103
160
|
|
|
104
|
-
const { inserted, nodes: result } = insert(withoutDrag);
|
|
105
|
-
// If the drop target wasn't found, nothing should change — return the
|
|
106
|
-
// original (drag node stays put) rather than silently dropping it.
|
|
161
|
+
const { inserted, nodes: result } = insert(withoutDrag, 0);
|
|
107
162
|
return inserted ? result : root;
|
|
108
163
|
}
|
|
109
164
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (Array.isArray(_children) && _children.length) {
|
|
121
|
-
_children.forEach((node) => {
|
|
122
|
-
keys.push(node[key] as string | number);
|
|
123
|
-
|
|
124
|
-
if (Array.isArray(node[children])) {
|
|
125
|
-
keys.push(...getChildKeys(node, fieldNames));
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
return keys;
|
|
130
|
-
}
|
|
131
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Flatten tree into a flat array with rich metadata.
|
|
167
|
+
*
|
|
168
|
+
* Key design decisions:
|
|
169
|
+
* - Uses `uid` (built from the key path) as the internal identifier, because
|
|
170
|
+
* user-provided `key` values may NOT be globally unique across the tree.
|
|
171
|
+
* - The returned Map is keyed by `uid` for O(1) lookups.
|
|
172
|
+
* - `childUids` is collected bottom-up in a single pass (O(n) total) instead
|
|
173
|
+
* of per-node recursive enumeration (which was O(n²)).
|
|
174
|
+
*/
|
|
132
175
|
export function tree2array<T extends Record<string, unknown>>(
|
|
133
176
|
tree: T | T[],
|
|
134
177
|
fieldNames: {
|
|
@@ -142,140 +185,203 @@ export function tree2array<T extends Record<string, unknown>>(
|
|
|
142
185
|
|
|
143
186
|
const data = Array.isArray(tree) ? tree : [tree];
|
|
144
187
|
const res: TreeNode<T>[] = [];
|
|
145
|
-
const map = new Map();
|
|
188
|
+
const map = new Map<string, TreeNode<T>>();
|
|
146
189
|
|
|
147
190
|
const { key, label, children } = fieldNames;
|
|
148
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Walk returns the list of descendant uids so the parent can collect them
|
|
194
|
+
* without a second traversal.
|
|
195
|
+
* Pushes nodes in PRE-ORDER (parent before children) so that visibleItems
|
|
196
|
+
* filtering can work in a single forward pass.
|
|
197
|
+
*/
|
|
149
198
|
function walk({
|
|
150
|
-
data,
|
|
151
|
-
|
|
199
|
+
data: node,
|
|
200
|
+
pUid,
|
|
152
201
|
depth,
|
|
153
202
|
path,
|
|
154
203
|
isLast,
|
|
155
204
|
lines,
|
|
156
205
|
}: {
|
|
157
206
|
data: T;
|
|
158
|
-
|
|
207
|
+
pUid: string | null;
|
|
159
208
|
depth: number;
|
|
160
|
-
path: string
|
|
209
|
+
path: Array<string | number>;
|
|
161
210
|
isLast: boolean;
|
|
162
211
|
lines: number[];
|
|
163
|
-
}) {
|
|
164
|
-
const
|
|
165
|
-
const _label =
|
|
166
|
-
const _children =
|
|
167
|
-
const nextPath = [...path,
|
|
168
|
-
const uid =
|
|
169
|
-
|
|
170
|
-
const isParent =
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const wrapperData = {
|
|
212
|
+
}): string[] {
|
|
213
|
+
const rawKey = node[key] as string | number;
|
|
214
|
+
const _label = node[label];
|
|
215
|
+
const _children = node[children] as T[] | undefined;
|
|
216
|
+
const nextPath = [...path, rawKey];
|
|
217
|
+
const uid = pUid ? `${pUid}\x00${String(rawKey)}` : String(rawKey);
|
|
218
|
+
|
|
219
|
+
const isParent = Array.isArray(_children) && _children.length > 0;
|
|
220
|
+
|
|
221
|
+
// Push parent FIRST (pre-order) so visibleItems can rely on forward scan
|
|
222
|
+
const wrapperData: TreeNode<T> = {
|
|
176
223
|
uid,
|
|
177
|
-
key:
|
|
178
|
-
label: _label,
|
|
224
|
+
key: rawKey,
|
|
225
|
+
label: _label as string,
|
|
226
|
+
title: node.title as string | undefined,
|
|
179
227
|
path,
|
|
180
|
-
|
|
228
|
+
pUid,
|
|
181
229
|
depth,
|
|
182
230
|
isLeaf: !isParent,
|
|
183
|
-
isLast
|
|
184
|
-
lines
|
|
185
|
-
disabled:
|
|
186
|
-
icon:
|
|
187
|
-
|
|
188
|
-
original:
|
|
189
|
-
}
|
|
231
|
+
isLast,
|
|
232
|
+
lines,
|
|
233
|
+
disabled: node.disabled as boolean | undefined,
|
|
234
|
+
icon: node.icon as React.ReactNode | (() => React.ReactNode) | undefined,
|
|
235
|
+
childUids: undefined, // will be filled after recursion
|
|
236
|
+
original: node,
|
|
237
|
+
};
|
|
190
238
|
|
|
191
239
|
res.push(wrapperData);
|
|
192
|
-
map.set(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
240
|
+
map.set(uid, wrapperData);
|
|
241
|
+
|
|
242
|
+
// Recurse children to collect their uids bottom-up
|
|
243
|
+
const childTotal = isParent ? _children!.length : 0;
|
|
244
|
+
const allDescendantUids: string[] = [];
|
|
245
|
+
|
|
246
|
+
if (isParent) {
|
|
247
|
+
_children!.forEach((child, index) => {
|
|
248
|
+
const childIsLast = index + 1 === childTotal;
|
|
249
|
+
const descendantUids = walk({
|
|
250
|
+
data: child,
|
|
251
|
+
pUid: uid,
|
|
252
|
+
depth: depth + 1,
|
|
253
|
+
path: nextPath,
|
|
254
|
+
isLast: childIsLast,
|
|
255
|
+
lines: isLast ? lines : [...lines, depth],
|
|
256
|
+
});
|
|
257
|
+
allDescendantUids.push(...descendantUids);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Now fill in childUids
|
|
262
|
+
if (allDescendantUids.length > 0) {
|
|
263
|
+
wrapperData.childUids = allDescendantUids;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Return this uid + all descendant uids to the caller
|
|
267
|
+
return [uid, ...allDescendantUids];
|
|
204
268
|
}
|
|
205
269
|
|
|
206
|
-
const
|
|
207
|
-
data.forEach((node, index) =>
|
|
270
|
+
const rootTotal = data.length;
|
|
271
|
+
data.forEach((node, index) => {
|
|
208
272
|
walk({
|
|
209
273
|
data: node,
|
|
210
|
-
|
|
274
|
+
pUid: null,
|
|
211
275
|
depth: 0,
|
|
212
276
|
path: [],
|
|
213
|
-
isLast: index + 1 ===
|
|
214
|
-
lines: index + 1 ===
|
|
215
|
-
})
|
|
216
|
-
);
|
|
277
|
+
isLast: index + 1 === rootTotal,
|
|
278
|
+
lines: index + 1 === rootTotal ? [] : [0],
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
217
282
|
return [res, map];
|
|
218
283
|
}
|
|
219
284
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
285
|
+
/**
|
|
286
|
+
* Check whether array `a` contains every element in `b`.
|
|
287
|
+
*/
|
|
288
|
+
export function containsAll<T>(a: Set<T>, b: Array<T>): boolean {
|
|
289
|
+
if (!b || b.length === 0) return true;
|
|
290
|
+
return b.every((i) => a.has(i));
|
|
225
291
|
}
|
|
226
292
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
293
|
+
/**
|
|
294
|
+
* Remove all elements in `b` from `a`.
|
|
295
|
+
*/
|
|
296
|
+
export function removeAll<T>(a: Set<T>, b: Array<T>): Set<T> {
|
|
297
|
+
const result = new Set<T>();
|
|
298
|
+
a.forEach((item) => result.add(item));
|
|
299
|
+
for (const item of b) {
|
|
300
|
+
result.delete(item);
|
|
301
|
+
}
|
|
302
|
+
return result;
|
|
231
303
|
}
|
|
232
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Compute new checked uids when a node is toggled.
|
|
307
|
+
* Uses Set-based operations for O(1) membership checks.
|
|
308
|
+
* Disabled nodes are NOT affected by parent/child propagation — their checked
|
|
309
|
+
* state remains unchanged unless they are the directly toggled node.
|
|
310
|
+
*/
|
|
233
311
|
export function getCheckedKeys({
|
|
234
312
|
checked,
|
|
235
313
|
keys,
|
|
236
|
-
|
|
314
|
+
uid,
|
|
237
315
|
map,
|
|
238
316
|
}: {
|
|
239
317
|
checked: boolean;
|
|
240
|
-
keys:
|
|
241
|
-
|
|
318
|
+
keys: Set<string>;
|
|
319
|
+
uid: string;
|
|
242
320
|
map: Map<string, TreeNode<unknown>>;
|
|
243
|
-
}) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
321
|
+
}): Set<string> {
|
|
322
|
+
const item = map.get(uid);
|
|
323
|
+
if (!item) return keys;
|
|
324
|
+
|
|
325
|
+
const childUids = item.childUids || [];
|
|
326
|
+
const newKeys = new Set<string>();
|
|
327
|
+
keys.forEach((k) => newKeys.add(k));
|
|
248
328
|
|
|
249
329
|
if (checked) {
|
|
250
|
-
|
|
330
|
+
// Add this node + non-disabled descendants
|
|
331
|
+
newKeys.add(uid);
|
|
332
|
+
for (const cuid of childUids) {
|
|
333
|
+
const child = map.get(cuid);
|
|
334
|
+
if (child && !child.disabled) {
|
|
335
|
+
newKeys.add(cuid);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
251
338
|
|
|
252
|
-
|
|
253
|
-
|
|
339
|
+
// Walk up: if all non-disabled children of an ancestor are checked, check the ancestor
|
|
340
|
+
let pUid = item.pUid;
|
|
341
|
+
while (pUid) {
|
|
342
|
+
const parent = map.get(pUid);
|
|
254
343
|
if (parent) {
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
344
|
+
if (parent.disabled) {
|
|
345
|
+
// Stop propagation at disabled ancestors
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
if (!newKeys.has(parent.uid) && parent.childUids) {
|
|
349
|
+
// Check if all non-disabled children are checked
|
|
350
|
+
const allNonDisabledChecked = parent.childUids.every((cuid) => {
|
|
351
|
+
const child = map.get(cuid);
|
|
352
|
+
return (child && child.disabled) || newKeys.has(cuid);
|
|
353
|
+
});
|
|
354
|
+
if (allNonDisabledChecked) {
|
|
355
|
+
newKeys.add(parent.uid);
|
|
356
|
+
}
|
|
260
357
|
}
|
|
261
|
-
|
|
358
|
+
pUid = parent.pUid;
|
|
262
359
|
} else {
|
|
263
|
-
|
|
360
|
+
pUid = null;
|
|
264
361
|
}
|
|
265
362
|
}
|
|
266
363
|
} else {
|
|
267
|
-
|
|
268
|
-
newKeys
|
|
364
|
+
// Remove this node + non-disabled descendants
|
|
365
|
+
newKeys.delete(uid);
|
|
366
|
+
for (const cuid of childUids) {
|
|
367
|
+
const child = map.get(cuid);
|
|
368
|
+
if (child && !child.disabled) {
|
|
369
|
+
newKeys.delete(cuid);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
269
372
|
|
|
270
|
-
|
|
271
|
-
|
|
373
|
+
// Walk up: uncheck non-disabled ancestors
|
|
374
|
+
let pUid = item.pUid;
|
|
375
|
+
while (pUid) {
|
|
376
|
+
const parent = map.get(pUid);
|
|
272
377
|
if (parent) {
|
|
273
|
-
if (
|
|
274
|
-
|
|
378
|
+
if (parent.disabled) {
|
|
379
|
+
break;
|
|
275
380
|
}
|
|
276
|
-
|
|
381
|
+
newKeys.delete(parent.uid);
|
|
382
|
+
pUid = parent.pUid;
|
|
277
383
|
} else {
|
|
278
|
-
|
|
384
|
+
pUid = null;
|
|
279
385
|
}
|
|
280
386
|
}
|
|
281
387
|
}
|
package/src/locale/_README.md
CHANGED
|
@@ -73,31 +73,40 @@ function LanguageSelector() {
|
|
|
73
73
|
}
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
###
|
|
76
|
+
### 自定义 / 部分覆盖语言包
|
|
77
|
+
|
|
78
|
+
传入一个带 `locale` 字段的(可以是**部分的**)对象,只需写要覆盖的 key,其余从对应内置语言包继承。对象会基于 `locale` 字段深度合并到内置包上,无需提供完整包。
|
|
77
79
|
|
|
78
80
|
```tsx
|
|
79
81
|
import { LocaleProvider } from '@rainbow-oh/yee-c/locale';
|
|
80
82
|
|
|
81
|
-
|
|
82
|
-
locale: 'zh_CN',
|
|
83
|
-
datepicker: {
|
|
84
|
-
now: '当前时间',
|
|
85
|
-
confirm: '好的',
|
|
86
|
-
},
|
|
87
|
-
select: {
|
|
88
|
-
noData: '空空如也',
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
|
|
83
|
+
// 只覆盖 datepicker.now/confirm 和 select.noData,其余沿用 zh_CN 内置包
|
|
92
84
|
function App() {
|
|
93
85
|
return (
|
|
94
|
-
<LocaleProvider
|
|
86
|
+
<LocaleProvider
|
|
87
|
+
defaultLocale={{
|
|
88
|
+
locale: 'zh_CN',
|
|
89
|
+
datepicker: { now: '当前时间', confirm: '好的' },
|
|
90
|
+
select: { noData: '空空如也' },
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
95
93
|
<MyComponent />
|
|
96
94
|
</LocaleProvider>
|
|
97
95
|
);
|
|
98
96
|
}
|
|
99
97
|
```
|
|
100
98
|
|
|
99
|
+
运行时切换语言也可传部分覆盖,同样合并到对应基础包:
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
const { setLocale } = useLocale();
|
|
103
|
+
|
|
104
|
+
// 切到英文,同时把弹窗的 OK 文案改成 "Sure"
|
|
105
|
+
setLocale({ locale: 'en_US', modal: { okText: 'Sure' } });
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
> 注意:对象形式必须带 `locale` 字段,用来确定基于哪个内置包合并。也可以用 `deepMerge(LOCALE_CONFIGS.zh_CN, { ... })` 自行预先生成完整包再传入。
|
|
109
|
+
|
|
101
110
|
### 异步加载语言包
|
|
102
111
|
|
|
103
112
|
```tsx
|
package/src/locale/context.tsx
CHANGED
|
@@ -7,10 +7,10 @@ import React, {
|
|
|
7
7
|
useMemo,
|
|
8
8
|
useState,
|
|
9
9
|
} from 'react';
|
|
10
|
-
import { DEFAULT_LOCALE, LOCALE_CONFIGS } from './constants';
|
|
11
|
-
import type { Locale, TFunction } from './interface';
|
|
10
|
+
import { DEFAULT_LOCALE, DEFAULT_LOCALE_CONFIG, LOCALE_CONFIGS } from './constants';
|
|
11
|
+
import type { Locale, LocaleConfigInput, TFunction } from './interface';
|
|
12
12
|
import defaultLocaleData from './lang/en_US';
|
|
13
|
-
import { isValidLocale, translate } from './util';
|
|
13
|
+
import { deepMerge, isValidLocale, translate } from './util';
|
|
14
14
|
|
|
15
15
|
// ============ Default value (fallback when no Provider) ============
|
|
16
16
|
|
|
@@ -30,12 +30,31 @@ const createDefaultValue = (): LocaleContextType => {
|
|
|
30
30
|
|
|
31
31
|
const defaultValue = createDefaultValue();
|
|
32
32
|
|
|
33
|
+
// ============ Locale input resolution ============
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve a locale input (a language code, or a possibly-partial locale
|
|
37
|
+
* object) into a full `Locale` pack. Partial objects are deep-merged onto the
|
|
38
|
+
* built-in pack matching their `locale` field, so only the overridden keys
|
|
39
|
+
* change and everything else is inherited.
|
|
40
|
+
*/
|
|
41
|
+
function resolveLocaleConfigInput(input: LocaleConfigInput): Locale {
|
|
42
|
+
if (typeof input === 'string') {
|
|
43
|
+
return LOCALE_CONFIGS[input] || defaultLocaleData;
|
|
44
|
+
}
|
|
45
|
+
if (input && typeof input.locale === 'string') {
|
|
46
|
+
const base = LOCALE_CONFIGS[input.locale] || DEFAULT_LOCALE_CONFIG;
|
|
47
|
+
return deepMerge(base, input);
|
|
48
|
+
}
|
|
49
|
+
return defaultLocaleData;
|
|
50
|
+
}
|
|
51
|
+
|
|
33
52
|
// ============ Context definition ============
|
|
34
53
|
|
|
35
54
|
interface LocaleContextType {
|
|
36
55
|
locale: Locale;
|
|
37
56
|
lang: string; // Current language code 'zh_CN'
|
|
38
|
-
setLocale: (locale:
|
|
57
|
+
setLocale: (locale: LocaleConfigInput) => void; // Supports object (full or partial) or language code
|
|
39
58
|
t: TFunction;
|
|
40
59
|
}
|
|
41
60
|
|
|
@@ -45,8 +64,9 @@ export const LocaleContext = createContext<LocaleContextType>(defaultValue);
|
|
|
45
64
|
|
|
46
65
|
interface LocaleProviderProps {
|
|
47
66
|
children: React.ReactNode;
|
|
48
|
-
/** Initial locale
|
|
49
|
-
|
|
67
|
+
/** Initial locale: a language code, or a full/partial locale object.
|
|
68
|
+
* Partial objects are merged onto the matching built-in pack. */
|
|
69
|
+
defaultLocale?: LocaleConfigInput;
|
|
50
70
|
/** Async locale loader function */
|
|
51
71
|
loadLocale?: (lang: string) => Promise<Locale>;
|
|
52
72
|
/** Whether to auto-fallback to default for missing fields */
|
|
@@ -60,13 +80,9 @@ export function LocaleProvider({
|
|
|
60
80
|
fallbackToDefault = true,
|
|
61
81
|
}: LocaleProviderProps) {
|
|
62
82
|
// State management
|
|
63
|
-
const [locale, setLocaleState] = useState<Locale>(() =>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return LOCALE_CONFIGS[initialLocale] || defaultLocaleData;
|
|
67
|
-
}
|
|
68
|
-
return isValidLocale(initialLocale) ? initialLocale : defaultLocaleData;
|
|
69
|
-
});
|
|
83
|
+
const [locale, setLocaleState] = useState<Locale>(() =>
|
|
84
|
+
resolveLocaleConfigInput(initialLocale),
|
|
85
|
+
);
|
|
70
86
|
|
|
71
87
|
const [lang, setLang] = useState<string>(
|
|
72
88
|
typeof initialLocale === 'string' ? initialLocale : initialLocale.locale,
|
|
@@ -131,7 +147,7 @@ export function LocaleProvider({
|
|
|
131
147
|
|
|
132
148
|
// Set locale method
|
|
133
149
|
const setLocale = useCallback(
|
|
134
|
-
(target:
|
|
150
|
+
(target: LocaleConfigInput) => {
|
|
135
151
|
if (typeof target === 'string') {
|
|
136
152
|
// Prefer sync lookup from static locale packs
|
|
137
153
|
const staticLocale = LOCALE_CONFIGS[target];
|
|
@@ -142,8 +158,9 @@ export function LocaleProvider({
|
|
|
142
158
|
}
|
|
143
159
|
// Otherwise use async loading
|
|
144
160
|
loadAndSetLocale(target);
|
|
145
|
-
} else if (
|
|
146
|
-
|
|
161
|
+
} else if (target && typeof target.locale === 'string') {
|
|
162
|
+
// Full or partial object: merge onto the matching built-in pack
|
|
163
|
+
setLocaleState(resolveLocaleConfigInput(target));
|
|
147
164
|
setLang(target.locale);
|
|
148
165
|
}
|
|
149
166
|
},
|
package/src/locale/interface.ts
CHANGED
|
@@ -197,3 +197,27 @@ export type TFunction = <K extends LocaleKey>(
|
|
|
197
197
|
key: K,
|
|
198
198
|
params?: LocaleParams<K>,
|
|
199
199
|
) => string;
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Recursively-optional version of T. Every level is optional; arrays are kept
|
|
203
|
+
* as-is (replaced wholesale rather than element-merged).
|
|
204
|
+
*
|
|
205
|
+
* Used so a locale override only needs to specify the keys it changes:
|
|
206
|
+
* `{ datepicker: { now: 'Now' } }` is a valid `DeepPartial<Locale>`.
|
|
207
|
+
*/
|
|
208
|
+
export type DeepPartial<T> = T extends (...args: any[]) => any
|
|
209
|
+
? T
|
|
210
|
+
: T extends object
|
|
211
|
+
? T extends readonly any[]
|
|
212
|
+
? T
|
|
213
|
+
: { [K in keyof T]?: DeepPartial<T[K]> }
|
|
214
|
+
: T;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Locale input accepted by `LocaleProvider` / `useLocale().setLocale`:
|
|
218
|
+
* - a language code string (`'zh_CN'`), resolved from the built-in packs, or
|
|
219
|
+
* - a (possibly partial) locale object. A partial object MUST carry `locale`
|
|
220
|
+
* so the provider knows which built-in pack to merge the overrides onto;
|
|
221
|
+
* only the keys you specify are overridden, everything else is inherited.
|
|
222
|
+
*/
|
|
223
|
+
export type LocaleConfigInput = string | (DeepPartial<Locale> & { locale: string });
|