1/** 2 * PowerTip 3 * 4 * @fileoverview jQuery plugin that creates hover tooltips. 5 * @link http://stevenbenner.github.com/jquery-powertip/ 6 * @author Steven Benner (http://stevenbenner.com/) 7 * @version 1.1.0 8 * @requires jQuery 1.7+ 9 * 10 * @license jQuery PowerTip Plugin v1.1.0 11 * http://stevenbenner.github.com/jquery-powertip/ 12 * Copyright 2012 Steven Benner (http://stevenbenner.com/) 13 * Released under the MIT license. 14 * <https://raw.github.com/stevenbenner/jquery-powertip/master/LICENSE.txt> 15 */ 16 17(function($) { 18 'use strict'; 19 20 // useful private variables 21 var $document = $(document), 22 $window = $(window), 23 $body = $('body'); 24 25 /** 26 * Session data 27 * Private properties global to all powerTip instances 28 * @type Object 29 */ 30 var session = { 31 isPopOpen: false, 32 isFixedPopOpen: false, 33 isClosing: false, 34 popOpenImminent: false, 35 activeHover: null, 36 currentX: 0, 37 currentY: 0, 38 previousX: 0, 39 previousY: 0, 40 desyncTimeout: null, 41 mouseTrackingActive: false 42 }; 43 44 /** 45 * Display hover tooltips on the matched elements. 46 * @param {Object} opts The options object to use for the plugin. 47 * @return {Object} jQuery object for the matched selectors. 48 */ 49 $.fn.powerTip = function(opts) { 50 51 // don't do any work if there were no matched elements 52 if (!this.length) { 53 return this; 54 } 55 56 // extend options 57 var options = $.extend({}, $.fn.powerTip.defaults, opts), 58 tipController = new TooltipController(options); 59 60 // hook mouse tracking 61 initMouseTracking(); 62 63 // setup the elements 64 this.each(function() { 65 var $this = $(this), 66 dataPowertip = $this.data('powertip'), 67 dataElem = $this.data('powertipjq'), 68 dataTarget = $this.data('powertiptarget'), 69 title = $this.attr('title'); 70 71 72 // attempt to use title attribute text if there is no data-powertip, 73 // data-powertipjq or data-powertiptarget. If we do use the title 74 // attribute, delete the attribute so the browser will not show it 75 if (!dataPowertip && !dataTarget && !dataElem && title) { 76 $this.data('powertip', title); 77 $this.removeAttr('title'); 78 } 79 80 // create hover controllers for each element 81 $this.data( 82 'displayController', 83 new DisplayController($this, options, tipController) 84 ); 85 }); 86 87 // attach hover events to all matched elements 88 return this.on({ 89 // mouse events 90 mouseenter: function(event) { 91 trackMouse(event); 92 session.previousX = event.pageX; 93 session.previousY = event.pageY; 94 $(this).data('displayController').show(); 95 }, 96 mouseleave: function() { 97 $(this).data('displayController').hide(); 98 }, 99 100 // keyboard events 101 focus: function() { 102 var element = $(this); 103 if (!isMouseOver(element)) { 104 element.data('displayController').show(true); 105 } 106 }, 107 blur: function() { 108 $(this).data('displayController').hide(true); 109 } 110 }); 111 112 }; 113 114 /** 115 * Default options for the powerTip plugin. 116 * @type Object 117 */ 118 $.fn.powerTip.defaults = { 119 fadeInTime: 200, 120 fadeOutTime: 100, 121 followMouse: false, 122 popupId: 'powerTip', 123 intentSensitivity: 7, 124 intentPollInterval: 100, 125 closeDelay: 100, 126 placement: 'n', 127 smartPlacement: false, 128 offset: 10, 129 mouseOnToPopup: false 130 }; 131 132 /** 133 * Default smart placement priority lists. 134 * The first item in the array is the highest priority, the last is the 135 * lowest. The last item is also the default, which will be used if all 136 * previous options do not fit. 137 * @type Object 138 */ 139 $.fn.powerTip.smartPlacementLists = { 140 n: ['n', 'ne', 'nw', 's'], 141 e: ['e', 'ne', 'se', 'w', 'nw', 'sw', 'n', 's', 'e'], 142 s: ['s', 'se', 'sw', 'n'], 143 w: ['w', 'nw', 'sw', 'e', 'ne', 'se', 'n', 's', 'w'], 144 nw: ['nw', 'w', 'sw', 'n', 's', 'se', 'nw'], 145 ne: ['ne', 'e', 'se', 'n', 's', 'sw', 'ne'], 146 sw: ['sw', 'w', 'nw', 's', 'n', 'ne', 'sw'], 147 se: ['se', 'e', 'ne', 's', 'n', 'nw', 'se'] 148 }; 149 150 /** 151 * Public API 152 * @type Object 153 */ 154 $.powerTip = { 155 156 /** 157 * Attempts to show the tooltip for the specified element. 158 * @public 159 * @param {Object} element The element that the tooltip should for. 160 */ 161 showTip: function(element) { 162 // close any open tooltip 163 $.powerTip.closeTip(); 164 // grab only the first matched element and ask it to show its tip 165 element = element.first(); 166 if (!isMouseOver(element)) { 167 element.data('displayController').show(true, true); 168 } 169 }, 170 171 /** 172 * Attempts to close any open tooltips. 173 * @public 174 */ 175 closeTip: function() { 176 $document.triggerHandler('closePowerTip'); 177 } 178 179 }; 180 181 /** 182 * Creates a new tooltip display controller. 183 * @private 184 * @constructor 185 * @param {Object} element The element that this controller will handle. 186 * @param {Object}
186 options Options object containing settings. 187 * @param {TooltipController} tipController The TooltipController for this instance. 188 */ 189 function DisplayController(element, options, tipController) { 190 var hoverTimer = null; 191 192 /** 193 * Begins the process of showing a tooltip. 194 * @private 195 * @param {Boolean=} immediate Skip intent testing (optional). 196 * @param {Boolean=} forceOpen Ignore cursor position and force tooltip to open (optional). 197 */ 198 function openTooltip(immediate, forceOpen) { 199 cancelTimer(); 200 if (!element.data('hasActiveHover')) { 201 if (!immediate) { 202 session.popOpenImminent = true; 203 hoverTimer = setTimeout( 204 function() { 205 hoverTimer = null; 206 checkForIntent(element); 207 }, 208 options.intentPollInterval 209 ); 210 } else { 211 if (forceOpen) { 212 element.data('forcedOpen', true); 213 } 214 tipController.showTip(element); 215 } 216 } 217 } 218 219 /** 220 * Begins the process of closing a tooltip. 221 * @private 222 * @param {Boolean=} disableDelay Disable close delay (optional). 223 */ 224 function closeTooltip(disableDelay) { 225 cancelTimer(); 226 if (element.data('hasActiveHover')) { 227 session.popOpenImminent = false; 228 element.data('forcedOpen', false); 229 if (!disableDelay) { 230 hoverTimer = setTimeout( 231 function() { 232 hoverTimer = null; 233 tipController.hideTip(element); 234 }, 235 options.closeDelay 236 ); 237 } else { 238 tipController.hideTip(element); 239 } 240 } 241 } 242 243 /** 244 * Checks mouse position to make sure that the user intended to hover 245 * on the specified element before showing the tooltip. 246 * @private 247 */ 248 function checkForIntent() { 249 // calculate mouse position difference 250 var xDifference = Math.abs(session.previousX - session.currentX), 251 yDifference = Math.abs(session.previousY - session.currentY), 252 totalDifference = xDifference + yDifference; 253 254 // check if difference has passed the sensitivity threshold 255 if (totalDifference < options.intentSensitivity) { 256 tipController.showTip(element); 257 } else { 258 // try again 259 session.previousX = session.currentX; 260 session.previousY = session.currentY; 261 openTooltip(); 262 } 263 } 264 265 /** 266 * Cancels active hover timer. 267 * @private 268 */ 269 function cancelTimer() { 270 hoverTimer = clearTimeout(hoverTimer); 271 } 272 273 // expose the methods 274 return { 275 show: openTooltip, 276 hide: closeTooltip, 277 cancel: cancelTimer 278 }; 279 } 280 281 /** 282 * Creates a new tooltip controller. 283 * @private 284 * @constructor 285 * @param {Object} options Options object containing settings. 286 */ 287 function TooltipController(options) { 288 289 // build and append popup div if it does not already exist 290 var tipElement = $('#' + options.popupId); 291 if (tipElement.length === 0) { 292 tipElement = $('<div></div>', { id: options.popupId }); 293 // grab body element if it was not populated when the script loaded 294 // this hack exists solely for jsfiddle support 295 if ($body.length === 0) { 296 $body = $('body'); 297 } 298 $body.append(tipElement); 299 } 300 301 // hook mousemove for cursor follow tooltips 302 if (options.followMouse) { 303 // only one positionTipOnCursor hook per popup element, please 304 if (!tipElement.data('hasMouseMove')) { 305 $document.on({ 306 mousemove: positionTipOnCursor, 307 scroll: positionTipOnCursor 308 }); 309 } 310 tipElement.data('hasMouseMove', true); 311 } 312 313 // if we want to be able to mouse onto the popup then we need to attach 314 // hover events to the popup that will cancel a close request on hover 315 // and start a new close request on mouseleave 316 if (options.followMouse || options.mouseOnToPopup) { 317 tipElement.on({ 318 mouseenter: function() { 319 if (tipElement.data('followMouse') || tipElement.data('mouseOnToPopup')) { 320 // check activeHover in case the mouse cursor entered 321 // the tooltip during the fadeOut and close cycle 322 if (session.activeHover) { 323 session.activeHover.data('displayController').cancel(); 324 } 325 } 326 }, 327 mouseleave: function() { 328 if (tipElement.data('mouseOnToPopup')) { 329 // check activeHover in case the mouse cursor entered 330 // the tooltip during the fadeOut and close cycle 331 if (session.activeHover) { 332 session.activeHover.data('displayController').hide(); 333 } 334 } 335 } 336 }); 337 } 338 339 /** 340 * Gives the specified element the active-hover state and queues up 341 * the showTip function. 342 * @private 343 * @param {Object} element The element that the tooltip should target. 344 */ 345 function beginShowTip(element) { 346 element.data('hasActiveHover', true); 347 // show popup, asap 348 tipElement.queue(function(next) { 349 showTip(element); 350 next(); 351 }); 352 } 353 354 /** 355 * Shows the tooltip popup, as soon as possible. 356 * @private 357 * @param {Object} element The element that the popup should target. 358 */ 359 function showTip(element) { 360 // it is possible, especially with keyboard navigation, to move on 361 // to another element with a tooltip during the queue to get to 362 // this point in the code. if that happens then we need to not
363 // proceed or we may have the fadeout callback for the last tooltip 364 // execute immediately after this code runs, causing bugs. 365 if (!element.data('hasActiveHover')) { 366 return; 367 } 368 369 // if the popup is open and we got asked to open another one then 370 // the old one is still in its fadeOut cycle, so wait and try again 371 if (session.isPopOpen) { 372 if (!session.isClosing) { 373 hideTip(session.activeHover); 374 } 375 tipElement.delay(100).queue(function(next) { 376 showTip(element); 377 next(); 378 }); 379 return; 380 } 381 382 // trigger powerTipPreRender event 383 element.trigger('powerTipPreRender'); 384 385 var tipText = element.data('powertip'), 386 tipTarget = element.data('powertiptarget'), 387 tipElem = element.data('powertipjq'), 388 tipContent = tipTarget ? $('#' + tipTarget) : []; 389 390 // set popup content 391 if (tipText) { 392 tipElement.html(tipText); 393 } else if (tipElem && tipElem.length > 0) { 394 tipElement.empty(); 395 tipElem.clone(true, true).appendTo(tipElement); 396 } else if (tipContent && tipContent.length > 0) { 397 tipElement.html($('#' + tipTarget).html()); 398 } else { 399 // we have no content to display, give up 400 return; 401 } 402 403 // trigger powerTipRender event 404 element.trigger('powerTipRender'); 405 406 // hook close event for triggering from the api 407 $document.on('closePowerTip', function() { 408 element.data('displayController').hide(true); 409 }); 410 411 session.activeHover = element; 412 session.isPopOpen = true; 413 414 tipElement.data('followMouse', options.followMouse); 415 tipElement.data('mouseOnToPopup', options.mouseOnToPopup); 416 417 // set popup position 418 if (!options.followMouse) { 419 positionTipOnElement(element); 420 session.isFixedPopOpen = true; 421 } else { 422 positionTipOnCursor(); 423 } 424 425 // fadein 426 tipElement.fadeIn(options.fadeInTime, function() { 427 // start desync polling 428 if (!session.desyncTimeout) { 429 session.desyncTimeout = setInterval(closeDesyncedTip, 500); 430 } 431 432 // trigger powerTipOpen event 433 element.trigger('powerTipOpen'); 434 }); 435 } 436 437 /** 438 * Hides the tooltip popup, immediately. 439 * @private 440 * @param {Object} element The element that the popup should target. 441 */ 442 function hideTip(element) { 443 session.isClosing = true; 444 element.data('hasActiveHover', false); 445 element.data('forcedOpen', false); 446 // reset session 447 session.activeHover = null; 448 session.isPopOpen = false; 449 // stop desync polling 450 session.desyncTimeout = clearInterval(session.desyncTimeout); 451 // unhook close event api listener 452 $document.off('closePowerTip'); 453 // fade out 454 tipElement.fadeOut(options.fadeOutTime, function() { 455 session.isClosing = false; 456 session.isFixedPopOpen = false; 457 tipElement.removeClass(); 458 // support mouse-follow and fixed position pops at the same 459 // time by moving the popup to the last known cursor location 460 // after it is hidden 461 setTipPosition( 462 session.currentX + options.offset, 463 session.currentY + options.offset 464 ); 465 466 // trigger powerTipClose event 467 element.trigger('powerTipClose'); 468 }); 469 } 470 471 /** 472 * Checks for a tooltip desync and closes the tooltip if one occurs. 473 * @private 474 */ 475 function closeDesyncedTip() { 476 // It is possible for the mouse cursor to leave an element without 477 // firing the mouseleave event. This seems to happen (in FF) if the 478 // element is disabled under mouse cursor, the element is moved out 479 // from under the mouse cursor (such as a slideDown() occurring 480 // above it), or if the browser is resized by code moving the 481 // element from under the mouse cursor. If this happens it will 482 // result in a desynced tooltip because we wait for any exiting 483 // open tooltips to close before opening a new one. So we should 484 // periodically check for a desync situation and close the tip if 485 // such a situation arises. 486 if (session.isPopOpen && !session.isClosing) { 487 var isDesynced = false; 488 489 // case 1: user already moused onto another tip - easy test 490 if (session.activeHover.data('hasActiveHover') === false) { 491 isDesynced = true; 492 } else { 493 // case 2: hanging tip - have to test if mouse position is 494 // not over the active hover and not over a tooltip set to 495 // let the user interact with it. 496 // for keyboard navigation, this only counts if the element 497 // does not have focus. 498 // for tooltips opened via the api we need to check if it 499 // has the forcedOpen flag. 500 if (!isMouseOver(session.activeHover) && !session.activeHover.is(":focus") && !session.activeHover.data('forcedOpen')) { 501 if (tipElement.data('mouseOnToPopup')) { 502 if (!isMouseOver(tipElement)) { 503 isDesynced = true; 504 } 505 } else { 506 isDesynced = true; 507 } 508 } 509 } 510 511 if (isDesynced) { 512 // close the desynced tip 513 hideTip(session.activeHover); 514 } 515 } 516 } 517 518 /** 519 * Moves the tooltip popup to the users mouse cursor. 520 * @private 521 */ 522 function positionTipOnCursor() { 523 // to support having fixed powertips on the same page as cursor 524 // powertips, where both instances are referencing the same popup 525 // element, we need to keep track of the mouse position constantly, 526 // but we should only set the pop location if a fixed pop is not 527 // currently open, a pop open is imminent or active, and the popup 528 // element in question does have a mouse-follow using it.
529 if ((session.isPopOpen && !session.isFixedPopOpen) || (session.popOpenImminent && !session.isFixedPopOpen && tipElement.data('hasMouseMove'))) { 530 // grab measurements 531 var scrollTop = $window.scrollTop(), 532 windowWidth = $window.width(), 533 windowHeight = $window.height(), 534 popWidth = tipElement.outerWidth(), 535 popHeight = tipElement.outerHeight(), 536 x = 0, 537 y = 0; 538 539 // constrain pop to browser viewport 540 if ((popWidth + session.currentX + options.offset) < windowWidth) { 541 x = session.currentX + options.offset; 542 } else { 543 x = windowWidth - popWidth; 544 } 545 if ((popHeight + session.currentY + options.offset) < (scrollTop + windowHeight)) { 546 y = session.currentY + options.offset; 547 } else { 548 y = scrollTop + windowHeight - popHeight; 549 } 550 551 // position the tooltip 552 setTipPosition(x, y); 553 } 554 } 555 556 /** 557 * Sets the tooltip popup too the correct position relative to the 558 * specified target element. Based on options settings. 559 * @private 560 * @param {Object} element The element that the popup should target. 561 */ 562 function positionTipOnElement(element) { 563 var tipWidth = tipElement.outerWidth(), 564 tipHeight = tipElement.outerHeight(), 565 priorityList, 566 placementCoords, 567 finalPlacement, 568 collisions; 569 570 // with smart placement we will try a series of placement 571 // options and use the first one that does not collide with the 572 // browser view port boundaries. 573 if (options.smartPlacement) { 574 575 // grab the placement priority list 576 priorityList = $.fn.powerTip.smartPlacementLists[options.placement]; 577 578 // iterate over the priority list and use the first placement 579 // option that does not collide with the viewport. if they all 580 // collide then the last placement in the list will be used. 581 $.each(priorityList, function(idx, pos) { 582 // get placement coordinates 583 placementCoords = computePlacementCoords( 584 element, 585 pos, 586 tipWidth, 587 tipHeight 588 ); 589 finalPlacement = pos; 590 591 // find collisions 592 collisions = getViewportCollisions( 593 placementCoords, 594 tipWidth, 595 tipHeight 596 ); 597 598 // break if there were no collisions 599 if (collisions.length === 0) { 600 return false; 601 } 602 }); 603 604 } else { 605 606 // if we're not going to use the smart placement feature then 607 // just compute the coordinates and do it 608 placementCoords = computePlacementCoords( 609 element, 610 options.placement, 611 tipWidth, 612 tipHeight 613 ); 614 finalPlacement = options.placement; 615 616 } 617 618 // add placement as class for CSS arrows 619 tipElement.addClass(finalPlacement); 620 621 // position the tooltip 622 setTipPosition(placementCoords.x, placementCoords.y); 623 } 624 625 /** 626 * Compute the top/left coordinates to display the tooltip at the 627 * specified placement relative to the specified element. 628 * @private 629 * @param {Object} element The element that the tooltip should target. 630 * @param {String} placement The placement for the tooltip. 631 * @param {Number} popWidth Width of the tooltip element in pixels. 632 * @param {Number} popHeight Height of the tooltip element in pixels. 633 * @retun {Object} An object with the x and y coordinates. 634 */ 635 function computePlacementCoords(element, placement, popWidth, popHeight) { 636 // grab measurements 637 var objectOffset = element.offset(), 638 objectWidth = element.outerWidth(), 639 objectHeight = element.outerHeight(), 640 x = 0, 641 y = 0; 642 643 // calculate the appropriate x and y position in the document 644 switch (placement) { 645 case 'n': 646 x = (objectOffset.left + (objectWidth / 2)) - (popWidth / 2); 647 y = objectOffset.top - popHeight - options.offset; 648 break; 649 case 'e': 650 x = objectOffset.left + objectWidth + options.offset; 651 y = (objectOffset.top + (objectHeight / 2)) - (popHeight / 2); 652 break; 653 case 's': 654 x = (objectOffset.left + (objectWidth / 2)) - (popWidth / 2); 655 y = objectOffset.top + objectHeight + options.offset; 656 break; 657 case 'w': 658 x = objectOffset.left - popWidth - options.offset; 659 y = (objectOffset.top + (objectHeight / 2)) - (popHeight / 2); 660 break; 661 case 'nw': 662 x = (objectOffset.left - popWidth) + 20; 663 y = objectOffset.top - popHeight - options.offset; 664 break;
665 case 'ne': 666 x = (objectOffset.left + objectWidth) - 20; 667 y = objectOffset.top - popHeight - options.offset; 668 break; 669 case 'sw': 670 x = (objectOffset.left - popWidth) + 20; 671 y = objectOffset.top + objectHeight + options.offset; 672 break; 673 case 'se': 674 x = (objectOffset.left + objectWidth) - 20; 675 y = objectOffset.top + objectHeight + options.offset; 676 break; 677 } 678 679 return { 680 x: Math.round(x), 681 y: Math.round(y) 682 }; 683 } 684 685 /** 686 * Sets the tooltip CSS position on the document. 687 * @private 688 * @param {Number} x Left position in pixels. 689 * @param {Number} y Top position in pixels. 690 */ 691 function setTipPosition(x, y) { 692 tipElement.css('left', x + 'px'); 693 tipElement.css('top', y + 'px'); 694 } 695 696 // expose methods 697 return { 698 showTip: beginShowTip, 699 hideTip: hideTip 700 }; 701 } 702 703 /** 704 * Hooks mouse position tracking to mousemove and scroll events. 705 * Prevents attaching the events more than once. 706 * @private 707 */ 708 function initMouseTracking() { 709 var lastScrollX = 0, 710 lastScrollY = 0; 711 712 if (!session.mouseTrackingActive) { 713 session.mouseTrackingActive = true; 714 715 // grab the current scroll position on load 716 $(function() { 717 lastScrollX = $document.scrollLeft(); 718 lastScrollY = $document.scrollTop(); 719 }); 720 721 // hook mouse position tracking 722 $document.on({ 723 mousemove: trackMouse, 724 scroll: function() { 725 var x = $document.scrollLeft(), 726 y = $document.scrollTop(); 727 if (x !== lastScrollX) { 728 session.currentX += x - lastScrollX; 729 lastScrollX = x; 730 } 731 if (y !== lastScrollY) { 732 session.currentY += y - lastScrollY; 733 lastScrollY = y; 734 } 735 } 736 }); 737 } 738 } 739 740 /** 741 * Saves the current mouse coordinates to the powerTip session object. 742 * @private 743 * @param {Object} event The mousemove event for the document. 744 */ 745 function trackMouse(event) { 746 session.currentX = event.pageX; 747 session.currentY = event.pageY; 748 } 749 750 /** 751 * Tests if the mouse is currently over the specified element. 752 * @private 753 * @param {Object} element The element to check for hover. 754 * @return {Boolean} 755 */ 756 function isMouseOver(element) { 757 var elementPosition = element.offset(); 758 return session.currentX >= elementPosition.left && 759 session.currentX <= elementPosition.left + element.outerWidth() && 760 session.currentY >= elementPosition.top && 761 session.currentY <= elementPosition.top + element.outerHeight(); 762 } 763 764 /** 765 * Finds any viewport collisions that an element (the tooltip) would have 766 * if it were absolutely positioned at the specified coordinates. 767 * @private 768 * @param {Object} coords Coordinates for the element. (e.g. {x: 123, y: 123}) 769 * @param {Number} elementWidth Width of the element in pixels. 770 * @param {Number} elementHeight Height of the element in pixels. 771 * @return {Array} Array of words representing directional collisions. 772 */ 773 function getViewportCollisions(coords, elementWidth, elementHeight) { 774 var scrollLeft = $window.scrollLeft(), 775 scrollTop = $window.scrollTop(), 776 windowWidth = $window.width(), 777 windowHeight = $window.height(), 778 collisions = []; 779 780 if (coords.y < scrollTop) { 781 collisions.push('top'); 782 } 783 if (coords.y + elementHeight > scrollTop + windowHeight) { 784 collisions.push('bottom'); 785 } 786 if (coords.x < scrollLeft) { 787 collisions.push('left'); 788 } 789 if (coords.x + elementWidth > scrollLeft + windowWidth) { 790 collisions.push('right'); 791 } 792 793 return collisions; 794 } 795 796}(jQuery));
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.