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");