source: imaps-frontend/node_modules/@use-gesture/core/src/engines/DragEngine.ts

main
Last change on this file was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

  • Property mode set to 100644
File size: 12.6 KB
Line 
1import { CoordinatesEngine } from './CoordinatesEngine'
2import { coordinatesConfigResolver } from '../config/coordinatesConfigResolver'
3import { pointerId, getPointerType, pointerValues } from '../utils/events'
4import { V } from '../utils/maths'
5import { Vector2 } from '../types'
6
7const 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
14export 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
380function persistEvent(event: PointerEvent) {
381 // @ts-ignore
382 'persist' in event && typeof event.persist === 'function' && event.persist()
383}
Note: See TracBrowser for help on using the repository browser.