/*
 * jQuery leviTip plugin
 * Version: 0.1.1
 *
 * Copyright (c) 2007 Roman Weich
 * http://p.sohei.org
 *
 * Dual licensed under the MIT and GPL licenses 
 * (This means that you can choose the license that best suits your project, and use it accordingly):
 *   http://www.opensource.org/licenses/mit-license.php
 *   http://www.gnu.org/licenses/gpl.html
 *
 * Changelog: 
 * v 0.1.1 - 2007-28-01
 *	-fix: error while unbinding events on jquery 1.2.2
 *	-fix: spelling error
 * v 0.1.0 - 2007-12-01
 */

/**
 * Yet another tooltip plugin.
 *
 * Features:
 * Displays a user defined tooltip on hover/click over/on a target element.
 * The way the content for the tooltip is retrieved can be configured through the source handlers / source types (see way below).
 *
 * @requires jquery.dimensions plugin
 * @requires {optional} jquery.bgiframe plugin (only for IE <= 6 to prevent a bug when showing the tooltip over a <select>-element)
 * @requires {optional} jquery.hoverintent plugin
 *
 * @param {map} options			An object for optional settings (options described below).
 *
 * @option {string} sourceType		The source type or source handler used to retrieve the contents for the levitip ('attribute', 'element', 'firstchild' and whatever you create yourself).
 *							Default value: 'attribute'
 * @option {string} source			The source as additional parameter to the sourceType. (see the description for the source handlers below)
 *							Default value: 'title'
 * @option {string} activateOn		What kind of user input will activate the levitip?
 *							Possible values: 'hover' and 'click'
 *							Default value: 'hover'
 * @option {string} insertInto		Where to insert the levitip div element into the DOM.
 *							Possible values: 'body' (inserted into the <body>-tag) and 'target' (inserted as child of the target element)
 *							Default value: 'body'
 * @option {string} addClass			A CSS class to add to the levitip div container.
 *							Default value: 'levitip'
 * @option {integer} leftOffset		The number of pixels to offset the position of the levitip to the left.
 *							Default value: 10
 * @option {integer} topOffset		The number of pixels to offset the position of the levitip to the top.
 *							Default value: 10
 * @option {integer} closeDelay		The number of milliseconds to wait before closing the levitip after the user hovered out of the target area.
 *							Default value: 100
 * @option {boolean} dropShadow		To drop or not to drop a shadow.
 *							The shadowbox CSS styles and the myshadow.png are required for this! (see the demo)
 *							Default value: true
 * @option {boolean} useHoverIntent	Use the hoverIntent plugin if found.
 *							Default value: true
 * @option {integer} hiSensitivity		Used by the hoverIntent plugin (sensitivity threshold).
 *							Default value: 7
 * @option {integer} hiInterval		Used by the hoverIntent plugin (milliseconds of polling interval).
 *							Default value: 50
 * @option {function} onOpen		Callback function which is triggered when the levitip is displayed.
 *							The passed parameters are: the levitip element (as jQuery object) and the target element.
 *							Default value: null
 * @option {function} onClose		Callback function which is triggered when the levitip is closed.
 *							The passed parameters are: the levitip element (as jQuery object) and the target element.
 *							Default value: null
 *
 * The examples are below, in the source handler description..
 *
 * @type jQuery
 *
 * @name leviTip
 * @cat Plugins/leviTip
 * @author Roman Weich (http://p.sohei.org)
 */

(function($)
{
	var activeTip = null, 
		sourceHandlers = {}, 
		$tip = null, 
		$innerTip = null,
		defaults = {
			sourceType: 'attribute',
			source: 'title',
			activateOn: 'hover',
			insertInto: 'body',
			addClass: 'levitip',
			topOffset: 10,
			leftOffset: 10,
			closeDelay: 100,
			dropShadow: true,
			useHoverIntent: true,
			hiSensitivity: 7,
			hiInterval: 50,
			onOpen: function(){},
			onClose: function(){}
		},
		mouseOver = $.fn.jquery <= "1.2.1" ? 'mouseover' : 'mouseenter',
		mouseOut = $.fn.jquery <= "1.2.1" ? 'mouseout' : 'mouseleave';

	$.extend({
		LeviTip: function(target, options)
		{
			this.init(target, options);
		}
	});
	$.extend($.LeviTip, {
		/**
		 * Adds a source handler object to the list of available handlers.
		 * A source handler is used to retrieve the contents, which will be displayed inside the levitip.
		 * The handler must have the following properties defined:
		 *	{string} type				The name of the handler. You have to use the same name in the sourceType levitip-option.
		 * 	{function} get(levitip-instance)	Searches and returns the element (or string) to be displayed in the levitip.
		 * The following properties are optional:
		 * 	{function} prepare(levitip-instance)	Is called, when the levitip for an element is initialized (e.g. on page load).
		 * 	{function} end(levitip-instance)	Is called, after the levitip was closed.
		 * @param {object} handler	The new source handler.
		 */
		addSourceHandler : function(handler)
		{
			if ( handler.get && handler.type )
				sourceHandlers[handler.type] = handler;
		},
		/**
		 * Closes the currently open levitip.
		 */
		closeLeviTip : function()
		{
			if ( activeTip )
				activeTip.close();
		},
		/**
		 * Sets the global default settings.
		 * @param {object} d	The object holding the settings.
		 */
		setDefaults : function(d)
		{
			$.extend(defaults, d);
		},
		prototype: {
			/**
			 * Initializes the new levitip instance.
			 * @param {element} target	The levitip target element (the one which activates the levitip).
			 * @param {object} o		The object holding the settings.
			 */
			init : function(target, o)
			{
				if ( !target )
				{
					return;
				}

				this.settings = $.extend({}, defaults, o);
				this.target = target;
				this.timer = this.tipHover = false;
				this.handler = sourceHandlers[this.settings.sourceType] || 0;
				this.pos = {cx:0, cy:0, px: 0, py: 0};

				var self = this,
					onHover = (this.settings.activateOn == 'hover' ? function(e)
					{
						if ( self.settings.activateOn == 'hover' )
							self.hoverIn(e);
					} : function(){});

				if ( !$tip )
				{
					$innerTip = $('<div class="innerbox"></div>');
					$tip = $('<div><div class="shadowbox1"></div><div class="shadowbox2"></div><div class="shadowbox3"></div></div>')
								.append($innerTip);
					$tip.css({position:'absolute', display: 'none'}).addClass('levitipouter').appendTo('body');
					//add iframe on ie 6
					if ( $.browser.msie && (!$.browser.version || parseInt($.browser.version) <= 6) && $.fn.bgiframe )
					{
						$tip.bgiframe();
					}
				}

				//no matching source handler found?
				if ( !this.handler )
				{
					return;
				}
				if ( this.handler.prepare )
				{
					this.handler.prepare(this);
				}

				if ( $.fn.hoverIntent && this.settings.useHoverIntent )
				{
					$(target).hoverIntent({
						interval: this.settings.hiInterval,
						sensitivity: this.settings.hiSensitivity,
						over: onHover,
						out: function()
						{
							self.hoverOut();
						},
						timeout: 0
					});
				}
				else
				{
					$(target).hover(onHover, function()
					{
						self.hoverOut();
					});
				}
				if ( this.settings.activateOn == 'click' )
				{
					$(target).click(function(e){
						self.hoverIn(e);
					});
				}
			},
			/**
			 * Displays the levitip. 
			 * Calls the user defined onOpen callback function at the end.
			 * @param {object} e		The event object.
			 */
			hoverIn: function(e)
			{
				if ( activeTip ) //already a levitip open?
				{
					if ( activeTip == this ) //the current one is already open?
					{
						if ( this.timer ) //is there a closetimer running?
						{
							clearTimeout(this.timer);
						}
						return;
					}
					else //close the other levitip
					{
						activeTip.close();
						activeTip = null;
					}
				}
				//where to insert?
				var into = ( this.settings.insertInto == 'target' ) ? this.target : 
								( this.settings.insertInto == 'body' ) ? 'body' : this.settings.insertInto;

				//insert levitip into document
				$tip.appendTo(into).css({visibility: 'hidden', display:'block'});
				//insert stuff into levitip
				var ins = this.handler.get(this);
				if ( !ins )
					return;
				$innerTip.html(ins).children().show();

				if ( this.settings.addClass )
				{
					$innerTip.addClass(this.settings.addClass);
				}
				//add the css styles to the outer div containers to display the shadow
				if ( this.settings.dropShadow )
				{
					$tip.addClass('outerbox');
				}

				//save mouse position
				this.pos = {cx: e.clientX, cy: e.clientY, px: e.pageX, py: e.pageY};
				//calc/set position
				this.setPosition();

				//show levitip
				$tip.css({display:'none', visibility: ''}).show();
				activeTip = this;
				if ( this.settings.insertInto == 'body' ) //make sure, the levitip wont be closed when moving the cursor out of the target -> over the levitip
				{
					var self = this;
					$tip.hover(function(e){
						self.tipHoverIn(e);
					},
					function(){
						self.tipHoverOut();
					});
				}
				
				//call user defined callback func
				if ( this.settings.onOpen )
				{
					this.settings.onOpen($tip, this.target);
				}
			},
			/**
			 * HoverOut function (called when hovering out of the target element)
			 */
			hoverOut: function()
			{
				var self = this;
				//close levitip after delay
				this.timer = setTimeout(function(){
					if ( !self.tipHover )
					{
						self.close();
					}
				}, this.settings.closeDelay);
			},
			/**
			 * HoverIn function of the levitip
			 */
			tipHoverIn: function()
			{
				this.tipHover = true;
			},
			/**
			 * HoverOut function of the levitip
			 */
			tipHoverOut: function()
			{
				this.tipHover = false;
				this.hoverOut();
			},
			/**
			 * Calculates and sets the position of the levitip.
			 * Displays the levitip on the lower right position of the cursor. When there is not enough space on the right side, the levitip is displayed on the left side of the cursor.  When there is not enough space
			 * below the cursor, the levitip is displayed on top of the cursor. If the levitip is again outside of the visible area on the left or on top, it will be shifted into view.
			 * Make sure the levitip is set to display: block before calling this function!
			 */
			setPosition: function()
			{
				var posX, posY, ww = $(window).width(), wh = $(window).height(), $op, opo;
				
				//calc position
				$op = $tip.offsetParent();
				opo = ( this.settings.insertInto == 'body' ) ? {left:0,top:0,scrollLeft:0,scrollTop:0} : $op.offset();
				if ( this.settings.insertInto == 'target' && $op.css('position') == 'fixed' )
				{
					posX = this.pos.cx;
					posY = this.pos.cy;
				}
				else
				{
					posX = this.pos.px;
					posY = this.pos.py;
				}
				posX += this.settings.leftOffset - opo.left - opo.scrollLeft;
				posY += this.settings.topOffset - opo.top - opo.scrollTop;
				//make sure, the tip wont get displayed outside the visible screen
				if ( ww < this.pos.cx + $tip[0].clientWidth + this.settings.leftOffset ) //outside on the right side?
				{
					var wsl = $(window).scrollLeft();
					posX -= $tip[0].clientWidth + this.settings.leftOffset * 2;
					if ( opo.left - wsl + posX < 0 ) //and now outside on the left? :)
					{
						posX -= opo.left - wsl + posX;
					}
				}
				if ( wh < this.pos.cy + $tip[0].clientHeight + this.settings.topOffset ) //outside on the bottom?
				{
					var wst = $(window).scrollTop();
					posY -= $tip[0].clientHeight + this.settings.topOffset * 2;
					if ( opo.top - wst + posY < 0 ) //outside on the top?
					{
						posY -= opo.top - wst + posY;
					}
				}
				//set position
				$tip.css({left: posX, top: posY});
			},
			/**
			 * Closes/hides the levitip.
			 * Calls the user defined onClose callback function at the end.
			 */
			close: function()
			{
				if ( this.timer )
				{
					clearTimeout(this.timer);
				}
				//hide levitip - unbind events - reset position to prevent flickering on next show() - remove dropshadow css class
				$tip.hide().unbind(mouseOver).unbind(mouseOut).css({left:0, top:0}).removeClass('outerbox');

				if ( this.settings.addClass )
				{
					$innerTip.removeClass(this.settings.addClass);
				}

				activeTip = false;
				if ( this.handler.end )
				{
					this.handler.end(this);
				}
				//call user defined callback func
				if ( this.settings.onClose )
				{
					this.settings.onClose($tip, this.target);
				}
			}
		}
	});

	/**
	 * "create" the jquery plugin
	 * the description is at the top of this file..
	 */
	$.fn.extend({
		leviTip: function(options)
		{
			return this.each(function(){
				new $.LeviTip(this, options);
			});
		}
	});

	//add default source handlers
	
	/**
	 * attribute source handler
	 * displays the attribute string as levitip content. the attribute has to be passed in the source-option.
	 *
	 * @example $('#someid').leviTip({sourceType: 'attribute', source: 'myattr'});
	 * @on <div id="someid" myattr="hello world!">...</div>
	 * @desc Will display "hello world!" as content of the levitip, when hovering over the div with the id #someid.
	 */
	$.LeviTip.addSourceHandler({
		type: 'attribute',
		get: function(levitip)
		{
			var attr = $(levitip.target).attr(levitip.settings.source);
			//remove title attribute (we dont want the default tooltip displayed)
			if ( levitip.settings.source == 'title' )
			{
				levitip.titleAttr = attr;
				$(levitip.target).attr('title', '');
			}
			return attr;
		},
		end: function(levitip)
		{
			//put the title attribute back again
			if ( levitip.settings.source == 'title' && levitip.titleAttr )
			{
				$(levitip.target).attr('title', levitip.titleAttr);
			}
		}
	});
	/**
	 * element source handler
	 * clones the element, which is found through the source-selector, and displays it as levitip content
	 *
	 * @example $('#someid').leviTip({sourceType: 'element', source: '#tooltip'});
	 * @on <div id="someid">...</div>
	 * @on <div id="tooltip">some stuff..<p>hello world!</p></div>
	 * @desc Will display a clone of the #tooltip div with all its contents as levitip, when hovering over the div with the id #someid.
	 *
	 * @example $('#someid').leviTip({sourceType: 'element', source: '#tooltip > p'});
	 * @on <div id="someid">...</div>
	 * @on <div id="tooltip">some stuff..<p>hello world!</p></div>
	 * @desc Will display a clone of the p-element inside the #tooltip div as levitip, when hovering over the div with the id #someid.
	 */
	$.LeviTip.addSourceHandler({
		type: 'element',
		prepare: function(levitip)
		{
			if ( levitip.settings.hideSourceElement )
			{
				$(levitip.settings.source).hide();
			}
		},
		get: function(levitip)
		{
			var $e = [];
			if ( levitip.settings.source )
			{
				$e = $(levitip.settings.source);
				if ( $e.length )
					$e = $e.clone(true).show();
			}
			return $e;
		}
	});
	/**
	 * firstchild source handler
	 * clones the element, which is the first child of the target element, and displays it as levitip content.
	 * the source-option will be ignored
	 *
	 * @example $('#someid').leviTip({sourceType: 'firstchild'});
	 * @on <div id="someid"><p>jquery rocks!</p><p>hello world!</p></div>
	 * @desc Will display a clone of the p-element ("jquery rocks!") inside the #someid div as levitip, when hovering over the div with the id #someid.
	 */
	$.LeviTip.addSourceHandler({
		type: 'firstchild',
		prepare: function(levitip)
		{
			if ( levitip.settings.hideSourceElement )
			{
				$(levitip.target.firstChild).hide();
			}
		},
		get: function(levitip)
		{
			var $e = $(levitip.target.firstChild);
			if ( $e.length )
				$e = $e.clone(true).show();
			return $e;
		}
	});
})(jQuery);

