]>
Commit | Line | Data |
---|---|---|
91e44d91 S |
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 |