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