tooltip.js 19.6 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
import {
9
  getjQuery,
10
11
12
13
14
15
16
17
18
  TRANSITION_END,
  emulateTransitionEnd,
  findShadowRoot,
  getTransitionDurationFromElement,
  getUID,
  isElement,
  makeArray,
  noop,
  typeCheckConfig
Johann-S's avatar
Johann-S committed
19
} from '../util/index'
20
21
22
import {
  DefaultWhitelist,
  sanitizeHtml
Johann-S's avatar
Johann-S committed
23
24
25
26
} from '../util/sanitizer'
import Data from '../dom/data'
import EventHandler from '../dom/event-handler'
import Manipulator from '../dom/manipulator'
27
import Popper from 'popper.js'
Johann-S's avatar
Johann-S committed
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
  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)',
59
60
  whiteList: 'object',
  popperConfig: '(null|object)'
Johann-S's avatar
Johann-S committed
61
62
63
}

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

const Default = {
XhmikosR's avatar
XhmikosR committed
72
73
  animation: true,
  template: '<div class="tooltip" role="tooltip">' +
74
                    '<div class="tooltip-arrow"></div>' +
75
                    '<div class="tooltip-inner"></div></div>',
XhmikosR's avatar
XhmikosR committed
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,
88
89
  whiteList: DefaultWhitelist,
  popperConfig: null
Johann-S's avatar
Johann-S committed
90
91
92
}

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

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

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

const Selector = {
116
  TOOLTIP_INNER: '.tooltip-inner'
Johann-S's avatar
Johann-S committed
117
118
119
}

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

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

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

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

Johann-S's avatar
Johann-S committed
145
146
    // Protected
    this.element = element
XhmikosR's avatar
XhmikosR committed
147
148
    this.config = this._getConfig(config)
    this.tip = null
fat's avatar
fat committed
149

Johann-S's avatar
Johann-S committed
150
    this._setListeners()
151
    Data.setData(element, this.constructor.DATA_KEY, this)
Johann-S's avatar
Johann-S committed
152
  }
fat's avatar
fat committed
153

Johann-S's avatar
Johann-S committed
154
  // Getters
fat's avatar
fat committed
155

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

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

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

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

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

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

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

Johann-S's avatar
Johann-S committed
184
  // Public
185

Johann-S's avatar
Johann-S committed
186
187
188
  enable() {
    this._isEnabled = true
  }
189

Johann-S's avatar
Johann-S committed
190
191
192
  disable() {
    this._isEnabled = false
  }
193

Johann-S's avatar
Johann-S committed
194
195
196
  toggleEnabled() {
    this._isEnabled = !this._isEnabled
  }
Jacob Thornton's avatar
Jacob Thornton committed
197

Johann-S's avatar
Johann-S committed
198
199
200
  toggle(event) {
    if (!this._isEnabled) {
      return
201
202
    }

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

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

Johann-S's avatar
Johann-S committed
215
      context._activeTrigger.click = !context._activeTrigger.click
fat's avatar
fat committed
216

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

Johann-S's avatar
Johann-S committed
228
      this._enter(null, this)
229
    }
Johann-S's avatar
Johann-S committed
230
  }
231

Johann-S's avatar
Johann-S committed
232
233
  dispose() {
    clearTimeout(this._timeout)
234

235
    Data.removeData(this.element, this.constructor.DATA_KEY)
236

237
    EventHandler.off(this.element, this.constructor.EVENT_KEY)
238
    EventHandler.off(SelectorEngine.closest(this.element, '.modal'), 'hide.bs.modal', this._hideModalHandler)
239

Johann-S's avatar
Johann-S committed
240
    if (this.tip) {
241
      this.tip.parentNode.removeChild(this.tip)
Johann-S's avatar
Johann-S committed
242
243
    }

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

Johann-S's avatar
Johann-S committed
252
253
    this._popper = null
    this.element = null
XhmikosR's avatar
XhmikosR committed
254
255
    this.config = null
    this.tip = null
Johann-S's avatar
Johann-S committed
256
  }
257

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

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

270
      if (showEvent.defaultPrevented || !isInTheDom) {
Johann-S's avatar
Johann-S committed
271
272
        return
      }
273

XhmikosR's avatar
XhmikosR committed
274
      const tip = this.getTipElement()
275
      const tipId = getUID(this.constructor.NAME)
276

Johann-S's avatar
Johann-S committed
277
278
      tip.setAttribute('id', tipId)
      this.element.setAttribute('aria-describedby', tipId)
279

Johann-S's avatar
Johann-S committed
280
      this.setContent()
281

Johann-S's avatar
Johann-S committed
282
      if (this.config.animation) {
283
        tip.classList.add(ClassName.FADE)
Johann-S's avatar
Johann-S committed
284
      }
285

XhmikosR's avatar
XhmikosR committed
286
287
288
      const placement = typeof this.config.placement === 'function' ?
        this.config.placement.call(this, tip, this.element) :
        this.config.placement
289

Johann-S's avatar
Johann-S committed
290
      const attachment = this._getAttachment(placement)
Johann-S's avatar
Johann-S committed
291
      this._addAttachmentClass(attachment)
292

293
      const container = this._getContainer()
294
      Data.setData(tip, this.constructor.DATA_KEY, this)
Johann-S's avatar
Johann-S committed
295

296
297
      if (!this.element.ownerDocument.documentElement.contains(this.tip)) {
        container.appendChild(tip)
Johann-S's avatar
Johann-S committed
298
      }
299

300
      EventHandler.trigger(this.element, this.constructor.Event.INSERTED)
301

302
      this._popper = new Popper(this.element, tip, this._getPopperConfig(attachment))
303

304
      tip.classList.add(ClassName.SHOW)
305

Johann-S's avatar
Johann-S committed
306
307
308
309
310
      // 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
311
        makeArray(document.body.children).forEach(element => {
312
          EventHandler.on(element, 'mouseover', noop())
313
        })
314
315
      }

XhmikosR's avatar
XhmikosR committed
316
      const complete = () => {
Johann-S's avatar
Johann-S committed
317
318
        if (this.config.animation) {
          this._fixTransition()
319
        }
XhmikosR's avatar
XhmikosR committed
320

Johann-S's avatar
Johann-S committed
321
        const prevHoverState = this._hoverState
XhmikosR's avatar
XhmikosR committed
322
        this._hoverState = null
323

324
        EventHandler.trigger(this.element, this.constructor.Event.SHOWN)
325

Johann-S's avatar
Johann-S committed
326
327
        if (prevHoverState === HoverState.OUT) {
          this._leave(null, this)
328
329
330
        }
      }

331
      if (this.tip.classList.contains(ClassName.FADE)) {
332
333
334
        const transitionDuration = getTransitionDurationFromElement(this.tip)
        EventHandler.one(this.tip, TRANSITION_END, complete)
        emulateTransitionEnd(this.tip, transitionDuration)
335
336
337
338
      } else {
        complete()
      }
    }
Johann-S's avatar
Johann-S committed
339
  }
340

Johann-S's avatar
Johann-S committed
341
  hide() {
XhmikosR's avatar
XhmikosR committed
342
343
    const tip = this.getTipElement()
    const complete = () => {
Johann-S's avatar
Johann-S committed
344
345
      if (this._hoverState !== HoverState.SHOW && tip.parentNode) {
        tip.parentNode.removeChild(tip)
346
      }
347

Johann-S's avatar
Johann-S committed
348
349
      this._cleanTipClass()
      this.element.removeAttribute('aria-describedby')
350
      EventHandler.trigger(this.element, this.constructor.Event.HIDDEN)
Johann-S's avatar
Johann-S committed
351
      this._popper.destroy()
352
353
    }

354
355
    const hideEvent = EventHandler.trigger(this.element, this.constructor.Event.HIDE)
    if (hideEvent.defaultPrevented) {
Johann-S's avatar
Johann-S committed
356
      return
357
358
    }

359
    tip.classList.remove(ClassName.SHOW)
360

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

Johann-S's avatar
Johann-S committed
368
369
370
    this._activeTrigger[Trigger.CLICK] = false
    this._activeTrigger[Trigger.FOCUS] = false
    this._activeTrigger[Trigger.HOVER] = false
371

372
    if (this.tip.classList.contains(ClassName.FADE)) {
373
374
375
376
      const transitionDuration = getTransitionDurationFromElement(tip)

      EventHandler.one(tip, TRANSITION_END, complete)
      emulateTransitionEnd(tip, transitionDuration)
Johann-S's avatar
Johann-S committed
377
378
    } else {
      complete()
379
380
    }

Johann-S's avatar
Johann-S committed
381
382
    this._hoverState = ''
  }
383

Johann-S's avatar
Johann-S committed
384
385
386
  update() {
    if (this._popper !== null) {
      this._popper.scheduleUpdate()
fat's avatar
fat committed
387
    }
Johann-S's avatar
Johann-S committed
388
  }
fat's avatar
fat committed
389

Johann-S's avatar
Johann-S committed
390
  // Protected
391

Johann-S's avatar
Johann-S committed
392
393
394
  isWithContent() {
    return Boolean(this.getTitle())
  }
395

Johann-S's avatar
Johann-S committed
396
  getTipElement() {
397
398
399
400
401
402
403
404
    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
405
406
407
408
409
    return this.tip
  }

  setContent() {
    const tip = this.getTipElement()
410
411
412
    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
413
414
  }

415
416
417
418
419
  setElementContent(element, content) {
    if (element === null) {
      return
    }

Johann-S's avatar
Johann-S committed
420
    if (typeof content === 'object' && isElement(content)) {
421
422
423
424
425
      if (content.jquery) {
        content = content[0]
      }

      // content is a DOM node or a jQuery
426
      if (this.config.html) {
427
428
429
        if (content.parentNode !== element) {
          element.innerHTML = ''
          element.appendChild(content)
430
        }
431
      } else {
432
        element.innerText = content.textContent
433
      }
434
435
436
437
438
439
440
441
442

      return
    }

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

443
      element.innerHTML = content
Johann-S's avatar
Johann-S committed
444
    } else {
445
      element.innerText = content
446
    }
Johann-S's avatar
Johann-S committed
447
  }
448

Johann-S's avatar
Johann-S committed
449
450
451
452
  getTitle() {
    let title = this.element.getAttribute('data-original-title')

    if (!title) {
XhmikosR's avatar
XhmikosR committed
453
454
455
      title = typeof this.config.title === 'function' ?
        this.config.title.call(this.element) :
        this.config.title
456
457
    }

Johann-S's avatar
Johann-S committed
458
459
    return title
  }
fat's avatar
fat committed
460

Johann-S's avatar
Johann-S committed
461
  // Private
462

463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
  _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)
    }

    let resultConfig = defaultBsConfig
    if (this.config.popperConfig) {
      resultConfig = {
        ...defaultBsConfig,
        ...this.config.popperConfig
      }
    }

    return resultConfig
  }

Johann-S's avatar
Johann-S committed
497
498
499
500
  _addAttachmentClass(attachment) {
    this.getTipElement().classList.add(`${CLASS_PREFIX}-${attachment}`)
  }

501
502
503
504
  _getOffset() {
    const offset = {}

    if (typeof this.config.offset === 'function') {
XhmikosR's avatar
XhmikosR committed
505
      offset.fn = data => {
506
507
508
509
510
511
512
513
514
515
516
517
518
519
        data.offsets = {
          ...data.offsets,
          ...this.config.offset(data.offsets, this.element) || {}
        }

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

    return offset
  }

520
521
522
523
524
  _getContainer() {
    if (this.config.container === false) {
      return document.body
    }

525
    if (isElement(this.config.container)) {
526
      return this.config.container
527
528
    }

529
    return SelectorEngine.findOne(this.config.container)
530
531
  }

Johann-S's avatar
Johann-S committed
532
533
534
535
536
537
538
  _getAttachment(placement) {
    return AttachmentMap[placement.toUpperCase()]
  }

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

XhmikosR's avatar
XhmikosR committed
539
    triggers.forEach(trigger => {
Johann-S's avatar
Johann-S committed
540
      if (trigger === 'click') {
541
        EventHandler.on(this.element,
Johann-S's avatar
Johann-S committed
542
543
          this.constructor.Event.CLICK,
          this.config.selector,
XhmikosR's avatar
XhmikosR committed
544
          event => this.toggle(event)
545
        )
Johann-S's avatar
Johann-S committed
546
      } else if (trigger !== Trigger.MANUAL) {
XhmikosR's avatar
XhmikosR committed
547
548
549
550
551
552
        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
553

554
555
556
        EventHandler.on(this.element,
          eventIn,
          this.config.selector,
XhmikosR's avatar
XhmikosR committed
557
          event => this._enter(event)
558
559
560
561
        )
        EventHandler.on(this.element,
          eventOut,
          this.config.selector,
XhmikosR's avatar
XhmikosR committed
562
          event => this._leave(event)
563
        )
564
      }
Johann-S's avatar
Johann-S committed
565
    })
566

567
568
569
570
571
572
    this._hideModalHandler = () => {
      if (this.element) {
        this.hide()
      }
    }

573
    EventHandler.on(SelectorEngine.closest(this.element, '.modal'),
574
      'hide.bs.modal',
575
      this._hideModalHandler
576
577
    )

Johann-S's avatar
Johann-S committed
578
579
580
581
582
    if (this.config.selector) {
      this.config = {
        ...this.config,
        trigger: 'manual',
        selector: ''
583
      }
Johann-S's avatar
Johann-S committed
584
585
586
587
    } else {
      this._fixTitle()
    }
  }
588

Johann-S's avatar
Johann-S committed
589
590
  _fixTitle() {
    const titleType = typeof this.element.getAttribute('data-original-title')
591
592

    if (this.element.getAttribute('title') || titleType !== 'string') {
Johann-S's avatar
Johann-S committed
593
594
595
596
      this.element.setAttribute(
        'data-original-title',
        this.element.getAttribute('title') || ''
      )
597

Johann-S's avatar
Johann-S committed
598
599
600
      this.element.setAttribute('title', '')
    }
  }
601

Johann-S's avatar
Johann-S committed
602
603
  _enter(event, context) {
    const dataKey = this.constructor.DATA_KEY
604
    context = context || Data.getData(event.delegateTarget, dataKey)
605

Johann-S's avatar
Johann-S committed
606
607
    if (!context) {
      context = new this.constructor(
608
        event.delegateTarget,
Johann-S's avatar
Johann-S committed
609
610
        this._getDelegateConfig()
      )
611
      Data.setData(event.delegateTarget, dataKey, context)
612
613
    }

Johann-S's avatar
Johann-S committed
614
615
616
617
618
    if (event) {
      context._activeTrigger[
        event.type === 'focusin' ? Trigger.FOCUS : Trigger.HOVER
      ] = true
    }
fat's avatar
fat committed
619

620
621
    if (context.getTipElement().classList.contains(ClassName.SHOW) ||
        context._hoverState === HoverState.SHOW) {
Johann-S's avatar
Johann-S committed
622
623
624
      context._hoverState = HoverState.SHOW
      return
    }
625

Johann-S's avatar
Johann-S committed
626
    clearTimeout(context._timeout)
627

Johann-S's avatar
Johann-S committed
628
    context._hoverState = HoverState.SHOW
629

Johann-S's avatar
Johann-S committed
630
631
632
633
    if (!context.config.delay || !context.config.delay.show) {
      context.show()
      return
    }
634

Johann-S's avatar
Johann-S committed
635
636
637
638
639
640
    context._timeout = setTimeout(() => {
      if (context._hoverState === HoverState.SHOW) {
        context.show()
      }
    }, context.config.delay.show)
  }
641

Johann-S's avatar
Johann-S committed
642
643
  _leave(event, context) {
    const dataKey = this.constructor.DATA_KEY
644
    context = context || Data.getData(event.delegateTarget, dataKey)
645

Johann-S's avatar
Johann-S committed
646
647
    if (!context) {
      context = new this.constructor(
648
        event.delegateTarget,
Johann-S's avatar
Johann-S committed
649
650
        this._getDelegateConfig()
      )
651
      Data.setData(event.delegateTarget, dataKey, context)
652
653
    }

Johann-S's avatar
Johann-S committed
654
655
656
657
658
    if (event) {
      context._activeTrigger[
        event.type === 'focusout' ? Trigger.FOCUS : Trigger.HOVER
      ] = false
    }
659

Johann-S's avatar
Johann-S committed
660
661
    if (context._isWithActiveTrigger()) {
      return
662
663
    }

Johann-S's avatar
Johann-S committed
664
    clearTimeout(context._timeout)
665

Johann-S's avatar
Johann-S committed
666
    context._hoverState = HoverState.OUT
667

Johann-S's avatar
Johann-S committed
668
669
670
671
    if (!context.config.delay || !context.config.delay.hide) {
      context.hide()
      return
    }
672

Johann-S's avatar
Johann-S committed
673
674
675
    context._timeout = setTimeout(() => {
      if (context._hoverState === HoverState.OUT) {
        context.hide()
676
      }
Johann-S's avatar
Johann-S committed
677
678
    }, context.config.delay.hide)
  }
679

Johann-S's avatar
Johann-S committed
680
681
682
683
684
  _isWithActiveTrigger() {
    for (const trigger in this._activeTrigger) {
      if (this._activeTrigger[trigger]) {
        return true
      }
685
686
    }

Johann-S's avatar
Johann-S committed
687
688
    return false
  }
689

Johann-S's avatar
Johann-S committed
690
  _getConfig(config) {
691
    const dataAttributes = Manipulator.getDataAttributes(this.element)
692
693

    Object.keys(dataAttributes)
XhmikosR's avatar
XhmikosR committed
694
      .forEach(dataAttr => {
695
696
697
698
699
        if (DISALLOWED_ATTRIBUTES.indexOf(dataAttr) !== -1) {
          delete dataAttributes[dataAttr]
        }
      })

Johann-S's avatar
Johann-S committed
700
    if (config && typeof config.container === 'object' && config.container.jquery) {
701
702
703
      config.container = config.container[0]
    }

Johann-S's avatar
Johann-S committed
704
705
    config = {
      ...this.constructor.Default,
706
      ...dataAttributes,
Johann-S's avatar
Johann-S committed
707
      ...typeof config === 'object' && config ? config : {}
708
709
    }

Johann-S's avatar
Johann-S committed
710
711
712
713
    if (typeof config.delay === 'number') {
      config.delay = {
        show: config.delay,
        hide: config.delay
Johann-S's avatar
Johann-S committed
714
      }
Johann-S's avatar
Johann-S committed
715
716
    }

Johann-S's avatar
Johann-S committed
717
718
    if (typeof config.title === 'number') {
      config.title = config.title.toString()
719
    }
720

Johann-S's avatar
Johann-S committed
721
722
    if (typeof config.content === 'number') {
      config.content = config.content.toString()
723
724
    }

725
    typeCheckConfig(
Johann-S's avatar
Johann-S committed
726
727
728
729
      NAME,
      config,
      this.constructor.DefaultType
    )
730

731
732
733
734
    if (config.sanitize) {
      config.template = sanitizeHtml(config.template, config.whiteList, config.sanitizeFn)
    }

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

Johann-S's avatar
Johann-S committed
738
739
  _getDelegateConfig() {
    const config = {}
740

Johann-S's avatar
Johann-S committed
741
742
743
744
    if (this.config) {
      for (const key in this.config) {
        if (this.constructor.Default[key] !== this.config[key]) {
          config[key] = this.config[key]
745
        }
Johann-S's avatar
Johann-S committed
746
747
      }
    }
748

Johann-S's avatar
Johann-S committed
749
750
751
752
    return config
  }

  _cleanTipClass() {
753
754
    const tip = this.getTipElement()
    const tabClass = tip.getAttribute('class').match(BSCLS_PREFIX_REGEX)
Johann-S's avatar
Johann-S committed
755
    if (tabClass !== null && tabClass.length) {
756
      tabClass
XhmikosR's avatar
XhmikosR committed
757
758
        .map(token => token.trim())
        .forEach(tClass => tip.classList.remove(tClass))
759
760
761
    }
  }

Johann-S's avatar
Johann-S committed
762
763
764
765
  _handlePopperPlacementChange(popperData) {
    const popperInstance = popperData.instance
    this.tip = popperInstance.popper
    this._cleanTipClass()
Johann-S's avatar
Johann-S committed
766
    this._addAttachmentClass(this._getAttachment(popperData.placement))
Johann-S's avatar
Johann-S committed
767
768
769
770
771
772
773
774
  }

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

776
    tip.classList.remove(ClassName.FADE)
Johann-S's avatar
Johann-S committed
777
778
779
780
781
782
783
784
    this.config.animation = false
    this.hide()
    this.show()
    this.config.animation = initConfigAnimation
  }

  // Static

785
  static jQueryInterface(config) {
Johann-S's avatar
Johann-S committed
786
    return this.each(function () {
XhmikosR's avatar
XhmikosR committed
787
      let data = Data.getData(this, DATA_KEY)
Johann-S's avatar
Johann-S committed
788
789
790
791
792
793
794
795
796
      const _config = typeof config === 'object' && config

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

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

Johann-S's avatar
Johann-S committed
798
799
800
801
      if (typeof config === 'string') {
        if (typeof data[config] === 'undefined') {
          throw new TypeError(`No method named "${config}"`)
        }
XhmikosR's avatar
XhmikosR committed
802

Johann-S's avatar
Johann-S committed
803
804
805
        data[config]()
      }
    })
806
  }
807

808
  static getInstance(element) {
809
810
    return Data.getData(element, DATA_KEY)
  }
Johann-S's avatar
Johann-S committed
811
812
}

813
814
const $ = getjQuery()

Johann-S's avatar
Johann-S committed
815
816
817
818
/**
 * ------------------------------------------------------------------------
 * jQuery
 * ------------------------------------------------------------------------
819
 * add .tooltip to jQuery only if jQuery is present
Johann-S's avatar
Johann-S committed
820
 */
Johann-S's avatar
Johann-S committed
821
/* istanbul ignore if */
822
if ($) {
XhmikosR's avatar
XhmikosR committed
823
  const JQUERY_NO_CONFLICT = $.fn[NAME]
824
  $.fn[NAME] = Tooltip.jQueryInterface
XhmikosR's avatar
XhmikosR committed
825
826
  $.fn[NAME].Constructor = Tooltip
  $.fn[NAME].noConflict = () => {
827
    $.fn[NAME] = JQUERY_NO_CONFLICT
828
    return Tooltip.jQueryInterface
829
  }
Johann-S's avatar
Johann-S committed
830
}
831
832

export default Tooltip