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 |