-
Notifications
You must be signed in to change notification settings - Fork 36
/
ProgressIndicator.js
242 lines (224 loc) · 8.2 KB
/
ProgressIndicator.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
/** @module deliteful/ProgressIndicator */
define([
"dcl/dcl",
"delite/hc",
"delite/register",
"delite/Widget",
"delite/handlebars!./ProgressIndicator/ProgressIndicator.html",
"requirejs-dplugins/css!./ProgressIndicator/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,
afterInitializeRendering: function () {
this.lineNodeList = this.linesNode.querySelectorAll("line");
var symbolId = 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();
}
});
});