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.

465 lines
15 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.progressCircular');
  8. goog.require('ngmaterial.core');
  9. /**
  10. * @ngdoc module
  11. * @name material.components.progressCircular
  12. * @description Module for a circular progressbar
  13. */
  14. angular.module('material.components.progressCircular', ['material.core']);
  15. /**
  16. * @ngdoc directive
  17. * @name mdProgressCircular
  18. * @module material.components.progressCircular
  19. * @restrict E
  20. *
  21. * @description
  22. * The circular progress directive is used to make loading content in your app as delightful and
  23. * painless as possible by minimizing the amount of visual change a user sees before they can view
  24. * and interact with content.
  25. *
  26. * For operations where the percentage of the operation completed can be determined, use a
  27. * determinate indicator. They give users a quick sense of how long an operation will take.
  28. *
  29. * For operations where the user is asked to wait a moment while something finishes up, and its
  30. * not necessary to expose what's happening behind the scenes and how long it will take, use an
  31. * indeterminate indicator.
  32. *
  33. * @param {string} md-mode Select from one of two modes: **'determinate'** and **'indeterminate'**.
  34. *
  35. * Note: if the `md-mode` value is set as undefined or specified as not 1 of the two (2) valid modes, then **'indeterminate'**
  36. * will be auto-applied as the mode.
  37. *
  38. * Note: if not configured, the `md-mode="indeterminate"` will be auto injected as an attribute.
  39. * If `value=""` is also specified, however, then `md-mode="determinate"` would be auto-injected instead.
  40. * @param {number=} value In determinate mode, this number represents the percentage of the
  41. * circular progress. Default: 0
  42. * @param {number=} md-diameter This specifies the diameter of the circular progress. The value
  43. * should be a pixel-size value (eg '100'). If this attribute is
  44. * not present then a default value of '50px' is assumed.
  45. *
  46. * @param {boolean=} ng-disabled Determines whether to disable the progress element.
  47. *
  48. * @usage
  49. * <hljs lang="html">
  50. * <md-progress-circular md-mode="determinate" value="..."></md-progress-circular>
  51. *
  52. * <md-progress-circular md-mode="determinate" ng-value="..."></md-progress-circular>
  53. *
  54. * <md-progress-circular md-mode="determinate" value="..." md-diameter="100"></md-progress-circular>
  55. *
  56. * <md-progress-circular md-mode="indeterminate"></md-progress-circular>
  57. * </hljs>
  58. */
  59. MdProgressCircularDirective.$inject = ["$window", "$mdProgressCircular", "$mdTheming", "$mdUtil", "$interval", "$log"];
  60. angular
  61. .module('material.components.progressCircular')
  62. .directive('mdProgressCircular', MdProgressCircularDirective);
  63. /* ngInject */
  64. function MdProgressCircularDirective($window, $mdProgressCircular, $mdTheming,
  65. $mdUtil, $interval, $log) {
  66. // Note that this shouldn't use use $$rAF, because it can cause an infinite loop
  67. // in any tests that call $animate.flush.
  68. var rAF = $window.requestAnimationFrame ||
  69. $window.webkitRequestAnimationFrame ||
  70. angular.noop;
  71. var cAF = $window.cancelAnimationFrame ||
  72. $window.webkitCancelAnimationFrame ||
  73. $window.webkitCancelRequestAnimationFrame ||
  74. angular.noop;
  75. var DEGREE_IN_RADIANS = $window.Math.PI / 180;
  76. var MODE_DETERMINATE = 'determinate';
  77. var MODE_INDETERMINATE = 'indeterminate';
  78. var DISABLED_CLASS = '_md-progress-circular-disabled';
  79. var INDETERMINATE_CLASS = 'md-mode-indeterminate';
  80. return {
  81. restrict: 'E',
  82. scope: {
  83. value: '@',
  84. mdDiameter: '@',
  85. mdMode: '@'
  86. },
  87. template:
  88. '<svg xmlns="http://www.w3.org/2000/svg">' +
  89. '<path fill="none"/>' +
  90. '</svg>',
  91. compile: function(element, attrs) {
  92. element.attr({
  93. 'aria-valuemin': 0,
  94. 'aria-valuemax': 100,
  95. 'role': 'progressbar'
  96. });
  97. if (angular.isUndefined(attrs.mdMode)) {
  98. var hasValue = angular.isDefined(attrs.value);
  99. var mode = hasValue ? MODE_DETERMINATE : MODE_INDETERMINATE;
  100. var info = "Auto-adding the missing md-mode='{0}' to the ProgressCircular element";
  101. // $log.debug( $mdUtil.supplant(info, [mode]) );
  102. attrs.$set('mdMode', mode);
  103. } else {
  104. attrs.$set('mdMode', attrs.mdMode.trim());
  105. }
  106. return MdProgressCircularLink;
  107. }
  108. };
  109. function MdProgressCircularLink(scope, element, attrs) {
  110. var node = element[0];
  111. var svg = angular.element(node.querySelector('svg'));
  112. var path = angular.element(node.querySelector('path'));
  113. var startIndeterminate = $mdProgressCircular.startIndeterminate;
  114. var endIndeterminate = $mdProgressCircular.endIndeterminate;
  115. var rotationIndeterminate = 0;
  116. var lastAnimationId = 0;
  117. var lastDrawFrame;
  118. var interval;
  119. $mdTheming(element);
  120. element.toggleClass(DISABLED_CLASS, attrs.hasOwnProperty('disabled'));
  121. // If the mode is indeterminate, it doesn't need to
  122. // wait for the next digest. It can start right away.
  123. if(scope.mdMode === MODE_INDETERMINATE){
  124. startIndeterminateAnimation();
  125. }
  126. scope.$on('$destroy', function(){
  127. cleanupIndeterminateAnimation();
  128. if (lastDrawFrame) {
  129. cAF(lastDrawFrame);
  130. }
  131. });
  132. scope.$watchGroup(['value', 'mdMode', function() {
  133. var isDisabled = node.disabled;
  134. // Sometimes the browser doesn't return a boolean, in
  135. // which case we should check whether the attribute is
  136. // present.
  137. if (isDisabled === true || isDisabled === false){
  138. return isDisabled;
  139. }
  140. return angular.isDefined(element.attr('disabled'));
  141. }], function(newValues, oldValues) {
  142. var mode = newValues[1];
  143. var isDisabled = newValues[2];
  144. var wasDisabled = oldValues[2];
  145. if (isDisabled !== wasDisabled) {
  146. element.toggleClass(DISABLED_CLASS, !!isDisabled);
  147. }
  148. if (isDisabled) {
  149. cleanupIndeterminateAnimation();
  150. } else {
  151. if (mode !== MODE_DETERMINATE && mode !== MODE_INDETERMINATE) {
  152. mode = MODE_INDETERMINATE;
  153. attrs.$set('mdMode', mode);
  154. }
  155. if (mode === MODE_INDETERMINATE) {
  156. startIndeterminateAnimation();
  157. } else {
  158. var newValue = clamp(newValues[0]);
  159. cleanupIndeterminateAnimation();
  160. element.attr('aria-valuenow', newValue);
  161. renderCircle(clamp(oldValues[0]), newValue);
  162. }
  163. }
  164. });
  165. // This is in a separate watch in order to avoid layout, unless
  166. // the value has actually changed.
  167. scope.$watch('mdDiameter', function(newValue) {
  168. var diameter = getSize(newValue);
  169. var strokeWidth = getStroke(diameter);
  170. var transformOrigin = (diameter / 2) + 'px';
  171. var dimensions = {
  172. width: diameter + 'px',
  173. height: diameter + 'px'
  174. };
  175. // The viewBox has to be applied via setAttribute, because it is
  176. // case-sensitive. If jQuery is included in the page, `.attr` lowercases
  177. // all attribute names.
  178. svg[0].setAttribute('viewBox', '0 0 ' + diameter + ' ' + diameter);
  179. // Usually viewBox sets the dimensions for the SVG, however that doesn't
  180. // seem to be the case on IE10.
  181. // Important! The transform origin has to be set from here and it has to
  182. // be in the format of "Ypx Ypx Ypx", otherwise the rotation wobbles in
  183. // IE and Edge, because they don't account for the stroke width when
  184. // rotating. Also "center" doesn't help in this case, it has to be a
  185. // precise value.
  186. svg
  187. .css(dimensions)
  188. .css('transform-origin', transformOrigin + ' ' + transformOrigin + ' ' + transformOrigin);
  189. element.css(dimensions);
  190. path.css('stroke-width', strokeWidth + 'px');
  191. });
  192. function renderCircle(animateFrom, animateTo, easing, duration, rotation) {
  193. var id = ++lastAnimationId;
  194. var startTime = $mdUtil.now();
  195. var changeInValue = animateTo - animateFrom;
  196. var diameter = getSize(scope.mdDiameter);
  197. var pathDiameter = diameter - getStroke(diameter);
  198. var ease = easing || $mdProgressCircular.easeFn;
  199. var animationDuration = duration || $mdProgressCircular.duration;
  200. // No need to animate it if the values are the same
  201. if (animateTo === animateFrom) {
  202. path.attr('d', getSvgArc(animateTo, diameter, pathDiameter, rotation));
  203. } else {
  204. lastDrawFrame = rAF(function animation() {
  205. var currentTime = $window.Math.max(0, $window.Math.min($mdUtil.now() - startTime, animationDuration));
  206. path.attr('d', getSvgArc(
  207. ease(currentTime, animateFrom, changeInValue, animationDuration),
  208. diameter,
  209. pathDiameter,
  210. rotation
  211. ));
  212. // Do not allow overlapping animations
  213. if (id === lastAnimationId && currentTime < animationDuration) {
  214. lastDrawFrame = rAF(animation);
  215. }
  216. });
  217. }
  218. }
  219. function animateIndeterminate() {
  220. renderCircle(
  221. startIndeterminate,
  222. endIndeterminate,
  223. $mdProgressCircular.easeFnIndeterminate,
  224. $mdProgressCircular.durationIndeterminate,
  225. rotationIndeterminate
  226. );
  227. // The % 100 technically isn't necessary, but it keeps the rotation
  228. // under 100, instead of becoming a crazy large number.
  229. rotationIndeterminate = (rotationIndeterminate + endIndeterminate) % 100;
  230. var temp = startIndeterminate;
  231. startIndeterminate = -endIndeterminate;
  232. endIndeterminate = -temp;
  233. }
  234. function startIndeterminateAnimation() {
  235. if (!interval) {
  236. // Note that this interval isn't supposed to trigger a digest.
  237. interval = $interval(
  238. animateIndeterminate,
  239. $mdProgressCircular.durationIndeterminate + 50,
  240. 0,
  241. false
  242. );
  243. animateIndeterminate();
  244. element
  245. .addClass(INDETERMINATE_CLASS)
  246. .removeAttr('aria-valuenow');
  247. }
  248. }
  249. function cleanupIndeterminateAnimation() {
  250. if (interval) {
  251. $interval.cancel(interval);
  252. interval = null;
  253. element.removeClass(INDETERMINATE_CLASS);
  254. }
  255. }
  256. }
  257. /**
  258. * Generates an arc following the SVG arc syntax.
  259. * Syntax spec: https://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
  260. *
  261. * @param {number} current Current value between 0 and 100.
  262. * @param {number} diameter Diameter of the container.
  263. * @param {number} pathDiameter Diameter of the path element.
  264. * @param {number=0} rotation The point at which the semicircle should start rendering.
  265. * Used for doing the indeterminate animation.
  266. *
  267. * @returns {string} String representation of an SVG arc.
  268. */
  269. function getSvgArc(current, diameter, pathDiameter, rotation) {
  270. // The angle can't be exactly 360, because the arc becomes hidden.
  271. var maximumAngle = 359.99 / 100;
  272. var startPoint = rotation || 0;
  273. var radius = diameter / 2;
  274. var pathRadius = pathDiameter / 2;
  275. var startAngle = startPoint * maximumAngle;
  276. var endAngle = current * maximumAngle;
  277. var start = polarToCartesian(radius, pathRadius, startAngle);
  278. var end = polarToCartesian(radius, pathRadius, endAngle + startAngle);
  279. var arcSweep = endAngle < 0 ? 0 : 1;
  280. var largeArcFlag;
  281. if (endAngle < 0) {
  282. largeArcFlag = endAngle >= -180 ? 0 : 1;
  283. } else {
  284. largeArcFlag = endAngle <= 180 ? 0 : 1;
  285. }
  286. return 'M' + start + 'A' + pathRadius + ',' + pathRadius +
  287. ' 0 ' + largeArcFlag + ',' + arcSweep + ' ' + end;
  288. }
  289. /**
  290. * Converts Polar coordinates to Cartesian.
  291. *
  292. * @param {number} radius Radius of the container.
  293. * @param {number} pathRadius Radius of the path element
  294. * @param {number} angleInDegress Angle at which to place the point.
  295. *
  296. * @returns {string} Cartesian coordinates in the format of `x,y`.
  297. */
  298. function polarToCartesian(radius, pathRadius, angleInDegrees) {
  299. var angleInRadians = (angleInDegrees - 90) * DEGREE_IN_RADIANS;
  300. return (radius + (pathRadius * $window.Math.cos(angleInRadians))) +
  301. ',' + (radius + (pathRadius * $window.Math.sin(angleInRadians)));
  302. }
  303. /**
  304. * Limits a value between 0 and 100.
  305. */
  306. function clamp(value) {
  307. return $window.Math.max(0, $window.Math.min(value || 0, 100));
  308. }
  309. /**
  310. * Determines the size of a progress circle, based on the provided
  311. * value in the following formats: `X`, `Ypx`, `Z%`.
  312. */
  313. function getSize(value) {
  314. var defaultValue = $mdProgressCircular.progressSize;
  315. if (value) {
  316. var parsed = parseFloat(value);
  317. if (value.lastIndexOf('%') === value.length - 1) {
  318. parsed = (parsed / 100) * defaultValue;
  319. }
  320. return parsed;
  321. }
  322. return defaultValue;
  323. }
  324. /**
  325. * Determines the circle's stroke width, based on
  326. * the provided diameter.
  327. */
  328. function getStroke(diameter) {
  329. return $mdProgressCircular.strokeWidth / 100 * diameter;
  330. }
  331. }
  332. /**
  333. * @ngdoc service
  334. * @name $mdProgressCircular
  335. * @module material.components.progressCircular
  336. *
  337. * @description
  338. * Allows the user to specify the default options for the `progressCircular` directive.
  339. *
  340. * @property {number} progressSize Diameter of the progress circle in pixels.
  341. * @property {number} strokeWidth Width of the circle's stroke as a percentage of the circle's size.
  342. * @property {number} duration Length of the circle animation in milliseconds.
  343. * @property {function} easeFn Default easing animation function.
  344. * @property {object} easingPresets Collection of pre-defined easing functions.
  345. *
  346. * @property {number} durationIndeterminate Duration of the indeterminate animation.
  347. * @property {number} startIndeterminate Indeterminate animation start point.
  348. * @property {number} endIndeterminate Indeterminate animation end point.
  349. * @property {function} easeFnIndeterminate Easing function to be used when animating
  350. * between the indeterminate values.
  351. *
  352. * @property {(function(object): object)} configure Used to modify the default options.
  353. *
  354. * @usage
  355. * <hljs lang="js">
  356. * myAppModule.config(function($mdProgressCircularProvider) {
  357. *
  358. * // Example of changing the default progress options.
  359. * $mdProgressCircularProvider.configure({
  360. * progressSize: 100,
  361. * strokeWidth: 20,
  362. * duration: 800
  363. * });
  364. * });
  365. * </hljs>
  366. *
  367. */
  368. angular
  369. .module('material.components.progressCircular')
  370. .provider("$mdProgressCircular", MdProgressCircularProvider);
  371. function MdProgressCircularProvider() {
  372. var progressConfig = {
  373. progressSize: 50,
  374. strokeWidth: 10,
  375. duration: 100,
  376. easeFn: linearEase,
  377. durationIndeterminate: 500,
  378. startIndeterminate: 3,
  379. endIndeterminate: 80,
  380. easeFnIndeterminate: materialEase,
  381. easingPresets: {
  382. linearEase: linearEase,
  383. materialEase: materialEase
  384. }
  385. };
  386. return {
  387. configure: function(options) {
  388. progressConfig = angular.extend(progressConfig, options || {});
  389. return progressConfig;
  390. },
  391. $get: function() { return progressConfig; }
  392. };
  393. function linearEase(t, b, c, d) {
  394. return c * t / d + b;
  395. }
  396. function materialEase(t, b, c, d) {
  397. // via http://www.timotheegroleau.com/Flash/experiments/easing_function_generator.htm
  398. // with settings of [0, 0, 1, 1]
  399. var ts = (t /= d) * t;
  400. var tc = ts * t;
  401. return b + c * (6 * tc * ts + -15 * ts * ts + 10 * tc);
  402. }
  403. }
  404. ngmaterial.components.progressCircular = angular.module("material.components.progressCircular");