-
-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(sort): add improved/updated sort implementation
- Loading branch information
Showing
7 changed files
with
488 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import React, { Children, useMemo, type FunctionComponent, type PropsWithChildren } from "react"; | ||
import { View, type FlexStyle, type ViewProps } from "react-native"; | ||
import type { UniqueIdentifier } from "../../../types"; | ||
import { useDraggableGrid, type UseDraggableGridOptions } from "../hooks/useDraggableGrid"; | ||
|
||
export type DraggableGridProps = Pick<ViewProps, "style"> & | ||
Pick<UseDraggableGridOptions, "onOrderChange" | "onOrderUpdate" | "shouldSwapWorklet"> & { | ||
direction?: FlexStyle["flexDirection"]; | ||
size?: number; | ||
gap?: number; | ||
}; | ||
|
||
export const DraggableGrid: FunctionComponent<PropsWithChildren<DraggableGridProps>> = ({ | ||
children, | ||
direction = "row", | ||
gap = 0, | ||
onOrderChange, | ||
onOrderUpdate, | ||
shouldSwapWorklet, | ||
size = 3, | ||
style: styleProp, | ||
}) => { | ||
const initialOrder = useMemo( | ||
() => | ||
Children.map(children, (child) => { | ||
// console.log("in"); | ||
if (React.isValidElement(child)) { | ||
return child.props.id; | ||
} | ||
return null; | ||
})?.filter(Boolean) as UniqueIdentifier[], | ||
[children], | ||
); | ||
|
||
const style = useMemo( | ||
() => | ||
Object.assign( | ||
{ | ||
flexDirection: direction, | ||
gap, | ||
flexWrap: "wrap", | ||
}, | ||
styleProp, | ||
), | ||
[gap, direction, styleProp], | ||
); | ||
|
||
useDraggableGrid({ | ||
direction: style.flexDirection, | ||
gap: style.gap, | ||
initialOrder, | ||
onOrderChange, | ||
onOrderUpdate, | ||
shouldSwapWorklet, | ||
size, | ||
}); | ||
|
||
return <View style={style}>{children}</View>; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import React, { Children, useMemo, type FunctionComponent, type PropsWithChildren } from "react"; | ||
import { View, type FlexStyle, type ViewProps } from "react-native"; | ||
import type { UniqueIdentifier } from "../../../types"; | ||
import { useDraggableStack, type UseDraggableStackOptions } from "../hooks/useDraggableStack"; | ||
|
||
export type DraggableStackProps = Pick<ViewProps, "style"> & | ||
Pick<UseDraggableStackOptions, "onOrderChange" | "onOrderUpdate" | "shouldSwapWorklet"> & { | ||
direction?: FlexStyle["flexDirection"]; | ||
gap?: number; | ||
}; | ||
|
||
export const DraggableStack: FunctionComponent<PropsWithChildren<DraggableStackProps>> = ({ | ||
children, | ||
direction = "row", | ||
gap = 0, | ||
onOrderChange, | ||
onOrderUpdate, | ||
shouldSwapWorklet, | ||
style: styleProp, | ||
}) => { | ||
const initialOrder = useMemo( | ||
() => | ||
Children.map(children, (child) => { | ||
// console.log("in"); | ||
if (React.isValidElement(child)) { | ||
return child.props.id; | ||
} | ||
return null; | ||
})?.filter(Boolean) as UniqueIdentifier[], | ||
[children], | ||
); | ||
|
||
const style = useMemo( | ||
() => | ||
Object.assign( | ||
{ | ||
flexDirection: direction, | ||
gap, | ||
}, | ||
styleProp, | ||
), | ||
[gap, direction, styleProp], | ||
); | ||
|
||
const horizontal = ["row", "row-reverse"].includes(style.flexDirection); | ||
|
||
useDraggableStack({ | ||
gap: style.gap, | ||
horizontal, | ||
initialOrder, | ||
onOrderChange, | ||
onOrderUpdate, | ||
shouldSwapWorklet, | ||
}); | ||
|
||
return <View style={style}>{children}</View>; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import { type FlexStyle } from "react-native"; | ||
import { useAnimatedReaction } from "react-native-reanimated"; | ||
import { swapByItemCenterPoint } from "../../../utils"; | ||
import { useDndContext } from "./../../../DndContext"; | ||
import { useDraggableSort, type UseDraggableSortOptions } from "./useDraggableSort"; | ||
|
||
export type UseDraggableGridOptions = Pick< | ||
UseDraggableSortOptions, | ||
"initialOrder" | "onOrderChange" | "onOrderUpdate" | "shouldSwapWorklet" | ||
> & { | ||
gap: number; | ||
size: number; | ||
direction: FlexStyle["flexDirection"]; | ||
}; | ||
|
||
export const useDraggableGrid = ({ | ||
initialOrder, | ||
onOrderChange, | ||
onOrderUpdate, | ||
gap, | ||
size, | ||
direction = "row", | ||
shouldSwapWorklet = swapByItemCenterPoint, | ||
}: UseDraggableGridOptions) => { | ||
const { draggableActiveId, draggableOffsets, draggableRestingOffsets, draggableLayouts } = useDndContext(); | ||
const horizontal = ["row", "row-reverse"].includes(direction); | ||
|
||
const { draggablePlaceholderIndex, draggableSortOrder } = useDraggableSort({ | ||
horizontal, | ||
initialOrder, | ||
onOrderChange, | ||
onOrderUpdate, | ||
shouldSwapWorklet, | ||
}); | ||
|
||
// Track sort order changes and update the offsets | ||
useAnimatedReaction( | ||
() => draggableSortOrder.value, | ||
(nextOrder, prevOrder) => { | ||
// Ignore initial reaction | ||
if (prevOrder === null) { | ||
return; | ||
} | ||
const { value: activeId } = draggableActiveId; | ||
const { value: layouts } = draggableLayouts; | ||
const { value: offsets } = draggableOffsets; | ||
const { value: restingOffsets } = draggableRestingOffsets; | ||
if (!activeId) { | ||
return; | ||
} | ||
|
||
const activeLayout = layouts[activeId].value; | ||
const { width, height } = activeLayout; | ||
const restingOffset = restingOffsets[activeId]; | ||
// const prevOffset = applyOffset(activeLayout, { x: restingOffset.x.value, y: restingOffset.y.value }); | ||
|
||
for (let nextIndex = 0; nextIndex < nextOrder.length; nextIndex++) { | ||
const itemId = nextOrder[nextIndex]; | ||
const prevIndex = prevOrder.findIndex((id) => id === itemId); | ||
// Skip items that haven't changed position | ||
if (nextIndex === prevIndex) { | ||
continue; | ||
} | ||
|
||
const prevRow = Math.floor(prevIndex / size); | ||
const prevCol = prevIndex % size; | ||
const nextRow = Math.floor(nextIndex / size); | ||
const nextCol = nextIndex % size; | ||
const moveCol = nextCol - prevCol; | ||
const moveRow = nextRow - prevRow; | ||
|
||
const offset = itemId === activeId ? restingOffset : offsets[itemId]; | ||
|
||
switch (direction) { | ||
case "row": | ||
offset.x.value += moveCol * (width + gap); | ||
offset.y.value += moveRow * (height + gap); | ||
break; | ||
case "row-reverse": | ||
offset.x.value += -1 * moveCol * (width + gap); | ||
offset.y.value += moveRow * (height + gap); | ||
break; | ||
case "column": | ||
offset.y.value += moveCol * (width + gap); | ||
offset.x.value += moveRow * (height + gap); | ||
break; | ||
case "column-reverse": | ||
offset.y.value += -1 * moveCol * (width + gap); | ||
offset.x.value += moveRow * (height + gap); | ||
break; | ||
default: | ||
break; | ||
} | ||
} | ||
}, | ||
[direction, gap, size], | ||
); | ||
|
||
return { draggablePlaceholderIndex, draggableSortOrder }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
import { LayoutRectangle } from "react-native"; | ||
import { runOnJS, useAnimatedReaction, useSharedValue } from "react-native-reanimated"; | ||
import { useDndContext } from "../../../DndContext"; | ||
import type { UniqueIdentifier } from "../../../types"; | ||
import { | ||
applyOffset, | ||
arraysEqual, | ||
centerAxis, | ||
moveArrayIndex, | ||
overlapsAxis, | ||
type Rectangle, | ||
} from "../../../utils"; | ||
|
||
export type UseDraggableSortOptions = { | ||
initialOrder?: UniqueIdentifier[]; | ||
horizontal?: boolean; | ||
onOrderChange?: (value: UniqueIdentifier[]) => void; | ||
onOrderUpdate?: (nextOrder: UniqueIdentifier[], prevOrder: UniqueIdentifier[]) => void; | ||
shouldSwapWorklet?: (activeLayout: Rectangle, itemLayout: Rectangle) => boolean; | ||
}; | ||
|
||
export const useDraggableSort = ({ | ||
horizontal = false, | ||
initialOrder = [], | ||
onOrderChange, | ||
onOrderUpdate, | ||
shouldSwapWorklet, | ||
}: UseDraggableSortOptions) => { | ||
const { draggableActiveId, draggableActiveLayout, draggableOffsets, draggableLayouts } = useDndContext(); | ||
|
||
const draggablePlaceholderIndex = useSharedValue(-1); | ||
const draggableLastOrder = useSharedValue<UniqueIdentifier[]>(initialOrder); | ||
const draggableSortOrder = useSharedValue<UniqueIdentifier[]>(initialOrder); | ||
|
||
// Core placeholder index logic | ||
const findPlaceholderIndex = (activeLayout: LayoutRectangle): number => { | ||
"worklet"; | ||
const { value: activeId } = draggableActiveId; | ||
const { value: layouts } = draggableLayouts; | ||
const { value: offsets } = draggableOffsets; | ||
const { value: sortOrder } = draggableSortOrder; | ||
const activeIndex = sortOrder.findIndex((id) => id === activeId); | ||
// const activeCenterPoint = centerPoint(activeLayout); | ||
// console.log(`activeLayout: ${JSON.stringify(activeLayout)}`); | ||
for (let itemIndex = 0; itemIndex < sortOrder.length; itemIndex++) { | ||
const itemId = sortOrder[itemIndex]; | ||
if (itemId === activeId) { | ||
continue; | ||
} | ||
if (!layouts[itemId]) { | ||
console.warn(`Unexpected missing layout ${itemId} in layouts!`); | ||
continue; | ||
} | ||
const itemLayout = applyOffset(layouts[itemId].value, { | ||
x: offsets[itemId].x.value, | ||
y: offsets[itemId].y.value, | ||
}); | ||
|
||
if (shouldSwapWorklet) { | ||
if (shouldSwapWorklet(activeLayout, itemLayout)) { | ||
// console.log(`Found placeholder index ${itemIndex} using custom shouldSwapWorklet!`); | ||
return itemIndex; | ||
} | ||
continue; | ||
} | ||
|
||
// Default to center axis | ||
const itemCenterAxis = centerAxis(itemLayout, horizontal); | ||
if (overlapsAxis(activeLayout, itemCenterAxis, horizontal)) { | ||
return itemIndex; | ||
} | ||
} | ||
// Fallback to current index | ||
// console.log(`Fallback to current index ${activeIndex}`); | ||
return activeIndex; | ||
}; | ||
|
||
// Track active layout changes and update the placeholder index | ||
useAnimatedReaction( | ||
() => [draggableActiveId.value, draggableActiveLayout.value] as const, | ||
([nextActiveId, nextActiveLayout], prev) => { | ||
// Ignore initial reaction | ||
if (prev === null) { | ||
return; | ||
} | ||
const [_prevActiveId, _prevActiveLayout] = prev; | ||
// No active layout | ||
if (nextActiveLayout === null) { | ||
return; | ||
} | ||
// Reset the placeholder index when the active id changes | ||
if (nextActiveId === null) { | ||
draggablePlaceholderIndex.value = -1; | ||
return; | ||
} | ||
// const axis = direction === "row" ? "x" : "y"; | ||
// const delta = prevActiveLayout !== null ? nextActiveLayout[axis] - prevActiveLayout[axis] : 0; | ||
draggablePlaceholderIndex.value = findPlaceholderIndex(nextActiveLayout); | ||
}, | ||
[], | ||
); | ||
|
||
// Track placeholder index changes and update the sort order | ||
useAnimatedReaction( | ||
() => [draggableActiveId.value, draggablePlaceholderIndex.value] as const, | ||
(next, prev) => { | ||
// Ignore initial reaction | ||
if (prev === null) { | ||
return; | ||
} | ||
const [_prevActiveId, prevPlaceholderIndex] = prev; | ||
const [nextActiveId, nextPlaceholderIndex] = next; | ||
const { value: prevOrder } = draggableSortOrder; | ||
// if (nextPlaceholderIndex !== prevPlaceholderIndex) { | ||
// console.log(`Placeholder index changed from ${prevPlaceholderIndex} to ${nextPlaceholderIndex}`); | ||
// } | ||
if (prevPlaceholderIndex !== -1 && nextPlaceholderIndex === -1) { | ||
// Notify the parent component of the order change | ||
if (nextActiveId === null && onOrderChange) { | ||
if (!arraysEqual(prevOrder, draggableLastOrder.value)) { | ||
runOnJS(onOrderChange)(prevOrder); | ||
} | ||
draggableLastOrder.value = prevOrder; | ||
} | ||
} | ||
// Only update the sort order when the placeholder index changes between two valid values | ||
if (prevPlaceholderIndex === -1 || nextPlaceholderIndex === -1) { | ||
return; | ||
} | ||
// Finally update the sort order | ||
const nextOrder = moveArrayIndex(prevOrder, prevPlaceholderIndex, nextPlaceholderIndex); | ||
// Notify the parent component of the order update | ||
if (onOrderUpdate) { | ||
runOnJS(onOrderUpdate)(nextOrder, prevOrder); | ||
} | ||
|
||
draggableSortOrder.value = nextOrder; | ||
}, | ||
[onOrderChange], | ||
); | ||
|
||
return { draggablePlaceholderIndex, draggableSortOrder }; | ||
}; |
Oops, something went wrong.