source: imaps-frontend/node_modules/bootstrap/js/src/tooltip.js@ d565449

main
Last change on this file since d565449 was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

  • Property mode set to 100644
File size: 15.7 KB
RevLine 
[d565449]1/**
2 * --------------------------------------------------------------------------
3 * Bootstrap tooltip.js
4 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
5 * --------------------------------------------------------------------------
6 */
7
8import * as Popper from '@popperjs/core'
9import BaseComponent from './base-component.js'
10import EventHandler from './dom/event-handler.js'
11import Manipulator from './dom/manipulator.js'
12import {
13 defineJQueryPlugin, execute, findShadowRoot, getElement, getUID, isRTL, noop
14} from './util/index.js'
15import { DefaultAllowlist } from './util/sanitizer.js'
16import TemplateFactory from './util/template-factory.js'
17
18/**
19 * Constants
20 */
21
22const NAME = 'tooltip'
23const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn'])
24
25const CLASS_NAME_FADE = 'fade'
26const CLASS_NAME_MODAL = 'modal'
27const CLASS_NAME_SHOW = 'show'
28
29const SELECTOR_TOOLTIP_INNER = '.tooltip-inner'
30const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`
31
32const EVENT_MODAL_HIDE = 'hide.bs.modal'
33
34const TRIGGER_HOVER = 'hover'
35const TRIGGER_FOCUS = 'focus'
36const TRIGGER_CLICK = 'click'
37const TRIGGER_MANUAL = 'manual'
38
39const EVENT_HIDE = 'hide'
40const EVENT_HIDDEN = 'hidden'
41const EVENT_SHOW = 'show'
42const EVENT_SHOWN = 'shown'
43const EVENT_INSERTED = 'inserted'
44const EVENT_CLICK = 'click'
45const EVENT_FOCUSIN = 'focusin'
46const EVENT_FOCUSOUT = 'focusout'
47const EVENT_MOUSEENTER = 'mouseenter'
48const EVENT_MOUSELEAVE = 'mouseleave'
49
50const AttachmentMap = {
51 AUTO: 'auto',
52 TOP: 'top',
53 RIGHT: isRTL() ? 'left' : 'right',
54 BOTTOM: 'bottom',
55 LEFT: isRTL() ? 'right' : 'left'
56}
57
58const Default = {
59 allowList: DefaultAllowlist,
60 animation: true,
61 boundary: 'clippingParents',
62 container: false,
63 customClass: '',
64 delay: 0,
65 fallbackPlacements: ['top', 'right', 'bottom', 'left'],
66 html: false,
67 offset: [0, 6],
68 placement: 'top',
69 popperConfig: null,
70 sanitize: true,
71 sanitizeFn: null,
72 selector: false,
73 template: '<div class="tooltip" role="tooltip">' +
74 '<div class="tooltip-arrow"></div>' +
75 '<div class="tooltip-inner"></div>' +
76 '</div>',
77 title: '',
78 trigger: 'hover focus'
79}
80
81const DefaultType = {
82 allowList: 'object',
83 animation: 'boolean',
84 boundary: '(string|element)',
85 container: '(string|element|boolean)',
86 customClass: '(string|function)',
87 delay: '(number|object)',
88 fallbackPlacements: 'array',
89 html: 'boolean',
90 offset: '(array|string|function)',
91 placement: '(string|function)',
92 popperConfig: '(null|object|function)',
93 sanitize: 'boolean',
94 sanitizeFn: '(null|function)',
95 selector: '(string|boolean)',
96 template: 'string',
97 title: '(string|element|function)',
98 trigger: 'string'
99}
100
101/**
102 * Class definition
103 */
104
105class Tooltip extends BaseComponent {
106 constructor(element, config) {
107 if (typeof Popper === 'undefined') {
108 throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org)')
109 }
110
111 super(element, config)
112
113 // Private
114 this._isEnabled = true
115 this._timeout = 0
116 this._isHovered = null
117 this._activeTrigger = {}
118 this._popper = null
119 this._templateFactory = null
120 this._newContent = null
121
122 // Protected
123 this.tip = null
124
125 this._setListeners()
126
127 if (!this._config.selector) {
128 this._fixTitle()
129 }
130 }
131
132 // Getters
133 static get Default() {
134 return Default
135 }
136
137 static get DefaultType() {
138 return DefaultType
139 }
140
141 static get NAME() {
142 return NAME
143 }
144
145 // Public
146 enable() {
147 this._isEnabled = true
148 }
149
150 disable() {
151 this._isEnabled = false
152 }
153
154 toggleEnabled() {
155 this._isEnabled = !this._isEnabled
156 }
157
158 toggle() {
159 if (!this._isEnabled) {
160 return
161 }
162
163 this._activeTrigger.click = !this._activeTrigger.click
164 if (this._isShown()) {
165 this._leave()
166 return
167 }
168
169 this._enter()
170 }
171
172 dispose() {
173 clearTimeout(this._timeout)
174
175 EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler)
176
177 if (this._element.getAttribute('data-bs-original-title')) {
178 this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title'))
179 }
180
181 this._disposePopper()
182 super.dispose()
183 }
184
185 show() {
186 if (this._element.style.display === 'none') {
187 throw new Error('Please use show on visible elements')
188 }
189
190 if (!(this._isWithContent() && this._isEnabled)) {
191 return
192 }
193
194 const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW))
195 const shadowRoot = findShadowRoot(this._element)
196 const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element)
197
198 if (showEvent.defaultPrevented || !isInTheDom) {
199 return
200 }
201
202 // TODO: v6 remove this or make it optional
203 this._disposePopper()
204
205 const tip = this._getTipElement()
206
207 this._element.setAttribute('aria-describedby', tip.getAttribute('id'))
208
209 const { container } = this._config
210
211 if (!this._element.ownerDocument.documentElement.contains(this.tip)) {
212 container.append(tip)
213 EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED))
214 }
215
216 this._popper = this._createPopper(tip)
217
218 tip.classList.add(CLASS_NAME_SHOW)
219
220 // If this is a touch-enabled device we add extra
221 // empty mouseover listeners to the body's immediate children;
222 // only needed because of broken event delegation on iOS
223 // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
224 if ('ontouchstart' in document.documentElement) {
225 for (const element of [].concat(...document.body.children)) {
226 EventHandler.on(element, 'mouseover', noop)
227 }
228 }
229
230 const complete = () => {
231 EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN))
232
233 if (this._isHovered === false) {
234 this._leave()
235 }
236
237 this._isHovered = false
238 }
239
240 this._queueCallback(complete, this.tip, this._isAnimated())
241 }
242
243 hide() {
244 if (!this._isShown()) {
245 return
246 }
247
248 const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE))
249 if (hideEvent.defaultPrevented) {
250 return
251 }
252
253 const tip = this._getTipElement()
254 tip.classList.remove(CLASS_NAME_SHOW)
255
256 // If this is a touch-enabled device we remove the extra
257 // empty mouseover listeners we added for iOS support
258 if ('ontouchstart' in document.documentElement) {
259 for (const element of [].concat(...document.body.children)) {
260 EventHandler.off(element, 'mouseover', noop)
261 }
262 }
263
264 this._activeTrigger[TRIGGER_CLICK] = false
265 this._activeTrigger[TRIGGER_FOCUS] = false
266 this._activeTrigger[TRIGGER_HOVER] = false
267 this._isHovered = null // it is a trick to support manual triggering
268
269 const complete = () => {
270 if (this._isWithActiveTrigger()) {
271 return
272 }
273
274 if (!this._isHovered) {
275 this._disposePopper()
276 }
277
278 this._element.removeAttribute('aria-describedby')
279 EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN))
280 }
281
282 this._queueCallback(complete, this.tip, this._isAnimated())
283 }
284
285 update() {
286 if (this._popper) {
287 this._popper.update()
288 }
289 }
290
291 // Protected
292 _isWithContent() {
293 return Boolean(this._getTitle())
294 }
295
296 _getTipElement() {
297 if (!this.tip) {
298 this.tip = this._createTipElement(this._newContent || this._getContentForTemplate())
299 }
300
301 return this.tip
302 }
303
304 _createTipElement(content) {
305 const tip = this._getTemplateFactory(content).toHtml()
306
307 // TODO: remove this check in v6
308 if (!tip) {
309 return null
310 }
311
312 tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW)
313 // TODO: v6 the following can be achieved with CSS only
314 tip.classList.add(`bs-${this.constructor.NAME}-auto`)
315
316 const tipId = getUID(this.constructor.NAME).toString()
317
318 tip.setAttribute('id', tipId)
319
320 if (this._isAnimated()) {
321 tip.classList.add(CLASS_NAME_FADE)
322 }
323
324 return tip
325 }
326
327 setContent(content) {
328 this._newContent = content
329 if (this._isShown()) {
330 this._disposePopper()
331 this.show()
332 }
333 }
334
335 _getTemplateFactory(content) {
336 if (this._templateFactory) {
337 this._templateFactory.changeContent(content)
338 } else {
339 this._templateFactory = new TemplateFactory({
340 ...this._config,
341 // the `content` var has to be after `this._config`
342 // to override config.content in case of popover
343 content,
344 extraClass: this._resolvePossibleFunction(this._config.customClass)
345 })
346 }
347
348 return this._templateFactory
349 }
350
351 _getContentForTemplate() {
352 return {
353 [SELECTOR_TOOLTIP_INNER]: this._getTitle()
354 }
355 }
356
357 _getTitle() {
358 return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title')
359 }
360
361 // Private
362 _initializeOnDelegatedTarget(event) {
363 return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig())
364 }
365
366 _isAnimated() {
367 return this._config.animation || (this.tip && this.tip.classList.contains(CLASS_NAME_FADE))
368 }
369
370 _isShown() {
371 return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW)
372 }
373
374 _createPopper(tip) {
375 const placement = execute(this._config.placement, [this, tip, this._element])
376 const attachment = AttachmentMap[placement.toUpperCase()]
377 return Popper.createPopper(this._element, tip, this._getPopperConfig(attachment))
378 }
379
380 _getOffset() {
381 const { offset } = this._config
382
383 if (typeof offset === 'string') {
384 return offset.split(',').map(value => Number.parseInt(value, 10))
385 }
386
387 if (typeof offset === 'function') {
388 return popperData => offset(popperData, this._element)
389 }
390
391 return offset
392 }
393
394 _resolvePossibleFunction(arg) {
395 return execute(arg, [this._element])
396 }
397
398 _getPopperConfig(attachment) {
399 const defaultBsPopperConfig = {
400 placement: attachment,
401 modifiers: [
402 {
403 name: 'flip',
404 options: {
405 fallbackPlacements: this._config.fallbackPlacements
406 }
407 },
408 {
409 name: 'offset',
410 options: {
411 offset: this._getOffset()
412 }
413 },
414 {
415 name: 'preventOverflow',
416 options: {
417 boundary: this._config.boundary
418 }
419 },
420 {
421 name: 'arrow',
422 options: {
423 element: `.${this.constructor.NAME}-arrow`
424 }
425 },
426 {
427 name: 'preSetPlacement',
428 enabled: true,
429 phase: 'beforeMain',
430 fn: data => {
431 // Pre-set Popper's placement attribute in order to read the arrow sizes properly.
432 // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement
433 this._getTipElement().setAttribute('data-popper-placement', data.state.placement)
434 }
435 }
436 ]
437 }
438
439 return {
440 ...defaultBsPopperConfig,
441 ...execute(this._config.popperConfig, [defaultBsPopperConfig])
442 }
443 }
444
445 _setListeners() {
446 const triggers = this._config.trigger.split(' ')
447
448 for (const trigger of triggers) {
449 if (trigger === 'click') {
450 EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK), this._config.selector, event => {
451 const context = this._initializeOnDelegatedTarget(event)
452 context.toggle()
453 })
454 } else if (trigger !== TRIGGER_MANUAL) {
455 const eventIn = trigger === TRIGGER_HOVER ?
456 this.constructor.eventName(EVENT_MOUSEENTER) :
457 this.constructor.eventName(EVENT_FOCUSIN)
458 const eventOut = trigger === TRIGGER_HOVER ?
459 this.constructor.eventName(EVENT_MOUSELEAVE) :
460 this.constructor.eventName(EVENT_FOCUSOUT)
461
462 EventHandler.on(this._element, eventIn, this._config.selector, event => {
463 const context = this._initializeOnDelegatedTarget(event)
464 context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true
465 context._enter()
466 })
467 EventHandler.on(this._element, eventOut, this._config.selector, event => {
468 const context = this._initializeOnDelegatedTarget(event)
469 context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] =
470 context._element.contains(event.relatedTarget)
471
472 context._leave()
473 })
474 }
475 }
476
477 this._hideModalHandler = () => {
478 if (this._element) {
479 this.hide()
480 }
481 }
482
483 EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler)
484 }
485
486 _fixTitle() {
487 const title = this._element.getAttribute('title')
488
489 if (!title) {
490 return
491 }
492
493 if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) {
494 this._element.setAttribute('aria-label', title)
495 }
496
497 this._element.setAttribute('data-bs-original-title', title) // DO NOT USE IT. Is only for backwards compatibility
498 this._element.removeAttribute('title')
499 }
500
501 _enter() {
502 if (this._isShown() || this._isHovered) {
503 this._isHovered = true
504 return
505 }
506
507 this._isHovered = true
508
509 this._setTimeout(() => {
510 if (this._isHovered) {
511 this.show()
512 }
513 }, this._config.delay.show)
514 }
515
516 _leave() {
517 if (this._isWithActiveTrigger()) {
518 return
519 }
520
521 this._isHovered = false
522
523 this._setTimeout(() => {
524 if (!this._isHovered) {
525 this.hide()
526 }
527 }, this._config.delay.hide)
528 }
529
530 _setTimeout(handler, timeout) {
531 clearTimeout(this._timeout)
532 this._timeout = setTimeout(handler, timeout)
533 }
534
535 _isWithActiveTrigger() {
536 return Object.values(this._activeTrigger).includes(true)
537 }
538
539 _getConfig(config) {
540 const dataAttributes = Manipulator.getDataAttributes(this._element)
541
542 for (const dataAttribute of Object.keys(dataAttributes)) {
543 if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) {
544 delete dataAttributes[dataAttribute]
545 }
546 }
547
548 config = {
549 ...dataAttributes,
550 ...(typeof config === 'object' && config ? config : {})
551 }
552 config = this._mergeConfigObj(config)
553 config = this._configAfterMerge(config)
554 this._typeCheckConfig(config)
555 return config
556 }
557
558 _configAfterMerge(config) {
559 config.container = config.container === false ? document.body : getElement(config.container)
560
561 if (typeof config.delay === 'number') {
562 config.delay = {
563 show: config.delay,
564 hide: config.delay
565 }
566 }
567
568 if (typeof config.title === 'number') {
569 config.title = config.title.toString()
570 }
571
572 if (typeof config.content === 'number') {
573 config.content = config.content.toString()
574 }
575
576 return config
577 }
578
579 _getDelegateConfig() {
580 const config = {}
581
582 for (const [key, value] of Object.entries(this._config)) {
583 if (this.constructor.Default[key] !== value) {
584 config[key] = value
585 }
586 }
587
588 config.selector = false
589 config.trigger = 'manual'
590
591 // In the future can be replaced with:
592 // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]])
593 // `Object.fromEntries(keysWithDifferentValues)`
594 return config
595 }
596
597 _disposePopper() {
598 if (this._popper) {
599 this._popper.destroy()
600 this._popper = null
601 }
602
603 if (this.tip) {
604 this.tip.remove()
605 this.tip = null
606 }
607 }
608
609 // Static
610 static jQueryInterface(config) {
611 return this.each(function () {
612 const data = Tooltip.getOrCreateInstance(this, config)
613
614 if (typeof config !== 'string') {
615 return
616 }
617
618 if (typeof data[config] === 'undefined') {
619 throw new TypeError(`No method named "${config}"`)
620 }
621
622 data[config]()
623 })
624 }
625}
626
627/**
628 * jQuery
629 */
630
631defineJQueryPlugin(Tooltip)
632
633export default Tooltip
Note: See TracBrowser for help on using the repository browser.