2 * Angular Material Design
3 * https://github.com/angular/material
7 goog.provide('ng.material.components.slider');
8 goog.require('ng.material.core');
11 * @name material.components.slider
13 angular.module('material.components.slider', [
16 .directive('mdSlider', SliderDirective);
21 * @module material.components.slider
24 * The `<md-slider>` component allows the user to choose from a range of
27 * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application)
28 * the slider is in the accent color by default. The primary color palette may be used with
29 * the `md-primary` class.
31 * It has two modes: 'normal' mode, where the user slides between a wide range
32 * of values, and 'discrete' mode, where the user slides between only a few
35 * To enable discrete mode, add the `md-discrete` attribute to a slider,
36 * and use the `step` attribute to change the distance between
37 * values the user is allowed to pick.
40 * <h4>Normal Mode</h4>
42 * <md-slider ng-model="myValue" min="5" max="500">
45 * <h4>Discrete Mode</h4>
47 * <md-slider md-discrete ng-model="myDiscreteValue" step="10" min="10" max="130">
51 * @param {boolean=} md-discrete Whether to enable discrete mode.
52 * @param {number=} step The distance between values the user is allowed to pick. Default 1.
53 * @param {number=} min The minimum value the user is allowed to pick. Default 0.
54 * @param {number=} max The maximum value the user is allowed to pick. Default 100.
56 function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdTheming, $mdGesture, $parse) {
61 '<div class="md-slider-wrapper">\
62 <div class="md-track-container">\
63 <div class="md-track"></div>\
64 <div class="md-track md-track-fill"></div>\
65 <div class="md-track-ticks"></div>\
67 <div class="md-thumb-container">\
68 <div class="md-thumb"></div>\
69 <div class="md-focus-thumb"></div>\
70 <div class="md-focus-ring"></div>\
71 <div class="md-sign">\
72 <span class="md-thumb-text"></span>\
74 <div class="md-disabled-thumb"></div>\
80 // **********************************************************
82 // **********************************************************
84 function compile (tElement, tAttrs) {
90 $mdAria.expect(tElement, 'aria-label');
95 function postLink(scope, element, attr, ngModelCtrl) {
97 ngModelCtrl = ngModelCtrl || {
98 // Mock ngModelController if it doesn't exist to give us
99 // the minimum functionality needed
100 $setViewValue: function(val) {
101 this.$viewValue = val;
102 this.$viewChangeListeners.forEach(function(cb) { cb(); });
106 $viewChangeListeners: []
109 var isDisabledParsed = attr.ngDisabled && $parse(attr.ngDisabled);
110 var isDisabledGetter = isDisabledParsed ?
111 function() { return isDisabledParsed(scope.$parent); } :
113 var thumb = angular.element(element[0].querySelector('.md-thumb'));
114 var thumbText = angular.element(element[0].querySelector('.md-thumb-text'));
115 var thumbContainer = thumb.parent();
116 var trackContainer = angular.element(element[0].querySelector('.md-track-container'));
117 var activeTrack = angular.element(element[0].querySelector('.md-track-fill'));
118 var tickContainer = angular.element(element[0].querySelector('.md-track-ticks'));
119 var throttledRefreshDimensions = $mdUtil.throttle(refreshSliderDimensions, 5000);
121 // Default values, overridable by attrs
122 angular.isDefined(attr.min) ? attr.$observe('min', updateMin) : updateMin(0);
123 angular.isDefined(attr.max) ? attr.$observe('max', updateMax) : updateMax(100);
124 angular.isDefined(attr.step)? attr.$observe('step', updateStep) : updateStep(1);
126 // We have to manually stop the $watch on ngDisabled because it exists
127 // on the parent scope, and won't be automatically destroyed when
128 // the component is destroyed.
129 var stopDisabledWatch = angular.noop;
130 if (attr.ngDisabled) {
131 stopDisabledWatch = scope.$parent.$watch(attr.ngDisabled, updateAriaDisabled);
134 $mdGesture.register(element, 'drag');
137 .on('keydown', keydownListener)
138 .on('$md.pressdown', onPressDown)
139 .on('$md.pressup', onPressUp)
140 .on('$md.dragstart', onDragStart)
141 .on('$md.drag', onDrag)
142 .on('$md.dragend', onDragEnd);
144 // On resize, recalculate the slider's dimensions and re-render
145 function updateAll() {
146 refreshSliderDimensions();
150 setTimeout(updateAll);
152 var debouncedUpdateAll = $$rAF.throttle(updateAll);
153 angular.element($window).on('resize', debouncedUpdateAll);
155 scope.$on('$destroy', function() {
156 angular.element($window).off('resize', debouncedUpdateAll);
160 ngModelCtrl.$render = ngModelRender;
161 ngModelCtrl.$viewChangeListeners.push(ngModelRender);
162 ngModelCtrl.$formatters.push(minMaxValidator);
163 ngModelCtrl.$formatters.push(stepValidator);
171 function updateMin(value) {
172 min = parseFloat(value);
173 element.attr('aria-valuemin', value);
176 function updateMax(value) {
177 max = parseFloat(value);
178 element.attr('aria-valuemax', value);
181 function updateStep(value) {
182 step = parseFloat(value);
185 function updateAriaDisabled(isDisabled) {
186 element.attr('aria-disabled', !!isDisabled);
189 // Draw the ticks with canvas.
190 // The alternative to drawing ticks with canvas is to draw one element for each tick,
191 // which could quickly become a performance bottleneck.
192 var tickCanvas, tickCtx;
193 function redrawTicks() {
194 if (!angular.isDefined(attr.mdDiscrete)) return;
196 var numSteps = Math.floor( (max - min) / step );
198 var trackTicksStyle = $window.getComputedStyle(tickContainer[0]);
199 tickCanvas = angular.element('<canvas style="position:absolute;">');
200 tickCtx = tickCanvas[0].getContext('2d');
201 tickCtx.fillStyle = trackTicksStyle.backgroundColor || 'black';
202 tickContainer.append(tickCanvas);
204 var dimensions = getSliderDimensions();
205 tickCanvas[0].width = dimensions.width;
206 tickCanvas[0].height = dimensions.height;
209 for (var i = 0; i <= numSteps; i++) {
210 distance = Math.floor(dimensions.width * (i / numSteps));
211 tickCtx.fillRect(distance - 1, 0, 2, dimensions.height);
217 * Refreshing Dimensions
219 var sliderDimensions = {};
220 refreshSliderDimensions();
221 function refreshSliderDimensions() {
222 sliderDimensions = trackContainer[0].getBoundingClientRect();
224 function getSliderDimensions() {
225 throttledRefreshDimensions();
226 return sliderDimensions;
230 * left/right arrow listener
232 function keydownListener(ev) {
233 if(element[0].hasAttribute('disabled')) {
238 if (ev.keyCode === $mdConstant.KEY_CODE.LEFT_ARROW) {
239 changeAmount = -step;
240 } else if (ev.keyCode === $mdConstant.KEY_CODE.RIGHT_ARROW) {
244 if (ev.metaKey || ev.ctrlKey || ev.altKey) {
248 ev.stopPropagation();
249 scope.$evalAsync(function() {
250 setModelValue(ngModelCtrl.$viewValue + changeAmount);
256 * ngModel setters and validators
258 function setModelValue(value) {
259 ngModelCtrl.$setViewValue( minMaxValidator(stepValidator(value)) );
261 function ngModelRender() {
262 if (isNaN(ngModelCtrl.$viewValue)) {
263 ngModelCtrl.$viewValue = ngModelCtrl.$modelValue;
266 var percent = (ngModelCtrl.$viewValue - min) / (max - min);
267 scope.modelValue = ngModelCtrl.$viewValue;
268 element.attr('aria-valuenow', ngModelCtrl.$viewValue);
269 setSliderPercent(percent);
270 thumbText.text( ngModelCtrl.$viewValue );
273 function minMaxValidator(value) {
274 if (angular.isNumber(value)) {
275 return Math.max(min, Math.min(max, value));
278 function stepValidator(value) {
279 if (angular.isNumber(value)) {
280 var formattedValue = (Math.round(value / step) * step);
281 // Format to 3 digits after the decimal point - fixes #2015.
282 return (Math.round(formattedValue * 1000) / 1000);
289 function setSliderPercent(percent) {
290 activeTrack.css('width', (percent * 100) + '%');
293 (percent * 100) + '%'
295 element.toggleClass('md-min', percent === 0);
302 var isDragging = false;
303 var isDiscrete = angular.isDefined(attr.mdDiscrete);
305 function onPressDown(ev) {
306 if (isDisabledGetter()) return;
308 element.addClass('active');
310 refreshSliderDimensions();
312 var exactVal = percentToValue( positionToPercent( ev.pointer.x ));
313 var closestVal = minMaxValidator( stepValidator(exactVal) );
314 scope.$apply(function() {
315 setModelValue( closestVal );
316 setSliderPercent( valueToPercent(closestVal));
319 function onPressUp(ev) {
320 if (isDisabledGetter()) return;
322 element.removeClass('dragging active');
324 var exactVal = percentToValue( positionToPercent( ev.pointer.x ));
325 var closestVal = minMaxValidator( stepValidator(exactVal) );
326 scope.$apply(function() {
327 setModelValue(closestVal);
331 function onDragStart(ev) {
332 if (isDisabledGetter()) return;
334 ev.stopPropagation();
336 element.addClass('dragging');
337 setSliderFromEvent(ev);
339 function onDrag(ev) {
340 if (!isDragging) return;
341 ev.stopPropagation();
342 setSliderFromEvent(ev);
344 function onDragEnd(ev) {
345 if (!isDragging) return;
346 ev.stopPropagation();
350 function setSliderFromEvent(ev) {
351 // While panning discrete, update only the
352 // visual positioning but not the model value.
353 if ( isDiscrete ) adjustThumbPosition( ev.pointer.x );
354 else doSlide( ev.pointer.x );
358 * Slide the UI by changing the model value
361 function doSlide( x ) {
362 scope.$evalAsync( function() {
363 setModelValue( percentToValue( positionToPercent(x) ));
368 * Slide the UI without changing the model (while dragging/panning)
371 function adjustThumbPosition( x ) {
372 var exactVal = percentToValue( positionToPercent( x ));
373 var closestVal = minMaxValidator( stepValidator(exactVal) );
374 setSliderPercent( positionToPercent(x) );
375 thumbText.text( closestVal );
379 * Convert horizontal position on slider to percentage value of offset from beginning...
383 function positionToPercent( x ) {
384 return Math.max(0, Math.min(1, (x - sliderDimensions.left) / (sliderDimensions.width)));
388 * Convert percentage offset on slide to equivalent model value
392 function percentToValue( percent ) {
393 return (min + percent * (max - min));
396 function valueToPercent( val ) {
397 return (val - min)/(max - min);
401 SliderDirective.$inject = ["$$rAF", "$window", "$mdAria", "$mdUtil", "$mdConstant", "$mdTheming", "$mdGesture", "$parse"];
403 ng.material.components.slider = angular.module("material.components.slider");