2 * Angular Material Design
3 * https://github.com/angular/material
7 goog.provide('ngmaterial.components.slider');
8 goog.require('ngmaterial.core');
11 * @name material.components.slider
13 SliderDirective['$inject'] = ["$$rAF", "$window", "$mdAria", "$mdUtil", "$mdConstant", "$mdTheming", "$mdGesture", "$parse", "$log", "$timeout"];
14 angular.module('material.components.slider', [
17 .directive('mdSlider', SliderDirective)
18 .directive('mdSliderContainer', SliderContainerDirective);
22 * @name mdSliderContainer
23 * @module material.components.slider
26 * The `<md-slider-container>` contains slider with two other elements.
30 * <h4>Normal Mode</h4>
34 function SliderContainerDirective() {
36 controller: function () {},
37 compile: function (elem) {
38 var slider = elem.find('md-slider');
44 var vertical = slider.attr('md-vertical');
46 if (vertical !== undefined) {
47 elem.attr('md-vertical', '');
50 if(!slider.attr('flex')) {
51 slider.attr('flex', '');
54 return function postLink(scope, element, attr, ctrl) {
55 element.addClass('_md'); // private md component indicator for styling
57 // We have to manually stop the $watch on ngDisabled because it exists
58 // on the parent scope, and won't be automatically destroyed when
59 // the component is destroyed.
60 function setDisable(value) {
61 element.children().attr('disabled', value);
62 element.find('input').attr('disabled', value);
65 var stopDisabledWatch = angular.noop;
70 else if (attr.ngDisabled) {
71 stopDisabledWatch = scope.$watch(attr.ngDisabled, function (value) {
76 scope.$on('$destroy', function () {
82 ctrl.fitInputWidthToTextLength = function (length) {
83 var input = element[0].querySelector('md-input-container');
86 var computedStyle = getComputedStyle(input);
87 var minWidth = parseInt(computedStyle.minWidth);
88 var padding = parseInt(computedStyle.padding) * 2;
90 initialMaxWidth = initialMaxWidth || parseInt(computedStyle.maxWidth);
91 var newMaxWidth = Math.max(initialMaxWidth, minWidth + padding + (minWidth / 2 * length));
93 input.style.maxWidth = newMaxWidth + 'px';
104 * @module material.components.slider
107 * The `<md-slider>` component allows the user to choose from a range of
110 * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application)
111 * the slider is in the accent color by default. The primary color palette may be used with
112 * the `md-primary` class.
114 * It has two modes: 'normal' mode, where the user slides between a wide range
115 * of values, and 'discrete' mode, where the user slides between only a few
118 * To enable discrete mode, add the `md-discrete` attribute to a slider,
119 * and use the `step` attribute to change the distance between
120 * values the user is allowed to pick.
123 * <h4>Normal Mode</h4>
125 * <md-slider ng-model="myValue" min="5" max="500">
128 * <h4>Discrete Mode</h4>
130 * <md-slider md-discrete ng-model="myDiscreteValue" step="10" min="10" max="130">
133 * <h4>Invert Mode</h4>
135 * <md-slider md-invert ng-model="myValue" step="10" min="10" max="130">
139 * @param {boolean=} md-discrete Whether to enable discrete mode.
140 * @param {boolean=} md-invert Whether to enable invert mode.
141 * @param {number=} step The distance between values the user is allowed to pick. Default 1.
142 * @param {number=} min The minimum value the user is allowed to pick. Default 0.
143 * @param {number=} max The maximum value the user is allowed to pick. Default 100.
144 * @param {number=} round The amount of numbers after the decimal point, maximum is 6 to prevent scientific notation. Default 3.
146 function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdTheming, $mdGesture, $parse, $log, $timeout) {
149 require: ['?ngModel', '?^mdSliderContainer'],
151 '<div class="md-slider-wrapper">' +
152 '<div class="md-slider-content">' +
153 '<div class="md-track-container">' +
154 '<div class="md-track"></div>' +
155 '<div class="md-track md-track-fill"></div>' +
156 '<div class="md-track-ticks"></div>' +
158 '<div class="md-thumb-container">' +
159 '<div class="md-thumb"></div>' +
160 '<div class="md-focus-thumb"></div>' +
161 '<div class="md-focus-ring"></div>' +
162 '<div class="md-sign">' +
163 '<span class="md-thumb-text"></span>' +
165 '<div class="md-disabled-thumb"></div>' +
172 // **********************************************************
174 // **********************************************************
176 function compile (tElement, tAttrs) {
177 var wrapper = angular.element(tElement[0].getElementsByClassName('md-slider-wrapper'));
179 var tabIndex = tAttrs.tabindex || 0;
180 wrapper.attr('tabindex', tabIndex);
182 if (tAttrs.disabled || tAttrs.ngDisabled) wrapper.attr('tabindex', -1);
184 wrapper.attr('role', 'slider');
186 $mdAria.expect(tElement, 'aria-label');
191 function postLink(scope, element, attr, ctrls) {
193 var ngModelCtrl = ctrls[0] || {
194 // Mock ngModelController if it doesn't exist to give us
195 // the minimum functionality needed
196 $setViewValue: function(val) {
197 this.$viewValue = val;
198 this.$viewChangeListeners.forEach(function(cb) { cb(); });
202 $viewChangeListeners: []
205 var containerCtrl = ctrls[1];
206 var container = angular.element($mdUtil.getClosest(element, '_md-slider-container', true));
207 var isDisabled = attr.ngDisabled ? angular.bind(null, $parse(attr.ngDisabled), scope.$parent) : function () {
208 return element[0].hasAttribute('disabled');
211 var thumb = angular.element(element[0].querySelector('.md-thumb'));
212 var thumbText = angular.element(element[0].querySelector('.md-thumb-text'));
213 var thumbContainer = thumb.parent();
214 var trackContainer = angular.element(element[0].querySelector('.md-track-container'));
215 var activeTrack = angular.element(element[0].querySelector('.md-track-fill'));
216 var tickContainer = angular.element(element[0].querySelector('.md-track-ticks'));
217 var wrapper = angular.element(element[0].getElementsByClassName('md-slider-wrapper'));
218 var content = angular.element(element[0].getElementsByClassName('md-slider-content'));
219 var throttledRefreshDimensions = $mdUtil.throttle(refreshSliderDimensions, 5000);
221 // Default values, overridable by attrs
222 var DEFAULT_ROUND = 3;
223 var vertical = angular.isDefined(attr.mdVertical);
224 var discrete = angular.isDefined(attr.mdDiscrete);
225 var invert = angular.isDefined(attr.mdInvert);
226 angular.isDefined(attr.min) ? attr.$observe('min', updateMin) : updateMin(0);
227 angular.isDefined(attr.max) ? attr.$observe('max', updateMax) : updateMax(100);
228 angular.isDefined(attr.step)? attr.$observe('step', updateStep) : updateStep(1);
229 angular.isDefined(attr.round)? attr.$observe('round', updateRound) : updateRound(DEFAULT_ROUND);
231 // We have to manually stop the $watch on ngDisabled because it exists
232 // on the parent scope, and won't be automatically destroyed when
233 // the component is destroyed.
234 var stopDisabledWatch = angular.noop;
235 if (attr.ngDisabled) {
236 stopDisabledWatch = scope.$parent.$watch(attr.ngDisabled, updateAriaDisabled);
239 $mdGesture.register(wrapper, 'drag', { horizontal: !vertical });
241 scope.mouseActive = false;
244 .on('keydown', keydownListener)
245 .on('mousedown', mouseDownListener)
246 .on('focus', focusListener)
247 .on('blur', blurListener)
248 .on('$md.pressdown', onPressDown)
249 .on('$md.pressup', onPressUp)
250 .on('$md.dragstart', onDragStart)
251 .on('$md.drag', onDrag)
252 .on('$md.dragend', onDragEnd);
254 // On resize, recalculate the slider's dimensions and re-render
255 function updateAll() {
256 refreshSliderDimensions();
259 setTimeout(updateAll, 0);
261 var debouncedUpdateAll = $$rAF.throttle(updateAll);
262 angular.element($window).on('resize', debouncedUpdateAll);
264 scope.$on('$destroy', function() {
265 angular.element($window).off('resize', debouncedUpdateAll);
268 ngModelCtrl.$render = ngModelRender;
269 ngModelCtrl.$viewChangeListeners.push(ngModelRender);
270 ngModelCtrl.$formatters.push(minMaxValidator);
271 ngModelCtrl.$formatters.push(stepValidator);
280 function updateMin(value) {
281 min = parseFloat(value);
282 element.attr('aria-valuemin', value);
285 function updateMax(value) {
286 max = parseFloat(value);
287 element.attr('aria-valuemax', value);
290 function updateStep(value) {
291 step = parseFloat(value);
293 function updateRound(value) {
294 // Set max round digits to 6, after 6 the input uses scientific notation
295 round = minMaxValidator(parseInt(value), 0, 6);
297 function updateAriaDisabled() {
298 element.attr('aria-disabled', !!isDisabled());
301 // Draw the ticks with canvas.
302 // The alternative to drawing ticks with canvas is to draw one element for each tick,
303 // which could quickly become a performance bottleneck.
304 var tickCanvas, tickCtx;
305 function redrawTicks() {
306 if (!discrete || isDisabled()) return;
307 if ( angular.isUndefined(step) ) return;
310 var msg = 'Slider step value must be greater than zero when in discrete mode';
312 throw new Error(msg);
315 var numSteps = Math.floor( (max - min) / step );
317 tickCanvas = angular.element('<canvas>').css('position', 'absolute');
318 tickContainer.append(tickCanvas);
320 tickCtx = tickCanvas[0].getContext('2d');
323 var dimensions = getSliderDimensions();
325 // If `dimensions` doesn't have height and width it might be the first attempt so we will refresh dimensions
326 if (dimensions && !dimensions.height && !dimensions.width) {
327 refreshSliderDimensions();
328 dimensions = sliderDimensions;
331 tickCanvas[0].width = dimensions.width;
332 tickCanvas[0].height = dimensions.height;
335 for (var i = 0; i <= numSteps; i++) {
336 var trackTicksStyle = $window.getComputedStyle(tickContainer[0]);
337 tickCtx.fillStyle = trackTicksStyle.color || 'black';
339 distance = Math.floor((vertical ? dimensions.height : dimensions.width) * (i / numSteps));
341 tickCtx.fillRect(vertical ? 0 : distance - 1,
342 vertical ? distance - 1 : 0,
343 vertical ? dimensions.width : 2,
344 vertical ? 2 : dimensions.height);
348 function clearTicks() {
349 if(tickCanvas && tickCtx) {
350 var dimensions = getSliderDimensions();
351 tickCtx.clearRect(0, 0, dimensions.width, dimensions.height);
356 * Refreshing Dimensions
358 var sliderDimensions = {};
359 refreshSliderDimensions();
360 function refreshSliderDimensions() {
361 sliderDimensions = trackContainer[0].getBoundingClientRect();
363 function getSliderDimensions() {
364 throttledRefreshDimensions();
365 return sliderDimensions;
369 * left/right/up/down arrow listener
371 function keydownListener(ev) {
372 if (isDisabled()) return;
375 if (vertical ? ev.keyCode === $mdConstant.KEY_CODE.DOWN_ARROW : ev.keyCode === $mdConstant.KEY_CODE.LEFT_ARROW) {
376 changeAmount = -step;
377 } else if (vertical ? ev.keyCode === $mdConstant.KEY_CODE.UP_ARROW : ev.keyCode === $mdConstant.KEY_CODE.RIGHT_ARROW) {
380 changeAmount = invert ? -changeAmount : changeAmount;
382 if (ev.metaKey || ev.ctrlKey || ev.altKey) {
386 ev.stopPropagation();
387 scope.$evalAsync(function() {
388 setModelValue(ngModelCtrl.$viewValue + changeAmount);
393 function mouseDownListener() {
396 scope.mouseActive = true;
397 wrapper.removeClass('md-focused');
399 $timeout(function() {
400 scope.mouseActive = false;
404 function focusListener() {
405 if (scope.mouseActive === false) {
406 wrapper.addClass('md-focused');
410 function blurListener() {
411 wrapper.removeClass('md-focused');
412 element.removeClass('md-active');
417 * ngModel setters and validators
419 function setModelValue(value) {
420 ngModelCtrl.$setViewValue( minMaxValidator(stepValidator(value)) );
422 function ngModelRender() {
423 if (isNaN(ngModelCtrl.$viewValue)) {
424 ngModelCtrl.$viewValue = ngModelCtrl.$modelValue;
427 ngModelCtrl.$viewValue = minMaxValidator(ngModelCtrl.$viewValue);
429 var percent = valueToPercent(ngModelCtrl.$viewValue);
430 scope.modelValue = ngModelCtrl.$viewValue;
431 element.attr('aria-valuenow', ngModelCtrl.$viewValue);
432 setSliderPercent(percent);
433 thumbText.text( ngModelCtrl.$viewValue );
436 function minMaxValidator(value, minValue, maxValue) {
437 if (angular.isNumber(value)) {
438 minValue = angular.isNumber(minValue) ? minValue : min;
439 maxValue = angular.isNumber(maxValue) ? maxValue : max;
441 return Math.max(minValue, Math.min(maxValue, value));
445 function stepValidator(value) {
446 if (angular.isNumber(value)) {
447 var formattedValue = (Math.round((value - min) / step) * step + min);
448 formattedValue = (Math.round(formattedValue * Math.pow(10, round)) / Math.pow(10, round));
450 if (containerCtrl && containerCtrl.fitInputWidthToTextLength){
451 $mdUtil.debounce(function () {
452 containerCtrl.fitInputWidthToTextLength(formattedValue.toString().length);
456 return formattedValue;
463 function setSliderPercent(percent) {
465 percent = clamp(percent);
467 var thumbPosition = (percent * 100) + '%';
468 var activeTrackPercent = invert ? (1 - percent) * 100 + '%' : thumbPosition;
471 thumbContainer.css('bottom', thumbPosition);
474 $mdUtil.bidiProperty(thumbContainer, 'left', 'right', thumbPosition);
478 activeTrack.css(vertical ? 'height' : 'width', activeTrackPercent);
480 element.toggleClass((invert ? 'md-max' : 'md-min'), percent === 0);
481 element.toggleClass((invert ? 'md-min' : 'md-max'), percent === 1);
487 var isDragging = false;
489 function onPressDown(ev) {
490 if (isDisabled()) return;
492 element.addClass('md-active');
494 refreshSliderDimensions();
496 var exactVal = percentToValue( positionToPercent( vertical ? ev.pointer.y : ev.pointer.x ));
497 var closestVal = minMaxValidator( stepValidator(exactVal) );
498 scope.$apply(function() {
499 setModelValue( closestVal );
500 setSliderPercent( valueToPercent(closestVal));
503 function onPressUp(ev) {
504 if (isDisabled()) return;
506 element.removeClass('md-dragging');
508 var exactVal = percentToValue( positionToPercent( vertical ? ev.pointer.y : ev.pointer.x ));
509 var closestVal = minMaxValidator( stepValidator(exactVal) );
510 scope.$apply(function() {
511 setModelValue(closestVal);
515 function onDragStart(ev) {
516 if (isDisabled()) return;
519 ev.stopPropagation();
521 element.addClass('md-dragging');
522 setSliderFromEvent(ev);
524 function onDrag(ev) {
525 if (!isDragging) return;
526 ev.stopPropagation();
527 setSliderFromEvent(ev);
529 function onDragEnd(ev) {
530 if (!isDragging) return;
531 ev.stopPropagation();
535 function setSliderFromEvent(ev) {
536 // While panning discrete, update only the
537 // visual positioning but not the model value.
538 if ( discrete ) adjustThumbPosition( vertical ? ev.pointer.y : ev.pointer.x );
539 else doSlide( vertical ? ev.pointer.y : ev.pointer.x );
543 * Slide the UI by changing the model value
546 function doSlide( x ) {
547 scope.$evalAsync( function() {
548 setModelValue( percentToValue( positionToPercent(x) ));
553 * Slide the UI without changing the model (while dragging/panning)
556 function adjustThumbPosition( x ) {
557 var exactVal = percentToValue( positionToPercent( x ));
558 var closestVal = minMaxValidator( stepValidator(exactVal) );
559 setSliderPercent( positionToPercent(x) );
560 thumbText.text( closestVal );
564 * Clamps the value to be between 0 and 1.
565 * @param {number} value The value to clamp.
568 function clamp(value) {
569 return Math.max(0, Math.min(value || 0, 1));
573 * Convert position on slider to percentage value of offset from beginning...
577 function positionToPercent( position ) {
578 var offset = vertical ? sliderDimensions.top : sliderDimensions.left;
579 var size = vertical ? sliderDimensions.height : sliderDimensions.width;
580 var calc = (position - offset) / size;
582 if (!vertical && $mdUtil.bidi() === 'rtl') {
586 return Math.max(0, Math.min(1, vertical ? 1 - calc : calc));
590 * Convert percentage offset on slide to equivalent model value
594 function percentToValue( percent ) {
595 var adjustedPercent = invert ? (1 - percent) : percent;
596 return (min + adjustedPercent * (max - min));
599 function valueToPercent( val ) {
600 var percent = (val - min) / (max - min);
601 return invert ? (1 - percent) : percent;
606 ngmaterial.components.slider = angular.module("material.components.slider");