Source: deliteful/ProgressIndicator.js

/** @module deliteful/ProgressIndicator */
define([
	"dcl/dcl",
	"delite/hc",
	"delite/register",
	"delite/Widget",
	"delite/handlebars!./ProgressIndicator/ProgressIndicator.html",
	"delite/theme!./ProgressIndicator/themes/{{theme}}/ProgressIndicator.css"
], function (dcl, has, register, Widget, template) {
	/**
	 * A widget that displays a round spinning graphical representation that indicates that a task is ongoing.
	 *
	 * This widget starts hidden and the spinning animation starts when the widget becomes visible. Default widget
	 * size is 40x40px.
	 *
	 * @example <caption>Set the "active" property to true to make the widget visible when it starts.</caption>
	 * <d-progress-indicator active="true"></d-progress-indicator>
	 *
	 * @example <caption>Use style properties "width" and "height" to customize the widget size</caption>
	 * <d-progress-indicator active="true" style="width: 100%; height: 100%"></d-progress-indicator>
	 *
	 * @class module:deliteful/ProgressIndicator
	 * @augments module:delite/Widget
	 */
	return register("d-progress-indicator", [HTMLElement, Widget],
		/** @lends module:deliteful/ProgressIndicator# */ {

		/**
		 * Set to false to hide the widget and stop any ongoing animation.
		 * Set to true to show the widget: animation automatically starts unless you set a number to the "value"
		 * property.
		 * @member {boolean}
		 * @default false
		 */
		active: false,

		/**
		 * A value from 0 to 100 that indicates a percentage of progression of an ongoing task.
		 * Set the value to NaN to hide the number and start the spinning animation. Negative values are converted to 0
		 * and values over 100 are converted to 100.
		 * @member {number}
		 * @default NaN
		 */
		value: NaN,

		/**
		 * The relative speed of the spinning animation.
		 * Accepted values are "slow", "normal" and "fast". Other values are converted to "normal". Note that the
		 * actual/real speed of the animation depends of the device/os/browser capabilities.
		 * @member {string}
		 * @default "normal"
		 */
		speed: "normal",

		/**
		 * The name of the CSS class of this widget.
		 * @member {string}
		 * @default "d-progress-indicator"
		 */
		baseClass: "d-progress-indicator",

		/* internal properties */
		_requestId: 0, //request animation id or clearTimeout param
		_lapsTime: 1000, //duration of an animation revolution in milliseconds
		_requestAnimationFunction: (
			(window.requestAnimationFrame && window.requestAnimationFrame.bind(window)) || // standard
			(window.webkitRequestAnimationFrame && window.webkitRequestAnimationFrame.bind(window)) || // webkit
			function (callBack) {// others (ie9)
				return this.defer(callBack, 1000 / 60);
			}),
		_cancelAnimationFunction: (
			window.cancelAnimationFrame || //standard
			window.webkitCancelRequestAnimationFrame || // webkit
			function (handle) {// others (ie9)
				handle.remove();
			}).bind(window),

		/* internal methods */
		_requestRendering: function (animationFrame) {
			//browser agnostic animation frame renderer
			//return a request id
			return this._requestAnimationFunction.call(this, animationFrame);//call on this to match this.defer
		},

		_cancelRequestRendering: function (requestId) {
			//browser agnostic animation frame canceler
			return this._cancelAnimationFunction(requestId);
		},

		_reset: function () {
			//reset text and opacity.
			//ensure that any pending frame animation request is done before doing the actual reset
			this._requestRendering(
				function () {
					//remove any displayed value
					this.msgNode.textContent = "";
					//reset the opacity
					for (var i = 0; i < 12; i++) {
						this.lineNodeList[i].style.opacity = (i + 1) * (1 / 12);
					}
				}.bind(this));
		},

		_stopAnimation: function () {
			//stops the animation (if already started)
			if (this._requestId) {
				this._cancelRequestRendering(this._requestId);
				this._requestId = 0;
			}
		},

		_startAnimation: function () {
			//starts the animation (if not already started)
			if (this._requestId) {
				//animation is already ongoing
				return;
			}
			//restore initial opacity and remove text
			this._reset();
			//compute the amount of opacity to subtract at each frame, on each line.
			//note: 16.7 is the average animation frame refresh interval in ms (~60FPS)
			var delta = 16.7 / this._lapsTime;
			//round spinning animation routine
			var frameAnimation = function () {
				//set lines opacity
				for (var i = 0, opacity; i < 12; i++) {
					opacity = (parseFloat(this.lineNodeList[i].style.opacity) - delta) % 1;
					this.lineNodeList[i].style.opacity = (opacity < 0) ? 1 : opacity;
				}
				//render the next frame
				this._requestId = this._requestRendering(frameAnimation);
			}.bind(this);
			//start the animation
			this._requestId = this._requestRendering(frameAnimation);
		},

		template: template,

		postRender: function () {
			this.lineNodeList = this.linesNode.querySelectorAll("line");
			var symbolId = this.baseClass + "-" + this.widgetId + "-symbol";
			this.symbolNode.id = symbolId;
			//set unique SVG symbol id
			this.useNode.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", "#" + symbolId);
			//set non-overridable styles
			this.svgNode.style.width = "100%";
			this.svgNode.style.height = "100%";
			this.svgNode.style.textAnchor = "middle";

			//a11y high contrast:
			//widget color is declared on svg line nodes (stroke) and text node (fill).
			//Unlike the color style property, stroke and fill are not updated by the browser when windows high contrast
			//mode is enabled. To ensure the widget is visible when high contrast mode is enabled,
			//we set the color property on the root node and check if it is forced by the browser. In such case we
			//force the stroke and fill values to reflect the high contrast color.
			var hcColor = has("highcontrast");
			if (hcColor) {
				this.linesNode.style.stroke = hcColor; // text value color
				this.msgNode.style.fill = hcColor; // lines color
				//android chrome 31.0.1650.59 hack: force to refresh text color otherwise color doesn't change.
				this.msgNode.textContent = this.msgNode.textContent;
			}

			//set initial widget appearance
			this._reset();
		},

		computeProperties: function (props) {
			var correctedValue = null;
			if ("speed" in props) {
				//fast: 500ms
				//slow: 2000ms
				//normal: 1000ms (also default and fallback value)
				correctedValue = (this.speed === "fast") ? 500:(this.speed === "slow") ? 2000:1000;
				if (this._lapsTime !== correctedValue) {
					this._lapsTime = correctedValue;
				}
			}
			if ("value" in props && !isNaN(this.value)) {
				correctedValue = Math.max(Math.min(this.value, 100), 0);
				if (this.value !== correctedValue) {
					this.value = correctedValue;
				}
			}
		},

		refreshRendering: function (props) {
			//refresh value
			if ("value" in props) {
				if (isNaN(this.value)) {
					//NaN: start the animation
					if (this.active) {
						this._startAnimation();
					}
				} else {
					//ensure any ongoing animation stops
					this._stopAnimation();
					//ensure pending frame animation requests are done before any updates
					this._requestRendering(function () {
						//display the integer value
						this.msgNode.textContent = Math.floor(this.value);
						//minimum amount of opacity.
						var minOpacity = 0.2;
						//update lines opacity
						for (var i = 0, opacity; i < 12; i++) {
							opacity = Math.min(Math.max((this.value * 0.12 - i), 0), 1) * (1 - minOpacity);
							this.lineNodeList[i].style.opacity = minOpacity + opacity;
						}
					}.bind(this));
				}

			}
			//refresh speed
			if ("speed" in props) {
				//if animation is ongoing, restart the animation to take the new speed into account
				if (this._requestId) {
					this._stopAnimation();
					this._startAnimation();
				}
			}
			//refresh active
			if ("active" in props) {
				if (this.active) {
					if (isNaN(this.value)) {
						//NaN: start the animation
						this._startAnimation();
					}
				} else {
					this._stopAnimation();
				}
				//set visibility in frame to be in sync with opacity/text changes.
				//Avoids mis-display when setting visibility=visible just after value=0.
				this._requestRendering(function () {
					this.style.visibility = this.active ? "visible" : "hidden";
				}.bind(this));
			}
			
		},

		destroy: function () {
			this._stopAnimation();
		}
	});
});