tooltip.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. /*!
  2. * Bootstrap tooltip.js v5.3.7 (https://getbootstrap.com/)
  3. * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
  4. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
  5. */
  6. (function (global, factory) {
  7. typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('@popperjs/core'), require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/manipulator.js'), require('./util/index.js'), require('./util/sanitizer.js'), require('./util/template-factory.js')) :
  8. typeof define === 'function' && define.amd ? define(['@popperjs/core', './base-component', './dom/event-handler', './dom/manipulator', './util/index', './util/sanitizer', './util/template-factory'], factory) :
  9. (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Tooltip = factory(global["@popperjs/core"], global.BaseComponent, global.EventHandler, global.Manipulator, global.Index, global.Sanitizer, global.TemplateFactory));
  10. })(this, (function (Popper, BaseComponent, EventHandler, Manipulator, index_js, sanitizer_js, TemplateFactory) { 'use strict';
  11. function _interopNamespaceDefault(e) {
  12. const n = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } });
  13. if (e) {
  14. for (const k in e) {
  15. if (k !== 'default') {
  16. const d = Object.getOwnPropertyDescriptor(e, k);
  17. Object.defineProperty(n, k, d.get ? d : {
  18. enumerable: true,
  19. get: () => e[k]
  20. });
  21. }
  22. }
  23. }
  24. n.default = e;
  25. return Object.freeze(n);
  26. }
  27. const Popper__namespace = /*#__PURE__*/_interopNamespaceDefault(Popper);
  28. /**
  29. * --------------------------------------------------------------------------
  30. * Bootstrap tooltip.js
  31. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
  32. * --------------------------------------------------------------------------
  33. */
  34. /**
  35. * Constants
  36. */
  37. const NAME = 'tooltip';
  38. const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']);
  39. const CLASS_NAME_FADE = 'fade';
  40. const CLASS_NAME_MODAL = 'modal';
  41. const CLASS_NAME_SHOW = 'show';
  42. const SELECTOR_TOOLTIP_INNER = '.tooltip-inner';
  43. const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`;
  44. const EVENT_MODAL_HIDE = 'hide.bs.modal';
  45. const TRIGGER_HOVER = 'hover';
  46. const TRIGGER_FOCUS = 'focus';
  47. const TRIGGER_CLICK = 'click';
  48. const TRIGGER_MANUAL = 'manual';
  49. const EVENT_HIDE = 'hide';
  50. const EVENT_HIDDEN = 'hidden';
  51. const EVENT_SHOW = 'show';
  52. const EVENT_SHOWN = 'shown';
  53. const EVENT_INSERTED = 'inserted';
  54. const EVENT_CLICK = 'click';
  55. const EVENT_FOCUSIN = 'focusin';
  56. const EVENT_FOCUSOUT = 'focusout';
  57. const EVENT_MOUSEENTER = 'mouseenter';
  58. const EVENT_MOUSELEAVE = 'mouseleave';
  59. const AttachmentMap = {
  60. AUTO: 'auto',
  61. TOP: 'top',
  62. RIGHT: index_js.isRTL() ? 'left' : 'right',
  63. BOTTOM: 'bottom',
  64. LEFT: index_js.isRTL() ? 'right' : 'left'
  65. };
  66. const Default = {
  67. allowList: sanitizer_js.DefaultAllowlist,
  68. animation: true,
  69. boundary: 'clippingParents',
  70. container: false,
  71. customClass: '',
  72. delay: 0,
  73. fallbackPlacements: ['top', 'right', 'bottom', 'left'],
  74. html: false,
  75. offset: [0, 6],
  76. placement: 'top',
  77. popperConfig: null,
  78. sanitize: true,
  79. sanitizeFn: null,
  80. selector: false,
  81. template: '<div class="tooltip" role="tooltip">' + '<div class="tooltip-arrow"></div>' + '<div class="tooltip-inner"></div>' + '</div>',
  82. title: '',
  83. trigger: 'hover focus'
  84. };
  85. const DefaultType = {
  86. allowList: 'object',
  87. animation: 'boolean',
  88. boundary: '(string|element)',
  89. container: '(string|element|boolean)',
  90. customClass: '(string|function)',
  91. delay: '(number|object)',
  92. fallbackPlacements: 'array',
  93. html: 'boolean',
  94. offset: '(array|string|function)',
  95. placement: '(string|function)',
  96. popperConfig: '(null|object|function)',
  97. sanitize: 'boolean',
  98. sanitizeFn: '(null|function)',
  99. selector: '(string|boolean)',
  100. template: 'string',
  101. title: '(string|element|function)',
  102. trigger: 'string'
  103. };
  104. /**
  105. * Class definition
  106. */
  107. class Tooltip extends BaseComponent {
  108. constructor(element, config) {
  109. if (typeof Popper__namespace === 'undefined') {
  110. throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org/docs/v2/)');
  111. }
  112. super(element, config);
  113. // Private
  114. this._isEnabled = true;
  115. this._timeout = 0;
  116. this._isHovered = null;
  117. this._activeTrigger = {};
  118. this._popper = null;
  119. this._templateFactory = null;
  120. this._newContent = null;
  121. // Protected
  122. this.tip = null;
  123. this._setListeners();
  124. if (!this._config.selector) {
  125. this._fixTitle();
  126. }
  127. }
  128. // Getters
  129. static get Default() {
  130. return Default;
  131. }
  132. static get DefaultType() {
  133. return DefaultType;
  134. }
  135. static get NAME() {
  136. return NAME;
  137. }
  138. // Public
  139. enable() {
  140. this._isEnabled = true;
  141. }
  142. disable() {
  143. this._isEnabled = false;
  144. }
  145. toggleEnabled() {
  146. this._isEnabled = !this._isEnabled;
  147. }
  148. toggle() {
  149. if (!this._isEnabled) {
  150. return;
  151. }
  152. if (this._isShown()) {
  153. this._leave();
  154. return;
  155. }
  156. this._enter();
  157. }
  158. dispose() {
  159. clearTimeout(this._timeout);
  160. EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);
  161. if (this._element.getAttribute('data-bs-original-title')) {
  162. this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title'));
  163. }
  164. this._disposePopper();
  165. super.dispose();
  166. }
  167. show() {
  168. if (this._element.style.display === 'none') {
  169. throw new Error('Please use show on visible elements');
  170. }
  171. if (!(this._isWithContent() && this._isEnabled)) {
  172. return;
  173. }
  174. const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW));
  175. const shadowRoot = index_js.findShadowRoot(this._element);
  176. const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element);
  177. if (showEvent.defaultPrevented || !isInTheDom) {
  178. return;
  179. }
  180. // TODO: v6 remove this or make it optional
  181. this._disposePopper();
  182. const tip = this._getTipElement();
  183. this._element.setAttribute('aria-describedby', tip.getAttribute('id'));
  184. const {
  185. container
  186. } = this._config;
  187. if (!this._element.ownerDocument.documentElement.contains(this.tip)) {
  188. container.append(tip);
  189. EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED));
  190. }
  191. this._popper = this._createPopper(tip);
  192. tip.classList.add(CLASS_NAME_SHOW);
  193. // If this is a touch-enabled device we add extra
  194. // empty mouseover listeners to the body's immediate children;
  195. // only needed because of broken event delegation on iOS
  196. // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
  197. if ('ontouchstart' in document.documentElement) {
  198. for (const element of [].concat(...document.body.children)) {
  199. EventHandler.on(element, 'mouseover', index_js.noop);
  200. }
  201. }
  202. const complete = () => {
  203. EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN));
  204. if (this._isHovered === false) {
  205. this._leave();
  206. }
  207. this._isHovered = false;
  208. };
  209. this._queueCallback(complete, this.tip, this._isAnimated());
  210. }
  211. hide() {
  212. if (!this._isShown()) {
  213. return;
  214. }
  215. const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE));
  216. if (hideEvent.defaultPrevented) {
  217. return;
  218. }
  219. const tip = this._getTipElement();
  220. tip.classList.remove(CLASS_NAME_SHOW);
  221. // If this is a touch-enabled device we remove the extra
  222. // empty mouseover listeners we added for iOS support
  223. if ('ontouchstart' in document.documentElement) {
  224. for (const element of [].concat(...document.body.children)) {
  225. EventHandler.off(element, 'mouseover', index_js.noop);
  226. }
  227. }
  228. this._activeTrigger[TRIGGER_CLICK] = false;
  229. this._activeTrigger[TRIGGER_FOCUS] = false;
  230. this._activeTrigger[TRIGGER_HOVER] = false;
  231. this._isHovered = null; // it is a trick to support manual triggering
  232. const complete = () => {
  233. if (this._isWithActiveTrigger()) {
  234. return;
  235. }
  236. if (!this._isHovered) {
  237. this._disposePopper();
  238. }
  239. this._element.removeAttribute('aria-describedby');
  240. EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN));
  241. };
  242. this._queueCallback(complete, this.tip, this._isAnimated());
  243. }
  244. update() {
  245. if (this._popper) {
  246. this._popper.update();
  247. }
  248. }
  249. // Protected
  250. _isWithContent() {
  251. return Boolean(this._getTitle());
  252. }
  253. _getTipElement() {
  254. if (!this.tip) {
  255. this.tip = this._createTipElement(this._newContent || this._getContentForTemplate());
  256. }
  257. return this.tip;
  258. }
  259. _createTipElement(content) {
  260. const tip = this._getTemplateFactory(content).toHtml();
  261. // TODO: remove this check in v6
  262. if (!tip) {
  263. return null;
  264. }
  265. tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW);
  266. // TODO: v6 the following can be achieved with CSS only
  267. tip.classList.add(`bs-${this.constructor.NAME}-auto`);
  268. const tipId = index_js.getUID(this.constructor.NAME).toString();
  269. tip.setAttribute('id', tipId);
  270. if (this._isAnimated()) {
  271. tip.classList.add(CLASS_NAME_FADE);
  272. }
  273. return tip;
  274. }
  275. setContent(content) {
  276. this._newContent = content;
  277. if (this._isShown()) {
  278. this._disposePopper();
  279. this.show();
  280. }
  281. }
  282. _getTemplateFactory(content) {
  283. if (this._templateFactory) {
  284. this._templateFactory.changeContent(content);
  285. } else {
  286. this._templateFactory = new TemplateFactory({
  287. ...this._config,
  288. // the `content` var has to be after `this._config`
  289. // to override config.content in case of popover
  290. content,
  291. extraClass: this._resolvePossibleFunction(this._config.customClass)
  292. });
  293. }
  294. return this._templateFactory;
  295. }
  296. _getContentForTemplate() {
  297. return {
  298. [SELECTOR_TOOLTIP_INNER]: this._getTitle()
  299. };
  300. }
  301. _getTitle() {
  302. return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title');
  303. }
  304. // Private
  305. _initializeOnDelegatedTarget(event) {
  306. return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig());
  307. }
  308. _isAnimated() {
  309. return this._config.animation || this.tip && this.tip.classList.contains(CLASS_NAME_FADE);
  310. }
  311. _isShown() {
  312. return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW);
  313. }
  314. _createPopper(tip) {
  315. const placement = index_js.execute(this._config.placement, [this, tip, this._element]);
  316. const attachment = AttachmentMap[placement.toUpperCase()];
  317. return Popper__namespace.createPopper(this._element, tip, this._getPopperConfig(attachment));
  318. }
  319. _getOffset() {
  320. const {
  321. offset
  322. } = this._config;
  323. if (typeof offset === 'string') {
  324. return offset.split(',').map(value => Number.parseInt(value, 10));
  325. }
  326. if (typeof offset === 'function') {
  327. return popperData => offset(popperData, this._element);
  328. }
  329. return offset;
  330. }
  331. _resolvePossibleFunction(arg) {
  332. return index_js.execute(arg, [this._element, this._element]);
  333. }
  334. _getPopperConfig(attachment) {
  335. const defaultBsPopperConfig = {
  336. placement: attachment,
  337. modifiers: [{
  338. name: 'flip',
  339. options: {
  340. fallbackPlacements: this._config.fallbackPlacements
  341. }
  342. }, {
  343. name: 'offset',
  344. options: {
  345. offset: this._getOffset()
  346. }
  347. }, {
  348. name: 'preventOverflow',
  349. options: {
  350. boundary: this._config.boundary
  351. }
  352. }, {
  353. name: 'arrow',
  354. options: {
  355. element: `.${this.constructor.NAME}-arrow`
  356. }
  357. }, {
  358. name: 'preSetPlacement',
  359. enabled: true,
  360. phase: 'beforeMain',
  361. fn: data => {
  362. // Pre-set Popper's placement attribute in order to read the arrow sizes properly.
  363. // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement
  364. this._getTipElement().setAttribute('data-popper-placement', data.state.placement);
  365. }
  366. }]
  367. };
  368. return {
  369. ...defaultBsPopperConfig,
  370. ...index_js.execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])
  371. };
  372. }
  373. _setListeners() {
  374. const triggers = this._config.trigger.split(' ');
  375. for (const trigger of triggers) {
  376. if (trigger === 'click') {
  377. EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK), this._config.selector, event => {
  378. const context = this._initializeOnDelegatedTarget(event);
  379. context._activeTrigger[TRIGGER_CLICK] = !(context._isShown() && context._activeTrigger[TRIGGER_CLICK]);
  380. context.toggle();
  381. });
  382. } else if (trigger !== TRIGGER_MANUAL) {
  383. const eventIn = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSEENTER) : this.constructor.eventName(EVENT_FOCUSIN);
  384. const eventOut = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSELEAVE) : this.constructor.eventName(EVENT_FOCUSOUT);
  385. EventHandler.on(this._element, eventIn, this._config.selector, event => {
  386. const context = this._initializeOnDelegatedTarget(event);
  387. context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true;
  388. context._enter();
  389. });
  390. EventHandler.on(this._element, eventOut, this._config.selector, event => {
  391. const context = this._initializeOnDelegatedTarget(event);
  392. context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget);
  393. context._leave();
  394. });
  395. }
  396. }
  397. this._hideModalHandler = () => {
  398. if (this._element) {
  399. this.hide();
  400. }
  401. };
  402. EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);
  403. }
  404. _fixTitle() {
  405. const title = this._element.getAttribute('title');
  406. if (!title) {
  407. return;
  408. }
  409. if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) {
  410. this._element.setAttribute('aria-label', title);
  411. }
  412. this._element.setAttribute('data-bs-original-title', title); // DO NOT USE IT. Is only for backwards compatibility
  413. this._element.removeAttribute('title');
  414. }
  415. _enter() {
  416. if (this._isShown() || this._isHovered) {
  417. this._isHovered = true;
  418. return;
  419. }
  420. this._isHovered = true;
  421. this._setTimeout(() => {
  422. if (this._isHovered) {
  423. this.show();
  424. }
  425. }, this._config.delay.show);
  426. }
  427. _leave() {
  428. if (this._isWithActiveTrigger()) {
  429. return;
  430. }
  431. this._isHovered = false;
  432. this._setTimeout(() => {
  433. if (!this._isHovered) {
  434. this.hide();
  435. }
  436. }, this._config.delay.hide);
  437. }
  438. _setTimeout(handler, timeout) {
  439. clearTimeout(this._timeout);
  440. this._timeout = setTimeout(handler, timeout);
  441. }
  442. _isWithActiveTrigger() {
  443. return Object.values(this._activeTrigger).includes(true);
  444. }
  445. _getConfig(config) {
  446. const dataAttributes = Manipulator.getDataAttributes(this._element);
  447. for (const dataAttribute of Object.keys(dataAttributes)) {
  448. if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) {
  449. delete dataAttributes[dataAttribute];
  450. }
  451. }
  452. config = {
  453. ...dataAttributes,
  454. ...(typeof config === 'object' && config ? config : {})
  455. };
  456. config = this._mergeConfigObj(config);
  457. config = this._configAfterMerge(config);
  458. this._typeCheckConfig(config);
  459. return config;
  460. }
  461. _configAfterMerge(config) {
  462. config.container = config.container === false ? document.body : index_js.getElement(config.container);
  463. if (typeof config.delay === 'number') {
  464. config.delay = {
  465. show: config.delay,
  466. hide: config.delay
  467. };
  468. }
  469. if (typeof config.title === 'number') {
  470. config.title = config.title.toString();
  471. }
  472. if (typeof config.content === 'number') {
  473. config.content = config.content.toString();
  474. }
  475. return config;
  476. }
  477. _getDelegateConfig() {
  478. const config = {};
  479. for (const [key, value] of Object.entries(this._config)) {
  480. if (this.constructor.Default[key] !== value) {
  481. config[key] = value;
  482. }
  483. }
  484. config.selector = false;
  485. config.trigger = 'manual';
  486. // In the future can be replaced with:
  487. // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]])
  488. // `Object.fromEntries(keysWithDifferentValues)`
  489. return config;
  490. }
  491. _disposePopper() {
  492. if (this._popper) {
  493. this._popper.destroy();
  494. this._popper = null;
  495. }
  496. if (this.tip) {
  497. this.tip.remove();
  498. this.tip = null;
  499. }
  500. }
  501. // Static
  502. static jQueryInterface(config) {
  503. return this.each(function () {
  504. const data = Tooltip.getOrCreateInstance(this, config);
  505. if (typeof config !== 'string') {
  506. return;
  507. }
  508. if (typeof data[config] === 'undefined') {
  509. throw new TypeError(`No method named "${config}"`);
  510. }
  511. data[config]();
  512. });
  513. }
  514. }
  515. /**
  516. * jQuery
  517. */
  518. index_js.defineJQueryPlugin(Tooltip);
  519. return Tooltip;
  520. }));
  521. //# sourceMappingURL=tooltip.js.map