cis_pace 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d4f9c3b5dc070c772d46da7c38841120b6679d9d
4
+ data.tar.gz: 5091b1e9200753af1565083dce9c866a0af1a417
5
+ SHA512:
6
+ metadata.gz: 140feded1c01846a2982dc9a904075bcba202b8015565550c2ef02bb8a8faa1b787d1ea47a9a2f9aa3e142c2716418c6eebc05635d68a5f6655e45989a6ea889
7
+ data.tar.gz: b5165fcdac9ede349969e9f4840aa5a5f74282638a354d2af4ea7d54627f9439efe96c140b14cd8ed8148a3703fa24a5deaf9b57047fe72fc339b079df8b922d
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,59 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ cis_pace (0.0.0)
5
+ coffee-rails
6
+ jquery-rails
7
+ railties
8
+
9
+ GEM
10
+ remote: http://rubygems.org/
11
+ specs:
12
+ actionpack (4.0.0)
13
+ activesupport (= 4.0.0)
14
+ builder (~> 3.1.0)
15
+ erubis (~> 2.7.0)
16
+ rack (~> 1.5.2)
17
+ rack-test (~> 0.6.2)
18
+ activesupport (4.0.0)
19
+ i18n (~> 0.6, >= 0.6.4)
20
+ minitest (~> 4.2)
21
+ multi_json (~> 1.3)
22
+ thread_safe (~> 0.1)
23
+ tzinfo (~> 0.3.37)
24
+ atomic (1.1.14)
25
+ builder (3.1.4)
26
+ coffee-rails (4.0.0)
27
+ coffee-script (>= 2.2.0)
28
+ railties (>= 4.0.0.beta, < 5.0)
29
+ coffee-script (2.2.0)
30
+ coffee-script-source
31
+ execjs
32
+ coffee-script-source (1.6.3)
33
+ erubis (2.7.0)
34
+ execjs (2.0.2)
35
+ i18n (0.6.5)
36
+ jquery-rails (3.0.4)
37
+ railties (>= 3.0, < 5.0)
38
+ thor (>= 0.14, < 2.0)
39
+ minitest (4.7.5)
40
+ multi_json (1.8.2)
41
+ rack (1.5.2)
42
+ rack-test (0.6.2)
43
+ rack (>= 1.0)
44
+ railties (4.0.0)
45
+ actionpack (= 4.0.0)
46
+ activesupport (= 4.0.0)
47
+ rake (>= 0.8.7)
48
+ thor (>= 0.18.1, < 2.0)
49
+ rake (10.1.0)
50
+ thor (0.18.1)
51
+ thread_safe (0.1.3)
52
+ atomic
53
+ tzinfo (0.3.38)
54
+
55
+ PLATFORMS
56
+ ruby
57
+
58
+ DEPENDENCIES
59
+ cis_pace!
data/README.md ADDED
File without changes
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,632 @@
1
+ defaultOptions =
2
+ # How long should it take for the bar to animate to a new
3
+ # point after receiving it
4
+ catchupTime: 500
5
+
6
+ # How quickly should the bar be moving before it has any progress
7
+ # info from a new source in %/ms
8
+ initialRate: .03
9
+
10
+ # What is the minimum amount of time the bar should be on the
11
+ # screen
12
+ minTime: 500
13
+
14
+ # What is the minimum amount of time the bar should sit after the last
15
+ # update before disappearing
16
+ ghostTime: 250
17
+
18
+ # Its easy for a bunch of the bar to be eaten in the first few frames
19
+ # before we know how much there is to load. This limits how much of
20
+ # the bar can be used per frame
21
+ maxProgressPerFrame: 10
22
+
23
+ # This tweaks the animation easing
24
+ easeFactor: 1.25
25
+
26
+ # Should pace automatically start when the page is loaded, or should it wait for `start` to
27
+ # be called? Always false if pace is loaded with AMD or CommonJS.
28
+ startOnPageLoad: true
29
+
30
+ # Should we restart the browser when pushState or replaceState is called? (Generally
31
+ # means ajax navigation has occured)
32
+ restartOnPushState: true
33
+
34
+ # Should we show the progress bar for every ajax request (not just regular or ajax-y page
35
+ # navigation)? Set to false to disable.
36
+ #
37
+ # If so, how many ms does the request have to be running for before we show the progress?
38
+ restartOnRequestAfter: 500
39
+
40
+ # What element should the pace element be appended to on the page?
41
+ target: 'body'
42
+
43
+ elements:
44
+ # How frequently in ms should we check for the elements being tested for
45
+ # using the element monitor?
46
+ checkInterval: 100
47
+
48
+ # What elements should we wait for before deciding the page is fully loaded (not required)
49
+ selectors: ['body']
50
+
51
+ eventLag:
52
+ # When we first start measuring event lag, not much is going on in the browser yet, so it's
53
+ # not uncommon for the numbers to be abnormally low for the first few samples. This configures
54
+ # how many samples we need before we consider a low number to mean completion.
55
+ minSamples: 10
56
+
57
+ # How many samples should we average to decide what the current lag is?
58
+ sampleCount: 3
59
+
60
+ # Above how many ms of lag is the CPU considered busy?
61
+ lagThreshold: 3
62
+
63
+ ajax:
64
+ # Which HTTP methods should we track?
65
+ trackMethods: ['GET']
66
+
67
+ # Should we track web socket connections?
68
+ trackWebSockets: false
69
+
70
+ now = ->
71
+ performance?.now?() ? +new Date
72
+
73
+ requestAnimationFrame = window.requestAnimationFrame or window.mozRequestAnimationFrame or
74
+ window.webkitRequestAnimationFrame or window.msRequestAnimationFrame
75
+
76
+ cancelAnimationFrame = window.cancelAnimationFrame or window.mozCancelAnimationFrame
77
+
78
+ if not requestAnimationFrame?
79
+ requestAnimationFrame = (fn) ->
80
+ setTimeout fn, 50
81
+
82
+ cancelAnimationFrame = (id) ->
83
+ clearTimeout id
84
+
85
+ runAnimation = (fn) ->
86
+ last = now()
87
+ tick = ->
88
+ diff = now() - last
89
+ last = now()
90
+
91
+ fn diff, ->
92
+ requestAnimationFrame tick
93
+
94
+ tick()
95
+
96
+ result = (obj, key, args...) ->
97
+ if typeof obj[key] is 'function'
98
+ obj[key](args...)
99
+ else
100
+ obj[key]
101
+
102
+ extend = (out, sources...) ->
103
+ for source in sources when source
104
+ for own key, val of source
105
+ if out[key]? and typeof out[key] is 'object' and val? and typeof val is 'object'
106
+ extend(out[key], val)
107
+ else
108
+ out[key] = val
109
+ out
110
+
111
+ avgAmplitude = (arr) ->
112
+ sum = count = 0
113
+ for v in arr
114
+ sum += Math.abs(v)
115
+ count++
116
+
117
+ sum / count
118
+
119
+ getFromDOM = (key='options', json=true) ->
120
+ el = document.querySelector "[data-pace-#{ key }]"
121
+
122
+ return unless el
123
+
124
+ data = el.getAttribute "data-pace-#{ key }"
125
+
126
+ return data if not json
127
+
128
+ try
129
+ return JSON.parse data
130
+ catch e
131
+ console?.error "Error parsing inline pace options", e
132
+
133
+ window.Pace ?= {}
134
+
135
+ options = Pace.options = extend defaultOptions, window.paceOptions, getFromDOM()
136
+
137
+ class NoTargetError extends Error
138
+
139
+ class Bar
140
+ constructor: ->
141
+ @progress = 0
142
+
143
+ getElement: ->
144
+ if not @el?
145
+ targetElement = document.querySelector options.target
146
+
147
+ if not targetElement
148
+ throw new NoTargetError
149
+
150
+ @el = document.createElement 'div'
151
+ @el.className = "pace pace-active"
152
+
153
+ document.body.className = document.body.className.replace 'pace-done', ''
154
+ document.body.className += ' pace-running'
155
+
156
+ @el.innerHTML = '''
157
+ <div class="pace-progress">
158
+ <div class="pace-progress-inner"></div>
159
+ </div>
160
+ <div class="pace-activity"></div>
161
+ '''
162
+ if targetElement.firstChild?
163
+ targetElement.insertBefore @el, targetElement.firstChild
164
+ else
165
+ targetElement.appendChild @el
166
+
167
+ @el
168
+
169
+ finish: ->
170
+ el = @getElement()
171
+
172
+ el.className = el.className.replace 'pace-active', ''
173
+ el.className += ' pace-inactive'
174
+
175
+ document.body.className = document.body.className.replace 'pace-running', ''
176
+ document.body.className += ' pace-done'
177
+
178
+ update: (prog) ->
179
+ @progress = prog
180
+
181
+ do @render
182
+
183
+ destroy: ->
184
+ try
185
+ @getElement().parentNode.removeChild(@getElement())
186
+ catch NoTargetError
187
+
188
+ @el = undefined
189
+
190
+ render: ->
191
+ if not document.querySelector(options.target)?
192
+ return false
193
+
194
+ el = @getElement()
195
+
196
+ el.children[0].style.width = "#{ @progress }%"
197
+
198
+ if not @lastRenderedProgress or @lastRenderedProgress|0 != @progress|0
199
+ # The whole-part of the number has changed
200
+
201
+ el.children[0].setAttribute 'data-progress-text', "#{ @progress|0 }%"
202
+
203
+ if @progress >= 100
204
+ # We cap it at 99 so we can use prefix-based attribute selectors
205
+ progressStr = '99'
206
+ else
207
+ progressStr = if @progress < 10 then "0" else ""
208
+ progressStr += @progress|0
209
+
210
+ el.children[0].setAttribute 'data-progress', "#{ progressStr }"
211
+
212
+ @lastRenderedProgress = @progress
213
+
214
+ done: ->
215
+ @progress >= 100
216
+
217
+ class Events
218
+ constructor: ->
219
+ @bindings = {}
220
+
221
+ trigger: (name, val) ->
222
+ if @bindings[name]?
223
+ for binding in @bindings[name]
224
+ binding.call @, val
225
+
226
+ on: (name, fn) ->
227
+ @bindings[name] ?= []
228
+ @bindings[name].push fn
229
+
230
+ _XMLHttpRequest = window.XMLHttpRequest
231
+ _XDomainRequest = window.XDomainRequest
232
+ _WebSocket = window.WebSocket
233
+
234
+ extendNative = (to, from) ->
235
+ for key of from::
236
+ try
237
+ val = from::[key]
238
+
239
+ if not to[key]? and typeof val isnt 'function'
240
+ to[key] = val
241
+ catch e
242
+
243
+ # We should only ever instantiate one of these
244
+ class RequestIntercept extends Events
245
+ constructor: ->
246
+ super
247
+
248
+ monitorXHR = (req) =>
249
+ _open = req.open
250
+ req.open = (type, url, async) =>
251
+ if (type ? 'GET').toUpperCase() in options.ajax.trackMethods
252
+ @trigger 'request', {type, url, request: req}
253
+
254
+ _open.apply req, arguments
255
+
256
+ window.XMLHttpRequest = (flags) ->
257
+ req = new _XMLHttpRequest(flags)
258
+
259
+ monitorXHR req
260
+
261
+ req
262
+
263
+ extendNative window.XMLHttpRequest, _XMLHttpRequest
264
+
265
+ if _XDomainRequest?
266
+ window.XDomainRequest = ->
267
+ req = new _XDomainRequest
268
+
269
+ monitorXHR req
270
+
271
+ req
272
+
273
+ extendNative window.XDomainRequest, _XDomainRequest
274
+
275
+ if _WebSocket? and options.ajax.trackWebSockets
276
+ window.WebSocket = (url, protocols) =>
277
+ req = new _WebSocket(url, protocols)
278
+
279
+ @trigger 'request', {type: 'socket', url, protocols, request: req}
280
+
281
+ req
282
+
283
+ extendNative window.WebSocket, _WebSocket
284
+
285
+ _intercept = null
286
+ getIntercept = ->
287
+ if not _intercept?
288
+ _intercept = new RequestIntercept
289
+ _intercept
290
+
291
+ if options.restartOnRequestAfter isnt false
292
+ # If we want to start the progress bar
293
+ # on every request, we need to hear the request
294
+ # and then inject it into the new ajax monitor
295
+ # start will have created.
296
+
297
+ getIntercept().on 'request', ({type, request}) ->
298
+ if not Pace.running
299
+ args = arguments
300
+
301
+ setTimeout ->
302
+ if type is 'socket'
303
+ stillActive = request.readyState < 2
304
+ else
305
+ stillActive = 0 < request.readyState < 4
306
+
307
+ if stillActive
308
+ Pace.restart()
309
+
310
+ for source in Pace.sources
311
+ if source instanceof AjaxMonitor
312
+ source.watch args...
313
+ break
314
+ , options.restartOnRequestAfter
315
+
316
+ class AjaxMonitor
317
+ constructor: ->
318
+ @elements = []
319
+
320
+ getIntercept().on 'request', => @watch arguments...
321
+
322
+ watch: ({type, request}) ->
323
+ if type is 'socket'
324
+ tracker = new SocketRequestTracker(request)
325
+ else
326
+ tracker = new XHRRequestTracker(request)
327
+
328
+ @elements.push tracker
329
+
330
+ class XHRRequestTracker
331
+ constructor: (request) ->
332
+ @progress = 0
333
+
334
+ if window.ProgressEvent?
335
+ # We're dealing with a modern browser with progress event support
336
+
337
+ size = null
338
+ request.addEventListener 'progress', (evt) =>
339
+ if evt.lengthComputable
340
+ @progress = 100 * evt.loaded / evt.total
341
+ else
342
+ # If it's chunked encoding, we have no way of knowing the total length of the
343
+ # response, all we can do is increment the progress with backoff such that we
344
+ # never hit 100% until it's done.
345
+ @progress = @progress + (100 - @progress) / 2
346
+
347
+ for event in ['load', 'abort', 'timeout', 'error']
348
+ request.addEventListener event, =>
349
+ @progress = 100
350
+
351
+ else
352
+ _onreadystatechange = request.onreadystatechange
353
+ request.onreadystatechange = =>
354
+ if request.readyState in [0, 4]
355
+ @progress = 100
356
+ else if request.readyState is 3
357
+ @progress = 50
358
+
359
+ _onreadystatechange?(arguments...)
360
+
361
+ class SocketRequestTracker
362
+ constructor: (request) ->
363
+ @progress = 0
364
+
365
+ for event in ['error', 'open']
366
+ request.addEventListener event, =>
367
+ @progress = 100
368
+
369
+ class ElementMonitor
370
+ constructor: (options={}) ->
371
+ @elements = []
372
+
373
+ options.selectors ?= []
374
+ for selector in options.selectors
375
+ @elements.push new ElementTracker selector
376
+
377
+ class ElementTracker
378
+ constructor: (@selector) ->
379
+ @progress = 0
380
+
381
+ @check()
382
+
383
+ check: ->
384
+ if document.querySelector(@selector)
385
+ @done()
386
+ else
387
+ setTimeout (=> @check()),
388
+ options.elements.checkInterval
389
+
390
+ done: ->
391
+ @progress = 100
392
+
393
+ class DocumentMonitor
394
+ states:
395
+ loading: 0
396
+ interactive: 50
397
+ complete: 100
398
+
399
+ constructor: ->
400
+ @progress = @states[document.readyState] ? 100
401
+
402
+ _onreadystatechange = document.onreadystatechange
403
+ document.onreadystatechange = =>
404
+ if @states[document.readyState]?
405
+ @progress = @states[document.readyState]
406
+
407
+ _onreadystatechange?(arguments...)
408
+
409
+ class EventLagMonitor
410
+ constructor: ->
411
+ @progress = 0
412
+
413
+ avg = 0
414
+
415
+ samples = []
416
+
417
+ points = 0
418
+ last = now()
419
+ interval = setInterval =>
420
+ diff = now() - last - 50
421
+ last = now()
422
+
423
+ samples.push diff
424
+
425
+ if samples.length > options.eventLag.sampleCount
426
+ samples.shift()
427
+
428
+ avg = avgAmplitude samples
429
+
430
+ if ++points >= options.eventLag.minSamples and avg < options.eventLag.lagThreshold
431
+ @progress = 100
432
+
433
+ clearInterval interval
434
+ else
435
+ @progress = 100 * (3 / (avg + 3))
436
+
437
+ , 50
438
+
439
+ class Scaler
440
+ constructor: (@source) ->
441
+ @last = @sinceLastUpdate = 0
442
+ @rate = options.initialRate
443
+ @catchup = 0
444
+ @progress = @lastProgress = 0
445
+
446
+ if @source?
447
+ @progress = result(@source, 'progress')
448
+
449
+ tick: (frameTime, val) ->
450
+ val ?= result(@source, 'progress')
451
+
452
+ if val >= 100
453
+ @done = true
454
+
455
+ if val == @last
456
+ @sinceLastUpdate += frameTime
457
+ else
458
+ if @sinceLastUpdate
459
+ @rate = (val - @last) / @sinceLastUpdate
460
+
461
+ @catchup = (val - @progress) / options.catchupTime
462
+
463
+ @sinceLastUpdate = 0
464
+ @last = val
465
+
466
+ if val > @progress
467
+ # After we've got a datapoint, we have catchupTime to
468
+ # get the progress bar to reflect that new data
469
+ @progress += @catchup * frameTime
470
+
471
+ scaling = (1 - Math.pow(@progress / 100, options.easeFactor))
472
+
473
+ # Based on the rate of the last update, we preemptively update
474
+ # the progress bar, scaling it so it can never hit 100% until we
475
+ # know it's done.
476
+ @progress += scaling * @rate * frameTime
477
+
478
+ @progress = Math.min(@lastProgress + options.maxProgressPerFrame, @progress)
479
+
480
+ @progress = Math.max(0, @progress)
481
+ @progress = Math.min(100, @progress)
482
+
483
+ @lastProgress = @progress
484
+
485
+ @progress
486
+
487
+ sources = null
488
+ scalers = null
489
+ bar = null
490
+ uniScaler = null
491
+ animation = null
492
+ cancelAnimation = null
493
+ Pace.running = false
494
+
495
+ handlePushState = ->
496
+ if options.restartOnPushState
497
+ Pace.restart()
498
+
499
+ # We reset the bar whenever it looks like an ajax navigation has occured.
500
+ if window.history.pushState?
501
+ _pushState = window.history.pushState
502
+ window.history.pushState = ->
503
+ handlePushState()
504
+
505
+ _pushState.apply window.history, arguments
506
+
507
+ if window.history.replaceState?
508
+ _replaceState = window.history.replaceState
509
+ window.history.replaceState = ->
510
+ handlePushState()
511
+
512
+ _replaceState.apply window.history, arguments
513
+
514
+ SOURCE_KEYS =
515
+ ajax: AjaxMonitor
516
+ elements: ElementMonitor
517
+ document: DocumentMonitor
518
+ eventLag: EventLagMonitor
519
+
520
+ do init = ->
521
+ Pace.sources = sources = []
522
+
523
+ for type in ['ajax', 'elements', 'document', 'eventLag']
524
+ if options[type] isnt false
525
+ sources.push new SOURCE_KEYS[type](options[type])
526
+
527
+ for source in options.extraSources ? []
528
+ sources.push new source(options)
529
+
530
+ Pace.bar = bar = new Bar
531
+
532
+ # Each source of progress data has it's own scaler to smooth its output
533
+ scalers = []
534
+
535
+ # We have an extra scaler for the final output to keep things looking nice as we add and
536
+ # remove sources
537
+ uniScaler = new Scaler
538
+
539
+ Pace.stop = ->
540
+ Pace.running = false
541
+
542
+ bar.destroy()
543
+
544
+ # Not all browsers support cancelAnimationFrame
545
+ cancelAnimation = true
546
+
547
+ if animation?
548
+ cancelAnimationFrame? animation
549
+ animation = null
550
+
551
+ init()
552
+
553
+ Pace.restart = ->
554
+ Pace.stop()
555
+ Pace.start()
556
+
557
+ Pace.go = ->
558
+ Pace.running = true
559
+
560
+ bar.render()
561
+
562
+ cancelAnimation = false
563
+ animation = runAnimation (frameTime, enqueueNextFrame) ->
564
+ # Every source gives us a progress number from 0 - 100
565
+ # It's up to us to figure out how to turn that into a smoothly moving bar
566
+ #
567
+ # Their progress numbers can only increment. We try to interpolate
568
+ # between the numbers.
569
+
570
+ remaining = 100 - bar.progress
571
+
572
+ count = sum = 0
573
+ done = true
574
+ # A source is composed of a bunch of elements, each with a raw, unscaled progress
575
+ for source, i in sources
576
+ scalerList = scalers[i] ?= []
577
+
578
+ elements = source.elements ? [source]
579
+
580
+ # Each element is given it's own scaler, which turns its value into something
581
+ # smoothed for display
582
+ for element, j in elements
583
+ scaler = scalerList[j] ?= new Scaler element
584
+
585
+ done &= scaler.done
586
+
587
+ continue if scaler.done
588
+
589
+ count++
590
+ sum += scaler.tick(frameTime)
591
+
592
+ avg = sum / count
593
+
594
+ bar.update uniScaler.tick(frameTime, avg)
595
+
596
+ start = now()
597
+ if bar.done() or done or cancelAnimation
598
+ bar.update 100
599
+
600
+ setTimeout ->
601
+ bar.finish()
602
+
603
+ Pace.running = false
604
+ , Math.max(options.ghostTime, Math.min(options.minTime, now() - start))
605
+ else
606
+ enqueueNextFrame()
607
+
608
+ Pace.start = (_options) ->
609
+ extend options, _options
610
+
611
+ Pace.running = true
612
+
613
+ try
614
+ bar.render()
615
+ catch NoTargetError
616
+
617
+ # It's usually possible to render a bit before the document declares itself ready
618
+ if not document.querySelector('.pace')
619
+ setTimeout Pace.start, 50
620
+ else
621
+ Pace.go()
622
+
623
+ if typeof define is 'function' and define.amd
624
+ # AMD
625
+ define -> Pace
626
+ else if typeof exports is 'object'
627
+ # CommonJS
628
+ module.exports = Pace
629
+ else
630
+ # Global
631
+ if options.startOnPageLoad
632
+ Pace.start()