@cntrl-site/sdk-nextjs 1.5.2 → 1.6.1

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.
@@ -27,6 +27,7 @@ const useItemTriggers_1 = require("./useItemTriggers");
27
27
  const useSizing_1 = require("./useSizing");
28
28
  const useItemPointerEvents_1 = require("./useItemPointerEvents");
29
29
  const useItemArea_1 = require("./useItemArea");
30
+ const useDraggable_1 = require("./useDraggable");
30
31
  const stickyFix = `
31
32
  -webkit-transform: translate3d(0, 0, 0);
32
33
  transform: translate3d(0, 0, 0);
@@ -49,6 +50,8 @@ const Item = ({ item, sectionId, articleHeight, isParentVisible = true, isInGrou
49
50
  const itemScale = (0, useItemScale_1.useItemScale)(item, sectionId);
50
51
  const interactionCtrl = (0, useItemInteractionCtrl_1.useItemInteractionCtrl)(item.id);
51
52
  const triggers = (0, useItemTriggers_1.useItemTriggers)(interactionCtrl);
53
+ const [position, setPosition] = (0, react_1.useState)({ x: 0, y: 0 });
54
+ const [isDraggingActive, setIsDraggingActive] = (0, react_1.useState)(false);
52
55
  const wrapperStateProps = interactionCtrl === null || interactionCtrl === void 0 ? void 0 : interactionCtrl.getState(['top', 'left']);
53
56
  const innerStateProps = interactionCtrl === null || interactionCtrl === void 0 ? void 0 : interactionCtrl.getState(['width', 'height', 'scale', 'top', 'left']);
54
57
  const { width, height, top, left } = (0, useItemArea_1.useItemArea)(item, sectionId, {
@@ -68,6 +71,23 @@ const Item = ({ item, sectionId, articleHeight, isParentVisible = true, isInGrou
68
71
  const sizingAxis = (0, useSizing_1.useSizing)(item);
69
72
  const ItemComponent = itemsMap_1.itemsMap[item.type] || noop;
70
73
  const sectionTop = rectObserver ? rectObserver.getSectionTop(sectionId) : 0;
74
+ const layoutParams = layout ? item.layoutParams[layout] : undefined;
75
+ const isDraggable = layoutParams && 'isDraggable' in layoutParams ? layoutParams.isDraggable : undefined;
76
+ (0, useDraggable_1.useDraggable)({ draggableRef: itemInnerRef.current, isEnabled: isDraggable !== null && isDraggable !== void 0 ? isDraggable : false }, ({ startX, startY, currentX, currentY, lastX, lastY, drag }) => {
77
+ const item = itemInnerRef.current;
78
+ if (!item)
79
+ return;
80
+ if (drag) {
81
+ setIsDraggingActive(true);
82
+ setPosition({
83
+ x: (currentX - startX) + lastX,
84
+ y: (currentY - startY) + lastY
85
+ });
86
+ }
87
+ else {
88
+ setIsDraggingActive(false);
89
+ }
90
+ });
71
91
  const handleItemResize = (height) => {
72
92
  if (!layout)
73
93
  return;
@@ -99,7 +119,7 @@ const Item = ({ item, sectionId, articleHeight, isParentVisible = true, isInGrou
99
119
  opacity: (keyframes.length !== 0 && !layout) ? 0 : 1,
100
120
  top: `${stickyTop * 100}vw`,
101
121
  height: isRichText && itemHeight !== undefined ? `${itemHeight * 100}vw` : 'unset',
102
- }, children: (0, jsx_runtime_1.jsx)(RichTextWrapper_1.RichTextWrapper, { isRichText: isRichText, children: (0, jsx_runtime_1.jsx)("div", Object.assign({ className: `item-${item.id}-inner`, ref: itemInnerRef, style: Object.assign(Object.assign(Object.assign({}, ((width !== undefined && height !== undefined)
122
+ }, children: (0, jsx_runtime_1.jsx)(RichTextWrapper_1.RichTextWrapper, { isRichText: isRichText, children: (0, jsx_runtime_1.jsx)("div", Object.assign({ className: `item-${item.id}-inner`, ref: itemInnerRef, style: Object.assign(Object.assign(Object.assign({ top: `${position.y}px`, left: `${position.x}px` }, ((width !== undefined && height !== undefined)
103
123
  ? {
104
124
  width: `${sizingAxis.x === 'manual'
105
125
  ? isRichText
@@ -108,7 +128,13 @@ const Item = ({ item, sectionId, articleHeight, isParentVisible = true, isInGrou
108
128
  : 'max-content'}`,
109
129
  height: `${sizingAxis.y === 'manual' ? `${height * 100}vw` : 'unset'}`
110
130
  }
111
- : {})), (scale !== undefined ? { transform: `scale(${scale})`, WebkitTransform: `scale(${scale})` } : {})), { transition: (_l = innerStateProps === null || innerStateProps === void 0 ? void 0 : innerStateProps.transition) !== null && _l !== void 0 ? _l : 'none', cursor: hasClickTriggers ? 'pointer' : 'unset', pointerEvents: allowPointerEvents ? 'auto' : 'none' }) }, triggers, { children: (0, jsx_runtime_1.jsx)(ItemComponent, { item: item, sectionId: sectionId, onResize: handleItemResize, articleHeight: articleHeight, interactionCtrl: interactionCtrl, onVisibilityChange: handleVisibilityChange }) })) }) }), (0, jsx_runtime_1.jsx)(style_1.default, { id: id, children: `
131
+ : {})), (scale !== undefined ? { transform: `scale(${scale})`, WebkitTransform: `scale(${scale})` } : {})), { transition: (_l = innerStateProps === null || innerStateProps === void 0 ? void 0 : innerStateProps.transition) !== null && _l !== void 0 ? _l : 'none', cursor: isDraggingActive
132
+ ? 'grabbing'
133
+ : isDraggable
134
+ ? 'grab'
135
+ : hasClickTriggers
136
+ ? 'pointer'
137
+ : 'unset', pointerEvents: allowPointerEvents ? 'auto' : 'none', userSelect: isDraggable ? 'none' : 'unset', WebkitUserSelect: isDraggable ? 'none' : 'unset', MozUserSelect: isDraggable ? 'none' : 'unset', msUserSelect: isDraggable ? 'none' : 'unset' }) }, triggers, { children: (0, jsx_runtime_1.jsx)(ItemComponent, { item: item, sectionId: sectionId, onResize: handleItemResize, articleHeight: articleHeight, interactionCtrl: interactionCtrl, onVisibilityChange: handleVisibilityChange }) })) }) }), (0, jsx_runtime_1.jsx)(style_1.default, { id: id, children: `
112
138
  ${(0, sdk_1.getLayoutStyles)(layouts, layoutValues, ([area, hidden, sticky, sectionHeight, layoutParams]) => {
113
139
  const sizingAxis = (0, useSizing_1.parseSizing)(layoutParams.sizing);
114
140
  const isScreenBasedBottom = area.positionType === sdk_1.PositionType.ScreenBased && area.anchorSide === sdk_1.AnchorSide.Bottom;
@@ -129,6 +155,7 @@ const Item = ({ item, sectionId, articleHeight, isParentVisible = true, isInGrou
129
155
  height: ${sizingAxis.y === 'manual' ? `${area.height * 100}vw` : 'unset'};
130
156
  transform-origin: ${ScaleAnchorMap_1.ScaleAnchorMap[scaleAnchor]};
131
157
  transform: scale(${area.scale});
158
+ position: relative;
132
159
  }
133
160
  .item-wrapper-${item.id} {
134
161
  position: ${area.positionType === sdk_1.PositionType.ScreenBased ? 'fixed' : 'absolute'};
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useDraggable = void 0;
4
+ const react_1 = require("react");
5
+ const INITIAL_STATE = {
6
+ drag: false,
7
+ startX: 0,
8
+ startY: 0,
9
+ currentX: 0,
10
+ currentY: 0,
11
+ pivotX: 0,
12
+ pivotY: 0,
13
+ lastX: 0,
14
+ lastY: 0
15
+ };
16
+ function useDraggable(data, dragHandler, preventDragOnChildren = false) {
17
+ const dragStateRef = (0, react_1.useRef)(INITIAL_STATE);
18
+ const prevDragStateRef = (0, react_1.useRef)();
19
+ const dragHandlerRef = (0, react_1.useRef)();
20
+ const untrackMouseMoveRef = (0, react_1.useRef)();
21
+ const animationFrameRef = (0, react_1.useRef)();
22
+ dragHandlerRef.current = dragHandler;
23
+ const setDragState = (0, react_1.useCallback)(action => {
24
+ const state = isStateGetter(action)
25
+ ? action(dragStateRef.current)
26
+ : action;
27
+ if (!prevDragStateRef.current) {
28
+ prevDragStateRef.current = dragStateRef.current;
29
+ }
30
+ dragStateRef.current = state;
31
+ if (animationFrameRef.current !== undefined)
32
+ return;
33
+ animationFrameRef.current = window.requestAnimationFrame(() => {
34
+ var _a;
35
+ animationFrameRef.current = undefined;
36
+ (_a = dragHandlerRef.current) === null || _a === void 0 ? void 0 : _a.call(dragHandlerRef, dragStateRef.current, prevDragStateRef.current);
37
+ prevDragStateRef.current = dragStateRef.current;
38
+ });
39
+ }, []);
40
+ const handleMouseMove = (0, react_1.useCallback)(event => {
41
+ event.stopPropagation();
42
+ const el = event.target;
43
+ if (!(el instanceof HTMLElement))
44
+ return;
45
+ setDragState(state => {
46
+ if (!state.drag) {
47
+ const clientRect = el.getBoundingClientRect();
48
+ return {
49
+ drag: true,
50
+ startX: event.clientX,
51
+ startY: event.clientY,
52
+ currentX: event.clientX,
53
+ currentY: event.clientY,
54
+ pivotX: event.clientX - clientRect.x,
55
+ pivotY: event.clientY - clientRect.y,
56
+ lastX: state.lastX,
57
+ lastY: state.lastY
58
+ };
59
+ }
60
+ return Object.assign(Object.assign({}, state), { currentX: state.currentX + event.movementX, currentY: state.currentY + event.movementY });
61
+ });
62
+ }, [setDragState, data.draggableRef, preventDragOnChildren]);
63
+ const handleMouseUp = (0, react_1.useCallback)(event => {
64
+ var _a;
65
+ event.stopPropagation();
66
+ setDragState(state => (Object.assign(Object.assign({}, state), { drag: false, lastX: state.currentX - state.startX + state.lastX, lastY: state.currentY - state.startY + state.lastY })));
67
+ (_a = untrackMouseMoveRef.current) === null || _a === void 0 ? void 0 : _a.call(untrackMouseMoveRef);
68
+ }, [setDragState]);
69
+ const handleScroll = (0, react_1.useCallback)((event) => {
70
+ setDragState(state => state);
71
+ }, [setDragState]);
72
+ const trackMouseMove = (0, react_1.useCallback)(() => {
73
+ window.addEventListener('mousemove', handleMouseMove, { capture: true });
74
+ window.addEventListener('mouseup', handleMouseUp, { capture: true });
75
+ window.addEventListener('scroll', handleScroll, { capture: true, passive: true });
76
+ return () => {
77
+ window.removeEventListener('mousemove', handleMouseMove, { capture: true });
78
+ window.removeEventListener('mouseup', handleMouseUp, { capture: true });
79
+ window.removeEventListener('scroll', handleScroll, { capture: true });
80
+ };
81
+ }, [handleMouseMove, handleMouseUp, handleScroll]);
82
+ const handleMouseDown = (0, react_1.useCallback)(event => {
83
+ var _a;
84
+ event.stopPropagation();
85
+ if (preventDragOnChildren && event.target instanceof Node && data.draggableRef instanceof Node) {
86
+ if (event.target !== data.draggableRef && data.draggableRef.contains(event.target))
87
+ return;
88
+ }
89
+ if (event.button !== 0)
90
+ return;
91
+ (_a = untrackMouseMoveRef.current) === null || _a === void 0 ? void 0 : _a.call(untrackMouseMoveRef);
92
+ untrackMouseMoveRef.current = trackMouseMove();
93
+ }, [trackMouseMove]);
94
+ const handleTouchMove = (0, react_1.useCallback)(event => {
95
+ event.stopPropagation();
96
+ event.preventDefault();
97
+ const touch = event.touches[0];
98
+ const el = event.target;
99
+ if (!(el instanceof HTMLElement))
100
+ return;
101
+ setDragState(state => {
102
+ if (!state.drag) {
103
+ const clientRect = el.getBoundingClientRect();
104
+ return {
105
+ drag: true,
106
+ startX: touch.clientX,
107
+ startY: touch.clientY,
108
+ currentX: touch.clientX,
109
+ currentY: touch.clientY,
110
+ pivotX: touch.clientX - clientRect.x,
111
+ pivotY: touch.clientY - clientRect.y,
112
+ lastX: state.lastX,
113
+ lastY: state.lastY
114
+ };
115
+ }
116
+ const movementX = touch.clientX - state.currentX;
117
+ const movementY = touch.clientY - state.currentY;
118
+ return Object.assign(Object.assign({}, state), { currentX: state.currentX + movementX, currentY: state.currentY + movementY });
119
+ });
120
+ }, [setDragState]);
121
+ const handleTouchEnd = (0, react_1.useCallback)(event => {
122
+ var _a;
123
+ event.stopPropagation();
124
+ document.body.style.overflow = '';
125
+ document.documentElement.style.overflow = '';
126
+ setDragState(state => (Object.assign(Object.assign({}, state), { drag: false, lastX: state.currentX - state.startX + state.lastX, lastY: state.currentY - state.startY + state.lastY })));
127
+ (_a = untrackMouseMoveRef.current) === null || _a === void 0 ? void 0 : _a.call(untrackMouseMoveRef);
128
+ }, [setDragState]);
129
+ const trackTouchMove = (0, react_1.useCallback)(() => {
130
+ window.addEventListener('touchmove', handleTouchMove, { capture: true, passive: false });
131
+ window.addEventListener('touchend', handleTouchEnd, { capture: true });
132
+ window.addEventListener('touchcancel', handleTouchEnd, { capture: true });
133
+ return () => {
134
+ window.removeEventListener('touchmove', handleTouchMove, { capture: true });
135
+ window.removeEventListener('touchend', handleTouchEnd, { capture: true });
136
+ window.removeEventListener('touchcancel', handleTouchEnd, { capture: true });
137
+ };
138
+ }, [handleTouchMove, handleTouchEnd]);
139
+ const handleTouchStart = (0, react_1.useCallback)(event => {
140
+ var _a;
141
+ event.stopPropagation();
142
+ if (preventDragOnChildren && event.target instanceof Node && data.draggableRef instanceof Node) {
143
+ if (event.target !== data.draggableRef && data.draggableRef.contains(event.target))
144
+ return;
145
+ }
146
+ document.body.style.overflow = 'hidden';
147
+ document.documentElement.style.overflow = 'hidden';
148
+ const selection = document.getSelection();
149
+ if (selection) {
150
+ selection.removeAllRanges();
151
+ }
152
+ (_a = untrackMouseMoveRef.current) === null || _a === void 0 ? void 0 : _a.call(untrackMouseMoveRef);
153
+ untrackMouseMoveRef.current = trackTouchMove();
154
+ }, [trackTouchMove]);
155
+ (0, react_1.useEffect)(() => {
156
+ if (!data.draggableRef || !data.isEnabled)
157
+ return;
158
+ const mouseHandler = handleMouseDown;
159
+ const touchHandler = handleTouchStart;
160
+ data.draggableRef.addEventListener('mousedown', mouseHandler);
161
+ data.draggableRef.addEventListener('touchstart', touchHandler);
162
+ return () => {
163
+ var _a, _b, _c;
164
+ (_a = data.draggableRef) === null || _a === void 0 ? void 0 : _a.removeEventListener('mousedown', mouseHandler);
165
+ (_b = data.draggableRef) === null || _b === void 0 ? void 0 : _b.removeEventListener('touchstart', touchHandler);
166
+ (_c = untrackMouseMoveRef.current) === null || _c === void 0 ? void 0 : _c.call(untrackMouseMoveRef);
167
+ if (animationFrameRef.current !== undefined) {
168
+ window.cancelAnimationFrame(animationFrameRef.current);
169
+ }
170
+ document.body.style.overflow = '';
171
+ document.documentElement.style.overflow = '';
172
+ };
173
+ }, [data.draggableRef, data.isEnabled, handleMouseDown, handleTouchStart]);
174
+ }
175
+ exports.useDraggable = useDraggable;
176
+ function isStateGetter(action) {
177
+ return typeof action === 'function';
178
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cntrl-site/sdk-nextjs",
3
- "version": "1.5.2",
3
+ "version": "1.6.1",
4
4
  "description": "SDK for Next.js",
5
5
  "author": "arsen@momdesign.nyc",
6
6
  "license": "MIT",
@@ -31,7 +31,7 @@
31
31
  "@antfu/eslint-config": "^3.8.0",
32
32
  "@cntrl-site/color": "^1.0.0",
33
33
  "@cntrl-site/effects": "^1.3.2",
34
- "@cntrl-site/sdk": "^1.19.0",
34
+ "@cntrl-site/sdk": "^1.19.3",
35
35
  "@types/vimeo__player": "^2.18.0",
36
36
  "@vimeo/player": "^2.25.0",
37
37
  "html-react-parser": "^3.0.1",
@@ -34,6 +34,7 @@ import { useItemTriggers } from './useItemTriggers';
34
34
  import { parseSizing, useSizing } from './useSizing';
35
35
  import { useItemPointerEvents } from './useItemPointerEvents';
36
36
  import { useItemArea } from './useItemArea';
37
+ import { useDraggable } from './useDraggable';
37
38
 
38
39
  export interface ItemProps<I extends ItemAny> {
39
40
  item: I;
@@ -78,6 +79,8 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight, isP
78
79
  const itemScale = useItemScale(item, sectionId);
79
80
  const interactionCtrl = useItemInteractionCtrl(item.id);
80
81
  const triggers = useItemTriggers(interactionCtrl);
82
+ const [position, setPosition] = useState({ x: 0, y: 0 });
83
+ const [isDraggingActive, setIsDraggingActive] = useState(false);
81
84
  const wrapperStateProps = interactionCtrl?.getState(['top', 'left']);
82
85
  const innerStateProps = interactionCtrl?.getState(['width', 'height', 'scale', 'top', 'left']);
83
86
  const { width, height, top, left } = useItemArea(item, sectionId, {
@@ -97,6 +100,21 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight, isP
97
100
  const sizingAxis = useSizing(item);
98
101
  const ItemComponent = itemsMap[item.type] || noop;
99
102
  const sectionTop = rectObserver ? rectObserver.getSectionTop(sectionId) : 0;
103
+ const layoutParams = layout ? item.layoutParams[layout] : undefined;
104
+ const isDraggable = layoutParams && 'isDraggable' in layoutParams ? layoutParams.isDraggable : undefined;
105
+ useDraggable({ draggableRef: itemInnerRef.current, isEnabled: isDraggable ?? false }, ({ startX, startY, currentX, currentY, lastX, lastY, drag }) => {
106
+ const item = itemInnerRef.current;
107
+ if (!item) return;
108
+ if (drag) {
109
+ setIsDraggingActive(true);
110
+ setPosition({
111
+ x: (currentX - startX) + lastX,
112
+ y: (currentY - startY) + lastY
113
+ });
114
+ } else {
115
+ setIsDraggingActive(false);
116
+ }
117
+ });
100
118
 
101
119
  const handleItemResize = (height: number) => {
102
120
  if (!layout) return;
@@ -151,6 +169,8 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight, isP
151
169
  className={`item-${item.id}-inner`}
152
170
  ref={itemInnerRef}
153
171
  style={{
172
+ top: `${position.y}px`,
173
+ left: `${position.x}px`,
154
174
  ...((width !== undefined && height !== undefined)
155
175
  ? {
156
176
  width: `${sizingAxis.x === 'manual'
@@ -163,8 +183,18 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight, isP
163
183
  : {}),
164
184
  ...(scale !== undefined ? { transform: `scale(${scale})`, WebkitTransform: `scale(${scale})` } : {}),
165
185
  transition: innerStateProps?.transition ?? 'none',
166
- cursor: hasClickTriggers ? 'pointer' : 'unset',
167
- pointerEvents: allowPointerEvents ? 'auto' : 'none'
186
+ cursor: isDraggingActive
187
+ ? 'grabbing'
188
+ : isDraggable
189
+ ? 'grab'
190
+ : hasClickTriggers
191
+ ? 'pointer'
192
+ : 'unset',
193
+ pointerEvents: allowPointerEvents ? 'auto' : 'none',
194
+ userSelect: isDraggable ? 'none' : 'unset',
195
+ WebkitUserSelect: isDraggable ? 'none' : 'unset',
196
+ MozUserSelect: isDraggable ? 'none' : 'unset',
197
+ msUserSelect: isDraggable ? 'none' : 'unset'
168
198
  }}
169
199
  {...triggers}
170
200
  >
@@ -200,6 +230,7 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight, isP
200
230
  height: ${sizingAxis.y === 'manual' ? `${area.height * 100}vw` : 'unset'};
201
231
  transform-origin: ${ScaleAnchorMap[scaleAnchor]};
202
232
  transform: scale(${area.scale});
233
+ position: relative;
203
234
  }
204
235
  .item-wrapper-${item.id} {
205
236
  position: ${area.positionType === PositionType.ScreenBased ? 'fixed' : 'absolute'};
@@ -0,0 +1,252 @@
1
+ import {
2
+ Dispatch,
3
+ SetStateAction,
4
+ useCallback,
5
+ useEffect,
6
+ useRef
7
+ } from 'react';
8
+
9
+ const INITIAL_STATE: DragState = {
10
+ drag: false,
11
+ startX: 0,
12
+ startY: 0,
13
+ currentX: 0,
14
+ currentY: 0,
15
+ pivotX: 0,
16
+ pivotY: 0,
17
+ lastX: 0,
18
+ lastY: 0
19
+ };
20
+
21
+ export function useDraggable(
22
+ data: {
23
+ draggableRef: HTMLElement | null | undefined,
24
+ isEnabled: boolean
25
+ },
26
+ dragHandler?: DragHandler,
27
+ preventDragOnChildren: boolean = false,
28
+ ) {
29
+ const dragStateRef = useRef<DragState>(INITIAL_STATE);
30
+ const prevDragStateRef = useRef<DragState | undefined>();
31
+ const dragHandlerRef = useRef<DragHandler | undefined>();
32
+ const untrackMouseMoveRef = useRef<(() => void) | undefined>();
33
+ const animationFrameRef = useRef<number | undefined>();
34
+ dragHandlerRef.current = dragHandler;
35
+
36
+ const setDragState = useCallback<Dispatch<SetStateAction<DragState>>>(
37
+ action => {
38
+ const state = isStateGetter<DragState>(action)
39
+ ? action(dragStateRef.current)
40
+ : action;
41
+ if (!prevDragStateRef.current) {
42
+ prevDragStateRef.current = dragStateRef.current;
43
+ }
44
+ dragStateRef.current = state;
45
+ if (animationFrameRef.current !== undefined) return;
46
+ animationFrameRef.current = window.requestAnimationFrame(() => {
47
+ animationFrameRef.current = undefined;
48
+ dragHandlerRef.current?.(
49
+ dragStateRef.current,
50
+ prevDragStateRef.current
51
+ );
52
+ prevDragStateRef.current = dragStateRef.current;
53
+ });
54
+ },
55
+ []
56
+ );
57
+
58
+ const handleMouseMove = useCallback<EventHandler<MouseEvent>>(
59
+ event => {
60
+ event.stopPropagation();
61
+ const el = event.target;
62
+ if (!(el instanceof HTMLElement)) return;
63
+ setDragState(state => {
64
+ if (!state.drag) {
65
+ const clientRect = el.getBoundingClientRect();
66
+ return {
67
+ drag: true,
68
+ startX: event.clientX,
69
+ startY: event.clientY,
70
+ currentX: event.clientX,
71
+ currentY: event.clientY,
72
+ pivotX: event.clientX - clientRect.x,
73
+ pivotY: event.clientY - clientRect.y,
74
+ lastX: state.lastX,
75
+ lastY: state.lastY
76
+ };
77
+ }
78
+ return {
79
+ ...state,
80
+ currentX: state.currentX + event.movementX,
81
+ currentY: state.currentY + event.movementY
82
+ };
83
+ });
84
+ },
85
+ [setDragState, data.draggableRef, preventDragOnChildren]
86
+ );
87
+
88
+ const handleMouseUp = useCallback<EventHandler<MouseEvent>>(
89
+ event => {
90
+ event.stopPropagation();
91
+ setDragState(state => ({
92
+ ...state,
93
+ drag: false,
94
+ lastX: state.currentX - state.startX + state.lastX,
95
+ lastY: state.currentY - state.startY + state.lastY
96
+ }));
97
+ untrackMouseMoveRef.current?.();
98
+ },
99
+ [ setDragState]
100
+ );
101
+
102
+ const handleScroll = useCallback<EventHandler<Event>>((event) => {
103
+ setDragState(state => state);
104
+ }, [setDragState]);
105
+
106
+ const trackMouseMove = useCallback(() => {
107
+ window.addEventListener('mousemove', handleMouseMove, { capture: true });
108
+ window.addEventListener('mouseup', handleMouseUp, { capture: true });
109
+ window.addEventListener('scroll', handleScroll, { capture: true, passive: true });
110
+ return () => {
111
+ window.removeEventListener('mousemove', handleMouseMove, { capture: true });
112
+ window.removeEventListener('mouseup', handleMouseUp, { capture: true });
113
+ window.removeEventListener('scroll', handleScroll, { capture: true });
114
+ };
115
+ }, [handleMouseMove, handleMouseUp, handleScroll]);
116
+
117
+ const handleMouseDown = useCallback<EventHandler<MouseEvent>>(
118
+ event => {
119
+ event.stopPropagation();
120
+ if (preventDragOnChildren && event.target instanceof Node && data.draggableRef instanceof Node) {
121
+ if (event.target !== data.draggableRef && data.draggableRef.contains(event.target)) return;
122
+ }
123
+ if (event.button !== 0) return;
124
+ untrackMouseMoveRef.current?.();
125
+ untrackMouseMoveRef.current = trackMouseMove();
126
+ },
127
+ [trackMouseMove]
128
+ );
129
+
130
+ const handleTouchMove = useCallback<EventHandler<TouchEvent>>(
131
+ event => {
132
+ event.stopPropagation();
133
+ event.preventDefault();
134
+ const touch = event.touches[0];
135
+ const el = event.target;
136
+ if (!(el instanceof HTMLElement)) return;
137
+
138
+ setDragState(state => {
139
+ if (!state.drag) {
140
+ const clientRect = el.getBoundingClientRect();
141
+ return {
142
+ drag: true,
143
+ startX: touch.clientX,
144
+ startY: touch.clientY,
145
+ currentX: touch.clientX,
146
+ currentY: touch.clientY,
147
+ pivotX: touch.clientX - clientRect.x,
148
+ pivotY: touch.clientY - clientRect.y,
149
+ lastX: state.lastX,
150
+ lastY: state.lastY
151
+ };
152
+ }
153
+
154
+ const movementX = touch.clientX - state.currentX;
155
+ const movementY = touch.clientY - state.currentY;
156
+
157
+ return {
158
+ ...state,
159
+ currentX: state.currentX + movementX,
160
+ currentY: state.currentY + movementY
161
+ };
162
+ });
163
+ },
164
+ [setDragState]
165
+ );
166
+
167
+ const handleTouchEnd = useCallback<EventHandler<TouchEvent>>(
168
+ event => {
169
+ event.stopPropagation();
170
+ document.body.style.overflow = '';
171
+ document.documentElement.style.overflow = '';
172
+ setDragState(state => ({
173
+ ...state,
174
+ drag: false,
175
+ lastX: state.currentX - state.startX + state.lastX,
176
+ lastY: state.currentY - state.startY + state.lastY
177
+ }));
178
+ untrackMouseMoveRef.current?.();
179
+ },
180
+ [setDragState]
181
+ );
182
+
183
+ const trackTouchMove = useCallback(() => {
184
+ window.addEventListener('touchmove', handleTouchMove, { capture: true, passive: false });
185
+ window.addEventListener('touchend', handleTouchEnd, { capture: true });
186
+ window.addEventListener('touchcancel', handleTouchEnd, { capture: true });
187
+ return () => {
188
+ window.removeEventListener('touchmove', handleTouchMove, { capture: true });
189
+ window.removeEventListener('touchend', handleTouchEnd, { capture: true });
190
+ window.removeEventListener('touchcancel', handleTouchEnd, { capture: true });
191
+ };
192
+ }, [handleTouchMove, handleTouchEnd]);
193
+
194
+ const handleTouchStart = useCallback<EventHandler<TouchEvent>>(
195
+ event => {
196
+ event.stopPropagation();
197
+ if (preventDragOnChildren && event.target instanceof Node && data.draggableRef instanceof Node) {
198
+ if (event.target !== data.draggableRef && data.draggableRef.contains(event.target)) return;
199
+ }
200
+ document.body.style.overflow = 'hidden';
201
+ document.documentElement.style.overflow = 'hidden';
202
+ const selection = document.getSelection();
203
+ if (selection) {
204
+ selection.removeAllRanges();
205
+ }
206
+ untrackMouseMoveRef.current?.();
207
+ untrackMouseMoveRef.current = trackTouchMove();
208
+ },
209
+ [trackTouchMove]
210
+ );
211
+
212
+ useEffect(() => {
213
+ if (!data.draggableRef || !data.isEnabled) return;
214
+ const mouseHandler = handleMouseDown;
215
+ const touchHandler = handleTouchStart;
216
+
217
+ data.draggableRef.addEventListener('mousedown', mouseHandler);
218
+ data.draggableRef.addEventListener('touchstart', touchHandler);
219
+
220
+ return () => {
221
+ data.draggableRef?.removeEventListener('mousedown', mouseHandler);
222
+ data.draggableRef?.removeEventListener('touchstart', touchHandler);
223
+ untrackMouseMoveRef.current?.();
224
+ if (animationFrameRef.current !== undefined) {
225
+ window.cancelAnimationFrame(animationFrameRef.current);
226
+ }
227
+ document.body.style.overflow = '';
228
+ document.documentElement.style.overflow = '';
229
+ };
230
+ }, [data.draggableRef, data.isEnabled, handleMouseDown, handleTouchStart]);
231
+ }
232
+
233
+ interface DragState {
234
+ drag: boolean;
235
+ startX: number;
236
+ startY: number;
237
+ currentX: number;
238
+ currentY: number;
239
+ pivotX: number;
240
+ pivotY: number;
241
+ lastX: number;
242
+ lastY: number;
243
+ }
244
+
245
+ type EventHandler<E extends Event> = (event: E) => void;
246
+ type DragHandler = (state: DragState, prevState?: DragState) => void;
247
+
248
+ function isStateGetter<T>(
249
+ action: SetStateAction<T>
250
+ ): action is (prevState: T) => T {
251
+ return typeof action === 'function';
252
+ }