react-native-sortables
Version:
Powerful Sortable Components for Flexible Content Reordering in React Native
487 lines (433 loc) • 16 kB
text/typescript
import type { SharedValue } from 'react-native-reanimated';
import { useAnimatedReaction, useDerivedValue } from 'react-native-reanimated';
import { useMutableValue } from '../../../../../integrations/reanimated';
import type {
Coordinate,
Dimension,
FlexLayout,
ItemSizes,
SortStrategyFactory
} from '../../../../../types';
import {
gt as gt_,
lt as lt_,
reorderInsert,
resolveDimension
} from '../../../../../utils';
import {
getAdditionalSwapOffset,
useCommonValuesContext,
useCustomHandleContext,
useDebugBoundingBox
} from '../../../../shared';
import { useFlexLayoutContext } from '../../FlexLayoutProvider';
import type { ItemGroupSwapResult } from './utils';
import {
getSwappedToGroupAfterIndices,
getSwappedToGroupBeforeIndices,
getTotalGroupSize
} from './utils';
const useInsertStrategy: SortStrategyFactory = () => {
const { activeItemKey, indexToKey, itemHeights, itemWidths, keyToIndex } =
useCommonValuesContext();
const {
appliedLayout,
calculateFlexLayout,
columnGap,
flexDirection,
keyToGroup,
rowGap
} = useFlexLayoutContext();
const { fixedItemKeys } = useCustomHandleContext() ?? {};
const isRow = flexDirection.startsWith('row');
const isReverse = flexDirection.endsWith('reverse');
const gt = isReverse ? lt_ : gt_;
const lt = isReverse ? gt_ : lt_;
let mainCoordinate: Coordinate;
let crossCoordinate: Coordinate;
let mainDimension: Dimension;
let crossDimension: Dimension;
let mainGap: number;
let mainItemSizes: SharedValue<ItemSizes>;
if (isRow) {
mainCoordinate = 'x';
crossCoordinate = 'y';
mainDimension = 'width';
crossDimension = 'height';
mainGap = columnGap;
mainItemSizes = itemWidths;
} else {
mainCoordinate = 'y';
crossCoordinate = 'x';
mainDimension = 'height';
crossDimension = 'width';
mainGap = rowGap;
mainItemSizes = itemHeights;
}
const swappedBeforeIndexes = useMutableValue<ItemGroupSwapResult | null>(
null
);
const swappedAfterIndexes = useMutableValue<ItemGroupSwapResult | null>(null);
const swappedBeforeLayout = useMutableValue<FlexLayout | null>(null);
const swappedAfterLayout = useMutableValue<FlexLayout | null>(null);
const debugBox = useDebugBoundingBox();
const activeGroupIndex = useDerivedValue(() => {
const key = activeItemKey.value;
if (key === null) return null;
return keyToGroup.value[key] ?? null;
});
useAnimatedReaction(
() =>
activeItemKey.value !== null &&
activeGroupIndex.value !== null &&
appliedLayout.value !== null
? {
activeItemIndex: keyToIndex.value[activeItemKey.value]!,
activeItemKey: activeItemKey.value,
currentGroupIndex: activeGroupIndex.value,
fixedKeys: fixedItemKeys?.value,
groupSizeLimit: appliedLayout.value.groupSizeLimit,
indexToKey: indexToKey.value,
itemGroups: appliedLayout.value.itemGroups,
keyToIndex: keyToIndex.value,
mainGap,
mainItemSizes: isRow ? itemWidths.value : itemHeights.value
}
: null,
props => {
swappedBeforeIndexes.value =
props && getSwappedToGroupBeforeIndices(props);
swappedAfterIndexes.value = props && getSwappedToGroupAfterIndices(props);
if (swappedBeforeIndexes.value) {
swappedBeforeLayout.value = calculateFlexLayout(
swappedBeforeIndexes.value.indexToKey
);
}
if (swappedAfterIndexes.value) {
swappedAfterLayout.value = calculateFlexLayout(
swappedAfterIndexes.value.indexToKey
);
}
}
);
return ({
activeIndex,
activeKey,
dimensions: activeItemDimensions,
position
}) => {
'worklet';
if (activeGroupIndex.value === null || appliedLayout.value === null) return;
let currentLayout = appliedLayout.value;
const sharedSwapProps = {
activeItemKey: activeKey,
fixedKeys: fixedItemKeys?.value,
groupSizeLimit: currentLayout.groupSizeLimit,
mainGap,
mainItemSizes: isRow ? itemWidths.value : itemHeights.value
};
// CROSS AXIS BOUNDS
let beforeIndexes = swappedBeforeIndexes.value;
let beforeLayout = swappedBeforeLayout.value;
let groupIndex = activeGroupIndex.value;
let firstAvailableInGroupIndex = activeIndex;
let itemIndexInGroup = 0;
const crossAxisPosition = position[crossCoordinate];
// Group before
let swapGroupBeforeOffset = Infinity;
let swapGroupBeforeBound = Infinity;
do {
if (!beforeIndexes) {
break;
}
if (swapGroupBeforeBound !== Infinity) {
groupIndex = beforeIndexes.groupIndex;
firstAvailableInGroupIndex = beforeIndexes.itemIndex;
itemIndexInGroup = beforeIndexes.itemIndexInGroup;
if (beforeLayout) currentLayout = beforeLayout;
beforeIndexes = getSwappedToGroupBeforeIndices({
...sharedSwapProps,
activeItemIndex: firstAvailableInGroupIndex,
currentGroupIndex: groupIndex,
indexToKey: beforeIndexes.indexToKey,
itemGroups: currentLayout.itemGroups,
keyToIndex: beforeIndexes.keyToIndex
});
if (!beforeIndexes) break;
beforeLayout = calculateFlexLayout(beforeIndexes.indexToKey);
}
swapGroupBeforeOffset =
currentLayout.crossAxisGroupOffsets[groupIndex] ?? 0;
if (groupIndex === 0) {
swapGroupBeforeBound = swapGroupBeforeOffset;
} else {
const swapOffset =
((beforeLayout?.crossAxisGroupOffsets[beforeIndexes.groupIndex] ??
0) +
swapGroupBeforeOffset +
(currentLayout.crossAxisGroupSizes[groupIndex] ?? 0)) /
2;
const additionalSwapOffset = getAdditionalSwapOffset(
beforeLayout?.crossAxisGroupSizes?.[beforeIndexes.groupIndex] ?? 0
);
swapGroupBeforeBound = swapOffset - additionalSwapOffset;
}
} while (
swapGroupBeforeBound > 0 &&
crossAxisPosition < swapGroupBeforeBound
);
// Group after
let afterIndexes = swappedAfterIndexes.value;
let afterLayout = swappedAfterLayout.value;
let swappedAfterGroupsCount =
swappedAfterLayout.value?.itemGroups.length ?? 0;
let swapGroupAfterOffset = -Infinity;
let swapGroupAfterBound = -Infinity;
do {
if (!afterIndexes) {
break;
}
if (swapGroupAfterBound !== -Infinity) {
swappedAfterGroupsCount =
swappedAfterLayout.value?.itemGroups.length ?? 0;
groupIndex = afterIndexes.groupIndex;
firstAvailableInGroupIndex = afterIndexes.itemIndex;
itemIndexInGroup = afterIndexes.itemIndexInGroup;
if (afterLayout) currentLayout = afterLayout;
afterIndexes = getSwappedToGroupAfterIndices({
...sharedSwapProps,
activeItemIndex: firstAvailableInGroupIndex,
currentGroupIndex: groupIndex,
indexToKey: afterIndexes.indexToKey,
itemGroups: currentLayout.itemGroups,
keyToIndex: afterIndexes.keyToIndex
});
if (!afterIndexes) break;
afterLayout = calculateFlexLayout(afterIndexes.indexToKey);
}
const currentGroupBeforeOffset =
currentLayout.crossAxisGroupOffsets[groupIndex] ?? 0;
swapGroupAfterOffset =
currentGroupBeforeOffset +
(currentLayout.crossAxisGroupSizes[groupIndex] ?? 0);
const afterSwapGroupBeforeOffset =
afterLayout?.crossAxisGroupOffsets[afterIndexes.groupIndex] ?? 0;
const afterSwapGroupSize = Math.max(
afterLayout?.crossAxisGroupSizes?.[afterIndexes.groupIndex] ?? 0,
activeItemDimensions[crossDimension]
);
const swapOffset = afterSwapGroupBeforeOffset
? (currentGroupBeforeOffset +
afterSwapGroupBeforeOffset +
afterSwapGroupSize) /
2
: swapGroupAfterOffset;
const additionalSwapOffset = getAdditionalSwapOffset(
afterLayout?.crossAxisGroupSizes?.[afterIndexes.groupIndex] ?? 0
);
swapGroupAfterBound = swapOffset + additionalSwapOffset;
} while (
crossAxisPosition > swapGroupAfterBound &&
groupIndex < swappedAfterGroupsCount &&
groupIndex >= activeGroupIndex.value
);
// MAIN AXIS BOUNDS
// currentGroup is the updated group after new layout calculation
// that contains the active item
const currentGroup = currentLayout.itemGroups[groupIndex];
if (!currentGroup) {
const lastItemIndex = indexToKey.value.length - 1;
if (activeIndex === lastItemIndex) {
return null;
}
return reorderInsert(
indexToKey.value,
activeIndex,
lastItemIndex,
fixedItemKeys?.value
);
}
const mainAxisPosition = position[mainCoordinate];
// Find the itemIndexInGroup of the active item if it is in the same group
if (groupIndex === activeGroupIndex.value) {
const firstItemKey = currentGroup[0];
if (firstItemKey === undefined) return;
const firstItemIndex = keyToIndex.value[firstItemKey];
if (firstItemIndex === undefined) return;
itemIndexInGroup = activeIndex - firstItemIndex;
}
const initialItemIndexInGroup = itemIndexInGroup;
const activeItemMainSize = activeItemDimensions[mainDimension];
// Item after
let swapItemAfterOffset = -Infinity;
let swapItemAfterBound = -Infinity;
let totalGroupSize = 0;
do {
if (swapItemAfterBound !== -Infinity) {
itemIndexInGroup++;
}
const currentItemKey = currentGroup[itemIndexInGroup]!;
const currentItemPosition = currentLayout.itemPositions[currentItemKey];
const itemMainSize = resolveDimension(
mainItemSizes.value,
currentItemKey
);
if (!currentItemPosition || itemMainSize === null) return;
swapItemAfterOffset =
currentItemPosition[mainCoordinate] + (isReverse ? 0 : itemMainSize);
const nextItemKey = currentGroup[itemIndexInGroup + 1];
if (nextItemKey === undefined) {
swapItemAfterBound = swapItemAfterOffset;
break;
}
const nextItemPosition = currentLayout.itemPositions[nextItemKey];
const nextItemMainSize = resolveDimension(
mainItemSizes.value,
nextItemKey
);
if (!nextItemPosition || nextItemMainSize === null) break;
const currentItemMainAxisPosition = currentItemPosition[mainCoordinate];
const nextItemMainAxisPosition = nextItemPosition[mainCoordinate];
const isCurrentBeforeNext =
currentItemMainAxisPosition < nextItemMainAxisPosition;
const sizeToAdd = isCurrentBeforeNext ? nextItemMainSize : itemMainSize;
const averageOffset =
(currentItemMainAxisPosition + nextItemMainAxisPosition + sizeToAdd) /
2;
const additionalSwapOffset = getAdditionalSwapOffset(sizeToAdd);
swapItemAfterBound =
averageOffset + (isCurrentBeforeNext ? 1 : -1) * additionalSwapOffset;
totalGroupSize += itemMainSize + mainGap;
if (totalGroupSize + activeItemMainSize > currentLayout.groupSizeLimit) {
break;
}
} while (
itemIndexInGroup < currentGroup.length - 1 &&
gt(mainAxisPosition, swapItemAfterBound)
);
// Item before
let canBeFirst = true;
const groupBefore = currentLayout.itemGroups[groupIndex - 1];
if (groupBefore && itemIndexInGroup > 0) {
const groupBeforeSize = getTotalGroupSize(
groupBefore,
mainItemSizes.value,
mainGap
);
canBeFirst =
groupBeforeSize + activeItemMainSize + mainGap >
currentLayout.groupSizeLimit;
}
let swapItemBeforeOffset = Infinity;
let swapItemBeforeBound = Infinity;
do {
if (swapItemBeforeBound !== Infinity) {
itemIndexInGroup--;
}
const currentItemKey = currentGroup[itemIndexInGroup]!;
const currentItemPosition = currentLayout.itemPositions[currentItemKey];
const currentItemMainSize = resolveDimension(
mainItemSizes.value,
currentItemKey
);
if (!currentItemPosition || currentItemMainSize === null) return;
swapItemBeforeOffset =
currentItemPosition[mainCoordinate] +
(isReverse ? currentItemMainSize : 0);
const prevItemKey = currentGroup[itemIndexInGroup - 1];
if (prevItemKey === undefined) {
swapItemBeforeBound = swapItemBeforeOffset;
break;
}
const prevItemPosition = currentLayout.itemPositions[prevItemKey];
const prevItemMainSize = resolveDimension(
mainItemSizes.value,
prevItemKey
);
if (!prevItemPosition || prevItemMainSize === null) return;
const currentItemMainAxisPosition = currentItemPosition[mainCoordinate];
const prevItemMainAxisPosition = prevItemPosition[mainCoordinate];
const isPrevBeforeCurrent =
prevItemMainAxisPosition < currentItemMainAxisPosition;
const sizeToAdd = isPrevBeforeCurrent
? currentItemMainSize
: prevItemMainSize;
const averageOffset =
(prevItemMainAxisPosition + currentItemMainAxisPosition + sizeToAdd) /
2;
const additionalSwapOffset = getAdditionalSwapOffset(sizeToAdd);
swapItemBeforeBound =
averageOffset - (isPrevBeforeCurrent ? 1 : -1) * additionalSwapOffset;
} while (
// handle edge case when the active item cannot be the first item of
// the current group
itemIndexInGroup > (canBeFirst ? 0 : 1) &&
lt(mainAxisPosition, swapItemBeforeBound)
);
// DEBUG ONLY
if (debugBox) {
if (swapGroupAfterOffset > swapGroupAfterBound) {
swapGroupAfterOffset = swapGroupAfterBound;
}
if (swapGroupBeforeOffset < swapGroupBeforeBound) {
swapGroupBeforeOffset = swapGroupBeforeBound;
}
if (swapItemAfterOffset > swapItemAfterBound) {
swapItemAfterOffset = swapItemAfterBound;
}
if (swapItemBeforeOffset < swapItemBeforeBound) {
swapItemBeforeOffset = swapItemBeforeBound;
}
if (isRow) {
debugBox.top.update(
{ x: swapItemBeforeBound, y: swapGroupBeforeBound },
{ x: swapItemAfterBound, y: swapGroupBeforeOffset }
);
debugBox.bottom.update(
{ x: swapItemBeforeBound, y: swapGroupAfterBound },
{ x: swapItemAfterBound, y: swapGroupAfterOffset }
);
debugBox.right.update(
{ x: swapItemAfterBound, y: swapGroupBeforeBound },
{ x: swapItemAfterOffset, y: swapGroupAfterBound }
);
debugBox.left.update(
{ x: swapItemBeforeBound, y: swapGroupBeforeBound },
{ x: swapItemBeforeOffset, y: swapGroupAfterBound }
);
} else {
debugBox.top.update(
{ x: swapGroupBeforeBound, y: swapItemBeforeBound },
{ x: swapGroupAfterBound, y: swapItemBeforeOffset }
);
debugBox.bottom.update(
{ x: swapGroupAfterBound, y: swapItemAfterOffset },
{ x: swapGroupBeforeBound, y: swapItemAfterBound }
);
debugBox.right.update(
{ x: swapGroupAfterOffset, y: swapItemBeforeBound },
{ x: swapGroupAfterBound, y: swapItemAfterBound }
);
debugBox.left.update(
{ x: swapGroupBeforeBound, y: swapItemBeforeBound },
{ x: swapGroupBeforeOffset, y: swapItemAfterBound }
);
}
}
const newIndex =
firstAvailableInGroupIndex + (itemIndexInGroup - initialItemIndexInGroup);
if (
newIndex === activeIndex ||
fixedItemKeys?.value?.[indexToKey.value[newIndex]!]
) {
return;
}
return reorderInsert(
indexToKey.value,
activeIndex,
newIndex,
fixedItemKeys?.value
);
};
};
export default useInsertStrategy;