1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 | import {disableTextSelection, restoreTextSelection} from './textSelection';
|
14 | import {DOMAttributes, MoveEvents, PointerType} from '@react-types/shared';
|
15 | import React, {useMemo, useRef} from 'react';
|
16 | import {useEffectEvent, useGlobalListeners} from '@react-aria/utils';
|
17 |
|
18 | export interface MoveResult {
|
19 |
|
20 | moveProps: DOMAttributes
|
21 | }
|
22 |
|
23 | interface EventBase {
|
24 | shiftKey: boolean,
|
25 | ctrlKey: boolean,
|
26 | metaKey: boolean,
|
27 | altKey: boolean
|
28 | }
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 | export function useMove(props: MoveEvents): MoveResult {
|
36 | let {onMoveStart, onMove, onMoveEnd} = props;
|
37 |
|
38 | let state = useRef<{
|
39 | didMove: boolean,
|
40 | lastPosition: {pageX: number, pageY: number} | null,
|
41 | id: number | null
|
42 | }>({didMove: false, lastPosition: null, id: null});
|
43 |
|
44 | let {addGlobalListener, removeGlobalListener} = useGlobalListeners();
|
45 |
|
46 | let move = useEffectEvent((originalEvent: EventBase, pointerType: PointerType, deltaX: number, deltaY: number) => {
|
47 | if (deltaX === 0 && deltaY === 0) {
|
48 | return;
|
49 | }
|
50 |
|
51 | if (!state.current.didMove) {
|
52 | state.current.didMove = true;
|
53 | onMoveStart?.({
|
54 | type: 'movestart',
|
55 | pointerType,
|
56 | shiftKey: originalEvent.shiftKey,
|
57 | metaKey: originalEvent.metaKey,
|
58 | ctrlKey: originalEvent.ctrlKey,
|
59 | altKey: originalEvent.altKey
|
60 | });
|
61 | }
|
62 | onMove?.({
|
63 | type: 'move',
|
64 | pointerType,
|
65 | deltaX: deltaX,
|
66 | deltaY: deltaY,
|
67 | shiftKey: originalEvent.shiftKey,
|
68 | metaKey: originalEvent.metaKey,
|
69 | ctrlKey: originalEvent.ctrlKey,
|
70 | altKey: originalEvent.altKey
|
71 | });
|
72 | });
|
73 |
|
74 | let end = useEffectEvent((originalEvent: EventBase, pointerType: PointerType) => {
|
75 | restoreTextSelection();
|
76 | if (state.current.didMove) {
|
77 | onMoveEnd?.({
|
78 | type: 'moveend',
|
79 | pointerType,
|
80 | shiftKey: originalEvent.shiftKey,
|
81 | metaKey: originalEvent.metaKey,
|
82 | ctrlKey: originalEvent.ctrlKey,
|
83 | altKey: originalEvent.altKey
|
84 | });
|
85 | }
|
86 | });
|
87 |
|
88 | let moveProps = useMemo(() => {
|
89 | let moveProps: DOMAttributes = {};
|
90 |
|
91 | let start = () => {
|
92 | disableTextSelection();
|
93 | state.current.didMove = false;
|
94 | };
|
95 |
|
96 | if (typeof PointerEvent === 'undefined') {
|
97 | let onMouseMove = (e: MouseEvent) => {
|
98 | if (e.button === 0) {
|
99 | move(e, 'mouse', e.pageX - (state.current.lastPosition?.pageX ?? 0), e.pageY - (state.current.lastPosition?.pageY ?? 0));
|
100 | state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
|
101 | }
|
102 | };
|
103 | let onMouseUp = (e: MouseEvent) => {
|
104 | if (e.button === 0) {
|
105 | end(e, 'mouse');
|
106 | removeGlobalListener(window, 'mousemove', onMouseMove, false);
|
107 | removeGlobalListener(window, 'mouseup', onMouseUp, false);
|
108 | }
|
109 | };
|
110 | moveProps.onMouseDown = (e: React.MouseEvent) => {
|
111 | if (e.button === 0) {
|
112 | start();
|
113 | e.stopPropagation();
|
114 | e.preventDefault();
|
115 | state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
|
116 | addGlobalListener(window, 'mousemove', onMouseMove, false);
|
117 | addGlobalListener(window, 'mouseup', onMouseUp, false);
|
118 | }
|
119 | };
|
120 |
|
121 | let onTouchMove = (e: TouchEvent) => {
|
122 | let touch = [...e.changedTouches].findIndex(({identifier}) => identifier === state.current.id);
|
123 | if (touch >= 0) {
|
124 | let {pageX, pageY} = e.changedTouches[touch];
|
125 | move(e, 'touch', pageX - (state.current.lastPosition?.pageX ?? 0), pageY - (state.current.lastPosition?.pageY ?? 0));
|
126 | state.current.lastPosition = {pageX, pageY};
|
127 | }
|
128 | };
|
129 | let onTouchEnd = (e: TouchEvent) => {
|
130 | let touch = [...e.changedTouches].findIndex(({identifier}) => identifier === state.current.id);
|
131 | if (touch >= 0) {
|
132 | end(e, 'touch');
|
133 | state.current.id = null;
|
134 | removeGlobalListener(window, 'touchmove', onTouchMove);
|
135 | removeGlobalListener(window, 'touchend', onTouchEnd);
|
136 | removeGlobalListener(window, 'touchcancel', onTouchEnd);
|
137 | }
|
138 | };
|
139 | moveProps.onTouchStart = (e: React.TouchEvent) => {
|
140 | if (e.changedTouches.length === 0 || state.current.id != null) {
|
141 | return;
|
142 | }
|
143 |
|
144 | let {pageX, pageY, identifier} = e.changedTouches[0];
|
145 | start();
|
146 | e.stopPropagation();
|
147 | e.preventDefault();
|
148 | state.current.lastPosition = {pageX, pageY};
|
149 | state.current.id = identifier;
|
150 | addGlobalListener(window, 'touchmove', onTouchMove, false);
|
151 | addGlobalListener(window, 'touchend', onTouchEnd, false);
|
152 | addGlobalListener(window, 'touchcancel', onTouchEnd, false);
|
153 | };
|
154 | } else {
|
155 | let onPointerMove = (e: PointerEvent) => {
|
156 | if (e.pointerId === state.current.id) {
|
157 | let pointerType = (e.pointerType || 'mouse') as PointerType;
|
158 |
|
159 |
|
160 |
|
161 |
|
162 | move(e, pointerType, e.pageX - (state.current.lastPosition?.pageX ?? 0), e.pageY - (state.current.lastPosition?.pageY ?? 0));
|
163 | state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
|
164 | }
|
165 | };
|
166 |
|
167 | let onPointerUp = (e: PointerEvent) => {
|
168 | if (e.pointerId === state.current.id) {
|
169 | let pointerType = (e.pointerType || 'mouse') as PointerType;
|
170 | end(e, pointerType);
|
171 | state.current.id = null;
|
172 | removeGlobalListener(window, 'pointermove', onPointerMove, false);
|
173 | removeGlobalListener(window, 'pointerup', onPointerUp, false);
|
174 | removeGlobalListener(window, 'pointercancel', onPointerUp, false);
|
175 | }
|
176 | };
|
177 |
|
178 | moveProps.onPointerDown = (e: React.PointerEvent) => {
|
179 | if (e.button === 0 && state.current.id == null) {
|
180 | start();
|
181 | e.stopPropagation();
|
182 | e.preventDefault();
|
183 | state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
|
184 | state.current.id = e.pointerId;
|
185 | addGlobalListener(window, 'pointermove', onPointerMove, false);
|
186 | addGlobalListener(window, 'pointerup', onPointerUp, false);
|
187 | addGlobalListener(window, 'pointercancel', onPointerUp, false);
|
188 | }
|
189 | };
|
190 | }
|
191 |
|
192 | let triggerKeyboardMove = (e: EventBase, deltaX: number, deltaY: number) => {
|
193 | start();
|
194 | move(e, 'keyboard', deltaX, deltaY);
|
195 | end(e, 'keyboard');
|
196 | };
|
197 |
|
198 | moveProps.onKeyDown = (e) => {
|
199 | switch (e.key) {
|
200 | case 'Left':
|
201 | case 'ArrowLeft':
|
202 | e.preventDefault();
|
203 | e.stopPropagation();
|
204 | triggerKeyboardMove(e, -1, 0);
|
205 | break;
|
206 | case 'Right':
|
207 | case 'ArrowRight':
|
208 | e.preventDefault();
|
209 | e.stopPropagation();
|
210 | triggerKeyboardMove(e, 1, 0);
|
211 | break;
|
212 | case 'Up':
|
213 | case 'ArrowUp':
|
214 | e.preventDefault();
|
215 | e.stopPropagation();
|
216 | triggerKeyboardMove(e, 0, -1);
|
217 | break;
|
218 | case 'Down':
|
219 | case 'ArrowDown':
|
220 | e.preventDefault();
|
221 | e.stopPropagation();
|
222 | triggerKeyboardMove(e, 0, 1);
|
223 | break;
|
224 | }
|
225 | };
|
226 |
|
227 | return moveProps;
|
228 | }, [state, addGlobalListener, removeGlobalListener, move, end]);
|
229 |
|
230 | return {moveProps};
|
231 | }
|