1 | ;
|
2 |
|
3 | import $ from 'jquery';
|
4 |
|
5 | import { GetYoDigits } from './foundation.util.core';
|
6 | import { MediaQuery } from './foundation.util.mediaQuery';
|
7 | import { Triggers } from './foundation.util.triggers';
|
8 | import { 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 |
|
18 | class 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 |
|
301 | Tooltip.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 |
|
462 | export {Tooltip};
|