-
Notifications
You must be signed in to change notification settings - Fork 8
/
index.js
154 lines (138 loc) · 4.59 KB
/
index.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
import version from './version'
import { nativeBubblingEventNames } from './events'
let prefix = 'debounced'
const defaultOptions = {
wait: 200, // ........ the number of milliseconds to wait
leading: false, // ... fire event on the leading edge of the timeout
trailing: true // .... fire event on the trailing edge of the timeout
}
const registeredEvents = {}
const timeouts = {}
//
/**
* Event dispatcher used to trigger all custom debounced events.
* @param {Event} sourceEvent - The original native event being debounced
* @param {String} type - The type of debounced event (leading, trailing)
*/
const dispatchDebouncedEvent = (sourceEvent, type) => {
const { bubbles, cancelable, composed } = sourceEvent
const debouncedEvent = new CustomEvent(`${prefix}:${sourceEvent.type}`, {
bubbles,
cancelable,
composed,
detail: { sourceEvent, type }
})
sourceEvent.target.dispatchEvent(debouncedEvent)
}
/**
* Builds an event handler for the sourceEvent that dispatches the debounced event(s).
* @param {Object} options - Debounce options
* @param {Number} options.wait - Milliseconds to wait before dispatching the trailing debounced event
* @param {Boolean} options.leading - Whether or not to dispatch a debounced event BEFORE the sourceEvent
* @param {Boolean} options.trailing - Whether or not to dispatch a debounced event AFTER the sourceEvent
* @returns {Function} - Event handler that dispatches the debounced event(s)
*/
const buildDebounceEventHandler = (options = {}) => {
const { wait, leading, trailing } = { ...defaultOptions, ...options }
return event => {
const key = [event.type, event.target]
// NOTE: Both leading and trailing debounced events are executed on the next tick of the event loop
// This allows the sourceEvent and its handlers to complete before the debounced event is dispatched
// dispatch leading debounced event
if (leading && !timeouts[key]) setTimeout(() => dispatchDebouncedEvent(event, 'leading'))
clearTimeout(timeouts[key]) // reset timeout
// NOTE: setTimeout returns a positive integer
// SEE: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#return_value
timeouts[key] = setTimeout(() => {
// dispatch trailing debounced event
if (trailing) dispatchDebouncedEvent(event, 'trailing')
delete timeouts[key] // cleanup
}, wait)
}
}
/**
* Unregisters an individual event from debouncing.
* @param {String} name - Name of the sourceEvent to unregister
*/
const unregisterEvent = name => {
document.removeEventListener(name, registeredEvents[name]?.handler)
delete registeredEvents[name]
return name
}
/**
* Registers an individual event for debouncing.
* @note Events can be re-registered (replaces existing entry)
* @param {String} name - Name of the sourceEvent to debounce
* @param {Object} options - Debounce options
*/
const registerEvent = (name, options = {}) => {
unregisterEvent(name)
options = { ...defaultOptions, ...options }
options.handler = buildDebounceEventHandler(options)
registeredEvents[name] = options
document.addEventListener(name, options.handler)
return { [name]: registeredEvents[name] }
}
/**
* Unregisters a list of events.
* @param {Array<String>} eventNames - List of event names to unregister
* @returns {Array<String>} - List of event names that were unregistered
*/
const unregister = (eventNames = []) => {
const names = { ...eventNames }
eventNames.forEach(name => unregisterEvent(name))
return names
}
/**
* Initializes debounced events.
*
* @example
* register([
* 'change',
* 'click'
* // more events...
* ], {
* wait: 200,
* leading: false,
* trailing: true
* })
*
* @param {Array<String>} eventNames - List of event names to register
* @param {Object} options - debounce options
*/
const register = (eventNames = [], options = {}) => {
if (!eventNames || eventNames.length === 0) eventNames = nativeBubblingEventNames
eventNames.forEach(name => registerEvent(name, options))
return eventNames.reduce((memo, name) => {
memo[name] = registeredEvents[name]
return memo
}, {})
}
export default {
initialize: register,
register,
unregister,
registerEvent,
unregisterEvent,
get defaultEventNames() {
return [...nativeBubblingEventNames]
},
get defaultOptions() {
return { ...defaultOptions }
},
get prefix() {
return prefix
},
set prefix(value) {
prefix = value
},
get registeredEvents() {
return { ...registeredEvents }
},
get registeredEventNames() {
return Object.keys(registeredEvents)
},
get version() {
return version
}
}