in modules/js/progressCircular/progressCircular.js [69:419]
function MdProgressCircularDirective($window, $mdProgressCircular, $mdTheming,
$mdUtil, $interval, $log) {
// Note that this shouldn't use use $$rAF, because it can cause an infinite loop
// in any tests that call $animate.flush.
var rAF = $window.requestAnimationFrame ||
$window.webkitRequestAnimationFrame ||
angular.noop;
var cAF = $window.cancelAnimationFrame ||
$window.webkitCancelAnimationFrame ||
$window.webkitCancelRequestAnimationFrame ||
angular.noop;
var MODE_DETERMINATE = 'determinate';
var MODE_INDETERMINATE = 'indeterminate';
var DISABLED_CLASS = '_md-progress-circular-disabled';
var INDETERMINATE_CLASS = 'md-mode-indeterminate';
return {
restrict: 'E',
scope: {
value: '@',
mdDiameter: '@',
mdMode: '@'
},
template:
'<svg xmlns="http://www.w3.org/2000/svg">' +
'<path fill="none"/>' +
'</svg>',
compile: function(element, attrs) {
element.attr({
'aria-valuemin': 0,
'aria-valuemax': 100,
'role': 'progressbar'
});
if (angular.isUndefined(attrs.mdMode)) {
var mode = attrs.hasOwnProperty('value') ? MODE_DETERMINATE : MODE_INDETERMINATE;
attrs.$set('mdMode', mode);
} else {
attrs.$set('mdMode', attrs.mdMode.trim());
}
return MdProgressCircularLink;
}
};
function MdProgressCircularLink(scope, element, attrs) {
var node = element[0];
var svg = angular.element(node.querySelector('svg'));
var path = angular.element(node.querySelector('path'));
var startIndeterminate = $mdProgressCircular.startIndeterminate;
var endIndeterminate = $mdProgressCircular.endIndeterminate;
var iterationCount = 0;
var lastAnimationId = 0;
var lastDrawFrame;
var interval;
$mdTheming(element);
element.toggleClass(DISABLED_CLASS, attrs.hasOwnProperty('disabled'));
// If the mode is indeterminate, it doesn't need to
// wait for the next digest. It can start right away.
if (scope.mdMode === MODE_INDETERMINATE){
startIndeterminateAnimation();
}
scope.$on('$destroy', function(){
cleanupIndeterminateAnimation();
if (lastDrawFrame) {
cAF(lastDrawFrame);
}
});
scope.$watchGroup(['value', 'mdMode', function() {
var isDisabled = node.disabled;
// Sometimes the browser doesn't return a boolean, in
// which case we should check whether the attribute is
// present.
if (isDisabled === true || isDisabled === false){
return isDisabled;
}
return angular.isDefined(element.attr('disabled'));
}], function(newValues, oldValues) {
var mode = newValues[1];
var isDisabled = newValues[2];
var wasDisabled = oldValues[2];
var diameter = 0;
var strokeWidth = 0;
if (isDisabled !== wasDisabled) {
element.toggleClass(DISABLED_CLASS, !!isDisabled);
}
if (isDisabled) {
cleanupIndeterminateAnimation();
} else {
if (mode !== MODE_DETERMINATE && mode !== MODE_INDETERMINATE) {
mode = MODE_INDETERMINATE;
attrs.$set('mdMode', mode);
}
if (mode === MODE_INDETERMINATE) {
if (oldValues[1] === MODE_DETERMINATE) {
diameter = getSize(scope.mdDiameter);
strokeWidth = getStroke(diameter);
path.attr('d', getSvgArc(diameter, strokeWidth, true));
path.attr('stroke-dasharray', getDashLength(diameter, strokeWidth, 75));
}
startIndeterminateAnimation();
} else {
var newValue = clamp(newValues[0]);
var oldValue = clamp(oldValues[0]);
cleanupIndeterminateAnimation();
if (oldValues[1] === MODE_INDETERMINATE) {
diameter = getSize(scope.mdDiameter);
strokeWidth = getStroke(diameter);
path.attr('d', getSvgArc(diameter, strokeWidth, false));
path.attr('stroke-dasharray', getDashLength(diameter, strokeWidth, 100));
}
element.attr('aria-valuenow', newValue);
renderCircle(oldValue, newValue);
}
}
});
// This is in a separate watch in order to avoid layout, unless
// the value has actually changed.
scope.$watch('mdDiameter', function(newValue) {
var diameter = getSize(newValue);
var strokeWidth = getStroke(diameter);
var value = clamp(scope.value);
var transformOrigin = (diameter / 2) + 'px';
var dimensions = {
width: diameter + 'px',
height: diameter + 'px'
};
// The viewBox has to be applied via setAttribute, because it is
// case-sensitive. If jQuery is included in the page, `.attr` lowercases
// all attribute names.
svg[0].setAttribute('viewBox', '0 0 ' + diameter + ' ' + diameter);
// Usually viewBox sets the dimensions for the SVG, however that doesn't
// seem to be the case on IE10.
// Important! The transform origin has to be set from here and it has to
// be in the format of "Ypx Ypx Ypx", otherwise the rotation wobbles in
// IE and Edge, because they don't account for the stroke width when
// rotating. Also "center" doesn't help in this case, it has to be a
// precise value.
svg
.css(dimensions)
.css('transform-origin', transformOrigin + ' ' + transformOrigin + ' ' + transformOrigin);
element.css(dimensions);
path.attr('stroke-width', strokeWidth);
path.attr('stroke-linecap', 'square');
if (scope.mdMode == MODE_INDETERMINATE) {
path.attr('d', getSvgArc(diameter, strokeWidth, true));
path.attr('stroke-dasharray', getDashLength(diameter, strokeWidth, 75));
path.attr('stroke-dashoffset', getDashOffset(diameter, strokeWidth, 1, 75));
} else {
path.attr('d', getSvgArc(diameter, strokeWidth, false));
path.attr('stroke-dasharray', getDashLength(diameter, strokeWidth, 100));
path.attr('stroke-dashoffset', getDashOffset(diameter, strokeWidth, 0, 100));
renderCircle(value, value);
}
});
function renderCircle(animateFrom, animateTo, easing, duration, iterationCount, maxValue) {
var id = ++lastAnimationId;
var startTime = $mdUtil.now();
var changeInValue = animateTo - animateFrom;
var diameter = getSize(scope.mdDiameter);
var strokeWidth = getStroke(diameter);
var ease = easing || $mdProgressCircular.easeFn;
var animationDuration = duration || $mdProgressCircular.duration;
var rotation = -90 * (iterationCount || 0);
var dashLimit = maxValue || 100;
// No need to animate it if the values are the same
if (animateTo === animateFrom) {
renderFrame(animateTo);
} else {
lastDrawFrame = rAF(function animation() {
var currentTime = $window.Math.max(0, $window.Math.min($mdUtil.now() - startTime, animationDuration));
renderFrame(ease(currentTime, animateFrom, changeInValue, animationDuration));
// Do not allow overlapping animations
if (id === lastAnimationId && currentTime < animationDuration) {
lastDrawFrame = rAF(animation);
}
});
}
function renderFrame(value) {
path.attr('stroke-dashoffset', getDashOffset(diameter, strokeWidth, value, dashLimit));
path.attr('transform','rotate(' + (rotation) + ' ' + diameter/2 + ' ' + diameter/2 + ')');
}
}
function animateIndeterminate() {
renderCircle(
startIndeterminate,
endIndeterminate,
$mdProgressCircular.easeFnIndeterminate,
$mdProgressCircular.durationIndeterminate,
iterationCount,
75
);
// The %4 technically isn't necessary, but it keeps the rotation
// under 360, instead of becoming a crazy large number.
iterationCount = ++iterationCount % 4;
}
function startIndeterminateAnimation() {
if (!interval) {
// Note that this interval isn't supposed to trigger a digest.
interval = $interval(
animateIndeterminate,
$mdProgressCircular.durationIndeterminate,
0,
false
);
animateIndeterminate();
element
.addClass(INDETERMINATE_CLASS)
.removeAttr('aria-valuenow');
}
}
function cleanupIndeterminateAnimation() {
if (interval) {
$interval.cancel(interval);
interval = null;
element.removeClass(INDETERMINATE_CLASS);
}
}
}
/**
* Returns SVG path data for progress circle
* Syntax spec: https://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
*
* @param {number} diameter Diameter of the container.
* @param {number} strokeWidth Stroke width to be used when drawing circle
* @param {boolean} indeterminate Use if progress circle will be used for indeterminate
*
* @returns {string} String representation of an SVG arc.
*/
function getSvgArc(diameter, strokeWidth, indeterminate) {
var radius = diameter / 2;
var offset = strokeWidth / 2;
var start = radius + ',' + offset; // ie: (25, 2.5) or 12 o'clock
var end = offset + ',' + radius; // ie: (2.5, 25) or 9 o'clock
var arcRadius = radius - offset;
return 'M' + start
+ 'A' + arcRadius + ',' + arcRadius + ' 0 1 1 ' + end // 75% circle
+ (indeterminate ? '' : 'A' + arcRadius + ',' + arcRadius + ' 0 0 1 ' + start); // loop to start
}
/**
* Return stroke length for progress circle
*
* @param {number} diameter Diameter of the container.
* @param {number} strokeWidth Stroke width to be used when drawing circle
* @param {number} value Percentage of circle (between 0 and 100)
* @param {number} maxArcLength Maximum length of arc as a percentage of circle (between 0 and 100)
*
* @returns {number} Stroke length for progress circle
*/
function getDashOffset(diameter, strokeWidth, value, maxArcLength) {
return getSpinnerCircumference(diameter, strokeWidth) * ((maxArcLength - value) / 100);
}
/**
* Limits a value between 0 and 100.
*/
function clamp(value) {
return $window.Math.max(0, $window.Math.min(value || 0, 100));
}
/**
* Determines the size of a progress circle, based on the provided
* value in the following formats: `X`, `Ypx`, `Z%`.
*/
function getSize(value) {
var defaultValue = $mdProgressCircular.progressSize;
if (value) {
var parsed = parseFloat(value);
if (value.lastIndexOf('%') === value.length - 1) {
parsed = (parsed / 100) * defaultValue;
}
return parsed;
}
return defaultValue;
}
/**
* Determines the circle's stroke width, based on
* the provided diameter.
*/
function getStroke(diameter) {
return $mdProgressCircular.strokeWidth / 100 * diameter;
}
/**
* Return length of the dash
*
* @param {number} diameter Diameter of the container.
* @param {number} strokeWidth Stroke width to be used when drawing circle
* @param {number} value Percentage of circle (between 0 and 100)
*
* @returns {number} Length of the dash
*/
function getDashLength(diameter, strokeWidth, value) {
return getSpinnerCircumference(diameter, strokeWidth) * (value / 100);
}
/**
* Return circumference of the spinner
*
* @param {number} diameter Diameter of the container.
* @param {number} strokeWidth Stroke width to be used when drawing circle
*
* @returns {number} Circumference of the spinner
*/
function getSpinnerCircumference(diameter, strokeWidth) {
return ((diameter - strokeWidth) * $window.Math.PI);
}
}