mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-24 20:36:49 +00:00
797 lines
22 KiB
JavaScript
797 lines
22 KiB
JavaScript
/**
|
|
* PowerTip
|
|
*
|
|
* @fileoverview jQuery plugin that creates hover tooltips.
|
|
* @link http://stevenbenner.github.com/jquery-powertip/
|
|
* @author Steven Benner (http://stevenbenner.com/)
|
|
* @version 1.1.0
|
|
* @requires jQuery 1.7+
|
|
*
|
|
* @license jQuery PowerTip Plugin v1.1.0
|
|
* http://stevenbenner.github.com/jquery-powertip/
|
|
* Copyright 2012 Steven Benner (http://stevenbenner.com/)
|
|
* Released under the MIT license.
|
|
* <https://raw.github.com/stevenbenner/jquery-powertip/master/LICENSE.txt>
|
|
*/
|
|
|
|
(function($) {
|
|
'use strict';
|
|
|
|
// useful private variables
|
|
var $document = $(document),
|
|
$window = $(window),
|
|
$body = $('body');
|
|
|
|
/**
|
|
* Session data
|
|
* Private properties global to all powerTip instances
|
|
* @type Object
|
|
*/
|
|
var session = {
|
|
isPopOpen: false,
|
|
isFixedPopOpen: false,
|
|
isClosing: false,
|
|
popOpenImminent: false,
|
|
activeHover: null,
|
|
currentX: 0,
|
|
currentY: 0,
|
|
previousX: 0,
|
|
previousY: 0,
|
|
desyncTimeout: null,
|
|
mouseTrackingActive: false
|
|
};
|
|
|
|
/**
|
|
* Display hover tooltips on the matched elements.
|
|
* @param {Object} opts The options object to use for the plugin.
|
|
* @return {Object} jQuery object for the matched selectors.
|
|
*/
|
|
$.fn.powerTip = function(opts) {
|
|
|
|
// don't do any work if there were no matched elements
|
|
if (!this.length) {
|
|
return this;
|
|
}
|
|
|
|
// extend options
|
|
var options = $.extend({}, $.fn.powerTip.defaults, opts),
|
|
tipController = new TooltipController(options);
|
|
|
|
// hook mouse tracking
|
|
initMouseTracking();
|
|
|
|
// setup the elements
|
|
this.each(function() {
|
|
var $this = $(this),
|
|
dataPowertip = $this.data('powertip'),
|
|
dataElem = $this.data('powertipjq'),
|
|
dataTarget = $this.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('powertip', title);
|
|
$this.removeAttr('title');
|
|
}
|
|
|
|
// create hover controllers for each element
|
|
$this.data(
|
|
'displayController',
|
|
new DisplayController($this, options, tipController)
|
|
);
|
|
});
|
|
|
|
// attach hover events to all matched elements
|
|
return this.on({
|
|
// mouse events
|
|
mouseenter: function(event) {
|
|
trackMouse(event);
|
|
session.previousX = event.pageX;
|
|
session.previousY = event.pageY;
|
|
$(this).data('displayController').show();
|
|
},
|
|
mouseleave: function() {
|
|
$(this).data('displayController').hide();
|
|
},
|
|
|
|
// keyboard events
|
|
focus: function() {
|
|
var element = $(this);
|
|
if (!isMouseOver(element)) {
|
|
element.data('displayController').show(true);
|
|
}
|
|
},
|
|
blur: function() {
|
|
$(this).data('displayController').hide(true);
|
|
}
|
|
});
|
|
|
|
};
|
|
|
|
/**
|
|
* Default options for the powerTip plugin.
|
|
* @type Object
|
|
*/
|
|
$.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
|
|
};
|
|
|
|
/**
|
|
* 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.
|
|
* @type Object
|
|
*/
|
|
$.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']
|
|
};
|
|
|
|
/**
|
|
* Public API
|
|
* @type Object
|
|
*/
|
|
$.powerTip = {
|
|
|
|
/**
|
|
* Attempts to show the tooltip for the specified element.
|
|
* @public
|
|
* @param {Object} element The element that the tooltip should for.
|
|
*/
|
|
showTip: function(element) {
|
|
// close any open tooltip
|
|
$.powerTip.closeTip();
|
|
// grab only the first matched element and ask it to show its tip
|
|
element = element.first();
|
|
if (!isMouseOver(element)) {
|
|
element.data('displayController').show(true, true);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Attempts to close any open tooltips.
|
|
* @public
|
|
*/
|
|
closeTip: function() {
|
|
$document.triggerHandler('closePowerTip');
|
|
}
|
|
|
|
};
|
|
|
|
/**
|
|
* Creates a new tooltip display controller.
|
|
* @private
|
|
* @constructor
|
|
* @param {Object} element The element that this controller will handle.
|
|
* @param {Object} options Options object containing settings.
|
|
* @param {TooltipController} tipController The TooltipController 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('hasActiveHover')) {
|
|
if (!immediate) {
|
|
session.popOpenImminent = true;
|
|
hoverTimer = setTimeout(
|
|
function() {
|
|
hoverTimer = null;
|
|
checkForIntent(element);
|
|
},
|
|
options.intentPollInterval
|
|
);
|
|
} else {
|
|
if (forceOpen) {
|
|
element.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();
|
|
if (element.data('hasActiveHover')) {
|
|
session.popOpenImminent = false;
|
|
element.data('forcedOpen', false);
|
|
if (!disableDelay) {
|
|
hoverTimer = setTimeout(
|
|
function() {
|
|
hoverTimer = null;
|
|
tipController.hideTip(element);
|
|
},
|
|
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);
|
|
}
|
|
|
|
// expose the methods
|
|
return {
|
|
show: openTooltip,
|
|
hide: closeTooltip,
|
|
cancel: cancelTimer
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates a new tooltip controller.
|
|
* @private
|
|
* @constructor
|
|
* @param {Object} options Options object containing settings.
|
|
*/
|
|
function TooltipController(options) {
|
|
|
|
// build and append popup div if it does not already exist
|
|
var tipElement = $('#' + options.popupId);
|
|
if (tipElement.length === 0) {
|
|
tipElement = $('<div></div>', { id: options.popupId });
|
|
// grab body element if it was not populated when the script loaded
|
|
// 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 popup element, please
|
|
if (!tipElement.data('hasMouseMove')) {
|
|
$document.on({
|
|
mousemove: positionTipOnCursor,
|
|
scroll: positionTipOnCursor
|
|
});
|
|
}
|
|
tipElement.data('hasMouseMove', true);
|
|
}
|
|
|
|
// if we want to be able to mouse onto the popup then we need to attach
|
|
// hover events to the popup that will cancel a close request on hover
|
|
// and start a new close request on mouseleave
|
|
if (options.followMouse || options.mouseOnToPopup) {
|
|
tipElement.on({
|
|
mouseenter: function() {
|
|
if (tipElement.data('followMouse') || tipElement.data('mouseOnToPopup')) {
|
|
// check activeHover in case the mouse cursor entered
|
|
// the tooltip during the fadeOut and close cycle
|
|
if (session.activeHover) {
|
|
session.activeHover.data('displayController').cancel();
|
|
}
|
|
}
|
|
},
|
|
mouseleave: function() {
|
|
if (tipElement.data('mouseOnToPopup')) {
|
|
// check activeHover in case the mouse cursor entered
|
|
// the tooltip during the fadeOut and close cycle
|
|
if (session.activeHover) {
|
|
session.activeHover.data('displayController').hide();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Gives the specified element the active-hover state and queues up
|
|
* the showTip function.
|
|
* @private
|
|
* @param {Object} element The element that the tooltip should target.
|
|
*/
|
|
function beginShowTip(element) {
|
|
element.data('hasActiveHover', true);
|
|
// show popup, asap
|
|
tipElement.queue(function(next) {
|
|
showTip(element);
|
|
next();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Shows the tooltip popup, as soon as possible.
|
|
* @private
|
|
* @param {Object} element The element that the popup should target.
|
|
*/
|
|
function showTip(element) {
|
|
// 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('hasActiveHover')) {
|
|
return;
|
|
}
|
|
|
|
// if the popup 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.isPopOpen) {
|
|
if (!session.isClosing) {
|
|
hideTip(session.activeHover);
|
|
}
|
|
tipElement.delay(100).queue(function(next) {
|
|
showTip(element);
|
|
next();
|
|
});
|
|
return;
|
|
}
|
|
|
|
// trigger powerTipPreRender event
|
|
element.trigger('powerTipPreRender');
|
|
|
|
var tipText = element.data('powertip'),
|
|
tipTarget = element.data('powertiptarget'),
|
|
tipElem = element.data('powertipjq'),
|
|
tipContent = tipTarget ? $('#' + tipTarget) : [];
|
|
|
|
// set popup content
|
|
if (tipText) {
|
|
tipElement.html(tipText);
|
|
} else if (tipElem && tipElem.length > 0) {
|
|
tipElement.empty();
|
|
tipElem.clone(true, true).appendTo(tipElement);
|
|
} else if (tipContent && tipContent.length > 0) {
|
|
tipElement.html($('#' + tipTarget).html());
|
|
} else {
|
|
// we have no content to display, give up
|
|
return;
|
|
}
|
|
|
|
// trigger powerTipRender event
|
|
element.trigger('powerTipRender');
|
|
|
|
// hook close event for triggering from the api
|
|
$document.on('closePowerTip', function() {
|
|
element.data('displayController').hide(true);
|
|
});
|
|
|
|
session.activeHover = element;
|
|
session.isPopOpen = true;
|
|
|
|
tipElement.data('followMouse', options.followMouse);
|
|
tipElement.data('mouseOnToPopup', options.mouseOnToPopup);
|
|
|
|
// set popup position
|
|
if (!options.followMouse) {
|
|
positionTipOnElement(element);
|
|
session.isFixedPopOpen = true;
|
|
} else {
|
|
positionTipOnCursor();
|
|
}
|
|
|
|
// fadein
|
|
tipElement.fadeIn(options.fadeInTime, function() {
|
|
// start desync polling
|
|
if (!session.desyncTimeout) {
|
|
session.desyncTimeout = setInterval(closeDesyncedTip, 500);
|
|
}
|
|
|
|
// trigger powerTipOpen event
|
|
element.trigger('powerTipOpen');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Hides the tooltip popup, immediately.
|
|
* @private
|
|
* @param {Object} element The element that the popup should target.
|
|
*/
|
|
function hideTip(element) {
|
|
session.isClosing = true;
|
|
element.data('hasActiveHover', false);
|
|
element.data('forcedOpen', false);
|
|
// reset session
|
|
session.activeHover = null;
|
|
session.isPopOpen = false;
|
|
// stop desync polling
|
|
session.desyncTimeout = clearInterval(session.desyncTimeout);
|
|
// unhook close event api listener
|
|
$document.off('closePowerTip');
|
|
// fade out
|
|
tipElement.fadeOut(options.fadeOutTime, function() {
|
|
session.isClosing = false;
|
|
session.isFixedPopOpen = false;
|
|
tipElement.removeClass();
|
|
// support mouse-follow and fixed position pops at the same
|
|
// time by moving the popup to the last known cursor location
|
|
// after it is hidden
|
|
setTipPosition(
|
|
session.currentX + options.offset,
|
|
session.currentY + options.offset
|
|
);
|
|
|
|
// trigger powerTipClose event
|
|
element.trigger('powerTipClose');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Checks for a tooltip desync and closes the tooltip if one occurs.
|
|
* @private
|
|
*/
|
|
function closeDesyncedTip() {
|
|
// It is possible for the mouse cursor to leave an element without
|
|
// firing the mouseleave event. This seems to happen (in FF) if the
|
|
// element is disabled under mouse cursor, the element is moved out
|
|
// from under the mouse cursor (such as a slideDown() occurring
|
|
// above it), or if the browser is resized by code moving the
|
|
// element from under the mouse cursor. If this happens it will
|
|
// result in a desynced tooltip because we wait for any exiting
|
|
// open tooltips to close before opening a new one. So we should
|
|
// periodically check for a desync situation and close the tip if
|
|
// such a situation arises.
|
|
if (session.isPopOpen && !session.isClosing) {
|
|
var isDesynced = false;
|
|
|
|
// case 1: user already moused onto another tip - easy test
|
|
if (session.activeHover.data('hasActiveHover') === false) {
|
|
isDesynced = true;
|
|
} else {
|
|
// case 2: 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('forcedOpen')) {
|
|
if (tipElement.data('mouseOnToPopup')) {
|
|
if (!isMouseOver(tipElement)) {
|
|
isDesynced = true;
|
|
}
|
|
} else {
|
|
isDesynced = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isDesynced) {
|
|
// close the desynced tip
|
|
hideTip(session.activeHover);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Moves the tooltip popup to the users mouse cursor.
|
|
* @private
|
|
*/
|
|
function positionTipOnCursor() {
|
|
// to support having fixed powertips on the same page as cursor
|
|
// powertips, where both instances are referencing the same popup
|
|
// element, we need to keep track of the mouse position constantly,
|
|
// but we should only set the pop location if a fixed pop is not
|
|
// currently open, a pop open is imminent or active, and the popup
|
|
// element in question does have a mouse-follow using it.
|
|
if ((session.isPopOpen && !session.isFixedPopOpen) || (session.popOpenImminent && !session.isFixedPopOpen && tipElement.data('hasMouseMove'))) {
|
|
// grab measurements
|
|
var scrollTop = $window.scrollTop(),
|
|
windowWidth = $window.width(),
|
|
windowHeight = $window.height(),
|
|
popWidth = tipElement.outerWidth(),
|
|
popHeight = tipElement.outerHeight(),
|
|
x = 0,
|
|
y = 0;
|
|
|
|
// constrain pop to browser viewport
|
|
if ((popWidth + session.currentX + options.offset) < windowWidth) {
|
|
x = session.currentX + options.offset;
|
|
} else {
|
|
x = windowWidth - popWidth;
|
|
}
|
|
if ((popHeight + session.currentY + options.offset) < (scrollTop + windowHeight)) {
|
|
y = session.currentY + options.offset;
|
|
} else {
|
|
y = scrollTop + windowHeight - popHeight;
|
|
}
|
|
|
|
// position the tooltip
|
|
setTipPosition(x, y);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the tooltip popup too the correct position relative to the
|
|
* specified target element. Based on options settings.
|
|
* @private
|
|
* @param {Object} element The element that the popup should target.
|
|
*/
|
|
function positionTipOnElement(element) {
|
|
var tipWidth = tipElement.outerWidth(),
|
|
tipHeight = tipElement.outerHeight(),
|
|
priorityList,
|
|
placementCoords,
|
|
finalPlacement,
|
|
collisions;
|
|
|
|
// with smart placement we will try a series of placement
|
|
// options and use the first one that does not collide with the
|
|
// browser view port boundaries.
|
|
if (options.smartPlacement) {
|
|
|
|
// grab the placement priority list
|
|
priorityList = $.fn.powerTip.smartPlacementLists[options.placement];
|
|
|
|
// iterate over the priority list and use the first placement
|
|
// option that does not collide with the viewport. if they all
|
|
// collide then the last placement in the list will be used.
|
|
$.each(priorityList, function(idx, pos) {
|
|
// get placement coordinates
|
|
placementCoords = computePlacementCoords(
|
|
element,
|
|
pos,
|
|
tipWidth,
|
|
tipHeight
|
|
);
|
|
finalPlacement = pos;
|
|
|
|
// find collisions
|
|
collisions = getViewportCollisions(
|
|
placementCoords,
|
|
tipWidth,
|
|
tipHeight
|
|
);
|
|
|
|
// break if there were no collisions
|
|
if (collisions.length === 0) {
|
|
return false;
|
|
}
|
|
});
|
|
|
|
} else {
|
|
|
|
// if we're not going to use the smart placement feature then
|
|
// just compute the coordinates and do it
|
|
placementCoords = computePlacementCoords(
|
|
element,
|
|
options.placement,
|
|
tipWidth,
|
|
tipHeight
|
|
);
|
|
finalPlacement = options.placement;
|
|
|
|
}
|
|
|
|
// add placement as class for CSS arrows
|
|
tipElement.addClass(finalPlacement);
|
|
|
|
// position the tooltip
|
|
setTipPosition(placementCoords.x, placementCoords.y);
|
|
}
|
|
|
|
/**
|
|
* Compute the top/left coordinates to display the tooltip at the
|
|
* specified placement relative to the specified element.
|
|
* @private
|
|
* @param {Object} element The element that the tooltip should target.
|
|
* @param {String} placement The placement for the tooltip.
|
|
* @param {Number} popWidth Width of the tooltip element in pixels.
|
|
* @param {Number} popHeight Height of the tooltip element in pixels.
|
|
* @retun {Object} An object with the x and y coordinates.
|
|
*/
|
|
function computePlacementCoords(element, placement, popWidth, popHeight) {
|
|
// grab measurements
|
|
var objectOffset = element.offset(),
|
|
objectWidth = element.outerWidth(),
|
|
objectHeight = element.outerHeight(),
|
|
x = 0,
|
|
y = 0;
|
|
|
|
// calculate the appropriate x and y position in the document
|
|
switch (placement) {
|
|
case 'n':
|
|
x = (objectOffset.left + (objectWidth / 2)) - (popWidth / 2);
|
|
y = objectOffset.top - popHeight - options.offset;
|
|
break;
|
|
case 'e':
|
|
x = objectOffset.left + objectWidth + options.offset;
|
|
y = (objectOffset.top + (objectHeight / 2)) - (popHeight / 2);
|
|
break;
|
|
case 's':
|
|
x = (objectOffset.left + (objectWidth / 2)) - (popWidth / 2);
|
|
y = objectOffset.top + objectHeight + options.offset;
|
|
break;
|
|
case 'w':
|
|
x = objectOffset.left - popWidth - options.offset;
|
|
y = (objectOffset.top + (objectHeight / 2)) - (popHeight / 2);
|
|
break;
|
|
case 'nw':
|
|
x = (objectOffset.left - popWidth) + 20;
|
|
y = objectOffset.top - popHeight - options.offset;
|
|
break;
|
|
case 'ne':
|
|
x = (objectOffset.left + objectWidth) - 20;
|
|
y = objectOffset.top - popHeight - options.offset;
|
|
break;
|
|
case 'sw':
|
|
x = (objectOffset.left - popWidth) + 20;
|
|
y = objectOffset.top + objectHeight + options.offset;
|
|
break;
|
|
case 'se':
|
|
x = (objectOffset.left + objectWidth) - 20;
|
|
y = objectOffset.top + objectHeight + options.offset;
|
|
break;
|
|
}
|
|
|
|
return {
|
|
x: Math.round(x),
|
|
y: Math.round(y)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Sets the tooltip CSS position on the document.
|
|
* @private
|
|
* @param {Number} x Left position in pixels.
|
|
* @param {Number} y Top position in pixels.
|
|
*/
|
|
function setTipPosition(x, y) {
|
|
tipElement.css('left', x + 'px');
|
|
tipElement.css('top', y + 'px');
|
|
}
|
|
|
|
// expose methods
|
|
return {
|
|
showTip: beginShowTip,
|
|
hideTip: hideTip
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Hooks mouse position tracking to mousemove and scroll events.
|
|
* Prevents attaching the events more than once.
|
|
* @private
|
|
*/
|
|
function initMouseTracking() {
|
|
var lastScrollX = 0,
|
|
lastScrollY = 0;
|
|
|
|
if (!session.mouseTrackingActive) {
|
|
session.mouseTrackingActive = true;
|
|
|
|
// grab the current scroll position on load
|
|
$(function() {
|
|
lastScrollX = $document.scrollLeft();
|
|
lastScrollY = $document.scrollTop();
|
|
});
|
|
|
|
// hook mouse position tracking
|
|
$document.on({
|
|
mousemove: trackMouse,
|
|
scroll: function() {
|
|
var x = $document.scrollLeft(),
|
|
y = $document.scrollTop();
|
|
if (x !== lastScrollX) {
|
|
session.currentX += x - lastScrollX;
|
|
lastScrollX = x;
|
|
}
|
|
if (y !== lastScrollY) {
|
|
session.currentY += y - lastScrollY;
|
|
lastScrollY = y;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Saves the current mouse coordinates to the powerTip session object.
|
|
* @private
|
|
* @param {Object} 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 {Object} element The element to check for hover.
|
|
* @return {Boolean}
|
|
*/
|
|
function isMouseOver(element) {
|
|
var elementPosition = element.offset();
|
|
return session.currentX >= elementPosition.left &&
|
|
session.currentX <= elementPosition.left + element.outerWidth() &&
|
|
session.currentY >= elementPosition.top &&
|
|
session.currentY <= elementPosition.top + element.outerHeight();
|
|
}
|
|
|
|
/**
|
|
* Finds any viewport collisions that an element (the tooltip) would have
|
|
* if it were absolutely positioned at the specified coordinates.
|
|
* @private
|
|
* @param {Object} coords Coordinates for the element. (e.g. {x: 123, y: 123})
|
|
* @param {Number} elementWidth Width of the element in pixels.
|
|
* @param {Number} elementHeight Height of the element in pixels.
|
|
* @return {Array} Array of words representing directional collisions.
|
|
*/
|
|
function getViewportCollisions(coords, elementWidth, elementHeight) {
|
|
var scrollLeft = $window.scrollLeft(),
|
|
scrollTop = $window.scrollTop(),
|
|
windowWidth = $window.width(),
|
|
windowHeight = $window.height(),
|
|
collisions = [];
|
|
|
|
if (coords.y < scrollTop) {
|
|
collisions.push('top');
|
|
}
|
|
if (coords.y + elementHeight > scrollTop + windowHeight) {
|
|
collisions.push('bottom');
|
|
}
|
|
if (coords.x < scrollLeft) {
|
|
collisions.push('left');
|
|
}
|
|
if (coords.x + elementWidth > scrollLeft + windowWidth) {
|
|
collisions.push('right');
|
|
}
|
|
|
|
return collisions;
|
|
}
|
|
|
|
}(jQuery));
|