/** * @class Ext.Carousel * @extends Ext.Panel * *

A customized Panel which provides the ability to slide back and forth between * different child items.

* *

Useful Properties

* * *

Useful Methods

* * *

Screenshot:

*

* *

Example code:


var carousel = new Ext.Carousel({
    items: [
        {
            html: '<p>Navigate the carousel on this page by swiping left/right.</p>',
            cls : 'card card1'
        },
        {
            html: '<p>Clicking on either side of the indicators below</p>',
            cls : 'card card2'
        },
        {
            html: 'Card #3',
            cls : 'card card3'
        }
    ]
});

var panel = new Ext.Panel({
    cls: 'cards',
    layout: {
        type : 'vbox',
        align: 'stretch'
    },
    defaults: {
        flex: 1
    },
    items: [
        carousel,
        {
            xtype    : 'carousel',
            ui       : 'light',
            direction: 'vertical',
            
            items: [
            {
                    html: '<p>Carousels can be vertical and given a ui of "light" or "dark".</p>',
                    cls : 'card card1'
                },
                {
                    html: 'Card #2',
                    cls : 'card card2'
                },
                {
                    html: 'Card #3',
                    cls : 'card card3'
                }
            ]
        }
    ]
});
* @xtype carousel */ Ext.Carousel = Ext.extend(Ext.Panel, {
/** * @cfg {String} baseCls * The base CSS class to apply to the Carousel's element (defaults to 'x-carousel'). */ baseCls: 'x-carousel',
/** * @cfg {Boolean} indicator * Provides an indicator while toggling between child items to let the user * know where they are in the card stack. */ indicator: true,
/** * @cfg {String} ui * Style options for Carousel. Default is 'dark'. 'light' is also available. */ ui: 'dark',
/** * @cfg {String} direction * The direction of the Carousel. Default is 'horizontal'. 'vertical' also available. */ direction: 'horizontal', // @private horizontal: false, // @private vertical: false, // @private initComponent: function() { this.layout = { type: 'card', // This will set the size of all cards in this container on each layout sizeAllCards: true, // This will prevent the hiding of items on card switch hideInactive: false, itemCls: 'x-carousel-item', targetCls: 'x-carousel-body', setOwner : function(owner) { Ext.layout.CardLayout.superclass.setOwner.call(this, owner); } }; if (this.indicator) { var cfg = Ext.isObject(this.indicator) ? this.indicator : {}; this.indicator = new Ext.Carousel.Indicator(Ext.apply({}, cfg, { direction: this.direction, carousel: this, ui: this.ui })); } if (this.direction == 'horizontal') { this.horizontal = true; } else { this.vertical = true; } Ext.Carousel.superclass.initComponent.call(this); }, // @private afterRender: function() { Ext.Carousel.superclass.afterRender.call(this); // Bind the required listeners this.mon(this.body, { drag: this.onDrag, dragThreshold: 5, dragend: this.onDragEnd, direction: this.direction, scope: this }); this.el.addCls(this.baseCls + '-' + this.direction); }, // private, inherit docs onAdd: function(){ Ext.Carousel.superclass.onAdd.apply(this, arguments); var indicator = this.indicator; if (indicator) { indicator.onCardAdd(); } }, // private, inherit docs onRemove: function(){ Ext.Carousel.superclass.onRemove.apply(this, arguments); var indicator = this.indicator; if (indicator) { indicator.onCardRemove(); } }, /** * The afterLayout method on the carousel just makes sure the active card * is still into view. It also makes sure the indicator is pointing to * the right card. * @private */ afterLayout : function() { Ext.Carousel.superclass.afterLayout.apply(this, arguments); this.currentSize = this.body.getSize(); this.currentScroll = {x: 0, y: 0}; this.updateCardPositions(); var activeItem = this.layout.getActiveItem(); if (activeItem && this.indicator) { this.indicator.onBeforeCardSwitch(this, activeItem, null, this.items.indexOf(activeItem)); } }, /** * The onDrag method sets the currentScroll object. It also slows down the drag * if we are at the bounds of the carousel. * @private */ onDrag : function(e) { this.currentScroll = { x: e.deltaX, y: e.deltaY }; // Slow the drag down in the bounce var activeIndex = this.items.items.indexOf(this.layout.activeItem); // If this is a horizontal carousel if (this.horizontal) { if ( // And we are on the first card and dragging left (activeIndex == 0 && e.deltaX > 0) || // Or on the last card and dragging right (activeIndex == this.items.length - 1 && e.deltaX < 0) ) { // Then slow the drag down this.currentScroll.x = e.deltaX / 2; } } // If this is a vertical carousel else if (this.vertical) { if ( // And we are on the first card and dragging up (activeIndex == 0 && e.deltaY > 0) || // Or on the last card and dragging down (activeIndex == this.items.length - 1 && e.deltaY < 0) ) { // Then slow the drag down this.currentScroll.y = e.deltaY / 2; } } // This will update all the cards to their correct position based on the current drag this.updateCardPositions(); }, /** * This will update all the cards to their correct position based on the current drag. * It can be passed true to animate the position updates. * @private */ updateCardPositions : function(animate) { var cards = this.items.items, ln = cards.length, cardOffset, i, card, el, style; // Now we loop over the items and position the active item // in the middle of the strip, and the two items on either // side to the left and right. for (i = 0; i < ln; i++) { card = cards[i]; // This means the items is within 2 cards of the active item if (this.isCardInRange(card)) { if (card.hidden) { card.show(); } el = card.el; style = el.dom.style; if (animate) { if (card === this.layout.activeItem) { el.on('webkitTransitionEnd', this.onTransitionEnd, this, {single: true}); } style.webkitTransitionDuration = '300ms'; } else { style.webkitTransitionDuration = '0ms'; } cardOffset = this.getCardOffset(card); if (this.horizontal) { Ext.Element.cssTransform(el, {translate: [cardOffset, 0]}); } else { Ext.Element.cssTransform(el, {translate: [0, cardOffset]}); } } else if (!card.hidden) { // All other items we position far away card.hide(); } } }, /** * Returns the amount of pixels from the current drag to a card. * @private */ getCardOffset : function(card) { var cardOffset = this.getCardIndexOffset(card), currentSize = this.currentSize, currentScroll = this.currentScroll; return this.horizontal ? (cardOffset * currentSize.width) + currentScroll.x : (cardOffset * currentSize.height) + currentScroll.y; }, /** * Returns the difference between the index of the active card and the passed card. * @private */ getCardIndexOffset : function(card) { return this.items.items.indexOf(card) - this.getActiveIndex(); }, /** * Returns true if the passed card is within 2 cards from the active card. * @private */ isCardInRange : function(card) { return Math.abs(this.getCardIndexOffset(card)) <= 2; },
/** * Returns the index of the currently active card. * @return {Number} The index of the currently active card. */ getActiveIndex : function() { return this.items.indexOf(this.layout.activeItem); }, /** * This determines if we are going to the next card, the previous card, or back to the active card. * @private */ onDragEnd : function(e, t) { var previousDelta, deltaOffset; if (this.horizontal) { deltaOffset = e.deltaX; previousDelta = e.previousDeltaX; } else { deltaOffset = e.deltaY; previousDelta = e.previousDeltaY; } // We have gone to the right if (deltaOffset < 0 && Math.abs(deltaOffset) > 3 && previousDelta <= 0 && this.layout.getNext()) { this.next(); } // We have gone to the left else if (deltaOffset > 0 && Math.abs(deltaOffset) > 3 && previousDelta >= 0 && this.layout.getPrev()) { this.prev(); } else { // drag back to current active card this.scrollToCard(this.layout.activeItem); } }, /** * Here we make sure that the card we are switching to is not translated * by the carousel anymore. This is only if we are switching card using * the setActiveItem of setActiveItem methods and thus customDrag is not set * to true. * @private */ onBeforeCardSwitch : function(newCard) { if (!this.customDrag && this.items.indexOf(newCard) != -1) { var style = newCard.el.dom.style; style.webkitTransitionDuration = null; style.webkitTransform = null; } return Ext.Carousel.superclass.onBeforeCardSwitch.apply(this, arguments); }, /** * This is an internal function that is called in onDragEnd that goes to * the next or previous card. * @private */ scrollToCard : function(newCard) { this.currentScroll = {x: 0, y: 0}; this.oldCard = this.layout.activeItem; if (newCard != this.oldCard && this.isCardInRange(newCard) && this.onBeforeCardSwitch(newCard, this.oldCard, this.items.indexOf(newCard), true) !== false) { this.layout.activeItem = newCard; if (this.horizontal) { this.currentScroll.x = -this.getCardOffset(newCard); } else { this.currentScroll.y = -this.getCardOffset(newCard); } } this.updateCardPositions(true); }, // @private onTransitionEnd : function(e, t) { this.customDrag = false; this.currentScroll = {x: 0, y: 0}; if (this.oldCard && this.layout.activeItem != this.oldCard) { this.onCardSwitch(this.layout.activeItem, this.oldCard, this.items.indexOf(this.layout.activeItem), true); } delete this.oldCard; }, /** * This function makes sure that all the cards are in correct locations * after a card switch * @private */ onCardSwitch : function(newCard, oldCard, index, animated) { this.currentScroll = {x: 0, y: 0}; this.updateCardPositions(); Ext.Carousel.superclass.onCardSwitch.apply(this, arguments); newCard.fireEvent('activate', newCard); },
/** * Switches the next card */ next: function() { var next = this.layout.getNext(); if (next) { this.customDrag = true; this.scrollToCard(next); } return this; },
/** * Switches the previous card */ prev: function() { var prev = this.layout.getPrev(); if (prev) { this.customDrag = true; this.scrollToCard(prev); } return this; },
/** * Method to determine whether this Sortable is currently disabled. * @return {Boolean} the disabled state of this Sortable. */ isVertical : function() { return this.vertical; },
/** * Method to determine whether this Sortable is currently sorting. * @return {Boolean} the sorting state of this Sortable. */ isHorizontal : function() { return this.horizontal; }, // private, inherit docs beforeDestroy: function(){ Ext.destroy(this.indicator); Ext.Carousel.superclass.beforeDestroy.call(this); } }); Ext.reg('carousel', Ext.Carousel);
/** * @class Ext.Carousel.Indicator * @extends Ext.Component * @xtype carouselindicator * @private * * A private utility class used by Ext.Carousel to create indicators. */ Ext.Carousel.Indicator = Ext.extend(Ext.Component, { baseCls: 'x-carousel-indicator', initComponent: function() { if (this.carousel.rendered) { this.render(this.carousel.body); this.onBeforeCardSwitch(null, null, this.carousel.items.indexOf(this.carousel.layout.getActiveItem())); } else { this.carousel.on('render', function() { this.render(this.carousel.body); }, this, {single: true}); } Ext.Carousel.Indicator.superclass.initComponent.call(this); }, // @private onRender: function() { Ext.Carousel.Indicator.superclass.onRender.apply(this, arguments); for (var i = 0, ln = this.carousel.items.length; i < ln; i++) { this.createIndicator(); } this.mon(this.carousel, { beforecardswitch: this.onBeforeCardSwitch, scope: this }); this.mon(this.el, { tap: this.onTap, scope: this }); this.el.addCls(this.baseCls + '-' + this.direction); }, // @private onTap: function(e, t) { var box = this.el.getPageBox(), centerX = box.left + (box.width / 2), centerY = box.top + (box.height / 2), carousel = this.carousel; if ((carousel.isHorizontal() && e.pageX > centerX) || (carousel.isVertical() && e.pageY > centerY)) { this.carousel.next(); } else { this.carousel.prev(); } }, // @private createIndicator: function() { this.indicators = this.indicators || []; this.indicators.push(this.el.createChild({ tag: 'span' })); }, // @private onBeforeCardSwitch: function(carousel, card, old, index) { if (Ext.isNumber(index) && index != -1 && this.indicators[index]) { this.indicators[index].radioCls('x-carousel-indicator-active'); } }, // @private onCardAdd: function() { if (this.rendered) { this.createIndicator(); } }, // @private onCardRemove: function() { if (this.rendered) { this.indicators.pop().remove(); } } }); Ext.reg('carouselindicator', Ext.Carousel.Indicator);