react-native-tree-multi-select 2.0.11 → 3.0.0-beta.1
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 +151 -0
- package/lib/module/TreeView.js +32 -2
- package/lib/module/TreeView.js.map +1 -1
- package/lib/module/components/DragOverlay.js +101 -0
- package/lib/module/components/DragOverlay.js.map +1 -0
- package/lib/module/components/DropIndicator.js +79 -0
- package/lib/module/components/DropIndicator.js.map +1 -0
- package/lib/module/components/NodeList.js +258 -32
- package/lib/module/components/NodeList.js.map +1 -1
- package/lib/module/helpers/index.js +1 -0
- package/lib/module/helpers/index.js.map +1 -1
- package/lib/module/helpers/moveTreeNode.helper.js +96 -0
- package/lib/module/helpers/moveTreeNode.helper.js.map +1 -0
- package/lib/module/helpers/toggleCheckbox.helper.js +88 -0
- package/lib/module/helpers/toggleCheckbox.helper.js.map +1 -1
- package/lib/module/hooks/useDragDrop.js +511 -0
- package/lib/module/hooks/useDragDrop.js.map +1 -0
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/store/treeView.store.js +19 -1
- package/lib/module/store/treeView.store.js.map +1 -1
- package/lib/module/types/dragDrop.types.js +4 -0
- package/lib/module/types/dragDrop.types.js.map +1 -0
- package/lib/typescript/src/TreeView.d.ts.map +1 -1
- package/lib/typescript/src/components/DragOverlay.d.ts +12 -0
- package/lib/typescript/src/components/DragOverlay.d.ts.map +1 -0
- package/lib/typescript/src/components/DropIndicator.d.ts +13 -0
- package/lib/typescript/src/components/DropIndicator.d.ts.map +1 -0
- package/lib/typescript/src/components/NodeList.d.ts.map +1 -1
- package/lib/typescript/src/helpers/index.d.ts +1 -0
- package/lib/typescript/src/helpers/index.d.ts.map +1 -1
- package/lib/typescript/src/helpers/moveTreeNode.helper.d.ts +13 -0
- package/lib/typescript/src/helpers/moveTreeNode.helper.d.ts.map +1 -0
- package/lib/typescript/src/helpers/toggleCheckbox.helper.d.ts +6 -0
- package/lib/typescript/src/helpers/toggleCheckbox.helper.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useDragDrop.d.ts +35 -0
- package/lib/typescript/src/hooks/useDragDrop.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/store/treeView.store.d.ts +8 -0
- package/lib/typescript/src/store/treeView.store.d.ts.map +1 -1
- package/lib/typescript/src/types/dragDrop.types.d.ts +21 -0
- package/lib/typescript/src/types/dragDrop.types.d.ts.map +1 -0
- package/lib/typescript/src/types/treeView.types.d.ts +90 -0
- package/lib/typescript/src/types/treeView.types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/TreeView.tsx +34 -0
- package/src/components/DragOverlay.tsx +112 -0
- package/src/components/DropIndicator.tsx +95 -0
- package/src/components/NodeList.tsx +302 -30
- package/src/helpers/index.ts +2 -1
- package/src/helpers/moveTreeNode.helper.ts +105 -0
- package/src/helpers/toggleCheckbox.helper.ts +96 -0
- package/src/hooks/useDragDrop.ts +643 -0
- package/src/index.tsx +19 -2
- package/src/store/treeView.store.ts +32 -0
- package/src/types/dragDrop.types.ts +23 -0
- package/src/types/treeView.types.ts +106 -0
|
@@ -2,16 +2,19 @@ import React from "react";
|
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
StyleSheet,
|
|
5
|
-
|
|
6
5
|
TouchableOpacity,
|
|
6
|
+
type NativeSyntheticEvent,
|
|
7
|
+
type NativeScrollEvent,
|
|
7
8
|
} from "react-native";
|
|
8
9
|
import { FlashList } from "@shopify/flash-list";
|
|
9
10
|
|
|
10
11
|
import type {
|
|
11
12
|
CheckboxValueType,
|
|
12
13
|
__FlattenedTreeNode__,
|
|
14
|
+
DropIndicatorStyleProps,
|
|
13
15
|
NodeListProps,
|
|
14
16
|
NodeProps,
|
|
17
|
+
TreeNode,
|
|
15
18
|
} from "../types/treeView.types";
|
|
16
19
|
|
|
17
20
|
import { useTreeViewStore } from "../store/treeView.store";
|
|
@@ -24,10 +27,13 @@ import {
|
|
|
24
27
|
} from "../helpers";
|
|
25
28
|
import { CheckboxView } from "./CheckboxView";
|
|
26
29
|
import { CustomExpandCollapseIcon } from "./CustomExpandCollapseIcon";
|
|
30
|
+
import { DragOverlay } from "./DragOverlay";
|
|
31
|
+
import type { DropPosition } from "../types/dragDrop.types";
|
|
27
32
|
import { defaultIndentationMultiplier } from "../constants/treeView.constants";
|
|
28
33
|
import { useShallow } from "zustand/react/shallow";
|
|
29
34
|
import { typedMemo } from "../utils/typedMemo";
|
|
30
35
|
import { ScrollToNodeHandler } from "../handlers/ScrollToNodeHandler";
|
|
36
|
+
import { useDragDrop } from "../hooks/useDragDrop";
|
|
31
37
|
|
|
32
38
|
const NodeList = typedMemo(_NodeList);
|
|
33
39
|
export default NodeList;
|
|
@@ -46,7 +52,16 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
46
52
|
CheckboxComponent,
|
|
47
53
|
ExpandCollapseIconComponent,
|
|
48
54
|
ExpandCollapseTouchableComponent,
|
|
49
|
-
CustomNodeRowComponent
|
|
55
|
+
CustomNodeRowComponent,
|
|
56
|
+
|
|
57
|
+
dragEnabled,
|
|
58
|
+
onDragEnd,
|
|
59
|
+
longPressDuration = 400,
|
|
60
|
+
autoScrollThreshold = 60,
|
|
61
|
+
autoScrollSpeed = 1.0,
|
|
62
|
+
dragOverlayOffset = -4,
|
|
63
|
+
autoExpandDelay = 800,
|
|
64
|
+
dragDropCustomizations,
|
|
50
65
|
} = props;
|
|
51
66
|
|
|
52
67
|
const {
|
|
@@ -66,6 +81,15 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
66
81
|
));
|
|
67
82
|
|
|
68
83
|
const flashListRef = React.useRef<FlashList<__FlattenedTreeNode__<ID>> | null>(null);
|
|
84
|
+
const containerRef = React.useRef<View>(null);
|
|
85
|
+
const internalDataRef = React.useRef<TreeNode<ID>[] | null>(null);
|
|
86
|
+
const measuredItemHeightRef = React.useRef(0);
|
|
87
|
+
|
|
88
|
+
const handleItemLayout = React.useCallback((height: number) => {
|
|
89
|
+
if (measuredItemHeightRef.current === 0 && height > 0) {
|
|
90
|
+
measuredItemHeightRef.current = height;
|
|
91
|
+
}
|
|
92
|
+
}, []);
|
|
69
93
|
|
|
70
94
|
const [initialScrollIndex, setInitialScrollIndex] = React.useState<number>(-1);
|
|
71
95
|
|
|
@@ -90,8 +114,46 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
90
114
|
updateInnerMostChildrenIds(updatedInnerMostChildrenIds);
|
|
91
115
|
}, [filteredTree, updateInnerMostChildrenIds]);
|
|
92
116
|
|
|
117
|
+
// --- Drag and drop ---
|
|
118
|
+
const {
|
|
119
|
+
panResponder,
|
|
120
|
+
overlayY,
|
|
121
|
+
isDragging,
|
|
122
|
+
draggedNode,
|
|
123
|
+
handleNodeTouchStart,
|
|
124
|
+
cancelLongPressTimer,
|
|
125
|
+
scrollOffsetRef,
|
|
126
|
+
} = useDragDrop<ID>({
|
|
127
|
+
storeId,
|
|
128
|
+
flattenedNodes: flattenedFilteredNodes,
|
|
129
|
+
flashListRef,
|
|
130
|
+
containerRef,
|
|
131
|
+
dragEnabled: dragEnabled ?? false,
|
|
132
|
+
onDragEnd,
|
|
133
|
+
longPressDuration,
|
|
134
|
+
autoScrollThreshold,
|
|
135
|
+
autoScrollSpeed,
|
|
136
|
+
internalDataRef,
|
|
137
|
+
measuredItemHeightRef,
|
|
138
|
+
dragOverlayOffset,
|
|
139
|
+
autoExpandDelay,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Combined onScroll handler
|
|
143
|
+
const handleScroll = React.useCallback((
|
|
144
|
+
event: NativeSyntheticEvent<NativeScrollEvent>
|
|
145
|
+
) => {
|
|
146
|
+
scrollOffsetRef.current = event.nativeEvent.contentOffset.y;
|
|
147
|
+
// Cancel long press timer if user is scrolling
|
|
148
|
+
cancelLongPressTimer();
|
|
149
|
+
// Forward to user's onScroll
|
|
150
|
+
treeFlashListProps?.onScroll?.(event as any);
|
|
151
|
+
}, [scrollOffsetRef, cancelLongPressTimer, treeFlashListProps]);
|
|
152
|
+
|
|
153
|
+
const effectiveIndentationMultiplier = indentationMultiplier ?? defaultIndentationMultiplier;
|
|
154
|
+
|
|
93
155
|
const nodeRenderer = React.useCallback((
|
|
94
|
-
{ item }: { item: __FlattenedTreeNode__<ID>; }
|
|
156
|
+
{ item, index }: { item: __FlattenedTreeNode__<ID>; index: number; }
|
|
95
157
|
) => {
|
|
96
158
|
return (
|
|
97
159
|
<Node<ID>
|
|
@@ -107,6 +169,14 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
107
169
|
ExpandCollapseIconComponent={ExpandCollapseIconComponent}
|
|
108
170
|
ExpandCollapseTouchableComponent={ExpandCollapseTouchableComponent}
|
|
109
171
|
CustomNodeRowComponent={CustomNodeRowComponent}
|
|
172
|
+
|
|
173
|
+
nodeIndex={index}
|
|
174
|
+
dragEnabled={dragEnabled}
|
|
175
|
+
isDragging={isDragging}
|
|
176
|
+
onNodeTouchStart={dragEnabled ? handleNodeTouchStart : undefined}
|
|
177
|
+
onNodeTouchEnd={dragEnabled ? cancelLongPressTimer : undefined}
|
|
178
|
+
onItemLayout={dragEnabled ? handleItemLayout : undefined}
|
|
179
|
+
dragDropCustomizations={dragDropCustomizations}
|
|
110
180
|
/>
|
|
111
181
|
);
|
|
112
182
|
}, [
|
|
@@ -116,9 +186,37 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
116
186
|
ExpandCollapseTouchableComponent,
|
|
117
187
|
CustomNodeRowComponent,
|
|
118
188
|
checkBoxViewStyleProps,
|
|
119
|
-
indentationMultiplier
|
|
189
|
+
indentationMultiplier,
|
|
190
|
+
dragEnabled,
|
|
191
|
+
isDragging,
|
|
192
|
+
handleNodeTouchStart,
|
|
193
|
+
dragDropCustomizations,
|
|
194
|
+
cancelLongPressTimer,
|
|
195
|
+
handleItemLayout,
|
|
120
196
|
]);
|
|
121
197
|
|
|
198
|
+
// Extract FlashList props but exclude onScroll (we provide our own combined handler)
|
|
199
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
200
|
+
const { onScroll: _userOnScroll, ...restFlashListProps } = treeFlashListProps ?? {};
|
|
201
|
+
|
|
202
|
+
const flashListElement = (
|
|
203
|
+
<FlashList
|
|
204
|
+
ref={flashListRef}
|
|
205
|
+
estimatedItemSize={36}
|
|
206
|
+
initialScrollIndex={initialScrollIndex}
|
|
207
|
+
removeClippedSubviews={true}
|
|
208
|
+
keyboardShouldPersistTaps="handled"
|
|
209
|
+
drawDistance={50}
|
|
210
|
+
ListHeaderComponent={<HeaderFooterView />}
|
|
211
|
+
ListFooterComponent={<HeaderFooterView />}
|
|
212
|
+
{...restFlashListProps}
|
|
213
|
+
onScroll={handleScroll}
|
|
214
|
+
scrollEnabled={isDragging ? false : (restFlashListProps?.scrollEnabled ?? true)}
|
|
215
|
+
data={flattenedFilteredNodes}
|
|
216
|
+
renderItem={nodeRenderer}
|
|
217
|
+
/>
|
|
218
|
+
);
|
|
219
|
+
|
|
122
220
|
return (
|
|
123
221
|
<>
|
|
124
222
|
<ScrollToNodeHandler
|
|
@@ -129,19 +227,30 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
129
227
|
setInitialScrollIndex={setInitialScrollIndex}
|
|
130
228
|
initialScrollNodeID={initialScrollNodeID} />
|
|
131
229
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
230
|
+
{dragEnabled ? (
|
|
231
|
+
<View
|
|
232
|
+
ref={containerRef}
|
|
233
|
+
style={styles.dragContainer}
|
|
234
|
+
{...panResponder.panHandlers}
|
|
235
|
+
>
|
|
236
|
+
{flashListElement}
|
|
237
|
+
{isDragging && draggedNode && (
|
|
238
|
+
<DragOverlay<ID>
|
|
239
|
+
overlayY={overlayY}
|
|
240
|
+
node={draggedNode}
|
|
241
|
+
level={draggedNode.level ?? 0}
|
|
242
|
+
indentationMultiplier={effectiveIndentationMultiplier}
|
|
243
|
+
CheckboxComponent={CheckboxComponent}
|
|
244
|
+
ExpandCollapseIconComponent={ExpandCollapseIconComponent}
|
|
245
|
+
CustomNodeRowComponent={CustomNodeRowComponent}
|
|
246
|
+
checkBoxViewStyleProps={checkBoxViewStyleProps}
|
|
247
|
+
dragDropCustomizations={dragDropCustomizations}
|
|
248
|
+
/>
|
|
249
|
+
)}
|
|
250
|
+
</View>
|
|
251
|
+
) : (
|
|
252
|
+
flashListElement
|
|
253
|
+
)}
|
|
145
254
|
</>
|
|
146
255
|
);
|
|
147
256
|
};
|
|
@@ -179,19 +288,35 @@ function _Node<ID>(props: NodeProps<ID>) {
|
|
|
179
288
|
ExpandCollapseIconComponent = CustomExpandCollapseIcon,
|
|
180
289
|
CheckboxComponent = CheckboxView,
|
|
181
290
|
ExpandCollapseTouchableComponent = TouchableOpacity,
|
|
182
|
-
CustomNodeRowComponent
|
|
291
|
+
CustomNodeRowComponent,
|
|
292
|
+
|
|
293
|
+
nodeIndex = 0,
|
|
294
|
+
dragEnabled,
|
|
295
|
+
isDragging: isDraggingGlobal,
|
|
296
|
+
onNodeTouchStart,
|
|
297
|
+
onNodeTouchEnd,
|
|
298
|
+
onItemLayout,
|
|
299
|
+
dragDropCustomizations,
|
|
183
300
|
} = props;
|
|
184
301
|
|
|
185
302
|
const {
|
|
186
303
|
isExpanded,
|
|
187
304
|
value,
|
|
305
|
+
isBeingDragged,
|
|
306
|
+
isDragInvalid,
|
|
307
|
+
isDropTarget,
|
|
308
|
+
nodeDropPosition,
|
|
188
309
|
} = useTreeViewStore<ID>(storeId)(useShallow(
|
|
189
310
|
state => ({
|
|
190
311
|
isExpanded: state.expanded.has(node.id),
|
|
191
312
|
value: getValue(
|
|
192
|
-
state.checked.has(node.id),
|
|
193
|
-
state.indeterminate.has(node.id)
|
|
313
|
+
state.checked.has(node.id),
|
|
314
|
+
state.indeterminate.has(node.id)
|
|
194
315
|
),
|
|
316
|
+
isBeingDragged: state.draggedNodeId === node.id,
|
|
317
|
+
isDragInvalid: state.invalidDragTargetIds.has(node.id),
|
|
318
|
+
isDropTarget: state.dropTargetNodeId === node.id,
|
|
319
|
+
nodeDropPosition: state.dropTargetNodeId === node.id ? state.dropPosition : null,
|
|
195
320
|
})
|
|
196
321
|
));
|
|
197
322
|
|
|
@@ -203,14 +328,51 @@ function _Node<ID>(props: NodeProps<ID>) {
|
|
|
203
328
|
toggleCheckboxes(storeId, [node.id]);
|
|
204
329
|
}, [storeId, node.id]);
|
|
205
330
|
|
|
331
|
+
const handleTouchStart = React.useCallback((e: any) => {
|
|
332
|
+
if (!onNodeTouchStart) return;
|
|
333
|
+
const { pageY, locationY } = e.nativeEvent;
|
|
334
|
+
onNodeTouchStart(node.id, pageY, locationY, nodeIndex);
|
|
335
|
+
}, [node.id, nodeIndex, onNodeTouchStart]);
|
|
336
|
+
|
|
337
|
+
const handleTouchEnd = React.useCallback(() => {
|
|
338
|
+
onNodeTouchEnd?.();
|
|
339
|
+
}, [onNodeTouchEnd]);
|
|
340
|
+
|
|
341
|
+
// Determine opacity for drag state
|
|
342
|
+
const dragOpacity = dragDropCustomizations?.draggedNodeOpacity ?? 0.3;
|
|
343
|
+
const nodeOpacity = (isDraggingGlobal && (isBeingDragged || isDragInvalid))
|
|
344
|
+
? dragOpacity
|
|
345
|
+
: 1.0;
|
|
346
|
+
|
|
347
|
+
const handleLayout = React.useCallback((e: any) => {
|
|
348
|
+
onItemLayout?.(e.nativeEvent.layout.height);
|
|
349
|
+
}, [onItemLayout]);
|
|
350
|
+
|
|
351
|
+
const touchHandlers = dragEnabled ? {
|
|
352
|
+
onTouchStart: handleTouchStart,
|
|
353
|
+
onTouchEnd: handleTouchEnd,
|
|
354
|
+
onTouchCancel: handleTouchEnd,
|
|
355
|
+
} : undefined;
|
|
356
|
+
|
|
357
|
+
const CustomDropIndicator = dragDropCustomizations?.CustomDropIndicatorComponent;
|
|
358
|
+
const dropIndicator = isDropTarget && nodeDropPosition ? (
|
|
359
|
+
CustomDropIndicator
|
|
360
|
+
? <CustomDropIndicator position={nodeDropPosition} />
|
|
361
|
+
: <NodeDropIndicator position={nodeDropPosition} styleProps={dragDropCustomizations?.dropIndicatorStyleProps} />
|
|
362
|
+
) : null;
|
|
363
|
+
|
|
206
364
|
if (!CustomNodeRowComponent) {
|
|
207
365
|
return (
|
|
208
366
|
<View
|
|
209
367
|
testID={`node_row_${node.id}`}
|
|
368
|
+
{...touchHandlers}
|
|
369
|
+
onLayout={onItemLayout ? handleLayout : undefined}
|
|
210
370
|
style={[
|
|
211
371
|
styles.nodeCheckboxAndArrowRow,
|
|
212
|
-
{ paddingStart: level * indentationMultiplier }
|
|
372
|
+
{ paddingStart: level * indentationMultiplier },
|
|
373
|
+
{ opacity: nodeOpacity },
|
|
213
374
|
]}>
|
|
375
|
+
{dropIndicator}
|
|
214
376
|
<CheckboxComponent
|
|
215
377
|
text={node.name}
|
|
216
378
|
onValueChange={_onCheck}
|
|
@@ -233,17 +395,84 @@ function _Node<ID>(props: NodeProps<ID>) {
|
|
|
233
395
|
}
|
|
234
396
|
else {
|
|
235
397
|
return (
|
|
236
|
-
<
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
398
|
+
<View
|
|
399
|
+
{...touchHandlers}
|
|
400
|
+
onLayout={onItemLayout ? handleLayout : undefined}
|
|
401
|
+
style={{ opacity: nodeOpacity }}
|
|
402
|
+
>
|
|
403
|
+
{dropIndicator}
|
|
404
|
+
<CustomNodeRowComponent
|
|
405
|
+
node={node}
|
|
406
|
+
level={level}
|
|
407
|
+
checkedValue={value}
|
|
408
|
+
isExpanded={isExpanded}
|
|
409
|
+
onCheck={_onCheck}
|
|
410
|
+
onExpand={_onToggleExpand}
|
|
411
|
+
isDragTarget={isDragInvalid}
|
|
412
|
+
isDragging={isDraggingGlobal}
|
|
413
|
+
isDraggedNode={isBeingDragged}
|
|
414
|
+
/>
|
|
415
|
+
</View>
|
|
243
416
|
);
|
|
244
417
|
}
|
|
245
418
|
};
|
|
246
419
|
|
|
420
|
+
function NodeDropIndicator({ position, styleProps }: {
|
|
421
|
+
position: DropPosition;
|
|
422
|
+
styleProps?: DropIndicatorStyleProps;
|
|
423
|
+
}) {
|
|
424
|
+
const lineColor = styleProps?.lineColor ?? "#0078FF";
|
|
425
|
+
const lineThickness = styleProps?.lineThickness ?? 3;
|
|
426
|
+
const circleSize = styleProps?.circleSize ?? 10;
|
|
427
|
+
const highlightColor = styleProps?.highlightColor ?? "rgba(0, 120, 255, 0.15)";
|
|
428
|
+
const highlightBorderColor = styleProps?.highlightBorderColor ?? "rgba(0, 120, 255, 0.5)";
|
|
429
|
+
|
|
430
|
+
if (position === "inside") {
|
|
431
|
+
return (
|
|
432
|
+
<View
|
|
433
|
+
pointerEvents="none"
|
|
434
|
+
style={[
|
|
435
|
+
styles.dropHighlight,
|
|
436
|
+
{
|
|
437
|
+
backgroundColor: highlightColor,
|
|
438
|
+
borderColor: highlightBorderColor,
|
|
439
|
+
},
|
|
440
|
+
]}
|
|
441
|
+
/>
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return (
|
|
446
|
+
<View
|
|
447
|
+
pointerEvents="none"
|
|
448
|
+
style={[
|
|
449
|
+
styles.dropLineContainer,
|
|
450
|
+
{ height: lineThickness },
|
|
451
|
+
position === "above" ? styles.dropLineTop : styles.dropLineBottom,
|
|
452
|
+
]}
|
|
453
|
+
>
|
|
454
|
+
<View style={[
|
|
455
|
+
styles.dropLineCircle,
|
|
456
|
+
{
|
|
457
|
+
width: circleSize,
|
|
458
|
+
height: circleSize,
|
|
459
|
+
borderRadius: circleSize / 2,
|
|
460
|
+
backgroundColor: lineColor,
|
|
461
|
+
marginLeft: -(circleSize / 2),
|
|
462
|
+
marginTop: -(circleSize / 2 - lineThickness / 2),
|
|
463
|
+
},
|
|
464
|
+
]} />
|
|
465
|
+
<View style={[
|
|
466
|
+
styles.dropLine,
|
|
467
|
+
{
|
|
468
|
+
height: lineThickness,
|
|
469
|
+
backgroundColor: lineColor,
|
|
470
|
+
},
|
|
471
|
+
]} />
|
|
472
|
+
</View>
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
247
476
|
const styles = StyleSheet.create({
|
|
248
477
|
defaultHeaderFooter: {
|
|
249
478
|
padding: 5
|
|
@@ -256,6 +485,49 @@ const styles = StyleSheet.create({
|
|
|
256
485
|
flexDirection: "row",
|
|
257
486
|
alignItems: "center",
|
|
258
487
|
minWidth: "100%"
|
|
259
|
-
}
|
|
488
|
+
},
|
|
489
|
+
dragContainer: {
|
|
490
|
+
flex: 1,
|
|
491
|
+
},
|
|
492
|
+
// Drop indicator styles (rendered by each node)
|
|
493
|
+
dropHighlight: {
|
|
494
|
+
position: "absolute",
|
|
495
|
+
top: 0,
|
|
496
|
+
bottom: 0,
|
|
497
|
+
left: 0,
|
|
498
|
+
right: 0,
|
|
499
|
+
backgroundColor: "rgba(0, 120, 255, 0.15)",
|
|
500
|
+
borderWidth: 2,
|
|
501
|
+
borderColor: "rgba(0, 120, 255, 0.5)",
|
|
502
|
+
borderRadius: 4,
|
|
503
|
+
zIndex: 10,
|
|
504
|
+
},
|
|
505
|
+
dropLineContainer: {
|
|
506
|
+
position: "absolute",
|
|
507
|
+
left: 0,
|
|
508
|
+
right: 0,
|
|
509
|
+
flexDirection: "row",
|
|
510
|
+
alignItems: "center",
|
|
511
|
+
height: 3,
|
|
512
|
+
zIndex: 10,
|
|
513
|
+
},
|
|
514
|
+
dropLineTop: {
|
|
515
|
+
top: 0,
|
|
516
|
+
},
|
|
517
|
+
dropLineBottom: {
|
|
518
|
+
bottom: 0,
|
|
519
|
+
},
|
|
520
|
+
dropLineCircle: {
|
|
521
|
+
width: 10,
|
|
522
|
+
height: 10,
|
|
523
|
+
borderRadius: 5,
|
|
524
|
+
backgroundColor: "#0078FF",
|
|
525
|
+
marginLeft: -5,
|
|
526
|
+
marginTop: -4,
|
|
527
|
+
},
|
|
528
|
+
dropLine: {
|
|
529
|
+
flex: 1,
|
|
530
|
+
height: 3,
|
|
531
|
+
backgroundColor: "#0078FF",
|
|
532
|
+
},
|
|
260
533
|
});
|
|
261
|
-
|
package/src/helpers/index.ts
CHANGED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { TreeNode } from "../types/treeView.types";
|
|
2
|
+
import type { DropPosition } from "../types/dragDrop.types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Move a node within a tree structure. Returns a new tree (no mutation).
|
|
6
|
+
*
|
|
7
|
+
* @param data - The current tree data
|
|
8
|
+
* @param draggedNodeId - The ID of the node to move
|
|
9
|
+
* @param targetNodeId - The ID of the target node
|
|
10
|
+
* @param position - Where to place relative to target: "above", "below", or "inside"
|
|
11
|
+
* @returns New tree data with the node moved, or the original data if the move is invalid
|
|
12
|
+
*/
|
|
13
|
+
export function moveTreeNode<ID>(
|
|
14
|
+
data: TreeNode<ID>[],
|
|
15
|
+
draggedNodeId: ID,
|
|
16
|
+
targetNodeId: ID,
|
|
17
|
+
position: DropPosition
|
|
18
|
+
): TreeNode<ID>[] {
|
|
19
|
+
if (draggedNodeId === targetNodeId) return data;
|
|
20
|
+
|
|
21
|
+
// Step 1: Deep clone the tree
|
|
22
|
+
const cloned = deepCloneTree(data);
|
|
23
|
+
|
|
24
|
+
// Step 2: Remove the dragged node
|
|
25
|
+
const removedNode = removeNodeById(cloned, draggedNodeId);
|
|
26
|
+
if (!removedNode) return data;
|
|
27
|
+
|
|
28
|
+
// Step 3: Insert at the new position
|
|
29
|
+
const inserted = insertNode(cloned, removedNode, targetNodeId, position);
|
|
30
|
+
if (!inserted) return data;
|
|
31
|
+
|
|
32
|
+
return cloned;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function deepCloneTree<ID>(nodes: TreeNode<ID>[]): TreeNode<ID>[] {
|
|
36
|
+
return nodes.map(node => ({
|
|
37
|
+
...node,
|
|
38
|
+
children: node.children ? deepCloneTree(node.children) : undefined,
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Remove a node by ID from the tree. Mutates the cloned tree in-place.
|
|
44
|
+
* Returns the removed node, or null if not found.
|
|
45
|
+
*/
|
|
46
|
+
function removeNodeById<ID>(
|
|
47
|
+
nodes: TreeNode<ID>[],
|
|
48
|
+
nodeId: ID,
|
|
49
|
+
): TreeNode<ID> | null {
|
|
50
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
51
|
+
if (nodes[i]!.id === nodeId) {
|
|
52
|
+
const [removed] = nodes.splice(i, 1);
|
|
53
|
+
return removed!;
|
|
54
|
+
}
|
|
55
|
+
const children = nodes[i]!.children;
|
|
56
|
+
if (children) {
|
|
57
|
+
const removed = removeNodeById(children, nodeId);
|
|
58
|
+
if (removed) {
|
|
59
|
+
// Clean up empty children arrays
|
|
60
|
+
if (children.length === 0) {
|
|
61
|
+
nodes[i] = { ...nodes[i]!, children: undefined };
|
|
62
|
+
}
|
|
63
|
+
return removed;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Insert a node relative to a target node. Mutates the cloned tree in-place.
|
|
72
|
+
* Returns true if insertion was successful.
|
|
73
|
+
*/
|
|
74
|
+
function insertNode<ID>(
|
|
75
|
+
nodes: TreeNode<ID>[],
|
|
76
|
+
nodeToInsert: TreeNode<ID>,
|
|
77
|
+
targetId: ID,
|
|
78
|
+
position: DropPosition,
|
|
79
|
+
): boolean {
|
|
80
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
81
|
+
if (nodes[i]!.id === targetId) {
|
|
82
|
+
if (position === "above") {
|
|
83
|
+
nodes.splice(i, 0, nodeToInsert);
|
|
84
|
+
} else if (position === "below") {
|
|
85
|
+
nodes.splice(i + 1, 0, nodeToInsert);
|
|
86
|
+
} else {
|
|
87
|
+
// "inside" - add as first child
|
|
88
|
+
const target = nodes[i]!;
|
|
89
|
+
if (target.children) {
|
|
90
|
+
target.children.unshift(nodeToInsert);
|
|
91
|
+
} else {
|
|
92
|
+
nodes[i] = { ...target, children: [nodeToInsert] };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
const children = nodes[i]!.children;
|
|
98
|
+
if (children) {
|
|
99
|
+
if (insertNode(children, nodeToInsert, targetId, position)) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
@@ -200,3 +200,99 @@ export function toggleCheckboxes<ID>(
|
|
|
200
200
|
updateChecked(tempChecked);
|
|
201
201
|
updateIndeterminate(tempIndeterminate);
|
|
202
202
|
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Recalculates checked/indeterminate state for all parent nodes bottom-up.
|
|
206
|
+
* Should be called after tree structure changes (e.g., drag-drop moves) to ensure
|
|
207
|
+
* parent states correctly reflect their children's checked states.
|
|
208
|
+
*/
|
|
209
|
+
export function recalculateCheckedStates<ID>(storeId: string) {
|
|
210
|
+
const treeViewStore = getTreeViewStore<ID>(storeId);
|
|
211
|
+
const {
|
|
212
|
+
checked,
|
|
213
|
+
updateChecked,
|
|
214
|
+
indeterminate,
|
|
215
|
+
updateIndeterminate,
|
|
216
|
+
nodeMap,
|
|
217
|
+
childToParentMap,
|
|
218
|
+
selectionPropagation,
|
|
219
|
+
} = treeViewStore.getState();
|
|
220
|
+
|
|
221
|
+
// Only recalculate if parent propagation is enabled
|
|
222
|
+
if (!selectionPropagation.toParents) return;
|
|
223
|
+
|
|
224
|
+
const tempChecked = new Set(checked);
|
|
225
|
+
const tempIndeterminate = new Set(indeterminate);
|
|
226
|
+
|
|
227
|
+
// Collect parent nodes and clean up leaf nodes that shouldn't be indeterminate.
|
|
228
|
+
// A leaf node (no children) can never be indeterminate — this can happen when
|
|
229
|
+
// all children of a formerly-indeterminate parent are dragged away.
|
|
230
|
+
const parentNodes: ID[] = [];
|
|
231
|
+
for (const [id, node] of nodeMap) {
|
|
232
|
+
if (node.children && node.children.length > 0) {
|
|
233
|
+
parentNodes.push(id);
|
|
234
|
+
} else {
|
|
235
|
+
tempIndeterminate.delete(id);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Sort by depth descending (deepest first) for correct bottom-up propagation
|
|
240
|
+
const nodeDepths = new Map<ID, number>();
|
|
241
|
+
function getDepth(nodeId: ID): number {
|
|
242
|
+
if (nodeDepths.has(nodeId)) return nodeDepths.get(nodeId)!;
|
|
243
|
+
let depth = 0;
|
|
244
|
+
let currentId: ID | undefined = nodeId;
|
|
245
|
+
while (currentId) {
|
|
246
|
+
const parentId = childToParentMap.get(currentId);
|
|
247
|
+
if (parentId) {
|
|
248
|
+
depth++;
|
|
249
|
+
currentId = parentId;
|
|
250
|
+
} else {
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
nodeDepths.set(nodeId, depth);
|
|
255
|
+
return depth;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
parentNodes.sort((a, b) => getDepth(b) - getDepth(a));
|
|
259
|
+
|
|
260
|
+
// Update each parent based on its children's current state
|
|
261
|
+
for (const parentId of parentNodes) {
|
|
262
|
+
const node = nodeMap.get(parentId);
|
|
263
|
+
if (!node?.children?.length) continue;
|
|
264
|
+
|
|
265
|
+
let allChecked = true;
|
|
266
|
+
let anyCheckedOrIndeterminate = false;
|
|
267
|
+
|
|
268
|
+
for (const child of node.children) {
|
|
269
|
+
const isChecked = tempChecked.has(child.id);
|
|
270
|
+
const isIndeterminate = tempIndeterminate.has(child.id);
|
|
271
|
+
|
|
272
|
+
if (isChecked) {
|
|
273
|
+
anyCheckedOrIndeterminate = true;
|
|
274
|
+
} else if (isIndeterminate) {
|
|
275
|
+
anyCheckedOrIndeterminate = true;
|
|
276
|
+
allChecked = false;
|
|
277
|
+
} else {
|
|
278
|
+
allChecked = false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!allChecked && anyCheckedOrIndeterminate) break;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (allChecked) {
|
|
285
|
+
tempChecked.add(parentId);
|
|
286
|
+
tempIndeterminate.delete(parentId);
|
|
287
|
+
} else if (anyCheckedOrIndeterminate) {
|
|
288
|
+
tempChecked.delete(parentId);
|
|
289
|
+
tempIndeterminate.add(parentId);
|
|
290
|
+
} else {
|
|
291
|
+
tempChecked.delete(parentId);
|
|
292
|
+
tempIndeterminate.delete(parentId);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
updateChecked(tempChecked);
|
|
297
|
+
updateIndeterminate(tempIndeterminate);
|
|
298
|
+
}
|