UNPKG

react-native-draggable-flatlist

Version:
349 lines (314 loc) 12.4 kB
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { FlatList, Gesture, GestureDetector } from "react-native-gesture-handler"; import Animated, { runOnJS, useAnimatedReaction, useAnimatedScrollHandler, useSharedValue, withSpring } from "react-native-reanimated"; import CellRendererComponent from "./CellRendererComponent"; import { DEFAULT_PROPS } from "../constants"; import PlaceholderItem from "./PlaceholderItem"; import RowItem from "./RowItem"; import PropsProvider from "../context/propsContext"; import AnimatedValueProvider, { useAnimatedValues } from "../context/animatedValueContext"; import RefProvider, { useRefs } from "../context/refContext"; import DraggableFlatListProvider from "../context/draggableFlatListContext"; import { useAutoScroll } from "../hooks/useAutoScroll"; import { useStableCallback } from "../hooks/useStableCallback"; import ScrollOffsetListener from "./ScrollOffsetListener"; import { typedMemo } from "../utils"; const AnimatedFlatList = Animated.createAnimatedComponent(FlatList); function DraggableFlatListInner(props) { const { cellDataRef, containerRef, flatlistRef, keyToIndexRef, propsRef, animationConfigRef } = useRefs(); const { activeCellOffset, activeCellSize, activeIndexAnim, containerSize, scrollOffset, scrollViewSize, spacerIndexAnim, horizontalAnim, placeholderOffset, touchTranslate, autoScrollDistance, panGestureState, isTouchActiveNative, viewableIndexMin, viewableIndexMax, disabled } = useAnimatedValues(); const reset = useStableCallback(() => { activeIndexAnim.value = -1; spacerIndexAnim.value = -1; touchTranslate.value = 0; activeCellSize.value = -1; activeCellOffset.value = -1; setActiveKey(null); }); const { dragHitSlop = DEFAULT_PROPS.dragHitSlop, scrollEnabled = DEFAULT_PROPS.scrollEnabled, activationDistance: activationDistanceProp = DEFAULT_PROPS.activationDistance } = props; let [activeKey, setActiveKey] = useState(null); const [layoutAnimationDisabled, setLayoutAnimationDisabled] = useState(!propsRef.current.enableLayoutAnimationExperimental); const keyExtractor = useStableCallback((item, index) => { if (!props.keyExtractor) { throw new Error("You must provide a keyExtractor to DraggableFlatList"); } return props.keyExtractor(item, index); }); const dataRef = useRef(props.data); const dataHasChanged = dataRef.current.map(keyExtractor).join("") !== props.data.map(keyExtractor).join(""); dataRef.current = props.data; if (dataHasChanged) { // When data changes make sure `activeKey` is nulled out in the same render pass activeKey = null; } useEffect(() => { if (!propsRef.current.enableLayoutAnimationExperimental) return; if (activeKey) { setLayoutAnimationDisabled(true); } else { // setTimeout result of trial-and-error to determine how long to wait before // re-enabling layout animations so that a drag reorder does not trigger it. setTimeout(() => { setLayoutAnimationDisabled(false); }, 100); } }, [activeKey]); useLayoutEffect(() => { props.data.forEach((d, i) => { const key = keyExtractor(d, i); keyToIndexRef.current.set(key, i); }); }, [props.data, keyExtractor, keyToIndexRef]); const drag = useStableCallback(activeKey => { if (disabled.value) return; const index = keyToIndexRef.current.get(activeKey); const cellData = cellDataRef.current.get(activeKey); if (cellData) { activeCellOffset.value = cellData.measurements.offset; activeCellSize.value = cellData.measurements.size; } const { onDragBegin } = propsRef.current; if (index !== undefined) { spacerIndexAnim.value = index; activeIndexAnim.value = index; setActiveKey(activeKey); onDragBegin === null || onDragBegin === void 0 ? void 0 : onDragBegin(index); } }); const onContainerLayout = _ref => { var _props$onContainerLay; let { nativeEvent: { layout } } = _ref; const { width, height } = layout; containerSize.value = props.horizontal ? width : height; (_props$onContainerLay = props.onContainerLayout) === null || _props$onContainerLay === void 0 ? void 0 : _props$onContainerLay.call(props, { layout, containerRef }); }; const onListContentSizeChange = (w, h) => { var _props$onContentSizeC; scrollViewSize.value = props.horizontal ? w : h; (_props$onContentSizeC = props.onContentSizeChange) === null || _props$onContentSizeC === void 0 ? void 0 : _props$onContentSizeC.call(props, w, h); }; const onContainerTouchStart = () => { if (!disabled.value) { isTouchActiveNative.value = true; } return false; }; const onContainerTouchEnd = () => { isTouchActiveNative.value = false; }; const extraData = useMemo(() => ({ activeKey, extraData: props.extraData }), [activeKey, props.extraData]); const renderItem = useCallback(_ref2 => { let { item, index } = _ref2; const key = keyExtractor(item, index); if (index !== keyToIndexRef.current.get(key)) { keyToIndexRef.current.set(key, index); } return /*#__PURE__*/React.createElement(RowItem, { item: item, itemKey: key, renderItem: props.renderItem, drag: drag, extraData: props.extraData }); }, [props.renderItem, props.extraData, drag, keyExtractor]); const onRelease = useStableCallback(index => { var _props$onRelease; (_props$onRelease = props.onRelease) === null || _props$onRelease === void 0 ? void 0 : _props$onRelease.call(props, index); }); const onDragEnd = useStableCallback(_ref3 => { let { from, to } = _ref3; const { onDragEnd, data } = props; const newData = [...data]; if (from !== to) { newData.splice(from, 1); newData.splice(to, 0, data[from]); } onDragEnd === null || onDragEnd === void 0 ? void 0 : onDragEnd({ from, to, data: newData }); reset(); }); const onPlaceholderIndexChange = useStableCallback(index => { var _props$onPlaceholderI; (_props$onPlaceholderI = props.onPlaceholderIndexChange) === null || _props$onPlaceholderI === void 0 ? void 0 : _props$onPlaceholderI.call(props, index); }); // Handle case where user ends drag without moving their finger. useAnimatedReaction(() => { return isTouchActiveNative.value; }, (cur, prev) => { if (cur !== prev && !cur) { const hasMoved = !!touchTranslate.value; if (!hasMoved && activeIndexAnim.value >= 0 && !disabled.value) { runOnJS(onRelease)(activeIndexAnim.value); runOnJS(onDragEnd)({ from: activeIndexAnim.value, to: spacerIndexAnim.value }); } } }, [isTouchActiveNative, onDragEnd, onRelease]); useAnimatedReaction(() => { return spacerIndexAnim.value; }, (cur, prev) => { if (prev !== null && cur !== prev && cur >= 0 && prev >= 0) { runOnJS(onPlaceholderIndexChange)(cur); } }, [spacerIndexAnim]); const gestureDisabled = useSharedValue(false); const panGesture = Gesture.Pan().onBegin(evt => { gestureDisabled.value = disabled.value; if (gestureDisabled.value) return; panGestureState.value = evt.state; }).onUpdate(evt => { if (gestureDisabled.value) return; panGestureState.value = evt.state; const translation = horizontalAnim.value ? evt.translationX : evt.translationY; touchTranslate.value = translation; }).onEnd(evt => { if (gestureDisabled.value) return; // Set touch val to current translate val isTouchActiveNative.value = false; const translation = horizontalAnim.value ? evt.translationX : evt.translationY; touchTranslate.value = translation + autoScrollDistance.value; panGestureState.value = evt.state; // Only call onDragEnd if actually dragging a cell if (activeIndexAnim.value === -1 || disabled.value) return; disabled.value = true; runOnJS(onRelease)(activeIndexAnim.value); const springTo = placeholderOffset.value - activeCellOffset.value; touchTranslate.value = withSpring(springTo, animationConfigRef.value, () => { runOnJS(onDragEnd)({ from: activeIndexAnim.value, to: spacerIndexAnim.value }); disabled.value = false; }); }).onTouchesDown(() => { runOnJS(onContainerTouchStart)(); }).onTouchesUp(() => { // Turning this into a worklet causes timing issues. We want it to run // just after the finger lifts. runOnJS(onContainerTouchEnd)(); }); if (dragHitSlop) panGesture.hitSlop(dragHitSlop); if (activationDistanceProp) { const activeOffset = [-activationDistanceProp, activationDistanceProp]; if (props.horizontal) { panGesture.activeOffsetX(activeOffset); } else { panGesture.activeOffsetY(activeOffset); } } const onScroll = useStableCallback(scrollOffset => { var _props$onScrollOffset; (_props$onScrollOffset = props.onScrollOffsetChange) === null || _props$onScrollOffset === void 0 ? void 0 : _props$onScrollOffset.call(props, scrollOffset); }); const scrollHandler = useAnimatedScrollHandler({ onScroll: evt => { scrollOffset.value = horizontalAnim.value ? evt.contentOffset.x : evt.contentOffset.y; runOnJS(onScroll)(scrollOffset.value); } }, [horizontalAnim]); useAutoScroll(); const onViewableItemsChanged = useStableCallback(info => { var _props$onViewableItem; const viewableIndices = info.viewableItems.filter(item => item.isViewable).map(item => item.index).filter(index => typeof index === "number"); const min = Math.min(...viewableIndices); const max = Math.max(...viewableIndices); viewableIndexMin.value = min; viewableIndexMax.value = max; (_props$onViewableItem = props.onViewableItemsChanged) === null || _props$onViewableItem === void 0 ? void 0 : _props$onViewableItem.call(props, info); }); return /*#__PURE__*/React.createElement(DraggableFlatListProvider, { activeKey: activeKey, keyExtractor: keyExtractor, horizontal: !!props.horizontal, layoutAnimationDisabled: layoutAnimationDisabled }, /*#__PURE__*/React.createElement(GestureDetector, { gesture: panGesture }, /*#__PURE__*/React.createElement(Animated.View, { style: props.containerStyle, ref: containerRef, onLayout: onContainerLayout }, props.renderPlaceholder && /*#__PURE__*/React.createElement(PlaceholderItem, { renderPlaceholder: props.renderPlaceholder }), /*#__PURE__*/React.createElement(AnimatedFlatList, _extends({}, props, { data: props.data, onViewableItemsChanged: onViewableItemsChanged, CellRendererComponent: CellRendererComponent, ref: flatlistRef, onContentSizeChange: onListContentSizeChange, scrollEnabled: !activeKey && scrollEnabled, renderItem: renderItem, extraData: extraData, keyExtractor: keyExtractor, onScroll: scrollHandler, scrollEventThrottle: 16, simultaneousHandlers: props.simultaneousHandlers, removeClippedSubviews: false })), !!props.onScrollOffsetChange && /*#__PURE__*/React.createElement(ScrollOffsetListener, { onScrollOffsetChange: props.onScrollOffsetChange, scrollOffset: scrollOffset })))); } function DraggableFlatList(props, ref) { return /*#__PURE__*/React.createElement(PropsProvider, props, /*#__PURE__*/React.createElement(AnimatedValueProvider, null, /*#__PURE__*/React.createElement(RefProvider, { flatListRef: ref }, /*#__PURE__*/React.createElement(MemoizedInner, props)))); } const MemoizedInner = typedMemo(DraggableFlatListInner); // Generic forwarded ref type assertion taken from: // https://fettblog.eu/typescript-react-generic-forward-refs/#option-1%3A-type-assertion export default /*#__PURE__*/React.forwardRef(DraggableFlatList); //# sourceMappingURL=DraggableFlatList.js.map