/*!
* JavaScript Custom Forms : Range Module
*
* Copyright 2014-2016 PSD2HTML - http://psd2html.com/jcf
* Released under the MIT license (LICENSE.txt)
*
* Version: 1.2.3
*/
(function(jcf) {
jcf.addModule(function($) {
'use strict';
return {
name: 'Range',
selector: 'input[type="range"]',
options: {
realElementClass: 'jcf-real-element',
fakeStructure: '',
dataListMark: '',
rangeDisplayWrapper: '',
rangeDisplay: '',
handleSelector: '.jcf-range-handle',
trackSelector: '.jcf-range-track',
activeHandleClass: 'jcf-active-handle',
verticalClass: 'jcf-vertical',
orientation: 'horizontal',
range: false, // or "min", "max", "all"
dragHandleCenter: true,
snapToMarks: true,
snapRadius: 5,
minRange: 0
},
matchElement: function(element) {
return element.is(this.selector);
},
init: function() {
this.initStructure();
this.attachEvents();
this.refresh();
},
initStructure: function() {
this.page = $('html');
this.realElement = $(this.options.element).addClass(this.options.hiddenClass);
this.fakeElement = $(this.options.fakeStructure).insertBefore(this.realElement).prepend(this.realElement);
this.track = this.fakeElement.find(this.options.trackSelector);
this.trackHolder = this.track.parent();
this.handle = this.fakeElement.find(this.options.handleSelector);
this.createdHandleCount = 0;
this.activeDragHandleIndex = 0;
this.isMultiple = this.realElement.prop('multiple') || typeof this.realElement.attr('multiple') === 'string';
this.values = this.isMultiple ? this.realElement.attr('value').split(',') : [this.realElement.val()];
this.handleCount = this.isMultiple ? this.values.length : 1;
// create range display
this.rangeDisplayWrapper = $(this.options.rangeDisplayWrapper).insertBefore(this.track);
if (this.options.range === 'min' || this.options.range === 'all') {
this.rangeMin = $(this.options.rangeDisplay).addClass('jcf-range-min').prependTo(this.rangeDisplayWrapper);
}
if (this.options.range === 'max' || this.options.range === 'all') {
this.rangeMax = $(this.options.rangeDisplay).addClass('jcf-range-max').prependTo(this.rangeDisplayWrapper);
}
// clone handles if needed
while (this.createdHandleCount < this.handleCount) {
this.createdHandleCount++;
this.handle.clone().addClass('jcf-index-' + this.createdHandleCount).insertBefore(this.handle);
// create mid ranges
if (this.createdHandleCount > 1) {
if (!this.rangeMid) {
this.rangeMid = $();
}
this.rangeMid = this.rangeMid.add($(this.options.rangeDisplay).addClass('jcf-range-mid').prependTo(this.rangeDisplayWrapper));
}
}
// grab all handles
this.handle.detach();
this.handle = null;
this.handles = this.fakeElement.find(this.options.handleSelector);
this.handles.eq(0).addClass(this.options.activeHandleClass);
// handle orientation
this.isVertical = (this.options.orientation === 'vertical');
this.directionProperty = this.isVertical ? 'top' : 'left';
this.offsetProperty = this.isVertical ? 'bottom' : 'left';
this.eventProperty = this.isVertical ? 'pageY' : 'pageX';
this.sizeProperty = this.isVertical ? 'height' : 'width';
this.sizeMethod = this.isVertical ? 'innerHeight' : 'innerWidth';
this.fakeElement.css('touchAction', this.isVertical ? 'pan-x' : 'pan-y');
if (this.isVertical) {
this.fakeElement.addClass(this.options.verticalClass);
}
// set initial values
this.minValue = parseFloat(this.realElement.attr('min'));
this.maxValue = parseFloat(this.realElement.attr('max'));
this.stepValue = parseFloat(this.realElement.attr('step')) || 1;
// check attribute values
this.minValue = isNaN(this.minValue) ? 0 : this.minValue;
this.maxValue = isNaN(this.maxValue) ? 100 : this.maxValue;
// handle range
if (this.stepValue !== 1) {
this.maxValue -= (this.maxValue - this.minValue) % this.stepValue;
}
this.stepsCount = (this.maxValue - this.minValue) / this.stepValue;
this.createDataList();
},
attachEvents: function() {
this.realElement.on({
focus: this.onFocus
});
this.trackHolder.on('jcf-pointerdown', this.onTrackPress);
this.handles.on('jcf-pointerdown', this.onHandlePress);
},
createDataList: function() {
var self = this,
dataValues = [],
dataListId = this.realElement.attr('list');
if (dataListId) {
$('#' + dataListId).find('option').each(function() {
var itemValue = parseFloat(this.value || this.innerHTML),
mark, markOffset;
if (!isNaN(itemValue)) {
markOffset = self.valueToOffset(itemValue);
dataValues.push({
value: itemValue,
offset: markOffset
});
mark = $(self.options.dataListMark).text(itemValue).attr({
'data-mark-value': itemValue
}).css(self.offsetProperty, markOffset + '%').appendTo(self.track);
}
});
if (dataValues.length) {
self.dataValues = dataValues;
}
}
},
getDragHandleRange: function(handleIndex) {
// calculate range for slider with multiple handles
var minStep = -Infinity,
maxStep = Infinity;
if (handleIndex > 0) {
minStep = this.valueToStepIndex(parseFloat(this.values[handleIndex - 1]) + this.options.minRange);
}
if (handleIndex < this.handleCount - 1) {
maxStep = this.valueToStepIndex(parseFloat(this.values[handleIndex + 1]) - this.options.minRange);
}
return {
minStepIndex: minStep,
maxStepIndex: maxStep
};
},
getNearestHandle: function(percent) {
// handle vertical sliders
if (this.isVertical) {
percent = 1 - percent;
}
// detect closest handle when track is pressed
var closestHandle = this.handles.eq(0),
closestDistance = Infinity,
self = this;
if (this.handleCount > 1) {
this.handles.each(function() {
var handleOffset = parseFloat(this.style[self.offsetProperty]) / 100,
handleDistance = Math.abs(handleOffset - percent);
if (handleDistance < closestDistance) {
closestDistance = handleDistance;
closestHandle = $(this);
}
});
}
return closestHandle;
},
onTrackPress: function(e) {
var trackSize, trackOffset, innerOffset;
e.preventDefault();
if (!this.realElement.is(':disabled') && !this.activeDragHandle) {
trackSize = this.track[this.sizeMethod]();
trackOffset = this.track.offset()[this.directionProperty];
this.activeDragHandle = this.getNearestHandle((e[this.eventProperty] - trackOffset) / this.trackHolder[this.sizeMethod]());
this.activeDragHandleIndex = this.handles.index(this.activeDragHandle);
this.handles.removeClass(this.options.activeHandleClass).eq(this.activeDragHandleIndex).addClass(this.options.activeHandleClass);
innerOffset = this.activeDragHandle[this.sizeMethod]() / 2;
this.dragData = {
trackSize: trackSize,
innerOffset: innerOffset,
trackOffset: trackOffset,
min: trackOffset,
max: trackOffset + trackSize
};
this.page.on({
'jcf-pointermove': this.onHandleMove,
'jcf-pointerup': this.onHandleRelease
});
if (e.pointerType === 'mouse') {
this.realElement.focus();
}
this.onHandleMove(e);
}
},
onHandlePress: function(e) {
var trackSize, trackOffset, innerOffset;
e.preventDefault();
if (!this.realElement.is(':disabled') && !this.activeDragHandle) {
this.activeDragHandle = $(e.currentTarget);
this.activeDragHandleIndex = this.handles.index(this.activeDragHandle);
this.handles.removeClass(this.options.activeHandleClass).eq(this.activeDragHandleIndex).addClass(this.options.activeHandleClass);
trackSize = this.track[this.sizeMethod]();
trackOffset = this.track.offset()[this.directionProperty];
innerOffset = this.options.dragHandleCenter ? this.activeDragHandle[this.sizeMethod]() / 2 : e[this.eventProperty] - this.handle.offset()[this.directionProperty];
this.dragData = {
trackSize: trackSize,
innerOffset: innerOffset,
trackOffset: trackOffset,
min: trackOffset,
max: trackOffset + trackSize
};
this.page.on({
'jcf-pointermove': this.onHandleMove,
'jcf-pointerup': this.onHandleRelease
});
if (e.pointerType === 'mouse') {
this.realElement.focus();
}
}
},
onHandleMove: function(e) {
var self = this,
newOffset, dragPercent, stepIndex, valuePercent, handleDragRange;
// calculate offset
if (this.isVertical) {
newOffset = this.dragData.max + (this.dragData.min - e[this.eventProperty]) - this.dragData.innerOffset;
} else {
newOffset = e[this.eventProperty] - this.dragData.innerOffset;
}
// fit in range
if (newOffset < this.dragData.min) {
newOffset = this.dragData.min;
} else if (newOffset > this.dragData.max) {
newOffset = this.dragData.max;
}
e.preventDefault();
if (this.options.snapToMarks && this.dataValues) {
// snap handle to marks
var dragOffset = newOffset - this.dragData.trackOffset;
dragPercent = (newOffset - this.dragData.trackOffset) / this.dragData.trackSize * 100;
$.each(this.dataValues, function(index, item) {
var markOffset = item.offset / 100 * self.dragData.trackSize,
markMin = markOffset - self.options.snapRadius,
markMax = markOffset + self.options.snapRadius;
if (dragOffset >= markMin && dragOffset <= markMax) {
dragPercent = item.offset;
return false;
}
});
} else {
// snap handle to steps
dragPercent = (newOffset - this.dragData.trackOffset) / this.dragData.trackSize * 100;
}
// move handle only in range
stepIndex = Math.round(dragPercent * this.stepsCount / 100);
if (this.handleCount > 1) {
handleDragRange = this.getDragHandleRange(this.activeDragHandleIndex);
if (stepIndex < handleDragRange.minStepIndex) {
stepIndex = Math.max(handleDragRange.minStepIndex, stepIndex);
} else if (stepIndex > handleDragRange.maxStepIndex) {
stepIndex = Math.min(handleDragRange.maxStepIndex, stepIndex);
}
}
valuePercent = stepIndex * (100 / this.stepsCount);
if (this.dragData.stepIndex !== stepIndex) {
this.dragData.stepIndex = stepIndex;
this.dragData.offset = valuePercent;
this.activeDragHandle.css(this.offsetProperty, this.dragData.offset + '%');
// update value(s) and trigger "input" event
this.values[this.activeDragHandleIndex] = '' + this.stepIndexToValue(stepIndex);
this.updateValues();
this.realElement.trigger('input');
}
},
onHandleRelease: function() {
var newValue;
if (typeof this.dragData.offset === 'number') {
newValue = this.stepIndexToValue(this.dragData.stepIndex);
this.realElement.val(newValue).trigger('change');
}
this.page.off({
'jcf-pointermove': this.onHandleMove,
'jcf-pointerup': this.onHandleRelease
});
delete this.activeDragHandle;
delete this.dragData;
},
onFocus: function() {
if (!this.fakeElement.hasClass(this.options.focusClass)) {
this.fakeElement.addClass(this.options.focusClass);
this.realElement.on({
blur: this.onBlur,
keydown: this.onKeyPress
});
}
},
onBlur: function() {
this.fakeElement.removeClass(this.options.focusClass);
this.realElement.off({
blur: this.onBlur,
keydown: this.onKeyPress
});
},
onKeyPress: function(e) {
var incValue = (e.which === 38 || e.which === 39),
decValue = (e.which === 37 || e.which === 40);
// handle TAB key in slider with multiple handles
if (e.which === 9 && this.handleCount > 1) {
if (e.shiftKey && this.activeDragHandleIndex > 0) {
this.activeDragHandleIndex--;
} else if (!e.shiftKey && this.activeDragHandleIndex < this.handleCount - 1) {
this.activeDragHandleIndex++;
} else {
return;
}
e.preventDefault();
this.handles.removeClass(this.options.activeHandleClass).eq(this.activeDragHandleIndex).addClass(this.options.activeHandleClass);
}
// handle cursor keys
if (decValue || incValue) {
e.preventDefault();
this.step(incValue ? this.stepValue : -this.stepValue);
}
},
updateValues: function() {
var value = this.values.join(',');
if (this.values.length > 1) {
this.realElement.prop('valueLow', this.values[0]);
this.realElement.prop('valueHigh', this.values[this.values.length - 1]);
this.realElement.val(value);
// if browser does not accept multiple values set only one
if (this.realElement.val() !== value) {
this.realElement.val(this.values[this.values.length - 1]);
}
} else {
this.realElement.val(value);
}
this.updateRanges();
},
updateRanges: function() {
// update display ranges
var self = this,
handle;
if (this.rangeMin) {
handle = this.handles[0];
this.rangeMin.css(this.offsetProperty, 0).css(this.sizeProperty, handle.style[this.offsetProperty]);
}
if (this.rangeMax) {
handle = this.handles[this.handles.length - 1];
this.rangeMax.css(this.offsetProperty, handle.style[this.offsetProperty]).css(this.sizeProperty, 100 - parseFloat(handle.style[this.offsetProperty]) + '%');
}
if (this.rangeMid) {
this.handles.each(function(index, curHandle) {
var prevHandle, midBox;
if (index > 0) {
prevHandle = self.handles[index - 1];
midBox = self.rangeMid[index - 1];
midBox.style[self.offsetProperty] = prevHandle.style[self.offsetProperty];
midBox.style[self.sizeProperty] = parseFloat(curHandle.style[self.offsetProperty]) - parseFloat(prevHandle.style[self.offsetProperty]) + '%';
}
});
}
},
step: function(changeValue) {
var originalValue = parseFloat(this.values[this.activeDragHandleIndex || 0]),
newValue = originalValue,
minValue = this.minValue,
maxValue = this.maxValue;
if (isNaN(originalValue)) {
newValue = 0;
}
newValue += changeValue;
if (this.handleCount > 1) {
if (this.activeDragHandleIndex > 0) {
minValue = parseFloat(this.values[this.activeDragHandleIndex - 1]) + this.options.minRange;
}
if (this.activeDragHandleIndex < this.handleCount - 1) {
maxValue = parseFloat(this.values[this.activeDragHandleIndex + 1]) - this.options.minRange;
}
}
if (newValue > maxValue) {
newValue = maxValue;
} else if (newValue < minValue) {
newValue = minValue;
}
if (newValue !== originalValue) {
this.values[this.activeDragHandleIndex || 0] = '' + newValue;
this.updateValues();
this.realElement.trigger('input').trigger('change');
this.setSliderValue(this.values);
}
},
valueToStepIndex: function(value) {
return (value - this.minValue) / this.stepValue;
},
stepIndexToValue: function(stepIndex) {
return this.minValue + this.stepValue * stepIndex;
},
valueToOffset: function(value) {
var range = this.maxValue - this.minValue,
percent = (value - this.minValue) / range;
return percent * 100;
},
getSliderValue: function() {
return $.map(this.values, function(value) {
return parseFloat(value) || 0;
});
},
setSliderValue: function(values) {
// set handle position accordion according to value
var self = this;
this.handles.each(function(index, handle) {
handle.style[self.offsetProperty] = self.valueToOffset(values[index]) + '%';
});
},
refresh: function() {
// handle disabled state
var isDisabled = this.realElement.is(':disabled');
this.fakeElement.toggleClass(this.options.disabledClass, isDisabled);
// refresh handle position according to current value
this.setSliderValue(this.getSliderValue());
this.updateRanges();
},
destroy: function() {
this.realElement.removeClass(this.options.hiddenClass).insertBefore(this.fakeElement);
this.fakeElement.remove();
this.realElement.off({
keydown: this.onKeyPress,
focus: this.onFocus,
blur: this.onBlur
});
}
};
});
}(jcf));