An unfinished system to manage all your paper documentation in an easy way.
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.

tooltip.js 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. /**!
  2. * @fileOverview Kickass library to create and place poppers near their reference elements.
  3. * @version 1.3.2
  4. * @license
  5. * Copyright (c) 2016 Federico Zivolo and contributors
  6. *
  7. * Permission is hereby granted, free of charge, to any person obtaining a copy
  8. * of this software and associated documentation files (the "Software"), to deal
  9. * in the Software without restriction, including without limitation the rights
  10. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. * copies of the Software, and to permit persons to whom the Software is
  12. * furnished to do so, subject to the following conditions:
  13. *
  14. * The above copyright notice and this permission notice shall be included in all
  15. * copies or substantial portions of the Software.
  16. *
  17. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  23. * SOFTWARE.
  24. */
  25. import Popper from 'popper.js';
  26. /**
  27. * Check if the given variable is a function
  28. * @method
  29. * @memberof Popper.Utils
  30. * @argument {Any} functionToCheck - variable to check
  31. * @returns {Boolean} answer to: is a function?
  32. */
  33. function isFunction(functionToCheck) {
  34. const getType = {};
  35. return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
  36. }
  37. var _extends = Object.assign || function (target) {
  38. for (var i = 1; i < arguments.length; i++) {
  39. var source = arguments[i];
  40. for (var key in source) {
  41. if (Object.prototype.hasOwnProperty.call(source, key)) {
  42. target[key] = source[key];
  43. }
  44. }
  45. }
  46. return target;
  47. };
  48. const DEFAULT_OPTIONS = {
  49. container: false,
  50. delay: 0,
  51. html: false,
  52. placement: 'top',
  53. title: '',
  54. template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
  55. trigger: 'hover focus',
  56. offset: 0,
  57. arrowSelector: '.tooltip-arrow, .tooltip__arrow',
  58. innerSelector: '.tooltip-inner, .tooltip__inner'
  59. };
  60. class Tooltip {
  61. /**
  62. * Create a new Tooltip.js instance
  63. * @class Tooltip
  64. * @param {HTMLElement} reference - The DOM node used as reference of the tooltip (it can be a jQuery element).
  65. * @param {Object} options
  66. * @param {String} options.placement='top'
  67. * Placement of the popper accepted values: `top(-start, -end), right(-start, -end), bottom(-start, -end),
  68. * left(-start, -end)`
  69. * @param {String} options.arrowSelector='.tooltip-arrow, .tooltip__arrow' - className used to locate the DOM arrow element in the tooltip.
  70. * @param {String} options.innerSelector='.tooltip-inner, .tooltip__inner' - className used to locate the DOM inner element in the tooltip.
  71. * @param {HTMLElement|String|false} options.container=false - Append the tooltip to a specific element.
  72. * @param {Number|Object} options.delay=0
  73. * Delay showing and hiding the tooltip (ms) - does not apply to manual trigger type.
  74. * If a number is supplied, delay is applied to both hide/show.
  75. * Object structure is: `{ show: 500, hide: 100 }`
  76. * @param {Boolean} options.html=false - Insert HTML into the tooltip. If false, the content will inserted with `textContent`.
  77. * @param {String} [options.template='<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>']
  78. * Base HTML to used when creating the tooltip.
  79. * The tooltip's `title` will be injected into the `.tooltip-inner` or `.tooltip__inner`.
  80. * `.tooltip-arrow` or `.tooltip__arrow` will become the tooltip's arrow.
  81. * The outermost wrapper element should have the `.tooltip` class.
  82. * @param {String|HTMLElement|TitleFunction} options.title='' - Default title value if `title` attribute isn't present.
  83. * @param {String} [options.trigger='hover focus']
  84. * How tooltip is triggered - click, hover, focus, manual.
  85. * You may pass multiple triggers; separate them with a space. `manual` cannot be combined with any other trigger.
  86. * @param {Boolean} options.closeOnClickOutside=false - Close a popper on click outside of the popper and reference element. This has effect only when options.trigger is 'click'.
  87. * @param {String|HTMLElement} options.boundariesElement
  88. * The element used as boundaries for the tooltip. For more information refer to Popper.js'
  89. * [boundariesElement docs](https://popper.js.org/popper-documentation.html)
  90. * @param {Number|String} options.offset=0 - Offset of the tooltip relative to its reference. For more information refer to Popper.js'
  91. * [offset docs](https://popper.js.org/popper-documentation.html)
  92. * @param {Object} options.popperOptions={} - Popper options, will be passed directly to popper instance. For more information refer to Popper.js'
  93. * [options docs](https://popper.js.org/popper-documentation.html)
  94. * @return {Object} instance - The generated tooltip instance
  95. */
  96. constructor(reference, options) {
  97. _initialiseProps.call(this);
  98. // apply user options over default ones
  99. options = _extends({}, DEFAULT_OPTIONS, options);
  100. reference.jquery && (reference = reference[0]);
  101. // cache reference and options
  102. this.reference = reference;
  103. this.options = options;
  104. // get events list
  105. const events = typeof options.trigger === 'string' ? options.trigger.split(' ').filter(trigger => ['click', 'hover', 'focus'].indexOf(trigger) !== -1) : [];
  106. // set initial state
  107. this._isOpen = false;
  108. this._popperOptions = {};
  109. // set event listeners
  110. this._setEventListeners(reference, events, options);
  111. }
  112. //
  113. // Public methods
  114. //
  115. /**
  116. * Reveals an element's tooltip. This is considered a "manual" triggering of the tooltip.
  117. * Tooltips with zero-length titles are never displayed.
  118. * @method Tooltip#show
  119. * @memberof Tooltip
  120. */
  121. /**
  122. * Hides an element’s tooltip. This is considered a “manual” triggering of the tooltip.
  123. * @method Tooltip#hide
  124. * @memberof Tooltip
  125. */
  126. /**
  127. * Hides and destroys an element’s tooltip.
  128. * @method Tooltip#dispose
  129. * @memberof Tooltip
  130. */
  131. /**
  132. * Toggles an element’s tooltip. This is considered a “manual” triggering of the tooltip.
  133. * @method Tooltip#toggle
  134. * @memberof Tooltip
  135. */
  136. /**
  137. * Updates the tooltip's title content
  138. * @method Tooltip#updateTitleContent
  139. * @memberof Tooltip
  140. * @param {String|HTMLElement} title - The new content to use for the title
  141. */
  142. //
  143. // Private methods
  144. //
  145. /**
  146. * Creates a new tooltip node
  147. * @memberof Tooltip
  148. * @private
  149. * @param {HTMLElement} reference
  150. * @param {String} template
  151. * @param {String|HTMLElement|TitleFunction} title
  152. * @param {Boolean} allowHtml
  153. * @return {HTMLElement} tooltipNode
  154. */
  155. _create(reference, template, title, allowHtml) {
  156. // create tooltip element
  157. const tooltipGenerator = window.document.createElement('div');
  158. tooltipGenerator.innerHTML = template.trim();
  159. const tooltipNode = tooltipGenerator.childNodes[0];
  160. // add unique ID to our tooltip (needed for accessibility reasons)
  161. tooltipNode.id = `tooltip_${Math.random().toString(36).substr(2, 10)}`;
  162. // set initial `aria-hidden` state to `false` (it's visible!)
  163. tooltipNode.setAttribute('aria-hidden', 'false');
  164. // add title to tooltip
  165. const titleNode = tooltipGenerator.querySelector(this.options.innerSelector);
  166. this._addTitleContent(reference, title, allowHtml, titleNode);
  167. // return the generated tooltip node
  168. return tooltipNode;
  169. }
  170. _addTitleContent(reference, title, allowHtml, titleNode) {
  171. if (title.nodeType === 1 || title.nodeType === 11) {
  172. // if title is a element node or document fragment, append it only if allowHtml is true
  173. allowHtml && titleNode.appendChild(title);
  174. } else if (isFunction(title)) {
  175. // if title is a function, call it and set textContent or innerHtml depending by `allowHtml` value
  176. const titleText = title.call(reference);
  177. allowHtml ? titleNode.innerHTML = titleText : titleNode.textContent = titleText;
  178. } else {
  179. // if it's just a simple text, set textContent or innerHtml depending by `allowHtml` value
  180. allowHtml ? titleNode.innerHTML = title : titleNode.textContent = title;
  181. }
  182. }
  183. _show(reference, options) {
  184. // don't show if it's already visible
  185. // or if it's not being showed
  186. if (this._isOpen && !this._isOpening) {
  187. return this;
  188. }
  189. this._isOpen = true;
  190. // if the tooltipNode already exists, just show it
  191. if (this._tooltipNode) {
  192. this._tooltipNode.style.visibility = 'visible';
  193. this._tooltipNode.setAttribute('aria-hidden', 'false');
  194. this.popperInstance.update();
  195. return this;
  196. }
  197. // get title
  198. const title = reference.getAttribute('title') || options.title;
  199. // don't show tooltip if no title is defined
  200. if (!title) {
  201. return this;
  202. }
  203. // create tooltip node
  204. const tooltipNode = this._create(reference, options.template, title, options.html);
  205. // Add `aria-describedby` to our reference element for accessibility reasons
  206. reference.setAttribute('aria-describedby', tooltipNode.id);
  207. // append tooltip to container
  208. const container = this._findContainer(options.container, reference);
  209. this._append(tooltipNode, container);
  210. this._popperOptions = _extends({}, options.popperOptions, {
  211. placement: options.placement
  212. });
  213. this._popperOptions.modifiers = _extends({}, this._popperOptions.modifiers, {
  214. arrow: _extends({}, this._popperOptions.modifiers && this._popperOptions.modifiers.arrow, {
  215. element: options.arrowSelector
  216. }),
  217. offset: _extends({}, this._popperOptions.modifiers && this._popperOptions.modifiers.offset, {
  218. offset: options.offset
  219. })
  220. });
  221. if (options.boundariesElement) {
  222. this._popperOptions.modifiers.preventOverflow = {
  223. boundariesElement: options.boundariesElement
  224. };
  225. }
  226. this.popperInstance = new Popper(reference, tooltipNode, this._popperOptions);
  227. this._tooltipNode = tooltipNode;
  228. return this;
  229. }
  230. _hide() /*reference, options*/{
  231. // don't hide if it's already hidden
  232. if (!this._isOpen) {
  233. return this;
  234. }
  235. this._isOpen = false;
  236. // hide tooltipNode
  237. this._tooltipNode.style.visibility = 'hidden';
  238. this._tooltipNode.setAttribute('aria-hidden', 'true');
  239. return this;
  240. }
  241. _dispose() {
  242. // remove event listeners first to prevent any unexpected behaviour
  243. this._events.forEach(({ func, event }) => {
  244. this.reference.removeEventListener(event, func);
  245. });
  246. this._events = [];
  247. if (this._tooltipNode) {
  248. this._hide();
  249. // destroy instance
  250. this.popperInstance.destroy();
  251. // destroy tooltipNode if removeOnDestroy is not set, as popperInstance.destroy() already removes the element
  252. if (!this.popperInstance.options.removeOnDestroy) {
  253. this._tooltipNode.parentNode.removeChild(this._tooltipNode);
  254. this._tooltipNode = null;
  255. }
  256. }
  257. return this;
  258. }
  259. _findContainer(container, reference) {
  260. // if container is a query, get the relative element
  261. if (typeof container === 'string') {
  262. container = window.document.querySelector(container);
  263. } else if (container === false) {
  264. // if container is `false`, set it to reference parent
  265. container = reference.parentNode;
  266. }
  267. return container;
  268. }
  269. /**
  270. * Append tooltip to container
  271. * @memberof Tooltip
  272. * @private
  273. * @param {HTMLElement} tooltipNode
  274. * @param {HTMLElement|String|false} container
  275. */
  276. _append(tooltipNode, container) {
  277. container.appendChild(tooltipNode);
  278. }
  279. _setEventListeners(reference, events, options) {
  280. const directEvents = [];
  281. const oppositeEvents = [];
  282. events.forEach(event => {
  283. switch (event) {
  284. case 'hover':
  285. directEvents.push('mouseenter');
  286. oppositeEvents.push('mouseleave');
  287. break;
  288. case 'focus':
  289. directEvents.push('focus');
  290. oppositeEvents.push('blur');
  291. break;
  292. case 'click':
  293. directEvents.push('click');
  294. oppositeEvents.push('click');
  295. break;
  296. }
  297. });
  298. // schedule show tooltip
  299. directEvents.forEach(event => {
  300. const func = evt => {
  301. if (this._isOpening === true) {
  302. return;
  303. }
  304. evt.usedByTooltip = true;
  305. this._scheduleShow(reference, options.delay, options, evt);
  306. };
  307. this._events.push({ event, func });
  308. reference.addEventListener(event, func);
  309. });
  310. // schedule hide tooltip
  311. oppositeEvents.forEach(event => {
  312. const func = evt => {
  313. if (evt.usedByTooltip === true) {
  314. return;
  315. }
  316. this._scheduleHide(reference, options.delay, options, evt);
  317. };
  318. this._events.push({ event, func });
  319. reference.addEventListener(event, func);
  320. if (event === 'click' && options.closeOnClickOutside) {
  321. document.addEventListener('mousedown', e => {
  322. if (!this._isOpening) {
  323. return;
  324. }
  325. const popper = this.popperInstance.popper;
  326. if (reference.contains(e.target) || popper.contains(e.target)) {
  327. return;
  328. }
  329. func(e);
  330. }, true);
  331. }
  332. });
  333. }
  334. _scheduleShow(reference, delay, options /*, evt */) {
  335. this._isOpening = true;
  336. // defaults to 0
  337. const computedDelay = delay && delay.show || delay || 0;
  338. this._showTimeout = window.setTimeout(() => this._show(reference, options), computedDelay);
  339. }
  340. _scheduleHide(reference, delay, options, evt) {
  341. this._isOpening = false;
  342. // defaults to 0
  343. const computedDelay = delay && delay.hide || delay || 0;
  344. window.clearTimeout(this._showTimeout);
  345. window.setTimeout(() => {
  346. if (this._isOpen === false) {
  347. return;
  348. }
  349. if (!document.body.contains(this._tooltipNode)) {
  350. return;
  351. }
  352. // if we are hiding because of a mouseleave, we must check that the new
  353. // reference isn't the tooltip, because in this case we don't want to hide it
  354. if (evt.type === 'mouseleave') {
  355. const isSet = this._setTooltipNodeEvent(evt, reference, delay, options);
  356. // if we set the new event, don't hide the tooltip yet
  357. // the new event will take care to hide it if necessary
  358. if (isSet) {
  359. return;
  360. }
  361. }
  362. this._hide(reference, options);
  363. }, computedDelay);
  364. }
  365. _updateTitleContent(title) {
  366. if (typeof this._tooltipNode === 'undefined') {
  367. if (typeof this.options.title !== 'undefined') {
  368. this.options.title = title;
  369. }
  370. return;
  371. }
  372. const titleNode = this._tooltipNode.querySelector(this.options.innerSelector);
  373. this._clearTitleContent(titleNode, this.options.html, this.reference.getAttribute('title') || this.options.title);
  374. this._addTitleContent(this.reference, title, this.options.html, titleNode);
  375. this.options.title = title;
  376. this.popperInstance.update();
  377. }
  378. _clearTitleContent(titleNode, allowHtml, lastTitle) {
  379. if (lastTitle.nodeType === 1 || lastTitle.nodeType === 11) {
  380. allowHtml && titleNode.removeChild(lastTitle);
  381. } else {
  382. allowHtml ? titleNode.innerHTML = '' : titleNode.textContent = '';
  383. }
  384. }
  385. }
  386. /**
  387. * Title function, its context is the Tooltip instance.
  388. * @memberof Tooltip
  389. * @callback TitleFunction
  390. * @return {String} placement - The desired title.
  391. */
  392. var _initialiseProps = function () {
  393. this.show = () => this._show(this.reference, this.options);
  394. this.hide = () => this._hide();
  395. this.dispose = () => this._dispose();
  396. this.toggle = () => {
  397. if (this._isOpen) {
  398. return this.hide();
  399. } else {
  400. return this.show();
  401. }
  402. };
  403. this.updateTitleContent = title => this._updateTitleContent(title);
  404. this._events = [];
  405. this._setTooltipNodeEvent = (evt, reference, delay, options) => {
  406. const relatedreference = evt.relatedreference || evt.toElement || evt.relatedTarget;
  407. const callback = evt2 => {
  408. const relatedreference2 = evt2.relatedreference || evt2.toElement || evt2.relatedTarget;
  409. // Remove event listener after call
  410. this._tooltipNode.removeEventListener(evt.type, callback);
  411. // If the new reference is not the reference element
  412. if (!reference.contains(relatedreference2)) {
  413. // Schedule to hide tooltip
  414. this._scheduleHide(reference, options.delay, options, evt2);
  415. }
  416. };
  417. if (this._tooltipNode.contains(relatedreference)) {
  418. // listen to mouseleave on the tooltip element to be able to hide the tooltip
  419. this._tooltipNode.addEventListener(evt.type, callback);
  420. return true;
  421. }
  422. return false;
  423. };
  424. };
  425. export default Tooltip;
  426. //# sourceMappingURL=tooltip.js.map