talos

Форк
0
764 строки · 17.8 Кб
1
/**
2
 * --------------------------------------------------------------------------
3
 * Bootstrap (v4.6.1): tooltip.js
4
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
5
 * --------------------------------------------------------------------------
6
 */
7

8
import { DefaultWhitelist, sanitizeHtml } from './tools/sanitizer'
9
import $ from 'jquery'
10
import Popper from 'popper.js'
11
import Util from './util'
12

13
/**
14
 * Constants
15
 */
16

17
const NAME = 'tooltip'
18
const VERSION = '4.6.1'
19
const DATA_KEY = 'bs.tooltip'
20
const EVENT_KEY = `.${DATA_KEY}`
21
const JQUERY_NO_CONFLICT = $.fn[NAME]
22
const CLASS_PREFIX = 'bs-tooltip'
23
const BSCLS_PREFIX_REGEX = new RegExp(`(^|\\s)${CLASS_PREFIX}\\S+`, 'g')
24
const DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn']
25

26
const CLASS_NAME_FADE = 'fade'
27
const CLASS_NAME_SHOW = 'show'
28

29
const HOVER_STATE_SHOW = 'show'
30
const HOVER_STATE_OUT = 'out'
31

32
const SELECTOR_TOOLTIP_INNER = '.tooltip-inner'
33
const SELECTOR_ARROW = '.arrow'
34

35
const TRIGGER_HOVER = 'hover'
36
const TRIGGER_FOCUS = 'focus'
37
const TRIGGER_CLICK = 'click'
38
const TRIGGER_MANUAL = 'manual'
39

40
const AttachmentMap = {
41
  AUTO: 'auto',
42
  TOP: 'top',
43
  RIGHT: 'right',
44
  BOTTOM: 'bottom',
45
  LEFT: 'left'
46
}
47

48
const Default = {
49
  animation: true,
50
  template: '<div class="tooltip" role="tooltip">' +
51
                    '<div class="arrow"></div>' +
52
                    '<div class="tooltip-inner"></div></div>',
53
  trigger: 'hover focus',
54
  title: '',
55
  delay: 0,
56
  html: false,
57
  selector: false,
58
  placement: 'top',
59
  offset: 0,
60
  container: false,
61
  fallbackPlacement: 'flip',
62
  boundary: 'scrollParent',
63
  customClass: '',
64
  sanitize: true,
65
  sanitizeFn: null,
66
  whiteList: DefaultWhitelist,
67
  popperConfig: null
68
}
69

70
const DefaultType = {
71
  animation: 'boolean',
72
  template: 'string',
73
  title: '(string|element|function)',
74
  trigger: 'string',
75
  delay: '(number|object)',
76
  html: 'boolean',
77
  selector: '(string|boolean)',
78
  placement: '(string|function)',
79
  offset: '(number|string|function)',
80
  container: '(string|element|boolean)',
81
  fallbackPlacement: '(string|array)',
82
  boundary: '(string|element)',
83
  customClass: '(string|function)',
84
  sanitize: 'boolean',
85
  sanitizeFn: '(null|function)',
86
  whiteList: 'object',
87
  popperConfig: '(null|object)'
88
}
89

90
const Event = {
91
  HIDE: `hide${EVENT_KEY}`,
92
  HIDDEN: `hidden${EVENT_KEY}`,
93
  SHOW: `show${EVENT_KEY}`,
94
  SHOWN: `shown${EVENT_KEY}`,
95
  INSERTED: `inserted${EVENT_KEY}`,
96
  CLICK: `click${EVENT_KEY}`,
97
  FOCUSIN: `focusin${EVENT_KEY}`,
98
  FOCUSOUT: `focusout${EVENT_KEY}`,
99
  MOUSEENTER: `mouseenter${EVENT_KEY}`,
100
  MOUSELEAVE: `mouseleave${EVENT_KEY}`
101
}
102

103
/**
104
 * Class definition
105
 */
106

107
class Tooltip {
108
  constructor(element, config) {
109
    if (typeof Popper === 'undefined') {
110
      throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org)')
111
    }
112

113
    // Private
114
    this._isEnabled = true
115
    this._timeout = 0
116
    this._hoverState = ''
117
    this._activeTrigger = {}
118
    this._popper = null
119

120
    // Protected
121
    this.element = element
122
    this.config = this._getConfig(config)
123
    this.tip = null
124

125
    this._setListeners()
126
  }
127

128
  // Getters
129
  static get VERSION() {
130
    return VERSION
131
  }
132

133
  static get Default() {
134
    return Default
135
  }
136

137
  static get NAME() {
138
    return NAME
139
  }
140

141
  static get DATA_KEY() {
142
    return DATA_KEY
143
  }
144

145
  static get Event() {
146
    return Event
147
  }
148

149
  static get EVENT_KEY() {
150
    return EVENT_KEY
151
  }
152

153
  static get DefaultType() {
154
    return DefaultType
155
  }
156

157
  // Public
158
  enable() {
159
    this._isEnabled = true
160
  }
161

162
  disable() {
163
    this._isEnabled = false
164
  }
165

166
  toggleEnabled() {
167
    this._isEnabled = !this._isEnabled
168
  }
169

170
  toggle(event) {
171
    if (!this._isEnabled) {
172
      return
173
    }
174

175
    if (event) {
176
      const dataKey = this.constructor.DATA_KEY
177
      let context = $(event.currentTarget).data(dataKey)
178

179
      if (!context) {
180
        context = new this.constructor(
181
          event.currentTarget,
182
          this._getDelegateConfig()
183
        )
184
        $(event.currentTarget).data(dataKey, context)
185
      }
186

187
      context._activeTrigger.click = !context._activeTrigger.click
188

189
      if (context._isWithActiveTrigger()) {
190
        context._enter(null, context)
191
      } else {
192
        context._leave(null, context)
193
      }
194
    } else {
195
      if ($(this.getTipElement()).hasClass(CLASS_NAME_SHOW)) {
196
        this._leave(null, this)
197
        return
198
      }
199

200
      this._enter(null, this)
201
    }
202
  }
203

204
  dispose() {
205
    clearTimeout(this._timeout)
206

207
    $.removeData(this.element, this.constructor.DATA_KEY)
208

209
    $(this.element).off(this.constructor.EVENT_KEY)
210
    $(this.element).closest('.modal').off('hide.bs.modal', this._hideModalHandler)
211

212
    if (this.tip) {
213
      $(this.tip).remove()
214
    }
215

216
    this._isEnabled = null
217
    this._timeout = null
218
    this._hoverState = null
219
    this._activeTrigger = null
220
    if (this._popper) {
221
      this._popper.destroy()
222
    }
223

224
    this._popper = null
225
    this.element = null
226
    this.config = null
227
    this.tip = null
228
  }
229

230
  show() {
231
    if ($(this.element).css('display') === 'none') {
232
      throw new Error('Please use show on visible elements')
233
    }
234

235
    const showEvent = $.Event(this.constructor.Event.SHOW)
236
    if (this.isWithContent() && this._isEnabled) {
237
      $(this.element).trigger(showEvent)
238

239
      const shadowRoot = Util.findShadowRoot(this.element)
240
      const isInTheDom = $.contains(
241
        shadowRoot !== null ? shadowRoot : this.element.ownerDocument.documentElement,
242
        this.element
243
      )
244

245
      if (showEvent.isDefaultPrevented() || !isInTheDom) {
246
        return
247
      }
248

249
      const tip = this.getTipElement()
250
      const tipId = Util.getUID(this.constructor.NAME)
251

252
      tip.setAttribute('id', tipId)
253
      this.element.setAttribute('aria-describedby', tipId)
254

255
      this.setContent()
256

257
      if (this.config.animation) {
258
        $(tip).addClass(CLASS_NAME_FADE)
259
      }
260

261
      const placement = typeof this.config.placement === 'function' ?
262
        this.config.placement.call(this, tip, this.element) :
263
        this.config.placement
264

265
      const attachment = this._getAttachment(placement)
266
      this.addAttachmentClass(attachment)
267

268
      const container = this._getContainer()
269
      $(tip).data(this.constructor.DATA_KEY, this)
270

271
      if (!$.contains(this.element.ownerDocument.documentElement, this.tip)) {
272
        $(tip).appendTo(container)
273
      }
274

275
      $(this.element).trigger(this.constructor.Event.INSERTED)
276

277
      this._popper = new Popper(this.element, tip, this._getPopperConfig(attachment))
278

279
      $(tip).addClass(CLASS_NAME_SHOW)
280
      $(tip).addClass(this.config.customClass)
281

282
      // If this is a touch-enabled device we add extra
283
      // empty mouseover listeners to the body's immediate children;
284
      // only needed because of broken event delegation on iOS
285
      // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
286
      if ('ontouchstart' in document.documentElement) {
287
        $(document.body).children().on('mouseover', null, $.noop)
288
      }
289

290
      const complete = () => {
291
        if (this.config.animation) {
292
          this._fixTransition()
293
        }
294

295
        const prevHoverState = this._hoverState
296
        this._hoverState = null
297

298
        $(this.element).trigger(this.constructor.Event.SHOWN)
299

300
        if (prevHoverState === HOVER_STATE_OUT) {
301
          this._leave(null, this)
302
        }
303
      }
304

305
      if ($(this.tip).hasClass(CLASS_NAME_FADE)) {
306
        const transitionDuration = Util.getTransitionDurationFromElement(this.tip)
307

308
        $(this.tip)
309
          .one(Util.TRANSITION_END, complete)
310
          .emulateTransitionEnd(transitionDuration)
311
      } else {
312
        complete()
313
      }
314
    }
315
  }
316

317
  hide(callback) {
318
    const tip = this.getTipElement()
319
    const hideEvent = $.Event(this.constructor.Event.HIDE)
320
    const complete = () => {
321
      if (this._hoverState !== HOVER_STATE_SHOW && tip.parentNode) {
322
        tip.parentNode.removeChild(tip)
323
      }
324

325
      this._cleanTipClass()
326
      this.element.removeAttribute('aria-describedby')
327
      $(this.element).trigger(this.constructor.Event.HIDDEN)
328
      if (this._popper !== null) {
329
        this._popper.destroy()
330
      }
331

332
      if (callback) {
333
        callback()
334
      }
335
    }
336

337
    $(this.element).trigger(hideEvent)
338

339
    if (hideEvent.isDefaultPrevented()) {
340
      return
341
    }
342

343
    $(tip).removeClass(CLASS_NAME_SHOW)
344

345
    // If this is a touch-enabled device we remove the extra
346
    // empty mouseover listeners we added for iOS support
347
    if ('ontouchstart' in document.documentElement) {
348
      $(document.body).children().off('mouseover', null, $.noop)
349
    }
350

351
    this._activeTrigger[TRIGGER_CLICK] = false
352
    this._activeTrigger[TRIGGER_FOCUS] = false
353
    this._activeTrigger[TRIGGER_HOVER] = false
354

355
    if ($(this.tip).hasClass(CLASS_NAME_FADE)) {
356
      const transitionDuration = Util.getTransitionDurationFromElement(tip)
357

358
      $(tip)
359
        .one(Util.TRANSITION_END, complete)
360
        .emulateTransitionEnd(transitionDuration)
361
    } else {
362
      complete()
363
    }
364

365
    this._hoverState = ''
366
  }
367

368
  update() {
369
    if (this._popper !== null) {
370
      this._popper.scheduleUpdate()
371
    }
372
  }
373

374
  // Protected
375
  isWithContent() {
376
    return Boolean(this.getTitle())
377
  }
378

379
  addAttachmentClass(attachment) {
380
    $(this.getTipElement()).addClass(`${CLASS_PREFIX}-${attachment}`)
381
  }
382

383
  getTipElement() {
384
    this.tip = this.tip || $(this.config.template)[0]
385
    return this.tip
386
  }
387

388
  setContent() {
389
    const tip = this.getTipElement()
390
    this.setElementContent($(tip.querySelectorAll(SELECTOR_TOOLTIP_INNER)), this.getTitle())
391
    $(tip).removeClass(`${CLASS_NAME_FADE} ${CLASS_NAME_SHOW}`)
392
  }
393

394
  setElementContent($element, content) {
395
    if (typeof content === 'object' && (content.nodeType || content.jquery)) {
396
      // Content is a DOM node or a jQuery
397
      if (this.config.html) {
398
        if (!$(content).parent().is($element)) {
399
          $element.empty().append(content)
400
        }
401
      } else {
402
        $element.text($(content).text())
403
      }
404

405
      return
406
    }
407

408
    if (this.config.html) {
409
      if (this.config.sanitize) {
410
        content = sanitizeHtml(content, this.config.whiteList, this.config.sanitizeFn)
411
      }
412

413
      $element.html(content)
414
    } else {
415
      $element.text(content)
416
    }
417
  }
418

419
  getTitle() {
420
    let title = this.element.getAttribute('data-original-title')
421

422
    if (!title) {
423
      title = typeof this.config.title === 'function' ?
424
        this.config.title.call(this.element) :
425
        this.config.title
426
    }
427

428
    return title
429
  }
430

431
  // Private
432
  _getPopperConfig(attachment) {
433
    const defaultBsConfig = {
434
      placement: attachment,
435
      modifiers: {
436
        offset: this._getOffset(),
437
        flip: {
438
          behavior: this.config.fallbackPlacement
439
        },
440
        arrow: {
441
          element: SELECTOR_ARROW
442
        },
443
        preventOverflow: {
444
          boundariesElement: this.config.boundary
445
        }
446
      },
447
      onCreate: data => {
448
        if (data.originalPlacement !== data.placement) {
449
          this._handlePopperPlacementChange(data)
450
        }
451
      },
452
      onUpdate: data => this._handlePopperPlacementChange(data)
453
    }
454

455
    return {
456
      ...defaultBsConfig,
457
      ...this.config.popperConfig
458
    }
459
  }
460

461
  _getOffset() {
462
    const offset = {}
463

464
    if (typeof this.config.offset === 'function') {
465
      offset.fn = data => {
466
        data.offsets = {
467
          ...data.offsets,
468
          ...this.config.offset(data.offsets, this.element)
469
        }
470

471
        return data
472
      }
473
    } else {
474
      offset.offset = this.config.offset
475
    }
476

477
    return offset
478
  }
479

480
  _getContainer() {
481
    if (this.config.container === false) {
482
      return document.body
483
    }
484

485
    if (Util.isElement(this.config.container)) {
486
      return $(this.config.container)
487
    }
488

489
    return $(document).find(this.config.container)
490
  }
491

492
  _getAttachment(placement) {
493
    return AttachmentMap[placement.toUpperCase()]
494
  }
495

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

499
    triggers.forEach(trigger => {
500
      if (trigger === 'click') {
501
        $(this.element).on(
502
          this.constructor.Event.CLICK,
503
          this.config.selector,
504
          event => this.toggle(event)
505
        )
506
      } else if (trigger !== TRIGGER_MANUAL) {
507
        const eventIn = trigger === TRIGGER_HOVER ?
508
          this.constructor.Event.MOUSEENTER :
509
          this.constructor.Event.FOCUSIN
510
        const eventOut = trigger === TRIGGER_HOVER ?
511
          this.constructor.Event.MOUSELEAVE :
512
          this.constructor.Event.FOCUSOUT
513

514
        $(this.element)
515
          .on(eventIn, this.config.selector, event => this._enter(event))
516
          .on(eventOut, this.config.selector, event => this._leave(event))
517
      }
518
    })
519

520
    this._hideModalHandler = () => {
521
      if (this.element) {
522
        this.hide()
523
      }
524
    }
525

526
    $(this.element).closest('.modal').on('hide.bs.modal', this._hideModalHandler)
527

528
    if (this.config.selector) {
529
      this.config = {
530
        ...this.config,
531
        trigger: 'manual',
532
        selector: ''
533
      }
534
    } else {
535
      this._fixTitle()
536
    }
537
  }
538

539
  _fixTitle() {
540
    const titleType = typeof this.element.getAttribute('data-original-title')
541

542
    if (this.element.getAttribute('title') || titleType !== 'string') {
543
      this.element.setAttribute(
544
        'data-original-title',
545
        this.element.getAttribute('title') || ''
546
      )
547

548
      this.element.setAttribute('title', '')
549
    }
550
  }
551

552
  _enter(event, context) {
553
    const dataKey = this.constructor.DATA_KEY
554
    context = context || $(event.currentTarget).data(dataKey)
555

556
    if (!context) {
557
      context = new this.constructor(
558
        event.currentTarget,
559
        this._getDelegateConfig()
560
      )
561
      $(event.currentTarget).data(dataKey, context)
562
    }
563

564
    if (event) {
565
      context._activeTrigger[
566
        event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER
567
      ] = true
568
    }
569

570
    if ($(context.getTipElement()).hasClass(CLASS_NAME_SHOW) || context._hoverState === HOVER_STATE_SHOW) {
571
      context._hoverState = HOVER_STATE_SHOW
572
      return
573
    }
574

575
    clearTimeout(context._timeout)
576

577
    context._hoverState = HOVER_STATE_SHOW
578

579
    if (!context.config.delay || !context.config.delay.show) {
580
      context.show()
581
      return
582
    }
583

584
    context._timeout = setTimeout(() => {
585
      if (context._hoverState === HOVER_STATE_SHOW) {
586
        context.show()
587
      }
588
    }, context.config.delay.show)
589
  }
590

591
  _leave(event, context) {
592
    const dataKey = this.constructor.DATA_KEY
593
    context = context || $(event.currentTarget).data(dataKey)
594

595
    if (!context) {
596
      context = new this.constructor(
597
        event.currentTarget,
598
        this._getDelegateConfig()
599
      )
600
      $(event.currentTarget).data(dataKey, context)
601
    }
602

603
    if (event) {
604
      context._activeTrigger[
605
        event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER
606
      ] = false
607
    }
608

609
    if (context._isWithActiveTrigger()) {
610
      return
611
    }
612

613
    clearTimeout(context._timeout)
614

615
    context._hoverState = HOVER_STATE_OUT
616

617
    if (!context.config.delay || !context.config.delay.hide) {
618
      context.hide()
619
      return
620
    }
621

622
    context._timeout = setTimeout(() => {
623
      if (context._hoverState === HOVER_STATE_OUT) {
624
        context.hide()
625
      }
626
    }, context.config.delay.hide)
627
  }
628

629
  _isWithActiveTrigger() {
630
    for (const trigger in this._activeTrigger) {
631
      if (this._activeTrigger[trigger]) {
632
        return true
633
      }
634
    }
635

636
    return false
637
  }
638

639
  _getConfig(config) {
640
    const dataAttributes = $(this.element).data()
641

642
    Object.keys(dataAttributes)
643
      .forEach(dataAttr => {
644
        if (DISALLOWED_ATTRIBUTES.indexOf(dataAttr) !== -1) {
645
          delete dataAttributes[dataAttr]
646
        }
647
      })
648

649
    config = {
650
      ...this.constructor.Default,
651
      ...dataAttributes,
652
      ...(typeof config === 'object' && config ? config : {})
653
    }
654

655
    if (typeof config.delay === 'number') {
656
      config.delay = {
657
        show: config.delay,
658
        hide: config.delay
659
      }
660
    }
661

662
    if (typeof config.title === 'number') {
663
      config.title = config.title.toString()
664
    }
665

666
    if (typeof config.content === 'number') {
667
      config.content = config.content.toString()
668
    }
669

670
    Util.typeCheckConfig(
671
      NAME,
672
      config,
673
      this.constructor.DefaultType
674
    )
675

676
    if (config.sanitize) {
677
      config.template = sanitizeHtml(config.template, config.whiteList, config.sanitizeFn)
678
    }
679

680
    return config
681
  }
682

683
  _getDelegateConfig() {
684
    const config = {}
685

686
    if (this.config) {
687
      for (const key in this.config) {
688
        if (this.constructor.Default[key] !== this.config[key]) {
689
          config[key] = this.config[key]
690
        }
691
      }
692
    }
693

694
    return config
695
  }
696

697
  _cleanTipClass() {
698
    const $tip = $(this.getTipElement())
699
    const tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX)
700
    if (tabClass !== null && tabClass.length) {
701
      $tip.removeClass(tabClass.join(''))
702
    }
703
  }
704

705
  _handlePopperPlacementChange(popperData) {
706
    this.tip = popperData.instance.popper
707
    this._cleanTipClass()
708
    this.addAttachmentClass(this._getAttachment(popperData.placement))
709
  }
710

711
  _fixTransition() {
712
    const tip = this.getTipElement()
713
    const initConfigAnimation = this.config.animation
714

715
    if (tip.getAttribute('x-placement') !== null) {
716
      return
717
    }
718

719
    $(tip).removeClass(CLASS_NAME_FADE)
720
    this.config.animation = false
721
    this.hide()
722
    this.show()
723
    this.config.animation = initConfigAnimation
724
  }
725

726
  // Static
727
  static _jQueryInterface(config) {
728
    return this.each(function () {
729
      const $element = $(this)
730
      let data = $element.data(DATA_KEY)
731
      const _config = typeof config === 'object' && config
732

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

737
      if (!data) {
738
        data = new Tooltip(this, _config)
739
        $element.data(DATA_KEY, data)
740
      }
741

742
      if (typeof config === 'string') {
743
        if (typeof data[config] === 'undefined') {
744
          throw new TypeError(`No method named "${config}"`)
745
        }
746

747
        data[config]()
748
      }
749
    })
750
  }
751
}
752

753
/**
754
 * jQuery
755
 */
756

757
$.fn[NAME] = Tooltip._jQueryInterface
758
$.fn[NAME].Constructor = Tooltip
759
$.fn[NAME].noConflict = () => {
760
  $.fn[NAME] = JQUERY_NO_CONFLICT
761
  return Tooltip._jQueryInterface
762
}
763

764
export default Tooltip
765

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.