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

twbs / bootstrap / 15434290827

04 Jun 2025 05:23AM CUT coverage: 96.074% (+0.07%) from 96.003%
15434290827

push

github

web-flow
Build(deps-dev): Bump zod from 3.25.48 to 3.25.49 (#41513)

Bumps the development-dependencies group with 1 update: [zod](https://github.com/colinhacks/zod).


Updates `zod` from 3.25.48 to 3.25.49
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v3.25.48...v3.25.49)

---
updated-dependencies:
- dependency-name: zod
  dependency-version: 3.25.49
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

668 of 728 branches covered (91.76%)

Branch coverage included in aggregate %.

2024 of 2074 relevant lines covered (97.59%)

360.98 hits per line

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

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

8
import * as Popper from '@popperjs/core'
9
import BaseComponent from './base-component.js'
10
import EventHandler from './dom/event-handler.js'
11
import Manipulator from './dom/manipulator.js'
12
import {
13
  defineJQueryPlugin, execute, findShadowRoot, getElement, getUID, isRTL, noop
14
} from './util/index.js'
15
import { DefaultAllowlist } from './util/sanitizer.js'
16
import TemplateFactory from './util/template-factory.js'
17

18
/**
19
 * Constants
20
 */
21

22
const NAME = 'tooltip'
2✔
23
const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn'])
2✔
24

25
const CLASS_NAME_FADE = 'fade'
2✔
26
const CLASS_NAME_MODAL = 'modal'
2✔
27
const CLASS_NAME_SHOW = 'show'
2✔
28

29
const SELECTOR_TOOLTIP_INNER = '.tooltip-inner'
2✔
30
const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`
2✔
31

32
const EVENT_MODAL_HIDE = 'hide.bs.modal'
2✔
33

34
const TRIGGER_HOVER = 'hover'
2✔
35
const TRIGGER_FOCUS = 'focus'
2✔
36
const TRIGGER_CLICK = 'click'
2✔
37
const TRIGGER_MANUAL = 'manual'
2✔
38

39
const EVENT_HIDE = 'hide'
2✔
40
const EVENT_HIDDEN = 'hidden'
2✔
41
const EVENT_SHOW = 'show'
2✔
42
const EVENT_SHOWN = 'shown'
2✔
43
const EVENT_INSERTED = 'inserted'
2✔
44
const EVENT_CLICK = 'click'
2✔
45
const EVENT_FOCUSIN = 'focusin'
2✔
46
const EVENT_FOCUSOUT = 'focusout'
2✔
47
const EVENT_MOUSEENTER = 'mouseenter'
2✔
48
const EVENT_MOUSELEAVE = 'mouseleave'
2✔
49

50
const AttachmentMap = {
2✔
51
  AUTO: 'auto',
52
  TOP: 'top',
53
  RIGHT: isRTL() ? 'left' : 'right',
2!
54
  BOTTOM: 'bottom',
55
  LEFT: isRTL() ? 'right' : 'left'
2!
56
}
57

58
const Default = {
2✔
59
  allowList: DefaultAllowlist,
60
  animation: true,
61
  boundary: 'clippingParents',
62
  container: false,
63
  customClass: '',
64
  delay: 0,
65
  fallbackPlacements: ['top', 'right', 'bottom', 'left'],
66
  html: false,
67
  offset: [0, 6],
68
  placement: 'top',
69
  popperConfig: null,
70
  sanitize: true,
71
  sanitizeFn: null,
72
  selector: false,
73
  template: '<div class="tooltip" role="tooltip">' +
74
            '<div class="tooltip-arrow"></div>' +
75
            '<div class="tooltip-inner"></div>' +
76
            '</div>',
77
  title: '',
78
  trigger: 'hover focus'
79
}
80

81
const DefaultType = {
2✔
82
  allowList: 'object',
83
  animation: 'boolean',
84
  boundary: '(string|element)',
85
  container: '(string|element|boolean)',
86
  customClass: '(string|function)',
87
  delay: '(number|object)',
88
  fallbackPlacements: 'array',
89
  html: 'boolean',
90
  offset: '(array|string|function)',
91
  placement: '(string|function)',
92
  popperConfig: '(null|object|function)',
93
  sanitize: 'boolean',
94
  sanitizeFn: '(null|function)',
95
  selector: '(string|boolean)',
96
  template: 'string',
97
  title: '(string|element|function)',
98
  trigger: 'string'
99
}
100

101
/**
102
 * Class definition
103
 */
104

105
class Tooltip extends BaseComponent {
106
  constructor(element, config) {
107
    if (typeof Popper === 'undefined') {
109!
108
      throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org/docs/v2/)')
×
109
    }
110

111
    super(element, config)
109✔
112

113
    // Private
114
    this._isEnabled = true
109✔
115
    this._timeout = 0
109✔
116
    this._isHovered = null
109✔
117
    this._activeTrigger = {}
109✔
118
    this._popper = null
109✔
119
    this._templateFactory = null
109✔
120
    this._newContent = null
109✔
121

122
    // Protected
123
    this.tip = null
109✔
124

125
    this._setListeners()
109✔
126

127
    if (!this._config.selector) {
109✔
128
      this._fixTitle()
108✔
129
    }
130
  }
131

132
  // Getters
133
  static get Default() {
134
    return Default
359✔
135
  }
136

137
  static get DefaultType() {
138
    return DefaultType
87✔
139
  }
140

141
  static get NAME() {
142
    return NAME
756✔
143
  }
144

145
  // Public
146
  enable() {
147
    this._isEnabled = true
1✔
148
  }
149

150
  disable() {
151
    this._isEnabled = false
2✔
152
  }
153

154
  toggleEnabled() {
155
    this._isEnabled = !this._isEnabled
1✔
156
  }
157

158
  toggle() {
159
    if (!this._isEnabled) {
10✔
160
      return
1✔
161
    }
162

163
    if (this._isShown()) {
9✔
164
      this._leave()
3✔
165
      return
3✔
166
    }
167

168
    this._enter()
6✔
169
  }
170

171
  dispose() {
172
    clearTimeout(this._timeout)
6✔
173

174
    EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler)
6✔
175

176
    if (this._element.getAttribute('data-bs-original-title')) {
6✔
177
      this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title'))
5✔
178
    }
179

180
    this._disposePopper()
6✔
181
    super.dispose()
6✔
182
  }
183

184
  show() {
185
    if (this._element.style.display === 'none') {
60✔
186
      throw new Error('Please use show on visible elements')
1✔
187
    }
188

189
    if (!(this._isWithContent() && this._isEnabled)) {
59✔
190
      return
2✔
191
    }
192

193
    const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW))
57✔
194
    const shadowRoot = findShadowRoot(this._element)
57✔
195
    const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element)
57✔
196

197
    if (showEvent.defaultPrevented || !isInTheDom) {
57✔
198
      return
3✔
199
    }
200

201
    // TODO: v6 remove this or make it optional
202
    this._disposePopper()
54✔
203

204
    const tip = this._getTipElement()
54✔
205

206
    this._element.setAttribute('aria-describedby', tip.getAttribute('id'))
54✔
207

208
    const { container } = this._config
54✔
209

210
    if (!this._element.ownerDocument.documentElement.contains(this.tip)) {
54✔
211
      container.append(tip)
54✔
212
      EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED))
54✔
213
    }
214

215
    this._popper = this._createPopper(tip)
54✔
216

217
    tip.classList.add(CLASS_NAME_SHOW)
54✔
218

219
    // If this is a touch-enabled device we add extra
220
    // empty mouseover listeners to the body's immediate children;
221
    // only needed because of broken event delegation on iOS
222
    // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
223
    if ('ontouchstart' in document.documentElement) {
54✔
224
      for (const element of [].concat(...document.body.children)) {
34✔
225
        EventHandler.on(element, 'mouseover', noop)
1,221✔
226
      }
227
    }
228

229
    const complete = () => {
54✔
230
      EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN))
54✔
231

232
      if (this._isHovered === false) {
54✔
233
        this._leave()
7✔
234
      }
235

236
      this._isHovered = false
54✔
237
    }
238

239
    this._queueCallback(complete, this.tip, this._isAnimated())
54✔
240
  }
241

242
  hide() {
243
    if (!this._isShown()) {
17✔
244
      return
2✔
245
    }
246

247
    const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE))
15✔
248
    if (hideEvent.defaultPrevented) {
15✔
249
      return
1✔
250
    }
251

252
    const tip = this._getTipElement()
14✔
253
    tip.classList.remove(CLASS_NAME_SHOW)
14✔
254

255
    // If this is a touch-enabled device we remove the extra
256
    // empty mouseover listeners we added for iOS support
257
    if ('ontouchstart' in document.documentElement) {
14✔
258
      for (const element of [].concat(...document.body.children)) {
9✔
259
        EventHandler.off(element, 'mouseover', noop)
323✔
260
      }
261
    }
262

263
    this._activeTrigger[TRIGGER_CLICK] = false
14✔
264
    this._activeTrigger[TRIGGER_FOCUS] = false
14✔
265
    this._activeTrigger[TRIGGER_HOVER] = false
14✔
266
    this._isHovered = null // it is a trick to support manual triggering
14✔
267

268
    const complete = () => {
14✔
269
      if (this._isWithActiveTrigger()) {
14✔
270
        return
1✔
271
      }
272

273
      if (!this._isHovered) {
13✔
274
        this._disposePopper()
13✔
275
      }
276

277
      this._element.removeAttribute('aria-describedby')
13✔
278
      EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN))
13✔
279
    }
280

281
    this._queueCallback(complete, this.tip, this._isAnimated())
14✔
282
  }
283

284
  update() {
285
    if (this._popper) {
2✔
286
      this._popper.update()
1✔
287
    }
288
  }
289

290
  // Protected
291
  _isWithContent() {
292
    return Boolean(this._getTitle())
46✔
293
  }
294

295
  _getTipElement() {
296
    if (!this.tip) {
186✔
297
      this.tip = this._createTipElement(this._newContent || this._getContentForTemplate())
65✔
298
    }
299

300
    return this.tip
186✔
301
  }
302

303
  _createTipElement(content) {
304
    const tip = this._getTemplateFactory(content).toHtml()
65✔
305

306
    // TODO: remove this check in v6
307
    if (!tip) {
65!
308
      return null
×
309
    }
310

311
    tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW)
65✔
312
    // TODO: v6 the following can be achieved with CSS only
313
    tip.classList.add(`bs-${this.constructor.NAME}-auto`)
65✔
314

315
    const tipId = getUID(this.constructor.NAME).toString()
65✔
316

317
    tip.setAttribute('id', tipId)
65✔
318

319
    if (this._isAnimated()) {
65✔
320
      tip.classList.add(CLASS_NAME_FADE)
61✔
321
    }
322

323
    return tip
65✔
324
  }
325

326
  setContent(content) {
327
    this._newContent = content
12✔
328
    if (this._isShown()) {
12✔
329
      this._disposePopper()
1✔
330
      this.show()
1✔
331
    }
332
  }
333

334
  _getTemplateFactory(content) {
335
    if (this._templateFactory) {
65✔
336
      this._templateFactory.changeContent(content)
6✔
337
    } else {
338
      this._templateFactory = new TemplateFactory({
59✔
339
        ...this._config,
340
        // the `content` var has to be after `this._config`
341
        // to override config.content in case of popover
342
        content,
343
        extraClass: this._resolvePossibleFunction(this._config.customClass)
344
      })
345
    }
346

347
    return this._templateFactory
65✔
348
  }
349

350
  _getContentForTemplate() {
351
    return {
43✔
352
      [SELECTOR_TOOLTIP_INNER]: this._getTitle()
353
    }
354
  }
355

356
  _getTitle() {
357
    return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title')
124✔
358
  }
359

360
  // Private
361
  _initializeOnDelegatedTarget(event) {
362
    return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig())
18✔
363
  }
364

365
  _isAnimated() {
366
    return this._config.animation || (this.tip && this.tip.classList.contains(CLASS_NAME_FADE))
133✔
367
  }
368

369
  _isShown() {
370
    return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW)
58✔
371
  }
372

373
  _createPopper(tip) {
374
    const placement = execute(this._config.placement, [this, tip, this._element])
54✔
375
    const attachment = AttachmentMap[placement.toUpperCase()]
54✔
376
    return Popper.createPopper(this._element, tip, this._getPopperConfig(attachment))
54✔
377
  }
378

379
  _getOffset() {
380
    const { offset } = this._config
58✔
381

382
    if (typeof offset === 'string') {
58✔
383
      return offset.split(',').map(value => Number.parseInt(value, 10))
2✔
384
    }
385

386
    if (typeof offset === 'function') {
57✔
387
      return popperData => offset(popperData, this._element)
30✔
388
    }
389

390
    return offset
55✔
391
  }
392

393
  _resolvePossibleFunction(arg) {
394
    return execute(arg, [this._element, this._element])
201✔
395
  }
396

397
  _getPopperConfig(attachment) {
398
    const defaultBsPopperConfig = {
56✔
399
      placement: attachment,
400
      modifiers: [
401
        {
402
          name: 'flip',
403
          options: {
404
            fallbackPlacements: this._config.fallbackPlacements
405
          }
406
        },
407
        {
408
          name: 'offset',
409
          options: {
410
            offset: this._getOffset()
411
          }
412
        },
413
        {
414
          name: 'preventOverflow',
415
          options: {
416
            boundary: this._config.boundary
417
          }
418
        },
419
        {
420
          name: 'arrow',
421
          options: {
422
            element: `.${this.constructor.NAME}-arrow`
423
          }
424
        },
425
        {
426
          name: 'preSetPlacement',
427
          enabled: true,
428
          phase: 'beforeMain',
429
          fn: data => {
430
            // Pre-set Popper's placement attribute in order to read the arrow sizes properly.
431
            // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement
432
            this._getTipElement().setAttribute('data-popper-placement', data.state.placement)
90✔
433
          }
434
        }
435
      ]
436
    }
437

438
    return {
56✔
439
      ...defaultBsPopperConfig,
440
      ...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])
441
    }
442
  }
443

444
  _setListeners() {
445
    const triggers = this._config.trigger.split(' ')
109✔
446

447
    for (const trigger of triggers) {
109✔
448
      if (trigger === 'click') {
191✔
449
        EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK), this._config.selector, event => {
27✔
450
          const context = this._initializeOnDelegatedTarget(event)
5✔
451
          context._activeTrigger[TRIGGER_CLICK] = !(context._isShown() && context._activeTrigger[TRIGGER_CLICK])
5✔
452
          context.toggle()
5✔
453
        })
454
      } else if (trigger !== TRIGGER_MANUAL) {
164✔
455
        const eventIn = trigger === TRIGGER_HOVER ?
163✔
456
          this.constructor.eventName(EVENT_MOUSEENTER) :
163✔
457
          this.constructor.eventName(EVENT_FOCUSIN)
458
        const eventOut = trigger === TRIGGER_HOVER ?
163✔
459
          this.constructor.eventName(EVENT_MOUSELEAVE) :
163✔
460
          this.constructor.eventName(EVENT_FOCUSOUT)
461

462
        EventHandler.on(this._element, eventIn, this._config.selector, event => {
163✔
463
          const context = this._initializeOnDelegatedTarget(event)
9✔
464
          context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true
9!
465
          context._enter()
9✔
466
        })
467
        EventHandler.on(this._element, eventOut, this._config.selector, event => {
163✔
468
          const context = this._initializeOnDelegatedTarget(event)
4✔
469
          context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] =
4!
470
            context._element.contains(event.relatedTarget)
471

472
          context._leave()
4✔
473
        })
474
      }
475
    }
476

477
    this._hideModalHandler = () => {
109✔
478
      if (this._element) {
×
479
        this.hide()
×
480
      }
481
    }
482

483
    EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler)
109✔
484
  }
485

486
  _fixTitle() {
487
    const title = this._element.getAttribute('title')
108✔
488

489
    if (!title) {
108✔
490
      return
31✔
491
    }
492

493
    if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) {
77✔
494
      this._element.setAttribute('aria-label', title)
58✔
495
    }
496

497
    this._element.setAttribute('data-bs-original-title', title) // DO NOT USE IT. Is only for backwards compatibility
77✔
498
    this._element.removeAttribute('title')
77✔
499
  }
500

501
  _enter() {
502
    if (this._isShown() || this._isHovered) {
15✔
503
      this._isHovered = true
2✔
504
      return
2✔
505
    }
506

507
    this._isHovered = true
13✔
508

509
    this._setTimeout(() => {
13✔
510
      if (this._isHovered) {
13✔
511
        this.show()
13✔
512
      }
513
    }, this._config.delay.show)
514
  }
515

516
  _leave() {
517
    if (this._isWithActiveTrigger()) {
14✔
518
      return
3✔
519
    }
520

521
    this._isHovered = false
11✔
522

523
    this._setTimeout(() => {
11✔
524
      if (!this._isHovered) {
8✔
525
        this.hide()
7✔
526
      }
527
    }, this._config.delay.hide)
528
  }
529

530
  _setTimeout(handler, timeout) {
531
    clearTimeout(this._timeout)
24✔
532
    this._timeout = setTimeout(handler, timeout)
24✔
533
  }
534

535
  _isWithActiveTrigger() {
536
    return Object.values(this._activeTrigger).includes(true)
28✔
537
  }
538

539
  _getConfig(config) {
540
    const dataAttributes = Manipulator.getDataAttributes(this._element)
109✔
541

542
    for (const dataAttribute of Object.keys(dataAttributes)) {
109✔
543
      if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) {
25✔
544
        delete dataAttributes[dataAttribute]
1✔
545
      }
546
    }
547

548
    config = {
109✔
549
      ...dataAttributes,
550
      ...(typeof config === 'object' && config ? config : {})
264✔
551
    }
552
    config = this._mergeConfigObj(config)
109✔
553
    config = this._configAfterMerge(config)
109✔
554
    this._typeCheckConfig(config)
109✔
555
    return config
109✔
556
  }
557

558
  _configAfterMerge(config) {
559
    config.container = config.container === false ? document.body : getElement(config.container)
109✔
560

561
    if (typeof config.delay === 'number') {
109✔
562
      config.delay = {
107✔
563
        show: config.delay,
564
        hide: config.delay
565
      }
566
    }
567

568
    if (typeof config.title === 'number') {
109✔
569
      config.title = config.title.toString()
1✔
570
    }
571

572
    if (typeof config.content === 'number') {
109✔
573
      config.content = config.content.toString()
1✔
574
    }
575

576
    return config
109✔
577
  }
578

579
  _getDelegateConfig() {
580
    const config = {}
18✔
581

582
    for (const [key, value] of Object.entries(this._config)) {
18✔
583
      if (this.constructor.Default[key] !== value) {
308✔
584
        config[key] = value
45✔
585
      }
586
    }
587

588
    config.selector = false
18✔
589
    config.trigger = 'manual'
18✔
590

591
    // In the future can be replaced with:
592
    // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]])
593
    // `Object.fromEntries(keysWithDifferentValues)`
594
    return config
18✔
595
  }
596

597
  _disposePopper() {
598
    if (this._popper) {
74✔
599
      this._popper.destroy()
18✔
600
      this._popper = null
18✔
601
    }
602

603
    if (this.tip) {
74✔
604
      this.tip.remove()
19✔
605
      this.tip = null
19✔
606
    }
607
  }
608

609
  // Static
610
  static jQueryInterface(config) {
611
    return this.each(function () {
4✔
612
      const data = Tooltip.getOrCreateInstance(this, config)
4✔
613

614
      if (typeof config !== 'string') {
4✔
615
        return
2✔
616
      }
617

618
      if (typeof data[config] === 'undefined') {
2✔
619
        throw new TypeError(`No method named "${config}"`)
1✔
620
      }
621

622
      data[config]()
1✔
623
    })
624
  }
625
}
626

627
/**
628
 * jQuery
629
 */
630

631
defineJQueryPlugin(Tooltip)
2✔
632

633
export default Tooltip
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