OLD | NEW |
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 /** | 5 /** |
6 * @fileoverview Card slider implementation. Allows you to create interactions | 6 * @fileoverview Card slider implementation. Allows you to create interactions |
7 * that have items that can slide left to right to reveal additional items. | 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 | 8 * Works by adding the necessary event handlers to a specific DOM structure |
9 * including a frame, container and cards. | 9 * including a frame, container and cards. |
10 * - The frame defines the boundary of one item. Each card will be expanded to | 10 * - The frame defines the boundary of one item. Each card will be expanded to |
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
51 * @type {!Array.<!Element>} | 51 * @type {!Array.<!Element>} |
52 * @private | 52 * @private |
53 */ | 53 */ |
54 this.cards_ = []; | 54 this.cards_ = []; |
55 | 55 |
56 /** | 56 /** |
57 * Index of currently shown card. | 57 * Index of currently shown card. |
58 * @type {number} | 58 * @type {number} |
59 * @private | 59 * @private |
60 */ | 60 */ |
61 this.currentCard_ = 0; | 61 this.currentCard_ = -1; |
62 | 62 |
63 /** | 63 /** |
64 * @type {number} | 64 * @type {number} |
65 * @private | 65 * @private |
66 */ | 66 */ |
67 this.cardWidth_ = cardWidth; | 67 this.cardWidth_ = cardWidth; |
68 | 68 |
69 /** | 69 /** |
70 * @type {!cr.ui.TouchHandler} | 70 * @type {!cr.ui.TouchHandler} |
71 * @private | 71 * @private |
72 */ | 72 */ |
73 this.touchHandler_ = new cr.ui.TouchHandler(this.container_); | 73 this.touchHandler_ = new cr.ui.TouchHandler(this.container_); |
74 } | 74 } |
75 | 75 |
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 }; | |
84 | |
85 | 76 |
86 /** | 77 /** |
87 * The time to transition between cards when animating. Measured in ms. | 78 * The time to transition between cards when animating. Measured in ms. |
88 * @type {number} | 79 * @type {number} |
89 * @private | 80 * @private |
90 * @const | 81 * @const |
91 */ | 82 */ |
92 CardSlider.TRANSITION_TIME_ = 200; | 83 CardSlider.TRANSITION_TIME_ = 200; |
93 | 84 |
94 | 85 |
(...skipping 30 matching lines...) Expand all Loading... |
125 | 116 |
126 this.updateCardWidths_(); | 117 this.updateCardWidths_(); |
127 | 118 |
128 this.mouseWheelScrollAmount_ = 0; | 119 this.mouseWheelScrollAmount_ = 0; |
129 this.mouseWheelCardSelected_ = false; | 120 this.mouseWheelCardSelected_ = false; |
130 this.mouseWheelIsContinuous_ = false; | 121 this.mouseWheelIsContinuous_ = false; |
131 this.scrollClearTimeout_ = null; | 122 this.scrollClearTimeout_ = null; |
132 this.frame_.addEventListener('mousewheel', | 123 this.frame_.addEventListener('mousewheel', |
133 this.onMouseWheel_.bind(this)); | 124 this.onMouseWheel_.bind(this)); |
134 this.container_.addEventListener( | 125 this.container_.addEventListener( |
135 'webkitTransitionEnd', this.onAnimationTransitioned_.bind(this)); | 126 'webkitTransitionEnd', this.onWebkitTransitionEnd_.bind(this)); |
136 | 127 |
137 // Also support touch events in case a touch screen happens to be | 128 // Also support touch events in case a touch screen happens to be |
138 // available. Ideally we would support touch events whenever they | 129 // available. Ideally we would support touch events whenever they |
139 // are fired, but for now restrict this extra code to when we know | 130 // are fired, but for now restrict this extra code to when we know |
140 // we want to support touch input. | 131 // we want to support touch input. |
141 if (cr.isTouchOptimized) { | 132 if (cr.isTouchOptimized) { |
142 var TouchHandler = cr.ui.TouchHandler; | 133 var TouchHandler = cr.ui.TouchHandler; |
143 this.container_.addEventListener(TouchHandler.EventType.TOUCH_START, | 134 this.container_.addEventListener(TouchHandler.EventType.TOUCH_START, |
144 this.onTouchStart_.bind(this)); | 135 this.onTouchStart_.bind(this)); |
145 this.container_.addEventListener(TouchHandler.EventType.DRAG_START, | 136 this.container_.addEventListener(TouchHandler.EventType.DRAG_START, |
(...skipping 146 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
292 * Resets the amount of horizontal scroll we've seen to 0. See | 283 * Resets the amount of horizontal scroll we've seen to 0. See |
293 * onMouseWheel_. | 284 * onMouseWheel_. |
294 * @private | 285 * @private |
295 */ | 286 */ |
296 clearMouseWheelScroll_: function() { | 287 clearMouseWheelScroll_: function() { |
297 this.mouseWheelScrollAmount_ = 0; | 288 this.mouseWheelScrollAmount_ = 0; |
298 this.mouseWheelCardSelected_ = false; | 289 this.mouseWheelCardSelected_ = false; |
299 }, | 290 }, |
300 | 291 |
301 /** | 292 /** |
302 * A handler for the animations ending their transition. | 293 * Handles the ends of -webkit-transitions on -webkit-transform (animated |
| 294 * card switches). |
| 295 * @param {Event} e The webkitTransitionEnd event. |
303 * @private | 296 * @private |
304 */ | 297 */ |
305 onAnimationTransitioned_: function(event) { | 298 onWebkitTransitionEnd_: function(e) { |
306 if (event.target.id == 'page-list') { | 299 // Ignore irrelevant transitions that might bubble up. |
307 cr.dispatchSimpleEvent(this.currentCardValue, 'cardSelectionCompleted', | 300 if (e.target !== this.container_ || |
308 true, true); | 301 e.propertyName != '-webkit-transform') { |
| 302 return; |
309 } | 303 } |
| 304 this.fireChangeEndedEvent_(true); |
310 }, | 305 }, |
311 | 306 |
312 /** | 307 /** |
| 308 * Dispatches a simple event to tell subscribers we're done moving to the |
| 309 * newly selected card. |
| 310 * @param {boolean} wasAnimated whether or not the change was animated. |
| 311 * @private |
| 312 */ |
| 313 fireChangeEndedEvent_: function(wasAnimated) { |
| 314 var e = document.createEvent('Event'); |
| 315 e.initEvent('cardSlider:card_change_ended', true, true); |
| 316 e.cardSlider = this; |
| 317 e.changedTo = this.currentCard_; |
| 318 e.wasAnimated = wasAnimated; |
| 319 this.container_.dispatchEvent(e); |
| 320 }, |
| 321 |
| 322 /** |
| 323 * Add a card to the card slider at a particular index. If the card being |
| 324 * added is inserted in front of the current card, cardSlider.currentCard |
| 325 * will be adjusted accordingly (to current card + 1). |
| 326 * @param {!Node} card A card that will be added to the card slider. |
| 327 * @param {number} index An index at which the given |card| should be |
| 328 * inserted. Must be positive and less than the number of cards. |
| 329 */ |
| 330 addCardAtIndex: function(card, index) { |
| 331 assert(card instanceof Node, '|card| isn\'t a Node'); |
| 332 this.assertValidIndex_(index); |
| 333 this.cards_ = Array.prototype.concat.call( |
| 334 this.cards_.slice(0, index), card, this.cards_.slice(index)); |
| 335 |
| 336 if (this.currentCard_ == -1) |
| 337 this.currentCard_ = 0; |
| 338 else if (index <= this.currentCard_) |
| 339 this.selectCard(this.currentCard_ + 1, false, true); |
| 340 |
| 341 this.fireAddedEvent_(card, index); |
| 342 }, |
| 343 |
| 344 /** |
| 345 * Append a card to the end of the list. |
| 346 * @param {!Node} card A card to add at the end of the card slider. |
| 347 */ |
| 348 appendCard: function(card) { |
| 349 assert(card instanceof Node, '|card| isn\'t a Node'); |
| 350 this.cards_.push(card); |
| 351 this.fireAddedEvent_(card, this.cards_.length - 1); |
| 352 }, |
| 353 |
| 354 /** |
| 355 * Dispatches a simple event to tell interested subscribers that a card was |
| 356 * added to this card slider. |
| 357 * @param {Node} card The recently added card. |
| 358 * @param {number} index The position of the newly added card. |
| 359 * @private |
| 360 */ |
| 361 fireAddedEvent_: function(card, index) { |
| 362 this.assertValidIndex_(index); |
| 363 var e = document.createEvent('Event'); |
| 364 e.initEvent('cardSlider:card_added', true, true); |
| 365 e.addedIndex = index; |
| 366 e.addedCard = card; |
| 367 this.container_.dispatchEvent(e); |
| 368 }, |
| 369 |
| 370 /** |
| 371 * Removes a card by index from the card slider. If the card to be removed |
| 372 * is the current card or in front of the current card, the current card |
| 373 * will be updated (to current card - 1). |
| 374 * @param {!Node} card A card to be removed. |
| 375 */ |
| 376 removeCard: function(card) { |
| 377 assert(card instanceof Node, '|card| isn\'t a Node'); |
| 378 this.removeCardAtIndex(this.cards_.indexOf(card)); |
| 379 }, |
| 380 |
| 381 /** |
| 382 * Removes a card by index from the card slider. If the card to be removed |
| 383 * is the current card or in front of the current card, the current card |
| 384 * will be updated (to current card - 1). |
| 385 * @param {number} index The index of the tile that should be removed. |
| 386 */ |
| 387 removeCardAtIndex: function(index) { |
| 388 this.assertValidIndex_(index); |
| 389 var removed = this.cards_.splice(index, 1).pop(); |
| 390 |
| 391 if (this.cards_.length == 0) |
| 392 this.currentCard_ = -1; |
| 393 else if (index < this.currentCard_) |
| 394 this.selectCard(this.currentCard_ - 1, false, true); |
| 395 |
| 396 this.fireRemovedEvent_(removed, index); |
| 397 }, |
| 398 |
| 399 /** |
| 400 * Dispatches a cardSlider:card_removed event so interested subscribers know |
| 401 * when a card was removed from this card slider. |
| 402 * @param {Node} card The recently removed card. |
| 403 * @param {number} index The index of the card before it was removed. |
| 404 * @private |
| 405 */ |
| 406 fireRemovedEvent_: function(card, index) { |
| 407 var e = document.createEvent('Event'); |
| 408 e.initEvent('cardSlider:card_removed', true, true); |
| 409 e.removedCard = card; |
| 410 e.removedIndex = index; |
| 411 this.container_.dispatchEvent(e); |
| 412 }, |
| 413 |
| 414 /** |
| 415 * Checks the the given |index| exists in this.cards_. |
| 416 * @param {number} index An index to check. |
| 417 * @private |
| 418 */ |
| 419 assertValidIndex_: function(index) { |
| 420 assert(index >= 0 && index < this.cards_.length); |
| 421 }, |
| 422 |
| 423 /** |
313 * Selects a new card, ensuring that it is a valid index, transforming the | 424 * Selects a new card, ensuring that it is a valid index, transforming the |
314 * view and possibly calling the change card callback. | 425 * view and possibly calling the change card callback. |
315 * @param {number} newCardIndex Index of card to show. | 426 * @param {number} newCardIndex Index of card to show. |
316 * @param {boolean=} opt_animate If true will animate transition from | 427 * @param {boolean=} opt_animate If true will animate transition from |
317 * current position to new position. | 428 * current position to new position. |
| 429 * @param {boolean=} opt_dontNotify If true, don't tell subscribers that |
| 430 * we've changed cards. |
318 */ | 431 */ |
319 selectCard: function(newCardIndex, opt_animate) { | 432 selectCard: function(newCardIndex, opt_animate, opt_dontNotify) { |
| 433 this.assertValidIndex_(newCardIndex); |
| 434 |
320 var previousCard = this.currentCardValue; | 435 var previousCard = this.currentCardValue; |
321 | |
322 var isChangingCard = | 436 var isChangingCard = |
323 !this.cards_[newCardIndex].classList.contains('selected-card'); | 437 !this.cards_[newCardIndex].classList.contains('selected-card'); |
324 | 438 |
325 if (isChangingCard) { | 439 if (isChangingCard) { |
326 previousCard.classList.remove('selected-card'); | 440 if (previousCard) |
327 // If we have a new card index and it is valid then update the left | 441 previousCard.classList.remove('selected-card'); |
328 // position and current card index. | |
329 this.currentCard_ = newCardIndex; | 442 this.currentCard_ = newCardIndex; |
330 this.currentCardValue.classList.add('selected-card'); | 443 this.currentCardValue.classList.add('selected-card'); |
331 } | 444 } |
332 | 445 |
333 this.transformToCurrentCard_(opt_animate); | 446 var willTransitionHappen = this.transformToCurrentCard_(opt_animate); |
334 | 447 |
335 if (isChangingCard) { | 448 if (isChangingCard && !opt_dontNotify) { |
336 var event = document.createEvent('Event'); | 449 var event = document.createEvent('Event'); |
337 event.initEvent(CardSlider.EventType.CARD_CHANGED, true, true); | 450 event.initEvent('cardSlider:card_changed', true, true); |
338 event.cardSlider = this; | 451 event.cardSlider = this; |
| 452 event.wasAnimated = !!opt_animate; |
339 this.container_.dispatchEvent(event); | 453 this.container_.dispatchEvent(event); |
340 | 454 |
341 // We also dispatch an event on the cards themselves. | 455 // We also dispatch an event on the cards themselves. |
342 if (previousCard) { | 456 if (previousCard) { |
343 cr.dispatchSimpleEvent(previousCard, 'carddeselected', | 457 cr.dispatchSimpleEvent(previousCard, 'carddeselected', |
344 true, true); | 458 true, true); |
345 } | 459 } |
346 cr.dispatchSimpleEvent(this.currentCardValue, 'cardselected', | 460 cr.dispatchSimpleEvent(this.currentCardValue, 'cardselected', |
347 true, true); | 461 true, true); |
348 } | 462 } |
| 463 |
| 464 // If we're not changing, animated, or transitioning, fire a |
| 465 // cardSlider:card_change_ended event right away. |
| 466 if ((!isChangingCard || !opt_animate || !willTransitionHappen) && |
| 467 !opt_dontNotify) { |
| 468 this.fireChangeEndedEvent_(false); |
| 469 } |
349 }, | 470 }, |
350 | 471 |
351 /** | 472 /** |
352 * Selects a card from the stack. Passes through to selectCard. | 473 * Selects a card from the stack. Passes through to selectCard. |
353 * @param {Node} newCard The card that should be selected. | 474 * @param {Node} newCard The card that should be selected. |
354 * @param {boolean=} opt_animate Whether to animate. | 475 * @param {boolean=} opt_animate Whether to animate. |
355 */ | 476 */ |
356 selectCardByValue: function(newCard, opt_animate) { | 477 selectCardByValue: function(newCard, opt_animate) { |
357 var i = this.cards_.indexOf(newCard); | 478 var i = this.cards_.indexOf(newCard); |
358 assert(i != -1); | 479 assert(i != -1); |
359 this.selectCard(i, opt_animate); | 480 this.selectCard(i, opt_animate); |
360 }, | 481 }, |
361 | 482 |
362 /** | 483 /** |
363 * Centers the view on the card denoted by this.currentCard. Can either | 484 * Centers the view on the card denoted by this.currentCard. Can either |
364 * animate to that card or snap to it. | 485 * animate to that card or snap to it. |
365 * @param {boolean=} opt_animate If true will animate transition from | 486 * @param {boolean=} opt_animate If true will animate transition from |
366 * current position to new position. | 487 * current position to new position. |
| 488 * @return {boolean} Whether or not a transformation was necessary. |
367 * @private | 489 * @private |
368 */ | 490 */ |
369 transformToCurrentCard_: function(opt_animate) { | 491 transformToCurrentCard_: function(opt_animate) { |
| 492 var prevLeft = this.currentLeft_; |
370 this.currentLeft_ = -this.cardWidth_ * | 493 this.currentLeft_ = -this.cardWidth_ * |
371 (isRTL() ? this.cards_.length - this.currentCard - 1 : | 494 (isRTL() ? this.cards_.length - this.currentCard - 1 : |
372 this.currentCard); | 495 this.currentCard); |
373 | 496 |
| 497 // If there's no change, return something to let the caller know there |
| 498 // won't be a transition occuring. |
| 499 if (prevLeft == this.currentLeft_) |
| 500 return false; |
| 501 |
374 // Animate to the current card, which will either transition if the | 502 // Animate to the current card, which will either transition if the |
375 // current card is new, or reset the existing card if we didn't drag | 503 // current card is new, or reset the existing card if we didn't drag |
376 // enough to change cards. | 504 // enough to change cards. |
377 var transition = ''; | 505 var transition = ''; |
378 if (opt_animate) { | 506 if (opt_animate) { |
379 transition = '-webkit-transform ' + CardSlider.TRANSITION_TIME_ + | 507 transition = '-webkit-transform ' + CardSlider.TRANSITION_TIME_ + |
380 'ms ease-in-out'; | 508 'ms ease-in-out'; |
381 } | 509 } |
382 this.container_.style.WebkitTransition = transition; | 510 this.container_.style.WebkitTransition = transition; |
383 this.translateTo_(this.currentLeft_); | 511 this.translateTo_(this.currentLeft_); |
| 512 |
| 513 return true; |
384 }, | 514 }, |
385 | 515 |
386 /** | 516 /** |
387 * Moves the view to the specified position. | 517 * Moves the view to the specified position. |
388 * @param {number} x Horizontal position to move to. | 518 * @param {number} x Horizontal position to move to. |
389 * @private | 519 * @private |
390 */ | 520 */ |
391 translateTo_: function(x) { | 521 translateTo_: function(x) { |
392 // We use a webkitTransform to slide because this is GPU accelerated on | 522 // We use a webkitTransform to slide because this is GPU accelerated on |
393 // Chrome and iOS. Once Chrome does GPU acceleration on the position | 523 // Chrome and iOS. Once Chrome does GPU acceleration on the position |
(...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
468 | 598 |
469 // Ensure we're at a card bounary | 599 // Ensure we're at a card bounary |
470 this.transformToCurrentCard_(true); | 600 this.transformToCurrentCard_(true); |
471 }, | 601 }, |
472 }; | 602 }; |
473 | 603 |
474 return { | 604 return { |
475 CardSlider: CardSlider | 605 CardSlider: CardSlider |
476 }; | 606 }; |
477 }); | 607 }); |
OLD | NEW |