|
1 //fgnass.github.com/spin.js#v1.3 |
|
2 |
|
3 /** |
|
4 * Copyright (c) 2011-2013 Felix Gnass |
|
5 * Licensed under the MIT license |
|
6 */ |
|
7 (function(root, factory) { |
|
8 |
|
9 /* CommonJS */ |
|
10 if (typeof exports == 'object') module.exports = factory() |
|
11 |
|
12 /* AMD module */ |
|
13 else if (typeof define == 'function' && define.amd) define(factory) |
|
14 |
|
15 /* Browser global */ |
|
16 else root.Spinner = factory() |
|
17 } |
|
18 (this, function() { |
|
19 "use strict"; |
|
20 |
|
21 var prefixes = ['webkit', 'Moz', 'ms', 'O'] /* Vendor prefixes */ |
|
22 , animations = {} /* Animation rules keyed by their name */ |
|
23 , useCssAnimations /* Whether to use CSS animations or setTimeout */ |
|
24 |
|
25 /** |
|
26 * Utility function to create elements. If no tag name is given, |
|
27 * a DIV is created. Optionally properties can be passed. |
|
28 */ |
|
29 function createEl(tag, prop) { |
|
30 var el = document.createElement(tag || 'div') |
|
31 , n |
|
32 |
|
33 for(n in prop) el[n] = prop[n] |
|
34 return el |
|
35 } |
|
36 |
|
37 /** |
|
38 * Appends children and returns the parent. |
|
39 */ |
|
40 function ins(parent /* child1, child2, ...*/) { |
|
41 for (var i=1, n=arguments.length; i<n; i++) |
|
42 parent.appendChild(arguments[i]) |
|
43 |
|
44 return parent |
|
45 } |
|
46 |
|
47 /** |
|
48 * Insert a new stylesheet to hold the @keyframe or VML rules. |
|
49 */ |
|
50 var sheet = (function() { |
|
51 var el = createEl('style', {type : 'text/css'}) |
|
52 ins(document.getElementsByTagName('head')[0], el) |
|
53 return el.sheet || el.styleSheet |
|
54 }()) |
|
55 |
|
56 /** |
|
57 * Creates an opacity keyframe animation rule and returns its name. |
|
58 * Since most mobile Webkits have timing issues with animation-delay, |
|
59 * we create separate rules for each line/segment. |
|
60 */ |
|
61 function addAnimation(alpha, trail, i, lines) { |
|
62 var name = ['opacity', trail, ~~(alpha*100), i, lines].join('-') |
|
63 , start = 0.01 + i/lines * 100 |
|
64 , z = Math.max(1 - (1-alpha) / trail * (100-start), alpha) |
|
65 , prefix = useCssAnimations.substring(0, useCssAnimations.indexOf('Animation')).toLowerCase() |
|
66 , pre = prefix && '-' + prefix + '-' || '' |
|
67 |
|
68 if (!animations[name]) { |
|
69 sheet.insertRule( |
|
70 '@' + pre + 'keyframes ' + name + '{' + |
|
71 '0%{opacity:' + z + '}' + |
|
72 start + '%{opacity:' + alpha + '}' + |
|
73 (start+0.01) + '%{opacity:1}' + |
|
74 (start+trail) % 100 + '%{opacity:' + alpha + '}' + |
|
75 '100%{opacity:' + z + '}' + |
|
76 '}', sheet.cssRules.length) |
|
77 |
|
78 animations[name] = 1 |
|
79 } |
|
80 |
|
81 return name |
|
82 } |
|
83 |
|
84 /** |
|
85 * Tries various vendor prefixes and returns the first supported property. |
|
86 */ |
|
87 function vendor(el, prop) { |
|
88 var s = el.style |
|
89 , pp |
|
90 , i |
|
91 |
|
92 if(s[prop] !== undefined) return prop |
|
93 prop = prop.charAt(0).toUpperCase() + prop.slice(1) |
|
94 for(i=0; i<prefixes.length; i++) { |
|
95 pp = prefixes[i]+prop |
|
96 if(s[pp] !== undefined) return pp |
|
97 } |
|
98 } |
|
99 |
|
100 /** |
|
101 * Sets multiple style properties at once. |
|
102 */ |
|
103 function css(el, prop) { |
|
104 for (var n in prop) |
|
105 el.style[vendor(el, n)||n] = prop[n] |
|
106 |
|
107 return el |
|
108 } |
|
109 |
|
110 /** |
|
111 * Fills in default values. |
|
112 */ |
|
113 function merge(obj) { |
|
114 for (var i=1; i < arguments.length; i++) { |
|
115 var def = arguments[i] |
|
116 for (var n in def) |
|
117 if (obj[n] === undefined) obj[n] = def[n] |
|
118 } |
|
119 return obj |
|
120 } |
|
121 |
|
122 /** |
|
123 * Returns the absolute page-offset of the given element. |
|
124 */ |
|
125 function pos(el) { |
|
126 var o = { x:el.offsetLeft, y:el.offsetTop } |
|
127 while((el = el.offsetParent)) |
|
128 o.x+=el.offsetLeft, o.y+=el.offsetTop |
|
129 |
|
130 return o |
|
131 } |
|
132 |
|
133 // Built-in defaults |
|
134 |
|
135 var defaults = { |
|
136 lines: 12, // The number of lines to draw |
|
137 length: 7, // The length of each line |
|
138 width: 5, // The line thickness |
|
139 radius: 10, // The radius of the inner circle |
|
140 rotate: 0, // Rotation offset |
|
141 corners: 1, // Roundness (0..1) |
|
142 color: '#000', // #rgb or #rrggbb |
|
143 direction: 1, // 1: clockwise, -1: counterclockwise |
|
144 speed: 1, // Rounds per second |
|
145 trail: 100, // Afterglow percentage |
|
146 opacity: 1/4, // Opacity of the lines |
|
147 fps: 20, // Frames per second when using setTimeout() |
|
148 zIndex: 2e9, // Use a high z-index by default |
|
149 className: 'spinner', // CSS class to assign to the element |
|
150 top: 'auto', // center vertically |
|
151 left: 'auto', // center horizontally |
|
152 position: 'relative' // element position |
|
153 } |
|
154 |
|
155 /** The constructor */ |
|
156 function Spinner(o) { |
|
157 if (typeof this == 'undefined') return new Spinner(o) |
|
158 this.opts = merge(o || {}, Spinner.defaults, defaults) |
|
159 } |
|
160 |
|
161 // Global defaults that override the built-ins: |
|
162 Spinner.defaults = {} |
|
163 |
|
164 merge(Spinner.prototype, { |
|
165 |
|
166 /** |
|
167 * Adds the spinner to the given target element. If this instance is already |
|
168 * spinning, it is automatically removed from its previous target b calling |
|
169 * stop() internally. |
|
170 */ |
|
171 spin: function(target) { |
|
172 this.stop() |
|
173 |
|
174 var self = this |
|
175 , o = self.opts |
|
176 , el = self.el = css(createEl(0, {className: o.className}), {position: o.position, width: 0, zIndex: o.zIndex}) |
|
177 , mid = o.radius+o.length+o.width |
|
178 , ep // element position |
|
179 , tp // target position |
|
180 |
|
181 if (target) { |
|
182 target.insertBefore(el, target.firstChild||null) |
|
183 tp = pos(target) |
|
184 ep = pos(el) |
|
185 css(el, { |
|
186 left: (o.left == 'auto' ? tp.x-ep.x + (target.offsetWidth >> 1) : parseInt(o.left, 10) + mid) + 'px', |
|
187 top: (o.top == 'auto' ? tp.y-ep.y + (target.offsetHeight >> 1) : parseInt(o.top, 10) + mid) + 'px' |
|
188 }) |
|
189 } |
|
190 |
|
191 el.setAttribute('role', 'progressbar') |
|
192 self.lines(el, self.opts) |
|
193 |
|
194 if (!useCssAnimations) { |
|
195 // No CSS animation support, use setTimeout() instead |
|
196 var i = 0 |
|
197 , start = (o.lines - 1) * (1 - o.direction) / 2 |
|
198 , alpha |
|
199 , fps = o.fps |
|
200 , f = fps/o.speed |
|
201 , ostep = (1-o.opacity) / (f*o.trail / 100) |
|
202 , astep = f/o.lines |
|
203 |
|
204 ;(function anim() { |
|
205 i++; |
|
206 for (var j = 0; j < o.lines; j++) { |
|
207 alpha = Math.max(1 - (i + (o.lines - j) * astep) % f * ostep, o.opacity) |
|
208 |
|
209 self.opacity(el, j * o.direction + start, alpha, o) |
|
210 } |
|
211 self.timeout = self.el && setTimeout(anim, ~~(1000/fps)) |
|
212 })() |
|
213 } |
|
214 return self |
|
215 }, |
|
216 |
|
217 /** |
|
218 * Stops and removes the Spinner. |
|
219 */ |
|
220 stop: function() { |
|
221 var el = this.el |
|
222 if (el) { |
|
223 clearTimeout(this.timeout) |
|
224 if (el.parentNode) el.parentNode.removeChild(el) |
|
225 this.el = undefined |
|
226 } |
|
227 return this |
|
228 }, |
|
229 |
|
230 /** |
|
231 * Internal method that draws the individual lines. Will be overwritten |
|
232 * in VML fallback mode below. |
|
233 */ |
|
234 lines: function(el, o) { |
|
235 var i = 0 |
|
236 , start = (o.lines - 1) * (1 - o.direction) / 2 |
|
237 , seg |
|
238 |
|
239 function fill(color, shadow) { |
|
240 return css(createEl(), { |
|
241 position: 'absolute', |
|
242 width: (o.length+o.width) + 'px', |
|
243 height: o.width + 'px', |
|
244 background: color, |
|
245 boxShadow: shadow, |
|
246 transformOrigin: 'left', |
|
247 transform: 'rotate(' + ~~(360/o.lines*i+o.rotate) + 'deg) translate(' + o.radius+'px' +',0)', |
|
248 borderRadius: (o.corners * o.width>>1) + 'px' |
|
249 }) |
|
250 } |
|
251 |
|
252 for (; i < o.lines; i++) { |
|
253 seg = css(createEl(), { |
|
254 position: 'absolute', |
|
255 top: 1+~(o.width/2) + 'px', |
|
256 transform: o.hwaccel ? 'translate3d(0,0,0)' : '', |
|
257 opacity: o.opacity, |
|
258 animation: useCssAnimations && addAnimation(o.opacity, o.trail, start + i * o.direction, o.lines) + ' ' + 1/o.speed + 's linear infinite' |
|
259 }) |
|
260 |
|
261 if (o.shadow) ins(seg, css(fill('#000', '0 0 4px ' + '#000'), {top: 2+'px'})) |
|
262 |
|
263 ins(el, ins(seg, fill(o.color, '0 0 1px rgba(0,0,0,.1)'))) |
|
264 } |
|
265 return el |
|
266 }, |
|
267 |
|
268 /** |
|
269 * Internal method that adjusts the opacity of a single line. |
|
270 * Will be overwritten in VML fallback mode below. |
|
271 */ |
|
272 opacity: function(el, i, val) { |
|
273 if (i < el.childNodes.length) el.childNodes[i].style.opacity = val |
|
274 } |
|
275 |
|
276 }) |
|
277 |
|
278 |
|
279 function initVML() { |
|
280 |
|
281 /* Utility function to create a VML tag */ |
|
282 function vml(tag, attr) { |
|
283 return createEl('<' + tag + ' xmlns="urn:schemas-microsoft.com:vml" class="spin-vml">', attr) |
|
284 } |
|
285 |
|
286 // No CSS transforms but VML support, add a CSS rule for VML elements: |
|
287 sheet.addRule('.spin-vml', 'behavior:url(#default#VML)') |
|
288 |
|
289 Spinner.prototype.lines = function(el, o) { |
|
290 var r = o.length+o.width |
|
291 , s = 2*r |
|
292 |
|
293 function grp() { |
|
294 return css( |
|
295 vml('group', { |
|
296 coordsize: s + ' ' + s, |
|
297 coordorigin: -r + ' ' + -r |
|
298 }), |
|
299 { width: s, height: s } |
|
300 ) |
|
301 } |
|
302 |
|
303 var margin = -(o.width+o.length)*2 + 'px' |
|
304 , g = css(grp(), {position: 'absolute', top: margin, left: margin}) |
|
305 , i |
|
306 |
|
307 function seg(i, dx, filter) { |
|
308 ins(g, |
|
309 ins(css(grp(), {rotation: 360 / o.lines * i + 'deg', left: ~~dx}), |
|
310 ins(css(vml('roundrect', {arcsize: o.corners}), { |
|
311 width: r, |
|
312 height: o.width, |
|
313 left: o.radius, |
|
314 top: -o.width>>1, |
|
315 filter: filter |
|
316 }), |
|
317 vml('fill', {color: o.color, opacity: o.opacity}), |
|
318 vml('stroke', {opacity: 0}) // transparent stroke to fix color bleeding upon opacity change |
|
319 ) |
|
320 ) |
|
321 ) |
|
322 } |
|
323 |
|
324 if (o.shadow) |
|
325 for (i = 1; i <= o.lines; i++) |
|
326 seg(i, -2, 'progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)') |
|
327 |
|
328 for (i = 1; i <= o.lines; i++) seg(i) |
|
329 return ins(el, g) |
|
330 } |
|
331 |
|
332 Spinner.prototype.opacity = function(el, i, val, o) { |
|
333 var c = el.firstChild |
|
334 o = o.shadow && o.lines || 0 |
|
335 if (c && i+o < c.childNodes.length) { |
|
336 c = c.childNodes[i+o]; c = c && c.firstChild; c = c && c.firstChild |
|
337 if (c) c.opacity = val |
|
338 } |
|
339 } |
|
340 } |
|
341 |
|
342 var probe = css(createEl('group'), {behavior: 'url(#default#VML)'}) |
|
343 |
|
344 if (!vendor(probe, 'transform') && probe.adj) initVML() |
|
345 else useCssAnimations = vendor(probe, 'animation') |
|
346 |
|
347 return Spinner |
|
348 |
|
349 })); |