cis_pace 0.0.0

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 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()