diff options
Diffstat (limited to 'jquery/jquery.powertip-1.2.0.js')
-rw-r--r-- | jquery/jquery.powertip-1.2.0.js | 1166 |
1 files changed, 1166 insertions, 0 deletions
diff --git a/jquery/jquery.powertip-1.2.0.js b/jquery/jquery.powertip-1.2.0.js new file mode 100644 index 0000000..07e87fe --- /dev/null +++ b/jquery/jquery.powertip-1.2.0.js @@ -0,0 +1,1166 @@ +/*! + PowerTip - v1.2.0 - 2013-04-03 + http://stevenbenner.github.com/jquery-powertip/ + Copyright (c) 2013 Steven Benner (http://stevenbenner.com/). + Released under MIT license. + https://raw.github.com/stevenbenner/jquery-powertip/master/LICENSE.txt +*/ +(function(factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else { + // Browser globals + factory(jQuery); + } +}(function($) { + + // useful private variables + var $document = $(document), + $window = $(window), + $body = $('body'); + + // constants + var DATA_DISPLAYCONTROLLER = 'displayController', + DATA_HASACTIVEHOVER = 'hasActiveHover', + DATA_FORCEDOPEN = 'forcedOpen', + DATA_HASMOUSEMOVE = 'hasMouseMove', + DATA_MOUSEONTOTIP = 'mouseOnToPopup', + DATA_ORIGINALTITLE = 'originalTitle', + DATA_POWERTIP = 'powertip', + DATA_POWERTIPJQ = 'powertipjq', + DATA_POWERTIPTARGET = 'powertiptarget', + RAD2DEG = 180 / Math.PI; + + /** + * Session data + * Private properties global to all powerTip instances + */ + var session = { + isTipOpen: false, + isFixedTipOpen: false, + isClosing: false, + tipOpenImminent: false, + activeHover: null, + currentX: 0, + currentY: 0, + previousX: 0, + previousY: 0, + desyncTimeout: null, + mouseTrackingActive: false, + delayInProgress: false, + windowWidth: 0, + windowHeight: 0, + scrollTop: 0, + scrollLeft: 0 + }; + + /** + * Collision enumeration + * @enum {number} + */ + var Collision = { + none: 0, + top: 1, + bottom: 2, + left: 4, + right: 8 + }; + + /** + * Display hover tooltips on the matched elements. + * @param {(Object|string)} opts The options object to use for the plugin, or + * the name of a method to invoke on the first matched element. + * @param {*=} [arg] Argument for an invoked method (optional). + * @return {jQuery} jQuery object for the matched selectors. + */ + $.fn.powerTip = function(opts, arg) { + // don't do any work if there were no matched elements + if (!this.length) { + return this; + } + + // handle api method calls on the plugin, e.g. powerTip('hide') + if ($.type(opts) === 'string' && $.powerTip[opts]) { + return $.powerTip[opts].call(this, this, arg); + } + + // extend options and instantiate TooltipController + var options = $.extend({}, $.fn.powerTip.defaults, opts), + tipController = new TooltipController(options); + + // hook mouse and viewport dimension tracking + initTracking(); + + // setup the elements + this.each(function elementSetup() { + var $this = $(this), + dataPowertip = $this.data(DATA_POWERTIP), + dataElem = $this.data(DATA_POWERTIPJQ), + dataTarget = $this.data(DATA_POWERTIPTARGET), + title; + + // handle repeated powerTip calls on the same element by destroying the + // original instance hooked to it and replacing it with this call + if ($this.data(DATA_DISPLAYCONTROLLER)) { + $.powerTip.destroy($this); + } + + // attempt to use title attribute text if there is no data-powertip, + // data-powertipjq or data-powertiptarget. If we do use the title + // attribute, delete the attribute so the browser will not show it + title = $this.attr('title'); + if (!dataPowertip && !dataTarget && !dataElem && title) { + $this.data(DATA_POWERTIP, title); + $this.data(DATA_ORIGINALTITLE, title); + $this.removeAttr('title'); + } + + // create hover controllers for each element + $this.data( + DATA_DISPLAYCONTROLLER, + new DisplayController($this, options, tipController) + ); + }); + + // attach events to matched elements if the manual options is not enabled + if (!options.manual) { + this.on({ + // mouse events + 'mouseenter.powertip': function elementMouseEnter(event) { + $.powerTip.show(this, event); + }, + 'mouseleave.powertip': function elementMouseLeave() { + $.powerTip.hide(this); + }, + // keyboard events + 'focus.powertip': function elementFocus() { + $.powerTip.show(this); + }, + 'blur.powertip': function elementBlur() { + $.powerTip.hide(this, true); + }, + 'keydown.powertip': function elementKeyDown(event) { + // close tooltip when the escape key is pressed + if (event.keyCode === 27) { + $.powerTip.hide(this, true); + } + } + }); + } + + return this; + }; + + /** + * Default options for the powerTip plugin. + */ + $.fn.powerTip.defaults = { + fadeInTime: 200, + fadeOutTime: 100, + followMouse: false, + popupId: 'powerTip', + intentSensitivity: 7, + intentPollInterval: 100, + closeDelay: 100, + placement: 'n', + smartPlacement: false, + offset: 10, + mouseOnToPopup: false, + manual: false + }; + + /** + * Default smart placement priority lists. + * The first item in the array is the highest priority, the last is the lowest. + * The last item is also the default, which will be used if all previous options + * do not fit. + */ + $.fn.powerTip.smartPlacementLists = { + n: ['n', 'ne', 'nw', 's'], + e: ['e', 'ne', 'se', 'w', 'nw', 'sw', 'n', 's', 'e'], + s: ['s', 'se', 'sw', 'n'], + w: ['w', 'nw', 'sw', 'e', 'ne', 'se', 'n', 's', 'w'], + nw: ['nw', 'w', 'sw', 'n', 's', 'se', 'nw'], + ne: ['ne', 'e', 'se', 'n', 's', 'sw', 'ne'], + sw: ['sw', 'w', 'nw', 's', 'n', 'ne', 'sw'], + se: ['se', 'e', 'ne', 's', 'n', 'nw', 'se'], + 'nw-alt': ['nw-alt', 'n', 'ne-alt', 'sw-alt', 's', 'se-alt', 'w', 'e'], + 'ne-alt': ['ne-alt', 'n', 'nw-alt', 'se-alt', 's', 'sw-alt', 'e', 'w'], + 'sw-alt': ['sw-alt', 's', 'se-alt', 'nw-alt', 'n', 'ne-alt', 'w', 'e'], + 'se-alt': ['se-alt', 's', 'sw-alt', 'ne-alt', 'n', 'nw-alt', 'e', 'w'] + }; + + /** + * Public API + */ + $.powerTip = { + /** + * Attempts to show the tooltip for the specified element. + * @param {jQuery|Element} element The element to open the tooltip for. + * @param {jQuery.Event=} event jQuery event for hover intent and mouse + * tracking (optional). + */ + show: function apiShowTip(element, event) { + if (event) { + trackMouse(event); + session.previousX = event.pageX; + session.previousY = event.pageY; + $(element).data(DATA_DISPLAYCONTROLLER).show(); + } else { + $(element).first().data(DATA_DISPLAYCONTROLLER).show(true, true); + } + return element; + }, + + /** + * Repositions the tooltip on the element. + * @param {jQuery|Element} element The element the tooltip is shown for. + */ + reposition: function apiResetPosition(element) { + $(element).first().data(DATA_DISPLAYCONTROLLER).resetPosition(); + return element; + }, + + /** + * Attempts to close any open tooltips. + * @param {(jQuery|Element)=} element The element with the tooltip that + * should be closed (optional). + * @param {boolean=} immediate Disable close delay (optional). + */ + hide: function apiCloseTip(element, immediate) { + if (element) { + $(element).first().data(DATA_DISPLAYCONTROLLER).hide(immediate); + } else { + if (session.activeHover) { + session.activeHover.data(DATA_DISPLAYCONTROLLER).hide(true); + } + } + return element; + }, + + /** + * Destroy and roll back any powerTip() instance on the specified element. + * @param {jQuery|Element} element The element with the powerTip instance. + */ + destroy: function apiDestroy(element) { + $(element).off('.powertip').each(function destroy() { + var $this = $(this), + dataAttributes = [ + DATA_ORIGINALTITLE, + DATA_DISPLAYCONTROLLER, + DATA_HASACTIVEHOVER, + DATA_FORCEDOPEN + ]; + + if ($this.data(DATA_ORIGINALTITLE)) { + $this.attr('title', $this.data(DATA_ORIGINALTITLE)); + dataAttributes.push(DATA_POWERTIP); + } + + $this.removeData(dataAttributes); + }); + return element; + } + }; + + // API aliasing + $.powerTip.showTip = $.powerTip.show; + $.powerTip.closeTip = $.powerTip.hide; + + /** + * Creates a new CSSCoordinates object. + * @private + * @constructor + */ + function CSSCoordinates() { + var me = this; + + // initialize object properties + me.top = 'auto'; + me.left = 'auto'; + me.right = 'auto'; + me.bottom = 'auto'; + + /** + * Set a property to a value. + * @private + * @param {string} property The name of the property. + * @param {number} value The value of the property. + */ + me.set = function(property, value) { + if ($.isNumeric(value)) { + me[property] = Math.round(value); + } + }; + } + + /** + * Creates a new tooltip display controller. + * @private + * @constructor + * @param {jQuery} element The element that this controller will handle. + * @param {Object} options Options object containing settings. + * @param {TooltipController} tipController The TooltipController object for + * this instance. + */ + function DisplayController(element, options, tipController) { + var hoverTimer = null; + + /** + * Begins the process of showing a tooltip. + * @private + * @param {boolean=} immediate Skip intent testing (optional). + * @param {boolean=} forceOpen Ignore cursor position and force tooltip to + * open (optional). + */ + function openTooltip(immediate, forceOpen) { + cancelTimer(); + if (!element.data(DATA_HASACTIVEHOVER)) { + if (!immediate) { + session.tipOpenImminent = true; + hoverTimer = setTimeout( + function intentDelay() { + hoverTimer = null; + checkForIntent(); + }, + options.intentPollInterval + ); + } else { + if (forceOpen) { + element.data(DATA_FORCEDOPEN, true); + } + tipController.showTip(element); + } + } + } + + /** + * Begins the process of closing a tooltip. + * @private + * @param {boolean=} disableDelay Disable close delay (optional). + */ + function closeTooltip(disableDelay) { + cancelTimer(); + session.tipOpenImminent = false; + if (element.data(DATA_HASACTIVEHOVER)) { + element.data(DATA_FORCEDOPEN, false); + if (!disableDelay) { + session.delayInProgress = true; + hoverTimer = setTimeout( + function closeDelay() { + hoverTimer = null; + tipController.hideTip(element); + session.delayInProgress = false; + }, + options.closeDelay + ); + } else { + tipController.hideTip(element); + } + } + } + + /** + * Checks mouse position to make sure that the user intended to hover on the + * specified element before showing the tooltip. + * @private + */ + function checkForIntent() { + // calculate mouse position difference + var xDifference = Math.abs(session.previousX - session.currentX), + yDifference = Math.abs(session.previousY - session.currentY), + totalDifference = xDifference + yDifference; + + // check if difference has passed the sensitivity threshold + if (totalDifference < options.intentSensitivity) { + tipController.showTip(element); + } else { + // try again + session.previousX = session.currentX; + session.previousY = session.currentY; + openTooltip(); + } + } + + /** + * Cancels active hover timer. + * @private + */ + function cancelTimer() { + hoverTimer = clearTimeout(hoverTimer); + session.delayInProgress = false; + } + + /** + * Repositions the tooltip on this element. + * @private + */ + function repositionTooltip() { + tipController.resetPosition(element); + } + + // expose the methods + this.show = openTooltip; + this.hide = closeTooltip; + this.cancel = cancelTimer; + this.resetPosition = repositionTooltip; + } + + /** + * Creates a new Placement Calculator. + * @private + * @constructor + */ + function PlacementCalculator() { + /** + * Compute the CSS position to display a tooltip at the specified placement + * relative to the specified element. + * @private + * @param {jQuery} element The element that the tooltip should target. + * @param {string} placement The placement for the tooltip. + * @param {number} tipWidth Width of the tooltip element in pixels. + * @param {number} tipHeight Height of the tooltip element in pixels. + * @param {number} offset Distance to offset tooltips in pixels. + * @return {CSSCoordinates} A CSSCoordinates object with the position. + */ + function computePlacementCoords(element, placement, tipWidth, tipHeight, offset) { + var placementBase = placement.split('-')[0], // ignore 'alt' for corners + coords = new CSSCoordinates(), + position; + + if (isSvgElement(element)) { + position = getSvgPlacement(element, placementBase); + } else { + position = getHtmlPlacement(element, placementBase); + } + + // calculate the appropriate x and y position in the document + switch (placement) { + case 'n': + coords.set('left', position.left - (tipWidth / 2)); + coords.set('bottom', session.windowHeight - position.top + offset); + break; + case 'e': + coords.set('left', position.left + offset); + coords.set('top', position.top - (tipHeight / 2)); + break; + case 's': + coords.set('left', position.left - (tipWidth / 2)); + coords.set('top', position.top + offset); + break; + case 'w': + coords.set('top', position.top - (tipHeight / 2)); + coords.set('right', session.windowWidth - position.left + offset); + break; + case 'nw': + coords.set('bottom', session.windowHeight - position.top + offset); + coords.set('right', session.windowWidth - position.left - 20); + break; + case 'nw-alt': + coords.set('left', position.left); + coords.set('bottom', session.windowHeight - position.top + offset); + break; + case 'ne': + coords.set('left', position.left - 20); + coords.set('bottom', session.windowHeight - position.top + offset); + break; + case 'ne-alt': + coords.set('bottom', session.windowHeight - position.top + offset); + coords.set('right', session.windowWidth - position.left); + break; + case 'sw': + coords.set('top', position.top + offset); + coords.set('right', session.windowWidth - position.left - 20); + break; + case 'sw-alt': + coords.set('left', position.left); + coords.set('top', position.top + offset); + break; + case 'se': + coords.set('left', position.left - 20); + coords.set('top', position.top + offset); + break; + case 'se-alt': + coords.set('top', position.top + offset); + coords.set('right', session.windowWidth - position.left); + break; + } + + return coords; + } + + /** + * Finds the tooltip attachment point in the document for a HTML DOM element + * for the specified placement. + * @private + * @param {jQuery} element The element that the tooltip should target. + * @param {string} placement The placement for the tooltip. + * @return {Object} An object with the top,left position values. + */ + function getHtmlPlacement(element, placement) { + var objectOffset = element.offset(), + objectWidth = element.outerWidth(), + objectHeight = element.outerHeight(), + left, + top; + + // calculate the appropriate x and y position in the document + switch (placement) { + case 'n': + left = objectOffset.left + objectWidth / 2; + top = objectOffset.top; + break; + case 'e': + left = objectOffset.left + objectWidth; + top = objectOffset.top + objectHeight / 2; + break; + case 's': + left = objectOffset.left + objectWidth / 2; + top = objectOffset.top + objectHeight; + break; + case 'w': + left = objectOffset.left; + top = objectOffset.top + objectHeight / 2; + break; + case 'nw': + left = objectOffset.left; + top = objectOffset.top; + break; + case 'ne': + left = objectOffset.left + objectWidth; + top = objectOffset.top; + break; + case 'sw': + left = objectOffset.left; + top = objectOffset.top + objectHeight; + break; + case 'se': + left = objectOffset.left + objectWidth; + top = objectOffset.top + objectHeight; + break; + } + + return { + top: top, + left: left + }; + } + + /** + * Finds the tooltip attachment point in the document for a SVG element for + * the specified placement. + * @private + * @param {jQuery} element The element that the tooltip should target. + * @param {string} placement The placement for the tooltip. + * @return {Object} An object with the top,left position values. + */ + function getSvgPlacement(element, placement) { + var svgElement = element.closest('svg')[0], + domElement = element[0], + point = svgElement.createSVGPoint(), + boundingBox = domElement.getBBox(), + matrix = domElement.getScreenCTM(), + halfWidth = boundingBox.width / 2, + halfHeight = boundingBox.height / 2, + placements = [], + placementKeys = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'], + coords, + rotation, + steps, + x; + + function pushPlacement() { + placements.push(point.matrixTransform(matrix)); + } + + // get bounding box corners and midpoints + point.x = boundingBox.x; + point.y = boundingBox.y; + pushPlacement(); + point.x += halfWidth; + pushPlacement(); + point.x += halfWidth; + pushPlacement(); + point.y += halfHeight; + pushPlacement(); + point.y += halfHeight; + pushPlacement(); + point.x -= halfWidth; + pushPlacement(); + point.x -= halfWidth; + pushPlacement(); + point.y -= halfHeight; + pushPlacement(); + + // determine rotation + if (placements[0].y !== placements[1].y || placements[0].x !== placements[7].x) { + rotation = Math.atan2(matrix.b, matrix.a) * RAD2DEG; + steps = Math.ceil(((rotation % 360) - 22.5) / 45); + if (steps < 1) { + steps += 8; + } + while (steps--) { + placementKeys.push(placementKeys.shift()); + } + } + + // find placement + for (x = 0; x < placements.length; x++) { + if (placementKeys[x] === placement) { + coords = placements[x]; + break; + } + } + + return { + top: coords.y + session.scrollTop, + left: coords.x + session.scrollLeft + }; + } + + // expose methods + this.compute = computePlacementCoords; + } + + /** + * Creates a new tooltip controller. + * @private + * @constructor + * @param {Object} options Options object containing settings. + */ + function TooltipController(options) { + var placementCalculator = new PlacementCalculator(), + tipElement = $('#' + options.popupId); + + // build and append tooltip div if it does not already exist + if (tipElement.length === 0) { + tipElement = $('<div/>', { id: options.popupId }); + // grab body element if it was not populated when the script loaded + // note: this hack exists solely for jsfiddle support + if ($body.length === 0) { + $body = $('body'); + } + $body.append(tipElement); + } + + // hook mousemove for cursor follow tooltips + if (options.followMouse) { + // only one positionTipOnCursor hook per tooltip element, please + if (!tipElement.data(DATA_HASMOUSEMOVE)) { + $document.on('mousemove', positionTipOnCursor); + $window.on('scroll', positionTipOnCursor); + tipElement.data(DATA_HASMOUSEMOVE, true); + } + } + + // if we want to be able to mouse onto the tooltip then we need to attach + // hover events to the tooltip that will cancel a close request on hover and + // start a new close request on mouseleave + if (options.mouseOnToPopup) { + tipElement.on({ + mouseenter: function tipMouseEnter() { + // we only let the mouse stay on the tooltip if it is set to let + // users interact with it + if (tipElement.data(DATA_MOUSEONTOTIP)) { + // check activeHover in case the mouse cursor entered the + // tooltip during the fadeOut and close cycle + if (session.activeHover) { + session.activeHover.data(DATA_DISPLAYCONTROLLER).cancel(); + } + } + }, + mouseleave: function tipMouseLeave() { + // check activeHover in case the mouse cursor entered the + // tooltip during the fadeOut and close cycle + if (session.activeHover) { + session.activeHover.data(DATA_DISPLAYCONTROLLER).hide(); + } + } + }); + } + + /** + * Gives the specified element the active-hover state and queues up the + * showTip function. + * @private + * @param {jQuery} element The element that the tooltip should target. + */ + function beginShowTip(element) { + element.data(DATA_HASACTIVEHOVER, true); + // show tooltip, asap + tipElement.queue(function queueTipInit(next) { + showTip(element); + next(); + }); + } + + /** + * Shows the tooltip, as soon as possible. + * @private + * @param {jQuery} element The element that the tooltip should target. + */ + function showTip(element) { + var tipContent; + + // it is possible, especially with keyboard navigation, to move on to + // another element with a tooltip during the queue to get to this point + // in the code. if that happens then we need to not proceed or we may + // have the fadeout callback for the last tooltip execute immediately + // after this code runs, causing bugs. + if (!element.data(DATA_HASACTIVEHOVER)) { + return; + } + + // if the tooltip is open and we got asked to open another one then the + // old one is still in its fadeOut cycle, so wait and try again + if (session.isTipOpen) { + if (!session.isClosing) { + hideTip(session.activeHover); + } + tipElement.delay(100).queue(function queueTipAgain(next) { + showTip(element); + next(); + }); + return; + } + + // trigger powerTipPreRender event + element.trigger('powerTipPreRender'); + + // set tooltip content + tipContent = getTooltipContent(element); + if (tipContent) { + tipElement.empty().append(tipContent); + } else { + // we have no content to display, give up + return; + } + + // trigger powerTipRender event + element.trigger('powerTipRender'); + + session.activeHover = element; + session.isTipOpen = true; + + tipElement.data(DATA_MOUSEONTOTIP, options.mouseOnToPopup); + + // set tooltip position + if (!options.followMouse) { + positionTipOnElement(element); + session.isFixedTipOpen = true; + } else { + positionTipOnCursor(); + } + + // fadein + tipElement.fadeIn(options.fadeInTime, function fadeInCallback() { + // start desync polling + if (!session.desyncTimeout) { + session.desyncTimeout = setInterval(closeDesyncedTip, 500); + } + + // trigger powerTipOpen event + element.trigger('powerTipOpen'); + }); + } + + /** + * Hides the tooltip. + * @private + * @param {jQuery} element The element that the tooltip should target. + */ + function hideTip(element) { + // reset session + session.isClosing = true; + session.activeHover = null; + session.isTipOpen = false; + + // stop desync polling + session.desyncTimeout = clearInterval(session.desyncTimeout); + + // reset element state + element.data(DATA_HASACTIVEHOVER, false); + element.data(DATA_FORCEDOPEN, false); + + // fade out + tipElement.fadeOut(options.fadeOutTime, function fadeOutCallback() { + var coords = new CSSCoordinates(); + + // reset session and tooltip element + session.isClosing = false; + session.isFixedTipOpen = false; + tipElement.removeClass(); + + // support mouse-follow and fixed position tips at the same time by + // moving the tooltip to the last cursor location after it is hidden + coords.set('top', session.currentY + options.offset); + coords.set('left', session.currentX + options.offset); + tipElement.css(coords); + + // trigger powerTipClose event + element.trigger('powerTipClose'); + }); + } + + /** + * Moves the tooltip to the users mouse cursor. + * @private + */ + function positionTipOnCursor() { + // to support having fixed tooltips on the same page as cursor tooltips, + // where both instances are referencing the same tooltip element, we + // need to keep track of the mouse position constantly, but we should + // only set the tip location if a fixed tip is not currently open, a tip + // open is imminent or active, and the tooltip element in question does + // have a mouse-follow using it. + if (!session.isFixedTipOpen && (session.isTipOpen || (session.tipOpenImminent && tipElement.data(DATA_HASMOUSEMOVE)))) { + // grab measurements + var tipWidth = tipElement.outerWidth(), + tipHeight = tipElement.outerHeight(), + coords = new CSSCoordinates(), + collisions, + collisionCount; + + // grab collisions + coords.set('top', session.currentY + options.offset); + coords.set('left', session.currentX + options.offset); + collisions = getViewportCollisions( + coords, + tipWidth, + tipHeight + ); + + // handle tooltip view port collisions + if (collisions !== Collision.none) { + collisionCount = countFlags(collisions); + if (collisionCount === 1) { + // if there is only one collision (bottom or right) then + // simply constrain the tooltip to the view port + if (collisions === Collision.right) { + coords.set('left', session.windowWidth - tipWidth); + } else if (collisions === Collision.bottom) { + coords.set('top', session.scrollTop + session.windowHeight - tipHeight); + } + } else { + // if the tooltip has more than one collision then it is + // trapped in the corner and should be flipped to get it out + // of the users way + coords.set('left', session.currentX - tipWidth - options.offset); + coords.set('top', session.currentY - tipHeight - options.offset); + } + } + + // position the tooltip + tipElement.css(coords); + } + } + + /** + * Sets the tooltip to the correct position relative to the specified target + * element. Based on options settings. + * @private + * @param {jQuery} element The element that the tooltip should target. + */ + function positionTipOnElement(element) { + var priorityList, + finalPlacement; + + if (options.smartPlacement) { + priorityList = $.fn.powerTip.smartPlacementLists[options.placement]; + + // iterate over the priority list and use the first placement option + // that does not collide with the view port. if they all collide + // then the last placement in the list will be used. + $.each(priorityList, function(idx, pos) { + // place tooltip and find collisions + var collisions = getViewportCollisions( + placeTooltip(element, pos), + tipElement.outerWidth(), + tipElement.outerHeight() + ); + + // update the final placement variable + finalPlacement = pos; + + // break if there were no collisions + if (collisions === Collision.none) { + return false; + } + }); + } else { + // if we're not going to use the smart placement feature then just + // compute the coordinates and do it + placeTooltip(element, options.placement); + finalPlacement = options.placement; + } + + // add placement as class for CSS arrows + tipElement.addClass(finalPlacement); + } + + /** + * Sets the tooltip position to the appropriate values to show the tip at + * the specified placement. This function will iterate and test the tooltip + * to support elastic tooltips. + * @private + * @param {jQuery} element The element that the tooltip should target. + * @param {string} placement The placement for the tooltip. + * @return {CSSCoordinates} A CSSCoordinates object with the top, left, and + * right position values. + */ + function placeTooltip(element, placement) { + var iterationCount = 0, + tipWidth, + tipHeight, + coords = new CSSCoordinates(); + + // set the tip to 0,0 to get the full expanded width + coords.set('top', 0); + coords.set('left', 0); + tipElement.css(coords); + + // to support elastic tooltips we need to check for a change in the + // rendered dimensions after the tooltip has been positioned + do { + // grab the current tip dimensions + tipWidth = tipElement.outerWidth(); + tipHeight = tipElement.outerHeight(); + + // get placement coordinates + coords = placementCalculator.compute( + element, + placement, + tipWidth, + tipHeight, + options.offset + ); + + // place the tooltip + tipElement.css(coords); + } while ( + // sanity check: limit to 5 iterations, and... + ++iterationCount <= 5 && + // try again if the dimensions changed after placement + (tipWidth !== tipElement.outerWidth() || tipHeight !== tipElement.outerHeight()) + ); + + return coords; + } + + /** + * Checks for a tooltip desync and closes the tooltip if one occurs. + * @private + */ + function closeDesyncedTip() { + var isDesynced = false; + // It is possible for the mouse cursor to leave an element without + // firing the mouseleave or blur event. This most commonly happens when + // the element is disabled under mouse cursor. If this happens it will + // result in a desynced tooltip because the tooltip was never asked to + // close. So we should periodically check for a desync situation and + // close the tip if such a situation arises. + if (session.isTipOpen && !session.isClosing && !session.delayInProgress) { + // user moused onto another tip or active hover is disabled + if (session.activeHover.data(DATA_HASACTIVEHOVER) === false || session.activeHover.is(':disabled')) { + isDesynced = true; + } else { + // hanging tip - have to test if mouse position is not over the + // active hover and not over a tooltip set to let the user + // interact with it. + // for keyboard navigation: this only counts if the element does + // not have focus. + // for tooltips opened via the api: we need to check if it has + // the forcedOpen flag. + if (!isMouseOver(session.activeHover) && !session.activeHover.is(':focus') && !session.activeHover.data(DATA_FORCEDOPEN)) { + if (tipElement.data(DATA_MOUSEONTOTIP)) { + if (!isMouseOver(tipElement)) { + isDesynced = true; + } + } else { + isDesynced = true; + } + } + } + + if (isDesynced) { + // close the desynced tip + hideTip(session.activeHover); + } + } + } + + // expose methods + this.showTip = beginShowTip; + this.hideTip = hideTip; + this.resetPosition = positionTipOnElement; + } + + /** + * Determine whether a jQuery object is an SVG element + * @private + * @param {jQuery} element The element to check + * @return {boolean} Whether this is an SVG element + */ + function isSvgElement(element) { + return window.SVGElement && element[0] instanceof SVGElement; + } + + /** + * Initializes the viewport dimension cache and hooks up the mouse position + * tracking and viewport dimension tracking events. + * Prevents attaching the events more than once. + * @private + */ + function initTracking() { + if (!session.mouseTrackingActive) { + session.mouseTrackingActive = true; + + // grab the current viewport dimensions on load + $(function getViewportDimensions() { + session.scrollLeft = $window.scrollLeft(); + session.scrollTop = $window.scrollTop(); + session.windowWidth = $window.width(); + session.windowHeight = $window.height(); + }); + + // hook mouse move tracking + $document.on('mousemove', trackMouse); + + // hook viewport dimensions tracking + $window.on({ + resize: function trackResize() { + session.windowWidth = $window.width(); + session.windowHeight = $window.height(); + }, + scroll: function trackScroll() { + var x = $window.scrollLeft(), + y = $window.scrollTop(); + if (x !== session.scrollLeft) { + session.currentX += x - session.scrollLeft; + session.scrollLeft = x; + } + if (y !== session.scrollTop) { + session.currentY += y - session.scrollTop; + session.scrollTop = y; + } + } + }); + } + } + + /** + * Saves the current mouse coordinates to the session object. + * @private + * @param {jQuery.Event} event The mousemove event for the document. + */ + function trackMouse(event) { + session.currentX = event.pageX; + session.currentY = event.pageY; + } + + /** + * Tests if the mouse is currently over the specified element. + * @private + * @param {jQuery} element The element to check for hover. + * @return {boolean} + */ + function isMouseOver(element) { + // use getBoundingClientRect() because jQuery's width() and height() + // methods do not work with SVG elements + // compute width/height because those properties do not exist on the object + // returned by getBoundingClientRect() in older versions of IE + var elementPosition = element.offset(), + elementBox = element[0].getBoundingClientRect(), + elementWidth = elementBox.right - elementBox.left, + elementHeight = elementBox.bottom - elementBox.top; + + return session.currentX >= elementPosition.left && + session.currentX <= elementPosition.left + elementWidth && + session.currentY >= elementPosition.top && + session.currentY <= elementPosition.top + elementHeight; + } + + /** + * Fetches the tooltip content from the specified element's data attributes. + * @private + * @param {jQuery} element The element to get the tooltip content for. + * @return {(string|jQuery|undefined)} The text/HTML string, jQuery object, or + * undefined if there was no tooltip content for the element. + */ + function getTooltipContent(element) { + var tipText = element.data(DATA_POWERTIP), + tipObject = element.data(DATA_POWERTIPJQ), + tipTarget = element.data(DATA_POWERTIPTARGET), + targetElement, + content; + + if (tipText) { + if ($.isFunction(tipText)) { + tipText = tipText.call(element[0]); + } + content = tipText; + } else if (tipObject) { + if ($.isFunction(tipObject)) { + tipObject = tipObject.call(element[0]); + } + if (tipObject.length > 0) { + content = tipObject.clone(true, true); + } + } else if (tipTarget) { + targetElement = $('#' + tipTarget); + if (targetElement.length > 0) { + content = targetElement.html(); + } + } + + return content; + } + + /** + * Finds any viewport collisions that an element (the tooltip) would have if it + * were absolutely positioned at the specified coordinates. + * @private + * @param {CSSCoordinates} coords Coordinates for the element. + * @param {number} elementWidth Width of the element in pixels. + * @param {number} elementHeight Height of the element in pixels. + * @return {number} Value with the collision flags. + */ + function getViewportCollisions(coords, elementWidth, elementHeight) { + var viewportTop = session.scrollTop, + viewportLeft = session.scrollLeft, + viewportBottom = viewportTop + session.windowHeight, + viewportRight = viewportLeft + session.windowWidth, + collisions = Collision.none; + + if (coords.top < viewportTop || Math.abs(coords.bottom - session.windowHeight) - elementHeight < viewportTop) { + collisions |= Collision.top; + } + if (coords.top + elementHeight > viewportBottom || Math.abs(coords.bottom - session.windowHeight) > viewportBottom) { + collisions |= Collision.bottom; + } + if (coords.left < viewportLeft || coords.right + elementWidth > viewportRight) { + collisions |= Collision.left; + } + if (coords.left + elementWidth > viewportRight || coords.right < viewportLeft) { + collisions |= Collision.right; + } + + return collisions; + } + + /** + * Counts the number of bits set on a flags value. + * @param {number} value The flags value. + * @return {number} The number of bits that have been set. + */ + function countFlags(value) { + var count = 0; + while (value) { + value &= value - 1; + count++; + } + return count; + } + +})); |