Option name | Type | Description |
---|---|---|
el | Element | |
params | Object |
RangeSlider constructor.
var RangeSlider = function(el, params) {
if (!el) {
return;
}
this._setParams(this.defaults, true);
this._cacheElements(el);
this._setParams(params || {});
this._bindEventListenerCallbacks();
this._addEventListeners();
this._updateDisabledClasses();
};
RangeSlider.prototype = {
Include common functionality.
_setParams: Base.setParams,
_toggleClass: Base.toggleClass,
_getElementOffset: Base.getElementOffset,
_getNodeListIndex: Base.getNodeListIndex,
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,
inputEls: null,
handleEls: null,
trackEl: null,
trackFillEl: null,
isActive: null,
isX: true,
onChange: null,
onWillChange: null,
position: 0,
width: 0,
height: 0,
mins: null,
maxes: null,
steps: null,
values: null,
percentages: null,
offsetLeft: 0,
offsetTop: 0,
handleSizePercentage: 0,
currentIndex: null,
lastIndex: null,
_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,
_onVisibleChildrenBound: null
},
Option name | Type | Description |
---|---|---|
index | Number | The index of the handle or input element. |
position | Number | The position of the pointer. |
type | String | Optional Which type of events to listen for. |
Start the slider moving.
start: function(index, position, type) {
// Noop if we're disabled or an invalid index was passed
if (index < 0 || this.inputEls[index].getAttribute('disabled') !== null) {
return;
}
this._addMoveEventListeners(type || 'mouse');
this._cacheSize();
this.isActive = this.isActive || [];
this.isActive[index] = true;
this.currentIndex = index;
this._updateActiveClasses(index);
this._updateDisabledClasses();
this._oldVal = this.values[index];
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 an invalid index was passed we haven't yet started dragging
if ((!position || !this.isActive || !this.isActive[this.currentIndex]) && !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(this.currentIndex, Math.round((percentage - this.handleSizePercentage / 2) * (this.highestMax - this.lowestMin)) + this.lowestMin);
},
Option name | Type | Description |
---|---|---|
index | Number | Optional The index of the handle or input element. |
type | String | Optional Which type of events to listen for. |
Stop listening to movements.
stop: function(index, type) {
if (index !== null && index !== undefined && this.currentIndex !== index) {
return;
}
this.isActive[this.currentIndex] = false;
this.lastIndex = this.currentIndex;
if(this._oldVal !== this.values[this.currentIndex]) {
(this.onChange || noop)(this, this.currentIndex, this.values[this.currentIndex]);
}
this.currentIndex = null;
this._updateActiveClasses(index);
this._removeMoveEventListeners(type || 'mouse');
},
Option name | Type | Description |
---|---|---|
index | Number | The index of the input element. |
value | Number |
Set the value of the handle.
setValue: function(index, value) {
// We don't have an input element at that index, so something went wrong.
if (!this.inputEls[index]) {
throw new Error('Cannot set value on input element with an index of ' + index + '. That element does not exist.');
}
// Move in increments if we have a defined step size
if (this.steps[index]) {
value = value - (value % this.steps[index]);
}
this.values = this.values || [];
// Check bounds of the new value
if (value > this.maxes[index]) {
value = this.maxes[index];
} else if (value < this.mins[index]) {
value = this.mins[index];
}
// If there is an input that comes after this, make sure we aren't going beyond it
if (this.values[index + 1] !== undefined && value >= this.values[index + 1]) {
value = this.values[index + 1] - (this.steps[index] || 1);
}
// If there is an input that comes before this, make sure we aren't going below it
else if (this.values[index - 1] !== undefined && value <= this.values[index - 1]) {
value = this.values[index - 1] + (this.steps[index] || 1);
}
// 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, index, value);
if (typeof change === 'number') {
value = change;
}
}
// Store value
this.values[index] = value;
// Update elements
this.inputEls[index].value = this.values[index];
this.handleEls[index].setAttribute('data-value', this.values[index]);
// Set the percentage
this.percentages = this.percentages || [];
this.percentages[index] = (this.values[index] - this.lowestMin) / (this.highestMax - this.lowestMin);
// Update the position of the handle
this._updateHandlePosition(index);
},
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.currentIndex, this.values[this.currentIndex] + this.steps[this.currentIndex] * (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.currentIndex, this.values[this.currentIndex] - this.steps[this.currentIndex] * (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.inputEls = this.el.querySelectorAll('input[type="number"]');
this.handleEls = this.el.querySelectorAll('.spark-slider__handle');
this.trackEl = this.el.querySelector('.spark-slider__track');
this.trackFillEl = this.trackEl.querySelector('.spark-slider__track-fill');
if (!this.inputEls || this.inputEls.length <= 1) {
throw new Error('Tried to create a slider instance without two number inputs.');
}
if (!this.handleEls || this.handleEls.length <= 1) {
throw new Error('Tried to create a slider instance without two handle buttons.');
}
var lowestMin = Infinity;
var highestMax = -Infinity;
var i = 0;
var len = this.inputEls.length;
var values = [];
this.mins = [];
this.maxes = [];
this.steps = [];
// Cache the size of the element so that we can properly set values on handles.
this._cacheSize();
// Set the minimum and max values for each element. Also set any predefined value.
for (; i < len; i++) {
this.mins[i] = parseInt(this.inputEls[i].getAttribute('min'), 10) || null;
this.maxes[i] = parseInt(this.inputEls[i].getAttribute('max'), 10) || null;
this.steps[i] = parseInt(this.inputEls[i].getAttribute('step'), 10) || 1;
if (this.mins[i] < lowestMin) {
lowestMin = this.mins[i];
}
if (this.maxes[i] > highestMax) {
highestMax = this.maxes[i];
}
}
this.lowestMin = lowestMin;
this.highestMax = highestMax;
i = 0;
// If we have a default value, set it.
for (; i < len; i++) {
values[i] = parseInt(this.inputEls[i].getAttribute('value'), 10);
// It's a number
if (!isNaN(values[i])) {
this.setValue(i, values[i]);
} else {
// Set as the minimum unless this is the last handle.
if (i + 1 === len) {
this.setValue(i, this.maxes[i] !== null ? this.maxes[i] : 0);
} else {
this.setValue(i, this.mins[i] !== null ? this.mins[i] : 0);
}
}
}
},
Save the element dimensions.
_cacheSize: function() {
this.width = this.trackEl.clientWidth;
this.height = this.trackEl.clientHeight;
this.handleSize = this.isX ? this.handleEls[0].clientWidth : this.handleEls[0].clientHeight;
this.handleSizePercentage = this.isX ? this.handleEls[0].clientWidth / this.width : this.handleEls[0].clientHeight / this.height;
var offset = this._getElementOffset(this.controlsEl);
this.offsetLeft = offset.left;
this.offsetTop = offset.top;
},
Option name | Type | Description |
---|---|---|
index | Number | The index of the handle element to update. |
Set the position of the handle.
_updateHandlePosition: function(index) {
// Adjust the percentage of the total to be the percentage of the total less the size of the handle.
var percentage = this.percentages[index] * (1 - this.handleSizePercentage) + (this.handleSizePercentage / 2);
percentage = Math.round(Math.min(percentage, 1) * 100);
this.handleEls[index].setAttribute('style', 'left: ' + percentage + '%;');
var firstPercentage = this.percentages[0];
var lastPercentage = this.percentages[this.percentages.length - 1];
this.trackFillEl.setAttribute('style', 'width: ' + ((lastPercentage - firstPercentage) * 100) + '%; left: ' + (firstPercentage * 100) + '%;');
},
Option name | Type | Description |
---|---|---|
index | Number | The index of the handle element to update. |
Update the active class on the handle.
_updateActiveClasses: function(index) {
this._toggleClass(this.handleEls, 'active', false);
this._toggleClass(this.handleEls[index], 'active', this.isActive[index]);
if (this.isActive.indexOf(true) !== -1) {
this.el.setAttribute('data-active-index', this.isActive.indexOf(true));
} else {
this.el.removeAttribute('data-active-index');
}
},
Update which handles are disabled.
_updateDisabledClasses: function() {
var disabledCount = 0;
for (var i = 0, len = this.inputEls.length; i < len; i++) {
if (this.inputEls[i].getAttribute('disabled') !== null) {
this._toggleClass(this.handleEls[i], 'disabled', true);
disabledCount++;
} else {
this._toggleClass(this.handleEls[i], 'disabled', false);
}
}
this._toggleClass(this.el, 'all-disabled', disabledCount === this.handleEls.length);
},
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._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);
this._onVisibleChildrenBound = this._onVisibleChildren.bind(this);
},
Add event listeners for touchstart and mouse click.
_addEventListeners: function() {
this.controlsEl.addEventListener('touchstart', this._onTouchStartBound);
this.controlsEl.addEventListener('mousedown', this._onMouseDownBound);
for (var i = 0, len = this.inputEls.length; i < len; i++) {
this.inputEls[i].addEventListener('change', this._onChangeBound);
}
for (var j = 0, len2 = this.handleEls.length; j < len2; j++) {
this.handleEls[j].addEventListener('focus', this._onFocusBound);
this.handleEls[j].addEventListener('click', this._onClickBound);
}
document.addEventListener('spark.visible-children', this._onVisibleChildrenBound, true);
},
Remove event listeners for touchstart and mouse click.
_removeEventListeners: function() {
this.controlsEl.removeEventListener('touchstart', this._onTouchStartBound);
this.controlsEl.removeEventListener('mousedown', this._onMouseDownBound);
document.removeEventListener('spark.visible-children', this._onVisibleChildrenBound);
for (var i = 0, len = this.inputEls.length; i < len; i++) {
this.inputEls[i].removeEventListener('change', this._onChangeBound);
}
for (var j = 0, len2 = this.handleEls.length; i < len2; i++) {
this.handleEls[j].removeEventListener('focus', this._onFocusBound);
this.handleEls[j].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);
for (var i = 0, len = this.handleEls.length; i < len; i++) {
this.handleEls[i].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 add |
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);
for (var i = 0, len = this.handleEls.length; i < len; i++) {
this.handleEls[i].removeEventListener('blur', this._onBlurBound);
}
break;
}
window.removeEventListener('resize', this._onResizeBound);
window.removeEventListener('orientationchange', this._onResizeBound);
},
Option name | Type | Description |
---|---|---|
e | Object |
When the touch starts, start the slider.
_onTouchStart: function(e) {
this.start(this._getNodeListIndex(this.handleEls, e.target), 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[this.currentIndex]) {
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[this.currentIndex]) {
return;
}
e.preventDefault();
this.stop(this._getNodeListIndex(this.handleEls, e.target), 'touch');
},
Option name | Type | Description |
---|---|---|
e | Object |
When the mouse presses down, start the slider.
_onMouseDown: function(e) {
this.start(this._getNodeListIndex(this.handleEls, e.target), 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[this.currentIndex]) {
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[this.currentIndex]) {
return;
}
this.stop(null, 'mouse');
},
Option name | Type | Description |
---|---|---|
e | Object |
Handle the spark.visible-children event
_onVisibleChildren: function(e) {
if(e.target.contains(this.el)) {
window.setTimeout(function() {
this._onResize();
}.bind(this),0);
}
},
Option name | Type | Description |
---|---|---|
e | Object |
When the window resizes, cache size values for the slider.
_onResize: function() {
this._cacheSize();
this._updateDisabledClasses();
for(var i = 0; i < this.handleEls.length; i++) {
this._updateHandlePosition(i);
}
},
Option name | Type | Description |
---|---|---|
e | Object |
When the element receives focus, start listening for keyboard events
_onFocus: function(e) {
this.start(this._getNodeListIndex(this.handleEls, e.target), 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) {
if (this._getNodeListIndex(this.inputEls, e.target) !== -1) {
return;
}
// 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(e) {
this.stop(this._getNodeListIndex(this.handleEls, e.target), '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) {
var index = this._getNodeListIndex(this.inputEls, e.target);
this._updateDisabledClasses();
if (e.target.value !== this.values[index]) {
this.setValue(index, e.target.value);
}
(this.onChange || noop)(this, index, this.values[index]);
},
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();
}
};
Base.exportjQuery(RangeSlider, 'RangeSlider');
return RangeSlider;
}));