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, ''))
Johann-S's avatar
Johann-S committed
195
196
197
  const fn = delegation ?
    bootstrapDelegationHandler(element, handler, delegationFn) :
    bootstrapHandler(element, handler)
198

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

205
206
  element.addEventListener(typeEvent, fn, delegation)
}
207

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

327
328
    if (defaultPrevented) {
      evt.preventDefault()
329

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

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

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

    return evt
Johann-S's avatar
Johann-S committed
346
  }
347
}
Johann-S's avatar
Johann-S committed
348

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