2 * Angular Material Design
3 * https://github.com/angular/material
7 (function( window, angular, undefined ){
12 * @name material.components.slider
14 angular.module('material.components.slider', [
17 .directive('mdSlider', SliderDirective);
22 * @module material.components.slider
25 * The `<md-slider>` component allows the user to choose from a range of
28 * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application)
29 * the slider is in the accent color by default. The primary color palette may be used with
30 * the `md-primary` class.
32 * It has two modes: 'normal' mode, where the user slides between a wide range
33 * of values, and 'discrete' mode, where the user slides between only a few
36 * To enable discrete mode, add the `md-discrete` attribute to a slider,
37 * and use the `step` attribute to change the distance between
38 * values the user is allowed to pick.
41 * <h4>Normal Mode</h4>
43 * <md-slider ng-model="myValue" min="5" max="500">
46 * <h4>Discrete Mode</h4>
48 * <md-slider md-discrete ng-model="myDiscreteValue" step="10" min="10" max="130">
52 * @param {boolean=} md-discrete Whether to enable discrete mode.
53 * @param {number=} step The distance between values the user is allowed to pick. Default 1.
54 * @param {number=} min The minimum value the user is allowed to pick. Default 0.
55 * @param {number=} max The maximum value the user is allowed to pick. Default 100.
57 function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdTheming, $mdGesture, $parse) {
62 '<div class="md-slider-wrapper">\
63 <div class="md-track-container">\
64 <div class="md-track"></div>\
65 <div class="md-track md-track-fill"></div>\
66 <div class="md-track-ticks"></div>\
68 <div class="md-thumb-container">\
69 <div class="md-thumb"></div>\
70 <div class="md-focus-thumb"></div>\
71 <div class="md-focus-ring"></div>\
72 <div class="md-sign">\
73 <span class="md-thumb-text"></span>\
75 <div class="md-disabled-thumb"></div>\
81 // **********************************************************
83 // **********************************************************
85 function compile (tElement, tAttrs) {
91 $mdAria.expect(tElement, 'aria-label');
96 function postLink(scope, element, attr, ngModelCtrl) {
98 ngModelCtrl = ngModelCtrl || {
99 // Mock ngModelController if it doesn't exist to give us
100 // the minimum functionality needed
101 $setViewValue: function(val) {
102 this.$viewValue = val;
103 this.$viewChangeListeners.forEach(function(cb) { cb(); });
107 $viewChangeListeners: []
110 var isDisabledParsed = attr.ngDisabled && $parse(attr.ngDisabled);
111 var isDisabledGetter = isDisabledParsed ?
112 function() { return isDisabledParsed(scope.$parent); } :
114 var thumb = angular.element(element[0].querySelector('.md-thumb'));
115 var thumbText = angular.element(element[0].querySelector('.md-thumb-text'));
116 var thumbContainer = thumb.parent();
117 var trackContainer = angular.element(element[0].querySelector('.md-track-container'));
118 var activeTrack = angular.element(element[0].querySelector('.md-track-fill'));
119 var tickContainer = angular.element(element[0].querySelector('.md-track-ticks'));
120 var throttledRefreshDimensions = $mdUtil.throttle(refreshSliderDimensions, 5000);
122 // Default values, overridable by attrs
123 angular.isDefined(attr.min) ? attr.$observe('min', updateMin) : updateMin(0);
124 angular.isDefined(attr.max) ? attr.$observe('max', updateMax) : updateMax(100);
125 angular.isDefined(attr.step)? attr.$observe('step', updateStep) : updateStep(1);
127 // We have to manually stop the $watch on ngDisabled because it exists
128 // on the parent scope, and won't be automatically destroyed when
129 // the component is destroyed.
130 var stopDisabledWatch = angular.noop;
131 if (attr.ngDisabled) {
132 stopDisabledWatch = scope.$parent.$watch(attr.ngDisabled, updateAriaDisabled);
135 $mdGesture.register(element, 'drag');
138 .on('keydown', keydownListener)
139 .on('$md.pressdown', onPressDown)
140 .on('$md.pressup', onPressUp)
141 .on('$md.dragstart', onDragStart)
142 .on('$md.drag', onDrag)
143 .on('$md.dragend', onDragEnd);
145 // On resize, recalculate the slider's dimensions and re-render
146 function updateAll() {
147 refreshSliderDimensions();
151 setTimeout(updateAll);
153 var debouncedUpdateAll = $$rAF.throttle(updateAll);
154 angular.element($window).on('resize', debouncedUpdateAll);
156 scope.$on('$destroy', function() {
157 angular.element($window).off('resize', debouncedUpdateAll);
161 ngModelCtrl.$render = ngModelRender;
162 ngModelCtrl.$viewChangeListeners.push(ngModelRender);
163 ngModelCtrl.$formatters.push(minMaxValidator);
164 ngModelCtrl.$formatters.push(stepValidator);
172 function updateMin(value) {
173 min = parseFloat(value);
174 element.attr('aria-valuemin', value);
177 function updateMax(value) {
178 max = parseFloat(value);
179 element.attr('aria-valuemax', value);
182 function updateStep(value) {
183 step = parseFloat(value);
186 function updateAriaDisabled(isDisabled) {
187 element.attr('aria-disabled', !!isDisabled);
190 // Draw the ticks with canvas.
191 // The alternative to drawing ticks with canvas is to draw one element for each tick,
192 // which could quickly become a performance bottleneck.
193 var tickCanvas, tickCtx;
194 function redrawTicks() {
195 if (!angular.isDefined(attr.mdDiscrete)) return;
197 var numSteps = Math.floor( (max - min) / step );
199 var trackTicksStyle = $window.getComputedStyle(tickContainer[0]);
200 tickCanvas = angular.element('<canvas style="position:absolute;">');
201 tickCtx = tickCanvas[0].getContext('2d');
202 tickCtx.fillStyle = trackTicksStyle.backgroundColor || 'black';
203 tickContainer.append(tickCanvas);
205 var dimensions = getSliderDimensions();
206 tickCanvas[0].width = dimensions.width;
207 tickCanvas[0].height = dimensions.height;
210 for (var i = 0; i <= numSteps; i++) {
211 distance = Math.floor(dimensions.width * (i / numSteps));
212 tickCtx.fillRect(distance - 1, 0, 2, dimensions.height);
218 * Refreshing Dimensions
220 var sliderDimensions = {};
221 refreshSliderDimensions();
222 function refreshSliderDimensions() {
223 sliderDimensions = trackContainer[0].getBoundingClientRect();
225 function getSliderDimensions() {
226 throttledRefreshDimensions();
227 return sliderDimensions;
231 * left/right arrow listener
233 function keydownListener(ev) {
234 if(element[0].hasAttribute('disabled')) {
239 if (ev.keyCode === $mdConstant.KEY_CODE.LEFT_ARROW) {
240 changeAmount = -step;
241 } else if (ev.keyCode === $mdConstant.KEY_CODE.RIGHT_ARROW) {
245 if (ev.metaKey || ev.ctrlKey || ev.altKey) {
249 ev.stopPropagation();
250 scope.$evalAsync(function() {
251 setModelValue(ngModelCtrl.$viewValue + changeAmount);
257 * ngModel setters and validators
259 function setModelValue(value) {
260 ngModelCtrl.$setViewValue( minMaxValidator(stepValidator(value)) );
262 function ngModelRender() {
263 if (isNaN(ngModelCtrl.$viewValue)) {
264 ngModelCtrl.$viewValue = ngModelCtrl.$modelValue;
267 var percent = (ngModelCtrl.$viewValue - min) / (max - min);
268 scope.modelValue = ngModelCtrl.$viewValue;
269 element.attr('aria-valuenow', ngModelCtrl.$viewValue);
270 setSliderPercent(percent);
271 thumbText.text( ngModelCtrl.$viewValue );
274 function minMaxValidator(value) {
275 if (angular.isNumber(value)) {
276 return Math.max(min, Math.min(max, value));
279 function stepValidator(value) {
280 if (angular.isNumber(value)) {
281 var formattedValue = (Math.round(value / step) * step);
282 // Format to 3 digits after the decimal point - fixes #2015.
283 return (Math.round(formattedValue * 1000) / 1000);
290 function setSliderPercent(percent) {
291 activeTrack.css('width', (percent * 100) + '%');
294 (percent * 100) + '%'
296 element.toggleClass('md-min', percent === 0);
303 var isDragging = false;
304 var isDiscrete = angular.isDefined(attr.mdDiscrete);
306 function onPressDown(ev) {
307 if (isDisabledGetter()) return;
309 element.addClass('active');
311 refreshSliderDimensions();
313 var exactVal = percentToValue( positionToPercent( ev.pointer.x ));
314 var closestVal = minMaxValidator( stepValidator(exactVal) );
315 scope.$apply(function() {
316 setModelValue( closestVal );
317 setSliderPercent( valueToPercent(closestVal));
320 function onPressUp(ev) {
321 if (isDisabledGetter()) return;
323 element.removeClass('dragging active');
325 var exactVal = percentToValue( positionToPercent( ev.pointer.x ));
326 var closestVal = minMaxValidator( stepValidator(exactVal) );
327 scope.$apply(function() {
328 setModelValue(closestVal);
332 function onDragStart(ev) {
333 if (isDisabledGetter()) return;
335 ev.stopPropagation();
337 element.addClass('dragging');
338 setSliderFromEvent(ev);
340 function onDrag(ev) {
341 if (!isDragging) return;
342 ev.stopPropagation();
343 setSliderFromEvent(ev);
345 function onDragEnd(ev) {
346 if (!isDragging) return;
347 ev.stopPropagation();
351 function setSliderFromEvent(ev) {
352 // While panning discrete, update only the
353 // visual positioning but not the model value.
354 if ( isDiscrete ) adjustThumbPosition( ev.pointer.x );
355 else doSlide( ev.pointer.x );
359 * Slide the UI by changing the model value
362 function doSlide( x ) {
363 scope.$evalAsync( function() {
364 setModelValue( percentToValue( positionToPercent(x) ));
369 * Slide the UI without changing the model (while dragging/panning)
372 function adjustThumbPosition( x ) {
373 var exactVal = percentToValue( positionToPercent( x ));
374 var closestVal = minMaxValidator( stepValidator(exactVal) );
375 setSliderPercent( positionToPercent(x) );
376 thumbText.text( closestVal );
380 * Convert horizontal position on slider to percentage value of offset from beginning...
384 function positionToPercent( x ) {
385 return Math.max(0, Math.min(1, (x - sliderDimensions.left) / (sliderDimensions.width)));
389 * Convert percentage offset on slide to equivalent model value
393 function percentToValue( percent ) {
394 return (min + percent * (max - min));
397 function valueToPercent( val ) {
398 return (val - min)/(max - min);
402 SliderDirective.$inject = ["$$rAF", "$window", "$mdAria", "$mdUtil", "$mdConstant", "$mdTheming", "$mdGesture", "$parse"];
404 })(window, window.angular);