diff options
Diffstat (limited to 'js/src/tooltip.js')
| -rw-r--r-- | js/src/tooltip.js | 247 |
1 files changed, 103 insertions, 144 deletions
diff --git a/js/src/tooltip.js b/js/src/tooltip.js index d35b5e0ab..288146472 100644 --- a/js/src/tooltip.js +++ b/js/src/tooltip.js @@ -1,6 +1,6 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.0.0-beta2): tooltip.js + * Bootstrap (v5.1.0): tooltip.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ @@ -9,19 +9,15 @@ import * as Popper from '@popperjs/core' import { defineJQueryPlugin, - emulateTransitionEnd, findShadowRoot, - getTransitionDurationFromElement, + getElement, getUID, isElement, isRTL, noop, typeCheckConfig } from './util/index' -import { - DefaultAllowlist, - sanitizeHtml -} from './util/sanitizer' +import { DefaultAllowlist, sanitizeHtml } from './util/sanitizer' import Data from './dom/data' import EventHandler from './dom/event-handler' import Manipulator from './dom/manipulator' @@ -38,7 +34,6 @@ const NAME = 'tooltip' 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 = new Set(['sanitize', 'allowList', 'sanitizeFn']) const DefaultType = { @@ -113,6 +108,9 @@ const HOVER_STATE_SHOW = 'show' const HOVER_STATE_OUT = 'out' 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' @@ -141,7 +139,7 @@ class Tooltip extends BaseComponent { this._popper = null // Protected - this.config = this._getConfig(config) + this._config = this._getConfig(config) this.tip = null this._setListeners() @@ -157,18 +155,10 @@ class Tooltip extends BaseComponent { 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 } @@ -215,24 +205,16 @@ class Tooltip extends BaseComponent { dispose() { clearTimeout(this._timeout) - EventHandler.off(this._element, this.constructor.EVENT_KEY) - EventHandler.off(this._element.closest(`.${CLASS_NAME_MODAL}`), 'hide.bs.modal', this._hideModalHandler) + EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler) - if (this.tip && this.tip.parentNode) { - this.tip.parentNode.removeChild(this.tip) + if (this.tip) { + this.tip.remove() } - this._isEnabled = null - this._timeout = null - this._hoverState = null - this._activeTrigger = null if (this._popper) { this._popper.destroy() } - this._popper = null - this.config = null - this.tip = null super.dispose() } @@ -261,33 +243,34 @@ class Tooltip extends BaseComponent { tip.setAttribute('id', tipId) this._element.setAttribute('aria-describedby', tipId) - this.setContent() - - if (this.config.animation) { + 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 + const placement = typeof this._config.placement === 'function' ? + this._config.placement.call(this, tip, this._element) : + this._config.placement const attachment = this._getAttachment(placement) this._addAttachmentClass(attachment) - const container = this._getContainer() - Data.setData(tip, this.constructor.DATA_KEY, this) + const { container } = this._config + Data.set(tip, this.constructor.DATA_KEY, this) if (!this._element.ownerDocument.documentElement.contains(this.tip)) { - container.appendChild(tip) + container.append(tip) + EventHandler.trigger(this._element, this.constructor.Event.INSERTED) } - EventHandler.trigger(this._element, this.constructor.Event.INSERTED) - - this._popper = Popper.createPopper(this._element, tip, this._getPopperConfig(attachment)) + if (this._popper) { + this._popper.update() + } else { + this._popper = Popper.createPopper(this._element, tip, this._getPopperConfig(attachment)) + } tip.classList.add(CLASS_NAME_SHOW) - const customClass = typeof this.config.customClass === 'function' ? this.config.customClass() : this.config.customClass + const customClass = this._resolvePossibleFunction(this._config.customClass) if (customClass) { tip.classList.add(...customClass.split(' ')) } @@ -298,7 +281,7 @@ class Tooltip extends BaseComponent { // 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()) + EventHandler.on(element, 'mouseover', noop) }) } @@ -313,13 +296,8 @@ class Tooltip extends BaseComponent { } } - if (this.tip.classList.contains(CLASS_NAME_FADE)) { - const transitionDuration = getTransitionDurationFromElement(this.tip) - EventHandler.one(this.tip, 'transitionend', complete) - emulateTransitionEnd(this.tip, transitionDuration) - } else { - complete() - } + const isAnimated = this.tip.classList.contains(CLASS_NAME_FADE) + this._queueCallback(complete, this.tip, isAnimated) } hide() { @@ -329,8 +307,12 @@ class Tooltip extends BaseComponent { const tip = this.getTipElement() const complete = () => { - if (this._hoverState !== HOVER_STATE_SHOW && tip.parentNode) { - tip.parentNode.removeChild(tip) + if (this._isWithActiveTrigger()) { + return + } + + if (this._hoverState !== HOVER_STATE_SHOW) { + tip.remove() } this._cleanTipClass() @@ -361,15 +343,8 @@ class Tooltip extends BaseComponent { 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, 'transitionend', complete) - emulateTransitionEnd(tip, transitionDuration) - } else { - complete() - } - + const isAnimated = this.tip.classList.contains(CLASS_NAME_FADE) + this._queueCallback(complete, this.tip, isAnimated) this._hoverState = '' } @@ -391,16 +366,30 @@ class Tooltip extends BaseComponent { } const element = document.createElement('div') - element.innerHTML = this.config.template + element.innerHTML = this._config.template + + const tip = element.children[0] + this.setContent(tip) + tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW) - this.tip = element.children[0] + this.tip = tip 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) + setContent(tip) { + this._sanitizeAndSetContent(tip, this.getTitle(), SELECTOR_TOOLTIP_INNER) + } + + _sanitizeAndSetContent(template, content, selector) { + const templateElement = SelectorEngine.findOne(selector, template) + + if (!content && templateElement) { + templateElement.remove() + return + } + + // we use append for html objects to maintain js events + this.setElementContent(templateElement, content) } setElementContent(element, content) { @@ -408,16 +397,14 @@ class Tooltip extends BaseComponent { return } - if (typeof content === 'object' && isElement(content)) { - if (content.jquery) { - content = content[0] - } + if (isElement(content)) { + content = getElement(content) // content is a DOM node or a jQuery - if (this.config.html) { + if (this._config.html) { if (content.parentNode !== element) { element.innerHTML = '' - element.appendChild(content) + element.append(content) } } else { element.textContent = content.textContent @@ -426,9 +413,9 @@ class Tooltip extends BaseComponent { return } - if (this.config.html) { - if (this.config.sanitize) { - content = sanitizeHtml(content, this.config.allowList, this.config.sanitizeFn) + if (this._config.html) { + if (this._config.sanitize) { + content = sanitizeHtml(content, this._config.allowList, this._config.sanitizeFn) } element.innerHTML = content @@ -438,15 +425,9 @@ class Tooltip extends BaseComponent { } getTitle() { - let title = this._element.getAttribute('data-bs-original-title') - - if (!title) { - title = typeof this.config.title === 'function' ? - this.config.title.call(this._element) : - this.config.title - } + const title = this._element.getAttribute('data-bs-original-title') || this._config.title - return title + return this._resolvePossibleFunction(title) } updateAttachment(attachment) { @@ -464,19 +445,11 @@ class Tooltip extends BaseComponent { // Private _initializeOnDelegatedTarget(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) - } - - return context + return context || this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig()) } _getOffset() { - const { offset } = this.config + const { offset } = this._config if (typeof offset === 'string') { return offset.split(',').map(val => Number.parseInt(val, 10)) @@ -489,6 +462,10 @@ class Tooltip extends BaseComponent { return offset } + _resolvePossibleFunction(content) { + return typeof content === 'function' ? content.call(this._element) : content + } + _getPopperConfig(attachment) { const defaultBsPopperConfig = { placement: attachment, @@ -496,8 +473,7 @@ class Tooltip extends BaseComponent { { name: 'flip', options: { - altBoundary: true, - fallbackPlacements: this.config.fallbackPlacements + fallbackPlacements: this._config.fallbackPlacements } }, { @@ -509,7 +485,7 @@ class Tooltip extends BaseComponent { { name: 'preventOverflow', options: { - boundary: this.config.boundary + boundary: this._config.boundary } }, { @@ -534,24 +510,12 @@ class Tooltip extends BaseComponent { return { ...defaultBsPopperConfig, - ...(typeof this.config.popperConfig === 'function' ? this.config.popperConfig(defaultBsPopperConfig) : this.config.popperConfig) + ...(typeof this._config.popperConfig === 'function' ? this._config.popperConfig(defaultBsPopperConfig) : this._config.popperConfig) } } _addAttachmentClass(attachment) { - this.getTipElement().classList.add(`${CLASS_PREFIX}-${this.updateAttachment(attachment)}`) - } - - _getContainer() { - if (this.config.container === false) { - return document.body - } - - if (isElement(this.config.container)) { - return this.config.container - } - - return SelectorEngine.findOne(this.config.container) + this.getTipElement().classList.add(`${this._getBasicClassPrefix()}-${this.updateAttachment(attachment)}`) } _getAttachment(placement) { @@ -559,11 +523,11 @@ class Tooltip extends BaseComponent { } _setListeners() { - const triggers = this.config.trigger.split(' ') + 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)) + 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 : @@ -572,8 +536,8 @@ class Tooltip extends BaseComponent { 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)) + EventHandler.on(this._element, eventIn, this._config.selector, event => this._enter(event)) + EventHandler.on(this._element, eventOut, this._config.selector, event => this._leave(event)) } }) @@ -583,11 +547,11 @@ class Tooltip extends BaseComponent { } } - EventHandler.on(this._element.closest(`.${CLASS_NAME_MODAL}`), 'hide.bs.modal', this._hideModalHandler) + EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler) - if (this.config.selector) { - this.config = { - ...this.config, + if (this._config.selector) { + this._config = { + ...this._config, trigger: 'manual', selector: '' } @@ -628,7 +592,7 @@ class Tooltip extends BaseComponent { context._hoverState = HOVER_STATE_SHOW - if (!context.config.delay || !context.config.delay.show) { + if (!context._config.delay || !context._config.delay.show) { context.show() return } @@ -637,7 +601,7 @@ class Tooltip extends BaseComponent { if (context._hoverState === HOVER_STATE_SHOW) { context.show() } - }, context.config.delay.show) + }, context._config.delay.show) } _leave(event, context) { @@ -646,7 +610,7 @@ class Tooltip extends BaseComponent { if (event) { context._activeTrigger[ event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER - ] = false + ] = context._element.contains(event.relatedTarget) } if (context._isWithActiveTrigger()) { @@ -657,7 +621,7 @@ class Tooltip extends BaseComponent { context._hoverState = HOVER_STATE_OUT - if (!context.config.delay || !context.config.delay.hide) { + if (!context._config.delay || !context._config.delay.hide) { context.hide() return } @@ -666,7 +630,7 @@ class Tooltip extends BaseComponent { if (context._hoverState === HOVER_STATE_OUT) { context.hide() } - }, context.config.delay.hide) + }, context._config.delay.hide) } _isWithActiveTrigger() { @@ -688,16 +652,14 @@ class Tooltip extends BaseComponent { } }) - if (config && typeof config.container === 'object' && config.container.jquery) { - config.container = config.container[0] - } - config = { ...this.constructor.Default, ...dataAttributes, ...(typeof config === 'object' && config ? config : {}) } + config.container = config.container === false ? document.body : getElement(config.container) + if (typeof config.delay === 'number') { config.delay = { show: config.delay, @@ -725,26 +687,32 @@ class Tooltip extends BaseComponent { _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] - } + for (const key in this._config) { + if (this.constructor.Default[key] !== this._config[key]) { + config[key] = this._config[key] } } + // 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 } _cleanTipClass() { const tip = this.getTipElement() - const tabClass = tip.getAttribute('class').match(BSCLS_PREFIX_REGEX) + const basicClassPrefixRegex = new RegExp(`(^|\\s)${this._getBasicClassPrefix()}\\S+`, 'g') + const tabClass = tip.getAttribute('class').match(basicClassPrefixRegex) if (tabClass !== null && tabClass.length > 0) { tabClass.map(token => token.trim()) .forEach(tClass => tip.classList.remove(tClass)) } } + _getBasicClassPrefix() { + return CLASS_PREFIX + } + _handlePopperPlacementChange(popperData) { const { state } = popperData @@ -761,16 +729,7 @@ class Tooltip extends BaseComponent { static jQueryInterface(config) { 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) - } + const data = Tooltip.getOrCreateInstance(this, config) if (typeof config === 'string') { if (typeof data[config] === 'undefined') { @@ -790,6 +749,6 @@ class Tooltip extends BaseComponent { * add .Tooltip to jQuery only if jQuery is present */ -defineJQueryPlugin(NAME, Tooltip) +defineJQueryPlugin(Tooltip) export default Tooltip |
