Skip to content

Commit

Permalink
feat(sort): add improved/updated sort implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
mgcrea committed Dec 19, 2023
1 parent 14159a8 commit d1bf845
Show file tree
Hide file tree
Showing 7 changed files with 488 additions and 0 deletions.
59 changes: 59 additions & 0 deletions src/features/sort/components/DraggableGrid.tsx
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>;
};
57 changes: 57 additions & 0 deletions src/features/sort/components/DraggableStack.tsx
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>;
};
100 changes: 100 additions & 0 deletions src/features/sort/hooks/useDraggableGrid.ts
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 };
};
143 changes: 143 additions & 0 deletions src/features/sort/hooks/useDraggableSort.ts
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 };
};
Loading

0 comments on commit d1bf845

Please sign in to comment.