tooltip.js 19.54 KiB
/**
 * --------------------------------------------------------------------------
 * Bootstrap (v4.3.1): tooltip.js
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 * --------------------------------------------------------------------------
 */
import {
  getjQuery,
  TRANSITION_END,
  emulateTransitionEnd,
  findShadowRoot,
  getTransitionDurationFromElement,
  getUID,
  isElement,
  noop,
  typeCheckConfig
} from './util/index'
import {
  DefaultWhitelist,
  sanitizeHtml
} from './util/sanitizer'
import Data from './dom/data'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
import Popper from 'popper.js'
import SelectorEngine from './dom/selector-engine'
/**
 * ------------------------------------------------------------------------
 * Constants
 * ------------------------------------------------------------------------
const NAME = 'tooltip'
const VERSION = '4.3.1'
const DATA_KEY = 'bs.tooltip'
const EVENT_KEY = `.${DATA_KEY}`
const CLASS_PREFIX = 'bs-tooltip'
const BSCLS_PREFIX_REGEX = new RegExp(`(^|\\s)${CLASS_PREFIX}\\S+`, 'g')
const DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn']
const DefaultType = {
  animation: 'boolean',
  template: 'string',
  title: '(string|element|function)',
  trigger: 'string',
  delay: '(number|object)',
  html: 'boolean',
  selector: '(string|boolean)',
  placement: '(string|function)',
  offset: '(number|string|function)',
  container: '(string|element|boolean)',
  fallbackPlacement: '(string|array)',
  boundary: '(string|element)',
  sanitize: 'boolean',
  sanitizeFn: '(null|function)',
  whiteList: 'object',
  popperConfig: '(null|object)'
const AttachmentMap = {
  AUTO: 'auto',
  TOP: 'top',
  RIGHT: 'right',
  BOTTOM: 'bottom',
  LEFT: 'left'
const Default = {
7172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
animation: true, template: '<div class="tooltip" role="tooltip">' + '<div class="tooltip-arrow"></div>' + '<div class="tooltip-inner"></div></div>', trigger: 'hover focus', title: '', delay: 0, html: false, selector: false, placement: 'top', offset: 0, container: false, fallbackPlacement: 'flip', boundary: 'scrollParent', sanitize: true, sanitizeFn: null, whiteList: DefaultWhitelist, popperConfig: null } const Event = { HIDE: `hide${EVENT_KEY}`, HIDDEN: `hidden${EVENT_KEY}`, SHOW: `show${EVENT_KEY}`, SHOWN: `shown${EVENT_KEY}`, INSERTED: `inserted${EVENT_KEY}`, CLICK: `click${EVENT_KEY}`, FOCUSIN: `focusin${EVENT_KEY}`, FOCUSOUT: `focusout${EVENT_KEY}`, MOUSEENTER: `mouseenter${EVENT_KEY}`, MOUSELEAVE: `mouseleave${EVENT_KEY}` } const CLASS_NAME_FADE = 'fade' const CLASS_NAME_MODAL = 'modal' const CLASS_NAME_SHOW = 'show' const HOVER_STATE_SHOW = 'show' const HOVER_STATE_OUT = 'out' const SELECTOR_TOOLTIP_INNER = '.tooltip-inner' const TRIGGER_HOVER = 'hover' const TRIGGER_FOCUS = 'focus' const TRIGGER_CLICK = 'click' const TRIGGER_MANUAL = 'manual' /** * ------------------------------------------------------------------------ * Class Definition * ------------------------------------------------------------------------ */ class Tooltip { constructor(element, config) { if (typeof Popper === 'undefined') { throw new TypeError('Bootstrap\'s tooltips require Popper.js (https://popper.js.org)') } // private this._isEnabled = true this._timeout = 0 this._hoverState = '' this._activeTrigger = {} this._popper = null // Protected this.element = element this.config = this._getConfig(config) this.tip = null
141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
this._setListeners() Data.setData(element, this.constructor.DATA_KEY, this) } // Getters static get VERSION() { return VERSION } static get Default() { return Default } static get NAME() { return NAME } static get DATA_KEY() { return DATA_KEY } static get Event() { return Event } static get EVENT_KEY() { return EVENT_KEY } static get DefaultType() { return DefaultType } // Public enable() { this._isEnabled = true } disable() { this._isEnabled = false } toggleEnabled() { this._isEnabled = !this._isEnabled } toggle(event) { if (!this._isEnabled) { return } if (event) { const dataKey = this.constructor.DATA_KEY let context = Data.getData(event.delegateTarget, dataKey) if (!context) { context = new this.constructor( event.delegateTarget, this._getDelegateConfig() ) Data.setData(event.delegateTarget, dataKey, context) } context._activeTrigger.click = !context._activeTrigger.click if (context._isWithActiveTrigger()) { context._enter(null, context)
211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
} else { context._leave(null, context) } } else { if (this.getTipElement().classList.contains(CLASS_NAME_SHOW)) { this._leave(null, this) return } this._enter(null, this) } } dispose() { clearTimeout(this._timeout) Data.removeData(this.element, this.constructor.DATA_KEY) EventHandler.off(this.element, this.constructor.EVENT_KEY) EventHandler.off(SelectorEngine.closest(this.element, `.${CLASS_NAME_MODAL}`), 'hide.bs.modal', this._hideModalHandler) if (this.tip) { this.tip.parentNode.removeChild(this.tip) } this._isEnabled = null this._timeout = null this._hoverState = null this._activeTrigger = null if (this._popper) { this._popper.destroy() } this._popper = null this.element = null this.config = null this.tip = null } show() { if (this.element.style.display === 'none') { throw new Error('Please use show on visible elements') } if (this.isWithContent() && this._isEnabled) { const showEvent = EventHandler.trigger(this.element, this.constructor.Event.SHOW) const shadowRoot = findShadowRoot(this.element) const isInTheDom = shadowRoot === null ? this.element.ownerDocument.documentElement.contains(this.element) : shadowRoot.contains(this.element) if (showEvent.defaultPrevented || !isInTheDom) { return } const tip = this.getTipElement() const tipId = getUID(this.constructor.NAME) tip.setAttribute('id', tipId) this.element.setAttribute('aria-describedby', tipId) this.setContent() if (this.config.animation) { tip.classList.add(CLASS_NAME_FADE) } const placement = typeof this.config.placement === 'function' ? this.config.placement.call(this, tip, this.element) : this.config.placement
281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
const attachment = this._getAttachment(placement) this._addAttachmentClass(attachment) const container = this._getContainer() Data.setData(tip, this.constructor.DATA_KEY, this) if (!this.element.ownerDocument.documentElement.contains(this.tip)) { container.appendChild(tip) } EventHandler.trigger(this.element, this.constructor.Event.INSERTED) this._popper = new Popper(this.element, tip, this._getPopperConfig(attachment)) tip.classList.add(CLASS_NAME_SHOW) // If this is a touch-enabled device we add extra // empty mouseover listeners to the body's immediate children; // only needed because of broken event delegation on iOS // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html if ('ontouchstart' in document.documentElement) { [].concat(...document.body.children).forEach(element => { EventHandler.on(element, 'mouseover', noop()) }) } const complete = () => { if (this.config.animation) { this._fixTransition() } const prevHoverState = this._hoverState this._hoverState = null EventHandler.trigger(this.element, this.constructor.Event.SHOWN) if (prevHoverState === HOVER_STATE_OUT) { this._leave(null, this) } } if (this.tip.classList.contains(CLASS_NAME_FADE)) { const transitionDuration = getTransitionDurationFromElement(this.tip) EventHandler.one(this.tip, TRANSITION_END, complete) emulateTransitionEnd(this.tip, transitionDuration) } else { complete() } } } hide() { const tip = this.getTipElement() const complete = () => { if (this._hoverState !== HOVER_STATE_SHOW && tip.parentNode) { tip.parentNode.removeChild(tip) } this._cleanTipClass() this.element.removeAttribute('aria-describedby') EventHandler.trigger(this.element, this.constructor.Event.HIDDEN) this._popper.destroy() } const hideEvent = EventHandler.trigger(this.element, this.constructor.Event.HIDE) if (hideEvent.defaultPrevented) { return }
351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
tip.classList.remove(CLASS_NAME_SHOW) // If this is a touch-enabled device we remove the extra // empty mouseover listeners we added for iOS support if ('ontouchstart' in document.documentElement) { [].concat(...document.body.children) .forEach(element => EventHandler.off(element, 'mouseover', noop)) } this._activeTrigger[TRIGGER_CLICK] = false this._activeTrigger[TRIGGER_FOCUS] = false this._activeTrigger[TRIGGER_HOVER] = false if (this.tip.classList.contains(CLASS_NAME_FADE)) { const transitionDuration = getTransitionDurationFromElement(tip) EventHandler.one(tip, TRANSITION_END, complete) emulateTransitionEnd(tip, transitionDuration) } else { complete() } this._hoverState = '' } update() { if (this._popper !== null) { this._popper.scheduleUpdate() } } // Protected isWithContent() { return Boolean(this.getTitle()) } getTipElement() { if (this.tip) { return this.tip } const element = document.createElement('div') element.innerHTML = this.config.template this.tip = element.children[0] return this.tip } setContent() { const tip = this.getTipElement() this.setElementContent(SelectorEngine.findOne(SELECTOR_TOOLTIP_INNER, tip), this.getTitle()) tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW) } setElementContent(element, content) { if (element === null) { return } if (typeof content === 'object' && isElement(content)) { if (content.jquery) { content = content[0] } // content is a DOM node or a jQuery if (this.config.html) { if (content.parentNode !== element) { element.innerHTML = '' element.appendChild(content)
421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
} } else { element.textContent = content.textContent } return } if (this.config.html) { if (this.config.sanitize) { content = sanitizeHtml(content, this.config.whiteList, this.config.sanitizeFn) } element.innerHTML = content } else { element.textContent = content } } getTitle() { let title = this.element.getAttribute('data-original-title') if (!title) { title = typeof this.config.title === 'function' ? this.config.title.call(this.element) : this.config.title } return title } // Private _getPopperConfig(attachment) { const defaultBsConfig = { placement: attachment, modifiers: { offset: this._getOffset(), flip: { behavior: this.config.fallbackPlacement }, arrow: { element: `.${this.constructor.NAME}-arrow` }, preventOverflow: { boundariesElement: this.config.boundary } }, onCreate: data => { if (data.originalPlacement !== data.placement) { this._handlePopperPlacementChange(data) } }, onUpdate: data => this._handlePopperPlacementChange(data) } return { ...defaultBsConfig, ...this.config.popperConfig } } _addAttachmentClass(attachment) { this.getTipElement().classList.add(`${CLASS_PREFIX}-${attachment}`) } _getOffset() { const offset = {} if (typeof this.config.offset === 'function') {
491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
offset.fn = data => { data.offsets = { ...data.offsets, ...this.config.offset(data.offsets, this.element) || {} } return data } } else { offset.offset = this.config.offset } return offset } _getContainer() { if (this.config.container === false) { return document.body } if (isElement(this.config.container)) { return this.config.container } return SelectorEngine.findOne(this.config.container) } _getAttachment(placement) { return AttachmentMap[placement.toUpperCase()] } _setListeners() { const triggers = this.config.trigger.split(' ') triggers.forEach(trigger => { if (trigger === 'click') { EventHandler.on(this.element, this.constructor.Event.CLICK, this.config.selector, event => this.toggle(event) ) } else if (trigger !== TRIGGER_MANUAL) { const eventIn = trigger === TRIGGER_HOVER ? this.constructor.Event.MOUSEENTER : this.constructor.Event.FOCUSIN const eventOut = trigger === TRIGGER_HOVER ? this.constructor.Event.MOUSELEAVE : this.constructor.Event.FOCUSOUT EventHandler.on(this.element, eventIn, this.config.selector, event => this._enter(event) ) EventHandler.on(this.element, eventOut, this.config.selector, event => this._leave(event) ) } }) this._hideModalHandler = () => { if (this.element) { this.hide() } } EventHandler.on(SelectorEngine.closest(this.element, `.${CLASS_NAME_MODAL}`), 'hide.bs.modal',
561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
this._hideModalHandler ) if (this.config.selector) { this.config = { ...this.config, trigger: 'manual', selector: '' } } else { this._fixTitle() } } _fixTitle() { const titleType = typeof this.element.getAttribute('data-original-title') if (this.element.getAttribute('title') || titleType !== 'string') { this.element.setAttribute( 'data-original-title', this.element.getAttribute('title') || '' ) this.element.setAttribute('title', '') } } _enter(event, context) { const dataKey = this.constructor.DATA_KEY context = context || Data.getData(event.delegateTarget, dataKey) if (!context) { context = new this.constructor( event.delegateTarget, this._getDelegateConfig() ) Data.setData(event.delegateTarget, dataKey, context) } if (event) { context._activeTrigger[ event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER ] = true } if (context.getTipElement().classList.contains(CLASS_NAME_SHOW) || context._hoverState === HOVER_STATE_SHOW) { context._hoverState = HOVER_STATE_SHOW return } clearTimeout(context._timeout) context._hoverState = HOVER_STATE_SHOW if (!context.config.delay || !context.config.delay.show) { context.show() return } context._timeout = setTimeout(() => { if (context._hoverState === HOVER_STATE_SHOW) { context.show() } }, context.config.delay.show) } _leave(event, context) { const dataKey = this.constructor.DATA_KEY context = context || Data.getData(event.delegateTarget, dataKey)
631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700
if (!context) { context = new this.constructor( event.delegateTarget, this._getDelegateConfig() ) Data.setData(event.delegateTarget, dataKey, context) } if (event) { context._activeTrigger[ event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER ] = false } if (context._isWithActiveTrigger()) { return } clearTimeout(context._timeout) context._hoverState = HOVER_STATE_OUT if (!context.config.delay || !context.config.delay.hide) { context.hide() return } context._timeout = setTimeout(() => { if (context._hoverState === HOVER_STATE_OUT) { context.hide() } }, context.config.delay.hide) } _isWithActiveTrigger() { for (const trigger in this._activeTrigger) { if (this._activeTrigger[trigger]) { return true } } return false } _getConfig(config) { const dataAttributes = Manipulator.getDataAttributes(this.element) Object.keys(dataAttributes) .forEach(dataAttr => { if (DISALLOWED_ATTRIBUTES.indexOf(dataAttr) !== -1) { delete dataAttributes[dataAttr] } }) if (config && typeof config.container === 'object' && config.container.jquery) { config.container = config.container[0] } config = { ...this.constructor.Default, ...dataAttributes, ...typeof config === 'object' && config ? config : {} } if (typeof config.delay === 'number') { config.delay = { show: config.delay, hide: config.delay }
701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770
} if (typeof config.title === 'number') { config.title = config.title.toString() } if (typeof config.content === 'number') { config.content = config.content.toString() } typeCheckConfig( NAME, config, this.constructor.DefaultType ) if (config.sanitize) { config.template = sanitizeHtml(config.template, config.whiteList, config.sanitizeFn) } return config } _getDelegateConfig() { const config = {} if (this.config) { for (const key in this.config) { if (this.constructor.Default[key] !== this.config[key]) { config[key] = this.config[key] } } } return config } _cleanTipClass() { const tip = this.getTipElement() const tabClass = tip.getAttribute('class').match(BSCLS_PREFIX_REGEX) if (tabClass !== null && tabClass.length > 0) { tabClass.map(token => token.trim()) .forEach(tClass => tip.classList.remove(tClass)) } } _handlePopperPlacementChange(popperData) { const popperInstance = popperData.instance this.tip = popperInstance.popper this._cleanTipClass() this._addAttachmentClass(this._getAttachment(popperData.placement)) } _fixTransition() { const tip = this.getTipElement() const initConfigAnimation = this.config.animation if (tip.getAttribute('x-placement') !== null) { return } tip.classList.remove(CLASS_NAME_FADE) this.config.animation = false this.hide() this.show() this.config.animation = initConfigAnimation } // Static static jQueryInterface(config) {
771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818
return this.each(function () { let data = Data.getData(this, DATA_KEY) const _config = typeof config === 'object' && config if (!data && /dispose|hide/.test(config)) { return } if (!data) { data = new Tooltip(this, _config) } if (typeof config === 'string') { if (typeof data[config] === 'undefined') { throw new TypeError(`No method named "${config}"`) } data[config]() } }) } static getInstance(element) { return Data.getData(element, DATA_KEY) } } const $ = getjQuery() /** * ------------------------------------------------------------------------ * jQuery * ------------------------------------------------------------------------ * add .tooltip to jQuery only if jQuery is present */ /* istanbul ignore if */ if ($) { const JQUERY_NO_CONFLICT = $.fn[NAME] $.fn[NAME] = Tooltip.jQueryInterface $.fn[NAME].Constructor = Tooltip $.fn[NAME].noConflict = () => { $.fn[NAME] = JQUERY_NO_CONFLICT return Tooltip.jQueryInterface } } export default Tooltip