OLD | NEW |
| (Empty) |
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 })(); | |
OLD | NEW |