event-handler.js 8.42 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
97
98
function bootstrapHandler(element, fn) {
  return function handler(event) {
    if (handler.oneOff) {
      EventHandler.off(element, event.type, fn)
Johann-S's avatar
Johann-S committed
99
    }
Johann-S's avatar
Johann-S committed
100

101
102
103
    return fn.apply(element, [event])
  }
}
104

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

XhmikosR's avatar
XhmikosR committed
109
    for (let { target } = event; target && target !== this; target = target.parentNode) {
110
111
112
113
      for (let i = domElements.length; i--;) {
        if (domElements[i] === target) {
          if (handler.oneOff) {
            EventHandler.off(element, event.type, fn)
Johann-S's avatar
Johann-S committed
114
          }
115
116

          return fn.apply(target, [event])
117
118
119
120
        }
      }
    }

121
122
123
124
    // To please ESLint
    return null
  }
}
125

126
function findHandler(events, handler, delegationSelector = null) {
127
128
129
130
  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
131

132
    if (event.originalHandler === handler && event.delegationSelector === delegationSelector) {
133
      return event
134
    }
135
136
  }

137
138
  return null
}
139

140
function normalizeParams(originalTypeEvent, handler, delegationFn) {
Johann-S's avatar
Johann-S committed
141
  const delegation = typeof handler === 'string'
142
  const originalHandler = delegation ? delegationFn : handler
143

144
145
146
  // 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
147

148
149
150
  if (custom) {
    typeEvent = custom
  }
151

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

154
155
  if (!isNative) {
    typeEvent = originalTypeEvent
156
157
  }

158
159
  return [delegation, originalHandler, typeEvent]
}
160

161
function addHandler(element, originalTypeEvent, handler, delegationFn, oneOff) {
XhmikosR's avatar
XhmikosR committed
162
  if (typeof originalTypeEvent !== 'string' || !element) {
163
164
    return
  }
165

166
167
168
169
  if (!handler) {
    handler = delegationFn
    delegationFn = null
  }
170

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

176
177
  if (previousFn) {
    previousFn.oneOff = previousFn.oneOff && oneOff
Johann-S's avatar
Johann-S committed
178

179
180
    return
  }
181

182
  const uid = getUidEvent(originalHandler, originalTypeEvent.replace(namespaceRegex, ''))
Johann-S's avatar
Johann-S committed
183
184
185
  const fn = delegation ?
    bootstrapDelegationHandler(element, handler, delegationFn) :
    bootstrapHandler(element, handler)
186

187
188
189
190
191
  fn.delegationSelector = delegation ? handler : null
  fn.originalHandler = originalHandler
  fn.oneOff = oneOff
  fn.uidEvent = uid
  handlers[uid] = fn
192

193
194
  element.addEventListener(typeEvent, fn, delegation)
}
195

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

199
  if (!fn) {
200
    return
Johann-S's avatar
Johann-S committed
201
  }
202

203
204
205
  element.removeEventListener(typeEvent, fn, Boolean(delegationSelector))
  delete events[typeEvent][fn.uidEvent]
}
206

207
208
function removeNamespacedHandlers(element, events, typeEvent, namespace) {
  const storeElementEvent = events[typeEvent] || {}
209

XhmikosR's avatar
XhmikosR committed
210
211
212
  Object.keys(storeElementEvent).forEach(handlerKey => {
    if (handlerKey.indexOf(namespace) > -1) {
      const event = storeElementEvent[handlerKey]
Johann-S's avatar
Johann-S committed
213

XhmikosR's avatar
XhmikosR committed
214
215
216
      removeHandler(element, events, typeEvent, event.originalHandler, event.delegationSelector)
    }
  })
217
}
Johann-S's avatar
Johann-S committed
218

219
220
221
222
const EventHandler = {
  on(element, event, handler, delegationFn) {
    addHandler(element, event, handler, delegationFn, false)
  },
Johann-S's avatar
Johann-S committed
223

224
225
226
  one(element, event, handler, delegationFn) {
    addHandler(element, event, handler, delegationFn, true)
  },
Johann-S's avatar
Johann-S committed
227

228
  off(element, originalTypeEvent, handler, delegationFn) {
XhmikosR's avatar
XhmikosR committed
229
    if (typeof originalTypeEvent !== 'string' || !element) {
230
231
      return
    }
232

233
234
235
    const [delegation, originalHandler, typeEvent] = normalizeParams(originalTypeEvent, handler, delegationFn)
    const inNamespace = typeEvent !== originalTypeEvent
    const events = getEvent(element)
Johann-S's avatar
Johann-S committed
236
    const isNamespace = originalTypeEvent.charAt(0) === '.'
237

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

244
245
246
      removeHandler(element, events, typeEvent, originalHandler, delegation ? handler : null)
      return
    }
247

248
    if (isNamespace) {
XhmikosR's avatar
XhmikosR committed
249
250
251
      Object.keys(events).forEach(elementEvent => {
        removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1))
      })
252
    }
Johann-S's avatar
Johann-S committed
253

254
    const storeElementEvent = events[typeEvent] || {}
XhmikosR's avatar
XhmikosR committed
255
256
    Object.keys(storeElementEvent).forEach(keyHandlers => {
      const handlerKey = keyHandlers.replace(stripUidRegex, '')
Johann-S's avatar
Johann-S committed
257

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

XhmikosR's avatar
XhmikosR committed
261
262
263
        removeHandler(element, events, typeEvent, event.originalHandler, event.delegationSelector)
      }
    })
264
  },
Johann-S's avatar
Johann-S committed
265

266
  trigger(element, event, args) {
XhmikosR's avatar
XhmikosR committed
267
    if (typeof event !== 'string' || !element) {
268
269
      return null
    }
270

XhmikosR's avatar
XhmikosR committed
271
    const typeEvent = event.replace(stripNameRegex, '')
272
    const inNamespace = event !== typeEvent
XhmikosR's avatar
XhmikosR committed
273
    const isNative = nativeEvents.indexOf(typeEvent) > -1
274

Johann-S's avatar
Johann-S committed
275
    let jQueryEvent
276
277
278
    let bubbles = true
    let nativeDispatch = true
    let defaultPrevented = false
Johann-S's avatar
Johann-S committed
279
    let evt = null
280

281
    if (inNamespace && $) {
282
      jQueryEvent = $.Event(event, args)
Johann-S's avatar
Johann-S committed
283

284
285
286
287
288
      $(element).trigger(jQueryEvent)
      bubbles = !jQueryEvent.isPropagationStopped()
      nativeDispatch = !jQueryEvent.isImmediatePropagationStopped()
      defaultPrevented = jQueryEvent.isDefaultPrevented()
    }
Johann-S's avatar
Johann-S committed
289

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

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

311
312
    if (defaultPrevented) {
      evt.preventDefault()
313

Johann-S's avatar
Johann-S committed
314
      if (!defaultPreventedPreservedOnDispatch) {
315
316
317
        Object.defineProperty(evt, 'defaultPrevented', {
          get: () => true
        })
Johann-S's avatar
Johann-S committed
318
      }
319
320
321
322
323
    }

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

325
326
    if (evt.defaultPrevented && typeof jQueryEvent !== 'undefined') {
      jQueryEvent.preventDefault()
327
    }
328
329

    return evt
Johann-S's avatar
Johann-S committed
330
  }
331
}
Johann-S's avatar
Johann-S committed
332

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