]> git.ipfire.org Git - ipfire.org.git/blob - static/scss/bootstrap-4.0.0-alpha.6/js/src/tooltip.js
.gitignore: Add .vscode
[ipfire.org.git] / static / scss / bootstrap-4.0.0-alpha.6 / js / src / tooltip.js
1 /* global Tether */
2
3 import Util from './util'
4
5
6 /**
7 * --------------------------------------------------------------------------
8 * Bootstrap (v4.0.0-alpha.6): tooltip.js
9 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
10 * --------------------------------------------------------------------------
11 */
12
13 const Tooltip = (($) => {
14
15 /**
16 * Check for Tether dependency
17 * Tether - http://tether.io/
18 */
19 if (typeof Tether === 'undefined') {
20 throw new Error('Bootstrap tooltips require Tether (http://tether.io/)')
21 }
22
23
24 /**
25 * ------------------------------------------------------------------------
26 * Constants
27 * ------------------------------------------------------------------------
28 */
29
30 const NAME = 'tooltip'
31 const VERSION = '4.0.0-alpha.6'
32 const DATA_KEY = 'bs.tooltip'
33 const EVENT_KEY = `.${DATA_KEY}`
34 const JQUERY_NO_CONFLICT = $.fn[NAME]
35 const TRANSITION_DURATION = 150
36 const CLASS_PREFIX = 'bs-tether'
37
38 const Default = {
39 animation : true,
40 template : '<div class="tooltip" role="tooltip">'
41 + '<div class="tooltip-inner"></div></div>',
42 trigger : 'hover focus',
43 title : '',
44 delay : 0,
45 html : false,
46 selector : false,
47 placement : 'top',
48 offset : '0 0',
49 constraints : [],
50 container : false
51 }
52
53 const DefaultType = {
54 animation : 'boolean',
55 template : 'string',
56 title : '(string|element|function)',
57 trigger : 'string',
58 delay : '(number|object)',
59 html : 'boolean',
60 selector : '(string|boolean)',
61 placement : '(string|function)',
62 offset : 'string',
63 constraints : 'array',
64 container : '(string|element|boolean)'
65 }
66
67 const AttachmentMap = {
68 TOP : 'bottom center',
69 RIGHT : 'middle left',
70 BOTTOM : 'top center',
71 LEFT : 'middle right'
72 }
73
74 const HoverState = {
75 SHOW : 'show',
76 OUT : 'out'
77 }
78
79 const Event = {
80 HIDE : `hide${EVENT_KEY}`,
81 HIDDEN : `hidden${EVENT_KEY}`,
82 SHOW : `show${EVENT_KEY}`,
83 SHOWN : `shown${EVENT_KEY}`,
84 INSERTED : `inserted${EVENT_KEY}`,
85 CLICK : `click${EVENT_KEY}`,
86 FOCUSIN : `focusin${EVENT_KEY}`,
87 FOCUSOUT : `focusout${EVENT_KEY}`,
88 MOUSEENTER : `mouseenter${EVENT_KEY}`,
89 MOUSELEAVE : `mouseleave${EVENT_KEY}`
90 }
91
92 const ClassName = {
93 FADE : 'fade',
94 SHOW : 'show'
95 }
96
97 const Selector = {
98 TOOLTIP : '.tooltip',
99 TOOLTIP_INNER : '.tooltip-inner'
100 }
101
102 const TetherClass = {
103 element : false,
104 enabled : false
105 }
106
107 const Trigger = {
108 HOVER : 'hover',
109 FOCUS : 'focus',
110 CLICK : 'click',
111 MANUAL : 'manual'
112 }
113
114
115 /**
116 * ------------------------------------------------------------------------
117 * Class Definition
118 * ------------------------------------------------------------------------
119 */
120
121 class Tooltip {
122
123 constructor(element, config) {
124
125 // private
126 this._isEnabled = true
127 this._timeout = 0
128 this._hoverState = ''
129 this._activeTrigger = {}
130 this._isTransitioning = false
131 this._tether = null
132
133 // protected
134 this.element = element
135 this.config = this._getConfig(config)
136 this.tip = null
137
138 this._setListeners()
139
140 }
141
142
143 // getters
144
145 static get VERSION() {
146 return VERSION
147 }
148
149 static get Default() {
150 return Default
151 }
152
153 static get NAME() {
154 return NAME
155 }
156
157 static get DATA_KEY() {
158 return DATA_KEY
159 }
160
161 static get Event() {
162 return Event
163 }
164
165 static get EVENT_KEY() {
166 return EVENT_KEY
167 }
168
169 static get DefaultType() {
170 return DefaultType
171 }
172
173
174 // public
175
176 enable() {
177 this._isEnabled = true
178 }
179
180 disable() {
181 this._isEnabled = false
182 }
183
184 toggleEnabled() {
185 this._isEnabled = !this._isEnabled
186 }
187
188 toggle(event) {
189 if (event) {
190 const dataKey = this.constructor.DATA_KEY
191 let context = $(event.currentTarget).data(dataKey)
192
193 if (!context) {
194 context = new this.constructor(
195 event.currentTarget,
196 this._getDelegateConfig()
197 )
198 $(event.currentTarget).data(dataKey, context)
199 }
200
201 context._activeTrigger.click = !context._activeTrigger.click
202
203 if (context._isWithActiveTrigger()) {
204 context._enter(null, context)
205 } else {
206 context._leave(null, context)
207 }
208
209 } else {
210
211 if ($(this.getTipElement()).hasClass(ClassName.SHOW)) {
212 this._leave(null, this)
213 return
214 }
215
216 this._enter(null, this)
217 }
218 }
219
220 dispose() {
221 clearTimeout(this._timeout)
222
223 this.cleanupTether()
224
225 $.removeData(this.element, this.constructor.DATA_KEY)
226
227 $(this.element).off(this.constructor.EVENT_KEY)
228 $(this.element).closest('.modal').off('hide.bs.modal')
229
230 if (this.tip) {
231 $(this.tip).remove()
232 }
233
234 this._isEnabled = null
235 this._timeout = null
236 this._hoverState = null
237 this._activeTrigger = null
238 this._tether = null
239
240 this.element = null
241 this.config = null
242 this.tip = null
243 }
244
245 show() {
246 if ($(this.element).css('display') === 'none') {
247 throw new Error('Please use show on visible elements')
248 }
249
250 const showEvent = $.Event(this.constructor.Event.SHOW)
251 if (this.isWithContent() && this._isEnabled) {
252 if (this._isTransitioning) {
253 throw new Error('Tooltip is transitioning')
254 }
255 $(this.element).trigger(showEvent)
256
257 const isInTheDom = $.contains(
258 this.element.ownerDocument.documentElement,
259 this.element
260 )
261
262 if (showEvent.isDefaultPrevented() || !isInTheDom) {
263 return
264 }
265
266 const tip = this.getTipElement()
267 const tipId = Util.getUID(this.constructor.NAME)
268
269 tip.setAttribute('id', tipId)
270 this.element.setAttribute('aria-describedby', tipId)
271
272 this.setContent()
273
274 if (this.config.animation) {
275 $(tip).addClass(ClassName.FADE)
276 }
277
278 const placement = typeof this.config.placement === 'function' ?
279 this.config.placement.call(this, tip, this.element) :
280 this.config.placement
281
282 const attachment = this._getAttachment(placement)
283
284 const container = this.config.container === false ? document.body : $(this.config.container)
285
286 $(tip)
287 .data(this.constructor.DATA_KEY, this)
288 .appendTo(container)
289
290 $(this.element).trigger(this.constructor.Event.INSERTED)
291
292 this._tether = new Tether({
293 attachment,
294 element : tip,
295 target : this.element,
296 classes : TetherClass,
297 classPrefix : CLASS_PREFIX,
298 offset : this.config.offset,
299 constraints : this.config.constraints,
300 addTargetClasses: false
301 })
302
303 Util.reflow(tip)
304 this._tether.position()
305
306 $(tip).addClass(ClassName.SHOW)
307
308 const complete = () => {
309 const prevHoverState = this._hoverState
310 this._hoverState = null
311 this._isTransitioning = false
312
313 $(this.element).trigger(this.constructor.Event.SHOWN)
314
315 if (prevHoverState === HoverState.OUT) {
316 this._leave(null, this)
317 }
318 }
319
320 if (Util.supportsTransitionEnd() && $(this.tip).hasClass(ClassName.FADE)) {
321 this._isTransitioning = true
322 $(this.tip)
323 .one(Util.TRANSITION_END, complete)
324 .emulateTransitionEnd(Tooltip._TRANSITION_DURATION)
325 return
326 }
327
328 complete()
329 }
330 }
331
332 hide(callback) {
333 const tip = this.getTipElement()
334 const hideEvent = $.Event(this.constructor.Event.HIDE)
335 if (this._isTransitioning) {
336 throw new Error('Tooltip is transitioning')
337 }
338 const complete = () => {
339 if (this._hoverState !== HoverState.SHOW && tip.parentNode) {
340 tip.parentNode.removeChild(tip)
341 }
342
343 this.element.removeAttribute('aria-describedby')
344 $(this.element).trigger(this.constructor.Event.HIDDEN)
345 this._isTransitioning = false
346 this.cleanupTether()
347
348 if (callback) {
349 callback()
350 }
351 }
352
353 $(this.element).trigger(hideEvent)
354
355 if (hideEvent.isDefaultPrevented()) {
356 return
357 }
358
359 $(tip).removeClass(ClassName.SHOW)
360
361 this._activeTrigger[Trigger.CLICK] = false
362 this._activeTrigger[Trigger.FOCUS] = false
363 this._activeTrigger[Trigger.HOVER] = false
364
365 if (Util.supportsTransitionEnd() &&
366 $(this.tip).hasClass(ClassName.FADE)) {
367 this._isTransitioning = true
368 $(tip)
369 .one(Util.TRANSITION_END, complete)
370 .emulateTransitionEnd(TRANSITION_DURATION)
371
372 } else {
373 complete()
374 }
375
376 this._hoverState = ''
377 }
378
379
380 // protected
381
382 isWithContent() {
383 return Boolean(this.getTitle())
384 }
385
386 getTipElement() {
387 return this.tip = this.tip || $(this.config.template)[0]
388 }
389
390 setContent() {
391 const $tip = $(this.getTipElement())
392
393 this.setElementContent($tip.find(Selector.TOOLTIP_INNER), this.getTitle())
394
395 $tip.removeClass(`${ClassName.FADE} ${ClassName.SHOW}`)
396
397 this.cleanupTether()
398 }
399
400 setElementContent($element, content) {
401 const html = this.config.html
402 if (typeof content === 'object' && (content.nodeType || content.jquery)) {
403 // content is a DOM node or a jQuery
404 if (html) {
405 if (!$(content).parent().is($element)) {
406 $element.empty().append(content)
407 }
408 } else {
409 $element.text($(content).text())
410 }
411 } else {
412 $element[html ? 'html' : 'text'](content)
413 }
414 }
415
416 getTitle() {
417 let title = this.element.getAttribute('data-original-title')
418
419 if (!title) {
420 title = typeof this.config.title === 'function' ?
421 this.config.title.call(this.element) :
422 this.config.title
423 }
424
425 return title
426 }
427
428 cleanupTether() {
429 if (this._tether) {
430 this._tether.destroy()
431 }
432 }
433
434
435 // private
436
437 _getAttachment(placement) {
438 return AttachmentMap[placement.toUpperCase()]
439 }
440
441 _setListeners() {
442 const triggers = this.config.trigger.split(' ')
443
444 triggers.forEach((trigger) => {
445 if (trigger === 'click') {
446 $(this.element).on(
447 this.constructor.Event.CLICK,
448 this.config.selector,
449 (event) => this.toggle(event)
450 )
451
452 } else if (trigger !== Trigger.MANUAL) {
453 const eventIn = trigger === Trigger.HOVER ?
454 this.constructor.Event.MOUSEENTER :
455 this.constructor.Event.FOCUSIN
456 const eventOut = trigger === Trigger.HOVER ?
457 this.constructor.Event.MOUSELEAVE :
458 this.constructor.Event.FOCUSOUT
459
460 $(this.element)
461 .on(
462 eventIn,
463 this.config.selector,
464 (event) => this._enter(event)
465 )
466 .on(
467 eventOut,
468 this.config.selector,
469 (event) => this._leave(event)
470 )
471 }
472
473 $(this.element).closest('.modal').on(
474 'hide.bs.modal',
475 () => this.hide()
476 )
477 })
478
479 if (this.config.selector) {
480 this.config = $.extend({}, this.config, {
481 trigger : 'manual',
482 selector : ''
483 })
484 } else {
485 this._fixTitle()
486 }
487 }
488
489 _fixTitle() {
490 const titleType = typeof this.element.getAttribute('data-original-title')
491 if (this.element.getAttribute('title') ||
492 titleType !== 'string') {
493 this.element.setAttribute(
494 'data-original-title',
495 this.element.getAttribute('title') || ''
496 )
497 this.element.setAttribute('title', '')
498 }
499 }
500
501 _enter(event, context) {
502 const dataKey = this.constructor.DATA_KEY
503
504 context = context || $(event.currentTarget).data(dataKey)
505
506 if (!context) {
507 context = new this.constructor(
508 event.currentTarget,
509 this._getDelegateConfig()
510 )
511 $(event.currentTarget).data(dataKey, context)
512 }
513
514 if (event) {
515 context._activeTrigger[
516 event.type === 'focusin' ? Trigger.FOCUS : Trigger.HOVER
517 ] = true
518 }
519
520 if ($(context.getTipElement()).hasClass(ClassName.SHOW) ||
521 context._hoverState === HoverState.SHOW) {
522 context._hoverState = HoverState.SHOW
523 return
524 }
525
526 clearTimeout(context._timeout)
527
528 context._hoverState = HoverState.SHOW
529
530 if (!context.config.delay || !context.config.delay.show) {
531 context.show()
532 return
533 }
534
535 context._timeout = setTimeout(() => {
536 if (context._hoverState === HoverState.SHOW) {
537 context.show()
538 }
539 }, context.config.delay.show)
540 }
541
542 _leave(event, context) {
543 const dataKey = this.constructor.DATA_KEY
544
545 context = context || $(event.currentTarget).data(dataKey)
546
547 if (!context) {
548 context = new this.constructor(
549 event.currentTarget,
550 this._getDelegateConfig()
551 )
552 $(event.currentTarget).data(dataKey, context)
553 }
554
555 if (event) {
556 context._activeTrigger[
557 event.type === 'focusout' ? Trigger.FOCUS : Trigger.HOVER
558 ] = false
559 }
560
561 if (context._isWithActiveTrigger()) {
562 return
563 }
564
565 clearTimeout(context._timeout)
566
567 context._hoverState = HoverState.OUT
568
569 if (!context.config.delay || !context.config.delay.hide) {
570 context.hide()
571 return
572 }
573
574 context._timeout = setTimeout(() => {
575 if (context._hoverState === HoverState.OUT) {
576 context.hide()
577 }
578 }, context.config.delay.hide)
579 }
580
581 _isWithActiveTrigger() {
582 for (const trigger in this._activeTrigger) {
583 if (this._activeTrigger[trigger]) {
584 return true
585 }
586 }
587
588 return false
589 }
590
591 _getConfig(config) {
592 config = $.extend(
593 {},
594 this.constructor.Default,
595 $(this.element).data(),
596 config
597 )
598
599 if (config.delay && typeof config.delay === 'number') {
600 config.delay = {
601 show : config.delay,
602 hide : config.delay
603 }
604 }
605
606 Util.typeCheckConfig(
607 NAME,
608 config,
609 this.constructor.DefaultType
610 )
611
612 return config
613 }
614
615 _getDelegateConfig() {
616 const config = {}
617
618 if (this.config) {
619 for (const key in this.config) {
620 if (this.constructor.Default[key] !== this.config[key]) {
621 config[key] = this.config[key]
622 }
623 }
624 }
625
626 return config
627 }
628
629
630 // static
631
632 static _jQueryInterface(config) {
633 return this.each(function () {
634 let data = $(this).data(DATA_KEY)
635 const _config = typeof config === 'object' && config
636
637 if (!data && /dispose|hide/.test(config)) {
638 return
639 }
640
641 if (!data) {
642 data = new Tooltip(this, _config)
643 $(this).data(DATA_KEY, data)
644 }
645
646 if (typeof config === 'string') {
647 if (data[config] === undefined) {
648 throw new Error(`No method named "${config}"`)
649 }
650 data[config]()
651 }
652 })
653 }
654
655 }
656
657
658 /**
659 * ------------------------------------------------------------------------
660 * jQuery
661 * ------------------------------------------------------------------------
662 */
663
664 $.fn[NAME] = Tooltip._jQueryInterface
665 $.fn[NAME].Constructor = Tooltip
666 $.fn[NAME].noConflict = function () {
667 $.fn[NAME] = JQUERY_NO_CONFLICT
668 return Tooltip._jQueryInterface
669 }
670
671 return Tooltip
672
673 })(jQuery)
674
675 export default Tooltip