enju_leaf 1.1.0.pre3 → 1.1.0.rc1

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.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/config/locales/translation_en.yml +1 -0
  3. data/config/locales/translation_ja.yml +1 -0
  4. data/lib/enju_leaf/engine.rb +0 -1
  5. data/lib/enju_leaf/version.rb +1 -1
  6. data/lib/enju_leaf.rb +5 -3
  7. data/lib/generators/enju_leaf/quick_install/quick_install_generator.rb +15 -7
  8. data/lib/generators/enju_leaf/setup/setup_generator.rb +0 -1
  9. data/lib/generators/enju_leaf/setup/templates/config/schedule.rb +3 -14
  10. data/lib/generators/enju_leaf/setup/templates/db/seeds.rb +7 -6
  11. data/lib/tasks/enju_leaf_tasks.rake +10 -1
  12. data/spec/dummy/db/migrate/006_create_items.rb +0 -4
  13. data/spec/dummy/db/schema.rb +7 -11
  14. data/spec/dummy/db/test.sqlite3 +0 -0
  15. data/spec/dummy/solr/data/test/index/{_3b.fdt → _3t.fdt} +0 -0
  16. data/spec/dummy/solr/data/test/index/{_3b.fdx → _3t.fdx} +0 -0
  17. data/spec/dummy/solr/data/test/index/{_3b.fnm → _3t.fnm} +0 -0
  18. data/spec/dummy/solr/data/test/index/{_3b.frq → _3t.frq} +0 -0
  19. data/spec/dummy/solr/data/test/index/{_3b.nrm → _3t.nrm} +0 -0
  20. data/spec/dummy/solr/data/test/index/{_3b.prx → _3t.prx} +0 -0
  21. data/spec/dummy/solr/data/test/index/{_3b.tii → _3t.tii} +0 -0
  22. data/spec/dummy/solr/data/test/index/{_3b.tis → _3t.tis} +0 -0
  23. data/spec/dummy/solr/data/test/index/segments.gen +0 -0
  24. data/spec/dummy/solr/data/test/index/{segments_6p → segments_7p} +0 -0
  25. data/spec/fixtures/items.yml +0 -49
  26. data/vendor/assets/javascripts/jquery.powertip.js +792 -422
  27. data/vendor/assets/stylesheets/jquery.powertip.css +34 -5
  28. metadata +45 -45
@@ -1,119 +1,159 @@
1
- /**
2
- * PowerTip
3
- *
4
- * @fileoverview jQuery plugin that creates hover tooltips.
5
- * @link http://stevenbenner.github.com/jquery-powertip/
6
- * @author Steven Benner (http://stevenbenner.com/)
7
- * @version 1.1.0
8
- * @requires jQuery 1.7+
9
- *
10
- * @license jQuery PowerTip Plugin v1.1.0
11
- * http://stevenbenner.github.com/jquery-powertip/
12
- * Copyright 2012 Steven Benner (http://stevenbenner.com/)
13
- * Released under the MIT license.
14
- * <https://raw.github.com/stevenbenner/jquery-powertip/master/LICENSE.txt>
15
- */
16
-
17
- (function($) {
18
- 'use strict';
1
+ /*!
2
+ PowerTip - v1.2.0 - 2013-04-03
3
+ http://stevenbenner.github.com/jquery-powertip/
4
+ Copyright (c) 2013 Steven Benner (http://stevenbenner.com/).
5
+ Released under MIT license.
6
+ https://raw.github.com/stevenbenner/jquery-powertip/master/LICENSE.txt
7
+ */
8
+ (function(factory) {
9
+ if (typeof define === 'function' && define.amd) {
10
+ // AMD. Register as an anonymous module.
11
+ define(['jquery'], factory);
12
+ } else {
13
+ // Browser globals
14
+ factory(jQuery);
15
+ }
16
+ }(function($) {
19
17
 
20
18
  // useful private variables
21
19
  var $document = $(document),
22
20
  $window = $(window),
23
21
  $body = $('body');
24
22
 
23
+ // constants
24
+ var DATA_DISPLAYCONTROLLER = 'displayController',
25
+ DATA_HASACTIVEHOVER = 'hasActiveHover',
26
+ DATA_FORCEDOPEN = 'forcedOpen',
27
+ DATA_HASMOUSEMOVE = 'hasMouseMove',
28
+ DATA_MOUSEONTOTIP = 'mouseOnToPopup',
29
+ DATA_ORIGINALTITLE = 'originalTitle',
30
+ DATA_POWERTIP = 'powertip',
31
+ DATA_POWERTIPJQ = 'powertipjq',
32
+ DATA_POWERTIPTARGET = 'powertiptarget',
33
+ RAD2DEG = 180 / Math.PI;
34
+
25
35
  /**
26
36
  * Session data
27
37
  * Private properties global to all powerTip instances
28
- * @type Object
29
38
  */
30
39
  var session = {
31
- isPopOpen: false,
32
- isFixedPopOpen: false,
40
+ isTipOpen: false,
41
+ isFixedTipOpen: false,
33
42
  isClosing: false,
34
- popOpenImminent: false,
43
+ tipOpenImminent: false,
35
44
  activeHover: null,
36
45
  currentX: 0,
37
46
  currentY: 0,
38
47
  previousX: 0,
39
48
  previousY: 0,
40
49
  desyncTimeout: null,
41
- mouseTrackingActive: false
50
+ mouseTrackingActive: false,
51
+ delayInProgress: false,
52
+ windowWidth: 0,
53
+ windowHeight: 0,
54
+ scrollTop: 0,
55
+ scrollLeft: 0
42
56
  };
43
57
 
44
58
  /**
45
- * Display hover tooltips on the matched elements.
46
- * @param {Object} opts The options object to use for the plugin.
47
- * @return {Object} jQuery object for the matched selectors.
59
+ * Collision enumeration
60
+ * @enum {number}
48
61
  */
49
- $.fn.powerTip = function(opts) {
62
+ var Collision = {
63
+ none: 0,
64
+ top: 1,
65
+ bottom: 2,
66
+ left: 4,
67
+ right: 8
68
+ };
50
69
 
70
+ /**
71
+ * Display hover tooltips on the matched elements.
72
+ * @param {(Object|string)} opts The options object to use for the plugin, or
73
+ * the name of a method to invoke on the first matched element.
74
+ * @param {*=} [arg] Argument for an invoked method (optional).
75
+ * @return {jQuery} jQuery object for the matched selectors.
76
+ */
77
+ $.fn.powerTip = function(opts, arg) {
51
78
  // don't do any work if there were no matched elements
52
79
  if (!this.length) {
53
80
  return this;
54
81
  }
55
82
 
56
- // extend options
83
+ // handle api method calls on the plugin, e.g. powerTip('hide')
84
+ if ($.type(opts) === 'string' && $.powerTip[opts]) {
85
+ return $.powerTip[opts].call(this, this, arg);
86
+ }
87
+
88
+ // extend options and instantiate TooltipController
57
89
  var options = $.extend({}, $.fn.powerTip.defaults, opts),
58
90
  tipController = new TooltipController(options);
59
91
 
60
- // hook mouse tracking
61
- initMouseTracking();
92
+ // hook mouse and viewport dimension tracking
93
+ initTracking();
62
94
 
63
95
  // setup the elements
64
- this.each(function() {
96
+ this.each(function elementSetup() {
65
97
  var $this = $(this),
66
- dataPowertip = $this.data('powertip'),
67
- dataElem = $this.data('powertipjq'),
68
- dataTarget = $this.data('powertiptarget'),
69
- title = $this.attr('title');
70
-
98
+ dataPowertip = $this.data(DATA_POWERTIP),
99
+ dataElem = $this.data(DATA_POWERTIPJQ),
100
+ dataTarget = $this.data(DATA_POWERTIPTARGET),
101
+ title;
102
+
103
+ // handle repeated powerTip calls on the same element by destroying the
104
+ // original instance hooked to it and replacing it with this call
105
+ if ($this.data(DATA_DISPLAYCONTROLLER)) {
106
+ $.powerTip.destroy($this);
107
+ }
71
108
 
72
109
  // attempt to use title attribute text if there is no data-powertip,
73
110
  // data-powertipjq or data-powertiptarget. If we do use the title
74
111
  // attribute, delete the attribute so the browser will not show it
112
+ title = $this.attr('title');
75
113
  if (!dataPowertip && !dataTarget && !dataElem && title) {
76
- $this.data('powertip', title);
114
+ $this.data(DATA_POWERTIP, title);
115
+ $this.data(DATA_ORIGINALTITLE, title);
77
116
  $this.removeAttr('title');
78
117
  }
79
118
 
80
119
  // create hover controllers for each element
81
120
  $this.data(
82
- 'displayController',
121
+ DATA_DISPLAYCONTROLLER,
83
122
  new DisplayController($this, options, tipController)
84
123
  );
85
124
  });
86
125
 
87
- // attach hover events to all matched elements
88
- return this.on({
89
- // mouse events
90
- mouseenter: function(event) {
91
- trackMouse(event);
92
- session.previousX = event.pageX;
93
- session.previousY = event.pageY;
94
- $(this).data('displayController').show();
95
- },
96
- mouseleave: function() {
97
- $(this).data('displayController').hide();
98
- },
99
-
100
- // keyboard events
101
- focus: function() {
102
- var element = $(this);
103
- if (!isMouseOver(element)) {
104
- element.data('displayController').show(true);
126
+ // attach events to matched elements if the manual options is not enabled
127
+ if (!options.manual) {
128
+ this.on({
129
+ // mouse events
130
+ 'mouseenter.powertip': function elementMouseEnter(event) {
131
+ $.powerTip.show(this, event);
132
+ },
133
+ 'mouseleave.powertip': function elementMouseLeave() {
134
+ $.powerTip.hide(this);
135
+ },
136
+ // keyboard events
137
+ 'focus.powertip': function elementFocus() {
138
+ $.powerTip.show(this);
139
+ },
140
+ 'blur.powertip': function elementBlur() {
141
+ $.powerTip.hide(this, true);
142
+ },
143
+ 'keydown.powertip': function elementKeyDown(event) {
144
+ // close tooltip when the escape key is pressed
145
+ if (event.keyCode === 27) {
146
+ $.powerTip.hide(this, true);
147
+ }
105
148
  }
106
- },
107
- blur: function() {
108
- $(this).data('displayController').hide(true);
109
- }
110
- });
149
+ });
150
+ }
111
151
 
152
+ return this;
112
153
  };
113
154
 
114
155
  /**
115
156
  * Default options for the powerTip plugin.
116
- * @type Object
117
157
  */
118
158
  $.fn.powerTip.defaults = {
119
159
  fadeInTime: 200,
@@ -126,15 +166,15 @@
126
166
  placement: 'n',
127
167
  smartPlacement: false,
128
168
  offset: 10,
129
- mouseOnToPopup: false
169
+ mouseOnToPopup: false,
170
+ manual: false
130
171
  };
131
172
 
132
173
  /**
133
174
  * Default smart placement priority lists.
134
- * The first item in the array is the highest priority, the last is the
135
- * lowest. The last item is also the default, which will be used if all
136
- * previous options do not fit.
137
- * @type Object
175
+ * The first item in the array is the highest priority, the last is the lowest.
176
+ * The last item is also the default, which will be used if all previous options
177
+ * do not fit.
138
178
  */
139
179
  $.fn.powerTip.smartPlacementLists = {
140
180
  n: ['n', 'ne', 'nw', 's'],
@@ -144,47 +184,125 @@
144
184
  nw: ['nw', 'w', 'sw', 'n', 's', 'se', 'nw'],
145
185
  ne: ['ne', 'e', 'se', 'n', 's', 'sw', 'ne'],
146
186
  sw: ['sw', 'w', 'nw', 's', 'n', 'ne', 'sw'],
147
- se: ['se', 'e', 'ne', 's', 'n', 'nw', 'se']
187
+ se: ['se', 'e', 'ne', 's', 'n', 'nw', 'se'],
188
+ 'nw-alt': ['nw-alt', 'n', 'ne-alt', 'sw-alt', 's', 'se-alt', 'w', 'e'],
189
+ 'ne-alt': ['ne-alt', 'n', 'nw-alt', 'se-alt', 's', 'sw-alt', 'e', 'w'],
190
+ 'sw-alt': ['sw-alt', 's', 'se-alt', 'nw-alt', 'n', 'ne-alt', 'w', 'e'],
191
+ 'se-alt': ['se-alt', 's', 'sw-alt', 'ne-alt', 'n', 'nw-alt', 'e', 'w']
148
192
  };
149
193
 
150
194
  /**
151
195
  * Public API
152
- * @type Object
153
196
  */
154
197
  $.powerTip = {
155
-
156
198
  /**
157
199
  * Attempts to show the tooltip for the specified element.
158
- * @public
159
- * @param {Object} element The element that the tooltip should for.
200
+ * @param {jQuery|Element} element The element to open the tooltip for.
201
+ * @param {jQuery.Event=} event jQuery event for hover intent and mouse
202
+ * tracking (optional).
160
203
  */
161
- showTip: function(element) {
162
- // close any open tooltip
163
- $.powerTip.closeTip();
164
- // grab only the first matched element and ask it to show its tip
165
- element = element.first();
166
- if (!isMouseOver(element)) {
167
- element.data('displayController').show(true, true);
204
+ show: function apiShowTip(element, event) {
205
+ if (event) {
206
+ trackMouse(event);
207
+ session.previousX = event.pageX;
208
+ session.previousY = event.pageY;
209
+ $(element).data(DATA_DISPLAYCONTROLLER).show();
210
+ } else {
211
+ $(element).first().data(DATA_DISPLAYCONTROLLER).show(true, true);
168
212
  }
213
+ return element;
214
+ },
215
+
216
+ /**
217
+ * Repositions the tooltip on the element.
218
+ * @param {jQuery|Element} element The element the tooltip is shown for.
219
+ */
220
+ reposition: function apiResetPosition(element) {
221
+ $(element).first().data(DATA_DISPLAYCONTROLLER).resetPosition();
222
+ return element;
169
223
  },
170
224
 
171
225
  /**
172
226
  * Attempts to close any open tooltips.
173
- * @public
227
+ * @param {(jQuery|Element)=} element The element with the tooltip that
228
+ * should be closed (optional).
229
+ * @param {boolean=} immediate Disable close delay (optional).
174
230
  */
175
- closeTip: function() {
176
- $document.triggerHandler('closePowerTip');
177
- }
231
+ hide: function apiCloseTip(element, immediate) {
232
+ if (element) {
233
+ $(element).first().data(DATA_DISPLAYCONTROLLER).hide(immediate);
234
+ } else {
235
+ if (session.activeHover) {
236
+ session.activeHover.data(DATA_DISPLAYCONTROLLER).hide(true);
237
+ }
238
+ }
239
+ return element;
240
+ },
241
+
242
+ /**
243
+ * Destroy and roll back any powerTip() instance on the specified element.
244
+ * @param {jQuery|Element} element The element with the powerTip instance.
245
+ */
246
+ destroy: function apiDestroy(element) {
247
+ $(element).off('.powertip').each(function destroy() {
248
+ var $this = $(this),
249
+ dataAttributes = [
250
+ DATA_ORIGINALTITLE,
251
+ DATA_DISPLAYCONTROLLER,
252
+ DATA_HASACTIVEHOVER,
253
+ DATA_FORCEDOPEN
254
+ ];
255
+
256
+ if ($this.data(DATA_ORIGINALTITLE)) {
257
+ $this.attr('title', $this.data(DATA_ORIGINALTITLE));
258
+ dataAttributes.push(DATA_POWERTIP);
259
+ }
178
260
 
261
+ $this.removeData(dataAttributes);
262
+ });
263
+ return element;
264
+ }
179
265
  };
180
266
 
267
+ // API aliasing
268
+ $.powerTip.showTip = $.powerTip.show;
269
+ $.powerTip.closeTip = $.powerTip.hide;
270
+
271
+ /**
272
+ * Creates a new CSSCoordinates object.
273
+ * @private
274
+ * @constructor
275
+ */
276
+ function CSSCoordinates() {
277
+ var me = this;
278
+
279
+ // initialize object properties
280
+ me.top = 'auto';
281
+ me.left = 'auto';
282
+ me.right = 'auto';
283
+ me.bottom = 'auto';
284
+
285
+ /**
286
+ * Set a property to a value.
287
+ * @private
288
+ * @param {string} property The name of the property.
289
+ * @param {number} value The value of the property.
290
+ */
291
+ me.set = function(property, value) {
292
+ if ($.isNumeric(value)) {
293
+ me[property] = Math.round(value);
294
+ }
295
+ };
296
+ }
297
+
181
298
  /**
182
299
  * Creates a new tooltip display controller.
183
300
  * @private
184
301
  * @constructor
185
- * @param {Object} element The element that this controller will handle.
302
+ * @param {jQuery} element The element that this controller will handle.
186
303
  * @param {Object} options Options object containing settings.
187
- * @param {TooltipController} tipController The TooltipController for this instance.
304
+ * @param {TooltipController} tipController The TooltipController object for
305
+ * this instance.
188
306
  */
189
307
  function DisplayController(element, options, tipController) {
190
308
  var hoverTimer = null;
@@ -192,24 +310,25 @@
192
310
  /**
193
311
  * Begins the process of showing a tooltip.
194
312
  * @private
195
- * @param {Boolean=} immediate Skip intent testing (optional).
196
- * @param {Boolean=} forceOpen Ignore cursor position and force tooltip to open (optional).
313
+ * @param {boolean=} immediate Skip intent testing (optional).
314
+ * @param {boolean=} forceOpen Ignore cursor position and force tooltip to
315
+ * open (optional).
197
316
  */
198
317
  function openTooltip(immediate, forceOpen) {
199
318
  cancelTimer();
200
- if (!element.data('hasActiveHover')) {
319
+ if (!element.data(DATA_HASACTIVEHOVER)) {
201
320
  if (!immediate) {
202
- session.popOpenImminent = true;
321
+ session.tipOpenImminent = true;
203
322
  hoverTimer = setTimeout(
204
- function() {
323
+ function intentDelay() {
205
324
  hoverTimer = null;
206
- checkForIntent(element);
325
+ checkForIntent();
207
326
  },
208
327
  options.intentPollInterval
209
328
  );
210
329
  } else {
211
330
  if (forceOpen) {
212
- element.data('forcedOpen', true);
331
+ element.data(DATA_FORCEDOPEN, true);
213
332
  }
214
333
  tipController.showTip(element);
215
334
  }
@@ -219,18 +338,20 @@
219
338
  /**
220
339
  * Begins the process of closing a tooltip.
221
340
  * @private
222
- * @param {Boolean=} disableDelay Disable close delay (optional).
341
+ * @param {boolean=} disableDelay Disable close delay (optional).
223
342
  */
224
343
  function closeTooltip(disableDelay) {
225
344
  cancelTimer();
226
- if (element.data('hasActiveHover')) {
227
- session.popOpenImminent = false;
228
- element.data('forcedOpen', false);
345
+ session.tipOpenImminent = false;
346
+ if (element.data(DATA_HASACTIVEHOVER)) {
347
+ element.data(DATA_FORCEDOPEN, false);
229
348
  if (!disableDelay) {
349
+ session.delayInProgress = true;
230
350
  hoverTimer = setTimeout(
231
- function() {
351
+ function closeDelay() {
232
352
  hoverTimer = null;
233
353
  tipController.hideTip(element);
354
+ session.delayInProgress = false;
234
355
  },
235
356
  options.closeDelay
236
357
  );
@@ -241,8 +362,8 @@
241
362
  }
242
363
 
243
364
  /**
244
- * Checks mouse position to make sure that the user intended to hover
245
- * on the specified element before showing the tooltip.
365
+ * Checks mouse position to make sure that the user intended to hover on the
366
+ * specified element before showing the tooltip.
246
367
  * @private
247
368
  */
248
369
  function checkForIntent() {
@@ -268,14 +389,238 @@
268
389
  */
269
390
  function cancelTimer() {
270
391
  hoverTimer = clearTimeout(hoverTimer);
392
+ session.delayInProgress = false;
393
+ }
394
+
395
+ /**
396
+ * Repositions the tooltip on this element.
397
+ * @private
398
+ */
399
+ function repositionTooltip() {
400
+ tipController.resetPosition(element);
271
401
  }
272
402
 
273
403
  // expose the methods
274
- return {
275
- show: openTooltip,
276
- hide: closeTooltip,
277
- cancel: cancelTimer
278
- };
404
+ this.show = openTooltip;
405
+ this.hide = closeTooltip;
406
+ this.cancel = cancelTimer;
407
+ this.resetPosition = repositionTooltip;
408
+ }
409
+
410
+ /**
411
+ * Creates a new Placement Calculator.
412
+ * @private
413
+ * @constructor
414
+ */
415
+ function PlacementCalculator() {
416
+ /**
417
+ * Compute the CSS position to display a tooltip at the specified placement
418
+ * relative to the specified element.
419
+ * @private
420
+ * @param {jQuery} element The element that the tooltip should target.
421
+ * @param {string} placement The placement for the tooltip.
422
+ * @param {number} tipWidth Width of the tooltip element in pixels.
423
+ * @param {number} tipHeight Height of the tooltip element in pixels.
424
+ * @param {number} offset Distance to offset tooltips in pixels.
425
+ * @return {CSSCoordinates} A CSSCoordinates object with the position.
426
+ */
427
+ function computePlacementCoords(element, placement, tipWidth, tipHeight, offset) {
428
+ var placementBase = placement.split('-')[0], // ignore 'alt' for corners
429
+ coords = new CSSCoordinates(),
430
+ position;
431
+
432
+ if (isSvgElement(element)) {
433
+ position = getSvgPlacement(element, placementBase);
434
+ } else {
435
+ position = getHtmlPlacement(element, placementBase);
436
+ }
437
+
438
+ // calculate the appropriate x and y position in the document
439
+ switch (placement) {
440
+ case 'n':
441
+ coords.set('left', position.left - (tipWidth / 2));
442
+ coords.set('bottom', session.windowHeight - position.top + offset);
443
+ break;
444
+ case 'e':
445
+ coords.set('left', position.left + offset);
446
+ coords.set('top', position.top - (tipHeight / 2));
447
+ break;
448
+ case 's':
449
+ coords.set('left', position.left - (tipWidth / 2));
450
+ coords.set('top', position.top + offset);
451
+ break;
452
+ case 'w':
453
+ coords.set('top', position.top - (tipHeight / 2));
454
+ coords.set('right', session.windowWidth - position.left + offset);
455
+ break;
456
+ case 'nw':
457
+ coords.set('bottom', session.windowHeight - position.top + offset);
458
+ coords.set('right', session.windowWidth - position.left - 20);
459
+ break;
460
+ case 'nw-alt':
461
+ coords.set('left', position.left);
462
+ coords.set('bottom', session.windowHeight - position.top + offset);
463
+ break;
464
+ case 'ne':
465
+ coords.set('left', position.left - 20);
466
+ coords.set('bottom', session.windowHeight - position.top + offset);
467
+ break;
468
+ case 'ne-alt':
469
+ coords.set('bottom', session.windowHeight - position.top + offset);
470
+ coords.set('right', session.windowWidth - position.left);
471
+ break;
472
+ case 'sw':
473
+ coords.set('top', position.top + offset);
474
+ coords.set('right', session.windowWidth - position.left - 20);
475
+ break;
476
+ case 'sw-alt':
477
+ coords.set('left', position.left);
478
+ coords.set('top', position.top + offset);
479
+ break;
480
+ case 'se':
481
+ coords.set('left', position.left - 20);
482
+ coords.set('top', position.top + offset);
483
+ break;
484
+ case 'se-alt':
485
+ coords.set('top', position.top + offset);
486
+ coords.set('right', session.windowWidth - position.left);
487
+ break;
488
+ }
489
+
490
+ return coords;
491
+ }
492
+
493
+ /**
494
+ * Finds the tooltip attachment point in the document for a HTML DOM element
495
+ * for the specified placement.
496
+ * @private
497
+ * @param {jQuery} element The element that the tooltip should target.
498
+ * @param {string} placement The placement for the tooltip.
499
+ * @return {Object} An object with the top,left position values.
500
+ */
501
+ function getHtmlPlacement(element, placement) {
502
+ var objectOffset = element.offset(),
503
+ objectWidth = element.outerWidth(),
504
+ objectHeight = element.outerHeight(),
505
+ left,
506
+ top;
507
+
508
+ // calculate the appropriate x and y position in the document
509
+ switch (placement) {
510
+ case 'n':
511
+ left = objectOffset.left + objectWidth / 2;
512
+ top = objectOffset.top;
513
+ break;
514
+ case 'e':
515
+ left = objectOffset.left + objectWidth;
516
+ top = objectOffset.top + objectHeight / 2;
517
+ break;
518
+ case 's':
519
+ left = objectOffset.left + objectWidth / 2;
520
+ top = objectOffset.top + objectHeight;
521
+ break;
522
+ case 'w':
523
+ left = objectOffset.left;
524
+ top = objectOffset.top + objectHeight / 2;
525
+ break;
526
+ case 'nw':
527
+ left = objectOffset.left;
528
+ top = objectOffset.top;
529
+ break;
530
+ case 'ne':
531
+ left = objectOffset.left + objectWidth;
532
+ top = objectOffset.top;
533
+ break;
534
+ case 'sw':
535
+ left = objectOffset.left;
536
+ top = objectOffset.top + objectHeight;
537
+ break;
538
+ case 'se':
539
+ left = objectOffset.left + objectWidth;
540
+ top = objectOffset.top + objectHeight;
541
+ break;
542
+ }
543
+
544
+ return {
545
+ top: top,
546
+ left: left
547
+ };
548
+ }
549
+
550
+ /**
551
+ * Finds the tooltip attachment point in the document for a SVG element for
552
+ * the specified placement.
553
+ * @private
554
+ * @param {jQuery} element The element that the tooltip should target.
555
+ * @param {string} placement The placement for the tooltip.
556
+ * @return {Object} An object with the top,left position values.
557
+ */
558
+ function getSvgPlacement(element, placement) {
559
+ var svgElement = element.closest('svg')[0],
560
+ domElement = element[0],
561
+ point = svgElement.createSVGPoint(),
562
+ boundingBox = domElement.getBBox(),
563
+ matrix = domElement.getScreenCTM(),
564
+ halfWidth = boundingBox.width / 2,
565
+ halfHeight = boundingBox.height / 2,
566
+ placements = [],
567
+ placementKeys = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'],
568
+ coords,
569
+ rotation,
570
+ steps,
571
+ x;
572
+
573
+ function pushPlacement() {
574
+ placements.push(point.matrixTransform(matrix));
575
+ }
576
+
577
+ // get bounding box corners and midpoints
578
+ point.x = boundingBox.x;
579
+ point.y = boundingBox.y;
580
+ pushPlacement();
581
+ point.x += halfWidth;
582
+ pushPlacement();
583
+ point.x += halfWidth;
584
+ pushPlacement();
585
+ point.y += halfHeight;
586
+ pushPlacement();
587
+ point.y += halfHeight;
588
+ pushPlacement();
589
+ point.x -= halfWidth;
590
+ pushPlacement();
591
+ point.x -= halfWidth;
592
+ pushPlacement();
593
+ point.y -= halfHeight;
594
+ pushPlacement();
595
+
596
+ // determine rotation
597
+ if (placements[0].y !== placements[1].y || placements[0].x !== placements[7].x) {
598
+ rotation = Math.atan2(matrix.b, matrix.a) * RAD2DEG;
599
+ steps = Math.ceil(((rotation % 360) - 22.5) / 45);
600
+ if (steps < 1) {
601
+ steps += 8;
602
+ }
603
+ while (steps--) {
604
+ placementKeys.push(placementKeys.shift());
605
+ }
606
+ }
607
+
608
+ // find placement
609
+ for (x = 0; x < placements.length; x++) {
610
+ if (placementKeys[x] === placement) {
611
+ coords = placements[x];
612
+ break;
613
+ }
614
+ }
615
+
616
+ return {
617
+ top: coords.y + session.scrollTop,
618
+ left: coords.x + session.scrollLeft
619
+ };
620
+ }
621
+
622
+ // expose methods
623
+ this.compute = computePlacementCoords;
279
624
  }
280
625
 
281
626
  /**
@@ -285,13 +630,14 @@
285
630
  * @param {Object} options Options object containing settings.
286
631
  */
287
632
  function TooltipController(options) {
633
+ var placementCalculator = new PlacementCalculator(),
634
+ tipElement = $('#' + options.popupId);
288
635
 
289
- // build and append popup div if it does not already exist
290
- var tipElement = $('#' + options.popupId);
636
+ // build and append tooltip div if it does not already exist
291
637
  if (tipElement.length === 0) {
292
- tipElement = $('<div></div>', { id: options.popupId });
638
+ tipElement = $('<div/>', { id: options.popupId });
293
639
  // grab body element if it was not populated when the script loaded
294
- // this hack exists solely for jsfiddle support
640
+ // note: this hack exists solely for jsfiddle support
295
641
  if ($body.length === 0) {
296
642
  $body = $('body');
297
643
  }
@@ -300,79 +646,79 @@
300
646
 
301
647
  // hook mousemove for cursor follow tooltips
302
648
  if (options.followMouse) {
303
- // only one positionTipOnCursor hook per popup element, please
304
- if (!tipElement.data('hasMouseMove')) {
305
- $document.on({
306
- mousemove: positionTipOnCursor,
307
- scroll: positionTipOnCursor
308
- });
649
+ // only one positionTipOnCursor hook per tooltip element, please
650
+ if (!tipElement.data(DATA_HASMOUSEMOVE)) {
651
+ $document.on('mousemove', positionTipOnCursor);
652
+ $window.on('scroll', positionTipOnCursor);
653
+ tipElement.data(DATA_HASMOUSEMOVE, true);
309
654
  }
310
- tipElement.data('hasMouseMove', true);
311
655
  }
312
656
 
313
- // if we want to be able to mouse onto the popup then we need to attach
314
- // hover events to the popup that will cancel a close request on hover
315
- // and start a new close request on mouseleave
316
- if (options.followMouse || options.mouseOnToPopup) {
657
+ // if we want to be able to mouse onto the tooltip then we need to attach
658
+ // hover events to the tooltip that will cancel a close request on hover and
659
+ // start a new close request on mouseleave
660
+ if (options.mouseOnToPopup) {
317
661
  tipElement.on({
318
- mouseenter: function() {
319
- if (tipElement.data('followMouse') || tipElement.data('mouseOnToPopup')) {
320
- // check activeHover in case the mouse cursor entered
321
- // the tooltip during the fadeOut and close cycle
662
+ mouseenter: function tipMouseEnter() {
663
+ // we only let the mouse stay on the tooltip if it is set to let
664
+ // users interact with it
665
+ if (tipElement.data(DATA_MOUSEONTOTIP)) {
666
+ // check activeHover in case the mouse cursor entered the
667
+ // tooltip during the fadeOut and close cycle
322
668
  if (session.activeHover) {
323
- session.activeHover.data('displayController').cancel();
669
+ session.activeHover.data(DATA_DISPLAYCONTROLLER).cancel();
324
670
  }
325
671
  }
326
672
  },
327
- mouseleave: function() {
328
- if (tipElement.data('mouseOnToPopup')) {
329
- // check activeHover in case the mouse cursor entered
330
- // the tooltip during the fadeOut and close cycle
331
- if (session.activeHover) {
332
- session.activeHover.data('displayController').hide();
333
- }
673
+ mouseleave: function tipMouseLeave() {
674
+ // check activeHover in case the mouse cursor entered the
675
+ // tooltip during the fadeOut and close cycle
676
+ if (session.activeHover) {
677
+ session.activeHover.data(DATA_DISPLAYCONTROLLER).hide();
334
678
  }
335
679
  }
336
680
  });
337
681
  }
338
682
 
339
683
  /**
340
- * Gives the specified element the active-hover state and queues up
341
- * the showTip function.
684
+ * Gives the specified element the active-hover state and queues up the
685
+ * showTip function.
342
686
  * @private
343
- * @param {Object} element The element that the tooltip should target.
687
+ * @param {jQuery} element The element that the tooltip should target.
344
688
  */
345
689
  function beginShowTip(element) {
346
- element.data('hasActiveHover', true);
347
- // show popup, asap
348
- tipElement.queue(function(next) {
690
+ element.data(DATA_HASACTIVEHOVER, true);
691
+ // show tooltip, asap
692
+ tipElement.queue(function queueTipInit(next) {
349
693
  showTip(element);
350
694
  next();
351
695
  });
352
696
  }
353
697
 
354
698
  /**
355
- * Shows the tooltip popup, as soon as possible.
699
+ * Shows the tooltip, as soon as possible.
356
700
  * @private
357
- * @param {Object} element The element that the popup should target.
701
+ * @param {jQuery} element The element that the tooltip should target.
358
702
  */
359
703
  function showTip(element) {
360
- // it is possible, especially with keyboard navigation, to move on
361
- // to another element with a tooltip during the queue to get to
362
- // this point in the code. if that happens then we need to not
363
- // proceed or we may have the fadeout callback for the last tooltip
364
- // execute immediately after this code runs, causing bugs.
365
- if (!element.data('hasActiveHover')) {
704
+ var tipContent;
705
+
706
+ // it is possible, especially with keyboard navigation, to move on to
707
+ // another element with a tooltip during the queue to get to this point
708
+ // in the code. if that happens then we need to not proceed or we may
709
+ // have the fadeout callback for the last tooltip execute immediately
710
+ // after this code runs, causing bugs.
711
+ if (!element.data(DATA_HASACTIVEHOVER)) {
366
712
  return;
367
713
  }
368
714
 
369
- // if the popup is open and we got asked to open another one then
370
- // the old one is still in its fadeOut cycle, so wait and try again
371
- if (session.isPopOpen) {
715
+ // if the tooltip is open and we got asked to open another one then the
716
+ // old one is still in its fadeOut cycle, so wait and try again
717
+ if (session.isTipOpen) {
372
718
  if (!session.isClosing) {
373
719
  hideTip(session.activeHover);
374
720
  }
375
- tipElement.delay(100).queue(function(next) {
721
+ tipElement.delay(100).queue(function queueTipAgain(next) {
376
722
  showTip(element);
377
723
  next();
378
724
  });
@@ -382,19 +728,10 @@
382
728
  // trigger powerTipPreRender event
383
729
  element.trigger('powerTipPreRender');
384
730
 
385
- var tipText = element.data('powertip'),
386
- tipTarget = element.data('powertiptarget'),
387
- tipElem = element.data('powertipjq'),
388
- tipContent = tipTarget ? $('#' + tipTarget) : [];
389
-
390
- // set popup content
391
- if (tipText) {
392
- tipElement.html(tipText);
393
- } else if (tipElem && tipElem.length > 0) {
394
- tipElement.empty();
395
- tipElem.clone(true, true).appendTo(tipElement);
396
- } else if (tipContent && tipContent.length > 0) {
397
- tipElement.html($('#' + tipTarget).html());
731
+ // set tooltip content
732
+ tipContent = getTooltipContent(element);
733
+ if (tipContent) {
734
+ tipElement.empty().append(tipContent);
398
735
  } else {
399
736
  // we have no content to display, give up
400
737
  return;
@@ -403,27 +740,21 @@
403
740
  // trigger powerTipRender event
404
741
  element.trigger('powerTipRender');
405
742
 
406
- // hook close event for triggering from the api
407
- $document.on('closePowerTip', function() {
408
- element.data('displayController').hide(true);
409
- });
410
-
411
743
  session.activeHover = element;
412
- session.isPopOpen = true;
744
+ session.isTipOpen = true;
413
745
 
414
- tipElement.data('followMouse', options.followMouse);
415
- tipElement.data('mouseOnToPopup', options.mouseOnToPopup);
746
+ tipElement.data(DATA_MOUSEONTOTIP, options.mouseOnToPopup);
416
747
 
417
- // set popup position
748
+ // set tooltip position
418
749
  if (!options.followMouse) {
419
750
  positionTipOnElement(element);
420
- session.isFixedPopOpen = true;
751
+ session.isFixedTipOpen = true;
421
752
  } else {
422
753
  positionTipOnCursor();
423
754
  }
424
755
 
425
756
  // fadein
426
- tipElement.fadeIn(options.fadeInTime, function() {
757
+ tipElement.fadeIn(options.fadeInTime, function fadeInCallback() {
427
758
  // start desync polling
428
759
  if (!session.desyncTimeout) {
429
760
  session.desyncTimeout = setInterval(closeDesyncedTip, 500);
@@ -435,33 +766,37 @@
435
766
  }
436
767
 
437
768
  /**
438
- * Hides the tooltip popup, immediately.
769
+ * Hides the tooltip.
439
770
  * @private
440
- * @param {Object} element The element that the popup should target.
771
+ * @param {jQuery} element The element that the tooltip should target.
441
772
  */
442
773
  function hideTip(element) {
443
- session.isClosing = true;
444
- element.data('hasActiveHover', false);
445
- element.data('forcedOpen', false);
446
774
  // reset session
775
+ session.isClosing = true;
447
776
  session.activeHover = null;
448
- session.isPopOpen = false;
777
+ session.isTipOpen = false;
778
+
449
779
  // stop desync polling
450
780
  session.desyncTimeout = clearInterval(session.desyncTimeout);
451
- // unhook close event api listener
452
- $document.off('closePowerTip');
781
+
782
+ // reset element state
783
+ element.data(DATA_HASACTIVEHOVER, false);
784
+ element.data(DATA_FORCEDOPEN, false);
785
+
453
786
  // fade out
454
- tipElement.fadeOut(options.fadeOutTime, function() {
787
+ tipElement.fadeOut(options.fadeOutTime, function fadeOutCallback() {
788
+ var coords = new CSSCoordinates();
789
+
790
+ // reset session and tooltip element
455
791
  session.isClosing = false;
456
- session.isFixedPopOpen = false;
792
+ session.isFixedTipOpen = false;
457
793
  tipElement.removeClass();
458
- // support mouse-follow and fixed position pops at the same
459
- // time by moving the popup to the last known cursor location
460
- // after it is hidden
461
- setTipPosition(
462
- session.currentX + options.offset,
463
- session.currentY + options.offset
464
- );
794
+
795
+ // support mouse-follow and fixed position tips at the same time by
796
+ // moving the tooltip to the last cursor location after it is hidden
797
+ coords.set('top', session.currentY + options.offset);
798
+ coords.set('left', session.currentX + options.offset);
799
+ tipElement.css(coords);
465
800
 
466
801
  // trigger powerTipClose event
467
802
  element.trigger('powerTipClose');
@@ -469,268 +804,245 @@
469
804
  }
470
805
 
471
806
  /**
472
- * Checks for a tooltip desync and closes the tooltip if one occurs.
473
- * @private
474
- */
475
- function closeDesyncedTip() {
476
- // It is possible for the mouse cursor to leave an element without
477
- // firing the mouseleave event. This seems to happen (in FF) if the
478
- // element is disabled under mouse cursor, the element is moved out
479
- // from under the mouse cursor (such as a slideDown() occurring
480
- // above it), or if the browser is resized by code moving the
481
- // element from under the mouse cursor. If this happens it will
482
- // result in a desynced tooltip because we wait for any exiting
483
- // open tooltips to close before opening a new one. So we should
484
- // periodically check for a desync situation and close the tip if
485
- // such a situation arises.
486
- if (session.isPopOpen && !session.isClosing) {
487
- var isDesynced = false;
488
-
489
- // case 1: user already moused onto another tip - easy test
490
- if (session.activeHover.data('hasActiveHover') === false) {
491
- isDesynced = true;
492
- } else {
493
- // case 2: hanging tip - have to test if mouse position is
494
- // not over the active hover and not over a tooltip set to
495
- // let the user interact with it.
496
- // for keyboard navigation, this only counts if the element
497
- // does not have focus.
498
- // for tooltips opened via the api we need to check if it
499
- // has the forcedOpen flag.
500
- if (!isMouseOver(session.activeHover) && !session.activeHover.is(":focus") && !session.activeHover.data('forcedOpen')) {
501
- if (tipElement.data('mouseOnToPopup')) {
502
- if (!isMouseOver(tipElement)) {
503
- isDesynced = true;
504
- }
505
- } else {
506
- isDesynced = true;
507
- }
508
- }
509
- }
510
-
511
- if (isDesynced) {
512
- // close the desynced tip
513
- hideTip(session.activeHover);
514
- }
515
- }
516
- }
517
-
518
- /**
519
- * Moves the tooltip popup to the users mouse cursor.
807
+ * Moves the tooltip to the users mouse cursor.
520
808
  * @private
521
809
  */
522
810
  function positionTipOnCursor() {
523
- // to support having fixed powertips on the same page as cursor
524
- // powertips, where both instances are referencing the same popup
525
- // element, we need to keep track of the mouse position constantly,
526
- // but we should only set the pop location if a fixed pop is not
527
- // currently open, a pop open is imminent or active, and the popup
528
- // element in question does have a mouse-follow using it.
529
- if ((session.isPopOpen && !session.isFixedPopOpen) || (session.popOpenImminent && !session.isFixedPopOpen && tipElement.data('hasMouseMove'))) {
811
+ // to support having fixed tooltips on the same page as cursor tooltips,
812
+ // where both instances are referencing the same tooltip element, we
813
+ // need to keep track of the mouse position constantly, but we should
814
+ // only set the tip location if a fixed tip is not currently open, a tip
815
+ // open is imminent or active, and the tooltip element in question does
816
+ // have a mouse-follow using it.
817
+ if (!session.isFixedTipOpen && (session.isTipOpen || (session.tipOpenImminent && tipElement.data(DATA_HASMOUSEMOVE)))) {
530
818
  // grab measurements
531
- var scrollTop = $window.scrollTop(),
532
- windowWidth = $window.width(),
533
- windowHeight = $window.height(),
534
- popWidth = tipElement.outerWidth(),
535
- popHeight = tipElement.outerHeight(),
536
- x = 0,
537
- y = 0;
538
-
539
- // constrain pop to browser viewport
540
- if ((popWidth + session.currentX + options.offset) < windowWidth) {
541
- x = session.currentX + options.offset;
542
- } else {
543
- x = windowWidth - popWidth;
544
- }
545
- if ((popHeight + session.currentY + options.offset) < (scrollTop + windowHeight)) {
546
- y = session.currentY + options.offset;
547
- } else {
548
- y = scrollTop + windowHeight - popHeight;
819
+ var tipWidth = tipElement.outerWidth(),
820
+ tipHeight = tipElement.outerHeight(),
821
+ coords = new CSSCoordinates(),
822
+ collisions,
823
+ collisionCount;
824
+
825
+ // grab collisions
826
+ coords.set('top', session.currentY + options.offset);
827
+ coords.set('left', session.currentX + options.offset);
828
+ collisions = getViewportCollisions(
829
+ coords,
830
+ tipWidth,
831
+ tipHeight
832
+ );
833
+
834
+ // handle tooltip view port collisions
835
+ if (collisions !== Collision.none) {
836
+ collisionCount = countFlags(collisions);
837
+ if (collisionCount === 1) {
838
+ // if there is only one collision (bottom or right) then
839
+ // simply constrain the tooltip to the view port
840
+ if (collisions === Collision.right) {
841
+ coords.set('left', session.windowWidth - tipWidth);
842
+ } else if (collisions === Collision.bottom) {
843
+ coords.set('top', session.scrollTop + session.windowHeight - tipHeight);
844
+ }
845
+ } else {
846
+ // if the tooltip has more than one collision then it is
847
+ // trapped in the corner and should be flipped to get it out
848
+ // of the users way
849
+ coords.set('left', session.currentX - tipWidth - options.offset);
850
+ coords.set('top', session.currentY - tipHeight - options.offset);
851
+ }
549
852
  }
550
853
 
551
854
  // position the tooltip
552
- setTipPosition(x, y);
855
+ tipElement.css(coords);
553
856
  }
554
857
  }
555
858
 
556
859
  /**
557
- * Sets the tooltip popup too the correct position relative to the
558
- * specified target element. Based on options settings.
860
+ * Sets the tooltip to the correct position relative to the specified target
861
+ * element. Based on options settings.
559
862
  * @private
560
- * @param {Object} element The element that the popup should target.
863
+ * @param {jQuery} element The element that the tooltip should target.
561
864
  */
562
865
  function positionTipOnElement(element) {
563
- var tipWidth = tipElement.outerWidth(),
564
- tipHeight = tipElement.outerHeight(),
565
- priorityList,
566
- placementCoords,
567
- finalPlacement,
568
- collisions;
569
-
570
- // with smart placement we will try a series of placement
571
- // options and use the first one that does not collide with the
572
- // browser view port boundaries.
573
- if (options.smartPlacement) {
866
+ var priorityList,
867
+ finalPlacement;
574
868
 
575
- // grab the placement priority list
869
+ if (options.smartPlacement) {
576
870
  priorityList = $.fn.powerTip.smartPlacementLists[options.placement];
577
871
 
578
- // iterate over the priority list and use the first placement
579
- // option that does not collide with the viewport. if they all
580
- // collide then the last placement in the list will be used.
872
+ // iterate over the priority list and use the first placement option
873
+ // that does not collide with the view port. if they all collide
874
+ // then the last placement in the list will be used.
581
875
  $.each(priorityList, function(idx, pos) {
582
- // get placement coordinates
583
- placementCoords = computePlacementCoords(
584
- element,
585
- pos,
586
- tipWidth,
587
- tipHeight
876
+ // place tooltip and find collisions
877
+ var collisions = getViewportCollisions(
878
+ placeTooltip(element, pos),
879
+ tipElement.outerWidth(),
880
+ tipElement.outerHeight()
588
881
  );
589
- finalPlacement = pos;
590
882
 
591
- // find collisions
592
- collisions = getViewportCollisions(
593
- placementCoords,
594
- tipWidth,
595
- tipHeight
596
- );
883
+ // update the final placement variable
884
+ finalPlacement = pos;
597
885
 
598
886
  // break if there were no collisions
599
- if (collisions.length === 0) {
887
+ if (collisions === Collision.none) {
600
888
  return false;
601
889
  }
602
890
  });
603
-
604
891
  } else {
605
-
606
- // if we're not going to use the smart placement feature then
607
- // just compute the coordinates and do it
608
- placementCoords = computePlacementCoords(
609
- element,
610
- options.placement,
611
- tipWidth,
612
- tipHeight
613
- );
892
+ // if we're not going to use the smart placement feature then just
893
+ // compute the coordinates and do it
894
+ placeTooltip(element, options.placement);
614
895
  finalPlacement = options.placement;
615
-
616
896
  }
617
897
 
618
898
  // add placement as class for CSS arrows
619
899
  tipElement.addClass(finalPlacement);
620
-
621
- // position the tooltip
622
- setTipPosition(placementCoords.x, placementCoords.y);
623
900
  }
624
901
 
625
902
  /**
626
- * Compute the top/left coordinates to display the tooltip at the
627
- * specified placement relative to the specified element.
903
+ * Sets the tooltip position to the appropriate values to show the tip at
904
+ * the specified placement. This function will iterate and test the tooltip
905
+ * to support elastic tooltips.
628
906
  * @private
629
- * @param {Object} element The element that the tooltip should target.
630
- * @param {String} placement The placement for the tooltip.
631
- * @param {Number} popWidth Width of the tooltip element in pixels.
632
- * @param {Number} popHeight Height of the tooltip element in pixels.
633
- * @retun {Object} An object with the x and y coordinates.
907
+ * @param {jQuery} element The element that the tooltip should target.
908
+ * @param {string} placement The placement for the tooltip.
909
+ * @return {CSSCoordinates} A CSSCoordinates object with the top, left, and
910
+ * right position values.
634
911
  */
635
- function computePlacementCoords(element, placement, popWidth, popHeight) {
636
- // grab measurements
637
- var objectOffset = element.offset(),
638
- objectWidth = element.outerWidth(),
639
- objectHeight = element.outerHeight(),
640
- x = 0,
641
- y = 0;
912
+ function placeTooltip(element, placement) {
913
+ var iterationCount = 0,
914
+ tipWidth,
915
+ tipHeight,
916
+ coords = new CSSCoordinates();
917
+
918
+ // set the tip to 0,0 to get the full expanded width
919
+ coords.set('top', 0);
920
+ coords.set('left', 0);
921
+ tipElement.css(coords);
922
+
923
+ // to support elastic tooltips we need to check for a change in the
924
+ // rendered dimensions after the tooltip has been positioned
925
+ do {
926
+ // grab the current tip dimensions
927
+ tipWidth = tipElement.outerWidth();
928
+ tipHeight = tipElement.outerHeight();
929
+
930
+ // get placement coordinates
931
+ coords = placementCalculator.compute(
932
+ element,
933
+ placement,
934
+ tipWidth,
935
+ tipHeight,
936
+ options.offset
937
+ );
642
938
 
643
- // calculate the appropriate x and y position in the document
644
- switch (placement) {
645
- case 'n':
646
- x = (objectOffset.left + (objectWidth / 2)) - (popWidth / 2);
647
- y = objectOffset.top - popHeight - options.offset;
648
- break;
649
- case 'e':
650
- x = objectOffset.left + objectWidth + options.offset;
651
- y = (objectOffset.top + (objectHeight / 2)) - (popHeight / 2);
652
- break;
653
- case 's':
654
- x = (objectOffset.left + (objectWidth / 2)) - (popWidth / 2);
655
- y = objectOffset.top + objectHeight + options.offset;
656
- break;
657
- case 'w':
658
- x = objectOffset.left - popWidth - options.offset;
659
- y = (objectOffset.top + (objectHeight / 2)) - (popHeight / 2);
660
- break;
661
- case 'nw':
662
- x = (objectOffset.left - popWidth) + 20;
663
- y = objectOffset.top - popHeight - options.offset;
664
- break;
665
- case 'ne':
666
- x = (objectOffset.left + objectWidth) - 20;
667
- y = objectOffset.top - popHeight - options.offset;
668
- break;
669
- case 'sw':
670
- x = (objectOffset.left - popWidth) + 20;
671
- y = objectOffset.top + objectHeight + options.offset;
672
- break;
673
- case 'se':
674
- x = (objectOffset.left + objectWidth) - 20;
675
- y = objectOffset.top + objectHeight + options.offset;
676
- break;
677
- }
939
+ // place the tooltip
940
+ tipElement.css(coords);
941
+ } while (
942
+ // sanity check: limit to 5 iterations, and...
943
+ ++iterationCount <= 5 &&
944
+ // try again if the dimensions changed after placement
945
+ (tipWidth !== tipElement.outerWidth() || tipHeight !== tipElement.outerHeight())
946
+ );
678
947
 
679
- return {
680
- x: Math.round(x),
681
- y: Math.round(y)
682
- };
948
+ return coords;
683
949
  }
684
950
 
685
951
  /**
686
- * Sets the tooltip CSS position on the document.
952
+ * Checks for a tooltip desync and closes the tooltip if one occurs.
687
953
  * @private
688
- * @param {Number} x Left position in pixels.
689
- * @param {Number} y Top position in pixels.
690
954
  */
691
- function setTipPosition(x, y) {
692
- tipElement.css('left', x + 'px');
693
- tipElement.css('top', y + 'px');
955
+ function closeDesyncedTip() {
956
+ var isDesynced = false;
957
+ // It is possible for the mouse cursor to leave an element without
958
+ // firing the mouseleave or blur event. This most commonly happens when
959
+ // the element is disabled under mouse cursor. If this happens it will
960
+ // result in a desynced tooltip because the tooltip was never asked to
961
+ // close. So we should periodically check for a desync situation and
962
+ // close the tip if such a situation arises.
963
+ if (session.isTipOpen && !session.isClosing && !session.delayInProgress) {
964
+ // user moused onto another tip or active hover is disabled
965
+ if (session.activeHover.data(DATA_HASACTIVEHOVER) === false || session.activeHover.is(':disabled')) {
966
+ isDesynced = true;
967
+ } else {
968
+ // hanging tip - have to test if mouse position is not over the
969
+ // active hover and not over a tooltip set to let the user
970
+ // interact with it.
971
+ // for keyboard navigation: this only counts if the element does
972
+ // not have focus.
973
+ // for tooltips opened via the api: we need to check if it has
974
+ // the forcedOpen flag.
975
+ if (!isMouseOver(session.activeHover) && !session.activeHover.is(':focus') && !session.activeHover.data(DATA_FORCEDOPEN)) {
976
+ if (tipElement.data(DATA_MOUSEONTOTIP)) {
977
+ if (!isMouseOver(tipElement)) {
978
+ isDesynced = true;
979
+ }
980
+ } else {
981
+ isDesynced = true;
982
+ }
983
+ }
984
+ }
985
+
986
+ if (isDesynced) {
987
+ // close the desynced tip
988
+ hideTip(session.activeHover);
989
+ }
990
+ }
694
991
  }
695
992
 
696
993
  // expose methods
697
- return {
698
- showTip: beginShowTip,
699
- hideTip: hideTip
700
- };
994
+ this.showTip = beginShowTip;
995
+ this.hideTip = hideTip;
996
+ this.resetPosition = positionTipOnElement;
701
997
  }
702
998
 
703
999
  /**
704
- * Hooks mouse position tracking to mousemove and scroll events.
705
- * Prevents attaching the events more than once.
1000
+ * Determine whether a jQuery object is an SVG element
706
1001
  * @private
1002
+ * @param {jQuery} element The element to check
1003
+ * @return {boolean} Whether this is an SVG element
707
1004
  */
708
- function initMouseTracking() {
709
- var lastScrollX = 0,
710
- lastScrollY = 0;
1005
+ function isSvgElement(element) {
1006
+ return window.SVGElement && element[0] instanceof SVGElement;
1007
+ }
711
1008
 
1009
+ /**
1010
+ * Initializes the viewport dimension cache and hooks up the mouse position
1011
+ * tracking and viewport dimension tracking events.
1012
+ * Prevents attaching the events more than once.
1013
+ * @private
1014
+ */
1015
+ function initTracking() {
712
1016
  if (!session.mouseTrackingActive) {
713
1017
  session.mouseTrackingActive = true;
714
1018
 
715
- // grab the current scroll position on load
716
- $(function() {
717
- lastScrollX = $document.scrollLeft();
718
- lastScrollY = $document.scrollTop();
1019
+ // grab the current viewport dimensions on load
1020
+ $(function getViewportDimensions() {
1021
+ session.scrollLeft = $window.scrollLeft();
1022
+ session.scrollTop = $window.scrollTop();
1023
+ session.windowWidth = $window.width();
1024
+ session.windowHeight = $window.height();
719
1025
  });
720
1026
 
721
- // hook mouse position tracking
722
- $document.on({
723
- mousemove: trackMouse,
724
- scroll: function() {
725
- var x = $document.scrollLeft(),
726
- y = $document.scrollTop();
727
- if (x !== lastScrollX) {
728
- session.currentX += x - lastScrollX;
729
- lastScrollX = x;
1027
+ // hook mouse move tracking
1028
+ $document.on('mousemove', trackMouse);
1029
+
1030
+ // hook viewport dimensions tracking
1031
+ $window.on({
1032
+ resize: function trackResize() {
1033
+ session.windowWidth = $window.width();
1034
+ session.windowHeight = $window.height();
1035
+ },
1036
+ scroll: function trackScroll() {
1037
+ var x = $window.scrollLeft(),
1038
+ y = $window.scrollTop();
1039
+ if (x !== session.scrollLeft) {
1040
+ session.currentX += x - session.scrollLeft;
1041
+ session.scrollLeft = x;
730
1042
  }
731
- if (y !== lastScrollY) {
732
- session.currentY += y - lastScrollY;
733
- lastScrollY = y;
1043
+ if (y !== session.scrollTop) {
1044
+ session.currentY += y - session.scrollTop;
1045
+ session.scrollTop = y;
734
1046
  }
735
1047
  }
736
1048
  });
@@ -738,9 +1050,9 @@
738
1050
  }
739
1051
 
740
1052
  /**
741
- * Saves the current mouse coordinates to the powerTip session object.
1053
+ * Saves the current mouse coordinates to the session object.
742
1054
  * @private
743
- * @param {Object} event The mousemove event for the document.
1055
+ * @param {jQuery.Event} event The mousemove event for the document.
744
1056
  */
745
1057
  function trackMouse(event) {
746
1058
  session.currentX = event.pageX;
@@ -750,47 +1062,105 @@
750
1062
  /**
751
1063
  * Tests if the mouse is currently over the specified element.
752
1064
  * @private
753
- * @param {Object} element The element to check for hover.
754
- * @return {Boolean}
1065
+ * @param {jQuery} element The element to check for hover.
1066
+ * @return {boolean}
755
1067
  */
756
1068
  function isMouseOver(element) {
757
- var elementPosition = element.offset();
1069
+ // use getBoundingClientRect() because jQuery's width() and height()
1070
+ // methods do not work with SVG elements
1071
+ // compute width/height because those properties do not exist on the object
1072
+ // returned by getBoundingClientRect() in older versions of IE
1073
+ var elementPosition = element.offset(),
1074
+ elementBox = element[0].getBoundingClientRect(),
1075
+ elementWidth = elementBox.right - elementBox.left,
1076
+ elementHeight = elementBox.bottom - elementBox.top;
1077
+
758
1078
  return session.currentX >= elementPosition.left &&
759
- session.currentX <= elementPosition.left + element.outerWidth() &&
1079
+ session.currentX <= elementPosition.left + elementWidth &&
760
1080
  session.currentY >= elementPosition.top &&
761
- session.currentY <= elementPosition.top + element.outerHeight();
1081
+ session.currentY <= elementPosition.top + elementHeight;
762
1082
  }
763
1083
 
764
1084
  /**
765
- * Finds any viewport collisions that an element (the tooltip) would have
766
- * if it were absolutely positioned at the specified coordinates.
1085
+ * Fetches the tooltip content from the specified element's data attributes.
767
1086
  * @private
768
- * @param {Object} coords Coordinates for the element. (e.g. {x: 123, y: 123})
769
- * @param {Number} elementWidth Width of the element in pixels.
770
- * @param {Number} elementHeight Height of the element in pixels.
771
- * @return {Array} Array of words representing directional collisions.
1087
+ * @param {jQuery} element The element to get the tooltip content for.
1088
+ * @return {(string|jQuery|undefined)} The text/HTML string, jQuery object, or
1089
+ * undefined if there was no tooltip content for the element.
1090
+ */
1091
+ function getTooltipContent(element) {
1092
+ var tipText = element.data(DATA_POWERTIP),
1093
+ tipObject = element.data(DATA_POWERTIPJQ),
1094
+ tipTarget = element.data(DATA_POWERTIPTARGET),
1095
+ targetElement,
1096
+ content;
1097
+
1098
+ if (tipText) {
1099
+ if ($.isFunction(tipText)) {
1100
+ tipText = tipText.call(element[0]);
1101
+ }
1102
+ content = tipText;
1103
+ } else if (tipObject) {
1104
+ if ($.isFunction(tipObject)) {
1105
+ tipObject = tipObject.call(element[0]);
1106
+ }
1107
+ if (tipObject.length > 0) {
1108
+ content = tipObject.clone(true, true);
1109
+ }
1110
+ } else if (tipTarget) {
1111
+ targetElement = $('#' + tipTarget);
1112
+ if (targetElement.length > 0) {
1113
+ content = targetElement.html();
1114
+ }
1115
+ }
1116
+
1117
+ return content;
1118
+ }
1119
+
1120
+ /**
1121
+ * Finds any viewport collisions that an element (the tooltip) would have if it
1122
+ * were absolutely positioned at the specified coordinates.
1123
+ * @private
1124
+ * @param {CSSCoordinates} coords Coordinates for the element.
1125
+ * @param {number} elementWidth Width of the element in pixels.
1126
+ * @param {number} elementHeight Height of the element in pixels.
1127
+ * @return {number} Value with the collision flags.
772
1128
  */
773
1129
  function getViewportCollisions(coords, elementWidth, elementHeight) {
774
- var scrollLeft = $window.scrollLeft(),
775
- scrollTop = $window.scrollTop(),
776
- windowWidth = $window.width(),
777
- windowHeight = $window.height(),
778
- collisions = [];
779
-
780
- if (coords.y < scrollTop) {
781
- collisions.push('top');
1130
+ var viewportTop = session.scrollTop,
1131
+ viewportLeft = session.scrollLeft,
1132
+ viewportBottom = viewportTop + session.windowHeight,
1133
+ viewportRight = viewportLeft + session.windowWidth,
1134
+ collisions = Collision.none;
1135
+
1136
+ if (coords.top < viewportTop || Math.abs(coords.bottom - session.windowHeight) - elementHeight < viewportTop) {
1137
+ collisions |= Collision.top;
782
1138
  }
783
- if (coords.y + elementHeight > scrollTop + windowHeight) {
784
- collisions.push('bottom');
1139
+ if (coords.top + elementHeight > viewportBottom || Math.abs(coords.bottom - session.windowHeight) > viewportBottom) {
1140
+ collisions |= Collision.bottom;
785
1141
  }
786
- if (coords.x < scrollLeft) {
787
- collisions.push('left');
1142
+ if (coords.left < viewportLeft || coords.right + elementWidth > viewportRight) {
1143
+ collisions |= Collision.left;
788
1144
  }
789
- if (coords.x + elementWidth > scrollLeft + windowWidth) {
790
- collisions.push('right');
1145
+ if (coords.left + elementWidth > viewportRight || coords.right < viewportLeft) {
1146
+ collisions |= Collision.right;
791
1147
  }
792
1148
 
793
1149
  return collisions;
794
1150
  }
795
1151
 
796
- }(jQuery));
1152
+ /**
1153
+ * Counts the number of bits set on a flags value.
1154
+ * @param {number} value The flags value.
1155
+ * @return {number} The number of bits that have been set.
1156
+ */
1157
+ function countFlags(value) {
1158
+ var count = 0;
1159
+ while (value) {
1160
+ value &= value - 1;
1161
+ count++;
1162
+ }
1163
+ return count;
1164
+ }
1165
+
1166
+ }));