You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

546 lines
18 KiB

5 months ago
  1. /*!
  2. * Bootstrap tooltip.js v5.3.3 (https://getbootstrap.com/)
  3. * Copyright 2011-2024 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)');
  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. this._activeTrigger.click = !this._activeTrigger.click;
  153. if (this._isShown()) {
  154. this._leave();
  155. return;
  156. }
  157. this._enter();
  158. }
  159. dispose() {
  160. clearTimeout(this._timeout);
  161. EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);
  162. if (this._element.getAttribute('data-bs-original-title')) {
  163. this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title'));
  164. }
  165. this._disposePopper();
  166. super.dispose();
  167. }
  168. show() {
  169. if (this._element.style.display === 'none') {
  170. throw new Error('Please use show on visible elements');
  171. }
  172. if (!(this._isWithContent() && this._isEnabled)) {
  173. return;
  174. }
  175. const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW));
  176. const shadowRoot = index_js.findShadowRoot(this._element);
  177. const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element);
  178. if (showEvent.defaultPrevented || !isInTheDom) {
  179. return;
  180. }
  181. // TODO: v6 remove this or make it optional
  182. this._disposePopper();
  183. const tip = this._getTipElement();
  184. this._element.setAttribute('aria-describedby', tip.getAttribute('id'));
  185. const {
  186. container
  187. } = this._config;
  188. if (!this._element.ownerDocument.documentElement.contains(this.tip)) {
  189. container.append(tip);
  190. EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED));
  191. }
  192. this._popper = this._createPopper(tip);
  193. tip.classList.add(CLASS_NAME_SHOW);
  194. // If this is a touch-enabled device we add extra
  195. // empty mouseover listeners to the body's immediate children;
  196. // only needed because of broken event delegation on iOS
  197. // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
  198. if ('ontouchstart' in document.documentElement) {
  199. for (const element of [].concat(...document.body.children)) {
  200. EventHandler.on(element, 'mouseover', index_js.noop);
  201. }
  202. }
  203. const complete = () => {
  204. EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN));
  205. if (this._isHovered === false) {
  206. this._leave();
  207. }
  208. this._isHovered = false;
  209. };
  210. this._queueCallback(complete, this.tip, this._isAnimated());
  211. }
  212. hide() {
  213. if (!this._isShown()) {
  214. return;
  215. }
  216. const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE));
  217. if (hideEvent.defaultPrevented) {
  218. return;
  219. }
  220. const tip = this._getTipElement();
  221. tip.classList.remove(CLASS_NAME_SHOW);
  222. // If this is a touch-enabled device we remove the extra
  223. // empty mouseover listeners we added for iOS support
  224. if ('ontouchstart' in document.documentElement) {
  225. for (const element of [].concat(...document.body.children)) {
  226. EventHandler.off(element, 'mouseover', index_js.noop);
  227. }
  228. }
  229. this._activeTrigger[TRIGGER_CLICK] = false;
  230. this._activeTrigger[TRIGGER_FOCUS] = false;
  231. this._activeTrigger[TRIGGER_HOVER] = false;
  232. this._isHovered = null; // it is a trick to support manual triggering
  233. const complete = () => {
  234. if (this._isWithActiveTrigger()) {
  235. return;
  236. }
  237. if (!this._isHovered) {
  238. this._disposePopper();
  239. }
  240. this._element.removeAttribute('aria-describedby');
  241. EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN));
  242. };
  243. this._queueCallback(complete, this.tip, this._isAnimated());
  244. }
  245. update() {
  246. if (this._popper) {
  247. this._popper.update();
  248. }
  249. }
  250. // Protected
  251. _isWithContent() {
  252. return Boolean(this._getTitle());
  253. }
  254. _getTipElement() {
  255. if (!this.tip) {
  256. this.tip = this._createTipElement(this._newContent || this._getContentForTemplate());
  257. }
  258. return this.tip;
  259. }
  260. _createTipElement(content) {
  261. const tip = this._getTemplateFactory(content).toHtml();
  262. // TODO: remove this check in v6
  263. if (!tip) {
  264. return null;
  265. }
  266. tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW);
  267. // TODO: v6 the following can be achieved with CSS only
  268. tip.classList.add(`bs-${this.constructor.NAME}-auto`);
  269. const tipId = index_js.getUID(this.constructor.NAME).toString();
  270. tip.setAttribute('id', tipId);
  271. if (this._isAnimated()) {
  272. tip.classList.add(CLASS_NAME_FADE);
  273. }
  274. return tip;
  275. }
  276. setContent(content) {
  277. this._newContent = content;
  278. if (this._isShown()) {
  279. this._disposePopper();
  280. this.show();
  281. }
  282. }
  283. _getTemplateFactory(content) {
  284. if (this._templateFactory) {
  285. this._templateFactory.changeContent(content);
  286. } else {
  287. this._templateFactory = new TemplateFactory({
  288. ...this._config,
  289. // the `content` var has to be after `this._config`
  290. // to override config.content in case of popover
  291. content,
  292. extraClass: this._resolvePossibleFunction(this._config.customClass)
  293. });
  294. }
  295. return this._templateFactory;
  296. }
  297. _getContentForTemplate() {
  298. return {
  299. [SELECTOR_TOOLTIP_INNER]: this._getTitle()
  300. };
  301. }
  302. _getTitle() {
  303. return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title');
  304. }
  305. // Private
  306. _initializeOnDelegatedTarget(event) {
  307. return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig());
  308. }
  309. _isAnimated() {
  310. return this._config.animation || this.tip && this.tip.classList.contains(CLASS_NAME_FADE);
  311. }
  312. _isShown() {
  313. return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW);
  314. }
  315. _createPopper(tip) {
  316. const placement = index_js.execute(this._config.placement, [this, tip, this._element]);
  317. const attachment = AttachmentMap[placement.toUpperCase()];
  318. return Popper__namespace.createPopper(this._element, tip, this._getPopperConfig(attachment));
  319. }
  320. _getOffset() {
  321. const {
  322. offset
  323. } = this._config;
  324. if (typeof offset === 'string') {
  325. return offset.split(',').map(value => Number.parseInt(value, 10));
  326. }
  327. if (typeof offset === 'function') {
  328. return popperData => offset(popperData, this._element);
  329. }
  330. return offset;
  331. }
  332. _resolvePossibleFunction(arg) {
  333. return index_js.execute(arg, [this._element]);
  334. }
  335. _getPopperConfig(attachment) {
  336. const defaultBsPopperConfig = {
  337. placement: attachment,
  338. modifiers: [{
  339. name: 'flip',
  340. options: {
  341. fallbackPlacements: this._config.fallbackPlacements
  342. }
  343. }, {
  344. name: 'offset',
  345. options: {
  346. offset: this._getOffset()
  347. }
  348. }, {
  349. name: 'preventOverflow',
  350. options: {
  351. boundary: this._config.boundary
  352. }
  353. }, {
  354. name: 'arrow',
  355. options: {
  356. element: `.${this.constructor.NAME}-arrow`
  357. }
  358. }, {
  359. name: 'preSetPlacement',
  360. enabled: true,
  361. phase: 'beforeMain',
  362. fn: data => {
  363. // Pre-set Popper's placement attribute in order to read the arrow sizes properly.
  364. // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement
  365. this._getTipElement().setAttribute('data-popper-placement', data.state.placement);
  366. }
  367. }]
  368. };
  369. return {
  370. ...defaultBsPopperConfig,
  371. ...index_js.execute(this._config.popperConfig, [defaultBsPopperConfig])
  372. };
  373. }
  374. _setListeners() {
  375. const triggers = this._config.trigger.split(' ');
  376. for (const trigger of triggers) {
  377. if (trigger === 'click') {
  378. EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK), this._config.selector, event => {
  379. const context = this._initializeOnDelegatedTarget(event);
  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