1 | import { CoordinatesEngine } from './CoordinatesEngine'
|
---|
2 | import { coordinatesConfigResolver } from '../config/coordinatesConfigResolver'
|
---|
3 | import { pointerId, getPointerType, pointerValues } from '../utils/events'
|
---|
4 | import { V } from '../utils/maths'
|
---|
5 | import { Vector2 } from '../types'
|
---|
6 |
|
---|
7 | const KEYS_DELTA_MAP = {
|
---|
8 | ArrowRight: (displacement: number, factor: number = 1) => [displacement * factor, 0],
|
---|
9 | ArrowLeft: (displacement: number, factor: number = 1) => [-1 * displacement * factor, 0],
|
---|
10 | ArrowUp: (displacement: number, factor: number = 1) => [0, -1 * displacement * factor],
|
---|
11 | ArrowDown: (displacement: number, factor: number = 1) => [0, displacement * factor]
|
---|
12 | }
|
---|
13 |
|
---|
14 | export class DragEngine extends CoordinatesEngine<'drag'> {
|
---|
15 | ingKey = 'dragging' as const
|
---|
16 |
|
---|
17 | // superseeds generic Engine reset call
|
---|
18 | reset(this: DragEngine) {
|
---|
19 | super.reset()
|
---|
20 | const state = this.state
|
---|
21 | state._pointerId = undefined
|
---|
22 | state._pointerActive = false
|
---|
23 | state._keyboardActive = false
|
---|
24 | state._preventScroll = false
|
---|
25 | state._delayed = false
|
---|
26 | state.swipe = [0, 0]
|
---|
27 | state.tap = false
|
---|
28 | state.canceled = false
|
---|
29 | state.cancel = this.cancel.bind(this)
|
---|
30 | }
|
---|
31 |
|
---|
32 | setup() {
|
---|
33 | const state = this.state
|
---|
34 |
|
---|
35 | if (state._bounds instanceof HTMLElement) {
|
---|
36 | const boundRect = state._bounds.getBoundingClientRect()
|
---|
37 | const targetRect = (state.currentTarget as HTMLElement).getBoundingClientRect()
|
---|
38 | const _bounds = {
|
---|
39 | left: boundRect.left - targetRect.left + state.offset[0],
|
---|
40 | right: boundRect.right - targetRect.right + state.offset[0],
|
---|
41 | top: boundRect.top - targetRect.top + state.offset[1],
|
---|
42 | bottom: boundRect.bottom - targetRect.bottom + state.offset[1]
|
---|
43 | }
|
---|
44 | state._bounds = coordinatesConfigResolver.bounds(_bounds) as [Vector2, Vector2]
|
---|
45 | }
|
---|
46 | }
|
---|
47 |
|
---|
48 | cancel() {
|
---|
49 | const state = this.state
|
---|
50 | if (state.canceled) return
|
---|
51 | state.canceled = true
|
---|
52 | state._active = false
|
---|
53 | setTimeout(() => {
|
---|
54 | // we run compute with no event so that kinematics won't be computed
|
---|
55 | this.compute()
|
---|
56 | this.emit()
|
---|
57 | }, 0)
|
---|
58 | }
|
---|
59 |
|
---|
60 | setActive() {
|
---|
61 | this.state._active = this.state._pointerActive || this.state._keyboardActive
|
---|
62 | }
|
---|
63 |
|
---|
64 | // superseeds Engine clean function
|
---|
65 | clean() {
|
---|
66 | this.pointerClean()
|
---|
67 | this.state._pointerActive = false
|
---|
68 | this.state._keyboardActive = false
|
---|
69 | super.clean()
|
---|
70 | }
|
---|
71 |
|
---|
72 | pointerDown(event: PointerEvent) {
|
---|
73 | const config = this.config
|
---|
74 | const state = this.state
|
---|
75 |
|
---|
76 | if (
|
---|
77 | event.buttons != null &&
|
---|
78 | // If the user submits an array as pointer.buttons, don't start the drag
|
---|
79 | // if event.buttons isn't included inside that array.
|
---|
80 | (Array.isArray(config.pointerButtons)
|
---|
81 | ? !config.pointerButtons.includes(event.buttons)
|
---|
82 | : // If the user submits a number as pointer.buttons, refuse the drag if
|
---|
83 | // config.pointerButtons is different than `-1` and if event.buttons
|
---|
84 | // doesn't match the combination.
|
---|
85 | config.pointerButtons !== -1 && config.pointerButtons !== event.buttons)
|
---|
86 | )
|
---|
87 | return
|
---|
88 |
|
---|
89 | const ctrlIds = this.ctrl.setEventIds(event)
|
---|
90 | // We need to capture all pointer ids so that we can keep track of them when
|
---|
91 | // they're released off the target
|
---|
92 | if (config.pointerCapture) {
|
---|
93 | ;(event.target as HTMLElement).setPointerCapture(event.pointerId)
|
---|
94 | }
|
---|
95 |
|
---|
96 | if (
|
---|
97 | // in some situations (https://github.com/pmndrs/use-gesture/issues/494#issuecomment-1127584116)
|
---|
98 | // like when a new browser tab is opened during a drag gesture, the drag
|
---|
99 | // can be interrupted mid-way, and can stall. This happens because the
|
---|
100 | // pointerId that initiated the gesture is lost, and since the drag
|
---|
101 | // persists until that pointerId is lifted with pointerup, it never ends.
|
---|
102 | //
|
---|
103 | // Therefore, when we detect that only one pointer is pressing the screen,
|
---|
104 | // we consider that the gesture can proceed.
|
---|
105 | ctrlIds &&
|
---|
106 | ctrlIds.size > 1 &&
|
---|
107 | state._pointerActive
|
---|
108 | )
|
---|
109 | return
|
---|
110 |
|
---|
111 | this.start(event)
|
---|
112 | this.setupPointer(event)
|
---|
113 |
|
---|
114 | state._pointerId = pointerId(event)
|
---|
115 | state._pointerActive = true
|
---|
116 |
|
---|
117 | this.computeValues(pointerValues(event))
|
---|
118 | this.computeInitial()
|
---|
119 |
|
---|
120 | if (config.preventScrollAxis && getPointerType(event) !== 'mouse') {
|
---|
121 | // when preventScrollAxis is set we don't consider the gesture active
|
---|
122 | // until it's deliberate
|
---|
123 | state._active = false
|
---|
124 | this.setupScrollPrevention(event)
|
---|
125 | } else if (config.delay > 0) {
|
---|
126 | this.setupDelayTrigger(event)
|
---|
127 | // makes sure we emit all events when `triggerAllEvents` flag is `true`
|
---|
128 | if (config.triggerAllEvents) {
|
---|
129 | this.compute(event)
|
---|
130 | this.emit()
|
---|
131 | }
|
---|
132 | } else {
|
---|
133 | this.startPointerDrag(event)
|
---|
134 | }
|
---|
135 | }
|
---|
136 |
|
---|
137 | startPointerDrag(event: PointerEvent) {
|
---|
138 | const state = this.state
|
---|
139 | state._active = true
|
---|
140 | state._preventScroll = true
|
---|
141 | state._delayed = false
|
---|
142 |
|
---|
143 | this.compute(event)
|
---|
144 | this.emit()
|
---|
145 | }
|
---|
146 |
|
---|
147 | pointerMove(event: PointerEvent) {
|
---|
148 | const state = this.state
|
---|
149 | const config = this.config
|
---|
150 |
|
---|
151 | if (!state._pointerActive) return
|
---|
152 |
|
---|
153 | const id = pointerId(event)
|
---|
154 | if (state._pointerId !== undefined && id !== state._pointerId) return
|
---|
155 | const _values = pointerValues(event)
|
---|
156 |
|
---|
157 | if (document.pointerLockElement === event.target) {
|
---|
158 | state._delta = [event.movementX, event.movementY]
|
---|
159 | } else {
|
---|
160 | state._delta = V.sub(_values, state._values)
|
---|
161 | this.computeValues(_values)
|
---|
162 | }
|
---|
163 |
|
---|
164 | V.addTo(state._movement, state._delta)
|
---|
165 | this.compute(event)
|
---|
166 |
|
---|
167 | // if the gesture is delayed but deliberate, then we can start it
|
---|
168 | // immediately.
|
---|
169 | if (state._delayed && state.intentional) {
|
---|
170 | this.timeoutStore.remove('dragDelay')
|
---|
171 | // makes sure `first` is still true when moving for the first time after a
|
---|
172 | // delay.
|
---|
173 | state.active = false
|
---|
174 | this.startPointerDrag(event)
|
---|
175 | return
|
---|
176 | }
|
---|
177 |
|
---|
178 | if (config.preventScrollAxis && !state._preventScroll) {
|
---|
179 | if (state.axis) {
|
---|
180 | if (state.axis === config.preventScrollAxis || config.preventScrollAxis === 'xy') {
|
---|
181 | state._active = false
|
---|
182 | this.clean()
|
---|
183 | return
|
---|
184 | } else {
|
---|
185 | this.timeoutStore.remove('startPointerDrag')
|
---|
186 | this.startPointerDrag(event)
|
---|
187 | return
|
---|
188 | }
|
---|
189 | } else {
|
---|
190 | return
|
---|
191 | }
|
---|
192 | }
|
---|
193 |
|
---|
194 | this.emit()
|
---|
195 | }
|
---|
196 |
|
---|
197 | pointerUp(event: PointerEvent) {
|
---|
198 | this.ctrl.setEventIds(event)
|
---|
199 | // We release the pointer id if it has pointer capture
|
---|
200 | try {
|
---|
201 | if (this.config.pointerCapture && (event.target as HTMLElement).hasPointerCapture(event.pointerId)) {
|
---|
202 | // this shouldn't be necessary as it should be automatic when releasing the pointer
|
---|
203 | ;(event.target as HTMLElement).releasePointerCapture(event.pointerId)
|
---|
204 | }
|
---|
205 | } catch {
|
---|
206 | if (process.env.NODE_ENV === 'development') {
|
---|
207 | // eslint-disable-next-line no-console
|
---|
208 | console.warn(
|
---|
209 | `[@use-gesture]: If you see this message, it's likely that you're using an outdated version of \`@react-three/fiber\`. \n\nPlease upgrade to the latest version.`
|
---|
210 | )
|
---|
211 | }
|
---|
212 | }
|
---|
213 |
|
---|
214 | const state = this.state
|
---|
215 | const config = this.config
|
---|
216 |
|
---|
217 | if (!state._active || !state._pointerActive) return
|
---|
218 |
|
---|
219 | const id = pointerId(event)
|
---|
220 | if (state._pointerId !== undefined && id !== state._pointerId) return
|
---|
221 |
|
---|
222 | this.state._pointerActive = false
|
---|
223 | this.setActive()
|
---|
224 | this.compute(event)
|
---|
225 |
|
---|
226 | const [dx, dy] = state._distance
|
---|
227 | state.tap = dx <= config.tapsThreshold && dy <= config.tapsThreshold
|
---|
228 |
|
---|
229 | if (state.tap && config.filterTaps) {
|
---|
230 | state._force = true
|
---|
231 | } else {
|
---|
232 | const [_dx, _dy] = state._delta
|
---|
233 | const [_mx, _my] = state._movement
|
---|
234 | const [svx, svy] = config.swipe.velocity
|
---|
235 | const [sx, sy] = config.swipe.distance
|
---|
236 | const sdt = config.swipe.duration
|
---|
237 |
|
---|
238 | if (state.elapsedTime < sdt) {
|
---|
239 | const _vx = Math.abs(_dx / state.timeDelta)
|
---|
240 | const _vy = Math.abs(_dy / state.timeDelta)
|
---|
241 |
|
---|
242 | if (_vx > svx && Math.abs(_mx) > sx) state.swipe[0] = Math.sign(_dx)
|
---|
243 | if (_vy > svy && Math.abs(_my) > sy) state.swipe[1] = Math.sign(_dy)
|
---|
244 | }
|
---|
245 | }
|
---|
246 |
|
---|
247 | this.emit()
|
---|
248 | }
|
---|
249 |
|
---|
250 | pointerClick(event: MouseEvent) {
|
---|
251 | // event.detail indicates the number of buttons being pressed. When it's
|
---|
252 | // null, it's likely to be a keyboard event from the Enter Key that could
|
---|
253 | // be used for accessibility, and therefore shouldn't be prevented.
|
---|
254 | // See https://github.com/pmndrs/use-gesture/issues/530
|
---|
255 | if (!this.state.tap && event.detail > 0) {
|
---|
256 | event.preventDefault()
|
---|
257 | event.stopPropagation()
|
---|
258 | }
|
---|
259 | }
|
---|
260 |
|
---|
261 | setupPointer(event: PointerEvent) {
|
---|
262 | const config = this.config
|
---|
263 | const device = config.device
|
---|
264 |
|
---|
265 | if (process.env.NODE_ENV === 'development') {
|
---|
266 | try {
|
---|
267 | if (device === 'pointer' && config.preventScrollDelay === undefined) {
|
---|
268 | // @ts-ignore (warning for r3f)
|
---|
269 | const currentTarget = 'uv' in event ? event.sourceEvent.currentTarget : event.currentTarget
|
---|
270 | const style = window.getComputedStyle(currentTarget)
|
---|
271 | if (style.touchAction === 'auto') {
|
---|
272 | // eslint-disable-next-line no-console
|
---|
273 | console.warn(
|
---|
274 | `[@use-gesture]: The drag target has its \`touch-action\` style property set to \`auto\`. It is recommended to add \`touch-action: 'none'\` so that the drag gesture behaves correctly on touch-enabled devices. For more information read this: https://use-gesture.netlify.app/docs/extras/#touch-action.\n\nThis message will only show in development mode. It won't appear in production. If this is intended, you can ignore it.`,
|
---|
275 | currentTarget
|
---|
276 | )
|
---|
277 | }
|
---|
278 | }
|
---|
279 | } catch {}
|
---|
280 | }
|
---|
281 |
|
---|
282 | if (config.pointerLock) {
|
---|
283 | ;(event.currentTarget as HTMLElement).requestPointerLock()
|
---|
284 | }
|
---|
285 |
|
---|
286 | if (!config.pointerCapture) {
|
---|
287 | this.eventStore.add(this.sharedConfig.window, device, 'change', this.pointerMove.bind(this))
|
---|
288 | this.eventStore.add(this.sharedConfig.window, device, 'end', this.pointerUp.bind(this))
|
---|
289 | this.eventStore.add(this.sharedConfig.window, device, 'cancel', this.pointerUp.bind(this))
|
---|
290 | }
|
---|
291 | }
|
---|
292 |
|
---|
293 | pointerClean() {
|
---|
294 | if (this.config.pointerLock && document.pointerLockElement === this.state.currentTarget) {
|
---|
295 | document.exitPointerLock()
|
---|
296 | }
|
---|
297 | }
|
---|
298 |
|
---|
299 | preventScroll(event: PointerEvent) {
|
---|
300 | if (this.state._preventScroll && event.cancelable) {
|
---|
301 | event.preventDefault()
|
---|
302 | }
|
---|
303 | }
|
---|
304 |
|
---|
305 | setupScrollPrevention(event: PointerEvent) {
|
---|
306 | // fixes https://github.com/pmndrs/use-gesture/issues/497
|
---|
307 | this.state._preventScroll = false
|
---|
308 | persistEvent(event)
|
---|
309 | // we add window listeners that will prevent the scroll when the user has started dragging
|
---|
310 | const remove = this.eventStore.add(this.sharedConfig.window, 'touch', 'change', this.preventScroll.bind(this), {
|
---|
311 | passive: false
|
---|
312 | })
|
---|
313 | this.eventStore.add(this.sharedConfig.window, 'touch', 'end', remove)
|
---|
314 | this.eventStore.add(this.sharedConfig.window, 'touch', 'cancel', remove)
|
---|
315 | this.timeoutStore.add('startPointerDrag', this.startPointerDrag.bind(this), this.config.preventScrollDelay!, event)
|
---|
316 | }
|
---|
317 |
|
---|
318 | setupDelayTrigger(event: PointerEvent) {
|
---|
319 | this.state._delayed = true
|
---|
320 | this.timeoutStore.add(
|
---|
321 | 'dragDelay',
|
---|
322 | () => {
|
---|
323 | // forces drag to start no matter the threshold when delay is reached
|
---|
324 | this.state._step = [0, 0]
|
---|
325 | this.startPointerDrag(event)
|
---|
326 | },
|
---|
327 | this.config.delay
|
---|
328 | )
|
---|
329 | }
|
---|
330 |
|
---|
331 | keyDown(event: KeyboardEvent) {
|
---|
332 | // @ts-ignore
|
---|
333 | const deltaFn = KEYS_DELTA_MAP[event.key]
|
---|
334 | if (deltaFn) {
|
---|
335 | const state = this.state
|
---|
336 | const factor = event.shiftKey ? 10 : event.altKey ? 0.1 : 1
|
---|
337 |
|
---|
338 | this.start(event)
|
---|
339 |
|
---|
340 | state._delta = deltaFn(this.config.keyboardDisplacement, factor)
|
---|
341 | state._keyboardActive = true
|
---|
342 | V.addTo(state._movement, state._delta)
|
---|
343 |
|
---|
344 | this.compute(event)
|
---|
345 | this.emit()
|
---|
346 | }
|
---|
347 | }
|
---|
348 |
|
---|
349 | keyUp(event: KeyboardEvent) {
|
---|
350 | if (!(event.key in KEYS_DELTA_MAP)) return
|
---|
351 |
|
---|
352 | this.state._keyboardActive = false
|
---|
353 | this.setActive()
|
---|
354 | this.compute(event)
|
---|
355 | this.emit()
|
---|
356 | }
|
---|
357 |
|
---|
358 | bind(bindFunction: any) {
|
---|
359 | const device = this.config.device
|
---|
360 |
|
---|
361 | bindFunction(device, 'start', this.pointerDown.bind(this))
|
---|
362 |
|
---|
363 | if (this.config.pointerCapture) {
|
---|
364 | bindFunction(device, 'change', this.pointerMove.bind(this))
|
---|
365 | bindFunction(device, 'end', this.pointerUp.bind(this))
|
---|
366 | bindFunction(device, 'cancel', this.pointerUp.bind(this))
|
---|
367 | bindFunction('lostPointerCapture', '', this.pointerUp.bind(this))
|
---|
368 | }
|
---|
369 |
|
---|
370 | if (this.config.keys) {
|
---|
371 | bindFunction('key', 'down', this.keyDown.bind(this))
|
---|
372 | bindFunction('key', 'up', this.keyUp.bind(this))
|
---|
373 | }
|
---|
374 | if (this.config.filterTaps) {
|
---|
375 | bindFunction('click', '', this.pointerClick.bind(this), { capture: true, passive: false })
|
---|
376 | }
|
---|
377 | }
|
---|
378 | }
|
---|
379 |
|
---|
380 | function persistEvent(event: PointerEvent) {
|
---|
381 | // @ts-ignore
|
---|
382 | 'persist' in event && typeof event.persist === 'function' && event.persist()
|
---|
383 | }
|
---|