Index: chrome/browser/resources/shared/js/cr/ui/card_slider.js |
=================================================================== |
--- chrome/browser/resources/shared/js/cr/ui/card_slider.js (revision 177292) |
+++ chrome/browser/resources/shared/js/cr/ui/card_slider.js (working copy) |
@@ -1,680 +0,0 @@ |
-// Copyright (c) 2012 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 |
-cr.define('cr.ui', 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 {number} cardWidth The width of each card should have. |
- */ |
- function CardSlider(frame, container, cardWidth) { |
- /** |
- * @type {!Element} |
- * @private |
- */ |
- this.frame_ = frame; |
- |
- /** |
- * @type {!Element} |
- * @private |
- */ |
- this.container_ = container; |
- |
- /** |
- * Array of card elements. |
- * @type {!Array.<!Element>} |
- * @private |
- */ |
- this.cards_ = []; |
- |
- /** |
- * Index of currently shown card. |
- * @type {number} |
- * @private |
- */ |
- this.currentCard_ = -1; |
- |
- /** |
- * @type {number} |
- * @private |
- */ |
- this.cardWidth_ = cardWidth; |
- |
- /** |
- * @type {!cr.ui.TouchHandler} |
- * @private |
- */ |
- this.touchHandler_ = new cr.ui.TouchHandler(this.container_); |
- } |
- |
- |
- /** |
- * The time to transition between cards when animating. Measured in ms. |
- * @type {number} |
- * @private |
- * @const |
- */ |
- CardSlider.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 |
- */ |
- CardSlider.TRANSITION_VELOCITY_THRESHOLD_ = 0.2; |
- |
- |
- CardSlider.prototype = { |
- /** |
- * The current left offset of the container relative to the frame. This |
- * position does not include deltas from active drag operations, and |
- * always aligns with a frame boundary. |
- * @type {number} |
- * @private |
- */ |
- currentLeft_: 0, |
- |
- /** |
- * Current offset relative to |currentLeft_| due to an active drag |
- * operation. |
- * @type {number} |
- * @private |
- */ |
- deltaX_: 0, |
- |
- /** |
- * Initialize all elements and event handlers. Must call after construction |
- * and before usage. |
- * @param {boolean} ignoreMouseWheelEvents If true, horizontal mouse wheel |
- * events will be ignored, rather than flipping between pages. |
- */ |
- initialize: function(ignoreMouseWheelEvents) { |
- 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.'); |
- |
- this.updateCardWidths_(); |
- |
- this.mouseWheelScrollAmount_ = 0; |
- this.mouseWheelCardSelected_ = false; |
- this.mouseWheelIsContinuous_ = false; |
- this.scrollClearTimeout_ = null; |
- if (!ignoreMouseWheelEvents) { |
- this.frame_.addEventListener('mousewheel', |
- this.onMouseWheel_.bind(this)); |
- } |
- this.container_.addEventListener( |
- 'webkitTransitionEnd', this.onWebkitTransitionEnd_.bind(this)); |
- |
- // Also support touch events in case a touch screen happens to be |
- // available. Note that this has minimal impact in the common case of |
- // no touch events (eg. we're mainly just adding listeners for events that |
- // will never trigger). |
- var TouchHandler = cr.ui.TouchHandler; |
- 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 CardSlider#setCards'); |
- this.cards_ = cards; |
- |
- this.updateCardWidths_(); |
- this.updateSelectedCardAttributes_(); |
- |
- // Jump to the given card index. |
- this.selectCard(index, false, false, true); |
- }, |
- |
- /** |
- * Ensures that for all cards: |
- * - if the card is the current card, then it has 'selected-card' in its |
- * classList, and is visible for accessibility |
- * - if the card is not the selected card, then it does not have |
- * 'selected-card' in its classList, and is invisible for accessibility. |
- * @private |
- */ |
- updateSelectedCardAttributes_: function() { |
- for (var i = 0; i < this.cards_.length; i++) { |
- if (i == this.currentCard_) { |
- this.cards_[i].classList.add('selected-card'); |
- this.cards_[i].removeAttribute('aria-hidden'); |
- } else { |
- this.cards_[i].classList.remove('selected-card'); |
- this.cards_[i].setAttribute('aria-hidden', true); |
- } |
- } |
- }, |
- |
- /** |
- * 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_; |
- }, |
- |
- /** |
- * Allows setting the current card index. |
- * @param {number} index A new index to set the current index to. |
- * @return {number} The new index after having been set. |
- */ |
- set currentCard(index) { |
- return (this.currentCard_ = index); |
- }, |
- |
- /** |
- * Returns the number of cards. |
- * @return {number} number of cards. |
- */ |
- get cardCount() { |
- return this.cards_.length; |
- }, |
- |
- /** |
- * Returns the current card itself. |
- * @return {!Element} the currently shown card. |
- */ |
- get currentCardValue() { |
- return this.cards_[this.currentCard_]; |
- }, |
- |
- /** |
- * Returns the frame holding the cards. |
- * @return {Element} The frame used to position the cards. |
- */ |
- get frame() { |
- return this.frame_; |
- }, |
- |
- /** |
- * Handle horizontal scrolls to flip between pages. |
- * @private |
- */ |
- onMouseWheel_: function(e) { |
- if (e.wheelDeltaX == 0) |
- return; |
- |
- // Continuous devices such as an Apple Touchpad or Apple MagicMouse will |
- // send arbitrary delta values. Conversly, standard mousewheels will |
- // send delta values in increments of 120. (There is of course a small |
- // chance we mistake a continuous device for a non-continuous device. |
- // Unfortunately there isn't a better way to do this until real touch |
- // events are available to desktop clients.) |
- var DISCRETE_DELTA = 120; |
- if (e.wheelDeltaX % DISCRETE_DELTA) |
- this.mouseWheelIsContinuous_ = true; |
- |
- if (this.mouseWheelIsContinuous_) { |
- // For continuous devices, detect a page swipe when the accumulated |
- // delta matches a pre-defined threshhold. After changing the page, |
- // ignore wheel events for a short time before repeating this process. |
- if (this.mouseWheelCardSelected_) return; |
- this.mouseWheelScrollAmount_ += e.wheelDeltaX; |
- if (Math.abs(this.mouseWheelScrollAmount_) >= 600) { |
- var pagesToScroll = this.mouseWheelScrollAmount_ > 0 ? 1 : -1; |
- if (!isRTL()) |
- pagesToScroll *= -1; |
- var newCardIndex = this.currentCard + pagesToScroll; |
- newCardIndex = Math.min(this.cards_.length - 1, |
- Math.max(0, newCardIndex)); |
- this.selectCard(newCardIndex, true); |
- this.mouseWheelCardSelected_ = true; |
- } |
- } else { |
- // For discrete devices, consider each wheel tick a page change. |
- var pagesToScroll = e.wheelDeltaX / DISCRETE_DELTA; |
- if (!isRTL()) |
- pagesToScroll *= -1; |
- var newCardIndex = this.currentCard + pagesToScroll; |
- newCardIndex = Math.min(this.cards_.length - 1, |
- Math.max(0, newCardIndex)); |
- this.selectCard(newCardIndex, true); |
- } |
- |
- // We got a mouse wheel event, so cancel any pending scroll wheel timeout. |
- if (this.scrollClearTimeout_ != null) |
- clearTimeout(this.scrollClearTimeout_); |
- // If we didn't use up all the scroll, hold onto it for a little bit, but |
- // drop it after a delay. |
- if (this.mouseWheelScrollAmount_ != 0) { |
- this.scrollClearTimeout_ = |
- setTimeout(this.clearMouseWheelScroll_.bind(this), 500); |
- } |
- }, |
- |
- /** |
- * Resets the amount of horizontal scroll we've seen to 0. See |
- * onMouseWheel_. |
- * @private |
- */ |
- clearMouseWheelScroll_: function() { |
- this.mouseWheelScrollAmount_ = 0; |
- this.mouseWheelCardSelected_ = false; |
- }, |
- |
- /** |
- * Handles the ends of -webkit-transitions on -webkit-transform (animated |
- * card switches). |
- * @param {Event} e The webkitTransitionEnd event. |
- * @private |
- */ |
- onWebkitTransitionEnd_: function(e) { |
- // Ignore irrelevant transitions that might bubble up. |
- if (e.target !== this.container_ || |
- e.propertyName != '-webkit-transform') { |
- return; |
- } |
- this.fireChangeEndedEvent_(true); |
- }, |
- |
- /** |
- * Dispatches a simple event to tell subscribers we're done moving to the |
- * newly selected card. |
- * @param {boolean} wasAnimated whether or not the change was animated. |
- * @private |
- */ |
- fireChangeEndedEvent_: function(wasAnimated) { |
- var e = document.createEvent('Event'); |
- e.initEvent('cardSlider:card_change_ended', true, true); |
- e.cardSlider = this; |
- e.changedTo = this.currentCard_; |
- e.wasAnimated = wasAnimated; |
- this.container_.dispatchEvent(e); |
- }, |
- |
- /** |
- * Add a card to the card slider at a particular index. If the card being |
- * added is inserted in front of the current card, cardSlider.currentCard |
- * will be adjusted accordingly (to current card + 1). |
- * @param {!Node} card A card that will be added to the card slider. |
- * @param {number} index An index at which the given |card| should be |
- * inserted. Must be positive and less than the number of cards. |
- */ |
- addCardAtIndex: function(card, index) { |
- assert(card instanceof Node, '|card| isn\'t a Node'); |
- this.assertValidIndex_(index); |
- this.cards_ = Array.prototype.concat.call( |
- this.cards_.slice(0, index), card, this.cards_.slice(index)); |
- |
- this.updateSelectedCardAttributes_(); |
- |
- if (this.currentCard_ == -1) |
- this.currentCard_ = 0; |
- else if (index <= this.currentCard_) |
- this.selectCard(this.currentCard_ + 1, false, true, true); |
- |
- this.fireAddedEvent_(card, index); |
- }, |
- |
- /** |
- * Append a card to the end of the list. |
- * @param {!Node} card A card to add at the end of the card slider. |
- */ |
- appendCard: function(card) { |
- assert(card instanceof Node, '|card| isn\'t a Node'); |
- this.cards_.push(card); |
- this.fireAddedEvent_(card, this.cards_.length - 1); |
- }, |
- |
- /** |
- * Dispatches a simple event to tell interested subscribers that a card was |
- * added to this card slider. |
- * @param {Node} card The recently added card. |
- * @param {number} index The position of the newly added card. |
- * @private |
- */ |
- fireAddedEvent_: function(card, index) { |
- this.assertValidIndex_(index); |
- var e = document.createEvent('Event'); |
- e.initEvent('cardSlider:card_added', true, true); |
- e.addedIndex = index; |
- e.addedCard = card; |
- this.container_.dispatchEvent(e); |
- }, |
- |
- /** |
- * Returns the card at a particular index. |
- * @param {number} index The index of the card to return. |
- * @return {!Element} The card at the given index. |
- */ |
- getCardAtIndex: function(index) { |
- this.assertValidIndex_(index); |
- return this.cards_[index]; |
- }, |
- |
- /** |
- * Removes a card by index from the card slider. If the card to be removed |
- * is the current card or in front of the current card, the current card |
- * will be updated (to current card - 1). |
- * @param {!Node} card A card to be removed. |
- */ |
- removeCard: function(card) { |
- assert(card instanceof Node, '|card| isn\'t a Node'); |
- this.removeCardAtIndex(this.cards_.indexOf(card)); |
- }, |
- |
- /** |
- * Removes a card by index from the card slider. If the card to be removed |
- * is the current card or in front of the current card, the current card |
- * will be updated (to current card - 1). |
- * @param {number} index The index of the tile that should be removed. |
- */ |
- removeCardAtIndex: function(index) { |
- this.assertValidIndex_(index); |
- var removed = this.cards_.splice(index, 1).pop(); |
- |
- if (this.cards_.length == 0) |
- this.currentCard_ = -1; |
- else if (index < this.currentCard_) |
- this.selectCard(this.currentCard_ - 1, false, true); |
- |
- this.fireRemovedEvent_(removed, index); |
- }, |
- |
- /** |
- * Dispatches a cardSlider:card_removed event so interested subscribers know |
- * when a card was removed from this card slider. |
- * @param {Node} card The recently removed card. |
- * @param {number} index The index of the card before it was removed. |
- * @private |
- */ |
- fireRemovedEvent_: function(card, index) { |
- var e = document.createEvent('Event'); |
- e.initEvent('cardSlider:card_removed', true, true); |
- e.removedCard = card; |
- e.removedIndex = index; |
- this.container_.dispatchEvent(e); |
- }, |
- |
- /** |
- * This re-syncs the -webkit-transform that's used to position the frame in |
- * the likely event it needs to be updated by a card being inserted or |
- * removed in the flow. |
- */ |
- repositionFrame: function() { |
- this.transformToCurrentCard_(); |
- }, |
- |
- /** |
- * Checks the the given |index| exists in this.cards_. |
- * @param {number} index An index to check. |
- * @private |
- */ |
- assertValidIndex_: function(index) { |
- assert(index >= 0 && index < this.cards_.length); |
- }, |
- |
- /** |
- * 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. |
- * @param {boolean=} opt_dontNotify If true, don't tell subscribers that |
- * we've changed cards. |
- * @param {boolean=} opt_forceChange If true, ignore if the card already |
- * selected. |
- */ |
- selectCard: function(newCardIndex, |
- opt_animate, |
- opt_dontNotify, |
- opt_forceChange) { |
- this.assertValidIndex_(newCardIndex); |
- |
- var previousCard = this.currentCardValue; |
- var isChangingCard = |
- !this.cards_[newCardIndex].classList.contains('selected-card'); |
- |
- if (typeof opt_forceChange != 'undefined' && opt_forceChange) |
- isChangingCard = true; |
- |
- if (isChangingCard) { |
- this.currentCard_ = newCardIndex; |
- this.updateSelectedCardAttributes_(); |
- } |
- |
- var willTransitionHappen = this.transformToCurrentCard_(opt_animate); |
- |
- if (isChangingCard && !opt_dontNotify) { |
- var event = document.createEvent('Event'); |
- event.initEvent('cardSlider:card_changed', true, true); |
- event.cardSlider = this; |
- event.wasAnimated = !!opt_animate; |
- this.container_.dispatchEvent(event); |
- |
- // We also dispatch an event on the cards themselves. |
- if (previousCard) { |
- cr.dispatchSimpleEvent(previousCard, 'carddeselected', |
- true, true); |
- } |
- cr.dispatchSimpleEvent(this.currentCardValue, 'cardselected', |
- true, true); |
- } |
- |
- // If we're not changing, animated, or transitioning, fire a |
- // cardSlider:card_change_ended event right away. |
- if ((!isChangingCard || !opt_animate || !willTransitionHappen) && |
- !opt_dontNotify) { |
- this.fireChangeEndedEvent_(false); |
- } |
- }, |
- |
- /** |
- * Selects a card from the stack. Passes through to selectCard. |
- * @param {Node} newCard The card that should be selected. |
- * @param {boolean=} opt_animate Whether to animate. |
- */ |
- selectCardByValue: function(newCard, opt_animate) { |
- var i = this.cards_.indexOf(newCard); |
- assert(i != -1); |
- this.selectCard(i, opt_animate); |
- }, |
- |
- /** |
- * 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. |
- * @return {boolean} Whether or not a transformation was necessary. |
- * @private |
- */ |
- transformToCurrentCard_: function(opt_animate) { |
- var prevLeft = this.currentLeft_; |
- this.currentLeft_ = -this.cardWidth_ * |
- (isRTL() ? this.cards_.length - this.currentCard - 1 : |
- this.currentCard); |
- |
- // If there's no change, return something to let the caller know there |
- // won't be a transition occuring. |
- if (prevLeft == this.currentLeft_ && this.deltaX_ == 0) |
- return false; |
- |
- // 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 ' + CardSlider.TRANSITION_TIME_ + |
- 'ms ease-in-out'; |
- } |
- this.container_.style.WebkitTransition = transition; |
- this.translateTo_(this.currentLeft_); |
- |
- return true; |
- }, |
- |
- /** |
- * 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.deltaX_ = x - this.currentLeft_; |
- this.container_.style.WebkitTransform = 'translate3d(' + x + 'px, 0, 0)'; |
- }, |
- |
- /* Touch ******************************************************************/ |
- |
- /** |
- * Clear any transition that is in progress and enable dragging for the |
- * touch. |
- * @param {!cr.ui.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 and there is more than one card to slide. |
- * @param {!cr.ui.TouchHandler.Event} e The TouchHandler event. |
- * @private |
- */ |
- onDragStart_: function(e) { |
- e.enableDrag = this.cardCount > 1 && 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 {!cr.ui.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); |
- }, |
- |
- /** |
- * 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 {!cr.ui.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) > |
- CardSlider.TRANSITION_VELOCITY_THRESHOLD_) { |
- // 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; |
- } |
- // Ensure that the new card index is valid. The new card index could be |
- // invalid if a swipe suggests scrolling off the end of the list of |
- // cards. |
- if (newCardIndex < 0) |
- newCardIndex = 0; |
- else if (newCardIndex >= this.cardCount) |
- newCardIndex = this.cardCount - 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); |
- }, |
- }; |
- |
- return { |
- CardSlider: CardSlider |
- }; |
-}); |