// tipsy, facebook style tooltips for jquery // version 1.0.0a // (c) 2008-2010 jason frame [jason@onehackoranother.com] // released under the mit license (function($) { function maybecall(thing, ctx) { return (typeof thing == 'function') ? (thing.call(ctx)) : thing; }; function iselementindom(ele) { while (ele = ele.parentnode) { if (ele == document) return true; } return false; }; function tipsy(element, options) { this.$element = $(element); this.options = options; this.enabled = true; this.fixtitle(); }; 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, height: this.$element[0].offsetheight }); 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}); } } }, hide: function() { if (this.options.fade) { this.tip().stop().fadeout(function() { $(this).remove(); }); } else { 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'); } }, gettitle: function() { var title, $e = this.$element, o = this.options; this.fixtitle(); var title, o = this.options; if (typeof o.title == 'string') { title = $e.attr(o.title == 'title' ? 'original-title' : o.title); } else if (typeof o.title == 'function') { title = o.title.call($e[0]); } title = ('' + title).replace(/(^\s*|\s*$)/, ""); return title || o.fallback; }, tip: function() { if (!this.$tip) { this.$tip = $('
').html('
'); this.$tip.data('tipsy-pointee', this.$element[0]); } return this.$tip; }, validate: function() { if (!this.$element[0].parentnode) { this.hide(); this.$element = null; this.options = null; } }, enable: function() { this.enabled = true; }, disable: function() { this.enabled = false; }, toggleenabled: function() { this.enabled = !this.enabled; } }; $.fn.tipsy = function(options) { if (options === true) { return this.data('tipsy'); } else if (typeof options == 'string') { var tipsy = this.data('tipsy'); if (tipsy) tipsy[options](); return this; } options = $.extend({}, $.fn.tipsy.defaults, options); function get(ele) { var tipsy = $.data(ele, 'tipsy'); if (!tipsy) { tipsy = new tipsy(ele, $.fn.tipsy.elementoptions(ele, options)); $.data(ele, 'tipsy', tipsy); } return tipsy; } function enter() { var tipsy = get(this); tipsy.hoverstate = 'in'; if (options.delayin == 0) { tipsy.show(); } else { tipsy.fixtitle(); settimeout(function() { if (tipsy.hoverstate == 'in') tipsy.show(); }, options.delayin); } }; function leave() { var tipsy = get(this); tipsy.hoverstate = 'out'; if (options.delayout == 0) { tipsy.hide(); } else { settimeout(function() { if (tipsy.hoverstate == 'out') tipsy.hide(); }, options.delayout); } }; if (!options.live) this.each(function() { get(this); }); if (options.trigger != 'manual') { var binder = options.live ? 'live' : 'bind', eventin = options.trigger == 'hover' ? 'mouseenter' : 'focus', eventout = options.trigger == 'hover' ? 'mouseleave' : 'blur'; this[binder](eventin, enter)[binder](eventout, leave); } return this; }; $.fn.tipsy.defaults = { classname: null, delayin: 0, delayout: 0, fade: false, fallback: '', gravity: 'n', html: false, live: false, offset: 0, opacity: 0.8, title: 'title', trigger: 'hover' }; $.fn.tipsy.revalidate = function() { $('.tipsy').each(function() { var pointee = $.data(this, 'tipsy-pointee'); if (!pointee || !iselementindom(pointee)) { $(this).remove(); } }); }; // overwrite this method to provide options on a per-element basis. // for example, you could store the gravity in a 'tipsy-gravity' attribute: // return $.extend({}, options, {gravity: $(ele).attr('tipsy-gravity') || 'n' }); // (remember - do not modify 'options' in place!) $.fn.tipsy.elementoptions = function(ele, options) { return $.metadata ? $.extend({}, options, $(ele).metadata()) : options; }; $.fn.tipsy.autons = function() { return $(this).offset().top > ($(document).scrolltop() + $(window).height() / 2) ? 's' : 'n'; }; $.fn.tipsy.autowe = function() { return $(this).offset().left > ($(document).scrollleft() + $(window).width() / 2) ? 'e' : 'w'; }; /** * yields a closure of the supplied parameters, producing a function that takes * no arguments and is suitable for use as an autogravity function like so: * * @param margin (int) - distance from the viewable region edge that an * element should be before setting its tooltip's gravity to be away * from that edge. * @param prefer (string, e.g. 'n', 'sw', 'w') - the direction to prefer * if there are no viewable region edges effecting the tooltip's * gravity. it will try to vary from this minimally, for example, * if 'sw' is preferred and an element is near the right viewable * region edge, but not the top edge, it will set the gravity for * that element's tooltip to be 'se', preserving the southern * component. */ $.fn.tipsy.autobounds = function(margin, prefer) { return function() { var dir = {ns: prefer[0], ew: (prefer.length > 1 ? prefer[1] : false)}, boundtop = $(document).scrolltop() + margin, boundleft = $(document).scrollleft() + margin, $this = $(this); if ($this.offset().top < boundtop) dir.ns = 'n'; if ($this.offset().left < boundleft) dir.ew = 'w'; if ($(window).width() + $(document).scrollleft() - $this.offset().left < margin) dir.ew = 'e'; if ($(window).height() + $(document).scrolltop() - $this.offset().top < margin) dir.ns = 's'; return dir.ns + (dir.ew ? dir.ew : ''); } }; })(jquery);