slider.js 20 KB

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