[Aura] Initial app list webui.
[chromium-blink-merge.git] / chrome / browser / resources / shared / js / cr / ui / card_slider.js
blobb98aa661157859e9ff9813a62f97a17cbd50dff2
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.
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  */
24 // Use an anonymous function to enable strict mode just for this file (which
25 // will be concatenated with other files when embedded in Chrome
26 cr.define('cr.ui', function() {
27   'use strict';
29   /**
30    * @constructor
31    * @param {!Element} frame The bounding rectangle that cards are visible in.
32    * @param {!Element} container The surrounding element that will have event
33    *     listeners attached to it.
34    * @param {number} cardWidth The width of each card should have.
35    */
36   function CardSlider(frame, container, cardWidth) {
37     /**
38      * @type {!Element}
39      * @private
40      */
41     this.frame_ = frame;
43     /**
44      * @type {!Element}
45      * @private
46      */
47     this.container_ = container;
49     /**
50      * Array of card elements.
51      * @type {!Array.<!Element>}
52      * @private
53      */
54     this.cards_ = [];
56     /**
57      * Index of currently shown card.
58      * @type {number}
59      * @private
60      */
61     this.currentCard_ = 0;
63     /**
64      * @type {number}
65      * @private
66      */
67     this.cardWidth_ = cardWidth;
69     /**
70      * @type {!cr.ui.TouchHandler}
71      * @private
72      */
73     this.touchHandler_ = new cr.ui.TouchHandler(this.container_);
74   }
76   /**
77    * Events fired by the slider.
78    * Events are fired at the container.
79    */
80   CardSlider.EventType = {
81     // Fired when the user slides to another card.
82     CARD_CHANGED: 'cardSlider:card_changed'
83   };
86   /**
87    * The time to transition between cards when animating. Measured in ms.
88    * @type {number}
89    * @private
90    * @const
91    */
92   CardSlider.TRANSITION_TIME_ = 200;
95   /**
96    * The minimum velocity required to transition cards if they did not drag past
97    * the halfway point between cards. Measured in pixels / ms.
98    * @type {number}
99    * @private
100    * @const
101    */
102   CardSlider.TRANSITION_VELOCITY_THRESHOLD_ = 0.2;
105   CardSlider.prototype = {
106     /**
107      * The current left offset of the container relative to the frame.
108      * @type {number}
109      * @private
110      */
111     currentLeft_: 0,
113     /**
114      * Initialize all elements and event handlers. Must call after construction
115      * and before usage.
116      */
117     initialize: function() {
118       var view = this.container_.ownerDocument.defaultView;
119       assert(view.getComputedStyle(this.container_).display == '-webkit-box',
120           'Container should be display -webkit-box.');
121       assert(view.getComputedStyle(this.frame_).overflow == 'hidden',
122           'Frame should be overflow hidden.');
123       assert(view.getComputedStyle(this.container_).position == 'static',
124           'Container should be position static.');
126       this.updateCardWidths_();
128       this.mouseWheelScrollAmount_ = 0;
129       this.mouseWheelCardSelected_ = false;
130       this.mouseWheelIsContinuous_ = false;
131       this.scrollClearTimeout_ = null;
132       this.frame_.addEventListener('mousewheel',
133                                    this.onMouseWheel_.bind(this));
135       if (document.documentElement.getAttribute('touchui')) {
136         var TouchHandler = cr.ui.TouchHandler;
137         this.container_.addEventListener(TouchHandler.EventType.TOUCH_START,
138                                          this.onTouchStart_.bind(this));
139         this.container_.addEventListener(TouchHandler.EventType.DRAG_START,
140                                          this.onDragStart_.bind(this));
141         this.container_.addEventListener(TouchHandler.EventType.DRAG_MOVE,
142                                          this.onDragMove_.bind(this));
143         this.container_.addEventListener(TouchHandler.EventType.DRAG_END,
144                                          this.onDragEnd_.bind(this));
146         this.touchHandler_.enable(/* opt_capture */ false);
147       }
148     },
150     /**
151      * Use in cases where the width of the frame has changed in order to update
152      * the width of cards. For example should be used when orientation changes
153      * in full width sliders.
154      * @param {number} newCardWidth Width all cards should have, in pixels.
155      */
156     resize: function(newCardWidth) {
157       if (newCardWidth != this.cardWidth_) {
158         this.cardWidth_ = newCardWidth;
160         this.updateCardWidths_();
162         // Must upate the transform on the container to show the correct card.
163         this.transformToCurrentCard_();
164       }
165     },
167     /**
168      * Sets the cards used. Can be called more than once to switch card sets.
169      * @param {!Array.<!Element>} cards The individual viewable cards.
170      * @param {number} index Index of the card to in the new set of cards to
171      *     navigate to.
172      */
173     setCards: function(cards, index) {
174       assert(index >= 0 && index < cards.length,
175           'Invalid index in CardSlider#setCards');
176       this.cards_ = cards;
178       this.updateCardWidths_();
180       // Jump to the given card index.
181       this.selectCard(index);
182     },
184     /**
185      * Updates the width of each card.
186      * @private
187      */
188     updateCardWidths_: function() {
189       for (var i = 0, card; card = this.cards_[i]; i++)
190         card.style.width = this.cardWidth_ + 'px';
191     },
193     /**
194      * Returns the index of the current card.
195      * @return {number} index of the current card.
196      */
197     get currentCard() {
198       return this.currentCard_;
199     },
201     /**
202      * Allows setting the current card index.
203      * @param {number} index A new index to set the current index to.
204      * @return {number} The new index after having been set.
205      */
206     set currentCard(index) {
207       return (this.currentCard_ = index);
208     },
210     /**
211      * Returns the number of cards.
212      * @return {number} number of cards.
213      */
214     get cardCount() {
215       return this.cards_.length;
216     },
218     /**
219      * Returns the current card itself.
220      * @return {!Element} the currently shown card.
221      */
222     get currentCardValue() {
223       return this.cards_[this.currentCard_];
224     },
226     /**
227      * Handle horizontal scrolls to flip between pages.
228      * @private
229      */
230     onMouseWheel_: function(e) {
231       if (e.wheelDeltaX == 0)
232         return;
234       // Prevent OS X 10.7+ history swiping on the NTP.
235       e.preventDefault();
237       // Continuous devices such as an Apple Touchpad or Apple MagicMouse will
238       // send arbitrary delta values. Conversly, standard mousewheels will
239       // send delta values in increments of 120.  (There is of course a small
240       // chance we mistake a continuous device for a non-continuous device.
241       // Unfortunately there isn't a better way to do this until real touch
242       // events are available to desktop clients.)
243       var DISCRETE_DELTA = 120;
244       if (e.wheelDeltaX % DISCRETE_DELTA)
245         this.mouseWheelIsContinuous_ = true;
247       if (this.mouseWheelIsContinuous_) {
248         // For continuous devices, detect a page swipe when the accumulated
249         // delta matches a pre-defined threshhold.  After changing the page,
250         // ignore wheel events for a short time before repeating this process.
251         if (this.mouseWheelCardSelected_) return;
252         this.mouseWheelScrollAmount_ += e.wheelDeltaX;
253         if (Math.abs(this.mouseWheelScrollAmount_) >= 600) {
254           var pagesToScroll = this.mouseWheelScrollAmount_ > 0 ? 1 : -1;
255           if (!isRTL())
256             pagesToScroll *= -1;
257           var newCardIndex = this.currentCard + pagesToScroll;
258           newCardIndex = Math.min(this.cards_.length - 1,
259                                   Math.max(0, newCardIndex));
260           this.selectCard(newCardIndex, true);
261           this.mouseWheelCardSelected_ = true;
262         }
263       } else {
264         // For discrete devices, consider each wheel tick a page change.
265         var pagesToScroll = e.wheelDeltaX / DISCRETE_DELTA;
266         if (!isRTL())
267           pagesToScroll *= -1;
268         var newCardIndex = this.currentCard + pagesToScroll;
269         newCardIndex = Math.min(this.cards_.length - 1,
270                                 Math.max(0, newCardIndex));
271         this.selectCard(newCardIndex, true);
272       }
274       // We got a mouse wheel event, so cancel any pending scroll wheel timeout.
275       if (this.scrollClearTimeout_ != null)
276         clearTimeout(this.scrollClearTimeout_);
277       // If we didn't use up all the scroll, hold onto it for a little bit, but
278       // drop it after a delay.
279       if (this.mouseWheelScrollAmount_ != 0) {
280         this.scrollClearTimeout_ =
281             setTimeout(this.clearMouseWheelScroll_.bind(this), 500);
282       }
283     },
285     /**
286      * Resets the amount of horizontal scroll we've seen to 0. See
287      * onMouseWheel_.
288      * @private
289      */
290     clearMouseWheelScroll_: function() {
291       this.mouseWheelScrollAmount_ = 0;
292       this.mouseWheelCardSelected_ = false;
293     },
295     /**
296      * Selects a new card, ensuring that it is a valid index, transforming the
297      * view and possibly calling the change card callback.
298      * @param {number} newCardIndex Index of card to show.
299      * @param {boolean=} opt_animate If true will animate transition from
300      *     current position to new position.
301      */
302     selectCard: function(newCardIndex, opt_animate) {
303       var previousCard = this.currentCardValue;
305       var isChangingCard =
306           !this.cards_[newCardIndex].classList.contains('selected-card');
308       if (isChangingCard) {
309         previousCard.classList.remove('selected-card');
310         // If we have a new card index and it is valid then update the left
311         // position and current card index.
312         this.currentCard_ = newCardIndex;
313         this.currentCardValue.classList.add('selected-card');
314       }
316       this.transformToCurrentCard_(opt_animate);
318       if (isChangingCard) {
319         var event = document.createEvent('Event');
320         event.initEvent(CardSlider.EventType.CARD_CHANGED, true, true);
321         event.cardSlider = this;
322         this.container_.dispatchEvent(event);
324         // We also dispatch an event on the cards themselves.
325         if (previousCard) {
326           cr.dispatchSimpleEvent(previousCard, 'carddeselected',
327                                  true, true);
328         }
329         cr.dispatchSimpleEvent(this.currentCardValue, 'cardselected',
330                                true, true);
331       }
332     },
334     /**
335      * Selects a card from the stack. Passes through to selectCard.
336      * @param {Node} newCard The card that should be selected.
337      * @param {boolean=} opt_animate Whether to animate.
338      */
339     selectCardByValue: function(newCard, opt_animate) {
340       var i = this.cards_.indexOf(newCard);
341       assert(i != -1);
342       this.selectCard(i, opt_animate);
343     },
345     /**
346      * Centers the view on the card denoted by this.currentCard. Can either
347      * animate to that card or snap to it.
348      * @param {boolean=} opt_animate If true will animate transition from
349      *     current position to new position.
350      * @private
351      */
352     transformToCurrentCard_: function(opt_animate) {
353       this.currentLeft_ = -this.cardWidth_ *
354           (isRTL() ? this.cards_.length - this.currentCard - 1 :
355                      this.currentCard);
357       // Animate to the current card, which will either transition if the
358       // current card is new, or reset the existing card if we didn't drag
359       // enough to change cards.
360       var transition = '';
361       if (opt_animate) {
362         transition = '-webkit-transform ' + CardSlider.TRANSITION_TIME_ +
363                      'ms ease-in-out';
364       }
365       this.container_.style.WebkitTransition = transition;
366       this.translateTo_(this.currentLeft_);
367     },
369     /**
370      * Moves the view to the specified position.
371      * @param {number} x Horizontal position to move to.
372      * @private
373      */
374     translateTo_: function(x) {
375       // We use a webkitTransform to slide because this is GPU accelerated on
376       // Chrome and iOS.  Once Chrome does GPU acceleration on the position
377       // fixed-layout elements we could simply set the element's position to
378       // fixed and modify 'left' instead.
379       this.container_.style.WebkitTransform = 'translate3d(' + x + 'px, 0, 0)';
380     },
382     /* Touch ******************************************************************/
384     /**
385      * Clear any transition that is in progress and enable dragging for the
386      * touch.
387      * @param {!cr.ui.TouchHandler.Event} e The TouchHandler event.
388      * @private
389      */
390     onTouchStart_: function(e) {
391       this.container_.style.WebkitTransition = '';
392       e.enableDrag = true;
393     },
395     /**
396      * Tell the TouchHandler that dragging is acceptable when the user begins by
397      * scrolling horizontally.
398      * @param {!cr.ui.TouchHandler.Event} e The TouchHandler event.
399      * @private
400      */
401     onDragStart_: function(e) {
402       e.enableDrag = Math.abs(e.dragDeltaX) > Math.abs(e.dragDeltaY);
403     },
405     /**
406      * On each drag move event reposition the container appropriately so the
407      * cards look like they are sliding.
408      * @param {!cr.ui.TouchHandler.Event} e The TouchHandler event.
409      * @private
410      */
411     onDragMove_: function(e) {
412       var deltaX = e.dragDeltaX;
413       // If dragging beyond the first or last card then apply a backoff so the
414       // dragging feels stickier than usual.
415       if (!this.currentCard && deltaX > 0 ||
416           this.currentCard == (this.cards_.length - 1) && deltaX < 0) {
417         deltaX /= 2;
418       }
419       this.translateTo_(this.currentLeft_ + deltaX);
420     },
422     /**
423      * On drag end events we may want to transition to another card, depending
424      * on the ending position of the drag and the velocity of the drag.
425      * @param {!cr.ui.TouchHandler.Event} e The TouchHandler event.
426      * @private
427      */
428     onDragEnd_: function(e) {
429       var deltaX = e.dragDeltaX;
430       var velocity = this.touchHandler_.getEndVelocity().x;
431       var newX = this.currentLeft_ + deltaX;
432       var newCardIndex = Math.round(-newX / this.cardWidth_);
434       if (newCardIndex == this.currentCard && Math.abs(velocity) >
435           CardSlider.TRANSITION_VELOCITY_THRESHOLD_) {
436         // If the drag wasn't far enough to change cards but the velocity was
437         // high enough to transition anyways. If the velocity is to the left
438         // (negative) then the user wishes to go right (card +1).
439         newCardIndex += velocity > 0 ? -1 : 1;
440       }
442       this.selectCard(newCardIndex, /* animate */ true);
443     },
445     /**
446      * Cancel any current touch/slide as if we saw a touch end
447      */
448     cancelTouch: function() {
449       // Stop listening to any current touch
450       this.touchHandler_.cancelTouch();
452       // Ensure we're at a card bounary
453       this.transformToCurrentCard_(true);
454     },
455   };
457   return {
458     CardSlider: CardSlider
459   };