This repository has been archived on 2018-10-12. You can view files and clone it, but cannot push or open issues or pull requests.
node-task/public/js/ink.tooltip.js

458 lines
20 KiB
JavaScript

/**
* Content Tooltips
* @module Ink.UI.Tooltip_1
* @version 1
*/
Ink.createModule('Ink.UI.Tooltip', '1', ['Ink.UI.Common_1', 'Ink.Dom.Event_1', 'Ink.Dom.Element_1', 'Ink.Dom.Selector_1', 'Ink.Util.Array_1', 'Ink.Dom.Css_1', 'Ink.Dom.Browser_1'], function (Common, InkEvent, InkElement, Selector, InkArray, Css) {
'use strict';
/**
* Tooltips are useful as a means to display information about functionality while avoiding clutter.
*
* Tooltips show up when you hover elements which "have" tooltips.
*
* This class will "give" a tooltip to many elements, selected by its first argument (`target`). This is contrary to the other UI modules in Ink, which are created once per element.
*
* You can define options either through the second argument of the Tooltip constructor, or as data-attributes in each `target` element. Options set through data-attributes all start with "data-tip", and override options passed into the Tooltip constructor.
*
* @class Ink.UI.Tooltip
* @constructor
*
* @param {DOMElement|String} target Target element or selector of elements, to display the tooltips on.
* @param {Object} [options] Options object
* @param {String} [options.text] Text content for the tooltip.
* @param {String} [options.html] HTML for the tooltip. Same as above, but won't escape HTML.
* @param {String} [options.where] Positioning for the tooltip. Options are 'up', 'down', 'left', 'right', 'mousemove' (follows the cursor), and 'mousefix' (stays fixed). Defaults to 'up'.
*
* @param {String} [options.color] Color of the tooltip. Options are red, orange, blue, green and black. Default is white.
* @param {Number} [options.fade] Number of seconds to fade in/out. Defaults to 0.3.
* @param {Boolean} [options.forever] Flag to prevent the tooltip from being erased when the mouse hovers away from the target.
* @param {Number} [options.timeout] Number of seconds the tooltip will stay open. Useful together with options.forever. Defaults to 0.
* @param {Number} [options.delay] Time the tooltip waits until it is displayed. Useful to avoid getting the attention of the user unnecessarily
* @param {DOMElement|Selector} [options.template] Element or selector containing HTML to be cloned into the tooltips. Can be a hidden element, because CSS `display` is set to `block`.
* @param {String} [options.templatefield] Selector within the template element to choose where the text is inserted into the tooltip. Useful when a wrapper DIV is required.
* @param {Number} [options.left] Spacing from the target to the tooltip, when `where` is `mousemove` or `mousefix`. Defaults to 10.
* @param {Number} [options.top] Spacing from the target to the tooltip, when `where` is `mousemove` or `mousefix`. Defaults to 10.
* @param {Number} [options.spacing] Spacing between the tooltip and the target element, when `where` is not `mousemove` or `mousefix`. Defaults to 8.
*
* @sample Ink_UI_Tooltip_1.html
*/
function Tooltip(element, options) {
this._init(element, options || {});
}
function EachTooltip(root, elm) {
this._init(root, elm);
}
var transitionDurationName,
transitionPropertyName,
transitionTimingFunctionName;
(function () { // Feature detection
var test = document.createElement('DIV');
var names = ['transition', 'oTransition', 'msTransition', 'mozTransition',
'webkitTransition'];
for (var i = 0; i < names.length; i++) {
if (typeof test.style[names[i] + 'Duration'] !== 'undefined') {
transitionDurationName = names[i] + 'Duration';
transitionPropertyName = names[i] + 'Property';
transitionTimingFunctionName = names[i] + 'TimingFunction';
break;
}
}
}());
// Body or documentElement
var bodies = document.getElementsByTagName('body');
var body = bodies.length ? bodies[0] : document.documentElement;
Tooltip.prototype = {
_init: function(element, options) {
var elements;
this.options = Ink.extendObj({
where: 'up',
zIndex: 10000,
left: 10,
top: 10,
spacing: 8,
forever: 0,
color: '',
timeout: 0,
delay: 0,
template: null,
templatefield: null,
fade: 0.3,
text: ''
}, options || {});
if (typeof element === 'string') {
elements = Selector.select(element);
} else if (typeof element === 'object') {
elements = [element];
} else {
throw 'Element expected';
}
this.tooltips = [];
for (var i = 0, len = elements.length; i < len; i++) {
this.tooltips[i] = new EachTooltip(this, elements[i]);
}
},
/**
* Destroys the tooltips created by this instance
*
* @method destroy
*/
destroy: function () {
InkArray.each(this.tooltips, function (tooltip) {
tooltip._destroy();
});
this.tooltips = null;
this.options = null;
}
};
EachTooltip.prototype = {
_oppositeDirections: {
left: 'right',
right: 'left',
up: 'down',
down: 'up'
},
_init: function(root, elm) {
InkEvent.observe(elm, 'mouseover', Ink.bindEvent(this._onMouseOver, this));
InkEvent.observe(elm, 'mouseout', Ink.bindEvent(this._onMouseOut, this));
InkEvent.observe(elm, 'mousemove', Ink.bindEvent(this._onMouseMove, this));
this.root = root;
this.element = elm;
this._delayTimeout = null;
this.tooltip = null;
Common.registerInstance(this, this.element);
},
_makeTooltip: function (mousePosition) {
if (!this._getOpt('text') &&
!this._getOpt('html') &&
!InkElement.hasAttribute(this.element, 'title')) {
return false;
}
var tooltip = this._createTooltipElement();
if (this.tooltip) {
this._removeTooltip();
}
this.tooltip = tooltip;
this._fadeInTooltipElement(tooltip);
this._placeTooltipElement(tooltip, mousePosition);
InkEvent.observe(tooltip, 'mouseover', Ink.bindEvent(this._onTooltipMouseOver, this));
var timeout = this._getFloatOpt('timeout');
if (timeout) {
setTimeout(Ink.bind(function () {
if (this.tooltip === tooltip) {
this._removeTooltip();
}
}, this), timeout * 1000);
}
},
_createTooltipElement: function () {
var template = this._getOpt('template'), // User template instead of our HTML
templatefield = this._getOpt('templatefield'),
tooltip, // The element we float
field; // Element where we write our message. Child or same as the above
if (template) { // The user told us of a template to use. We copy it.
var temp = document.createElement('DIV');
temp.innerHTML = Common.elOrSelector(template, 'options.template').outerHTML;
tooltip = temp.firstChild;
if (templatefield) {
field = Selector.select(templatefield, tooltip);
if (field) {
field = field[0];
} else {
throw 'options.templatefield must be a valid selector within options.template';
}
} else {
field = tooltip; // Assume same element if user did not specify a field
}
} else { // We create the default structure
tooltip = document.createElement('DIV');
Css.addClassName(tooltip, 'ink-tooltip');
Css.addClassName(tooltip, this._getOpt('color'));
field = document.createElement('DIV');
Css.addClassName(field, 'content');
tooltip.appendChild(field);
}
if (this._getOpt('html')) {
field.innerHTML = this._getOpt('html');
} else if (this._getOpt('text')) {
InkElement.setTextContent(field, this._getOpt('text'));
} else {
InkElement.setTextContent(field, this.element.getAttribute('title'));
}
tooltip.style.display = 'block';
tooltip.style.position = 'absolute';
tooltip.style.zIndex = this._getIntOpt('zIndex');
return tooltip;
},
_fadeInTooltipElement: function (tooltip) {
var fadeTime = this._getFloatOpt('fade');
if (transitionDurationName && fadeTime) {
tooltip.style.opacity = '0';
tooltip.style[transitionDurationName] = fadeTime + 's';
tooltip.style[transitionPropertyName] = 'opacity';
tooltip.style[transitionTimingFunctionName] = 'ease-in-out';
setTimeout(function () {
tooltip.style.opacity = '1';
}, 0); // Wait a tick
}
},
_placeTooltipElement: function (tooltip, mousePosition) {
var where = this._getOpt('where');
if (where === 'mousemove' || where === 'mousefix') {
var mPos = mousePosition;
this._setPos(mPos[0], mPos[1]);
body.appendChild(tooltip);
} else if (where.match(/(up|down|left|right)/)) {
body.appendChild(tooltip);
var targetElementPos = InkElement.offset(this.element);
var tleft = targetElementPos[0],
ttop = targetElementPos[1];
var centerh = (InkElement.elementWidth(this.element) / 2) - (InkElement.elementWidth(tooltip) / 2),
centerv = (InkElement.elementHeight(this.element) / 2) - (InkElement.elementHeight(tooltip) / 2);
var spacing = this._getIntOpt('spacing');
var tooltipDims = InkElement.elementDimensions(tooltip);
var elementDims = InkElement.elementDimensions(this.element);
var maxX = InkElement.scrollWidth() + InkElement.viewportWidth();
var maxY = InkElement.scrollHeight() + InkElement.viewportHeight();
where = this._getWhereValueInsideViewport(where, {
left: tleft - tooltipDims[0],
right: tleft + tooltipDims[0],
top: ttop + tooltipDims[1],
bottom: ttop + tooltipDims[1]
}, {
right: maxX,
bottom: maxY
});
if (where === 'up') {
ttop -= tooltipDims[1];
ttop -= spacing;
tleft += centerh;
} else if (where === 'down') {
ttop += elementDims[1];
ttop += spacing;
tleft += centerh;
} else if (where === 'left') {
tleft -= tooltipDims[0];
tleft -= spacing;
ttop += centerv;
} else if (where === 'right') {
tleft += elementDims[0];
tleft += spacing;
ttop += centerv;
}
var arrow = null;
if (where.match(/(up|down|left|right)/)) {
arrow = document.createElement('SPAN');
Css.addClassName(arrow, 'arrow');
Css.addClassName(arrow, this._oppositeDirections[where]);
tooltip.appendChild(arrow);
}
var tooltipLeft = tleft;
var tooltipTop = ttop;
var toBottom = (tooltipTop + tooltipDims[1]) - maxY;
var toRight = (tooltipLeft + tooltipDims[0]) - maxX;
var toLeft = 0 - tooltipLeft;
var toTop = 0 - tooltipTop;
if (toBottom > 0) {
if (arrow) { arrow.style.top = (tooltipDims[1] / 2) + toBottom + 'px'; }
tooltipTop -= toBottom;
} else if (toTop > 0) {
if (arrow) { arrow.style.top = (tooltipDims[1] / 2) - toTop + 'px'; }
tooltipTop += toTop;
} else if (toRight > 0) {
if (arrow) { arrow.style.left = (tooltipDims[0] / 2) + toRight + 'px'; }
tooltipLeft -= toRight;
} else if (toLeft > 0) {
if (arrow) { arrow.style.left = (tooltipDims[0] / 2) - toLeft + 'px'; }
tooltipLeft += toLeft;
}
tooltip.style.left = tooltipLeft + 'px';
tooltip.style.top = tooltipTop + 'px';
}
},
/**
* Get a value for "where" (left/right/up/down) which doesn't put the
* tooltip off the screen
*
* @method _getWhereValueInsideViewport
* @param where {String} "where" value which was given by the user and we might change
* @param bbox {BoundingBox} A bounding box like what you get from getBoundingClientRect ({top, bottom, left, right}) with pixel positions from the top left corner of the viewport.
* @param viewport {BoundingBox} Bounding box for the viewport. "top" and "left" are omitted because these coordinates are relative to the top-left corner of the viewport so they are zero.
*
* @TODO: we can't use getBoundingClientRect in this case because it returns {0,0,0,0} on our uncreated tooltip.
*/
_getWhereValueInsideViewport: function (where, bbox, viewport) {
if (where === 'left' && bbox.left < 0) {
return 'right';
} else if (where === 'right' && bbox.right > viewport.right) {
return 'left';
} else if (where === 'up' && bbox.top < 0) {
return 'down';
} else if (where === 'down' && bbox.bottom > viewport.bottom) {
return 'up';
}
return where;
},
_removeTooltip: function() {
var tooltip = this.tooltip;
if (!tooltip) {return;}
var remove = Ink.bind(InkElement.remove, {}, tooltip);
if (this._getOpt('where') !== 'mousemove' && transitionDurationName) {
tooltip.style.opacity = 0;
// remove() will operate on correct tooltip, although this.tooltip === null then
setTimeout(remove, this._getFloatOpt('fade') * 1000);
} else {
remove();
}
this.tooltip = null;
},
_getOpt: function (option) {
var dataAttrVal = InkElement.data(this.element)[InkElement._camelCase('tip-' + option)];
if (dataAttrVal /* either null or "" may signify the absense of this attribute*/) {
return dataAttrVal;
}
var instanceOption = this.root.options[option];
if (typeof instanceOption !== 'undefined') {
return instanceOption;
}
},
_getIntOpt: function (option) {
return parseInt(this._getOpt(option), 10);
},
_getFloatOpt: function (option) {
return parseFloat(this._getOpt(option), 10);
},
_destroy: function () {
if (this.tooltip) {
InkElement.remove(this.tooltip);
}
this.root = null; // Cyclic reference = memory leaks
this.element = null;
this.tooltip = null;
},
_onMouseOver: function(e) {
// on IE < 10 you can't access the mouse event not even a tick after it fired
var mousePosition = this._getMousePosition(e);
var delay = this._getFloatOpt('delay');
if (delay) {
this._delayTimeout = setTimeout(Ink.bind(function () {
if (!this.tooltip) {
this._makeTooltip(mousePosition);
}
this._delayTimeout = null;
}, this), delay * 1000);
} else {
this._makeTooltip(mousePosition);
}
},
_onMouseMove: function(e) {
if (this._getOpt('where') === 'mousemove' && this.tooltip) {
var mPos = this._getMousePosition(e);
this._setPos(mPos[0], mPos[1]);
}
},
_onMouseOut: function () {
if (!this._getIntOpt('forever')) {
this._removeTooltip();
}
if (this._delayTimeout) {
clearTimeout(this._delayTimeout);
this._delayTimeout = null;
}
},
_onTooltipMouseOver: function () {
if (this.tooltip) { // If tooltip is already being removed, this has no effect
this._removeTooltip();
}
},
_setPos: function(left, top) {
left += this._getIntOpt('left');
top += this._getIntOpt('top');
var pageDims = this._getPageXY();
if (this.tooltip) {
var elmDims = [InkElement.elementWidth(this.tooltip), InkElement.elementHeight(this.tooltip)];
var scrollDim = this._getScroll();
if((elmDims[0] + left - scrollDim[0]) >= (pageDims[0] - 20)) {
left = (left - elmDims[0] - this._getIntOpt('left') - 10);
}
if((elmDims[1] + top - scrollDim[1]) >= (pageDims[1] - 20)) {
top = (top - elmDims[1] - this._getIntOpt('top') - 10);
}
this.tooltip.style.left = left + 'px';
this.tooltip.style.top = top + 'px';
}
},
_getPageXY: function() {
var cWidth = 0;
var cHeight = 0;
if( typeof( window.innerWidth ) === 'number' ) {
cWidth = window.innerWidth;
cHeight = window.innerHeight;
} else if( document.documentElement && ( document.documentElement.clientWidth || document.documentElement.clientHeight ) ) {
cWidth = document.documentElement.clientWidth;
cHeight = document.documentElement.clientHeight;
} else if( document.body && ( document.body.clientWidth || document.body.clientHeight ) ) {
cWidth = document.body.clientWidth;
cHeight = document.body.clientHeight;
}
return [parseInt(cWidth, 10), parseInt(cHeight, 10)];
},
_getScroll: function() {
var dd = document.documentElement, db = document.body;
if (dd && (dd.scrollLeft || dd.scrollTop)) {
return [dd.scrollLeft, dd.scrollTop];
} else if (db) {
return [db.scrollLeft, db.scrollTop];
} else {
return [0, 0];
}
},
_getMousePosition: function(e) {
return [parseInt(InkEvent.pointerX(e), 10), parseInt(InkEvent.pointerY(e), 10)];
}
};
return Tooltip;
});