123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755 |
- defaultOptions =
- # How long should it take for the bar to animate to a new
- # point after receiving it
- catchupTime: 100
- # How quickly should the bar be moving before it has any progress
- # info from a new source in %/ms
- initialRate: .03
- # What is the minimum amount of time the bar should be on the
- # screen. Irrespective of this number, the bar will always be on screen for
- # 33 * (100 / maxProgressPerFrame) + ghostTime ms.
- minTime: 250
- # What is the minimum amount of time the bar should sit after the last
- # update before disappearing
- ghostTime: 100
- # Its easy for a bunch of the bar to be eaten in the first few frames
- # before we know how much there is to load. This limits how much of
- # the bar can be used per frame
- maxProgressPerFrame: 20
- # This tweaks the animation easing
- easeFactor: 1.25
- # Should pace automatically start when the page is loaded, or should it wait for `start` to
- # be called? Always false if pace is loaded with AMD or CommonJS.
- startOnPageLoad: true
- # Should we restart the browser when pushState or replaceState is called? (Generally
- # means ajax navigation has occured)
- restartOnPushState: true
- # Should we show the progress bar for every ajax request (not just regular or ajax-y page
- # navigation)? Set to false to disable.
- #
- # If so, how many ms does the request have to be running for before we show the progress?
- restartOnRequestAfter: 500
- # What element should the pace element be appended to on the page?
- target: 'body'
- elements:
- # How frequently in ms should we check for the elements being tested for
- # using the element monitor?
- checkInterval: 100
- # What elements should we wait for before deciding the page is fully loaded (not required)
- selectors: ['body']
- eventLag:
- # When we first start measuring event lag, not much is going on in the browser yet, so it's
- # not uncommon for the numbers to be abnormally low for the first few samples. This configures
- # how many samples we need before we consider a low number to mean completion.
- minSamples: 10
- # How many samples should we average to decide what the current lag is?
- sampleCount: 3
- # Above how many ms of lag is the CPU considered busy?
- lagThreshold: 3
- ajax:
- # Which HTTP methods should we track?
- trackMethods: ['GET']
- # Should we track web socket connections?
- trackWebSockets: true
- # A list of regular expressions or substrings of URLS we should ignore (for both tracking and restarting)
- ignoreURLs: []
- now = ->
- performance?.now?() ? +new Date
- requestAnimationFrame = window.requestAnimationFrame or window.mozRequestAnimationFrame or
- window.webkitRequestAnimationFrame or window.msRequestAnimationFrame
- cancelAnimationFrame = window.cancelAnimationFrame or window.mozCancelAnimationFrame
- if not requestAnimationFrame?
- requestAnimationFrame = (fn) ->
- setTimeout fn, 50
- cancelAnimationFrame = (id) ->
- clearTimeout id
- runAnimation = (fn) ->
- last = now()
- tick = ->
- diff = now() - last
- if diff >= 33
- # Don't run faster than 30 fps
- last = now()
- fn diff, ->
- requestAnimationFrame tick
- else
- setTimeout tick, (33 - diff)
- tick()
- result = (obj, key, args...) ->
- if typeof obj[key] is 'function'
- obj[key](args...)
- else
- obj[key]
- extend = (out, sources...) ->
- for source in sources when source
- for own key, val of source
- if out[key]? and typeof out[key] is 'object' and val? and typeof val is 'object'
- extend(out[key], val)
- else
- out[key] = val
- out
- avgAmplitude = (arr) ->
- sum = count = 0
- for v in arr
- sum += Math.abs(v)
- count++
- sum / count
- getFromDOM = (key='options', json=true) ->
- el = document.querySelector "[data-pace-#{ key }]"
- return unless el
- data = el.getAttribute "data-pace-#{ key }"
- return data if not json
- try
- return JSON.parse data
- catch e
- console?.error "Error parsing inline pace options", e
- class Evented
- on: (event, handler, ctx, once=false) ->
- @bindings ?= {}
- @bindings[event] ?= []
- @bindings[event].push {handler, ctx, once}
- once: (event, handler, ctx) ->
- @on(event, handler, ctx, true)
- off: (event, handler) ->
- return unless @bindings?[event]?
- if not handler?
- delete @bindings[event]
- else
- i = 0
- while i < @bindings[event].length
- if @bindings[event][i].handler is handler
- @bindings[event].splice i, 1
- else
- i++
- trigger: (event, args...) ->
- if @bindings?[event]
- i = 0
- while i < @bindings[event].length
- {handler, ctx, once} = @bindings[event][i]
- handler.apply(ctx ? @, args)
- if once
- @bindings[event].splice i, 1
- else
- i++
- Pace = window.Pace or {}
- window.Pace = Pace
- extend Pace, Evented::
- options = Pace.options = extend {}, defaultOptions, window.paceOptions, getFromDOM()
- for source in ['ajax', 'document', 'eventLag', 'elements']
- # true enables them without configuration, so we grab the config from the defaults
- if options[source] is true
- options[source] = defaultOptions[source]
- class NoTargetError extends Error
- class Bar
- constructor: ->
- @progress = 0
- getElement: ->
- if not @el?
- targetElement = document.querySelector options.target
- if not targetElement
- throw new NoTargetError
- @el = document.createElement 'div'
- @el.className = "pace pace-active"
- document.body.className = document.body.className.replace /pace-done/g, ''
- document.body.className += ' pace-running'
- @el.innerHTML = '''
- <div class="pace-progress">
- <div class="pace-progress-inner"></div>
- </div>
- <div class="pace-activity"></div>
- '''
- if targetElement.firstChild?
- targetElement.insertBefore @el, targetElement.firstChild
- else
- targetElement.appendChild @el
- @el
- finish: ->
- el = @getElement()
- el.className = el.className.replace 'pace-active', ''
- el.className += ' pace-inactive'
- document.body.className = document.body.className.replace 'pace-running', ''
- document.body.className += ' pace-done'
- update: (prog) ->
- @progress = prog
- do @render
- destroy: ->
- try
- @getElement().parentNode.removeChild(@getElement())
- catch NoTargetError
- @el = undefined
- render: ->
- if not document.querySelector(options.target)?
- return false
- el = @getElement()
- transform = "translate3d(#{ @progress }%, 0, 0)"
- for key in ['webkitTransform', 'msTransform', 'transform']
- el.children[0].style[key] = transform
- if not @lastRenderedProgress or @lastRenderedProgress|0 != @progress|0
- # The whole-part of the number has changed
- el.children[0].setAttribute 'data-progress-text', "#{ @progress|0 }%"
- if @progress >= 100
- # We cap it at 99 so we can use prefix-based attribute selectors
- progressStr = '99'
- else
- progressStr = if @progress < 10 then "0" else ""
- progressStr += @progress|0
- el.children[0].setAttribute 'data-progress', "#{ progressStr }"
- @lastRenderedProgress = @progress
- done: ->
- @progress >= 100
- class Events
- constructor: ->
- @bindings = {}
- trigger: (name, val) ->
- if @bindings[name]?
- for binding in @bindings[name]
- binding.call @, val
- on: (name, fn) ->
- @bindings[name] ?= []
- @bindings[name].push fn
- _XMLHttpRequest = window.XMLHttpRequest
- _XDomainRequest = window.XDomainRequest
- _WebSocket = window.WebSocket
- extendNative = (to, from) ->
- for key of from::
- try
- if not to[key]? and typeof from[key] isnt 'function'
- if typeof Object.defineProperty is 'function'
- Object.defineProperty(to, key, {
- get: ->
- return from::[key];
- ,
- configurable: true,
- enumerable: true })
- else
- to[key] = from::[key]
- catch e
- ignoreStack = []
- Pace.ignore = (fn, args...) ->
- ignoreStack.unshift 'ignore'
- ret = fn(args...)
- ignoreStack.shift()
- ret
- Pace.track = (fn, args...) ->
- ignoreStack.unshift 'track'
- ret = fn(args...)
- ignoreStack.shift()
- ret
- shouldTrack = (method='GET') ->
- if ignoreStack[0] is 'track'
- return 'force'
- if not ignoreStack.length and options.ajax
- if method is 'socket' and options.ajax.trackWebSockets
- return true
- else if method.toUpperCase() in options.ajax.trackMethods
- return true
- return false
- # We should only ever instantiate one of these
- class RequestIntercept extends Events
- constructor: ->
- super
- monitorXHR = (req) =>
- _open = req.open
- req.open = (type, url, async) =>
- if shouldTrack(type)
- @trigger 'request', {type, url, request: req}
- _open.apply req, arguments
- window.XMLHttpRequest = (flags) ->
- req = new _XMLHttpRequest(flags)
- monitorXHR req
- req
- try
- extendNative window.XMLHttpRequest, _XMLHttpRequest
- if _XDomainRequest?
- window.XDomainRequest = ->
- req = new _XDomainRequest
- monitorXHR req
- req
- try
- extendNative window.XDomainRequest, _XDomainRequest
- if _WebSocket? and options.ajax.trackWebSockets
- window.WebSocket = (url, protocols) =>
- if protocols?
- req = new _WebSocket(url, protocols)
- else
- req = new _WebSocket(url)
- if shouldTrack('socket')
- @trigger 'request', {type: 'socket', url, protocols, request: req}
- req
- try
- extendNative window.WebSocket, _WebSocket
- _intercept = null
- getIntercept = ->
- if not _intercept?
- _intercept = new RequestIntercept
- _intercept
- shouldIgnoreURL = (url) ->
- for pattern in options.ajax.ignoreURLs
- if typeof pattern is 'string'
- if url.indexOf(pattern) isnt -1
- return true
- else
- if pattern.test(url)
- return true
- return false
- # If we want to start the progress bar
- # on every request, we need to hear the request
- # and then inject it into the new ajax monitor
- # start will have created.
- getIntercept().on 'request', ({type, request, url}) ->
- return if shouldIgnoreURL(url)
- if not Pace.running and (options.restartOnRequestAfter isnt false or shouldTrack(type) is 'force')
- args = arguments
- after = options.restartOnRequestAfter or 0
- if typeof after is 'boolean'
- after = 0
- setTimeout ->
- if type is 'socket'
- stillActive = request.readyState < 2
- else
- stillActive = 0 < request.readyState < 4
- if stillActive
- Pace.restart()
- for source in Pace.sources
- if source instanceof AjaxMonitor
- source.watch args...
- break
- , after
- class AjaxMonitor
- constructor: ->
- @elements = []
- getIntercept().on 'request', => @watch arguments...
- watch: ({type, request, url}) ->
- return if shouldIgnoreURL(url)
- if type is 'socket'
- tracker = new SocketRequestTracker(request)
- else
- tracker = new XHRRequestTracker(request)
- @elements.push tracker
- class XHRRequestTracker
- constructor: (request) ->
- @progress = 0
- if window.ProgressEvent?
- # We're dealing with a modern browser with progress event support
- size = null
- request.addEventListener 'progress', (evt) =>
- if evt.lengthComputable
- @progress = 100 * evt.loaded / evt.total
- else
- # If it's chunked encoding, we have no way of knowing the total length of the
- # response, all we can do is increment the progress with backoff such that we
- # never hit 100% until it's done.
- @progress = @progress + (100 - @progress) / 2
- , false
- for event in ['load', 'abort', 'timeout', 'error']
- request.addEventListener event, =>
- @progress = 100
- , false
- else
- _onreadystatechange = request.onreadystatechange
- request.onreadystatechange = =>
- if request.readyState in [0, 4]
- @progress = 100
- else if request.readyState is 3
- @progress = 50
- _onreadystatechange?(arguments...)
- class SocketRequestTracker
- constructor: (request) ->
- @progress = 0
- for event in ['error', 'open']
- request.addEventListener event, =>
- @progress = 100
- , false
- class ElementMonitor
- constructor: (options={}) ->
- @elements = []
- options.selectors ?= []
- for selector in options.selectors
- @elements.push new ElementTracker selector
- class ElementTracker
- constructor: (@selector) ->
- @progress = 0
- @check()
- check: ->
- if document.querySelector(@selector)
- @done()
- else
- setTimeout (=> @check()),
- options.elements.checkInterval
- done: ->
- @progress = 100
- class DocumentMonitor
- states:
- loading: 0
- interactive: 50
- complete: 100
- constructor: ->
- @progress = @states[document.readyState] ? 100
- _onreadystatechange = document.onreadystatechange
- document.onreadystatechange = =>
- if @states[document.readyState]?
- @progress = @states[document.readyState]
- _onreadystatechange?(arguments...)
- class EventLagMonitor
- constructor: ->
- @progress = 0
- avg = 0
- samples = []
- points = 0
- last = now()
- interval = setInterval =>
- diff = now() - last - 50
- last = now()
- samples.push diff
- if samples.length > options.eventLag.sampleCount
- samples.shift()
- avg = avgAmplitude samples
- if ++points >= options.eventLag.minSamples and avg < options.eventLag.lagThreshold
- @progress = 100
- clearInterval interval
- else
- @progress = 100 * (3 / (avg + 3))
- , 50
- class Scaler
- constructor: (@source) ->
- @last = @sinceLastUpdate = 0
- @rate = options.initialRate
- @catchup = 0
- @progress = @lastProgress = 0
- if @source?
- @progress = result(@source, 'progress')
- tick: (frameTime, val) ->
- val ?= result(@source, 'progress')
- if val >= 100
- @done = true
- if val == @last
- @sinceLastUpdate += frameTime
- else
- if @sinceLastUpdate
- @rate = (val - @last) / @sinceLastUpdate
- @catchup = (val - @progress) / options.catchupTime
- @sinceLastUpdate = 0
- @last = val
- if val > @progress
- # After we've got a datapoint, we have catchupTime to
- # get the progress bar to reflect that new data
- @progress += @catchup * frameTime
- scaling = (1 - Math.pow(@progress / 100, options.easeFactor))
- # Based on the rate of the last update, we preemptively update
- # the progress bar, scaling it so it can never hit 100% until we
- # know it's done.
- @progress += scaling * @rate * frameTime
- @progress = Math.min(@lastProgress + options.maxProgressPerFrame, @progress)
- @progress = Math.max(0, @progress)
- @progress = Math.min(100, @progress)
- @lastProgress = @progress
- @progress
- sources = null
- scalers = null
- bar = null
- uniScaler = null
- animation = null
- cancelAnimation = null
- Pace.running = false
- handlePushState = ->
- if options.restartOnPushState
- Pace.restart()
- # We reset the bar whenever it looks like an ajax navigation has occured.
- if window.history.pushState?
- _pushState = window.history.pushState
- window.history.pushState = ->
- handlePushState()
- _pushState.apply window.history, arguments
- if window.history.replaceState?
- _replaceState = window.history.replaceState
- window.history.replaceState = ->
- handlePushState()
- _replaceState.apply window.history, arguments
- SOURCE_KEYS =
- ajax: AjaxMonitor
- elements: ElementMonitor
- document: DocumentMonitor
- eventLag: EventLagMonitor
- do init = ->
- Pace.sources = sources = []
- for type in ['ajax', 'elements', 'document', 'eventLag']
- if options[type] isnt false
- sources.push new SOURCE_KEYS[type](options[type])
- for source in options.extraSources ? []
- sources.push new source(options)
- Pace.bar = bar = new Bar
- # Each source of progress data has it's own scaler to smooth its output
- scalers = []
- # We have an extra scaler for the final output to keep things looking nice as we add and
- # remove sources
- uniScaler = new Scaler
- Pace.stop = ->
- Pace.trigger 'stop'
- Pace.running = false
- bar.destroy()
- # Not all browsers support cancelAnimationFrame
- cancelAnimation = true
- if animation?
- cancelAnimationFrame? animation
- animation = null
- init()
- Pace.restart = ->
- Pace.trigger 'restart'
- Pace.stop()
- Pace.start()
- Pace.go = ->
- Pace.running = true
- bar.render()
- start = now()
- cancelAnimation = false
- animation = runAnimation (frameTime, enqueueNextFrame) ->
- # Every source gives us a progress number from 0 - 100
- # It's up to us to figure out how to turn that into a smoothly moving bar
- #
- # Their progress numbers can only increment. We try to interpolate
- # between the numbers.
- remaining = 100 - bar.progress
- count = sum = 0
- done = true
- # A source is composed of a bunch of elements, each with a raw, unscaled progress
- for source, i in sources
- scalerList = scalers[i] ?= []
- elements = source.elements ? [source]
- # Each element is given it's own scaler, which turns its value into something
- # smoothed for display
- for element, j in elements
- scaler = scalerList[j] ?= new Scaler element
- done &= scaler.done
- continue if scaler.done
- count++
- sum += scaler.tick(frameTime)
- avg = sum / count
- bar.update uniScaler.tick(frameTime, avg)
- if bar.done() or done or cancelAnimation
- bar.update 100
- Pace.trigger 'done'
- setTimeout ->
- bar.finish()
- Pace.running = false
- Pace.trigger 'hide'
- , Math.max(options.ghostTime, Math.max(options.minTime - (now() - start), 0))
- else
- enqueueNextFrame()
- Pace.start = (_options) ->
- extend options, _options
- Pace.running = true
- try
- bar.render()
- catch NoTargetError
- # It's usually possible to render a bit before the document declares itself ready
- if not document.querySelector('.pace')
- setTimeout Pace.start, 50
- else
- Pace.trigger 'start'
- Pace.go()
- if typeof define is 'function' and define.amd
- # AMD
- define ['pace'], -> Pace
- else if typeof exports is 'object'
- # CommonJS
- module.exports = Pace
- else
- # Global
- if options.startOnPageLoad
- Pace.start()
|