tooltips.js
4282 lines
| 114.4 KiB
| application/javascript
|
JavascriptLexer
r4026 | /** | ||
* tooltipster http://iamceege.github.io/tooltipster/ | |||
* A rockin' custom tooltip jQuery plugin | |||
* Developed by Caleb Jacob and Louis Ameline | |||
* MIT license | |||
*/ | |||
(function (root, factory) { | |||
if (typeof define === 'function' && define.amd) { | |||
// AMD. Register as an anonymous module unless amdModuleId is set | |||
define(["jquery"], function (a0) { | |||
return (factory(a0)); | |||
}); | |||
} else if (typeof exports === 'object') { | |||
// Node. Does not work with strict CommonJS, but | |||
// only CommonJS-like environments that support module.exports, | |||
// like Node. | |||
module.exports = factory(require("jquery")); | |||
} else { | |||
factory(jQuery); | |||
} | |||
}(this, function ($) { | |||
// This file will be UMDified by a build task. | |||
var defaults = { | |||
animation: 'fade', | |||
animationDuration: 350, | |||
content: null, | |||
contentAsHTML: false, | |||
contentCloning: false, | |||
debug: true, | |||
delay: 300, | |||
delayTouch: [300, 500], | |||
functionInit: null, | |||
functionBefore: null, | |||
functionReady: null, | |||
functionAfter: null, | |||
functionFormat: null, | |||
IEmin: 6, | |||
interactive: false, | |||
multiple: false, | |||
// will default to document.body, or must be an element positioned at (0, 0) | |||
// in the document, typically like the very top views of an app. | |||
parent: null, | |||
plugins: ['sideTip'], | |||
repositionOnScroll: false, | |||
restoration: 'none', | |||
selfDestruction: true, | |||
theme: [], | |||
timer: 0, | |||
trackerInterval: 500, | |||
trackOrigin: false, | |||
trackTooltip: false, | |||
trigger: 'hover', | |||
triggerClose: { | |||
click: false, | |||
mouseleave: false, | |||
originClick: false, | |||
scroll: false, | |||
tap: false, | |||
touchleave: false | |||
}, | |||
triggerOpen: { | |||
click: false, | |||
mouseenter: false, | |||
tap: false, | |||
touchstart: false | |||
}, | |||
updateAnimation: 'rotate', | |||
zIndex: 9999999 | |||
}, | |||
// we'll avoid using the 'window' global as a good practice but npm's | |||
// jquery@<2.1.0 package actually requires a 'window' global, so not sure | |||
// it's useful at all | |||
win = (typeof window != 'undefined') ? window : null, | |||
// env will be proxied by the core for plugins to have access its properties | |||
env = { | |||
// detect if this device can trigger touch events. Better have a false | |||
// positive (unused listeners, that's ok) than a false negative. | |||
// https://github.com/Modernizr/Modernizr/blob/master/feature-detects/touchevents.js | |||
// http://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript | |||
hasTouchCapability: !!( | |||
win | |||
&& ( 'ontouchstart' in win | |||
|| (win.DocumentTouch && win.document instanceof win.DocumentTouch) | |||
|| win.navigator.maxTouchPoints | |||
) | |||
), | |||
hasTransitions: transitionSupport(), | |||
IE: false, | |||
// don't set manually, it will be updated by a build task after the manifest | |||
semVer: '4.2.7', | |||
window: win | |||
}, | |||
core = function() { | |||
// core variables | |||
// the core emitters | |||
this.__$emitterPrivate = $({}); | |||
this.__$emitterPublic = $({}); | |||
this.__instancesLatestArr = []; | |||
// collects plugin constructors | |||
this.__plugins = {}; | |||
// proxy env variables for plugins who might use them | |||
this._env = env; | |||
}; | |||
// core methods | |||
core.prototype = { | |||
/** | |||
* A function to proxy the public methods of an object onto another | |||
* | |||
* @param {object} constructor The constructor to bridge | |||
* @param {object} obj The object that will get new methods (an instance or the core) | |||
* @param {string} pluginName A plugin name for the console log message | |||
* @return {core} | |||
* @private | |||
*/ | |||
__bridge: function(constructor, obj, pluginName) { | |||
// if it's not already bridged | |||
if (!obj[pluginName]) { | |||
var fn = function() {}; | |||
fn.prototype = constructor; | |||
var pluginInstance = new fn(); | |||
// the _init method has to exist in instance constructors but might be missing | |||
// in core constructors | |||
if (pluginInstance.__init) { | |||
pluginInstance.__init(obj); | |||
} | |||
$.each(constructor, function(methodName, fn) { | |||
// don't proxy "private" methods, only "protected" and public ones | |||
if (methodName.indexOf('__') != 0) { | |||
// if the method does not exist yet | |||
if (!obj[methodName]) { | |||
obj[methodName] = function() { | |||
return pluginInstance[methodName].apply(pluginInstance, Array.prototype.slice.apply(arguments)); | |||
}; | |||
// remember to which plugin this method corresponds (several plugins may | |||
// have methods of the same name, we need to be sure) | |||
obj[methodName].bridged = pluginInstance; | |||
} | |||
else if (defaults.debug) { | |||
console.log('The '+ methodName +' method of the '+ pluginName | |||
+' plugin conflicts with another plugin or native methods'); | |||
} | |||
} | |||
}); | |||
obj[pluginName] = pluginInstance; | |||
} | |||
return this; | |||
}, | |||
/** | |||
* For mockup in Node env if need be, for testing purposes | |||
* | |||
* @return {core} | |||
* @private | |||
*/ | |||
__setWindow: function(window) { | |||
env.window = window; | |||
return this; | |||
}, | |||
/** | |||
* Returns a ruler, a tool to help measure the size of a tooltip under | |||
* various settings. Meant for plugins | |||
* | |||
* @see Ruler | |||
* @return {object} A Ruler instance | |||
* @protected | |||
*/ | |||
_getRuler: function($tooltip) { | |||
return new Ruler($tooltip); | |||
}, | |||
/** | |||
* For internal use by plugins, if needed | |||
* | |||
* @return {core} | |||
* @protected | |||
*/ | |||
_off: function() { | |||
this.__$emitterPrivate.off.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments)); | |||
return this; | |||
}, | |||
/** | |||
* For internal use by plugins, if needed | |||
* | |||
* @return {core} | |||
* @protected | |||
*/ | |||
_on: function() { | |||
this.__$emitterPrivate.on.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments)); | |||
return this; | |||
}, | |||
/** | |||
* For internal use by plugins, if needed | |||
* | |||
* @return {core} | |||
* @protected | |||
*/ | |||
_one: function() { | |||
this.__$emitterPrivate.one.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments)); | |||
return this; | |||
}, | |||
/** | |||
* Returns (getter) or adds (setter) a plugin | |||
* | |||
* @param {string|object} plugin Provide a string (in the full form | |||
* "namespace.name") to use as as getter, an object to use as a setter | |||
* @return {object|core} | |||
* @protected | |||
*/ | |||
_plugin: function(plugin) { | |||
var self = this; | |||
// getter | |||
if (typeof plugin == 'string') { | |||
var pluginName = plugin, | |||
p = null; | |||
// if the namespace is provided, it's easy to search | |||
if (pluginName.indexOf('.') > 0) { | |||
p = self.__plugins[pluginName]; | |||
} | |||
// otherwise, return the first name that matches | |||
else { | |||
$.each(self.__plugins, function(i, plugin) { | |||
if (plugin.name.substring(plugin.name.length - pluginName.length - 1) == '.'+ pluginName) { | |||
p = plugin; | |||
return false; | |||
} | |||
}); | |||
} | |||
return p; | |||
} | |||
// setter | |||
else { | |||
// force namespaces | |||
if (plugin.name.indexOf('.') < 0) { | |||
throw new Error('Plugins must be namespaced'); | |||
} | |||
self.__plugins[plugin.name] = plugin; | |||
// if the plugin has core features | |||
if (plugin.core) { | |||
// bridge non-private methods onto the core to allow new core methods | |||
self.__bridge(plugin.core, self, plugin.name); | |||
} | |||
return this; | |||
} | |||
}, | |||
/** | |||
* Trigger events on the core emitters | |||
* | |||
* @returns {core} | |||
* @protected | |||
*/ | |||
_trigger: function() { | |||
var args = Array.prototype.slice.apply(arguments); | |||
if (typeof args[0] == 'string') { | |||
args[0] = { type: args[0] }; | |||
} | |||
// note: the order of emitters matters | |||
this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate, args); | |||
this.__$emitterPublic.trigger.apply(this.__$emitterPublic, args); | |||
return this; | |||
}, | |||
/** | |||
* Returns instances of all tooltips in the page or an a given element | |||
* | |||
* @param {string|HTML object collection} selector optional Use this | |||
* parameter to restrict the set of objects that will be inspected | |||
* for the retrieval of instances. By default, all instances in the | |||
* page are returned. | |||
* @return {array} An array of instance objects | |||
* @public | |||
*/ | |||
instances: function(selector) { | |||
var instances = [], | |||
sel = selector || '.tooltipstered'; | |||
$(sel).each(function() { | |||
var $this = $(this), | |||
ns = $this.data('tooltipster-ns'); | |||
if (ns) { | |||
$.each(ns, function(i, namespace) { | |||
instances.push($this.data(namespace)); | |||
}); | |||
} | |||
}); | |||
return instances; | |||
}, | |||
/** | |||
* Returns the Tooltipster objects generated by the last initializing call | |||
* | |||
* @return {array} An array of instance objects | |||
* @public | |||
*/ | |||
instancesLatest: function() { | |||
return this.__instancesLatestArr; | |||
}, | |||
/** | |||
* For public use only, not to be used by plugins (use ::_off() instead) | |||
* | |||
* @return {core} | |||
* @public | |||
*/ | |||
off: function() { | |||
this.__$emitterPublic.off.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); | |||
return this; | |||
}, | |||
/** | |||
* For public use only, not to be used by plugins (use ::_on() instead) | |||
* | |||
* @return {core} | |||
* @public | |||
*/ | |||
on: function() { | |||
this.__$emitterPublic.on.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); | |||
return this; | |||
}, | |||
/** | |||
* For public use only, not to be used by plugins (use ::_one() instead) | |||
* | |||
* @return {core} | |||
* @public | |||
*/ | |||
one: function() { | |||
this.__$emitterPublic.one.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); | |||
return this; | |||
}, | |||
/** | |||
* Returns all HTML elements which have one or more tooltips | |||
* | |||
* @param {string} selector optional Use this to restrict the results | |||
* to the descendants of an element | |||
* @return {array} An array of HTML elements | |||
* @public | |||
*/ | |||
origins: function(selector) { | |||
var sel = selector ? | |||
selector +' ' : | |||
''; | |||
return $(sel +'.tooltipstered').toArray(); | |||
}, | |||
/** | |||
* Change default options for all future instances | |||
* | |||
* @param {object} d The options that should be made defaults | |||
* @return {core} | |||
* @public | |||
*/ | |||
setDefaults: function(d) { | |||
$.extend(defaults, d); | |||
return this; | |||
}, | |||
/** | |||
* For users to trigger their handlers on the public emitter | |||
* | |||
* @returns {core} | |||
* @public | |||
*/ | |||
triggerHandler: function() { | |||
this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); | |||
return this; | |||
} | |||
}; | |||
// $.tooltipster will be used to call core methods | |||
$.tooltipster = new core(); | |||
// the Tooltipster instance class (mind the capital T) | |||
$.Tooltipster = function(element, options) { | |||
// list of instance variables | |||
// stack of custom callbacks provided as parameters to API methods | |||
this.__callbacks = { | |||
close: [], | |||
open: [] | |||
}; | |||
// the schedule time of DOM removal | |||
this.__closingTime; | |||
// this will be the user content shown in the tooltip. A capital "C" is used | |||
// because there is also a method called content() | |||
this.__Content; | |||
// for the size tracker | |||
this.__contentBcr; | |||
// to disable the tooltip after destruction | |||
this.__destroyed = false; | |||
// we can't emit directly on the instance because if a method with the same | |||
// name as the event exists, it will be called by jQuery. Se we use a plain | |||
// object as emitter. This emitter is for internal use by plugins, | |||
// if needed. | |||
this.__$emitterPrivate = $({}); | |||
// this emitter is for the user to listen to events without risking to mess | |||
// with our internal listeners | |||
this.__$emitterPublic = $({}); | |||
this.__enabled = true; | |||
// the reference to the gc interval | |||
this.__garbageCollector; | |||
// various position and size data recomputed before each repositioning | |||
this.__Geometry; | |||
// the tooltip position, saved after each repositioning by a plugin | |||
this.__lastPosition; | |||
// a unique namespace per instance | |||
this.__namespace = 'tooltipster-'+ Math.round(Math.random()*1000000); | |||
this.__options; | |||
// will be used to support origins in scrollable areas | |||
this.__$originParents; | |||
this.__pointerIsOverOrigin = false; | |||
// to remove themes if needed | |||
this.__previousThemes = []; | |||
// the state can be either: appearing, stable, disappearing, closed | |||
this.__state = 'closed'; | |||
// timeout references | |||
this.__timeouts = { | |||
close: [], | |||
open: null | |||
}; | |||
// store touch events to be able to detect emulated mouse events | |||
this.__touchEvents = []; | |||
// the reference to the tracker interval | |||
this.__tracker = null; | |||
// the element to which this tooltip is associated | |||
this._$origin; | |||
// this will be the tooltip element (jQuery wrapped HTML element). | |||
// It's the job of a plugin to create it and append it to the DOM | |||
this._$tooltip; | |||
// launch | |||
this.__init(element, options); | |||
}; | |||
$.Tooltipster.prototype = { | |||
/** | |||
* @param origin | |||
* @param options | |||
* @private | |||
*/ | |||
__init: function(origin, options) { | |||
var self = this; | |||
self._$origin = $(origin); | |||
self.__options = $.extend(true, {}, defaults, options); | |||
// some options may need to be reformatted | |||
self.__optionsFormat(); | |||
// don't run on old IE if asked no to | |||
if ( !env.IE | |||
|| env.IE >= self.__options.IEmin | |||
) { | |||
// note: the content is null (empty) by default and can stay that | |||
// way if the plugin remains initialized but not fed any content. The | |||
// tooltip will just not appear. | |||
// let's save the initial value of the title attribute for later | |||
// restoration if need be. | |||
var initialTitle = null; | |||
// it will already have been saved in case of multiple tooltips | |||
if (self._$origin.data('tooltipster-initialTitle') === undefined) { | |||
initialTitle = self._$origin.attr('title'); | |||
// we do not want initialTitle to be "undefined" because | |||
// of how jQuery's .data() method works | |||
if (initialTitle === undefined) initialTitle = null; | |||
self._$origin.data('tooltipster-initialTitle', initialTitle); | |||
} | |||
// If content is provided in the options, it has precedence over the | |||
// title attribute. | |||
// Note: an empty string is considered content, only 'null' represents | |||
// the absence of content. | |||
// Also, an existing title="" attribute will result in an empty string | |||
// content | |||
if (self.__options.content !== null) { | |||
self.__contentSet(self.__options.content); | |||
} | |||
else { | |||
var selector = self._$origin.attr('data-tooltip-content'), | |||
$el; | |||
if (selector){ | |||
$el = $(selector); | |||
} | |||
if ($el && $el[0]) { | |||
self.__contentSet($el.first()); | |||
} | |||
else { | |||
self.__contentSet(initialTitle); | |||
} | |||
} | |||
self._$origin | |||
// strip the title off of the element to prevent the default tooltips | |||
// from popping up | |||
.removeAttr('title') | |||
// to be able to find all instances on the page later (upon window | |||
// events in particular) | |||
.addClass('tooltipstered'); | |||
// set listeners on the origin | |||
self.__prepareOrigin(); | |||
// set the garbage collector | |||
self.__prepareGC(); | |||
// init plugins | |||
$.each(self.__options.plugins, function(i, pluginName) { | |||
self._plug(pluginName); | |||
}); | |||
// to detect swiping | |||
if (env.hasTouchCapability) { | |||
$(env.window.document.body).on('touchmove.'+ self.__namespace +'-triggerOpen', function(event) { | |||
self._touchRecordEvent(event); | |||
}); | |||
} | |||
self | |||
// prepare the tooltip when it gets created. This event must | |||
// be fired by a plugin | |||
._on('created', function() { | |||
self.__prepareTooltip(); | |||
}) | |||
// save position information when it's sent by a plugin | |||
._on('repositioned', function(e) { | |||
self.__lastPosition = e.position; | |||
}); | |||
} | |||
else { | |||
self.__options.disabled = true; | |||
} | |||
}, | |||
/** | |||
* Insert the content into the appropriate HTML element of the tooltip | |||
* | |||
* @returns {self} | |||
* @private | |||
*/ | |||
__contentInsert: function() { | |||
var self = this, | |||
$el = self._$tooltip.find('.tooltipster-content'), | |||
formattedContent = self.__Content, | |||
format = function(content) { | |||
formattedContent = content; | |||
}; | |||
self._trigger({ | |||
type: 'format', | |||
content: self.__Content, | |||
format: format | |||
}); | |||
if (self.__options.functionFormat) { | |||
formattedContent = self.__options.functionFormat.call( | |||
self, | |||
self, | |||
{ origin: self._$origin[0] }, | |||
self.__Content | |||
); | |||
} | |||
if (typeof formattedContent === 'string' && !self.__options.contentAsHTML) { | |||
$el.text(formattedContent); | |||
} | |||
else { | |||
$el | |||
.empty() | |||
.append(formattedContent); | |||
} | |||
return self; | |||
}, | |||
/** | |||
* Save the content, cloning it beforehand if need be | |||
* | |||
* @param content | |||
* @returns {self} | |||
* @private | |||
*/ | |||
__contentSet: function(content) { | |||
// clone if asked. Cloning the object makes sure that each instance has its | |||
// own version of the content (in case a same object were provided for several | |||
// instances) | |||
// reminder: typeof null === object | |||
if (content instanceof $ && this.__options.contentCloning) { | |||
content = content.clone(true); | |||
} | |||
this.__Content = content; | |||
this._trigger({ | |||
type: 'updated', | |||
content: content | |||
}); | |||
return this; | |||
}, | |||
/** | |||
* Error message about a method call made after destruction | |||
* | |||
* @private | |||
*/ | |||
__destroyError: function() { | |||
throw new Error('This tooltip has been destroyed and cannot execute your method call.'); | |||
}, | |||
/** | |||
* Gather all information about dimensions and available space, | |||
* called before every repositioning | |||
* | |||
* @private | |||
* @returns {object} | |||
*/ | |||
__geometry: function() { | |||
var self = this, | |||
$target = self._$origin, | |||
originIsArea = self._$origin.is('area'); | |||
// if this._$origin is a map area, the target we'll need | |||
// the dimensions of is actually the image using the map, | |||
// not the area itself | |||
if (originIsArea) { | |||
var mapName = self._$origin.parent().attr('name'); | |||
$target = $('img[usemap="#'+ mapName +'"]'); | |||
} | |||
var bcr = $target[0].getBoundingClientRect(), | |||
$document = $(env.window.document), | |||
$window = $(env.window), | |||
$parent = $target, | |||
// some useful properties of important elements | |||
geo = { | |||
// available space for the tooltip, see down below | |||
available: { | |||
document: null, | |||
window: null | |||
}, | |||
document: { | |||
size: { | |||
height: $document.height(), | |||
width: $document.width() | |||
} | |||
}, | |||
window: { | |||
scroll: { | |||
// the second ones are for IE compatibility | |||
left: env.window.scrollX || env.window.document.documentElement.scrollLeft, | |||
top: env.window.scrollY || env.window.document.documentElement.scrollTop | |||
}, | |||
size: { | |||
height: $window.height(), | |||
width: $window.width() | |||
} | |||
}, | |||
origin: { | |||
// the origin has a fixed lineage if itself or one of its | |||
// ancestors has a fixed position | |||
fixedLineage: false, | |||
// relative to the document | |||
offset: {}, | |||
size: { | |||
height: bcr.bottom - bcr.top, | |||
width: bcr.right - bcr.left | |||
}, | |||
usemapImage: originIsArea ? $target[0] : null, | |||
// relative to the window | |||
windowOffset: { | |||
bottom: bcr.bottom, | |||
left: bcr.left, | |||
right: bcr.right, | |||
top: bcr.top | |||
} | |||
} | |||
}, | |||
geoFixed = false; | |||
// if the element is a map area, some properties may need | |||
// to be recalculated | |||
if (originIsArea) { | |||
var shape = self._$origin.attr('shape'), | |||
coords = self._$origin.attr('coords'); | |||
if (coords) { | |||
coords = coords.split(','); | |||
$.map(coords, function(val, i) { | |||
coords[i] = parseInt(val); | |||
}); | |||
} | |||
// if the image itself is the area, nothing more to do | |||
if (shape != 'default') { | |||
switch(shape) { | |||
case 'circle': | |||
var circleCenterLeft = coords[0], | |||
circleCenterTop = coords[1], | |||
circleRadius = coords[2], | |||
areaTopOffset = circleCenterTop - circleRadius, | |||
areaLeftOffset = circleCenterLeft - circleRadius; | |||
geo.origin.size.height = circleRadius * 2; | |||
geo.origin.size.width = geo.origin.size.height; | |||
geo.origin.windowOffset.left += areaLeftOffset; | |||
geo.origin.windowOffset.top += areaTopOffset; | |||
break; | |||
case 'rect': | |||
var areaLeft = coords[0], | |||
areaTop = coords[1], | |||
areaRight = coords[2], | |||
areaBottom = coords[3]; | |||
geo.origin.size.height = areaBottom - areaTop; | |||
geo.origin.size.width = areaRight - areaLeft; | |||
geo.origin.windowOffset.left += areaLeft; | |||
geo.origin.windowOffset.top += areaTop; | |||
break; | |||
case 'poly': | |||
var areaSmallestX = 0, | |||
areaSmallestY = 0, | |||
areaGreatestX = 0, | |||
areaGreatestY = 0, | |||
arrayAlternate = 'even'; | |||
for (var i = 0; i < coords.length; i++) { | |||
var areaNumber = coords[i]; | |||
if (arrayAlternate == 'even') { | |||
if (areaNumber > areaGreatestX) { | |||
areaGreatestX = areaNumber; | |||
if (i === 0) { | |||
areaSmallestX = areaGreatestX; | |||
} | |||
} | |||
if (areaNumber < areaSmallestX) { | |||
areaSmallestX = areaNumber; | |||
} | |||
arrayAlternate = 'odd'; | |||
} | |||
else { | |||
if (areaNumber > areaGreatestY) { | |||
areaGreatestY = areaNumber; | |||
if (i == 1) { | |||
areaSmallestY = areaGreatestY; | |||
} | |||
} | |||
if (areaNumber < areaSmallestY) { | |||
areaSmallestY = areaNumber; | |||
} | |||
arrayAlternate = 'even'; | |||
} | |||
} | |||
geo.origin.size.height = areaGreatestY - areaSmallestY; | |||
geo.origin.size.width = areaGreatestX - areaSmallestX; | |||
geo.origin.windowOffset.left += areaSmallestX; | |||
geo.origin.windowOffset.top += areaSmallestY; | |||
break; | |||
} | |||
} | |||
} | |||
// user callback through an event | |||
var edit = function(r) { | |||
geo.origin.size.height = r.height, | |||
geo.origin.windowOffset.left = r.left, | |||
geo.origin.windowOffset.top = r.top, | |||
geo.origin.size.width = r.width | |||
}; | |||
self._trigger({ | |||
type: 'geometry', | |||
edit: edit, | |||
geometry: { | |||
height: geo.origin.size.height, | |||
left: geo.origin.windowOffset.left, | |||
top: geo.origin.windowOffset.top, | |||
width: geo.origin.size.width | |||
} | |||
}); | |||
// calculate the remaining properties with what we got | |||
geo.origin.windowOffset.right = geo.origin.windowOffset.left + geo.origin.size.width; | |||
geo.origin.windowOffset.bottom = geo.origin.windowOffset.top + geo.origin.size.height; | |||
geo.origin.offset.left = geo.origin.windowOffset.left + geo.window.scroll.left; | |||
geo.origin.offset.top = geo.origin.windowOffset.top + geo.window.scroll.top; | |||
geo.origin.offset.bottom = geo.origin.offset.top + geo.origin.size.height; | |||
geo.origin.offset.right = geo.origin.offset.left + geo.origin.size.width; | |||
// the space that is available to display the tooltip relatively to the document | |||
geo.available.document = { | |||
bottom: { | |||
height: geo.document.size.height - geo.origin.offset.bottom, | |||
width: geo.document.size.width | |||
}, | |||
left: { | |||
height: geo.document.size.height, | |||
width: geo.origin.offset.left | |||
}, | |||
right: { | |||
height: geo.document.size.height, | |||
width: geo.document.size.width - geo.origin.offset.right | |||
}, | |||
top: { | |||
height: geo.origin.offset.top, | |||
width: geo.document.size.width | |||
} | |||
}; | |||
// the space that is available to display the tooltip relatively to the viewport | |||
// (the resulting values may be negative if the origin overflows the viewport) | |||
geo.available.window = { | |||
bottom: { | |||
// the inner max is here to make sure the available height is no bigger | |||
// than the viewport height (when the origin is off screen at the top). | |||
// The outer max just makes sure that the height is not negative (when | |||
// the origin overflows at the bottom). | |||
height: Math.max(geo.window.size.height - Math.max(geo.origin.windowOffset.bottom, 0), 0), | |||
width: geo.window.size.width | |||
}, | |||
left: { | |||
height: geo.window.size.height, | |||
width: Math.max(geo.origin.windowOffset.left, 0) | |||
}, | |||
right: { | |||
height: geo.window.size.height, | |||
width: Math.max(geo.window.size.width - Math.max(geo.origin.windowOffset.right, 0), 0) | |||
}, | |||
top: { | |||
height: Math.max(geo.origin.windowOffset.top, 0), | |||
width: geo.window.size.width | |||
} | |||
}; | |||
while ($parent[0].tagName.toLowerCase() != 'html') { | |||
if ($parent.css('position') == 'fixed') { | |||
geo.origin.fixedLineage = true; | |||
break; | |||
} | |||
$parent = $parent.parent(); | |||
} | |||
return geo; | |||
}, | |||
/** | |||
* Some options may need to be formated before being used | |||
* | |||
* @returns {self} | |||
* @private | |||
*/ | |||
__optionsFormat: function() { | |||
if (typeof this.__options.animationDuration == 'number') { | |||
this.__options.animationDuration = [this.__options.animationDuration, this.__options.animationDuration]; | |||
} | |||
if (typeof this.__options.delay == 'number') { | |||
this.__options.delay = [this.__options.delay, this.__options.delay]; | |||
} | |||
if (typeof this.__options.delayTouch == 'number') { | |||
this.__options.delayTouch = [this.__options.delayTouch, this.__options.delayTouch]; | |||
} | |||
if (typeof this.__options.theme == 'string') { | |||
this.__options.theme = [this.__options.theme]; | |||
} | |||
// determine the future parent | |||
if (this.__options.parent === null) { | |||
this.__options.parent = $(env.window.document.body); | |||
} | |||
else if (typeof this.__options.parent == 'string') { | |||
this.__options.parent = $(this.__options.parent); | |||
} | |||
if (this.__options.trigger == 'hover') { | |||
this.__options.triggerOpen = { | |||
mouseenter: true, | |||
touchstart: true | |||
}; | |||
this.__options.triggerClose = { | |||
mouseleave: true, | |||
originClick: true, | |||
touchleave: true | |||
}; | |||
} | |||
else if (this.__options.trigger == 'click') { | |||
this.__options.triggerOpen = { | |||
click: true, | |||
tap: true | |||
}; | |||
this.__options.triggerClose = { | |||
click: true, | |||
tap: true | |||
}; | |||
} | |||
// for the plugins | |||
this._trigger('options'); | |||
return this; | |||
}, | |||
/** | |||
* Schedules or cancels the garbage collector task | |||
* | |||
* @returns {self} | |||
* @private | |||
*/ | |||
__prepareGC: function() { | |||
var self = this; | |||
// in case the selfDestruction option has been changed by a method call | |||
if (self.__options.selfDestruction) { | |||
// the GC task | |||
self.__garbageCollector = setInterval(function() { | |||
var now = new Date().getTime(); | |||
// forget the old events | |||
self.__touchEvents = $.grep(self.__touchEvents, function(event, i) { | |||
// 1 minute | |||
return now - event.time > 60000; | |||
}); | |||
// auto-destruct if the origin is gone | |||
if (!bodyContains(self._$origin)) { | |||
self.close(function(){ | |||
self.destroy(); | |||
}); | |||
} | |||
}, 20000); | |||
} | |||
else { | |||
clearInterval(self.__garbageCollector); | |||
} | |||
return self; | |||
}, | |||
/** | |||
* Sets listeners on the origin if the open triggers require them. | |||
* Unlike the listeners set at opening time, these ones | |||
* remain even when the tooltip is closed. It has been made a | |||
* separate method so it can be called when the triggers are | |||
* changed in the options. Closing is handled in _open() | |||
* because of the bindings that may be needed on the tooltip | |||
* itself | |||
* | |||
* @returns {self} | |||
* @private | |||
*/ | |||
__prepareOrigin: function() { | |||
var self = this; | |||
// in case we're resetting the triggers | |||
self._$origin.off('.'+ self.__namespace +'-triggerOpen'); | |||
// if the device is touch capable, even if only mouse triggers | |||
// are asked, we need to listen to touch events to know if the mouse | |||
// events are actually emulated (so we can ignore them) | |||
if (env.hasTouchCapability) { | |||
self._$origin.on( | |||
'touchstart.'+ self.__namespace +'-triggerOpen ' + | |||
'touchend.'+ self.__namespace +'-triggerOpen ' + | |||
'touchcancel.'+ self.__namespace +'-triggerOpen', | |||
function(event){ | |||
self._touchRecordEvent(event); | |||
} | |||
); | |||
} | |||
// mouse click and touch tap work the same way | |||
if ( self.__options.triggerOpen.click | |||
|| (self.__options.triggerOpen.tap && env.hasTouchCapability) | |||
) { | |||
var eventNames = ''; | |||
if (self.__options.triggerOpen.click) { | |||
eventNames += 'click.'+ self.__namespace +'-triggerOpen '; | |||
} | |||
if (self.__options.triggerOpen.tap && env.hasTouchCapability) { | |||
eventNames += 'touchend.'+ self.__namespace +'-triggerOpen'; | |||
} | |||
self._$origin.on(eventNames, function(event) { | |||
if (self._touchIsMeaningfulEvent(event)) { | |||
self._open(event); | |||
} | |||
}); | |||
} | |||
// mouseenter and touch start work the same way | |||
if ( self.__options.triggerOpen.mouseenter | |||
|| (self.__options.triggerOpen.touchstart && env.hasTouchCapability) | |||
) { | |||
var eventNames = ''; | |||
if (self.__options.triggerOpen.mouseenter) { | |||
eventNames += 'mouseenter.'+ self.__namespace +'-triggerOpen '; | |||
} | |||
if (self.__options.triggerOpen.touchstart && env.hasTouchCapability) { | |||
eventNames += 'touchstart.'+ self.__namespace +'-triggerOpen'; | |||
} | |||
self._$origin.on(eventNames, function(event) { | |||
if ( self._touchIsTouchEvent(event) | |||
|| !self._touchIsEmulatedEvent(event) | |||
) { | |||
self.__pointerIsOverOrigin = true; | |||
self._openShortly(event); | |||
} | |||
}); | |||
} | |||
// info for the mouseleave/touchleave close triggers when they use a delay | |||
if ( self.__options.triggerClose.mouseleave | |||
|| (self.__options.triggerClose.touchleave && env.hasTouchCapability) | |||
) { | |||
var eventNames = ''; | |||
if (self.__options.triggerClose.mouseleave) { | |||
eventNames += 'mouseleave.'+ self.__namespace +'-triggerOpen '; | |||
} | |||
if (self.__options.triggerClose.touchleave && env.hasTouchCapability) { | |||
eventNames += 'touchend.'+ self.__namespace +'-triggerOpen touchcancel.'+ self.__namespace +'-triggerOpen'; | |||
} | |||
self._$origin.on(eventNames, function(event) { | |||
if (self._touchIsMeaningfulEvent(event)) { | |||
self.__pointerIsOverOrigin = false; | |||
} | |||
}); | |||
} | |||
return self; | |||
}, | |||
/** | |||
* Do the things that need to be done only once after the tooltip | |||
* HTML element it has been created. It has been made a separate | |||
* method so it can be called when options are changed. Remember | |||
* that the tooltip may actually exist in the DOM before it is | |||
* opened, and present after it has been closed: it's the display | |||
* plugin that takes care of handling it. | |||
* | |||
* @returns {self} | |||
* @private | |||
*/ | |||
__prepareTooltip: function() { | |||
var self = this, | |||
p = self.__options.interactive ? 'auto' : ''; | |||
// this will be useful to know quickly if the tooltip is in | |||
// the DOM or not | |||
self._$tooltip | |||
.attr('id', self.__namespace) | |||
.css({ | |||
// pointer events | |||
'pointer-events': p, | |||
zIndex: self.__options.zIndex | |||
}); | |||
// themes | |||
// remove the old ones and add the new ones | |||
$.each(self.__previousThemes, function(i, theme) { | |||
self._$tooltip.removeClass(theme); | |||
}); | |||
$.each(self.__options.theme, function(i, theme) { | |||
self._$tooltip.addClass(theme); | |||
}); | |||
self.__previousThemes = $.merge([], self.__options.theme); | |||
return self; | |||
}, | |||
/** | |||
* Handles the scroll on any of the parents of the origin (when the | |||
* tooltip is open) | |||
* | |||
* @param {object} event | |||
* @returns {self} | |||
* @private | |||
*/ | |||
__scrollHandler: function(event) { | |||
var self = this; | |||
if (self.__options.triggerClose.scroll) { | |||
self._close(event); | |||
} | |||
else { | |||
// if the origin or tooltip have been removed: do nothing, the tracker will | |||
// take care of it later | |||
if (bodyContains(self._$origin) && bodyContains(self._$tooltip)) { | |||
var geo = null; | |||
// if the scroll happened on the window | |||
if (event.target === env.window.document) { | |||
// if the origin has a fixed lineage, window scroll will have no | |||
// effect on its position nor on the position of the tooltip | |||
if (!self.__Geometry.origin.fixedLineage) { | |||
// we don't need to do anything unless repositionOnScroll is true | |||
// because the tooltip will already have moved with the window | |||
// (and of course with the origin) | |||
if (self.__options.repositionOnScroll) { | |||
self.reposition(event); | |||
} | |||
} | |||
} | |||
// if the scroll happened on another parent of the tooltip, it means | |||
// that it's in a scrollable area and now needs to have its position | |||
// adjusted or recomputed, depending ont the repositionOnScroll | |||
// option. Also, if the origin is partly hidden due to a parent that | |||
// hides its overflow, we'll just hide (not close) the tooltip. | |||
else { | |||
geo = self.__geometry(); | |||
var overflows = false; | |||
// a fixed position origin is not affected by the overflow hiding | |||
// of a parent | |||
if (self._$origin.css('position') != 'fixed') { | |||
self.__$originParents.each(function(i, el) { | |||
var $el = $(el), | |||
overflowX = $el.css('overflow-x'), | |||
overflowY = $el.css('overflow-y'); | |||
if (overflowX != 'visible' || overflowY != 'visible') { | |||
var bcr = el.getBoundingClientRect(); | |||
if (overflowX != 'visible') { | |||
if ( geo.origin.windowOffset.left < bcr.left | |||
|| geo.origin.windowOffset.right > bcr.right | |||
) { | |||
overflows = true; | |||
return false; | |||
} | |||
} | |||
if (overflowY != 'visible') { | |||
if ( geo.origin.windowOffset.top < bcr.top | |||
|| geo.origin.windowOffset.bottom > bcr.bottom | |||
) { | |||
overflows = true; | |||
return false; | |||
} | |||
} | |||
} | |||
// no need to go further if fixed, for the same reason as above | |||
if ($el.css('position') == 'fixed') { | |||
return false; | |||
} | |||
}); | |||
} | |||
if (overflows) { | |||
self._$tooltip.css('visibility', 'hidden'); | |||
} | |||
else { | |||
self._$tooltip.css('visibility', 'visible'); | |||
// reposition | |||
if (self.__options.repositionOnScroll) { | |||
self.reposition(event); | |||
} | |||
// or just adjust offset | |||
else { | |||
// we have to use offset and not windowOffset because this way, | |||
// only the scroll distance of the scrollable areas are taken into | |||
// account (the scrolltop value of the main window must be | |||
// ignored since the tooltip already moves with it) | |||
var offsetLeft = geo.origin.offset.left - self.__Geometry.origin.offset.left, | |||
offsetTop = geo.origin.offset.top - self.__Geometry.origin.offset.top; | |||
// add the offset to the position initially computed by the display plugin | |||
self._$tooltip.css({ | |||
left: self.__lastPosition.coord.left + offsetLeft, | |||
top: self.__lastPosition.coord.top + offsetTop | |||
}); | |||
} | |||
} | |||
} | |||
self._trigger({ | |||
type: 'scroll', | |||
event: event, | |||
geo: geo | |||
}); | |||
} | |||
} | |||
return self; | |||
}, | |||
/** | |||
* Changes the state of the tooltip | |||
* | |||
* @param {string} state | |||
* @returns {self} | |||
* @private | |||
*/ | |||
__stateSet: function(state) { | |||
this.__state = state; | |||
this._trigger({ | |||
type: 'state', | |||
state: state | |||
}); | |||
return this; | |||
}, | |||
/** | |||
* Clear appearance timeouts | |||
* | |||
* @returns {self} | |||
* @private | |||
*/ | |||
__timeoutsClear: function() { | |||
// there is only one possible open timeout: the delayed opening | |||
// when the mouseenter/touchstart open triggers are used | |||
clearTimeout(this.__timeouts.open); | |||
this.__timeouts.open = null; | |||
// ... but several close timeouts: the delayed closing when the | |||
// mouseleave close trigger is used and the timer option | |||
$.each(this.__timeouts.close, function(i, timeout) { | |||
clearTimeout(timeout); | |||
}); | |||
this.__timeouts.close = []; | |||
return this; | |||
}, | |||
/** | |||
* Start the tracker that will make checks at regular intervals | |||
* | |||
* @returns {self} | |||
* @private | |||
*/ | |||
__trackerStart: function() { | |||
var self = this, | |||
$content = self._$tooltip.find('.tooltipster-content'); | |||
// get the initial content size | |||
if (self.__options.trackTooltip) { | |||
self.__contentBcr = $content[0].getBoundingClientRect(); | |||
} | |||
self.__tracker = setInterval(function() { | |||
// if the origin or tooltip elements have been removed. | |||
// Note: we could destroy the instance now if the origin has | |||
// been removed but we'll leave that task to our garbage collector | |||
if (!bodyContains(self._$origin) || !bodyContains(self._$tooltip)) { | |||
self._close(); | |||
} | |||
// if everything is alright | |||
else { | |||
// compare the former and current positions of the origin to reposition | |||
// the tooltip if need be | |||
if (self.__options.trackOrigin) { | |||
var g = self.__geometry(), | |||
identical = false; | |||
// compare size first (a change requires repositioning too) | |||
if (areEqual(g.origin.size, self.__Geometry.origin.size)) { | |||
// for elements that have a fixed lineage (see __geometry()), we track the | |||
// top and left properties (relative to window) | |||
if (self.__Geometry.origin.fixedLineage) { | |||
if (areEqual(g.origin.windowOffset, self.__Geometry.origin.windowOffset)) { | |||
identical = true; | |||
} | |||
} | |||
// otherwise, track total offset (relative to document) | |||
else { | |||
if (areEqual(g.origin.offset, self.__Geometry.origin.offset)) { | |||
identical = true; | |||
} | |||
} | |||
} | |||
if (!identical) { | |||
// close the tooltip when using the mouseleave close trigger | |||
// (see https://github.com/iamceege/tooltipster/pull/253) | |||
if (self.__options.triggerClose.mouseleave) { | |||
self._close(); | |||
} | |||
else { | |||
self.reposition(); | |||
} | |||
} | |||
} | |||
if (self.__options.trackTooltip) { | |||
var currentBcr = $content[0].getBoundingClientRect(); | |||
if ( currentBcr.height !== self.__contentBcr.height | |||
|| currentBcr.width !== self.__contentBcr.width | |||
) { | |||
self.reposition(); | |||
self.__contentBcr = currentBcr; | |||
} | |||
} | |||
} | |||
}, self.__options.trackerInterval); | |||
return self; | |||
}, | |||
/** | |||
* Closes the tooltip (after the closing delay) | |||
* | |||
* @param event | |||
* @param callback | |||
* @param force Set to true to override a potential refusal of the user's function | |||
* @returns {self} | |||
* @protected | |||
*/ | |||
_close: function(event, callback, force) { | |||
var self = this, | |||
ok = true; | |||
self._trigger({ | |||
type: 'close', | |||
event: event, | |||
stop: function() { | |||
ok = false; | |||
} | |||
}); | |||
// a destroying tooltip (force == true) may not refuse to close | |||
if (ok || force) { | |||
// save the method custom callback and cancel any open method custom callbacks | |||
if (callback) self.__callbacks.close.push(callback); | |||
self.__callbacks.open = []; | |||
// clear open/close timeouts | |||
self.__timeoutsClear(); | |||
var finishCallbacks = function() { | |||
// trigger any close method custom callbacks and reset them | |||
$.each(self.__callbacks.close, function(i,c) { | |||
c.call(self, self, { | |||
event: event, | |||
origin: self._$origin[0] | |||
}); | |||
}); | |||
self.__callbacks.close = []; | |||
}; | |||
if (self.__state != 'closed') { | |||
var necessary = true, | |||
d = new Date(), | |||
now = d.getTime(), | |||
newClosingTime = now + self.__options.animationDuration[1]; | |||
// the tooltip may already already be disappearing, but if a new | |||
// call to close() is made after the animationDuration was changed | |||
// to 0 (for example), we ought to actually close it sooner than | |||
// previously scheduled. In that case it should be noted that the | |||
// browser will not adapt the animation duration to the new | |||
// animationDuration that was set after the start of the closing | |||
// animation. | |||
// Note: the same thing could be considered at opening, but is not | |||
// really useful since the tooltip is actually opened immediately | |||
// upon a call to _open(). Since it would not make the opening | |||
// animation finish sooner, its sole impact would be to trigger the | |||
// state event and the open callbacks sooner than the actual end of | |||
// the opening animation, which is not great. | |||
if (self.__state == 'disappearing') { | |||
if ( newClosingTime > self.__closingTime | |||
// in case closing is actually overdue because the script | |||
// execution was suspended. See #679 | |||
&& self.__options.animationDuration[1] > 0 | |||
) { | |||
necessary = false; | |||
} | |||
} | |||
if (necessary) { | |||
self.__closingTime = newClosingTime; | |||
if (self.__state != 'disappearing') { | |||
self.__stateSet('disappearing'); | |||
} | |||
var finish = function() { | |||
// stop the tracker | |||
clearInterval(self.__tracker); | |||
// a "beforeClose" option has been asked several times but would | |||
// probably useless since the content element is still accessible | |||
// via ::content(), and because people can always use listeners | |||
// inside their content to track what's going on. For the sake of | |||
// simplicity, this has been denied. Bur for the rare people who | |||
// really need the option (for old browsers or for the case where | |||
// detaching the content is actually destructive, for file or | |||
// password inputs for example), this event will do the work. | |||
self._trigger({ | |||
type: 'closing', | |||
event: event | |||
}); | |||
// unbind listeners which are no longer needed | |||
self._$tooltip | |||
.off('.'+ self.__namespace +'-triggerClose') | |||
.removeClass('tooltipster-dying'); | |||
// orientationchange, scroll and resize listeners | |||
$(env.window).off('.'+ self.__namespace +'-triggerClose'); | |||
// scroll listeners | |||
self.__$originParents.each(function(i, el) { | |||
$(el).off('scroll.'+ self.__namespace +'-triggerClose'); | |||
}); | |||
// clear the array to prevent memory leaks | |||
self.__$originParents = null; | |||
$(env.window.document.body).off('.'+ self.__namespace +'-triggerClose'); | |||
self._$origin.off('.'+ self.__namespace +'-triggerClose'); | |||
self._off('dismissable'); | |||
// a plugin that would like to remove the tooltip from the | |||
// DOM when closed should bind on this | |||
self.__stateSet('closed'); | |||
// trigger event | |||
self._trigger({ | |||
type: 'after', | |||
event: event | |||
}); | |||
// call our constructor custom callback function | |||
if (self.__options.functionAfter) { | |||
self.__options.functionAfter.call(self, self, { | |||
event: event, | |||
origin: self._$origin[0] | |||
}); | |||
} | |||
// call our method custom callbacks functions | |||
finishCallbacks(); | |||
}; | |||
if (env.hasTransitions) { | |||
self._$tooltip.css({ | |||
'-moz-animation-duration': self.__options.animationDuration[1] + 'ms', | |||
'-ms-animation-duration': self.__options.animationDuration[1] + 'ms', | |||
'-o-animation-duration': self.__options.animationDuration[1] + 'ms', | |||
'-webkit-animation-duration': self.__options.animationDuration[1] + 'ms', | |||
'animation-duration': self.__options.animationDuration[1] + 'ms', | |||
'transition-duration': self.__options.animationDuration[1] + 'ms' | |||
}); | |||
self._$tooltip | |||
// clear both potential open and close tasks | |||
.clearQueue() | |||
.removeClass('tooltipster-show') | |||
// for transitions only | |||
.addClass('tooltipster-dying'); | |||
if (self.__options.animationDuration[1] > 0) { | |||
self._$tooltip.delay(self.__options.animationDuration[1]); | |||
} | |||
self._$tooltip.queue(finish); | |||
} | |||
else { | |||
self._$tooltip | |||
.stop() | |||
.fadeOut(self.__options.animationDuration[1], finish); | |||
} | |||
} | |||
} | |||
// if the tooltip is already closed, we still need to trigger | |||
// the method custom callbacks | |||
else { | |||
finishCallbacks(); | |||
} | |||
} | |||
return self; | |||
}, | |||
/** | |||
* For internal use by plugins, if needed | |||
* | |||
* @returns {self} | |||
* @protected | |||
*/ | |||
_off: function() { | |||
this.__$emitterPrivate.off.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments)); | |||
return this; | |||
}, | |||
/** | |||
* For internal use by plugins, if needed | |||
* | |||
* @returns {self} | |||
* @protected | |||
*/ | |||
_on: function() { | |||
this.__$emitterPrivate.on.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments)); | |||
return this; | |||
}, | |||
/** | |||
* For internal use by plugins, if needed | |||
* | |||
* @returns {self} | |||
* @protected | |||
*/ | |||
_one: function() { | |||
this.__$emitterPrivate.one.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments)); | |||
return this; | |||
}, | |||
/** | |||
* Opens the tooltip right away. | |||
* | |||
* @param event | |||
* @param callback Will be called when the opening animation is over | |||
* @returns {self} | |||
* @protected | |||
*/ | |||
_open: function(event, callback) { | |||
var self = this; | |||
// if the destruction process has not begun and if this was not | |||
// triggered by an unwanted emulated click event | |||
if (!self.__destroying) { | |||
// check that the origin is still in the DOM | |||
if ( bodyContains(self._$origin) | |||
// if the tooltip is enabled | |||
&& self.__enabled | |||
) { | |||
var ok = true; | |||
// if the tooltip is not open yet, we need to call functionBefore. | |||
// otherwise we can jst go on | |||
if (self.__state == 'closed') { | |||
// trigger an event. The event.stop function allows the callback | |||
// to prevent the opening of the tooltip | |||
self._trigger({ | |||
type: 'before', | |||
event: event, | |||
stop: function() { | |||
ok = false; | |||
} | |||
}); | |||
if (ok && self.__options.functionBefore) { | |||
// call our custom function before continuing | |||
ok = self.__options.functionBefore.call(self, self, { | |||
event: event, | |||
origin: self._$origin[0] | |||
}); | |||
} | |||
} | |||
if (ok !== false) { | |||
// if there is some content | |||
if (self.__Content !== null) { | |||
// save the method callback and cancel close method callbacks | |||
if (callback) { | |||
self.__callbacks.open.push(callback); | |||
} | |||
self.__callbacks.close = []; | |||
// get rid of any appearance timeouts | |||
self.__timeoutsClear(); | |||
var extraTime, | |||
finish = function() { | |||
if (self.__state != 'stable') { | |||
self.__stateSet('stable'); | |||
} | |||
// trigger any open method custom callbacks and reset them | |||
$.each(self.__callbacks.open, function(i,c) { | |||
c.call(self, self, { | |||
origin: self._$origin[0], | |||
tooltip: self._$tooltip[0] | |||
}); | |||
}); | |||
self.__callbacks.open = []; | |||
}; | |||
// if the tooltip is already open | |||
if (self.__state !== 'closed') { | |||
// the timer (if any) will start (or restart) right now | |||
extraTime = 0; | |||
// if it was disappearing, cancel that | |||
if (self.__state === 'disappearing') { | |||
self.__stateSet('appearing'); | |||
if (env.hasTransitions) { | |||
self._$tooltip | |||
.clearQueue() | |||
.removeClass('tooltipster-dying') | |||
.addClass('tooltipster-show'); | |||
if (self.__options.animationDuration[0] > 0) { | |||
self._$tooltip.delay(self.__options.animationDuration[0]); | |||
} | |||
self._$tooltip.queue(finish); | |||
} | |||
else { | |||
// in case the tooltip was currently fading out, bring it back | |||
// to life | |||
self._$tooltip | |||
.stop() | |||
.fadeIn(finish); | |||
} | |||
} | |||
// if the tooltip is already open, we still need to trigger the method | |||
// custom callback | |||
else if (self.__state == 'stable') { | |||
finish(); | |||
} | |||
} | |||
// if the tooltip isn't already open, open it | |||
else { | |||
// a plugin must bind on this and store the tooltip in this._$tooltip | |||
self.__stateSet('appearing'); | |||
// the timer (if any) will start when the tooltip has fully appeared | |||
// after its transition | |||
extraTime = self.__options.animationDuration[0]; | |||
// insert the content inside the tooltip | |||
self.__contentInsert(); | |||
// reposition the tooltip and attach to the DOM | |||
self.reposition(event, true); | |||
// animate in the tooltip. If the display plugin wants no css | |||
// animations, it may override the animation option with a | |||
// dummy value that will produce no effect | |||
if (env.hasTransitions) { | |||
// note: there seems to be an issue with start animations which | |||
// are randomly not played on fast devices in both Chrome and FF, | |||
// couldn't find a way to solve it yet. It seems that applying | |||
// the classes before appending to the DOM helps a little, but | |||
// it messes up some CSS transitions. The issue almost never | |||
// happens when delay[0]==0 though | |||
self._$tooltip | |||
.addClass('tooltipster-'+ self.__options.animation) | |||
.addClass('tooltipster-initial') | |||
.css({ | |||
'-moz-animation-duration': self.__options.animationDuration[0] + 'ms', | |||
'-ms-animation-duration': self.__options.animationDuration[0] + 'ms', | |||
'-o-animation-duration': self.__options.animationDuration[0] + 'ms', | |||
'-webkit-animation-duration': self.__options.animationDuration[0] + 'ms', | |||
'animation-duration': self.__options.animationDuration[0] + 'ms', | |||
'transition-duration': self.__options.animationDuration[0] + 'ms' | |||
}); | |||
setTimeout( | |||
function() { | |||
// a quick hover may have already triggered a mouseleave | |||
if (self.__state != 'closed') { | |||
self._$tooltip | |||
.addClass('tooltipster-show') | |||
.removeClass('tooltipster-initial'); | |||
if (self.__options.animationDuration[0] > 0) { | |||
self._$tooltip.delay(self.__options.animationDuration[0]); | |||
} | |||
self._$tooltip.queue(finish); | |||
} | |||
}, | |||
0 | |||
); | |||
} | |||
else { | |||
// old browsers will have to live with this | |||
self._$tooltip | |||
.css('display', 'none') | |||
.fadeIn(self.__options.animationDuration[0], finish); | |||
} | |||
// checks if the origin is removed while the tooltip is open | |||
self.__trackerStart(); | |||
// NOTE: the listeners below have a '-triggerClose' namespace | |||
// because we'll remove them when the tooltip closes (unlike | |||
// the '-triggerOpen' listeners). So some of them are actually | |||
// not about close triggers, rather about positioning. | |||
$(env.window) | |||
// reposition on resize | |||
.on('resize.'+ self.__namespace +'-triggerClose', function(e) { | |||
var $ae = $(document.activeElement); | |||
// reposition only if the resize event was not triggered upon the opening | |||
// of a virtual keyboard due to an input field being focused within the tooltip | |||
// (otherwise the repositioning would lose the focus) | |||
if ( (!$ae.is('input') && !$ae.is('textarea')) | |||
|| !$.contains(self._$tooltip[0], $ae[0]) | |||
) { | |||
self.reposition(e); | |||
} | |||
}) | |||
// same as below for parents | |||
.on('scroll.'+ self.__namespace +'-triggerClose', function(e) { | |||
self.__scrollHandler(e); | |||
}); | |||
self.__$originParents = self._$origin.parents(); | |||
// scrolling may require the tooltip to be moved or even | |||
// repositioned in some cases | |||
self.__$originParents.each(function(i, parent) { | |||
$(parent).on('scroll.'+ self.__namespace +'-triggerClose', function(e) { | |||
self.__scrollHandler(e); | |||
}); | |||
}); | |||
if ( self.__options.triggerClose.mouseleave | |||
|| (self.__options.triggerClose.touchleave && env.hasTouchCapability) | |||
) { | |||
// we use an event to allow users/plugins to control when the mouseleave/touchleave | |||
// close triggers will come to action. It allows to have more triggering elements | |||
// than just the origin and the tooltip for example, or to cancel/delay the closing, | |||
// or to make the tooltip interactive even if it wasn't when it was open, etc. | |||
self._on('dismissable', function(event) { | |||
if (event.dismissable) { | |||
if (event.delay) { | |||
timeout = setTimeout(function() { | |||
// event.event may be undefined | |||
self._close(event.event); | |||
}, event.delay); | |||
self.__timeouts.close.push(timeout); | |||
} | |||
else { | |||
self._close(event); | |||
} | |||
} | |||
else { | |||
clearTimeout(timeout); | |||
} | |||
}); | |||
// now set the listeners that will trigger 'dismissable' events | |||
var $elements = self._$origin, | |||
eventNamesIn = '', | |||
eventNamesOut = '', | |||
timeout = null; | |||
// if we have to allow interaction, bind on the tooltip too | |||
if (self.__options.interactive) { | |||
$elements = $elements.add(self._$tooltip); | |||
} | |||
if (self.__options.triggerClose.mouseleave) { | |||
eventNamesIn += 'mouseenter.'+ self.__namespace +'-triggerClose '; | |||
eventNamesOut += 'mouseleave.'+ self.__namespace +'-triggerClose '; | |||
} | |||
if (self.__options.triggerClose.touchleave && env.hasTouchCapability) { | |||
eventNamesIn += 'touchstart.'+ self.__namespace +'-triggerClose'; | |||
eventNamesOut += 'touchend.'+ self.__namespace +'-triggerClose touchcancel.'+ self.__namespace +'-triggerClose'; | |||
} | |||
$elements | |||
// close after some time spent outside of the elements | |||
.on(eventNamesOut, function(event) { | |||
// it's ok if the touch gesture ended up to be a swipe, | |||
// it's still a "touch leave" situation | |||
if ( self._touchIsTouchEvent(event) | |||
|| !self._touchIsEmulatedEvent(event) | |||
) { | |||
var delay = (event.type == 'mouseleave') ? | |||
self.__options.delay : | |||
self.__options.delayTouch; | |||
self._trigger({ | |||
delay: delay[1], | |||
dismissable: true, | |||
event: event, | |||
type: 'dismissable' | |||
}); | |||
} | |||
}) | |||
// suspend the mouseleave timeout when the pointer comes back | |||
// over the elements | |||
.on(eventNamesIn, function(event) { | |||
// it's also ok if the touch event is a swipe gesture | |||
if ( self._touchIsTouchEvent(event) | |||
|| !self._touchIsEmulatedEvent(event) | |||
) { | |||
self._trigger({ | |||
dismissable: false, | |||
event: event, | |||
type: 'dismissable' | |||
}); | |||
} | |||
}); | |||
} | |||
// close the tooltip when the origin gets a mouse click (common behavior of | |||
// native tooltips) | |||
if (self.__options.triggerClose.originClick) { | |||
self._$origin.on('click.'+ self.__namespace + '-triggerClose', function(event) { | |||
// we could actually let a tap trigger this but this feature just | |||
// does not make sense on touch devices | |||
if ( !self._touchIsTouchEvent(event) | |||
&& !self._touchIsEmulatedEvent(event) | |||
) { | |||
self._close(event); | |||
} | |||
}); | |||
} | |||
// set the same bindings for click and touch on the body to close the tooltip | |||
if ( self.__options.triggerClose.click | |||
|| (self.__options.triggerClose.tap && env.hasTouchCapability) | |||
) { | |||
// don't set right away since the click/tap event which triggered this method | |||
// (if it was a click/tap) is going to bubble up to the body, we don't want it | |||
// to close the tooltip immediately after it opened | |||
setTimeout(function() { | |||
if (self.__state != 'closed') { | |||
var eventNames = '', | |||
$body = $(env.window.document.body); | |||
if (self.__options.triggerClose.click) { | |||
eventNames += 'click.'+ self.__namespace +'-triggerClose '; | |||
} | |||
if (self.__options.triggerClose.tap && env.hasTouchCapability) { | |||
eventNames += 'touchend.'+ self.__namespace +'-triggerClose'; | |||
} | |||
$body.on(eventNames, function(event) { | |||
if (self._touchIsMeaningfulEvent(event)) { | |||
self._touchRecordEvent(event); | |||
if (!self.__options.interactive || !$.contains(self._$tooltip[0], event.target)) { | |||
self._close(event); | |||
} | |||
} | |||
}); | |||
// needed to detect and ignore swiping | |||
if (self.__options.triggerClose.tap && env.hasTouchCapability) { | |||
$body.on('touchstart.'+ self.__namespace +'-triggerClose', function(event) { | |||
self._touchRecordEvent(event); | |||
}); | |||
} | |||
} | |||
}, 0); | |||
} | |||
self._trigger('ready'); | |||
// call our custom callback | |||
if (self.__options.functionReady) { | |||
self.__options.functionReady.call(self, self, { | |||
origin: self._$origin[0], | |||
tooltip: self._$tooltip[0] | |||
}); | |||
} | |||
} | |||
// if we have a timer set, let the countdown begin | |||
if (self.__options.timer > 0) { | |||
var timeout = setTimeout(function() { | |||
self._close(); | |||
}, self.__options.timer + extraTime); | |||
self.__timeouts.close.push(timeout); | |||
} | |||
} | |||
} | |||
} | |||
} | |||
return self; | |||
}, | |||
/** | |||
* When using the mouseenter/touchstart open triggers, this function will | |||
* schedule the opening of the tooltip after the delay, if there is one | |||
* | |||
* @param event | |||
* @returns {self} | |||
* @protected | |||
*/ | |||
_openShortly: function(event) { | |||
var self = this, | |||
ok = true; | |||
if (self.__state != 'stable' && self.__state != 'appearing') { | |||
// if a timeout is not already running | |||
if (!self.__timeouts.open) { | |||
self._trigger({ | |||
type: 'start', | |||
event: event, | |||
stop: function() { | |||
ok = false; | |||
} | |||
}); | |||
if (ok) { | |||
var delay = (event.type.indexOf('touch') == 0) ? | |||
self.__options.delayTouch : | |||
self.__options.delay; | |||
if (delay[0]) { | |||
self.__timeouts.open = setTimeout(function() { | |||
self.__timeouts.open = null; | |||
// open only if the pointer (mouse or touch) is still over the origin. | |||
// The check on the "meaningful event" can only be made here, after some | |||
// time has passed (to know if the touch was a swipe or not) | |||
if (self.__pointerIsOverOrigin && self._touchIsMeaningfulEvent(event)) { | |||
// signal that we go on | |||
self._trigger('startend'); | |||
self._open(event); | |||
} | |||
else { | |||
// signal that we cancel | |||
self._trigger('startcancel'); | |||
} | |||
}, delay[0]); | |||
} | |||
else { | |||
// signal that we go on | |||
self._trigger('startend'); | |||
self._open(event); | |||
} | |||
} | |||
} | |||
} | |||
return self; | |||
}, | |||
/** | |||
* Meant for plugins to get their options | |||
* | |||
* @param {string} pluginName The name of the plugin that asks for its options | |||
* @param {object} defaultOptions The default options of the plugin | |||
* @returns {object} The options | |||
* @protected | |||
*/ | |||
_optionsExtract: function(pluginName, defaultOptions) { | |||
var self = this, | |||
options = $.extend(true, {}, defaultOptions); | |||
// if the plugin options were isolated in a property named after the | |||
// plugin, use them (prevents conflicts with other plugins) | |||
var pluginOptions = self.__options[pluginName]; | |||
// if not, try to get them as regular options | |||
if (!pluginOptions){ | |||
pluginOptions = {}; | |||
$.each(defaultOptions, function(optionName, value) { | |||
var o = self.__options[optionName]; | |||
if (o !== undefined) { | |||
pluginOptions[optionName] = o; | |||
} | |||
}); | |||
} | |||
// let's merge the default options and the ones that were provided. We'd want | |||
// to do a deep copy but not let jQuery merge arrays, so we'll do a shallow | |||
// extend on two levels, that will be enough if options are not more than 1 | |||
// level deep | |||
$.each(options, function(optionName, value) { | |||
if (pluginOptions[optionName] !== undefined) { | |||
if (( typeof value == 'object' | |||
&& !(value instanceof Array) | |||
&& value != null | |||
) | |||
&& | |||
( typeof pluginOptions[optionName] == 'object' | |||
&& !(pluginOptions[optionName] instanceof Array) | |||
&& pluginOptions[optionName] != null | |||
) | |||
) { | |||
$.extend(options[optionName], pluginOptions[optionName]); | |||
} | |||
else { | |||
options[optionName] = pluginOptions[optionName]; | |||
} | |||
} | |||
}); | |||
return options; | |||
}, | |||
/** | |||
* Used at instantiation of the plugin, or afterwards by plugins that activate themselves | |||
* on existing instances | |||
* | |||
* @param {object} pluginName | |||
* @returns {self} | |||
* @protected | |||
*/ | |||
_plug: function(pluginName) { | |||
var plugin = $.tooltipster._plugin(pluginName); | |||
if (plugin) { | |||
// if there is a constructor for instances | |||
if (plugin.instance) { | |||
// proxy non-private methods on the instance to allow new instance methods | |||
$.tooltipster.__bridge(plugin.instance, this, plugin.name); | |||
} | |||
} | |||
else { | |||
throw new Error('The "'+ pluginName +'" plugin is not defined'); | |||
} | |||
return this; | |||
}, | |||
/** | |||
* This will return true if the event is a mouse event which was | |||
* emulated by the browser after a touch event. This allows us to | |||
* really dissociate mouse and touch triggers. | |||
* | |||
* There is a margin of error if a real mouse event is fired right | |||
* after (within the delay shown below) a touch event on the same | |||
* element, but hopefully it should not happen often. | |||
* | |||
* @returns {boolean} | |||
* @protected | |||
*/ | |||
_touchIsEmulatedEvent: function(event) { | |||
var isEmulated = false, | |||
now = new Date().getTime(); | |||
for (var i = this.__touchEvents.length - 1; i >= 0; i--) { | |||
var e = this.__touchEvents[i]; | |||
// delay, in milliseconds. It's supposed to be 300ms in | |||
// most browsers (350ms on iOS) to allow a double tap but | |||
// can be less (check out FastClick for more info) | |||
if (now - e.time < 500) { | |||
if (e.target === event.target) { | |||
isEmulated = true; | |||
} | |||
} | |||
else { | |||
break; | |||
} | |||
} | |||
return isEmulated; | |||
}, | |||
/** | |||
* Returns false if the event was an emulated mouse event or | |||
* a touch event involved in a swipe gesture. | |||
* | |||
* @param {object} event | |||
* @returns {boolean} | |||
* @protected | |||
*/ | |||
_touchIsMeaningfulEvent: function(event) { | |||
return ( | |||
(this._touchIsTouchEvent(event) && !this._touchSwiped(event.target)) | |||
|| (!this._touchIsTouchEvent(event) && !this._touchIsEmulatedEvent(event)) | |||
); | |||
}, | |||
/** | |||
* Checks if an event is a touch event | |||
* | |||
* @param {object} event | |||
* @returns {boolean} | |||
* @protected | |||
*/ | |||
_touchIsTouchEvent: function(event){ | |||
return event.type.indexOf('touch') == 0; | |||
}, | |||
/** | |||
* Store touch events for a while to detect swiping and emulated mouse events | |||
* | |||
* @param {object} event | |||
* @returns {self} | |||
* @protected | |||
*/ | |||
_touchRecordEvent: function(event) { | |||
if (this._touchIsTouchEvent(event)) { | |||
event.time = new Date().getTime(); | |||
this.__touchEvents.push(event); | |||
} | |||
return this; | |||
}, | |||
/** | |||
* Returns true if a swipe happened after the last touchstart event fired on | |||
* event.target. | |||
* | |||
* We need to differentiate a swipe from a tap before we let the event open | |||
* or close the tooltip. A swipe is when a touchmove (scroll) event happens | |||
* on the body between the touchstart and the touchend events of an element. | |||
* | |||
* @param {object} target The HTML element that may have triggered the swipe | |||
* @returns {boolean} | |||
* @protected | |||
*/ | |||
_touchSwiped: function(target) { | |||
var swiped = false; | |||
for (var i = this.__touchEvents.length - 1; i >= 0; i--) { | |||
var e = this.__touchEvents[i]; | |||
if (e.type == 'touchmove') { | |||
swiped = true; | |||
break; | |||
} | |||
else if ( | |||
e.type == 'touchstart' | |||
&& target === e.target | |||
) { | |||
break; | |||
} | |||
} | |||
return swiped; | |||
}, | |||
/** | |||
* Triggers an event on the instance emitters | |||
* | |||
* @returns {self} | |||
* @protected | |||
*/ | |||
_trigger: function() { | |||
var args = Array.prototype.slice.apply(arguments); | |||
if (typeof args[0] == 'string') { | |||
args[0] = { type: args[0] }; | |||
} | |||
// add properties to the event | |||
args[0].instance = this; | |||
args[0].origin = this._$origin ? this._$origin[0] : null; | |||
args[0].tooltip = this._$tooltip ? this._$tooltip[0] : null; | |||
// note: the order of emitters matters | |||
this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate, args); | |||
$.tooltipster._trigger.apply($.tooltipster, args); | |||
this.__$emitterPublic.trigger.apply(this.__$emitterPublic, args); | |||
return this; | |||
}, | |||
/** | |||
* Deactivate a plugin on this instance | |||
* | |||
* @returns {self} | |||
* @protected | |||
*/ | |||
_unplug: function(pluginName) { | |||
var self = this; | |||
// if the plugin has been activated on this instance | |||
if (self[pluginName]) { | |||
var plugin = $.tooltipster._plugin(pluginName); | |||
// if there is a constructor for instances | |||
if (plugin.instance) { | |||
// unbridge | |||
$.each(plugin.instance, function(methodName, fn) { | |||
// if the method exists (privates methods do not) and comes indeed from | |||
// this plugin (may be missing or come from a conflicting plugin). | |||
if ( self[methodName] | |||
&& self[methodName].bridged === self[pluginName] | |||
) { | |||
delete self[methodName]; | |||
} | |||
}); | |||
} | |||
// destroy the plugin | |||
if (self[pluginName].__destroy) { | |||
self[pluginName].__destroy(); | |||
} | |||
// remove the reference to the plugin instance | |||
delete self[pluginName]; | |||
} | |||
return self; | |||
}, | |||
/** | |||
* @see self::_close | |||
* @returns {self} | |||
* @public | |||
*/ | |||
close: function(callback) { | |||
if (!this.__destroyed) { | |||
this._close(null, callback); | |||
} | |||
else { | |||
this.__destroyError(); | |||
} | |||
return this; | |||
}, | |||
/** | |||
* Sets or gets the content of the tooltip | |||
* | |||
* @returns {mixed|self} | |||
* @public | |||
*/ | |||
content: function(content) { | |||
var self = this; | |||
// getter method | |||
if (content === undefined) { | |||
return self.__Content; | |||
} | |||
// setter method | |||
else { | |||
if (!self.__destroyed) { | |||
// change the content | |||
self.__contentSet(content); | |||
if (self.__Content !== null) { | |||
// update the tooltip if it is open | |||
if (self.__state !== 'closed') { | |||
// reset the content in the tooltip | |||
self.__contentInsert(); | |||
// reposition and resize the tooltip | |||
self.reposition(); | |||
// if we want to play a little animation showing the content changed | |||
if (self.__options.updateAnimation) { | |||
if (env.hasTransitions) { | |||
// keep the reference in the local scope | |||
var animation = self.__options.updateAnimation; | |||
self._$tooltip.addClass('tooltipster-update-'+ animation); | |||
// remove the class after a while. The actual duration of the | |||
// update animation may be shorter, it's set in the CSS rules | |||
setTimeout(function() { | |||
if (self.__state != 'closed') { | |||
self._$tooltip.removeClass('tooltipster-update-'+ animation); | |||
} | |||
}, 1000); | |||
} | |||
else { | |||
self._$tooltip.fadeTo(200, 0.5, function() { | |||
if (self.__state != 'closed') { | |||
self._$tooltip.fadeTo(200, 1); | |||
} | |||
}); | |||
} | |||
} | |||
} | |||
} | |||
else { | |||
self._close(); | |||
} | |||
} | |||
else { | |||
self.__destroyError(); | |||
} | |||
return self; | |||
} | |||
}, | |||
/** | |||
* Destroys the tooltip | |||
* | |||
* @returns {self} | |||
* @public | |||
*/ | |||
destroy: function() { | |||
var self = this; | |||
if (!self.__destroyed) { | |||
if(self.__state != 'closed'){ | |||
// no closing delay | |||
self.option('animationDuration', 0) | |||
// force closing | |||
._close(null, null, true); | |||
} | |||
else { | |||
// there might be an open timeout still running | |||
self.__timeoutsClear(); | |||
} | |||
// send event | |||
self._trigger('destroy'); | |||
self.__destroyed = true; | |||
self._$origin | |||
.removeData(self.__namespace) | |||
// remove the open trigger listeners | |||
.off('.'+ self.__namespace +'-triggerOpen'); | |||
// remove the touch listener | |||
$(env.window.document.body).off('.' + self.__namespace +'-triggerOpen'); | |||
var ns = self._$origin.data('tooltipster-ns'); | |||
// if the origin has been removed from DOM, its data may | |||
// well have been destroyed in the process and there would | |||
// be nothing to clean up or restore | |||
if (ns) { | |||
// if there are no more tooltips on this element | |||
if (ns.length === 1) { | |||
// optional restoration of a title attribute | |||
var title = null; | |||
if (self.__options.restoration == 'previous') { | |||
title = self._$origin.data('tooltipster-initialTitle'); | |||
} | |||
else if (self.__options.restoration == 'current') { | |||
// old school technique to stringify when outerHTML is not supported | |||
title = (typeof self.__Content == 'string') ? | |||
self.__Content : | |||
$('<div></div>').append(self.__Content).html(); | |||
} | |||
if (title) { | |||
self._$origin.attr('title', title); | |||
} | |||
// final cleaning | |||
self._$origin.removeClass('tooltipstered'); | |||
self._$origin | |||
.removeData('tooltipster-ns') | |||
.removeData('tooltipster-initialTitle'); | |||
} | |||
else { | |||
// remove the instance namespace from the list of namespaces of | |||
// tooltips present on the element | |||
ns = $.grep(ns, function(el, i) { | |||
return el !== self.__namespace; | |||
}); | |||
self._$origin.data('tooltipster-ns', ns); | |||
} | |||
} | |||
// last event | |||
self._trigger('destroyed'); | |||
// unbind private and public event listeners | |||
self._off(); | |||
self.off(); | |||
// remove external references, just in case | |||
self.__Content = null; | |||
self.__$emitterPrivate = null; | |||
self.__$emitterPublic = null; | |||
self.__options.parent = null; | |||
self._$origin = null; | |||
self._$tooltip = null; | |||
// make sure the object is no longer referenced in there to prevent | |||
// memory leaks | |||
$.tooltipster.__instancesLatestArr = $.grep($.tooltipster.__instancesLatestArr, function(el, i) { | |||
return self !== el; | |||
}); | |||
clearInterval(self.__garbageCollector); | |||
} | |||
else { | |||
self.__destroyError(); | |||
} | |||
// we return the scope rather than true so that the call to | |||
// .tooltipster('destroy') actually returns the matched elements | |||
// and applies to all of them | |||
return self; | |||
}, | |||
/** | |||
* Disables the tooltip | |||
* | |||
* @returns {self} | |||
* @public | |||
*/ | |||
disable: function() { | |||
if (!this.__destroyed) { | |||
// close first, in case the tooltip would not disappear on | |||
// its own (no close trigger) | |||
this._close(); | |||
this.__enabled = false; | |||
return this; | |||
} | |||
else { | |||
this.__destroyError(); | |||
} | |||
return this; | |||
}, | |||
/** | |||
* Returns the HTML element of the origin | |||
* | |||
* @returns {self} | |||
* @public | |||
*/ | |||
elementOrigin: function() { | |||
if (!this.__destroyed) { | |||
return this._$origin[0]; | |||
} | |||
else { | |||
this.__destroyError(); | |||
} | |||
}, | |||
/** | |||
* Returns the HTML element of the tooltip | |||
* | |||
* @returns {self} | |||
* @public | |||
*/ | |||
elementTooltip: function() { | |||
return this._$tooltip ? this._$tooltip[0] : null; | |||
}, | |||
/** | |||
* Enables the tooltip | |||
* | |||
* @returns {self} | |||
* @public | |||
*/ | |||
enable: function() { | |||
this.__enabled = true; | |||
return this; | |||
}, | |||
/** | |||
* Alias, deprecated in 4.0.0 | |||
* | |||
* @param {function} callback | |||
* @returns {self} | |||
* @public | |||
*/ | |||
hide: function(callback) { | |||
return this.close(callback); | |||
}, | |||
/** | |||
* Returns the instance | |||
* | |||
* @returns {self} | |||
* @public | |||
*/ | |||
instance: function() { | |||
return this; | |||
}, | |||
/** | |||
* For public use only, not to be used by plugins (use ::_off() instead) | |||
* | |||
* @returns {self} | |||
* @public | |||
*/ | |||
off: function() { | |||
if (!this.__destroyed) { | |||
this.__$emitterPublic.off.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); | |||
} | |||
return this; | |||
}, | |||
/** | |||
* For public use only, not to be used by plugins (use ::_on() instead) | |||
* | |||
* @returns {self} | |||
* @public | |||
*/ | |||
on: function() { | |||
if (!this.__destroyed) { | |||
this.__$emitterPublic.on.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); | |||
} | |||
else { | |||
this.__destroyError(); | |||
} | |||
return this; | |||
}, | |||
/** | |||
* For public use only, not to be used by plugins | |||
* | |||
* @returns {self} | |||
* @public | |||
*/ | |||
one: function() { | |||
if (!this.__destroyed) { | |||
this.__$emitterPublic.one.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); | |||
} | |||
else { | |||
this.__destroyError(); | |||
} | |||
return this; | |||
}, | |||
/** | |||
* @see self::_open | |||
* @returns {self} | |||
* @public | |||
*/ | |||
open: function(callback) { | |||
if (!this.__destroyed) { | |||
this._open(null, callback); | |||
} | |||
else { | |||
this.__destroyError(); | |||
} | |||
return this; | |||
}, | |||
/** | |||
* Get or set options. For internal use and advanced users only. | |||
* | |||
* @param {string} o Option name | |||
* @param {mixed} val optional A new value for the option | |||
* @return {mixed|self} If val is omitted, the value of the option | |||
* is returned, otherwise the instance itself is returned | |||
* @public | |||
*/ | |||
option: function(o, val) { | |||
// getter | |||
if (val === undefined) { | |||
return this.__options[o]; | |||
} | |||
// setter | |||
else { | |||
if (!this.__destroyed) { | |||
// change value | |||
this.__options[o] = val; | |||
// format | |||
this.__optionsFormat(); | |||
// re-prepare the triggers if needed | |||
if ($.inArray(o, ['trigger', 'triggerClose', 'triggerOpen']) >= 0) { | |||
this.__prepareOrigin(); | |||
} | |||
if (o === 'selfDestruction') { | |||
this.__prepareGC(); | |||
} | |||
} | |||
else { | |||
this.__destroyError(); | |||
} | |||
return this; | |||
} | |||
}, | |||
/** | |||
* This method is in charge of setting the position and size properties of the tooltip. | |||
* All the hard work is delegated to the display plugin. | |||
* Note: The tooltip may be detached from the DOM at the moment the method is called | |||
* but must be attached by the end of the method call. | |||
* | |||
* @param {object} event For internal use only. Defined if an event such as | |||
* window resizing triggered the repositioning | |||
* @param {boolean} tooltipIsDetached For internal use only. Set this to true if you | |||
* know that the tooltip not being in the DOM is not an issue (typically when the | |||
* tooltip element has just been created but has not been added to the DOM yet). | |||
* @returns {self} | |||
* @public | |||
*/ | |||
reposition: function(event, tooltipIsDetached) { | |||
var self = this; | |||
if (!self.__destroyed) { | |||
// if the tooltip is still open and the origin is still in the DOM | |||
if (self.__state != 'closed' && bodyContains(self._$origin)) { | |||
// if the tooltip has not been removed from DOM manually (or if it | |||
// has been detached on purpose) | |||
if (tooltipIsDetached || bodyContains(self._$tooltip)) { | |||
if (!tooltipIsDetached) { | |||
// detach in case the tooltip overflows the window and adds | |||
// scrollbars to it, so __geometry can be accurate | |||
self._$tooltip.detach(); | |||
} | |||
// refresh the geometry object before passing it as a helper | |||
self.__Geometry = self.__geometry(); | |||
// let a plugin fo the rest | |||
self._trigger({ | |||
type: 'reposition', | |||
event: event, | |||
helper: { | |||
geo: self.__Geometry | |||
} | |||
}); | |||
} | |||
} | |||
} | |||
else { | |||
self.__destroyError(); | |||
} | |||
return self; | |||
}, | |||
/** | |||
* Alias, deprecated in 4.0.0 | |||
* | |||
* @param callback | |||
* @returns {self} | |||
* @public | |||
*/ | |||
show: function(callback) { | |||
return this.open(callback); | |||
}, | |||
/** | |||
* Returns some properties about the instance | |||
* | |||
* @returns {object} | |||
* @public | |||
*/ | |||
status: function() { | |||
return { | |||
destroyed: this.__destroyed, | |||
enabled: this.__enabled, | |||
open: this.__state !== 'closed', | |||
state: this.__state | |||
}; | |||
}, | |||
/** | |||
* For public use only, not to be used by plugins | |||
* | |||
* @returns {self} | |||
* @public | |||
*/ | |||
triggerHandler: function() { | |||
if (!this.__destroyed) { | |||
this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments)); | |||
} | |||
else { | |||
this.__destroyError(); | |||
} | |||
return this; | |||
} | |||
}; | |||
$.fn.tooltipster = function() { | |||
// for using in closures | |||
var args = Array.prototype.slice.apply(arguments), | |||
// common mistake: an HTML element can't be in several tooltips at the same time | |||
contentCloningWarning = 'You are using a single HTML element as content for several tooltips. You probably want to set the contentCloning option to TRUE.'; | |||
// this happens with $(sel).tooltipster(...) when $(sel) does not match anything | |||
if (this.length === 0) { | |||
// still chainable | |||
return this; | |||
} | |||
// this happens when calling $(sel).tooltipster('methodName or options') | |||
// where $(sel) matches one or more elements | |||
else { | |||
// method calls | |||
if (typeof args[0] === 'string') { | |||
var v = '#*$~&'; | |||
this.each(function() { | |||
// retrieve the namepaces of the tooltip(s) that exist on that element. | |||
// We will interact with the first tooltip only. | |||
var ns = $(this).data('tooltipster-ns'), | |||
// self represents the instance of the first tooltipster plugin | |||
// associated to the current HTML object of the loop | |||
self = ns ? $(this).data(ns[0]) : null; | |||
// if the current element holds a tooltipster instance | |||
if (self) { | |||
if (typeof self[args[0]] === 'function') { | |||
if ( this.length > 1 | |||
&& args[0] == 'content' | |||
&& ( args[1] instanceof $ | |||
|| (typeof args[1] == 'object' && args[1] != null && args[1].tagName) | |||
) | |||
&& !self.__options.contentCloning | |||
&& self.__options.debug | |||
) { | |||
console.log(contentCloningWarning); | |||
} | |||
// note : args[1] and args[2] may not be defined | |||
var resp = self[args[0]](args[1], args[2]); | |||
} | |||
else { | |||
throw new Error('Unknown method "'+ args[0] +'"'); | |||
} | |||
// if the function returned anything other than the instance | |||
// itself (which implies chaining, except for the `instance` method) | |||
if (resp !== self || args[0] === 'instance') { | |||
v = resp; | |||
// return false to stop .each iteration on the first element | |||
// matched by the selector | |||
return false; | |||
} | |||
} | |||
else { | |||
throw new Error('You called Tooltipster\'s "'+ args[0] +'" method on an uninitialized element'); | |||
} | |||
}); | |||
return (v !== '#*$~&') ? v : this; | |||
} | |||
// first argument is undefined or an object: the tooltip is initializing | |||
else { | |||
// reset the array of last initialized objects | |||
$.tooltipster.__instancesLatestArr = []; | |||
// is there a defined value for the multiple option in the options object ? | |||
var multipleIsSet = args[0] && args[0].multiple !== undefined, | |||
// if the multiple option is set to true, or if it's not defined but | |||
// set to true in the defaults | |||
multiple = (multipleIsSet && args[0].multiple) || (!multipleIsSet && defaults.multiple), | |||
// same for content | |||
contentIsSet = args[0] && args[0].content !== undefined, | |||
content = (contentIsSet && args[0].content) || (!contentIsSet && defaults.content), | |||
// same for contentCloning | |||
contentCloningIsSet = args[0] && args[0].contentCloning !== undefined, | |||
contentCloning = | |||
(contentCloningIsSet && args[0].contentCloning) | |||
|| (!contentCloningIsSet && defaults.contentCloning), | |||
// same for debug | |||
debugIsSet = args[0] && args[0].debug !== undefined, | |||
debug = (debugIsSet && args[0].debug) || (!debugIsSet && defaults.debug); | |||
if ( this.length > 1 | |||
&& ( content instanceof $ | |||
|| (typeof content == 'object' && content != null && content.tagName) | |||
) | |||
&& !contentCloning | |||
&& debug | |||
) { | |||
console.log(contentCloningWarning); | |||
} | |||
// create a tooltipster instance for each element if it doesn't | |||
// already have one or if the multiple option is set, and attach the | |||
// object to it | |||
this.each(function() { | |||
var go = false, | |||
$this = $(this), | |||
ns = $this.data('tooltipster-ns'), | |||
obj = null; | |||
if (!ns) { | |||
go = true; | |||
} | |||
else if (multiple) { | |||
go = true; | |||
} | |||
else if (debug) { | |||
console.log('Tooltipster: one or more tooltips are already attached to the element below. Ignoring.'); | |||
console.log(this); | |||
} | |||
if (go) { | |||
obj = new $.Tooltipster(this, args[0]); | |||
// save the reference of the new instance | |||
if (!ns) ns = []; | |||
ns.push(obj.__namespace); | |||
$this.data('tooltipster-ns', ns); | |||
// save the instance itself | |||
$this.data(obj.__namespace, obj); | |||
// call our constructor custom function. | |||
// we do this here and not in ::init() because we wanted | |||
// the object to be saved in $this.data before triggering | |||
// it | |||
if (obj.__options.functionInit) { | |||
obj.__options.functionInit.call(obj, obj, { | |||
origin: this | |||
}); | |||
} | |||
// and now the event, for the plugins and core emitter | |||
obj._trigger('init'); | |||
} | |||
$.tooltipster.__instancesLatestArr.push(obj); | |||
}); | |||
return this; | |||
} | |||
} | |||
}; | |||
// Utilities | |||
r1 | |||
/** | |||
r4026 | * A class to check if a tooltip can fit in given dimensions | ||
* | |||
* @param {object} $tooltip The jQuery wrapped tooltip element, or a clone of it | |||
*/ | |||
function Ruler($tooltip) { | |||
// list of instance variables | |||
this.$container; | |||
this.constraints = null; | |||
this.__$tooltip; | |||
this.__init($tooltip); | |||
} | |||
Ruler.prototype = { | |||
/** | |||
* Move the tooltip into an invisible div that does not allow overflow to make | |||
* size tests. Note: the tooltip may or may not be attached to the DOM at the | |||
* moment this method is called, it does not matter. | |||
* | |||
* @param {object} $tooltip The object to test. May be just a clone of the | |||
* actual tooltip. | |||
* @private | |||
*/ | |||
__init: function($tooltip) { | |||
this.__$tooltip = $tooltip; | |||
this.__$tooltip | |||
.css({ | |||
// for some reason we have to specify top and left 0 | |||
left: 0, | |||
// any overflow will be ignored while measuring | |||
overflow: 'hidden', | |||
// positions at (0,0) without the div using 100% of the available width | |||
position: 'absolute', | |||
top: 0 | |||
}) | |||
// overflow must be auto during the test. We re-set this in case | |||
// it were modified by the user | |||
.find('.tooltipster-content') | |||
.css('overflow', 'auto'); | |||
this.$container = $('<div class="tooltipster-ruler"></div>') | |||
.append(this.__$tooltip) | |||
.appendTo(env.window.document.body); | |||
}, | |||
/** | |||
* Force the browser to redraw (re-render) the tooltip immediately. This is required | |||
* when you changed some CSS properties and need to make something with it | |||
* immediately, without waiting for the browser to redraw at the end of instructions. | |||
* | |||
* @see http://stackoverflow.com/questions/3485365/how-can-i-force-webkit-to-redraw-repaint-to-propagate-style-changes | |||
* @private | |||
*/ | |||
__forceRedraw: function() { | |||
// note: this would work but for Webkit only | |||
//this.__$tooltip.close(); | |||
//this.__$tooltip[0].offsetHeight; | |||
//this.__$tooltip.open(); | |||
// works in FF too | |||
var $p = this.__$tooltip.parent(); | |||
this.__$tooltip.detach(); | |||
this.__$tooltip.appendTo($p); | |||
}, | |||
/** | |||
* Set maximum dimensions for the tooltip. A call to ::measure afterwards | |||
* will tell us if the content overflows or if it's ok | |||
* | |||
* @param {int} width | |||
* @param {int} height | |||
* @return {Ruler} | |||
* @public | |||
*/ | |||
constrain: function(width, height) { | |||
this.constraints = { | |||
width: width, | |||
height: height | |||
}; | |||
this.__$tooltip.css({ | |||
// we disable display:flex, otherwise the content would overflow without | |||
// creating horizontal scrolling (which we need to detect). | |||
display: 'block', | |||
// reset any previous height | |||
height: '', | |||
// we'll check if horizontal scrolling occurs | |||
overflow: 'auto', | |||
// we'll set the width and see what height is generated and if there | |||
// is horizontal overflow | |||
width: width | |||
}); | |||
return this; | |||
}, | |||
/** | |||
* Reset the tooltip content overflow and remove the test container | |||
* | |||
* @returns {Ruler} | |||
* @public | |||
*/ | |||
destroy: function() { | |||
// in case the element was not a clone | |||
this.__$tooltip | |||
.detach() | |||
.find('.tooltipster-content') | |||
.css({ | |||
// reset to CSS value | |||
display: '', | |||
overflow: '' | |||
}); | |||
this.$container.remove(); | |||
}, | |||
/** | |||
* Removes any constraints | |||
* | |||
* @returns {Ruler} | |||
* @public | |||
*/ | |||
free: function() { | |||
this.constraints = null; | |||
// reset to natural size | |||
this.__$tooltip.css({ | |||
display: '', | |||
height: '', | |||
overflow: 'visible', | |||
width: '' | |||
}); | |||
return this; | |||
}, | |||
/** | |||
* Returns the size of the tooltip. When constraints are applied, also returns | |||
* whether the tooltip fits in the provided dimensions. | |||
* The idea is to see if the new height is small enough and if the content does | |||
* not overflow horizontally. | |||
* | |||
* @param {int} width | |||
* @param {int} height | |||
* @returns {object} An object with a bool `fits` property and a `size` property | |||
* @public | |||
*/ | |||
measure: function() { | |||
this.__forceRedraw(); | |||
var tooltipBcr = this.__$tooltip[0].getBoundingClientRect(), | |||
result = { size: { | |||
// bcr.width/height are not defined in IE8- but in this | |||
// case, bcr.right/bottom will have the same value | |||
// except in iOS 8+ where tooltipBcr.bottom/right are wrong | |||
// after scrolling for reasons yet to be determined. | |||
// tooltipBcr.top/left might not be 0, see issue #514 | |||
height: tooltipBcr.height || (tooltipBcr.bottom - tooltipBcr.top), | |||
width: tooltipBcr.width || (tooltipBcr.right - tooltipBcr.left) | |||
}}; | |||
if (this.constraints) { | |||
// note: we used to use offsetWidth instead of boundingRectClient but | |||
// it returned rounded values, causing issues with sub-pixel layouts. | |||
// note2: noticed that the bcrWidth of text content of a div was once | |||
// greater than the bcrWidth of its container by 1px, causing the final | |||
// tooltip box to be too small for its content. However, evaluating | |||
// their widths one against the other (below) surprisingly returned | |||
// equality. Happened only once in Chrome 48, was not able to reproduce | |||
// => just having fun with float position values... | |||
var $content = this.__$tooltip.find('.tooltipster-content'), | |||
height = this.__$tooltip.outerHeight(), | |||
contentBcr = $content[0].getBoundingClientRect(), | |||
fits = { | |||
height: height <= this.constraints.height, | |||
width: ( | |||
// this condition accounts for min-width property that | |||
// may apply | |||
tooltipBcr.width <= this.constraints.width | |||
// the -1 is here because scrollWidth actually returns | |||
// a rounded value, and may be greater than bcr.width if | |||
// it was rounded up. This may cause an issue for contents | |||
// which actually really overflow by 1px or so, but that | |||
// should be rare. Not sure how to solve this efficiently. | |||
// See http://blogs.msdn.com/b/ie/archive/2012/02/17/sub-pixel-rendering-and-the-css-object-model.aspx | |||
&& contentBcr.width >= $content[0].scrollWidth - 1 | |||
) | |||
}; | |||
result.fits = fits.height && fits.width; | |||
} | |||
// old versions of IE get the width wrong for some reason and it causes | |||
// the text to be broken to a new line, so we round it up. If the width | |||
// is the width of the screen though, we can assume it is accurate. | |||
if ( env.IE | |||
&& env.IE <= 11 | |||
&& result.size.width !== env.window.document.documentElement.clientWidth | |||
) { | |||
result.size.width = Math.ceil(result.size.width) + 1; | |||
} | |||
return result; | |||
} | |||
}; | |||
// quick & dirty compare function, not bijective nor multidimensional | |||
function areEqual(a,b) { | |||
var same = true; | |||
$.each(a, function(i, _) { | |||
if (b[i] === undefined || a[i] !== b[i]) { | |||
same = false; | |||
return false; | |||
} | |||
}); | |||
return same; | |||
} | |||
/** | |||
* A fast function to check if an element is still in the DOM. It | |||
* tries to use an id as ids are indexed by the browser, or falls | |||
* back to jQuery's `contains` method. May fail if two elements | |||
* have the same id, but so be it | |||
* | |||
* @param {object} $obj A jQuery-wrapped HTML element | |||
* @return {boolean} | |||
r1 | */ | ||
r4026 | function bodyContains($obj) { | ||
var id = $obj.attr('id'), | |||
el = id ? env.window.document.getElementById(id) : null; | |||
// must also check that the element with the id is the one we want | |||
return el ? el === $obj[0] : $.contains(env.window.document.body, $obj[0]); | |||
} | |||
// detect IE versions for dirty fixes | |||
var uA = navigator.userAgent.toLowerCase(); | |||
if (uA.indexOf('msie') != -1) env.IE = parseInt(uA.split('msie')[1]); | |||
else if (uA.toLowerCase().indexOf('trident') !== -1 && uA.indexOf(' rv:11') !== -1) env.IE = 11; | |||
else if (uA.toLowerCase().indexOf('edge/') != -1) env.IE = parseInt(uA.toLowerCase().split('edge/')[1]); | |||
// detecting support for CSS transitions | |||
function transitionSupport() { | |||
// env.window is not defined yet when this is called | |||
if (!win) return false; | |||
var b = win.document.body || win.document.documentElement, | |||
s = b.style, | |||
p = 'transition', | |||
v = ['Moz', 'Webkit', 'Khtml', 'O', 'ms']; | |||
if (typeof s[p] == 'string') { return true; } | |||
p = p.charAt(0).toUpperCase() + p.substr(1); | |||
for (var i=0; i<v.length; i++) { | |||
if (typeof s[v[i] + p] == 'string') { return true; } | |||
} | |||
return false; | |||
r385 | } | ||
r4026 | |||
// we'll return jQuery for plugins not to have to declare it as a dependency, | |||
// but it's done by a build task since it should be included only once at the | |||
// end when we concatenate the main file with a plugin | |||
// sideTip is Tooltipster's default plugin. | |||
// This file will be UMDified by a build task. | |||
var pluginName = 'tooltipster.sideTip'; | |||
$.tooltipster._plugin({ | |||
name: pluginName, | |||
instance: { | |||
/** | |||
* Defaults are provided as a function for an easy override by inheritance | |||
* | |||
* @return {object} An object with the defaults options | |||
* @private | |||
*/ | |||
__defaults: function() { | |||
return { | |||
// if the tooltip should display an arrow that points to the origin | |||
arrow: true, | |||
// the distance in pixels between the tooltip and the origin | |||
distance: 6, | |||
// allows to easily change the position of the tooltip | |||
functionPosition: null, | |||
maxWidth: null, | |||
// used to accomodate the arrow of tooltip if there is one. | |||
// First to make sure that the arrow target is not too close | |||
// to the edge of the tooltip, so the arrow does not overflow | |||
// the tooltip. Secondly when we reposition the tooltip to | |||
// make sure that it's positioned in such a way that the arrow is | |||
// still pointing at the target (and not a few pixels beyond it). | |||
// It should be equal to or greater than half the width of | |||
// the arrow (by width we mean the size of the side which touches | |||
// the side of the tooltip). | |||
minIntersection: 16, | |||
minWidth: 0, | |||
// deprecated in 4.0.0. Listed for _optionsExtract to pick it up | |||
position: null, | |||
side: 'top', | |||
// set to false to position the tooltip relatively to the document rather | |||
// than the window when we open it | |||
viewportAware: true | |||
}; | |||
}, | |||
/** | |||
* Run once: at instantiation of the plugin | |||
* | |||
* @param {object} instance The tooltipster object that instantiated this plugin | |||
* @private | |||
*/ | |||
__init: function(instance) { | |||
var self = this; | |||
// list of instance variables | |||
self.__instance = instance; | |||
self.__namespace = 'tooltipster-sideTip-'+ Math.round(Math.random()*1000000); | |||
self.__previousState = 'closed'; | |||
self.__options; | |||
// initial formatting | |||
self.__optionsFormat(); | |||
self.__instance._on('state.'+ self.__namespace, function(event) { | |||
if (event.state == 'closed') { | |||
self.__close(); | |||
} | |||
else if (event.state == 'appearing' && self.__previousState == 'closed') { | |||
self.__create(); | |||
} | |||
self.__previousState = event.state; | |||
}); | |||
// reformat every time the options are changed | |||
self.__instance._on('options.'+ self.__namespace, function() { | |||
self.__optionsFormat(); | |||
}); | |||
self.__instance._on('reposition.'+ self.__namespace, function(e) { | |||
self.__reposition(e.event, e.helper); | |||
}); | |||
}, | |||
/** | |||
* Called when the tooltip has closed | |||
* | |||
* @private | |||
*/ | |||
__close: function() { | |||
// detach our content object first, so the next jQuery's remove() | |||
// call does not unbind its event handlers | |||
if (this.__instance.content() instanceof $) { | |||
this.__instance.content().detach(); | |||
} | |||
// remove the tooltip from the DOM | |||
this.__instance._$tooltip.remove(); | |||
this.__instance._$tooltip = null; | |||
}, | |||
/** | |||
* Creates the HTML element of the tooltip. | |||
* | |||
* @private | |||
*/ | |||
__create: function() { | |||
// note: we wrap with a .tooltipster-box div to be able to set a margin on it | |||
// (.tooltipster-base must not have one) | |||
var $html = $( | |||
'<div class="tooltipster-base tooltipster-sidetip">' + | |||
'<div class="tooltipster-box">' + | |||
'<div class="tooltipster-content"></div>' + | |||
'</div>' + | |||
'<div class="tooltipster-arrow">' + | |||
'<div class="tooltipster-arrow-uncropped">' + | |||
'<div class="tooltipster-arrow-border"></div>' + | |||
'<div class="tooltipster-arrow-background"></div>' + | |||
'</div>' + | |||
'</div>' + | |||
'</div>' | |||
); | |||
// hide arrow if asked | |||
if (!this.__options.arrow) { | |||
$html | |||
.find('.tooltipster-box') | |||
.css('margin', 0) | |||
.end() | |||
.find('.tooltipster-arrow') | |||
.hide(); | |||
} | |||
// apply min/max width if asked | |||
if (this.__options.minWidth) { | |||
$html.css('min-width', this.__options.minWidth + 'px'); | |||
} | |||
if (this.__options.maxWidth) { | |||
$html.css('max-width', this.__options.maxWidth + 'px'); | |||
} | |||
this.__instance._$tooltip = $html; | |||
// tell the instance that the tooltip element has been created | |||
this.__instance._trigger('created'); | |||
}, | |||
/** | |||
* Used when the plugin is to be unplugged | |||
* | |||
* @private | |||
*/ | |||
__destroy: function() { | |||
this.__instance._off('.'+ self.__namespace); | |||
}, | |||
/** | |||
* (Re)compute this.__options from the options declared to the instance | |||
* | |||
* @private | |||
*/ | |||
__optionsFormat: function() { | |||
var self = this; | |||
// get the options | |||
self.__options = self.__instance._optionsExtract(pluginName, self.__defaults()); | |||
// for backward compatibility, deprecated in v4.0.0 | |||
if (self.__options.position) { | |||
self.__options.side = self.__options.position; | |||
} | |||
// options formatting | |||
// format distance as a four-cell array if it ain't one yet and then make | |||
// it an object with top/bottom/left/right properties | |||
if (typeof self.__options.distance != 'object') { | |||
self.__options.distance = [self.__options.distance]; | |||
} | |||
if (self.__options.distance.length < 4) { | |||
if (self.__options.distance[1] === undefined) self.__options.distance[1] = self.__options.distance[0]; | |||
if (self.__options.distance[2] === undefined) self.__options.distance[2] = self.__options.distance[0]; | |||
if (self.__options.distance[3] === undefined) self.__options.distance[3] = self.__options.distance[1]; | |||
self.__options.distance = { | |||
top: self.__options.distance[0], | |||
right: self.__options.distance[1], | |||
bottom: self.__options.distance[2], | |||
left: self.__options.distance[3] | |||
}; | |||
} | |||
// let's transform: | |||
// 'top' into ['top', 'bottom', 'right', 'left'] | |||
// 'right' into ['right', 'left', 'top', 'bottom'] | |||
// 'bottom' into ['bottom', 'top', 'right', 'left'] | |||
// 'left' into ['left', 'right', 'top', 'bottom'] | |||
if (typeof self.__options.side == 'string') { | |||
var opposites = { | |||
'top': 'bottom', | |||
'right': 'left', | |||
'bottom': 'top', | |||
'left': 'right' | |||
}; | |||
self.__options.side = [self.__options.side, opposites[self.__options.side]]; | |||
if (self.__options.side[0] == 'left' || self.__options.side[0] == 'right') { | |||
self.__options.side.push('top', 'bottom'); | |||
} | |||
else { | |||
self.__options.side.push('right', 'left'); | |||
} | |||
} | |||
// misc | |||
// disable the arrow in IE6 unless the arrow option was explicitly set to true | |||
if ( $.tooltipster._env.IE === 6 | |||
&& self.__options.arrow !== true | |||
) { | |||
self.__options.arrow = false; | |||
} | |||
}, | |||
/** | |||
* This method must compute and set the positioning properties of the | |||
* tooltip (left, top, width, height, etc.). It must also make sure the | |||
* tooltip is eventually appended to its parent (since the element may be | |||
* detached from the DOM at the moment the method is called). | |||
* | |||
* We'll evaluate positioning scenarios to find which side can contain the | |||
* tooltip in the best way. We'll consider things relatively to the window | |||
* (unless the user asks not to), then to the document (if need be, or if the | |||
* user explicitly requires the tests to run on the document). For each | |||
* scenario, measures are taken, allowing us to know how well the tooltip | |||
* is going to fit. After that, a sorting function will let us know what | |||
* the best scenario is (we also allow the user to choose his favorite | |||
* scenario by using an event). | |||
* | |||
* @param {object} helper An object that contains variables that plugin | |||
* creators may find useful (see below) | |||
* @param {object} helper.geo An object with many layout properties | |||
* about objects of interest (window, document, origin). This should help | |||
* plugin users compute the optimal position of the tooltip | |||
* @private | |||
*/ | |||
__reposition: function(event, helper) { | |||
var self = this, | |||
finalResult, | |||
// to know where to put the tooltip, we need to know on which point | |||
// of the x or y axis we should center it. That coordinate is the target | |||
targets = self.__targetFind(helper), | |||
testResults = []; | |||
// make sure the tooltip is detached while we make tests on a clone | |||
self.__instance._$tooltip.detach(); | |||
// we could actually provide the original element to the Ruler and | |||
// not a clone, but it just feels right to keep it out of the | |||
// machinery. | |||
var $clone = self.__instance._$tooltip.clone(), | |||
// start position tests session | |||
ruler = $.tooltipster._getRuler($clone), | |||
satisfied = false, | |||
animation = self.__instance.option('animation'); | |||
// an animation class could contain properties that distort the size | |||
if (animation) { | |||
$clone.removeClass('tooltipster-'+ animation); | |||
} | |||
// start evaluating scenarios | |||
$.each(['window', 'document'], function(i, container) { | |||
var takeTest = null; | |||
// let the user decide to keep on testing or not | |||
self.__instance._trigger({ | |||
container: container, | |||
helper: helper, | |||
satisfied: satisfied, | |||
takeTest: function(bool) { | |||
takeTest = bool; | |||
}, | |||
results: testResults, | |||
type: 'positionTest' | |||
}); | |||
if ( takeTest == true | |||
|| ( takeTest != false | |||
&& satisfied == false | |||
// skip the window scenarios if asked. If they are reintegrated by | |||
// the callback of the positionTest event, they will have to be | |||
// excluded using the callback of positionTested | |||
&& (container != 'window' || self.__options.viewportAware) | |||
) | |||
) { | |||
// for each allowed side | |||
for (var i=0; i < self.__options.side.length; i++) { | |||
var distance = { | |||
horizontal: 0, | |||
vertical: 0 | |||
}, | |||
side = self.__options.side[i]; | |||
if (side == 'top' || side == 'bottom') { | |||
distance.vertical = self.__options.distance[side]; | |||
} | |||
else { | |||
distance.horizontal = self.__options.distance[side]; | |||
} | |||
// this may have an effect on the size of the tooltip if there are css | |||
// rules for the arrow or something else | |||
self.__sideChange($clone, side); | |||
$.each(['natural', 'constrained'], function(i, mode) { | |||
takeTest = null; | |||
// emit an event on the instance | |||
self.__instance._trigger({ | |||
container: container, | |||
event: event, | |||
helper: helper, | |||
mode: mode, | |||
results: testResults, | |||
satisfied: satisfied, | |||
side: side, | |||
takeTest: function(bool) { | |||
takeTest = bool; | |||
}, | |||
type: 'positionTest' | |||
}); | |||
if ( takeTest == true | |||
|| ( takeTest != false | |||
&& satisfied == false | |||
) | |||
) { | |||
var testResult = { | |||
container: container, | |||
// we let the distance as an object here, it can make things a little easier | |||
// during the user's calculations at positionTest/positionTested | |||
distance: distance, | |||
// whether the tooltip can fit in the size of the viewport (does not mean | |||
// that we'll be able to make it initially entirely visible, see 'whole') | |||
fits: null, | |||
mode: mode, | |||
outerSize: null, | |||
side: side, | |||
size: null, | |||
target: targets[side], | |||
// check if the origin has enough surface on screen for the tooltip to | |||
// aim at it without overflowing the viewport (this is due to the thickness | |||
// of the arrow represented by the minIntersection length). | |||
// If not, the tooltip will have to be partly or entirely off screen in | |||
// order to stay docked to the origin. This value will stay null when the | |||
// container is the document, as it is not relevant | |||
whole: null | |||
}; | |||
// get the size of the tooltip with or without size constraints | |||
var rulerConfigured = (mode == 'natural') ? | |||
ruler.free() : | |||
ruler.constrain( | |||
helper.geo.available[container][side].width - distance.horizontal, | |||
helper.geo.available[container][side].height - distance.vertical | |||
), | |||
rulerResults = rulerConfigured.measure(); | |||
testResult.size = rulerResults.size; | |||
testResult.outerSize = { | |||
height: rulerResults.size.height + distance.vertical, | |||
width: rulerResults.size.width + distance.horizontal | |||
}; | |||
if (mode == 'natural') { | |||
if( helper.geo.available[container][side].width >= testResult.outerSize.width | |||
&& helper.geo.available[container][side].height >= testResult.outerSize.height | |||
) { | |||
testResult.fits = true; | |||
} | |||
else { | |||
testResult.fits = false; | |||
} | |||
} | |||
else { | |||
testResult.fits = rulerResults.fits; | |||
} | |||
if (container == 'window') { | |||
if (!testResult.fits) { | |||
testResult.whole = false; | |||
} | |||
else { | |||
if (side == 'top' || side == 'bottom') { | |||
testResult.whole = ( | |||
helper.geo.origin.windowOffset.right >= self.__options.minIntersection | |||
&& helper.geo.window.size.width - helper.geo.origin.windowOffset.left >= self.__options.minIntersection | |||
); | |||
} | |||
else { | |||
testResult.whole = ( | |||
helper.geo.origin.windowOffset.bottom >= self.__options.minIntersection | |||
&& helper.geo.window.size.height - helper.geo.origin.windowOffset.top >= self.__options.minIntersection | |||
); | |||
} | |||
} | |||
} | |||
testResults.push(testResult); | |||
// we don't need to compute more positions if we have one fully on screen | |||
if (testResult.whole) { | |||
satisfied = true; | |||
} | |||
else { | |||
// don't run the constrained test unless the natural width was greater | |||
// than the available width, otherwise it's pointless as we know it | |||
// wouldn't fit either | |||
if ( testResult.mode == 'natural' | |||
&& ( testResult.fits | |||
|| testResult.size.width <= helper.geo.available[container][side].width | |||
) | |||
) { | |||
return false; | |||
} | |||
} | |||
} | |||
}); | |||
} | |||
} | |||
}); | |||
// the user may eliminate the unwanted scenarios from testResults, but he's | |||
// not supposed to alter them at this point. functionPosition and the | |||
// position event serve that purpose. | |||
self.__instance._trigger({ | |||
edit: function(r) { | |||
testResults = r; | |||
}, | |||
event: event, | |||
helper: helper, | |||
results: testResults, | |||
type: 'positionTested' | |||
}); | |||
/** | |||
* Sort the scenarios to find the favorite one. | |||
* | |||
* The favorite scenario is when we can fully display the tooltip on screen, | |||
* even if it means that the middle of the tooltip is no longer centered on | |||
* the middle of the origin (when the origin is near the edge of the screen | |||
* or even partly off screen). We want the tooltip on the preferred side, | |||
* even if it means that we have to use a constrained size rather than a | |||
* natural one (as long as it fits). When the origin is off screen at the top | |||
* the tooltip will be positioned at the bottom (if allowed), if the origin | |||
* is off screen on the right, it will be positioned on the left, etc. | |||
* If there are no scenarios where the tooltip can fit on screen, or if the | |||
* user does not want the tooltip to fit on screen (viewportAware == false), | |||
* we fall back to the scenarios relative to the document. | |||
* | |||
* When the tooltip is bigger than the viewport in either dimension, we stop | |||
* looking at the window scenarios and consider the document scenarios only, | |||
* with the same logic to find on which side it would fit best. | |||
* | |||
* If the tooltip cannot fit the document on any side, we force it at the | |||
* bottom, so at least the user can scroll to see it. | |||
*/ | |||
testResults.sort(function(a, b) { | |||
// best if it's whole (the tooltip fits and adapts to the viewport) | |||
if (a.whole && !b.whole) { | |||
return -1; | |||
} | |||
else if (!a.whole && b.whole) { | |||
return 1; | |||
} | |||
else if (a.whole && b.whole) { | |||
var ai = self.__options.side.indexOf(a.side), | |||
bi = self.__options.side.indexOf(b.side); | |||
// use the user's sides fallback array | |||
if (ai < bi) { | |||
return -1; | |||
} | |||
else if (ai > bi) { | |||
return 1; | |||
} | |||
else { | |||
// will be used if the user forced the tests to continue | |||
return a.mode == 'natural' ? -1 : 1; | |||
} | |||
} | |||
else { | |||
// better if it fits | |||
if (a.fits && !b.fits) { | |||
return -1; | |||
} | |||
else if (!a.fits && b.fits) { | |||
return 1; | |||
} | |||
else if (a.fits && b.fits) { | |||
var ai = self.__options.side.indexOf(a.side), | |||
bi = self.__options.side.indexOf(b.side); | |||
// use the user's sides fallback array | |||
if (ai < bi) { | |||
return -1; | |||
} | |||
else if (ai > bi) { | |||
return 1; | |||
} | |||
else { | |||
// will be used if the user forced the tests to continue | |||
return a.mode == 'natural' ? -1 : 1; | |||
} | |||
} | |||
else { | |||
// if everything failed, this will give a preference to the case where | |||
// the tooltip overflows the document at the bottom | |||
if ( a.container == 'document' | |||
&& a.side == 'bottom' | |||
&& a.mode == 'natural' | |||
) { | |||
return -1; | |||
} | |||
else { | |||
return 1; | |||
} | |||
} | |||
} | |||
}); | |||
finalResult = testResults[0]; | |||
// now let's find the coordinates of the tooltip relatively to the window | |||
finalResult.coord = {}; | |||
switch (finalResult.side) { | |||
case 'left': | |||
case 'right': | |||
finalResult.coord.top = Math.floor(finalResult.target - finalResult.size.height / 2); | |||
break; | |||
case 'bottom': | |||
case 'top': | |||
finalResult.coord.left = Math.floor(finalResult.target - finalResult.size.width / 2); | |||
break; | |||
} | |||
switch (finalResult.side) { | |||
case 'left': | |||
finalResult.coord.left = helper.geo.origin.windowOffset.left - finalResult.outerSize.width; | |||
break; | |||
case 'right': | |||
finalResult.coord.left = helper.geo.origin.windowOffset.right + finalResult.distance.horizontal; | |||
break; | |||
case 'top': | |||
finalResult.coord.top = helper.geo.origin.windowOffset.top - finalResult.outerSize.height; | |||
break; | |||
case 'bottom': | |||
finalResult.coord.top = helper.geo.origin.windowOffset.bottom + finalResult.distance.vertical; | |||
break; | |||
} | |||
// if the tooltip can potentially be contained within the viewport dimensions | |||
// and that we are asked to make it fit on screen | |||
if (finalResult.container == 'window') { | |||
// if the tooltip overflows the viewport, we'll move it accordingly (then it will | |||
// not be centered on the middle of the origin anymore). We only move horizontally | |||
// for top and bottom tooltips and vice versa. | |||
if (finalResult.side == 'top' || finalResult.side == 'bottom') { | |||
// if there is an overflow on the left | |||
if (finalResult.coord.left < 0) { | |||
// prevent the overflow unless the origin itself gets off screen (minus the | |||
// margin needed to keep the arrow pointing at the target) | |||
if (helper.geo.origin.windowOffset.right - this.__options.minIntersection >= 0) { | |||
finalResult.coord.left = 0; | |||
} | |||
else { | |||
finalResult.coord.left = helper.geo.origin.windowOffset.right - this.__options.minIntersection - 1; | |||
} | |||
} | |||
// or an overflow on the right | |||
else if (finalResult.coord.left > helper.geo.window.size.width - finalResult.size.width) { | |||
if (helper.geo.origin.windowOffset.left + this.__options.minIntersection <= helper.geo.window.size.width) { | |||
finalResult.coord.left = helper.geo.window.size.width - finalResult.size.width; | |||
} | |||
else { | |||
finalResult.coord.left = helper.geo.origin.windowOffset.left + this.__options.minIntersection + 1 - finalResult.size.width; | |||
} | |||
} | |||
} | |||
else { | |||
// overflow at the top | |||
if (finalResult.coord.top < 0) { | |||
if (helper.geo.origin.windowOffset.bottom - this.__options.minIntersection >= 0) { | |||
finalResult.coord.top = 0; | |||
} | |||
else { | |||
finalResult.coord.top = helper.geo.origin.windowOffset.bottom - this.__options.minIntersection - 1; | |||
} | |||
} | |||
// or at the bottom | |||
else if (finalResult.coord.top > helper.geo.window.size.height - finalResult.size.height) { | |||
if (helper.geo.origin.windowOffset.top + this.__options.minIntersection <= helper.geo.window.size.height) { | |||
finalResult.coord.top = helper.geo.window.size.height - finalResult.size.height; | |||
} | |||
else { | |||
finalResult.coord.top = helper.geo.origin.windowOffset.top + this.__options.minIntersection + 1 - finalResult.size.height; | |||
} | |||
} | |||
} | |||
} | |||
else { | |||
// there might be overflow here too but it's easier to handle. If there has | |||
// to be an overflow, we'll make sure it's on the right side of the screen | |||
// (because the browser will extend the document size if there is an overflow | |||
// on the right, but not on the left). The sort function above has already | |||
// made sure that a bottom document overflow is preferred to a top overflow, | |||
// so we don't have to care about it. | |||
// if there is an overflow on the right | |||
if (finalResult.coord.left > helper.geo.window.size.width - finalResult.size.width) { | |||
// this may actually create on overflow on the left but we'll fix it in a sec | |||
finalResult.coord.left = helper.geo.window.size.width - finalResult.size.width; | |||
} | |||
// if there is an overflow on the left | |||
if (finalResult.coord.left < 0) { | |||
// don't care if it overflows the right after that, we made our best | |||
finalResult.coord.left = 0; | |||
} | |||
} | |||
// submit the positioning proposal to the user function which may choose to change | |||
// the side, size and/or the coordinates | |||
// first, set the rules that corresponds to the proposed side: it may change | |||
// the size of the tooltip, and the custom functionPosition may want to detect the | |||
// size of something before making a decision. So let's make things easier for the | |||
// implementor | |||
self.__sideChange($clone, finalResult.side); | |||
// add some variables to the helper | |||
helper.tooltipClone = $clone[0]; | |||
helper.tooltipParent = self.__instance.option('parent').parent[0]; | |||
// move informative values to the helper | |||
helper.mode = finalResult.mode; | |||
helper.whole = finalResult.whole; | |||
// add some variables to the helper for the functionPosition callback (these | |||
// will also be added to the event fired by self.__instance._trigger but that's | |||
// ok, we're just being consistent) | |||
helper.origin = self.__instance._$origin[0]; | |||
helper.tooltip = self.__instance._$tooltip[0]; | |||
// leave only the actionable values in there for functionPosition | |||
delete finalResult.container; | |||
delete finalResult.fits; | |||
delete finalResult.mode; | |||
delete finalResult.outerSize; | |||
delete finalResult.whole; | |||
// keep only the distance on the relevant side, for clarity | |||
finalResult.distance = finalResult.distance.horizontal || finalResult.distance.vertical; | |||
// beginners may not be comfortable with the concept of editing the object | |||
// passed by reference, so we provide an edit function and pass a clone | |||
var finalResultClone = $.extend(true, {}, finalResult); | |||
// emit an event on the instance | |||
self.__instance._trigger({ | |||
edit: function(result) { | |||
finalResult = result; | |||
}, | |||
event: event, | |||
helper: helper, | |||
position: finalResultClone, | |||
type: 'position' | |||
}); | |||
if (self.__options.functionPosition) { | |||
var result = self.__options.functionPosition.call(self, self.__instance, helper, finalResultClone); | |||
if (result) finalResult = result; | |||
} | |||
// end the positioning tests session (the user might have had a | |||
// use for it during the position event, now it's over) | |||
ruler.destroy(); | |||
// compute the position of the target relatively to the tooltip root | |||
// element so we can place the arrow and make the needed adjustments | |||
var arrowCoord, | |||
maxVal; | |||
if (finalResult.side == 'top' || finalResult.side == 'bottom') { | |||
arrowCoord = { | |||
prop: 'left', | |||
val: finalResult.target - finalResult.coord.left | |||
}; | |||
maxVal = finalResult.size.width - this.__options.minIntersection; | |||
} | |||
else { | |||
arrowCoord = { | |||
prop: 'top', | |||
val: finalResult.target - finalResult.coord.top | |||
}; | |||
maxVal = finalResult.size.height - this.__options.minIntersection; | |||
} | |||
// cannot lie beyond the boundaries of the tooltip, minus the | |||
// arrow margin | |||
if (arrowCoord.val < this.__options.minIntersection) { | |||
arrowCoord.val = this.__options.minIntersection; | |||
} | |||
else if (arrowCoord.val > maxVal) { | |||
arrowCoord.val = maxVal; | |||
} | |||
var originParentOffset; | |||
// let's convert the window-relative coordinates into coordinates relative to the | |||
// future positioned parent that the tooltip will be appended to | |||
if (helper.geo.origin.fixedLineage) { | |||
// same as windowOffset when the position is fixed | |||
originParentOffset = helper.geo.origin.windowOffset; | |||
} | |||
else { | |||
// this assumes that the parent of the tooltip is located at | |||
// (0, 0) in the document, typically like when the parent is | |||
// <body>. | |||
// If we ever allow other types of parent, .tooltipster-ruler | |||
// will have to be appended to the parent to inherit css style | |||
// values that affect the display of the text and such. | |||
originParentOffset = { | |||
left: helper.geo.origin.windowOffset.left + helper.geo.window.scroll.left, | |||
top: helper.geo.origin.windowOffset.top + helper.geo.window.scroll.top | |||
}; | |||
} | |||
finalResult.coord = { | |||
left: originParentOffset.left + (finalResult.coord.left - helper.geo.origin.windowOffset.left), | |||
top: originParentOffset.top + (finalResult.coord.top - helper.geo.origin.windowOffset.top) | |||
}; | |||
// set position values on the original tooltip element | |||
self.__sideChange(self.__instance._$tooltip, finalResult.side); | |||
if (helper.geo.origin.fixedLineage) { | |||
self.__instance._$tooltip | |||
.css('position', 'fixed'); | |||
} | |||
else { | |||
// CSS default | |||
self.__instance._$tooltip | |||
.css('position', ''); | |||
} | |||
self.__instance._$tooltip | |||
.css({ | |||
left: finalResult.coord.left, | |||
top: finalResult.coord.top, | |||
// we need to set a size even if the tooltip is in its natural size | |||
// because when the tooltip is positioned beyond the width of the body | |||
// (which is by default the width of the window; it will happen when | |||
// you scroll the window horizontally to get to the origin), its text | |||
// content will otherwise break lines at each word to keep up with the | |||
// body overflow strategy. | |||
height: finalResult.size.height, | |||
width: finalResult.size.width | |||
}) | |||
.find('.tooltipster-arrow') | |||
.css({ | |||
'left': '', | |||
'top': '' | |||
}) | |||
.css(arrowCoord.prop, arrowCoord.val); | |||
// append the tooltip HTML element to its parent | |||
self.__instance._$tooltip.appendTo(self.__instance.option('parent')); | |||
self.__instance._trigger({ | |||
type: 'repositioned', | |||
event: event, | |||
position: finalResult | |||
}); | |||
}, | |||
/** | |||
* Make whatever modifications are needed when the side is changed. This has | |||
* been made an independant method for easy inheritance in custom plugins based | |||
* on this default plugin. | |||
* | |||
* @param {object} $obj | |||
* @param {string} side | |||
* @private | |||
*/ | |||
__sideChange: function($obj, side) { | |||
$obj | |||
.removeClass('tooltipster-bottom') | |||
.removeClass('tooltipster-left') | |||
.removeClass('tooltipster-right') | |||
.removeClass('tooltipster-top') | |||
.addClass('tooltipster-'+ side); | |||
}, | |||
/** | |||
* Returns the target that the tooltip should aim at for a given side. | |||
* The calculated value is a distance from the edge of the window | |||
* (left edge for top/bottom sides, top edge for left/right side). The | |||
* tooltip will be centered on that position and the arrow will be | |||
* positioned there (as much as possible). | |||
* | |||
* @param {object} helper | |||
* @return {integer} | |||
* @private | |||
*/ | |||
__targetFind: function(helper) { | |||
var target = {}, | |||
rects = this.__instance._$origin[0].getClientRects(); | |||
// these lines fix a Chrome bug (issue #491) | |||
if (rects.length > 1) { | |||
var opacity = this.__instance._$origin.css('opacity'); | |||
if(opacity == 1) { | |||
this.__instance._$origin.css('opacity', 0.99); | |||
rects = this.__instance._$origin[0].getClientRects(); | |||
this.__instance._$origin.css('opacity', 1); | |||
} | |||
} | |||
// by default, the target will be the middle of the origin | |||
if (rects.length < 2) { | |||
target.top = Math.floor(helper.geo.origin.windowOffset.left + (helper.geo.origin.size.width / 2)); | |||
target.bottom = target.top; | |||
target.left = Math.floor(helper.geo.origin.windowOffset.top + (helper.geo.origin.size.height / 2)); | |||
target.right = target.left; | |||
} | |||
// if multiple client rects exist, the element may be text split | |||
// up into multiple lines and the middle of the origin may not be | |||
// best option anymore. We need to choose the best target client rect | |||
else { | |||
// top: the first | |||
var targetRect = rects[0]; | |||
target.top = Math.floor(targetRect.left + (targetRect.right - targetRect.left) / 2); | |||
// right: the middle line, rounded down in case there is an even | |||
// number of lines (looks more centered => check out the | |||
// demo with 4 split lines) | |||
if (rects.length > 2) { | |||
targetRect = rects[Math.ceil(rects.length / 2) - 1]; | |||
} | |||
else { | |||
targetRect = rects[0]; | |||
} | |||
target.right = Math.floor(targetRect.top + (targetRect.bottom - targetRect.top) / 2); | |||
// bottom: the last | |||
targetRect = rects[rects.length - 1]; | |||
target.bottom = Math.floor(targetRect.left + (targetRect.right - targetRect.left) / 2); | |||
// left: the middle line, rounded up | |||
if (rects.length > 2) { | |||
targetRect = rects[Math.ceil((rects.length + 1) / 2) - 1]; | |||
} | |||
else { | |||
targetRect = rects[rects.length - 1]; | |||
} | |||
target.left = Math.floor(targetRect.top + (targetRect.bottom - targetRect.top) / 2); | |||
} | |||
return target; | |||
} | |||
} | |||
}); | |||
/* a build task will add "return $;" here */ | |||
return $; | |||
})); | |||
// | |||
// $(document).ready(function() { | |||
// $('.tooltip-hovercard').tooltipster({ | |||
// theme: 'tooltipster-shadow', | |||
// animation: 'fade', | |||
// delay: 100, | |||
// contentCloning: true, | |||
// | |||
// }); |