diff options
author | Dimitri van Heesch <doxygen@gmail.com> | 2019-05-30 09:27:29 (GMT) |
---|---|---|
committer | Dimitri van Heesch <doxygen@gmail.com> | 2019-05-30 09:27:29 (GMT) |
commit | 7b745c4b8182430d3eacde1cb1520730fee1b01e (patch) | |
tree | 6306fc9835d115c33a073daa07f9518bcbc13b78 /jquery/jquery.powertip-1.3.1.mod.js | |
parent | 2b0cffb6a7afe95f3fb872c553a0de60d19029af (diff) | |
download | Doxygen-7b745c4b8182430d3eacde1cb1520730fee1b01e.zip Doxygen-7b745c4b8182430d3eacde1cb1520730fee1b01e.tar.gz Doxygen-7b745c4b8182430d3eacde1cb1520730fee1b01e.tar.bz2 |
Updated jquery and worked around performance issues in powertip plugin
Diffstat (limited to 'jquery/jquery.powertip-1.3.1.mod.js')
-rw-r--r-- | jquery/jquery.powertip-1.3.1.mod.js | 1324 |
1 files changed, 1324 insertions, 0 deletions
diff --git a/jquery/jquery.powertip-1.3.1.mod.js b/jquery/jquery.powertip-1.3.1.mod.js new file mode 100644 index 0000000..197599c --- /dev/null +++ b/jquery/jquery.powertip-1.3.1.mod.js @@ -0,0 +1,1324 @@ +/*! + PowerTip v1.3.1 (2018-04-15) + https://stevenbenner.github.io/jquery-powertip/ + Copyright (c) 2018 Steven Benner (http://stevenbenner.com/). + Released under MIT license. + https://raw.github.com/stevenbenner/jquery-powertip/master/LICENSE.txt +*/ +(function(root, factory) { + // support loading the plugin via common patterns + if (typeof define === 'function' && define.amd) { + // load the plugin as an amd module + define([ 'jquery' ], factory); + } else if (typeof module === 'object' && module.exports) { + // load the plugin as a commonjs module + module.exports = factory(require('jquery')); + } else { + // load the plugin as a global + factory(root.jQuery); + } +}(this, 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', + EVENT_NAMESPACE = '.powertip', + RAD2DEG = 180 / Math.PI, + MOUSE_EVENTS = [ + 'click', + 'dblclick', + 'mousedown', + 'mouseup', + 'mousemove', + 'mouseover', + 'mouseout', + 'mouseenter', + 'mouseleave', + 'contextmenu' + ]; + + /** + * Session data + * Private properties global to all powerTip instances + */ + var session = { + tooltips: null, + isTipOpen: false, + isFixedTipOpen: false, + isClosing: false, + tipOpenImminent: false, + activeHover: null, + currentX: 0, + currentY: 0, + previousX: 0, + previousY: 0, + desyncTimeout: null, + closeDelayTimeout: 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) { + var targetElements = this, + options, + tipController; + + // don't do any work if there were no matched elements + if (!targetElements.length) { + return targetElements; + } + + // handle api method calls on the plugin, e.g. powerTip('hide') + if ($.type(opts) === 'string' && $.powerTip[opts]) { + return $.powerTip[opts].call(targetElements, targetElements, arg); + } + + // extend options + options = $.extend({}, $.fn.powerTip.defaults, opts); + + // instantiate the TooltipController for this instance + tipController = new TooltipController(options); + + // hook mouse and viewport dimension tracking + initTracking(); + + // setup the elements + targetElements.each(function elementSetup() { + var $this = $(this), + dataPowertip = $this.data(DATA_POWERTIP), + dataElem = $this.data(DATA_POWERTIPJQ), + dataTarget = $this.data(DATA_POWERTIPTARGET), + title = $this.attr('title'); + + // 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 + 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 option is not enabled + if (!options.manual) { + // attach open events + $.each(options.openEvents, function(idx, evt) { + if ($.inArray(evt, options.closeEvents) > -1) { + // event is in both openEvents and closeEvents, so toggle it + targetElements.on(evt + EVENT_NAMESPACE, function elementToggle(event) { + $.powerTip.toggle(this, event); + }); + } else { + targetElements.on(evt + EVENT_NAMESPACE, function elementOpen(event) { + $.powerTip.show(this, event); + }); + } + }); + + // attach close events + $.each(options.closeEvents, function(idx, evt) { + if ($.inArray(evt, options.openEvents) < 0) { + targetElements.on(evt + EVENT_NAMESPACE, function elementClose(event) { + // set immediate to true for any event without mouse info + $.powerTip.hide(this, !isMouseEvent(event)); + }); + } + }); + + // attach escape key close event + targetElements.on('keydown' + EVENT_NAMESPACE, function elementKeyDown(event) { + // always close tooltip when the escape key is pressed + if (event.keyCode === 27) { + $.powerTip.hide(this, true); + } + }); + } + + return targetElements; + }; + + /** + * Default options for the powerTip plugin. + */ + $.fn.powerTip.defaults = { + fadeInTime: 200, + fadeOutTime: 100, + followMouse: false, + popupId: 'powerTip', + popupClass: null, + intentSensitivity: 7, + intentPollInterval: 100, + closeDelay: 100, + placement: 'n', + smartPlacement: false, + offset: 10, + mouseOnToPopup: false, + manual: false, + openEvents: [ 'mouseenter', 'focus' ], + closeEvents: [ 'mouseleave', 'blur' ] + }; + + /** + * 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). + * @return {jQuery|Element} The original jQuery object or DOM Element. + */ + show: function apiShowTip(element, event) { + // if we were given a mouse event then run the hover intent testing, + // otherwise, simply show the tooltip asap + if (isMouseEvent(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. + * @return {jQuery|Element} The original jQuery object or DOM Element. + */ + 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). + * @return {jQuery|Element|undefined} The original jQuery object or DOM + * Element, if one was specified. + */ + hide: function apiCloseTip(element, immediate) { + var displayController; + + // set immediate to true when no element is specified + immediate = element ? immediate : true; + + // find the relevant display controller + if (element) { + displayController = $(element).first().data(DATA_DISPLAYCONTROLLER); + } else if (session.activeHover) { + displayController = session.activeHover.data(DATA_DISPLAYCONTROLLER); + } + + // if found, hide the tip + if (displayController) { + displayController.hide(immediate); + } + + return element; + }, + + /** + * Toggles the tooltip for the specified element. This will open a closed + * tooltip, or close an open tooltip. + * @param {jQuery|Element} element The element with the tooltip that + * should be toggled. + * @param {jQuery.Event=} event jQuery event for hover intent and mouse + * tracking (optional). + * @return {jQuery|Element} The original jQuery object or DOM Element. + */ + toggle: function apiToggle(element, event) { + if (session.activeHover && session.activeHover.is(element)) { + // tooltip for element is active, so close it + $.powerTip.hide(element, !isMouseEvent(event)); + } else { + // tooltip for element is not active, so open it + $.powerTip.show(element, event); + } + 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, + myCloseDelay = 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); + } + closeAnyDelayed(); + tipController.showTip(element); + } + } else { + // cursor left and returned to this element, cancel close + cancelClose(); + } + } + + /** + * Begins the process of closing a tooltip. + * @private + * @param {boolean=} disableDelay Disable close delay (optional). + */ + function closeTooltip(disableDelay) { + // if this instance already has a close delay in progress then halt it + if (myCloseDelay) { + myCloseDelay = session.closeDelayTimeout = clearTimeout(myCloseDelay); + session.delayInProgress = false; + } + cancelTimer(); + session.tipOpenImminent = false; + if (element.data(DATA_HASACTIVEHOVER)) { + element.data(DATA_FORCEDOPEN, false); + if (!disableDelay) { + session.delayInProgress = true; + session.closeDelayTimeout = setTimeout( + function closeDelay() { + session.closeDelayTimeout = null; + tipController.hideTip(element); + session.delayInProgress = false; + myCloseDelay = null; + }, + options.closeDelay + ); + // save internal reference close delay id so we can check if the + // active close delay belongs to this instance + myCloseDelay = session.closeDelayTimeout; + } 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) { + cancelClose(); + closeAnyDelayed(); + tipController.showTip(element); + } else { + // try again + session.previousX = session.currentX; + session.previousY = session.currentY; + openTooltip(); + } + } + + /** + * Cancels active hover timer. + * @private + * @param {boolean=} stopClose Cancel any active close delay timer. + */ + function cancelTimer(stopClose) { + hoverTimer = clearTimeout(hoverTimer); + // cancel the current close delay if the active close delay is for this + // element or the stopClose argument is true + if (session.closeDelayTimeout && myCloseDelay === session.closeDelayTimeout || stopClose) { + cancelClose(); + } + } + + /** + * Cancels any active close delay timer. + * @private + */ + function cancelClose() { + session.closeDelayTimeout = clearTimeout(session.closeDelayTimeout); + session.delayInProgress = false; + } + + /** + * Asks any tooltips waiting on their close delay to close now. + * @private + */ + function closeAnyDelayed() { + // if another element is waiting for its close delay then we should ask + // it to close immediately so we can proceed without unexpected timeout + // code being run during this tooltip's lifecycle + if (session.delayInProgress && session.activeHover && !session.activeHover.is(element)) { + session.activeHover.data(DATA_DISPLAYCONTROLLER).hide(true); + } + } + + /** + * 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; + + /** + * Transform and append the current points to the placements list. + * @private + */ + 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); + // remember the tooltip elements that the plugin has created + session.tooltips = session.tooltips ? session.tooltips.add(tipElement) : 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' + EVENT_NAMESPACE, positionTipOnCursor); + $window.on('scroll' + EVENT_NAMESPACE, positionTipOnCursor); + tipElement.data(DATA_HASMOUSEMOVE, true); + } + } + + /** + * 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); + + // add custom class to tooltip element + tipElement.addClass(options.popupClass); + + // set tooltip position + // revert to static placement when the "force open" flag was set because + // that flag means that we do not have accurate mouse position info + if (!options.followMouse || element.data(DATA_FORCEDOPEN)) { + positionTipOnElement(element); + session.isFixedTipOpen = true; + } else { + positionTipOnCursor(); + } + + // close tooltip when clicking anywhere on the page, with the exception + // of the tooltip's trigger element and any elements that are within a + // tooltip that has 'mouseOnToPopup' option enabled + // always enable this feature when the "force open" flag is set on a + // followMouse tooltip because we reverted to static placement above + if (!element.data(DATA_FORCEDOPEN) && !options.followMouse) { + $document.on('click' + EVENT_NAMESPACE, function documentClick(event) { + var target = event.target; + if (target !== element[0]) { + if (options.mouseOnToPopup) { + if (target !== tipElement[0] && !$.contains(tipElement[0], target)) { + $.powerTip.hide(); + } + } else { + $.powerTip.hide(); + } + } + }); + } + + // if we want to be able to mouse on to the tooltip then we need to + // attach hover events to the tooltip that will cancel a close request + // on mouseenter and start a new close request on mouseleave + // only hook these listeners if we're not in manual mode + if (options.mouseOnToPopup && !options.manual) { + tipElement.on('mouseenter' + EVENT_NAMESPACE, function tipMouseEnter() { + // 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(); + } + }); + tipElement.on('mouseleave' + EVENT_NAMESPACE, function tipMouseLeave() { + // check activeHover in case the mouse cursor left the tooltip + // during the fadeOut and close cycle + if (session.activeHover) { + session.activeHover.data(DATA_DISPLAYCONTROLLER).hide(); + } + }); + } + + // 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.isTipOpen = false; + + // stop desync polling + session.desyncTimeout = clearInterval(session.desyncTimeout); + + // reset element state + element.data(DATA_HASACTIVEHOVER, false); + element.data(DATA_FORCEDOPEN, false); + + // remove document click handler + $document.off('click' + EVENT_NAMESPACE); + + // unbind the mouseOnToPopup events if they were set + tipElement.off(EVENT_NAMESPACE); + + // fade out + tipElement.fadeOut(options.fadeOutTime, function fadeOutCallback() { + var coords = new CSSCoordinates(); + + // reset session and tooltip element + session.activeHover = null; + 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() { + var tipWidth, + tipHeight, + coords, + collisions, + collisionCount; + + // 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 + tipWidth = tipElement.outerWidth(); + tipHeight = tipElement.outerHeight(); + coords = new CSSCoordinates(); + + // 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.scrollLeft + 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; + + // when the followMouse option is enabled and the "force open" flag is + // set we revert to static positioning. since the developer may not have + // considered this scenario we should use smart placement + if (options.smartPlacement || (options.followMouse && element.data(DATA_FORCEDOPEN))) { + 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 + return collisions !== Collision.none; + }); + } 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.removeClass('w nw sw e ne se n s w se-alt sw-alt ne-alt nw-alt'); + 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, + hasDesyncableCloseEvent = $.grep( + [ 'mouseleave', 'mouseout', 'blur', 'focusout' ], + function(eventType) { + return $.inArray(eventType, options.closeEvents) !== -1; + } + ).length > 0; + + // 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 && hasDesyncableCloseEvent) { + if (session.activeHover.data(DATA_HASACTIVEHOVER) === false || session.activeHover.is(':disabled')) { + // user moused onto another tip or active hover is disabled + isDesynced = true; + } else if (!isMouseOver(session.activeHover) && !session.activeHover.is(':focus') && !session.activeHover.data(DATA_FORCEDOPEN)) { + // 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 (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 Boolean(window.SVGElement && element[0] instanceof SVGElement); + } + + /** + * Determines if the specified jQuery.Event object has mouse data. + * @private + * @param {jQuery.Event=} event The jQuery.Event object to test. + * @return {boolean} True if there is mouse data, otherwise false. + */ + function isMouseEvent(event) { + return Boolean(event && $.inArray(event.type, MOUSE_EVENTS) > -1 && + typeof event.pageX === 'number'); + } + + /** + * 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 + getViewportDimensions(); + $(getViewportDimensions); + + // hook mouse move tracking + $document.on('mousemove' + EVENT_NAMESPACE, trackMouse); + + // hook viewport dimensions tracking + $window.on('resize' + EVENT_NAMESPACE, trackResize); + $window.on('scroll' + EVENT_NAMESPACE, trackScroll); + } + } + + /** + * Updates the viewport dimensions cache. + * @private + */ + function getViewportDimensions() { + session.scrollLeft = $window.scrollLeft(); + session.scrollTop = $window.scrollTop(); + session.windowWidth = $window.width(); + session.windowHeight = $window.height(); + } + + /** + * Updates the window size info in the viewport dimensions cache. + * @private + */ + function trackResize() { + session.windowWidth = $window.width(); + session.windowHeight = $window.height(); + } + + /** + * Updates the scroll offset info in the viewport dimensions cache. + * @private + */ + 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} True if the mouse is over the element, otherwise false. + */ + 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; + } + + // return api for commonjs and amd environments + return $.powerTip; +})); |