From 830472789135156313183f0baf55a52fcafaf0d9 Mon Sep 17 00:00:00 2001 From: Ofer Sadgat Date: Mon, 16 Sep 2024 20:09:26 -0700 Subject: [PATCH 1/4] Get the correct receiver --- src/hooks/useDraxRegistry.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/hooks/useDraxRegistry.ts b/src/hooks/useDraxRegistry.ts index 60e476e..785c373 100644 --- a/src/hooks/useDraxRegistry.ts +++ b/src/hooks/useDraxRegistry.ts @@ -145,6 +145,18 @@ const getAbsoluteViewEntryFromRegistry = ( return data && { id, data }; }; +/** + * If multiple recievers match, we need to pick the one that is on top. This + * is first done by filtering out all that are parents (because parent views are below child ones) + * and then if there are any further possibilities, it chooses the smallest one. + */ +const getTopMostReceiver = (receivers: DraxFoundAbsoluteViewEntry[]) => { + const ids = receivers.map(receiver => receiver.data.parentId); + receivers = receivers.filter(receiver => !ids.includes(receiver.id)); + receivers.sort((receiverA, receiverB) => receiverA.data.measurements.height*receiverA.data.measurements.width - receiverB.data.measurements.height*receiverB.data.measurements.width); + return receivers[0]; +}; + /** * Find all monitoring views and the latest receptive view that * contain the touch coordinates, excluding the specified view. @@ -155,7 +167,7 @@ const findMonitorsAndReceiverInRegistry = ( excludeViewId: string, ) => { const monitors: DraxFoundAbsoluteViewEntry[] = []; - let receiver: DraxFoundAbsoluteViewEntry | undefined; + let receivers: DraxFoundAbsoluteViewEntry[] = []; // console.log(`find monitors and receiver for absolute position (${absolutePosition.x}, ${absolutePosition.y})`); registry.viewIds.forEach((targetId) => { @@ -212,14 +224,14 @@ const findMonitorsAndReceiverInRegistry = ( if (receptive) { // It's the latest receiver found. - receiver = foundView; + receivers.push(foundView); // console.log('it\'s a receiver'); } } }); return { monitors, - receiver, + receiver: getTopMostReceiver(receivers) }; }; From da5e2d3ddbee23b37ec3f966da29052fc7e0e40a Mon Sep 17 00:00:00 2001 From: Ofer Sadgat Date: Mon, 16 Sep 2024 20:15:39 -0700 Subject: [PATCH 2/4] add build --- build/DraxContext.d.ts | 3 + build/DraxContext.js | 6 + build/DraxList.d.ts | 8 + build/DraxList.js | 497 +++++++++++++++++++++ build/DraxProvider.d.ts | 4 + build/DraxProvider.js | 641 ++++++++++++++++++++++++++++ build/DraxScrollView.d.ts | 6 + build/DraxScrollView.js | 197 +++++++++ build/DraxSubprovider.d.ts | 3 + build/DraxSubprovider.js | 18 + build/DraxView.d.ts | 3 + build/DraxView.js | 416 ++++++++++++++++++ build/hooks/index.d.ts | 4 + build/hooks/index.js | 11 + build/hooks/useDraxContext.d.ts | 1 + build/hooks/useDraxContext.js | 13 + build/hooks/useDraxId.d.ts | 1 + build/hooks/useDraxId.js | 13 + build/hooks/useDraxRegistry.d.ts | 77 ++++ build/hooks/useDraxRegistry.js | 712 +++++++++++++++++++++++++++++++ build/hooks/useDraxState.d.ts | 10 + build/hooks/useDraxState.js | 129 ++++++ build/index.d.ts | 7 + build/index.js | 30 ++ build/math.d.ts | 22 + build/math.js | 64 +++ build/params.d.ts | 18 + build/params.js | 21 + build/transform.d.ts | 4 + build/transform.js | 17 + build/types.d.ts | 656 ++++++++++++++++++++++++++++ build/types.js | 43 ++ 32 files changed, 3655 insertions(+) create mode 100644 build/DraxContext.d.ts create mode 100644 build/DraxContext.js create mode 100644 build/DraxList.d.ts create mode 100644 build/DraxList.js create mode 100644 build/DraxProvider.d.ts create mode 100644 build/DraxProvider.js create mode 100644 build/DraxScrollView.d.ts create mode 100644 build/DraxScrollView.js create mode 100644 build/DraxSubprovider.d.ts create mode 100644 build/DraxSubprovider.js create mode 100644 build/DraxView.d.ts create mode 100644 build/DraxView.js create mode 100644 build/hooks/index.d.ts create mode 100644 build/hooks/index.js create mode 100644 build/hooks/useDraxContext.d.ts create mode 100644 build/hooks/useDraxContext.js create mode 100644 build/hooks/useDraxId.d.ts create mode 100644 build/hooks/useDraxId.js create mode 100644 build/hooks/useDraxRegistry.d.ts create mode 100644 build/hooks/useDraxRegistry.js create mode 100644 build/hooks/useDraxState.d.ts create mode 100644 build/hooks/useDraxState.js create mode 100644 build/index.d.ts create mode 100644 build/index.js create mode 100644 build/math.d.ts create mode 100644 build/math.js create mode 100644 build/params.d.ts create mode 100644 build/params.js create mode 100644 build/transform.d.ts create mode 100644 build/transform.js create mode 100644 build/types.d.ts create mode 100644 build/types.js diff --git a/build/DraxContext.d.ts b/build/DraxContext.d.ts new file mode 100644 index 0000000..b952f40 --- /dev/null +++ b/build/DraxContext.d.ts @@ -0,0 +1,3 @@ +/// +import { DraxContextValue } from './types'; +export declare const DraxContext: import("react").Context; diff --git a/build/DraxContext.js b/build/DraxContext.js new file mode 100644 index 0000000..7c35fea --- /dev/null +++ b/build/DraxContext.js @@ -0,0 +1,6 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DraxContext = void 0; +const react_1 = require("react"); +exports.DraxContext = (0, react_1.createContext)(undefined); +exports.DraxContext.displayName = 'Drax'; diff --git a/build/DraxList.d.ts b/build/DraxList.d.ts new file mode 100644 index 0000000..53b0b07 --- /dev/null +++ b/build/DraxList.d.ts @@ -0,0 +1,8 @@ +import { PropsWithChildren, Ref } from 'react'; +import { FlatList } from 'react-native'; +import { DraxListProps } from './types'; +declare type DraxListType = (props: PropsWithChildren> & { + ref?: Ref; +}) => JSX.Element; +export declare const DraxList: DraxListType; +export {}; diff --git a/build/DraxList.js b/build/DraxList.js new file mode 100644 index 0000000..456c190 --- /dev/null +++ b/build/DraxList.js @@ -0,0 +1,497 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DraxList = void 0; +const react_1 = __importStar(require("react")); +const react_native_1 = require("react-native"); +const DraxView_1 = require("./DraxView"); +const DraxSubprovider_1 = require("./DraxSubprovider"); +const hooks_1 = require("./hooks"); +const types_1 = require("./types"); +const params_1 = require("./params"); +const defaultStyles = react_native_1.StyleSheet.create({ + draggingStyle: { opacity: 0 }, + dragReleasedStyle: { opacity: 0.5 }, +}); +const DraxListUnforwarded = (props, forwardedRef) => { + const { data, style, flatListStyle, itemStyles, renderItemContent, renderItemHoverContent, onItemDragStart, onItemDragPositionChange, onItemDragEnd, onItemReorder, viewPropsExtractor, id: idProp, reorderable: reorderableProp, onScroll: onScrollProp, itemsDraggable = true, lockItemDragsToMainAxis = false, longPressDelay = params_1.defaultListItemLongPressDelay, ...flatListProps } = props; + // Copy the value of the horizontal property for internal use. + const horizontal = flatListProps.horizontal ?? false; + // Get the item count for internal use. + const itemCount = data?.length ?? 0; + // Set a sensible default for reorderable prop. + const reorderable = reorderableProp ?? (onItemReorder !== undefined); + // The unique identifer for this list's Drax view. + const id = (0, hooks_1.useDraxId)(idProp); + // FlatList, used for scrolling. + const flatListRef = (0, react_1.useRef)(null); + // FlatList node handle, used for measuring children. + const nodeHandleRef = (0, react_1.useRef)(null); + // Container view measurements, for scrolling by percentage. + const containerMeasurementsRef = (0, react_1.useRef)(undefined); + // Content size, for scrolling by percentage. + const contentSizeRef = (0, react_1.useRef)(undefined); + // Scroll position, for Drax bounds checking and auto-scrolling. + const scrollPositionRef = (0, react_1.useRef)({ x: 0, y: 0 }); + // Original index of the currently dragged list item, if any. + const draggedItemRef = (0, react_1.useRef)(undefined); + // Auto-scrolling state. + const scrollStateRef = (0, react_1.useRef)(types_1.AutoScrollDirection.None); + // Auto-scrolling interval. + const scrollIntervalRef = (0, react_1.useRef)(undefined); + // List item measurements, for determining shift. + const itemMeasurementsRef = (0, react_1.useRef)([]); + // Drax view registrations, for remeasuring after reorder. + const registrationsRef = (0, react_1.useRef)([]); + // Shift offsets. + const shiftsRef = (0, react_1.useRef)([]); + // Maintain cache of reordered list indexes until data updates. + const [originalIndexes, setOriginalIndexes] = (0, react_1.useState)([]); + // Maintain the index the item is currently dragged to. + const draggedToIndex = (0, react_1.useRef)(undefined); + // Adjust measurements, registrations, and shift value arrays as item count changes. + (0, react_1.useEffect)(() => { + const itemMeasurements = itemMeasurementsRef.current; + const registrations = registrationsRef.current; + const shifts = shiftsRef.current; + if (itemMeasurements.length > itemCount) { + itemMeasurements.splice(itemCount - itemMeasurements.length); + } + else { + while (itemMeasurements.length < itemCount) { + itemMeasurements.push(undefined); + } + } + if (registrations.length > itemCount) { + registrations.splice(itemCount - registrations.length); + } + else { + while (registrations.length < itemCount) { + registrations.push(undefined); + } + } + if (shifts.length > itemCount) { + shifts.splice(itemCount - shifts.length); + } + else { + while (shifts.length < itemCount) { + shifts.push({ + targetValue: 0, + animatedValue: new react_native_1.Animated.Value(0), + }); + } + } + }, [itemCount]); + // Clear reorders when data changes. + (0, react_1.useLayoutEffect)(() => { + // console.log('clear reorders'); + setOriginalIndexes(data ? [...Array(data.length).keys()] : []); + }, [data]); + // Apply the reorder cache to the data. + const reorderedData = (0, react_1.useMemo)(() => { + // console.log('refresh sorted data'); + if (!id || !data) { + return null; + } + if (data.length !== originalIndexes.length) { + return data; + } + return originalIndexes.map((index) => data[index]); + }, [id, data, originalIndexes]); + // Get shift transform for list item at index. + const getShiftTransform = (0, react_1.useCallback)((index) => { + const shift = shiftsRef.current[index]?.animatedValue ?? 0; + return horizontal + ? [{ translateX: shift }] + : [{ translateY: shift }]; + }, [horizontal]); + // Set the currently dragged list item. + const setDraggedItem = (0, react_1.useCallback)((originalIndex) => { + draggedItemRef.current = originalIndex; + }, []); + // Clear the currently dragged list item. + const resetDraggedItem = (0, react_1.useCallback)(() => { + draggedItemRef.current = undefined; + }, []); + // Drax view renderItem wrapper. + const renderItem = (0, react_1.useCallback)((info) => { + const { index, item } = info; + const originalIndex = originalIndexes[index]; + const { style: itemStyle, draggingStyle = defaultStyles.draggingStyle, dragReleasedStyle = defaultStyles.dragReleasedStyle, ...otherStyleProps } = itemStyles ?? {}; + return (react_1.default.createElement(DraxView_1.DraxView, { style: [itemStyle, { transform: getShiftTransform(originalIndex) }], draggingStyle: draggingStyle, dragReleasedStyle: dragReleasedStyle, ...otherStyleProps, longPressDelay: longPressDelay, lockDragXPosition: lockItemDragsToMainAxis && !horizontal, lockDragYPosition: lockItemDragsToMainAxis && horizontal, draggable: itemsDraggable, payload: { index, originalIndex }, ...(viewPropsExtractor?.(item) ?? {}), onDragEnd: resetDraggedItem, onDragDrop: resetDraggedItem, onMeasure: (measurements) => { + if (originalIndex !== undefined) { + // console.log(`measuring [${index}, ${originalIndex}]: (${measurements?.x}, ${measurements?.y})`); + itemMeasurementsRef.current[originalIndex] = measurements; + } + }, registration: (registration) => { + if (registration && originalIndex !== undefined) { + // console.log(`registering [${index}, ${originalIndex}], ${registration.id}`); + registrationsRef.current[originalIndex] = registration; + registration.measure(); + } + }, renderContent: (contentProps) => renderItemContent(info, contentProps), renderHoverContent: renderItemHoverContent + && ((hoverContentProps) => renderItemHoverContent(info, hoverContentProps)) })); + }, [ + originalIndexes, + itemStyles, + viewPropsExtractor, + getShiftTransform, + resetDraggedItem, + itemsDraggable, + renderItemContent, + renderItemHoverContent, + longPressDelay, + lockItemDragsToMainAxis, + horizontal, + ]); + // Track the size of the container view. + const onMeasureContainer = (0, react_1.useCallback)((measurements) => { + containerMeasurementsRef.current = measurements; + }, []); + // Track the size of the content. + const onContentSizeChange = (0, react_1.useCallback)((width, height) => { + contentSizeRef.current = { x: width, y: height }; + }, []); + // Set FlatList and node handle refs. + const setFlatListRefs = (0, react_1.useCallback)((ref) => { + flatListRef.current = ref; + nodeHandleRef.current = ref && (0, react_native_1.findNodeHandle)(ref); + if (forwardedRef) { + if (typeof forwardedRef === 'function') { + forwardedRef(ref); + } + else { + // eslint-disable-next-line no-param-reassign + forwardedRef.current = ref; + } + } + }, [forwardedRef]); + // Update tracked scroll position when list is scrolled. + const onScroll = (0, react_1.useCallback)((event) => { + const { nativeEvent: { contentOffset } } = event; + scrollPositionRef.current = { ...contentOffset }; + onScrollProp?.(event); + }, [onScrollProp]); + // Handle auto-scrolling on interval. + const doScroll = (0, react_1.useCallback)(() => { + const flatList = flatListRef.current; + const containerMeasurements = containerMeasurementsRef.current; + const contentSize = contentSizeRef.current; + if (!flatList || !containerMeasurements || !contentSize) { + return; + } + let containerLength; + let contentLength; + let prevOffset; + if (horizontal) { + containerLength = containerMeasurements.width; + contentLength = contentSize.x; + prevOffset = scrollPositionRef.current.x; + } + else { + containerLength = containerMeasurements.height; + contentLength = contentSize.y; + prevOffset = scrollPositionRef.current.y; + } + const jumpLength = containerLength * 0.2; + let offset; + if (scrollStateRef.current === types_1.AutoScrollDirection.Forward) { + const maxOffset = contentLength - containerLength; + if (prevOffset < maxOffset) { + offset = Math.min(prevOffset + jumpLength, maxOffset); + } + } + else if (scrollStateRef.current === types_1.AutoScrollDirection.Back) { + if (prevOffset > 0) { + offset = Math.max(prevOffset - jumpLength, 0); + } + } + if (offset !== undefined) { + flatList.scrollToOffset({ offset }); + flatList.flashScrollIndicators(); + } + }, [horizontal]); + // Start the auto-scrolling interval. + const startScroll = (0, react_1.useCallback)(() => { + if (scrollIntervalRef.current) { + return; + } + doScroll(); + scrollIntervalRef.current = setInterval(doScroll, 250); + }, [doScroll]); + // Stop the auto-scrolling interval. + const stopScroll = (0, react_1.useCallback)(() => { + if (scrollIntervalRef.current) { + clearInterval(scrollIntervalRef.current); + scrollIntervalRef.current = undefined; + } + }, []); + // If startScroll changes, refresh our interval. + (0, react_1.useEffect)(() => { + if (scrollIntervalRef.current) { + stopScroll(); + startScroll(); + } + }, [stopScroll, startScroll]); + // Reset all shift values. + const resetShifts = (0, react_1.useCallback)(() => { + shiftsRef.current.forEach((shift) => { + // eslint-disable-next-line no-param-reassign + shift.targetValue = 0; + shift.animatedValue.setValue(0); + }); + }, []); + // Update shift values in response to a drag. + const updateShifts = (0, react_1.useCallback)(({ index: fromIndex, originalIndex: fromOriginalIndex }, { index: toIndex }) => { + const { width = 50, height = 50 } = itemMeasurementsRef.current[fromOriginalIndex] ?? {}; + const offset = horizontal ? width : height; + originalIndexes.forEach((originalIndex, index) => { + const shift = shiftsRef.current[originalIndex]; + let newTargetValue = 0; + if (index > fromIndex && index <= toIndex) { + newTargetValue = -offset; + } + else if (index < fromIndex && index >= toIndex) { + newTargetValue = offset; + } + if (shift.targetValue !== newTargetValue) { + shift.targetValue = newTargetValue; + react_native_1.Animated.timing(shift.animatedValue, { + duration: 200, + toValue: newTargetValue, + useNativeDriver: true, + }).start(); + } + }); + }, [originalIndexes, horizontal]); + // Calculate absolute position of list item for snapback. + const calculateSnapbackTarget = (0, react_1.useCallback)(({ index: fromIndex, originalIndex: fromOriginalIndex }, { index: toIndex, originalIndex: toOriginalIndex }) => { + const containerMeasurements = containerMeasurementsRef.current; + const itemMeasurements = itemMeasurementsRef.current; + if (containerMeasurements) { + let targetPos; + if (fromIndex < toIndex) { + // Target pos(toIndex + 1) - pos(fromIndex) + const nextIndex = toIndex + 1; + let nextPos; + if (nextIndex < itemCount) { + // toIndex + 1 is in the list. We can measure the position of the next item. + const nextMeasurements = itemMeasurements[originalIndexes[nextIndex]]; + if (nextMeasurements) { + nextPos = { x: nextMeasurements.x, y: nextMeasurements.y }; + } + } + else { + // toIndex is the last item of the list. We can use the list content size. + const contentSize = contentSizeRef.current; + if (contentSize) { + nextPos = horizontal + ? { x: contentSize.x, y: 0 } + : { x: 0, y: contentSize.y }; + } + } + const fromMeasurements = itemMeasurements[fromOriginalIndex]; + if (nextPos && fromMeasurements) { + targetPos = horizontal + ? { x: nextPos.x - fromMeasurements.width, y: nextPos.y } + : { x: nextPos.x, y: nextPos.y - fromMeasurements.height }; + } + } + else { + // Target pos(toIndex) + const toMeasurements = itemMeasurements[toOriginalIndex]; + if (toMeasurements) { + targetPos = { x: toMeasurements.x, y: toMeasurements.y }; + } + } + if (targetPos) { + const scrollPosition = scrollPositionRef.current; + return { + x: containerMeasurements.x - scrollPosition.x + targetPos.x, + y: containerMeasurements.y - scrollPosition.y + targetPos.y, + }; + } + } + return types_1.DraxSnapbackTargetPreset.None; + }, [horizontal, itemCount, originalIndexes]); + // Stop auto-scrolling, and potentially update shifts and reorder data. + const handleInternalDragEnd = (0, react_1.useCallback)((eventData, totalDragEnd) => { + // Always stop auto-scroll on drag end. + scrollStateRef.current = types_1.AutoScrollDirection.None; + stopScroll(); + const { dragged, receiver } = eventData; + // Check if we need to handle this drag end. + if (reorderable && dragged.parentId === id) { + // Determine list indexes of dragged/received items, if any. + const fromPayload = dragged.payload; + const toPayload = receiver?.parentId === id + ? receiver.payload + : undefined; + const { index: fromIndex, originalIndex: fromOriginalIndex } = fromPayload; + const { index: toIndex, originalIndex: toOriginalIndex } = toPayload ?? {}; + const toItem = (toOriginalIndex !== undefined) ? data?.[toOriginalIndex] : undefined; + // Reset all shifts and call callback, regardless of whether toPayload exists. + resetShifts(); + if (totalDragEnd) { + onItemDragEnd?.({ + ...eventData, + toIndex, + toItem, + cancelled: (0, types_1.isWithCancelledFlag)(eventData) ? eventData.cancelled : false, + index: fromIndex, + item: data?.[fromOriginalIndex], + }); + } + // Reset currently dragged over position index to undefined. + if (draggedToIndex.current !== undefined) { + if (!totalDragEnd) { + onItemDragPositionChange?.({ + ...eventData, + index: fromIndex, + item: data?.[fromOriginalIndex], + toIndex: undefined, + previousIndex: draggedToIndex.current, + }); + } + draggedToIndex.current = undefined; + } + if (toPayload !== undefined) { + // If dragged item and received item were ours, reorder data. + // console.log(`moving ${fromPayload.index} -> ${toPayload.index}`); + const snapbackTarget = calculateSnapbackTarget(fromPayload, toPayload); + if (data) { + const newOriginalIndexes = originalIndexes.slice(); + newOriginalIndexes.splice(toIndex, 0, newOriginalIndexes.splice(fromIndex, 1)[0]); + setOriginalIndexes(newOriginalIndexes); + onItemReorder?.({ + fromIndex, + fromItem: data[fromOriginalIndex], + toIndex: toIndex, + toItem: data[toOriginalIndex], + }); + } + return snapbackTarget; + } + } + return undefined; + }, [ + id, + data, + stopScroll, + reorderable, + resetShifts, + calculateSnapbackTarget, + originalIndexes, + onItemDragEnd, + onItemDragPositionChange, + onItemReorder, + ]); + // Monitor drag starts to handle callbacks. + const onMonitorDragStart = (0, react_1.useCallback)((eventData) => { + const { dragged } = eventData; + // First, check if we need to do anything. + if (reorderable && dragged.parentId === id) { + // One of our list items is starting to be dragged. + const { index, originalIndex } = dragged.payload; + setDraggedItem(originalIndex); + onItemDragStart?.({ + ...eventData, + index, + item: data?.[originalIndex], + }); + } + }, [ + id, + reorderable, + data, + setDraggedItem, + onItemDragStart, + ]); + // Monitor drags to react with item shifts and auto-scrolling. + const onMonitorDragOver = (0, react_1.useCallback)((eventData) => { + const { dragged, receiver, monitorOffsetRatio } = eventData; + // First, check if we need to shift items. + if (reorderable && dragged.parentId === id) { + // One of our list items is being dragged. + const fromPayload = dragged.payload; + // Find its current position index in the list, if any. + const toPayload = receiver?.parentId === id + ? receiver.payload + : undefined; + // Check and update currently dragged over position index. + const toIndex = toPayload?.index; + if (toIndex !== draggedToIndex.current) { + onItemDragPositionChange?.({ + ...eventData, + toIndex, + index: fromPayload.index, + item: data?.[fromPayload.originalIndex], + previousIndex: draggedToIndex.current, + }); + draggedToIndex.current = toIndex; + } + // Update shift transforms for items in the list. + updateShifts(fromPayload, toPayload ?? fromPayload); + } + // Next, see if we need to auto-scroll. + const ratio = horizontal ? monitorOffsetRatio.x : monitorOffsetRatio.y; + if (ratio > 0.1 && ratio < 0.9) { + scrollStateRef.current = types_1.AutoScrollDirection.None; + stopScroll(); + } + else { + if (ratio >= 0.9) { + scrollStateRef.current = types_1.AutoScrollDirection.Forward; + } + else if (ratio <= 0.1) { + scrollStateRef.current = types_1.AutoScrollDirection.Back; + } + startScroll(); + } + }, [ + id, + reorderable, + data, + updateShifts, + horizontal, + stopScroll, + startScroll, + onItemDragPositionChange, + ]); + // Monitor drag exits to stop scrolling, update shifts, and update draggedToIndex. + const onMonitorDragExit = (0, react_1.useCallback)((eventData) => handleInternalDragEnd(eventData, false), [handleInternalDragEnd]); + /* + * Monitor drag ends to stop scrolling, update shifts, and possibly reorder. + * This addresses the Android case where if we drag a list item and auto-scroll + * too far, the drag gets cancelled. + */ + const onMonitorDragEnd = (0, react_1.useCallback)((eventData) => handleInternalDragEnd(eventData, true), [handleInternalDragEnd]); + // Monitor drag drops to stop scrolling, update shifts, and possibly reorder. + const onMonitorDragDrop = (0, react_1.useCallback)((eventData) => handleInternalDragEnd(eventData, true), [handleInternalDragEnd]); + return (react_1.default.createElement(DraxView_1.DraxView, { id: id, style: style, scrollPositionRef: scrollPositionRef, onMeasure: onMeasureContainer, onMonitorDragStart: onMonitorDragStart, onMonitorDragOver: onMonitorDragOver, onMonitorDragExit: onMonitorDragExit, onMonitorDragEnd: onMonitorDragEnd, onMonitorDragDrop: onMonitorDragDrop }, + react_1.default.createElement(DraxSubprovider_1.DraxSubprovider, { parent: { id, nodeHandleRef } }, + react_1.default.createElement(react_native_1.FlatList, { ...flatListProps, style: flatListStyle, ref: setFlatListRefs, renderItem: renderItem, onScroll: onScroll, onContentSizeChange: onContentSizeChange, data: reorderedData })))); +}; +exports.DraxList = (0, react_1.forwardRef)(DraxListUnforwarded); diff --git a/build/DraxProvider.d.ts b/build/DraxProvider.d.ts new file mode 100644 index 0000000..c03e7af --- /dev/null +++ b/build/DraxProvider.d.ts @@ -0,0 +1,4 @@ +/// +/// +import { DraxProviderProps } from './types'; +export declare const DraxProvider: ({ debug, style, children, }: DraxProviderProps) => JSX.Element; diff --git a/build/DraxProvider.js b/build/DraxProvider.js new file mode 100644 index 0000000..7b62fc2 --- /dev/null +++ b/build/DraxProvider.js @@ -0,0 +1,641 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DraxProvider = void 0; +const react_1 = __importStar(require("react")); +const react_native_1 = require("react-native"); +const react_native_gesture_handler_1 = require("react-native-gesture-handler"); +const hooks_1 = require("./hooks"); +const DraxContext_1 = require("./DraxContext"); +const types_1 = require("./types"); +const math_1 = require("./math"); +const DraxProvider = ({ debug = false, style = styles.provider, children, }) => { + const { getViewState, getTrackingStatus, dispatch, } = (0, hooks_1.useDraxState)(); + const { getAbsoluteViewData, getTrackingDragged, getTrackingReceiver, getTrackingMonitorIds, getTrackingMonitors, getDragPositionData, findMonitorsAndReceiver, getHoverItems, registerView, updateViewProtocol, updateViewMeasurements, resetReceiver, resetDrag, startDrag, updateDragPosition, updateReceiver, setMonitorIds, unregisterView, } = (0, hooks_1.useDraxRegistry)(dispatch); + const rootNodeHandleRef = (0, react_1.useRef)(null); + const handleGestureStateChange = (0, react_1.useCallback)((id, event) => { + if (debug) { + console.log(`handleGestureStateChange(${id}, ${JSON.stringify(event, null, 2)})`); + } + // Get info on the currently dragged view, if any. + const dragged = getTrackingDragged(); + /* + * Case 1: We're already dragging a different view. + * Case 2: This view can't be found/measured. + * Case 3: This is the view we're already dragging. + * Case 3a: The drag is not ending. + * Case 3b: The drag is ending. + * Case 4: We're not already dragging a view. + * Case 4a: This view is not draggable. + * Case 4b: No drag is starting. + * Case 4c: A drag is starting. + */ + if (dragged && dragged.id !== id) { + // Case 1: We're already dragging a different view. + if (debug) { + console.log(`Ignoring gesture state change because another view is being dragged: ${dragged.id}`); + } + return; + } + const draggedData = dragged?.data ?? getAbsoluteViewData(id); + if (!draggedData) { + // Case 2: This view can't be found/measured. + if (dragged?.id === id) { + if (debug) { + console.log(`Data for currently dragged view id ${id} could not be found`); + // TODO: reset drag and notify monitors + } + } + else if (debug) { + console.log(`Ignoring gesture for view id ${id} because view data was not found`); + } + return; + } + /* + * Documentation on gesture handler state flow used in switches below: + * https://github.com/kmagiera/react-native-gesture-handler/blob/master/docs/state.md + */ + const { state: gestureState, // Used in switch logic below; see block comment above. + x: grabX, // x position of touch relative to dragged view + y: grabY, // y position of touch relative to dragged view + absoluteX: parentX, // x position of touch relative to parent of dragged view + absoluteY: parentY, // y position of touch relative to parent of dragged view + } = event; + /** Position of touch relative to parent of dragged view */ + const dragParentPosition = { x: parentX, y: parentY }; + const { x: absoluteX, // absolute x position of dragged view within DraxProvider + y: absoluteY, // absolute y position of dragged view within DraxProvider + width, // width of dragged view + height, // height of dragged view + } = draggedData.absoluteMeasurements; + if (dragged) { + // Case 3: This is the view we're already dragging. + let endDrag = false; + let cancelled = false; + let shouldDrop = false; + switch (gestureState) { + case react_native_gesture_handler_1.State.BEGAN: + // This should never happen, but we'll do nothing. + if (debug) { + console.log(`Received unexpected BEGAN event for dragged view id ${id}`); + } + break; + case react_native_gesture_handler_1.State.ACTIVE: + // This should also never happen, but we'll do nothing. + if (debug) { + console.log(`Received unexpected ACTIVE event for dragged view id ${id}`); + } + break; + case react_native_gesture_handler_1.State.CANCELLED: + // The gesture handler system has cancelled, so end the drag without dropping. + if (debug) { + console.log(`Stop dragging view id ${id} (CANCELLED)`); + } + endDrag = true; + cancelled = true; + break; + case react_native_gesture_handler_1.State.FAILED: + // This should never happen, but let's end the drag without dropping. + if (debug) { + console.log(`Received unexpected FAILED event for dragged view id ${id}`); + } + endDrag = true; + cancelled = true; + break; + case react_native_gesture_handler_1.State.END: + // User has ended the gesture, so end the drag, dropping into receiver if applicable. + if (debug) { + console.log(`Stop dragging view id ${id} (END)`); + } + endDrag = true; + shouldDrop = true; + break; + default: + if (debug) { + console.warn(`Unrecognized gesture state ${gestureState} for dragged view`); + } + break; + } + if (!endDrag) { + // Case 3a: The drag is not ending. + return; + } + // Case 3b: The drag is ending. + // Get the absolute position data for the drag touch. + const dragPositionData = getDragPositionData({ + parentPosition: dragParentPosition, + draggedMeasurements: draggedData.absoluteMeasurements, + lockXPosition: draggedData.protocol.lockDragXPosition, + lockYPosition: draggedData.protocol.lockDragYPosition, + }); + if (!dragPositionData) { + // Failed to get absolute position of drag. This should never happen. + return; + } + const { dragAbsolutePosition, dragTranslation, dragTranslationRatio, } = dragPositionData; + // Prepare event data for dragged view. + const eventDataDragged = { + id, + dragTranslationRatio, + parentId: draggedData.parentId, + payload: draggedData.protocol.dragPayload, + dragOffset: dragged.tracking.dragOffset, + grabOffset: dragged.tracking.grabOffset, + grabOffsetRatio: dragged.tracking.grabOffsetRatio, + hoverPosition: dragged.tracking.hoverPosition, + }; + // Get data for receiver view (if any) before we reset. + const receiver = getTrackingReceiver(); + // Get the monitors (if any) before we reset. + const monitors = getTrackingMonitors(); + // Snapback target, which may be modified by responses from protocols. + let snapbackTarget = types_1.DraxSnapbackTargetPreset.Default; + if (receiver && shouldDrop) { + // It's a successful drop into a receiver, let them both know, and check for response. + let responded = false; + // Prepare event data for receiver view. + const eventDataReceiver = { + id: receiver.id, + parentId: receiver.data.parentId, + payload: receiver.data.protocol.receiverPayload, + receiveOffset: receiver.tracking.receiveOffset, + receiveOffsetRatio: receiver.tracking.receiveOffsetRatio, + }; + const eventData = { + dragAbsolutePosition, + dragTranslation, + dragged: eventDataDragged, + receiver: eventDataReceiver, + }; + let response = draggedData.protocol.onDragDrop?.(eventData); + if (response !== undefined) { + snapbackTarget = response; + responded = true; + } + response = receiver.data.protocol.onReceiveDragDrop?.(eventData); + if (!responded && response !== undefined) { + snapbackTarget = response; + responded = true; + } + // And let any active monitors know too. + if (monitors.length > 0) { + monitors.forEach(({ data: monitorData }) => { + if (monitorData) { + const { relativePosition: monitorOffset, relativePositionRatio: monitorOffsetRatio, } = (0, math_1.getRelativePosition)(dragAbsolutePosition, monitorData.absoluteMeasurements); + response = monitorData.protocol.onMonitorDragDrop?.({ + ...eventData, + monitorOffset, + monitorOffsetRatio, + }); + } + if (!responded && response !== undefined) { + snapbackTarget = response; + responded = true; + } + }); + } + } + else { + // There is no receiver, or the drag was cancelled. + // Prepare common event data. + const eventData = { + dragAbsolutePosition, + dragTranslation, + cancelled, + dragged: eventDataDragged, + }; + // Let the dragged item know the drag ended, and capture any response. + let responded = false; + let response = draggedData.protocol.onDragEnd?.(eventData); + if (response !== undefined) { + snapbackTarget = response; + responded = true; + } + // Prepare receiver event data, or undefined if no receiver. + const eventReceiverData = receiver && { + id: receiver.id, + parentId: receiver.data.parentId, + payload: receiver.data.protocol.receiverPayload, + receiveOffset: receiver.tracking.receiveOffset, + receiveOffsetRatio: receiver.tracking.receiveOffsetRatio, + }; + // If there is a receiver but drag was cancelled, let it know the drag exited it. + receiver?.data.protocol.onReceiveDragExit?.({ + ...eventData, + receiver: eventReceiverData, + }); + // And let any active monitors know too. + if (monitors.length > 0) { + const monitorEventData = { + ...eventData, + receiver: eventReceiverData, + }; + monitors.forEach(({ data: monitorData }) => { + const { relativePosition: monitorOffset, relativePositionRatio: monitorOffsetRatio, } = (0, math_1.getRelativePosition)(dragAbsolutePosition, monitorData.absoluteMeasurements); + response = monitorData.protocol.onMonitorDragEnd?.({ + ...monitorEventData, + monitorOffset, + monitorOffsetRatio, + cancelled, + }); + if (!responded && response !== undefined) { + snapbackTarget = response; + responded = true; + } + }); + } + } + // Reset the drag. + resetDrag(snapbackTarget); + return; + } + // Case 4: We're not already dragging a view. + if (!draggedData.protocol.draggable) { + // Case 4a: This view is not draggable. + return; + } + let shouldStartDrag = false; + switch (gestureState) { + case react_native_gesture_handler_1.State.ACTIVE: + shouldStartDrag = true; + break; + case react_native_gesture_handler_1.State.BEGAN: + // Do nothing until the gesture becomes active. + break; + case react_native_gesture_handler_1.State.CANCELLED: + case react_native_gesture_handler_1.State.FAILED: + case react_native_gesture_handler_1.State.END: + // Do nothing because we weren't tracking this gesture. + break; + default: + if (debug) { + console.warn(`Unrecognized gesture state ${gestureState} for non-dragged view id ${id}`); + } + break; + } + if (!shouldStartDrag) { + // Case 4b: No drag is starting. + return; + } + // Case 4c: A drag is starting. + /* + * First, verify that the touch is still within the dragged view. + * Because we are using a LongPressGestureHandler with unlimited + * distance to handle the drag, it could be out of bounds before + * it even starts. (For some reason, LongPressGestureHandler does + * not provide us with a BEGAN state change event in iOS.) + */ + if (grabX >= 0 && grabY >= 0 && grabX < width && grabY < height) { + /* + * To determine drag start position in absolute coordinates, we add: + * absolute coordinates of dragged view + * + relative coordinates of touch within view + * + * NOTE: if view is transformed, these will be wrong. + */ + const dragAbsolutePosition = { + x: absoluteX + grabX, + y: absoluteY + grabY, + }; + const grabOffset = { x: grabX, y: grabY }; + const grabOffsetRatio = { + x: grabX / width, + y: grabY / height, + }; + const { dragOffset, dragTranslation, dragTranslationRatio, hoverPosition, } = startDrag({ + grabOffset, + grabOffsetRatio, + dragAbsolutePosition, + dragParentPosition, + draggedId: id, + }); + if (debug) { + console.log(`Start dragging view id ${id} at absolute position (${dragAbsolutePosition.x}, ${dragAbsolutePosition.y})`); + } + const eventData = { + dragAbsolutePosition, + dragTranslation, + dragged: { + id, + dragOffset, + grabOffset, + grabOffsetRatio, + hoverPosition, + dragTranslationRatio, + parentId: draggedData.parentId, + payload: draggedData.protocol.dragPayload, + }, + }; + draggedData.protocol.onDragStart?.(eventData); + // Find which monitors and receiver this drag is over. + const { monitors } = findMonitorsAndReceiver(dragAbsolutePosition, id); + // Notify monitors and update monitor tracking. + if (monitors.length > 0) { + const newMonitorIds = monitors.map(({ id: monitorId, data: monitorData, relativePosition: monitorOffset, relativePositionRatio: monitorOffsetRatio, }) => { + const monitorEventData = { + ...eventData, + monitorOffset, + monitorOffsetRatio, + }; + monitorData.protocol.onMonitorDragStart?.(monitorEventData); + return monitorId; + }); + setMonitorIds(newMonitorIds); + } + } + }, [ + getAbsoluteViewData, + getDragPositionData, + getTrackingDragged, + getTrackingReceiver, + getTrackingMonitors, + resetDrag, + startDrag, + findMonitorsAndReceiver, + setMonitorIds, + debug, + ]); + const handleGestureEvent = (0, react_1.useCallback)((id, event) => { + if (debug) { + console.log(`handleGestureEvent(${id}, ${JSON.stringify(event, null, 2)})`); + } + const dragged = getTrackingDragged(); + if (dragged === undefined) { + // We're not tracking any gesture yet. + if (debug) { + console.log('Ignoring gesture event because we have not initialized a drag'); + } + return; + } + if (dragged.id !== id) { + // This is not a gesture we're tracking. We don't support multiple simultaneous drags. + if (debug) { + console.log('Ignoring gesture event because this is not the view being dragged'); + } + return; + } + const { absoluteX: parentX, // x position of touch relative to parent of dragged view + absoluteY: parentY, // y position of touch relative to parent of dragged view + } = event; + if (debug) { + console.log(`Dragged item absolute coordinates (${dragged.data.absoluteMeasurements.x}, ${dragged.data.absoluteMeasurements.y})`); + console.log(`Native event in-view touch coordinates: (${event.x}, ${event.y})`); + } + /** Position of touch relative to parent of dragged view */ + const parentPosition = { x: parentX, y: parentY }; + // Get the absolute position data for the drag touch. + const dragPositionData = getDragPositionData({ + parentPosition, + draggedMeasurements: dragged.data.absoluteMeasurements, + lockXPosition: dragged.data.protocol.lockDragXPosition, + lockYPosition: dragged.data.protocol.lockDragYPosition, + }); + if (!dragPositionData) { + // Failed to get drag position data. This should never happen. + return; + } + const { dragAbsolutePosition, dragTranslation, dragTranslationRatio, } = dragPositionData; + if (debug) { + console.log(`Drag at absolute coordinates (${dragAbsolutePosition.x}, ${dragAbsolutePosition.y})\n`); + console.log(`Drag translation (${dragTranslation.x}, ${dragTranslation.y})`); + console.log(`Drag translation ratio (${dragTranslationRatio.x}, ${dragTranslationRatio.y})`); + } + // Find which monitors and receiver this drag is over. + const { monitors, receiver } = findMonitorsAndReceiver(dragAbsolutePosition, dragged.id); + // Get the previous receiver, if any. + const oldReceiver = getTrackingReceiver(); + // Always update the drag position. + updateDragPosition(dragAbsolutePosition); + const draggedProtocol = dragged.data.protocol; + // Prepare event data for dragged view. + const eventDataDragged = { + dragTranslationRatio, + id: dragged.id, + parentId: dragged.data.parentId, + payload: dragged.data.protocol.dragPayload, + dragOffset: dragged.tracking.dragOffset, + grabOffset: dragged.tracking.grabOffset, + grabOffsetRatio: dragged.tracking.grabOffsetRatio, + hoverPosition: dragged.tracking.hoverPosition, + }; + // Prepare base drag event data. + const dragEventData = { + dragAbsolutePosition, + dragTranslation, + dragged: eventDataDragged, + }; + // Prepare event data stub for monitor updates later so we can optionally add receiver. + const monitorEventDataStub = { + ...dragEventData, + }; + /* + * Consider the following cases for new and old receiver ids: + * Case 1: new exists, old exists, new is the same as old + * Case 2: new exists, old exists, new is different from old + * Case 3: new exists, old does not exist + * Case 4: new does not exist, old exists + * Case 5: new does not exist, old does not exist + */ + if (receiver) { + // New receiver exists. + const receiverProtocol = receiver.data.protocol; + // Update the receiver. + const trackingReceiver = updateReceiver(receiver, dragged); + if (trackingReceiver === undefined) { + // This should never happen, but just in case. + if (debug) { + console.log('Failed to update tracking receiver'); + } + return; + } + // Prepare event data for receiver view. + const eventDataReceiver = { + id: receiver.id, + parentId: receiver.data.parentId, + payload: receiver.data.protocol.receiverPayload, + receiveOffset: trackingReceiver.receiveOffset, + receiveOffsetRatio: trackingReceiver.receiveOffsetRatio, + }; + // Add receiver data to monitor event stub. + monitorEventDataStub.receiver = eventDataReceiver; + // Prepare event data for callbacks. + const eventData = { + ...dragEventData, + receiver: eventDataReceiver, + }; + if (oldReceiver) { + if (receiver.id === oldReceiver.id) { + // Case 1: new exists, old exists, new is the same as old + // Call the protocol event callbacks for dragging over the receiver. + draggedProtocol.onDragOver?.(eventData); + receiverProtocol.onReceiveDragOver?.(eventData); + } + else { + // Case 2: new exists, old exists, new is different from old + // Prepare event data with old receiver. + const eventDataOldReceiver = { + ...dragEventData, + receiver: { + id: oldReceiver.id, + parentId: oldReceiver.data.parentId, + payload: oldReceiver.data.protocol.receiverPayload, + receiveOffset: oldReceiver.tracking.receiveOffset, + receiveOffsetRatio: oldReceiver.tracking.receiveOffsetRatio, + }, + }; + // Call the protocol event callbacks for exiting the old receiver... + draggedProtocol.onDragExit?.(eventDataOldReceiver); + oldReceiver.data.protocol.onReceiveDragExit?.({ + ...eventDataOldReceiver, + cancelled: false, + }); + // ...and entering the new receiver. + draggedProtocol.onDragEnter?.(eventData); + receiverProtocol.onReceiveDragEnter?.(eventData); + } + } + else { + // Case 3: new exists, old does not exist + // Call the protocol event callbacks for entering the new receiver. + draggedProtocol.onDragEnter?.(eventData); + receiverProtocol.onReceiveDragEnter?.(eventData); + } + } + else if (oldReceiver) { + // Case 4: new does not exist, old exists + // Reset the old receiver. + resetReceiver(); + // Prepare event data with old receiver. + const eventData = { + ...dragEventData, + receiver: { + id: oldReceiver.id, + parentId: oldReceiver.data.parentId, + payload: oldReceiver.data.protocol.receiverPayload, + receiveOffset: oldReceiver.tracking.receiveOffset, + receiveOffsetRatio: oldReceiver.tracking.receiveOffsetRatio, + }, + }; + // Call the protocol event callbacks for exiting the old receiver. + draggedProtocol.onDragExit?.(eventData); + oldReceiver.data.protocol.onReceiveDragExit?.({ + ...eventData, + cancelled: false, + }); + } + else { + // Case 5: new does not exist, old does not exist + // Call the protocol event callback for dragging. + draggedProtocol.onDrag?.(dragEventData); + } + // Notify monitors and update monitor tracking, if necessary. + const prevMonitorIds = getTrackingMonitorIds(); + if (monitors.length > 0 || prevMonitorIds.length > 0) { + const newMonitorIds = monitors.map(({ id: monitorId, data: monitorData, relativePosition: monitorOffset, relativePositionRatio: monitorOffsetRatio, }) => { + const monitorEventData = { + ...monitorEventDataStub, + monitorOffset, + monitorOffsetRatio, + }; + if (prevMonitorIds.includes(monitorId)) { + // Drag was already over this monitor. + monitorData.protocol.onMonitorDragOver?.(monitorEventData); + } + else { + // Drag is entering monitor. + monitorData.protocol.onMonitorDragEnter?.(monitorEventData); + } + return monitorId; + }); + prevMonitorIds.filter((monitorId) => !newMonitorIds.includes(monitorId)) + .forEach((monitorId) => { + // Drag has exited monitor. + const monitorData = getAbsoluteViewData(monitorId); + if (monitorData) { + const { relativePosition: monitorOffset, relativePositionRatio: monitorOffsetRatio, } = (0, math_1.getRelativePosition)(dragAbsolutePosition, monitorData.absoluteMeasurements); + monitorData.protocol.onMonitorDragExit?.({ + ...monitorEventDataStub, + monitorOffset, + monitorOffsetRatio, + }); + } + }); + setMonitorIds(newMonitorIds); + } + }, [ + getAbsoluteViewData, + getDragPositionData, + getTrackingDragged, + getTrackingReceiver, + getTrackingMonitorIds, + findMonitorsAndReceiver, + resetReceiver, + updateDragPosition, + updateReceiver, + setMonitorIds, + debug, + ]); + const contextValue = { + getViewState, + getTrackingStatus, + registerView, + unregisterView, + updateViewProtocol, + updateViewMeasurements, + handleGestureStateChange, + handleGestureEvent, + rootNodeHandleRef, + }; + const hoverViews = []; + const trackingStatus = getTrackingStatus(); + getHoverItems().forEach(({ id, key, internalRenderHoverView, hoverPosition, dimensions, }) => { + const viewState = getViewState(id); + if (viewState) { + const hoverView = internalRenderHoverView({ + key, + hoverPosition, + viewState, + trackingStatus, + dimensions, + }); + if (hoverView) { + hoverViews.push(hoverView); + } + } + }); + const setRootNodeHandleRef = (0, react_1.useCallback)((ref) => { + rootNodeHandleRef.current = ref && (0, react_native_1.findNodeHandle)(ref); + }, []); + return (react_1.default.createElement(DraxContext_1.DraxContext.Provider, { value: contextValue }, + react_1.default.createElement(react_native_1.View, { style: style, ref: setRootNodeHandleRef }, + children, + react_1.default.createElement(react_native_1.View, { style: react_native_1.StyleSheet.absoluteFill, pointerEvents: "none" }, hoverViews)))); +}; +exports.DraxProvider = DraxProvider; +const styles = react_native_1.StyleSheet.create({ + provider: { + flex: 1, + }, +}); diff --git a/build/DraxScrollView.d.ts b/build/DraxScrollView.d.ts new file mode 100644 index 0000000..bf6f087 --- /dev/null +++ b/build/DraxScrollView.d.ts @@ -0,0 +1,6 @@ +import React from 'react'; +import { ScrollView } from 'react-native'; +import { DraxScrollViewProps } from './types'; +export declare const DraxScrollView: React.ForwardRefExoticComponent>; diff --git a/build/DraxScrollView.js b/build/DraxScrollView.js new file mode 100644 index 0000000..c7847ab --- /dev/null +++ b/build/DraxScrollView.js @@ -0,0 +1,197 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DraxScrollView = void 0; +const react_1 = __importStar(require("react")); +const react_native_1 = require("react-native"); +const DraxView_1 = require("./DraxView"); +const DraxSubprovider_1 = require("./DraxSubprovider"); +const hooks_1 = require("./hooks"); +const types_1 = require("./types"); +const params_1 = require("./params"); +const DraxScrollViewUnforwarded = (props, forwardedRef) => { + const { children, style, onScroll: onScrollProp, onContentSizeChange: onContentSizeChangeProp, scrollEventThrottle = params_1.defaultScrollEventThrottle, autoScrollIntervalLength = params_1.defaultAutoScrollIntervalLength, autoScrollJumpRatio = params_1.defaultAutoScrollJumpRatio, autoScrollBackThreshold = params_1.defaultAutoScrollBackThreshold, autoScrollForwardThreshold = params_1.defaultAutoScrollForwardThreshold, id: idProp, ...scrollViewProps } = props; + // The unique identifer for this view. + const id = (0, hooks_1.useDraxId)(idProp); + // Scrollable view, used for scrolling. + const scrollRef = (0, react_1.useRef)(null); + // ScrollView node handle, used for measuring children. + const nodeHandleRef = (0, react_1.useRef)(null); + // Container view measurements, for scrolling by percentage. + const containerMeasurementsRef = (0, react_1.useRef)(undefined); + // Content size, for scrolling by percentage. + const contentSizeRef = (0, react_1.useRef)(undefined); + // Scroll position, for Drax bounds checking and auto-scrolling. + const scrollPositionRef = (0, react_1.useRef)({ x: 0, y: 0 }); + // Auto-scroll state. + const autoScrollStateRef = (0, react_1.useRef)({ + x: types_1.AutoScrollDirection.None, + y: types_1.AutoScrollDirection.None, + }); + // Auto-scroll interval. + const autoScrollIntervalRef = (0, react_1.useRef)(undefined); + // Handle auto-scrolling on interval. + const doScroll = (0, react_1.useCallback)(() => { + const scroll = scrollRef.current; + const containerMeasurements = containerMeasurementsRef.current; + const contentSize = contentSizeRef.current; + if (!scroll || !containerMeasurements || !contentSize) { + return; + } + const scrollPosition = scrollPositionRef.current; + const autoScrollState = autoScrollStateRef.current; + const jump = { + x: containerMeasurements.width * autoScrollJumpRatio, + y: containerMeasurements.height * autoScrollJumpRatio, + }; + let xNew; + let yNew; + if (autoScrollState.x === types_1.AutoScrollDirection.Forward) { + const xMax = contentSize.x - containerMeasurements.width; + if (scrollPosition.x < xMax) { + xNew = Math.min(scrollPosition.x + jump.x, xMax); + } + } + else if (autoScrollState.x === types_1.AutoScrollDirection.Back) { + if (scrollPosition.x > 0) { + xNew = Math.max(scrollPosition.x - jump.x, 0); + } + } + if (autoScrollState.y === types_1.AutoScrollDirection.Forward) { + const yMax = contentSize.y - containerMeasurements.height; + if (scrollPosition.y < yMax) { + yNew = Math.min(scrollPosition.y + jump.y, yMax); + } + } + else if (autoScrollState.y === types_1.AutoScrollDirection.Back) { + if (scrollPosition.y > 0) { + yNew = Math.max(scrollPosition.y - jump.y, 0); + } + } + if (xNew !== undefined || yNew !== undefined) { + scroll.scrollTo({ + x: xNew ?? scrollPosition.x, + y: yNew ?? scrollPosition.y, + }); + scroll.flashScrollIndicators(); // ScrollView typing is missing this method + } + }, [autoScrollJumpRatio]); + // Start the auto-scrolling interval. + const startScroll = (0, react_1.useCallback)(() => { + if (autoScrollIntervalRef.current) { + return; + } + doScroll(); + autoScrollIntervalRef.current = setInterval(doScroll, autoScrollIntervalLength); + }, [doScroll, autoScrollIntervalLength]); + // Stop the auto-scrolling interval. + const stopScroll = (0, react_1.useCallback)(() => { + if (autoScrollIntervalRef.current) { + clearInterval(autoScrollIntervalRef.current); + autoScrollIntervalRef.current = undefined; + } + }, []); + // If startScroll changes, refresh our interval. + (0, react_1.useEffect)(() => { + if (autoScrollIntervalRef.current) { + stopScroll(); + startScroll(); + } + }, [stopScroll, startScroll]); + // Clear auto-scroll direction and stop the auto-scrolling interval. + const resetScroll = (0, react_1.useCallback)(() => { + const autoScrollState = autoScrollStateRef.current; + autoScrollState.x = types_1.AutoScrollDirection.None; + autoScrollState.y = types_1.AutoScrollDirection.None; + stopScroll(); + }, [stopScroll]); + // Track the size of the container view. + const onMeasureContainer = (0, react_1.useCallback)((measurements) => { + containerMeasurementsRef.current = measurements; + }, []); + // Monitor drag-over events to react with auto-scrolling. + const onMonitorDragOver = (0, react_1.useCallback)((event) => { + const { monitorOffsetRatio } = event; + const autoScrollState = autoScrollStateRef.current; + if (monitorOffsetRatio.x >= autoScrollForwardThreshold) { + autoScrollState.x = types_1.AutoScrollDirection.Forward; + } + else if (monitorOffsetRatio.x <= autoScrollBackThreshold) { + autoScrollState.x = types_1.AutoScrollDirection.Back; + } + else { + autoScrollState.x = types_1.AutoScrollDirection.None; + } + if (monitorOffsetRatio.y >= autoScrollForwardThreshold) { + autoScrollState.y = types_1.AutoScrollDirection.Forward; + } + else if (monitorOffsetRatio.y <= autoScrollBackThreshold) { + autoScrollState.y = types_1.AutoScrollDirection.Back; + } + else { + autoScrollState.y = types_1.AutoScrollDirection.None; + } + if (autoScrollState.x === types_1.AutoScrollDirection.None && autoScrollState.y === types_1.AutoScrollDirection.None) { + stopScroll(); + } + else { + startScroll(); + } + }, [ + stopScroll, + startScroll, + autoScrollBackThreshold, + autoScrollForwardThreshold, + ]); + // Set the ScrollView and node handle refs. + const setScrollViewRefs = (0, react_1.useCallback)((ref) => { + scrollRef.current = ref; + nodeHandleRef.current = ref && (0, react_native_1.findNodeHandle)(ref); + if (forwardedRef) { + if (typeof forwardedRef === 'function') { + forwardedRef(ref); + } + else { + // eslint-disable-next-line no-param-reassign + forwardedRef.current = ref; + } + } + }, [forwardedRef]); + // Track content size. + const onContentSizeChange = (0, react_1.useCallback)((width, height) => { + contentSizeRef.current = { x: width, y: height }; + return onContentSizeChangeProp?.(width, height); + }, [onContentSizeChangeProp]); + // Update tracked scroll position when list is scrolled. + const onScroll = (0, react_1.useCallback)((event) => { + const { nativeEvent: { contentOffset } } = event; + scrollPositionRef.current = { ...contentOffset }; + return onScrollProp?.(event); + }, [onScrollProp]); + return id ? (react_1.default.createElement(DraxView_1.DraxView, { id: id, style: style, scrollPositionRef: scrollPositionRef, onMeasure: onMeasureContainer, onMonitorDragOver: onMonitorDragOver, onMonitorDragExit: resetScroll, onMonitorDragEnd: resetScroll, onMonitorDragDrop: resetScroll }, + react_1.default.createElement(DraxSubprovider_1.DraxSubprovider, { parent: { id, nodeHandleRef } }, + react_1.default.createElement(react_native_1.ScrollView, { ...scrollViewProps, ref: setScrollViewRefs, onContentSizeChange: onContentSizeChange, onScroll: onScroll, scrollEventThrottle: scrollEventThrottle }, children)))) : null; +}; +exports.DraxScrollView = (0, react_1.forwardRef)(DraxScrollViewUnforwarded); diff --git a/build/DraxSubprovider.d.ts b/build/DraxSubprovider.d.ts new file mode 100644 index 0000000..93ca221 --- /dev/null +++ b/build/DraxSubprovider.d.ts @@ -0,0 +1,3 @@ +import { FunctionComponent } from 'react'; +import { DraxSubproviderProps } from './types'; +export declare const DraxSubprovider: FunctionComponent; diff --git a/build/DraxSubprovider.js b/build/DraxSubprovider.js new file mode 100644 index 0000000..b9fa06d --- /dev/null +++ b/build/DraxSubprovider.js @@ -0,0 +1,18 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DraxSubprovider = void 0; +const react_1 = __importDefault(require("react")); +const DraxContext_1 = require("./DraxContext"); +const hooks_1 = require("./hooks"); +const DraxSubprovider = ({ parent, children }) => { + const contextValue = (0, hooks_1.useDraxContext)(); + const subContextValue = { + ...contextValue, + parent, + }; + return (react_1.default.createElement(DraxContext_1.DraxContext.Provider, { value: subContextValue }, children)); +}; +exports.DraxSubprovider = DraxSubprovider; diff --git a/build/DraxView.d.ts b/build/DraxView.d.ts new file mode 100644 index 0000000..9622efd --- /dev/null +++ b/build/DraxView.d.ts @@ -0,0 +1,3 @@ +import { PropsWithChildren } from 'react'; +import { DraxViewProps } from './types'; +export declare const DraxView: ({ onDragStart, onDrag, onDragEnter, onDragOver, onDragExit, onDragEnd, onDragDrop, onSnapbackEnd, onReceiveDragEnter, onReceiveDragOver, onReceiveDragExit, onReceiveDragDrop, onMonitorDragStart, onMonitorDragEnter, onMonitorDragOver, onMonitorDragExit, onMonitorDragEnd, onMonitorDragDrop, animateSnapback, snapbackDelay, snapbackDuration, snapbackAnimator, payload, dragPayload, receiverPayload, style, dragInactiveStyle, draggingStyle, draggingWithReceiverStyle, draggingWithoutReceiverStyle, dragReleasedStyle, hoverStyle, hoverDraggingStyle, hoverDraggingWithReceiverStyle, hoverDraggingWithoutReceiverStyle, hoverDragReleasedStyle, receiverInactiveStyle, receivingStyle, otherDraggingStyle, otherDraggingWithReceiverStyle, otherDraggingWithoutReceiverStyle, renderContent, renderHoverContent, registration, onMeasure, scrollPositionRef, lockDragXPosition, lockDragYPosition, children, noHover, isParent, longPressDelay, id: idProp, parent: parentProp, draggable: draggableProp, receptive: receptiveProp, monitoring: monitoringProp, ...props }: PropsWithChildren) => JSX.Element; diff --git a/build/DraxView.js b/build/DraxView.js new file mode 100644 index 0000000..07bc812 --- /dev/null +++ b/build/DraxView.js @@ -0,0 +1,416 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DraxView = void 0; +const react_1 = __importStar(require("react")); +const react_native_1 = require("react-native"); +const react_native_gesture_handler_1 = require("react-native-gesture-handler"); +const lodash_throttle_1 = __importDefault(require("lodash.throttle")); +const hooks_1 = require("./hooks"); +const types_1 = require("./types"); +const params_1 = require("./params"); +const math_1 = require("./math"); +const DraxSubprovider_1 = require("./DraxSubprovider"); +const transform_1 = require("./transform"); +const DraxView = ({ onDragStart, onDrag, onDragEnter, onDragOver, onDragExit, onDragEnd, onDragDrop, onSnapbackEnd, onReceiveDragEnter, onReceiveDragOver, onReceiveDragExit, onReceiveDragDrop, onMonitorDragStart, onMonitorDragEnter, onMonitorDragOver, onMonitorDragExit, onMonitorDragEnd, onMonitorDragDrop, animateSnapback, snapbackDelay, snapbackDuration, snapbackAnimator, payload, dragPayload, receiverPayload, style, dragInactiveStyle, draggingStyle, draggingWithReceiverStyle, draggingWithoutReceiverStyle, dragReleasedStyle, hoverStyle, hoverDraggingStyle, hoverDraggingWithReceiverStyle, hoverDraggingWithoutReceiverStyle, hoverDragReleasedStyle, receiverInactiveStyle, receivingStyle, otherDraggingStyle, otherDraggingWithReceiverStyle, otherDraggingWithoutReceiverStyle, renderContent, renderHoverContent, registration, onMeasure, scrollPositionRef, lockDragXPosition, lockDragYPosition, children, noHover = false, isParent = false, longPressDelay = params_1.defaultLongPressDelay, id: idProp, parent: parentProp, draggable: draggableProp, receptive: receptiveProp, monitoring: monitoringProp, ...props }) => { + // Coalesce protocol props into capabilities. + const draggable = draggableProp ?? (dragPayload !== undefined + || payload !== undefined + || !!onDrag + || !!onDragEnd + || !!onDragEnter + || !!onDragExit + || !!onDragOver + || !!onDragStart + || !!onDragDrop); + const receptive = receptiveProp ?? (receiverPayload !== undefined + || payload !== undefined + || !!onReceiveDragEnter + || !!onReceiveDragExit + || !!onReceiveDragOver + || !!onReceiveDragDrop); + const monitoring = monitoringProp ?? (!!onMonitorDragStart + || !!onMonitorDragEnter + || !!onMonitorDragOver + || !!onMonitorDragExit + || !!onMonitorDragEnd + || !!onMonitorDragDrop); + // The unique identifier for this view. + const id = (0, hooks_1.useDraxId)(idProp); + // The underlying View, for measuring. + const viewRef = (0, react_1.useRef)(null); + // The underlying View node handle, used for subprovider nesting if this is a Drax parent view. + const nodeHandleRef = (0, react_1.useRef)(null); + // This view's measurements, for reference. + const measurementsRef = (0, react_1.useRef)(undefined); + // Connect with Drax. + const { getViewState, getTrackingStatus, registerView, unregisterView, updateViewProtocol, updateViewMeasurements, handleGestureEvent, handleGestureStateChange, rootNodeHandleRef, parent: contextParent, } = (0, hooks_1.useDraxContext)(); + // Identify Drax parent view (if any) from context or prop override. + const parent = parentProp ?? contextParent; + const parentId = parent?.id; + // Identify parent node handle ref. + const parentNodeHandleRef = parent ? parent.nodeHandleRef : rootNodeHandleRef; + // Register and unregister with Drax context when necessary. + (0, react_1.useEffect)(() => { + // Register with Drax context after we have an id. + registerView({ id, parentId, scrollPositionRef }); + // Unregister when we unmount or id changes. + return () => unregisterView({ id }); + }, [ + id, + parentId, + scrollPositionRef, + registerView, + unregisterView, + ]); + // Combine hover styles for given internal render props. + const getCombinedHoverStyle = (0, react_1.useCallback)(({ viewState: { dragStatus }, trackingStatus: { receiving: anyReceiving }, hoverPosition, dimensions, }) => { + // Start with base style, calculated dimensions, and hover base style. + const hoverStyles = [ + style, + dimensions, + hoverStyle, + ]; + // Apply style style overrides based on state. + if (dragStatus === types_1.DraxViewDragStatus.Dragging) { + hoverStyles.push(hoverDraggingStyle); + if (anyReceiving) { + hoverStyles.push(hoverDraggingWithReceiverStyle); + } + else { + hoverStyles.push(hoverDraggingWithoutReceiverStyle); + } + } + else if (dragStatus === types_1.DraxViewDragStatus.Released) { + hoverStyles.push(hoverDragReleasedStyle); + } + // Remove any layout styles. + const flattenedHoverStyle = (0, transform_1.flattenStylesWithoutLayout)(hoverStyles); + // Apply hover transform. + const transform = hoverPosition.getTranslateTransform(); + return (0, transform_1.mergeStyleTransform)(flattenedHoverStyle, transform); + }, [ + style, + hoverStyle, + hoverDraggingStyle, + hoverDraggingWithReceiverStyle, + hoverDraggingWithoutReceiverStyle, + hoverDragReleasedStyle, + ]); + // Internal render function for hover views, used in protocol by provider. + const internalRenderHoverView = (0, react_1.useMemo)(() => ((draggable && !noHover) + ? (internalProps) => { + let content; + const render = renderHoverContent ?? renderContent; + if (render) { + const renderProps = { + children, + hover: true, + viewState: internalProps.viewState, + trackingStatus: internalProps.trackingStatus, + dimensions: internalProps.dimensions, + }; + content = render(renderProps); + } + else { + content = children; + } + return (react_1.default.createElement(react_native_1.Animated.View, { ...props, key: internalProps.key, style: getCombinedHoverStyle(internalProps) }, content)); + } + : undefined), [ + draggable, + noHover, + renderHoverContent, + renderContent, + getCombinedHoverStyle, + props, + children, + ]); + // Report updates to our protocol callbacks when we have an id and whenever the props change. + (0, react_1.useEffect)(() => { + updateViewProtocol({ + id, + protocol: { + onDragStart, + onDrag, + onDragEnter, + onDragOver, + onDragExit, + onDragEnd, + onDragDrop, + onSnapbackEnd, + onReceiveDragEnter, + onReceiveDragOver, + onReceiveDragExit, + onReceiveDragDrop, + onMonitorDragStart, + onMonitorDragEnter, + onMonitorDragOver, + onMonitorDragExit, + onMonitorDragEnd, + onMonitorDragDrop, + animateSnapback, + snapbackDelay, + snapbackDuration, + snapbackAnimator, + internalRenderHoverView, + draggable, + receptive, + monitoring, + lockDragXPosition, + lockDragYPosition, + dragPayload: dragPayload ?? payload, + receiverPayload: receiverPayload ?? payload, + }, + }); + }, [ + id, + updateViewProtocol, + children, + onDragStart, + onDrag, + onDragEnter, + onDragOver, + onDragExit, + onDragEnd, + onDragDrop, + onSnapbackEnd, + onReceiveDragEnter, + onReceiveDragOver, + onReceiveDragExit, + onReceiveDragDrop, + onMonitorDragStart, + onMonitorDragEnter, + onMonitorDragOver, + onMonitorDragExit, + onMonitorDragEnd, + onMonitorDragDrop, + animateSnapback, + snapbackDelay, + snapbackDuration, + snapbackAnimator, + payload, + dragPayload, + receiverPayload, + draggable, + receptive, + monitoring, + lockDragXPosition, + lockDragYPosition, + internalRenderHoverView, + ]); + // Connect gesture state change handling into Drax context, tied to this id. + const onHandlerStateChange = (0, react_1.useCallback)(({ nativeEvent }) => handleGestureStateChange(id, nativeEvent), [id, handleGestureStateChange]); + // Create throttled gesture event handler, tied to this id. + const throttledHandleGestureEvent = (0, react_1.useMemo)(() => (0, lodash_throttle_1.default)((event) => { + // Pass the event up to the Drax context. + handleGestureEvent(id, event); + }, 10), [id, handleGestureEvent]); + // Connect gesture event handling into Drax context, extracting nativeEvent. + const onGestureEvent = (0, react_1.useCallback)(({ nativeEvent }) => throttledHandleGestureEvent(nativeEvent), [throttledHandleGestureEvent]); + // Build a callback which will report our measurements to Drax context, + // onMeasure, and an optional measurement handler. + const buildMeasureCallback = (0, react_1.useCallback)((measurementHandler) => ((x, y, width, height) => { + /* + * In certain cases (on Android), all of these values can be + * undefined when the view is not on screen; This should not + * happen with the measurement functions we're using, but just + * for the sake of paranoia, we'll check and use undefined + * for the entire measurements object. + */ + const measurements = (height === undefined + ? undefined + : { + height, + x: x, + y: y, + width: width, + }); + measurementsRef.current = measurements; + updateViewMeasurements({ id, measurements }); + onMeasure?.(measurements); + measurementHandler?.(measurements); + }), [id, updateViewMeasurements, onMeasure]); + // Callback which will report our measurements to Drax context and onMeasure. + const updateMeasurements = (0, react_1.useMemo)(() => buildMeasureCallback(), [buildMeasureCallback]); + // Measure and report our measurements to Drax context, onMeasure, and an + // optional measurement handler on demand. + const measureWithHandler = (0, react_1.useCallback)((measurementHandler) => { + const view = viewRef.current; + if (view) { + const nodeHandle = parentNodeHandleRef.current; + if (nodeHandle) { + const measureCallback = measurementHandler + ? buildMeasureCallback(measurementHandler) + : updateMeasurements; + // console.log('definitely measuring in reference to something'); + view.measureLayout(nodeHandle, measureCallback, () => { + // console.log('Failed to measure Drax view in relation to parent nodeHandle'); + }); + } + else { + // console.log('No parent nodeHandle to measure Drax view in relation to'); + } + } + else { + // console.log('No view to measure'); + } + }, [ + parentNodeHandleRef, + buildMeasureCallback, + updateMeasurements, + ]); + // Measure and send our measurements to Drax context and onMeasure, used when this view finishes layout. + const onLayout = (0, react_1.useCallback)(() => { + // console.log(`onLayout ${id}`); + measureWithHandler(); + }, [measureWithHandler]); + // Establish dimensions/orientation change handler when necessary. + (0, react_1.useEffect)(() => { + const handler = ( /* { screen: { width, height } }: { screen: ScaledSize } */) => { + // console.log(`Dimensions changed to ${width}/${height}`); + setTimeout(measureWithHandler, 100); + }; + const listener = react_native_1.Dimensions.addEventListener('change', handler); + return () => listener.remove(); + }, [measureWithHandler]); + // Register and unregister externally when necessary. + (0, react_1.useEffect)(() => { + if (registration) { // Register externally when registration is set. + registration({ + id, + measure: measureWithHandler, + }); + return () => registration(undefined); // Unregister when we unmount or registration changes. + } + return undefined; + }, [id, registration, measureWithHandler]); + // Get the render-related state for rendering. + const viewState = getViewState(id); + const trackingStatus = getTrackingStatus(); + // Get full render props for non-hovering view content. + const getRenderContentProps = (0, react_1.useCallback)(() => { + const measurements = measurementsRef.current; + const dimensions = measurements && (0, math_1.extractDimensions)(measurements); + return { + viewState, + trackingStatus, + children, + dimensions, + hover: false, + }; + }, [ + viewState, + trackingStatus, + children, + ]); + // Combined style for current render-related state. + const combinedStyle = (0, react_1.useMemo)(() => { + const { dragStatus = types_1.DraxViewDragStatus.Inactive, receiveStatus = types_1.DraxViewReceiveStatus.Inactive, } = viewState ?? {}; + const { dragging: anyDragging, receiving: anyReceiving, } = trackingStatus; + // Start with base style. + const styles = [style]; + // Apply style overrides for drag state. + if (dragStatus === types_1.DraxViewDragStatus.Dragging) { + styles.push(draggingStyle); + if (anyReceiving) { + styles.push(draggingWithReceiverStyle); + } + else { + styles.push(draggingWithoutReceiverStyle); + } + } + else if (dragStatus === types_1.DraxViewDragStatus.Released) { + styles.push(dragReleasedStyle); + } + else { + styles.push(dragInactiveStyle); + if (anyDragging) { + styles.push(otherDraggingStyle); + if (anyReceiving) { + styles.push(otherDraggingWithReceiverStyle); + } + else { + styles.push(otherDraggingWithoutReceiverStyle); + } + } + } + // Apply style overrides for receiving state. + if (receiveStatus === types_1.DraxViewReceiveStatus.Receiving) { + styles.push(receivingStyle); + } + else { + styles.push(receiverInactiveStyle); + } + return react_native_1.StyleSheet.flatten(styles); + }, [ + viewState, + trackingStatus, + style, + dragInactiveStyle, + draggingStyle, + draggingWithReceiverStyle, + draggingWithoutReceiverStyle, + dragReleasedStyle, + receivingStyle, + receiverInactiveStyle, + otherDraggingStyle, + otherDraggingWithReceiverStyle, + otherDraggingWithoutReceiverStyle, + ]); + // The rendered React children of this view. + const renderedChildren = (0, react_1.useMemo)(() => { + let content; + if (renderContent) { + const renderContentProps = getRenderContentProps(); + content = renderContent(renderContentProps); + } + else { + content = children; + } + if (isParent) { + // This is a Drax parent, so wrap children in subprovider. + content = (react_1.default.createElement(DraxSubprovider_1.DraxSubprovider, { parent: { id, nodeHandleRef } }, content)); + } + return content; + }, [ + renderContent, + getRenderContentProps, + children, + isParent, + id, + nodeHandleRef, + ]); + const setViewRefs = (0, react_1.useCallback)((ref) => { + viewRef.current = ref; + nodeHandleRef.current = ref && (0, react_native_1.findNodeHandle)(ref); + }, []); + return (react_1.default.createElement(react_native_gesture_handler_1.LongPressGestureHandler, { maxDist: Number.MAX_SAFE_INTEGER, shouldCancelWhenOutside: false, minDurationMs: longPressDelay, onHandlerStateChange: onHandlerStateChange, onGestureEvent: onGestureEvent /* Workaround incorrect typings. */, enabled: draggable }, + react_1.default.createElement(react_native_1.Animated.View, { ...props, style: combinedStyle, ref: setViewRefs, onLayout: onLayout, collapsable: false }, renderedChildren))); +}; +exports.DraxView = DraxView; diff --git a/build/hooks/index.d.ts b/build/hooks/index.d.ts new file mode 100644 index 0000000..fbdaaf9 --- /dev/null +++ b/build/hooks/index.d.ts @@ -0,0 +1,4 @@ +export { useDraxContext } from './useDraxContext'; +export { useDraxId } from './useDraxId'; +export { useDraxRegistry } from './useDraxRegistry'; +export { useDraxState } from './useDraxState'; diff --git a/build/hooks/index.js b/build/hooks/index.js new file mode 100644 index 0000000..c957546 --- /dev/null +++ b/build/hooks/index.js @@ -0,0 +1,11 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.useDraxState = exports.useDraxRegistry = exports.useDraxId = exports.useDraxContext = void 0; +var useDraxContext_1 = require("./useDraxContext"); +Object.defineProperty(exports, "useDraxContext", { enumerable: true, get: function () { return useDraxContext_1.useDraxContext; } }); +var useDraxId_1 = require("./useDraxId"); +Object.defineProperty(exports, "useDraxId", { enumerable: true, get: function () { return useDraxId_1.useDraxId; } }); +var useDraxRegistry_1 = require("./useDraxRegistry"); +Object.defineProperty(exports, "useDraxRegistry", { enumerable: true, get: function () { return useDraxRegistry_1.useDraxRegistry; } }); +var useDraxState_1 = require("./useDraxState"); +Object.defineProperty(exports, "useDraxState", { enumerable: true, get: function () { return useDraxState_1.useDraxState; } }); diff --git a/build/hooks/useDraxContext.d.ts b/build/hooks/useDraxContext.d.ts new file mode 100644 index 0000000..f03b263 --- /dev/null +++ b/build/hooks/useDraxContext.d.ts @@ -0,0 +1 @@ +export declare const useDraxContext: () => import("..").DraxContextValue; diff --git a/build/hooks/useDraxContext.js b/build/hooks/useDraxContext.js new file mode 100644 index 0000000..744429f --- /dev/null +++ b/build/hooks/useDraxContext.js @@ -0,0 +1,13 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.useDraxContext = void 0; +const react_1 = require("react"); +const DraxContext_1 = require("../DraxContext"); +const useDraxContext = () => { + const drax = (0, react_1.useContext)(DraxContext_1.DraxContext); + if (!drax) { + throw Error('No DraxProvider found'); + } + return drax; +}; +exports.useDraxContext = useDraxContext; diff --git a/build/hooks/useDraxId.d.ts b/build/hooks/useDraxId.d.ts new file mode 100644 index 0000000..87afaf7 --- /dev/null +++ b/build/hooks/useDraxId.d.ts @@ -0,0 +1 @@ +export declare const useDraxId: (explicitId?: string) => string; diff --git a/build/hooks/useDraxId.js b/build/hooks/useDraxId.js new file mode 100644 index 0000000..d1cd282 --- /dev/null +++ b/build/hooks/useDraxId.js @@ -0,0 +1,13 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.useDraxId = void 0; +const react_1 = require("react"); +const math_1 = require("../math"); +// Return explicitId, or a consistent randomly generated identifier if explicitId is falsy. +const useDraxId = (explicitId) => { + // A generated unique identifier for this view, for use if id prop is not specified. + const [randomId] = (0, react_1.useState)(math_1.generateRandomId); + // We use || rather than ?? for the return value in case explicitId is an empty string. + return explicitId || randomId; +}; +exports.useDraxId = useDraxId; diff --git a/build/hooks/useDraxRegistry.d.ts b/build/hooks/useDraxRegistry.d.ts new file mode 100644 index 0000000..c2ff48e --- /dev/null +++ b/build/hooks/useDraxRegistry.d.ts @@ -0,0 +1,77 @@ +/// +import { Animated } from 'react-native'; +import { RegisterViewPayload, UnregisterViewPayload, UpdateViewProtocolPayload, UpdateViewMeasurementsPayload, DraxViewData, DraxViewMeasurements, Position, DraxFoundAbsoluteViewEntry, StartDragPayload, DraxAbsoluteViewEntry, DraxAbsoluteViewData, DraxStateDispatch, DraxSnapbackTarget } from '../types'; +interface GetDragPositionDataParams { + parentPosition: Position; + draggedMeasurements: DraxViewMeasurements; + lockXPosition?: boolean; + lockYPosition?: boolean; +} +/** Create a Drax registry and wire up all of the methods. */ +export declare const useDraxRegistry: (stateDispatch: DraxStateDispatch) => { + getViewData: (id: string | undefined) => DraxViewData | undefined; + getAbsoluteViewData: (id: string | undefined) => DraxAbsoluteViewData | undefined; + getTrackingDragged: () => { + tracking: import("../types").DraxTrackingDrag; + id: string; + data: DraxAbsoluteViewData; + } | undefined; + getTrackingReceiver: () => { + tracking: import("../types").DraxTrackingReceiver; + id: string; + data: DraxAbsoluteViewData; + } | undefined; + getTrackingMonitorIds: () => string[]; + getTrackingMonitors: () => DraxAbsoluteViewEntry[]; + getDragPositionData: (params: GetDragPositionDataParams) => { + dragAbsolutePosition: { + x: number; + y: number; + }; + dragTranslation: { + x: number; + y: number; + }; + dragTranslationRatio: { + x: number; + y: number; + }; + } | undefined; + findMonitorsAndReceiver: (absolutePosition: Position, excludeViewId: string) => { + monitors: DraxFoundAbsoluteViewEntry[]; + receiver: DraxFoundAbsoluteViewEntry; + }; + getHoverItems: () => { + internalRenderHoverView: (props: import("../types").DraxInternalRenderHoverViewProps) => import("react").ReactNode; + key: string; + id: string; + hoverPosition: Animated.ValueXY; + dimensions: { + width: number; + height: number; + }; + }[]; + registerView: (payload: RegisterViewPayload) => void; + updateViewProtocol: (payload: UpdateViewProtocolPayload) => void; + updateViewMeasurements: (payload: UpdateViewMeasurementsPayload) => void; + resetReceiver: () => void; + resetDrag: (snapbackTarget?: DraxSnapbackTarget) => void; + startDrag: (payload: StartDragPayload) => { + dragAbsolutePosition: Position; + dragTranslation: { + x: number; + y: number; + }; + dragTranslationRatio: { + x: number; + y: number; + }; + dragOffset: Position; + hoverPosition: Animated.ValueXY; + }; + updateDragPosition: (dragAbsolutePosition: Position) => void; + updateReceiver: (receiver: DraxFoundAbsoluteViewEntry, dragged: DraxAbsoluteViewEntry) => import("../types").DraxTrackingReceiver | undefined; + setMonitorIds: (monitorIds: string[]) => void; + unregisterView: (payload: UnregisterViewPayload) => void; +}; +export {}; diff --git a/build/hooks/useDraxRegistry.js b/build/hooks/useDraxRegistry.js new file mode 100644 index 0000000..ffe6fc2 --- /dev/null +++ b/build/hooks/useDraxRegistry.js @@ -0,0 +1,712 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.useDraxRegistry = void 0; +const react_1 = require("react"); +const react_native_1 = require("react-native"); +const useDraxState_1 = require("./useDraxState"); +const types_1 = require("../types"); +const math_1 = require("../math"); +const params_1 = require("../params"); +/* + * The registry functions mutate their registry parameter, so let's + * disable the "no parameter reassignment" rule for the entire file: + */ +/* eslint-disable no-param-reassign */ +/** Create an initial empty Drax registry. */ +const createInitialRegistry = (stateDispatch) => ({ + stateDispatch, + viewIds: [], + viewDataById: {}, + drag: undefined, + releaseIds: [], + releaseById: {}, +}); +/** Create the initial empty protocol data for a newly registered view. */ +const createInitialProtocol = () => ({ + draggable: false, + receptive: false, + monitoring: false, +}); +/** Get data for a registered view by its id. */ +const getViewDataFromRegistry = (registry, id) => ((id && registry.viewIds.includes(id)) ? registry.viewDataById[id] : undefined); +/** Get absolute measurements for a registered view, incorporating parents and clipping. */ +const getAbsoluteMeasurementsForViewFromRegistry = (registry, { measurements, parentId }, clipped = false) => { + if (!measurements) { + // console.log('Failed to get absolute measurements for view: no measurements'); + return undefined; + } + if (!parentId) { + return measurements; + } + const parentViewData = getViewDataFromRegistry(registry, parentId); + if (!parentViewData) { + // console.log(`Failed to get absolute measurements for view: no view data for parent id ${parentId}`); + return undefined; + } + const parentMeasurements = getAbsoluteMeasurementsForViewFromRegistry(registry, parentViewData, clipped); + if (!parentMeasurements) { + // console.log(`Failed to get absolute measurements for view: no absolute measurements for parent id ${parentId}`); + return undefined; + } + const { x, y, width, height, } = measurements; + const { x: parentX, y: parentY, } = parentMeasurements; + const { x: offsetX, y: offsetY } = parentViewData.scrollPositionRef?.current || { x: 0, y: 0 }; + const abs = { + width, + height, + x: parentX + x - offsetX, + y: parentY + y - offsetY, + }; + return clipped ? (0, math_1.clipMeasurements)(abs, parentMeasurements) : abs; +}; +/** Get data, including absolute measurements, for a registered view by its id. */ +const getAbsoluteViewDataFromRegistry = (registry, id) => { + const viewData = getViewDataFromRegistry(registry, id); + if (!viewData) { + // console.log(`No view data for id ${id}`); + return undefined; + } + const absoluteMeasurements = getAbsoluteMeasurementsForViewFromRegistry(registry, viewData); + if (!absoluteMeasurements) { + // console.log(`No absolute measurements for id ${id}`); + return undefined; + } + return { + ...viewData, + measurements: viewData.measurements, + absoluteMeasurements, + }; +}; +/** Convenience function to return a view's id and absolute data. */ +const getAbsoluteViewEntryFromRegistry = (registry, id) => { + if (id === undefined) { + return undefined; + } + const data = getAbsoluteViewDataFromRegistry(registry, id); + return data && { id, data }; +}; +/** + * If multiple recievers match, we need to pick the one that is on top. This + * is first done by filtering out all that are parents (because parent views are below child ones) + * and then if there are any further possibilities, it chooses the smallest one. + */ +const getTopMostReceiver = (receivers) => { + const ids = receivers.map(receiver => receiver.data.parentId); + receivers = receivers.filter(receiver => !ids.includes(receiver.id)); + receivers.sort((receiverA, receiverB) => receiverA.data.measurements.height * receiverA.data.measurements.width - receiverB.data.measurements.height * receiverB.data.measurements.width); + return receivers[0]; +}; +/** + * Find all monitoring views and the latest receptive view that + * contain the touch coordinates, excluding the specified view. + */ +const findMonitorsAndReceiverInRegistry = (registry, absolutePosition, excludeViewId) => { + const monitors = []; + let receivers = []; + // console.log(`find monitors and receiver for absolute position (${absolutePosition.x}, ${absolutePosition.y})`); + registry.viewIds.forEach((targetId) => { + // console.log(`checking target id ${targetId}`); + if (targetId === excludeViewId) { + // Don't consider the excluded view. + // console.log('excluded'); + return; + } + const target = getViewDataFromRegistry(registry, targetId); + if (!target) { + // This should never happen, but just in case. + // console.log('no view data found'); + return; + } + const { receptive, monitoring } = target.protocol; + if (!receptive && !monitoring) { + // Only consider receptive or monitoring views. + // console.log('not receptive nor monitoring'); + return; + } + const absoluteMeasurements = getAbsoluteMeasurementsForViewFromRegistry(registry, target, true); + if (!absoluteMeasurements) { + // Only consider views for which we have absolute measurements. + // console.log('failed to find absolute measurements'); + return; + } + // console.log(`absolute measurements: ${JSON.stringify(absoluteMeasurements, null, 2)}`); + if ((0, math_1.isPointInside)(absolutePosition, absoluteMeasurements)) { + // Drag point is within this target. + const foundView = { + id: targetId, + data: { + ...target, + measurements: target.measurements, + absoluteMeasurements, + }, + ...(0, math_1.getRelativePosition)(absolutePosition, absoluteMeasurements), + }; + if (monitoring) { + // Add it to the list of monitors. + monitors.push(foundView); + // console.log('it\'s a monitor'); + } + if (receptive) { + // It's the latest receiver found. + receivers.push(foundView); + // console.log('it\'s a receiver'); + } + } + }); + return { + monitors, + receiver: getTopMostReceiver(receivers) + }; +}; +/** Get id and data for the currently dragged view, if any. */ +const getTrackingDraggedFromRegistry = (registry) => { + const tracking = registry.drag; + if (tracking !== undefined) { + const viewEntry = getAbsoluteViewEntryFromRegistry(registry, tracking.draggedId); + if (viewEntry !== undefined) { + return { + ...viewEntry, + tracking, + }; + } + } + return undefined; +}; +/** Get id and data for the currently receiving view, if any. */ +const getTrackingReceiverFromRegistry = (registry) => { + const tracking = registry.drag?.receiver; + if (tracking !== undefined) { + const viewEntry = getAbsoluteViewEntryFromRegistry(registry, tracking.receiverId); + if (viewEntry !== undefined) { + return { + ...viewEntry, + tracking, + }; + } + } + return undefined; +}; +/** Get ids for all currently monitoring views. */ +const getTrackingMonitorIdsFromRegistry = (registry) => (registry.drag?.monitorIds || []); +/** Get id and data for all currently monitoring views. */ +const getTrackingMonitorsFromRegistry = (registry) => (registry.drag?.monitorIds + .map((id) => getAbsoluteViewEntryFromRegistry(registry, id)) + .filter((value) => !!value) + || []); +/** Get the array of hover items for dragged and released views */ +const getHoverItemsFromRegistry = (registry) => { + const hoverItems = []; + // Find all released view hover items, in order from oldest to newest. + registry.releaseIds.forEach((releaseId) => { + const release = registry.releaseById[releaseId]; + if (release) { + const { viewId, hoverPosition } = release; + const releasedData = getAbsoluteViewDataFromRegistry(registry, viewId); + if (releasedData) { + const { protocol: { internalRenderHoverView }, measurements } = releasedData; + if (internalRenderHoverView) { + hoverItems.push({ + hoverPosition, + internalRenderHoverView, + key: releaseId, + id: viewId, + dimensions: (0, math_1.extractDimensions)(measurements), + }); + } + } + } + }); + // Find the currently dragged hover item. + const { id: draggedId, data: draggedData } = getTrackingDraggedFromRegistry(registry) ?? {}; + if (draggedData) { + const { protocol: { internalRenderHoverView }, measurements } = draggedData; + if (draggedId && internalRenderHoverView) { + hoverItems.push({ + internalRenderHoverView, + key: `dragged-hover-${draggedId}`, + id: draggedId, + hoverPosition: registry.drag.hoverPosition, + dimensions: (0, math_1.extractDimensions)(measurements), + }); + } + } + return hoverItems; +}; +/** + * Get the absolute position of a drag already in progress from touch + * coordinates within the immediate parent view of the dragged view. + */ +const getDragPositionDataFromRegistry = (registry, { parentPosition, draggedMeasurements, lockXPosition = false, lockYPosition = false, }) => { + if (!registry.drag) { + return undefined; + } + /* + * To determine drag position in absolute coordinates, we add: + * absolute coordinates of drag start + * + translation offset of drag + */ + const { absoluteStartPosition, parentStartPosition, } = registry.drag; + const dragTranslation = { + x: lockXPosition ? 0 : (parentPosition.x - parentStartPosition.x), + y: lockYPosition ? 0 : (parentPosition.y - parentStartPosition.y), + }; + const dragTranslationRatio = { + x: dragTranslation.x / draggedMeasurements.width, + y: dragTranslation.y / draggedMeasurements.height, + }; + const dragAbsolutePosition = { + x: absoluteStartPosition.x + dragTranslation.x, + y: absoluteStartPosition.y + dragTranslation.y, + }; + return { + dragAbsolutePosition, + dragTranslation, + dragTranslationRatio, + }; +}; +/** Register a Drax view. */ +const registerViewInRegistry = (registry, { id, parentId, scrollPositionRef }) => { + const { viewIds, viewDataById, stateDispatch } = registry; + // Make sure not to duplicate registered view id. + if (viewIds.indexOf(id) < 0) { + viewIds.push(id); + } + // Maintain any existing view data. + const existingData = getViewDataFromRegistry(registry, id); + // console.log(`Register view ${id} with parent ${parentId}`); + viewDataById[id] = { + parentId, + scrollPositionRef, + protocol: existingData?.protocol ?? createInitialProtocol(), + measurements: existingData?.measurements, // Starts undefined. + }; + stateDispatch(useDraxState_1.actions.createViewState({ id })); +}; +/** Update a view's protocol callbacks/data. */ +const updateViewProtocolInRegistry = (registry, { id, protocol }) => { + const existingData = getViewDataFromRegistry(registry, id); + if (existingData) { + registry.viewDataById[id].protocol = protocol; + } +}; +/** Update a view's measurements. */ +const updateViewMeasurementsInRegistry = (registry, { id, measurements }) => { + const existingData = getViewDataFromRegistry(registry, id); + if (existingData) { + // console.log(`Update ${id} measurements: @(${measurements?.x}, ${measurements?.y}) ${measurements?.width}x${measurements?.height}`); + registry.viewDataById[id].measurements = measurements; + } +}; +/** Reset the receiver in drag tracking, if any. */ +const resetReceiverInRegistry = ({ drag, stateDispatch }) => { + if (!drag) { + return; + } + const { draggedId, receiver } = drag; + if (!receiver) { + // console.log('no receiver to clear'); + return; + } + // console.log('clearing receiver'); + drag.receiver = undefined; + stateDispatch(useDraxState_1.actions.updateTrackingStatus({ receiving: false })); + stateDispatch(useDraxState_1.actions.updateViewState({ + id: draggedId, + viewStateUpdate: { + draggingOverReceiver: undefined, + }, + })); + stateDispatch(useDraxState_1.actions.updateViewState({ + id: receiver.receiverId, + viewStateUpdate: { + receiveStatus: types_1.DraxViewReceiveStatus.Inactive, + receiveOffset: undefined, + receiveOffsetRatio: undefined, + receivingDrag: undefined, + }, + })); +}; +/** Track a new release, returning its unique identifier. */ +const createReleaseInRegistry = (registry, release) => { + const releaseId = (0, math_1.generateRandomId)(); + registry.releaseIds.push(releaseId); + registry.releaseById[releaseId] = release; + return releaseId; +}; +/** Stop tracking a release, given its unique identifier. */ +const deleteReleaseInRegistry = (registry, releaseId) => { + registry.releaseIds = registry.releaseIds.filter((id) => id !== releaseId); + delete registry.releaseById[releaseId]; +}; +/** Reset drag tracking, if any. */ +const resetDragInRegistry = (registry, snapbackTarget = types_1.DraxSnapbackTargetPreset.Default) => { + const { drag, stateDispatch } = registry; + if (!drag) { + return; + } + resetReceiverInRegistry(registry); + const { draggedId, hoverPosition } = drag; + const draggedData = getAbsoluteViewDataFromRegistry(registry, draggedId); + // Clear the drag. + // console.log('clearing drag'); + registry.drag = undefined; + // Determine if/where/how to snapback. + let snapping = false; + if (snapbackTarget !== types_1.DraxSnapbackTargetPreset.None && draggedData) { + const { internalRenderHoverView, onSnapbackEnd, snapbackAnimator, animateSnapback = true, snapbackDelay = params_1.defaultSnapbackDelay, snapbackDuration = params_1.defaultSnapbackDuration, } = draggedData.protocol; + if (internalRenderHoverView && animateSnapback) { + let toValue; + if ((0, types_1.isPosition)(snapbackTarget)) { + // Snapback to specified target. + toValue = snapbackTarget; + } + else { + // Snapback to default position (where original view is). + toValue = { + x: draggedData.absoluteMeasurements.x, + y: draggedData.absoluteMeasurements.y, + }; + } + if (toValue && snapbackDuration > 0) { + snapping = true; + // Add a release to tracking. + const releaseId = createReleaseInRegistry(registry, { hoverPosition, viewId: draggedId }); + // Animate the released hover snapback. + let animation; + if (snapbackAnimator) { + animation = snapbackAnimator({ + hoverPosition, + toValue, + delay: snapbackDelay, + duration: snapbackDuration, + }); + } + else { + animation = react_native_1.Animated.timing(hoverPosition, { + toValue, + delay: snapbackDelay, + duration: snapbackDuration, + useNativeDriver: true, + }); + } + animation.start(({ finished }) => { + // Remove the release from tracking, regardless of whether animation finished. + deleteReleaseInRegistry(registry, releaseId); + // Call the snapback end handler, regardless of whether animation of finished. + onSnapbackEnd?.(); + // If the animation finished, update the view state for the released view to be inactive. + if (finished) { + stateDispatch(useDraxState_1.actions.updateViewState({ + id: draggedId, + viewStateUpdate: { + dragStatus: types_1.DraxViewDragStatus.Inactive, + hoverPosition: undefined, + grabOffset: undefined, + grabOffsetRatio: undefined, + }, + })); + } + }); + } + } + } + // Update the drag tracking status. + stateDispatch(useDraxState_1.actions.updateTrackingStatus({ dragging: false })); + // Update the view state, data dependent on whether snapping back. + const viewStateUpdate = { + dragAbsolutePosition: undefined, + dragTranslation: undefined, + dragTranslationRatio: undefined, + dragOffset: undefined, + }; + if (snapping) { + viewStateUpdate.dragStatus = types_1.DraxViewDragStatus.Released; + } + else { + viewStateUpdate.dragStatus = types_1.DraxViewDragStatus.Inactive; + viewStateUpdate.hoverPosition = undefined; + viewStateUpdate.grabOffset = undefined; + viewStateUpdate.grabOffsetRatio = undefined; + } + stateDispatch(useDraxState_1.actions.updateViewState({ + viewStateUpdate, + id: draggedId, + })); +}; +/** Start tracking a drag. */ +const startDragInRegistry = (registry, { dragAbsolutePosition, dragParentPosition, draggedId, grabOffset, grabOffsetRatio, }) => { + const { stateDispatch } = registry; + resetDragInRegistry(registry); + const dragTranslation = { x: 0, y: 0 }; + const dragTranslationRatio = { x: 0, y: 0 }; + const dragOffset = grabOffset; + const hoverPosition = new react_native_1.Animated.ValueXY({ + x: dragAbsolutePosition.x - grabOffset.x, + y: dragAbsolutePosition.y - grabOffset.y, + }); + registry.drag = { + absoluteStartPosition: dragAbsolutePosition, + parentStartPosition: dragParentPosition, + draggedId, + dragAbsolutePosition, + dragTranslation, + dragTranslationRatio, + dragOffset, + grabOffset, + grabOffsetRatio, + hoverPosition, + receiver: undefined, + monitorIds: [], + }; + stateDispatch(useDraxState_1.actions.updateTrackingStatus({ dragging: true })); + stateDispatch(useDraxState_1.actions.updateViewState({ + id: draggedId, + viewStateUpdate: { + dragAbsolutePosition, + dragTranslation, + dragTranslationRatio, + dragOffset, + grabOffset, + grabOffsetRatio, + hoverPosition, + dragStatus: types_1.DraxViewDragStatus.Dragging, + }, + })); + return { + dragAbsolutePosition, + dragTranslation, + dragTranslationRatio, + dragOffset, + hoverPosition, + }; +}; +/** Update drag position. */ +const updateDragPositionInRegistry = (registry, dragAbsolutePosition) => { + const { drag, stateDispatch } = registry; + if (!drag) { + return; + } + const dragged = getTrackingDraggedFromRegistry(registry); + if (!dragged) { + return; + } + const { absoluteMeasurements } = dragged.data; + const { draggedId, grabOffset, hoverPosition } = drag; + const dragTranslation = { + x: dragAbsolutePosition.x - drag.absoluteStartPosition.x, + y: dragAbsolutePosition.y - drag.absoluteStartPosition.y, + }; + const dragTranslationRatio = { + x: dragTranslation.x / absoluteMeasurements.width, + y: dragTranslation.y / absoluteMeasurements.height, + }; + const dragOffset = { + x: dragAbsolutePosition.x - absoluteMeasurements.x, + y: dragAbsolutePosition.y - absoluteMeasurements.y, + }; + drag.dragAbsolutePosition = dragAbsolutePosition; + drag.dragTranslation = dragTranslation; + drag.dragTranslationRatio = dragTranslationRatio; + drag.dragOffset = dragOffset; + hoverPosition.setValue({ + x: dragAbsolutePosition.x - grabOffset.x, + y: dragAbsolutePosition.y - grabOffset.y, + }); + stateDispatch(useDraxState_1.actions.updateViewState({ + id: draggedId, + viewStateUpdate: { + dragAbsolutePosition, + dragTranslation, + dragTranslationRatio, + dragOffset, + }, + })); +}; +/** Update receiver for a drag. */ +const updateReceiverInRegistry = (registry, receiver, dragged) => { + const { drag, stateDispatch } = registry; + if (!drag) { + return undefined; + } + const { relativePosition, relativePositionRatio, id: receiverId, data: receiverData, } = receiver; + const { parentId: receiverParentId, protocol: { receiverPayload }, } = receiverData; + const { id: draggedId, data: draggedData, } = dragged; + const { parentId: draggedParentId, protocol: { dragPayload }, } = draggedData; + const oldReceiver = drag.receiver; + const receiveOffset = relativePosition; + const receiveOffsetRatio = relativePositionRatio; + const receiverUpdate = { + receivingDrag: { + id: draggedId, + parentId: draggedParentId, + payload: dragPayload, + }, + receiveOffset, + receiveOffsetRatio, + }; + if (oldReceiver?.receiverId === receiverId) { + // Same receiver, update offsets. + oldReceiver.receiveOffset = receiveOffset; + oldReceiver.receiveOffsetRatio = receiveOffsetRatio; + } + else { + // New receiver. + if (oldReceiver) { + // Clear the old receiver. + resetReceiverInRegistry(registry); + } + drag.receiver = { + receiverId, + receiveOffset, + receiveOffsetRatio, + }; + receiverUpdate.receiveStatus = types_1.DraxViewReceiveStatus.Receiving; + stateDispatch(useDraxState_1.actions.updateTrackingStatus({ receiving: true })); + } + stateDispatch(useDraxState_1.actions.updateViewState({ + id: receiverId, + viewStateUpdate: receiverUpdate, + })); + stateDispatch(useDraxState_1.actions.updateViewState({ + id: draggedId, + viewStateUpdate: { + draggingOverReceiver: { + id: receiverId, + parentId: receiverParentId, + payload: receiverPayload, + }, + }, + })); + return drag.receiver; +}; +/** Set the monitors for a drag. */ +const setMonitorIdsInRegistry = ({ drag }, monitorIds) => { + if (drag) { + drag.monitorIds = monitorIds; + } +}; +/** Unregister a Drax view. */ +const unregisterViewInRegistry = (registry, { id }) => { + const { [id]: removed, ...viewDataById } = registry.viewDataById; + registry.viewIds = registry.viewIds.filter((thisId) => thisId !== id); + registry.viewDataById = viewDataById; + if (registry.drag?.draggedId === id) { + resetDragInRegistry(registry); + } + else if (registry.drag?.receiver?.receiverId === id) { + resetReceiverInRegistry(registry); + } + registry.stateDispatch(useDraxState_1.actions.deleteViewState({ id })); +}; +/** Create a Drax registry and wire up all of the methods. */ +const useDraxRegistry = (stateDispatch) => { + /** Registry for tracking views and drags. */ + const registryRef = (0, react_1.useRef)(createInitialRegistry(stateDispatch)); + /** Ensure that the registry has the latest version of state dispatch, although it should never change. */ + (0, react_1.useEffect)(() => { + registryRef.current.stateDispatch = stateDispatch; + }, [stateDispatch]); + /** + * + * Getters/finders, with no state reactions. + * + */ + /** Get data for a registered view by its id. */ + const getViewData = (0, react_1.useCallback)((id) => getViewDataFromRegistry(registryRef.current, id), []); + /** Get data, including absolute measurements, for a registered view by its id. */ + const getAbsoluteViewData = (0, react_1.useCallback)((id) => getAbsoluteViewDataFromRegistry(registryRef.current, id), []); + /** Get id and data for the currently dragged view, if any. */ + const getTrackingDragged = (0, react_1.useCallback)(() => getTrackingDraggedFromRegistry(registryRef.current), []); + /** Get id and data for the currently receiving view, if any. */ + const getTrackingReceiver = (0, react_1.useCallback)(() => getTrackingReceiverFromRegistry(registryRef.current), []); + /** Get ids for all currently monitoring views. */ + const getTrackingMonitorIds = (0, react_1.useCallback)(() => getTrackingMonitorIdsFromRegistry(registryRef.current), []); + /** Get id and data for all currently monitoring views. */ + const getTrackingMonitors = (0, react_1.useCallback)(() => getTrackingMonitorsFromRegistry(registryRef.current), []); + /** + * Get the absolute position of a drag already in progress from touch + * coordinates within the immediate parent view of the dragged view. + */ + const getDragPositionData = (0, react_1.useCallback)((params) => (getDragPositionDataFromRegistry(registryRef.current, params)), []); + /** + * Find all monitoring views and the latest receptive view that + * contain the touch coordinates, excluding the specified view. + */ + const findMonitorsAndReceiver = (0, react_1.useCallback)((absolutePosition, excludeViewId) => (findMonitorsAndReceiverInRegistry(registryRef.current, absolutePosition, excludeViewId)), []); + /** Get the array of hover items for dragged and released views */ + const getHoverItems = (0, react_1.useCallback)(() => getHoverItemsFromRegistry(registryRef.current), []); + /** + * + * Imperative methods without state reactions (data management only). + * + */ + /** Update a view's protocol callbacks/data. */ + const updateViewProtocol = (0, react_1.useCallback)((payload) => updateViewProtocolInRegistry(registryRef.current, payload), []); + /** Update a view's measurements. */ + const updateViewMeasurements = (0, react_1.useCallback)((payload) => updateViewMeasurementsInRegistry(registryRef.current, payload), []); + /** + * + * Imperative methods with potential state reactions. + * + */ + /** Register a Drax view. */ + const registerView = (0, react_1.useCallback)((payload) => registerViewInRegistry(registryRef.current, payload), []); + /** Reset the receiver in drag tracking, if any. */ + const resetReceiver = (0, react_1.useCallback)(() => resetReceiverInRegistry(registryRef.current), []); + /** Reset drag tracking, if any. */ + const resetDrag = (0, react_1.useCallback)((snapbackTarget) => resetDragInRegistry(registryRef.current, snapbackTarget), []); + /** Start tracking a drag. */ + const startDrag = (0, react_1.useCallback)((payload) => startDragInRegistry(registryRef.current, payload), []); + /** Update drag position. */ + const updateDragPosition = (0, react_1.useCallback)((dragAbsolutePosition) => (updateDragPositionInRegistry(registryRef.current, dragAbsolutePosition)), []); + /** Update the receiver for a drag. */ + const updateReceiver = (0, react_1.useCallback)((receiver, dragged) => (updateReceiverInRegistry(registryRef.current, receiver, dragged)), []); + /** Set the monitors for a drag. */ + const setMonitorIds = (0, react_1.useCallback)((monitorIds) => setMonitorIdsInRegistry(registryRef.current, monitorIds), []); + /** Unregister a Drax view. */ + const unregisterView = (0, react_1.useCallback)((payload) => unregisterViewInRegistry(registryRef.current, payload), []); + /** Create the Drax registry object for return, only replacing reference when necessary. */ + const draxRegistry = (0, react_1.useMemo)(() => ({ + getViewData, + getAbsoluteViewData, + getTrackingDragged, + getTrackingReceiver, + getTrackingMonitorIds, + getTrackingMonitors, + getDragPositionData, + findMonitorsAndReceiver, + getHoverItems, + registerView, + updateViewProtocol, + updateViewMeasurements, + resetReceiver, + resetDrag, + startDrag, + updateDragPosition, + updateReceiver, + setMonitorIds, + unregisterView, + }), [ + getViewData, + getAbsoluteViewData, + getTrackingDragged, + getTrackingReceiver, + getTrackingMonitorIds, + getTrackingMonitors, + getDragPositionData, + findMonitorsAndReceiver, + getHoverItems, + registerView, + updateViewProtocol, + updateViewMeasurements, + resetReceiver, + resetDrag, + startDrag, + updateDragPosition, + updateReceiver, + setMonitorIds, + unregisterView, + ]); + return draxRegistry; +}; +exports.useDraxRegistry = useDraxRegistry; diff --git a/build/hooks/useDraxState.d.ts b/build/hooks/useDraxState.d.ts new file mode 100644 index 0000000..e29fe89 --- /dev/null +++ b/build/hooks/useDraxState.d.ts @@ -0,0 +1,10 @@ +/// +import { DraxViewState, DraxStateActionCreators, CreateViewStatePayload, UpdateViewStatePayload, DeleteViewStatePayload, UpdateTrackingStatusPayload } from '../types'; +/** Collection of Drax action creators */ +export declare const actions: DraxStateActionCreators; +/** Create a Drax state and wire up its methods. */ +export declare const useDraxState: () => { + getViewState: (id: string | undefined) => DraxViewState | undefined; + getTrackingStatus: () => import("../types").DraxTrackingStatus; + dispatch: import("react").Dispatch | import("typesafe-actions").PayloadAction<"updateViewState", UpdateViewStatePayload> | import("typesafe-actions").PayloadAction<"deleteViewState", DeleteViewStatePayload> | import("typesafe-actions").PayloadAction<"updateTrackingStatus", UpdateTrackingStatusPayload>>; +}; diff --git a/build/hooks/useDraxState.js b/build/hooks/useDraxState.js new file mode 100644 index 0000000..10a98e9 --- /dev/null +++ b/build/hooks/useDraxState.js @@ -0,0 +1,129 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.useDraxState = exports.actions = void 0; +const react_1 = require("react"); +const typesafe_actions_1 = require("typesafe-actions"); +const lodash_isequal_1 = __importDefault(require("lodash.isequal")); +const types_1 = require("../types"); +/** Create the initial empty view state data for a newly registered view. */ +const createInitialViewState = () => ({ + dragStatus: types_1.DraxViewDragStatus.Inactive, + dragAbsolutePosition: undefined, + dragOffset: undefined, + grabOffset: undefined, + grabOffsetRatio: undefined, + draggingOverReceiver: undefined, + receiveStatus: types_1.DraxViewReceiveStatus.Inactive, + receiveOffset: undefined, + receiveOffsetRatio: undefined, + receivingDrag: undefined, +}); +/** Create an initial empty Drax state. */ +const createInitialState = () => ({ + viewStateById: {}, + trackingStatus: { + dragging: false, + receiving: false, + }, +}); +/** Selector for a view state by view id. */ +const selectViewState = (state, id) => (id === undefined ? undefined : state.viewStateById[id]); +/** Selector for tracking status. */ +const selectTrackingStatus = (state) => state.trackingStatus; +/** Collection of Drax action creators */ +exports.actions = { + createViewState: (0, typesafe_actions_1.createAction)('createViewState')(), + updateViewState: (0, typesafe_actions_1.createAction)('updateViewState')(), + deleteViewState: (0, typesafe_actions_1.createAction)('deleteViewState')(), + updateTrackingStatus: (0, typesafe_actions_1.createAction)('updateTrackingStatus')(), +}; +/** The DraxState reducer. */ +const reducer = (state, action) => { + switch (action.type) { + case (0, typesafe_actions_1.getType)(exports.actions.createViewState): { + const { id } = action.payload; + const viewState = selectViewState(state, id); + if (viewState) { + return state; + } + return { + ...state, + viewStateById: { + ...state.viewStateById, + [id]: createInitialViewState(), + }, + }; + } + case (0, typesafe_actions_1.getType)(exports.actions.updateViewState): { + const { id, viewStateUpdate } = action.payload; + const viewState = selectViewState(state, id); + if (viewState) { + const newViewState = { + ...viewState, + ...viewStateUpdate, + }; + if ((0, lodash_isequal_1.default)(viewState, newViewState)) { + return state; + } + return { + ...state, + viewStateById: { + ...state.viewStateById, + [id]: newViewState, + }, + }; + } + return state; + } + case (0, typesafe_actions_1.getType)(exports.actions.deleteViewState): { + const { id } = action.payload; + const { [id]: removed, ...viewStateById } = state.viewStateById; + if (removed) { + return { + ...state, + viewStateById, + }; + } + return state; + } + case (0, typesafe_actions_1.getType)(exports.actions.updateTrackingStatus): { + return { + ...state, + trackingStatus: { + ...state.trackingStatus, + ...action.payload, + }, + }; + } + default: + return state; + } +}; +/** Create a Drax state and wire up its methods. */ +const useDraxState = () => { + /** Reducer for storing view states and tracking status. */ + const [state, dispatch] = (0, react_1.useReducer)(reducer, undefined, createInitialState); + /** Get state for a view by its id. */ + const getViewState = (0, react_1.useCallback)((id) => selectViewState(state, id), [state]); + /** Get the current tracking status. */ + const getTrackingStatus = (0, react_1.useCallback)(() => selectTrackingStatus(state), [state]); + /** Create the Drax state object for return, only replacing reference when necessary. */ + const draxState = (0, react_1.useMemo)(() => ({ + getViewState, + getTrackingStatus, + dispatch, + }), [ + getViewState, + getTrackingStatus, + ]); + /* + useEffect(() => { + console.log(`Rendering drax state ${JSON.stringify(state, null, 2)}`); + }); + */ + return draxState; +}; +exports.useDraxState = useDraxState; diff --git a/build/index.d.ts b/build/index.d.ts new file mode 100644 index 0000000..3f862fb --- /dev/null +++ b/build/index.d.ts @@ -0,0 +1,7 @@ +export * from './types'; +export { DraxContext } from './DraxContext'; +export { DraxList } from './DraxList'; +export { DraxProvider } from './DraxProvider'; +export { DraxScrollView } from './DraxScrollView'; +export { DraxSubprovider } from './DraxSubprovider'; +export { DraxView } from './DraxView'; diff --git a/build/index.js b/build/index.js new file mode 100644 index 0000000..f6c50d1 --- /dev/null +++ b/build/index.js @@ -0,0 +1,30 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DraxView = exports.DraxSubprovider = exports.DraxScrollView = exports.DraxProvider = exports.DraxList = exports.DraxContext = void 0; +__exportStar(require("./types"), exports); +var DraxContext_1 = require("./DraxContext"); +Object.defineProperty(exports, "DraxContext", { enumerable: true, get: function () { return DraxContext_1.DraxContext; } }); +var DraxList_1 = require("./DraxList"); +Object.defineProperty(exports, "DraxList", { enumerable: true, get: function () { return DraxList_1.DraxList; } }); +var DraxProvider_1 = require("./DraxProvider"); +Object.defineProperty(exports, "DraxProvider", { enumerable: true, get: function () { return DraxProvider_1.DraxProvider; } }); +var DraxScrollView_1 = require("./DraxScrollView"); +Object.defineProperty(exports, "DraxScrollView", { enumerable: true, get: function () { return DraxScrollView_1.DraxScrollView; } }); +var DraxSubprovider_1 = require("./DraxSubprovider"); +Object.defineProperty(exports, "DraxSubprovider", { enumerable: true, get: function () { return DraxSubprovider_1.DraxSubprovider; } }); +var DraxView_1 = require("./DraxView"); +Object.defineProperty(exports, "DraxView", { enumerable: true, get: function () { return DraxView_1.DraxView; } }); diff --git a/build/math.d.ts b/build/math.d.ts new file mode 100644 index 0000000..d6b34b9 --- /dev/null +++ b/build/math.d.ts @@ -0,0 +1,22 @@ +import { DraxViewMeasurements, Position } from './types'; +export declare const clipMeasurements: (vm: DraxViewMeasurements, cvm: DraxViewMeasurements) => DraxViewMeasurements; +export declare const isPointInside: ({ x, y }: Position, { width, height, x: x0, y: y0, }: DraxViewMeasurements) => boolean; +export declare const getRelativePosition: ({ x, y }: Position, { width, height, x: x0, y: y0, }: DraxViewMeasurements) => { + relativePosition: { + x: number; + y: number; + }; + relativePositionRatio: { + x: number; + y: number; + }; +}; +export declare const extractPosition: ({ x, y }: DraxViewMeasurements) => { + x: number; + y: number; +}; +export declare const extractDimensions: ({ width, height }: DraxViewMeasurements) => { + width: number; + height: number; +}; +export declare const generateRandomId: () => string; diff --git a/build/math.js b/build/math.js new file mode 100644 index 0000000..c9242b0 --- /dev/null +++ b/build/math.js @@ -0,0 +1,64 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.generateRandomId = exports.extractDimensions = exports.extractPosition = exports.getRelativePosition = exports.isPointInside = exports.clipMeasurements = void 0; +const clipMeasurements = (vm, cvm) => { + let { width, height, x: x0, y: y0, } = vm; + let x1 = x0 + width; + let y1 = y0 + height; + const { width: cwidth, height: cheight, x: cx0, y: cy0, } = cvm; + const cx1 = cx0 + cwidth; + const cy1 = cy0 + cheight; + if (x0 >= cx1 || x1 <= cx0 || y0 >= cy1 || y1 <= cy0) { + return { + x: -1, + y: -1, + width: 0, + height: 0, + }; + } + if (x0 < cx0) { + width -= cx0 - x0; + x0 = cx0; + } + if (x1 > cx1) { + width -= x1 - cx1; + x1 = cx1; + } + if (y0 < cy0) { + height -= cy0 - y0; + y0 = cy0; + } + if (y1 > cy1) { + height -= y1 - cy1; + y1 = cy1; + } + return { + width, + height, + x: x0, + y: y0, + }; +}; +exports.clipMeasurements = clipMeasurements; +const isPointInside = ({ x, y }, { width, height, x: x0, y: y0, }) => (x >= x0 && y >= y0 && x < x0 + width && y < y0 + height); +exports.isPointInside = isPointInside; +const getRelativePosition = ({ x, y }, { width, height, x: x0, y: y0, }) => { + const rx = x - x0; + const ry = y - y0; + return { + relativePosition: { x: rx, y: ry }, + relativePositionRatio: { x: rx / width, y: ry / height }, + }; +}; +exports.getRelativePosition = getRelativePosition; +const extractPosition = ({ x, y }) => ({ x, y }); +exports.extractPosition = extractPosition; +const extractDimensions = ({ width, height }) => ({ width, height }); +exports.extractDimensions = extractDimensions; +/* + * Previously we were using the uuid library to generate unique identifiers for Drax + * components. Since we do not need them to be cryptographically secure and likely + * won't need very many of them, let's just use this simple function. + */ +const generateRandomId = () => (`${Math.random().toString(36).substr(2)}${Math.random().toString(36).substr(2)}`); +exports.generateRandomId = generateRandomId; diff --git a/build/params.d.ts b/build/params.d.ts new file mode 100644 index 0000000..3ebd070 --- /dev/null +++ b/build/params.d.ts @@ -0,0 +1,18 @@ +/** Default snapback delay in milliseconds */ +export declare const defaultSnapbackDelay = 100; +/** Default snapback duration in milliseconds */ +export declare const defaultSnapbackDuration = 250; +/** Default pre-drag long press delay in milliseconds */ +export declare const defaultLongPressDelay = 0; +/** Default pre-drag long press delay in milliseconds for DraxList items */ +export declare const defaultListItemLongPressDelay = 250; +/** Default scroll event throttle (number of events per second) for DraxScrollView */ +export declare const defaultScrollEventThrottle = 8; +/** Default interval length in milliseconds for auto-scrolling jumps */ +export declare const defaultAutoScrollIntervalLength = 250; +/** Default auto-scroll jump distance, as a fraction relative to content width/length */ +export declare const defaultAutoScrollJumpRatio = 0.2; +/** Default drag-over maximum position threshold for auto-scroll back, as a fraction relative to content width/length */ +export declare const defaultAutoScrollBackThreshold = 0.1; +/** Default drag-over minimum position threshold for auto-scroll forward, as a fraction relative to content width/length */ +export declare const defaultAutoScrollForwardThreshold = 0.9; diff --git a/build/params.js b/build/params.js new file mode 100644 index 0000000..862f2f6 --- /dev/null +++ b/build/params.js @@ -0,0 +1,21 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.defaultAutoScrollForwardThreshold = exports.defaultAutoScrollBackThreshold = exports.defaultAutoScrollJumpRatio = exports.defaultAutoScrollIntervalLength = exports.defaultScrollEventThrottle = exports.defaultListItemLongPressDelay = exports.defaultLongPressDelay = exports.defaultSnapbackDuration = exports.defaultSnapbackDelay = void 0; +/** Default snapback delay in milliseconds */ +exports.defaultSnapbackDelay = 100; +/** Default snapback duration in milliseconds */ +exports.defaultSnapbackDuration = 250; +/** Default pre-drag long press delay in milliseconds */ +exports.defaultLongPressDelay = 0; +/** Default pre-drag long press delay in milliseconds for DraxList items */ +exports.defaultListItemLongPressDelay = 250; +/** Default scroll event throttle (number of events per second) for DraxScrollView */ +exports.defaultScrollEventThrottle = 8; +/** Default interval length in milliseconds for auto-scrolling jumps */ +exports.defaultAutoScrollIntervalLength = 250; +/** Default auto-scroll jump distance, as a fraction relative to content width/length */ +exports.defaultAutoScrollJumpRatio = 0.2; +/** Default drag-over maximum position threshold for auto-scroll back, as a fraction relative to content width/length */ +exports.defaultAutoScrollBackThreshold = 0.1; +/** Default drag-over minimum position threshold for auto-scroll forward, as a fraction relative to content width/length */ +exports.defaultAutoScrollForwardThreshold = 0.9; diff --git a/build/transform.d.ts b/build/transform.d.ts new file mode 100644 index 0000000..b7206cd --- /dev/null +++ b/build/transform.d.ts @@ -0,0 +1,4 @@ +import { Animated, StyleProp, ViewStyle } from 'react-native'; +import { AnimatedViewStyleWithoutLayout } from './types'; +export declare const flattenStylesWithoutLayout: (styles: StyleProp>[]) => AnimatedViewStyleWithoutLayout; +export declare const mergeStyleTransform: (style: AnimatedViewStyleWithoutLayout, transform: Animated.WithAnimatedValue) => AnimatedViewStyleWithoutLayout; diff --git a/build/transform.js b/build/transform.js new file mode 100644 index 0000000..43aca3a --- /dev/null +++ b/build/transform.js @@ -0,0 +1,17 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.mergeStyleTransform = exports.flattenStylesWithoutLayout = void 0; +const react_native_1 = require("react-native"); +const flattenStylesWithoutLayout = (styles) => { + const { margin, marginHorizontal, marginVertical, marginLeft, marginRight, marginTop, marginBottom, marginStart, marginEnd, left, right, top, bottom, flex, flexBasis, flexDirection, flexGrow, flexShrink, ...flattened } = react_native_1.StyleSheet.flatten(styles); + return flattened; +}; +exports.flattenStylesWithoutLayout = flattenStylesWithoutLayout; +const mergeStyleTransform = (style, transform) => ({ + ...style, + transform: [ + ...(transform ?? []), + ...(style.transform ?? []), + ], +}); +exports.mergeStyleTransform = mergeStyleTransform; diff --git a/build/types.d.ts b/build/types.d.ts new file mode 100644 index 0000000..9e8c720 --- /dev/null +++ b/build/types.d.ts @@ -0,0 +1,656 @@ +import { RefObject, ReactNode } from 'react'; +import { ViewProps, Animated, FlatListProps, ViewStyle, StyleProp, ScrollViewProps, ListRenderItemInfo } from 'react-native'; +import { LongPressGestureHandlerStateChangeEvent, LongPressGestureHandlerGestureEvent } from 'react-native-gesture-handler'; +import { PayloadActionCreator, ActionType } from 'typesafe-actions'; +/** Gesture state change event expected by Drax handler */ +export declare type DraxGestureStateChangeEvent = LongPressGestureHandlerStateChangeEvent['nativeEvent']; +/** Gesture event expected by Drax handler */ +export declare type DraxGestureEvent = LongPressGestureHandlerGestureEvent['nativeEvent']; +/** An xy-coordinate position value */ +export interface Position { + /** Position on horizontal x-axis, positive is right */ + x: number; + /** Position on vertical y-axis, positive is down */ + y: number; +} +/** Predicate for checking if something is a Position */ +export declare const isPosition: (something: any) => something is Position; +/** Dimensions of a view */ +export interface ViewDimensions { + /** Width of view */ + width: number; + /** Height of view */ + height: number; +} +/** Measurements of a Drax view for bounds checking purposes, relative to Drax parent view or DraxProvider (absolute) */ +export interface DraxViewMeasurements extends Position, ViewDimensions { +} +/** Data about a view involved in a Drax event */ +export interface DraxEventViewData { + /** The view's id */ + id: string; + /** The view's parent id, if any */ + parentId?: string; + /** The view's payload for this event */ + payload: any; +} +/** Data about a dragged view involved in a Drax event */ +export interface DraxEventDraggedViewData extends DraxEventViewData { + /** The ratio of the drag translation to the dimensions of the view */ + dragTranslationRatio: Position; + /** The relative offset of the drag point from the view */ + dragOffset: Position; + /** The relative offset of where the view was grabbed */ + grabOffset: Position; + /** The relative offset/dimensions ratio of where the view was grabbed */ + grabOffsetRatio: Position; + /** The position in absolute coordinates of the dragged hover view (dragAbsolutePosition - grabOffset) */ + hoverPosition: Animated.ValueXY; +} +/** Data about a receiver view involved in a Drax event */ +export interface DraxEventReceiverViewData extends DraxEventViewData { + /** The relative offset of the drag point in the receiving view */ + receiveOffset: Position; + /** The relative offset/dimensions ratio of the drag point in the receiving view */ + receiveOffsetRatio: Position; +} +/** Data about a Drax drag event */ +export interface DraxDragEventData { + /** Position of the drag event in absolute coordinates */ + dragAbsolutePosition: Position; + /** The absolute drag distance from where the drag started */ + dragTranslation: Position; + /** Data about the dragged view */ + dragged: DraxEventDraggedViewData; +} +/** Supplemental type for adding a cancelled flag */ +export interface WithCancelledFlag { + /** True if the event was cancelled */ + cancelled: boolean; +} +/** Predicate for checking if something has a cancelled flag */ +export declare const isWithCancelledFlag: (something: any) => something is WithCancelledFlag; +/** Data about a Drax drag end event */ +export interface DraxDragEndEventData extends DraxDragEventData, WithCancelledFlag { +} +/** Data about a Drax drag event that involves a receiver */ +export interface DraxDragWithReceiverEventData extends DraxDragEventData { + /** The receiver for the drag event */ + receiver: DraxEventReceiverViewData; +} +/** Data about a Drax drag/receive end event */ +export interface DraxDragWithReceiverEndEventData extends DraxDragWithReceiverEventData, WithCancelledFlag { +} +/** Data about a Drax snapback, used for custom animations */ +export interface DraxSnapbackData { + hoverPosition: Animated.ValueXY; + toValue: Position; + delay: number; + duration: number; +} +/** Data about a Drax monitor event */ +export interface DraxMonitorEventData extends DraxDragEventData { + /** The receiver for the monitor event, if any */ + receiver?: DraxEventReceiverViewData; + /** Event position relative to the monitor */ + monitorOffset: Position; + /** Event position/dimensions ratio relative to the monitor */ + monitorOffsetRatio: Position; +} +/** Data about a Drax monitor drag end event */ +export interface DraxMonitorEndEventData extends DraxMonitorEventData, WithCancelledFlag { +} +/** Data about a Drax monitor drag-drop event */ +export interface DraxMonitorDragDropEventData extends Required { +} +/** Preset values for specifying snapback targets without a Position */ +export declare enum DraxSnapbackTargetPreset { + Default = 0, + None = 1 +} +/** Target for snapback hover view release animation: none, default, or specified Position */ +export declare type DraxSnapbackTarget = DraxSnapbackTargetPreset | Position; +/** + * Response type for Drax protocol callbacks involving end of a drag, + * allowing override of default release snapback behavior. + */ +export declare type DraxProtocolDragEndResponse = void | DraxSnapbackTarget; +/** Props provided to an internal render function for a hovering copy of a Drax view */ +export interface DraxInternalRenderHoverViewProps { + /** Key of the hover view React node */ + key: string; + /** Hover position of the view */ + hoverPosition: Animated.ValueXY; + /** State for the view */ + viewState: DraxViewState; + /** Drax tracking status */ + trackingStatus: DraxTrackingStatus; + /** Dimensions for the view */ + dimensions: ViewDimensions; +} +/** Props provided to a render function for a Drax view */ +export interface DraxRenderContentProps { + /** State for the view, if available */ + viewState?: DraxViewState; + /** Drax tracking status */ + trackingStatus: DraxTrackingStatus; + /** Is this a hovering copy of the view? */ + hover: boolean; + /** React children of the DraxView */ + children: ReactNode; + /** Dimensions for the view, if available */ + dimensions?: ViewDimensions; +} +/** Props provided to a render function for a hovering copy of a Drax view, compatible with DraxRenderContentProps */ +export interface DraxRenderHoverContentProps extends Required { +} +/** Callback protocol for communicating Drax events to views */ +export interface DraxProtocol { + /** Called in the dragged view when a drag action begins */ + onDragStart?: (data: DraxDragEventData) => void; + /** Called in the dragged view repeatedly while dragged, not over any receiver */ + onDrag?: (data: DraxDragEventData) => void; + /** Called in the dragged view when initially dragged over a new receiver */ + onDragEnter?: (data: DraxDragWithReceiverEventData) => void; + /** Called in the dragged view repeatedly while dragged over a receiver */ + onDragOver?: (data: DraxDragWithReceiverEventData) => void; + /** Called in the dragged view when dragged off of a receiver */ + onDragExit?: (data: DraxDragWithReceiverEventData) => void; + /** Called in the dragged view when drag ends not over any receiver or is cancelled */ + onDragEnd?: (data: DraxDragEndEventData) => DraxProtocolDragEndResponse; + /** Called in the dragged view when drag ends over a receiver */ + onDragDrop?: (data: DraxDragWithReceiverEventData) => DraxProtocolDragEndResponse; + /** Called in the dragged view when drag release snapback ends */ + onSnapbackEnd?: () => void; + /** Called in the receiver view each time an item is initially dragged over it */ + onReceiveDragEnter?: (data: DraxDragWithReceiverEventData) => void; + /** Called in the receiver view repeatedly while an item is dragged over it */ + onReceiveDragOver?: (data: DraxDragWithReceiverEventData) => void; + /** Called in the receiver view when item is dragged off of it or drag is cancelled */ + onReceiveDragExit?: (data: DraxDragWithReceiverEndEventData) => void; + /** Called in the receiver view when drag ends over it */ + onReceiveDragDrop?: (data: DraxDragWithReceiverEventData) => DraxProtocolDragEndResponse; + /** Called in the monitor view when a drag action begins over it */ + onMonitorDragStart?: (data: DraxMonitorEventData) => void; + /** Called in the monitor view each time an item is initially dragged over it */ + onMonitorDragEnter?: (data: DraxMonitorEventData) => void; + /** Called in the monitor view repeatedly while an item is dragged over it */ + onMonitorDragOver?: (data: DraxMonitorEventData) => void; + /** Called in the monitor view when item is dragged off of it */ + onMonitorDragExit?: (data: DraxMonitorEventData) => void; + /** Called in the monitor view when drag ends over it while not over any receiver or drag is cancelled */ + onMonitorDragEnd?: (data: DraxMonitorEndEventData) => DraxProtocolDragEndResponse; + /** Called in the monitor view when drag ends over it while over a receiver */ + onMonitorDragDrop?: (data: DraxMonitorDragDropEventData) => DraxProtocolDragEndResponse; + /** Whether or not to animate hover view snapback after drag release, defaults to true */ + animateSnapback?: boolean; + /** Delay in ms before hover view snapback begins after drag is released */ + snapbackDelay?: number; + /** Duration in ms for hover view snapback to complete */ + snapbackDuration?: number; + /** Function returning custom hover view snapback animation */ + snapbackAnimator?: (data: DraxSnapbackData) => Animated.CompositeAnimation; + /** Payload that will be delivered to receiver views when this view is dragged; overrides `payload` */ + dragPayload?: any; + /** Payload that will be delievered to dragged views when this view receives them; overrides `payload` */ + receiverPayload?: any; + /** Whether the view can be dragged */ + draggable: boolean; + /** Whether the view can receive drags */ + receptive: boolean; + /** Whether the view can monitor drags */ + monitoring: boolean; + /** If true, lock drag's x-position */ + lockDragXPosition?: boolean; + /** If true, lock drag's y position */ + lockDragYPosition?: boolean; + /** Function used internally for rendering hovering copy of view when dragged/released */ + internalRenderHoverView?: (props: DraxInternalRenderHoverViewProps) => ReactNode; +} +/** Props for components implementing the protocol */ +export interface DraxProtocolProps extends Partial> { + /** Convenience prop to provide one value for both `dragPayload` and `receiverPayload` */ + payload?: any; +} +/** The states a dragged view can be in */ +export declare enum DraxViewDragStatus { + /** View is not being dragged */ + Inactive = 0, + /** View is being actively dragged; an active drag touch began in this view */ + Dragging = 1, + /** View has been released but has not yet snapped back to inactive */ + Released = 2 +} +/** The states a receiver view can be in */ +export declare enum DraxViewReceiveStatus { + /** View is not receiving a drag */ + Inactive = 0, + /** View is receiving a drag; an active drag touch point is currently over this view */ + Receiving = 1 +} +/** Information about a view, used internally by the Drax provider */ +export interface DraxViewData { + /** The view's Drax parent view id, if nested */ + parentId?: string; + /** The view's scroll position ref, if it is a scrollable parent view */ + scrollPositionRef?: RefObject; + /** The view's protocol callbacks and data */ + protocol: DraxProtocol; + /** The view's measurements for bounds checking */ + measurements?: DraxViewMeasurements; +} +/** Information about a view, plus its clipped absolute measurements */ +export interface DraxAbsoluteViewData extends Omit, Required> { + /** Absolute measurements for view */ + absoluteMeasurements: DraxViewMeasurements; +} +/** Wrapper of id and absolute data for a view */ +export interface DraxAbsoluteViewEntry { + /** The view's unique identifier */ + id: string; + data: DraxAbsoluteViewData; +} +/** Wrapper of id and absolute data for a view found when checking a position */ +export interface DraxFoundAbsoluteViewEntry extends DraxAbsoluteViewEntry { + /** Position, relative to the view, of the touch for which it was found */ + relativePosition: Position; + /** Position/dimensions ratio, relative to the view, of the touch for which it was found */ + relativePositionRatio: Position; +} +/** Tracking information about the current receiver, used internally by the Drax provider */ +export interface DraxTrackingReceiver { + /** View id of the current receiver */ + receiverId: string; + /** The relative offset of the drag point in the receiving view */ + receiveOffset: Position; + /** The relative offset/dimensions ratio of the drag point in the receiving view */ + receiveOffsetRatio: Position; +} +/** Tracking information about the current drag, used internally by the Drax provider */ +export interface DraxTrackingDrag { + /** View id of the dragged view */ + draggedId: string; + /** Start position of the drag in absolute coordinates */ + absoluteStartPosition: Position; + /** Start position of the drag relative to dragged view's immediate parent */ + parentStartPosition: Position; + /** The position in absolute coordinates of the drag point */ + dragAbsolutePosition: Position; + /** The absolute drag distance from where the drag started (dragAbsolutePosition - absoluteStartPosition) */ + dragTranslation: Position; + /** The ratio of the drag translation to the dimensions of the view */ + dragTranslationRatio: Position; + /** The relative offset of the drag point from the view */ + dragOffset: Position; + /** The relative offset within the dragged view of where it was grabbed */ + grabOffset: Position; + /** The relative offset/dimensions ratio within the dragged view of where it was grabbed */ + grabOffsetRatio: Position; + /** The position in absolute coordinates of the dragged hover view (dragAbsolutePosition - grabOffset) */ + hoverPosition: Animated.ValueXY; + /** Tracking information about the current drag receiver, if any */ + receiver?: DraxTrackingReceiver; + /** View ids of monitors that the drag is currently over */ + monitorIds: string[]; +} +/** Tracking information about a view that was released and is snapping back */ +export interface DraxTrackingRelease { + /** View id of the released view */ + viewId: string; + /** The position in absolute coordinates of the released hover view */ + hoverPosition: Animated.ValueXY; +} +/** Tracking status for reference in views */ +export interface DraxTrackingStatus { + /** Is any view being dragged? */ + dragging: boolean; + /** Is any view receiving a drag? */ + receiving: boolean; +} +/** Render-related state for a registered view */ +export interface DraxViewState { + /** Current drag status of the view: Dragged, Released, or Inactive */ + dragStatus: DraxViewDragStatus; + /** If being dragged, the position in absolute coordinates of the drag point */ + dragAbsolutePosition?: Position; + /** If being dragged, the absolute drag distance from where the drag started (dragAbsolutePosition - absoluteStartPosition) */ + dragTranslation?: Position; + /** If being dragged, the ratio of the drag translation to the dimensions of the view */ + dragTranslationRatio?: Position; + /** If being dragged, the relative offset of the drag point from the view */ + dragOffset?: Position; + /** If being dragged, the relative offset of where the view was grabbed */ + grabOffset?: Position; + /** If being dragged, the relative offset/dimensions ratio of where the view was grabbed */ + grabOffsetRatio?: Position; + /** The position in absolute coordinates of the dragged hover view (dragAbsolutePosition - grabOffset) */ + hoverPosition?: Animated.ValueXY; + /** Data about the receiver this view is being dragged over, if any */ + draggingOverReceiver?: DraxEventViewData; + /** Current receive status of the view: Receiving or Inactive */ + receiveStatus: DraxViewReceiveStatus; + /** If receiving a drag, the relative offset of the drag point in the view */ + receiveOffset?: Position; + /** If receiving a drag, the relative offset/dimensions ratio of the drag point in the view */ + receiveOffsetRatio?: Position; + /** Data about the dragged item this view is receiving, if any */ + receivingDrag?: DraxEventViewData; +} +/** Drax provider render state; maintains render-related data */ +export interface DraxState { + /** Render-related state for all registered views, keyed by their unique identifiers */ + viewStateById: { + /** Render-related state for a registered view, keyed by its unique identifier */ + [id: string]: DraxViewState; + }; + /** Tracking status indicating whether anything is being dragged/received */ + trackingStatus: DraxTrackingStatus; +} +/** Payload to start tracking a drag */ +export interface StartDragPayload { + /** Absolute position of where the drag started */ + dragAbsolutePosition: Position; + /** Position relative to the dragged view's immediate parent where the drag started */ + dragParentPosition: Position; + /** The dragged view's unique identifier */ + draggedId: string; + /** The relative offset within the view of where it was grabbed */ + grabOffset: Position; + /** The relative offset/dimensions ratio within the view of where it was grabbed */ + grabOffsetRatio: Position; +} +/** Payload for registering a Drax view */ +export interface RegisterViewPayload { + /** The view's unique identifier */ + id: string; + /** The view's Drax parent view id, if nested */ + parentId?: string; + /** The view's scroll position ref, if it is a scrollable parent view */ + scrollPositionRef?: RefObject; +} +/** Payload for unregistering a Drax view */ +export interface UnregisterViewPayload { + /** The view's unique identifier */ + id: string; +} +/** Payload for updating the protocol values of a registered view */ +export interface UpdateViewProtocolPayload { + /** The view's unique identifier */ + id: string; + /** The current protocol values for the view */ + protocol: DraxProtocol; +} +/** Payload for reporting the latest measurements of a view after layout */ +export interface UpdateViewMeasurementsPayload { + /** The view's unique identifier */ + id: string; + /** The view's measurements */ + measurements: DraxViewMeasurements | undefined; +} +/** Payload used by Drax provider internally for creating a view's state */ +export interface CreateViewStatePayload { + /** The view's unique identifier */ + id: string; +} +/** Payload used by Drax provider internally for updating a view's state */ +export interface UpdateViewStatePayload { + /** The view's unique identifier */ + id: string; + /** The view state update */ + viewStateUpdate: Partial; +} +/** Payload used by Drax provider internally for deleting a view's state */ +export interface DeleteViewStatePayload { + /** The view's unique identifier */ + id: string; +} +/** Payload used by Drax provider internally for updating tracking status */ +export interface UpdateTrackingStatusPayload extends Partial { +} +/** Collection of Drax state action creators */ +export interface DraxStateActionCreators { + createViewState: PayloadActionCreator<'createViewState', CreateViewStatePayload>; + updateViewState: PayloadActionCreator<'updateViewState', UpdateViewStatePayload>; + deleteViewState: PayloadActionCreator<'deleteViewState', DeleteViewStatePayload>; + updateTrackingStatus: PayloadActionCreator<'updateTrackingStatus', UpdateTrackingStatusPayload>; +} +/** Dispatchable Drax state action */ +export declare type DraxStateAction = ActionType; +/** Dispatcher of Drax state actions */ +export declare type DraxStateDispatch = (action: DraxStateAction) => void; +/** Drax provider internal registry; maintains view data and tracks drags, updating state */ +export interface DraxRegistry { + /** A list of the unique identifiers of the registered views, in order of registration */ + viewIds: string[]; + /** Data about all registered views, keyed by their unique identifiers */ + viewDataById: { + /** Data about a registered view, keyed by its unique identifier */ + [id: string]: DraxViewData; + }; + /** Information about the current drag, if any */ + drag?: DraxTrackingDrag; + /** A list of the unique identifiers of tracked drag releases, in order of release */ + releaseIds: string[]; + /** Released drags that are snapping back, keyed by unique release identifier */ + releaseById: { + [releaseId: string]: DraxTrackingRelease; + }; + /** Drax state dispatch function */ + stateDispatch: DraxStateDispatch; +} +/** Context value used internally by Drax provider */ +export interface DraxContextValue { + /** Get a Drax view state by view id, if it exists */ + getViewState: (id: string) => DraxViewState | undefined; + /** Get current Drax tracking status */ + getTrackingStatus: () => DraxTrackingStatus; + /** Register a Drax view */ + registerView: (payload: RegisterViewPayload) => void; + /** Unregister a Drax view */ + unregisterView: (payload: UnregisterViewPayload) => void; + /** Update protocol for a registered Drax view */ + updateViewProtocol: (payload: UpdateViewProtocolPayload) => void; + /** Update view measurements for a registered Drax view */ + updateViewMeasurements: (payload: UpdateViewMeasurementsPayload) => void; + /** Handle gesture state change for a registered Drax view */ + handleGestureStateChange: (id: string, event: DraxGestureStateChangeEvent) => void; + /** Handle gesture event for a registered Drax view */ + handleGestureEvent: (id: string, event: DraxGestureEvent) => void; + /** Root node handle ref for the Drax provider, for measuring non-parented views in relation to */ + rootNodeHandleRef: RefObject; + /** Drax parent view for all views under this context, when nesting */ + parent?: DraxParentView; +} +/** Optional props that can be passed to a DraxProvider to modify its behavior */ +export interface DraxProviderProps { + style?: StyleProp; + debug?: boolean; + children?: ReactNode; +} +/** Props that are passed to a DraxSubprovider, used internally for nesting views */ +export interface DraxSubproviderProps { + /** Drax parent view for all views under this subprovider, when nesting */ + parent: DraxParentView; +} +/** Methods provided by a DraxView when registered externally */ +export interface DraxViewRegistration { + id: string; + measure: (measurementHandler?: DraxViewMeasurementHandler) => void; +} +/** Information about the parent of a nested DraxView, primarily used for scrollable parent views */ +export interface DraxParentView { + /** Drax view id of the parent */ + id: string; + /** Ref to node handle of the parent, for measuring relative to */ + nodeHandleRef: RefObject; +} +/** Function that receives a Drax view measurement */ +export interface DraxViewMeasurementHandler { + (measurements: DraxViewMeasurements | undefined): void; +} +/** Layout-related style keys that are omitted from hover view styles */ +export declare type LayoutStyleKey = ('margin' | 'marginHorizontal' | 'marginVertical' | 'marginLeft' | 'marginRight' | 'marginTop' | 'marginBottom' | 'marginStart' | 'marginEnd' | 'left' | 'right' | 'top' | 'bottom' | 'flex' | 'flexBasis' | 'flexDirection' | 'flexGrow' | 'flexShrink'); +/** Style for a Animated.View used for a hover view */ +export declare type AnimatedViewStyleWithoutLayout = Omit, LayoutStyleKey>; +/** Style prop for a Animated.View used for a hover view */ +export declare type AnimatedViewStylePropWithoutLayout = StyleProp; +/** Style-related props for a Drax view */ +export interface DraxViewStyleProps { + /** Custom style prop to allow animated values */ + style?: StyleProp>; + /** Additional view style applied while this view is not being dragged or released */ + dragInactiveStyle?: StyleProp>; + /** Additional view style applied while this view is being dragged */ + draggingStyle?: StyleProp>; + /** Additional view style applied while this view is being dragged over a receiver */ + draggingWithReceiverStyle?: StyleProp>; + /** Additional view style applied while this view is being dragged NOT over a receiver */ + draggingWithoutReceiverStyle?: StyleProp>; + /** Additional view style applied while this view has just been released from a drag */ + dragReleasedStyle?: StyleProp>; + /** Additional view style applied to the hovering copy of this view during drag/release */ + hoverStyle?: AnimatedViewStylePropWithoutLayout; + /** Additional view style applied to the hovering copy of this view while dragging */ + hoverDraggingStyle?: AnimatedViewStylePropWithoutLayout; + /** Additional view style applied to the hovering copy of this view while dragging over a receiver */ + hoverDraggingWithReceiverStyle?: AnimatedViewStylePropWithoutLayout; + /** Additional view style applied to the hovering copy of this view while dragging NOT over a receiver */ + hoverDraggingWithoutReceiverStyle?: AnimatedViewStylePropWithoutLayout; + /** Additional view style applied to the hovering copy of this view when just released */ + hoverDragReleasedStyle?: AnimatedViewStylePropWithoutLayout; + /** Additional view style applied while this view is not receiving a drag */ + receiverInactiveStyle?: StyleProp>; + /** Additional view style applied while this view is receiving a drag */ + receivingStyle?: StyleProp>; + /** Additional view style applied to this view while any other view is being dragged */ + otherDraggingStyle?: StyleProp>; + /** Additional view style applied to this view while any other view is being dragged over a receiver */ + otherDraggingWithReceiverStyle?: StyleProp>; + /** Additional view style applied to this view while any other view is being dragged NOT over a receiver */ + otherDraggingWithoutReceiverStyle?: StyleProp>; +} +/** Custom render function for content of a DraxView */ +export interface DraxViewRenderContent { + (props: DraxRenderContentProps): ReactNode; +} +/** Custom render function for content of hovering copy of a DraxView */ +export interface DraxViewRenderHoverContent { + (props: DraxRenderHoverContentProps): ReactNode; +} +/** Props for a DraxView; combines protocol props and standard view props */ +export interface DraxViewProps extends Omit, DraxProtocolProps, DraxViewStyleProps { + /** Custom render function for content of this view */ + renderContent?: DraxViewRenderContent; + /** Custom render function for content of hovering copy of this view, defaults to renderContent */ + renderHoverContent?: DraxViewRenderHoverContent; + /** If true, do not render hover view copies for this view when dragging */ + noHover?: boolean; + /** For external registration of this view, to access internal methods, similar to a ref */ + registration?: (registration: DraxViewRegistration | undefined) => void; + /** For receiving view measurements externally */ + onMeasure?: DraxViewMeasurementHandler; + /** Unique Drax view id, auto-generated if omitted */ + id?: string; + /** Drax parent view, if nesting */ + parent?: DraxParentView; + /** If true, treat this view as a Drax parent view for nested children */ + isParent?: boolean; + /** The view's scroll position ref, if it is a scrollable parent view */ + scrollPositionRef?: RefObject; + /** Time in milliseconds view needs to be pressed before drag starts */ + longPressDelay?: number; +} +/** Auto-scroll direction used internally by DraxScrollView and DraxList */ +export declare enum AutoScrollDirection { + /** Auto-scrolling back toward the beginning of list */ + Back = -1, + /** Not auto-scrolling */ + None = 0, + /** Auto-scrolling forward toward the end of the list */ + Forward = 1 +} +/** Auto-scroll state used internally by DraxScrollView */ +export interface AutoScrollState { + x: AutoScrollDirection; + y: AutoScrollDirection; +} +/** Props for auto-scroll options, used by DraxScrollView and DraxList */ +export interface DraxAutoScrollProps { + autoScrollIntervalLength?: number; + autoScrollJumpRatio?: number; + autoScrollBackThreshold?: number; + autoScrollForwardThreshold?: number; +} +/** Props for a DraxScrollView; extends standard ScrollView props */ +export interface DraxScrollViewProps extends ScrollViewProps, DraxAutoScrollProps { + /** Unique drax view id, auto-generated if omitted */ + id?: string; +} +/** DraxList item being dragged */ +export interface DraxListDraggedItemData { + index: number; + item?: TItem; +} +/** Event data for when a list item reorder drag action begins */ +export interface DraxListOnItemDragStartEventData extends DraxDragEventData, DraxListDraggedItemData { +} +/** Event data for when a list item position (index) changes during a reorder drag */ +export interface DraxListOnItemDragPositionChangeEventData extends DraxMonitorEventData, DraxListDraggedItemData { + toIndex: number | undefined; + previousIndex: number | undefined; +} +/** Event data for when a list item reorder drag action ends */ +export interface DraxListOnItemDragEndEventData extends DraxMonitorEventData, WithCancelledFlag, DraxListDraggedItemData { + toIndex?: number; + toItem?: TItem; +} +/** Event data for when an item is released in a new position within a DraxList, reordering the list */ +export interface DraxListOnItemReorderEventData { + fromItem: TItem; + fromIndex: number; + toItem: TItem; + toIndex: number; +} +/** Render function for content of a DraxList item's DraxView */ +export interface DraxListRenderItemContent { + (info: ListRenderItemInfo, props: DraxRenderContentProps): ReactNode; +} +/** Render function for content of a DraxList item's hovering copy */ +export interface DraxListRenderItemHoverContent { + (info: ListRenderItemInfo, props: DraxRenderHoverContentProps): ReactNode; +} +/** Callback handler for when a list item is moved within a DraxList, reordering the list */ +export interface DraxListOnItemReorder { + (eventData: DraxListOnItemReorderEventData): void; +} +/** Props for a DraxList; extends standard FlatList props */ +export interface DraxListProps extends Omit, 'renderItem'>, DraxAutoScrollProps { + /** Unique drax view id, auto-generated if omitted */ + id?: string; + /** Style prop for the underlying FlatList */ + flatListStyle?: StyleProp; + /** Style props to apply to all DraxView items in the list */ + itemStyles?: DraxViewStyleProps; + /** Render function for content of an item's DraxView */ + renderItemContent: DraxListRenderItemContent; + /** Render function for content of an item's hovering copy, defaults to renderItemContent */ + renderItemHoverContent?: DraxListRenderItemHoverContent; + /** Callback handler for when a list item reorder drag action begins */ + onItemDragStart?: (eventData: DraxListOnItemDragStartEventData) => void; + /** Callback handler for when a list item position (index) changes during a reorder drag */ + onItemDragPositionChange?: (eventData: DraxListOnItemDragPositionChangeEventData) => void; + /** Callback handler for when a list item reorder drag action ends */ + onItemDragEnd?: (eventData: DraxListOnItemDragEndEventData) => void; + /** Callback handler for when a list item is moved within the list, reordering the list */ + onItemReorder?: DraxListOnItemReorder; + /** Can the list be reordered by dragging items? Defaults to true if onItemReorder is set. */ + reorderable?: boolean; + /** Can the items be dragged? Defaults to true. */ + itemsDraggable?: boolean; + /** If true, lock item drags to the list's main axis */ + lockItemDragsToMainAxis?: boolean; + /** Time in milliseconds view needs to be pressed before drag starts */ + longPressDelay?: number; + /** Function that receives an item and returns a list of DraxViewProps to apply to that item's DraxView */ + viewPropsExtractor?: (item: TItem) => Partial; +} diff --git a/build/types.js b/build/types.js new file mode 100644 index 0000000..2689479 --- /dev/null +++ b/build/types.js @@ -0,0 +1,43 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.AutoScrollDirection = exports.DraxViewReceiveStatus = exports.DraxViewDragStatus = exports.DraxSnapbackTargetPreset = exports.isWithCancelledFlag = exports.isPosition = void 0; +/** Predicate for checking if something is a Position */ +const isPosition = (something) => (typeof something === 'object' && something !== null && typeof something.x === 'number' && typeof something.y === 'number'); +exports.isPosition = isPosition; +/** Predicate for checking if something has a cancelled flag */ +const isWithCancelledFlag = (something) => (typeof something === 'object' && something !== null && typeof something.cancelled === 'boolean'); +exports.isWithCancelledFlag = isWithCancelledFlag; +/** Preset values for specifying snapback targets without a Position */ +var DraxSnapbackTargetPreset; +(function (DraxSnapbackTargetPreset) { + DraxSnapbackTargetPreset[DraxSnapbackTargetPreset["Default"] = 0] = "Default"; + DraxSnapbackTargetPreset[DraxSnapbackTargetPreset["None"] = 1] = "None"; +})(DraxSnapbackTargetPreset = exports.DraxSnapbackTargetPreset || (exports.DraxSnapbackTargetPreset = {})); +/** The states a dragged view can be in */ +var DraxViewDragStatus; +(function (DraxViewDragStatus) { + /** View is not being dragged */ + DraxViewDragStatus[DraxViewDragStatus["Inactive"] = 0] = "Inactive"; + /** View is being actively dragged; an active drag touch began in this view */ + DraxViewDragStatus[DraxViewDragStatus["Dragging"] = 1] = "Dragging"; + /** View has been released but has not yet snapped back to inactive */ + DraxViewDragStatus[DraxViewDragStatus["Released"] = 2] = "Released"; +})(DraxViewDragStatus = exports.DraxViewDragStatus || (exports.DraxViewDragStatus = {})); +/** The states a receiver view can be in */ +var DraxViewReceiveStatus; +(function (DraxViewReceiveStatus) { + /** View is not receiving a drag */ + DraxViewReceiveStatus[DraxViewReceiveStatus["Inactive"] = 0] = "Inactive"; + /** View is receiving a drag; an active drag touch point is currently over this view */ + DraxViewReceiveStatus[DraxViewReceiveStatus["Receiving"] = 1] = "Receiving"; +})(DraxViewReceiveStatus = exports.DraxViewReceiveStatus || (exports.DraxViewReceiveStatus = {})); +/** Auto-scroll direction used internally by DraxScrollView and DraxList */ +var AutoScrollDirection; +(function (AutoScrollDirection) { + /** Auto-scrolling back toward the beginning of list */ + AutoScrollDirection[AutoScrollDirection["Back"] = -1] = "Back"; + /** Not auto-scrolling */ + AutoScrollDirection[AutoScrollDirection["None"] = 0] = "None"; + /** Auto-scrolling forward toward the end of the list */ + AutoScrollDirection[AutoScrollDirection["Forward"] = 1] = "Forward"; +})(AutoScrollDirection = exports.AutoScrollDirection || (exports.AutoScrollDirection = {})); From 5ca77f0ed9bcdd694d58b5f72bf7a63b43a1e647 Mon Sep 17 00:00:00 2001 From: Ofer Sadgat Date: Thu, 21 Nov 2024 12:55:06 -0800 Subject: [PATCH 3/4] allow getting access to view ref --- build/DraxView.d.ts | 2 +- build/DraxView.js | 5 ++++- build/types.d.ts | 4 +++- src/DraxView.tsx | 4 ++++ src/types.ts | 4 ++++ 5 files changed, 16 insertions(+), 3 deletions(-) diff --git a/build/DraxView.d.ts b/build/DraxView.d.ts index 9622efd..d89a58a 100644 --- a/build/DraxView.d.ts +++ b/build/DraxView.d.ts @@ -1,3 +1,3 @@ import { PropsWithChildren } from 'react'; import { DraxViewProps } from './types'; -export declare const DraxView: ({ onDragStart, onDrag, onDragEnter, onDragOver, onDragExit, onDragEnd, onDragDrop, onSnapbackEnd, onReceiveDragEnter, onReceiveDragOver, onReceiveDragExit, onReceiveDragDrop, onMonitorDragStart, onMonitorDragEnter, onMonitorDragOver, onMonitorDragExit, onMonitorDragEnd, onMonitorDragDrop, animateSnapback, snapbackDelay, snapbackDuration, snapbackAnimator, payload, dragPayload, receiverPayload, style, dragInactiveStyle, draggingStyle, draggingWithReceiverStyle, draggingWithoutReceiverStyle, dragReleasedStyle, hoverStyle, hoverDraggingStyle, hoverDraggingWithReceiverStyle, hoverDraggingWithoutReceiverStyle, hoverDragReleasedStyle, receiverInactiveStyle, receivingStyle, otherDraggingStyle, otherDraggingWithReceiverStyle, otherDraggingWithoutReceiverStyle, renderContent, renderHoverContent, registration, onMeasure, scrollPositionRef, lockDragXPosition, lockDragYPosition, children, noHover, isParent, longPressDelay, id: idProp, parent: parentProp, draggable: draggableProp, receptive: receptiveProp, monitoring: monitoringProp, ...props }: PropsWithChildren) => JSX.Element; +export declare const DraxView: ({ onDragStart, onDrag, onDragEnter, onDragOver, onDragExit, onDragEnd, onDragDrop, onSnapbackEnd, onReceiveDragEnter, onReceiveDragOver, onReceiveDragExit, onReceiveDragDrop, onMonitorDragStart, onMonitorDragEnter, onMonitorDragOver, onMonitorDragExit, onMonitorDragEnd, onMonitorDragDrop, animateSnapback, snapbackDelay, snapbackDuration, snapbackAnimator, payload, dragPayload, receiverPayload, style, dragInactiveStyle, draggingStyle, draggingWithReceiverStyle, draggingWithoutReceiverStyle, dragReleasedStyle, hoverStyle, hoverDraggingStyle, hoverDraggingWithReceiverStyle, hoverDraggingWithoutReceiverStyle, hoverDragReleasedStyle, receiverInactiveStyle, receivingStyle, otherDraggingStyle, otherDraggingWithReceiverStyle, otherDraggingWithoutReceiverStyle, renderContent, renderHoverContent, registration, onMeasure, scrollPositionRef, lockDragXPosition, lockDragYPosition, children, viewRef: inputViewRef, noHover, isParent, longPressDelay, id: idProp, parent: parentProp, draggable: draggableProp, receptive: receptiveProp, monitoring: monitoringProp, ...props }: PropsWithChildren) => JSX.Element; diff --git a/build/DraxView.js b/build/DraxView.js index 07bc812..19b9f91 100644 --- a/build/DraxView.js +++ b/build/DraxView.js @@ -37,7 +37,7 @@ const params_1 = require("./params"); const math_1 = require("./math"); const DraxSubprovider_1 = require("./DraxSubprovider"); const transform_1 = require("./transform"); -const DraxView = ({ onDragStart, onDrag, onDragEnter, onDragOver, onDragExit, onDragEnd, onDragDrop, onSnapbackEnd, onReceiveDragEnter, onReceiveDragOver, onReceiveDragExit, onReceiveDragDrop, onMonitorDragStart, onMonitorDragEnter, onMonitorDragOver, onMonitorDragExit, onMonitorDragEnd, onMonitorDragDrop, animateSnapback, snapbackDelay, snapbackDuration, snapbackAnimator, payload, dragPayload, receiverPayload, style, dragInactiveStyle, draggingStyle, draggingWithReceiverStyle, draggingWithoutReceiverStyle, dragReleasedStyle, hoverStyle, hoverDraggingStyle, hoverDraggingWithReceiverStyle, hoverDraggingWithoutReceiverStyle, hoverDragReleasedStyle, receiverInactiveStyle, receivingStyle, otherDraggingStyle, otherDraggingWithReceiverStyle, otherDraggingWithoutReceiverStyle, renderContent, renderHoverContent, registration, onMeasure, scrollPositionRef, lockDragXPosition, lockDragYPosition, children, noHover = false, isParent = false, longPressDelay = params_1.defaultLongPressDelay, id: idProp, parent: parentProp, draggable: draggableProp, receptive: receptiveProp, monitoring: monitoringProp, ...props }) => { +const DraxView = ({ onDragStart, onDrag, onDragEnter, onDragOver, onDragExit, onDragEnd, onDragDrop, onSnapbackEnd, onReceiveDragEnter, onReceiveDragOver, onReceiveDragExit, onReceiveDragDrop, onMonitorDragStart, onMonitorDragEnter, onMonitorDragOver, onMonitorDragExit, onMonitorDragEnd, onMonitorDragDrop, animateSnapback, snapbackDelay, snapbackDuration, snapbackAnimator, payload, dragPayload, receiverPayload, style, dragInactiveStyle, draggingStyle, draggingWithReceiverStyle, draggingWithoutReceiverStyle, dragReleasedStyle, hoverStyle, hoverDraggingStyle, hoverDraggingWithReceiverStyle, hoverDraggingWithoutReceiverStyle, hoverDragReleasedStyle, receiverInactiveStyle, receivingStyle, otherDraggingStyle, otherDraggingWithReceiverStyle, otherDraggingWithoutReceiverStyle, renderContent, renderHoverContent, registration, onMeasure, scrollPositionRef, lockDragXPosition, lockDragYPosition, children, viewRef: inputViewRef, noHover = false, isParent = false, longPressDelay = params_1.defaultLongPressDelay, id: idProp, parent: parentProp, draggable: draggableProp, receptive: receptiveProp, monitoring: monitoringProp, ...props }) => { // Coalesce protocol props into capabilities. const draggable = draggableProp ?? (dragPayload !== undefined || payload !== undefined @@ -407,6 +407,9 @@ const DraxView = ({ onDragStart, onDrag, onDragEnter, onDragOver, onDragExit, on nodeHandleRef, ]); const setViewRefs = (0, react_1.useCallback)((ref) => { + if (inputViewRef) { + inputViewRef.current = ref; + } viewRef.current = ref; nodeHandleRef.current = ref && (0, react_native_1.findNodeHandle)(ref); }, []); diff --git a/build/types.d.ts b/build/types.d.ts index 9e8c720..ca47805 100644 --- a/build/types.d.ts +++ b/build/types.d.ts @@ -1,5 +1,5 @@ import { RefObject, ReactNode } from 'react'; -import { ViewProps, Animated, FlatListProps, ViewStyle, StyleProp, ScrollViewProps, ListRenderItemInfo } from 'react-native'; +import { ViewProps, Animated, FlatListProps, ViewStyle, StyleProp, ScrollViewProps, ListRenderItemInfo, View } from 'react-native'; import { LongPressGestureHandlerStateChangeEvent, LongPressGestureHandlerGestureEvent } from 'react-native-gesture-handler'; import { PayloadActionCreator, ActionType } from 'typesafe-actions'; /** Gesture state change event expected by Drax handler */ @@ -549,6 +549,8 @@ export interface DraxViewProps extends Omit, DraxProtocolPro registration?: (registration: DraxViewRegistration | undefined) => void; /** For receiving view measurements externally */ onMeasure?: DraxViewMeasurementHandler; + /** For receiving view measurements externally */ + viewRef?: React.MutableRefObject; /** Unique Drax view id, auto-generated if omitted */ id?: string; /** Drax parent view, if nesting */ diff --git a/src/DraxView.tsx b/src/DraxView.tsx index ef96586..3d00f2f 100644 --- a/src/DraxView.tsx +++ b/src/DraxView.tsx @@ -89,6 +89,7 @@ export const DraxView = ( lockDragXPosition, lockDragYPosition, children, + viewRef: inputViewRef, noHover = false, isParent = false, longPressDelay = defaultLongPressDelay, @@ -595,6 +596,9 @@ export const DraxView = ( const setViewRefs = useCallback( (ref: View | null) => { + if (inputViewRef){ + inputViewRef.current = ref; + } viewRef.current = ref; nodeHandleRef.current = ref && findNodeHandle(ref); }, diff --git a/src/types.ts b/src/types.ts index 399f279..058b093 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,6 +7,7 @@ import { StyleProp, ScrollViewProps, ListRenderItemInfo, + View, } from 'react-native'; import { LongPressGestureHandlerStateChangeEvent, @@ -718,6 +719,9 @@ export interface DraxViewProps extends Omit, DraxProtocolPro /** For receiving view measurements externally */ onMeasure?: DraxViewMeasurementHandler; + /** For receiving view measurements externally */ + viewRef?: React.MutableRefObject; + /** Unique Drax view id, auto-generated if omitted */ id?: string; From 6f6395d7365649000d5a9c81070116dc55e8ee87 Mon Sep 17 00:00:00 2001 From: Ofer Sadgat Date: Thu, 21 Nov 2024 15:06:05 -0800 Subject: [PATCH 4/4] allow ref to be function --- build/DraxView.js | 7 ++++++- build/types.d.ts | 2 +- src/DraxView.tsx | 6 +++++- src/types.ts | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/build/DraxView.js b/build/DraxView.js index 19b9f91..abd893c 100644 --- a/build/DraxView.js +++ b/build/DraxView.js @@ -408,7 +408,12 @@ const DraxView = ({ onDragStart, onDrag, onDragEnter, onDragOver, onDragExit, on ]); const setViewRefs = (0, react_1.useCallback)((ref) => { if (inputViewRef) { - inputViewRef.current = ref; + if (typeof (inputViewRef) === 'function') { + inputViewRef(ref); + } + else { + inputViewRef.current = ref; + } } viewRef.current = ref; nodeHandleRef.current = ref && (0, react_native_1.findNodeHandle)(ref); diff --git a/build/types.d.ts b/build/types.d.ts index ca47805..734033c 100644 --- a/build/types.d.ts +++ b/build/types.d.ts @@ -550,7 +550,7 @@ export interface DraxViewProps extends Omit, DraxProtocolPro /** For receiving view measurements externally */ onMeasure?: DraxViewMeasurementHandler; /** For receiving view measurements externally */ - viewRef?: React.MutableRefObject; + viewRef?: React.MutableRefObject | ((viewRef: View | null) => void); /** Unique Drax view id, auto-generated if omitted */ id?: string; /** Drax parent view, if nesting */ diff --git a/src/DraxView.tsx b/src/DraxView.tsx index 3d00f2f..776863b 100644 --- a/src/DraxView.tsx +++ b/src/DraxView.tsx @@ -597,7 +597,11 @@ export const DraxView = ( const setViewRefs = useCallback( (ref: View | null) => { if (inputViewRef){ - inputViewRef.current = ref; + if (typeof(inputViewRef) === 'function'){ + inputViewRef(ref); + } else { + inputViewRef.current = ref; + } } viewRef.current = ref; nodeHandleRef.current = ref && findNodeHandle(ref); diff --git a/src/types.ts b/src/types.ts index 058b093..7b796fc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -720,7 +720,7 @@ export interface DraxViewProps extends Omit, DraxProtocolPro onMeasure?: DraxViewMeasurementHandler; /** For receiving view measurements externally */ - viewRef?: React.MutableRefObject; + viewRef?: React.MutableRefObject | ((viewRef: View|null) => void); /** Unique Drax view id, auto-generated if omitted */ id?: string;