1 | import { EngineMap } from './actions'
|
---|
2 | import { parse } from './config/resolver'
|
---|
3 | import { isTouch, parseProp, toHandlerProp, touchIds } from './utils/events'
|
---|
4 | import { EventStore } from './EventStore'
|
---|
5 | import { TimeoutStore } from './TimeoutStore'
|
---|
6 | import { chain } from './utils/fn'
|
---|
7 | import { GestureKey, InternalConfig, InternalHandlers, NativeHandlers, State, UserGestureConfig } from './types'
|
---|
8 |
|
---|
9 | export class Controller {
|
---|
10 | /**
|
---|
11 | * The list of gestures handled by the Controller.
|
---|
12 | */
|
---|
13 | public gestures = new Set<GestureKey>()
|
---|
14 | /**
|
---|
15 | * The event store that keeps track of the config.target listeners.
|
---|
16 | */
|
---|
17 | private _targetEventStore = new EventStore(this)
|
---|
18 | /**
|
---|
19 | * Object that keeps track of all gesture event listeners.
|
---|
20 | */
|
---|
21 | public gestureEventStores: { [key in GestureKey]?: EventStore } = {}
|
---|
22 | public gestureTimeoutStores: { [key in GestureKey]?: TimeoutStore } = {}
|
---|
23 | public handlers: InternalHandlers = {}
|
---|
24 | private nativeHandlers?: NativeHandlers
|
---|
25 | public config = {} as InternalConfig
|
---|
26 | public pointerIds = new Set<number>()
|
---|
27 | public touchIds = new Set<number>()
|
---|
28 | public state = {
|
---|
29 | shared: {
|
---|
30 | shiftKey: false,
|
---|
31 | metaKey: false,
|
---|
32 | ctrlKey: false,
|
---|
33 | altKey: false
|
---|
34 | }
|
---|
35 | } as State
|
---|
36 |
|
---|
37 | constructor(handlers: InternalHandlers) {
|
---|
38 | resolveGestures(this, handlers)
|
---|
39 | }
|
---|
40 | /**
|
---|
41 | * Sets pointer or touch ids based on the event.
|
---|
42 | * @param event
|
---|
43 | */
|
---|
44 | setEventIds(event: TouchEvent | PointerEvent) {
|
---|
45 | if (isTouch(event)) {
|
---|
46 | this.touchIds = new Set(touchIds(event as TouchEvent))
|
---|
47 | return this.touchIds
|
---|
48 | } else if ('pointerId' in event) {
|
---|
49 | if (event.type === 'pointerup' || event.type === 'pointercancel') this.pointerIds.delete(event.pointerId)
|
---|
50 | else if (event.type === 'pointerdown') this.pointerIds.add(event.pointerId)
|
---|
51 | return this.pointerIds
|
---|
52 | }
|
---|
53 | }
|
---|
54 | /**
|
---|
55 | * Attaches handlers to the controller.
|
---|
56 | * @param handlers
|
---|
57 | * @param nativeHandlers
|
---|
58 | */
|
---|
59 | applyHandlers(handlers: InternalHandlers, nativeHandlers?: NativeHandlers) {
|
---|
60 | this.handlers = handlers
|
---|
61 | this.nativeHandlers = nativeHandlers
|
---|
62 | }
|
---|
63 | /**
|
---|
64 | * Compute and attaches a config to the controller.
|
---|
65 | * @param config
|
---|
66 | * @param gestureKey
|
---|
67 | */
|
---|
68 | applyConfig(config: UserGestureConfig, gestureKey?: GestureKey) {
|
---|
69 | this.config = parse(config, gestureKey, this.config)
|
---|
70 | }
|
---|
71 | /**
|
---|
72 | * Cleans all side effects (listeners, timeouts). When the gesture is
|
---|
73 | * destroyed (in React, when the component is unmounted.)
|
---|
74 | */
|
---|
75 | clean() {
|
---|
76 | this._targetEventStore.clean()
|
---|
77 | for (const key of this.gestures) {
|
---|
78 | this.gestureEventStores[key]!.clean()
|
---|
79 | this.gestureTimeoutStores[key]!.clean()
|
---|
80 | }
|
---|
81 | }
|
---|
82 | /**
|
---|
83 | * Executes side effects (attaching listeners to a `config.target`). Ran on
|
---|
84 | * each render.
|
---|
85 | */
|
---|
86 | effect() {
|
---|
87 | if (this.config.shared.target) this.bind()
|
---|
88 | return () => this._targetEventStore.clean()
|
---|
89 | }
|
---|
90 | /**
|
---|
91 | * The bind function that can be returned by the gesture handler (a hook in
|
---|
92 | * React for example.)
|
---|
93 | * @param args
|
---|
94 | */
|
---|
95 | bind(...args: any[]) {
|
---|
96 | const sharedConfig = this.config.shared
|
---|
97 | const props: any = {}
|
---|
98 |
|
---|
99 | let target
|
---|
100 | if (sharedConfig.target) {
|
---|
101 | target = sharedConfig.target()
|
---|
102 | // if target is undefined let's stop
|
---|
103 | if (!target) return
|
---|
104 | }
|
---|
105 |
|
---|
106 | if (sharedConfig.enabled) {
|
---|
107 | // Adding gesture handlers
|
---|
108 | for (const gestureKey of this.gestures) {
|
---|
109 | const gestureConfig = this.config[gestureKey]!
|
---|
110 | const bindFunction = bindToProps(props, gestureConfig.eventOptions, !!target)
|
---|
111 | if (gestureConfig.enabled) {
|
---|
112 | const Engine = EngineMap.get(gestureKey)!
|
---|
113 | // @ts-ignore
|
---|
114 | new Engine(this, args, gestureKey).bind(bindFunction)
|
---|
115 | }
|
---|
116 | }
|
---|
117 |
|
---|
118 | // Adding native handlers
|
---|
119 | const nativeBindFunction = bindToProps(props, sharedConfig.eventOptions, !!target)
|
---|
120 | for (const eventKey in this.nativeHandlers) {
|
---|
121 | nativeBindFunction(
|
---|
122 | eventKey,
|
---|
123 | '',
|
---|
124 | // @ts-ignore
|
---|
125 | (event) => this.nativeHandlers[eventKey]({ ...this.state.shared, event, args }),
|
---|
126 | undefined,
|
---|
127 | true
|
---|
128 | )
|
---|
129 | }
|
---|
130 | }
|
---|
131 |
|
---|
132 | // If target isn't set, we return an object that contains gesture handlers
|
---|
133 | // mapped to props handler event keys.
|
---|
134 | for (const handlerProp in props) {
|
---|
135 | props[handlerProp] = chain(...props[handlerProp])
|
---|
136 | }
|
---|
137 |
|
---|
138 | // When target isn't specified then return hanlder props.
|
---|
139 | if (!target) return props
|
---|
140 |
|
---|
141 | // When target is specified, then add listeners to the controller target
|
---|
142 | // store.
|
---|
143 | for (const handlerProp in props) {
|
---|
144 | const { device, capture, passive } = parseProp(handlerProp)
|
---|
145 | this._targetEventStore.add(target, device, '', props[handlerProp], { capture, passive })
|
---|
146 | }
|
---|
147 | }
|
---|
148 | }
|
---|
149 |
|
---|
150 | function setupGesture(ctrl: Controller, gestureKey: GestureKey) {
|
---|
151 | ctrl.gestures.add(gestureKey)
|
---|
152 | ctrl.gestureEventStores[gestureKey] = new EventStore(ctrl, gestureKey)
|
---|
153 | ctrl.gestureTimeoutStores[gestureKey] = new TimeoutStore()
|
---|
154 | }
|
---|
155 |
|
---|
156 | function resolveGestures(ctrl: Controller, internalHandlers: InternalHandlers) {
|
---|
157 | // make sure hover handlers are added first to prevent bugs such as #322
|
---|
158 | // where the hover pointerLeave handler is removed before the move
|
---|
159 | // pointerLeave, which prevents hovering: false to be fired.
|
---|
160 | if (internalHandlers.drag) setupGesture(ctrl, 'drag')
|
---|
161 | if (internalHandlers.wheel) setupGesture(ctrl, 'wheel')
|
---|
162 | if (internalHandlers.scroll) setupGesture(ctrl, 'scroll')
|
---|
163 | if (internalHandlers.move) setupGesture(ctrl, 'move')
|
---|
164 | if (internalHandlers.pinch) setupGesture(ctrl, 'pinch')
|
---|
165 | if (internalHandlers.hover) setupGesture(ctrl, 'hover')
|
---|
166 | }
|
---|
167 |
|
---|
168 | const bindToProps =
|
---|
169 | (props: any, eventOptions: AddEventListenerOptions, withPassiveOption: boolean) =>
|
---|
170 | (
|
---|
171 | device: string,
|
---|
172 | action: string,
|
---|
173 | handler: (event: any) => void,
|
---|
174 | options: AddEventListenerOptions = {},
|
---|
175 | isNative = false
|
---|
176 | ) => {
|
---|
177 | const capture = options.capture ?? eventOptions.capture
|
---|
178 | const passive = options.passive ?? eventOptions.passive
|
---|
179 | // a native handler is already passed as a prop like "onMouseDown"
|
---|
180 | let handlerProp = isNative ? device : toHandlerProp(device, action, capture)
|
---|
181 | if (withPassiveOption && passive) handlerProp += 'Passive'
|
---|
182 | props[handlerProp] = props[handlerProp] || []
|
---|
183 | props[handlerProp].push(handler)
|
---|
184 | }
|
---|