/*
	FILENAME: weldMenu.js
	AUTHOR: Philip Siedow-Thompson (psiedow@co.weld.co.us)
	WRITTEN: 1/22/2009
	REVISIONS: none
	DEPENDENCIES: prototype.js v.1.6.0 (DO NOT USE ANY OTHER VERSION UNTIL TESTED!)
*/
weldMenu = {};


	/*------------------------------------------------------------------------------------
		DEFINE ENVIRONMENTAL VARS - override in a seperate file to change these values.
	--------------------------------------------------------------------------------------*/
	weldMenu.environmentalVars = {menuReprieveTime:0.5,		//Time menu stays open after mousout in sec
								  frameRate:30,				//Menu expansion framerate in frames/sec
								  expansionDuration:0.8,	//Time menus take to expand in sec.
								  menuOffsetLeft:145,		//Menu x-offest from parent node in px
								  menuOffsetTop:4};			//Menu y-offest from parent node in px

	/*------------------------------------------------------------------------------------
		ON LOAD - regesters with the onload handler and initiates menu builder.
	--------------------------------------------------------------------------------------*/
	weldMenu.onLoad = function()
		{
			var builder = new weldMenu.MenuBuilder();
				builder.build();
		}

	if(document.attachEvent) window.attachEvent("onload",weldMenu.onLoad);
		else {window.addEventListener("load",weldMenu.onLoad,false);}

/*------------------------------------------------------------------------------------
	DEFINE INTERPOLATION FUNCTIONS - these are used by the animation class to interpolate
										between the start and end values.
--------------------------------------------------------------------------------------*/
weldMenu.interpolate = {};
	
	/* 
		FUNCTION: linear
		DESCRIPTION: a linear interpolation function.
		ARGUMENTS: 	a_start:number - starting value
					a_delta:number - total change (end-start)
					a_step:number - current step/time
					a_duration:number - total steps/time
	*/
	weldMenu.interpolate.linear = function(a_start,a_delta,a_step,a_duration)
		{
			return (a_delta * (a_step/a_duration)) + a_start;		
		}
		
	/* 
		FUNCTION: exponential
		DESCRIPTION: an exponential interpolation function.
		ARGUMENTS: 	a_start:number - starting value
					a_delta:number - total change (end-start)
					a_step:number - current step/time
					a_duration:number - total steps/time
	*/
	weldMenu.interpolate.exponential = function(a_start,a_delta,a_step,a_duration)
		{
			return (a_step>=a_duration)?a_start + a_delta: a_delta * 1.001 * (-Math.pow(2, -10 * a_step/a_duration) + 1) + a_start;
		}
		
		
/*------------------------------------------------------------------------------------
	WELDMENU.ANIMATE - an animation function (interpolates between points and set the
						appropriate styles on each frame.
--------------------------------------------------------------------------------------*/	
weldMenu.animate = Class.create({
	
	/* 
		FUNCTION: constructor
		DESCRIPTION: initializes a animation object for a node.
		ARGUMENTS:	a_node:node - the block displayed node to animate.
					a_interp:function - one of the interpolation functions.
					a_framesPerSec - the frame rate of the animation function in frames/sec.
	*/
		initialize: function(a_node,a_interp,a_framesPerSec)
		{
			this.timer = null;
			this.startVals = null;
			this.deltaVals = null;
			this.length = 0;
			this.firstFrameTime = 0;
			this.frameRate = (a_framesPerSec && a_framesPerSec>0 && a_framesPerSec<1000)?Math.ceil(1000/a_framesPerSec):16;
			this.interp = (a_interp)?a_interp:weldMenu.interpolate.linear;
			this.node = $(a_node);
				if(!this.node)throw new Error('The node passed to weldMenu.animate is not valid.');
		},
		
	/* 
		FUNCTION: isAnimating
		DESCRIPTION: Indicates whether the object is currently animating.
		ARGUMENTS: none
		RETURNS: boolean
	*/
		isAnimating: function()
		{
			return (this.timer!=null);
		},
		
	/* 
		FUNCTION: onFrame
		DESCRIPTION: PRIVATEish - called on each frame, performs the lerp and sets the styles for the node.
		ARGUMENTS: none
		RETURNS: none
	*/
		onFrame: function()
		{
			var cTime = new Date().getTime();
			var tDelta = cTime-this.firstFrameTime;
		
			for(var key in this.startVals)
			{
				var simp = {};
					simp[key] = Math.ceil(this.interp(this.startVals[key],this.deltaVals[key],tDelta,this.length)) + 'px';
				this.node.setStyle(simp);
			}
			
			if(tDelta>=this.length)
			{
				window.clearInterval(this.timer);
				this.timer = null;
			}
		},
		
	/* 
		FUNCTION: animate
		DESCRIPTION: Call to initiate an animation.
		ARGUMENTS:	a_start:object - an object containing starting key-value pairs.
					a_end:object - an object containing starting key-value pairs.
					a_sec:number - the duration of the animation
		RETURNS: none
	*/
		animate: function(a_start,a_end,a_sec)
		{
			var thisRef = this;
			
			this.firstFrameTime = new Date().getTime();
			this.length = (a_sec>0)?1000 * a_sec:0;
			this.startVals = {};
			this.deltaVals = {};
			for(var key in a_start)
			{
				if(a_end[key])
				{
					this.startVals[key] = a_start[key];
					this.deltaVals[key] = a_end[key]-a_start[key];
				}
			}
			
			this.onFrame();
			this.timer = window.setInterval(function(){thisRef.onFrame();},this.frameRate);
		}
						
});


/*------------------------------------------------------------------------------------
	WELDMENU.KILLTIMER - A timer that initiates multiple callbacks on completion
--------------------------------------------------------------------------------------*/	
weldMenu.killTimer = Class.create({
								  
	/* 
		FUNCTION: constructor
		DESCRIPTION: creates a kill timer with a particular duration.
		ARGUMENTS: a_duration:number - number of seconds to run
	*/
		initialize: function(a_duration)
		{
			this.duration = (a_duration>0)?a_duration*1000:1000;
			this.timer = null;
			this.callbacks = new Array();
		},
		
	/* 
		FUNCTION: startTimer
		DESCRIPTION: starts the timer and adds callback, this can be called multiple times when the timer is running.
		ARGUMENTS: a_calback:function - a no argument callback to be notified on timer completion.
		RETURNS: none
	*/
		startTimer: function(a_callback)
		{	
			if(!this.timer)
			{
				var thisRef = this;
				this.timer = window.setTimeout(function(){thisRef.notify();},this.duration);
			}
			this.callbacks.push(a_callback);	
		},
						
	/* 
		FUNCTION: stopTimer
		DESCRIPTION: stops and clears the timer, but does not notify the callbacks of completion
		ARGUMENTS: none
		RETURNS: none
	*/
		stopTimer: function()
		{
			this.callbacks = new Array();
			if(this.timer)
			{
				window.clearTimeout(this.timer);
				this.timer = null;
			}
		},
		
	/* 
		FUNCTION: notify
		DESCRIPTION: stops the timer and notifies callbacks!
		ARGUMENTS: none
		RETURNS: none
	*/
		notify: function()
		{
			for(var i=0; i< this.callbacks.length; i++)
			{
				this.callbacks[i]();
			}
			this.stopTimer();
		},
	
	/* 
		FUNCTION: isRunning
		DESCRIPTION: returns true if timer is running.
		ARGUMENTS: none
		RETURNS: boolean
	*/
		isRunning: function()
		{
			return (this.timer!=null);
		}
});

/*------------------------------------------------------------------------------------
	WELDMENU.MENUNODE - The flyout menu nodes.
--------------------------------------------------------------------------------------*/
weldMenu.MenuNode = Class.create({

	/* 
		FUNCTION: constructor
		DESCRIPTION: creates a menu wrapper for a node.
		ARGUMENTS: a_node:node - the menus node
	*/
		initialize: function(a_node)
		{
			this.parent = null;
			this.children = new Array();
			this.timer = new weldMenu.killTimer(weldMenu.environmentalVars.menuReprieveTime);
			this.node = $(a_node);
				if(!this.node)throw new Error('The node passed to weldMenu.MenuNode is not valid.');
			
			var thisRef = this;
			this.node.onmouseout = function(){thisRef.menuClose();}
			this.node.onmouseover = function(){thisRef.menuReprieve();}
			
			this.dimensions = this.node.getDimensions();
			this.animate =  new weldMenu.animate(this.node,weldMenu.interpolate.exponential,weldMenu.environmentalVars.frameRate);
			this.isOpen = false;
		},
		
	/* 
		FUNCTION: show
		DESCRIPTION: makes the menu animate and be visible, calling menuOpen is prefered.
		ARGUMENTS: none
		RETURNS: none
	*/
		show: function()
		{
			this.menuReprieve();
			var positionParent = $(this.node.parentNode);
			if(positionParent && !this.isOpen)
			{
				this.isOpen = true
				var offsets = positionParent.positionedOffset();
				this.node.setStyle({left:offsets.left + weldMenu.environmentalVars.menuOffsetLeft + 'px', top:offsets.top + weldMenu.environmentalVars.menuOffsetTop + 'px'});
				this.node.setStyle({overflow:'hidden'});
				this.animate.animate({height:1,width:1},{height:this.dimensions.height, width:this.dimensions.width},weldMenu.environmentalVars.expansionDuration);
			}
			this.node.setStyle({visibility:'visible'});
		},
		
	/* 
		FUNCTION: hide
		DESCRIPTION: hides the menu. calling menuClose or menuKill is prefered.
		ARGUMENTS: none
		RETURNS: none
	*/
		hide: function()
		{
			this.node.setStyle({visibility:'hidden'});
		},

	/* 
		FUNCTION: menuOpen
		DESCRIPTION: makes menu visible and closes other menus, keeps children alive.
		ARGUMENTS: none
		RETURNS: none
	*/
		menuOpen: function()
		{
			if(this.timer.isRunning())this.timer.stopTimer();
			
			if(this.parent)
				{
					var siblings = this.parent.getChildren();
					for(var i=0; i<siblings.length; i++)
					{
						if(siblings[i]!=this)siblings[i].menuKill();
					}
					
					this.parent.menuReprieve();
				}
										
			this.show();
		},
		
	/* 
		FUNCTION: menuClose
		DESCRIPTION: hides the menu after a length of time.
		ARGUMENTS: none
		RETURNS: none
	*/
		menuClose: function()
		{
			if(!this.timer.isRunning())
			{
				var thisRef = this;
				this.timer.startTimer(function(){thisRef.menuKill();});
			}
		},
		
	/* 
		FUNCTION: menuKill
		DESCRIPTION: imediately hides the menu.
		ARGUMENTS: none
		RETURNS: none
	*/
		menuKill: function()
		{
			for(var i=0; i<this.children.length; i++)
			{
				this.children[i].menuKill();
			}
			
			this.isOpen = false;
			this.hide();
		},
		
	/* 
		FUNCTION: menuReprieve
		DESCRIPTION: keeps the menu from closing temporarily
		ARGUMENTS: none
		RETURNS: none
	*/
		menuReprieve: function()
		{
			this.timer.stopTimer();
			if(this.parent)this.parent.menuReprieve();
		},
		
	/* 
		FUNCTION: getChildren
		DESCRIPTION: returns any menu nodes that are children of the current node
		ARGUMENTS: none
		RETURNS: array of menuNodes
	*/
		getChildren: function()
		{
			return this.children;
		},
		
	/* 
		FUNCTION: addChild
		DESCRIPTION: adds a child to the child array for the menu
		ARGUMENTS: a_child:menuNode 
		RETURNS: menuNode
	*/
		addChild: function(a_child)
		{
			this.children.push(a_child);
			a_child.setParent(this);
			return a_child;
		},
		
	/* 
		FUNCTION: getParent
		DESCRIPTION: returns the parent node
		ARGUMENTS: none
		RETURNS: menuNode|menuRoot
	*/
		getParent: function()
		{
			return this.parent;
		},
		
	/* 
		FUNCTION: setParent
		DESCRIPTION:
		ARGUMENTS: a_parent:menNode|rootNode
		RETURNS: none
	*/
		setParent: function(a_parent)
		{
			this.parent = a_parent;
		}
});

/*------------------------------------------------------------------------------------
	WELDMENU.MENUROOT - a menuNode that cannot be displayed, used as a place holder
						for parent nodes of the menuNodes. There should only be one
						instance.
--------------------------------------------------------------------------------------*/
weldMenu.MenuRoot = Class.create({
								 
	/* 
		FUNCTION: constructor
		DESCRIPTION: initializes class.
		ARGUMENTS:
		RETURNS:
	*/
		initialize: function()
		{
			this.children = new Array();
		},
		
	/* 
		FUNCTION: menuReprieve
		DESCRIPTION: required for compatibility with menuNodes, causes no action.
		ARGUMENTS: none
		RETURNS: none
	*/
		menuReprieve: function()
		{
			//This must accept but ignores reprieve calls
		},
		
	/* 
		FUNCTION: getChildren
		DESCRIPTION: returns the child list.
		ARGUMENTS: none
		RETURNS: array:menuNodes
	*/
		getChildren: function()
		{
			return this.children;
		},
		
	/* 
		FUNCTION: addChild
		DESCRIPTION: adds a child to the child list
		ARGUMENTS: a_child:menu node
		RETURNS: none
	*/
		addChild: function(a_child)
		{
			this.children.push(a_child);
			a_child.setParent(this);
			return a_child;
		}
});

/*------------------------------------------------------------------------------------
	WELDMENU.MENUTRIGGER - link between dom nodes and menuNodes, captures mouseevents
							and comunicates with the menNodes (open and close)
--------------------------------------------------------------------------------------*/
weldMenu.MenuTrigger = Class.create({
									
	/* 
		FUNCTION: constructor
		DESCRIPTION: initializes the trigger, binds it to a nodes.
		ARGUMENTS: a_node:node - the node to capture mouse events on.
	*/
		initialize: function(a_node)
		{
			this.menu = null;
			this.node = $(a_node);
				if(!this.node)throw new Error('The node passed to weldMenu.MenuNode is not valid.');
		
			var thisRef = this;
			this.node.onmouseover = function(){thisRef.openMenu();};
			this.node.onmouseout = function(){thisRef.closeMenu();};
		},
		
	/* 
		FUNCTION: openMenu
		DESCRIPTION: opens the bound menuNode (called from onmouseover)
		ARGUMENTS: none
		RETURNS: none
	*/
		openMenu: function()
		{
			if(this.menu)this.menu.menuOpen();
		},
		
	/* 
		FUNCTION: closeMenu
		DESCRIPTION: closes the bound menu (called from onmousout)
		ARGUMENTS: none
		RETURNS: none
	*/
		closeMenu: function()
		{
			if(this.menu)this.menu.menuClose();
		},
		
	/* 
		FUNCTION: setMenu
		DESCRIPTION: binds a menu to the trigger. Only one menuNode is allowed per trigger.
		ARGUMENTS: a_menu:menuNode
		RETURNS: none
	*/
		setMenu: function(a_menu)
		{
			this.menu = a_menu;
		}
});

/*------------------------------------------------------------------------------------
	WELDMENU.MENUBUILDER - Automatically builds menNodes, triggers and the root node and
							binds them together. call on onLoad.
--------------------------------------------------------------------------------------*/
weldMenu.MenuBuilder = Class.create({
									
	/* 
		FUNCTION: constructor
		DESCRIPTION: initializes the builder
		ARGUMENTS: none
		RETURNS: none
	*/
		initialize: function()
		{
			this.root = new weldMenu.MenuRoot();
		},
		
	/* 
		FUNCTION: build
		DESCRIPTION: performs all building operation, ONLY CALL ONCE ONLOAD!
		ARGUMENTS: none
		RETURNS: none
	*/
		build: function()
		{
			var triggers = $$('.weldMenu_trigger');
			for(var i=0; i<triggers.length; i++)
			{
				var triggerInst = new weldMenu.MenuTrigger(triggers[i]);
				
				var siblings = triggers[i].nextSiblings();
				for(var j=0; j<siblings.length; j++)
				{
					if(siblings[j].hasClassName('weldMenu_flyout'))
					{
						var menuNode = new weldMenu.MenuNode(siblings[j]);
						this.root.addChild(menuNode);
						triggerInst.setMenu(menuNode);
						break;
					}
				}
			}
		}
});
