@kietpt2003/react-native-core-ui
Version:
React Native Core UI components by KietPT
283 lines (282 loc) • 14.1 kB
JavaScript
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);