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 { getjQuery } 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
const $ = getjQuery()
18
19
const namespaceRegex = /[^.]*(?=\..*)\.|.*/
const stripNameRegex = /\..*/
XhmikosR's avatar
XhmikosR committed
20
21
22
23
24
const keyEventRegex = /^key/
const stripUidRegex = /::\d+$/
const eventRegistry = {} // Events storage
let uidEvent = 1
const customEvents = {
25
26
27
  mouseenter: 'mouseover',
  mouseleave: 'mouseout'
}
XhmikosR's avatar
XhmikosR committed
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const nativeEvents = [
  'click',
  'dblclick',
  'mouseup',
  'mousedown',
  'contextmenu',
  'mousewheel',
  'DOMMouseScroll',
  'mouseover',
  'mouseout',
  'mousemove',
  'selectstart',
  'selectend',
  'keydown',
  'keypress',
  'keyup',
44
  'orientationchange',
XhmikosR's avatar
XhmikosR committed
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
74
  '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'
75
]
76

77
78
79
80
81
/**
 * ------------------------------------------------------------------------
 * Private methods
 * ------------------------------------------------------------------------
 */
Johann-S's avatar
Johann-S committed
82

83
function getUidEvent(element, uid) {
84
  return (uid && `${uid}::${uidEvent++}`) || element.uidEvent || uidEvent++
85
86
87
88
}

function getEvent(element) {
  const uid = getUidEvent(element)
XhmikosR's avatar
XhmikosR committed
89

90
  element.uidEvent = uid
XhmikosR's avatar
XhmikosR committed
91
  eventRegistry[uid] = eventRegistry[uid] || {}
92

XhmikosR's avatar
XhmikosR committed
93
  return eventRegistry[uid]
94
}
95

96
97
98
function fixEvent(event, element) {
  // Add which for key events
  if (event.which === null && keyEventRegex.test(event.type)) {
XhmikosR's avatar
XhmikosR committed
99
    event.which = event.charCode === null ? event.keyCode : event.charCode
Johann-S's avatar
Johann-S committed
100
101
  }

102
103
  event.delegateTarget = element
}
104

105
106
107
108
109
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
110
    }
Johann-S's avatar
Johann-S committed
111

112
113
114
    return fn.apply(element, [event])
  }
}
115

116
117
118
function bootstrapDelegationHandler(element, selector, fn) {
  return function handler(event) {
    const domElements = element.querySelectorAll(selector)
Johann-S's avatar
Johann-S committed
119

XhmikosR's avatar
XhmikosR committed
120
    for (let { target } = event; target && target !== this; target = target.parentNode) {
121
122
123
      for (let i = domElements.length; i--;) {
        if (domElements[i] === target) {
          fixEvent(event, target)
Johann-S's avatar
Johann-S committed
124

125
126
          if (handler.oneOff) {
            EventHandler.off(element, event.type, fn)
Johann-S's avatar
Johann-S committed
127
          }
128
129

          return fn.apply(target, [event])
130
131
132
133
        }
      }
    }

134
135
136
137
    // To please ESLint
    return null
  }
}
138

139
function findHandler(events, handler, delegationSelector = null) {
140
141
142
143
  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
144

145
    if (event.originalHandler === handler && event.delegationSelector === delegationSelector) {
146
      return event
147
    }
148
149
  }

150
151
  return null
}
152

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

157
158
159
  // 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
160

161
162
163
  if (custom) {
    typeEvent = custom
  }
164

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

167
168
  if (!isNative) {
    typeEvent = originalTypeEvent
169
170
  }

171
172
  return [delegation, originalHandler, typeEvent]
}
173

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

179
180
181
182
  if (!handler) {
    handler = delegationFn
    delegationFn = null
  }
183

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

189
190
  if (previousFn) {
    previousFn.oneOff = previousFn.oneOff && oneOff
Johann-S's avatar
Johann-S committed
191

192
193
    return
  }
194

195
  const uid = getUidEvent(originalHandler, originalTypeEvent.replace(namespaceRegex, ''))
Johann-S's avatar
Johann-S committed
196
197
198
  const fn = delegation ?
    bootstrapDelegationHandler(element, handler, delegationFn) :
    bootstrapHandler(element, handler)
199

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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