Skip to content

Commit bd54bd3

Browse files
committed
Initial commit
0 parents  commit bd54bd3

8 files changed

Lines changed: 609 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

Gruntfile.coffee

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module.exports = (grunt) ->
2+
grunt.initConfig
3+
pkg: grunt.file.readJSON("package.json")
4+
coffee:
5+
compile:
6+
files:
7+
'mprogress.js': 'mprogress.coffee'
8+
9+
watch:
10+
coffee:
11+
files: ['mprogress.coffee']
12+
tasks: ["coffee", "uglify"]
13+
14+
uglify:
15+
options:
16+
banner: "/*! <%= pkg.name %> <%= pkg.version %> */\n"
17+
18+
dist:
19+
src: 'mprogress.js'
20+
dest: 'mprogress.min.js'
21+
22+
grunt.loadNpmTasks 'grunt-contrib-watch'
23+
grunt.loadNpmTasks 'grunt-contrib-uglify'
24+
grunt.loadNpmTasks 'grunt-contrib-coffee'
25+
26+
grunt.registerTask 'default', ['coffee', 'uglify']

demo.html

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<head>
2+
<style>
3+
.mprogress-bar {
4+
position: absolute;
5+
top: 0;
6+
left: 0;
7+
height: 3px;
8+
background-color: #FFC01F;
9+
}
10+
</style>
11+
12+
<script src="jquery.min.js"></script>
13+
<script src="mprogress.js"></script>
14+
<script>
15+
function load(time){
16+
$.ajax({
17+
url: "http://localhost:5646/" + time,
18+
complete: function(){
19+
console.log('done', arguments);
20+
}
21+
});
22+
};
23+
24+
load(20);
25+
load(100);
26+
load(500);
27+
load(2000);
28+
load(3000);
29+
</script>
30+
</head>
31+
<body></body>

jquery.min.js

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mprogress.coffee

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
# Track ajax requests, we want a clean event for their progress
2+
3+
# How long should it take for the bar to animate to a new
4+
# point after receiving it
5+
CATCHUP_TIME = 500
6+
7+
# How quickly should the bar be moving before it has any progress
8+
# info from a new source
9+
INITIAL_RATE = .03
10+
11+
# What is the minimum amount of time the bar should be on the
12+
# screen
13+
MIN_TIME = 500
14+
15+
# What is the minimum amount of time the bar should sit after the last
16+
# update before disappearing
17+
GHOST_TIME = 250
18+
19+
now = ->
20+
performance?.now?() ? +new Date
21+
22+
runAnimation = (fn) ->
23+
last = now()
24+
tick = ->
25+
diff = now() - last
26+
last = now()
27+
28+
fn diff, ->
29+
requestAnimationFrame tick
30+
31+
tick()
32+
33+
result = (obj, key, args...) ->
34+
if typeof obj[key] is 'function'
35+
obj[key](args...)
36+
else
37+
obj[key]
38+
39+
avgKey = (arr, key, args...) ->
40+
sum = 0
41+
for item in arr
42+
sum += result(item, key, args...)
43+
44+
sum / arr.length
45+
46+
class Bar
47+
constructor: ->
48+
@progress = 0
49+
50+
getElement: ->
51+
if not @el?
52+
@el = $('<div>')[0]
53+
@el.className = 'mprogress-bar'
54+
$('body').append @el
55+
56+
@el
57+
58+
hide: ->
59+
@getElement().style.display = 'none'
60+
61+
update: (prog) ->
62+
@progress = prog
63+
64+
do @render
65+
66+
render: ->
67+
@getElement().style.width = "#{ @progress }%"
68+
69+
done: ->
70+
@progress >= 100
71+
72+
# Every 100ms, we decide what the next progress should be
73+
# Every time a new thing happens, we decide what the progress should be
74+
# CSS animations can't give us backoff
75+
76+
class Events
77+
constructor: ->
78+
@bindings = {}
79+
80+
trigger: (name, val) ->
81+
if @bindings[name]?
82+
for binding in @bindings[name]
83+
binding.call @, val
84+
85+
on: (name, fn) ->
86+
@bindings[name] ?= []
87+
@bindings[name].push fn
88+
89+
# We should only ever instantiate one of these
90+
_XMLHttpRequest = window.XMLHttpRequest
91+
class RequestIntercept extends Events
92+
constructor: ->
93+
super
94+
95+
_intercept = @
96+
97+
window.XMLHttpRequest = ->
98+
req = new _XMLHttpRequest
99+
100+
_open = req.open
101+
req.open = (type, url, async) ->
102+
_intercept.trigger 'request', {type, url, request: req}
103+
104+
_open.apply @, arguments
105+
106+
req
107+
108+
intercept = new RequestIntercept
109+
110+
class AjaxMonitor
111+
constructor: ->
112+
@elements = []
113+
114+
intercept.on 'request', ({request}) =>
115+
@watch request
116+
117+
watch: (request) ->
118+
tracker = new RequestTracker(request)
119+
120+
@elements.push tracker
121+
122+
class RequestTracker
123+
constructor: (request) ->
124+
@progress = 0
125+
126+
size = null
127+
request.onprogress = =>
128+
try
129+
headers = request.getAllResponseHeaders()
130+
131+
for name, val of headers
132+
if name.toLowerCase() is 'content-length'
133+
size = +val
134+
break
135+
136+
catch e
137+
138+
if size?
139+
# This is not perfect, as size is in bytes, length is in chars
140+
try
141+
@progress = request.responseText.length / size
142+
catch e
143+
else
144+
# If it's chunked encoding, we have no way of knowing the total length of the
145+
# response, all we can do is incrememnt the progress with backoff such that we
146+
# never hit 100% until it's done.
147+
@progress = @progress + (100 - @progress) / 2
148+
149+
request.onload = request.onerror = request.ontimeout = request.onabort = =>
150+
@progress = 100
151+
152+
class Scaler
153+
constructor: (@source) ->
154+
@last = @sinceLastUpdate = 0
155+
@rate = 0.03
156+
@catchup = 0
157+
158+
@progress = result(@source, 'progress')
159+
160+
tick: (frameTime) ->
161+
val = result(@source, 'progress')
162+
163+
if val >= 100
164+
@done = true
165+
166+
if val == @last
167+
@sinceLastUpdate += frameTime
168+
else
169+
@rate = (val - @last) / @sinceLastUpdate
170+
171+
@catchup = (val - @progress) / CATCHUP_TIME
172+
173+
@sinceLastUpdate = 0
174+
@last = val
175+
176+
if val > @progress
177+
# After we've got a datapoint, we have CATCHUP_TIME to
178+
# get the progress bar to reflect that new data
179+
@progress += @catchup * frameTime
180+
181+
scaling = (1 - Math.pow(@progress / 100, 2))
182+
183+
# Based on the rate of the last update, we preemptively update
184+
# the progress bar, scaling it so it can never hit 100% until we
185+
# know it's done.
186+
@progress += scaling * @rate * frameTime
187+
188+
@progress = Math.max(0, @progress)
189+
@progress = Math.min(100, @progress)
190+
191+
@progress
192+
193+
sources = [new AjaxMonitor]
194+
scalers = []
195+
196+
$ ->
197+
bar = new Bar
198+
bar.render()
199+
200+
runAnimation (frameTime, enqueueNextFrame) ->
201+
# Every source gives us a progress number from 0 - 100
202+
# It's up to us to figure out how to turn that into a smoothly moving bar
203+
#
204+
# Their progress numbers can only increment. We try to interpolate
205+
# between the numbers.
206+
207+
max = 0
208+
for source, i in sources
209+
scalerList = scalers[i] ?= []
210+
211+
avg = sum = 0
212+
done = !!source.elements.length
213+
for element, j in source.elements
214+
scaler = scalerList[j] ?= new Scaler element
215+
216+
sum += scaler.tick(frameTime)
217+
218+
done &= scaler.done
219+
220+
if source.elements.length
221+
avg = sum / source.elements.length
222+
223+
max = Math.max(max, avg)
224+
225+
bar.update max
226+
227+
start = now()
228+
if bar.done() or done
229+
bar.update 100
230+
231+
setTimeout ->
232+
bar.hide()
233+
, Math.max(GHOST_TIME, Math.min(MIN_TIME, now() - start))
234+
else
235+
enqueueNextFrame()

0 commit comments

Comments
 (0)