tooltipster-rails 3.2.6 → 4.1.2

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