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 = {
Include common functionality.
_setParams: Base.setParams,
_toggleClass: Base.toggleClass,
_getElementOffset: Base.getElementOffset,
_elementHasParent: Base.elementHasParent,
Whitelisted parameters which can be set on construction.
_whitelistedParams: ['isX', 'onChange', 'onWillChange'],
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
},
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);
},
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);
},
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');
},
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();
},
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));
},
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));
},
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);
},
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);
}
},
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;
},
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 + '%;');
},
Update the active class on the handle.
_updateActiveClasses: function() {
this._toggleClass(this.handleEl, 'active', this.isActive);
this._toggleClass(this.el, 'active', this.isActive);
},
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);
},
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);
},
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);
},
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);
},
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);
},
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;
},
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');
},
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);
},
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');
},
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');
},
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);
},
Option name | Type | Description |
---|---|---|
e | Object |
When the window fires a mouseup event, stop tracking
_onMouseUp: function() {
if (!this.isActive) {
return;
}
this.stop('mouse');
},
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');
// }
},
Option name | Type | Description |
---|---|---|
e | Object |
When the window resizes, cache size values for the slider.
_onResize: function() {
this._cacheSize();
},
Option name | Type | Description |
---|---|---|
e | Object |
When the element receives focus, start listening for keyboard events
_onFocus: function() {
this.start(null, 'keyboard');
},
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);
}
},
Option name | Type | Description |
---|---|---|
e | Object |
When the element loses focus, stop listening for keyboard events
_onBlur: function() {
this.stop('keyboard');
},
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);
},
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();
},
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;
}));