6a21de4ae3296f27130d3b941bdb0b4cf2df3cb9
[vnfsdk/refrepo.git] /
1 /*!
2  * Angular Material Design
3  * https://github.com/angular/material
4  * @license MIT
5  * v1.1.3
6  */
7 goog.provide('ngmaterial.components.slider');
8 goog.require('ngmaterial.core');
9   /**
10    * @ngdoc module
11    * @name material.components.slider
12    */
13 SliderDirective['$inject'] = ["$$rAF", "$window", "$mdAria", "$mdUtil", "$mdConstant", "$mdTheming", "$mdGesture", "$parse", "$log", "$timeout"];
14   angular.module('material.components.slider', [
15     'material.core'
16   ])
17   .directive('mdSlider', SliderDirective)
18   .directive('mdSliderContainer', SliderContainerDirective);
19
20 /**
21  * @ngdoc directive
22  * @name mdSliderContainer
23  * @module material.components.slider
24  * @restrict E
25  * @description
26  * The `<md-slider-container>` contains slider with two other elements.
27  *
28  *
29  * @usage
30  * <h4>Normal Mode</h4>
31  * <hljs lang="html">
32  * </hljs>
33  */
34 function SliderContainerDirective() {
35   return {
36     controller: function () {},
37     compile: function (elem) {
38       var slider = elem.find('md-slider');
39
40       if (!slider) {
41         return;
42       }
43
44       var vertical = slider.attr('md-vertical');
45
46       if (vertical !== undefined) {
47         elem.attr('md-vertical', '');
48       }
49
50       if(!slider.attr('flex')) {
51         slider.attr('flex', '');
52       }
53
54       return function postLink(scope, element, attr, ctrl) {
55         element.addClass('_md');     // private md component indicator for styling
56
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);
63         }
64
65         var stopDisabledWatch = angular.noop;
66
67         if (attr.disabled) {
68           setDisable(true);
69         }
70         else if (attr.ngDisabled) {
71           stopDisabledWatch = scope.$watch(attr.ngDisabled, function (value) {
72             setDisable(value);
73           });
74         }
75
76         scope.$on('$destroy', function () {
77           stopDisabledWatch();
78         });
79
80         var initialMaxWidth;
81
82         ctrl.fitInputWidthToTextLength = function (length) {
83           var input = element[0].querySelector('md-input-container');
84
85           if (input) {
86             var computedStyle = getComputedStyle(input);
87             var minWidth = parseInt(computedStyle.minWidth);
88             var padding = parseInt(computedStyle.padding) * 2;
89
90             initialMaxWidth = initialMaxWidth || parseInt(computedStyle.maxWidth);
91             var newMaxWidth = Math.max(initialMaxWidth, minWidth + padding + (minWidth / 2 * length));
92
93             input.style.maxWidth = newMaxWidth + 'px';
94           }
95         };
96       };
97     }
98   };
99 }
100
101 /**
102  * @ngdoc directive
103  * @name mdSlider
104  * @module material.components.slider
105  * @restrict E
106  * @description
107  * The `<md-slider>` component allows the user to choose from a range of
108  * values.
109  *
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.
113  *
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
116  * select values.
117  *
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.
121  *
122  * @usage
123  * <h4>Normal Mode</h4>
124  * <hljs lang="html">
125  * <md-slider ng-model="myValue" min="5" max="500">
126  * </md-slider>
127  * </hljs>
128  * <h4>Discrete Mode</h4>
129  * <hljs lang="html">
130  * <md-slider md-discrete ng-model="myDiscreteValue" step="10" min="10" max="130">
131  * </md-slider>
132  * </hljs>
133  * <h4>Invert Mode</h4>
134  * <hljs lang="html">
135  * <md-slider md-invert ng-model="myValue" step="10" min="10" max="130">
136  * </md-slider>
137  * </hljs>
138  *
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.
145  */
146 function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdTheming, $mdGesture, $parse, $log, $timeout) {
147   return {
148     scope: {},
149     require: ['?ngModel', '?^mdSliderContainer'],
150     template:
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>' +
157           '</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>' +
164             '</div>' +
165             '<div class="md-disabled-thumb"></div>' +
166           '</div>' +
167         '</div>' +
168       '</div>',
169     compile: compile
170   };
171
172   // **********************************************************
173   // Private Methods
174   // **********************************************************
175
176   function compile (tElement, tAttrs) {
177     var wrapper = angular.element(tElement[0].getElementsByClassName('md-slider-wrapper'));
178
179     var tabIndex = tAttrs.tabindex || 0;
180     wrapper.attr('tabindex', tabIndex);
181
182     if (tAttrs.disabled || tAttrs.ngDisabled) wrapper.attr('tabindex', -1);
183
184     wrapper.attr('role', 'slider');
185
186     $mdAria.expect(tElement, 'aria-label');
187
188     return postLink;
189   }
190
191   function postLink(scope, element, attr, ctrls) {
192     $mdTheming(element);
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(); });
199       },
200       $parsers: [],
201       $formatters: [],
202       $viewChangeListeners: []
203     };
204
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');
209         };
210
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);
220
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);
230
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);
237     }
238
239     $mdGesture.register(wrapper, 'drag', { horizontal: !vertical });
240
241     scope.mouseActive = false;
242
243     wrapper
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);
253
254     // On resize, recalculate the slider's dimensions and re-render
255     function updateAll() {
256       refreshSliderDimensions();
257       ngModelRender();
258     }
259     setTimeout(updateAll, 0);
260
261     var debouncedUpdateAll = $$rAF.throttle(updateAll);
262     angular.element($window).on('resize', debouncedUpdateAll);
263
264     scope.$on('$destroy', function() {
265       angular.element($window).off('resize', debouncedUpdateAll);
266     });
267
268     ngModelCtrl.$render = ngModelRender;
269     ngModelCtrl.$viewChangeListeners.push(ngModelRender);
270     ngModelCtrl.$formatters.push(minMaxValidator);
271     ngModelCtrl.$formatters.push(stepValidator);
272
273     /**
274      * Attributes
275      */
276     var min;
277     var max;
278     var step;
279     var round;
280     function updateMin(value) {
281       min = parseFloat(value);
282       element.attr('aria-valuemin', value);
283       updateAll();
284     }
285     function updateMax(value) {
286       max = parseFloat(value);
287       element.attr('aria-valuemax', value);
288       updateAll();
289     }
290     function updateStep(value) {
291       step = parseFloat(value);
292     }
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);
296     }
297     function updateAriaDisabled() {
298       element.attr('aria-disabled', !!isDisabled());
299     }
300
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;
308
309       if ( step <= 0 ) {
310         var msg = 'Slider step value must be greater than zero when in discrete mode';
311         $log.error(msg);
312         throw new Error(msg);
313       }
314
315       var numSteps = Math.floor( (max - min) / step );
316       if (!tickCanvas) {
317         tickCanvas = angular.element('<canvas>').css('position', 'absolute');
318         tickContainer.append(tickCanvas);
319
320         tickCtx = tickCanvas[0].getContext('2d');
321       }
322
323       var dimensions = getSliderDimensions();
324
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;
329       }
330
331       tickCanvas[0].width = dimensions.width;
332       tickCanvas[0].height = dimensions.height;
333
334       var distance;
335       for (var i = 0; i <= numSteps; i++) {
336         var trackTicksStyle = $window.getComputedStyle(tickContainer[0]);
337         tickCtx.fillStyle = trackTicksStyle.color || 'black';
338
339         distance = Math.floor((vertical ? dimensions.height : dimensions.width) * (i / numSteps));
340
341         tickCtx.fillRect(vertical ? 0 : distance - 1,
342           vertical ? distance - 1 : 0,
343           vertical ? dimensions.width : 2,
344           vertical ? 2 : dimensions.height);
345       }
346     }
347
348     function clearTicks() {
349       if(tickCanvas && tickCtx) {
350         var dimensions = getSliderDimensions();
351         tickCtx.clearRect(0, 0, dimensions.width, dimensions.height);
352       }
353     }
354
355     /**
356      * Refreshing Dimensions
357      */
358     var sliderDimensions = {};
359     refreshSliderDimensions();
360     function refreshSliderDimensions() {
361       sliderDimensions = trackContainer[0].getBoundingClientRect();
362     }
363     function getSliderDimensions() {
364       throttledRefreshDimensions();
365       return sliderDimensions;
366     }
367
368     /**
369      * left/right/up/down arrow listener
370      */
371     function keydownListener(ev) {
372       if (isDisabled()) return;
373
374       var changeAmount;
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) {
378         changeAmount = step;
379       }
380       changeAmount = invert ? -changeAmount : changeAmount;
381       if (changeAmount) {
382         if (ev.metaKey || ev.ctrlKey || ev.altKey) {
383           changeAmount *= 4;
384         }
385         ev.preventDefault();
386         ev.stopPropagation();
387         scope.$evalAsync(function() {
388           setModelValue(ngModelCtrl.$viewValue + changeAmount);
389         });
390       }
391     }
392
393     function mouseDownListener() {
394       redrawTicks();
395
396       scope.mouseActive = true;
397       wrapper.removeClass('md-focused');
398
399       $timeout(function() {
400         scope.mouseActive = false;
401       }, 100);
402     }
403
404     function focusListener() {
405       if (scope.mouseActive === false) {
406         wrapper.addClass('md-focused');
407       }
408     }
409
410     function blurListener() {
411       wrapper.removeClass('md-focused');
412       element.removeClass('md-active');
413       clearTicks();
414     }
415
416     /**
417      * ngModel setters and validators
418      */
419     function setModelValue(value) {
420       ngModelCtrl.$setViewValue( minMaxValidator(stepValidator(value)) );
421     }
422     function ngModelRender() {
423       if (isNaN(ngModelCtrl.$viewValue)) {
424         ngModelCtrl.$viewValue = ngModelCtrl.$modelValue;
425       }
426
427       ngModelCtrl.$viewValue = minMaxValidator(ngModelCtrl.$viewValue);
428
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 );
434     }
435
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;
440
441         return Math.max(minValue, Math.min(maxValue, value));
442       }
443     }
444
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));
449
450         if (containerCtrl && containerCtrl.fitInputWidthToTextLength){
451           $mdUtil.debounce(function () {
452             containerCtrl.fitInputWidthToTextLength(formattedValue.toString().length);
453           }, 100)();
454         }
455
456         return formattedValue;
457       }
458     }
459
460     /**
461      * @param percent 0-1
462      */
463     function setSliderPercent(percent) {
464
465       percent = clamp(percent);
466
467       var thumbPosition = (percent * 100) + '%';
468       var activeTrackPercent = invert ? (1 - percent) * 100 + '%' : thumbPosition;
469
470       if (vertical) {
471         thumbContainer.css('bottom', thumbPosition);
472       }
473       else {
474         $mdUtil.bidiProperty(thumbContainer, 'left', 'right', thumbPosition);
475       }
476
477       
478       activeTrack.css(vertical ? 'height' : 'width', activeTrackPercent);
479
480       element.toggleClass((invert ? 'md-max' : 'md-min'), percent === 0);
481       element.toggleClass((invert ? 'md-min' : 'md-max'), percent === 1);
482     }
483
484     /**
485      * Slide listeners
486      */
487     var isDragging = false;
488
489     function onPressDown(ev) {
490       if (isDisabled()) return;
491
492       element.addClass('md-active');
493       element[0].focus();
494       refreshSliderDimensions();
495
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));
501       });
502     }
503     function onPressUp(ev) {
504       if (isDisabled()) return;
505
506       element.removeClass('md-dragging');
507
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);
512         ngModelRender();
513       });
514     }
515     function onDragStart(ev) {
516       if (isDisabled()) return;
517       isDragging = true;
518
519       ev.stopPropagation();
520
521       element.addClass('md-dragging');
522       setSliderFromEvent(ev);
523     }
524     function onDrag(ev) {
525       if (!isDragging) return;
526       ev.stopPropagation();
527       setSliderFromEvent(ev);
528     }
529     function onDragEnd(ev) {
530       if (!isDragging) return;
531       ev.stopPropagation();
532       isDragging = false;
533     }
534
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 );
540     }
541
542     /**
543      * Slide the UI by changing the model value
544      * @param x
545      */
546     function doSlide( x ) {
547       scope.$evalAsync( function() {
548         setModelValue( percentToValue( positionToPercent(x) ));
549       });
550     }
551
552     /**
553      * Slide the UI without changing the model (while dragging/panning)
554      * @param x
555      */
556     function adjustThumbPosition( x ) {
557       var exactVal = percentToValue( positionToPercent( x ));
558       var closestVal = minMaxValidator( stepValidator(exactVal) );
559       setSliderPercent( positionToPercent(x) );
560       thumbText.text( closestVal );
561     }
562
563     /**
564     * Clamps the value to be between 0 and 1.
565     * @param {number} value The value to clamp.
566     * @returns {number}
567     */
568     function clamp(value) {
569       return Math.max(0, Math.min(value || 0, 1));
570     }
571
572     /**
573      * Convert position on slider to percentage value of offset from beginning...
574      * @param position
575      * @returns {number}
576      */
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;
581
582       if (!vertical && $mdUtil.bidi() === 'rtl') {
583         calc = 1 - calc;
584       }
585
586       return Math.max(0, Math.min(1, vertical ? 1 - calc : calc));
587     }
588
589     /**
590      * Convert percentage offset on slide to equivalent model value
591      * @param percent
592      * @returns {*}
593      */
594     function percentToValue( percent ) {
595       var adjustedPercent = invert ? (1 - percent) : percent;
596       return (min + adjustedPercent * (max - min));
597     }
598
599     function valueToPercent( val ) {
600       var percent = (val - min) / (max - min);
601       return invert ? (1 - percent) : percent;
602     }
603   }
604 }
605
606 ngmaterial.components.slider = angular.module("material.components.slider");