/*
 * Accordion 1.5 - jQuery menu widget
 *
 * Copyright (c) 2007 Jörn Zaefferer, Frank Marcia
 *
 * http://bassistance.de/jquery-plugins/jquery-plugin-accordion/
 *
 * Dual licensed under the MIT and GPL licenses:
 *   http://www.opensource.org/licenses/mit-license.php
 *   http://www.gnu.org/licenses/gpl.html
 *
 * Revision: $Id: jquery.accordion.js 2880 2007-08-24 21:44:37Z joern.zaefferer $
 *
 */

/**
 * Make the selected elements Accordion widgets.
 *
 * Semantic requirements:
 *
 * If the structure of your container is flat with unique
 * tags for header and content elements, eg. a definition list
 * (dl > dt + dd), you don't have to specify any options at
 * all.
 *
 * If your structure uses the same elements for header and
 * content or uses some kind of nested structure, you have to
 * specify the header elements, eg. via class, see the second example.
 *
 * Use activate(Number) to change the active content programmatically.
 *
 * A change event is triggered everytime the accordion changes. Apart from
 * the event object, all arguments are jQuery objects.
 * Arguments: event, newHeader, oldHeader, newContent, oldContent
 *
 * @example jQuery('#nav').Accordion();
 * @before <dl id="nav">
 *   <dt>Header 1</dt>
 *   <dd>Content 1</dd>
 *   <dt>Header 2</dt>
 *   <dd>Content 2</dd>
 * </dl>
 * @desc Creates an Accordion from the given definition list
 *
 * @example jQuery('#nav').Accordion({
 *   header: '.title'
 * });
 * @before <div id="nav">
 *  <div>
 *    <div class="title">Header 1</div>
 *    <div>Content 1</div>
 *  </div>
 *  <div>
 *    <div class="title">Header 2</div>
 *    <div>Content 2</div>
 *  </div>
 * </div>
 * @desc Creates an Accordion from the given div structure
 *
 * @example jQuery('#nav').Accordion({
 *   header: '.head',
 * 	 navigation: true
 * });
 * @before <ul id="nav">
 *   <li>
 *     <a class="head" href="books/">Books</a>
 *     <ul>
 *       <li><a href="books/fantasy/">Fantasy</a></li>
 *       <li><a href="books/programming/">Programming</a></li>
 *     </ul>
 *   </li>
 *   <li>
 *     <a class="head" href="movies/">Movies</a>
 *     <ul>
 *       <li><a href="movies/fantasy/">Fantasy</a></li>
 *       <li><a href="movies/programming/">Programming</a></li>
 *     </ul>
 *   </li>
 * </ul>
 * @after <ul id="nav">
 *   <li>
 *     <a class="head" href="">Books</a>
 *     <ul style="display: none">
 *       <li><a href="books/fantasy/">Fantasy</a></li>
 *       <li><a href="books/programming/">Programming</a></li>
 *     </ul>
 *   </li>
 *   <li>
 *     <a class="head" href="">Movies</a>
 *     <ul>
 *       <li><a class="current" href="movies/fantasy/">Fantasy</a></li>
 *       <li><a href="movies/programming/">Programming</a></li>
 *     </ul>
 *   </li>
 * </ul>
 * @desc Creates an Accordion from the given navigation list, activating those accordion parts
 * that match the current location.href. Assuming the user clicked on "Fantasy" in the "Movies" section,
 * the accordion displayed after loading the page with the "Movies" section open and the "Fantasy" link highlighted
 * with a class "current".
 *
 * @example jQuery('#accordion').Accordion().change(function(event, newHeader, oldHeader, newContent, oldContent) {
 *   jQuery('#status').html(newHeader.text());
 * });
 * @desc Updates the element with id status with the text of the selected header every time the accordion changes
 *
 * @param Map options key/value pairs of optional settings.
 * @option String|Element|jQuery|Boolean|Number active Selector for the active element. Set to false to display none at start. Default: first child
 * @option String|Element|jQuery header Selector for the header element, eg. 'div.title', 'a.head'. Default: first child's tagname
 * @option String|Number speed 
 * @option String selectedClass Class for active header elements. Default: 'selected'
 * @option Boolean alwaysOpen Whether there must be one content element open. Default: true
 * @option Boolean|String animated Choose your favorite animation, or disable them (set to false). In addition to the default, "bounceslide" and "easeslide" are supported (both require the easing plugin). Default: 'slide'
 * @option String event The event on which to trigger the accordion, eg. "mouseover". Default: "click"
 * @option Boolean navigation If set, looks for the anchor that matches location.href and activates it. Great for href-based pseudo-state-saving. Default: false
 * @option Boolean autoheight If set, the highest content part is used as height reference for all other parts. Provides more consistent animations. Default: false
 *
 * @type jQuery
 * @see activate(Number)
 * @name Accordion
 * @cat Plugins/Accordion
 */

/**
 * Activate a content part of the Accordion programmatically.
 *
 * The index can be a zero-indexed number to match the position of the header to close
 * or a string expression matching an element. Pass -1 to close all (only possible with alwaysOpen:false).
 *
 * @example jQuery('#accordion').activate(1);
 * @desc Activate the second content of the Accordion contained in <div id="accordion">.
 *
 * @example jQuery('#accordion').activate("a:first");
 * @desc Activate the first element matching the given expression.
 *
 * @example jQuery('#nav').activate(false);
 * @desc Close all content parts of the accordion.
 *
 * @param String|Element|jQuery|Boolean|Number index An Integer specifying the zero-based index of the content to be
 *				 activated or an expression specifying the element, or an element/jQuery object, or a boolean false to close all.
 *
 * @type jQuery
 * @name activate
 * @cat Plugins/Accordion
 */

(function($) {

$.Accordion = {};
$.extend($.Accordion, {
	defaults: {
		selectedClass: "selected",
		alwaysOpen: true,
		animated: 'slide',
		event: "click"
	},
	Animations: {
		slide: function(settings, additions) {
			settings = $.extend({
				easing: "swing",
				duration: 300
			}, settings, additions);
			if ( !settings.toHide.size() ) {
				settings.toShow.animate({height: "show"}, {
					duration: settings.duration,
					easing: settings.easing,
					complete: settings.finished
				});
				return;
			}
			var height = settings.toHide.height();
			settings.toShow.css({ height: 0, overflow: 'hidden' }).show();
			settings.toHide.filter(":hidden").each(settings.finished).end().filter(":visible").animate({height:"hide"},{
				step: function(n){
					settings.toShow.height(Math.ceil(height - ($.fn.stop ? n * height : n)));
				},
				duration: settings.duration,
				easing: settings.easing,
				complete: settings.finished
			});
		},
		bounceslide: function(settings) {
			this.slide(settings, {
				easing: settings.down ? "bounceout" : "swing",
				duration: settings.down ? 1000 : 200
			});
		},
		easeslide: function(settings) {
			this.slide(settings, {
				easing: "easeinout",
				duration: 700
			})
		}
	}
});

$.fn.extend({
	nextUntil: function(expr) {
	    var match = [];
	
	    // We need to figure out which elements to push onto the array
	    this.each(function(){
	        // Traverse through the sibling nodes
	        for( var i = this.nextSibling; i; i = i.nextSibling ) {
	            // Make sure that we're only dealing with elements
	            if ( i.nodeType != 1 ) continue;
	
	            // If we find a match then we need to stop
	            if ( $.filter( expr, [i] ).r.length ) break;
	
	            // Otherwise, add it on to the stack
	            match.push( i );
	        }
	    });
	
	    return this.pushStack( match );
	},
	// the plugin method itself
	Accordion: function(settings) {
		if ( !this.length )
			return this;
	
		// setup configuration
		settings = $.extend({}, $.Accordion.defaults, {
			// define context defaults
			header: $(':first-child', this)[0].tagName // take first childs tagName as header
		}, settings);
		
		if ( settings.navigation ) {
			var current = this.find("a").filter(function() { return this.href == location.href; });
			if ( current.length ) {
				if ( current.filter(settings.header).length ) {
					settings.active = current;
				} else {
					settings.active = current.parent().parent().prev();
					current.addClass("current");
				}
			}
		}
		
		// calculate active if not specified, using the first header
		var container = this,
			headers = container.find(settings.header),
			active = findActive(settings.active),
			running = 0;

		if ( settings.autoheight ) {
			var maxHeight = 0;
			headers.nextUntil(settings.header).each(function() {
				maxHeight = Math.max(maxHeight, $(this).height());
			}).height(maxHeight);
		}

		headers
			.not(active || "")
			.nextUntil(settings.header)
			.hide();
		active.addClass(settings.selectedClass);
		
		
		function findActive(selector) {
			return selector != undefined
				? typeof selector == "number"
					? headers.eq(selector)
					: headers.not(headers.not(selector))
				: selector === false
					? $("<div>")
					: headers.eq(0)
		}
		
		function toggle(toShow, toHide, data, clickedActive, down) {
			var finished = function(cancel) {
				running = cancel ? 0 : --running;
				if ( running )
					return;
				// trigger custom change event
				container.trigger("change", data);
			};
			
			// count elements to animate
			running = toHide.size() == 0 ? toShow.size() : toHide.size();
			
			if ( settings.animated ) {
				if ( !settings.alwaysOpen && clickedActive ) {
					toShow.slideToggle(settings.animated);
					finished(true);
				} else {
					$.Accordion.Animations[settings.animated]({
						toShow: toShow,
						toHide: toHide,
						finished: finished,
						down: down
					});
				}
			} else {
				if ( !settings.alwaysOpen && clickedActive ) {
					toShow.toggle();
				} else {
					toHide.hide();
					toShow.show();
				}
				finished(true);
			}
		}
		
		function clickHandler(event) {
			// called only when using activate(false) to close all parts programmatically
			if ( !event.target && !settings.alwaysOpen ) {
				active.toggleClass(settings.selectedClass);
				var toHide = active.nextUntil(settings.header);
				var toShow = active = $([]);
				toggle( toShow, toHide );
				return;
			}
			// get the click target
			var clicked = $(event.target);
			
			// due to the event delegation model, we have to check if one
			// of the parent elements is our actual header, and find that
			if ( clicked.parents(settings.header).length )
				while ( !clicked.is(settings.header) )
					clicked = clicked.parent();
			
			var clickedActive = clicked[0] == active[0];
			
			// if animations are still active, or the active header is the target, ignore click
			if(running || (settings.alwaysOpen && clickedActive) || !clicked.is(settings.header))
				return;

			// switch classes
			active.toggleClass(settings.selectedClass);
			if ( !clickedActive ) {
				clicked.addClass(settings.selectedClass);
			}

			// find elements to show and hide
			var toShow = clicked.nextUntil(settings.header),
				toHide = active.nextUntil(settings.header),
				data = [clicked, active, toShow, toHide],
				down = headers.index( active[0] ) > headers.index( clicked[0] );
			
			active = clickedActive ? $([]) : clicked;
			toggle( toShow, toHide, data, clickedActive, down );

			return !toShow.length;
		};
		function activateHandler(event, index) {
			// IE manages to call activateHandler on normal clicks
			if ( arguments.length == 1 )
				return;
			// call clickHandler with custom event
			clickHandler({
				target: findActive(index)[0]
			});
		};

		return container
			.bind(settings.event, clickHandler)
			.bind("activate", activateHandler);
	},
	activate: function(index) {
		return this.trigger('activate', [index]);
	}
});

})(jQuery);
