YUI recommends YUI 3.

YUI 2 has been deprecated since 2011. This site acts as an archive for files and documentation.

Yahoo! UI Library

Carousel Widget  2.7.0

Yahoo! UI Library > carousel > Carousel.js (source view)
Search:
 
Filters
/**
 * The Carousel module provides a widget for browsing among a set of like
 * objects represented pictorially.
 *
 * @module carousel
 * @requires yahoo, dom, event, element
 * @optional animation
 * @namespace YAHOO.widget
 * @title Carousel Widget
 * @beta
 */
(function () {

    var WidgetName;             // forward declaration

    /**
     * The Carousel widget.
     *
     * @class Carousel
     * @extends YAHOO.util.Element
     * @constructor
     * @param el {HTMLElement | String} The HTML element that represents the
     * the container that houses the Carousel.
     * @param cfg {Object} (optional) The configuration values
     */
    YAHOO.widget.Carousel = function (el, cfg) {
        YAHOO.log("Component creation", WidgetName);

        YAHOO.widget.Carousel.superclass.constructor.call(this, el, cfg);
    };

    /*
     * Private variables of the Carousel component
     */

    /* Some abbreviations to avoid lengthy typing and lookups. */
    var Carousel    = YAHOO.widget.Carousel,
        Dom         = YAHOO.util.Dom,
        Event       = YAHOO.util.Event,
        JS          = YAHOO.lang;

    /**
     * The widget name.
     * @private
     * @static
     */
    WidgetName = "Carousel";

    /**
     * The internal table of Carousel instances.
     * @private
     * @static
     */
    var instances = {},

    /*
     * Custom events of the Carousel component
     */

    /**
     * @event afterScroll
     * @description Fires when the Carousel has scrolled to the previous or
     * next page.  Passes back the index of the first and last visible items in
     * the Carousel.  See
     * <a href="YAHOO.util.Element.html#addListener">Element.addListener</a>
     * for more information on listening for this event.
     * @type YAHOO.util.CustomEvent
     */
    afterScrollEvent = "afterScroll",

    /**
     * @event allItemsRemovedEvent
     * @description Fires when all items have been removed from the Carousel.
     * See
     * <a href="YAHOO.util.Element.html#addListener">Element.addListener</a>
     * for more information on listening for this event.
     * @type YAHOO.util.CustomEvent
     */
    allItemsRemovedEvent = "allItemsRemoved",

    /**
     * @event beforeHide
     * @description Fires before the Carousel is hidden.  See
     * <a href="YAHOO.util.Element.html#addListener">Element.addListener</a>
     * for more information on listening for this event.
     * @type YAHOO.util.CustomEvent
     */
    beforeHideEvent = "beforeHide",

    /**
     * @event beforePageChange
     * @description Fires when the Carousel is about to scroll to the previous
     * or next page.  Passes back the page number of the current page.  Note
     * that the first page number is zero.  See
     * <a href="YAHOO.util.Element.html#addListener">Element.addListener</a>
     * for more information on listening for this event.
     * @type YAHOO.util.CustomEvent
     */
    beforePageChangeEvent = "beforePageChange",

    /**
     * @event beforeScroll
     * @description Fires when the Carousel is about to scroll to the previous
     * or next page.  Passes back the index of the first and last visible items
     * in the Carousel and the direction (backward/forward) of the scroll.  See
     * <a href="YAHOO.util.Element.html#addListener">Element.addListener</a>
     * for more information on listening for this event.
     * @type YAHOO.util.CustomEvent
     */
    beforeScrollEvent = "beforeScroll",

    /**
     * @event beforeShow
     * @description Fires when the Carousel is about to be shown.  See
     * <a href="YAHOO.util.Element.html#addListener">Element.addListener</a>
     * for more information on listening for this event.
     * @type YAHOO.util.CustomEvent
     */
    beforeShowEvent = "beforeShow",

    /**
     * @event blur
     * @description Fires when the Carousel loses focus.  See
     * <a href="YAHOO.util.Element.html#addListener">Element.addListener</a>
     * for more information on listening for this event.
     * @type YAHOO.util.CustomEvent
     */
    blurEvent = "blur",

    /**
     * @event focus
     * @description Fires when the Carousel gains focus.  See
     * <a href="YAHOO.util.Element.html#addListener">Element.addListener</a>
     * for more information on listening for this event.
     * @type YAHOO.util.CustomEvent
     */
    focusEvent = "focus",

    /**
     * @event hide
     * @description Fires when the Carousel is hidden.  See
     * <a href="YAHOO.util.Element.html#addListener">Element.addListener</a>
     * for more information on listening for this event.
     * @type YAHOO.util.CustomEvent
     */
    hideEvent = "hide",

    /**
     * @event itemAdded
     * @description Fires when an item has been added to the Carousel.  Passes
     * back the content of the item that would be added, the index at which the
     * item would be added, and the event itself.  See
     * <a href="YAHOO.util.Element.html#addListener">Element.addListener</a>
     * for more information on listening for this event.
     * @type YAHOO.util.CustomEvent
     */
    itemAddedEvent = "itemAdded",

    /**
     * @event itemRemoved
     * @description Fires when an item has been removed from the Carousel.
     * Passes back the content of the item that would be removed, the index
     * from which the item would be removed, and the event itself.  See
     * <a href="YAHOO.util.Element.html#addListener">Element.addListener</a>
     * for more information on listening for this event.
     * @type YAHOO.util.CustomEvent
     */
    itemRemovedEvent = "itemRemoved",

    /**
     * @event itemSelected
     * @description Fires when an item has been selected in the Carousel.
     * Passes back the index of the selected item in the Carousel.  Note, that
     * the index begins from zero.  See
     * <a href="YAHOO.util.Element.html#addListener">Element.addListener</a>
     * for more information on listening for this event.
     * @type YAHOO.util.CustomEvent
     */
    itemSelectedEvent = "itemSelected",

    /**
     * @event loadItems
     * @description Fires when the Carousel needs more items to be loaded for
     * displaying them.  Passes back the first and last visible items in the
     * Carousel, and the number of items needed to be loaded.  See
     * <a href="YAHOO.util.Element.html#addListener">Element.addListener</a>
     * for more information on listening for this event.
     * @type YAHOO.util.CustomEvent
     */
    loadItemsEvent = "loadItems",

    /**
     * @event navigationStateChange
     * @description Fires when the state of either one of the navigation
     * buttons are changed from enabled to disabled or vice versa.  Passes back
     * the state (true/false) of the previous and next buttons.  The value true
     * signifies the button is enabled, false signifies disabled.  See
     * <a href="YAHOO.util.Element.html#addListener">Element.addListener</a>
     * for more information on listening for this event.
     * @type YAHOO.util.CustomEvent
     */
    navigationStateChangeEvent = "navigationStateChange",

    /**
     * @event pageChange
     * @description Fires after the Carousel has scrolled to the previous or
     * next page.  Passes back the page number of the current page.  Note
     * that the first page number is zero.  See
     * <a href="YAHOO.util.Element.html#addListener">Element.addListener</a>
     * for more information on listening for this event.
     * @type YAHOO.util.CustomEvent
     */
    pageChangeEvent = "pageChange",

    /*
     * Internal event.
     * @event render
     * @description Fires when the Carousel is rendered.  See
     * <a href="YAHOO.util.Element.html#addListener">Element.addListener</a>
     * for more information on listening for this event.
     * @type YAHOO.util.CustomEvent
     */
    renderEvent = "render",

    /**
     * @event show
     * @description Fires when the Carousel is shown.  See
     * <a href="YAHOO.util.Element.html#addListener">Element.addListener</a>
     * for more information on listening for this event.
     * @type YAHOO.util.CustomEvent
     */
    showEvent = "show",

    /**
     * @event startAutoPlay
     * @description Fires when the auto play has started in the Carousel.  See
     * <a href="YAHOO.util.Element.html#addListener">Element.addListener</a>
     * for more information on listening for this event.
     * @type YAHOO.util.CustomEvent
     */
    startAutoPlayEvent = "startAutoPlay",

    /**
     * @event stopAutoPlay
     * @description Fires when the auto play has been stopped in the Carousel.
     * See
     * <a href="YAHOO.util.Element.html#addListener">Element.addListener</a>
     * for more information on listening for this event.
     * @type YAHOO.util.CustomEvent
     */
    stopAutoPlayEvent = "stopAutoPlay",

    /*
     * Internal event.
     * @event uiUpdateEvent
     * @description Fires when the UI has been updated.
     * See
     * <a href="YAHOO.util.Element.html#addListener">Element.addListener</a>
     * for more information on listening for this event.
     * @type YAHOO.util.CustomEvent
     */
    uiUpdateEvent = "uiUpdate";

    /*
     * Private helper functions used by the Carousel component
     */

    /**
     * Create an element, set its class name and optionally install the element
     * to its parent.
     * @method createElement
     * @param el {String} The element to be created
     * @param attrs {Object} Configuration of parent, class and id attributes.
     * If the content is specified, it is inserted after creation of the
     * element. The content can also be an HTML element in which case it would
     * be appended as a child node of the created element.
     * @private
     */
    function createElement(el, attrs) {
        var newEl = document.createElement(el);

        attrs = attrs || {};
        if (attrs.className) {
            Dom.addClass(newEl, attrs.className);
        }

        if (attrs.parent) {
            attrs.parent.appendChild(newEl);
        }

        if (attrs.id) {
            newEl.setAttribute("id", attrs.id);
        }

        if (attrs.content) {
            if (attrs.content.nodeName) {
                newEl.appendChild(attrs.content);
            } else {
                newEl.innerHTML = attrs.content;
            }
        }

        return newEl;
    }

    /**
     * Get the computed style of an element.
     *
     * @method getStyle
     * @param el {HTMLElement} The element for which the style needs to be
     * returned.
     * @param style {String} The style attribute
     * @param type {String} "int", "float", etc. (defaults to int)
     * @private
     */
    function getStyle(el, style, type) {
        var value;

        if (!el) {
            return 0;
        }

        function getStyleIntVal(el, style) {
            var val;

            /*
             * XXX: Safari calculates incorrect marginRight for an element
             * which has its parent element style set to overflow: hidden
             * https://bugs.webkit.org/show_bug.cgi?id=13343
             * Let us assume marginLeft == marginRight
             */
            if (style == "marginRight" && YAHOO.env.ua.webkit) {
                val = parseInt(Dom.getStyle(el, "marginLeft"), 10);
            } else {
                val = parseInt(Dom.getStyle(el, style), 10);
            }

            return JS.isNumber(val) ? val : 0;
        }

        function getStyleFloatVal(el, style) {
            var val;

            /*
             * XXX: Safari calculates incorrect marginRight for an element
             * which has its parent element style set to overflow: hidden
             * https://bugs.webkit.org/show_bug.cgi?id=13343
             * Let us assume marginLeft == marginRight
             */
            if (style == "marginRight" && YAHOO.env.ua.webkit) {
                val = parseFloat(Dom.getStyle(el, "marginLeft"));
            } else {
                val = parseFloat(Dom.getStyle(el, style));
            }

            return JS.isNumber(val) ? val : 0;
        }

        if (typeof type == "undefined") {
            type = "int";
        }

        switch (style) {
        case "height":
            value = el.offsetHeight;
            if (value > 0) {
                value += getStyleIntVal(el, "marginTop")        +
                        getStyleIntVal(el, "marginBottom");
            } else {
                value = getStyleFloatVal(el, "height")          +
                        getStyleIntVal(el, "marginTop")         +
                        getStyleIntVal(el, "marginBottom")      +
                        getStyleIntVal(el, "borderTopWidth")    +
                        getStyleIntVal(el, "borderBottomWidth") +
                        getStyleIntVal(el, "paddingTop")        +
                        getStyleIntVal(el, "paddingBottom");
            }
            break;
        case "width":
            value = el.offsetWidth;
            if (value > 0) {
                value += getStyleIntVal(el, "marginLeft")       +
                        getStyleIntVal(el, "marginRight");
            } else {
                value = getStyleFloatVal(el, "width")           +
                        getStyleIntVal(el, "marginLeft")        +
                        getStyleIntVal(el, "marginRight")       +
                        getStyleIntVal(el, "borderLeftWidth")   +
                        getStyleIntVal(el, "borderRightWidth")  +
                        getStyleIntVal(el, "paddingLeft")       +
                        getStyleIntVal(el, "paddingRight");
            }
            break;
        default:
            if (type == "int") {
                value = getStyleIntVal(el, style);
            } else if (type == "float") {
                value = getStyleFloatVal(el, style);
            } else {
                value = Dom.getStyle(el, style);
            }
            break;
        }

        return value;
    }

    /**
     * Compute and return the height or width of a single Carousel item
     * depending upon the orientation.
     *
     * @method getCarouselItemSize
     * @param which {String} "height" or "width" to be returned.  If this is
     * passed explicitly, the calculated size is not cached.
     * @private
     */
    function getCarouselItemSize(which) {
        var carousel = this,
            child,
            size     = 0,
            vertical = false;

        if (carousel._itemsTable.numItems === 0) {
            return 0;
        }

        if (typeof which == "undefined") {
            if (carousel._itemsTable.size > 0) {
                return carousel._itemsTable.size;
            }
        }

        if (JS.isUndefined(carousel._itemsTable.items[0])) {
            return 0;
        }

        child = Dom.get(carousel._itemsTable.items[0].id);

        if (typeof which == "undefined") {
            vertical = carousel.get("isVertical");
        } else {
            vertical = which == "height";
        }

        if (vertical) {
            size = getStyle(child, "height");
        } else {
            size = getStyle(child, "width");
        }

        if (typeof which == "undefined") {
            carousel._itemsTable.size = size; // save the size for later
        }

        return size;
    }

    /**
     * Return the index of the first item in the view port for displaying item
     * in "pos".
     *
     * @method getFirstVisibleForPosition
     * @param pos {Number} The position of the item to be displayed
     * @private
     */
    function getFirstVisibleForPosition(pos) {
        var num = this.get("numVisible");

        return Math.floor(pos / num) * num;
    }

    /**
     * Return the scrolling offset size given the number of elements to
     * scroll.
     *
     * @method getScrollOffset
     * @param delta {Number} The delta number of elements to scroll by.
     * @private
     */
    function getScrollOffset(delta) {
        var itemSize = 0,
            size     = 0;

        itemSize = getCarouselItemSize.call(this);
        size     = itemSize * delta;

        // XXX: really, when the orientation is vertical, the scrolling
        // is not exactly the number of elements into element size.
        if (this.get("isVertical")) {
            size -= delta;
        }

        return size;
    }

    /**
     * Scroll the Carousel by a page backward.
     *
     * @method scrollPageBackward
     * @param {Event} ev The event object
     * @param {Object} obj The context object
     * @private
     */
    function scrollPageBackward(ev, obj) {
        obj.scrollPageBackward();
        Event.preventDefault(ev);
    }

    /**
     * Scroll the Carousel by a page forward.
     *
     * @method scrollPageForward
     * @param {Event} ev The event object
     * @param {Object} obj The context object
     * @private
     */
    function scrollPageForward(ev, obj) {
        obj.scrollPageForward();
        Event.preventDefault(ev);
    }

    /**
     * Set the selected item.
     *
     * @method setItemSelection
     * @param {Number} newpos The index of the new position
     * @param {Number} oldpos The index of the previous position
     * @private
     */
     function setItemSelection(newpos, oldpos) {
        var carousel = this,
            cssClass   = carousel.CLASSES,
            el,
            firstItem  = carousel._firstItem,
            isCircular = carousel.get("isCircular"),
            numItems   = carousel.get("numItems"),
            numVisible = carousel.get("numVisible"),
            position   = oldpos,
            sentinel   = firstItem + numVisible - 1;

        if (position >= 0 && position < numItems) {
            if (!JS.isUndefined(carousel._itemsTable.items[position])) {
                el = Dom.get(carousel._itemsTable.items[position].id);
                if (el) {
                    Dom.removeClass(el, cssClass.SELECTED_ITEM);
                }
            }
        }

        if (JS.isNumber(newpos)) {
            newpos = parseInt(newpos, 10);
            newpos = JS.isNumber(newpos) ? newpos : 0;
        } else {
            newpos = firstItem;
        }

        if (JS.isUndefined(carousel._itemsTable.items[newpos])) {
            newpos = getFirstVisibleForPosition.call(carousel, newpos);
            carousel.scrollTo(newpos); // still loading the item
        }

        if (!JS.isUndefined(carousel._itemsTable.items[newpos])) {
            el = Dom.get(carousel._itemsTable.items[newpos].id);
            if (el) {
                Dom.addClass(el, cssClass.SELECTED_ITEM);
            }
        }

        if (newpos < firstItem || newpos > sentinel) { // out of focus
            newpos = getFirstVisibleForPosition.call(carousel, newpos);
            carousel.scrollTo(newpos);
        }
    }

    /**
     * Fire custom events for enabling/disabling navigation elements.
     *
     * @method syncNavigation
     * @private
     */
    function syncNavigation() {
        var attach   = false,
            carousel = this,
            cssClass = carousel.CLASSES,
            i,
            navigation,
            sentinel;

        // Don't do anything if the Carousel is not rendered
        if (!carousel._hasRendered) {
            return;
        }

        navigation = carousel.get("navigation");
        sentinel   = carousel._firstItem + carousel.get("numVisible");

        if (navigation.prev) {
            if (carousel.get("numItems") === 0 || carousel._firstItem === 0) {
                if (carousel.get("numItems") === 0 ||
                   !carousel.get("isCircular")) {
                    Event.removeListener(navigation.prev, "click",
                            scrollPageBackward);
                    Dom.addClass(navigation.prev, cssClass.FIRST_NAV_DISABLED);
                    for (i = 0; i < carousel._navBtns.prev.length; i++) {
                        carousel._navBtns.prev[i].setAttribute("disabled",
                                "true");
                    }
                    carousel._prevEnabled = false;
                } else {
                    attach = !carousel._prevEnabled;
                }
            } else {
                attach = !carousel._prevEnabled;
            }

            if (attach) {
                Event.on(navigation.prev, "click", scrollPageBackward,
                         carousel);
                Dom.removeClass(navigation.prev, cssClass.FIRST_NAV_DISABLED);
                for (i = 0; i < carousel._navBtns.prev.length; i++) {
                    carousel._navBtns.prev[i].removeAttribute("disabled");
                }
                carousel._prevEnabled = true;
            }
        }

        attach = false;
        if (navigation.next) {
            if (sentinel >= carousel.get("numItems")) {
                if (!carousel.get("isCircular")) {
                    Event.removeListener(navigation.next, "click",
                            scrollPageForward);
                    Dom.addClass(navigation.next, cssClass.DISABLED);
                    for (i = 0; i < carousel._navBtns.next.length; i++) {
                        carousel._navBtns.next[i].setAttribute("disabled",
                                "true");
                    }
                    carousel._nextEnabled = false;
                } else {
                    attach = !carousel._nextEnabled;
                }
            } else {
                attach = !carousel._nextEnabled;
            }

            if (attach) {
                Event.on(navigation.next, "click", scrollPageForward,
                         carousel);
                Dom.removeClass(navigation.next, cssClass.DISABLED);
                for (i = 0; i < carousel._navBtns.next.length; i++) {
                    carousel._navBtns.next[i].removeAttribute("disabled");
                }
                carousel._nextEnabled = true;
            }
        }

        carousel.fireEvent(navigationStateChangeEvent,
                { next: carousel._nextEnabled, prev: carousel._prevEnabled });
    }

    /**
     * Synchronize and redraw the Pager UI if necessary.
     *
     * @method syncPagerUi
     * @private
     */
    function syncPagerUi(page) {
        var carousel = this, numPages, numVisible;

        // Don't do anything if the Carousel is not rendered
        if (!carousel._hasRendered) {
            return;
        }

        numVisible = carousel.get("numVisible");

        if (!JS.isNumber(page)) {
            page = Math.ceil(carousel.get("selectedItem") / numVisible);
        }
        numPages = Math.ceil(carousel.get("numItems") / numVisible);

        carousel._pages.num = numPages;
        carousel._pages.cur = page;

        if (numPages > carousel.CONFIG.MAX_PAGER_BUTTONS) {
            carousel._updatePagerMenu();
        } else {
            carousel._updatePagerButtons();
        }
    }

    /**
     * Handle UI update.
     * Call the appropriate methods on events fired when an item is added, or
     * removed for synchronizing the DOM.
     *
     * @method syncUi
     * @param {Object} o The item that needs to be added or removed
     * @private
     */
    function syncUi(o) {
        var carousel = this;

        if (!JS.isObject(o)) {
            return;
        }

        switch (o.ev) {
        case itemAddedEvent:
            carousel._syncUiForItemAdd(o);
            break;
        case itemRemovedEvent:
            carousel._syncUiForItemRemove(o);
            break;
        case loadItemsEvent:
            carousel._syncUiForLazyLoading(o);
            break;
        }

        carousel.fireEvent(uiUpdateEvent);
    }

    /**
     * Update the state variables after scrolling the Carousel view port.
     *
     * @method updateStateAfterScroll
     * @param {Integer} item The index to which the Carousel has scrolled to.
     * @param {Integer} sentinel The last element in the view port.
     * @private
     */
    function updateStateAfterScroll(item, sentinel) {
        var carousel   = this,
            page       = carousel.get("currentPage"),
            newPage,
            numPerPage = carousel.get("numVisible");

        newPage = parseInt(carousel._firstItem / numPerPage, 10);
        if (newPage != page) {
            carousel.setAttributeConfig("currentPage", { value: newPage });
            carousel.fireEvent(pageChangeEvent, newPage);
        }

        if (carousel.get("selectOnScroll")) {
            if (carousel.get("selectedItem") != carousel._selectedItem) {
                carousel.set("selectedItem", carousel._selectedItem);
            }
        }

        clearTimeout(carousel._autoPlayTimer);
        delete carousel._autoPlayTimer;
        if (carousel.isAutoPlayOn()) {
            carousel.startAutoPlay();
        }

        carousel.fireEvent(afterScrollEvent,
                           { first: carousel._firstItem,
                             last: sentinel },
                           carousel);
    }

    /*
     * Static members and methods of the Carousel component
     */

    /**
     * Return the appropriate Carousel object based on the id associated with
     * the Carousel element or false if none match.
     * @method getById
     * @public
     * @static
     */
    Carousel.getById = function (id) {
        return instances[id] ? instances[id].object : false;
    };

    YAHOO.extend(Carousel, YAHOO.util.Element, {

        /*
         * Internal variables used within the Carousel component
         */

        /**
         * The Animation object.
         *
         * @property _animObj
         * @private
         */
        _animObj: null,

        /**
         * The Carousel element.
         *
         * @property _carouselEl
         * @private
         */
        _carouselEl: null,

        /**
         * The Carousel clipping container element.
         *
         * @property _clipEl
         * @private
         */
        _clipEl: null,

        /**
         * The current first index of the Carousel.
         *
         * @property _firstItem
         * @private
         */
        _firstItem: 0,

        /**
         * Does the Carousel element have focus?
         *
         * @property _hasFocus
         * @private
         */
        _hasFocus: false,

        /**
         * Is the Carousel rendered already?
         *
         * @property _hasRendered
         * @private
         */
        _hasRendered: false,

        /**
         * Is the animation still in progress?
         *
         * @property _isAnimationInProgress
         * @private
         */
        _isAnimationInProgress: false,

        /**
         * Is the auto-scrolling of Carousel in progress?
         *
         * @property _isAutoPlayInProgress
         * @private
         */
        _isAutoPlayInProgress: false,

        /**
         * The table of items in the Carousel.
         * The numItems is the number of items in the Carousel, items being the
         * array of items in the Carousel.  The size is the size of a single
         * item in the Carousel.  It is cached here for efficiency (to avoid
         * computing the size multiple times).
         *
         * @property _itemsTable
         * @private
         */
        _itemsTable: null,

        /**
         * The Carousel navigation buttons.
         *
         * @property _navBtns
         * @private
         */
        _navBtns: null,

        /**
         * The Carousel navigation.
         *
         * @property _navEl
         * @private
         */
        _navEl: null,

        /**
         * Status of the next navigation item.
         *
         * @property _nextEnabled
         * @private
         */
        _nextEnabled: true,

        /**
         * The Carousel pages structure.
         * This is an object of the total number of pages and the current page.
         *
         * @property _pages
         * @private
         */
        _pages: null,

        /**
         * Status of the previous navigation item.
         *
         * @property _prevEnabled
         * @private
         */
        _prevEnabled: true,

        /**
         * Whether the Carousel size needs to be recomputed or not?
         *
         * @property _recomputeSize
         * @private
         */
        _recomputeSize: true,

        /*
         * CSS classes used by the Carousel component
         */

        CLASSES: {

            /**
             * The class name of the Carousel navigation buttons.
             *
             * @property BUTTON
             * @default "yui-carousel-button"
             */
            BUTTON: "yui-carousel-button",

            /**
             * The class name of the Carousel element.
             *
             * @property CAROUSEL
             * @default "yui-carousel"
             */
            CAROUSEL: "yui-carousel",

            /**
             * The class name of the container of the items in the Carousel.
             *
             * @property CAROUSEL_EL
             * @default "yui-carousel-element"
             */
            CAROUSEL_EL: "yui-carousel-element",

            /**
             * The class name of the Carousel's container element.
             *
             * @property CONTAINER
             * @default "yui-carousel-container"
             */
            CONTAINER: "yui-carousel-container",

            /**
             * The class name of the Carousel's container element.
             *
             * @property CONTENT
             * @default "yui-carousel-content"
             */
            CONTENT: "yui-carousel-content",

            /**
             * The class name of a disabled navigation button.
             *
             * @property DISABLED
             * @default "yui-carousel-button-disabled"
             */
            DISABLED: "yui-carousel-button-disabled",

            /**
             * The class name of the first Carousel navigation button.
             *
             * @property FIRST_NAV
             * @default " yui-carousel-first-button"
             */
            FIRST_NAV: " yui-carousel-first-button",

            /**
             * The class name of a first disabled navigation button.
             *
             * @property FIRST_NAV_DISABLED
             * @default "yui-carousel-first-button-disabled"
             */
            FIRST_NAV_DISABLED: "yui-carousel-first-button-disabled",

            /**
             * The class name of a first page element.
             *
             * @property FIRST_PAGE
             * @default "yui-carousel-nav-first-page"
             */
            FIRST_PAGE: "yui-carousel-nav-first-page",

            /**
             * The class name of the Carousel navigation button that has focus.
             *
             * @property FOCUSSED_BUTTON
             * @default "yui-carousel-button-focus"
             */
            FOCUSSED_BUTTON: "yui-carousel-button-focus",

            /**
             * The class name of a horizontally oriented Carousel.
             *
             * @property HORIZONTAL
             * @default "yui-carousel-horizontal"
             */
            HORIZONTAL: "yui-carousel-horizontal",

            /**
             * The element to be used as the progress indicator when the item
             * is still being loaded.
             *
             * @property ITEM_LOADING
             * @default The progress indicator (spinner) image CSS class
             */
            ITEM_LOADING: "yui-carousel-item-loading",

            /**
             * The class name that will be set if the Carousel adjusts itself
             * for a minimum width.
             *
             * @property MIN_WIDTH
             * @default "yui-carousel-min-width"
             */
            MIN_WIDTH: "yui-carousel-min-width",

            /**
             * The navigation element container class name.
             *
             * @property NAVIGATION
             * @default "yui-carousel-nav"
             */
            NAVIGATION: "yui-carousel-nav",

            /**
             * The class name of the next Carousel navigation button.
             *
             * @property NEXT_NAV
             * @default " yui-carousel-next-button"
             */
            NEXT_NAV: " yui-carousel-next-button",

            /**
             * The class name of the next navigation link. This variable is
             * not only used for styling, but also for identifying the link
             * within the Carousel container.
             *
             * @property NEXT_PAGE
             * @default "yui-carousel-next"
             */
            NEXT_PAGE: "yui-carousel-next",

            /**
             * The class name for the navigation container for prev/next.
             *
             * @property NAV_CONTAINER
             * @default "yui-carousel-buttons"
             */
            NAV_CONTAINER: "yui-carousel-buttons",

            /**
             * The class name of the focussed page navigation.  This class is
             * specifically used for the ugly focus handling in Opera.
             *
             * @property PAGE_FOCUS
             * @default "yui-carousel-nav-page-focus"
             */
            PAGE_FOCUS: "yui-carousel-nav-page-focus",

            /**
             * The class name of the previous navigation link. This variable
             * is not only used for styling, but also for identifying the link
             * within the Carousel container.
             *
             * @property PREV_PAGE
             * @default "yui-carousel-prev"
             */
            PREV_PAGE: "yui-carousel-prev",

            /**
             * The class name of the selected item.
             *
             * @property SELECTED_ITEM
             * @default "yui-carousel-item-selected"
             */
            SELECTED_ITEM: "yui-carousel-item-selected",

            /**
             * The class name of the selected paging navigation.
             *
             * @property SELECTED_NAV
             * @default "yui-carousel-nav-page-selected"
             */
            SELECTED_NAV: "yui-carousel-nav-page-selected",

            /**
             * The class name of a vertically oriented Carousel.
             *
             * @property VERTICAL
             * @default "yui-carousel-vertical"
             */
            VERTICAL: "yui-carousel-vertical",

            /**
             * The class name of the (vertical) Carousel's container element.
             *
             * @property VERTICAL_CONTAINER
             * @default "yui-carousel-vertical-container"
             */
            VERTICAL_CONTAINER: "yui-carousel-vertical-container",

            /**
             * The class name of a visible Carousel.
             *
             * @property VISIBLE
             * @default "yui-carousel-visible"
             */
            VISIBLE: "yui-carousel-visible"

        },

        /*
         * Configuration attributes for configuring the Carousel component
         */

        CONFIG: {

            /**
             * The offset of the first visible item in the Carousel.
             *
             * @property FIRST_VISIBLE
             * @default 0
             */
            FIRST_VISIBLE: 0,

            /**
             * The minimum width of the horizontal Carousel container to support
             * the navigation buttons.
             *
             * @property HORZ_MIN_WIDTH
             * @default 180
             */
            HORZ_MIN_WIDTH: 180,

            /**
             * The maximum number of pager buttons allowed beyond which the UI
             * of the pager would be a drop-down of pages instead of buttons.
             *
             * @property MAX_PAGER_BUTTONS
             * @default 5
             */
            MAX_PAGER_BUTTONS: 5,

            /**
             * The minimum width of the vertical Carousel container to support
             * the navigation buttons.
             *
             * @property VERT_MIN_WIDTH
             * @default 99
             */
            VERT_MIN_WIDTH: 99,

            /**
             * The number of visible items in the Carousel.
             *
             * @property NUM_VISIBLE
             * @default 3
             */
            NUM_VISIBLE: 3

        },

        /*
         * Internationalizable strings in the Carousel component
         */

        STRINGS: {

            /**
             * The content to be used as the progress indicator when the item
             * is still being loaded.
             *
             * @property ITEM_LOADING_CONTENT
             * @default "Loading"
             */
            ITEM_LOADING_CONTENT: "Loading",

            /**
             * The next navigation button name/text.
             *
             * @property NEXT_BUTTON_TEXT
             * @default "Next Page"
             */
            NEXT_BUTTON_TEXT: "Next Page",

            /**
             * The prefix text for the pager in case the UI is a drop-down.
             *
             * @property PAGER_PREFIX_TEXT
             * @default "Go to page "
             */
            PAGER_PREFIX_TEXT: "Go to page ",

            /**
             * The previous navigation button name/text.
             *
             * @property PREVIOUS_BUTTON_TEXT
             * @default "Previous Page"
             */
            PREVIOUS_BUTTON_TEXT: "Previous Page"

        },

        /*
         * Public methods of the Carousel component
         */

        /**
         * Insert or append an item to the Carousel.
         *
         * @method addItem
         * @public
         * @param item {String | Object | HTMLElement} The item to be appended
         * to the Carousel. If the parameter is a string, it is assumed to be
         * the content of the newly created item. If the parameter is an
         * object, it is assumed to supply the content and an optional class
         * and an optional id of the newly created item.
         * @param index {Number} optional The position to where in the list
         * (starts from zero).
         * @return {Boolean} Return true on success, false otherwise
         */
        addItem: function (item, index) {
            var carousel = this,
                className,
                content,
                elId,
                numItems = carousel.get("numItems");

            if (!item) {
                return false;
            }

            if (JS.isString(item) || item.nodeName) {
                content = item.nodeName ? item.innerHTML : item;
            } else if (JS.isObject(item)) {
                content = item.content;
            } else {
                YAHOO.log("Invalid argument to addItem", "error", WidgetName);
                return false;
            }

            className = item.className || "";
            elId      = item.id ? item.id : Dom.generateId();

            if (JS.isUndefined(index)) {
                carousel._itemsTable.items.push({
                        item      : content,
                        className : className,
                        id        : elId
                });
            } else {
                if (index < 0 || index >= numItems) {
                    YAHOO.log("Index out of bounds", "error", WidgetName);
                    return false;
                }
                carousel._itemsTable.items.splice(index, 0, {
                        item      : content,
                        className : className,
                        id        : elId
                });
            }
            carousel._itemsTable.numItems++;

            if (numItems < carousel._itemsTable.items.length) {
                carousel.set("numItems", carousel._itemsTable.items.length);
            }

            carousel.fireEvent(itemAddedEvent, { pos: index, ev: itemAddedEvent });

            return true;
        },

        /**
         * Insert or append multiple items to the Carousel.
         *
         * @method addItems
         * @public
         * @param items {Array} An array of items to be added with each item
         * representing an item, index pair [{item, index}, ...]
         * @return {Boolean} Return true on success, false otherwise
         */
        addItems: function (items) {
            var i, n, rv = true;

            if (!JS.isArray(items)) {
                return false;
            }

            for (i = 0, n = items.length; i < n; i++) {
                if (this.addItem(items[i][0], items[i][1]) === false) {
                    rv = false;
                }
            }

            return rv;
        },

        /**
         * Remove focus from the Carousel.
         *
         * @method blur
         * @public
         */
        blur: function () {
            this._carouselEl.blur();
            this.fireEvent(blurEvent);
        },

        /**
         * Clears the items from Carousel.
         *
         * @method clearItems
         * public
         */
        clearItems: function () {
            var carousel = this, n = carousel.get("numItems");

            while (n > 0) {
                if (!carousel.removeItem(0)) {
                    YAHOO.log("Item could not be removed - missing?",
                              "warn", WidgetName);
                }
                /*
                    For dynamic loading, the numItems may be much larger than
                    the actual number of items in the table.  So, set the
                    numItems to zero, and break out of the loop if the table
                    is already empty.
                 */
                if (carousel._itemsTable.numItems === 0) {
                    carousel.set("numItems", 0);
                    break;
                }
                n--;
            }

            carousel.fireEvent(allItemsRemovedEvent);
        },

        /**
         * Set focus on the Carousel.
         *
         * @method focus
         * @public
         */
        focus: function () {
            var carousel = this,
                first,
                focusEl,
                isSelectionInvisible,
                itemsTable,
                last,
                numVisible,
                selectOnScroll,
                selected,
                selItem;

            // Don't do anything if the Carousel is not rendered
            if (!carousel._hasRendered) {
                return;
            }

            if (carousel.isAnimating()) {
                // this messes up real bad!
                return;
            }

            selItem              = carousel.get("selectedItem");
            numVisible           = carousel.get("numVisible");
            selectOnScroll       = carousel.get("selectOnScroll");
            selected             = (selItem >= 0) ?
                                   carousel.getItem(selItem) : null;
            first                = carousel.get("firstVisible");
            last                 = first + numVisible - 1;
            isSelectionInvisible = (selItem < first || selItem > last);
            focusEl              = (selected && selected.id) ?
                                   Dom.get(selected.id) : null;
            itemsTable           = carousel._itemsTable;

            if (!selectOnScroll && isSelectionInvisible) {
                focusEl = (itemsTable && itemsTable.items &&
                           itemsTable.items[first]) ?
                        Dom.get(itemsTable.items[first].id) : null;
            }

            if (focusEl) {
                try {
                    focusEl.focus();
                } catch (ex) {
                    // ignore focus errors
                }
            }

            carousel.fireEvent(focusEvent);
        },

        /**
         * Hide the Carousel.
         *
         * @method hide
         * @public
         */
        hide: function () {
            var carousel = this;

            if (carousel.fireEvent(beforeHideEvent) !== false) {
                carousel.removeClass(carousel.CLASSES.VISIBLE);
                carousel.fireEvent(hideEvent);
            }
        },

        /**
         * Initialize the Carousel.
         *
         * @method init
         * @public
         * @param el {HTMLElement | String} The html element that represents
         * the Carousel container.
         * @param attrs {Object} The set of configuration attributes for
         * creating the Carousel.
         */
        init: function (el, attrs) {
            var carousel = this,
                elId     = el,  // save for a rainy day
                parse    = false;

            if (!el) {
                YAHOO.log(el + " is neither an HTML element, nor a string",
                        "error", WidgetName);
                return;
            }

            carousel._hasRendered = false;
            carousel._navBtns     = { prev: [], next: [] };
            carousel._pages       = { el: null, num: 0, cur: 0 };
            carousel._itemsTable  = { loading: {}, numItems: 0,
                                      items: [], size: 0 };

            YAHOO.log("Component initialization", WidgetName);

            if (JS.isString(el)) {
                el = Dom.get(el);
            } else if (!el.nodeName) {
                YAHOO.log(el + " is neither an HTML element, nor a string",
                        "error", WidgetName);
                return;
            }

            Carousel.superclass.init.call(carousel, el, attrs);

            if (el) {
                if (!el.id) {   // in case the HTML element is passed
                    el.setAttribute("id", Dom.generateId());
                }
                parse = carousel._parseCarousel(el);
                if (!parse) {
                    carousel._createCarousel(elId);
                }
            } else {
                el = carousel._createCarousel(elId);
            }
            elId = el.id;

            carousel.initEvents();

            if (parse) {
                carousel._parseCarouselItems();
            }

            if (!attrs || typeof attrs.isVertical == "undefined") {
                carousel.set("isVertical", false);
            }

            carousel._parseCarouselNavigation(el);
            carousel._navEl = carousel._setupCarouselNavigation();

            instances[elId] = { object: carousel };

            carousel._loadItems();
        },

        /**
         * Initialize the configuration attributes used to create the Carousel.
         *
         * @method initAttributes
         * @public
         * @param attrs {Object} The set of configuration attributes for
         * creating the Carousel.
         */
        initAttributes: function (attrs) {
            var carousel = this;

            attrs = attrs || {};
            Carousel.superclass.initAttributes.call(carousel, attrs);

            /**
             * @attribute carouselEl
             * @description The type of the Carousel element.
             * @default OL
             * @type Boolean
             */
            carousel.setAttributeConfig("carouselEl", {
                    validator : JS.isString,
                    value     : attrs.carouselEl || "OL"
            });

            /**
             * @attribute carouselItemEl
             * @description The type of the list of items within the Carousel.
             * @default LI
             * @type Boolean
             */
            carousel.setAttributeConfig("carouselItemEl", {
                    validator : JS.isString,
                    value     : attrs.carouselItemEl || "LI"
            });

            /**
             * @attribute currentPage
             * @description The current page number (read-only.)
             * @type Number
             */
            carousel.setAttributeConfig("currentPage", {
                    readOnly : true,
                    value    : 0
            });

            /**
             * @attribute firstVisible
             * @description The index to start the Carousel from (indexes begin
             * from zero)
             * @default 0
             * @type Number
             */
            carousel.setAttributeConfig("firstVisible", {
                    method    : carousel._setFirstVisible,
                    validator : carousel._validateFirstVisible,
                    value     :
                        attrs.firstVisible || carousel.CONFIG.FIRST_VISIBLE
            });

            /**
             * @attribute selectOnScroll
             * @description Set this to true to automatically set focus to
             * follow scrolling in the Carousel.
             * @default true
             * @type Boolean
             */
            carousel.setAttributeConfig("selectOnScroll", {
                    validator : JS.isBoolean,
                    value     : attrs.selectOnScroll || true
            });

            /**
             * @attribute numVisible
             * @description The number of visible items in the Carousel's
             * viewport.
             * @default 3
             * @type Number
             */
            carousel.setAttributeConfig("numVisible", {
                    method    : carousel._setNumVisible,
                    validator : carousel._validateNumVisible,
                    value     : attrs.numVisible || carousel.CONFIG.NUM_VISIBLE
            });

            /**
             * @attribute numItems
             * @description The number of items in the Carousel.
             * @type Number
             */
            carousel.setAttributeConfig("numItems", {
                    method    : carousel._setNumItems,
                    validator : carousel._validateNumItems,
                    value     : carousel._itemsTable.numItems
            });

            /**
             * @attribute scrollIncrement
             * @description The number of items to scroll by for arrow keys.
             * @default 1
             * @type Number
             */
            carousel.setAttributeConfig("scrollIncrement", {
                    validator : carousel._validateScrollIncrement,
                    value     : attrs.scrollIncrement || 1
            });

            /**
             * @attribute selectedItem
             * @description The index of the selected item.
             * @type Number
             */
            carousel.setAttributeConfig("selectedItem", {
                    method    : carousel._setSelectedItem,
                    validator : JS.isNumber,
                    value     : -1
            });

            /**
             * @attribute revealAmount
             * @description The percentage of the item to be revealed on each
             * side of the Carousel (before and after the first and last item
             * in the Carousel's viewport.)
             * @default 0
             * @type Number
             */
            carousel.setAttributeConfig("revealAmount", {
                    method    : carousel._setRevealAmount,
                    validator : carousel._validateRevealAmount,
                    value     : attrs.revealAmount || 0
            });

            /**
             * @attribute isCircular
             * @description Set this to true to wrap scrolling of the contents
             * in the Carousel.
             * @default false
             * @type Boolean
             */
            carousel.setAttributeConfig("isCircular", {
                    validator : JS.isBoolean,
                    value     : attrs.isCircular || false
            });

            /**
             * @attribute isVertical
             * @description True if the orientation of the Carousel is vertical
             * @default false
             * @type Boolean
             */
            carousel.setAttributeConfig("isVertical", {
                    method    : carousel._setOrientation,
                    validator : JS.isBoolean,
                    value     : attrs.isVertical || false
            });

            /**
             * @attribute navigation
             * @description The set of navigation controls for Carousel
             * @default <br>
             * { prev: null, // the previous navigation element<br>
             *   next: null } // the next navigation element
             * @type Object
             */
            carousel.setAttributeConfig("navigation", {
                    method    : carousel._setNavigation,
                    validator : carousel._validateNavigation,
                    value     :
                        attrs.navigation || {prev: null,next: null,page: null}
            });

            /**
             * @attribute animation
             * @description The optional animation attributes for the Carousel.
             * @default <br>
             * { speed: 0, // the animation speed (in seconds)<br>
             *   effect: null } // the animation effect (like
             *   YAHOO.util.Easing.easeOut)
             * @type Object
             */
            carousel.setAttributeConfig("animation", {
                    validator : carousel._validateAnimation,
                    value     : attrs.animation || { speed: 0, effect: null }
            });

            /**
             * @attribute autoPlay
             * @description Set this to time in milli-seconds to have the
             * Carousel automatically scroll the contents.
             * @type Number
             * @deprecated Use autoPlayInterval instead.
             */
            carousel.setAttributeConfig("autoPlay", {
                    validator : JS.isNumber,
                    value     : attrs.autoPlay || 0
            });

            /**
             * @attribute autoPlayInterval
             * @description The delay in milli-seconds for scrolling the
             * Carousel during auto-play.
             * Note: The startAutoPlay() method needs to be invoked to trigger
             * automatic scrolling of Carousel.
             * @type Number
             */
            carousel.setAttributeConfig("autoPlayInterval", {
                    validator : JS.isNumber,
                    value     : attrs.autoPlayInterval || 0
            });
        },

        /**
         * Initialize and bind the event handlers.
         *
         * @method initEvents
         * @public
         */
        initEvents: function () {
            var carousel = this,
                cssClass = carousel.CLASSES,
                focussedLi;

            carousel.on("keydown", carousel._keyboardEventHandler);

            carousel.on(afterScrollEvent, syncNavigation);

            carousel.on(itemAddedEvent, syncUi);

            carousel.on(itemRemovedEvent, syncUi);

            carousel.on(itemSelectedEvent, function () {
                if (carousel._hasFocus) {
                    carousel.focus();
                }
            });

            carousel.on(loadItemsEvent, syncUi);

            carousel.on(allItemsRemovedEvent, function (ev) {
                carousel.scrollTo(0);
                syncNavigation.call(carousel);
                syncPagerUi.call(carousel);
            });

            carousel.on(pageChangeEvent, syncPagerUi, carousel);

            carousel.on(renderEvent, function (ev) {
                carousel.set("selectedItem", carousel.get("firstVisible"));
                syncNavigation.call(carousel, ev);
                syncPagerUi.call(carousel, ev);
                carousel._setClipContainerSize();
            });

            carousel.on("selectedItemChange", function (ev) {
                setItemSelection.call(carousel, ev.newValue, ev.prevValue);
                if (ev.newValue >= 0) {
                    carousel._updateTabIndex(
                            carousel.getElementForItem(ev.newValue));
                }
                carousel.fireEvent(itemSelectedEvent, ev.newValue);
            });

            carousel.on(uiUpdateEvent, function (ev) {
                syncNavigation.call(carousel, ev);
                syncPagerUi.call(carousel, ev);
            });

            carousel.on("firstVisibleChange", function (ev) {
                if (!carousel.get("selectOnScroll")) {
                    if (ev.newValue >= 0) {
                        carousel._updateTabIndex(
                                carousel.getElementForItem(ev.newValue));
                    }
                }
            });

            // Handle item selection on mouse click
            carousel.on("click", function (ev) {
                if (carousel.isAutoPlayOn()) {
                    carousel.stopAutoPlay();
                }
                carousel._itemClickHandler(ev);
                carousel._pagerClickHandler(ev);
            });

            // Restore the focus on the navigation buttons

            Event.onFocus(carousel.get("element"), function (ev, obj) {
                var target = Event.getTarget(ev);

                if (target && target.nodeName.toUpperCase() == "A" &&
                    Dom.getAncestorByClassName(target, cssClass.NAVIGATION)) {
                    if (focussedLi) {
                        Dom.removeClass(focussedLi, cssClass.PAGE_FOCUS);
                    }
                    focussedLi = target.parentNode;
                    Dom.addClass(focussedLi, cssClass.PAGE_FOCUS);
                } else {
                    if (focussedLi) {
                        Dom.removeClass(focussedLi, cssClass.PAGE_FOCUS);
                    }
                }

                obj._hasFocus = true;
                obj._updateNavButtons(Event.getTarget(ev), true);
            }, carousel);

            Event.onBlur(carousel.get("element"), function (ev, obj) {
                obj._hasFocus = false;
                obj._updateNavButtons(Event.getTarget(ev), false);
            }, carousel);
        },

        /**
         * Return true if the Carousel is still animating, or false otherwise.
         *
         * @method isAnimating
         * @return {Boolean} Return true if animation is still in progress, or
         * false otherwise.
         * @public
         */
        isAnimating: function () {
            return this._isAnimationInProgress;
        },

        /**
         * Return true if the auto-scrolling of Carousel is "on", or false
         * otherwise.
         *
         * @method isAutoPlayOn
         * @return {Boolean} Return true if autoPlay is "on", or false
         * otherwise.
         * @public
         */
        isAutoPlayOn: function () {
            return this._isAutoPlayInProgress;
        },

        /**
         * Return the carouselItemEl at index or null if the index is not
         * found.
         *
         * @method getElementForItem
         * @param index {Number} The index of the item to be returned
         * @return {Element} Return the item at index or null if not found
         * @public
         */
        getElementForItem: function (index) {
            var carousel = this;

            if (index < 0 || index >= carousel.get("numItems")) {
                YAHOO.log("Index out of bounds", "error", WidgetName);
                return null;
            }

            // TODO: may be cache the item
            if (carousel._itemsTable.numItems > index) {
                if (!JS.isUndefined(carousel._itemsTable.items[index])) {
                    return Dom.get(carousel._itemsTable.items[index].id);
                }
            }

            return null;
        },

        /**
         * Return the carouselItemEl for all items in the Carousel.
         *
         * @method getElementForItems
         * @return {Array} Return all the items
         * @public
         */
        getElementForItems: function () {
            var carousel = this, els = [], i;

            for (i = 0; i < carousel._itemsTable.numItems; i++) {
                els.push(carousel.getElementForItem(i));
            }

            return els;
        },

        /**
         * Return the item at index or null if the index is not found.
         *
         * @method getItem
         * @param index {Number} The index of the item to be returned
         * @return {Object} Return the item at index or null if not found
         * @public
         */
        getItem: function (index) {
            var carousel = this;

            if (index < 0 || index >= carousel.get("numItems")) {
                YAHOO.log("Index out of bounds", "error", WidgetName);
                return null;
            }

            if (carousel._itemsTable.numItems > index) {
                if (!JS.isUndefined(carousel._itemsTable.items[index])) {
                    return carousel._itemsTable.items[index];
                }
            }

            return null;
        },

        /**
         * Return all items as an array.
         *
         * @method getItems
         * @return {Array} Return all items in the Carousel
         * @public
         */
        getItems: function (index) {
            return this._itemsTable.items;
        },

        /**
         * Return the position of the Carousel item that has the id "id", or -1
         * if the id is not found.
         *
         * @method getItemPositionById
         * @param index {Number} The index of the item to be returned
         * @public
         */
        getItemPositionById: function (id) {
            var carousel = this, i = 0, n = carousel._itemsTable.numItems;

            while (i < n) {
                if (!JS.isUndefined(carousel._itemsTable.items[i])) {
                    if (carousel._itemsTable.items[i].id == id) {
                        return i;
                    }
                }
                i++;
            }

            return -1;
        },

        /**
         * Return all visible items as an array.
         *
         * @method getVisibleItems
         * @return {Array} The array of visible items
         * @public
         */
        getVisibleItems: function () {
            var carousel = this,
                i        = carousel.get("firstVisible"),
                n        = i + carousel.get("numVisible"),
                r        = [];

            while (i < n) {
                r.push(carousel.getElementForItem(i));
                i++;
            }

            return r;
        },

        /**
         * Remove an item at index from the Carousel.
         *
         * @method removeItem
         * @public
         * @param index {Number} The position to where in the list (starts from
         * zero).
         * @return {Boolean} Return true on success, false otherwise
         */
        removeItem: function (index) {
            var carousel = this,
                item,
                num      = carousel.get("numItems");

            if (index < 0 || index >= num) {
                YAHOO.log("Index out of bounds", "error", WidgetName);
                return false;
            }

            item = carousel._itemsTable.items.splice(index, 1);
            if (item && item.length == 1) {
                carousel._itemsTable.numItems--;
                carousel.set("numItems", num - 1);

                carousel.fireEvent(itemRemovedEvent,
                        { item: item[0], pos: index, ev: itemRemovedEvent });
                return true;
            }

            return false;
        },

        /**
         * Render the Carousel.
         *
         * @method render
         * @public
         * @param appendTo {HTMLElement | String} The element to which the
         * Carousel should be appended prior to rendering.
         * @return {Boolean} Status of the operation
         */
        render: function (appendTo) {
            var carousel = this,
                cssClass = carousel.CLASSES;

            carousel.addClass(cssClass.CAROUSEL);

            if (!carousel._clipEl) {
                carousel._clipEl = carousel._createCarouselClip();
                carousel._clipEl.appendChild(carousel._carouselEl);
            }

            if (appendTo) {
                carousel.appendChild(carousel._clipEl);
                carousel.appendTo(appendTo);
            } else {
                if (!Dom.inDocument(carousel.get("element"))) {
                    YAHOO.log("Nothing to render. The container should be " +
                            "within the document if appendTo is not "       +
                            "specified", "error", WidgetName);
                    return false;
                }
                carousel.appendChild(carousel._clipEl);
            }

            if (carousel.get("isVertical")) {
                carousel.addClass(cssClass.VERTICAL);
            } else {
                carousel.addClass(cssClass.HORIZONTAL);
            }

            if (carousel.get("numItems") < 1) {
                YAHOO.log("No items in the Carousel to render", "warn",
                        WidgetName);
                return false;
            }

            carousel._refreshUi();

            return true;
        },

        /**
         * Scroll the Carousel by an item backward.
         *
         * @method scrollBackward
         * @public
         */
        scrollBackward: function () {
            var carousel = this;

            carousel.scrollTo(carousel._firstItem -
                              carousel.get("scrollIncrement"));
        },

        /**
         * Scroll the Carousel by an item forward.
         *
         * @method scrollForward
         * @public
         */
        scrollForward: function () {
            var carousel = this;

            carousel.scrollTo(carousel._firstItem +
                              carousel.get("scrollIncrement"));
        },

        /**
         * Scroll the Carousel by a page backward.
         *
         * @method scrollPageBackward
         * @public
         */
        scrollPageBackward: function () {
            var carousel = this,
                item     = carousel._firstItem - carousel.get("numVisible");

            if (carousel.get("selectOnScroll")) {
                carousel._selectedItem = carousel._getSelectedItem(item);
            } else {
                item = carousel._getValidIndex(item);
            }
            carousel.scrollTo(item);
        },

        /**
         * Scroll the Carousel by a page forward.
         *
         * @method scrollPageForward
         * @public
         */
        scrollPageForward: function () {
            var carousel = this,
                item     = carousel._firstItem + carousel.get("numVisible");

            if (carousel.get("selectOnScroll")) {
                carousel._selectedItem = carousel._getSelectedItem(item);
            } else {
                item = carousel._getValidIndex(item);
            }
            carousel.scrollTo(item);
        },

        /**
         * Scroll the Carousel to make the item the first visible item.
         *
         * @method scrollTo
         * @public
         * @param item Number The index of the element to position at.
         * @param dontSelect Boolean True if select should be avoided
         */
        scrollTo: function (item, dontSelect) {
            var carousel   = this,
                animate, animCfg, isCircular, delta, direction, firstItem,
                numItems, numPerPage, offset, page, rv, sentinel,
                stopAutoScroll;

            if (JS.isUndefined(item) || item == carousel._firstItem ||
                carousel.isAnimating()) {
                return;         // nothing to do!
            }

            animCfg        = carousel.get("animation");
            isCircular     = carousel.get("isCircular");
            firstItem      = carousel._firstItem;
            numItems       = carousel.get("numItems");
            numPerPage     = carousel.get("numVisible");
            page           = carousel.get("currentPage");
            stopAutoScroll = function () {
                if (carousel.isAutoPlayOn()) {
                    carousel.stopAutoPlay();
                }
            };

            if (item < 0) {
                if (isCircular) {
                    item = numItems + item;
                } else {
                    stopAutoScroll.call(carousel);
                    return;
                }
            } else if (numItems > 0 && item > numItems - 1) {
                if (carousel.get("isCircular")) {
                    item = numItems - item;
                } else {
                    stopAutoScroll.call(carousel);
                    return;
                }
            }

            direction = (carousel._firstItem > item) ? "backward" : "forward";

            sentinel  = firstItem + numPerPage;
            sentinel  = (sentinel > numItems - 1) ? numItems - 1 : sentinel;
            rv = carousel.fireEvent(beforeScrollEvent,
                    { dir: direction, first: firstItem, last: sentinel });
            if (rv === false) { // scrolling is prevented
                return;
            }

            carousel.fireEvent(beforePageChangeEvent, { page: page });

            delta = firstItem - item; // yes, the delta is reverse
            carousel._firstItem = item;
            carousel.set("firstVisible", item);

            YAHOO.log("Scrolling to " + item + " delta = " + delta,WidgetName);

            carousel._loadItems(); // do we have all the items to display?

            sentinel  = item + numPerPage;
            sentinel  = (sentinel > numItems - 1) ? numItems - 1 : sentinel;

            offset    = getScrollOffset.call(carousel, delta);
            YAHOO.log("Scroll offset = " + offset, WidgetName);

            animate   = animCfg.speed > 0;

            if (animate) {
                carousel._animateAndSetCarouselOffset(offset, item, sentinel,
                        dontSelect);
            } else {
                carousel._setCarouselOffset(offset);
                updateStateAfterScroll.call(carousel, item, sentinel);
            }
        },

        /**
         * Select the previous item in the Carousel.
         *
         * @method selectPreviousItem
         * @public
         */
        selectPreviousItem: function () {
            var carousel = this,
                newpos   = 0,
                selected = carousel.get("selectedItem");

            if (selected == this._firstItem) {
                newpos = selected - carousel.get("numVisible");
                carousel._selectedItem = carousel._getSelectedItem(selected-1);
                carousel.scrollTo(newpos);
            } else {
                newpos = carousel.get("selectedItem") -
                         carousel.get("scrollIncrement");
                carousel.set("selectedItem",carousel._getSelectedItem(newpos));
            }
        },

        /**
         * Select the next item in the Carousel.
         *
         * @method selectNextItem
         * @public
         */
        selectNextItem: function () {
            var carousel = this, newpos = 0;

            newpos = carousel.get("selectedItem") +
                     carousel.get("scrollIncrement");
            carousel.set("selectedItem", carousel._getSelectedItem(newpos));
        },

        /**
         * Display the Carousel.
         *
         * @method show
         * @public
         */
        show: function () {
            var carousel = this,
                cssClass = carousel.CLASSES;

            if (carousel.fireEvent(beforeShowEvent) !== false) {
                carousel.addClass(cssClass.VISIBLE);
                carousel.fireEvent(showEvent);
            }
        },

        /**
         * Start auto-playing the Carousel.
         *
         * @method startAutoPlay
         * @public
         */
        startAutoPlay: function () {
            var carousel = this, timer;

            if (JS.isUndefined(carousel._autoPlayTimer)) {
                timer = carousel.get("autoPlayInterval");
                if (timer <= 0) {
                    return;
                }
                carousel._isAutoPlayInProgress = true;
                carousel.fireEvent(startAutoPlayEvent);
                carousel._autoPlayTimer = setTimeout(function () {
                    carousel._autoScroll();
                }, timer);
            }
        },

        /**
         * Stop auto-playing the Carousel.
         *
         * @method stopAutoPlay
         * @public
         */
        stopAutoPlay: function () {
            var carousel = this;

            if (!JS.isUndefined(carousel._autoPlayTimer)) {
                clearTimeout(carousel._autoPlayTimer);
                delete carousel._autoPlayTimer;
                carousel._isAutoPlayInProgress = false;
                carousel.fireEvent(stopAutoPlayEvent);
            }
        },

        /**
         * Return the string representation of the Carousel.
         *
         * @method toString
         * @public
         * @return {String}
         */
        toString: function () {
            return WidgetName + (this.get ? " (#" + this.get("id") + ")" : "");
        },

        /*
         * Protected methods of the Carousel component
         */

        /**
         * Set the Carousel offset to the passed offset after animating.
         *
         * @method _animateAndSetCarouselOffset
         * @param {Integer} offset The offset to which the Carousel has to be
         * scrolled to.
         * @param {Integer} item The index to which the Carousel will scroll.
         * @param {Integer} sentinel The last element in the view port.
         * @protected
         */
        _animateAndSetCarouselOffset: function (offset, item, sentinel) {
            var carousel = this,
                animCfg  = carousel.get("animation"),
                animObj  = null;

            if (carousel.get("isVertical")) {
                animObj = new YAHOO.util.Motion(carousel._carouselEl,
                        { points: { by: [0, offset] } },
                        animCfg.speed, animCfg.effect);
            } else {
                animObj = new YAHOO.util.Motion(carousel._carouselEl,
                        { points: { by: [offset, 0] } },
                        animCfg.speed, animCfg.effect);
            }

            carousel._isAnimationInProgress = true;
            animObj.onComplete.subscribe(carousel._animationCompleteHandler,
                                         { scope: carousel, item: item,
                                           last: sentinel });
            animObj.animate();
        },

        /**
         * Handle the animation complete event.
         *
         * @method _animationCompleteHandler
         * @param {Event} ev The event.
         * @param {Array} p The event parameters.
         * @param {Object} o The object that has the state of the Carousel
         * @protected
         */
        _animationCompleteHandler: function (ev, p, o) {
            o.scope._isAnimationInProgress = false;
            updateStateAfterScroll.call(o.scope, o.item, o.last);
        },

        /**
         * Automatically scroll the contents of the Carousel.
         * @method _autoScroll
         * @protected
         */
        _autoScroll: function() {
            var carousel  = this,
                currIndex = carousel._firstItem,
                index;

            if (currIndex >= carousel.get("numItems") - 1) {
                if (carousel.get("isCircular")) {
                    index = 0;
                } else {
                    carousel.stopAutoPlay();
                }
            } else {
                index = currIndex + carousel.get("numVisible");
            }

            carousel._selectedItem = carousel._getSelectedItem(index);
            carousel.scrollTo.call(carousel, index);
        },

        /**
         * Create the Carousel.
         *
         * @method createCarousel
         * @param elId {String} The id of the element to be created
         * @protected
         */
        _createCarousel: function (elId) {
            var carousel = this,
                cssClass = carousel.CLASSES,
                el       = Dom.get(elId);

            if (!el) {
                el = createElement("DIV", {
                        className : cssClass.CAROUSEL,
                        id        : elId
                });
            }

            if (!carousel._carouselEl) {
                carousel._carouselEl=createElement(carousel.get("carouselEl"),
                        { className: cssClass.CAROUSEL_EL });
            }

            return el;
        },

        /**
         * Create the Carousel clip container.
         *
         * @method createCarouselClip
         * @protected
         */
        _createCarouselClip: function () {
            return createElement("DIV", { className: this.CLASSES.CONTENT });
        },

        /**
         * Create the Carousel item.
         *
         * @method createCarouselItem
         * @param obj {Object} The attributes of the element to be created
         * @protected
         */
        _createCarouselItem: function (obj) {
            return createElement(this.get("carouselItemEl"), {
                    className : obj.className,
                    content   : obj.content,
                    id        : obj.id
            });
        },

        /**
         * Return a valid item for a possibly out of bounds index considering
         * the isCircular property.
         *
         * @method _getValidIndex
         * @param index {Number} The index of the item to be returned
         * @return {Object} Return a valid item index
         * @protected
         */
        _getValidIndex: function (index) {
            var carousel   = this,
                isCircular = carousel.get("isCircular"),
                numItems   = carousel.get("numItems"),
                sentinel   = numItems - 1;

            if (index < 0) {
                index = isCircular ? numItems + index : 0;
            } else if (index > sentinel) {
                index = isCircular ? index - numItems : sentinel;
            }

            return index;
        },

        /**
         * Get the value for the selected item.
         *
         * @method _getSelectedItem
         * @param val {Number} The new value for "selected" item
         * @return {Number} The new value that would be set
         * @protected
         */
        _getSelectedItem: function (val) {
            var carousel   = this,
                isCircular = carousel.get("isCircular"),
                numItems   = carousel.get("numItems"),
                sentinel   = numItems - 1;

            if (val < 0) {
                if (isCircular) {
                    val = numItems + val;
                } else {
                    val = carousel.get("selectedItem");
                }
            } else if (val > sentinel) {
                if (isCircular) {
                    val = val - numItems;
                } else {
                    val = carousel.get("selectedItem");
                }
            }

            return val;
        },

        /**
         * The "click" handler for the item.
         *
         * @method _itemClickHandler
         * @param {Event} ev The event object
         * @protected
         */
        _itemClickHandler: function (ev) {
            var carousel  = this,
                container = carousel.get("element"),
                el,
                item,
                target    = YAHOO.util.Event.getTarget(ev);

            while (target && target != container &&
                   target.id != carousel._carouselEl) {
                el = target.nodeName;
                if (el.toUpperCase() == carousel.get("carouselItemEl")) {
                    break;
                }
                target = target.parentNode;
            }

            if ((item = carousel.getItemPositionById(target.id)) >= 0) {
                YAHOO.log("Setting selection to " + item, WidgetName);
                carousel.set("selectedItem", carousel._getSelectedItem(item));
                carousel.focus();
            }
        },

        /**
         * The keyboard event handler for Carousel.
         *
         * @method _keyboardEventHandler
         * @param ev {Event} The event that is being handled.
         * @protected
         */
        _keyboardEventHandler: function (ev) {
            var carousel = this,
                key      = Event.getCharCode(ev),
                prevent  = false;

            if (carousel.isAnimating()) {
                return;         // do not mess while animation is in progress
            }

            switch (key) {
            case 0x25:          // left arrow
            case 0x26:          // up arrow
                carousel.selectPreviousItem();
                prevent = true;
                break;
            case 0x27:          // right arrow
            case 0x28:          // down arrow
                carousel.selectNextItem();
                prevent = true;
                break;
            case 0x21:          // page-up
                carousel.scrollPageBackward();
                prevent = true;
                break;
            case 0x22:          // page-down
                carousel.scrollPageForward();
                prevent = true;
                break;
            }

            if (prevent) {
                if (carousel.isAutoPlayOn()) {
                    carousel.stopAutoPlay();
                }
                Event.preventDefault(ev);
            }
        },

        /**
         * The load the required set of items that are needed for display.
         *
         * @method _loadItems
         * @protected
         */
        _loadItems: function() {
            var carousel   = this,
                first      = carousel.get("firstVisible"),
                last       = 0,
                numItems   = carousel.get("numItems"),
                numVisible = carousel.get("numVisible"),
                reveal     = carousel.get("revealAmount");

            last = first + numVisible - 1 + (reveal ? 1 : 0);
            last = last > numItems - 1 ? numItems - 1 : last;

            if (!carousel.getItem(first) || !carousel.getItem(last)) {
                carousel.fireEvent(loadItemsEvent, {
                        ev: loadItemsEvent, first: first, last: last,
                        num: last - first
                });
            }
        },

        /**
         * The "click" handler for the pager navigation.
         *
         * @method _pagerClickHandler
         * @param {Event} ev The event object
         * @protected
         */
        _pagerClickHandler: function (ev) {
            var carousel = this,
                pos,
                target   = Event.getTarget(ev),
                val;

            function getPagerNode(el) {
                var itemEl = carousel.get("carouselItemEl");

                if (el.nodeName.toUpperCase() == itemEl.toUpperCase()) {
                    el = Dom.getChildrenBy(el, function (node) {
                        // either an anchor or select at least
                        return node.href || node.value;
                    });
                    if (el && el[0]) {
                        return el[0];
                    }
                } else if (el.href || el.value) {
                    return el;
                }

                return null;
            }

            if (target) {
                target = getPagerNode(target);
                if (!target) {
                    return;
                }
                val = target.href || target.value;
                if (JS.isString(val) && val) {
                    pos = val.lastIndexOf("#");
                    if (pos != -1) {
                        val = carousel.getItemPositionById(
                                val.substring(pos + 1));
                        carousel._selectedItem = val;
                        carousel.scrollTo(val);
                        if (!target.value) { // not a select element
                            carousel.focus();
                        }
                        Event.preventDefault(ev);
                    }
                }
            }
        },

        /**
         * Find the Carousel within a container. The Carousel is identified by
         * the first element that matches the carousel element tag or the
         * element that has the Carousel class.
         *
         * @method parseCarousel
         * @param parent {HTMLElement} The parent element to look under
         * @return {Boolean} True if Carousel is found, false otherwise
         * @protected
         */
        _parseCarousel: function (parent) {
            var carousel = this, child, cssClass, domEl, found, node;

            cssClass  = carousel.CLASSES;
            domEl     = carousel.get("carouselEl");
            found     = false;

            for (child = parent.firstChild; child; child = child.nextSibling) {
                if (child.nodeType == 1) {
                    node = child.nodeName;
                    if (node.toUpperCase() == domEl) {
                        carousel._carouselEl = child;
                        Dom.addClass(carousel._carouselEl,
                                     carousel.CLASSES.CAROUSEL_EL);
                        YAHOO.log("Found Carousel - " + node +
                                (child.id ? " (#" + child.id + ")" : ""),
                                WidgetName);
                        found = true;
                    }
                }
            }

            return found;
        },

        /**
         * Find the items within the Carousel and add them to the items table.
         * A Carousel item is identified by elements that matches the carousel
         * item element tag.
         *
         * @method parseCarouselItems
         * @protected
         */
        _parseCarouselItems: function () {
            var carousel = this,
                child,
                domItemEl,
                elId,
                node,
                parent   = carousel._carouselEl;

            domItemEl = carousel.get("carouselItemEl");

            for (child = parent.firstChild; child; child = child.nextSibling) {
                if (child.nodeType == 1) {
                    node = child.nodeName;
                    if (node.toUpperCase() == domItemEl) {
                        if (child.id) {
                            elId = child.id;
                        } else {
                            elId = Dom.generateId();
                            child.setAttribute("id", elId);
                        }
                        carousel.addItem(child);
                    }
                }
            }
        },

        /**
         * Find the Carousel navigation within a container. The navigation
         * elements need to match the carousel navigation class names.
         *
         * @method parseCarouselNavigation
         * @param parent {HTMLElement} The parent element to look under
         * @return {Boolean} True if at least one is found, false otherwise
         * @protected
         */
        _parseCarouselNavigation: function (parent) {
            var carousel = this,
                cfg,
                cssClass = carousel.CLASSES,
                el,
                i,
                j,
                nav,
                rv       = false;

            nav = Dom.getElementsByClassName(cssClass.PREV_PAGE, "*", parent);
            if (nav.length > 0) {
                for (i in nav) {
                    if (nav.hasOwnProperty(i)) {
                        el = nav[i];
                        YAHOO.log("Found Carousel previous page navigation - " +
                                el + (el.id ? " (#" + el.id + ")" : ""),
                                WidgetName);
                        if (el.nodeName == "INPUT" ||
                            el.nodeName == "BUTTON") {
                            carousel._navBtns.prev.push(el);
                        } else {
                            j = el.getElementsByTagName("INPUT");
                            if (JS.isArray(j) && j.length > 0) {
                                carousel._navBtns.prev.push(j[0]);
                            } else {
                                j = el.getElementsByTagName("BUTTON");
                                if (JS.isArray(j) && j.length > 0) {
                                    carousel._navBtns.prev.push(j[0]);
                                }
                            }
                        }
                    }
                }
                cfg = { prev: nav };
            }

            nav = Dom.getElementsByClassName(cssClass.NEXT_PAGE, "*", parent);
            if (nav.length > 0) {
                for (i in nav) {
                    if (nav.hasOwnProperty(i)) {
                        el = nav[i];
                        YAHOO.log("Found Carousel next page navigation - " +
                                el + (el.id ? " (#" + el.id + ")" : ""),
                                WidgetName);
                        if (el.nodeName == "INPUT" ||
                            el.nodeName == "BUTTON") {
                            carousel._navBtns.next.push(el);
                        } else {
                            j = el.getElementsByTagName("INPUT");
                            if (JS.isArray(j) && j.length > 0) {
                                carousel._navBtns.next.push(j[0]);
                            } else {
                                j = el.getElementsByTagName("BUTTON");
                                if (JS.isArray(j) && j.length > 0) {
                                    carousel._navBtns.next.push(j[0]);
                                }
                            }
                        }
                    }
                }
                if (cfg) {
                    cfg.next = nav;
                } else {
                    cfg = { next: nav };
                }
            }

            if (cfg) {
                carousel.set("navigation", cfg);
                rv = true;
            }

            return rv;
        },

        /**
         * Refresh the widget UI if it is not already rendered, on first item
         * addition.
         *
         * @method _refreshUi
         * @protected
         */
        _refreshUi: function () {
            var carousel = this;

            // Set the rendered state appropriately.
            carousel._hasRendered = true;
            carousel.fireEvent(renderEvent);
        },

        /**
         * Set the Carousel offset to the passed offset.
         *
         * @method _setCarouselOffset
         * @protected
         */
        _setCarouselOffset: function (offset) {
            var carousel = this, which;

            which   = carousel.get("isVertical") ? "top" : "left";
            offset += offset !== 0 ? getStyle(carousel._carouselEl, which) : 0;
            Dom.setStyle(carousel._carouselEl, which, offset + "px");
        },

        /**
         * Setup/Create the Carousel navigation element (if needed).
         *
         * @method _setupCarouselNavigation
         * @protected
         */
        _setupCarouselNavigation: function () {
            var carousel = this,
                btn, cfg, cssClass, nav, navContainer, nextButton, prevButton;

            cssClass = carousel.CLASSES;

            // TODO: can the _navBtns be tested against instead?
            navContainer = Dom.getElementsByClassName(cssClass.NAVIGATION,
                    "DIV", carousel.get("element"));

            if (navContainer.length === 0) {
                navContainer = createElement("DIV",
                        { className: cssClass.NAVIGATION });
                carousel.insertBefore(navContainer,
                        Dom.getFirstChild(carousel.get("element")));
            } else {
                navContainer = navContainer[0];
            }

            carousel._pages.el = createElement("UL");
            navContainer.appendChild(carousel._pages.el);

            nav = carousel.get("navigation");
            if (JS.isString(nav.prev) || JS.isArray(nav.prev)) {
                if (JS.isString(nav.prev)) {
                    nav.prev = [nav.prev];
                }
                for (btn in nav.prev) {
                    if (nav.prev.hasOwnProperty(btn)) {
                        carousel._navBtns.prev.push(Dom.get(nav.prev[btn]));
                    }
                }
            } else {
                // TODO: separate method for creating a navigation button
                prevButton = createElement("SPAN",
                        { className: cssClass.BUTTON + cssClass.FIRST_NAV });
                // XXX: for IE 6.x
                Dom.setStyle(prevButton, "visibility", "visible");
                btn = Dom.generateId();
                prevButton.innerHTML = "<button type=\"button\" "      +
                        "id=\"" + btn + "\" name=\""                   +
                        carousel.STRINGS.PREVIOUS_BUTTON_TEXT + "\">"  +
                        carousel.STRINGS.PREVIOUS_BUTTON_TEXT + "</button>";
                navContainer.appendChild(prevButton);
                btn = Dom.get(btn);
                carousel._navBtns.prev = [btn];
                cfg = { prev: [prevButton] };
            }

            if (JS.isString(nav.next) || JS.isArray(nav.next)) {
                if (JS.isString(nav.next)) {
                    nav.next = [nav.next];
                }
                for (btn in nav.next) {
                    if (nav.next.hasOwnProperty(btn)) {
                        carousel._navBtns.next.push(Dom.get(nav.next[btn]));
                    }
                }
            } else {
                // TODO: separate method for creating a navigation button
                nextButton = createElement("SPAN",
                        { className: cssClass.BUTTON + cssClass.NEXT_NAV });
                // XXX: for IE 6.x
                Dom.setStyle(nextButton, "visibility", "visible");
                btn = Dom.generateId();
                nextButton.innerHTML = "<button type=\"button\" "      +
                        "id=\"" + btn + "\" name=\""                   +
                        carousel.STRINGS.NEXT_BUTTON_TEXT + "\">"      +
                        carousel.STRINGS.NEXT_BUTTON_TEXT + "</button>";
                navContainer.appendChild(nextButton);
                btn = Dom.get(btn);
                carousel._navBtns.next = [btn];
                if (cfg) {
                    cfg.next = [nextButton];
                } else {
                    cfg = { next: [nextButton] };
                }
            }

            if (cfg) {
                carousel.set("navigation", cfg);
            }

            return navContainer;
        },

        /**
         * Set the clip container size (based on the new numVisible value).
         *
         * @method _setClipContainerSize
         * @param clip {HTMLElement} The clip container element.
         * @param num {Number} optional The number of items per page.
         * @protected
         */
        _setClipContainerSize: function (clip, num) {
            var carousel = this,
                attr, currVal, isVertical, itemSize, reveal, size, which;

            isVertical = carousel.get("isVertical");
            reveal     = carousel.get("revealAmount");
            which      = isVertical ? "height" : "width";
            attr       = isVertical ? "top" : "left";

            clip       = clip || carousel._clipEl;
            if (!clip) {
                return;
            }

            num        = num  || carousel.get("numVisible");
            itemSize   = getCarouselItemSize.call(carousel, which);
            size       = itemSize * num;

            // TODO: try to re-use the _hasRendered indicator
            carousel._recomputeSize = (size === 0); // bleh!
            if (carousel._recomputeSize) {
                carousel._hasRendered = false;
                return;             // no use going further, bail out!
            }

            if (reveal > 0) {
                reveal = itemSize * (reveal / 100) * 2;
                size += reveal;
                // TODO: set the Carousel's initial offset somwehere
                currVal = parseFloat(Dom.getStyle(carousel._carouselEl, attr));
                currVal = JS.isNumber(currVal) ? currVal : 0;
                Dom.setStyle(carousel._carouselEl,
                             attr, currVal + (reveal / 2) + "px");
            }

            if (isVertical) {
                size += getStyle(carousel._carouselEl, "marginTop")        +
                        getStyle(carousel._carouselEl, "marginBottom")     +
                        getStyle(carousel._carouselEl, "paddingTop")       +
                        getStyle(carousel._carouselEl, "paddingBottom")    +
                        getStyle(carousel._carouselEl, "borderTopWidth")   +
                        getStyle(carousel._carouselEl, "borderBottomWidth");
                // XXX: for vertical Carousel
                Dom.setStyle(clip, which, (size - (num - 1)) + "px");
            } else {
                size += getStyle(carousel._carouselEl, "marginLeft")      +
                        getStyle(carousel._carouselEl, "marginRight")     +
                        getStyle(carousel._carouselEl, "paddingLeft")     +
                        getStyle(carousel._carouselEl, "paddingRight")    +
                        getStyle(carousel._carouselEl, "borderLeftWidth") +
                        getStyle(carousel._carouselEl, "borderRightWidth");
                Dom.setStyle(clip, which, size + "px");
            }

            carousel._setContainerSize(clip); // adjust the container size too
        },

        /**
         * Set the container size.
         *
         * @method _setContainerSize
         * @param clip {HTMLElement} The clip container element.
         * @param attr {String} Either set the height or width.
         * @protected
         */
        _setContainerSize: function (clip, attr) {
            var carousel = this,
                config   = carousel.CONFIG,
                cssClass = carousel.CLASSES,
                isVertical,
                size;

            isVertical = carousel.get("isVertical");
            clip       = clip || carousel._clipEl;
            attr       = attr || (isVertical ? "height" : "width");
            size       = parseFloat(Dom.getStyle(clip, attr), 10);

            size = JS.isNumber(size) ? size : 0;

            if (isVertical) {
                size += getStyle(carousel._carouselEl, "marginTop")         +
                        getStyle(carousel._carouselEl, "marginBottom")      +
                        getStyle(carousel._carouselEl, "paddingTop")        +
                        getStyle(carousel._carouselEl, "paddingBottom")     +
                        getStyle(carousel._carouselEl, "borderTopWidth")    +
                        getStyle(carousel._carouselEl, "borderBottomWidth") +
                        getStyle(carousel._navEl, "height");
            } else {
                size += getStyle(clip, "marginLeft")                    +
                        getStyle(clip, "marginRight")                   +
                        getStyle(clip, "paddingLeft")                   +
                        getStyle(clip, "paddingRight")                  +
                        getStyle(clip, "borderLeftWidth")               +
                        getStyle(clip, "borderRightWidth");
            }

            if (!isVertical) {
                if (size < config.HORZ_MIN_WIDTH) {
                    size = config.HORZ_MIN_WIDTH;
                    carousel.addClass(cssClass.MIN_WIDTH);
                }
            }
            carousel.setStyle(attr,  size + "px");

            // Additionally the width of the container should be set for
            // the vertical Carousel
            if (isVertical) {
                size = getCarouselItemSize.call(carousel, "width");
                if (size < config.VERT_MIN_WIDTH) {
                    size = config.VERT_MIN_WIDTH;
                    carousel.addClass(cssClass.MIN_WIDTH);
                }
                carousel.setStyle("width",  size + "px");
            }
        },

        /**
         * Set the value for the Carousel's first visible item.
         *
         * @method _setFirstVisible
         * @param val {Number} The new value for firstVisible
         * @return {Number} The new value that would be set
         * @protected
         */
        _setFirstVisible: function (val) {
            var carousel = this;

            if (val >= 0 && val < carousel.get("numItems")) {
                carousel.scrollTo(val);
            } else {
                val = carousel.get("firstVisible");
            }
            return val;
        },

        /**
         * Set the value for the Carousel's navigation.
         *
         * @method _setNavigation
         * @param cfg {Object} The navigation configuration
         * @return {Object} The new value that would be set
         * @protected
         */
        _setNavigation: function (cfg) {
            var carousel = this;

            if (cfg.prev) {
                Event.on(cfg.prev, "click", scrollPageBackward, carousel);
            }
            if (cfg.next) {
                Event.on(cfg.next, "click", scrollPageForward, carousel);
            }
        },

        /**
         * Set the value for the number of visible items in the Carousel.
         *
         * @method _setNumVisible
         * @param val {Number} The new value for numVisible
         * @return {Number} The new value that would be set
         * @protected
         */
        _setNumVisible: function (val) {
            var carousel = this;

            carousel._setClipContainerSize(carousel._clipEl, val);
        },

        /**
         * Set the number of items in the Carousel.
         * Warning: Setting this to a lower number than the current removes
         * items from the end.
         *
         * @method _setNumItems
         * @param val {Number} The new value for numItems
         * @return {Number} The new value that would be set
         * @protected
         */
        _setNumItems: function (val) {
            var carousel = this,
                num      = carousel._itemsTable.numItems;

            if (JS.isArray(carousel._itemsTable.items)) {
                if (carousel._itemsTable.items.length != num) { // out of sync
                    num = carousel._itemsTable.items.length;
                    carousel._itemsTable.numItems = num;
                }
            }

            if (val < num) {
                while (num > val) {
                    carousel.removeItem(num - 1);
                    num--;
                }
            }

            return val;
        },

        /**
         * Set the orientation of the Carousel.
         *
         * @method _setOrientation
         * @param val {Boolean} The new value for isVertical
         * @return {Boolean} The new value that would be set
         * @protected
         */
        _setOrientation: function (val) {
            var carousel = this,
                cssClass = carousel.CLASSES;

            if (val) {
                carousel.replaceClass(cssClass.HORIZONTAL, cssClass.VERTICAL);
            } else {
                carousel.replaceClass(cssClass.VERTICAL, cssClass.HORIZONTAL);
            }
            carousel._itemsTable.size = 0; // force recalculation next time
            return val;
        },

        /**
         * Set the value for the reveal amount percentage in the Carousel.
         *
         * @method _setRevealAmount
         * @param val {Number} The new value for revealAmount
         * @return {Number} The new value that would be set
         * @protected
         */
        _setRevealAmount: function (val) {
            var carousel = this;

            if (val >= 0 && val <= 100) {
                val = parseInt(val, 10);
                val = JS.isNumber(val) ? val : 0;
                carousel._setClipContainerSize();
            } else {
                val = carousel.get("revealAmount");
            }
            return val;
        },

        /**
         * Set the value for the selected item.
         *
         * @method _setSelectedItem
         * @param val {Number} The new value for "selected" item
         * @protected
         */
        _setSelectedItem: function (val) {
            this._selectedItem = val;
        },

        /**
         * Synchronize and redraw the UI after an item is added.
         *
         * @method _syncUiForItemAdd
         * @protected
         */
        _syncUiForItemAdd: function (obj) {
            var carousel   = this,
                carouselEl = carousel._carouselEl,
                el,
                item,
                itemsTable = carousel._itemsTable,
                oel,
                pos,
                sibling;

            pos  = JS.isUndefined(obj.pos) ? itemsTable.numItems - 1 : obj.pos;
            if (!JS.isUndefined(itemsTable.items[pos])) {
                item = itemsTable.items[pos];
                if (item && !JS.isUndefined(item.id)) {
                    oel  = Dom.get(item.id);
                }
            }
            if (!oel) {
                el = carousel._createCarouselItem({
                        className : item.className,
                        content   : item.item,
                        id        : item.id
                });
                if (JS.isUndefined(obj.pos)) {
                    if (!JS.isUndefined(itemsTable.loading[pos])) {
                        oel = itemsTable.loading[pos];
                        // if oel is null, it is a problem ...
                    }
                    if (oel) {
                        // replace the node
                        carouselEl.replaceChild(el, oel);
                        // ... and remove the item from the data structure
                        delete itemsTable.loading[pos];
                    } else {
                        carouselEl.appendChild(el);
                    }
                } else {
                    if (!JS.isUndefined(itemsTable.items[obj.pos + 1])) {
                        sibling = Dom.get(itemsTable.items[obj.pos + 1].id);
                    }
                    if (sibling) {
                        carouselEl.insertBefore(el, sibling);
                    } else {
                        YAHOO.log("Unable to find sibling","error",WidgetName);
                    }
                }
            } else {
                if (JS.isUndefined(obj.pos)) {
                    if (!Dom.isAncestor(carousel._carouselEl, oel)) {
                        carouselEl.appendChild(oel);
                    }
                } else {
                    if (!Dom.isAncestor(carouselEl, oel)) {
                        if (!JS.isUndefined(itemsTable.items[obj.pos + 1])) {
                            carouselEl.insertBefore(oel,
                                    Dom.get(itemsTable.items[obj.pos + 1].id));
                        }
                    }
                }
            }

            if (!carousel._hasRendered) {
                carousel._refreshUi();
            }

            if (carousel.get("selectedItem") < 0) {
                carousel.set("selectedItem", carousel.get("firstVisible"));
            }
        },

        /**
         * Synchronize and redraw the UI after an item is removed.
         *
         * @method _syncUiForItemAdd
         * @protected
         */
        _syncUiForItemRemove: function (obj) {
            var carousel   = this,
                carouselEl = carousel._carouselEl,
                el, item, num, pos;

            num  = carousel.get("numItems");
            item = obj.item;
            pos  = obj.pos;

            if (item && (el = Dom.get(item.id))) {
                if (el && Dom.isAncestor(carouselEl, el)) {
                    Event.purgeElement(el, true);
                    carouselEl.removeChild(el);
                }

                if (carousel.get("selectedItem") == pos) {
                    pos = pos >= num ? num - 1 : pos;
                    carousel.set("selectedItem", pos);
                }
            } else {
                YAHOO.log("Unable to find item", "warn", WidgetName);
            }
        },

        /**
         * Synchronize and redraw the UI for lazy loading.
         *
         * @method _syncUiForLazyLoading
         * @protected
         */
        _syncUiForLazyLoading: function (obj) {
            var carousel   = this,
                carouselEl = carousel._carouselEl,
                el,
                i,
                itemsTable = carousel._itemsTable,
                sibling;

            for (i = obj.first; i <= obj.last; i++) {
                el = carousel._createCarouselItem({
                        className : carousel.CLASSES.ITEM_LOADING,
                        content   : carousel.STRINGS.ITEM_LOADING_CONTENT,
                        id        : Dom.generateId()
                });
                if (el) {
                    if (!JS.isUndefined(itemsTable.items[obj.last + 1])) {
                        sibling = Dom.get(itemsTable.items[obj.last + 1].id);
                        if (sibling) {
                            carouselEl.insertBefore(el, sibling);
                        } else {
                            YAHOO.log("Unable to find sibling", "error",
                                    WidgetName);
                        }
                    } else {
                        carouselEl.appendChild(el);
                    }
                }
                itemsTable.loading[i] = el;
            }
        },

        /**
         * Set the correct class for the navigation buttons.
         *
         * @method _updateNavButtons
         * @param el {Object} The target button
         * @param setFocus {Boolean} True to set focus ring, false otherwise.
         * @protected
         */
        _updateNavButtons: function (el, setFocus) {
            var children,
                cssClass = this.CLASSES,
                grandParent,
                parent   = el.parentNode;

            if (!parent) {
                return;
            }
            grandParent = parent.parentNode;

            if (el.nodeName.toUpperCase() == "BUTTON" &&
                Dom.hasClass(parent, cssClass.BUTTON)) {
                if (setFocus) {
                    if (grandParent) {
                        children = Dom.getChildren(grandParent);
                        if (children) {
                            Dom.removeClass(children, cssClass.FOCUSSED_BUTTON);
                        }
                    }
                    Dom.addClass(parent, cssClass.FOCUSSED_BUTTON);
                } else {
                    Dom.removeClass(parent, cssClass.FOCUSSED_BUTTON);
                }
            }
        },

        /**
         * Update the UI for the pager buttons based on the current page and
         * the number of pages.
         *
         * @method _updatePagerButtons
         * @protected
         */
        _updatePagerButtons: function () {
            var carousel = this,
                css      = carousel.CLASSES,
                cur      = carousel._pages.cur, // current page
                el,
                html,
                i,
                item,
                n        = carousel.get("numVisible"),
                num      = carousel._pages.num, // total pages
                pager    = carousel._pages.el;  // the pager container element

            if (num === 0 || !pager) {
                return;         // don't do anything if number of pages is 0
            }

            // Hide the pager before redrawing it
            Dom.setStyle(pager, "visibility", "hidden");

            // Remove all nodes from the pager
            while (pager.firstChild) {
                pager.removeChild(pager.firstChild);
            }

            for (i = 0; i < num; i++) {
                if (JS.isUndefined(carousel._itemsTable.items[i * n])) {
                    Dom.setStyle(pager, "visibility", "visible");
                    break;
                }
                item = carousel._itemsTable.items[i * n].id;

                el   = document.createElement("LI");
                if (!el) {
                    YAHOO.log("Unable to create an LI pager button", "error",
                              WidgetName);
                    Dom.setStyle(pager, "visibility", "visible");
                    break;
                }

                if (i === 0) {
                    Dom.addClass(el, css.FIRST_PAGE);
                }
                if (i == cur) {
                    Dom.addClass(el, css.SELECTED_NAV);
                }

                // TODO: use a template string for i18N compliance
                html = "<a href=\"#" + item + "\" tabindex=\"0\"><em>"   +
                        carousel.STRINGS.PAGER_PREFIX_TEXT + " " + (i+1) +
                        "</em></a>";
                el.innerHTML = html;

                pager.appendChild(el);
            }

            // Show the pager now
            Dom.setStyle(pager, "visibility", "visible");
        },

        /**
         * Update the UI for the pager menu based on the current page and
         * the number of pages.  If the number of pages is greater than
         * MAX_PAGER_BUTTONS, then the selection of pages is provided by a drop
         * down menu instead of a set of buttons.
         *
         * @method _updatePagerMenu
         * @protected
         */
        _updatePagerMenu: function () {
            var carousel = this,
                cur      = carousel._pages.cur, // current page
                el,
                i,
                item,
                n        = carousel.get("numVisible"),
                num      = carousel._pages.num, // total pages
                pager    = carousel._pages.el,  // the pager container element
                sel;

            if (num === 0) {
                return;         // don't do anything if number of pages is 0
            }

            sel = document.createElement("SELECT");
            if (!sel) {
                YAHOO.log("Unable to create the pager menu", "error",
                          WidgetName);
                return;
            }

            // Hide the pager before redrawing it
            Dom.setStyle(pager, "visibility", "hidden");

            // Remove all nodes from the pager
            while (pager.firstChild) {
                pager.removeChild(pager.firstChild);
            }

            for (i = 0; i < num; i++) {
                if (JS.isUndefined(carousel._itemsTable.items[i * n])) {
                    Dom.setStyle(pager, "visibility", "visible");
                    break;
                }
                item = carousel._itemsTable.items[i * n].id;

                el   = document.createElement("OPTION");
                if (!el) {
                    YAHOO.log("Unable to create an OPTION pager menu", "error",
                              WidgetName);
                    Dom.setStyle(pager, "visibility", "visible");
                    break;
                }
                el.value     = "#" + item;
                // TODO: use a template string for i18N compliance
                el.innerHTML = carousel.STRINGS.PAGER_PREFIX_TEXT+" "+(i+1);

                if (i == cur) {
                    el.setAttribute("selected", "selected");
                }

                sel.appendChild(el);
            }

            el = document.createElement("FORM");
            if (!el) {
                YAHOO.log("Unable to create the pager menu", "error",
                          WidgetName);
            } else {
                el.appendChild(sel);
                pager.appendChild(el);
            }

            // Show the pager now
            Dom.setStyle(pager, "visibility", "visible");
        },

        /**
         * Set the correct tab index for the Carousel items.
         *
         * @method _updateTabIndex
         * @param el {Object} The element to be focussed
         * @protected
         */
        _updateTabIndex: function (el) {
            var carousel = this;

            if (el) {
                if (carousel._focusableItemEl) {
                    carousel._focusableItemEl.tabIndex = -1;
                }
                carousel._focusableItemEl = el;
                el.tabIndex = 0;
            }
        },

        /**
         * Validate animation parameters.
         *
         * @method _validateAnimation
         * @param cfg {Object} The animation configuration
         * @return {Boolean} The status of the validation
         * @protected
         */
        _validateAnimation: function (cfg) {
            var rv = true;

            if (JS.isObject(cfg)) {
                if (cfg.speed) {
                    rv = rv && JS.isNumber(cfg.speed);
                }
                if (cfg.effect) {
                    rv = rv && JS.isFunction(cfg.effect);
                } else if (!JS.isUndefined(YAHOO.util.Easing)) {
                    cfg.effect = YAHOO.util.Easing.easeOut;
                }
            } else {
                rv = false;
            }

            return rv;
        },

        /**
         * Validate the firstVisible value.
         *
         * @method _validateFirstVisible
         * @param val {Number} The first visible value
         * @return {Boolean} The status of the validation
         * @protected
         */
        _validateFirstVisible: function (val) {
            var carousel = this, numItems = carousel.get("numItems");

            if (JS.isNumber(val)) {
                if (numItems === 0 && val == numItems) {
                    return true;
                } else {
                    return (val >= 0 && val < numItems);
                }
            }

            return false;
        },

        /**
         * Validate and navigation parameters.
         *
         * @method _validateNavigation
         * @param cfg {Object} The navigation configuration
         * @return {Boolean} The status of the validation
         * @protected
         */
        _validateNavigation : function (cfg) {
            var i;

            if (!JS.isObject(cfg)) {
                return false;
            }

            if (cfg.prev) {
                if (!JS.isArray(cfg.prev)) {
                    return false;
                }
                for (i in cfg.prev) {
                    if (cfg.prev.hasOwnProperty(i)) {
                        if (!JS.isString(cfg.prev[i].nodeName)) {
                            return false;
                        }
                    }
                }
            }

            if (cfg.next) {
                if (!JS.isArray(cfg.next)) {
                    return false;
                }
                for (i in cfg.next) {
                    if (cfg.next.hasOwnProperty(i)) {
                        if (!JS.isString(cfg.next[i].nodeName)) {
                            return false;
                        }
                    }
                }
            }

            return true;
        },

        /**
         * Validate the numItems value.
         *
         * @method _validateNumItems
         * @param val {Number} The numItems value
         * @return {Boolean} The status of the validation
         * @protected
         */
        _validateNumItems: function (val) {
            return JS.isNumber(val) && (val >= 0);
        },

        /**
         * Validate the numVisible value.
         *
         * @method _validateNumVisible
         * @param val {Number} The numVisible value
         * @return {Boolean} The status of the validation
         * @protected
         */
        _validateNumVisible: function (val) {
            var rv = false;

            if (JS.isNumber(val)) {
                rv = val > 0 && val <= this.get("numItems");
            }

            return rv;
        },

        /**
         * Validate the revealAmount value.
         *
         * @method _validateRevealAmount
         * @param val {Number} The revealAmount value
         * @return {Boolean} The status of the validation
         * @protected
         */
        _validateRevealAmount: function (val) {
            var rv = false;

            if (JS.isNumber(val)) {
                rv = val >= 0 && val < 100;
            }

            return rv;
        },

        /**
         * Validate the scrollIncrement value.
         *
         * @method _validateScrollIncrement
         * @param val {Number} The scrollIncrement value
         * @return {Boolean} The status of the validation
         * @protected
         */
        _validateScrollIncrement: function (val) {
            var rv = false;

            if (JS.isNumber(val)) {
                rv = (val > 0 && val < this.get("numItems"));
            }

            return rv;
        }

    });

})();
/*
;;  Local variables: **
;;  mode: js2 **
;;  indent-tabs-mode: nil **
;;  End: **
*/

Copyright © 2009 Yahoo! Inc. All rights reserved.