UNPKG

react-native-sortables

Version:

Powerful Sortable Components for Flexible Content Reordering in React Native

398 lines (383 loc) 14.8 kB
"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