• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

twbs / bootstrap / 9856469587

09 Jul 2024 11:49AM CUT coverage: 96.07%. Remained the same
9856469587

Pull #40619

github

web-flow
Merge ac79a1794 into 7c392498f
Pull Request #40619: Docs: Fix a minor accessibility issue (checkout example missing h1)

666 of 726 branches covered (91.74%)

Branch coverage included in aggregate %.

2023 of 2073 relevant lines covered (97.59%)

249.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.01
/js/src/dom/event-handler.js
1
/**
2
 * --------------------------------------------------------------------------
3
 * Bootstrap dom/event-handler.js
4
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
5
 * --------------------------------------------------------------------------
6
 */
7

8
import { getjQuery } from '../util/index.js'
9

10
/**
11
 * Constants
12
 */
13

14
const namespaceRegex = /[^.]*(?=\..*)\.|.*/
18✔
15
const stripNameRegex = /\..*/
18✔
16
const stripUidRegex = /::\d+$/
18✔
17
const eventRegistry = {} // Events storage
18✔
18
let uidEvent = 1
18✔
19
const customEvents = {
18✔
20
  mouseenter: 'mouseover',
21
  mouseleave: 'mouseout'
22
}
23

24
const nativeEvents = new Set([
18✔
25
  'click',
26
  'dblclick',
27
  'mouseup',
28
  'mousedown',
29
  'contextmenu',
30
  'mousewheel',
31
  'DOMMouseScroll',
32
  'mouseover',
33
  'mouseout',
34
  'mousemove',
35
  'selectstart',
36
  'selectend',
37
  'keydown',
38
  'keypress',
39
  'keyup',
40
  'orientationchange',
41
  'touchstart',
42
  'touchmove',
43
  'touchend',
44
  'touchcancel',
45
  'pointerdown',
46
  'pointermove',
47
  'pointerup',
48
  'pointerleave',
49
  'pointercancel',
50
  'gesturestart',
51
  'gesturechange',
52
  'gestureend',
53
  'focus',
54
  'blur',
55
  'change',
56
  'reset',
57
  'select',
58
  'submit',
59
  'focusin',
60
  'focusout',
61
  'load',
62
  'unload',
63
  'beforeunload',
64
  'resize',
65
  'move',
66
  'DOMContentLoaded',
67
  'readystatechange',
68
  'error',
69
  'abort',
70
  'scroll'
71
])
72

73
/**
74
 * Private methods
75
 */
76

77
function makeEventUid(element, uid) {
78
  return (uid && `${uid}::${uidEvent++}`) || element.uidEvent || uidEvent++
7,984✔
79
}
80

81
function getElementEvents(element) {
82
  const uid = makeEventUid(element)
5,555✔
83

84
  element.uidEvent = uid
5,555✔
85
  eventRegistry[uid] = eventRegistry[uid] || {}
5,555✔
86

87
  return eventRegistry[uid]
5,555✔
88
}
89

90
function bootstrapHandler(element, fn) {
91
  return function handler(event) {
2,399✔
92
    hydrateObj(event, { delegateTarget: element })
757✔
93

94
    if (handler.oneOff) {
757✔
95
      EventHandler.off(element, event.type, fn)
21✔
96
    }
97

98
    return fn.apply(element, [event])
757✔
99
  }
100
}
101

102
function bootstrapDelegationHandler(element, selector, fn) {
103
  return function handler(event) {
30✔
104
    const domElements = element.querySelectorAll(selector)
2,320✔
105

106
    for (let { target } = event; target && target !== this; target = target.parentNode) {
2,320✔
107
      for (const domElement of domElements) {
10,886✔
108
        if (domElement !== target) {
459✔
109
          continue
306✔
110
        }
111

112
        hydrateObj(event, { delegateTarget: target })
153✔
113

114
        if (handler.oneOff) {
153✔
115
          EventHandler.off(element, event.type, selector, fn)
1✔
116
        }
117

118
        return fn.apply(target, [event])
153✔
119
      }
120
    }
121
  }
122
}
123

124
function findHandler(events, callable, delegationSelector = null) {
×
125
  return Object.values(events)
5,578✔
126
    .find(event => event.callable === callable && event.delegationSelector === delegationSelector)
4,880✔
127
}
128

129
function normalizeParameters(originalTypeEvent, handler, delegationFunction) {
130
  const isDelegated = typeof handler === 'string'
5,555✔
131
  // TODO: tooltip passes `false` instead of selector, so we need to check
132
  const callable = isDelegated ? delegationFunction : (handler || delegationFunction)
5,555✔
133
  let typeEvent = getTypeEvent(originalTypeEvent)
5,555✔
134

135
  if (!nativeEvents.has(typeEvent)) {
5,555✔
136
    typeEvent = originalTypeEvent
182✔
137
  }
138

139
  return [isDelegated, callable, typeEvent]
5,555✔
140
}
141

142
function addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) {
143
  if (typeof originalTypeEvent !== 'string' || !element) {
4,391✔
144
    return
107✔
145
  }
146

147
  let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction)
4,284✔
148

149
  // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position
150
  // this prevents the handler from being dispatched the same way as mouseover or mouseout does
151
  if (originalTypeEvent in customEvents) {
4,284✔
152
    const wrapFunction = fn => {
5✔
153
      return function (event) {
5✔
154
        if (!event.relatedTarget || (event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget))) {
25✔
155
          return fn.call(this, event)
9✔
156
        }
157
      }
158
    }
159

160
    callable = wrapFunction(callable)
5✔
161
  }
162

163
  const events = getElementEvents(element)
4,284✔
164
  const handlers = events[typeEvent] || (events[typeEvent] = {})
4,284✔
165
  const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null)
4,284✔
166

167
  if (previousFunction) {
4,284✔
168
    previousFunction.oneOff = previousFunction.oneOff && oneOff
1,855!
169

170
    return
1,855✔
171
  }
172

173
  const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, ''))
2,429✔
174
  const fn = isDelegated ?
2,429✔
175
    bootstrapDelegationHandler(element, handler, callable) :
2,429✔
176
    bootstrapHandler(element, callable)
177

178
  fn.delegationSelector = isDelegated ? handler : null
2,429✔
179
  fn.callable = callable
2,429✔
180
  fn.oneOff = oneOff
2,429✔
181
  fn.uidEvent = uid
2,429✔
182
  handlers[uid] = fn
2,429✔
183

184
  element.addEventListener(typeEvent, fn, isDelegated)
2,429✔
185
}
186

187
function removeHandler(element, events, typeEvent, handler, delegationSelector) {
188
  const fn = findHandler(events[typeEvent], handler, delegationSelector)
1,294✔
189

190
  if (!fn) {
1,294!
191
    return
×
192
  }
193

194
  element.removeEventListener(typeEvent, fn, Boolean(delegationSelector))
1,294✔
195
  delete events[typeEvent][fn.uidEvent]
1,294✔
196
}
197

198
function removeNamespacedHandlers(element, events, typeEvent, namespace) {
199
  const storeElementEvent = events[typeEvent] || {}
303!
200

201
  for (const [handlerKey, event] of Object.entries(storeElementEvent)) {
303✔
202
    if (handlerKey.includes(namespace)) {
339✔
203
      removeHandler(element, events, typeEvent, event.callable, event.delegationSelector)
176✔
204
    }
205
  }
206
}
207

208
function getTypeEvent(event) {
209
  // allow to get the native events from namespaced events ('click.bs.button' --> 'click')
210
  event = event.replace(stripNameRegex, '')
9,871✔
211
  return customEvents[event] || event
9,871✔
212
}
213

214
const EventHandler = {
18✔
215
  on(element, event, handler, delegationFunction) {
216
    addHandler(element, event, handler, delegationFunction, false)
4,361✔
217
  },
218

219
  one(element, event, handler, delegationFunction) {
220
    addHandler(element, event, handler, delegationFunction, true)
30✔
221
  },
222

223
  off(element, originalTypeEvent, handler, delegationFunction) {
224
    if (typeof originalTypeEvent !== 'string' || !element) {
1,279✔
225
      return
8✔
226
    }
227

228
    const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction)
1,271✔
229
    const inNamespace = typeEvent !== originalTypeEvent
1,271✔
230
    const events = getElementEvents(element)
1,271✔
231
    const storeElementEvent = events[typeEvent] || {}
1,271✔
232
    const isNamespace = originalTypeEvent.startsWith('.')
1,271✔
233

234
    if (typeof callable !== 'undefined') {
1,271✔
235
      // Simplest case: handler is passed, remove that listener ONLY.
236
      if (!Object.keys(storeElementEvent).length) {
1,116✔
237
        return
35✔
238
      }
239

240
      removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null)
1,081✔
241
      return
1,081✔
242
    }
243

244
    if (isNamespace) {
155✔
245
      for (const elementEvent of Object.keys(events)) {
116✔
246
        removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1))
303✔
247
      }
248
    }
249

250
    for (const [keyHandlers, event] of Object.entries(storeElementEvent)) {
155✔
251
      const handlerKey = keyHandlers.replace(stripUidRegex, '')
38✔
252

253
      if (!inNamespace || originalTypeEvent.includes(handlerKey)) {
38✔
254
        removeHandler(element, events, typeEvent, event.callable, event.delegationSelector)
37✔
255
      }
256
    }
257
  },
258

259
  trigger(element, event, args) {
260
    if (typeof event !== 'string' || !element) {
4,316!
261
      return null
×
262
    }
263

264
    const $ = getjQuery()
4,316✔
265
    const typeEvent = getTypeEvent(event)
4,316✔
266
    const inNamespace = event !== typeEvent
4,316✔
267

268
    let jQueryEvent = null
4,316✔
269
    let bubbles = true
4,316✔
270
    let nativeDispatch = true
4,316✔
271
    let defaultPrevented = false
4,316✔
272

273
    if (inNamespace && $) {
4,316!
274
      jQueryEvent = $.Event(event, args)
×
275

276
      $(element).trigger(jQueryEvent)
×
277
      bubbles = !jQueryEvent.isPropagationStopped()
×
278
      nativeDispatch = !jQueryEvent.isImmediatePropagationStopped()
×
279
      defaultPrevented = jQueryEvent.isDefaultPrevented()
×
280
    }
281

282
    const evt = hydrateObj(new Event(event, { bubbles, cancelable: true }), args)
4,316✔
283

284
    if (defaultPrevented) {
4,316!
285
      evt.preventDefault()
×
286
    }
287

288
    if (nativeDispatch) {
4,316✔
289
      element.dispatchEvent(evt)
4,316✔
290
    }
291

292
    if (evt.defaultPrevented && jQueryEvent) {
4,316!
293
      jQueryEvent.preventDefault()
×
294
    }
295

296
    return evt
4,316✔
297
  }
298
}
299

300
function hydrateObj(obj, meta = {}) {
3,833✔
301
  for (const [key, value] of Object.entries(meta)) {
5,226✔
302
    try {
1,563✔
303
      obj[key] = value
1,563✔
304
    } catch {
305
      Object.defineProperty(obj, key, {
1✔
306
        configurable: true,
307
        get() {
308
          return value
1✔
309
        }
310
      })
311
    }
312
  }
313

314
  return obj
5,226✔
315
}
316

317
export default EventHandler
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc