react-native-reanimated-dnd 1.0.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.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +633 -0
  3. package/lib/components/Draggable.d.ts +5 -0
  4. package/lib/components/Draggable.js +265 -0
  5. package/lib/components/Droppable.d.ts +264 -0
  6. package/lib/components/Droppable.js +284 -0
  7. package/lib/components/Sortable.d.ts +184 -0
  8. package/lib/components/Sortable.js +225 -0
  9. package/lib/components/SortableItem.d.ts +158 -0
  10. package/lib/components/SortableItem.js +251 -0
  11. package/lib/components/sortableUtils.d.ts +21 -0
  12. package/lib/components/sortableUtils.js +50 -0
  13. package/lib/context/DropContext.d.ts +118 -0
  14. package/lib/context/DropContext.js +233 -0
  15. package/lib/hooks/index.d.ts +4 -0
  16. package/lib/hooks/index.js +5 -0
  17. package/lib/hooks/useDraggable.d.ts +101 -0
  18. package/lib/hooks/useDraggable.js +567 -0
  19. package/lib/hooks/useDroppable.d.ts +129 -0
  20. package/lib/hooks/useDroppable.js +261 -0
  21. package/lib/hooks/useSortable.d.ts +174 -0
  22. package/lib/hooks/useSortable.js +361 -0
  23. package/lib/hooks/useSortableList.d.ts +182 -0
  24. package/lib/hooks/useSortableList.js +211 -0
  25. package/lib/index.d.ts +11 -0
  26. package/lib/index.js +16 -0
  27. package/lib/types/context.d.ts +166 -0
  28. package/lib/types/context.js +80 -0
  29. package/lib/types/draggable.d.ts +313 -0
  30. package/lib/types/draggable.js +31 -0
  31. package/lib/types/droppable.d.ts +197 -0
  32. package/lib/types/droppable.js +1 -0
  33. package/lib/types/index.d.ts +4 -0
  34. package/lib/types/index.js +8 -0
  35. package/lib/types/sortable.d.ts +432 -0
  36. package/lib/types/sortable.js +6 -0
  37. package/package.json +59 -0
@@ -0,0 +1,361 @@
1
+ import { useState, useRef, useEffect } from "react";
2
+ import React from "react";
3
+ import { runOnJS, runOnUI, useAnimatedGestureHandler, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, withSpring, withTiming, } from "react-native-reanimated";
4
+ export var ScrollDirection;
5
+ (function (ScrollDirection) {
6
+ ScrollDirection["None"] = "none";
7
+ ScrollDirection["Up"] = "up";
8
+ ScrollDirection["Down"] = "down";
9
+ })(ScrollDirection || (ScrollDirection = {}));
10
+ export function clamp(value, lowerBound, upperBound) {
11
+ "worklet";
12
+ return Math.max(lowerBound, Math.min(value, upperBound));
13
+ }
14
+ export function objectMove(object, from, to) {
15
+ "worklet";
16
+ const newObject = Object.assign({}, object);
17
+ for (const id in object) {
18
+ if (object[id] === from) {
19
+ newObject[id] = to;
20
+ }
21
+ if (object[id] === to) {
22
+ newObject[id] = from;
23
+ }
24
+ }
25
+ return newObject;
26
+ }
27
+ export function listToObject(list) {
28
+ const values = Object.values(list);
29
+ const object = {};
30
+ for (let i = 0; i < values.length; i++) {
31
+ object[values[i].id] = i;
32
+ }
33
+ return object;
34
+ }
35
+ export function setPosition(positionY, itemsCount, positions, id, itemHeight) {
36
+ "worklet";
37
+ const newPosition = clamp(Math.floor(positionY / itemHeight), 0, itemsCount - 1);
38
+ if (newPosition !== positions.value[id]) {
39
+ positions.value = objectMove(positions.value, positions.value[id], newPosition);
40
+ }
41
+ }
42
+ export function setAutoScroll(positionY, lowerBound, upperBound, scrollThreshold, autoScroll) {
43
+ "worklet";
44
+ if (positionY <= lowerBound + scrollThreshold) {
45
+ autoScroll.value = ScrollDirection.Up;
46
+ }
47
+ else if (positionY >= upperBound - scrollThreshold) {
48
+ autoScroll.value = ScrollDirection.Down;
49
+ }
50
+ else {
51
+ autoScroll.value = ScrollDirection.None;
52
+ }
53
+ }
54
+ /**
55
+ * A hook for creating sortable list items with drag-and-drop reordering capabilities.
56
+ *
57
+ * This hook provides the core functionality for individual items within a sortable list,
58
+ * handling drag gestures, position animations, auto-scrolling, and reordering logic.
59
+ * It works in conjunction with useSortableList to provide a complete sortable solution.
60
+ *
61
+ * @template T - The type of data associated with the sortable item
62
+ * @param options - Configuration options for the sortable item behavior
63
+ * @returns Object containing animated styles, gesture handlers, and state for the sortable item
64
+ *
65
+ * @example
66
+ * Basic sortable item:
67
+ * ```typescript
68
+ * import { useSortable } from './hooks/useSortable';
69
+ *
70
+ * function SortableTaskItem({ task, positions, ...sortableProps }) {
71
+ * const { animatedStyle, panGestureHandler, isMoving } = useSortable({
72
+ * id: task.id,
73
+ * positions,
74
+ * ...sortableProps,
75
+ * onMove: (id, from, to) => {
76
+ * console.log(`Task ${id} moved from ${from} to ${to}`);
77
+ * reorderTasks(id, from, to);
78
+ * }
79
+ * });
80
+ *
81
+ * return (
82
+ * <PanGestureHandler {...panGestureHandler}>
83
+ * <Animated.View style={[styles.taskItem, animatedStyle]}>
84
+ * <Text style={[styles.taskText, isMoving && styles.dragging]}>
85
+ * {task.title}
86
+ * </Text>
87
+ * </Animated.View>
88
+ * </PanGestureHandler>
89
+ * );
90
+ * }
91
+ * ```
92
+ *
93
+ * @example
94
+ * Sortable item with drag handle:
95
+ * ```typescript
96
+ * import { useSortable } from './hooks/useSortable';
97
+ * import { SortableHandle } from './components/SortableItem';
98
+ *
99
+ * function TaskWithHandle({ task, ...sortableProps }) {
100
+ * const { animatedStyle, panGestureHandler, hasHandle } = useSortable({
101
+ * id: task.id,
102
+ * ...sortableProps,
103
+ * children: (
104
+ * <View style={styles.taskContent}>
105
+ * <Text>{task.title}</Text>
106
+ * <SortableHandle>
107
+ * <Icon name="drag-handle" />
108
+ * </SortableHandle>
109
+ * </View>
110
+ * )
111
+ * });
112
+ *
113
+ * return (
114
+ * <PanGestureHandler {...panGestureHandler}>
115
+ * <Animated.View style={[styles.taskItem, animatedStyle]}>
116
+ * <View style={styles.taskContent}>
117
+ * <Text>{task.title}</Text>
118
+ * <SortableHandle>
119
+ * <Icon name="drag-handle" />
120
+ * </SortableHandle>
121
+ * </View>
122
+ * </Animated.View>
123
+ * </PanGestureHandler>
124
+ * );
125
+ * }
126
+ * ```
127
+ *
128
+ * @example
129
+ * Sortable item with callbacks and state tracking:
130
+ * ```typescript
131
+ * function AdvancedSortableItem({ item, ...sortableProps }) {
132
+ * const [isDragging, setIsDragging] = useState(false);
133
+ *
134
+ * const { animatedStyle, panGestureHandler } = useSortable({
135
+ * id: item.id,
136
+ * ...sortableProps,
137
+ * onDragStart: (id, position) => {
138
+ * setIsDragging(true);
139
+ * analytics.track('drag_start', { itemId: id, position });
140
+ * },
141
+ * onDrop: (id, position) => {
142
+ * setIsDragging(false);
143
+ * analytics.track('drag_end', { itemId: id, position });
144
+ * },
145
+ * onDragging: (id, overItemId, yPosition) => {
146
+ * if (overItemId) {
147
+ * showDropPreview(overItemId);
148
+ * }
149
+ * }
150
+ * });
151
+ *
152
+ * return (
153
+ * <PanGestureHandler {...panGestureHandler}>
154
+ * <Animated.View
155
+ * style={[
156
+ * styles.item,
157
+ * animatedStyle,
158
+ * isDragging && styles.dragging
159
+ * ]}
160
+ * >
161
+ * <Text>{item.title}</Text>
162
+ * </Animated.View>
163
+ * </PanGestureHandler>
164
+ * );
165
+ * }
166
+ * ```
167
+ *
168
+ * @see {@link UseSortableOptions} for configuration options
169
+ * @see {@link UseSortableReturn} for return value details
170
+ * @see {@link useSortableList} for list-level management
171
+ * @see {@link SortableItem} for component implementation
172
+ * @see {@link Sortable} for high-level sortable list component
173
+ */
174
+ export function useSortable(options) {
175
+ const { id, positions, lowerBound, autoScrollDirection, itemsCount, itemHeight, containerHeight = 500, onMove, onDragStart, onDrop, onDragging, children, handleComponent, } = options;
176
+ const [isMoving, setIsMoving] = useState(false);
177
+ const [hasHandle, setHasHandle] = useState(false);
178
+ const movingSV = useSharedValue(false);
179
+ const currentOverItemId = useSharedValue(null);
180
+ const onDraggingLastCallTimestamp = useSharedValue(0);
181
+ const THROTTLE_INTERVAL = 50; // milliseconds
182
+ const positionY = useSharedValue(0);
183
+ const top = useSharedValue(0);
184
+ const targetLowerBound = useSharedValue(0);
185
+ useEffect(() => {
186
+ runOnUI(() => {
187
+ "worklet";
188
+ const initialTopVal = positions.value[id] * itemHeight;
189
+ const initialLowerBoundVal = lowerBound.value;
190
+ top.value = initialTopVal;
191
+ positionY.value = initialTopVal;
192
+ targetLowerBound.value = initialLowerBoundVal;
193
+ })();
194
+ // eslint-disable-next-line react-hooks/exhaustive-deps
195
+ }, []);
196
+ const calculatedContainerHeight = useRef(containerHeight).current;
197
+ const upperBound = useDerivedValue(() => lowerBound.value + calculatedContainerHeight);
198
+ useEffect(() => {
199
+ if (!children || !handleComponent) {
200
+ setHasHandle(false);
201
+ return;
202
+ }
203
+ const checkForHandle = (child) => {
204
+ if (React.isValidElement(child)) {
205
+ if (child.type === handleComponent) {
206
+ return true;
207
+ }
208
+ if (child.props && child.props.children) {
209
+ if (React.Children.toArray(child.props.children).some(checkForHandle)) {
210
+ return true;
211
+ }
212
+ }
213
+ }
214
+ return false;
215
+ };
216
+ setHasHandle(React.Children.toArray(children).some(checkForHandle));
217
+ }, [children, handleComponent]);
218
+ useAnimatedReaction(() => positionY.value, (currentY, previousY) => {
219
+ if (currentY === null || !movingSV.value) {
220
+ return;
221
+ }
222
+ if (previousY !== null && currentY === previousY) {
223
+ return;
224
+ }
225
+ // Calculate target discrete position
226
+ const clampedPosition = Math.min(Math.max(0, Math.ceil(currentY / itemHeight)), itemsCount - 1);
227
+ // Determine overItemId based on the current state of positions.value
228
+ // BEFORE setPosition modifies it for this specific currentY
229
+ let newOverItemId = null;
230
+ for (const [itemIdIter, itemPosIter] of Object.entries(positions.value)) {
231
+ if (itemPosIter === clampedPosition && itemIdIter !== id) {
232
+ newOverItemId = itemIdIter;
233
+ break;
234
+ }
235
+ }
236
+ if (currentOverItemId.value !== newOverItemId) {
237
+ currentOverItemId.value = newOverItemId;
238
+ }
239
+ if (onDragging) {
240
+ const now = Date.now();
241
+ if (now - onDraggingLastCallTimestamp.value > THROTTLE_INTERVAL) {
242
+ runOnJS(onDragging)(id, newOverItemId, Math.round(currentY));
243
+ onDraggingLastCallTimestamp.value = now;
244
+ }
245
+ }
246
+ // Update visual position and logical positions
247
+ top.value = currentY;
248
+ setPosition(currentY, itemsCount, positions, id, itemHeight);
249
+ setAutoScroll(currentY, lowerBound.value, upperBound.value, itemHeight, autoScrollDirection);
250
+ }, [
251
+ movingSV,
252
+ itemHeight,
253
+ itemsCount,
254
+ positions,
255
+ id,
256
+ onDragging,
257
+ lowerBound,
258
+ upperBound,
259
+ autoScrollDirection,
260
+ currentOverItemId,
261
+ top,
262
+ onDraggingLastCallTimestamp,
263
+ ]);
264
+ useAnimatedReaction(() => positions.value[id], (currentPosition, previousPosition) => {
265
+ if (currentPosition !== null &&
266
+ previousPosition !== null &&
267
+ currentPosition !== previousPosition) {
268
+ if (!movingSV.value) {
269
+ top.value = withSpring(currentPosition * itemHeight);
270
+ if (onMove) {
271
+ runOnJS(onMove)(id, previousPosition, currentPosition);
272
+ }
273
+ }
274
+ }
275
+ }, [movingSV]);
276
+ useAnimatedReaction(() => autoScrollDirection.value, (scrollDirection, previousValue) => {
277
+ if (scrollDirection !== null &&
278
+ previousValue !== null &&
279
+ scrollDirection !== previousValue) {
280
+ switch (scrollDirection) {
281
+ case ScrollDirection.Up: {
282
+ targetLowerBound.value = lowerBound.value;
283
+ targetLowerBound.value = withTiming(0, { duration: 1500 });
284
+ break;
285
+ }
286
+ case ScrollDirection.Down: {
287
+ const contentHeight = itemsCount * itemHeight;
288
+ const maxScroll = contentHeight - calculatedContainerHeight;
289
+ targetLowerBound.value = lowerBound.value;
290
+ targetLowerBound.value = withTiming(maxScroll, { duration: 1500 });
291
+ break;
292
+ }
293
+ case ScrollDirection.None: {
294
+ targetLowerBound.value = lowerBound.value;
295
+ break;
296
+ }
297
+ }
298
+ }
299
+ });
300
+ useAnimatedReaction(() => targetLowerBound.value, (targetLowerBoundValue, previousValue) => {
301
+ if (targetLowerBoundValue !== null &&
302
+ previousValue !== null &&
303
+ targetLowerBoundValue !== previousValue) {
304
+ if (movingSV.value) {
305
+ lowerBound.value = targetLowerBoundValue;
306
+ }
307
+ }
308
+ }, [movingSV]);
309
+ const panGestureHandler = useAnimatedGestureHandler({
310
+ onStart(event, ctx) {
311
+ "worklet";
312
+ ctx.initialItemContentY = positions.value[id] * itemHeight;
313
+ ctx.initialFingerAbsoluteY = event.absoluteY;
314
+ ctx.initialLowerBound = lowerBound.value;
315
+ positionY.value = ctx.initialItemContentY;
316
+ movingSV.value = true;
317
+ runOnJS(setIsMoving)(true);
318
+ if (onDragStart) {
319
+ runOnJS(onDragStart)(id, positions.value[id]);
320
+ }
321
+ },
322
+ onActive(event, ctx) {
323
+ "worklet";
324
+ const fingerDyScreen = event.absoluteY - ctx.initialFingerAbsoluteY;
325
+ const scrollDeltaSinceStart = lowerBound.value - ctx.initialLowerBound;
326
+ positionY.value =
327
+ ctx.initialItemContentY + fingerDyScreen + scrollDeltaSinceStart;
328
+ },
329
+ onFinish() {
330
+ "worklet";
331
+ const finishPosition = positions.value[id] * itemHeight;
332
+ top.value = withTiming(finishPosition);
333
+ movingSV.value = false;
334
+ runOnJS(setIsMoving)(false);
335
+ if (onDrop) {
336
+ runOnJS(onDrop)(id, positions.value[id]);
337
+ }
338
+ currentOverItemId.value = null;
339
+ },
340
+ });
341
+ const animatedStyle = useAnimatedStyle(() => {
342
+ "worklet";
343
+ return {
344
+ position: "absolute",
345
+ left: 0,
346
+ right: 0,
347
+ top: top.value,
348
+ zIndex: movingSV.value ? 1 : 0,
349
+ backgroundColor: "#000000",
350
+ shadowColor: "black",
351
+ shadowOpacity: withSpring(movingSV.value ? 0.2 : 0),
352
+ shadowRadius: 10,
353
+ };
354
+ }, [movingSV]);
355
+ return {
356
+ animatedStyle,
357
+ panGestureHandler,
358
+ isMoving,
359
+ hasHandle,
360
+ };
361
+ }
@@ -0,0 +1,182 @@
1
+ import { DropProviderRef } from "../types/context";
2
+ export interface UseSortableListOptions<TData> {
3
+ data: TData[];
4
+ itemHeight: number;
5
+ itemKeyExtractor?: (item: TData, index: number) => string;
6
+ }
7
+ export interface UseSortableListReturn<TData> {
8
+ positions: any;
9
+ scrollY: any;
10
+ autoScroll: any;
11
+ scrollViewRef: any;
12
+ dropProviderRef: React.RefObject<DropProviderRef>;
13
+ handleScroll: any;
14
+ handleScrollEnd: () => void;
15
+ contentHeight: number;
16
+ getItemProps: (item: TData, index: number) => {
17
+ id: string;
18
+ positions: any;
19
+ lowerBound: any;
20
+ autoScrollDirection: any;
21
+ itemsCount: number;
22
+ itemHeight: number;
23
+ };
24
+ }
25
+ /**
26
+ * A hook for managing sortable lists with drag-and-drop reordering capabilities.
27
+ *
28
+ * This hook provides the foundational state management and utilities needed to create
29
+ * sortable lists. It handles position tracking, scroll synchronization, auto-scrolling,
30
+ * and provides helper functions for individual sortable items.
31
+ *
32
+ * @template TData - The type of data items in the sortable list (must extend `{ id: string }`)
33
+ * @param options - Configuration options for the sortable list
34
+ * @returns Object containing shared values, refs, handlers, and utilities for the sortable list
35
+ *
36
+ * @example
37
+ * Basic sortable list setup:
38
+ * ```typescript
39
+ * import { useSortableList } from './hooks/useSortableList';
40
+ * import { SortableItem } from './components/SortableItem';
41
+ *
42
+ * interface Task {
43
+ * id: string;
44
+ * title: string;
45
+ * completed: boolean;
46
+ * }
47
+ *
48
+ * function TaskList() {
49
+ * const [tasks, setTasks] = useState<Task[]>([
50
+ * { id: '1', title: 'Learn React Native', completed: false },
51
+ * { id: '2', title: 'Build an app', completed: false },
52
+ * { id: '3', title: 'Deploy to store', completed: false }
53
+ * ]);
54
+ *
55
+ * const {
56
+ * scrollViewRef,
57
+ * dropProviderRef,
58
+ * handleScroll,
59
+ * handleScrollEnd,
60
+ * contentHeight,
61
+ * getItemProps,
62
+ * } = useSortableList({
63
+ * data: tasks,
64
+ * itemHeight: 60,
65
+ * });
66
+ *
67
+ * return (
68
+ * <GestureHandlerRootView style={styles.container}>
69
+ * <DropProvider ref={dropProviderRef}>
70
+ * <Animated.ScrollView
71
+ * ref={scrollViewRef}
72
+ * onScroll={handleScroll}
73
+ * scrollEventThrottle={16}
74
+ * style={styles.scrollView}
75
+ * contentContainerStyle={{ height: contentHeight }}
76
+ * onScrollEndDrag={handleScrollEnd}
77
+ * onMomentumScrollEnd={handleScrollEnd}
78
+ * >
79
+ * {tasks.map((task, index) => {
80
+ * const itemProps = getItemProps(task, index);
81
+ * return (
82
+ * <SortableItem key={task.id} {...itemProps}>
83
+ * <View style={styles.taskItem}>
84
+ * <Text>{task.title}</Text>
85
+ * </View>
86
+ * </SortableItem>
87
+ * );
88
+ * })}
89
+ * </Animated.ScrollView>
90
+ * </DropProvider>
91
+ * </GestureHandlerRootView>
92
+ * );
93
+ * }
94
+ * ```
95
+ *
96
+ * @example
97
+ * Sortable list with custom key extractor:
98
+ * ```typescript
99
+ * interface CustomItem {
100
+ * uuid: string;
101
+ * name: string;
102
+ * order: number;
103
+ * }
104
+ *
105
+ * function CustomSortableList() {
106
+ * const [items, setItems] = useState<CustomItem[]>(data);
107
+ *
108
+ * const sortableListProps = useSortableList({
109
+ * data: items,
110
+ * itemHeight: 50,
111
+ * itemKeyExtractor: (item) => item.uuid, // Use uuid instead of id
112
+ * });
113
+ *
114
+ * const { getItemProps, ...otherProps } = sortableListProps;
115
+ *
116
+ * return (
117
+ * <SortableListContainer {...otherProps}>
118
+ * {items.map((item, index) => {
119
+ * const itemProps = getItemProps(item, index);
120
+ * return (
121
+ * <SortableItem key={item.uuid} {...itemProps}>
122
+ * <View style={styles.customItem}>
123
+ * <Text>{item.name}</Text>
124
+ * <Text>Order: {item.order}</Text>
125
+ * </View>
126
+ * </SortableItem>
127
+ * );
128
+ * })}
129
+ * </SortableListContainer>
130
+ * );
131
+ * }
132
+ * ```
133
+ *
134
+ * @example
135
+ * Sortable list with reordering logic:
136
+ * ```typescript
137
+ * function ReorderableTaskList() {
138
+ * const [tasks, setTasks] = useState(initialTasks);
139
+ *
140
+ * const handleReorder = useCallback((id: string, from: number, to: number) => {
141
+ * setTasks(prevTasks => {
142
+ * const newTasks = [...prevTasks];
143
+ * const [movedTask] = newTasks.splice(from, 1);
144
+ * newTasks.splice(to, 0, movedTask);
145
+ * return newTasks;
146
+ * });
147
+ * }, []);
148
+ *
149
+ * const sortableProps = useSortableList({
150
+ * data: tasks,
151
+ * itemHeight: 80,
152
+ * });
153
+ *
154
+ * return (
155
+ * <SortableListContainer {...sortableProps}>
156
+ * {tasks.map((task, index) => {
157
+ * const itemProps = sortableProps.getItemProps(task, index);
158
+ * return (
159
+ * <SortableItem
160
+ * key={task.id}
161
+ * {...itemProps}
162
+ * onMove={handleReorder}
163
+ * >
164
+ * <TaskCard task={task} />
165
+ * </SortableItem>
166
+ * );
167
+ * })}
168
+ * </SortableListContainer>
169
+ * );
170
+ * }
171
+ * ```
172
+ *
173
+ * @see {@link UseSortableListOptions} for configuration options
174
+ * @see {@link UseSortableListReturn} for return value details
175
+ * @see {@link useSortable} for individual item management
176
+ * @see {@link SortableItem} for component implementation
177
+ * @see {@link Sortable} for high-level sortable list component
178
+ * @see {@link DropProvider} for drag-and-drop context
179
+ */
180
+ export declare function useSortableList<TData extends {
181
+ id: string;
182
+ }>(options: UseSortableListOptions<TData>): UseSortableListReturn<TData>;