Commit 39ef89e7 authored by Valentin Hervieu's avatar Valentin Hervieu Committed by Valentin Hervieu

Refactor the slider architecture. Add a rzSliderOptions that handle all the…

Refactor the slider architecture. Add a rzSliderOptions that handle all the options except model, high and rzSliderTplUrl
parent d26c0eaf
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
body { font-family: 'Open Sans', sans-serif; color: #1f2636; font-size: 14px; } body { font-family: 'Open Sans', sans-serif; color: #1f2636; font-size: 14px; }
header { background: #0db9f0; color: #fff; margin: -40px; margin-bottom: 40px; text-align: center; padding: 40px 0; } header { background: #0db9f0; color: #fff; margin: -40px; margin-bottom: 40px; text-align: center; padding: 40px 0; }
h1 { font-weight: 300; } h1 { font-weight: 300; }
h2 {margin-bottom:10px;}
.wrapper { background: #fff; padding: 40px; } .wrapper { background: #fff; padding: 40px; }
article { margin-bottom: 40px; } article { margin-bottom: 40px; }
var app = angular.module('rzSliderDemo', ['rzModule']);
app.controller('MainCtrl', function($scope, $timeout) {
//Minimal slider config
$scope.minSlider = {
value: 10
};
//Slider with selection bar
$scope.slider_visible_bar = {
value: 10,
options: {
showSelectionBar: true
}
};
//Range slider config
$scope.minRangeSlider = {
minValue: 10,
maxValue: 90,
options: {
floor: 0,
ceil: 100,
step: 1
}
};
//Slider config with floor, ceil and step
$scope.slider_floor_ceil = {
value: 12,
options: {
floor: 10,
ceil: 100,
step: 5
}
};
//Slider config with callbacks
$scope.slider_callbacks = {
value: 100,
options: {
onStart: function() {
$scope.otherData.start = $scope.slider_callbacks.value * 10;
},
onChange: function() {
$scope.otherData.change = $scope.slider_callbacks.value * 10;
},
onEnd: function() {
$scope.otherData.end = $scope.slider_callbacks.value * 10;
}
}
};
$scope.otherData = {start: 0, change: 0, end: 0};
//Slider config with custom display function
$scope.slider_translate = {
minValue: 100,
maxValue: 400,
options: {
ceil: 500,
floor: 0,
translate: function(value) {
return '$' + value;
}
}
};
//Slider config with custom display function displaying letters
var alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
$scope.slider_alphabet = {
value: 0,
options: {
ceil: alphabet.length - 1,
floor: 0,
translate: function(value) {
if (value >= 0 && value < alphabet.length)
return alphabet[value];
return '';
}
}
};
//Slider with ticks
$scope.slider_ticks = {
value: 5,
options: {
ceil: 10,
floor: 0,
showTicks: true
}
};
//Slider with ticks and values
$scope.slider_ticks_values = {
value: 5,
options: {
ceil: 10,
floor: 0,
showTicksValues: true
}
};
//Range slider with ticks and values
$scope.range_slider_ticks_values = {
minValue: 1,
maxValue: 8,
options: {
ceil: 10,
floor: 0,
showTicksValues: true
}
};
//Slider with draggable range
$scope.slider_draggable_range = {
minValue: 1,
maxValue: 8,
options: {
ceil: 10,
floor: 0,
draggableRange: true
}
};
//Read-only slider
$scope.read_only_slider = {
value: 50,
options: {
ceil: 100,
floor: 0,
readOnly: true
}
};
//Disabled slider
$scope.disabled_slider = {
value: 50,
options: {
ceil: 100,
floor: 0,
disabled: true
}
};
$scope.visible = false;
$scope.toggle = function() {
$scope.visible = !$scope.visible;
$timeout(function() {
$scope.$broadcast('rzSliderForceRender');
});
};
$scope.slider_toggle = {
value: 5,
options: {
ceil: 10,
floor: 0
}
};
});
...@@ -2,240 +2,151 @@ ...@@ -2,240 +2,151 @@
<html ng-app="rzSliderDemo"> <html ng-app="rzSliderDemo">
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>AngularJS Touch Slider</title> <title>AngularJS Touch Slider</title>
<link rel="stylesheet" href="demo.css"/> <link rel="stylesheet" href="demo.css"/>
<link rel="stylesheet" href="../dist/rzslider.css"/> <link rel="stylesheet" href="../dist/rzslider.css"/>
<link href='http://fonts.googleapis.com/css?family=Open+Sans:300,400,700' rel='stylesheet' type='text/css'> <link href='http://fonts.googleapis.com/css?family=Open+Sans:300,400,700' rel='stylesheet' type='text/css'>
</head> </head>
<body ng-controller="MainCtrl"> <body ng-controller="MainCtrl">
<div class="wrapper"> <div class="wrapper">
<header> <header>
<h1>AngularJS Touch Slider</h1> <h1>AngularJS Touch Slider</h1>
</header> </header>
<article> <article>
<h2>Min/max slider example</h2> <h2>Simple slider</h2>
Value: Model: <input type="number" ng-model="minSlider.value"/><br/>
<pre>{{ slider_data | json }}</pre> <rzslider rz-slider-model="minSlider.value"></rzslider>
<p>Value linked on change: {{ otherData.value }}</p> </article>
<rzslider <article>
rz-slider-floor="0.5" <h2>Range slider</h2>
rz-slider-ceil="10.5" Min Value: <input type="number" ng-model="minRangeSlider.minValue"/><br/>
rz-slider-step="0.3" Max Value: <input type="number" ng-model="minRangeSlider.maxValue"/><br/>
rz-slider-precision="1" <rzslider
rz-slider-model="slider_data.value" rz-slider-model="minRangeSlider.minValue"
rz-slider-on-start="onStart()" rz-slider-high="minRangeSlider.maxValue"
rz-slider-on-change="onChange()" rz-slider-options="minRangeSlider.options"
rz-slider-on-end="onEnd()"></rzslider> ></rzslider>
</article> </article>
<article> <article>
<h2>Min/max slider example</h2> <h2>Slider with visible selection bar</h2>
Value: <rzslider
<pre>{{ priceSlider | json }}</pre> rz-slider-model="slider_visible_bar.value"
rz-slider-options="slider_visible_bar.options"
<input type="text" ng-model="priceSlider.min"/><br/> ></rzslider>
<input type="text" ng-model="priceSlider.max"/><br/> </article>
<rzslider <article>
rz-slider-floor="priceSlider.floor" <h2>Slider with custom floor/ceil/step</h2>
rz-slider-ceil="priceSlider.ceil" <rzslider
rz-slider-model="priceSlider.min" rz-slider-model="slider_floor_ceil.value"
rz-slider-high="priceSlider.max" rz-slider-options="slider_floor_ceil.options"
rz-slider-step="6" ></rzslider>
rz-slider-tpl-url="rzSliderTpl.html"></rzslider> </article>
</article>
<article>
<article> <h2>Slider with callbacks on start, change and end</h2>
<h2>Currency slider example</h2> <p>Value linked on start: {{ otherData.start }}</p>
<p>Value linked on change: {{ otherData.change }}</p>
Value: {{ priceSlider2 | json }} <p>Value linked on end: {{ otherData.end }}</p>
<rzslider
rz-slider-floor="0" <rzslider
rz-slider-ceil="450" rz-slider-model="slider_callbacks.value"
rz-slider-model="priceSlider2" rz-slider-options="slider_callbacks.options"
rz-slider-translate="translate" ></rzslider>
rz-slider-tpl-url="rzSliderTpl.html"></rzslider> </article>
</article>
<article>
<article> <h2>Slider with custom display function</h2>
<h2>One value slider example</h2> <rzslider
rz-slider-model="slider_translate.minValue"
Value: {{ priceSlider3 | json }} rz-slider-high="slider_translate.maxValue"
<rzslider rz-slider-model="priceSlider3" rz-slider-options="slider_translate.options"
rz-slider-floor="50" ></rzslider>
rz-slider-ceil="450" </article>
rz-slider-always-show-bar="true"
rz-slider-tpl-url="rzSliderTpl.html"></rzslider> <article>
</article> <h2>Slider with Alphabet</h2>
<rzslider
<article> rz-slider-model="slider_alphabet.value"
<h2>Alphabet slider example</h2> rz-slider-options="slider_alphabet.options"
Value: {{ alphabetTranslate(letter) }} ></rzslider>
<rzslider </article>
rz-slider-floor="0"
rz-slider-ceil="letterMax" <article>
rz-slider-model="letter" <h2>Slider with ticks</h2>
rz-slider-translate="alphabetTranslate" <rzslider
rz-slider-tpl-url="rzSliderTpl.html"></rzslider> rz-slider-model="slider_ticks.value"
</article> rz-slider-options="slider_ticks.options"
></rzslider>
<article> </article>
<h2>Slider with ticks example</h2>
Value: {{ priceSlider4 | json }} <article>
<rzslider rz-slider-model="priceSlider4" <h2>Slider with ticks and values</h2>
rz-slider-floor="0" <rzslider
rz-slider-ceil="10" rz-slider-model="slider_ticks_values.value"
rz-slider-show-ticks="true"></rzslider> rz-slider-options="slider_ticks_values.options"
</article> ></rzslider>
</article>
<article>
<h2>Slider with ticks value example</h2> <article>
Value: {{ priceSlider5 | json }} <h2>Range slider with ticks and values</h2>
<rzslider rz-slider-model="priceSlider5" <rzslider
rz-slider-floor="0" rz-slider-model="range_slider_ticks_values.minValue"
rz-slider-ceil="10" rz-slider-high="range_slider_ticks_values.maxValue"
rz-slider-show-ticks-value="true"></rzslider> rz-slider-options="range_slider_ticks_values.options"
</article> ></rzslider>
</article>
<article>
<h2>Slider with ticks value and visible bar example</h2> <article>
Value: {{ priceSlider6 | json }} <h2>Slider with draggable range</h2>
<rzslider rz-slider-model="priceSlider6" <rzslider
rz-slider-floor="0.5" rz-slider-model="slider_draggable_range.minValue"
rz-slider-ceil="1.5" rz-slider-high="slider_draggable_range.maxValue"
rz-slider-step="0.1" rz-slider-options="slider_draggable_range.options"
rz-slider-precision="1" ></rzslider>
rz-slider-always-show-bar="true" </article>
rz-slider-show-ticks-value="true"></rzslider>
</article> <article>
<h2>Disabled slider</h2>
<article> <label>Disabled <input type="checkbox" ng-model="disabled_slider.options.disabled"></label>
<h2>Range Slider with ticks value example</h2> <rzslider
Value: {{ priceSlider7 | json }} rz-slider-model="disabled_slider.value"
<rzslider rz-slider-model="priceSlider7.min" rz-slider-options="disabled_slider.options"
rz-slider-high="priceSlider7.max" ></rzslider>
rz-slider-floor="0" </article>
rz-slider-ceil="10"
rz-slider-show-ticks-value="true"></rzslider> <article>
</article> <h2>Read-only slider</h2>
<label>Read-only <input type="checkbox" ng-model="read_only_slider.options.readOnly"></label>
<article> <rzslider
<h2>Draggable range example</h2> rz-slider-model="read_only_slider.value"
Value: rz-slider-options="read_only_slider.options"
<pre>{{ priceSlider | json }}</pre> ></rzslider>
</article>
<input type="text" ng-model="priceSlider.min"/><br/>
<input type="text" ng-model="priceSlider.max"/><br/> <article>
<h2>Toggle slider example</h2>
<rzslider <button ng-click="toggle()">{{ visible ? 'Hide' : 'Show' }}</button>
rz-slider-draggable-range="true" <br/>
rz-slider-floor="priceSlider.floor" <div ng-show="visible">
rz-slider-ceil="priceSlider.ceil" <rzslider
rz-slider-model="priceSlider.min" rz-slider-model="slider_toggle.value"
rz-slider-high="priceSlider.max" rz-slider-options="slider_toggle.options"></rzslider>
rz-slider-step="5" </div>
rz-slider-tpl-url="rzSliderTpl.html"></rzslider> </article>
</article>
<article>
<h2>Toggle slider example</h2>
<button ng-click="toggle()">Show</button>
<div ng-show="visible">
<rzslider rz-slider-model="toggleSlider.value"
rz-slider-floor="toggleSlider.floor"
rz-slider-ceil="toggleSlider.ceil"></rzslider>
</div>
</article>
<article>
<h2>Disabled slider example</h2>
<label>Disable slider <input type="checkbox" ng-model="disableSlider"></label>
<rzslider rz-slider-model="priceSlider8"
rz-slider-floor="0"
rz-slider-ceil="10"
rz-slider-disabled="disableSlider"></rzslider>
</article>
</div>
</div>
</body>
<script src="../bower_components/angular/angular.js"></script> <script src="../bower_components/angular/angular.js"></script>
<script src="../dist/rzslider.js"></script> <script src="../dist/rzslider.js"></script>
<script> <script src="demo.js"></script>
var app = angular.module('rzSliderDemo', ['rzModule']);
app.controller('MainCtrl', function($scope, $timeout) {
$scope.priceSlider = {
min: 100,
max: 400,
ceil: 500,
floor: 0
};
$scope.priceSlider2 = 150;
$scope.priceSlider3 = 250;
$scope.priceSlider4 = 5;
$scope.priceSlider5 = 5;
$scope.priceSlider6 = 1;
$scope.priceSlider7 = {
min: 2,
max: 8
};
$scope.priceSlider8 = 5;
$scope.disableSlider = true;
$scope.translate = function(value) {
return '$' + value;
};
var alphabetArray = 'abcdefghijklmnopqrstuvwxyz'.split('');
$scope.letter = 5;
$scope.letterMax = alphabetArray.length - 1;
$scope.alphabetTranslate = function(value) {
return alphabetArray[value].toUpperCase();
};
$scope.slider_data = {value: 1};
$scope.otherData = {value: 10};
$scope.onStart = function() {
console.info('started', $scope.slider_data.value);
};
$scope.onChange = function() {
console.info('changed', $scope.slider_data.value);
$scope.otherData.value = $scope.slider_data.value * 10;
};
$scope.onEnd = function() {
console.info('ended', $scope.slider_data.value);
};
$scope.visible = false;
$scope.toggle = function() {
$scope.visible = !$scope.visible;
$timeout(function() {
$scope.$broadcast('rzSliderForceRender');
});
};
$scope.toggleSlider = {
value: 1,
ceil: 500,
floor: 0
};
});
</script>
</body>
</html> </html>
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
/*jslint unparam: true */ /*jslint unparam: true */
/*global angular: false, console: false, define, module */ /*global angular: false, console: false, define, module */
(function (root, factory) { (function(root, factory) {
'use strict'; 'use strict';
if (typeof define === 'function' && define.amd) { if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module. // AMD. Register as an anonymous module.
...@@ -27,1458 +27,1344 @@ ...@@ -27,1458 +27,1344 @@
factory(root.angular); factory(root.angular);
} }
}(this, function (angular) { }(this, function(angular) {
'use strict'; 'use strict';
var module = angular.module('rzModule', []) var module = angular.module('rzModule', [])
.value('throttle', .value('defaultOptions', {
/** floor: 0,
* throttle ceil: null, //defaults to rz-slider-model
* step: 1,
* Taken from underscore project precision: 0,
* translate: null,
* @param {Function} func draggableRange: false,
* @param {number} wait showSelectionBar: false,
* @param {ThrottleOptions} options hideLimitLabels: false,
* @returns {Function} readOnly: false,
*/ disabled: false,
function throttle(func, wait, options) { interval: 350,
'use strict'; showTicks: false,
var getTime = (Date.now || function() { showTicksValues: false,
return new Date().getTime(); onStart: null,
}); onChange: null,
var context, args, result; onEnd: null
var timeout = null; })
var previous = 0;
options = options || {}; .value('throttle',
var later = function() { /**
previous = options.leading === false ? 0 : getTime(); * throttle
timeout = null; *
result = func.apply(context, args); * Taken from underscore project
context = args = null; *
}; * @param {Function} func
return function() { * @param {number} wait
var now = getTime(); * @param {ThrottleOptions} options
if (!previous && options.leading === false) { previous = now; } * @returns {Function}
var remaining = wait - (now - previous); */
context = this; function throttle(func, wait, options) {
args = arguments; 'use strict';
if (remaining <= 0) { var getTime = (Date.now || function() {
clearTimeout(timeout); return new Date().getTime();
timeout = null;
previous = now;
result = func.apply(context, args);
context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
})
.factory('RzSlider', ['$timeout', '$document', '$window', 'throttle', function($timeout, $document, $window, throttle)
{
'use strict';
/**
* Slider
*
* @param {ngScope} scope The AngularJS scope
* @param {Element} sliderElem The slider directive element wrapped in jqLite
* @param {*} attributes The slider directive attributes
* @constructor
*/
var Slider = function(scope, sliderElem, attributes)
{
/**
* The slider's scope
*
* @type {ngScope}
*/
this.scope = scope;
/**
* The slider attributes
*
* @type {Object}
*/
this.attributes = attributes;
/**
* Slider element wrapped in jqLite
*
* @type {jqLite}
*/
this.sliderElem = sliderElem;
/**
* Slider type
*
* @type {boolean} Set to true for range slider
*/
this.range = attributes.rzSliderHigh !== undefined && attributes.rzSliderModel !== undefined;
/**
* Whether to allow draggable range
*
* @type {boolean} Set to true for draggable range slider
*/
this.dragRange = this.range && attributes.rzSliderDraggableRange === 'true';
/**
* Values recorded when first dragging the bar
*
* @type {Object}
*/
this.dragging = {
active: false,
value: 0,
difference: 0,
offset: 0,
lowDist: 0,
highDist: 0
};
/**
* Half of the width of the slider handles
*
* @type {number}
*/
this.handleHalfWidth = 0;
/**
* Always show selection bar
*
* @type {boolean}
*/
this.alwaysShowBar = !!attributes.rzSliderAlwaysShowBar;
/**
* Maximum left the slider handle can have
*
* @type {number}
*/
this.maxLeft = 0;
/**
* Precision
*
* @type {number}
*/
this.precision = 0;
/**
* Step
*
* @type {number}
*/
this.step = 0;
/**
* The name of the handle we are currently tracking
*
* @type {string}
*/
this.tracking = '';
/**
* Minimum value (floor) of the model
*
* @type {number}
*/
this.minValue = 0;
/**
* Maximum value (ceiling) of the model
*
* @type {number}
*/
this.maxValue = 0;
/**
* Hide limit labels
*
* @type {boolean}
*/
this.hideLimitLabels = !!attributes.rzSliderHideLimitLabels;
/**
* Only present model values
*
* Do not allow to change values
*
* @type {boolean}
*/
this.presentOnly = attributes.rzSliderPresentOnly === 'true';
/**
* Display ticks on each possible value.
*
* @type {boolean}
*/
this.showTicks = attributes.rzSliderShowTicks || attributes.rzSliderShowTicksValue;
/**
* Display the value on each tick.
*
* @type {boolean}
*/
this.showTicksValue = attributes.rzSliderShowTicksValue;
/**
* Disable the slider
*
* @type {boolean}
*/
this.disabled = this.scope.rzSliderDisabled;
/**
* The interval at which the slider updates when the model/high values
* are altered from outside the slider
*
* @type {number}
*/
this.interval = this.scope.rzSliderInterval !== null ? this.scope.rzSliderInterval : 350;
/**
* The delta between min and max value
*
* @type {number}
*/
this.valueRange = 0;
/**
* Set to true if init method already executed
*
* @type {boolean}
*/
this.initHasRun = false;
/**
* Custom translate function
*
* @type {function}
*/
this.customTrFn = this.scope.rzSliderTranslate() || function(value) { return String(value); };
/**
* Array of de-registration functions to call on $destroy
*
* @type {Array.<Function>}
*/
this.deRegFuncs = [];
// Slider DOM elements wrapped in jqLite
this.fullBar = null; // The whole slider bar
this.selBar = null; // Highlight between two handles
this.minH = null; // Left slider handle
this.maxH = null; // Right slider handle
this.flrLab = null; // Floor label
this.ceilLab = null; // Ceiling label
this.minLab = null; // Label above the low value
this.maxLab = null; // Label above the high value
this.cmbLab = null; // Combined label
this.ticks = null; // The ticks
// Initialize slider
this.init();
};
// Add instance methods
Slider.prototype = {
/**
* Initialize slider
*
* @returns {undefined}
*/
init: function()
{
var thrLow, thrHigh, unRegFn,
calcDimFn = angular.bind(this, this.calcViewDimensions),
self = this;
this.initElemHandles();
this.addAccessibility();
this.setDisabledState();
this.calcViewDimensions();
this.setMinAndMax();
this.precision = this.scope.rzSliderPrecision === undefined ? 0 : +this.scope.rzSliderPrecision;
this.step = this.scope.rzSliderStep === undefined ? 1 : +this.scope.rzSliderStep;
$timeout(function()
{
self.updateCeilLab();
self.updateFloorLab();
self.initHandles();
self.bindEvents();
}); });
var context, args, result;
// Recalculate slider view dimensions var timeout = null;
unRegFn = this.scope.$on('reCalcViewDimensions', calcDimFn); var previous = 0;
this.deRegFuncs.push(unRegFn); options = options || {};
var later = function() {
// Recalculate stuff if view port dimensions have changed previous = options.leading === false ? 0 : getTime();
angular.element($window).on('resize', calcDimFn); timeout = null;
result = func.apply(context, args);
this.initHasRun = true; context = args = null;
};
// Watch for changes to the model return function() {
var now = getTime();
thrLow = throttle(function() if (!previous && options.leading === false) {
{ previous = now;
self.setMinAndMax();
self.updateLowHandle(self.valueToOffset(self.scope.rzSliderModel));
self.updateSelectionBar();
self.updateTicksScale();
if(self.range)
{
self.updateCmbLabel();
} }
var remaining = wait - (now - previous);
}, self.interval); context = this;
args = arguments;
thrHigh = throttle(function() if (remaining <= 0) {
{ clearTimeout(timeout);
self.setMinAndMax(); timeout = null;
self.updateHighHandle(self.valueToOffset(self.scope.rzSliderHigh)); previous = now;
self.updateSelectionBar(); result = func.apply(context, args);
self.updateTicksScale(); context = args = null;
self.updateCmbLabel(); } else if (!timeout && options.trailing !== false) {
}, self.interval); timeout = setTimeout(later, remaining);
this.scope.$on('rzSliderForceRender', function()
{
self.resetLabelsValue();
thrLow();
if(self.range) { thrHigh(); }
self.resetSlider();
});
// Watchers
unRegFn = this.scope.$watch('rzSliderModel', function(newValue, oldValue)
{
if(newValue === oldValue) { return; }
thrLow();
});
this.deRegFuncs.push(unRegFn);
unRegFn = this.scope.$watch('rzSliderHigh', function(newValue, oldValue)
{
if(newValue === oldValue) { return; }
thrHigh();
});
this.deRegFuncs.push(unRegFn);
this.scope.$watch('rzSliderFloor', function(newValue, oldValue)
{
if(newValue === oldValue) { return; }
self.resetSlider();
});
this.deRegFuncs.push(unRegFn);
unRegFn = this.scope.$watch('rzSliderCeil', function(newValue, oldValue)
{
if(newValue === oldValue) { return; }
self.resetSlider();
});
this.deRegFuncs.push(unRegFn);
unRegFn = this.scope.$watch('rzSliderShowTicks', function(newValue, oldValue)
{
if(newValue === oldValue) { return; }
self.resetSlider();
});
this.deRegFuncs.push(unRegFn);
unRegFn = this.scope.$watch('rzSliderShowTicksValue', function(newValue, oldValue)
{
if(newValue === oldValue) { return; }
self.resetSlider();
});
this.deRegFuncs.push(unRegFn);
unRegFn = this.scope.$watch('rzSliderDisabled', function(newValue, oldValue)
{
if(newValue === oldValue) { return; }
self.resetSlider();
if(self.disabled)
self.unbindEvents();
else
self.bindEvents();
});
this.deRegFuncs.push(unRegFn);
this.scope.$on('$destroy', function()
{
self.unbindEvents();
angular.element($window).off('resize', calcDimFn);
self.deRegFuncs.map(function(unbind) { unbind(); });
});
},
/**
* Resets slider
*
* @returns {undefined}
*/
resetSlider: function()
{
this.setMinAndMax();
this.updateCeilLab();
this.updateFloorLab();
this.setDisabledState();
this.calcViewDimensions();
},
/**
* Set the disabled state based on rzSliderDisabled
*
* @returns {undefined}
*/
setDisabledState: function()
{
this.disabled = this.scope.rzSliderDisabled;
if(this.disabled) {
this.sliderElem.attr('disabled', 'disabled');
}
else {
this.sliderElem.attr('disabled', null);
}
},
/**
* Reset label values
*
* @return {undefined}
*/
resetLabelsValue: function()
{
this.minLab.rzsv = undefined;
this.maxLab.rzsv = undefined;
},
/**
* Initialize slider handles positions and labels
*
* Run only once during initialization and every time view port changes size
*
* @returns {undefined}
*/
initHandles: function()
{
this.updateLowHandle(this.valueToOffset(this.scope.rzSliderModel));
/*
the order here is important since the selection bar should be
updated after the high handle but before the combined label
*/
if(this.range)
this.updateHighHandle(this.valueToOffset(this.scope.rzSliderHigh));
this.updateSelectionBar();
if(this.range)
this.updateCmbLabel();
this.updateTicksScale();
},
/**
* Translate value to human readable format
*
* @param {number|string} value
* @param {jqLite} label
* @param {boolean} [useCustomTr]
* @returns {undefined}
*/
translateFn: function(value, label, useCustomTr)
{
useCustomTr = useCustomTr === undefined ? true : useCustomTr;
var valStr = (useCustomTr ? this.customTrFn(value) : value).toString(),
getWidth = false;
if(label.rzsv === undefined || label.rzsv.length !== valStr.length || (label.rzsv.length > 0 && label.rzsw === 0))
{
getWidth = true;
label.rzsv = valStr;
}
label.text(valStr);
// Update width only when length of the label have changed
if(getWidth) { this.getWidth(label); }
},
/**
* Set maximum and minimum values for the slider
*
* @returns {undefined}
*/
setMinAndMax: function()
{
if(this.scope.rzSliderFloor)
{
this.minValue = +this.scope.rzSliderFloor;
}
else
{
this.minValue = this.scope.rzSliderFloor = 0;
}
if(this.scope.rzSliderCeil)
{
this.maxValue = +this.scope.rzSliderCeil;
}
else
{
this.maxValue = this.scope.rzSliderCeil = this.range ? this.scope.rzSliderHigh : this.scope.rzSliderModel;
}
if(this.scope.rzSliderStep)
{
this.step = +this.scope.rzSliderStep;
}
this.valueRange = this.maxValue - this.minValue;
},
/**
* Set the slider children to variables for easy access
*
* Run only once during initialization
*
* @returns {undefined}
*/
initElemHandles: function()
{
// Assign all slider elements to object properties for easy access
angular.forEach(this.sliderElem.children(), function(elem, index)
{
var jElem = angular.element(elem);
switch(index)
{
case 0: this.fullBar = jElem; break;
case 1: this.selBar = jElem; break;
case 2: this.minH = jElem; break;
case 3: this.maxH = jElem; break;
case 4: this.flrLab = jElem; break;
case 5: this.ceilLab = jElem; break;
case 6: this.minLab = jElem; break;
case 7: this.maxLab = jElem; break;
case 8: this.cmbLab = jElem; break;
case 9: this.ticks = jElem; break;
} }
return result;
};
})
}, this); .factory('RzSlider', ['$timeout', '$document', '$window', 'defaultOptions', 'throttle', function($timeout, $document, $window, defaultOptions, throttle) {
'use strict';
// Initialize offset cache properties
this.selBar.rzsl = 0;
this.minH.rzsl = 0;
this.maxH.rzsl = 0;
this.flrLab.rzsl = 0;
this.ceilLab.rzsl = 0;
this.minLab.rzsl = 0;
this.maxLab.rzsl = 0;
this.cmbLab.rzsl = 0;
// Hide limit labels
if(this.hideLimitLabels)
{
this.flrLab.rzAlwaysHide = true;
this.ceilLab.rzAlwaysHide = true;
this.hideEl(this.flrLab);
this.hideEl(this.ceilLab);
}
if(this.showTicksValue) {
this.flrLab.rzAlwaysHide = true;
this.ceilLab.rzAlwaysHide = true;
this.minLab.rzAlwaysHide = true;
this.maxLab.rzAlwaysHide = true;
this.cmbLab.rzAlwaysHide = true;
this.hideEl(this.flrLab);
this.hideEl(this.ceilLab);
this.hideEl(this.minLab);
this.hideEl(this.maxLab);
this.hideEl(this.cmbLab);
}
// Remove stuff not needed in single slider
if(this.range === false)
{
this.cmbLab.remove();
this.maxLab.remove();
// Hide max handle
this.maxH.rzAlwaysHide = true;
this.maxH[0].style.zIndex = '-1000';
this.hideEl(this.maxH);
}
// Show selection bar for single slider or not
if(this.range === false && this.alwaysShowBar === false)
{
this.maxH.remove();
this.selBar.remove();
}
// If using draggable range, use appropriate cursor for this.selBar.
if (this.dragRange)
{
this.selBar.css('cursor', 'move');
this.selBar.addClass('rz-draggable');
}
},
/**
* Adds accessibility atributes
*
* Run only once during initialization
*
* @returns {undefined}
*/
addAccessibility: function ()
{
this.sliderElem.attr("role", "slider");
},
/**
* Calculate dimensions that are dependent on view port size
*
* Run once during initialization and every time view port changes size.
*
* @returns {undefined}
*/
calcViewDimensions: function ()
{
var handleWidth = this.getWidth(this.minH);
this.handleHalfWidth = handleWidth / 2;
this.barWidth = this.getWidth(this.fullBar);
this.maxLeft = this.barWidth - handleWidth;
this.getWidth(this.sliderElem);
this.sliderElem.rzsl = this.sliderElem[0].getBoundingClientRect().left;
if(this.initHasRun)
{
this.updateFloorLab();
this.updateCeilLab();
this.initHandles();
}
},
/**
* Update the ticks position
*
* @returns {undefined}
*/
updateTicksScale: function() {
if(!this.showTicks) return;
if(!this.step) return; //if step is 0, the following loop will be endless.
var positions = '',
ticksCount = Math.round((this.maxValue - this.minValue) / this.step) + 1;
for (var i = 0; i < ticksCount; i++) {
var value = this.roundStep(this.minValue + i * this.step);
var selectedClass = this.isTickSelected(value) ? 'selected': '';
positions += '<li class="tick '+ selectedClass +'">';
if(this.showTicksValue)
positions += '<span class="tick-value">'+ this.getDisplayValue(value) +'</span>';
positions += '</li>';
}
this.ticks.html(positions);
},
isTickSelected: function(value) {
var tickLeft = this.valueToOffset(value);
if(!this.range && this.alwaysShowBar && value <= this.scope.rzSliderModel)
return true;
if(this.range && value >= this.scope.rzSliderModel && value <= this.scope.rzSliderHigh)
return true;
return false;
},
/**
* Update position of the ceiling label
*
* @returns {undefined}
*/
updateCeilLab: function()
{
this.translateFn(this.scope.rzSliderCeil, this.ceilLab);
this.setLeft(this.ceilLab, this.barWidth - this.ceilLab.rzsw);
this.getWidth(this.ceilLab);
},
/**
* Update position of the floor label
*
* @returns {undefined}
*/
updateFloorLab: function()
{
this.translateFn(this.scope.rzSliderFloor, this.flrLab);
this.getWidth(this.flrLab);
},
/**
* Call the onStart callback if defined
*
* @returns {undefined}
*/
callOnStart: function() {
if(this.scope.rzSliderOnStart) {
var self = this;
$timeout(function() {
self.scope.rzSliderOnStart();
});
}
},
/**
* Call the onChange callback if defined
*
* @returns {undefined}
*/
callOnChange: function() {
if(this.scope.rzSliderOnChange) {
var self = this;
$timeout(function() {
self.scope.rzSliderOnChange();
});
}
},
/**
* Call the onEnd callback if defined
*
* @returns {undefined}
*/
callOnEnd: function() {
if(this.scope.rzSliderOnEnd) {
var self = this;
$timeout(function() {
self.scope.rzSliderOnEnd();
});
}
},
/**
* Update slider handles and label positions
*
* @param {string} which
* @param {number} newOffset
*/
updateHandles: function(which, newOffset)
{
if(which === 'rzSliderModel')
{
this.updateLowHandle(newOffset);
this.updateSelectionBar();
this.updateTicksScale();
if(this.range)
{
this.updateCmbLabel();
}
return;
}
if(which === 'rzSliderHigh') /**
{ * Slider
this.updateHighHandle(newOffset); *
this.updateSelectionBar(); * @param {ngScope} scope The AngularJS scope
this.updateTicksScale(); * @param {Element} sliderElem The slider directive element wrapped in jqLite
* @constructor
*/
var Slider = function(scope, sliderElem) {
/**
* The slider's scope
*
* @type {ngScope}
*/
this.scope = scope;
/**
* Slider element wrapped in jqLite
*
* @type {jqLite}
*/
this.sliderElem = sliderElem;
/**
* Slider type
*
* @type {boolean} Set to true for range slider
*/
this.range = this.scope.rzSliderModel !== undefined && this.scope.rzSliderHigh !== undefined;
/**
* Values recorded when first dragging the bar
*
* @type {Object}
*/
this.dragging = {
active: false,
value: 0,
difference: 0,
offset: 0,
lowDist: 0,
highDist: 0
};
/**
* Half of the width of the slider handles
*
* @type {number}
*/
this.handleHalfWidth = 0;
/**
* Maximum left the slider handle can have
*
* @type {number}
*/
this.maxLeft = 0;
/**
* Precision
*
* @type {number}
*/
this.precision = 0;
/**
* Step
*
* @type {number}
*/
this.step = 0;
/**
* The name of the handle we are currently tracking
*
* @type {string}
*/
this.tracking = '';
/**
* Minimum value (floor) of the model
*
* @type {number}
*/
this.minValue = 0;
/**
* Maximum value (ceiling) of the model
*
* @type {number}
*/
this.maxValue = 0;
/**
* The delta between min and max value
*
* @type {number}
*/
this.valueRange = 0;
/**
* Set to true if init method already executed
*
* @type {boolean}
*/
this.initHasRun = false;
/** If the slider events are already bound to the slider
*
* @type {boolean}
*/
this.eventsBound = false;
// Slider DOM elements wrapped in jqLite
this.fullBar = null; // The whole slider bar
this.selBar = null; // Highlight between two handles
this.minH = null; // Left slider handle
this.maxH = null; // Right slider handle
this.flrLab = null; // Floor label
this.ceilLab = null; // Ceiling label
this.minLab = null; // Label above the low value
this.maxLab = null; // Label above the high value
this.cmbLab = null; // Combined label
this.ticks = null; // The ticks
// Initialize slider
this.init();
};
if(this.range) // Add instance methods
{ Slider.prototype = {
/**
* Initialize slider
*
* @returns {undefined}
*/
init: function() {
var thrLow, thrHigh,
calcDimFn = angular.bind(this, this.calcViewDimensions),
self = this;
this.applyOptions();
this.initElemHandles();
this.addAccessibility();
this.manageEventsBindings();
this.setDisabledState();
this.calcViewDimensions();
this.setMinAndMax();
$timeout(function() {
self.updateCeilLab();
self.updateFloorLab();
self.initHandles();
self.bindEvents();
});
// Recalculate slider view dimensions
this.scope.$on('reCalcViewDimensions', calcDimFn);
// Recalculate stuff if view port dimensions have changed
angular.element($window).on('resize', calcDimFn);
this.initHasRun = true;
// Watch for changes to the model
thrLow = throttle(function() {
self.setMinAndMax();
self.updateLowHandle(self.valueToOffset(self.scope.rzSliderModel));
self.updateSelectionBar();
self.updateTicksScale();
if (self.range) {
self.updateCmbLabel();
}
}, self.interval);
thrHigh = throttle(function() {
self.setMinAndMax();
self.updateHighHandle(self.valueToOffset(self.scope.rzSliderHigh));
self.updateSelectionBar();
self.updateTicksScale();
self.updateCmbLabel();
}, self.interval);
this.scope.$on('rzSliderForceRender', function() {
self.resetLabelsValue();
thrLow();
if (self.range) {
thrHigh();
}
self.resetSlider();
});
// Watchers
this.scope.$watch('rzSliderModel', function(newValue, oldValue) {
if (newValue === oldValue)
return;
thrLow();
});
this.scope.$watch('rzSliderHigh', function(newValue, oldValue) {
if (newValue === oldValue)
return;
thrHigh();
});
this.scope.$watch('rzSliderOptions', function(newValue, oldValue) {
if (newValue === oldValue)
return;
self.applyOptions();
self.resetSlider();
}, true);
this.scope.$on('$destroy', function() {
self.unbindEvents();
angular.element($window).off('resize', calcDimFn);
});
},
/**
* Read the user options and apply them to the slider model
*/
applyOptions: function() {
var userOpts = this.scope.rzSliderOptions;
this.options = {};
for (var option_name in defaultOptions) {
if (!userOpts || userOpts[option_name] === undefined)
this.options[option_name] = defaultOptions[option_name];
else
this.options[option_name] = userOpts[option_name];
}
this.options.draggableRange = this.range && this.options.draggableRange;
this.options.showTicks = this.options.showTicks || this.options.showTicksValues;
if (this.options.translate)
this.customTrFn = this.options.translate;
else
this.customTrFn = function(value) {
return String(value);
};
},
/**
* Resets slider
*
* @returns {undefined}
*/
resetSlider: function() {
this.setMinAndMax();
this.updateCeilLab();
this.updateFloorLab();
this.manageEventsBindings();
this.setDisabledState();
this.calcViewDimensions();
},
/**
* Manage the events bindings based on readOnly and disabled options
*
* @returns {undefined}
*/
manageEventsBindings: function() {
if ((this.options.disabled || this.options.readOnly) && this.eventsBound)
this.unbindEvents();
else if ((!this.options.disabled || !this.options.readOnly) && !this.eventsBound)
this.bindEvents();
},
/**
* Set the disabled state based on rzSliderDisabled
*
* @returns {undefined}
*/
setDisabledState: function() {
if (this.options.disabled) {
this.sliderElem.attr('disabled', 'disabled');
}
else {
this.sliderElem.attr('disabled', null);
}
if (this.options.disabled && this.eventsBound)
this.unbindEvents();
else if (!this.options.disabled && !this.eventsBound)
this.bindEvents();
},
/**
* Reset label values
*
* @return {undefined}
*/
resetLabelsValue: function() {
this.minLab.rzsv = undefined;
this.maxLab.rzsv = undefined;
},
/**
* Initialize slider handles positions and labels
*
* Run only once during initialization and every time view port changes size
*
* @returns {undefined}
*/
initHandles: function() {
this.updateLowHandle(this.valueToOffset(this.scope.rzSliderModel));
/*
the order here is important since the selection bar should be
updated after the high handle but before the combined label
*/
if (this.range)
this.updateHighHandle(this.valueToOffset(this.scope.rzSliderHigh));
this.updateSelectionBar();
if (this.range)
this.updateCmbLabel();
this.updateTicksScale();
},
/**
* Translate value to human readable format
*
* @param {number|string} value
* @param {jqLite} label
* @param {boolean} [useCustomTr]
* @returns {undefined}
*/
translateFn: function(value, label, useCustomTr) {
useCustomTr = useCustomTr === undefined ? true : useCustomTr;
var valStr = String((useCustomTr ? this.customTrFn(value) : value)),
getWidth = false;
if (label.rzsv === undefined || label.rzsv.length !== valStr.length || (label.rzsv.length > 0 && label.rzsw === 0)) {
getWidth = true;
label.rzsv = valStr;
}
label.text(valStr);
// Update width only when length of the label have changed
if (getWidth) {
this.getWidth(label);
}
},
/**
* Set maximum and minimum values for the slider and ensure the model and high
* value match these limits
* @returns {undefined}
*/
setMinAndMax: function() {
this.step = +this.options.step;
this.precision = +this.options.precision;
this.scope.rzSliderModel = this.roundStep(this.scope.rzSliderModel);
if (this.range)
this.scope.rzSliderHigh = this.roundStep(this.scope.rzSliderHigh);
this.minValue = +this.options.floor;
if (this.scope.rzSliderModel < this.minValue)
this.scope.rzSliderModel = this.minValue;
if (this.range && this.scope.rzSliderHigh < this.minValue)
this.scope.rzSliderHigh = this.minValue;
if (this.options.ceil) {
this.maxValue = +this.options.ceil;
if (this.scope.rzSliderModel > this.maxValue)
this.scope.rzSliderModel = this.maxValue;
if (this.range && this.scope.rzSliderHigh > this.maxValue)
this.scope.rzSliderHigh = this.maxValue;
}
else
this.maxValue = this.options.ceil = this.range ? this.scope.rzSliderHigh : this.scope.rzSliderModel;
this.valueRange = this.maxValue - this.minValue;
},
/**
* Set the slider children to variables for easy access
*
* Run only once during initialization
*
* @returns {undefined}
*/
initElemHandles: function() {
// Assign all slider elements to object properties for easy access
angular.forEach(this.sliderElem.children(), function(elem, index) {
var jElem = angular.element(elem);
switch (index) {
case 0:
this.fullBar = jElem;
break;
case 1:
this.selBar = jElem;
break;
case 2:
this.minH = jElem;
break;
case 3:
this.maxH = jElem;
break;
case 4:
this.flrLab = jElem;
break;
case 5:
this.ceilLab = jElem;
break;
case 6:
this.minLab = jElem;
break;
case 7:
this.maxLab = jElem;
break;
case 8:
this.cmbLab = jElem;
break;
case 9:
this.ticks = jElem;
break;
}
}, this);
// Initialize offset cache properties
this.selBar.rzsl = 0;
this.minH.rzsl = 0;
this.maxH.rzsl = 0;
this.flrLab.rzsl = 0;
this.ceilLab.rzsl = 0;
this.minLab.rzsl = 0;
this.maxLab.rzsl = 0;
this.cmbLab.rzsl = 0;
// Hide limit labels
if (this.options.hideLimitLabels) {
this.flrLab.rzAlwaysHide = true;
this.ceilLab.rzAlwaysHide = true;
this.hideEl(this.flrLab);
this.hideEl(this.ceilLab);
}
if (this.options.showTicksValues) {
this.flrLab.rzAlwaysHide = true;
this.ceilLab.rzAlwaysHide = true;
this.minLab.rzAlwaysHide = true;
this.maxLab.rzAlwaysHide = true;
this.cmbLab.rzAlwaysHide = true;
this.hideEl(this.flrLab);
this.hideEl(this.ceilLab);
this.hideEl(this.minLab);
this.hideEl(this.maxLab);
this.hideEl(this.cmbLab);
}
// Remove stuff not needed in single slider
if (this.range === false) {
this.cmbLab.remove();
this.maxLab.remove();
// Hide max handle
this.maxH.rzAlwaysHide = true;
this.maxH[0].style.zIndex = '-1000';
this.hideEl(this.maxH);
}
// Show selection bar for single slider or not
if (this.range === false && this.options.showSelectionBar === false) {
this.maxH.remove();
this.selBar.remove();
}
// If using draggable range, use appropriate cursor for this.selBar.
if (this.options.draggableRange) {
this.selBar.css('cursor', 'move');
this.selBar.addClass('rz-draggable');
}
},
/**
* Adds accessibility atributes
*
* Run only once during initialization
*
* @returns {undefined}
*/
addAccessibility: function() {
this.sliderElem.attr("role", "slider");
},
/**
* Calculate dimensions that are dependent on view port size
*
* Run once during initialization and every time view port changes size.
*
* @returns {undefined}
*/
calcViewDimensions: function() {
var handleWidth = this.getWidth(this.minH);
this.handleHalfWidth = handleWidth / 2;
this.barWidth = this.getWidth(this.fullBar);
this.maxLeft = this.barWidth - handleWidth;
this.getWidth(this.sliderElem);
this.sliderElem.rzsl = this.sliderElem[0].getBoundingClientRect().left;
if (this.initHasRun) {
this.updateFloorLab();
this.updateCeilLab();
this.initHandles();
}
},
/**
* Update the ticks position
*
* @returns {undefined}
*/
updateTicksScale: function() {
if (!this.options.showTicks) return;
if (!this.step) return; //if step is 0, the following loop will be endless.
var positions = '',
ticksCount = Math.round((this.maxValue - this.minValue) / this.step) + 1;
for (var i = 0; i < ticksCount; i++) {
var value = this.roundStep(this.minValue + i * this.step);
var selectedClass = this.isTickSelected(value) ? 'selected' : '';
positions += '<li class="tick ' + selectedClass + '">';
if (this.options.showTicksValues)
positions += '<span class="tick-value">' + this.getDisplayValue(value) + '</span>';
positions += '</li>';
}
this.ticks.html(positions);
},
isTickSelected: function(value) {
if (!this.range && this.options.showSelectionBar && value <= this.scope.rzSliderModel)
return true;
if (this.range && value >= this.scope.rzSliderModel && value <= this.scope.rzSliderHigh)
return true;
return false;
},
/**
* Update position of the ceiling label
*
* @returns {undefined}
*/
updateCeilLab: function() {
this.translateFn(this.maxValue, this.ceilLab);
this.setLeft(this.ceilLab, this.barWidth - this.ceilLab.rzsw);
this.getWidth(this.ceilLab);
},
/**
* Update position of the floor label
*
* @returns {undefined}
*/
updateFloorLab: function() {
this.translateFn(this.minValue, this.flrLab);
this.getWidth(this.flrLab);
},
/**
* Call the onStart callback if defined
*
* @returns {undefined}
*/
callOnStart: function() {
if (this.options.onStart) {
var self = this;
$timeout(function() {
self.options.onStart();
});
}
},
/**
* Call the onChange callback if defined
*
* @returns {undefined}
*/
callOnChange: function() {
if (this.options.onChange) {
var self = this;
$timeout(function() {
self.options.onChange();
});
}
},
/**
* Call the onEnd callback if defined
*
* @returns {undefined}
*/
callOnEnd: function() {
if (this.options.onEnd) {
var self = this;
$timeout(function() {
self.options.onEnd();
});
}
},
/**
* Update slider handles and label positions
*
* @param {string} which
* @param {number} newOffset
*/
updateHandles: function(which, newOffset) {
if (which === 'rzSliderModel') {
this.updateLowHandle(newOffset);
this.updateSelectionBar();
this.updateTicksScale();
if (this.range) {
this.updateCmbLabel();
}
return;
}
if (which === 'rzSliderHigh') {
this.updateHighHandle(newOffset);
this.updateSelectionBar();
this.updateTicksScale();
if (this.range) {
this.updateCmbLabel();
}
return;
}
// Update both
this.updateLowHandle(newOffset);
this.updateHighHandle(newOffset);
this.updateSelectionBar();
this.updateTicksScale();
this.updateCmbLabel(); this.updateCmbLabel();
} },
return;
} /**
* Update low slider handle position and label
// Update both *
this.updateLowHandle(newOffset); * @param {number} newOffset
this.updateHighHandle(newOffset); * @returns {undefined}
this.updateSelectionBar(); */
this.updateTicksScale(); updateLowHandle: function(newOffset) {
this.updateCmbLabel(); this.setLeft(this.minH, newOffset);
}, this.translateFn(this.scope.rzSliderModel, this.minLab);
this.setLeft(this.minLab, newOffset - this.minLab.rzsw / 2 + this.handleHalfWidth);
/**
* Update low slider handle position and label this.shFloorCeil();
* },
* @param {number} newOffset
* @returns {undefined} /**
*/ * Update high slider handle position and label
updateLowHandle: function(newOffset) *
{ * @param {number} newOffset
this.setLeft(this.minH, newOffset); * @returns {undefined}
this.translateFn(this.scope.rzSliderModel, this.minLab); */
this.setLeft(this.minLab, newOffset - this.minLab.rzsw / 2 + this.handleHalfWidth); updateHighHandle: function(newOffset) {
this.setLeft(this.maxH, newOffset);
this.shFloorCeil(); this.translateFn(this.scope.rzSliderHigh, this.maxLab);
}, this.setLeft(this.maxLab, newOffset - this.maxLab.rzsw / 2 + this.handleHalfWidth);
/** this.shFloorCeil();
* Update high slider handle position and label },
*
* @param {number} newOffset /**
* @returns {undefined} * Show / hide floor / ceiling label
*/ *
updateHighHandle: function(newOffset) * @returns {undefined}
{ */
this.setLeft(this.maxH, newOffset); shFloorCeil: function() {
this.translateFn(this.scope.rzSliderHigh, this.maxLab); var flHidden = false, clHidden = false;
this.setLeft(this.maxLab, newOffset - this.maxLab.rzsw / 2 + this.handleHalfWidth);
if (this.minLab.rzsl <= this.flrLab.rzsl + this.flrLab.rzsw + 5) {
flHidden = true;
this.hideEl(this.flrLab);
}
else {
flHidden = false;
this.showEl(this.flrLab);
}
if (this.minLab.rzsl + this.minLab.rzsw >= this.ceilLab.rzsl - this.handleHalfWidth - 10) {
clHidden = true;
this.hideEl(this.ceilLab);
}
else {
clHidden = false;
this.showEl(this.ceilLab);
}
if (this.range) {
if (this.maxLab.rzsl + this.maxLab.rzsw >= this.ceilLab.rzsl - 10) {
this.hideEl(this.ceilLab);
}
else if (!clHidden) {
this.showEl(this.ceilLab);
}
// Hide or show floor label
if (this.maxLab.rzsl <= this.flrLab.rzsl + this.flrLab.rzsw + this.handleHalfWidth) {
this.hideEl(this.flrLab);
}
else if (!flHidden) {
this.showEl(this.flrLab);
}
}
},
/**
* Update slider selection bar, combined label and range label
*
* @returns {undefined}
*/
updateSelectionBar: function() {
this.setWidth(this.selBar, Math.abs(this.maxH.rzsl - this.minH.rzsl) + this.handleHalfWidth);
this.setLeft(this.selBar, this.range ? this.minH.rzsl + this.handleHalfWidth : 0);
},
/**
* Update combined label position and value
*
* @returns {undefined}
*/
updateCmbLabel: function() {
var lowTr, highTr;
if (this.minLab.rzsl + this.minLab.rzsw + 10 >= this.maxLab.rzsl) {
lowTr = this.getDisplayValue(this.scope.rzSliderModel);
highTr = this.getDisplayValue(this.scope.rzSliderHigh);
this.translateFn(lowTr + ' - ' + highTr, this.cmbLab, false);
this.setLeft(this.cmbLab, this.selBar.rzsl + this.selBar.rzsw / 2 - this.cmbLab.rzsw / 2);
this.hideEl(this.minLab);
this.hideEl(this.maxLab);
this.showEl(this.cmbLab);
}
else {
this.showEl(this.maxLab);
this.showEl(this.minLab);
this.hideEl(this.cmbLab);
}
},
/**
* Return the translated value if a translate function is provided else the original value
* @param value
* @returns {*}
*/
getDisplayValue: function(value) {
return this.customTrFn(value);
},
/**
* Round value to step and precision
*
* @param {number} value
* @returns {number}
*/
roundStep: function(value) {
var step = this.step,
remainder = +((value - this.minValue) % step).toFixed(3),
steppedValue = remainder > (step / 2) ? value + step - remainder : value - remainder;
steppedValue = steppedValue.toFixed(this.precision);
return +steppedValue;
},
/**
* Hide element
*
* @param element
* @returns {jqLite} The jqLite wrapped DOM element
*/
hideEl: function(element) {
return element.css({opacity: 0});
},
/**
* Show element
*
* @param element The jqLite wrapped DOM element
* @returns {jqLite} The jqLite
*/
showEl: function(element) {
if (!!element.rzAlwaysHide) {
return element;
}
return element.css({opacity: 1});
},
/**
* Set element left offset
*
* @param {jqLite} elem The jqLite wrapped DOM element
* @param {number} left
* @returns {number}
*/
setLeft: function(elem, left) {
elem.rzsl = left;
elem.css({left: left + 'px'});
return left;
},
/**
* Get element width
*
* @param {jqLite} elem The jqLite wrapped DOM element
* @returns {number}
*/
getWidth: function(elem) {
var val = elem[0].getBoundingClientRect();
elem.rzsw = val.right - val.left;
return elem.rzsw;
},
/**
* Set element width
*
* @param {jqLite} elem The jqLite wrapped DOM element
* @param {number} width
* @returns {number}
*/
setWidth: function(elem, width) {
elem.rzsw = width;
elem.css({width: width + 'px'});
return width;
},
/**
* Translate value to pixel offset
*
* @param {number} val
* @returns {number}
*/
valueToOffset: function(val) {
return (val - this.minValue) * this.maxLeft / this.valueRange || 0;
},
/**
* Translate offset to model value
*
* @param {number} offset
* @returns {number}
*/
offsetToValue: function(offset) {
return (offset / this.maxLeft) * this.valueRange + this.minValue;
},
// Events
/**
* Get the X-coordinate of an event
*
* @param {Object} event The event
* @returns {number}
*/
getEventX: function(event) {
/* http://stackoverflow.com/a/12336075/282882 */
//noinspection JSLint
if ('clientX' in event) {
return event.clientX;
}
return event.originalEvent === undefined ?
event.touches[0].clientX
: event.originalEvent.touches[0].clientX;
},
/**
* Get the handle closest to an event.
*
* @param event {Event} The event
* @returns {jqLite} The handle closest to the event.
*/
getNearestHandle: function(event) {
if (!this.range) {
return this.minH;
}
var offset = this.getEventX(event) - this.sliderElem.rzsl - this.handleHalfWidth;
return Math.abs(offset - this.minH.rzsl) < Math.abs(offset - this.maxH.rzsl) ? this.minH : this.maxH;
},
/**
* Bind mouse and touch events to slider handles
*
* @returns {undefined}
*/
bindEvents: function() {
if (this.options.readOnly || this.options.disabled) return;
var barTracking, barStart, barMove;
if (this.options.draggableRange) {
barTracking = 'rzSliderDrag';
barStart = this.onDragStart;
barMove = this.onDragMove;
}
else {
barTracking = 'rzSliderModel';
barStart = this.onStart;
barMove = this.onMove;
}
this.minH.on('mousedown', angular.bind(this, this.onStart, this.minH, 'rzSliderModel'));
if (this.range) {
this.maxH.on('mousedown', angular.bind(this, this.onStart, this.maxH, 'rzSliderHigh'));
}
this.fullBar.on('mousedown', angular.bind(this, this.onStart, null, null));
this.fullBar.on('mousedown', angular.bind(this, this.onMove, this.fullBar));
this.selBar.on('mousedown', angular.bind(this, barStart, null, barTracking));
this.selBar.on('mousedown', angular.bind(this, barMove, this.selBar));
this.ticks.on('mousedown', angular.bind(this, this.onStart, null, null));
this.ticks.on('mousedown', angular.bind(this, this.onMove, this.ticks));
this.minH.on('touchstart', angular.bind(this, this.onStart, this.minH, 'rzSliderModel'));
if (this.range) {
this.maxH.on('touchstart', angular.bind(this, this.onStart, this.maxH, 'rzSliderHigh'));
}
this.fullBar.on('touchstart', angular.bind(this, this.onStart, null, null));
this.fullBar.on('touchstart', angular.bind(this, this.onMove, this.fullBar));
this.selBar.on('touchstart', angular.bind(this, barStart, null, barTracking));
this.selBar.on('touchstart', angular.bind(this, barMove, this.selBar));
this.ticks.on('touchstart', angular.bind(this, this.onStart, null, null));
this.ticks.on('touchstart', angular.bind(this, this.onMove, this.ticks));
this.eventsBound = true;
},
/**
* Unbind mouse and touch events to slider handles
*
* @returns {undefined}
*/
unbindEvents: function() {
this.minH.off();
this.maxH.off();
this.fullBar.off();
this.selBar.off();
this.ticks.off();
this.eventsBound = false;
},
/**
* onStart event handler
*
* @param {?Object} pointer The jqLite wrapped DOM element; if null, the closest handle is used
* @param {?string} ref The name of the handle being changed; if null, the closest handle's value is modified
* @param {Event} event The event
* @returns {undefined}
*/
onStart: function(pointer, ref, event) {
var ehMove, ehEnd,
eventNames = this.getEventNames(event);
event.stopPropagation();
event.preventDefault();
if (this.tracking !== '') {
return;
}
// We have to do this in case the HTML where the sliders are on
// have been animated into view.
this.calcViewDimensions();
if (pointer) {
this.tracking = ref;
}
else {
pointer = this.getNearestHandle(event);
this.tracking = pointer === this.minH ? 'rzSliderModel' : 'rzSliderHigh';
}
pointer.addClass('rz-active');
ehMove = angular.bind(this, this.dragging.active ? this.onDragMove : this.onMove, pointer);
ehEnd = angular.bind(this, this.onEnd, ehMove);
$document.on(eventNames.moveEvent, ehMove);
$document.one(eventNames.endEvent, ehEnd);
this.callOnStart();
},
/**
* onMove event handler
*
* @param {jqLite} pointer
* @param {Event} event The event
* @returns {undefined}
*/
onMove: function(pointer, event) {
var eventX = this.getEventX(event),
sliderLO, newOffset, newValue;
sliderLO = this.sliderElem.rzsl;
newOffset = eventX - sliderLO - this.handleHalfWidth;
if (newOffset <= 0) {
if (pointer.rzsl === 0)
return;
newValue = this.minValue;
newOffset = 0;
}
else if (newOffset >= this.maxLeft) {
if (pointer.rzsl === this.maxLeft)
return;
newValue = this.maxValue;
newOffset = this.maxLeft;
}
else {
newValue = this.offsetToValue(newOffset);
newValue = this.roundStep(newValue);
newOffset = this.valueToOffset(newValue);
}
this.positionTrackingHandle(newValue, newOffset);
},
/**
* onDragStart event handler
*
* Handles dragging of the middle bar.
*
* @param {Object} pointer The jqLite wrapped DOM element
* @param {string} ref One of the refLow, refHigh values
* @param {Event} event The event
* @returns {undefined}
*/
onDragStart: function(pointer, ref, event) {
var offset = this.getEventX(event) - this.sliderElem.rzsl - this.handleHalfWidth;
this.dragging = {
active: true,
value: this.offsetToValue(offset),
difference: this.scope.rzSliderHigh - this.scope.rzSliderModel,
offset: offset,
lowDist: offset - this.minH.rzsl,
highDist: this.maxH.rzsl - offset
};
this.minH.addClass('rz-active');
this.maxH.addClass('rz-active');
this.shFloorCeil(); this.onStart(pointer, ref, event);
}, },
/**
* onDragMove event handler
*
* Handles dragging of the middle bar.
*
* @param {jqLite} pointer
* @param {Event} event The event
* @returns {undefined}
*/
onDragMove: function(pointer, event) {
var newOffset = this.getEventX(event) - this.sliderElem.rzsl - this.handleHalfWidth,
newMinOffset, newMaxOffset,
newMinValue, newMaxValue;
if (newOffset <= this.dragging.lowDist) {
if (pointer.rzsl === this.dragging.lowDist) {
return;
}
newMinValue = this.minValue;
newMinOffset = 0;
newMaxValue = this.dragging.difference;
newMaxOffset = this.valueToOffset(newMaxValue);
}
else if (newOffset >= this.maxLeft - this.dragging.highDist) {
if (pointer.rzsl === this.dragging.highDist) {
return;
}
newMaxValue = this.maxValue;
newMaxOffset = this.maxLeft;
newMinValue = this.maxValue - this.dragging.difference;
newMinOffset = this.valueToOffset(newMinValue);
}
else {
newMinValue = this.offsetToValue(newOffset - this.dragging.lowDist);
newMinValue = this.roundStep(newMinValue);
newMinOffset = this.valueToOffset(newMinValue);
newMaxValue = newMinValue + this.dragging.difference;
newMaxOffset = this.valueToOffset(newMaxValue);
}
this.positionTrackingBar(newMinValue, newMaxValue, newMinOffset, newMaxOffset);
},
/**
* Set the new value and offset for the entire bar
*
* @param {number} newMinValue the new minimum value
* @param {number} newMaxValue the new maximum value
* @param {number} newMinOffset the new minimum offset
* @param {number} newMaxOffset the new maximum offset
*/
positionTrackingBar: function(newMinValue, newMaxValue, newMinOffset, newMaxOffset) {
this.scope.rzSliderModel = newMinValue;
this.scope.rzSliderHigh = newMaxValue;
this.updateHandles('rzSliderModel', newMinOffset);
this.updateHandles('rzSliderHigh', newMaxOffset);
this.scope.$apply();
this.callOnChange();
},
/**
* Set the new value and offset to the current tracking handle
*
* @param {number} newValue new model value
* @param {number} newOffset new offset value
*/
positionTrackingHandle: function(newValue, newOffset) {
if (this.range) {
/* This is to check if we need to switch the min and max handles*/
if (this.tracking === 'rzSliderModel' && newValue >= this.scope.rzSliderHigh) {
this.scope[this.tracking] = this.scope.rzSliderHigh;
this.updateHandles(this.tracking, this.maxH.rzsl);
this.tracking = 'rzSliderHigh';
this.minH.removeClass('rz-active');
this.maxH.addClass('rz-active');
/* We need to apply here because we are not sure that we will enter the next block */
this.scope.$apply();
this.callOnChange();
}
else if (this.tracking === 'rzSliderHigh' && newValue <= this.scope.rzSliderModel) {
this.scope[this.tracking] = this.scope.rzSliderModel;
this.updateHandles(this.tracking, this.minH.rzsl);
this.tracking = 'rzSliderModel';
this.maxH.removeClass('rz-active');
this.minH.addClass('rz-active');
/* We need to apply here because we are not sure that we will enter the next block */
this.scope.$apply();
this.callOnChange();
}
}
if (this.scope[this.tracking] !== newValue) {
this.scope[this.tracking] = newValue;
this.updateHandles(this.tracking, newOffset);
this.scope.$apply();
this.callOnChange();
}
},
/**
* onEnd event handler
*
* @param {Event} event The event
* @param {Function} ehMove The the bound move event handler
* @returns {undefined}
*/
onEnd: function(ehMove, event) {
var moveEventName = this.getEventNames(event).moveEvent;
/** this.minH.removeClass('rz-active');
* Show / hide floor / ceiling label this.maxH.removeClass('rz-active');
*
* @returns {undefined}
*/
shFloorCeil: function()
{
var flHidden = false, clHidden = false;
if(this.minLab.rzsl <= this.flrLab.rzsl + this.flrLab.rzsw + 5)
{
flHidden = true;
this.hideEl(this.flrLab);
}
else
{
flHidden = false;
this.showEl(this.flrLab);
}
if(this.minLab.rzsl + this.minLab.rzsw >= this.ceilLab.rzsl - this.handleHalfWidth - 10)
{
clHidden = true;
this.hideEl(this.ceilLab);
}
else
{
clHidden = false;
this.showEl(this.ceilLab);
}
if(this.range)
{
if(this.maxLab.rzsl + this.maxLab.rzsw >= this.ceilLab.rzsl - 10)
{
this.hideEl(this.ceilLab);
}
else if( ! clHidden)
{
this.showEl(this.ceilLab);
}
// Hide or show floor label $document.off(moveEventName, ehMove);
if(this.maxLab.rzsl <= this.flrLab.rzsl + this.flrLab.rzsw + this.handleHalfWidth)
{ this.scope.$emit('slideEnded');
this.hideEl(this.flrLab); this.tracking = '';
this.dragging.active = false;
this.callOnEnd();
},
/**
* Get event names for move and event end
*
* @param {Event} event The event
*
* @return {{moveEvent: string, endEvent: string}}
*/
getEventNames: function(event) {
var eventNames = {moveEvent: '', endEvent: ''};
if (event.touches || (event.originalEvent !== undefined && event.originalEvent.touches)) {
eventNames.moveEvent = 'touchmove';
eventNames.endEvent = 'touchend';
}
else {
eventNames.moveEvent = 'mousemove';
eventNames.endEvent = 'mouseup';
}
return eventNames;
} }
else if( ! flHidden)
{
this.showEl(this.flrLab);
}
}
},
/**
* Update slider selection bar, combined label and range label
*
* @returns {undefined}
*/
updateSelectionBar: function()
{
this.setWidth(this.selBar, Math.abs(this.maxH.rzsl - this.minH.rzsl) + this.handleHalfWidth);
this.setLeft(this.selBar, this.range ? this.minH.rzsl + this.handleHalfWidth : 0);
},
/**
* Update combined label position and value
*
* @returns {undefined}
*/
updateCmbLabel: function()
{
var lowTr, highTr;
if(this.minLab.rzsl + this.minLab.rzsw + 10 >= this.maxLab.rzsl)
{
lowTr = this.getDisplayValue(this.scope.rzSliderModel);
highTr = this.getDisplayValue(this.scope.rzSliderHigh);
this.translateFn(lowTr + ' - ' + highTr, this.cmbLab, false);
this.setLeft(this.cmbLab, this.selBar.rzsl + this.selBar.rzsw / 2 - this.cmbLab.rzsw / 2);
this.hideEl(this.minLab);
this.hideEl(this.maxLab);
this.showEl(this.cmbLab);
}
else
{
this.showEl(this.maxLab);
this.showEl(this.minLab);
this.hideEl(this.cmbLab);
}
},
/**
* Return the translated value if a translate function is provided else the original value
* @param value
* @returns {*}
*/
getDisplayValue: function(value) {
return this.customTrFn ? this.customTrFn(value): value;
},
/**
* Round value to step and precision
*
* @param {number} value
* @returns {number}
*/
roundStep: function(value)
{
var step = this.step,
remainder = +((value - this.minValue) % step).toFixed(3),
steppedValue = remainder > (step / 2) ? value + step - remainder : value - remainder;
steppedValue = steppedValue.toFixed(this.precision);
return +steppedValue;
},
/**
* Hide element
*
* @param element
* @returns {jqLite} The jqLite wrapped DOM element
*/
hideEl: function (element)
{
return element.css({opacity: 0});
},
/**
* Show element
*
* @param element The jqLite wrapped DOM element
* @returns {jqLite} The jqLite
*/
showEl: function (element)
{
if(!!element.rzAlwaysHide) { return element; }
return element.css({opacity: 1});
},
/**
* Set element left offset
*
* @param {jqLite} elem The jqLite wrapped DOM element
* @param {number} left
* @returns {number}
*/
setLeft: function (elem, left)
{
elem.rzsl = left;
elem.css({left: left + 'px'});
return left;
},
/**
* Get element width
*
* @param {jqLite} elem The jqLite wrapped DOM element
* @returns {number}
*/
getWidth: function(elem)
{
var val = elem[0].getBoundingClientRect();
elem.rzsw = val.right - val.left;
return elem.rzsw;
},
/**
* Set element width
*
* @param {jqLite} elem The jqLite wrapped DOM element
* @param {number} width
* @returns {number}
*/
setWidth: function(elem, width)
{
elem.rzsw = width;
elem.css({width: width + 'px'});
return width;
},
/**
* Translate value to pixel offset
*
* @param {number} val
* @returns {number}
*/
valueToOffset: function(val)
{
return (val - this.minValue) * this.maxLeft / this.valueRange || 0;
},
/**
* Translate offset to model value
*
* @param {number} offset
* @returns {number}
*/
offsetToValue: function(offset)
{
return (offset / this.maxLeft) * this.valueRange + this.minValue;
},
// Events
/**
* Get the X-coordinate of an event
*
* @param {Object} event The event
* @returns {number}
*/
getEventX: function(event)
{
/* http://stackoverflow.com/a/12336075/282882 */
//noinspection JSLint
if('clientX' in event)
{
return event.clientX;
}
return event.originalEvent === undefined ?
event.touches[0].clientX
: event.originalEvent.touches[0].clientX;
},
/**
* Get the handle closest to an event.
*
* @param event {Event} The event
* @returns {jqLite} The handle closest to the event.
*/
getNearestHandle: function(event)
{
if (!this.range) { return this.minH; }
var offset = this.getEventX(event) - this.sliderElem.rzsl - this.handleHalfWidth;
return Math.abs(offset - this.minH.rzsl) < Math.abs(offset - this.maxH.rzsl) ? this.minH : this.maxH;
},
/**
* Bind mouse and touch events to slider handles
*
* @returns {undefined}
*/
bindEvents: function()
{
if(this.presentOnly || this.disabled) return;
var barTracking, barStart, barMove;
if (this.dragRange)
{
barTracking = 'rzSliderDrag';
barStart = this.onDragStart;
barMove = this.onDragMove;
}
else
{
barTracking = 'rzSliderModel';
barStart = this.onStart;
barMove = this.onMove;
}
this.minH.on('mousedown', angular.bind(this, this.onStart, this.minH, 'rzSliderModel'));
if(this.range) { this.maxH.on('mousedown', angular.bind(this, this.onStart, this.maxH, 'rzSliderHigh')); }
this.fullBar.on('mousedown', angular.bind(this, this.onStart, null, null));
this.fullBar.on('mousedown', angular.bind(this, this.onMove, this.fullBar));
this.selBar.on('mousedown', angular.bind(this, barStart, null, barTracking));
this.selBar.on('mousedown', angular.bind(this, barMove, this.selBar));
this.ticks.on('mousedown', angular.bind(this, this.onStart, null, null));
this.ticks.on('mousedown', angular.bind(this, this.onMove, this.ticks));
this.minH.on('touchstart', angular.bind(this, this.onStart, this.minH, 'rzSliderModel'));
if(this.range) { this.maxH.on('touchstart', angular.bind(this, this.onStart, this.maxH, 'rzSliderHigh')); }
this.fullBar.on('touchstart', angular.bind(this, this.onStart, null, null));
this.fullBar.on('touchstart', angular.bind(this, this.onMove, this.fullBar));
this.selBar.on('touchstart', angular.bind(this, barStart, null, barTracking));
this.selBar.on('touchstart', angular.bind(this, barMove, this.selBar));
this.ticks.on('touchstart', angular.bind(this, this.onStart, null, null));
this.ticks.on('touchstart', angular.bind(this, this.onMove, this.ticks));
},
/**
* Unbind mouse and touch events to slider handles
*
* @returns {undefined}
*/
unbindEvents: function()
{
this.minH.off();
this.maxH.off();
this.fullBar.off();
this.selBar.off();
this.ticks.off();
},
/**
* onStart event handler
*
* @param {?Object} pointer The jqLite wrapped DOM element; if null, the closest handle is used
* @param {?string} ref The name of the handle being changed; if null, the closest handle's value is modified
* @param {Event} event The event
* @returns {undefined}
*/
onStart: function (pointer, ref, event)
{
var ehMove, ehEnd,
eventNames = this.getEventNames(event);
event.stopPropagation();
event.preventDefault();
if(this.tracking !== '') { return; }
// We have to do this in case the HTML where the sliders are on
// have been animated into view.
this.calcViewDimensions();
if(pointer)
{
this.tracking = ref;
}
else
{
pointer = this.getNearestHandle(event);
this.tracking = pointer === this.minH ? 'rzSliderModel' : 'rzSliderHigh';
}
pointer.addClass('rz-active');
ehMove = angular.bind(this, this.dragging.active ? this.onDragMove : this.onMove, pointer);
ehEnd = angular.bind(this, this.onEnd, ehMove);
$document.on(eventNames.moveEvent, ehMove);
$document.one(eventNames.endEvent, ehEnd);
this.callOnStart();
},
/**
* onMove event handler
*
* @param {jqLite} pointer
* @param {Event} event The event
* @returns {undefined}
*/
onMove: function (pointer, event)
{
var eventX = this.getEventX(event),
sliderLO, newOffset, newValue;
sliderLO = this.sliderElem.rzsl;
newOffset = eventX - sliderLO - this.handleHalfWidth;
if(newOffset <= 0)
{
if(pointer.rzsl === 0)
return;
newValue = this.minValue;
newOffset = 0;
}
else if(newOffset >= this.maxLeft)
{
if(pointer.rzsl === this.maxLeft)
return;
newValue = this.maxValue;
newOffset = this.maxLeft;
}
else {
newValue = this.offsetToValue(newOffset);
newValue = this.roundStep(newValue);
newOffset = this.valueToOffset(newValue);
}
this.positionTrackingHandle(newValue, newOffset);
},
/**
* onDragStart event handler
*
* Handles dragging of the middle bar.
*
* @param {Object} pointer The jqLite wrapped DOM element
* @param {string} ref One of the refLow, refHigh values
* @param {Event} event The event
* @returns {undefined}
*/
onDragStart: function(pointer, ref, event)
{
var offset = this.getEventX(event) - this.sliderElem.rzsl - this.handleHalfWidth;
this.dragging = {
active: true,
value: this.offsetToValue(offset),
difference: this.scope.rzSliderHigh - this.scope.rzSliderModel,
offset: offset,
lowDist: offset - this.minH.rzsl,
highDist: this.maxH.rzsl - offset
}; };
this.minH.addClass('rz-active');
this.maxH.addClass('rz-active');
this.onStart(pointer, ref, event);
},
/**
* onDragMove event handler
*
* Handles dragging of the middle bar.
*
* @param {jqLite} pointer
* @param {Event} event The event
* @returns {undefined}
*/
onDragMove: function(pointer, event)
{
var newOffset = this.getEventX(event) - this.sliderElem.rzsl - this.handleHalfWidth,
newMinOffset, newMaxOffset,
newMinValue, newMaxValue;
if (newOffset <= this.dragging.lowDist)
{
if (pointer.rzsl === this.dragging.lowDist) { return; }
newMinValue = this.minValue;
newMinOffset = 0;
newMaxValue = this.dragging.difference;
newMaxOffset = this.valueToOffset(newMaxValue);
}
else if (newOffset >= this.maxLeft - this.dragging.highDist)
{
if (pointer.rzsl === this.dragging.highDist) { return; }
newMaxValue = this.maxValue;
newMaxOffset = this.maxLeft;
newMinValue = this.maxValue - this.dragging.difference;
newMinOffset = this.valueToOffset(newMinValue);
}
else
{
newMinValue = this.offsetToValue(newOffset - this.dragging.lowDist);
newMinValue = this.roundStep(newMinValue);
newMinOffset = this.valueToOffset(newMinValue);
newMaxValue = newMinValue + this.dragging.difference;
newMaxOffset = this.valueToOffset(newMaxValue);
}
this.positionTrackingBar(newMinValue, newMaxValue, newMinOffset, newMaxOffset);
},
/** return Slider;
* Set the new value and offset for the entire bar }])
*
* @param {number} newMinValue the new minimum value .directive('rzslider', ['RzSlider', function(RzSlider) {
* @param {number} newMaxValue the new maximum value 'use strict';
* @param {number} newMinOffset the new minimum offset
* @param {number} newMaxOffset the new maximum offset return {
*/ restrict: 'E',
positionTrackingBar: function(newMinValue, newMaxValue, newMinOffset, newMaxOffset) scope: {
{ rzSliderModel: '=?',
this.scope.rzSliderModel = newMinValue; rzSliderHigh: '=?',
this.scope.rzSliderHigh = newMaxValue; rzSliderOptions: '=?',
this.updateHandles('rzSliderModel', newMinOffset); rzSliderTplUrl: '@'
this.updateHandles('rzSliderHigh', newMaxOffset); },
this.scope.$apply();
this.callOnChange(); /**
}, * Return template URL
*
/** * @param {jqLite} elem
* Set the new value and offset to the current tracking handle * @param {Object} attrs
* * @return {string}
* @param {number} newValue new model value */
* @param {number} newOffset new offset value templateUrl: function(elem, attrs) {
*/ //noinspection JSUnresolvedVariable
positionTrackingHandle: function(newValue, newOffset) return attrs.rzSliderTplUrl || 'rzSliderTpl.html';
{ },
if(this.range)
{ link: function(scope, elem) {
/* This is to check if we need to switch the min and max handles*/ return new RzSlider(scope, elem);
if (this.tracking === 'rzSliderModel' && newValue >= this.scope.rzSliderHigh)
{
this.scope[this.tracking] = this.scope.rzSliderHigh;
this.updateHandles(this.tracking, this.maxH.rzsl);
this.tracking = 'rzSliderHigh';
this.minH.removeClass('rz-active');
this.maxH.addClass('rz-active');
/* We need to apply here because we are not sure that we will enter the next block */
this.scope.$apply();
this.callOnChange();
} }
else if(this.tracking === 'rzSliderHigh' && newValue <= this.scope.rzSliderModel) };
{ }]);
this.scope[this.tracking] = this.scope.rzSliderModel;
this.updateHandles(this.tracking, this.minH.rzsl);
this.tracking = 'rzSliderModel';
this.maxH.removeClass('rz-active');
this.minH.addClass('rz-active');
/* We need to apply here because we are not sure that we will enter the next block */
this.scope.$apply();
this.callOnChange();
}
}
if(this.scope[this.tracking] !== newValue)
{
this.scope[this.tracking] = newValue;
this.updateHandles(this.tracking, newOffset);
this.scope.$apply();
this.callOnChange();
}
},
/**
* onEnd event handler
*
* @param {Event} event The event
* @param {Function} ehMove The the bound move event handler
* @returns {undefined}
*/
onEnd: function(ehMove, event)
{
var moveEventName = this.getEventNames(event).moveEvent;
this.minH.removeClass('rz-active');
this.maxH.removeClass('rz-active');
$document.off(moveEventName, ehMove);
this.scope.$emit('slideEnded');
this.tracking = '';
this.dragging.active = false;
this.callOnEnd();
},
/**
* Get event names for move and event end
*
* @param {Event} event The event
*
* @return {{moveEvent: string, endEvent: string}}
*/
getEventNames: function(event)
{
var eventNames = {moveEvent: '', endEvent: ''};
if(event.touches || (event.originalEvent !== undefined && event.originalEvent.touches))
{
eventNames.moveEvent = 'touchmove';
eventNames.endEvent = 'touchend';
}
else
{
eventNames.moveEvent = 'mousemove';
eventNames.endEvent = 'mouseup';
}
return eventNames;
}
};
return Slider;
}])
.directive('rzslider', ['RzSlider', function(RzSlider)
{
'use strict';
return {
restrict: 'E',
scope: {
rzSliderFloor: '=?',
rzSliderCeil: '=?',
rzSliderStep: '@',
rzSliderPrecision: '@',
rzSliderModel: '=?',
rzSliderHigh: '=?',
rzSliderDraggable: '@',
rzSliderTranslate: '&',
rzSliderHideLimitLabels: '=?',
rzSliderAlwaysShowBar: '=?',
rzSliderPresentOnly: '@',
rzSliderOnStart: '&',
rzSliderOnChange: '&',
rzSliderOnEnd: '&',
rzSliderShowTicks: '=?',
rzSliderShowTicksValue: '=?',
rzSliderDisabled: '=?',
rzSliderInterval: '=?',
},
/**
* Return template URL
*
* @param {jqLite} elem
* @param {Object} attrs
* @return {string}
*/
templateUrl: function(elem, attrs) {
//noinspection JSUnresolvedVariable
return attrs.rzSliderTplUrl || 'rzSliderTpl.html';
},
link: function(scope, elem, attr)
{
return new RzSlider(scope, elem, attr);
}
};
}]);
// IDE assist // IDE assist
/** /**
* @name ngScope * @name ngScope
* *
* @property {number} rzSliderModel * @property {number} rzSliderModel
* @property {number} rzSliderHigh * @property {number} rzSliderHigh
* @property {number} rzSliderCeil * @property {Object} rzSliderOptions
*/ */
/** /**
* @name jqLite * @name jqLite
* *
* @property {number|undefined} rzsl rzslider label left offset * @property {number|undefined} rzsl rzslider label left offset
* @property {number|undefined} rzsw rzslider element width * @property {number|undefined} rzsw rzslider element width
* @property {string|undefined} rzsv rzslider label value/text * @property {string|undefined} rzsv rzslider label value/text
* @property {Function} css * @property {Function} css
* @property {Function} text * @property {Function} text
*/ */
/** /**
* @name Event * @name Event
* @property {Array} touches * @property {Array} touches
* @property {Event} originalEvent * @property {Event} originalEvent
*/ */
/** /**
* @name ThrottleOptions * @name ThrottleOptions
* *
* @property {boolean} leading * @property {boolean} leading
* @property {boolean} trailing * @property {boolean} trailing
*/ */
module.run(['$templateCache', function($templateCache) { module.run(['$templateCache', function($templateCache) {
'use strict'; 'use strict';
......
/*! angularjs-slider - v1.1.0 - (c) Rafal Zajac <rzajac@gmail.com>, Valentin Hervieu <valentin@hervieu.me>, Jussi Saarivirta <jusasi@gmail.com>, Angelin Sirbu <angelin.sirbu@gmail.com>, https://github.com/rzajac/angularjs-slider.git - 2015-11-07 */ /*! angularjs-slider - v1.1.0 - (c) Rafal Zajac <rzajac@gmail.com>, Valentin Hervieu <valentin@hervieu.me>, Jussi Saarivirta <jusasi@gmail.com>, Angelin Sirbu <angelin.sirbu@gmail.com>, https://github.com/rzajac/angularjs-slider.git - 2015-11-11 */
rzslider{position:relative;display:inline-block;width:100%;height:4px;margin:30px 0 15px 0;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}rzslider[disabled]{cursor:not-allowed}rzslider[disabled] .rz-pointer{cursor:not-allowed;background-color:#d8e0f3}rzslider span{position:absolute;display:inline-block;white-space:nowrap}rzslider .rz-base{width:100%;height:100%;padding:0}rzslider .rz-bar-wrapper{left:0;z-index:1;width:100%;height:32px;padding-top:16px;margin-top:-16px;box-sizing:border-box}rzslider .rz-bar{left:0;z-index:1;width:100%;height:4px;background:#d8e0f3;-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px}rzslider .rz-bar.rz-selection{z-index:2;background:#0db9f0;-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px}rzslider .rz-pointer{top:-14px;z-index:3;width:32px;height:32px;cursor:pointer;background-color:#0db9f0;-webkit-border-radius:16px;-moz-border-radius:16px;border-radius:16px}rzslider .rz-pointer:after{position:absolute;top:12px;left:12px;width:8px;height:8px;background:#fff;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;content:''}rzslider .rz-pointer:hover:after{background-color:#fff}rzslider .rz-pointer.rz-active:after{background-color:#451aff}rzslider .rz-bubble{bottom:16px;padding:1px 3px;color:#55637d;cursor:default}rzslider .rz-bubble.rz-selection{top:16px}rzslider .rz-bubble.rz-limit{color:#55637d}rzslider .rz-ticks{position:absolute;top:-3px;left:0;z-index:1;display:flex;width:100%;padding:0 11px;margin:0;list-style:none;box-sizing:border-box;justify-content:space-between}rzslider .rz-ticks .tick{width:10px;height:10px;text-align:center;cursor:pointer;background:#d8e0f3;border-radius:50%}rzslider .rz-ticks .tick.selected{background:#0db9f0}rzslider .rz-ticks .tick .tick-value{position:absolute;top:-30px;transform:translate(-50%,0)} rzslider{position:relative;display:inline-block;width:100%;height:4px;margin:30px 0 15px 0;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}rzslider[disabled]{cursor:not-allowed}rzslider[disabled] .rz-pointer{cursor:not-allowed;background-color:#d8e0f3}rzslider span{position:absolute;display:inline-block;white-space:nowrap}rzslider .rz-base{width:100%;height:100%;padding:0}rzslider .rz-bar-wrapper{left:0;z-index:1;width:100%;height:32px;padding-top:16px;margin-top:-16px;box-sizing:border-box}rzslider .rz-bar{left:0;z-index:1;width:100%;height:4px;background:#d8e0f3;-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px}rzslider .rz-bar.rz-selection{z-index:2;background:#0db9f0;-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px}rzslider .rz-pointer{top:-14px;z-index:3;width:32px;height:32px;cursor:pointer;background-color:#0db9f0;-webkit-border-radius:16px;-moz-border-radius:16px;border-radius:16px}rzslider .rz-pointer:after{position:absolute;top:12px;left:12px;width:8px;height:8px;background:#fff;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;content:''}rzslider .rz-pointer:hover:after{background-color:#fff}rzslider .rz-pointer.rz-active:after{background-color:#451aff}rzslider .rz-bubble{bottom:16px;padding:1px 3px;color:#55637d;cursor:default}rzslider .rz-bubble.rz-selection{top:16px}rzslider .rz-bubble.rz-limit{color:#55637d}rzslider .rz-ticks{position:absolute;top:-3px;left:0;z-index:1;display:flex;width:100%;padding:0 11px;margin:0;list-style:none;box-sizing:border-box;justify-content:space-between}rzslider .rz-ticks .tick{width:10px;height:10px;text-align:center;cursor:pointer;background:#d8e0f3;border-radius:50%}rzslider .rz-ticks .tick.selected{background:#0db9f0}rzslider .rz-ticks .tick .tick-value{position:absolute;top:-30px;transform:translate(-50%,0)}
\ No newline at end of file
/*! angularjs-slider - v1.1.0 - (c) Rafal Zajac <rzajac@gmail.com>, Valentin Hervieu <valentin@hervieu.me>, Jussi Saarivirta <jusasi@gmail.com>, Angelin Sirbu <angelin.sirbu@gmail.com>, https://github.com/rzajac/angularjs-slider.git - 2015-11-07 */ /*! angularjs-slider - v1.1.0 - (c) Rafal Zajac <rzajac@gmail.com>, Valentin Hervieu <valentin@hervieu.me>, Jussi Saarivirta <jusasi@gmail.com>, Angelin Sirbu <angelin.sirbu@gmail.com>, https://github.com/rzajac/angularjs-slider.git - 2015-11-11 */
!function(a,b){"use strict";"function"==typeof define&&define.amd?define(["angular"],b):"object"==typeof module&&module.exports?module.exports=b(require("angular")):b(a.angular)}(this,function(a){"use strict";var b=a.module("rzModule",[]).value("throttle",function(a,b,c){var d,e,f,g=Date.now||function(){return(new Date).getTime()},h=null,i=0;c=c||{};var j=function(){i=c.leading===!1?0:g(),h=null,f=a.apply(d,e),d=e=null};return function(){var k=g();i||c.leading!==!1||(i=k);var l=b-(k-i);return d=this,e=arguments,0>=l?(clearTimeout(h),h=null,i=k,f=a.apply(d,e),d=e=null):h||c.trailing===!1||(h=setTimeout(j,l)),f}}).factory("RzSlider",["$timeout","$document","$window","throttle",function(b,c,d,e){var f=function(a,b,c){this.scope=a,this.attributes=c,this.sliderElem=b,this.range=void 0!==c.rzSliderHigh&&void 0!==c.rzSliderModel,this.dragRange=this.range&&"true"===c.rzSliderDraggableRange,this.dragging={active:!1,value:0,difference:0,offset:0,lowDist:0,highDist:0},this.handleHalfWidth=0,this.alwaysShowBar=!!c.rzSliderAlwaysShowBar,this.maxLeft=0,this.precision=0,this.step=0,this.tracking="",this.minValue=0,this.maxValue=0,this.hideLimitLabels=!!c.rzSliderHideLimitLabels,this.presentOnly="true"===c.rzSliderPresentOnly,this.showTicks=c.rzSliderShowTicks||c.rzSliderShowTicksValue,this.showTicksValue=c.rzSliderShowTicksValue,this.disabled=this.scope.rzSliderDisabled,this.interval=null!==this.scope.rzSliderInterval?this.scope.rzSliderInterval:350,this.valueRange=0,this.initHasRun=!1,this.customTrFn=this.scope.rzSliderTranslate()||function(a){return String(a)},this.deRegFuncs=[],this.fullBar=null,this.selBar=null,this.minH=null,this.maxH=null,this.flrLab=null,this.ceilLab=null,this.minLab=null,this.maxLab=null,this.cmbLab=null,this.ticks=null,this.init()};return f.prototype={init:function(){var c,f,g,h=a.bind(this,this.calcViewDimensions),i=this;this.initElemHandles(),this.addAccessibility(),this.setDisabledState(),this.calcViewDimensions(),this.setMinAndMax(),this.precision=void 0===this.scope.rzSliderPrecision?0:+this.scope.rzSliderPrecision,this.step=void 0===this.scope.rzSliderStep?1:+this.scope.rzSliderStep,b(function(){i.updateCeilLab(),i.updateFloorLab(),i.initHandles(),i.bindEvents()}),g=this.scope.$on("reCalcViewDimensions",h),this.deRegFuncs.push(g),a.element(d).on("resize",h),this.initHasRun=!0,c=e(function(){i.setMinAndMax(),i.updateLowHandle(i.valueToOffset(i.scope.rzSliderModel)),i.updateSelectionBar(),i.updateTicksScale(),i.range&&i.updateCmbLabel()},i.interval),f=e(function(){i.setMinAndMax(),i.updateHighHandle(i.valueToOffset(i.scope.rzSliderHigh)),i.updateSelectionBar(),i.updateTicksScale(),i.updateCmbLabel()},i.interval),this.scope.$on("rzSliderForceRender",function(){i.resetLabelsValue(),c(),i.range&&f(),i.resetSlider()}),g=this.scope.$watch("rzSliderModel",function(a,b){a!==b&&c()}),this.deRegFuncs.push(g),g=this.scope.$watch("rzSliderHigh",function(a,b){a!==b&&f()}),this.deRegFuncs.push(g),this.scope.$watch("rzSliderFloor",function(a,b){a!==b&&i.resetSlider()}),this.deRegFuncs.push(g),g=this.scope.$watch("rzSliderCeil",function(a,b){a!==b&&i.resetSlider()}),this.deRegFuncs.push(g),g=this.scope.$watch("rzSliderShowTicks",function(a,b){a!==b&&i.resetSlider()}),this.deRegFuncs.push(g),g=this.scope.$watch("rzSliderShowTicksValue",function(a,b){a!==b&&i.resetSlider()}),this.deRegFuncs.push(g),g=this.scope.$watch("rzSliderDisabled",function(a,b){a!==b&&(i.resetSlider(),i.disabled?i.unbindEvents():i.bindEvents())}),this.deRegFuncs.push(g),this.scope.$on("$destroy",function(){i.unbindEvents(),a.element(d).off("resize",h),i.deRegFuncs.map(function(a){a()})})},resetSlider:function(){this.setMinAndMax(),this.updateCeilLab(),this.updateFloorLab(),this.setDisabledState(),this.calcViewDimensions()},setDisabledState:function(){this.disabled=this.scope.rzSliderDisabled,this.disabled?this.sliderElem.attr("disabled","disabled"):this.sliderElem.attr("disabled",null)},resetLabelsValue:function(){this.minLab.rzsv=void 0,this.maxLab.rzsv=void 0},initHandles:function(){this.updateLowHandle(this.valueToOffset(this.scope.rzSliderModel)),this.range&&this.updateHighHandle(this.valueToOffset(this.scope.rzSliderHigh)),this.updateSelectionBar(),this.range&&this.updateCmbLabel(),this.updateTicksScale()},translateFn:function(a,b,c){c=void 0===c?!0:c;var d=(c?this.customTrFn(a):a).toString(),e=!1;(void 0===b.rzsv||b.rzsv.length!==d.length||b.rzsv.length>0&&0===b.rzsw)&&(e=!0,b.rzsv=d),b.text(d),e&&this.getWidth(b)},setMinAndMax:function(){this.scope.rzSliderFloor?this.minValue=+this.scope.rzSliderFloor:this.minValue=this.scope.rzSliderFloor=0,this.scope.rzSliderCeil?this.maxValue=+this.scope.rzSliderCeil:this.maxValue=this.scope.rzSliderCeil=this.range?this.scope.rzSliderHigh:this.scope.rzSliderModel,this.scope.rzSliderStep&&(this.step=+this.scope.rzSliderStep),this.valueRange=this.maxValue-this.minValue},initElemHandles:function(){a.forEach(this.sliderElem.children(),function(b,c){var d=a.element(b);switch(c){case 0:this.fullBar=d;break;case 1:this.selBar=d;break;case 2:this.minH=d;break;case 3:this.maxH=d;break;case 4:this.flrLab=d;break;case 5:this.ceilLab=d;break;case 6:this.minLab=d;break;case 7:this.maxLab=d;break;case 8:this.cmbLab=d;break;case 9:this.ticks=d}},this),this.selBar.rzsl=0,this.minH.rzsl=0,this.maxH.rzsl=0,this.flrLab.rzsl=0,this.ceilLab.rzsl=0,this.minLab.rzsl=0,this.maxLab.rzsl=0,this.cmbLab.rzsl=0,this.hideLimitLabels&&(this.flrLab.rzAlwaysHide=!0,this.ceilLab.rzAlwaysHide=!0,this.hideEl(this.flrLab),this.hideEl(this.ceilLab)),this.showTicksValue&&(this.flrLab.rzAlwaysHide=!0,this.ceilLab.rzAlwaysHide=!0,this.minLab.rzAlwaysHide=!0,this.maxLab.rzAlwaysHide=!0,this.cmbLab.rzAlwaysHide=!0,this.hideEl(this.flrLab),this.hideEl(this.ceilLab),this.hideEl(this.minLab),this.hideEl(this.maxLab),this.hideEl(this.cmbLab)),this.range===!1&&(this.cmbLab.remove(),this.maxLab.remove(),this.maxH.rzAlwaysHide=!0,this.maxH[0].style.zIndex="-1000",this.hideEl(this.maxH)),this.range===!1&&this.alwaysShowBar===!1&&(this.maxH.remove(),this.selBar.remove()),this.dragRange&&(this.selBar.css("cursor","move"),this.selBar.addClass("rz-draggable"))},addAccessibility:function(){this.sliderElem.attr("role","slider")},calcViewDimensions:function(){var a=this.getWidth(this.minH);this.handleHalfWidth=a/2,this.barWidth=this.getWidth(this.fullBar),this.maxLeft=this.barWidth-a,this.getWidth(this.sliderElem),this.sliderElem.rzsl=this.sliderElem[0].getBoundingClientRect().left,this.initHasRun&&(this.updateFloorLab(),this.updateCeilLab(),this.initHandles())},updateTicksScale:function(){if(this.showTicks&&this.step){for(var a="",b=Math.round((this.maxValue-this.minValue)/this.step)+1,c=0;b>c;c++){var d=this.roundStep(this.minValue+c*this.step),e=this.isTickSelected(d)?"selected":"";a+='<li class="tick '+e+'">',this.showTicksValue&&(a+='<span class="tick-value">'+this.getDisplayValue(d)+"</span>"),a+="</li>"}this.ticks.html(a)}},isTickSelected:function(a){this.valueToOffset(a);return!this.range&&this.alwaysShowBar&&a<=this.scope.rzSliderModel?!0:this.range&&a>=this.scope.rzSliderModel&&a<=this.scope.rzSliderHigh?!0:!1},updateCeilLab:function(){this.translateFn(this.scope.rzSliderCeil,this.ceilLab),this.setLeft(this.ceilLab,this.barWidth-this.ceilLab.rzsw),this.getWidth(this.ceilLab)},updateFloorLab:function(){this.translateFn(this.scope.rzSliderFloor,this.flrLab),this.getWidth(this.flrLab)},callOnStart:function(){if(this.scope.rzSliderOnStart){var a=this;b(function(){a.scope.rzSliderOnStart()})}},callOnChange:function(){if(this.scope.rzSliderOnChange){var a=this;b(function(){a.scope.rzSliderOnChange()})}},callOnEnd:function(){if(this.scope.rzSliderOnEnd){var a=this;b(function(){a.scope.rzSliderOnEnd()})}},updateHandles:function(a,b){return"rzSliderModel"===a?(this.updateLowHandle(b),this.updateSelectionBar(),this.updateTicksScale(),void(this.range&&this.updateCmbLabel())):"rzSliderHigh"===a?(this.updateHighHandle(b),this.updateSelectionBar(),this.updateTicksScale(),void(this.range&&this.updateCmbLabel())):(this.updateLowHandle(b),this.updateHighHandle(b),this.updateSelectionBar(),this.updateTicksScale(),void this.updateCmbLabel())},updateLowHandle:function(a){this.setLeft(this.minH,a),this.translateFn(this.scope.rzSliderModel,this.minLab),this.setLeft(this.minLab,a-this.minLab.rzsw/2+this.handleHalfWidth),this.shFloorCeil()},updateHighHandle:function(a){this.setLeft(this.maxH,a),this.translateFn(this.scope.rzSliderHigh,this.maxLab),this.setLeft(this.maxLab,a-this.maxLab.rzsw/2+this.handleHalfWidth),this.shFloorCeil()},shFloorCeil:function(){var a=!1,b=!1;this.minLab.rzsl<=this.flrLab.rzsl+this.flrLab.rzsw+5?(a=!0,this.hideEl(this.flrLab)):(a=!1,this.showEl(this.flrLab)),this.minLab.rzsl+this.minLab.rzsw>=this.ceilLab.rzsl-this.handleHalfWidth-10?(b=!0,this.hideEl(this.ceilLab)):(b=!1,this.showEl(this.ceilLab)),this.range&&(this.maxLab.rzsl+this.maxLab.rzsw>=this.ceilLab.rzsl-10?this.hideEl(this.ceilLab):b||this.showEl(this.ceilLab),this.maxLab.rzsl<=this.flrLab.rzsl+this.flrLab.rzsw+this.handleHalfWidth?this.hideEl(this.flrLab):a||this.showEl(this.flrLab))},updateSelectionBar:function(){this.setWidth(this.selBar,Math.abs(this.maxH.rzsl-this.minH.rzsl)+this.handleHalfWidth),this.setLeft(this.selBar,this.range?this.minH.rzsl+this.handleHalfWidth:0)},updateCmbLabel:function(){var a,b;this.minLab.rzsl+this.minLab.rzsw+10>=this.maxLab.rzsl?(a=this.getDisplayValue(this.scope.rzSliderModel),b=this.getDisplayValue(this.scope.rzSliderHigh),this.translateFn(a+" - "+b,this.cmbLab,!1),this.setLeft(this.cmbLab,this.selBar.rzsl+this.selBar.rzsw/2-this.cmbLab.rzsw/2),this.hideEl(this.minLab),this.hideEl(this.maxLab),this.showEl(this.cmbLab)):(this.showEl(this.maxLab),this.showEl(this.minLab),this.hideEl(this.cmbLab))},getDisplayValue:function(a){return this.customTrFn?this.customTrFn(a):a},roundStep:function(a){var b=this.step,c=+((a-this.minValue)%b).toFixed(3),d=c>b/2?a+b-c:a-c;return d=d.toFixed(this.precision),+d},hideEl:function(a){return a.css({opacity:0})},showEl:function(a){return a.rzAlwaysHide?a:a.css({opacity:1})},setLeft:function(a,b){return a.rzsl=b,a.css({left:b+"px"}),b},getWidth:function(a){var b=a[0].getBoundingClientRect();return a.rzsw=b.right-b.left,a.rzsw},setWidth:function(a,b){return a.rzsw=b,a.css({width:b+"px"}),b},valueToOffset:function(a){return(a-this.minValue)*this.maxLeft/this.valueRange||0},offsetToValue:function(a){return a/this.maxLeft*this.valueRange+this.minValue},getEventX:function(a){return"clientX"in a?a.clientX:void 0===a.originalEvent?a.touches[0].clientX:a.originalEvent.touches[0].clientX},getNearestHandle:function(a){if(!this.range)return this.minH;var b=this.getEventX(a)-this.sliderElem.rzsl-this.handleHalfWidth;return Math.abs(b-this.minH.rzsl)<Math.abs(b-this.maxH.rzsl)?this.minH:this.maxH},bindEvents:function(){if(!this.presentOnly&&!this.disabled){var b,c,d;this.dragRange?(b="rzSliderDrag",c=this.onDragStart,d=this.onDragMove):(b="rzSliderModel",c=this.onStart,d=this.onMove),this.minH.on("mousedown",a.bind(this,this.onStart,this.minH,"rzSliderModel")),this.range&&this.maxH.on("mousedown",a.bind(this,this.onStart,this.maxH,"rzSliderHigh")),this.fullBar.on("mousedown",a.bind(this,this.onStart,null,null)),this.fullBar.on("mousedown",a.bind(this,this.onMove,this.fullBar)),this.selBar.on("mousedown",a.bind(this,c,null,b)),this.selBar.on("mousedown",a.bind(this,d,this.selBar)),this.ticks.on("mousedown",a.bind(this,this.onStart,null,null)),this.ticks.on("mousedown",a.bind(this,this.onMove,this.ticks)),this.minH.on("touchstart",a.bind(this,this.onStart,this.minH,"rzSliderModel")),this.range&&this.maxH.on("touchstart",a.bind(this,this.onStart,this.maxH,"rzSliderHigh")),this.fullBar.on("touchstart",a.bind(this,this.onStart,null,null)),this.fullBar.on("touchstart",a.bind(this,this.onMove,this.fullBar)),this.selBar.on("touchstart",a.bind(this,c,null,b)),this.selBar.on("touchstart",a.bind(this,d,this.selBar)),this.ticks.on("touchstart",a.bind(this,this.onStart,null,null)),this.ticks.on("touchstart",a.bind(this,this.onMove,this.ticks))}},unbindEvents:function(){this.minH.off(),this.maxH.off(),this.fullBar.off(),this.selBar.off(),this.ticks.off()},onStart:function(b,d,e){var f,g,h=this.getEventNames(e);e.stopPropagation(),e.preventDefault(),""===this.tracking&&(this.calcViewDimensions(),b?this.tracking=d:(b=this.getNearestHandle(e),this.tracking=b===this.minH?"rzSliderModel":"rzSliderHigh"),b.addClass("rz-active"),f=a.bind(this,this.dragging.active?this.onDragMove:this.onMove,b),g=a.bind(this,this.onEnd,f),c.on(h.moveEvent,f),c.one(h.endEvent,g),this.callOnStart())},onMove:function(a,b){var c,d,e,f=this.getEventX(b);if(c=this.sliderElem.rzsl,d=f-c-this.handleHalfWidth,0>=d){if(0===a.rzsl)return;e=this.minValue,d=0}else if(d>=this.maxLeft){if(a.rzsl===this.maxLeft)return;e=this.maxValue,d=this.maxLeft}else e=this.offsetToValue(d),e=this.roundStep(e),d=this.valueToOffset(e);this.positionTrackingHandle(e,d)},onDragStart:function(a,b,c){var d=this.getEventX(c)-this.sliderElem.rzsl-this.handleHalfWidth;this.dragging={active:!0,value:this.offsetToValue(d),difference:this.scope.rzSliderHigh-this.scope.rzSliderModel,offset:d,lowDist:d-this.minH.rzsl,highDist:this.maxH.rzsl-d},this.minH.addClass("rz-active"),this.maxH.addClass("rz-active"),this.onStart(a,b,c)},onDragMove:function(a,b){var c,d,e,f,g=this.getEventX(b)-this.sliderElem.rzsl-this.handleHalfWidth;if(g<=this.dragging.lowDist){if(a.rzsl===this.dragging.lowDist)return;e=this.minValue,c=0,f=this.dragging.difference,d=this.valueToOffset(f)}else if(g>=this.maxLeft-this.dragging.highDist){if(a.rzsl===this.dragging.highDist)return;f=this.maxValue,d=this.maxLeft,e=this.maxValue-this.dragging.difference,c=this.valueToOffset(e)}else e=this.offsetToValue(g-this.dragging.lowDist),e=this.roundStep(e),c=this.valueToOffset(e),f=e+this.dragging.difference,d=this.valueToOffset(f);this.positionTrackingBar(e,f,c,d)},positionTrackingBar:function(a,b,c,d){this.scope.rzSliderModel=a,this.scope.rzSliderHigh=b,this.updateHandles("rzSliderModel",c),this.updateHandles("rzSliderHigh",d),this.scope.$apply(),this.callOnChange()},positionTrackingHandle:function(a,b){this.range&&("rzSliderModel"===this.tracking&&a>=this.scope.rzSliderHigh?(this.scope[this.tracking]=this.scope.rzSliderHigh,this.updateHandles(this.tracking,this.maxH.rzsl),this.tracking="rzSliderHigh",this.minH.removeClass("rz-active"),this.maxH.addClass("rz-active"),this.scope.$apply(),this.callOnChange()):"rzSliderHigh"===this.tracking&&a<=this.scope.rzSliderModel&&(this.scope[this.tracking]=this.scope.rzSliderModel,this.updateHandles(this.tracking,this.minH.rzsl),this.tracking="rzSliderModel",this.maxH.removeClass("rz-active"),this.minH.addClass("rz-active"),this.scope.$apply(),this.callOnChange())),this.scope[this.tracking]!==a&&(this.scope[this.tracking]=a,this.updateHandles(this.tracking,b),this.scope.$apply(),this.callOnChange())},onEnd:function(a,b){var d=this.getEventNames(b).moveEvent;this.minH.removeClass("rz-active"),this.maxH.removeClass("rz-active"),c.off(d,a),this.scope.$emit("slideEnded"),this.tracking="",this.dragging.active=!1,this.callOnEnd()},getEventNames:function(a){var b={moveEvent:"",endEvent:""};return a.touches||void 0!==a.originalEvent&&a.originalEvent.touches?(b.moveEvent="touchmove",b.endEvent="touchend"):(b.moveEvent="mousemove",b.endEvent="mouseup"),b}},f}]).directive("rzslider",["RzSlider",function(a){return{restrict:"E",scope:{rzSliderFloor:"=?",rzSliderCeil:"=?",rzSliderStep:"@",rzSliderPrecision:"@",rzSliderModel:"=?",rzSliderHigh:"=?",rzSliderDraggable:"@",rzSliderTranslate:"&",rzSliderHideLimitLabels:"=?",rzSliderAlwaysShowBar:"=?",rzSliderPresentOnly:"@",rzSliderOnStart:"&",rzSliderOnChange:"&",rzSliderOnEnd:"&",rzSliderShowTicks:"=?",rzSliderShowTicksValue:"=?",rzSliderDisabled:"=?",rzSliderInterval:"=?"},templateUrl:function(a,b){return b.rzSliderTplUrl||"rzSliderTpl.html"},link:function(b,c,d){return new a(b,c,d)}}}]);return b.run(["$templateCache",function(a){a.put("rzSliderTpl.html",'<span class=rz-bar-wrapper><span class=rz-bar></span></span> <span class=rz-bar-wrapper><span class="rz-bar rz-selection"></span></span> <span class=rz-pointer></span> <span class=rz-pointer></span> <span class="rz-bubble rz-limit"></span> <span class="rz-bubble rz-limit"></span> <span class=rz-bubble></span> <span class=rz-bubble></span> <span class=rz-bubble></span><ul class=rz-ticks></ul>')}]),b}); !function(a,b){"use strict";"function"==typeof define&&define.amd?define(["angular"],b):"object"==typeof module&&module.exports?module.exports=b(require("angular")):b(a.angular)}(this,function(a){"use strict";var b=a.module("rzModule",[]).value("defaultOptions",{floor:0,ceil:null,step:1,precision:0,translate:null,draggableRange:!1,showSelectionBar:!1,hideLimitLabels:!1,readOnly:!1,disabled:!1,interval:350,showTicks:!1,showTicksValues:!1,onStart:null,onChange:null,onEnd:null}).value("throttle",function(a,b,c){var d,e,f,g=Date.now||function(){return(new Date).getTime()},h=null,i=0;c=c||{};var j=function(){i=c.leading===!1?0:g(),h=null,f=a.apply(d,e),d=e=null};return function(){var k=g();i||c.leading!==!1||(i=k);var l=b-(k-i);return d=this,e=arguments,0>=l?(clearTimeout(h),h=null,i=k,f=a.apply(d,e),d=e=null):h||c.trailing===!1||(h=setTimeout(j,l)),f}}).factory("RzSlider",["$timeout","$document","$window","defaultOptions","throttle",function(b,c,d,e,f){var g=function(a,b){this.scope=a,this.sliderElem=b,this.range=void 0!==this.scope.rzSliderModel&&void 0!==this.scope.rzSliderHigh,this.dragging={active:!1,value:0,difference:0,offset:0,lowDist:0,highDist:0},this.handleHalfWidth=0,this.maxLeft=0,this.precision=0,this.step=0,this.tracking="",this.minValue=0,this.maxValue=0,this.valueRange=0,this.initHasRun=!1,this.eventsBound=!1,this.fullBar=null,this.selBar=null,this.minH=null,this.maxH=null,this.flrLab=null,this.ceilLab=null,this.minLab=null,this.maxLab=null,this.cmbLab=null,this.ticks=null,this.init()};return g.prototype={init:function(){var c,e,g=a.bind(this,this.calcViewDimensions),h=this;this.applyOptions(),this.initElemHandles(),this.addAccessibility(),this.manageEventsBindings(),this.setDisabledState(),this.calcViewDimensions(),this.setMinAndMax(),b(function(){h.updateCeilLab(),h.updateFloorLab(),h.initHandles(),h.bindEvents()}),this.scope.$on("reCalcViewDimensions",g),a.element(d).on("resize",g),this.initHasRun=!0,c=f(function(){h.setMinAndMax(),h.updateLowHandle(h.valueToOffset(h.scope.rzSliderModel)),h.updateSelectionBar(),h.updateTicksScale(),h.range&&h.updateCmbLabel()},h.interval),e=f(function(){h.setMinAndMax(),h.updateHighHandle(h.valueToOffset(h.scope.rzSliderHigh)),h.updateSelectionBar(),h.updateTicksScale(),h.updateCmbLabel()},h.interval),this.scope.$on("rzSliderForceRender",function(){h.resetLabelsValue(),c(),h.range&&e(),h.resetSlider()}),this.scope.$watch("rzSliderModel",function(a,b){a!==b&&c()}),this.scope.$watch("rzSliderHigh",function(a,b){a!==b&&e()}),this.scope.$watch("rzSliderOptions",function(a,b){a!==b&&(h.applyOptions(),h.resetSlider())},!0),this.scope.$on("$destroy",function(){h.unbindEvents(),a.element(d).off("resize",g)})},applyOptions:function(){var a=this.scope.rzSliderOptions;this.options={};for(var b in e)a&&void 0!==a[b]?this.options[b]=a[b]:this.options[b]=e[b];this.options.draggableRange=this.range&&this.options.draggableRange,this.options.showTicks=this.options.showTicks||this.options.showTicksValues,this.options.translate?this.customTrFn=this.options.translate:this.customTrFn=function(a){return String(a)}},resetSlider:function(){this.setMinAndMax(),this.updateCeilLab(),this.updateFloorLab(),this.manageEventsBindings(),this.setDisabledState(),this.calcViewDimensions()},manageEventsBindings:function(){(this.options.disabled||this.options.readOnly)&&this.eventsBound?this.unbindEvents():this.options.disabled&&this.options.readOnly||this.eventsBound||this.bindEvents()},setDisabledState:function(){this.options.disabled?this.sliderElem.attr("disabled","disabled"):this.sliderElem.attr("disabled",null),this.options.disabled&&this.eventsBound?this.unbindEvents():this.options.disabled||this.eventsBound||this.bindEvents()},resetLabelsValue:function(){this.minLab.rzsv=void 0,this.maxLab.rzsv=void 0},initHandles:function(){this.updateLowHandle(this.valueToOffset(this.scope.rzSliderModel)),this.range&&this.updateHighHandle(this.valueToOffset(this.scope.rzSliderHigh)),this.updateSelectionBar(),this.range&&this.updateCmbLabel(),this.updateTicksScale()},translateFn:function(a,b,c){c=void 0===c?!0:c;var d=String(c?this.customTrFn(a):a),e=!1;(void 0===b.rzsv||b.rzsv.length!==d.length||b.rzsv.length>0&&0===b.rzsw)&&(e=!0,b.rzsv=d),b.text(d),e&&this.getWidth(b)},setMinAndMax:function(){this.step=+this.options.step,this.precision=+this.options.precision,this.scope.rzSliderModel=this.roundStep(this.scope.rzSliderModel),this.range&&(this.scope.rzSliderHigh=this.roundStep(this.scope.rzSliderHigh)),this.minValue=+this.options.floor,this.scope.rzSliderModel<this.minValue&&(this.scope.rzSliderModel=this.minValue),this.range&&this.scope.rzSliderHigh<this.minValue&&(this.scope.rzSliderHigh=this.minValue),this.options.ceil?(this.maxValue=+this.options.ceil,this.scope.rzSliderModel>this.maxValue&&(this.scope.rzSliderModel=this.maxValue),this.range&&this.scope.rzSliderHigh>this.maxValue&&(this.scope.rzSliderHigh=this.maxValue)):this.maxValue=this.options.ceil=this.range?this.scope.rzSliderHigh:this.scope.rzSliderModel,this.valueRange=this.maxValue-this.minValue},initElemHandles:function(){a.forEach(this.sliderElem.children(),function(b,c){var d=a.element(b);switch(c){case 0:this.fullBar=d;break;case 1:this.selBar=d;break;case 2:this.minH=d;break;case 3:this.maxH=d;break;case 4:this.flrLab=d;break;case 5:this.ceilLab=d;break;case 6:this.minLab=d;break;case 7:this.maxLab=d;break;case 8:this.cmbLab=d;break;case 9:this.ticks=d}},this),this.selBar.rzsl=0,this.minH.rzsl=0,this.maxH.rzsl=0,this.flrLab.rzsl=0,this.ceilLab.rzsl=0,this.minLab.rzsl=0,this.maxLab.rzsl=0,this.cmbLab.rzsl=0,this.options.hideLimitLabels&&(this.flrLab.rzAlwaysHide=!0,this.ceilLab.rzAlwaysHide=!0,this.hideEl(this.flrLab),this.hideEl(this.ceilLab)),this.options.showTicksValues&&(this.flrLab.rzAlwaysHide=!0,this.ceilLab.rzAlwaysHide=!0,this.minLab.rzAlwaysHide=!0,this.maxLab.rzAlwaysHide=!0,this.cmbLab.rzAlwaysHide=!0,this.hideEl(this.flrLab),this.hideEl(this.ceilLab),this.hideEl(this.minLab),this.hideEl(this.maxLab),this.hideEl(this.cmbLab)),this.range===!1&&(this.cmbLab.remove(),this.maxLab.remove(),this.maxH.rzAlwaysHide=!0,this.maxH[0].style.zIndex="-1000",this.hideEl(this.maxH)),this.range===!1&&this.options.showSelectionBar===!1&&(this.maxH.remove(),this.selBar.remove()),this.options.draggableRange&&(this.selBar.css("cursor","move"),this.selBar.addClass("rz-draggable"))},addAccessibility:function(){this.sliderElem.attr("role","slider")},calcViewDimensions:function(){var a=this.getWidth(this.minH);this.handleHalfWidth=a/2,this.barWidth=this.getWidth(this.fullBar),this.maxLeft=this.barWidth-a,this.getWidth(this.sliderElem),this.sliderElem.rzsl=this.sliderElem[0].getBoundingClientRect().left,this.initHasRun&&(this.updateFloorLab(),this.updateCeilLab(),this.initHandles())},updateTicksScale:function(){if(this.options.showTicks&&this.step){for(var a="",b=Math.round((this.maxValue-this.minValue)/this.step)+1,c=0;b>c;c++){var d=this.roundStep(this.minValue+c*this.step),e=this.isTickSelected(d)?"selected":"";a+='<li class="tick '+e+'">',this.options.showTicksValues&&(a+='<span class="tick-value">'+this.getDisplayValue(d)+"</span>"),a+="</li>"}this.ticks.html(a)}},isTickSelected:function(a){return!this.range&&this.options.showSelectionBar&&a<=this.scope.rzSliderModel?!0:this.range&&a>=this.scope.rzSliderModel&&a<=this.scope.rzSliderHigh?!0:!1},updateCeilLab:function(){this.translateFn(this.maxValue,this.ceilLab),this.setLeft(this.ceilLab,this.barWidth-this.ceilLab.rzsw),this.getWidth(this.ceilLab)},updateFloorLab:function(){this.translateFn(this.minValue,this.flrLab),this.getWidth(this.flrLab)},callOnStart:function(){if(this.options.onStart){var a=this;b(function(){a.options.onStart()})}},callOnChange:function(){if(this.options.onChange){var a=this;b(function(){a.options.onChange()})}},callOnEnd:function(){if(this.options.onEnd){var a=this;b(function(){a.options.onEnd()})}},updateHandles:function(a,b){return"rzSliderModel"===a?(this.updateLowHandle(b),this.updateSelectionBar(),this.updateTicksScale(),void(this.range&&this.updateCmbLabel())):"rzSliderHigh"===a?(this.updateHighHandle(b),this.updateSelectionBar(),this.updateTicksScale(),void(this.range&&this.updateCmbLabel())):(this.updateLowHandle(b),this.updateHighHandle(b),this.updateSelectionBar(),this.updateTicksScale(),void this.updateCmbLabel())},updateLowHandle:function(a){this.setLeft(this.minH,a),this.translateFn(this.scope.rzSliderModel,this.minLab),this.setLeft(this.minLab,a-this.minLab.rzsw/2+this.handleHalfWidth),this.shFloorCeil()},updateHighHandle:function(a){this.setLeft(this.maxH,a),this.translateFn(this.scope.rzSliderHigh,this.maxLab),this.setLeft(this.maxLab,a-this.maxLab.rzsw/2+this.handleHalfWidth),this.shFloorCeil()},shFloorCeil:function(){var a=!1,b=!1;this.minLab.rzsl<=this.flrLab.rzsl+this.flrLab.rzsw+5?(a=!0,this.hideEl(this.flrLab)):(a=!1,this.showEl(this.flrLab)),this.minLab.rzsl+this.minLab.rzsw>=this.ceilLab.rzsl-this.handleHalfWidth-10?(b=!0,this.hideEl(this.ceilLab)):(b=!1,this.showEl(this.ceilLab)),this.range&&(this.maxLab.rzsl+this.maxLab.rzsw>=this.ceilLab.rzsl-10?this.hideEl(this.ceilLab):b||this.showEl(this.ceilLab),this.maxLab.rzsl<=this.flrLab.rzsl+this.flrLab.rzsw+this.handleHalfWidth?this.hideEl(this.flrLab):a||this.showEl(this.flrLab))},updateSelectionBar:function(){this.setWidth(this.selBar,Math.abs(this.maxH.rzsl-this.minH.rzsl)+this.handleHalfWidth),this.setLeft(this.selBar,this.range?this.minH.rzsl+this.handleHalfWidth:0)},updateCmbLabel:function(){var a,b;this.minLab.rzsl+this.minLab.rzsw+10>=this.maxLab.rzsl?(a=this.getDisplayValue(this.scope.rzSliderModel),b=this.getDisplayValue(this.scope.rzSliderHigh),this.translateFn(a+" - "+b,this.cmbLab,!1),this.setLeft(this.cmbLab,this.selBar.rzsl+this.selBar.rzsw/2-this.cmbLab.rzsw/2),this.hideEl(this.minLab),this.hideEl(this.maxLab),this.showEl(this.cmbLab)):(this.showEl(this.maxLab),this.showEl(this.minLab),this.hideEl(this.cmbLab))},getDisplayValue:function(a){return this.customTrFn(a)},roundStep:function(a){var b=this.step,c=+((a-this.minValue)%b).toFixed(3),d=c>b/2?a+b-c:a-c;return d=d.toFixed(this.precision),+d},hideEl:function(a){return a.css({opacity:0})},showEl:function(a){return a.rzAlwaysHide?a:a.css({opacity:1})},setLeft:function(a,b){return a.rzsl=b,a.css({left:b+"px"}),b},getWidth:function(a){var b=a[0].getBoundingClientRect();return a.rzsw=b.right-b.left,a.rzsw},setWidth:function(a,b){return a.rzsw=b,a.css({width:b+"px"}),b},valueToOffset:function(a){return(a-this.minValue)*this.maxLeft/this.valueRange||0},offsetToValue:function(a){return a/this.maxLeft*this.valueRange+this.minValue},getEventX:function(a){return"clientX"in a?a.clientX:void 0===a.originalEvent?a.touches[0].clientX:a.originalEvent.touches[0].clientX},getNearestHandle:function(a){if(!this.range)return this.minH;var b=this.getEventX(a)-this.sliderElem.rzsl-this.handleHalfWidth;return Math.abs(b-this.minH.rzsl)<Math.abs(b-this.maxH.rzsl)?this.minH:this.maxH},bindEvents:function(){if(!this.options.readOnly&&!this.options.disabled){var b,c,d;this.options.draggableRange?(b="rzSliderDrag",c=this.onDragStart,d=this.onDragMove):(b="rzSliderModel",c=this.onStart,d=this.onMove),this.minH.on("mousedown",a.bind(this,this.onStart,this.minH,"rzSliderModel")),this.range&&this.maxH.on("mousedown",a.bind(this,this.onStart,this.maxH,"rzSliderHigh")),this.fullBar.on("mousedown",a.bind(this,this.onStart,null,null)),this.fullBar.on("mousedown",a.bind(this,this.onMove,this.fullBar)),this.selBar.on("mousedown",a.bind(this,c,null,b)),this.selBar.on("mousedown",a.bind(this,d,this.selBar)),this.ticks.on("mousedown",a.bind(this,this.onStart,null,null)),this.ticks.on("mousedown",a.bind(this,this.onMove,this.ticks)),this.minH.on("touchstart",a.bind(this,this.onStart,this.minH,"rzSliderModel")),this.range&&this.maxH.on("touchstart",a.bind(this,this.onStart,this.maxH,"rzSliderHigh")),this.fullBar.on("touchstart",a.bind(this,this.onStart,null,null)),this.fullBar.on("touchstart",a.bind(this,this.onMove,this.fullBar)),this.selBar.on("touchstart",a.bind(this,c,null,b)),this.selBar.on("touchstart",a.bind(this,d,this.selBar)),this.ticks.on("touchstart",a.bind(this,this.onStart,null,null)),this.ticks.on("touchstart",a.bind(this,this.onMove,this.ticks)),this.eventsBound=!0}},unbindEvents:function(){this.minH.off(),this.maxH.off(),this.fullBar.off(),this.selBar.off(),this.ticks.off(),this.eventsBound=!1},onStart:function(b,d,e){var f,g,h=this.getEventNames(e);e.stopPropagation(),e.preventDefault(),""===this.tracking&&(this.calcViewDimensions(),b?this.tracking=d:(b=this.getNearestHandle(e),this.tracking=b===this.minH?"rzSliderModel":"rzSliderHigh"),b.addClass("rz-active"),f=a.bind(this,this.dragging.active?this.onDragMove:this.onMove,b),g=a.bind(this,this.onEnd,f),c.on(h.moveEvent,f),c.one(h.endEvent,g),this.callOnStart())},onMove:function(a,b){var c,d,e,f=this.getEventX(b);if(c=this.sliderElem.rzsl,d=f-c-this.handleHalfWidth,0>=d){if(0===a.rzsl)return;e=this.minValue,d=0}else if(d>=this.maxLeft){if(a.rzsl===this.maxLeft)return;e=this.maxValue,d=this.maxLeft}else e=this.offsetToValue(d),e=this.roundStep(e),d=this.valueToOffset(e);this.positionTrackingHandle(e,d)},onDragStart:function(a,b,c){var d=this.getEventX(c)-this.sliderElem.rzsl-this.handleHalfWidth;this.dragging={active:!0,value:this.offsetToValue(d),difference:this.scope.rzSliderHigh-this.scope.rzSliderModel,offset:d,lowDist:d-this.minH.rzsl,highDist:this.maxH.rzsl-d},this.minH.addClass("rz-active"),this.maxH.addClass("rz-active"),this.onStart(a,b,c)},onDragMove:function(a,b){var c,d,e,f,g=this.getEventX(b)-this.sliderElem.rzsl-this.handleHalfWidth;if(g<=this.dragging.lowDist){if(a.rzsl===this.dragging.lowDist)return;e=this.minValue,c=0,f=this.dragging.difference,d=this.valueToOffset(f)}else if(g>=this.maxLeft-this.dragging.highDist){if(a.rzsl===this.dragging.highDist)return;f=this.maxValue,d=this.maxLeft,e=this.maxValue-this.dragging.difference,c=this.valueToOffset(e)}else e=this.offsetToValue(g-this.dragging.lowDist),e=this.roundStep(e),c=this.valueToOffset(e),f=e+this.dragging.difference,d=this.valueToOffset(f);this.positionTrackingBar(e,f,c,d)},positionTrackingBar:function(a,b,c,d){this.scope.rzSliderModel=a,this.scope.rzSliderHigh=b,this.updateHandles("rzSliderModel",c),this.updateHandles("rzSliderHigh",d),this.scope.$apply(),this.callOnChange()},positionTrackingHandle:function(a,b){this.range&&("rzSliderModel"===this.tracking&&a>=this.scope.rzSliderHigh?(this.scope[this.tracking]=this.scope.rzSliderHigh,this.updateHandles(this.tracking,this.maxH.rzsl),this.tracking="rzSliderHigh",this.minH.removeClass("rz-active"),this.maxH.addClass("rz-active"),this.scope.$apply(),this.callOnChange()):"rzSliderHigh"===this.tracking&&a<=this.scope.rzSliderModel&&(this.scope[this.tracking]=this.scope.rzSliderModel,this.updateHandles(this.tracking,this.minH.rzsl),this.tracking="rzSliderModel",this.maxH.removeClass("rz-active"),this.minH.addClass("rz-active"),this.scope.$apply(),this.callOnChange())),this.scope[this.tracking]!==a&&(this.scope[this.tracking]=a,this.updateHandles(this.tracking,b),this.scope.$apply(),this.callOnChange())},onEnd:function(a,b){var d=this.getEventNames(b).moveEvent;this.minH.removeClass("rz-active"),this.maxH.removeClass("rz-active"),c.off(d,a),this.scope.$emit("slideEnded"),this.tracking="",this.dragging.active=!1,this.callOnEnd()},getEventNames:function(a){var b={moveEvent:"",endEvent:""};return a.touches||void 0!==a.originalEvent&&a.originalEvent.touches?(b.moveEvent="touchmove",b.endEvent="touchend"):(b.moveEvent="mousemove",b.endEvent="mouseup"),b}},g}]).directive("rzslider",["RzSlider",function(a){return{restrict:"E",scope:{rzSliderModel:"=?",rzSliderHigh:"=?",rzSliderOptions:"=?",rzSliderTplUrl:"@"},templateUrl:function(a,b){return b.rzSliderTplUrl||"rzSliderTpl.html"},link:function(b,c){return new a(b,c)}}}]);return b.run(["$templateCache",function(a){a.put("rzSliderTpl.html",'<span class=rz-bar-wrapper><span class=rz-bar></span></span> <span class=rz-bar-wrapper><span class="rz-bar rz-selection"></span></span> <span class=rz-pointer></span> <span class=rz-pointer></span> <span class="rz-bubble rz-limit"></span> <span class="rz-bubble rz-limit"></span> <span class=rz-bubble></span> <span class=rz-bubble></span> <span class=rz-bubble></span><ul class=rz-ticks></ul>')}]),b});
\ No newline at end of file \ No newline at end of file
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
/*jslint unparam: true */ /*jslint unparam: true */
/*global angular: false, console: false, define, module */ /*global angular: false, console: false, define, module */
(function (root, factory) { (function(root, factory) {
'use strict'; 'use strict';
if (typeof define === 'function' && define.amd) { if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module. // AMD. Register as an anonymous module.
...@@ -27,1458 +27,1344 @@ ...@@ -27,1458 +27,1344 @@
factory(root.angular); factory(root.angular);
} }
}(this, function (angular) { }(this, function(angular) {
'use strict'; 'use strict';
var module = angular.module('rzModule', []) var module = angular.module('rzModule', [])
.value('throttle', .value('defaultOptions', {
/** floor: 0,
* throttle ceil: null, //defaults to rz-slider-model
* step: 1,
* Taken from underscore project precision: 0,
* translate: null,
* @param {Function} func draggableRange: false,
* @param {number} wait showSelectionBar: false,
* @param {ThrottleOptions} options hideLimitLabels: false,
* @returns {Function} readOnly: false,
*/ disabled: false,
function throttle(func, wait, options) { interval: 350,
'use strict'; showTicks: false,
var getTime = (Date.now || function() { showTicksValues: false,
return new Date().getTime(); onStart: null,
}); onChange: null,
var context, args, result; onEnd: null
var timeout = null; })
var previous = 0;
options = options || {}; .value('throttle',
var later = function() { /**
previous = options.leading === false ? 0 : getTime(); * throttle
timeout = null; *
result = func.apply(context, args); * Taken from underscore project
context = args = null; *
}; * @param {Function} func
return function() { * @param {number} wait
var now = getTime(); * @param {ThrottleOptions} options
if (!previous && options.leading === false) { previous = now; } * @returns {Function}
var remaining = wait - (now - previous); */
context = this; function throttle(func, wait, options) {
args = arguments; 'use strict';
if (remaining <= 0) { var getTime = (Date.now || function() {
clearTimeout(timeout); return new Date().getTime();
timeout = null;
previous = now;
result = func.apply(context, args);
context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
})
.factory('RzSlider', function($timeout, $document, $window, throttle)
{
'use strict';
/**
* Slider
*
* @param {ngScope} scope The AngularJS scope
* @param {Element} sliderElem The slider directive element wrapped in jqLite
* @param {*} attributes The slider directive attributes
* @constructor
*/
var Slider = function(scope, sliderElem, attributes)
{
/**
* The slider's scope
*
* @type {ngScope}
*/
this.scope = scope;
/**
* The slider attributes
*
* @type {Object}
*/
this.attributes = attributes;
/**
* Slider element wrapped in jqLite
*
* @type {jqLite}
*/
this.sliderElem = sliderElem;
/**
* Slider type
*
* @type {boolean} Set to true for range slider
*/
this.range = attributes.rzSliderHigh !== undefined && attributes.rzSliderModel !== undefined;
/**
* Whether to allow draggable range
*
* @type {boolean} Set to true for draggable range slider
*/
this.dragRange = this.range && attributes.rzSliderDraggableRange === 'true';
/**
* Values recorded when first dragging the bar
*
* @type {Object}
*/
this.dragging = {
active: false,
value: 0,
difference: 0,
offset: 0,
lowDist: 0,
highDist: 0
};
/**
* Half of the width of the slider handles
*
* @type {number}
*/
this.handleHalfWidth = 0;
/**
* Always show selection bar
*
* @type {boolean}
*/
this.alwaysShowBar = !!attributes.rzSliderAlwaysShowBar;
/**
* Maximum left the slider handle can have
*
* @type {number}
*/
this.maxLeft = 0;
/**
* Precision
*
* @type {number}
*/
this.precision = 0;
/**
* Step
*
* @type {number}
*/
this.step = 0;
/**
* The name of the handle we are currently tracking
*
* @type {string}
*/
this.tracking = '';
/**
* Minimum value (floor) of the model
*
* @type {number}
*/
this.minValue = 0;
/**
* Maximum value (ceiling) of the model
*
* @type {number}
*/
this.maxValue = 0;
/**
* Hide limit labels
*
* @type {boolean}
*/
this.hideLimitLabels = !!attributes.rzSliderHideLimitLabels;
/**
* Only present model values
*
* Do not allow to change values
*
* @type {boolean}
*/
this.presentOnly = attributes.rzSliderPresentOnly === 'true';
/**
* Display ticks on each possible value.
*
* @type {boolean}
*/
this.showTicks = attributes.rzSliderShowTicks || attributes.rzSliderShowTicksValue;
/**
* Display the value on each tick.
*
* @type {boolean}
*/
this.showTicksValue = attributes.rzSliderShowTicksValue;
/**
* Disable the slider
*
* @type {boolean}
*/
this.disabled = this.scope.rzSliderDisabled;
/**
* The interval at which the slider updates when the model/high values
* are altered from outside the slider
*
* @type {number}
*/
this.interval = this.scope.rzSliderInterval !== null ? this.scope.rzSliderInterval : 350;
/**
* The delta between min and max value
*
* @type {number}
*/
this.valueRange = 0;
/**
* Set to true if init method already executed
*
* @type {boolean}
*/
this.initHasRun = false;
/**
* Custom translate function
*
* @type {function}
*/
this.customTrFn = this.scope.rzSliderTranslate() || function(value) { return String(value); };
/**
* Array of de-registration functions to call on $destroy
*
* @type {Array.<Function>}
*/
this.deRegFuncs = [];
// Slider DOM elements wrapped in jqLite
this.fullBar = null; // The whole slider bar
this.selBar = null; // Highlight between two handles
this.minH = null; // Left slider handle
this.maxH = null; // Right slider handle
this.flrLab = null; // Floor label
this.ceilLab = null; // Ceiling label
this.minLab = null; // Label above the low value
this.maxLab = null; // Label above the high value
this.cmbLab = null; // Combined label
this.ticks = null; // The ticks
// Initialize slider
this.init();
};
// Add instance methods
Slider.prototype = {
/**
* Initialize slider
*
* @returns {undefined}
*/
init: function()
{
var thrLow, thrHigh, unRegFn,
calcDimFn = angular.bind(this, this.calcViewDimensions),
self = this;
this.initElemHandles();
this.addAccessibility();
this.setDisabledState();
this.calcViewDimensions();
this.setMinAndMax();
this.precision = this.scope.rzSliderPrecision === undefined ? 0 : +this.scope.rzSliderPrecision;
this.step = this.scope.rzSliderStep === undefined ? 1 : +this.scope.rzSliderStep;
$timeout(function()
{
self.updateCeilLab();
self.updateFloorLab();
self.initHandles();
self.bindEvents();
}); });
var context, args, result;
// Recalculate slider view dimensions var timeout = null;
unRegFn = this.scope.$on('reCalcViewDimensions', calcDimFn); var previous = 0;
this.deRegFuncs.push(unRegFn); options = options || {};
var later = function() {
// Recalculate stuff if view port dimensions have changed previous = options.leading === false ? 0 : getTime();
angular.element($window).on('resize', calcDimFn); timeout = null;
result = func.apply(context, args);
this.initHasRun = true; context = args = null;
};
// Watch for changes to the model return function() {
var now = getTime();
thrLow = throttle(function() if (!previous && options.leading === false) {
{ previous = now;
self.setMinAndMax();
self.updateLowHandle(self.valueToOffset(self.scope.rzSliderModel));
self.updateSelectionBar();
self.updateTicksScale();
if(self.range)
{
self.updateCmbLabel();
} }
var remaining = wait - (now - previous);
}, self.interval); context = this;
args = arguments;
thrHigh = throttle(function() if (remaining <= 0) {
{ clearTimeout(timeout);
self.setMinAndMax(); timeout = null;
self.updateHighHandle(self.valueToOffset(self.scope.rzSliderHigh)); previous = now;
self.updateSelectionBar(); result = func.apply(context, args);
self.updateTicksScale(); context = args = null;
self.updateCmbLabel(); } else if (!timeout && options.trailing !== false) {
}, self.interval); timeout = setTimeout(later, remaining);
this.scope.$on('rzSliderForceRender', function()
{
self.resetLabelsValue();
thrLow();
if(self.range) { thrHigh(); }
self.resetSlider();
});
// Watchers
unRegFn = this.scope.$watch('rzSliderModel', function(newValue, oldValue)
{
if(newValue === oldValue) { return; }
thrLow();
});
this.deRegFuncs.push(unRegFn);
unRegFn = this.scope.$watch('rzSliderHigh', function(newValue, oldValue)
{
if(newValue === oldValue) { return; }
thrHigh();
});
this.deRegFuncs.push(unRegFn);
this.scope.$watch('rzSliderFloor', function(newValue, oldValue)
{
if(newValue === oldValue) { return; }
self.resetSlider();
});
this.deRegFuncs.push(unRegFn);
unRegFn = this.scope.$watch('rzSliderCeil', function(newValue, oldValue)
{
if(newValue === oldValue) { return; }
self.resetSlider();
});
this.deRegFuncs.push(unRegFn);
unRegFn = this.scope.$watch('rzSliderShowTicks', function(newValue, oldValue)
{
if(newValue === oldValue) { return; }
self.resetSlider();
});
this.deRegFuncs.push(unRegFn);
unRegFn = this.scope.$watch('rzSliderShowTicksValue', function(newValue, oldValue)
{
if(newValue === oldValue) { return; }
self.resetSlider();
});
this.deRegFuncs.push(unRegFn);
unRegFn = this.scope.$watch('rzSliderDisabled', function(newValue, oldValue)
{
if(newValue === oldValue) { return; }
self.resetSlider();
if(self.disabled)
self.unbindEvents();
else
self.bindEvents();
});
this.deRegFuncs.push(unRegFn);
this.scope.$on('$destroy', function()
{
self.unbindEvents();
angular.element($window).off('resize', calcDimFn);
self.deRegFuncs.map(function(unbind) { unbind(); });
});
},
/**
* Resets slider
*
* @returns {undefined}
*/
resetSlider: function()
{
this.setMinAndMax();
this.updateCeilLab();
this.updateFloorLab();
this.setDisabledState();
this.calcViewDimensions();
},
/**
* Set the disabled state based on rzSliderDisabled
*
* @returns {undefined}
*/
setDisabledState: function()
{
this.disabled = this.scope.rzSliderDisabled;
if(this.disabled) {
this.sliderElem.attr('disabled', 'disabled');
}
else {
this.sliderElem.attr('disabled', null);
}
},
/**
* Reset label values
*
* @return {undefined}
*/
resetLabelsValue: function()
{
this.minLab.rzsv = undefined;
this.maxLab.rzsv = undefined;
},
/**
* Initialize slider handles positions and labels
*
* Run only once during initialization and every time view port changes size
*
* @returns {undefined}
*/
initHandles: function()
{
this.updateLowHandle(this.valueToOffset(this.scope.rzSliderModel));
/*
the order here is important since the selection bar should be
updated after the high handle but before the combined label
*/
if(this.range)
this.updateHighHandle(this.valueToOffset(this.scope.rzSliderHigh));
this.updateSelectionBar();
if(this.range)
this.updateCmbLabel();
this.updateTicksScale();
},
/**
* Translate value to human readable format
*
* @param {number|string} value
* @param {jqLite} label
* @param {boolean} [useCustomTr]
* @returns {undefined}
*/
translateFn: function(value, label, useCustomTr)
{
useCustomTr = useCustomTr === undefined ? true : useCustomTr;
var valStr = (useCustomTr ? this.customTrFn(value) : value).toString(),
getWidth = false;
if(label.rzsv === undefined || label.rzsv.length !== valStr.length || (label.rzsv.length > 0 && label.rzsw === 0))
{
getWidth = true;
label.rzsv = valStr;
}
label.text(valStr);
// Update width only when length of the label have changed
if(getWidth) { this.getWidth(label); }
},
/**
* Set maximum and minimum values for the slider
*
* @returns {undefined}
*/
setMinAndMax: function()
{
if(this.scope.rzSliderFloor)
{
this.minValue = +this.scope.rzSliderFloor;
}
else
{
this.minValue = this.scope.rzSliderFloor = 0;
}
if(this.scope.rzSliderCeil)
{
this.maxValue = +this.scope.rzSliderCeil;
}
else
{
this.maxValue = this.scope.rzSliderCeil = this.range ? this.scope.rzSliderHigh : this.scope.rzSliderModel;
}
if(this.scope.rzSliderStep)
{
this.step = +this.scope.rzSliderStep;
}
this.valueRange = this.maxValue - this.minValue;
},
/**
* Set the slider children to variables for easy access
*
* Run only once during initialization
*
* @returns {undefined}
*/
initElemHandles: function()
{
// Assign all slider elements to object properties for easy access
angular.forEach(this.sliderElem.children(), function(elem, index)
{
var jElem = angular.element(elem);
switch(index)
{
case 0: this.fullBar = jElem; break;
case 1: this.selBar = jElem; break;
case 2: this.minH = jElem; break;
case 3: this.maxH = jElem; break;
case 4: this.flrLab = jElem; break;
case 5: this.ceilLab = jElem; break;
case 6: this.minLab = jElem; break;
case 7: this.maxLab = jElem; break;
case 8: this.cmbLab = jElem; break;
case 9: this.ticks = jElem; break;
} }
return result;
};
})
}, this); .factory('RzSlider', function($timeout, $document, $window, defaultOptions, throttle) {
'use strict';
// Initialize offset cache properties
this.selBar.rzsl = 0;
this.minH.rzsl = 0;
this.maxH.rzsl = 0;
this.flrLab.rzsl = 0;
this.ceilLab.rzsl = 0;
this.minLab.rzsl = 0;
this.maxLab.rzsl = 0;
this.cmbLab.rzsl = 0;
// Hide limit labels
if(this.hideLimitLabels)
{
this.flrLab.rzAlwaysHide = true;
this.ceilLab.rzAlwaysHide = true;
this.hideEl(this.flrLab);
this.hideEl(this.ceilLab);
}
if(this.showTicksValue) {
this.flrLab.rzAlwaysHide = true;
this.ceilLab.rzAlwaysHide = true;
this.minLab.rzAlwaysHide = true;
this.maxLab.rzAlwaysHide = true;
this.cmbLab.rzAlwaysHide = true;
this.hideEl(this.flrLab);
this.hideEl(this.ceilLab);
this.hideEl(this.minLab);
this.hideEl(this.maxLab);
this.hideEl(this.cmbLab);
}
// Remove stuff not needed in single slider
if(this.range === false)
{
this.cmbLab.remove();
this.maxLab.remove();
// Hide max handle
this.maxH.rzAlwaysHide = true;
this.maxH[0].style.zIndex = '-1000';
this.hideEl(this.maxH);
}
// Show selection bar for single slider or not
if(this.range === false && this.alwaysShowBar === false)
{
this.maxH.remove();
this.selBar.remove();
}
// If using draggable range, use appropriate cursor for this.selBar.
if (this.dragRange)
{
this.selBar.css('cursor', 'move');
this.selBar.addClass('rz-draggable');
}
},
/**
* Adds accessibility atributes
*
* Run only once during initialization
*
* @returns {undefined}
*/
addAccessibility: function ()
{
this.sliderElem.attr("role", "slider");
},
/**
* Calculate dimensions that are dependent on view port size
*
* Run once during initialization and every time view port changes size.
*
* @returns {undefined}
*/
calcViewDimensions: function ()
{
var handleWidth = this.getWidth(this.minH);
this.handleHalfWidth = handleWidth / 2;
this.barWidth = this.getWidth(this.fullBar);
this.maxLeft = this.barWidth - handleWidth;
this.getWidth(this.sliderElem);
this.sliderElem.rzsl = this.sliderElem[0].getBoundingClientRect().left;
if(this.initHasRun)
{
this.updateFloorLab();
this.updateCeilLab();
this.initHandles();
}
},
/**
* Update the ticks position
*
* @returns {undefined}
*/
updateTicksScale: function() {
if(!this.showTicks) return;
if(!this.step) return; //if step is 0, the following loop will be endless.
var positions = '',
ticksCount = Math.round((this.maxValue - this.minValue) / this.step) + 1;
for (var i = 0; i < ticksCount; i++) {
var value = this.roundStep(this.minValue + i * this.step);
var selectedClass = this.isTickSelected(value) ? 'selected': '';
positions += '<li class="tick '+ selectedClass +'">';
if(this.showTicksValue)
positions += '<span class="tick-value">'+ this.getDisplayValue(value) +'</span>';
positions += '</li>';
}
this.ticks.html(positions);
},
isTickSelected: function(value) {
var tickLeft = this.valueToOffset(value);
if(!this.range && this.alwaysShowBar && value <= this.scope.rzSliderModel)
return true;
if(this.range && value >= this.scope.rzSliderModel && value <= this.scope.rzSliderHigh)
return true;
return false;
},
/**
* Update position of the ceiling label
*
* @returns {undefined}
*/
updateCeilLab: function()
{
this.translateFn(this.scope.rzSliderCeil, this.ceilLab);
this.setLeft(this.ceilLab, this.barWidth - this.ceilLab.rzsw);
this.getWidth(this.ceilLab);
},
/**
* Update position of the floor label
*
* @returns {undefined}
*/
updateFloorLab: function()
{
this.translateFn(this.scope.rzSliderFloor, this.flrLab);
this.getWidth(this.flrLab);
},
/**
* Call the onStart callback if defined
*
* @returns {undefined}
*/
callOnStart: function() {
if(this.scope.rzSliderOnStart) {
var self = this;
$timeout(function() {
self.scope.rzSliderOnStart();
});
}
},
/**
* Call the onChange callback if defined
*
* @returns {undefined}
*/
callOnChange: function() {
if(this.scope.rzSliderOnChange) {
var self = this;
$timeout(function() {
self.scope.rzSliderOnChange();
});
}
},
/**
* Call the onEnd callback if defined
*
* @returns {undefined}
*/
callOnEnd: function() {
if(this.scope.rzSliderOnEnd) {
var self = this;
$timeout(function() {
self.scope.rzSliderOnEnd();
});
}
},
/**
* Update slider handles and label positions
*
* @param {string} which
* @param {number} newOffset
*/
updateHandles: function(which, newOffset)
{
if(which === 'rzSliderModel')
{
this.updateLowHandle(newOffset);
this.updateSelectionBar();
this.updateTicksScale();
if(this.range)
{
this.updateCmbLabel();
}
return;
}
if(which === 'rzSliderHigh') /**
{ * Slider
this.updateHighHandle(newOffset); *
this.updateSelectionBar(); * @param {ngScope} scope The AngularJS scope
this.updateTicksScale(); * @param {Element} sliderElem The slider directive element wrapped in jqLite
* @constructor
*/
var Slider = function(scope, sliderElem) {
/**
* The slider's scope
*
* @type {ngScope}
*/
this.scope = scope;
/**
* Slider element wrapped in jqLite
*
* @type {jqLite}
*/
this.sliderElem = sliderElem;
/**
* Slider type
*
* @type {boolean} Set to true for range slider
*/
this.range = this.scope.rzSliderModel !== undefined && this.scope.rzSliderHigh !== undefined;
/**
* Values recorded when first dragging the bar
*
* @type {Object}
*/
this.dragging = {
active: false,
value: 0,
difference: 0,
offset: 0,
lowDist: 0,
highDist: 0
};
/**
* Half of the width of the slider handles
*
* @type {number}
*/
this.handleHalfWidth = 0;
/**
* Maximum left the slider handle can have
*
* @type {number}
*/
this.maxLeft = 0;
/**
* Precision
*
* @type {number}
*/
this.precision = 0;
/**
* Step
*
* @type {number}
*/
this.step = 0;
/**
* The name of the handle we are currently tracking
*
* @type {string}
*/
this.tracking = '';
/**
* Minimum value (floor) of the model
*
* @type {number}
*/
this.minValue = 0;
/**
* Maximum value (ceiling) of the model
*
* @type {number}
*/
this.maxValue = 0;
/**
* The delta between min and max value
*
* @type {number}
*/
this.valueRange = 0;
/**
* Set to true if init method already executed
*
* @type {boolean}
*/
this.initHasRun = false;
/** If the slider events are already bound to the slider
*
* @type {boolean}
*/
this.eventsBound = false;
// Slider DOM elements wrapped in jqLite
this.fullBar = null; // The whole slider bar
this.selBar = null; // Highlight between two handles
this.minH = null; // Left slider handle
this.maxH = null; // Right slider handle
this.flrLab = null; // Floor label
this.ceilLab = null; // Ceiling label
this.minLab = null; // Label above the low value
this.maxLab = null; // Label above the high value
this.cmbLab = null; // Combined label
this.ticks = null; // The ticks
// Initialize slider
this.init();
};
if(this.range) // Add instance methods
{ Slider.prototype = {
/**
* Initialize slider
*
* @returns {undefined}
*/
init: function() {
var thrLow, thrHigh,
calcDimFn = angular.bind(this, this.calcViewDimensions),
self = this;
this.applyOptions();
this.initElemHandles();
this.addAccessibility();
this.manageEventsBindings();
this.setDisabledState();
this.calcViewDimensions();
this.setMinAndMax();
$timeout(function() {
self.updateCeilLab();
self.updateFloorLab();
self.initHandles();
self.bindEvents();
});
// Recalculate slider view dimensions
this.scope.$on('reCalcViewDimensions', calcDimFn);
// Recalculate stuff if view port dimensions have changed
angular.element($window).on('resize', calcDimFn);
this.initHasRun = true;
// Watch for changes to the model
thrLow = throttle(function() {
self.setMinAndMax();
self.updateLowHandle(self.valueToOffset(self.scope.rzSliderModel));
self.updateSelectionBar();
self.updateTicksScale();
if (self.range) {
self.updateCmbLabel();
}
}, self.interval);
thrHigh = throttle(function() {
self.setMinAndMax();
self.updateHighHandle(self.valueToOffset(self.scope.rzSliderHigh));
self.updateSelectionBar();
self.updateTicksScale();
self.updateCmbLabel();
}, self.interval);
this.scope.$on('rzSliderForceRender', function() {
self.resetLabelsValue();
thrLow();
if (self.range) {
thrHigh();
}
self.resetSlider();
});
// Watchers
this.scope.$watch('rzSliderModel', function(newValue, oldValue) {
if (newValue === oldValue)
return;
thrLow();
});
this.scope.$watch('rzSliderHigh', function(newValue, oldValue) {
if (newValue === oldValue)
return;
thrHigh();
});
this.scope.$watch('rzSliderOptions', function(newValue, oldValue) {
if (newValue === oldValue)
return;
self.applyOptions();
self.resetSlider();
}, true);
this.scope.$on('$destroy', function() {
self.unbindEvents();
angular.element($window).off('resize', calcDimFn);
});
},
/**
* Read the user options and apply them to the slider model
*/
applyOptions: function() {
var userOpts = this.scope.rzSliderOptions;
this.options = {};
for (var option_name in defaultOptions) {
if (!userOpts || userOpts[option_name] === undefined)
this.options[option_name] = defaultOptions[option_name];
else
this.options[option_name] = userOpts[option_name];
}
this.options.draggableRange = this.range && this.options.draggableRange;
this.options.showTicks = this.options.showTicks || this.options.showTicksValues;
if (this.options.translate)
this.customTrFn = this.options.translate;
else
this.customTrFn = function(value) {
return String(value);
};
},
/**
* Resets slider
*
* @returns {undefined}
*/
resetSlider: function() {
this.setMinAndMax();
this.updateCeilLab();
this.updateFloorLab();
this.manageEventsBindings();
this.setDisabledState();
this.calcViewDimensions();
},
/**
* Manage the events bindings based on readOnly and disabled options
*
* @returns {undefined}
*/
manageEventsBindings: function() {
if ((this.options.disabled || this.options.readOnly) && this.eventsBound)
this.unbindEvents();
else if ((!this.options.disabled || !this.options.readOnly) && !this.eventsBound)
this.bindEvents();
},
/**
* Set the disabled state based on rzSliderDisabled
*
* @returns {undefined}
*/
setDisabledState: function() {
if (this.options.disabled) {
this.sliderElem.attr('disabled', 'disabled');
}
else {
this.sliderElem.attr('disabled', null);
}
if (this.options.disabled && this.eventsBound)
this.unbindEvents();
else if (!this.options.disabled && !this.eventsBound)
this.bindEvents();
},
/**
* Reset label values
*
* @return {undefined}
*/
resetLabelsValue: function() {
this.minLab.rzsv = undefined;
this.maxLab.rzsv = undefined;
},
/**
* Initialize slider handles positions and labels
*
* Run only once during initialization and every time view port changes size
*
* @returns {undefined}
*/
initHandles: function() {
this.updateLowHandle(this.valueToOffset(this.scope.rzSliderModel));
/*
the order here is important since the selection bar should be
updated after the high handle but before the combined label
*/
if (this.range)
this.updateHighHandle(this.valueToOffset(this.scope.rzSliderHigh));
this.updateSelectionBar();
if (this.range)
this.updateCmbLabel();
this.updateTicksScale();
},
/**
* Translate value to human readable format
*
* @param {number|string} value
* @param {jqLite} label
* @param {boolean} [useCustomTr]
* @returns {undefined}
*/
translateFn: function(value, label, useCustomTr) {
useCustomTr = useCustomTr === undefined ? true : useCustomTr;
var valStr = String((useCustomTr ? this.customTrFn(value) : value)),
getWidth = false;
if (label.rzsv === undefined || label.rzsv.length !== valStr.length || (label.rzsv.length > 0 && label.rzsw === 0)) {
getWidth = true;
label.rzsv = valStr;
}
label.text(valStr);
// Update width only when length of the label have changed
if (getWidth) {
this.getWidth(label);
}
},
/**
* Set maximum and minimum values for the slider and ensure the model and high
* value match these limits
* @returns {undefined}
*/
setMinAndMax: function() {
this.step = +this.options.step;
this.precision = +this.options.precision;
this.scope.rzSliderModel = this.roundStep(this.scope.rzSliderModel);
if (this.range)
this.scope.rzSliderHigh = this.roundStep(this.scope.rzSliderHigh);
this.minValue = +this.options.floor;
if (this.scope.rzSliderModel < this.minValue)
this.scope.rzSliderModel = this.minValue;
if (this.range && this.scope.rzSliderHigh < this.minValue)
this.scope.rzSliderHigh = this.minValue;
if (this.options.ceil) {
this.maxValue = +this.options.ceil;
if (this.scope.rzSliderModel > this.maxValue)
this.scope.rzSliderModel = this.maxValue;
if (this.range && this.scope.rzSliderHigh > this.maxValue)
this.scope.rzSliderHigh = this.maxValue;
}
else
this.maxValue = this.options.ceil = this.range ? this.scope.rzSliderHigh : this.scope.rzSliderModel;
this.valueRange = this.maxValue - this.minValue;
},
/**
* Set the slider children to variables for easy access
*
* Run only once during initialization
*
* @returns {undefined}
*/
initElemHandles: function() {
// Assign all slider elements to object properties for easy access
angular.forEach(this.sliderElem.children(), function(elem, index) {
var jElem = angular.element(elem);
switch (index) {
case 0:
this.fullBar = jElem;
break;
case 1:
this.selBar = jElem;
break;
case 2:
this.minH = jElem;
break;
case 3:
this.maxH = jElem;
break;
case 4:
this.flrLab = jElem;
break;
case 5:
this.ceilLab = jElem;
break;
case 6:
this.minLab = jElem;
break;
case 7:
this.maxLab = jElem;
break;
case 8:
this.cmbLab = jElem;
break;
case 9:
this.ticks = jElem;
break;
}
}, this);
// Initialize offset cache properties
this.selBar.rzsl = 0;
this.minH.rzsl = 0;
this.maxH.rzsl = 0;
this.flrLab.rzsl = 0;
this.ceilLab.rzsl = 0;
this.minLab.rzsl = 0;
this.maxLab.rzsl = 0;
this.cmbLab.rzsl = 0;
// Hide limit labels
if (this.options.hideLimitLabels) {
this.flrLab.rzAlwaysHide = true;
this.ceilLab.rzAlwaysHide = true;
this.hideEl(this.flrLab);
this.hideEl(this.ceilLab);
}
if (this.options.showTicksValues) {
this.flrLab.rzAlwaysHide = true;
this.ceilLab.rzAlwaysHide = true;
this.minLab.rzAlwaysHide = true;
this.maxLab.rzAlwaysHide = true;
this.cmbLab.rzAlwaysHide = true;
this.hideEl(this.flrLab);
this.hideEl(this.ceilLab);
this.hideEl(this.minLab);
this.hideEl(this.maxLab);
this.hideEl(this.cmbLab);
}
// Remove stuff not needed in single slider
if (this.range === false) {
this.cmbLab.remove();
this.maxLab.remove();
// Hide max handle
this.maxH.rzAlwaysHide = true;
this.maxH[0].style.zIndex = '-1000';
this.hideEl(this.maxH);
}
// Show selection bar for single slider or not
if (this.range === false && this.options.showSelectionBar === false) {
this.maxH.remove();
this.selBar.remove();
}
// If using draggable range, use appropriate cursor for this.selBar.
if (this.options.draggableRange) {
this.selBar.css('cursor', 'move');
this.selBar.addClass('rz-draggable');
}
},
/**
* Adds accessibility atributes
*
* Run only once during initialization
*
* @returns {undefined}
*/
addAccessibility: function() {
this.sliderElem.attr("role", "slider");
},
/**
* Calculate dimensions that are dependent on view port size
*
* Run once during initialization and every time view port changes size.
*
* @returns {undefined}
*/
calcViewDimensions: function() {
var handleWidth = this.getWidth(this.minH);
this.handleHalfWidth = handleWidth / 2;
this.barWidth = this.getWidth(this.fullBar);
this.maxLeft = this.barWidth - handleWidth;
this.getWidth(this.sliderElem);
this.sliderElem.rzsl = this.sliderElem[0].getBoundingClientRect().left;
if (this.initHasRun) {
this.updateFloorLab();
this.updateCeilLab();
this.initHandles();
}
},
/**
* Update the ticks position
*
* @returns {undefined}
*/
updateTicksScale: function() {
if (!this.options.showTicks) return;
if (!this.step) return; //if step is 0, the following loop will be endless.
var positions = '',
ticksCount = Math.round((this.maxValue - this.minValue) / this.step) + 1;
for (var i = 0; i < ticksCount; i++) {
var value = this.roundStep(this.minValue + i * this.step);
var selectedClass = this.isTickSelected(value) ? 'selected' : '';
positions += '<li class="tick ' + selectedClass + '">';
if (this.options.showTicksValues)
positions += '<span class="tick-value">' + this.getDisplayValue(value) + '</span>';
positions += '</li>';
}
this.ticks.html(positions);
},
isTickSelected: function(value) {
if (!this.range && this.options.showSelectionBar && value <= this.scope.rzSliderModel)
return true;
if (this.range && value >= this.scope.rzSliderModel && value <= this.scope.rzSliderHigh)
return true;
return false;
},
/**
* Update position of the ceiling label
*
* @returns {undefined}
*/
updateCeilLab: function() {
this.translateFn(this.maxValue, this.ceilLab);
this.setLeft(this.ceilLab, this.barWidth - this.ceilLab.rzsw);
this.getWidth(this.ceilLab);
},
/**
* Update position of the floor label
*
* @returns {undefined}
*/
updateFloorLab: function() {
this.translateFn(this.minValue, this.flrLab);
this.getWidth(this.flrLab);
},
/**
* Call the onStart callback if defined
*
* @returns {undefined}
*/
callOnStart: function() {
if (this.options.onStart) {
var self = this;
$timeout(function() {
self.options.onStart();
});
}
},
/**
* Call the onChange callback if defined
*
* @returns {undefined}
*/
callOnChange: function() {
if (this.options.onChange) {
var self = this;
$timeout(function() {
self.options.onChange();
});
}
},
/**
* Call the onEnd callback if defined
*
* @returns {undefined}
*/
callOnEnd: function() {
if (this.options.onEnd) {
var self = this;
$timeout(function() {
self.options.onEnd();
});
}
},
/**
* Update slider handles and label positions
*
* @param {string} which
* @param {number} newOffset
*/
updateHandles: function(which, newOffset) {
if (which === 'rzSliderModel') {
this.updateLowHandle(newOffset);
this.updateSelectionBar();
this.updateTicksScale();
if (this.range) {
this.updateCmbLabel();
}
return;
}
if (which === 'rzSliderHigh') {
this.updateHighHandle(newOffset);
this.updateSelectionBar();
this.updateTicksScale();
if (this.range) {
this.updateCmbLabel();
}
return;
}
// Update both
this.updateLowHandle(newOffset);
this.updateHighHandle(newOffset);
this.updateSelectionBar();
this.updateTicksScale();
this.updateCmbLabel(); this.updateCmbLabel();
} },
return;
} /**
* Update low slider handle position and label
// Update both *
this.updateLowHandle(newOffset); * @param {number} newOffset
this.updateHighHandle(newOffset); * @returns {undefined}
this.updateSelectionBar(); */
this.updateTicksScale(); updateLowHandle: function(newOffset) {
this.updateCmbLabel(); this.setLeft(this.minH, newOffset);
}, this.translateFn(this.scope.rzSliderModel, this.minLab);
this.setLeft(this.minLab, newOffset - this.minLab.rzsw / 2 + this.handleHalfWidth);
/**
* Update low slider handle position and label this.shFloorCeil();
* },
* @param {number} newOffset
* @returns {undefined} /**
*/ * Update high slider handle position and label
updateLowHandle: function(newOffset) *
{ * @param {number} newOffset
this.setLeft(this.minH, newOffset); * @returns {undefined}
this.translateFn(this.scope.rzSliderModel, this.minLab); */
this.setLeft(this.minLab, newOffset - this.minLab.rzsw / 2 + this.handleHalfWidth); updateHighHandle: function(newOffset) {
this.setLeft(this.maxH, newOffset);
this.shFloorCeil(); this.translateFn(this.scope.rzSliderHigh, this.maxLab);
}, this.setLeft(this.maxLab, newOffset - this.maxLab.rzsw / 2 + this.handleHalfWidth);
/** this.shFloorCeil();
* Update high slider handle position and label },
*
* @param {number} newOffset /**
* @returns {undefined} * Show / hide floor / ceiling label
*/ *
updateHighHandle: function(newOffset) * @returns {undefined}
{ */
this.setLeft(this.maxH, newOffset); shFloorCeil: function() {
this.translateFn(this.scope.rzSliderHigh, this.maxLab); var flHidden = false, clHidden = false;
this.setLeft(this.maxLab, newOffset - this.maxLab.rzsw / 2 + this.handleHalfWidth);
if (this.minLab.rzsl <= this.flrLab.rzsl + this.flrLab.rzsw + 5) {
flHidden = true;
this.hideEl(this.flrLab);
}
else {
flHidden = false;
this.showEl(this.flrLab);
}
if (this.minLab.rzsl + this.minLab.rzsw >= this.ceilLab.rzsl - this.handleHalfWidth - 10) {
clHidden = true;
this.hideEl(this.ceilLab);
}
else {
clHidden = false;
this.showEl(this.ceilLab);
}
if (this.range) {
if (this.maxLab.rzsl + this.maxLab.rzsw >= this.ceilLab.rzsl - 10) {
this.hideEl(this.ceilLab);
}
else if (!clHidden) {
this.showEl(this.ceilLab);
}
// Hide or show floor label
if (this.maxLab.rzsl <= this.flrLab.rzsl + this.flrLab.rzsw + this.handleHalfWidth) {
this.hideEl(this.flrLab);
}
else if (!flHidden) {
this.showEl(this.flrLab);
}
}
},
/**
* Update slider selection bar, combined label and range label
*
* @returns {undefined}
*/
updateSelectionBar: function() {
this.setWidth(this.selBar, Math.abs(this.maxH.rzsl - this.minH.rzsl) + this.handleHalfWidth);
this.setLeft(this.selBar, this.range ? this.minH.rzsl + this.handleHalfWidth : 0);
},
/**
* Update combined label position and value
*
* @returns {undefined}
*/
updateCmbLabel: function() {
var lowTr, highTr;
if (this.minLab.rzsl + this.minLab.rzsw + 10 >= this.maxLab.rzsl) {
lowTr = this.getDisplayValue(this.scope.rzSliderModel);
highTr = this.getDisplayValue(this.scope.rzSliderHigh);
this.translateFn(lowTr + ' - ' + highTr, this.cmbLab, false);
this.setLeft(this.cmbLab, this.selBar.rzsl + this.selBar.rzsw / 2 - this.cmbLab.rzsw / 2);
this.hideEl(this.minLab);
this.hideEl(this.maxLab);
this.showEl(this.cmbLab);
}
else {
this.showEl(this.maxLab);
this.showEl(this.minLab);
this.hideEl(this.cmbLab);
}
},
/**
* Return the translated value if a translate function is provided else the original value
* @param value
* @returns {*}
*/
getDisplayValue: function(value) {
return this.customTrFn(value);
},
/**
* Round value to step and precision
*
* @param {number} value
* @returns {number}
*/
roundStep: function(value) {
var step = this.step,
remainder = +((value - this.minValue) % step).toFixed(3),
steppedValue = remainder > (step / 2) ? value + step - remainder : value - remainder;
steppedValue = steppedValue.toFixed(this.precision);
return +steppedValue;
},
/**
* Hide element
*
* @param element
* @returns {jqLite} The jqLite wrapped DOM element
*/
hideEl: function(element) {
return element.css({opacity: 0});
},
/**
* Show element
*
* @param element The jqLite wrapped DOM element
* @returns {jqLite} The jqLite
*/
showEl: function(element) {
if (!!element.rzAlwaysHide) {
return element;
}
return element.css({opacity: 1});
},
/**
* Set element left offset
*
* @param {jqLite} elem The jqLite wrapped DOM element
* @param {number} left
* @returns {number}
*/
setLeft: function(elem, left) {
elem.rzsl = left;
elem.css({left: left + 'px'});
return left;
},
/**
* Get element width
*
* @param {jqLite} elem The jqLite wrapped DOM element
* @returns {number}
*/
getWidth: function(elem) {
var val = elem[0].getBoundingClientRect();
elem.rzsw = val.right - val.left;
return elem.rzsw;
},
/**
* Set element width
*
* @param {jqLite} elem The jqLite wrapped DOM element
* @param {number} width
* @returns {number}
*/
setWidth: function(elem, width) {
elem.rzsw = width;
elem.css({width: width + 'px'});
return width;
},
/**
* Translate value to pixel offset
*
* @param {number} val
* @returns {number}
*/
valueToOffset: function(val) {
return (val - this.minValue) * this.maxLeft / this.valueRange || 0;
},
/**
* Translate offset to model value
*
* @param {number} offset
* @returns {number}
*/
offsetToValue: function(offset) {
return (offset / this.maxLeft) * this.valueRange + this.minValue;
},
// Events
/**
* Get the X-coordinate of an event
*
* @param {Object} event The event
* @returns {number}
*/
getEventX: function(event) {
/* http://stackoverflow.com/a/12336075/282882 */
//noinspection JSLint
if ('clientX' in event) {
return event.clientX;
}
return event.originalEvent === undefined ?
event.touches[0].clientX
: event.originalEvent.touches[0].clientX;
},
/**
* Get the handle closest to an event.
*
* @param event {Event} The event
* @returns {jqLite} The handle closest to the event.
*/
getNearestHandle: function(event) {
if (!this.range) {
return this.minH;
}
var offset = this.getEventX(event) - this.sliderElem.rzsl - this.handleHalfWidth;
return Math.abs(offset - this.minH.rzsl) < Math.abs(offset - this.maxH.rzsl) ? this.minH : this.maxH;
},
/**
* Bind mouse and touch events to slider handles
*
* @returns {undefined}
*/
bindEvents: function() {
if (this.options.readOnly || this.options.disabled) return;
var barTracking, barStart, barMove;
if (this.options.draggableRange) {
barTracking = 'rzSliderDrag';
barStart = this.onDragStart;
barMove = this.onDragMove;
}
else {
barTracking = 'rzSliderModel';
barStart = this.onStart;
barMove = this.onMove;
}
this.minH.on('mousedown', angular.bind(this, this.onStart, this.minH, 'rzSliderModel'));
if (this.range) {
this.maxH.on('mousedown', angular.bind(this, this.onStart, this.maxH, 'rzSliderHigh'));
}
this.fullBar.on('mousedown', angular.bind(this, this.onStart, null, null));
this.fullBar.on('mousedown', angular.bind(this, this.onMove, this.fullBar));
this.selBar.on('mousedown', angular.bind(this, barStart, null, barTracking));
this.selBar.on('mousedown', angular.bind(this, barMove, this.selBar));
this.ticks.on('mousedown', angular.bind(this, this.onStart, null, null));
this.ticks.on('mousedown', angular.bind(this, this.onMove, this.ticks));
this.minH.on('touchstart', angular.bind(this, this.onStart, this.minH, 'rzSliderModel'));
if (this.range) {
this.maxH.on('touchstart', angular.bind(this, this.onStart, this.maxH, 'rzSliderHigh'));
}
this.fullBar.on('touchstart', angular.bind(this, this.onStart, null, null));
this.fullBar.on('touchstart', angular.bind(this, this.onMove, this.fullBar));
this.selBar.on('touchstart', angular.bind(this, barStart, null, barTracking));
this.selBar.on('touchstart', angular.bind(this, barMove, this.selBar));
this.ticks.on('touchstart', angular.bind(this, this.onStart, null, null));
this.ticks.on('touchstart', angular.bind(this, this.onMove, this.ticks));
this.eventsBound = true;
},
/**
* Unbind mouse and touch events to slider handles
*
* @returns {undefined}
*/
unbindEvents: function() {
this.minH.off();
this.maxH.off();
this.fullBar.off();
this.selBar.off();
this.ticks.off();
this.eventsBound = false;
},
/**
* onStart event handler
*
* @param {?Object} pointer The jqLite wrapped DOM element; if null, the closest handle is used
* @param {?string} ref The name of the handle being changed; if null, the closest handle's value is modified
* @param {Event} event The event
* @returns {undefined}
*/
onStart: function(pointer, ref, event) {
var ehMove, ehEnd,
eventNames = this.getEventNames(event);
event.stopPropagation();
event.preventDefault();
if (this.tracking !== '') {
return;
}
// We have to do this in case the HTML where the sliders are on
// have been animated into view.
this.calcViewDimensions();
if (pointer) {
this.tracking = ref;
}
else {
pointer = this.getNearestHandle(event);
this.tracking = pointer === this.minH ? 'rzSliderModel' : 'rzSliderHigh';
}
pointer.addClass('rz-active');
ehMove = angular.bind(this, this.dragging.active ? this.onDragMove : this.onMove, pointer);
ehEnd = angular.bind(this, this.onEnd, ehMove);
$document.on(eventNames.moveEvent, ehMove);
$document.one(eventNames.endEvent, ehEnd);
this.callOnStart();
},
/**
* onMove event handler
*
* @param {jqLite} pointer
* @param {Event} event The event
* @returns {undefined}
*/
onMove: function(pointer, event) {
var eventX = this.getEventX(event),
sliderLO, newOffset, newValue;
sliderLO = this.sliderElem.rzsl;
newOffset = eventX - sliderLO - this.handleHalfWidth;
if (newOffset <= 0) {
if (pointer.rzsl === 0)
return;
newValue = this.minValue;
newOffset = 0;
}
else if (newOffset >= this.maxLeft) {
if (pointer.rzsl === this.maxLeft)
return;
newValue = this.maxValue;
newOffset = this.maxLeft;
}
else {
newValue = this.offsetToValue(newOffset);
newValue = this.roundStep(newValue);
newOffset = this.valueToOffset(newValue);
}
this.positionTrackingHandle(newValue, newOffset);
},
/**
* onDragStart event handler
*
* Handles dragging of the middle bar.
*
* @param {Object} pointer The jqLite wrapped DOM element
* @param {string} ref One of the refLow, refHigh values
* @param {Event} event The event
* @returns {undefined}
*/
onDragStart: function(pointer, ref, event) {
var offset = this.getEventX(event) - this.sliderElem.rzsl - this.handleHalfWidth;
this.dragging = {
active: true,
value: this.offsetToValue(offset),
difference: this.scope.rzSliderHigh - this.scope.rzSliderModel,
offset: offset,
lowDist: offset - this.minH.rzsl,
highDist: this.maxH.rzsl - offset
};
this.minH.addClass('rz-active');
this.maxH.addClass('rz-active');
this.shFloorCeil(); this.onStart(pointer, ref, event);
}, },
/**
* onDragMove event handler
*
* Handles dragging of the middle bar.
*
* @param {jqLite} pointer
* @param {Event} event The event
* @returns {undefined}
*/
onDragMove: function(pointer, event) {
var newOffset = this.getEventX(event) - this.sliderElem.rzsl - this.handleHalfWidth,
newMinOffset, newMaxOffset,
newMinValue, newMaxValue;
if (newOffset <= this.dragging.lowDist) {
if (pointer.rzsl === this.dragging.lowDist) {
return;
}
newMinValue = this.minValue;
newMinOffset = 0;
newMaxValue = this.dragging.difference;
newMaxOffset = this.valueToOffset(newMaxValue);
}
else if (newOffset >= this.maxLeft - this.dragging.highDist) {
if (pointer.rzsl === this.dragging.highDist) {
return;
}
newMaxValue = this.maxValue;
newMaxOffset = this.maxLeft;
newMinValue = this.maxValue - this.dragging.difference;
newMinOffset = this.valueToOffset(newMinValue);
}
else {
newMinValue = this.offsetToValue(newOffset - this.dragging.lowDist);
newMinValue = this.roundStep(newMinValue);
newMinOffset = this.valueToOffset(newMinValue);
newMaxValue = newMinValue + this.dragging.difference;
newMaxOffset = this.valueToOffset(newMaxValue);
}
this.positionTrackingBar(newMinValue, newMaxValue, newMinOffset, newMaxOffset);
},
/**
* Set the new value and offset for the entire bar
*
* @param {number} newMinValue the new minimum value
* @param {number} newMaxValue the new maximum value
* @param {number} newMinOffset the new minimum offset
* @param {number} newMaxOffset the new maximum offset
*/
positionTrackingBar: function(newMinValue, newMaxValue, newMinOffset, newMaxOffset) {
this.scope.rzSliderModel = newMinValue;
this.scope.rzSliderHigh = newMaxValue;
this.updateHandles('rzSliderModel', newMinOffset);
this.updateHandles('rzSliderHigh', newMaxOffset);
this.scope.$apply();
this.callOnChange();
},
/**
* Set the new value and offset to the current tracking handle
*
* @param {number} newValue new model value
* @param {number} newOffset new offset value
*/
positionTrackingHandle: function(newValue, newOffset) {
if (this.range) {
/* This is to check if we need to switch the min and max handles*/
if (this.tracking === 'rzSliderModel' && newValue >= this.scope.rzSliderHigh) {
this.scope[this.tracking] = this.scope.rzSliderHigh;
this.updateHandles(this.tracking, this.maxH.rzsl);
this.tracking = 'rzSliderHigh';
this.minH.removeClass('rz-active');
this.maxH.addClass('rz-active');
/* We need to apply here because we are not sure that we will enter the next block */
this.scope.$apply();
this.callOnChange();
}
else if (this.tracking === 'rzSliderHigh' && newValue <= this.scope.rzSliderModel) {
this.scope[this.tracking] = this.scope.rzSliderModel;
this.updateHandles(this.tracking, this.minH.rzsl);
this.tracking = 'rzSliderModel';
this.maxH.removeClass('rz-active');
this.minH.addClass('rz-active');
/* We need to apply here because we are not sure that we will enter the next block */
this.scope.$apply();
this.callOnChange();
}
}
if (this.scope[this.tracking] !== newValue) {
this.scope[this.tracking] = newValue;
this.updateHandles(this.tracking, newOffset);
this.scope.$apply();
this.callOnChange();
}
},
/**
* onEnd event handler
*
* @param {Event} event The event
* @param {Function} ehMove The the bound move event handler
* @returns {undefined}
*/
onEnd: function(ehMove, event) {
var moveEventName = this.getEventNames(event).moveEvent;
/** this.minH.removeClass('rz-active');
* Show / hide floor / ceiling label this.maxH.removeClass('rz-active');
*
* @returns {undefined}
*/
shFloorCeil: function()
{
var flHidden = false, clHidden = false;
if(this.minLab.rzsl <= this.flrLab.rzsl + this.flrLab.rzsw + 5)
{
flHidden = true;
this.hideEl(this.flrLab);
}
else
{
flHidden = false;
this.showEl(this.flrLab);
}
if(this.minLab.rzsl + this.minLab.rzsw >= this.ceilLab.rzsl - this.handleHalfWidth - 10)
{
clHidden = true;
this.hideEl(this.ceilLab);
}
else
{
clHidden = false;
this.showEl(this.ceilLab);
}
if(this.range)
{
if(this.maxLab.rzsl + this.maxLab.rzsw >= this.ceilLab.rzsl - 10)
{
this.hideEl(this.ceilLab);
}
else if( ! clHidden)
{
this.showEl(this.ceilLab);
}
// Hide or show floor label $document.off(moveEventName, ehMove);
if(this.maxLab.rzsl <= this.flrLab.rzsl + this.flrLab.rzsw + this.handleHalfWidth)
{ this.scope.$emit('slideEnded');
this.hideEl(this.flrLab); this.tracking = '';
this.dragging.active = false;
this.callOnEnd();
},
/**
* Get event names for move and event end
*
* @param {Event} event The event
*
* @return {{moveEvent: string, endEvent: string}}
*/
getEventNames: function(event) {
var eventNames = {moveEvent: '', endEvent: ''};
if (event.touches || (event.originalEvent !== undefined && event.originalEvent.touches)) {
eventNames.moveEvent = 'touchmove';
eventNames.endEvent = 'touchend';
}
else {
eventNames.moveEvent = 'mousemove';
eventNames.endEvent = 'mouseup';
}
return eventNames;
} }
else if( ! flHidden)
{
this.showEl(this.flrLab);
}
}
},
/**
* Update slider selection bar, combined label and range label
*
* @returns {undefined}
*/
updateSelectionBar: function()
{
this.setWidth(this.selBar, Math.abs(this.maxH.rzsl - this.minH.rzsl) + this.handleHalfWidth);
this.setLeft(this.selBar, this.range ? this.minH.rzsl + this.handleHalfWidth : 0);
},
/**
* Update combined label position and value
*
* @returns {undefined}
*/
updateCmbLabel: function()
{
var lowTr, highTr;
if(this.minLab.rzsl + this.minLab.rzsw + 10 >= this.maxLab.rzsl)
{
lowTr = this.getDisplayValue(this.scope.rzSliderModel);
highTr = this.getDisplayValue(this.scope.rzSliderHigh);
this.translateFn(lowTr + ' - ' + highTr, this.cmbLab, false);
this.setLeft(this.cmbLab, this.selBar.rzsl + this.selBar.rzsw / 2 - this.cmbLab.rzsw / 2);
this.hideEl(this.minLab);
this.hideEl(this.maxLab);
this.showEl(this.cmbLab);
}
else
{
this.showEl(this.maxLab);
this.showEl(this.minLab);
this.hideEl(this.cmbLab);
}
},
/**
* Return the translated value if a translate function is provided else the original value
* @param value
* @returns {*}
*/
getDisplayValue: function(value) {
return this.customTrFn ? this.customTrFn(value): value;
},
/**
* Round value to step and precision
*
* @param {number} value
* @returns {number}
*/
roundStep: function(value)
{
var step = this.step,
remainder = +((value - this.minValue) % step).toFixed(3),
steppedValue = remainder > (step / 2) ? value + step - remainder : value - remainder;
steppedValue = steppedValue.toFixed(this.precision);
return +steppedValue;
},
/**
* Hide element
*
* @param element
* @returns {jqLite} The jqLite wrapped DOM element
*/
hideEl: function (element)
{
return element.css({opacity: 0});
},
/**
* Show element
*
* @param element The jqLite wrapped DOM element
* @returns {jqLite} The jqLite
*/
showEl: function (element)
{
if(!!element.rzAlwaysHide) { return element; }
return element.css({opacity: 1});
},
/**
* Set element left offset
*
* @param {jqLite} elem The jqLite wrapped DOM element
* @param {number} left
* @returns {number}
*/
setLeft: function (elem, left)
{
elem.rzsl = left;
elem.css({left: left + 'px'});
return left;
},
/**
* Get element width
*
* @param {jqLite} elem The jqLite wrapped DOM element
* @returns {number}
*/
getWidth: function(elem)
{
var val = elem[0].getBoundingClientRect();
elem.rzsw = val.right - val.left;
return elem.rzsw;
},
/**
* Set element width
*
* @param {jqLite} elem The jqLite wrapped DOM element
* @param {number} width
* @returns {number}
*/
setWidth: function(elem, width)
{
elem.rzsw = width;
elem.css({width: width + 'px'});
return width;
},
/**
* Translate value to pixel offset
*
* @param {number} val
* @returns {number}
*/
valueToOffset: function(val)
{
return (val - this.minValue) * this.maxLeft / this.valueRange || 0;
},
/**
* Translate offset to model value
*
* @param {number} offset
* @returns {number}
*/
offsetToValue: function(offset)
{
return (offset / this.maxLeft) * this.valueRange + this.minValue;
},
// Events
/**
* Get the X-coordinate of an event
*
* @param {Object} event The event
* @returns {number}
*/
getEventX: function(event)
{
/* http://stackoverflow.com/a/12336075/282882 */
//noinspection JSLint
if('clientX' in event)
{
return event.clientX;
}
return event.originalEvent === undefined ?
event.touches[0].clientX
: event.originalEvent.touches[0].clientX;
},
/**
* Get the handle closest to an event.
*
* @param event {Event} The event
* @returns {jqLite} The handle closest to the event.
*/
getNearestHandle: function(event)
{
if (!this.range) { return this.minH; }
var offset = this.getEventX(event) - this.sliderElem.rzsl - this.handleHalfWidth;
return Math.abs(offset - this.minH.rzsl) < Math.abs(offset - this.maxH.rzsl) ? this.minH : this.maxH;
},
/**
* Bind mouse and touch events to slider handles
*
* @returns {undefined}
*/
bindEvents: function()
{
if(this.presentOnly || this.disabled) return;
var barTracking, barStart, barMove;
if (this.dragRange)
{
barTracking = 'rzSliderDrag';
barStart = this.onDragStart;
barMove = this.onDragMove;
}
else
{
barTracking = 'rzSliderModel';
barStart = this.onStart;
barMove = this.onMove;
}
this.minH.on('mousedown', angular.bind(this, this.onStart, this.minH, 'rzSliderModel'));
if(this.range) { this.maxH.on('mousedown', angular.bind(this, this.onStart, this.maxH, 'rzSliderHigh')); }
this.fullBar.on('mousedown', angular.bind(this, this.onStart, null, null));
this.fullBar.on('mousedown', angular.bind(this, this.onMove, this.fullBar));
this.selBar.on('mousedown', angular.bind(this, barStart, null, barTracking));
this.selBar.on('mousedown', angular.bind(this, barMove, this.selBar));
this.ticks.on('mousedown', angular.bind(this, this.onStart, null, null));
this.ticks.on('mousedown', angular.bind(this, this.onMove, this.ticks));
this.minH.on('touchstart', angular.bind(this, this.onStart, this.minH, 'rzSliderModel'));
if(this.range) { this.maxH.on('touchstart', angular.bind(this, this.onStart, this.maxH, 'rzSliderHigh')); }
this.fullBar.on('touchstart', angular.bind(this, this.onStart, null, null));
this.fullBar.on('touchstart', angular.bind(this, this.onMove, this.fullBar));
this.selBar.on('touchstart', angular.bind(this, barStart, null, barTracking));
this.selBar.on('touchstart', angular.bind(this, barMove, this.selBar));
this.ticks.on('touchstart', angular.bind(this, this.onStart, null, null));
this.ticks.on('touchstart', angular.bind(this, this.onMove, this.ticks));
},
/**
* Unbind mouse and touch events to slider handles
*
* @returns {undefined}
*/
unbindEvents: function()
{
this.minH.off();
this.maxH.off();
this.fullBar.off();
this.selBar.off();
this.ticks.off();
},
/**
* onStart event handler
*
* @param {?Object} pointer The jqLite wrapped DOM element; if null, the closest handle is used
* @param {?string} ref The name of the handle being changed; if null, the closest handle's value is modified
* @param {Event} event The event
* @returns {undefined}
*/
onStart: function (pointer, ref, event)
{
var ehMove, ehEnd,
eventNames = this.getEventNames(event);
event.stopPropagation();
event.preventDefault();
if(this.tracking !== '') { return; }
// We have to do this in case the HTML where the sliders are on
// have been animated into view.
this.calcViewDimensions();
if(pointer)
{
this.tracking = ref;
}
else
{
pointer = this.getNearestHandle(event);
this.tracking = pointer === this.minH ? 'rzSliderModel' : 'rzSliderHigh';
}
pointer.addClass('rz-active');
ehMove = angular.bind(this, this.dragging.active ? this.onDragMove : this.onMove, pointer);
ehEnd = angular.bind(this, this.onEnd, ehMove);
$document.on(eventNames.moveEvent, ehMove);
$document.one(eventNames.endEvent, ehEnd);
this.callOnStart();
},
/**
* onMove event handler
*
* @param {jqLite} pointer
* @param {Event} event The event
* @returns {undefined}
*/
onMove: function (pointer, event)
{
var eventX = this.getEventX(event),
sliderLO, newOffset, newValue;
sliderLO = this.sliderElem.rzsl;
newOffset = eventX - sliderLO - this.handleHalfWidth;
if(newOffset <= 0)
{
if(pointer.rzsl === 0)
return;
newValue = this.minValue;
newOffset = 0;
}
else if(newOffset >= this.maxLeft)
{
if(pointer.rzsl === this.maxLeft)
return;
newValue = this.maxValue;
newOffset = this.maxLeft;
}
else {
newValue = this.offsetToValue(newOffset);
newValue = this.roundStep(newValue);
newOffset = this.valueToOffset(newValue);
}
this.positionTrackingHandle(newValue, newOffset);
},
/**
* onDragStart event handler
*
* Handles dragging of the middle bar.
*
* @param {Object} pointer The jqLite wrapped DOM element
* @param {string} ref One of the refLow, refHigh values
* @param {Event} event The event
* @returns {undefined}
*/
onDragStart: function(pointer, ref, event)
{
var offset = this.getEventX(event) - this.sliderElem.rzsl - this.handleHalfWidth;
this.dragging = {
active: true,
value: this.offsetToValue(offset),
difference: this.scope.rzSliderHigh - this.scope.rzSliderModel,
offset: offset,
lowDist: offset - this.minH.rzsl,
highDist: this.maxH.rzsl - offset
}; };
this.minH.addClass('rz-active');
this.maxH.addClass('rz-active');
this.onStart(pointer, ref, event);
},
/**
* onDragMove event handler
*
* Handles dragging of the middle bar.
*
* @param {jqLite} pointer
* @param {Event} event The event
* @returns {undefined}
*/
onDragMove: function(pointer, event)
{
var newOffset = this.getEventX(event) - this.sliderElem.rzsl - this.handleHalfWidth,
newMinOffset, newMaxOffset,
newMinValue, newMaxValue;
if (newOffset <= this.dragging.lowDist)
{
if (pointer.rzsl === this.dragging.lowDist) { return; }
newMinValue = this.minValue;
newMinOffset = 0;
newMaxValue = this.dragging.difference;
newMaxOffset = this.valueToOffset(newMaxValue);
}
else if (newOffset >= this.maxLeft - this.dragging.highDist)
{
if (pointer.rzsl === this.dragging.highDist) { return; }
newMaxValue = this.maxValue;
newMaxOffset = this.maxLeft;
newMinValue = this.maxValue - this.dragging.difference;
newMinOffset = this.valueToOffset(newMinValue);
}
else
{
newMinValue = this.offsetToValue(newOffset - this.dragging.lowDist);
newMinValue = this.roundStep(newMinValue);
newMinOffset = this.valueToOffset(newMinValue);
newMaxValue = newMinValue + this.dragging.difference;
newMaxOffset = this.valueToOffset(newMaxValue);
}
this.positionTrackingBar(newMinValue, newMaxValue, newMinOffset, newMaxOffset);
},
/**
* Set the new value and offset for the entire bar
*
* @param {number} newMinValue the new minimum value
* @param {number} newMaxValue the new maximum value
* @param {number} newMinOffset the new minimum offset
* @param {number} newMaxOffset the new maximum offset
*/
positionTrackingBar: function(newMinValue, newMaxValue, newMinOffset, newMaxOffset)
{
this.scope.rzSliderModel = newMinValue;
this.scope.rzSliderHigh = newMaxValue;
this.updateHandles('rzSliderModel', newMinOffset);
this.updateHandles('rzSliderHigh', newMaxOffset);
this.scope.$apply();
this.callOnChange();
},
/** return Slider;
* Set the new value and offset to the current tracking handle })
*
* @param {number} newValue new model value .directive('rzslider', function(RzSlider) {
* @param {number} newOffset new offset value 'use strict';
*/
positionTrackingHandle: function(newValue, newOffset) return {
{ restrict: 'E',
if(this.range) scope: {
{ rzSliderModel: '=?',
/* This is to check if we need to switch the min and max handles*/ rzSliderHigh: '=?',
if (this.tracking === 'rzSliderModel' && newValue >= this.scope.rzSliderHigh) rzSliderOptions: '=?',
{ rzSliderTplUrl: '@'
this.scope[this.tracking] = this.scope.rzSliderHigh; },
this.updateHandles(this.tracking, this.maxH.rzsl);
this.tracking = 'rzSliderHigh'; /**
this.minH.removeClass('rz-active'); * Return template URL
this.maxH.addClass('rz-active'); *
/* We need to apply here because we are not sure that we will enter the next block */ * @param {jqLite} elem
this.scope.$apply(); * @param {Object} attrs
this.callOnChange(); * @return {string}
*/
templateUrl: function(elem, attrs) {
//noinspection JSUnresolvedVariable
return attrs.rzSliderTplUrl || 'rzSliderTpl.html';
},
link: function(scope, elem) {
return new RzSlider(scope, elem);
} }
else if(this.tracking === 'rzSliderHigh' && newValue <= this.scope.rzSliderModel) };
{ });
this.scope[this.tracking] = this.scope.rzSliderModel;
this.updateHandles(this.tracking, this.minH.rzsl);
this.tracking = 'rzSliderModel';
this.maxH.removeClass('rz-active');
this.minH.addClass('rz-active');
/* We need to apply here because we are not sure that we will enter the next block */
this.scope.$apply();
this.callOnChange();
}
}
if(this.scope[this.tracking] !== newValue)
{
this.scope[this.tracking] = newValue;
this.updateHandles(this.tracking, newOffset);
this.scope.$apply();
this.callOnChange();
}
},
/**
* onEnd event handler
*
* @param {Event} event The event
* @param {Function} ehMove The the bound move event handler
* @returns {undefined}
*/
onEnd: function(ehMove, event)
{
var moveEventName = this.getEventNames(event).moveEvent;
this.minH.removeClass('rz-active');
this.maxH.removeClass('rz-active');
$document.off(moveEventName, ehMove);
this.scope.$emit('slideEnded');
this.tracking = '';
this.dragging.active = false;
this.callOnEnd();
},
/**
* Get event names for move and event end
*
* @param {Event} event The event
*
* @return {{moveEvent: string, endEvent: string}}
*/
getEventNames: function(event)
{
var eventNames = {moveEvent: '', endEvent: ''};
if(event.touches || (event.originalEvent !== undefined && event.originalEvent.touches))
{
eventNames.moveEvent = 'touchmove';
eventNames.endEvent = 'touchend';
}
else
{
eventNames.moveEvent = 'mousemove';
eventNames.endEvent = 'mouseup';
}
return eventNames;
}
};
return Slider;
})
.directive('rzslider', function(RzSlider)
{
'use strict';
return {
restrict: 'E',
scope: {
rzSliderFloor: '=?',
rzSliderCeil: '=?',
rzSliderStep: '@',
rzSliderPrecision: '@',
rzSliderModel: '=?',
rzSliderHigh: '=?',
rzSliderDraggable: '@',
rzSliderTranslate: '&',
rzSliderHideLimitLabels: '=?',
rzSliderAlwaysShowBar: '=?',
rzSliderPresentOnly: '@',
rzSliderOnStart: '&',
rzSliderOnChange: '&',
rzSliderOnEnd: '&',
rzSliderShowTicks: '=?',
rzSliderShowTicksValue: '=?',
rzSliderDisabled: '=?',
rzSliderInterval: '=?',
},
/**
* Return template URL
*
* @param {jqLite} elem
* @param {Object} attrs
* @return {string}
*/
templateUrl: function(elem, attrs) {
//noinspection JSUnresolvedVariable
return attrs.rzSliderTplUrl || 'rzSliderTpl.html';
},
link: function(scope, elem, attr)
{
return new RzSlider(scope, elem, attr);
}
};
});
// IDE assist // IDE assist
/** /**
* @name ngScope * @name ngScope
* *
* @property {number} rzSliderModel * @property {number} rzSliderModel
* @property {number} rzSliderHigh * @property {number} rzSliderHigh
* @property {number} rzSliderCeil * @property {Object} rzSliderOptions
*/ */
/** /**
* @name jqLite * @name jqLite
* *
* @property {number|undefined} rzsl rzslider label left offset * @property {number|undefined} rzsl rzslider label left offset
* @property {number|undefined} rzsw rzslider element width * @property {number|undefined} rzsw rzslider element width
* @property {string|undefined} rzsv rzslider label value/text * @property {string|undefined} rzsv rzslider label value/text
* @property {Function} css * @property {Function} css
* @property {Function} text * @property {Function} text
*/ */
/** /**
* @name Event * @name Event
* @property {Array} touches * @property {Array} touches
* @property {Event} originalEvent * @property {Event} originalEvent
*/ */
/** /**
* @name ThrottleOptions * @name ThrottleOptions
* *
* @property {boolean} leading * @property {boolean} leading
* @property {boolean} trailing * @property {boolean} trailing
*/ */
/*templateReplacement*/ /*templateReplacement*/
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment