react-native-sortables
Version:
Powerful Sortable Components for Flexible Content Reordering in React Native
398 lines (383 loc) • 14.8 kB
JavaScript
"use strict";
import { useCallback } from 'react';
import { clamp, interpolate, measure, useAnimatedReaction, withTiming } from 'react-native-reanimated';
import { useHaptics } from '../../integrations/haptics';
import { clearAnimatedTimeout, setAnimatedTimeout, useMutableValue, useStableCallbackValue } from '../../integrations/reanimated';
import { DragActivationState, LayerState } from '../../types';
import { getItemDimensions, getKeyToIndex, getOffsetDistance } from '../../utils';
import { createProvider } from '../utils';
import { useAutoScrollContext } from './AutoScrollProvider';
import { useCommonValuesContext } from './CommonValuesProvider';
import { useCustomHandleContext } from './CustomHandleProvider';
import { useLayerContext } from './LayerProvider';
import { useMeasurementsContext } from './MeasurementsProvider';
import { useMultiZoneContext } from './MultiZoneProvider';
import { usePortalContext } from './PortalProvider';
const {
DragProvider,
useDragContext
} = createProvider('Drag')(({
hapticsEnabled,
onActiveItemDropped,
onDragEnd: stableOnDragEnd,
onDragMove,
onDragStart,
onOrderChange,
overDrag
}) => {
const {
activationAnimationDuration,
activationState,
activeAnimationProgress,
activeItemDimensions,
activeItemDropped,
activeItemKey,
activeItemPosition,
containerHeight,
containerId,
containerRef,
containerWidth,
dragActivationDelay,
dragActivationFailOffset,
dropAnimationDuration,
enableActiveItemSnap,
inactiveAnimationProgress,
inactiveItemOpacity,
inactiveItemScale,
indexToKey,
itemHeights,
itemPositions,
itemWidths,
keyToIndex,
prevActiveItemKey,
snapOffsetX,
snapOffsetY,
sortEnabled,
touchPosition,
usesAbsoluteLayout
} = useCommonValuesContext();
const {
handleContainerMeasurement
} = useMeasurementsContext();
const {
updateLayer
} = useLayerContext() ?? {};
const {
scrollOffsetDiff,
updateStartScrollOffset
} = useAutoScrollContext() ?? {};
const {
activeHandleMeasurements,
activeHandleOffset,
updateActiveHandleMeasurements
} = useCustomHandleContext() ?? {};
const {
activeItemAbsolutePosition
} = usePortalContext() ?? {};
const {
activeContainerId,
activeItemDimensions: multiZoneActiveItemDimensions
} = useMultiZoneContext() ?? {};
const haptics = useHaptics(hapticsEnabled);
const hasHorizontalOverDrag = overDrag === 'horizontal' || overDrag === 'both';
const hasVerticalOverDrag = overDrag === 'vertical' || overDrag === 'both';
const touchStartTouch = useMutableValue(null);
const currentTouch = useMutableValue(null);
const dragStartItemTouchOffset = useMutableValue(null);
const dragStartTouchPosition = useMutableValue(null);
const dragStartIndex = useMutableValue(-1);
// used for activation and deactivation (drop)
const activationTimeoutId = useMutableValue(-1);
// Create stable callbacks to avoid re-rendering when the callback
// function is not memoized
const stableOnDragStart = useStableCallbackValue(onDragStart);
const stableOnDragMove = useStableCallbackValue(onDragMove);
const stableOnOrderChange = useStableCallbackValue(onOrderChange);
const stableOnActiveItemDropped = useStableCallbackValue(onActiveItemDropped);
// ACTIVE ITEM POSITION UPDATER
useAnimatedReaction(() => ({
activeDimensions: activeItemDimensions.value,
containerH: containerHeight.value,
containerW: containerWidth.value,
enableSnap: enableActiveItemSnap.value,
itemTouchOffset: dragStartItemTouchOffset.value,
key: activeItemKey.value,
offsetDiff: scrollOffsetDiff?.value,
offsetX: snapOffsetX.value,
offsetY: snapOffsetY.value,
progress: activeAnimationProgress.value,
snapDimensions: activeHandleMeasurements?.value ?? activeItemDimensions.value,
snapOffset: activeHandleOffset?.value,
startTouch: touchStartTouch.value,
startTouchPosition: dragStartTouchPosition.value,
touch: currentTouch.value
}), ({
activeDimensions,
containerH,
containerW,
enableSnap,
itemTouchOffset,
key,
offsetDiff,
offsetX,
offsetY,
progress,
snapDimensions,
snapOffset,
startTouch,
startTouchPosition,
touch
}) => {
if (key === null || containerH === null || containerW === null || !activeDimensions || !snapDimensions || !itemTouchOffset || !startTouchPosition || !touch || !startTouch) {
touchPosition.value = null;
return;
}
touchPosition.value = {
x: startTouchPosition.x + (touch.absoluteX - startTouch.absoluteX) + (offsetDiff?.x ?? 0),
y: startTouchPosition.y + (touch.absoluteY - startTouch.absoluteY) + (offsetDiff?.y ?? 0)
};
let tX = itemTouchOffset.x;
let tY = itemTouchOffset.y;
if (enableSnap) {
tX = (snapOffset?.x ?? 0) + getOffsetDistance(offsetX, snapDimensions.width);
tY = (snapOffset?.y ?? 0) + getOffsetDistance(offsetY, snapDimensions.height);
}
const translate = (from, to) => from === to ? from : interpolate(progress, [0, 1], [from, to]);
const snapX = translate(itemTouchOffset.x, tX);
const snapY = translate(itemTouchOffset.y, tY);
const unclampedActiveX = touchPosition.value.x - snapX;
const unclampedActiveY = touchPosition.value.y - snapY;
const activeX = hasHorizontalOverDrag ? unclampedActiveX : clamp(unclampedActiveX, 0, containerW - activeDimensions.width);
const activeY = hasVerticalOverDrag ? unclampedActiveY : clamp(unclampedActiveY, 0, containerH - activeDimensions.height);
activeItemPosition.value = {
x: activeX,
y: activeY
};
if (activeItemAbsolutePosition) {
activeItemAbsolutePosition.value = {
x: touch.absoluteX + activeX - unclampedActiveX - snapX,
y: touch.absoluteY + activeY - unclampedActiveY - snapY
};
}
});
/**
* DRAG HANDLERS
*/
const handleDragStart = useCallback((touch, key, position, dimensions, activationAnimationProgress) => {
'worklet';
const containerMeasurements = measure(containerRef);
if (!position || !dimensions || !containerMeasurements) {
return;
}
activeAnimationProgress.value = 0;
activeItemDropped.value = false;
prevActiveItemKey.value = activeItemKey.value;
activeItemKey.value = key;
activeItemPosition.value = position;
activeItemDimensions.value = dimensions;
dragStartIndex.value = keyToIndex.value[key] ?? -1;
activationState.value = DragActivationState.ACTIVE;
if (activeContainerId) {
activeContainerId.value = containerId;
}
if (multiZoneActiveItemDimensions) {
multiZoneActiveItemDimensions.value = dimensions;
}
updateLayer?.(LayerState.FOCUSED);
updateStartScrollOffset?.();
let touchedItemPosition = position;
// We need to update the custom handle measurements if the custom handle
// is used (touch position is relative to the handle in this case)
updateActiveHandleMeasurements?.(key);
if (activeHandleMeasurements?.value) {
const {
pageX,
pageY
} = activeHandleMeasurements.value;
touchedItemPosition = {
x: pageX - containerMeasurements.pageX,
y: pageY - containerMeasurements.pageY
};
}
// Touch position relative to the top-left corner of the sortable
// container
const touchX = touchedItemPosition.x + touch.x;
const touchY = touchedItemPosition.y + touch.y;
touchPosition.value = {
x: touchX,
y: touchY
};
dragStartTouchPosition.value = touchPosition.value;
dragStartItemTouchOffset.value = {
x: touchX - position.x,
y: touchY - position.y
};
const hasInactiveAnimation = inactiveItemOpacity.value !== 1 || inactiveItemScale.value !== 1;
const animate = () => withTiming(1, {
duration: activationAnimationDuration.value
});
inactiveAnimationProgress.value = hasInactiveAnimation ? animate() : 0;
activeAnimationProgress.value = animate();
activationAnimationProgress.value = animate();
haptics.medium();
stableOnDragStart({
fromIndex: dragStartIndex.value,
indexToKey: indexToKey.value,
key,
keyToIndex: keyToIndex.value
});
}, [activationAnimationDuration, activeAnimationProgress, activeContainerId, activeHandleMeasurements, activeItemDimensions, activeItemDropped, activationState, activeItemKey, activeItemPosition, containerId, dragStartIndex, dragStartItemTouchOffset, dragStartTouchPosition, haptics, inactiveAnimationProgress, inactiveItemOpacity, inactiveItemScale, indexToKey, keyToIndex, multiZoneActiveItemDimensions, containerRef, prevActiveItemKey, stableOnDragStart, touchPosition, updateLayer, updateActiveHandleMeasurements, updateStartScrollOffset]);
const handleTouchStart = useCallback((e, key, activationAnimationProgress, activate, fail) => {
'worklet';
const touch = e.allTouches[0];
if (!touch ||
// Ignore touch if another item is already being touched/activated
// if the current item is still animated to the drag end position
// or sorting is disabled at all
!sortEnabled.value || activationAnimationProgress.value > 0 || activeItemKey.value !== null) {
fail();
return;
}
if (!usesAbsoluteLayout.value) {
const measurements = measure(containerRef);
if (measurements) {
handleContainerMeasurement(measurements.width, measurements.height);
}
}
touchStartTouch.value = touch;
currentTouch.value = touch;
activationState.value = DragActivationState.TOUCHED;
clearAnimatedTimeout(activationTimeoutId.value);
// Start handling touch after a delay to prevent accidental activation
// e.g. while scrolling the ScrollView
activationTimeoutId.value = setAnimatedTimeout(() => {
if (!usesAbsoluteLayout.value) {
return;
}
const position = itemPositions.value[key];
const dimensions = getItemDimensions(key, itemWidths.value, itemHeights.value);
if (!position || !dimensions) {
return;
}
handleDragStart(touch, key, position, dimensions, activationAnimationProgress);
activate();
}, dragActivationDelay.value);
}, [activeItemKey, activationState, activationTimeoutId, containerRef, currentTouch, dragActivationDelay, handleContainerMeasurement, handleDragStart, itemHeights, itemWidths, itemPositions, sortEnabled, touchStartTouch, usesAbsoluteLayout]);
const handleTouchesMove = useCallback((e, fail) => {
'worklet';
const touch = e.allTouches[0];
currentTouch.value = touch ?? null;
if (!touch) {
fail();
return;
}
if (activeItemKey.value) {
stableOnDragMove({
fromIndex: dragStartIndex.value,
key: activeItemKey.value,
touchData: touch
});
}
if (activationState.value === DragActivationState.TOUCHED) {
if (!touchStartTouch.value) {
fail();
return;
}
const dX = touch.absoluteX - touchStartTouch.value.absoluteX;
const dY = touch.absoluteY - touchStartTouch.value.absoluteY;
// Cancel touch if the touch moved too far from the initial position
// before the item activation animation starts
const r = Math.sqrt(dX * dX + dY * dY);
if (
// activeItemKey is set after the drag activation delay passes
// and we don't want to cancel the touch anymore after this time
activeItemKey.value === null && r >= dragActivationFailOffset.value) {
fail();
return;
}
}
}, [activationState, activeItemKey, currentTouch, dragActivationFailOffset, touchStartTouch, dragStartIndex, stableOnDragMove]);
const handleDragEnd = useCallback((key, activationAnimationProgress) => {
'worklet';
if (activeItemKey.value && activeItemKey.value !== key) {
return;
}
clearAnimatedTimeout(activationTimeoutId.value);
const fromIndex = dragStartIndex.value;
const toIndex = keyToIndex.value[key];
touchStartTouch.value = null;
currentTouch.value = null;
activationState.value = DragActivationState.INACTIVE;
if (activeItemKey.value === null) {
return;
}
if (activeHandleMeasurements) {
activeHandleMeasurements.value = null;
}
if (activeContainerId) {
activeContainerId.value = null;
}
if (multiZoneActiveItemDimensions) {
multiZoneActiveItemDimensions.value = null;
}
prevActiveItemKey.value = activeItemKey.value;
dragStartItemTouchOffset.value = null;
dragStartTouchPosition.value = null;
activeItemPosition.value = null;
activeItemDimensions.value = null;
touchPosition.value = null;
activeItemKey.value = null;
dragStartIndex.value = -1;
const animate = callback => withTiming(0, {
duration: dropAnimationDuration.value
}, callback);
activationAnimationProgress.value = animate();
inactiveAnimationProgress.value = animate();
activeAnimationProgress.value = animate();
updateStartScrollOffset?.(null);
updateLayer?.(LayerState.INTERMEDIATE);
haptics.medium();
stableOnDragEnd({
fromIndex,
indexToKey: indexToKey.value,
key,
keyToIndex: keyToIndex.value,
toIndex
});
activationTimeoutId.value = setAnimatedTimeout(() => {
prevActiveItemKey.value = null;
activeItemDropped.value = true;
updateLayer?.(LayerState.IDLE);
stableOnActiveItemDropped({
fromIndex,
indexToKey: indexToKey.value,
key,
keyToIndex: keyToIndex.value,
toIndex
});
}, dropAnimationDuration.value);
}, [activeContainerId, activeItemKey, activeItemDimensions, activeItemDropped, activeItemPosition, prevActiveItemKey, activationTimeoutId, activeAnimationProgress, activationState, currentTouch, dropAnimationDuration, dragStartIndex, dragStartItemTouchOffset, dragStartTouchPosition, haptics, inactiveAnimationProgress, indexToKey, keyToIndex, multiZoneActiveItemDimensions, stableOnActiveItemDropped, stableOnDragEnd, touchPosition, touchStartTouch, updateLayer, updateStartScrollOffset, activeHandleMeasurements]);
const handleOrderChange = useCallback((key, fromIndex, toIndex, newOrder) => {
'worklet';
indexToKey.value = newOrder;
haptics.light();
stableOnOrderChange({
fromIndex,
indexToKey: indexToKey.value,
key,
keyToIndex: getKeyToIndex(newOrder),
toIndex
});
}, [indexToKey, stableOnOrderChange, haptics]);
return {
value: {
dropAnimationDuration,
handleDragEnd,
handleOrderChange,
handleTouchesMove,
handleTouchStart
}
};
});
export { DragProvider, useDragContext };
//# sourceMappingURL=DragProvider.js.map