// slideMenu.js

// Functions to turn a static hierachical HTML menu with no specific javascript markup
// (implemented using unordered lists) into a dynamic, collapsible, animated menu

// BUILDING THE DYNAMIC MENU

var menuContainerDiv = 'browsepropertyby'; // ID of menu container
var menuArray; // array of menu item objects
/*
The following menu:

- Item 1
--- SubItem 1.1
--- SubItem 1.2
- Item 2
--- SubItem 2.1
------ SubSubItem 2.1.2

represented in html as:

<div id="menuContainerDiv">
    <ul>
        <li>...Item 1...
            <ul>
                <li>...SubItem 1.1...</li>
                <li>...SubItem 1.2...</li>
            </ul>
        </li>
        <li>...Item 2...
            <ul>
                <li>...SubItem 2.1...
                    <ul>
                        <li>...SubSubItem 2.1.2...</li>
                    </ul>
                </li>
            </ul>
        </li>
    </ul>
</div>

would be placed in menuArray as:

menuArray[0] = Item 1
menuArray[0].children[0] = SubItem 1.1
menuArray[0].children[1] = SubItem 1.2
menuArray[1] = Item 2
menuArray[1].children[0] = SubItem 2.1
menuArray[1].children[0].children[0] = SubItem 2.1.2
*/

var activeItems; // array of currently active menu item objects 
var activeItemFound;

// set onclick listeners for all links in menu items (contained within div with ID menuDiv)
// that have children
function setLinkListeners(menuDiv)
{
	
	var menuContainerObj = getObject(menuDiv);
	// prevents flicker by setting invisible at first, then carrying out setup
	//menuContainerObj.style.visibility = 'hidden';
	
	// get actual ul (unordered list) object
	var menuObj = getFirstOccurenceIn('UL', menuContainerObj);
		
	// build a multidimensional array for the menu
	menuArray = buildArrayFrom(menuObj);
	
	// set up active items
	activeItems = new Object();
	
	activeItemFound = false;
	// add listeners and register active items
	applyFunctionToTree(menuArray, setupItem);
	
	// make sure one item is active
	if (!activeItemFound)
	{
	    // set the first menu item as active
	    menuArray[0].listItem.className = 'active';
	    activeItems['level0'] = menuArray[0].listItem;
	}
	// prevent flicker by setting visible now
	//menuContainerObj.style.visibility = 'visible';
}

// if list item is marked 'active', add to activeItems and propagate change up menu hierachy
function setupItem(listItemObj, level)
{
	// if listItem has class 'active'
	if (listItemObj.listItem.className == 'active')
	{
	    activeItemFound = true; // global var
		// register in activeItems array
		activeItems['level' + level] = listItemObj.listItem;
		
		// register parent lis and set classNames to active
		if (level > 0) 
		{
		    var parentLi = listItemObj.listItem;
		    for (var parentLevel = level - 1; parentLevel >= 0; parentLevel--)
		    {
		        // (2 x parentNode to jump to parent UL, then parent LI)
		        parentLi = parentLi.parentNode.parentNode;
		        parentLi.className = 'active';
		        activeItems['level' + parentLevel] = parentLi;
		    }
		}
	}

	// if link is not a leaf
	if (listItemObj.children != false)
	{
		// get anchor
		var link = getFirstOccurenceIn('A', listItemObj.listItem);
		// set onclick listener
		link.onclick = function() 
			       { 
			       	    return setMenuItemActive(listItemObj.listItem, level);
			       };
	}
}

// set menu item as active
function setMenuItemActive(listItem, level)
{
	// if not in activeItems array
	if (listItem != activeItems['level' + level] && !inMotion)
	{
		inMotion = true;
		// close previous item if it exists
		if (activeItems['level' + level])
		{
			closeAndOpenULs(getFirstOccurenceIn('UL', activeItems['level' + level]), 
					getFirstOccurenceIn('UL', listItem)
					);
		}
		else justOpenUL(getFirstOccurenceIn('UL', listItem));
				
		// register as active
		activeItems['level' + level] = listItem;
		return false;
	}
	else // it's already active - go to the link
	{ 
	    return true; 
	}
}

// ANIMATION/MOTION OF MENU

// variables for close/open ULs code

var closingIntervalID; // for timer
var openingIntervalID; // for timer
var cUL; // for closing UL obj
var oUL; // for opening UL obj
var oULHeight; // for opening UL offsetHeight
var cULHeight; // for closing UL offsetHeight
var inMotion = false; // boolean - are the uls in transition?

// velocity variables
var transitionTime = 0.2; // time taken for menu to transition (in seconds)
var intervalTime = 30; // in millis
// ensure transitionTime is a whole even number of intervalTime to avoid motion sideeffects
var realTime = Math.floor(((transitionTime * 1000) / intervalTime)) * intervalTime;
if (realTime % 2 != 0) realTime++;
var openAccel;
var closeAccel;
var velocity;

function closeAndOpenULs(closeUL, openUL)
{
	setupForClose(closeUL);
	setupForOpen(openUL);
	velocity = 0;
	closingIntervalID = setInterval("closeMenu()", 30);
}

function justOpenUL(openUL)
{
	setupForOpen(openUL);
	velocity = 0;
	openingIntervalID = setInterval("openMenu()", intervalTime);
}

function setupForOpen(openUL)
{
	openUL.style.overflow = 'hidden';
	
	// complete hack to get height - quickly switch on display, get height, then set height as 0 - is there a better way?
	openUL.style.display = 'block';
	// get full openUL height for later
	oULHeight = openUL.offsetHeight;
	
	// remove 3px gap
	openUL.style.height = '0px'; 
	openUL.style.margin = '0px';
	openUL.style.padding = '0px';
	openUL.style.border = '0px';
	
	oUL = openUL;
	
	// do speed calculations
	// accel = (distance - initial velocity(0)) / (1/2 time)
	openAccel = oULHeight / (realTime >> 1);
}

function setupForClose(closeUL)
{
	closeUL.style.overflow = 'hidden';
	closeUL.style.height = closeUL.offsetHeight + 'px';
	// get full closeUL height for later
	cULHeight = closeUL.offsetHeight;
	
	cUL = closeUL;
	
	// do speed calculations
	// accel = (distance - initial velocity(0)) / (1/2 time)
	closeAccel = closeUL.offsetHeight / (realTime >> 1);
}
	
function closeMenu()
{
	// still moving
	if (parseInt(cUL.style.height) > 0 && velocity >= 0)
	{
	    // avoid overshooting the target
		cUL.style.height = (parseInt(cUL.style.height) - Math.min(velocity, parseInt(cUL.style.height))) + 'px';
		velocity += closeAccel;
		
		// past halfway
		if (parseInt(cUL.style.height) < Math.floor(cULHeight >> 1)) 
		{
			// time to decellerate -> invert accel
			closeAccel = Math.abs(closeAccel) * -1;
		}
	}
	else 
	{
	    // closing menu section has stopped moving
	    
		// set all attributes as if ul was originally not active
		cUL.style.height = '';
		cUL.parentNode.className = '';
		cUL.style.display = 'none';
		clearInterval(closingIntervalID);
		// open menu ready to be opened
		velocity = 0;
		openingIntervalID = setInterval("openMenu()", intervalTime);
	}
}

function openMenu()
{
	// still moving
	if (parseInt(oUL.style.height) < oULHeight && velocity >= 0)
	{
	    // avoid overshooting destination
		oUL.style.height = (parseInt(oUL.style.height) + Math.min(velocity, oULHeight - parseInt(oUL.style.height))) + 'px';
		velocity += openAccel;
		
		// past halfway
		if (parseInt(oUL.style.height) > (oULHeight >> 1)) 
		{
			// time to decellerate -> invert accel
			openAccel = Math.abs(openAccel) * -1;
		}
	}
	else 
	{
	    // opening menu has stopped moving
		oUL.style.height = 'auto';
		clearInterval(openingIntervalID);
		inMotion = false;
	}
}

// DATA STRUCTURE FUNCTIONS

// apply given function (of form func(treeElement, level) ) to every item in the tree
function applyFunctionToTree(tree, func)
{
	var level = 0;
	applyFunctionToSubTree(tree, level, func);	
}

function applyFunctionToSubTree(subTree, baseLevel, func)
{
	for (var i = 0; i < subTree.length; i++)
	{
		var currElement = subTree[i];
		func(currElement, baseLevel);
		if (currElement.children != false)
		{
			applyFunctionToSubTree(currElement.children, baseLevel + 1, func);
		}
	}
}

// build a multidimensional array for a ul/li nested menu
function buildArrayFrom(menuObj)
{
	return buildLevelIn(menuObj);
}

function buildLevelIn(container)
{
	// array of level (and sub-levels) to return
	var retArray = new Array();
	// get top level links
	var topLevelLI = getFirstOccurenceIn('LI', container);
	var index = 0;
	
	while (topLevelLI != false)
	{
		retArray[index] = new Object();
		
		// place list item in array
		retArray[index].listItem = topLevelLI;
		
		
		// if it has valid children build next level
		var childUL = getFirstOccurenceIn('UL', topLevelLI);
		if (childUL != false)
		{
			retArray[index].children = buildLevelIn(childUL);
		}
		else retArray[index].children = false;
		
		// increment index
		index++;
		
		// next LI
		topLevelLI = getNextAdjacentOccurence('LI', topLevelLI);
	}
	
	return retArray;
}

// get first occurence of element with tagName == tagStr and container containingObj
function getFirstOccurenceIn(tagStr, containingObj)
{
	if (containingObj.firstChild) var tempObj = containingObj.firstChild;
	else 
	{
		//alert('Container has no children in getFirstOccurence(' + tagStr + ')');
		return;
	}
	
	while (tempObj.tagName != tagStr)
	{
		if (tempObj.nextSibling) tempObj = tempObj.nextSibling;
		else 
		{
			return false;
		}
	}
	
	if (tempObj != null) return tempObj;
}

// get next occurence of element with tagName == tagStr and sibling siblingObj
function getNextAdjacentOccurence(tagStr, siblingObj)
{
	if (siblingObj.nextSibling) var tempObj = siblingObj.nextSibling;
	else
	{
		return false;
	}
	
	while (tempObj.tagName != tagStr)
	{
		if (tempObj.nextSibling) tempObj = tempObj.nextSibling;
		else 
		{
			return false;
		}
	}
	
	if (tempObj != null) return tempObj;
}
	

// basic defensive function to get object referenced by string 'id'
function getObject(id)
{
	if (document.getElementById)
	{
		if (document.getElementById(id))
		{	
			return document.getElementById(id);
		}
		else
		{
			trace('Element with id = ' + id + ' does not exist');
		}
	}
	else
	{
		trace('getElementbyId not supported');
	}
}

// ensure that our onLoad functions do not override previously attached functions
function setOnLoad()
{
	if (window.onload != null) 
	{
	    // save previous functions
		var currentLoadFunc = window.onload;
		window.onload = function()
				{
				    // run previous functions
				    currentLoadFunc();

				    // slideMenu setup functions here
				    runOnLoad();
				};        
	}
	// no functions are set to run on load
	else
	{
	    window.onload = function()
				{
				    // slideMenu setup functions here
                    runOnLoad();
				};   
	}
}

function runOnLoad()
{
    setLinkListeners(menuContainerDiv);
}

// setup the functions that should run once the full page and DOM tree has loaded
setOnLoad();
