Slider

function
 Slider() 

Option name Type Description
el Element
params Object

Slider constructor.

var Slider = function(el, params) {

  if (!el) {
    return;
  }

  this._setParams(this.defaults, true);
  this._cacheElements(el);
  this._setParams(params || {});
  this._bindEventListenerCallbacks();
  this._addEventListeners();
};

Slider.prototype = {

_setParams

property
 _setParams 

Include common functionality.

_setParams: Base.setParams,
_toggleClass: Base.toggleClass,
_getElementOffset: Base.getElementOffset,
_elementHasParent: Base.elementHasParent,

_whitelistedParams

property
 _whitelistedParams 

Whitelisted parameters which can be set on construction.

_whitelistedParams: ['isX', 'onChange', 'onWillChange'],

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.

defaults: {
  el: null,
  controlsEl: null,
  inputEl: null,
  handleEl: null,
  trackEl: null,
  trackFillEl: null,
  isActive: false,
  isX: true,
  onChange: null,
  onWillChange: null,
  position: 0,
  width: 0,
  height: 0,
  min: 0,
  max: 0,
  step: 1,
  value: 0,
  percentage: 0,
  offsetLeft: 0,
  offsetTop: 0,
  handleSizePercentage: 0,
  truncatePrecision: 0,
  truncateCharacters: ['k', 'm', 'b', 't'],
  _oldVal: null,
  _onTouchStartBound: null,
  _onTouchMoveBound: null,
  _onTouchEndBound: null,
  _onMouseDownBound: null,
  _onMouseMoveBound: null,
  _onMouseUpBound: null,
  _onMouseOutBound: null,
  _onFocusBound: null,
  _onKeydownBound: null,
  _onBlurBound: null,
  _onChangeBound: null,
  _onResizeBound: null,
  _onClickBound: null
},

start

method
 start() 

Option name Type Description
position Number

The position of the pointer.

type String

Optional Which type of events to listen for.

Start the slider moving.

start: function(position, type) {

  if (this.inputEl.getAttribute('disabled') !== null) {
    return;
  }

  this._addMoveEventListeners(type || 'mouse');
  this._cacheSize();
  this.isActive = true;
  this._updateActiveClasses();
  this._oldVal = this.value;
  this.move(position);
},

move

method
 move() 

Option name Type Description
position Number
force Boolean

Force the move Optional

Move the value to a given position

move: function(position, force) {

  // Noop if we haven't yet started dragging
  if ((!position || !this.isActive) && !force) {
    return;
  }

  // Treat positions beyond the boundaries as the boundaries
  if (this.isX) {

    // Too far left
    if (position < this.offsetLeft) {
      position = this.offsetLeft;
    }
    // Too far right
    else if (position > this.offsetLeft + this.width) {
      position = this.offsetLeft + this.width;
    }
  } else {

    // Too far top
    if (position < this.offsetTop) {
      position = this.offsetTop;
    }
    // Too far bottom
    else if (position > this.offsetTop + this.height) {
      position = this.offsetTop + this.height;
    }
  }

  // The percentage of the new position relative to slider-container width or height.
  var percentage = this.isX ? (position - this.offsetLeft) / (this.width - this.handleSize) : (position - this.offsetTop) / this.height;

  // The value of the input as a percentage of the value range.
  this.setValue(Math.round((percentage - this.handleSizePercentage / 2) * (this.max - this.min)) + this.min);
},

stop

method
 stop() 

Option name Type Description
type String

Optional Which type of events to listen for.

Stop listening to movements.

stop: function(type) {
  this.isActive = false;
  if(this._oldVal !== this.value) {
    (this.onChange || noop)(this, this.value);
  }
  this._updateActiveClasses();
  this._removeMoveEventListeners(type || 'mouse');
},

setValue

method
 setValue() 

Option name Type Description
value Number

Set the value of the handle.

setValue: function(value) {

  // Move in increments if we have a defined step size
  if (this.step) {
    value = value - (value % this.step);
  }

  // Check bounds of the new value
  if (value > this.max) {
    value = this.max;
  } else if (value < this.min) {
    value = this.min;
  }

  // If there is an onWillChange callback, run it. If it returns
  // false, then this new value should be considered invalid.
  if (typeof this.onWillChange === 'function') {
    var change = this.onWillChange(this, value);
    if (typeof change === 'number') {
      value = change;
    }
  }

  // Store value
  this.value = value;

  // Update elements
  this.inputEl.value = this.value;
  this.handleEl.setAttribute('data-value', this._truncateValueText(this.value));

  // Set the percentage
  this.percentage = (this.value - this.min) / (this.max - this.min);

  // Update the position of the handle
  this._updateHandlePosition();
},

increment

method
 increment() 

Option name Type Description
useMultiplier Boolean

Optional Increment by a multiplied version of the step

Increment the value by the step size.

increment: function(useMultiplier) {
  this.setValue(this.value + this.step * (useMultiplier ? 10 : 1));
},

decrement

method
 decrement() 

Option name Type Description
useMultiplier Boolean

Optional Increment by a multiplied version of the step

Decrement the value by the step size.

decrement: function(useMultiplier) {
  this.setValue(this.value - this.step * (useMultiplier ? 10 : 1));
},

remove

method
 remove() 

Option name Type Description
leaveElement Boolean

Leave the element intact.

Remove the element from the DOM and prepare for garbage collection by dereferencing values.

remove: function(leaveElement) {
  this._removeMoveEventListeners('touch');
  this._removeMoveEventListeners('mouse');
  this._removeMoveEventListeners('keyboard');
  Base.remove.call(this, leaveElement);
},

_cacheElements

method
 _cacheElements() 

Option name Type Description
el Element

Store a reference to the whole slider, as well as the
input element. Also, get some default values from the input
element (min, max, steps).

_cacheElements: function(el) {

  this.el = el;
  this.controlsEl = this.el.querySelector('.spark-slider__controls');
  this.inputEl = this.el.querySelector('input[type="number"]');
  this.handleEl = this.el.querySelector('.spark-slider__handle');
  this.trackEl = this.el.querySelector('.spark-slider__track');
  this.trackFillEl = this.trackEl.querySelector('.spark-slider__track-fill');

  if (!this.inputEl) {
    throw new Error('Tried to create a slider instance without a number input.');
  }

  if (!this.handleEl) {
    throw new Error('Tried to create a slider instance without a handle button.');
  }

  this.min = parseInt(this.inputEl.getAttribute('min'), 10) || this.min;
  this.max = parseInt(this.inputEl.getAttribute('max'), 10) || this.max;
  this.step = parseInt(this.inputEl.getAttribute('step'), 10) || this.step;

  // If we have a default value, set it after we cache the size of the element.
  // We have to do that first so we know the bounds of the slider.
  var value = parseInt(this.inputEl.getAttribute('value'), 10) || parseInt(this.inputEl.getAttribute('min'), 10);
  if (!isNaN(value)) {
    this._cacheSize();
    this.setValue(value);
  }
},

_cacheSize

method
 _cacheSize() 

Save the element dimensions.

_cacheSize: function() {

  this.width = this.trackEl.offsetWidth;
  this.height = this.trackEl.offsetHeight;

  this.handleSize = this.isX ? this.handleEl.offsetWidth : this.handleEl.offsetHeight;
  this.handleSizePercentage = this.isX ? this.handleEl.offsetWidth / this.width : this.handleEl.offsetHeight / this.height;

  var offset = this._getElementOffset(this.controlsEl);
  this.offsetLeft = offset.left;
  this.offsetTop = offset.top;
},

_updateHandlePosition

method
 _updateHandlePosition() 

Set the position of the handle.

_updateHandlePosition: function() {

  // Adjust the percentage of the total to be the percentage of the total less the size of the handle.
  var handleSize = isNaN(this.handleSizePercentage) ? 0 : this.handleSizePercentage;
  var percentage = this.percentage * (1 - handleSize) + (handleSize / 2);

  percentage = Math.round(Math.min(percentage, 1) * 100);

  this.handleEl.setAttribute('style', 'left: ' + percentage + '%;');
  this.trackEl.setAttribute('data-percentage', percentage);
  this.trackFillEl.setAttribute('style', 'width: ' + percentage + '%;');
},

_updateActiveClasses

method
 _updateActiveClasses() 

Update the active class on the handle.

_updateActiveClasses: function() {
  this._toggleClass(this.handleEl, 'active', this.isActive);
  this._toggleClass(this.el, 'active', this.isActive);
},

_bindEventListenerCallbacks

method
 _bindEventListenerCallbacks() 

Create bound versions of event listener callbacks and store them.
Otherwise we can't unbind from these events later because the
function signatures won't match.

_bindEventListenerCallbacks: function() {

  this._onTouchStartBound = this._onTouchStart.bind(this);
  this._onTouchMoveBound = this._onTouchMove.bind(this);
  this._onTouchEndBound = this._onTouchEnd.bind(this);

  this._onClickBound = this._onClick.bind(this);
  this._onMouseDownBound = this._onMouseDown.bind(this);
  this._onMouseMoveBound = this._onMouseMove.bind(this);
  this._onMouseUpBound = this._onMouseUp.bind(this);
  this._onMouseOutBound = this._onMouseOut.bind(this);

  this._onFocusBound = this._onFocus.bind(this);
  this._onKeydownBound = this._onKeydown.bind(this);
  this._onBlurBound = this._onBlur.bind(this);

  this._onChangeBound = this._onChange.bind(this);

  this._onResizeBound = this._onResize.bind(this);
},

_addEventListeners

method
 _addEventListeners() 

Add event listeners for touchstart and mouse click.

_addEventListeners: function() {

  this.controlsEl.addEventListener('touchstart', this._onTouchStartBound);
  this.controlsEl.addEventListener('mousedown', this._onMouseDownBound);

  this.inputEl.addEventListener('change', this._onChangeBound);

  this.handleEl.addEventListener('focus', this._onFocusBound);
  this.handleEl.addEventListener('click', this._onClickBound);
},

_removeEventListeners

method
 _removeEventListeners() 

Remove event listeners for touchstart and mouse click.

_removeEventListeners: function() {

  this.controlsEl.removeEventListener('touchstart', this._onTouchStartBound);
  this.controlsEl.removeEventListener('mousedown', this._onMouseDownBound);

  this.inputEl.removeEventListener('change', this._onChangeBound);

  this.handleEl.removeEventListener('focus', this._onFocusBound);
  this.handleEl.removeEventListener('click', this._onClickBound);
},

_addMoveEventListeners

method
 _addMoveEventListeners() 

Option name Type Description
type String

Which type of listeners to add

Add event listeners for touchmove, touchend, mousemove and mouseup.
We add these to the window so that the user can move off of the element
but keep dragging the slider handle. Otherwise it's really hard to
use the slider unless it's massive.

_addMoveEventListeners: function(type) {

  // Only listen for events of the type we asked for.
  switch (type) {
    case 'mouse':
      window.addEventListener('mousemove', this._onMouseMoveBound);
      window.addEventListener('mouseout', this._onMouseOutBound);
      window.addEventListener('mouseup', this._onMouseUpBound);
      break;
    case 'touch':
      window.addEventListener('touchmove', this._onTouchMoveBound);
      window.addEventListener('touchend', this._onTouchEndBound);
      break;
    case 'keyboard':
      window.addEventListener('keydown', this._onKeydownBound);
      this.handleEl.addEventListener('blur', this._onBlurBound);
      break;
  }

  window.addEventListener('resize', this._onResizeBound);
  window.addEventListener('orientationchange', this._onResizeBound);
},

_removeMoveEventListeners

method
 _removeMoveEventListeners() 

Option name Type Description
type String

Which type of listeners to remove

Remove event listeners for move events.

_removeMoveEventListeners: function(type) {

  // Only unbind events of the type we asked for.
  switch (type) {
    case 'mouse':
      window.removeEventListener('mousemove', this._onMouseMoveBound);
      window.removeEventListener('mouseup', this._onMouseUpBound);
      break;
    case 'touch':
      window.removeEventListener('touchmove', this._onTouchMoveBound);
      window.removeEventListener('touchend', this._onTouchEndBound);
      break;
    case 'keyboard':
      window.removeEventListener('keydown', this._onKeydownBound);
      this.handleEl.removeEventListener('blur', this._onBlurBound);
      break;
  }

  window.removeEventListener('resize', this._onResizeBound);
  window.removeEventListener('orientationchange', this._onResizeBound);
},

_truncateValueText

method
 _truncateValueText() 

Option name Type Description
value String
return String

Truncate value text to fit.

_truncateValueText: function(value, precision, characters) {

  if (value.length < 5) {
    return value;
  }

  var isNegative = value < 0 ? true : false;

  value = Math.abs(value);
  precision = precision || this.truncatePrecision;
  characters = characters || this.truncateCharacters;

  var i = characters.length - 1;

  precision = Math.pow(10, precision);

  for (i; i >= 0; i--) {

    var size = Math.pow(10, (i + 1) * 3);

    if (size <= value) {

      value = Math.round(value * precision / size) / precision;

      // @todo: what is this doing? i can't remember how we would ever
      // end up inside this condition.
      if ((value === 1000) && (i < characters.length - 1)) {
        value = 1;
        i++;
      }

      value += characters[i];

      break;
    }
  }

  return (isNegative ? '-' : '') + value;
},

_onTouchStart

method
 _onTouchStart() 

Option name Type Description
e Object

When the touch starts, start the slider.

_onTouchStart: function(e) {

  if (this.inputEl.getAttribute('disabled') !== null) {
    return;
  }

  e.preventDefault();

  this.start(this.isX ? e.touches[0].pageX : e.touches[0].pageY, 'touch');
},

_onTouchMove

method
 _onTouchMove() 

Option name Type Description
e Object

When the window fires a touchmove event, adjust our value accordingly

_onTouchMove: function(e) {

  if (!this.isActive) {
    return;
  }

  e.preventDefault();

  this.move(this.isX ? e.touches[0].pageX : e.touches[0].pageY);
},

_onTouchEnd

method
 _onTouchEnd() 

Option name Type Description
e Object

When the window fires a touchend event, stop tracking touches

_onTouchEnd: function(e) {

  if (!this.isActive) {
    return;
  }

  e.preventDefault();

  this.stop('touch');
},

_onMouseDown

method
 _onMouseDown() 

Option name Type Description
e Object

When the mouse presses down, start the slider.

_onMouseDown: function(e) {

  if (this.inputEl.getAttribute('disabled') !== null) {
    return;
  }

  e.preventDefault();

  this.start(this.isX ? e.pageX : e.pageY, 'mouse');
},

_onMouseMove

method
 _onMouseMove() 

Option name Type Description
e Object

When the window fires a mousemove event, adjust our value accordingly

_onMouseMove: function(e) {

  if (!this.isActive) {
    return;
  }

  e.preventDefault();

  this.move(this.isX ? e.pageX : e.pageY);
},

_onMouseUp

method
 _onMouseUp() 

Option name Type Description
e Object

When the window fires a mouseup event, stop tracking

_onMouseUp: function() {

  if (!this.isActive) {
    return;
  }

  this.stop('mouse');
},

_onMouseOut

method
 _onMouseOut() 

Option name Type Description
e Object

When the window fires a mouseout event, stop tracking if it was the html element.

_onMouseOut: function() {
  // @todo: make this work
  // if (e.relatedTarget === doc.body.parentNode) {
  //   this.stop('mouse');
  // }
},

_onResize

method
 _onResize() 

Option name Type Description
e Object

When the window resizes, cache size values for the slider.

_onResize: function() {
  this._cacheSize();
},

_onFocus

method
 _onFocus() 

Option name Type Description
e Object

When the element receives focus, start listening for keyboard events

_onFocus: function() {
  this.start(null, 'keyboard');
},

_onKeydown

method
 _onKeydown() 

Option name Type Description
e Object

When a key is pressed, see if it's a left or right arrow and move the
handle accordingly. If the shift key is pressed, we'll increment and
decrement by bigger values.

_onKeydown: function(e) {

  // Left for x or up for y
  var increment = (this.isX && e.keyCode === 39) || (!this.isX && e.keyCode === 38);

  // Right for x or down for y
  var decrement = (this.isX && e.keyCode === 37) || (!this.isX && e.keyCode === 40);

  if (increment) {
    this.increment(e.shiftKey);
  } else if (decrement) {
    this.decrement(e.shiftKey);
  }
},

_onBlur

method
 _onBlur() 

Option name Type Description
e Object

When the element loses focus, stop listening for keyboard events

_onBlur: function() {
  this.stop('keyboard');
},

_onChange

method
 _onChange() 

Option name Type Description
e Object

When the input value changes, set our interal value if it's not already our value.

_onChange: function(e) {
  if (e.target.value !== this.value) {
    this.setValue(e.target.value);
  }
  (this.onChange || noop)(this, this.value);
},

_onClick

method
 _onClick() 

Option name Type Description
e Object

Prevent click events on the button. This way we don't accidentally submit the form.

_onClick: function(e) {
  e.preventDefault();
},

_onWindowClick

method
 _onWindowClick() 

Option name Type Description
e Object

When the window is clicked and the element isn't part of the slider, trigger a blur.

_onWindowClick: function(e) {

  if (this._elementHasParent(e.target, this.el)) {
    this._onBlur();
  }
}
  };

  Base.exportjQuery(Slider, 'Slider');

  return Slider;
}));