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,211 @@
1
+ import { useRef, useCallback } from "react";
2
+ import { scrollTo, useAnimatedReaction, useAnimatedRef, useAnimatedScrollHandler, useSharedValue, } from "react-native-reanimated";
3
+ import { listToObject } from "../components/sortableUtils";
4
+ import { ScrollDirection } from "../types/sortable";
5
+ /**
6
+ * A hook for managing sortable lists with drag-and-drop reordering capabilities.
7
+ *
8
+ * This hook provides the foundational state management and utilities needed to create
9
+ * sortable lists. It handles position tracking, scroll synchronization, auto-scrolling,
10
+ * and provides helper functions for individual sortable items.
11
+ *
12
+ * @template TData - The type of data items in the sortable list (must extend `{ id: string }`)
13
+ * @param options - Configuration options for the sortable list
14
+ * @returns Object containing shared values, refs, handlers, and utilities for the sortable list
15
+ *
16
+ * @example
17
+ * Basic sortable list setup:
18
+ * ```typescript
19
+ * import { useSortableList } from './hooks/useSortableList';
20
+ * import { SortableItem } from './components/SortableItem';
21
+ *
22
+ * interface Task {
23
+ * id: string;
24
+ * title: string;
25
+ * completed: boolean;
26
+ * }
27
+ *
28
+ * function TaskList() {
29
+ * const [tasks, setTasks] = useState<Task[]>([
30
+ * { id: '1', title: 'Learn React Native', completed: false },
31
+ * { id: '2', title: 'Build an app', completed: false },
32
+ * { id: '3', title: 'Deploy to store', completed: false }
33
+ * ]);
34
+ *
35
+ * const {
36
+ * scrollViewRef,
37
+ * dropProviderRef,
38
+ * handleScroll,
39
+ * handleScrollEnd,
40
+ * contentHeight,
41
+ * getItemProps,
42
+ * } = useSortableList({
43
+ * data: tasks,
44
+ * itemHeight: 60,
45
+ * });
46
+ *
47
+ * return (
48
+ * <GestureHandlerRootView style={styles.container}>
49
+ * <DropProvider ref={dropProviderRef}>
50
+ * <Animated.ScrollView
51
+ * ref={scrollViewRef}
52
+ * onScroll={handleScroll}
53
+ * scrollEventThrottle={16}
54
+ * style={styles.scrollView}
55
+ * contentContainerStyle={{ height: contentHeight }}
56
+ * onScrollEndDrag={handleScrollEnd}
57
+ * onMomentumScrollEnd={handleScrollEnd}
58
+ * >
59
+ * {tasks.map((task, index) => {
60
+ * const itemProps = getItemProps(task, index);
61
+ * return (
62
+ * <SortableItem key={task.id} {...itemProps}>
63
+ * <View style={styles.taskItem}>
64
+ * <Text>{task.title}</Text>
65
+ * </View>
66
+ * </SortableItem>
67
+ * );
68
+ * })}
69
+ * </Animated.ScrollView>
70
+ * </DropProvider>
71
+ * </GestureHandlerRootView>
72
+ * );
73
+ * }
74
+ * ```
75
+ *
76
+ * @example
77
+ * Sortable list with custom key extractor:
78
+ * ```typescript
79
+ * interface CustomItem {
80
+ * uuid: string;
81
+ * name: string;
82
+ * order: number;
83
+ * }
84
+ *
85
+ * function CustomSortableList() {
86
+ * const [items, setItems] = useState<CustomItem[]>(data);
87
+ *
88
+ * const sortableListProps = useSortableList({
89
+ * data: items,
90
+ * itemHeight: 50,
91
+ * itemKeyExtractor: (item) => item.uuid, // Use uuid instead of id
92
+ * });
93
+ *
94
+ * const { getItemProps, ...otherProps } = sortableListProps;
95
+ *
96
+ * return (
97
+ * <SortableListContainer {...otherProps}>
98
+ * {items.map((item, index) => {
99
+ * const itemProps = getItemProps(item, index);
100
+ * return (
101
+ * <SortableItem key={item.uuid} {...itemProps}>
102
+ * <View style={styles.customItem}>
103
+ * <Text>{item.name}</Text>
104
+ * <Text>Order: {item.order}</Text>
105
+ * </View>
106
+ * </SortableItem>
107
+ * );
108
+ * })}
109
+ * </SortableListContainer>
110
+ * );
111
+ * }
112
+ * ```
113
+ *
114
+ * @example
115
+ * Sortable list with reordering logic:
116
+ * ```typescript
117
+ * function ReorderableTaskList() {
118
+ * const [tasks, setTasks] = useState(initialTasks);
119
+ *
120
+ * const handleReorder = useCallback((id: string, from: number, to: number) => {
121
+ * setTasks(prevTasks => {
122
+ * const newTasks = [...prevTasks];
123
+ * const [movedTask] = newTasks.splice(from, 1);
124
+ * newTasks.splice(to, 0, movedTask);
125
+ * return newTasks;
126
+ * });
127
+ * }, []);
128
+ *
129
+ * const sortableProps = useSortableList({
130
+ * data: tasks,
131
+ * itemHeight: 80,
132
+ * });
133
+ *
134
+ * return (
135
+ * <SortableListContainer {...sortableProps}>
136
+ * {tasks.map((task, index) => {
137
+ * const itemProps = sortableProps.getItemProps(task, index);
138
+ * return (
139
+ * <SortableItem
140
+ * key={task.id}
141
+ * {...itemProps}
142
+ * onMove={handleReorder}
143
+ * >
144
+ * <TaskCard task={task} />
145
+ * </SortableItem>
146
+ * );
147
+ * })}
148
+ * </SortableListContainer>
149
+ * );
150
+ * }
151
+ * ```
152
+ *
153
+ * @see {@link UseSortableListOptions} for configuration options
154
+ * @see {@link UseSortableListReturn} for return value details
155
+ * @see {@link useSortable} for individual item management
156
+ * @see {@link SortableItem} for component implementation
157
+ * @see {@link Sortable} for high-level sortable list component
158
+ * @see {@link DropProvider} for drag-and-drop context
159
+ */
160
+ export function useSortableList(options) {
161
+ const { data, itemHeight, itemKeyExtractor = (item) => item.id } = options;
162
+ // Set up shared values
163
+ const positions = useSharedValue(listToObject(data));
164
+ const scrollY = useSharedValue(0);
165
+ const autoScroll = useSharedValue(ScrollDirection.None);
166
+ const scrollViewRef = useAnimatedRef();
167
+ const dropProviderRef = useRef(null);
168
+ // Scrolling synchronization
169
+ useAnimatedReaction(() => scrollY.value, (scrolling) => {
170
+ scrollTo(scrollViewRef, 0, scrolling, false);
171
+ });
172
+ // Handle scroll events
173
+ const handleScroll = useAnimatedScrollHandler((event) => {
174
+ scrollY.value = event.contentOffset.y;
175
+ });
176
+ const handleScrollEnd = useCallback(() => {
177
+ let localScrollTimeout = null;
178
+ if (localScrollTimeout) {
179
+ clearTimeout(localScrollTimeout);
180
+ }
181
+ localScrollTimeout = setTimeout(() => {
182
+ var _a;
183
+ (_a = dropProviderRef.current) === null || _a === void 0 ? void 0 : _a.requestPositionUpdate();
184
+ }, 50);
185
+ }, []);
186
+ // Calculate content height
187
+ const contentHeight = data.length * itemHeight;
188
+ // Helper to get props for each sortable item
189
+ const getItemProps = useCallback((item, index) => {
190
+ const id = itemKeyExtractor(item, index);
191
+ return {
192
+ id,
193
+ positions,
194
+ lowerBound: scrollY,
195
+ autoScrollDirection: autoScroll,
196
+ itemsCount: data.length,
197
+ itemHeight,
198
+ };
199
+ }, [data.length, itemHeight, itemKeyExtractor, positions, scrollY, autoScroll]);
200
+ return {
201
+ positions,
202
+ scrollY,
203
+ autoScroll,
204
+ scrollViewRef,
205
+ dropProviderRef,
206
+ handleScroll,
207
+ handleScrollEnd,
208
+ contentHeight,
209
+ getItemProps,
210
+ };
211
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ export { Draggable } from "./components/Draggable";
2
+ export { Droppable } from "./components/Droppable";
3
+ export { Sortable } from "./components/Sortable";
4
+ export { SortableItem } from "./components/SortableItem";
5
+ export { DropProvider } from "./context/DropContext";
6
+ export * from "./types";
7
+ export { listToObject, setAutoScroll, setPosition, clamp, objectMove, ScrollDirection, } from "./components/sortableUtils";
8
+ export { useDraggable } from "./hooks/useDraggable";
9
+ export { useDroppable } from "./hooks/useDroppable";
10
+ export { useSortable } from "./hooks/useSortable";
11
+ export { useSortableList } from "./hooks/useSortableList";
package/lib/index.js ADDED
@@ -0,0 +1,16 @@
1
+ // Components
2
+ export { Draggable } from "./components/Draggable";
3
+ export { Droppable } from "./components/Droppable";
4
+ export { Sortable } from "./components/Sortable";
5
+ export { SortableItem } from "./components/SortableItem";
6
+ // Context
7
+ export { DropProvider } from "./context/DropContext";
8
+ // Types
9
+ export * from "./types";
10
+ // Utils
11
+ export { listToObject, setAutoScroll, setPosition, clamp, objectMove, ScrollDirection, } from "./components/sortableUtils";
12
+ // Hooks
13
+ export { useDraggable } from "./hooks/useDraggable";
14
+ export { useDroppable } from "./hooks/useDroppable";
15
+ export { useSortable } from "./hooks/useSortable";
16
+ export { useSortableList } from "./hooks/useSortableList";
@@ -0,0 +1,166 @@
1
+ import { ReactNode } from "react";
2
+ /**
3
+ * Alignment options for positioning dropped items within a droppable area.
4
+ *
5
+ * Determines where within the droppable bounds the draggable item will be positioned
6
+ * when successfully dropped. Can be combined with DropOffset for fine-tuning.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * // Center the dropped item (default)
11
+ * const centerAlignment: DropAlignment = 'center';
12
+ *
13
+ * // Position at top-left corner
14
+ * const topLeftAlignment: DropAlignment = 'top-left';
15
+ *
16
+ * // Position at bottom edge, centered horizontally
17
+ * const bottomCenterAlignment: DropAlignment = 'bottom-center';
18
+ * ```
19
+ *
20
+ * @see {@link DropOffset} for additional positioning control
21
+ * @see {@link UseDroppableOptions} for usage in droppables
22
+ */
23
+ export type DropAlignment = "center" | "top-left" | "top-center" | "top-right" | "center-left" | "center-right" | "bottom-left" | "bottom-center" | "bottom-right";
24
+ /**
25
+ * Pixel offset to apply after alignment positioning.
26
+ *
27
+ * Provides fine-grained control over the exact position where dropped items
28
+ * are placed within a droppable area. Applied after the DropAlignment calculation.
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * // No offset (default)
33
+ * const noOffset: DropOffset = { x: 0, y: 0 };
34
+ *
35
+ * // Move 10px right and 5px down from aligned position
36
+ * const customOffset: DropOffset = { x: 10, y: 5 };
37
+ *
38
+ * // Move 20px left from aligned position
39
+ * const leftOffset: DropOffset = { x: -20, y: 0 };
40
+ * ```
41
+ *
42
+ * @see {@link DropAlignment} for base positioning
43
+ * @see {@link UseDroppableOptions} for usage in droppables
44
+ */
45
+ export interface DropOffset {
46
+ /** Horizontal offset in pixels (positive = right, negative = left) */
47
+ x: number;
48
+ /** Vertical offset in pixels (positive = down, negative = up) */
49
+ y: number;
50
+ }
51
+ export interface DroppedItemsMap<TData = unknown> {
52
+ [draggableId: string]: {
53
+ droppableId: string;
54
+ data: TData;
55
+ };
56
+ }
57
+ export interface DropSlot<TData = unknown> {
58
+ id: string;
59
+ x: number;
60
+ y: number;
61
+ width: number;
62
+ height: number;
63
+ onDrop: (data: TData) => void;
64
+ dropAlignment?: DropAlignment;
65
+ dropOffset?: DropOffset;
66
+ capacity?: number;
67
+ }
68
+ export type PositionUpdateListener = () => void;
69
+ export interface SlotsContextValue<TData = unknown> {
70
+ register: (id: number, slot: DropSlot<TData>) => void;
71
+ unregister: (id: number) => void;
72
+ getSlots: () => Record<number, DropSlot<TData>>;
73
+ isRegistered: (id: number) => boolean;
74
+ setActiveHoverSlot: (id: number | null) => void;
75
+ activeHoverSlotId: number | null;
76
+ registerPositionUpdateListener: (id: string, listener: PositionUpdateListener) => void;
77
+ unregisterPositionUpdateListener: (id: string) => void;
78
+ requestPositionUpdate: () => void;
79
+ registerDroppedItem: (draggableId: string, droppableId: string, itemData: any) => void;
80
+ unregisterDroppedItem: (draggableId: string) => void;
81
+ getDroppedItems: () => DroppedItemsMap<any>;
82
+ hasAvailableCapacity: (droppableId: string) => boolean;
83
+ onDragging?: (payload: {
84
+ x: number;
85
+ y: number;
86
+ tx: number;
87
+ ty: number;
88
+ itemData: any;
89
+ }) => void;
90
+ onDragStart?: (data: any) => void;
91
+ onDragEnd?: (data: any) => void;
92
+ }
93
+ export declare const SlotsContext: import("react").Context<SlotsContextValue<any>>;
94
+ /**
95
+ * Props for the DropProvider component.
96
+ *
97
+ * @see {@link DropProvider} for component usage
98
+ */
99
+ export interface DropProviderProps {
100
+ /** The child components that will have access to the drag-and-drop context */
101
+ children: ReactNode;
102
+ /**
103
+ * Callback fired when layout updates are complete.
104
+ * Useful for triggering additional UI updates after position recalculations.
105
+ */
106
+ onLayoutUpdateComplete?: () => void;
107
+ /**
108
+ * Callback fired when the dropped items mapping changes.
109
+ * Provides access to the current state of which items are dropped where.
110
+ *
111
+ * @param droppedItems - Current mapping of draggable IDs to their drop locations
112
+ */
113
+ onDroppedItemsUpdate?: (droppedItems: DroppedItemsMap) => void;
114
+ /**
115
+ * Global callback fired during drag operations.
116
+ * Receives position updates for all draggable items.
117
+ *
118
+ * @param payload - Position and data information for the dragging item
119
+ */
120
+ onDragging?: (payload: {
121
+ x: number;
122
+ y: number;
123
+ tx: number;
124
+ ty: number;
125
+ itemData: any;
126
+ }) => void;
127
+ /**
128
+ * Global callback fired when any drag operation starts.
129
+ * @param data - The data associated with the draggable item
130
+ */
131
+ onDragStart?: (data: any) => void;
132
+ /**
133
+ * Global callback fired when any drag operation ends.
134
+ * @param data - The data associated with the draggable item
135
+ */
136
+ onDragEnd?: (data: any) => void;
137
+ }
138
+ /**
139
+ * Imperative handle interface for the DropProvider component.
140
+ * Provides methods that can be called on the DropProvider ref.
141
+ *
142
+ * @example
143
+ * ```typescript
144
+ * const dropProviderRef = useRef<DropProviderRef>(null);
145
+ *
146
+ * // Trigger position update
147
+ * dropProviderRef.current?.requestPositionUpdate();
148
+ *
149
+ * // Get current dropped items
150
+ * const droppedItems = dropProviderRef.current?.getDroppedItems();
151
+ * ```
152
+ *
153
+ * @see {@link DropProvider} for component usage
154
+ */
155
+ export interface DropProviderRef {
156
+ /**
157
+ * Manually trigger a position update for all registered droppables and draggables.
158
+ * Useful after layout changes or when positions may have become stale.
159
+ */
160
+ requestPositionUpdate: () => void;
161
+ /**
162
+ * Get the current mapping of dropped items.
163
+ * @returns Object mapping draggable IDs to their drop information
164
+ */
165
+ getDroppedItems: () => DroppedItemsMap;
166
+ }
@@ -0,0 +1,80 @@
1
+ // Define DropAlignment and DropOffset types here
2
+ import { createContext } from "react";
3
+ // Default context value using 'any' for broad compatibility
4
+ const defaultSlotsContextValue = {
5
+ register: (_id, _slot) => {
6
+ if (process.env.NODE_ENV !== "production") {
7
+ console.warn("SlotsContext: register called without a Provider.");
8
+ }
9
+ },
10
+ unregister: (_id) => {
11
+ if (process.env.NODE_ENV !== "production") {
12
+ console.warn("SlotsContext: unregister called without a Provider.");
13
+ }
14
+ },
15
+ getSlots: () => {
16
+ if (process.env.NODE_ENV !== "production") {
17
+ console.warn("SlotsContext: getSlots called without a Provider.");
18
+ }
19
+ return {};
20
+ },
21
+ isRegistered: (_id) => {
22
+ if (process.env.NODE_ENV !== "production") {
23
+ console.warn("SlotsContext: isRegistered called without a Provider.");
24
+ }
25
+ return false;
26
+ },
27
+ setActiveHoverSlot: (_id) => {
28
+ if (process.env.NODE_ENV !== "production") {
29
+ console.warn("SlotsContext: setActiveHoverSlot called without a Provider.");
30
+ }
31
+ },
32
+ activeHoverSlotId: null,
33
+ registerPositionUpdateListener: (_id, _listener) => {
34
+ if (process.env.NODE_ENV !== "production") {
35
+ console.warn("SlotsContext: registerPositionUpdateListener called without a Provider.");
36
+ }
37
+ },
38
+ unregisterPositionUpdateListener: (_id) => {
39
+ if (process.env.NODE_ENV !== "production") {
40
+ console.warn("SlotsContext: unregisterPositionUpdateListener called without a Provider.");
41
+ }
42
+ },
43
+ requestPositionUpdate: () => {
44
+ if (process.env.NODE_ENV !== "production") {
45
+ console.warn("SlotsContext: requestPositionUpdate called without a Provider (internally).");
46
+ }
47
+ },
48
+ // Update default implementations
49
+ registerDroppedItem: (_draggableId, _droppableId, _itemData) => {
50
+ if (process.env.NODE_ENV !== "production") {
51
+ console.warn("SlotsContext: registerDroppedItem called without a Provider.");
52
+ }
53
+ },
54
+ unregisterDroppedItem: (_draggableId) => {
55
+ if (process.env.NODE_ENV !== "production") {
56
+ console.warn("SlotsContext: unregisterDroppedItem called without a Provider.");
57
+ }
58
+ },
59
+ getDroppedItems: () => {
60
+ if (process.env.NODE_ENV !== "production") {
61
+ console.warn("SlotsContext: getDroppedItems called without a Provider.");
62
+ }
63
+ return {};
64
+ },
65
+ hasAvailableCapacity: (_droppableId) => {
66
+ if (process.env.NODE_ENV !== "production") {
67
+ console.warn("SlotsContext: hasAvailableCapacity called without a Provider.");
68
+ }
69
+ return false;
70
+ },
71
+ onDragging: (payload) => {
72
+ if (process.env.NODE_ENV !== "production") {
73
+ console.warn("SlotsContext: onDragging called without a Provider.");
74
+ }
75
+ },
76
+ onDragStart: undefined,
77
+ onDragEnd: undefined,
78
+ };
79
+ // Create the context
80
+ export const SlotsContext = createContext(defaultSlotsContextValue);