<body ng-app="app" ng-controller="controller">
<range-slider config="config" model="model"></range-slider>
<hr />
<div class="row">
<div>
<pre>model = {{model | json}}</pre>
</div>
<div>
<pre>config = {{config | json}}</pre>
</div>
<div>
<p><input type="number" ng-model="config.min" /> <span>Min</span></p>
<p><input type="number" ng-model="config.max" /> <span>Max</span></p>
<p><input type="number" ng-model="config.gap" step="0.1" /> <span>Gap</span></p>
<p><input type="number" ng-model="config.step" step="0.01" /> <span>Step</span></p>
<p><input type="number" ng-model="config.precision" /> <span>Precision</span></p>
</div>
</div>
</body>
$dark: #212429;
$light: #fff;
$min: #17b5fa;
$gap: #9ccf5e;
$max: #f45035;
@mixin slider-thumbs() {
pointer-events: all;
position: relative;
z-index: 9;
outline: 0;
width: 1em;
height: 1em;
z-index: 10;
background: $light;
box-shadow: 0 .15em .5em 0 rgba($dark, .35);
border-radius: 2em;
cursor: col-resize;
border: 0;
}
@mixin slider-tracks() {
pointer-events: none;
position: absolute;
padding: 0;
margin: 0;
top: 0;
right: 0;
bottom: 0;
left: 0;
height: 100%;
width: 100%;
outline: none;
border-radius: 1em;
background: none;
border: 0;
}
.range-slider {
position: relative;
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
padding: 0 0 1.5em 0;
font-size: 4.5vmin;
input {
appearance: none;
appearance: none;
font-size: 1em;
z-index: 1;
@include slider-tracks();
&:active,
&:focus {
outline: 0;
z-index: 2;
}
&::focus-outer {
border: 0;
}
// slider track
&::slider-runnable-track {
margin: 0 -.5em;
}
&::range-track {
@include slider-tracks();
}
// slider thumbs
&::slider-thumb {
appearance: none;
@include slider-thumbs();
}
&::range-thumb {
appearance: none;
@include slider-thumbs();
}
}
.label {
padding: 1em;
user-select: none;
pointer-events: none;
}
}
.range-slider-set {
position: relative;
flex: 1;
height: .5em;
border-radius: 1em;
background: rgba(mix($dark, $light), .5);
pointer-events: none;
[class*='span-'] {
pointer-events: none;
position: absolute;
top:0;
bottom: 0;
&.span-min {
border-radius: 1em 0 0 1em;
}
&.span-gap[style*='left:0%'] {
border-top-left-radius: 1em;
border-bottom-left-radius: 1em;
}
&.span-gap[style*='right:0%'] {
border-top-right-radius: 1em;
border-bottom-right-radius: 1em;
}
&.span-max {
border-radius: 0 1em 1em 0;
}
}
.span-min {background: $min linear-gradient(90deg, $min 50%, $gap 100%);}
.span-gap {background: $gap;}
.span-max {background: $max linear-gradient(90deg, $gap 0%, $max 50%);}
.handle {
position: absolute;
top: 100%;
left: 50%;
line-height: 1;
white-space: nowrap;
text-align: center;
width: auto;
cursor: pointer;
z-index: 3;
&.from-value {
transform: translate(-100%, 50%);
}
&.to-value {
transform: translate(0%, 50%);
}
}
}
// boring
body {
background: $dark;
color: $light;
font-family: sans-serif;
}
.row {
display: flex;
justify-content: space-between;
font-size: 2.5vmin;
> div {
flex: 1;
padding: 1.5em;
}
}
input {
border: 2px solid;
padding: .5em;
border-radius: .4em;
font-size: inherit;
font-family: inherit;
width: 3em;
text-align: right;
}
View Compiled
(function(angular, _) {
'use strict';
angular
.module('app', [])
.controller('controller', ['$scope', function($scope) {
$scope.config = {};
$scope.model = {from:65, to:80};
}])
.directive('rangeSlider', [function() {
return {
restrict: 'E',
replace: true,
scope: {
config: '=?',
model: '=',
},
template: '' +
'<div class="range-slider">' +
'<div class="label min-value"><span>{{config.min}}</span></div>' +
'<div class="range-slider-set">' +
'<div class="span-min" style="left:0;right:{{(100 - _handleMinX)}}%"></div>' +
'<div class="span-gap" style="left:{{_handleMinX}}%;right:{{(100 - _handleMaxX)}}%"></div>' +
'<div class="span-max" style="left:{{_handleMaxX}}%;right:0"></div>' +
'<input type="range" class="slider-min" ng-change="_which=0" ng-model="_model[0]" min="{{_values.min}}" max="{{_values.max}}" step="{{_step}}" />' +
'<input type="range" class="slider-max" ng-change="_which=1" ng-model="_model[1]" min="{{_values.min}}" max="{{_values.max}}" step="{{_step}}" />' +
'<div class="handle from-value" style="left:{{_handleMinX}}%"><span>{{_model[0]|number:config.precision}}</span></div>' +
'<div class="handle to-value" style="left:{{_handleMaxX}}%"><span>{{_model[1]|number:config.precision}}</span></div>' +
'</div>' +
'<div class="label max-value"><span>{{config.max}}</span></div>' +
'</div>',
link: function link(scope, element) {
var defaultConfig = {
min: 0,
max: 100,
gap: 10,
step: 0.1,
precision: 1,
};
scope.config = _.extend({}, angular.copy(defaultConfig), scope.config);
scope._model = [
scope.model.from,
scope.model.to
];
scope._values = {
min: scope.config.min || 0,
max: scope.config.max || 100
};
scope._labels = {
min: scope._values.min,
max: scope._values.max,
};
scope._step = scope.config.step || 1;
scope._gap = parseFloat(scope.config.gap) || 0;
// Responsible for determining which slider the user was moving,
// which help us resolve occurrences of sliders overlapping.
scope._which = 0;
var _notInRunLoop = function _notInRunLoop() {
return !scope.$root.$$phase;
};
var _reevaluateInputs = function() {
var inputElements = element.find('input');
_.each(inputElements, function(inputElement, index) {
inputElement = angular.element(inputElement);
inputElement.val(scope._model[index]);
});
};
var _updateModel = function _updateModel(model) {
scope.model = {
from: parseFloat((model[0]).toFixed(scope.config.precision)),
to: parseFloat((model[1]).toFixed(scope.config.precision)),
};
scope._handleMinX = ((scope._model[0] - scope._values.min) / (scope._values.max - scope._values.min)) * 100;
scope._handleMaxX = ((scope._model[1] - scope._values.min) / (scope._values.max - scope._values.min)) * 100;
scope._handleMinX = scope._handleMinX < 0 ? 0 : scope._handleMinX;
scope._handleMaxX = scope._handleMaxX > 100 ? 100 : scope._handleMaxX;
if (_notInRunLoop()) {
try {
// Sometimes we're outside of the Angular run-loop,
// and therefore need to manually invoke the `apply` method!
scope.$apply();
} catch (e) {}
}
};
scope.$watch('config', function(n, o) {
scope.config = _.extend({}, angular.copy(defaultConfig), scope.config);
if (n && o && n !== o) {
scope.config.min = parseFloat(scope.config.min);
scope.config.max = parseFloat(scope.config.max);
scope.config.gap = parseFloat(scope.config.gap);
// fix the gap
if (scope.config.gap > scope.config.max - scope.config.min) {
scope.config.gap = scope.config.max - scope.config.min;
}
scope.config.step = parseFloat(scope.config.step);
scope._gap = scope.config.gap;
scope._step = scope.config.step;
scope._values = {
min: scope.config.min || 0,
max: scope.config.max || 100
};
scope._labels = {
min: scope.config.labels.min || scope._values.min,
max: scope.config.labels.max || scope._values.max
};
_reevaluateInputs();
}
}, true);
// Listen for any changes to the original model.
scope.$watch('model', function alteredValues() {
scope._model = [scope.model.from, scope.model.to];
_reevaluateInputs();
}, true);
// Observe the `_model` for any changes.
scope.$watchCollection('_model', function modelChanged() {
scope._model[0] = parseFloat(scope._model[0])*1;
scope._model[1] = parseFloat(scope._model[1])*1;
// User was moving the first slider.
if (scope._which === 0 && scope._model[1] - scope._gap < scope._model[0]) {
scope._model[1] = scope._model[0] + scope._gap;
}
// Otherwise they were moving the second slider.
if (scope._which === 1 && scope._model[0] + scope._gap > scope._model[1]) {
scope._model[0] = scope._model[1] - scope._gap;
}
// Constrain to the min/max values.
(function constrainMinMax() {
if (scope._model[0] < scope._values.min) {
scope._model[0] = scope._values.min;
}
if (scope._model[1] < scope._values.min) {
scope._model[1] = scope._values.min;
}
if (scope._model[0] > scope._values.max) {
scope._model[0] = scope._values.max;
}
if (scope._model[1] > scope._values.max) {
scope._model[1] = scope._values.max;
}
// mind the gap
var _m0Gap = scope._model[0] + scope._gap;
var _m1Gap = scope._model[1] - scope._gap;
if (scope._model[0] > _m1Gap) {
scope._model[0] = _m1Gap;
}
if (scope._model[1] < _m0Gap) {
scope._model[1] = _m0Gap;
}
})();
// Update the model!
_updateModel(scope._model);
_reevaluateInputs();
});
}
};
}]);
})(window.angular, window._);
This Pen doesn't use any external CSS resources.