eventHandler.js 9.23 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)
 * --------------------------------------------------------------------------
 */

10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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
})()

// CustomEvent polyfill for IE (see: https://mzl.la/2v76Zvn)
if (typeof window.CustomEvent !== 'function') {
  window.CustomEvent = (event, params) => {
    params = params || {
      bubbles: false,
24
25
      cancelable: false,
      detail: null
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
    }
    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
        })
      }
    }
    return evt
  }

  window.CustomEvent.prototype = window.Event.prototype
}

// 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
63
64
const namespaceRegex = /[^.]*(?=\..*)\.|.*/
const stripNameRegex = /\..*/
Johann-S's avatar
Johann-S committed
65
const keyEventRegex  = /^key/
66
const stripUidRegex  = /::\d+$/
Johann-S's avatar
Johann-S committed
67
68
69
70
71
72
73
74
75
76
77
78
79
80

// Events storage
const eventRegistry = {}
let uidEvent = 1

function getUidEvent(element, uid) {
  return element.uidEvent = uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++
}

function getEvent(element) {
  const uid = getUidEvent(element)
  return eventRegistry[uid] = eventRegistry[uid] || {}
}

81
82
83
84
85
86
87
88
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',
89
  'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout',
90
91
92
  'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange',
  'error', 'abort', 'scroll'
]
Johann-S's avatar
Johann-S committed
93

Johann-S's avatar
Johann-S committed
94
95
96
97
98
99
100
101
102
103
104
105
106
const customEvents = {
  mouseenter: 'mouseover',
  mouseleave: 'mouseout'
}

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
107
108
function bootstrapHandler(element, fn) {
  return function (event) {
Johann-S's avatar
Johann-S committed
109
    event = fixEvent(event)
Johann-S's avatar
Johann-S committed
110
111
112
113
    return fn.apply(element, [event])
  }
}

114
function bootstrapDelegationHandler(element, selector, fn) {
115
  return function (event) {
Johann-S's avatar
Johann-S committed
116
    event = fixEvent(event)
117
    const domElements = element.querySelectorAll(selector)
118
119
120
121
122
123
124
    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])
        }
      }
    }
125
126
    // To please ESLint
    return null
127
128
129
  }
}

130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
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]
}

function removeNamespacedHandlers(element, events, typeEvent, namespace) {
  const storeElementEvent = events[typeEvent] || {}
  for (const handlerKey in storeElementEvent) {
    if (!Object.prototype.hasOwnProperty.call(storeElementEvent, handlerKey)) {
      continue
    }

    if (handlerKey.indexOf(namespace) > -1) {
      removeHandler(element, events, typeEvent, storeElementEvent[handlerKey].originalHandler)
    }
  }
}

Johann-S's avatar
Johann-S committed
150
const EventHandler = {
151
  on(element, originalTypeEvent, handler, delegationFn) {
Johann-S's avatar
Johann-S committed
152
153
154
155
156
    if (typeof originalTypeEvent !== 'string' ||
        (typeof element === 'undefined' || element === null)) {
      return
    }

Johann-S's avatar
Johann-S committed
157
158
    const delegation      = typeof handler === 'string'
    const originalHandler = delegation ? delegationFn : handler
Johann-S's avatar
Johann-S committed
159

Johann-S's avatar
Johann-S committed
160
161
    // 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
162
163
164
165
166
167

    const custom = customEvents[typeEvent]
    if (custom) {
      typeEvent = custom
    }

Johann-S's avatar
Johann-S committed
168
169
170
171
172
173
    const isNative = nativeEvents.indexOf(typeEvent) > -1
    if (!isNative) {
      typeEvent = originalTypeEvent
    }
    const events    = getEvent(element)
    const handlers  = events[typeEvent] || (events[typeEvent] = {})
Johann-S's avatar
Johann-S committed
174
    const uid       = getUidEvent(originalHandler, originalTypeEvent.replace(namespaceRegex, ''))
Johann-S's avatar
Johann-S committed
175
    if (handlers[uid]) {
Johann-S's avatar
Johann-S committed
176
177
      return
    }
Johann-S's avatar
Johann-S committed
178

179
    const fn = !delegation ? bootstrapHandler(element, handler) : bootstrapDelegationHandler(element, handler, delegationFn)
Johann-S's avatar
Johann-S committed
180
    fn.isDelegation = delegation
Johann-S's avatar
Johann-S committed
181
    handlers[uid] = fn
Johann-S's avatar
Johann-S committed
182
    originalHandler.uidEvent = uid
Johann-S's avatar
Johann-S committed
183
    fn.originalHandler = originalHandler
184
    element.addEventListener(typeEvent, fn, delegation)
Johann-S's avatar
Johann-S committed
185
186
187
  },

  one(element, event, handler) {
Johann-S's avatar
Johann-S committed
188
    function complete(e) {
189
      EventHandler.off(element, event, complete)
Johann-S's avatar
Johann-S committed
190
      handler.apply(element, [e])
Johann-S's avatar
Johann-S committed
191
    }
Johann-S's avatar
Johann-S committed
192
    EventHandler.on(element, event, complete)
Johann-S's avatar
Johann-S committed
193
194
  },

Johann-S's avatar
Johann-S committed
195
196
197
198
  off(element, originalTypeEvent, handler) {
    if (typeof originalTypeEvent !== 'string' ||
       (typeof element === 'undefined' || element === null)) {
      return
Johann-S's avatar
Johann-S committed
199
200
    }

Johann-S's avatar
Johann-S committed
201
202
    const events      = getEvent(element)
    let typeEvent     = originalTypeEvent.replace(stripNameRegex, '')
203

Johann-S's avatar
Johann-S committed
204
205
206
207
208
    const inNamespace = typeEvent !== originalTypeEvent
    const custom      = customEvents[typeEvent]
    if (custom) {
      typeEvent = custom
    }
209

Johann-S's avatar
Johann-S committed
210
211
212
    const isNative = nativeEvents.indexOf(typeEvent) > -1
    if (!isNative) {
      typeEvent = originalTypeEvent
Johann-S's avatar
Johann-S committed
213
    }
214

215
216
217
218
219
220
221
222
223
224
225
226
    if (typeof handler !== 'undefined') {
      // Simplest case: handler is passed, remove that listener ONLY.
      if (!events || !events[typeEvent]) {
        return
      }

      removeHandler(element, events, typeEvent, handler)
      return
    }

    const isNamespace = originalTypeEvent.charAt(0) === '.'
    if (isNamespace) {
Johann-S's avatar
Johann-S committed
227
228
229
230
231
      for (const elementEvent in events) {
        if (!Object.prototype.hasOwnProperty.call(events, elementEvent)) {
          continue
        }

232
        removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.substr(1))
Johann-S's avatar
Johann-S committed
233
      }
234
235
236
237
238
239
    }

    const storeElementEvent = events[typeEvent] || {}
    for (const keyHandlers in storeElementEvent) {
      if (!Object.prototype.hasOwnProperty.call(storeElementEvent, keyHandlers)) {
        continue
Johann-S's avatar
Johann-S committed
240
241
      }

242
243
244
245
      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
246
    }
Johann-S's avatar
Johann-S committed
247
  },
Johann-S's avatar
Johann-S committed
248

249
  trigger(element, event, args) {
Johann-S's avatar
Johann-S committed
250
    if (typeof event !== 'string' ||
251
        (typeof element === 'undefined' || element === null)) {
Johann-S's avatar
Johann-S committed
252
253
      return null
    }
Johann-S's avatar
Johann-S committed
254
255

    const typeEvent   = event.replace(stripNameRegex, '')
256
    const inNamespace = event !== typeEvent
Johann-S's avatar
Johann-S committed
257
    const isNative    = nativeEvents.indexOf(typeEvent) > -1
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274

    const $ = Util.jQuery
    let jQueryEvent

    let bubbles = true
    let nativeDispatch = true
    let defaultPrevented = false

    if (inNamespace && typeof $ !== 'undefined') {
      jQueryEvent = new $.Event(event, args)

      $(element).trigger(jQueryEvent)
      bubbles = !jQueryEvent.isPropagationStopped()
      nativeDispatch = !jQueryEvent.isImmediatePropagationStopped()
      defaultPrevented = jQueryEvent.isDefaultPrevented()
    }

Johann-S's avatar
Johann-S committed
275
276
    let evt           = null

Johann-S's avatar
Johann-S committed
277
    if (isNative) {
Johann-S's avatar
Johann-S committed
278
279
      evt = document.createEvent('HTMLEvents')
      evt.initEvent(typeEvent, true, true)
Johann-S's avatar
Johann-S committed
280
    } else {
Johann-S's avatar
Johann-S committed
281
      evt = new CustomEvent(event, {
282
        bubbles,
Johann-S's avatar
Johann-S committed
283
        cancelable: true
Johann-S's avatar
Johann-S committed
284
285
      })
    }
Johann-S's avatar
Johann-S committed
286
287
288
289
290

    // merge custom informations in our event
    if (typeof args !== 'undefined') {
      evt = Util.extend(evt, args)
    }
291
292
293
294
295
296
297
298
299
300
301
302
303

    if (defaultPrevented) {
      evt.preventDefault()
    }

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

    if (evt.defaultPrevented && typeof jQueryEvent !== 'undefined') {
      jQueryEvent.preventDefault()
    }

Johann-S's avatar
Johann-S committed
304
    return evt
Johann-S's avatar
Johann-S committed
305
306
307
  }
}

308
309
310
311
312
313
314
315
316
317
318
319
320
321
// 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
322
export default EventHandler