loada 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/loada.min.js ADDED
@@ -0,0 +1 @@
1
+ !function(){var a,b=function(a,b){return function(){return a.apply(b,arguments)}},c=[].slice;a=document.getElementsByTagName("head")[0],this.Loada=function(){function d(a,c){var d,e;if(this.set=a,this._loadGroup=b(this._loadGroup,this),this.options={prefix:"loada",localStorage:!0},this.requires={input:[],set:{},length:0},this.set||(this.set="*"),c)for(d in c)e=c[d],this.options[d]=e;this.key=""+this.options.prefix+"."+this.set,this.setup()}return d.prototype.Progress=function(){function a(a,b){this.count=a,this.progressCallback=b,this.data={}}return a.prototype.set=function(a,b){return this.data[a]=Math.min(b,100),"function"==typeof this.progressCallback?this.progressCallback(this.total()):void 0},a.prototype.total=function(){var a,b,c,d;c=0,d=this.data;for(a in d)b=d[a],c+=Math.round(100*(b/this.count))/100;return c},a}(),d.set=function(){return function(a,b,c){c.prototype=a.prototype;var d=new c,e=a.apply(d,b);return Object(e)===e?e:d}(this,arguments,function(){})},d.prototype.setup=function(){return this.options.localStorage&&(this.storage=localStorage[this.key]||{},"string"==typeof this.storage)?this.storage=JSON.parse(localStorage[this.key]):void 0},d.prototype.save=function(){return localStorage[this.key]=JSON.stringify(this.storage)},d.prototype.clear=function(){return delete localStorage[this.key]},d.prototype.get=function(a){var b;return null!=(b=this.storage[a])?b.source:void 0},d.prototype.expire=function(){var a,b,c,d,e,f,g,h,i=this;f=new Date,a=function(a){return a.expirationDate&&new Date(a.expirationDate)<=f},b=function(){return!i.requires.set[d]},c=function(a){return i.requires.set[d].revision!==a.revision},g=this.storage,h=[];for(d in g)e=g[d],a(e)||b(e)||c(e)?h.push(delete this.storage[d]):h.push(void 0);return h},d.prototype.require=function(){var a,b,d,e,f,g;for(a=1<=arguments.length?c.call(arguments,0):[],e=0,f=a.length;f>e;e++)b=a[e],b.key||(b.key=b.url),b.type||(b.type=null!=(g=b.url)?g.split(".").pop():void 0),null==b.localStorage&&(b.localStorage=!0),null==b.require&&(b.require=!0),b.expires&&(d=new Date,b.expirationDate=d.setTime(d.getTime()+1e3*60*60*b.expires)),"js"===b.type||"css"===b.type||"text"===b.type?this.requires.set[b.key]=b:console.error("Unknown asset type for "+b.url+" – skipped");return this.requires.length+=a.length,this.requires.input.push(a)},d.prototype.load=function(a){var b,c,d=this;return a||(a={}),this.options.localStorage&&this.expire(),c=new this.Progress(this.requires.length,a.progress),b=0,this._ensureSizes(null!=a.progress,function(){var e,f,g,h,i;for(h=d.requires.input,i=[],f=0,g=h.length;g>f;f++)e=h[f],b++,i.push(d._loadGroup(e.slice(0),c,function(){return b--,0===b?(d.save(),"function"==typeof a.success?a.success():void 0):void 0}));return i})},d.prototype._ensureSizes=function(a,b){var c,d,e,f,g,h,i=this;if(!a){f=this.requires.set;for(c in f)d=f[c],d.size=0;return b()}e=0,g=this.requires.set,h=[];for(c in g)d=g[c],h.push(function(a){return null==a.size?(e++,i._ajax("HEAD",a.url,function(c){var d;return e--,d=c.getResponseHeader("Content-Length"),d&&(d=parseInt(d)),a.size=d||0,0===e?b():void 0})):void 0}(d));return h},d.prototype._loadGroup=function(a,b,c){var d,e,f=this;return d=a.shift(),d?this.options.localStorage&&this.storage[d.key]&&d.localStorage?(null!=b&&b.set(d.key,100),this.storage[d.key].require&&this._inject(this.storage[d.key]),this._loadGroup(a,b,c)):(e=this.options.localStorage?"_loadAJAX":"_loadInline",this[e](d,b,function(){return f.options.localStorage&&(f.storage[d.key]=d),d.require&&f._inject(d),f._loadGroup(a,b,c)})):c()},d.prototype._loadAJAX=function(a,b,c){var d,e;return e=this._ajax("GET",a.url,function(e){return a.source=e.responseText,clearInterval(d),null!=b&&b.set(a.key,100),c()}),a.size>0?d=setInterval(function(){var c;return c=Math.round(100*100*(e.responseText.length/a.size))/100,null!=b?b.set(a.key,c):void 0},100):void 0},d.prototype._loadInline=function(b,c,d){var e,f,g;return"js"!==b.type?(console.error("Attempt to load something other than JS without localStorage."),console.error(""+b.url+" is not loaded!"),null!=c&&c.set(b.key,100),d()):(g=document.createElement("script"),e=!1,f=function(){return e||null!=this.readyState&&"loaded"!==this.readyState&&"complete"!==this.readyState?void 0:(e=!0,null!=c&&c.set(b.key,100),"function"==typeof d&&d(),g.onload=g.onreadystatechange=null)},g.onload=g.onreadystatechange=f,g.src=b.url,a.appendChild(g))},d.prototype._inject=function(b){var c,d;return"js"===b.type?(c=document.createElement("script"),c.defer=!0,c.text=b.source,a.appendChild(c)):"css"===b.type?(d=document.createElement("style"),d.innerHTML=b.source,a.appendChild(d)):void 0},d.prototype._ajax=function(a,b,c){var d;return d=window.XMLHttpRequest?new XMLHttpRequest:new ActiveXObject("Microsoft.XMLHTTP"),d.open(a,b,1),d.onreadystatechange=function(){return d.readyState>3?"function"==typeof c?c(d):void 0:void 0},d.send(),d},d}()}.call(this);
data/lib/loada.rb ADDED
@@ -0,0 +1,14 @@
1
+ require 'json'
2
+
3
+ module Loada
4
+ PACKAGE = File.expand_path("../../package.json", __FILE__)
5
+
6
+ # Converting semver to the notation compatible with rubygems
7
+ VERSION = JSON.parse(File.read(PACKAGE))['version'].gsub '-', '.'
8
+
9
+ def self.assets_paths
10
+ [
11
+ File.expand_path('../../src', __FILE__)
12
+ ]
13
+ end
14
+ end
data/loada.gemspec ADDED
@@ -0,0 +1,17 @@
1
+ require File.expand_path("../lib/loada.rb", __FILE__)
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "loada"
5
+ s.version = Loada::VERSION
6
+ s.platform = Gem::Platform::RUBY
7
+ s.summary = "The Loada"
8
+ s.email = "boris@staal.io"
9
+ s.homepage = "http://github.com/inossidabile/loada"
10
+ s.description = "A gem wrapper to include Loada via the asset pipeline"
11
+ s.authors = ['Boris Staal']
12
+
13
+ s.files = `git ls-files`.split("\n")
14
+ s.require_paths = ["lib"]
15
+
16
+ s.add_dependency 'sprockets'
17
+ end
data/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "loada",
3
+ "version": "0.1.3",
4
+ "description": "Browser assets preloader with the support of localStorage-based caching and progress tracking",
5
+ "repository": "",
6
+ "keywords": [
7
+ "browser",
8
+ "preloader",
9
+ "assets"
10
+ ],
11
+ "author": "Boris Staal",
12
+ "license": "MIT",
13
+ "devDependencies": {
14
+ "grunt": "~0.4.1",
15
+ "grunt-contrib-coffee": "~0.7.0",
16
+ "grunt-contrib-jasmine": "~0.5.1",
17
+ "grunt-contrib-connect": "~0.3.0",
18
+ "grunt-release": "~0.3.5",
19
+ "grunt-coffeelint": "0.0.6",
20
+ "grunt-contrib-watch": "~0.4.4",
21
+ "bower": "~0.9.2",
22
+ "grunt-contrib-uglify": "~0.2.2"
23
+ }
24
+ }
@@ -0,0 +1,2 @@
1
+ afterEach ->
2
+ localStorage.clear()
@@ -0,0 +1,318 @@
1
+ describe "Loada", ->
2
+
3
+ it "initializes properly", ->
4
+ set = Loada.set(undefined, localStorage: false)
5
+ expect(set.set).toEqual '*'
6
+ expect(set.options.localStorage).toBeFalsy()
7
+ expect(set.storage).toBeUndefined()
8
+
9
+ localStorage['loada.foo'] = JSON.stringify('foo': 'bar')
10
+
11
+ set = Loada.set 'foo'
12
+ expect(set.set).toEqual 'foo'
13
+ expect(set.options.localStorage).toBeTruthy()
14
+ expect(set.storage).toEqual {'foo': 'bar'}
15
+
16
+ describe "expiration", ->
17
+ it "cuts by date", ->
18
+ expired = new Date
19
+ unexpired = new Date
20
+ unexpired.setTime(unexpired.getTime() + 60*1000)
21
+
22
+ libraries =
23
+ 'foo.js': {expirationDate: expired}
24
+ 'bar.js': {expirationDate: unexpired}
25
+ localStorage['loada.*'] = JSON.stringify libraries
26
+
27
+ set = Loada.set()
28
+ set.require url: 'foo.js'
29
+ set.require url: 'bar.js'
30
+
31
+ expect(set.storage['foo.js']).toBeDefined()
32
+ expect(set.storage['bar.js']).toBeDefined()
33
+
34
+ set.expire()
35
+ expect(set.storage['foo.js']).toBeUndefined()
36
+ expect(set.storage['bar.js']).toBeDefined()
37
+
38
+ it "cuts by existance", ->
39
+ localStorage['loada.*'] = JSON.stringify { 'foo.js': {}, 'bar.js': {} }
40
+
41
+ set = Loada.set()
42
+ set.require url: 'bar.js'
43
+
44
+ expect(set.storage['foo.js']).toBeDefined()
45
+ expect(set.storage['bar.js']).toBeDefined()
46
+
47
+ set.expire()
48
+ expect(set.storage['foo.js']).toBeUndefined()
49
+ expect(set.storage['bar.js']).toBeDefined()
50
+
51
+ it "cuts by revision", ->
52
+ localStorage['loada.*'] = JSON.stringify
53
+ 'foo.js': {revision: 1}
54
+ 'bar.js': {}
55
+ 'baz.js': {}
56
+
57
+ set = Loada.set()
58
+ set.require url: 'foo.js', revision: 1
59
+ set.require url: 'bar.js', revision: 1
60
+ set.require url: 'baz.js'
61
+
62
+ expect(set.storage['foo.js']).toBeDefined()
63
+ expect(set.storage['bar.js']).toBeDefined()
64
+ expect(set.storage['baz.js']).toBeDefined()
65
+
66
+ set.expire()
67
+ expect(set.storage['foo.js']).toBeDefined()
68
+ expect(set.storage['bar.js']).toBeUndefined()
69
+ expect(set.storage['baz.js']).toBeDefined()
70
+
71
+ describe "size ensurer", ->
72
+ beforeEach ->
73
+ @set = Loada.set()
74
+ @set.require url: 'foo.js', size: 100
75
+ @set.require url: 'bar.js'
76
+ @set.require url: 'baz.js'
77
+
78
+ it "zerofills with no progress tracking", ->
79
+ callback = sinon.spy()
80
+ @set._ensureSizes false, callback
81
+ expect(callback.callCount).toEqual 1
82
+ expect(@set.requires.set['foo.js'].size).toEqual 0
83
+
84
+ it "gets sizes with progress tracking", ->
85
+ callback = sinon.spy()
86
+ server = sinon.fakeServer.create()
87
+ @set._ensureSizes true, callback
88
+
89
+ waits 0
90
+
91
+ runs ->
92
+ expect(server.requests[0].url).toEqual 'bar.js'
93
+ expect(server.requests[1].url).toEqual 'baz.js'
94
+
95
+ server.requests[0].respond 200, {'Content-Length': '100'}, ''
96
+ server.requests[1].respond 200, {}, ''
97
+ server.restore()
98
+
99
+ waits 0
100
+
101
+ runs ->
102
+ expect(callback.callCount).toEqual 1
103
+ expect(@set.requires.set['foo.js'].size).toEqual 100
104
+ expect(@set.requires.set['bar.js'].size).toEqual 100
105
+ expect(@set.requires.set['baz.js'].size).toEqual(0)
106
+
107
+ describe "loader", ->
108
+ beforeEach ->
109
+ @server = sinon.fakeServer.create()
110
+ @set = Loada.set()
111
+ sinon.stub @set, '_inject'
112
+
113
+ afterEach ->
114
+ @set._inject.restore()
115
+ @server.restore()
116
+
117
+ it "gets single from cache", ->
118
+ localStorage['loada.*'] = JSON.stringify
119
+ 'foo.js': {require: true}
120
+
121
+ @set.setup()
122
+ callback = sinon.spy()
123
+ @set.require url: 'foo.js'
124
+
125
+ @set._loadGroup @set.requires.input[0], null, callback
126
+
127
+ waits 0
128
+
129
+ runs ->
130
+ expect(@server.requests.length).toEqual 0
131
+ expect(callback.callCount).toEqual 1
132
+ expect(@set._inject.callCount).toEqual 1
133
+ expect(@set._inject.args[0][0]).toEqual {require: true}
134
+
135
+ it "gets single from net", ->
136
+ callback = sinon.spy()
137
+ @set.require url: 'foo.js'
138
+
139
+ @set._loadGroup @set.requires.input[0], null, callback
140
+
141
+ waits 0
142
+
143
+ runs ->
144
+ @server.requests[0].respond 200, {}, 'foobar'
145
+
146
+ waits 0
147
+
148
+ runs ->
149
+ library =
150
+ url: 'foo.js'
151
+ key: 'foo.js'
152
+ type: 'js'
153
+ source: 'foobar'
154
+ localStorage: true
155
+ require: true
156
+
157
+ expect(@server.requests.length).toEqual 1
158
+ expect(@set.storage['foo.js']).toEqual library
159
+ expect(callback.callCount).toEqual 1
160
+ expect(@set._inject.callCount).toEqual 1
161
+ expect(@set._inject.args[0][0]).toEqual library
162
+
163
+ it "orders properly", ->
164
+ @set.require(
165
+ { url: 'foo.js' },
166
+ { url: 'bar.js' }
167
+ )
168
+ @set.require url: 'baz.js'
169
+
170
+ @set._loadGroup @set.requires.input[0], null, ->
171
+ @set._loadGroup @set.requires.input[1], null, ->
172
+
173
+ waits 0
174
+
175
+ runs ->
176
+ @server.requests[0].respond 200, {}, 'foobar'
177
+ @server.requests[1].respond 200, {}, 'foobar'
178
+ @server.requests[2].respond 200, {}, 'foobar'
179
+
180
+ waits 0
181
+
182
+ runs ->
183
+ expect(@server.requests[0].url).toEqual 'foo.js'
184
+ expect(@server.requests[1].url).toEqual 'baz.js'
185
+ expect(@server.requests[2].url).toEqual 'bar.js'
186
+
187
+ describe "progress", ->
188
+ it "tracks with cache", ->
189
+ progress = {set: sinon.spy()}
190
+
191
+ localStorage['loada.*'] = JSON.stringify
192
+ 'foo.js': {}
193
+ 'bar.js': {}
194
+
195
+ @set.setup()
196
+ @set.require(
197
+ { url: 'foo.js' },
198
+ { url: 'bar.js' }
199
+ )
200
+
201
+ @set._loadGroup @set.requires.input[0], progress, ->
202
+
203
+ expect(progress.set.callCount).toEqual 2
204
+ expect(progress.set.args[0]).toEqual ['foo.js', 100]
205
+ expect(progress.set.args[1]).toEqual ['bar.js', 100]
206
+
207
+ it "tracks with net", ->
208
+ progress = {set: sinon.spy()}
209
+
210
+ @set.require(
211
+ { url: 'foo.js' },
212
+ { url: 'bar.js' }
213
+ )
214
+
215
+ @set._loadGroup @set.requires.input[0], progress, ->
216
+
217
+ waits 0
218
+
219
+ runs ->
220
+ @server.requests[0].respond 200, {}, 'foobar'
221
+ @server.requests[1].respond 200, {}, 'foobar'
222
+
223
+ waits 0
224
+
225
+ runs ->
226
+ expect(progress.set.callCount).toEqual 2
227
+ expect(progress.set.args[0]).toEqual ['foo.js', 100]
228
+ expect(progress.set.args[1]).toEqual ['bar.js', 100]
229
+
230
+ it "loads through inlining", ->
231
+ window.TEST = 0
232
+
233
+ set = Loada.set()
234
+ server = sinon.fakeServer.create()
235
+ callback = sinon.spy()
236
+
237
+ set._loadInline {url: 'spec/support/test.js', key: 'test.js', type: 'js'}, null, callback
238
+
239
+ waits 100
240
+
241
+ runs ->
242
+ expect(callback.callCount).toEqual 1
243
+ expect(window.TEST).toEqual 1
244
+ delete window.TEST
245
+
246
+ it "injects", ->
247
+ window.TEST = 0
248
+
249
+ set = Loada.set()
250
+ set._inject source: 'window.TEST = 1', type: 'js'
251
+
252
+ expect(window.TEST).toEqual 1
253
+ delete window.TEST
254
+
255
+ it "caches", ->
256
+ set = Loada.set()
257
+ server = sinon.fakeServer.create()
258
+ sinon.stub set, '_inject'
259
+ set.require url: 'foo.js'
260
+ set.require url: 'bar.js'
261
+
262
+ set.load()
263
+
264
+ waits 0
265
+
266
+ runs ->
267
+ expect(server.requests.length).toEqual 2
268
+ server.requests[0].respond 200, {}, 'foobar'
269
+ server.requests[1].respond 200, {}, 'foobar'
270
+
271
+ waits 0
272
+
273
+ runs ->
274
+ set.load()
275
+
276
+ runs ->
277
+ expect(server.requests.length).toEqual 2
278
+
279
+ runs ->
280
+ server.restore()
281
+ set._inject.restore()
282
+
283
+ it "loads", ->
284
+ window.TEST = 0
285
+
286
+ progress = sinon.spy()
287
+ success = sinon.spy()
288
+
289
+ set = Loada.set()
290
+ set.require url: 'spec/support/test.js'
291
+ set.load
292
+ progress: progress
293
+ success: success
294
+
295
+ waits 100
296
+
297
+ runs ->
298
+ expect(progress.callCount).toEqual 1
299
+ expect(progress.args[0][0]).toEqual 100
300
+ expect(success.callCount).toEqual 1
301
+ expect(window.TEST).toEqual 1
302
+
303
+ delete window.TEST
304
+
305
+ it "loads text", ->
306
+ set = Loada.set()
307
+ server = sinon.fakeServer.create()
308
+ set.require url: 'foo', type: 'text'
309
+
310
+ set.load()
311
+
312
+ waits 0
313
+
314
+ runs ->
315
+ expect(server.requests.length).toEqual 1
316
+ server.requests[0].respond 200, {}, 'foobar'
317
+
318
+ expect(set.get 'foo').toEqual 'foobar'
@@ -0,0 +1 @@
1
+ window.TEST = 1
data/src/loada.coffee ADDED
@@ -0,0 +1,294 @@
1
+ $head = document.getElementsByTagName("head")[0]
2
+
3
+ #
4
+ # Loada is the kickass in-browser assets (JS/CSS) loader supporting localStorage
5
+ # and progress tracking.
6
+ #
7
+ # @see https://github.com/inossidabile/loada/
8
+ #
9
+ class @Loada
10
+ Progress: class
11
+ constructor: (@count, @progressCallback) ->
12
+ @data = {}
13
+
14
+ set: (key, percent) ->
15
+ @data[key] = Math.min(percent, 100.0)
16
+ @progressCallback? @total()
17
+
18
+ total: ->
19
+ total = 0
20
+ total += Math.round(percent/@count*100)/100 for key, percent of @data
21
+ total
22
+
23
+ #
24
+ # Alias for the #{constructor}
25
+ #
26
+ @set: ->
27
+ new @ arguments...
28
+
29
+ #
30
+ # @param [String] Name of the libraries set
31
+ # @param [Object]
32
+ #
33
+ constructor: (@set, options) ->
34
+ @options =
35
+ prefix: 'loada'
36
+ localStorage: true
37
+
38
+ @requires =
39
+ input: []
40
+ set: {}
41
+ length: 0
42
+
43
+ @set ||= '*'
44
+ @options[k] = v for k,v of options if options
45
+ @key = "#{@options.prefix}.#{@set}"
46
+
47
+ @setup()
48
+
49
+ #
50
+ # Loads current available cache for the current set into the instance
51
+ #
52
+ setup: ->
53
+ if @options.localStorage
54
+ @storage = localStorage[@key] || {}
55
+
56
+ if typeof(@storage) == 'string'
57
+ @storage = JSON.parse(localStorage[@key])
58
+
59
+ #
60
+ # Saves current set state to localStorage
61
+ #
62
+ save: ->
63
+ localStorage[@key] = JSON.stringify @storage
64
+
65
+ #
66
+ # Removes localStorage entry bound to current set
67
+ #
68
+ clear: -> delete localStorage[@key]
69
+
70
+ #
71
+ # Gets raw source of an asset
72
+ #
73
+ # @param key [String] Key of the asset
74
+ #
75
+ get: (key) ->
76
+ @storage[key]?.source
77
+
78
+ #
79
+ # Cleans up current state
80
+ #
81
+ # Removes entries that are:
82
+ # * expired by date (`expires` option)
83
+ # * got new revision (`revision` option)
84
+ # * not found in the current requires list
85
+ #
86
+ expire: ->
87
+ now = new Date
88
+
89
+ byDate = (library) =>
90
+ library.expirationDate && new Date(library.expirationDate) <= now
91
+
92
+ byExistance = (library) =>
93
+ !@requires.set[key]
94
+
95
+ byRevision = (library) =>
96
+ @requires.set[key].revision != library.revision
97
+
98
+ for key, library of @storage
99
+
100
+ if byDate(library) || byExistance(library) || byRevision(library)
101
+ delete @storage[key]
102
+
103
+ #
104
+ # Adds library that should be loaded
105
+ #
106
+ # All the libraries passed within one call will be loaded
107
+ # successive. Separate {#require} calls are loading in parallel.
108
+ #
109
+ # Library object consists of the following options:
110
+ # * **url**: location of the asset to load
111
+ # * **key**: storage key (default to url)
112
+ # * **type**: js/css (defaults to url extension parsing)
113
+ # * **revision**: manual revision number – triggers cache bust whenever it changes
114
+ # * **expires**: amount of hours to keep entry for (defaults to unlimited cache)
115
+ # * **cache**: whether localStorage should be used for particular asset
116
+ # * **size**: asset download size (defaults to additional HEAD request result)
117
+ #
118
+ # @param libraries [Object] Library object
119
+ #
120
+ require: (libraries...) ->
121
+ for library in libraries
122
+ library.key ||= library.url
123
+ library.type ||= library.url?.split('.').pop()
124
+ library.localStorage = true unless library.localStorage?
125
+ library.require = true unless library.require?
126
+
127
+ if library.expires
128
+ now = new Date
129
+ library.expirationDate = now.setTime(now.getTime() + library.expires*60*60*1000)
130
+
131
+ if library.type == 'js' || library.type == 'css' || library.type == 'text'
132
+ @requires.set[library.key] = library
133
+ else
134
+ console.error "Unknown asset type for #{library.url} – skipped"
135
+
136
+ @requires.length += libraries.length
137
+ @requires.input.push libraries
138
+
139
+ #
140
+ # Starts the inclusion of required libraries
141
+ #
142
+ # Accepts following options:
143
+ # * **success**: gets called as soon as all the assets are loaded
144
+ # * **progress(percent)**: ticks from time to time passing you curent download progress
145
+ #
146
+ # @param [Object] callbacks List of callbacks
147
+ #
148
+ load: (callbacks) ->
149
+ callbacks ||= {}
150
+ @expire() if @options.localStorage
151
+
152
+ progress = new @Progress(@requires.length, callbacks.progress)
153
+ loaders = 0
154
+
155
+ @_ensureSizes callbacks.progress?, =>
156
+ for group in @requires.input
157
+ loaders++
158
+
159
+ @_loadGroup group.slice(0), progress, =>
160
+ loaders--
161
+ if loaders == 0
162
+ @save()
163
+ callbacks.success?()
164
+
165
+ #
166
+ # Ensures every asset has 'size' option
167
+ #
168
+ # Runs HEAD request trying to parse Content-Length header for
169
+ # those that don't have.
170
+ #
171
+ # @private
172
+ #
173
+ _ensureSizes: (perform, callback) ->
174
+ unless perform
175
+ library.size = 0 for key, library of @requires.set
176
+ return callback()
177
+
178
+ requests = 0
179
+
180
+ for key, library of @requires.set
181
+ do (library) =>
182
+ unless library.size?
183
+ requests++
184
+
185
+ @_ajax 'HEAD', library.url, (xhr) ->
186
+ requests--
187
+ size = xhr.getResponseHeader('Content-Length')
188
+ size = parseInt(size) if size
189
+ library.size = size || 0
190
+ callback() if requests == 0
191
+
192
+ #
193
+ # Loads one require group successively
194
+ #
195
+ # @private
196
+ #
197
+ _loadGroup: (group, progress, callback) =>
198
+ library = group.shift()
199
+
200
+ return callback() unless library
201
+
202
+ if @options.localStorage && @storage[library.key] && library.localStorage
203
+ progress?.set library.key, 100
204
+ @_inject @storage[library.key] if @storage[library.key].require
205
+ @_loadGroup group, progress, callback
206
+ else
207
+ method = if @options.localStorage
208
+ "_loadAJAX"
209
+ else
210
+ "_loadInline"
211
+
212
+ @[method] library, progress, =>
213
+ @storage[library.key] = library if @options.localStorage
214
+ @_inject library if library.require
215
+ @_loadGroup group, progress, callback
216
+
217
+ #
218
+ # Loads one asset by AJAX query and stores it into instance
219
+ #
220
+ # @private
221
+ #
222
+ _loadAJAX: (library, progress, callback) ->
223
+ xhr = @_ajax 'GET', library.url, (xhr) =>
224
+ library.source = xhr.responseText
225
+ clearInterval poller
226
+ progress?.set library.key, 100
227
+ callback()
228
+
229
+ if library.size > 0
230
+ poller = setInterval (->
231
+ percent = Math.round(xhr.responseText.length / library.size * 100 * 100) / 100
232
+ progress?.set library.key, percent
233
+ ),
234
+ 100
235
+
236
+ #
237
+ # Loads one asset by inlining it into DOM
238
+ #
239
+ # @private
240
+ #
241
+ _loadInline: (library, progress, callback) ->
242
+ if library.type != 'js'
243
+ console.error "Attempt to load something other than JS without localStorage."
244
+ console.error "#{library.url} is not loaded!"
245
+ progress?.set library.key, 100
246
+ return callback()
247
+
248
+ script = document.createElement "script"
249
+ done = false
250
+
251
+ proceed = ->
252
+ if !done && (!@readyState? || @readyState == "loaded" || @readyState == "complete")
253
+ done = true
254
+ progress?.set library.key, 100
255
+ callback?()
256
+ script.onload = script.onreadystatechange = null
257
+
258
+ script.onload = script.onreadystatechange = proceed
259
+
260
+ script.src = library.url
261
+ $head.appendChild script
262
+
263
+ #
264
+ # Activates downloaded asset
265
+ #
266
+ # @private
267
+ #
268
+ _inject: (library) ->
269
+ if library.type == 'js'
270
+ script = document.createElement "script"
271
+ script.defer = true
272
+ script.text = library.source
273
+ $head.appendChild script
274
+ else if library.type == 'css'
275
+ style = document.createElement "style"
276
+ style.innerHTML = library.source
277
+ $head.appendChild style
278
+
279
+ #
280
+ # Starring custom XHR wrapper!
281
+ #
282
+ # @private
283
+ #
284
+ _ajax: (method, url, callback) ->
285
+ if window.XMLHttpRequest
286
+ xhr = new XMLHttpRequest
287
+ else
288
+ xhr = new ActiveXObject 'Microsoft.XMLHTTP'
289
+
290
+ xhr.open method, url, 1
291
+ xhr.onreadystatechange = -> callback?(xhr) if xhr.readyState > 3
292
+ xhr.send()
293
+
294
+ xhr