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.6
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,623 bytes, lines 459-1646
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 + geo.window.scroll.left; 879 geo.origin.offset.top = geo.origin.windowOffset.top + geo.window.scroll.top; 880 geo.origin.offset.bottom = geo.origin.offset.top + geo.origin.size.height; 881 geo.origin.offset.right = geo.origin.offset.left + geo.origin.size.width; 882 883 // the space that is available to display the tooltip relatively to the document 884 geo.available.document = { 885 bottom: { 886 height: geo.document.size.height - geo.origin.offset.bottom, 887 width: geo.document.size.width 888 }, 889 left: { 890 height: geo.document.size.height, 891 width: geo.origin.offset.left 892 }, 893 right: { 894 height: geo.document.size.height, 895 width: geo.document.size.width - geo.origin.offset.right 896 }, 897 top: { 898 height: geo.origin.offset.top, 899 width: geo.document.size.width 900 } 901 }; 902 903 // the space that is available to display the tooltip relatively to the viewport 904 // (the resulting values may be negative if the origin overflows the viewport) 905 geo.available.window = { 906 bottom: { 907 // the inner max is here to make sure the available height is no bigger 908 // than the viewport height (when the origin is off screen at the top). 909 // The outer max just makes sure that the height is not negative (when 910 // the origin overflows at the bottom). 911 height: Math.max(geo.window.size.height - Math.max(geo.origin.windowOffset.bottom, 0), 0), 912 width: geo.window.size.width 913 }, 914 left: { 915 height: geo.window.size.height, 916 width: Math.max(geo.origin.windowOffset.left, 0) 917 }, 918 right: { 919 height: geo.window.size.height, 920 width: Math.max(geo.window.size.width - Math.max(geo.origin.windowOffset.right, 0), 0) 921 }, 922 top: { 923 height: Math.max(geo.origin.windowOffset.top, 0), 924 width: geo.window.size.width 925 } 926 }; 927 928 while ($parent[0].tagName.toLowerCase() != 'html') { 929 930 if ($parent.css('position') == 'fixed') { 931 geo.origin.fixedLineage = true; 932 break; 933 } 934 935 $parent = $parent.parent(); 936 } 937 938 return geo; 939 }, 940 941 /** 942 * Some options may need to be formated before being used 943 * 944 * @returns {self} 945 * @private 946 */ 947 __optionsFormat: function() { 948 949 if (typeof this.__options.animationDuration == 'number') { 950 this.__options.animationDuration = [this.__options.animationDuration, this.__options.animationDuration]; 951 } 952 953 if (typeof this.__options.delay == 'number') { 954 this.__options.delay = [this.__options.delay, this.__options.delay]; 955 } 956 957 if (typeof this.__options.delayTouch == 'number') { 958 this.__options.delayTouch = [this.__options.delayTouch, this.__options.delayTouch]; 959 } 960 961 if (typeof this.__options.theme == 'string') { 962 this.__options.theme = [this.__options.theme]; 963 } 964 965 // determine the future parent 966 if (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 origin: self._$origin[0] 1560 }); 1561 } 1562 1563 // call our method custom callbacks functions 1564 finishCallbacks(); 1565 }; 1566 1567 if (env.hasTransitions) { 1568 1569 self._$tooltip.css({ 1570 '-moz-animation-duration': self.__options.animationDuration[1] + 'ms', 1571 '-ms-animation-duration': self.__options.animationDuration[1] + 'ms', 1572 '-o-animation-duration': self.__options.animationDuration[1] + 'ms', 1573 '-webkit-animation-duration': self.__options.animationDuration[1] + 'ms', 1574 'animation-duration': self.__options.animationDuration[1] + 'ms', 1575 'transition-duration': self.__options.animationDuration[1] + 'ms' 1576 }); 1577 1578 self._$tooltip 1579 // clear both potential open and close tasks 1580 .clearQueue() 1581 .removeClass('tooltipster-show') 1582 // for transitions only 1583 .addClass('tooltipster-dying'); 1584 1585 if (self.__options.animationDuration[1] > 0) { 1586 self._$tooltip.delay(self.__options.animationDuration[1]); 1587 } 1588 1589 self._$tooltip.queue(finish); 1590 } 1591 else { 1592 1593 self._$tooltip 1594 .stop() 1595 .fadeOut(self.__options.animationDuration[1], finish); 1596 } 1597 } 1598 } 1599 // if the tooltip is already closed, we still need to trigger 1600 // the method custom callbacks 1601 else { 1602 finishCallbacks(); 1603 } 1604 } 1605 1606 return self; 1607 }, 1608 1609 /** 1610 * For internal use by plugins, if needed 1611 * 1612 * @returns {self} 1613 * @protected 1614 */ 1615 _off: function() { 1616 this.__$emitterPrivate.off.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments)); 1617 return this; 1618 }, 1619 1620 /** 1621 * For internal use by plugins, if needed 1622 * 1623 * @returns {self} 1624 * @protected 1625 */ 1626 _on: function() { 1627 this.__$emitterPrivate.on.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments)); 1628 return this; 1629 }, 1630 1631 /** 1632 * For internal use by plugins, if needed 1633 * 1634 * @returns {self} 1635 * @protected 1636 */ 1637 _one: function() { 1638 this.__$emitterPrivate.one.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments)); 1639 return this; 1640 }, 1641 1642 /** 1643 * Opens the tooltip right away 1644 * 1645 * @param event 1646 * @param callback
vendor: 11,384 bytes, lines 1646-1983
1646 1647 * @returns {self} 1648 * @protected 1649 */ 1650 _open: function(event, callback) { 1651 1652 var self = this; 1653 1654 // if the destruction process has not begun and if this was not 1655 // triggered by an unwanted emulated click event 1656 if (!self.__destroying) { 1657 1658 // check that the origin is still in the DOM 1659 if ( bodyContains(self._$origin) 1660 // if the tooltip is enabled 1661 && self.__enabled 1662 ) { 1663 1664 var ok = true; 1665 1666 // if the tooltip is not open yet, we need to call functionBefore. 1667 // otherwise we can jst go on 1668 if (self.__state == 'closed') { 1669 1670 // trigger an event. The event.stop function allows the callback 1671 // to prevent the opening of the tooltip 1672 self._trigger({ 1673 type: 'before', 1674 event: event, 1675 stop: function() { 1676 ok = false; 1677 } 1678 }); 1679 1680 if (ok && self.__options.functionBefore) { 1681 1682 // call our custom function before continuing 1683 ok = self.__options.functionBefore.call(self, self, { 1684 event: event, 1685 origin: self._$origin[0] 1686 }); 1687 } 1688 } 1689 1690 if (ok !== false) { 1691 1692 // if there is some content 1693 if (self.__Content !== null) { 1694 1695 // save the method callback and cancel close method callbacks 1696 if (callback) { 1697 self.__callbacks.open.push(callback); 1698 } 1699 self.__callbacks.close = []; 1700 1701 // get rid of any appearance timeouts 1702 self.__timeoutsClear(); 1703 1704 var extraTime, 1705 finish = function() { 1706 1707 if (self.__state != 'stable') { 1708 self.__stateSet('stable'); 1709 } 1710 1711 // trigger any open method custom callbacks and reset them 1712 $.each(self.__callbacks.open, function(i,c) { 1713 c.call(self, self, { 1714 origin: self._$origin[0], 1715 tooltip: self._$tooltip[0] 1716 }); 1717 }); 1718 1719 self.__callbacks.open = []; 1720 }; 1721 1722 // if the tooltip is already open 1723 if (self.__state !== 'closed') { 1724 1725 // the timer (if any) will start (or restart) right now 1726 extraTime = 0; 1727 1728 // if it was disappearing, cancel that 1729 if (self.__state === 'disappearing') { 1730 1731 self.__stateSet('appearing'); 1732 1733 if (env.hasTransitions) { 1734 1735 self._$tooltip 1736 .clearQueue() 1737 .removeClass('tooltipster-dying') 1738 .addClass('tooltipster-show'); 1739 1740 if (self.__options.animationDuration[0] > 0) { 1741 self._$tooltip.delay(self.__options.animationDuration[0]); 1742 } 1743 1744 self._$tooltip.queue(finish); 1745 } 1746 else { 1747 // in case the tooltip was currently fading out, bring it back 1748 // to life 1749 self._$tooltip 1750 .stop() 1751 .fadeIn(finish); 1752 } 1753 } 1754 // if the tooltip is already open, we still need to trigger the method 1755 // custom callback 1756 else if (self.__state == 'stable') { 1757 finish(); 1758 } 1759 } 1760 // if the tooltip isn't already open, open it 1761 else { 1762 1763 // a plugin must bind on this and store the tooltip in this._$tooltip 1764 self.__stateSet('appearing'); 1765 1766 // the timer (if any) will start when the tooltip has fully appeared 1767 // after its transition 1768 extraTime = self.__options.animationDuration[0]; 1769 1770 // insert the content inside the tooltip 1771 self.__contentInsert(); 1772 1773 // reposition the tooltip and attach to the DOM 1774 self.reposition(event, true); 1775 1776 // animate in the tooltip. If the display plugin wants no css 1777 // animations, it may override the animation option with a 1778 // dummy value that will produce no effect 1779 if (env.hasTransitions) { 1780 1781 // note: there seems to be an issue with start animations which 1782 // are randomly not played on fast devices in both Chrome and FF, 1783 // couldn't find a way to solve it yet. It seems that applying 1784 // the classes before appending to the DOM helps a little, but 1785 // it messes up some CSS transitions. The issue almost never 1786 // happens when delay[0]==0 though 1787 self._$tooltip 1788 .addClass('tooltipster-'+ self.__options.animation) 1789 .addClass('tooltipster-initial') 1790 .css({ 1791 '-moz-animation-duration': self.__options.animationDuration[0] + 'ms', 1792 '-ms-animation-duration': self.__options.animationDuration[0] + 'ms', 1793 '-o-animation-duration': self.__options.animationDuration[0] + 'ms', 1794 '-webkit-animation-duration': self.__options.animationDuration[0] + 'ms', 1795 'animation-duration': self.__options.animationDuration[0] + 'ms', 1796 'transition-duration': self.__options.animationDuration[0] + 'ms' 1797 }); 1798 1799 setTimeout( 1800 function() { 1801 1802 // a quick hover may have already triggered a mouseleave 1803 if (self.__state != 'closed') { 1804 1805 self._$tooltip 1806 .addClass('tooltipster-show') 1807 .removeClass('tooltipster-initial'); 1808 1809 if (self.__options.animationDuration[0] > 0) { 1810 self._$tooltip.delay(self.__options.animationDuration[0]); 1811 } 1812 1813 self._$tooltip.queue(finish); 1814 } 1815 }, 1816 0 1817 ); 1818 } 1819 else { 1820 1821 // old browsers will have to live with this 1822 self._$tooltip 1823 .css('display', 'none') 1824 .fadeIn(self.__options.animationDuration[0], finish); 1825 } 1826 1827 // checks if the origin is removed while the tooltip is open 1828 self.__trackerStart(); 1829 1830 // NOTE: the listeners below have a '-triggerClose' namespace 1831 // because we'll remove them when the tooltip closes (unlike 1832 // the '-triggerOpen' listeners). So some of them are actually 1833 // not about close triggers, rather about positioning. 1834 1835 $(env.window) 1836 // reposition on resize 1837 .on('resize.'+ self.__namespace +'-triggerClose', function(e) { 1838 1839 var $ae = $(document.activeElement); 1840 1841 // reposition only if the resize event was not triggered upon the opening 1842 // of a virtual keyboard due to an input field being focused within the tooltip 1843 // (otherwise the repositioning would lose the focus) 1844 if ( (!$ae.is('input') && !$ae.is('textarea')) 1845 || !$.contains(self._$tooltip[0], $ae[0]) 1846 ) { 1847 self.reposition(e); 1848 } 1849 }) 1850 // same as below for parents 1851 .on('scroll.'+ self.__namespace +'-triggerClose', function(e) { 1852 self.__scrollHandler(e); 1853 }); 1854 1855 self.__$originParents = self._$origin.parents(); 1856 1857 // scrolling may require the tooltip to be moved or even 1858 // repositioned in some cases 1859 self.__$originParents.each(function(i, parent) { 1860 1861 $(parent).on('scroll.'+ self.__namespace +'-triggerClose', function(e) { 1862 self.__scrollHandler(e); 1863 }); 1864 }); 1865 1866 if ( self.__options.triggerClose.mouseleave 1867 || (self.__options.triggerClose.touchleave && env.hasTouchCapability) 1868 ) { 1869 1870 // we use an event to allow users/plugins to control when the mouseleave/touchleave 1871 // close triggers will come to action. It allows to have more triggering elements 1872 // than just the origin and the tooltip for example, or to cancel/delay the closing, 1873 // or to make the tooltip interactive even if it wasn't when it was open, etc. 1874 self._on('dismissable', function(event) { 1875 1876 if (event.dismissable) { 1877 1878 if (event.delay) { 1879 1880 timeout = setTimeout(function() { 1881 // event.event may be undefined 1882 self._close(event.event); 1883 }, event.delay); 1884 1885 self.__timeouts.close.push(timeout); 1886 } 1887 else { 1888 self._close(event); 1889 } 1890 } 1891 else { 1892 clearTimeout(timeout); 1893 } 1894 }); 1895 1896 // now set the listeners that will trigger 'dismissable' events 1897 var $elements = self._$origin, 1898 eventNamesIn = '', 1899 eventNamesOut = '', 1900 timeout = null; 1901 1902 // if we have to allow interaction, bind on the tooltip too 1903 if (self.__options.interactive) { 1904 $elements = $elements.add(self._$tooltip); 1905 } 1906 1907 if (self.__options.triggerClose.mouseleave) { 1908 eventNamesIn += 'mouseenter.'+ self.__namespace +'-triggerClose '; 1909 eventNamesOut += 'mouseleave.'+ self.__namespace +'-triggerClose '; 1910 } 1911 if (self.__options.triggerClose.touchleave && env.hasTouchCapability) { 1912 eventNamesIn += 'touchstart.'+ self.__namespace +'-triggerClose'; 1913 eventNamesOut += 'touchend.'+ self.__namespace +'-triggerClose touchcancel.'+ self.__namespace +'-triggerClose'; 1914 } 1915 1916 $elements 1917 // close after some time spent outside of the elements 1918 .on(eventNamesOut, function(event) { 1919 1920 // it's ok if the touch gesture ended up to be a swipe, 1921 // it's still a "touch leave" situation 1922 if ( self._touchIsTouchEvent(event) 1923 || !self._touchIsEmulatedEvent(event) 1924 ) { 1925 1926 var delay = (event.type == 'mouseleave') ? 1927 self.__options.delay : 1928 self.__options.delayTouch; 1929 1930 self._trigger({ 1931 delay: delay[1], 1932 dismissable: true, 1933 event: event, 1934 type: 'dismissable' 1935 }); 1936 } 1937 }) 1938 // suspend the mouseleave timeout when the pointer comes back 1939 // over the elements 1940 .on(eventNamesIn, function(event) { 1941 1942 // it's also ok if the touch event is a swipe gesture 1943 if ( self._touchIsTouchEvent(event) 1944 || !self._touchIsEmulatedEvent(event) 1945 ) { 1946 self._trigger({ 1947 dismissable: false, 1948 event: event, 1949 type: 'dismissable' 1950 }); 1951 } 1952 }); 1953 } 1954 1955 // close the tooltip when the origin gets a mouse click (common behavior of 1956 // native tooltips) 1957 if (self.__options.triggerClose.originClick) { 1958 1959 self._$origin.on('click.'+ self.__namespace + '-triggerClose', function(event) { 1960 1961 // we could actually let a tap trigger this but this feature just 1962 // does not make sense on touch devices 1963 if ( !self._touchIsTouchEvent(event) 1964 && !self._touchIsEmulatedEvent(event) 1965 ) { 1966 self._close(event); 1967 } 1968 }); 1969 } 1970 1971 // set the same bindings for click and touch on the body to close the tooltip 1972 if ( self.__options.triggerClose.click 1973 || (self.__options.triggerClose.tap && env.hasTouchCapability) 1974 ) { 1975 1976 // don't set right away since the click/tap event which triggered this method 1977 // (if it was a click/tap) is going to bubble up to the body, we don't want it 1978 // to close the tooltip immediately after it opened 1979 setTimeout(function() { 1980 1981 if (self.__state != 'closed') { 1982 1983 var eventNames = ''
1983; 1984 if (self.__options.triggerClose.click) { 1985 eventNames += 'click.'+ self.__namespace +'-triggerClose '; 1986 } 1987 if (self.__options.triggerClose.tap && env.hasTouchCapability) { 1988 eventNames += 'touchend.'+ self.__namespace +'-triggerClose'; 1989 } 1990 1991 $('body').on(eventNames, function(event) { 1992 1993 if (self._touchIsMeaningfulEvent(event)) { 1994 1995 self._touchRecordEvent(event); 1996 1997 if (!self.__options.interactive || !$.contains(self._$tooltip[0], event.target)) { 1998 self._close(event); 1999 } 2000 } 2001 }); 2002 2003 // needed to detect and ignore swiping 2004 if (self.__options.triggerClose.tap && env.hasTouchCapability) { 2005 2006 $('body')
vendor: 11,059 bytes, lines 2006-2488
2006.on('touchstart.'+ self.__namespace +'-triggerClose', function(event) { 2007 self._touchRecordEvent(event); 2008 }); 2009 } 2010 } 2011 }, 0); 2012 } 2013 2014 self._trigger('ready'); 2015 2016 // call our custom callback 2017 if (self.__options.functionReady) { 2018 self.__options.functionReady.call(self, self, { 2019 origin: self._$origin[0], 2020 tooltip: self._$tooltip[0] 2021 }); 2022 } 2023 } 2024 2025 // if we have a timer set, let the countdown begin 2026 if (self.__options.timer > 0) { 2027 2028 var timeout = setTimeout(function() { 2029 self._close(); 2030 }, self.__options.timer + extraTime); 2031 2032 self.__timeouts.close.push(timeout); 2033 } 2034 } 2035 } 2036 } 2037 } 2038 2039 return self; 2040 }, 2041 2042 /** 2043 * When using the mouseenter/touchstart open triggers, this function will 2044 * schedule the opening of the tooltip after the delay, if there is one 2045 * 2046 * @param event 2047 * @returns {self} 2048 * @protected 2049 */ 2050 _openShortly: function(event) { 2051 2052 var self = this, 2053 ok = true; 2054 2055 if (self.__state != 'stable' && self.__state != 'appearing') { 2056 2057 // if a timeout is not already running 2058 if (!self.__timeouts.open) { 2059 2060 self._trigger({ 2061 type: 'start', 2062 event: event, 2063 stop: function() { 2064 ok = false; 2065 } 2066 }); 2067 2068 if (ok) { 2069 2070 var delay = (event.type.indexOf('touch') == 0) ? 2071 self.__options.delayTouch : 2072 self.__options.delay; 2073 2074 if (delay[0]) { 2075 2076 self.__timeouts.open = setTimeout(function() { 2077 2078 self.__timeouts.open = null; 2079 2080 // open only if the pointer (mouse or touch) is still over the origin. 2081 // The check on the "meaningful event" can only be made here, after some 2082 // time has passed (to know if the touch was a swipe or not) 2083 if (self.__pointerIsOverOrigin && self._touchIsMeaningfulEvent(event)) { 2084 2085 // signal that we go on 2086 self._trigger('startend'); 2087 2088 self._open(event); 2089 } 2090 else { 2091 // signal that we cancel 2092 self._trigger('startcancel'); 2093 } 2094 }, delay[0]); 2095 } 2096 else { 2097 // signal that we go on 2098 self._trigger('startend'); 2099 2100 self._open(event); 2101 } 2102 } 2103 } 2104 } 2105 2106 return self; 2107 }, 2108 2109 /** 2110 * Meant for plugins to get their options 2111 * 2112 * @param {string} pluginName The name of the plugin that asks for its options 2113 * @param {object} defaultOptions The default options of the plugin 2114 * @returns {object} The options 2115 * @protected 2116 */ 2117 _optionsExtract: function(pluginName, defaultOptions) { 2118 2119 var self = this, 2120 options = $.extend(true, {}, defaultOptions); 2121 2122 // if the plugin options were isolated in a property named after the 2123 // plugin, use them (prevents conflicts with other plugins) 2124 var pluginOptions = self.__options[pluginName]; 2125 2126 // if not, try to get them as regular options 2127 if (!pluginOptions){ 2128 2129 pluginOptions = {}; 2130 2131 $.each(defaultOptions, function(optionName, value) { 2132 2133 var o = self.__options[optionName]; 2134 2135 if (o !== undefined) { 2136 pluginOptions[optionName] = o; 2137 } 2138 }); 2139 } 2140 2141 // let's merge the default options and the ones that were provided. We'd want 2142 // to do a deep copy but not let jQuery merge arrays, so we'll do a shallow 2143 // extend on two levels, that will be enough if options are not more than 1 2144 // level deep 2145 $.each(options, function(optionName, value) { 2146 2147 if (pluginOptions[optionName] !== undefined) { 2148 2149 if (( typeof value == 'object' 2150 && !(value instanceof Array) 2151 && value != null 2152 ) 2153 && 2154 ( typeof pluginOptions[optionName] == 'object' 2155 && !(pluginOptions[optionName] instanceof Array) 2156 && pluginOptions[optionName] != null 2157 ) 2158 ) { 2159 $.extend(options[optionName], pluginOptions[optionName]); 2160 } 2161 else { 2162 options[optionName] = pluginOptions[optionName]; 2163 } 2164 } 2165 }); 2166 2167 return options; 2168 }, 2169 2170 /** 2171 * Used at instantiation of the plugin, or afterwards by plugins that activate themselves 2172 * on existing instances 2173 * 2174 * @param {object} pluginName 2175 * @returns {self} 2176 * @protected 2177 */ 2178 _plug: function(pluginName) { 2179 2180 var plugin = $.tooltipster._plugin(pluginName); 2181 2182 if (plugin) { 2183 2184 // if there is a constructor for instances 2185 if (plugin.instance) { 2186 2187 // proxy non-private methods on the instance to allow new instance methods 2188 $.tooltipster.__bridge(plugin.instance, this, plugin.name); 2189 } 2190 } 2191 else { 2192 throw new Error('The "'+ pluginName +'" plugin is not defined'); 2193 } 2194 2195 return this; 2196 }, 2197 2198 /** 2199 * This will return true if the event is a mouse event which was 2200 * emulated by the browser after a touch event. This allows us to 2201 * really dissociate mouse and touch triggers. 2202 * 2203 * There is a margin of error if a real mouse event is fired right 2204 * after (within the delay shown below) a touch event on the same 2205 * element, but hopefully it should not happen often. 2206 * 2207 * @returns {boolean} 2208 * @protected 2209 */ 2210 _touchIsEmulatedEvent: function(event) { 2211 2212 var isEmulated = false, 2213 now = new Date().getTime(); 2214 2215 for (var i = this.__touchEvents.length - 1; i >= 0; i--) { 2216 2217 var e = this.__touchEvents[i]; 2218 2219 // delay, in milliseconds. It's supposed to be 300ms in 2220 // most browsers (350ms on iOS) to allow a double tap but 2221 // can be less (check out FastClick for more info) 2222 if (now - e.time < 500) { 2223 2224 if (e.target === event.target) { 2225 isEmulated = true; 2226 } 2227 } 2228 else { 2229 break; 2230 } 2231 } 2232 2233 return isEmulated; 2234 }, 2235 2236 /** 2237 * Returns false if the event was an emulated mouse event or 2238 * a touch event involved in a swipe gesture. 2239 * 2240 * @param {object} event 2241 * @returns {boolean} 2242 * @protected 2243 */ 2244 _touchIsMeaningfulEvent: function(event) { 2245 return ( 2246 (this._touchIsTouchEvent(event) && !this._touchSwiped(event.target)) 2247 || (!this._touchIsTouchEvent(event) && !this._touchIsEmulatedEvent(event)) 2248 ); 2249 }, 2250 2251 /** 2252 * Checks if an event is a touch event 2253 * 2254 * @param {object} event 2255 * @returns {boolean} 2256 * @protected 2257 */ 2258 _touchIsTouchEvent: function(event){ 2259 return event.type.indexOf('touch') == 0; 2260 }, 2261 2262 /** 2263 * Store touch events for a while to detect swiping and emulated mouse events 2264 * 2265 * @param {object} event 2266 * @returns {self} 2267 * @protected 2268 */ 2269 _touchRecordEvent: function(event) { 2270 2271 if (this._touchIsTouchEvent(event)) { 2272 event.time = new Date().getTime(); 2273 this.__touchEvents.push(event); 2274 } 2275 2276 return this; 2277 }, 2278 2279 /** 2280 * Returns true if a swipe happened after the last touchstart event fired on 2281 * event.target. 2282 * 2283 * We need to differentiate a swipe from a tap before we let the event open 2284 * or close the tooltip. A swipe is when a touchmove (scroll) event happens 2285 * on the body between the touchstart and the touchend events of an element. 2286 * 2287 * @param {object} target The HTML element that may have triggered the swipe 2288 * @returns {boolean} 2289 * @protected 2290 */ 2291 _touchSwiped: function(target) { 2292 2293 var swiped = false; 2294 2295 for (var i = this.__touchEvents.length - 1; i >= 0; i--) { 2296 2297 var e = this.__touchEvents[i]; 2298 2299 if (e.type == 'touchmove') { 2300 swiped = true; 2301 break; 2302 } 2303 else if ( 2304 e.type == 'touchstart' 2305 && target === e.target 2306 ) { 2307 break; 2308 } 2309 } 2310 2311 return swiped; 2312 }, 2313 2314 /** 2315 * Triggers an event on the instance emitters 2316 * 2317 * @returns {self} 2318 * @protected 2319 */ 2320 _trigger: function() { 2321 2322 var args = Array.prototype.slice.apply(arguments); 2323 2324 if (typeof args[0] == 'string') { 2325 args[0] = { type: args[0] }; 2326 } 2327 2328 // add properties to the event 2329 args[0].instance = this; 2330 args[0].origin = this._$origin ? this._$origin[0] : null; 2331 args[0].tooltip = this._$tooltip ? this._$tooltip[0] : null; 2332 2333 // note: the order of emitters matters 2334 this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate, args); 2335 $.tooltipster._trigger.apply($.tooltipster, args); 2336 this.__$emitterPublic.trigger.apply(this.__$emitterPublic, args); 2337 2338 return this; 2339 }, 2340 2341 /** 2342 * Deactivate a plugin on this instance 2343 * 2344 * @returns {self} 2345 * @protected 2346 */ 2347 _unplug: function(pluginName) { 2348 2349 var self = this; 2350 2351 // if the plugin has been activated on this instance 2352 if (self[pluginName]) { 2353 2354 var plugin = $.tooltipster._plugin(pluginName); 2355 2356 // if there is a constructor for instances 2357 if (plugin.instance) { 2358 2359 // unbridge 2360 $.each(plugin.instance, function(methodName, fn) { 2361 2362 // if the method exists (privates methods do not) and comes indeed from 2363 // this plugin (may be missing or come from a conflicting plugin). 2364 if ( self[methodName] 2365 && self[methodName].bridged === self[pluginName] 2366 ) { 2367 delete self[methodName]; 2368 } 2369 }); 2370 } 2371 2372 // destroy the plugin 2373 if (self[pluginName].__destroy) { 2374 self[pluginName].__destroy(); 2375 } 2376 2377 // remove the reference to the plugin instance 2378 delete self[pluginName]; 2379 } 2380 2381 return self; 2382 }, 2383 2384 /** 2385 * @see self::_close 2386 * @returns {self} 2387 * @public 2388 */ 2389 close: function(callback) { 2390 2391 if (!this.__destroyed) { 2392 this._close(null, callback); 2393 } 2394 else { 2395 this.__destroyError(); 2396 } 2397 2398 return this; 2399 }, 2400 2401 /** 2402 * Sets or gets the content of the tooltip 2403 * 2404 * @returns {mixed|self} 2405 * @public 2406 */ 2407 content: function(content) { 2408 2409 var self = this; 2410 2411 // getter method 2412 if (content === undefined) { 2413 return self.__Content; 2414 } 2415 // setter method 2416 else { 2417 2418 if (!self.__destroyed) { 2419 2420 // change the content 2421 self.__contentSet(content); 2422 2423 if (self.__Content !== null) { 2424 2425 // update the tooltip if it is open 2426 if (self.__state !== 'closed') { 2427 2428 // reset the content in the tooltip 2429 self.__contentInsert(); 2430 2431 // reposition and resize the tooltip 2432 self.reposition(); 2433 2434 // if we want to play a little animation showing the content changed 2435 if (self.__options.updateAnimation) { 2436 2437 if (env.hasTransitions) { 2438 2439 // keep the reference in the local scope 2440 var animation = self.__options.updateAnimation; 2441 2442 self._$tooltip.addClass('tooltipster-update-'+ animation); 2443 2444 // remove the class after a while. The actual duration of the 2445 // update animation may be shorter, it's set in the CSS rules 2446 setTimeout(function() { 2447 2448 if (self.__state != 'closed') { 2449 2450 self._$tooltip.removeClass('tooltipster-update-'+ animation); 2451 } 2452 }, 1000); 2453 } 2454 else { 2455 self._$tooltip.fadeTo(200, 0.5, function() { 2456 if (self.__state != 'closed') { 2457 self._$tooltip.fadeTo(200, 1); 2458 } 2459 }); 2460 } 2461 } 2462 } 2463 } 2464 else { 2465 self._close(); 2466 } 2467 } 2468 else { 2469 self.__destroyError(); 2470 } 2471 2472 return self; 2473 } 2474 }, 2475 2476 /** 2477 * Destroys the tooltip 2478 * 2479 * @returns {self} 2480 * @public 2481 */ 2482 destroy: function() { 2483 2484 var self = this; 2485 2486 if (!self.__destroyed) { 2487 2488 if
vendor: 14,673 bytes, lines 2488-3087
2488 (!self.__destroying) { 2489 2490 self.__destroying = true; 2491 2492 self._close(null, function() { 2493 2494 self._trigger('destroy'); 2495 2496 self.__destroying = false; 2497 self.__destroyed = true; 2498 2499 self._$origin 2500 .removeData(self.__namespace) 2501 // remove the open trigger listeners 2502 .off('.'+ self.__namespace +'-triggerOpen'); 2503 2504 // remove the touch listener 2505 $('body').off('.' + self.__namespace +'-triggerOpen'); 2506 2507 var ns = self._$origin.data('tooltipster-ns'); 2508 2509 // if the origin has been removed from DOM, its data may 2510 // well have been destroyed in the process and there would 2511 // be nothing to clean up or restore 2512 if (ns) { 2513 2514 // if there are no more tooltips on this element 2515 if (ns.length === 1) { 2516 2517 // optional restoration of a title attribute 2518 var title = null; 2519 if (self.__options.restoration == 'previous') { 2520 title = self._$origin.data('tooltipster-initialTitle'); 2521 } 2522 else if (self.__options.restoration == 'current') { 2523 2524 // old school technique to stringify when outerHTML is not supported 2525 title = (typeof self.__Content == 'string') ? 2526 self.__Content : 2527 $('<div></div>').append(self.__Content).html(); 2528 } 2529 2530 if (title) { 2531 self._$origin.attr('title', title); 2532 } 2533 2534 // final cleaning 2535 2536 self._$origin.removeClass('tooltipstered'); 2537 2538 self._$origin 2539 .removeData('tooltipster-ns') 2540 .removeData('tooltipster-initialTitle'); 2541 } 2542 else { 2543 // remove the instance namespace from the list of namespaces of 2544 // tooltips present on the element 2545 ns = $.grep(ns, function(el, i) { 2546 return el !== self.__namespace; 2547 }); 2548 self._$origin.data('tooltipster-ns', ns); 2549 } 2550 } 2551 2552 // last event 2553 self._trigger('destroyed'); 2554 2555 // unbind private and public event listeners 2556 self._off(); 2557 self.off(); 2558 2559 // remove external references, just in case 2560 self.__Content = null; 2561 self.__$emitterPrivate = null; 2562 self.__$emitterPublic = null; 2563 self.__options.parent = null; 2564 self._$origin = null; 2565 self._$tooltip = null; 2566 2567 // make sure the object is no longer referenced in there to prevent 2568 // memory leaks 2569 $.tooltipster.__instancesLatestArr = $.grep($.tooltipster.__instancesLatestArr, function(el, i) { 2570 return self !== el; 2571 }); 2572 2573 clearInterval(self.__garbageCollector); 2574 }); 2575 } 2576 } 2577 else { 2578 self.__destroyError(); 2579 } 2580 2581 // we return the scope rather than true so that the call to 2582 // .tooltipster('destroy') actually returns the matched elements 2583 // and applies to all of them 2584 return self; 2585 }, 2586 2587 /** 2588 * Disables the tooltip 2589 * 2590 * @returns {self} 2591 * @public 2592 */ 2593 disable: function() { 2594 2595 if (!this.__destroyed) { 2596 2597 // close first, in case the tooltip would not disappear on 2598 // its own (no close trigger) 2599 this._close(); 2600 this.__enabled = false; 2601 2602 return this; 2603 } 2604 else { 2605 this.__destroyError(); 2606 } 2607 2608 return this; 2609 }, 2610 2611 /** 2612 * Returns the HTML element of the origin 2613 * 2614 * @returns {self} 2615 * @public 2616 */ 2617 elementOrigin: function() { 2618 2619 if (!this.__destroyed) { 2620 return this._$origin[0]; 2621 } 2622 else { 2623 this.__destroyError(); 2624 } 2625 }, 2626 2627 /** 2628 * Returns the HTML element of the tooltip 2629 * 2630 * @returns {self} 2631 * @public 2632 */ 2633 elementTooltip: function() { 2634 return this._$tooltip ? this._$tooltip[0] : null; 2635 }, 2636 2637 /** 2638 * Enables the tooltip 2639 * 2640 * @returns {self} 2641 * @public 2642 */ 2643 enable: function() { 2644 this.__enabled = true; 2645 return this; 2646 }, 2647 2648 /** 2649 * Alias, deprecated in 4.0.0 2650 * 2651 * @param {function} callback 2652 * @returns {self} 2653 * @public 2654 */ 2655 hide: function(callback) { 2656 return this.close(callback); 2657 }, 2658 2659 /** 2660 * Returns the instance 2661 * 2662 * @returns {self} 2663 * @public 2664 */ 2665 instance: function() { 2666 return this; 2667 }, 2668 2669 /** 2670 * For public use only, not to be used by plugins (use ::_off() instead) 2671 * 2672 * @returns {self} 2673 * @public 2674 */ 2675 off: function() { 2676 2677 if (!this.__destroyed) { 2678 this.__$emitterPublic.off.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); 2679 } 2680 2681 return this; 2682 }, 2683 2684 /** 2685 * For public use only, not to be used by plugins (use ::_on() instead) 2686 * 2687 * @returns {self} 2688 * @public 2689 */ 2690 on: function() { 2691 2692 if (!this.__destroyed) { 2693 this.__$emitterPublic.on.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); 2694 } 2695 else { 2696 this.__destroyError(); 2697 } 2698 2699 return this; 2700 }, 2701 2702 /** 2703 * For public use only, not to be used by plugins 2704 * 2705 * @returns {self} 2706 * @public 2707 */ 2708 one: function() { 2709 2710 if (!this.__destroyed) { 2711 this.__$emitterPublic.one.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); 2712 } 2713 else { 2714 this.__destroyError(); 2715 } 2716 2717 return this; 2718 }, 2719 2720 /** 2721 * @see self::_open 2722 * @returns {self} 2723 * @public 2724 */ 2725 open: function(callback) { 2726 2727 if (!this.__destroyed && !this.__destroying) { 2728 this._open(null, callback); 2729 } 2730 else { 2731 this.__destroyError(); 2732 } 2733 2734 return this; 2735 }, 2736 2737 /** 2738 * Get or set options. For internal use and advanced users only. 2739 * 2740 * @param {string} o Option name 2741 * @param {mixed} val optional A new value for the option 2742 * @return {mixed|self} If val is omitted, the value of the option 2743 * is returned, otherwise the instance itself is returned 2744 * @public 2745 */ 2746 option: function(o, val) { 2747 2748 // getter 2749 if (val === undefined) { 2750 return this.__options[o]; 2751 } 2752 // setter 2753 else { 2754 2755 if (!this.__destroyed) { 2756 2757 // change value 2758 this.__options[o] = val; 2759 2760 // format 2761 this.__optionsFormat(); 2762 2763 // re-prepare the triggers if needed 2764 if ($.inArray(o, ['trigger', 'triggerClose', 'triggerOpen']) >= 0) { 2765 this.__prepareOrigin(); 2766 } 2767 2768 if (o === 'selfDestruction') { 2769 this.__prepareGC(); 2770 } 2771 } 2772 else { 2773 this.__destroyError(); 2774 } 2775 2776 return this; 2777 } 2778 }, 2779 2780 /** 2781 * This method is in charge of setting the position and size properties of the tooltip. 2782 * All the hard work is delegated to the display plugin. 2783 * Note: The tooltip may be detached from the DOM at the moment the method is called 2784 * but must be attached by the end of the method call. 2785 * 2786 * @param {object} event For internal use only. Defined if an event such as 2787 * window resizing triggered the repositioning 2788 * @param {boolean} tooltipIsDetached For internal use only. Set this to true if you 2789 * know that the tooltip not being in the DOM is not an issue (typically when the 2790 * tooltip element has just been created but has not been added to the DOM yet). 2791 * @returns {self} 2792 * @public 2793 */ 2794 reposition: function(event, tooltipIsDetached) { 2795 2796 var self = this; 2797 2798 if (!self.__destroyed) { 2799 2800 // if the tooltip is still open and the origin is still in the DOM 2801 if (self.__state != 'closed' && bodyContains(self._$origin)) { 2802 2803 // if the tooltip has not been removed from DOM manually (or if it 2804 // has been detached on purpose) 2805 if (tooltipIsDetached || bodyContains(self._$tooltip)) { 2806 2807 if (!tooltipIsDetached) { 2808 // detach in case the tooltip overflows the window and adds 2809 // scrollbars to it, so __geometry can be accurate 2810 self._$tooltip.detach(); 2811 } 2812 2813 // refresh the geometry object before passing it as a helper 2814 self.__Geometry = self.__geometry(); 2815 2816 // let a plugin fo the rest 2817 self._trigger({ 2818 type: 'reposition', 2819 event: event, 2820 helper: { 2821 geo: self.__Geometry 2822 } 2823 }); 2824 } 2825 } 2826 } 2827 else { 2828 self.__destroyError(); 2829 } 2830 2831 return self; 2832 }, 2833 2834 /** 2835 * Alias, deprecated in 4.0.0 2836 * 2837 * @param callback 2838 * @returns {self} 2839 * @public 2840 */ 2841 show: function(callback) { 2842 return this.open(callback); 2843 }, 2844 2845 /** 2846 * Returns some properties about the instance 2847 * 2848 * @returns {object} 2849 * @public 2850 */ 2851 status: function() { 2852 2853 return { 2854 destroyed: this.__destroyed, 2855 destroying: this.__destroying, 2856 enabled: this.__enabled, 2857 open: this.__state !== 'closed', 2858 state: this.__state 2859 }; 2860 }, 2861 2862 /** 2863 * For public use only, not to be used by plugins 2864 * 2865 * @returns {self} 2866 * @public 2867 */ 2868 triggerHandler: function() { 2869 2870 if (!this.__destroyed) { 2871 this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); 2872 } 2873 else { 2874 this.__destroyError(); 2875 } 2876 2877 return this; 2878 } 2879}; 2880 2881$.fn.tooltipster = function() { 2882 2883 // for using in closures 2884 var args = Array.prototype.slice.apply(arguments), 2885 // common mistake: an HTML element can't be in several tooltips at the same time 2886 contentCloningWarning = 'You are using a single HTML element as content for several tooltips. You probably want to set the contentCloning option to TRUE.'; 2887 2888 // this happens with $(sel).tooltipster(...) when $(sel) does not match anything 2889 if (this.length === 0) { 2890 2891 // still chainable 2892 return this; 2893 } 2894 // this happens when calling $(sel).tooltipster('methodName or options') 2895 // where $(sel) matches one or more elements 2896 else { 2897 2898 // method calls 2899 if (typeof args[0] === 'string') { 2900 2901 var v = '#*$~&'; 2902 2903 this.each(function() { 2904 2905 // retrieve the namepaces of the tooltip(s) that exist on that element. 2906 // We will interact with the first tooltip only. 2907 var ns = $(this).data('tooltipster-ns'), 2908 // self represents the instance of the first tooltipster plugin 2909 // associated to the current HTML object of the loop 2910 self = ns ? $(this).data(ns[0]) : null; 2911 2912 // if the current element holds a tooltipster instance 2913 if (self) { 2914 2915 if (typeof self[args[0]] === 'function') { 2916 2917 if ( this.length > 1 2918 && args[0] == 'content' 2919 && ( args[1] instanceof $ 2920 || (typeof args[1] == 'object' && args[1] != null && args[1].tagName) 2921 ) 2922 && !self.__options.contentCloning 2923 && self.__options.debug 2924 ) { 2925 console.log(contentCloningWarning); 2926 } 2927 2928 // note : args[1] and args[2] may not be defined 2929 var resp = self[args[0]](args[1], args[2]); 2930 } 2931 else { 2932 throw new Error('Unknown method "'+ args[0] +'"'); 2933 } 2934 2935 // if the function returned anything other than the instance 2936 // itself (which implies chaining, except for the `instance` method) 2937 if (resp !== self || args[0] === 'instance') { 2938 2939 v = resp; 2940 2941 // return false to stop .each iteration on the first element 2942 // matched by the selector 2943 return false; 2944 } 2945 } 2946 else { 2947 throw new Error('You called Tooltipster\'s "'+ args[0] +'" method on an uninitialized element'); 2948 } 2949 }); 2950 2951 return (v !== '#*$~&') ? v : this; 2952 } 2953 // first argument is undefined or an object: the tooltip is initializing 2954 else { 2955 2956 // reset the array of last initialized objects 2957 $.tooltipster.__instancesLatestArr = []; 2958 2959 // is there a defined value for the multiple option in the options object ? 2960 var multipleIsSet = args[0] && args[0].multiple !== undefined, 2961 // if the multiple option is set to true, or if it's not defined but 2962 // set to true in the defaults 2963 multiple = (multipleIsSet && args[0].multiple) || (!multipleIsSet && defaults.multiple), 2964 // same for content 2965 contentIsSet = args[0] && args[0].content !== undefined, 2966 content = (contentIsSet && args[0].content) || (!contentIsSet && defaults.content), 2967 // same for contentCloning 2968 contentCloningIsSet = args[0] && args[0].contentCloning !== undefined, 2969 contentCloning = 2970 (contentCloningIsSet && args[0].contentCloning) 2971 || (!contentCloningIsSet && defaults.contentCloning), 2972 // same for debug 2973 debugIsSet = args[0] && args[0].debug !== undefined, 2974 debug = (debugIsSet && args[0].debug) || (!debugIsSet && defaults.debug); 2975 2976 if ( this.length > 1 2977 && ( content instanceof $ 2978 || (typeof content == 'object' && content != null && content.tagName) 2979 ) 2980 && !contentCloning 2981 && debug 2982 ) { 2983 console.log(contentCloningWarning); 2984 } 2985 2986 // create a tooltipster instance for each element if it doesn't 2987 // already have one or if the multiple option is set, and attach the 2988 // object to it 2989 this.each(function() { 2990 2991 var go = false, 2992 $this = $(this), 2993 ns = $this.data('tooltipster-ns'), 2994 obj = null; 2995 2996 if (!ns) { 2997 go = true; 2998 } 2999 else if (multiple) { 3000 go = true; 3001 } 3002 else if (debug) { 3003 console.log('Tooltipster: one or more tooltips are already attached to the element below. Ignoring.'); 3004 console.log(this); 3005 } 3006 3007 if (go) { 3008 obj = new $.Tooltipster(this, args[0]); 3009 3010 // save the reference of the new instance 3011 if (!ns) ns = []; 3012 ns.push(obj.__namespace); 3013 $this.data('tooltipster-ns', ns); 3014 3015 // save the instance itself 3016 $this.data(obj.__namespace, obj); 3017 3018 // call our constructor custom function. 3019 // we do this here and not in ::init() because we wanted 3020 // the object to be saved in $this.data before triggering 3021 // it 3022 if (obj.__options.functionInit) { 3023 obj.__options.functionInit.call(obj, obj, { 3024 origin: this 3025 }); 3026 } 3027 3028 // and now the event, for the plugins and core emitter 3029 obj._trigger('init'); 3030 } 3031 3032 $.tooltipster.__instancesLatestArr.push(obj); 3033 }); 3034 3035 return this; 3036 } 3037 } 3038}; 3039 3040// Utilities 3041 3042/** 3043 * A class to check if a tooltip can fit in given dimensions 3044 * 3045 * @param {object} $tooltip The jQuery wrapped tooltip element, or a clone of it 3046 */ 3047function Ruler($tooltip) { 3048 3049 // list of instance variables 3050 3051 this.$container; 3052 this.constraints = null; 3053 this.__$tooltip; 3054 3055 this.__init($tooltip); 3056} 3057 3058Ruler.prototype = { 3059 3060 /** 3061 * Move the tooltip into an invisible div that does not allow overflow to make 3062 * size tests. Note: the tooltip may or may not be attached to the DOM at the 3063 * moment this method is called, it does not matter. 3064 * 3065 * @param {object} $tooltip The object to test. May be just a clone of the 3066 * actual tooltip. 3067 * @private 3068 */ 3069 __init: function($tooltip) { 3070 3071 this.__$tooltip = $tooltip; 3072 3073 this.__$tooltip 3074 .css({ 3075 // for some reason we have to specify top and left 0 3076 left: 0, 3077 // any overflow will be ignored while measuring 3078 overflow: 'hidden', 3079 // positions at (0,0) without the div using 100% of the available width 3080 position: 'absolute', 3081 top: 0 3082 }) 3083 // overflow must be auto during the test. We re-set this in case 3084 // it were modified by the user 3085 .find('.tooltipster-content') 3086 .css('overflow', 'auto'); 3087
3088 this.$container = $('<div class="tooltipster-ruler"></div>') 3089 .append(this.__$tooltip) 3090 .appendTo('body'); 3091 }, 3092 3093 /** 3094 * Force the browser to redraw (re-render) the tooltip immediately. This is required 3095 * when you changed some CSS properties and need to make something with it 3096 * immediately, without waiting for the browser to redraw at the end of instructions. 3097 * 3098 * @see http://stackoverflow.com/questions/3485365/how-can-i-force-webkit-to-redraw-repaint-to-propagate-style-changes 3099 * @private 3100 */ 3101 __forceRedraw: function() { 3102 3103 // note: this would work but for Webkit only 3104 //this.__$tooltip.close(); 3105 //this.__$tooltip[0].offsetHeight; 3106 //this.__$tooltip.open(); 3107 3108 // works in FF too 3109 var $p = this.__$tooltip.parent(); 3110 this.__$tooltip.detach(); 3111 this.__$tooltip.appendTo($p); 3112 }, 3113 3114 /** 3115 * Set maximum dimensions for the tooltip. A call to ::measure afterwards 3116 * will tell us if the content overflows or if it's ok 3117 * 3118 * @param {int} width 3119 * @param {int} height 3120 * @return {Ruler} 3121 * @public 3122 */ 3123 constrain: function(width, height) { 3124 3125 this.constraints = { 3126 width: width, 3127 height: height 3128 }; 3129 3130 this.__$tooltip.css({ 3131 // we disable display:flex, otherwise the content would overflow without 3132 // creating horizontal scrolling (which we need to detect). 3133 display: 'block', 3134 // reset any previous height 3135 height: '', 3136 // we'll check if horizontal scrolling occurs 3137 overflow: 'auto', 3138 // we'll set the width and see what height is generated and if there 3139 // is horizontal overflow 3140 width: width 3141 }); 3142 3143 return this; 3144 }, 3145 3146 /** 3147 * Reset the tooltip content overflow and remove the test container 3148 * 3149 * @returns {Ruler} 3150 * @public 3151 */ 3152 destroy: function() { 3153 3154 // in case the element was not a clone 3155 this.__$tooltip 3156 .detach() 3157 .find('.tooltipster-content') 3158 .css({ 3159 // reset to CSS value 3160 display: '', 3161 overflow: '' 3162 }); 3163 3164 this.$container.remove(); 3165 }, 3166 3167 /** 3168 * Removes any constraints 3169 * 3170 * @returns {Ruler} 3171 * @public 3172 */ 3173 free: function() { 3174 3175 this.constraints = null; 3176 3177 // reset to natural size 3178 this.__$tooltip.css({ 3179 display: '', 3180 height: '', 3181 overflow: 'visible', 3182 width: '' 3183 }); 3184 3185 return this; 3186 }, 3187 3188 /** 3189 * Returns the size of the tooltip. When constraints are applied, also returns
3190 * whether the tooltip fits in the provided dimensions. 3191 * The idea is to see if the new height is small enough and if the content does 3192 * not overflow horizontally. 3193 * 3194 * @param {int} width 3195 * @param {int} height 3196 * @returns {object} An object with a bool `fits` property and a `size` property 3197 * @public 3198 */ 3199 measure: function() { 3200 3201 this.__forceRedraw(); 3202 3203 var tooltipBcr = this.__$tooltip[0].getBoundingClientRect(), 3204 result = { size: { 3205 // bcr.width/height are not defined in IE8- but in this 3206 // case, bcr.right/bottom will have the same value 3207 // except in iOS 8+ where tooltipBcr.bottom/right are wrong 3208 // after scrolling for reasons yet to be determined 3209 height: tooltipBcr.height || tooltipBcr.bottom, 3210 width: tooltipBcr.width || tooltipBcr.right 3211 }}; 3212 3213 if (this.constraints) { 3214 3215 // note: we used to use offsetWidth instead of boundingRectClient but 3216 // it returned rounded values, causing issues with sub-pixel layouts. 3217 3218 // note2: noticed that the bcrWidth of text content of a div was once 3219 // greater than the bcrWidth of its container by 1px, causing the final 3220 // tooltip box to be too small for its content. However, evaluating 3221 // their widths one against the other (below) surprisingly returned
vendor: 3,354 bytes, lines 3222-3317
3222 // equality. Happened only once in Chrome 48, was not able to reproduce 3223 // => just having fun with float position values... 3224 3225 var $content = this.__$tooltip.find('.tooltipster-content'), 3226 height = this.__$tooltip.outerHeight(), 3227 contentBcr = $content[0].getBoundingClientRect(), 3228 fits = { 3229 height: height <= this.constraints.height, 3230 width: ( 3231 // this condition accounts for min-width property that 3232 // may apply 3233 tooltipBcr.width <= this.constraints.width 3234 // the -1 is here because scrollWidth actually returns 3235 // a rounded value, and may be greater than bcr.width if 3236 // it was rounded up. This may cause an issue for contents 3237 // which actually really overflow by 1px or so, but that 3238 // should be rare. Not sure how to solve this efficiently. 3239 // See http://blogs.msdn.com/b/ie/archive/2012/02/17/sub-pixel-rendering-and-the-css-object-model.aspx 3240 && contentBcr.width >= $content[0].scrollWidth - 1 3241 ) 3242 }; 3243 3244 result.fits = fits.height && fits.width; 3245 } 3246 3247 // old versions of IE get the width wrong for some reason and it causes 3248 // the text to be broken to a new line, so we round it up. If the width 3249 // is the width of the screen though, we can assume it is accurate. 3250 if ( env.IE 3251 && env.IE <= 11 3252 && result.size.width !== env.window.document.documentElement.clientWidth 3253 ) { 3254 result.size.width = Math.ceil(result.size.width) + 1; 3255 } 3256 3257 return result; 3258 } 3259}; 3260 3261// quick & dirty compare function, not bijective nor multidimensional 3262function areEqual(a,b) { 3263 var same = true; 3264 $.each(a, function(i, _) { 3265 if (b[i] === undefined || a[i] !== b[i]) { 3266 same = false; 3267 return false; 3268 } 3269 }); 3270 return same; 3271} 3272 3273/** 3274 * A fast function to check if an element is still in the DOM. It 3275 * tries to use an id as ids are indexed by the browser, or falls 3276 * back to jQuery's `contains` method. May fail if two elements 3277 * have the same id, but so be it 3278 * 3279 * @param {object} $obj A jQuery-wrapped HTML element 3280 * @return {boolean} 3281 */ 3282function bodyContains($obj) { 3283 var id = $obj.attr('id'), 3284 el = id ? env.window.document.getElementById(id) : null; 3285 // must also check that the element with the id is the one we want 3286 return el ? el === $obj[0] : $.contains(env.window.document.body, $obj[0]); 3287} 3288 3289// detect IE versions for dirty fixes 3290var uA = navigator.userAgent.toLowerCase(); 3291if (uA.indexOf('msie') != -1) env.IE = parseInt(uA.split('msie')[1]); 3292else if (uA.toLowerCase().indexOf('trident') !== -1 && uA.indexOf(' rv:11') !== -1) env.IE = 11; 3293else if (uA.toLowerCase().indexOf('edge/') != -1) env.IE = parseInt(uA.toLowerCase().split('edge/')[1]); 3294 3295// detecting support for CSS transitions 3296function transitionSupport() { 3297 3298 // env.window is not defined yet when this is called 3299 if (!win) return false; 3300 3301 var b = win.document.body || win.document.documentElement, 3302 s = b.style, 3303 p = 'transition', 3304 v = ['Moz', 'Webkit', 'Khtml', 'O', 'ms']; 3305 3306 if (typeof s[p] == 'string') { return true; } 3307 3308 p = p.charAt(0).toUpperCase() + p.substr(1); 3309 for (var i=0; i<v.length; i++) { 3310 if (typeof s[v[i] + p] == 'string') { return true; } 3311 } 3312 return false; 3313} 3314 3315// we'll return jQuery for plugins not to have to declare it as a dependency, 3316// but it's done by a build task since it should be included only once at the 3317// end when we concatenate the core
vendor: 5,237 bytes, lines 3317-3503
3317 file with a plugin 3318// sideTip is Tooltipster's default plugin. 3319// This file will be UMDified by a build task. 3320 3321var pluginName = 'tooltipster.sideTip'; 3322 3323$.tooltipster._plugin({ 3324 name: pluginName, 3325 instance: { 3326 /** 3327 * Defaults are provided as a function for an easy override by inheritance 3328 * 3329 * @return {object} An object with the defaults options 3330 * @private 3331 */ 3332 __defaults: function() { 3333 3334 return { 3335 // if the tooltip should display an arrow that points to the origin 3336 arrow: true, 3337 // the distance in pixels between the tooltip and the origin 3338 distance: 6, 3339 // allows to easily change the position of the tooltip 3340 functionPosition: null, 3341 maxWidth: null, 3342 // used to accomodate the arrow of tooltip if there is one. 3343 // First to make sure that the arrow target is not too close 3344 // to the edge of the tooltip, so the arrow does not overflow 3345 // the tooltip. Secondly when we reposition the tooltip to 3346 // make sure that it's positioned in such a way that the arrow is 3347 // still pointing at the target (and not a few pixels beyond it). 3348 // It should be equal to or greater than half the width of 3349 // the arrow (by width we mean the size of the side which touches 3350 // the side of the tooltip). 3351 minIntersection: 16, 3352 minWidth: 0, 3353 // deprecated in 4.0.0. Listed for _optionsExtract to pick it up 3354 position: null, 3355 side: 'top', 3356 // set to false to position the tooltip relatively to the document rather 3357 // than the window when we open it 3358 viewportAware: true 3359 }; 3360 }, 3361 3362 /** 3363 * Run once: at instantiation of the plugin 3364 * 3365 * @param {object} instance The tooltipster object that instantiated this plugin 3366 * @private 3367 */ 3368 __init: function(instance) { 3369 3370 var self = this; 3371 3372 // list of instance variables 3373 3374 self.__instance = instance; 3375 self.__namespace = 'tooltipster-sideTip-'+ Math.round(Math.random()*1000000); 3376 self.__previousState = 'closed'; 3377 self.__options; 3378 3379 // initial formatting 3380 self.__optionsFormat(); 3381 3382 self.__instance._on('state.'+ self.__namespace, function(event) { 3383 3384 if (event.state == 'closed') { 3385 self.__close(); 3386 } 3387 else if (event.state == 'appearing' && self.__previousState == 'closed') { 3388 self.__create(); 3389 } 3390 3391 self.__previousState = event.state; 3392 }); 3393 3394 // reformat every time the options are changed 3395 self.__instance._on('options.'+ self.__namespace, function() { 3396 self.__optionsFormat(); 3397 }); 3398 3399 self.__instance._on('reposition.'+ self.__namespace, function(e) { 3400 self.__reposition(e.event, e.helper); 3401 }); 3402 }, 3403 3404 /** 3405 * Called when the tooltip has closed 3406 * 3407 * @private 3408 */ 3409 __close: function() { 3410 3411 // detach our content object first, so the next jQuery's remove() 3412 // call does not unbind its event handlers 3413 if (this.__instance.content() instanceof $) { 3414 this.__instance.content().detach(); 3415 } 3416 3417 // remove the tooltip from the DOM 3418 this.__instance._$tooltip.remove(); 3419 this.__instance._$tooltip = null; 3420 }, 3421 3422 /** 3423 * Creates the HTML element of the tooltip. 3424 * 3425 * @private 3426 */ 3427 __create: function() { 3428 3429 // note: we wrap with a .tooltipster-box div to be able to set a margin on it 3430 // (.tooltipster-base must not have one) 3431 var $html = $( 3432 '<div class="tooltipster-base tooltipster-sidetip">' + 3433 '<div class="tooltipster-box">' + 3434 '<div class="tooltipster-content"></div>' + 3435 '</div>' + 3436 '<div class="tooltipster-arrow">' + 3437 '<div class="tooltipster-arrow-uncropped">' + 3438 '<div class="tooltipster-arrow-border"></div>' + 3439 '<div class="tooltipster-arrow-background"></div>' + 3440 '</div>' + 3441 '</div>' + 3442 '</div>' 3443 ); 3444 3445 // hide arrow if asked 3446 if (!this.__options.arrow) { 3447 $html 3448 .find('.tooltipster-box') 3449 .css('margin', 0) 3450 .end() 3451 .find('.tooltipster-arrow') 3452 .hide(); 3453 } 3454 3455 // apply min/max width if asked 3456 if (this.__options.minWidth) { 3457 $html.css('min-width', this.__options.minWidth + 'px'); 3458 } 3459 if (this.__options.maxWidth) { 3460 $html.css('max-width', this.__options.maxWidth + 'px'); 3461 } 3462 3463 this.__instance._$tooltip = $html; 3464 3465 // tell the instance that the tooltip element has been created 3466 this.__instance._trigger('created'); 3467 }, 3468 3469 /** 3470 * Used when the plugin is to be unplugged 3471 * 3472 * @private 3473 */ 3474 __destroy: function() { 3475 this.__instance._off('.'+ self.__namespace); 3476 }, 3477 3478 /** 3479 * (Re)compute this.__options from the options declared to the instance 3480 * 3481 * @private 3482 */ 3483 __optionsFormat: function() { 3484 3485 var self = this; 3486 3487 // get the options 3488 self.__options = self.__instance._optionsExtract(pluginName, self.__defaults()); 3489 3490 // for backward compatibility, deprecated in v4.0.0 3491 if (self.__options.position) { 3492 self.__options.side = self.__options.position; 3493 } 3494 3495 // options formatting 3496 3497 // format distance as a four-cell array if it ain't one yet and then make 3498 // it an object with top/bottom/left/right properties 3499 if (typeof self.__options.distance != 'object') { 3500 self.__options.distance = [self.__options.distance]; 3501 } 3502 if (self.__options.distance.length < 4) { 3503
3503 3504 if (self.__options.distance[1] === undefined) self.__options.distance[1] = self.__options.distance[0]; 3505 if (self.__options.distance[2] === undefined) self.__options.distance[2] = self.__options.distance[0]; 3506 if (self.__options.distance[3] === undefined) self.__options.distance[3] = self.__options.distance[1]; 3507 3508 self.__options.distance = { 3509 top: self.__options.distance[0], 3510 right: self.__options.distance[1], 3511 bottom: self.__options.distance[2], 3512 left: self.__options.distance[3] 3513 }; 3514 }
vendor: 18,138 bytes, lines 3514-4038
3514 3515 3516 // let's transform: 3517 // 'top' into ['top', 'bottom', 'right', 'left'] 3518 // 'right' into ['right', 'left', 'top', 'bottom'] 3519 // 'bottom' into ['bottom', 'top', 'right', 'left'] 3520 // 'left' into ['left', 'right', 'top', 'bottom'] 3521 if (typeof self.__options.side == 'string') { 3522 3523 var opposites = { 3524 'top': 'bottom', 3525 'right': 'left', 3526 'bottom': 'top', 3527 'left': 'right' 3528 }; 3529 3530 self.__options.side = [self.__options.side, opposites[self.__options.side]]; 3531 3532 if (self.__options.side[0] == 'left' || self.__options.side[0] == 'right') { 3533 self.__options.side.push('top', 'bottom'); 3534 } 3535 else { 3536 self.__options.side.push('right', 'left'); 3537 } 3538 } 3539 3540 // misc 3541 // disable the arrow in IE6 unless the arrow option was explicitly set to true 3542 if ( $.tooltipster._env.IE === 6 3543 && self.__options.arrow !== true 3544 ) { 3545 self.__options.arrow = false; 3546 } 3547 }, 3548 3549 /** 3550 * This method must compute and set the positioning properties of the 3551 * tooltip (left, top, width, height, etc.). It must also make sure the 3552 * tooltip is eventually appended to its parent (since the element may be 3553 * detached from the DOM at the moment the method is called). 3554 * 3555 * We'll evaluate positioning scenarios to find which side can contain the 3556 * tooltip in the best way. We'll consider things relatively to the window 3557 * (unless the user asks not to), then to the document (if need be, or if the 3558 * user explicitly requires the tests to run on the document). For each 3559 * scenario, measures are taken, allowing us to know how well the tooltip 3560 * is going to fit. After that, a sorting function will let us know what 3561 * the best scenario is (we also allow the user to choose his favorite 3562 * scenario by using an event). 3563 * 3564 * @param {object} helper An object that contains variables that plugin 3565 * creators may find useful (see below) 3566 * @param {object} helper.geo An object with many layout properties 3567 * about objects of interest (window, document, origin). This should help 3568 * plugin users compute the optimal position of the tooltip 3569 * @private 3570 */ 3571 __reposition: function(event, helper) { 3572 3573 var self = this, 3574 finalResult, 3575 // to know where to put the tooltip, we need to know on which point 3576 // of the x or y axis we should center it. That coordinate is the target 3577 targets = self.__targetFind(helper), 3578 testResults = []; 3579 3580 // make sure the tooltip is detached while we make tests on a clone 3581 self.__instance._$tooltip.detach(); 3582 3583 // we could actually provide the original element to the Ruler and 3584 // not a clone, but it just feels right to keep it out of the 3585 // machinery. 3586 var $clone = self.__instance._$tooltip.clone(), 3587 // start position tests session 3588 ruler = $.tooltipster._getRuler($clone), 3589 satisfied = false, 3590 animation = self.__instance.option('animation'); 3591 3592 // an animation class could contain properties that distort the size 3593 if (animation) { 3594 $clone.removeClass('tooltipster-'+ animation); 3595 } 3596 3597 // start evaluating scenarios 3598 $.each(['window', 'document'], function(i, container) { 3599 3600 var takeTest = null; 3601 3602 // let the user decide to keep on testing or not 3603 self.__instance._trigger({ 3604 container: container, 3605 helper: helper, 3606 satisfied: satisfied, 3607 takeTest: function(bool) { 3608 takeTest = bool; 3609 }, 3610 results: testResults, 3611 type: 'positionTest' 3612 }); 3613 3614 if ( takeTest == true 3615 || ( takeTest != false 3616 && satisfied == false 3617 // skip the window scenarios if asked. If they are reintegrated by 3618 // the callback of the positionTest event, they will have to be 3619 // excluded using the callback of positionTested 3620 && (container != 'window' || self.__options.viewportAware) 3621 ) 3622 ) { 3623 3624 // for each allowed side 3625 for (var i=0; i < self.__options.side.length; i++) { 3626 3627 var distance = { 3628 horizontal: 0, 3629 vertical: 0 3630 }, 3631 side = self.__options.side[i]; 3632 3633 if (side == 'top' || side == 'bottom') { 3634 distance.vertical = self.__options.distance[side]; 3635 } 3636 else { 3637 distance.horizontal = self.__options.distance[side]; 3638 } 3639 3640 // this may have an effect on the size of the tooltip if there are css 3641 // rules for the arrow or something else 3642 self.__sideChange($clone, side); 3643 3644 $.each(['natural', 'constrained'], function(i, mode) { 3645 3646 takeTest = null; 3647 3648 // emit an event on the instance 3649 self.__instance._trigger({ 3650 container: container, 3651 event: event, 3652 helper: helper, 3653 mode: mode, 3654 results: testResults, 3655 satisfied: satisfied, 3656 side: side, 3657 takeTest: function(bool) { 3658 takeTest = bool; 3659 }, 3660 type: 'positionTest' 3661 }); 3662 3663 if ( takeTest == true 3664 || ( takeTest != false 3665 && satisfied == false 3666 ) 3667 ) { 3668 3669 var testResult = { 3670 container: container, 3671 // we let the distance as an object here, it can make things a little easier 3672 // during the user's calculations at positionTest/positionTested 3673 distance: distance, 3674 // whether the tooltip can fit in the size of the viewport (does not mean 3675 // that we'll be able to make it initially entirely visible, see 'whole') 3676 fits: null, 3677 mode: mode, 3678 outerSize: null, 3679 side: side, 3680 size: null, 3681 target: targets[side], 3682 // check if the origin has enough surface on screen for the tooltip to 3683 // aim at it without overflowing the viewport (this is due to the thickness 3684 // of the arrow represented by the minIntersection length). 3685 // If not, the tooltip will have to be partly or entirely off screen in 3686 // order to stay docked to the origin. This value will stay null when the 3687 // container is the document, as it is not relevant 3688 whole: null 3689 }; 3690 3691 // get the size of the tooltip with or without size constraints 3692 var rulerConfigured = (mode == 'natural') ? 3693 ruler.free() : 3694 ruler.constrain( 3695 helper.geo.available[container][side].width - distance.horizontal, 3696 helper.geo.available[container][side].height - distance.vertical 3697 ), 3698 rulerResults = rulerConfigured.measure(); 3699 3700 testResult.size = rulerResults.size; 3701 testResult.outerSize = { 3702 height: rulerResults.size.height + distance.vertical, 3703 width: rulerResults.size.width + distance.horizontal 3704 }; 3705 3706 if (mode == 'natural') { 3707 3708 if( helper.geo.available[container][side].width >= testResult.outerSize.width 3709 && helper.geo.available[container][side].height >= testResult.outerSize.height 3710 ) { 3711 testResult.fits = true; 3712 } 3713 else { 3714 testResult.fits = false; 3715 } 3716 } 3717 else { 3718 testResult.fits = rulerResults.fits; 3719 } 3720 3721 if (container == 'window') { 3722 3723 if (!testResult.fits) { 3724 testResult.whole = false; 3725 } 3726 else { 3727 if (side == 'top' || side == 'bottom') { 3728 3729 testResult.whole = ( 3730 helper.geo.origin.windowOffset.right >= self.__options.minIntersection 3731 && helper.geo.window.size.width - helper.geo.origin.windowOffset.left >= self.__options.minIntersection 3732 ); 3733 } 3734 else { 3735 testResult.whole = ( 3736 helper.geo.origin.windowOffset.bottom >= self.__options.minIntersection 3737 && helper.geo.window.size.height - helper.geo.origin.windowOffset.top >= self.__options.minIntersection 3738 ); 3739 } 3740 } 3741 } 3742 3743 testResults.push(testResult); 3744 3745 // we don't need to compute more positions if we have one fully on screen 3746 if (testResult.whole) { 3747 satisfied = true; 3748 } 3749 else { 3750 // don't run the constrained test unless the natural width was greater 3751 // than the available width, otherwise it's pointless as we know it 3752 // wouldn't fit either 3753 if ( testResult.mode == 'natural' 3754 && ( testResult.fits 3755 || testResult.size.width <= helper.geo.available[container][side].width 3756 ) 3757 ) { 3758 return false; 3759 } 3760 } 3761 } 3762 }); 3763 } 3764 } 3765 }); 3766 3767 // the user may eliminate the unwanted scenarios from testResults, but he's 3768 // not supposed to alter them at this point. functionPosition and the 3769 // position event serve that purpose. 3770 self.__instance._trigger({ 3771 edit: function(r) { 3772 testResults = r; 3773 }, 3774 event: event, 3775 helper: helper, 3776 results: testResults, 3777 type: 'positionTested' 3778 }); 3779 3780 /** 3781 * Sort the scenarios to find the favorite one. 3782 * 3783 * The favorite scenario is when we can fully display the tooltip on screen, 3784 * even if it means that the middle of the tooltip is no longer centered on 3785 * the middle of the origin (when the origin is near the edge of the screen 3786 * or even partly off screen). We want the tooltip on the preferred side, 3787 * even if it means that we have to use a constrained size rather than a 3788 * natural one (as long as it fits). When the origin is off screen at the top 3789 * the tooltip will be positioned at the bottom (if allowed), if the origin 3790 * is off screen on the right, it will be positioned on the left, etc. 3791 * If there are no scenarios where the tooltip can fit on screen, or if the 3792 * user does not want the tooltip to fit on screen (viewportAware == false), 3793 * we fall back to the scenarios relative to the document. 3794 * 3795 * When the tooltip is bigger than the viewport in either dimension, we stop 3796 * looking at the window scenarios and consider the document scenarios only, 3797 * with the same logic to find on which side it would fit best. 3798 * 3799 * If the tooltip cannot fit the document on any side, we force it at the 3800 * bottom, so at least the user can scroll to see it. 3801 */ 3802 testResults.sort(function(a, b) { 3803 3804 // best if it's whole (the tooltip fits and adapts to the viewport) 3805 if (a.whole && !b.whole) { 3806 return -1; 3807 } 3808 else if (!a.whole && b.whole) { 3809 return 1; 3810 } 3811 else if (a.whole && b.whole) { 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 // better if it fits 3831 if (a.fits && !b.fits) { 3832 return -1; 3833 } 3834 else if (!a.fits && b.fits) { 3835 return 1; 3836 } 3837 else if (a.fits && b.fits) { 3838 3839 var ai = self.__options.side.indexOf(a.side), 3840 bi = self.__options.side.indexOf(b.side); 3841 3842 // use the user's sides fallback array 3843 if (ai < bi) { 3844 return -1; 3845 } 3846 else if (ai > bi) { 3847 return 1; 3848 } 3849 else { 3850 // will be used if the user forced the tests to continue 3851 return a.mode == 'natural' ? -1 : 1; 3852 } 3853 } 3854 else { 3855 3856 // if everything failed, this will give a preference to the case where 3857 // the tooltip overflows the document at the bottom 3858 if ( a.container == 'document' 3859 && a.side == 'bottom' 3860 && a.mode == 'natural' 3861 ) { 3862 return -1; 3863 } 3864 else { 3865 return 1; 3866 } 3867 } 3868 } 3869 }); 3870 3871 finalResult = testResults[0]; 3872 3873 3874 // now let's find the coordinates of the tooltip relatively to the window 3875 finalResult.coord = {}; 3876 3877 switch (finalResult.side) { 3878 3879 case 'left': 3880 case 'right': 3881 finalResult.coord.top = Math.floor(finalResult.target - finalResult.size.height / 2); 3882 break; 3883 3884 case 'bottom': 3885 case 'top': 3886 finalResult.coord.left = Math.floor(finalResult.target - finalResult.size.width / 2); 3887 break; 3888 } 3889 3890 switch (finalResult.side) { 3891 3892 case 'left': 3893 finalResult.coord.left = helper.geo.origin.windowOffset.left - finalResult.outerSize.width; 3894 break; 3895 3896 case 'right': 3897 finalResult.coord.left = helper.geo.origin.windowOffset.right + finalResult.distance.horizontal; 3898 break; 3899 3900 case 'top': 3901 finalResult.coord.top = helper.geo.origin.windowOffset.top - finalResult.outerSize.height; 3902 break; 3903 3904 case 'bottom': 3905 finalResult.coord.top = helper.geo.origin.windowOffset.bottom + finalResult.distance.vertical; 3906 break; 3907 } 3908 3909 // if the tooltip can potentially be contained within the viewport dimensions 3910 // and that we are asked to make it fit on screen 3911 if (finalResult.container == 'window') { 3912 3913 // if the tooltip overflows the viewport, we'll move it accordingly (then it will 3914 // not be centered on the middle of the origin anymore). We only move horizontally 3915 // for top and bottom tooltips and vice versa. 3916 if (finalResult.side == 'top' || finalResult.side == 'bottom') { 3917 3918 // if there is an overflow on the left 3919 if (finalResult.coord.left < 0) { 3920 3921 // prevent the overflow unless the origin itself gets off screen (minus the 3922 // margin needed to keep the arrow pointing at the target) 3923 if (helper.geo.origin.windowOffset.right - this.__options.minIntersection >= 0) { 3924 finalResult.coord.left = 0; 3925 } 3926 else { 3927 finalResult.coord.left = helper.geo.origin.windowOffset.right - this.__options.minIntersection - 1; 3928 } 3929 } 3930 // or an overflow on the right 3931 else if (finalResult.coord.left > helper.geo.window.size.width - finalResult.size.width) { 3932 3933 if (helper.geo.origin.windowOffset.left + this.__options.minIntersection <= helper.geo.window.size.width) { 3934 finalResult.coord.left = helper.geo.window.size.width - finalResult.size.width; 3935 } 3936 else { 3937 finalResult.coord.left = helper.geo.origin.windowOffset.left + this.__options.minIntersection + 1 - finalResult.size.width; 3938 } 3939 } 3940 } 3941 else { 3942 3943 // overflow at the top 3944 if (finalResult.coord.top < 0) { 3945 3946 if (helper.geo.origin.windowOffset.bottom - this.__options.minIntersection >= 0) { 3947 finalResult.coord.top = 0; 3948 } 3949 else { 3950 finalResult.coord.top = helper.geo.origin.windowOffset.bottom - this.__options.minIntersection - 1; 3951 } 3952 } 3953 // or at the bottom 3954 else if (finalResult.coord.top > helper.geo.window.size.height - finalResult.size.height) { 3955 3956 if (helper.geo.origin.windowOffset.top + this.__options.minIntersection <= helper.geo.window.size.height) { 3957 finalResult.coord.top = helper.geo.window.size.height - finalResult.size.height; 3958 } 3959 else { 3960 finalResult.coord.top = helper.geo.origin.windowOffset.top + this.__options.minIntersection + 1 - finalResult.size.height; 3961 } 3962 } 3963 } 3964 } 3965 else { 3966 3967 // there might be overflow here too but it's easier to handle. If there has 3968 // to be an overflow, we'll make sure it's on the right side of the screen 3969 // (because the browser will extend the document size if there is an overflow 3970 // on the right, but not on the left). The sort function above has already 3971 // made sure that a bottom document overflow is preferred to a top overflow, 3972 // so we don't have to care about it. 3973 3974 // if there is an overflow on the right 3975 if (finalResult.coord.left > helper.geo.window.size.width - finalResult.size.width) { 3976 3977 // this may actually create on overflow on the left but we'll fix it in a sec 3978 finalResult.coord.left = helper.geo.window.size.width - finalResult.size.width; 3979 } 3980 3981 // if there is an overflow on the left 3982 if (finalResult.coord.left < 0) { 3983 3984 // don't care if it overflows the right after that, we made our best 3985 finalResult.coord.left = 0; 3986 } 3987 } 3988 3989 3990 // submit the positioning proposal to the user function which may choose to change 3991 // the side, size and/or the coordinates 3992 3993 // first, set the rules that corresponds to the proposed side: it may change 3994 // the size of the tooltip, and the custom functionPosition may want to detect the 3995 // size of something before making a decision. So let's make things easier for the 3996 // implementor 3997 self.__sideChange($clone, finalResult.side); 3998 3999 // add some variables to the helper 4000 helper.tooltipClone = $clone[0]; 4001 helper.tooltipParent = self.__instance.option('parent').parent[0]; 4002 // move informative values to the helper 4003 helper.mode = finalResult.mode; 4004 helper.whole = finalResult.whole; 4005 // add some variables to the helper for the functionPosition callback (these 4006 // will also be added to the event fired by self.__instance._trigger but that's 4007 // ok, we're just being consistent) 4008 helper.origin = self.__instance._$origin[0]; 4009 helper.tooltip = self.__instance._$tooltip[0]; 4010 4011 // leave only the actionable values in there for functionPosition 4012 delete finalResult.container; 4013 delete finalResult.fits; 4014 delete finalResult.mode; 4015 delete finalResult.outerSize; 4016 delete finalResult.whole; 4017 4018 // keep only the distance on the relevant side, for clarity 4019 finalResult.distance = finalResult.distance.horizontal || finalResult.distance.vertical; 4020 4021 // beginners may not be comfortable with the concept of editing the object 4022 // passed by reference, so we provide an edit function and pass a clone 4023 var finalResultClone = $.extend(true, {}, finalResult); 4024 4025 // emit an event on the instance 4026 self.__instance._trigger({ 4027 edit: function(result) { 4028 finalResult = result; 4029 }, 4030 event: event, 4031 helper: helper, 4032 position: finalResultClone, 4033 type: 'position' 4034 }); 4035 4036 if (self.__options.functionPosition) { 4037 4038
4038 4039 var result = self.__options.functionPosition.call(self, self.__instance, helper, finalResultClone); 4040 4041 if (result) finalResult = result; 4042 } 4043 4044 // end the positioning tests session (the user might have had a 4045 // use for it during the position event, now it's over) 4046 ruler.destroy(); 4047
vendor: 6,410 bytes, lines 4047-4249
4047 4048 4049 // compute the position of the target relatively to the tooltip root 4050 // element so we can place the arrow and make the needed adjustments 4051 var arrowCoord, 4052 maxVal; 4053 4054 if (finalResult.side == 'top' || finalResult.side == 'bottom') { 4055 4056 arrowCoord = { 4057 prop: 'left', 4058 val: finalResult.target - finalResult.coord.left 4059 }; 4060 maxVal = finalResult.size.width - this.__options.minIntersection; 4061 } 4062 else { 4063 4064 arrowCoord = { 4065 prop: 'top', 4066 val: finalResult.target - finalResult.coord.top 4067 }; 4068 maxVal = finalResult.size.height - this.__options.minIntersection; 4069 } 4070 4071 // cannot lie beyond the boundaries of the tooltip, minus the 4072 // arrow margin 4073 if (arrowCoord.val < this.__options.minIntersection) { 4074 arrowCoord.val = this.__options.minIntersection; 4075 } 4076 else if (arrowCoord.val > maxVal) { 4077 arrowCoord.val = maxVal; 4078 } 4079 4080 var originParentOffset; 4081 4082 // let's convert the window-relative coordinates into coordinates relative to the 4083 // future positioned parent that the tooltip will be appended to 4084 if (helper.geo.origin.fixedLineage) { 4085 4086 // same as windowOffset when the position is fixed 4087 originParentOffset = helper.geo.origin.windowOffset; 4088 } 4089 else { 4090 4091 // this assumes that the parent of the tooltip is located at 4092 // (0, 0) in the document, typically like when the parent is 4093 // <body>. 4094 // If we ever allow other types of parent, .tooltipster-ruler 4095 // will have to be appended to the parent to inherit css style 4096 // values that affect the display of the text and such. 4097 originParentOffset = { 4098 left: helper.geo.origin.windowOffset.left + helper.geo.window.scroll.left, 4099 top: helper.geo.origin.windowOffset.top + helper.geo.window.scroll.top 4100 }; 4101 } 4102 4103 finalResult.coord = { 4104 left: originParentOffset.left + (finalResult.coord.left - helper.geo.origin.windowOffset.left), 4105 top: originParentOffset.top + (finalResult.coord.top - helper.geo.origin.windowOffset.top) 4106 }; 4107 4108 // set position values on the original tooltip element 4109 4110 self.__sideChange(self.__instance._$tooltip, finalResult.side); 4111 4112 if (helper.geo.origin.fixedLineage) { 4113 self.__instance._$tooltip 4114 .css('position', 'fixed'); 4115 } 4116 else { 4117 // CSS default 4118 self.__instance._$tooltip 4119 .css('position', ''); 4120 } 4121 4122 self.__instance._$tooltip 4123 .css({ 4124 left: finalResult.coord.left, 4125 top: finalResult.coord.top, 4126 // we need to set a size even if the tooltip is in its natural size 4127 // because when the tooltip is positioned beyond the width of the body 4128 // (which is by default the width of the window; it will happen when 4129 // you scroll the window horizontally to get to the origin), its text 4130 // content will otherwise break lines at each word to keep up with the 4131 // body overflow strategy. 4132 height: finalResult.size.height, 4133 width: finalResult.size.width 4134 }) 4135 .find('.tooltipster-arrow') 4136 .css({ 4137 'left': '', 4138 'top': '' 4139 }) 4140 .css(arrowCoord.prop, arrowCoord.val); 4141 4142 // append the tooltip HTML element to its parent 4143 self.__instance._$tooltip.appendTo(self.__instance.option('parent')); 4144 4145 self.__instance._trigger({ 4146 type: 'repositioned', 4147 event: event, 4148 position: finalResult 4149 }); 4150 }, 4151 4152 /** 4153 * Make whatever modifications are needed when the side is changed. This has 4154 * been made an independant method for easy inheritance in custom plugins based 4155 * on this default plugin. 4156 * 4157 * @param {object} $obj 4158 * @param {string} side 4159 * @private 4160 */ 4161 __sideChange: function($obj, side) { 4162 4163 $obj 4164 .removeClass('tooltipster-bottom') 4165 .removeClass('tooltipster-left') 4166 .removeClass('tooltipster-right') 4167 .removeClass('tooltipster-top') 4168 .addClass('tooltipster-'+ side); 4169 }, 4170 4171 /** 4172 * Returns the target that the tooltip should aim at for a given side. 4173 * The calculated value is a distance from the edge of the window 4174 * (left edge for top/bottom sides, top edge for left/right side). The 4175 * tooltip will be centered on that position and the arrow will be 4176 * positioned there (as much as possible). 4177 * 4178 * @param {object} helper 4179 * @return {integer} 4180 * @private 4181 */ 4182 __targetFind: function(helper) { 4183 4184 var target = {}, 4185 rects = this.__instance._$origin[0].getClientRects(); 4186 4187 // these lines fix a Chrome bug (issue #491) 4188 if (rects.length > 1) { 4189 var opacity = this.__instance._$origin.css('opacity'); 4190 if(opacity == 1) { 4191 this.__instance._$origin.css('opacity', 0.99); 4192 rects = this.__instance._$origin[0].getClientRects(); 4193 this.__instance._$origin.css('opacity', 1); 4194 } 4195 } 4196 4197 // by default, the target will be the middle of the origin 4198 if (rects.length < 2) { 4199 4200 target.top = Math.floor(helper.geo.origin.windowOffset.left + (helper.geo.origin.size.width / 2)); 4201 target.bottom = target.top; 4202 4203 target.left = Math.floor(helper.geo.origin.windowOffset.top + (helper.geo.origin.size.height / 2)); 4204 target.right = target.left; 4205 } 4206 // if multiple client rects exist, the element may be text split 4207 // up into multiple lines and the middle of the origin may not be 4208 // best option anymore. We need to choose the best target client rect 4209 else { 4210 4211 // top: the first 4212 var targetRect = rects[0]; 4213 target.top = Math.floor(targetRect.left + (targetRect.right - targetRect.left) / 2); 4214 4215 // right: the middle line, rounded down in case there is an even 4216 // number of lines (looks more centered => check out the 4217 // demo with 4 split lines) 4218 if (rects.length > 2) { 4219 targetRect = rects[Math.ceil(rects.length / 2) - 1]; 4220 } 4221 else { 4222 targetRect = rects[0]; 4223 } 4224 target.right = Math.floor(targetRect.top + (targetRect.bottom - targetRect.top) / 2); 4225 4226 // bottom: the last 4227 targetRect = rects[rects.length - 1]; 4228 target.bottom = Math.floor(targetRect.left + (targetRect.right - targetRect.left) / 2); 4229 4230 // left: the middle line, rounded up 4231 if (rects.length > 2) { 4232 targetRect = rects[Math.ceil((rects.length + 1) / 2) - 1]; 4233 } 4234 else { 4235 targetRect = rects[rects.length - 1]; 4236 } 4237 4238 target.left = Math.floor(targetRect.top + (targetRect.bottom - targetRect.top) / 2); 4239 } 4240 4241 return target; 4242 } 4243 } 4244}); 4245 4246/* a build task will add "return $;" here */ 4247return $; 4248 4249}));
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.