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