react-native-sortables
Version:
Powerful Sortable Components for Flexible Content Reordering in React Native
268 lines (245 loc) • 8.39 kB
text/typescript
import { type SharedValue, useDerivedValue } from 'react-native-reanimated';
import type {
Coordinate,
Dimension,
ReorderFunction,
SortStrategyFactory
} from '../../../../types';
import {
getAdditionalSwapOffset,
useCommonValuesContext,
useCustomHandleContext,
useDebugBoundingBox
} from '../../../shared';
import { useAutoOffsetAdjustmentContext } from '../../AutoOffsetAdjustmentProvider';
import { useGridLayoutContext } from '../GridLayoutProvider';
import { calculateLayout, getCrossIndex, getMainIndex } from '../utils';
export const createGridStrategy =
(
useInactiveIndexToKey: () => SharedValue<Array<string>>,
reorder: ReorderFunction
): SortStrategyFactory =>
() => {
const {
containerHeight,
containerWidth,
indexToKey,
itemHeights,
itemWidths
} = useCommonValuesContext();
const { crossGap, isVertical, mainGap, mainGroupSize, numGroups } =
useGridLayoutContext();
const { additionalCrossOffset } = useAutoOffsetAdjustmentContext() ?? {};
const { fixedItemKeys } = useCustomHandleContext() ?? {};
const othersIndexToKey = useInactiveIndexToKey();
const debugBox = useDebugBoundingBox();
const othersLayout = useDerivedValue(() =>
additionalCrossOffset?.value === null
? null
: calculateLayout({
gaps: {
cross: crossGap.value,
main: mainGap.value
},
indexToKey: othersIndexToKey.value,
isVertical,
itemHeights: itemHeights.value,
itemWidths: itemWidths.value,
numGroups,
startCrossOffset: additionalCrossOffset?.value
})
);
let mainContainerSize: SharedValue<null | number>;
let crossContainerSize: SharedValue<null | number>;
let mainCoordinate: Coordinate;
let crossCoordinate: Coordinate;
let crossDimension: Dimension;
if (isVertical) {
mainContainerSize = containerWidth;
crossContainerSize = containerHeight;
mainCoordinate = 'x';
crossCoordinate = 'y';
crossDimension = 'height';
} else {
mainContainerSize = containerHeight;
crossContainerSize = containerWidth;
mainCoordinate = 'y';
crossCoordinate = 'x';
crossDimension = 'width';
}
return ({ activeIndex, dimensions, position }) => {
'worklet';
if (
!othersLayout.value ||
crossContainerSize.value === null ||
mainContainerSize.value === null ||
mainGroupSize.value === null
) {
return;
}
const { crossAxisOffsets } = othersLayout.value;
const startCrossIndex = getCrossIndex(activeIndex, numGroups);
const startMainIndex = getMainIndex(activeIndex, numGroups);
const startCrossSize = dimensions[crossDimension];
let crossIndex = startCrossIndex;
let mainIndex = startMainIndex;
const getItemCrossSize = (index: number) =>
crossAxisOffsets[index] !== undefined &&
crossAxisOffsets[index + 1] !== undefined
? crossAxisOffsets[index + 1]! -
crossAxisOffsets[index] -
crossGap.value
: null;
// CROSS AXIS BOUNDS
// Before bound
let crossBeforeOffset = -Infinity;
let crossBeforeBound = Infinity;
let crossCurrentSize = startCrossSize;
do {
if (crossBeforeBound !== Infinity) {
crossIndex--;
}
crossBeforeOffset = crossAxisOffsets[crossIndex] ?? 0;
const crossBeforeSize = getItemCrossSize(crossIndex - 1);
if (crossBeforeSize) {
const swapOffset =
((crossAxisOffsets[crossIndex - 1] ?? 0) +
crossBeforeOffset +
crossCurrentSize) /
2;
const additionalBeforeOffset =
getAdditionalSwapOffset(crossBeforeSize);
crossBeforeBound = swapOffset - additionalBeforeOffset;
crossCurrentSize = crossBeforeSize;
} else {
crossBeforeBound = 0;
}
} while (
crossBeforeBound > 0 &&
position[crossCoordinate] < crossBeforeBound
);
// After bound
let crossAfterOffset = Infinity;
let crossAfterBound = -Infinity;
crossCurrentSize = startCrossSize;
do {
if (crossAfterBound !== -Infinity) {
crossIndex++;
}
const nextCrossAxisOffset = crossAxisOffsets[crossIndex + 1];
if (!nextCrossAxisOffset) {
break;
}
crossAfterOffset =
(crossAxisOffsets[crossIndex] ?? 0) + crossCurrentSize;
const crossAfterSize = getItemCrossSize(crossIndex + 1);
const swapOffset = (crossAfterOffset + nextCrossAxisOffset) / 2;
const additionalAfterOffset = getAdditionalSwapOffset(crossAfterSize);
crossAfterBound = swapOffset + additionalAfterOffset;
if (crossAfterSize) {
crossCurrentSize = crossAfterSize;
}
} while (
crossAfterBound < crossContainerSize.value &&
position[crossCoordinate] > crossAfterBound
);
// MAIN AXIS BOUNDS
const additionalOffset = getAdditionalSwapOffset(mainContainerSize.value);
// Before bound
let mainBeforeOffset = -Infinity;
let mainBeforeBound = Infinity;
do {
if (mainBeforeBound !== Infinity) {
mainIndex--;
}
mainBeforeOffset = mainIndex * (mainGroupSize.value + mainGap.value);
mainBeforeBound = mainBeforeOffset - additionalOffset;
} while (
mainBeforeBound > 0 &&
position[mainCoordinate] < mainBeforeBound
);
// After bound
let mainAfterOffset = Infinity;
let mainAfterBound = -Infinity;
do {
if (mainAfterBound !== -Infinity) {
mainIndex++;
}
mainAfterOffset =
mainIndex * (mainGroupSize.value + mainGap.value) +
mainGroupSize.value;
mainAfterBound = mainAfterOffset + additionalOffset;
} while (
mainAfterBound < mainContainerSize.value &&
position[mainCoordinate] > mainAfterBound
);
// DEBUG ONLY
if (debugBox) {
if (isVertical) {
debugBox.top.update(
{ x: mainBeforeBound, y: crossBeforeBound },
{
x: mainAfterBound,
y: Math.max(crossBeforeOffset, crossBeforeBound)
}
);
debugBox.bottom.update(
{
x: mainBeforeBound,
y: Math.min(crossAfterOffset, crossAfterBound)
},
{ x: mainAfterBound, y: crossAfterBound }
);
debugBox.left.update(
{ x: mainBeforeBound, y: crossBeforeBound },
{ x: mainBeforeOffset, y: crossAfterBound }
);
debugBox.right.update(
{ x: mainAfterOffset, y: crossBeforeBound },
{ x: mainAfterBound, y: crossAfterBound }
);
} else {
debugBox.top.update(
{ x: crossBeforeBound, y: mainBeforeBound },
{ x: crossAfterBound, y: mainBeforeOffset }
);
debugBox.bottom.update(
{ x: crossBeforeBound, y: mainAfterBound },
{ x: crossAfterBound, y: mainAfterOffset }
);
debugBox.left.update(
{ x: crossBeforeBound, y: mainBeforeBound },
{
x: Math.max(crossBeforeOffset, crossBeforeBound),
y: mainAfterBound
}
);
debugBox.right.update(
{
x: Math.min(crossAfterOffset, crossAfterBound),
y: mainAfterBound
},
{ x: crossAfterBound, y: mainBeforeBound }
);
}
}
const idxToKey = indexToKey.value;
const itemsCount = idxToKey.length;
const limitedCrossIndex = Math.max(
0,
Math.min(crossIndex, Math.floor((itemsCount - 1) / numGroups))
);
const newIndex = Math.min(
Math.max(0, limitedCrossIndex * numGroups + mainIndex),
itemsCount - 1
);
if (
newIndex === activeIndex ||
fixedItemKeys?.value[idxToKey[newIndex]!]
) {
return;
}
// return the new order of items
return reorder(idxToKey, activeIndex, newIndex, fixedItemKeys?.value);
};
};