unforassets 0.1.0

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