|
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