tooltip.js 19.4 KB
Newer Older
1
2
/**
 * --------------------------------------------------------------------------
XhmikosR's avatar
XhmikosR committed
3
 * Bootstrap (v4.3.1): tooltip.js
4
5
6
7
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 * --------------------------------------------------------------------------
 */

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

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

XhmikosR's avatar
XhmikosR committed
36
37
38
39
40
41
const NAME = 'tooltip'
const VERSION = '4.3.1'
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')
42
const DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn']
Johann-S's avatar
Johann-S committed
43
44

const DefaultType = {
XhmikosR's avatar
XhmikosR committed
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
  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)',
  whiteList: '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
87
  trigger: 'hover focus',
  title: '',
  delay: 0,
  html: false,
  selector: false,
  placement: 'top',
  offset: 0,
  container: false,
  fallbackPlacement: 'flip',
  boundary: 'scrollParent',
  sanitize: true,
  sanitizeFn: null,
  whiteList: DefaultWhitelist
Johann-S's avatar
Johann-S committed
88
89
90
}

const HoverState = {
XhmikosR's avatar
XhmikosR committed
91
92
  SHOW: 'show',
  OUT: 'out'
Johann-S's avatar
Johann-S committed
93
94
95
}

const Event = {
XhmikosR's avatar
XhmikosR committed
96
97
98
99
100
101
102
103
104
105
  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
106
107
108
}

const ClassName = {
XhmikosR's avatar
XhmikosR committed
109
110
  FADE: 'fade',
  SHOW: 'show'
Johann-S's avatar
Johann-S committed
111
112
113
}

const Selector = {
XhmikosR's avatar
XhmikosR committed
114
115
  TOOLTIP_INNER: '.tooltip-inner',
  TOOLTIP_ARROW: '.tooltip-arrow'
Johann-S's avatar
Johann-S committed
116
117
118
}

const Trigger = {
XhmikosR's avatar
XhmikosR committed
119
120
121
122
  HOVER: 'hover',
  FOCUS: 'focus',
  CLICK: 'click',
  MANUAL: 'manual'
Johann-S's avatar
Johann-S committed
123
}
124

Johann-S's avatar
Johann-S committed
125
126
127
128
129
/**
 * ------------------------------------------------------------------------
 * Class Definition
 * ------------------------------------------------------------------------
 */
130

Johann-S's avatar
Johann-S committed
131
132
133
134
135
136
137
class Tooltip {
  constructor(element, config) {
    /**
     * Check for Popper dependency
     * Popper - https://popper.js.org
     */
    if (typeof Popper === 'undefined') {
138
      throw new TypeError('Bootstrap\'s tooltips require Popper.js (https://popper.js.org)')
139
140
    }

Johann-S's avatar
Johann-S committed
141
    // private
XhmikosR's avatar
XhmikosR committed
142
143
144
    this._isEnabled = true
    this._timeout = 0
    this._hoverState = ''
Johann-S's avatar
Johann-S committed
145
    this._activeTrigger = {}
XhmikosR's avatar
XhmikosR committed
146
    this._popper = null
147

Johann-S's avatar
Johann-S committed
148
149
    // Protected
    this.element = element
XhmikosR's avatar
XhmikosR committed
150
151
    this.config = this._getConfig(config)
    this.tip = null
fat's avatar
fat committed
152

Johann-S's avatar
Johann-S committed
153
    this._setListeners()
154
    Data.setData(element, this.constructor.DATA_KEY, this)
Johann-S's avatar
Johann-S committed
155
  }
fat's avatar
fat committed
156

Johann-S's avatar
Johann-S committed
157
  // Getters
fat's avatar
fat committed
158

Johann-S's avatar
Johann-S committed
159
160
161
  static get VERSION() {
    return VERSION
  }
fat's avatar
fat committed
162

Johann-S's avatar
Johann-S committed
163
164
165
  static get Default() {
    return Default
  }
fat's avatar
fat committed
166

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

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

Johann-S's avatar
Johann-S committed
175
176
177
  static get Event() {
    return Event
  }
178

Johann-S's avatar
Johann-S committed
179
180
181
  static get EVENT_KEY() {
    return EVENT_KEY
  }
182

Johann-S's avatar
Johann-S committed
183
184
185
  static get DefaultType() {
    return DefaultType
  }
186

Johann-S's avatar
Johann-S committed
187
  // Public
188

Johann-S's avatar
Johann-S committed
189
190
191
  enable() {
    this._isEnabled = true
  }
192

Johann-S's avatar
Johann-S committed
193
194
195
  disable() {
    this._isEnabled = false
  }
196

Johann-S's avatar
Johann-S committed
197
198
199
  toggleEnabled() {
    this._isEnabled = !this._isEnabled
  }
Jacob Thornton's avatar
Jacob Thornton committed
200

Johann-S's avatar
Johann-S committed
201
202
203
  toggle(event) {
    if (!this._isEnabled) {
      return
204
205
    }

Johann-S's avatar
Johann-S committed
206
207
    if (event) {
      const dataKey = this.constructor.DATA_KEY
208
      let context = Data.getData(event.delegateTarget, dataKey)
fat's avatar
fat committed
209

Johann-S's avatar
Johann-S committed
210
211
      if (!context) {
        context = new this.constructor(
212
          event.delegateTarget,
Johann-S's avatar
Johann-S committed
213
214
          this._getDelegateConfig()
        )
215
        Data.setData(event.delegateTarget, dataKey, context)
Johann-S's avatar
Johann-S committed
216
      }
fat's avatar
fat committed
217

Johann-S's avatar
Johann-S committed
218
      context._activeTrigger.click = !context._activeTrigger.click
fat's avatar
fat committed
219

Johann-S's avatar
Johann-S committed
220
221
222
223
      if (context._isWithActiveTrigger()) {
        context._enter(null, context)
      } else {
        context._leave(null, context)
fat's avatar
fat committed
224
      }
Johann-S's avatar
Johann-S committed
225
    } else {
226
      if (this.getTipElement().classList.contains(ClassName.SHOW)) {
Johann-S's avatar
Johann-S committed
227
228
        this._leave(null, this)
        return
229
      }
fat's avatar
fat committed
230

Johann-S's avatar
Johann-S committed
231
      this._enter(null, this)
232
    }
Johann-S's avatar
Johann-S committed
233
  }
234

Johann-S's avatar
Johann-S committed
235
236
  dispose() {
    clearTimeout(this._timeout)
237

238
    Data.removeData(this.element, this.constructor.DATA_KEY)
239

240
241
    EventHandler.off(this.element, this.constructor.EVENT_KEY)
    EventHandler.off(SelectorEngine.closest(this.element, '.modal'), 'hide.bs.modal')
242

Johann-S's avatar
Johann-S committed
243
    if (this.tip) {
244
      this.tip.parentNode.removeChild(this.tip)
Johann-S's avatar
Johann-S committed
245
246
    }

XhmikosR's avatar
XhmikosR committed
247
248
249
    this._isEnabled = null
    this._timeout = null
    this._hoverState = null
Johann-S's avatar
Johann-S committed
250
251
252
253
    this._activeTrigger = null
    if (this._popper !== null) {
      this._popper.destroy()
    }
254

Johann-S's avatar
Johann-S committed
255
256
    this._popper = null
    this.element = null
XhmikosR's avatar
XhmikosR committed
257
258
    this.config = null
    this.tip = null
Johann-S's avatar
Johann-S committed
259
  }
260

Johann-S's avatar
Johann-S committed
261
  show() {
262
    if (this.element.style.display === 'none') {
Johann-S's avatar
Johann-S committed
263
264
      throw new Error('Please use show on visible elements')
    }
265

Johann-S's avatar
Johann-S committed
266
    if (this.isWithContent() && this._isEnabled) {
267
      const showEvent = EventHandler.trigger(this.element, this.constructor.Event.SHOW)
268
      const shadowRoot = findShadowRoot(this.element)
XhmikosR's avatar
XhmikosR committed
269
270
271
      const isInTheDom = shadowRoot === null ?
        this.element.ownerDocument.documentElement.contains(this.element) :
        shadowRoot.contains(this.element)
272

273
      if (showEvent.defaultPrevented || !isInTheDom) {
Johann-S's avatar
Johann-S committed
274
275
        return
      }
276

XhmikosR's avatar
XhmikosR committed
277
      const tip = this.getTipElement()
278
      const tipId = getUID(this.constructor.NAME)
279

Johann-S's avatar
Johann-S committed
280
281
      tip.setAttribute('id', tipId)
      this.element.setAttribute('aria-describedby', tipId)
282

Johann-S's avatar
Johann-S committed
283
      this.setContent()
284

Johann-S's avatar
Johann-S committed
285
      if (this.config.animation) {
286
        tip.classList.add(ClassName.FADE)
Johann-S's avatar
Johann-S committed
287
      }
288

XhmikosR's avatar
XhmikosR committed
289
290
291
      const placement = typeof this.config.placement === 'function' ?
        this.config.placement.call(this, tip, this.element) :
        this.config.placement
292

Johann-S's avatar
Johann-S committed
293
294
      const attachment = this._getAttachment(placement)
      this.addAttachmentClass(attachment)
295

296
      const container = this._getContainer()
297
      Data.setData(tip, this.constructor.DATA_KEY, this)
Johann-S's avatar
Johann-S committed
298

299
300
      if (!this.element.ownerDocument.documentElement.contains(this.tip)) {
        container.appendChild(tip)
Johann-S's avatar
Johann-S committed
301
      }
302

303
      EventHandler.trigger(this.element, this.constructor.Event.INSERTED)
304

Johann-S's avatar
Johann-S committed
305
306
307
      this._popper = new Popper(this.element, tip, {
        placement: attachment,
        modifiers: {
308
          offset: this._getOffset(),
Johann-S's avatar
Johann-S committed
309
310
311
312
          flip: {
            behavior: this.config.fallbackPlacement
          },
          arrow: {
313
            element: Selector.TOOLTIP_ARROW
Johann-S's avatar
Johann-S committed
314
315
316
317
318
          },
          preventOverflow: {
            boundariesElement: this.config.boundary
          }
        },
XhmikosR's avatar
XhmikosR committed
319
        onCreate: data => {
Johann-S's avatar
Johann-S committed
320
321
          if (data.originalPlacement !== data.placement) {
            this._handlePopperPlacementChange(data)
322
          }
Johann-S's avatar
Johann-S committed
323
        },
XhmikosR's avatar
XhmikosR committed
324
        onUpdate: data => this._handlePopperPlacementChange(data)
Johann-S's avatar
Johann-S committed
325
      })
326

327
      tip.classList.add(ClassName.SHOW)
328

Johann-S's avatar
Johann-S committed
329
330
331
332
333
      // 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) {
XhmikosR's avatar
XhmikosR committed
334
        makeArray(document.body.children).forEach(element => {
335
          EventHandler.on(element, 'mouseover', noop())
336
        })
337
338
      }

XhmikosR's avatar
XhmikosR committed
339
      const complete = () => {
Johann-S's avatar
Johann-S committed
340
341
        if (this.config.animation) {
          this._fixTransition()
342
        }
XhmikosR's avatar
XhmikosR committed
343

Johann-S's avatar
Johann-S committed
344
        const prevHoverState = this._hoverState
XhmikosR's avatar
XhmikosR committed
345
        this._hoverState = null
346

347
        EventHandler.trigger(this.element, this.constructor.Event.SHOWN)
348

Johann-S's avatar
Johann-S committed
349
350
        if (prevHoverState === HoverState.OUT) {
          this._leave(null, this)
351
352
353
        }
      }

354
      if (this.tip.classList.contains(ClassName.FADE)) {
355
356
357
        const transitionDuration = getTransitionDurationFromElement(this.tip)
        EventHandler.one(this.tip, TRANSITION_END, complete)
        emulateTransitionEnd(this.tip, transitionDuration)
358
359
360
361
      } else {
        complete()
      }
    }
Johann-S's avatar
Johann-S committed
362
  }
363

Johann-S's avatar
Johann-S committed
364
  hide(callback) {
XhmikosR's avatar
XhmikosR committed
365
366
    const tip = this.getTipElement()
    const complete = () => {
Johann-S's avatar
Johann-S committed
367
368
      if (this._hoverState !== HoverState.SHOW && tip.parentNode) {
        tip.parentNode.removeChild(tip)
369
      }
370

Johann-S's avatar
Johann-S committed
371
372
      this._cleanTipClass()
      this.element.removeAttribute('aria-describedby')
373
      EventHandler.trigger(this.element, this.constructor.Event.HIDDEN)
Johann-S's avatar
Johann-S committed
374
375
376
      if (this._popper !== null) {
        this._popper.destroy()
      }
377

Johann-S's avatar
Johann-S committed
378
379
380
      if (callback) {
        callback()
      }
381
382
    }

383
384
    const hideEvent = EventHandler.trigger(this.element, this.constructor.Event.HIDE)
    if (hideEvent.defaultPrevented) {
Johann-S's avatar
Johann-S committed
385
      return
386
387
    }

388
    tip.classList.remove(ClassName.SHOW)
389

Johann-S's avatar
Johann-S committed
390
391
392
    // If this is a touch-enabled device we remove the extra
    // empty mouseover listeners we added for iOS support
    if ('ontouchstart' in document.documentElement) {
393
      makeArray(document.body.children)
XhmikosR's avatar
XhmikosR committed
394
        .forEach(element => EventHandler.off(element, 'mouseover', noop))
395
396
    }

Johann-S's avatar
Johann-S committed
397
398
399
    this._activeTrigger[Trigger.CLICK] = false
    this._activeTrigger[Trigger.FOCUS] = false
    this._activeTrigger[Trigger.HOVER] = false
400

401
    if (this.tip.classList.contains(ClassName.FADE)) {
402
403
404
405
      const transitionDuration = getTransitionDurationFromElement(tip)

      EventHandler.one(tip, TRANSITION_END, complete)
      emulateTransitionEnd(tip, transitionDuration)
Johann-S's avatar
Johann-S committed
406
407
    } else {
      complete()
408
409
    }

Johann-S's avatar
Johann-S committed
410
411
    this._hoverState = ''
  }
412

Johann-S's avatar
Johann-S committed
413
414
415
  update() {
    if (this._popper !== null) {
      this._popper.scheduleUpdate()
fat's avatar
fat committed
416
    }
Johann-S's avatar
Johann-S committed
417
  }
fat's avatar
fat committed
418

Johann-S's avatar
Johann-S committed
419
  // Protected
420

Johann-S's avatar
Johann-S committed
421
422
423
  isWithContent() {
    return Boolean(this.getTitle())
  }
424

Johann-S's avatar
Johann-S committed
425
  addAttachmentClass(attachment) {
426
    this.getTipElement().classList.add(`${CLASS_PREFIX}-${attachment}`)
Johann-S's avatar
Johann-S committed
427
  }
428

Johann-S's avatar
Johann-S committed
429
  getTipElement() {
430
431
432
433
434
435
436
437
    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
438
439
440
441
442
    return this.tip
  }

  setContent() {
    const tip = this.getTipElement()
443
444
445
    this.setElementContent(SelectorEngine.findOne(Selector.TOOLTIP_INNER, tip), this.getTitle())
    tip.classList.remove(ClassName.FADE)
    tip.classList.remove(ClassName.SHOW)
Johann-S's avatar
Johann-S committed
446
447
  }

448
449
450
451
452
  setElementContent(element, content) {
    if (element === null) {
      return
    }

Johann-S's avatar
Johann-S committed
453
    if (typeof content === 'object' && (content.nodeType || content.jquery)) {
454
455
456
457
458
      if (content.jquery) {
        content = content[0]
      }

      // content is a DOM node or a jQuery
459
      if (this.config.html) {
460
461
462
        if (content.parentNode !== element) {
          element.innerHTML = ''
          element.appendChild(content)
463
        }
464
      } else {
465
        element.innerText = content.textContent
466
      }
467
468
469
470
471
472
473
474
475

      return
    }

    if (this.config.html) {
      if (this.config.sanitize) {
        content = sanitizeHtml(content, this.config.whiteList, this.config.sanitizeFn)
      }

476
      element.innerHTML = content
Johann-S's avatar
Johann-S committed
477
    } else {
478
      element.innerText = content
479
    }
Johann-S's avatar
Johann-S committed
480
  }
481

Johann-S's avatar
Johann-S committed
482
483
484
485
  getTitle() {
    let title = this.element.getAttribute('data-original-title')

    if (!title) {
XhmikosR's avatar
XhmikosR committed
486
487
488
      title = typeof this.config.title === 'function' ?
        this.config.title.call(this.element) :
        this.config.title
489
490
    }

Johann-S's avatar
Johann-S committed
491
492
    return title
  }
fat's avatar
fat committed
493

Johann-S's avatar
Johann-S committed
494
  // Private
495

496
497
498
499
  _getOffset() {
    const offset = {}

    if (typeof this.config.offset === 'function') {
XhmikosR's avatar
XhmikosR committed
500
      offset.fn = data => {
501
502
503
504
505
506
507
508
509
510
511
512
513
514
        data.offsets = {
          ...data.offsets,
          ...this.config.offset(data.offsets, this.element) || {}
        }

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

    return offset
  }

515
516
517
518
519
  _getContainer() {
    if (this.config.container === false) {
      return document.body
    }

520
    if (isElement(this.config.container)) {
521
      return this.config.container
522
523
    }

524
    return SelectorEngine.findOne(this.config.container)
525
526
  }

Johann-S's avatar
Johann-S committed
527
528
529
530
531
532
533
  _getAttachment(placement) {
    return AttachmentMap[placement.toUpperCase()]
  }

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

XhmikosR's avatar
XhmikosR committed
534
    triggers.forEach(trigger => {
Johann-S's avatar
Johann-S committed
535
      if (trigger === 'click') {
536
        EventHandler.on(this.element,
Johann-S's avatar
Johann-S committed
537
538
          this.constructor.Event.CLICK,
          this.config.selector,
XhmikosR's avatar
XhmikosR committed
539
          event => this.toggle(event)
540
        )
Johann-S's avatar
Johann-S committed
541
      } else if (trigger !== Trigger.MANUAL) {
XhmikosR's avatar
XhmikosR committed
542
543
544
545
546
547
        const eventIn = trigger === Trigger.HOVER ?
          this.constructor.Event.MOUSEENTER :
          this.constructor.Event.FOCUSIN
        const eventOut = trigger === Trigger.HOVER ?
          this.constructor.Event.MOUSELEAVE :
          this.constructor.Event.FOCUSOUT
Johann-S's avatar
Johann-S committed
548

549
550
551
        EventHandler.on(this.element,
          eventIn,
          this.config.selector,
XhmikosR's avatar
XhmikosR committed
552
          event => this._enter(event)
553
554
555
556
        )
        EventHandler.on(this.element,
          eventOut,
          this.config.selector,
XhmikosR's avatar
XhmikosR committed
557
          event => this._leave(event)
558
        )
559
      }
Johann-S's avatar
Johann-S committed
560
    })
561

562
    EventHandler.on(SelectorEngine.closest(this.element, '.modal'),
563
564
565
566
567
568
569
570
      'hide.bs.modal',
      () => {
        if (this.element) {
          this.hide()
        }
      }
    )

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

Johann-S's avatar
Johann-S committed
582
583
  _fixTitle() {
    const titleType = typeof this.element.getAttribute('data-original-title')
584
585

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

Johann-S's avatar
Johann-S committed
591
592
593
      this.element.setAttribute('title', '')
    }
  }
594

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

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

Johann-S's avatar
Johann-S committed
607
608
609
610
611
    if (event) {
      context._activeTrigger[
        event.type === 'focusin' ? Trigger.FOCUS : Trigger.HOVER
      ] = true
    }
fat's avatar
fat committed
612

613
614
    if (context.getTipElement().classList.contains(ClassName.SHOW) ||
        context._hoverState === HoverState.SHOW) {
Johann-S's avatar
Johann-S committed
615
616
617
      context._hoverState = HoverState.SHOW
      return
    }
618

Johann-S's avatar
Johann-S committed
619
    clearTimeout(context._timeout)
620

Johann-S's avatar
Johann-S committed
621
    context._hoverState = HoverState.SHOW
622

Johann-S's avatar
Johann-S committed
623
624
625
626
    if (!context.config.delay || !context.config.delay.show) {
      context.show()
      return
    }
627

Johann-S's avatar
Johann-S committed
628
629
630
631
632
633
    context._timeout = setTimeout(() => {
      if (context._hoverState === HoverState.SHOW) {
        context.show()
      }
    }, context.config.delay.show)
  }
634

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

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

Johann-S's avatar
Johann-S committed
647
648
649
650
651
    if (event) {
      context._activeTrigger[
        event.type === 'focusout' ? Trigger.FOCUS : Trigger.HOVER
      ] = false
    }
652

Johann-S's avatar
Johann-S committed
653
654
    if (context._isWithActiveTrigger()) {
      return
655
656
    }

Johann-S's avatar
Johann-S committed
657
    clearTimeout(context._timeout)
658

Johann-S's avatar
Johann-S committed
659
    context._hoverState = HoverState.OUT
660

Johann-S's avatar
Johann-S committed
661
662
663
664
    if (!context.config.delay || !context.config.delay.hide) {
      context.hide()
      return
    }
665

Johann-S's avatar
Johann-S committed
666
667
668
    context._timeout = setTimeout(() => {
      if (context._hoverState === HoverState.OUT) {
        context.hide()
669
      }
Johann-S's avatar
Johann-S committed
670
671
    }, context.config.delay.hide)
  }
672

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

Johann-S's avatar
Johann-S committed
680
681
    return false
  }
682

Johann-S's avatar
Johann-S committed
683
  _getConfig(config) {
684
    const dataAttributes = Manipulator.getDataAttributes(this.element)
685
686

    Object.keys(dataAttributes)
XhmikosR's avatar
XhmikosR committed
687
      .forEach(dataAttr => {
688
689
690
691
692
        if (DISALLOWED_ATTRIBUTES.indexOf(dataAttr) !== -1) {
          delete dataAttributes[dataAttr]
        }
      })

Johann-S's avatar
Johann-S committed
693
    if (config && typeof config.container === 'object' && config.container.jquery) {
694
695
696
      config.container = config.container[0]
    }

Johann-S's avatar
Johann-S committed
697
698
    config = {
      ...this.constructor.Default,
699
      ...dataAttributes,
Johann-S's avatar
Johann-S committed
700
      ...typeof config === 'object' && config ? config : {}
701
702
    }

Johann-S's avatar
Johann-S committed
703
704
705
706
    if (typeof config.delay === 'number') {
      config.delay = {
        show: config.delay,
        hide: config.delay
Johann-S's avatar
Johann-S committed
707
      }
Johann-S's avatar
Johann-S committed
708
709
    }

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

Johann-S's avatar
Johann-S committed
714
715
    if (typeof config.content === 'number') {
      config.content = config.content.toString()
716
717
    }

718
    typeCheckConfig(
Johann-S's avatar
Johann-S committed
719
720
721
722
      NAME,
      config,
      this.constructor.DefaultType
    )
723

724
725
726
727
    if (config.sanitize) {
      config.template = sanitizeHtml(config.template, config.whiteList, config.sanitizeFn)
    }

Johann-S's avatar
Johann-S committed
728
729
    return config
  }
730

Johann-S's avatar
Johann-S committed
731
732
  _getDelegateConfig() {
    const config = {}
733

Johann-S's avatar
Johann-S committed
734
735
736
737
    if (this.config) {
      for (const key in this.config) {
        if (this.constructor.Default[key] !== this.config[key]) {
          config[key] = this.config[key]
738
        }
Johann-S's avatar
Johann-S committed
739
740
      }
    }
741

Johann-S's avatar
Johann-S committed
742
743
744
745
    return config
  }

  _cleanTipClass() {
746
747
    const tip = this.getTipElement()
    const tabClass = tip.getAttribute('class').match(BSCLS_PREFIX_REGEX)
Johann-S's avatar
Johann-S committed
748
    if (tabClass !== null && tabClass.length) {
749
      tabClass
XhmikosR's avatar
XhmikosR committed
750
751
        .map(token => token.trim())
        .forEach(tClass => tip.classList.remove(tClass))
752
753
754
    }
  }

Johann-S's avatar
Johann-S committed
755
756
757
758
759
760
761
762
763
764
765
766
767
  _handlePopperPlacementChange(popperData) {
    const popperInstance = popperData.instance
    this.tip = popperInstance.popper
    this._cleanTipClass()
    this.addAttachmentClass(this._getAttachment(popperData.placement))
  }

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

769
    tip.classList.remove(ClassName.FADE)
Johann-S's avatar
Johann-S committed
770
771
772
773
774
775
776
777
778
779
    this.config.animation = false
    this.hide()
    this.show()
    this.config.animation = initConfigAnimation
  }

  // Static

  static _jQueryInterface(config) {
    return this.each(function () {
XhmikosR's avatar
XhmikosR committed
780
      let data = Data.getData(this, DATA_KEY)
Johann-S's avatar
Johann-S committed
781
782
783
784
785
786
787
788
789
      const _config = typeof config === 'object' && config

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

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

Johann-S's avatar
Johann-S committed
791
792
793
794
      if (typeof config === 'string') {
        if (typeof data[config] === 'undefined') {
          throw new TypeError(`No method named "${config}"`)
        }
XhmikosR's avatar
XhmikosR committed
795

Johann-S's avatar
Johann-S committed
796
797
798
        data[config]()
      }
    })
799
  }
800
801
802
803

  static _getInstance(element) {
    return Data.getData(element, DATA_KEY)
  }
Johann-S's avatar
Johann-S committed
804
805
806
807
808
809
}

/**
 * ------------------------------------------------------------------------
 * jQuery
 * ------------------------------------------------------------------------
810
 * add .tooltip to jQuery only if jQuery is present
Johann-S's avatar
Johann-S committed
811
 */
812

813
if (typeof $ !== 'undefined') {
XhmikosR's avatar
XhmikosR committed
814
815
816
817
  const JQUERY_NO_CONFLICT = $.fn[NAME]
  $.fn[NAME] = Tooltip._jQueryInterface
  $.fn[NAME].Constructor = Tooltip
  $.fn[NAME].noConflict = () => {
818
819
820
    $.fn[NAME] = JQUERY_NO_CONFLICT
    return Tooltip._jQueryInterface
  }
Johann-S's avatar
Johann-S committed
821
}
822
823

export default Tooltip