event-handler.js 8.49 KB
Newer Older
Johann-S's avatar
Johann-S committed
1
2
/**
 * --------------------------------------------------------------------------
3
 * Bootstrap (v5.0.0-alpha1): dom/event-handler.js
4
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
Johann-S's avatar
Johann-S committed
5
6
7
 * --------------------------------------------------------------------------
 */

8
import { getjQuery } from '../util/index'
Johann-S's avatar
Johann-S committed
9
import { defaultPreventedPreservedOnDispatch } from './polyfill'
Johann-S's avatar
Johann-S committed
10

11
12
13
14
15
/**
 * ------------------------------------------------------------------------
 * Constants
 * ------------------------------------------------------------------------
 */
Johann-S's avatar
Johann-S committed
16

17
const $ = getjQuery()
18
19
const namespaceRegex = /[^.]*(?=\..*)\.|.*/
const stripNameRegex = /\..*/
XhmikosR's avatar
XhmikosR committed
20
21
22
23
const stripUidRegex = /::\d+$/
const eventRegistry = {} // Events storage
let uidEvent = 1
const customEvents = {
24
25
26
  mouseenter: 'mouseover',
  mouseleave: 'mouseout'
}
XhmikosR's avatar
XhmikosR committed
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const nativeEvents = [
  'click',
  'dblclick',
  'mouseup',
  'mousedown',
  'contextmenu',
  'mousewheel',
  'DOMMouseScroll',
  'mouseover',
  'mouseout',
  'mousemove',
  'selectstart',
  'selectend',
  'keydown',
  'keypress',
  'keyup',
43
  'orientationchange',
XhmikosR's avatar
XhmikosR committed
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
  'touchstart',
  'touchmove',
  'touchend',
  'touchcancel',
  'pointerdown',
  'pointermove',
  'pointerup',
  'pointerleave',
  'pointercancel',
  'gesturestart',
  'gesturechange',
  'gestureend',
  'focus',
  'blur',
  'change',
  'reset',
  'select',
  'submit',
  'focusin',
  'focusout',
  'load',
  'unload',
  'beforeunload',
  'resize',
  'move',
  'DOMContentLoaded',
  'readystatechange',
  'error',
  'abort',
  'scroll'
74
]
75

76
77
78
79
80
/**
 * ------------------------------------------------------------------------
 * Private methods
 * ------------------------------------------------------------------------
 */
Johann-S's avatar
Johann-S committed
81

82
function getUidEvent(element, uid) {
83
  return (uid && `${uid}::${uidEvent++}`) || element.uidEvent || uidEvent++
84
85
86
87
}

function getEvent(element) {
  const uid = getUidEvent(element)
XhmikosR's avatar
XhmikosR committed
88

89
  element.uidEvent = uid
XhmikosR's avatar
XhmikosR committed
90
  eventRegistry[uid] = eventRegistry[uid] || {}
91

XhmikosR's avatar
XhmikosR committed
92
  return eventRegistry[uid]
93
}
94

95
96
function bootstrapHandler(element, fn) {
  return function handler(event) {
97
    event.delegateTarget = element
XhmikosR's avatar
XhmikosR committed
98

99
100
    if (handler.oneOff) {
      EventHandler.off(element, event.type, fn)
Johann-S's avatar
Johann-S committed
101
    }
Johann-S's avatar
Johann-S committed
102

103
104
105
    return fn.apply(element, [event])
  }
}
106

107
108
109
function bootstrapDelegationHandler(element, selector, fn) {
  return function handler(event) {
    const domElements = element.querySelectorAll(selector)
Johann-S's avatar
Johann-S committed
110

XhmikosR's avatar
XhmikosR committed
111
    for (let { target } = event; target && target !== this; target = target.parentNode) {
112
113
      for (let i = domElements.length; i--;) {
        if (domElements[i] === target) {
114
          event.delegateTarget = target
XhmikosR's avatar
XhmikosR committed
115

116
117
          if (handler.oneOff) {
            EventHandler.off(element, event.type, fn)
Johann-S's avatar
Johann-S committed
118
          }
119
120

          return fn.apply(target, [event])
121
122
123
124
        }
      }
    }

125
126
127
128
    // To please ESLint
    return null
  }
}
129

130
function findHandler(events, handler, delegationSelector = null) {
131
132
133
134
  const uidEventList = Object.keys(events)

  for (let i = 0, len = uidEventList.length; i < len; i++) {
    const event = events[uidEventList[i]]
Johann-S's avatar
Johann-S committed
135

136
    if (event.originalHandler === handler && event.delegationSelector === delegationSelector) {
137
      return event
138
    }
139
140
  }

141
142
  return null
}
143

144
function normalizeParams(originalTypeEvent, handler, delegationFn) {
Johann-S's avatar
Johann-S committed
145
  const delegation = typeof handler === 'string'
146
  const originalHandler = delegation ? delegationFn : handler
147

148
149
150
  // allow to get the native events from namespaced events ('click.bs.button' --> 'click')
  let typeEvent = originalTypeEvent.replace(stripNameRegex, '')
  const custom = customEvents[typeEvent]
Johann-S's avatar
Johann-S committed
151

152
153
154
  if (custom) {
    typeEvent = custom
  }
155

156
  const isNative = nativeEvents.indexOf(typeEvent) > -1
Johann-S's avatar
Johann-S committed
157

158
159
  if (!isNative) {
    typeEvent = originalTypeEvent
160
161
  }

162
163
  return [delegation, originalHandler, typeEvent]
}
164

165
function addHandler(element, originalTypeEvent, handler, delegationFn, oneOff) {
XhmikosR's avatar
XhmikosR committed
166
  if (typeof originalTypeEvent !== 'string' || !element) {
167
168
    return
  }
169

170
171
172
173
  if (!handler) {
    handler = delegationFn
    delegationFn = null
  }
174

175
  const [delegation, originalHandler, typeEvent] = normalizeParams(originalTypeEvent, handler, delegationFn)
XhmikosR's avatar
XhmikosR committed
176
177
  const events = getEvent(element)
  const handlers = events[typeEvent] || (events[typeEvent] = {})
178
  const previousFn = findHandler(handlers, originalHandler, delegation ? handler : null)
179

180
181
  if (previousFn) {
    previousFn.oneOff = previousFn.oneOff && oneOff
Johann-S's avatar
Johann-S committed
182

183
184
    return
  }
185

186
  const uid = getUidEvent(originalHandler, originalTypeEvent.replace(namespaceRegex, ''))
Johann-S's avatar
Johann-S committed
187
188
189
  const fn = delegation ?
    bootstrapDelegationHandler(element, handler, delegationFn) :
    bootstrapHandler(element, handler)
190

191
192
193
194
195
  fn.delegationSelector = delegation ? handler : null
  fn.originalHandler = originalHandler
  fn.oneOff = oneOff
  fn.uidEvent = uid
  handlers[uid] = fn
196

197
198
  element.addEventListener(typeEvent, fn, delegation)
}
199

200
201
function removeHandler(element, events, typeEvent, handler, delegationSelector) {
  const fn = findHandler(events[typeEvent], handler, delegationSelector)
Johann-S's avatar
Johann-S committed
202

203
  if (!fn) {
204
    return
Johann-S's avatar
Johann-S committed
205
  }
206

207
208
209
  element.removeEventListener(typeEvent, fn, Boolean(delegationSelector))
  delete events[typeEvent][fn.uidEvent]
}
210

211
212
function removeNamespacedHandlers(element, events, typeEvent, namespace) {
  const storeElementEvent = events[typeEvent] || {}
213

XhmikosR's avatar
XhmikosR committed
214
215
216
  Object.keys(storeElementEvent).forEach(handlerKey => {
    if (handlerKey.indexOf(namespace) > -1) {
      const event = storeElementEvent[handlerKey]
Johann-S's avatar
Johann-S committed
217

XhmikosR's avatar
XhmikosR committed
218
219
220
      removeHandler(element, events, typeEvent, event.originalHandler, event.delegationSelector)
    }
  })
221
}
Johann-S's avatar
Johann-S committed
222

223
224
225
226
const EventHandler = {
  on(element, event, handler, delegationFn) {
    addHandler(element, event, handler, delegationFn, false)
  },
Johann-S's avatar
Johann-S committed
227

228
229
230
  one(element, event, handler, delegationFn) {
    addHandler(element, event, handler, delegationFn, true)
  },
Johann-S's avatar
Johann-S committed
231

232
  off(element, originalTypeEvent, handler, delegationFn) {
XhmikosR's avatar
XhmikosR committed
233
    if (typeof originalTypeEvent !== 'string' || !element) {
234
235
      return
    }
236

237
238
239
    const [delegation, originalHandler, typeEvent] = normalizeParams(originalTypeEvent, handler, delegationFn)
    const inNamespace = typeEvent !== originalTypeEvent
    const events = getEvent(element)
Johann-S's avatar
Johann-S committed
240
    const isNamespace = originalTypeEvent.charAt(0) === '.'
241

242
243
244
    if (typeof originalHandler !== 'undefined') {
      // Simplest case: handler is passed, remove that listener ONLY.
      if (!events || !events[typeEvent]) {
245
246
247
        return
      }

248
249
250
      removeHandler(element, events, typeEvent, originalHandler, delegation ? handler : null)
      return
    }
251

252
    if (isNamespace) {
XhmikosR's avatar
XhmikosR committed
253
254
255
      Object.keys(events).forEach(elementEvent => {
        removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1))
      })
256
    }
Johann-S's avatar
Johann-S committed
257

258
    const storeElementEvent = events[typeEvent] || {}
XhmikosR's avatar
XhmikosR committed
259
260
    Object.keys(storeElementEvent).forEach(keyHandlers => {
      const handlerKey = keyHandlers.replace(stripUidRegex, '')
Johann-S's avatar
Johann-S committed
261

XhmikosR's avatar
XhmikosR committed
262
263
      if (!inNamespace || originalTypeEvent.indexOf(handlerKey) > -1) {
        const event = storeElementEvent[keyHandlers]
Johann-S's avatar
Johann-S committed
264

XhmikosR's avatar
XhmikosR committed
265
266
267
        removeHandler(element, events, typeEvent, event.originalHandler, event.delegationSelector)
      }
    })
268
  },
Johann-S's avatar
Johann-S committed
269

270
  trigger(element, event, args) {
XhmikosR's avatar
XhmikosR committed
271
    if (typeof event !== 'string' || !element) {
272
273
      return null
    }
274

XhmikosR's avatar
XhmikosR committed
275
    const typeEvent = event.replace(stripNameRegex, '')
276
    const inNamespace = event !== typeEvent
XhmikosR's avatar
XhmikosR committed
277
    const isNative = nativeEvents.indexOf(typeEvent) > -1
278

Johann-S's avatar
Johann-S committed
279
    let jQueryEvent
280
281
282
    let bubbles = true
    let nativeDispatch = true
    let defaultPrevented = false
Johann-S's avatar
Johann-S committed
283
    let evt = null
284

285
    if (inNamespace && $) {
286
      jQueryEvent = $.Event(event, args)
Johann-S's avatar
Johann-S committed
287

288
289
290
291
292
      $(element).trigger(jQueryEvent)
      bubbles = !jQueryEvent.isPropagationStopped()
      nativeDispatch = !jQueryEvent.isImmediatePropagationStopped()
      defaultPrevented = jQueryEvent.isDefaultPrevented()
    }
Johann-S's avatar
Johann-S committed
293

294
295
296
297
    if (isNative) {
      evt = document.createEvent('HTMLEvents')
      evt.initEvent(typeEvent, bubbles, true)
    } else {
Johann-S's avatar
Johann-S committed
298
      evt = new CustomEvent(event, {
299
300
301
302
        bubbles,
        cancelable: true
      })
    }
303

XhmikosR's avatar
XhmikosR committed
304
    // merge custom information in our event
305
    if (typeof args !== 'undefined') {
XhmikosR's avatar
XhmikosR committed
306
307
308
309
310
      Object.keys(args).forEach(key => {
        Object.defineProperty(evt, key, {
          get() {
            return args[key]
          }
311
        })
XhmikosR's avatar
XhmikosR committed
312
      })
313
    }
314

315
316
    if (defaultPrevented) {
      evt.preventDefault()
317

Johann-S's avatar
Johann-S committed
318
      if (!defaultPreventedPreservedOnDispatch) {
319
320
321
        Object.defineProperty(evt, 'defaultPrevented', {
          get: () => true
        })
Johann-S's avatar
Johann-S committed
322
      }
323
324
325
326
327
    }

    if (nativeDispatch) {
      element.dispatchEvent(evt)
    }
328

329
330
    if (evt.defaultPrevented && typeof jQueryEvent !== 'undefined') {
      jQueryEvent.preventDefault()
331
    }
332
333

    return evt
Johann-S's avatar
Johann-S committed
334
  }
335
}
Johann-S's avatar
Johann-S committed
336

Johann-S's avatar
Johann-S committed
337
export default EventHandler