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