 /*
 * TipTip
 * Copyright 2010 Drew Wilson
 * www.drewwilson.com
 * code.drewwilson.com/entry/tiptip-jquery-plugin
 *
 * Version 1.3   -   Updated: Mar. 23, 2010
 *
 * This Plug-In will create a custom tooltip to replace the default
 * browser tooltip. It is extremely lightweight and very smart in
 * that it detects the edges of the browser window and will make sure
 * the tooltip stays within the current window size. As a result the
 * tooltip will adjust itself to be displayed above, below, to the left 
 * or to the right depending on what is necessary to stay within the
 * browser window. It is completely customizable as well via CSS.
 *
 * This TipTip jQuery plug-in is dual licensed under the MIT and GPL licenses:
 *   http://www.opensource.org/licenses/mit-license.php
 *   http://www.gnu.org/licenses/gpl.html
 */

(function ($) {
	var idCount = 0,
		timeout = [],
		namespace = 'tiptip',
		opposites = {
			left	: 'right',
			right	: 'left',
			top		: 'bottom',
			bottom	: 'top',
			center	: 'center'
		},
		xClasses = ['left', 'center', 'right'],
		yClasses = ['top', 'center', 'bottom'],
		defaultPosition = {
			my		: 'bottom center',
			at		: 'top center'
		};
	
	$.fn.tipTip = function (method) {
	
		var defaults = { 
				activation: 'hover',
				keepAlive: false,
				maxWidth: '200px',
				edgeOffset: '0 5',
				delay: 400,
				fadeIn: 200,
				fadeOut: 200,
				attribute: 'title',
				content: false, // HTML or String to fill TipTIp with
			  	enter: function () {},
			  	exit: function () {}
		  	},
	 	opts = $.extend({}, defaults, method),
	 	
	 	self = this,
	 	
	 	/**
	 	 * Returns the event name with the corresponding namespace
	 	 *
	 	 * @param String event name
	 	 *
	 	 * @return String namespaced event name
	 	 */
	 	namespacedEvent = function (name) {
	 		return name + '.' + namespace;
	 	},
	 	
	 	/**
	 	 * Converts the String into the equivalent Object
	 	 *
	 	 * @param String position
	 	 *
	 	 * @return Object
	 	 */
	 	normalizePosition = function(pos) {
	 		var position = defaultPosition;
	 		
	 		if (typeof pos !== 'string') {
	 			pos = '';
	 		}
	 		
	 		pos = pos.replace('center', '');
	 		pos = $.trim(pos);
	 		pos = pos.split(' ');
	 		
	 		if (pos[1]) {
	 			if (pos[0].search(/left|right/) !== -1) {
	 				position.my = opposites[pos[0]] + ' ' + pos[1];
	 				position.at = pos[0] + ' ' + pos[1];
	 			} else {
	 				position.my = pos[1] + ' ' + opposites[pos[0]];
	 				position.at = pos[1] + ' ' + pos[0];
	 			}
	 		}
	 		else {
		 		position.my = opposites[pos[0]];
		 		position.at = pos[0];
	 		}
	 		
	 		return position;
	 	},
	 	
	 	update_position = function (org_elem, data) {
	 		var position,
	 			tempPosition,
	 			visible = data.tiptip_holder.is(':visible'),
	 			$tip	= data.tiptip_holder
	 						.removeAttr('class')
	 						.removeAttr('style')
	 						.css({
							//	'display'	: visible ? 'block' : 'none',
	 							'max-width' : data.opts.maxWidth
	 						})
	 						.addClass('tiptip_holder'),
	 			$el		= org_elem,
	 			my,
	 			at,
	 			stringPos,
	 			defaults = { 
 					of			: $el,
 					collision	: 'flip flip'
 				},
 				offset = data.opts.edgeOffset || '',
 				tipClass;
 			
 			offset = offset.toString().split(' ');
 			if (offset.length <= 1) {
 				offset = offset.push(offset[0]);
 			}
	 		
	 		if (!data.opts.position) {
	 			position = defaultPosition;	
	 		}
	 		else if (typeof data.opts.position === 'object') {
	 			var tmp;
	 			position = data.opts.position;
	 			
	 			if (!position.at) {
	 				tmp = normalizePosition(data.opts.position);
	 				position.at = tmp.at;
	 			}
	 		}
	 		else if (typeof data.opts.position === 'string') {
	 			stringPos = data.opts.position.split(' ');
	 			
	 			if (stringPos[0].search(/top|bottom|left|right|center/) !== -1) {
 					position = normalizePosition(data.opts.position);
	 			} else {
	 				position = defaultPosition;
	 			}	
	 		} 
	 		else {
	 			console.error('no valid position');
	 			return;
	 		}
	 		
	 		tempPosition = $.extend({}, defaults, position);
	 		tempPosition.of = $el;
	 		
	 		// adjust offset
	 		if (tempPosition.my) {
		 		if (tempPosition.my.search(/bottom/) !== -1) {
		 			offset[1] = parseInt(offset[1], 10) * -1;
	
		 		} else if (tempPosition.my.search(/right/) !== -1) {
		 			offset[0] = parseInt(offset[0], 10) * -1;
		 		}
	 		}
	 		
	 		tipClass = getTipTempClasses(tempPosition);

	 		tempPosition = $.extend({}, tempPosition, {
	 			offset: parseInt(offset[0], 10) + ' ' + parseInt(offset[1], 10)
	 		});
	 			 		
	 		/*$('body').children('div.dummy').remove();
	 		$dummy = $('<div>',{
	 			className: 'dummy'
	 		})
	 		.css({
	 			'position'		: 'absolute',
	 			'background'	: 'red',
	 			'width'			: 150,
	 			'height'		: 10
	 		})
	 		.appendTo('body')
	 		.position(tempPosition);
	 		*/
			
			// update position and add classes after position check (collision-detection)
	 		$tip
		 		.css({
		 			'top'	: 0,
		 			'left'	: 0
		 		})
		 		.addClass(tipClass)
		 		.position(tempPosition)
		 		.removeAttr('class')
		 		.addClass('tiptip_holder')
		 		.addClass(getTipClasses($tip, $el))
		 		.css({
		 			'display': visible ? 'block' : 'none'	
		 		});
	 	},
	 	
	 	/**
	 	 * Calculated the position-dependent classes from the position of the elements.
	 	 *
	 	 * @param Object $tip Tooltip
	 	 * @param Object $el Parent element of the Tooltip
	 	 *
	 	 * @return String Position-dependent classes
	 	 */
	 	getTipClasses = function ($tip, $el) {
	 		var	tp,
		 		tw = $tip.outerWidth(),
		 		th = $tip.outerHeight(),
		 		visible = $tip.is(':visible'),
	 			ep = $el.offset(),
	 			ew = $el.outerWidth(),
	 			eh = $el.outerHeight(),
	 			tx = '',
	 			ty = '',
	 			step,
	 			off,
	 			pos,
	 			h,
	 			v;
	 		
	 		// fix to get current position
	 		tp = $tip.show().offset();
	 		tp = $tip.offset();
	 		
	 		if (!visible) {
	 			$tip.hide();
	 		}
	 		
	 		// horizontal
	 		if (tp.left < ep.left) {
	 			tx = 'left';
	 		} else if (tp.left >= ep.left && tp.left < ep.left + ew) {
	 			tx = 'center';
	 		} else if (tp.left >= ep.left + ew) {
	 			tx = 'right';
	 		}
	 		
	 		// vertical
	 		if (tp.top < ep.top) {
	 			ty = 'top';
	 		} else if (tp.top >= ep.top && tp.top < ep.top + eh) {
	 			ty = 'center';
	 		} else if (tp.top >= ep.top + eh) {
	 			ty = 'bottom';
	 		}
	 		
	 		// check horizontal center direction
	 		if (tx == 'center') {
	 			step = ew / 3;
	 			off = tp.left - ep.left;
	 			pos = Math.round(off / step);
	 			
	 			// arrow center
	 			if (Math.round((ew  - tw) / 2) == off) {
	 				pos = 1;
	 			}
				
	 			h = 'a_' + xClasses[pos];
	 			v = opposites[ty];
	 			
	 			return 'tip_p_' + v + ' ' + h;
	 		}
	 		// check vertical center direction
	 		else if (ty == 'center') {
	 			step = eh / 3;
	 			off = tp.top - ep.top;
	 			pos = Math.round(off / step);
	 			
	 			h = 'a_' + yClasses[pos];
	 			v = opposites[tx];
	 			
	 			return 'tip_p_' + v + ' ' + h;
	 		}
	 		// check if tooltip is wider than the element
	 		else if (ew <= tw) {
	 			tx = 'center';
	 			
	 			step = ew / 3;
	 			off = tp.left - ep.left;
	 			pos = Math.round(off / step);
	 			
	 			pos = pos >= 0 ? pos : 1;
	 			
	 			h = 'a_' + xClasses[pos];
	 			v = opposites[ty];
	 			
	 			return 'tip_p_' + v + ' ' + h;
	 		}
	 		// check corners
	 		else if (tx.search(/left|right/) !== -1 && ty.search(/top|bottom/) !== -1) {
	 			return 'c_' + tx + ' c_' + ty;
	 		}
	 		
	 		return '';
	 	},
	 	
	 	/**
	 	 * Calculated the position-dependent classes from the available options.
	 	 * The position can be changed by the collision detection.
	 	 *
	 	 * @param Object Position-Options from $.fn.position
	 	 *
	 	 * @return String Position-dependent classes 
	 	 */
	 	getTipTempClasses = function (p) {
	 		p.my = p.my != undefined ? p.my : defaultPosition.my;
	 		
	 		var tipClassPrefix = 'tip_',
	 			h_class,
	 			v_class,
	 			h = p.my.split(' ')[0];
	 			v = p.my.split(' ')[1];
	 		
	 		// corners
	 		if ('center' != opposites[h] && opposites[h] != p.at.split(' ')[0] && opposites[v] != p.at.split(' ')[1]) {
	 			return 'tip_p_corner';
	 		}
	 		// left/right
	 		if (h != p.at.split(' ')[0] && h != 'center') {	 			
	 			return 'tip_p_' + h;
	 		} 
	 		// top/bottom
	 		else if (v != p.at.split(' ')[1]) {
	 			return 'tip_p_' + opposites[p.at.split(' ')[1]];
	 		}
	 		
	 		return '';
	 	},
	 	
	 	/**
	 	 * Sets the contents of the tooltip.
	 	 * The content can be passed as a string or using a callback function. 
	 	 * The parameters of the function is the content element.
	 	 *
	 	 * @example
	 	 *	set_content(data, 'new content');
	 	 *
	 	 * @example
	 	 *	set_content(data, function ($content) {
	 	 *		$content.find('h1').html('new title');
	 	 *		return $content.html();
	 	 *	});
	 	 * 
	 	 * @param Object tooltip data of the element
	 	 * @param String/Function content
	 	 */
	 	set_content = function (data, content) {
	 		data.org_title = content;
	 		if (typeof content == 'string') {
	 			data.tiptip_content.html(content);
	 		}
	 		else if ($.isFunction(content)) {
	 			data.tiptip_content.html(content(data.tiptip_content));
	 		}
	 	},
	 		 	
	 	active_tiptip = function(org_elem) {
	 		var data = org_elem.data('tipTip'),
	 			visible = data.tiptip_holder.is(':visible');
	 			
	 		data.opts.enter.call(this);
	 		set_content(data, data.org_title);
	 		
 			update_position(org_elem, data);
 			
 			if (timeout[data.id]) {
 				clearTimeout(timeout[data.id]);
 			}
 			
 			if (visible) {
				data.tiptip_holder
//					.stop(true,true)
					.show();
			}
			else {
 				timeout[data.id] = setTimeout(function() {
					data.tiptip_holder
	//					.stop(true,true)
						.fadeIn(opts.fadeIn);
				}, data.opts.delay);
			}
 		},
 		
 		deactive_tiptip = function(org_elem) {
 			var data = org_elem.data('tipTip');
 			//console.log(data);
 			if (data != undefined) {
 				data.opts.exit.call(this);
 				if (timeout[data.id]){ clearTimeout(timeout[data.id]); }
 				data.tiptip_holder.fadeOut(opts.fadeOut);
 			} else {
 				$('.tiptip_holder').fadeOut(opts.fadeOut);
 				console.error('Data fehlt.');
 			}
 		}
	 	
	 	methods = {
		 
		 	init: function () {
			 	
			 	opts.activation = opts.activation.toString().split(' ');
			 	//opts.defaultPosition = opts.defaultPosition.toString().split(' ');
			 	
				return this.each(function () {
				
					var thisIdCount,
						data = $(this).data('tipTip');

					if (data && data.id) {
						data.id = thisIdCount;
					} else {
						thisIdCount = idCount;
						idCount++;
					}
					
					// Setup tip tip elements and render them to the DOM
					if($('.tiptip_holder[rel=' + thisIdCount + ']').length <= 0){
						var tiptip_holder = $('<div class="tiptip_holder" rel="' + thisIdCount + '" style="max-width:'+ opts.maxWidth +';"></div>');
						
						tiptip_holder.data('el' , $(this));
						
						var tiptip_content = $('<div class="tiptip_content"></div>');
						var tiptip_arrow = $('<div class="tiptip_arrow"></div>');
						
						$('body').append(tiptip_holder.html(tiptip_content).prepend(tiptip_arrow.html('<div class="tiptip_arrow_inner"></div>')));
					} else {
						var tiptip_holder = $('.tiptip_holder[rel=' + thisIdCount + ']');
						var tiptip_content = $('.tiptip_content', tiptip_holder);
						var tiptip_arrow = $('.tiptip_arrow', tiptip_holder);
					}
					
					if ($.browser.msie && parseInt($.browser.version,10) < 7) { 
						tiptip_arrow.remove();
					}
					
					var org_elem = $(this);
					
					if(opts.content){
						var org_title = opts.content;
					} else {
						var org_title = org_elem.attr(opts.attribute);
					}
					
					tiptip_holder.hide();
					
					// save data
					org_elem.data('tipTip', {
						tiptip_holder	: tiptip_holder,
						tiptip_content	: tiptip_content,
						tiptip_arrow	: tiptip_arrow,
						org_title		: org_title,
						opts			: opts,
						id				: thisIdCount
					});
					
					if (org_title != '') {
						if (!opts.content) {
							org_elem.removeAttr(opts.attribute); //remove original Attribute
						}
						timeout[thisIdCount] = false;
						
						if ($.inArray('hover', opts.activation) > -1) {
							org_elem
								.bind(namespacedEvent('mouseenter'), function () {
									active_tiptip(org_elem);
								})
								.bind(namespacedEvent('mouseleave'), function () {
									if(!opts.keepAlive && !opts.internalKeepAlive){
										deactive_tiptip(org_elem);
									}
								});

							if (opts.keepAlive) {
								tiptip_holder.hover(function(){}, function(){
									deactive_tiptip(org_elem);
								});
							}
						}
						
						if ($.inArray('focus', opts.activation) > -1) {
							org_elem
								.bind(namespacedEvent('focus'), function () {
									if ($.inArray('hover', opts.activation) > -1) {
										opts.internalKeepAlive = true;
									}
									active_tiptip(org_elem);	
								})
								.bind(namespacedEvent('blur'), function () {
									if ($.inArray('hover', opts.activation) > -1) {
										opts.internalKeepAlive = false;
									}
									deactive_tiptip(org_elem);
								});
						}
						
						if ($.inArray('click', opts.activation) > -1) {
							org_elem
								.bind(namespacedEvent('click'), function () {	
									active_tiptip(org_elem);
									return false;
								})
								.bind(namespacedEvent('mouseleave'), function () {
									if(!opts.keepAlive && !opts.internalKeepAlive){
										deactive_tiptip(org_elem);
									}
								});

							if (opts.keepAlive) {
								tiptip_holder.hover(function () {}, function () {
									deactive_tiptip(org_elem);
								});
							}
						}
					}
				});
			},
			
			close: function () {
				return this.each(function () {
					var $el = $(this);
					deactive_tiptip($el);
				});
			},
			
			open: function () {
				return this.each(function () {
					var $el = $(this);
					active_tiptip($el);
				});
			},
			
			updatePosition: function (position) {
				position = position || false;
				
				return this.each(function () {
					var $this	= $(this),
						data	= $this.data('tipTip');

					if (typeof position === 'string' || typeof data.opts.position === 'string') {
						data.opts.position = position;
					} else {
						data.opts.position = $.extend({}, data.opts.position, position);
					}

					update_position($this, data);
				});
			},
			
			/**
			 * Sets the contents of the tooltip.
			 *
			 * @see set_content()
			 */
			updateContent: function (content) {
				return this.each(function () {
					var $this	= $(this),
						data	= $this.data('tipTip');
					
					set_content(data, content);
				});
			},
			
			destroy: function () {
				return this.each(function () {
					var $el		= $(this);
						data	= $el.data('tipTip');
					
					deactive_tiptip($el);
					
					// wait for fading
					setTimeout(function () {
						data.tiptip_holder.remove();
						$el.removeData('tipTip');
						$el.unbind('.' + namespace);
					}, data.opts.fadeOut + 100);
				});
			}
		};
		
		method = method || {};
		
		if (methods[method]) {
			return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
		}
		else if (typeof method === 'object' || !method) {
			return methods.init.apply(this, arguments);
		}
		else {
			console.error('undefined method %s', method);
		}		
	}
})(jQuery);  	
