enju_leaf 1.1.0.pre3 → 1.1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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
+ }));