/******
 * Tipped: A tooltip plugin for jQuery
 * http://code.google.com/p/tipped/
 *
 * Copyright 2010, University of Alberta
 *
 * Tipped is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Tipped is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License v2
 * along with Tipped.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>
 *
 *
 * Compatibility: Tested with jQuery 1.4.2, though it should work with 1.3.x
 *
 * v1.5		- Fixed bug with 'title' attribute being added back after tip goes away
 *			- Added 'position' and 'posX', 'posY' options
 *			- Added 'delay' option
 * v1.4		- Added logic that resizes tips that are larger than the viewport, to fit inside the viewport
 *			- Added oversizeStick option
 * v1.3.4:  - Made themeroller compatible
 *			- Used removeAttr() to remove title attribute, rather than setting the attribute to blank
 *			- Thanks to Durval Agnelo for the advice/contribution
 * v1.3.3:	- Became a good jQuery citizen and return the jQuery object from tipped() so it supports chaining
 *			- Fixed a bug that emptied out the title stored in data(), if tipped() is called
 *			  on an element twice
 * v1.3.2:	Fixed 'title' based tips that were trying to show the title from the attribute after it was emptied out
 * v1.3.1:	Did some stuff
 * v1.3:	Reposition tooltip at top left before width calculation for repositioning done.  This
 *			prevents inline elements from being squished.
 * v1.2:	Fixed showing/hiding of the "Close" button if there are tips with both "hover" and "click" mode
 * v1.1: 	- Added turning off of default tooltip that appears when an elelment has a title
 *			- #tipped element is now created explicitely as an window variable - fixes a problem with Safari
 * v1.0: 	Initial release
 */
(function($) {
	/******
	/*	Options
		
			ajaxType:	The type of HTTP request to make.
				Possible values: Anything $.ajax accepts (usually 'GET' or 'POST')
				Default: 'POST'
			
			cache:		Whether or not to cache AJAX requests.  Cache is based on URL, not URL + data, so if 
						you are making multiple requests to the same URL with different data, turn off cache
				Default: false
			
			closer:		The HTML to display when a tip is to be manually closed (ie: when triggered by a click).  
						All text in 'closer' will be injected inside another element that has the close listener
				Default: 'Close'
			
			delay:		The milliseconds to wait between when the trigger is hovered over, and the tip appears.
						Ignored if "mode" is "click".
				Default: 0
			marginX:	The pixels to the right of the element that the tip should appear.  This amount will be
						overridden if necessary to ensure the entire tip shows on the screen.
				Possible values: Any integer.  Negative numbers will position the tip to the left of the right
								 edge of the triggering element
				Default: '10'
				
			marginY:	The pixels to the bottom of the element that the tip should appear.  This amount will be
						overridden if necessary to ensure the entire tip shows on the screen.
				Possible values: Any integer.  Negative numbers will position the tip above of the bottom
								 edge of the triggering element
				Default: '10'
				
			mode:		The type of tip to make.  'Hover' shows and hides on hover, 'Click' is triggered with a
						click and requires clicking of the closer to go away
				Possible values: 'hover', 'click'
				Default: 'hover'
			
			oversizeStick:	Whether to revert to "click" mode if the content is too large for the screen.  If the
							content is too large, scrollbars appear.  Users can't use those scrollbars though, if
							the tooltip disappears when they hover off the target.  This will remedy that problem.
							
							The 'closer' option will be used when necessary.
				Possible values: true,false
				Default: true
				
			params:		An object representing the parameters to send along with an AJAX request as 'data'
				Possible values:
					A callback: Data passed will be the object returned from this function.  Function will be passed
								a jQuery object representing the triggering element
					An object: Will be used as the data
				Default: {}
			
			position:	The method Tipped will use to determine position.
				Possible values:
					'absolute': The position of the tip will be determined by the posX and posY parameters, 
								with no application of the margins and no consideration for where the triggering 
								element is
					'mouse':	The position of the tip will be determined by the location of the mouse when the 
								tip is triggered. Margins will be applied.
					'element':	The position of the tip will be determined by the bottom right corner of the 
								triggering element. Margins will be applied.
				Default: 'element'
			
			posX:	The absolute position on the x-axis the tooltip will have when displayed.  Only used if the 
					'position' option is "absolute"
				Possible values:
					A callback:  Function must return an integer.  Function will be passed a jQuery object 
								 representing the triggering element
					An integer
				Default value: 0
			
			posY:	The absolute position on the y-axis the tooltip will have when displayed.  Only used if the 
					'position' option is "absolute"
				Possible values:
					A callback:  Function must return an integer.  Function will be passed a jQuery object 
								 representing the triggering element
					An integer
				Default value: 0
			
			source:		The source of the value to display.
				Possible values: 
					'title':	Value to display will be pulled from the 'title' attribute of the triggering element
					A callback: Value to display will be returned from the callback function.  Function will be passed
								a jQuery object representing the triggering element
					'url':		An AJAX request will be made to the address specified by the 'url' option
					Any other string:	Will be displayed
				Default: 'title'
				
			themeroller:	Whether or not to make Themeroller compatible
				Possible values: true, false
				Default: false
				
			throbber:	The URL to the image to display while the AJAX request is being sent.  If blank, no throbber
						will be shown.
				Default: ''
				
			url:		The web address to make the AJAX request to.  Unused if 'source' is not 'url'
	*/
	
	var defaults = {
		ajaxType:'POST',
		cache:false,
		cached:{},
		closer:'Close',
		delay:0,
		marginX:10,
		marginY:10,
		mode:'hover',
		oversizeStick:true,
		params:{},
		position:'element',
		posX:0,
		posY:0,
		source:'title',
		themeroller:false,
		throbber:'',
		url:''
	};
	
	//create single tooltip
	window.$tip = {};
	window.$tip_content = {};

	$(document).ready(function(){
		$tip = $("#tipped").length ? $("#tipped") : $('<div id = "tipped"><div id = "tipped_content"></div></div>').appendTo(document.body).data('showing',false);		
		$tip_content = $("#tipped_content");
	});

	$.fn.tipped = function(settings){
		this.each(function(i){
			
			$target = $(this);//shortcut
			
			//store settings
			settings = $.extend({},defaults,settings);
			$target.data('tipped',{settings:settings});	
			
			//make compatible with themeroller
			if(settings.themeroller)
				$tip.addClass('ui-helper-hidden ui-widget ui-dialog ui-corner-all');
			else
				$tip.removeClass('ui-helper-hidden ui-widget ui-dialog ui-corner-all');
				
			
			//2 modes act differently
			if(settings.mode == 'hover')
			{
				var t = undefined;
				$target
					.mouseover(function(e){
						var $thisTarget = $(this);
						$.fn.tipped.removeTitle($thisTarget);
						t = setTimeout(function(){$.fn.tipped.initiateShow($thisTarget,e);},settings.delay);
					})
					.mouseout(function(){
						$.fn.tipped.hideTip($(this));
						clearTimeout(t);
					});
			}
			else if(settings.mode == 'click')
			{
				$.fn.tipped.addCloser($target);
				
				if(settings.themeroller)
				{
					$("#tipped-closer")
						.addClass('ui-button ui-state-hover ui-state-default')
						.hover(function(){$(this).addClass('ui-state-hover');},function(){$(this).removeClass('ui-state-hover');})
						.mousedown(function(){$(this).addClass('ui-state-active');})
						.mouseup(function(){$(this).removeClass('ui-state-active');})
				}
				else
					$("#tipped-closer").removeClass('ui-button ui-state-hover ui-state-default');				
					
				$target.click(function(){ $.fn.tipped.initiateShow($(this)); });
			}	
		});
		
		return this;
	};
	
	/**
	 * Function: addCloser()
	 * Purpose: To add a "close" element to the tooltip
	 * Parameters: $target: a jQuery object that has had a tip bound to it.
	 */
	$.fn.tipped.addCloser = function($target)
	{
		//don't add if it's already there
		if($("#tipped-closer").length == 0)
		{
			$tip.append('<div id = "tipped-closer-wrapper"><span id = "tipped-closer">'+$target.data('tipped').settings.closer+'</span>');
			$("#tipped-closer").click(function(){ $.fn.tipped.hideTip($target); });
		}
		else
			$("#tipped-closer-wrapper").show();
	}
	
	/**
	 * Function: removeTitle()
	 * Purpose: To remove the title attribute from the target, and store that value in data
	 * Parameters: $target: a jQuery object that has had a tip bound to it.
	 */
	$.fn.tipped.removeTitle = function($target)
	{
		if($target.data('tipped').title === undefined)
			$target.data('tipped',$.extend($target.data('tipped'),{title:$target.attr('title')}));
		
		//IE doesn't respect removal of the attribute, so need to set it to blank.
		$target.removeAttr('title').attr('title','');
	}

	
	/**
	 * Function: initiateShow()
	 * Purpose: To initiate the showing of a tip.
	 * Parameters: $target: a jQuery object that has had a tip bound to it.  Tipped uses
	 *                      the settings associated with the $target to determine what to display
	 *			   e: The jQuery event that triggered the hover.  Only provided if "position" is "mouse"
	 */
	$.fn.tipped.initiateShow = function($target,e)
	{	
		//shortcuts
		var settings = $target.data('tipped').settings;
		var cached = $tip.data('cached');

		//manage the closer
		if(settings.mode != 'click')
			$("#tipped-closer-wrapper").hide();
		else
			$("#tipped-closer-wrapper").show();

		//AJAX
		if(settings.source === 'url')
		{
			//if we're not caching, retrieve the value
			if(!settings.cache || cached === undefined || cached[settings.url] === undefined)
			{
				//set parameters
				var data = {};
				if(typeof settings.params == 'function')
					data = settings.params($target);
				else if(typeof settings.params == 'object')
					data = settings.params;
					
				$.ajax({
					beforeSend:function(){
						show($target,'<img src = '+settings.throbber+' alt = "Loading..." />',e);
					},
					data:data,
					error:function(){
						show($target,'Unable to retrieve contents',e);
					},
					success:function(display){
						if($tip.data('showing'))
							show($target,display,e);
						
						//cache results if necessary
						if(settings.cache)
						{
							var newCache = new Object;
							newCache[settings.url] = display;
							cached = $.extend(cached,newCache);
							$tip.data('cached',cached);
						}
					},
					type:settings.ajaxType,
					url:settings.url
				});
				return;
			}
			//otherwise, show the cached copy
			else
			{
				show($target,cached[settings.url],e);
				return;
			}
		}
				
		
		var value = '';
		
		//'title' attribute
		if(settings.source === 'title')
			value = $target.data('tipped').title;
		
		//any other string
		else if(typeof settings.source == 'string')
			value = settings.source;
			
		//custom function
		else if(typeof settings.source == 'function')
			value = settings.source($target);
		
		//jQuery object
		else if(typeof settings.source == 'object')
			value = settings.source.html();
		
		show($target,value,e);
	}
	
	/*
	 * Function: hideTip()
	 * Purpose: To hide the tip
	 * Parameters: $target:	a jQuery object representing the element that triggered the tip
	 */
	$.fn.tipped.hideTip = function($target)
	{
		//$target.attr('title',$target.data('tipped').title);
		$tip.data('showing',false).data('original','').hide();
		$tip_content.html('');
	}
	
	
	/*
	 * Function: getTrigger()
	 * Purpose: To provide access to the element that triggered the tip.  Useful for 
	 *          clicked tips that need to know who triggered them
	 *
	 * Access with: $.getTrigger()
	 */
	$.extend({
		getTrigger:function(){
			return $tip.data('original');
		}
	});

	/*
	 * Function: show()
	 * Purpose: To actually show the tip
	 * Parameters: $target: The element (wrapped in a jQuery object) that triggered the showing of this tip
	 *			   value: The HTML to place into the tip
 	 *
	 * Note: This function is private
	 */
	function show($target,value,e)
	{
		$tip_content.html(value);
		setPosition($target,e)
		$tip.data('showing',true).data('original',$target).show();
	}
	
	
	/*
	 * Function: setPosition()
	 * Purpose: To set the position of the tip.  This function is called after the content of the tip
	 *          is set, allowing the function to make a dynamic decision about the position of the tip
	 *			
	 *			The tip is always displayed fully on the screen & will be moved to ensure that.
	 * Parameters: $elem: a jQuery object representing the element relative to which the tip is to be positioned.
	 *
	 * Note: This function is private
	 */
	function setPosition($target)
	{
		var settings = $target.data('tipped').settings;		

		//position tip in the top left corner, so full, proper width gets calculated
		$tip.css({left:0,top:0});

		resize($target,settings);

		//determine element position on screen
		var posX = 0;
		var posY = 0;
		
		switch(settings.position){
			case 'mouse':
				posX = e.pageX + settings.marginX;
				posY = e.pageY + settings.marginY;
				break;
			case 'absolute':
				if(typeof settings.posX == 'function')
					posX = settings.posX($target);
				else
					posX = settings.posX;
				
				if(typeof settings.posY == 'function')
					posY = settings.posY($target);
				else
					posY = settings.posY;
				break;
			default:
				var targetPos = $target.offset();
				posX = targetPos.left + $target.outerWidth() + settings.marginX;
				posY = targetPos.top + $target.outerHeight() + settings.marginY;
				break;
		}
		
		//adjust to ensure tip is inside viewable screen
		var right = posX + $tip.outerWidth();
		var bottom = posY + $tip.outerHeight();
		
		var windowWidth = $(window).width() + $(window).scrollLeft()-5;
		var windowHeight = $(window).height() + $(window).scrollTop()-5;
		
		posX = (right > windowWidth) ? posX - (right - windowWidth) : posX;
		posY = (bottom > windowHeight) ? posY - (bottom - windowHeight) : posY

		$tip.css({ left: posX, top: posY });
	}
	
	function resize($target,settings)
	{
		//reset height & width settings - may not be needed
		$tip_content.css({height:'auto',width:'auto'});
		$tip.css({height:'auto',width:'auto'});
		
		if($tip.outerHeight() > $(window).height())
		{
			if(settings.oversizeStick)
			{
				$.fn.tipped.addCloser($elem);
				$elem.unbind('mouseout');
			}		
			
			var innerHeightDifference = $tip.outerHeight() - $tip_content.outerHeight();
			var tipHeightDifference = $tip.outerHeight() - $tip.height();
			
			//-10 to account for the -5 applied in setPosition
			$tip.css('height',$(window).height() - tipHeightDifference-10);
			
			//+20 to account for the scrollbar. Browsers don't account for "auto" placed scrollbars in width calculations
			//so the contents ends up getting squished
			$tip_content.css({height:$tip.outerHeight() - innerHeightDifference,overflow:"auto",width:$tip.outerWidth()+20});
		}
		if($tip.outerWidth() > $(window).width())
		{
			if(settings.oversizeStick)
			{
				$.fn.tipped.addCloser($elem);
				$target.unbind('mouseout');
			}
			$tip_content.css({width:$(window).width() - 20,overflow:"auto",height:$tip.outerWidth()+20});
		}
	}
})(jQuery);