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.
- package/LICENSE +21 -0
- package/README.md +633 -0
- package/lib/components/Draggable.d.ts +5 -0
- package/lib/components/Draggable.js +265 -0
- package/lib/components/Droppable.d.ts +264 -0
- package/lib/components/Droppable.js +284 -0
- package/lib/components/Sortable.d.ts +184 -0
- package/lib/components/Sortable.js +225 -0
- package/lib/components/SortableItem.d.ts +158 -0
- package/lib/components/SortableItem.js +251 -0
- package/lib/components/sortableUtils.d.ts +21 -0
- package/lib/components/sortableUtils.js +50 -0
- package/lib/context/DropContext.d.ts +118 -0
- package/lib/context/DropContext.js +233 -0
- package/lib/hooks/index.d.ts +4 -0
- package/lib/hooks/index.js +5 -0
- package/lib/hooks/useDraggable.d.ts +101 -0
- package/lib/hooks/useDraggable.js +567 -0
- package/lib/hooks/useDroppable.d.ts +129 -0
- package/lib/hooks/useDroppable.js +261 -0
- package/lib/hooks/useSortable.d.ts +174 -0
- package/lib/hooks/useSortable.js +361 -0
- package/lib/hooks/useSortableList.d.ts +182 -0
- package/lib/hooks/useSortableList.js +211 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.js +16 -0
- package/lib/types/context.d.ts +166 -0
- package/lib/types/context.js +80 -0
- package/lib/types/draggable.d.ts +313 -0
- package/lib/types/draggable.js +31 -0
- package/lib/types/droppable.d.ts +197 -0
- package/lib/types/droppable.js +1 -0
- package/lib/types/index.d.ts +4 -0
- package/lib/types/index.js +8 -0
- package/lib/types/sortable.d.ts +432 -0
- package/lib/types/sortable.js +6 -0
- package/package.json +59 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// context/DropContext.tsx
|
|
2
|
+
import React, { useRef, useState, useMemo, useCallback, forwardRef, useImperativeHandle, useEffect, } from "react";
|
|
3
|
+
import { SlotsContext, } from "../types/context";
|
|
4
|
+
/**
|
|
5
|
+
* Provider component that enables drag-and-drop functionality for its children.
|
|
6
|
+
*
|
|
7
|
+
* The DropProvider creates the context necessary for draggable and droppable components
|
|
8
|
+
* to communicate with each other. It manages the registration of drop zones, tracks
|
|
9
|
+
* active hover states, handles collision detection, and maintains the state of dropped items.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* Basic setup:
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { DropProvider } from './context/DropContext';
|
|
15
|
+
* import { Draggable, Droppable } from './components';
|
|
16
|
+
*
|
|
17
|
+
* function App() {
|
|
18
|
+
* return (
|
|
19
|
+
* <DropProvider>
|
|
20
|
+
* <View style={styles.container}>
|
|
21
|
+
* <Draggable data={{ id: '1', name: 'Item 1' }}>
|
|
22
|
+
* <Text>Drag me!</Text>
|
|
23
|
+
* </Draggable>
|
|
24
|
+
*
|
|
25
|
+
* <Droppable onDrop={(data) => console.log('Dropped:', data)}>
|
|
26
|
+
* <Text>Drop zone</Text>
|
|
27
|
+
* </Droppable>
|
|
28
|
+
* </View>
|
|
29
|
+
* </DropProvider>
|
|
30
|
+
* );
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* With callbacks and ref:
|
|
36
|
+
* ```typescript
|
|
37
|
+
* function AdvancedApp() {
|
|
38
|
+
* const dropProviderRef = useRef<DropProviderRef>(null);
|
|
39
|
+
* const [droppedItems, setDroppedItems] = useState({});
|
|
40
|
+
*
|
|
41
|
+
* const handleLayoutChange = () => {
|
|
42
|
+
* // Trigger position update after layout changes
|
|
43
|
+
* dropProviderRef.current?.requestPositionUpdate();
|
|
44
|
+
* };
|
|
45
|
+
*
|
|
46
|
+
* return (
|
|
47
|
+
* <DropProvider
|
|
48
|
+
* ref={dropProviderRef}
|
|
49
|
+
* onDroppedItemsUpdate={setDroppedItems}
|
|
50
|
+
* onDragStart={(data) => console.log('Drag started:', data)}
|
|
51
|
+
* onDragEnd={(data) => console.log('Drag ended:', data)}
|
|
52
|
+
* onDragging={({ x, y, itemData }) => {
|
|
53
|
+
* console.log(`${itemData.name} at (${x}, ${y})`);
|
|
54
|
+
* }}
|
|
55
|
+
* >
|
|
56
|
+
* <ScrollView onLayout={handleLayoutChange}>
|
|
57
|
+
* {/* Your draggable and droppable components *\/}
|
|
58
|
+
* </ScrollView>
|
|
59
|
+
* </DropProvider>
|
|
60
|
+
* );
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* Multiple drop zones with capacity:
|
|
66
|
+
* ```typescript
|
|
67
|
+
* function TaskBoard() {
|
|
68
|
+
* const [tasks, setTasks] = useState(initialTasks);
|
|
69
|
+
*
|
|
70
|
+
* return (
|
|
71
|
+
* <DropProvider
|
|
72
|
+
* onDroppedItemsUpdate={(dropped) => {
|
|
73
|
+
* // Update task positions based on drops
|
|
74
|
+
* updateTaskPositions(dropped);
|
|
75
|
+
* }}
|
|
76
|
+
* >
|
|
77
|
+
* <View style={styles.board}>
|
|
78
|
+
* {tasks.map(task => (
|
|
79
|
+
* <Draggable key={task.id} data={task}>
|
|
80
|
+
* <TaskCard task={task} />
|
|
81
|
+
* </Draggable>
|
|
82
|
+
* ))}
|
|
83
|
+
*
|
|
84
|
+
* <Droppable
|
|
85
|
+
* droppableId="todo"
|
|
86
|
+
* capacity={10}
|
|
87
|
+
* onDrop={(task) => moveTask(task.id, 'todo')}
|
|
88
|
+
* >
|
|
89
|
+
* <Column title="To Do" />
|
|
90
|
+
* </Droppable>
|
|
91
|
+
*
|
|
92
|
+
* <Droppable
|
|
93
|
+
* droppableId="in-progress"
|
|
94
|
+
* capacity={5}
|
|
95
|
+
* onDrop={(task) => moveTask(task.id, 'in-progress')}
|
|
96
|
+
* >
|
|
97
|
+
* <Column title="In Progress" />
|
|
98
|
+
* </Droppable>
|
|
99
|
+
*
|
|
100
|
+
* <Droppable
|
|
101
|
+
* droppableId="done"
|
|
102
|
+
* onDrop={(task) => moveTask(task.id, 'done')}
|
|
103
|
+
* >
|
|
104
|
+
* <Column title="Done" />
|
|
105
|
+
* </Droppable>
|
|
106
|
+
* </View>
|
|
107
|
+
* </DropProvider>
|
|
108
|
+
* );
|
|
109
|
+
* }
|
|
110
|
+
* ```
|
|
111
|
+
*
|
|
112
|
+
* @see {@link Draggable} for draggable components
|
|
113
|
+
* @see {@link Droppable} for droppable components
|
|
114
|
+
* @see {@link useDraggable} for draggable hook
|
|
115
|
+
* @see {@link useDroppable} for droppable hook
|
|
116
|
+
* @see {@link DropProviderRef} for imperative handle interface
|
|
117
|
+
* @see {@link DroppedItemsMap} for dropped items data structure
|
|
118
|
+
*/
|
|
119
|
+
// The DropProvider component, now forwardRef
|
|
120
|
+
export const DropProvider = forwardRef(({ children, onLayoutUpdateComplete, onDroppedItemsUpdate, onDragging, onDragStart, onDragEnd, }, ref) => {
|
|
121
|
+
const slotsRef = useRef({});
|
|
122
|
+
const [activeHoverSlotId, setActiveHoverSlotIdState] = useState(null);
|
|
123
|
+
// New state for tracking dropped items
|
|
124
|
+
const [droppedItems, setDroppedItems] = useState({});
|
|
125
|
+
const positionUpdateListenersRef = useRef({});
|
|
126
|
+
const registerPositionUpdateListener = useCallback((id, listener) => {
|
|
127
|
+
positionUpdateListenersRef.current[id] = listener;
|
|
128
|
+
}, []);
|
|
129
|
+
const unregisterPositionUpdateListener = useCallback((id) => {
|
|
130
|
+
delete positionUpdateListenersRef.current[id];
|
|
131
|
+
}, []);
|
|
132
|
+
// Call the update callback whenever droppedItems changes
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (onDroppedItemsUpdate) {
|
|
135
|
+
onDroppedItemsUpdate(droppedItems);
|
|
136
|
+
}
|
|
137
|
+
}, [droppedItems, onDroppedItemsUpdate]);
|
|
138
|
+
// Update method to use string IDs
|
|
139
|
+
const registerDroppedItem = useCallback((draggableId, droppableId, itemData) => {
|
|
140
|
+
setDroppedItems((prev) => ({
|
|
141
|
+
...prev,
|
|
142
|
+
[draggableId]: {
|
|
143
|
+
droppableId,
|
|
144
|
+
data: itemData,
|
|
145
|
+
},
|
|
146
|
+
}));
|
|
147
|
+
}, []);
|
|
148
|
+
const unregisterDroppedItem = useCallback((draggableId) => {
|
|
149
|
+
setDroppedItems((prev) => {
|
|
150
|
+
const newItems = { ...prev };
|
|
151
|
+
delete newItems[draggableId];
|
|
152
|
+
return newItems;
|
|
153
|
+
});
|
|
154
|
+
}, []);
|
|
155
|
+
const getDroppedItems = useCallback(() => {
|
|
156
|
+
return droppedItems;
|
|
157
|
+
}, [droppedItems]);
|
|
158
|
+
// This is the actual function that does the work
|
|
159
|
+
const internalRequestPositionUpdate = useCallback(() => {
|
|
160
|
+
const listeners = positionUpdateListenersRef.current;
|
|
161
|
+
Object.values(listeners).forEach((listener) => {
|
|
162
|
+
listener();
|
|
163
|
+
});
|
|
164
|
+
onLayoutUpdateComplete === null || onLayoutUpdateComplete === void 0 ? void 0 : onLayoutUpdateComplete();
|
|
165
|
+
}, [onLayoutUpdateComplete]);
|
|
166
|
+
// Expose requestPositionUpdate and getDroppedItems via ref
|
|
167
|
+
useImperativeHandle(ref, () => ({
|
|
168
|
+
requestPositionUpdate: internalRequestPositionUpdate,
|
|
169
|
+
getDroppedItems,
|
|
170
|
+
}));
|
|
171
|
+
// Add a method to check if a droppable has capacity available
|
|
172
|
+
const hasAvailableCapacity = useCallback((droppableId) => {
|
|
173
|
+
// Find all draggables currently dropped on this droppable
|
|
174
|
+
const droppedCount = Object.values(droppedItems).filter((item) => item.droppableId === droppableId).length;
|
|
175
|
+
// Find the droppable's registered capacity
|
|
176
|
+
const droppableSlot = Object.values(slotsRef.current).find((slot) => slot.id === droppableId);
|
|
177
|
+
if (!droppableSlot)
|
|
178
|
+
return false; // Not found or not registered
|
|
179
|
+
// Use the slot's capacity if specified, default to 1
|
|
180
|
+
const capacity = droppableSlot.capacity !== undefined ? droppableSlot.capacity : 1;
|
|
181
|
+
// Check if more capacity is available
|
|
182
|
+
return droppedCount < capacity;
|
|
183
|
+
}, [droppedItems]);
|
|
184
|
+
// Create a wrapper for onDragStart that also triggers position update
|
|
185
|
+
const handleDragStart = useCallback((data) => {
|
|
186
|
+
if (onDragStart) {
|
|
187
|
+
onDragStart(data);
|
|
188
|
+
}
|
|
189
|
+
internalRequestPositionUpdate();
|
|
190
|
+
}, [onDragStart, internalRequestPositionUpdate]);
|
|
191
|
+
// Update the context value with the new method
|
|
192
|
+
const contextValue = useMemo(() => ({
|
|
193
|
+
register: (id, slot) => {
|
|
194
|
+
slotsRef.current[id] = slot;
|
|
195
|
+
},
|
|
196
|
+
unregister: (id) => {
|
|
197
|
+
delete slotsRef.current[id];
|
|
198
|
+
},
|
|
199
|
+
isRegistered: (id) => {
|
|
200
|
+
return slotsRef.current[id] !== undefined;
|
|
201
|
+
},
|
|
202
|
+
getSlots: () => slotsRef.current,
|
|
203
|
+
setActiveHoverSlot: (id) => setActiveHoverSlotIdState(id),
|
|
204
|
+
activeHoverSlotId,
|
|
205
|
+
registerPositionUpdateListener,
|
|
206
|
+
unregisterPositionUpdateListener,
|
|
207
|
+
requestPositionUpdate: internalRequestPositionUpdate,
|
|
208
|
+
registerDroppedItem,
|
|
209
|
+
unregisterDroppedItem,
|
|
210
|
+
getDroppedItems,
|
|
211
|
+
hasAvailableCapacity,
|
|
212
|
+
onDragging,
|
|
213
|
+
onDragStart: handleDragStart,
|
|
214
|
+
onDragEnd,
|
|
215
|
+
}), [
|
|
216
|
+
activeHoverSlotId,
|
|
217
|
+
registerPositionUpdateListener,
|
|
218
|
+
unregisterPositionUpdateListener,
|
|
219
|
+
internalRequestPositionUpdate,
|
|
220
|
+
registerDroppedItem,
|
|
221
|
+
unregisterDroppedItem,
|
|
222
|
+
getDroppedItems,
|
|
223
|
+
hasAvailableCapacity,
|
|
224
|
+
onDragging,
|
|
225
|
+
handleDragStart,
|
|
226
|
+
onDragEnd,
|
|
227
|
+
]);
|
|
228
|
+
return (<SlotsContext.Provider value={contextValue}>
|
|
229
|
+
{children}
|
|
230
|
+
</SlotsContext.Provider>);
|
|
231
|
+
});
|
|
232
|
+
// Adding a display name for better debugging in React DevTools
|
|
233
|
+
DropProvider.displayName = "DropProvider";
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { UseDraggableOptions, UseDraggableReturn } from "../types/draggable";
|
|
2
|
+
/**
|
|
3
|
+
* A powerful hook for creating draggable components with advanced features like
|
|
4
|
+
* collision detection, bounded dragging, axis constraints, and custom animations.
|
|
5
|
+
*
|
|
6
|
+
* This hook provides the core functionality for drag-and-drop interactions,
|
|
7
|
+
* handling gesture recognition, position tracking, collision detection with drop zones,
|
|
8
|
+
* and smooth animations.
|
|
9
|
+
*
|
|
10
|
+
* @template TData - The type of data associated with the draggable item
|
|
11
|
+
* @param options - Configuration options for the draggable behavior
|
|
12
|
+
* @returns Object containing props, gesture handlers, and state for the draggable component
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* Basic draggable component:
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { useDraggable } from './hooks/useDraggable';
|
|
18
|
+
*
|
|
19
|
+
* function MyDraggable() {
|
|
20
|
+
* const { animatedViewProps, gesture, state } = useDraggable({
|
|
21
|
+
* data: { id: '1', name: 'Draggable Item' },
|
|
22
|
+
* onDragStart: (data) => console.log('Started dragging:', data.name),
|
|
23
|
+
* onDragEnd: (data) => console.log('Finished dragging:', data.name),
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* return (
|
|
27
|
+
* <GestureDetector gesture={gesture}>
|
|
28
|
+
* <Animated.View {...animatedViewProps}>
|
|
29
|
+
* <Text>Drag me!</Text>
|
|
30
|
+
* </Animated.View>
|
|
31
|
+
* </GestureDetector>
|
|
32
|
+
* );
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* Draggable with custom animation and bounds:
|
|
38
|
+
* ```typescript
|
|
39
|
+
* function BoundedDraggable() {
|
|
40
|
+
* const boundsRef = useRef<View>(null);
|
|
41
|
+
*
|
|
42
|
+
* const { animatedViewProps, gesture } = useDraggable({
|
|
43
|
+
* data: { id: '2', type: 'bounded' },
|
|
44
|
+
* dragBoundsRef: boundsRef,
|
|
45
|
+
* dragAxis: 'x', // Only horizontal movement
|
|
46
|
+
* animationFunction: (toValue) => {
|
|
47
|
+
* 'worklet';
|
|
48
|
+
* return withTiming(toValue, { duration: 300 });
|
|
49
|
+
* },
|
|
50
|
+
* collisionAlgorithm: 'center',
|
|
51
|
+
* });
|
|
52
|
+
*
|
|
53
|
+
* return (
|
|
54
|
+
* <View ref={boundsRef} style={styles.container}>
|
|
55
|
+
* <GestureDetector gesture={gesture}>
|
|
56
|
+
* <Animated.View {...animatedViewProps}>
|
|
57
|
+
* <Text>Bounded horizontal draggable</Text>
|
|
58
|
+
* </Animated.View>
|
|
59
|
+
* </GestureDetector>
|
|
60
|
+
* </View>
|
|
61
|
+
* );
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* Draggable with state tracking:
|
|
67
|
+
* ```typescript
|
|
68
|
+
* function StatefulDraggable() {
|
|
69
|
+
* const [dragState, setDragState] = useState(DraggableState.IDLE);
|
|
70
|
+
*
|
|
71
|
+
* const { animatedViewProps, gesture } = useDraggable({
|
|
72
|
+
* data: { id: '3', status: 'active' },
|
|
73
|
+
* onStateChange: setDragState,
|
|
74
|
+
* onDragging: ({ x, y, tx, ty }) => {
|
|
75
|
+
* console.log(`Position: (${x + tx}, ${y + ty})`);
|
|
76
|
+
* },
|
|
77
|
+
* });
|
|
78
|
+
*
|
|
79
|
+
* return (
|
|
80
|
+
* <GestureDetector gesture={gesture}>
|
|
81
|
+
* <Animated.View
|
|
82
|
+
* {...animatedViewProps}
|
|
83
|
+
* style={[
|
|
84
|
+
* animatedViewProps.style,
|
|
85
|
+
* { opacity: dragState === DraggableState.DRAGGING ? 0.7 : 1 }
|
|
86
|
+
* ]}
|
|
87
|
+
* >
|
|
88
|
+
* <Text>State: {dragState}</Text>
|
|
89
|
+
* </Animated.View>
|
|
90
|
+
* </GestureDetector>
|
|
91
|
+
* );
|
|
92
|
+
* }
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* @see {@link DraggableState} for state management
|
|
96
|
+
* @see {@link CollisionAlgorithm} for collision detection options
|
|
97
|
+
* @see {@link AnimationFunction} for custom animations
|
|
98
|
+
* @see {@link UseDraggableOptions} for configuration options
|
|
99
|
+
* @see {@link UseDraggableReturn} for return value details
|
|
100
|
+
*/
|
|
101
|
+
export declare const useDraggable: <TData = unknown>(options: UseDraggableOptions<TData>) => UseDraggableReturn;
|