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

twbs / bootstrap / 4463571712

pending completion
4463571712

push

github

GitHub
Proposal to use `scroll-margin-top` instead of introducing padding and negative margin (#38220)

662 of 722 branches covered (91.69%)

Branch coverage included in aggregate %.

2020 of 2070 relevant lines covered (97.58%)

318.18 hits per line

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

91.05
/js/src/dom/event-handler.js
1
/**
2
 * --------------------------------------------------------------------------
3
 * Bootstrap (v5.3.0-alpha1): 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,129✔
79
}
80

81
function getElementEvents(element) {
82
  const uid = makeEventUid(element)
4,915✔
83

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

87
  return eventRegistry[uid]
4,915✔
88
}
89

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

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

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

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

106
    for (let { target } = event; target && target !== this; target = target.parentNode) {
2,356✔
107
      for (const domElement of domElements) {
11,178✔
108
        if (domElement !== target) {
457✔
109
          continue
306✔
110
        }
111

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

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

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

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

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

135
  if (!nativeEvents.has(typeEvent)) {
4,915✔
136
    typeEvent = originalTypeEvent
178✔
137
  }
138

139
  return [isDelegated, callable, typeEvent]
4,915✔
140
}
141

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

147
  let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction)
3,754✔
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) {
3,754✔
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)
3,754✔
164
  const handlers = events[typeEvent] || (events[typeEvent] = {})
3,754✔
165
  const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null)
3,754✔
166

167
  if (previousFunction) {
3,754✔
168
    previousFunction.oneOff = previousFunction.oneOff && oneOff
1,540!
169

170
    return
1,540✔
171
  }
172

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

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

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

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

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

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

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

201
  for (const [handlerKey, event] of Object.entries(storeElementEvent)) {
300✔
202
    if (handlerKey.includes(namespace)) {
335✔
203
      removeHandler(element, events, typeEvent, event.callable, event.delegationSelector)
174✔
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, '')
12,336✔
211
  return customEvents[event] || event
12,336✔
212
}
213

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

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

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

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

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

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

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

250
    for (const [keyHandlers, event] of Object.entries(storeElementEvent)) {
153✔
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) {
7,421!
261
      return null
×
262
    }
263

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

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

273
    if (inNamespace && $) {
7,421!
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
    let evt = new Event(event, { bubbles, cancelable: true })
7,421✔
283
    evt = hydrateObj(evt, args)
7,421✔
284

285
    if (defaultPrevented) {
7,421!
286
      evt.preventDefault()
×
287
    }
288

289
    if (nativeDispatch) {
7,421✔
290
      element.dispatchEvent(evt)
7,421✔
291
    }
292

293
    if (evt.defaultPrevented && jQueryEvent) {
7,421!
294
      jQueryEvent.preventDefault()
×
295
    }
296

297
    return evt
7,421✔
298
  }
299
}
300

301
function hydrateObj(obj, meta = {}) {
6,965✔
302
  for (const [key, value] of Object.entries(meta)) {
8,367✔
303
    try {
1,572✔
304
      obj[key] = value
1,572✔
305
    } catch {
306
      Object.defineProperty(obj, key, {
1✔
307
        configurable: true,
308
        get() {
309
          return value
1✔
310
        }
311
      })
312
    }
313
  }
314

315
  return obj
8,367✔
316
}
317

318
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