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,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>;
|