react-native-sortables
Version:
Powerful Sortable Components for Flexible Content Reordering in React Native
224 lines (220 loc) • 9.17 kB
JavaScript
"use strict";
import { useCallback, useMemo, useRef } from 'react';
import { clamp, makeMutable, measure, useAnimatedReaction } from 'react-native-reanimated';
import { clearAnimatedTimeout, setAnimatedTimeout, useMutableValue } from '../../integrations/reanimated';
import { calculateSnapOffset, resolveDimension } from '../../utils';
import { useAutoScrollContext, useCommonValuesContext, useCustomHandleContext } from '../shared';
import { createProvider } from '../utils';
import { calculateItemCrossOffset } from './GridLayoutProvider/utils';
var AutoOffsetAdjustmentState = /*#__PURE__*/function (AutoOffsetAdjustmentState) {
AutoOffsetAdjustmentState[AutoOffsetAdjustmentState["ENABLED"] = 0] = "ENABLED";
// Auto adjustment is enabled but the additional cross offset is not applied yet
AutoOffsetAdjustmentState[AutoOffsetAdjustmentState["DISABLED"] = 1] = "DISABLED";
// Auto adjustment is disabled
AutoOffsetAdjustmentState[AutoOffsetAdjustmentState["APPLIED"] = 2] = "APPLIED";
// Additional cross offset is applied
AutoOffsetAdjustmentState[AutoOffsetAdjustmentState["RESET"] = 3] = "RESET"; // Additional cross offset is being reset (intermediate state after APPLIED)
return AutoOffsetAdjustmentState;
}(AutoOffsetAdjustmentState || {});
const {
AutoOffsetAdjustmentProvider,
useAutoOffsetAdjustmentContext
} = createProvider('AutoOffsetAdjustment', {
guarded: false
})(({
autoAdjustOffsetResetTimeout,
autoAdjustOffsetScrollPadding
}) => {
const {
activeItemDimensions,
activeItemDropped,
activeItemKey,
activeItemPosition,
containerRef,
enableActiveItemSnap,
itemPositions,
keyToIndex,
prevActiveItemKey,
snapOffsetX,
snapOffsetY,
sortEnabled,
touchPosition
} = useCommonValuesContext();
const {
activeHandleMeasurements,
activeHandleOffset
} = useCustomHandleContext() ?? {};
const {
scrollableRef,
scrollBy
} = useAutoScrollContext() ?? {};
const additionalCrossOffset = useMutableValue(null);
const scrollPadding = useMemo(() => autoAdjustOffsetScrollPadding === null ? null : Array.isArray(autoAdjustOffsetScrollPadding) ? autoAdjustOffsetScrollPadding : [autoAdjustOffsetScrollPadding, autoAdjustOffsetScrollPadding], [autoAdjustOffsetScrollPadding]);
const contextRef = useRef(null);
contextRef.current ??= makeMutable({
keepInViewData: null,
prevSortEnabled: sortEnabled.value,
resetTimeoutId: 0,
state: AutoOffsetAdjustmentState.DISABLED
});
const context = contextRef.current;
const disableAutoOffsetAdjustment = useCallback(() => {
'worklet';
const ctx = context.value;
if (ctx.prevSortEnabled === null) {
return;
}
clearAnimatedTimeout(ctx.resetTimeoutId);
ctx.state = AutoOffsetAdjustmentState.DISABLED;
sortEnabled.value = ctx.prevSortEnabled;
ctx.prevSortEnabled = null;
additionalCrossOffset.value = null;
}, [context, sortEnabled, additionalCrossOffset]);
useAnimatedReaction(() => activeItemDropped.value, dropped => {
clearAnimatedTimeout(context.value.resetTimeoutId);
if (dropped && context.value.state !== AutoOffsetAdjustmentState.DISABLED) {
context.value.resetTimeoutId = setAnimatedTimeout(disableAutoOffsetAdjustment, autoAdjustOffsetResetTimeout);
} else {
context.value.state = AutoOffsetAdjustmentState.ENABLED;
}
});
const adjustScrollToKeepItemInView = useCallback(padding => {
'worklet';
const keepInViewData = context.value.keepInViewData;
if (!scrollBy || !scrollableRef || !keepInViewData) {
return;
}
const containerMeasurements = measure(containerRef);
const scrollableMeasurements = measure(scrollableRef);
if (!containerMeasurements || !scrollableMeasurements) {
return;
}
const {
isVertical,
itemCrossOffset,
itemCrossSize
} = keepInViewData;
const {
height: scrollableHeight,
pageX: scrollableX,
pageY: scrollableY,
width: scrollableWidth
} = scrollableMeasurements;
const {
pageX: containerX,
pageY: containerY
} = containerMeasurements;
const scrollableCrossSize = isVertical ? scrollableHeight : scrollableWidth;
const relativeScrollOffset = isVertical ? scrollableY - containerY : scrollableX - containerX;
const relativeItemOffset = itemCrossOffset - relativeScrollOffset;
const minOffset = padding[0];
const maxOffset = scrollableCrossSize - itemCrossSize - padding[1];
let clampedOffset = 0;
if (minOffset > maxOffset) {
// Center the item if padding is too large
clampedOffset = (minOffset + maxOffset) / 2;
} else {
clampedOffset = clamp(relativeItemOffset, minOffset, maxOffset);
}
scrollBy(relativeItemOffset - clampedOffset, true);
context.value.keepInViewData = null;
}, [containerRef, scrollableRef, context, scrollBy]);
const adaptLayoutProps = useCallback((props, prevProps) => {
'worklet';
const itemKey = activeItemKey.value ?? prevActiveItemKey.value;
const ctx = context.value;
if (ctx.state === AutoOffsetAdjustmentState.DISABLED) {
return props;
}
if (ctx.state === AutoOffsetAdjustmentState.RESET || itemKey === null) {
// This auto adjustment must be delayed one frame after the scrollBy call
if (scrollPadding) {
setAnimatedTimeout(() => adjustScrollToKeepItemInView(scrollPadding));
}
disableAutoOffsetAdjustment();
return props;
}
const {
gaps,
indexToKey,
isVertical,
itemHeights,
itemWidths,
numGroups
} = props;
const crossItemSizes = isVertical ? itemHeights : itemWidths;
const prevCrossIteSizes = isVertical ? prevProps?.itemHeights : prevProps?.itemWidths;
const crossOffsetsChanged = crossItemSizes !== prevCrossIteSizes || gaps.cross !== prevProps?.gaps.cross;
if (!crossOffsetsChanged) {
return {
...props,
startCrossOffset: additionalCrossOffset.value
};
}
const crossCoordinate = isVertical ? 'y' : 'x';
const autoOffsetAdjustmentCommonProps = {
crossGap: gaps.cross,
crossItemSizes,
indexToKey: indexToKey,
numGroups
};
if (activeItemKey.value === null && additionalCrossOffset.value !== null && prevCrossIteSizes !== null) {
if (!scrollableRef) {
disableAutoOffsetAdjustment();
return props;
}
const prevActiveKey = prevActiveItemKey.value;
const oldCrossOffset = itemPositions.value[prevActiveKey]?.[crossCoordinate] ?? 0;
const newCrossOffset = calculateItemCrossOffset({
...autoOffsetAdjustmentCommonProps,
itemKey: prevActiveKey
});
const scrollDistance = newCrossOffset - oldCrossOffset;
const startCrossOffset = additionalCrossOffset.value + scrollDistance;
const itemCrossSize = resolveDimension(crossItemSizes, prevActiveKey);
ctx.keepInViewData = itemCrossSize === null ? null : {
isVertical,
itemCrossOffset: newCrossOffset,
itemCrossSize
};
setAnimatedTimeout(() => scrollBy?.(scrollDistance, false));
ctx.state = AutoOffsetAdjustmentState.RESET;
return {
...props,
gaps: prevProps?.gaps ?? gaps,
[isVertical ? 'itemHeights' : 'itemWidths']: prevCrossIteSizes,
requestNextLayout: true,
shouldAnimateLayout: false,
startCrossOffset
};
}
let snapBasedOffset = 0;
if (enableActiveItemSnap.value && touchPosition.value && activeItemPosition.value && activeItemDimensions.value) {
const offset = calculateSnapOffset(snapOffsetX.value, snapOffsetY.value, activeHandleMeasurements?.value ?? activeItemDimensions.value, activeHandleOffset?.value);
snapBasedOffset = isVertical ? touchPosition.value.y - activeItemPosition.value.y - offset.y : touchPosition.value.x - activeItemPosition.value.x - offset.x;
}
const activeItemCrossOffset = calculateItemCrossOffset({
...autoOffsetAdjustmentCommonProps,
itemKey
});
const activeItemIndex = keyToIndex.value[itemKey];
const itemAtActiveIndexKey = indexToKey[activeItemIndex];
const itemAtActiveIndexOffset = itemPositions.value[itemAtActiveIndexKey]?.[crossCoordinate] ?? 0;
const startCrossOffset = Math.max(0, itemAtActiveIndexOffset - activeItemCrossOffset + snapBasedOffset);
additionalCrossOffset.value = startCrossOffset;
ctx.prevSortEnabled ??= sortEnabled.value;
sortEnabled.value = false;
return {
...props,
startCrossOffset
};
}, [activeItemKey, activeHandleMeasurements, activeHandleOffset, activeItemDimensions, activeItemPosition, additionalCrossOffset, adjustScrollToKeepItemInView, disableAutoOffsetAdjustment, enableActiveItemSnap, itemPositions, keyToIndex, prevActiveItemKey, snapOffsetX, snapOffsetY, touchPosition, scrollPadding, scrollBy, context, sortEnabled, scrollableRef]);
return {
value: {
adaptLayoutProps,
additionalCrossOffset
}
};
});
export { AutoOffsetAdjustmentProvider, useAutoOffsetAdjustmentContext };
//# sourceMappingURL=AutoOffsetAdjustmentProvider.js.map