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

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
98
function bootstrapHandler(element, fn) {
  return function handler(event) {
    if (handler.oneOff) {
      EventHandler.off(element, event.type, fn)
Johann-S's avatar
Johann-S committed
99
    }
Johann-S's avatar
Johann-S committed
100

101
102
103
    return fn.apply(element, [event])
  }
}
104

105
106
107
function bootstrapDelegationHandler(element, selector, fn) {
  return function handler(event) {
    const domElements = element.querySelectorAll(selector)
Johann-S's avatar
Johann-S committed
108

XhmikosR's avatar
XhmikosR committed
109
    for (let { target } = event; target && target !== this; target = target.parentNode) {
110
111
112
113
      for (let i = domElements.length; i--;) {
        if (domElements[i] === target) {
          if (handler.oneOff) {
            EventHandler.off(element, event.type, fn)
Johann-S's avatar
Johann-S committed
114
          }
115
116

          return fn.apply(target, [event])
117
118
119
120
        }
      }
    }

121
122
123
124
    // To please ESLint
    return null
  }
}
125

126
function findHandler(events, handler, delegationSelector = null) {
127
128
129
130
  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
131

132
    if (event.originalHandler === handler && event.delegationSelector === delegationSelector) {
133
      return event
134
    }
135
136
  }

137
138
  return null
}
139

140
function normalizeParams(originalTypeEvent, handler, delegationFn) {
Johann-S's avatar
Johann-S committed
141
  const delegation = typeof handler === 'string'
142
  const originalHandler = delegation ? delegationFn : handler
143

144
145
146
  // 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
147

148
149
150
  if (custom) {
    typeEvent = custom
  }
151

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

154
155
  if (!isNative) {
    typeEvent = originalTypeEvent
156
157
  }

158
159
  return [delegation, originalHandler, typeEvent]
}
160

161
function addHandler(element, originalTypeEvent, handler, delegationFn, oneOff) {
XhmikosR's avatar
XhmikosR committed
162
  if (typeof originalTypeEvent !== 'string' || !element) {
163
164
    return
  }
165

166
167
168
169
  if (!handler) {
    handler = delegationFn
    delegationFn = null
  }
170

171
  const [delegation, originalHandler, typeEvent] = normalizeParams(originalTypeEvent, handler, delegationFn)
XhmikosR's avatar
XhmikosR committed
172
173
  const events = getEvent(element)
  const handlers = events[typeEvent] || (events[typeEvent] = {})
174
  const previousFn = findHandler(handlers, originalHandler, delegation ? handler : null)
175

176
177
  if (previousFn) {
    previousFn.oneOff = previousFn.oneOff && oneOff
Johann-S's avatar
Johann-S committed
178

179
180
    return
  }
181

182
  const uid = getUidEvent(originalHandler, originalTypeEvent.replace(namespaceRegex, ''))
Johann-S's avatar
Johann-S committed
183
184
185
  const fn = delegation ?
    bootstrapDelegationHandler(element, handler, delegationFn) :
    bootstrapHandler(element, handler)
186

187
188
189
190
191
  fn.delegationSelector = delegation ? handler : null
  fn.originalHandler = originalHandler
  fn.oneOff = oneOff
  fn.uidEvent = uid
  handlers[uid] = fn
192

193
194
  element.addEventListener(typeEvent, fn, delegation)
}
195

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

199
  if (!fn) {
200
    return
Johann-S's avatar
Johann-S committed
201
  }
202

203
204
205
  element.removeEventListener(typeEvent, fn, Boolean(delegationSelector))
  delete events[typeEvent][fn.uidEvent]
}
206

207
208
function removeNamespacedHandlers(element, events, typeEvent, namespace) {
  const storeElementEvent = events[typeEvent] || {}
209

Johann-S's avatar
Johann-S committed
210
  Object.keys(storeElementEvent)
XhmikosR's avatar
XhmikosR committed
211
    .forEach(handlerKey => {
Johann-S's avatar
Johann-S committed
212
213
214
215
216
217
      if (handlerKey.indexOf(namespace) > -1) {
        const event = storeElementEvent[handlerKey]

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

220
221
222
223
const EventHandler = {
  on(element, event, handler, delegationFn) {
    addHandler(element, event, handler, delegationFn, false)
  },
Johann-S's avatar
Johann-S committed
224

225
226
227
  one(element, event, handler, delegationFn) {
    addHandler(element, event, handler, delegationFn, true)
  },
Johann-S's avatar
Johann-S committed
228

229
  off(element, originalTypeEvent, handler, delegationFn) {
XhmikosR's avatar
XhmikosR committed
230
    if (typeof originalTypeEvent !== 'string' || !element) {
231
232
      return
    }
233

234
235
236
    const [delegation, originalHandler, typeEvent] = normalizeParams(originalTypeEvent, handler, delegationFn)
    const inNamespace = typeEvent !== originalTypeEvent
    const events = getEvent(element)
Johann-S's avatar
Johann-S committed
237
    const isNamespace = originalTypeEvent.charAt(0) === '.'
238

239
240
241
    if (typeof originalHandler !== 'undefined') {
      // Simplest case: handler is passed, remove that listener ONLY.
      if (!events || !events[typeEvent]) {
242
243
244
        return
      }

245
246
247
      removeHandler(element, events, typeEvent, originalHandler, delegation ? handler : null)
      return
    }
248

249
    if (isNamespace) {
Johann-S's avatar
Johann-S committed
250
      Object.keys(events)
XhmikosR's avatar
XhmikosR committed
251
        .forEach(elementEvent => {
XhmikosR's avatar
XhmikosR committed
252
          removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1))
Johann-S's avatar
Johann-S committed
253
        })
254
    }
Johann-S's avatar
Johann-S committed
255

256
    const storeElementEvent = events[typeEvent] || {}
Johann-S's avatar
Johann-S committed
257
    Object.keys(storeElementEvent)
XhmikosR's avatar
XhmikosR committed
258
      .forEach(keyHandlers => {
Johann-S's avatar
Johann-S committed
259
        const handlerKey = keyHandlers.replace(stripUidRegex, '')
Johann-S's avatar
Johann-S committed
260

Johann-S's avatar
Johann-S committed
261
262
263
264
265
266
        if (!inNamespace || originalTypeEvent.indexOf(handlerKey) > -1) {
          const event = storeElementEvent[keyHandlers]

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

269
  trigger(element, event, args) {
XhmikosR's avatar
XhmikosR committed
270
    if (typeof event !== 'string' || !element) {
271
272
      return null
    }
273

XhmikosR's avatar
XhmikosR committed
274
    const typeEvent = event.replace(stripNameRegex, '')
275
    const inNamespace = event !== typeEvent
XhmikosR's avatar
XhmikosR committed
276
    const isNative = nativeEvents.indexOf(typeEvent) > -1
277

Johann-S's avatar
Johann-S committed
278
    let jQueryEvent
279
280
281
    let bubbles = true
    let nativeDispatch = true
    let defaultPrevented = false
Johann-S's avatar
Johann-S committed
282
    let evt = null
283

284
    if (inNamespace && $) {
285
      jQueryEvent = $.Event(event, args)
Johann-S's avatar
Johann-S committed
286

287
288
289
290
291
      $(element).trigger(jQueryEvent)
      bubbles = !jQueryEvent.isPropagationStopped()
      nativeDispatch = !jQueryEvent.isImmediatePropagationStopped()
      defaultPrevented = jQueryEvent.isDefaultPrevented()
    }
Johann-S's avatar
Johann-S committed
292

293
294
295
296
    if (isNative) {
      evt = document.createEvent('HTMLEvents')
      evt.initEvent(typeEvent, bubbles, true)
    } else {
Johann-S's avatar
Johann-S committed
297
      evt = new CustomEvent(event, {
298
299
300
301
        bubbles,
        cancelable: true
      })
    }
302

303
304
305
    // merge custom informations in our event
    if (typeof args !== 'undefined') {
      Object.keys(args)
XhmikosR's avatar
XhmikosR committed
306
        .forEach(key => {
307
308
309
310
          Object.defineProperty(evt, key, {
            get() {
              return args[key]
            }
Johann-S's avatar
Johann-S committed
311
          })
312
313
        })
    }
314

315
316
    if (defaultPrevented) {
      evt.preventDefault()
317

Johann-S's avatar
Johann-S committed
318
      if (!defaultPreventedPreservedOnDispatch) {
319
320
321
        Object.defineProperty(evt, 'defaultPrevented', {
          get: () => true
        })
Johann-S's avatar
Johann-S committed
322
      }
323
324
325
326
327
    }

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

329
330
    if (evt.defaultPrevented && typeof jQueryEvent !== 'undefined') {
      jQueryEvent.preventDefault()
331
    }
332
333

    return evt
Johann-S's avatar
Johann-S committed
334
  }
335
}
Johann-S's avatar
Johann-S committed
336

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