event-handler.js 8.77 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) {
XhmikosR's avatar
XhmikosR committed
139
  for (const uid of Object.keys(events)) {
140
    const event = events[uid]
Johann-S's avatar
Johann-S committed
141

142
143
144
    if (event.originalHandler === handler && event.delegationSelector === delegationSelector) {
      return events[uid]
    }
145
146
  }

147
148
  return null
}
149

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

154
155
156
  // 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
157

158
159
160
  if (custom) {
    typeEvent = custom
  }
161

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

164
165
  if (!isNative) {
    typeEvent = originalTypeEvent
166
167
  }

168
169
  return [delegation, originalHandler, typeEvent]
}
170

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

176
177
178
179
  if (!handler) {
    handler = delegationFn
    delegationFn = null
  }
180

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

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

189
190
    return
  }
191

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

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

201
202
  element.addEventListener(typeEvent, fn, delegation)
}
203

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

207
208
  if (fn === null) {
    return
Johann-S's avatar
Johann-S committed
209
  }
210

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

323
324
    if (defaultPrevented) {
      evt.preventDefault()
325

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

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

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

    return evt
Johann-S's avatar
Johann-S committed
342
  }
343
}
Johann-S's avatar
Johann-S committed
344

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