You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

605 lines
19 KiB

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