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.
Files changed (58) hide show
  1. package/README.md +151 -0
  2. package/lib/module/TreeView.js +32 -2
  3. package/lib/module/TreeView.js.map +1 -1
  4. package/lib/module/components/DragOverlay.js +104 -0
  5. package/lib/module/components/DragOverlay.js.map +1 -0
  6. package/lib/module/components/DropIndicator.js +79 -0
  7. package/lib/module/components/DropIndicator.js.map +1 -0
  8. package/lib/module/components/NodeList.js +288 -33
  9. package/lib/module/components/NodeList.js.map +1 -1
  10. package/lib/module/helpers/index.js +1 -0
  11. package/lib/module/helpers/index.js.map +1 -1
  12. package/lib/module/helpers/moveTreeNode.helper.js +96 -0
  13. package/lib/module/helpers/moveTreeNode.helper.js.map +1 -0
  14. package/lib/module/helpers/toggleCheckbox.helper.js +88 -0
  15. package/lib/module/helpers/toggleCheckbox.helper.js.map +1 -1
  16. package/lib/module/hooks/useDragDrop.js +683 -0
  17. package/lib/module/hooks/useDragDrop.js.map +1 -0
  18. package/lib/module/index.js +1 -1
  19. package/lib/module/index.js.map +1 -1
  20. package/lib/module/store/treeView.store.js +22 -1
  21. package/lib/module/store/treeView.store.js.map +1 -1
  22. package/lib/module/types/dragDrop.types.js +4 -0
  23. package/lib/module/types/dragDrop.types.js.map +1 -0
  24. package/lib/typescript/src/TreeView.d.ts.map +1 -1
  25. package/lib/typescript/src/components/DragOverlay.d.ts +13 -0
  26. package/lib/typescript/src/components/DragOverlay.d.ts.map +1 -0
  27. package/lib/typescript/src/components/DropIndicator.d.ts +13 -0
  28. package/lib/typescript/src/components/DropIndicator.d.ts.map +1 -0
  29. package/lib/typescript/src/components/NodeList.d.ts.map +1 -1
  30. package/lib/typescript/src/helpers/index.d.ts +1 -0
  31. package/lib/typescript/src/helpers/index.d.ts.map +1 -1
  32. package/lib/typescript/src/helpers/moveTreeNode.helper.d.ts +13 -0
  33. package/lib/typescript/src/helpers/moveTreeNode.helper.d.ts.map +1 -0
  34. package/lib/typescript/src/helpers/toggleCheckbox.helper.d.ts +6 -0
  35. package/lib/typescript/src/helpers/toggleCheckbox.helper.d.ts.map +1 -1
  36. package/lib/typescript/src/hooks/useDragDrop.d.ts +40 -0
  37. package/lib/typescript/src/hooks/useDragDrop.d.ts.map +1 -0
  38. package/lib/typescript/src/index.d.ts +4 -2
  39. package/lib/typescript/src/index.d.ts.map +1 -1
  40. package/lib/typescript/src/store/treeView.store.d.ts +9 -0
  41. package/lib/typescript/src/store/treeView.store.d.ts.map +1 -1
  42. package/lib/typescript/src/types/dragDrop.types.d.ts +21 -0
  43. package/lib/typescript/src/types/dragDrop.types.d.ts.map +1 -0
  44. package/lib/typescript/src/types/treeView.types.d.ts +94 -0
  45. package/lib/typescript/src/types/treeView.types.d.ts.map +1 -1
  46. package/package.json +1 -1
  47. package/src/TreeView.tsx +34 -0
  48. package/src/components/DragOverlay.tsx +114 -0
  49. package/src/components/DropIndicator.tsx +95 -0
  50. package/src/components/NodeList.tsx +327 -30
  51. package/src/helpers/index.ts +2 -1
  52. package/src/helpers/moveTreeNode.helper.ts +105 -0
  53. package/src/helpers/toggleCheckbox.helper.ts +96 -0
  54. package/src/hooks/useDragDrop.ts +835 -0
  55. package/src/index.tsx +19 -2
  56. package/src/store/treeView.store.ts +36 -0
  57. package/src/types/dragDrop.types.ts +23 -0
  58. 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
- <FlashList
133
- ref={flashListRef}
134
- estimatedItemSize={36}
135
- initialScrollIndex={initialScrollIndex}
136
- removeClippedSubviews={true}
137
- keyboardShouldPersistTaps="handled"
138
- drawDistance={50}
139
- ListHeaderComponent={<HeaderFooterView />}
140
- ListFooterComponent={<HeaderFooterView />}
141
- {...treeFlashListProps}
142
- data={flattenedFilteredNodes}
143
- renderItem={nodeRenderer}
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), // isChecked
193
- state.indeterminate.has(node.id) // isIndeterminate
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
- <CustomNodeRowComponent
237
- node={node}
238
- level={level}
239
- checkedValue={value}
240
- isExpanded={isExpanded}
241
- onCheck={_onCheck}
242
- onExpand={_onToggleExpand} />
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
-
@@ -5,4 +5,5 @@ export * from "./treeNode.helper";
5
5
  export * from "./selectAll.helper";
6
6
  export * from "./toggleCheckbox.helper";
7
7
  export * from "./search.helper";
8
- export * from "./flattenTree.helper";
8
+ export * from "./flattenTree.helper";
9
+ export * from "./moveTreeNode.helper";