Skip to content

Commit

Permalink
feat: added mode swap
Browse files Browse the repository at this point in the history
  • Loading branch information
le-ar committed Jul 28, 2024
1 parent ec4d507 commit f80eeeb
Show file tree
Hide file tree
Showing 12 changed files with 231 additions and 61 deletions.
2 changes: 1 addition & 1 deletion example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -673,4 +673,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: 329483eb6daf495a1eab8db2c188f13aaa25dcf9

COCOAPODS: 1.13.0
COCOAPODS: 1.15.2
2 changes: 1 addition & 1 deletion example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { PairsDragDrop } from './PairsDragDrop';
import { SimpleDragDrop } from './SimpleDragDrop';

export default function App() {
const [step, setStep] = useState(1);
const [step, setStep] = useState(0);
const onNextStep = useCallback(() => {
setStep((s) => (s + 1) % 2);
}, []);
Expand Down
1 change: 1 addition & 0 deletions example/src/SimpleDragDrop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export function SimpleDragDrop(props: SimpleDragDropProps) {
renderItem={renderItem}
extractId={extractId}
style={style}
config={{ mode: 'SWAP', axis: { horizontal: true, vertical: true } }}
/>
);
}
4 changes: 2 additions & 2 deletions src/components/DragDropContextRootView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,8 @@ export function DragDropContextRootView(props: DragDropContextRootViewProps) {
}, [startDragMeasureInfo]);

const value = useMemo<DragDropContextType>(
() => ({ registerDragDropArea, removeDragDropArea, startDrag }),
[registerDragDropArea, removeDragDropArea, startDrag]
() => ({ isMoving, registerDragDropArea, removeDragDropArea, startDrag }),
[isMoving, registerDragDropArea, removeDragDropArea, startDrag]
);

const gesture = useDragDropMove(
Expand Down
1 change: 1 addition & 0 deletions src/components/DragDropItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ export const DragDropItem = React.memo(function DragDropItem<T>(
return (
<Animated.View
ref={ref}
key={itemId}
layout={customAnimation}
onLayout={({ nativeEvent }) => {
size.value = {
Expand Down
2 changes: 2 additions & 0 deletions src/contexts/DragDropContext.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React from 'react';
import type { DragDropAreaHandler } from '../handlers';
import type { DragDropItemType } from '../types';
import type { SharedValue } from 'react-native-reanimated';

export interface DragDropContextType {
isMoving?: SharedValue<boolean>;
startDrag: (
area: {
id: number;
Expand Down
47 changes: 41 additions & 6 deletions src/helpers/DoublyLinkedList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export type DoublyLinkedListHistory<T> =
| DoublyLinkedListHistoryItem<
'MoveAfter',
{ id: DoublyLinkedListNodeId; afterId: DoublyLinkedListNodeId | null }
>
| DoublyLinkedListHistoryItem<
'Swap',
{ ids: [id: DoublyLinkedListNodeId, id: DoublyLinkedListNodeId] }
>;

export type DoublyLinkedListHistoryTypes =
Expand Down Expand Up @@ -275,12 +279,7 @@ export function DLAddItemBefore<T>(
u?: undefined
) {
'worklet';
const nextItem =
beforeId == null
? list.tail == null
? null
: list.nodes[list.tail]
: list.nodes[beforeId];
const nextItem = beforeId == null ? null : list.nodes[beforeId];

if (nextItem === undefined) {
return list;
Expand Down Expand Up @@ -397,6 +396,39 @@ export function DLMoveItemAfter<T>(
return list;
}

export function DLSwapItems<T>(
list: DoublyLinkedList<T>,
itemIds: [id: number | string, id: number | string]
) {
'worklet';
const items = [list.nodes[itemIds[0]], list.nodes[itemIds[1]]];
if (items.some((i) => i == null)) {
return list;
}

DLRemoveItem(list, itemIds[0], 1 as unknown as undefined);
DLRemoveItem(list, itemIds[1], 1 as unknown as undefined);

DLAddItemBefore(
list,
items[1]!.id,
items[1]!.data,
items[0]!.nextId === items[1]!.id ? items[1]!.nextId : items[0]!.nextId,
1 as unknown as undefined
);
DLAddItemBefore(
list,
items[0]!.id,
items[0]!.data,
items[1]!.nextId === items[0]!.nextId ? items[1]!.id : items[1]!.nextId,
1 as unknown as undefined
);

DLAddHistory(list, 'Swap', { ids: [...itemIds] });

return list;
}

export function DLRestoreHistory<T>(
list: DoublyLinkedList<T>,
historyItem: DoublyLinkedListHistory<T>
Expand Down Expand Up @@ -425,6 +457,8 @@ export function DLRestoreHistory<T>(
DLMoveItemAfter(list, historyItem.data.id, historyItem.data.afterId);
} else if (historyItem.type === 'MoveBefore') {
DLMoveItemBefore(list, historyItem.data.id, historyItem.data.beforeId);
} else if (historyItem.type === 'Swap') {
DLSwapItems(list, historyItem.data.ids);
}

return list;
Expand All @@ -438,3 +472,4 @@ export const SDLAddItemBefore = cModify(DLAddItemBefore);
export const SDLRemoveItem = cModify(DLRemoveItem);
export const SDLMoveItemBefore = cModify(DLMoveItemBefore);
export const SDLMoveItemAfter = cModify(DLMoveItemAfter);
export const SDLSwapItems = cModify(DLSwapItems);
6 changes: 5 additions & 1 deletion src/hooks/useDragDropList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ export function useDragDropList<T, TComponent extends Component>(
listConfig
);
const { itemMoved, tryPutItem, removeItem, onTransitionDone } =
useDragDropListItemsActions(itemsListShared, findNeighboursItem);
useDragDropListItemsActions(
itemsListShared,
config?.mode ?? 'MOVE',
findNeighboursItem
);
const { areaId } = useDragDropArea(
areaRef,
{
Expand Down
204 changes: 157 additions & 47 deletions src/hooks/useDragDropListItemsActions.ts
Original file line number Diff line number Diff line change
@@ -1,85 +1,75 @@
import { useCallback } from 'react';
import { modify } from '@procraft/react-native-autoscroll';
import { useCallback, useContext, useMemo } from 'react';
import {
useDerivedValue,
useSharedValue,
type MeasuredDimensions,
type SharedValue,
} from 'react-native-reanimated';
import { DragDropContext } from '../contexts';
import type { DragDropAreaHandler } from '../handlers';
import {
SDLAddItemAfter,
SDLAddItemBefore,
SDLMoveItemAfter,
SDLMoveItemBefore,
SDLRemoveItem,
SDLSwapItems,
type DoublyLinkedList,
} from '../helpers';
import type { DragDropItemId, DragDropItemType, ItemPosition } from '../types';
import type {
DragDropAreaConfigMode,
DragDropItemId,
DragDropItemType,
ItemPosition,
} from '../types';
import { useSharedMap } from './useSharedMap';

export function useDragDropListItemsActions<T>(
itemsListShared: SharedValue<DoublyLinkedList<DragDropItemType<T>>>,
mode: DragDropAreaConfigMode,
findNeighboursItem: (
itemId: string | number,
hoverMeasurement: MeasuredDimensions
) => ItemPosition | null,
onRemoveItem?: (itemId: DragDropItemId) => void
) {
const { isMoving } = useContext(DragDropContext);

const prevSwap = useSharedValue<
[id: DragDropItemId, id: DragDropItemId] | null
>(null);

useDerivedValue(() => {
if (!isMoving?.value) {
prevSwap.value = null;
}
}, [isMoving]);

const [movingIds, addMovingId, removeMovingId] = useSharedMap<
DragDropItemId,
null
>({});

const isItemMoved = useCallback(
(position: ItemPosition | null) => {
'worklet';
if (position?.afterId != null) {
return position?.afterId in movingIds.value;
}
if (position?.beforeId != null) {
return position?.beforeId in movingIds.value;
}
return false;
},
[movingIds]
const moveBehavior = useMemo(
() =>
createMoveStrategy(
mode,
movingIds,
prevSwap,
itemsListShared,
addMovingId
),
[addMovingId, itemsListShared, mode, movingIds, prevSwap]
);

const itemMoved = useCallback<DragDropAreaHandler<T>['itemMoved']>(
(item, hoverMeasurement) => {
'worklet';
const movedResult = findNeighboursItem(item.id, hoverMeasurement);

const listItem = itemsListShared.value.nodes[item.id];
const canMove =
movedResult == null
? false
: !isItemMoved(movedResult) && listItem != null;
if (
canMove &&
movedResult?.afterId !== undefined &&
listItem!.prevId !== movedResult.afterId
) {
SDLMoveItemAfter(itemsListShared, [item.id, movedResult.afterId], true);
if (movedResult.afterId != null) {
addMovingId(movedResult.afterId, null);
}
} else if (
canMove &&
movedResult?.beforeId !== undefined &&
listItem!.nextId !== movedResult.beforeId
) {
SDLMoveItemBefore(
itemsListShared,
[item.id, movedResult.beforeId],
true
);
if (movedResult.beforeId != null) {
addMovingId(movedResult.beforeId, null);
}
} else {
return null;
}
return movedResult;
return moveBehavior(movedResult, item);
},
[addMovingId, findNeighboursItem, isItemMoved, itemsListShared]
[findNeighboursItem, moveBehavior]
);

const tryPutItem = useCallback<DragDropAreaHandler<T>['tryPutItem']>(
Expand Down Expand Up @@ -124,3 +114,123 @@ export function useDragDropListItemsActions<T>(

return { itemMoved, tryPutItem, removeItem, onTransitionDone };
}

type MoveStrategy<T> = (
movedResult: ItemPosition | null,
item: DragDropItemType<T>
) => ItemPosition | null;

function createMoveStrategy<T>(
mode: 'MOVE' | 'SWAP',
movingIds: SharedValue<Record<DragDropItemId, null>>,
prevSwap: SharedValue<[id: DragDropItemId, id: DragDropItemId] | null>,
itemsListShared: SharedValue<DoublyLinkedList<DragDropItemType<T>>>,
addMovingId: (id: DragDropItemId, value: null) => void
): MoveStrategy<T> {
if (mode === 'MOVE') {
return createMoveBehavior(movingIds, itemsListShared, addMovingId);
} else if (mode === 'SWAP') {
return createSwapBehavior(
movingIds,
prevSwap,
itemsListShared,
addMovingId
);
} else {
throw new Error(`Unknown mode: ${mode}`);
}
}

function createSwapBehavior<T>(
movingIds: SharedValue<Record<DragDropItemId, null>>,
prevSwap: SharedValue<[id: DragDropItemId, id: DragDropItemId] | null>,
itemsListShared: SharedValue<DoublyLinkedList<DragDropItemType<T>>>,
addMovingId: (id: DragDropItemId, value: null) => void
): MoveStrategy<T> {
'worklet';
return (movedResult, item) => {
'worklet';

const itemHoveredId = movedResult?.overId;
if (
itemHoveredId == null ||
itemHoveredId === item.id ||
itemHoveredId in movingIds.value
) {
return null;
}

const inPrevPosition =
prevSwap.value?.[0] === item.id && prevSwap.value?.[1] === itemHoveredId;
if (prevSwap.value != null && !inPrevPosition) {
SDLSwapItems(itemsListShared, [prevSwap.value], true);
addMovingId(prevSwap.value[1], null);
}
SDLSwapItems(itemsListShared, [[item.id, itemHoveredId]], true);
addMovingId(itemHoveredId, null);
modify(
prevSwap,
() => {
'worklet';
return inPrevPosition ? null : [item.id, itemHoveredId];
},
true
);

return movedResult;
};
}

function createMoveBehavior<T>(
movingIds: SharedValue<Record<DragDropItemId, null>>,
itemsListShared: SharedValue<DoublyLinkedList<DragDropItemType<T>>>,
addMovingId: (id: DragDropItemId, value: null) => void
): MoveStrategy<T> {
'worklet';
return (movedResult, item) => {
const listItem = itemsListShared.value.nodes[item.id];
const canMove =
movedResult == null
? false
: !isItemMoved(movingIds, movedResult) && listItem != null;

if (
canMove &&
movedResult?.afterId !== undefined &&
listItem!.prevId !== movedResult.afterId
) {
SDLMoveItemAfter(itemsListShared, [item.id, movedResult.afterId], true);

if (movedResult.afterId != null) {
addMovingId(movedResult.afterId, null);
}
} else if (
canMove &&
movedResult?.beforeId !== undefined &&
listItem!.nextId !== movedResult.beforeId
) {
SDLMoveItemBefore(itemsListShared, [item.id, movedResult.beforeId], true);
if (movedResult.beforeId != null) {
addMovingId(movedResult.beforeId, null);
}
} else {
return null;
}

return movedResult;
};
}

function isItemMoved(
movingIds: SharedValue<Record<DragDropItemId, null>>,
position: ItemPosition | null
) {
'worklet';
if (position?.afterId != null) {
return position?.afterId in movingIds.value;
}
if (position?.beforeId != null) {
return position?.beforeId in movingIds.value;
}
return false;
}
Loading

0 comments on commit f80eeeb

Please sign in to comment.