loada 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/Gruntfile.coffee +106 -0
- data/README.md +153 -0
- data/bower.json +18 -0
- data/lib/loada.js +321 -0
- data/lib/loada.min.js +1 -0
- data/lib/loada.rb +14 -0
- data/loada.gemspec +17 -0
- data/package.json +24 -0
- data/spec/helpers/spec_helper.coffee +2 -0
- data/spec/loada_spec.coffee +318 -0
- data/spec/support/test.js +1 -0
- data/src/loada.coffee +294 -0
- metadata +71 -0
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,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
|