commonroutesApp/www/lib/angular-material/modules/js/slider/slider.js
www/lib/angular-material/modules/js/slider/slider.jsRaw
1/*!
2 * Angular Material Design
3 * https://github.com/angular/material
4 * @license MIT
5 * v1.1.1
6 */
7(function( window, angular, undefined ){
8"use strict";
9
10 /**
11 * @ngdoc module
12 * @name material.components.slider
13 */
14SliderDirective.$inject = ["$$rAF", "$window", "$mdAria", "$mdUtil", "$mdConstant", "$mdTheming", "$mdGesture", "$parse", "$log", "$timeout"];
15 angular.module('material.components.slider', [
16 'material.core'
17 ])
18 .directive('mdSlider', SliderDirective)
19 .directive('mdSliderContainer', SliderContainerDirective);
20
21/**
22 * @ngdoc directive
23 * @name mdSliderContainer
24 * @module material.components.slider
25 * @restrict E
26 * @description
27 * The `<md-slider-container>` contains slider with two other elements.
28 *
29 *
30 * @usage
31 * <h4>Normal Mode</h4>
32 * <hljs lang="html">
33 * </hljs>
34 */
35function SliderContainerDirective() {
36 return {
37 controller: function () {},
38 compile: function (elem) {
39 var slider = elem.find('md-slider');
40
41 if (!slider) {
42 return;
43 }
44
45 var vertical = slider.attr('md-vertical');
46
47 if (vertical !== undefined) {
48 elem.attr('md-vertical', '');
49 }
50
51 if(!slider.attr('flex')) {
52 slider.attr('flex', '');
53 }
54
55 return function postLink(scope, element, attr, ctrl) {
56 element.addClass('_md'); // private md component indicator for styling
57
58 // We have to manually stop the $watch on ngDisabled because it exists
59 // on the parent scope, and won't be automatically destroyed when
60 // the component is destroyed.
61 function setDisable(value) {
62 element.children().attr('disabled', value);
63 element.find('input').attr('disabled', value);
64 }
65
66 var stopDisabledWatch = angular.noop;
67
68 if (attr.disabled) {
69 setDisable(true);
70 }
71 else if (attr.ngDisabled) {
72 stopDisabledWatch = scope.$watch(attr.ngDisabled, function (value) {
73 setDisable(value);
74 });
75 }
76
77 scope.$on('$destroy', function () {
78 stopDisabledWatch();
79 });
80
81 var initialMaxWidth;
82
83 ctrl.fitInputWidthToTextLength = function (length) {
84 var input = element[0].querySelector('md-input-container');
85
86 if (input) {
87 var computedStyle = getComputedStyle(input);
88 var minWidth = parseInt(computedStyle.minWidth);
89 var padding = parseInt(computedStyle.padding) * 2;
90
91 initialMaxWidth = initialMaxWidth || parseInt(computedStyle.maxWidth);
92 var newMaxWidth = Math.max(initialMaxWidth, minWidth + padding + (minWidth / 2 * length));
93
94 input.style.maxWidth = newMaxWidth + 'px';
95 }
96 };
97 };
98 }
99 };
100}
101
102/**
103 * @ngdoc directive
104 * @name mdSlider
105 * @module material.components.slider
106 * @restrict E
107 * @description
108 * The `<md-slider>` component allows the user to choose from a range of
109 * values.
110 *
111 * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application)
112 * the slider is in the accent color by default. The primary color palette may be used with
113 * the `md-primary` class.
114 *
115 * It has two modes: 'normal' mode, where the user slides between a wide range
116 * of values, and 'discrete' mode, where the user slides between only a few
117 * select values.
118 *
119 * To enable discrete mode, add the `md-discrete` attribute to a slider,
120 * and use the `step` attribute to change the distance between
121 * values the user is allowed to pick.
122 *
123 * @usage
124 * <h4>Normal Mode</h4>
125 * <hljs lang="html">
126 * <md-slider ng-model="myValue" min="5" max="500">
127 * </md-slider>
128 * </hljs>
129 * <h4>Discrete Mode</h4>
130 * <hljs lang="html">
131 * <md-slider md-discrete ng-model="myDiscreteValue" step="10" min="10" max="130">
132 * </md-slider>
133 * </hljs>
134 * <h4>Invert Mode</h4>
135 * <hljs lang="html">
136 * <md-slider md-invert ng-model="myValue" step="10" min="10" max="130">
137 * </md-slider>
138 * </hljs>
139 *
140 * @param {boolean=} md-discrete Whether to enable discrete mode.
141 * @param {boolean=} md-invert Whether to enable invert mode.
142 * @param {number=} step The distance between values the user is allowed to pick. Default 1.
143 * @param {number=} min The minimum value the user is allowed to pick. Default 0.
144 * @param {number=} max The maximum value the user is allowed to pick. Default 100.
145 * @param {number=} round The amount of numbers after the decimal point, maximum is 6 to prevent scientific notation. Default 3.
146 */
147function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdTheming, $mdGesture, $parse, $log, $timeout) {
148 return {
149 scope: {},
150 require: ['?ngModel', '?^mdSliderContainer'],
151 template:
152 '<div class="md-slider-wrapper">' +
153 '<div class="md-slider-content">' +
154 '<div class="md-track-container">' +
155 '<div class="md-track"></div>' +
156 '<div class="md-track md-track-fill"></div>' +
157 '<div class="md-track-ticks"></div>' +
158 '</div>' +
159 '<div class="md-thumb-container">' +
160 '<div class="md-thumb"></div>' +
161 '<div class="md-focus-thumb"></div>' +
162 '<div class="md-focus-ring"></div>' +
163 '<div class="md-sign">' +
164 '<span class="md-thumb-text"></span>' +
165 '</div>' +
166 '<div class="md-disabled-thumb"></div>' +
167 '</div>' +
168 '</div>' +
169 '</div>',
170 compile: compile
171 };
172
173 // **********************************************************
174 // Private Methods
175 // **********************************************************
176
177 function compile (tElement, tAttrs) {
178 var wrapper = angular.element(tElement[0].getElementsByClassName('md-slider-wrapper'));
179
180 var tabIndex = tAttrs.tabindex || 0;
181 wrapper.attr('tabindex', tabIndex);
182
183 if (tAttrs.disabled || tAttrs.ngDisabled) wrapper.attr('tabindex', -1);
184
185 wrapper.attr('role', 'slider');
186
187 $mdAria.expect(tElement, 'aria-label');
188
189 return postLink;
190 }
191
192 function postLink(scope, element, attr, ctrls) {
193 $mdTheming(element);
194 var ngModelCtrl = ctrls[0] || {
195 // Mock ngModelController if it doesn't exist to give us
196 // the minimum functionality needed
197 $setViewValue: function(val) {
198 this.$viewValue = val;
199 this.$viewChangeListeners.forEach(function(cb) { cb(); });
200 },
201 $parsers: [],
202 $formatters: [],
203 $viewChangeListeners: []
204 };
205
206 var containerCtrl = ctrls[1];
207 var container = angular.element($mdUtil.getClosest(element, '_md-slider-container', true));
208 var isDisabled = attr.ngDisabled ? angular.bind(null, $parse(attr.ngDisabled), scope.$parent) : function () {
209 return element[0].hasAttribute('disabled');
210 };
211
212 var thumb = angular.element(element[0].querySelector('.md-thumb'));
213 var thumbText = angular.element(element[0].querySelector('.md-thumb-text'));
214 var thumbContainer = thumb.parent();
215 var trackContainer = angular.element(element[0].querySelector('.md-track-container'));
216 var activeTrack = angular.element(element[0].querySelector('.md-track-fill'));
217 var tickContainer = angular.element(element[0].querySelector('.md-track-ticks'));
218 var wrapper = angular.element(element[0].getElementsByClassName('md-slider-wrapper'));
219 var content = angular.element(element[0].getElementsByClassName('md-slider-content'));
220 var throttledRefreshDimensions = $mdUtil.throttle(refreshSliderDimensions, 5000);
221
222 // Default values, overridable by attrs
223 var DEFAULT_ROUND = 3;
224 var vertical = angular.isDefined(attr.mdVertical);
225 var discrete = angular.isDefined(attr.mdDiscrete);
226 var invert = angular.isDefined(attr.mdInvert);
227 angular.isDefined(attr.min) ? attr.$observe('min', updateMin) : updateMin(0);
228 angular.isDefined(attr.max) ? attr.$observe('max', updateMax) : updateMax(100);
229 angular.isDefined(attr.step)? attr.$observe('step', updateStep) : updateStep(1);
230 angular.isDefined(attr.round)? attr.$observe('round', updateRound) : updateRound(DEFAULT_ROUND);
231
232 // We have to manually stop the $watch on ngDisabled because it exists
233 // on the parent scope, and won't be automatically destroyed when
234 // the component is destroyed.
235 var stopDisabledWatch = angular.noop;
236 if (attr.ngDisabled) {
237 stopDisabledWatch = scope.$parent.$watch(attr.ngDisabled, updateAriaDisabled);
238 }
239
240 $mdGesture.register(wrapper, 'drag', { horizontal: !vertical });
241
242 scope.mouseActive = false;
243
244 wrapper
245 .on('keydown', keydownListener)
246 .on('mousedown', mouseDownListener)
247 .on('focus', focusListener)
248 .on('blur', blurListener)
249 .on('$md.pressdown', onPressDown)
250 .on('$md.pressup', onPressUp)
251 .on('$md.dragstart', onDragStart)
252 .on('$md.drag', onDrag)
253 .on('$md.dragend', onDragEnd);
254
255 // On resize, recalculate the slider's dimensions and re-render
256 function updateAll() {
257 refreshSliderDimensions();
258 ngModelRender();
259 }
260 setTimeout(updateAll, 0);
261
262 var debouncedUpdateAll = $$rAF.throttle(updateAll);
263 angular.element($window).on('resize', debouncedUpdateAll);
264
265 scope.$on('$destroy', function() {
266 angular.element($window).off('resize', debouncedUpdateAll);
267 });
268
269 ngModelCtrl.$render = ngModelRender;
270 ngModelCtrl.$viewChangeListeners.push(ngModelRender);
271 ngModelCtrl.$formatters.push(minMaxValidator);
272 ngModelCtrl.$formatters.push(stepValidator);
273
274 /**
275 * Attributes
276 */
277 var min;
278 var max;
279 var step;
280 var round;
281 function updateMin(value) {
282 min = parseFloat(value);
283 element.attr('aria-valuemin', value);
284 updateAll();
285 }
286 function updateMax(value) {
287 max = parseFloat(value);
288 element.attr('aria-valuemax', value);
289 updateAll();
290 }
291 function updateStep(value) {
292 step = parseFloat(value);
293 }
294 function updateRound(value) {
295 // Set max round digits to 6, after 6 the input uses scientific notation
296 round = minMaxValidator(parseInt(value), 0, 6);
297 }
298 function updateAriaDisabled() {
299 element.attr('aria-disabled', !!isDisabled());
300 }
301
302 // Draw the ticks with canvas.
303 // The alternative to drawing ticks with canvas is to draw one element for each tick,
304 // which could quickly become a performance bottleneck.
305 var tickCanvas, tickCtx;
306 function redrawTicks() {
307 if (!discrete || isDisabled()) return;
308 if ( angular.isUndefined(step) ) return;
309
310 if ( step <= 0 ) {
311 var msg = 'Slider step value must be greater than zero when in discrete mode';
312 $log.error(msg);
313 throw new Error(msg);
314 }
315
316 var numSteps = Math.floor( (max - min) / step );
317 if (!tickCanvas) {
318 tickCanvas = angular.element('<canvas>').css('position', 'absolute');
319 tickContainer.append(tickCanvas);
320
321 tickCtx = tickCanvas[0].getContext('2d');
322 }
323
324 var dimensions = getSliderDimensions();
325
326 // If `dimensions` doesn't have height and width it might be the first attempt so we will refresh dimensions
327 if (dimensions && !dimensions.height && !dimensions.width) {
328 refreshSliderDimensions();
329 dimensions = sliderDimensions;
330 }
331
332 tickCanvas[0].width = dimensions.width;
333 tickCanvas[0].height = dimensions.height;
334
335 var distance;
336 for (var i = 0; i <= numSteps; i++) {
337 var trackTicksStyle = $window.getComputedStyle(tickContainer[0]);
338 tickCtx.fillStyle = trackTicksStyle.color || 'black';
339
340 distance = Math.floor((vertical ? dimensions.height : dimensions.width) * (i / numSteps));
341
342 tickCtx.fillRect(vertical ? 0 : distance - 1,
343 vertical ? distance - 1 : 0,
344 vertical ? dimensions.width : 2,
345 vertical ? 2 : dimensions.height);
346 }
347 }
348
349 function clearTicks() {
350 if(tickCanvas && tickCtx) {
351 var dimensions = getSliderDimensions();
352 tickCtx.clearRect(0, 0, dimensions.width, dimensions.height);
353 }
354 }
355
356 /**
357 * Refreshing Dimensions
358 */
359 var sliderDimensions = {};
360 refreshSliderDimensions();
361 function refreshSliderDimensions() {
362 sliderDimensions = trackContainer[0].getBoundingClientRect();
363 }
364 function getSliderDimensions() {
365 throttledRefreshDimensions();
366 return sliderDimensions;
367 }
368
369 /**
370 * left/right/up/down arrow listener
371 */
372 function keydownListener(ev) {
373 if (isDisabled()) return;
374
375 var changeAmount;
376 if (vertical ? ev.keyCode === $mdConstant.KEY_CODE.DOWN_ARROW : ev.keyCode === $mdConstant.KEY_CODE.LEFT_ARROW) {
377 changeAmount = -step;
378 } else if (vertical ? ev.keyCode === $mdConstant.KEY_CODE.UP_ARROW : ev.keyCode === $mdConstant.KEY_CODE.RIGHT_ARROW) {
379 changeAmount = step;
380 }
381 changeAmount = invert ? -changeAmount : changeAmount;
382 if (changeAmount) {
383 if (ev.metaKey || ev.ctrlKey || ev.altKey) {
384 changeAmount *= 4;
385 }
386 ev.preventDefault();
387 ev.stopPropagation();
388 scope.$evalAsync(function() {
389 setModelValue(ngModelCtrl.$viewValue + changeAmount);
390 });
391 }
392 }
393
394 function mouseDownListener() {
395 redrawTicks();
396
397 scope.mouseActive = true;
398 wrapper.removeClass('md-focused');
399
400 $timeout(function() {
401 scope.mouseActive = false;
402 }, 100);
403 }
404
405 function focusListener() {
406 if (scope.mouseActive === false) {
407 wrapper.addClass('md-focused');
408 }
409 }
410
411 function blurListener() {
412 wrapper.removeClass('md-focused');
413 element.removeClass('md-active');
414 clearTicks();
415 }
416
417 /**
418 * ngModel setters and validators
419 */
420 function setModelValue(value) {
421 ngModelCtrl.$setViewValue( minMaxValidator(stepValidator(value)) );
422 }
423 function ngModelRender() {
424 if (isNaN(ngModelCtrl.$viewValue)) {
425 ngModelCtrl.$viewValue = ngModelCtrl.$modelValue;
426 }
427
428 ngModelCtrl.$viewValue = minMaxValidator(ngModelCtrl.$viewValue);
429
430 var percent = valueToPercent(ngModelCtrl.$viewValue);
431 scope.modelValue = ngModelCtrl.$viewValue;
432 element.attr('aria-valuenow', ngModelCtrl.$viewValue);
433 setSliderPercent(percent);
434 thumbText.text( ngModelCtrl.$viewValue );
435 }
436
437 function minMaxValidator(value, minValue, maxValue) {
438 if (angular.isNumber(value)) {
439 minValue = angular.isNumber(minValue) ? minValue : min;
440 maxValue = angular.isNumber(maxValue) ? maxValue : max;
441
442 return Math.max(minValue, Math.min(maxValue, value));
443 }
444 }
445
446 function stepValidator(value) {
447 if (angular.isNumber(value)) {
448 var formattedValue = (Math.round((value - min) / step) * step + min);
449 formattedValue = (Math.round(formattedValue * Math.pow(10, round)) / Math.pow(10, round));
450
451 if (containerCtrl && containerCtrl.fitInputWidthToTextLength){
452 $mdUtil.debounce(function () {
453 containerCtrl.fitInputWidthToTextLength(formattedValue.toString().length);
454 }, 100)();
455 }
456
457 return formattedValue;
458 }
459 }
460
461 /**
462 * @param percent 0-1
463 */
464 function setSliderPercent(percent) {
465
466 percent = clamp(percent);
467
468 var thumbPosition = (percent * 100) + '%';
469 var activeTrackPercent = invert ? (1 - percent) * 100 + '%' : thumbPosition;
470
471 if (vertical) {
472 thumbContainer.css('bottom', thumbPosition);
473 }
474 else {
475 $mdUtil.bidiProperty(thumbContainer, 'left', 'right', thumbPosition);
476 }
477
478
479 activeTrack.css(vertical ? 'height' : 'width', activeTrackPercent);
480
481 element.toggleClass((invert ? 'md-max' : 'md-min'), percent === 0);
482 element.toggleClass((invert ? 'md-min' : 'md-max'), percent === 1);
483 }
484
485 /**
486 * Slide listeners
487 */
488 var isDragging = false;
489
490 function onPressDown(ev) {
491 if (isDisabled()) return;
492
493 element.addClass('md-active');
494 element[0].focus();
495 refreshSliderDimensions();
496
497 var exactVal = percentToValue( positionToPercent( vertical ? ev.pointer.y : ev.pointer.x ));
498 var closestVal = minMaxValidator( stepValidator(exactVal) );
499 scope.$apply(function() {
500 setModelValue( closestVal );
501 setSliderPercent( valueToPercent(closestVal));
502 });
503 }
504 function onPressUp(ev) {
505 if (isDisabled()) return;
506
507 element.removeClass('md-dragging');
508
509 var exactVal = percentToValue( positionToPercent( vertical ? ev.pointer.y : ev.pointer.x ));
510 var closestVal = minMaxValidator( stepValidator(exactVal) );
511 scope.$apply(function() {
512 setModelValue(closestVal);
513 ngModelRender();
514 });
515 }
516 function onDragStart(ev) {
517 if (isDisabled()) return;
518 isDragging = true;
519
520 ev.stopPropagation();
521
522 element.addClass('md-dragging');
523 setSliderFromEvent(ev);
524 }
525 function onDrag(ev) {
526 if (!isDragging) return;
527 ev.stopPropagation();
528 setSliderFromEvent(ev);
529 }
530 function onDragEnd(ev) {
531 if (!isDragging) return;
532 ev.stopPropagation();
533 isDragging = false;
534 }
535
536 function setSliderFromEvent(ev) {
537 // While panning discrete, update only the
538 // visual positioning but not the model value.
539 if ( discrete ) adjustThumbPosition( vertical ? ev.pointer.y : ev.pointer.x );
540 else doSlide( vertical ? ev.pointer.y : ev.pointer.x );
541 }
542
543 /**
544 * Slide the UI by changing the model value
545 * @param x
546 */
547 function doSlide( x ) {
548 scope.$evalAsync( function() {
549 setModelValue( percentToValue( positionToPercent(x) ));
550 });
551 }
552
553 /**
554 * Slide the UI without changing the model (while dragging/panning)
555 * @param x
556 */
557 function adjustThumbPosition( x ) {
558 var exactVal = percentToValue( positionToPercent( x ));
559 var closestVal = minMaxValidator( stepValidator(exactVal) );
560 setSliderPercent( positionToPercent(x) );
561 thumbText.text( closestVal );
562 }
563
564 /**
565 * Clamps the value to be between 0 and 1.
566 * @param {number} value The value to clamp.
567 * @returns {number}
568 */
569 function clamp(value) {
570 return Math.max(0, Math.min(value || 0, 1));
571 }
572
573 /**
574 * Convert position on slider to percentage value of offset from beginning...
575 * @param position
576 * @returns {number}
577 */
578 function positionToPercent( position ) {
579 var offset = vertical ? sliderDimensions.top : sliderDimensions.left;
580 var size = vertical ? sliderDimensions.height : sliderDimensions.width;
581 var calc = (position - offset) / size;
582
583 if (!vertical && $mdUtil.bidi() === 'rtl') {
584 calc = 1 - calc;
585 }
586
587 return Math.max(0, Math.min(1, vertical ? 1 - calc : calc));
588 }
589
590 /**
591 * Convert percentage offset on slide to equivalent model value
592 * @param percent
593 * @returns {*}
594 */
595 function percentToValue( percent ) {
596 var adjustedPercent = invert ? (1 - percent) : percent;
597 return (min + adjustedPercent * (max - min));
598 }
599
600 function valueToPercent( val ) {
601 var percent = (val - min) / (max - min);
602 return invert ? (1 - percent) : percent;
603 }
604 }
605}
606
607})(window, window.angular);