UNPKG

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