react-native-sortables
Version:
Powerful Sortable Components for Flexible Content Reordering in React Native
271 lines (232 loc) • 7.11 kB
text/typescript
import type { ItemSizes } from '../../../../../types';
import { reorderInsert, resolveDimension } from '../../../../../utils';
export type ItemGroupSwapProps = {
activeItemKey: string;
activeItemIndex: number; // can be in different group than the currentGroupIndex
currentGroupIndex: number;
groupSizeLimit: number;
indexToKey: Array<string>;
keyToIndex: Record<string, number>;
mainItemSizes: ItemSizes;
itemGroups: Array<Array<string>>;
mainGap: number;
fixedKeys: Record<string, boolean> | undefined;
};
export type ItemGroupSwapResult = {
indexToKey: Array<string>;
keyToIndex: Record<string, number>;
itemIndex: number;
itemIndexInGroup: number;
groupIndex: number;
};
const getGroupItemIndex = (
inGroupIndex: number,
group: Array<string>,
keyToIndex: Record<string, number>
) => {
'worklet';
const key = group[inGroupIndex];
if (key === undefined) return null;
return keyToIndex[key] ?? null;
};
export const getTotalGroupSize = (
group: Array<string>,
mainItemSizes: ItemSizes,
gap: number
) => {
'worklet';
const sizesSum = group.reduce(
(total, key) => total + (resolveDimension(mainItemSizes, key) ?? 0),
0
);
return sizesSum + gap * (group.length - 1);
};
type SwappedGroupIndexesResult = null | Omit<
ItemGroupSwapResult,
'indexToKey' | 'keyToIndex'
>;
const getIndexesWhenSwappedToGroupBefore = ({
activeItemKey,
currentGroupIndex,
fixedKeys,
groupSizeLimit,
indexToKey,
itemGroups,
keyToIndex,
mainGap,
mainItemSizes
}: ItemGroupSwapProps): SwappedGroupIndexesResult => {
'worklet';
if (groupSizeLimit === Infinity) {
return null;
}
const activeMainSize = resolveDimension(mainItemSizes, activeItemKey) ?? 0;
for (let groupIdx = currentGroupIndex; groupIdx > 0; groupIdx--) {
const groupBefore = itemGroups[groupIdx - 1]!;
const firstInGroupBeforeIndex = getGroupItemIndex(
0,
groupBefore,
keyToIndex
);
const lastInGroupBeforeIndex = getGroupItemIndex(
groupBefore.length - 1,
groupBefore,
keyToIndex
);
if (firstInGroupBeforeIndex === null || lastInGroupBeforeIndex === null) {
return null;
}
let itemIndex = firstInGroupBeforeIndex;
let totalGroupSize = 0;
const groupBeforeBefore = itemGroups[groupIdx - 2];
if (groupBeforeBefore) {
// If there is a group before the group before the active group,
// we have to check whether the active item won't wrap to this group
const totalGroupBeforeBeforeSize = getTotalGroupSize(
groupBeforeBefore,
mainItemSizes,
mainGap
);
const canBeFirstInGroupBefore =
totalGroupBeforeBeforeSize + activeMainSize > groupSizeLimit;
if (!canBeFirstInGroupBefore) {
const firstItemKey = groupBefore[0];
if (firstItemKey === undefined) {
return null;
}
totalGroupSize +=
(resolveDimension(mainItemSizes, firstItemKey) ?? 0) + mainGap;
itemIndex++;
}
}
for (; itemIndex <= lastInGroupBeforeIndex; itemIndex++) {
const itemKey = indexToKey[itemIndex];
if (itemKey === undefined) {
return null;
}
if (totalGroupSize + activeMainSize > groupSizeLimit) {
break;
}
if (!fixedKeys?.[itemKey]) {
return {
groupIndex: groupIdx - 1,
itemIndex,
itemIndexInGroup: itemIndex - firstInGroupBeforeIndex
};
}
const itemMainSize = resolveDimension(mainItemSizes, itemKey) ?? 0;
totalGroupSize += itemMainSize + mainGap;
}
}
return null;
};
const getIndexesWhenSwappedToGroupAfter = ({
activeItemKey,
currentGroupIndex,
fixedKeys,
groupSizeLimit,
indexToKey,
itemGroups,
keyToIndex,
mainGap,
mainItemSizes
}: ItemGroupSwapProps): SwappedGroupIndexesResult => {
'worklet';
const activeGroup = itemGroups[currentGroupIndex];
if (groupSizeLimit === Infinity || activeGroup === undefined) {
return null;
}
const firstInActiveGroupIndex = getGroupItemIndex(0, activeGroup, keyToIndex);
if (firstInActiveGroupIndex === null) {
return null;
}
const getItemMainSize = (key: string) =>
resolveDimension(mainItemSizes, key) ?? 0;
// We need to remove the active item from the its group, fit all items
// in the remaining space between the active item's group and the target group,
// and then insert the active item in the target group
const activeItemIndex = keyToIndex[activeItemKey]!;
let totalGroupSize = 0;
for (let i = firstInActiveGroupIndex; i < activeItemIndex; i++) {
const key = indexToKey[i]!;
totalGroupSize += getItemMainSize(key) + mainGap;
}
let firstInGroupIndex = firstInActiveGroupIndex;
let groupIndex = currentGroupIndex;
let emptyIndex = activeItemIndex;
const doesNotFitInGroup = (key: string) =>
totalGroupSize + getItemMainSize(key) > groupSizeLimit;
const includeItem = (key: string) => {
const itemMainSize = getItemMainSize(key);
if (doesNotFitInGroup(key)) {
groupIndex++;
totalGroupSize = 0;
firstInGroupIndex = keyToIndex[key]!;
}
totalGroupSize += itemMainSize + mainGap;
};
for (let i = activeItemIndex + 1; i < indexToKey.length; i++) {
const key = indexToKey[i]!;
if (fixedKeys?.[key]) {
continue;
}
// Fill the empty slot with the current item and add all fixed
// position items that were before this item
includeItem(key);
for (let j = emptyIndex + 1; j < i; j++) {
includeItem(indexToKey[j]!);
}
emptyIndex = i;
if (doesNotFitInGroup(activeItemKey)) {
const nextItemKey = indexToKey[i + 1];
if (nextItemKey === undefined || doesNotFitInGroup(nextItemKey)) {
return {
groupIndex: groupIndex + 1,
itemIndex: i,
itemIndexInGroup: 0
};
}
} else if (groupIndex > currentGroupIndex) {
return {
groupIndex,
itemIndex: i,
itemIndexInGroup: i - firstInGroupIndex
};
}
}
return null;
};
export const getSwappedToGroupBeforeIndices = (
props: ItemGroupSwapProps
): ItemGroupSwapResult | null => {
'worklet';
const indexes = getIndexesWhenSwappedToGroupBefore(props);
if (indexes === null) return null;
const indexToKey = reorderInsert(
props.indexToKey,
props.activeItemIndex,
indexes.itemIndex,
props.fixedKeys
);
const keyToIndex = Object.fromEntries(
indexToKey.map((key, index) => [key, index])
);
return { ...indexes, indexToKey, keyToIndex };
};
export const getSwappedToGroupAfterIndices = (
props: ItemGroupSwapProps
): ItemGroupSwapResult | null => {
'worklet';
const indexes = getIndexesWhenSwappedToGroupAfter(props);
if (indexes === null) return null;
const indexToKey = reorderInsert(
props.indexToKey,
props.activeItemIndex,
indexes.itemIndex,
props.fixedKeys
);
const keyToIndex = Object.fromEntries(
indexToKey.map((key, index) => [key, index])
);
return { ...indexes, indexToKey, keyToIndex };
};