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