UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

734 lines (588 loc) 22.4 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2004-2012 1&1 Internet AG, Germany, http://www.1und1.de License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * Christopher Zuendorf (czuendorf) ************************************************************************ */ /** * Creates a Carousel widget. * A carousel is a widget which can switch between several sub pages {@link qx.ui.mobile.container.Composite}. * A page switch is triggered by a swipe to left, for next page, or a swipe to right for * previous page. * * A carousel shows by default a pagination indicator at the bottom of the carousel. * This pagination indicator can be hidden by property <code>showPagination</code>. * * *Example* * * Here is a little example of how to use the widget. * * <pre class='javascript'> * * var carousel = new qx.ui.mobile.container.Carousel(); * var carouselPage1 = new qx.ui.mobile.container.Composite(); * var carouselPage2 = new qx.ui.mobile.container.Composite(); * * carouselPage1.add(new qx.ui.mobile.basic.Label("This is a carousel. Please swipe left.")); * carouselPage2.add(new qx.ui.mobile.basic.Label("Now swipe right.")); * * carousel.add(carouselPage1); * carousel.add(carouselPage2); * </pre> * */ qx.Class.define("qx.ui.mobile.container.Carousel", { extend : qx.ui.mobile.container.Composite, include : qx.ui.mobile.core.MResize, /* ***************************************************************************** CONSTRUCTOR ***************************************************************************** */ /** * @param transitionDuration {Integer ? 0.4} transition duration on carouselPage change in seconds. */ construct : function(transitionDuration) { this.base(arguments); if (transitionDuration) { this.setTransitionDuration(transitionDuration); } this.__snapPointsX = []; this.__onMoveOffset = [0, 0]; this.__lastOffset = [0, 0]; this.__boundsX = [0, 0]; this.__pages = []; this.__paginationLabels = []; var carouselScroller = this.__carouselScroller = new qx.ui.mobile.container.Composite(new qx.ui.mobile.layout.HBox()); carouselScroller.setTransformUnit("px"); carouselScroller.addCssClass("carousel-scroller"); carouselScroller.addListener("pointerdown", this._onPointerDown, this); carouselScroller.addListener("pointerup", this._onPointerUp, this); carouselScroller.addListener("track", this._onTrack, this); carouselScroller.addListener("swipe", this._onSwipe, this); this.addListener("touchmove", qx.bom.Event.preventDefault, this); this.addListener("appear", this._onContainerUpdate, this); qx.event.Registration.addListener(this.__carouselScroller.getContainerElement(),"transitionEnd",this._onScrollerTransitionEnd, this); qx.event.Registration.addListener(window, "orientationchange", this._onContainerUpdate, this); qx.event.Registration.addListener(window, "resize", this._onContainerUpdate, this); qx.event.Registration.addListener(this.getContentElement(), "scroll", this._onNativeScroll, this); var pagination = this.__pagination = new qx.ui.mobile.container.Composite(); pagination.setLayout(new qx.ui.mobile.layout.HBox()); pagination.setTransformUnit("px"); pagination.addCssClass("carousel-pagination"); this.setLayout(new qx.ui.mobile.layout.VBox()); this._add(carouselScroller, { flex: 1 }); this._add(pagination, { flex: 1 }); }, /* ***************************************************************************** PROPERTIES ***************************************************************************** */ properties : { // overridden defaultCssClass : { refine : true, init : "carousel" }, /** Property for setting visibility of pagination indicator */ showPagination : { check : "Boolean", init : true, apply : "_applyShowPagination" }, /** Defines whether the carousel should scroll back to first or last page * when the start/end of carousel pages is reached */ scrollLoop : { check : "Boolean", init : true }, /** * Defines the height of the carousel. If value is equal to <code>null</code> * the height is set to <code>100%</code>. */ height : { check : "Number", init : 200, nullable : true, apply : "_updateCarouselLayout" }, /** * The current visible page index. */ currentIndex : { check : "Number", init : 0, apply : "_scrollToPage", event : "changeCurrentIndex" }, /** * Duration of the carousel page transition. */ transitionDuration : { check : "Number", init : 0.5 } }, /* ***************************************************************************** MEMBERS ***************************************************************************** */ members : { __carouselScroller : null, __carouselScrollerWidth : null, __carouselWidth : null, __paginationLabels : null, __pagination : null, __snapPointsX : null, __onMoveOffset : null, __lastOffset : null, __boundsX : null, __pages : null, __showTransition : null, __isPageScrollTarget : null, __deltaX : null, __deltaY : null, // overridden /** * Adds a page to the end of the carousel. * @param page {qx.ui.mobile.container.Composite} The composite which should be added as a page to the end of carousel. */ add : function(page) { if (qx.core.Environment.get("qx.debug")) { if (!page instanceof qx.ui.mobile.container.Composite) { throw new Error("Page is expected to be an instance of qx.ui.mobile.container.Composite."); } } page.addCssClass("carousel-page"); this.__pages.push(page); this.__carouselScroller.add(page, { flex: 1 }); var paginationLabel = this._createPaginationLabel(); this.__paginationLabels.push(paginationLabel); this.__pagination.add(paginationLabel); this._setTransitionDuration(0); this._onContainerUpdate(); }, /** * Removes a carousel page from carousel identified by its index. * @param pageIndex {Integer} The page index which should be removed from carousel. * @return {qx.ui.mobile.container.Composite} the page which was removed from carousel. */ removePageByIndex : function(pageIndex) { if (this.__pages && this.__pages.length > pageIndex) { if (pageIndex <= this.getCurrentIndex() && this.getCurrentIndex() !== 0) { this.setCurrentIndex(this.getCurrentIndex() - 1); } var targetPage = this.__pages[pageIndex]; var paginationLabel = this.__paginationLabels[pageIndex]; this.__carouselScroller.remove(targetPage); this.__pagination.remove(paginationLabel); paginationLabel.removeListener("tap", this._onPaginationLabelTap, { self: this, targetIndex: pageIndex - 1 }); qx.util.DisposeUtil.destroyContainer(paginationLabel); this.__pages.splice(pageIndex, 1); this.__paginationLabels.splice(pageIndex, 1); this._onContainerUpdate(); return targetPage; } }, // overridden removeAll : function() { var removedPages = []; if (this.__pages) { for (var i = this.__pages.length - 1; i >= 0; i--) { removedPages.push(this.removePageByIndex(i)); } } return removedPages; }, /** * Scrolls the carousel to next page. */ nextPage : function() { if (this.getCurrentIndex() == this.__pages.length - 1) { if (this.isScrollLoop() && this.__pages.length > 1) { this._doScrollLoop(); } } else { this.setCurrentIndex(this.getCurrentIndex() + 1); } }, /** * Scrolls the carousel to previous page. */ previousPage : function() { if (this.getCurrentIndex() === 0) { if (this.isScrollLoop() && this.__pages.length > 1) { this._doScrollLoop(); } } else { this.setCurrentIndex(this.getCurrentIndex() - 1); } }, /** * Returns the current page count of this carousel. * @return {Integer} the current page count */ getPageCount : function() { if(this.__pages) { return this.__pages.length; } return 0; }, /** * Scrolls the carousel to the page with the given pageIndex. * @param pageIndex {Integer} the target page index, which should be visible * @param showTransition {Boolean ? true} flag if a transition should be shown or not */ _scrollToPage : function(pageIndex, showTransition) { if (pageIndex >= this.__pages.length || pageIndex < 0) { return; } this._updatePagination(pageIndex); var snapPoint = -pageIndex * this.__carouselWidth; this._updateScrollerPosition(snapPoint); // Update lastOffset, because snapPoint has changed. this.__lastOffset[0] = snapPoint; }, /** * Manages the the scroll loop. First fades out carousel scroller >> * waits till fading is done >> scrolls to pageIndex >> waits till scrolling is done * >> fades scroller in. */ _doScrollLoop : function() { this._setTransitionDuration(this.getTransitionDuration()); setTimeout(function() { this._setScrollersOpacity(0); }.bind(this), 0); }, /** * Event handler for <code>transitionEnd</code> event on carouselScroller. */ _onScrollerTransitionEnd : function() { var opacity = qx.bom.element.Style.get(this.__carouselScroller.getContainerElement(), "opacity"); if (opacity === 0) { var pageIndex = null; if (this.getCurrentIndex() == this.__pages.length - 1) { pageIndex = 0; } if (this.getCurrentIndex() === 0) { pageIndex = this.__pages.length - 1; } this._setTransitionDuration(0); this.setCurrentIndex(pageIndex); setTimeout(function() { this._setTransitionDuration(this.getTransitionDuration()); this._setScrollersOpacity(1); }.bind(this), 0); } }, /** * Factory method for a paginationLabel. * @return {qx.ui.mobile.container.Composite} the created pagination label. */ _createPaginationLabel : function() { var paginationIndex = this.__pages.length; var paginationLabel = new qx.ui.mobile.container.Composite(); var paginationLabelText = new qx.ui.mobile.basic.Label("" + paginationIndex); paginationLabel.add(paginationLabelText); paginationLabel.addCssClass("carousel-pagination-label"); paginationLabel.addListener("tap", this._onPaginationLabelTap, { self: this, targetIndex: paginationIndex - 1 }); return paginationLabel; }, /** * Changes the opacity of the carouselScroller element. * @param opacity {Integer} the target value of the opacity. */ _setScrollersOpacity : function(opacity) { if (this.__carouselScroller) { qx.bom.element.Style.set(this.__carouselScroller.getContainerElement(), "opacity", opacity); } }, /** * Called when showPagination property is changed. * Manages <code>show()</code> and <code>hide()</code> of pagination container. */ _applyShowPagination : function(value, old) { if (value) { if (this.__pages.length > 1) { this.__pagination.show(); } } else { this.__pagination.hide(); } }, /** * Handles a tap on paginationLabel. */ _onPaginationLabelTap : function() { this.self.setCurrentIndex(this.targetIndex); }, /** * Updates the layout of the carousel the carousel scroller and its pages. */ _updateCarouselLayout : function() { if (!this.getContainerElement()) { return; } var carouselSize = qx.bom.element.Dimension.getSize(this.getContainerElement()); this.__carouselWidth = carouselSize.width; if (this.getHeight() !== null) { this._setStyle("height", this.getHeight() / 16 + "rem"); } else { this._setStyle("height", "100%"); } qx.bom.element.Style.set(this.__carouselScroller.getContentElement(), "width", this.__pages.length * carouselSize.width + "px"); for (var i = 0; i < this.__pages.length; i++) { var pageContentElement = this.__pages[i].getContentElement(); qx.bom.element.Style.set(pageContentElement, "width", carouselSize.width + "px"); qx.bom.element.Style.set(pageContentElement, "height", carouselSize.height + "px"); } if (this.__pages.length == 1) { this.__pagination.exclude(); } else { if (this.isShowPagination()) { this.__pagination.show(); } } this._refreshScrollerPosition(); }, /** * Synchronizes the positions of the scroller to the current shown page index. */ _refreshScrollerPosition : function() { this.__carouselScrollerWidth = qx.bom.element.Dimension.getWidth(this.__carouselScroller.getContentElement()); this._scrollToPage(this.getCurrentIndex()); }, /** * Handles window resize, device orientatonChange or page appear events. */ _onContainerUpdate : function() { this._setTransitionDuration(0); this._updateCarouselLayout(); this._refreshScrollerPosition(); }, /** * Returns the current horizontal position of the carousel scrolling container. * @return {Number} the horizontal position */ _getScrollerOffset : function() { var transformMatrix = qx.bom.element.Style.get(this.__carouselScroller.getContentElement(), "transform"); var transformValueArray = transformMatrix.substr(7, transformMatrix.length - 8).split(', '); var i = 4; // Check if MSCSSMatrix is used. if('MSCSSMatrix' in window && !('WebKitCSSMatrix' in window)) { i = transformValueArray.length - 4; } return Math.floor(parseInt(transformValueArray[i], 10)); }, /** * Event handler for <code>pointerdown</code> events. * @param evt {qx.event.type.Pointer} The pointer event. */ _onPointerDown : function(evt) { if(!evt.isPrimary()) { return; } this.__lastOffset[0] = this._getScrollerOffset(); this.__isPageScrollTarget = null; this.__boundsX[0] = -this.__carouselScrollerWidth + this.__carouselWidth; this._updateScrollerPosition(this.__lastOffset[0]); }, /** * Event handler for <code>track</code> events. * @param evt {qx.event.type.Track} The track event. */ _onTrack : function(evt) { if(!evt.isPrimary()) { return; } this._setTransitionDuration(0); this.__deltaX = evt.getDelta().x; this.__deltaY = evt.getDelta().y; if (this.__isPageScrollTarget === null) { this.__isPageScrollTarget = (evt.getDelta().axis == "y"); } if (!this.__isPageScrollTarget) { this.__onMoveOffset[0] = Math.floor(this.__deltaX + this.__lastOffset[0]); if (this.__onMoveOffset[0] >= this.__boundsX[1]) { this.__onMoveOffset[0] = this.__boundsX[1]; } if (this.__onMoveOffset[0] <= this.__boundsX[0]) { this.__onMoveOffset[0] = this.__boundsX[0]; } this._updateScrollerPosition(this.__onMoveOffset[0]); evt.preventDefault(); } }, /** * Handler for <code>pointerup</code> event on carousel scroller. * @param evt {qx.event.type.Pointer} the pointerup event. */ _onPointerUp : function(evt) { if(!evt.isPrimary()) { return; } this._setTransitionDuration(this.getTransitionDuration()); this._refreshScrollerPosition(); }, /** * Handler for swipe event on carousel scroller. * @param evt {qx.event.type.Swipe} The swipe event. */ _onSwipe : function(evt) { if(!evt.isPrimary()) { return; } if (evt.getDuration() < 750 && Math.abs(evt.getDistance()) > 50) { var duration = this._calculateTransitionDuration(this.__deltaX, evt.getDuration()); duration = Math.min(this.getTransitionDuration(),duration); this._setTransitionDuration(duration); if (evt.getDirection() == "left") { this.nextPage(); } else if (evt.getDirection() == "right") { this.previousPage(); } } else { this._snapCarouselPage(); } }, /** * Calculates the duration the transition will need till the next carousel * snap point is reached. * @param deltaX {Integer} the distance on axis between pointerdown and pointerup. * @param duration {Number} the swipe duration. * @return {Number} the transition duration. */ _calculateTransitionDuration : function(deltaX, duration) { var distanceX = this.__carouselWidth - Math.abs(deltaX); var transitionDuration = (distanceX / Math.abs(deltaX)) * duration; return (transitionDuration / 1000); }, /** * Handles the native scroll event on the carousel container. * This is needed for preventing "scrollIntoView" method. * * @param evt {qx.event.type.Native} the native scroll event. */ _onNativeScroll : function(evt) { var nativeEvent = evt.getNativeEvent(); nativeEvent.srcElement.scrollLeft = 0; nativeEvent.srcElement.scrollTop = 0; }, /** * Applies the CSS property "transitionDuration" to the carouselScroller. * @param value {Number} the target value of the transitionDuration. */ _setTransitionDuration : function(value) { qx.bom.element.Style.set(this.__carouselScroller.getContentElement(), "transitionDuration", value+"s"); }, /** * Snaps carouselScroller offset to a carouselPage. * It determines which carouselPage is the nearest and moves * carouselScrollers offset till nearest carouselPage's left border is aligned to carousel's left border. */ _snapCarouselPage : function() { this._setTransitionDuration(this.getTransitionDuration()); var leastDistance = 10000; var nearestPageIndex = 0; // Determine nearest snapPoint. for (var i = 0; i < this.__pages.length; i++) { var snapPoint = -i * this.__carouselWidth; var distance = this.__onMoveOffset[0] - snapPoint; if (Math.abs(distance) < leastDistance) { leastDistance = Math.abs(distance); nearestPageIndex = i; } } if (this.getCurrentIndex() == nearestPageIndex) { this._refreshScrollerPosition(); } else { this.setCurrentIndex(nearestPageIndex); } }, /** * Updates the pagination indicator of this carousel. * Removes the active state from from paginationLabel with oldActiveIndex, * Adds actives state to paginationLabel new ActiveIndex. * @param newActiveIndex {Integer} Index of paginationLabel which should have active state */ _updatePagination : function(newActiveIndex) { for (var i = 0; i < this.__paginationLabels.length; i++) { this.__paginationLabels[i].removeCssClass("active"); } var newActiveLabel = this.__paginationLabels[newActiveIndex]; if (newActiveLabel && newActiveLabel.getContainerElement()) { newActiveLabel.addCssClass("active"); } if (this.__paginationLabels.length) { var paginationStyle = getComputedStyle(this.__pagination.getContentElement()); var paginationWidth = parseFloat(paginationStyle.width,10); if(isNaN(paginationWidth)) { return; } var paginationLabelWidth = paginationWidth/this.__paginationLabels.length; var left = null; var translate = (this.__carouselWidth / 2) - newActiveIndex * paginationLabelWidth - paginationLabelWidth / 2; if (paginationWidth < this.__carouselWidth) { left = this.__carouselWidth / 2 - paginationWidth / 2 + "px"; translate = 0; } qx.bom.element.Style.set(this.__pagination.getContentElement(), "left", left); this.__pagination.setTranslateX(translate); } }, /** * Assign new position of carousel scrolling container. * @param x {Integer} scroller's x position. */ _updateScrollerPosition : function(x) { if(isNaN(x) || this.__carouselScroller.getContentElement() === null) { return; } this.__carouselScroller.setTranslateX(x); }, /** * Remove all listeners. */ _removeListeners : function() { this.__carouselScroller.removeListener("pointerdown", this._onPointerDown, this); this.__carouselScroller.removeListener("track", this._onTrack, this); this.__carouselScroller.removeListener("pointerup", this._onPointerUp, this); this.__carouselScroller.removeListener("swipe", this._onSwipe, this); this.__carouselScroller.removeListener("touchmove", qx.bom.Event.preventDefault, this); this.removeListener("appear", this._onContainerUpdate, this); qx.event.Registration.removeListener(window, "orientationchange", this._onContainerUpdate, this); qx.event.Registration.removeListener(window, "resize", this._onContainerUpdate, this); qx.event.Registration.removeListener(this.getContentElement(), "scroll", this._onNativeScroll, this); } }, destruct : function() { this._removeListeners(); this._disposeObjects("__carouselScroller"," __pagination"); qx.util.DisposeUtil.destroyContainer(this); qx.util.DisposeUtil.disposeArray(this,"__paginationLabels"); this.__pages = this.__paginationLabels = this.__snapPointsX = this.__onMoveOffset = this.__lastOffset = this.__boundsX = this.__isPageScrollTarget = null; } });