react-native-drax 0.11.0-alpha.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +1 -1
- package/README.md +390 -227
- package/lib/module/DebugOverlay.js +121 -0
- package/lib/module/DebugOverlay.js.map +1 -0
- package/lib/module/Drax.js +36 -0
- package/lib/module/Drax.js.map +1 -0
- package/lib/module/DraxContext.js +6 -0
- package/lib/module/DraxContext.js.map +1 -0
- package/lib/module/DraxHandle.js +47 -0
- package/lib/module/DraxHandle.js.map +1 -0
- package/lib/module/DraxHandleContext.js +11 -0
- package/lib/module/DraxHandleContext.js.map +1 -0
- package/lib/module/DraxList.js +108 -0
- package/lib/module/DraxList.js.map +1 -0
- package/lib/module/DraxProvider.js +203 -0
- package/lib/module/DraxProvider.js.map +1 -0
- package/lib/module/DraxScrollView.js +167 -0
- package/lib/module/DraxScrollView.js.map +1 -0
- package/lib/module/DraxSubprovider.js +21 -0
- package/lib/module/DraxSubprovider.js.map +1 -0
- package/lib/module/DraxView.js +348 -0
- package/lib/module/DraxView.js.map +1 -0
- package/lib/module/HoverLayer.js +152 -0
- package/lib/module/HoverLayer.js.map +1 -0
- package/lib/module/SortableBoardContainer.js +386 -0
- package/lib/module/SortableBoardContainer.js.map +1 -0
- package/lib/module/SortableBoardContext.js +6 -0
- package/lib/module/SortableBoardContext.js.map +1 -0
- package/lib/module/SortableContainer.js +571 -0
- package/lib/module/SortableContainer.js.map +1 -0
- package/lib/module/SortableItem.js +226 -0
- package/lib/module/SortableItem.js.map +1 -0
- package/lib/module/SortableItemContext.js +38 -0
- package/lib/module/SortableItemContext.js.map +1 -0
- package/lib/module/compat/detectVersion.js +19 -0
- package/lib/module/compat/detectVersion.js.map +1 -0
- package/lib/module/compat/index.js +5 -0
- package/lib/module/compat/index.js.map +1 -0
- package/lib/module/compat/types.js +4 -0
- package/lib/module/compat/types.js.map +1 -0
- package/lib/module/compat/useDraxPanGesture.js +94 -0
- package/lib/module/compat/useDraxPanGesture.js.map +1 -0
- package/lib/module/hooks/index.js +5 -0
- package/lib/module/hooks/index.js.map +1 -0
- package/lib/module/hooks/useCallbackDispatch.js +688 -0
- package/lib/module/hooks/useCallbackDispatch.js.map +1 -0
- package/lib/module/hooks/useDragGesture.js +240 -0
- package/lib/module/hooks/useDragGesture.js.map +1 -0
- package/lib/module/hooks/useDraxContext.js +12 -0
- package/lib/module/hooks/useDraxContext.js.map +1 -0
- package/lib/module/hooks/useDraxId.js +13 -0
- package/lib/module/hooks/useDraxId.js.map +1 -0
- package/lib/module/hooks/useDraxMethods.js +73 -0
- package/lib/module/hooks/useDraxMethods.js.map +1 -0
- package/lib/module/hooks/useDraxScrollHandler.js +97 -0
- package/lib/module/hooks/useDraxScrollHandler.js.map +1 -0
- package/lib/module/hooks/useSortableBoard.js +37 -0
- package/lib/module/hooks/useSortableBoard.js.map +1 -0
- package/lib/module/hooks/useSortableList.js +988 -0
- package/lib/module/hooks/useSortableList.js.map +1 -0
- package/lib/module/hooks/useSpatialIndex.js +283 -0
- package/lib/module/hooks/useSpatialIndex.js.map +1 -0
- package/lib/module/hooks/useViewStyles.js +158 -0
- package/lib/module/hooks/useViewStyles.js.map +1 -0
- package/lib/module/hooks/useWebScrollFreeze.js +52 -0
- package/lib/module/hooks/useWebScrollFreeze.js.map +1 -0
- package/lib/module/index.js +37 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/math.js +294 -0
- package/lib/module/math.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/params.js +88 -0
- package/lib/module/params.js.map +1 -0
- package/lib/module/types.js +215 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/DebugOverlay.d.ts +17 -0
- package/lib/typescript/src/DebugOverlay.d.ts.map +1 -0
- package/lib/typescript/src/Drax.d.ts +28 -0
- package/lib/typescript/src/Drax.d.ts.map +1 -0
- package/lib/typescript/src/DraxContext.d.ts +3 -0
- package/lib/typescript/src/DraxContext.d.ts.map +1 -0
- package/lib/typescript/src/DraxHandle.d.ts +25 -0
- package/lib/typescript/src/DraxHandle.d.ts.map +1 -0
- package/lib/typescript/src/DraxHandleContext.d.ts +12 -0
- package/lib/typescript/src/DraxHandleContext.d.ts.map +1 -0
- package/lib/typescript/src/DraxList.d.ts +66 -0
- package/lib/typescript/src/DraxList.d.ts.map +1 -0
- package/lib/typescript/src/DraxProvider.d.ts +4 -0
- package/lib/typescript/src/DraxProvider.d.ts.map +1 -0
- package/lib/typescript/src/DraxScrollView.d.ts +7 -0
- package/lib/typescript/src/DraxScrollView.d.ts.map +1 -0
- package/lib/typescript/src/DraxSubprovider.d.ts +4 -0
- package/lib/typescript/src/DraxSubprovider.d.ts.map +1 -0
- package/lib/typescript/src/DraxView.d.ts +4 -0
- package/lib/typescript/src/DraxView.d.ts.map +1 -0
- package/lib/typescript/src/HoverLayer.d.ts +38 -0
- package/lib/typescript/src/HoverLayer.d.ts.map +1 -0
- package/lib/typescript/src/SortableBoardContainer.d.ts +11 -0
- package/lib/typescript/src/SortableBoardContainer.d.ts.map +1 -0
- package/lib/typescript/src/SortableBoardContext.d.ts +4 -0
- package/lib/typescript/src/SortableBoardContext.d.ts.map +1 -0
- package/lib/typescript/src/SortableContainer.d.ts +13 -0
- package/lib/typescript/src/SortableContainer.d.ts.map +1 -0
- package/lib/typescript/src/SortableItem.d.ts +14 -0
- package/lib/typescript/src/SortableItem.d.ts.map +1 -0
- package/lib/typescript/src/SortableItemContext.d.ts +37 -0
- package/lib/typescript/src/SortableItemContext.d.ts.map +1 -0
- package/lib/typescript/src/compat/detectVersion.d.ts +2 -0
- package/lib/typescript/src/compat/detectVersion.d.ts.map +1 -0
- package/lib/typescript/src/compat/index.d.ts +4 -0
- package/lib/typescript/src/compat/index.d.ts.map +1 -0
- package/lib/typescript/src/compat/types.d.ts +33 -0
- package/lib/typescript/src/compat/types.d.ts.map +1 -0
- package/lib/typescript/src/compat/useDraxPanGesture.d.ts +8 -0
- package/lib/typescript/src/compat/useDraxPanGesture.d.ts.map +1 -0
- package/lib/typescript/src/hooks/index.d.ts +3 -0
- package/lib/typescript/src/hooks/index.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useCallbackDispatch.d.ts +40 -0
- package/lib/typescript/src/hooks/useCallbackDispatch.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useDragGesture.d.ts +17 -0
- package/lib/typescript/src/hooks/useDragGesture.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useDraxContext.d.ts +2 -0
- package/lib/typescript/src/hooks/useDraxContext.d.ts.map +1 -0
- package/{build → lib/typescript/src}/hooks/useDraxId.d.ts +1 -0
- package/lib/typescript/src/hooks/useDraxId.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useDraxMethods.d.ts +13 -0
- package/lib/typescript/src/hooks/useDraxMethods.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useDraxScrollHandler.d.ts +27 -0
- package/lib/typescript/src/hooks/useDraxScrollHandler.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useSortableBoard.d.ts +10 -0
- package/lib/typescript/src/hooks/useSortableBoard.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useSortableList.d.ts +11 -0
- package/lib/typescript/src/hooks/useSortableList.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useSpatialIndex.d.ts +22 -0
- package/lib/typescript/src/hooks/useSpatialIndex.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useViewStyles.d.ts +183 -0
- package/lib/typescript/src/hooks/useViewStyles.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useWebScrollFreeze.d.ts +14 -0
- package/lib/typescript/src/hooks/useWebScrollFreeze.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +25 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/math.d.ts +76 -0
- package/lib/typescript/src/math.d.ts.map +1 -0
- package/{build → lib/typescript/src}/params.d.ts +13 -9
- package/lib/typescript/src/params.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +756 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +164 -34
- package/src/DebugOverlay.tsx +140 -0
- package/src/Drax.ts +33 -0
- package/src/DraxContext.ts +8 -0
- package/src/DraxHandle.tsx +52 -0
- package/src/DraxHandleContext.ts +15 -0
- package/src/DraxList.tsx +181 -0
- package/src/DraxProvider.tsx +224 -0
- package/src/DraxScrollView.tsx +180 -0
- package/src/DraxSubprovider.tsx +22 -0
- package/src/DraxView.tsx +430 -0
- package/src/HoverLayer.tsx +167 -0
- package/src/SortableBoardContainer.tsx +439 -0
- package/src/SortableBoardContext.ts +6 -0
- package/src/SortableContainer.tsx +650 -0
- package/src/SortableItem.tsx +264 -0
- package/src/SortableItemContext.ts +46 -0
- package/src/compat/detectVersion.ts +17 -0
- package/src/compat/index.ts +7 -0
- package/src/compat/types.ts +35 -0
- package/src/compat/useDraxPanGesture.ts +112 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useCallbackDispatch.tsx +830 -0
- package/src/hooks/useDragGesture.ts +273 -0
- package/src/hooks/useDraxContext.ts +11 -0
- package/src/hooks/useDraxId.ts +11 -0
- package/src/hooks/useDraxMethods.ts +71 -0
- package/src/hooks/useDraxScrollHandler.ts +121 -0
- package/src/hooks/useSortableBoard.ts +44 -0
- package/src/hooks/useSortableList.ts +1063 -0
- package/src/hooks/useSpatialIndex.ts +336 -0
- package/src/hooks/useViewStyles.ts +180 -0
- package/src/hooks/useWebScrollFreeze.ts +60 -0
- package/src/index.ts +111 -0
- package/src/math.ts +333 -0
- package/src/params.ts +74 -0
- package/src/types.ts +933 -0
- package/.editorconfig +0 -15
- package/.eslintrc.js +0 -4
- package/.prettierrc +0 -16
- package/CHANGELOG.md +0 -270
- package/CODE-OF-CONDUCT.md +0 -85
- package/CONTRIBUTING.md +0 -15
- package/FUNDING.yml +0 -4
- package/build/AllHoverViews.d.ts +0 -0
- package/build/AllHoverViews.js +0 -30
- package/build/DraxContext.d.ts +0 -2
- package/build/DraxContext.js +0 -6
- package/build/DraxList.d.ts +0 -8
- package/build/DraxList.js +0 -512
- package/build/DraxListItem.d.ts +0 -7
- package/build/DraxListItem.js +0 -121
- package/build/DraxProvider.d.ts +0 -2
- package/build/DraxProvider.js +0 -704
- package/build/DraxScrollView.d.ts +0 -6
- package/build/DraxScrollView.js +0 -136
- package/build/DraxSubprovider.d.ts +0 -3
- package/build/DraxSubprovider.js +0 -18
- package/build/DraxView.d.ts +0 -8
- package/build/DraxView.js +0 -93
- package/build/HoverView.d.ts +0 -8
- package/build/HoverView.js +0 -40
- package/build/PanGestureDetector.d.ts +0 -3
- package/build/PanGestureDetector.js +0 -49
- package/build/hooks/index.d.ts +0 -4
- package/build/hooks/index.js +0 -11
- package/build/hooks/useContent.d.ts +0 -23
- package/build/hooks/useContent.js +0 -212
- package/build/hooks/useDraxContext.d.ts +0 -1
- package/build/hooks/useDraxContext.js +0 -13
- package/build/hooks/useDraxId.js +0 -13
- package/build/hooks/useDraxProtocol.d.ts +0 -5
- package/build/hooks/useDraxProtocol.js +0 -32
- package/build/hooks/useDraxRegistry.d.ts +0 -78
- package/build/hooks/useDraxRegistry.js +0 -714
- package/build/hooks/useDraxScrollHandler.d.ts +0 -25
- package/build/hooks/useDraxScrollHandler.js +0 -89
- package/build/hooks/useDraxState.d.ts +0 -10
- package/build/hooks/useDraxState.js +0 -132
- package/build/hooks/useMeasurements.d.ts +0 -9
- package/build/hooks/useMeasurements.js +0 -119
- package/build/hooks/useStatus.d.ts +0 -11
- package/build/hooks/useStatus.js +0 -96
- package/build/index.d.ts +0 -9
- package/build/index.js +0 -33
- package/build/math.d.ts +0 -22
- package/build/math.js +0 -68
- package/build/params.js +0 -27
- package/build/transform.d.ts +0 -11
- package/build/transform.js +0 -59
- package/build/types.d.ts +0 -807
- package/build/types.js +0 -46
- package/docs/concept.md +0 -79
- package/docs/images/color-drag-drop.gif +0 -0
- package/docs/images/deck-cards.gif +0 -0
- package/docs/images/drag-drop-events.jpg +0 -0
- package/docs/images/knight-moves.gif +0 -0
- package/docs/images/reorderable-list.gif +0 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
import type { SharedValue } from 'react-native-reanimated';
|
|
3
|
+
import { runOnJS } from 'react-native-worklets';
|
|
4
|
+
|
|
5
|
+
import { useDraxPanGesture } from '../compat';
|
|
6
|
+
import { computeAbsolutePositionWorklet, hitTestWorklet } from '../math';
|
|
7
|
+
import type { Position } from '../types';
|
|
8
|
+
import { useDraxContext } from './useDraxContext';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates a Pan gesture for a draggable DraxView.
|
|
12
|
+
* Hit-testing runs entirely on the UI thread — zero runOnJS per frame
|
|
13
|
+
* unless the receiver changes.
|
|
14
|
+
*
|
|
15
|
+
* On RNGH v3, `enabledSV` and `longPressDelaySV` are SharedValues that
|
|
16
|
+
* reconfigure the native gesture handler on the UI thread — zero JS bridge.
|
|
17
|
+
* On RNGH v2, they are mirrored to plain values with gesture recreation on change.
|
|
18
|
+
*/
|
|
19
|
+
export const useDragGesture = (
|
|
20
|
+
id: string,
|
|
21
|
+
viewSpatialIndexSV: SharedValue<number>,
|
|
22
|
+
enabledSV: SharedValue<boolean>,
|
|
23
|
+
longPressDelaySV: SharedValue<number>,
|
|
24
|
+
lockDragXPosition?: boolean,
|
|
25
|
+
lockDragYPosition?: boolean,
|
|
26
|
+
dragBoundsSV?: SharedValue<{ x: number; y: number; width: number; height: number } | null>,
|
|
27
|
+
dragActivationFailOffset?: number,
|
|
28
|
+
scrollHorizontal?: boolean
|
|
29
|
+
) => {
|
|
30
|
+
const {
|
|
31
|
+
draggedIdSV,
|
|
32
|
+
receiverIdSV,
|
|
33
|
+
rejectedReceiverIdSV,
|
|
34
|
+
dragPhaseSV,
|
|
35
|
+
hoverPositionSV,
|
|
36
|
+
dragAbsolutePositionSV,
|
|
37
|
+
spatialIndexSV,
|
|
38
|
+
scrollOffsetsSV,
|
|
39
|
+
grabOffsetSV,
|
|
40
|
+
startPositionSV,
|
|
41
|
+
rootOffsetSV,
|
|
42
|
+
handleDragStart,
|
|
43
|
+
handleReceiverChange,
|
|
44
|
+
handleDragEnd,
|
|
45
|
+
} = useDraxContext();
|
|
46
|
+
|
|
47
|
+
// On web, RNGH defaults touch-action to 'none' which blocks native scroll.
|
|
48
|
+
// Allow the scroll direction so users can scroll before long-press activates.
|
|
49
|
+
// SortableContainer freezes the scroll container when drag starts.
|
|
50
|
+
//
|
|
51
|
+
// Priority: lockDragYPosition (explicit axis lock → pan-x) > scrollHorizontal
|
|
52
|
+
// (hint from SortableItem for horizontal lists without axis lock) > default pan-y.
|
|
53
|
+
const touchAction = Platform.OS === 'web'
|
|
54
|
+
? ((lockDragYPosition || scrollHorizontal) ? 'pan-x' : 'pan-y')
|
|
55
|
+
: undefined;
|
|
56
|
+
|
|
57
|
+
const failOffset = dragActivationFailOffset !== undefined
|
|
58
|
+
? [-dragActivationFailOffset, dragActivationFailOffset] as [number, number]
|
|
59
|
+
: undefined;
|
|
60
|
+
|
|
61
|
+
const gesture = useDraxPanGesture({
|
|
62
|
+
enabledSV,
|
|
63
|
+
longPressDelaySV,
|
|
64
|
+
maxPointers: 1,
|
|
65
|
+
shouldCancelWhenOutside: false,
|
|
66
|
+
touchAction,
|
|
67
|
+
failOffsetX: failOffset,
|
|
68
|
+
failOffsetY: failOffset,
|
|
69
|
+
onActivate: (event) => {
|
|
70
|
+
'worklet';
|
|
71
|
+
|
|
72
|
+
// Convert screen-absolute touch to root-view-relative
|
|
73
|
+
const rootOffset = rootOffsetSV.value;
|
|
74
|
+
const rootRelX = event.absoluteX - rootOffset.x;
|
|
75
|
+
const rootRelY = event.absoluteY - rootOffset.y;
|
|
76
|
+
|
|
77
|
+
// Derive the view's visual position from the gesture event.
|
|
78
|
+
// event.x/y = touch relative to the view's bounds (accounts for transforms).
|
|
79
|
+
// This is more accurate than the spatial index for sortable items where
|
|
80
|
+
// permanent shifts move views via CSS transform without updating layout.
|
|
81
|
+
const viewAbsPos: Position = {
|
|
82
|
+
x: rootRelX - event.x,
|
|
83
|
+
y: rootRelY - event.y,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Grab offset = touch position within the view
|
|
87
|
+
const grabOffset: Position = {
|
|
88
|
+
x: event.x,
|
|
89
|
+
y: event.y,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Store shared state (all positions in root-relative space).
|
|
93
|
+
// DO NOT set dragPhaseSV here — it's set by HoverLayer's useLayoutEffect
|
|
94
|
+
// AFTER hover content is committed to the DOM. This prevents the grab blink
|
|
95
|
+
// (item going invisible before hover is visible).
|
|
96
|
+
draggedIdSV.value = id;
|
|
97
|
+
grabOffsetSV.value = grabOffset;
|
|
98
|
+
startPositionSV.value = { x: rootRelX, y: rootRelY };
|
|
99
|
+
dragAbsolutePositionSV.value = { x: rootRelX, y: rootRelY };
|
|
100
|
+
|
|
101
|
+
// Compute initial hover position (root-relative)
|
|
102
|
+
let hoverX = lockDragXPosition ? viewAbsPos.x : rootRelX - grabOffset.x;
|
|
103
|
+
let hoverY = lockDragYPosition ? viewAbsPos.y : rootRelY - grabOffset.y;
|
|
104
|
+
|
|
105
|
+
// Clamp to drag bounds if specified
|
|
106
|
+
if (dragBoundsSV?.value) {
|
|
107
|
+
const b = dragBoundsSV.value;
|
|
108
|
+
const entries = spatialIndexSV.value;
|
|
109
|
+
const viewEntry = entries[viewSpatialIndexSV.value];
|
|
110
|
+
const vw = viewEntry ? viewEntry.width : 0;
|
|
111
|
+
const vh = viewEntry ? viewEntry.height : 0;
|
|
112
|
+
hoverX = Math.max(b.x, Math.min(b.x + b.width - vw, hoverX));
|
|
113
|
+
hoverY = Math.max(b.y, Math.min(b.y + b.height - vh, hoverY));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
hoverPositionSV.value = { x: hoverX, y: hoverY };
|
|
117
|
+
|
|
118
|
+
// Reset receiver and rejection cache
|
|
119
|
+
receiverIdSV.value = '';
|
|
120
|
+
rejectedReceiverIdSV.value = '';
|
|
121
|
+
|
|
122
|
+
// Bounce to JS for callback dispatch + hover content setup
|
|
123
|
+
runOnJS(handleDragStart)(
|
|
124
|
+
id,
|
|
125
|
+
{ x: rootRelX, y: rootRelY },
|
|
126
|
+
grabOffset
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
onUpdate: (event) => {
|
|
130
|
+
'worklet';
|
|
131
|
+
|
|
132
|
+
// Convert screen-absolute touch to root-view-relative
|
|
133
|
+
const rootOffset = rootOffsetSV.value;
|
|
134
|
+
const rootRelX = event.absoluteX - rootOffset.x;
|
|
135
|
+
const rootRelY = event.absoluteY - rootOffset.y;
|
|
136
|
+
const rootRelPos: Position = { x: rootRelX, y: rootRelY };
|
|
137
|
+
|
|
138
|
+
dragAbsolutePositionSV.value = rootRelPos;
|
|
139
|
+
|
|
140
|
+
// Compute hover position (root-relative)
|
|
141
|
+
const grabOffset = grabOffsetSV.value;
|
|
142
|
+
|
|
143
|
+
// Read current spatial index
|
|
144
|
+
const spatialIndex = viewSpatialIndexSV.value;
|
|
145
|
+
const entries = spatialIndexSV.value;
|
|
146
|
+
const viewAbsPos = computeAbsolutePositionWorklet(
|
|
147
|
+
spatialIndex,
|
|
148
|
+
entries,
|
|
149
|
+
scrollOffsetsSV.value
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
let hoverX = lockDragXPosition ? viewAbsPos.x : rootRelX - grabOffset.x;
|
|
153
|
+
let hoverY = lockDragYPosition ? viewAbsPos.y : rootRelY - grabOffset.y;
|
|
154
|
+
|
|
155
|
+
// Clamp to drag bounds if specified
|
|
156
|
+
if (dragBoundsSV?.value) {
|
|
157
|
+
const b = dragBoundsSV.value;
|
|
158
|
+
const viewEntry = entries[spatialIndex];
|
|
159
|
+
const vw = viewEntry ? viewEntry.width : 0;
|
|
160
|
+
const vh = viewEntry ? viewEntry.height : 0;
|
|
161
|
+
hoverX = Math.max(b.x, Math.min(b.x + b.width - vw, hoverX));
|
|
162
|
+
hoverY = Math.max(b.y, Math.min(b.y + b.height - vh, hoverY));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
hoverPositionSV.value = { x: hoverX, y: hoverY };
|
|
166
|
+
|
|
167
|
+
// Hit-test at the center of the hover view (not at the raw finger position)
|
|
168
|
+
// so that receiving activates when the dragged item visually overlaps the receiver.
|
|
169
|
+
const viewEntry = entries[spatialIndex];
|
|
170
|
+
const hitTestPos: Position = {
|
|
171
|
+
x: hoverX + (viewEntry ? viewEntry.width / 2 : 0),
|
|
172
|
+
y: hoverY + (viewEntry ? viewEntry.height / 2 : 0),
|
|
173
|
+
};
|
|
174
|
+
dragAbsolutePositionSV.value = hitTestPos;
|
|
175
|
+
|
|
176
|
+
const result = hitTestWorklet(
|
|
177
|
+
hitTestPos,
|
|
178
|
+
entries,
|
|
179
|
+
scrollOffsetsSV.value,
|
|
180
|
+
id,
|
|
181
|
+
viewEntry ? { width: viewEntry.width, height: viewEntry.height } : undefined
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// Skip the rejected receiver — don't set it in receiverIdSV and don't
|
|
185
|
+
// send it to JS. This prevents the reject → clear → re-detect → reject loop.
|
|
186
|
+
let candidateReceiverId = result.receiverId;
|
|
187
|
+
if (candidateReceiverId === rejectedReceiverIdSV.value) {
|
|
188
|
+
candidateReceiverId = '';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Clear rejection cache once drag leaves the rejected receiver's bounds
|
|
192
|
+
if (result.receiverId !== rejectedReceiverIdSV.value && rejectedReceiverIdSV.value !== '') {
|
|
193
|
+
rejectedReceiverIdSV.value = '';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Always bounce to JS for callback dispatch.
|
|
197
|
+
// handleReceiverChange safely handles same-receiver calls (skips exit/enter)
|
|
198
|
+
// and dispatches continuous callbacks (onDrag, onDragOver, onReceiveDragOver)
|
|
199
|
+
// plus monitor position updates for slot detection.
|
|
200
|
+
const oldReceiver = receiverIdSV.value;
|
|
201
|
+
const receiverChanged = candidateReceiverId !== oldReceiver;
|
|
202
|
+
if (receiverChanged) {
|
|
203
|
+
receiverIdSV.value = candidateReceiverId;
|
|
204
|
+
}
|
|
205
|
+
runOnJS(handleReceiverChange)(
|
|
206
|
+
oldReceiver,
|
|
207
|
+
candidateReceiverId,
|
|
208
|
+
hitTestPos,
|
|
209
|
+
result.monitorIds
|
|
210
|
+
);
|
|
211
|
+
},
|
|
212
|
+
onDeactivate: (_event) => {
|
|
213
|
+
'worklet';
|
|
214
|
+
|
|
215
|
+
const currentDraggedId = draggedIdSV.value;
|
|
216
|
+
const currentReceiverId = receiverIdSV.value;
|
|
217
|
+
|
|
218
|
+
// Run final hit-test to capture current monitors.
|
|
219
|
+
// Monitor IDs are only updated on receiver changes, so if no receiver
|
|
220
|
+
// change happened during the drag, monitors would be empty in handleDragEnd.
|
|
221
|
+
const deactivateEntries = spatialIndexSV.value;
|
|
222
|
+
const viewEntryFinal = deactivateEntries[viewSpatialIndexSV.value];
|
|
223
|
+
const finalDims = viewEntryFinal
|
|
224
|
+
? { width: viewEntryFinal.width, height: viewEntryFinal.height }
|
|
225
|
+
: undefined;
|
|
226
|
+
const finalHitResult = hitTestWorklet(
|
|
227
|
+
dragAbsolutePositionSV.value,
|
|
228
|
+
deactivateEntries,
|
|
229
|
+
scrollOffsetsSV.value,
|
|
230
|
+
id,
|
|
231
|
+
finalDims
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// Set phase and clear receiver on UI thread so useAnimatedStyle
|
|
235
|
+
// re-evaluates immediately (receiver style clears instantly).
|
|
236
|
+
dragPhaseSV.value = 'releasing';
|
|
237
|
+
receiverIdSV.value = '';
|
|
238
|
+
|
|
239
|
+
// Bounce to JS for end callbacks + snap animation
|
|
240
|
+
runOnJS(handleDragEnd)(currentDraggedId, currentReceiverId, false, finalHitResult.monitorIds);
|
|
241
|
+
},
|
|
242
|
+
onFinalize: (_event, didSucceed) => {
|
|
243
|
+
'worklet';
|
|
244
|
+
|
|
245
|
+
// If gesture was cancelled (not ended normally).
|
|
246
|
+
// Check draggedIdSV (set in onActivate) instead of dragPhaseSV
|
|
247
|
+
// because phase is now set later in handleDragStart via runOnUI.
|
|
248
|
+
if (!didSucceed && draggedIdSV.value !== '') {
|
|
249
|
+
const currentDraggedId = draggedIdSV.value;
|
|
250
|
+
const currentReceiverId = receiverIdSV.value;
|
|
251
|
+
|
|
252
|
+
const viewEntryCancel = spatialIndexSV.value[viewSpatialIndexSV.value];
|
|
253
|
+
const cancelDims = viewEntryCancel
|
|
254
|
+
? { width: viewEntryCancel.width, height: viewEntryCancel.height }
|
|
255
|
+
: undefined;
|
|
256
|
+
const finalHitResult = hitTestWorklet(
|
|
257
|
+
dragAbsolutePositionSV.value,
|
|
258
|
+
spatialIndexSV.value,
|
|
259
|
+
scrollOffsetsSV.value,
|
|
260
|
+
id,
|
|
261
|
+
cancelDims
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
dragPhaseSV.value = 'releasing';
|
|
265
|
+
receiverIdSV.value = '';
|
|
266
|
+
|
|
267
|
+
runOnJS(handleDragEnd)(currentDraggedId, currentReceiverId, true, finalHitResult.monitorIds);
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
return gesture;
|
|
273
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { generateRandomId } from '../math';
|
|
4
|
+
|
|
5
|
+
// Return explicitId, or a consistent randomly generated identifier if explicitId is falsy.
|
|
6
|
+
export const useDraxId = (explicitId?: string) => {
|
|
7
|
+
// A generated unique identifier for this view, for use if id prop is not specified.
|
|
8
|
+
const [randomId] = useState(generateRandomId);
|
|
9
|
+
// We use || rather than ?? for the return value in case explicitId is an empty string.
|
|
10
|
+
return explicitId || randomId;
|
|
11
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useDraxContext } from './useDraxContext';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Imperative methods for controlling and querying the Drax provider.
|
|
5
|
+
*
|
|
6
|
+
* Must be called within a `<DraxProvider>`.
|
|
7
|
+
*/
|
|
8
|
+
export const useDraxMethods = () => {
|
|
9
|
+
const ctx = useDraxContext();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Trigger re-measurement of all registered views.
|
|
13
|
+
* Useful after dynamic layout changes where onLayout may not fire
|
|
14
|
+
* (e.g., orientation changes, animated layout transitions).
|
|
15
|
+
*/
|
|
16
|
+
const requestPositionUpdate = () => {
|
|
17
|
+
// Re-measuring is done by reading each view's measure function from
|
|
18
|
+
// the spatial index. For now, this triggers a spatial index refresh
|
|
19
|
+
// by writing the current value back — views that are still mounted
|
|
20
|
+
// will re-measure on the next layout pass.
|
|
21
|
+
const current = ctx.spatialIndexSV.value;
|
|
22
|
+
ctx.spatialIndexSV.value = [...current];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get the set of dragged item IDs currently dropped on a specific receiver.
|
|
27
|
+
* Returns an empty Set if no items have been dropped on the receiver.
|
|
28
|
+
*/
|
|
29
|
+
const getDroppedItems = (receiverId?: string): Map<string, Set<string>> | Set<string> => {
|
|
30
|
+
if (receiverId) {
|
|
31
|
+
return ctx.droppedItemsRef.current.get(receiverId) ?? new Set();
|
|
32
|
+
}
|
|
33
|
+
return new Map(ctx.droppedItemsRef.current);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Clear all tracked dropped items for a specific receiver,
|
|
38
|
+
* or clear all dropped items if no receiverId is provided.
|
|
39
|
+
* Call this when items are programmatically removed from a drop zone.
|
|
40
|
+
*/
|
|
41
|
+
const clearDroppedItems = (receiverId?: string) => {
|
|
42
|
+
if (receiverId) {
|
|
43
|
+
ctx.droppedItemsRef.current.delete(receiverId);
|
|
44
|
+
} else {
|
|
45
|
+
ctx.droppedItemsRef.current.clear();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if a drag is currently active.
|
|
51
|
+
*/
|
|
52
|
+
const isDragging = (): boolean => {
|
|
53
|
+
return ctx.draggedIdSV.value !== '';
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get the ID of the currently dragged view, or undefined if no drag is active.
|
|
58
|
+
*/
|
|
59
|
+
const getDraggedId = (): string | undefined => {
|
|
60
|
+
const id = ctx.draggedIdSV.value;
|
|
61
|
+
return id || undefined;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
requestPositionUpdate,
|
|
66
|
+
getDroppedItems,
|
|
67
|
+
clearDroppedItems,
|
|
68
|
+
isDragging,
|
|
69
|
+
getDraggedId,
|
|
70
|
+
};
|
|
71
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { Ref, RefObject } from 'react';
|
|
2
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
3
|
+
import type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
|
|
4
|
+
import { FlatList, ScrollView } from 'react-native';
|
|
5
|
+
import {
|
|
6
|
+
runOnUI,
|
|
7
|
+
useSharedValue,
|
|
8
|
+
} from 'react-native-reanimated';
|
|
9
|
+
|
|
10
|
+
import { defaultAutoScrollIntervalLength } from '../params';
|
|
11
|
+
import type { DraxViewMeasurements, Position } from '../types';
|
|
12
|
+
import { useDraxId } from './useDraxId';
|
|
13
|
+
|
|
14
|
+
// FlatList is invariant in its type parameter — `any` is the only valid union constraint
|
|
15
|
+
type ScrollableComponents = FlatList<any> | ScrollView;
|
|
16
|
+
|
|
17
|
+
type DraxScrollHandlerArgs<T extends ScrollableComponents> = {
|
|
18
|
+
idProp?: string;
|
|
19
|
+
onContentSizeChangeProp?: (w: number, h: number) => void;
|
|
20
|
+
onScrollProp?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
|
21
|
+
externalRef?: Ref<T>;
|
|
22
|
+
doScroll: RefObject<() => void>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const useDraxScrollHandler = <T extends ScrollableComponents>({
|
|
26
|
+
idProp,
|
|
27
|
+
onContentSizeChangeProp,
|
|
28
|
+
onScrollProp,
|
|
29
|
+
externalRef,
|
|
30
|
+
doScroll,
|
|
31
|
+
}: DraxScrollHandlerArgs<T>) => {
|
|
32
|
+
const scrollRef = useRef<T>(null);
|
|
33
|
+
const id = useDraxId(idProp);
|
|
34
|
+
const containerMeasurementsRef = useRef<DraxViewMeasurements | undefined>(
|
|
35
|
+
undefined
|
|
36
|
+
);
|
|
37
|
+
const scrollIntervalRef = useRef<ReturnType<typeof setInterval> | undefined>(
|
|
38
|
+
undefined
|
|
39
|
+
);
|
|
40
|
+
const contentSizeRef = useRef<Position | undefined>(undefined);
|
|
41
|
+
|
|
42
|
+
const scrollPosition = useSharedValue<Position>({ x: 0, y: 0 });
|
|
43
|
+
|
|
44
|
+
const onMeasureContainer = (measurements: DraxViewMeasurements | undefined) => {
|
|
45
|
+
containerMeasurementsRef.current = measurements;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const onContentSizeChange = (width: number, height: number) => {
|
|
49
|
+
contentSizeRef.current = { x: width, y: height };
|
|
50
|
+
return onContentSizeChangeProp?.(width, height);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
54
|
+
onScrollProp?.(event);
|
|
55
|
+
|
|
56
|
+
runOnUI((_scrollPos: typeof scrollPosition, _event: NativeScrollEvent) => {
|
|
57
|
+
'worklet';
|
|
58
|
+
_scrollPos.value = {
|
|
59
|
+
x: _event.contentOffset.x,
|
|
60
|
+
y: _event.contentOffset.y,
|
|
61
|
+
};
|
|
62
|
+
})(scrollPosition, event.nativeEvent);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const setScrollRefs = (instance: T | null) => {
|
|
66
|
+
if (instance) {
|
|
67
|
+
scrollRef.current = instance;
|
|
68
|
+
if (externalRef) {
|
|
69
|
+
if (typeof externalRef === 'function') {
|
|
70
|
+
externalRef(instance);
|
|
71
|
+
} else {
|
|
72
|
+
externalRef.current = instance;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const startScroll = useCallback(() => {
|
|
79
|
+
if (scrollIntervalRef.current) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
doScroll.current();
|
|
83
|
+
scrollIntervalRef.current = setInterval(
|
|
84
|
+
() => doScroll.current(),
|
|
85
|
+
defaultAutoScrollIntervalLength
|
|
86
|
+
);
|
|
87
|
+
}, [doScroll]);
|
|
88
|
+
|
|
89
|
+
const stopScroll = useCallback(() => {
|
|
90
|
+
if (scrollIntervalRef.current) {
|
|
91
|
+
clearInterval(scrollIntervalRef.current);
|
|
92
|
+
scrollIntervalRef.current = undefined;
|
|
93
|
+
}
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (scrollIntervalRef.current) {
|
|
98
|
+
stopScroll();
|
|
99
|
+
startScroll();
|
|
100
|
+
}
|
|
101
|
+
}, [stopScroll, startScroll]);
|
|
102
|
+
|
|
103
|
+
// Clean up interval on unmount
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
return () => stopScroll();
|
|
106
|
+
}, [stopScroll]);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
id,
|
|
110
|
+
containerMeasurementsRef,
|
|
111
|
+
contentSizeRef,
|
|
112
|
+
onContentSizeChange,
|
|
113
|
+
onMeasureContainer,
|
|
114
|
+
onScroll,
|
|
115
|
+
scrollRef,
|
|
116
|
+
scrollPosition,
|
|
117
|
+
setScrollRefs,
|
|
118
|
+
startScroll,
|
|
119
|
+
stopScroll,
|
|
120
|
+
};
|
|
121
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useCallback, useRef } from 'react';
|
|
2
|
+
import type {
|
|
3
|
+
SortableBoardHandle,
|
|
4
|
+
SortableBoardInternal,
|
|
5
|
+
SortableBoardTransferState,
|
|
6
|
+
SortableListInternal,
|
|
7
|
+
UseSortableBoardOptions,
|
|
8
|
+
} from '../types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Board-level coordinator for cross-container sortable drag.
|
|
12
|
+
*
|
|
13
|
+
* Maintains a registry of columns (each with their own useSortableList)
|
|
14
|
+
* and tracks cross-container transfer state. The actual monitor callbacks
|
|
15
|
+
* are handled by SortableBoardContainer.
|
|
16
|
+
*/
|
|
17
|
+
export const useSortableBoard = <TItem,>(
|
|
18
|
+
options: UseSortableBoardOptions<TItem>
|
|
19
|
+
): SortableBoardHandle<TItem> => {
|
|
20
|
+
const { keyExtractor, onTransfer } = options;
|
|
21
|
+
|
|
22
|
+
const columnsRef = useRef<Map<string, SortableListInternal<unknown>>>(new Map());
|
|
23
|
+
const transferStateRef = useRef<SortableBoardTransferState | undefined>(undefined);
|
|
24
|
+
|
|
25
|
+
const registerColumn = useCallback((id: string, internal: SortableListInternal<unknown>) => {
|
|
26
|
+
columnsRef.current.set(id, internal);
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
const unregisterColumn = useCallback((id: string) => {
|
|
30
|
+
columnsRef.current.delete(id);
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
const internal: SortableBoardInternal<TItem> = {
|
|
34
|
+
keyExtractor,
|
|
35
|
+
onTransfer,
|
|
36
|
+
columns: columnsRef.current,
|
|
37
|
+
registerColumn,
|
|
38
|
+
unregisterColumn,
|
|
39
|
+
transferState: transferStateRef,
|
|
40
|
+
// finalizeTransfer is set by SortableBoardContainer (needs DraxContext access)
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return { _internal: internal };
|
|
44
|
+
};
|