/*
 * Accordion 1.3 - jQuery menu widget
 *
 * Copyright (c) 2006 Jörn Zaefferer, Frank Marcia
 *
 * 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 1524 2007-03-13 20:09:19Z joern $
 *
 */

/**
 * 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: 'div.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: 'a.head'
 * });
 * @before <ul id="nav">
 *   <li>
 *     <a class="head">Header 1</a>
 *     <ul>
 *       <li><a href="#">Link 1</a></li>
 *       <li><a href="#">Link 2></a></li>
 *     </ul>
 *   </li>
 *   <li>
 *     <a class="head">Header 2</a>
 *     <ul>
 *       <li><a href="#">Link 3</a></li>
 *       <li><a href="#">Link 4></a></li>
 *     </ul>
 *   </li>
 * </ul>
 * @desc Creates an Accordion from the given navigation list
 *
 * @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 active Selector for the active element, default is the first child, set to false to display none at start
 * @option String|Element|jQuery header Selector for the header element, eg. div.title, a.head, default is the first child's tagname
 * @option String|Number showSpeed Speed for the slideIn, default is 'slow' (for numbers: smaller = faster)
 * @option String|Number hideSpeed Speed for the slideOut, default is 'fast' (for numbers: smaller = faster)
 * @option String selectedClass Class for active header elements, default is 'selected'
 * @option Boolean alwaysOpen Whether there must be one content element open, default is true.
 * @option Boolean animated Set to false to disable animations. Default: true
 * @option String event The event on which to trigger the accordion, eg. "mouseover". Default: "click"
 *
 * @type jQuery
 * @see activate(Number)
 * @name Accordion
 * @cat Plugins/Accordion
 */

/**
 * Activate a content part of the Accordion programmatically at the given zero-based index.
 *
 * If the index is not specified, it defaults to zero, if it is an invalid index, eg. a string,
 * nothing happens.
 *
 * @example jQuery('#accordion').activate(1);
 * @desc Activate the second content of the Accordion contained in <div id="accordion">.
 *
 * @example jQuery('#nav').activate();
 * @desc Activate the first content of the Accordion contained in <ul id="nav">.
 *
 * @param Number index (optional) An Integer specifying the zero-based index of the content to be
 *         activated. Default: 0
 *
 * @type jQuery
 * @name activate
 * @cat Plugins/Accordion
 */

/**
 * Override the default settings of the Accordion. Affects only following plugin calls.
 *
 * @example jQuery.Accordion.setDefaults({
 *      showSpeed: 1000,
 *    hideSpeed: 150
 * });
 *
 * @param Map options key/value pairs of optional settings, see Accordion() for details
 *
 * @type jQuery
 * @name setDefaults
 * @cat Plugins/Accordion
 */

jQuery.fn.extend({
  // nextUntil is necessary, would be nice to have this in jQuery core
  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 ( jQuery.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) {
    // setup configuration
    settings = jQuery.extend({}, jQuery.Accordion.defaults, {
      // define context defaults
      header: jQuery(':first-child', this)[0].tagName // take first childs tagName as header
    }, settings);

    // calculate active if not specified, using the first header
    var container = this,
      active = settings.active
        ? jQuery(settings.active, this)
        : settings.active === false
          ? jQuery('<div>')
          : jQuery(settings.header, this).eq(0),
      running = 0;

    container.find(settings.header)
      .not(active || '')
      .nextUntil(settings.header)
      .hide();
    active.addClass(settings.selectedClass);

    function clickHandler(event) {
      // get the click target
      var clicked = jQuery(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];
      active = clickedActive ? jQuery([]) : clicked;
      // count elements to animate
      running = toHide.size() + toShow.size();
      var finished = function(cancel) {
        running = cancel ? 0 : --running;
        if ( running )
          return;

        // trigger custom change event
        container.trigger('change', data);
      };
      // TODO if hideSpeed is set to zero, animations are crappy
      // workaround: use hide instead
      // solution: animate should check for speed of 0 and do something about it
      if ( settings.animated ) {
        if ( !settings.alwaysOpen && clickedActive ) {
          toShow.slideToggle(settings.showSpeed);
          finished(true);
        } else {
          toHide.filter(':hidden').each(finished).end().filter(':visible').slideUp(settings.hideSpeed, finished);
          toShow.slideDown(settings.showSpeed, finished);
        }
      } else {
        if ( !settings.alwaysOpen && clickedActive ) {toShow.toggle();} else {
          toHide.hide();
          toShow.show();
        }
        finished(true);
      }

      return false;
    };
    function activateHandlder(event, index) {
      // call clickHandler with custom event
      clickHandler({
        target: jQuery(settings.header, this)[index]
      });
    };

    return container
      .bind(settings.event, clickHandler)
      .bind('activate', activateHandlder);
  },
  // programmatic triggering
  activate: function(index) {
    return this.trigger('activate', [index || 0]);
  }
});

jQuery.Accordion = {};
jQuery.extend(jQuery.Accordion, {
  defaults: {
    selectedClass: 'selected',
    showSpeed: 525,
    hideSpeed: 525,
    alwaysOpen: false,
    animated: true,
    event: 'click'
  },
  setDefaults: function(settings) {
    jQuery.extend(jQuery.Accordion.defaults, settings);
  }
});