Rev 1 | AutorÃa | Comparar con el anterior | Ultima modificación | Ver Log |
/*** --------------------------------------------------------------------------* Bootstrap tooltip.js* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)* --------------------------------------------------------------------------*/import * as Popper from 'core/popper2'import BaseComponent from './base-component'import EventHandler from './dom/event-handler'import Manipulator from './dom/manipulator'import {defineJQueryPlugin, execute, findShadowRoot, getElement, getUID, isRTL, noop} from './util/index'import { DefaultAllowlist } from './util/sanitizer'import TemplateFactory from './util/template-factory'/*** Constants*/const NAME = 'tooltip'const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn'])const CLASS_NAME_FADE = 'fade'const CLASS_NAME_MODAL = 'modal'const CLASS_NAME_SHOW = 'show'const SELECTOR_TOOLTIP_INNER = '.tooltip-inner'const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`const EVENT_MODAL_HIDE = 'hide.bs.modal'const TRIGGER_HOVER = 'hover'const TRIGGER_FOCUS = 'focus'const TRIGGER_CLICK = 'click'const TRIGGER_MANUAL = 'manual'const EVENT_HIDE = 'hide'const EVENT_HIDDEN = 'hidden'const EVENT_SHOW = 'show'const EVENT_SHOWN = 'shown'const EVENT_INSERTED = 'inserted'const EVENT_CLICK = 'click'const EVENT_FOCUSIN = 'focusin'const EVENT_FOCUSOUT = 'focusout'const EVENT_MOUSEENTER = 'mouseenter'const EVENT_MOUSELEAVE = 'mouseleave'const AttachmentMap = {AUTO: 'auto',TOP: 'top',RIGHT: isRTL() ? 'left' : 'right',BOTTOM: 'bottom',LEFT: isRTL() ? 'right' : 'left'}const Default = {allowList: DefaultAllowlist,animation: true,boundary: 'clippingParents',container: false,customClass: '',delay: 0,fallbackPlacements: ['top', 'right', 'bottom', 'left'],html: false,offset: [0, 6],placement: 'top',popperConfig: null,sanitize: true,sanitizeFn: null,selector: false,template: '<div class="tooltip" role="tooltip">' +'<div class="tooltip-arrow"></div>' +'<div class="tooltip-inner"></div>' +'</div>',title: '',trigger: 'hover focus'}const DefaultType = {allowList: 'object',animation: 'boolean',boundary: '(string|element)',container: '(string|element|boolean)',customClass: '(string|function)',delay: '(number|object)',fallbackPlacements: 'array',html: 'boolean',offset: '(array|string|function)',placement: '(string|function)',popperConfig: '(null|object|function)',sanitize: 'boolean',sanitizeFn: '(null|function)',selector: '(string|boolean)',template: 'string',title: '(string|element|function)',trigger: 'string'}/*** Class definition*/class Tooltip extends BaseComponent {constructor(element, config) {if (typeof Popper === 'undefined') {throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org)')}super(element, config)// Privatethis._isEnabled = truethis._timeout = 0this._isHovered = nullthis._activeTrigger = {}this._popper = nullthis._templateFactory = nullthis._newContent = null// Protectedthis.tip = nullthis._setListeners()if (!this._config.selector) {this._fixTitle()}}// Gettersstatic get Default() {return Default}static get DefaultType() {return DefaultType}static get NAME() {return NAME}// Publicenable() {this._isEnabled = true}disable() {this._isEnabled = false}toggleEnabled() {this._isEnabled = !this._isEnabled}toggle() {if (!this._isEnabled) {return}this._activeTrigger.click = !this._activeTrigger.clickif (this._isShown()) {this._leave()return}this._enter()}dispose() {clearTimeout(this._timeout)EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler)if (this._element.getAttribute('data-bs-original-title')) {this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title'))}this._disposePopper()super.dispose()}show() {if (this._element.style.display === 'none') {throw new Error('Please use show on visible elements')}if (!(this._isWithContent() && this._isEnabled)) {return}const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW))const shadowRoot = findShadowRoot(this._element)const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element)if (showEvent.defaultPrevented || !isInTheDom) {return}// TODO: v6 remove this or make it optionalthis._disposePopper()const tip = this._getTipElement()this._element.setAttribute('aria-describedby', tip.getAttribute('id'))const { container } = this._configif (!this._element.ownerDocument.documentElement.contains(this.tip)) {container.append(tip)EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED))}this._popper = this._createPopper(tip)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.htmlif ('ontouchstart' in document.documentElement) {for (const element of [].concat(...document.body.children)) {EventHandler.on(element, 'mouseover', noop)}}const complete = () => {EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN))if (this._isHovered === false) {this._leave()}this._isHovered = false}this._queueCallback(complete, this.tip, this._isAnimated())}hide() {if (!this._isShown()) {return}const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE))if (hideEvent.defaultPrevented) {return}const tip = this._getTipElement()tip.classList.remove(CLASS_NAME_SHOW)// If this is a touch-enabled device we remove the extra// empty mouseover listeners we added for iOS supportif ('ontouchstart' in document.documentElement) {for (const element of [].concat(...document.body.children)) {EventHandler.off(element, 'mouseover', noop)}}this._activeTrigger[TRIGGER_CLICK] = falsethis._activeTrigger[TRIGGER_FOCUS] = falsethis._activeTrigger[TRIGGER_HOVER] = falsethis._isHovered = null // it is a trick to support manual triggeringconst complete = () => {if (this._isWithActiveTrigger()) {return}if (!this._isHovered) {this._disposePopper()}this._element.removeAttribute('aria-describedby')EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN))}this._queueCallback(complete, this.tip, this._isAnimated())}update() {if (this._popper) {this._popper.update()}}// Protected_isWithContent() {return Boolean(this._getTitle())}_getTipElement() {if (!this.tip) {this.tip = this._createTipElement(this._newContent || this._getContentForTemplate())}return this.tip}_createTipElement(content) {const tip = this._getTemplateFactory(content).toHtml()// TODO: remove this check in v6if (!tip) {return null}tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW)// TODO: v6 the following can be achieved with CSS onlytip.classList.add(`bs-${this.constructor.NAME}-auto`)const tipId = getUID(this.constructor.NAME).toString()tip.setAttribute('id', tipId)if (this._isAnimated()) {tip.classList.add(CLASS_NAME_FADE)}return tip}setContent(content) {this._newContent = contentif (this._isShown()) {this._disposePopper()this.show()}}_getTemplateFactory(content) {if (this._templateFactory) {this._templateFactory.changeContent(content)} else {this._templateFactory = new TemplateFactory({...this._config,// the `content` var has to be after `this._config`// to override config.content in case of popovercontent,extraClass: this._resolvePossibleFunction(this._config.customClass)})}return this._templateFactory}_getContentForTemplate() {return {[SELECTOR_TOOLTIP_INNER]: this._getTitle()}}_getTitle() {return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title')}// Private_initializeOnDelegatedTarget(event) {return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig())}_isAnimated() {return this._config.animation || (this.tip && this.tip.classList.contains(CLASS_NAME_FADE))}_isShown() {return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW)}_createPopper(tip) {const placement = execute(this._config.placement, [this, tip, this._element])const attachment = AttachmentMap[placement.toUpperCase()]return Popper.createPopper(this._element, tip, this._getPopperConfig(attachment))}_getOffset() {const { offset } = this._configif (typeof offset === 'string') {return offset.split(',').map(value => Number.parseInt(value, 10))}if (typeof offset === 'function') {return popperData => offset(popperData, this._element)}return offset}_resolvePossibleFunction(arg) {return execute(arg, [this._element])}_getPopperConfig(attachment) {const defaultBsPopperConfig = {placement: attachment,modifiers: [{name: 'flip',options: {fallbackPlacements: this._config.fallbackPlacements}},{name: 'offset',options: {offset: this._getOffset()}},{name: 'preventOverflow',options: {boundary: this._config.boundary}},{name: 'arrow',options: {element: `.${this.constructor.NAME}-arrow`}},{name: 'preSetPlacement',enabled: true,phase: 'beforeMain',fn: data => {// Pre-set Popper's placement attribute in order to read the arrow sizes properly.// Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placementthis._getTipElement().setAttribute('data-popper-placement', data.state.placement)}}]}return {...defaultBsPopperConfig,...execute(this._config.popperConfig, [defaultBsPopperConfig])}}_setListeners() {const triggers = this._config.trigger.split(' ')for (const trigger of triggers) {if (trigger === 'click') {EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK), this._config.selector, event => {const context = this._initializeOnDelegatedTarget(event)context.toggle()})} else if (trigger !== TRIGGER_MANUAL) {const eventIn = trigger === TRIGGER_HOVER ?this.constructor.eventName(EVENT_MOUSEENTER) :this.constructor.eventName(EVENT_FOCUSIN)const eventOut = trigger === TRIGGER_HOVER ?this.constructor.eventName(EVENT_MOUSELEAVE) :this.constructor.eventName(EVENT_FOCUSOUT)EventHandler.on(this._element, eventIn, this._config.selector, event => {const context = this._initializeOnDelegatedTarget(event)context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = truecontext._enter()})EventHandler.on(this._element, eventOut, this._config.selector, event => {const context = this._initializeOnDelegatedTarget(event)context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] =context._element.contains(event.relatedTarget)context._leave()})}}this._hideModalHandler = () => {if (this._element) {this.hide()}}EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler)}_fixTitle() {const title = this._element.getAttribute('title')if (!title) {return}if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) {this._element.setAttribute('aria-label', title)}this._element.setAttribute('data-bs-original-title', title) // DO NOT USE IT. Is only for backwards compatibilitythis._element.removeAttribute('title')}_enter() {if (this._isShown() || this._isHovered) {this._isHovered = truereturn}this._isHovered = truethis._setTimeout(() => {if (this._isHovered) {this.show()}}, this._config.delay.show)}_leave() {if (this._isWithActiveTrigger()) {return}this._isHovered = falsethis._setTimeout(() => {if (!this._isHovered) {this.hide()}}, this._config.delay.hide)}_setTimeout(handler, timeout) {clearTimeout(this._timeout)this._timeout = setTimeout(handler, timeout)}_isWithActiveTrigger() {return Object.values(this._activeTrigger).includes(true)}_getConfig(config) {const dataAttributes = Manipulator.getDataAttributes(this._element)for (const dataAttribute of Object.keys(dataAttributes)) {if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) {delete dataAttributes[dataAttribute]}}config = {...dataAttributes,...(typeof config === 'object' && config ? config : {})}config = this._mergeConfigObj(config)config = this._configAfterMerge(config)this._typeCheckConfig(config)return config}_configAfterMerge(config) {config.container = config.container === false ? document.body : getElement(config.container)if (typeof config.delay === 'number') {config.delay = {show: config.delay,hide: config.delay}}if (typeof config.title === 'number') {config.title = config.title.toString()}if (typeof config.content === 'number') {config.content = config.content.toString()}return config}_getDelegateConfig() {const config = {}for (const [key, value] of Object.entries(this._config)) {if (this.constructor.Default[key] !== value) {config[key] = value}}config.selector = falseconfig.trigger = 'manual'// In the future can be replaced with:// const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]])// `Object.fromEntries(keysWithDifferentValues)`return config}_disposePopper() {if (this._popper) {this._popper.destroy()this._popper = null}if (this.tip) {this.tip.remove()this.tip = null}}// Staticstatic jQueryInterface(config) {return this.each(function () {const data = Tooltip.getOrCreateInstance(this, config)if (typeof config !== 'string') {return}if (typeof data[config] === 'undefined') {throw new TypeError(`No method named "${config}"`)}data[config]()})}}/*** jQuery*/defineJQueryPlugin(Tooltip)export default Tooltip