qtip2-jquery-rails 2.0.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 54ad09ca414ab2a41a0c3a660c9f31e65002a350
4
+ data.tar.gz: 42b81c32f880237bf602d094f524782fbba07fb1
5
+ SHA512:
6
+ metadata.gz: cc2c1a85377bb2a7160620810f7576d4a4335f50820aa34b6e1fba9a0d06a07651a1de607aea13b3e1c70a00422bf6acf351fbee83fbdedc2af701fa422deef3
7
+ data.tar.gz: b30f2785eabe968e3aa6af8a432c86a02638ae13d236d4a47276ef2710539e348468b3f11e44fc35e35b74272d5cce63353fb7da879dc0e4dff0a00bcdfe8dcf
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in jquery-qtip2-rails.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Doc Walker
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # Qtip2::Jquery::Rails
2
+
3
+ qTip2 jQuery plugin packaged for the Rails 3.1+ asset pipeline.
4
+
5
+ ## Installation
6
+
7
+ Add these lines to your application's Gemfile:
8
+
9
+ # qtip2 jquery plugin packaged for the rails asset pipeline
10
+ gem 'qtip2-jquery-rails', '~> 2.0.0'
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install qtip2-jquery-rails
19
+
20
+ ## Usage
21
+
22
+ Add these lines to `app/assets/javascripts/application.js`
23
+
24
+ // provides speech bubble tips from gem 'qtip2-jquery-rails':
25
+ //= require jquery.qtip
26
+
27
+ Add these lines to `app/assets/stylesheets/application.css`
28
+
29
+ provides speech bubble tips from gem 'qtip2-jquery-rails':
30
+ = require jquery.qtip
31
+
32
+ ## Contributing
33
+
34
+ 1. Fork it
35
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
36
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
37
+ 4. Push to the branch (`git push origin my-new-feature`)
38
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,10 @@
1
+ require "qtip2/jquery/rails/version"
2
+
3
+ module Qtip2
4
+ module Jquery
5
+ module Rails
6
+ class Engine < ::Rails::Engine
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Qtip2
2
+ module Jquery
3
+ module Rails
4
+ # Use qtip2 version; append a pre-release version identifier if gem
5
+ # is updated without updating version of qtip2 (e.g. "2.0.0.1").
6
+ # Refer to Semantic Versioning 2.0.0 (http://semver.org).
7
+ VERSION = "2.0.0"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'qtip2/jquery/rails/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "qtip2-jquery-rails"
8
+ spec.version = Qtip2::Jquery::Rails::VERSION
9
+ spec.authors = ["Doc Walker"]
10
+ spec.email = ["doc.walker@jameshardie.com"]
11
+ spec.description = %q{qTip2 jQuery plugin}
12
+ spec.summary = %q{qTip2 packaged for the Rails 3.1+ asset pipeline.}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "railties", "~> 3.1"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency "rake"
25
+ end
@@ -0,0 +1,3365 @@
1
+ /*! qTip2 - Pretty powerful tooltips - v2.0.0 - 2012-10-25
2
+ * http://craigsworks.com/projects/qtip2/
3
+ * Copyright (c) 2012 Craig Michael Thompson; Licensed MIT, GPL */
4
+
5
+ /*jslint browser: true, onevar: true, undef: true, nomen: true, bitwise: true, regexp: true, newcap: true, immed: true, strict: true */
6
+ /*global window: false, jQuery: false, console: false, define: false */
7
+
8
+ /* Cache window, document, undefined */
9
+ (function( window, document, undefined ) {
10
+
11
+ // Uses AMD or browser globals to create a jQuery plugin.
12
+ (function( factory ) {
13
+ "use strict";
14
+ if(typeof define === 'function' && define.amd) {
15
+ define(['jquery'], factory);
16
+ }
17
+ else if(jQuery && !jQuery.fn.qtip) {
18
+ factory(jQuery);
19
+ }
20
+ }
21
+ (function($) {
22
+ /* This currently causes issues with Safari 6, so for it's disabled */
23
+ //"use strict"; // (Dis)able ECMAScript "strict" operation for this function. See more: http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/
24
+
25
+ // Munge the primitives - Paul Irish tip
26
+ var TRUE = true,
27
+ FALSE = false,
28
+ NULL = null,
29
+
30
+ // Side names and other stuff
31
+ X = 'x', Y = 'y',
32
+ WIDTH = 'width',
33
+ HEIGHT = 'height',
34
+ TOP = 'top',
35
+ LEFT = 'left',
36
+ BOTTOM = 'bottom',
37
+ RIGHT = 'right',
38
+ CENTER = 'center',
39
+ FLIP = 'flip',
40
+ FLIPINVERT = 'flipinvert',
41
+ SHIFT = 'shift',
42
+
43
+ // Shortcut vars
44
+ QTIP, PLUGINS, MOUSE,
45
+ usedIDs = {},
46
+ uitooltip = 'ui-tooltip',
47
+ widget = 'ui-widget',
48
+ disabled = 'ui-state-disabled',
49
+ selector = 'div.qtip.'+uitooltip,
50
+ defaultClass = uitooltip + '-default',
51
+ focusClass = uitooltip + '-focus',
52
+ hoverClass = uitooltip + '-hover',
53
+ replaceSuffix = '_replacedByqTip',
54
+ oldtitle = 'oldtitle',
55
+ trackingBound,
56
+ redrawContainer;
57
+
58
+ /*
59
+ * redraw() container for width/height calculations
60
+ */
61
+ redrawContainer = $('<div/>', { id: 'qtip-rcontainer' });
62
+ $(function() { redrawContainer.appendTo(document.body); });
63
+
64
+
65
+
66
+ // Option object sanitizer
67
+ function sanitizeOptions(opts)
68
+ {
69
+ var invalid = function(a) { return a === NULL || 'object' !== typeof a; },
70
+ invalidContent = function(c) { return !$.isFunction(c) && ((!c && !c.attr) || c.length < 1 || ('object' === typeof c && !c.jquery)); };
71
+
72
+ if(!opts || 'object' !== typeof opts) { return FALSE; }
73
+
74
+ if(invalid(opts.metadata)) {
75
+ opts.metadata = { type: opts.metadata };
76
+ }
77
+
78
+ if('content' in opts) {
79
+ if(invalid(opts.content) || opts.content.jquery) {
80
+ opts.content = { text: opts.content };
81
+ }
82
+
83
+ if(invalidContent(opts.content.text || FALSE)) {
84
+ opts.content.text = FALSE;
85
+ }
86
+
87
+ if('title' in opts.content) {
88
+ if(invalid(opts.content.title)) {
89
+ opts.content.title = { text: opts.content.title };
90
+ }
91
+
92
+ if(invalidContent(opts.content.title.text || FALSE)) {
93
+ opts.content.title.text = FALSE;
94
+ }
95
+ }
96
+ }
97
+
98
+ if('position' in opts && invalid(opts.position)) {
99
+ opts.position = { my: opts.position, at: opts.position };
100
+ }
101
+
102
+ if('show' in opts && invalid(opts.show)) {
103
+ opts.show = opts.show.jquery ? { target: opts.show } : { event: opts.show };
104
+ }
105
+
106
+ if('hide' in opts && invalid(opts.hide)) {
107
+ opts.hide = opts.hide.jquery ? { target: opts.hide } : { event: opts.hide };
108
+ }
109
+
110
+ if('style' in opts && invalid(opts.style)) {
111
+ opts.style = { classes: opts.style };
112
+ }
113
+
114
+ // Sanitize plugin options
115
+ $.each(PLUGINS, function() {
116
+ if(this.sanitize) { this.sanitize(opts); }
117
+ });
118
+
119
+ return opts;
120
+ }
121
+
122
+ /*
123
+ * Core plugin implementation
124
+ */
125
+ function QTip(target, options, id, attr)
126
+ {
127
+ // Declare this reference
128
+ var self = this,
129
+ docBody = document.body,
130
+ tooltipID = uitooltip + '-' + id,
131
+ isPositioning = 0,
132
+ isDrawing = 0,
133
+ tooltip = $(),
134
+ namespace = '.qtip-' + id,
135
+ elements, cache;
136
+
137
+ // Setup class attributes
138
+ self.id = id;
139
+ self.rendered = FALSE;
140
+ self.destroyed = FALSE;
141
+ self.elements = elements = { target: target };
142
+ self.timers = { img: {} };
143
+ self.options = options;
144
+ self.checks = {};
145
+ self.plugins = {};
146
+ self.cache = cache = {
147
+ event: {},
148
+ target: $(),
149
+ disabled: FALSE,
150
+ attr: attr,
151
+ onTarget: FALSE,
152
+ lastClass: ''
153
+ };
154
+
155
+ /*
156
+ * Private core functions
157
+ */
158
+ function convertNotation(notation)
159
+ {
160
+ var i = 0, obj, option = options,
161
+
162
+ // Split notation into array
163
+ levels = notation.split('.');
164
+
165
+ // Loop through
166
+ while( option = option[ levels[i++] ] ) {
167
+ if(i < levels.length) { obj = option; }
168
+ }
169
+
170
+ return [obj || options, levels.pop()];
171
+ }
172
+
173
+ function triggerEvent(type, args, event) {
174
+ var callback = $.Event('tooltip'+type);
175
+ callback.originalEvent = (event ? $.extend({}, event) : NULL) || cache.event || NULL;
176
+ tooltip.trigger(callback, [self].concat(args || []));
177
+
178
+ return !callback.isDefaultPrevented();
179
+ }
180
+
181
+ function setWidget()
182
+ {
183
+ var on = options.style.widget;
184
+
185
+ tooltip.toggleClass('ui-helper-reset '+widget, on).toggleClass(defaultClass, options.style.def && !on);
186
+
187
+ if(elements.content) {
188
+ elements.content.toggleClass(widget+'-content', on);
189
+ }
190
+
191
+ if(elements.titlebar) {
192
+ elements.titlebar.toggleClass(widget+'-header', on);
193
+ }
194
+ if(elements.button) {
195
+ elements.button.toggleClass(uitooltip+'-icon', !on);
196
+ }
197
+ }
198
+
199
+ function removeTitle(reposition)
200
+ {
201
+ if(elements.title) {
202
+ elements.titlebar.remove();
203
+ elements.titlebar = elements.title = elements.button = NULL;
204
+
205
+ // Reposition if enabled
206
+ if(reposition !== FALSE) { self.reposition(); }
207
+ }
208
+ }
209
+
210
+ function createButton()
211
+ {
212
+ var button = options.content.title.button,
213
+ isString = typeof button === 'string',
214
+ close = isString ? button : 'Close tooltip';
215
+
216
+ if(elements.button) { elements.button.remove(); }
217
+
218
+ // Use custom button if one was supplied by user, else use default
219
+ if(button.jquery) {
220
+ elements.button = button;
221
+ }
222
+ else {
223
+ elements.button = $('<a />', {
224
+ 'class': 'ui-state-default ui-tooltip-close ' + (options.style.widget ? '' : uitooltip+'-icon'),
225
+ 'title': close,
226
+ 'aria-label': close
227
+ })
228
+ .prepend(
229
+ $('<span />', {
230
+ 'class': 'ui-icon ui-icon-close',
231
+ 'html': '&times;'
232
+ })
233
+ );
234
+ }
235
+
236
+ // Create button and setup attributes
237
+ elements.button.appendTo(elements.titlebar)
238
+ .attr('role', 'button')
239
+ .click(function(event) {
240
+ if(!tooltip.hasClass(disabled)) { self.hide(event); }
241
+ return FALSE;
242
+ });
243
+
244
+ // Redraw the tooltip when we're done
245
+ self.redraw();
246
+ }
247
+
248
+ function createTitle()
249
+ {
250
+ var id = tooltipID+'-title';
251
+
252
+ // Destroy previous title element, if present
253
+ if(elements.titlebar) { removeTitle(); }
254
+
255
+ // Create title bar and title elements
256
+ elements.titlebar = $('<div />', {
257
+ 'class': uitooltip + '-titlebar ' + (options.style.widget ? 'ui-widget-header' : '')
258
+ })
259
+ .append(
260
+ elements.title = $('<div />', {
261
+ 'id': id,
262
+ 'class': uitooltip + '-title',
263
+ 'aria-atomic': TRUE
264
+ })
265
+ )
266
+ .insertBefore(elements.content)
267
+
268
+ // Button-specific events
269
+ .delegate('.ui-tooltip-close', 'mousedown keydown mouseup keyup mouseout', function(event) {
270
+ $(this).toggleClass('ui-state-active ui-state-focus', event.type.substr(-4) === 'down');
271
+ })
272
+ .delegate('.ui-tooltip-close', 'mouseover mouseout', function(event){
273
+ $(this).toggleClass('ui-state-hover', event.type === 'mouseover');
274
+ });
275
+
276
+ // Create button if enabled
277
+ if(options.content.title.button) { createButton(); }
278
+
279
+ // Redraw the tooltip dimensions if it's rendered
280
+ else if(self.rendered){ self.redraw(); }
281
+ }
282
+
283
+ function updateButton(button)
284
+ {
285
+ var elem = elements.button,
286
+ title = elements.title;
287
+
288
+ // Make sure tooltip is rendered and if not, return
289
+ if(!self.rendered) { return FALSE; }
290
+
291
+ if(!button) {
292
+ elem.remove();
293
+ }
294
+ else {
295
+ if(!title) {
296
+ createTitle();
297
+ }
298
+ createButton();
299
+ }
300
+ }
301
+
302
+ function updateTitle(content, reposition)
303
+ {
304
+ var elem = elements.title;
305
+
306
+ // Make sure tooltip is rendered and if not, return
307
+ if(!self.rendered || !content) { return FALSE; }
308
+
309
+ // Use function to parse content
310
+ if($.isFunction(content)) {
311
+ content = content.call(target, cache.event, self);
312
+ }
313
+
314
+ // Remove title if callback returns false or null/undefined (but not '')
315
+ if(content === FALSE || (!content && content !== '')) { return removeTitle(FALSE); }
316
+
317
+ // Append new content if its a DOM array and show it if hidden
318
+ else if(content.jquery && content.length > 0) {
319
+ elem.empty().append(content.css({ display: 'block' }));
320
+ }
321
+
322
+ // Content is a regular string, insert the new content
323
+ else { elem.html(content); }
324
+
325
+ // Redraw and reposition
326
+ self.redraw();
327
+ if(reposition !== FALSE && self.rendered && tooltip[0].offsetWidth > 0) {
328
+ self.reposition(cache.event);
329
+ }
330
+ }
331
+
332
+ function updateContent(content, reposition)
333
+ {
334
+ var elem = elements.content;
335
+
336
+ // Make sure tooltip is rendered and content is defined. If not return
337
+ if(!self.rendered || !content) { return FALSE; }
338
+
339
+ // Use function to parse content
340
+ if($.isFunction(content)) {
341
+ content = content.call(target, cache.event, self) || '';
342
+ }
343
+
344
+ // Append new content if its a DOM array and show it if hidden
345
+ if(content.jquery && content.length > 0) {
346
+ elem.empty().append(content.css({ display: 'block' }));
347
+ }
348
+
349
+ // Content is a regular string, insert the new content
350
+ else { elem.html(content); }
351
+
352
+ // Image detection
353
+ function detectImages(next) {
354
+ var images, srcs = {};
355
+
356
+ function imageLoad(image) {
357
+ // Clear src from object and any timers and events associated with the image
358
+ if(image) {
359
+ delete srcs[image.src];
360
+ clearTimeout(self.timers.img[image.src]);
361
+ $(image).unbind(namespace);
362
+ }
363
+
364
+ // If queue is empty after image removal, update tooltip and continue the queue
365
+ if($.isEmptyObject(srcs)) {
366
+ self.redraw();
367
+ if(reposition !== FALSE) {
368
+ self.reposition(cache.event);
369
+ }
370
+
371
+ next();
372
+ }
373
+ }
374
+
375
+ // Find all content images without dimensions, and if no images were found, continue
376
+ if((images = elem.find('img[src]:not([height]):not([width])')).length === 0) { return imageLoad(); }
377
+
378
+ // Apply timer to each image to poll for dimensions
379
+ images.each(function(i, elem) {
380
+ // Skip if the src is already present
381
+ if(srcs[elem.src] !== undefined) { return; }
382
+
383
+ // Keep track of how many times we poll for image dimensions.
384
+ // If it doesn't return in a reasonable amount of time, it's better
385
+ // to display the tooltip, rather than hold up the queue.
386
+ var iterations = 0, maxIterations = 3;
387
+
388
+ (function timer(){
389
+ // When the dimensions are found, remove the image from the queue
390
+ if(elem.height || elem.width || (iterations > maxIterations)) { return imageLoad(elem); }
391
+
392
+ // Increase iterations and restart timer
393
+ iterations += 1;
394
+ self.timers.img[elem.src] = setTimeout(timer, 700);
395
+ }());
396
+
397
+ // Also apply regular load/error event handlers
398
+ $(elem).bind('error'+namespace+' load'+namespace, function(){ imageLoad(this); });
399
+
400
+ // Store the src and element in our object
401
+ srcs[elem.src] = elem;
402
+ });
403
+ }
404
+
405
+ /*
406
+ * If we're still rendering... insert into 'fx' queue our image dimension
407
+ * checker which will halt the showing of the tooltip until image dimensions
408
+ * can be detected properly.
409
+ */
410
+ if(self.rendered < 0) { tooltip.queue('fx', detectImages); }
411
+
412
+ // We're fully rendered, so reset isDrawing flag and proceed without queue delay
413
+ else { isDrawing = 0; detectImages($.noop); }
414
+
415
+ return self;
416
+ }
417
+
418
+ function assignEvents()
419
+ {
420
+ var posOptions = options.position,
421
+ targets = {
422
+ show: options.show.target,
423
+ hide: options.hide.target,
424
+ viewport: $(posOptions.viewport),
425
+ document: $(document),
426
+ body: $(document.body),
427
+ window: $(window)
428
+ },
429
+ events = {
430
+ show: $.trim('' + options.show.event).split(' '),
431
+ hide: $.trim('' + options.hide.event).split(' ')
432
+ },
433
+ IE6 = $.browser.msie && parseInt($.browser.version, 10) === 6;
434
+
435
+ // Define show event method
436
+ function showMethod(event)
437
+ {
438
+ if(tooltip.hasClass(disabled)) { return FALSE; }
439
+
440
+ // Clear hide timers
441
+ clearTimeout(self.timers.show);
442
+ clearTimeout(self.timers.hide);
443
+
444
+ // Start show timer
445
+ var callback = function(){ self.toggle(TRUE, event); };
446
+ if(options.show.delay > 0) {
447
+ self.timers.show = setTimeout(callback, options.show.delay);
448
+ }
449
+ else{ callback(); }
450
+ }
451
+
452
+ // Define hide method
453
+ function hideMethod(event)
454
+ {
455
+ if(tooltip.hasClass(disabled) || isPositioning || isDrawing) { return FALSE; }
456
+
457
+ // Check if new target was actually the tooltip element
458
+ var relatedTarget = $(event.relatedTarget || event.target),
459
+ ontoTooltip = relatedTarget.closest(selector)[0] === tooltip[0],
460
+ ontoTarget = relatedTarget[0] === targets.show[0];
461
+
462
+ // Clear timers and stop animation queue
463
+ clearTimeout(self.timers.show);
464
+ clearTimeout(self.timers.hide);
465
+
466
+ // Prevent hiding if tooltip is fixed and event target is the tooltip. Or if mouse positioning is enabled and cursor momentarily overlaps
467
+ if((posOptions.target === 'mouse' && ontoTooltip) || (options.hide.fixed && ((/mouse(out|leave|move)/).test(event.type) && (ontoTooltip || ontoTarget)))) {
468
+ try { event.preventDefault(); event.stopImmediatePropagation(); } catch(e) {} return;
469
+ }
470
+
471
+ // If tooltip has displayed, start hide timer
472
+ if(options.hide.delay > 0) {
473
+ self.timers.hide = setTimeout(function(){ self.hide(event); }, options.hide.delay);
474
+ }
475
+ else{ self.hide(event); }
476
+ }
477
+
478
+ // Define inactive method
479
+ function inactiveMethod(event)
480
+ {
481
+ if(tooltip.hasClass(disabled)) { return FALSE; }
482
+
483
+ // Clear timer
484
+ clearTimeout(self.timers.inactive);
485
+ self.timers.inactive = setTimeout(function(){ self.hide(event); }, options.hide.inactive);
486
+ }
487
+
488
+ function repositionMethod(event) {
489
+ if(self.rendered && tooltip[0].offsetWidth > 0) { self.reposition(event); }
490
+ }
491
+
492
+ // On mouseenter/mouseleave...
493
+ tooltip.bind('mouseenter'+namespace+' mouseleave'+namespace, function(event) {
494
+ var state = event.type === 'mouseenter';
495
+
496
+ // Focus the tooltip on mouseenter (z-index stacking)
497
+ if(state) { self.focus(event); }
498
+
499
+ // Add hover class
500
+ tooltip.toggleClass(hoverClass, state);
501
+ });
502
+
503
+ // If using mouseout/mouseleave as a hide event...
504
+ if(/mouse(out|leave)/i.test(options.hide.event)) {
505
+ // Hide tooltips when leaving current window/frame (but not select/option elements)
506
+ if(options.hide.leave === 'window') {
507
+ targets.window.bind('mouseout'+namespace+' blur'+namespace, function(event) {
508
+ if(!/select|option/.test(event.target.nodeName) && !event.relatedTarget) { self.hide(event); }
509
+ });
510
+ }
511
+ }
512
+
513
+ // Enable hide.fixed
514
+ if(options.hide.fixed) {
515
+ // Add tooltip as a hide target
516
+ targets.hide = targets.hide.add(tooltip);
517
+
518
+ // Clear hide timer on tooltip hover to prevent it from closing
519
+ tooltip.bind('mouseover'+namespace, function() {
520
+ if(!tooltip.hasClass(disabled)) { clearTimeout(self.timers.hide); }
521
+ });
522
+ }
523
+
524
+ /*
525
+ * Make sure hoverIntent functions properly by using mouseleave to clear show timer if
526
+ * mouseenter/mouseout is used for show.event, even if it isn't in the users options.
527
+ */
528
+ else if(/mouse(over|enter)/i.test(options.show.event)) {
529
+ targets.hide.bind('mouseleave'+namespace, function(event) {
530
+ clearTimeout(self.timers.show);
531
+ });
532
+ }
533
+
534
+ // Hide tooltip on document mousedown if unfocus events are enabled
535
+ if(('' + options.hide.event).indexOf('unfocus') > -1) {
536
+ posOptions.container.closest('html').bind('mousedown'+namespace+' touchstart'+namespace, function(event) {
537
+ var elem = $(event.target),
538
+ enabled = self.rendered && !tooltip.hasClass(disabled) && tooltip[0].offsetWidth > 0,
539
+ isAncestor = elem.parents(selector).filter(tooltip[0]).length > 0;
540
+
541
+ if(elem[0] !== target[0] && elem[0] !== tooltip[0] && !isAncestor &&
542
+ !target.has(elem[0]).length && !elem.attr('disabled')
543
+ ) {
544
+ self.hide(event);
545
+ }
546
+ });
547
+ }
548
+
549
+ // Check if the tooltip hides when inactive
550
+ if('number' === typeof options.hide.inactive) {
551
+ // Bind inactive method to target as a custom event
552
+ targets.show.bind('qtip-'+id+'-inactive', inactiveMethod);
553
+
554
+ // Define events which reset the 'inactive' event handler
555
+ $.each(QTIP.inactiveEvents, function(index, type){
556
+ targets.hide.add(elements.tooltip).bind(type+namespace+'-inactive', inactiveMethod);
557
+ });
558
+ }
559
+
560
+ // Apply hide events
561
+ $.each(events.hide, function(index, type) {
562
+ var showIndex = $.inArray(type, events.show),
563
+ targetHide = $(targets.hide);
564
+
565
+ // Both events and targets are identical, apply events using a toggle
566
+ if((showIndex > -1 && targetHide.add(targets.show).length === targetHide.length) || type === 'unfocus')
567
+ {
568
+ targets.show.bind(type+namespace, function(event) {
569
+ if(tooltip[0].offsetWidth > 0) { hideMethod(event); }
570
+ else { showMethod(event); }
571
+ });
572
+
573
+ // Don't bind the event again
574
+ delete events.show[ showIndex ];
575
+ }
576
+
577
+ // Events are not identical, bind normally
578
+ else { targets.hide.bind(type+namespace, hideMethod); }
579
+ });
580
+
581
+ // Apply show events
582
+ $.each(events.show, function(index, type) {
583
+ targets.show.bind(type+namespace, showMethod);
584
+ });
585
+
586
+ // Check if the tooltip hides when mouse is moved a certain distance
587
+ if('number' === typeof options.hide.distance) {
588
+ // Bind mousemove to target to detect distance difference
589
+ targets.show.add(tooltip).bind('mousemove'+namespace, function(event) {
590
+ var origin = cache.origin || {},
591
+ limit = options.hide.distance,
592
+ abs = Math.abs;
593
+
594
+ // Check if the movement has gone beyond the limit, and hide it if so
595
+ if(abs(event.pageX - origin.pageX) >= limit || abs(event.pageY - origin.pageY) >= limit) {
596
+ self.hide(event);
597
+ }
598
+ });
599
+ }
600
+
601
+ // Mouse positioning events
602
+ if(posOptions.target === 'mouse') {
603
+ // Cache mousemove coords on show targets
604
+ targets.show.bind('mousemove'+namespace, function(event) {
605
+ MOUSE = { pageX: event.pageX, pageY: event.pageY, type: 'mousemove' };
606
+ });
607
+
608
+ // If mouse adjustment is on...
609
+ if(posOptions.adjust.mouse) {
610
+ // Apply a mouseleave event so we don't get problems with overlapping
611
+ if(options.hide.event) {
612
+ // Hide when we leave the tooltip and not onto the show target
613
+ tooltip.bind('mouseleave'+namespace, function(event) {
614
+ if((event.relatedTarget || event.target) !== targets.show[0]) { self.hide(event); }
615
+ });
616
+
617
+ // Track if we're on the target or not
618
+ elements.target.bind('mouseenter'+namespace+' mouseleave'+namespace, function(event) {
619
+ cache.onTarget = event.type === 'mouseenter';
620
+ });
621
+ }
622
+
623
+ // Update tooltip position on mousemove
624
+ targets.document.bind('mousemove'+namespace, function(event) {
625
+ // Update the tooltip position only if the tooltip is visible and adjustment is enabled
626
+ if(self.rendered && cache.onTarget && !tooltip.hasClass(disabled) && tooltip[0].offsetWidth > 0) {
627
+ self.reposition(event || MOUSE);
628
+ }
629
+ });
630
+ }
631
+ }
632
+
633
+ // Adjust positions of the tooltip on window resize if enabled
634
+ if(posOptions.adjust.resize || targets.viewport.length) {
635
+ ($.event.special.resize ? targets.viewport : targets.window).bind('resize'+namespace, repositionMethod);
636
+ }
637
+
638
+ // Adjust tooltip position on scroll if screen adjustment is enabled
639
+ if(targets.viewport.length || (IE6 && tooltip.css('position') === 'fixed')) {
640
+ targets.viewport.bind('scroll'+namespace, repositionMethod);
641
+ }
642
+ }
643
+
644
+ function unassignEvents()
645
+ {
646
+ var targets = [
647
+ options.show.target[0],
648
+ options.hide.target[0],
649
+ self.rendered && elements.tooltip[0],
650
+ options.position.container[0],
651
+ options.position.viewport[0],
652
+ options.position.container.closest('html')[0], // unfocus
653
+ window,
654
+ document
655
+ ];
656
+
657
+ // Check if tooltip is rendered
658
+ if(self.rendered) {
659
+ $([]).pushStack( $.grep(targets, function(i){ return typeof i === 'object'; }) ).unbind(namespace);
660
+ }
661
+
662
+ // Tooltip isn't yet rendered, remove render event
663
+ else { options.show.target.unbind(namespace+'-create'); }
664
+ }
665
+
666
+ // Setup builtin .set() option checks
667
+ self.checks.builtin = {
668
+ // Core checks
669
+ '^id$': function(obj, o, v) {
670
+ var id = v === TRUE ? QTIP.nextid : v,
671
+ tooltipID = uitooltip + '-' + id;
672
+
673
+ if(id !== FALSE && id.length > 0 && !$('#'+tooltipID).length) {
674
+ tooltip[0].id = tooltipID;
675
+ elements.content[0].id = tooltipID + '-content';
676
+ elements.title[0].id = tooltipID + '-title';
677
+ }
678
+ },
679
+
680
+ // Content checks
681
+ '^content.text$': function(obj, o, v){ updateContent(v); },
682
+ '^content.title.text$': function(obj, o, v) {
683
+ // Remove title if content is null
684
+ if(!v) { return removeTitle(); }
685
+
686
+ // If title isn't already created, create it now and update
687
+ if(!elements.title && v) { createTitle(); }
688
+ updateTitle(v);
689
+ },
690
+ '^content.title.button$': function(obj, o, v){ updateButton(v); },
691
+
692
+ // Position checks
693
+ '^position.(my|at)$': function(obj, o, v){
694
+ // Parse new corner value into Corner objecct
695
+ if('string' === typeof v) {
696
+ obj[o] = new PLUGINS.Corner(v);
697
+ }
698
+ },
699
+ '^position.container$': function(obj, o, v){
700
+ if(self.rendered) { tooltip.appendTo(v); }
701
+ },
702
+
703
+ // Show checks
704
+ '^show.ready$': function() {
705
+ if(!self.rendered) { self.render(1); }
706
+ else { self.toggle(TRUE); }
707
+ },
708
+
709
+ // Style checks
710
+ '^style.classes$': function(obj, o, v) {
711
+ tooltip.attr('class', uitooltip + ' qtip ' + v);
712
+ },
713
+ '^style.widget|content.title': setWidget,
714
+
715
+ // Events check
716
+ '^events.(render|show|move|hide|focus|blur)$': function(obj, o, v) {
717
+ tooltip[($.isFunction(v) ? '' : 'un') + 'bind']('tooltip'+o, v);
718
+ },
719
+
720
+ // Properties which require event reassignment
721
+ '^(show|hide|position).(event|target|fixed|inactive|leave|distance|viewport|adjust)': function() {
722
+ var posOptions = options.position;
723
+
724
+ // Set tracking flag
725
+ tooltip.attr('tracking', posOptions.target === 'mouse' && posOptions.adjust.mouse);
726
+
727
+ // Reassign events
728
+ unassignEvents(); assignEvents();
729
+ }
730
+ };
731
+
732
+ /*
733
+ * Public API methods
734
+ */
735
+ $.extend(self, {
736
+ render: function(show)
737
+ {
738
+ if(self.rendered) { return self; } // If tooltip has already been rendered, exit
739
+
740
+ var text = options.content.text,
741
+ title = options.content.title.text,
742
+ posOptions = options.position;
743
+
744
+ // Add ARIA attributes to target
745
+ $.attr(target[0], 'aria-describedby', tooltipID);
746
+
747
+ // Create tooltip element
748
+ tooltip = elements.tooltip = $('<div/>', {
749
+ 'id': tooltipID,
750
+ 'class': uitooltip + ' qtip ' + defaultClass + ' ' + options.style.classes + ' '+ uitooltip + '-pos-' + options.position.my.abbrev(),
751
+ 'width': options.style.width || '',
752
+ 'height': options.style.height || '',
753
+ 'tracking': posOptions.target === 'mouse' && posOptions.adjust.mouse,
754
+
755
+ /* ARIA specific attributes */
756
+ 'role': 'alert',
757
+ 'aria-live': 'polite',
758
+ 'aria-atomic': FALSE,
759
+ 'aria-describedby': tooltipID + '-content',
760
+ 'aria-hidden': TRUE
761
+ })
762
+ .toggleClass(disabled, cache.disabled)
763
+ .data('qtip', self)
764
+ .appendTo(options.position.container)
765
+ .append(
766
+ // Create content element
767
+ elements.content = $('<div />', {
768
+ 'class': uitooltip + '-content',
769
+ 'id': tooltipID + '-content',
770
+ 'aria-atomic': TRUE
771
+ })
772
+ );
773
+
774
+ // Set rendered flag and prevent redundant redraw/reposition calls for now
775
+ self.rendered = -1;
776
+ isDrawing = 1; isPositioning = 1;
777
+
778
+ // Create title...
779
+ if(title) {
780
+ createTitle();
781
+
782
+ // Update title only if its not a callback (called in toggle if so)
783
+ if(!$.isFunction(title)) { updateTitle(title, FALSE); }
784
+ }
785
+
786
+ // Set proper rendered flag and update content if not a callback function (called in toggle)
787
+ if(!$.isFunction(text)) { updateContent(text, FALSE); }
788
+ self.rendered = TRUE;
789
+
790
+ // Setup widget classes
791
+ setWidget();
792
+
793
+ // Assign passed event callbacks (before plugins!)
794
+ $.each(options.events, function(name, callback) {
795
+ if($.isFunction(callback)) {
796
+ tooltip.bind(name === 'toggle' ? 'tooltipshow tooltiphide' : 'tooltip'+name, callback);
797
+ }
798
+ });
799
+
800
+ // Initialize 'render' plugins
801
+ $.each(PLUGINS, function() {
802
+ if(this.initialize === 'render') { this(self); }
803
+ });
804
+
805
+ // Assign events
806
+ assignEvents();
807
+
808
+ /* Queue this part of the render process in our fx queue so we can
809
+ * load images before the tooltip renders fully.
810
+ *
811
+ * See: updateContent method
812
+ */
813
+ tooltip.queue('fx', function(next) {
814
+ // tooltiprender event
815
+ triggerEvent('render');
816
+
817
+ // Reset flags
818
+ isDrawing = 0; isPositioning = 0;
819
+
820
+ // Redraw the tooltip manually now we're fully rendered
821
+ self.redraw();
822
+
823
+ // Show tooltip if needed
824
+ if(options.show.ready || show) {
825
+ self.toggle(TRUE, cache.event, FALSE);
826
+ }
827
+
828
+ next(); // Move on to next method in queue
829
+ });
830
+
831
+ return self;
832
+ },
833
+
834
+ get: function(notation)
835
+ {
836
+ var result, o;
837
+
838
+ switch(notation.toLowerCase())
839
+ {
840
+ case 'dimensions':
841
+ result = {
842
+ height: tooltip.outerHeight(FALSE), width: tooltip.outerWidth(FALSE)
843
+ };
844
+ break;
845
+
846
+ case 'offset':
847
+ result = PLUGINS.offset(tooltip, options.position.container);
848
+ break;
849
+
850
+ default:
851
+ o = convertNotation(notation.toLowerCase());
852
+ result = o[0][ o[1] ];
853
+ result = result.precedance ? result.string() : result;
854
+ break;
855
+ }
856
+
857
+ return result;
858
+ },
859
+
860
+ set: function(option, value)
861
+ {
862
+ var rmove = /^position\.(my|at|adjust|target|container)|style|content|show\.ready/i,
863
+ rdraw = /^content\.(title|attr)|style/i,
864
+ reposition = FALSE,
865
+ redraw = FALSE,
866
+ checks = self.checks,
867
+ name;
868
+
869
+ function callback(notation, args) {
870
+ var category, rule, match;
871
+
872
+ for(category in checks) {
873
+ for(rule in checks[category]) {
874
+ if(match = (new RegExp(rule, 'i')).exec(notation)) {
875
+ args.push(match);
876
+ checks[category][rule].apply(self, args);
877
+ }
878
+ }
879
+ }
880
+ }
881
+
882
+ // Convert singular option/value pair into object form
883
+ if('string' === typeof option) {
884
+ name = option; option = {}; option[name] = value;
885
+ }
886
+ else { option = $.extend(TRUE, {}, option); }
887
+
888
+ // Set all of the defined options to their new values
889
+ $.each(option, function(notation, value) {
890
+ var obj = convertNotation( notation.toLowerCase() ), previous;
891
+
892
+ // Set new obj value
893
+ previous = obj[0][ obj[1] ];
894
+ obj[0][ obj[1] ] = 'object' === typeof value && value.nodeType ? $(value) : value;
895
+
896
+ // Set the new params for the callback
897
+ option[notation] = [obj[0], obj[1], value, previous];
898
+
899
+ // Also check if we need to reposition / redraw
900
+ reposition = rmove.test(notation) || reposition;
901
+ redraw = rdraw.test(notation) || redraw;
902
+ });
903
+
904
+ // Re-sanitize options
905
+ sanitizeOptions(options);
906
+
907
+ /*
908
+ * Execute any valid callbacks for the set options
909
+ * Also set isPositioning/isDrawing so we don't get loads of redundant repositioning
910
+ * and redraw calls.
911
+ */
912
+ isPositioning = isDrawing = 1; $.each(option, callback); isPositioning = isDrawing = 0;
913
+
914
+ // Update position / redraw if needed
915
+ if(self.rendered && tooltip[0].offsetWidth > 0) {
916
+ if(reposition) {
917
+ self.reposition( options.position.target === 'mouse' ? NULL : cache.event );
918
+ }
919
+ if(redraw) { self.redraw(); }
920
+ }
921
+
922
+ return self;
923
+ },
924
+
925
+ toggle: function(state, event)
926
+ {
927
+ // Try to prevent flickering when tooltip overlaps show element
928
+ if(event) {
929
+ if((/over|enter/).test(event.type) && (/out|leave/).test(cache.event.type) &&
930
+ options.show.target.add(event.target).length === options.show.target.length &&
931
+ tooltip.has(event.relatedTarget).length) {
932
+ return self;
933
+ }
934
+
935
+ // Cache event
936
+ cache.event = $.extend({}, event);
937
+ }
938
+
939
+ // Render the tooltip if showing and it isn't already
940
+ if(!self.rendered) { return state ? self.render(1) : self; }
941
+
942
+ var type = state ? 'show' : 'hide',
943
+ opts = options[type],
944
+ otherOpts = options[ !state ? 'show' : 'hide' ],
945
+ posOptions = options.position,
946
+ contentOptions = options.content,
947
+ visible = tooltip[0].offsetWidth > 0,
948
+ animate = state || opts.target.length === 1,
949
+ sameTarget = !event || opts.target.length < 2 || cache.target[0] === event.target,
950
+ showEvent, delay;
951
+
952
+ // Detect state if valid one isn't provided
953
+ if((typeof state).search('boolean|number')) { state = !visible; }
954
+
955
+ // Return if element is already in correct state
956
+ if(!tooltip.is(':animated') && visible === state && sameTarget) { return self; }
957
+
958
+ // tooltipshow/tooltiphide events
959
+ if(!triggerEvent(type, [90])) { return self; }
960
+
961
+ // Set ARIA hidden status attribute
962
+ $.attr(tooltip[0], 'aria-hidden', !!!state);
963
+
964
+ // Execute state specific properties
965
+ if(state) {
966
+ // Store show origin coordinates
967
+ cache.origin = $.extend({}, MOUSE);
968
+
969
+ // Focus the tooltip
970
+ self.focus(event);
971
+
972
+ // Update tooltip content & title if it's a dynamic function
973
+ if($.isFunction(contentOptions.text)) { updateContent(contentOptions.text, FALSE); }
974
+ if($.isFunction(contentOptions.title.text)) { updateTitle(contentOptions.title.text, FALSE); }
975
+
976
+ // Cache mousemove events for positioning purposes (if not already tracking)
977
+ if(!trackingBound && posOptions.target === 'mouse' && posOptions.adjust.mouse) {
978
+ $(document).bind('mousemove.qtip', function(event) {
979
+ MOUSE = { pageX: event.pageX, pageY: event.pageY, type: 'mousemove' };
980
+ });
981
+ trackingBound = TRUE;
982
+ }
983
+
984
+ // Update the tooltip position
985
+ self.reposition(event, arguments[2]);
986
+
987
+ // Hide other tooltips if tooltip is solo
988
+ if(!!opts.solo) {
989
+ $(selector, opts.solo).not(tooltip).qtip('hide', $.Event('tooltipsolo'));
990
+ }
991
+ }
992
+ else {
993
+ // Clear show timer if we're hiding
994
+ clearTimeout(self.timers.show);
995
+
996
+ // Remove cached origin on hide
997
+ delete cache.origin;
998
+
999
+ // Remove mouse tracking event if not needed (all tracking qTips are hidden)
1000
+ if(trackingBound && !$(selector+'[tracking="true"]:visible', opts.solo).not(tooltip).length) {
1001
+ $(document).unbind('mousemove.qtip');
1002
+ trackingBound = FALSE;
1003
+ }
1004
+
1005
+ // Blur the tooltip
1006
+ self.blur(event);
1007
+ }
1008
+
1009
+ // Define post-animation, state specific properties
1010
+ function after() {
1011
+ if(state) {
1012
+ // Prevent antialias from disappearing in IE by removing filter
1013
+ if($.browser.msie) { tooltip[0].style.removeAttribute('filter'); }
1014
+
1015
+ // Remove overflow setting to prevent tip bugs
1016
+ tooltip.css('overflow', '');
1017
+
1018
+ // Autofocus elements if enabled
1019
+ if('string' === typeof opts.autofocus) {
1020
+ $(opts.autofocus, tooltip).focus();
1021
+ }
1022
+
1023
+ // If set, hide tooltip when inactive for delay period
1024
+ opts.target.trigger('qtip-'+id+'-inactive');
1025
+ }
1026
+ else {
1027
+ // Reset CSS states
1028
+ tooltip.css({
1029
+ display: '',
1030
+ visibility: '',
1031
+ opacity: '',
1032
+ left: '',
1033
+ top: ''
1034
+ });
1035
+ }
1036
+
1037
+ // tooltipvisible/tooltiphidden events
1038
+ triggerEvent(state ? 'visible' : 'hidden');
1039
+ }
1040
+
1041
+ // If no effect type is supplied, use a simple toggle
1042
+ if(opts.effect === FALSE || animate === FALSE) {
1043
+ tooltip[ type ]();
1044
+ after.call(tooltip);
1045
+ }
1046
+
1047
+ // Use custom function if provided
1048
+ else if($.isFunction(opts.effect)) {
1049
+ tooltip.stop(1, 1);
1050
+ opts.effect.call(tooltip, self);
1051
+ tooltip.queue('fx', function(n){ after(); n(); });
1052
+ }
1053
+
1054
+ // Use basic fade function by default
1055
+ else { tooltip.fadeTo(90, state ? 1 : 0, after); }
1056
+
1057
+ // If inactive hide method is set, active it
1058
+ if(state) { opts.target.trigger('qtip-'+id+'-inactive'); }
1059
+
1060
+ return self;
1061
+ },
1062
+
1063
+ show: function(event){ return self.toggle(TRUE, event); },
1064
+
1065
+ hide: function(event){ return self.toggle(FALSE, event); },
1066
+
1067
+ focus: function(event)
1068
+ {
1069
+ if(!self.rendered) { return self; }
1070
+
1071
+ var qtips = $(selector),
1072
+ curIndex = parseInt(tooltip[0].style.zIndex, 10),
1073
+ newIndex = QTIP.zindex + qtips.length,
1074
+ cachedEvent = $.extend({}, event),
1075
+ focusedElem;
1076
+
1077
+ // Only update the z-index if it has changed and tooltip is not already focused
1078
+ if(!tooltip.hasClass(focusClass))
1079
+ {
1080
+ // tooltipfocus event
1081
+ if(triggerEvent('focus', [newIndex], cachedEvent)) {
1082
+ // Only update z-index's if they've changed
1083
+ if(curIndex !== newIndex) {
1084
+ // Reduce our z-index's and keep them properly ordered
1085
+ qtips.each(function() {
1086
+ if(this.style.zIndex > curIndex) {
1087
+ this.style.zIndex = this.style.zIndex - 1;
1088
+ }
1089
+ });
1090
+
1091
+ // Fire blur event for focused tooltip
1092
+ qtips.filter('.' + focusClass).qtip('blur', cachedEvent);
1093
+ }
1094
+
1095
+ // Set the new z-index
1096
+ tooltip.addClass(focusClass)[0].style.zIndex = newIndex;
1097
+ }
1098
+ }
1099
+
1100
+ return self;
1101
+ },
1102
+
1103
+ blur: function(event) {
1104
+ // Set focused status to FALSE
1105
+ tooltip.removeClass(focusClass);
1106
+
1107
+ // tooltipblur event
1108
+ triggerEvent('blur', [tooltip.css('zIndex')], event);
1109
+
1110
+ return self;
1111
+ },
1112
+
1113
+ reposition: function(event, effect)
1114
+ {
1115
+ if(!self.rendered || isPositioning) { return self; }
1116
+
1117
+ // Set positioning flag
1118
+ isPositioning = 1;
1119
+
1120
+ var target = options.position.target,
1121
+ posOptions = options.position,
1122
+ my = posOptions.my,
1123
+ at = posOptions.at,
1124
+ adjust = posOptions.adjust,
1125
+ method = adjust.method.split(' '),
1126
+ elemWidth = tooltip.outerWidth(FALSE),
1127
+ elemHeight = tooltip.outerHeight(FALSE),
1128
+ targetWidth = 0,
1129
+ targetHeight = 0,
1130
+ fixed = tooltip.css('position') === 'fixed',
1131
+ viewport = posOptions.viewport,
1132
+ position = { left: 0, top: 0 },
1133
+ container = posOptions.container,
1134
+ visible = tooltip[0].offsetWidth > 0,
1135
+ adjusted, offset, win;
1136
+
1137
+ // Check if absolute position was passed
1138
+ if($.isArray(target) && target.length === 2) {
1139
+ // Force left top and set position
1140
+ at = { x: LEFT, y: TOP };
1141
+ position = { left: target[0], top: target[1] };
1142
+ }
1143
+
1144
+ // Check if mouse was the target
1145
+ else if(target === 'mouse' && ((event && event.pageX) || cache.event.pageX)) {
1146
+ // Force left top to allow flipping
1147
+ at = { x: LEFT, y: TOP };
1148
+
1149
+ // Use cached event if one isn't available for positioning
1150
+ event = MOUSE && MOUSE.pageX && (adjust.mouse || !event || !event.pageX) ? { pageX: MOUSE.pageX, pageY: MOUSE.pageY } :
1151
+ (event && (event.type === 'resize' || event.type === 'scroll') ? cache.event :
1152
+ event && event.pageX && event.type === 'mousemove' ? event :
1153
+ !adjust.mouse && cache.origin && cache.origin.pageX && options.show.distance ? cache.origin :
1154
+ event) || event || cache.event || MOUSE || {};
1155
+
1156
+ // Use event coordinates for position
1157
+ position = { top: event.pageY, left: event.pageX };
1158
+ }
1159
+
1160
+ // Target wasn't mouse or absolute...
1161
+ else {
1162
+ // Check if event targetting is being used
1163
+ if(target === 'event' && event && event.target && event.type !== 'scroll' && event.type !== 'resize') {
1164
+ cache.target = $(event.target);
1165
+ }
1166
+ else if(target !== 'event'){
1167
+ cache.target = $(target.jquery ? target : elements.target);
1168
+ }
1169
+ target = cache.target;
1170
+
1171
+ // Parse the target into a jQuery object and make sure there's an element present
1172
+ target = $(target).eq(0);
1173
+ if(target.length === 0) { return self; }
1174
+
1175
+ // Check if window or document is the target
1176
+ else if(target[0] === document || target[0] === window) {
1177
+ targetWidth = PLUGINS.iOS ? window.innerWidth : target.width();
1178
+ targetHeight = PLUGINS.iOS ? window.innerHeight : target.height();
1179
+
1180
+ if(target[0] === window) {
1181
+ position = {
1182
+ top: (viewport || target).scrollTop(),
1183
+ left: (viewport || target).scrollLeft()
1184
+ };
1185
+ }
1186
+ }
1187
+
1188
+ // Use Imagemap/SVG plugins if needed
1189
+ else if(PLUGINS.imagemap && target.is('area')) {
1190
+ adjusted = PLUGINS.imagemap(self, target, at, PLUGINS.viewport ? method : FALSE);
1191
+ }
1192
+ else if(PLUGINS.svg && typeof target[0].xmlbase === 'string') {
1193
+ adjusted = PLUGINS.svg(self, target, at, PLUGINS.viewport ? method : FALSE);
1194
+ }
1195
+
1196
+ else {
1197
+ targetWidth = target.outerWidth(FALSE);
1198
+ targetHeight = target.outerHeight(FALSE);
1199
+
1200
+ position = PLUGINS.offset(target, container);
1201
+ }
1202
+
1203
+ // Parse returned plugin values into proper variables
1204
+ if(adjusted) {
1205
+ targetWidth = adjusted.width;
1206
+ targetHeight = adjusted.height;
1207
+ offset = adjusted.offset;
1208
+ position = adjusted.position;
1209
+ }
1210
+
1211
+ // Adjust for position.fixed tooltips (and also iOS scroll bug in v3.2-4.0 & v4.3-4.3.2)
1212
+ if((PLUGINS.iOS > 3.1 && PLUGINS.iOS < 4.1) ||
1213
+ (PLUGINS.iOS >= 4.3 && PLUGINS.iOS < 4.33) ||
1214
+ (!PLUGINS.iOS && fixed)
1215
+ ){
1216
+ win = $(window);
1217
+ position.left -= win.scrollLeft();
1218
+ position.top -= win.scrollTop();
1219
+ }
1220
+
1221
+ // Adjust position relative to target
1222
+ position.left += at.x === RIGHT ? targetWidth : at.x === CENTER ? targetWidth / 2 : 0;
1223
+ position.top += at.y === BOTTOM ? targetHeight : at.y === CENTER ? targetHeight / 2 : 0;
1224
+ }
1225
+
1226
+ // Adjust position relative to tooltip
1227
+ position.left += adjust.x + (my.x === RIGHT ? -elemWidth : my.x === CENTER ? -elemWidth / 2 : 0);
1228
+ position.top += adjust.y + (my.y === BOTTOM ? -elemHeight : my.y === CENTER ? -elemHeight / 2 : 0);
1229
+
1230
+ // Use viewport adjustment plugin if enabled
1231
+ if(PLUGINS.viewport) {
1232
+ position.adjusted = PLUGINS.viewport(
1233
+ self, position, posOptions, targetWidth, targetHeight, elemWidth, elemHeight
1234
+ );
1235
+
1236
+ // Apply offsets supplied by positioning plugin (if used)
1237
+ if(offset && position.adjusted.left) { position.left += offset.left; }
1238
+ if(offset && position.adjusted.top) { position.top += offset.top; }
1239
+ }
1240
+
1241
+ // Viewport adjustment is disabled, set values to zero
1242
+ else { position.adjusted = { left: 0, top: 0 }; }
1243
+
1244
+ // tooltipmove event
1245
+ if(!triggerEvent('move', [position, viewport.elem || viewport], event)) { return self; }
1246
+ delete position.adjusted;
1247
+
1248
+ // If effect is disabled, target it mouse, no animation is defined or positioning gives NaN out, set CSS directly
1249
+ if(effect === FALSE || !visible || isNaN(position.left) || isNaN(position.top) || target === 'mouse' || !$.isFunction(posOptions.effect)) {
1250
+ tooltip.css(position);
1251
+ }
1252
+
1253
+ // Use custom function if provided
1254
+ else if($.isFunction(posOptions.effect)) {
1255
+ posOptions.effect.call(tooltip, self, $.extend({}, position));
1256
+ tooltip.queue(function(next) {
1257
+ // Reset attributes to avoid cross-browser rendering bugs
1258
+ $(this).css({ opacity: '', height: '' });
1259
+ if($.browser.msie) { this.style.removeAttribute('filter'); }
1260
+
1261
+ next();
1262
+ });
1263
+ }
1264
+
1265
+ // Set positioning flag
1266
+ isPositioning = 0;
1267
+
1268
+ return self;
1269
+ },
1270
+
1271
+ // Max/min width simulator function for all browsers.. yeaaah!
1272
+ redraw: function()
1273
+ {
1274
+ if(self.rendered < 1 || isDrawing) { return self; }
1275
+
1276
+ var style = options.style,
1277
+ container = options.position.container,
1278
+ perc, width, max, min;
1279
+
1280
+ // Set drawing flag
1281
+ isDrawing = 1;
1282
+
1283
+ // tooltipredraw event
1284
+ triggerEvent('redraw');
1285
+
1286
+ // If tooltip has a set height/width, just set it... like a boss!
1287
+ if(style.height) { tooltip.css(HEIGHT, style.height); }
1288
+ if(style.width) { tooltip.css(WIDTH, style.width); }
1289
+
1290
+ // Simulate max/min width if not set width present...
1291
+ else {
1292
+ // Reset width and add fluid class
1293
+ tooltip.css(WIDTH, '').appendTo(redrawContainer);
1294
+
1295
+ // Grab our tooltip width (add 1 if odd so we don't get wrapping problems.. huzzah!)
1296
+ width = tooltip.width();
1297
+ if(width % 2 < 1) { width += 1; }
1298
+
1299
+ // Grab our max/min properties
1300
+ max = tooltip.css('max-width') || '';
1301
+ min = tooltip.css('min-width') || '';
1302
+
1303
+ // Parse into proper pixel values
1304
+ perc = (max + min).indexOf('%') > -1 ? container.width() / 100 : 0;
1305
+ max = ((max.indexOf('%') > -1 ? perc : 1) * parseInt(max, 10)) || width;
1306
+ min = ((min.indexOf('%') > -1 ? perc : 1) * parseInt(min, 10)) || 0;
1307
+
1308
+ // Determine new dimension size based on max/min/current values
1309
+ width = max + min ? Math.min(Math.max(width, min), max) : width;
1310
+
1311
+ // Set the newly calculated width and remvoe fluid class
1312
+ tooltip.css(WIDTH, Math.round(width)).appendTo(container);
1313
+ }
1314
+
1315
+ // tooltipredrawn event
1316
+ triggerEvent('redrawn');
1317
+
1318
+ // Set drawing flag
1319
+ isDrawing = 0;
1320
+
1321
+ return self;
1322
+ },
1323
+
1324
+ disable: function(state)
1325
+ {
1326
+ if('boolean' !== typeof state) {
1327
+ state = !(tooltip.hasClass(disabled) || cache.disabled);
1328
+ }
1329
+
1330
+ if(self.rendered) {
1331
+ tooltip.toggleClass(disabled, state);
1332
+ $.attr(tooltip[0], 'aria-disabled', state);
1333
+ }
1334
+ else {
1335
+ cache.disabled = !!state;
1336
+ }
1337
+
1338
+ return self;
1339
+ },
1340
+
1341
+ enable: function() { return self.disable(FALSE); },
1342
+
1343
+ destroy: function()
1344
+ {
1345
+ var t = target[0],
1346
+ title = $.attr(t, oldtitle),
1347
+ elemAPI = target.data('qtip');
1348
+
1349
+ // Set flag the signify destroy is taking place to plugins
1350
+ self.destroyed = TRUE;
1351
+
1352
+ // Destroy tooltip and any associated plugins if rendered
1353
+ if(self.rendered) {
1354
+ tooltip.stop(1,0).remove();
1355
+
1356
+ $.each(self.plugins, function() {
1357
+ if(this.destroy) { this.destroy(); }
1358
+ });
1359
+ }
1360
+
1361
+ // Clear timers and remove bound events
1362
+ clearTimeout(self.timers.show);
1363
+ clearTimeout(self.timers.hide);
1364
+ unassignEvents();
1365
+
1366
+ // If the API if actually this qTip API...
1367
+ if(!elemAPI || self === elemAPI) {
1368
+ // Remove api object
1369
+ $.removeData(t, 'qtip');
1370
+
1371
+ // Reset old title attribute if removed
1372
+ if(options.suppress && title) {
1373
+ $.attr(t, 'title', title);
1374
+ target.removeAttr(oldtitle);
1375
+ }
1376
+
1377
+ // Remove ARIA attributes
1378
+ target.removeAttr('aria-describedby');
1379
+ }
1380
+
1381
+ // Remove qTip events associated with this API
1382
+ target.unbind('.qtip-'+id);
1383
+
1384
+ // Remove ID from sued id object
1385
+ delete usedIDs[self.id];
1386
+
1387
+ return target;
1388
+ }
1389
+ });
1390
+ }
1391
+
1392
+ // Initialization method
1393
+ function init(id, opts)
1394
+ {
1395
+ var obj, posOptions, attr, config, title,
1396
+
1397
+ // Setup element references
1398
+ elem = $(this),
1399
+ docBody = $(document.body),
1400
+
1401
+ // Use document body instead of document element if needed
1402
+ newTarget = this === document ? docBody : elem,
1403
+
1404
+ // Grab metadata from element if plugin is present
1405
+ metadata = (elem.metadata) ? elem.metadata(opts.metadata) : NULL,
1406
+
1407
+ // If metadata type if HTML5, grab 'name' from the object instead, or use the regular data object otherwise
1408
+ metadata5 = opts.metadata.type === 'html5' && metadata ? metadata[opts.metadata.name] : NULL,
1409
+
1410
+ // Grab data from metadata.name (or data-qtipopts as fallback) using .data() method,
1411
+ html5 = elem.data(opts.metadata.name || 'qtipopts');
1412
+
1413
+ // If we don't get an object returned attempt to parse it manualyl without parseJSON
1414
+ try { html5 = typeof html5 === 'string' ? $.parseJSON(html5) : html5; } catch(e) {}
1415
+
1416
+ // Merge in and sanitize metadata
1417
+ config = $.extend(TRUE, {}, QTIP.defaults, opts,
1418
+ typeof html5 === 'object' ? sanitizeOptions(html5) : NULL,
1419
+ sanitizeOptions(metadata5 || metadata));
1420
+
1421
+ // Re-grab our positioning options now we've merged our metadata and set id to passed value
1422
+ posOptions = config.position;
1423
+ config.id = id;
1424
+
1425
+ // Setup missing content if none is detected
1426
+ if('boolean' === typeof config.content.text) {
1427
+ attr = elem.attr(config.content.attr);
1428
+
1429
+ // Grab from supplied attribute if available
1430
+ if(config.content.attr !== FALSE && attr) { config.content.text = attr; }
1431
+
1432
+ // No valid content was found, abort render
1433
+ else { return FALSE; }
1434
+ }
1435
+
1436
+ // Setup target options
1437
+ if(!posOptions.container.length) { posOptions.container = docBody; }
1438
+ if(posOptions.target === FALSE) { posOptions.target = newTarget; }
1439
+ if(config.show.target === FALSE) { config.show.target = newTarget; }
1440
+ if(config.show.solo === TRUE) { config.show.solo = posOptions.container.closest('body'); }
1441
+ if(config.hide.target === FALSE) { config.hide.target = newTarget; }
1442
+ if(config.position.viewport === TRUE) { config.position.viewport = posOptions.container; }
1443
+
1444
+ // Ensure we only use a single container
1445
+ posOptions.container = posOptions.container.eq(0);
1446
+
1447
+ // Convert position corner values into x and y strings
1448
+ posOptions.at = new PLUGINS.Corner(posOptions.at);
1449
+ posOptions.my = new PLUGINS.Corner(posOptions.my);
1450
+
1451
+ // Destroy previous tooltip if overwrite is enabled, or skip element if not
1452
+ if($.data(this, 'qtip')) {
1453
+ if(config.overwrite) {
1454
+ elem.qtip('destroy');
1455
+ }
1456
+ else if(config.overwrite === FALSE) {
1457
+ return FALSE;
1458
+ }
1459
+ }
1460
+
1461
+ // Remove title attribute and store it if present
1462
+ if(config.suppress && (title = $.attr(this, 'title'))) {
1463
+ // Final attr call fixes event delegatiom and IE default tooltip showing problem
1464
+ $(this).removeAttr('title').attr(oldtitle, title).attr('title', '');
1465
+ }
1466
+
1467
+ // Initialize the tooltip and add API reference
1468
+ obj = new QTip(elem, config, id, !!attr);
1469
+ $.data(this, 'qtip', obj);
1470
+
1471
+ // Catch remove/removeqtip events on target element to destroy redundant tooltip
1472
+ elem.bind('remove.qtip-'+id+' removeqtip.qtip-'+id, function(){ obj.destroy(); });
1473
+
1474
+ return obj;
1475
+ }
1476
+
1477
+ // jQuery $.fn extension method
1478
+ QTIP = $.fn.qtip = function(options, notation, newValue)
1479
+ {
1480
+ var command = ('' + options).toLowerCase(), // Parse command
1481
+ returned = NULL,
1482
+ args = $.makeArray(arguments).slice(1),
1483
+ event = args[args.length - 1],
1484
+ opts = this[0] ? $.data(this[0], 'qtip') : NULL;
1485
+
1486
+ // Check for API request
1487
+ if((!arguments.length && opts) || command === 'api') {
1488
+ return opts;
1489
+ }
1490
+
1491
+ // Execute API command if present
1492
+ else if('string' === typeof options)
1493
+ {
1494
+ this.each(function()
1495
+ {
1496
+ var api = $.data(this, 'qtip');
1497
+ if(!api) { return TRUE; }
1498
+
1499
+ // Cache the event if possible
1500
+ if(event && event.timeStamp) { api.cache.event = event; }
1501
+
1502
+ // Check for specific API commands
1503
+ if((command === 'option' || command === 'options') && notation) {
1504
+ if($.isPlainObject(notation) || newValue !== undefined) {
1505
+ api.set(notation, newValue);
1506
+ }
1507
+ else {
1508
+ returned = api.get(notation);
1509
+ return FALSE;
1510
+ }
1511
+ }
1512
+
1513
+ // Execute API command
1514
+ else if(api[command]) {
1515
+ api[command].apply(api[command], args);
1516
+ }
1517
+ });
1518
+
1519
+ return returned !== NULL ? returned : this;
1520
+ }
1521
+
1522
+ // No API commands. validate provided options and setup qTips
1523
+ else if('object' === typeof options || !arguments.length)
1524
+ {
1525
+ opts = sanitizeOptions($.extend(TRUE, {}, options));
1526
+
1527
+ // Bind the qTips
1528
+ return QTIP.bind.call(this, opts, event);
1529
+ }
1530
+ };
1531
+
1532
+ // $.fn.qtip Bind method
1533
+ QTIP.bind = function(opts, event)
1534
+ {
1535
+ return this.each(function(i) {
1536
+ var options, targets, events, namespace, api, id;
1537
+
1538
+ // Find next available ID, or use custom ID if provided
1539
+ id = $.isArray(opts.id) ? opts.id[i] : opts.id;
1540
+ id = !id || id === FALSE || id.length < 1 || usedIDs[id] ? QTIP.nextid++ : (usedIDs[id] = id);
1541
+
1542
+ // Setup events namespace
1543
+ namespace = '.qtip-'+id+'-create';
1544
+
1545
+ // Initialize the qTip and re-grab newly sanitized options
1546
+ api = init.call(this, id, opts);
1547
+ if(api === FALSE) { return TRUE; }
1548
+ options = api.options;
1549
+
1550
+ // Initialize plugins
1551
+ $.each(PLUGINS, function() {
1552
+ if(this.initialize === 'initialize') { this(api); }
1553
+ });
1554
+
1555
+ // Determine hide and show targets
1556
+ targets = { show: options.show.target, hide: options.hide.target };
1557
+ events = {
1558
+ show: $.trim('' + options.show.event).replace(/ /g, namespace+' ') + namespace,
1559
+ hide: $.trim('' + options.hide.event).replace(/ /g, namespace+' ') + namespace
1560
+ };
1561
+
1562
+ /*
1563
+ * Make sure hoverIntent functions properly by using mouseleave as a hide event if
1564
+ * mouseenter/mouseout is used for show.event, even if it isn't in the users options.
1565
+ */
1566
+ if(/mouse(over|enter)/i.test(events.show) && !/mouse(out|leave)/i.test(events.hide)) {
1567
+ events.hide += ' mouseleave' + namespace;
1568
+ }
1569
+
1570
+ /*
1571
+ * Also make sure initial mouse targetting works correctly by caching mousemove coords
1572
+ * on show targets before the tooltip has rendered.
1573
+ *
1574
+ * Also set onTarget when triggered to keep mouse tracking working
1575
+ */
1576
+ targets.show.bind('mousemove'+namespace, function(event) {
1577
+ MOUSE = { pageX: event.pageX, pageY: event.pageY, type: 'mousemove' };
1578
+ api.cache.onTarget = TRUE;
1579
+ });
1580
+
1581
+ // Define hoverIntent function
1582
+ function hoverIntent(event) {
1583
+ function render() {
1584
+ // Cache mouse coords,render and render the tooltip
1585
+ api.render(typeof event === 'object' || options.show.ready);
1586
+
1587
+ // Unbind show and hide events
1588
+ targets.show.add(targets.hide).unbind(namespace);
1589
+ }
1590
+
1591
+ // Only continue if tooltip isn't disabled
1592
+ if(api.cache.disabled) { return FALSE; }
1593
+
1594
+ // Cache the event data
1595
+ api.cache.event = $.extend({}, event);
1596
+ api.cache.target = event ? $(event.target) : [undefined];
1597
+
1598
+ // Start the event sequence
1599
+ if(options.show.delay > 0) {
1600
+ clearTimeout(api.timers.show);
1601
+ api.timers.show = setTimeout(render, options.show.delay);
1602
+ if(events.show !== events.hide) {
1603
+ targets.hide.bind(events.hide, function() { clearTimeout(api.timers.show); });
1604
+ }
1605
+ }
1606
+ else { render(); }
1607
+ }
1608
+
1609
+ // Bind show events to target
1610
+ targets.show.bind(events.show, hoverIntent);
1611
+
1612
+ // Prerendering is enabled, create tooltip now
1613
+ if(options.show.ready || options.prerender) { hoverIntent(event); }
1614
+ });
1615
+ };
1616
+
1617
+ // Setup base plugins
1618
+ PLUGINS = QTIP.plugins = {
1619
+ // Corner object parser
1620
+ Corner: function(corner) {
1621
+ corner = ('' + corner).replace(/([A-Z])/, ' $1').replace(/middle/gi, CENTER).toLowerCase();
1622
+ this.x = (corner.match(/left|right/i) || corner.match(/center/) || ['inherit'])[0].toLowerCase();
1623
+ this.y = (corner.match(/top|bottom|center/i) || ['inherit'])[0].toLowerCase();
1624
+
1625
+ var f = corner.charAt(0); this.precedance = (f === 't' || f === 'b' ? Y : X);
1626
+
1627
+ this.string = function() { return this.precedance === Y ? this.y+this.x : this.x+this.y; };
1628
+ this.abbrev = function() {
1629
+ var x = this.x.substr(0,1), y = this.y.substr(0,1);
1630
+ return x === y ? x : this.precedance === Y ? y + x : x + y;
1631
+ };
1632
+
1633
+ this.invertx = function(center) { this.x = this.x === LEFT ? RIGHT : this.x === RIGHT ? LEFT : center || this.x; };
1634
+ this.inverty = function(center) { this.y = this.y === TOP ? BOTTOM : this.y === BOTTOM ? TOP : center || this.y; };
1635
+
1636
+ this.clone = function() {
1637
+ return {
1638
+ x: this.x, y: this.y, precedance: this.precedance,
1639
+ string: this.string, abbrev: this.abbrev, clone: this.clone,
1640
+ invertx: this.invertx, inverty: this.inverty
1641
+ };
1642
+ };
1643
+ },
1644
+
1645
+ // Custom (more correct for qTip!) offset calculator
1646
+ offset: function(elem, container) {
1647
+ var pos = elem.offset(),
1648
+ docBody = elem.closest('body')[0],
1649
+ parent = container, scrolled,
1650
+ coffset, overflow;
1651
+
1652
+ function scroll(e, i) {
1653
+ pos.left += i * e.scrollLeft();
1654
+ pos.top += i * e.scrollTop();
1655
+ }
1656
+
1657
+ if(parent) {
1658
+ // Compensate for non-static containers offset
1659
+ do {
1660
+ if(parent.css('position') !== 'static') {
1661
+ coffset = parent.position();
1662
+
1663
+ // Account for element positioning, borders and margins
1664
+ pos.left -= coffset.left + (parseInt(parent.css('borderLeftWidth'), 10) || 0) + (parseInt(parent.css('marginLeft'), 10) || 0);
1665
+ pos.top -= coffset.top + (parseInt(parent.css('borderTopWidth'), 10) || 0) + (parseInt(parent.css('marginTop'), 10) || 0);
1666
+
1667
+ // If this is the first parent element with an overflow of "scroll" or "auto", store it
1668
+ if(!scrolled && (overflow = parent.css('overflow')) !== 'hidden' && overflow !== 'visible') { scrolled = parent; }
1669
+ }
1670
+ }
1671
+ while((parent = $(parent[0].offsetParent)).length);
1672
+
1673
+ // Compensate for containers scroll if it also has an offsetParent
1674
+ if(scrolled && scrolled[0] !== docBody) { scroll( scrolled, 1 ); }
1675
+ }
1676
+
1677
+ return pos;
1678
+ },
1679
+
1680
+ /*
1681
+ * iOS version detection
1682
+ */
1683
+ iOS: parseFloat(
1684
+ ('' + (/CPU.*OS ([0-9_]{1,5})|(CPU like).*AppleWebKit.*Mobile/i.exec(navigator.userAgent) || [0,''])[1])
1685
+ .replace('undefined', '3_2').replace('_', '.').replace('_', '')
1686
+ ) || FALSE,
1687
+
1688
+ /*
1689
+ * jQuery-specific $.fn overrides
1690
+ */
1691
+ fn: {
1692
+ /* Allow other plugins to successfully retrieve the title of an element with a qTip applied */
1693
+ attr: function(attr, val) {
1694
+ if(this.length) {
1695
+ var self = this[0],
1696
+ title = 'title',
1697
+ api = $.data(self, 'qtip');
1698
+
1699
+ if(attr === title && api && 'object' === typeof api && api.options.suppress) {
1700
+ if(arguments.length < 2) {
1701
+ return $.attr(self, oldtitle);
1702
+ }
1703
+
1704
+ // If qTip is rendered and title was originally used as content, update it
1705
+ if(api && api.options.content.attr === title && api.cache.attr) {
1706
+ api.set('content.text', val);
1707
+ }
1708
+
1709
+ // Use the regular attr method to set, then cache the result
1710
+ return this.attr(oldtitle, val);
1711
+ }
1712
+ }
1713
+
1714
+ return $.fn['attr'+replaceSuffix].apply(this, arguments);
1715
+ },
1716
+
1717
+ /* Allow clone to correctly retrieve cached title attributes */
1718
+ clone: function(keepData) {
1719
+ var titles = $([]), title = 'title',
1720
+
1721
+ // Clone our element using the real clone method
1722
+ elems = $.fn['clone'+replaceSuffix].apply(this, arguments);
1723
+
1724
+ // Grab all elements with an oldtitle set, and change it to regular title attribute, if keepData is false
1725
+ if(!keepData) {
1726
+ elems.filter('['+oldtitle+']').attr('title', function() {
1727
+ return $.attr(this, oldtitle);
1728
+ })
1729
+ .removeAttr(oldtitle);
1730
+ }
1731
+
1732
+ return elems;
1733
+ }
1734
+ }
1735
+ };
1736
+
1737
+ // Apply the fn overrides above
1738
+ $.each(PLUGINS.fn, function(name, func) {
1739
+ if(!func || $.fn[name+replaceSuffix]) { return TRUE; }
1740
+
1741
+ var old = $.fn[name+replaceSuffix] = $.fn[name];
1742
+ $.fn[name] = function() {
1743
+ return func.apply(this, arguments) || old.apply(this, arguments);
1744
+ };
1745
+ });
1746
+
1747
+ /* Fire off 'removeqtip' handler in $.cleanData if jQuery UI not present (it already does similar).
1748
+ * This snippet is taken directly from jQuery UI source code found here:
1749
+ * http://code.jquery.com/ui/jquery-ui-git.js
1750
+ */
1751
+ if(!$.ui) {
1752
+ $['cleanData'+replaceSuffix] = $.cleanData;
1753
+ $.cleanData = function( elems ) {
1754
+ for(var i = 0, elem; (elem = elems[i]) !== undefined; i++) {
1755
+ try { $( elem ).triggerHandler('removeqtip'); }
1756
+ catch( e ) {}
1757
+ }
1758
+ $['cleanData'+replaceSuffix]( elems );
1759
+ };
1760
+ }
1761
+
1762
+ // Set global qTip properties
1763
+ QTIP.version = '@VERSION';
1764
+ QTIP.nextid = 0;
1765
+ QTIP.inactiveEvents = 'click dblclick mousedown mouseup mousemove mouseleave mouseenter'.split(' ');
1766
+ QTIP.zindex = 15000;
1767
+
1768
+ // Define configuration defaults
1769
+ QTIP.defaults = {
1770
+ prerender: FALSE,
1771
+ id: FALSE,
1772
+ overwrite: TRUE,
1773
+ suppress: TRUE,
1774
+ content: {
1775
+ text: TRUE,
1776
+ attr: 'title',
1777
+ title: {
1778
+ text: FALSE,
1779
+ button: FALSE
1780
+ }
1781
+ },
1782
+ position: {
1783
+ my: 'top left',
1784
+ at: 'bottom right',
1785
+ target: FALSE,
1786
+ container: FALSE,
1787
+ viewport: FALSE,
1788
+ adjust: {
1789
+ x: 0, y: 0,
1790
+ mouse: TRUE,
1791
+ resize: TRUE,
1792
+ method: 'flip flip'
1793
+ },
1794
+ effect: function(api, pos, viewport) {
1795
+ $(this).animate(pos, {
1796
+ duration: 200,
1797
+ queue: FALSE
1798
+ });
1799
+ }
1800
+ },
1801
+ show: {
1802
+ target: FALSE,
1803
+ event: 'mouseenter',
1804
+ effect: TRUE,
1805
+ delay: 90,
1806
+ solo: FALSE,
1807
+ ready: FALSE,
1808
+ autofocus: FALSE
1809
+ },
1810
+ hide: {
1811
+ target: FALSE,
1812
+ event: 'mouseleave',
1813
+ effect: TRUE,
1814
+ delay: 0,
1815
+ fixed: FALSE,
1816
+ inactive: FALSE,
1817
+ leave: 'window',
1818
+ distance: FALSE
1819
+ },
1820
+ style: {
1821
+ classes: '',
1822
+ widget: FALSE,
1823
+ width: FALSE,
1824
+ height: FALSE,
1825
+ def: TRUE
1826
+ },
1827
+ events: {
1828
+ render: NULL,
1829
+ move: NULL,
1830
+ show: NULL,
1831
+ hide: NULL,
1832
+ toggle: NULL,
1833
+ visible: NULL,
1834
+ hidden: NULL,
1835
+ focus: NULL,
1836
+ blur: NULL
1837
+ }
1838
+ };
1839
+
1840
+
1841
+ PLUGINS.svg = function(api, svg, corner, adjustMethod)
1842
+ {
1843
+ var doc = $(document),
1844
+ elem = svg[0],
1845
+ result = {
1846
+ width: 0, height: 0,
1847
+ position: { top: 1e10, left: 1e10 }
1848
+ },
1849
+ box, mtx, root, point, tPoint;
1850
+
1851
+ // Ascend the parentNode chain until we find an element with getBBox()
1852
+ while(!elem.getBBox) { elem = elem.parentNode; }
1853
+
1854
+ // Check for a valid bounding box method
1855
+ if (elem.getBBox && elem.parentNode) {
1856
+ box = elem.getBBox();
1857
+ mtx = elem.getScreenCTM();
1858
+ root = elem.farthestViewportElement || elem;
1859
+
1860
+ // Return if no method is found
1861
+ if(!root.createSVGPoint) { return result; }
1862
+
1863
+ // Create our point var
1864
+ point = root.createSVGPoint();
1865
+
1866
+ // Adjust top and left
1867
+ point.x = box.x;
1868
+ point.y = box.y;
1869
+ tPoint = point.matrixTransform(mtx);
1870
+ result.position.left = tPoint.x;
1871
+ result.position.top = tPoint.y;
1872
+
1873
+ // Adjust width and height
1874
+ point.x += box.width;
1875
+ point.y += box.height;
1876
+ tPoint = point.matrixTransform(mtx);
1877
+ result.width = tPoint.x - result.position.left;
1878
+ result.height = tPoint.y - result.position.top;
1879
+
1880
+ // Adjust by scroll offset
1881
+ result.position.left += doc.scrollLeft();
1882
+ result.position.top += doc.scrollTop();
1883
+ }
1884
+
1885
+ return result;
1886
+ };
1887
+
1888
+
1889
+ function Ajax(api)
1890
+ {
1891
+ var self = this,
1892
+ tooltip = api.elements.tooltip,
1893
+ opts = api.options.content.ajax,
1894
+ defaults = QTIP.defaults.content.ajax,
1895
+ namespace = '.qtip-ajax',
1896
+ rscript = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
1897
+ first = TRUE,
1898
+ stop = FALSE,
1899
+ xhr;
1900
+
1901
+ api.checks.ajax = {
1902
+ '^content.ajax': function(obj, name, v) {
1903
+ // If content.ajax object was reset, set our local var
1904
+ if(name === 'ajax') { opts = v; }
1905
+
1906
+ if(name === 'once') {
1907
+ self.init();
1908
+ }
1909
+ else if(opts && opts.url) {
1910
+ self.load();
1911
+ }
1912
+ else {
1913
+ tooltip.unbind(namespace);
1914
+ }
1915
+ }
1916
+ };
1917
+
1918
+ $.extend(self, {
1919
+ init: function() {
1920
+ // Make sure ajax options are enabled and bind event
1921
+ if(opts && opts.url) {
1922
+ tooltip.unbind(namespace)[ opts.once ? 'one' : 'bind' ]('tooltipshow'+namespace, self.load);
1923
+ }
1924
+
1925
+ return self;
1926
+ },
1927
+
1928
+ load: function(event) {
1929
+ if(stop) {stop = FALSE; return; }
1930
+
1931
+ var hasSelector = opts.url.lastIndexOf(' '),
1932
+ url = opts.url,
1933
+ selector,
1934
+ hideFirst = !opts.loading && first;
1935
+
1936
+ // If loading option is disabled, prevent the tooltip showing until we've completed the request
1937
+ if(hideFirst) { try{ event.preventDefault(); } catch(e) {} }
1938
+
1939
+ // Make sure default event hasn't been prevented
1940
+ else if(event && event.isDefaultPrevented()) { return self; }
1941
+
1942
+ // Cancel old request
1943
+ if(xhr && xhr.abort) { xhr.abort(); }
1944
+
1945
+ // Check if user delcared a content selector like in .load()
1946
+ if(hasSelector > -1) {
1947
+ selector = url.substr(hasSelector);
1948
+ url = url.substr(0, hasSelector);
1949
+ }
1950
+
1951
+ // Define common after callback for both success/error handlers
1952
+ function after() {
1953
+ var complete;
1954
+
1955
+ // Don't proceed if tooltip is destroyed
1956
+ if(api.destroyed) { return; }
1957
+
1958
+ // Set first flag to false
1959
+ first = FALSE;
1960
+
1961
+ // Re-display tip if loading and first time, and reset first flag
1962
+ if(hideFirst) { stop = TRUE; api.show(event.originalEvent); }
1963
+
1964
+ // Call users complete method if it was defined
1965
+ if((complete = defaults.complete || opts.complete) && $.isFunction(complete)) {
1966
+ complete.apply(opts.context || api, arguments);
1967
+ }
1968
+ }
1969
+
1970
+ // Define success handler
1971
+ function successHandler(content, status, jqXHR) {
1972
+ var success;
1973
+
1974
+ // Don't proceed if tooltip is destroyed
1975
+ if(api.destroyed) { return; }
1976
+
1977
+ // If URL contains a selector
1978
+ if(selector && 'string' === typeof content) {
1979
+ // Create a dummy div to hold the results and grab the selector element
1980
+ content = $('<div/>')
1981
+ // inject the contents of the document in, removing the scripts
1982
+ // to avoid any 'Permission Denied' errors in IE
1983
+ .append(content.replace(rscript, ""))
1984
+
1985
+ // Locate the specified elements
1986
+ .find(selector);
1987
+ }
1988
+
1989
+ // Call the success function if one is defined
1990
+ if((success = defaults.success || opts.success) && $.isFunction(success)) {
1991
+ success.call(opts.context || api, content, status, jqXHR);
1992
+ }
1993
+
1994
+ // Otherwise set the content
1995
+ else { api.set('content.text', content); }
1996
+ }
1997
+
1998
+ // Error handler
1999
+ function errorHandler(xhr, status, error) {
2000
+ if(api.destroyed || xhr.status === 0) { return; }
2001
+ api.set('content.text', status + ': ' + error);
2002
+ }
2003
+
2004
+ // Setup $.ajax option object and process the request
2005
+ xhr = $.ajax(
2006
+ $.extend({
2007
+ error: defaults.error || errorHandler,
2008
+ context: api
2009
+ },
2010
+ opts, { url: url, success: successHandler, complete: after })
2011
+ );
2012
+ },
2013
+
2014
+ destroy: function() {
2015
+ // Cancel ajax request if possible
2016
+ if(xhr && xhr.abort) { xhr.abort(); }
2017
+
2018
+ // Set api.destroyed flag
2019
+ api.destroyed = TRUE;
2020
+ }
2021
+ });
2022
+
2023
+ self.init();
2024
+ }
2025
+
2026
+
2027
+ PLUGINS.ajax = function(api)
2028
+ {
2029
+ var self = api.plugins.ajax;
2030
+
2031
+ return 'object' === typeof self ? self : (api.plugins.ajax = new Ajax(api));
2032
+ };
2033
+
2034
+ PLUGINS.ajax.initialize = 'render';
2035
+
2036
+ // Setup plugin sanitization
2037
+ PLUGINS.ajax.sanitize = function(options)
2038
+ {
2039
+ var content = options.content, opts;
2040
+ if(content && 'ajax' in content) {
2041
+ opts = content.ajax;
2042
+ if(typeof opts !== 'object') { opts = options.content.ajax = { url: opts }; }
2043
+ if('boolean' !== typeof opts.once && opts.once) { opts.once = !!opts.once; }
2044
+ }
2045
+ };
2046
+
2047
+ // Extend original api defaults
2048
+ $.extend(TRUE, QTIP.defaults, {
2049
+ content: {
2050
+ ajax: {
2051
+ loading: TRUE,
2052
+ once: TRUE
2053
+ }
2054
+ }
2055
+ });
2056
+
2057
+
2058
+ // Tip coordinates calculator
2059
+ function calculateTip(corner, width, height)
2060
+ {
2061
+ var width2 = Math.ceil(width / 2), height2 = Math.ceil(height / 2),
2062
+
2063
+ // Define tip coordinates in terms of height and width values
2064
+ tips = {
2065
+ bottomright: [[0,0], [width,height], [width,0]],
2066
+ bottomleft: [[0,0], [width,0], [0,height]],
2067
+ topright: [[0,height], [width,0], [width,height]],
2068
+ topleft: [[0,0], [0,height], [width,height]],
2069
+ topcenter: [[0,height], [width2,0], [width,height]],
2070
+ bottomcenter: [[0,0], [width,0], [width2,height]],
2071
+ rightcenter: [[0,0], [width,height2], [0,height]],
2072
+ leftcenter: [[width,0], [width,height], [0,height2]]
2073
+ };
2074
+
2075
+ // Set common side shapes
2076
+ tips.lefttop = tips.bottomright; tips.righttop = tips.bottomleft;
2077
+ tips.leftbottom = tips.topright; tips.rightbottom = tips.topleft;
2078
+
2079
+ return tips[ corner.string() ];
2080
+ }
2081
+
2082
+
2083
+ function Tip(qTip, command)
2084
+ {
2085
+ var self = this,
2086
+ opts = qTip.options.style.tip,
2087
+ elems = qTip.elements,
2088
+ tooltip = elems.tooltip,
2089
+ cache = { top: 0, left: 0 },
2090
+ size = {
2091
+ width: opts.width,
2092
+ height: opts.height
2093
+ },
2094
+ color = { },
2095
+ border = opts.border || 0,
2096
+ namespace = '.qtip-tip',
2097
+ hasCanvas = !!($('<canvas />')[0] || {}).getContext,
2098
+ tiphtml;
2099
+
2100
+ self.corner = NULL;
2101
+ self.mimic = NULL;
2102
+ self.border = border;
2103
+ self.offset = opts.offset;
2104
+ self.size = size;
2105
+
2106
+ // Add new option checks for the plugin
2107
+ qTip.checks.tip = {
2108
+ '^position.my|style.tip.(corner|mimic|border)$': function() {
2109
+ // Make sure a tip can be drawn
2110
+ if(!self.init()) {
2111
+ self.destroy();
2112
+ }
2113
+
2114
+ // Reposition the tooltip
2115
+ qTip.reposition();
2116
+ },
2117
+ '^style.tip.(height|width)$': function() {
2118
+ // Re-set dimensions and redraw the tip
2119
+ size = {
2120
+ width: opts.width,
2121
+ height: opts.height
2122
+ };
2123
+ self.create();
2124
+ self.update();
2125
+
2126
+ // Reposition the tooltip
2127
+ qTip.reposition();
2128
+ },
2129
+ '^content.title.text|style.(classes|widget)$': function() {
2130
+ if(elems.tip && elems.tip.length) {
2131
+ self.update();
2132
+ }
2133
+ }
2134
+ };
2135
+
2136
+ function whileVisible(callback) {
2137
+ var visible = tooltip.is(':visible');
2138
+ tooltip.show(); callback(); tooltip.toggle(visible);
2139
+ }
2140
+
2141
+ function swapDimensions() {
2142
+ size.width = opts.height;
2143
+ size.height = opts.width;
2144
+ }
2145
+
2146
+ function resetDimensions() {
2147
+ size.width = opts.width;
2148
+ size.height = opts.height;
2149
+ }
2150
+
2151
+ function reposition(event, api, pos, viewport) {
2152
+ if(!elems.tip) { return; }
2153
+
2154
+ var newCorner = self.corner.clone(),
2155
+ adjust = pos.adjusted,
2156
+ method = qTip.options.position.adjust.method.split(' '),
2157
+ horizontal = method[0],
2158
+ vertical = method[1] || method[0],
2159
+ shift = { left: FALSE, top: FALSE, x: 0, y: 0 },
2160
+ offset, css = {}, props;
2161
+
2162
+ // If our tip position isn't fixed e.g. doesn't adjust with viewport...
2163
+ if(self.corner.fixed !== TRUE) {
2164
+ // Horizontal - Shift or flip method
2165
+ if(horizontal === SHIFT && newCorner.precedance === X && adjust.left && newCorner.y !== CENTER) {
2166
+ newCorner.precedance = newCorner.precedance === X ? Y : X;
2167
+ }
2168
+ else if(horizontal !== SHIFT && adjust.left){
2169
+ newCorner.x = newCorner.x === CENTER ? (adjust.left > 0 ? LEFT : RIGHT) : (newCorner.x === LEFT ? RIGHT : LEFT);
2170
+ }
2171
+
2172
+ // Vertical - Shift or flip method
2173
+ if(vertical === SHIFT && newCorner.precedance === Y && adjust.top && newCorner.x !== CENTER) {
2174
+ newCorner.precedance = newCorner.precedance === Y ? X : Y;
2175
+ }
2176
+ else if(vertical !== SHIFT && adjust.top) {
2177
+ newCorner.y = newCorner.y === CENTER ? (adjust.top > 0 ? TOP : BOTTOM) : (newCorner.y === TOP ? BOTTOM : TOP);
2178
+ }
2179
+
2180
+ // Update and redraw the tip if needed (check cached details of last drawn tip)
2181
+ if(newCorner.string() !== cache.corner.string() && (cache.top !== adjust.top || cache.left !== adjust.left)) {
2182
+ self.update(newCorner, FALSE);
2183
+ }
2184
+ }
2185
+
2186
+ // Setup tip offset properties
2187
+ offset = self.position(newCorner, adjust);
2188
+ offset[ newCorner.x ] += parseWidth(newCorner, newCorner.x);
2189
+ offset[ newCorner.y ] += parseWidth(newCorner, newCorner.y);
2190
+
2191
+ // Readjust offset object to make it left/top
2192
+ if(offset.right !== undefined) { offset.left = -offset.right; }
2193
+ if(offset.bottom !== undefined) { offset.top = -offset.bottom; }
2194
+ offset.user = Math.max(0, opts.offset);
2195
+
2196
+ // Viewport "shift" specific adjustments
2197
+ if(shift.left = (horizontal === SHIFT && !!adjust.left)) {
2198
+ if(newCorner.x === CENTER) {
2199
+ css['margin-left'] = shift.x = offset['margin-left'] - adjust.left;
2200
+ }
2201
+ else {
2202
+ props = offset.right !== undefined ?
2203
+ [ adjust.left, -offset.left ] : [ -adjust.left, offset.left ];
2204
+
2205
+ if( (shift.x = Math.max(props[0], props[1])) > props[0] ) {
2206
+ pos.left -= adjust.left;
2207
+ shift.left = FALSE;
2208
+ }
2209
+
2210
+ css[ offset.right !== undefined ? RIGHT : LEFT ] = shift.x;
2211
+ }
2212
+ }
2213
+ if(shift.top = (vertical === SHIFT && !!adjust.top)) {
2214
+ if(newCorner.y === CENTER) {
2215
+ css['margin-top'] = shift.y = offset['margin-top'] - adjust.top;
2216
+ }
2217
+ else {
2218
+ props = offset.bottom !== undefined ?
2219
+ [ adjust.top, -offset.top ] : [ -adjust.top, offset.top ];
2220
+
2221
+ if( (shift.y = Math.max(props[0], props[1])) > props[0] ) {
2222
+ pos.top -= adjust.top;
2223
+ shift.top = FALSE;
2224
+ }
2225
+
2226
+ css[ offset.bottom !== undefined ? BOTTOM : TOP ] = shift.y;
2227
+ }
2228
+ }
2229
+
2230
+ /*
2231
+ * If the tip is adjusted in both dimensions, or in a
2232
+ * direction that would cause it to be anywhere but the
2233
+ * outer border, hide it!
2234
+ */
2235
+ elems.tip.css(css).toggle(
2236
+ !((shift.x && shift.y) || (newCorner.x === CENTER && shift.y) || (newCorner.y === CENTER && shift.x))
2237
+ );
2238
+
2239
+ // Adjust position to accomodate tip dimensions
2240
+ pos.left -= offset.left.charAt ? offset.user : horizontal !== SHIFT || shift.top || !shift.left && !shift.top ? offset.left : 0;
2241
+ pos.top -= offset.top.charAt ? offset.user : vertical !== SHIFT || shift.left || !shift.left && !shift.top ? offset.top : 0;
2242
+
2243
+ // Cache details
2244
+ cache.left = adjust.left; cache.top = adjust.top;
2245
+ cache.corner = newCorner.clone();
2246
+ }
2247
+
2248
+ function parseCorner() {
2249
+ var corner = opts.corner,
2250
+ posOptions = qTip.options.position,
2251
+ at = posOptions.at,
2252
+ my = posOptions.my.string ? posOptions.my.string() : posOptions.my;
2253
+
2254
+ // Detect corner and mimic properties
2255
+ if(corner === FALSE || (my === FALSE && at === FALSE)) {
2256
+ return FALSE;
2257
+ }
2258
+ else {
2259
+ if(corner === TRUE) {
2260
+ self.corner = new PLUGINS.Corner(my);
2261
+ }
2262
+ else if(!corner.string) {
2263
+ self.corner = new PLUGINS.Corner(corner);
2264
+ self.corner.fixed = TRUE;
2265
+ }
2266
+ }
2267
+
2268
+ // Cache it
2269
+ cache.corner = new PLUGINS.Corner( self.corner.string() );
2270
+
2271
+ return self.corner.string() !== 'centercenter';
2272
+ }
2273
+
2274
+ /* border width calculator */
2275
+ function parseWidth(corner, side, use) {
2276
+ side = !side ? corner[corner.precedance] : side;
2277
+
2278
+ var isTitleTop = elems.titlebar && corner.y === TOP,
2279
+ elem = isTitleTop ? elems.titlebar : tooltip,
2280
+ borderSide = 'border-' + side + '-width',
2281
+ css = function(elem) { return parseInt(elem.css(borderSide), 10); },
2282
+ val;
2283
+
2284
+ // Grab the border-width value (make tooltip visible first)
2285
+ whileVisible(function() {
2286
+ val = (use ? css(use) : (css(elems.content) || css(elem) || css(tooltip))) || 0;
2287
+ });
2288
+ return val;
2289
+ }
2290
+
2291
+ function parseRadius(corner) {
2292
+ var isTitleTop = elems.titlebar && corner.y === TOP,
2293
+ elem = isTitleTop ? elems.titlebar : elems.content,
2294
+ moz = $.browser.mozilla,
2295
+ prefix = moz ? '-moz-' : $.browser.webkit ? '-webkit-' : '',
2296
+ nonStandard = 'border-radius-' + corner.y + corner.x,
2297
+ standard = 'border-' + corner.y + '-' + corner.x + '-radius',
2298
+ css = function(c) { return parseInt(elem.css(c), 10) || parseInt(tooltip.css(c), 10); },
2299
+ val;
2300
+
2301
+ whileVisible(function() {
2302
+ val = css(standard) || css(prefix + standard) || css(prefix + nonStandard) || css(nonStandard) || 0;
2303
+ });
2304
+ return val;
2305
+ }
2306
+
2307
+ function parseColours(actual) {
2308
+ var i, fill, border,
2309
+ tip = elems.tip.css('cssText', ''),
2310
+ corner = actual || self.corner,
2311
+ invalid = /rgba?\(0, 0, 0(, 0)?\)|transparent|#123456/i,
2312
+ borderSide = 'border-' + corner[ corner.precedance ] + '-color',
2313
+ bgColor = 'background-color',
2314
+ transparent = 'transparent',
2315
+ important = ' !important',
2316
+
2317
+ titlebar = elems.titlebar,
2318
+ useTitle = titlebar && (corner.y === TOP || (corner.y === CENTER && tip.position().top + (size.height / 2) + opts.offset < titlebar.outerHeight(TRUE))),
2319
+ colorElem = useTitle ? titlebar : elems.content;
2320
+
2321
+ function css(elem, prop, compare) {
2322
+ var val = elem.css(prop) || transparent;
2323
+ if(compare && val === elem.css(compare)) { return FALSE; }
2324
+ else { return invalid.test(val) ? FALSE : val; }
2325
+ }
2326
+
2327
+ // Ensure tooltip is visible then...
2328
+ whileVisible(function() {
2329
+ // Attempt to detect the background colour from various elements, left-to-right precedance
2330
+ color.fill = css(tip, bgColor) || css(colorElem, bgColor) || css(elems.content, bgColor) ||
2331
+ css(tooltip, bgColor) || tip.css(bgColor);
2332
+
2333
+ // Attempt to detect the correct border side colour from various elements, left-to-right precedance
2334
+ color.border = css(tip, borderSide, 'color') || css(colorElem, borderSide, 'color') ||
2335
+ css(elems.content, borderSide, 'color') || css(tooltip, borderSide, 'color') || tooltip.css(borderSide);
2336
+
2337
+ // Reset background and border colours
2338
+ $('*', tip).add(tip).css('cssText', bgColor+':'+transparent+important+';border:0'+important+';');
2339
+ });
2340
+ }
2341
+
2342
+ function calculateSize(corner) {
2343
+ var y = corner.precedance === Y,
2344
+ width = size [ y ? WIDTH : HEIGHT ],
2345
+ height = size [ y ? HEIGHT : WIDTH ],
2346
+ isCenter = corner.string().indexOf(CENTER) > -1,
2347
+ base = width * (isCenter ? 0.5 : 1),
2348
+ pow = Math.pow,
2349
+ round = Math.round,
2350
+ bigHyp, ratio, result,
2351
+
2352
+ smallHyp = Math.sqrt( pow(base, 2) + pow(height, 2) ),
2353
+
2354
+ hyp = [
2355
+ (border / base) * smallHyp, (border / height) * smallHyp
2356
+ ];
2357
+ hyp[2] = Math.sqrt( pow(hyp[0], 2) - pow(border, 2) );
2358
+ hyp[3] = Math.sqrt( pow(hyp[1], 2) - pow(border, 2) );
2359
+
2360
+ bigHyp = smallHyp + hyp[2] + hyp[3] + (isCenter ? 0 : hyp[0]);
2361
+ ratio = bigHyp / smallHyp;
2362
+
2363
+ result = [ round(ratio * height), round(ratio * width) ];
2364
+ return { height: result[ y ? 0 : 1 ], width: result[ y ? 1 : 0 ] };
2365
+ }
2366
+
2367
+ function createVML(tag, props, style) {
2368
+ return '<qvml:'+tag+' xmlns="urn:schemas-microsoft.com:vml" class="qtip-vml" '+(props||'')+
2369
+ ' style="behavior: url(#default#VML); '+(style||'')+ '" />';
2370
+ }
2371
+
2372
+ $.extend(self, {
2373
+ init: function()
2374
+ {
2375
+ var enabled = parseCorner() && (hasCanvas || $.browser.msie);
2376
+
2377
+ // Determine tip corner and type
2378
+ if(enabled) {
2379
+ // Create a new tip and draw it
2380
+ self.create();
2381
+ self.update();
2382
+
2383
+ // Bind update events
2384
+ tooltip.unbind(namespace).bind('tooltipmove'+namespace, reposition);
2385
+
2386
+ // Fix for issue of tips not showing after redraw in IE (VML...)
2387
+ if(!hasCanvas) {
2388
+ tooltip.bind('tooltipredraw tooltipredrawn', function(event) {
2389
+ if(event.type === 'tooltipredraw') {
2390
+ tiphtml = elems.tip.html();
2391
+ elems.tip.html('');
2392
+ }
2393
+ else { elems.tip.html(tiphtml); }
2394
+ });
2395
+ }
2396
+ }
2397
+
2398
+ return enabled;
2399
+ },
2400
+
2401
+ create: function()
2402
+ {
2403
+ var width = size.width,
2404
+ height = size.height,
2405
+ vml;
2406
+
2407
+ // Remove previous tip element if present
2408
+ if(elems.tip) { elems.tip.remove(); }
2409
+
2410
+ // Create tip element and prepend to the tooltip
2411
+ elems.tip = $('<div />', { 'class': 'ui-tooltip-tip' }).css({ width: width, height: height }).prependTo(tooltip);
2412
+
2413
+ // Create tip drawing element(s)
2414
+ if(hasCanvas) {
2415
+ // save() as soon as we create the canvas element so FF2 doesn't bork on our first restore()!
2416
+ $('<canvas />').appendTo(elems.tip)[0].getContext('2d').save();
2417
+ }
2418
+ else {
2419
+ vml = createVML('shape', 'coordorigin="0,0"', 'position:absolute;');
2420
+ elems.tip.html(vml + vml);
2421
+
2422
+ // Prevent mousing down on the tip since it causes problems with .live() handling in IE due to VML
2423
+ $('*', elems.tip).bind('click mousedown', function(event) { event.stopPropagation(); });
2424
+ }
2425
+ },
2426
+
2427
+ update: function(corner, position)
2428
+ {
2429
+ var tip = elems.tip,
2430
+ inner = tip.children(),
2431
+ width = size.width,
2432
+ height = size.height,
2433
+ mimic = opts.mimic,
2434
+ round = Math.round,
2435
+ precedance, context, coords, translate, newSize;
2436
+
2437
+ // Re-determine tip if not already set
2438
+ if(!corner) { corner = cache.corner || self.corner; }
2439
+
2440
+ // Use corner property if we detect an invalid mimic value
2441
+ if(mimic === FALSE) { mimic = corner; }
2442
+
2443
+ // Otherwise inherit mimic properties from the corner object as necessary
2444
+ else {
2445
+ mimic = new PLUGINS.Corner(mimic);
2446
+ mimic.precedance = corner.precedance;
2447
+
2448
+ if(mimic.x === 'inherit') { mimic.x = corner.x; }
2449
+ else if(mimic.y === 'inherit') { mimic.y = corner.y; }
2450
+ else if(mimic.x === mimic.y) {
2451
+ mimic[ corner.precedance ] = corner[ corner.precedance ];
2452
+ }
2453
+ }
2454
+ precedance = mimic.precedance;
2455
+
2456
+ // Ensure the tip width.height are relative to the tip position
2457
+ if(corner.precedance === X) { swapDimensions(); }
2458
+ else { resetDimensions(); }
2459
+
2460
+ // Set the tip dimensions
2461
+ elems.tip.css({
2462
+ width: (width = size.width),
2463
+ height: (height = size.height)
2464
+ });
2465
+
2466
+ // Update our colours
2467
+ parseColours(corner);
2468
+
2469
+ // Detect border width, taking into account colours
2470
+ if(color.border !== 'transparent') {
2471
+ // Grab border width
2472
+ border = parseWidth(corner, NULL);
2473
+
2474
+ // If border width isn't zero, use border color as fill (1.0 style tips)
2475
+ if(opts.border === 0 && border > 0) { color.fill = color.border; }
2476
+
2477
+ // Set border width (use detected border width if opts.border is true)
2478
+ self.border = border = opts.border !== TRUE ? opts.border : border;
2479
+ }
2480
+
2481
+ // Border colour was invalid, set border to zero
2482
+ else { self.border = border = 0; }
2483
+
2484
+ // Calculate coordinates
2485
+ coords = calculateTip(mimic, width , height);
2486
+
2487
+ // Determine tip size
2488
+ self.size = newSize = calculateSize(corner);
2489
+ tip.css(newSize);
2490
+
2491
+ // Calculate tip translation
2492
+ if(corner.precedance === Y) {
2493
+ translate = [
2494
+ round(mimic.x === LEFT ? border : mimic.x === RIGHT ? newSize.width - width - border : (newSize.width - width) / 2),
2495
+ round(mimic.y === TOP ? newSize.height - height : 0)
2496
+ ];
2497
+ }
2498
+ else {
2499
+ translate = [
2500
+ round(mimic.x === LEFT ? newSize.width - width : 0),
2501
+ round(mimic.y === TOP ? border : mimic.y === BOTTOM ? newSize.height - height - border : (newSize.height - height) / 2)
2502
+ ];
2503
+ }
2504
+
2505
+ // Canvas drawing implementation
2506
+ if(hasCanvas) {
2507
+ // Set the canvas size using calculated size
2508
+ inner.attr(newSize);
2509
+
2510
+ // Grab canvas context and clear/save it
2511
+ context = inner[0].getContext('2d');
2512
+ context.restore(); context.save();
2513
+ context.clearRect(0,0,3000,3000);
2514
+
2515
+ // Set properties
2516
+ context.fillStyle = color.fill;
2517
+ context.strokeStyle = color.border;
2518
+ context.lineWidth = border * 2;
2519
+ context.lineJoin = 'miter';
2520
+ context.miterLimit = 100;
2521
+
2522
+ // Translate origin
2523
+ context.translate(translate[0], translate[1]);
2524
+
2525
+ // Draw the tip
2526
+ context.beginPath();
2527
+ context.moveTo(coords[0][0], coords[0][1]);
2528
+ context.lineTo(coords[1][0], coords[1][1]);
2529
+ context.lineTo(coords[2][0], coords[2][1]);
2530
+ context.closePath();
2531
+
2532
+ // Apply fill and border
2533
+ if(border) {
2534
+ // Make sure transparent borders are supported by doing a stroke
2535
+ // of the background colour before the stroke colour
2536
+ if(tooltip.css('background-clip') === 'border-box') {
2537
+ context.strokeStyle = color.fill;
2538
+ context.stroke();
2539
+ }
2540
+ context.strokeStyle = color.border;
2541
+ context.stroke();
2542
+ }
2543
+ context.fill();
2544
+ }
2545
+
2546
+ // VML (IE Proprietary implementation)
2547
+ else {
2548
+ // Setup coordinates string
2549
+ coords = 'm' + coords[0][0] + ',' + coords[0][1] + ' l' + coords[1][0] +
2550
+ ',' + coords[1][1] + ' ' + coords[2][0] + ',' + coords[2][1] + ' xe';
2551
+
2552
+ // Setup VML-specific offset for pixel-perfection
2553
+ translate[2] = border && /^(r|b)/i.test(corner.string()) ?
2554
+ parseFloat($.browser.version, 10) === 8 ? 2 : 1 : 0;
2555
+
2556
+ // Set initial CSS
2557
+ inner.css({
2558
+ coordsize: (width+border) + ' ' + (height+border),
2559
+ antialias: ''+(mimic.string().indexOf(CENTER) > -1),
2560
+ left: translate[0],
2561
+ top: translate[1],
2562
+ width: width + border,
2563
+ height: height + border
2564
+ })
2565
+ .each(function(i) {
2566
+ var $this = $(this);
2567
+
2568
+ // Set shape specific attributes
2569
+ $this[ $this.prop ? 'prop' : 'attr' ]({
2570
+ coordsize: (width+border) + ' ' + (height+border),
2571
+ path: coords,
2572
+ fillcolor: color.fill,
2573
+ filled: !!i,
2574
+ stroked: !i
2575
+ })
2576
+ .toggle(!!(border || i));
2577
+
2578
+ // Check if border is enabled and add stroke element
2579
+ if(!i && $this.html() === '') {
2580
+ $this.html(
2581
+ createVML('stroke', 'weight="'+(border*2)+'px" color="'+color.border+'" miterlimit="1000" joinstyle="miter"')
2582
+ );
2583
+ }
2584
+ });
2585
+ }
2586
+
2587
+ // Position if needed
2588
+ if(position !== FALSE) { self.position(corner); }
2589
+ },
2590
+
2591
+ // Tip positioning method
2592
+ position: function(corner)
2593
+ {
2594
+ var tip = elems.tip,
2595
+ position = {},
2596
+ userOffset = Math.max(0, opts.offset),
2597
+ precedance, dimensions, corners;
2598
+
2599
+ // Return if tips are disabled or tip is not yet rendered
2600
+ if(opts.corner === FALSE || !tip) { return FALSE; }
2601
+
2602
+ // Inherit corner if not provided
2603
+ corner = corner || self.corner;
2604
+ precedance = corner.precedance;
2605
+
2606
+ // Determine which tip dimension to use for adjustment
2607
+ dimensions = calculateSize(corner);
2608
+
2609
+ // Setup corners and offset array
2610
+ corners = [ corner.x, corner.y ];
2611
+ if(precedance === X) { corners.reverse(); }
2612
+
2613
+ // Calculate tip position
2614
+ $.each(corners, function(i, side) {
2615
+ var b, bc, br;
2616
+
2617
+ if(side === CENTER) {
2618
+ b = precedance === Y ? LEFT : TOP;
2619
+ position[ b ] = '50%';
2620
+ position['margin-' + b] = -Math.round(dimensions[ precedance === Y ? WIDTH : HEIGHT ] / 2) + userOffset;
2621
+ }
2622
+ else {
2623
+ b = parseWidth(corner, side);
2624
+ bc = parseWidth(corner, side, elems.content);
2625
+ br = parseRadius(corner);
2626
+
2627
+ position[ side ] = i ? bc : (userOffset + (br > b ? br : -b));
2628
+ }
2629
+ });
2630
+
2631
+ // Adjust for tip dimensions
2632
+ position[ corner[precedance] ] -= dimensions[ precedance === X ? WIDTH : HEIGHT ];
2633
+
2634
+ // Set and return new position
2635
+ tip.css({ top: '', bottom: '', left: '', right: '', margin: '' }).css(position);
2636
+ return position;
2637
+ },
2638
+
2639
+ destroy: function()
2640
+ {
2641
+ // Remove the tip element
2642
+ if(elems.tip) { elems.tip.remove(); }
2643
+ elems.tip = false;
2644
+
2645
+ // Unbind events
2646
+ tooltip.unbind(namespace);
2647
+ }
2648
+ });
2649
+
2650
+ self.init();
2651
+ }
2652
+
2653
+ PLUGINS.tip = function(api)
2654
+ {
2655
+ var self = api.plugins.tip;
2656
+
2657
+ return 'object' === typeof self ? self : (api.plugins.tip = new Tip(api));
2658
+ };
2659
+
2660
+ // Initialize tip on render
2661
+ PLUGINS.tip.initialize = 'render';
2662
+
2663
+ // Setup plugin sanitization options
2664
+ PLUGINS.tip.sanitize = function(options)
2665
+ {
2666
+ var style = options.style, opts;
2667
+ if(style && 'tip' in style) {
2668
+ opts = options.style.tip;
2669
+ if(typeof opts !== 'object'){ options.style.tip = { corner: opts }; }
2670
+ if(!(/string|boolean/i).test(typeof opts['corner'])) { opts['corner'] = TRUE; }
2671
+ if(typeof opts.width !== 'number'){ delete opts.width; }
2672
+ if(typeof opts.height !== 'number'){ delete opts.height; }
2673
+ if(typeof opts.border !== 'number' && opts.border !== TRUE){ delete opts.border; }
2674
+ if(typeof opts.offset !== 'number'){ delete opts.offset; }
2675
+ }
2676
+ };
2677
+
2678
+ // Extend original qTip defaults
2679
+ $.extend(TRUE, QTIP.defaults, {
2680
+ style: {
2681
+ tip: {
2682
+ corner: TRUE,
2683
+ mimic: FALSE,
2684
+ width: 6,
2685
+ height: 6,
2686
+ border: TRUE,
2687
+ offset: 0
2688
+ }
2689
+ }
2690
+ });
2691
+
2692
+
2693
+ function Modal(api)
2694
+ {
2695
+ var self = this,
2696
+ options = api.options.show.modal,
2697
+ elems = api.elements,
2698
+ tooltip = elems.tooltip,
2699
+ overlaySelector = '#qtip-overlay',
2700
+ globalNamespace = '.qtipmodal',
2701
+ namespace = globalNamespace + api.id,
2702
+ attr = 'is-modal-qtip',
2703
+ docBody = $(document.body),
2704
+ focusableSelector = PLUGINS.modal.focusable.join(','),
2705
+ focusableElems = {}, overlay;
2706
+
2707
+ // Setup option set checks
2708
+ api.checks.modal = {
2709
+ '^show.modal.(on|blur)$': function() {
2710
+ // Initialise
2711
+ self.init();
2712
+
2713
+ // Show the modal if not visible already and tooltip is visible
2714
+ elems.overlay.toggle( tooltip.is(':visible') );
2715
+ },
2716
+ '^content.text$': function() {
2717
+ updateFocusable();
2718
+ }
2719
+ };
2720
+
2721
+ function updateFocusable() {
2722
+ focusableElems = $(focusableSelector, tooltip).not('[disabled]').map(function() {
2723
+ return typeof this.focus === 'function' ? this : null;
2724
+ });
2725
+ }
2726
+
2727
+ function focusInputs(blurElems) {
2728
+ // Blurring body element in IE causes window.open windows to unfocus!
2729
+ if(focusableElems.length < 1 && blurElems.length) { blurElems.not('body').blur(); }
2730
+
2731
+ // Focus the inputs
2732
+ else { focusableElems.first().focus(); }
2733
+ }
2734
+
2735
+ function stealFocus(event) {
2736
+ var target = $(event.target),
2737
+ container = target.closest('.qtip'),
2738
+ targetOnTop;
2739
+
2740
+ // Determine if input container target is above this
2741
+ targetOnTop = container.length < 1 ? FALSE :
2742
+ (parseInt(container[0].style.zIndex, 10) > parseInt(tooltip[0].style.zIndex, 10));
2743
+
2744
+ // If we're showing a modal, but focus has landed on an input below
2745
+ // this modal, divert focus to the first visible input in this modal
2746
+ // or if we can't find one... the tooltip itself
2747
+ if(!targetOnTop && ($(event.target).closest(selector)[0] !== tooltip[0])) {
2748
+ focusInputs(target);
2749
+ }
2750
+ }
2751
+
2752
+ $.extend(self, {
2753
+ init: function()
2754
+ {
2755
+ // If modal is disabled... return
2756
+ if(!options.on) { return self; }
2757
+
2758
+ // Create the overlay if needed
2759
+ overlay = self.create();
2760
+
2761
+ // Add unique attribute so we can grab modal tooltips easily via a selector
2762
+ tooltip.attr(attr, TRUE)
2763
+
2764
+ // Set z-index
2765
+ .css('z-index', PLUGINS.modal.zindex + $(selector+'['+attr+']').length)
2766
+
2767
+ // Remove previous bound events in globalNamespace
2768
+ .unbind(globalNamespace).unbind(namespace)
2769
+
2770
+ // Apply our show/hide/focus modal events
2771
+ .bind('tooltipshow'+globalNamespace+' tooltiphide'+globalNamespace, function(event, api, duration) {
2772
+ var oEvent = event.originalEvent;
2773
+
2774
+ // Make sure mouseout doesn't trigger a hide when showing the modal and mousing onto backdrop
2775
+ if(event.target === tooltip[0]) {
2776
+ if(oEvent && event.type === 'tooltiphide' && /mouse(leave|enter)/.test(oEvent.type) && $(oEvent.relatedTarget).closest(overlay[0]).length) {
2777
+ try { event.preventDefault(); } catch(e) {}
2778
+ }
2779
+ else if(!oEvent || (oEvent && !oEvent.solo)) {
2780
+ self[ event.type.replace('tooltip', '') ](event, duration);
2781
+ }
2782
+ }
2783
+ })
2784
+
2785
+ // Adjust modal z-index on tooltip focus
2786
+ .bind('tooltipfocus'+globalNamespace, function(event) {
2787
+ // If focus was cancelled before it reearch us, don't do anything
2788
+ if(event.isDefaultPrevented() || event.target !== tooltip[0]) { return; }
2789
+
2790
+ var qtips = $(selector).filter('['+attr+']'),
2791
+
2792
+ // Keep the modal's lower than other, regular qtips
2793
+ newIndex = PLUGINS.modal.zindex + qtips.length,
2794
+ curIndex = parseInt(tooltip[0].style.zIndex, 10);
2795
+
2796
+ // Set overlay z-index
2797
+ overlay[0].style.zIndex = newIndex - 2;
2798
+
2799
+ // Reduce modal z-index's and keep them properly ordered
2800
+ qtips.each(function() {
2801
+ if(this.style.zIndex > curIndex) {
2802
+ this.style.zIndex -= 1;
2803
+ }
2804
+ });
2805
+
2806
+ // Fire blur event for focused tooltip
2807
+ qtips.end().filter('.' + focusClass).qtip('blur', event.originalEvent);
2808
+
2809
+ // Set the new z-index
2810
+ tooltip.addClass(focusClass)[0].style.zIndex = newIndex;
2811
+
2812
+ // Prevent default handling
2813
+ try { event.preventDefault(); } catch(e) {}
2814
+ })
2815
+
2816
+ // Focus any other visible modals when this one hides
2817
+ .bind('tooltiphide'+globalNamespace, function(event) {
2818
+ if(event.target === tooltip[0]) {
2819
+ $('[' + attr + ']').filter(':visible').not(tooltip).last().qtip('focus', event);
2820
+ }
2821
+ });
2822
+
2823
+ // Apply keyboard "Escape key" close handler
2824
+ if(options.escape) {
2825
+ $(document).unbind(namespace).bind('keydown'+namespace, function(event) {
2826
+ if(event.keyCode === 27 && tooltip.hasClass(focusClass)) {
2827
+ api.hide(event);
2828
+ }
2829
+ });
2830
+ }
2831
+
2832
+ // Apply click handler for blur option
2833
+ if(options.blur) {
2834
+ elems.overlay.unbind(namespace).bind('click'+namespace, function(event) {
2835
+ if(tooltip.hasClass(focusClass)) { api.hide(event); }
2836
+ });
2837
+ }
2838
+
2839
+ // Update focusable elements
2840
+ updateFocusable();
2841
+
2842
+ return self;
2843
+ },
2844
+
2845
+ create: function()
2846
+ {
2847
+ var elem = $(overlaySelector);
2848
+
2849
+ // Return if overlay is already rendered
2850
+ if(elem.length) {
2851
+ // Modal overlay should always be below all tooltips if possible
2852
+ return (elems.overlay = elem.insertAfter( $(selector).last() ));
2853
+ }
2854
+
2855
+ // Create document overlay
2856
+ overlay = elems.overlay = $('<div />', {
2857
+ id: overlaySelector.substr(1),
2858
+ html: '<div></div>',
2859
+ mousedown: function() { return FALSE; }
2860
+ })
2861
+ .hide()
2862
+ .insertAfter( $(selector).last() );
2863
+
2864
+ // Update position on window resize or scroll
2865
+ function resize() {
2866
+ overlay.css({
2867
+ height: $(window).height(),
2868
+ width: $(window).width()
2869
+ });
2870
+ }
2871
+ $(window).unbind(globalNamespace).bind('resize'+globalNamespace, resize);
2872
+ resize(); // Fire it initially too
2873
+
2874
+ return overlay;
2875
+ },
2876
+
2877
+ toggle: function(event, state, duration)
2878
+ {
2879
+ // Make sure default event hasn't been prevented
2880
+ if(event && event.isDefaultPrevented()) { return self; }
2881
+
2882
+ var effect = options.effect,
2883
+ type = state ? 'show': 'hide',
2884
+ visible = overlay.is(':visible'),
2885
+ modals = $('[' + attr + ']').filter(':visible').not(tooltip),
2886
+ zindex;
2887
+
2888
+ // Create our overlay if it isn't present already
2889
+ if(!overlay) { overlay = self.create(); }
2890
+
2891
+ // Prevent modal from conflicting with show.solo, and don't hide backdrop is other modals are visible
2892
+ if((overlay.is(':animated') && visible === state) || (!state && modals.length)) { return self; }
2893
+
2894
+ // State specific...
2895
+ if(state) {
2896
+ // Set position
2897
+ overlay.css({ left: 0, top: 0 });
2898
+
2899
+ // Toggle backdrop cursor style on show
2900
+ overlay.toggleClass('blurs', options.blur);
2901
+
2902
+ // IF the modal can steal the focus
2903
+ if(options.stealfocus !== FALSE) {
2904
+ // Make sure we can't focus anything outside the tooltip
2905
+ docBody.bind('focusin'+namespace, stealFocus);
2906
+
2907
+ // Blur the current item and focus anything in the modal we an
2908
+ focusInputs( $('body :focus') );
2909
+ }
2910
+ }
2911
+ else {
2912
+ // Undelegate focus handler
2913
+ docBody.unbind('focusin'+namespace);
2914
+ }
2915
+
2916
+ // Stop all animations
2917
+ overlay.stop(TRUE, FALSE);
2918
+
2919
+ // Use custom function if provided
2920
+ if($.isFunction(effect)) {
2921
+ effect.call(overlay, state);
2922
+ }
2923
+
2924
+ // If no effect type is supplied, use a simple toggle
2925
+ else if(effect === FALSE) {
2926
+ overlay[ type ]();
2927
+ }
2928
+
2929
+ // Use basic fade function
2930
+ else {
2931
+ overlay.fadeTo( parseInt(duration, 10) || 90, state ? 1 : 0, function() {
2932
+ if(!state) { $(this).hide(); }
2933
+ });
2934
+ }
2935
+
2936
+ // Reset position on hide
2937
+ if(!state) {
2938
+ overlay.queue(function(next) {
2939
+ overlay.css({ left: '', top: '' });
2940
+ next();
2941
+ });
2942
+ }
2943
+
2944
+ return self;
2945
+ },
2946
+
2947
+ show: function(event, duration) { return self.toggle(event, TRUE, duration); },
2948
+ hide: function(event, duration) { return self.toggle(event, FALSE, duration); },
2949
+
2950
+ destroy: function()
2951
+ {
2952
+ var delBlanket = overlay;
2953
+
2954
+ if(delBlanket) {
2955
+ // Check if any other modal tooltips are present
2956
+ delBlanket = $('[' + attr + ']').not(tooltip).length < 1;
2957
+
2958
+ // Remove overlay if needed
2959
+ if(delBlanket) {
2960
+ elems.overlay.remove();
2961
+ $(document).unbind(globalNamespace);
2962
+ }
2963
+ else {
2964
+ elems.overlay.unbind(globalNamespace+api.id);
2965
+ }
2966
+
2967
+ // Undelegate focus handler
2968
+ docBody.undelegate('*', 'focusin'+namespace);
2969
+ }
2970
+
2971
+ // Remove bound events
2972
+ return tooltip.removeAttr(attr).unbind(globalNamespace);
2973
+ }
2974
+ });
2975
+
2976
+ self.init();
2977
+ }
2978
+
2979
+ PLUGINS.modal = function(api) {
2980
+ var self = api.plugins.modal;
2981
+
2982
+ return 'object' === typeof self ? self : (api.plugins.modal = new Modal(api));
2983
+ };
2984
+
2985
+ // Plugin needs to be initialized on render
2986
+ PLUGINS.modal.initialize = 'render';
2987
+
2988
+ // Setup sanitiztion rules
2989
+ PLUGINS.modal.sanitize = function(opts) {
2990
+ if(opts.show) {
2991
+ if(typeof opts.show.modal !== 'object') { opts.show.modal = { on: !!opts.show.modal }; }
2992
+ else if(typeof opts.show.modal.on === 'undefined') { opts.show.modal.on = TRUE; }
2993
+ }
2994
+ };
2995
+
2996
+ // Base z-index for all modal tooltips (use qTip core z-index as a base)
2997
+ PLUGINS.modal.zindex = QTIP.zindex - 200;
2998
+
2999
+ // Defines the selector used to select all 'focusable' elements within the modal when using the show.modal.stealfocus option.
3000
+ // Selectors initially taken from http://stackoverflow.com/questions/7668525/is-there-a-jquery-selector-to-get-all-elements-that-can-get-focus
3001
+ PLUGINS.modal.focusable = ['a[href]', 'area[href]', 'input', 'select', 'textarea', 'button', 'iframe', 'object', 'embed', '[tabindex]', '[contenteditable]'];
3002
+
3003
+ // Extend original api defaults
3004
+ $.extend(TRUE, QTIP.defaults, {
3005
+ show: {
3006
+ modal: {
3007
+ on: FALSE,
3008
+ effect: TRUE,
3009
+ blur: TRUE,
3010
+ stealfocus: TRUE,
3011
+ escape: TRUE
3012
+ }
3013
+ }
3014
+ });
3015
+
3016
+
3017
+ PLUGINS.viewport = function(api, position, posOptions, targetWidth, targetHeight, elemWidth, elemHeight)
3018
+ {
3019
+ var target = posOptions.target,
3020
+ tooltip = api.elements.tooltip,
3021
+ my = posOptions.my,
3022
+ at = posOptions.at,
3023
+ adjust = posOptions.adjust,
3024
+ method = adjust.method.split(' '),
3025
+ methodX = method[0],
3026
+ methodY = method[1] || method[0],
3027
+ viewport = posOptions.viewport,
3028
+ container = posOptions.container,
3029
+ cache = api.cache,
3030
+ tip = api.plugins.tip,
3031
+ adjusted = { left: 0, top: 0 },
3032
+ fixed, newMy, newClass;
3033
+
3034
+ // If viewport is not a jQuery element, or it's the window/document or no adjustment method is used... return
3035
+ if(!viewport.jquery || target[0] === window || target[0] === document.body || adjust.method === 'none') {
3036
+ return adjusted;
3037
+ }
3038
+
3039
+ // Cache our viewport details
3040
+ fixed = tooltip.css('position') === 'fixed';
3041
+ viewport = {
3042
+ elem: viewport,
3043
+ height: viewport[ (viewport[0] === window ? 'h' : 'outerH') + 'eight' ](),
3044
+ width: viewport[ (viewport[0] === window ? 'w' : 'outerW') + 'idth' ](),
3045
+ scrollleft: fixed ? 0 : viewport.scrollLeft(),
3046
+ scrolltop: fixed ? 0 : viewport.scrollTop(),
3047
+ offset: viewport.offset() || { left: 0, top: 0 }
3048
+ };
3049
+ container = {
3050
+ elem: container,
3051
+ scrollLeft: container.scrollLeft(),
3052
+ scrollTop: container.scrollTop(),
3053
+ offset: container.offset() || { left: 0, top: 0 }
3054
+ };
3055
+
3056
+ // Generic calculation method
3057
+ function calculate(side, otherSide, type, adjust, side1, side2, lengthName, targetLength, elemLength) {
3058
+ var initialPos = position[side1],
3059
+ mySide = my[side], atSide = at[side],
3060
+ isShift = type === SHIFT,
3061
+ viewportScroll = -container.offset[side1] + viewport.offset[side1] + viewport['scroll'+side1],
3062
+ myLength = mySide === side1 ? elemLength : mySide === side2 ? -elemLength : -elemLength / 2,
3063
+ atLength = atSide === side1 ? targetLength : atSide === side2 ? -targetLength : -targetLength / 2,
3064
+ tipLength = tip && tip.size ? tip.size[lengthName] || 0 : 0,
3065
+ tipAdjust = tip && tip.corner && tip.corner.precedance === side && !isShift ? tipLength : 0,
3066
+ overflow1 = viewportScroll - initialPos + tipAdjust,
3067
+ overflow2 = initialPos + elemLength - viewport[lengthName] - viewportScroll + tipAdjust,
3068
+ offset = myLength - (my.precedance === side || mySide === my[otherSide] ? atLength : 0) - (atSide === CENTER ? targetLength / 2 : 0);
3069
+
3070
+ // shift
3071
+ if(isShift) {
3072
+ tipAdjust = tip && tip.corner && tip.corner.precedance === otherSide ? tipLength : 0;
3073
+ offset = (mySide === side1 ? 1 : -1) * myLength - tipAdjust;
3074
+
3075
+ // Adjust position but keep it within viewport dimensions
3076
+ position[side1] += overflow1 > 0 ? overflow1 : overflow2 > 0 ? -overflow2 : 0;
3077
+ position[side1] = Math.max(
3078
+ -container.offset[side1] + viewport.offset[side1] + (tipAdjust && tip.corner[side] === CENTER ? tip.offset : 0),
3079
+ initialPos - offset,
3080
+ Math.min(
3081
+ Math.max(-container.offset[side1] + viewport.offset[side1] + viewport[lengthName], initialPos + offset),
3082
+ position[side1]
3083
+ )
3084
+ );
3085
+ }
3086
+
3087
+ // flip/flipinvert
3088
+ else {
3089
+ // Update adjustment amount depending on if using flipinvert or flip
3090
+ adjust *= (type === FLIPINVERT ? 2 : 0);
3091
+
3092
+ // Check for overflow on the left/top
3093
+ if(overflow1 > 0 && (mySide !== side1 || overflow2 > 0)) {
3094
+ position[side1] -= offset + adjust;
3095
+ newMy['invert'+side](side1);
3096
+ }
3097
+
3098
+ // Check for overflow on the bottom/right
3099
+ else if(overflow2 > 0 && (mySide !== side2 || overflow1 > 0) ) {
3100
+ position[side1] -= (mySide === CENTER ? -offset : offset) + adjust;
3101
+ newMy['invert'+side](side2);
3102
+ }
3103
+
3104
+ // Make sure we haven't made things worse with the adjustment and reset if so
3105
+ if(position[side1] < viewportScroll && -position[side1] > overflow2) {
3106
+ position[side1] = initialPos; newMy = my.clone();
3107
+ }
3108
+ }
3109
+
3110
+ return position[side1] - initialPos;
3111
+ }
3112
+
3113
+ // Set newMy if using flip or flipinvert methods
3114
+ if(methodX !== 'shift' || methodY !== 'shift') { newMy = my.clone(); }
3115
+
3116
+ // Adjust position based onviewport and adjustment options
3117
+ adjusted = {
3118
+ left: methodX !== 'none' ? calculate( X, Y, methodX, adjust.x, LEFT, RIGHT, WIDTH, targetWidth, elemWidth ) : 0,
3119
+ top: methodY !== 'none' ? calculate( Y, X, methodY, adjust.y, TOP, BOTTOM, HEIGHT, targetHeight, elemHeight ) : 0
3120
+ };
3121
+
3122
+ // Set tooltip position class if it's changed
3123
+ if(newMy && cache.lastClass !== (newClass = uitooltip + '-pos-' + newMy.abbrev())) {
3124
+ tooltip.removeClass(api.cache.lastClass).addClass( (api.cache.lastClass = newClass) );
3125
+ }
3126
+
3127
+ return adjusted;
3128
+ };
3129
+ PLUGINS.imagemap = function(api, area, corner, adjustMethod)
3130
+ {
3131
+ if(!area.jquery) { area = $(area); }
3132
+
3133
+ var cache = (api.cache.areas = {}),
3134
+ shape = (area[0].shape || area.attr('shape')).toLowerCase(),
3135
+ coordsString = area[0].coords || area.attr('coords'),
3136
+ baseCoords = coordsString.split(','),
3137
+ coords = [],
3138
+ image = $('img[usemap="#'+area.parent('map').attr('name')+'"]'),
3139
+ imageOffset = image.offset(),
3140
+ result = {
3141
+ width: 0, height: 0,
3142
+ position: {
3143
+ top: 1e10, right: 0,
3144
+ bottom: 0, left: 1e10
3145
+ }
3146
+ },
3147
+ i = 0, next = 0, dimensions;
3148
+
3149
+ // POLY area coordinate calculator
3150
+ // Special thanks to Ed Cradock for helping out with this.
3151
+ // Uses a binary search algorithm to find suitable coordinates.
3152
+ function polyCoordinates(result, coords, corner)
3153
+ {
3154
+ var i = 0,
3155
+ compareX = 1, compareY = 1,
3156
+ realX = 0, realY = 0,
3157
+ newWidth = result.width,
3158
+ newHeight = result.height;
3159
+
3160
+ // Use a binary search algorithm to locate most suitable coordinate (hopefully)
3161
+ while(newWidth > 0 && newHeight > 0 && compareX > 0 && compareY > 0)
3162
+ {
3163
+ newWidth = Math.floor(newWidth / 2);
3164
+ newHeight = Math.floor(newHeight / 2);
3165
+
3166
+ if(corner.x === LEFT){ compareX = newWidth; }
3167
+ else if(corner.x === RIGHT){ compareX = result.width - newWidth; }
3168
+ else{ compareX += Math.floor(newWidth / 2); }
3169
+
3170
+ if(corner.y === TOP){ compareY = newHeight; }
3171
+ else if(corner.y === BOTTOM){ compareY = result.height - newHeight; }
3172
+ else{ compareY += Math.floor(newHeight / 2); }
3173
+
3174
+ i = coords.length; while(i--)
3175
+ {
3176
+ if(coords.length < 2){ break; }
3177
+
3178
+ realX = coords[i][0] - result.position.left;
3179
+ realY = coords[i][1] - result.position.top;
3180
+
3181
+ if((corner.x === LEFT && realX >= compareX) ||
3182
+ (corner.x === RIGHT && realX <= compareX) ||
3183
+ (corner.x === CENTER && (realX < compareX || realX > (result.width - compareX))) ||
3184
+ (corner.y === TOP && realY >= compareY) ||
3185
+ (corner.y === BOTTOM && realY <= compareY) ||
3186
+ (corner.y === CENTER && (realY < compareY || realY > (result.height - compareY)))) {
3187
+ coords.splice(i, 1);
3188
+ }
3189
+ }
3190
+ }
3191
+
3192
+ return { left: coords[0][0], top: coords[0][1] };
3193
+ }
3194
+
3195
+ // Make sure we account for padding and borders on the image
3196
+ imageOffset.left += Math.ceil((image.outerWidth() - image.width()) / 2);
3197
+ imageOffset.top += Math.ceil((image.outerHeight() - image.height()) / 2);
3198
+
3199
+ // Parse coordinates into proper array
3200
+ if(shape === 'poly') {
3201
+ i = baseCoords.length; while(i--)
3202
+ {
3203
+ next = [ parseInt(baseCoords[--i], 10), parseInt(baseCoords[i+1], 10) ];
3204
+
3205
+ if(next[0] > result.position.right){ result.position.right = next[0]; }
3206
+ if(next[0] < result.position.left){ result.position.left = next[0]; }
3207
+ if(next[1] > result.position.bottom){ result.position.bottom = next[1]; }
3208
+ if(next[1] < result.position.top){ result.position.top = next[1]; }
3209
+
3210
+ coords.push(next);
3211
+ }
3212
+ }
3213
+ else {
3214
+ i = -1; while(i++ < baseCoords.length) {
3215
+ coords.push( parseInt(baseCoords[i], 10) );
3216
+ }
3217
+ }
3218
+
3219
+ // Calculate details
3220
+ switch(shape)
3221
+ {
3222
+ case 'rect':
3223
+ result = {
3224
+ width: Math.abs(coords[2] - coords[0]),
3225
+ height: Math.abs(coords[3] - coords[1]),
3226
+ position: {
3227
+ left: Math.min(coords[0], coords[2]),
3228
+ top: Math.min(coords[1], coords[3])
3229
+ }
3230
+ };
3231
+ break;
3232
+
3233
+ case 'circle':
3234
+ result = {
3235
+ width: coords[2] + 2,
3236
+ height: coords[2] + 2,
3237
+ position: { left: coords[0], top: coords[1] }
3238
+ };
3239
+ break;
3240
+
3241
+ case 'poly':
3242
+ result.width = Math.abs(result.position.right - result.position.left);
3243
+ result.height = Math.abs(result.position.bottom - result.position.top);
3244
+
3245
+ if(corner.abbrev() === 'c') {
3246
+ result.position = {
3247
+ left: result.position.left + (result.width / 2),
3248
+ top: result.position.top + (result.height / 2)
3249
+ };
3250
+ }
3251
+ else {
3252
+ // Calculate if we can't find a cached value
3253
+ if(!cache[corner+coordsString]) {
3254
+ result.position = polyCoordinates(result, coords.slice(), corner);
3255
+
3256
+ // If flip adjustment is enabled, also calculate the closest opposite point
3257
+ if(adjustMethod && (adjustMethod[0] === 'flip' || adjustMethod[1] === 'flip')) {
3258
+ result.offset = polyCoordinates(result, coords.slice(), {
3259
+ x: corner.x === LEFT ? RIGHT : corner.x === RIGHT ? LEFT : CENTER,
3260
+ y: corner.y === TOP ? BOTTOM : corner.y === BOTTOM ? TOP : CENTER
3261
+ });
3262
+
3263
+ result.offset.left -= result.position.left;
3264
+ result.offset.top -= result.position.top;
3265
+ }
3266
+
3267
+ // Store the result
3268
+ cache[corner+coordsString] = result;
3269
+ }
3270
+
3271
+ // Grab the cached result
3272
+ result = cache[corner+coordsString];
3273
+ }
3274
+
3275
+ result.width = result.height = 0;
3276
+ break;
3277
+ }
3278
+
3279
+ // Add image position to offset coordinates
3280
+ result.position.left += imageOffset.left;
3281
+ result.position.top += imageOffset.top;
3282
+
3283
+ return result;
3284
+ };
3285
+
3286
+
3287
+ /*
3288
+ * BGIFrame adaption (http://plugins.jquery.com/project/bgiframe)
3289
+ * Special thanks to Brandon Aaron
3290
+ */
3291
+ function BGIFrame(api)
3292
+ {
3293
+ var self = this,
3294
+ elems = api.elements,
3295
+ tooltip = elems.tooltip,
3296
+ namespace = '.bgiframe-' + api.id;
3297
+
3298
+ $.extend(self, {
3299
+ init: function()
3300
+ {
3301
+ // Create the BGIFrame element
3302
+ elems.bgiframe = $('<iframe class="ui-tooltip-bgiframe" frameborder="0" tabindex="-1" src="javascript:\'\';" ' +
3303
+ ' style="display:block; position:absolute; z-index:-1; filter:alpha(opacity=0); ' +
3304
+ '-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";"></iframe>');
3305
+
3306
+ // Append the new element to the tooltip
3307
+ elems.bgiframe.appendTo(tooltip);
3308
+
3309
+ // Update BGIFrame on tooltip move
3310
+ tooltip.bind('tooltipmove'+namespace, self.adjust);
3311
+ },
3312
+
3313
+ adjust: function()
3314
+ {
3315
+ var dimensions = api.get('dimensions'), // Determine current tooltip dimensions
3316
+ plugin = api.plugins.tip,
3317
+ tip = elems.tip,
3318
+ tipAdjust, offset;
3319
+
3320
+ // Adjust border offset
3321
+ offset = parseInt(tooltip.css('border-left-width'), 10) || 0;
3322
+ offset = { left: -offset, top: -offset };
3323
+
3324
+ // Adjust for tips plugin
3325
+ if(plugin && tip) {
3326
+ tipAdjust = (plugin.corner.precedance === 'x') ? ['width', 'left'] : ['height', 'top'];
3327
+ offset[ tipAdjust[1] ] -= tip[ tipAdjust[0] ]();
3328
+ }
3329
+
3330
+ // Update bgiframe
3331
+ elems.bgiframe.css(offset).css(dimensions);
3332
+ },
3333
+
3334
+ destroy: function()
3335
+ {
3336
+ // Remove iframe
3337
+ elems.bgiframe.remove();
3338
+
3339
+ // Remove bound events
3340
+ tooltip.unbind(namespace);
3341
+ }
3342
+ });
3343
+
3344
+ self.init();
3345
+ }
3346
+
3347
+ PLUGINS.bgiframe = function(api)
3348
+ {
3349
+ var browser = $.browser,
3350
+ self = api.plugins.bgiframe;
3351
+
3352
+ // Proceed only if the browser is IE6 and offending elements are present
3353
+ if($('select, object').length < 1 || !(browser.msie && (''+browser.version).charAt(0) === '6')) {
3354
+ return FALSE;
3355
+ }
3356
+
3357
+ return 'object' === typeof self ? self : (api.plugins.bgiframe = new BGIFrame(api));
3358
+ };
3359
+
3360
+ // Plugin needs to be initialized on render
3361
+ PLUGINS.bgiframe.initialize = 'render';
3362
+
3363
+
3364
+ }));
3365
+ }( window, document ));