eventHandler.js 8.79 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
function getUidEvent(element, uid) {
  return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++
}

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

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

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

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

103
104
  event.delegateTarget = element
}
105

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

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

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

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

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

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

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

140
function findHandler(events, handler, delegationSelector = null) {
Johann-S's avatar
Johann-S committed
141
  const uidList = Object.keys(events)
142

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

147
148
149
    if (event.originalHandler === handler && event.delegationSelector === delegationSelector) {
      return events[uid]
    }
150
151
  }

152
153
  return null
}
154

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

159
160
161
  // 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
162

163
164
165
  if (custom) {
    typeEvent = custom
  }
166

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

169
170
  if (!isNative) {
    typeEvent = originalTypeEvent
171
172
  }

173
174
  return [delegation, originalHandler, typeEvent]
}
175

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

181
182
183
184
  if (!handler) {
    handler = delegationFn
    delegationFn = null
  }
185

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

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

194
195
    return
  }
196

197
  const uid = getUidEvent(originalHandler, originalTypeEvent.replace(namespaceRegex, ''))
XhmikosR's avatar
XhmikosR committed
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
213
  if (fn === null) {
    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
298
    if (inNamespace && typeof $ !== 'undefined') {
      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
310
311
312
313
314
    if (isNative) {
      evt = document.createEvent('HTMLEvents')
      evt.initEvent(typeEvent, bubbles, true)
    } else {
      evt = new CustomEvent(event, {
        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

331
332
333
334
      if (!Polyfill.defaultPreventedPreservedOnDispatch) {
        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