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