-
Notifications
You must be signed in to change notification settings - Fork 315
/
Copy pathController.ts
141 lines (126 loc) · 5.25 KB
/
Controller.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import {
StateKey,
State,
Fn,
ReactEventHandlerKey,
ReactEventHandlers,
InternalConfig,
InternalHandlers,
} from './types'
import { getInitialState } from './utils/state'
import { addListeners, removeListeners } from './utils/event'
import { chainFns } from './utils/utils'
type GestureTimeouts = Partial<{ [stateKey in StateKey]: number }>
type WindowListeners = Partial<{ [stateKey in StateKey]: [string, Fn][] }>
type Bindings = Partial<{ [eventName in ReactEventHandlerKey]: Fn[] }>
/**
* The controller will keep track of the state for all gestures and also keep
* track of timeouts, and window listeners.
*
* @template BinderType the type the bind function should return
*/
export default class Controller {
public config!: InternalConfig
public handlers!: Partial<InternalHandlers>
public state: State = getInitialState() // state for all gestures
public timeouts: GestureTimeouts = {} // keeping track of timeouts for debounced gestures (such as move, scroll, wheel)
private domListeners: [string, Fn][] = [] // when config.domTarget is set, we attach events directly to the dom
private windowListeners: WindowListeners = {} // keeps track of window listeners added by gestures (drag only at the moment)
private bindings: Bindings = {} // an object holding the handlers associated to the gestures
/**
* Function ran on component unmount: cleans timeouts and removes dom listeners set by the bind function.
*/
public clean = (): void => {
this.resetBindings()
Object.values(this.timeouts).forEach(clearTimeout)
Object.keys(this.windowListeners).forEach(stateKey => this.removeWindowListeners(stateKey as StateKey))
}
/**
* Function run every time the bind function is run (ie on every render).
* Resets the binding object and remove dom listeners attached to config.domTarget
*/
public resetBindings = (): void => {
this.bindings = {}
const domTarget = this.getDomTarget()
if (domTarget) {
removeListeners(domTarget, this.domListeners, this.config.eventOptions)
this.domListeners = []
}
}
/**
* Returns the domTarget element and parses a ref if needed.
*/
private getDomTarget = (): EventTarget | null | undefined => {
const { domTarget } = this.config
return domTarget && 'current' in domTarget ? domTarget.current : domTarget
}
/**
* Commodity function to let recognizers simply add listeners to config.window.
*/
public addWindowListeners = (stateKey: StateKey, listeners: [string, Fn][]): void => {
if (!this.config.window) return
// we use this.windowListeners to keep track of the listeners we add
this.windowListeners[stateKey] = listeners
addListeners(this.config.window, listeners, this.config.eventOptions)
}
/**
* Commodity function to let recognizers simply remove listeners to config.window.
*/
public removeWindowListeners = (stateKey: StateKey): void => {
if (!this.config.window) return
const listeners = this.windowListeners[stateKey]
if (listeners) {
removeListeners(this.config.window, listeners, this.config.eventOptions)
delete this.windowListeners[stateKey]
}
}
/**
* When config.domTarget is set, this function will add dom listeners to it
*/
public addDomTargetListeners = (target: EventTarget): void => {
/** We iterate on the entries of this.binding for each event, then we chain
* the array of functions mapped to it and push them to this.domListeners
*/
Object.entries(this.bindings).forEach(([event, fns]) => {
this.domListeners.push([event.substr(2).toLowerCase(), chainFns(...(fns as Fn[]))])
})
addListeners(target, this.domListeners, this.config.eventOptions)
}
/**
* this.bindings is an object which keys match ReactEventHandlerKeys.
* Since a recognizer might want to bind a handler function to an event key already used by a previously
* added recognizer, we need to make sure that each event key is an array of all the functions mapped for
* that key.
*/
public addBindings = (eventNames: ReactEventHandlerKey | ReactEventHandlerKey[], fn: Fn): void => {
const eventNamesArray = !Array.isArray(eventNames) ? [eventNames] : eventNames
eventNamesArray.forEach(eventName => {
if (this.bindings[eventName]) this.bindings[eventName]!.push(fn)
else this.bindings[eventName] = [fn]
})
}
/**
* getBindings will return an object that will be bound by users
* to the react component they want to interact with.
*/
public getBindings = (): ReactEventHandlers => {
const bindings: ReactEventHandlers = {}
const { captureString } = this.config
Object.entries(this.bindings).forEach(([event, fns]) => {
const fnsArray = Array.isArray(fns) ? fns : [fns]
const key = (event + captureString) as ReactEventHandlerKey
bindings[key] = chainFns(...(fnsArray as Fn[]))
})
return bindings
}
public getBind = () => {
// If config.domTarget is set we add event listeners to it and return the clean function.
if (this.config.domTarget) {
const domTarget = this.getDomTarget()
domTarget && this.addDomTargetListeners(domTarget)
return this.clean
}
// If not, we return an object that contains gesture handlers mapped to react handler event keys.
return this.getBindings()
}
}