|
4 | 4 | * @fileoverview jQuery plugin that creates hover tooltips.
|
5 | 5 | * @link https://github.com/stevenbenner/jquery-powertip
|
6 | 6 | * @author Steven Benner
|
7 |
| - * @version 1.0 |
| 7 | + * @version 1.0.1 |
8 | 8 | * @requires jQuery 1.7 or later
|
9 | 9 | * @license jQuery PowerTip Plugin
|
10 | 10 | * <https://github.com/stevenbenner/jquery-powertip>
|
|
28 | 28 | var session = {
|
29 | 29 | isPopOpen: false,
|
30 | 30 | isFixedPopOpen: false,
|
31 |
| - isMouseConstatnlyTracked: false, |
32 | 31 | popOpenImminent: false,
|
33 | 32 | activeHover: null,
|
34 | 33 | mouseTarget: null,
|
35 | 34 | currentX: 0,
|
36 | 35 | currentY: 0,
|
37 | 36 | previousX: 0,
|
38 |
| - previousY: 0 |
| 37 | + previousY: 0, |
| 38 | + desyncTimeout: null |
39 | 39 | };
|
40 | 40 |
|
41 | 41 | /**
|
|
53 | 53 | // extend options
|
54 | 54 | var options = $.extend({}, $.fn.powerTip.defaults, opts);
|
55 | 55 |
|
| 56 | + // hook mouse tracking, once |
| 57 | + hookOnMoveOnce(); |
| 58 | + |
56 | 59 | // build and append popup div if it does not already exist
|
57 | 60 | var tipElement = $('#' + options.popupId);
|
58 | 61 | if (tipElement.length === 0) {
|
59 | 62 | tipElement = $('<div></div>', { id: options.popupId });
|
60 | 63 | $body.append(tipElement);
|
61 | 64 | }
|
62 | 65 |
|
63 |
| - // because of the intent delay we need to constantly track the cursor |
64 |
| - // position for mouse-follow powertips |
| 66 | + // hook mousemove for cursor follow tooltips |
65 | 67 | if (options.followMouse) {
|
66 | 68 | // only one movePop hook per popup element, please
|
67 | 69 | if (!tipElement.data('hasMouseMove')) {
|
68 | 70 | $window.on('mousemove', movePop);
|
69 | 71 | }
|
70 |
| - session.isMouseConstatnlyTracked = true; |
71 | 72 | tipElement.data('hasMouseMove', true);
|
72 | 73 | }
|
73 | 74 |
|
|
116 | 117 | session.mouseTarget = element;
|
117 | 118 | session.previousX = event.pageX;
|
118 | 119 | session.previousY = event.pageY;
|
119 |
| - if (!session.isMouseConstatnlyTracked) { |
120 |
| - element.on('mousemove', trackMouse); |
121 |
| - } |
122 | 120 | if (!element.data('hasActiveHover')) {
|
123 | 121 | session.popOpenImminent = true;
|
124 | 122 | setHoverTimer(element, 'show');
|
|
128 | 126 | var element = $(this);
|
129 | 127 | cancelHoverTimer(element);
|
130 | 128 | session.mouseTarget = null;
|
131 |
| - if (!session.isMouseConstatnlyTracked) { |
132 |
| - element.off('mousemove', trackMouse); |
133 |
| - } |
134 | 129 | session.popOpenImminent = false;
|
135 | 130 | if (element.data('hasActiveHover')) {
|
136 | 131 | setHoverTimer(element, 'hide');
|
|
155 | 150 |
|
156 | 151 | // check if difference has passed the sensitivity threshold
|
157 | 152 | if (totalDifference < options.intentSensitivity) {
|
158 |
| - if (!session.isMouseConstatnlyTracked) { |
159 |
| - element.off('mousemove', trackMouse); |
160 |
| - } |
161 | 153 | element.data('hasActiveHover', true);
|
162 | 154 | // show popup, asap
|
163 | 155 | showTip(element);
|
|
215 | 207 | tipElement.data('mouseOnToPopup', options.mouseOnToPopup);
|
216 | 208 |
|
217 | 209 | // fadein
|
218 |
| - tipElement.stop(true, true).fadeIn(options.fadeInTime); |
| 210 | + tipElement.stop(true, true).fadeIn(options.fadeInTime, function() { |
| 211 | + // start desync polling |
| 212 | + if (!session.desyncTimeout) { |
| 213 | + session.desyncTimeout = setInterval(closeDesyncedTip, 500); |
| 214 | + } |
| 215 | + }); |
219 | 216 | }
|
220 | 217 |
|
221 | 218 | /**
|
|
234 | 231 | // after it is hidden
|
235 | 232 | tipElement.css('left', session.currentX + options.offset + 'px');
|
236 | 233 | tipElement.css('top', session.currentY + options.offset + 'px');
|
| 234 | + // stop desync polling |
| 235 | + session.desyncTimeout = clearInterval(session.desyncTimeout); |
237 | 236 | });
|
238 | 237 | }
|
239 | 238 |
|
| 239 | + /** |
| 240 | + * Checks for a tooltip desync and closes the tooltip if one occurs. |
| 241 | + * @private |
| 242 | + */ |
| 243 | + function closeDesyncedTip() { |
| 244 | + // It is possible for the mouse cursor to leave an element without |
| 245 | + // firing the mouseleave event. This seems to happen (in FF) if the |
| 246 | + // element is disabled under mouse cursor, the element is moved out |
| 247 | + // from under the mouse cursor (such as a slideDown() occurring |
| 248 | + // above it), or if the browser is resized by code moving the |
| 249 | + // element from under the mouse cursor. If this happens it will |
| 250 | + // result in a desynced tooltip because we wait for any exiting |
| 251 | + // open tooltips to close before opening a new one. So we should |
| 252 | + // periodically check for a desync situation and close the tip if |
| 253 | + // such a situation arises. |
| 254 | + if (session.isPopOpen) { |
| 255 | + var isDesynced = false; |
| 256 | + |
| 257 | + // case 1: user already moused onto another tip - easy test |
| 258 | + if (session.activeHover.data('hasActiveHover') === false) { |
| 259 | + isDesynced = true; |
| 260 | + } else { |
| 261 | + // case 2: hanging tip - have to test if mouse position is |
| 262 | + // not over the active hover and not over a tooltip set to |
| 263 | + // let the user interact with it |
| 264 | + if (!isMouseOver(session.activeHover)) { |
| 265 | + if (tipElement.data('mouseOnToPopup')) { |
| 266 | + if (!isMouseOver(tipElement)) { |
| 267 | + isDesynced = true; |
| 268 | + } |
| 269 | + } else { |
| 270 | + isDesynced = true; |
| 271 | + } |
| 272 | + } |
| 273 | + } |
| 274 | + |
| 275 | + if (isDesynced) { |
| 276 | + // close the desynced tip |
| 277 | + hideTip(session.activeHover); |
| 278 | + } |
| 279 | + } |
| 280 | + } |
| 281 | + |
240 | 282 | /**
|
241 | 283 | * Moves the tooltip popup to the users mouse cursor.
|
242 | 284 | * @private
|
|
249 | 291 | // but we should only set the pop location if a fixed pop is not
|
250 | 292 | // currently open, a pop open is imminent or active, and the popup
|
251 | 293 | // element in question does have a mouse-follow using it.
|
252 |
| - trackMouse(event); |
253 | 294 | if ((session.isPopOpen && !session.isFixedPopOpen) || (session.popOpenImminent && !session.isFixedPopOpen && tipElement.data('hasMouseMove'))) {
|
254 | 295 | // grab measurements
|
255 | 296 | var scrollTop = $window.scrollTop(),
|
|
364 | 405 | mouseOnToPopup: false
|
365 | 406 | };
|
366 | 407 |
|
| 408 | + var onMoveHooked = false; |
| 409 | + /** |
| 410 | + * Hooks the trackMouse() function to the window's mousemove event. |
| 411 | + * Prevents attaching the event more than once. |
| 412 | + * @private |
| 413 | + */ |
| 414 | + function hookOnMoveOnce() { |
| 415 | + if (!onMoveHooked) { |
| 416 | + onMoveHooked = true; |
| 417 | + $window.on('mousemove', trackMouse); |
| 418 | + } |
| 419 | + } |
| 420 | + |
367 | 421 | /**
|
368 | 422 | * Saves the current mouse coordinates to the powerTip session object.
|
369 | 423 | * @private
|
|
386 | 440 | }
|
387 | 441 | }
|
388 | 442 |
|
| 443 | + /** |
| 444 | + * Tests if the mouse is currently over the specified element. |
| 445 | + * @private |
| 446 | + * @param {Object} element The element to check for hover. |
| 447 | + * @return {Boolean} |
| 448 | + */ |
| 449 | + function isMouseOver(element) { |
| 450 | + var elementPosition = element.offset(); |
| 451 | + return session.currentX >= elementPosition.left && |
| 452 | + session.currentX <= elementPosition.left + element.outerWidth() && |
| 453 | + session.currentY >= elementPosition.top && |
| 454 | + session.currentY <= elementPosition.top + element.outerHeight(); |
| 455 | + } |
| 456 | + |
389 | 457 | }(jQuery));
|
0 commit comments