| 1 |
// tipsy, facebook style tooltips for jquery |
| 2 |
// version 1.0.0a |
| 3 |
// (c) 2008-2010 jason frame [jason@onehackoranother.com] |
| 4 |
// released under the MIT license |
| 5 |
|
| 6 |
(function($) { |
| 7 |
|
| 8 |
function maybeCall(thing, ctx) { |
| 9 |
return (typeof thing == 'function') ? (thing.call(ctx)) : thing; |
| 10 |
}; |
| 11 |
|
| 12 |
function isElementInDOM(ele) { |
| 13 |
while (ele = ele.parentNode) { |
| 14 |
if (ele == document) return true; |
| 15 |
} |
| 16 |
return false; |
| 17 |
}; |
| 18 |
|
| 19 |
function Tipsy(element, options) { |
| 20 |
this.$element = $(element); |
| 21 |
this.options = options; |
| 22 |
this.enabled = true; |
| 23 |
this.fixTitle(); |
| 24 |
}; |
| 25 |
|
| 26 |
Tipsy.prototype = { |
| 27 |
show: function() { |
| 28 |
var title = this.getTitle(); |
| 29 |
if (title && this.enabled) { |
| 30 |
var $tip = this.tip(); |
| 31 |
|
| 32 |
$tip.find('.tipsy-inner')[this.options.html ? 'html' : 'text'](title); |
| 33 |
$tip[0].className = 'tipsy'; // reset classname in case of dynamic gravity |
| 34 |
$tip.remove().css({top: 0, left: 0, visibility: 'hidden', display: 'block'}).prependTo(document.body); |
| 35 |
|
| 36 |
var pos = $.extend({}, this.$element.offset(), { |
| 37 |
width: this.$element[0].offsetWidth, |
| 38 |
height: this.$element[0].offsetHeight |
| 39 |
}); |
| 40 |
|
| 41 |
var actualWidth = $tip[0].offsetWidth, |
| 42 |
actualHeight = $tip[0].offsetHeight, |
| 43 |
gravity = maybeCall(this.options.gravity, this.$element[0]); |
| 44 |
|
| 45 |
var tp; |
| 46 |
switch (gravity.charAt(0)) { |
| 47 |
case 'n': |
| 48 |
tp = {top: pos.top + pos.height + this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2}; |
| 49 |
break; |
| 50 |
case 's': |
| 51 |
tp = {top: pos.top - actualHeight - this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2}; |
| 52 |
break; |
| 53 |
case 'e': |
| 54 |
tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth - this.options.offset}; |
| 55 |
break; |
| 56 |
case 'w': |
| 57 |
tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width + this.options.offset}; |
| 58 |
break; |
| 59 |
} |
| 60 |
|
| 61 |
if (gravity.length == 2) { |
| 62 |
if (gravity.charAt(1) == 'w') { |
| 63 |
tp.left = pos.left + pos.width / 2 - 15; |
| 64 |
} else { |
| 65 |
tp.left = pos.left + pos.width / 2 - actualWidth + 15; |
| 66 |
} |
| 67 |
} |
| 68 |
|
| 69 |
$tip.css(tp).addClass('tipsy-' + gravity); |
| 70 |
$tip.find('.tipsy-arrow')[0].className = 'tipsy-arrow tipsy-arrow-' + gravity.charAt(0); |
| 71 |
if (this.options.className) { |
| 72 |
$tip.addClass(maybeCall(this.options.className, this.$element[0])); |
| 73 |
} |
| 74 |
|
| 75 |
if (this.options.fade) { |
| 76 |
$tip.stop().css({opacity: 0, display: 'block', visibility: 'visible'}).animate({opacity: this.options.opacity}); |
| 77 |
} else { |
| 78 |
$tip.css({visibility: 'visible', opacity: this.options.opacity}); |
| 79 |
} |
| 80 |
} |
| 81 |
}, |
| 82 |
|
| 83 |
hide: function() { |
| 84 |
if (this.options.fade) { |
| 85 |
this.tip().stop().fadeOut(function() { $(this).remove(); }); |
| 86 |
} else { |
| 87 |
this.tip().remove(); |
| 88 |
} |
| 89 |
}, |
| 90 |
|
| 91 |
fixTitle: function() { |
| 92 |
var $e = this.$element; |
| 93 |
if ($e.attr('title') || typeof($e.attr('original-title')) != 'string') { |
| 94 |
$e.attr('original-title', $e.attr('title') || '').removeAttr('title'); |
| 95 |
} |
| 96 |
}, |
| 97 |
|
| 98 |
getTitle: function() { |
| 99 |
var title, $e = this.$element, o = this.options; |
| 100 |
this.fixTitle(); |
| 101 |
var title, o = this.options; |
| 102 |
if (typeof o.title == 'string') { |
| 103 |
title = $e.attr(o.title == 'title' ? 'original-title' : o.title); |
| 104 |
} else if (typeof o.title == 'function') { |
| 105 |
title = o.title.call($e[0]); |
| 106 |
} |
| 107 |
title = ('' + title).replace(/(^\s*|\s*$)/, ""); |
| 108 |
return title || o.fallback; |
| 109 |
}, |
| 110 |
|
| 111 |
tip: function() { |
| 112 |
if (!this.$tip) { |
| 113 |
this.$tip = $('<div class="tipsy"></div>').html('<div class="tipsy-arrow"></div><div class="tipsy-inner"></div>'); |
| 114 |
this.$tip.data('tipsy-pointee', this.$element[0]); |
| 115 |
} |
| 116 |
return this.$tip; |
| 117 |
}, |
| 118 |
|
| 119 |
validate: function() { |
| 120 |
if (!this.$element[0].parentNode) { |
| 121 |
this.hide(); |
| 122 |
this.$element = null; |
| 123 |
this.options = null; |
| 124 |
} |
| 125 |
}, |
| 126 |
|
| 127 |
enable: function() { this.enabled = true; }, |
| 128 |
disable: function() { this.enabled = false; }, |
| 129 |
toggleEnabled: function() { this.enabled = !this.enabled; } |
| 130 |
}; |
| 131 |
|
| 132 |
$.fn.tipsy = function(options) { |
| 133 |
|
| 134 |
if (options === true) { |
| 135 |
return this.data('tipsy'); |
| 136 |
} else if (typeof options == 'string') { |
| 137 |
var tipsy = this.data('tipsy'); |
| 138 |
if (tipsy) tipsy[options](); |
| 139 |
return this; |
| 140 |
} |
| 141 |
|
| 142 |
options = $.extend({}, $.fn.tipsy.defaults, options); |
| 143 |
|
| 144 |
function get(ele) { |
| 145 |
var tipsy = $.data(ele, 'tipsy'); |
| 146 |
if (!tipsy) { |
| 147 |
tipsy = new Tipsy(ele, $.fn.tipsy.elementOptions(ele, options)); |
| 148 |
$.data(ele, 'tipsy', tipsy); |
| 149 |
} |
| 150 |
return tipsy; |
| 151 |
} |
| 152 |
|
| 153 |
function enter() { |
| 154 |
var tipsy = get(this); |
| 155 |
tipsy.hoverState = 'in'; |
| 156 |
if (options.delayIn == 0) { |
| 157 |
tipsy.show(); |
| 158 |
} else { |
| 159 |
tipsy.fixTitle(); |
| 160 |
setTimeout(function() { if (tipsy.hoverState == 'in') tipsy.show(); }, options.delayIn); |
| 161 |
} |
| 162 |
}; |
| 163 |
|
| 164 |
function leave() { |
| 165 |
var tipsy = get(this); |
| 166 |
tipsy.hoverState = 'out'; |
| 167 |
if (options.delayOut == 0) { |
| 168 |
tipsy.hide(); |
| 169 |
} else { |
| 170 |
setTimeout(function() { if (tipsy.hoverState == 'out') tipsy.hide(); }, options.delayOut); |
| 171 |
} |
| 172 |
}; |
| 173 |
|
| 174 |
if (!options.live) this.each(function() { get(this); }); |
| 175 |
|
| 176 |
if (options.trigger != 'manual') { |
| 177 |
var binder = options.live ? 'live' : 'bind', |
| 178 |
eventIn = options.trigger == 'hover' ? 'mouseenter' : 'focus', |
| 179 |
eventOut = options.trigger == 'hover' ? 'mouseleave' : 'blur'; |
| 180 |
this[binder](eventIn, enter)[binder](eventOut, leave); |
| 181 |
} |
| 182 |
|
| 183 |
return this; |
| 184 |
|
| 185 |
}; |
| 186 |
|
| 187 |
$.fn.tipsy.defaults = { |
| 188 |
className: null, |
| 189 |
delayIn: 0, |
| 190 |
delayOut: 0, |
| 191 |
fade: false, |
| 192 |
fallback: '', |
| 193 |
gravity: 'n', |
| 194 |
html: false, |
| 195 |
live: false, |
| 196 |
offset: 0, |
| 197 |
opacity: 0.8, |
| 198 |
title: 'title', |
| 199 |
trigger: 'hover' |
| 200 |
}; |
| 201 |
|
| 202 |
$.fn.tipsy.revalidate = function() { |
| 203 |
$('.tipsy').each(function() { |
| 204 |
var pointee = $.data(this, 'tipsy-pointee'); |
| 205 |
if (!pointee || !isElementInDOM(pointee)) { |
| 206 |
$(this).remove(); |
| 207 |
} |
| 208 |
}); |
| 209 |
}; |
| 210 |
|
| 211 |
// Overwrite this method to provide options on a per-element basis. |
| 212 |
// For example, you could store the gravity in a 'tipsy-gravity' attribute: |
| 213 |
// return $.extend({}, options, {gravity: $(ele).attr('tipsy-gravity') || 'n' }); |
| 214 |
// (remember - do not modify 'options' in place!) |
| 215 |
$.fn.tipsy.elementOptions = function(ele, options) { |
| 216 |
return $.metadata ? $.extend({}, options, $(ele).metadata()) : options; |
| 217 |
}; |
| 218 |
|
| 219 |
$.fn.tipsy.autoNS = function() { |
| 220 |
return $(this).offset().top > ($(document).scrollTop() + $(window).height() / 2) ? 's' : 'n'; |
| 221 |
}; |
| 222 |
|
| 223 |
$.fn.tipsy.autoWE = function() { |
| 224 |
return $(this).offset().left > ($(document).scrollLeft() + $(window).width() / 2) ? 'e' : 'w'; |
| 225 |
}; |
| 226 |
|
| 227 |
/** |
| 228 |
* yields a closure of the supplied parameters, producing a function that takes |
| 229 |
* no arguments and is suitable for use as an autogravity function like so: |
| 230 |
* |
| 231 |
* @param margin (int) - distance from the viewable region edge that an |
| 232 |
* element should be before setting its tooltip's gravity to be away |
| 233 |
* from that edge. |
| 234 |
* @param prefer (string, e.g. 'n', 'sw', 'w') - the direction to prefer |
| 235 |
* if there are no viewable region edges effecting the tooltip's |
| 236 |
* gravity. It will try to vary from this minimally, for example, |
| 237 |
* if 'sw' is preferred and an element is near the right viewable |
| 238 |
* region edge, but not the top edge, it will set the gravity for |
| 239 |
* that element's tooltip to be 'se', preserving the southern |
| 240 |
* component. |
| 241 |
*/ |
| 242 |
$.fn.tipsy.autoBounds = function(margin, prefer) { |
| 243 |
return function() { |
| 244 |
var dir = {ns: prefer[0], ew: (prefer.length > 1 ? prefer[1] : false)}, |
| 245 |
boundTop = $(document).scrollTop() + margin, |
| 246 |
boundLeft = $(document).scrollLeft() + margin, |
| 247 |
$this = $(this); |
| 248 |
|
| 249 |
if ($this.offset().top < boundTop) dir.ns = 'n'; |
| 250 |
if ($this.offset().left < boundLeft) dir.ew = 'w'; |
| 251 |
if ($(window).width() + $(document).scrollLeft() - $this.offset().left < margin) dir.ew = 'e'; |
| 252 |
if ($(window).height() + $(document).scrollTop() - $this.offset().top < margin) dir.ns = 's'; |
| 253 |
|
| 254 |
return dir.ns + (dir.ew ? dir.ew : ''); |
| 255 |
} |
| 256 |
}; |
| 257 |
|
| 258 |
})(jQuery); |
| 259 |
|