Source: js/task_manager.js

/* global Card, eventSafety, SettingsListener, LazyLoader,
          Service, StackManager */

(function(exports) {
  'use strict';

  var DEBUG = false;

  /**
   * Represent a stack of apps as cards
   * @class TaskManager
   */
  function TaskManager(appWindowManager) {
    this.appWindowManager = appWindowManager;
    this.stack = null;
    this.cardsByAppID = {};
  }

  TaskManager.prototype = Object.create({
    /**
     * The setting that enables/disables using screenshots vs. icons for the
     *  card preview
     * @memberof TaskCard.prototype
     */
    SCREENSHOT_PREVIEWS_SETTING_KEY: 'app.cards_view.screenshots.enabled',

    DURATION: 200,

    // Arbitrarily chosen to be 4x larger than the gecko18 drag
    // threshold.  This constant should be a truemm/mozmm value, but
    // it's hard for us to evaluate that here.
    SWIPE_UP_THRESHOLD: 100,

    SWITCH_CARD_THRESHOLD: 30,

    /**
     * Cached value of the screenshots.enabled setting
     * @memberOf TaskManager.prototype
     */
    useAppScreenshotPreviews: true,

    /**
     * Index into the stack of the current app/card
     * @memberOf TaskManager.prototype
     */
    position: 0,


    _shouldGoBackHome: false,
    _active: false,

    windowWidth: window.innerWidth,
    windowHeight: window.innerHeight
  }, {
    /**
     * Getter for the current card
     * @memberOf TaskManager.prototype
     */
    currentCard: {
      get: function cs_getCurrentCard() {
        return this.getCardAtIndex(this.position);
      }
    }
  });

  TaskManager.prototype.EVENT_PREFIX = 'taskmanager';
  TaskManager.prototype.name = 'TaskManager';

  TaskManager.prototype.setHierarchy = function() {
    return true;
  };
  /**
   * initialize
   * @memberOf TaskManager.prototype
   */
  TaskManager.prototype.start = function() {
    this._fetchElements();
    this._registerEvents();
    this._appClosedHandler = this._appClosed.bind(this);
    Service.request('registerHierarchy', this);
    return LazyLoader.load([
      'js/card.js',
      'js/cards_helper.js'
    ]);
  };

  TaskManager.prototype.stop = function() {
    this._unregisterEvents();
    Service.request('unregisterHierarchy', this);
  };

  TaskManager.prototype._fetchElements = function() {
    this.element = document.getElementById('cards-view'),
    this.cardsList = document.getElementById('cards-list');
    this.screenElement = document.getElementById('screen');
  };

  TaskManager.prototype._registerEvents = function() {
    window.addEventListener('taskmanagershow', this);

    this.onPreviewSettingsChange = function(settingValue) {
      this.useAppScreenshotPreviews = settingValue;
    }.bind(this);

    SettingsListener.observe(this.SCREENSHOT_PREVIEWS_SETTING_KEY,
                             this.useAppScreenshotPreviews,
                             this.onPreviewSettingsChange);
  };

  TaskManager.prototype._unregisterEvents = function() {
    window.removeEventListener('taskmanagershow', this);

    SettingsListener.unobserve(this.SCREENSHOT_PREVIEWS_SETTING_KEY,
                               this.onPreviewSettingsChange);
  };

  TaskManager.prototype._appClosed = function cs_appClosed(evt) {
    window.removeEventListener('appclosed', this._appClosedHandler);
    window.removeEventListener('homescreenclosed', this._appClosedHandler);
    this.screenElement.classList.add('cards-view');
    this.element.classList.remove('from-home');
  };

  /**
   * Build and display the card switcher overlay
   * Note that we rebuild the switcher each time we need it rather
   * than trying to keep it in sync with app launches.
   *
   * @memberOf TaskManager.prototype
   * @param filterName {string} The name of the filter to apply. Only two fitler
   *                            types are supported at this time: 'browser-only'
   *                            and 'apps-only'.
   */
  TaskManager.prototype.show = function cs_showCardSwitcher(filterName) {
    if (this.isShown()) {
      return;
    }
    if (document.mozFullScreen) {
      document.mozCancelFullScreen();
    }
    this.calculateDimensions();
    this.newStackPosition = null;
    // start listening for the various events we need to handle while
    // the card view is showing
    this._registerShowingEvents();

    if (this.filter(filterName)) {
      // Update visual style to indicate we're filtered.
      this.element.classList.add('filtered');
    }

    // First add an item to the cardsList for each running app
    var stack = this.stack;
    stack.forEach(function(app, position) {
      this.addCard(position, app);
    }, this);

    this.unfilteredStack.forEach(function(app, position) {
      app.enterTaskManager();
    });

    this._placeCards();
    this.setActive(true);

    var screenElement = this.screenElement;
    var activeApp = Service.query('AppWindowManager.getActiveWindow');
    if (!activeApp) {
      screenElement.classList.add('cards-view');
      return;
    }

    if (activeApp.isHomescreen) {
      // Ensure the homescreen is in a closed state, as the user may choose
      // one of the app.
      activeApp.close('home-to-cardview');
      this.element.classList.add('from-home');
      window.addEventListener('homescreenclosed', this._appClosedHandler);
    } else {
      window.addEventListener('appclosed', this._appClosedHandler);
    }
  };

  /**
   * Hide the card switcher
   *
   * @memberOf TaskManager.prototype
   *
   */
  TaskManager.prototype.hide = function cs_hideCardSwitcher() {
    if (!this.isActive()) {
      return;
    }
    this._unregisterShowingEvents();
    this._removeCards();
    this.setActive(false);
    window.removeEventListener('appclosed', this._appClosedHandler);
    window.removeEventListener('homescreenclosed', this._appClosedHandler);
    this.screenElement.classList.remove('cards-view');

    var detail;
    if (!isNaN(this.newStackPosition)) {
      detail = { 'detail': { 'newStackPosition': this.newStackPosition }};
    }
    this.publishNextTick('cardviewclosed', detail);
  };


  TaskManager.prototype._showingEventsRegistered = false;

  TaskManager.prototype._registerShowingEvents = function() {
    if (this._showingEventsRegistered) {
      return;
    }
    this._showingEventsRegistered = true;
    window.addEventListener('lockscreen-appopened', this);
    window.addEventListener('attentionopened', this);
    window.addEventListener('appopen', this);
    window.addEventListener('appterminated', this);
    window.addEventListener('wheel', this);
    window.addEventListener('resize', this);

    this.element.addEventListener('touchstart', this);
    this.element.addEventListener('touchmove', this);
    this.element.addEventListener('touchend', this);
  };

  TaskManager.prototype._unregisterShowingEvents = function() {
    if (!this._showingEventsRegistered) {
      return;
    }
    window.removeEventListener('lockscreen-appopened', this);
    window.removeEventListener('attentionopened', this);
    window.removeEventListener('appopen', this);
    window.removeEventListener('appterminated', this);
    window.removeEventListener('wheel', this);
    window.removeEventListener('resize', this);
    if (this.element) {
      this.element.removeEventListener('touchstart', this);
      this.element.removeEventListener('touchmove', this);
      this.element.removeEventListener('touchend', this);
    }
    this._showingEventsRegistered = false;
  };

  /**
   * Is the view currently active
   * @memberOf TaskManager.prototype
   *
   * XXX It would be nice to rename that to isActive, in order to be synced
   * with setActive method.
   */
  TaskManager.prototype.isShown = function() {
    return this.isActive();
  };

  /**
   * Is the view currently active
   * @memberOf TaskManager.prototype
   */
  TaskManager.prototype.isActive = function() {
    return this._active;
  };

  /**
   * Toggle to activate/deactivate (mostly adding classes to elements)
   * @param {Boolean} true to activate, false to deactivate
   * @memberOf TaskManager.prototype
   */
  TaskManager.prototype.setActive = function(active) {
    if (active == this._active) {
      return;
    }
    this._active = active;
    if (active) {
      this.publish(this.EVENT_PREFIX + '-activated');
    } else {
      this.publish(this.EVENT_PREFIX + '-deactivated');
    }
    this.element.classList.toggle('active', active);
    this.element.classList.toggle('empty', !this.stack.length && active);

    // XXX This code is weird as it does not seems symetric.
    // In one direction we considered that the card view is already shown,
    // while on the other this is before it is closed!
    this.publishNextTick(active ? 'cardviewshown' : 'cardviewbeforeclose');
  };

  /**
   * Apply filter 'filterName' to the card stack.
   *
   * @memberOf TaskManager.prototype
   * @param filterName {string} The name of the filter to apply.
   * @returns true if a filter was applied, false if not.
   */
  TaskManager.prototype.filter = function cs_filterCardStack(filterName) {
    var unfilteredStack = this.unfilteredStack = StackManager.snapshot();

    var noRecentWindows = document.getElementById('cards-no-recent-windows');
    switch (filterName) {
      // Filter out any application that is not a system browser window.
      case 'browser-only':
        this.stack = unfilteredStack.filter(function(app) {
          return app.isBrowser() ||
            (app.manifest && app.manifest.role === 'search');
        });
        navigator.mozL10n.setAttributes(noRecentWindows,
                                        'no-recent-browser-windows');
        break;

      // Filter out any application that is not an application only window.
      case 'apps-only':
        this.stack = unfilteredStack.filter(function(app) {
          return !app.isBrowser();
        });
        navigator.mozL10n.setAttributes(noRecentWindows,
                                        'no-recent-app-windows');
        break;

      default:
        this.stack = unfilteredStack;
        break;
    }

    this.position = this.stack.indexOf(unfilteredStack[StackManager.position]);
    if (this.position === -1 || StackManager.outOfStack()) {
      this.position = this.stack.length - 1;
      this._shouldGoBackHome = true;
    } else {
      this._shouldGoBackHome = false;
    }

    return this.stack !== unfilteredStack;
  };


  /**
   * Insert a new card for the given app
   *
   * @memberOf TaskManager.prototype
   * @param {Number} position in the stack for the new card
   * @param {AppWindow} app The appWindow the card should wrap and represent
   */
  TaskManager.prototype.addCard = function cs_addCard(position, app) {
    var config = {
      manager: this,
      position: position,
      app: app,
      windowWidth: this.windowWidth,
      windowHeight: this.windowHeight
    };
    this.instantiateCard(config);
  };

  TaskManager.prototype.instantiateCard = function(config) {
    var card = new Card(config);
    this.cardsByAppID[config.app.instanceID] = card;
    this.cardsList.appendChild(card.render());

    if (config.position <= this.position - 2 ||
        config.position >= this.position + 2) {
      card.element.style.visibility = 'hidden';
    }
  };

  /**
   * Remove the given card
   *
   * @memberOf TaskManager.prototype
   * @param {object} card the card instance to be removed
   */
  TaskManager.prototype.removeCard = function cs_removeCard(card) {
    var element = card.element;
    var position = element.dataset.position;
    delete this.cardsByAppID[card.app.instanceID];
    card.destroy();
    element = null;

    // stop tracking this app in our own stack.
    this.stack.splice(position, 1);

    // Update the card positions.
    var cardNodes = this.cardsList.childNodes;
    for (var i = position, remainingCard = null; i < cardNodes.length; i++) {
      remainingCard = this.getCardForElement(cardNodes[i]);
      if (remainingCard) {
        remainingCard.position = i;
        cardNodes[i].dataset.position = i;
      }
    }

    // Fix for non selectable cards when we remove the last card
    // Described in https://bugzilla.mozilla.org/show_bug.cgi?id=825293
    var cardsLength = cardNodes.length;
    if (!cardsLength) {
      var homescreen = Service.query('getHomescreen', true);
      this.exitToApp(homescreen);
    }

    if (cardsLength === this.position) {
      this.position--;
    }
    this.alignCurrentCard();
  };

  /**
   * Remove all cards
   *
   * @memberOf TaskManager.prototype
   */
  TaskManager.prototype._removeCards = function cs_removeCards() {
    this.stack.forEach(function(app, idx) {
      var card = this.cardsByAppID[app.instanceID];
      card && card.destroy();
    }, this);

    this.unfilteredStack.forEach(function(app, position) {
      app.leaveTaskManager();
    });

    this.cardsByAppID = {};
    this.element.classList.remove('filtered');
    this.cardsList.innerHTML = '';
  };

  /**
   * Handle the given action on the given card.
   *
   * @memberOf TaskManager.prototype
   * @param  {Card} card The card to call the action on.
   * @param  {String} actionName The name of the action to invoke.
   */
  TaskManager.prototype.cardAction = function cs_cardAction(card, actionName) {
    switch (actionName) {
      case 'close' :
        card.killApp();
        break;

      case 'favorite' :
        debug('cardAction: TODO: favorite ' + card.element.dataset.origin);
        break;

      case 'select' :

        if (this.position != card.position) {
          // Make the target app, the selected app
          this.position = card.position;
          this.alignCurrentCard();
        }

        var self = this;
        this.currentCard.element.addEventListener('transitionend',
          function afterTransition(e) {
            e.target.removeEventListener('transitionend', afterTransition);
            self.exitToApp(card.app);
          });
        this.currentCard.element.classList.add('select');

        break;
    }
  };

  TaskManager.prototype.exitToApp = function(app) {
    // The cards view class is removed here in order to let the window
    // manager repaints everything.
    this.screenElement.classList.remove('cards-view');
    // immediately stop listening for input events
    this._unregisterShowingEvents();

    if (this._shouldGoBackHome) {
      app = app || Service.query('getHomescreen', true);
    } else if (!app) {
      app = this.stack ? this.stack[this.position] :
                         Service.query('getHomescreen', true);
    }
    // to know if position has changed we need index into original stack,
    var position = this.unfilteredStack ? this.unfilteredStack.indexOf(app) :
                                          -1;

    if (position !== StackManager.position) {
      this.newStackPosition = position;
    }

    setTimeout(() => {
      var finish = () => {
        this.element.classList.remove('to-home');
        this.hide();
      };
      eventSafety(app.element, '_opened', finish, 400);

      if (app.isHomescreen) {
        this.element.classList.add('to-home');
        app.open('home-from-cardview');
      } else {
        app.open('from-cardview');
      }
    }, 100);

  };

  /**
   * Handle wheel events produced by the screen reader on two finger swipe.
   * @memberOf TaskManager.prototype
   * @param  {DOMEvent} evt The event.
   */
  TaskManager.prototype.handleWheel = function cs_handleWheel(evt) {
    if (evt.deltaMode !== evt.DOM_DELTA_PAGE || evt.deltaY < 0) {
      return;
    }
    if (evt.deltaY > 0) {
      // Two finger swipe up.
      var card = this.currentCard;
      if (card.app.killable()) {
        card.killApp();
      } else {
        card.applyStyle({ MozTransform: '' });
      }
    } else if (evt.deltaX > 0 &&
      this.position < this.cardsList.childNodes.length - 1) {
      // Two finger swipe left.
      this.position++;
    } else if (evt.deltaX < 0 && this.position > 0) {
      // Two finger swipe right.
      this.position--;
    }
    this.alignCurrentCard();
  };

  TaskManager.prototype.respondToHierarchyEvent = function(evt) {
    if (this['_handle_' + evt.type]) {
      return this['_handle_' + evt.type](evt);
    }
    return true;
  };

  TaskManager.prototype._handle_home = function() {
    if (this.isActive()) {
      this._shouldGoBackHome = true;
      this.exitToApp();
      return false;
    }
    return true;
  };

  TaskManager.prototype._handle_holdhome = function(evt) {
    if (this.isShown()) {
      return true;
    }

    var filter = null;
    if (evt.type === 'taskmanagershow') {
      filter = (evt.detail && evt.detail.filter) || null;
    }

    var currOrientation = Service.query('fetchCurrentOrientation');
    var shouldResize = (Service.query('defaultOrientation').split('-')[0] !=
                        currOrientation.split('-')[0]);
    var shouldHideKeyboard = Service.query('keyboardEnabled');

    this.publish('cardviewbeforeshow'); // Will hide the keyboard if needed

    var finish = () => {
      if (shouldHideKeyboard) {
        window.addEventListener('keyboardhidden', function kbHidden() {
          window.removeEventListener('keyboardhidden', kbHidden);
          shouldHideKeyboard = false;
          setTimeout(finish);
        });
        return;
      }

      screen.mozLockOrientation(Service.query('defaultOrientation'));
      if (shouldResize) {
        // aspect ratio change will produce resize event
        window.addEventListener('resize', function resized() {
          window.removeEventListener('resize', resized);
          shouldResize = false;
          setTimeout(finish);
        });
        return;
      }

      var app = Service.query('AppWindowManager.getActiveWindow');
      if (app && !app.isHomescreen) {
        app.getScreenshot(function onGettingRealtimeScreenshot() {
          this.show(filter);
        }.bind(this), 0, 0, 400);
      } else {
        this.show(filter);
      }
    };

    finish();
  };

  /**
   * Handle (synthetic) tap events on the card list
   *
   * @memberOf TaskManager.prototype
   * @param  {DOMEvent} evt The event.
   */
  TaskManager.prototype.handleTap = function cs_handleTap(evt) {
    var targetNode = evt.target;

    var cardElem = null;
    var tmpNode = targetNode;
    while (tmpNode) {
      if (tmpNode.classList && tmpNode.classList.contains('card')) {
        cardElem = tmpNode;
        break;
      }
      tmpNode = tmpNode.parentNode;
    }

    var card = this.getCardForElement(cardElem);
    if (!card) {
      return;
    }

    if ('buttonAction' in targetNode.dataset) {
      this.cardAction(card, targetNode.dataset.buttonAction);
      return;
    }

    if (('position' in targetNode.dataset) || card) {
      this.cardAction(card, 'select');
      return;
    }
  };

  /**
   * Gets current sizing information on resize or render.
   * @memberOf TaskManager.prototype
   * @param  {DOMEvent} evt The event.
   */
  TaskManager.prototype.calculateDimensions =
    function cv_calculateDimensions(evt) {
    this.windowWidth = window.innerWidth;
    this.windowHeight = window.innerHeight;
  };

  /**
   * Default event handler
   * @memberOf TaskManager.prototype
   * @param  {DOMEvent} evt The event.
   */
  TaskManager.prototype.handleEvent = function cv_handleEvent(evt) {
    var app;
    switch (evt.type) {
      case 'touchstart':
        this.onTouchStart(evt);
        evt.preventDefault();
        evt.stopPropagation();
        break;

      case 'touchmove':
        this.onTouchMove(evt);
        evt.stopPropagation();
        evt.preventDefault();
        break;

      case 'touchend':
        this.onTouchEnd(evt);
        evt.stopPropagation();
        evt.preventDefault();
        break;

      case 'resize':
        this.calculateDimensions();
        this.alignCurrentCard();
        break;

      case 'wheel':
        this.handleWheel(evt);
        break;

      case 'lockscreen-appopened':
      case 'attentionopened':
        this.exitToApp();
        break;

      case 'taskmanagershow':
        this._handle_holdhome(evt);
        break;

      case 'taskmanagerhide':
      case 'appopen':
        this.hide();
        break;

      case 'appterminated':
        app = evt.detail;
        var card = app && this.cardsByAppID[app.instanceID];
        if (card && card.app && app.instanceID === card.app.instanceID) {
          this.removeCard(card);
        }
        break;
    }
  };

  TaskManager.prototype.publish = function tm_publish(type, detail) {
    var event = new CustomEvent(type, detail || null);
    window.dispatchEvent(event);
  };

  TaskManager.prototype.publishNextTick = function tm_publish(type, detail) {
    var event = new CustomEvent(type, detail || null);
    setTimeout(function nextTick() {
      window.dispatchEvent(event);
    });
  };

   /**
    * Return the card object at the given index into the stack
    * @memberOf TaskManager.prototype
    * @param {Number} idx index into the stack
    */
  TaskManager.prototype.getCardAtIndex = function(idx) {
    if (this.stack && idx > -1 && idx < this.stack.length) {
      var app = this.stack[idx];
      var card = app && this.cardsByAppID[app.instanceID];
      if (card) {
        return card;
      }
    }
    debug('getCardAtIndex, no card at idx: ' + idx);
    return null;
  };

  /**
   * Return the card object that owns the given element
   * @memberOf TaskManager.prototype
   * @param {DOMNode} element
   */
  TaskManager.prototype.getCardForElement = function(element) {
    return element && this.cardsByAppID[element.dataset.appInstanceId];
  };

  /**
   * Add ARIA attributes to available cards.
   * @memberOf TaskManager.prototype
   */
  TaskManager.prototype._setAccessibilityAttributes = function() {
    this.stack.forEach(function(app, idx) {
      var card = this.cardsByAppID[app.instanceID];
      if (!card) {
        return;
      }

      // Hide non-current apps from the screen reader.
      card.setVisibleForScreenReader(idx === this.position);
      // Update the screen reader card list size.
      card.element.setAttribute('aria-setsize', this.stack.length);
      // Update the screen reader card index.
      card.element.setAttribute('aria-posinset', idx + 1);
    }, this);
  };

  /**
   * Arrange the cards around the current position
   * @memberOf TaskManager.prototype
   */
  TaskManager.prototype._placeCards = function() {
    this.stack.forEach(function(app, idx) {
      var card = this.cardsByAppID[app.instanceID];
      if (!card) {
        return;
      }

      card.move(0, 0);
      card.element.classList.toggle('current', (idx == this.position));
    }.bind(this));

    this._setAccessibilityAttributes();
  };

  /**
   * Get the current card front and center
   * @memberOf TaskManager.prototype
   */
  TaskManager.prototype.alignCurrentCard = function(duration, callback) {
    this._setupCardsTransition(duration || this.DURATION);
    this._placeCards();
  };

  /**
   * Adjust card positions by our current delta values
   * @memberOf TaskManager.prototype
   */
  TaskManager.prototype.moveCards = function() {
    var deltaX = this.deltaX;
    var sign = (deltaX > 0) ? -1 : 1;

    // Resistance at the extremities of the strip
    if (this.onExtremity()) {
      deltaX /= 1.5;
    }

    var current = this.position;
    this.stack.forEach(function(app, idx) {
      var card = this.cardsByAppID[app.instanceID];
      if (idx >= current - 2 && idx <= current + 2) {
        card.move(Math.abs(deltaX) * sign);
      }
    }, this);
  };

  /**
   * Check if the current gesture happens at an extremity
   * @memberOf TaskManager.prototype
   */
  TaskManager.prototype.onExtremity = function() {
    var sign = (this.deltaX > 0) ? -1 : 1;
    return (this.position === 0 && sign === 1 ||
            this.position === this.stack.length - 1 && sign === -1);
  };

  /**
   * @memberOf TaskManager.prototype
   * @param {DOMEvent} evt
   */
  TaskManager.prototype.onTouchMoveForDeleting = function(evt) {
    var dx = this.deltaX;
    var dy = this.deltaY;

    if (dy > 0) {
      var card = this.getCardForElement(evt.target);
      if (!card) {
        return;
      }

      if ('function' == typeof card.move) {
        card.move(dx, -dy);
      } else {
        card.applyStyle({
          transform: 'translateY(' + (-dy) + 'px)'
        });
      }
    }
  };

  /**
   * @memberOf TaskManager.prototype
   * @param {DOMEvent} evt
   */
  TaskManager.prototype.onTouchStart = function cs_onTouchStart(evt) {
    // If there is no card in the cardsView, go back to home screen
    if (this.element.classList.contains('empty')) {
      var homescreen = Service.query('getHomescreen', true);
      this.exitToApp(homescreen);
      return;
    }

    this._dragPhase = '';
    this.deltaX = 0;
    this.deltaY = 0;
    this.startTouchPosition = [evt.touches[0].pageX, evt.touches[0].pageY];
    this.startTouchDate = Date.now();
    this._resetCardsTransition();
  };

  /**
   * Handle end-of-drag events on the card list
   *
   * @memberOf TaskManager.prototype
   * @param  {DOMEvent} evt The event.
   */
  TaskManager.prototype.onTouchEnd = function cs_onTouchEnd(evt) {
    this.deltaX = evt.changedTouches[0].pageX - this.startTouchPosition[0];
    this.deltaY = evt.changedTouches[0].pageY - this.startTouchPosition[1];

    // Does the gesture is a swipe to delete a card ?
    if (this._dragPhase == 'cross-slide') {
      var element = evt.target;
      var card = this.getCardForElement(element);
      if (!card) {
        return;
      }

      if (-this.deltaY > this.SWIPE_UP_THRESHOLD && card.app.killable()) {
        card.killApp();
      } else {
        card.applyStyle({ transform: '' });
      }

      this.alignCurrentCard();
      return;
    }

    // Does the gesture is a tap ?
    if (Math.abs(this.deltaX) <= /* tap threshold */ 1) {
      this.handleTap(evt);
      return;
    }

    // The gesture is a simple swipe, move the target card at the center.
    var speed = this.deltaX / (Date.now() - this.startTouchDate);
    var inertia = speed * 250;
    var boosted = this.deltaX + inertia;
    var progress = Math.abs(boosted) / this.windowWidth;

    if (progress > 0.5) {
      progress -= 0.5;
    }

    var switching = Math.abs(boosted) >= this.SWITCH_CARD_THRESHOLD;
    if (switching) {
      if (this.deltaX < 0 &&
          this.position < this.cardsList.childNodes.length - 1) {
        this.position++;
      } else if (this.deltaX > 0 && this.position > 0) {
        this.position--;
      }
    }

    var durationLeft = Math.max(50, (1 - progress) * this.DURATION);
    this.alignCurrentCard(durationLeft);
  };


  /**
   * @memberOf TaskManager.prototype
   * @param {DOMEvent} evt
   */
  TaskManager.prototype.onTouchMove = function cs_onTouchMove(evt) {
    this.deltaX = this.startTouchPosition[0] - evt.touches[0].pageX;
    this.deltaY = this.startTouchPosition[1] - evt.touches[0].pageY;

    switch (this._dragPhase) {
      case '':
        if (this.deltaY > this.SWIPE_UP_THRESHOLD &&
            evt.target.classList.contains('card')) {
          // We don't want user to scroll the CardsView when one of the card is
          // already dragger upwards
          this._dragPhase = 'cross-slide';
          this.onTouchMoveForDeleting(evt);
        } else {
          // If we are not removing Cards now and Snapping Scrolling is enabled,
          // we want to scroll the CardList
          if (Math.abs(this.deltaX) > this.SWITCH_CARD_THRESHOLD) {
            this._dragPhase = 'scrolling';
          }

          this.moveCards();
        }
        break;

      case 'cross-slide':
        this.onTouchMoveForDeleting(evt);
        break;

      case 'scrolling':
        this.moveCards();
        break;
    }
  };

  TaskManager.prototype._setupCardsTransition = function(duration) {
    var position = this.position;

    var self = this;
    this.stack.forEach(function(app, idx) {
      var card = self.cardsByAppID[app.instanceID];

      if (idx < position - 2 || idx > position + 2) {
        card.element.style.visibility = 'hidden';
        return;
      }

      card.element.style.visibility = '';

      var style = { transition: 'transform ' + duration + 'ms linear'};
      card.applyStyle(style);
    });

  };

  TaskManager.prototype._resetCardsTransition = function() {
    var zeroTransitionStyle = { transition: '' };
    this.stack.forEach(function(app, idx) {
      var card = this.cardsByAppID[app.instanceID];
      card.applyStyle(zeroTransitionStyle);
    }, this);
  };

  exports.TaskManager = TaskManager;

  function debug(message) {
    if (DEBUG) {
      console.log('TaskManager > \n  ', message);
    }
  }
})(window);