pace.coffee 18 KB

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