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.
- 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
|