eventHandler.js 8.77 KB
Newer Older
Johann-S's avatar
Johann-S committed
1
2
/**
 * --------------------------------------------------------------------------
XhmikosR's avatar
4.3.1.    
XhmikosR committed
3
 * Bootstrap (v4.3.1): dom/eventHandler.js
Johann-S's avatar
Johann-S committed
4
5
6
7
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 * --------------------------------------------------------------------------
 */

8
9
10
import {
  jQuery as $
} from '../util/index'
Johann-S's avatar
Johann-S committed
11
12
import Polyfill from './polyfill'

13
14
15
16
17
/**
 * ------------------------------------------------------------------------
 * Constants
 * ------------------------------------------------------------------------
 */
Johann-S's avatar
Johann-S committed
18

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

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

84
85
86
87
88
89
90
91
92
93
function getUidEvent(element, uid) {
  return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++
}

function getEvent(element) {
  const uid = getUidEvent(element)
  element.uidEvent = uid

  return eventRegistry[uid] = eventRegistry[uid] || {}
}
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) {
Johann-S's avatar
Johann-S committed
139
  const uidList = Object.keys(events)
140

Johann-S's avatar
Johann-S committed
141
142
  for (let i = 0; i < uidList.length; i++) {
    const uid = uidList[i]
143
    const event = events[uid]
Johann-S's avatar
Johann-S committed
144

145
146
147
    if (event.originalHandler === handler && event.delegationSelector === delegationSelector) {
      return events[uid]
    }
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, ''))
XhmikosR's avatar
XhmikosR committed
196
  const fn = delegation ? bootstrapDelegationHandler(element, handler, delegationFn) : bootstrapHandler(element, handler)
197

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

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

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

210
211
  if (fn === null) {
    return
Johann-S's avatar
Johann-S committed
212
  }
213

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

304
305
306
307
308
309
310
311
312
    if (isNative) {
      evt = document.createEvent('HTMLEvents')
      evt.initEvent(typeEvent, bubbles, true)
    } else {
      evt = new CustomEvent(event, {
        bubbles,
        cancelable: true
      })
    }
313

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

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

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

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

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

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

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