Home | History | Annotate | Download | only in touch_ntp
      1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 /**
      6  * @fileoverview Card slider implementation. Allows you to create interactions
      7  * that have items that can slide left to right to reveal additional items.
      8  * Works by adding the necessary event handlers to a specific DOM structure
      9  * including a frame, container and cards.
     10  * - The frame defines the boundary of one item. Each card will be expanded to
     11  *   fill the width of the frame. This element is also overflow hidden so that
     12  *   the additional items left / right do not trigger horizontal scrolling.
     13  * - The container is what all the touch events are attached to. This element
     14  *   will be expanded to be the width of all cards.
     15  * - The cards are the individual viewable items. There should be one card for
     16  *   each item in the list. Only one card will be visible at a time. Two cards
     17  *   will be visible while you are transitioning between cards.
     18  *
     19  * This class is designed to work well on any hardware-accelerated touch device.
     20  * It should still work on pre-hardware accelerated devices it just won't feel
     21  * very good. It should also work well with a mouse.
     22  */
     23 
     24 
     25 // Use an anonymous function to enable strict mode just for this file (which
     26 // will be concatenated with other files when embedded in Chrome
     27 var Slider = (function() {
     28   'use strict';
     29 
     30   /**
     31    * @constructor
     32    * @param {!Element} frame The bounding rectangle that cards are visible in.
     33    * @param {!Element} container The surrounding element that will have event
     34    *     listeners attached to it.
     35    * @param {!Array.<!Element>} cards The individual viewable cards.
     36    * @param {number} currentCard The index of the card that is currently
     37    *     visible.
     38    * @param {number} cardWidth The width of each card should have.
     39    */
     40   function Slider(frame, container, cards, currentCard, cardWidth) {
     41     /**
     42      * @type {!Element}
     43      * @private
     44      */
     45     this.frame_ = frame;
     46 
     47     /**
     48      * @type {!Element}
     49      * @private
     50      */
     51     this.container_ = container;
     52 
     53     /**
     54      * @type {!Array.<!Element>}
     55      * @private
     56      */
     57     this.cards_ = cards;
     58 
     59     /**
     60      * @type {number}
     61      * @private
     62      */
     63     this.currentCard_ = currentCard;
     64 
     65     /**
     66      * @type {number}
     67      * @private
     68      */
     69     this.cardWidth_ = cardWidth;
     70 
     71     /**
     72      * @type {!TouchHandler}
     73      * @private
     74      */
     75     this.touchHandler_ = new TouchHandler(this.container_);
     76   }
     77 
     78 
     79   /**
     80    * Events fired by the slider.
     81    * Events are fired at the container.
     82    */
     83   Slider.EventType = {
     84     // Fired when the user slides to another card.
     85     CARD_CHANGED: 'slider:card_changed'
     86   };
     87 
     88 
     89   /**
     90    * The time to transition between cards when animating. Measured in ms.
     91    * @type {number}
     92    * @private
     93    * @const
     94    */
     95   Slider.TRANSITION_TIME_ = 200;
     96 
     97 
     98   /**
     99    * The minimum velocity required to transition cards if they did not drag past
    100    * the halfway point between cards. Measured in pixels / ms.
    101    * @type {number}
    102    * @private
    103    * @const
    104    */
    105   Slider.TRANSITION_VELOCITY_THRESHOLD_ = 0.2;
    106 
    107 
    108   Slider.prototype = {
    109     /**
    110      * The current left offset of the container relative to the frame.
    111      * @type {number}
    112      * @private
    113      */
    114     currentLeft_: 0,
    115 
    116     /**
    117      * Initialize all elements and event handlers. Must call after construction
    118      * and before usage.
    119      */
    120     initialize: function() {
    121       var view = this.container_.ownerDocument.defaultView;
    122       assert(view.getComputedStyle(this.container_).display == '-webkit-box',
    123           'Container should be display -webkit-box.');
    124       assert(view.getComputedStyle(this.frame_).overflow == 'hidden',
    125           'Frame should be overflow hidden.');
    126       assert(view.getComputedStyle(this.container_).position == 'static',
    127           'Container should be position static.');
    128       for (var i = 0, card; card = this.cards_[i]; i++) {
    129         assert(view.getComputedStyle(card).position == 'static',
    130             'Cards should be position static.');
    131       }
    132 
    133       this.updateCardWidths_();
    134       this.transformToCurrentCard_();
    135 
    136       this.container_.addEventListener(TouchHandler.EventType.TOUCH_START,
    137                                        this.onTouchStart_.bind(this));
    138       this.container_.addEventListener(TouchHandler.EventType.DRAG_START,
    139                                        this.onDragStart_.bind(this));
    140       this.container_.addEventListener(TouchHandler.EventType.DRAG_MOVE,
    141                                        this.onDragMove_.bind(this));
    142       this.container_.addEventListener(TouchHandler.EventType.DRAG_END,
    143                                        this.onDragEnd_.bind(this));
    144 
    145       this.touchHandler_.enable(/* opt_capture */ false);
    146     },
    147 
    148     /**
    149      * Use in cases where the width of the frame has changed in order to update
    150      * the width of cards. For example should be used when orientation changes
    151      * in full width sliders.
    152      * @param {number} newCardWidth Width all cards should have, in pixels.
    153      */
    154     resize: function(newCardWidth) {
    155       if (newCardWidth != this.cardWidth_) {
    156         this.cardWidth_ = newCardWidth;
    157 
    158         this.updateCardWidths_();
    159 
    160         // Must upate the transform on the container to show the correct card.
    161         this.transformToCurrentCard_();
    162       }
    163     },
    164 
    165     /**
    166      * Sets the cards used. Can be called more than once to switch card sets.
    167      * @param {!Array.<!Element>} cards The individual viewable cards.
    168      * @param {number} index Index of the card to in the new set of cards to
    169      *     navigate to.
    170      */
    171     setCards: function(cards, index) {
    172       assert(index >= 0 && index < cards.length,
    173           'Invalid index in Slider#setCards');
    174       this.cards_ = cards;
    175 
    176       this.updateCardWidths_();
    177 
    178       // Jump to the given card index.
    179       this.selectCard(index);
    180     },
    181 
    182     /**
    183      * Updates the width of each card.
    184      * @private
    185      */
    186     updateCardWidths_: function() {
    187       for (var i = 0, card; card = this.cards_[i]; i++)
    188         card.style.width = this.cardWidth_ + 'px';
    189     },
    190 
    191     /**
    192      * Returns the index of the current card.
    193      * @return {number} index of the current card.
    194      */
    195     get currentCard() {
    196       return this.currentCard_;
    197     },
    198 
    199     /**
    200      * Clear any transition that is in progress and enable dragging for the
    201      * touch.
    202      * @param {!TouchHandler.Event} e The TouchHandler event.
    203      * @private
    204      */
    205     onTouchStart_: function(e) {
    206       this.container_.style.WebkitTransition = '';
    207       e.enableDrag = true;
    208     },
    209 
    210 
    211     /**
    212      * Tell the TouchHandler that dragging is acceptable when the user begins by
    213      * scrolling horizontally.
    214      * @param {!TouchHandler.Event} e The TouchHandler event.
    215      * @private
    216      */
    217     onDragStart_: function(e) {
    218       e.enableDrag = Math.abs(e.dragDeltaX) > Math.abs(e.dragDeltaY);
    219     },
    220 
    221     /**
    222      * On each drag move event reposition the container appropriately so the
    223      * cards look like they are sliding.
    224      * @param {!TouchHandler.Event} e The TouchHandler event.
    225      * @private
    226      */
    227     onDragMove_: function(e) {
    228       var deltaX = e.dragDeltaX;
    229       // If dragging beyond the first or last card then apply a backoff so the
    230       // dragging feels stickier than usual.
    231       if (!this.currentCard && deltaX > 0 ||
    232           this.currentCard == (this.cards_.length - 1) && deltaX < 0) {
    233         deltaX /= 2;
    234       }
    235       this.translateTo_(this.currentLeft_ + deltaX);
    236     },
    237 
    238     /**
    239      * Moves the view to the specified position.
    240      * @param {number} x Horizontal position to move to.
    241      * @private
    242      */
    243     translateTo_: function(x) {
    244       // We use a webkitTransform to slide because this is GPU accelerated on
    245       // Chrome and iOS.  Once Chrome does GPU acceleration on the position
    246       // fixed-layout elements we could simply set the element's position to
    247       // fixed and modify 'left' instead.
    248       this.container_.style.WebkitTransform = 'translate3d(' + x + 'px, 0, 0)';
    249     },
    250 
    251     /**
    252      * On drag end events we may want to transition to another card, depending
    253      * on the ending position of the drag and the velocity of the drag.
    254      * @param {!TouchHandler.Event} e The TouchHandler event.
    255      * @private
    256      */
    257     onDragEnd_: function(e) {
    258       var deltaX = e.dragDeltaX;
    259       var velocity = this.touchHandler_.getEndVelocity().x;
    260       var newX = this.currentLeft_ + deltaX;
    261       var newCardIndex = Math.round(-newX / this.cardWidth_);
    262 
    263       if (newCardIndex == this.currentCard && Math.abs(velocity) >
    264           Slider.TRANSITION_VELOCITY_THRESHOLD_) {
    265         // If the drag wasn't far enough to change cards but the velocity was
    266         // high enough to transition anyways. If the velocity is to the left
    267         // (negative) then the user wishes to go right (card +1).
    268         newCardIndex += velocity > 0 ? -1 : 1;
    269       }
    270 
    271       this.selectCard(newCardIndex, /* animate */ true);
    272     },
    273 
    274     /**
    275      * Cancel any current touch/slide as if we saw a touch end
    276      */
    277     cancelTouch: function() {
    278       // Stop listening to any current touch
    279       this.touchHandler_.cancelTouch();
    280 
    281       // Ensure we're at a card bounary
    282       this.transformToCurrentCard_(true);
    283     },
    284 
    285     /**
    286      * Selects a new card, ensuring that it is a valid index, transforming the
    287      * view and possibly calling the change card callback.
    288      * @param {number} newCardIndex Index of card to show.
    289      * @param {boolean=} opt_animate If true will animate transition from
    290      *     current position to new position.
    291      */
    292     selectCard: function(newCardIndex, opt_animate) {
    293       var isChangingCard = newCardIndex >= 0 &&
    294           newCardIndex < this.cards_.length &&
    295           newCardIndex != this.currentCard;
    296       if (isChangingCard) {
    297         // If we have a new card index and it is valid then update the left
    298         // position and current card index.
    299         this.currentCard_ = newCardIndex;
    300       }
    301 
    302       this.transformToCurrentCard_(opt_animate);
    303 
    304       if (isChangingCard) {
    305         var event = document.createEvent('Event');
    306         event.initEvent(Slider.EventType.CARD_CHANGED, true, true);
    307         event.slider = this;
    308         this.container_.dispatchEvent(event);
    309       }
    310     },
    311 
    312     /**
    313      * Centers the view on the card denoted by this.currentCard. Can either
    314      * animate to that card or snap to it.
    315      * @param {boolean=} opt_animate If true will animate transition from
    316      *     current position to new position.
    317      * @private
    318      */
    319     transformToCurrentCard_: function(opt_animate) {
    320       this.currentLeft_ = -this.currentCard * this.cardWidth_;
    321 
    322       // Animate to the current card, which will either transition if the
    323       // current card is new, or reset the existing card if we didn't drag
    324       // enough to change cards.
    325       var transition = '';
    326       if (opt_animate) {
    327         transition = '-webkit-transform ' + Slider.TRANSITION_TIME_ +
    328                      'ms ease-in-out';
    329       }
    330       this.container_.style.WebkitTransition = transition;
    331       this.translateTo_(this.currentLeft_);
    332     }
    333   };
    334 
    335   return Slider;
    336 })();
    337