source: trip-planner-front/node_modules/bootstrap/js/src/tooltip.js@ 6a3a178

Last change on this file since 6a3a178 was 6a3a178, checked in by Ema <ema_spirova@…>, 3 years ago

initial commit

  • Property mode set to 100644
File size: 19.0 KB
Line 
1/**
2 * --------------------------------------------------------------------------
3 * Bootstrap (v5.1.3): tooltip.js
4 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
5 * --------------------------------------------------------------------------
6 */
7
8import * as Popper from '@popperjs/core'
9
10import {
11 defineJQueryPlugin,
12 findShadowRoot,
13 getElement,
14 getUID,
15 isElement,
16 isRTL,
17 noop,
18 typeCheckConfig
19} from './util/index'
20import { DefaultAllowlist, sanitizeHtml } from './util/sanitizer'
21import Data from './dom/data'
22import EventHandler from './dom/event-handler'
23import Manipulator from './dom/manipulator'
24import SelectorEngine from './dom/selector-engine'
25import BaseComponent from './base-component'
26
27/**
28 * ------------------------------------------------------------------------
29 * Constants
30 * ------------------------------------------------------------------------
31 */
32
33const NAME = 'tooltip'
34const DATA_KEY = 'bs.tooltip'
35const EVENT_KEY = `.${DATA_KEY}`
36const CLASS_PREFIX = 'bs-tooltip'
37const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn'])
38
39const DefaultType = {
40 animation: 'boolean',
41 template: 'string',
42 title: '(string|element|function)',
43 trigger: 'string',
44 delay: '(number|object)',
45 html: 'boolean',
46 selector: '(string|boolean)',
47 placement: '(string|function)',
48 offset: '(array|string|function)',
49 container: '(string|element|boolean)',
50 fallbackPlacements: 'array',
51 boundary: '(string|element)',
52 customClass: '(string|function)',
53 sanitize: 'boolean',
54 sanitizeFn: '(null|function)',
55 allowList: 'object',
56 popperConfig: '(null|object|function)'
57}
58
59const AttachmentMap = {
60 AUTO: 'auto',
61 TOP: 'top',
62 RIGHT: isRTL() ? 'left' : 'right',
63 BOTTOM: 'bottom',
64 LEFT: isRTL() ? 'right' : 'left'
65}
66
67const Default = {
68 animation: true,
69 template: '<div class="tooltip" role="tooltip">' +
70 '<div class="tooltip-arrow"></div>' +
71 '<div class="tooltip-inner"></div>' +
72 '</div>',
73 trigger: 'hover focus',
74 title: '',
75 delay: 0,
76 html: false,
77 selector: false,
78 placement: 'top',
79 offset: [0, 0],
80 container: false,
81 fallbackPlacements: ['top', 'right', 'bottom', 'left'],
82 boundary: 'clippingParents',
83 customClass: '',
84 sanitize: true,
85 sanitizeFn: null,
86 allowList: DefaultAllowlist,
87 popperConfig: null
88}
89
90const 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
103const CLASS_NAME_FADE = 'fade'
104const CLASS_NAME_MODAL = 'modal'
105const CLASS_NAME_SHOW = 'show'
106
107const HOVER_STATE_SHOW = 'show'
108const HOVER_STATE_OUT = 'out'
109
110const SELECTOR_TOOLTIP_INNER = '.tooltip-inner'
111const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`
112
113const EVENT_MODAL_HIDE = 'hide.bs.modal'
114
115const TRIGGER_HOVER = 'hover'
116const TRIGGER_FOCUS = 'focus'
117const TRIGGER_CLICK = 'click'
118const TRIGGER_MANUAL = 'manual'
119
120/**
121 * ------------------------------------------------------------------------
122 * Class Definition
123 * ------------------------------------------------------------------------
124 */
125
126class Tooltip extends BaseComponent {
127 constructor(element, config) {
128 if (typeof Popper === 'undefined') {
129 throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org)')
130 }
131
132 super(element)
133
134 // private
135 this._isEnabled = true
136 this._timeout = 0
137 this._hoverState = ''
138 this._activeTrigger = {}
139 this._popper = null
140
141 // Protected
142 this._config = this._getConfig(config)
143 this.tip = null
144
145 this._setListeners()
146 }
147
148 // Getters
149
150 static get Default() {
151 return Default
152 }
153
154 static get NAME() {
155 return NAME
156 }
157
158 static get Event() {
159 return Event
160 }
161
162 static get DefaultType() {
163 return DefaultType
164 }
165
166 // Public
167
168 enable() {
169 this._isEnabled = true
170 }
171
172 disable() {
173 this._isEnabled = false
174 }
175
176 toggleEnabled() {
177 this._isEnabled = !this._isEnabled
178 }
179
180 toggle(event) {
181 if (!this._isEnabled) {
182 return
183 }
184
185 if (event) {
186 const context = this._initializeOnDelegatedTarget(event)
187
188 context._activeTrigger.click = !context._activeTrigger.click
189
190 if (context._isWithActiveTrigger()) {
191 context._enter(null, context)
192 } else {
193 context._leave(null, context)
194 }
195 } else {
196 if (this.getTipElement().classList.contains(CLASS_NAME_SHOW)) {
197 this._leave(null, this)
198 return
199 }
200
201 this._enter(null, this)
202 }
203 }
204
205 dispose() {
206 clearTimeout(this._timeout)
207
208 EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler)
209
210 if (this.tip) {
211 this.tip.remove()
212 }
213
214 this._disposePopper()
215 super.dispose()
216 }
217
218 show() {
219 if (this._element.style.display === 'none') {
220 throw new Error('Please use show on visible elements')
221 }
222
223 if (!(this.isWithContent() && this._isEnabled)) {
224 return
225 }
226
227 const showEvent = EventHandler.trigger(this._element, this.constructor.Event.SHOW)
228 const shadowRoot = findShadowRoot(this._element)
229 const isInTheDom = shadowRoot === null ?
230 this._element.ownerDocument.documentElement.contains(this._element) :
231 shadowRoot.contains(this._element)
232
233 if (showEvent.defaultPrevented || !isInTheDom) {
234 return
235 }
236
237 // A trick to recreate a tooltip in case a new title is given by using the NOT documented `data-bs-original-title`
238 // This will be removed later in favor of a `setContent` method
239 if (this.constructor.NAME === 'tooltip' && this.tip && this.getTitle() !== this.tip.querySelector(SELECTOR_TOOLTIP_INNER).innerHTML) {
240 this._disposePopper()
241 this.tip.remove()
242 this.tip = null
243 }
244
245 const tip = this.getTipElement()
246 const tipId = getUID(this.constructor.NAME)
247
248 tip.setAttribute('id', tipId)
249 this._element.setAttribute('aria-describedby', tipId)
250
251 if (this._config.animation) {
252 tip.classList.add(CLASS_NAME_FADE)
253 }
254
255 const placement = typeof this._config.placement === 'function' ?
256 this._config.placement.call(this, tip, this._element) :
257 this._config.placement
258
259 const attachment = this._getAttachment(placement)
260 this._addAttachmentClass(attachment)
261
262 const { container } = this._config
263 Data.set(tip, this.constructor.DATA_KEY, this)
264
265 if (!this._element.ownerDocument.documentElement.contains(this.tip)) {
266 container.append(tip)
267 EventHandler.trigger(this._element, this.constructor.Event.INSERTED)
268 }
269
270 if (this._popper) {
271 this._popper.update()
272 } else {
273 this._popper = Popper.createPopper(this._element, tip, this._getPopperConfig(attachment))
274 }
275
276 tip.classList.add(CLASS_NAME_SHOW)
277
278 const customClass = this._resolvePossibleFunction(this._config.customClass)
279 if (customClass) {
280 tip.classList.add(...customClass.split(' '))
281 }
282
283 // If this is a touch-enabled device we add extra
284 // empty mouseover listeners to the body's immediate children;
285 // only needed because of broken event delegation on iOS
286 // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
287 if ('ontouchstart' in document.documentElement) {
288 [].concat(...document.body.children).forEach(element => {
289 EventHandler.on(element, 'mouseover', noop)
290 })
291 }
292
293 const complete = () => {
294 const prevHoverState = this._hoverState
295
296 this._hoverState = null
297 EventHandler.trigger(this._element, this.constructor.Event.SHOWN)
298
299 if (prevHoverState === HOVER_STATE_OUT) {
300 this._leave(null, this)
301 }
302 }
303
304 const isAnimated = this.tip.classList.contains(CLASS_NAME_FADE)
305 this._queueCallback(complete, this.tip, isAnimated)
306 }
307
308 hide() {
309 if (!this._popper) {
310 return
311 }
312
313 const tip = this.getTipElement()
314 const complete = () => {
315 if (this._isWithActiveTrigger()) {
316 return
317 }
318
319 if (this._hoverState !== HOVER_STATE_SHOW) {
320 tip.remove()
321 }
322
323 this._cleanTipClass()
324 this._element.removeAttribute('aria-describedby')
325 EventHandler.trigger(this._element, this.constructor.Event.HIDDEN)
326
327 this._disposePopper()
328 }
329
330 const hideEvent = EventHandler.trigger(this._element, this.constructor.Event.HIDE)
331 if (hideEvent.defaultPrevented) {
332 return
333 }
334
335 tip.classList.remove(CLASS_NAME_SHOW)
336
337 // If this is a touch-enabled device we remove the extra
338 // empty mouseover listeners we added for iOS support
339 if ('ontouchstart' in document.documentElement) {
340 [].concat(...document.body.children)
341 .forEach(element => EventHandler.off(element, 'mouseover', noop))
342 }
343
344 this._activeTrigger[TRIGGER_CLICK] = false
345 this._activeTrigger[TRIGGER_FOCUS] = false
346 this._activeTrigger[TRIGGER_HOVER] = false
347
348 const isAnimated = this.tip.classList.contains(CLASS_NAME_FADE)
349 this._queueCallback(complete, this.tip, isAnimated)
350 this._hoverState = ''
351 }
352
353 update() {
354 if (this._popper !== null) {
355 this._popper.update()
356 }
357 }
358
359 // Protected
360
361 isWithContent() {
362 return Boolean(this.getTitle())
363 }
364
365 getTipElement() {
366 if (this.tip) {
367 return this.tip
368 }
369
370 const element = document.createElement('div')
371 element.innerHTML = this._config.template
372
373 const tip = element.children[0]
374 this.setContent(tip)
375 tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW)
376
377 this.tip = tip
378 return this.tip
379 }
380
381 setContent(tip) {
382 this._sanitizeAndSetContent(tip, this.getTitle(), SELECTOR_TOOLTIP_INNER)
383 }
384
385 _sanitizeAndSetContent(template, content, selector) {
386 const templateElement = SelectorEngine.findOne(selector, template)
387
388 if (!content && templateElement) {
389 templateElement.remove()
390 return
391 }
392
393 // we use append for html objects to maintain js events
394 this.setElementContent(templateElement, content)
395 }
396
397 setElementContent(element, content) {
398 if (element === null) {
399 return
400 }
401
402 if (isElement(content)) {
403 content = getElement(content)
404
405 // content is a DOM node or a jQuery
406 if (this._config.html) {
407 if (content.parentNode !== element) {
408 element.innerHTML = ''
409 element.append(content)
410 }
411 } else {
412 element.textContent = content.textContent
413 }
414
415 return
416 }
417
418 if (this._config.html) {
419 if (this._config.sanitize) {
420 content = sanitizeHtml(content, this._config.allowList, this._config.sanitizeFn)
421 }
422
423 element.innerHTML = content
424 } else {
425 element.textContent = content
426 }
427 }
428
429 getTitle() {
430 const title = this._element.getAttribute('data-bs-original-title') || this._config.title
431
432 return this._resolvePossibleFunction(title)
433 }
434
435 updateAttachment(attachment) {
436 if (attachment === 'right') {
437 return 'end'
438 }
439
440 if (attachment === 'left') {
441 return 'start'
442 }
443
444 return attachment
445 }
446
447 // Private
448
449 _initializeOnDelegatedTarget(event, context) {
450 return context || this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig())
451 }
452
453 _getOffset() {
454 const { offset } = this._config
455
456 if (typeof offset === 'string') {
457 return offset.split(',').map(val => Number.parseInt(val, 10))
458 }
459
460 if (typeof offset === 'function') {
461 return popperData => offset(popperData, this._element)
462 }
463
464 return offset
465 }
466
467 _resolvePossibleFunction(content) {
468 return typeof content === 'function' ? content.call(this._element) : content
469 }
470
471 _getPopperConfig(attachment) {
472 const defaultBsPopperConfig = {
473 placement: attachment,
474 modifiers: [
475 {
476 name: 'flip',
477 options: {
478 fallbackPlacements: this._config.fallbackPlacements
479 }
480 },
481 {
482 name: 'offset',
483 options: {
484 offset: this._getOffset()
485 }
486 },
487 {
488 name: 'preventOverflow',
489 options: {
490 boundary: this._config.boundary
491 }
492 },
493 {
494 name: 'arrow',
495 options: {
496 element: `.${this.constructor.NAME}-arrow`
497 }
498 },
499 {
500 name: 'onChange',
501 enabled: true,
502 phase: 'afterWrite',
503 fn: data => this._handlePopperPlacementChange(data)
504 }
505 ],
506 onFirstUpdate: data => {
507 if (data.options.placement !== data.placement) {
508 this._handlePopperPlacementChange(data)
509 }
510 }
511 }
512
513 return {
514 ...defaultBsPopperConfig,
515 ...(typeof this._config.popperConfig === 'function' ? this._config.popperConfig(defaultBsPopperConfig) : this._config.popperConfig)
516 }
517 }
518
519 _addAttachmentClass(attachment) {
520 this.getTipElement().classList.add(`${this._getBasicClassPrefix()}-${this.updateAttachment(attachment)}`)
521 }
522
523 _getAttachment(placement) {
524 return AttachmentMap[placement.toUpperCase()]
525 }
526
527 _setListeners() {
528 const triggers = this._config.trigger.split(' ')
529
530 triggers.forEach(trigger => {
531 if (trigger === 'click') {
532 EventHandler.on(this._element, this.constructor.Event.CLICK, this._config.selector, event => this.toggle(event))
533 } else if (trigger !== TRIGGER_MANUAL) {
534 const eventIn = trigger === TRIGGER_HOVER ?
535 this.constructor.Event.MOUSEENTER :
536 this.constructor.Event.FOCUSIN
537 const eventOut = trigger === TRIGGER_HOVER ?
538 this.constructor.Event.MOUSELEAVE :
539 this.constructor.Event.FOCUSOUT
540
541 EventHandler.on(this._element, eventIn, this._config.selector, event => this._enter(event))
542 EventHandler.on(this._element, eventOut, this._config.selector, event => this._leave(event))
543 }
544 })
545
546 this._hideModalHandler = () => {
547 if (this._element) {
548 this.hide()
549 }
550 }
551
552 EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler)
553
554 if (this._config.selector) {
555 this._config = {
556 ...this._config,
557 trigger: 'manual',
558 selector: ''
559 }
560 } else {
561 this._fixTitle()
562 }
563 }
564
565 _fixTitle() {
566 const title = this._element.getAttribute('title')
567 const originalTitleType = typeof this._element.getAttribute('data-bs-original-title')
568
569 if (title || originalTitleType !== 'string') {
570 this._element.setAttribute('data-bs-original-title', title || '')
571 if (title && !this._element.getAttribute('aria-label') && !this._element.textContent) {
572 this._element.setAttribute('aria-label', title)
573 }
574
575 this._element.setAttribute('title', '')
576 }
577 }
578
579 _enter(event, context) {
580 context = this._initializeOnDelegatedTarget(event, context)
581
582 if (event) {
583 context._activeTrigger[
584 event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER
585 ] = true
586 }
587
588 if (context.getTipElement().classList.contains(CLASS_NAME_SHOW) || context._hoverState === HOVER_STATE_SHOW) {
589 context._hoverState = HOVER_STATE_SHOW
590 return
591 }
592
593 clearTimeout(context._timeout)
594
595 context._hoverState = HOVER_STATE_SHOW
596
597 if (!context._config.delay || !context._config.delay.show) {
598 context.show()
599 return
600 }
601
602 context._timeout = setTimeout(() => {
603 if (context._hoverState === HOVER_STATE_SHOW) {
604 context.show()
605 }
606 }, context._config.delay.show)
607 }
608
609 _leave(event, context) {
610 context = this._initializeOnDelegatedTarget(event, context)
611
612 if (event) {
613 context._activeTrigger[
614 event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER
615 ] = context._element.contains(event.relatedTarget)
616 }
617
618 if (context._isWithActiveTrigger()) {
619 return
620 }
621
622 clearTimeout(context._timeout)
623
624 context._hoverState = HOVER_STATE_OUT
625
626 if (!context._config.delay || !context._config.delay.hide) {
627 context.hide()
628 return
629 }
630
631 context._timeout = setTimeout(() => {
632 if (context._hoverState === HOVER_STATE_OUT) {
633 context.hide()
634 }
635 }, context._config.delay.hide)
636 }
637
638 _isWithActiveTrigger() {
639 for (const trigger in this._activeTrigger) {
640 if (this._activeTrigger[trigger]) {
641 return true
642 }
643 }
644
645 return false
646 }
647
648 _getConfig(config) {
649 const dataAttributes = Manipulator.getDataAttributes(this._element)
650
651 Object.keys(dataAttributes).forEach(dataAttr => {
652 if (DISALLOWED_ATTRIBUTES.has(dataAttr)) {
653 delete dataAttributes[dataAttr]
654 }
655 })
656
657 config = {
658 ...this.constructor.Default,
659 ...dataAttributes,
660 ...(typeof config === 'object' && config ? config : {})
661 }
662
663 config.container = config.container === false ? document.body : getElement(config.container)
664
665 if (typeof config.delay === 'number') {
666 config.delay = {
667 show: config.delay,
668 hide: config.delay
669 }
670 }
671
672 if (typeof config.title === 'number') {
673 config.title = config.title.toString()
674 }
675
676 if (typeof config.content === 'number') {
677 config.content = config.content.toString()
678 }
679
680 typeCheckConfig(NAME, config, this.constructor.DefaultType)
681
682 if (config.sanitize) {
683 config.template = sanitizeHtml(config.template, config.allowList, config.sanitizeFn)
684 }
685
686 return config
687 }
688
689 _getDelegateConfig() {
690 const config = {}
691
692 for (const key in this._config) {
693 if (this.constructor.Default[key] !== this._config[key]) {
694 config[key] = this._config[key]
695 }
696 }
697
698 // In the future can be replaced with:
699 // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]])
700 // `Object.fromEntries(keysWithDifferentValues)`
701 return config
702 }
703
704 _cleanTipClass() {
705 const tip = this.getTipElement()
706 const basicClassPrefixRegex = new RegExp(`(^|\\s)${this._getBasicClassPrefix()}\\S+`, 'g')
707 const tabClass = tip.getAttribute('class').match(basicClassPrefixRegex)
708 if (tabClass !== null && tabClass.length > 0) {
709 tabClass.map(token => token.trim())
710 .forEach(tClass => tip.classList.remove(tClass))
711 }
712 }
713
714 _getBasicClassPrefix() {
715 return CLASS_PREFIX
716 }
717
718 _handlePopperPlacementChange(popperData) {
719 const { state } = popperData
720
721 if (!state) {
722 return
723 }
724
725 this.tip = state.elements.popper
726 this._cleanTipClass()
727 this._addAttachmentClass(this._getAttachment(state.placement))
728 }
729
730 _disposePopper() {
731 if (this._popper) {
732 this._popper.destroy()
733 this._popper = null
734 }
735 }
736
737 // Static
738
739 static jQueryInterface(config) {
740 return this.each(function () {
741 const data = Tooltip.getOrCreateInstance(this, config)
742
743 if (typeof config === 'string') {
744 if (typeof data[config] === 'undefined') {
745 throw new TypeError(`No method named "${config}"`)
746 }
747
748 data[config]()
749 }
750 })
751 }
752}
753
754/**
755 * ------------------------------------------------------------------------
756 * jQuery
757 * ------------------------------------------------------------------------
758 * add .Tooltip to jQuery only if jQuery is present
759 */
760
761defineJQueryPlugin(Tooltip)
762
763export default Tooltip
Note: See TracBrowser for help on using the repository browser.