slider.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. /*!
  2. * Angular Material Design
  3. * https://github.com/angular/material
  4. * @license MIT
  5. * v0.11.4
  6. */
  7. (function( window, angular, undefined ){
  8. "use strict";
  9. /**
  10. * @ngdoc module
  11. * @name material.components.slider
  12. */
  13. angular.module('material.components.slider', [
  14. 'material.core'
  15. ])
  16. .directive('mdSlider', SliderDirective);
  17. /**
  18. * @ngdoc directive
  19. * @name mdSlider
  20. * @module material.components.slider
  21. * @restrict E
  22. * @description
  23. * The `<md-slider>` component allows the user to choose from a range of
  24. * values.
  25. *
  26. * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application)
  27. * the slider is in the accent color by default. The primary color palette may be used with
  28. * the `md-primary` class.
  29. *
  30. * It has two modes: 'normal' mode, where the user slides between a wide range
  31. * of values, and 'discrete' mode, where the user slides between only a few
  32. * select values.
  33. *
  34. * To enable discrete mode, add the `md-discrete` attribute to a slider,
  35. * and use the `step` attribute to change the distance between
  36. * values the user is allowed to pick.
  37. *
  38. * @usage
  39. * <h4>Normal Mode</h4>
  40. * <hljs lang="html">
  41. * <md-slider ng-model="myValue" min="5" max="500">
  42. * </md-slider>
  43. * </hljs>
  44. * <h4>Discrete Mode</h4>
  45. * <hljs lang="html">
  46. * <md-slider md-discrete ng-model="myDiscreteValue" step="10" min="10" max="130">
  47. * </md-slider>
  48. * </hljs>
  49. *
  50. * @param {boolean=} md-discrete Whether to enable discrete mode.
  51. * @param {number=} step The distance between values the user is allowed to pick. Default 1.
  52. * @param {number=} min The minimum value the user is allowed to pick. Default 0.
  53. * @param {number=} max The maximum value the user is allowed to pick. Default 100.
  54. */
  55. function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdTheming, $mdGesture, $parse, $log) {
  56. return {
  57. scope: {},
  58. require: '?ngModel',
  59. template:
  60. '<div class="md-slider-wrapper">' +
  61. '<div class="md-track-container">' +
  62. '<div class="md-track"></div>' +
  63. '<div class="md-track md-track-fill"></div>' +
  64. '<div class="md-track-ticks"></div>' +
  65. '</div>' +
  66. '<div class="md-thumb-container">' +
  67. '<div class="md-thumb"></div>' +
  68. '<div class="md-focus-thumb"></div>' +
  69. '<div class="md-focus-ring"></div>' +
  70. '<div class="md-sign">' +
  71. '<span class="md-thumb-text"></span>' +
  72. '</div>' +
  73. '<div class="md-disabled-thumb"></div>' +
  74. '</div>' +
  75. '</div>',
  76. compile: compile
  77. };
  78. // **********************************************************
  79. // Private Methods
  80. // **********************************************************
  81. function compile (tElement, tAttrs) {
  82. tElement.attr({
  83. tabIndex: 0,
  84. role: 'slider'
  85. });
  86. $mdAria.expect(tElement, 'aria-label');
  87. return postLink;
  88. }
  89. function postLink(scope, element, attr, ngModelCtrl) {
  90. $mdTheming(element);
  91. ngModelCtrl = ngModelCtrl || {
  92. // Mock ngModelController if it doesn't exist to give us
  93. // the minimum functionality needed
  94. $setViewValue: function(val) {
  95. this.$viewValue = val;
  96. this.$viewChangeListeners.forEach(function(cb) { cb(); });
  97. },
  98. $parsers: [],
  99. $formatters: [],
  100. $viewChangeListeners: []
  101. };
  102. var isDisabledGetter = angular.noop;
  103. if (attr.disabled != null) {
  104. isDisabledGetter = function() { return true; };
  105. } else if (attr.ngDisabled) {
  106. isDisabledGetter = angular.bind(null, $parse(attr.ngDisabled), scope.$parent);
  107. }
  108. var thumb = angular.element(element[0].querySelector('.md-thumb'));
  109. var thumbText = angular.element(element[0].querySelector('.md-thumb-text'));
  110. var thumbContainer = thumb.parent();
  111. var trackContainer = angular.element(element[0].querySelector('.md-track-container'));
  112. var activeTrack = angular.element(element[0].querySelector('.md-track-fill'));
  113. var tickContainer = angular.element(element[0].querySelector('.md-track-ticks'));
  114. var throttledRefreshDimensions = $mdUtil.throttle(refreshSliderDimensions, 5000);
  115. // Default values, overridable by attrs
  116. angular.isDefined(attr.min) ? attr.$observe('min', updateMin) : updateMin(0);
  117. angular.isDefined(attr.max) ? attr.$observe('max', updateMax) : updateMax(100);
  118. angular.isDefined(attr.step)? attr.$observe('step', updateStep) : updateStep(1);
  119. // We have to manually stop the $watch on ngDisabled because it exists
  120. // on the parent scope, and won't be automatically destroyed when
  121. // the component is destroyed.
  122. var stopDisabledWatch = angular.noop;
  123. if (attr.ngDisabled) {
  124. stopDisabledWatch = scope.$parent.$watch(attr.ngDisabled, updateAriaDisabled);
  125. }
  126. $mdGesture.register(element, 'drag');
  127. element
  128. .on('keydown', keydownListener)
  129. .on('$md.pressdown', onPressDown)
  130. .on('$md.pressup', onPressUp)
  131. .on('$md.dragstart', onDragStart)
  132. .on('$md.drag', onDrag)
  133. .on('$md.dragend', onDragEnd);
  134. // On resize, recalculate the slider's dimensions and re-render
  135. function updateAll() {
  136. refreshSliderDimensions();
  137. ngModelRender();
  138. redrawTicks();
  139. }
  140. setTimeout(updateAll, 0);
  141. var debouncedUpdateAll = $$rAF.throttle(updateAll);
  142. angular.element($window).on('resize', debouncedUpdateAll);
  143. scope.$on('$destroy', function() {
  144. angular.element($window).off('resize', debouncedUpdateAll);
  145. stopDisabledWatch();
  146. });
  147. ngModelCtrl.$render = ngModelRender;
  148. ngModelCtrl.$viewChangeListeners.push(ngModelRender);
  149. ngModelCtrl.$formatters.push(minMaxValidator);
  150. ngModelCtrl.$formatters.push(stepValidator);
  151. /**
  152. * Attributes
  153. */
  154. var min;
  155. var max;
  156. var step;
  157. function updateMin(value) {
  158. min = parseFloat(value);
  159. element.attr('aria-valuemin', value);
  160. updateAll();
  161. }
  162. function updateMax(value) {
  163. max = parseFloat(value);
  164. element.attr('aria-valuemax', value);
  165. updateAll();
  166. }
  167. function updateStep(value) {
  168. step = parseFloat(value);
  169. redrawTicks();
  170. }
  171. function updateAriaDisabled(isDisabled) {
  172. element.attr('aria-disabled', !!isDisabled);
  173. }
  174. // Draw the ticks with canvas.
  175. // The alternative to drawing ticks with canvas is to draw one element for each tick,
  176. // which could quickly become a performance bottleneck.
  177. var tickCanvas, tickCtx;
  178. function redrawTicks() {
  179. if (!angular.isDefined(attr.mdDiscrete)) return;
  180. if ( angular.isUndefined(step) ) return;
  181. if ( step <= 0 ) {
  182. var msg = 'Slider step value must be greater than zero when in discrete mode';
  183. $log.error(msg);
  184. throw new Error(msg);
  185. }
  186. var numSteps = Math.floor( (max - min) / step );
  187. if (!tickCanvas) {
  188. tickCanvas = angular.element('<canvas style="position:absolute;">');
  189. tickContainer.append(tickCanvas);
  190. var trackTicksStyle = $window.getComputedStyle(tickContainer[0]);
  191. tickCtx = tickCanvas[0].getContext('2d');
  192. tickCtx.fillStyle = trackTicksStyle.backgroundColor || 'black';
  193. }
  194. var dimensions = getSliderDimensions();
  195. tickCanvas[0].width = dimensions.width;
  196. tickCanvas[0].height = dimensions.height;
  197. var distance;
  198. for (var i = 0; i <= numSteps; i++) {
  199. distance = Math.floor(dimensions.width * (i / numSteps));
  200. tickCtx.fillRect(distance - 1, 0, 2, dimensions.height);
  201. }
  202. }
  203. /**
  204. * Refreshing Dimensions
  205. */
  206. var sliderDimensions = {};
  207. refreshSliderDimensions();
  208. function refreshSliderDimensions() {
  209. sliderDimensions = trackContainer[0].getBoundingClientRect();
  210. }
  211. function getSliderDimensions() {
  212. throttledRefreshDimensions();
  213. return sliderDimensions;
  214. }
  215. /**
  216. * left/right arrow listener
  217. */
  218. function keydownListener(ev) {
  219. if(element[0].hasAttribute('disabled')) {
  220. return;
  221. }
  222. var changeAmount;
  223. if (ev.keyCode === $mdConstant.KEY_CODE.LEFT_ARROW) {
  224. changeAmount = -step;
  225. } else if (ev.keyCode === $mdConstant.KEY_CODE.RIGHT_ARROW) {
  226. changeAmount = step;
  227. }
  228. if (changeAmount) {
  229. if (ev.metaKey || ev.ctrlKey || ev.altKey) {
  230. changeAmount *= 4;
  231. }
  232. ev.preventDefault();
  233. ev.stopPropagation();
  234. scope.$evalAsync(function() {
  235. setModelValue(ngModelCtrl.$viewValue + changeAmount);
  236. });
  237. }
  238. }
  239. /**
  240. * ngModel setters and validators
  241. */
  242. function setModelValue(value) {
  243. ngModelCtrl.$setViewValue( minMaxValidator(stepValidator(value)) );
  244. }
  245. function ngModelRender() {
  246. if (isNaN(ngModelCtrl.$viewValue)) {
  247. ngModelCtrl.$viewValue = ngModelCtrl.$modelValue;
  248. }
  249. var percent = (ngModelCtrl.$viewValue - min) / (max - min);
  250. scope.modelValue = ngModelCtrl.$viewValue;
  251. element.attr('aria-valuenow', ngModelCtrl.$viewValue);
  252. setSliderPercent(percent);
  253. thumbText.text( ngModelCtrl.$viewValue );
  254. }
  255. function minMaxValidator(value) {
  256. if (angular.isNumber(value)) {
  257. return Math.max(min, Math.min(max, value));
  258. }
  259. }
  260. function stepValidator(value) {
  261. if (angular.isNumber(value)) {
  262. var formattedValue = (Math.round(value / step) * step);
  263. // Format to 3 digits after the decimal point - fixes #2015.
  264. return (Math.round(formattedValue * 1000) / 1000);
  265. }
  266. }
  267. /**
  268. * @param percent 0-1
  269. */
  270. function setSliderPercent(percent) {
  271. var percentStr = (percent * 100) + '%';
  272. activeTrack.css('width', percentStr);
  273. thumbContainer.css('left',percentStr);
  274. element.toggleClass('md-min', percent === 0);
  275. element.toggleClass('md-max', percent === 1);
  276. }
  277. /**
  278. * Slide listeners
  279. */
  280. var isDragging = false;
  281. var isDiscrete = angular.isDefined(attr.mdDiscrete);
  282. function onPressDown(ev) {
  283. if (isDisabledGetter()) return;
  284. element.addClass('md-active');
  285. element[0].focus();
  286. refreshSliderDimensions();
  287. var exactVal = percentToValue( positionToPercent( ev.pointer.x ));
  288. var closestVal = minMaxValidator( stepValidator(exactVal) );
  289. scope.$apply(function() {
  290. setModelValue( closestVal );
  291. setSliderPercent( valueToPercent(closestVal));
  292. });
  293. }
  294. function onPressUp(ev) {
  295. if (isDisabledGetter()) return;
  296. element.removeClass('md-dragging md-active');
  297. var exactVal = percentToValue( positionToPercent( ev.pointer.x ));
  298. var closestVal = minMaxValidator( stepValidator(exactVal) );
  299. scope.$apply(function() {
  300. setModelValue(closestVal);
  301. ngModelRender();
  302. });
  303. }
  304. function onDragStart(ev) {
  305. if (isDisabledGetter()) return;
  306. isDragging = true;
  307. ev.stopPropagation();
  308. element.addClass('md-dragging');
  309. setSliderFromEvent(ev);
  310. }
  311. function onDrag(ev) {
  312. if (!isDragging) return;
  313. ev.stopPropagation();
  314. setSliderFromEvent(ev);
  315. }
  316. function onDragEnd(ev) {
  317. if (!isDragging) return;
  318. ev.stopPropagation();
  319. isDragging = false;
  320. }
  321. function setSliderFromEvent(ev) {
  322. // While panning discrete, update only the
  323. // visual positioning but not the model value.
  324. if ( isDiscrete ) adjustThumbPosition( ev.pointer.x );
  325. else doSlide( ev.pointer.x );
  326. }
  327. /**
  328. * Slide the UI by changing the model value
  329. * @param x
  330. */
  331. function doSlide( x ) {
  332. scope.$evalAsync( function() {
  333. setModelValue( percentToValue( positionToPercent(x) ));
  334. });
  335. }
  336. /**
  337. * Slide the UI without changing the model (while dragging/panning)
  338. * @param x
  339. */
  340. function adjustThumbPosition( x ) {
  341. var exactVal = percentToValue( positionToPercent( x ));
  342. var closestVal = minMaxValidator( stepValidator(exactVal) );
  343. setSliderPercent( positionToPercent(x) );
  344. thumbText.text( closestVal );
  345. }
  346. /**
  347. * Convert horizontal position on slider to percentage value of offset from beginning...
  348. * @param x
  349. * @returns {number}
  350. */
  351. function positionToPercent( x ) {
  352. return Math.max(0, Math.min(1, (x - sliderDimensions.left) / (sliderDimensions.width)));
  353. }
  354. /**
  355. * Convert percentage offset on slide to equivalent model value
  356. * @param percent
  357. * @returns {*}
  358. */
  359. function percentToValue( percent ) {
  360. return (min + percent * (max - min));
  361. }
  362. function valueToPercent( val ) {
  363. return (val - min)/(max - min);
  364. }
  365. }
  366. }
  367. SliderDirective.$inject = ["$$rAF", "$window", "$mdAria", "$mdUtil", "$mdConstant", "$mdTheming", "$mdGesture", "$parse", "$log"];
  368. })(window, window.angular);