/*
 * AccordianHandler.js
 * Copyright MetroSpark.com 2005 | created by sunder : fraktion@yahoo.com
 * 
 * Javascript object containing all things necessary for an accordian ui element.  
 * This UI Element's behaviour was designed to have only one accordian tab open 
 * at a time but offer the flexibility to have many open at the start to show
 * the contents of the accordian.
 *
 * CSS:
 * .myTabClass { background-color: #c9c9cd ;padding: 4px;cursor: pointer;}
 * .myTabSelectedClass {background-color: #a8cf82 ;}
 * .myTabOverClass {background-color: #a8cf82;}
 * .myContentClass {height: 0px;overflow:auto;}
 *
 * HTML: 
 * <div id="myWrapperID">
 *      <div class="myTabClass myTabSelectedClass" selectedTab="true" style="height:300px;">Tab1</div>
 *      <div class="myContentClass" style="overflow:hidden;">....</div>
 *      <div class="myTabClass">Tab1</div>
 *      <div class="myContentClass" style="overflow:auto;">....</div>
 * </div>
 * 
 * SCRIPT: preferrably outside of markup in the head tag
 * <script type="text/javascript">
 *     // state the content height (300 in example) if you do not have 
 *     // an accordian open at start
 *     var accordian = new AccordianHandler("myWrapperID", 300);
 *     // typically these defaults can be set inside the js file itself, as you 
 *     // would want accordian elements to look similar across your site.
 *     acc1.accTabClass = "myTabClass";
 *     acc1.accTabSelectedClass = "myTabSelectedClass";
 *     acc1.accTabOverClass = "myTabOverClass";
 *     acc1.accContentClass = "myContentClass";
 * </script>
 *
 * HTML NOTE: 
 * selectedTab="" attribute is the only non standard html required.  Many
 * times the attribute is not even necessary if you want all tabs to open 
 * fully on first click.
 * 
 * HTML NOTE:
 * Explicit statement of overflow css required for some implementations in 
 * safari.  Overriding overflow property in js, or even changing the classname
 * to one that has a different overflow property was not adding scrollbars.  
 * Only explicitly stating inline in the html element would make the scroll
 * appearance work differently than what is stated in the class.  Boo Hiss.
 *
 * SCRIPT NOTE:
 * You can have the defaults for the site set in another js file or in this 
 * file and prevent setting of any vars in the head.
 */

/*
 * TODO: Add listeners to each step of the process.
 * TODO: Add a setting to create horizontal accordians as well.
 * TODO: Add an array of classnames that are contained in the tabs and are 
 *       "safe" to click (i.e. won't open or close the accordian).  Currently 
 *       only checkboxes are "safe" clickable in the accordian tab.
 */

/**
 * AccordianHandler constructor function creates new instance with all 
 * properties set to default values.
 *
 * accId            | string | accordianWrapperID
 * contentMaxHeight | int    | height accordian content will expand to.
 */
function AccordianHandler(accId,contentMaxHeight) {
    /**
     * Handle to accordians wrapper / parentId
     */
    this.accId = accId;
    this.slideRate = 10;
    this.slideIncrement = 20;
    this.intervalID = null;

	/**
	 * Max Height of the content
	 */
    this.contentMaxHeight = contentMaxHeight | 0;

    /**
     * Elements caching all the accordian elements
     */
    this.accTabs = new Array();
    this.accContentEls = new Array();
    this.selectedTabIndex = null;
    this.lastSelectedIndex = null;

    /**
     * All animations are run in one step to impact cpu performance the least.
	 * I use the animationsToRun array to keep track of what type of animation
	 * to run on each element of the accContentEls array.  
     */
    this.animationsToRun = new Array();

    /**
     * relavent identifiying classNames
     */
    this.accTabClass = "aT";
    this.accTabSelectedClass = "selected";
    this.accTabOverClass = "";
    this.accContentClass = "aC";
    this.accContentOpenClass = "aS";

    this.debugString = "start\n";

    /**
     * relavent listeners
     */
	this.onAnimateStartListener = null;
	this.onOpenListener = null;
	this.onCloseListener = null;
	this.onOverTabListener = null;
	this.onOffTabListener = null;

    // add onLoad event handler to initialize the accordian
	this.initialized = false;
    var handler = this;
    addOnLoadEventHandler(function(){handler.initAccordian()});	

}


/**
 * Function is responsible for getting handles to all the 
 * accordian elements within the accordian wrapper that was provided.
 */
AccordianHandler.prototype.initAccordian = 
function initAccordian() {
	if (this.initialized) {
		return;
	} else {
		this.initialized = true;
	}

    // get the child elements of the accordian wrapper
    arrChildren = document.getElementById(this.accId).childNodes;

	// if there is no max height we should add up the current open divs to 
	// get the height.
	var needNewHeight = (this.contentMaxHeight == 0) ? true : false;

    // cycle through the divs and get handles to the accordian tabs and content
    for ( var i = 0 ; i < arrChildren.length ; i ++ ) { 
        var el = arrChildren[i];
		
		// if no classname jump to next element
        if (! el.className)
        	continue;

		// if this is an accordian tab then add it to the array of accordian tabs
		if ( el.className.indexOf(this.accTabClass)  != -1 ) {
			// store and add events to the tab
			this.accTabs[this.accTabs.length] = el;
			this.addTabEvents(el,this.accTabs.length - 1);
			// if it's selected keep a handle to it
			if ( el.getAttribute("selectedTab") != null && el.getAttribute("selectedTab") == "true" ) {
				this.selectedTabIndex = this.accTabs.length - 1;
			}
		}

		// if this is an accordian content div then add it to the array of 
		// accordian content elements
		if ( el.className.indexOf(this.accContentClass)  != -1) {
			// since we will be resizing the elements height, we need to
			// make sure that the height is initialized or the animations
			// could break and run forever in the background.
			if ( isNaN(parseInt(el.style.height)) && el.className.indexOf(this.accContentOpenClass) == -1) {
				el.style.height = "0px";
			}
			// if there is no explicit content height set, add the height of 
			// all content elements to set the new content height.
			if  (needNewHeight) {
				this.contentMaxHeight = this.contentMaxHeight + parseInt(el.style.height);
			}
			this.accContentEls[this.accContentEls.length] = el;
		}
    }

	// initialize the array that tells what animations should run per content element
	this.animationsToRun.length = this.accTabs.length;

};


/**
 * Function called will add applicable events to the table cell.
 * Two step addition needed because of ie bug in handling of 
 * adding events directly in a for loop.
 *
 * tab      | Object | tab that we want to add functionality to
 * tabIndex | int    | position of tab in the accTabs array
 */
AccordianHandler.prototype.addTabEvents = 
function addTabEvents(tab , tabIndex) {
    var handler = this;
    addOnEventHandler(tab, "click", function(event){tab.blur();handler.selectAccordian(tabIndex, event)});
    addOnEventHandler(tab, "mouseover", function(){handler.overTab(tab)});
    addOnEventHandler(tab, "mouseout", function(){handler.offTab(tab)});
};


/**
 * Function called when a accordian tab element is clicked or double clicked
 *
 * tabIndex | int          | position of tab/content in the tabs arrays
 * ev       | event object | event that called selectAccordian
 */
AccordianHandler.prototype.selectAccordian = 
function selectAccordian(tabIndex, ev) {
    // if there is a menu/popup don't run the show accordian action
    if (typeof MenuHandler != "undefined" && MenuHandler.arrCurrentlyShowingMenus.length > 0) {
        return;
    }

    // don't close / open the accordian if a checkbox was clicked.
    // NOTE:  Likely this should check a array of classnames and not the type
    //        of element clicked as there might be areas that will be
    //        "not accordian zones". 
    var targetEl = (ev.target) ? ev.target : ev.srcElement;
    if( targetEl.type == "checkbox") {
        return;
    }
    
    // stop all running animations
	this.stopAllAnimations();

    // if there are open accordian elements close them
    for ( var i = 0 ; i < this.accContentEls.length ; i ++ ) { 
		// init the animations to run for each element
		this.animationsToRun[i] = "none";
        if ( parseInt(this.accContentEls[i].style.height) > 0 ) { 
            // lastSelectedIndex != i ensures that a close will occur if this
            // is not the element that was lastSelected in the accordian.
            // lastSelectedIndex != tabIndex will ensure that open accordians
            // close if the tabIndex is new.  if it is a double clicked element
            // then code later will deal with it.
            if (this.lastSelectedIndex != i || this.lastSelectedIndex != tabIndex) {
				this.animationsToRun[i] = "close";
				// set the classes of the tab we are closing
				this.accTabs[i].className = this.accTabClass;
            }
        }
    }

    // check if the tab is already open and open and close it depending.
    if (tabIndex == this.selectedTabIndex) {
		this.animationsToRun[tabIndex] = "close";
        // no longer a selected tab so clear it.
        this.selectedTabIndex = null;

		// set the classes of the tab we are closing
		var tab = this.accTabs[tabIndex];
		tab.className = this.accTabClass;
		this.debugString = this.debugString + "\ntabIndex close: " + tabIndex;

    } else {
		this.animationsToRun[tabIndex] = "open";
        // regardless of initial setting, there can only be one tab open at 
        // a time.  set the newly openend tab as the one that is selected.
        this.selectedTabIndex = tabIndex;

		// set the classes
		var tab = this.accTabs[tabIndex];
		tab.className = this.accTabClass + " " + this.accTabSelectedClass;
		this.debugString = this.debugString + "\ntabIndex open: " + tabIndex;
    }
		
	// if listener exists for animate start run it.
	if (this.onAnimateStartListener != null) { this.onAnimateStartListener(tabIndex); }

	// run the animations
    var handler = this;
	this.intervalId = setInterval(function(){handler.animateAccordian()}, this.slideRate);

    // set this current index to what was last selected by the user.
    // this used to determine if the user has clicked twice on and element, 
    // and if that is the case preventing two setInterval functions from 
    // blocking each other from completion.
    this.lastSelectedIndex = tabIndex;

};



/**
 * Function called will execute a step for all animations that are 
 * set in the animationsToRun array.  Run all animation steps in 
 * one call of setInterval to reduce the number of timers that are 
 * running.
 */
AccordianHandler.prototype.animateAccordian = 
function animateAccordian() {
	var animationsRun = 0;
	// cycle through animations to run and run applicable animation if set.
	for (var i = 0; i < this.animationsToRun.length ; i ++ ) { 
		if ( this.animationsToRun[i] == "close" )  {
			this.slideClose(i);
			animationsRun ++;
		}
		if ( this.animationsToRun[i] == "open") {
			this.slideOpen(i);
			animationsRun ++;
		}	
	}
	// stop interval if no animations are running
	if ( animationsRun == 0 ) {	clearInterval(this.intervalId); }
};



/**
 * Increases height of a new content div by the preset number of pixels
 * designated by the slideIncrement variable and will stop when the
 * height reaches a value designated by the contentMaxHeight variable
 *
 * tabIndex | int    | position of tab in the accTabs array
 */
AccordianHandler.prototype.slideOpen = 
function slideOpen(tabIndex) {    
	var elStyle = this.accContentEls[tabIndex].style;
    if ( (this.contentMaxHeight - parseInt(elStyle.height)) >= this.slideIncrement) {
		// increase height
        elStyle.height = (parseInt(elStyle.height) + this.slideIncrement) + "px";
    } else {
		// set height to content height and clear the animation from those 
		// that should run
        elStyle.height = this.contentMaxHeight + "px";
        this.animationsToRun[tabIndex] = null;
		// if listener exists for onOpen run it.
		if (this.onOpenListener != null) { this.onOpenListener(tabIndex); }
    }
};



/**
 * Decreases the height of a content div by the preset number of pixels
 * designated by the slideIncrement variable and will stop when the
 * height reaches a value below the slideIncrement
 *
 * tabIndex | int    | position of tab in the accTabs array
 */
AccordianHandler.prototype.slideClose = 
function slideClose(tabIndex) {
	var elStyle = this.accContentEls[tabIndex].style;
    if ( parseInt(elStyle.height) >= this.slideIncrement) {
		// decrease height
        elStyle.height = (parseInt(elStyle.height) - this.slideIncrement) + "px";
    } else {
		// set height to content height and clear the animation from those 
		// that should run
        elStyle.height = "0px";
        this.animationsToRun[tabIndex] = null;
		// if listener exists for onClose run it.
		if (this.onCloseListener != null) { this.onCloseListener(tabIndex); }
    }
};


/**
 * Function stops all animations currently running
 */
AccordianHandler.prototype.stopAllAnimations = 
function stopAllAnimations() {
	// stop executing animation
	if (this.intervalId != null) { clearInterval(this.intervalId); }
	// reset the animations array
	this.animationsToRun = new Array(this.accTabs.length);
};


/**
 * Function changes the class when the mouse is over a tab
 *
 * tab | DOM Object | tab that was moused over
 */
AccordianHandler.prototype.overTab = 
function overTab(tab) {
    if ( tab.className.indexOf(this.accTabSelectedClass) == -1 ) {
        tab.setAttribute("originalClassName",tab.className);
        tab.className = tab.className + " " + this.accTabOverClass;
    }
	// if listener exists for onOver run it.
	if (this.onOverTabListener != null) { this.onOverTabListener(tabIndex); }
};

/**
 * Function reverts the class when the mouse moves off the tab
 *
 * tab | DOM Object | tab that was moused off
 */
AccordianHandler.prototype.offTab = 
function offTab(tab) {
    if ( tab.className.indexOf(this.accTabSelectedClass) == -1 ) {
        tab.className = tab.getAttribute("originalClassName");
    }
	// if listener exists for onOver run it.
	if (this.onOffTabListener != null) { this.onOffTabListener(tabIndex); }
};


