-
Notifications
You must be signed in to change notification settings - Fork 2
/
compose.js
187 lines (156 loc) · 6.19 KB
/
compose.js
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
// @flow
import React from 'react'
import type {ComponentType} from 'react'
type EventName = string
type TimerEvent = EventName | EventName[]
type ComposerSettings = {
eventPropName: EventName,
triggerEvent: TimerEvent,
defaultDuration?: number,
cancelEvent?: TimerEvent,
shouldResetTimerOnRetrigger?: boolean,
beforeHandle?: (
handler: (event?: SyntheticEvent<>) => void,
event?: SyntheticEvent<>
) => boolean | void,
}
const _omit = (props: {}, propToOmit: string): {} => {
let propsCopy = {...props}
delete propsCopy[propToOmit]
return propsCopy
}
type InternalHandler = (eventName: EventName, event?: SyntheticEvent<>) => void
const _eventNamesToHandlerLookup = (
eventNames?: EventName[],
handler: InternalHandler
): {[EventName]: InternalHandler} =>
(eventNames || []).reduce(
(lookup, eventName) => ({
...lookup,
[eventName]: handler.bind(null, eventName),
}),
{}
)
const _isValidDuration = (duration: number): boolean => duration > 0
export default ({
eventPropName,
triggerEvent,
defaultDuration = 0,
cancelEvent,
shouldResetTimerOnRetrigger = true,
beforeHandle = () => true,
}: ComposerSettings) => {
if (process.env.NODE_ENV !== 'production' && !eventPropName) {
throw new Error('`eventPropName` configuration must be specified')
}
if (process.env.NODE_ENV !== 'production' && !triggerEvent) {
throw new Error('`triggerEvent` configuration must be specified')
}
let triggerEvents = Array.isArray(triggerEvent)
? triggerEvent
: [triggerEvent]
let cancelEvents
if (cancelEvent) {
cancelEvents = Array.isArray(cancelEvent) ? cancelEvent : [cancelEvent]
}
return (duration: number) => {
let timeoutDuration = defaultDuration
let durationSuffix = ''
// if defaultDuration is specified and there's a duration override,
// use the override. Otherwise there's no timeout happening
if (_isValidDuration(defaultDuration) && _isValidDuration(duration)) {
timeoutDuration = duration
durationSuffix = `-${duration}`
}
// if the duration is passed the composite even prop name needs to be parameterized
let compositeEventPropName = `${eventPropName}${durationSuffix}`
return <Props: {}>(Component: ComponentType<{}>): ComponentType<Props> => {
if (process.env.NODE_ENV !== 'production' && !Component) {
throw new Error('Component to enhance must be specified')
}
return class extends React.Component<Props> {
static displayName = `CompositeEvent-${eventPropName}${durationSuffix}`
_delayTimeout = null
_callSpecificHandler = (eventName: EventName, e?: SyntheticEvent<>) => {
let onEvent = this.props[eventName]
if (onEvent) {
onEvent(e)
}
}
_clearTimeout = () => {
this._delayTimeout = clearTimeout(this._delayTimeout)
}
_callCompositeEvent = (e?: SyntheticEvent<>) => {
let onCompositeEvent = this.props[compositeEventPropName]
// If a before handle function is defined, call it and check to see what the function returns
// truthy - means that it wants the HOC to to call the final handler with the event object
// falsy - means that it doesn't want the HOC to do anything
// The function receives the composite event handler + event object to make it's decision and
// can call the handler directly
if (beforeHandle(onCompositeEvent, e)) {
onCompositeEvent(e)
}
}
_handleTriggerEvent = (eventName: EventName, e?: SyntheticEvent<>) => {
let onCompositeEvent = this.props[compositeEventPropName]
// If a specific handler was passed, call that one first
this._callSpecificHandler(eventName, e)
// If no composite event handler was passed in, we can
// quit early
if (!onCompositeEvent) {
return
}
// Call the composite event handler
if (_isValidDuration(timeoutDuration)) {
// If shouldResetTimerOnRetrigger flag is not explicitly turned off, we need to
// clear any existing timeout and start a fresh timer because a retrigger happened.
if (shouldResetTimerOnRetrigger !== false) {
this._clearTimeout()
}
// And we can start a timeout as long as we don't have an active one going
if (!this._delayTimeout) {
this._delayTimeout = setTimeout(
() => this._callCompositeEvent(e),
timeoutDuration
)
}
} else {
this._callCompositeEvent(e)
}
}
_handleCancelEvent = (eventName: EventName, e?: SyntheticEvent<>) => {
// If a specific handler was passed, call that one first
this._callSpecificHandler(eventName, e)
// just cancel the timeout so composite handler won't be called
this._clearTimeout()
}
render() {
// Want to pass all the props through to the underlying Component except the passed
// compositeEventPropName, which we need to handle specially.
// This will also include separate specific handlers matching trigger & cancel events
let passThruProps = _omit(this.props, compositeEventPropName)
// Create an object mapping of the trigger/cancel events to handlers.
// The handler needs to bind the event name so that we can check to see if
// a specific handler was specified so we can fire that too
let triggerEventHandlers = _eventNamesToHandlerLookup(
triggerEvents,
this._handleTriggerEvent
)
let cancelEventHandlers = _eventNamesToHandlerLookup(
cancelEvents,
this._handleCancelEvent
)
// As a result of cancelEventHandlers going after triggerEventHandlers below, if
// a cancel event matches a trigger event, the composite event will never be triggered
return (
<Component
{...passThruProps}
{...triggerEventHandlers}
{...cancelEventHandlers}
/>
)
}
}
}
}
}