UNPKG

@kietpt2003/react-native-core-ui

Version:
283 lines (282 loc) 14.1 kB
var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import React from "react"; import { InteractionManager, } from "react-native"; 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 PlaceholderItem from "./PlaceholderItem"; import RowItem from "./RowItem"; import ScrollOffsetListener from "./ScrollOffsetListener"; import { typedMemo } from "../utils/DraggableFlatList/typedMemo"; import { useStableCallback } from "../hooks/DraggableFlatList/useStableCallback"; import { useAutoScroll } from "../hooks/DraggableFlatList/useAutoScroll"; import DraggableFlatListProvider from "../context/DraggableFlatList/draggableFlatListContext"; import { DEFAULT_PROPS } from "../constants/DraggableFlatList/constants"; import PropsProvider from "../context/DraggableFlatList/propsContext"; import AnimatedValueProvider, { useAnimatedValues, } from "../context/DraggableFlatList/animatedValueContext"; import RefProvider, { useRefs } from "../context/DraggableFlatList/refContext"; import { WEB } from "../utils"; // Web version import let DraggableFlatListWeb = null; if (WEB) { try { DraggableFlatListWeb = require("./DraggableFlatList.web").default; } catch (e) { // Web version not available or failed to load console.warn("Web version of DraggableFlatList not available:", e); } } 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] = React.useState(null); const [layoutAnimationDisabled, setLayoutAnimationDisabled] = React.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 = React.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; InteractionManager.runAfterInteractions(() => { reset(); }); } React.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]); React.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 = ({ nativeEvent: { layout }, }) => { var _a; const { width, height } = layout; containerSize.value = props.horizontal ? width : height; (_a = props.onContainerLayout) === null || _a === void 0 ? void 0 : _a.call(props, { layout, containerRef }); }; const onListContentSizeChange = (w, h) => { var _a; scrollViewSize.value = props.horizontal ? w : h; (_a = props.onContentSizeChange) === null || _a === void 0 ? void 0 : _a.call(props, w, h); }; const onContainerTouchStart = () => { if (!disabled.value) { isTouchActiveNative.value = true; } return false; }; const onContainerTouchEnd = () => { isTouchActiveNative.value = false; }; const extraData = React.useMemo(() => ({ activeKey, extraData: props.extraData, }), [activeKey, props.extraData]); const renderItem = React.useCallback(({ item, index }) => { const key = keyExtractor(item, index); if (index !== keyToIndexRef.current.get(key)) { keyToIndexRef.current.set(key, index); } return (_jsx(RowItem, { item: item, itemKey: key, renderItem: props.renderItem, drag: drag, extraData: props.extraData })); }, [props.renderItem, props.extraData, drag, keyExtractor]); const onRelease = useStableCallback((index) => { var _a; (_a = props.onRelease) === null || _a === void 0 ? void 0 : _a.call(props, index); }); const onDragEnd = useStableCallback(({ from, to }) => { 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 }); setActiveKey(null); }); const onPlaceholderIndexChange = useStableCallback((index) => { var _a; (_a = props.onPlaceholderIndexChange) === null || _a === void 0 ? void 0 : _a.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 _a; (_a = props.onScrollOffsetChange) === null || _a === void 0 ? void 0 : _a.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 _a; 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; (_a = props.onViewableItemsChanged) === null || _a === void 0 ? void 0 : _a.call(props, info); }); return (_jsx(DraggableFlatListProvider, { activeKey: activeKey, keyExtractor: keyExtractor, horizontal: !!props.horizontal, layoutAnimationDisabled: layoutAnimationDisabled, children: _jsx(GestureDetector, { gesture: panGesture, children: _jsxs(Animated.View, { style: props.containerStyle, ref: containerRef, onLayout: onContainerLayout, children: [props.renderPlaceholder && (_jsx(PlaceholderItem, { renderPlaceholder: props.renderPlaceholder })), _jsx(AnimatedFlatList, Object.assign({}, 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 && (_jsx(ScrollOffsetListener, { onScrollOffsetChange: props.onScrollOffsetChange, scrollOffset: scrollOffset }))] }) }) })); } function DraggableFlatList(props, ref) { // Use web version if available and on web platform if (WEB && DraggableFlatListWeb) { const { data, renderItem, keyExtractor, onDragEnd, onDragBegin, onRelease, onPlaceholderIndexChange, activationDistance, containerStyle, horizontal } = props, otherProps = __rest(props, ["data", "renderItem", "keyExtractor", "onDragEnd", "onDragBegin", "onRelease", "onPlaceholderIndexChange", "activationDistance", "containerStyle", "horizontal"]); return (_jsx(DraggableFlatListWeb, Object.assign({ data: data, renderItem: renderItem, keyExtractor: keyExtractor, onDragEnd: onDragEnd, onDragBegin: onDragBegin, onRelease: onRelease, onPlaceholderIndexChange: onPlaceholderIndexChange, activationDistance: activationDistance, containerStyle: containerStyle ? containerStyle : undefined, horizontal: horizontal }, otherProps))); } return (_jsx(PropsProvider, Object.assign({}, props, { children: _jsx(AnimatedValueProvider, { children: _jsx(RefProvider, { flatListRef: ref, children: _jsx(MemoizedInner, Object.assign({}, 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 React.forwardRef(DraggableFlatList);