/*
 * File: main.js
 * Created: 11/09/2008
 * Description:
 * Global javascript file for grove.com Web site. Relies on XXXX framework (url goes here).
 */
 
 /* 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,
    point:        YAHOO.util.Point,
    ua:           YAHOO.env.ua,
    widget:       YAHOO.widget
  },

  /* Global vars for saving page overlays. */
  overlay, 
  videoOverlay,
  lookInsideOverlay,
  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,
  AddToCartForm,
  MultipleAddToCart,
  ProductGallery,
  
  // Global var for consulting team pages
  teamPages;

/* On load behaviors */ 
Y.event.on(window, 'load', function()
{
// Array of youtube videos.
  var videos =
    [
      { id: 'vidTOMIGF',          url: 'http://www.youtube.com/v/x-DwbbSiarY&hl=en&fs=1&autoplay=0&showsearch=0&enablejsapi=1&playerapiid=viewVidTOMIGF' },
      { id: 'vidVMWK2011',        url: 'http://www.youtube.com/v/LbN8AKbIFZo&hl=en&fs=1&autoplay=0&showsearch=0&enablejsapi=1&playerapiid=viewVidVMWK2011' },
      { id: 'vidVM2011',          url: 'http://www.youtube.com/v/34aQCY8Rk0I&hl=en&fs=1&autoplay=0&showsearch=0&enablejsapi=1&playerapiid=viewVidVM2011' },
      { id: 'vidTpld',            url: 'http://www.youtube.com/v/T6bGbGou7Zg&hl=en&fs=1&autoplay=0&showsearch=0&enablejsapi=1&playerapiid=viewVidTpld' },
      { id: 'vidIFVP2010',        url: 'http://vimeo.com/moogaloop.swf?clip_id=13920360&amp;server=vimeo.com&amp;show_title=1&amp;show_byline=1&amp;show_portrait=1&amp;color=00ADEF&amp;fullscreen=1&amp;autoplay=0&amp;loop=0' },
      { id: 'vidTEDxDS',          url: 'http://www.youtube.com/v/aAjtAI0vNQ8&hl=en&fs=1&autoplay=1&showsearch=0&enablejsapi=1&playerapiid=viewVidTEDxDS' },
      { id: 'vidSToct09',         url: 'http://www.youtube.com/v/nzXM2A9Fc7s&hl=en&fs=1&autoplay=1&showsearch=0&enablejsapi=1&playerapiid=viewVidSToct09' },
      { id: 'vidProdAPK',         url: 'http://content.screencast.com/users/GroveConsultants/folders/Default/media/9fd0922d-7675-41de-bf48-7f1bbd04a06c/flvplayer.swf',
        width: '481', height: '300',
        flashvars: 'thumb= http://content.screencast.com/users/GroveConsultants/folders/Default/media/9fd0922d-7675-41de-bf48-7f1bbd04a06c/FirstFrame.jpg&containerwidth=640&containerheight=378&content=http://content.screencast.com/users/GroveConsultants/folders/Default/media/9fd0922d-7675-41de-bf48-7f1bbd04a06c/APKWebinar_V2.mp4 ',
        base: 'http://content.screencast.com/users/GroveConsultants/folders/Default/media/9fd0922d-7675-41de-bf48-7f1bbd04a06c/' },
      { id: 'vidProdCompass',     url: 'http://content.screencast.com/users/GroveConsultants/folders/Default/media/a4ee7d9f-e0e0-434d-8ea9-ce8f9f1e7d97/flvplayer.swf',
        width: '481', height: '300',
        flashvars: 'thumb= http://content.screencast.com/users/GroveConsultants/folders/Default/media/a4ee7d9f-e0e0-434d-8ea9-ce8f9f1e7d97/FirstFrame.jpg&containerwidth=640&containerheight=378&content=http://content.screencast.com/users/GroveConsultants/folders/Default/media/a4ee7d9f-e0e0-434d-8ea9-ce8f9f1e7d97/Final%20Prod%20HD_YouTube.mp4 ',
        base: 'http://content.screencast.com/users/GroveConsultants/folders/Default/media/a4ee7d9f-e0e0-434d-8ea9-ce8f9f1e7d97/' },
      { 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' },
    // Old flash-based home page
//    homeattrs = { id: 'home', name: 'home', align: 'middle' },
    
    // Array to hold staff ShowHide widgets.
    staff = [],
    faqFilter,
    workshopTable,
    workshopControls,
    workshopDetails,
    i,
    workshopControl,
    workshopShowHides,
    workshopNav,
    navPrefix = '',
    homeFlag;
    
  // Detect the home page by looking for a DOM element with id homepage.
  if (Y.dom.get('homepage'))
  {
    // Get the flag indicating that the user has already visited the home page, set as a cookie.
    homeFlag = Y.cookie.get('homeFlag');
    
    // If the flag is set to true, disable the home page animation.
    params.flashvars = (homeFlag != null && homeFlag == 'true') ? 'disableAnim=true' : 'disableAnim=false';

    // Reset the cookie for another hour.
    createCookie('homeFlag', 'true', 1);

    // Old flash-based home page
    // 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);
//    }
    
    // Clear the flashvars for the left nav.
    params.flashvars = '1=1';
  }

  // 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/';
  }
  
  // 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)
  {
  	if (o.id != 'storeCarousel'){ //instantiated as part of ProductsByCategoryMgr class
  		new Carousel(o);
  	}
  });

  // Activate products-by-category widget on store home page.
  if (Y.dom.get('storeCategories') && Y.dom.get('storeCarousel'))
  {
    new ProductsByCategoryMgr('storeCategories', 'storeCarousel');
  }

  // Use swfobject to load the videos if the corresponding div exists in the page.
  Y.dom.batch(videos, function(video)
  {
    if (Y.dom.get(video.id))
    {
      // Determine the width and height of the video.
      // Default to 481 x 389 for YouTube standard def.
      var width = video.width || 481,
          height = video.height || 389,
          // Set up params and attributes for flash embed.
          params = { allowscriptaccess: 'always', allowfullscreen: 'true' },
          attrs = { name: video.id };
      
      // Set any custom base or flashvars, if provided.
      if (video.base) params.base = video.base;
      if (video.flashvars) params.flashvars = video.flashvars;
      params.base = video.base;
      params.flashvars = video.flashvars;
      swfobject.embedSWF(video.url, video.id, width, height, '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('videoOverlay'))
  {
    videoOverlay = new OverlayMgr('videoOverlay');
  }
  
  if (Y.dom.get('lookInsideOverlay'))
  {
    lookInsideOverlay = new OverlayMgr('lookInsideOverlay');
    new Gallery('lookInsideOverlay');
  }

  /* 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); 
    });
  });
  
  /* Look inside overlays */
  Y.dom.batch(Y.getByClass('lookInsideOverlayTrigger', 'a'), function(o)
  {
    Y.event.on(o, 'click', function(e)
    {
      lookInsideOverlay.show(this.getAttribute('rel'));
      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);
    });
  });
  
  // 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');
    });
  });
  
  // Attach behaviors for store subcategory item rollovers.
  // restrict to modStoreSubCat
  Y.dom.batch(Y.getByClass('modCatItem', 'div', 'modStoreSubCat'), function(o)
  {
    Y.event.on(o, 'mouseover', function(e)
    {
      Y.dom.addClass(this, 'modCatItemHover');
    });
    
    Y.event.on(o, 'mouseout', function(e) {
      Y.dom.removeClass(this, 'modCatItemHover');
    });
  });
  
  // Set up filters. Assumes filtered containers have class "filtered" and "fadeFiltered"
  // and their corresponding filter control has a class name to match the filter
  // container id.
  Y.dom.batch(Y.getByClass('filtered'), function(o)
  {
  	if (o.id != 'productCatFilter'){ //instantiated as part of ProductsByCategoryMgr class
  		new FilteredElements(o, Y.getByClass(o.id)[0]);
  	}
  });
  
  Y.dom.batch(Y.getByClass('fadeFiltered'), function(o)
  {
    new FadedFilteredElements(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]);
  }
  
  // Initialize add to cart forms.
  new AddToCartForm('addToCart');
  new AddToCartForm('download');
  
  Y.dom.batch(Y.getByClass('gallery', 'div'), function(o)
  {
    new Gallery(o);
  });
  
  Y.dom.batch(Y.getByClass('prodGallery', 'div'), function(o)
  {
    new ProductGallery(o);
  });
});
/* END window.onload event handler */

/* 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:    'none',
    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);
  
  /* Create a custom event for a filter change */
  this.onchangefilter = new Y.customEvent('changeFilter', this);
}

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');
    }

    /* Fire the custom event */
    this.onchangefilter.fire(filters);
  }
}

/**
 * Class for managing filtered elements.
 * Fades out elements that don't match the filter.
 * <p>Usage: var newFadedFilteredElements = new FadedFilteredElements(elContainer, filterEl);</p>
 * @class FadedFilteredElements
 * @requires YAHOO.util.Anim
 * @requires YAHOO.util.CustomEvent
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Easing
 * @requires YAHOO.util.Event
 * @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.
 */
FadedFilteredElements = function(elContainer, filterEl)
{
  // Class identifying elements to be filtered.
  this.EL_CLASS = 'filteredElement';
  // Special filter identifier to show all elements.
  this.ALL_FILTER = 'all';
  // Opacity value to fade to.
  this.FADED_OPACITY = 0.2;
  // Duration for fade in seconds.
  this.DURATION = 0.4;
  // Easing for fade.
  this.EASE = Y.easing.easeOut;

  // 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);
}

FadedFilteredElements.prototype =
{
  /*
    Event handler for the filter's onchangefilter event.
    Checks each element to see if it matches the filter.
    Fades out elements that don't match the filter.  
  */
  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,
        fadeIn = [],
        fadeOut = [];

    // Check each element.
    // Determine if the element needs to faded in or faded out.
    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;
        }
      }

      // Save elements that change state.
      if (show && (Y.dom.getStyle(el, 'opacity') < 1))
      {
        fadeIn[fadeIn.length] = el;
      }
      else if (!show && (Y.dom.getStyle(el, 'opacity') > this.FADED_OPACITY))
      {
        fadeOut[fadeOut.length] = el;
      }
    }

    // Fade the elements synchronously.
    this.fadeElements(fadeIn, fadeOut);      
  },
  
  // Fades in and out elements synchronously.
  fadeElements: function(fadeInEls, fadeOutEls)
  {
    // Abort any currently running transition.
    if (this.anim) this.anim.stop();
 
    // Start an empty animation.
    this.anim = fadeAnim = new Y.anim(null, null, this.DURATION);

    // We're animating multiple items, so intercept each tween.      
    fadeAnim.onTween.subscribe(function(type, data)
    {
      Y.dom.setStyle(fadeInEls, 'opacity', this.EASE(data[0].currentFrame, this.FADED_OPACITY, 1 - this.FADED_OPACITY, this.DURATION * 1000));
      Y.dom.setStyle(fadeOutEls, 'opacity', this.EASE(data[0].currentFrame, 1, this.FADED_OPACITY - 1, this.DURATION * 1000));
    }, this, true);
    
    // Clear the saved animation.
    fadeAnim.onComplete.subscribe(function()
    {
      this.anim = null;
    }, this, true);
    
    // Okay, go!
    fadeAnim.animate();
  }
}

/**
 * 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');
  },
  
  /* Clears the navigation controls */
  clearNav: function()
  {
    // Remove the pagination.
    Y.dom.batch(this.navItems, function(o)
    {
      o.parentNode.removeChild(o);
      
    });
    this.navItems = [];
    
    // Disable and hide the previous and next buttons.
    Y.event.removeListener(this.prevBtn, 'click');
    Y.event.removeListener(this.nextBtn, 'click');
    Y.dom.setStyle([this.prevBtn, this.nextBtn], 'visibility', 'hidden');
  },

  /* 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, disableAnim)
  {
    var destination = 0 - (pageNum * this.itemWidth * this.ITEMS_PER_PAGE);
    if (disableAnim)
    {
      Y.dom.setStyle(this.itemContainer, 'left', '0px');
    }
    else
    {
      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;
  }
}

/**
 * Class to handle interactions for add to cart store forms.
 * Assumes that submit buttons are identified by class name submit.
 * Assumes that quantity fields are identified by class name qty.
 * Assumes that required checkboxes are identified by class name checkRequired.
 * Validates form by making sure total quantity is greater than 0 and that all required checkboxes are checked.
 * Enables and disables add to cart button based on validity of form.
 * Supports MultipleAddToCart widgets.
 * Technically, this could be built more modular by setting up a validation framework with individual validators
 * for different form elements including a custom validator for total quantity and a specialized validator for
 * MultipleAddToCart widgets. However, this should be sufficient for our needs.
 * <p>Usage: var newAddToCartForm = new AddToCartForm(formEl);</p>
 * @class AddToCartForm
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Event
 * @requires YAHOO.util.CustomEvent
 * @constructor
 * @param {String | HTMLElement} formEl Reference to the form element.
 */
AddToCartForm = function(formEl)
{
  this.SUBMIT_CLASS = 'submit';
  this.QUANTITY_CLASS = 'qty';
  this.CHECK_REQ_CLASS = 'checkRequired';
  this.MULTI_ADD_CLASS = 'multiAddToCart';
  
  this.form = Y.dom.get(formEl);
  if (!this.form) return;
  
  // We'll need the submit buttons so that we can disable them.
  this.submitBtns = Y.getByClass(this.SUBMIT_CLASS, 'input', this.form);
  
  // Intercept the form submit
  Y.event.on(this.form, 'submit', this.onSubmitForm, this, true);

  // Set up the quantity fields.  
  this.updateQuantityFields();
  
  // Get the required checkbox fields. Used to validate agreement to policies.
  this.checkRequiredFields = Y.getByClass(this.CHECK_REQ_CLASS, 'input', this.form);
  Y.event.on(this.checkRequiredFields, 'click', this.onClickCheckRequired, this, true);
    
  // See if there's any multiple add to cart widgets.
  Y.dom.batch(Y.getByClass(this.MULTI_ADD_CLASS, '*', this.form), function(o) {
    var multiAddToCart = new MultipleAddToCart(o);
    
    // Subscribe to add and remove events of MultipleAddToCart.
    multiAddToCart.onadd.subscribe(this.onChangeMultiAddToCart, this, true);
    multiAddToCart.ondelete.subscribe(this.onChangeMultiAddToCart, this, true);
  }, this, true);
}

AddToCartForm.prototype =
{
  // Event handler for a form submit.
  onSubmitForm: function(e)
  {
    // Abort if we don't validate.
    if (!this.validate())
    {
      Y.event.stopEvent(e);
    }
  },
  
  // Event handler for a change in quantity.
  onChangeQuantity: function(e)
  {
    // Enable/disable the submit buttons.
    this.updateSubmitButtons();
  },
  
  // Event handler for a click on a required checkbox.
  onClickCheckRequired: function(e)
  {
    // Enable/disable the submit buttons.
    this.updateSubmitButtons();
  },
  
  // Event handler for a change in the number of rows in a MultipleAddToCart widget.
  onChangeMultiAddToCart: function(e)
  {
    // Reset the quantity fields.
    this.updateQuantityFields();
    // Enable/disable the submit buttons.
    this.updateSubmitButtons();
  },
  
  // Gets the quantity fields in the form and attaches event handlers for quantity changes.
  updateQuantityFields: function()
  {
    this.quantityFields = Y.getByClass(this.QUANTITY_CLASS, 'input', this.form);
    // Reset the event handlers.
    Y.event.removeListener(this.quantityFields, 'change');
    Y.event.on(this.quantityFields, 'change', this.onChangeQuantity, this, true);
  },
  
  // Enables or disables the submit buttons based on whether or not the form validates.
  updateSubmitButtons: function()
  {
    if (this.validate())
    {
      Y.dom.removeClass(this.submitBtns, 'disabled');  
    }
    else
    {
      Y.dom.addClass(this.submitBtns, 'disabled');  
    }
  },
  
  // Validates the form.
  validate: function()
  {
    // Make sure that all required checkboxes are checked and the quantity total is greater than 0.
    return (this.validateCheckRequired() && (this.getTotal() > 0));
  },
  
  // Validates that all required checkboxes are checked.
  validateCheckRequired: function()
  {
    for (var i = 0; i < this.checkRequiredFields.length; i++)
    {
      if (!this.checkRequiredFields[i].checked) return false;
    }
    return true;
  },
  
  // Validates and corrects each quantity field to make sure the value is a positive integer.
  // Returns the total quantity.
  getTotal: function()
  {
    var total = 0;
    Y.dom.batch(this.quantityFields, function(o)
    {
      // If value is empty, spaces or NaN, make it 0. Otherwise, parseInt the value.
      o.value = ((/^\s*$/).test(o.value) || isNaN(o.value)) ? 0 : parseInt(o.value);
      // Make sure the value is a positive number. 
      if (o.value < 0) o.value = 0;
      total += o.value;
    });
    return total;
  }
}

/**
 * Widget to handle multiple add to cart functionality.
 * Assumes that there is a template row for each add to cart line identified by class name template.
 * Assumes the add button is identified by class name add.
 * Assumes each item field is identified by class name item.
 * Assumes each quantity field is identified by class name qty.
 * Assumes each delete button is identified by class name del.
 * Handles adding and removing of add to cart lines.
 * Renumbers field ids and names in each add to cart line automatically.
 * Assumes each field id and name follows the pattern "identifier#"
 * (e.g. "item1" could be the id and/or name for the item field of the first add to cart line).
 * Assumes the form starts out with one row only (the template row).
 * Provides custom events onadd and ondelete that fire when an add to cart line is added or removed.
 * <p>Usage: var newMultipleAddToCart = new MultipleAddToCart(multiEl);</p>
 * @class MultipleAddToCart
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Event
 * @requires YAHOO.util.CustomEvent
 * @constructor
 * @param {String | HTMLElement} multiEl Reference to the container element for the multiple add to cart widget.
 */
MultipleAddToCart = function(multiEl)
{
  this.TEMPLATE_CLASS = 'template';
  this.ADD_CLASS = 'add';
  this.ITEM_CLASS = 'item';
  this.QUANTITY_CLASS = 'qty';
  this.DELETE_CLASS = 'del';

  this.container = Y.dom.get(multiEl);
  if (!this.container) return;
  
  // Get the first row, set up vars for future use.
  var firstRow = Y.getByClass(this.TEMPLATE_CLASS, '*', this.container)[0],
      itemSelect,
      quantityField,
      deleteField,
      re = /^(.+)[0-9]+$/,  // RegExp for the id and name pattern. e.g. "identifier#"
      matches;
      
  // Save a clone of the first row so that we can replicate later. Don't add it to the dom.
  this.template = firstRow.cloneNode(true);
  // Track our rows in an array. Start with the first row.
  this.rows = [ firstRow ];

  // Get the fields from our template.
  itemSelect = this.getItemSelect(this.template);
  quantityField = this.getQuantityField(this.template);
  deleteField = this.getDeleteField(this.template);

  // Determine the maximum number of rows to create. Allow as many rows as there are unique items to select from.
  this.maxRows = itemSelect.getElementsByTagName('option').length;

  // Pattern match for the id and name prefix for item selects.
  matches = re.exec(itemSelect.name);
  this.itemSelect = { namePrefix: matches[1]};
  
  matches = re.exec(itemSelect.id);
  this.itemSelect.idPrefix = matches[1];

  // Pattern match for the id and name prefix for quantity fields.
  matches = re.exec(quantityField.name);
  this.quantityField = { namePrefix: matches[1]};
  
  matches = re.exec(quantityField.id);
  this.quantityField.idPrefix = matches[1];

  // Pattern match for the id and name prefix for delete buttons.
  matches = re.exec(deleteField.name);
  this.deleteField = { namePrefix: matches[1] };
  
  matches = re.exec(deleteField.id);
  this.deleteField.idPrefix = matches[1];
  
  // Attach event handler for clicking on first row delete button. Pass in the first row as an argument.
  Y.event.on(this.getDeleteField(firstRow), 'click', this.onClickDelete, firstRow, this);

  // Save the add button and attach an event handler.
  this.addBtn = Y.getByClass(this.ADD_CLASS, 'input', this.container);
  Y.event.on(this.addBtn, 'click', this.onAddClick, this, true);
  
  // Create a custom event for adding a row.
  this.onadd = new Y.customEvent('add', this);
  // Create a custom event for removing a row.
  this.ondelete = new Y.customEvent('remove', this);
}

MultipleAddToCart.prototype =
{
  // Event handler for clicking on the add button.
  onAddClick: function(e)
  {
    // Add a row if we aren't at the max.
    if (this.rows.length < this.maxRows)
    {
      this.addRow();
    }
    Y.event.stopEvent(e);
  },
  
  // Event handler for clicking on a delete button.
  onClickDelete: function(e, row)
  {
    // Delete the row if it's not the last.
    if (this.rows.length > 1)
    {
      this.deleteRow(row);
    }
    Y.event.stopEvent(e);
  },
  
  // Adds a new add to cart line.
  addRow: function()
  {
    // Clone the template, find the last row to append to.
    var newRow = this.template.cloneNode(true),
        lastRow = this.rows[this.rows.length - 1];
    
    // Initialize the new row.
    this.initRow(newRow);
    
    // Add the row to the dom and track it in our local array.
    Y.dom.insertAfter(newRow, lastRow);
    this.rows[this.rows.length] = newRow;

    // Enable all delete buttons.
    Y.dom.removeClass(Y.getByClass(this.DELETE_CLASS, 'input', this.container), 'disabled');
    
    // If we're at the max rows, disable the add button.
    if (this.rows.length >= this.maxRows)
    {
      Y.dom.addClass(this.addBtn, 'disabled');
    }
    
    // Fire the custom event. Pass in the added row.
    this.onadd.fire(newRow);
  },
  
  // Deletes an add to cart line.
  deleteRow: function(row)
  {
    var newRows = [],
        i,
        currRow;

    // Remove the row from our local array.
    for (i = 0; i < this.rows.length; i++)
    {
      currRow = this.rows[i];
      if (currRow != row)
      {
        // Renumber the rows as we go.
        this.setRowIdAndName(currRow, newRows.length + 1);
        newRows[newRows.length] = currRow;
      }
    }
    this.rows = newRows;
    
    // Enable the add button.
    Y.dom.removeClass(this.addBtn, 'disabled');
    
    // Disable all delete buttons if we're down to our last line.
    if (this.rows.length <= 1)
    {
      Y.dom.addClass(Y.getByClass(this.DELETE_CLASS, 'input', this.container), 'disabled');
    }
    
    // Remove the row from the dom.
    row.parentNode.removeChild(row);
    
    // Fire the custom event. Pass in the deleted row.
    this.ondelete.fire(row);
  },
  
  // Initializes a new row.
  initRow: function(row)
  {
    // Number the field ids and names.
    this.setRowIdAndName(row, this.rows.length + 1);

    // Deselect all item options.
    Y.dom.batch(this.getItemSelect(row).getElementsByTagName('option'), function(o)
    {
      o.selected = false;
    });

    // Zero the quantity field.
    this.getQuantityField(row).value = 0;

    // Attach an event handler to the delete button. Pass in the row as an argument.    
    Y.event.on(this.getDeleteField(row), 'click', this.onClickDelete, row, this);
  },
  
  // Numbers the fields in a row with the given index.
  setRowIdAndName: function(row, index)
  {
    var itemSelect = this.getItemSelect(row),
        quantityField = this.getQuantityField(row),
        deleteField = this.getDeleteField(row);

    itemSelect.id = this.itemSelect.idPrefix + index;
    itemSelect.name = this.itemSelect.namePrefix + index;

    quantityField.id = this.quantityField.idPrefix + index;
    quantityField.name = this.quantityField.namePrefix + index;

    deleteField.id = this.deleteField.idPrefix + index;
    deleteField.name = this.deleteField.namePrefix + index;
  },
  
  // Finds the item select for an add to cart line.
  getItemSelect: function(row)
  {
    return Y.getByClass(this.ITEM_CLASS, 'select', row)[0];
  },
  
  // Finds the quantity field for an add to cart line.
  getQuantityField: function(row)
  {
    return Y.getByClass(this.QUANTITY_CLASS, 'input', row)[0];
  },
  
  // Finds the delete button for an add to cart line.
  getDeleteField: function(row)
  {
    return Y.getByClass(this.DELETE_CLASS, 'input', row)[0];
  }
}

/**
 * Class for browsing products by category functionality on store home page.
 * This class makes use of the Carousel class.
 * <p>Usage: var productsByCategoryMgr = new ProductsByCategoryMgr(categoryList, carouselEl);</p>
 * @class ProductsByCategoryMgr
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Anim
 * @requires YAHOO.util.Easing
 * @requires YAHOO.util.Event
 * @constructor
 * @param {String | HTMLElement} categoryList Reference to the UL containing the category links.
 * @param {String | HTMLElement} carouselEl Reference to the carousel container element.
 */
ProductsByCategoryMgr = function(categoryList, carouselEl)
{
	// Animation settings
	this.DURATION = 0.7;
	this.EASE = Y.easing.easeOutStrong;
	this.CAROUSEL_HEIGHT = 189;
	
	// Elements
  this.catContainer = Y.dom.get(categoryList);
  this.carouselEl = Y.dom.get(carouselEl);
  this.carouselCont = this.carouselEl.parentNode;
  
  // Instantiate carousel and update settings
  this.carousel = new Carousel(this.carouselEl);
  this.carousel.ITEMS_PER_PAGE = 3;
  
  if (YAHOO.env.ua.ie)
  {
    Y.dom.setStyle(this.carouselEl, 'display', 'none');
  }
  
  // Instantiate filter for carousel items
	this.filter = new FilteredElements(this.carouselEl, this.catContainer);
 
  // Subscribe to a filter change.
  this.filter.onchangefilter.subscribe(this.selectCatHandler, this, true);
  
  // Track whether carousel is shown.
  this.carouselShown = false;
};

ProductsByCategoryMgr.prototype =
{
  /* Event handler for category buttons. */
  selectCatHandler: function(e, filterStr)
  {
		// Update carousel items
		this.updateCarouselCtrls();
		// Open carousel if not already open
		this.showCarousel();
  },

  /* Shows the carousel. */
  showCarousel: function()
  {
    // Open carousel if not already open
		if (!this.carouselShown)
		{
	    var oAnimDrawer = new Y.anim(this.carouselCont,
        {
          height: { to: this.CAROUSEL_HEIGHT }
        },
        this.DURATION,
        this.EASE
      );
	    oAnimDrawer.onComplete.subscribe(function(type, args)
	    {
	      if (YAHOO.env.ua.ie)
	      {
	        Y.dom.setStyle(this.carouselEl, 'display', 'block');
	      }
	      else
	      {
          var oAnimFade = new Y.anim(this.carouselEl,
            {
              opacity: { to: 1 }
            },
            this.DURATION,
            this.EASE
          );
          oAnimFade.animate();
	      }
      }, this, true);
	    oAnimDrawer.animate();
	    
	    this.carouselShown = true;
		}
  },

  /* Updates the carousel navigation based on filtering. */
  updateCarouselCtrls: function()
  {
		// Reset items based on how many elements are showing
  	var allProds = Y.dom.getElementsByClassName('filteredElement', 'li', this.carouselEl),
  	    prods = [];
  	Y.dom.batch(allProds, function(o){
  		if (Y.dom.getStyle(o, 'display') != 'none') prods.push(o);
  	});

  	// Set the new items.
  	this.carousel.items = prods;
  	
  	// Reload the nav.
  	this.carousel.clearNav();
  	this.carousel.loadNav();
  	
  	// Go to page 0, don't animate.
  	this.carousel.goToPage(0, true);
  }
};

/**
 * Widget for displaying a gallery of images.
 * Assumes widget components are marked by specifc class names (see constants in constructor). 
 * <p>Usage: var newGallery = new Gallery(galleryEl);</p>
 * @class Gallery
 * @requires YAHOO.util.Anim
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Easing
 * @requires YAHOO.util.Event
 * @constructor
 * @param {String | HTMLElement} galleryEl Reference to the gallery container element.
 */
Gallery = function(galleryEl)
{
  // Class name constants.
  this.SHOWN_CLASS = 'shown';
  this.CONTROLS_CLASS = 'control';
  this.COUNT_CLASS = 'count';
  this.PREV_CLASS = 'prev';
  this.NEXT_CLASS = 'next';
  
  // Delimiter for image count display.
  this.COUNT_DELIM = '/';

  // Transition params for changing images.
  this.DURATION = 0.3;
  this.EASE = Y.easing.easeIn;
  
  this.gallery = Y.dom.get(galleryEl);
  if (!this.gallery) return;
  
  // Get the widget components.
  var counts = Y.getByClass(this.COUNT_CLASS, '*', this.gallery),
      i;
  if (counts.length > 0)
  {
    this.count = counts[0];
  }
  this.controls = Y.getByClass(this.CONTROLS_CLASS, 'div', this.gallery)[0];
  this.prevBtn = Y.getByClass(this.PREV_CLASS, 'a', this.controls)[0];
  this.nextBtn = Y.getByClass(this.NEXT_CLASS, 'a', this.controls)[0]

  this.images = this.gallery.getElementsByTagName('img');
  
  // Track the index of the currently shown image.
  this.currIndex = 0;
  for (i = 0; i < this.images.length; i++)
  {
    if (Y.dom.hasClass(this.images[i], this.SHOWN_CLASS))
    {
      this.currIndex = i;
      break;
    }
  }
  
  // Update the count.
  this.updateCount();
  
  // Set event handlers for previous and next buttons.
  Y.event.on(this.prevBtn, 'click', this.onClickPrev, this, true);
  Y.event.on(this.nextBtn, 'click', this.onClickNext, this, true);
}

Gallery.prototype =
{
  // Event handler for clicking on the previous button.
  onClickPrev: function(e)
  {
    // Wait till we're done animating image transitions
    if (!this.isAnimating)
    {
      // Show the previous image. If at the beginning, move to the end.
      this.showImage((this.currIndex <= 0) ? (this.images.length - 1) : (this.currIndex - 1));
    }
    // Abort default click event.
    Y.event.stopEvent(e);
  },
  
  // Event handler for clicking on the next button.
  onClickNext: function(e)
  {
    // Wait till we're done animating image transitions
    if (!this.isAnimating)
    {
      // Show the next image. If at the end, move to the beginning.
      this.showImage((this.currIndex >= (this.images.length - 1)) ? 0 : (this.currIndex + 1));
    }
    // Abort default click event.
    Y.event.stopEvent(e);  
  },
  
  // Shows the image at index with a nice crossfade.
  showImage: function(index)
  {
    // Don't bother if the image doesn't change.
    if (index == this.currIndex) return;
    
    // Start an empty animation.
    var crossFadeAnim = new Y.anim(null, null, this.DURATION);

    // We're animating multiple items, so intercept each tween.      
    crossFadeAnim.onTween.subscribe(function(type, data)
    {
      // Crossfade the opacity of the currently selected image and the newly selected image.
      Y.dom.setStyle(this.images[this.currIndex], 'opacity', this.EASE(data[0].currentFrame, 1, -1, this.DURATION * 1000));
      Y.dom.setStyle(this.images[index], 'opacity', this.EASE(data[0].currentFrame, 0, 1, this.DURATION * 1000));
    }, this, true);
    
    // After animating, set the display on the images and update the caption.
    crossFadeAnim.onComplete.subscribe(function()
    {
      // Hide the old image.
      Y.dom.removeClass(this.images[this.currIndex], this.SHOWN_CLASS);

      // Update the index tracker.
      this.currIndex = index;

      // Update the count.
      this.updateCount();
  
      this.isAnimating = false;
    }, this, true);

    // Prepare to start the transition.
    // Set the opacity of the new image to 0 and show it.
    Y.dom.setStyle(this.images[index], 'opacity', 0);
    Y.dom.addClass(this.images[index], this.SHOWN_CLASS);
    
    // Track the animation.
    this.isAnimating = true;
    
    // Okay, go!
    crossFadeAnim.animate();
  },
  
  // Updates the image count display.
  updateCount: function()
  {
    if (this.count)
    {
      this.count.innerHTML = (this.currIndex + 1) + this.COUNT_DELIM + this.images.length;
    }
  }
}

/**
 * Widget for displaying a gallery of product images. For use on product page.
 * Has additional effects and interactions beyond a standard gallery.
 * Assumes widget components are marked by specifc class names (see constants in constructor). 
 * <p>Usage: var newProductGallery = new ProductGallery(galleryEl);</p>
 * @class ProductGallery
 * @requires YAHOO.util.Anim
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Easing
 * @requires YAHOO.util.Event
 * @requires YAHOO.util.Region
 * @constructor
 * @param {String | HTMLElement} galleryEl Reference to the gallery container element.
 */
ProductGallery = function(galleryEl)
{
  // Class name constants.
  this.SHOWN_CLASS = 'shown';
  this.PROMPT_CLASS = 'prompt';
  this.CONTROLS_CLASS = 'control';
  this.CAPTION_CLASS = 'caption';
  this.PANEL_CLASS = 'panel';
  this.NAME_CLASS = 'name';
  this.COUNT_CLASS = 'count';
  this.PREV_CLASS = 'prev';
  this.NEXT_CLASS = 'next';

  // Height of the caption in pixels.
  this.CAPTION_HEIGHT = 21;
  
  // Delimiter for image count display.
  this.COUNT_DELIM = ' of ';
  
  // Transition params for display of controls.
  this.CONTROL_DURATION = 0.4;
  this.CONTROL_EASE = Y.easing.easeOut;

  // Transition params for changing images.
  this.IMAGE_DURATION = 0.3;
  this.IMAGE_EASE = Y.easing.easeIn;
  
  this.gallery = Y.dom.get(galleryEl);
  if (!this.gallery) return;
  
  // Get the widget components.
  this.displayPrompt = Y.getByClass(this.PROMPT_CLASS, 'div', this.gallery)[0];
  
  this.caption = Y.getByClass(this.CAPTION_CLASS, 'div', this.gallery)[0];
  this.captionPanel = Y.getByClass(this.PANEL_CLASS, 'div', this.gallery)[0];
  this.captionName = Y.getByClass(this.NAME_CLASS, '*', this.caption)[0];
  this.captionCount = Y.getByClass(this.COUNT_CLASS, '*', this.caption)[0];
  
  this.controls = Y.getByClass(this.CONTROLS_CLASS, 'div', this.gallery)[0];
  this.prevBtn = Y.getByClass(this.PREV_CLASS, 'a', this.controls)[0];
  this.nextBtn = Y.getByClass(this.NEXT_CLASS, 'a', this.controls)[0]

  this.images = this.gallery.getElementsByTagName('img');
  
  // Track the index of the currently shown image.
  this.currIndex = 0;
  for (var i = 0; i < this.images.length; i++)
  {
    if (Y.dom.hasClass(this.images[i], this.SHOWN_CLASS))
    {
      this.currIndex = i;
      break;
    }
  }
  
  // Populate the caption.
  this.updateCaption();
  
  // Track whether the controls are show.
  this.isShown = false;
  
  // Track mouse movement on the page.
  Y.event.on(document, 'mousemove', this.onMousemove, this, true);
  
  // Set event handlers for previous and next buttons.
  Y.event.on(this.prevBtn, 'click', this.onClickPrev, this, true);
  Y.event.on(this.nextBtn, 'click', this.onClickNext, this, true);
}

ProductGallery.prototype =
{
  // Event handler for a mouse move.
  onMousemove: function(e)
  {
    // Determine if the cursor is inside the gallery.
    var pos = new Y.point(Y.event.getPageX(e), Y.event.getPageY(e))
    if (Y.region.getRegion(this.gallery).contains(pos))
    {
      if (!this.isShown)
      {
        // Abort any existing attempt to hide the controls.
        if (this.hideAnim) this.hideAnim.stop();
        this.showControls();
      }
    }
    else
    {
      if (this.isShown)
      {
        // Abort any existing attempt to show the controls.
        if (this.showAnim) this.showAnim.stop();
        this.hideControls();
      }
    }
  },
  
  // Event handler for clicking on the previous button.
  onClickPrev: function(e)
  {
    // Wait till we're done animating image transitions
    if (!this.isAnimating)
    {
      // Show the previous image. If at the beginning, move to the end.
      this.showImage((this.currIndex <= 0) ? (this.images.length - 1) : (this.currIndex - 1));
    }
    // Abort default click event.
    Y.event.stopEvent(e);
  },
  
  // Event handler for clicking on the next button.
  onClickNext: function(e)
  {
    // Wait till we're done animating image transitions
    if (!this.isAnimating)
    {
      // Show the next image. If at the end, move to the beginning.
      this.showImage((this.currIndex >= (this.images.length - 1)) ? 0 : (this.currIndex + 1));
    }
    // Abort default click event.
    Y.event.stopEvent(e);  
  },
  
  // Displays the controls with a nice animation.
  showControls: function()
  {
    // Start an empty animation.
    this.showAnim = crossFadeAnim = new Y.anim(null, null, this.CONTROL_DURATION);

    // We're animating multiple items, so intercept each tween.      
    crossFadeAnim.onTween.subscribe(function(type, data)
    {
      // Crossfade the opacity of the elements.
      Y.dom.setStyle(this.displayPrompt, 'opacity', this.CONTROL_EASE(data[0].currentFrame, 1, -1, this.CONTROL_DURATION * 1000));
      Y.dom.setStyle(this.controls, 'opacity', this.CONTROL_EASE(data[0].currentFrame, 0, 1, this.CONTROL_DURATION * 1000));
      // Slide in the caption
      Y.dom.setStyle(this.caption, 'height', this.CONTROL_EASE(data[0].currentFrame, 0, this.CAPTION_HEIGHT, this.CONTROL_DURATION * 1000) + 'px');
      Y.dom.setStyle(this.captionPanel, 'top', this.CONTROL_EASE(data[0].currentFrame, -1 * this.CAPTION_HEIGHT, this.CAPTION_HEIGHT, this.CONTROL_DURATION * 1000) + 'px');
    }, this, true);
    
    // After animating set the display of the elements.
    crossFadeAnim.onComplete.subscribe(function()
    {
      Y.dom.setStyle(this.displayPrompt, 'display', 'none');
      
      // Clear the animation.
      this.showAnim = null;
    }, this, true);

    // Prepare to start the transition.
    // Set the opacity of the controls to 0 and show it.
    Y.dom.setStyle(this.controls, 'opacity', 0);
    Y.dom.setStyle(this.controls, 'display', 'block');
    
    // Set the height of the caption to 0, slide the panel out of sight, display the caption.
    Y.dom.setStyle(this.caption, 'height', '0px');
    Y.dom.setStyle(this.captionPanel, 'top', '-' + this.CAPTION_HEIGHT + 'px')
    Y.dom.setStyle(this.caption, 'display', 'block');
    
    // Track the shown state.
    this.isShown = true;

    // Okay, go!
    crossFadeAnim.animate();
  },
  
  // Hides the controls with a nice animation.
  hideControls: function()
  {
    // Start an empty animation.
    this.hideAnim = crossFadeAnim = new Y.anim(null, null, this.CONTROL_DURATION);

    // We're animating multiple items, so intercept each tween.      
    crossFadeAnim.onTween.subscribe(function(type, data)
    {
      // Crossfade the opacity of the elements.
      Y.dom.setStyle(this.displayPrompt, 'opacity', this.CONTROL_EASE(data[0].currentFrame, 0, 1, this.CONTROL_DURATION * 1000));
      Y.dom.setStyle(this.controls, 'opacity', this.CONTROL_EASE(data[0].currentFrame, 1, -1, this.CONTROL_DURATION * 1000));
      // Slide out the caption
      Y.dom.setStyle(this.caption, 'height', this.CONTROL_EASE(data[0].currentFrame, this.CAPTION_HEIGHT, -1 * this.CAPTION_HEIGHT, this.CONTROL_DURATION * 1000) + 'px');
      Y.dom.setStyle(this.captionPanel, 'top', this.CONTROL_EASE(data[0].currentFrame, 0, -1 * this.CAPTION_HEIGHT, this.CONTROL_DURATION * 1000) + 'px');
    }, this, true);
    
    // After animating set the display of the elements.
    crossFadeAnim.onComplete.subscribe(function()
    {
      Y.dom.setStyle(this.controls, 'display', 'none');
      Y.dom.setStyle(this.caption, 'display', 'none');
      
      // Clear the animation.
      this.hideAnim = null;
    }, this, true);

    // Prepare to start the transition.
    // Set the opacity of the prompt to 0 and show it.
    Y.dom.setStyle(this.displayPrompt, 'opacity', 0);
    Y.dom.setStyle(this.displayPrompt, 'display', 'block');
    
    // Track the shown state.
    this.isShown = false;

    // Okay, go!
    crossFadeAnim.animate();
  },
  
  // Shows the image at index with a nice crossfade.
  showImage: function(index)
  {
    // Don't bother if the image doesn't change.
    if (index == this.currIndex) return;
    
    // Start an empty animation.
    var crossFadeAnim = new Y.anim(null, null, this.IMAGE_DURATION);

    // We're animating multiple items, so intercept each tween.      
    crossFadeAnim.onTween.subscribe(function(type, data)
    {
      // Crossfade the opacity of the currently selected image and the newly selected image.
      Y.dom.setStyle(this.images[this.currIndex], 'opacity', this.IMAGE_EASE(data[0].currentFrame, 1, -1, this.IMAGE_DURATION * 1000));
      Y.dom.setStyle(this.images[index], 'opacity', this.IMAGE_EASE(data[0].currentFrame, 0, 1, this.IMAGE_DURATION * 1000));
    }, this, true);
    
    // After animating, set the display on the images and update the caption.
    crossFadeAnim.onComplete.subscribe(function()
    {
      // Hide the old image.
      Y.dom.removeClass(this.images[this.currIndex], this.SHOWN_CLASS);

      // Update the index tracker.
      this.currIndex = index;

      // Update the caption.
      this.updateCaption();
  
      this.isAnimating = false;
    }, this, true);

    // Prepare to start the transition.
    // Set the opacity of the new image to 0 and show it.
    Y.dom.setStyle(this.images[index], 'opacity', 0);
    Y.dom.addClass(this.images[index], this.SHOWN_CLASS);
    
    // Track the animation.
    this.isAnimating = true;
    
    // Okay, go!
    crossFadeAnim.animate();
  },
  
  // Updates the caption text and count.
  updateCaption: function()
  {
    this.captionCount.innerHTML = (this.currIndex + 1) + this.COUNT_DELIM + this.images.length;
    // Use the image alt attribute for the caption text.
    this.captionName.innerHTML = this.images[this.currIndex].getAttribute('alt');
  }
}

