@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
package/src/Tree/tree.tsx
CHANGED
|
@@ -2,17 +2,19 @@ import clsx from 'clsx';
|
|
|
2
2
|
import React, {
|
|
3
3
|
ChangeEvent,
|
|
4
4
|
createContext,
|
|
5
|
-
useCallback,
|
|
6
5
|
useContext,
|
|
7
6
|
useMemo,
|
|
8
7
|
useRef,
|
|
9
8
|
useState,
|
|
10
9
|
} from 'react';
|
|
10
|
+
import { AnimatePresence, motion } from 'motion/react';
|
|
11
11
|
import { GlobalContext } from '../Config-Provider';
|
|
12
|
+
import useEvent from '../hooks/useEvent';
|
|
12
13
|
import useMergedState from '../hooks/useMergedState';
|
|
14
|
+
import useVirtualList from '../hooks/useVirtualList';
|
|
13
15
|
import mergeContextToProps from '../utils/mergeContextToProps';
|
|
14
|
-
import type { DragPosition, TreeProps } from './interface';
|
|
15
|
-
import
|
|
16
|
+
import type { DragPosition, TreeNode, TreeProps } from './interface';
|
|
17
|
+
import TreeNodeComp from './tree-node';
|
|
16
18
|
import { getCheckedKeys, tree2array } from './utils/tree-tools';
|
|
17
19
|
|
|
18
20
|
import './style/index.less';
|
|
@@ -23,7 +25,7 @@ const DEFAULTFIELDNAMES = {
|
|
|
23
25
|
children: 'children' as const,
|
|
24
26
|
};
|
|
25
27
|
|
|
26
|
-
type DropState = {
|
|
28
|
+
type DropState = { uid: string; position: DragPosition } | null;
|
|
27
29
|
|
|
28
30
|
export const TreeContext = createContext<{
|
|
29
31
|
prefixCls?: string;
|
|
@@ -32,26 +34,28 @@ export const TreeContext = createContext<{
|
|
|
32
34
|
icon?: React.ReactNode | ((props: any) => React.ReactNode);
|
|
33
35
|
showLine?: boolean;
|
|
34
36
|
switcherIcon?: [React.ReactNode, React.ReactNode];
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
selectedUids: Set<string>;
|
|
38
|
+
checkedUids: Set<string>;
|
|
39
|
+
indeterminateUids: Set<string>;
|
|
40
|
+
expandedUids: Set<string>;
|
|
38
41
|
draggable?: boolean;
|
|
39
|
-
|
|
42
|
+
draggingUid: string | null;
|
|
40
43
|
dropState: DropState;
|
|
41
|
-
onCheck: (event: ChangeEvent<HTMLInputElement
|
|
42
|
-
onSelect: (
|
|
43
|
-
onExpand: (
|
|
44
|
-
onNodeDragStart: (
|
|
45
|
-
onNodeDragOver: (
|
|
46
|
-
onNodeDragLeave: (
|
|
47
|
-
onNodeDrop: (
|
|
44
|
+
onCheck: (event: ChangeEvent<HTMLInputElement>, uid: string) => void;
|
|
45
|
+
onSelect: (uid: string) => void;
|
|
46
|
+
onExpand: (uid: string) => void;
|
|
47
|
+
onNodeDragStart: (uid: string, e: React.DragEvent) => void;
|
|
48
|
+
onNodeDragOver: (uid: string, e: React.DragEvent) => void;
|
|
49
|
+
onNodeDragLeave: (uid: string, e: React.DragEvent) => void;
|
|
50
|
+
onNodeDrop: (uid: string, e: React.DragEvent) => void;
|
|
48
51
|
}>({
|
|
49
52
|
showIcon: true,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
selectedUids: new Set(),
|
|
54
|
+
checkedUids: new Set(),
|
|
55
|
+
indeterminateUids: new Set(),
|
|
56
|
+
expandedUids: new Set(),
|
|
53
57
|
draggable: false,
|
|
54
|
-
|
|
58
|
+
draggingUid: null,
|
|
55
59
|
dropState: null,
|
|
56
60
|
onCheck: () => {},
|
|
57
61
|
onSelect: () => {},
|
|
@@ -62,11 +66,115 @@ export const TreeContext = createContext<{
|
|
|
62
66
|
onNodeDrop: () => {},
|
|
63
67
|
});
|
|
64
68
|
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Helpers: convert between external user keys and internal uids
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
const EMPTY_ARRAY: string[] = [];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Given a list of user-provided keys, find matching uids.
|
|
77
|
+
* A key may match multiple nodes (non-unique keys), so we return all matches.
|
|
78
|
+
* Returns an array of uid strings (for use with useMergedState).
|
|
79
|
+
*/
|
|
80
|
+
function keysToUidArray(
|
|
81
|
+
keys: Array<string | number> | undefined,
|
|
82
|
+
flatItems: TreeNode<any>[],
|
|
83
|
+
): string[] | undefined {
|
|
84
|
+
if (!keys) return undefined;
|
|
85
|
+
if (keys.length === 0) return EMPTY_ARRAY;
|
|
86
|
+
const keySet = new Set(keys.map(String));
|
|
87
|
+
const uids: string[] = [];
|
|
88
|
+
for (const item of flatItems) {
|
|
89
|
+
if (keySet.has(String(item.key))) {
|
|
90
|
+
uids.push(item.uid);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return uids;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Hook that memoizes a uid array derived from external keys.
|
|
98
|
+
* Only returns a new reference when the actual uid content changes.
|
|
99
|
+
* This prevents useMergedState from seeing a "new value" every render.
|
|
100
|
+
*/
|
|
101
|
+
function useStableUidArray(
|
|
102
|
+
keys: Array<string | number> | undefined,
|
|
103
|
+
flatItems: TreeNode<any>[],
|
|
104
|
+
): string[] | undefined {
|
|
105
|
+
const ref = useRef<{
|
|
106
|
+
keys: Array<string | number> | undefined;
|
|
107
|
+
result: string[] | undefined;
|
|
108
|
+
}>({
|
|
109
|
+
keys: undefined,
|
|
110
|
+
result: undefined,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Only recompute if external keys reference or flatItems changed
|
|
114
|
+
const computed = useMemo(
|
|
115
|
+
() => keysToUidArray(keys, flatItems),
|
|
116
|
+
[keys, flatItems],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// Stable reference: reuse previous if content is identical
|
|
120
|
+
if (computed === undefined) {
|
|
121
|
+
ref.current = { keys, result: undefined };
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const prev = ref.current.result;
|
|
126
|
+
if (
|
|
127
|
+
prev &&
|
|
128
|
+
prev.length === computed.length &&
|
|
129
|
+
prev.every((v, i) => v === computed[i])
|
|
130
|
+
) {
|
|
131
|
+
return prev;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
ref.current = { keys, result: computed };
|
|
135
|
+
return computed;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Convert an array of uids to user keys (for external callbacks).
|
|
140
|
+
*/
|
|
141
|
+
function uidsToKeys(
|
|
142
|
+
uids: string[],
|
|
143
|
+
map: Map<string, TreeNode<any>>,
|
|
144
|
+
): Array<string | number> {
|
|
145
|
+
const keys: Array<string | number> = [];
|
|
146
|
+
for (const uid of uids) {
|
|
147
|
+
const node = map.get(uid);
|
|
148
|
+
if (node) keys.push(node.key);
|
|
149
|
+
}
|
|
150
|
+
return keys;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Build a Set from an array (avoids downlevelIteration issues with Set constructor). */
|
|
154
|
+
function arrayToSet(arr: string[]): Set<string> {
|
|
155
|
+
const s = new Set<string>();
|
|
156
|
+
for (const item of arr) {
|
|
157
|
+
s.add(item);
|
|
158
|
+
}
|
|
159
|
+
return s;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Convert a Set to an array (avoids spread/iteration issues). */
|
|
163
|
+
function setToArray(s: Set<string>): string[] {
|
|
164
|
+
const arr: string[] = [];
|
|
165
|
+
s.forEach((v) => arr.push(v));
|
|
166
|
+
return arr;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Tree Component
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
65
173
|
const Tree = <T extends Record<string, unknown> = any>(
|
|
66
174
|
baseprops: TreeProps<T>,
|
|
67
175
|
) => {
|
|
68
176
|
const { tree } = useContext(GlobalContext);
|
|
69
|
-
// @ts-expect-error - tree context type may not match TreeProps<T> exactly
|
|
177
|
+
// @ts-expect-error - tree context type may not match TreeProps<T> exactly
|
|
70
178
|
const props = mergeContextToProps(baseprops, tree);
|
|
71
179
|
|
|
72
180
|
const {
|
|
@@ -94,279 +202,471 @@ const Tree = <T extends Record<string, unknown> = any>(
|
|
|
94
202
|
onSelect,
|
|
95
203
|
onCheck,
|
|
96
204
|
onExpand,
|
|
205
|
+
virtual = false,
|
|
206
|
+
height,
|
|
207
|
+
itemHeight = 28,
|
|
97
208
|
} = props;
|
|
98
209
|
|
|
99
|
-
|
|
100
|
-
Array<string | number>
|
|
101
|
-
>([], {
|
|
102
|
-
value: selectedKeys,
|
|
103
|
-
defaultValue: defaultSelectedKeys,
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
const [mergedCheckedKeys, setMergedCheckedKeys] = useMergedState<
|
|
107
|
-
Array<string | number>
|
|
108
|
-
>([], {
|
|
109
|
-
value: checkedKeys,
|
|
110
|
-
defaultValue: defaultCheckedKeys,
|
|
111
|
-
});
|
|
112
|
-
|
|
210
|
+
// ======================== Data ========================
|
|
113
211
|
const [flatItems, itemMap] = useMemo(
|
|
114
212
|
() => tree2array(dataSource ?? [], fieldNames),
|
|
115
213
|
[dataSource, fieldNames],
|
|
116
214
|
);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
return
|
|
215
|
+
|
|
216
|
+
// All parent uids (for defaultExpandAll)
|
|
217
|
+
const allParentUidArray = useMemo(() => {
|
|
218
|
+
const uids: string[] = [];
|
|
219
|
+
for (const item of flatItems) {
|
|
220
|
+
if (!item.isLeaf) uids.push(item.uid);
|
|
221
|
+
}
|
|
222
|
+
return uids;
|
|
125
223
|
}, [flatItems]);
|
|
126
224
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
225
|
+
// ==================== State (array-based for useMergedState compatibility) ====================
|
|
226
|
+
// useMergedState works with arrays (serializable, isEqual-friendly).
|
|
227
|
+
// We derive Sets via useMemo for O(1) lookups in render/handlers.
|
|
228
|
+
// useStableUidArray ensures stable references to prevent infinite re-render loops.
|
|
229
|
+
|
|
230
|
+
const stableExpandedValue = useStableUidArray(expandedKeys, flatItems);
|
|
231
|
+
const stableExpandedDefault = useStableUidArray(defaultExpandedKeys, flatItems);
|
|
232
|
+
const stableSelectedValue = useStableUidArray(selectedKeys, flatItems);
|
|
233
|
+
const stableSelectedDefault = useStableUidArray(defaultSelectedKeys, flatItems);
|
|
234
|
+
const stableCheckedValue = useStableUidArray(checkedKeys, flatItems);
|
|
235
|
+
const stableCheckedDefault = useStableUidArray(defaultCheckedKeys, flatItems);
|
|
236
|
+
|
|
237
|
+
// --- Expanded ---
|
|
238
|
+
const [expandedUidArray, setExpandedUidArray] = useMergedState<string[]>(
|
|
239
|
+
() => (defaultExpandAll ? allParentUidArray : []),
|
|
240
|
+
{
|
|
241
|
+
value: stableExpandedValue,
|
|
242
|
+
defaultValue: stableExpandedDefault,
|
|
132
243
|
},
|
|
244
|
+
);
|
|
245
|
+
const expandedUidSet = useMemo(
|
|
246
|
+
() => arrayToSet(expandedUidArray),
|
|
247
|
+
[expandedUidArray],
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
// --- Selected ---
|
|
251
|
+
const [selectedUidArray, setSelectedUidArray] = useMergedState<string[]>(
|
|
252
|
+
[],
|
|
133
253
|
{
|
|
134
|
-
value:
|
|
135
|
-
defaultValue:
|
|
254
|
+
value: stableSelectedValue,
|
|
255
|
+
defaultValue: stableSelectedDefault,
|
|
136
256
|
},
|
|
137
257
|
);
|
|
258
|
+
const selectedUidSet = useMemo(
|
|
259
|
+
() => arrayToSet(selectedUidArray),
|
|
260
|
+
[selectedUidArray],
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
// --- Checked ---
|
|
264
|
+
const [checkedUidArray, setCheckedUidArray] = useMergedState<string[]>([], {
|
|
265
|
+
value: stableCheckedValue,
|
|
266
|
+
defaultValue: stableCheckedDefault,
|
|
267
|
+
});
|
|
268
|
+
const checkedUidSet = useMemo(
|
|
269
|
+
() => arrayToSet(checkedUidArray),
|
|
270
|
+
[checkedUidArray],
|
|
271
|
+
);
|
|
138
272
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
273
|
+
// --- Indeterminate (half-select) ---
|
|
274
|
+
// A non-leaf node is indeterminate if:
|
|
275
|
+
// - It is NOT itself checked
|
|
276
|
+
// - At least one of its descendants IS checked
|
|
277
|
+
const indeterminateUidSet = useMemo(() => {
|
|
278
|
+
const result = new Set<string>();
|
|
279
|
+
for (const item of flatItems) {
|
|
280
|
+
if (
|
|
281
|
+
!item.isLeaf &&
|
|
282
|
+
!checkedUidSet.has(item.uid) &&
|
|
283
|
+
item.childUids &&
|
|
284
|
+
item.childUids.some((cuid) => checkedUidSet.has(cuid))
|
|
285
|
+
) {
|
|
286
|
+
result.add(item.uid);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return result;
|
|
290
|
+
}, [flatItems, checkedUidSet]);
|
|
291
|
+
|
|
292
|
+
// ==================== Visible Items ====================
|
|
293
|
+
// A node is visible if it's a root OR its parent is both visible and expanded.
|
|
294
|
+
// flatItems is in pre-order so we can compute in a single forward pass.
|
|
295
|
+
const visibleItems = useMemo(() => {
|
|
296
|
+
const visibleSet = new Set<string>();
|
|
297
|
+
const result: TreeNode<T>[] = [];
|
|
298
|
+
|
|
299
|
+
for (const item of flatItems) {
|
|
300
|
+
if (item.depth === 0) {
|
|
301
|
+
visibleSet.add(item.uid);
|
|
302
|
+
result.push(item);
|
|
303
|
+
} else if (
|
|
304
|
+
item.pUid &&
|
|
305
|
+
visibleSet.has(item.pUid) &&
|
|
306
|
+
expandedUidSet.has(item.pUid)
|
|
307
|
+
) {
|
|
308
|
+
visibleSet.add(item.uid);
|
|
309
|
+
result.push(item);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return result;
|
|
314
|
+
}, [flatItems, expandedUidSet]);
|
|
315
|
+
|
|
316
|
+
// ==================== Handlers ====================
|
|
317
|
+
// All handlers use useEvent for stable references — they always read
|
|
318
|
+
// the latest state via closure without being listed as deps.
|
|
319
|
+
|
|
320
|
+
const handleCheck = useEvent(
|
|
321
|
+
(event: ChangeEvent<HTMLInputElement>, uid: string) => {
|
|
142
322
|
const checked = event.target.checked;
|
|
143
|
-
const treeNode = itemMap.get(
|
|
323
|
+
const treeNode = itemMap.get(uid);
|
|
144
324
|
|
|
145
|
-
const
|
|
325
|
+
const newUidSet = getCheckedKeys({
|
|
146
326
|
checked,
|
|
147
|
-
keys:
|
|
148
|
-
|
|
149
|
-
map: itemMap,
|
|
327
|
+
keys: checkedUidSet,
|
|
328
|
+
uid,
|
|
329
|
+
map: itemMap as Map<string, TreeNode<unknown>>,
|
|
150
330
|
});
|
|
151
331
|
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
.filter((node): node is T => node !== undefined);
|
|
155
|
-
|
|
156
|
-
setMergedCheckedKeys(newKeys);
|
|
332
|
+
const newUidArr = setToArray(newUidSet);
|
|
333
|
+
setCheckedUidArray(newUidArr);
|
|
157
334
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
335
|
+
if (treeNode && onCheck) {
|
|
336
|
+
const externalKeys = uidsToKeys(newUidArr, itemMap);
|
|
337
|
+
const nodes: T[] = [];
|
|
338
|
+
newUidSet.forEach((u) => {
|
|
339
|
+
const n = itemMap.get(u)?.original;
|
|
340
|
+
if (n) nodes.push(n);
|
|
341
|
+
});
|
|
342
|
+
onCheck(externalKeys, {
|
|
161
343
|
checked,
|
|
162
344
|
checkedNodes: nodes,
|
|
163
|
-
node:
|
|
345
|
+
node: treeNode.original,
|
|
164
346
|
});
|
|
165
347
|
}
|
|
166
348
|
},
|
|
167
|
-
[mergedCheckedKeys, itemMap, onCheck],
|
|
168
349
|
);
|
|
169
350
|
|
|
170
|
-
const handleSelect =
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const newKeys = multiple
|
|
174
|
-
? selected
|
|
175
|
-
? [...mergedSelectedKeys.filter((k) => k !== key)]
|
|
176
|
-
: [...mergedSelectedKeys, key]
|
|
177
|
-
: selected
|
|
178
|
-
? []
|
|
179
|
-
: [key];
|
|
180
|
-
|
|
181
|
-
setMergedSelectedKeys(newKeys);
|
|
182
|
-
|
|
183
|
-
const nodes = newKeys
|
|
184
|
-
.map((k) => itemMap.get(k as string)?.original)
|
|
185
|
-
.filter((node): node is T => node !== undefined);
|
|
186
|
-
const treeNode = itemMap.get(key as string);
|
|
187
|
-
const originalNode = treeNode?.original;
|
|
188
|
-
|
|
189
|
-
if (originalNode) {
|
|
190
|
-
onSelect?.(newKeys, {
|
|
191
|
-
selected: !selected,
|
|
192
|
-
selectedNodes: nodes,
|
|
193
|
-
node: originalNode,
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
},
|
|
197
|
-
[mergedSelectedKeys, itemMap, multiple, onSelect],
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
const handleExpand = useCallback(
|
|
201
|
-
(key: string | number) => {
|
|
202
|
-
const expanded = mergedExpandedKeys.includes(key);
|
|
203
|
-
const newKeys = expanded
|
|
204
|
-
? [...mergedExpandedKeys.filter((k) => k !== key)]
|
|
205
|
-
: [...mergedExpandedKeys, key];
|
|
206
|
-
|
|
207
|
-
setMergedExpandedKeys(newKeys);
|
|
351
|
+
const handleSelect = useEvent((uid: string) => {
|
|
352
|
+
const selected = selectedUidSet.has(uid);
|
|
353
|
+
let newUidArr: string[];
|
|
208
354
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
.filter((
|
|
355
|
+
if (multiple) {
|
|
356
|
+
if (selected) {
|
|
357
|
+
newUidArr = selectedUidArray.filter((u) => u !== uid);
|
|
358
|
+
} else {
|
|
359
|
+
newUidArr = [...selectedUidArray, uid];
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
newUidArr = selected ? [] : [uid];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
setSelectedUidArray(newUidArr);
|
|
366
|
+
|
|
367
|
+
const treeNode = itemMap.get(uid);
|
|
368
|
+
if (treeNode && onSelect) {
|
|
369
|
+
const externalKeys = uidsToKeys(newUidArr, itemMap);
|
|
370
|
+
const nodes: T[] = [];
|
|
371
|
+
for (const u of newUidArr) {
|
|
372
|
+
const n = itemMap.get(u)?.original;
|
|
373
|
+
if (n) nodes.push(n);
|
|
374
|
+
}
|
|
375
|
+
onSelect(externalKeys, {
|
|
376
|
+
selected: !selected,
|
|
377
|
+
selectedNodes: nodes,
|
|
378
|
+
node: treeNode.original,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
});
|
|
212
382
|
|
|
213
|
-
|
|
383
|
+
const handleExpand = useEvent((uid: string) => {
|
|
384
|
+
const expanded = expandedUidSet.has(uid);
|
|
385
|
+
const newUidArr = expanded
|
|
386
|
+
? expandedUidArray.filter((u) => u !== uid)
|
|
387
|
+
: [...expandedUidArray, uid];
|
|
388
|
+
|
|
389
|
+
setExpandedUidArray(newUidArr);
|
|
390
|
+
|
|
391
|
+
if (onExpand) {
|
|
392
|
+
const externalKeys = uidsToKeys(newUidArr, itemMap);
|
|
393
|
+
const treeNode = itemMap.get(uid);
|
|
394
|
+
const nodes: T[] = [];
|
|
395
|
+
for (const u of newUidArr) {
|
|
396
|
+
const n = itemMap.get(u)?.original;
|
|
397
|
+
if (n) nodes.push(n);
|
|
398
|
+
}
|
|
399
|
+
onExpand(externalKeys, {
|
|
214
400
|
expanded: !expanded,
|
|
215
401
|
expandedNodes: nodes,
|
|
216
|
-
node:
|
|
402
|
+
node: treeNode!.original,
|
|
217
403
|
});
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
221
406
|
|
|
222
|
-
// ---
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const dragKeyRef = useRef<string | number | null>(null);
|
|
226
|
-
const [draggingKey, setDraggingKey] = useState<string | number | null>(null);
|
|
407
|
+
// --- Drag & Drop ---
|
|
408
|
+
const dragUidRef = useRef<string | null>(null);
|
|
409
|
+
const [draggingUid, setDraggingUid] = useState<string | null>(null);
|
|
227
410
|
const [dropState, setDropState] = useState<DropState>(null);
|
|
228
411
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
(key: string | number, e: React.DragEvent) => {
|
|
252
|
-
const node = itemMap.get(key as string);
|
|
253
|
-
if (node?.disabled) {
|
|
254
|
-
e.preventDefault();
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
dragKeyRef.current = key;
|
|
258
|
-
setDraggingKey(key);
|
|
259
|
-
if (e.dataTransfer) {
|
|
260
|
-
e.dataTransfer.effectAllowed = 'move';
|
|
261
|
-
try {
|
|
262
|
-
e.dataTransfer.setData('text/plain', String(key));
|
|
263
|
-
} catch {
|
|
264
|
-
/* some browsers throw on setData in certain modes */
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
},
|
|
268
|
-
[itemMap],
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
const handleDragOver = useCallback(
|
|
272
|
-
(key: string | number, e: React.DragEvent) => {
|
|
273
|
-
const dragKey = dragKeyRef.current;
|
|
274
|
-
if (dragKey === null) return;
|
|
275
|
-
if (isIllegalMove(dragKey, key)) return;
|
|
276
|
-
const dropNode = itemMap.get(key as string);
|
|
277
|
-
if (!dropNode || dropNode.disabled) return;
|
|
278
|
-
|
|
279
|
-
const position = resolvePosition(
|
|
280
|
-
(e.currentTarget as HTMLElement).getBoundingClientRect(),
|
|
281
|
-
e.clientY,
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
const dragNode = itemMap.get(dragKey as string);
|
|
285
|
-
if (
|
|
286
|
-
allowDrop &&
|
|
287
|
-
dragNode &&
|
|
288
|
-
allowDrop({
|
|
289
|
-
dragNode: dragNode.original,
|
|
290
|
-
dropNode: dropNode.original,
|
|
291
|
-
position,
|
|
292
|
-
}) === false
|
|
293
|
-
) {
|
|
294
|
-
return; // not calling preventDefault shows the "no-drop" cursor
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
e.preventDefault();
|
|
298
|
-
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
|
299
|
-
setDropState((prev) =>
|
|
300
|
-
prev && prev.key === key && prev.position === position
|
|
301
|
-
? prev
|
|
302
|
-
: { key, position },
|
|
303
|
-
);
|
|
304
|
-
},
|
|
305
|
-
[itemMap, isIllegalMove, resolvePosition, allowDrop],
|
|
306
|
-
);
|
|
307
|
-
|
|
308
|
-
const handleDragLeave = useCallback(
|
|
309
|
-
// Intentionally a no-op: dragleave fires spuriously when crossing child
|
|
310
|
-
// elements. Drop state is cleared in handleDragEnd at the Tree root.
|
|
311
|
-
() => {},
|
|
312
|
-
[],
|
|
313
|
-
);
|
|
314
|
-
|
|
315
|
-
const handleDrop = useCallback(
|
|
316
|
-
(key: string | number, e: React.DragEvent) => {
|
|
412
|
+
const resolvePosition = (rect: DOMRect, clientY: number): DragPosition => {
|
|
413
|
+
const offset = (clientY - rect.top) / rect.height;
|
|
414
|
+
if (offset < 0.25) return 'before';
|
|
415
|
+
if (offset > 0.75) return 'after';
|
|
416
|
+
return 'inside';
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const isIllegalMove = (dragUid: string, dropUid: string): boolean => {
|
|
420
|
+
if (dragUid === dropUid) return true;
|
|
421
|
+
const dropNode = itemMap.get(dropUid);
|
|
422
|
+
if (!dropNode) return true;
|
|
423
|
+
let current: TreeNode<T> | undefined = dropNode;
|
|
424
|
+
while (current) {
|
|
425
|
+
if (current.pUid === dragUid) return true;
|
|
426
|
+
current = current.pUid ? itemMap.get(current.pUid) : undefined;
|
|
427
|
+
}
|
|
428
|
+
return false;
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const handleDragStart = useEvent((uid: string, e: React.DragEvent) => {
|
|
432
|
+
const node = itemMap.get(uid);
|
|
433
|
+
if (node?.disabled) {
|
|
317
434
|
e.preventDefault();
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const dropNode = itemMap.get(key as string);
|
|
329
|
-
if (!dragNode || !dropNode) return;
|
|
330
|
-
if (
|
|
331
|
-
allowDrop &&
|
|
332
|
-
allowDrop({
|
|
333
|
-
dragNode: dragNode.original,
|
|
334
|
-
dropNode: dropNode.original,
|
|
335
|
-
position,
|
|
336
|
-
}) === false
|
|
337
|
-
) {
|
|
338
|
-
return;
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
dragUidRef.current = uid;
|
|
438
|
+
setDraggingUid(uid);
|
|
439
|
+
if (e.dataTransfer) {
|
|
440
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
441
|
+
try {
|
|
442
|
+
e.dataTransfer.setData('text/plain', uid);
|
|
443
|
+
} catch {
|
|
444
|
+
/* some browsers throw */
|
|
339
445
|
}
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const handleDragOver = useEvent((uid: string, e: React.DragEvent) => {
|
|
450
|
+
const dragUid = dragUidRef.current;
|
|
451
|
+
if (dragUid === null) return;
|
|
452
|
+
if (isIllegalMove(dragUid, uid)) return;
|
|
453
|
+
const dropNode = itemMap.get(uid);
|
|
454
|
+
if (!dropNode || dropNode.disabled) return;
|
|
455
|
+
|
|
456
|
+
const position = resolvePosition(
|
|
457
|
+
(e.currentTarget as HTMLElement).getBoundingClientRect(),
|
|
458
|
+
e.clientY,
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
const dragNode = itemMap.get(dragUid);
|
|
462
|
+
if (
|
|
463
|
+
allowDrop &&
|
|
464
|
+
dragNode &&
|
|
465
|
+
allowDrop({
|
|
466
|
+
dragNode: dragNode.original,
|
|
467
|
+
dropNode: dropNode.original,
|
|
468
|
+
position,
|
|
469
|
+
}) === false
|
|
470
|
+
) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
e.preventDefault();
|
|
475
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
|
476
|
+
setDropState((prev) =>
|
|
477
|
+
prev && prev.uid === uid && prev.position === position
|
|
478
|
+
? prev
|
|
479
|
+
: { uid, position },
|
|
480
|
+
);
|
|
481
|
+
});
|
|
340
482
|
|
|
341
|
-
|
|
483
|
+
const handleDragLeave = useEvent(() => {});
|
|
484
|
+
|
|
485
|
+
const handleDrop = useEvent((uid: string, e: React.DragEvent) => {
|
|
486
|
+
e.preventDefault();
|
|
487
|
+
const dragUid = dragUidRef.current;
|
|
488
|
+
if (dragUid === null) return;
|
|
489
|
+
|
|
490
|
+
const position = resolvePosition(
|
|
491
|
+
(e.currentTarget as HTMLElement).getBoundingClientRect(),
|
|
492
|
+
e.clientY,
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
if (isIllegalMove(dragUid, uid)) return;
|
|
496
|
+
const dragNode = itemMap.get(dragUid);
|
|
497
|
+
const dropNode = itemMap.get(uid);
|
|
498
|
+
if (!dragNode || !dropNode) return;
|
|
499
|
+
if (
|
|
500
|
+
allowDrop &&
|
|
501
|
+
allowDrop({
|
|
342
502
|
dragNode: dragNode.original,
|
|
343
503
|
dropNode: dropNode.original,
|
|
344
|
-
dragKey,
|
|
345
|
-
dropKey: key,
|
|
346
504
|
position,
|
|
347
|
-
|
|
505
|
+
}) === false
|
|
506
|
+
) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
onDrop?.({
|
|
511
|
+
dragNode: dragNode.original,
|
|
512
|
+
dropNode: dropNode.original,
|
|
513
|
+
dragKey: dragNode.key,
|
|
514
|
+
dropKey: dropNode.key,
|
|
515
|
+
dragPath: [...dragNode.path, dragNode.key],
|
|
516
|
+
dropPath: [...dropNode.path, dropNode.key],
|
|
517
|
+
position,
|
|
518
|
+
dropToGap: position !== 'inside',
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// Auto-expand the target after a drop-inside
|
|
522
|
+
if (position === 'inside') {
|
|
523
|
+
setExpandedUidArray((prev) => {
|
|
524
|
+
if (prev.includes(uid)) return prev;
|
|
525
|
+
return [...prev, uid];
|
|
348
526
|
});
|
|
527
|
+
}
|
|
349
528
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
prev.includes(key) ? prev : [...prev, key],
|
|
355
|
-
);
|
|
356
|
-
}
|
|
529
|
+
dragUidRef.current = null;
|
|
530
|
+
setDraggingUid(null);
|
|
531
|
+
setDropState(null);
|
|
532
|
+
});
|
|
357
533
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
534
|
+
const handleDragEnd = useEvent(() => {
|
|
535
|
+
dragUidRef.current = null;
|
|
536
|
+
setDraggingUid(null);
|
|
537
|
+
setDropState(null);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// ==================== Context Value (memoized) ====================
|
|
541
|
+
const contextValue = useMemo(
|
|
542
|
+
() => ({
|
|
543
|
+
prefixCls,
|
|
544
|
+
checkable,
|
|
545
|
+
showIcon,
|
|
546
|
+
icon,
|
|
547
|
+
showLine,
|
|
548
|
+
switcherIcon,
|
|
549
|
+
selectedUids: selectedUidSet,
|
|
550
|
+
checkedUids: checkedUidSet,
|
|
551
|
+
indeterminateUids: indeterminateUidSet,
|
|
552
|
+
expandedUids: expandedUidSet,
|
|
553
|
+
draggable,
|
|
554
|
+
draggingUid,
|
|
555
|
+
dropState,
|
|
556
|
+
onCheck: handleCheck,
|
|
557
|
+
onSelect: handleSelect,
|
|
558
|
+
onExpand: handleExpand,
|
|
559
|
+
onNodeDragStart: handleDragStart,
|
|
560
|
+
onNodeDragOver: handleDragOver,
|
|
561
|
+
onNodeDragLeave: handleDragLeave,
|
|
562
|
+
onNodeDrop: handleDrop,
|
|
563
|
+
}),
|
|
564
|
+
[
|
|
565
|
+
prefixCls,
|
|
566
|
+
checkable,
|
|
567
|
+
showIcon,
|
|
568
|
+
icon,
|
|
569
|
+
showLine,
|
|
570
|
+
switcherIcon,
|
|
571
|
+
selectedUidSet,
|
|
572
|
+
checkedUidSet,
|
|
573
|
+
indeterminateUidSet,
|
|
574
|
+
expandedUidSet,
|
|
575
|
+
draggable,
|
|
576
|
+
draggingUid,
|
|
577
|
+
dropState,
|
|
578
|
+
// handlers are stable (useEvent), but listed for correctness
|
|
579
|
+
handleCheck,
|
|
580
|
+
handleSelect,
|
|
581
|
+
handleExpand,
|
|
582
|
+
handleDragStart,
|
|
583
|
+
handleDragOver,
|
|
584
|
+
handleDragLeave,
|
|
585
|
+
handleDrop,
|
|
586
|
+
],
|
|
363
587
|
);
|
|
364
588
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
589
|
+
// ==================== Virtual Scroll ====================
|
|
590
|
+
const useVirtual = virtual && height !== null && height !== undefined;
|
|
591
|
+
|
|
592
|
+
const { start, end, offsetY, totalHeight, viewportRef, onScroll } =
|
|
593
|
+
useVirtualList({
|
|
594
|
+
itemCount: useVirtual ? visibleItems.length : 0,
|
|
595
|
+
itemHeight,
|
|
596
|
+
overscan: 4,
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// ==================== Render ====================
|
|
600
|
+
if (useVirtual) {
|
|
601
|
+
const slicedItems = visibleItems.slice(start, end);
|
|
602
|
+
return (
|
|
603
|
+
<div
|
|
604
|
+
className={clsx(prefixCls, className)}
|
|
605
|
+
style={style}
|
|
606
|
+
onDragEnd={handleDragEnd}
|
|
607
|
+
>
|
|
608
|
+
<TreeContext.Provider value={contextValue}>
|
|
609
|
+
<div
|
|
610
|
+
ref={viewportRef}
|
|
611
|
+
onScroll={onScroll}
|
|
612
|
+
style={{ height, overflowY: 'auto' }}
|
|
613
|
+
>
|
|
614
|
+
<div style={{ height: totalHeight, position: 'relative' }}>
|
|
615
|
+
{slicedItems.map((node, i) => (
|
|
616
|
+
<TreeNodeComp
|
|
617
|
+
node={node}
|
|
618
|
+
key={node.uid}
|
|
619
|
+
style={{
|
|
620
|
+
position: 'absolute',
|
|
621
|
+
top: 0,
|
|
622
|
+
left: 0,
|
|
623
|
+
width: '100%',
|
|
624
|
+
height: itemHeight,
|
|
625
|
+
transform: `translateY(${offsetY + i * itemHeight}px)`,
|
|
626
|
+
}}
|
|
627
|
+
/>
|
|
628
|
+
))}
|
|
629
|
+
</div>
|
|
630
|
+
</div>
|
|
631
|
+
</TreeContext.Provider>
|
|
632
|
+
</div>
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// ==================== Render (non-virtual) ====================
|
|
637
|
+
// Group consecutive children of the same parent into segments.
|
|
638
|
+
// Each group animates as a single unit (one height measurement) rather than
|
|
639
|
+
// each node individually, drastically reducing layout thrashing.
|
|
640
|
+
type Segment =
|
|
641
|
+
| { type: 'node'; node: TreeNode<T> }
|
|
642
|
+
| { type: 'group'; pUid: string; nodes: TreeNode<T>[] };
|
|
643
|
+
|
|
644
|
+
const segments: Segment[] = [];
|
|
645
|
+
let currentGroup: { pUid: string; nodes: TreeNode<T>[] } | null = null;
|
|
646
|
+
|
|
647
|
+
for (const node of visibleItems) {
|
|
648
|
+
if (node.depth === 0) {
|
|
649
|
+
// Root nodes are always visible, render directly without animation
|
|
650
|
+
if (currentGroup) {
|
|
651
|
+
segments.push({ type: 'group', ...currentGroup });
|
|
652
|
+
currentGroup = null;
|
|
653
|
+
}
|
|
654
|
+
segments.push({ type: 'node', node });
|
|
655
|
+
} else {
|
|
656
|
+
// Non-root: group consecutive children under the same parent
|
|
657
|
+
if (currentGroup && currentGroup.pUid === node.pUid) {
|
|
658
|
+
currentGroup.nodes.push(node);
|
|
659
|
+
} else {
|
|
660
|
+
if (currentGroup) {
|
|
661
|
+
segments.push({ type: 'group', ...currentGroup });
|
|
662
|
+
}
|
|
663
|
+
currentGroup = { pUid: node.pUid!, nodes: [node] };
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (currentGroup) {
|
|
668
|
+
segments.push({ type: 'group', ...currentGroup });
|
|
669
|
+
}
|
|
370
670
|
|
|
371
671
|
return (
|
|
372
672
|
<div
|
|
@@ -374,32 +674,31 @@ const Tree = <T extends Record<string, unknown> = any>(
|
|
|
374
674
|
style={style}
|
|
375
675
|
onDragEnd={handleDragEnd}
|
|
376
676
|
>
|
|
377
|
-
<TreeContext.Provider
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
))}
|
|
677
|
+
<TreeContext.Provider value={contextValue}>
|
|
678
|
+
<AnimatePresence initial={false}>
|
|
679
|
+
{segments.map((segment) => {
|
|
680
|
+
if (segment.type === 'node') {
|
|
681
|
+
return <TreeNodeComp node={segment.node} key={segment.node.uid} />;
|
|
682
|
+
}
|
|
683
|
+
// Keep the wrapper mounted throughout native drag operations.
|
|
684
|
+
// Replacing this tree after dragStart removes the browser's drag
|
|
685
|
+
// source and cancels the drag before dragOver/drop can fire.
|
|
686
|
+
return (
|
|
687
|
+
<motion.div
|
|
688
|
+
key={segment.pUid + '\x01children'}
|
|
689
|
+
initial={{ height: 0, opacity: 0 }}
|
|
690
|
+
animate={{ height: 'auto', opacity: 1 }}
|
|
691
|
+
exit={{ height: 0, opacity: 0 }}
|
|
692
|
+
transition={{ duration: 0.15 }}
|
|
693
|
+
style={{ overflow: 'hidden' }}
|
|
694
|
+
>
|
|
695
|
+
{segment.nodes.map((node) => (
|
|
696
|
+
<TreeNodeComp node={node} key={node.uid} />
|
|
697
|
+
))}
|
|
698
|
+
</motion.div>
|
|
699
|
+
);
|
|
700
|
+
})}
|
|
701
|
+
</AnimatePresence>
|
|
403
702
|
</TreeContext.Provider>
|
|
404
703
|
</div>
|
|
405
704
|
);
|