vendor: 68,153 bytes, lines 1-2424
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 25var defaults = { 26 animation: 'fade', 27 animationDuration: 350, 28 content: null, 29 contentAsHTML: false, 30 contentCloning: false, 31 debug: true, 32 delay: 300, 33 delayTouch: [300, 500], 34 functionInit: null, 35 functionBefore: null, 36 functionReady: null, 37 functionAfter: null, 38 functionFormat: null, 39 IEmin: 6, 40 interactive: false, 41 multiple: false, 42 // will default to document.body, or must be an element positioned at (0, 0) 43 // in the document, typically like the very top views of an app. 44 parent: null, 45 plugins: ['sideTip'], 46 repositionOnScroll: false, 47 restoration: 'none', 48 selfDestruction: true, 49 theme: [], 50 timer: 0, 51 trackerInterval: 500, 52 trackOrigin: false, 53 trackTooltip: false, 54 trigger: 'hover', 55 triggerClose: { 56 click: false, 57 mouseleave: false, 58 originClick: false, 59 scroll: false, 60 tap: false, 61 touchleave: false 62 }, 63 triggerOpen: { 64 click: false, 65 mouseenter: false, 66 tap: false, 67 touchstart: false 68 }, 69 updateAnimation: 'rotate', 70 zIndex: 9999999 71 }, 72 // we'll avoid using the 'window' global as a good practice but npm's 73 // jquery@<2.1.0 package actually requires a 'window' global, so not sure 74 // it's useful at all 75 win = (typeof window != 'undefined') ? window : null, 76 // env will be proxied by the core for plugins to have access its properties 77 env = { 78 // detect if this device can trigger touch events. Better have a false 79 // positive (unused listeners, that's ok) than a false negative. 80 // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/touchevents.js 81 // http://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript 82 hasTouchCapability: !!( 83 win 84 && ( 'ontouchstart' in win 85 || (win.DocumentTouch && win.document instanceof win.DocumentTouch) 86 || win.navigator.maxTouchPoints 87 ) 88 ), 89 hasTransitions: transitionSupport(), 90 IE: false, 91 // don't set manually, it will be updated by a build task after the manifest 92 semVer: '4.2.6', 93 window: win 94 }, 95 core = function() { 96 97 // core variables 98 99 // the core emitters 100 this.__$emitterPrivate = $({}); 101 this.__$emitterPublic = $({}); 102 this.__instancesLatestArr = []; 103 // collects plugin constructors 104 this.__plugins = {}; 105 // proxy env variables for plugins who might use them 106 this._env = env; 107 }; 108 109// core methods 110core.prototype = { 111 112 /** 113 * A function to proxy the public methods of an object onto another 114 * 115 * @param {object} constructor The constructor to bridge 116 * @param {object} obj The object that will get new methods (an instance or the core) 117 * @param {string} pluginName A plugin name for the console log message 118 * @return {core} 119 * @private 120 */ 121 __bridge: function(constructor, obj, pluginName) { 122 123 // if it's not already bridged 124 if (!obj[pluginName]) { 125 126 var fn = function() {}; 127 fn.prototype = constructor; 128 129 var pluginInstance = new fn(); 130 131 // the _init method has to exist in instance constructors but might be missing 132 // in core constructors 133 if (pluginInstance.__init) { 134 pluginInstance.__init(obj); 135 } 136 137 $.each(constructor, function(methodName, fn) { 138 139 // don't proxy "private" methods, only "protected" and public ones 140 if (methodName.indexOf('__') != 0) { 141 142 // if the method does not exist yet 143 if (!obj[methodName]) { 144 145 obj[methodName] = function() { 146 return pluginInstance[methodName].apply(pluginInstance, Array.prototype.slice.apply(arguments)); 147 }; 148 149 // remember to which plugin this method corresponds (several plugins may 150 // have methods of the same name, we need to be sure) 151 obj[methodName].bridged = pluginInstance; 152 } 153 else if (defaults.debug) { 154 155 //console.log('The '+ methodName +' method of the '+ pluginName +' plugin conflicts with another plugin or native methods'); 156 } 157 } 158 }); 159 160 obj[pluginName] = pluginInstance; 161 } 162 163 return this; 164 }, 165 166 /** 167 * For mockup in Node env if need be, for testing purposes 168 * 169 * @return {core} 170 * @private 171 */ 172 __setWindow: function(window) { 173 env.window = window; 174 return this; 175 }, 176 177 /** 178 * Returns a ruler, a tool to help measure the size of a tooltip under 179 * various settings. Meant for plugins 180 * 181 * @see Ruler 182 * @return {object} A Ruler instance 183 * @protected 184 */ 185 _getRuler: function($tooltip) { 186 return new Ruler($tooltip); 187 }, 188 189 /** 190 * For internal use by plugins, if needed 191 * 192 * @return {core} 193 * @protected 194 */ 195 _off: function() { 196 this.__$emitterPrivate.off.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments)); 197 return this; 198 }, 199 200 /** 201 * For internal use by plugins, if needed 202 * 203 * @return {core} 204 * @protected 205 */ 206 _on: function() { 207 this.__$emitterPrivate.on.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments)); 208 return this; 209 }, 210 211 /** 212 * For internal use by plugins, if needed 213 * 214 * @return {core} 215 * @protected 216 */ 217 _one: function() { 218 this.__$emitterPrivate.one.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments)); 219 return this; 220 }, 221 222 /** 223 * Returns (getter) or adds (setter) a plugin 224 * 225 * @param {string|object} plugin Provide a string (in the full form 226 * "namespace.name") to use as as getter, an object to use as a setter 227 * @return {object|core} 228 * @protected 229 */ 230 _plugin: function(plugin) { 231 232 var self = this; 233 234 // getter 235 if (typeof plugin == 'string') { 236 237 var pluginName = plugin, 238 p = null; 239 240 // if the namespace is provided, it's easy to search 241 if (pluginName.indexOf('.') > 0) { 242 p = self.__plugins[pluginName]; 243 } 244 // otherwise, return the first name that matches 245 else { 246 $.each(self.__plugins, function(i, plugin) { 247 248 if (plugin.name.substring(plugin.name.length - pluginName.length - 1) == '.'+ pluginName) { 249 p = plugin; 250 return false; 251 } 252 }); 253 } 254 255 return p; 256 } 257 // setter 258 else { 259 260 // force namespaces 261 if (plugin.name.indexOf('.') < 0) { 262 throw new Error('Plugins must be namespaced'); 263 } 264 265 self.__plugins[plugin.name] = plugin; 266 267 // if the plugin has core features 268 if (plugin.core) { 269 270 // bridge non-private methods onto the core to allow new core methods 271 self.__bridge(plugin.core, self, plugin.name); 272 } 273 274 return this; 275 } 276 }, 277 278 /** 279 * Trigger events on the core emitters 280 * 281 * @returns {core} 282 * @protected 283 */ 284 _trigger: function() { 285 286 var args = Array.prototype.slice.apply(arguments); 287 288 if (typeof args[0] == 'string') { 289 args[0] = { type: args[0] }; 290 } 291 292 // note: the order of emitters matters 293 this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate, args); 294 this.__$emitterPublic.trigger.apply(this.__$emitterPublic, args); 295 296 return this; 297 }, 298 299 /** 300 * Returns instances of all tooltips in the page or an a given element 301 * 302 * @param {string|HTML object collection} selector optional Use this 303 * parameter to restrict the set of objects that will be inspected 304 * for the retrieval of instances. By default, all instances in the 305 * page are returned. 306 * @return {array} An array of instance objects 307 * @public 308 */ 309 instances: function(selector) { 310 311 var instances = [], 312 sel = selector || '.tooltipstered'; 313 314 $(sel).each(function() { 315 316 var $this = $(this), 317 ns = $this.data('tooltipster-ns'); 318 319 if (ns) { 320 321 $.each(ns, function(i, namespace) { 322 instances.push($this.data(namespace)); 323 }); 324 } 325 }); 326 327 return instances; 328 }, 329 330 /** 331 * Returns the Tooltipster objects generated by the last initializing call 332 * 333 * @return {array} An array of instance objects 334 * @public 335 */ 336 instancesLatest: function() { 337 return this.__instancesLatestArr; 338 }, 339 340 /** 341 * For public use only, not to be used by plugins (use ::_off() instead) 342 * 343 * @return {core} 344 * @public 345 */ 346 off: function() { 347 this.__$emitterPublic.off.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); 348 return this; 349 }, 350 351 /** 352 * For public use only, not to be used by plugins (use ::_on() instead) 353 * 354 * @return {core} 355 * @public 356 */ 357 on: function() { 358 this.__$emitterPublic.on.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); 359 return this; 360 }, 361 362 /** 363 * For public use only, not to be used by plugins (use ::_one() instead) 364 * 365 * @return {core} 366 * @public 367 */ 368 one: function() { 369 this.__$emitterPublic.one.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); 370 return this; 371 }, 372 373 /** 374 * Returns all HTML elements which have one or more tooltips 375 * 376 * @param {string} selector optional Use this to restrict the results 377 * to the descendants of an element 378 * @return {array} An array of HTML elements 379 * @public 380 */ 381 origins: function(selector) { 382 383 var sel = selector ? 384 selector +' ' : 385 ''; 386 387 return $(sel +'.tooltipstered').toArray(); 388 }, 389 390 /** 391 * Change default options for all future instances 392 * 393 * @param {object} d The options that should be made defaults 394 * @return {core} 395 * @public 396 */ 397 setDefaults: function(d) { 398 $.extend(defaults, d); 399 return this; 400 }, 401 402 /** 403 * For users to trigger their handlers on the public emitter 404 * 405 * @returns {core} 406 * @public 407 */ 408 triggerHandler: function() { 409 this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); 410 return this; 411 } 412}; 413 414// $.tooltipster will be used to call core methods 415$.tooltipster = new core(); 416 417// the Tooltipster instance class (mind the capital T) 418$.Tooltipster = function(element, options) { 419 420 // list of instance variables 421 422 // stack of custom callbacks provided as parameters to API methods 423 this.__callbacks = { 424 close: [], 425 open: [] 426 }; 427 // the schedule time of DOM removal 428 this.__closingTime; 429 // this will be the user content shown in the tooltip. A capital "C" is used 430 // because there is also a method called content() 431 this.__Content; 432 // for the size tracker 433 this.__contentBcr; 434 // to disable the tooltip after destruction 435 this.__destroyed = false; 436 // we can't emit directly on the instance because if a method with the same 437 // name as the event exists, it will be called by jQuery. Se we use a plain 438 // object as emitter. This emitter is for internal use by plugins, 439 // if needed. 440 this.__$emitterPrivate = $({}); 441 // this emitter is for the user to listen to events without risking to mess 442 // with our internal listeners 443 this.__$emitterPublic = $({}); 444 this.__enabled = true; 445 // the reference to the gc interval 446 this.__garbageCollector; 447 // various position and size data recomputed before each repositioning 448 this.__Geometry; 449 // the tooltip position, saved after each repositioning by a plugin 450 this.__lastPosition; 451 // a unique namespace per instance 452 this.__namespace = 'tooltipster-'+ Math.round(Math.random()*1000000); 453 this.__options; 454 // will be used to support origins in scrollable areas 455 this.__$originParents; 456 this.__pointerIsOverOrigin = false; 457 // to remove themes if needed 458 this.__previousThemes = []; 459 // the state can be either: appearing, stable, disappearing, closed 460 this.__state = 'closed'; 461 // timeout references 462 this.__timeouts = { 463 close: [], 464 open: null 465 }; 466 // store touch events to be able to detect emulated mouse events 467 this.__touchEvents = []; 468 // the reference to the tracker interval 469 this.__tracker = null; 470 // the element to which this tooltip is associated 471 this._$origin; 472 // this will be the tooltip element (jQuery wrapped HTML element). 473 // It's the job of a plugin to create it and append it to the DOM 474 this._$tooltip; 475 476 // launch 477 this.__init(element, options); 478}; 479 480$.Tooltipster.prototype = { 481 482 /** 483 * @param origin 484 * @param options 485 * @private 486 */ 487 __init: function(origin, options) { 488 489 var self = this; 490 491 self._$origin = $(origin); 492 self.__options = $.extend(true, {}, defaults, options); 493 494 // some options may need to be reformatted 495 self.__optionsFormat(); 496 497 // don't run on old IE if asked no to 498 if ( !env.IE 499 || env.IE >= self.__options.IEmin 500 ) { 501 502 // note: the content is null (empty) by default and can stay that 503 // way if the plugin remains initialized but not fed any content. The 504 // tooltip will just not appear. 505 506 // let's save the initial value of the title attribute for later 507 // restoration if need be. 508 var initialTitle = null; 509 510 // it will already have been saved in case of multiple tooltips 511 if (self._$origin.data('tooltipster-initialTitle') === undefined) { 512 513 initialTitle = self._$origin.attr('title'); 514 515 // we do not want initialTitle to be "undefined" because 516 // of how jQuery's .data() method works 517 if (initialTitle === undefined) initialTitle = null; 518 519 self._$origin.data('tooltipster-initialTitle', initialTitle); 520 } 521 522 // If content is provided in the options, it has precedence over the 523 // title attribute. 524 // Note: an empty string is considered content, only 'null' represents 525 // the absence of content. 526 // Also, an existing title="" attribute will result in an empty string 527 // content 528 if (self.__options.content !== null) { 529 self.__contentSet(self.__options.content); 530 } 531 else { 532 533 var selector = self._$origin.attr('data-tooltip-content'), 534 $el; 535 536 if (selector){ 537 $el = $(selector); 538 } 539 540 if ($el && $el[0]) { 541 self.__contentSet($el.first()); 542 } 543 else { 544 self.__contentSet(initialTitle); 545 } 546 } 547 548 self._$origin 549 // strip the title off of the element to prevent the default tooltips 550 // from popping up 551 .removeAttr('title') 552 // to be able to find all instances on the page later (upon window 553 // events in particular) 554 .addClass('tooltipstered'); 555 556 // set listeners on the origin 557 self.__prepareOrigin(); 558 559 // set the garbage collector 560 self.__prepareGC(); 561 562 // init plugins 563 $.each(self.__options.plugins, function(i, pluginName) { 564 self._plug(pluginName); 565 }); 566 567 // to detect swiping 568 if (env.hasTouchCapability) { 569 $(env.window.document.body).on('touchmove.'+ self.__namespace +'-triggerOpen', function(event) { 570 self._touchRecordEvent(event); 571 }); 572 } 573 574 self 575 // prepare the tooltip when it gets created. This event must 576 // be fired by a plugin 577 ._on('created', function() { 578 self.__prepareTooltip(); 579 }) 580 // save position information when it's sent by a plugin 581 ._on('repositioned', function(e) { 582 self.__lastPosition = e.position; 583 }); 584 } 585 else { 586 self.__options.disabled = true; 587 } 588 }, 589 590 /** 591 * Insert the content into the appropriate HTML element of the tooltip 592 * 593 * @returns {self} 594 * @private 595 */ 596 __contentInsert: function() { 597 598 var self = this, 599 $el = self._$tooltip.find('.tooltipster-content'), 600 formattedContent = self.__Content, 601 format = function(content) { 602 formattedContent = content; 603 }; 604 605 self._trigger({ 606 type: 'format', 607 content: self.__Content, 608 format: format 609 }); 610 611 if (self.__options.functionFormat) { 612 613 formattedContent = self.__options.functionFormat.call( 614 self, 615 self, 616 { origin: self._$origin[0] }, 617 self.__Content 618 ); 619 } 620 621 if (typeof formattedContent === 'string' && !self.__options.contentAsHTML) { 622 $el.text(formattedContent); 623 } 624 else { 625 $el 626 .empty() 627 .append(formattedContent); 628 } 629 630 return self; 631 }, 632 633 /** 634 * Save the content, cloning it beforehand if need be 635 * 636 * @param content 637 * @returns {self} 638 * @private 639 */ 640 __contentSet: function(content) { 641 642 // clone if asked. Cloning the object makes sure that each instance has its 643 // own version of the content (in case a same object were provided for several 644 // instances) 645 // reminder: typeof null === object 646 if (content instanceof $ && this.__options.contentCloning) { 647 content = content.clone(true); 648 } 649 650 this.__Content = content; 651 652 this._trigger({ 653 type: 'updated', 654 content: content 655 }); 656 657 return this; 658 }, 659 660 /** 661 * Error message about a method call made after destruction 662 * 663 * @private 664 */ 665 __destroyError: function() { 666 throw new Error('This tooltip has been destroyed and cannot execute your method call.'); 667 }, 668 669 /** 670 * Gather all information about dimensions and available space, 671 * called before every repositioning 672 * 673 * @private 674 * @returns {object} 675 */ 676 __geometry: function() { 677 678 var self = this, 679 $target = self._$origin, 680 originIsArea = self._$origin.is('area'); 681 682 // if this._$origin is a map area, the target we'll need 683 // the dimensions of is actually the image using the map, 684 // not the area itself 685 if (originIsArea) { 686 687 var mapName = self._$origin.parent().attr('name'); 688 689 $target = $('img[usemap="#'+ mapName +'"]'); 690 } 691 692 var bcr = $target[0].getBoundingClientRect(), 693 $document = $(env.window.document), 694 $window = $(env.window), 695 $parent = $target, 696 // some useful properties of important elements 697 geo = { 698 // available space for the tooltip, see down below 699 available: { 700 document: null, 701 window: null 702 }, 703 document: { 704 size: { 705 height: $document.height(), 706 width: $document.width() 707 } 708 }, 709 window: { 710 scroll: { 711 // the second ones are for IE compatibility 712 left: env.window.scrollX || env.window.document.documentElement.scrollLeft, 713 top: env.window.scrollY || env.window.document.documentElement.scrollTop 714 }, 715 size: { 716 height: $window.height(), 717 width: $window.width() 718 } 719 }, 720 origin: { 721 // the origin has a fixed lineage if itself or one of its 722 // ancestors has a fixed position 723 fixedLineage: false, 724 // relative to the document 725 offset: {}, 726 size: { 727 height: bcr.bottom - bcr.top, 728 width: bcr.right - bcr.left 729 }, 730 usemapImage: originIsArea ? $target[0] : null, 731 // relative to the window 732 windowOffset: { 733 bottom: bcr.bottom, 734 left: bcr.left, 735 right: bcr.right, 736 top: bcr.top 737 } 738 } 739 }, 740 geoFixed = false; 741 742 // if the element is a map area, some properties may need 743 // to be recalculated 744 if (originIsArea) { 745 746 var shape = self._$origin.attr('shape'), 747 coords = self._$origin.attr('coords'); 748 749 if (coords) { 750 751 coords = coords.split(','); 752 753 $.map(coords, function(val, i) { 754 coords[i] = parseInt(val); 755 }); 756 } 757 758 // if the image itself is the area, nothing more to do 759 if (shape != 'default') { 760 761 switch(shape) { 762 763 case 'circle': 764 765 var circleCenterLeft = coords[0], 766 circleCenterTop = coords[1], 767 circleRadius = coords[2], 768 areaTopOffset = circleCenterTop - circleRadius, 769 areaLeftOffset = circleCenterLeft - circleRadius; 770 771 geo.origin.size.height = circleRadius * 2; 772 geo.origin.size.width = geo.origin.size.height; 773 774 geo.origin.windowOffset.left += areaLeftOffset; 775 geo.origin.windowOffset.top += areaTopOffset; 776 777 break; 778 779 case 'rect': 780 781 var areaLeft = coords[0], 782 areaTop = coords[1], 783 areaRight = coords[2], 784 areaBottom = coords[3]; 785 786 geo.origin.size.height = areaBottom - areaTop; 787 geo.origin.size.width = areaRight - areaLeft; 788 789 geo.origin.windowOffset.left += areaLeft; 790 geo.origin.windowOffset.top += areaTop; 791 792 break; 793 794 case 'poly': 795 796 var areaSmallestX = 0, 797 areaSmallestY = 0, 798 areaGreatestX = 0, 799 areaGreatestY = 0, 800 arrayAlternate = 'even'; 801 802 for (var i = 0; i < coords.length; i++) { 803 804 var areaNumber = coords[i]; 805 806 if (arrayAlternate == 'even') { 807 808 if (areaNumber > areaGreatestX) { 809 810 areaGreatestX = areaNumber; 811 812 if (i === 0) { 813 areaSmallestX = areaGreatestX; 814 } 815 } 816 817 if (areaNumber < areaSmallestX) { 818 areaSmallestX = areaNumber; 819 } 820 821 arrayAlternate = 'odd'; 822 } 823 else { 824 if (areaNumber > areaGreatestY) { 825 826 areaGreatestY = areaNumber; 827 828 if (i == 1) { 829 areaSmallestY = areaGreatestY; 830 } 831 } 832 833 if (areaNumber < areaSmallestY) { 834 areaSmallestY = areaNumber; 835 } 836 837 arrayAlternate = 'even'; 838 } 839 } 840 841 geo.origin.size.height = areaGreatestY - areaSmallestY; 842 geo.origin.size.width = areaGreatestX - areaSmallestX; 843 844 geo.origin.windowOffset.left += areaSmallestX; 845 geo.origin.windowOffset.top += areaSmallestY; 846 847 break; 848 } 849 } 850 } 851 852 // user callback through an event 853 var edit = function(r) { 854 geo.origin.size.height = r.height, 855 geo.origin.windowOffset.left = r.left, 856 geo.origin.windowOffset.top = r.top, 857 geo.origin.size.width = r.width 858 }; 859 860 self._trigger({ 861 type: 'geometry', 862 edit: edit, 863 geometry: { 864 height: geo.origin.size.height, 865 left: geo.origin.windowOffset.left, 866 top: geo.origin.windowOffset.top, 867 width: geo.origin.size.width 868 } 869 }); 870 871 // calculate the remaining properties with what we got 872 873 geo.origin.windowOffset.right = geo.origin.windowOffset.left + geo.origin.size.width; 874 geo.origin.windowOffset.bottom = geo.origin.windowOffset.top + geo.origin.size.height; 875 876 geo.origin.offset.left = geo.origin.windowOffset.left + geo.window.scroll.left; 877 geo.origin.offset.top = geo.origin.windowOffset.top + geo.window.scroll.top; 878 geo.origin.offset.bottom = geo.origin.offset.top + geo.origin.size.height; 879 geo.origin.offset.right = geo.origin.offset.left + geo.origin.size.width; 880 881 // the space that is available to display the tooltip relatively to the document 882 geo.available.document = { 883 bottom: { 884 height: geo.document.size.height - geo.origin.offset.bottom, 885 width: geo.document.size.width 886 }, 887 left: { 888 height: geo.document.size.height, 889 width: geo.origin.offset.left 890 }, 891 right: { 892 height: geo.document.size.height, 893 width: geo.document.size.width - geo.origin.offset.right 894 }, 895 top: { 896 height: geo.origin.offset.top, 897 width: geo.document.size.width 898 } 899 }; 900 901 // the space that is available to display the tooltip relatively to the viewport 902 // (the resulting values may be negative if the origin overflows the viewport) 903 geo.available.window = { 904 bottom: { 905 // the inner max is here to make sure the available height is no bigger 906 // than the viewport height (when the origin is off screen at the top). 907 // The outer max just makes sure that the height is not negative (when 908 // the origin overflows at the bottom). 909 height: Math.max(geo.window.size.height - Math.max(geo.origin.windowOffset.bottom, 0), 0), 910 width: geo.window.size.width 911 }, 912 left: { 913 height: geo.window.size.height, 914 width: Math.max(geo.origin.windowOffset.left, 0) 915 }, 916 right: { 917 height: geo.window.size.height, 918 width: Math.max(geo.window.size.width - Math.max(geo.origin.windowOffset.right, 0), 0) 919 }, 920 top: { 921 height: Math.max(geo.origin.windowOffset.top, 0), 922 width: geo.window.size.width 923 } 924 }; 925 926 while ($parent[0].tagName.toLowerCase() != 'html') { 927 928 if ($parent.css('position') == 'fixed') { 929 geo.origin.fixedLineage = true; 930 break; 931 } 932 933 $parent = $parent.parent(); 934 } 935 936 return geo; 937 }, 938 939 /** 940 * Some options may need to be formated before being used 941 * 942 * @returns {self} 943 * @private 944 */ 945 __optionsFormat: function() { 946 947 if (typeof this.__options.animationDuration == 'number') { 948 this.__options.animationDuration = [this.__options.animationDuration, this.__options.animationDuration]; 949 } 950 951 if (typeof this.__options.delay == 'number') { 952 this.__options.delay = [this.__options.delay, this.__options.delay]; 953 } 954 955 if (typeof this.__options.delayTouch == 'number') { 956 this.__options.delayTouch = [this.__options.delayTouch, this.__options.delayTouch]; 957 } 958 959 if (typeof this.__options.theme == 'string') { 960 this.__options.theme = [this.__options.theme]; 961 } 962 963 // determine the future parent 964 if (this.__options.parent === null) { 965 this.__options.parent = $(env.window.document.body); 966 } 967 else if (typeof this.__options.parent == 'string') { 968 this.__options.parent = $(this.__options.parent); 969 } 970 971 if (this.__options.trigger == 'hover') { 972 973 this.__options.triggerOpen = { 974 mouseenter: true, 975 touchstart: true 976 }; 977 978 this.__options.triggerClose = { 979 mouseleave: true, 980 originClick: true, 981 touchleave: true 982 }; 983 } 984 else if (this.__options.trigger == 'click') { 985 986 this.__options.triggerOpen = { 987 click: true, 988 tap: true 989 }; 990 991 this.__options.triggerClose = { 992 click: true, 993 tap: true 994 }; 995 } 996 997 // for the plugins 998 this._trigger('options'); 999 1000 return this; 1001 }, 1002 1003 /** 1004 * Schedules or cancels the garbage collector task 1005 * 1006 * @returns {self} 1007 * @private 1008 */ 1009 __prepareGC: function() { 1010 1011 var self = this; 1012 1013 // in case the selfDestruction option has been changed by a method call 1014 if (self.__options.selfDestruction) { 1015 1016 // the GC task 1017 self.__garbageCollector = setInterval(function() { 1018 1019 var now = new Date().getTime(); 1020 1021 // forget the old events 1022 self.__touchEvents = $.grep(self.__touchEvents, function(event, i) { 1023 // 1 minute 1024 return now - event.time > 60000; 1025 }); 1026 1027 // auto-destruct if the origin is gone 1028 if (!bodyContains(self._$origin)) { 1029 1030 self.close(function(){ 1031 self.destroy(); 1032 }); 1033 } 1034 }, 20000); 1035 } 1036 else { 1037 clearInterval(self.__garbageCollector); 1038 } 1039 1040 return self; 1041 }, 1042 1043 /** 1044 * Sets listeners on the origin if the open triggers require them. 1045 * Unlike the listeners set at opening time, these ones 1046 * remain even when the tooltip is closed. It has been made a 1047 * separate method so it can be called when the triggers are 1048 * changed in the options. Closing is handled in _open() 1049 * because of the bindings that may be needed on the tooltip 1050 * itself 1051 * 1052 * @returns {self} 1053 * @private 1054 */ 1055 __prepareOrigin: function() { 1056 1057 var self = this; 1058 1059 // in case we're resetting the triggers 1060 self._$origin.off('.'+ self.__namespace +'-triggerOpen'); 1061 1062 // if the device is touch capable, even if only mouse triggers 1063 // are asked, we need to listen to touch events to know if the mouse 1064 // events are actually emulated (so we can ignore them) 1065 if (env.hasTouchCapability) { 1066 1067 self._$origin.on( 1068 'touchstart.'+ self.__namespace +'-triggerOpen ' + 1069 'touchend.'+ self.__namespace +'-triggerOpen ' + 1070 'touchcancel.'+ self.__namespace +'-triggerOpen', 1071 function(event){ 1072 self._touchRecordEvent(event); 1073 } 1074 ); 1075 } 1076 1077 // mouse click and touch tap work the same way 1078 if ( self.__options.triggerOpen.click 1079 || (self.__options.triggerOpen.tap && env.hasTouchCapability) 1080 ) { 1081 1082 var eventNames = ''; 1083 if (self.__options.triggerOpen.click) { 1084 eventNames += 'click.'+ self.__namespace +'-triggerOpen '; 1085 } 1086 if (self.__options.triggerOpen.tap && env.hasTouchCapability) { 1087 eventNames += 'touchend.'+ self.__namespace +'-triggerOpen'; 1088 } 1089 1090 self._$origin.on(eventNames, function(event) { 1091 if (self._touchIsMeaningfulEvent(event)) { 1092 self._open(event); 1093 } 1094 }); 1095 } 1096 1097 // mouseenter and touch start work the same way 1098 if ( self.__options.triggerOpen.mouseenter 1099 || (self.__options.triggerOpen.touchstart && env.hasTouchCapability) 1100 ) { 1101 1102 var eventNames = ''; 1103 if (self.__options.triggerOpen.mouseenter) { 1104 eventNames += 'mouseenter.'+ self.__namespace +'-triggerOpen '; 1105 } 1106 if (self.__options.triggerOpen.touchstart && env.hasTouchCapability) { 1107 eventNames += 'touchstart.'+ self.__namespace +'-triggerOpen'; 1108 } 1109 1110 self._$origin.on(eventNames, function(event) { 1111 if ( self._touchIsTouchEvent(event) 1112 || !self._touchIsEmulatedEvent(event) 1113 ) { 1114 self.__pointerIsOverOrigin = true; 1115 self._openShortly(event); 1116 } 1117 }); 1118 } 1119 1120 // info for the mouseleave/touchleave close triggers when they use a delay 1121 if ( self.__options.triggerClose.mouseleave 1122 || (self.__options.triggerClose.touchleave && env.hasTouchCapability) 1123 ) { 1124 1125 var eventNames = ''; 1126 if (self.__options.triggerClose.mouseleave) { 1127 eventNames += 'mouseleave.'+ self.__namespace +'-triggerOpen '; 1128 } 1129 if (self.__options.triggerClose.touchleave && env.hasTouchCapability) { 1130 eventNames += 'touchend.'+ self.__namespace +'-triggerOpen touchcancel.'+ self.__namespace +'-triggerOpen'; 1131 } 1132 1133 self._$origin.on(eventNames, function(event) { 1134 1135 if (self._touchIsMeaningfulEvent(event)) { 1136 self.__pointerIsOverOrigin = false; 1137 } 1138 }); 1139 } 1140 1141 return self; 1142 }, 1143 1144 /** 1145 * Do the things that need to be done only once after the tooltip 1146 * HTML element it has been created. It has been made a separate 1147 * method so it can be called when options are changed. Remember 1148 * that the tooltip may actually exist in the DOM before it is 1149 * opened, and present after it has been closed: it's the display 1150 * plugin that takes care of handling it. 1151 * 1152 * @returns {self} 1153 * @private 1154 */ 1155 __prepareTooltip: function() { 1156 1157 var self = this, 1158 p = self.__options.interactive ? 'auto' : ''; 1159 1160 // this will be useful to know quickly if the tooltip is in 1161 // the DOM or not 1162 self._$tooltip 1163 .attr('id', self.__namespace) 1164 .css({ 1165 // pointer events 1166 'pointer-events': p, 1167 zIndex: self.__options.zIndex 1168 }); 1169 1170 // themes 1171 // remove the old ones and add the new ones 1172 $.each(self.__previousThemes, function(i, theme) { 1173 self._$tooltip.removeClass(theme); 1174 }); 1175 $.each(self.__options.theme, function(i, theme) { 1176 self._$tooltip.addClass(theme); 1177 }); 1178 1179 self.__previousThemes = $.merge([], self.__options.theme); 1180 1181 return self; 1182 }, 1183 1184 /** 1185 * Handles the scroll on any of the parents of the origin (when the 1186 * tooltip is open) 1187 * 1188 * @param {object} event 1189 * @returns {self} 1190 * @private 1191 */ 1192 __scrollHandler: function(event) { 1193 1194 var self = this; 1195 1196 if (self.__options.triggerClose.scroll) { 1197 self._close(event); 1198 } 1199 else { 1200 1201 // if the origin or tooltip have been removed: do nothing, the tracker will 1202 // take care of it later 1203 if (bodyContains(self._$origin) && bodyContains(self._$tooltip)) { 1204 1205 var geo = null; 1206 1207 // if the scroll happened on the window 1208 if (event.target === env.window.document) { 1209 1210 // if the origin has a fixed lineage, window scroll will have no 1211 // effect on its position nor on the position of the tooltip 1212 if (!self.__Geometry.origin.fixedLineage) { 1213 1214 // we don't need to do anything unless repositionOnScroll is true 1215 // because the tooltip will already have moved with the window 1216 // (and of course with the origin) 1217 if (self.__options.repositionOnScroll) { 1218 self.reposition(event); 1219 } 1220 } 1221 } 1222 // if the scroll happened on another parent of the tooltip, it means 1223 // that it's in a scrollable area and now needs to have its position 1224 // adjusted or recomputed, depending ont the repositionOnScroll 1225 // option. Also, if the origin is partly hidden due to a parent that 1226 // hides its overflow, we'll just hide (not close) the tooltip. 1227 else { 1228 1229 geo = self.__geometry(); 1230 1231 var overflows = false; 1232 1233 // a fixed position origin is not affected by the overflow hiding 1234 // of a parent 1235 if (self._$origin.css('position') != 'fixed') { 1236 1237 self.__$originParents.each(function(i, el) { 1238 1239 var $el = $(el), 1240 overflowX = $el.css('overflow-x'), 1241 overflowY = $el.css('overflow-y'); 1242 1243 if (overflowX != 'visible' || overflowY != 'visible') { 1244 1245 var bcr = el.getBoundingClientRect(); 1246 1247 if (overflowX != 'visible') { 1248 1249 if ( geo.origin.windowOffset.left < bcr.left 1250 || geo.origin.windowOffset.right > bcr.right 1251 ) { 1252 overflows = true; 1253 return false; 1254 } 1255 } 1256 1257 if (overflowY != 'visible') { 1258 1259 if ( geo.origin.windowOffset.top < bcr.top 1260 || geo.origin.windowOffset.bottom > bcr.bottom 1261 ) { 1262 overflows = true; 1263 return false; 1264 } 1265 } 1266 } 1267 1268 // no need to go further if fixed, for the same reason as above 1269 if ($el.css('position') == 'fixed') { 1270 return false; 1271 } 1272 }); 1273 } 1274 1275 if (overflows) { 1276 self._$tooltip.css('visibility', 'hidden'); 1277 } 1278 else { 1279 1280 self._$tooltip.css('visibility', 'visible'); 1281 1282 // reposition 1283 if (self.__options.repositionOnScroll) { 1284 self.reposition(event); 1285 } 1286 // or just adjust offset 1287 else { 1288 1289 // we have to use offset and not windowOffset because this way, 1290 // only the scroll distance of the scrollable areas are taken into 1291 // account (the scrolltop value of the main window must be 1292 // ignored since the tooltip already moves with it) 1293 var offsetLeft = geo.origin.offset.left - self.__Geometry.origin.offset.left, 1294 offsetTop = geo.origin.offset.top - self.__Geometry.origin.offset.top; 1295 1296 // add the offset to the position initially computed by the display plugin 1297 self._$tooltip.css({ 1298 left: self.__lastPosition.coord.left + offsetLeft, 1299 top: self.__lastPosition.coord.top + offsetTop 1300 }); 1301 } 1302 } 1303 } 1304 1305 self._trigger({ 1306 type: 'scroll', 1307 event: event, 1308 geo: geo 1309 }); 1310 } 1311 } 1312 1313 return self; 1314 }, 1315 1316 /** 1317 * Changes the state of the tooltip 1318 * 1319 * @param {string} state 1320 * @returns {self} 1321 * @private 1322 */ 1323 __stateSet: function(state) { 1324 1325 this.__state = state; 1326 1327 this._trigger({ 1328 type: 'state', 1329 state: state 1330 }); 1331 1332 return this; 1333 }, 1334 1335 /** 1336 * Clear appearance timeouts 1337 * 1338 * @returns {self} 1339 * @private 1340 */ 1341 __timeoutsClear: function() { 1342 1343 // there is only one possible open timeout: the delayed opening 1344 // when the mouseenter/touchstart open triggers are used 1345 clearTimeout(this.__timeouts.open); 1346 this.__timeouts.open = null; 1347 1348 // ... but several close timeouts: the delayed closing when the 1349 // mouseleave close trigger is used and the timer option 1350 $.each(this.__timeouts.close, function(i, timeout) { 1351 clearTimeout(timeout); 1352 }); 1353 this.__timeouts.close = []; 1354 1355 return this; 1356 }, 1357 1358 /** 1359 * Start the tracker that will make checks at regular intervals 1360 * 1361 * @returns {self} 1362 * @private 1363 */ 1364 __trackerStart: function() { 1365 1366 var self = this, 1367 $content = self._$tooltip.find('.tooltipster-content'); 1368 1369 // get the initial content size 1370 if (self.__options.trackTooltip) { 1371 self.__contentBcr = $content[0].getBoundingClientRect(); 1372 } 1373 1374 self.__tracker = setInterval(function() { 1375 1376 // if the origin or tooltip elements have been removed. 1377 // Note: we could destroy the instance now if the origin has 1378 // been removed but we'll leave that task to our garbage collector 1379 if (!bodyContains(self._$origin) || !bodyContains(self._$tooltip)) { 1380 self._close(); 1381 } 1382 // if everything is alright 1383 else { 1384 1385 // compare the former and current positions of the origin to reposition 1386 // the tooltip if need be 1387 if (self.__options.trackOrigin) { 1388 1389 var g = self.__geometry(), 1390 identical = false; 1391 1392 // compare size first (a change requires repositioning too) 1393 if (areEqual(g.origin.size, self.__Geometry.origin.size)) { 1394 1395 // for elements that have a fixed lineage (see __geometry()), we track the 1396 // top and left properties (relative to window) 1397 if (self.__Geometry.origin.fixedLineage) { 1398 if (areEqual(g.origin.windowOffset, self.__Geometry.origin.windowOffset)) { 1399 identical = true; 1400 } 1401 } 1402 // otherwise, track total offset (relative to document) 1403 else { 1404 if (areEqual(g.origin.offset, self.__Geometry.origin.offset)) { 1405 identical = true; 1406 } 1407 } 1408 } 1409 1410 if (!identical) { 1411 1412 // close the tooltip when using the mouseleave close trigger 1413 // (see https://github.com/iamceege/tooltipster/pull/253) 1414 if (self.__options.triggerClose.mouseleave) { 1415 self._close(); 1416 } 1417 else { 1418 self.reposition(); 1419 } 1420 } 1421 } 1422 1423 if (self.__options.trackTooltip) { 1424 1425 var currentBcr = $content[0].getBoundingClientRect(); 1426 1427 if ( currentBcr.height !== self.__contentBcr.height 1428 || currentBcr.width !== self.__contentBcr.width 1429 ) { 1430 self.reposition(); 1431 self.__contentBcr = currentBcr; 1432 } 1433 } 1434 } 1435 }, self.__options.trackerInterval); 1436 1437 return self; 1438 }, 1439 1440 /** 1441 * Closes the tooltip (after the closing delay) 1442 * 1443 * @param event 1444 * @param callback 1445 * @param force Set to true to override a potential refusal of the user's function 1446 * @returns {self} 1447 * @protected 1448 */ 1449 _close: function(event, callback, force) { 1450 1451 var self = this, 1452 ok = true; 1453 1454 self._trigger({ 1455 type: 'close', 1456 event: event, 1457 stop: function() { 1458 ok = false; 1459 } 1460 }); 1461 1462 // a destroying tooltip (force == true) may not refuse to close 1463 if (ok || force) { 1464 1465 // save the method custom callback and cancel any open method custom callbacks 1466 if (callback) self.__callbacks.close.push(callback); 1467 self.__callbacks.open = []; 1468 1469 // clear open/close timeouts 1470 self.__timeoutsClear(); 1471 1472 var finishCallbacks = function() { 1473 1474 // trigger any close method custom callbacks and reset them 1475 $.each(self.__callbacks.close, function(i,c) { 1476 c.call(self, self, { 1477 event: event, 1478 origin: self._$origin[0] 1479 }); 1480 }); 1481 1482 self.__callbacks.close = []; 1483 }; 1484 1485 if (self.__state != 'closed') { 1486 1487 var necessary = true, 1488 d = new Date(), 1489 now = d.getTime(), 1490 newClosingTime = now + self.__options.animationDuration[1]; 1491 1492 // the tooltip may already already be disappearing, but if a new 1493 // call to close() is made after the animationDuration was changed 1494 // to 0 (for example), we ought to actually close it sooner than 1495 // previously scheduled. In that case it should be noted that the 1496 // browser will not adapt the animation duration to the new 1497 // animationDuration that was set after the start of the closing 1498 // animation. 1499 // Note: the same thing could be considered at opening, but is not 1500 // really useful since the tooltip is actually opened immediately 1501 // upon a call to _open(). Since it would not make the opening 1502 // animation finish sooner, its sole impact would be to trigger the 1503 // state event and the open callbacks sooner than the actual end of 1504 // the opening animation, which is not great. 1505 if (self.__state == 'disappearing') { 1506 1507 if ( newClosingTime > self.__closingTime 1508 // in case closing is actually overdue because the script 1509 // execution was suspended. See #679 1510 && self.__options.animationDuration[1] > 0 1511 ) { 1512 necessary = false; 1513 } 1514 } 1515 1516 if (necessary) { 1517 1518 self.__closingTime = newClosingTime; 1519 1520 if (self.__state != 'disappearing') { 1521 self.__stateSet('disappearing'); 1522 } 1523 1524 var finish = function() { 1525 1526 // stop the tracker 1527 clearInterval(self.__tracker); 1528 1529 // a "beforeClose" option has been asked several times but would 1530 // probably useless since the content element is still accessible 1531 // via ::content(), and because people can always use listeners 1532 // inside their content to track what's going on. For the sake of 1533 // simplicity, this has been denied. Bur for the rare people who 1534 // really need the option (for old browsers or for the case where 1535 // detaching the content is actually destructive, for file or 1536 // password inputs for example), this event will do the work. 1537 self._trigger({ 1538 type: 'closing', 1539 event: event 1540 }); 1541 1542 // unbind listeners which are no longer needed 1543 1544 self._$tooltip 1545 .off('.'+ self.__namespace +'-triggerClose') 1546 .removeClass('tooltipster-dying'); 1547 1548 // orientationchange, scroll and resize listeners 1549 $(env.window).off('.'+ self.__namespace +'-triggerClose'); 1550 1551 // scroll listeners 1552 self.__$originParents.each(function(i, el) { 1553 $(el).off('scroll.'+ self.__namespace +'-triggerClose'); 1554 }); 1555 // clear the array to prevent memory leaks 1556 self.__$originParents = null; 1557 1558 $(env.window.document.body).off('.'+ self.__namespace +'-triggerClose'); 1559 1560 self._$origin.off('.'+ self.__namespace +'-triggerClose'); 1561 1562 self._off('dismissable'); 1563 1564 // a plugin that would like to remove the tooltip from the 1565 // DOM when closed should bind on this 1566 self.__stateSet('closed'); 1567 1568 // trigger event 1569 self._trigger({ 1570 type: 'after', 1571 event: event 1572 }); 1573 1574 // call our constructor custom callback function 1575 if (self.__options.functionAfter) { 1576 self.__options.functionAfter.call(self, self, { 1577 event: event, 1578 origin: self._$origin[0] 1579 }); 1580 } 1581 1582 // call our method custom callbacks functions 1583 finishCallbacks(); 1584 }; 1585 1586 if (env.hasTransitions) { 1587 1588 self._$tooltip.css({ 1589 '-moz-animation-duration': self.__options.animationDuration[1] + 'ms', 1590 '-ms-animation-duration': self.__options.animationDuration[1] + 'ms', 1591 '-o-animation-duration': self.__options.animationDuration[1] + 'ms', 1592 '-webkit-animation-duration': self.__options.animationDuration[1] + 'ms', 1593 'animation-duration': self.__options.animationDuration[1] + 'ms', 1594 'transition-duration': self.__options.animationDuration[1] + 'ms' 1595 }); 1596 1597 self._$tooltip 1598 // clear both potential open and close tasks 1599 .clearQueue() 1600 .removeClass('tooltipster-show') 1601 // for transitions only 1602 .addClass('tooltipster-dying'); 1603 1604 if (self.__options.animationDuration[1] > 0) { 1605 self._$tooltip.delay(self.__options.animationDuration[1]); 1606 } 1607 1608 self._$tooltip.queue(finish); 1609 } 1610 else { 1611 1612 self._$tooltip 1613 .stop() 1614 .fadeOut(self.__options.animationDuration[1], finish); 1615 } 1616 } 1617 } 1618 // if the tooltip is already closed, we still need to trigger 1619 // the method custom callbacks 1620 else { 1621 finishCallbacks(); 1622 } 1623 } 1624 1625 return self; 1626 }, 1627 1628 /** 1629 * For internal use by plugins, if needed 1630 * 1631 * @returns {self} 1632 * @protected 1633 */ 1634 _off: function() { 1635 this.__$emitterPrivate.off.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments)); 1636 return this; 1637 }, 1638 1639 /** 1640 * For internal use by plugins, if needed 1641 * 1642 * @returns {self} 1643 * @protected 1644 */ 1645 _on: function() { 1646 this.__$emitterPrivate.on.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments)); 1647 return this; 1648 }, 1649 1650 /** 1651 * For internal use by plugins, if needed 1652 * 1653 * @returns {self} 1654 * @protected 1655 */ 1656 _one: function() { 1657 this.__$emitterPrivate.one.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments)); 1658 return this; 1659 }, 1660 1661 /** 1662 * Opens the tooltip right away. 1663 * 1664 * @param event 1665 * @param callback Will be called when the opening animation is over 1666 * @returns {self} 1667 * @protected 1668 */ 1669 _open: function(event, callback) { 1670 1671 var self = this; 1672 1673 // if the destruction process has not begun and if this was not 1674 // triggered by an unwanted emulated click event 1675 if (!self.__destroying) { 1676 1677 // check that the origin is still in the DOM 1678 if ( bodyContains(self._$origin) 1679 // if the tooltip is enabled 1680 && self.__enabled 1681 ) { 1682 1683 var ok = true; 1684 1685 // if the tooltip is not open yet, we need to call functionBefore. 1686 // otherwise we can jst go on 1687 if (self.__state == 'closed') { 1688 1689 // trigger an event. The event.stop function allows the callback 1690 // to prevent the opening of the tooltip 1691 self._trigger({ 1692 type: 'before', 1693 event: event, 1694 stop: function() { 1695 ok = false; 1696 } 1697 }); 1698 1699 if (ok && self.__options.functionBefore) { 1700 1701 // call our custom function before continuing 1702 ok = self.__options.functionBefore.call(self, self, { 1703 event: event, 1704 origin: self._$origin[0] 1705 }); 1706 } 1707 } 1708 1709 if (ok !== false) { 1710 1711 // if there is some content 1712 if (self.__Content !== null) { 1713 1714 // save the method callback and cancel close method callbacks 1715 if (callback) { 1716 self.__callbacks.open.push(callback); 1717 } 1718 self.__callbacks.close = []; 1719 1720 // get rid of any appearance timeouts 1721 self.__timeoutsClear(); 1722 1723 var extraTime, 1724 finish = function() { 1725 1726 if (self.__state != 'stable') { 1727 self.__stateSet('stable'); 1728 } 1729 1730 // trigger any open method custom callbacks and reset them 1731 $.each(self.__callbacks.open, function(i,c) { 1732 c.call(self, self, { 1733 origin: self._$origin[0], 1734 tooltip: self._$tooltip[0] 1735 }); 1736 }); 1737 1738 self.__callbacks.open = []; 1739 }; 1740 1741 // if the tooltip is already open 1742 if (self.__state !== 'closed') { 1743 1744 // the timer (if any) will start (or restart) right now 1745 extraTime = 0; 1746 1747 // if it was disappearing, cancel that 1748 if (self.__state === 'disappearing') { 1749 1750 self.__stateSet('appearing'); 1751 1752 if (env.hasTransitions) { 1753 1754 self._$tooltip 1755 .clearQueue() 1756 .removeClass('tooltipster-dying') 1757 .addClass('tooltipster-show'); 1758 1759 if (self.__options.animationDuration[0] > 0) { 1760 self._$tooltip.delay(self.__options.animationDuration[0]); 1761 } 1762 1763 self._$tooltip.queue(finish); 1764 } 1765 else { 1766 // in case the tooltip was currently fading out, bring it back 1767 // to life 1768 self._$tooltip 1769 .stop() 1770 .fadeIn(finish); 1771 } 1772 } 1773 // if the tooltip is already open, we still need to trigger the method 1774 // custom callback 1775 else if (self.__state == 'stable') { 1776 finish(); 1777 } 1778 } 1779 // if the tooltip isn't already open, open it 1780 else { 1781 1782 // a plugin must bind on this and store the tooltip in this._$tooltip 1783 self.__stateSet('appearing'); 1784 1785 // the timer (if any) will start when the tooltip has fully appeared 1786 // after its transition 1787 extraTime = self.__options.animationDuration[0]; 1788 1789 // insert the content inside the tooltip 1790 self.__contentInsert(); 1791 1792 // reposition the tooltip and attach to the DOM 1793 self.reposition(event, true); 1794 1795 // animate in the tooltip. If the display plugin wants no css 1796 // animations, it may override the animation option with a 1797 // dummy value that will produce no effect 1798 if (env.hasTransitions) { 1799 1800 // note: there seems to be an issue with start animations which 1801 // are randomly not played on fast devices in both Chrome and FF, 1802 // couldn't find a way to solve it yet. It seems that applying 1803 // the classes before appending to the DOM helps a little, but 1804 // it messes up some CSS transitions. The issue almost never 1805 // happens when delay[0]==0 though 1806 self._$tooltip 1807 .addClass('tooltipster-'+ self.__options.animation) 1808 .addClass('tooltipster-initial') 1809 .css({ 1810 '-moz-animation-duration': self.__options.animationDuration[0] + 'ms', 1811 '-ms-animation-duration': self.__options.animationDuration[0] + 'ms', 1812 '-o-animation-duration': self.__options.animationDuration[0] + 'ms', 1813 '-webkit-animation-duration': self.__options.animationDuration[0] + 'ms', 1814 'animation-duration': self.__options.animationDuration[0] + 'ms', 1815 'transition-duration': self.__options.animationDuration[0] + 'ms' 1816 }); 1817 1818 setTimeout( 1819 function() { 1820 1821 // a quick hover may have already triggered a mouseleave 1822 if (self.__state != 'closed') { 1823 1824 self._$tooltip 1825 .addClass('tooltipster-show') 1826 .removeClass('tooltipster-initial'); 1827 1828 if (self.__options.animationDuration[0] > 0) { 1829 self._$tooltip.delay(self.__options.animationDuration[0]); 1830 } 1831 1832 self._$tooltip.queue(finish); 1833 } 1834 }, 1835 0 1836 ); 1837 } 1838 else { 1839 1840 // old browsers will have to live with this 1841 self._$tooltip 1842 .css('display', 'none') 1843 .fadeIn(self.__options.animationDuration[0], finish); 1844 } 1845 1846 // checks if the origin is removed while the tooltip is open 1847 self.__trackerStart(); 1848 1849 // NOTE: the listeners below have a '-triggerClose' namespace 1850 // because we'll remove them when the tooltip closes (unlike 1851 // the '-triggerOpen' listeners). So some of them are actually 1852 // not about close triggers, rather about positioning. 1853 1854 $(env.window) 1855 // reposition on resize 1856 .on('resize.'+ self.__namespace +'-triggerClose', function(e) { 1857 1858 var $ae = $(document.activeElement); 1859 1860 // reposition only if the resize event was not triggered upon the opening 1861 // of a virtual keyboard due to an input field being focused within the tooltip 1862 // (otherwise the repositioning would lose the focus) 1863 if ( (!$ae.is('input') && !$ae.is('textarea')) 1864 || !$.contains(self._$tooltip[0], $ae[0]) 1865 ) { 1866 self.reposition(e); 1867 } 1868 }) 1869 // same as below for parents 1870 .on('scroll.'+ self.__namespace +'-triggerClose', function(e) { 1871 self.__scrollHandler(e); 1872 }); 1873 1874 self.__$originParents = self._$origin.parents(); 1875 1876 // scrolling may require the tooltip to be moved or even 1877 // repositioned in some cases 1878 self.__$originParents.each(function(i, parent) { 1879 1880 $(parent).on('scroll.'+ self.__namespace +'-triggerClose', function(e) { 1881 self.__scrollHandler(e); 1882 }); 1883 }); 1884 1885 if ( self.__options.triggerClose.mouseleave 1886 || (self.__options.triggerClose.touchleave && env.hasTouchCapability) 1887 ) { 1888 1889 // we use an event to allow users/plugins to control when the mouseleave/touchleave 1890 // close triggers will come to action. It allows to have more triggering elements 1891 // than just the origin and the tooltip for example, or to cancel/delay the closing, 1892 // or to make the tooltip interactive even if it wasn't when it was open, etc. 1893 self._on('dismissable', function(event) { 1894 1895 if (event.dismissable) { 1896 1897 if (event.delay) { 1898 1899 timeout = setTimeout(function() { 1900 // event.event may be undefined 1901 self._close(event.event); 1902 }, event.delay); 1903 1904 self.__timeouts.close.push(timeout); 1905 } 1906 else { 1907 self._close(event); 1908 } 1909 } 1910 else { 1911 clearTimeout(timeout); 1912 } 1913 }); 1914 1915 // now set the listeners that will trigger 'dismissable' events 1916 var $elements = self._$origin, 1917 eventNamesIn = '', 1918 eventNamesOut = '', 1919 timeout = null; 1920 1921 // if we have to allow interaction, bind on the tooltip too 1922 if (self.__options.interactive) { 1923 $elements = $elements.add(self._$tooltip); 1924 } 1925 1926 if (self.__options.triggerClose.mouseleave) { 1927 eventNamesIn += 'mouseenter.'+ self.__namespace +'-triggerClose '; 1928 eventNamesOut += 'mouseleave.'+ self.__namespace +'-triggerClose '; 1929 } 1930 if (self.__options.triggerClose.touchleave && env.hasTouchCapability) { 1931 eventNamesIn += 'touchstart.'+ self.__namespace +'-triggerClose'; 1932 eventNamesOut += 'touchend.'+ self.__namespace +'-triggerClose touchcancel.'+ self.__namespace +'-triggerClose'; 1933 } 1934 1935 $elements 1936 // close after some time spent outside of the elements 1937 .on(eventNamesOut, function(event) { 1938 1939 // it's ok if the touch gesture ended up to be a swipe, 1940 // it's still a "touch leave" situation 1941 if ( self._touchIsTouchEvent(event) 1942 || !self._touchIsEmulatedEvent(event) 1943 ) { 1944 1945 var delay = (event.type == 'mouseleave') ? 1946 self.__options.delay : 1947 self.__options.delayTouch; 1948 1949 self._trigger({ 1950 delay: delay[1], 1951 dismissable: true, 1952 event: event, 1953 type: 'dismissable' 1954 }); 1955 } 1956 }) 1957 // suspend the mouseleave timeout when the pointer comes back 1958 // over the elements 1959 .on(eventNamesIn, function(event) { 1960 1961 // it's also ok if the touch event is a swipe gesture 1962 if ( self._touchIsTouchEvent(event) 1963 || !self._touchIsEmulatedEvent(event) 1964 ) { 1965 self._trigger({ 1966 dismissable: false, 1967 event: event, 1968 type: 'dismissable' 1969 }); 1970 } 1971 }); 1972 } 1973 1974 // close the tooltip when the origin gets a mouse click (common behavior of 1975 // native tooltips) 1976 if (self.__options.triggerClose.originClick) { 1977 1978 self._$origin.on('click.'+ self.__namespace + '-triggerClose', function(event) { 1979 1980 // we could actually let a tap trigger this but this feature just 1981 // does not make sense on touch devices 1982 if ( !self._touchIsTouchEvent(event) 1983 && !self._touchIsEmulatedEvent(event) 1984 ) { 1985 self._close(event); 1986 } 1987 }); 1988 } 1989 1990 // set the same bindings for click and touch on the body to close the tooltip 1991 if ( self.__options.triggerClose.click 1992 || (self.__options.triggerClose.tap && env.hasTouchCapability) 1993 ) { 1994 1995 // don't set right away since the click/tap event which triggered this method 1996 // (if it was a click/tap) is going to bubble up to the body, we don't want it 1997 // to close the tooltip immediately after it opened 1998 setTimeout(function() { 1999 2000 if (self.__state != 'closed') { 2001 2002 var eventNames = '', 2003 $body = $(env.window.document.body); 2004 2005 if (self.__options.triggerClose.click) { 2006 eventNames += 'click.'+ self.__namespace +'-triggerClose '; 2007 } 2008 if (self.__options.triggerClose.tap && env.hasTouchCapability) { 2009 eventNames += 'touchend.'+ self.__namespace +'-triggerClose'; 2010 } 2011 2012 $body.on(eventNames, function(event) { 2013 2014 if (self._touchIsMeaningfulEvent(event)) { 2015 2016 self._touchRecordEvent(event); 2017 2018 if (!self.__options.interactive || !$.contains(self._$tooltip[0], event.target)) { 2019 self._close(event); 2020 } 2021 } 2022 }); 2023 2024 // needed to detect and ignore swiping 2025 if (self.__options.triggerClose.tap && env.hasTouchCapability) { 2026 2027 $body.on('touchstart.'+ self.__namespace +'-triggerClose', function(event) { 2028 self._touchRecordEvent(event); 2029 }); 2030 } 2031 } 2032 }, 0); 2033 } 2034 2035 self._trigger('ready'); 2036 2037 // call our custom callback 2038 if (self.__options.functionReady) { 2039 self.__options.functionReady.call(self, self, { 2040 origin: self._$origin[0], 2041 tooltip: self._$tooltip[0] 2042 }); 2043 } 2044 } 2045 2046 // if we have a timer set, let the countdown begin 2047 if (self.__options.timer > 0) { 2048 2049 var timeout = setTimeout(function() { 2050 self._close(); 2051 }, self.__options.timer + extraTime); 2052 2053 self.__timeouts.close.push(timeout); 2054 } 2055 } 2056 } 2057 } 2058 } 2059 2060 return self; 2061 }, 2062 2063 /** 2064 * When using the mouseenter/touchstart open triggers, this function will 2065 * schedule the opening of the tooltip after the delay, if there is one 2066 * 2067 * @param event 2068 * @returns {self} 2069 * @protected 2070 */ 2071 _openShortly: function(event) { 2072 2073 var self = this, 2074 ok = true; 2075 2076 if (self.__state != 'stable' && self.__state != 'appearing') { 2077 2078 // if a timeout is not already running 2079 if (!self.__timeouts.open) { 2080 2081 self._trigger({ 2082 type: 'start', 2083 event: event, 2084 stop: function() { 2085 ok = false; 2086 } 2087 }); 2088 2089 if (ok) { 2090 2091 var delay = (event.type.indexOf('touch') == 0) ? 2092 self.__options.delayTouch : 2093 self.__options.delay; 2094 2095 if (delay[0]) { 2096 2097 self.__timeouts.open = setTimeout(function() { 2098 2099 self.__timeouts.open = null; 2100 2101 // open only if the pointer (mouse or touch) is still over the origin. 2102 // The check on the "meaningful event" can only be made here, after some 2103 // time has passed (to know if the touch was a swipe or not) 2104 if (self.__pointerIsOverOrigin && self._touchIsMeaningfulEvent(event)) { 2105 2106 // signal that we go on 2107 self._trigger('startend'); 2108 2109 self._open(event); 2110 } 2111 else { 2112 // signal that we cancel 2113 self._trigger('startcancel'); 2114 } 2115 }, delay[0]); 2116 } 2117 else { 2118 // signal that we go on 2119 self._trigger('startend'); 2120 2121 self._open(event); 2122 } 2123 } 2124 } 2125 } 2126 2127 return self; 2128 }, 2129 2130 /** 2131 * Meant for plugins to get their options 2132 * 2133 * @param {string} pluginName The name of the plugin that asks for its options 2134 * @param {object} defaultOptions The default options of the plugin 2135 * @returns {object} The options 2136 * @protected 2137 */ 2138 _optionsExtract: function(pluginName, defaultOptions) { 2139 2140 var self = this, 2141 options = $.extend(true, {}, defaultOptions); 2142 2143 // if the plugin options were isolated in a property named after the 2144 // plugin, use them (prevents conflicts with other plugins) 2145 var pluginOptions = self.__options[pluginName]; 2146 2147 // if not, try to get them as regular options 2148 if (!pluginOptions){ 2149 2150 pluginOptions = {}; 2151 2152 $.each(defaultOptions, function(optionName, value) { 2153 2154 var o = self.__options[optionName]; 2155 2156 if (o !== undefined) { 2157 pluginOptions[optionName] = o; 2158 } 2159 }); 2160 } 2161 2162 // let's merge the default options and the ones that were provided. We'd want 2163 // to do a deep copy but not let jQuery merge arrays, so we'll do a shallow 2164 // extend on two levels, that will be enough if options are not more than 1 2165 // level deep 2166 $.each(options, function(optionName, value) { 2167 2168 if (pluginOptions[optionName] !== undefined) { 2169 2170 if (( typeof value == 'object' 2171 && !(value instanceof Array) 2172 && value != null 2173 ) 2174 && 2175 ( typeof pluginOptions[optionName] == 'object' 2176 && !(pluginOptions[optionName] instanceof Array) 2177 && pluginOptions[optionName] != null 2178 ) 2179 ) { 2180 $.extend(options[optionName], pluginOptions[optionName]); 2181 } 2182 else { 2183 options[optionName] = pluginOptions[optionName]; 2184 } 2185 } 2186 }); 2187 2188 return options; 2189 }, 2190 2191 /** 2192 * Used at instantiation of the plugin, or afterwards by plugins that activate themselves 2193 * on existing instances 2194 * 2195 * @param {object} pluginName 2196 * @returns {self} 2197 * @protected 2198 */ 2199 _plug: function(pluginName) { 2200 2201 var plugin = $.tooltipster._plugin(pluginName); 2202 2203 if (plugin) { 2204 2205 // if there is a constructor for instances 2206 if (plugin.instance) { 2207 2208 // proxy non-private methods on the instance to allow new instance methods 2209 $.tooltipster.__bridge(plugin.instance, this, plugin.name); 2210 } 2211 } 2212 else { 2213 throw new Error('The "'+ pluginName +'" plugin is not defined'); 2214 } 2215 2216 return this; 2217 }, 2218 2219 /** 2220 * This will return true if the event is a mouse event which was 2221 * emulated by the browser after a touch event. This allows us to 2222 * really dissociate mouse and touch triggers. 2223 * 2224 * There is a margin of error if a real mouse event is fired right 2225 * after (within the delay shown below) a touch event on the same 2226 * element, but hopefully it should not happen often. 2227 * 2228 * @returns {boolean} 2229 * @protected 2230 */ 2231 _touchIsEmulatedEvent: function(event) { 2232 2233 var isEmulated = false, 2234 now = new Date().getTime(); 2235 2236 for (var i = this.__touchEvents.length - 1; i >= 0; i--) { 2237 2238 var e = this.__touchEvents[i]; 2239 2240 // delay, in milliseconds. It's supposed to be 300ms in 2241 // most browsers (350ms on iOS) to allow a double tap but 2242 // can be less (check out FastClick for more info) 2243 if (now - e.time < 500) { 2244 2245 if (e.target === event.target) { 2246 isEmulated = true; 2247 } 2248 } 2249 else { 2250 break; 2251 } 2252 } 2253 2254 return isEmulated; 2255 }, 2256 2257 /** 2258 * Returns false if the event was an emulated mouse event or 2259 * a touch event involved in a swipe gesture. 2260 * 2261 * @param {object} event 2262 * @returns {boolean} 2263 * @protected 2264 */ 2265 _touchIsMeaningfulEvent: function(event) { 2266 return ( 2267 (this._touchIsTouchEvent(event) && !this._touchSwiped(event.target)) 2268 || (!this._touchIsTouchEvent(event) && !this._touchIsEmulatedEvent(event)) 2269 ); 2270 }, 2271 2272 /** 2273 * Checks if an event is a touch event 2274 * 2275 * @param {object} event 2276 * @returns {boolean} 2277 * @protected 2278 */ 2279 _touchIsTouchEvent: function(event){ 2280 return event.type.indexOf('touch') == 0; 2281 }, 2282 2283 /** 2284 * Store touch events for a while to detect swiping and emulated mouse events 2285 * 2286 * @param {object} event 2287 * @returns {self} 2288 * @protected 2289 */ 2290 _touchRecordEvent: function(event) { 2291 2292 if (this._touchIsTouchEvent(event)) { 2293 event.time = new Date().getTime(); 2294 this.__touchEvents.push(event); 2295 } 2296 2297 return this; 2298 }, 2299 2300 /** 2301 * Returns true if a swipe happened after the last touchstart event fired on 2302 * event.target. 2303 * 2304 * We need to differentiate a swipe from a tap before we let the event open 2305 * or close the tooltip. A swipe is when a touchmove (scroll) event happens 2306 * on the body between the touchstart and the touchend events of an element. 2307 * 2308 * @param {object} target The HTML element that may have triggered the swipe 2309 * @returns {boolean} 2310 * @protected 2311 */ 2312 _touchSwiped: function(target) { 2313 2314 var swiped = false; 2315 2316 for (var i = this.__touchEvents.length - 1; i >= 0; i--) { 2317 2318 var e = this.__touchEvents[i]; 2319 2320 if (e.type == 'touchmove') { 2321 swiped = true; 2322 break; 2323 } 2324 else if ( 2325 e.type == 'touchstart' 2326 && target === e.target 2327 ) { 2328 break; 2329 } 2330 } 2331 2332 return swiped; 2333 }, 2334 2335 /** 2336 * Triggers an event on the instance emitters 2337 * 2338 * @returns {self} 2339 * @protected 2340 */ 2341 _trigger: function() { 2342 2343 var args = Array.prototype.slice.apply(arguments); 2344 2345 if (typeof args[0] == 'string') { 2346 args[0] = { type: args[0] }; 2347 } 2348 2349 // add properties to the event 2350 args[0].instance = this; 2351 args[0].origin = this._$origin ? this._$origin[0] : null; 2352 args[0].tooltip = this._$tooltip ? this._$tooltip[0] : null; 2353 2354 // note: the order of emitters matters 2355 this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate, args); 2356 $.tooltipster._trigger.apply($.tooltipster, args); 2357 this.__$emitterPublic.trigger.apply(this.__$emitterPublic, args); 2358 2359 return this; 2360 }, 2361 2362 /** 2363 * Deactivate a plugin on this instance 2364 * 2365 * @returns {self} 2366 * @protected 2367 */ 2368 _unplug: function(pluginName) { 2369 2370 var self = this; 2371 2372 // if the plugin has been activated on this instance 2373 if (self[pluginName]) { 2374 2375 var plugin = $.tooltipster._plugin(pluginName); 2376 2377 // if there is a constructor for instances 2378 if (plugin.instance) { 2379 2380 // unbridge 2381 $.each(plugin.instance, function(methodName, fn) { 2382 2383 // if the method exists (privates methods do not) and comes indeed from 2384 // this plugin (may be missing or come from a conflicting plugin). 2385 if ( self[methodName] 2386 && self[methodName].bridged === self[pluginName] 2387 ) { 2388 delete self[methodName]; 2389 } 2390 }); 2391 } 2392 2393 // destroy the plugin 2394 if (self[pluginName].__destroy) { 2395 self[pluginName].__destroy(); 2396 } 2397 2398 // remove the reference to the plugin instance 2399 delete self[pluginName]; 2400 } 2401 2402 return self; 2403 }, 2404 2405 /** 2406 * @see self::_close 2407 * @returns {self} 2408 * @public 2409 */ 2410 close: function(callback) { 2411 2412 if (!this.__destroyed) { 2413 this._close(null, callback); 2414 } 2415 else { 2416 this.__destroyError(); 2417 } 2418 2419 return this; 2420 }, 2421 2422 /** 2423 * Sets or gets the content of the tooltip 2424 *
2425 * @returns {mixed|self} 2426 * @public 2427 */ 2428 content: function(content) { 2429 2430 var self = this; 2431 2432 // getter method 2433 if (content === undefined) { 2434 return self.__Content; 2435 } 2436 // setter method 2437 else { 2438 2439 if (!self.__destroyed) { 2440 2441 // change the content 2442 self.__contentSet(content); 2443 2444 if (self.__Content !== null) { 2445 2446 // update the tooltip if it is open 2447 if (self.__state !== 'closed') { 2448 2449 // reset the content in the tooltip 2450 self.__contentInsert(); 2451 2452 // reposition and resize the tooltip 2453 self.reposition(); 2454 2455 // if we want to play a little animation showing the content changed 2456 if (self.__options.updateAnimation) { 2457 2458 if (env.hasTransitions) { 2459 2460 // keep the reference in the local scope 2461 var animation = self.__options.updateAnimation; 2462 2463 self._$tooltip.addClass('tooltipster-update-'+ animation); 2464 2465 // remove the class after a while. The actual duration of the 2466 // update animation may be shorter, it's set in the CSS rules 2467 setTimeout(function() { 2468 2469 if (self.__state != 'closed') { 2470 2471 self._$tooltip.removeClass('tooltipster-update-'+ animation); 2472 } 2473 }, 1000); 2474 } 2475 else { 2476 self._$tooltip.fadeTo(200, 0.5, function() { 2477 if (self.__state != 'closed') { 2478 self._$tooltip.fadeTo(200, 1); 2479 } 2480 });
vendor: 22,868 bytes, lines 2481-3344
2481 } 2482 } 2483 } 2484 } 2485 else { 2486 self._close(); 2487 } 2488 } 2489 else { 2490 self.__destroyError(); 2491 } 2492 2493 return self; 2494 } 2495 }, 2496 2497 /** 2498 * Destroys the tooltip 2499 * 2500 * @returns {self} 2501 * @public 2502 */ 2503 destroy: function() { 2504 2505 var self = this; 2506 2507 if (!self.__destroyed) { 2508 2509 if(self.__state != 'closed'){ 2510 2511 // no closing delay 2512 self.option('animationDuration', 0) 2513 // force closing 2514 ._close(null, null, true); 2515 } 2516 else { 2517 // there might be an open timeout still running 2518 self.__timeoutsClear(); 2519 } 2520 2521 // send event 2522 self._trigger('destroy'); 2523 2524 self.__destroyed = true; 2525 2526 self._$origin 2527 .removeData(self.__namespace) 2528 // remove the open trigger listeners 2529 .off('.'+ self.__namespace +'-triggerOpen'); 2530 2531 // remove the touch listener 2532 $(env.window.document.body).off('.' + self.__namespace +'-triggerOpen'); 2533 2534 var ns = self._$origin.data('tooltipster-ns'); 2535 2536 // if the origin has been removed from DOM, its data may 2537 // well have been destroyed in the process and there would 2538 // be nothing to clean up or restore 2539 if (ns) { 2540 2541 // if there are no more tooltips on this element 2542 if (ns.length === 1) { 2543 2544 // optional restoration of a title attribute 2545 var title = null; 2546 if (self.__options.restoration == 'previous') { 2547 title = self._$origin.data('tooltipster-initialTitle'); 2548 } 2549 else if (self.__options.restoration == 'current') { 2550 2551 // old school technique to stringify when outerHTML is not supported 2552 title = (typeof self.__Content == 'string') ? 2553 self.__Content : 2554 $('<div></div>').append(self.__Content).html(); 2555 } 2556 2557 if (title) { 2558 self._$origin.attr('title', title); 2559 } 2560 2561 // final cleaning 2562 2563 self._$origin.removeClass('tooltipstered'); 2564 2565 self._$origin 2566 .removeData('tooltipster-ns') 2567 .removeData('tooltipster-initialTitle'); 2568 } 2569 else { 2570 // remove the instance namespace from the list of namespaces of 2571 // tooltips present on the element 2572 ns = $.grep(ns, function(el, i) { 2573 return el !== self.__namespace; 2574 }); 2575 self._$origin.data('tooltipster-ns', ns); 2576 } 2577 } 2578 2579 // last event 2580 self._trigger('destroyed'); 2581 2582 // unbind private and public event listeners 2583 self._off(); 2584 self.off(); 2585 2586 // remove external references, just in case 2587 self.__Content = null; 2588 self.__$emitterPrivate = null; 2589 self.__$emitterPublic = null; 2590 self.__options.parent = null; 2591 self._$origin = null; 2592 self._$tooltip = null; 2593 2594 // make sure the object is no longer referenced in there to prevent 2595 // memory leaks 2596 $.tooltipster.__instancesLatestArr = $.grep($.tooltipster.__instancesLatestArr, function(el, i) { 2597 return self !== el; 2598 }); 2599 2600 clearInterval(self.__garbageCollector); 2601 } 2602 else { 2603 self.__destroyError(); 2604 } 2605 2606 // we return the scope rather than true so that the call to 2607 // .tooltipster('destroy') actually returns the matched elements 2608 // and applies to all of them 2609 return self; 2610 }, 2611 2612 /** 2613 * Disables the tooltip 2614 * 2615 * @returns {self} 2616 * @public 2617 */ 2618 disable: function() { 2619 2620 if (!this.__destroyed) { 2621 2622 // close first, in case the tooltip would not disappear on 2623 // its own (no close trigger) 2624 this._close(); 2625 this.__enabled = false; 2626 2627 return this; 2628 } 2629 else { 2630 this.__destroyError(); 2631 } 2632 2633 return this; 2634 }, 2635 2636 /** 2637 * Returns the HTML element of the origin 2638 * 2639 * @returns {self} 2640 * @public 2641 */ 2642 elementOrigin: function() { 2643 2644 if (!this.__destroyed) { 2645 return this._$origin[0]; 2646 } 2647 else { 2648 this.__destroyError(); 2649 } 2650 }, 2651 2652 /** 2653 * Returns the HTML element of the tooltip 2654 * 2655 * @returns {self} 2656 * @public 2657 */ 2658 elementTooltip: function() { 2659 return this._$tooltip ? this._$tooltip[0] : null; 2660 }, 2661 2662 /** 2663 * Enables the tooltip 2664 * 2665 * @returns {self} 2666 * @public 2667 */ 2668 enable: function() { 2669 this.__enabled = true; 2670 return this; 2671 }, 2672 2673 /** 2674 * Alias, deprecated in 4.0.0 2675 * 2676 * @param {function} callback 2677 * @returns {self} 2678 * @public 2679 */ 2680 hide: function(callback) { 2681 return this.close(callback); 2682 }, 2683 2684 /** 2685 * Returns the instance 2686 * 2687 * @returns {self} 2688 * @public 2689 */ 2690 instance: function() { 2691 return this; 2692 }, 2693 2694 /** 2695 * For public use only, not to be used by plugins (use ::_off() instead) 2696 * 2697 * @returns {self} 2698 * @public 2699 */ 2700 off: function() { 2701 2702 if (!this.__destroyed) { 2703 this.__$emitterPublic.off.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); 2704 } 2705 2706 return this; 2707 }, 2708 2709 /** 2710 * For public use only, not to be used by plugins (use ::_on() instead) 2711 * 2712 * @returns {self} 2713 * @public 2714 */ 2715 on: function() { 2716 2717 if (!this.__destroyed) { 2718 this.__$emitterPublic.on.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); 2719 } 2720 else { 2721 this.__destroyError(); 2722 } 2723 2724 return this; 2725 }, 2726 2727 /** 2728 * For public use only, not to be used by plugins 2729 * 2730 * @returns {self} 2731 * @public 2732 */ 2733 one: function() { 2734 2735 if (!this.__destroyed) { 2736 this.__$emitterPublic.one.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); 2737 } 2738 else { 2739 this.__destroyError(); 2740 } 2741 2742 return this; 2743 }, 2744 2745 /** 2746 * @see self::_open 2747 * @returns {self} 2748 * @public 2749 */ 2750 open: function(callback) { 2751 2752 if (!this.__destroyed) { 2753 this._open(null, callback); 2754 } 2755 else { 2756 this.__destroyError(); 2757 } 2758 2759 return this; 2760 }, 2761 2762 /** 2763 * Get or set options. For internal use and advanced users only. 2764 * 2765 * @param {string} o Option name 2766 * @param {mixed} val optional A new value for the option 2767 * @return {mixed|self} If val is omitted, the value of the option 2768 * is returned, otherwise the instance itself is returned 2769 * @public 2770 */ 2771 option: function(o, val) { 2772 2773 // getter 2774 if (val === undefined) { 2775 return this.__options[o]; 2776 } 2777 // setter 2778 else { 2779 2780 if (!this.__destroyed) { 2781 2782 // change value 2783 this.__options[o] = val; 2784 2785 // format 2786 this.__optionsFormat(); 2787 2788 // re-prepare the triggers if needed 2789 if ($.inArray(o, ['trigger', 'triggerClose', 'triggerOpen']) >= 0) { 2790 this.__prepareOrigin(); 2791 } 2792 2793 if (o === 'selfDestruction') { 2794 this.__prepareGC(); 2795 } 2796 } 2797 else { 2798 this.__destroyError(); 2799 } 2800 2801 return this; 2802 } 2803 }, 2804 2805 /** 2806 * This method is in charge of setting the position and size properties of the tooltip. 2807 * All the hard work is delegated to the display plugin. 2808 * Note: The tooltip may be detached from the DOM at the moment the method is called 2809 * but must be attached by the end of the method call. 2810 * 2811 * @param {object} event For internal use only. Defined if an event such as 2812 * window resizing triggered the repositioning 2813 * @param {boolean} tooltipIsDetached For internal use only. Set this to true if you 2814 * know that the tooltip not being in the DOM is not an issue (typically when the 2815 * tooltip element has just been created but has not been added to the DOM yet). 2816 * @returns {self} 2817 * @public 2818 */ 2819 reposition: function(event, tooltipIsDetached) { 2820 2821 var self = this; 2822 2823 if (!self.__destroyed) { 2824 2825 // if the tooltip is still open and the origin is still in the DOM 2826 if (self.__state != 'closed' && bodyContains(self._$origin)) { 2827 2828 // if the tooltip has not been removed from DOM manually (or if it 2829 // has been detached on purpose) 2830 if (tooltipIsDetached || bodyContains(self._$tooltip)) { 2831 2832 if (!tooltipIsDetached) { 2833 // detach in case the tooltip overflows the window and adds 2834 // scrollbars to it, so __geometry can be accurate 2835 self._$tooltip.detach(); 2836 } 2837 2838 // refresh the geometry object before passing it as a helper 2839 self.__Geometry = self.__geometry(); 2840 2841 // let a plugin fo the rest 2842 self._trigger({ 2843 type: 'reposition', 2844 event: event, 2845 helper: { 2846 geo: self.__Geometry 2847 } 2848 }); 2849 } 2850 } 2851 } 2852 else { 2853 self.__destroyError(); 2854 } 2855 2856 return self; 2857 }, 2858 2859 /** 2860 * Alias, deprecated in 4.0.0 2861 * 2862 * @param callback 2863 * @returns {self} 2864 * @public 2865 */ 2866 show: function(callback) { 2867 return this.open(callback); 2868 }, 2869 2870 /** 2871 * Returns some properties about the instance 2872 * 2873 * @returns {object} 2874 * @public 2875 */ 2876 status: function() { 2877 2878 return { 2879 destroyed: this.__destroyed, 2880 enabled: this.__enabled, 2881 open: this.__state !== 'closed', 2882 state: this.__state 2883 }; 2884 }, 2885 2886 /** 2887 * For public use only, not to be used by plugins 2888 * 2889 * @returns {self} 2890 * @public 2891 */ 2892 triggerHandler: function() { 2893 2894 if (!this.__destroyed) { 2895 this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); 2896 } 2897 else { 2898 this.__destroyError(); 2899 } 2900 2901 return this; 2902 } 2903}; 2904 2905$.fn.tooltipster = function() { 2906 2907 // for using in closures 2908 var args = Array.prototype.slice.apply(arguments), 2909 // common mistake: an HTML element can't be in several tooltips at the same time 2910 contentCloningWarning = 'You are using a single HTML element as content for several tooltips. You probably want to set the contentCloning option to TRUE.'; 2911 2912 // this happens with $(sel).tooltipster(...) when $(sel) does not match anything 2913 if (this.length === 0) { 2914 2915 // still chainable 2916 return this; 2917 } 2918 // this happens when calling $(sel).tooltipster('methodName or options') 2919 // where $(sel) matches one or more elements 2920 else { 2921 2922 // method calls 2923 if (typeof args[0] === 'string') { 2924 2925 var v = '#*$~&'; 2926 2927 this.each(function() { 2928 2929 // retrieve the namepaces of the tooltip(s) that exist on that element. 2930 // We will interact with the first tooltip only. 2931 var ns = $(this).data('tooltipster-ns'), 2932 // self represents the instance of the first tooltipster plugin 2933 // associated to the current HTML object of the loop 2934 self = ns ? $(this).data(ns[0]) : null; 2935 2936 // if the current element holds a tooltipster instance 2937 if (self) { 2938 2939 if (typeof self[args[0]] === 'function') { 2940 2941 if ( this.length > 1 2942 && args[0] == 'content' 2943 && ( args[1] instanceof $ 2944 || (typeof args[1] == 'object' && args[1] != null && args[1].tagName) 2945 ) 2946 && !self.__options.contentCloning 2947 && self.__options.debug 2948 ) { 2949 console.log(contentCloningWarning); 2950 } 2951 2952 // note : args[1] and args[2] may not be defined 2953 var resp = self[args[0]](args[1], args[2]); 2954 } 2955 else { 2956 throw new Error('Unknown method "'+ args[0] +'"'); 2957 } 2958 2959 // if the function returned anything other than the instance 2960 // itself (which implies chaining, except for the `instance` method) 2961 if (resp !== self || args[0] === 'instance') { 2962 2963 v = resp; 2964 2965 // return false to stop .each iteration on the first element 2966 // matched by the selector 2967 return false; 2968 } 2969 } 2970 else { 2971 throw new Error('You called Tooltipster\'s "'+ args[0] +'" method on an uninitialized element'); 2972 } 2973 }); 2974 2975 return (v !== '#*$~&') ? v : this; 2976 } 2977 // first argument is undefined or an object: the tooltip is initializing 2978 else { 2979 2980 // reset the array of last initialized objects 2981 $.tooltipster.__instancesLatestArr = []; 2982 2983 // is there a defined value for the multiple option in the options object ? 2984 var multipleIsSet = args[0] && args[0].multiple !== undefined, 2985 // if the multiple option is set to true, or if it's not defined but 2986 // set to true in the defaults 2987 multiple = (multipleIsSet && args[0].multiple) || (!multipleIsSet && defaults.multiple), 2988 // same for content 2989 contentIsSet = args[0] && args[0].content !== undefined, 2990 content = (contentIsSet && args[0].content) || (!contentIsSet && defaults.content), 2991 // same for contentCloning 2992 contentCloningIsSet = args[0] && args[0].contentCloning !== undefined, 2993 contentCloning = 2994 (contentCloningIsSet && args[0].contentCloning) 2995 || (!contentCloningIsSet && defaults.contentCloning), 2996 // same for debug 2997 debugIsSet = args[0] && args[0].debug !== undefined, 2998 debug = (debugIsSet && args[0].debug) || (!debugIsSet && defaults.debug); 2999 3000 if ( this.length > 1 3001 && ( content instanceof $ 3002 || (typeof content == 'object' && content != null && content.tagName) 3003 ) 3004 && !contentCloning 3005 && debug 3006 ) { 3007 console.log(contentCloningWarning); 3008 } 3009 3010 // create a tooltipster instance for each element if it doesn't 3011 // already have one or if the multiple option is set, and attach the 3012 // object to it 3013 this.each(function() { 3014 3015 var go = false, 3016 $this = $(this), 3017 ns = $this.data('tooltipster-ns'), 3018 obj = null; 3019 3020 if (!ns) { 3021 go = true; 3022 } 3023 else if (multiple) { 3024 go = true; 3025 } 3026 else if (debug) { 3027 //console.log('Tooltipster: one or more tooltips are already attached to the element below. Ignoring.'); 3028 //console.log(this); 3029 } 3030 3031 if (go) { 3032 obj = new $.Tooltipster(this, args[0]); 3033 3034 // save the reference of the new instance 3035 if (!ns) ns = []; 3036 ns.push(obj.__namespace); 3037 $this.data('tooltipster-ns', ns); 3038 3039 // save the instance itself 3040 $this.data(obj.__namespace, obj); 3041 3042 // call our constructor custom function. 3043 // we do this here and not in ::init() because we wanted 3044 // the object to be saved in $this.data before triggering 3045 // it 3046 if (obj.__options.functionInit) { 3047 obj.__options.functionInit.call(obj, obj, { 3048 origin: this 3049 }); 3050 } 3051 3052 // and now the event, for the plugins and core emitter 3053 obj._trigger('init'); 3054 } 3055 3056 $.tooltipster.__instancesLatestArr.push(obj); 3057 }); 3058 3059 return this; 3060 } 3061 } 3062}; 3063 3064// Utilities 3065 3066/** 3067 * A class to check if a tooltip can fit in given dimensions 3068 * 3069 * @param {object} $tooltip The jQuery wrapped tooltip element, or a clone of it 3070 */ 3071function Ruler($tooltip) { 3072 3073 // list of instance variables 3074 3075 this.$container; 3076 this.constraints = null; 3077 this.__$tooltip; 3078 3079 this.__init($tooltip); 3080} 3081 3082Ruler.prototype = { 3083 3084 /** 3085 * Move the tooltip into an invisible div that does not allow overflow to make 3086 * size tests. Note: the tooltip may or may not be attached to the DOM at the 3087 * moment this method is called, it does not matter. 3088 * 3089 * @param {object} $tooltip The object to test. May be just a clone of the 3090 * actual tooltip. 3091 * @private 3092 */ 3093 __init: function($tooltip) { 3094 3095 this.__$tooltip = $tooltip; 3096 3097 this.__$tooltip 3098 .css({ 3099 // for some reason we have to specify top and left 0 3100 left: 0, 3101 // any overflow will be ignored while measuring 3102 overflow: 'hidden', 3103 // positions at (0,0) without the div using 100% of the available width 3104 position: 'absolute', 3105 top: 0 3106 }) 3107 // overflow must be auto during the test. We re-set this in case 3108 // it were modified by the user 3109 .find('.tooltipster-content') 3110 .css('overflow', 'auto'); 3111 3112 this.$container = $('<div class="tooltipster-ruler"></div>') 3113 .append(this.__$tooltip) 3114 .appendTo(env.window.document.body); 3115 }, 3116 3117 /** 3118 * Force the browser to redraw (re-render) the tooltip immediately. This is required 3119 * when you changed some CSS properties and need to make something with it 3120 * immediately, without waiting for the browser to redraw at the end of instructions. 3121 * 3122 * @see http://stackoverflow.com/questions/3485365/how-can-i-force-webkit-to-redraw-repaint-to-propagate-style-changes 3123 * @private 3124 */ 3125 __forceRedraw: function() { 3126 3127 // note: this would work but for Webkit only 3128 //this.__$tooltip.close(); 3129 //this.__$tooltip[0].offsetHeight; 3130 //this.__$tooltip.open(); 3131 3132 // works in FF too 3133 var $p = this.__$tooltip.parent(); 3134 this.__$tooltip.detach(); 3135 this.__$tooltip.appendTo($p); 3136 }, 3137 3138 /** 3139 * Set maximum dimensions for the tooltip. A call to ::measure afterwards 3140 * will tell us if the content overflows or if it's ok 3141 * 3142 * @param {int} width 3143 * @param {int} height 3144 * @return {Ruler} 3145 * @public 3146 */ 3147 constrain: function(width, height) { 3148 3149 this.constraints = { 3150 width: width, 3151 height: height 3152 }; 3153 3154 this.__$tooltip.css({ 3155 // we disable display:flex, otherwise the content would overflow without 3156 // creating horizontal scrolling (which we need to detect). 3157 display: 'block', 3158 // reset any previous height 3159 height: '', 3160 // we'll check if horizontal scrolling occurs 3161 overflow: 'auto', 3162 // we'll set the width and see what height is generated and if there 3163 // is horizontal overflow 3164 width: width 3165 }); 3166 3167 return this; 3168 }, 3169 3170 /** 3171 * Reset the tooltip content overflow and remove the test container 3172 * 3173 * @returns {Ruler} 3174 * @public 3175 */ 3176 destroy: function() { 3177 3178 // in case the element was not a clone 3179 this.__$tooltip 3180 .detach() 3181 .find('.tooltipster-content') 3182 .css({ 3183 // reset to CSS value 3184 display: '', 3185 overflow: '' 3186 }); 3187 3188 this.$container.remove(); 3189 }, 3190 3191 /** 3192 * Removes any constraints 3193 * 3194 * @returns {Ruler} 3195 * @public 3196 */ 3197 free: function() { 3198 3199 this.constraints = null; 3200 3201 // reset to natural size 3202 this.__$tooltip.css({ 3203 display: '', 3204 height: '', 3205 overflow: 'visible', 3206 width: '' 3207 }); 3208 3209 return this; 3210 }, 3211 3212 /** 3213 * Returns the size of the tooltip. When constraints are applied, also returns 3214 * whether the tooltip fits in the provided dimensions. 3215 * The idea is to see if the new height is small enough and if the content does 3216 * not overflow horizontally. 3217 * 3218 * @param {int} width 3219 * @param {int} height 3220 * @returns {object} An object with a bool `fits` property and a `size` property 3221 * @public 3222 */ 3223 measure: function() { 3224 3225 this.__forceRedraw(); 3226 3227 var tooltipBcr = this.__$tooltip[0].getBoundingClientRect(), 3228 result = { size: { 3229 // bcr.width/height are not defined in IE8- but in this 3230 // case, bcr.right/bottom will have the same value 3231 // except in iOS 8+ where tooltipBcr.bottom/right are wrong 3232 // after scrolling for reasons yet to be determined. 3233 // tooltipBcr.top/left might not be 0, see issue #514 3234 height: tooltipBcr.height || (tooltipBcr.bottom - tooltipBcr.top), 3235 width: tooltipBcr.width || (tooltipBcr.right - tooltipBcr.left) 3236 }}; 3237 3238 if (this.constraints) { 3239 3240 // note: we used to use offsetWidth instead of boundingRectClient but 3241 // it returned rounded values, causing issues with sub-pixel layouts. 3242 3243 // note2: noticed that the bcrWidth of text content of a div was once 3244 // greater than the bcrWidth of its container by 1px, causing the final 3245 // tooltip box to be too small for its content. However, evaluating 3246 // their widths one against the other (below) surprisingly returned 3247 // equality. Happened only once in Chrome 48, was not able to reproduce 3248 // => just having fun with float position values... 3249 3250 var $content = this.__$tooltip.find('.tooltipster-content'), 3251 height = this.__$tooltip.outerHeight(), 3252 contentBcr = $content[0].getBoundingClientRect(), 3253 fits = { 3254 height: height <= this.constraints.height, 3255 width: ( 3256 // this condition accounts for min-width property that 3257 // may apply 3258 tooltipBcr.width <= this.constraints.width 3259 // the -1 is here because scrollWidth actually returns 3260 // a rounded value, and may be greater than bcr.width if 3261 // it was rounded up. This may cause an issue for contents 3262 // which actually really overflow by 1px or so, but that 3263 // should be rare. Not sure how to solve this efficiently. 3264 // See http://blogs.msdn.com/b/ie/archive/2012/02/17/sub-pixel-rendering-and-the-css-object-model.aspx 3265 && contentBcr.width >= $content[0].scrollWidth - 1 3266 ) 3267 }; 3268 3269 result.fits = fits.height && fits.width; 3270 } 3271 3272 // old versions of IE get the width wrong for some reason and it causes 3273 // the text to be broken to a new line, so we round it up. If the width 3274 // is the width of the screen though, we can assume it is accurate. 3275 if ( env.IE 3276 && env.IE <= 11 3277 && result.size.width !== env.window.document.documentElement.clientWidth 3278 ) { 3279 result.size.width = Math.ceil(result.size.width) + 1; 3280 } 3281 3282 return result; 3283 } 3284}; 3285 3286// quick & dirty compare function, not bijective nor multidimensional 3287function areEqual(a,b) { 3288 var same = true; 3289 $.each(a, function(i, _) { 3290 if (b[i] === undefined || a[i] !== b[i]) { 3291 same = false; 3292 return false; 3293 } 3294 }); 3295 return same; 3296} 3297 3298/** 3299 * A fast function to check if an element is still in the DOM. It 3300 * tries to use an id as ids are indexed by the browser, or falls 3301 * back to jQuery's `contains` method. May fail if two elements 3302 * have the same id, but so be it 3303 * 3304 * @param {object} $obj A jQuery-wrapped HTML element 3305 * @return {boolean} 3306 */ 3307function bodyContains($obj) { 3308 var id = $obj.attr('id'), 3309 el = id ? env.window.document.getElementById(id) : null; 3310 // must also check that the element with the id is the one we want 3311 return el ? el === $obj[0] : $.contains(env.window.document.body, $obj[0]); 3312} 3313 3314// detect IE versions for dirty fixes 3315var uA = navigator.userAgent.toLowerCase(); 3316if (uA.indexOf('msie') != -1) env.IE = parseInt(uA.split('msie')[1]); 3317else if (uA.toLowerCase().indexOf('trident') !== -1 && uA.indexOf(' rv:11') !== -1) env.IE = 11; 3318else if (uA.toLowerCase().indexOf('edge/') != -1) env.IE = parseInt(uA.toLowerCase().split('edge/')[1]); 3319 3320// detecting support for CSS transitions 3321function transitionSupport() { 3322 3323 // env.window is not defined yet when this is called 3324 if (!win) return false; 3325 3326 var b = win.document.body || win.document.documentElement, 3327 s = b.style, 3328 p = 'transition', 3329 v = ['Moz', 'Webkit', 'Khtml', 'O', 'ms']; 3330 3331 if (typeof s[p] == 'string') { return true; } 3332 3333 p = p.charAt(0).toUpperCase() + p.substr(1); 3334 for (var i=0; i<v.length; i++) { 3335 if (typeof s[v[i] + p] == 'string') { return true; } 3336 } 3337 return false; 3338} 3339 3340// we'll return jQuery for plugins not to have to declare it as a dependency, 3341// but it's done by a build task since it should be included only once at the 3342// end when we concatenate the main file with a pluginreturn $; 3343 3344}));
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.