tooltip.js 19.5 KB
Newer Older
1
2
/**
 * --------------------------------------------------------------------------
3
 * Bootstrap (v5.0.0-alpha1): tooltip.js
4
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
5
6
7
 * --------------------------------------------------------------------------
 */

8
import {
9
  getjQuery,
10
11
12
13
14
15
16
17
  TRANSITION_END,
  emulateTransitionEnd,
  findShadowRoot,
  getTransitionDurationFromElement,
  getUID,
  isElement,
  noop,
  typeCheckConfig
18
} from './util/index'
19
import {
20
  DefaultAllowlist,
21
  sanitizeHtml
22
23
24
25
} from './util/sanitizer'
import Data from './dom/data'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
26
import Popper from 'popper.js'
27
import SelectorEngine from './dom/selector-engine'
28

Johann-S's avatar
Johann-S committed
29
30
31
32
33
/**
 * ------------------------------------------------------------------------
 * Constants
 * ------------------------------------------------------------------------
 */
34

XhmikosR's avatar
XhmikosR committed
35
const NAME = 'tooltip'
36
const VERSION = '5.0.0-alpha1'
XhmikosR's avatar
XhmikosR committed
37
38
39
40
const DATA_KEY = 'bs.tooltip'
const EVENT_KEY = `.${DATA_KEY}`
const CLASS_PREFIX = 'bs-tooltip'
const BSCLS_PREFIX_REGEX = new RegExp(`(^|\\s)${CLASS_PREFIX}\\S+`, 'g')
41
const DISALLOWED_ATTRIBUTES = ['sanitize', 'allowList', 'sanitizeFn']
Johann-S's avatar
Johann-S committed
42
43

const DefaultType = {
XhmikosR's avatar
XhmikosR committed
44
45
46
47
48
49
50
51
52
53
54
55
56
57
  animation: 'boolean',
  template: 'string',
  title: '(string|element|function)',
  trigger: 'string',
  delay: '(number|object)',
  html: 'boolean',
  selector: '(string|boolean)',
  placement: '(string|function)',
  offset: '(number|string|function)',
  container: '(string|element|boolean)',
  fallbackPlacement: '(string|array)',
  boundary: '(string|element)',
  sanitize: 'boolean',
  sanitizeFn: '(null|function)',
58
  allowList: 'object',
59
  popperConfig: '(null|object)'
Johann-S's avatar
Johann-S committed
60
61
62
}

const AttachmentMap = {
XhmikosR's avatar
XhmikosR committed
63
64
65
66
67
  AUTO: 'auto',
  TOP: 'top',
  RIGHT: 'right',
  BOTTOM: 'bottom',
  LEFT: 'left'
Johann-S's avatar
Johann-S committed
68
69
70
}

const Default = {
XhmikosR's avatar
XhmikosR committed
71
72
  animation: true,
  template: '<div class="tooltip" role="tooltip">' +
73
                    '<div class="tooltip-arrow"></div>' +
74
                    '<div class="tooltip-inner"></div></div>',
XhmikosR's avatar
XhmikosR committed
75
76
77
78
79
80
81
82
83
84
85
86
  trigger: 'hover focus',
  title: '',
  delay: 0,
  html: false,
  selector: false,
  placement: 'top',
  offset: 0,
  container: false,
  fallbackPlacement: 'flip',
  boundary: 'scrollParent',
  sanitize: true,
  sanitizeFn: null,
87
  allowList: DefaultAllowlist,
88
  popperConfig: null
Johann-S's avatar
Johann-S committed
89
90
91
}

const Event = {
XhmikosR's avatar
XhmikosR committed
92
93
94
95
96
97
98
99
100
101
  HIDE: `hide${EVENT_KEY}`,
  HIDDEN: `hidden${EVENT_KEY}`,
  SHOW: `show${EVENT_KEY}`,
  SHOWN: `shown${EVENT_KEY}`,
  INSERTED: `inserted${EVENT_KEY}`,
  CLICK: `click${EVENT_KEY}`,
  FOCUSIN: `focusin${EVENT_KEY}`,
  FOCUSOUT: `focusout${EVENT_KEY}`,
  MOUSEENTER: `mouseenter${EVENT_KEY}`,
  MOUSELEAVE: `mouseleave${EVENT_KEY}`
Johann-S's avatar
Johann-S committed
102
103
}

XhmikosR's avatar
XhmikosR committed
104
105
106
const CLASS_NAME_FADE = 'fade'
const CLASS_NAME_MODAL = 'modal'
const CLASS_NAME_SHOW = 'show'
Johann-S's avatar
Johann-S committed
107

XhmikosR's avatar
XhmikosR committed
108
109
const HOVER_STATE_SHOW = 'show'
const HOVER_STATE_OUT = 'out'
Johann-S's avatar
Johann-S committed
110

XhmikosR's avatar
XhmikosR committed
111
112
113
114
115
116
const SELECTOR_TOOLTIP_INNER = '.tooltip-inner'

const TRIGGER_HOVER = 'hover'
const TRIGGER_FOCUS = 'focus'
const TRIGGER_CLICK = 'click'
const TRIGGER_MANUAL = 'manual'
117

Johann-S's avatar
Johann-S committed
118
119
120
121
122
/**
 * ------------------------------------------------------------------------
 * Class Definition
 * ------------------------------------------------------------------------
 */
123

Johann-S's avatar
Johann-S committed
124
125
126
class Tooltip {
  constructor(element, config) {
    if (typeof Popper === 'undefined') {
127
      throw new TypeError('Bootstrap\'s tooltips require Popper.js (https://popper.js.org)')
128
129
    }

Johann-S's avatar
Johann-S committed
130
    // private
XhmikosR's avatar
XhmikosR committed
131
132
133
    this._isEnabled = true
    this._timeout = 0
    this._hoverState = ''
Johann-S's avatar
Johann-S committed
134
    this._activeTrigger = {}
XhmikosR's avatar
XhmikosR committed
135
    this._popper = null
136

Johann-S's avatar
Johann-S committed
137
138
    // Protected
    this.element = element
XhmikosR's avatar
XhmikosR committed
139
140
    this.config = this._getConfig(config)
    this.tip = null
fat's avatar
fat committed
141

Johann-S's avatar
Johann-S committed
142
    this._setListeners()
143
    Data.setData(element, this.constructor.DATA_KEY, this)
Johann-S's avatar
Johann-S committed
144
  }
fat's avatar
fat committed
145

Johann-S's avatar
Johann-S committed
146
  // Getters
fat's avatar
fat committed
147

Johann-S's avatar
Johann-S committed
148
149
150
  static get VERSION() {
    return VERSION
  }
fat's avatar
fat committed
151

Johann-S's avatar
Johann-S committed
152
153
154
  static get Default() {
    return Default
  }
fat's avatar
fat committed
155

Johann-S's avatar
Johann-S committed
156
157
158
  static get NAME() {
    return NAME
  }
159

Johann-S's avatar
Johann-S committed
160
161
162
  static get DATA_KEY() {
    return DATA_KEY
  }
163

Johann-S's avatar
Johann-S committed
164
165
166
  static get Event() {
    return Event
  }
167

Johann-S's avatar
Johann-S committed
168
169
170
  static get EVENT_KEY() {
    return EVENT_KEY
  }
171

Johann-S's avatar
Johann-S committed
172
173
174
  static get DefaultType() {
    return DefaultType
  }
175

Johann-S's avatar
Johann-S committed
176
  // Public
177

Johann-S's avatar
Johann-S committed
178
179
180
  enable() {
    this._isEnabled = true
  }
181

Johann-S's avatar
Johann-S committed
182
183
184
  disable() {
    this._isEnabled = false
  }
185

Johann-S's avatar
Johann-S committed
186
187
188
  toggleEnabled() {
    this._isEnabled = !this._isEnabled
  }
Jacob Thornton's avatar
Jacob Thornton committed
189

Johann-S's avatar
Johann-S committed
190
191
192
  toggle(event) {
    if (!this._isEnabled) {
      return
193
194
    }

Johann-S's avatar
Johann-S committed
195
196
    if (event) {
      const dataKey = this.constructor.DATA_KEY
197
      let context = Data.getData(event.delegateTarget, dataKey)
fat's avatar
fat committed
198

Johann-S's avatar
Johann-S committed
199
200
      if (!context) {
        context = new this.constructor(
201
          event.delegateTarget,
Johann-S's avatar
Johann-S committed
202
203
          this._getDelegateConfig()
        )
204
        Data.setData(event.delegateTarget, dataKey, context)
Johann-S's avatar
Johann-S committed
205
      }
fat's avatar
fat committed
206

Johann-S's avatar
Johann-S committed
207
      context._activeTrigger.click = !context._activeTrigger.click
fat's avatar
fat committed
208

Johann-S's avatar
Johann-S committed
209
210
211
212
      if (context._isWithActiveTrigger()) {
        context._enter(null, context)
      } else {
        context._leave(null, context)
fat's avatar
fat committed
213
      }
Johann-S's avatar
Johann-S committed
214
    } else {
XhmikosR's avatar
XhmikosR committed
215
      if (this.getTipElement().classList.contains(CLASS_NAME_SHOW)) {
Johann-S's avatar
Johann-S committed
216
217
        this._leave(null, this)
        return
218
      }
fat's avatar
fat committed
219

Johann-S's avatar
Johann-S committed
220
      this._enter(null, this)
221
    }
Johann-S's avatar
Johann-S committed
222
  }
223

Johann-S's avatar
Johann-S committed
224
225
  dispose() {
    clearTimeout(this._timeout)
226

227
    Data.removeData(this.element, this.constructor.DATA_KEY)
228

229
    EventHandler.off(this.element, this.constructor.EVENT_KEY)
230
    EventHandler.off(this.element.closest(`.${CLASS_NAME_MODAL}`), 'hide.bs.modal', this._hideModalHandler)
231

Johann-S's avatar
Johann-S committed
232
    if (this.tip) {
233
      this.tip.parentNode.removeChild(this.tip)
Johann-S's avatar
Johann-S committed
234
235
    }

XhmikosR's avatar
XhmikosR committed
236
237
238
    this._isEnabled = null
    this._timeout = null
    this._hoverState = null
Johann-S's avatar
Johann-S committed
239
    this._activeTrigger = null
240
    if (this._popper) {
Johann-S's avatar
Johann-S committed
241
242
      this._popper.destroy()
    }
243

Johann-S's avatar
Johann-S committed
244
245
    this._popper = null
    this.element = null
XhmikosR's avatar
XhmikosR committed
246
247
    this.config = null
    this.tip = null
Johann-S's avatar
Johann-S committed
248
  }
249

Johann-S's avatar
Johann-S committed
250
  show() {
251
    if (this.element.style.display === 'none') {
Johann-S's avatar
Johann-S committed
252
253
      throw new Error('Please use show on visible elements')
    }
254

Johann-S's avatar
Johann-S committed
255
    if (this.isWithContent() && this._isEnabled) {
256
      const showEvent = EventHandler.trigger(this.element, this.constructor.Event.SHOW)
257
      const shadowRoot = findShadowRoot(this.element)
XhmikosR's avatar
XhmikosR committed
258
259
260
      const isInTheDom = shadowRoot === null ?
        this.element.ownerDocument.documentElement.contains(this.element) :
        shadowRoot.contains(this.element)
261

262
      if (showEvent.defaultPrevented || !isInTheDom) {
Johann-S's avatar
Johann-S committed
263
264
        return
      }
265

XhmikosR's avatar
XhmikosR committed
266
      const tip = this.getTipElement()
267
      const tipId = getUID(this.constructor.NAME)
268

Johann-S's avatar
Johann-S committed
269
270
      tip.setAttribute('id', tipId)
      this.element.setAttribute('aria-describedby', tipId)
271

Johann-S's avatar
Johann-S committed
272
      this.setContent()
273

Johann-S's avatar
Johann-S committed
274
      if (this.config.animation) {
XhmikosR's avatar
XhmikosR committed
275
        tip.classList.add(CLASS_NAME_FADE)
Johann-S's avatar
Johann-S committed
276
      }
277

XhmikosR's avatar
XhmikosR committed
278
279
280
      const placement = typeof this.config.placement === 'function' ?
        this.config.placement.call(this, tip, this.element) :
        this.config.placement
281

Johann-S's avatar
Johann-S committed
282
      const attachment = this._getAttachment(placement)
Johann-S's avatar
Johann-S committed
283
      this._addAttachmentClass(attachment)
284

285
      const container = this._getContainer()
286
      Data.setData(tip, this.constructor.DATA_KEY, this)
Johann-S's avatar
Johann-S committed
287

288
289
      if (!this.element.ownerDocument.documentElement.contains(this.tip)) {
        container.appendChild(tip)
Johann-S's avatar
Johann-S committed
290
      }
291

292
      EventHandler.trigger(this.element, this.constructor.Event.INSERTED)
293

294
      this._popper = new Popper(this.element, tip, this._getPopperConfig(attachment))
295

XhmikosR's avatar
XhmikosR committed
296
      tip.classList.add(CLASS_NAME_SHOW)
297

Johann-S's avatar
Johann-S committed
298
299
300
301
302
      // If this is a touch-enabled device we add extra
      // empty mouseover listeners to the body's immediate children;
      // only needed because of broken event delegation on iOS
      // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
      if ('ontouchstart' in document.documentElement) {
303
        [].concat(...document.body.children).forEach(element => {
304
          EventHandler.on(element, 'mouseover', noop())
305
        })
306
307
      }

XhmikosR's avatar
XhmikosR committed
308
      const complete = () => {
Johann-S's avatar
Johann-S committed
309
310
        if (this.config.animation) {
          this._fixTransition()
311
        }
XhmikosR's avatar
XhmikosR committed
312

Johann-S's avatar
Johann-S committed
313
        const prevHoverState = this._hoverState
XhmikosR's avatar
XhmikosR committed
314
        this._hoverState = null
315

316
        EventHandler.trigger(this.element, this.constructor.Event.SHOWN)
317

XhmikosR's avatar
XhmikosR committed
318
        if (prevHoverState === HOVER_STATE_OUT) {
Johann-S's avatar
Johann-S committed
319
          this._leave(null, this)
320
321
322
        }
      }

XhmikosR's avatar
XhmikosR committed
323
      if (this.tip.classList.contains(CLASS_NAME_FADE)) {
324
325
326
        const transitionDuration = getTransitionDurationFromElement(this.tip)
        EventHandler.one(this.tip, TRANSITION_END, complete)
        emulateTransitionEnd(this.tip, transitionDuration)
327
328
329
330
      } else {
        complete()
      }
    }
Johann-S's avatar
Johann-S committed
331
  }
332

Johann-S's avatar
Johann-S committed
333
  hide() {
334
335
336
337
    if (!this._popper) {
      return
    }

XhmikosR's avatar
XhmikosR committed
338
339
    const tip = this.getTipElement()
    const complete = () => {
XhmikosR's avatar
XhmikosR committed
340
      if (this._hoverState !== HOVER_STATE_SHOW && tip.parentNode) {
Johann-S's avatar
Johann-S committed
341
        tip.parentNode.removeChild(tip)
342
      }
343

Johann-S's avatar
Johann-S committed
344
345
      this._cleanTipClass()
      this.element.removeAttribute('aria-describedby')
346
      EventHandler.trigger(this.element, this.constructor.Event.HIDDEN)
Johann-S's avatar
Johann-S committed
347
      this._popper.destroy()
348
349
    }

350
351
    const hideEvent = EventHandler.trigger(this.element, this.constructor.Event.HIDE)
    if (hideEvent.defaultPrevented) {
Johann-S's avatar
Johann-S committed
352
      return
353
354
    }

XhmikosR's avatar
XhmikosR committed
355
    tip.classList.remove(CLASS_NAME_SHOW)
356

Johann-S's avatar
Johann-S committed
357
358
359
    // If this is a touch-enabled device we remove the extra
    // empty mouseover listeners we added for iOS support
    if ('ontouchstart' in document.documentElement) {
360
      [].concat(...document.body.children)
XhmikosR's avatar
XhmikosR committed
361
        .forEach(element => EventHandler.off(element, 'mouseover', noop))
362
363
    }

XhmikosR's avatar
XhmikosR committed
364
365
366
    this._activeTrigger[TRIGGER_CLICK] = false
    this._activeTrigger[TRIGGER_FOCUS] = false
    this._activeTrigger[TRIGGER_HOVER] = false
367

XhmikosR's avatar
XhmikosR committed
368
    if (this.tip.classList.contains(CLASS_NAME_FADE)) {
369
370
371
372
      const transitionDuration = getTransitionDurationFromElement(tip)

      EventHandler.one(tip, TRANSITION_END, complete)
      emulateTransitionEnd(tip, transitionDuration)
Johann-S's avatar
Johann-S committed
373
374
    } else {
      complete()
375
376
    }

Johann-S's avatar
Johann-S committed
377
378
    this._hoverState = ''
  }
379

Johann-S's avatar
Johann-S committed
380
381
382
  update() {
    if (this._popper !== null) {
      this._popper.scheduleUpdate()
fat's avatar
fat committed
383
    }
Johann-S's avatar
Johann-S committed
384
  }
fat's avatar
fat committed
385

Johann-S's avatar
Johann-S committed
386
  // Protected
387

Johann-S's avatar
Johann-S committed
388
389
390
  isWithContent() {
    return Boolean(this.getTitle())
  }
391

Johann-S's avatar
Johann-S committed
392
  getTipElement() {
393
394
395
396
397
398
399
400
    if (this.tip) {
      return this.tip
    }

    const element = document.createElement('div')
    element.innerHTML = this.config.template

    this.tip = element.children[0]
Johann-S's avatar
Johann-S committed
401
402
403
404
405
    return this.tip
  }

  setContent() {
    const tip = this.getTipElement()
XhmikosR's avatar
XhmikosR committed
406
    this.setElementContent(SelectorEngine.findOne(SELECTOR_TOOLTIP_INNER, tip), this.getTitle())
407
    tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW)
Johann-S's avatar
Johann-S committed
408
409
  }

410
411
412
413
414
  setElementContent(element, content) {
    if (element === null) {
      return
    }

Johann-S's avatar
Johann-S committed
415
    if (typeof content === 'object' && isElement(content)) {
416
417
418
419
420
      if (content.jquery) {
        content = content[0]
      }

      // content is a DOM node or a jQuery
421
      if (this.config.html) {
422
423
424
        if (content.parentNode !== element) {
          element.innerHTML = ''
          element.appendChild(content)
425
        }
426
      } else {
427
        element.textContent = content.textContent
428
      }
429
430
431
432
433
434

      return
    }

    if (this.config.html) {
      if (this.config.sanitize) {
435
        content = sanitizeHtml(content, this.config.allowList, this.config.sanitizeFn)
436
437
      }

438
      element.innerHTML = content
Johann-S's avatar
Johann-S committed
439
    } else {
440
      element.textContent = content
441
    }
Johann-S's avatar
Johann-S committed
442
  }
443

Johann-S's avatar
Johann-S committed
444
445
446
447
  getTitle() {
    let title = this.element.getAttribute('data-original-title')

    if (!title) {
XhmikosR's avatar
XhmikosR committed
448
449
450
      title = typeof this.config.title === 'function' ?
        this.config.title.call(this.element) :
        this.config.title
451
452
    }

Johann-S's avatar
Johann-S committed
453
454
    return title
  }
fat's avatar
fat committed
455

Johann-S's avatar
Johann-S committed
456
  // Private
457

458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
  _getPopperConfig(attachment) {
    const defaultBsConfig = {
      placement: attachment,
      modifiers: {
        offset: this._getOffset(),
        flip: {
          behavior: this.config.fallbackPlacement
        },
        arrow: {
          element: `.${this.constructor.NAME}-arrow`
        },
        preventOverflow: {
          boundariesElement: this.config.boundary
        }
      },
      onCreate: data => {
        if (data.originalPlacement !== data.placement) {
          this._handlePopperPlacementChange(data)
        }
      },
      onUpdate: data => this._handlePopperPlacementChange(data)
    }

481
482
483
    return {
      ...defaultBsConfig,
      ...this.config.popperConfig
484
485
486
    }
  }

Johann-S's avatar
Johann-S committed
487
488
489
490
  _addAttachmentClass(attachment) {
    this.getTipElement().classList.add(`${CLASS_PREFIX}-${attachment}`)
  }

491
492
493
494
  _getOffset() {
    const offset = {}

    if (typeof this.config.offset === 'function') {
XhmikosR's avatar
XhmikosR committed
495
      offset.fn = data => {
496
497
        data.offsets = {
          ...data.offsets,
498
          ...(this.config.offset(data.offsets, this.element) || {})
499
500
501
502
503
504
505
506
507
508
509
        }

        return data
      }
    } else {
      offset.offset = this.config.offset
    }

    return offset
  }

510
511
512
513
514
  _getContainer() {
    if (this.config.container === false) {
      return document.body
    }

515
    if (isElement(this.config.container)) {
516
      return this.config.container
517
518
    }

519
    return SelectorEngine.findOne(this.config.container)
520
521
  }

Johann-S's avatar
Johann-S committed
522
523
524
525
526
527
528
  _getAttachment(placement) {
    return AttachmentMap[placement.toUpperCase()]
  }

  _setListeners() {
    const triggers = this.config.trigger.split(' ')

XhmikosR's avatar
XhmikosR committed
529
    triggers.forEach(trigger => {
Johann-S's avatar
Johann-S committed
530
      if (trigger === 'click') {
531
        EventHandler.on(this.element,
Johann-S's avatar
Johann-S committed
532
533
          this.constructor.Event.CLICK,
          this.config.selector,
XhmikosR's avatar
XhmikosR committed
534
          event => this.toggle(event)
535
        )
XhmikosR's avatar
XhmikosR committed
536
537
      } else if (trigger !== TRIGGER_MANUAL) {
        const eventIn = trigger === TRIGGER_HOVER ?
XhmikosR's avatar
XhmikosR committed
538
539
          this.constructor.Event.MOUSEENTER :
          this.constructor.Event.FOCUSIN
XhmikosR's avatar
XhmikosR committed
540
        const eventOut = trigger === TRIGGER_HOVER ?
XhmikosR's avatar
XhmikosR committed
541
542
          this.constructor.Event.MOUSELEAVE :
          this.constructor.Event.FOCUSOUT
Johann-S's avatar
Johann-S committed
543

544
545
546
        EventHandler.on(this.element,
          eventIn,
          this.config.selector,
XhmikosR's avatar
XhmikosR committed
547
          event => this._enter(event)
548
549
550
551
        )
        EventHandler.on(this.element,
          eventOut,
          this.config.selector,
XhmikosR's avatar
XhmikosR committed
552
          event => this._leave(event)
553
        )
554
      }
Johann-S's avatar
Johann-S committed
555
    })
556

557
558
559
560
561
562
    this._hideModalHandler = () => {
      if (this.element) {
        this.hide()
      }
    }

563
    EventHandler.on(this.element.closest(`.${CLASS_NAME_MODAL}`),
564
      'hide.bs.modal',
565
      this._hideModalHandler
566
567
    )

Johann-S's avatar
Johann-S committed
568
569
570
571
572
    if (this.config.selector) {
      this.config = {
        ...this.config,
        trigger: 'manual',
        selector: ''
573
      }
Johann-S's avatar
Johann-S committed
574
575
576
577
    } else {
      this._fixTitle()
    }
  }
578

Johann-S's avatar
Johann-S committed
579
580
  _fixTitle() {
    const titleType = typeof this.element.getAttribute('data-original-title')
581
582

    if (this.element.getAttribute('title') || titleType !== 'string') {
Johann-S's avatar
Johann-S committed
583
584
585
586
      this.element.setAttribute(
        'data-original-title',
        this.element.getAttribute('title') || ''
      )
587

Johann-S's avatar
Johann-S committed
588
589
590
      this.element.setAttribute('title', '')
    }
  }
591

Johann-S's avatar
Johann-S committed
592
593
  _enter(event, context) {
    const dataKey = this.constructor.DATA_KEY
594
    context = context || Data.getData(event.delegateTarget, dataKey)
595

Johann-S's avatar
Johann-S committed
596
597
    if (!context) {
      context = new this.constructor(
598
        event.delegateTarget,
Johann-S's avatar
Johann-S committed
599
600
        this._getDelegateConfig()
      )
601
      Data.setData(event.delegateTarget, dataKey, context)
602
603
    }

Johann-S's avatar
Johann-S committed
604
605
    if (event) {
      context._activeTrigger[
XhmikosR's avatar
XhmikosR committed
606
        event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER
Johann-S's avatar
Johann-S committed
607
608
      ] = true
    }
fat's avatar
fat committed
609

XhmikosR's avatar
XhmikosR committed
610
611
612
    if (context.getTipElement().classList.contains(CLASS_NAME_SHOW) ||
        context._hoverState === HOVER_STATE_SHOW) {
      context._hoverState = HOVER_STATE_SHOW
Johann-S's avatar
Johann-S committed
613
614
      return
    }
615

Johann-S's avatar
Johann-S committed
616
    clearTimeout(context._timeout)
617

XhmikosR's avatar
XhmikosR committed
618
    context._hoverState = HOVER_STATE_SHOW
619

Johann-S's avatar
Johann-S committed
620
621
622
623
    if (!context.config.delay || !context.config.delay.show) {
      context.show()
      return
    }
624

Johann-S's avatar
Johann-S committed
625
    context._timeout = setTimeout(() => {
XhmikosR's avatar
XhmikosR committed
626
      if (context._hoverState === HOVER_STATE_SHOW) {
Johann-S's avatar
Johann-S committed
627
628
629
630
        context.show()
      }
    }, context.config.delay.show)
  }
631

Johann-S's avatar
Johann-S committed
632
633
  _leave(event, context) {
    const dataKey = this.constructor.DATA_KEY
634
    context = context || Data.getData(event.delegateTarget, dataKey)
635

Johann-S's avatar
Johann-S committed
636
637
    if (!context) {
      context = new this.constructor(
638
        event.delegateTarget,
Johann-S's avatar
Johann-S committed
639
640
        this._getDelegateConfig()
      )
641
      Data.setData(event.delegateTarget, dataKey, context)
642
643
    }

Johann-S's avatar
Johann-S committed
644
645
    if (event) {
      context._activeTrigger[
XhmikosR's avatar
XhmikosR committed
646
        event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER
Johann-S's avatar
Johann-S committed
647
648
      ] = false
    }
649

Johann-S's avatar
Johann-S committed
650
651
    if (context._isWithActiveTrigger()) {
      return
652
653
    }

Johann-S's avatar
Johann-S committed
654
    clearTimeout(context._timeout)
655

XhmikosR's avatar
XhmikosR committed
656
    context._hoverState = HOVER_STATE_OUT
657

Johann-S's avatar
Johann-S committed
658
659
660
661
    if (!context.config.delay || !context.config.delay.hide) {
      context.hide()
      return
    }
662

Johann-S's avatar
Johann-S committed
663
    context._timeout = setTimeout(() => {
XhmikosR's avatar
XhmikosR committed
664
      if (context._hoverState === HOVER_STATE_OUT) {
Johann-S's avatar
Johann-S committed
665
        context.hide()
666
      }
Johann-S's avatar
Johann-S committed
667
668
    }, context.config.delay.hide)
  }
669

Johann-S's avatar
Johann-S committed
670
671
672
673
674
  _isWithActiveTrigger() {
    for (const trigger in this._activeTrigger) {
      if (this._activeTrigger[trigger]) {
        return true
      }
675
676
    }

Johann-S's avatar
Johann-S committed
677
678
    return false
  }
679

Johann-S's avatar
Johann-S committed
680
  _getConfig(config) {
681
    const dataAttributes = Manipulator.getDataAttributes(this.element)
682

XhmikosR's avatar
XhmikosR committed
683
684
685
686
687
    Object.keys(dataAttributes).forEach(dataAttr => {
      if (DISALLOWED_ATTRIBUTES.indexOf(dataAttr) !== -1) {
        delete dataAttributes[dataAttr]
      }
    })
688

Johann-S's avatar
Johann-S committed
689
    if (config && typeof config.container === 'object' && config.container.jquery) {
690
691
692
      config.container = config.container[0]
    }

Johann-S's avatar
Johann-S committed
693
694
    config = {
      ...this.constructor.Default,
695
      ...dataAttributes,
696
      ...(typeof config === 'object' && config ? config : {})
697
698
    }

Johann-S's avatar
Johann-S committed
699
700
701
702
    if (typeof config.delay === 'number') {
      config.delay = {
        show: config.delay,
        hide: config.delay
Johann-S's avatar
Johann-S committed
703
      }
Johann-S's avatar
Johann-S committed
704
705
    }

Johann-S's avatar
Johann-S committed
706
707
    if (typeof config.title === 'number') {
      config.title = config.title.toString()
708
    }
709

Johann-S's avatar
Johann-S committed
710
711
    if (typeof config.content === 'number') {
      config.content = config.content.toString()
712
713
    }

XhmikosR's avatar
XhmikosR committed
714
    typeCheckConfig(NAME, config, this.constructor.DefaultType)
715

716
    if (config.sanitize) {
717
      config.template = sanitizeHtml(config.template, config.allowList, config.sanitizeFn)
718
719
    }

Johann-S's avatar
Johann-S committed
720
721
    return config
  }
722

Johann-S's avatar
Johann-S committed
723
724
  _getDelegateConfig() {
    const config = {}
725

Johann-S's avatar
Johann-S committed
726
727
728
729
    if (this.config) {
      for (const key in this.config) {
        if (this.constructor.Default[key] !== this.config[key]) {
          config[key] = this.config[key]
730
        }
Johann-S's avatar
Johann-S committed
731
732
      }
    }
733

Johann-S's avatar
Johann-S committed
734
735
736
737
    return config
  }

  _cleanTipClass() {
738
739
    const tip = this.getTipElement()
    const tabClass = tip.getAttribute('class').match(BSCLS_PREFIX_REGEX)
740
741
    if (tabClass !== null && tabClass.length > 0) {
      tabClass.map(token => token.trim())
XhmikosR's avatar
XhmikosR committed
742
        .forEach(tClass => tip.classList.remove(tClass))
743
744
745
    }
  }

Johann-S's avatar
Johann-S committed
746
  _handlePopperPlacementChange(popperData) {
747
    this.tip = popperData.instance.popper
Johann-S's avatar
Johann-S committed
748
    this._cleanTipClass()
Johann-S's avatar
Johann-S committed
749
    this._addAttachmentClass(this._getAttachment(popperData.placement))
Johann-S's avatar
Johann-S committed
750
751
752
753
754
755
756
757
  }

  _fixTransition() {
    const tip = this.getTipElement()
    const initConfigAnimation = this.config.animation
    if (tip.getAttribute('x-placement') !== null) {
      return
    }
XhmikosR's avatar
XhmikosR committed
758

XhmikosR's avatar
XhmikosR committed
759
    tip.classList.remove(CLASS_NAME_FADE)
Johann-S's avatar
Johann-S committed
760
761
762
763
764
765
766
767
    this.config.animation = false
    this.hide()
    this.show()
    this.config.animation = initConfigAnimation
  }

  // Static

768
  static jQueryInterface(config) {
Johann-S's avatar
Johann-S committed
769
    return this.each(function () {
XhmikosR's avatar
XhmikosR committed
770
      let data = Data.getData(this, DATA_KEY)
Johann-S's avatar
Johann-S committed
771
772
773
774
775
776
777
778
779
      const _config = typeof config === 'object' && config

      if (!data && /dispose|hide/.test(config)) {
        return
      }

      if (!data) {
        data = new Tooltip(this, _config)
      }
780

Johann-S's avatar
Johann-S committed
781
782
783
784
      if (typeof config === 'string') {
        if (typeof data[config] === 'undefined') {
          throw new TypeError(`No method named "${config}"`)
        }
XhmikosR's avatar
XhmikosR committed
785

Johann-S's avatar
Johann-S committed
786
787
788
        data[config]()
      }
    })
789
  }
790

791
  static getInstance(element) {
792
793
    return Data.getData(element, DATA_KEY)
  }
Johann-S's avatar
Johann-S committed
794
795
}

796
797
const $ = getjQuery()

Johann-S's avatar
Johann-S committed
798
799
800
801
/**
 * ------------------------------------------------------------------------
 * jQuery
 * ------------------------------------------------------------------------
802
 * add .tooltip to jQuery only if jQuery is present
Johann-S's avatar
Johann-S committed
803
 */
Johann-S's avatar
Johann-S committed
804
/* istanbul ignore if */
805
if ($) {
XhmikosR's avatar
XhmikosR committed
806
  const JQUERY_NO_CONFLICT = $.fn[NAME]
807
  $.fn[NAME] = Tooltip.jQueryInterface
XhmikosR's avatar
XhmikosR committed
808
809
  $.fn[NAME].Constructor = Tooltip
  $.fn[NAME].noConflict = () => {
810
    $.fn[NAME] = JQUERY_NO_CONFLICT
811
    return Tooltip.jQueryInterface
812
  }
Johann-S's avatar
Johann-S committed
813
}
814
815

export default Tooltip