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.

458 lines
15 KiB

7 years ago
  1. /*!
  2. * Angular Material Design
  3. * https://github.com/angular/material
  4. * @license MIT
  5. * v1.1.3
  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 MODE_DETERMINATE = 'determinate';
  76. var MODE_INDETERMINATE = 'indeterminate';
  77. var DISABLED_CLASS = '_md-progress-circular-disabled';
  78. var INDETERMINATE_CLASS = 'md-mode-indeterminate';
  79. return {
  80. restrict: 'E',
  81. scope: {
  82. value: '@',
  83. mdDiameter: '@',
  84. mdMode: '@'
  85. },
  86. template:
  87. '<svg xmlns="http://www.w3.org/2000/svg">' +
  88. '<path fill="none"/>' +
  89. '</svg>',
  90. compile: function(element, attrs) {
  91. element.attr({
  92. 'aria-valuemin': 0,
  93. 'aria-valuemax': 100,
  94. 'role': 'progressbar'
  95. });
  96. if (angular.isUndefined(attrs.mdMode)) {
  97. var mode = attrs.hasOwnProperty('value') ? MODE_DETERMINATE : MODE_INDETERMINATE;
  98. attrs.$set('mdMode', mode);
  99. } else {
  100. attrs.$set('mdMode', attrs.mdMode.trim());
  101. }
  102. return MdProgressCircularLink;
  103. }
  104. };
  105. function MdProgressCircularLink(scope, element, attrs) {
  106. var node = element[0];
  107. var svg = angular.element(node.querySelector('svg'));
  108. var path = angular.element(node.querySelector('path'));
  109. var startIndeterminate = $mdProgressCircular.startIndeterminate;
  110. var endIndeterminate = $mdProgressCircular.endIndeterminate;
  111. var iterationCount = 0;
  112. var lastAnimationId = 0;
  113. var lastDrawFrame;
  114. var interval;
  115. $mdTheming(element);
  116. element.toggleClass(DISABLED_CLASS, attrs.hasOwnProperty('disabled'));
  117. // If the mode is indeterminate, it doesn't need to
  118. // wait for the next digest. It can start right away.
  119. if(scope.mdMode === MODE_INDETERMINATE){
  120. startIndeterminateAnimation();
  121. }
  122. scope.$on('$destroy', function(){
  123. cleanupIndeterminateAnimation();
  124. if (lastDrawFrame) {
  125. cAF(lastDrawFrame);
  126. }
  127. });
  128. scope.$watchGroup(['value', 'mdMode', function() {
  129. var isDisabled = node.disabled;
  130. // Sometimes the browser doesn't return a boolean, in
  131. // which case we should check whether the attribute is
  132. // present.
  133. if (isDisabled === true || isDisabled === false){
  134. return isDisabled;
  135. }
  136. return angular.isDefined(element.attr('disabled'));
  137. }], function(newValues, oldValues) {
  138. var mode = newValues[1];
  139. var isDisabled = newValues[2];
  140. var wasDisabled = oldValues[2];
  141. if (isDisabled !== wasDisabled) {
  142. element.toggleClass(DISABLED_CLASS, !!isDisabled);
  143. }
  144. if (isDisabled) {
  145. cleanupIndeterminateAnimation();
  146. } else {
  147. if (mode !== MODE_DETERMINATE && mode !== MODE_INDETERMINATE) {
  148. mode = MODE_INDETERMINATE;
  149. attrs.$set('mdMode', mode);
  150. }
  151. if (mode === MODE_INDETERMINATE) {
  152. startIndeterminateAnimation();
  153. } else {
  154. var newValue = clamp(newValues[0]);
  155. cleanupIndeterminateAnimation();
  156. element.attr('aria-valuenow', newValue);
  157. renderCircle(clamp(oldValues[0]), newValue);
  158. }
  159. }
  160. });
  161. // This is in a separate watch in order to avoid layout, unless
  162. // the value has actually changed.
  163. scope.$watch('mdDiameter', function(newValue) {
  164. var diameter = getSize(newValue);
  165. var strokeWidth = getStroke(diameter);
  166. var value = clamp(scope.value);
  167. var transformOrigin = (diameter / 2) + 'px';
  168. var dimensions = {
  169. width: diameter + 'px',
  170. height: diameter + 'px'
  171. };
  172. // The viewBox has to be applied via setAttribute, because it is
  173. // case-sensitive. If jQuery is included in the page, `.attr` lowercases
  174. // all attribute names.
  175. svg[0].setAttribute('viewBox', '0 0 ' + diameter + ' ' + diameter);
  176. // Usually viewBox sets the dimensions for the SVG, however that doesn't
  177. // seem to be the case on IE10.
  178. // Important! The transform origin has to be set from here and it has to
  179. // be in the format of "Ypx Ypx Ypx", otherwise the rotation wobbles in
  180. // IE and Edge, because they don't account for the stroke width when
  181. // rotating. Also "center" doesn't help in this case, it has to be a
  182. // precise value.
  183. svg
  184. .css(dimensions)
  185. .css('transform-origin', transformOrigin + ' ' + transformOrigin + ' ' + transformOrigin);
  186. element.css(dimensions);
  187. path.attr('stroke-width', strokeWidth);
  188. path.attr('stroke-linecap', 'square');
  189. if (scope.mdMode == MODE_INDETERMINATE) {
  190. path.attr('d', getSvgArc(diameter, strokeWidth, true));
  191. path.attr('stroke-dasharray', (diameter - strokeWidth) * $window.Math.PI * 0.75);
  192. path.attr('stroke-dashoffset', getDashLength(diameter, strokeWidth, 1, 75));
  193. } else {
  194. path.attr('d', getSvgArc(diameter, strokeWidth, false));
  195. path.attr('stroke-dasharray', (diameter - strokeWidth) * $window.Math.PI);
  196. path.attr('stroke-dashoffset', getDashLength(diameter, strokeWidth, 0, 100));
  197. renderCircle(value, value);
  198. }
  199. });
  200. function renderCircle(animateFrom, animateTo, easing, duration, iterationCount, maxValue) {
  201. var id = ++lastAnimationId;
  202. var startTime = $mdUtil.now();
  203. var changeInValue = animateTo - animateFrom;
  204. var diameter = getSize(scope.mdDiameter);
  205. var strokeWidth = getStroke(diameter);
  206. var ease = easing || $mdProgressCircular.easeFn;
  207. var animationDuration = duration || $mdProgressCircular.duration;
  208. var rotation = -90 * (iterationCount || 0);
  209. var dashLimit = maxValue || 100;
  210. // No need to animate it if the values are the same
  211. if (animateTo === animateFrom) {
  212. renderFrame(animateTo);
  213. } else {
  214. lastDrawFrame = rAF(function animation() {
  215. var currentTime = $window.Math.max(0, $window.Math.min($mdUtil.now() - startTime, animationDuration));
  216. renderFrame(ease(currentTime, animateFrom, changeInValue, animationDuration));
  217. // Do not allow overlapping animations
  218. if (id === lastAnimationId && currentTime < animationDuration) {
  219. lastDrawFrame = rAF(animation);
  220. }
  221. });
  222. }
  223. function renderFrame(value) {
  224. path.attr('stroke-dashoffset', getDashLength(diameter, strokeWidth, value, dashLimit));
  225. path.attr('transform','rotate(' + (rotation) + ' ' + diameter/2 + ' ' + diameter/2 + ')');
  226. }
  227. }
  228. function animateIndeterminate() {
  229. renderCircle(
  230. startIndeterminate,
  231. endIndeterminate,
  232. $mdProgressCircular.easeFnIndeterminate,
  233. $mdProgressCircular.durationIndeterminate,
  234. iterationCount,
  235. 75
  236. );
  237. // The %4 technically isn't necessary, but it keeps the rotation
  238. // under 360, instead of becoming a crazy large number.
  239. iterationCount = ++iterationCount % 4;
  240. }
  241. function startIndeterminateAnimation() {
  242. if (!interval) {
  243. // Note that this interval isn't supposed to trigger a digest.
  244. interval = $interval(
  245. animateIndeterminate,
  246. $mdProgressCircular.durationIndeterminate,
  247. 0,
  248. false
  249. );
  250. animateIndeterminate();
  251. element
  252. .addClass(INDETERMINATE_CLASS)
  253. .removeAttr('aria-valuenow');
  254. }
  255. }
  256. function cleanupIndeterminateAnimation() {
  257. if (interval) {
  258. $interval.cancel(interval);
  259. interval = null;
  260. element.removeClass(INDETERMINATE_CLASS);
  261. }
  262. }
  263. }
  264. /**
  265. * Returns SVG path data for progress circle
  266. * Syntax spec: https://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
  267. *
  268. * @param {number} diameter Diameter of the container.
  269. * @param {number} strokeWidth Stroke width to be used when drawing circle
  270. * @param {boolean} indeterminate Use if progress circle will be used for indeterminate
  271. *
  272. * @returns {string} String representation of an SVG arc.
  273. */
  274. function getSvgArc(diameter, strokeWidth, indeterminate) {
  275. var radius = diameter / 2;
  276. var offset = strokeWidth / 2;
  277. var start = radius + ',' + offset; // ie: (25, 2.5) or 12 o'clock
  278. var end = offset + ',' + radius; // ie: (2.5, 25) or 9 o'clock
  279. var arcRadius = radius - offset;
  280. return 'M' + start
  281. + 'A' + arcRadius + ',' + arcRadius + ' 0 1 1 ' + end // 75% circle
  282. + (indeterminate ? '' : 'A' + arcRadius + ',' + arcRadius + ' 0 0 1 ' + start); // loop to start
  283. }
  284. /**
  285. * Return stroke length for progress circle
  286. *
  287. * @param {number} diameter Diameter of the container.
  288. * @param {number} strokeWidth Stroke width to be used when drawing circle
  289. * @param {number} value Percentage of circle (between 0 and 100)
  290. * @param {number} limit Max percentage for circle
  291. *
  292. * @returns {number} Stroke length for progres circle
  293. */
  294. function getDashLength(diameter, strokeWidth, value, limit) {
  295. return (diameter - strokeWidth) * $window.Math.PI * ( (3 * (limit || 100) / 100) - (value/100) );
  296. }
  297. /**
  298. * Limits a value between 0 and 100.
  299. */
  300. function clamp(value) {
  301. return $window.Math.max(0, $window.Math.min(value || 0, 100));
  302. }
  303. /**
  304. * Determines the size of a progress circle, based on the provided
  305. * value in the following formats: `X`, `Ypx`, `Z%`.
  306. */
  307. function getSize(value) {
  308. var defaultValue = $mdProgressCircular.progressSize;
  309. if (value) {
  310. var parsed = parseFloat(value);
  311. if (value.lastIndexOf('%') === value.length - 1) {
  312. parsed = (parsed / 100) * defaultValue;
  313. }
  314. return parsed;
  315. }
  316. return defaultValue;
  317. }
  318. /**
  319. * Determines the circle's stroke width, based on
  320. * the provided diameter.
  321. */
  322. function getStroke(diameter) {
  323. return $mdProgressCircular.strokeWidth / 100 * diameter;
  324. }
  325. }
  326. /**
  327. * @ngdoc service
  328. * @name $mdProgressCircular
  329. * @module material.components.progressCircular
  330. *
  331. * @description
  332. * Allows the user to specify the default options for the `progressCircular` directive.
  333. *
  334. * @property {number} progressSize Diameter of the progress circle in pixels.
  335. * @property {number} strokeWidth Width of the circle's stroke as a percentage of the circle's size.
  336. * @property {number} duration Length of the circle animation in milliseconds.
  337. * @property {function} easeFn Default easing animation function.
  338. * @property {object} easingPresets Collection of pre-defined easing functions.
  339. *
  340. * @property {number} durationIndeterminate Duration of the indeterminate animation.
  341. * @property {number} startIndeterminate Indeterminate animation start point.
  342. * @property {number} endIndeterminate Indeterminate animation end point.
  343. * @property {function} easeFnIndeterminate Easing function to be used when animating
  344. * between the indeterminate values.
  345. *
  346. * @property {(function(object): object)} configure Used to modify the default options.
  347. *
  348. * @usage
  349. * <hljs lang="js">
  350. * myAppModule.config(function($mdProgressCircularProvider) {
  351. *
  352. * // Example of changing the default progress options.
  353. * $mdProgressCircularProvider.configure({
  354. * progressSize: 100,
  355. * strokeWidth: 20,
  356. * duration: 800
  357. * });
  358. * });
  359. * </hljs>
  360. *
  361. */
  362. angular
  363. .module('material.components.progressCircular')
  364. .provider("$mdProgressCircular", MdProgressCircularProvider);
  365. function MdProgressCircularProvider() {
  366. var progressConfig = {
  367. progressSize: 50,
  368. strokeWidth: 10,
  369. duration: 100,
  370. easeFn: linearEase,
  371. durationIndeterminate: 1333,
  372. startIndeterminate: 1,
  373. endIndeterminate: 149,
  374. easeFnIndeterminate: materialEase,
  375. easingPresets: {
  376. linearEase: linearEase,
  377. materialEase: materialEase
  378. }
  379. };
  380. return {
  381. configure: function(options) {
  382. progressConfig = angular.extend(progressConfig, options || {});
  383. return progressConfig;
  384. },
  385. $get: function() { return progressConfig; }
  386. };
  387. function linearEase(t, b, c, d) {
  388. return c * t / d + b;
  389. }
  390. function materialEase(t, b, c, d) {
  391. // via http://www.timotheegroleau.com/Flash/experiments/easing_function_generator.htm
  392. // with settings of [0, 0, 1, 1]
  393. var ts = (t /= d) * t;
  394. var tc = ts * t;
  395. return b + c * (6 * tc * ts + -15 * ts * ts + 10 * tc);
  396. }
  397. }
  398. ngmaterial.components.progressCircular = angular.module("material.components.progressCircular");