﻿/* 
AjaxDetailTip
---------
Description: 
A detail tip class that uses positiontip and iFrameShimFix for extension/composites.
This class is forked from DetailTip Class v0.4 for ajax functionality and needs merged.
created: 			6/2008 timw
last modified: 	9/23/2008 timw
version:				0.2
*/
var AjaxDetailTip = Class.create({
	// elTip : the id of the tip container (will be created if it doesn't exist in dom)
	// elTriggers : an array of elements that trigger showing a detail tip (must have rel="dt_#").
	initialize: function(elTip, elTriggers, options){
		this.options = Object.extend({
			delay: 0.4,								// Delay before tip is shown or hidden
			useEffects: false,					// use Fade/Appear effects?
			useClose: true,						// add a close link?
			triggerEvent: 'mouseover',			// which event to trigger the showing of a tip? ('mouseover' or 'click')
			useMouseMove: false,					// adjust the position on mouse move?
			useAjax: true,							// wether or not the content comes through xhr
			xhrUrl: rootVirtual + 'calendar/popover.aspx',// xhr url to retrieve content
			paramKeyName: 'performanceNumber'// querystring label for passing id's to xhr page
		}, options || {});
		
		// extend nested position preferences.
		this.positionPrefs = Object.extend({
			orientation: 'east',
			xOffset: 0,	
			yOffset: 0,
			minMargin: 0,
			useCentering: true,
			useArrow: true,
			arrowClassName: 'dt_arrow',
			arrowDimensions: {width: 20, height: 20}
		}, this.options.positionPrefs || {});
		
		this.elTip = $(elTip) || false;
		
		this.elTriggers = elTriggers;
		
		// element check
		if(this.elTriggers.length == 0){return;}
		
		if(!this.elTip){
			// tip doesn't exist, create it as first child of body
			this.elTip = new Element('div', {id: 'detailtip'});
			var tipMarkup = 
				((this.positionPrefs.useArrow) ? '<div class="dt_arrow"></div>': '')
				+ '<div class="dt_head">'
				+ ((this.options.useClose) ? '<div class="dt_close"><a href="#close#">close</a></div>' : '')
				+ '</div>'
				+ '<div class="detailtip_content"><!-- content will go here --></div>'
				+ '<div class="dt_foot"></div>'
			;
			this.elTip.update(tipMarkup);
			// generic markup only includes div.detailtip_content
			//this.elTip.insert({bottom: new Element('div', {className: 'detailtip_content'})});
			$(document.body).insert({top: this.elTip });
		}
		else if(this.elTip.parentNode.nodeName != 'BODY'){
			// make sure tooltip is first child of body to avoid z-index issues
			$(document.body).insert({top: this.elTip });
		}
		if(!this.elTip.down('div.detailtip_content')){
			// make sure we have a content element
			this.elTip.insert({bottom: new Element('div', {className: 'detailtip_content'})});
		}
		
		this.elTipContent = this.elTip.down('div.detailtip_content');
		this.elTip.hide();
		this.isVisible = false;
		
		// attach event handlers
		this.elTip.observe('mouseover', this.__tipOver.bindAsEventListener(this));
		this.elTip.observe('mouseout', this.__tipOut.bindAsEventListener(this));
		
		var handle = this.__handleTriggerEvents.bindAsEventListener(this);
		this.elTriggers.invoke('observe', this.options.triggerEvent, handle);
		
		if(this.options.useClose){
			var lnkClose = this.elTip.down('div.dt_close a');
			lnkClose.observe('click', this.__closeClick.bindAsEventListener(this));
			// also listen to mouseout for auto-closing when another trigger is moused over.
			this.elTriggers.invoke('observe', 'mouseout', handle);
		}
		else{
			this.elTriggers.invoke('observe', 'mouseout', handle);
		}
		
		if(this.options.useMouseMove){
			this.elTriggers.invoke('observe', 'mousemove', handle);
		}
		// body click for user clicking outside tip to close tip
		Event.observe(document.body, 'click', this.__bodyClick.bindAsEventListener(this));
		
		// private members
		this._tpm = new TipPositionManager(this.positionPrefs); // class instance for managing tip positioning.
		this._isIE6 = false /*@cc_on || @_jscript_version < 5.7 @*/;		
		this._inTip = false;
		this._inTrigger = false;
		this._timeoutId = null;
		this._currentDetailKey = -1;
		this._effect = null;
		this._contentCache = $H();
		this._lastTriggerIndex = null;
		this._keyInTransit = false;
		this._approxXhrLatencyMS = 200;//should be no more than this.options.delay
	},
	
	__bodyClick: function(e){
		// note:e.relatedTarget is the node to which the pointer went, e.currentTarget is the node to which the event is attached
		var el = e.element();
		//may need to also make sure el is not in a trigger (this.elTriggers) before hiding.
		if(this.isVisible && !el.descendantOf(this.elTip)){
			this.hideTip();
		}
	},
	
	__tipOver: function(e){
		this._inTip = true;
	},

	__tipOut: function(e){
		if(e.relatedTarget && !e.relatedTarget.descendantOf(this.elTip)){
			this._inTip = false
		}
		if(!this.options.useClose){
			// auto-close
			this._clearTimer();
			this._timeoutId = setTimeout(this.hideTip.bind(this), this.options.delay*1000);			
		}
	},
	
	__closeClick: function(e){
		e.stop();
		this.hideTip(true);
	},
	
	__handleTriggerEvents: function(e){
		var trigger = e.element();
		// console.log(e.type);
		// make sure we have the trigger, not a child node.
		while(this.elTriggers.indexOf(trigger) == -1 && trigger.nodeName != 'BODY'){
			trigger = trigger.up();
		}
		var triggerIndex = this.elTriggers.indexOf(trigger);
		
		// mouseover or click
		/////////////////////
		if(e.type == this.options.triggerEvent){
			this._inTrigger = true;
			var isNewTrigger = this._lastTriggerIndex !== triggerIndex;
	
			var key = this.getTipKey(trigger); // key == tipID
			if(!key || this._inTip){ console.log('invalid key in trigger'); return; }// invalid key or user is in the tip
			
			var isNewKey = (this._currentDetailKey != key); // requesting a different key id (could already exist)
			
			//console.log('this.isVisible: ' + this.isVisible + ', isNewKey: ' + isNewKey + ', isNewTrigger: ' + isNewTrigger + ', this._keyInTransit: ' + this._keyInTransit + ', this._timeoutId: ' + this._timeoutId);

			if(isNewTrigger){
				this._lastTriggerIndex = triggerIndex;
				this._currentDetailKey = key;
				this._tpm.setPosition(trigger, this.elTip, {left: e.clientX, top: e.clientY});
				if(this._isIE6){
					this._tpm.iFrameShimFix('activate', this.elTip);
				}
			}
			
			if(isNewKey && this.isVisible){
				//console.log('forcing hide of previous detail');
				if(this.options.useEffects && this._effect){
					this._effect.cancel();
				}
				this.hideTip(true);
			}

			this._clearTimer();
			
			if(isNewKey){
				// we have a new id that isn't in the cache and we want to use ajax
				if(!this._contentCache.get(key) && this.options.useAjax){
					this._timeoutId = setTimeout(this.xhrRequestContent.bind(this, key), ((this.options.delay*1000) - this._approxXhrLatencyMS));
				}
				else{
					// non-ajax
					var content = this.getLocalContent(key);
					this.updateContent(content);
					this._timeoutId = setTimeout(this.showTip.bind(this, key), this.options.delay*1000);
				}
			}
			else{
				if(this.options.useAjax && key !== this._keyInTransit){
					if(!this._contentCache.get(key)){
						// deal with trigger hover, then off, then back on again before content was requested
						//console.log('not a new key, not in transit, not in cache, go get it');
						this._timeoutId = setTimeout(this.xhrRequestContent.bind(this, key), ((this.options.delay*1000) - this._approxXhrLatencyMS));						
					}
					else{
						//console.log('not a new key, not in transit, but exists in cache--put showTip on timer');					
						this._timeoutId = setTimeout(this.showTip.bind(this, key), this.options.delay*1000);
					}				
				}
				else{
					// non-ajax old key (TODO: need to double-check this logic for non-ajax)
					this._timeoutId = setTimeout(this.showTip.bind(this, key), this.options.delay*1000);
				}
			}
						
			//removed from here

		}
		// mousemove
		/////////////////////
		else if(e.type == 'mousemove'){
			// reposition the tip according to mouse movement
			this._tpm.setPosition(trigger, this.elTip, {left: e.clientX, top: e.clientY});
			if(this._isIE6){
				this._tpm.iFrameShimFix('activate', this.elTip);
			}
		}
		// mouseout
		/////////////////////
		else if(e.type == 'mouseout'){
			this._inTrigger = false;

			this._clearTimer();
			if(!this.options.useClose){
				this._timeoutId = setTimeout(this.hideTip.bind(this), this.options.delay*1000);
			}
		}
	},
	
	getTipKey: function(trigger){
		return trigger.readAttribute('rel') || null;
	},
	
	getLocalContent: function(tipID){
		//console.log('getting local content for ' + tipID);
		if(!this._contentCache.get(tipID)){
			//add to cache
			this._contentCache.set(tipID, $(tipID).innerHTML);
		}
		return this._contentCache.get(tipID);
	},
	
	xhrRequestContent: function(tipID){
		//console.log('getting remote content for ' + tipID);
		var recordID = /\d+$/.exec(tipID); // parse the performance id from tipID (e.g. dt_629)
		new Ajax.Request( this.options.xhrUrl,{
			method: 'get',
			parameters: this.options.paramKeyName + '=' + recordID,
			onSuccess: this.__xhrSuccess.bind(this),
			onFailure: this.__xhrFailure.bind(this)
		});
		this._keyInTransit = tipID;
	},
	
	__xhrSuccess: function(transport){
		// save response
		var form = transport.responseXML.getElementsByTagName('form')[0];
		// get the id for this response
		var recordID = parseInt(transport.responseXML.getElementsByTagName('perfno')[0].firstChild.nodeValue);
		var tipID = 'dt_' + recordID;// markup to following convention rel="dt_#"
		this._keyInTransit = false;
		this._clearTimer();
		
		for (var i=0; i < form.childNodes.length; i++) {
			var node = form.childNodes[i];
			if (node.nodeType == 4) {// 4 = CDATA section
				// save response in dom cache.
				this._contentCache.set(tipID, node.nodeValue);
				// make sure the user hasn't moved on.
				if(this._currentDetailKey == tipID){
					this.updateContent(node.nodeValue);
					this.showTip(tipID);
				}

				break;
			}
		}
	},
	
	__xhrFailure: function(transport){
		this._keyInTransit = false;
		console.log('something went wrong with the xhr request.\n"'+ transport.status+':'+transport.statusText+'"\ntransport:');
		console.log(transport);
		this.hideTip();
	},
	
	updateContent: function(content){
		this.elTipContent.update(content);
	},
	
	showTip: function(tipID){
		//console.log('show failed for ' + tipID);
		if(this._inTrigger && (tipID == this._currentDetailKey)){
			//console.log('show passed for ' + tipID);
			if(this.options.useEffects){
				this._effect = new Effect.Appear(this.elTip, {duration: 0.15});
			}
			else{
				this.elTip.show();
			}
			
			this.isVisible = true;
		}
	},
	
	hideTip: function(force){
		//console.log('hide fired');
		if(!this._inTip || (force === true)){
			if(this.options.useEffects && force != true){
				this._effect = new Effect.Fade(this.elTip, {duration: 0.15});
			}
			else{
				this.elTip.hide();
			}
			
			this.isVisible = false;
			
			if(this._isIE6){
				this._tpm.iFrameShimFix('deactivate', this.elTip);
			}			
		}
	},
	
	_clearTimer: function(){
		if(typeof(this._timeoutId) == 'number'){
			clearTimeout(this._timeoutId);
		}
		this._timeoutId == null;	
	}
});