UNPKG

react-native-draggable-masonry-grid

Version:

React native flatlist based implementation of masonary grid with drag/drop functionality

632 lines (611 loc) 30.4 kB
"use strict"; import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, createElement as _createElement } from 'react'; import { Animated, FlatList, PanResponder, useWindowDimensions, View } from 'react-native'; import { GridCardWrapper } from "./GridCardWrapper.js"; import { arrayMoveFromIndexToIndex, checkIfContainsCoordinates, getGridColumns, getViewVertices, useStateRef } from "./utils.js"; import { jsx as _jsx } from "react/jsx-runtime"; const REARRANGE_ANIMATION_DURATION_MS = 200; const DRAGGED_ITEM_SCALE = 1.2; const AUTO_SCROLL_VELOCITY = 0.5; const AUTO_SCROLL_MAYBE_LOADED_MORE_CLIPS_CHECK_POLL_MS = 500; const REARRANGE_CHECK_THROTTLE_MS = 100; const WAIT_FOR_RESPONDER_TRANSFER_MS = 200; const WINDOW_SIZE_CHECK_THROTTLE_MS = 200; /** * Notes: * - Does not support sticky headers yet */ function GridList(props, ref) { /*----------------------------- extract data -----------------------------*/ const initialNumToRender = props.initialNumToRender ?? 5; const numberOfColumns = props.numColumns ?? 2; const onEndReachedThreshold = props.onEndReachedThreshold ?? 0; const windowSize = props.windowSize ?? 21; const propsRef = useRef(props); propsRef.current = props; const [rearrangedRawData, setRearrangedRawData] = useState(props.data); useLayoutEffect(() => setRearrangedRawData(props.data), [props.data]); const { columnsForGrid, gridDataInSequence, columnHeights } = useMemo(() => getGridColumns(rearrangedRawData, numberOfColumns, props.columnWidth), [rearrangedRawData, numberOfColumns, props.columnWidth]); const gridDataInSequenceRef = useRef(gridDataInSequence); gridDataInSequenceRef.current = gridDataInSequence; const wobbleAnimationConfig = useRef(props.wobbleAnimationConfig).current; const gridItemsCombinedHeight = Math.max(...columnHeights); const rearrangeCacheRef = useRef({ columnsForGrid, columnHeights, gridDataInSequence, rawDataInSequence: rearrangedRawData }); /*----------------------------- declare states and refs -----------------------------*/ const { height } = useWindowDimensions(); const flatListHeightRef = useRef(height); flatListHeightRef.current = height; const headerHeightRef = useRef(0); const listContentHeightRef = useRef(0); listContentHeightRef.current = gridItemsCombinedHeight + headerHeightRef.current; const onTouchResponderGrantPageYRef = useRef(0); const onTouchResponderGrantScrollYRef = useRef(0); const listRef = useRef(null); const listScrollOffsetRef = useRef(0); const topScrollOffsetRef = useRef(0); const bottomScrollOffsetRef = useRef(0); bottomScrollOffsetRef.current = listContentHeightRef.current - flatListHeightRef.current; const cardsCacheRef = useRef({}); const keysOfUndraggableItemsRef = useRef([]); const responderReleaseTimeoutRef = useRef(undefined); const rearrangeAnimationsTimeoutRef = useRef(undefined); const [shouldSetPanResponderRef, setShouldSetPanResponder] = useStateRef(false); const [itemDraggedRef, setItemDragged] = useStateRef(undefined); const lastWindowCheckMsRef = useRef(0); const lastOnEndReachedCheckKeyRef = useRef(undefined); const lastScrollEventSentMsRef = useRef(0); const lastScrollEventOffsetRef = useRef(0); const animatedDraggedItemPositionRef = useRef(new Animated.ValueXY()); const animatedDraggedItemScaleRef = useRef(new Animated.Value(1)); const animatedDraggedItemTranslateYRef = useRef(new Animated.Value(0)); const animatedScrollYRef = useRef(new Animated.Value(0)); const animatedScrollOffsetRef = useRef(0); const [flatlistRenderKey, setFlatlistRenderKey] = useState(0); /*----------------------------- Helpers -----------------------------*/ const keyExtractor = useCallback(item => { if (item.type === 'empty_space') { return String(`${item.columnIndex}-${item.index}`); } return propsRef.current.keyExtractor(item); }, []); const lastItemInList = gridDataInSequenceRef.current[gridDataInSequenceRef.current.length - 1]; const lastItemInListKeyRef = useRef(''); lastItemInListKeyRef.current = lastItemInList ? keyExtractor(lastItemInList) : ''; keysOfUndraggableItemsRef.current = useMemo(() => gridDataInSequence.filter(item => !item.isDraggable).map(v => keyExtractor(v)), [gridDataInSequence, keyExtractor]); const getItemOffsetOnFlatlist = useCallback(item => item.offsetY + headerHeightRef.current, []); const getTotalScrollOffset = useCallback(() => listScrollOffsetRef.current + animatedScrollOffsetRef.current, []); const getOverlapingCard = useCallback((vertices, cardKeysToSkip) => Object.values(cardsCacheRef.current).find(cardCache => !cardKeysToSkip.includes(cardCache.key) && cardCache.vertices && // if centers of both cards are contained within each other checkIfContainsCoordinates(cardCache.vertices.center, vertices) && checkIfContainsCoordinates(vertices.center, cardCache.vertices)), []); const calculateAndCacheCardVertices = useCallback(async (skipDraggedItem = false) => { await Promise.all(Object.values(cardsCacheRef.current).map(async cardCache => { if (skipDraggedItem && itemDraggedRef.current && keyExtractor(itemDraggedRef.current) === cardCache.key) return; cardCache.vertices = await getViewVertices(cardCache.ref.current?.viewRef, { offsets: cardCache.offsets }); })); }, [itemDraggedRef, keyExtractor]); const cardCacheInitialize = useCallback(item => { const cardKey = keyExtractor(item); const position = new Animated.ValueXY(); const cardRef = /*#__PURE__*/React.createRef(); cardsCacheRef.current[cardKey] = { offsets: item.verticeoffsets, key: cardKey, position, ref: cardRef }; }, [keyExtractor]); /*----------------------------- Scroll based events handlers -----------------------------*/ const renderItemsInWindow = useCallback(scrollOffset => { // length of distance we are allowed to display above/below visible viewport const windowExtensionLength = flatListHeightRef.current * (windowSize - 1) / 2; const minOffsetY = Math.round(scrollOffset - windowExtensionLength); const maxOffsetY = Math.round(scrollOffset + flatListHeightRef.current + windowExtensionLength); const itemDraggedKey = itemDraggedRef.current ? keyExtractor(itemDraggedRef.current) : undefined; // render/un-render cards gridDataInSequenceRef.current.forEach(v => { const key = keyExtractor(v); const cardTopPoint = getItemOffsetOnFlatlist(v); const cardBottomPoint = getItemOffsetOnFlatlist(v) + v.height; const shouldRender = cardBottomPoint >= minOffsetY && cardTopPoint <= maxOffsetY || key === itemDraggedKey; cardsCacheRef.current[key]?.ref.current?.setShouldRender(shouldRender); }); }, [getItemOffsetOnFlatlist, itemDraggedRef, keyExtractor, windowSize]); const onScroll = useCallback(scrollOffset => { if (scrollOffset === lastScrollEventOffsetRef.current) return; lastScrollEventOffsetRef.current = scrollOffset; const nowMs = new Date().getTime(); if (propsRef.current.onEndReached) { const distanceFromEnd = Math.round(bottomScrollOffsetRef.current - scrollOffset); const distanceFromEndRatio = distanceFromEnd / listContentHeightRef.current; if (distanceFromEnd < 10 || distanceFromEndRatio <= onEndReachedThreshold) { if (lastOnEndReachedCheckKeyRef.current !== lastItemInListKeyRef.current) { lastOnEndReachedCheckKeyRef.current = lastItemInListKeyRef.current; propsRef.current.onEndReached({ distanceFromEnd }); } } } const timeSinceLastCheckMs = nowMs - lastWindowCheckMsRef.current; if (timeSinceLastCheckMs > WINDOW_SIZE_CHECK_THROTTLE_MS) { lastWindowCheckMsRef.current = nowMs; renderItemsInWindow(scrollOffset); } if (propsRef.current.onScroll) { const timeSinceLastEventMs = nowMs - lastScrollEventSentMsRef.current; if (!propsRef.current.scrollEventThrottle || timeSinceLastEventMs >= propsRef.current.scrollEventThrottle) { lastScrollEventOffsetRef.current = nowMs; propsRef.current.onScroll(scrollOffset); } } }, [onEndReachedThreshold, propsRef, renderItemsInWindow]); /*----------------------------- Auto scrolling handlers -----------------------------*/ const autoScrollVaiablesRef = useRef({}); const autoScrollMethods = useMemo(() => ({ isScrolling: () => !!autoScrollVaiablesRef.current.scrollAnimation, startScroll: direction => { const triggerScroll = () => { const desiredOffset = direction === 'up' ? topScrollOffsetRef.current : bottomScrollOffsetRef.current; const currentScrollOffset = getTotalScrollOffset(); const distanceToDesiredOffset = desiredOffset - currentScrollOffset; // do not attempt to scroll if distanceToDesiredOffset is insignificant if (Math.abs(distanceToDesiredOffset) < 10) return; // adjust animation duration based on distance to offset, to keep scroll animation speed consistent const animationDurationMS = Math.abs(distanceToDesiredOffset / AUTO_SCROLL_VELOCITY); autoScrollVaiablesRef.current.scrollAnimation = Animated.parallel([Animated.timing(animatedScrollYRef.current, { useNativeDriver: true, duration: animationDurationMS, // animate to top/bottom of the list, but take current non animated list offset into account toValue: listScrollOffsetRef.current - desiredOffset }), Animated.timing(animatedDraggedItemTranslateYRef.current, { useNativeDriver: true, duration: animationDurationMS, // keep dragged item where it is on screen by applying opposite animation effect toValue: desiredOffset - listScrollOffsetRef.current })]); autoScrollVaiablesRef.current.scrollAnimation.start(() => { autoScrollVaiablesRef.current.scrollAnimation = undefined; }); }; // trigger immidiatly triggerScroll(); // if onEndReached is provided, keep polling in case new clips have been loaded underneath if (propsRef.current.onEndReached) { // clear previous interval in case called multiple times if (autoScrollVaiablesRef.current.scrollLoadMorePollInterval) clearInterval(autoScrollVaiablesRef.current.scrollLoadMorePollInterval); autoScrollVaiablesRef.current.scrollLoadMorePollInterval = setInterval(() => { if (autoScrollVaiablesRef.current.scrollAnimation) return; // if scroll animation does not exist, it means previous scroll has finished // and attempt to start next scroll in case new clips have been loaded below triggerScroll(); }, AUTO_SCROLL_MAYBE_LOADED_MORE_CLIPS_CHECK_POLL_MS); } }, stopScroll: () => { clearInterval(autoScrollVaiablesRef.current.scrollLoadMorePollInterval); autoScrollVaiablesRef.current.scrollAnimation?.stop(); autoScrollVaiablesRef.current.scrollAnimation = undefined; } }), [getTotalScrollOffset, propsRef]); /*----------------------------- Auto scroll listner -----------------------------*/ useEffect(() => { const listner = animatedScrollYRef.current.addListener(v => { animatedScrollOffsetRef.current = -v.value; onScroll(getTotalScrollOffset()); }); return () => { // eslint-disable-next-line react-hooks/exhaustive-deps animatedScrollYRef.current.removeListener(listner); }; }, [animatedScrollYRef, getTotalScrollOffset, onScroll]); /*----------------------------- Drag/drop Rearrange handlers -----------------------------*/ const getDataInRearrangedSequence = useCallback((draggedCardKey, overlappingCardKey, dataInCurrentSequence) => { const { gridDataInSequence, rawDataInSequence } = dataInCurrentSequence; const itemDragged = gridDataInSequence.find(itemOriginal => keyExtractor(itemOriginal) === draggedCardKey); const itemOverlapping = gridDataInSequence.find(itemOriginal => keyExtractor(itemOriginal) === overlappingCardKey); if (!itemDragged || !itemOverlapping) return dataInCurrentSequence; const { originalIndex: indexToMoveFrom } = itemDragged; const { originalIndex: indexToMoveTo } = itemOverlapping; const rearrangedRawData = arrayMoveFromIndexToIndex(rawDataInSequence, indexToMoveFrom, indexToMoveTo); const rearrangedGridData = getGridColumns(rearrangedRawData, numberOfColumns, propsRef.current.columnWidth); return { ...rearrangedGridData, rawDataInSequence: rearrangedRawData }; }, [keyExtractor, numberOfColumns]); const getRearrangeAnimation = useCallback((position, value) => Animated.timing(position, { toValue: value, duration: REARRANGE_ANIMATION_DURATION_MS, useNativeDriver: true }), []); const runRearrangeAnimations = useCallback(async (rearrangedGridDataInSequence, skipDraggedItem = false, prallelAnimations) => { const animations = prallelAnimations ?? []; gridDataInSequenceRef.current.forEach(item => { // do not rearrange empty spaces if (item.type === 'empty_space') return; const key = keyExtractor(item); const oldPosition = item.position; const newPosition = rearrangedGridDataInSequence.find(newItem => keyExtractor(newItem) === key)?.position; if (!cardsCacheRef.current[key] || !newPosition || skipDraggedItem && keyExtractor(item) === keyExtractor(itemDraggedRef.current)) return; animations.push(getRearrangeAnimation(cardsCacheRef.current[key].position, { x: newPosition.x - oldPosition.x, y: newPosition.y - oldPosition.y })); }); if (!skipDraggedItem) { animations.push(getRearrangeAnimation(animatedDraggedItemPositionRef.current, { x: 0, y: 0 })); } return new Promise(res => Animated.parallel(animations).start(() => res())); }, [keyExtractor, itemDraggedRef, getRearrangeAnimation]); const resetRearrangeAnimations = useCallback(() => { Object.values(cardsCacheRef.current).forEach(cardCache => cardCache.position.setValue({ x: 0, y: 0 })); }, []); const handleItemDragStart = useCallback(item => { setShouldSetPanResponder(true); setItemDragged(item); calculateAndCacheCardVertices(); Animated.spring(animatedDraggedItemScaleRef.current, { toValue: DRAGGED_ITEM_SCALE, useNativeDriver: true }).start(); }, [calculateAndCacheCardVertices, setItemDragged, setShouldSetPanResponder]); const triggerDragEnd = useCallback(() => { if (!itemDraggedRef.current) return; setShouldSetPanResponder(false); autoScrollMethods.stopScroll(); // reset scroll animations and set actual scroll value const currentScrollOffset = getTotalScrollOffset(); // Given both animated scroll value and list scroll values are changing simultaneously // We update the variables simultaneously instead of waiting for event to fire for each // which may result in unessary triggering of onScroll methods animatedScrollOffsetRef.current = 0; listScrollOffsetRef.current = currentScrollOffset; // update the scrolls to move all scroll to flatlist native scroll animatedScrollYRef.current.setValue(animatedScrollOffsetRef.current); listRef.current?.scrollToOffset({ animated: false, offset: listScrollOffsetRef.current }); // remove current animated effects from dragged item const animationUndoScrollOffset = getRearrangeAnimation(animatedDraggedItemTranslateYRef.current, 0); const animationUndoScale = Animated.spring(animatedDraggedItemScaleRef.current, { toValue: 1, useNativeDriver: true }); // animate dragged item to it's new position runRearrangeAnimations(rearrangeCacheRef.current.gridDataInSequence, false, [animationUndoScrollOffset, animationUndoScale]).finally(() => { // reset data setItemDragged(undefined); // set it false again just in case user tried to drag another item meanwhile setShouldSetPanResponder(false); setRearrangedRawData(rearrangeCacheRef.current.rawDataInSequence); // force re render root flatlist setFlatlistRenderKey(v => v + 1); // to reduce the flicker when scrolling to the current offset of newly render flatlist we do: const currentScrollOffset = getTotalScrollOffset(); // move all scroll offset to animated value so when list is rendered it's already at correct offset animatedScrollOffsetRef.current = currentScrollOffset; listScrollOffsetRef.current = 0; animatedScrollYRef.current.setValue(-animatedScrollOffsetRef.current); listRef.current?.scrollToOffset({ animated: false, offset: listScrollOffsetRef.current }); // once list is rendered move all scroll offset back to list's scrollview setTimeout(() => { animatedScrollOffsetRef.current = 0; listScrollOffsetRef.current = currentScrollOffset; animatedScrollYRef.current.setValue(animatedScrollOffsetRef.current); listRef.current?.scrollToOffset({ animated: false, offset: listScrollOffsetRef.current }); }, 0); // make sure the values are reset in case animation was interrupted animatedDraggedItemTranslateYRef.current.setValue(0); animatedDraggedItemScaleRef.current.setValue(1); propsRef.current.onRearrange(rearrangeCacheRef.current.rawDataInSequence); }); }, [itemDraggedRef, setShouldSetPanResponder, autoScrollMethods, getTotalScrollOffset, getRearrangeAnimation, runRearrangeAnimations, setItemDragged]); const handleItemDragEnd = useCallback((skipTimeout = false) => { if (!itemDraggedRef.current) return; clearTimeout(responderReleaseTimeoutRef.current); if (typeof skipTimeout === 'boolean' && skipTimeout) { triggerDragEnd(); } else { responderReleaseTimeoutRef.current = setTimeout(triggerDragEnd, WAIT_FOR_RESPONDER_TRANSFER_MS); } }, [itemDraggedRef, triggerDragEnd]); const matchAnimationPositionsToCurrentDataSequence = useCallback(async () => { const gridData = getGridColumns(propsRef.current.data, numberOfColumns, propsRef.current.columnWidth); rearrangeCacheRef.current = { ...gridData, rawDataInSequence: propsRef.current.data }; await runRearrangeAnimations(gridData.gridDataInSequence); }, [numberOfColumns, runRearrangeAnimations]); const draggedItemPanhandlers = useMemo(() => PanResponder.create({ onStartShouldSetPanResponder: () => !!shouldSetPanResponderRef.current, onMoveShouldSetPanResponder: () => !!shouldSetPanResponderRef.current, onStartShouldSetPanResponderCapture: () => !!shouldSetPanResponderRef.current, onMoveShouldSetPanResponderCapture: () => !!shouldSetPanResponderRef.current, // Do not consider responder as released if it's being transfered to this responder from child responder // that triggered the drag onPanResponderGrant: e => { onTouchResponderGrantPageYRef.current = e.nativeEvent.pageY; onTouchResponderGrantScrollYRef.current = getTotalScrollOffset(); // clear timeout that primed to release the dragged item when touch event was remove from responder else where // because the touch has been move to this pan responder now setTimeout(() => clearTimeout(responderReleaseTimeoutRef.current), 0); }, onPanResponderMove: Animated.event([null, { dx: animatedDraggedItemPositionRef.current.x, dy: animatedDraggedItemPositionRef.current.y }], { listener: e => { if (!itemDraggedRef.current) return; const itemDraggedKey = keyExtractor(itemDraggedRef.current); const itemDraggedoffsets = itemDraggedRef.current.verticeoffsets; const itemDraggedCardRef = cardsCacheRef.current[itemDraggedKey]?.ref; const offsetPadding = Math.min(50, Math.round(itemDraggedRef.current.height / 2)); // pageY is current offset of the touch, substract pageY recorded on touch begin == relative change in offset const touchMovedOffsetY = e.nativeEvent.pageY - onTouchResponderGrantPageYRef.current; // substracting the scroll offset (recorded o}, [getTotalScrollOffset, onScroll])n touch begin), from item offset on the flatlist // == distance of item on the screen relative to flatlist view const touchBeginItemLocationY = getItemOffsetOnFlatlist(itemDraggedRef.current) - onTouchResponderGrantScrollYRef.current; // Adding the distance y touch has moved to the initial location y of item on screen // == current location y of item on the screen const itemCurrentOffsetY = Math.round(touchMovedOffsetY + touchBeginItemLocationY); const isItemOffScreenTop = itemCurrentOffsetY < -offsetPadding + (propsRef.current.viewOffsets?.top ?? 0); const isItemOffScreenBottom = itemCurrentOffsetY + itemDraggedRef.current.height > flatListHeightRef.current + offsetPadding - (propsRef.current.viewOffsets?.bottom ?? 0); if (isItemOffScreenTop || isItemOffScreenBottom) { const desiredOffset = isItemOffScreenTop ? topScrollOffsetRef.current : bottomScrollOffsetRef.current; const distanceToDesiredOffset = desiredOffset - getTotalScrollOffset(); if (Math.abs(distanceToDesiredOffset) > 10) { if (!autoScrollMethods.isScrolling()) { autoScrollMethods.startScroll(isItemOffScreenTop ? 'up' : 'down'); } // if auto scrolling skip rearrange to avoid additional processing time return; } } else { autoScrollMethods.stopScroll(); } (async () => { const vertices = await getViewVertices(itemDraggedCardRef?.current?.viewRef, { offsets: itemDraggedoffsets, scale: DRAGGED_ITEM_SCALE }); const draggedCardKey = keyExtractor(itemDraggedRef.current); const overlappingCardKey = getOverlapingCard(vertices, // skip overlap check with self and undraggable items [itemDraggedKey, ...keysOfUndraggableItemsRef.current])?.key; if (!overlappingCardKey) return; // do not clear timeout unless overlappingCardKey has changed if (overlappingCardKey === rearrangeAnimationsTimeoutRef.current?.key) return; clearTimeout(rearrangeAnimationsTimeoutRef.current?.timeout); const animationTimeout = setTimeout(async () => { rearrangeCacheRef.current = getDataInRearrangedSequence(draggedCardKey, overlappingCardKey, rearrangeCacheRef.current); await runRearrangeAnimations(rearrangeCacheRef.current.gridDataInSequence, true); // caluclate and store vertices of rearranged card positions await calculateAndCacheCardVertices(); rearrangeAnimationsTimeoutRef.current = undefined; }, REARRANGE_CHECK_THROTTLE_MS); rearrangeAnimationsTimeoutRef.current = { key: overlappingCardKey, timeout: animationTimeout }; })(); }, useNativeDriver: false }), onPanResponderRelease: () => handleItemDragEnd(true) }), [autoScrollMethods, calculateAndCacheCardVertices, getDataInRearrangedSequence, getItemOffsetOnFlatlist, getOverlapingCard, getTotalScrollOffset, handleItemDragEnd, itemDraggedRef, keyExtractor, runRearrangeAnimations, shouldSetPanResponderRef]).panHandlers; /*----------------------------- Handler for grid -----------------------------*/ useImperativeHandle(ref, () => ({ animateToOriginalPositions: matchAnimationPositionsToCurrentDataSequence, scrollToTop: (animated = false) => { listRef.current?.scrollToOffset({ offset: 0, animated }); }, scrollToIndex: ({ animated = false, index, offset }) => { const itemToScrollTo = gridDataInSequenceRef.current.find(item => item.originalIndex === index); if (!itemToScrollTo) return; setTimeout(() => { listRef.current?.scrollToOffset({ offset: getItemOffsetOnFlatlist(itemToScrollTo) + offset, animated }); }, 0); } }), [getItemOffsetOnFlatlist, matchAnimationPositionsToCurrentDataSequence]); /*----------------------------- Clear all timeouts etc on unmount -----------------------------*/ useEffect(() => { return () => { clearTimeout(responderReleaseTimeoutRef.current); clearTimeout(rearrangeAnimationsTimeoutRef.current?.timeout); // eslint-disable-next-line react-hooks/exhaustive-deps clearInterval(autoScrollVaiablesRef.current.scrollLoadMorePollInterval); }; }, []); /*----------------------------- Data change handler -----------------------------*/ useLayoutEffect(() => { // when grid data changes // - render new items in view renderItemsInWindow(getTotalScrollOffset()); // - calculate vertices of new items calculateAndCacheCardVertices(); // - reset animations as grid will do the work to show data in new sequence resetRearrangeAnimations(); rearrangeCacheRef.current = { columnsForGrid, columnHeights, gridDataInSequence, rawDataInSequence: rearrangedRawData }; }, [calculateAndCacheCardVertices, columnHeights, columnsForGrid, getTotalScrollOffset, gridDataInSequence, rearrangedRawData, renderItemsInWindow, resetRearrangeAnimations]); /*************** Main render ***************/ const renderItem = useCallback(({ item }) => { if (item.type === 'empty_space') { return /*#__PURE__*/_jsx(View, { style: { height: item.height, opacity: 0, width: propsRef.current.columnWidth } }); } const key = keyExtractor(item); if (!cardsCacheRef.current[key]) { cardCacheInitialize(item); } if (!cardsCacheRef.current[key]) { throw new Error('[renderItem] cardsCacheRef.current[key] cannot be undefined'); } const { position, ref } = cardsCacheRef.current[key]; const isThisCardBeingDragged = itemDraggedRef.current && key === keyExtractor(itemDraggedRef.current); return /*#__PURE__*/_jsx(GridCardWrapper // mimic react native flatlist behaviour , { alwaysRender: item.index < initialNumToRender, panHandlers: draggedItemPanhandlers, ref: ref, style: { height: item.height, transform: [...position.getTranslateTransform(), ...(isThisCardBeingDragged ? [...animatedDraggedItemPositionRef.current.getTranslateTransform(), { translateY: animatedDraggedItemTranslateYRef.current }, { scale: animatedDraggedItemScaleRef.current }] : [])], width: propsRef.current.columnWidth }, wobble: !isThisCardBeingDragged && !!propsRef.current.wobble, wobbleAnimationConfig: wobbleAnimationConfig, children: propsRef.current.renderItem(item, () => handleItemDragStart(item), handleItemDragEnd) }); }, [cardCacheInitialize, draggedItemPanhandlers, handleItemDragEnd, handleItemDragStart, initialNumToRender, itemDraggedRef, keyExtractor, wobbleAnimationConfig]); const renderCellComponent = useCallback(({ style, ...cellComponentProps }) => { const item = cellComponentProps.item; const isDragged = itemDraggedRef.current && keyExtractor(itemDraggedRef.current) === keyExtractor(item); return /*#__PURE__*/_jsx(View, { style: [style, { zIndex: isDragged ? 1 : 0 }], ...cellComponentProps }); }, [itemDraggedRef, keyExtractor]); const getItemLayout = useCallback((data, index) => { const content = data?.[index]; return { index, length: content?.height ?? 0, offset: content?.offsetY ?? 0 }; }, []); const renderColumn = useCallback(({ item: columnItem, index }) => /*#__PURE__*/_jsx(Animated.FlatList, { // to make zIndex work in FlatList CellRendererComponent: renderCellComponent // @ts-ignore , data: columnItem, getItemLayout: getItemLayout, initialNumToRender: Number.MAX_SAFE_INTEGER, keyExtractor: keyExtractor, listKey: String(index), renderItem: renderItem, scrollEnabled: false, showsVerticalScrollIndicator: false, style: { transform: [{ translateY: animatedScrollYRef.current }], zIndex: itemDraggedRef.current?.columnIndex === index ? 1 : 0 }, viewabilityConfigCallbackPairs: propsRef.current.columnViewabilityConfigCallbackPairs?.[index], windowSize: Number.MAX_SAFE_INTEGER }), [getItemLayout, itemDraggedRef, keyExtractor, renderCellComponent, renderItem]); return /*#__PURE__*/_createElement(FlatList, { ...props, key: flatlistRenderKey, data: columnsForGrid.find(col => col.length > 0) ? columnsForGrid : [], disableScrollViewPanResponder: shouldSetPanResponderRef.current, initialNumToRender: numberOfColumns, keyExtractor: (_, index) => String(index), ListHeaderComponent: props.ListHeaderComponent ? /*#__PURE__*/_jsx(Animated.View, { onLayout: e => headerHeightRef.current = Math.round(e.nativeEvent.layout.height), style: { transform: [{ translateY: animatedScrollYRef.current }] }, children: props.ListHeaderComponent }) : null, ListFooterComponent: props.ListFooterComponent ? /*#__PURE__*/_jsx(Animated.View, { style: { transform: [{ translateY: animatedScrollYRef.current }] }, children: props.ListFooterComponent }) : null, numColumns: numberOfColumns, onLayout: e => { flatListHeightRef.current = Math.round(e.nativeEvent.layout.height); props.onLayout?.(e); }, onScroll: e => { listScrollOffsetRef.current = e.nativeEvent.contentOffset.y; onScroll(getTotalScrollOffset()); }, ref: listRef, renderItem: renderColumn, scrollEventThrottle: 1, scrollEnabled: !shouldSetPanResponderRef.current && props.scrollEnabled }); } // to allow generic props with forwardRef export const DraggableMasonryGridList = /*#__PURE__*/forwardRef(GridList); //# sourceMappingURL=GridList.js.map