'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 = $('
').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};