vendor: 104,280 bytes, lines 1-3787
1/** 2 * tooltipster http://iamceege.github.io/tooltipster/ 3 * A rockin' custom tooltip jQuery plugin 4 * Developed by Caleb Jacob and Louis Ameline 5 * MIT license 6 */ 7(function (root, factory) { 8 if (typeof define === 'function' && define.amd) { 9 // AMD. Register as an anonymous module unless amdModuleId is set 10 define(["jquery"], function (a0) { 11 return (factory(a0)); 12 }); 13 } else if (typeof exports === 'object') { 14 // Node. Does not work with strict CommonJS, but 15 // only CommonJS-like environments that support module.exports, 16 // like Node. 17 module.exports = factory(require("jquery")); 18 } else { 19 factory(jQuery); 20 } 21}(this, function ($) { 22 23 // This file will be UMDified by a build task. 24 25 var defaults = { 26 animation: 'fade', 27 animationDuration: 350, 28 content: null, 29 ppclass: '', 30 contentAsHTML: false, 31 contentCloning: false, 32 debug: true, 33 delay: 300, 34 delayTouch: [300, 500], 35 functionInit: null, 36 functionBefore: null, 37 functionReady: null, 38 functionAfter: null, 39 functionFormat: null, 40 IEmin: 6, 41 interactive: false, 42 multiple: false, 43 // will default to document.body, or must be an element positioned at (0, 0) 44 // in the document, typically like the very top views of an app. 45 parent: null, 46 plugins: ['sideTip'], 47 repositionOnScroll: false, 48 restoration: 'none', 49 selfDestruction: true, 50 theme: [], 51 timer: 0, 52 trackerInterval: 500, 53 trackOrigin: false, 54 trackTooltip: false, 55 trigger: 'hover', 56 triggerClose: { 57 click: false, 58 mouseleave: false, 59 originClick: false, 60 scroll: false, 61 tap: false, 62 touchleave: false 63 }, 64 triggerOpen: { 65 click: false, 66 mouseenter: false, 67 tap: false, 68 touchstart: false 69 }, 70 updateAnimation: 'rotate', 71 zIndex: 9999999 72 }, 73 // we'll avoid using the 'window' global as a good practice but npm's 74 // jquery@<2.1.0 package actually requires a 'window' global, so not sure 75 // it's useful at all 76 win = (typeof window != 'undefined') ? window : null, 77 // env will be proxied by the core for plugins to have access its properties 78 env = { 79 // detect if this device can trigger touch events. Better have a false 80 // positive (unused listeners, that's ok) than a false negative. 81 // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/touchevents.js 82 // http://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript 83 hasTouchCapability: !!( 84 win 85 && ('ontouchstart' in win 86 || (win.DocumentTouch && win.document instanceof win.DocumentTouch) 87 || win.navigator.maxTouchPoints 88 ) 89 ), 90 hasTransitions: transitionSupport(), 91 IE: false, 92 // don't set manually, it will be updated by a build task after the manifest 93 semVer: '4.2.6', 94 window: win 95 }, 96 core = function () { 97 98 // core variables 99 100 // the core emitters 101 this.__$emitterPrivate = $({}); 102 this.__$emitterPublic = $({}); 103 this.__instancesLatestArr = []; 104 // collects plugin constructors 105 this.__plugins = {}; 106 // proxy env variables for plugins who might use them 107 this._env = env; 108 }; 109 110 // core methods 111 core.prototype = { 112 113 /** 114 * A function to proxy the public methods of an object onto another 115 * 116 * @param {object} constructor The constructor to bridge 117 * @param {object} obj The object that will get new methods (an instance or the core) 118 * @param {string} pluginName A plugin name for the console log message 119 * @return {core} 120 * @private 121 */ 122 __bridge: function (constructor, obj, pluginName) { 123 124 // if it's not already bridged 125 if (!obj[pluginName]) { 126 127 var fn = function () { }; 128 fn.prototype = constructor; 129 130 var pluginInstance = new fn(); 131 132 // the _init method has to exist in instance constructors but might be missing 133 // in core constructors 134 if (pluginInstance.__init) { 135 pluginInstance.__init(obj); 136 } 137 138 $.each(constructor, function (methodName, fn) { 139 140 // don't proxy "private" methods, only "protected" and public ones 141 if (methodName.indexOf('__') != 0) { 142 143 // if the method does not exist yet 144 if (!obj[methodName]) { 145 146 obj[methodName] = function () { 147 return pluginInstance[methodName].apply(pluginInstance, Array.prototype.slice.apply(arguments)); 148 }; 149 150 // remember to which plugin this method corresponds (several plugins may 151 // have methods of the same name, we need to be sure) 152 obj[methodName].bridged = pluginInstance; 153 } 154 else if (defaults.debug) { 155 156 console.log('The ' + methodName + ' method of the ' + pluginName 157 + ' plugin conflicts with another plugin or native methods'); 158 } 159 } 160 }); 161 162 obj[pluginName] = pluginInstance; 163 } 164 165 return this; 166 }, 167 168 /** 169 * For mockup in Node env if need be, for testing purposes 170 * 171 * @return {core} 172 * @private 173 */ 174 __setWindow: function (window) { 175 env.window = window; 176 return this; 177 }, 178 179 /** 180 * Returns a ruler, a tool to help measure the size of a tooltip under 181 * various settings. Meant for plugins 182 * 183 * @see Ruler 184 * @return {object} A Ruler instance 185 * @protected 186 */ 187 _getRuler: function ($tooltip) { 188 return new Ruler($tooltip); 189 }, 190 191 /** 192 * For internal use by plugins, if needed 193 * 194 * @return {core} 195 * @protected 196 */ 197 _off: function () { 198 this.__$emitterPrivate.off.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments)); 199 return this; 200 }, 201 202 /** 203 * For internal use by plugins, if needed 204 * 205 * @return {core} 206 * @protected 207 */ 208 _on: function () { 209 this.__$emitterPrivate.on.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments)); 210 return this; 211 }, 212 213 /** 214 * For internal use by plugins, if needed 215 * 216 * @return {core} 217 * @protected 218 */ 219 _one: function () { 220 this.__$emitterPrivate.one.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments)); 221 return this; 222 }, 223 224 /** 225 * Returns (getter) or adds (setter) a plugin 226 * 227 * @param {string|object} plugin Provide a string (in the full form 228 * "namespace.name") to use as as getter, an object to use as a setter 229 * @return {object|core} 230 * @protected 231 */ 232 _plugin: function (plugin) { 233 234 var self = this; 235 236 // getter 237 if (typeof plugin == 'string') { 238 239 var pluginName = plugin, 240 p = null; 241 242 // if the namespace is provided, it's easy to search 243 if (pluginName.indexOf('.') > 0) { 244 p = self.__plugins[pluginName]; 245 } 246 // otherwise, return the first name that matches 247 else { 248 $.each(self.__plugins, function (i, plugin) { 249 250 if (plugin.name.substring(plugin.name.length - pluginName.length - 1) == '.' + pluginName) { 251 p = plugin; 252 return false; 253 } 254 }); 255 } 256 257 return p; 258 } 259 // setter 260 else { 261 262 // force namespaces 263 if (plugin.name.indexOf('.') < 0) { 264 throw new Error('Plugins must be namespaced'); 265 } 266 267 self.__plugins[plugin.name] = plugin; 268 269 // if the plugin has core features 270 if (plugin.core) { 271 272 // bridge non-private methods onto the core to allow new core methods 273 self.__bridge(plugin.core, self, plugin.name); 274 } 275 276 return this; 277 } 278 }, 279 280 /** 281 * Trigger events on the core emitters 282 * 283 * @returns {core} 284 * @protected 285 */ 286 _trigger: function () { 287 288 var args = Array.prototype.slice.apply(arguments); 289 290 if (typeof args[0] == 'string') { 291 args[0] = { type: args[0] }; 292 } 293 294 // note: the order of emitters matters 295 this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate, args); 296 this.__$emitterPublic.trigger.apply(this.__$emitterPublic, args); 297 298 return this; 299 }, 300 301 /** 302 * Returns instances of all tooltips in the page or an a given element 303 * 304 * @param {string|HTML object collection} selector optional Use this 305 * parameter to restrict the set of objects that will be inspected 306 * for the retrieval of instances. By default, all instances in the 307 * page are returned. 308 * @return {array} An array of instance objects 309 * @public 310 */ 311 instances: function (selector) { 312 313 var instances = [], 314 sel = selector || '.tooltipstered'; 315 316 $(sel).each(function () { 317 318 var $this = $(this), 319 ns = $this.data('tooltipster-ns'); 320 321 if (ns) { 322 323 $.each(ns, function (i, namespace) { 324 instances.push($this.data(namespace)); 325 }); 326 } 327 }); 328 329 return instances; 330 }, 331 332 /** 333 * Returns the Tooltipster objects generated by the last initializing call 334 * 335 * @return {array} An array of instance objects 336 * @public 337 */ 338 instancesLatest: function () { 339 return this.__instancesLatestArr; 340 }, 341 342 /** 343 * For public use only, not to be used by plugins (use ::_off() instead) 344 * 345 * @return {core} 346 * @public 347 */ 348 off: function () { 349 this.__$emitterPublic.off.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); 350 return this; 351 }, 352 353 /** 354 * For public use only, not to be used by plugins (use ::_on() instead) 355 * 356 * @return {core} 357 * @public 358 */ 359 on: function () { 360 this.__$emitterPublic.on.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); 361 return this; 362 }, 363 364 /** 365 * For public use only, not to be used by plugins (use ::_one() instead) 366 * 367 * @return {core} 368 * @public 369 */ 370 one: function () { 371 this.__$emitterPublic.one.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); 372 return this; 373 }, 374 375 /** 376 * Returns all HTML elements which have one or more tooltips 377 * 378 * @param {string} selector optional Use this to restrict the results 379 * to the descendants of an element 380 * @return {array} An array of HTML elements 381 * @public 382 */ 383 origins: function (selector) { 384 385 var sel = selector ? 386 selector + ' ' : 387 ''; 388 389 return $(sel + '.tooltipstered').toArray(); 390 }, 391 392 /** 393 * Change default options for all future instances 394 * 395 * @param {object} d The options that should be made defaults 396 * @return {core} 397 * @public 398 */ 399 setDefaults: function (d) { 400 $.extend(defaults, d); 401 return this; 402 }, 403 404 /** 405 * For users to trigger their handlers on the public emitter 406 * 407 * @returns {core} 408 * @public 409 */ 410 triggerHandler: function () { 411 this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); 412 return this; 413 } 414 }; 415 416 // $.tooltipster will be used to call core methods 417 $.tooltipster = new core(); 418 419 // the Tooltipster instance class (mind the capital T) 420 $.Tooltipster = function (element, options) { 421 422 // list of instance variables 423 424 // stack of custom callbacks provided as parameters to API methods 425 this.__callbacks = { 426 close: [], 427 open: [] 428 }; 429 // the schedule time of DOM removal 430 this.__closingTime; 431 // this will be the user content shown in the tooltip. A capital "C" is used 432 // because there is also a method called content() 433 this.__Content; 434 // for the size tracker 435 this.__contentBcr; 436 // to disable the tooltip after destruction 437 this.__destroyed = false; 438 // we can't emit directly on the instance because if a method with the same 439 // name as the event exists, it will be called by jQuery. Se we use a plain 440 // object as emitter. This emitter is for internal use by plugins, 441 // if needed. 442 this.__$emitterPrivate = $({}); 443 // this emitter is for the user to listen to events without risking to mess 444 // with our internal listeners 445 this.__$emitterPublic = $({}); 446 this.__enabled = true; 447 // the reference to the gc interval 448 this.__garbageCollector; 449 // various position and size data recomputed before each repositioning 450 this.__Geometry; 451 // the tooltip position, saved after each repositioning by a plugin 452 this.__lastPosition; 453 // a unique namespace per instance 454 this.__namespace = 'tooltipster-' + Math.round(Math.random() * 1000000); 455 this.__options; 456 // will be used to support origins in scrollable areas 457 this.__$originParents; 458 this.__pointerIsOverOrigin = false; 459 // to remove themes if needed 460 this.__previousThemes = []; 461 // the state can be either: appearing, stable, disappearing, closed 462 this.__state = 'closed'; 463 // timeout references 464 this.__timeouts = { 465 close: [], 466 open: null 467 }; 468 // store touch events to be able to detect emulated mouse events 469 this.__touchEvents = []; 470 // the reference to the tracker interval 471 this.__tracker = null; 472 // the element to which this tooltip is associated 473 this._$origin; 474 // this will be the tooltip element (jQuery wrapped HTML element). 475 // It's the job of a plugin to create it and append it to the DOM 476 this._$tooltip; 477 478 // launch 479 this.__init(element, options); 480 }; 481 482 $.Tooltipster.prototype = { 483 484 /** 485 * @param origin 486 * @param options 487 * @private 488 */ 489 __init: function (origin, options) { 490 491 var self = this; 492 493 self._$origin = $(origin); 494 self.__options = $.extend(true, {}, defaults, options); 495 496 // some options may need to be reformatted 497 self.__optionsFormat(); 498 499 // don't run on old IE if asked no to 500 if (!env.IE 501 || env.IE >= self.__options.IEmin 502 ) { 503 504 // note: the content is null (empty) by default and can stay that 505 // way if the plugin remains initialized but not fed any content. The 506 // tooltip will just not appear. 507 508 // let's save the initial value of the title attribute for later 509 // restoration if need be. 510 var initialTitle = null; 511 512 // it will already have been saved in case of multiple tooltips 513 if (self._$origin.data('tooltipster-initialTitle') === undefined) { 514 515 initialTitle = self._$origin.attr('title'); 516 517 // we do not want initialTitle to be "undefined" because 518 // of how jQuery's .data() method works 519 if (initialTitle === undefined) initialTitle = null; 520 521 self._$origin.data('tooltipster-initialTitle', initialTitle); 522 } 523 524 // If content is provided in the options, it has precedence over the 525 // title attribute. 526 // Note: an empty string is considered content, only 'null' represents 527 // the absence of content. 528 // Also, an existing title="" attribute will result in an empty string 529 // content 530 if (self.__options.content !== null) { 531 self.__contentSet(self.__options.content); 532 } 533 else { 534 535 var selector = self._$origin.attr('data-tooltip-content'), 536 $el; 537 538 if (selector) { 539 $el = $(selector); 540 } 541 542 if ($el && $el[0]) { 543 self.__contentSet($el.first()); 544 } 545 else { 546 self.__contentSet(initialTitle); 547 } 548 } 549 550 self._$origin 551 // strip the title off of the element to prevent the default tooltips 552 // from popping up 553 .removeAttr('title') 554 // to be able to find all instances on the page later (upon window 555 // events in particular) 556 .addClass('tooltipstered'); 557 558 // set listeners on the origin 559 self.__prepareOrigin(); 560 561 // set the garbage collector 562 self.__prepareGC(); 563 564 // init plugins 565 $.each(self.__options.plugins, function (i, pluginName) { 566 self._plug(pluginName); 567 }); 568 569 // to detect swiping 570 if (env.hasTouchCapability) { 571 $(env.window.document.body).on('touchmove.' + self.__namespace + '-triggerOpen', function (event) { 572 self._touchRecordEvent(event); 573 }); 574 } 575 576 self 577 // prepare the tooltip when it gets created. This event must 578 // be fired by a plugin 579 ._on('created', function () { 580 self.__prepareTooltip(); 581 }) 582 // save position information when it's sent by a plugin 583 ._on('repositioned', function (e) { 584 self.__lastPosition = e.position; 585 }); 586 } 587 else { 588 self.__options.disabled = true; 589 } 590 }, 591 592 /** 593 * Insert the content into the appropriate HTML element of the tooltip 594 * 595 * @returns {self} 596 * @private 597 */ 598 __contentInsert: function () { 599 600 var self = this, 601 $el = self._$tooltip.find('.tooltipster-content'), 602 formattedContent = self.__Content, 603 format = function (content) { 604 formattedContent = content; 605 }; 606 607 self._trigger({ 608 type: 'format', 609 content: self.__Content, 610 format: format 611 }); 612 613 if (self.__options.functionFormat) { 614 615 formattedContent = self.__options.functionFormat.call( 616 self, 617 self, 618 { origin: self._$origin[0] }, 619 self.__Content 620 ); 621 } 622 623 if (typeof formattedContent === 'string' && !self.__options.contentAsHTML) { 624 $el.text(formattedContent); 625 } 626 else { 627 $el 628 .empty() 629 .append(formattedContent); 630 } 631 632 return self; 633 }, 634 635 /** 636 * Save the content, cloning it beforehand if need be 637 * 638 * @param content 639 * @returns {self} 640 * @private 641 */ 642 __contentSet: function (content) { 643 644 // clone if asked. Cloning the object makes sure that each instance has its 645 // own version of the content (in case a same object were provided for several 646 // instances) 647 // reminder: typeof null === object 648 if (content instanceof $ && this.__options.contentCloning) { 649 content = content.clone(true); 650 } 651 652 this.__Content = content; 653 654 this._trigger({ 655 type: 'updated', 656 content: content 657 }); 658 659 return this; 660 }, 661 662 /** 663 * Error message about a method call made after destruction 664 * 665 * @private 666 */ 667 __destroyError: function () { 668 throw new Error('This tooltip has been destroyed and cannot execute your method call.'); 669 }, 670 671 /** 672 * Gather all information about dimensions and available space, 673 * called before every repositioning 674 * 675 * @private 676 * @returns {object} 677 */ 678 __geometry: function () { 679 680 var self = this, 681 $target = self._$origin, 682 originIsArea = self._$origin.is('area'); 683 684 // if this._$origin is a map area, the target we'll need 685 // the dimensions of is actually the image using the map, 686 // not the area itself 687 if (originIsArea) { 688 689 var mapName = self._$origin.parent().attr('name'); 690 691 $target = $('img[usemap="#' + mapName + '"]'); 692 } 693 694 var bcr = $target[0].getBoundingClientRect(), 695 $document = $(env.window.document), 696 $window = $(env.window), 697 $parent = $target, 698 // some useful properties of important elements 699 geo = { 700 // available space for the tooltip, see down below 701 available: { 702 document: null, 703 window: null 704 }, 705 document: { 706 size: { 707 height: $document.height(), 708 width: $document.width() 709 } 710 }, 711 window: { 712 scroll: { 713 // the second ones are for IE compatibility 714 left: env.window.scrollX || env.window.document.documentElement.scrollLeft, 715 top: env.window.scrollY || env.window.document.documentElement.scrollTop 716 }, 717 size: { 718 height: $window.height(), 719 width: $window.width() 720 } 721 }, 722 origin: { 723 // the origin has a fixed lineage if itself or one of its 724 // ancestors has a fixed position 725 fixedLineage: false, 726 // relative to the document 727 offset: {}, 728 size: { 729 height: bcr.bottom - bcr.top, 730 width: bcr.right - bcr.left 731 }, 732 usemapImage: originIsArea ? $target[0] : null, 733 // relative to the window 734 windowOffset: { 735 bottom: bcr.bottom, 736 left: bcr.left, 737 right: bcr.right, 738 top: bcr.top 739 } 740 } 741 }, 742 geoFixed = false; 743 744 // if the element is a map area, some properties may need 745 // to be recalculated 746 if (originIsArea) { 747 748 var shape = self._$origin.attr('shape'), 749 coords = self._$origin.attr('coords'); 750 751 if (coords) { 752 753 coords = coords.split(','); 754 755 $.map(coords, function (val, i) { 756 coords[i] = parseInt(val); 757 }); 758 } 759 760 // if the image itself is the area, nothing more to do 761 if (shape != 'default') { 762 763 switch (shape) { 764 765 case 'circle': 766 767 var circleCenterLeft = coords[0], 768 circleCenterTop = coords[1], 769 circleRadius = coords[2], 770 areaTopOffset = circleCenterTop - circleRadius, 771 areaLeftOffset = circleCenterLeft - circleRadius; 772 773 geo.origin.size.height = circleRadius * 2; 774 geo.origin.size.width = geo.origin.size.height; 775 776 geo.origin.windowOffset.left += areaLeftOffset; 777 geo.origin.windowOffset.top += areaTopOffset; 778 779 break; 780 781 case 'rect': 782 783 var areaLeft = coords[0], 784 areaTop = coords[1], 785 areaRight = coords[2], 786 areaBottom = coords[3]; 787 788 geo.origin.size.height = areaBottom - areaTop; 789 geo.origin.size.width = areaRight - areaLeft; 790 791 geo.origin.windowOffset.left += areaLeft; 792 geo.origin.windowOffset.top += areaTop; 793 794 break; 795 796 case 'poly': 797 798 var areaSmallestX = 0, 799 areaSmallestY = 0, 800 areaGreatestX = 0, 801 areaGreatestY = 0, 802 arrayAlternate = 'even'; 803 804 for (var i = 0; i < coords.length; i++) { 805 806 var areaNumber = coords[i]; 807 808 if (arrayAlternate == 'even') { 809 810 if (areaNumber > areaGreatestX) { 811 812 areaGreatestX = areaNumber; 813 814 if (i === 0) { 815 areaSmallestX = areaGreatestX; 816 } 817 } 818 819 if (areaNumber < areaSmallestX) { 820 areaSmallestX = areaNumber; 821 } 822 823 arrayAlternate = 'odd'; 824 } 825 else { 826 if (areaNumber > areaGreatestY) { 827 828 areaGreatestY = areaNumber; 829 830 if (i == 1) { 831 areaSmallestY = areaGreatestY; 832 } 833 } 834 835 if (areaNumber < areaSmallestY) { 836 areaSmallestY = areaNumber; 837 } 838 839 arrayAlternate = 'even'; 840 } 841 } 842 843 geo.origin.size.height = areaGreatestY - areaSmallestY; 844 geo.origin.size.width = areaGreatestX - areaSmallestX; 845 846 geo.origin.windowOffset.left += areaSmallestX; 847 geo.origin.windowOffset.top += areaSmallestY; 848 849 break; 850 } 851 } 852 } 853 854 // user callback through an event 855 var edit = function (r) { 856 geo.origin.size.height = r.height, 857 geo.origin.windowOffset.left = r.left, 858 geo.origin.windowOffset.top = r.top, 859 geo.origin.size.width = r.width 860 }; 861 862 self._trigger({ 863 type: 'geometry', 864 edit: edit, 865 geometry: { 866 height: geo.origin.size.height, 867 left: geo.origin.windowOffset.left, 868 top: geo.origin.windowOffset.top, 869 width: geo.origin.size.width 870 } 871 }); 872 873 // calculate the remaining properties with what we got 874 875 geo.origin.windowOffset.right = geo.origin.windowOffset.left + geo.origin.size.width; 876 geo.origin.windowOffset.bottom = geo.origin.windowOffset.top + geo.origin.size.height; 877 878 geo.origin.offset.left = geo.origin.windowOffset.left + geo.window.scroll.left; 879 geo.origin.offset.top = geo.origin.windowOffset.top + geo.window.scroll.top; 880 geo.origin.offset.bottom = geo.origin.offset.top + geo.origin.size.height; 881 geo.origin.offset.right = geo.origin.offset.left + geo.origin.size.width; 882 883 // the space that is available to display the tooltip relatively to the document 884 geo.available.document = { 885 bottom: { 886 height: geo.document.size.height - geo.origin.offset.bottom, 887 width: geo.document.size.width 888 }, 889 left: { 890 height: geo.document.size.height, 891 width: geo.origin.offset.left 892 }, 893 right: { 894 height: geo.document.size.height, 895 width: geo.document.size.width - geo.origin.offset.right 896 }, 897 top: { 898 height: geo.origin.offset.top, 899 width: geo.document.size.width 900 } 901 }; 902 903 // the space that is available to display the tooltip relatively to the viewport 904 // (the resulting values may be negative if the origin overflows the viewport) 905 geo.available.window = { 906 bottom: { 907 // the inner max is here to make sure the available height is no bigger 908 // than the viewport height (when the origin is off screen at the top). 909 // The outer max just makes sure that the height is not negative (when 910 // the origin overflows at the bottom). 911 height: Math.max(geo.window.size.height - Math.max(geo.origin.windowOffset.bottom, 0), 0), 912 width: geo.window.size.width 913 }, 914 left: { 915 height: geo.window.size.height, 916 width: Math.max(geo.origin.windowOffset.left, 0) 917 }, 918 right: { 919 height: geo.window.size.height, 920 width: Math.max(geo.window.size.width - Math.max(geo.origin.windowOffset.right, 0), 0) 921 }, 922 top: { 923 height: Math.max(geo.origin.windowOffset.top, 0), 924 width: geo.window.size.width 925 } 926 }; 927 928 while ($parent[0].tagName.toLowerCase() != 'html') { 929 930 if ($parent.css('position') == 'fixed') { 931 geo.origin.fixedLineage = true; 932 break; 933 } 934 935 $parent = $parent.parent(); 936 } 937 938 return geo; 939 }, 940 941 /** 942 * Some options may need to be formated before being used 943 * 944 * @returns {self} 945 * @private 946 */ 947 __optionsFormat: function () { 948 949 if (typeof this.__options.animationDuration == 'number') { 950 this.__options.animationDuration = [this.__options.animationDuration, this.__options.animationDuration]; 951 } 952 953 if (typeof this.__options.delay == 'number') { 954 this.__options.delay = [this.__options.delay, this.__options.delay]; 955 } 956 957 if (typeof this.__options.delayTouch == 'number') { 958 this.__options.delayTouch = [this.__options.delayTouch, this.__options.delayTouch]; 959 } 960 961 if (typeof this.__options.theme == 'string') { 962 this.__options.theme = [this.__options.theme]; 963 } 964 965 // determine the future parent 966 if (this.__options.parent === null) { 967 this.__options.parent = $(env.window.document.body); 968 969 } 970 else if (typeof this.__options.parent == 'string') { 971 this.__options.parent = $(this.__options.parent); 972 } 973 974 if (this.__options.trigger == 'hover') { 975 976 this.__options.triggerOpen = { 977 mouseenter: true, 978 touchstart: true 979 }; 980 981 this.__options.triggerClose = { 982 mouseleave: true, 983 originClick: true, 984 touchleave: true 985 }; 986 } 987 else if (this.__options.trigger == 'click') { 988 989 this.__options.triggerOpen = { 990 click: true, 991 tap: true 992 }; 993 994 this.__options.triggerClose = { 995 click: true, 996 tap: true 997 }; 998 } 999 1000 // for the plugins 1001 this._trigger('options'); 1002 1003 return this; 1004 }, 1005 1006 /** 1007 * Schedules or cancels the garbage collector task 1008 * 1009 * @returns {self} 1010 * @private 1011 */ 1012 __prepareGC: function () { 1013 1014 var self = this; 1015 1016 // in case the selfDestruction option has been changed by a method call 1017 if (self.__options.selfDestruction) { 1018 1019 // the GC task 1020 self.__garbageCollector = setInterval(function () { 1021 1022 var now = new Date().getTime(); 1023 1024 // forget the old events 1025 self.__touchEvents = $.grep(self.__touchEvents, function (event, i) { 1026 // 1 minute 1027 return now - event.time > 60000; 1028 }); 1029 1030 // auto-destruct if the origin is gone 1031 if (!bodyContains(self._$origin)) { 1032 1033 self.close(function () { 1034 self.destroy(); 1035 }); 1036 } 1037 }, 20000); 1038 } 1039 else { 1040 clearInterval(self.__garbageCollector); 1041 } 1042 1043 return self; 1044 }, 1045 1046 /** 1047 * Sets listeners on the origin if the open triggers require them. 1048 * Unlike the listeners set at opening time, these ones 1049 * remain even when the tooltip is closed. It has been made a 1050 * separate method so it can be called when the triggers are 1051 * changed in the options. Closing is handled in _open() 1052 * because of the bindings that may be needed on the tooltip 1053 * itself 1054 * 1055 * @returns {self} 1056 * @private 1057 */ 1058 __prepareOrigin: function () { 1059 1060 var self = this; 1061 1062 // in case we're resetting the triggers 1063 self._$origin.off('.' + self.__namespace + '-triggerOpen'); 1064 1065 // if the device is touch capable, even if only mouse triggers 1066 // are asked, we need to listen to touch events to know if the mouse 1067 // events are actually emulated (so we can ignore them) 1068 if (env.hasTouchCapability) { 1069 1070 self._$origin.on( 1071 'touchstart.' + self.__namespace + '-triggerOpen ' + 1072 'touchend.' + self.__namespace + '-triggerOpen ' + 1073 'touchcancel.' + self.__namespace + '-triggerOpen', 1074 function (event) { 1075 self._touchRecordEvent(event); 1076 } 1077 ); 1078 } 1079 1080 // mouse click and touch tap work the same way 1081 if (self.__options.triggerOpen.click 1082 || (self.__options.triggerOpen.tap && env.hasTouchCapability) 1083 ) { 1084 1085 var eventNames = ''; 1086 if (self.__options.triggerOpen.click) { 1087 eventNames += 'click.' + self.__namespace + '-triggerOpen '; 1088 } 1089 if (self.__options.triggerOpen.tap && env.hasTouchCapability) { 1090 eventNames += 'touchend.' + self.__namespace + '-triggerOpen'; 1091 } 1092 1093 self._$origin.on(eventNames, function (event) { 1094 if (self._touchIsMeaningfulEvent(event)) { 1095 self._open(event); 1096 } 1097 }); 1098 } 1099 1100 // mouseenter and touch start work the same way 1101 if (self.__options.triggerOpen.mouseenter 1102 || (self.__options.triggerOpen.touchstart && env.hasTouchCapability) 1103 ) { 1104 1105 var eventNames = ''; 1106 if (self.__options.triggerOpen.mouseenter) { 1107 eventNames += 'mouseenter.' + self.__namespace + '-triggerOpen '; 1108 } 1109 if (self.__options.triggerOpen.touchstart && env.hasTouchCapability) { 1110 eventNames += 'touchstart.' + self.__namespace + '-triggerOpen'; 1111 } 1112 1113 self._$origin.on(eventNames, function (event) { 1114 if (self._touchIsTouchEvent(event) 1115 || !self._touchIsEmulatedEvent(event) 1116 ) { 1117 self.__pointerIsOverOrigin = true; 1118 self._openShortly(event); 1119 } 1120 }); 1121 } 1122 1123 // info for the mouseleave/touchleave close triggers when they use a delay 1124 if (self.__options.triggerClose.mouseleave 1125 || (self.__options.triggerClose.touchleave && env.hasTouchCapability) 1126 ) { 1127 1128 var eventNames = ''; 1129 if (self.__options.triggerClose.mouseleave) { 1130 eventNames += 'mouseleave.' + self.__namespace + '-triggerOpen '; 1131 } 1132 if (self.__options.triggerClose.touchleave && env.hasTouchCapability) { 1133 eventNames += 'touchend.' + self.__namespace + '-triggerOpen touchcancel.' + self.__namespace + '-triggerOpen'; 1134 } 1135 1136 self._$origin.on(eventNames, function (event) { 1137 1138 if (self._touchIsMeaningfulEvent(event)) { 1139 self.__pointerIsOverOrigin = false; 1140 } 1141 }); 1142 } 1143 1144 return self; 1145 }, 1146 1147 /** 1148 * Do the things that need to be done only once after the tooltip 1149 * HTML element it has been created. It has been made a separate 1150 * method so it can be called when options are changed. Remember 1151 * that the tooltip may actually exist in the DOM before it is 1152 * opened, and present after it has been closed: it's the display 1153 * plugin that takes care of handling it. 1154 * 1155 * @returns {self} 1156 * @private 1157 */ 1158 __prepareTooltip: function () { 1159 1160 var self = this, 1161 p = self.__options.interactive ? 'auto' : ''; 1162 1163 // this will be useful to know quickly if the tooltip is in 1164 // the DOM or not 1165 self._$tooltip 1166 .attr('id', self.__namespace) 1167 .css({ 1168 // pointer events 1169 'pointer-events': p, 1170 zIndex: self.__options.zIndex 1171 }); 1172 1173 // themes 1174 // remove the old ones and add the new ones 1175 $.each(self.__previousThemes, function (i, theme) { 1176 self._$tooltip.removeClass(theme); 1177 }); 1178 $.each(self.__options.theme, function (i, theme) { 1179 self._$tooltip.addClass(theme); 1180 }); 1181 1182 self.__previousThemes = $.merge([], self.__options.theme); 1183 1184 return self; 1185 }, 1186 1187 /** 1188 * Handles the scroll on any of the parents of the origin (when the 1189 * tooltip is open) 1190 * 1191 * @param {object} event 1192 * @returns {self} 1193 * @private 1194 */ 1195 __scrollHandler: function (event) { 1196 1197 var self = this; 1198 1199 if (self.__options.triggerClose.scroll) { 1200 self._close(event); 1201 } 1202 else { 1203 1204 // if the origin or tooltip have been removed: do nothing, the tracker will 1205 // take care of it later 1206 if (bodyContains(self._$origin) && bodyContains(self._$tooltip)) { 1207 1208 var geo = null; 1209 1210 // if the scroll happened on the window 1211 if (event.target === env.window.document) { 1212 1213 // if the origin has a fixed lineage, window scroll will have no 1214 // effect on its position nor on the position of the tooltip 1215 if (!self.__Geometry.origin.fixedLineage) { 1216 1217 // we don't need to do anything unless repositionOnScroll is true 1218 // because the tooltip will already have moved with the window 1219 // (and of course with the origin) 1220 if (self.__options.repositionOnScroll) { 1221 self.reposition(event); 1222 } 1223 } 1224 } 1225 // if the scroll happened on another parent of the tooltip, it means 1226 // that it's in a scrollable area and now needs to have its position 1227 // adjusted or recomputed, depending ont the repositionOnScroll 1228 // option. Also, if the origin is partly hidden due to a parent that 1229 // hides its overflow, we'll just hide (not close) the tooltip. 1230 else { 1231 1232 geo = self.__geometry(); 1233 1234 var overflows = false; 1235 1236 // a fixed position origin is not affected by the overflow hiding 1237 // of a parent 1238 if (self._$origin.css('position') != 'fixed') { 1239 1240 self.__$originParents.each(function (i, el) { 1241 1242 var $el = $(el), 1243 overflowX = $el.css('overflow-x'), 1244 overflowY = $el.css('overflow-y'); 1245 1246 if (overflowX != 'visible' || overflowY != 'visible') { 1247 1248 var bcr = el.getBoundingClientRect(); 1249 1250 if (overflowX != 'visible') { 1251 1252 if (geo.origin.windowOffset.left < bcr.left 1253 || geo.origin.windowOffset.right > bcr.right 1254 ) { 1255 overflows = true; 1256 return false; 1257 } 1258 } 1259 1260 if (overflowY != 'visible') { 1261 1262 if (geo.origin.windowOffset.top < bcr.top 1263 || geo.origin.windowOffset.bottom > bcr.bottom 1264 ) { 1265 overflows = true; 1266 return false; 1267 } 1268 } 1269 } 1270 1271 // no need to go further if fixed, for the same reason as above 1272 if ($el.css('position') == 'fixed') { 1273 return false; 1274 } 1275 }); 1276 } 1277 1278 if (overflows) { 1279 self._$tooltip.css('visibility', 'hidden'); 1280 } 1281 else { 1282 1283 self._$tooltip.css('visibility', 'visible'); 1284 1285 // reposition 1286 if (self.__options.repositionOnScroll) { 1287 self.reposition(event); 1288 } 1289 // or just adjust offset 1290 else { 1291 1292 // we have to use offset and not windowOffset because this way, 1293 // only the scroll distance of the scrollable areas are taken into 1294 // account (the scrolltop value of the main window must be 1295 // ignored since the tooltip already moves with it) 1296 var offsetLeft = geo.origin.offset.left - self.__Geometry.origin.offset.left, 1297 offsetTop = geo.origin.offset.top - self.__Geometry.origin.offset.top; 1298 1299 // add the offset to the position initially computed by the display plugin 1300 self._$tooltip.css({ 1301 left: self.__lastPosition.coord.left + offsetLeft, 1302 top: self.__lastPosition.coord.top + offsetTop 1303 }); 1304 } 1305 } 1306 } 1307 1308 self._trigger({ 1309 type: 'scroll', 1310 event: event, 1311 geo: geo 1312 }); 1313 } 1314 } 1315 1316 return self; 1317 }, 1318 1319 /** 1320 * Changes the state of the tooltip 1321 * 1322 * @param {string} state 1323 * @returns {self} 1324 * @private 1325 */ 1326 __stateSet: function (state) { 1327 1328 this.__state = state; 1329 1330 this._trigger({ 1331 type: 'state', 1332 state: state 1333 }); 1334 1335 return this; 1336 }, 1337 1338 /** 1339 * Clear appearance timeouts 1340 * 1341 * @returns {self} 1342 * @private 1343 */ 1344 __timeoutsClear: function () { 1345 1346 // there is only one possible open timeout: the delayed opening 1347 // when the mouseenter/touchstart open triggers are used 1348 clearTimeout(this.__timeouts.open); 1349 this.__timeouts.open = null; 1350 1351 // ... but several close timeouts: the delayed closing when the 1352 // mouseleave close trigger is used and the timer option 1353 $.each(this.__timeouts.close, function (i, timeout) { 1354 clearTimeout(timeout); 1355 }); 1356 this.__timeouts.close = []; 1357 1358 return this; 1359 }, 1360 1361 /** 1362 * Start the tracker that will make checks at regular intervals 1363 * 1364 * @returns {self} 1365 * @private 1366 */ 1367 __trackerStart: function () { 1368 1369 var self = this, 1370 $content = self._$tooltip.find('.tooltipster-content'); 1371 1372 // get the initial content size 1373 if (self.__options.trackTooltip) { 1374 self.__contentBcr = $content[0].getBoundingClientRect(); 1375 } 1376 1377 self.__tracker = setInterval(function () { 1378 1379 // if the origin or tooltip elements have been removed. 1380 // Note: we could destroy the instance now if the origin has 1381 // been removed but we'll leave that task to our garbage collector 1382 if (!bodyContains(self._$origin) || !bodyContains(self._$tooltip)) { 1383 self._close(); 1384 } 1385 // if everything is alright 1386 else { 1387 1388 // compare the former and current positions of the origin to reposition 1389 // the tooltip if need be 1390 if (self.__options.trackOrigin) { 1391 1392 var g = self.__geometry(), 1393 identical = false; 1394 1395 // compare size first (a change requires repositioning too) 1396 if (areEqual(g.origin.size, self.__Geometry.origin.size)) { 1397 1398 // for elements that have a fixed lineage (see __geometry()), we track the 1399 // top and left properties (relative to window) 1400 if (self.__Geometry.origin.fixedLineage) { 1401 if (areEqual(g.origin.windowOffset, self.__Geometry.origin.windowOffset)) { 1402 identical = true; 1403 } 1404 } 1405 // otherwise, track total offset (relative to document) 1406 else { 1407 if (areEqual(g.origin.offset, self.__Geometry.origin.offset)) { 1408 identical = true; 1409 } 1410 } 1411 } 1412 1413 if (!identical) { 1414 1415 // close the tooltip when using the mouseleave close trigger 1416 // (see https://github.com/iamceege/tooltipster/pull/253) 1417 if (self.__options.triggerClose.mouseleave) { 1418 self._close(); 1419 } 1420 else { 1421 self.reposition(); 1422 } 1423 } 1424 } 1425 1426 if (self.__options.trackTooltip) { 1427 1428 var currentBcr = $content[0].getBoundingClientRect(); 1429 1430 if (currentBcr.height !== self.__contentBcr.height 1431 || currentBcr.width !== self.__contentBcr.width 1432 ) { 1433 self.reposition(); 1434 self.__contentBcr = currentBcr; 1435 } 1436 } 1437 } 1438 }, self.__options.trackerInterval); 1439 1440 return self; 1441 }, 1442 1443 /** 1444 * Closes the tooltip (after the closing delay) 1445 * 1446 * @param event 1447 * @param callback 1448 * @param force Set to true to override a potential refusal of the user's function 1449 * @returns {self} 1450 * @protected 1451 */ 1452 _close: function (event, callback, force) { 1453 var self = this, 1454 ok = true; 1455 1456 self._trigger({ 1457 type: 'close', 1458 event: event, 1459 stop: function () { 1460 ok = false; 1461 } 1462 }); 1463 1464 // a destroying tooltip (force == true) may not refuse to close 1465 if (ok || force) { 1466 1467 // save the method custom callback and cancel any open method custom callbacks 1468 if (callback) self.__callbacks.close.push(callback); 1469 self.__callbacks.open = []; 1470 1471 // clear open/close timeouts 1472 self.__timeoutsClear(); 1473 1474 var finishCallbacks = function () { 1475 1476 // trigger any close method custom callbacks and reset them 1477 $.each(self.__callbacks.close, function (i, c) { 1478 c.call(self, self, { 1479 event: event, 1480 origin: self._$origin[0] 1481 }); 1482 }); 1483 1484 self.__callbacks.close = []; 1485 }; 1486 1487 if (self.__state != 'closed') { 1488 1489 var necessary = true, 1490 d = new Date(), 1491 now = d.getTime(), 1492 newClosingTime = now + self.__options.animationDuration[1]; 1493 1494 // the tooltip may already already be disappearing, but if a new 1495 // call to close() is made after the animationDuration was changed 1496 // to 0 (for example), we ought to actually close it sooner than 1497 // previously scheduled. In that case it should be noted that the 1498 // browser will not adapt the animation duration to the new 1499 // animationDuration that was set after the start of the closing 1500 // animation. 1501 // Note: the same thing could be considered at opening, but is not 1502 // really useful since the tooltip is actually opened immediately 1503 // upon a call to _open(). Since it would not make the opening 1504 // animation finish sooner, its sole impact would be to trigger the 1505 // state event and the open callbacks sooner than the actual end of 1506 // the opening animation, which is not great. 1507 if (self.__state == 'disappearing') { 1508 1509 if (newClosingTime > self.__closingTime 1510 // in case closing is actually overdue because the script 1511 // execution was suspended. See #679 1512 && self.__options.animationDuration[1] > 0 1513 ) { 1514 necessary = false; 1515 } 1516 } 1517 1518 if (necessary) { 1519 1520 self.__closingTime = newClosingTime; 1521 1522 if (self.__state != 'disappearing') { 1523 self.__stateSet('disappearing'); 1524 } 1525 1526 var finish = function () { 1527 1528 // stop the tracker 1529 clearInterval(self.__tracker); 1530 1531 // a "beforeClose" option has been asked several times but would 1532 // probably useless since the content element is still accessible 1533 // via ::content(), and because people can always use listeners 1534 // inside their content to track what's going on. For the sake of 1535 // simplicity, this has been denied. Bur for the rare people who 1536 // really need the option (for old browsers or for the case where 1537 // detaching the content is actually destructive, for file or 1538 // password inputs for example), this event will do the work. 1539 self._trigger({ 1540 type: 'closing', 1541 event: event 1542 }); 1543 1544 // unbind listeners which are no longer needed 1545 1546 self._$tooltip 1547 .off('.' + self.__namespace + '-triggerClose') 1548 .removeClass('tooltipster-dying'); 1549 1550 // orientationchange, scroll and resize listeners 1551 $(env.window).off('.' + self.__namespace + '-triggerClose'); 1552 1553 // scroll listeners 1554 self.__$originParents.each(function (i, el) { 1555 $(el).off('scroll.' + self.__namespace + '-triggerClose'); 1556 }); 1557 // clear the array to prevent memory leaks 1558 self.__$originParents = null; 1559 1560 $(env.window.document.body).off('.' + self.__namespace + '-triggerClose'); 1561 1562 self._$origin.off('.' + self.__namespace + '-triggerClose'); 1563 1564 self._off('dismissable'); 1565 1566 // a plugin that would like to remove the tooltip from the 1567 // DOM when closed should bind on this 1568 self.__stateSet('closed'); 1569 1570 // trigger event 1571 self._trigger({ 1572 type: 'after', 1573 event: event 1574 }); 1575 1576 // call our constructor custom callback function 1577 if (self.__options.functionAfter) { 1578 self.__options.functionAfter.call(self, self, { 1579 event: event, 1580 origin: self._$origin[0] 1581 }); 1582 } 1583 1584 // call our method custom callbacks functions 1585 finishCallbacks(); 1586 }; 1587 1588 if (env.hasTransitions) { 1589 1590 self._$tooltip.css({ 1591 '-moz-animation-duration': self.__options.animationDuration[1] + 'ms', 1592 '-ms-animation-duration': self.__options.animationDuration[1] + 'ms', 1593 '-o-animation-duration': self.__options.animationDuration[1] + 'ms', 1594 '-webkit-animation-duration': self.__options.animationDuration[1] + 'ms', 1595 'animation-duration': self.__options.animationDuration[1] + 'ms', 1596 'transition-duration': self.__options.animationDuration[1] + 'ms' 1597 }); 1598 1599 self._$tooltip 1600 // clear both potential open and close tasks 1601 .clearQueue() 1602 .removeClass('tooltipster-show') 1603 // for transitions only 1604 .addClass('tooltipster-dying'); 1605 1606 if (self.__options.animationDuration[1] > 0) { 1607 self._$tooltip.delay(self.__options.animationDuration[1]); 1608 } 1609 1610 self._$tooltip.queue(finish); 1611 } 1612 else { 1613 1614 self._$tooltip 1615 .stop() 1616 .fadeOut(self.__options.animationDuration[1], finish); 1617 } 1618 } 1619 } 1620 // if the tooltip is already closed, we still need to trigger 1621 // the method custom callbacks 1622 else { 1623 finishCallbacks(); 1624 } 1625 } 1626 1627 return self; 1628 }, 1629 1630 /** 1631 * For internal use by plugins, if needed 1632 * 1633 * @returns {self} 1634 * @protected 1635 */ 1636 _off: function () { 1637 this.__$emitterPrivate.off.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments)); 1638 return this; 1639 }, 1640 1641 /** 1642 * For internal use by plugins, if needed 1643 * 1644 * @returns {self} 1645 * @protected 1646 */ 1647 _on: function () { 1648 this.__$emitterPrivate.on.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments)); 1649 return this; 1650 }, 1651 1652 /** 1653 * For internal use by plugins, if needed 1654 * 1655 * @returns {self} 1656 * @protected 1657 */ 1658 _one: function () { 1659 this.__$emitterPrivate.one.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments)); 1660 return this; 1661 }, 1662 1663 /** 1664 * Opens the tooltip right away. 1665 * 1666 * @param event 1667 * @param callback Will be called when the opening animation is over 1668 * @returns {self} 1669 * @protected 1670 */ 1671 _open: function (event, callback) { 1672 1673 var self = this; 1674 1675 // if the destruction process has not begun and if this was not 1676 // triggered by an unwanted emulated click event 1677 if (!self.__destroying) { 1678 1679 // check that the origin is still in the DOM 1680 if (bodyContains(self._$origin) 1681 // if the tooltip is enabled 1682 && self.__enabled 1683 ) { 1684 1685 var ok = true; 1686 1687 // if the tooltip is not open yet, we need to call functionBefore. 1688 // otherwise we can jst go on 1689 if (self.__state == 'closed') { 1690 1691 // trigger an event. The event.stop function allows the callback 1692 // to prevent the opening of the tooltip 1693 self._trigger({ 1694 type: 'before', 1695 event: event, 1696 stop: function () { 1697 ok = false; 1698 } 1699 }); 1700 1701 if (ok && self.__options.functionBefore) { 1702 1703 // call our custom function before continuing 1704 ok = self.__options.functionBefore.call(self, self, { 1705 event: event, 1706 origin: self._$origin[0] 1707 }); 1708 } 1709 } 1710 1711 if (ok !== false) { 1712 1713 // if there is some content 1714 if (self.__Content !== null) { 1715 1716 // save the method callback and cancel close method callbacks 1717 if (callback) { 1718 self.__callbacks.open.push(callback); 1719 } 1720 self.__callbacks.close = []; 1721 1722 // get rid of any appearance timeouts 1723 self.__timeoutsClear(); 1724 1725 var extraTime, 1726 finish = function () { 1727 1728 if (self.__state != 'stable') { 1729 self.__stateSet('stable'); 1730 } 1731 1732 // trigger any open method custom callbacks and reset them 1733 $.each(self.__callbacks.open, function (i, c) { 1734 c.call(self, self, { 1735 origin: self._$origin[0], 1736 tooltip: self._$tooltip[0] 1737 }); 1738 }); 1739 1740 self.__callbacks.open = []; 1741 }; 1742 1743 // if the tooltip is already open 1744 if (self.__state !== 'closed') { 1745 1746 // the timer (if any) will start (or restart) right now 1747 extraTime = 0; 1748 1749 // if it was disappearing, cancel that 1750 if (self.__state === 'disappearing') { 1751 1752 self.__stateSet('appearing'); 1753 1754 if (env.hasTransitions) { 1755 1756 self._$tooltip 1757 .clearQueue() 1758 .removeClass('tooltipster-dying') 1759 .addClass('tooltipster-show'); 1760 1761 if (self.__options.animationDuration[0] > 0) { 1762 self._$tooltip.delay(self.__options.animationDuration[0]); 1763 } 1764 1765 self._$tooltip.queue(finish); 1766 } 1767 else { 1768 // in case the tooltip was currently fading out, bring it back 1769 // to life 1770 self._$tooltip 1771 .stop() 1772 .fadeIn(finish); 1773 } 1774 } 1775 // if the tooltip is already open, we still need to trigger the method 1776 // custom callback 1777 else if (self.__state == 'stable') { 1778 finish(); 1779 } 1780 } 1781 // if the tooltip isn't already open, open it 1782 else { 1783 1784 // a plugin must bind on this and store the tooltip in this._$tooltip 1785 self.__stateSet('appearing'); 1786 1787 // the timer (if any) will start when the tooltip has fully appeared 1788 // after its transition 1789 extraTime = self.__options.animationDuration[0]; 1790 1791 // insert the content inside the tooltip 1792 self.__contentInsert(); 1793 1794 // reposition the tooltip and attach to the DOM 1795 self.reposition(event, true); 1796 1797 // animate in the tooltip. If the display plugin wants no css 1798 // animations, it may override the animation option with a 1799 // dummy value that will produce no effect 1800 if (env.hasTransitions) { 1801 1802 // note: there seems to be an issue with start animations which 1803 // are randomly not played on fast devices in both Chrome and FF, 1804 // couldn't find a way to solve it yet. It seems that applying 1805 // the classes before appending to the DOM helps a little, but 1806 // it messes up some CSS transitions. The issue almost never 1807 // happens when delay[0]==0 though 1808 self._$tooltip 1809 .addClass('tooltipster-' + self.__options.animation) 1810 .addClass('tooltipster-initial') 1811 .css({ 1812 '-moz-animation-duration': self.__options.animationDuration[0] + 'ms', 1813 '-ms-animation-duration': self.__options.animationDuration[0] + 'ms', 1814 '-o-animation-duration': self.__options.animationDuration[0] + 'ms', 1815 '-webkit-animation-duration': self.__options.animationDuration[0] + 'ms', 1816 'animation-duration': self.__options.animationDuration[0] + 'ms', 1817 'transition-duration': self.__options.animationDuration[0] + 'ms' 1818 }); 1819 1820 setTimeout( 1821 function () { 1822 1823 // a quick hover may have already triggered a mouseleave 1824 if (self.__state != 'closed') { 1825 1826 self._$tooltip 1827 .addClass('tooltipster-show') 1828 .removeClass('tooltipster-initial'); 1829 1830 if (self.__options.animationDuration[0] > 0) { 1831 self._$tooltip.delay(self.__options.animationDuration[0]); 1832 } 1833 1834 self._$tooltip.queue(finish); 1835 } 1836 }, 1837 0 1838 ); 1839 } 1840 else { 1841 1842 // old browsers will have to live with this 1843 self._$tooltip 1844 .css('display', 'none') 1845 .fadeIn(self.__options.animationDuration[0], finish); 1846 } 1847 1848 // checks if the origin is removed while the tooltip is open 1849 self.__trackerStart(); 1850 1851 // NOTE: the listeners below have a '-triggerClose' namespace 1852 // because we'll remove them when the tooltip closes (unlike 1853 // the '-triggerOpen' listeners). So some of them are actually 1854 // not about close triggers, rather about positioning. 1855 1856 $(env.window) 1857 // reposition on resize 1858 .on('resize.' + self.__namespace + '-triggerClose', function (e) { 1859 1860 var $ae = $(document.activeElement); 1861 1862 // reposition only if the resize event was not triggered upon the opening 1863 // of a virtual keyboard due to an input field being focused within the tooltip 1864 // (otherwise the repositioning would lose the focus) 1865 if ((!$ae.is('input') && !$ae.is('textarea')) 1866 || !$.contains(self._$tooltip[0], $ae[0]) 1867 ) { 1868 self.reposition(e); 1869 } 1870 }) 1871 // same as below for parents 1872 .on('scroll.' + self.__namespace + '-triggerClose', function (e) { 1873 self.__scrollHandler(e); 1874 }); 1875 1876 self.__$originParents = self._$origin.parents(); 1877 1878 // scrolling may require the tooltip to be moved or even 1879 // repositioned in some cases 1880 self.__$originParents.each(function (i, parent) { 1881 1882 $(parent).on('scroll.' + self.__namespace + '-triggerClose', function (e) { 1883 self.__scrollHandler(e); 1884 }); 1885 }); 1886 1887 if (self.__options.triggerClose.mouseleave 1888 || (self.__options.triggerClose.touchleave && env.hasTouchCapability) 1889 ) { 1890 1891 // we use an event to allow users/plugins to control when the mouseleave/touchleave 1892 // close triggers will come to action. It allows to have more triggering elements 1893 // than just the origin and the tooltip for example, or to cancel/delay the closing, 1894 // or to make the tooltip interactive even if it wasn't when it was open, etc. 1895 self._on('dismissable', function (event) { 1896 1897 if (event.dismissable) { 1898 1899 if (event.delay) { 1900 1901 timeout = setTimeout(function () { 1902 // event.event may be undefined 1903 self._close(event.event); 1904 }, event.delay); 1905 1906 self.__timeouts.close.push(timeout); 1907 } 1908 else { 1909 self._close(event); 1910 } 1911 } 1912 else { 1913 clearTimeout(timeout); 1914 } 1915 }); 1916 1917 // now set the listeners that will trigger 'dismissable' events 1918 var $elements = self._$origin, 1919 eventNamesIn = '', 1920 eventNamesOut = '', 1921 timeout = null; 1922 1923 // if we have to allow interaction, bind on the tooltip too 1924 if (self.__options.interactive) { 1925 $elements = $elements.add(self._$tooltip); 1926 } 1927 1928 if (self.__options.triggerClose.mouseleave) { 1929 eventNamesIn += 'mouseenter.' + self.__namespace + '-triggerClose '; 1930 eventNamesOut += 'mouseleave.' + self.__namespace + '-triggerClose '; 1931 } 1932 if (self.__options.triggerClose.touchleave && env.hasTouchCapability) { 1933 eventNamesIn += 'touchstart.' + self.__namespace + '-triggerClose'; 1934 eventNamesOut += 'touchend.' + self.__namespace + '-triggerClose touchcancel.' + self.__namespace + '-triggerClose'; 1935 } 1936 1937 $elements 1938 // close after some time spent outside of the elements 1939 .on(eventNamesOut, function (event) { 1940 1941 // it's ok if the touch gesture ended up to be a swipe, 1942 // it's still a "touch leave" situation 1943 if (self._touchIsTouchEvent(event) 1944 || !self._touchIsEmulatedEvent(event) 1945 ) { 1946 1947 var delay = (event.type == 'mouseleave') ? 1948 self.__options.delay : 1949 self.__options.delayTouch; 1950 1951 self._trigger({ 1952 delay: delay[1], 1953 dismissable: true, 1954 event: event, 1955 type: 'dismissable' 1956 }); 1957 } 1958 }) 1959 // suspend the mouseleave timeout when the pointer comes back 1960 // over the elements 1961 .on(eventNamesIn, function (event) { 1962 1963 // it's also ok if the touch event is a swipe gesture 1964 if (self._touchIsTouchEvent(event) 1965 || !self._touchIsEmulatedEvent(event) 1966 ) { 1967 self._trigger({ 1968 dismissable: false, 1969 event: event, 1970 type: 'dismissable' 1971 }); 1972 } 1973 }); 1974 } 1975 1976 // close the tooltip when the origin gets a mouse click (common behavior of 1977 // native tooltips) 1978 if (self.__options.triggerClose.originClick) { 1979 1980 self._$origin.on('click.' + self.__namespace + '-triggerClose', function (event) { 1981 1982 // we could actually let a tap trigger this but this feature just 1983 // does not make sense on touch devices 1984 if (!self._touchIsTouchEvent(event) 1985 && !self._touchIsEmulatedEvent(event) 1986 ) { 1987 self._close(event); 1988 } 1989 }); 1990 } 1991 1992 // set the same bindings for click and touch on the body to close the tooltip 1993 if (self.__options.triggerClose.click 1994 || (self.__options.triggerClose.tap && env.hasTouchCapability) 1995 ) { 1996 1997 // don't set right away since the click/tap event which triggered this method 1998 // (if it was a click/tap) is going to bubble up to the body, we don't want it 1999 // to close the tooltip immediately after it opened 2000 setTimeout(function () { 2001 2002 if (self.__state != 'closed') { 2003 2004 var eventNames = '', 2005 $body = $(env.window.document.body); 2006 2007 if (self.__options.triggerClose.click) { 2008 eventNames += 'click.' + self.__namespace + '-triggerClose '; 2009 } 2010 if (self.__options.triggerClose.tap && env.hasTouchCapability) { 2011 eventNames += 'touchend.' + self.__namespace + '-triggerClose'; 2012 } 2013 2014 $body.on(eventNames, function (event) { 2015 2016 if (self._touchIsMeaningfulEvent(event)) { 2017 2018 self._touchRecordEvent(event); 2019 2020 if (!self.__options.interactive || !$.contains(self._$tooltip[0], event.target)) { 2021 self._close(event); 2022 } 2023 } 2024 }); 2025 2026 // needed to detect and ignore swiping 2027 if (self.__options.triggerClose.tap && env.hasTouchCapability) { 2028 2029 $body.on('touchstart.' + self.__namespace + '-triggerClose', function (event) { 2030 self._touchRecordEvent(event); 2031 }); 2032 } 2033 } 2034 }, 0); 2035 } 2036 2037 self._trigger('ready'); 2038 2039 // call our custom callback 2040 if (self.__options.functionReady) { 2041 self.__options.functionReady.call(self, self, { 2042 origin: self._$origin[0], 2043 tooltip: self._$tooltip[0] 2044 }); 2045 } 2046 } 2047 2048 // if we have a timer set, let the countdown begin 2049 if (self.__options.timer > 0) { 2050 2051 var timeout = setTimeout(function () { 2052 self._close(); 2053 }, self.__options.timer + extraTime); 2054 2055 self.__timeouts.close.push(timeout); 2056 } 2057 } 2058 } 2059 } 2060 } 2061 2062 return self; 2063 }, 2064 2065 /** 2066 * When using the mouseenter/touchstart open triggers, this function will 2067 * schedule the opening of the tooltip after the delay, if there is one 2068 * 2069 * @param event 2070 * @returns {self} 2071 * @protected 2072 */ 2073 _openShortly: function (event) { 2074 2075 var self = this, 2076 ok = true; 2077 2078 if (self.__state != 'stable' && self.__state != 'appearing') { 2079 2080 // if a timeout is not already running 2081 if (!self.__timeouts.open) { 2082 2083 self._trigger({ 2084 type: 'start', 2085 event: event, 2086 stop: function () { 2087 ok = false; 2088 } 2089 }); 2090 2091 if (ok) { 2092 2093 var delay = (event.type.indexOf('touch') == 0) ? 2094 self.__options.delayTouch : 2095 self.__options.delay; 2096 2097 if (delay[0]) { 2098 2099 self.__timeouts.open = setTimeout(function () { 2100 2101 self.__timeouts.open = null; 2102 2103 // open only if the pointer (mouse or touch) is still over the origin. 2104 // The check on the "meaningful event" can only be made here, after some 2105 // time has passed (to know if the touch was a swipe or not) 2106 if (self.__pointerIsOverOrigin && self._touchIsMeaningfulEvent(event)) { 2107 2108 // signal that we go on 2109 self._trigger('startend'); 2110 2111 self._open(event); 2112 } 2113 else { 2114 // signal that we cancel 2115 self._trigger('startcancel'); 2116 } 2117 }, delay[0]); 2118 } 2119 else { 2120 // signal that we go on 2121 self._trigger('startend'); 2122 2123 self._open(event); 2124 } 2125 } 2126 } 2127 } 2128 2129 return self; 2130 }, 2131 2132 /** 2133 * Meant for plugins to get their options 2134 * 2135 * @param {string} pluginName The name of the plugin that asks for its options 2136 * @param {object} defaultOptions The default options of the plugin 2137 * @returns {object} The options 2138 * @protected 2139 */ 2140 _optionsExtract: function (pluginName, defaultOptions) { 2141 2142 var self = this, 2143 options = $.extend(true, {}, defaultOptions); 2144 2145 // if the plugin options were isolated in a property named after the 2146 // plugin, use them (prevents conflicts with other plugins) 2147 var pluginOptions = self.__options[pluginName]; 2148 2149 // if not, try to get them as regular options 2150 if (!pluginOptions) { 2151 2152 pluginOptions = {}; 2153 2154 $.each(defaultOptions, function (optionName, value) { 2155 2156 var o = self.__options[optionName]; 2157 2158 if (o !== undefined) { 2159 pluginOptions[optionName] = o; 2160 } 2161 }); 2162 } 2163 2164 // let's merge the default options and the ones that were provided. We'd want 2165 // to do a deep copy but not let jQuery merge arrays, so we'll do a shallow 2166 // extend on two levels, that will be enough if options are not more than 1 2167 // level deep 2168 $.each(options, function (optionName, value) { 2169 2170 if (pluginOptions[optionName] !== undefined) { 2171 2172 if ((typeof value == 'object' 2173 && !(value instanceof Array) 2174 && value != null 2175 ) 2176 && 2177 (typeof pluginOptions[optionName] == 'object' 2178 && !(pluginOptions[optionName] instanceof Array) 2179 && pluginOptions[optionName] != null 2180 ) 2181 ) { 2182 $.extend(options[optionName], pluginOptions[optionName]); 2183 } 2184 else { 2185 options[optionName] = pluginOptions[optionName]; 2186 } 2187 } 2188 }); 2189 2190 return options; 2191 }, 2192 2193 /** 2194 * Used at instantiation of the plugin, or afterwards by plugins that activate themselves 2195 * on existing instances 2196 * 2197 * @param {object} pluginName 2198 * @returns {self} 2199 * @protected 2200 */ 2201 _plug: function (pluginName) { 2202 2203 var plugin = $.tooltipster._plugin(pluginName); 2204 2205 if (plugin) { 2206 2207 // if there is a constructor for instances 2208 if (plugin.instance) { 2209 2210 // proxy non-private methods on the instance to allow new instance methods 2211 $.tooltipster.__bridge(plugin.instance, this, plugin.name); 2212 } 2213 } 2214 else { 2215 throw new Error('The "' + pluginName + '" plugin is not defined'); 2216 } 2217 2218 return this; 2219 }, 2220 2221 /** 2222 * This will return true if the event is a mouse event which was 2223 * emulated by the browser after a touch event. This allows us to 2224 * really dissociate mouse and touch triggers. 2225 * 2226 * There is a margin of error if a real mouse event is fired right 2227 * after (within the delay shown below) a touch event on the same 2228 * element, but hopefully it should not happen often. 2229 * 2230 * @returns {boolean} 2231 * @protected 2232 */ 2233 _touchIsEmulatedEvent: function (event) { 2234 2235 var isEmulated = false, 2236 now = new Date().getTime(); 2237 2238 for (var i = this.__touchEvents.length - 1; i >= 0; i--) { 2239 2240 var e = this.__touchEvents[i]; 2241 2242 // delay, in milliseconds. It's supposed to be 300ms in 2243 // most browsers (350ms on iOS) to allow a double tap but 2244 // can be less (check out FastClick for more info) 2245 if (now - e.time < 500) { 2246 2247 if (e.target === event.target) { 2248 isEmulated = true; 2249 } 2250 } 2251 else { 2252 break; 2253 } 2254 } 2255 2256 return isEmulated; 2257 }, 2258 2259 /** 2260 * Returns false if the event was an emulated mouse event or 2261 * a touch event involved in a swipe gesture. 2262 * 2263 * @param {object} event 2264 * @returns {boolean} 2265 * @protected 2266 */ 2267 _touchIsMeaningfulEvent: function (event) { 2268 return ( 2269 (this._touchIsTouchEvent(event) && !this._touchSwiped(event.target)) 2270 || (!this._touchIsTouchEvent(event) && !this._touchIsEmulatedEvent(event)) 2271 ); 2272 }, 2273 2274 /** 2275 * Checks if an event is a touch event 2276 * 2277 * @param {object} event 2278 * @returns {boolean} 2279 * @protected 2280 */ 2281 _touchIsTouchEvent: function (event) { 2282 return event.type.indexOf('touch') == 0; 2283 }, 2284 2285 /** 2286 * Store touch events for a while to detect swiping and emulated mouse events 2287 * 2288 * @param {object} event 2289 * @returns {self} 2290 * @protected 2291 */ 2292 _touchRecordEvent: function (event) { 2293 2294 if (this._touchIsTouchEvent(event)) { 2295 event.time = new Date().getTime(); 2296 this.__touchEvents.push(event); 2297 } 2298 2299 return this; 2300 }, 2301 2302 /** 2303 * Returns true if a swipe happened after the last touchstart event fired on 2304 * event.target. 2305 * 2306 * We need to differentiate a swipe from a tap before we let the event open 2307 * or close the tooltip. A swipe is when a touchmove (scroll) event happens 2308 * on the body between the touchstart and the touchend events of an element. 2309 * 2310 * @param {object} target The HTML element that may have triggered the swipe 2311 * @returns {boolean} 2312 * @protected 2313 */ 2314 _touchSwiped: function (target) { 2315 2316 var swiped = false; 2317 2318 for (var i = this.__touchEvents.length - 1; i >= 0; i--) { 2319 2320 var e = this.__touchEvents[i]; 2321 2322 if (e.type == 'touchmove') { 2323 swiped = true; 2324 break; 2325 } 2326 else if ( 2327 e.type == 'touchstart' 2328 && target === e.target 2329 ) { 2330 break; 2331 } 2332 } 2333 2334 return swiped; 2335 }, 2336 2337 /** 2338 * Triggers an event on the instance emitters 2339 * 2340 * @returns {self} 2341 * @protected 2342 */ 2343 _trigger: function () { 2344 2345 var args = Array.prototype.slice.apply(arguments); 2346 2347 if (typeof args[0] == 'string') { 2348 args[0] = { type: args[0] }; 2349 } 2350 2351 // add properties to the event 2352 args[0].instance = this; 2353 args[0].origin = this._$origin ? this._$origin[0] : null; 2354 args[0].tooltip = this._$tooltip ? this._$tooltip[0] : null; 2355 2356 // note: the order of emitters matters 2357 this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate, args); 2358 $.tooltipster._trigger.apply($.tooltipster, args); 2359 this.__$emitterPublic.trigger.apply(this.__$emitterPublic, args); 2360 2361 return this; 2362 }, 2363 2364 /** 2365 * Deactivate a plugin on this instance 2366 * 2367 * @returns {self} 2368 * @protected 2369 */ 2370 _unplug: function (pluginName) { 2371 2372 var self = this; 2373 2374 // if the plugin has been activated on this instance 2375 if (self[pluginName]) { 2376 2377 var plugin = $.tooltipster._plugin(pluginName); 2378 2379 // if there is a constructor for instances 2380 if (plugin.instance) { 2381 2382 // unbridge 2383 $.each(plugin.instance, function (methodName, fn) { 2384 2385 // if the method exists (privates methods do not) and comes indeed from 2386 // this plugin (may be missing or come from a conflicting plugin). 2387 if (self[methodName] 2388 && self[methodName].bridged === self[pluginName] 2389 ) { 2390 delete self[methodName]; 2391 } 2392 }); 2393 } 2394 2395 // destroy the plugin 2396 if (self[pluginName].__destroy) { 2397 self[pluginName].__destroy(); 2398 } 2399 2400 // remove the reference to the plugin instance 2401 delete self[pluginName]; 2402 } 2403 2404 return self; 2405 }, 2406 2407 /** 2408 * @see self::_close 2409 * @returns {self} 2410 * @public 2411 */ 2412 close: function (callback) { 2413 2414 if (!this.__destroyed) { 2415 this._close(null, callback); 2416 } 2417 else { 2418 this.__destroyError(); 2419 } 2420 2421 return this; 2422 }, 2423 2424 /** 2425 * Sets or gets the content of the tooltip 2426 * 2427 * @returns {mixed|self} 2428 * @public 2429 */ 2430 content: function (content) { 2431 2432 var self = this; 2433 2434 // getter method 2435 if (content === undefined) { 2436 return self.__Content; 2437 } 2438 // setter method 2439 else { 2440 2441 if (!self.__destroyed) { 2442 2443 // change the content 2444 self.__contentSet(content); 2445 2446 if (self.__Content !== null) { 2447 2448 // update the tooltip if it is open 2449 if (self.__state !== 'closed') { 2450 2451 // reset the content in the tooltip 2452 self.__contentInsert(); 2453 2454 // reposition and resize the tooltip 2455 self.reposition(); 2456 2457 // if we want to play a little animation showing the content changed 2458 if (self.__options.updateAnimation) { 2459 2460 if (env.hasTransitions) { 2461 2462 // keep the reference in the local scope 2463 var animation = self.__options.updateAnimation; 2464 2465 self._$tooltip.addClass('tooltipster-update-' + animation); 2466 2467 // remove the class after a while. The actual duration of the 2468 // update animation may be shorter, it's set in the CSS rules 2469 setTimeout(function () { 2470 2471 if (self.__state != 'closed') { 2472 2473 self._$tooltip.removeClass('tooltipster-update-' + animation); 2474 } 2475 }, 1000); 2476 } 2477 else { 2478 self._$tooltip.fadeTo(200, 0.5, function () { 2479 if (self.__state != 'closed') { 2480 self._$tooltip.fadeTo(200, 1); 2481 } 2482 }); 2483 } 2484 } 2485 } 2486 } 2487 else { 2488 self._close(); 2489 } 2490 } 2491 else { 2492 self.__destroyError(); 2493 } 2494 2495 return self; 2496 } 2497 }, 2498 2499 /** 2500 * Destroys the tooltip 2501 * 2502 * @returns {self} 2503 * @public 2504 */ 2505 destroy: function () { 2506 2507 var self = this; 2508 2509 if (!self.__destroyed) { 2510 2511 if (self.__state != 'closed') { 2512 2513 // no closing delay 2514 self.option('animationDuration', 0) 2515 // force closing 2516 ._close(null, null, true); 2517 } 2518 else { 2519 // there might be an open timeout still running 2520 self.__timeoutsClear(); 2521 } 2522 2523 // send event 2524 self._trigger('destroy'); 2525 2526 self.__destroyed = true; 2527 2528 self._$origin 2529 .removeData(self.__namespace) 2530 // remove the open trigger listeners 2531 .off('.' + self.__namespace + '-triggerOpen'); 2532 2533 // remove the touch listener 2534 $(env.window.document.body).off('.' + self.__namespace + '-triggerOpen'); 2535 2536 var ns = self._$origin.data('tooltipster-ns'); 2537 2538 // if the origin has been removed from DOM, its data may 2539 // well have been destroyed in the process and there would 2540 // be nothing to clean up or restore 2541 if (ns) { 2542 2543 // if there are no more tooltips on this element 2544 if (ns.length === 1) { 2545 2546 // optional restoration of a title attribute 2547 var title = null; 2548 if (self.__options.restoration == 'previous') { 2549 title = self._$origin.data('tooltipster-initialTitle'); 2550 } 2551 else if (self.__options.restoration == 'current') { 2552 2553 // old school technique to stringify when outerHTML is not supported 2554 title = (typeof self.__Content == 'string') ? 2555 self.__Content : 2556 $('<div></div>').append(self.__Content).html(); 2557 } 2558 2559 if (title) { 2560 self._$origin.attr('title', title); 2561 } 2562 2563 // final cleaning 2564 self._$origin.removeClass('tooltipstered'); 2565 2566 self._$origin 2567 .removeData('tooltipster-ns') 2568 .removeData('tooltipster-initialTitle'); 2569 } 2570 else { 2571 // remove the instance namespace from the list of namespaces of 2572 // tooltips present on the element 2573 ns = $.grep(ns, function (el, i) { 2574 return el !== self.__namespace; 2575 }); 2576 self._$origin.data('tooltipster-ns', ns); 2577 } 2578 } 2579 2580 // last event 2581 self._trigger('destroyed'); 2582 2583 // unbind private and public event listeners 2584 self._off(); 2585 self.off(); 2586 2587 // remove external references, just in case 2588 self.__Content = null; 2589 self.__$emitterPrivate = null; 2590 self.__$emitterPublic = null; 2591 self.__options.parent = null; 2592 self._$origin = null; 2593 self._$tooltip = null; 2594 2595 // make sure the object is no longer referenced in there to prevent 2596 // memory leaks 2597 $.tooltipster.__instancesLatestArr = $.grep($.tooltipster.__instancesLatestArr, function (el, i) { 2598 return self !== el; 2599 }); 2600 2601 clearInterval(self.__garbageCollector); 2602 } 2603 else { 2604 self.__destroyError(); 2605 } 2606 2607 // we return the scope rather than true so that the call to 2608 // .tooltipster('destroy') actually returns the matched elements 2609 // and applies to all of them 2610 return self; 2611 }, 2612 2613 /** 2614 * Disables the tooltip 2615 * 2616 * @returns {self} 2617 * @public 2618 */ 2619 disable: function () { 2620 2621 if (!this.__destroyed) { 2622 2623 // close first, in case the tooltip would not disappear on 2624 // its own (no close trigger) 2625 this._close(); 2626 this.__enabled = false; 2627 2628 return this; 2629 } 2630 else { 2631 this.__destroyError(); 2632 } 2633 2634 return this; 2635 }, 2636 2637 /** 2638 * Returns the HTML element of the origin 2639 * 2640 * @returns {self} 2641 * @public 2642 */ 2643 elementOrigin: function () { 2644 2645 if (!this.__destroyed) { 2646 return this._$origin[0]; 2647 } 2648 else { 2649 this.__destroyError(); 2650 } 2651 }, 2652 2653 /** 2654 * Returns the HTML element of the tooltip 2655 * 2656 * @returns {self} 2657 * @public 2658 */ 2659 elementTooltip: function () { 2660 return this._$tooltip ? this._$tooltip[0] : null; 2661 }, 2662 2663 /** 2664 * Enables the tooltip 2665 * 2666 * @returns {self} 2667 * @public 2668 */ 2669 enable: function () { 2670 this.__enabled = true; 2671 return this; 2672 }, 2673 2674 /** 2675 * Alias, deprecated in 4.0.0 2676 * 2677 * @param {function} callback 2678 * @returns {self} 2679 * @public 2680 */ 2681 hide: function (callback) { 2682 return this.close(callback); 2683 }, 2684 2685 /** 2686 * Returns the instance 2687 * 2688 * @returns {self} 2689 * @public 2690 */ 2691 instance: function () { 2692 return this; 2693 }, 2694 2695 /** 2696 * For public use only, not to be used by plugins (use ::_off() instead) 2697 * 2698 * @returns {self} 2699 * @public 2700 */ 2701 off: function () { 2702 2703 if (!this.__destroyed) { 2704 this.__$emitterPublic.off.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); 2705 } 2706 2707 return this; 2708 }, 2709 2710 /** 2711 * For public use only, not to be used by plugins (use ::_on() instead) 2712 * 2713 * @returns {self} 2714 * @public 2715 */ 2716 on: function () { 2717 2718 if (!this.__destroyed) { 2719 this.__$emitterPublic.on.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); 2720 } 2721 else { 2722 this.__destroyError(); 2723 } 2724 2725 return this; 2726 }, 2727 2728 /** 2729 * For public use only, not to be used by plugins 2730 * 2731 * @returns {self} 2732 * @public 2733 */ 2734 one: function () { 2735 2736 if (!this.__destroyed) { 2737 this.__$emitterPublic.one.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); 2738 } 2739 else { 2740 this.__destroyError(); 2741 } 2742 2743 return this; 2744 }, 2745 2746 /** 2747 * @see self::_open 2748 * @returns {self} 2749 * @public 2750 */ 2751 open: function (callback) { 2752 2753 if (!this.__destroyed) { 2754 this._open(null, callback); 2755 } 2756 else { 2757 this.__destroyError(); 2758 } 2759 2760 return this; 2761 }, 2762 2763 /** 2764 * Get or set options. For internal use and advanced users only. 2765 * 2766 * @param {string} o Option name 2767 * @param {mixed} val optional A new value for the option 2768 * @return {mixed|self} If val is omitted, the value of the option 2769 * is returned, otherwise the instance itself is returned 2770 * @public 2771 */ 2772 option: function (o, val) { 2773 2774 // getter 2775 if (val === undefined) { 2776 return this.__options[o]; 2777 } 2778 // setter 2779 else { 2780 2781 if (!this.__destroyed) { 2782 2783 // change value 2784 this.__options[o] = val; 2785 2786 // format 2787 this.__optionsFormat(); 2788 2789 // re-prepare the triggers if needed 2790 if ($.inArray(o, ['trigger', 'triggerClose', 'triggerOpen']) >= 0) { 2791 this.__prepareOrigin(); 2792 } 2793 2794 if (o === 'selfDestruction') { 2795 this.__prepareGC(); 2796 } 2797 } 2798 else { 2799 this.__destroyError(); 2800 } 2801 2802 return this; 2803 } 2804 }, 2805 2806 /** 2807 * This method is in charge of setting the position and size properties of the tooltip. 2808 * All the hard work is delegated to the display plugin. 2809 * Note: The tooltip may be detached from the DOM at the moment the method is called 2810 * but must be attached by the end of the method call. 2811 * 2812 * @param {object} event For internal use only. Defined if an event such as 2813 * window resizing triggered the repositioning 2814 * @param {boolean} tooltipIsDetached For internal use only. Set this to true if you 2815 * know that the tooltip not being in the DOM is not an issue (typically when the 2816 * tooltip element has just been created but has not been added to the DOM yet). 2817 * @returns {self} 2818 * @public 2819 */ 2820 reposition: function (event, tooltipIsDetached) { 2821 2822 var self = this; 2823 2824 if (!self.__destroyed) { 2825 2826 // if the tooltip is still open and the origin is still in the DOM 2827 if (self.__state != 'closed' && bodyContains(self._$origin)) { 2828 2829 // if the tooltip has not been removed from DOM manually (or if it 2830 // has been detached on purpose) 2831 if (tooltipIsDetached || bodyContains(self._$tooltip)) { 2832 2833 if (!tooltipIsDetached) { 2834 // detach in case the tooltip overflows the window and adds 2835 // scrollbars to it, so __geometry can be accurate 2836 self._$tooltip.detach(); 2837 } 2838 2839 // refresh the geometry object before passing it as a helper 2840 self.__Geometry = self.__geometry(); 2841 2842 // let a plugin fo the rest 2843 self._trigger({ 2844 type: 'reposition', 2845 event: event, 2846 helper: { 2847 geo: self.__Geometry 2848 } 2849 }); 2850 } 2851 } 2852 } 2853 else { 2854 self.__destroyError(); 2855 } 2856 2857 return self; 2858 }, 2859 2860 /** 2861 * Alias, deprecated in 4.0.0 2862 * 2863 * @param callback 2864 * @returns {self} 2865 * @public 2866 */ 2867 show: function (callback) { 2868 return this.open(callback); 2869 }, 2870 2871 /** 2872 * Returns some properties about the instance 2873 * 2874 * @returns {object} 2875 * @public 2876 */ 2877 status: function () { 2878 2879 return { 2880 destroyed: this.__destroyed, 2881 enabled: this.__enabled, 2882 open: this.__state !== 'closed', 2883 state: this.__state 2884 }; 2885 }, 2886 2887 /** 2888 * For public use only, not to be used by plugins 2889 * 2890 * @returns {self} 2891 * @public 2892 */ 2893 triggerHandler: function () { 2894 2895 if (!this.__destroyed) { 2896 this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); 2897 } 2898 else { 2899 this.__destroyError(); 2900 } 2901 2902 return this; 2903 } 2904 }; 2905 2906 $.fn.tooltipster = function () { 2907 2908 // for using in closures 2909 var args = Array.prototype.slice.apply(arguments), 2910 // common mistake: an HTML element can't be in several tooltips at the same time 2911 contentCloningWarning = 'You are using a single HTML element as content for several tooltips. You probably want to set the contentCloning option to TRUE.'; 2912 2913 // this happens with $(sel).tooltipster(...) when $(sel) does not match anything 2914 if (this.length === 0) { 2915 2916 // still chainable 2917 return this; 2918 } 2919 // this happens when calling $(sel).tooltipster('methodName or options') 2920 // where $(sel) matches one or more elements 2921 else { 2922 2923 // method calls 2924 if (typeof args[0] === 'string') { 2925 2926 var v = '#*$~&'; 2927 2928 this.each(function () { 2929 2930 // retrieve the namepaces of the tooltip(s) that exist on that element. 2931 // We will interact with the first tooltip only. 2932 var ns = $(this).data('tooltipster-ns'), 2933 // self represents the instance of the first tooltipster plugin 2934 // associated to the current HTML object of the loop 2935 self = ns ? $(this).data(ns[0]) : null; 2936 2937 // if the current element holds a tooltipster instance 2938 if (self) { 2939 2940 if (typeof self[args[0]] === 'function') { 2941 2942 if (this.length > 1 2943 && args[0] == 'content' 2944 && (args[1] instanceof $ 2945 || (typeof args[1] == 'object' && args[1] != null && args[1].tagName) 2946 ) 2947 && !self.__options.contentCloning 2948 && self.__options.debug 2949 ) { 2950 console.log(contentCloningWarning); 2951 } 2952 2953 // note : args[1] and args[2] may not be defined 2954 var resp = self[args[0]](args[1], args[2]); 2955 } 2956 else { 2957 throw new Error('Unknown method "' + args[0] + '"'); 2958 } 2959 2960 // if the function returned anything other than the instance 2961 // itself (which implies chaining, except for the `instance` method) 2962 if (resp !== self || args[0] === 'instance') { 2963 2964 v = resp; 2965 2966 // return false to stop .each iteration on the first element 2967 // matched by the selector 2968 return false; 2969 } 2970 } 2971 else { 2972 throw new Error('You called Tooltipster\'s "' + args[0] + '" method on an uninitialized element'); 2973 } 2974 }); 2975 2976 return (v !== '#*$~&') ? v : this; 2977 } 2978 // first argument is undefined or an object: the tooltip is initializing 2979 else { 2980 2981 // reset the array of last initialized objects 2982 $.tooltipster.__instancesLatestArr = []; 2983 2984 // is there a defined value for the multiple option in the options object ? 2985 var multipleIsSet = args[0] && args[0].multiple !== undefined, 2986 // if the multiple option is set to true, or if it's not defined but 2987 // set to true in the defaults 2988 multiple = (multipleIsSet && args[0].multiple) || (!multipleIsSet && defaults.multiple), 2989 // same for content 2990 contentIsSet = args[0] && args[0].content !== undefined, 2991 content = (contentIsSet && args[0].content) || (!contentIsSet && defaults.content), 2992 // same for contentCloning 2993 contentCloningIsSet = args[0] && args[0].contentCloning !== undefined, 2994 contentCloning = 2995 (contentCloningIsSet && args[0].contentCloning) 2996 || (!contentCloningIsSet && defaults.contentCloning), 2997 // same for debug 2998 debugIsSet = args[0] && args[0].debug !== undefined, 2999 debug = (debugIsSet && args[0].debug) || (!debugIsSet && defaults.debug); 3000 3001 if (this.length > 1 3002 && (content instanceof $ 3003 || (typeof content == 'object' && content != null && content.tagName) 3004 ) 3005 && !contentCloning 3006 && debug 3007 ) { 3008 console.log(contentCloningWarning); 3009 } 3010 3011 // create a tooltipster instance for each element if it doesn't 3012 // already have one or if the multiple option is set, and attach the 3013 // object to it 3014 this.each(function () { 3015 3016 var go = false, 3017 $this = $(this), 3018 ns = $this.data('tooltipster-ns'), 3019 obj = null; 3020 3021 if (!ns) { 3022 go = true; 3023 } 3024 else if (multiple) { 3025 go = true; 3026 } 3027 else if (debug) { 3028 console.log('Tooltipster: one or more tooltips are already attached to the element below. Ignoring.'); 3029 console.log(this); 3030 } 3031 3032 if (go) { 3033 obj = new $.Tooltipster(this, args[0]); 3034 3035 // save the reference of the new instance 3036 if (!ns) ns = []; 3037 ns.push(obj.__namespace); 3038 $this.data('tooltipster-ns', ns); 3039 3040 // save the instance itself 3041 $this.data(obj.__namespace, obj); 3042 3043 // call our constructor custom function. 3044 // we do this here and not in ::init() because we wanted 3045 // the object to be saved in $this.data before triggering 3046 // it 3047 if (obj.__options.functionInit) { 3048 obj.__options.functionInit.call(obj, obj, { 3049 origin: this 3050 }); 3051 } 3052 3053 // and now the event, for the plugins and core emitter 3054 obj._trigger('init'); 3055 } 3056 3057 $.tooltipster.__instancesLatestArr.push(obj); 3058 }); 3059 3060 return this; 3061 } 3062 } 3063 }; 3064 3065 // Utilities 3066 3067 /** 3068 * A class to check if a tooltip can fit in given dimensions 3069 * 3070 * @param {object} $tooltip The jQuery wrapped tooltip element, or a clone of it 3071 */ 3072 function Ruler($tooltip) { 3073 3074 // list of instance variables 3075 3076 this.$container; 3077 this.constraints = null; 3078 this.__$tooltip; 3079 3080 this.__init($tooltip); 3081 } 3082 3083 Ruler.prototype = { 3084 3085 /** 3086 * Move the tooltip into an invisible div that does not allow overflow to make 3087 * size tests. Note: the tooltip may or may not be attached to the DOM at the 3088 * moment this method is called, it does not matter. 3089 * 3090 * @param {object} $tooltip The object to test. May be just a clone of the 3091 * actual tooltip. 3092 * @private 3093 */ 3094 __init: function ($tooltip) { 3095 3096 this.__$tooltip = $tooltip; 3097 3098 this.__$tooltip 3099 .css({ 3100 // for some reason we have to specify top and left 0 3101 left: 0, 3102 // any overflow will be ignored while measuring 3103 overflow: 'hidden', 3104 // positions at (0,0) without the div using 100% of the available width 3105 position: 'absolute', 3106 top: 0 3107 }) 3108 // overflow must be auto during the test. We re-set this in case 3109 // it were modified by the user 3110 .find('.tooltipster-content') 3111 .css('overflow', 'auto'); 3112 3113 this.$container = $('<div class="tooltipster-ruler"></div>') 3114 .append(this.__$tooltip) 3115 .appendTo(env.window.document.body); 3116 }, 3117 3118 /** 3119 * Force the browser to redraw (re-render) the tooltip immediately. This is required 3120 * when you changed some CSS properties and need to make something with it 3121 * immediately, without waiting for the browser to redraw at the end of instructions. 3122 * 3123 * @see http://stackoverflow.com/questions/3485365/how-can-i-force-webkit-to-redraw-repaint-to-propagate-style-changes 3124 * @private 3125 */ 3126 __forceRedraw: function () { 3127 3128 // note: this would work but for Webkit only 3129 //this.__$tooltip.close(); 3130 //this.__$tooltip[0].offsetHeight; 3131 //this.__$tooltip.open(); 3132 3133 // works in FF too 3134 var $p = this.__$tooltip.parent(); 3135 this.__$tooltip.detach(); 3136 this.__$tooltip.appendTo($p); 3137 }, 3138 3139 /** 3140 * Set maximum dimensions for the tooltip. A call to ::measure afterwards 3141 * will tell us if the content overflows or if it's ok 3142 * 3143 * @param {int} width 3144 * @param {int} height 3145 * @return {Ruler} 3146 * @public 3147 */ 3148 constrain: function (width, height) { 3149 3150 this.constraints = { 3151 width: width, 3152 height: height 3153 }; 3154 3155 this.__$tooltip.css({ 3156 // we disable display:flex, otherwise the content would overflow without 3157 // creating horizontal scrolling (which we need to detect). 3158 display: 'block', 3159 // reset any previous height 3160 height: '', 3161 // we'll check if horizontal scrolling occurs 3162 overflow: 'auto', 3163 // we'll set the width and see what height is generated and if there 3164 // is horizontal overflow 3165 width: width 3166 }); 3167 3168 return this; 3169 }, 3170 3171 /** 3172 * Reset the tooltip content overflow and remove the test container 3173 * 3174 * @returns {Ruler} 3175 * @public 3176 */ 3177 destroy: function () { 3178 3179 // in case the element was not a clone 3180 this.__$tooltip 3181 .detach() 3182 .find('.tooltipster-content') 3183 .css({ 3184 // reset to CSS value 3185 display: '', 3186 overflow: '' 3187 }); 3188 3189 this.$container.remove(); 3190 }, 3191 3192 /** 3193 * Removes any constraints 3194 * 3195 * @returns {Ruler} 3196 * @public 3197 */ 3198 free: function () { 3199 3200 this.constraints = null; 3201 3202 // reset to natural size 3203 this.__$tooltip.css({ 3204 display: '', 3205 height: '', 3206 overflow: 'visible', 3207 width: '' 3208 }); 3209 3210 return this; 3211 }, 3212 3213 /** 3214 * Returns the size of the tooltip. When constraints are applied, also returns 3215 * whether the tooltip fits in the provided dimensions. 3216 * The idea is to see if the new height is small enough and if the content does 3217 * not overflow horizontally. 3218 * 3219 * @param {int} width 3220 * @param {int} height 3221 * @returns {object} An object with a bool `fits` property and a `size` property 3222 * @public 3223 */ 3224 measure: function () { 3225 3226 this.__forceRedraw(); 3227 3228 var tooltipBcr = this.__$tooltip[0].getBoundingClientRect(), 3229 result = { 3230 size: { 3231 // bcr.width/height are not defined in IE8- but in this 3232 // case, bcr.right/bottom will have the same value 3233 // except in iOS 8+ where tooltipBcr.bottom/right are wrong 3234 // after scrolling for reasons yet to be determined. 3235 // tooltipBcr.top/left might not be 0, see issue #514 3236 height: tooltipBcr.height || (tooltipBcr.bottom - tooltipBcr.top), 3237 width: tooltipBcr.width || (tooltipBcr.right - tooltipBcr.left) 3238 } 3239 }; 3240 3241 if (this.constraints) { 3242 3243 // note: we used to use offsetWidth instead of boundingRectClient but 3244 // it returned rounded values, causing issues with sub-pixel layouts. 3245 3246 // note2: noticed that the bcrWidth of text content of a div was once 3247 // greater than the bcrWidth of its container by 1px, causing the final 3248 // tooltip box to be too small for its content. However, evaluating 3249 // their widths one against the other (below) surprisingly returned 3250 // equality. Happened only once in Chrome 48, was not able to reproduce 3251 // => just having fun with float position values... 3252 3253 var $content = this.__$tooltip.find('.tooltipster-content'), 3254 height = this.__$tooltip.outerHeight(), 3255 contentBcr = $content[0].getBoundingClientRect(), 3256 fits = { 3257 height: height <= this.constraints.height, 3258 width: ( 3259 // this condition accounts for min-width property that 3260 // may apply 3261 tooltipBcr.width <= this.constraints.width 3262 // the -1 is here because scrollWidth actually returns 3263 // a rounded value, and may be greater than bcr.width if 3264 // it was rounded up. This may cause an issue for contents 3265 // which actually really overflow by 1px or so, but that 3266 // should be rare. Not sure how to solve this efficiently. 3267 // See http://blogs.msdn.com/b/ie/archive/2012/02/17/sub-pixel-rendering-and-the-css-object-model.aspx 3268 && contentBcr.width >= $content[0].scrollWidth - 1 3269 ) 3270 }; 3271 3272 result.fits = fits.height && fits.width; 3273 } 3274 3275 // old versions of IE get the width wrong for some reason and it causes 3276 // the text to be broken to a new line, so we round it up. If the width 3277 // is the width of the screen though, we can assume it is accurate. 3278 if (env.IE 3279 && env.IE <= 11 3280 && result.size.width !== env.window.document.documentElement.clientWidth 3281 ) { 3282 result.size.width = Math.ceil(result.size.width) + 1; 3283 } 3284 3285 return result; 3286 } 3287 }; 3288 3289 // quick & dirty compare function, not bijective nor multidimensional 3290 function areEqual(a, b) { 3291 var same = true; 3292 $.each(a, function (i, _) { 3293 if (b[i] === undefined || a[i] !== b[i]) { 3294 same = false; 3295 return false; 3296 } 3297 }); 3298 return same; 3299 } 3300 3301 /** 3302 * A fast function to check if an element is still in the DOM. It 3303 * tries to use an id as ids are indexed by the browser, or falls 3304 * back to jQuery's `contains` method. May fail if two elements 3305 * have the same id, but so be it 3306 * 3307 * @param {object} $obj A jQuery-wrapped HTML element 3308 * @return {boolean} 3309 */ 3310 function bodyContains($obj) { 3311 var id = $obj.attr('id'), 3312 el = id ? env.window.document.getElementById(id) : null; 3313 // must also check that the element with the id is the one we want 3314 return el ? el === $obj[0] : $.contains(env.window.document.body, $obj[0]); 3315 } 3316 3317 // detect IE versions for dirty fixes 3318 var uA = navigator.userAgent.toLowerCase(); 3319 if (uA.indexOf('msie') != -1) env.IE = parseInt(uA.split('msie')[1]); 3320 else if (uA.toLowerCase().indexOf('trident') !== -1 && uA.indexOf(' rv:11') !== -1) env.IE = 11; 3321 else if (uA.toLowerCase().indexOf('edge/') != -1) env.IE = parseInt(uA.toLowerCase().split('edge/')[1]); 3322 3323 // detecting support for CSS transitions 3324 function transitionSupport() { 3325 3326 // env.window is not defined yet when this is called 3327 if (!win) return false; 3328 3329 var b = win.document.body || win.document.documentElement, 3330 s = b.style, 3331 p = 'transition', 3332 v = ['Moz', 'Webkit', 'Khtml', 'O', 'ms']; 3333 3334 if (typeof s[p] == 'string') { return true; } 3335 3336 p = p.charAt(0).toUpperCase() + p.substr(1); 3337 for (var i = 0; i < v.length; i++) { 3338 if (typeof s[v[i] + p] == 'string') { return true; } 3339 } 3340 return false; 3341 } 3342 3343 // we'll return jQuery for plugins not to have to declare it as a dependency, 3344 // but it's done by a build task since it should be included only once at the 3345 // end when we concatenate the main file with a plugin 3346 // sideTip is Tooltipster's default plugin. 3347 // This file will be UMDified by a build task. 3348 3349 var pluginName = 'tooltipster.sideTip'; 3350 3351 $.tooltipster._plugin({ 3352 name: pluginName, 3353 instance: { 3354 /** 3355 * Defaults are provided as a function for an easy override by inheritance 3356 * 3357 * @return {object} An object with the defaults options 3358 * @private 3359 */ 3360 __defaults: function () { 3361 3362 return { 3363 // if the tooltip should display an arrow that points to the origin 3364 arrow: true, 3365 ppclass: '', 3366 // the distance in pixels between the tooltip and the origin 3367 distance: 6, 3368 // allows to easily change the position of the tooltip 3369 functionPosition: null, 3370 maxWidth: null, 3371 // used to accomodate the arrow of tooltip if there is one. 3372 // First to make sure that the arrow target is not too close 3373 // to the edge of the tooltip, so the arrow does not overflow 3374 // the tooltip. Secondly when we reposition the tooltip to 3375 // make sure that it's positioned in such a way that the arrow is 3376 // still pointing at the target (and not a few pixels beyond it). 3377 // It should be equal to or greater than half the width of 3378 // the arrow (by width we mean the size of the side which touches 3379 // the side of the tooltip). 3380 minIntersection: 16, 3381 minWidth: 0, 3382 // deprecated in 4.0.0. Listed for _optionsExtract to pick it up 3383 position: null, 3384 side: 'top', 3385 // set to false to position the tooltip relatively to the document rather 3386 // than the window when we open it 3387 viewportAware: true 3388 }; 3389 }, 3390 3391 /** 3392 * Run once: at instantiation of the plugin 3393 * 3394 * @param {object} instance The tooltipster object that instantiated this plugin 3395 * @private 3396 */ 3397 __init: function (instance) { 3398 3399 var self = this; 3400 3401 // list of instance variables 3402 3403 self.__instance = instance; 3404 self.__namespace = 'tooltipster-sideTip-' + Math.round(Math.random() * 1000000); 3405 self.__previousState = 'closed'; 3406 self.__options; 3407 3408 // initial formatting 3409 self.__optionsFormat(); 3410 3411 self.__instance._on('state.' + self.__namespace, function (event) { 3412 3413 if (event.state == 'closed') { 3414 self.__close(); 3415 } 3416 else if (event.state == 'appearing' && self.__previousState == 'closed') { 3417 self.__create(); 3418 } 3419 3420 self.__previousState = event.state; 3421 }); 3422 3423 // reformat every time the options are changed 3424 self.__instance._on('options.' + self.__namespace, function () { 3425 self.__optionsFormat(); 3426 }); 3427 3428 self.__instance._on('reposition.' + self.__namespace, function (e) { 3429 self.__reposition(e.event, e.helper); 3430 }); 3431 }, 3432 3433 /** 3434 * Called when the tooltip has closed 3435 * 3436 * @private 3437 */ 3438 __close: function () { 3439 3440 // detach our content object first, so the next jQuery's remove() 3441 // call does not unbind its event handlers 3442 if (this.__instance.content() instanceof $) { 3443 this.__instance.content().detach(); 3444 } 3445 3446 // remove the tooltip from the DOM 3447 this.__instance._$tooltip.remove(); 3448 this.__instance._$tooltip = null; 3449 }, 3450 3451 /** 3452 * Creates the HTML element of the tooltip. 3453 * 3454 * @private 3455 */ 3456 __create: function () { 3457 3458 // note: we wrap with a .tooltipster-box div to be able to set a margin on it 3459 // (.tooltipster-base must not have one) 3460 var $html = $( 3461 '<div class="tooltipster-base tooltipster-sidetip ' + this.__options.ppclass + ' pp-tooltipster-active">' + 3462 '<div class="tooltipster-box">' + 3463 '<div class="tooltipster-content"></div>' + 3464 '</div>' + 3465 '<div class="tooltipster-arrow">' + 3466 '<div class="tooltipster-arrow-uncropped">' + 3467 '<div class="tooltipster-arrow-border"></div>' + 3468 '<div class="tooltipster-arrow-background"></div>' + 3469 '</div>' + 3470 '</div>' + 3471 '</div>' 3472 ); 3473 3474 // hide arrow if asked 3475 if (!this.__options.arrow) { 3476 $html 3477 .find('.tooltipster-box') 3478 .css('margin', 0) 3479 .end() 3480 .find('.tooltipster-arrow') 3481 .hide(); 3482 } 3483 3484 // apply min/max width if asked 3485 if (this.__options.minWidth) { 3486 $html.css('min-width', this.__options.minWidth + 'px'); 3487 } 3488 if (this.__options.maxWidth) { 3489 $html.css('max-width', this.__options.maxWidth + 'px'); 3490 } 3491 3492 this.__instance._$tooltip = $html; 3493 3494 // tell the instance that the tooltip element has been created 3495 this.__instance._trigger('created'); 3496 }, 3497 3498 /** 3499 * Used when the plugin is to be unplugged 3500 * 3501 * @private 3502 */ 3503 __destroy: function () { 3504 this.__instance._off('.' + self.__namespace); 3505 }, 3506 3507 /** 3508 * (Re)compute this.__options from the options declared to the instance 3509 * 3510 * @private 3511 */ 3512 __optionsFormat: function () { 3513 3514 var self = this; 3515 3516 // get the options 3517 self.__options = self.__instance._optionsExtract(pluginName, self.__defaults()); 3518 3519 // for backward compatibility, deprecated in v4.0.0 3520 if (self.__options.position) { 3521 self.__options.side = self.__options.position; 3522 } 3523 3524 // options formatting 3525 3526 // format distance as a four-cell array if it ain't one yet and then make 3527 // it an object with top/bottom/left/right properties 3528 if (typeof self.__options.distance != 'object') { 3529 self.__options.distance = [self.__options.distance]; 3530 } 3531 if (self.__options.distance.length < 4) { 3532 3533 if (self.__options.distance[1] === undefined) self.__options.distance[1] = self.__options.distance[0]; 3534 if (self.__options.distance[2] === undefined) self.__options.distance[2] = self.__options.distance[0]; 3535 if (self.__options.distance[3] === undefined) self.__options.distance[3] = self.__options.distance[1]; 3536 3537 self.__options.distance = { 3538 top: self.__options.distance[0], 3539 right: self.__options.distance[1], 3540 bottom: self.__options.distance[2], 3541 left: self.__options.distance[3] 3542 }; 3543 } 3544 3545 // let's transform: 3546 // 'top' into ['top', 'bottom', 'right', 'left'] 3547 // 'right' into ['right', 'left', 'top', 'bottom'] 3548 // 'bottom' into ['bottom', 'top', 'right', 'left'] 3549 // 'left' into ['left', 'right', 'top', 'bottom'] 3550 if (typeof self.__options.side == 'string') { 3551 3552 var opposites = { 3553 'top': 'bottom', 3554 'right': 'left', 3555 'bottom': 'top', 3556 'left': 'right' 3557 }; 3558 3559 self.__options.side = [self.__options.side, opposites[self.__options.side]]; 3560 3561 if (self.__options.side[0] == 'left' || self.__options.side[0] == 'right') { 3562 self.__options.side.push('top', 'bottom'); 3563 } 3564 else { 3565 self.__options.side.push('right', 'left'); 3566 } 3567 } 3568 3569 // misc 3570 // disable the arrow in IE6 unless the arrow option was explicitly set to true 3571 if ($.tooltipster._env.IE === 6 3572 && self.__options.arrow !== true 3573 ) { 3574 self.__options.arrow = false; 3575 } 3576 }, 3577 3578 /** 3579 * This method must compute and set the positioning properties of the 3580 * tooltip (left, top, width, height, etc.). It must also make sure the 3581 * tooltip is eventually appended to its parent (since the element may be 3582 * detached from the DOM at the moment the method is called). 3583 * 3584 * We'll evaluate positioning scenarios to find which side can contain the 3585 * tooltip in the best way. We'll consider things relatively to the window 3586 * (unless the user asks not to), then to the document (if need be, or if the 3587 * user explicitly requires the tests to run on the document). For each 3588 * scenario, measures are taken, allowing us to know how well the tooltip 3589 * is going to fit. After that, a sorting function will let us know what 3590 * the best scenario is (we also allow the user to choose his favorite 3591 * scenario by using an event). 3592 * 3593 * @param {object} helper An object that contains variables that plugin 3594 * creators may find useful (see below) 3595 * @param {object} helper.geo An object with many layout properties 3596 * about objects of interest (window, document, origin). This should help 3597 * plugin users compute the optimal position of the tooltip 3598 * @private 3599 */ 3600 __reposition: function (event, helper) { 3601 3602 var self = this, 3603 finalResult, 3604 // to know where to put the tooltip, we need to know on which point 3605 // of the x or y axis we should center it. That coordinate is the target 3606 targets = self.__targetFind(helper), 3607 testResults = []; 3608 3609 // make sure the tooltip is detached while we make tests on a clone 3610 self.__instance._$tooltip.detach(); 3611 3612 // we could actually provide the original element to the Ruler and 3613 // not a clone, but it just feels right to keep it out of the 3614 // machinery. 3615 var $clone = self.__instance._$tooltip.clone(), 3616 // start position tests session 3617 ruler = $.tooltipster._getRuler($clone), 3618 satisfied = false, 3619 animation = self.__instance.option('animation'); 3620 3621 // an animation class could contain properties that distort the size 3622 if (animation) { 3623 $clone.removeClass('tooltipster-' + animation); 3624 } 3625 3626 // start evaluating scenarios 3627 $.each(['window', 'document'], function (i, container) { 3628 3629 var takeTest = null; 3630 3631 // let the user decide to keep on testing or not 3632 self.__instance._trigger({ 3633 container: container, 3634 helper: helper, 3635 satisfied: satisfied, 3636 takeTest: function (bool) { 3637 takeTest = bool; 3638 }, 3639 results: testResults, 3640 type: 'positionTest' 3641 }); 3642 3643 if (takeTest == true 3644 || (takeTest != false 3645 && satisfied == false 3646 // skip the window scenarios if asked. If they are reintegrated by 3647 // the callback of the positionTest event, they will have to be 3648 // excluded using the callback of positionTested 3649 && (container != 'window' || self.__options.viewportAware) 3650 ) 3651 ) { 3652 3653 // for each allowed side 3654 for (var i = 0; i < self.__options.side.length; i++) { 3655 3656 var distance = { 3657 horizontal: 0, 3658 vertical: 0 3659 }, 3660 side = self.__options.side[i]; 3661 3662 if (side == 'top' || side == 'bottom') { 3663 distance.vertical = self.__options.distance[side]; 3664 } 3665 else { 3666 distance.horizontal = self.__options.distance[side]; 3667 } 3668 3669 // this may have an effect on the size of the tooltip if there are css 3670 // rules for the arrow or something else 3671 self.__sideChange($clone, side); 3672 3673 $.each(['natural', 'constrained'], function (i, mode) { 3674 3675 takeTest = null; 3676 3677 // emit an event on the instance 3678 self.__instance._trigger({ 3679 container: container, 3680 event: event, 3681 helper: helper, 3682 mode: mode, 3683 results: testResults, 3684 satisfied: satisfied, 3685 side: side, 3686 takeTest: function (bool) { 3687 takeTest = bool; 3688 }, 3689 type: 'positionTest' 3690 }); 3691 3692 if (takeTest == true 3693 || (takeTest != false 3694 && satisfied == false 3695 ) 3696 ) { 3697 3698 var testResult = { 3699 container: container, 3700 // we let the distance as an object here, it can make things a little easier 3701 // during the user's calculations at positionTest/positionTested 3702 distance: distance, 3703 // whether the tooltip can fit in the size of the viewport (does not mean 3704 // that we'll be able to make it initially entirely visible, see 'whole') 3705 fits: null, 3706 mode: mode, 3707 outerSize: null, 3708 side: side, 3709 size: null, 3710 target: targets[side], 3711 // check if the origin has enough surface on screen for the tooltip to 3712 // aim at it without overflowing the viewport (this is due to the thickness 3713 // of the arrow represented by the minIntersection length). 3714 // If not, the tooltip will have to be partly or entirely off screen in 3715 // order to stay docked to the origin. This value will stay null when the 3716 // container is the document, as it is not relevant 3717 whole: null 3718 }; 3719 3720 // get the size of the tooltip with or without size constraints 3721 var rulerConfigured = (mode == 'natural') ? 3722 ruler.free() : 3723 ruler.constrain( 3724 helper.geo.available[container][side].width - distance.horizontal, 3725 helper.geo.available[container][side].height - distance.vertical 3726 ), 3727 rulerResults = rulerConfigured.measure(); 3728 3729 testResult.size = rulerResults.size; 3730 testResult.outerSize = { 3731 height: rulerResults.size.height + distance.vertical, 3732 width: rulerResults.size.width + distance.horizontal 3733 }; 3734 3735 if (mode == 'natural') { 3736 3737 if (helper.geo.available[container][side].width >= testResult.outerSize.width 3738 && helper.geo.available[container][side].height >= testResult.outerSize.height 3739 ) { 3740 testResult.fits = true; 3741 } 3742 else { 3743 testResult.fits = false; 3744 } 3745 } 3746 else { 3747 testResult.fits = rulerResults.fits; 3748 } 3749 3750 if (container == 'window') { 3751 3752 if (!testResult.fits) { 3753 testResult.whole = false; 3754 } 3755 else { 3756 if (side == 'top' || side == 'bottom') { 3757 3758 testResult.whole = ( 3759 helper.geo.origin.windowOffset.right >= self.__options.minIntersection 3760 && helper.geo.window.size.width - helper.geo.origin.windowOffset.left >= self.__options.minIntersection 3761 ); 3762 } 3763 else { 3764 testResult.whole = ( 3765 helper.geo.origin.windowOffset.bottom >= self.__options.minIntersection 3766 && helper.geo.window.size.height - helper.geo.origin.windowOffset.top >= self.__options.minIntersection 3767 ); 3768 } 3769 } 3770 } 3771 3772 testResults.push(testResult); 3773 3774 // we don't need to compute more positions if we have one fully on screen 3775 if (testResult.whole) { 3776 satisfied = true; 3777 } 3778 else { 3779 // don't run the constrained test unless the natural width was greater 3780 // than the available width, otherwise it's pointless as we know it 3781 // wouldn't fit either 3782 if (testResult.mode == 'natural' 3783 && (testResult.fits 3784 || testResult.size.width <= helper.geo.available[container][side].width 3785 ) 3786 ) { 3787 return false;
3788 } 3789 } 3790 } 3791 }); 3792 } 3793 } 3794 }); 3795 3796 // the user may eliminate the unwanted scenarios from testResults, but he's 3797 // not supposed to alter them at this point. functionPosition and the 3798 // position event serve that purpose. 3799 self.__instance._trigger({ 3800 edit: function (r) { 3801 testResults = r; 3802 }, 3803 event: event, 3804 helper: helper, 3805 results: testResults, 3806 type: 'positionTested' 3807 }); 3808 3809 /** 3810 * Sort the scenarios to find the favorite one. 3811 * 3812 * The favorite scenario is when we can fully display the tooltip on screen, 3813 * even if it means that the middle of the tooltip is no longer centered on 3814 * the middle of the origin (when the origin is near the edge of the screen 3815 * or even partly off screen). We want the tooltip on the preferred side, 3816 * even if it means that we have to use a constrained size rather than a 3817 * natural one (as long as it fits). When the origin is off screen at the top 3818 * the tooltip will be positioned at the bottom (if allowed), if the origin 3819 * is off screen on the right, it will be positioned on the left, etc. 3820 * If there are no scenarios where the tooltip can fit on screen, or if the 3821 * user does not want the tooltip to fit on screen (viewportAware == false), 3822 * we fall back to the scenarios relative to the document. 3823 * 3824 * When the tooltip is bigger than the viewport in either dimension, we stop 3825 * looking at the window scenarios and consider the document scenarios only, 3826 * with the same logic to find on which side it would fit best. 3827 * 3828 * If the tooltip cannot fit the document on any side, we force it at the 3829 * bottom, so at least the user can scroll to see it. 3830 */ 3831 testResults.sort(function (a, b) { 3832
vendor: 14,643 bytes, lines 3833-4276
3833 // best if it's whole (the tooltip fits and adapts to the viewport) 3834 if (a.whole && !b.whole) { 3835 return -1; 3836 } 3837 else if (!a.whole && b.whole) { 3838 return 1; 3839 } 3840 else if (a.whole && b.whole) { 3841 3842 var ai = self.__options.side.indexOf(a.side), 3843 bi = self.__options.side.indexOf(b.side); 3844 3845 // use the user's sides fallback array 3846 if (ai < bi) { 3847 return -1; 3848 } 3849 else if (ai > bi) { 3850 return 1; 3851 } 3852 else { 3853 // will be used if the user forced the tests to continue 3854 return a.mode == 'natural' ? -1 : 1; 3855 } 3856 } 3857 else { 3858 3859 // better if it fits 3860 if (a.fits && !b.fits) { 3861 return -1; 3862 } 3863 else if (!a.fits && b.fits) { 3864 return 1; 3865 } 3866 else if (a.fits && b.fits) { 3867 3868 var ai = self.__options.side.indexOf(a.side), 3869 bi = self.__options.side.indexOf(b.side); 3870 3871 // use the user's sides fallback array 3872 if (ai < bi) { 3873 return -1; 3874 } 3875 else if (ai > bi) { 3876 return 1; 3877 } 3878 else { 3879 // will be used if the user forced the tests to continue 3880 return a.mode == 'natural' ? -1 : 1; 3881 } 3882 } 3883 else { 3884 3885 // if everything failed, this will give a preference to the case where 3886 // the tooltip overflows the document at the bottom 3887 if (a.container == 'document' 3888 && a.side == 'bottom' 3889 && a.mode == 'natural' 3890 ) { 3891 return -1; 3892 } 3893 else { 3894 return 1; 3895 } 3896 } 3897 } 3898 }); 3899 3900 finalResult = testResults[0]; 3901 3902 3903 // now let's find the coordinates of the tooltip relatively to the window 3904 finalResult.coord = {}; 3905 3906 switch (finalResult.side) { 3907 3908 case 'left': 3909 case 'right': 3910 finalResult.coord.top = Math.floor(finalResult.target - finalResult.size.height / 2); 3911 break; 3912 3913 case 'bottom': 3914 case 'top': 3915 finalResult.coord.left = Math.floor(finalResult.target - finalResult.size.width / 2); 3916 break; 3917 } 3918 3919 switch (finalResult.side) { 3920 3921 case 'left': 3922 finalResult.coord.left = helper.geo.origin.windowOffset.left - finalResult.outerSize.width; 3923 break; 3924 3925 case 'right': 3926 finalResult.coord.left = helper.geo.origin.windowOffset.right + finalResult.distance.horizontal; 3927 break; 3928 3929 case 'top': 3930 finalResult.coord.top = helper.geo.origin.windowOffset.top - finalResult.outerSize.height; 3931 break; 3932 3933 case 'bottom': 3934 finalResult.coord.top = helper.geo.origin.windowOffset.bottom + finalResult.distance.vertical; 3935 break; 3936 } 3937 3938 // if the tooltip can potentially be contained within the viewport dimensions 3939 // and that we are asked to make it fit on screen 3940 if (finalResult.container == 'window') { 3941 3942 // if the tooltip overflows the viewport, we'll move it accordingly (then it will 3943 // not be centered on the middle of the origin anymore). We only move horizontally 3944 // for top and bottom tooltips and vice versa. 3945 if (finalResult.side == 'top' || finalResult.side == 'bottom') { 3946 3947 // if there is an overflow on the left 3948 if (finalResult.coord.left < 0) { 3949 3950 // prevent the overflow unless the origin itself gets off screen (minus the 3951 // margin needed to keep the arrow pointing at the target) 3952 if (helper.geo.origin.windowOffset.right - this.__options.minIntersection >= 0) { 3953 finalResult.coord.left = 0; 3954 } 3955 else { 3956 finalResult.coord.left = helper.geo.origin.windowOffset.right - this.__options.minIntersection - 1; 3957 } 3958 } 3959 // or an overflow on the right 3960 else if (finalResult.coord.left > helper.geo.window.size.width - finalResult.size.width) { 3961 3962 if (helper.geo.origin.windowOffset.left + this.__options.minIntersection <= helper.geo.window.size.width) { 3963 finalResult.coord.left = helper.geo.window.size.width - finalResult.size.width; 3964 } 3965 else { 3966 finalResult.coord.left = helper.geo.origin.windowOffset.left + this.__options.minIntersection + 1 - finalResult.size.width; 3967 } 3968 } 3969 } 3970 else { 3971 3972 // overflow at the top 3973 if (finalResult.coord.top < 0) { 3974 3975 if (helper.geo.origin.windowOffset.bottom - this.__options.minIntersection >= 0) { 3976 finalResult.coord.top = 0; 3977 } 3978 else { 3979 finalResult.coord.top = helper.geo.origin.windowOffset.bottom - this.__options.minIntersection - 1; 3980 } 3981 } 3982 // or at the bottom 3983 else if (finalResult.coord.top > helper.geo.window.size.height - finalResult.size.height) { 3984 3985 if (helper.geo.origin.windowOffset.top + this.__options.minIntersection <= helper.geo.window.size.height) { 3986 finalResult.coord.top = helper.geo.window.size.height - finalResult.size.height; 3987 } 3988 else { 3989 finalResult.coord.top = helper.geo.origin.windowOffset.top + this.__options.minIntersection + 1 - finalResult.size.height; 3990 } 3991 } 3992 } 3993 } 3994 else { 3995 3996 // there might be overflow here too but it's easier to handle. If there has 3997 // to be an overflow, we'll make sure it's on the right side of the screen 3998 // (because the browser will extend the document size if there is an overflow 3999 // on the right, but not on the left). The sort function above has already 4000 // made sure that a bottom document overflow is preferred to a top overflow, 4001 // so we don't have to care about it. 4002 4003 // if there is an overflow on the right 4004 if (finalResult.coord.left > helper.geo.window.size.width - finalResult.size.width) { 4005 4006 // this may actually create on overflow on the left but we'll fix it in a sec 4007 finalResult.coord.left = helper.geo.window.size.width - finalResult.size.width; 4008 } 4009 4010 // if there is an overflow on the left 4011 if (finalResult.coord.left < 0) { 4012 4013 // don't care if it overflows the right after that, we made our best 4014 finalResult.coord.left = 0; 4015 } 4016 } 4017 4018 4019 // submit the positioning proposal to the user function which may choose to change 4020 // the side, size and/or the coordinates 4021 4022 // first, set the rules that corresponds to the proposed side: it may change 4023 // the size of the tooltip, and the custom functionPosition may want to detect the 4024 // size of something before making a decision. So let's make things easier for the 4025 // implementor 4026 self.__sideChange($clone, finalResult.side); 4027 4028 // add some variables to the helper 4029 helper.tooltipClone = $clone[0]; 4030 helper.tooltipParent = self.__instance.option('parent').parent[0]; 4031 // move informative values to the helper 4032 helper.mode = finalResult.mode; 4033 helper.whole = finalResult.whole; 4034 // add some variables to the helper for the functionPosition callback (these 4035 // will also be added to the event fired by self.__instance._trigger but that's 4036 // ok, we're just being consistent) 4037 helper.origin = self.__instance._$origin[0]; 4038 helper.tooltip = self.__instance._$tooltip[0]; 4039 4040 // leave only the actionable values in there for functionPosition 4041 delete finalResult.container; 4042 delete finalResult.fits; 4043 delete finalResult.mode; 4044 delete finalResult.outerSize; 4045 delete finalResult.whole; 4046 4047 // keep only the distance on the relevant side, for clarity 4048 finalResult.distance = finalResult.distance.horizontal || finalResult.distance.vertical; 4049 4050 // beginners may not be comfortable with the concept of editing the object 4051 // passed by reference, so we provide an edit function and pass a clone 4052 var finalResultClone = $.extend(true, {}, finalResult); 4053 4054 // emit an event on the instance 4055 self.__instance._trigger({ 4056 edit: function (result) { 4057 finalResult = result; 4058 }, 4059 event: event, 4060 helper: helper, 4061 position: finalResultClone, 4062 type: 'position' 4063 }); 4064 4065 if (self.__options.functionPosition) { 4066 4067 var result = self.__options.functionPosition.call(self, self.__instance, helper, finalResultClone); 4068 4069 if (result) finalResult = result; 4070 } 4071 4072 // end the positioning tests session (the user might have had a 4073 // use for it during the position event, now it's over) 4074 ruler.destroy(); 4075 4076 // compute the position of the target relatively to the tooltip root 4077 // element so we can place the arrow and make the needed adjustments 4078 var arrowCoord, 4079 maxVal; 4080 4081 if (finalResult.side == 'top' || finalResult.side == 'bottom') { 4082 4083 arrowCoord = { 4084 prop: 'left', 4085 val: finalResult.target - finalResult.coord.left 4086 }; 4087 maxVal = finalResult.size.width - this.__options.minIntersection; 4088 } 4089 else { 4090 4091 arrowCoord = { 4092 prop: 'top', 4093 val: finalResult.target - finalResult.coord.top 4094 }; 4095 maxVal = finalResult.size.height - this.__options.minIntersection; 4096 } 4097 4098 // cannot lie beyond the boundaries of the tooltip, minus the 4099 // arrow margin 4100 if (arrowCoord.val < this.__options.minIntersection) { 4101 arrowCoord.val = this.__options.minIntersection; 4102 } 4103 else if (arrowCoord.val > maxVal) { 4104 arrowCoord.val = maxVal; 4105 } 4106 4107 var originParentOffset; 4108 4109 // let's convert the window-relative coordinates into coordinates relative to the 4110 // future positioned parent that the tooltip will be appended to 4111 if (helper.geo.origin.fixedLineage) { 4112 4113 // same as windowOffset when the position is fixed 4114 originParentOffset = helper.geo.origin.windowOffset; 4115 } 4116 else { 4117 4118 // this assumes that the parent of the tooltip is located at 4119 // (0, 0) in the document, typically like when the parent is 4120 // <body>. 4121 // If we ever allow other types of parent, .tooltipster-ruler 4122 // will have to be appended to the parent to inherit css style 4123 // values that affect the display of the text and such. 4124 originParentOffset = { 4125 left: helper.geo.origin.windowOffset.left + helper.geo.window.scroll.left, 4126 top: helper.geo.origin.windowOffset.top + helper.geo.window.scroll.top 4127 }; 4128 } 4129 4130 finalResult.coord = { 4131 left: originParentOffset.left + (finalResult.coord.left - helper.geo.origin.windowOffset.left), 4132 top: originParentOffset.top + (finalResult.coord.top - helper.geo.origin.windowOffset.top) 4133 }; 4134 4135 // set position values on the original tooltip element 4136 4137 self.__sideChange(self.__instance._$tooltip, finalResult.side); 4138 4139 if (helper.geo.origin.fixedLineage) { 4140 self.__instance._$tooltip 4141 .css('position', 'fixed'); 4142 } 4143 else { 4144 // CSS default 4145 self.__instance._$tooltip 4146 .css('position', ''); 4147 } 4148 4149 self.__instance._$tooltip 4150 .css({ 4151 left: finalResult.coord.left, 4152 top: finalResult.coord.top, 4153 // we need to set a size even if the tooltip is in its natural size 4154 // because when the tooltip is positioned beyond the width of the body 4155 // (which is by default the width of the window; it will happen when 4156 // you scroll the window horizontally to get to the origin), its text 4157 // content will otherwise break lines at each word to keep up with the 4158 // body overflow strategy. 4159 height: finalResult.size.height, 4160 width: finalResult.size.width 4161 }) 4162 .find('.tooltipster-arrow') 4163 .css({ 4164 'left': '', 4165 'top': '' 4166 }) 4167 .css(arrowCoord.prop, arrowCoord.val); 4168 4169 // append the tooltip HTML element to its parent 4170 self.__instance._$tooltip.appendTo(self.__instance.option('parent')); 4171 4172 self.__instance._trigger({ 4173 type: 'repositioned', 4174 event: event, 4175 position: finalResult 4176 }); 4177 }, 4178 4179 /** 4180 * Make whatever modifications are needed when the side is changed. This has 4181 * been made an independant method for easy inheritance in custom plugins based 4182 * on this default plugin. 4183 * 4184 * @param {object} $obj 4185 * @param {string} side 4186 * @private 4187 */ 4188 __sideChange: function ($obj, side) { 4189 4190 $obj 4191 .removeClass('tooltipster-bottom') 4192 .removeClass('tooltipster-left') 4193 .removeClass('tooltipster-right') 4194 .removeClass('tooltipster-top') 4195 .addClass('tooltipster-' + side); 4196 }, 4197 4198 /** 4199 * Returns the target that the tooltip should aim at for a given side. 4200 * The calculated value is a distance from the edge of the window 4201 * (left edge for top/bottom sides, top edge for left/right side). The 4202 * tooltip will be centered on that position and the arrow will be 4203 * positioned there (as much as possible). 4204 * 4205 * @param {object} helper 4206 * @return {integer} 4207 * @private 4208 */ 4209 __targetFind: function (helper) { 4210 4211 var target = {}, 4212 rects = this.__instance._$origin[0].getClientRects(); 4213 4214 // these lines fix a Chrome bug (issue #491) 4215 if (rects.length > 1) { 4216 var opacity = this.__instance._$origin.css('opacity'); 4217 if (opacity == 1) { 4218 this.__instance._$origin.css('opacity', 0.99); 4219 rects = this.__instance._$origin[0].getClientRects(); 4220 this.__instance._$origin.css('opacity', 1); 4221 } 4222 } 4223 4224 // by default, the target will be the middle of the origin 4225 if (rects.length < 2) { 4226 4227 target.top = Math.floor(helper.geo.origin.windowOffset.left + (helper.geo.origin.size.width / 2)); 4228 target.bottom = target.top; 4229 4230 target.left = Math.floor(helper.geo.origin.windowOffset.top + (helper.geo.origin.size.height / 2)); 4231 target.right = target.left; 4232 } 4233 // if multiple client rects exist, the element may be text split 4234 // up into multiple lines and the middle of the origin may not be 4235 // best option anymore. We need to choose the best target client rect 4236 else { 4237 4238 // top: the first 4239 var targetRect = rects[0]; 4240 target.top = Math.floor(targetRect.left + (targetRect.right - targetRect.left) / 2); 4241 4242 // right: the middle line, rounded down in case there is an even 4243 // number of lines (looks more centered => check out the 4244 // demo with 4 split lines) 4245 if (rects.length > 2) { 4246 targetRect = rects[Math.ceil(rects.length / 2) - 1]; 4247 } 4248 else { 4249 targetRect = rects[0]; 4250 } 4251 target.right = Math.floor(targetRect.top + (targetRect.bottom - targetRect.top) / 2); 4252 4253 // bottom: the last 4254 targetRect = rects[rects.length - 1]; 4255 target.bottom = Math.floor(targetRect.left + (targetRect.right - targetRect.left) / 2); 4256 4257 // left: the middle line, rounded up 4258 if (rects.length > 2) { 4259 targetRect = rects[Math.ceil((rects.length + 1) / 2) - 1]; 4260 } 4261 else { 4262 targetRect = rects[rects.length - 1]; 4263 } 4264 4265 target.left = Math.floor(targetRect.top + (targetRect.bottom - targetRect.top) / 2); 4266 } 4267 4268 return target; 4269 } 4270 } 4271 }); 4272 4273 /* a build task will add "return $;" here */ 4274 return $; 4275 4276}));
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.