/**
* 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
*/
(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);
this._navBtns = {};
this._pages = {};
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
*/
var afterScrollEvent = "afterScroll";
/**
* @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
*/
var 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
*/
var 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
*/
var 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
*/
var 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
*/
var 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
*/
var 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
*/
var 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
*/
var 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
*/
var 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
*/
var 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
*/
var 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
*/
var 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
*/
var pageChangeEvent = "pageChange";
/**
* @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
*/
var 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
*/
var 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
*/
var 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
*/
var stopAutoPlayEvent = "stopAutoPlay";
/*
* Private helper functions used by the Carousel component
*/
/**
* Automatically scroll the contents of the Carousel.
* @method autoScroll
* @private
*/
function autoScroll() {
var currIndex = this._firstItem,
index;
if (currIndex >= this.get("numItems") - 1) {
if (this.get("isCircular")) {
index = 0;
} else {
this.stopAutoPlay();
}
} else {
index = currIndex + this.get("numVisible");
}
this.scrollTo.call(this, index);
}
/**
* 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;
function getStyleIntVal(el, style) {
var val;
val = parseInt(Dom.getStyle(el, style), 10);
return JS.isNumber(val) ? val : 0;
}
function getStyleFloatVal(el, style) {
var val;
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);
// 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) {
value = getStyleIntVal(el, "marginLeft");
}
} 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 child,
size = 0,
vertical = false;
if (this._itemsTable.numItems === 0) {
return 0;
}
if (typeof which == "undefined") {
if (this._itemsTable.size > 0) {
return this._itemsTable.size;
}
}
if (JS.isUndefined(this._itemsTable.items[0])) {
return 0;
}
child = Dom.get(this._itemsTable.items[0].id);
if (typeof which == "undefined") {
vertical = this.get("isVertical");
} else {
vertical = which == "height";
}
if (vertical) {
size = getStyle(child, "height");
} else {
size = getStyle(child, "width");
}
if (typeof which == "undefined") {
this._itemsTable.size = size; // save the size for later
}
return size;
}
/**
* 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;
}
/**
* The load the required set of items that are needed for display.
*
* @method loadItems
* @private
*/
function loadItems() {
var first = this.get("firstVisible"),
last = 0,
numItems = this.get("numItems"),
numVisible = this.get("numVisible"),
reveal = this.get("revealAmount");
last = first + numVisible - 1 + (reveal ? 1 : 0);
last = last > numItems - 1 ? numItems - 1 : last;
if (!this.getItem(first) || !this.getItem(last)) {
this.fireEvent(loadItemsEvent, {
ev: loadItemsEvent,
first: first, last: last,
num: last - first
});
}
}
/**
* 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} newposition The index of the new position
* @param {Number} oldposition The index of the previous position
* @private
*/
function setItemSelection(newposition, oldposition) {
var backwards,
cssClass = this.CLASSES,
el,
firstItem = this._firstItem,
isCircular = this.get("isCircular"),
numItems = this.get("numItems"),
numVisible = this.get("numVisible"),
position = oldposition,
sentinel = firstItem + numVisible - 1;
backwards = numVisible > 1 && !isCircular && position > newposition;
if (position >= 0 && position < numItems) {
if (!JS.isUndefined(this._itemsTable.items[position])) {
el = Dom.get(this._itemsTable.items[position].id);
if (el) {
Dom.removeClass(el, cssClass.SELECTED_ITEM);
}
}
}
if (JS.isNumber(newposition)) {
newposition = parseInt(newposition, 10);
newposition = JS.isNumber(newposition) ? newposition : 0;
} else {
newposition = firstItem;
}
if (JS.isUndefined(this._itemsTable.items[newposition])) {
this.scrollTo(newposition); // still loading the item
}
if (!JS.isUndefined(this._itemsTable.items[newposition])) {
el = Dom.get(this._itemsTable.items[newposition].id);
if (el) {
Dom.addClass(el, cssClass.SELECTED_ITEM);
}
}
if (newposition < firstItem || newposition > sentinel) {
// out of focus
if (backwards) {
this.scrollTo(firstItem - numVisible, true);
} else {
this.scrollTo(newposition);
}
}
}
/**
* Fire custom events for enabling/disabling navigation elements.
*
* @method syncNavigation
* @private
*/
function syncNavigation() {
var attach = false,
cssClass = this.CLASSES,
i,
navigation,
sentinel;
navigation = this.get("navigation");
sentinel = this._firstItem + this.get("numVisible");
if (navigation.prev) {
if (this._firstItem === 0) {
if (!this.get("isCircular")) {
Event.removeListener(navigation.prev, "click",
scrollPageBackward);
Dom.addClass(navigation.prev, cssClass.FIRST_NAV_DISABLED);
for (i = 0; i < this._navBtns.prev.length; i++) {
this._navBtns.prev[i].setAttribute("disabled", "true");
}
this._prevEnabled = false;
} else {
attach = !this._prevEnabled;
}
} else {
attach = !this._prevEnabled;
}
if (attach) {
Event.on(navigation.prev, "click", scrollPageBackward, this);
Dom.removeClass(navigation.prev, cssClass.FIRST_NAV_DISABLED);
for (i = 0; i < this._navBtns.prev.length; i++) {
this._navBtns.prev[i].removeAttribute("disabled");
}
this._prevEnabled = true;
}
}
attach = false;
if (navigation.next) {
if (sentinel >= this.get("numItems")) {
if (!this.get("isCircular")) {
Event.removeListener(navigation.next, "click",
scrollPageForward);
Dom.addClass(navigation.next, cssClass.DISABLED);
for (i = 0; i < this._navBtns.next.length; i++) {
this._navBtns.next[i].setAttribute("disabled", "true");
}
this._nextEnabled = false;
} else {
attach = !this._nextEnabled;
}
} else {
attach = !this._nextEnabled;
}
if (attach) {
Event.on(navigation.next, "click", scrollPageForward, this);
Dom.removeClass(navigation.next, cssClass.DISABLED);
for (i = 0; i < this._navBtns.next.length; i++) {
this._navBtns.next[i].removeAttribute("disabled");
}
this._nextEnabled = true;
}
}
this.fireEvent(navigationStateChangeEvent,
{ next: this._nextEnabled, prev: this._prevEnabled });
}
/**
* Fire custom events for synchronizing the DOM.
*
* @method syncUI
* @param {Object} o The item that needs to be added or removed
* @private
*/
function syncUI(o) {
var el, i, item, num, oel, pos, sibling;
if (!JS.isObject(o)) {
return;
}
switch (o.ev) {
case itemAddedEvent:
pos = JS.isUndefined(o.pos) ? this._itemsTable.numItems-1 : o.pos;
if (!JS.isUndefined(this._itemsTable.items[pos])) {
item = this._itemsTable.items[pos];
if (item && !JS.isUndefined(item.id)) {
oel = Dom.get(item.id);
}
}
if (!oel) {
el = this._createCarouselItem({
className : item.className,
content : item.item,
id : item.id
});
if (JS.isUndefined(o.pos)) {
if (!JS.isUndefined(this._itemsTable.loading[pos])) {
oel = this._itemsTable.loading[pos];
}
if (oel) {
this._carouselEl.replaceChild(el, oel);
} else {
this._carouselEl.appendChild(el);
}
} else {
if (!JS.isUndefined(this._itemsTable.items[o.pos + 1])) {
sibling = Dom.get(this._itemsTable.items[o.pos + 1].id);
}
if (sibling) {
this._carouselEl.insertBefore(el, sibling);
} else {
YAHOO.log("Unable to find sibling","error",WidgetName);
}
}
} else {
if (JS.isUndefined(o.pos)) {
if (!Dom.isAncestor(this._carouselEl, oel)) {
this._carouselEl.appendChild(oel);
}
} else {
if (!Dom.isAncestor(this._carouselEl, oel)) {
if (!JS.isUndefined(this._itemsTable.items[o.pos+1])) {
this._carouselEl.insertBefore(oel, Dom.get(
this._itemsTable.items[o.pos+1].id));
}
}
}
}
if (this._recomputeSize) {
this._setClipContainerSize();
}
break;
case itemRemovedEvent:
num = this.get("numItems");
item = o.item;
pos = o.pos;
if (item && (el = Dom.get(item.id))) {
if (el && Dom.isAncestor(this._carouselEl, el)) {
Event.purgeElement(el, true);
this._carouselEl.removeChild(el);
}
if (this.get("selectedItem") == pos) {
pos = pos >= num ? num - 1 : pos;
this.set("selectedItem", pos);
}
} else {
YAHOO.log("Unable to find item", "warn", WidgetName);
}
break;
case loadItemsEvent:
for (i = o.first; i <= o.last; i++) {
el = this._createCarouselItem({
content : this.CONFIG.ITEM_LOADING,
id : Dom.generateId()
});
if (el) {
if (!JS.isUndefined(this._itemsTable.items[o.last + 1])) {
sibling = Dom.get(this._itemsTable.items[o.last+1].id);
if (sibling) {
this._carouselEl.insertBefore(el, sibling);
} else {
YAHOO.log("Unable to find sibling", "error",
WidgetName);
}
} else {
this._carouselEl.appendChild(el);
}
}
this._itemsTable.loading[i] = el;
}
break;
}
}
/*
* 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] : false;
};
YAHOO.extend(Carousel, YAHOO.util.Element, {
/*
* Internal variables used within the Carousel component
*/
/**
* 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,
/**
* Is the animation still in progress?
*
* @property _isAnimationInProgress
* @private
*/
_isAnimationInProgress: 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 navigation element container class name.
*
* @property NAVIGATION
* @default "yui-carousel-nav"
*/
NAVIGATION: "yui-carousel-nav",
/**
* 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 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 element to be used as the progress indicator when the item
* is still being loaded.
*
* @property ITEM_LOADING
* @default The progress indicator (spinner) image
*/
ITEM_LOADING: "<img " +
"src=\"../../build/carousel/assets/ajax-loader.gif\" " +
"alt=\"Loading\" " +
"style=\"margin-top:-32px;position:relative;top:50%;\">",
/**
* The tag name of the Carousel item.
*
* @property ITEM_TAG_NAME
* @default "LI"
*/
ITEM_TAG_NAME: "LI",
/**
* 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 Carousel container to support the
* navigation buttons.
*
* @property MIN_WIDTH
* @default 99
*/
MIN_WIDTH: 99,
/**
* The number of visible items in the Carousel.
*
* @property NUM_VISIBLE
* @default 3
*/
NUM_VISIBLE: 3,
/**
* The tag name of the Carousel.
*
* @property TAG_NAME
* @default "OL"
*/
TAG_NAME: "OL"
},
/*
* Internationalizable strings in the Carousel component
*/
STRINGS: {
/**
* 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 className, content, el, elId, numItems = this.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)) {
this._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;
}
this._itemsTable.items.splice(index, 0, {
item : content,
className : className,
id : elId
});
}
this._itemsTable.numItems++;
if (numItems < this._itemsTable.items.length) {
this.set("numItems", this._itemsTable.items.length);
}
this.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 n = this.get("numItems");
while (n > 0) {
this.removeItem(0);
n--;
}
},
/**
* Set focus on the Carousel.
*
* @method focus
* @public
*/
focus: function () {
var selItem,
numVisible,
selectOnScroll,
selected,
first,
last,
isSelectionInvisible,
focusEl,
itemsTable;
if (this._isAnimationInProgress) {
// this messes up real bad!
return;
}
selItem = this.get("selectedItem");
numVisible = this.get("numVisible");
selectOnScroll = this.get("selectOnScroll");
selected = this.getItem(selItem);
first = this.get("firstVisible");
last = first + numVisible - 1;
isSelectionInvisible = (selItem < first || selItem > last);
focusEl = (selected && selected.id) ?
Dom.get(selected.id) : null;
itemsTable = this._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
}
}
this.fireEvent(focusEvent);
},
/**
* Hide the Carousel.
*
* @method hide
* @public
*/
hide: function () {
if (this.fireEvent(beforeHideEvent) !== false) {
this.removeClass(this.CLASSES.VISIBLE);
this.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 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;
}
this._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;
}
if (el) {
if (!el.id) { // in case the HTML element is passed
el.setAttribute("id", Dom.generateId());
}
this._parseCarousel(el);
parse = true;
} else {
el = this._createCarousel(elId);
}
elId = el.id;
Carousel.superclass.init.call(this, el, attrs);
this.initEvents();
if (parse) {
this._parseCarouselItems();
}
if (!attrs || typeof attrs.isVertical == "undefined") {
this.set("isVertical", false);
}
this._parseCarouselNavigation(el);
this._navEl = this._setupCarouselNavigation();
instances[elId] = this;
loadItems.call(this);
},
/**
* 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) {
attrs = attrs || {};
Carousel.superclass.initAttributes.call(this, attrs);
/**
* @attribute currentPage
* @description The current page number (read-only.)
* @type Number
*/
this.setAttributeConfig("currentPage", {
readOnly : true,
value : 0
});
/**
* @attribute firstVisible
* @description The index to start the Carousel from (indexes begin
* from zero)
* @default 0
* @type Number
*/
this.setAttributeConfig("firstVisible", {
method : this._setFirstVisible,
validator : this._validateFirstVisible,
value : attrs.firstVisible || this.CONFIG.FIRST_VISIBLE
});
/**
* @attribute selectOnScroll
* @description Set this to true to automatically set focus to
* follow scrolling in the Carousel.
* @default true
* @type Boolean
*/
this.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
*/
this.setAttributeConfig("numVisible", {
method : this._setNumVisible,
validator : this._validateNumVisible,
value : attrs.numVisible || this.CONFIG.NUM_VISIBLE
});
/**
* @attribute numItems
* @description The number of items in the Carousel.
* @type Number
*/
this.setAttributeConfig("numItems", {
method : this._setNumItems,
validator : this._validateNumItems,
value : this._itemsTable.numItems
});
/**
* @attribute scrollIncrement
* @description The number of items to scroll by for arrow keys.
* @default 1
* @type Number
*/
this.setAttributeConfig("scrollIncrement", {
validator : this._validateScrollIncrement,
value : attrs.scrollIncrement || 1
});
/**
* @attribute selectedItem
* @description The index of the selected item.
* @type Number
*/
this.setAttributeConfig("selectedItem", {
method : this._setSelectedItem,
validator : JS.isNumber,
value : 0
});
/**
* @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
*/
this.setAttributeConfig("revealAmount", {
method : this._setRevealAmount,
validator : this._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
*/
this.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
*/
this.setAttributeConfig("isVertical", {
method : this._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
*/
this.setAttributeConfig("navigation", {
method : this._setNavigation,
validator : this._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
*/
this.setAttributeConfig("animation", {
validator : this._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
*/
this.setAttributeConfig("autoPlay", {
validator : JS.isNumber,
value : attrs.autoPlay || 0
});
},
/**
* Initialize and bind the event handlers.
*
* @method initEvents
* @public
*/
initEvents: function () {
var cssClass = this.CLASSES;
this.on("keydown", this._keyboardEventHandler);
this.subscribe(afterScrollEvent, syncNavigation);
this.on(afterScrollEvent, this.focus);
this.subscribe(itemAddedEvent, syncUI);
this.subscribe(itemAddedEvent, syncNavigation);
this.subscribe(itemRemovedEvent, syncUI);
this.subscribe(itemRemovedEvent, syncNavigation);
this.on(itemSelectedEvent, this.focus);
this.subscribe(loadItemsEvent, syncUI);
this.subscribe(pageChangeEvent, this._syncPagerUI);
this.subscribe(renderEvent, syncNavigation);
this.subscribe(renderEvent, this._syncPagerUI);
this.on("selectedItemChange", function (ev) {
setItemSelection.call(this, ev.newValue, ev.prevValue);
this._updateTabIndex(this.getElementForItem(ev.newValue));
this.fireEvent(itemSelectedEvent, ev.newValue);
});
this.on("firstVisibleChange", function (ev) {
if (!this.get("selectOnScroll")) {
this._updateTabIndex(this.getElementForItem(ev.newValue));
}
});
// Handle item selection on mouse click
this.on("click", this._itemClickHandler);
// Handle page navigation
this.on("click", this._pagerClickHandler);
// Restore the focus on the navigation buttons
Event.onFocus(this.get("element"), function (ev, obj) {
obj._updateNavButtons(Event.getTarget(ev), true);
}, this);
Event.onBlur(this.get("element"), function (ev, obj) {
obj._updateNavButtons(Event.getTarget(ev), false);
}, this);
},
/**
* Return the ITEM_TAG_NAME 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) {
if (index < 0 || index >= this.get("numItems")) {
YAHOO.log("Index out of bounds", "error", WidgetName);
return null;
}
// TODO: may be cache the item
if (this._itemsTable.numItems > index) {
if (!JS.isUndefined(this._itemsTable.items[index])) {
return Dom.get(this._itemsTable.items[index].id);
}
}
return null;
},
/**
* Return the ITEM_TAG_NAME for all items in the Carousel.
*
* @method getElementForItems
* @return {Array} Return all the items
* @public
*/
getElementForItems: function () {
var els = [], i;
for (i = 0; i < this._itemsTable.numItems; i++) {
els.push(this.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) {
if (index < 0 || index >= this.get("numItems")) {
YAHOO.log("Index out of bounds", "error", WidgetName);
return null;
}
if (this._itemsTable.numItems > index) {
if (!JS.isUndefined(this._itemsTable.items[index])) {
return this._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 i = 0, n = this._itemsTable.numItems;
while (i < n) {
if (!JS.isUndefined(this._itemsTable.items[i])) {
if (this._itemsTable.items[i].id == id) {
return i;
}
}
i++;
}
return -1;
},
/**
* 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 item, num = this.get("numItems");
if (index < 0 || index >= num) {
YAHOO.log("Index out of bounds", "error", WidgetName);
return false;
}
item = this._itemsTable.items.splice(index, 1);
if (item && item.length == 1) {
this.set("numItems", num - 1);
this.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 config = this.CONFIG,
cssClass = this.CLASSES,
size;
this.addClass(cssClass.CAROUSEL);
if (!this._clipEl) {
this._clipEl = this._createCarouselClip();
this._clipEl.appendChild(this._carouselEl);
}
if (appendTo) {
this.appendChild(this._clipEl);
this.appendTo(appendTo);
this._setClipContainerSize();
} else {
if (!Dom.inDocument(this.get("element"))) {
YAHOO.log("Nothing to render. The container should be " +
"within the document if appendTo is not " +
"specified", "error", WidgetName);
return false;
}
this.appendChild(this._clipEl);
}
if (this.get("isVertical")) {
size = getCarouselItemSize.call(this);
size = size < config.MIN_WIDTH ? config.MIN_WIDTH : size;
this.setStyle("width", size + "px");
this.addClass(cssClass.VERTICAL);
} else {
this.addClass(cssClass.HORIZONTAL);
}
if (this.get("numItems") < 1) {
YAHOO.log("No items in the Carousel to render", "warn",
WidgetName);
return false;
}
// Make sure at least one item is selected
this.set("selectedItem", this.get("firstVisible"));
this.fireEvent(renderEvent);
// By now, the navigation would have been rendered, so calculate
// the container height now.
this._setContainerSize();
return true;
},
/**
* Scroll the Carousel by an item backward.
*
* @method scrollBackward
* @public
*/
scrollBackward: function () {
this.scrollTo(this._firstItem - this.get("scrollIncrement"));
},
/**
* Scroll the Carousel by an item forward.
*
* @method scrollForward
* @public
*/
scrollForward: function () {
this.scrollTo(this._firstItem + this.get("scrollIncrement"));
},
/**
* Scroll the Carousel by a page backward.
*
* @method scrollPageBackward
* @public
*/
scrollPageBackward: function () {
this.scrollTo(this._firstItem - this.get("numVisible"));
},
/**
* Scroll the Carousel by a page forward.
*
* @method scrollPageForward
* @public
*/
scrollPageForward: function () {
this.scrollTo(this._firstItem + this.get("numVisible"));
},
/**
* 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 anim,
animate,
animAttrs,
animCfg = this.get("animation"),
isCircular = this.get("isCircular"),
delta,
direction,
firstItem = this._firstItem,
newPage,
numItems = this.get("numItems"),
numPerPage = this.get("numVisible"),
offset,
page = this.get("currentPage"),
rv,
sentinel,
which;
if (item == firstItem) {
return; // nothing to do!
}
if (this._isAnimationInProgress) {
return; // let it take its own sweet time to complete
}
if (item < 0) {
if (isCircular) {
item = numItems + item;
} else {
return;
}
} else if (item > numItems - 1) {
if (this.get("isCircular")) {
item = numItems - item;
} else {
return;
}
}
direction = (this._firstItem > item) ? "backward" : "forward";
sentinel = firstItem + numPerPage;
sentinel = (sentinel > numItems - 1) ? numItems - 1 : sentinel;
rv = this.fireEvent(beforeScrollEvent,
{ dir: direction, first: firstItem, last: sentinel });
if (rv === false) { // scrolling is prevented
return;
}
this.fireEvent(beforePageChangeEvent, { page: page });
delta = firstItem - item; // yes, the delta is reverse
this._firstItem = item;
this.set("firstVisible", item);
YAHOO.log("Scrolling to " + item + " delta = " + delta, WidgetName);
loadItems.call(this); // do we have all the items to display?
sentinel = item + numPerPage;
sentinel = (sentinel > numItems - 1) ? numItems - 1 : sentinel;
which = this.get("isVertical") ? "top" : "left";
offset = getScrollOffset.call(this, delta);
YAHOO.log("Scroll offset = " + offset, WidgetName);
animate = animCfg.speed > 0;
if (animate) {
this._isAnimationInProgress = true;
if (this.get("isVertical")) {
animAttrs = { points: { by: [0, offset] } };
} else {
animAttrs = { points: { by: [offset, 0] } };
}
anim = new YAHOO.util.Motion(this._carouselEl, animAttrs,
animCfg.speed, animCfg.effect);
anim.onComplete.subscribe(function (ev) {
var first = this.get("firstVisible");
this._isAnimationInProgress = false;
this.fireEvent(afterScrollEvent,
{ first: first, last: sentinel });
}, null, this);
anim.animate();
anim = null;
} else {
offset += getStyle(this._carouselEl, which);
Dom.setStyle(this._carouselEl, which, offset + "px");
}
newPage = parseInt(this._firstItem / numPerPage, 10);
if (newPage != page) {
this.setAttributeConfig("currentPage", { value: newPage });
this.fireEvent(pageChangeEvent, newPage);
}
if (!dontSelect) {
if (this.get("selectOnScroll")) {
if (item != this._selectedItem) { // out of sync
this.set("selectedItem", this._getSelectedItem(item));
}
}
}
delete this._autoPlayTimer;
if (this.get("autoPlay") > 0) {
this.startAutoPlay();
}
if (!animate) {
this.fireEvent(afterScrollEvent,
{ first: item, last: sentinel });
}
},
/**
* Display the Carousel.
*
* @method show
* @public
*/
show: function () {
var cssClass = this.CLASSES;
if (this.fireEvent(beforeShowEvent) !== false) {
this.addClass(cssClass.VISIBLE);
this.fireEvent(showEvent);
}
},
/**
* Start auto-playing the Carousel.
*
* @method startAutoPlay
* @public
*/
startAutoPlay: function () {
var self = this,
timer = this.get("autoPlay");
if (timer > 0) {
if (!JS.isUndefined(this._autoPlayTimer)) {
return;
}
this.fireEvent(startAutoPlayEvent);
this._autoPlayTimer = setTimeout(function () {
autoScroll.call(self); }, timer);
}
},
/**
* Stop auto-playing the Carousel.
*
* @method stopAutoPlay
* @public
*/
stopAutoPlay: function () {
if (!JS.isUndefined(this._autoPlayTimer)) {
clearTimeout(this._autoPlayTimer);
delete this._autoPlayTimer;
this.set("autoPlay", 0);
this.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
*/
/**
* Create the Carousel.
*
* @method createCarousel
* @param elId {String} The id of the element to be created
* @protected
*/
_createCarousel: function (elId) {
var cssClass = this.CLASSES;
var el = createElement("DIV", {
className : cssClass.CAROUSEL,
id : elId
});
if (!this._carouselEl) {
this._carouselEl = createElement(this.CONFIG.TAG_NAME,
{ className: cssClass.CAROUSEL_EL });
}
return el;
},
/**
* Create the Carousel clip container.
*
* @method createCarouselClip
* @protected
*/
_createCarouselClip: function () {
var el = createElement("DIV", { className: this.CLASSES.CONTENT });
this._setClipContainerSize(el);
return el;
},
/**
* Create the Carousel item.
*
* @method createCarouselItem
* @param obj {Object} The attributes of the element to be created
* @protected
*/
_createCarouselItem: function (obj) {
return createElement(this.CONFIG.ITEM_TAG_NAME, {
className : obj.className,
content : obj.content,
id : obj.id
});
},
/**
* 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 isCircular = this.get("isCircular"),
numItems = this.get("numItems"),
sentinel = numItems - 1;
if (val < 0) {
if (isCircular) {
val = numItems + val;
} else {
val = this.get("selectedItem");
}
} else if (val > sentinel) {
if (isCircular) {
val = val - numItems;
} else {
val = this.get("selectedItem");
}
}
return val;
},
/**
* The "click" handler for the item.
*
* @method _itemClickHandler
* @param {Event} ev The event object
* @protected
*/
_itemClickHandler: function (ev) {
var container = this.get("element"),
el,
item,
target = YAHOO.util.Event.getTarget(ev);
while (target && target != container &&
target.id != this._carouselEl) {
el = target.nodeName;
if (el.toUpperCase() == this.CONFIG.ITEM_TAG_NAME) {
break;
}
target = target.parentNode;
}
if ((item = this.getItemPositionById(target.id)) >= 0) {
YAHOO.log("Setting selection to " + item, WidgetName);
this.set("selectedItem", this._getSelectedItem(item));
}
},
/**
* The keyboard event handler for Carousel.
*
* @method _keyboardEventHandler
* @param ev {Event} The event that is being handled.
* @protected
*/
_keyboardEventHandler: function (ev) {
var key = Event.getCharCode(ev),
prevent = false,
position = 0,
selItem;
if (this._isAnimationInProgress) {
return; // do not mess while animation is in progress
}
switch (key) {
case 0x25: // left arrow
case 0x26: // up arrow
selItem = this.get("selectedItem");
if (selItem == this._firstItem) {
position = selItem - this.get("numVisible");
this.scrollTo(position);
this.set("selectedItem", this._getSelectedItem(selItem-1));
} else {
position = this.get("selectedItem") -
this.get("scrollIncrement");
this.set("selectedItem", this._getSelectedItem(position));
}
prevent = true;
break;
case 0x27: // right arrow
case 0x28: // down arrow
position = this.get("selectedItem")+this.get("scrollIncrement");
this.set("selectedItem", this._getSelectedItem(position));
prevent = true;
break;
case 0x21: // page-up
this.scrollPageBackward();
prevent = true;
break;
case 0x22: // page-down
this.scrollPageForward();
prevent = true;
break;
}
if (prevent) {
Event.preventDefault(ev);
}
},
/**
* The "click" handler for the pager navigation.
*
* @method _pagerClickHandler
* @param {Event} ev The event object
* @protected
*/
_pagerClickHandler: function (ev) {
var pos, target, val;
target = Event.getTarget(ev);
val = target.href || target.value;
if (JS.isString(val) && val) {
pos = val.lastIndexOf("#");
if (pos != -1) {
val = this.getItemPositionById(val.substring(pos + 1));
this.scrollTo(val);
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 child, cssClass, found, node;
cssClass = this.CLASSES;
found = false;
for (child = parent.firstChild; child; child = child.nextSibling) {
if (child.nodeType == 1) {
node = child.nodeName;
if (node.toUpperCase() == this.CONFIG.TAG_NAME) {
this._carouselEl = child;
Dom.addClass(this._carouselEl,this.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 child,
elId,
node,
parent = this._carouselEl;
for (child = parent.firstChild; child; child = child.nextSibling) {
if (child.nodeType == 1) {
node = child.nodeName;
if (node.toUpperCase() == this.CONFIG.ITEM_TAG_NAME) {
if (child.id) {
elId = child.id;
} else {
elId = Dom.generateId();
child.setAttribute("id", elId);
}
this.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 cfg, cssClass = this.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") {
if (typeof this._navBtns.prev == "undefined") {
this._navBtns.prev = [];
}
this._navBtns.prev.push(el);
} else {
j = el.getElementsByTagName("INPUT");
if (JS.isArray(j) && j.length > 0) {
this._navBtns.prev.push(j[0]);
} else {
j = el.getElementsByTagName("BUTTON");
if (JS.isArray(j) && j.length > 0) {
this._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") {
if (typeof this._navBtns.next == "undefined") {
this._navBtns.next = [];
}
this._navBtns.next.push(el);
} else {
j = el.getElementsByTagName("INPUT");
if (JS.isArray(j) && j.length > 0) {
this._navBtns.next.push(j[0]);
} else {
j = el.getElementsByTagName("BUTTON");
if (JS.isArray(j) && j.length > 0) {
this._navBtns.next.push(j[0]);
}
}
}
}
}
if (cfg) {
cfg.next = nav;
} else {
cfg = { next: nav };
}
}
if (cfg) {
this.set("navigation", cfg);
rv = true;
}
return rv;
},
/**
* Setup/Create the Carousel navigation element (if needed).
*
* @method _setupCarouselNavigation
* @protected
*/
_setupCarouselNavigation: function () {
var btn, cfg, cssClass, nav, navContainer, nextButton, pageEl,
prevButton;
cssClass = this.CLASSES;
navContainer = Dom.getElementsByClassName(cssClass.NAVIGATION,
"DIV", this.get("element"));
if (navContainer.length === 0) {
navContainer = createElement("DIV",
{ className: cssClass.NAVIGATION });
this.insertBefore(navContainer,
Dom.getFirstChild(this.get("element")));
} else {
navContainer = navContainer[0];
}
this._pages.el = createElement("UL");
navContainer.appendChild(this._pages.el);
nav = this.get("navigation");
if (nav.prev && nav.prev.length > 0) {
navContainer.appendChild(nav.prev[0]);
} 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 = "<input type=\"button\" " +
"id=\"" + btn + "\" " +
"value=\"" + this.STRINGS.PREVIOUS_BUTTON_TEXT + "\" " +
"name=\"" + this.STRINGS.PREVIOUS_BUTTON_TEXT + "\">";
navContainer.appendChild(prevButton);
btn = Dom.get(btn);
this._navBtns.prev = [btn];
cfg = { prev: [prevButton] };
}
if (nav.next && nav.next.length > 0) {
navContainer.appendChild(nav.next[0]);
} else {
// TODO: separate method for creating a navigation button
nextButton = createElement("SPAN",
{ className: cssClass.BUTTON });
// XXX: for IE 6.x
Dom.setStyle(nextButton, "visibility", "visible");
btn = Dom.generateId();
nextButton.innerHTML = "<input type=\"button\" " +
"id=\"" + btn + "\" " +
"value=\"" + this.STRINGS.NEXT_BUTTON_TEXT + "\" " +
"name=\"" + this.STRINGS.NEXT_BUTTON_TEXT + "\">";
navContainer.appendChild(nextButton);
btn = Dom.get(btn);
this._navBtns.next = [btn];
if (cfg) {
cfg.next = [nextButton];
} else {
cfg = { next: [nextButton] };
}
}
if (cfg) {
this.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 attr, currVal, isVertical, itemSize, reveal, size, which;
isVertical = this.get("isVertical");
reveal = this.get("revealAmount");
which = isVertical ? "height" : "width";
attr = isVertical ? "top" : "left";
clip = clip || this._clipEl;
if (!clip) {
return;
}
num = num || this.get("numVisible");
itemSize = getCarouselItemSize.call(this, which);
size = itemSize * num;
this._recomputeSize = (size === 0); // bleh!
if (this._recomputeSize) {
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(this._carouselEl, attr));
currVal = JS.isNumber(currVal) ? currVal : 0;
Dom.setStyle(this._carouselEl, attr, currVal+(reveal/2)+"px");
}
if (isVertical) {
size += getStyle(this._carouselEl, "marginTop") +
getStyle(this._carouselEl, "marginBottom") +
getStyle(this._carouselEl, "paddingTop") +
getStyle(this._carouselEl, "paddingBottom") +
getStyle(this._carouselEl, "borderTop") +
getStyle(this._carouselEl, "borderBottom");
// XXX: for vertical Carousel
Dom.setStyle(clip, which, (size - (num - 1)) + "px");
} else {
size += getStyle(this._carouselEl, "marginLeft") +
getStyle(this._carouselEl, "marginRight") +
getStyle(this._carouselEl, "paddingLeft") +
getStyle(this._carouselEl, "paddingRight") +
getStyle(this._carouselEl, "borderLeft") +
getStyle(this._carouselEl, "borderRight");
Dom.setStyle(clip, which, size + "px");
}
this._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 isVertical, size;
isVertical = this.get("isVertical");
clip = clip || this._clipEl;
attr = attr || (isVertical ? "height" : "width");
size = parseFloat(Dom.getStyle(clip, attr), 10);
size = JS.isNumber(size) ? size : 0;
size += getStyle(clip, "marginLeft") +
getStyle(clip, "marginRight") +
getStyle(clip, "paddingLeft") +
getStyle(clip, "paddingRight") +
getStyle(clip, "borderLeft") +
getStyle(clip, "borderRight");
if (isVertical) {
size += getStyle(this._navEl, "height");
}
this.setStyle(attr, 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) {
if (val >= 0 && val < this.get("numItems")) {
this.scrollTo(val);
} else {
val = this.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) {
if (cfg.prev) {
Event.on(cfg.prev, "click", scrollPageBackward, this);
}
if (cfg.next) {
Event.on(cfg.next, "click", scrollPageForward, this);
}
},
/**
* 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) {
if (val > 1 && val < this.get("numItems")) {
this._setClipContainerSize(this._clipEl, val);
} else {
val = this.get("numVisible");
}
return 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 num = this._itemsTable.numItems;
if (JS.isArray(this._itemsTable.items)) {
if (this._itemsTable.items.length != num) { // out of sync
num = this._itemsTable.items.length;
this._itemsTable.numItems = num;
}
}
if (val < num) {
while (num > val) {
this.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 cssClass = this.CLASSES;
if (val) {
this.replaceClass(cssClass.HORIZONTAL, cssClass.VERTICAL);
} else {
this.replaceClass(cssClass.VERTICAL, cssClass.HORIZONTAL);
}
this._itemsTable.size = 0; // invalidate our size computation cache
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) {
if (val >= 0 && val <= 100) {
val = parseInt(val, 10);
val = JS.isNumber(val) ? val : 0;
this._setClipContainerSize();
} else {
val = this.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 Pager UI if necessary.
*
* @method _syncPagerUI
* @protected
*/
_syncPagerUI: function (page) {
var a,
cssClass = this.CLASSES,
i,
markup = "",
numPages,
numVisible = this.get("numVisible");
page = page || 0;
numPages = Math.ceil(this.get("numItems") / numVisible);
this._pages.num = numPages;
this._pages.cur = page;
if (numPages > this.CONFIG.MAX_PAGER_BUTTONS) {
markup = "<form><select>";
} else {
markup = "";
}
for (i = 0; i < numPages; i++) {
if (JS.isUndefined(this._itemsTable.items[i * numVisible])) {
break;
}
a = this._itemsTable.items[i * numVisible].id;
if (numPages > this.CONFIG.MAX_PAGER_BUTTONS) {
markup += "<option value=\"#" + a + "\" " +
(i == page ? " selected" : "") + ">" +
this.STRINGS.PAGER_PREFIX_TEXT + " " + (i+1) +
"</option>";
} else {
markup += "<li class=\"" +
(i === 0 ? cssClass.FIRST_PAGE : "") +
(i == page ? " " + cssClass.SELECTED_NAV : "") +
"\"><a href=\"#" + a + "\" tabindex=\"0\"><em>" +
this.STRINGS.PAGER_PREFIX_TEXT + " " + (i+1) +
"</em></a></li>";
}
}
if (numPages > this.CONFIG.MAX_PAGER_BUTTONS) {
markup += "</select></form>";
}
this._pages.el.innerHTML = markup;
markup = null;
},
/**
* 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() == "INPUT" &&
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);
}
}
},
/**
* Set the correct tab index for the Carousel items.
*
* @method _updateTabIndex
* @param el {Object} The element to be focussed
* @protected
*/
_updateTabIndex: function (el) {
if (el) {
if (this._focusableItemEl) {
this._focusableItemEl.tabIndex = -1;
}
this._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 rv = false;
if (JS.isNumber(val)) {
rv = (val >= 0 && val < this.get("numItems"));
}
return rv;
},
/**
* 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) {
var rv = false;
if (JS.isNumber(val)) {
rv = val > 0;
}
return rv;
},
/**
* 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;
}
});
})();