// tipsy, facebook style tooltips for jquery
// version 1.0.0a
// (c) 2008-2010 jason frame [jason@onehackoranother.com]
// released under the MIT license
// Modified by Justin Donaldson [donaldson@bigml.com]
// Added:
// 1) Correctly handle svg elements (title children and/or xlink:title attr)
// 2) Add some new dynamic gravity placement functions autoNWNE and autoSWSE
// 3) z-index override for tipsies on top of tipsies
// 4) Custom className argument that allows adding classes to the tipsy popup
// 5) Add custom cancelHide function argument that can override a tipsy hide
// behavior. This is useful when you wish to keep a parent tipsy open while
// its child is still active.
(function($) {
function maybeCall(thing, ctx) {
return (typeof thing == 'function') ? (thing.call(ctx)) : thing;
}
// CAUTION the current implementation does not allow for tipsied elements to stay out of DOM (in between events)
// i.e. don't remove, store, then re-insert tipsied elements (and why would you want to do that anyway?)
var garbageCollect = (function() {
var currentInterval;
var to = null;
var tipsies = [];
function _do() {
for (var i = 0; i < tipsies.length;) {
var t = tipsies[i];
// FIXME? the 2nd (non-paranoid) check is from the link below, it should be replaced if a better way is found
// http://stackoverflow.com/questions/4040715/check-if-cached-jquery-object-is-still-in-dom
if (t.options.gcInterval === 0 || t.$element.closest('body').length === 0) {
t.hoverState = 'out';
t.hide();
tipsies.splice(i,1);
} else {
i++;
}
}
}
function _loop() {
to = setTimeout(function() { _do(); _loop(); }, currentInterval);
}
return function(t) {
if (t.options.gcInterval === 0) return;
if (to && t.options.gcInterval < currentInterval) {
clearTimeout(to); to = null;
currentInterval = t.options.gcInterval;
}
tipsies.push(t);
if (!to) _loop();
};
})();
function Tipsy(element, options) {
this.$element = $(element);
this.options = options;
this.enabled = true;
this.fixTitle();
garbageCollect(this);
}
Tipsy.prototype = {
show: function() {
var title = this.getTitle();
if (title && this.enabled) {
var $tip = this.tip();
$tip.find('.tipsy-inner')[this.options.html ? 'html' : 'text'](title);
$tip[0].className = 'tipsy'; // reset classname in case of dynamic gravity
$tip.remove().css({top: 0, left: 0, visibility: 'hidden', display: 'block'}).prependTo(document.body);
var pos = $.extend({}, this.$element.offset(), {
width: this.$element[0].offsetWidth || 0,
height: this.$element[0].offsetHeight || 0
});
if (typeof this.$element[0].nearestViewportElement == 'object') {
// SVG
var el = this.$element[0];
var rect = el.getBoundingClientRect();
pos.width = rect.width;
pos.height = rect.height;
}
var actualWidth = $tip[0].offsetWidth,
actualHeight = $tip[0].offsetHeight,
gravity = maybeCall(this.options.gravity, this.$element[0]);
var tp;
switch (gravity.charAt(0)) {
case 'n':
tp = {top: pos.top + pos.height + this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2};
break;
case 's':
tp = {top: pos.top - actualHeight - this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2};
break;
case 'e':
tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth - this.options.offset};
break;
case 'w':
tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width + this.options.offset};
break;
}
if (gravity.length == 2) {
if (gravity.charAt(1) == 'w') {
tp.left = pos.left + pos.width / 2 - 15;
} else {
tp.left = pos.left + pos.width / 2 - actualWidth + 15;
}
}
$tip.css(tp).addClass('tipsy-' + gravity);
$tip.find('.tipsy-arrow')[0].className = 'tipsy-arrow tipsy-arrow-' + gravity.charAt(0);
if (this.options.className) {
$tip.addClass(maybeCall(this.options.className, this.$element[0]));
}
if (this.options.fade) {
$tip.stop().css({opacity: 0, display: 'block', visibility: 'visible'}).animate({opacity: this.options.opacity});
} else {
$tip.css({visibility: 'visible', opacity: this.options.opacity});
}
$tip.css({'z-index':this.options.zIndex});
var t = this;
var set_hovered = function(set_hover){
return function(){
t.$tip.stop();
t.tipHovered = set_hover;
if (!set_hover){
if (t.options.delayOut === 0 && t.options.trigger != 'manual') {
t.hide();
} else {
setTimeout(function() {
if (t.hoverState == 'out') t.hide(); }, t.options.delayOut);
}
}
};
};
$tip.hover(set_hovered(true), set_hovered(false));
}
},
hide: function() {
if (this.options.fade) {
this.tip().stop().fadeOut(function() { $(this).remove(); });
} else {
if (this.options.cancelHide == null || !this.options.cancelHide()){
this.tip().remove();
}
}
},
fixTitle: function() {
var $e = this.$element;
if ($e.attr('title') || typeof($e.attr('original-title')) != 'string') {
$e.attr('original-title', $e.attr('title') || '').removeAttr('title');
}
if (typeof $e.context.nearestViewportElement == 'object'){
if ($e.children('title').length){
$e.append('