blob: 29d76321048180f5ff9bb166d3b09170519f65b5 [file] [edit]
/*!
* Bootstrap tooltip.js v6.0.0-alpha1 (https://getbootstrap.com/)
* Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
import { arrow, autoUpdate, computePosition, flip, offset, shift } from "@floating-ui/dom";
import BaseComponent from "./base-component.js";
import EventHandler from "./dom/event-handler.js";
import Manipulator from "./dom/manipulator.js";
import { execute, findShadowRoot, getElement, getTransitionDurationFromElement, getUID, isRTL, noop } from "./util/index.js";
import { DefaultAllowlist } from "./util/sanitizer.js";
import TemplateFactory from "./util/template-factory.js";
import { createBreakpointListeners, disposeBreakpointListeners, getResponsivePlacement, parseResponsivePlacement, toFloatingOffset } from "./util/floating-ui.js";
//#region js/src/tooltip.ts
/**
* --------------------------------------------------------------------------
* Bootstrap tooltip.ts
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
/**
* Constants
*/
const NAME = "tooltip";
const DISALLOWED_ATTRIBUTES = /* @__PURE__ */ new Set([
"sanitize",
"allowList",
"sanitizeFn"
]);
const ESCAPE_KEY = "Escape";
const CLASS_NAME_MODAL = "modal";
const CLASS_NAME_SHOW = "show";
const SELECTOR_TOOLTIP_INNER = ".tooltip-inner";
const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`;
const SELECTOR_DATA_TOGGLE = "[data-bs-toggle=\"tooltip\"]";
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 EVENT_KEYDOWN = "keydown";
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",
floatingConfig: 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)",
floatingConfig: "(null|object|function)",
sanitize: "boolean",
sanitizeFn: "(null|function)",
selector: "(string|boolean)",
template: "string",
title: "(string|element|function)",
trigger: "string"
};
/**
* Class definition
*/
var Tooltip = class extends BaseComponent {
constructor(element, config) {
if (typeof computePosition === "undefined") throw new TypeError("Bootstrap's tooltips require Floating UI (https://floating-ui.com)");
super(element, config);
this._isEnabled = true;
this._timeout = 0;
this._resolveTimeout = null;
this._isHovered = null;
this._activeTrigger = {};
this._floatingCleanup = null;
this._keydownHandler = null;
this._templateFactory = null;
this._newContent = null;
this._mediaQueryListeners = [];
this._responsivePlacements = null;
this.tip = null;
this._parseResponsivePlacements();
this._setListeners();
if (!this._config.selector) this._fixTitle();
}
static get Default() {
return Default;
}
static get DefaultType() {
return DefaultType;
}
static get NAME() {
return NAME;
}
enable() {
this._isEnabled = true;
}
disable() {
this._isEnabled = false;
}
toggleEnabled() {
this._isEnabled = !this._isEnabled;
}
toggle() {
if (!this._isEnabled) return Promise.resolve();
return this._isShown() ? this._leave() : this._enter();
}
dispose() {
this._clearTimeout();
this._removeEscapeListener();
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._disposeFloating();
this._disposeMediaQueryListeners();
super.dispose();
}
async 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 isInTheDom = (findShadowRoot(this._element) || this._element.ownerDocument.documentElement).contains(this._element);
if (showEvent.defaultPrevented || !isInTheDom) {
this._isHovered = false;
return;
}
this._disposeFloating();
const tip = this._getTipElement();
this._element.setAttribute("aria-describedby", tip.getAttribute("id"));
let { container } = this._config;
const closestDialog = this._element.closest("dialog[open]");
if (closestDialog && container === document.body) container = closestDialog;
if (!this._element.ownerDocument.documentElement.contains(this.tip)) {
container.append(tip);
EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED));
}
await this._createFloating(tip);
tip.classList.add(CLASS_NAME_SHOW);
this._setEscapeListener();
if ("ontouchstart" in document.documentElement) for (const element of 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;
};
await this._queueCallback(complete, this.tip, this._isAnimated());
}
async hide() {
if (!this._isShown()) return;
if (EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE)).defaultPrevented) return;
this._removeEscapeListener();
this._getTipElement().classList.remove(CLASS_NAME_SHOW);
if ("ontouchstart" in document.documentElement) for (const element of document.body.children) EventHandler.off(element, "mouseover", noop);
this._activeTrigger[TRIGGER_CLICK] = false;
this._activeTrigger[TRIGGER_FOCUS] = false;
this._activeTrigger[TRIGGER_HOVER] = false;
this._isHovered = null;
const complete = () => {
if (this._isWithActiveTrigger()) return;
if (!this._isHovered) this._disposeFloating();
this._element.removeAttribute("aria-describedby");
EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN));
};
await this._queueCallback(complete, this.tip, this._isAnimated());
}
update() {
if (this._floatingCleanup && this.tip) this._updateFloatingPosition();
}
_isWithContent() {
return Boolean(this._getTitle()) || this._hasNewContent();
}
_hasNewContent() {
return Boolean(this._newContent) && Object.values(this._newContent).some(Boolean);
}
_getTipElement() {
if (!this.tip) this.tip = this._createTipElement(this._newContent || this._getContentForTemplate());
return this.tip;
}
_createTipElement(content) {
const tip = this._getTemplateFactory(content).toHtml();
tip.classList.remove(CLASS_NAME_SHOW);
tip.classList.add(`bs-${this.constructor.NAME}-auto`);
const tipId = getUID(this.constructor.NAME).toString();
tip.setAttribute("id", tipId);
if (!this._config.animation) tip.classList.add(this._getInstantClassName());
return tip;
}
setContent(content) {
this._newContent = content;
if (this._isShown()) {
this._disposeFloating();
this.show();
}
}
_getTemplateFactory(content) {
if (this._templateFactory) this._templateFactory.changeContent(content);
else this._templateFactory = new TemplateFactory({
...this._config,
content,
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");
}
_initializeOnDelegatedTarget(event) {
return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig());
}
_getInstantClassName() {
return `${this.constructor.NAME}-instant`;
}
_isAnimated() {
return getTransitionDurationFromElement(this.tip) > 0;
}
_isShown() {
return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW);
}
_getPlacement(tip) {
if (this._responsivePlacements) {
const placement = getResponsivePlacement(this._responsivePlacements, "top");
return AttachmentMap[placement.toUpperCase()] || placement;
}
const placement = execute(this._config.placement, [
this,
tip,
this._element
]);
return AttachmentMap[placement.toUpperCase()] || placement;
}
_parseResponsivePlacements() {
if (typeof this._config.placement !== "string") {
this._responsivePlacements = null;
return;
}
this._responsivePlacements = parseResponsivePlacement(this._config.placement, "top");
if (this._responsivePlacements) this._setupMediaQueryListeners();
}
_setupMediaQueryListeners() {
this._disposeMediaQueryListeners();
this._mediaQueryListeners = createBreakpointListeners(() => {
if (this._isShown()) this._updateFloatingPosition();
});
}
_disposeMediaQueryListeners() {
disposeBreakpointListeners(this._mediaQueryListeners);
this._mediaQueryListeners = [];
}
async _createFloating(tip) {
const placement = this._getPlacement(tip);
const arrowElement = tip.querySelector(`.${this.constructor.NAME}-arrow`);
await this._updateFloatingPosition(tip, placement, arrowElement);
this._floatingCleanup = autoUpdate(this._element, tip, () => this._updateFloatingPosition(tip, null, arrowElement));
}
async _updateFloatingPosition(tip = this.tip, placement = null, arrowElement = null) {
if (!tip) return;
if (!placement) placement = this._getPlacement(tip);
if (!arrowElement) arrowElement = tip.querySelector(`.${this.constructor.NAME}-arrow`);
const middleware = this._getFloatingMiddleware(arrowElement);
const floatingConfig = this._getFloatingConfig(placement, middleware);
const { x, y, placement: finalPlacement, middlewareData } = await computePosition(this._element, tip, floatingConfig);
Object.assign(tip.style, {
position: "absolute",
left: `${x}px`,
top: `${y}px`
});
if (arrowElement) arrowElement.style.position = "absolute";
Manipulator.setDataAttribute(tip, "placement", finalPlacement);
if (arrowElement && middlewareData.arrow) {
const { x: arrowX, y: arrowY } = middlewareData.arrow;
const isVertical = finalPlacement.startsWith("top") || finalPlacement.startsWith("bottom");
Object.assign(arrowElement.style, {
left: isVertical && arrowX !== void 0 ? `${arrowX}px` : "",
top: !isVertical && arrowY !== void 0 ? `${arrowY}px` : "",
right: "",
bottom: ""
});
}
}
_getOffset() {
const { offset } = this._config;
if (typeof offset === "string") return offset.split(",").map((value) => Number.parseInt(value, 10));
if (typeof offset === "function") return ({ placement, rects }) => {
return toFloatingOffset(offset({
placement,
reference: rects.reference,
floating: rects.floating
}, this._element));
};
return offset;
}
_resolvePossibleFunction(arg) {
return execute(arg, [this._element, this._element]);
}
_getFloatingMiddleware(arrowElement) {
const offsetValue = this._getOffset();
const middleware = [
offset(typeof offsetValue === "function" ? offsetValue : toFloatingOffset(offsetValue)),
flip({ fallbackPlacements: this._config.fallbackPlacements }),
shift({ boundary: this._config.boundary === "clippingParents" ? "clippingAncestors" : this._config.boundary })
];
if (arrowElement) middleware.push(arrow({ element: arrowElement }));
return middleware;
}
_getFloatingConfig(placement, middleware) {
const defaultConfig = {
placement,
middleware
};
return {
...defaultConfig,
...execute(this._config.floatingConfig, [void 0, defaultConfig])
};
}
_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._activeTrigger[TRIGGER_CLICK] = !(context._isShown() && context._activeTrigger[TRIGGER_CLICK]);
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] = true;
context._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);
}
_setEscapeListener() {
if (this._keydownHandler) return;
this._keydownHandler = (event) => {
if (event.key !== ESCAPE_KEY || !this._isShown() || !this.tip.isConnected) return;
event.preventDefault();
event.stopPropagation();
this.hide();
};
this._element.ownerDocument.addEventListener(EVENT_KEYDOWN, this._keydownHandler, true);
}
_removeEscapeListener() {
if (!this._keydownHandler) return;
this._element.ownerDocument.removeEventListener(EVENT_KEYDOWN, this._keydownHandler, true);
this._keydownHandler = null;
}
_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);
this._element.removeAttribute("title");
}
_enter() {
if (this._isShown() || this._isHovered) {
this._isHovered = true;
return Promise.resolve();
}
this._isHovered = true;
return this._setTimeout(() => this._isHovered ? this.show() : void 0, this._config.delay.show);
}
_leave() {
if (this._isWithActiveTrigger()) return Promise.resolve();
this._isHovered = false;
return this._setTimeout(() => this._isHovered ? void 0 : this.hide(), this._config.delay.hide);
}
_setTimeout(handler, timeout) {
this._clearTimeout();
return new Promise((resolve) => {
this._resolveTimeout = resolve;
this._timeout = setTimeout(() => {
this._resolveTimeout = null;
resolve(handler());
}, timeout);
});
}
_clearTimeout() {
clearTimeout(this._timeout);
if (this._resolveTimeout) {
this._resolveTimeout();
this._resolveTimeout = null;
}
}
_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" || typeof config.title === "boolean") config.title = config.title.toString();
if (typeof config.content === "number" || typeof config.content === "boolean") 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 = false;
config.trigger = "manual";
return config;
}
_disposeFloating() {
if (this._floatingCleanup) {
this._floatingCleanup();
this._floatingCleanup = null;
}
if (this.tip) {
this.tip.remove();
this.tip = null;
}
}
};
/**
* Data API implementation - auto-initialize tooltips
*/
const initTooltip = (event) => {
const target = event.target.closest(SELECTOR_DATA_TOGGLE);
if (!target) return;
Tooltip.getOrCreateInstance(target);
};
EventHandler.on(document, EVENT_FOCUSIN, SELECTOR_DATA_TOGGLE, initTooltip);
EventHandler.on(document, EVENT_MOUSEENTER, SELECTOR_DATA_TOGGLE, initTooltip);
//#endregion
export { Tooltip as default };
//# sourceMappingURL=tooltip.js.map