457 lines
12 KiB
JavaScript
457 lines
12 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
import $ from 'jquery';
|
||
|
|
||
|
import { GetYoDigits, ignoreMousedisappear } from './foundation.core.utils';
|
||
|
import { MediaQuery } from './foundation.util.mediaQuery';
|
||
|
import { Triggers } from './foundation.util.triggers';
|
||
|
import { Positionable } from './foundation.positionable';
|
||
|
|
||
|
/**
|
||
|
* Tooltip module.
|
||
|
* @module foundation.tooltip
|
||
|
* @requires foundation.util.box
|
||
|
* @requires foundation.util.mediaQuery
|
||
|
* @requires foundation.util.triggers
|
||
|
*/
|
||
|
|
||
|
class Tooltip extends Positionable {
|
||
|
/**
|
||
|
* Creates a new instance of a Tooltip.
|
||
|
* @class
|
||
|
* @name Tooltip
|
||
|
* @fires Tooltip#init
|
||
|
* @param {jQuery} element - jQuery object to attach a tooltip to.
|
||
|
* @param {Object} options - object to extend the default configuration.
|
||
|
*/
|
||
|
_setup(element, options) {
|
||
|
this.$element = element;
|
||
|
this.options = $.extend({}, Tooltip.defaults, this.$element.data(), options);
|
||
|
this.className = 'Tooltip'; // ie9 back compat
|
||
|
|
||
|
this.isActive = false;
|
||
|
this.isClick = false;
|
||
|
|
||
|
// Triggers init is idempotent, just need to make sure it is initialized
|
||
|
Triggers.init($);
|
||
|
|
||
|
this._init();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Initializes the tooltip by setting the creating the tip element, adding it's text, setting private variables and setting attributes on the anchor.
|
||
|
* @private
|
||
|
*/
|
||
|
_init() {
|
||
|
MediaQuery._init();
|
||
|
var elemId = this.$element.attr('aria-describedby') || GetYoDigits(6, 'tooltip');
|
||
|
|
||
|
this.options.tipText = this.options.tipText || this.$element.attr('title');
|
||
|
this.template = this.options.template ? $(this.options.template) : this._buildTemplate(elemId);
|
||
|
|
||
|
if (this.options.allowHtml) {
|
||
|
this.template.appendTo(document.body)
|
||
|
.html(this.options.tipText)
|
||
|
.hide();
|
||
|
} else {
|
||
|
this.template.appendTo(document.body)
|
||
|
.text(this.options.tipText)
|
||
|
.hide();
|
||
|
}
|
||
|
|
||
|
this.$element.attr({
|
||
|
'title': '',
|
||
|
'aria-describedby': elemId,
|
||
|
'data-yeti-box': elemId,
|
||
|
'data-toggle': elemId,
|
||
|
'data-resize': elemId
|
||
|
}).addClass(this.options.triggerClass);
|
||
|
|
||
|
super._init();
|
||
|
this._events();
|
||
|
}
|
||
|
|
||
|
_getDefaultPosition() {
|
||
|
// handle legacy classnames
|
||
|
var position = this.$element[0].className.match(/\b(top|left|right|bottom)\b/g);
|
||
|
return position ? position[0] : 'top';
|
||
|
}
|
||
|
|
||
|
_getDefaultAlignment() {
|
||
|
return 'center';
|
||
|
}
|
||
|
|
||
|
_getHOffset() {
|
||
|
if(this.position === 'left' || this.position === 'right') {
|
||
|
return this.options.hOffset + this.options.tooltipWidth;
|
||
|
} else {
|
||
|
return this.options.hOffset
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_getVOffset() {
|
||
|
if(this.position === 'top' || this.position === 'bottom') {
|
||
|
return this.options.vOffset + this.options.tooltipHeight;
|
||
|
} else {
|
||
|
return this.options.vOffset
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* builds the tooltip element, adds attributes, and returns the template.
|
||
|
* @private
|
||
|
*/
|
||
|
_buildTemplate(id) {
|
||
|
var templateClasses = (`${this.options.tooltipClass} ${this.options.templateClasses}`).trim();
|
||
|
var $template = $('<div></div>').addClass(templateClasses).attr({
|
||
|
'role': 'tooltip',
|
||
|
'aria-hidden': true,
|
||
|
'data-is-active': false,
|
||
|
'data-is-focus': false,
|
||
|
'id': id
|
||
|
});
|
||
|
return $template;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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.
|
||
|
* if the tooltip is larger than the screen width, default to full width - any user selected margin
|
||
|
* @private
|
||
|
*/
|
||
|
_setPosition() {
|
||
|
super._setPosition(this.$element, this.template);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* reveals the tooltip, and fires an event to close any other open tooltips on the page
|
||
|
* @fires Tooltip#closeme
|
||
|
* @fires Tooltip#show
|
||
|
* @function
|
||
|
*/
|
||
|
show() {
|
||
|
if (this.options.showOn !== 'all' && !MediaQuery.is(this.options.showOn)) {
|
||
|
// console.error('The screen is too small to display this tooltip');
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
var _this = this;
|
||
|
this.template.css('visibility', 'hidden').show();
|
||
|
this._setPosition();
|
||
|
this.template.removeClass('top bottom left right').addClass(this.position)
|
||
|
this.template.removeClass('align-top align-bottom align-left align-right align-center').addClass('align-' + this.alignment);
|
||
|
|
||
|
/**
|
||
|
* Fires to close all other open tooltips on the page
|
||
|
* @event Closeme#tooltip
|
||
|
*/
|
||
|
this.$element.trigger('closeme.zf.tooltip', this.template.attr('id'));
|
||
|
|
||
|
|
||
|
this.template.attr({
|
||
|
'data-is-active': true,
|
||
|
'aria-hidden': false
|
||
|
});
|
||
|
_this.isActive = true;
|
||
|
// console.log(this.template);
|
||
|
this.template.stop().hide().css('visibility', '').fadeIn(this.options.fadeInDuration, function() {
|
||
|
//maybe do stuff?
|
||
|
});
|
||
|
/**
|
||
|
* Fires when the tooltip is shown
|
||
|
* @event Tooltip#show
|
||
|
*/
|
||
|
this.$element.trigger('show.zf.tooltip');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Hides the current tooltip, and resets the positioning class if it was changed due to collision
|
||
|
* @fires Tooltip#hide
|
||
|
* @function
|
||
|
*/
|
||
|
hide() {
|
||
|
// console.log('hiding', this.$element.data('yeti-box'));
|
||
|
var _this = this;
|
||
|
this.template.stop().attr({
|
||
|
'aria-hidden': true,
|
||
|
'data-is-active': false
|
||
|
}).fadeOut(this.options.fadeOutDuration, function() {
|
||
|
_this.isActive = false;
|
||
|
_this.isClick = false;
|
||
|
});
|
||
|
/**
|
||
|
* fires when the tooltip is hidden
|
||
|
* @event Tooltip#hide
|
||
|
*/
|
||
|
this.$element.trigger('hide.zf.tooltip');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* adds event listeners for the tooltip and its anchor
|
||
|
* TODO combine some of the listeners like focus and mouseenter, etc.
|
||
|
* @private
|
||
|
*/
|
||
|
_events() {
|
||
|
var _this = this;
|
||
|
var $template = this.template;
|
||
|
var isFocus = false;
|
||
|
|
||
|
if (!this.options.disableHover) {
|
||
|
|
||
|
this.$element
|
||
|
.on('mouseenter.zf.tooltip', function(e) {
|
||
|
if (!_this.isActive) {
|
||
|
_this.timeout = setTimeout(function() {
|
||
|
_this.show();
|
||
|
}, _this.options.hoverDelay);
|
||
|
}
|
||
|
})
|
||
|
.on('mouseleave.zf.tooltip', ignoreMousedisappear(function(e) {
|
||
|
clearTimeout(_this.timeout);
|
||
|
if (!isFocus || (_this.isClick && !_this.options.clickOpen)) {
|
||
|
_this.hide();
|
||
|
}
|
||
|
}));
|
||
|
}
|
||
|
|
||
|
if (this.options.clickOpen) {
|
||
|
this.$element.on('mousedown.zf.tooltip', function(e) {
|
||
|
e.stopImmediatePropagation();
|
||
|
if (_this.isClick) {
|
||
|
//_this.hide();
|
||
|
// _this.isClick = false;
|
||
|
} else {
|
||
|
_this.isClick = true;
|
||
|
if ((_this.options.disableHover || !_this.$element.attr('tabindex')) && !_this.isActive) {
|
||
|
_this.show();
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
} else {
|
||
|
this.$element.on('mousedown.zf.tooltip', function(e) {
|
||
|
e.stopImmediatePropagation();
|
||
|
_this.isClick = true;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (!this.options.disableForTouch) {
|
||
|
this.$element
|
||
|
.on('tap.zf.tooltip touchend.zf.tooltip', function(e) {
|
||
|
_this.isActive ? _this.hide() : _this.show();
|
||
|
});
|
||
|
}
|
||
|
|
||
|
this.$element.on({
|
||
|
// 'toggle.zf.trigger': this.toggle.bind(this),
|
||
|
// 'close.zf.trigger': this.hide.bind(this)
|
||
|
'close.zf.trigger': this.hide.bind(this)
|
||
|
});
|
||
|
|
||
|
this.$element
|
||
|
.on('focus.zf.tooltip', function(e) {
|
||
|
isFocus = true;
|
||
|
if (_this.isClick) {
|
||
|
// If we're not showing open on clicks, we need to pretend a click-launched focus isn't
|
||
|
// a real focus, otherwise on hover and come back we get bad behavior
|
||
|
if(!_this.options.clickOpen) { isFocus = false; }
|
||
|
return false;
|
||
|
} else {
|
||
|
_this.show();
|
||
|
}
|
||
|
})
|
||
|
|
||
|
.on('focusout.zf.tooltip', function(e) {
|
||
|
isFocus = false;
|
||
|
_this.isClick = false;
|
||
|
_this.hide();
|
||
|
})
|
||
|
|
||
|
.on('resizeme.zf.trigger', function() {
|
||
|
if (_this.isActive) {
|
||
|
_this._setPosition();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* adds a toggle method, in addition to the static show() & hide() functions
|
||
|
* @function
|
||
|
*/
|
||
|
toggle() {
|
||
|
if (this.isActive) {
|
||
|
this.hide();
|
||
|
} else {
|
||
|
this.show();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Destroys an instance of tooltip, removes template element from the view.
|
||
|
* @function
|
||
|
*/
|
||
|
_destroy() {
|
||
|
this.$element.attr('title', this.template.text())
|
||
|
.off('.zf.trigger .zf.tooltip')
|
||
|
.removeClass(this.options.triggerClass)
|
||
|
.removeClass('top right left bottom')
|
||
|
.removeAttr('aria-describedby data-disable-hover data-resize data-toggle data-tooltip data-yeti-box');
|
||
|
|
||
|
this.template.remove();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Tooltip.defaults = {
|
||
|
disableForTouch: false,
|
||
|
/**
|
||
|
* Time, in ms, before a tooltip should open on hover.
|
||
|
* @option
|
||
|
* @type {number}
|
||
|
* @default 200
|
||
|
*/
|
||
|
hoverDelay: 200,
|
||
|
/**
|
||
|
* Time, in ms, a tooltip should take to fade into view.
|
||
|
* @option
|
||
|
* @type {number}
|
||
|
* @default 150
|
||
|
*/
|
||
|
fadeInDuration: 150,
|
||
|
/**
|
||
|
* Time, in ms, a tooltip should take to fade out of view.
|
||
|
* @option
|
||
|
* @type {number}
|
||
|
* @default 150
|
||
|
*/
|
||
|
fadeOutDuration: 150,
|
||
|
/**
|
||
|
* Disables hover events from opening the tooltip if set to true
|
||
|
* @option
|
||
|
* @type {boolean}
|
||
|
* @default false
|
||
|
*/
|
||
|
disableHover: false,
|
||
|
/**
|
||
|
* Optional addtional classes to apply to the tooltip template on init.
|
||
|
* @option
|
||
|
* @type {string}
|
||
|
* @default ''
|
||
|
*/
|
||
|
templateClasses: '',
|
||
|
/**
|
||
|
* Non-optional class added to tooltip templates. Foundation default is 'tooltip'.
|
||
|
* @option
|
||
|
* @type {string}
|
||
|
* @default 'tooltip'
|
||
|
*/
|
||
|
tooltipClass: 'tooltip',
|
||
|
/**
|
||
|
* Class applied to the tooltip anchor element.
|
||
|
* @option
|
||
|
* @type {string}
|
||
|
* @default 'has-tip'
|
||
|
*/
|
||
|
triggerClass: 'has-tip',
|
||
|
/**
|
||
|
* Minimum breakpoint size at which to open the tooltip.
|
||
|
* @option
|
||
|
* @type {string}
|
||
|
* @default 'small'
|
||
|
*/
|
||
|
showOn: 'small',
|
||
|
/**
|
||
|
* Custom template to be used to generate markup for tooltip.
|
||
|
* @option
|
||
|
* @type {string}
|
||
|
* @default ''
|
||
|
*/
|
||
|
template: '',
|
||
|
/**
|
||
|
* Text displayed in the tooltip template on open.
|
||
|
* @option
|
||
|
* @type {string}
|
||
|
* @default ''
|
||
|
*/
|
||
|
tipText: '',
|
||
|
touchCloseText: 'Tap to close.',
|
||
|
/**
|
||
|
* Allows the tooltip to remain open if triggered with a click or touch event.
|
||
|
* @option
|
||
|
* @type {boolean}
|
||
|
* @default true
|
||
|
*/
|
||
|
clickOpen: true,
|
||
|
/**
|
||
|
* Position of tooltip. Can be left, right, bottom, top, or auto.
|
||
|
* @option
|
||
|
* @type {string}
|
||
|
* @default 'auto'
|
||
|
*/
|
||
|
position: 'auto',
|
||
|
/**
|
||
|
* Alignment of tooltip relative to anchor. Can be left, right, bottom, top, center, or auto.
|
||
|
* @option
|
||
|
* @type {string}
|
||
|
* @default 'auto'
|
||
|
*/
|
||
|
alignment: 'auto',
|
||
|
/**
|
||
|
* Allow overlap of container/window. If false, tooltip will first try to
|
||
|
* position as defined by data-position and data-alignment, but reposition if
|
||
|
* it would cause an overflow. @option
|
||
|
* @type {boolean}
|
||
|
* @default false
|
||
|
*/
|
||
|
allowOverlap: false,
|
||
|
/**
|
||
|
* Allow overlap of only the bottom of the container. This is the most common
|
||
|
* behavior for dropdowns, allowing the dropdown to extend the bottom of the
|
||
|
* screen but not otherwise influence or break out of the container.
|
||
|
* Less common for tooltips.
|
||
|
* @option
|
||
|
* @type {boolean}
|
||
|
* @default false
|
||
|
*/
|
||
|
allowBottomOverlap: false,
|
||
|
/**
|
||
|
* Distance, in pixels, the template should push away from the anchor on the Y axis.
|
||
|
* @option
|
||
|
* @type {number}
|
||
|
* @default 0
|
||
|
*/
|
||
|
vOffset: 0,
|
||
|
/**
|
||
|
* Distance, in pixels, the template should push away from the anchor on the X axis
|
||
|
* @option
|
||
|
* @type {number}
|
||
|
* @default 0
|
||
|
*/
|
||
|
hOffset: 0,
|
||
|
/**
|
||
|
* Distance, in pixels, the template spacing auto-adjust for a vertical tooltip
|
||
|
* @option
|
||
|
* @type {number}
|
||
|
* @default 14
|
||
|
*/
|
||
|
tooltipHeight: 14,
|
||
|
/**
|
||
|
* Distance, in pixels, the template spacing auto-adjust for a horizontal tooltip
|
||
|
* @option
|
||
|
* @type {number}
|
||
|
* @default 12
|
||
|
*/
|
||
|
tooltipWidth: 12,
|
||
|
/**
|
||
|
* Allow HTML in tooltip. Warning: If you are loading user-generated content into tooltips,
|
||
|
* allowing HTML may open yourself up to XSS attacks.
|
||
|
* @option
|
||
|
* @type {boolean}
|
||
|
* @default false
|
||
|
*/
|
||
|
allowHtml: false
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* TODO utilize resize event trigger
|
||
|
*/
|
||
|
|
||
|
export {Tooltip};
|