2  * Angular Material Design
 
   3  * https://github.com/angular/material
 
   7 goog.provide('ngmaterial.components.progressCircular');
 
   8 goog.require('ngmaterial.core');
 
  11  * @name material.components.progressCircular
 
  12  * @description Module for a circular progressbar
 
  15 angular.module('material.components.progressCircular', ['material.core']);
 
  19  * @name mdProgressCircular
 
  20  * @module material.components.progressCircular
 
  24  * The circular progress directive is used to make loading content in your app as delightful and
 
  25  * painless as possible by minimizing the amount of visual change a user sees before they can view
 
  26  * and interact with content.
 
  28  * For operations where the percentage of the operation completed can be determined, use a
 
  29  * determinate indicator. They give users a quick sense of how long an operation will take.
 
  31  * For operations where the user is asked to wait a moment while something finishes up, and it’s
 
  32  * not necessary to expose what's happening behind the scenes and how long it will take, use an
 
  33  * indeterminate indicator.
 
  35  * @param {string} md-mode Select from one of two modes: **'determinate'** and **'indeterminate'**.
 
  37  * Note: if the `md-mode` value is set as undefined or specified as not 1 of the two (2) valid modes, then **'indeterminate'**
 
  38  * will be auto-applied as the mode.
 
  40  * Note: if not configured, the `md-mode="indeterminate"` will be auto injected as an attribute.
 
  41  * If `value=""` is also specified, however, then `md-mode="determinate"` would be auto-injected instead.
 
  42  * @param {number=} value In determinate mode, this number represents the percentage of the
 
  43  *     circular progress. Default: 0
 
  44  * @param {number=} md-diameter This specifies the diameter of the circular progress. The value
 
  45  * should be a pixel-size value (eg '100'). If this attribute is
 
  46  * not present then a default value of '50px' is assumed.
 
  48  * @param {boolean=} ng-disabled Determines whether to disable the progress element.
 
  52  * <md-progress-circular md-mode="determinate" value="..."></md-progress-circular>
 
  54  * <md-progress-circular md-mode="determinate" ng-value="..."></md-progress-circular>
 
  56  * <md-progress-circular md-mode="determinate" value="..." md-diameter="100"></md-progress-circular>
 
  58  * <md-progress-circular md-mode="indeterminate"></md-progress-circular>
 
  62 MdProgressCircularDirective['$inject'] = ["$window", "$mdProgressCircular", "$mdTheming", "$mdUtil", "$interval", "$log"];
 
  64   .module('material.components.progressCircular')
 
  65   .directive('mdProgressCircular', MdProgressCircularDirective);
 
  68 function MdProgressCircularDirective($window, $mdProgressCircular, $mdTheming,
 
  69                                      $mdUtil, $interval, $log) {
 
  71   // Note that this shouldn't use use $$rAF, because it can cause an infinite loop
 
  72   // in any tests that call $animate.flush.
 
  73   var rAF = $window.requestAnimationFrame ||
 
  74             $window.webkitRequestAnimationFrame ||
 
  77   var cAF = $window.cancelAnimationFrame ||
 
  78             $window.webkitCancelAnimationFrame ||
 
  79             $window.webkitCancelRequestAnimationFrame ||
 
  82   var MODE_DETERMINATE = 'determinate';
 
  83   var MODE_INDETERMINATE = 'indeterminate';
 
  84   var DISABLED_CLASS = '_md-progress-circular-disabled';
 
  85   var INDETERMINATE_CLASS = 'md-mode-indeterminate';
 
  95       '<svg xmlns="http://www.w3.org/2000/svg">' +
 
  96         '<path fill="none"/>' +
 
  98     compile: function(element, attrs) {
 
 101         'aria-valuemax': 100,
 
 102         'role': 'progressbar'
 
 105       if (angular.isUndefined(attrs.mdMode)) {
 
 106         var mode = attrs.hasOwnProperty('value') ? MODE_DETERMINATE : MODE_INDETERMINATE;
 
 107         attrs.$set('mdMode', mode);
 
 109         attrs.$set('mdMode', attrs.mdMode.trim());
 
 112       return MdProgressCircularLink;
 
 116   function MdProgressCircularLink(scope, element, attrs) {
 
 117     var node = element[0];
 
 118     var svg = angular.element(node.querySelector('svg'));
 
 119     var path = angular.element(node.querySelector('path'));
 
 120     var startIndeterminate = $mdProgressCircular.startIndeterminate;
 
 121     var endIndeterminate = $mdProgressCircular.endIndeterminate;
 
 122     var iterationCount = 0;
 
 123     var lastAnimationId = 0;
 
 128     element.toggleClass(DISABLED_CLASS, attrs.hasOwnProperty('disabled'));
 
 130     // If the mode is indeterminate, it doesn't need to
 
 131     // wait for the next digest. It can start right away.
 
 132     if(scope.mdMode === MODE_INDETERMINATE){
 
 133       startIndeterminateAnimation();
 
 136     scope.$on('$destroy', function(){
 
 137       cleanupIndeterminateAnimation();
 
 144     scope.$watchGroup(['value', 'mdMode', function() {
 
 145       var isDisabled = node.disabled;
 
 147       // Sometimes the browser doesn't return a boolean, in
 
 148       // which case we should check whether the attribute is
 
 150       if (isDisabled === true || isDisabled === false){
 
 154       return angular.isDefined(element.attr('disabled'));
 
 155     }], function(newValues, oldValues) {
 
 156       var mode = newValues[1];
 
 157       var isDisabled = newValues[2];
 
 158       var wasDisabled = oldValues[2];
 
 160       if (isDisabled !== wasDisabled) {
 
 161         element.toggleClass(DISABLED_CLASS, !!isDisabled);
 
 165         cleanupIndeterminateAnimation();
 
 167         if (mode !== MODE_DETERMINATE && mode !== MODE_INDETERMINATE) {
 
 168           mode = MODE_INDETERMINATE;
 
 169           attrs.$set('mdMode', mode);
 
 172         if (mode === MODE_INDETERMINATE) {
 
 173           startIndeterminateAnimation();
 
 175           var newValue = clamp(newValues[0]);
 
 177           cleanupIndeterminateAnimation();
 
 179           element.attr('aria-valuenow', newValue);
 
 180           renderCircle(clamp(oldValues[0]), newValue);
 
 186     // This is in a separate watch in order to avoid layout, unless
 
 187     // the value has actually changed.
 
 188     scope.$watch('mdDiameter', function(newValue) {
 
 189       var diameter = getSize(newValue);
 
 190       var strokeWidth = getStroke(diameter);
 
 191       var value = clamp(scope.value);
 
 192       var transformOrigin = (diameter / 2) + 'px';
 
 194         width: diameter + 'px',
 
 195         height: diameter + 'px'
 
 198       // The viewBox has to be applied via setAttribute, because it is
 
 199       // case-sensitive. If jQuery is included in the page, `.attr` lowercases
 
 200       // all attribute names.
 
 201       svg[0].setAttribute('viewBox', '0 0 ' + diameter + ' ' + diameter);
 
 203       // Usually viewBox sets the dimensions for the SVG, however that doesn't
 
 204       // seem to be the case on IE10.
 
 205       // Important! The transform origin has to be set from here and it has to
 
 206       // be in the format of "Ypx Ypx Ypx", otherwise the rotation wobbles in
 
 207       // IE and Edge, because they don't account for the stroke width when
 
 208       // rotating. Also "center" doesn't help in this case, it has to be a
 
 212         .css('transform-origin', transformOrigin + ' ' + transformOrigin + ' ' + transformOrigin);
 
 214       element.css(dimensions);
 
 216       path.attr('stroke-width', strokeWidth);
 
 217       path.attr('stroke-linecap', 'square');
 
 218       if (scope.mdMode == MODE_INDETERMINATE) {
 
 219         path.attr('d', getSvgArc(diameter, strokeWidth, true));
 
 220         path.attr('stroke-dasharray', (diameter - strokeWidth) * $window.Math.PI * 0.75);
 
 221         path.attr('stroke-dashoffset', getDashLength(diameter, strokeWidth, 1, 75));
 
 223         path.attr('d', getSvgArc(diameter, strokeWidth, false));
 
 224         path.attr('stroke-dasharray', (diameter - strokeWidth) * $window.Math.PI);
 
 225         path.attr('stroke-dashoffset', getDashLength(diameter, strokeWidth, 0, 100));
 
 226         renderCircle(value, value);
 
 231     function renderCircle(animateFrom, animateTo, easing, duration, iterationCount, maxValue) {
 
 232       var id = ++lastAnimationId;
 
 233       var startTime = $mdUtil.now();
 
 234       var changeInValue = animateTo - animateFrom;
 
 235       var diameter = getSize(scope.mdDiameter);
 
 236       var strokeWidth = getStroke(diameter);
 
 237       var ease = easing || $mdProgressCircular.easeFn;
 
 238       var animationDuration = duration || $mdProgressCircular.duration;
 
 239       var rotation = -90 * (iterationCount || 0);
 
 240       var dashLimit = maxValue || 100;
 
 242       // No need to animate it if the values are the same
 
 243       if (animateTo === animateFrom) {
 
 244         renderFrame(animateTo);
 
 246         lastDrawFrame = rAF(function animation() {
 
 247           var currentTime = $window.Math.max(0, $window.Math.min($mdUtil.now() - startTime, animationDuration));
 
 249           renderFrame(ease(currentTime, animateFrom, changeInValue, animationDuration));
 
 251           // Do not allow overlapping animations
 
 252           if (id === lastAnimationId && currentTime < animationDuration) {
 
 253             lastDrawFrame = rAF(animation);
 
 258       function renderFrame(value) {
 
 259         path.attr('stroke-dashoffset', getDashLength(diameter, strokeWidth, value, dashLimit));
 
 260         path.attr('transform','rotate(' + (rotation) + ' ' + diameter/2 + ' ' + diameter/2 + ')');
 
 264     function animateIndeterminate() {
 
 268         $mdProgressCircular.easeFnIndeterminate,
 
 269         $mdProgressCircular.durationIndeterminate,
 
 274       // The %4 technically isn't necessary, but it keeps the rotation
 
 275       // under 360, instead of becoming a crazy large number.
 
 276       iterationCount = ++iterationCount % 4;
 
 280     function startIndeterminateAnimation() {
 
 282         // Note that this interval isn't supposed to trigger a digest.
 
 283         interval = $interval(
 
 284           animateIndeterminate,
 
 285           $mdProgressCircular.durationIndeterminate,
 
 290         animateIndeterminate();
 
 293           .addClass(INDETERMINATE_CLASS)
 
 294           .removeAttr('aria-valuenow');
 
 298     function cleanupIndeterminateAnimation() {
 
 300         $interval.cancel(interval);
 
 302         element.removeClass(INDETERMINATE_CLASS);
 
 308    * Returns SVG path data for progress circle
 
 309    * Syntax spec: https://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
 
 311    * @param {number} diameter Diameter of the container.
 
 312    * @param {number} strokeWidth Stroke width to be used when drawing circle
 
 313    * @param {boolean} indeterminate Use if progress circle will be used for indeterminate
 
 315    * @returns {string} String representation of an SVG arc.
 
 317   function getSvgArc(diameter, strokeWidth, indeterminate) {
 
 318     var radius = diameter / 2;
 
 319     var offset = strokeWidth / 2;
 
 320     var start = radius + ',' + offset; // ie: (25, 2.5) or 12 o'clock
 
 321     var end = offset + ',' + radius;   // ie: (2.5, 25) or  9 o'clock
 
 322     var arcRadius = radius - offset;
 
 324          + 'A' + arcRadius + ',' + arcRadius + ' 0 1 1 ' + end // 75% circle
 
 325          + (indeterminate ? '' : 'A' + arcRadius + ',' + arcRadius + ' 0 0 1 ' + start); // loop to start
 
 329    * Return stroke length for progress circle
 
 331    * @param {number} diameter Diameter of the container.
 
 332    * @param {number} strokeWidth Stroke width to be used when drawing circle
 
 333    * @param {number} value Percentage of circle (between 0 and 100)
 
 334    * @param {number} limit Max percentage for circle
 
 336    * @returns {number} Stroke length for progres circle
 
 338   function getDashLength(diameter, strokeWidth, value, limit) {
 
 339     return (diameter - strokeWidth) * $window.Math.PI * ( (3 * (limit || 100) / 100) - (value/100) );
 
 343    * Limits a value between 0 and 100.
 
 345   function clamp(value) {
 
 346     return $window.Math.max(0, $window.Math.min(value || 0, 100));
 
 350    * Determines the size of a progress circle, based on the provided
 
 351    * value in the following formats: `X`, `Ypx`, `Z%`.
 
 353   function getSize(value) {
 
 354     var defaultValue = $mdProgressCircular.progressSize;
 
 357       var parsed = parseFloat(value);
 
 359       if (value.lastIndexOf('%') === value.length - 1) {
 
 360         parsed = (parsed / 100) * defaultValue;
 
 370    * Determines the circle's stroke width, based on
 
 371    * the provided diameter.
 
 373   function getStroke(diameter) {
 
 374     return $mdProgressCircular.strokeWidth / 100 * diameter;
 
 381  * @name $mdProgressCircular
 
 382  * @module material.components.progressCircular
 
 385  * Allows the user to specify the default options for the `progressCircular` directive.
 
 387  * @property {number} progressSize Diameter of the progress circle in pixels.
 
 388  * @property {number} strokeWidth Width of the circle's stroke as a percentage of the circle's size.
 
 389  * @property {number} duration Length of the circle animation in milliseconds.
 
 390  * @property {function} easeFn Default easing animation function.
 
 391  * @property {object} easingPresets Collection of pre-defined easing functions.
 
 393  * @property {number} durationIndeterminate Duration of the indeterminate animation.
 
 394  * @property {number} startIndeterminate Indeterminate animation start point.
 
 395  * @property {number} endIndeterminate Indeterminate animation end point.
 
 396  * @property {function} easeFnIndeterminate Easing function to be used when animating
 
 397  * between the indeterminate values.
 
 399  * @property {(function(object): object)} configure Used to modify the default options.
 
 403  *   myAppModule.config(function($mdProgressCircularProvider) {
 
 405  *     // Example of changing the default progress options.
 
 406  *     $mdProgressCircularProvider.configure({
 
 417   .module('material.components.progressCircular')
 
 418   .provider("$mdProgressCircular", MdProgressCircularProvider);
 
 420 function MdProgressCircularProvider() {
 
 421   var progressConfig = {
 
 427     durationIndeterminate: 1333,
 
 428     startIndeterminate: 1,
 
 429     endIndeterminate: 149,
 
 430     easeFnIndeterminate: materialEase,
 
 433       linearEase: linearEase,
 
 434       materialEase: materialEase
 
 439     configure: function(options) {
 
 440       progressConfig = angular.extend(progressConfig, options || {});
 
 441       return progressConfig;
 
 443     $get: function() { return progressConfig; }
 
 446   function linearEase(t, b, c, d) {
 
 447     return c * t / d + b;
 
 450   function materialEase(t, b, c, d) {
 
 451     // via http://www.timotheegroleau.com/Flash/experiments/easing_function_generator.htm
 
 452     // with settings of [0, 0, 1, 1]
 
 453     var ts = (t /= d) * t;
 
 455     return b + c * (6 * tc * ts + -15 * ts * ts + 10 * tc);
 
 459 ngmaterial.components.progressCircular = angular.module("material.components.progressCircular");