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