Skip to content

Commit 9602f44

Browse files
committed
feat(DraggableSort): properly track added/removed items (closes #23)
1 parent 06744f7 commit 9602f44

File tree

7 files changed

+120
-36
lines changed

7 files changed

+120
-36
lines changed

example/src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ export const App: FunctionComponent = () => {
99
return (
1010
<SafeAreaView style={styles.container}>
1111
<GestureHandlerRootView>
12-
<DraggableBasicExample />
12+
{/* <DraggableBasicExample /> */}
1313
{/* <DraggableGridExample /> */}
14-
{/* <DraggableStackExample /> */}
14+
<DraggableStackExample />
1515
</GestureHandlerRootView>
1616
</SafeAreaView>
1717
);

example/src/pages/DraggableStackExample.tsx

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import React, {type FunctionComponent} from 'react';
2-
import {StyleSheet, Text, View} from 'react-native';
1+
import React, {useState, type FunctionComponent} from 'react';
2+
import {Button, StyleSheet, Text, View} from 'react-native';
33
import {
44
DndProvider,
55
type ObjectWithId,
66
Draggable,
77
DraggableStack,
88
type DraggableStackProps,
9-
} from '@mgcrea/react-native-dnd';
9+
} from '@mgcrea/react-native-dnd/src';
1010

1111
const items = ['🤓', '🤖🤖', '👻👻👻', '👾👾👾👾'];
1212
const data = items.map((letter, index) => ({
@@ -22,6 +22,8 @@ export const DraggableStackExample: FunctionComponent = () => {
2222
console.log('onStackOrderUpdate', value);
2323
};
2424

25+
const [items, setItems] = useState(data);
26+
2527
return (
2628
<View style={styles.container}>
2729
<Text style={styles.title}>DraggableStack Example</Text>
@@ -32,7 +34,7 @@ export const DraggableStackExample: FunctionComponent = () => {
3234
style={styles.stack}
3335
onOrderChange={onStackOrderChange}
3436
onOrderUpdate={onStackOrderUpdate}>
35-
{data.map(letter => (
37+
{items.map(letter => (
3638
<Draggable
3739
key={letter.id}
3840
id={letter.id}
@@ -42,6 +44,28 @@ export const DraggableStackExample: FunctionComponent = () => {
4244
))}
4345
</DraggableStack>
4446
</DndProvider>
47+
<Button
48+
title="Add"
49+
onPress={() => {
50+
setItems(prevItems => {
51+
const randomIndex = 2; //Math.floor(Math.random() * prevItems.length);
52+
prevItems.splice(randomIndex, 0, {
53+
value: '🤪',
54+
id: `${prevItems.length}-🤪`,
55+
});
56+
return prevItems.slice();
57+
});
58+
}}
59+
/>
60+
<Button
61+
title="Delete"
62+
onPress={() => {
63+
setItems(prevItems => {
64+
const randomIndex = Math.floor(Math.random() * prevItems.length);
65+
return prevItems.filter((_, index) => index !== randomIndex);
66+
});
67+
}}
68+
/>
4569
</View>
4670
);
4771
};
@@ -69,6 +93,7 @@ const styles = StyleSheet.create({
6993
},
7094
draggable: {
7195
backgroundColor: 'seagreen',
96+
opacity: 0.5,
7297
height: 100,
7398
borderColor: 'rgba(0,0,0,0.2)',
7499
borderWidth: 1,

src/DndContext.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export type DraggableStates = Record<UniqueIdentifier, SharedValue<DraggableStat
1616

1717
export type DndContextValue = {
1818
containerRef: RefObject<View>;
19+
draggableIds: SharedValue<UniqueIdentifier[]>;
20+
droppableIds: SharedValue<UniqueIdentifier[]>;
1921
draggableLayouts: SharedValue<Layouts>;
2022
droppableLayouts: SharedValue<Layouts>;
2123
draggableOptions: SharedValue<DraggableOptions>;

src/DndProvider.tsx

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ export const DndProvider = forwardRef<DndProviderHandle, PropsWithChildren<DndPr
9090
ref,
9191
) {
9292
const containerRef = useRef<View | null>(null);
93+
const draggableIds = useSharedValue<UniqueIdentifier[]>([]);
94+
const droppableIds = useSharedValue<UniqueIdentifier[]>([]);
9395
const draggableLayouts = useSharedValue<Layouts>({});
9496
const droppableLayouts = useSharedValue<Layouts>({});
9597
const draggableOptions = useSharedValue<DraggableOptions>({});
@@ -105,20 +107,10 @@ export const DndProvider = forwardRef<DndProviderHandle, PropsWithChildren<DndPr
105107
const draggableContentOffset = useSharedPoint(0, 0);
106108
const panGestureState = useSharedValue<GestureEventPayload["state"]>(0);
107109

108-
useAnimatedReaction(
109-
() => draggableActiveId.value,
110-
(next, prev) => {
111-
if (next !== null) {
112-
if (onActivation) {
113-
runOnJS(onActivation)(next, prev);
114-
}
115-
}
116-
},
117-
[],
118-
);
119-
120110
const contextValue = useRef<DndContextValue>({
121111
containerRef,
112+
draggableIds,
113+
droppableIds,
122114
draggableLayouts,
123115
droppableLayouts,
124116
draggableOptions,
@@ -149,6 +141,19 @@ export const DndProvider = forwardRef<DndProviderHandle, PropsWithChildren<DndPr
149141
[],
150142
);
151143

144+
// Handle activation changes
145+
useAnimatedReaction(
146+
() => draggableActiveId.value,
147+
(next, prev) => {
148+
if (next !== null) {
149+
if (onActivation) {
150+
runOnJS(onActivation)(next, prev);
151+
}
152+
}
153+
},
154+
[],
155+
);
156+
152157
const panGesture = useMemo(() => {
153158
const findActiveLayoutId = (point: Point): UniqueIdentifier | null => {
154159
"worklet";

src/features/sort/hooks/useDraggableSort.ts

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@ import type { UniqueIdentifier } from "../../../types";
55
import {
66
applyOffset,
77
arraysEqual,
8-
centerAxis,
98
type Direction,
9+
doesOverlapOnAxis,
1010
moveArrayIndex,
11-
overlapsAxis,
1211
type Rectangle,
1312
} from "../../../utils";
1413

@@ -31,9 +30,10 @@ export const useDraggableSort = ({
3130
initialOrder = [],
3231
onOrderChange,
3332
onOrderUpdate,
34-
shouldSwapWorklet,
33+
shouldSwapWorklet = doesOverlapOnAxis,
3534
}: UseDraggableSortOptions) => {
36-
const { draggableActiveId, draggableActiveLayout, draggableOffsets, draggableLayouts } = useDndContext();
35+
const { draggableIds, draggableActiveId, draggableActiveLayout, draggableOffsets, draggableLayouts } =
36+
useDndContext();
3737
const direction = horizontal ? "horizontal" : "vertical";
3838

3939
const draggablePlaceholderIndex = useSharedValue(-1);
@@ -65,25 +65,46 @@ export const useDraggableSort = ({
6565
y: offsets[itemId].y.value,
6666
});
6767

68-
if (shouldSwapWorklet) {
69-
if (shouldSwapWorklet(activeLayout, itemLayout, direction)) {
70-
// console.log(`Found placeholder index ${itemIndex} using custom shouldSwapWorklet!`);
71-
return itemIndex;
72-
}
73-
continue;
74-
}
75-
76-
// Default to center axis
77-
const itemCenterAxis = centerAxis(itemLayout, direction);
78-
if (overlapsAxis(activeLayout, itemCenterAxis, direction)) {
68+
if (shouldSwapWorklet(activeLayout, itemLayout, direction)) {
69+
// console.log(`Found placeholder index ${itemIndex} using custom shouldSwapWorklet!`);
7970
return itemIndex;
8071
}
72+
continue;
8173
}
8274
// Fallback to current index
83-
// console.log(`Fallback to current index ${activeIndex}`);
8475
return activeIndex;
8576
};
8677

78+
// Track added/removed draggable items and update the sort order
79+
useAnimatedReaction(
80+
() => draggableIds.value,
81+
(next, prev) => {
82+
if (prev === null || prev.length === 0) {
83+
return;
84+
}
85+
86+
// Handle removed draggable items
87+
const removedIds = prev.filter((id) => !next.includes(id));
88+
if (removedIds.length > 0) {
89+
draggableSortOrder.value = draggableSortOrder.get().filter((itemId) => !removedIds.includes(itemId));
90+
}
91+
92+
// Handle added draggable items by inserting them at the correct index
93+
const layouts = draggableLayouts.get();
94+
const addedIds = next.filter((id) => !prev.includes(id));
95+
addedIds.forEach((id) => {
96+
const index = Object.entries(layouts)
97+
.sort(([, a], [, b]) => a.get()[horizontal ? "x" : "y"] - b.get()[horizontal ? "x" : "y"])
98+
.findIndex(([key]) => key === id);
99+
const nextOrder = draggableSortOrder.value.slice();
100+
nextOrder.splice(index, 0, id);
101+
// draggableLastOrder.value = draggableSortOrder.value.slice();
102+
draggableSortOrder.value = nextOrder;
103+
});
104+
},
105+
[],
106+
);
107+
87108
// Track active layout changes and update the placeholder index
88109
useAnimatedReaction(
89110
() => [draggableActiveId.value, draggableActiveLayout.value] as const,

src/features/sort/hooks/useDraggableStack.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,25 @@ export const useDraggableStack = ({
3535
shouldSwapWorklet: worklet,
3636
});
3737

38+
// Track items being added or removed from the stack
39+
useAnimatedReaction(
40+
() => draggableSortOrder.value,
41+
(nextOrder, prevOrder) => {
42+
// Ignore initial reaction
43+
if (prevOrder === null) {
44+
return;
45+
}
46+
// Ignore same size stacks
47+
if (nextOrder.length === prevOrder.length) {
48+
return;
49+
}
50+
51+
const { value: layouts } = draggableLayouts;
52+
const { value: offsets } = draggableOffsets;
53+
},
54+
[],
55+
);
56+
3857
// Track sort order changes and update the offsets
3958
useAnimatedReaction(
4059
() => draggableSortOrder.value,

src/hooks/useDraggable.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useLayoutEffect } from "react";
44
import { LayoutRectangle, ViewProps } from "react-native";
5-
import { runOnUI, useSharedValue } from "react-native-reanimated";
5+
import { measure, runOnUI, useSharedValue } from "react-native-reanimated";
66
import { DraggableState, useDndContext } from "../DndContext";
77
import { useLatestSharedValue, useNodeRef } from "../hooks";
88
import { Data, NativeElement, UniqueIdentifier } from "../types";
@@ -50,6 +50,7 @@ export const useDraggable = ({
5050
activationTolerance = Infinity,
5151
}: UseDraggableOptions) => {
5252
const {
53+
draggableIds,
5354
draggableLayouts,
5455
draggableOffsets,
5556
draggableRestingOffsets,
@@ -81,6 +82,10 @@ export const useDraggable = ({
8182
useLayoutEffect(() => {
8283
const runLayoutEffect = () => {
8384
"worklet";
85+
if (draggableIds.value.includes(id)) {
86+
throw new Error(`Duplicate draggable id found: ${id}`);
87+
}
88+
// draggableIds.value = [...draggableIds.value, id]; // We have to wait for layout
8489
draggableLayouts.value[id] = layout;
8590
draggableOffsets.value[id] = offset;
8691
draggableRestingOffsets.value[id] = restingOffset;
@@ -96,6 +101,7 @@ export const useDraggable = ({
96101
delete draggableRestingOffsets.value[id];
97102
delete draggableOptions.value[id];
98103
delete draggableStates.value[id];
104+
draggableIds.value = draggableIds.value.filter((draggableId) => draggableId !== id);
99105
};
100106
// if(node && node.key === key)
101107
runOnUI(cleanupLayoutEffect)();
@@ -106,7 +112,13 @@ export const useDraggable = ({
106112
const onLayout: ViewProps["onLayout"] = () => {
107113
assert(containerRef.current);
108114
node.current?.measureLayout(containerRef.current, (x, y, width, height) => {
109-
layout.value = { x, y, width, height };
115+
runOnUI(() => {
116+
layout.value = { x, y, width, height };
117+
// Only add the draggable once the layout is available
118+
if (!draggableIds.value.includes(id)) {
119+
draggableIds.value = [...draggableIds.get(), id];
120+
}
121+
})();
110122
});
111123
};
112124

0 commit comments

Comments
 (0)