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