Sabre Spark

Carousel

Create a Carousel

Option name Type Description
module components/carousel.js
(function(root, factory) {

  'use strict';

  if (typeof define === 'function' && define.amd) {
    define(['spark/components/base', 'spark/helpers/debounce', 'spark/helpers/transform'], factory);
  }
  else if (typeof module === 'object' && module.exports) {
    module.exports = factory(require('./base'), require('../helpers/debounce'), require('../helpers/transform'));
  }
  else {
    root.Spark = root.Spark || {};
    root.Spark.Carousel = factory(root.Spark.BaseComponent, root.Spark.debounce, root.Spark.transform);
  }
}(this, function(Base, Debounce, Transform) {

  'use strict';
Example
new Carousel(el);

Carousel constructor.

Option name Type Description
el Element
params Object
var Carousel = function(el, params) {

  params = params || {};

  if (!el) {
    return;
  }
  this._init(el);

};

Carousel.prototype = {

_setParams

property
 _setParams 

Include common functionality.

_setParams: Base.setParams,
_toggleClass: Base.toggleClass,
_addClass: Base.addClass,
_removeClass: Base.removeClass,
_hasClass: Base.hasClass,
_getElementMatchingParent: Base.getElementMatchingParent,
_debounce: Debounce,
_transform: Transform,
removeB: Base.remove,

defaults

property
 defaults 

Default values for internal properties we will be setting.
These are set on each construction so we don't leak properties
into the prototype chain.

Option name Type Description
defaults: {
  el: null
},

_whitelistedParams

property
 _whitelistedParams 

Whitelisted parameters which can be set on construction.

Option name Type Description
_whitelistedParams: [],

_carouselItem

method
 _carouselItem() 

Carousel Item Contructor, exposes access to functions setTransform, setSelected, and currentPosition

Option name Type Description
el Element

Node to initalize as carouselItem

parent Object

reference to the parent carousel

dot Element

Node to use as dot element

order Number

original order in the markup

_carouselItem: function(el, parent, dot, order) {
  var a = {};
  a.el = el;
  a.parent = parent;
  a.dot = dot;
  a.order = order;
  a.addTransform = function(x) {
    a.setTransform(a.transform.x + x);
  };
  a.setTransform = function(x) {
    x = x ? x : 0;
    a.transform = {
      'x': x
    };
    a.el.setAttribute('style', a.parent._transform('translate3d', a.transform.x + 'px, 0px, 0px'));
  };
  a.setSelected = function(b) {
    if (a.setSelected.selected === b && typeof a.setSelected.selected !== 'undefined') {
      return;
    }
    if (b) {
      a.parent._addClass(a.el, 'selected');
      a.parent._addClass(a.dot, 'selected');
      a.setSelected.selected = true;
    }
    else {
      a.parent._removeClass(a.el, 'selected');
      a.parent._removeClass(a.dot, 'selected');
      a.setSelected.selected = false;
    }
  };
  a.currentPosition = function() {
    return parent.transform.x + a.transform.x + a.dims.left + (a.width / 2) - parent.dims.left;
  };
  a.setTransform();
  a.dims = el.getBoundingClientRect();
  a.width = a.dims.width;
  a.el.sparkcarouselitem = a;
  a.dot.sparkcarouselitemdot = a;
  return a;
},

_init

method
 _init() 

Scans element and sets up or resets the carousel based on configuration

Option name Type Description
el Element

Node to initalize as the carousel

_init: function(el) {
  //cache elements and controls
  this.el = el;
  this.el.sparkcarousel = this;
  this.backe = this.el.querySelector('.spark-carousel__back');
  this.forwarde = this.el.querySelector('.spark-carousel__forward');
  this.outerContainer = this.el.querySelector('.spark-carousel__outer-container');
  this.containerMask = this.el.querySelector('.spark-carousel__container-mask');
  this.container = this.el.querySelector('.spark-carousel__container');
  this.dots = this.el.querySelector('.spark-carousel__dots');
  this.pauseEl = this.el.querySelector('.spark-carousel__pause');
  //get the options from the element
  this.opts = {};
  this.opts.wrapItems = this.el.attributes['data-spark-carousel-wrap-items'] ? true : false;
  this.opts.startingVelocity = this.el.attributes['data-spark-carousel-scroll-velocity'] ? this.el.attributes['data-spark-carousel-scroll-velocity'].value : 10;
  this.opts.smoothScroll = this.el.attributes['data-spark-carousel-smooth-scroll'] ? true : false;
  this.opts.smoothScrollCenterItems = this.el.attributes['data-spark-carousel-smooth-scroll-center'] ? true : false;
  this.opts.panelScroll = this.el.attributes['data-spark-carousel-panel'] ? true : false;
  this.opts.edgeScroll = this.opts.panelScroll || this.el.attributes['data-spark-carousel-edge'] ? true : false;
  this.opts.autoAdvance = this.el.attributes['data-spark-carousel-auto-advance'] ? this.el.attributes['data-spark-carousel-auto-advance'].value : false;
  //setup autoAdvance
  if (this.opts.autoAdvance && !this.autoAdvance && !this.pause) {
    this.autoAdvance = window.setTimeout(function() {
      this._autoAdvance();
    }.bind(this), this.opts.autoAdvance * 1000);
  }
  //conditionally bind pause handlers.
  //needs to be done here so that resetting the carousel will work correctly
  if (this.opts.autoAdvance && !this.pauseH) {
    this.pauseH = this._pause.bind(this);
    this.pauseEl.addEventListener('click', this.pauseH);
  }
  //collect items and cache sizing
  var a = this.el.querySelectorAll('.spark-carousel__item');
  this.items = [];
  this.totalItemWidth = 0;
  this.dims = this.containerMask.getBoundingClientRect();
  this.width = this.dims.width;
  this.height = this.dims.height;
  var dots = document.createDocumentFragment();
  var b;
  //create our carouselItems
  for (var i = 0; i < a.length; i++) {
    b = document.createElement('div');
    dots.appendChild(b);
    this.items.push(new this._carouselItem(a[i], this, b, i));
    this.totalItemWidth += this.items[i].width;
  }
  //if we're resetting we need to empty out the exisiting elements first
  while (this.dots.firstChild) {
    this.dots.removeChild(this.dots.firstChild);
  }
  this.dots.appendChild(dots);
  //this is to test if we're dealing with 2011 flexbox (IE10) and need to do an adjustment
  //this is because ms-flex-pack: center doesn't work like 2012 flexbox center-pack
  if (typeof this.container.style.msFlexAlign !== 'undefined') {
    this._transformItems(-(this.totalItemWidth - this.width) / 2);
  }
  //setup inital transform
  this._setTransform();
  //need to bind this event handler here as we are always going to need to be listening
  //for this event, in order to react to container visibility changing
  this._handleVisibleChildren = this._handleVisibleChildrenH.bind(this);
  document.addEventListener('spark.visible-children', this._handleVisibleChildren, true);
  if (!this._rafHandler) {
    this._rafHandler = this._rafHandlerH.bind(this);
  }
  //need to also listen to resize events, even if we don't have items overflowing
  if (!this.resizeH) {
    this.resizeH = this._debounce(this._resize.bind(this), 100);
    window.addEventListener('resize', this.resizeH);
  }
  //if we haven't already init'd event listerers, and we have items overflowing
  if (this.totalItemWidth > this.width) {
    if (!this.touchstartH) {
      this._removeClass(this.el, 'noscroll');
      this._setupListeners();
      //center the first item
      this._addTransform(-this.items[0].currentPosition() + (this.width / 2));
    }
  }
  //if we don't have overflowing items, then disable scrolling and remove listeners
  else {
    this._addClass(this.el, 'noscroll');
    this._removeListeners();
  }
  //set the new selected item
  this._updateSelected();
  //finally, display the element
  this._addClass(this.el, 'ready');
},

remove

method
 remove() 

Tears down the component, removes listeners, and conditionally delete the DOM element

Option name Type Description
leaveElement Boolean

Falsey value will remove the DOM element as well as the component instance

remove: function(leaveElement) {
  window.removeEventListener('resize', this.resizeH);
  delete this.resizeH;
  document.removeEventListener('spark.visible-children', this._handleVisibleChildren, true);
  delete this._handleVisibleChildren;
  this.removeB(leaveElement);
},

_pause

method
 _pause() 

Pause/unpause the autoAdvance feature

_pause: function() {
  if (this.pause) {
    delete this.moves;
    this.autoAdvance = window.setTimeout(function() {
      this._autoAdvance();
    }.bind(this), this.opts.autoAdvance * 1000);
  }
  else {
    this._rafCancel();
    if (!this.opts.smoothScroll || this.opts.smoothScrollCenterItems) {
      this._scrollTo(this._selectedItem());
    }
    window.clearTimeout(this.autoAdvance);
    delete this.autoAdvance;
  }
  this._setPause(!this.pause);
},

_setPause

method
 _setPause() 

toggles the pause class on the element

Option name Type Description
b Boolean

Truthy value will give the element the pause class

_setPause: function(b) {
  if (typeof b === 'undefined') {
    this.pause = typeof this.pause === 'undefined' ? false : this.pause;
    return this.pause;
  }
  else {
    if (b) {
      this._addClass(this.el, 'pause');
    }
    else {
      this._removeClass(this.el, 'pause');
    }
    this.pause = b;
    return this.pause;
  }
},

_autoAdvance

method
 _autoAdvance() 

function called by window.setTimeout, will check first to see if element is in use before triggering a slide advance

_autoAdvance: function() {
  if (!this.moves && !this._laststart && !this.paused) {
    this._rafCancel();
    var a = this.items.indexOf(this._selectedItem());
    this._scrollToItem = true;
    this._scrollTo(this.items[a === this.items.length - 1 ? 0 : a + 1]);
    this.autoAdvance = window.setTimeout(function() {
      this._autoAdvance();
    }.bind(this), this.opts.autoAdvance * 1000);
  }
},

_setupListeners

method
 _setupListeners() 

initalize and bind even listeners

_setupListeners: function() {
  this.touchstartH = this._touchstart.bind(this);
  this.container.addEventListener('touchstart', this.touchstartH);
  this.touchmoveH = this._touchmove.bind(this);
  window.addEventListener('touchmove', this.touchmoveH);
  this.touchendH = this._touchend.bind(this);
  window.addEventListener('touchend', this.touchendH);
  this.mousedownH = this._mousedown.bind(this);
  this.container.addEventListener('mousedown', this.mousedownH);
  this.mousemoveH = this._mousemove.bind(this);
  window.addEventListener('mousemove', this.mousemoveH);
  this.mouseupH = this._mouseup.bind(this);
  window.addEventListener('mouseup', this.mouseupH);
  this.forwardH = this._forward.bind(this);
  this.forwarde.addEventListener('click', this.forwardH);
  this.backH = this._back.bind(this);
  this.backe.addEventListener('click', this.backH);
  this.clickH = this._click.bind(this);
  this.el.addEventListener('click', this.clickH);
  this._focusHandler = this._scrollToClicked.bind(this);
  this.container.addEventListener('focus', this._focusHandler, true);
},

_removeListeners

method
 _removeListeners() 

Removes non-essential event listeners, called when tearing down the component, or our content
does not exceed the width of our element

_removeListeners: function() {
  this.el.removeEventListener('touchstart', this.touchstartH);
  delete this.touchstartH;
  window.removeEventListener('touchmove', this.touchmoveH);
  delete this.touchmoveH;
  window.removeEventListener('touchend', this.touchendH);
  delete this.touchendH;
  this.el.removeEventListener('mousedown', this.mousedownH);
  delete this.mousedownH;
  window.removeEventListener('mousemove', this.mousemoveH);
  delete this.mousemoveH;
  window.removeEventListener('mouseup', this.mouseupH);
  delete this.mouseupH;
  this.forwarde.removeEventListener('click', this.forwardH);
  delete this.forwardH;
  this.backe.removeEventListener('click', this.backH);
  delete this.backH;
  this.el.removeEventListener('click', this.clickH);
  delete this.clickH;
  this.container.removeEventListener('focus', this._focusHandler, true);
  delete this._focusHandler;
  if (this.pauseEl) {
    this.pauseEl.removeEventListener('click', this.pauseH);
    delete this.pauseH;
  }
},

_handleVisibleChildrenH

method
 _handleVisibleChildrenH() 

Event handler for the spark.visible-children event, just call the change function to handle any
visibility or sizing changes

Option name Type Description
e Event

The spark.visible-children event

_handleVisibleChildrenH: function(e) {
  if (e.target.contains(this.el)) {
    window.setTimeout(function() {
      this.change();
    }.bind(this), 0);
  }
},

_forward

method
 _forward() 

Forward button click handler triggers a scrollTo to the "next" element

Option name Type Description
e Event

The click event

_forward: function(e) {
  var s = this.items.indexOf(this._selectedItem());
  s++;
  if (s > this.items.length - 1) {
    if (this.opts.wrapItems) {
      s = 0;
    }
    else {
      s--;
    }
  }
  delete this.moves;
  var a = this._startingVelocity;
  a = a < -this.opts.startingVelocity ? a : -this.opts.startingVelocity;
  this._rafCancel();
  this._scrollToItem = true;
  this._scrollTo(this.items[s], a);
  if (e) {
    e.preventDefault();
  }
},

_back

method
 _back() 

Back button click handler triggers a scrollTo to the "previous" element

Option name Type Description
e Event

The click event

_back: function(e) {
  var s = this.items.indexOf(this._selectedItem());
  s--;
  if (s < 0) {
    if (this.opts.wrapItems) {
      s = this.items.length - 1;
    }
    else {
      s++;
    }
  }
  delete this.moves;
  var a = this._startingVelocity;
  a = a > this.opts.startingVelocity ? a : this.opts.startingVelocity;
  this._rafCancel();
  this._scrollToItem = true;
  this._scrollTo(this.items[s], a);
  if (e) {
    e.preventDefault();
  }
},

_movestart

method
 _movestart() 

Move start handler, handles both touchstart and mousedown events

Option name Type Description
e Object

The start event

_movestart: function(e) {
  this._rafCancel();
  this.moves = [];
  this.moves.push(e);
},

_move

method
 _move() 

Move handler, handles internal move event objects

Option name Type Description
e Object

The move event

_move: function(e) {
  if (this.moves && this.moves.length > 1) {
    this._addTransform(e.pageX - this.moves[this.moves.length - 1].pageX);
    this.moves.push(e);
    e.preventDefault = true;
  }
  else {
    if (this.moves && this.moves[0]) {
      if (Math.abs(this.moves[0].pageX - e.pageX) > Math.abs(this.moves[0].pageY - e.pageY) && Math.abs(this.moves[0].pageX - e.pageX) > 5 && e.cancelable) {
        this._addTransform(e.pageX - this.moves[0].pageX);
        this.moves.push(e);
        e.preventDefault = true;
      }
      else {
        if (Math.abs(this.moves[0].pageX - e.pageX) > 5) {
          this.moves[0] = e;
        }
      }
    }
    if (e.type === 'touchend' || e.type === 'mouseup') {
      delete this.moves;
    }
  }
},

_moveend

method
 _moveend() 

Move end handler, handles both touchend and mouseup events

Option name Type Description
e Object

The moveend event

_moveend: function(e) {
  this._move(e);
  if (!this.opts.smoothScroll) {
    this._settle(this.moves);
  }
  else {
    this._interiaScroll(this.moves);
  }
  return e;
},

_resize

method
 _resize() 

Resize event handler, calls change to handle any element dimension changes

_resize: function() {
  this.change();
},

change

method
 change() 

Calling the change function will handle updating the element to take into account
any styling, sizing, or visibility changes, and the addition or removal of any carouselItems

change: function() {
  var dims = this.el.getBoundingClientRect();
  if (dims.width !== this.width || dims.height !== this.height) {
    if (this.autoAdvance) {
      window.clearTimeout(this.autoAdvance);
      delete this.autoAdvance;
    }
    this._rafCancel();
    var c = this._selectedItem();
    this._setTransform(0);
    this._setTransformItems(0);
    this._init(this.el);
    if (this.items.indexOf(c.el.sparkcarouselitem) > -1 && this.totalItemWidth > this.width) {
      if (this.opts.wrapItems) {
        this._addTransform(-this.totalItemWidth + (-c.el.sparkcarouselitem.currentPosition() + (this.width / 2)));
      }
      else {
        this._addTransform(-c.el.sparkcarouselitem.currentPosition() + (this.width / 2));
      }
    }
  }
},

_touchstart

method
 _touchstart() 

Touchstart event handler, passes necessary data points to the movestart function

Option name Type Description
e Object

The touchstart event

_touchstart: function(e) {
  var a = {
    'type': e.type,
    'pageX': e.touches[0].pageX,
    'pageY': e.touches[0].pageY,
    'timeStamp': e.timeStamp
  };
  this._movestart(a);
},

_touchmove

method
 _touchmove() 

Touchmove event handler, passes necessary data points to the move function

Option name Type Description
e Object

The touchmove event

_touchmove: function(e) {
  var a = {
    'type': e.type,
    'pageX': e.touches[0].pageX,
    'pageY': e.touches[0].pageY,
    'timeStamp': e.timeStamp,
    'cancelable': e.cancelable
  };
  this._move(a);
  if (a.preventDefault) {
    e.preventDefault();
  }
},

_touchend

method
 _touchend() 

Touchend event handler, passes necessary data points to the moveend function

Option name Type Description
e Object

The touchend event

_touchend: function(e) {
  if (this.moves && this.moves.length > 2 && e.cancelable) {
    var a = {
      'type': e.type,
      'pageX': this.moves[this.moves.length - 1].pageX,
      'pageY': this.moves[this.moves.length - 1].pageY,
      'timeStamp': e.timeStamp
    };
    this._moveend(a);
  } else {
    delete this.moves;
  }
},

_mousedown

method
 _mousedown() 

Mousedown event handler, passes necessary data points to the movestart function

Option name Type Description
e Object

The mousedown event

_mousedown: function(e) {
  if (e.button !== 0) {
    return e;
  }
  this.isMouseDown = true;
  var a = {
    'type': e.type,
    'pageX': e.pageX,
    'pageY': e.pageY,
    'timeStamp': e.timeStamp
  };
  this._movestart(a);
  e.preventDefault();
},

_mousemove

method
 _mousemove() 

Mousemove event handler, passes necessary data points to the move function

Option name Type Description
e Object

The mousemove event

_mousemove: function(e) {
  if (this.isMouseDown) {
    var a = {
      'type': e.type,
      'pageX': e.clientX,
      'pageY': e.clientY,
      'timeStamp': e.timeStamp,
      //this was changed to correct an issue in safari - it doesn't report cancelable correctly
      'cancelable': true
    };
    this._move(a);
    if (a.preventDefault) {
      e.preventDefault();
    }
  }
},

_mouseup

method
 _mouseup() 

Mouseup event handler, passes necessary data points to the moveend function

Option name Type Description
e Object

The mouseup event

_mouseup: function(e) {
  if (this.moves && this.moves.length > 2) {
    var a = {
      'type': e.type,
      'pageX': e.pageX,
      'pageY': e.pageY,
      'timeStamp': e.timeStamp
    };
    this._moveend(a);
    this.mouseUpHandled = true;
  }
  else {
    delete this.moves;
    this._scrollToClicked(e);
  }
  this.isMouseDown = false;
},

_click

method
 _click() 

Click event handler

Option name Type Description
e Object

The click event

_click: function(e) {
  //if we are already tracking moves, then this will be handled by the mouseend event handler and we should prevent the default action
  if (this.moves) {
    e.preventDefault();
  }
  //if it has already been handled by mouseup handler, prevent the action
  if (this.mouseUpHandled) {
    e.preventDefault();
  }
  //reset our handled state
  delete this.mouseUpHandled;
  //checking both this.moves and this.mouseUpHandled ensures we capture events correctly in all browsers, where the order of the mouseup/click events can vary
},

_velocity

method
 _velocity() 

Calculate the user's recent cursor/finger velocity

Option name Type Description
moves Array

The array of cursor positions

_velocity: function(moves) {
  var avg = 0;
  var m = Math.min(6, moves.length - 1);
  for (var i = 1; i < m; i++) {
    if (moves[moves.length - i].timeStamp === moves[moves.length - i - 1].timeStamp) {
      avg += avg / i;
    }
    else {
      avg += (10 * (moves[moves.length - i].pageX - moves[moves.length - i - 1].pageX) / (moves[moves.length - i].timeStamp - moves[moves.length - i - 1].timeStamp)) / m;
    }
  }
  return avg;
},

_scrollToClicked

method
 _scrollToClicked() 

Handles click events on items and dots, scrolling to the clicked item

Option name Type Description
e Event

The click event

_scrollToClicked: function(e) {
  var tar = e.target;
  if (this.el.contains(tar)) {
    while (!tar.sparkcarousel) {
      if (tar.sparkcarouselitem) {
        this.containerMask.scrollLeft = 0;
        delete this.moves;
        this._rafCancel();
        this._scrollTo(tar.sparkcarouselitem);
        e.preventDefault();
        break;
      }
      if (tar.sparkcarouselitemdot) {
        this.containerMask.scrollLeft = 0;
        delete this.moves;
        this._rafCancel();
        var v = tar.sparkcarouselitemdot.order < this._selectedItem().order ? this.opts.startingVelocity : -this.opts.startingVelocity;
        this._scrollTo(tar.sparkcarouselitemdot, v);
        e.preventDefault();
        break;
      }
      tar = tar.parentNode;
    }
  }
},

_scrollTo

method
 _scrollTo() 

Scroll to the carouselItem, with specified startingVelocity, auto determines default velocity if not specified

Option name Type Description
item Object

The carouselItem to scroll to

startingVelocity Number

The startingVelocity of the scroll animation

_scrollTo: function(item, startingVelocity) {
  var offset = this.width / 2;
  var currentPosition = item.currentPosition();
  if (!startingVelocity) {
    startingVelocity = offset - item.currentPosition() > 0 ? this.opts.startingVelocity : -this.opts.startingVelocity;
  }
  if (this.opts.wrapItems) {
    if (startingVelocity > 0) {
      //left
      if (currentPosition > offset) {
        this._totalDistance = offset + this.totalItemWidth - currentPosition;
      }
      else {
        this._totalDistance = offset - currentPosition;
      }
    }
    else {
      //right
      if (currentPosition < offset) {
        this._totalDistance = -(this.totalItemWidth + currentPosition - offset);
      }
      else {
        this._totalDistance = offset - currentPosition;
      }
    }
  }
  else {
    this._totalDistance = offset - currentPosition;
  }
  this._startingVelocity = startingVelocity;
  delete this.moves;
  this._scrollToItem = true;
  this._raf = window.requestAnimationFrame(this._rafHandler);
},

_rafHandlerH

method
 _rafHandlerH() 

This is the animator function, it examines the options set on the carousel object
and selectively adds transform and requests addtional animation frames if necesary

Option name Type Description
t Number

The timestamp for the current animation frame

_rafHandlerH: function(t) {
  if (this.opts.autoAdvance && this.autoAdvance) {
    window.clearTimeout(this.autoAdvance);
    delete this.autoAdvance;
  }
  var frames;
  if (this.moves || !this._startingVelocity) {
    this._rafCancel();
    return;
  }
  if (!this._laststart) {
    this._laststart = t;
  }
  if (!this._remainingDistance) {
    this._remainingDistance = this._totalDistance;
  }
  if (!this._lastframe) {
    this._lastframe = t;
    frames = 1;
  }
  else {
    frames = (t - this._lastframe) / ((1 / 60) * 1000);
  }
  var d = this._startingVelocity * frames;
  if (this.opts.smoothScroll && !this._scrollToItem) {
    this._addTransform(d);
    this._startingVelocity *= Math.pow(0.97, frames);
    if (this.opts.smoothScrollCenterItems && Math.abs(this._startingVelocity) < 1) {
      this._scrollTo(this._selectedItem());
    }
    if (Math.abs(this._startingVelocity) < 0.5) {
      if ((this._startingVelocity > 0 && this.transform.x > ((this.totalItemWidth / 2) - (this.items[0].width / 2))) ||
        (this._startingVelocity < 0 && this.transform.x < (-((this.totalItemWidth / 2) - (this.items[this.items.length - 1].width / 2))))) {
        this._scrollToItem = true;
        this._scrollTo(this._selectedItem());
      }
      else {
        this._rafCancel();
      }
    }
    else {
      this._raf = window.requestAnimationFrame(this._rafHandler);
    }
  }
  else {
    if (this._startingVelocity > 0) {
      if (d < this._remainingDistance) {
        this._addTransform(d);
        this._remainingDistance -= d;
        if (this._remainingDistance > this._totalDistance / 2) {
          this._startingVelocity *= Math.pow(1.15, frames);
        }
        else {
          this._startingVelocity *= Math.pow(0.9, frames);
          this._startingVelocity = this._startingVelocity > 2 ? this._startingVelocity : 2;
        }
        this._raf = window.requestAnimationFrame(this._rafHandler);
      }
      else {
        this._addTransform(this._remainingDistance);
        this._rafCancel();
      }
    }
    else {
      if (d > this._remainingDistance) {
        this._addTransform(d);
        this._remainingDistance -= d;
        if (this._remainingDistance < this._totalDistance / 2) {
          this._startingVelocity *= Math.pow(1.15, frames);
        }
        else {
          this._startingVelocity *= Math.pow(0.9, frames);
          this._startingVelocity = this._startingVelocity < -2 ? this._startingVelocity : -2;
        }
        this._raf = window.requestAnimationFrame(this._rafHandler);
      }
      else {
        this._addTransform(this._remainingDistance);
        this._rafCancel();
      }
    }
  }
  this._lastframe = t;
},

_rafCancel

method
 _rafCancel() 

This is the animator clearing function
it clears values used during animation, and selectively enables autoAdvance

_rafCancel: function() {
  if (this.opts.autoAdvance && !this.autoAdvance && !this.pause) {
    this.autoAdvance = window.setTimeout(function() {
      this._autoAdvance();
    }.bind(this), this.opts.autoAdvance * 1000);
  }
  window.cancelAnimationFrame(this._raf);
  delete this._scrollToItem;
  delete this._laststart;
  delete this._startingVelocity;
  delete this._remainingDistance;
  delete this._totalDistance;
  delete this._lastframe;
},

_interiaScroll

method
 _interiaScroll() 

This computes values necessary to start an animation frame when the carousel is
configured to use smoothScroll

Option name Type Description
moves Array

The captured move events

_interiaScroll: function(moves) {
  if (moves[moves.length - 1].timeStamp - moves[moves.length - 2].timeStamp > 100 || moves.length < 3) {
    if(this.opts.smoothScrollCenterItems) {
      return this._scrollTo(this._selectedItem());
    }
    return;
  }
  this._startingVelocity = this._velocity(moves);
  delete this.moves;
  this._raf = window.requestAnimationFrame(this._rafHandler);
},

_settle

method
 _settle() 

This determines which carousel item should be focused based on the previous moves
made by the user

Option name Type Description
moves Array

The captured move events

_settle: function(moves) {
  if (moves && moves.length > 3) {
    if (moves[moves.length - 1].timeStamp - moves[moves.length - 2].timeStamp > 80) {
      return this._scrollTo(this._selectedItem());
    }
    var v1 = 10 * (moves[moves.length - 3].pageX - moves[moves.length - 4].pageX) / (moves[moves.length - 3].timeStamp - moves[moves.length - 4].timeStamp);
    var v2 = 10 * (moves[moves.length - 2].pageX - moves[moves.length - 3].pageX) / (moves[moves.length - 2].timeStamp - moves[moves.length - 3].timeStamp);
    if (Math.abs(v1) < Math.abs(v2) || Math.abs(v2) > 0.5 && Math.abs(v2) > 0.5) {
      //user is probably trying to go to next or prev item
      var s = this.items.indexOf(this._selectedItem());
      if (v2 > 0) {
        //prev
        if (s > 0) {
          this._scrollTo(this.items[s - 1], v2);
        }
        else {
          if (this.opts.wrapItems) {
            this._scrollTo(this.items[this.items.length - 1], v2);
          }
          else {
            this._scrollTo(this.items[0]);
          }
        }
      }
      else {
        //next
        if (s < this.items.length - 1) {
          this._scrollTo(this.items[s + 1], v2);
        }
        else {
          if (this.opts.wrapItems) {
            this._scrollTo(this.items[0], v2);
          }
          else {
            this._scrollTo(this.items[this.items.length - 1]);
          }
        }
      }
    }
    else {
      if (this._selectedItem().currentPosition() > this.width / 2) {
        this._scrollTo(this._selectedItem(), -this.opts.startingVelocity);
      }
      else {
        this._scrollTo(this._selectedItem(), this.opts.startingVelocity);
      }
    }
  }
},

_transformItems

method
 _transformItems() 

Transforms the position of all carouselItems

Option name Type Description
x Number

The pixel value to transform

_transformItems: function(x) {
  for (var i = 0; i < this.items.length; i++) {
    this.items[i].addTransform(x);
  }
},

_setTransformItems

method
 _setTransformItems() 

Sets the transform position of all carouselItems

Option name Type Description
x Number

The pixel value to transform

_setTransformItems: function(x) {
  for (var i = 0; i < this.items.length; i++) {
    this.items[i].setTransform(x);
  }
},

_addTransform

method
 _addTransform() 

Adds transform to the container element, does checking for bounds conditions and
wraps items if necessary and configured

Option name Type Description
x Number

The pixel value to transform

_addTransform: function(x) {
  var a;
  if ((this.opts.smoothScrollCenterItems || !this.opts.smoothScroll) && !this.opts.wrapItems && !this.opts.edgeScroll) {
    var l = this.items.indexOf(this.selectedItem);
    if (l === this.items.length - 1) {
      this._leftbound(true);
    }
    else {
      this._leftbound(false);
    }
    if (l === 0) {
      this._rightbound(true);
    }
    else {
      this._rightbound(false);
    }
  }
  else {
    this._leftbound(false);
    this._rightbound(false);
  }
  if (this.transform.x + x < 0 && x < 0) {
    if (this.opts.wrapItems) {
      //wrap items until we have covered the visible area
      while (this.transform.x + x < -(this.totalItemWidth - this.width) / 2 && (this.totalItemWidth < this.width ? this.transform.x + x < -this.totalItemWidth / 2 : true)) {
        a = this.items.shift();
        this.items.push(a);
        a.addTransform(this.totalItemWidth);
        this._transformItems(-a.width);
        x += a.width;
      }
    }
    else {
      //there is a 1 pixel adjustment to account for some math rounding
      if (this.opts.edgeScroll && x < 0 && this.transform.x + x - 1 <= -(this.totalItemWidth - this.width) / 2) {
        this._leftbound(true);
        return this._setTransform(-(this.totalItemWidth - this.width) / 2);
      }
      //progressively reduce scrolling when no more items to the right
      if (x < 0 && (this.transform.x + x) < (-((this.totalItemWidth / 2) - (this.items[this.items.length - 1].width / 2)))) {
        x = x * (((this.totalItemWidth / 2) + (this.items[this.items.length - 1].width / 2) + (this.transform.x + x)) / this.items[this.items.length - 1].width);
        x = x > 0 ? 0 : x;
      }
    }
    return this._setTransform(this.transform.x + x);
  }
  else {
    if (this.transform.x + x > 0 && x > 0) {
      if (this.opts.wrapItems) {
        //wrap items until we have covered the visible area
        while (this.transform.x + x > -(this.width - this.totalItemWidth) / 2 && (this.totalItemWidth < this.width ? this.transform.x + x > this.totalItemWidth / 2 : true)) {
          a = this.items.pop();
          this.items.unshift(a);
          a.addTransform(-this.totalItemWidth);
          this._transformItems(a.width);
          x -= a.width;
        }
      }
      else {
        //there is a 1 pixel adjustment to account for some math rounding
        if (this.opts.edgeScroll && x > 0 && this.transform.x + x + 1 >= (this.totalItemWidth - this.width) / 2) {
          this._rightbound(true);
          return this._setTransform((this.totalItemWidth - this.width) / 2);
        }
        //progressively reduce scrolling when no more items to the left
        if (x > 0 && (this.transform.x + x) > ((this.totalItemWidth / 2) - (this.items[0].width / 2))) {
          x = x * (((this.totalItemWidth / 2) + (this.items[0].width / 2) - (this.transform.x + x)) / this.items[0].width);
          x = x < 0 ? 0 : x;
        }
      }
    }
    return this._setTransform(this.transform.x + x);
  }
},

_leftbound

method
 _leftbound() 

Sets the leftbound class

Option name Type Description
b Boolean

Set or unset the leftbound class

_leftbound: function(b) {
  if (typeof b === 'undefined') {
    this.leftbound = typeof this.leftbound === 'undefined' ? false : this.leftbound;
    return this.leftbound;
  }
  else {
    if (b) {
      this._addClass(this.el, 'leftbound');
    }
    else {
      this._removeClass(this.el, 'leftbound');
    }
    this.leftbound = b;
    return this.leftbound;
  }
},

_rightbound

method
 _rightbound() 

Sets the rightbound class

Option name Type Description
b Boolean

Set or unset the rightbound class

_rightbound: function(b) {
  if (typeof b === 'undefined') {
    this.rightbound = typeof this.rightbound === 'undefined' ? false : this.rightbound;
    return this.rightbound;
  }
  else {
    if (b) {
      this._addClass(this.el, 'rightbound');
    }
    else {
      this._removeClass(this.el, 'rightbound');
    }
    this.rightbound = b;
    return this.rightbound;
  }
},

_updateSelected

method
 _updateSelected() 

Updates the selected item, by seeing which item has its center closest
to the center of the carousel

_updateSelected: function() {
  var tar = this.width / 2;
  var i = -1;
  var a = 1;
  var b = 0;
  while (a > b) {
    i++;
    if (i > this.items.length - 2) {
      break;
    }
    a = Math.abs(tar - this.items[i].currentPosition());
    b = Math.abs(tar - this.items[i + 1].currentPosition());
  }
  return this._selectedItem(this.items[i]);
},

_selectedItem

method
 _selectedItem() 

Stores the selected item for the carousel, and updates the previously
selected item and newly selected item to have the correct states
Conditionally sets the leftbound/rightbound states depending on configuration

Option name Type Description
item Object

Optional: the new item select, if omitted it will return the currently selected item.

_selectedItem: function(item) {
  if (typeof item !== 'object') {
    if (this.selectedItem) {
      return this.selectedItem;
    }
    else {
      return this._updateSelected();
    }
  }
  else {
    if (this.selectedItem) {
      this.selectedItem.setSelected(false);
    }
    this.selectedItem = item;
    if ((this.opts.smoothScrollCenterItems || !this.opts.smoothScroll) && !this.opts.wrapItems && !this.opts.edgeScroll) {
      var l = this.items.indexOf(this.selectedItem);
      if (l === this.items.length - 1) {
        this._leftbound(true);
      }
      else {
        this._leftbound(false);
      }
      if (l === 0) {
        this._rightbound(true);
      }
      else {
        this._rightbound(false);
      }
    }
    this.selectedItem.setSelected(true);
  }
},

_setTransform

method
 _setTransform() 

Sets the transform for the carousel container

Option name Type Description
x Number

The pixel value to transform

_setTransform: function(x) {
  x = x ? x : 0;
  this.transform = {
    'x': x
  };
  this.container.setAttribute('style', this._transform('translate3d', x + 'px, 0px, 0px'));
  this._updateSelected();
  return x;
}
  };
  Base.exportjQuery(Carousel, 'Carousel');

  return Carousel;
}));