/** * 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; });