/*
 * File: main.js
 * Created:  11/09/2008
 * Modified: 02/17/2009
 * Description:
 * Global javascript file for grove.com news section.
 */

 /* Alias YUI libraries for shorthand access. */
var Y =
  {
    anim:         YAHOO.util.Anim,
    cookie:       YAHOO.util.Cookie,
    customEvent:  YAHOO.util.CustomEvent,
    dom:          YAHOO.util.Dom,
    getByClass:   YAHOO.util.Dom.getElementsByClassName,
    easing:       YAHOO.util.Easing,
    effect:       YAHOO.widget.ContainerEffect,
    event:        YAHOO.util.Event,
    json:         YAHOO.lang.JSON,
    lang:         YAHOO.lang,
    region:       YAHOO.util.Region,
    ua:           YAHOO.env.ua,
    widget:       YAHOO.widget
  },

  /* Global vars for saving page overlays. */
  overlay,
  overlayPP,
  videoOverlay,
  tabbedOverlays = [],
  videoOverlays = [],

  // Map of special (non-printing) character codes.
  // http://www.cambiaresearch.com/c4/702b8cd1-e5b0-42e6-83ac-25f0306e3e25/Javascript-Char-Codes-Key-Codes.aspx
  SpecialChars = makeHash(
  'backspace',  8,    'tab',        9,    'enter',      13,   'shift',      16,
  'ctrl',       17,   'alt',        18,   'pauseBreak', 19,   'caps',       20,
  'esc',        27,   'pageUp',     33,   'pageDown',   34,   'end',        35,
  'home',       36,   'leftArrow',  37,   'upArrow',    38,   'rightarrow', 39,
  'downArrow',  40,   'insert',     45,   'del',        46
  ),

  // Global session object.
  SessionCache = {},
  
  // Classes.
  ClassToggle,
  ShowHide,
  ShowHideSet,
  Pages,
  OverlayMgr,
  TabDisplay,
  TabbedOverlay,
  VideoOverlay,
  Filter,
  FilteredElements,
  Carousel,
  InfoBubble,
  SelectNav,
  
  // Global var for consulting team pages
  teamPages;

/* On load behaviors */ 
Y.event.on(window, 'load', function()
{
  // Array of youtube videos.
  var videos =
    [
      { id: 'vidProdIntro',       url: 'http://www.youtube.com/v/HDxGoAQtMHs&hl=en&fs=1&autoplay=1&showsearch=0&enablejsapi=1&playerapiid=viewVidProdIntro' },
      { id: 'vidProdMtgStartup',  url: 'http://www.youtube.com/v/_ROWTBm_4pM&hl=en&fs=1&autoplay=1&showsearch=0&enablejsapi=1&playerapiid=viewVidProdMtgStartup' },
      { id: 'vidProdHistory',     url: 'http://www.youtube.com/v/DKno6lyJH-0&hl=en&fs=1&autoplay=1&showsearch=0&enablejsapi=1&playerapiid=viewVidProdHistory' },
      { id: 'vidProdContextMap',  url: 'http://www.youtube.com/v/2MF5x5X84WM&hl=en&fs=1&autoplay=1&showsearch=0&enablejsapi=1&playerapiid=viewVidProdContextMap' },
      { id: 'vidProdSPOTMatrix',  url: 'http://www.youtube.com/v/X6oLgiw1exA&hl=en&fs=1&autoplay=1&showsearch=0&enablejsapi=1&playerapiid=viewVidProdSPOTMatrix' },
      { id: 'vidProdCoverStory',  url: 'http://www.youtube.com/v/BKyk6ELxxzM&hl=en&fs=1&autoplay=1&showsearch=0&enablejsapi=1&playerapiid=viewVidProdCoverStory' },
      { id: 'vidProd5BoldSteps',  url: 'http://www.youtube.com/v/twVTGc-v1E8&hl=en&fs=1&autoplay=1&showsearch=0&enablejsapi=1&playerapiid=viewVidProd5BoldSteps' },
      { id: 'vidProdGamePlan',    url: 'http://www.youtube.com/v/i3XtlniRvmY&hl=en&fs=1&autoplay=1&showsearch=0&enablejsapi=1&playerapiid=viewVidProdGamePlan' },
      { id: 'vidStoryTED2008',    url: 'http://www.youtube.com/v/MUTTCpzsZpo&hl=en&fs=1&autoplay=1&showsearch=0&enablejsapi=1&playerapiid=viewVidStoryTED2008' }
    ],
    // Flash nav params
    params = { flashvars: '1=1', play: 'true', loop: 'true', menu: 'true', quality: 'high', scale: 'showall', salign: '', wmode: 'window', bgcolor: '#ffffff', devicefont: 'false', allowscriptaccess: 'always', allowfullscreen: 'false' },
    attrs = { id: 'nav', name: 'nav', align: 'middle' },
    homeattrs = { id: 'home', name: 'home', align: 'middle' },
    
    // Array to hold staff ShowHide widgets.
    staff = [],
    faqFilter,
    workshopTable,
    workshopControls,
    workshopDetails,
    i,
    workshopControl,
    workshopShowHides,
    workshopNav,
    navPrefix = '';
    
  // Embed the flash home page.
  if (Y.dom.get('noFlashHome'))
  {
    swfobject.embedSWF('flash/home.swf', 'noFlashHome', '748', '465', '9.0.0', 'flash/expressInstall.swf', false, params, homeattrs);
  }

  // Setup the navigation xml for the left nav.
  if ((/netsuite/i).test(document.domain) || (/fusionbot/i).test(document.domain))
  {
    params.flashvars = 'navXml=http://www.grove.com/site/navigation.xml';
    navPrefix = 'http://www.grove.com/site/';
  }
  else if (typeof(themeDir) != 'undefined')
  {
    params.flashvars = 'navXml=' + themeDir + 'navigation.xml';
    navPrefix = themeDir;
  }
  
  // Preset the page id for the left nav.
  if (typeof(pageId) != 'undefined') params.flashvars += '&pageID=' + pageId + '&disableOpenAnim=1';
  
  // Embed the flash nav.
  swfobject.embedSWF(navPrefix + 'flash/nav.swf', 'noFlashNav', '200', '480', '9.0.0', navPrefix + 'flash/expressInstall.swf', false, params, attrs);
  
  // Attach behaviors to links that launch new windows
  Y.dom.batch(Y.getByClass('_newwin', 'a'), function(o)
  {
    Y.event.on(o, 'click', function(e)
    {
      window.open(this.href, 'grovewin');
      Y.event.preventDefault(e);
    });
  });
  
  // Attach hover and click behaviors for sIFR replaced calloutLinks.
  // sIFR requires us to have the link inside the replaced element which means we can't attach CSS-style hovers for the container-element.
  Y.dom.batch(Y.getByClass('calloutLink', 'span'), function(o)
  {
    Y.event.on(o, 'mouseover', function(e)
    {
      Y.dom.setStyle(this, 'backgroundColor', '#f7f7f4');
    });
    Y.event.on(o, 'mouseout', function(e)
    {
      Y.dom.setStyle(this, 'backgroundColor', 'transparent');
    });
    Y.event.on(o, 'click', function(e)
    {
      var links = this.getElementsByTagName('a');
      if (links.length > 0)
      {
        window.location = links[0].href;
      }
    });
  });
  
  // Attach hover and click behaviors for sIFR replaced affiliateLinks.
  // sIFR requires us to have the link inside the replaced element which means we can't attach CSS-style hovers for the container-element.
  Y.dom.batch(Y.getByClass('affiliateLink', 'span'), function(o)
  {
    Y.event.on(o, 'mouseover', function(e)
    {
      Y.dom.setStyle(this, 'backgroundColor', '#f7f7f4');
    });
    Y.event.on(o, 'mouseout', function(e)
    {
      Y.dom.setStyle(this, 'backgroundColor', 'transparent');
    });
    Y.event.on(o, 'click', function(e)
    {
      var links = this.getElementsByTagName('a');
      if (links.length > 0)
      {
        window.location = links[0].href;
      }
    });
  });
  
  // Initialize ShowHide for elements with class name staffLink. Add them to the array.
  Y.dom.batch(Y.getByClass('staffLink', 'a'), function(o)
  {
    staff[staff.length] = new ShowHide(o, Y.getByClass(o.id));
  });
  
  // Initialize ShowHideSet for staff ShowHide widgets.
  new ShowHideSet(staff);
  
  // Initialize Pages widget for elements with class name page and controls with class name pageLink
  teamPages = new Pages(Y.getByClass('page', 'div'), Y.getByClass('pageLink', 'span'));
  
  // Initialize ShowHide for elements with class name showHide
  Y.dom.batch(Y.getByClass('showHide'), function(o)
  {
    new ShowHide(o, Y.getByClass(o.id));
  });
  
  // Initialize ShowHideSets for FAQs.
  faqFilter = Y.dom.get('faqFilter');
  if (faqFilter)
  {
    Y.dom.batch(faqFilter.getElementsByTagName('dl'), function(o)
    {
      if (o.className != '')
      {
        var showHides = [],
            currDt;
        Y.dom.batch(Y.dom.getChildren(o), function(p)
        {
          switch (p.tagName.toLowerCase())
          {
            case 'dt':
              currDt = p.getElementsByTagName('a')[0];
              break;
            case 'dd':
              showHides[showHides.length] = new ShowHide(currDt, [p]);
              break;
          }
        });
        new ShowHideSet(showHides);
      }
    });
  }
  
  // Attach behaviors to links that open print dialogues
  Y.dom.batch(Y.getByClass('printLink', 'a'), function(o)
  {
    Y.event.on(o, 'click', function(e)
    {
      window.print();
      Y.event.preventDefault(e);
    });
  });
  
  // Attach autotab behaviors to form inputs.
  Y.dom.batch(Y.getByClass('autotab', 'input'), function(o) {
    Y.event.on(o, 'keyup', function(e) {
      // Skip if a non-printing key was pressed.
      if (isSpecialChar(Y.event.getCharCode(e)))
      {
        return;
      }
      
      // If we're at the end of the field, move focus to the next field.
      if (this.value.length >= this.maxLength) {
        for (var i = 0; i < this.form.length; i++)
        {
          if (this.form[i] == this)
          {
            this.form[++i % this.form.length].focus();
            break;
          }
        }
      }
      Y.event.stopEvent(e);
    });
  });
  
  // Class toggles for our services page.
  Y.dom.batch(Y.getByClass('serviceHead', 'a'), function(o)
  {
    new ClassToggle(o, Y.getByClass(o.id), 'open', 'open');
  });

  // Activate carousels.
  Y.dom.batch(Y.getByClass('carousel', 'div'), function(o)
  {
    new Carousel(o);
  });

  // Use swfobject to load the videos if the corresponding div exists in the page.
  Y.dom.batch(videos, function(video)
  {
    var params = { allowscriptaccess: 'always', allowfullscreen: 'true' },
        attrs = { name: video.id };
    if (Y.dom.get(video.id))
    {
      swfobject.embedSWF(video.url, video.id, '481', '389', '9.0.0', 'flash/expressInstall.swf', false, params, attrs);
    }
  });
  
  /* Initialize the overlays */
  if (Y.dom.get('overlay'))
  {
    overlay = new OverlayMgr('overlay');
  }
  
  if (Y.dom.get('overlayPP'))
  {
    overlayPP = new OverlayMgr('overlayPP');
  }
  
  if (Y.dom.get('videoOverlay'))
  {
    videoOverlay = new OverlayMgr('videoOverlay');
  }

  /* Tabbed overlays */
  Y.dom.batch(Y.getByClass('tabbedOverlayTrigger', 'a'), function(o)
  {
    var tabContent = o.getAttribute('rel').split('-')[0];
    if (typeof(tabbedOverlays[tabContent]) == 'undefined')
    {
      tabbedOverlays[tabContent] = new TabbedOverlay(overlay, tabContent);
    }
    Y.event.on(o, 'click', function(e)
    {
      var rels = this.getAttribute('rel').split('-');
      tabbedOverlays[rels[0]].show(rels[1]);
      Y.event.stopEvent(e);
    });
  });
  
  /* Video overlays */
  Y.dom.batch(Y.getByClass('videoOverlayTrigger', 'a'), function(o)
  {
    var videoContent = o.getAttribute('rel');
    if (typeof(videoOverlays[videoContent]) == 'undefined')
    {
      videoOverlays[videoContent] = new VideoOverlay(videoOverlay, videoContent);
    }
    Y.event.on(o, 'click', function(e)
    {
      videoOverlays[this.getAttribute('rel')].show();
      Y.event.stopEvent(e); 
    });
  });
  
  /* Generic overlays */
  Y.dom.batch(Y.getByClass('overlayTrigger', 'a'), function(o)
  {
    Y.event.on(o, 'click', function(e)
    {
      overlay.show(this.getAttribute('rel'));
      Y.event.stopEvent(e);
    });
  });
	
	Y.dom.batch(Y.getByClass('overlayPPTrigger', 'a'), function(o)
  {
    Y.event.on(o, 'click', function(e)
    {
      overlayPP.show(this.getAttribute('rel'));
      Y.event.stopEvent(e);
    });
  });
  
  // Attach behaviors for feature box rollovers.
  Y.dom.batch(Y.getByClass('featureBox', 'div'), function(o)
  {
    Y.event.on(o, 'mouseover', function(e)
    {
      Y.dom.addClass(this, 'featureBoxHover');
    });
    
    Y.event.on(o, 'mouseout', function(e) {
      Y.dom.removeClass(this, 'featureBoxHover');
    });
  });
  
  // Set up filters. Assumes filtered containers have class "filtered"
  // and their corresponding filter control has a class name to match the filter
  // container id.
  Y.dom.batch(Y.getByClass('filtered'), function(o)
  {
    new FilteredElements(o, Y.getByClass(o.id)[0]);
  });
  
  // Initialize the info bubbles.
  Y.dom.batch(Y.getByClass('infoBubble', 'a'), function(o)
  {
    var context = Y.dom.get(o.getAttribute('rel'));
    new InfoBubble(o, context, context.getAttribute('alt'));
  });

  // Add behaviors for workshop table.  
  workshopTable = Y.getByClass('workshop', 'table');
  if (workshopTable)
  {
    workshopShowHides = [];
    workshopControls = Y.getByClass('expand', 'tr');
    workshopDetails = Y.getByClass('detail', 'tr');

    for (i = 0; i < workshopControls.length; i++)
    {
      workshopControl = workshopControls[i];
      
      // Add hovers
      Y.event.on(workshopControl, 'mouseover', function(e)
      {
        Y.dom.batch(this.getElementsByTagName('td'), function(o)
        {
          Y.dom.addClass(o, 'expandHover');
        });
      }, workshopControl, true);

      Y.event.on(workshopControl, 'mouseout', function(e)
      {
        Y.dom.batch(this.getElementsByTagName('td'), function(o)
        {
          Y.dom.removeClass(o, 'expandHover');
        });
      }, workshopControl, true);
      
      // Add show/hide functionality.
      workshopShowHides[workshopShowHides.length] = new ShowHide(workshopControl, [workshopDetails[i]]);
    }
    
    // Make the workshops a show/hide set.
    new ShowHideSet(workshopShowHides);
  }
  
  // Initialize the workshop nav.
  workshopNav = Y.dom.get('workshopNav');
  if (workshopNav)
  {
    new SelectNav(workshopNav.getElementsByTagName('select')[0]);
  }
});

/* Utility Functions */

/**
 * Creates a cookie that expires in a given number of hours.
 * @requires YAHOO.util.Cookie
 * @param {String} name The name of the cookie to create.
 * @param {String} value The value to save with the cookie.
 * @param {Integer} hours The number of hours before the cookie expires.
 */
function createCookie(name, value, hours)
{
  var date = new Date();
  date.setTime(date.getTime() + (hours * 60 * 60 * 1000));
  
  Y.cookie.set(name, value, {expires: date.toGMTString(), path: '/'});
}

/**
 * Returns the width of an element.
 * @requires YAHOO.util.Dom
 * @param {String | HTMLElement} el Reference to the element.
 */
function getElementWidth(el)
{
  var region = Y.dom.getRegion(el);
  return (region.right - region.left);
}

/**
 * Returns the height of an element.
 * @requires YAHOO.util.Dom
 * @param {String | HTMLElement} el Reference to the element.
 */
function getElementHeight(el)
{
  var region = Y.dom.getRegion(el);
  return (region.bottom - region.top);
}

/**
 * Utility function to construct a javascript hash array.
 * Pass in a paired list of name/value pairs.
 * Ignores the last argument if there is an odd number of arguments.
 */
function makeHash()
{
  var returnVal = [],
      i;
  for (i = 0; i < arguments.length; i += 2)
  {
    if (typeof(arguments[i + 1]) != 'undefined')
    {
      returnVal[arguments[i]] = arguments[i + 1];
    }
  }
  return returnVal;
}

/**
 * Returns true for character codes corresponding to special non-printing keys.
 * @param {Integer} charCode The character code to test.
 */
function isSpecialChar(charCode)
{
  for (var character in SpecialChars)
  {
    if (charCode == SpecialChars[character])
    {
      return true;
    }
  }
  return false;
}

/**
 * Determines the default display type of an element based on its tag name.
 * Based on W3C default CSS recommendations: http://www.w3.org/TR/CSS21/sample.html
 * Proper display values have been commented out and replaced by empty string to handle browser compatibility issues for current set of browsers
 * Hopefully, at some point, browsers can support full compatibility and we can use the proper values
 * http://reference.sitepoint.com/css/display
 * @requires YAHOO.util.Dom
 * @param {String | HTMLElement} el Reference to the element to determine default display type for.
 */
function getDefaultDisplayType(el)
{
  var element = Y.dom.get(el);
  
  // Most likely tags are listed first.
  switch (element.tagName.toLowerCase())
  {
    case 'table':
      return '';
      // IE doesn't support display table.
      //return 'table';
    case 'tr':
      return '';
      // IE doesn't support display table-row.
      //return 'table-row';
    case 'td':
    case 'th':
      return '';
      // IE doesn't support display table-cell.
      //return 'table-cell';
    case 'li':
      return 'list-item';
    case 'input':
    case 'select':
      return '';
      // FF2 doesn't support display inline-block.
      //return 'inline-block';
    case 'div':
    case 'p':
    case 'h1':
    case 'h2':
    case 'h3':
    case 'h4':
    case 'h5':
    case 'h6':
    case 'form':
    case 'ol':
    case 'ul':
    case 'hr':
    case 'blockquote':
    case 'dd':
    case 'dl':
    case 'dt':
    case 'fieldset':
    case 'center':
    case 'dir':
    case 'menu':
    case 'pre':
    case 'html':
    case 'body':
    case 'address':
    case 'frame':
    case 'frameset':
    case 'noframes':
      return 'block';
    case 'thead':
      return 'table-header-group';
    case 'tbody':
      return '';
      // IE doesn't support display table-row-group.
      //return 'table-row-group';
    case 'tfoot':
      return 'table-footer-group';
    case 'col':
      return '';
      // IE doesn't support display table-column.
      //return 'table-column';
    case 'colgroup':
      return '';
      // IE doesn't support display table-column-group.
      //return 'table-column-group';
    case 'caption':
      return '';
      // IE doesn't support display table-caption.
      //return 'table-caption';
    default:
      return 'block';
  }
}

/* Widgets */

/*
  Client-side Session Management
  In absence of server-side session management, can be used to save javascript objects in json format using cookies.
  To be used sparingly. Minimize the size of data stored with this technique.
  Each object to be stored is limited by the character limits of cookies. In addition, all data is sent with the request and response, adding to the bandwidth load.
*/

SessionCache.SESSION_TTL = 2; // Session expires in 2 hours.

/**
 * Gets an object from the session cache.
 * @requires YAHOO.lang.JSON
 * @requires YAHOO.util.Cookie
 * @param {String} cacheName The unique identifier for the object to retrieve.
 */
SessionCache.get = function(cacheName)
{
  var cookie = Y.cookie.get(cacheName);
  return (cookie == null) ? {} : Y.json.parse(cookie);
}

/**
 * Saves an object in the session cache.
 * @requires YAHOO.lang.JSON
 * @requires YAHOO.util.Cookie
 * @param {String} cacheName The unique identifier for the object to save.
 * @param {Object} obj The object to save.
 */
SessionCache.set = function(cacheName, obj)
{
  var jsonString = Y.json.stringify(obj);
  createCookie(cacheName, jsonString, SessionCache.SESSION_TTL);
}

/**
 * Class for toggling a class name on multiple DOM elements.
 * <p>Usage: var newClassToggle = new ClassToggle(controlEl, [subjectEl1, subjectEl2, ...], controlOnClass, onClass);</p>
 * @class ClassToggle
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Event
 * @constructor
 * @param {String | HTMLElement} controlEl Reference to the clickable element that controls the class toggle functionality.
 * @param {Array} subjectEls The elements to toggle the class name for.
 */
ClassToggle = function(controlEl, subjectEls, controlOnClass, onClass)
{
  // Class name applied to control to indicate the toggle is on.
	this.controlOnClass = controlOnClass;
	
	// Class name applied to subjects when the toggle is on.
	this.onClass = onClass;
	
	this.control = Y.dom.get(controlEl);
	
	if (!this.control)
	{
	  return;
	}
	
	this.subjects = [];
	for (var i = 0; i < subjectEls.length; i++)
	{
	  this.subjects[i] = Y.dom.get(subjectEls[i]);
	}
	
	// Add click event listener
	Y.event.on(this.control, 'click', this.click, this, true);
}

ClassToggle.prototype = 
{
  /* Toggles the class name of the subjects */
  click: function(e)
  {
    this.toggleClass();
    
    // Disable default link behavior
    Y.event.stopEvent(e);
  },

  /* Returns true if the class is toggled on */
  isOn: function()
  {
    return Y.dom.hasClass(this.control, this.controlOnClass);
  },
  
  /* Adds the class to all of the subjects */
  turnOn: function()
  {
    if (!this.isOn())
    {
      this.toggleClass();
    }
  },
  
  /* Removes the class from all of the subjects */
  turnOff: function()
  {
    if (this.isOn())
    {
      this.toggleClass();
    }
  },
  
  /* Toggles addition and removal of the class from the subjects */
  toggleClass: function()
  {
    var isOn = this.isOn(),
        i,
        subject;
    
    if (isOn)
    {
      Y.dom.removeClass(this.control, this.controlOnClass);
    }
    else
    {
      Y.dom.addClass(this.control, this.controlOnClass);
    }
    
    for (i = 0; i < this.subjects.length; i++)
    {
      subject = this.subjects[i];
      if (subject)
      {
        if (isOn)
        {
          Y.dom.removeClass(subject, this.onClass);
        }
        else
        {
          Y.dom.addClass(subject, this.onClass);
        }
      }
    }
  }
}

/**
 * Class for show/hide functionality. Saves show/hide state across sessions with cookies.
 * <p>Usage: var newShowHide = new ShowHide(controlEl, [subjectEl1, subjectEl2, ...]);</p>
 * @class ShowHide
 * @requires YAHOO.util.CustomEvent
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Event
 * @constructor
 * @param {String | HTMLElement} controlEl Reference to the clickable element that controls the show/hide functionality.
 * @param {Array} subjectEls The elements to toggle between show/hide. To track state across the session, the subject elements must have a unique id.
 */
ShowHide = function(controlEl, subjectEls)
{
  // Class name applied to control to indicate whether the element is hidden.
  this.HIDDEN_CLASS = 'closed';
  // Key used for tracking show/hide in session cache.
  this.CACHE_NAME = 'showHide';

  this.control = Y.dom.get(controlEl);

  if (!this.control)
  {
    return;
  }

  this.subjects = [];
  this.displayType = [];
  for (var i = 0; i < subjectEls.length; i++)
  {
    this.subjects[i] = Y.dom.get(subjectEls[i]);
    /* Get original display setting for subjects. */
    if (this.subjects[i])
    {
      this.displayType[i] = getDefaultDisplayType(this.subjects[i]);
    }
  }
  
  // Add click event listener
  Y.event.on(this.control, 'click', this.click, this, true);
  
  // Add custom event for state change
  this.onshow = new Y.customEvent('show', this);
  this.onhide = new Y.customEvent('hide', this);
}

ShowHide.prototype =
{
  /* Toggles the show/hide of subjects */
  click: function(e)
  {
    this.toggleDisplay();

    /* Disable default link behavior */
    Y.event.stopEvent(e);
  },
  
  /* Returns true if the subjects are hidden */
  isHidden: function()
  {
    return Y.dom.hasClass(this.control, this.HIDDEN_CLASS);
  },
  
  /* Hides the subjects */
  hide: function()
  {
    if (!this.isHidden())
    {
      this.toggleDisplay();
    }
  },
  
  /* Shows the subjects */
  show: function()
  {
    if (this.isHidden())
    {
      this.toggleDisplay();
    }
  },

  /* Switches subject elements between original display type and display none */
  /* Changes the class on control to reflect show/hide state */
  toggleDisplay: function()
  {
    var hidden = this.isHidden(),
        showHideState = {},
        i,
        subject;
    if (hidden)
    {
      Y.dom.removeClass(this.control, this.HIDDEN_CLASS);
      this.onshow.fire();
    }
    else
    {
      Y.dom.addClass(this.control, this.HIDDEN_CLASS);
      this.onhide.fire();
    }

    for (i = 0; i < this.subjects.length; i++)
    {
      subject = this.subjects[i];
      if (subject)
      {
        if (hidden)
        {
          Y.dom.setStyle(subject, 'display', this.displayType[i]);
          showHideState[subject.id] = 1;
        }
        else
        {
          Y.dom.setStyle(subject, 'display', 'none');
          showHideState[subject.id] = 0;
        }
      }
    }
    
    // Maintain the show/hide state across HTTP requests.
    this.saveShowHideState(showHideState);
  },
  
  /* Saves the show/hide state across HTTP requests */
  saveShowHideState: function(showHideState)
  {
    var currShowHideState = SessionCache.get(this.CACHE_NAME),
        key;

    // The cache holds state for all show/hide instances.
    // So maintain the current state and only override the settings for this instance.
    for (key in showHideState)
    {
      currShowHideState[key] = showHideState[key];
    }

    SessionCache.set(this.CACHE_NAME, currShowHideState);
  }
}

/**
 * Class to manage a set of ShowHide widgets. Ensures that only one ShowHide widget in the set is shown at one time.
 * <p>Usage: var newShowHideSet = new ShowHideSet(showHides);</p>
 * @class ShowHideSet
 * @requires YAHOO.util.CustomEvent
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Event
 * @constructor
 * @param {Array} showHides References to the individual ShowHide widgets.
 */
ShowHideSet = function(showHides)
{
  this.showHides = showHides;

  var i,
      showHide;
  
  // Subscribe to the custom onshow event of each widget.
  for (i = 0; i < this.showHides.length; i++)
  {
    showHide = this.showHides[i];
    showHide.onshow.subscribe(this.onShow, showHide, this);
  }
}

ShowHideSet.prototype =
{
  /* Hide any shown ShowHide widgets */
  onShow: function(e, args, showHide)
  {
    // If there is a ShowHide widget currently shown that isn't the same as the one about to be shown, hide it.
    if (this.currShowHide && this.currShowHide != showHide)
    {
      this.currShowHide.hide();
    }
    
    // Track the newly shown widget.
    this.currShowHide = showHide;
  }
}

/**
 * Class for page functionality. Represents multiple elements as pages that have internal close buttons and are triggered with external controls.
 * <p>Usage: var newPages = new Pages(pageEls, controlEls);</p>
 * @class Pages
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Event
 * @constructor
 * @param {Array} pageEls References to the individual page elements.
 * @param {Array} controlEls References to the external controls to open the pages.
 */
Pages = function(pageEls, controlEls)
{
  // Class name for close buttons.
  this.CLOSE_CLASS = 'closeLink';

  var i,
      page,
      control,
      controlRel;

  // Get all pages and index them in an object by id.
  this.pages = {};
  for (i = 0; i < pageEls.length; i++)
  {
    page = Y.dom.get(pageEls[i]);
    if (page)
    {
      this.pages[page.id] = page;
     
      // Find the close buttons inside the page and attach event handler for closing the page. 
      Y.dom.batch(Y.getByClass(this.CLOSE_CLASS, 'a', page), function(o)
      {
        Y.event.on(o, 'click', this.clickCloseHandler, page, this);
      }, this, true);
    }
  }

  // Get all the external page controls and save them in an array.
  this.controls = [];
  for (i = 0; i < controlEls.length; i++)
  {
    control = Y.dom.get(controlEls[i]);
    if (control)
    {
      this.controls[this.controls.length] = control;
      
      // Attach event handler to open the page. The rel attribute of the control should match the id of the page to open.
      controlRel = control.getAttribute('rel');
      if (typeof(this.pages[controlRel]) != 'undefined')
      {
        Y.event.on(control, 'click', this.clickControlHandler, this.pages[controlRel], this);
      }
    }
  }
}

Pages.prototype =
{
  /* Event handler for clicking a close button. */
  clickCloseHandler: function(e, page)
  {
    this.closePage(page);

    /* Disable default link behavior */
    Y.event.stopEvent(e);
  },
  
  /* Event handler for clicking a page open control. */
  clickControlHandler: function(e, page)
  {
    this.openPage(page);

    /* Disable default link behavior */
    Y.event.stopEvent(e);
  },
  
  /* Closes the specified page. */
  closePage: function(page)
  {
    Y.dom.setStyle(page, 'display', 'none');
  },
  
  /* Opens the specified page. */
  openPage: function(page)
  {
    // If another page is already open, close it first.
    if (this.currPage)
    {
      this.closePage(this.currPage);
    }
    
    Y.dom.setStyle(page, 'display', 'block');
    
    // Track the currently open page.
    this.currPage = page;
  }
}

/**
 * Class for overlay/popup functionality.
 * <p>Usage: var newOverlayMgr = new OverlayMgr(overlayEl);</p>
 * @class OverlayMgr
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Event
 * @requires YAHOO.widget.Panel
 * @requires YAHOO.widget.ContainerEffect
 * @constructor
 * @param {String | HTMLElement} overlayEl    Reference to the overlay container.
 */
OverlayMgr = function(overlayEl)
{
  // Class name for close buttons.
  this.CLOSE_CLASS = 'overlayClose';

  // Define elements
  this.cont = Y.dom.get(overlayEl);

  // Instantiate and render overlay.
  this.overlay = new Y.widget.Panel(this.cont,
  {
    fixedcenter: false,
    modal:       true,
    visible:     false,
    close:       false,
    underlay:    false,
    effect:      { effect: Y.effect.FADE, duration: 0.5 }
  });
  // Add an event handler for when the overlay finishes hiding.
  this.overlay.hideEvent.subscribe(this.hideCompleteHandler, this, true);
  this.overlay.render(document.body);
  
  // Set up event handlers to close the overlay.
  this.btnsClose = Y.getByClass(this.CLOSE_CLASS, 'a', this.cont);
  Y.dom.batch(this.btnsClose, function(btnClose)
  {
	  Y.event.on(btnClose, 'click', this.clickCloseHandler, this, true);
  }, this, true);

  // Close the overlay if the ESC key is pressed.
  Y.event.on(document, 'keypress', this.keypressHandler, this, true);
  
  // Close the overlay if the mask is clicked on.
  this.overlay.buildMask(); // YUI does lazy creation of the mask. But we need it now.
  Y.event.on(this.overlay.mask, 'click', this.clickMaskHandler, this, true)
  
  /* Create custom events for show and hide */
  this.onshow = new Y.customEvent('show', this);
  this.onhide = new Y.customEvent('hide', this);
  
  // Track states
  this.activeCont = null; // Active content selection
  this.shown = false;     // Shown status
}

OverlayMgr.prototype =
{
  /* Captures ESC keystroke. */
  keypressHandler: function(e)
  {
    if (Y.event.getCharCode(e) == SpecialChars['esc'])
    {
      this.hide();
    }
  },
  
  /* Event handler for clicking a close button. */
  clickCloseHandler: function(e)
  {
    this.hide();
    Y.event.stopEvent(e);
  },
  
  /* Event handler for clicking on the mask. */
  clickMaskHandler: function(e)
  {
    this.hide();
    Y.event.stopEvent(e);
  },
  
  /* Loads the content for display in the overlay. */
  load: function(content)
  {
    this.clear();
    content = Y.dom.get(content);
    Y.dom.setStyle(content, 'display', 'block');
    this.activeCont = content;
  },
  
  /* Hides the active content */
  clear: function()
  {
    if (this.activeCont)
    {
      Y.dom.setStyle(this.activeCont, 'display', 'none');
      this.activeCont = false;
    }
  },

  /* Shows the overlay */
  show: function(content)
  {
    // Load the content first so we can measure it.
	  this.load(content);
    
    // Center the overlay in the window.
    var scrollTop =  Y.dom.getDocumentScrollTop(),
        scrollLeft = Y.dom.getDocumentScrollLeft(),
        yPos = scrollTop + (Y.dom.getViewportHeight() - getElementHeight(this.cont))/2,
        xPos = scrollLeft + (Y.dom.getViewportWidth() - getElementWidth(this.cont))/2;
    
    // Make sure the top and left of the overlay shows in the window.
    yPos = (yPos < scrollTop) ? scrollTop : yPos;
    xPos = (xPos < scrollLeft) ? scrollLeft : xPos;  
    
    this.overlay.cfg.setProperty('y', yPos);
    this.overlay.cfg.setProperty('x', xPos);
    this.overlay.show();
    this.shown = true;
    this.onshow.fire();
  },

  /* Hides the overlay */
  hide: function()
  {
    // FF2 on Mac has a problem clearing scrollbars on an overlay, so hide the active content now.
    if (Y.ua.gecko && Y.ua.gecko < 1.9 && (/Macintosh/).test(navigator.userAgent))
    {
      this.clear();
    }
    this.overlay.hide();
    this.shown = false;
    this.onhide.fire();
  },

  /* Reposition overlay so that it doesn't affect the size of the page */  
  hideCompleteHandler: function()
  {
    this.overlay.cfg.setProperty('y', 0);
    this.overlay.cfg.setProperty('x', 0);
  }
}

/**
 * Class for managing a tabbed display.
 * <p>Usage: var newTabDisplay = new TabDisplay(tabContainer, displayContainer);</p>
 * @class TabDisplay
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Event
 * @constructor
 * @param {String | HTMLElement} tabContainer Reference to the tab container. Tabs are expected to be links with class name "tab".
 * @param {String | HTMLElement} displayContainer Reference to the display container. Displays are assumed to have a class name that matches the rel attribute of the corresponding tab.
 */
TabDisplay = function(tabContainer, displayContainer)
{
  // Class name applied to selected tab.
  this.SEL_TAB_CLASS = 'sel';
  this.TAB_CLASS = 'tab';

  // Track the active tab.
  this.activeIndex = null;

  // Load the tabs and displays, attach event handlers for tab clicks, save active tab index.
  this.tabs = [];
  this.displays = [];
  Y.dom.batch(Y.getByClass(this.TAB_CLASS, 'a', tabContainer), function(tab)
  {
    var index = tab.getAttribute('rel');
    
    this.tabs[index] = tab;
    this.displays[index] = Y.getByClass(index, '*', displayContainer);
    
    if (Y.dom.hasClass(tab, this.SEL_TAB_CLASS))
    {
      this.activeIndex = index;
    }

    Y.event.on(tab, 'click', this.clickTabHandler, index, this);
  }, this, true);
}

TabDisplay.prototype =
{
  /* Event handler for a tab click. */
  clickTabHandler: function(e, index)
  {
    this.displayTab(index);
    Y.event.stopEvent(e);
  },
  
  /* Displays the tab at index. */
  displayTab: function(index)
  {
    // Toggle off the active tab and displays.
    if (this.activeIndex)
    {
      Y.dom.removeClass(this.tabs[this.activeIndex], this.SEL_TAB_CLASS);
      Y.dom.batch(this.displays[this.activeIndex], function(display)
      {
        Y.dom.setStyle(display, 'display', 'none');
      });
    }
    
    // Toggle on the new tab and displays at index.
    Y.dom.addClass(this.tabs[index], this.SEL_TAB_CLASS);
    Y.dom.batch(this.displays[index], function(display)
    {
      Y.dom.setStyle(display, 'display', 'block');
    });
    
    // Save the active tab index.
    this.activeIndex = index;
  }
}

/**
 * Class for managing an overlay with tabbed content display.
 * <p>Usage: var newTabbedOverlay = new TabbedOverlay(overlayMgr, tabContent);</p>
 * @class TabbedOverlay
 * @requires OverlayMgr
 * @requires TabDisplay
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Event
 * @requires YAHOO.widget.Panel
 * @requires YAHOO.widget.ContainerEffect
 * @constructor
 * @param {OverlayMgr} overlayMgr Reference to the OverlayMgr.
 * @param {String | HTMLElement} tabContent Reference to the tabbed display content.
 */
TabbedOverlay = function(overlayMgr, tabContent)
{
  // Save the overlayMgr and tabContent.
  this.overlayMgr = overlayMgr;
  this.tabContent = Y.dom.get(tabContent);
  
  // Make a TabDisplay with the tabContent.
  this.tabDisplay = new TabDisplay(tabContent, tabContent);
}

TabbedOverlay.prototype =
{
  /* Display the tab with rel attribute "index" */
  show: function(index)
  {
    this.tabDisplay.displayTab(index);
    this.overlayMgr.show(this.tabContent);
  },
  
  /* Hide the overlay */
  hide: function()
  {
    this.overlayMgr.hide();
  }
}

/**
 * Class for managing an overlay with video content.
 * Utilizes YouTube JavaScript Player API (http://code.google.com/apis/youtube/js_api_reference.html)
 * <p>Usage: var newVideoOverlay = new VideoOverlay(overlayMgr, videoContent);</p>
 * @class VideoOverlay
 * @requires OverlayMgr
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Event
 * @requires YAHOO.widget.Panel
 * @requires YAHOO.widget.ContainerEffect
 * @constructor
 * @param {OverlayMgr} overlayMgr Reference to the OverlayMgr.
 * @param {String | HTMLElement} videoContent Reference to the video content.
 */
VideoOverlay = function(overlayMgr, videoContent)
{
  // Save the overlayMgr and videoContent.
  this.overlayMgr = overlayMgr;
  this.videoContent = Y.dom.get(videoContent);

  // Find the video. Mozilla uses the embed tag.
  this.video = this.videoContent.getElementsByTagName('object')[0]
    || this.videoContent.getElementsByTagName('embed')[0];

  // Subscribe to hide event of overlayMgr.
  this.overlayMgr.onhide.subscribe(this.overlayHiddenHandler, this, true);
  
  // Track shown state.
  this.shown = false;
}

VideoOverlay.prototype =
{
  /* Makes sure the video pauses if the user closed the overlay before the player was ready to receive API calls. */
  playerReadyHandler: function()
  {
    if (!this.shown)
    {
      this.pauseVideo();
    }
  },
  
  /* Stops the video if the overlay is hidden by itself. */
  overlayHiddenHandler: function(e)
  {
    if (this.shown)
    {
      this.pauseVideo();
      this.shown = false;
    }
  },

  // Show the video content in the overlay. Autoplay the video.
  show: function()
  {
    this.shown = true;
    this.overlayMgr.show(this.videoContent);
    this.playVideo();
  },
  
  // Hide the video content. Pause the video.
  hide: function()
  {
    this.pauseVideo();
    this.overlayMgr.hide();
    this.shown = false;
  },

  /* Pauses the video */  
  pauseVideo: function()
  {
    // If the video is ready to receive api calls and is not paused (state 2), pause it.
    if (this.video.getPlayerState && typeof(this.video.getPlayerState()) != 'undefined' && this.video.getPlayerState() != 2)
    {
      this.video.pauseVideo();
    }
    Y.dom.setStyle(this.video, 'visibility', 'hidden');
  },
  
  /* Plays the video */  
  playVideo: function()
  {
    Y.dom.setStyle(this.video, 'visibility', 'visible');
    // If the video is ready to receive api calls and is not playing (state 1), seek to the beginning and play it.
    if (this.video.getPlayerState && typeof(this.video.getPlayerState()) != 'undefined' && this.video.getPlayerState() != 1)
    {
      this.video.seekTo(0, true);
      this.video.playVideo();
    }
  }
}

/**
 * Captures YouTubePlayerReady event. See YouTube JavaScript Player API (http://code.google.com/apis/youtube/js_api_reference.html)
 * Calls playerReadyHandler for appropriate VideoOverlay.
 * @param {String} playerApiId The id of the YouTube video that has become ready to receive API calls.
 */
function onYouTubePlayerReady(playerApiId)
{
  videoOverlays[playerApiId].playerReadyHandler();
}

/**
 * Class for filtering widget.
 * <p>Usage: var newFilter = new Filter(filterEl);</p>
 * @class Filter
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Event
 * @requires YAHOO.util.CustomEvent
 * @constructor
 * @param {String | HTMLElement} filterEl Reference to the filter container element.
 */
Filter = function(filterEl)
{
  this.SELECTED_CLASS = 'sel';
  this.INACTIVE_CLASS = 'inactive';

  /* Get the container element and individual filters */
  this.filter = Y.dom.get(filterEl);
  this.filters = this.filter.getElementsByTagName('a');
  
  /* Create a custom event for a filter change */
  this.onchangefilter = new Y.customEvent('changeFilter', this);
  
  /* Assign click events for each filter. Determine the currently selected filter */
  this.selectedIndex = 0;
  var filter,
      i;
  for (i = 0; i < this.filters.length; i++)
  {
    filter = this.filters[i];
    if (Y.dom.hasClass(filter, this.SELECTED_CLASS))
    {
      this.selectedIndex = i;
    }
    Y.event.on(filter, 'click', this.changeFilter, i, this);
  }
}

Filter.prototype =
{
  /* Retrieves the value of the selected filter */
  getFilter: function()
  {
    return this.filters[this.selectedIndex].getAttribute('rel');
  },
  
  /* Event handler for a filter being clicked */
  changeFilter: function(e, filterIndex)
  {
    var filter = this.filters[filterIndex];
    
    /* Ignore inactive filters */
    if (!Y.dom.hasClass(filter, this.INACTIVE_CLASS))
    {
      /* Fire the custom event and select the filter */
      this.onchangefilter.fire(filter.getAttribute('rel'));
      this.selectFilter(filterIndex);
    }

    /* Disable default link behavior */
    Y.event.stopEvent(e);
  },
  
  /* Deselects the currently selected filter and selects the filter at index */
  selectFilter: function(index)
  {
    Y.dom.removeClass(this.filters[this.selectedIndex], this.SELECTED_CLASS);
    Y.dom.addClass(this.filters[index], this.SELECTED_CLASS);

    this.selectedIndex = index;
  },
  
  /* Enables or disables a filter button */
  activateFilter: function(value, active)
  {
    /* Find the filter with the given value */
    var filter,
        i;
    for (i = 0; i < this.filters.length; i++)
    {
      filter = this.filters[i];
      if (filter.innerHTML == value)
      {
        if (active)
        {
          Y.dom.removeClass(filter, this.INACTIVE_CLASS);
        }
        else
        {
          Y.dom.addClass(filter, this.INACTIVE_CLASS)
        }
        return;
      }
    }
  }
}

/**
 * Class for managing filtered elements.
 * <p>Usage: var newFilteredElements = new FilteredElements(elContainer, filterEl);</p>
 * @class FilteredElements
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Event
 * @requires YAHOO.util.CustomEvent
 * @requires Filter
 * @constructor
 * @param {String | HTMLElement} elContainer Reference to the element containing elements to be filtered.
 * @param {String | HTMLElement} filterEl Reference to the element containing the filter control.
 */
FilteredElements = function(elContainer, filterEl)
{
  // Class identifying elements to be filtered.
  this.EL_CLASS = 'filteredElement';
  // Special filter identifier to show all elements.
  this.ALL_FILTER = 'all';

  // Instantiate the filter.
  this.filter = new Filter(filterEl);
 
  // Find the elements to be filtered.
  this.els = Y.getByClass(this.EL_CLASS, '*', elContainer);
  
  // Subscribe to the filter's onchangefilter event.
  this.filter.onchangefilter.subscribe(this.filterChangeHandler, this, true);
}

FilteredElements.prototype =
{
  /*
    Event handler for the filter's onchangefilter event.
    Checks each element to see if it matches the filter and displays it if it matches.  
  */
  filterChangeHandler: function(e, filterStr)
  {
    // Treat the filter string as a space separated list of filters.
    var filters = filterStr.toString().split(/\s+/),
        i, j,
        el,
        show,
        filter;

    // Check each element.
    for (i = 0; i < this.els.length; i++)
    {
      el = this.els[i];
      show = false;
      
      // Check each filter.
      for (j = 0; j < filters.length; j++)
      {
        // Show the element if it's the all filter or has a class to match the filter.
        filter = filters[j];
        if (filter == this.ALL_FILTER || Y.dom.hasClass(el, filter))
        {
          show = true;
          break;
        }
      }
      
      // Show the element.
      Y.dom.setStyle(el, 'display', show ? 'block' : 'none');
    }
  }
}

/**
 * Class for carousel functionality.
 * <p>Usage: var newCarousel = new Carousel(carouselEl);</p>
 * @class Carousel
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Anim
 * @requires YAHOO.util.Easing
 * @requires YAHOO.util.Event
 * @constructor
 * @param {String | HTMLElement} carouselEl Reference to the carousel container element.
 */
Carousel = function(carouselEl)
{
  this.SEL_PAGE_CLASS = 'sel';
  this.ITEMS_PER_PAGE = 4;

	// Animation settings
	this.DURATION = 0.7;
	this.EASE = Y.easing.easeOutStrong;
	
	// Class name for a disabled button.
	this.DISABLED_CLASS = 'disabled';
	
	// Elements
  this.carousel = Y.dom.get(carouselEl);
	this.itemContainer = Y.getByClass('items', 'ul', this.carousel)[0];
  this.items = this.itemContainer.getElementsByTagName('li');
	this.nav = Y.getByClass('nav', 'tr', this.carousel)[0];
	this.prevBtn = Y.getByClass('prev', 'a', this.carousel)[0];
	this.nextBtn = Y.getByClass('next', 'a', this.carousel)[0];
	this.navItems = [];
  this.counter = Y.getByClass('counter', 'span', this.carousel)[0];
	
	// Calculate item width.
	this.itemWidth = getElementWidth(this.items[0]) +	parseInt(Y.dom.getStyle(this.items[0], 'margin-right')) + parseInt(Y.dom.getStyle(this.items[0], 'margin-left'));
  
  // Track current page.
  this.currPage = 0;
	
	// Load page nav and update item counter.
	this.loadNav();
  this.updateNav(this.currPage);
  this.updateCounter();
};

Carousel.prototype =
{
  /* Populates carousel page navigation. */
  loadNav: function()
  {
    this.numPages = Math.ceil(this.items.length / this.ITEMS_PER_PAGE);
    
    // If only one page, don't bother with navigation.
    if (this.numPages < 2)
    {
      return;
    }

    var i,
        navItem;
    
    // Add pagination.
    for (i = 0; i < this.numPages; i++)
    {
      navItem = this.createNavItem(i);
      this.nav.appendChild(navItem);
      this.navItems[this.navItems.length] = navItem;
    }
    
    // Enable previous and next buttons.
    Y.event.on(this.prevBtn, 'click', this.clickPrevHandler, this, true);
    Y.event.on(this.nextBtn, 'click', this.clickNextHandler, this, true);

    // Display the previous and next buttons.
    Y.dom.setStyle([this.prevBtn, this.nextBtn], 'visibility', 'visible');
  },

  /* Creates a navigation item. */
  createNavItem: function(pageNum)
  {
    var navItem = document.createElement('td'),
        link = document.createElement('a'),
        comment = document.createComment('for IE');
    link.href = '#';
    link.appendChild(comment);
    Y.event.on(link, 'click', this.clickNavHandler, pageNum, this);
    navItem.appendChild(link);
    return navItem;
  },

  /* Updates nav to indicate currently selected page. */
  updateNav: function(newPage)
  {
    Y.dom.removeClass(this.navItems[this.currPage], this.SEL_PAGE_CLASS);
    Y.dom.addClass(this.navItems[newPage], this.SEL_PAGE_CLASS);

    // Disable the buttons if they don't apply.
    if (newPage <= 0)
    {
      Y.dom.addClass(this.prevBtn, this.DISABLED_CLASS); 
    }
    else
    {
      Y.dom.removeClass(this.prevBtn, this.DISABLED_CLASS);
    }
    
    if (newPage >= this.numPages - 1)
    {
      Y.dom.addClass(this.nextBtn, this.DISABLED_CLASS); 
    }
    else
    {
      Y.dom.removeClass(this.nextBtn, this.DISABLED_CLASS);
    }
  },

  /* Updates the counter with the number of carousel items. */
  updateCounter: function()
  {
    this.counter.innerHTML = this.items.length;
  },

  /* Event handler for when a nav item is clicked. */
  clickNavHandler: function(e, pageNum)
  {
    this.goToPage(pageNum);
    Y.event.stopEvent(e);
  },
  
  clickPrevHandler: function(e)
  {
    if (this.currPage > 0)
    {
      this.goToPage(this.currPage - 1);
    }
    Y.event.stopEvent(e);
  },
  
  clickNextHandler: function(e)
  {
    if (this.currPage < this.numPages - 1)
    {
      this.goToPage(this.currPage + 1);
    }
    Y.event.stopEvent(e);
  },

  /* Instructs the carousel to move to a specific page. */
  goToPage: function(pageNum)
  {
	  if (pageNum == this.currPage)
    {
      return;
    }
    var destination = 0 - (pageNum * this.itemWidth * this.ITEMS_PER_PAGE);
    this.move(destination);
    this.updateNav(pageNum);
    this.currPage = pageNum;
  },
  
  /* Moves the carousel. */
  move: function(destination)
  {
    var oAnim = new Y.anim(this.itemContainer,
      {
        left: { to: destination }
      },
      this.DURATION,
      this.EASE
    );
    oAnim.animate();
  }
};

/**
 * Class to display an info bubble on the page.
 * <p>Usage: var newInfoBubble = new InfoBubble(trigger, context, info);</p>
 * @class InfoBubble
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Anim
 * @requires YAHOO.util.Easing
 * @requires YAHOO.util.Event
 * @constructor
 * @param {String | HTMLElement} trigger Reference to the element that will trigger the bubble with a hover.
 * @param {String | HTMLElement} context Reference to the element that the bubble will display next to.
 * @param {String} info The info copy to display in the bubble.
 */
InfoBubble = function(trigger, context, info)
{
  // Offset constants for displaying the info bubble relative to the context element.
  this.OFFSET_X = 132;
  this.OFFSET_Y = -7;
  
  // Class name for the bubble.
  this.BUBBLE_CLASS = 'bubble';
  
  // Animation duration.
  this.DUR = 0.2;
  
  // Get the elements.
  this.trigger = Y.dom.get(trigger);
  this.context = Y.dom.get(context);
  
  if (!this.trigger || !this.context)
  {
    return;
  }
  
  // Create the bubble.
  this.createBubble(info);
  
  // Attach event handlers.
  Y.event.on(this.trigger, 'mouseover', this.mouseOverTriggerHandler, this, true);
  Y.event.on(this.trigger, 'mouseout', this.mouseOutTriggerHandler, this, true);
}

InfoBubble.prototype =
{
  /* Event handler for hovering over the trigger */
  mouseOverTriggerHandler: function(e)
  {
    this.show();
    Y.event.stopEvent(e);
  },
  
  /* Event handler for hovering off the trigger */
  mouseOutTriggerHandler: function(e)
  {
    this.hide();
    Y.event.stopEvent(e);
  },
  
  /* Create the bubble element and appends it to the document */
  createBubble: function(info)
  {
    this.bubble = document.createElement('div');
    Y.dom.addClass(this.bubble, this.BUBBLE_CLASS);
    this.bubble.innerHTML = info;
    document.body.appendChild(this.bubble);
  },
  
  /* Shows the bubble next to the context element with a short fade in */
  show: function()
  {
    Y.dom.setStyle(this.bubble, 'opacity', 0);
    Y.dom.setStyle(this.bubble, 'display', 'block')
    Y.dom.setXY(this.bubble, [Y.dom.getX(this.context) + this.OFFSET_X, Y.dom.getY(this.context) + this.OFFSET_Y]);
    var appear = new Y.anim(this.bubble, { opacity: { from: 0, to: 1 } }, this.DUR, Y.easing.EaseOut);
    appear.animate();
  },
  
  /* Hides the bubble with a short fade out */
  hide: function()
  {
    var fade = new Y.anim(this.bubble, { opacity: { from: 1, to: 0 } }, this.DUR, Y.easing.EaseOut);
    fade.onComplete.subscribe(function()
    {
      Y.dom.setStyle(this.bubble, 'display', 'none');
    }, this, true);
    fade.animate();
  }
}


/**
 * Class to convert a form select into a navigational element.
 * Uses the value of the select to determine the URL to navigate to.
 * <p>Usage: var newSelectNav = new SelectNav(selectEl);</p>
 * @class SelectNav
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Event
 * @constructor
 * @param {String | HTMLElement} selectEl Reference to the form select element that serves as navigation.
 */
SelectNav = function(selectEl)
{
  this.select = Y.dom.get(selectEl);
  
  if (this.select && this.select.options)
  {
    // Attach event handler.
    Y.event.on(this.select, 'change', this.selectChangeHandler, this, true);
  }
}

SelectNav.prototype =
{
  /* Event handler for a change in the selection */
  selectChangeHandler: function(e)
  {
    Y.event.stopEvent(e);
    this.go(this.getUrl());
  },
  
  /* Retrieves the url for the current selection */
  getUrl: function()
  {
    return this.select.options[this.select.selectedIndex].value;
  },
  
  /* Navigates to url */
  go: function(url)
  {
    window.location = url;
  }
}
