8
import { DefaultWhitelist, sanitizeHtml } from './tools/sanitizer'
10
import Popper from 'popper.js'
11
import Util from './util'
18
const VERSION = '4.6.1'
19
const DATA_KEY = 'bs.tooltip'
20
const EVENT_KEY = `.${DATA_KEY}`
21
const JQUERY_NO_CONFLICT = $.fn[NAME]
22
const CLASS_PREFIX = 'bs-tooltip'
23
const BSCLS_PREFIX_REGEX = new RegExp(`(^|\\s)${CLASS_PREFIX}\\S+`, 'g')
24
const DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn']
26
const CLASS_NAME_FADE = 'fade'
27
const CLASS_NAME_SHOW = 'show'
29
const HOVER_STATE_SHOW = 'show'
30
const HOVER_STATE_OUT = 'out'
32
const SELECTOR_TOOLTIP_INNER = '.tooltip-inner'
33
const SELECTOR_ARROW = '.arrow'
35
const TRIGGER_HOVER = 'hover'
36
const TRIGGER_FOCUS = 'focus'
37
const TRIGGER_CLICK = 'click'
38
const TRIGGER_MANUAL = 'manual'
40
const AttachmentMap = {
50
template: '<div class="tooltip" role="tooltip">' +
51
'<div class="arrow"></div>' +
52
'<div class="tooltip-inner"></div></div>',
53
trigger: 'hover focus',
61
fallbackPlacement: 'flip',
62
boundary: 'scrollParent',
66
whiteList: DefaultWhitelist,
73
title: '(string|element|function)',
75
delay: '(number|object)',
77
selector: '(string|boolean)',
78
placement: '(string|function)',
79
offset: '(number|string|function)',
80
container: '(string|element|boolean)',
81
fallbackPlacement: '(string|array)',
82
boundary: '(string|element)',
83
customClass: '(string|function)',
85
sanitizeFn: '(null|function)',
87
popperConfig: '(null|object)'
91
HIDE: `hide${EVENT_KEY}`,
92
HIDDEN: `hidden${EVENT_KEY}`,
93
SHOW: `show${EVENT_KEY}`,
94
SHOWN: `shown${EVENT_KEY}`,
95
INSERTED: `inserted${EVENT_KEY}`,
96
CLICK: `click${EVENT_KEY}`,
97
FOCUSIN: `focusin${EVENT_KEY}`,
98
FOCUSOUT: `focusout${EVENT_KEY}`,
99
MOUSEENTER: `mouseenter${EVENT_KEY}`,
100
MOUSELEAVE: `mouseleave${EVENT_KEY}`
108
constructor(element, config) {
109
if (typeof Popper === 'undefined') {
110
throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org)')
114
this._isEnabled = true
116
this._hoverState = ''
117
this._activeTrigger = {}
121
this.element = element
122
this.config = this._getConfig(config)
129
static get VERSION() {
133
static get Default() {
141
static get DATA_KEY() {
149
static get EVENT_KEY() {
153
static get DefaultType() {
159
this._isEnabled = true
163
this._isEnabled = false
167
this._isEnabled = !this._isEnabled
171
if (!this._isEnabled) {
176
const dataKey = this.constructor.DATA_KEY
177
let context = $(event.currentTarget).data(dataKey)
180
context = new this.constructor(
182
this._getDelegateConfig()
184
$(event.currentTarget).data(dataKey, context)
187
context._activeTrigger.click = !context._activeTrigger.click
189
if (context._isWithActiveTrigger()) {
190
context._enter(null, context)
192
context._leave(null, context)
195
if ($(this.getTipElement()).hasClass(CLASS_NAME_SHOW)) {
196
this._leave(null, this)
200
this._enter(null, this)
205
clearTimeout(this._timeout)
207
$.removeData(this.element, this.constructor.DATA_KEY)
209
$(this.element).off(this.constructor.EVENT_KEY)
210
$(this.element).closest('.modal').off('hide.bs.modal', this._hideModalHandler)
216
this._isEnabled = null
218
this._hoverState = null
219
this._activeTrigger = null
221
this._popper.destroy()
231
if ($(this.element).css('display') === 'none') {
232
throw new Error('Please use show on visible elements')
235
const showEvent = $.Event(this.constructor.Event.SHOW)
236
if (this.isWithContent() && this._isEnabled) {
237
$(this.element).trigger(showEvent)
239
const shadowRoot = Util.findShadowRoot(this.element)
240
const isInTheDom = $.contains(
241
shadowRoot !== null ? shadowRoot : this.element.ownerDocument.documentElement,
245
if (showEvent.isDefaultPrevented() || !isInTheDom) {
249
const tip = this.getTipElement()
250
const tipId = Util.getUID(this.constructor.NAME)
252
tip.setAttribute('id', tipId)
253
this.element.setAttribute('aria-describedby', tipId)
257
if (this.config.animation) {
258
$(tip).addClass(CLASS_NAME_FADE)
261
const placement = typeof this.config.placement === 'function' ?
262
this.config.placement.call(this, tip, this.element) :
263
this.config.placement
265
const attachment = this._getAttachment(placement)
266
this.addAttachmentClass(attachment)
268
const container = this._getContainer()
269
$(tip).data(this.constructor.DATA_KEY, this)
271
if (!$.contains(this.element.ownerDocument.documentElement, this.tip)) {
272
$(tip).appendTo(container)
275
$(this.element).trigger(this.constructor.Event.INSERTED)
277
this._popper = new Popper(this.element, tip, this._getPopperConfig(attachment))
279
$(tip).addClass(CLASS_NAME_SHOW)
280
$(tip).addClass(this.config.customClass)
286
if ('ontouchstart' in document.documentElement) {
287
$(document.body).children().on('mouseover', null, $.noop)
290
const complete = () => {
291
if (this.config.animation) {
292
this._fixTransition()
295
const prevHoverState = this._hoverState
296
this._hoverState = null
298
$(this.element).trigger(this.constructor.Event.SHOWN)
300
if (prevHoverState === HOVER_STATE_OUT) {
301
this._leave(null, this)
305
if ($(this.tip).hasClass(CLASS_NAME_FADE)) {
306
const transitionDuration = Util.getTransitionDurationFromElement(this.tip)
309
.one(Util.TRANSITION_END, complete)
310
.emulateTransitionEnd(transitionDuration)
318
const tip = this.getTipElement()
319
const hideEvent = $.Event(this.constructor.Event.HIDE)
320
const complete = () => {
321
if (this._hoverState !== HOVER_STATE_SHOW && tip.parentNode) {
322
tip.parentNode.removeChild(tip)
325
this._cleanTipClass()
326
this.element.removeAttribute('aria-describedby')
327
$(this.element).trigger(this.constructor.Event.HIDDEN)
328
if (this._popper !== null) {
329
this._popper.destroy()
337
$(this.element).trigger(hideEvent)
339
if (hideEvent.isDefaultPrevented()) {
343
$(tip).removeClass(CLASS_NAME_SHOW)
347
if ('ontouchstart' in document.documentElement) {
348
$(document.body).children().off('mouseover', null, $.noop)
351
this._activeTrigger[TRIGGER_CLICK] = false
352
this._activeTrigger[TRIGGER_FOCUS] = false
353
this._activeTrigger[TRIGGER_HOVER] = false
355
if ($(this.tip).hasClass(CLASS_NAME_FADE)) {
356
const transitionDuration = Util.getTransitionDurationFromElement(tip)
359
.one(Util.TRANSITION_END, complete)
360
.emulateTransitionEnd(transitionDuration)
365
this._hoverState = ''
369
if (this._popper !== null) {
370
this._popper.scheduleUpdate()
376
return Boolean(this.getTitle())
379
addAttachmentClass(attachment) {
380
$(this.getTipElement()).addClass(`${CLASS_PREFIX}-${attachment}`)
384
this.tip = this.tip || $(this.config.template)[0]
389
const tip = this.getTipElement()
390
this.setElementContent($(tip.querySelectorAll(SELECTOR_TOOLTIP_INNER)), this.getTitle())
391
$(tip).removeClass(`${CLASS_NAME_FADE} ${CLASS_NAME_SHOW}`)
394
setElementContent($element, content) {
395
if (typeof content === 'object' && (content.nodeType || content.jquery)) {
397
if (this.config.html) {
398
if (!$(content).parent().is($element)) {
399
$element.empty().append(content)
402
$element.text($(content).text())
408
if (this.config.html) {
409
if (this.config.sanitize) {
410
content = sanitizeHtml(content, this.config.whiteList, this.config.sanitizeFn)
413
$element.html(content)
415
$element.text(content)
420
let title = this.element.getAttribute('data-original-title')
423
title = typeof this.config.title === 'function' ?
424
this.config.title.call(this.element) :
432
_getPopperConfig(attachment) {
433
const defaultBsConfig = {
434
placement: attachment,
436
offset: this._getOffset(),
438
behavior: this.config.fallbackPlacement
441
element: SELECTOR_ARROW
444
boundariesElement: this.config.boundary
448
if (data.originalPlacement !== data.placement) {
449
this._handlePopperPlacementChange(data)
452
onUpdate: data => this._handlePopperPlacementChange(data)
457
...this.config.popperConfig
464
if (typeof this.config.offset === 'function') {
465
offset.fn = data => {
468
...this.config.offset(data.offsets, this.element)
474
offset.offset = this.config.offset
481
if (this.config.container === false) {
485
if (Util.isElement(this.config.container)) {
486
return $(this.config.container)
489
return $(document).find(this.config.container)
492
_getAttachment(placement) {
493
return AttachmentMap[placement.toUpperCase()]
497
const triggers = this.config.trigger.split(' ')
499
triggers.forEach(trigger => {
500
if (trigger === 'click') {
502
this.constructor.Event.CLICK,
503
this.config.selector,
504
event => this.toggle(event)
506
} else if (trigger !== TRIGGER_MANUAL) {
507
const eventIn = trigger === TRIGGER_HOVER ?
508
this.constructor.Event.MOUSEENTER :
509
this.constructor.Event.FOCUSIN
510
const eventOut = trigger === TRIGGER_HOVER ?
511
this.constructor.Event.MOUSELEAVE :
512
this.constructor.Event.FOCUSOUT
515
.on(eventIn, this.config.selector, event => this._enter(event))
516
.on(eventOut, this.config.selector, event => this._leave(event))
520
this._hideModalHandler = () => {
526
$(this.element).closest('.modal').on('hide.bs.modal', this._hideModalHandler)
528
if (this.config.selector) {
540
const titleType = typeof this.element.getAttribute('data-original-title')
542
if (this.element.getAttribute('title') || titleType !== 'string') {
543
this.element.setAttribute(
544
'data-original-title',
545
this.element.getAttribute('title') || ''
548
this.element.setAttribute('title', '')
552
_enter(event, context) {
553
const dataKey = this.constructor.DATA_KEY
554
context = context || $(event.currentTarget).data(dataKey)
557
context = new this.constructor(
559
this._getDelegateConfig()
561
$(event.currentTarget).data(dataKey, context)
565
context._activeTrigger[
566
event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER
570
if ($(context.getTipElement()).hasClass(CLASS_NAME_SHOW) || context._hoverState === HOVER_STATE_SHOW) {
571
context._hoverState = HOVER_STATE_SHOW
575
clearTimeout(context._timeout)
577
context._hoverState = HOVER_STATE_SHOW
579
if (!context.config.delay || !context.config.delay.show) {
584
context._timeout = setTimeout(() => {
585
if (context._hoverState === HOVER_STATE_SHOW) {
588
}, context.config.delay.show)
591
_leave(event, context) {
592
const dataKey = this.constructor.DATA_KEY
593
context = context || $(event.currentTarget).data(dataKey)
596
context = new this.constructor(
598
this._getDelegateConfig()
600
$(event.currentTarget).data(dataKey, context)
604
context._activeTrigger[
605
event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER
609
if (context._isWithActiveTrigger()) {
613
clearTimeout(context._timeout)
615
context._hoverState = HOVER_STATE_OUT
617
if (!context.config.delay || !context.config.delay.hide) {
622
context._timeout = setTimeout(() => {
623
if (context._hoverState === HOVER_STATE_OUT) {
626
}, context.config.delay.hide)
629
_isWithActiveTrigger() {
630
for (const trigger in this._activeTrigger) {
631
if (this._activeTrigger[trigger]) {
640
const dataAttributes = $(this.element).data()
642
Object.keys(dataAttributes)
643
.forEach(dataAttr => {
644
if (DISALLOWED_ATTRIBUTES.indexOf(dataAttr) !== -1) {
645
delete dataAttributes[dataAttr]
650
...this.constructor.Default,
652
...(typeof config === 'object' && config ? config : {})
655
if (typeof config.delay === 'number') {
662
if (typeof config.title === 'number') {
663
config.title = config.title.toString()
666
if (typeof config.content === 'number') {
667
config.content = config.content.toString()
670
Util.typeCheckConfig(
673
this.constructor.DefaultType
676
if (config.sanitize) {
677
config.template = sanitizeHtml(config.template, config.whiteList, config.sanitizeFn)
683
_getDelegateConfig() {
687
for (const key in this.config) {
688
if (this.constructor.Default[key] !== this.config[key]) {
689
config[key] = this.config[key]
698
const $tip = $(this.getTipElement())
699
const tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX)
700
if (tabClass !== null && tabClass.length) {
701
$tip.removeClass(tabClass.join(''))
705
_handlePopperPlacementChange(popperData) {
706
this.tip = popperData.instance.popper
707
this._cleanTipClass()
708
this.addAttachmentClass(this._getAttachment(popperData.placement))
712
const tip = this.getTipElement()
713
const initConfigAnimation = this.config.animation
715
if (tip.getAttribute('x-placement') !== null) {
719
$(tip).removeClass(CLASS_NAME_FADE)
720
this.config.animation = false
723
this.config.animation = initConfigAnimation
727
static _jQueryInterface(config) {
728
return this.each(function () {
729
const $element = $(this)
730
let data = $element.data(DATA_KEY)
731
const _config = typeof config === 'object' && config
733
if (!data && /dispose|hide/.test(config)) {
738
data = new Tooltip(this, _config)
739
$element.data(DATA_KEY, data)
742
if (typeof config === 'string') {
743
if (typeof data[config] === 'undefined') {
744
throw new TypeError(`No method named "${config}"`)
757
$.fn[NAME] = Tooltip._jQueryInterface
758
$.fn[NAME].Constructor = Tooltip
759
$.fn[NAME].noConflict = () => {
760
$.fn[NAME] = JQUERY_NO_CONFLICT
761
return Tooltip._jQueryInterface
764
export default Tooltip