'
);
// 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
// .
// 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 $;
}));