forked from samuel-p/sp-codes.de
444 lines
13 KiB
JavaScript
444 lines
13 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
!function($) {
|
||
|
|
||
|
/**
|
||
|
* Tooltip module.
|
||
|
* @module foundation.tooltip
|
||
|
* @requires foundation.util.box
|
||
|
* @requires foundation.util.triggers
|
||
|
*/
|
||
|
|
||
|
class Tooltip {
|
||
|
/**
|
||
|
* Creates a new instance of a Tooltip.
|
||
|
* @class
|
||
|
* @fires Tooltip#init
|
||
|
* @param {jQuery} element - jQuery object to attach a tooltip to.
|
||
|
* @param {Object} options - object to extend the default configuration.
|
||
|
*/
|
||
|
constructor(element, options) {
|
||
|
this.$element = element;
|
||
|
this.options = $.extend({}, Tooltip.defaults, this.$element.data(), options);
|
||
|
|
||
|
this.isActive = false;
|
||
|
this.isClick = false;
|
||
|
this._init();
|
||
|
|
||
|
Foundation.registerPlugin(this, 'Tooltip');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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() {
|
||
|
var elemId = this.$element.attr('aria-describedby') || Foundation.GetYoDigits(6, 'tooltip');
|
||
|
|
||
|
this.options.positionClass = this.options.positionClass || this._getPositionClass(this.$element);
|
||
|
this.options.tipText = this.options.tipText || this.$element.attr('title');
|
||
|
this.template = this.options.template ? $(this.options.template) : this._buildTemplate(elemId);
|
||
|
|
||
|
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.triggerClass);
|
||
|
|
||
|
//helper variables to track movement on collisions
|
||
|
this.usedPositions = [];
|
||
|
this.counter = 4;
|
||
|
this.classChanged = false;
|
||
|
|
||
|
this._events();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Grabs the current positioning class, if present, and returns the value or an empty string.
|
||
|
* @private
|
||
|
*/
|
||
|
_getPositionClass(element) {
|
||
|
if (!element) { return ''; }
|
||
|
// var position = element.attr('class').match(/top|left|right/g);
|
||
|
var position = element[0].className.match(/\b(top|left|right)\b/g);
|
||
|
position = position ? position[0] : '';
|
||
|
return position;
|
||
|
};
|
||
|
/**
|
||
|
* builds the tooltip element, adds attributes, and returns the template.
|
||
|
* @private
|
||
|
*/
|
||
|
_buildTemplate(id) {
|
||
|
var templateClasses = (`${this.options.tooltipClass} ${this.options.positionClass} ${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;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Function that gets called if a collision event is detected.
|
||
|
* @param {String} position - positioning class to try
|
||
|
* @private
|
||
|
*/
|
||
|
_reposition(position) {
|
||
|
this.usedPositions.push(position ? position : 'bottom');
|
||
|
|
||
|
//default, try switching to opposite side
|
||
|
if (!position && (this.usedPositions.indexOf('top') < 0)) {
|
||
|
this.template.addClass('top');
|
||
|
} else if (position === 'top' && (this.usedPositions.indexOf('bottom') < 0)) {
|
||
|
this.template.removeClass(position);
|
||
|
} else if (position === 'left' && (this.usedPositions.indexOf('right') < 0)) {
|
||
|
this.template.removeClass(position)
|
||
|
.addClass('right');
|
||
|
} else if (position === 'right' && (this.usedPositions.indexOf('left') < 0)) {
|
||
|
this.template.removeClass(position)
|
||
|
.addClass('left');
|
||
|
}
|
||
|
|
||
|
//if default change didn't work, try bottom or left first
|
||
|
else if (!position && (this.usedPositions.indexOf('top') > -1) && (this.usedPositions.indexOf('left') < 0)) {
|
||
|
this.template.addClass('left');
|
||
|
} else if (position === 'top' && (this.usedPositions.indexOf('bottom') > -1) && (this.usedPositions.indexOf('left') < 0)) {
|
||
|
this.template.removeClass(position)
|
||
|
.addClass('left');
|
||
|
} else if (position === 'left' && (this.usedPositions.indexOf('right') > -1) && (this.usedPositions.indexOf('bottom') < 0)) {
|
||
|
this.template.removeClass(position);
|
||
|
} else if (position === 'right' && (this.usedPositions.indexOf('left') > -1) && (this.usedPositions.indexOf('bottom') < 0)) {
|
||
|
this.template.removeClass(position);
|
||
|
}
|
||
|
//if nothing cleared, set to bottom
|
||
|
else {
|
||
|
this.template.removeClass(position);
|
||
|
}
|
||
|
this.classChanged = true;
|
||
|
this.counter--;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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() {
|
||
|
var position = this._getPositionClass(this.template),
|
||
|
$tipDims = Foundation.Box.GetDimensions(this.template),
|
||
|
$anchorDims = Foundation.Box.GetDimensions(this.$element),
|
||
|
direction = (position === 'left' ? 'left' : ((position === 'right') ? 'left' : 'top')),
|
||
|
param = (direction === 'top') ? 'height' : 'width',
|
||
|
offset = (param === 'height') ? this.options.vOffset : this.options.hOffset,
|
||
|
_this = this;
|
||
|
|
||
|
if (($tipDims.width >= $tipDims.windowDims.width) || (!this.counter && !Foundation.Box.ImNotTouchingYou(this.template))) {
|
||
|
this.template.offset(Foundation.Box.GetOffsets(this.template, this.$element, 'center bottom', this.options.vOffset, this.options.hOffset, true)).css({
|
||
|
// this.$element.offset(Foundation.GetOffsets(this.template, this.$element, 'center bottom', this.options.vOffset, this.options.hOffset, true)).css({
|
||
|
'width': $anchorDims.windowDims.width - (this.options.hOffset * 2),
|
||
|
'height': 'auto'
|
||
|
});
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
this.template.offset(Foundation.Box.GetOffsets(this.template, this.$element,'center ' + (position || 'bottom'), this.options.vOffset, this.options.hOffset));
|
||
|
|
||
|
while(!Foundation.Box.ImNotTouchingYou(this.template) && this.counter) {
|
||
|
this._reposition(position);
|
||
|
this._setPosition();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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' && !Foundation.MediaQuery.atLeast(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();
|
||
|
|
||
|
/**
|
||
|
* 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;
|
||
|
if (_this.classChanged) {
|
||
|
_this.template
|
||
|
.removeClass(_this._getPositionClass(_this.template))
|
||
|
.addClass(_this.options.positionClass);
|
||
|
|
||
|
_this.usedPositions = [];
|
||
|
_this.counter = 4;
|
||
|
_this.classChanged = 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', 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.tootip')
|
||
|
// .removeClass('has-tip')
|
||
|
.removeAttr('aria-describedby')
|
||
|
.removeAttr('data-yeti-box')
|
||
|
.removeAttr('data-toggle')
|
||
|
.removeAttr('data-resize');
|
||
|
|
||
|
this.template.remove();
|
||
|
|
||
|
Foundation.unregisterPlugin(this);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Tooltip.defaults = {
|
||
|
disableForTouch: false,
|
||
|
/**
|
||
|
* Time, in ms, before a tooltip should open on hover.
|
||
|
* @option
|
||
|
* @example 200
|
||
|
*/
|
||
|
hoverDelay: 200,
|
||
|
/**
|
||
|
* Time, in ms, a tooltip should take to fade into view.
|
||
|
* @option
|
||
|
* @example 150
|
||
|
*/
|
||
|
fadeInDuration: 150,
|
||
|
/**
|
||
|
* Time, in ms, a tooltip should take to fade out of view.
|
||
|
* @option
|
||
|
* @example 150
|
||
|
*/
|
||
|
fadeOutDuration: 150,
|
||
|
/**
|
||
|
* Disables hover events from opening the tooltip if set to true
|
||
|
* @option
|
||
|
* @example false
|
||
|
*/
|
||
|
disableHover: false,
|
||
|
/**
|
||
|
* Optional addtional classes to apply to the tooltip template on init.
|
||
|
* @option
|
||
|
* @example 'my-cool-tip-class'
|
||
|
*/
|
||
|
templateClasses: '',
|
||
|
/**
|
||
|
* Non-optional class added to tooltip templates. Foundation default is 'tooltip'.
|
||
|
* @option
|
||
|
* @example 'tooltip'
|
||
|
*/
|
||
|
tooltipClass: 'tooltip',
|
||
|
/**
|
||
|
* Class applied to the tooltip anchor element.
|
||
|
* @option
|
||
|
* @example 'has-tip'
|
||
|
*/
|
||
|
triggerClass: 'has-tip',
|
||
|
/**
|
||
|
* Minimum breakpoint size at which to open the tooltip.
|
||
|
* @option
|
||
|
* @example 'small'
|
||
|
*/
|
||
|
showOn: 'small',
|
||
|
/**
|
||
|
* Custom template to be used to generate markup for tooltip.
|
||
|
* @option
|
||
|
* @example '<div class="tooltip"></div>'
|
||
|
*/
|
||
|
template: '',
|
||
|
/**
|
||
|
* Text displayed in the tooltip template on open.
|
||
|
* @option
|
||
|
* @example 'Some cool space fact here.'
|
||
|
*/
|
||
|
tipText: '',
|
||
|
touchCloseText: 'Tap to close.',
|
||
|
/**
|
||
|
* Allows the tooltip to remain open if triggered with a click or touch event.
|
||
|
* @option
|
||
|
* @example true
|
||
|
*/
|
||
|
clickOpen: true,
|
||
|
/**
|
||
|
* Additional positioning classes, set by the JS
|
||
|
* @option
|
||
|
* @example 'top'
|
||
|
*/
|
||
|
positionClass: '',
|
||
|
/**
|
||
|
* Distance, in pixels, the template should push away from the anchor on the Y axis.
|
||
|
* @option
|
||
|
* @example 10
|
||
|
*/
|
||
|
vOffset: 10,
|
||
|
/**
|
||
|
* Distance, in pixels, the template should push away from the anchor on the X axis, if aligned to a side.
|
||
|
* @option
|
||
|
* @example 12
|
||
|
*/
|
||
|
hOffset: 12
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* TODO utilize resize event trigger
|
||
|
*/
|
||
|
|
||
|
// Window exports
|
||
|
Foundation.plugin(Tooltip, 'Tooltip');
|
||
|
|
||
|
}(jQuery);
|