/** * Carousels, like [tabs](#!/guide/tabs), are a great way to allow the user to swipe through multiple full-screen pages. * A Carousel shows only one of its pages at a time but allows you to swipe through with your finger. * * Carousels can be oriented either horizontally or vertically and are easy to configure - they just work like any other * Container. Here's how to set up a simple horizontal Carousel: * * @example * Ext.create('Ext.Carousel', { * fullscreen: true, * * defaults: { * styleHtmlContent: true * }, * * items: [ * { * html : 'Item 1', * style: 'background-color: #5E99CC' * }, * { * html : 'Item 2', * style: 'background-color: #759E60' * }, * { * html : 'Item 3' * } * ] * }); * * We can also make Carousels orient themselves vertically: * * @example preview * Ext.create('Ext.Carousel', { * fullscreen: true, * direction: 'vertical', * * defaults: { * styleHtmlContent: true * }, * * items: [ * { * html : 'Item 1', * style: 'background-color: #759E60' * }, * { * html : 'Item 2', * style: 'background-color: #5E99CC' * } * ] * }); * * ### Common Configurations * * {@link #ui} defines the style of the carousel * * {@link #direction} defines the direction of the carousel * * {@link #indicator} defines if the indicator show be shown * * ### Useful Methods * * {@link #next} moves to the next card * * {@link #previous} moves to the previous card * * {@link #setActiveItem} moves to the passed card */Ext.define('Ext.carousel.Carousel', { extend: 'Ext.Container', alternateClassName: 'Ext.Carousel', xtype: 'carousel', requires: [ 'Ext.fx.easing.EaseOut', 'Ext.carousel.Item', 'Ext.carousel.Indicator', 'Ext.util.TranslatableGroup' ], config: { /** * @cfg layout * Hide layout config in Carousel. It only causes confusion. * @accessor * @private */ /** * @cfg * @inheritdoc */ baseCls: 'x-carousel', /** * @cfg {String} direction * The direction of the Carousel, either 'horizontal' or 'vertical'. * @accessor */ direction: 'horizontal', directionLock: false, animation: { duration: 250, easing: { type: 'ease-out' } }, /** * @cfg draggable * @hide */ /** * @cfg {Boolean} indicator * Provides an indicator while toggling between child items to let the user * know where they are in the card stack. * @accessor */ indicator: true, /** * @cfg {String} ui * Style options for Carousel. Default is 'dark'. 'light' is also available. * @accessor */ ui: 'dark', itemConfig: {}, bufferSize: 1, itemLength: null }, itemLength: 0, offset: 0, flickStartOffset: 0, flickStartTime: 0, dragDirection: 0, count: 0, painted: false, activeIndex: -1, beforeInitialize: function() { var me = this; me.element.on({ resize: 'onSizeChange', dragstart: 'onDragStart', drag: 'onDrag', dragend: 'onDragEnd', scope: me }); me.carouselItems = []; me.orderedCarouselItems = []; me.inactiveCarouselItems = []; me.hiddenTranslation = 0; }, updateBufferSize: function(size) { var ItemClass = Ext.carousel.Item, total = size * 2 + 1, isRendered = this.isRendered(), innerElement = this.innerElement, items = this.carouselItems, ln = items.length, itemConfig = this.getItemConfig(), itemLength = this.getItemLength(), direction = this.getDirection(), setterName = direction === 'horizontal' ? 'setWidth' : 'setHeight', i, item; for (i = ln; i < total; i++) { item = Ext.factory(itemConfig, ItemClass); if (itemLength) { item[setterName].call(item, itemLength); } item.setLayoutSizeFlags(this.LAYOUT_BOTH); items.push(item); innerElement.append(item.renderElement); if (isRendered && item.setRendered(true)) { item.fireEvent('renderedchange', this, item, true); } } this.getTranslatable().setActiveIndex(size); }, setRendered: function(rendered) { var wasRendered = this.rendered; if (rendered !== wasRendered) { this.rendered = rendered; var items = this.items.items, carouselItems = this.carouselItems, i, ln, item; for (i = 0,ln = items.length; i < ln; i++) { item = items[i]; if (!item.isInnerItem()) { item.setRendered(rendered); } } for (i = 0,ln = carouselItems.length; i < ln; i++) { carouselItems[i].setRendered(rendered); } return true; } return false; }, onSizeChange: function() { this.refreshSizing(); this.refreshCarouselItems(); this.refreshActiveItem(); }, onItemAdd: function(item, index) { this.callParent(arguments); var innerIndex = this.getInnerItems().indexOf(item), indicator = this.getIndicator(); if (indicator && item.isInnerItem()) { indicator.addIndicator(); } if (innerIndex <= this.getActiveIndex()) { this.refreshActiveIndex(); } if (this.isIndexDirty(innerIndex) && !this.isItemsInitializing) { this.refreshActiveItem(); } }, doItemLayoutAdd: function(item) { if (item.isInnerItem()) { return; } this.callParent(arguments); }, onItemRemove: function(item, index) { this.callParent(arguments); var innerIndex = this.getInnerItems().indexOf(item), indicator = this.getIndicator(), carouselItems = this.carouselItems, i, ln, carouselItem; if (item.isInnerItem() && indicator) { indicator.removeIndicator(); } if (innerIndex <= this.getActiveIndex()) { this.refreshActiveIndex(); } if (this.isIndexDirty(innerIndex)) { for (i = 0,ln = carouselItems.length; i < ln; i++) { carouselItem = carouselItems[i]; if (carouselItem.getComponent() === item) { carouselItem.setComponent(null); } } this.refreshActiveItem(); } }, doItemLayoutRemove: function(item) { if (item.isInnerItem()) { return; } this.callParent(arguments); }, onInnerItemMove: function(item, toIndex, fromIndex) { if ((this.isIndexDirty(toIndex) || this.isIndexDirty(fromIndex))) { this.refreshActiveItem(); } }, doItemLayoutMove: function(item) { if (item.isInnerItem()) { return; } this.callParent(arguments); }, isIndexDirty: function(index) { var activeIndex = this.getActiveIndex(), bufferSize = this.getBufferSize(); return (index >= activeIndex - bufferSize && index <= activeIndex + bufferSize); }, getTranslatable: function() { var me = this, translatable = me.translatable; if (!translatable) { me.translatable = translatable = new Ext.util.TranslatableGroup(); translatable.setItems(me.orderedCarouselItems); translatable.on('animationend', 'onAnimationEnd', me); } return translatable; }, onDragStart: function(e) { var direction = this.getDirection(), absDeltaX = e.absDeltaX, absDeltaY = e.absDeltaY, directionLock = this.getDirectionLock(); this.isDragging = true; if (directionLock) { if ((direction === 'horizontal' && absDeltaX > absDeltaY) || (direction === 'vertical' && absDeltaY > absDeltaX)) { e.stopPropagation(); } else { this.isDragging = false; return; } } this.getTranslatable().stopAnimation(); this.dragStartOffset = this.offset; this.dragDirection = 0; }, onDrag: function(e) { if (!this.isDragging) { return; } var startOffset = this.dragStartOffset, direction = this.getDirection(), delta = direction === 'horizontal' ? e.deltaX : e.deltaY, lastOffset = this.offset, flickStartTime = this.flickStartTime, dragDirection = this.dragDirection, now = Ext.Date.now(), currentActiveIndex = this.getActiveIndex(), maxIndex = this.getMaxItemIndex(), lastDragDirection = dragDirection, offset; if ((currentActiveIndex === 0 && delta > 0) || (currentActiveIndex === maxIndex && delta < 0)) { delta *= 0.5; } offset = startOffset + delta; if (offset > lastOffset) { dragDirection = 1; } else if (offset < lastOffset) { dragDirection = -1; } if (dragDirection !== lastDragDirection || (now - flickStartTime) > 300) { this.flickStartOffset = lastOffset; this.flickStartTime = now; } this.dragDirection = dragDirection; this.setOffset(offset); }, onDragEnd: function(e) { if (!this.isDragging) { return; } this.onDrag(e); this.isDragging = false; var now = Ext.Date.now(), itemLength = this.itemLength, threshold = itemLength / 2, offset = this.offset, activeIndex = this.getActiveIndex(), maxIndex = this.getMaxItemIndex(), animationDirection = 0, flickDistance = offset - this.flickStartOffset, flickDuration = now - this.flickStartTime, indicator = this.getIndicator(), velocity; if (flickDuration > 0 && Math.abs(flickDistance) >= 10) { velocity = flickDistance / flickDuration; if (Math.abs(velocity) >= 1) { if (velocity < 0 && activeIndex < maxIndex) { animationDirection = -1; } else if (velocity > 0 && activeIndex > 0) { animationDirection = 1; } } } if (animationDirection === 0) { if (activeIndex < maxIndex && offset < -threshold) { animationDirection = -1; } else if (activeIndex > 0 && offset > threshold) { animationDirection = 1; } } if (indicator) { indicator.setActiveIndex(activeIndex - animationDirection); } this.animationDirection = animationDirection; this.setOffsetAnimated(animationDirection * itemLength); }, applyAnimation: function(animation) { animation.easing = Ext.factory(animation.easing, Ext.fx.easing.EaseOut); return animation; }, updateDirection: function(direction) { var indicator = this.getIndicator(); this.currentAxis = (direction === 'horizontal') ? 'x' : 'y'; if (indicator) { indicator.setDirection(direction); } }, /** * @private * @chainable */ setOffset: function(offset) { this.offset = offset; if (Ext.isNumber(this.itemOffset)) { this.getTranslatable().translateAxis(this.currentAxis, offset + this.itemOffset); } return this; }, /** * @private * @return {Ext.carousel.Carousel} this * @chainable */ setOffsetAnimated: function(offset) { var indicator = this.getIndicator(); if (indicator) { indicator.setActiveIndex(this.getActiveIndex() - this.animationDirection); } this.offset = offset; this.getTranslatable().translateAxis(this.currentAxis, offset + this.itemOffset, this.getAnimation()); return this; }, onAnimationEnd: function(translatable) { var currentActiveIndex = this.getActiveIndex(), animationDirection = this.animationDirection, axis = this.currentAxis, currentOffset = translatable[axis], itemLength = this.itemLength, offset; if (animationDirection === -1) { offset = itemLength + currentOffset; } else if (animationDirection === 1) { offset = currentOffset - itemLength; } else { offset = currentOffset; } offset -= this.itemOffset; this.offset = offset; this.setActiveItem(currentActiveIndex - animationDirection); }, refresh: function() { this.refreshSizing(); this.refreshActiveItem(); }, refreshSizing: function() { var element = this.element, itemLength = this.getItemLength(), translatableItemLength = { x: 0, y: 0 }, itemOffset, containerSize; if (this.getDirection() === 'horizontal') { containerSize = element.getWidth(); } else { containerSize = element.getHeight(); } this.hiddenTranslation = -containerSize; if (itemLength === null) { itemLength = containerSize; itemOffset = 0; } else { itemOffset = (containerSize - itemLength) / 2; } this.itemLength = itemLength; this.itemOffset = itemOffset; translatableItemLength[this.currentAxis] = itemLength; this.getTranslatable().setItemLength(translatableItemLength); }, refreshOffset: function() { this.setOffset(this.offset); }, refreshActiveItem: function() { this.updateActiveItem(this.getActiveItem()); }, /** * Returns the index of the currently active card. * @return {Number} The index of the currently active card. */ getActiveIndex: function() { return this.activeIndex; }, refreshActiveIndex: function() { this.activeIndex = this.getInnerItemIndex(this.getActiveItem()); }, refreshCarouselItems: function() { var items = this.carouselItems, i, ln, item; for (i = 0,ln = items.length; i < ln; i++) { item = items[i]; item.getTranslatable().refresh(); } this.refreshInactiveCarouselItems(); }, refreshInactiveCarouselItems: function() { var items = this.inactiveCarouselItems, hiddenTranslation = this.hiddenTranslation, axis = this.currentAxis, i, ln, item; for (i = 0,ln = items.length; i < ln; i++) { item = items[i]; item.translateAxis(axis, hiddenTranslation); } }, /** * @private * @return {Number} */ getMaxItemIndex: function() { return this.innerItems.length - 1; }, /** * @private * @return {Number} */ getInnerItemIndex: function(item) { return this.innerItems.indexOf(item); }, /** * @private * @return {Object} */ getInnerItemAt: function(index) { return this.innerItems[index]; }, /** * @private * @return {Object} */ applyActiveItem: function() { var activeItem = this.callParent(arguments), activeIndex; if (activeItem) { activeIndex = this.getInnerItemIndex(activeItem); if (activeIndex !== -1) { this.activeIndex = activeIndex; return activeItem; } } }, updateActiveItem: function(activeItem) { var activeIndex = this.getActiveIndex(), maxIndex = this.getMaxItemIndex(), indicator = this.getIndicator(), bufferSize = this.getBufferSize(), carouselItems = this.carouselItems.slice(), orderedCarouselItems = this.orderedCarouselItems, visibleIndexes = {}, visibleItems = {}, visibleItem, component, id, i, index, ln, carouselItem; if (carouselItems.length === 0) { return; } this.callParent(arguments); orderedCarouselItems.length = 0; if (activeItem) { id = activeItem.getId(); visibleItems[id] = activeItem; visibleIndexes[id] = bufferSize; if (activeIndex > 0) { for (i = 1; i <= bufferSize; i++) { index = activeIndex - i; if (index >= 0) { visibleItem = this.getInnerItemAt(index); id = visibleItem.getId(); visibleItems[id] = visibleItem; visibleIndexes[id] = bufferSize - i; } else { break; } } } if (activeIndex < maxIndex) { for (i = 1; i <= bufferSize; i++) { index = activeIndex + i; if (index <= maxIndex) { visibleItem = this.getInnerItemAt(index); id = visibleItem.getId(); visibleItems[id] = visibleItem; visibleIndexes[id] = bufferSize + i; } else { break; } } } for (i = 0,ln = carouselItems.length; i < ln; i++) { carouselItem = carouselItems[i]; component = carouselItem.getComponent(); if (component) { id = component.getId(); if (visibleIndexes.hasOwnProperty(id)) { carouselItems.splice(i, 1); i--; ln--; delete visibleItems[id]; orderedCarouselItems[visibleIndexes[id]] = carouselItem; } } } for (id in visibleItems) { if (visibleItems.hasOwnProperty(id)) { visibleItem = visibleItems[id]; carouselItem = carouselItems.pop(); carouselItem.setComponent(visibleItem); orderedCarouselItems[visibleIndexes[id]] = carouselItem; } } } this.inactiveCarouselItems.length = 0; this.inactiveCarouselItems = carouselItems; this.refreshOffset(); this.refreshInactiveCarouselItems(); if (indicator) { indicator.setActiveIndex(activeIndex); } }, /** * Switches to the next card. * @return {Ext.carousel.Carousel} this * @chainable */ next: function() { this.setOffset(0); if (this.activeIndex === this.getMaxItemIndex()) { return this; } this.animationDirection = -1; this.setOffsetAnimated(-this.itemLength); return this; }, /** * Switches to the previous card. * @return {Ext.carousel.Carousel} this * @chainable */ previous: function() { this.setOffset(0); if (this.activeIndex === 0) { return this; } this.animationDirection = 1; this.setOffsetAnimated(this.itemLength); return this; }, /** * @private */ applyIndicator: function(indicator, currentIndicator) { return Ext.factory(indicator, Ext.carousel.Indicator, currentIndicator); }, /** * @private */ updateIndicator: function(indicator) { if (indicator) { this.insertFirst(indicator); indicator.setUi(this.getUi()); indicator.on({ next: 'next', previous: 'previous', scope: this }); } }, destroy: function() { var me = this, carouselItems = me.carouselItems.slice(); me.carouselItems.length = 0; Ext.destroy(carouselItems, me.getIndicator(), me.translatable); me.callParent(); delete me.carouselItems; }});