/*! * 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));