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