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,284 @@
|
|
|
1
|
+
// Node Modules
|
|
2
|
+
import React from "react";
|
|
3
|
+
import Animated from "react-native-reanimated";
|
|
4
|
+
import { useDroppable } from "../hooks/useDroppable";
|
|
5
|
+
let _nextDroppableId = 1;
|
|
6
|
+
export const _getUniqueDroppableId = () => {
|
|
7
|
+
return _nextDroppableId++;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* A component that creates drop zones for receiving draggable items.
|
|
11
|
+
*
|
|
12
|
+
* The Droppable component provides visual feedback when draggable items hover over it
|
|
13
|
+
* and handles the drop logic when items are released. It integrates seamlessly with
|
|
14
|
+
* the drag-and-drop context to provide collision detection and proper positioning
|
|
15
|
+
* of dropped items.
|
|
16
|
+
*
|
|
17
|
+
* @template TData - The type of data that can be dropped on this droppable
|
|
18
|
+
* @param props - Configuration props for the droppable component
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* Basic drop zone:
|
|
22
|
+
* ```typescript
|
|
23
|
+
* import { Droppable } from './components/Droppable';
|
|
24
|
+
*
|
|
25
|
+
* function BasicDropZone() {
|
|
26
|
+
* const handleDrop = (data) => {
|
|
27
|
+
* console.log('Item dropped:', data);
|
|
28
|
+
* // Handle the dropped item
|
|
29
|
+
* addItemToList(data);
|
|
30
|
+
* };
|
|
31
|
+
*
|
|
32
|
+
* return (
|
|
33
|
+
* <Droppable onDrop={handleDrop}>
|
|
34
|
+
* <View style={styles.dropZone}>
|
|
35
|
+
* <Text>Drop items here</Text>
|
|
36
|
+
* </View>
|
|
37
|
+
* </Droppable>
|
|
38
|
+
* );
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* Drop zone with visual feedback:
|
|
44
|
+
* ```typescript
|
|
45
|
+
* function VisualDropZone() {
|
|
46
|
+
* const [isHovered, setIsHovered] = useState(false);
|
|
47
|
+
*
|
|
48
|
+
* return (
|
|
49
|
+
* <Droppable
|
|
50
|
+
* onDrop={(data) => {
|
|
51
|
+
* console.log('Dropped:', data.name);
|
|
52
|
+
* processDroppedItem(data);
|
|
53
|
+
* }}
|
|
54
|
+
* onActiveChange={setIsHovered}
|
|
55
|
+
* activeStyle={{
|
|
56
|
+
* backgroundColor: 'rgba(0, 255, 0, 0.2)',
|
|
57
|
+
* borderColor: '#00ff00',
|
|
58
|
+
* borderWidth: 2,
|
|
59
|
+
* transform: [{ scale: 1.05 }]
|
|
60
|
+
* }}
|
|
61
|
+
* style={styles.dropZone}
|
|
62
|
+
* >
|
|
63
|
+
* <View style={[
|
|
64
|
+
* styles.dropContent,
|
|
65
|
+
* isHovered && styles.hoveredContent
|
|
66
|
+
* ]}>
|
|
67
|
+
* <Icon
|
|
68
|
+
* name="cloud-upload"
|
|
69
|
+
* size={32}
|
|
70
|
+
* color={isHovered ? '#00ff00' : '#666'}
|
|
71
|
+
* />
|
|
72
|
+
* <Text style={styles.dropText}>
|
|
73
|
+
* {isHovered ? 'Release to drop' : 'Drag files here'}
|
|
74
|
+
* </Text>
|
|
75
|
+
* </View>
|
|
76
|
+
* </Droppable>
|
|
77
|
+
* );
|
|
78
|
+
* }
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* Drop zone with custom alignment and capacity:
|
|
83
|
+
* ```typescript
|
|
84
|
+
* function TaskColumn() {
|
|
85
|
+
* const [tasks, setTasks] = useState([]);
|
|
86
|
+
* const maxTasks = 5;
|
|
87
|
+
*
|
|
88
|
+
* return (
|
|
89
|
+
* <Droppable
|
|
90
|
+
* droppableId="todo-column"
|
|
91
|
+
* onDrop={(task) => {
|
|
92
|
+
* if (tasks.length < maxTasks) {
|
|
93
|
+
* setTasks(prev => [...prev, task]);
|
|
94
|
+
* updateTaskStatus(task.id, 'todo');
|
|
95
|
+
* }
|
|
96
|
+
* }}
|
|
97
|
+
* dropAlignment="top-center"
|
|
98
|
+
* dropOffset={{ x: 0, y: 10 }}
|
|
99
|
+
* capacity={maxTasks}
|
|
100
|
+
* activeStyle={{
|
|
101
|
+
* backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
102
|
+
* borderColor: '#3b82f6',
|
|
103
|
+
* borderWidth: 2,
|
|
104
|
+
* borderStyle: 'dashed'
|
|
105
|
+
* }}
|
|
106
|
+
* style={styles.column}
|
|
107
|
+
* >
|
|
108
|
+
* <Text style={styles.columnTitle}>
|
|
109
|
+
* To Do ({tasks.length}/{maxTasks})
|
|
110
|
+
* </Text>
|
|
111
|
+
*
|
|
112
|
+
* {tasks.map(task => (
|
|
113
|
+
* <TaskCard key={task.id} task={task} />
|
|
114
|
+
* ))}
|
|
115
|
+
*
|
|
116
|
+
* {tasks.length === 0 && (
|
|
117
|
+
* <Text style={styles.emptyText}>
|
|
118
|
+
* Drop tasks here
|
|
119
|
+
* </Text>
|
|
120
|
+
* )}
|
|
121
|
+
* </Droppable>
|
|
122
|
+
* );
|
|
123
|
+
* }
|
|
124
|
+
* ```
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* Conditional drop zone with validation:
|
|
128
|
+
* ```typescript
|
|
129
|
+
* function RestrictedDropZone() {
|
|
130
|
+
* const [canAcceptFiles, setCanAcceptFiles] = useState(true);
|
|
131
|
+
* const [uploadProgress, setUploadProgress] = useState(0);
|
|
132
|
+
*
|
|
133
|
+
* const handleDrop = (fileData) => {
|
|
134
|
+
* // Validate file type and size
|
|
135
|
+
* if (fileData.type !== 'image') {
|
|
136
|
+
* showError('Only image files are allowed');
|
|
137
|
+
* return;
|
|
138
|
+
* }
|
|
139
|
+
*
|
|
140
|
+
* if (fileData.size > 5000000) { // 5MB limit
|
|
141
|
+
* showError('File size must be under 5MB');
|
|
142
|
+
* return;
|
|
143
|
+
* }
|
|
144
|
+
*
|
|
145
|
+
* // Start upload
|
|
146
|
+
* setCanAcceptFiles(false);
|
|
147
|
+
* uploadFile(fileData, setUploadProgress)
|
|
148
|
+
* .then(() => {
|
|
149
|
+
* showSuccess('File uploaded successfully');
|
|
150
|
+
* setCanAcceptFiles(true);
|
|
151
|
+
* setUploadProgress(0);
|
|
152
|
+
* })
|
|
153
|
+
* .catch(() => {
|
|
154
|
+
* showError('Upload failed');
|
|
155
|
+
* setCanAcceptFiles(true);
|
|
156
|
+
* setUploadProgress(0);
|
|
157
|
+
* });
|
|
158
|
+
* };
|
|
159
|
+
*
|
|
160
|
+
* return (
|
|
161
|
+
* <Droppable
|
|
162
|
+
* onDrop={handleDrop}
|
|
163
|
+
* dropDisabled={!canAcceptFiles}
|
|
164
|
+
* onActiveChange={(active) => {
|
|
165
|
+
* if (active && !canAcceptFiles) {
|
|
166
|
+
* showTooltip('Upload in progress...');
|
|
167
|
+
* }
|
|
168
|
+
* }}
|
|
169
|
+
* activeStyle={{
|
|
170
|
+
* backgroundColor: canAcceptFiles
|
|
171
|
+
* ? 'rgba(34, 197, 94, 0.1)'
|
|
172
|
+
* : 'rgba(239, 68, 68, 0.1)',
|
|
173
|
+
* borderColor: canAcceptFiles ? '#22c55e' : '#ef4444'
|
|
174
|
+
* }}
|
|
175
|
+
* style={[
|
|
176
|
+
* styles.uploadZone,
|
|
177
|
+
* !canAcceptFiles && styles.disabled
|
|
178
|
+
* ]}
|
|
179
|
+
* >
|
|
180
|
+
* <View style={styles.uploadContent}>
|
|
181
|
+
* {uploadProgress > 0 ? (
|
|
182
|
+
* <>
|
|
183
|
+
* <ProgressBar progress={uploadProgress} />
|
|
184
|
+
* <Text>Uploading... {Math.round(uploadProgress * 100)}%</Text>
|
|
185
|
+
* </>
|
|
186
|
+
* ) : (
|
|
187
|
+
* <>
|
|
188
|
+
* <Icon
|
|
189
|
+
* name="image"
|
|
190
|
+
* size={48}
|
|
191
|
+
* color={canAcceptFiles ? '#22c55e' : '#ef4444'}
|
|
192
|
+
* />
|
|
193
|
+
* <Text style={styles.uploadText}>
|
|
194
|
+
* {canAcceptFiles
|
|
195
|
+
* ? 'Drop images here (max 5MB)'
|
|
196
|
+
* : 'Upload in progress...'}
|
|
197
|
+
* </Text>
|
|
198
|
+
* </>
|
|
199
|
+
* )}
|
|
200
|
+
* </View>
|
|
201
|
+
* </Droppable>
|
|
202
|
+
* );
|
|
203
|
+
* }
|
|
204
|
+
* ```
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* Multiple drop zones with different behaviors:
|
|
208
|
+
* ```typescript
|
|
209
|
+
* function MultiDropZoneExample() {
|
|
210
|
+
* const [items, setItems] = useState([]);
|
|
211
|
+
* const [trash, setTrash] = useState([]);
|
|
212
|
+
*
|
|
213
|
+
* return (
|
|
214
|
+
* <DropProvider>
|
|
215
|
+
* <View style={styles.container}>
|
|
216
|
+
* {/* Source items *\/}
|
|
217
|
+
* {items.map(item => (
|
|
218
|
+
* <Draggable key={item.id} data={item}>
|
|
219
|
+
* <ItemCard item={item} />
|
|
220
|
+
* </Draggable>
|
|
221
|
+
* ))}
|
|
222
|
+
*
|
|
223
|
+
* {/* Archive drop zone *\/}
|
|
224
|
+
* <Droppable
|
|
225
|
+
* droppableId="archive"
|
|
226
|
+
* onDrop={(item) => {
|
|
227
|
+
* archiveItem(item.id);
|
|
228
|
+
* setItems(prev => prev.filter(i => i.id !== item.id));
|
|
229
|
+
* }}
|
|
230
|
+
* dropAlignment="center"
|
|
231
|
+
* activeStyle={styles.archiveActive}
|
|
232
|
+
* >
|
|
233
|
+
* <View style={styles.archiveZone}>
|
|
234
|
+
* <Icon name="archive" size={24} />
|
|
235
|
+
* <Text>Archive</Text>
|
|
236
|
+
* </View>
|
|
237
|
+
* </Droppable>
|
|
238
|
+
*
|
|
239
|
+
* {/* Trash drop zone *\/}
|
|
240
|
+
* <Droppable
|
|
241
|
+
* droppableId="trash"
|
|
242
|
+
* onDrop={(item) => {
|
|
243
|
+
* setTrash(prev => [...prev, item]);
|
|
244
|
+
* setItems(prev => prev.filter(i => i.id !== item.id));
|
|
245
|
+
* }}
|
|
246
|
+
* dropAlignment="center"
|
|
247
|
+
* activeStyle={styles.trashActive}
|
|
248
|
+
* capacity={10} // Max 10 items in trash
|
|
249
|
+
* >
|
|
250
|
+
* <View style={styles.trashZone}>
|
|
251
|
+
* <Icon name="trash" size={24} />
|
|
252
|
+
* <Text>Trash ({trash.length}/10)</Text>
|
|
253
|
+
* </View>
|
|
254
|
+
* </Droppable>
|
|
255
|
+
* </View>
|
|
256
|
+
* </DropProvider>
|
|
257
|
+
* );
|
|
258
|
+
* }
|
|
259
|
+
* ```
|
|
260
|
+
*
|
|
261
|
+
* @see {@link useDroppable} for the underlying hook
|
|
262
|
+
* @see {@link Draggable} for draggable components
|
|
263
|
+
* @see {@link DropAlignment} for alignment options
|
|
264
|
+
* @see {@link DropOffset} for offset configuration
|
|
265
|
+
* @see {@link UseDroppableOptions} for configuration options
|
|
266
|
+
* @see {@link UseDroppableReturn} for hook return details
|
|
267
|
+
* @see {@link DropProvider} for drag-and-drop context setup
|
|
268
|
+
*/
|
|
269
|
+
export const Droppable = ({ onDrop, dropDisabled, onActiveChange, dropAlignment, dropOffset, activeStyle, droppableId, capacity, style, children, }) => {
|
|
270
|
+
const { viewProps, animatedViewRef } = useDroppable({
|
|
271
|
+
onDrop,
|
|
272
|
+
dropDisabled,
|
|
273
|
+
onActiveChange,
|
|
274
|
+
dropAlignment,
|
|
275
|
+
dropOffset,
|
|
276
|
+
activeStyle,
|
|
277
|
+
droppableId,
|
|
278
|
+
capacity,
|
|
279
|
+
});
|
|
280
|
+
// The style is now fully handled in the hook and returned via viewProps.style
|
|
281
|
+
return (<Animated.View ref={animatedViewRef} {...viewProps} style={[style, viewProps.style]} collapsable={false}>
|
|
282
|
+
{children}
|
|
283
|
+
</Animated.View>);
|
|
284
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { SortableProps } from "../types/sortable";
|
|
3
|
+
/**
|
|
4
|
+
* A high-level component for creating sortable lists with smooth reordering animations.
|
|
5
|
+
*
|
|
6
|
+
* The Sortable component provides a complete solution for sortable lists, handling
|
|
7
|
+
* all the complex state management, gesture handling, and animations internally.
|
|
8
|
+
* It renders a scrollable list where items can be dragged to reorder them with
|
|
9
|
+
* smooth animations and auto-scrolling support.
|
|
10
|
+
*
|
|
11
|
+
* @template TData - The type of data items in the list (must extend `{ id: string }`)
|
|
12
|
+
* @param props - Configuration props for the sortable list
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* Basic sortable list:
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { Sortable } from './components/Sortable';
|
|
18
|
+
*
|
|
19
|
+
* interface Task {
|
|
20
|
+
* id: string;
|
|
21
|
+
* title: string;
|
|
22
|
+
* completed: boolean;
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* function TaskList() {
|
|
26
|
+
* const [tasks, setTasks] = useState<Task[]>([
|
|
27
|
+
* { id: '1', title: 'Learn React Native', completed: false },
|
|
28
|
+
* { id: '2', title: 'Build an app', completed: false },
|
|
29
|
+
* { id: '3', title: 'Deploy to store', completed: false }
|
|
30
|
+
* ]);
|
|
31
|
+
*
|
|
32
|
+
* const renderTask = ({ item, id, positions, ...props }) => (
|
|
33
|
+
* <SortableItem key={id} id={id} positions={positions} {...props}>
|
|
34
|
+
* <View style={styles.taskItem}>
|
|
35
|
+
* <Text>{item.title}</Text>
|
|
36
|
+
* <Text>{item.completed ? '✓' : '○'}</Text>
|
|
37
|
+
* </View>
|
|
38
|
+
* </SortableItem>
|
|
39
|
+
* );
|
|
40
|
+
*
|
|
41
|
+
* return (
|
|
42
|
+
* <Sortable
|
|
43
|
+
* data={tasks}
|
|
44
|
+
* renderItem={renderTask}
|
|
45
|
+
* itemHeight={60}
|
|
46
|
+
* style={styles.list}
|
|
47
|
+
* />
|
|
48
|
+
* );
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* Sortable list with custom styling and callbacks:
|
|
54
|
+
* ```typescript
|
|
55
|
+
* function AdvancedTaskList() {
|
|
56
|
+
* const [tasks, setTasks] = useState(initialTasks);
|
|
57
|
+
*
|
|
58
|
+
* const renderTask = ({ item, id, positions, ...props }) => (
|
|
59
|
+
* <SortableItem
|
|
60
|
+
* key={id}
|
|
61
|
+
* id={id}
|
|
62
|
+
* positions={positions}
|
|
63
|
+
* {...props}
|
|
64
|
+
* onMove={(itemId, from, to) => {
|
|
65
|
+
* // Update data when items are reordered
|
|
66
|
+
* const newTasks = [...tasks];
|
|
67
|
+
* const [movedTask] = newTasks.splice(from, 1);
|
|
68
|
+
* newTasks.splice(to, 0, movedTask);
|
|
69
|
+
* setTasks(newTasks);
|
|
70
|
+
*
|
|
71
|
+
* // Analytics
|
|
72
|
+
* analytics.track('task_reordered', { taskId: itemId, from, to });
|
|
73
|
+
* }}
|
|
74
|
+
* onDragStart={(itemId) => {
|
|
75
|
+
* hapticFeedback();
|
|
76
|
+
* setDraggingTask(itemId);
|
|
77
|
+
* }}
|
|
78
|
+
* onDrop={(itemId) => {
|
|
79
|
+
* setDraggingTask(null);
|
|
80
|
+
* }}
|
|
81
|
+
* >
|
|
82
|
+
* <Animated.View style={[styles.taskItem, item.priority === 'high' && styles.highPriority]}>
|
|
83
|
+
* <Text style={styles.taskTitle}>{item.title}</Text>
|
|
84
|
+
* <Text style={styles.taskDue}>{item.dueDate}</Text>
|
|
85
|
+
* <View style={styles.dragHandle}>
|
|
86
|
+
* <Icon name="drag-handle" size={20} color="#666" />
|
|
87
|
+
* </View>
|
|
88
|
+
* </Animated.View>
|
|
89
|
+
* </SortableItem>
|
|
90
|
+
* );
|
|
91
|
+
*
|
|
92
|
+
* return (
|
|
93
|
+
* <View style={styles.container}>
|
|
94
|
+
* <Text style={styles.header}>My Tasks ({tasks.length})</Text>
|
|
95
|
+
* <Sortable
|
|
96
|
+
* data={tasks}
|
|
97
|
+
* renderItem={renderTask}
|
|
98
|
+
* itemHeight={80}
|
|
99
|
+
* style={styles.sortableList}
|
|
100
|
+
* contentContainerStyle={styles.listContent}
|
|
101
|
+
* />
|
|
102
|
+
* </View>
|
|
103
|
+
* );
|
|
104
|
+
* }
|
|
105
|
+
* ```
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* Sortable list with drag handles:
|
|
109
|
+
* ```typescript
|
|
110
|
+
* function SortableWithHandles() {
|
|
111
|
+
* const [items, setItems] = useState(data);
|
|
112
|
+
*
|
|
113
|
+
* const renderItem = ({ item, id, positions, ...props }) => (
|
|
114
|
+
* <SortableItem key={id} id={id} positions={positions} {...props}>
|
|
115
|
+
* <View style={styles.itemContainer}>
|
|
116
|
+
* <View style={styles.itemContent}>
|
|
117
|
+
* <Text style={styles.itemTitle}>{item.title}</Text>
|
|
118
|
+
* <Text style={styles.itemSubtitle}>{item.subtitle}</Text>
|
|
119
|
+
* </View>
|
|
120
|
+
*
|
|
121
|
+
* {/* Only this handle area can initiate dragging *\/}
|
|
122
|
+
* <SortableItem.Handle style={styles.dragHandle}>
|
|
123
|
+
* <View style={styles.handleIcon}>
|
|
124
|
+
* <View style={styles.handleDot} />
|
|
125
|
+
* <View style={styles.handleDot} />
|
|
126
|
+
* <View style={styles.handleDot} />
|
|
127
|
+
* </View>
|
|
128
|
+
* </SortableItem.Handle>
|
|
129
|
+
* </View>
|
|
130
|
+
* </SortableItem>
|
|
131
|
+
* );
|
|
132
|
+
*
|
|
133
|
+
* return (
|
|
134
|
+
* <Sortable
|
|
135
|
+
* data={items}
|
|
136
|
+
* renderItem={renderItem}
|
|
137
|
+
* itemHeight={70}
|
|
138
|
+
* />
|
|
139
|
+
* );
|
|
140
|
+
* }
|
|
141
|
+
* ```
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* Sortable list with custom key extractor:
|
|
145
|
+
* ```typescript
|
|
146
|
+
* interface CustomItem {
|
|
147
|
+
* uuid: string;
|
|
148
|
+
* name: string;
|
|
149
|
+
* order: number;
|
|
150
|
+
* }
|
|
151
|
+
*
|
|
152
|
+
* function CustomSortableList() {
|
|
153
|
+
* const [items, setItems] = useState<CustomItem[]>(data);
|
|
154
|
+
*
|
|
155
|
+
* const renderItem = ({ item, id, positions, ...props }) => (
|
|
156
|
+
* <SortableItem key={id} id={id} positions={positions} {...props}>
|
|
157
|
+
* <View style={styles.customItem}>
|
|
158
|
+
* <Text>{item.name}</Text>
|
|
159
|
+
* <Text>Order: {item.order}</Text>
|
|
160
|
+
* </View>
|
|
161
|
+
* </SortableItem>
|
|
162
|
+
* );
|
|
163
|
+
*
|
|
164
|
+
* return (
|
|
165
|
+
* <Sortable
|
|
166
|
+
* data={items}
|
|
167
|
+
* renderItem={renderItem}
|
|
168
|
+
* itemHeight={50}
|
|
169
|
+
* itemKeyExtractor={(item) => item.uuid} // Use uuid instead of id
|
|
170
|
+
* />
|
|
171
|
+
* );
|
|
172
|
+
* }
|
|
173
|
+
* ```
|
|
174
|
+
*
|
|
175
|
+
* @see {@link SortableItem} for individual item component
|
|
176
|
+
* @see {@link useSortableList} for the underlying hook
|
|
177
|
+
* @see {@link SortableRenderItemProps} for render function props
|
|
178
|
+
* @see {@link UseSortableListOptions} for configuration options
|
|
179
|
+
* @see {@link UseSortableListReturn} for hook return details
|
|
180
|
+
* @see {@link DropProvider} for drag-and-drop context
|
|
181
|
+
*/
|
|
182
|
+
export declare function Sortable<TData extends {
|
|
183
|
+
id: string;
|
|
184
|
+
}>({ data, renderItem, itemHeight, style, contentContainerStyle, itemKeyExtractor, }: SortableProps<TData>): React.JSX.Element;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { StyleSheet } from "react-native";
|
|
3
|
+
import Animated from "react-native-reanimated";
|
|
4
|
+
import { GestureHandlerRootView, ScrollView, } from "react-native-gesture-handler";
|
|
5
|
+
import { DropProvider } from "../context/DropContext";
|
|
6
|
+
import { useSortableList, } from "../hooks/useSortableList";
|
|
7
|
+
// Create an animated version of the ScrollView
|
|
8
|
+
const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView);
|
|
9
|
+
/**
|
|
10
|
+
* A high-level component for creating sortable lists with smooth reordering animations.
|
|
11
|
+
*
|
|
12
|
+
* The Sortable component provides a complete solution for sortable lists, handling
|
|
13
|
+
* all the complex state management, gesture handling, and animations internally.
|
|
14
|
+
* It renders a scrollable list where items can be dragged to reorder them with
|
|
15
|
+
* smooth animations and auto-scrolling support.
|
|
16
|
+
*
|
|
17
|
+
* @template TData - The type of data items in the list (must extend `{ id: string }`)
|
|
18
|
+
* @param props - Configuration props for the sortable list
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* Basic sortable list:
|
|
22
|
+
* ```typescript
|
|
23
|
+
* import { Sortable } from './components/Sortable';
|
|
24
|
+
*
|
|
25
|
+
* interface Task {
|
|
26
|
+
* id: string;
|
|
27
|
+
* title: string;
|
|
28
|
+
* completed: boolean;
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* function TaskList() {
|
|
32
|
+
* const [tasks, setTasks] = useState<Task[]>([
|
|
33
|
+
* { id: '1', title: 'Learn React Native', completed: false },
|
|
34
|
+
* { id: '2', title: 'Build an app', completed: false },
|
|
35
|
+
* { id: '3', title: 'Deploy to store', completed: false }
|
|
36
|
+
* ]);
|
|
37
|
+
*
|
|
38
|
+
* const renderTask = ({ item, id, positions, ...props }) => (
|
|
39
|
+
* <SortableItem key={id} id={id} positions={positions} {...props}>
|
|
40
|
+
* <View style={styles.taskItem}>
|
|
41
|
+
* <Text>{item.title}</Text>
|
|
42
|
+
* <Text>{item.completed ? '✓' : '○'}</Text>
|
|
43
|
+
* </View>
|
|
44
|
+
* </SortableItem>
|
|
45
|
+
* );
|
|
46
|
+
*
|
|
47
|
+
* return (
|
|
48
|
+
* <Sortable
|
|
49
|
+
* data={tasks}
|
|
50
|
+
* renderItem={renderTask}
|
|
51
|
+
* itemHeight={60}
|
|
52
|
+
* style={styles.list}
|
|
53
|
+
* />
|
|
54
|
+
* );
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* Sortable list with custom styling and callbacks:
|
|
60
|
+
* ```typescript
|
|
61
|
+
* function AdvancedTaskList() {
|
|
62
|
+
* const [tasks, setTasks] = useState(initialTasks);
|
|
63
|
+
*
|
|
64
|
+
* const renderTask = ({ item, id, positions, ...props }) => (
|
|
65
|
+
* <SortableItem
|
|
66
|
+
* key={id}
|
|
67
|
+
* id={id}
|
|
68
|
+
* positions={positions}
|
|
69
|
+
* {...props}
|
|
70
|
+
* onMove={(itemId, from, to) => {
|
|
71
|
+
* // Update data when items are reordered
|
|
72
|
+
* const newTasks = [...tasks];
|
|
73
|
+
* const [movedTask] = newTasks.splice(from, 1);
|
|
74
|
+
* newTasks.splice(to, 0, movedTask);
|
|
75
|
+
* setTasks(newTasks);
|
|
76
|
+
*
|
|
77
|
+
* // Analytics
|
|
78
|
+
* analytics.track('task_reordered', { taskId: itemId, from, to });
|
|
79
|
+
* }}
|
|
80
|
+
* onDragStart={(itemId) => {
|
|
81
|
+
* hapticFeedback();
|
|
82
|
+
* setDraggingTask(itemId);
|
|
83
|
+
* }}
|
|
84
|
+
* onDrop={(itemId) => {
|
|
85
|
+
* setDraggingTask(null);
|
|
86
|
+
* }}
|
|
87
|
+
* >
|
|
88
|
+
* <Animated.View style={[styles.taskItem, item.priority === 'high' && styles.highPriority]}>
|
|
89
|
+
* <Text style={styles.taskTitle}>{item.title}</Text>
|
|
90
|
+
* <Text style={styles.taskDue}>{item.dueDate}</Text>
|
|
91
|
+
* <View style={styles.dragHandle}>
|
|
92
|
+
* <Icon name="drag-handle" size={20} color="#666" />
|
|
93
|
+
* </View>
|
|
94
|
+
* </Animated.View>
|
|
95
|
+
* </SortableItem>
|
|
96
|
+
* );
|
|
97
|
+
*
|
|
98
|
+
* return (
|
|
99
|
+
* <View style={styles.container}>
|
|
100
|
+
* <Text style={styles.header}>My Tasks ({tasks.length})</Text>
|
|
101
|
+
* <Sortable
|
|
102
|
+
* data={tasks}
|
|
103
|
+
* renderItem={renderTask}
|
|
104
|
+
* itemHeight={80}
|
|
105
|
+
* style={styles.sortableList}
|
|
106
|
+
* contentContainerStyle={styles.listContent}
|
|
107
|
+
* />
|
|
108
|
+
* </View>
|
|
109
|
+
* );
|
|
110
|
+
* }
|
|
111
|
+
* ```
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* Sortable list with drag handles:
|
|
115
|
+
* ```typescript
|
|
116
|
+
* function SortableWithHandles() {
|
|
117
|
+
* const [items, setItems] = useState(data);
|
|
118
|
+
*
|
|
119
|
+
* const renderItem = ({ item, id, positions, ...props }) => (
|
|
120
|
+
* <SortableItem key={id} id={id} positions={positions} {...props}>
|
|
121
|
+
* <View style={styles.itemContainer}>
|
|
122
|
+
* <View style={styles.itemContent}>
|
|
123
|
+
* <Text style={styles.itemTitle}>{item.title}</Text>
|
|
124
|
+
* <Text style={styles.itemSubtitle}>{item.subtitle}</Text>
|
|
125
|
+
* </View>
|
|
126
|
+
*
|
|
127
|
+
* {/* Only this handle area can initiate dragging *\/}
|
|
128
|
+
* <SortableItem.Handle style={styles.dragHandle}>
|
|
129
|
+
* <View style={styles.handleIcon}>
|
|
130
|
+
* <View style={styles.handleDot} />
|
|
131
|
+
* <View style={styles.handleDot} />
|
|
132
|
+
* <View style={styles.handleDot} />
|
|
133
|
+
* </View>
|
|
134
|
+
* </SortableItem.Handle>
|
|
135
|
+
* </View>
|
|
136
|
+
* </SortableItem>
|
|
137
|
+
* );
|
|
138
|
+
*
|
|
139
|
+
* return (
|
|
140
|
+
* <Sortable
|
|
141
|
+
* data={items}
|
|
142
|
+
* renderItem={renderItem}
|
|
143
|
+
* itemHeight={70}
|
|
144
|
+
* />
|
|
145
|
+
* );
|
|
146
|
+
* }
|
|
147
|
+
* ```
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* Sortable list with custom key extractor:
|
|
151
|
+
* ```typescript
|
|
152
|
+
* interface CustomItem {
|
|
153
|
+
* uuid: string;
|
|
154
|
+
* name: string;
|
|
155
|
+
* order: number;
|
|
156
|
+
* }
|
|
157
|
+
*
|
|
158
|
+
* function CustomSortableList() {
|
|
159
|
+
* const [items, setItems] = useState<CustomItem[]>(data);
|
|
160
|
+
*
|
|
161
|
+
* const renderItem = ({ item, id, positions, ...props }) => (
|
|
162
|
+
* <SortableItem key={id} id={id} positions={positions} {...props}>
|
|
163
|
+
* <View style={styles.customItem}>
|
|
164
|
+
* <Text>{item.name}</Text>
|
|
165
|
+
* <Text>Order: {item.order}</Text>
|
|
166
|
+
* </View>
|
|
167
|
+
* </SortableItem>
|
|
168
|
+
* );
|
|
169
|
+
*
|
|
170
|
+
* return (
|
|
171
|
+
* <Sortable
|
|
172
|
+
* data={items}
|
|
173
|
+
* renderItem={renderItem}
|
|
174
|
+
* itemHeight={50}
|
|
175
|
+
* itemKeyExtractor={(item) => item.uuid} // Use uuid instead of id
|
|
176
|
+
* />
|
|
177
|
+
* );
|
|
178
|
+
* }
|
|
179
|
+
* ```
|
|
180
|
+
*
|
|
181
|
+
* @see {@link SortableItem} for individual item component
|
|
182
|
+
* @see {@link useSortableList} for the underlying hook
|
|
183
|
+
* @see {@link SortableRenderItemProps} for render function props
|
|
184
|
+
* @see {@link UseSortableListOptions} for configuration options
|
|
185
|
+
* @see {@link UseSortableListReturn} for hook return details
|
|
186
|
+
* @see {@link DropProvider} for drag-and-drop context
|
|
187
|
+
*/
|
|
188
|
+
export function Sortable({ data, renderItem, itemHeight, style, contentContainerStyle, itemKeyExtractor = (item) => item.id, }) {
|
|
189
|
+
const sortableOptions = {
|
|
190
|
+
data,
|
|
191
|
+
itemHeight,
|
|
192
|
+
itemKeyExtractor,
|
|
193
|
+
};
|
|
194
|
+
const { scrollViewRef, dropProviderRef, handleScroll, handleScrollEnd, contentHeight, getItemProps, } = useSortableList(sortableOptions);
|
|
195
|
+
return (<GestureHandlerRootView style={styles.flex}>
|
|
196
|
+
<DropProvider ref={dropProviderRef}>
|
|
197
|
+
<AnimatedScrollView ref={scrollViewRef} onScroll={handleScroll} scrollEventThrottle={16} style={[styles.scrollView, style]} contentContainerStyle={[
|
|
198
|
+
{ height: contentHeight },
|
|
199
|
+
contentContainerStyle,
|
|
200
|
+
]} onScrollEndDrag={handleScrollEnd} onMomentumScrollEnd={handleScrollEnd} simultaneousHandlers={dropProviderRef}>
|
|
201
|
+
{data.map((item, index) => {
|
|
202
|
+
// Get the item props from our hook
|
|
203
|
+
const itemProps = getItemProps(item, index);
|
|
204
|
+
// Create the complete props with the item and index
|
|
205
|
+
const sortableItemProps = {
|
|
206
|
+
item,
|
|
207
|
+
index,
|
|
208
|
+
...itemProps,
|
|
209
|
+
};
|
|
210
|
+
return renderItem(sortableItemProps);
|
|
211
|
+
})}
|
|
212
|
+
</AnimatedScrollView>
|
|
213
|
+
</DropProvider>
|
|
214
|
+
</GestureHandlerRootView>);
|
|
215
|
+
}
|
|
216
|
+
const styles = StyleSheet.create({
|
|
217
|
+
flex: {
|
|
218
|
+
flex: 1,
|
|
219
|
+
},
|
|
220
|
+
scrollView: {
|
|
221
|
+
flex: 1,
|
|
222
|
+
position: "relative",
|
|
223
|
+
backgroundColor: "white",
|
|
224
|
+
},
|
|
225
|
+
});
|