eventHandler.js 11.1 KB
Newer Older
Johann-S's avatar
Johann-S committed
1
2
import Util from '../util'

Johann-S's avatar
Johann-S committed
3
4
/**
 * --------------------------------------------------------------------------
Johann-S's avatar
Johann-S committed
5
 * Bootstrap (v4.0.0-beta): dom/eventHandler.js
Johann-S's avatar
Johann-S committed
6
7
8
9
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 * --------------------------------------------------------------------------
 */

Johann-S's avatar
Johann-S committed
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const EventHandler = (() => {

  /**
   * ------------------------------------------------------------------------
   * Polyfills
   * ------------------------------------------------------------------------
   */

  // defaultPrevented is broken in IE.
  // https://connect.microsoft.com/IE/feedback/details/790389/event-defaultprevented-returns-false-after-preventdefault-was-called
  const workingDefaultPrevented = (() => {
    const e = document.createEvent('CustomEvent')
    e.initEvent('Bootstrap', true, true)
    e.preventDefault()
    return e.defaultPrevented
  })()
26

Johann-S's avatar
Johann-S committed
27
  let defaultPreventedPreservedOnDispatch = true
28

Johann-S's avatar
Johann-S committed
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
  // CustomEvent polyfill for IE (see: https://mzl.la/2v76Zvn)
  if (typeof window.CustomEvent !== 'function') {
    window.CustomEvent = (event, params) => {
      params = params || {
        bubbles: false,
        cancelable: false,
        detail: null
      }
      const evt = document.createEvent('CustomEvent')
      evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail)
      if (!workingDefaultPrevented) {
        const origPreventDefault = Event.prototype.preventDefault
        evt.preventDefault = () => {
          if (!evt.cancelable) {
            return
          }

          origPreventDefault.call(evt)
          Object.defineProperty(evt, 'defaultPrevented', {
            get() {
              return true
            },
            configurable: true
          })
53
54
        }
      }
Johann-S's avatar
Johann-S committed
55
      return evt
56
57
    }

Johann-S's avatar
Johann-S committed
58
59
60
61
62
63
64
    window.CustomEvent.prototype = window.Event.prototype
  } else {
    // MSEdge resets defaultPrevented flag upon dispatchEvent call if at least one listener is attached
    defaultPreventedPreservedOnDispatch = (() => {
      const e = new CustomEvent('Bootstrap', {
        cancelable: true
      })
65

Johann-S's avatar
Johann-S committed
66
67
      const element = document.createElement('div')
      element.addEventListener('Bootstrap', () => null)
68

Johann-S's avatar
Johann-S committed
69
70
71
72
      e.preventDefault()
      element.dispatchEvent(e)
      return e.defaultPrevented
    })()
73
  }
Johann-S's avatar
Johann-S committed
74

Johann-S's avatar
Johann-S committed
75
76
77
78
79
80
81
82
83
84
85
  // Event constructor shim
  if (!window.Event || typeof window.Event !== 'function') {
    const origEvent = window.Event
    window.Event = (inType, params) => {
      params = params || {}
      const e = document.createEvent('Event')
      e.initEvent(inType, Boolean(params.bubbles), Boolean(params.cancelable))
      return e
    }
    window.Event.prototype = origEvent.prototype
  }
Johann-S's avatar
Johann-S committed
86

Johann-S's avatar
Johann-S committed
87
88
89
90
91
  /**
   * ------------------------------------------------------------------------
   * Constants
   * ------------------------------------------------------------------------
   */
Johann-S's avatar
Johann-S committed
92

Johann-S's avatar
Johann-S committed
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
  const TransitionEndEvent = {
    WebkitTransition : 'webkitTransitionEnd',
    transition       : 'transitionend'
  }
  const namespaceRegex = /[^.]*(?=\..*)\.|.*/
  const stripNameRegex = /\..*/
  const keyEventRegex  = /^key/
  const stripUidRegex  = /::\d+$/
  const eventRegistry  = {}   // Events storage
  let uidEvent         = 1
  const customEvents   = {
    mouseenter: 'mouseover',
    mouseleave: 'mouseout'
  }
  const nativeEvents   = [
    'click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu',
    'mousewheel', 'DOMMouseScroll',
    'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend',
    'keydown', 'keypress', 'keyup',
    'orientationchange',
    'touchstart', 'touchmove', 'touchend', 'touchcancel',
    'gesturestart', 'gesturechange', 'gestureend',
    'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout',
    'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange',
    'error', 'abort', 'scroll'
  ]

  /**
   * ------------------------------------------------------------------------
   * Private methods
   * ------------------------------------------------------------------------
   */


  function getUidEvent(element, uid) {
    return element.uidEvent = uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++
  }
Johann-S's avatar
Johann-S committed
130

Johann-S's avatar
Johann-S committed
131
132
133
134
  function getEvent(element) {
    const uid = getUidEvent(element)
    return eventRegistry[uid] = eventRegistry[uid] || {}
  }
Johann-S's avatar
Johann-S committed
135

Johann-S's avatar
Johann-S committed
136
137
138
139
140
141
  function fixEvent(event) {
    // Add which for key events
    if (event.which === null && keyEventRegex.test(event.type)) {
      event.which = event.charCode !== null ? event.charCode : event.keyCode
    }
    return event
Johann-S's avatar
Johann-S committed
142
143
  }

Johann-S's avatar
Johann-S committed
144
145
146
147
148
  function bootstrapHandler(element, fn) {
    return function (event) {
      event = fixEvent(event)
      return fn.apply(element, [event])
    }
Johann-S's avatar
Johann-S committed
149
150
  }

Johann-S's avatar
Johann-S committed
151
152
153
154
155
156
157
158
159
  function bootstrapDelegationHandler(element, selector, fn) {
    return function (event) {
      event = fixEvent(event)
      const domElements = element.querySelectorAll(selector)
      for (let target = event.target; target && target !== this; target = target.parentNode) {
        for (let i = domElements.length; i--;) {
          if (domElements[i] === target) {
            return fn.apply(target, [event])
          }
160
161
        }
      }
Johann-S's avatar
Johann-S committed
162
163
      // To please ESLint
      return null
164
165
166
    }
  }

Johann-S's avatar
Johann-S committed
167
168
169
170
171
172
  function removeHandler(element, events, typeEvent, handler) {
    const uidEvent = handler.uidEvent
    const fn = events[typeEvent][uidEvent]
    element.removeEventListener(typeEvent, fn, fn.delegation)
    delete events[typeEvent][uidEvent]
  }
173

Johann-S's avatar
Johann-S committed
174
175
176
177
178
179
  function removeNamespacedHandlers(element, events, typeEvent, namespace) {
    const storeElementEvent = events[typeEvent] || {}
    for (const handlerKey in storeElementEvent) {
      if (!Object.prototype.hasOwnProperty.call(storeElementEvent, handlerKey)) {
        continue
      }
180

Johann-S's avatar
Johann-S committed
181
182
183
      if (handlerKey.indexOf(namespace) > -1) {
        removeHandler(element, events, typeEvent, storeElementEvent[handlerKey].originalHandler)
      }
184
185
186
    }
  }

Johann-S's avatar
Johann-S committed
187
188
189
190
191
192
  return {
    on(element, originalTypeEvent, handler, delegationFn) {
      if (typeof originalTypeEvent !== 'string' ||
          (typeof element === 'undefined' || element === null)) {
        return
      }
Johann-S's avatar
Johann-S committed
193

Johann-S's avatar
Johann-S committed
194
195
      const delegation      = typeof handler === 'string'
      const originalHandler = delegation ? delegationFn : handler
Johann-S's avatar
Johann-S committed
196

Johann-S's avatar
Johann-S committed
197
198
      // allow to get the native events from namespaced events ('click.bs.button' --> 'click')
      let typeEvent = originalTypeEvent.replace(stripNameRegex, '')
Johann-S's avatar
Johann-S committed
199

Johann-S's avatar
Johann-S committed
200
201
202
203
      const custom = customEvents[typeEvent]
      if (custom) {
        typeEvent = custom
      }
Johann-S's avatar
Johann-S committed
204

Johann-S's avatar
Johann-S committed
205
206
207
208
209
210
211
212
213
214
      const isNative = nativeEvents.indexOf(typeEvent) > -1
      if (!isNative) {
        typeEvent = originalTypeEvent
      }
      const events    = getEvent(element)
      const handlers  = events[typeEvent] || (events[typeEvent] = {})
      const uid       = getUidEvent(originalHandler, originalTypeEvent.replace(namespaceRegex, ''))
      if (handlers[uid]) {
        return
      }
Johann-S's avatar
Johann-S committed
215

Johann-S's avatar
Johann-S committed
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
      const fn = !delegation ? bootstrapHandler(element, handler) : bootstrapDelegationHandler(element, handler, delegationFn)
      fn.isDelegation = delegation
      handlers[uid] = fn
      originalHandler.uidEvent = uid
      fn.originalHandler = originalHandler
      element.addEventListener(typeEvent, fn, delegation)
    },

    one(element, event, handler) {
      function complete(e) {
        EventHandler.off(element, event, complete)
        handler.apply(element, [e])
      }
      EventHandler.on(element, event, complete)
    },
Johann-S's avatar
Johann-S committed
231

Johann-S's avatar
Johann-S committed
232
233
234
235
236
    off(element, originalTypeEvent, handler) {
      if (typeof originalTypeEvent !== 'string' ||
        (typeof element === 'undefined' || element === null)) {
        return
      }
Johann-S's avatar
Johann-S committed
237

Johann-S's avatar
Johann-S committed
238
239
      const events      = getEvent(element)
      let typeEvent     = originalTypeEvent.replace(stripNameRegex, '')
240

Johann-S's avatar
Johann-S committed
241
242
243
244
245
      const inNamespace = typeEvent !== originalTypeEvent
      const custom      = customEvents[typeEvent]
      if (custom) {
        typeEvent = custom
      }
246

Johann-S's avatar
Johann-S committed
247
248
249
250
251
252
253
254
255
256
      const isNative = nativeEvents.indexOf(typeEvent) > -1
      if (!isNative) {
        typeEvent = originalTypeEvent
      }

      if (typeof handler !== 'undefined') {
        // Simplest case: handler is passed, remove that listener ONLY.
        if (!events || !events[typeEvent]) {
          return
        }
257

Johann-S's avatar
Johann-S committed
258
        removeHandler(element, events, typeEvent, handler)
259
260
261
        return
      }

Johann-S's avatar
Johann-S committed
262
263
264
265
266
267
      const isNamespace = originalTypeEvent.charAt(0) === '.'
      if (isNamespace) {
        for (const elementEvent in events) {
          if (!Object.prototype.hasOwnProperty.call(events, elementEvent)) {
            continue
          }
268

Johann-S's avatar
Johann-S committed
269
          removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.substr(1))
Johann-S's avatar
Johann-S committed
270
271
        }
      }
272

Johann-S's avatar
Johann-S committed
273
274
275
276
277
278
279
280
281
282
      const storeElementEvent = events[typeEvent] || {}
      for (const keyHandlers in storeElementEvent) {
        if (!Object.prototype.hasOwnProperty.call(storeElementEvent, keyHandlers)) {
          continue
        }

        const handlerKey = keyHandlers.replace(stripUidRegex, '')
        if (!inNamespace || originalTypeEvent.indexOf(handlerKey) > -1) {
          removeHandler(element, events, typeEvent, storeElementEvent[keyHandlers].originalHandler)
        }
Johann-S's avatar
Johann-S committed
283
      }
Johann-S's avatar
Johann-S committed
284
    },
Johann-S's avatar
Johann-S committed
285

Johann-S's avatar
Johann-S committed
286
287
288
289
    trigger(element, event, args) {
      if (typeof event !== 'string' ||
          (typeof element === 'undefined' || element === null)) {
        return null
290
      }
Johann-S's avatar
Johann-S committed
291

Johann-S's avatar
Johann-S committed
292
293
294
      const typeEvent   = event.replace(stripNameRegex, '')
      const inNamespace = event !== typeEvent
      const isNative    = nativeEvents.indexOf(typeEvent) > -1
Johann-S's avatar
Johann-S committed
295

Johann-S's avatar
Johann-S committed
296
297
      const $ = Util.jQuery
      let jQueryEvent
298

Johann-S's avatar
Johann-S committed
299
300
301
      let bubbles = true
      let nativeDispatch = true
      let defaultPrevented = false
302

Johann-S's avatar
Johann-S committed
303
304
      if (inNamespace && typeof $ !== 'undefined') {
        jQueryEvent = new $.Event(event, args)
305

Johann-S's avatar
Johann-S committed
306
307
308
309
310
        $(element).trigger(jQueryEvent)
        bubbles = !jQueryEvent.isPropagationStopped()
        nativeDispatch = !jQueryEvent.isImmediatePropagationStopped()
        defaultPrevented = jQueryEvent.isDefaultPrevented()
      }
311

Johann-S's avatar
Johann-S committed
312
      let evt           = null
313

Johann-S's avatar
Johann-S committed
314
315
316
317
318
319
320
321
322
      if (isNative) {
        evt = document.createEvent('HTMLEvents')
        evt.initEvent(typeEvent, bubbles, true)
      } else {
        evt = new CustomEvent(event, {
          bubbles,
          cancelable: true
        })
      }
Johann-S's avatar
Johann-S committed
323

Johann-S's avatar
Johann-S committed
324
325
326
327
      // merge custom informations in our event
      if (typeof args !== 'undefined') {
        evt = Util.extend(evt, args)
      }
Johann-S's avatar
Johann-S committed
328

Johann-S's avatar
Johann-S committed
329
330
      if (defaultPrevented) {
        evt.preventDefault()
331

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

Johann-S's avatar
Johann-S committed
339
340
      if (nativeDispatch) {
        element.dispatchEvent(evt)
341
      }
342

Johann-S's avatar
Johann-S committed
343
344
345
      if (evt.defaultPrevented && typeof jQueryEvent !== 'undefined') {
        jQueryEvent.preventDefault()
      }
346

Johann-S's avatar
Johann-S committed
347
      return evt
348
    }
Johann-S's avatar
Johann-S committed
349
  }
Johann-S's avatar
Johann-S committed
350
})()
Johann-S's avatar
Johann-S committed
351

352
353
354
355
356
357
358
359
360
361
362
363
364
365
// focusin and focusout polyfill
if (typeof window.onfocusin === 'undefined') {
  (() => {
    function listenerFocus(event) {
      EventHandler.trigger(event.target, 'focusin')
    }
    function listenerBlur(event) {
      EventHandler.trigger(event.target, 'focusout')
    }
    EventHandler.on(document, 'focus', 'input', listenerFocus)
    EventHandler.on(document, 'blur', 'input', listenerBlur)
  })()
}

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