codekingpro/portable-devtools
114k
1/**2 * --------------------------------------------------------------------------3 * Bootstrap (v4.6.2): tooltip.js4 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)5 * --------------------------------------------------------------------------6 */7 8import { DefaultWhitelist, sanitizeHtml } from './tools/sanitizer'9import $ from 'jquery'10import Popper from 'popper.js'11import Util from './util'12 13/**14 * Constants15 */16 17const NAME = 'tooltip'18const VERSION = '4.6.2'19const DATA_KEY = 'bs.tooltip'20const EVENT_KEY = `.${DATA_KEY}`21const JQUERY_NO_CONFLICT = $.fn[NAME]22const CLASS_PREFIX = 'bs-tooltip'23const BSCLS_PREFIX_REGEX = new RegExp(`(^|\\s)${CLASS_PREFIX}\\S+`, 'g')24const DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn']25 26const CLASS_NAME_FADE = 'fade'27const CLASS_NAME_SHOW = 'show'28 29const HOVER_STATE_SHOW = 'show'30const HOVER_STATE_OUT = 'out'31 32const SELECTOR_TOOLTIP_INNER = '.tooltip-inner'33const SELECTOR_ARROW = '.arrow'34 35const TRIGGER_HOVER = 'hover'36const TRIGGER_FOCUS = 'focus'37const TRIGGER_CLICK = 'click'38const TRIGGER_MANUAL = 'manual'39 40const AttachmentMap = {41 AUTO: 'auto',42 TOP: 'top',43 RIGHT: 'right',44 BOTTOM: 'bottom',45 LEFT: 'left'46}47 48const 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: null68}69 70const 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 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 103/**104 * Class definition105 */106 107class 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 // Private114 this._isEnabled = true115 this._timeout = 0116 this._hoverState = ''117 this._activeTrigger = {}118 this._popper = null119 120 // Protected121 this.element = element122 this.config = this._getConfig(config)123 this.tip = null124 125 this._setListeners()126 }127 128 // Getters129 static get VERSION() {130 return VERSION131 }132 133 static get Default() {134 return Default135 }136 137 static get NAME() {138 return NAME139 }140 141 static get DATA_KEY() {142 return DATA_KEY143 }144 145 static get Event() {146 return Event147 }148 149 static get EVENT_KEY() {150 return EVENT_KEY151 }152 153 static get DefaultType() {154 return DefaultType155 }156 157 // Public158 enable() {159 this._isEnabled = true160 }161 162 disable() {163 this._isEnabled = false164 }165 166 toggleEnabled() {167 this._isEnabled = !this._isEnabled168 }169 170 toggle(event) {171 if (!this._isEnabled) {172 return173 }174 175 if (event) {176 const dataKey = this.constructor.DATA_KEY177 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.click188 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 return198 }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 = null217 this._timeout = null218 this._hoverState = null219 this._activeTrigger = null220 if (this._popper) {221 this._popper.destroy()222 }223 224 this._popper = null225 this.element = null226 this.config = null227 this.tip = null228 }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.element243 )244 245 if (showEvent.isDefaultPrevented() || !isInTheDom) {246 return247 }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.placement264 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 extra283 // empty mouseover listeners to the body's immediate children;284 // only needed because of broken event delegation on iOS285 // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html286 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._hoverState296 this._hoverState = null297 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 return341 }342 343 $(tip).removeClass(CLASS_NAME_SHOW)344 345 // If this is a touch-enabled device we remove the extra346 // empty mouseover listeners we added for iOS support347 if ('ontouchstart' in document.documentElement) {348 $(document.body).children().off('mouseover', null, $.noop)349 }350 351 this._activeTrigger[TRIGGER_CLICK] = false352 this._activeTrigger[TRIGGER_FOCUS] = false353 this._activeTrigger[TRIGGER_HOVER] = false354 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 // Protected375 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.tip386 }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 jQuery397 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 return406 }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.title426 }427 428 return title429 }430 431 // Private432 _getPopperConfig(attachment) {433 const defaultBsConfig = {434 placement: attachment,435 modifiers: {436 offset: this._getOffset(),437 flip: {438 behavior: this.config.fallbackPlacement439 },440 arrow: {441 element: SELECTOR_ARROW442 },443 preventOverflow: {444 boundariesElement: this.config.boundary445 }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.popperConfig458 }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 data472 }473 } else {474 offset.offset = this.config.offset475 }476 477 return offset478 }479 480 _getContainer() {481 if (this.config.container === false) {482 return document.body483 }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.FOCUSIN510 const eventOut = trigger === TRIGGER_HOVER ?511 this.constructor.Event.MOUSELEAVE :512 this.constructor.Event.FOCUSOUT513 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_KEY554 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_HOVER567 ] = true568 }569 570 if ($(context.getTipElement()).hasClass(CLASS_NAME_SHOW) || context._hoverState === HOVER_STATE_SHOW) {571 context._hoverState = HOVER_STATE_SHOW572 return573 }574 575 clearTimeout(context._timeout)576 577 context._hoverState = HOVER_STATE_SHOW578 579 if (!context.config.delay || !context.config.delay.show) {580 context.show()581 return582 }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_KEY593 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_HOVER606 ] = false607 }608 609 if (context._isWithActiveTrigger()) {610 return611 }612 613 clearTimeout(context._timeout)614 615 context._hoverState = HOVER_STATE_OUT616 617 if (!context.config.delay || !context.config.delay.hide) {618 context.hide()619 return620 }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 true633 }634 }635 636 return false637 }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.delay659 }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.DefaultType674 )675 676 if (config.sanitize) {677 config.template = sanitizeHtml(config.template, config.whiteList, config.sanitizeFn)678 }679 680 return config681 }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 config695 }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.popper707 this._cleanTipClass()708 this.addAttachmentClass(this._getAttachment(popperData.placement))709 }710 711 _fixTransition() {712 const tip = this.getTipElement()713 const initConfigAnimation = this.config.animation714 715 if (tip.getAttribute('x-placement') !== null) {716 return717 }718 719 $(tip).removeClass(CLASS_NAME_FADE)720 this.config.animation = false721 this.hide()722 this.show()723 this.config.animation = initConfigAnimation724 }725 726 // Static727 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' && config732 733 if (!data && /dispose|hide/.test(config)) {734 return735 }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 * jQuery755 */756 757$.fn[NAME] = Tooltip._jQueryInterface758$.fn[NAME].Constructor = Tooltip759$.fn[NAME].noConflict = () => {760 $.fn[NAME] = JQUERY_NO_CONFLICT761 return Tooltip._jQueryInterface762}763 764export default Tooltip765 