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