powertip-rails 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,74 @@
1
+ /**
2
+ * PowerTip Grunt Config
3
+ */
4
+
5
+ module.exports = function(grunt) {
6
+ 'use strict';
7
+
8
+ grunt.initConfig({
9
+ pkg: '<json:package.json>',
10
+ meta: {
11
+ banner: '/*!\n' +
12
+ ' <%= pkg.title %> - v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>\n' +
13
+ ' <%= pkg.homepage %>\n' +
14
+ ' Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %> (<%= pkg.author.url %>).\n' +
15
+ ' Released under <%= _.pluck(pkg.licenses, "type").join(", ") %> license.\n' +
16
+ ' <%= _.pluck(pkg.licenses, "url").join("\n") %>\n' +
17
+ '*/'
18
+ },
19
+ concat: {
20
+ dist: {
21
+ src: [
22
+ '<banner:meta.banner>',
23
+ '<file_strip_banner:src/intro.js>',
24
+ '<file_strip_banner:src/core.js>',
25
+ '<file_strip_banner:src/csscoordinates.js>',
26
+ '<file_strip_banner:src/displaycontroller.js>',
27
+ '<file_strip_banner:src/placementcalculator.js>',
28
+ '<file_strip_banner:src/tooltipcontroller.js>',
29
+ '<file_strip_banner:src/utility.js>',
30
+ '<file_strip_banner:src/outro.js>'
31
+ ],
32
+ dest: 'dist/jquery.powertip-<%= pkg.version %>.js'
33
+ }
34
+ },
35
+ min: {
36
+ dist: {
37
+ src: [
38
+ '<banner:meta.banner>',
39
+ '<config:concat.dist.dest>'
40
+ ],
41
+ dest: 'dist/jquery.powertip-<%= pkg.version %>.min.js'
42
+ }
43
+ },
44
+ qunit: {
45
+ files: [
46
+ 'test/index.html'
47
+ ]
48
+ },
49
+ lint: {
50
+ dist: 'dist/jquery.powertip-<%= pkg.version %>.js',
51
+ grunt: 'grunt.js',
52
+ tests: 'test/**/*.js'
53
+ },
54
+ watch: {
55
+ files: [
56
+ '<config:lint.grunt>',
57
+ '<config:lint.tests>',
58
+ 'src/**/*.js'
59
+ ],
60
+ tasks: 'lint:grunt lint:tests concat lint:dist'
61
+ },
62
+ jshint: {
63
+ dist: grunt.file.readJSON('src/.jshintrc'),
64
+ tests: grunt.file.readJSON('test/.jshintrc'),
65
+ grunt: grunt.file.readJSON('.jshintrc')
66
+ },
67
+ uglify: {}
68
+ });
69
+
70
+ grunt.registerTask('default', 'lint:grunt lint:tests concat lint:dist qunit min');
71
+
72
+ grunt.registerTask('travis', 'lint:grunt lint:tests concat lint:dist qunit');
73
+
74
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * PowerTip Intro
3
+ *
4
+ * @fileoverview Opening lines for dist version.
5
+ * @link http://stevenbenner.github.com/jquery-powertip/
6
+ * @author Steven Benner (http://stevenbenner.com/)
7
+ * @requires jQuery 1.7+
8
+ */
9
+
10
+ (function($, window) {
@@ -0,0 +1,10 @@
1
+ /**
2
+ * PowerTip Outro
3
+ *
4
+ * @fileoverview Closing lines for dist version.
5
+ * @link http://stevenbenner.github.com/jquery-powertip/
6
+ * @author Steven Benner (http://stevenbenner.com/)
7
+ * @requires jQuery 1.7+
8
+ */
9
+
10
+ }(jQuery, window));
@@ -0,0 +1,224 @@
1
+ /**
2
+ * PowerTip PlacementCalculator
3
+ *
4
+ * @fileoverview PlacementCalculator object that computes tooltip position.
5
+ * @link http://stevenbenner.github.com/jquery-powertip/
6
+ * @author Steven Benner (http://stevenbenner.com/)
7
+ * @requires jQuery 1.7+
8
+ */
9
+
10
+ /**
11
+ * Creates a new Placement Calculator.
12
+ * @private
13
+ * @constructor
14
+ */
15
+ function PlacementCalculator() {
16
+ /**
17
+ * Compute the CSS position to display a tooltip at the specified placement
18
+ * relative to the specified element.
19
+ * @private
20
+ * @param {jQuery} element The element that the tooltip should target.
21
+ * @param {string} placement The placement for the tooltip.
22
+ * @param {number} tipWidth Width of the tooltip element in pixels.
23
+ * @param {number} tipHeight Height of the tooltip element in pixels.
24
+ * @param {number} offset Distance to offset tooltips in pixels.
25
+ * @return {CSSCoordinates} A CSSCoordinates object with the position.
26
+ */
27
+ function computePlacementCoords(element, placement, tipWidth, tipHeight, offset) {
28
+ var placementBase = placement.split('-')[0], // ignore 'alt' for corners
29
+ coords = new CSSCoordinates(),
30
+ position;
31
+
32
+ if (isSvgElement(element)) {
33
+ position = getSvgPlacement(element, placementBase);
34
+ } else {
35
+ position = getHtmlPlacement(element, placementBase);
36
+ }
37
+
38
+ // calculate the appropriate x and y position in the document
39
+ switch (placement) {
40
+ case 'n':
41
+ coords.set('left', position.left - (tipWidth / 2));
42
+ coords.set('top', position.top - tipHeight - offset);
43
+ break;
44
+ case 'e':
45
+ coords.set('left', position.left + offset);
46
+ coords.set('top', position.top - (tipHeight / 2));
47
+ break;
48
+ case 's':
49
+ coords.set('left', position.left - (tipWidth / 2));
50
+ coords.set('top', position.top + offset);
51
+ break;
52
+ case 'w':
53
+ coords.set('top', position.top - (tipHeight / 2));
54
+ coords.set('right', $window.width() - position.left + offset);
55
+ break;
56
+ case 'nw':
57
+ coords.set('top', position.top - tipHeight - offset);
58
+ coords.set('right', $window.width() - position.left - 20);
59
+ break;
60
+ case 'nw-alt':
61
+ coords.set('left', position.left);
62
+ coords.set('top', position.top - tipHeight - offset);
63
+ break;
64
+ case 'ne':
65
+ coords.set('left', position.left - 20);
66
+ coords.set('top', position.top - tipHeight - offset);
67
+ break;
68
+ case 'ne-alt':
69
+ coords.set('top', position.top - tipHeight - offset);
70
+ coords.set('right', $window.width() - position.left);
71
+ break;
72
+ case 'sw':
73
+ coords.set('top', position.top + offset);
74
+ coords.set('right', $window.width() - position.left - 20);
75
+ break;
76
+ case 'sw-alt':
77
+ coords.set('left', position.left);
78
+ coords.set('top', position.top + offset);
79
+ break;
80
+ case 'se':
81
+ coords.set('left', position.left - 20);
82
+ coords.set('top', position.top + offset);
83
+ break;
84
+ case 'se-alt':
85
+ coords.set('top', position.top + offset);
86
+ coords.set('right', $window.width() - position.left);
87
+ break;
88
+ }
89
+
90
+ return coords;
91
+ }
92
+
93
+ /**
94
+ * Finds the tooltip attachment point in the document for a HTML DOM element
95
+ * for the specified placement.
96
+ * @private
97
+ * @param {jQuery} element The element that the tooltip should target.
98
+ * @param {string} placement The placement for the tooltip.
99
+ * @return {Object} An object with the top,left position values.
100
+ */
101
+ function getHtmlPlacement(element, placement) {
102
+ var objectOffset = element.offset(),
103
+ objectWidth = element.outerWidth(),
104
+ objectHeight = element.outerHeight(),
105
+ left,
106
+ top;
107
+
108
+ // calculate the appropriate x and y position in the document
109
+ switch (placement) {
110
+ case 'n':
111
+ left = objectOffset.left + objectWidth / 2;
112
+ top = objectOffset.top;
113
+ break;
114
+ case 'e':
115
+ left = objectOffset.left + objectWidth;
116
+ top = objectOffset.top + objectHeight / 2;
117
+ break;
118
+ case 's':
119
+ left = objectOffset.left + objectWidth / 2;
120
+ top = objectOffset.top + objectHeight;
121
+ break;
122
+ case 'w':
123
+ left = objectOffset.left;
124
+ top = objectOffset.top + objectHeight / 2;
125
+ break;
126
+ case 'nw':
127
+ left = objectOffset.left;
128
+ top = objectOffset.top;
129
+ break;
130
+ case 'ne':
131
+ left = objectOffset.left + objectWidth;
132
+ top = objectOffset.top;
133
+ break;
134
+ case 'sw':
135
+ left = objectOffset.left;
136
+ top = objectOffset.top + objectHeight;
137
+ break;
138
+ case 'se':
139
+ left = objectOffset.left + objectWidth;
140
+ top = objectOffset.top + objectHeight;
141
+ break;
142
+ }
143
+
144
+ return {
145
+ top: top,
146
+ left: left
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Finds the tooltip attachment point in the document for a SVG element for
152
+ * the specified placement.
153
+ * @private
154
+ * @param {jQuery} element The element that the tooltip should target.
155
+ * @param {string} placement The placement for the tooltip.
156
+ * @return {Object} An object with the top,left position values.
157
+ */
158
+ function getSvgPlacement(element, placement) {
159
+ var svgElement = element.closest('svg')[0],
160
+ domElement = element[0],
161
+ point = svgElement.createSVGPoint(),
162
+ boundingBox = domElement.getBBox(),
163
+ matrix = domElement.getScreenCTM(),
164
+ halfWidth = boundingBox.width / 2,
165
+ halfHeight = boundingBox.height / 2,
166
+ placements = [],
167
+ placementKeys = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'],
168
+ coords,
169
+ rotation,
170
+ steps,
171
+ x;
172
+
173
+ function pushPlacement() {
174
+ placements.push(point.matrixTransform(matrix));
175
+ }
176
+
177
+ // get bounding box corners and midpoints
178
+ point.x = boundingBox.x;
179
+ point.y = boundingBox.y;
180
+ pushPlacement();
181
+ point.x += halfWidth;
182
+ pushPlacement();
183
+ point.x += halfWidth;
184
+ pushPlacement();
185
+ point.y += halfHeight;
186
+ pushPlacement();
187
+ point.y += halfHeight;
188
+ pushPlacement();
189
+ point.x -= halfWidth;
190
+ pushPlacement();
191
+ point.x -= halfWidth;
192
+ pushPlacement();
193
+ point.y -= halfHeight;
194
+ pushPlacement();
195
+
196
+ // determine rotation
197
+ if (placements[0].y !== placements[1].y || placements[0].x !== placements[7].x) {
198
+ rotation = Math.atan2(matrix.b, matrix.a) * RAD2DEG;
199
+ steps = Math.ceil(((rotation % 360) - 22.5) / 45);
200
+ if (steps < 1) {
201
+ steps += 8;
202
+ }
203
+ while (steps--) {
204
+ placementKeys.push(placementKeys.shift());
205
+ }
206
+ }
207
+
208
+ // find placement
209
+ for (x = 0; x < placements.length; x++) {
210
+ if (placementKeys[x] === placement) {
211
+ coords = placements[x];
212
+ break;
213
+ }
214
+ }
215
+
216
+ return {
217
+ top: coords.y + $window.scrollTop(),
218
+ left: coords.x + $window.scrollLeft()
219
+ };
220
+ }
221
+
222
+ // expose methods
223
+ this.compute = computePlacementCoords;
224
+ }
@@ -0,0 +1,383 @@
1
+ /**
2
+ * PowerTip TooltipController
3
+ *
4
+ * @fileoverview TooltipController object that manages tips for an instance.
5
+ * @link http://stevenbenner.github.com/jquery-powertip/
6
+ * @author Steven Benner (http://stevenbenner.com/)
7
+ * @requires jQuery 1.7+
8
+ */
9
+
10
+ /**
11
+ * Creates a new tooltip controller.
12
+ * @private
13
+ * @constructor
14
+ * @param {Object} options Options object containing settings.
15
+ */
16
+ function TooltipController(options) {
17
+ var placementCalculator = new PlacementCalculator(),
18
+ tipElement = $('#' + options.popupId);
19
+
20
+ // build and append tooltip div if it does not already exist
21
+ if (tipElement.length === 0) {
22
+ tipElement = $('<div/>', { id: options.popupId });
23
+ // grab body element if it was not populated when the script loaded
24
+ // note: this hack exists solely for jsfiddle support
25
+ if ($body.length === 0) {
26
+ $body = $('body');
27
+ }
28
+ $body.append(tipElement);
29
+ }
30
+
31
+ // hook mousemove for cursor follow tooltips
32
+ if (options.followMouse) {
33
+ // only one positionTipOnCursor hook per tooltip element, please
34
+ if (!tipElement.data(DATA_HASMOUSEMOVE)) {
35
+ $document.on({
36
+ mousemove: positionTipOnCursor,
37
+ scroll: positionTipOnCursor
38
+ });
39
+ tipElement.data(DATA_HASMOUSEMOVE, true);
40
+ }
41
+ }
42
+
43
+ // if we want to be able to mouse onto the tooltip then we need to attach
44
+ // hover events to the tooltip that will cancel a close request on hover and
45
+ // start a new close request on mouseleave
46
+ if (options.mouseOnToPopup) {
47
+ tipElement.on({
48
+ mouseenter: function tipMouseEnter() {
49
+ // we only let the mouse stay on the tooltip if it is set to let
50
+ // users interact with it
51
+ if (tipElement.data(DATA_MOUSEONTOTIP)) {
52
+ // check activeHover in case the mouse cursor entered the
53
+ // tooltip during the fadeOut and close cycle
54
+ if (session.activeHover) {
55
+ session.activeHover.data(DATA_DISPLAYCONTROLLER).cancel();
56
+ }
57
+ }
58
+ },
59
+ mouseleave: function tipMouseLeave() {
60
+ // check activeHover in case the mouse cursor entered the
61
+ // tooltip during the fadeOut and close cycle
62
+ if (session.activeHover) {
63
+ session.activeHover.data(DATA_DISPLAYCONTROLLER).hide();
64
+ }
65
+ }
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Gives the specified element the active-hover state and queues up the
71
+ * showTip function.
72
+ * @private
73
+ * @param {jQuery} element The element that the tooltip should target.
74
+ */
75
+ function beginShowTip(element) {
76
+ element.data(DATA_HASACTIVEHOVER, true);
77
+ // show tooltip, asap
78
+ tipElement.queue(function queueTipInit(next) {
79
+ showTip(element);
80
+ next();
81
+ });
82
+ }
83
+
84
+ /**
85
+ * Shows the tooltip, as soon as possible.
86
+ * @private
87
+ * @param {jQuery} element The element that the tooltip should target.
88
+ */
89
+ function showTip(element) {
90
+ var tipContent;
91
+
92
+ // it is possible, especially with keyboard navigation, to move on to
93
+ // another element with a tooltip during the queue to get to this point
94
+ // in the code. if that happens then we need to not proceed or we may
95
+ // have the fadeout callback for the last tooltip execute immediately
96
+ // after this code runs, causing bugs.
97
+ if (!element.data(DATA_HASACTIVEHOVER)) {
98
+ return;
99
+ }
100
+
101
+ // if the tooltip is open and we got asked to open another one then the
102
+ // old one is still in its fadeOut cycle, so wait and try again
103
+ if (session.isTipOpen) {
104
+ if (!session.isClosing) {
105
+ hideTip(session.activeHover);
106
+ }
107
+ tipElement.delay(100).queue(function queueTipAgain(next) {
108
+ showTip(element);
109
+ next();
110
+ });
111
+ return;
112
+ }
113
+
114
+ // trigger powerTipPreRender event
115
+ element.trigger('powerTipPreRender');
116
+
117
+ // set tooltip content
118
+ tipContent = getTooltipContent(element);
119
+ if (tipContent) {
120
+ tipElement.empty().append(tipContent);
121
+ } else {
122
+ // we have no content to display, give up
123
+ return;
124
+ }
125
+
126
+ // trigger powerTipRender event
127
+ element.trigger('powerTipRender');
128
+
129
+ session.activeHover = element;
130
+ session.isTipOpen = true;
131
+
132
+ tipElement.data(DATA_MOUSEONTOTIP, options.mouseOnToPopup);
133
+
134
+ // set tooltip position
135
+ if (!options.followMouse) {
136
+ positionTipOnElement(element);
137
+ session.isFixedTipOpen = true;
138
+ } else {
139
+ positionTipOnCursor();
140
+ }
141
+
142
+ // fadein
143
+ tipElement.fadeIn(options.fadeInTime, function fadeInCallback() {
144
+ // start desync polling
145
+ if (!session.desyncTimeout) {
146
+ session.desyncTimeout = window.setInterval(closeDesyncedTip, 500);
147
+ }
148
+
149
+ // trigger powerTipOpen event
150
+ element.trigger('powerTipOpen');
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Hides the tooltip.
156
+ * @private
157
+ * @param {jQuery} element The element that the tooltip should target.
158
+ */
159
+ function hideTip(element) {
160
+ // reset session
161
+ session.isClosing = true;
162
+ session.activeHover = null;
163
+ session.isTipOpen = false;
164
+
165
+ // stop desync polling
166
+ session.desyncTimeout = window.clearInterval(session.desyncTimeout);
167
+
168
+ // reset element state
169
+ element.data(DATA_HASACTIVEHOVER, false);
170
+ element.data(DATA_FORCEDOPEN, false);
171
+
172
+ // fade out
173
+ tipElement.fadeOut(options.fadeOutTime, function fadeOutCallback() {
174
+ var coords = new CSSCoordinates();
175
+
176
+ // reset session and tooltip element
177
+ session.isClosing = false;
178
+ session.isFixedTipOpen = false;
179
+ tipElement.removeClass();
180
+
181
+ // support mouse-follow and fixed position tips at the same time by
182
+ // moving the tooltip to the last cursor location after it is hidden
183
+ coords.set('top', session.currentY + options.offset);
184
+ coords.set('left', session.currentX + options.offset);
185
+ tipElement.css(coords);
186
+
187
+ // trigger powerTipClose event
188
+ element.trigger('powerTipClose');
189
+ });
190
+ }
191
+
192
+ /**
193
+ * Moves the tooltip to the users mouse cursor.
194
+ * @private
195
+ */
196
+ function positionTipOnCursor() {
197
+ // to support having fixed tooltips on the same page as cursor tooltips,
198
+ // where both instances are referencing the same tooltip element, we
199
+ // need to keep track of the mouse position constantly, but we should
200
+ // only set the tip location if a fixed tip is not currently open, a tip
201
+ // open is imminent or active, and the tooltip element in question does
202
+ // have a mouse-follow using it.
203
+ if (!session.isFixedTipOpen && (session.isTipOpen || (session.tipOpenImminent && tipElement.data(DATA_HASMOUSEMOVE)))) {
204
+ // grab measurements
205
+ var tipWidth = tipElement.outerWidth(),
206
+ tipHeight = tipElement.outerHeight(),
207
+ coords = new CSSCoordinates(),
208
+ collisions,
209
+ collisionCount;
210
+
211
+ // grab collisions
212
+ coords.set('top', session.currentY + options.offset);
213
+ coords.set('left', session.currentX + options.offset);
214
+ collisions = getViewportCollisions(
215
+ coords,
216
+ tipWidth,
217
+ tipHeight
218
+ );
219
+
220
+ // handle tooltip view port collisions
221
+ if (collisions !== Collision.none) {
222
+ collisionCount = countFlags(collisions);
223
+ if (collisionCount === 1) {
224
+ // if there is only one collision (bottom or right) then
225
+ // simply constrain the tooltip to the view port
226
+ if (collisions === Collision.right) {
227
+ coords.set('left', $window.width() - tipWidth);
228
+ } else if (collisions === Collision.bottom) {
229
+ coords.set('top', $window.scrollTop() + $window.height() - tipHeight);
230
+ }
231
+ } else {
232
+ // if the tooltip has more than one collision then it is
233
+ // trapped in the corner and should be flipped to get it out
234
+ // of the users way
235
+ coords.set('left', session.currentX - tipWidth - options.offset);
236
+ coords.set('top', session.currentY - tipHeight - options.offset);
237
+ }
238
+ }
239
+
240
+ // position the tooltip
241
+ tipElement.css(coords);
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Sets the tooltip to the correct position relative to the specified target
247
+ * element. Based on options settings.
248
+ * @private
249
+ * @param {jQuery} element The element that the tooltip should target.
250
+ */
251
+ function positionTipOnElement(element) {
252
+ var priorityList,
253
+ finalPlacement;
254
+
255
+ if (options.smartPlacement) {
256
+ priorityList = $.fn.powerTip.smartPlacementLists[options.placement];
257
+
258
+ // iterate over the priority list and use the first placement option
259
+ // that does not collide with the view port. if they all collide
260
+ // then the last placement in the list will be used.
261
+ $.each(priorityList, function(idx, pos) {
262
+ // place tooltip and find collisions
263
+ var collisions = getViewportCollisions(
264
+ placeTooltip(element, pos),
265
+ tipElement.outerWidth(),
266
+ tipElement.outerHeight()
267
+ );
268
+
269
+ // update the final placement variable
270
+ finalPlacement = pos;
271
+
272
+ // break if there were no collisions
273
+ if (collisions === Collision.none) {
274
+ return false;
275
+ }
276
+ });
277
+ } else {
278
+ // if we're not going to use the smart placement feature then just
279
+ // compute the coordinates and do it
280
+ placeTooltip(element, options.placement);
281
+ finalPlacement = options.placement;
282
+ }
283
+
284
+ // add placement as class for CSS arrows
285
+ tipElement.addClass(finalPlacement);
286
+ }
287
+
288
+ /**
289
+ * Sets the tooltip position to the appropriate values to show the tip at
290
+ * the specified placement. This function will iterate and test the tooltip
291
+ * to support elastic tooltips.
292
+ * @private
293
+ * @param {jQuery} element The element that the tooltip should target.
294
+ * @param {string} placement The placement for the tooltip.
295
+ * @return {CSSCoordinates} A CSSCoordinates object with the top, left, and
296
+ * right position values.
297
+ */
298
+ function placeTooltip(element, placement) {
299
+ var iterationCount = 0,
300
+ tipWidth,
301
+ tipHeight,
302
+ coords = new CSSCoordinates();
303
+
304
+ // set the tip to 0,0 to get the full expanded width
305
+ coords.set('top', 0);
306
+ coords.set('left', 0);
307
+ tipElement.css(coords);
308
+
309
+ // to support elastic tooltips we need to check for a change in the
310
+ // rendered dimensions after the tooltip has been positioned
311
+ do {
312
+ // grab the current tip dimensions
313
+ tipWidth = tipElement.outerWidth();
314
+ tipHeight = tipElement.outerHeight();
315
+
316
+ // get placement coordinates
317
+ coords = placementCalculator.compute(
318
+ element,
319
+ placement,
320
+ tipWidth,
321
+ tipHeight,
322
+ options.offset
323
+ );
324
+
325
+ // place the tooltip
326
+ tipElement.css(coords);
327
+ } while (
328
+ // sanity check: limit to 5 iterations, and...
329
+ ++iterationCount <= 5 &&
330
+ // try again if the dimensions changed after placement
331
+ (tipWidth !== tipElement.outerWidth() || tipHeight !== tipElement.outerHeight())
332
+ );
333
+
334
+ return coords;
335
+ }
336
+
337
+ /**
338
+ * Checks for a tooltip desync and closes the tooltip if one occurs.
339
+ * @private
340
+ */
341
+ function closeDesyncedTip() {
342
+ var isDesynced = false;
343
+ // It is possible for the mouse cursor to leave an element without
344
+ // firing the mouseleave or blur event. This most commonly happens when
345
+ // the element is disabled under mouse cursor. If this happens it will
346
+ // result in a desynced tooltip because the tooltip was never asked to
347
+ // close. So we should periodically check for a desync situation and
348
+ // close the tip if such a situation arises.
349
+ if (session.isTipOpen && !session.isClosing && !session.delayInProgress) {
350
+ // user moused onto another tip or active hover is disabled
351
+ if (session.activeHover.data(DATA_HASACTIVEHOVER) === false || session.activeHover.is(':disabled')) {
352
+ isDesynced = true;
353
+ } else {
354
+ // hanging tip - have to test if mouse position is not over the
355
+ // active hover and not over a tooltip set to let the user
356
+ // interact with it.
357
+ // for keyboard navigation: this only counts if the element does
358
+ // not have focus.
359
+ // for tooltips opened via the api: we need to check if it has
360
+ // the forcedOpen flag.
361
+ if (!isMouseOver(session.activeHover) && !session.activeHover.is(':focus') && !session.activeHover.data(DATA_FORCEDOPEN)) {
362
+ if (tipElement.data(DATA_MOUSEONTOTIP)) {
363
+ if (!isMouseOver(tipElement)) {
364
+ isDesynced = true;
365
+ }
366
+ } else {
367
+ isDesynced = true;
368
+ }
369
+ }
370
+ }
371
+
372
+ if (isDesynced) {
373
+ // close the desynced tip
374
+ hideTip(session.activeHover);
375
+ }
376
+ }
377
+ }
378
+
379
+ // expose methods
380
+ this.showTip = beginShowTip;
381
+ this.hideTip = hideTip;
382
+ this.resetPosition = positionTipOnElement;
383
+ }