foundation.tooltip.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. 'use strict';
  2. import $ from 'jquery';
  3. import { GetYoDigits, ignoreMousedisappear } from './foundation.core.utils';
  4. import { MediaQuery } from './foundation.util.mediaQuery';
  5. import { Triggers } from './foundation.util.triggers';
  6. import { Positionable } from './foundation.positionable';
  7. /**
  8. * Tooltip module.
  9. * @module foundation.tooltip
  10. * @requires foundation.util.box
  11. * @requires foundation.util.mediaQuery
  12. * @requires foundation.util.triggers
  13. */
  14. class Tooltip extends Positionable {
  15. /**
  16. * Creates a new instance of a Tooltip.
  17. * @class
  18. * @name Tooltip
  19. * @fires Tooltip#init
  20. * @param {jQuery} element - jQuery object to attach a tooltip to.
  21. * @param {Object} options - object to extend the default configuration.
  22. */
  23. _setup(element, options) {
  24. this.$element = element;
  25. this.options = $.extend({}, Tooltip.defaults, this.$element.data(), options);
  26. this.className = 'Tooltip'; // ie9 back compat
  27. this.isActive = false;
  28. this.isClick = false;
  29. // Triggers init is idempotent, just need to make sure it is initialized
  30. Triggers.init($);
  31. this._init();
  32. }
  33. /**
  34. * Initializes the tooltip by setting the creating the tip element, adding it's text, setting private variables and setting attributes on the anchor.
  35. * @private
  36. */
  37. _init() {
  38. MediaQuery._init();
  39. var elemId = this.$element.attr('aria-describedby') || GetYoDigits(6, 'tooltip');
  40. this.options.tipText = this.options.tipText || this.$element.attr('title');
  41. this.template = this.options.template ? $(this.options.template) : this._buildTemplate(elemId);
  42. if (this.options.allowHtml) {
  43. this.template.appendTo(document.body)
  44. .html(this.options.tipText)
  45. .hide();
  46. } else {
  47. this.template.appendTo(document.body)
  48. .text(this.options.tipText)
  49. .hide();
  50. }
  51. this.$element.attr({
  52. 'title': '',
  53. 'aria-describedby': elemId,
  54. 'data-yeti-box': elemId,
  55. 'data-toggle': elemId,
  56. 'data-resize': elemId
  57. }).addClass(this.options.triggerClass);
  58. super._init();
  59. this._events();
  60. }
  61. _getDefaultPosition() {
  62. // handle legacy classnames
  63. var position = this.$element[0].className.match(/\b(top|left|right|bottom)\b/g);
  64. return position ? position[0] : 'top';
  65. }
  66. _getDefaultAlignment() {
  67. return 'center';
  68. }
  69. _getHOffset() {
  70. if(this.position === 'left' || this.position === 'right') {
  71. return this.options.hOffset + this.options.tooltipWidth;
  72. } else {
  73. return this.options.hOffset
  74. }
  75. }
  76. _getVOffset() {
  77. if(this.position === 'top' || this.position === 'bottom') {
  78. return this.options.vOffset + this.options.tooltipHeight;
  79. } else {
  80. return this.options.vOffset
  81. }
  82. }
  83. /**
  84. * builds the tooltip element, adds attributes, and returns the template.
  85. * @private
  86. */
  87. _buildTemplate(id) {
  88. var templateClasses = (`${this.options.tooltipClass} ${this.options.templateClasses}`).trim();
  89. var $template = $('<div></div>').addClass(templateClasses).attr({
  90. 'role': 'tooltip',
  91. 'aria-hidden': true,
  92. 'data-is-active': false,
  93. 'data-is-focus': false,
  94. 'id': id
  95. });
  96. return $template;
  97. }
  98. /**
  99. * sets the position class of an element and recursively calls itself until there are no more possible positions to attempt, or the tooltip element is no longer colliding.
  100. * if the tooltip is larger than the screen width, default to full width - any user selected margin
  101. * @private
  102. */
  103. _setPosition() {
  104. super._setPosition(this.$element, this.template);
  105. }
  106. /**
  107. * reveals the tooltip, and fires an event to close any other open tooltips on the page
  108. * @fires Tooltip#closeme
  109. * @fires Tooltip#show
  110. * @function
  111. */
  112. show() {
  113. if (this.options.showOn !== 'all' && !MediaQuery.is(this.options.showOn)) {
  114. // console.error('The screen is too small to display this tooltip');
  115. return false;
  116. }
  117. var _this = this;
  118. this.template.css('visibility', 'hidden').show();
  119. this._setPosition();
  120. this.template.removeClass('top bottom left right').addClass(this.position)
  121. this.template.removeClass('align-top align-bottom align-left align-right align-center').addClass('align-' + this.alignment);
  122. /**
  123. * Fires to close all other open tooltips on the page
  124. * @event Closeme#tooltip
  125. */
  126. this.$element.trigger('closeme.zf.tooltip', this.template.attr('id'));
  127. this.template.attr({
  128. 'data-is-active': true,
  129. 'aria-hidden': false
  130. });
  131. _this.isActive = true;
  132. // console.log(this.template);
  133. this.template.stop().hide().css('visibility', '').fadeIn(this.options.fadeInDuration, function() {
  134. //maybe do stuff?
  135. });
  136. /**
  137. * Fires when the tooltip is shown
  138. * @event Tooltip#show
  139. */
  140. this.$element.trigger('show.zf.tooltip');
  141. }
  142. /**
  143. * Hides the current tooltip, and resets the positioning class if it was changed due to collision
  144. * @fires Tooltip#hide
  145. * @function
  146. */
  147. hide() {
  148. // console.log('hiding', this.$element.data('yeti-box'));
  149. var _this = this;
  150. this.template.stop().attr({
  151. 'aria-hidden': true,
  152. 'data-is-active': false
  153. }).fadeOut(this.options.fadeOutDuration, function() {
  154. _this.isActive = false;
  155. _this.isClick = false;
  156. });
  157. /**
  158. * fires when the tooltip is hidden
  159. * @event Tooltip#hide
  160. */
  161. this.$element.trigger('hide.zf.tooltip');
  162. }
  163. /**
  164. * adds event listeners for the tooltip and its anchor
  165. * TODO combine some of the listeners like focus and mouseenter, etc.
  166. * @private
  167. */
  168. _events() {
  169. var _this = this;
  170. var $template = this.template;
  171. var isFocus = false;
  172. if (!this.options.disableHover) {
  173. this.$element
  174. .on('mouseenter.zf.tooltip', function(e) {
  175. if (!_this.isActive) {
  176. _this.timeout = setTimeout(function() {
  177. _this.show();
  178. }, _this.options.hoverDelay);
  179. }
  180. })
  181. .on('mouseleave.zf.tooltip', ignoreMousedisappear(function(e) {
  182. clearTimeout(_this.timeout);
  183. if (!isFocus || (_this.isClick && !_this.options.clickOpen)) {
  184. _this.hide();
  185. }
  186. }));
  187. }
  188. if (this.options.clickOpen) {
  189. this.$element.on('mousedown.zf.tooltip', function(e) {
  190. e.stopImmediatePropagation();
  191. if (_this.isClick) {
  192. //_this.hide();
  193. // _this.isClick = false;
  194. } else {
  195. _this.isClick = true;
  196. if ((_this.options.disableHover || !_this.$element.attr('tabindex')) && !_this.isActive) {
  197. _this.show();
  198. }
  199. }
  200. });
  201. } else {
  202. this.$element.on('mousedown.zf.tooltip', function(e) {
  203. e.stopImmediatePropagation();
  204. _this.isClick = true;
  205. });
  206. }
  207. if (!this.options.disableForTouch) {
  208. this.$element
  209. .on('tap.zf.tooltip touchend.zf.tooltip', function(e) {
  210. _this.isActive ? _this.hide() : _this.show();
  211. });
  212. }
  213. this.$element.on({
  214. // 'toggle.zf.trigger': this.toggle.bind(this),
  215. // 'close.zf.trigger': this.hide.bind(this)
  216. 'close.zf.trigger': this.hide.bind(this)
  217. });
  218. this.$element
  219. .on('focus.zf.tooltip', function(e) {
  220. isFocus = true;
  221. if (_this.isClick) {
  222. // If we're not showing open on clicks, we need to pretend a click-launched focus isn't
  223. // a real focus, otherwise on hover and come back we get bad behavior
  224. if(!_this.options.clickOpen) { isFocus = false; }
  225. return false;
  226. } else {
  227. _this.show();
  228. }
  229. })
  230. .on('focusout.zf.tooltip', function(e) {
  231. isFocus = false;
  232. _this.isClick = false;
  233. _this.hide();
  234. })
  235. .on('resizeme.zf.trigger', function() {
  236. if (_this.isActive) {
  237. _this._setPosition();
  238. }
  239. });
  240. }
  241. /**
  242. * adds a toggle method, in addition to the static show() & hide() functions
  243. * @function
  244. */
  245. toggle() {
  246. if (this.isActive) {
  247. this.hide();
  248. } else {
  249. this.show();
  250. }
  251. }
  252. /**
  253. * Destroys an instance of tooltip, removes template element from the view.
  254. * @function
  255. */
  256. _destroy() {
  257. this.$element.attr('title', this.template.text())
  258. .off('.zf.trigger .zf.tooltip')
  259. .removeClass(this.options.triggerClass)
  260. .removeClass('top right left bottom')
  261. .removeAttr('aria-describedby data-disable-hover data-resize data-toggle data-tooltip data-yeti-box');
  262. this.template.remove();
  263. }
  264. }
  265. Tooltip.defaults = {
  266. disableForTouch: false,
  267. /**
  268. * Time, in ms, before a tooltip should open on hover.
  269. * @option
  270. * @type {number}
  271. * @default 200
  272. */
  273. hoverDelay: 200,
  274. /**
  275. * Time, in ms, a tooltip should take to fade into view.
  276. * @option
  277. * @type {number}
  278. * @default 150
  279. */
  280. fadeInDuration: 150,
  281. /**
  282. * Time, in ms, a tooltip should take to fade out of view.
  283. * @option
  284. * @type {number}
  285. * @default 150
  286. */
  287. fadeOutDuration: 150,
  288. /**
  289. * Disables hover events from opening the tooltip if set to true
  290. * @option
  291. * @type {boolean}
  292. * @default false
  293. */
  294. disableHover: false,
  295. /**
  296. * Optional addtional classes to apply to the tooltip template on init.
  297. * @option
  298. * @type {string}
  299. * @default ''
  300. */
  301. templateClasses: '',
  302. /**
  303. * Non-optional class added to tooltip templates. Foundation default is 'tooltip'.
  304. * @option
  305. * @type {string}
  306. * @default 'tooltip'
  307. */
  308. tooltipClass: 'tooltip',
  309. /**
  310. * Class applied to the tooltip anchor element.
  311. * @option
  312. * @type {string}
  313. * @default 'has-tip'
  314. */
  315. triggerClass: 'has-tip',
  316. /**
  317. * Minimum breakpoint size at which to open the tooltip.
  318. * @option
  319. * @type {string}
  320. * @default 'small'
  321. */
  322. showOn: 'small',
  323. /**
  324. * Custom template to be used to generate markup for tooltip.
  325. * @option
  326. * @type {string}
  327. * @default ''
  328. */
  329. template: '',
  330. /**
  331. * Text displayed in the tooltip template on open.
  332. * @option
  333. * @type {string}
  334. * @default ''
  335. */
  336. tipText: '',
  337. touchCloseText: 'Tap to close.',
  338. /**
  339. * Allows the tooltip to remain open if triggered with a click or touch event.
  340. * @option
  341. * @type {boolean}
  342. * @default true
  343. */
  344. clickOpen: true,
  345. /**
  346. * Position of tooltip. Can be left, right, bottom, top, or auto.
  347. * @option
  348. * @type {string}
  349. * @default 'auto'
  350. */
  351. position: 'auto',
  352. /**
  353. * Alignment of tooltip relative to anchor. Can be left, right, bottom, top, center, or auto.
  354. * @option
  355. * @type {string}
  356. * @default 'auto'
  357. */
  358. alignment: 'auto',
  359. /**
  360. * Allow overlap of container/window. If false, tooltip will first try to
  361. * position as defined by data-position and data-alignment, but reposition if
  362. * it would cause an overflow. @option
  363. * @type {boolean}
  364. * @default false
  365. */
  366. allowOverlap: false,
  367. /**
  368. * Allow overlap of only the bottom of the container. This is the most common
  369. * behavior for dropdowns, allowing the dropdown to extend the bottom of the
  370. * screen but not otherwise influence or break out of the container.
  371. * Less common for tooltips.
  372. * @option
  373. * @type {boolean}
  374. * @default false
  375. */
  376. allowBottomOverlap: false,
  377. /**
  378. * Distance, in pixels, the template should push away from the anchor on the Y axis.
  379. * @option
  380. * @type {number}
  381. * @default 0
  382. */
  383. vOffset: 0,
  384. /**
  385. * Distance, in pixels, the template should push away from the anchor on the X axis
  386. * @option
  387. * @type {number}
  388. * @default 0
  389. */
  390. hOffset: 0,
  391. /**
  392. * Distance, in pixels, the template spacing auto-adjust for a vertical tooltip
  393. * @option
  394. * @type {number}
  395. * @default 14
  396. */
  397. tooltipHeight: 14,
  398. /**
  399. * Distance, in pixels, the template spacing auto-adjust for a horizontal tooltip
  400. * @option
  401. * @type {number}
  402. * @default 12
  403. */
  404. tooltipWidth: 12,
  405. /**
  406. * Allow HTML in tooltip. Warning: If you are loading user-generated content into tooltips,
  407. * allowing HTML may open yourself up to XSS attacks.
  408. * @option
  409. * @type {boolean}
  410. * @default false
  411. */
  412. allowHtml: false
  413. };
  414. /**
  415. * TODO utilize resize event trigger
  416. */
  417. export {Tooltip};