Sabre Spark

Slider

A slider for number inputs.

Option name Type Description
module components/slider.js
Example
new Slider(el, {
  // Optional. Slide along the x or y-axis?
  isX: true,
  // onChange callback
  onChange: function(inst, val){},
});

Slider

function
 Slider() 

Slider constructor.

Option name Type Description
el Element
params Object
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.

Option name Type Description
_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.

Option name Type Description
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() 

Start the slider moving.

Option name Type Description
position Number

The position of the pointer.

type String

Optional Which type of events to listen for.

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() 

Move the value to a given position

Option name Type Description
position Number
force Boolean

Force the move Optional

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() 

Stop listening to movements.

Option name Type Description
type String

Optional Which type of events to listen for.

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() 

Set the value of the handle.

Option name Type Description
value Number
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() 

Increment the value by the step size.

Option name Type Description
useMultiplier Boolean

Optional Increment by a multiplied version of the step

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

decrement

method
 decrement() 

Decrement the value by the step size.

Option name Type Description
useMultiplier Boolean

Optional Increment by a multiplied version of the step

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

remove

method
 remove() 

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

Option name Type Description
leaveElement Boolean

Leave the element intact.

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

_cacheElements

method
 _cacheElements() 

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).

Option name Type Description
el Element
_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() 

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.

Option name Type Description
type String

Which type of listeners to add

_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() 

Remove event listeners for move events.

Option name Type Description
type String

Which type of listeners to remove

_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() 

Truncate value text to fit.

Option name Type Description
value String
return String
_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() 

When the touch starts, start the slider.

Option name Type Description
e Object
_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() 

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

Option name Type Description
e Object
_onTouchMove: function(e) {

  if (!this.isActive) {
    return;
  }

  e.preventDefault();

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

_onTouchEnd

method
 _onTouchEnd() 

When the window fires a touchend event, stop tracking touches

Option name Type Description
e Object
_onTouchEnd: function(e) {

  if (!this.isActive) {
    return;
  }

  e.preventDefault();

  this.stop('touch');
},

_onMouseDown

method
 _onMouseDown() 

When the mouse presses down, start the slider.

Option name Type Description
e Object
_onMouseDown: function(e) {

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

  e.preventDefault();

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

_onMouseMove

method
 _onMouseMove() 

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

Option name Type Description
e Object
_onMouseMove: function(e) {

  if (!this.isActive) {
    return;
  }

  e.preventDefault();

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

_onMouseUp

method
 _onMouseUp() 

When the window fires a mouseup event, stop tracking

Option name Type Description
e Object
_onMouseUp: function() {

  if (!this.isActive) {
    return;
  }

  this.stop('mouse');
},

_onMouseOut

method
 _onMouseOut() 

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

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

_onResize

method
 _onResize() 

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

Option name Type Description
e Object
_onResize: function() {
  this._cacheSize();
},

_onFocus

method
 _onFocus() 

When the element receives focus, start listening for keyboard events

Option name Type Description
e Object
_onFocus: function() {
  this.start(null, 'keyboard');
},

_onKeydown

method
 _onKeydown() 

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.

Option name Type Description
e Object
_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() 

When the element loses focus, stop listening for keyboard events

Option name Type Description
e Object
_onBlur: function() {
  this.stop('keyboard');
},

_onChange

method
 _onChange() 

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

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

_onClick

method
 _onClick() 

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

Option name Type Description
e Object
_onClick: function(e) {
  e.preventDefault();
},

_onWindowClick

method
 _onWindowClick() 

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

Option name Type Description
e Object
_onWindowClick: function(e) {

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

  Base.exportjQuery(Slider, 'Slider');

  return Slider;
}));