react-native-tree-multi-select 2.0.13 → 3.0.0-beta.2
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 +104 -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 +288 -33
- 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 +683 -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 +22 -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 +13 -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 +40 -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 +9 -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 +94 -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 +114 -0
- package/src/components/DropIndicator.tsx +95 -0
- package/src/components/NodeList.tsx +327 -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 +835 -0
- package/src/index.tsx +19 -2
- package/src/store/treeView.store.ts +36 -0
- package/src/types/dragDrop.types.ts +23 -0
- package/src/types/treeView.types.ts +110 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Animated, StyleSheet, View } from "react-native";
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
__FlattenedTreeNode__,
|
|
6
|
+
CheckBoxViewProps,
|
|
7
|
+
DragDropCustomizations,
|
|
8
|
+
TreeItemCustomizations,
|
|
9
|
+
} from "../types/treeView.types";
|
|
10
|
+
import { CheckboxView } from "./CheckboxView";
|
|
11
|
+
import { CustomExpandCollapseIcon } from "./CustomExpandCollapseIcon";
|
|
12
|
+
import { defaultIndentationMultiplier } from "../constants/treeView.constants";
|
|
13
|
+
|
|
14
|
+
interface DragOverlayProps<ID> extends TreeItemCustomizations<ID> {
|
|
15
|
+
overlayY: Animated.Value;
|
|
16
|
+
overlayX: Animated.Value;
|
|
17
|
+
node: __FlattenedTreeNode__<ID>;
|
|
18
|
+
level: number;
|
|
19
|
+
dragDropCustomizations?: DragDropCustomizations<ID>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function _DragOverlay<ID>(props: DragOverlayProps<ID>) {
|
|
23
|
+
const {
|
|
24
|
+
overlayY,
|
|
25
|
+
overlayX,
|
|
26
|
+
node,
|
|
27
|
+
level,
|
|
28
|
+
indentationMultiplier = defaultIndentationMultiplier,
|
|
29
|
+
CheckboxComponent = CheckboxView as React.ComponentType<CheckBoxViewProps>,
|
|
30
|
+
ExpandCollapseIconComponent = CustomExpandCollapseIcon,
|
|
31
|
+
CustomNodeRowComponent,
|
|
32
|
+
checkBoxViewStyleProps,
|
|
33
|
+
dragDropCustomizations,
|
|
34
|
+
} = props;
|
|
35
|
+
|
|
36
|
+
const overlayStyleProps = dragDropCustomizations?.dragOverlayStyleProps;
|
|
37
|
+
const CustomOverlay = dragDropCustomizations?.CustomDragOverlayComponent;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Animated.View
|
|
41
|
+
pointerEvents="none"
|
|
42
|
+
style={[
|
|
43
|
+
styles.overlay,
|
|
44
|
+
overlayStyleProps && {
|
|
45
|
+
...(overlayStyleProps.backgroundColor != null && { backgroundColor: overlayStyleProps.backgroundColor }),
|
|
46
|
+
...(overlayStyleProps.shadowColor != null && { shadowColor: overlayStyleProps.shadowColor }),
|
|
47
|
+
...(overlayStyleProps.shadowOpacity != null && { shadowOpacity: overlayStyleProps.shadowOpacity }),
|
|
48
|
+
...(overlayStyleProps.shadowRadius != null && { shadowRadius: overlayStyleProps.shadowRadius }),
|
|
49
|
+
...(overlayStyleProps.elevation != null && { elevation: overlayStyleProps.elevation }),
|
|
50
|
+
},
|
|
51
|
+
overlayStyleProps?.style,
|
|
52
|
+
{ transform: [{ translateX: overlayX }, { translateY: overlayY }] },
|
|
53
|
+
]}
|
|
54
|
+
>
|
|
55
|
+
{CustomOverlay ? (
|
|
56
|
+
<CustomOverlay node={node} level={level} />
|
|
57
|
+
) : CustomNodeRowComponent ? (
|
|
58
|
+
<CustomNodeRowComponent
|
|
59
|
+
node={node}
|
|
60
|
+
level={level}
|
|
61
|
+
checkedValue={false}
|
|
62
|
+
isExpanded={false}
|
|
63
|
+
onCheck={() => {}}
|
|
64
|
+
onExpand={() => {}}
|
|
65
|
+
/>
|
|
66
|
+
) : (
|
|
67
|
+
<View
|
|
68
|
+
style={[
|
|
69
|
+
styles.nodeRow,
|
|
70
|
+
{ paddingStart: level * indentationMultiplier },
|
|
71
|
+
]}
|
|
72
|
+
>
|
|
73
|
+
<CheckboxComponent
|
|
74
|
+
text={node.name}
|
|
75
|
+
onValueChange={() => {}}
|
|
76
|
+
value={false}
|
|
77
|
+
{...checkBoxViewStyleProps}
|
|
78
|
+
/>
|
|
79
|
+
{node.children?.length ? (
|
|
80
|
+
<View style={styles.expandArrow}>
|
|
81
|
+
<ExpandCollapseIconComponent isExpanded={false} />
|
|
82
|
+
</View>
|
|
83
|
+
) : null}
|
|
84
|
+
</View>
|
|
85
|
+
)}
|
|
86
|
+
</Animated.View>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const DragOverlay = React.memo(_DragOverlay) as typeof _DragOverlay;
|
|
91
|
+
|
|
92
|
+
const styles = StyleSheet.create({
|
|
93
|
+
overlay: {
|
|
94
|
+
position: "absolute",
|
|
95
|
+
left: 0,
|
|
96
|
+
right: 0,
|
|
97
|
+
zIndex: 9999,
|
|
98
|
+
elevation: 10,
|
|
99
|
+
shadowColor: "#000",
|
|
100
|
+
shadowOffset: { width: 0, height: 2 },
|
|
101
|
+
shadowOpacity: 0.25,
|
|
102
|
+
shadowRadius: 4,
|
|
103
|
+
backgroundColor: "rgba(255, 255, 255, 0.95)",
|
|
104
|
+
},
|
|
105
|
+
nodeRow: {
|
|
106
|
+
flex: 1,
|
|
107
|
+
flexDirection: "row",
|
|
108
|
+
alignItems: "center",
|
|
109
|
+
minWidth: "100%",
|
|
110
|
+
},
|
|
111
|
+
expandArrow: {
|
|
112
|
+
flex: 1,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Animated, View, StyleSheet } from "react-native";
|
|
3
|
+
import type { DropPosition } from "../types/dragDrop.types";
|
|
4
|
+
|
|
5
|
+
interface DropIndicatorProps {
|
|
6
|
+
position: DropPosition;
|
|
7
|
+
overlayY: Animated.Value;
|
|
8
|
+
itemHeight: number;
|
|
9
|
+
targetLevel: number;
|
|
10
|
+
indentationMultiplier: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const DropIndicator = React.memo(function DropIndicator(
|
|
14
|
+
props: DropIndicatorProps
|
|
15
|
+
) {
|
|
16
|
+
const {
|
|
17
|
+
position,
|
|
18
|
+
overlayY,
|
|
19
|
+
itemHeight,
|
|
20
|
+
targetLevel,
|
|
21
|
+
indentationMultiplier,
|
|
22
|
+
} = props;
|
|
23
|
+
|
|
24
|
+
const indent = targetLevel * indentationMultiplier;
|
|
25
|
+
|
|
26
|
+
if (position === "inside") {
|
|
27
|
+
return (
|
|
28
|
+
<Animated.View
|
|
29
|
+
pointerEvents="none"
|
|
30
|
+
style={[
|
|
31
|
+
styles.highlightIndicator,
|
|
32
|
+
{
|
|
33
|
+
transform: [{ translateY: overlayY }],
|
|
34
|
+
height: itemHeight,
|
|
35
|
+
left: indent,
|
|
36
|
+
},
|
|
37
|
+
]}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// For "above", the line is at the overlay's top edge (offset 0)
|
|
43
|
+
// For "below", the line is at the overlay's bottom edge (offset +itemHeight)
|
|
44
|
+
const lineOffset = position === "above" ? 0 : itemHeight;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Animated.View
|
|
48
|
+
pointerEvents="none"
|
|
49
|
+
style={[
|
|
50
|
+
styles.lineContainer,
|
|
51
|
+
{
|
|
52
|
+
transform: [{ translateY: Animated.add(overlayY, lineOffset - 1) }],
|
|
53
|
+
left: indent,
|
|
54
|
+
},
|
|
55
|
+
]}
|
|
56
|
+
>
|
|
57
|
+
<View style={styles.lineCircle} />
|
|
58
|
+
<View style={styles.line} />
|
|
59
|
+
</Animated.View>
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const styles = StyleSheet.create({
|
|
64
|
+
highlightIndicator: {
|
|
65
|
+
position: "absolute",
|
|
66
|
+
left: 0,
|
|
67
|
+
right: 0,
|
|
68
|
+
backgroundColor: "rgba(0, 200, 0, 0.2)",
|
|
69
|
+
borderWidth: 2,
|
|
70
|
+
borderColor: "rgba(0, 200, 0, 0.6)",
|
|
71
|
+
borderRadius: 4,
|
|
72
|
+
zIndex: 9998,
|
|
73
|
+
},
|
|
74
|
+
lineContainer: {
|
|
75
|
+
position: "absolute",
|
|
76
|
+
right: 0,
|
|
77
|
+
flexDirection: "row",
|
|
78
|
+
alignItems: "center",
|
|
79
|
+
height: 3,
|
|
80
|
+
zIndex: 9998,
|
|
81
|
+
},
|
|
82
|
+
lineCircle: {
|
|
83
|
+
width: 10,
|
|
84
|
+
height: 10,
|
|
85
|
+
borderRadius: 5,
|
|
86
|
+
backgroundColor: "#00CC00",
|
|
87
|
+
marginLeft: -5,
|
|
88
|
+
marginTop: -4,
|
|
89
|
+
},
|
|
90
|
+
line: {
|
|
91
|
+
flex: 1,
|
|
92
|
+
height: 3,
|
|
93
|
+
backgroundColor: "#00CC00",
|
|
94
|
+
},
|
|
95
|
+
});
|
|
@@ -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,50 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
90
114
|
updateInnerMostChildrenIds(updatedInnerMostChildrenIds);
|
|
91
115
|
}, [filteredTree, updateInnerMostChildrenIds]);
|
|
92
116
|
|
|
117
|
+
const effectiveIndentationMultiplier = indentationMultiplier ?? defaultIndentationMultiplier;
|
|
118
|
+
|
|
119
|
+
// --- Drag and drop ---
|
|
120
|
+
const {
|
|
121
|
+
panResponder,
|
|
122
|
+
overlayY,
|
|
123
|
+
overlayX,
|
|
124
|
+
isDragging,
|
|
125
|
+
draggedNode,
|
|
126
|
+
effectiveDropLevel,
|
|
127
|
+
handleNodeTouchStart,
|
|
128
|
+
handleNodeTouchEnd,
|
|
129
|
+
cancelLongPressTimer,
|
|
130
|
+
scrollOffsetRef,
|
|
131
|
+
} = useDragDrop<ID>({
|
|
132
|
+
storeId,
|
|
133
|
+
flattenedNodes: flattenedFilteredNodes,
|
|
134
|
+
flashListRef,
|
|
135
|
+
containerRef,
|
|
136
|
+
dragEnabled: dragEnabled ?? false,
|
|
137
|
+
onDragEnd,
|
|
138
|
+
longPressDuration,
|
|
139
|
+
autoScrollThreshold,
|
|
140
|
+
autoScrollSpeed,
|
|
141
|
+
internalDataRef,
|
|
142
|
+
measuredItemHeightRef,
|
|
143
|
+
dragOverlayOffset,
|
|
144
|
+
autoExpandDelay,
|
|
145
|
+
indentationMultiplier: effectiveIndentationMultiplier,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Combined onScroll handler
|
|
149
|
+
const handleScroll = React.useCallback((
|
|
150
|
+
event: NativeSyntheticEvent<NativeScrollEvent>
|
|
151
|
+
) => {
|
|
152
|
+
scrollOffsetRef.current = event.nativeEvent.contentOffset.y;
|
|
153
|
+
// Cancel long press timer if user is scrolling
|
|
154
|
+
cancelLongPressTimer();
|
|
155
|
+
// Forward to user's onScroll
|
|
156
|
+
treeFlashListProps?.onScroll?.(event as any);
|
|
157
|
+
}, [scrollOffsetRef, cancelLongPressTimer, treeFlashListProps]);
|
|
158
|
+
|
|
93
159
|
const nodeRenderer = React.useCallback((
|
|
94
|
-
{ item }: { item: __FlattenedTreeNode__<ID>; }
|
|
160
|
+
{ item, index }: { item: __FlattenedTreeNode__<ID>; index: number; }
|
|
95
161
|
) => {
|
|
96
162
|
return (
|
|
97
163
|
<Node<ID>
|
|
@@ -107,6 +173,14 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
107
173
|
ExpandCollapseIconComponent={ExpandCollapseIconComponent}
|
|
108
174
|
ExpandCollapseTouchableComponent={ExpandCollapseTouchableComponent}
|
|
109
175
|
CustomNodeRowComponent={CustomNodeRowComponent}
|
|
176
|
+
|
|
177
|
+
nodeIndex={index}
|
|
178
|
+
dragEnabled={dragEnabled}
|
|
179
|
+
isDragging={isDragging}
|
|
180
|
+
onNodeTouchStart={dragEnabled ? handleNodeTouchStart : undefined}
|
|
181
|
+
onNodeTouchEnd={dragEnabled ? handleNodeTouchEnd : undefined}
|
|
182
|
+
onItemLayout={dragEnabled ? handleItemLayout : undefined}
|
|
183
|
+
dragDropCustomizations={dragDropCustomizations}
|
|
110
184
|
/>
|
|
111
185
|
);
|
|
112
186
|
}, [
|
|
@@ -116,9 +190,37 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
116
190
|
ExpandCollapseTouchableComponent,
|
|
117
191
|
CustomNodeRowComponent,
|
|
118
192
|
checkBoxViewStyleProps,
|
|
119
|
-
indentationMultiplier
|
|
193
|
+
indentationMultiplier,
|
|
194
|
+
dragEnabled,
|
|
195
|
+
isDragging,
|
|
196
|
+
handleNodeTouchStart,
|
|
197
|
+
handleNodeTouchEnd,
|
|
198
|
+
dragDropCustomizations,
|
|
199
|
+
handleItemLayout,
|
|
120
200
|
]);
|
|
121
201
|
|
|
202
|
+
// Extract FlashList props but exclude onScroll (we provide our own combined handler)
|
|
203
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
204
|
+
const { onScroll: _userOnScroll, ...restFlashListProps } = treeFlashListProps ?? {};
|
|
205
|
+
|
|
206
|
+
const flashListElement = (
|
|
207
|
+
<FlashList
|
|
208
|
+
ref={flashListRef}
|
|
209
|
+
estimatedItemSize={36}
|
|
210
|
+
initialScrollIndex={initialScrollIndex}
|
|
211
|
+
removeClippedSubviews={true}
|
|
212
|
+
keyboardShouldPersistTaps="handled"
|
|
213
|
+
drawDistance={50}
|
|
214
|
+
ListHeaderComponent={<HeaderFooterView />}
|
|
215
|
+
ListFooterComponent={<HeaderFooterView />}
|
|
216
|
+
{...restFlashListProps}
|
|
217
|
+
onScroll={handleScroll}
|
|
218
|
+
scrollEnabled={isDragging ? false : (restFlashListProps?.scrollEnabled ?? true)}
|
|
219
|
+
data={flattenedFilteredNodes}
|
|
220
|
+
renderItem={nodeRenderer}
|
|
221
|
+
/>
|
|
222
|
+
);
|
|
223
|
+
|
|
122
224
|
return (
|
|
123
225
|
<>
|
|
124
226
|
<ScrollToNodeHandler
|
|
@@ -129,19 +231,31 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
129
231
|
setInitialScrollIndex={setInitialScrollIndex}
|
|
130
232
|
initialScrollNodeID={initialScrollNodeID} />
|
|
131
233
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
234
|
+
{dragEnabled ? (
|
|
235
|
+
<View
|
|
236
|
+
ref={containerRef}
|
|
237
|
+
style={styles.dragContainer}
|
|
238
|
+
{...panResponder.panHandlers}
|
|
239
|
+
>
|
|
240
|
+
{flashListElement}
|
|
241
|
+
{isDragging && draggedNode && (
|
|
242
|
+
<DragOverlay<ID>
|
|
243
|
+
overlayY={overlayY}
|
|
244
|
+
overlayX={overlayX}
|
|
245
|
+
node={draggedNode}
|
|
246
|
+
level={effectiveDropLevel}
|
|
247
|
+
indentationMultiplier={effectiveIndentationMultiplier}
|
|
248
|
+
CheckboxComponent={CheckboxComponent}
|
|
249
|
+
ExpandCollapseIconComponent={ExpandCollapseIconComponent}
|
|
250
|
+
CustomNodeRowComponent={CustomNodeRowComponent}
|
|
251
|
+
checkBoxViewStyleProps={checkBoxViewStyleProps}
|
|
252
|
+
dragDropCustomizations={dragDropCustomizations}
|
|
253
|
+
/>
|
|
254
|
+
)}
|
|
255
|
+
</View>
|
|
256
|
+
) : (
|
|
257
|
+
flashListElement
|
|
258
|
+
)}
|
|
145
259
|
</>
|
|
146
260
|
);
|
|
147
261
|
};
|
|
@@ -179,38 +293,106 @@ function _Node<ID>(props: NodeProps<ID>) {
|
|
|
179
293
|
ExpandCollapseIconComponent = CustomExpandCollapseIcon,
|
|
180
294
|
CheckboxComponent = CheckboxView,
|
|
181
295
|
ExpandCollapseTouchableComponent = TouchableOpacity,
|
|
182
|
-
CustomNodeRowComponent
|
|
296
|
+
CustomNodeRowComponent,
|
|
297
|
+
|
|
298
|
+
nodeIndex = 0,
|
|
299
|
+
dragEnabled,
|
|
300
|
+
isDragging: isDraggingGlobal,
|
|
301
|
+
onNodeTouchStart,
|
|
302
|
+
onNodeTouchEnd,
|
|
303
|
+
onItemLayout,
|
|
304
|
+
dragDropCustomizations,
|
|
183
305
|
} = props;
|
|
184
306
|
|
|
185
307
|
const {
|
|
186
308
|
isExpanded,
|
|
187
309
|
value,
|
|
310
|
+
isBeingDragged,
|
|
311
|
+
isDragInvalid,
|
|
312
|
+
isDropTarget,
|
|
313
|
+
nodeDropPosition,
|
|
314
|
+
nodeDropLevel,
|
|
188
315
|
} = useTreeViewStore<ID>(storeId)(useShallow(
|
|
189
316
|
state => ({
|
|
190
317
|
isExpanded: state.expanded.has(node.id),
|
|
191
318
|
value: getValue(
|
|
192
|
-
state.checked.has(node.id),
|
|
193
|
-
state.indeterminate.has(node.id)
|
|
319
|
+
state.checked.has(node.id),
|
|
320
|
+
state.indeterminate.has(node.id)
|
|
194
321
|
),
|
|
322
|
+
isBeingDragged: state.draggedNodeId === node.id,
|
|
323
|
+
isDragInvalid: state.invalidDragTargetIds.has(node.id),
|
|
324
|
+
isDropTarget: state.dropTargetNodeId === node.id,
|
|
325
|
+
nodeDropPosition: state.dropTargetNodeId === node.id ? state.dropPosition : null,
|
|
326
|
+
nodeDropLevel: state.dropTargetNodeId === node.id ? state.dropLevel : null,
|
|
195
327
|
})
|
|
196
328
|
));
|
|
197
329
|
|
|
330
|
+
// Track when this node was dragged so we can swallow the onPress/onCheck
|
|
331
|
+
// that fires when the user lifts their finger after a long-press-initiated drag.
|
|
332
|
+
// The flag is set during render (synchronous) and cleared on the next touch start.
|
|
333
|
+
const wasDraggedRef = React.useRef(false);
|
|
334
|
+
if (isDraggingGlobal && isBeingDragged) {
|
|
335
|
+
wasDraggedRef.current = true;
|
|
336
|
+
}
|
|
337
|
+
|
|
198
338
|
const _onToggleExpand = React.useCallback(() => {
|
|
339
|
+
if (wasDraggedRef.current) return;
|
|
199
340
|
handleToggleExpand(storeId, node.id);
|
|
200
341
|
}, [storeId, node.id]);
|
|
201
342
|
|
|
202
343
|
const _onCheck = React.useCallback(() => {
|
|
344
|
+
if (wasDraggedRef.current) return;
|
|
203
345
|
toggleCheckboxes(storeId, [node.id]);
|
|
204
346
|
}, [storeId, node.id]);
|
|
205
347
|
|
|
348
|
+
const handleTouchStart = React.useCallback((e: any) => {
|
|
349
|
+
wasDraggedRef.current = false;
|
|
350
|
+
if (!onNodeTouchStart) return;
|
|
351
|
+
const { pageY, locationY } = e.nativeEvent;
|
|
352
|
+
onNodeTouchStart(node.id, pageY, locationY, nodeIndex);
|
|
353
|
+
}, [node.id, nodeIndex, onNodeTouchStart]);
|
|
354
|
+
|
|
355
|
+
const handleTouchEnd = React.useCallback(() => {
|
|
356
|
+
onNodeTouchEnd?.();
|
|
357
|
+
}, [onNodeTouchEnd]);
|
|
358
|
+
|
|
359
|
+
// Determine opacity for drag state
|
|
360
|
+
const dragOpacity = dragDropCustomizations?.draggedNodeOpacity ?? 0.3;
|
|
361
|
+
const nodeOpacity = (isDraggingGlobal && (isBeingDragged || isDragInvalid))
|
|
362
|
+
? dragOpacity
|
|
363
|
+
: 1.0;
|
|
364
|
+
|
|
365
|
+
const handleLayout = React.useCallback((e: any) => {
|
|
366
|
+
onItemLayout?.(e.nativeEvent.layout.height);
|
|
367
|
+
}, [onItemLayout]);
|
|
368
|
+
|
|
369
|
+
const touchHandlers = dragEnabled ? {
|
|
370
|
+
onTouchStart: handleTouchStart,
|
|
371
|
+
onTouchEnd: handleTouchEnd,
|
|
372
|
+
onTouchCancel: handleTouchEnd,
|
|
373
|
+
} : undefined;
|
|
374
|
+
|
|
375
|
+
const CustomDropIndicator = dragDropCustomizations?.CustomDropIndicatorComponent;
|
|
376
|
+
const indicatorLevel = nodeDropLevel ?? level;
|
|
377
|
+
const dropIndicator = isDropTarget && nodeDropPosition ? (
|
|
378
|
+
CustomDropIndicator
|
|
379
|
+
? <CustomDropIndicator position={nodeDropPosition} level={indicatorLevel} indentationMultiplier={indentationMultiplier} />
|
|
380
|
+
: <NodeDropIndicator position={nodeDropPosition} level={indicatorLevel} indentationMultiplier={indentationMultiplier} styleProps={dragDropCustomizations?.dropIndicatorStyleProps} />
|
|
381
|
+
) : null;
|
|
382
|
+
|
|
206
383
|
if (!CustomNodeRowComponent) {
|
|
207
384
|
return (
|
|
208
385
|
<View
|
|
209
386
|
testID={`node_row_${node.id}`}
|
|
387
|
+
{...touchHandlers}
|
|
388
|
+
onLayout={onItemLayout ? handleLayout : undefined}
|
|
210
389
|
style={[
|
|
211
390
|
styles.nodeCheckboxAndArrowRow,
|
|
212
|
-
{ paddingStart: level * indentationMultiplier }
|
|
391
|
+
{ paddingStart: level * indentationMultiplier },
|
|
392
|
+
{ opacity: nodeOpacity },
|
|
393
|
+
dropIndicator ? styles.nodeOverflowVisible : undefined,
|
|
213
394
|
]}>
|
|
395
|
+
{dropIndicator}
|
|
214
396
|
<CheckboxComponent
|
|
215
397
|
text={node.name}
|
|
216
398
|
onValueChange={_onCheck}
|
|
@@ -233,17 +415,93 @@ function _Node<ID>(props: NodeProps<ID>) {
|
|
|
233
415
|
}
|
|
234
416
|
else {
|
|
235
417
|
return (
|
|
236
|
-
<
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
418
|
+
<View
|
|
419
|
+
{...touchHandlers}
|
|
420
|
+
onLayout={onItemLayout ? handleLayout : undefined}
|
|
421
|
+
style={[
|
|
422
|
+
{ opacity: nodeOpacity },
|
|
423
|
+
dropIndicator ? styles.nodeOverflowVisible : undefined,
|
|
424
|
+
]}
|
|
425
|
+
>
|
|
426
|
+
{dropIndicator}
|
|
427
|
+
<CustomNodeRowComponent
|
|
428
|
+
node={node}
|
|
429
|
+
level={level}
|
|
430
|
+
checkedValue={value}
|
|
431
|
+
isExpanded={isExpanded}
|
|
432
|
+
onCheck={_onCheck}
|
|
433
|
+
onExpand={_onToggleExpand}
|
|
434
|
+
isDragTarget={isDragInvalid}
|
|
435
|
+
isDragging={isDraggingGlobal}
|
|
436
|
+
isDraggedNode={isBeingDragged}
|
|
437
|
+
/>
|
|
438
|
+
</View>
|
|
243
439
|
);
|
|
244
440
|
}
|
|
245
441
|
};
|
|
246
442
|
|
|
443
|
+
function NodeDropIndicator({ position, level, indentationMultiplier, styleProps }: {
|
|
444
|
+
position: DropPosition;
|
|
445
|
+
level: number;
|
|
446
|
+
indentationMultiplier: number;
|
|
447
|
+
styleProps?: DropIndicatorStyleProps;
|
|
448
|
+
}) {
|
|
449
|
+
const lineColor = styleProps?.lineColor ?? "#0078FF";
|
|
450
|
+
const lineThickness = styleProps?.lineThickness ?? 3;
|
|
451
|
+
const circleSize = styleProps?.circleSize ?? 10;
|
|
452
|
+
const highlightColor = styleProps?.highlightColor ?? "rgba(0, 120, 255, 0.15)";
|
|
453
|
+
const highlightBorderColor = styleProps?.highlightBorderColor ?? "rgba(0, 120, 255, 0.5)";
|
|
454
|
+
|
|
455
|
+
// Indent the line to match the node's nesting level so users can
|
|
456
|
+
// visually distinguish drops at different tree depths.
|
|
457
|
+
const leftOffset = level * indentationMultiplier;
|
|
458
|
+
|
|
459
|
+
if (position === "inside") {
|
|
460
|
+
return (
|
|
461
|
+
<View
|
|
462
|
+
pointerEvents="none"
|
|
463
|
+
style={[
|
|
464
|
+
styles.dropHighlight,
|
|
465
|
+
{
|
|
466
|
+
left: leftOffset,
|
|
467
|
+
backgroundColor: highlightColor,
|
|
468
|
+
borderColor: highlightBorderColor,
|
|
469
|
+
},
|
|
470
|
+
]}
|
|
471
|
+
/>
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Ensure the circle isn't clipped at shallow indent levels
|
|
476
|
+
const safeLeftOffset = Math.max(leftOffset, circleSize / 2);
|
|
477
|
+
|
|
478
|
+
return (
|
|
479
|
+
<View
|
|
480
|
+
pointerEvents="none"
|
|
481
|
+
style={[
|
|
482
|
+
styles.dropLineContainer,
|
|
483
|
+
{ height: lineThickness, left: safeLeftOffset },
|
|
484
|
+
position === "above" ? styles.dropLineTop : styles.dropLineBottom,
|
|
485
|
+
]}
|
|
486
|
+
>
|
|
487
|
+
<View style={{
|
|
488
|
+
width: circleSize,
|
|
489
|
+
height: circleSize,
|
|
490
|
+
borderRadius: circleSize / 2,
|
|
491
|
+
backgroundColor: lineColor,
|
|
492
|
+
marginLeft: -(circleSize / 2),
|
|
493
|
+
}} />
|
|
494
|
+
<View style={[
|
|
495
|
+
styles.dropLine,
|
|
496
|
+
{
|
|
497
|
+
height: lineThickness,
|
|
498
|
+
backgroundColor: lineColor,
|
|
499
|
+
},
|
|
500
|
+
]} />
|
|
501
|
+
</View>
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
247
505
|
const styles = StyleSheet.create({
|
|
248
506
|
defaultHeaderFooter: {
|
|
249
507
|
padding: 5
|
|
@@ -256,6 +514,45 @@ const styles = StyleSheet.create({
|
|
|
256
514
|
flexDirection: "row",
|
|
257
515
|
alignItems: "center",
|
|
258
516
|
minWidth: "100%"
|
|
259
|
-
}
|
|
517
|
+
},
|
|
518
|
+
dragContainer: {
|
|
519
|
+
flex: 1,
|
|
520
|
+
},
|
|
521
|
+
// Drop indicator styles (rendered by each node)
|
|
522
|
+
dropHighlight: {
|
|
523
|
+
position: "absolute",
|
|
524
|
+
top: 0,
|
|
525
|
+
bottom: 0,
|
|
526
|
+
left: 0,
|
|
527
|
+
right: 0,
|
|
528
|
+
backgroundColor: "rgba(0, 120, 255, 0.15)",
|
|
529
|
+
borderWidth: 2,
|
|
530
|
+
borderColor: "rgba(0, 120, 255, 0.5)",
|
|
531
|
+
borderRadius: 4,
|
|
532
|
+
zIndex: 10,
|
|
533
|
+
},
|
|
534
|
+
dropLineContainer: {
|
|
535
|
+
position: "absolute",
|
|
536
|
+
left: 0,
|
|
537
|
+
right: 0,
|
|
538
|
+
flexDirection: "row",
|
|
539
|
+
alignItems: "center",
|
|
540
|
+
height: 3,
|
|
541
|
+
zIndex: 10,
|
|
542
|
+
overflow: "visible",
|
|
543
|
+
},
|
|
544
|
+
dropLineTop: {
|
|
545
|
+
top: 0,
|
|
546
|
+
},
|
|
547
|
+
dropLineBottom: {
|
|
548
|
+
bottom: 0,
|
|
549
|
+
},
|
|
550
|
+
dropLine: {
|
|
551
|
+
flex: 1,
|
|
552
|
+
height: 3,
|
|
553
|
+
backgroundColor: "#0078FF",
|
|
554
|
+
},
|
|
555
|
+
nodeOverflowVisible: {
|
|
556
|
+
overflow: "visible",
|
|
557
|
+
},
|
|
260
558
|
});
|
|
261
|
-
|
package/src/helpers/index.ts
CHANGED