spin.js 9.8 KB

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