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

8
import { jQuery as $ } from '../util/index'
Johann-S's avatar
Johann-S committed
9
import { createCustomEvent, 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
18
const namespaceRegex = /[^.]*(?=\..*)\.|.*/
const stripNameRegex = /\..*/
XhmikosR's avatar
XhmikosR committed
19
20
21
22
23
const keyEventRegex = /^key/
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
83
84
85
86
87
function getUidEvent(element, uid) {
  return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++
}

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
function fixEvent(event, element) {
  // Add which for key events
  if (event.which === null && keyEventRegex.test(event.type)) {
XhmikosR's avatar
XhmikosR committed
98
    event.which = event.charCode === null ? event.keyCode : event.charCode
Johann-S's avatar
Johann-S committed
99
100
  }

101
102
  event.delegateTarget = element
}
103

104
105
106
107
108
function bootstrapHandler(element, fn) {
  return function handler(event) {
    fixEvent(event, element)
    if (handler.oneOff) {
      EventHandler.off(element, event.type, fn)
Johann-S's avatar
Johann-S committed
109
    }
Johann-S's avatar
Johann-S committed
110

111
112
113
    return fn.apply(element, [event])
  }
}
114

115
116
117
function bootstrapDelegationHandler(element, selector, fn) {
  return function handler(event) {
    const domElements = element.querySelectorAll(selector)
Johann-S's avatar
Johann-S committed
118

XhmikosR's avatar
XhmikosR committed
119
    for (let { target } = event; target && target !== this; target = target.parentNode) {
120
121
122
      for (let i = domElements.length; i--;) {
        if (domElements[i] === target) {
          fixEvent(event, target)
Johann-S's avatar
Johann-S committed
123

124
125
          if (handler.oneOff) {
            EventHandler.off(element, event.type, fn)
Johann-S's avatar
Johann-S committed
126
          }
127
128

          return fn.apply(target, [event])
129
130
131
132
        }
      }
    }

133
134
135
136
    // To please ESLint
    return null
  }
}
137

138
function findHandler(events, handler, delegationSelector = null) {
139
140
141
142
  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
143

144
    if (event.originalHandler === handler && event.delegationSelector === delegationSelector) {
145
      return event
146
    }
147
148
  }

149
150
  return null
}
151

152
function normalizeParams(originalTypeEvent, handler, delegationFn) {
Johann-S's avatar
Johann-S committed
153
  const delegation = typeof handler === 'string'
154
  const originalHandler = delegation ? delegationFn : handler
155

156
157
158
  // 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
159

160
161
162
  if (custom) {
    typeEvent = custom
  }
163

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

166
167
  if (!isNative) {
    typeEvent = originalTypeEvent
168
169
  }

170
171
  return [delegation, originalHandler, typeEvent]
}
172

173
function addHandler(element, originalTypeEvent, handler, delegationFn, oneOff) {
XhmikosR's avatar
XhmikosR committed
174
  if (typeof originalTypeEvent !== 'string' || !element) {
175
176
    return
  }
177

178
179
180
181
  if (!handler) {
    handler = delegationFn
    delegationFn = null
  }
182

183
  const [delegation, originalHandler, typeEvent] = normalizeParams(originalTypeEvent, handler, delegationFn)
XhmikosR's avatar
XhmikosR committed
184
185
  const events = getEvent(element)
  const handlers = events[typeEvent] || (events[typeEvent] = {})
186
  const previousFn = findHandler(handlers, originalHandler, delegation ? handler : null)
187

188
189
  if (previousFn) {
    previousFn.oneOff = previousFn.oneOff && oneOff
Johann-S's avatar
Johann-S committed
190

191
192
    return
  }
193

194
  const uid = getUidEvent(originalHandler, originalTypeEvent.replace(namespaceRegex, ''))
XhmikosR's avatar
XhmikosR committed
195
  const fn = delegation ? bootstrapDelegationHandler(element, handler, delegationFn) : bootstrapHandler(element, handler)
196

197
198
199
200
201
  fn.delegationSelector = delegation ? handler : null
  fn.originalHandler = originalHandler
  fn.oneOff = oneOff
  fn.uidEvent = uid
  handlers[uid] = fn
202

203
204
  element.addEventListener(typeEvent, fn, delegation)
}
205

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

209
  if (!fn) {
210
    return
Johann-S's avatar
Johann-S committed
211
  }
212

213
214
215
  element.removeEventListener(typeEvent, fn, Boolean(delegationSelector))
  delete events[typeEvent][fn.uidEvent]
}
216

217
218
function removeNamespacedHandlers(element, events, typeEvent, namespace) {
  const storeElementEvent = events[typeEvent] || {}
219

Johann-S's avatar
Johann-S committed
220
  Object.keys(storeElementEvent)
XhmikosR's avatar
XhmikosR committed
221
    .forEach(handlerKey => {
Johann-S's avatar
Johann-S committed
222
223
224
225
226
227
      if (handlerKey.indexOf(namespace) > -1) {
        const event = storeElementEvent[handlerKey]

        removeHandler(element, events, typeEvent, event.originalHandler, event.delegationSelector)
      }
    })
228
}
Johann-S's avatar
Johann-S committed
229

230
231
232
233
const EventHandler = {
  on(element, event, handler, delegationFn) {
    addHandler(element, event, handler, delegationFn, false)
  },
Johann-S's avatar
Johann-S committed
234

235
236
237
  one(element, event, handler, delegationFn) {
    addHandler(element, event, handler, delegationFn, true)
  },
Johann-S's avatar
Johann-S committed
238

239
  off(element, originalTypeEvent, handler, delegationFn) {
XhmikosR's avatar
XhmikosR committed
240
    if (typeof originalTypeEvent !== 'string' || !element) {
241
242
      return
    }
243

244
245
246
    const [delegation, originalHandler, typeEvent] = normalizeParams(originalTypeEvent, handler, delegationFn)
    const inNamespace = typeEvent !== originalTypeEvent
    const events = getEvent(element)
Johann-S's avatar
Johann-S committed
247
    const isNamespace = originalTypeEvent.charAt(0) === '.'
248

249
250
251
    if (typeof originalHandler !== 'undefined') {
      // Simplest case: handler is passed, remove that listener ONLY.
      if (!events || !events[typeEvent]) {
252
253
254
        return
      }

255
256
257
      removeHandler(element, events, typeEvent, originalHandler, delegation ? handler : null)
      return
    }
258

259
    if (isNamespace) {
Johann-S's avatar
Johann-S committed
260
      Object.keys(events)
XhmikosR's avatar
XhmikosR committed
261
        .forEach(elementEvent => {
Johann-S's avatar
Johann-S committed
262
263
          removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.substr(1))
        })
264
    }
Johann-S's avatar
Johann-S committed
265

266
    const storeElementEvent = events[typeEvent] || {}
Johann-S's avatar
Johann-S committed
267
    Object.keys(storeElementEvent)
XhmikosR's avatar
XhmikosR committed
268
      .forEach(keyHandlers => {
Johann-S's avatar
Johann-S committed
269
        const handlerKey = keyHandlers.replace(stripUidRegex, '')
Johann-S's avatar
Johann-S committed
270

Johann-S's avatar
Johann-S committed
271
272
273
274
275
276
        if (!inNamespace || originalTypeEvent.indexOf(handlerKey) > -1) {
          const event = storeElementEvent[keyHandlers]

          removeHandler(element, events, typeEvent, event.originalHandler, event.delegationSelector)
        }
      })
277
  },
Johann-S's avatar
Johann-S committed
278

279
  trigger(element, event, args) {
XhmikosR's avatar
XhmikosR committed
280
    if (typeof event !== 'string' || !element) {
281
282
      return null
    }
283

XhmikosR's avatar
XhmikosR committed
284
    const typeEvent = event.replace(stripNameRegex, '')
285
    const inNamespace = event !== typeEvent
XhmikosR's avatar
XhmikosR committed
286
    const isNative = nativeEvents.indexOf(typeEvent) > -1
287

Johann-S's avatar
Johann-S committed
288
    let jQueryEvent
289
290
291
    let bubbles = true
    let nativeDispatch = true
    let defaultPrevented = false
Johann-S's avatar
Johann-S committed
292
    let evt = null
293

294
295
    if (inNamespace && typeof $ !== 'undefined') {
      jQueryEvent = $.Event(event, args)
Johann-S's avatar
Johann-S committed
296

297
298
299
300
301
      $(element).trigger(jQueryEvent)
      bubbles = !jQueryEvent.isPropagationStopped()
      nativeDispatch = !jQueryEvent.isImmediatePropagationStopped()
      defaultPrevented = jQueryEvent.isDefaultPrevented()
    }
Johann-S's avatar
Johann-S committed
302

303
304
305
306
    if (isNative) {
      evt = document.createEvent('HTMLEvents')
      evt.initEvent(typeEvent, bubbles, true)
    } else {
Johann-S's avatar
Johann-S committed
307
      evt = createCustomEvent(event, {
308
309
310
311
        bubbles,
        cancelable: true
      })
    }
312

313
314
315
    // merge custom informations in our event
    if (typeof args !== 'undefined') {
      Object.keys(args)
XhmikosR's avatar
XhmikosR committed
316
        .forEach(key => {
317
318
319
320
          Object.defineProperty(evt, key, {
            get() {
              return args[key]
            }
Johann-S's avatar
Johann-S committed
321
          })
322
323
        })
    }
324

325
326
    if (defaultPrevented) {
      evt.preventDefault()
327

Johann-S's avatar
Johann-S committed
328
      if (!defaultPreventedPreservedOnDispatch) {
329
330
331
        Object.defineProperty(evt, 'defaultPrevented', {
          get: () => true
        })
Johann-S's avatar
Johann-S committed
332
      }
333
334
335
336
337
    }

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

339
340
    if (evt.defaultPrevented && typeof jQueryEvent !== 'undefined') {
      jQueryEvent.preventDefault()
341
    }
342
343

    return evt
Johann-S's avatar
Johann-S committed
344
  }
345
}
Johann-S's avatar
Johann-S committed
346

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