virt2/api/jquery/plugins/odometer/0.4.6/odometer.coffee

543 lines
14 KiB
CoffeeScript
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

VALUE_HTML = '<span class="odometer-value"></span>'
RIBBON_HTML = '<span class="odometer-ribbon"><span class="odometer-ribbon-inner">' + VALUE_HTML + '</span></span>'
DIGIT_HTML = '<span class="odometer-digit"><span class="odometer-digit-spacer">8</span><span class="odometer-digit-inner">' + RIBBON_HTML + '</span></span>'
FORMAT_MARK_HTML = '<span class="odometer-formatting-mark"></span>'
# The bit within the parenthesis will be repeated, so (,ddd) becomes 123,456,789....
#
# If your locale uses spaces to seperate digits, you could consider using a
# Narrow No-Break Space (), as it's a bit more correct.
#
# Numbers will be rounded to the number of digits after the radix seperator.
#
# When values are set using `.update` or the `.innerHTML`-type attributes,
# strings are assumed to already be in the locale's format.
#
# This is just the default, it can also be set as options.format.
DIGIT_FORMAT = '(,ddd).dd'
FORMAT_PARSER = /^\(?([^)]*)\)?(?:(.)(d+))?$/
# What is our target framerate?
FRAMERATE = 30
# How long will the animation last?
DURATION = 2000
# What is the fastest we should update values when we are
# counting up (not using the wheel animation).
COUNT_FRAMERATE = 20
# What is the minimum number of frames for each value on the wheel?
# We won't render more values than could be reasonably seen
FRAMES_PER_VALUE = 2
# If more than one digit is hitting the frame limit, they would all get
# capped at that limit and appear to be moving at the same rate. This
# factor adds a boost to subsequent digits to make them appear faster.
DIGIT_SPEEDBOOST = .5
MS_PER_FRAME = 1000 / FRAMERATE
COUNT_MS_PER_FRAME = 1000 / COUNT_FRAMERATE
TRANSITION_END_EVENTS = 'transitionend webkitTransitionEnd oTransitionEnd otransitionend MSTransitionEnd'
transitionCheckStyles = document.createElement('div').style
TRANSITION_SUPPORT = transitionCheckStyles.transition? or transitionCheckStyles.webkitTransition? or
transitionCheckStyles.mozTransition? or transitionCheckStyles.oTransition?
requestAnimationFrame = window.requestAnimationFrame or window.mozRequestAnimationFrame or
window.webkitRequestAnimationFrame or window.msRequestAnimationFrame
MutationObserver = window.MutationObserver or window.WebKitMutationObserver or window.MozMutationObserver
createFromHTML = (html) ->
el = document.createElement('div')
el.innerHTML = html
el.children[0]
removeClass = (el, name) ->
el.className = el.className.replace new RegExp("(^| )#{ name.split(' ').join('|') }( |$)", 'gi'), ' '
addClass = (el, name) ->
removeClass el, name
el.className += " #{ name }"
trigger = (el, name) ->
# Custom DOM events are not supported in IE8
if document.createEvent?
evt = document.createEvent('HTMLEvents')
evt.initEvent(name, true, true)
el.dispatchEvent(evt)
now = ->
window.performance?.now?() ? +new Date
round = (val, precision=0) ->
return Math.round(val) unless precision
val *= Math.pow(10, precision)
val += 0.5
val = Math.floor(val)
val /= Math.pow(10, precision)
truncate = (val) ->
# | 0 fails on numbers greater than 2^32
if val < 0
Math.ceil(val)
else
Math.floor(val)
fractionalPart = (val) ->
val - round(val)
_jQueryWrapped = false
do wrapJQuery = ->
return if _jQueryWrapped
if window.jQuery?
_jQueryWrapped = true
# We need to wrap jQuery's .html and .text because they don't always
# call .innerHTML/.innerText
for property in ['html', 'text']
do (property) ->
old = window.jQuery.fn[property]
window.jQuery.fn[property] = (val) ->
if not val? or not this[0]?.odometer?
return old.apply this, arguments
this[0].odometer.update val
# In case jQuery is brought in after this file
setTimeout wrapJQuery, 0
class Odometer
constructor: (@options) ->
@el = @options.el
return @el.odometer if @el.odometer?
@el.odometer = @
for k, v of Odometer.options
if not @options[k]?
@options[k] = v
@options.duration ?= DURATION
@MAX_VALUES = ((@options.duration / MS_PER_FRAME) / FRAMES_PER_VALUE) | 0
@resetFormat()
@value = @cleanValue(@options.value ? '')
@renderInside()
@render()
try
for property in ['innerHTML', 'innerText', 'textContent'] when @el[property]?
do (property) =>
Object.defineProperty @el, property,
get: =>
if property is 'innerHTML'
@inside.outerHTML
else
# It's just a single HTML element, so innerText is the
# same as outerText.
@inside.innerText ? @inside.textContent
set: (val) =>
@update val
catch e
# Safari
@watchForMutations()
@
renderInside: ->
@inside = document.createElement 'div'
@inside.className = 'odometer-inside'
@el.innerHTML = ''
@el.appendChild @inside
watchForMutations: ->
# Safari doesn't allow us to wrap .innerHTML, so we listen for it
# changing.
return unless MutationObserver?
try
@observer ?= new MutationObserver (mutations) =>
newVal = @el.innerText
@renderInside()
@render @value
@update newVal
@watchMutations = true
@startWatchingMutations()
catch e
startWatchingMutations: ->
if @watchMutations
@observer.observe @el, {childList: true}
stopWatchingMutations: ->
@observer?.disconnect()
cleanValue: (val) ->
if typeof val is 'string'
# We need to normalize the format so we can properly turn it into
# a float.
val = val.replace((@format.radix ? '.'), '<radix>')
val = val.replace /[.,]/g, ''
val = val.replace '<radix>', '.'
val = parseFloat(val, 10) or 0
round(val, @format.precision)
bindTransitionEnd: ->
return if @transitionEndBound
@transitionEndBound = true
# The event will be triggered once for each ribbon, we only
# want one render though
renderEnqueued = false
for event in TRANSITION_END_EVENTS.split(' ')
@el.addEventListener event, =>
return true if renderEnqueued
renderEnqueued = true
setTimeout =>
@render()
renderEnqueued = false
trigger @el, 'odometerdone'
, 0
true
, false
resetFormat: ->
format = @options.format ? DIGIT_FORMAT
format or= 'd'
parsed = FORMAT_PARSER.exec format
if not parsed
throw new Error "Odometer: Unparsable digit format"
[repeating, radix, fractional] = parsed[1..3]
precision = fractional?.length or 0
@format = {repeating, radix, precision}
render: (value=@value) ->
@stopWatchingMutations()
@resetFormat()
@inside.innerHTML = ''
theme = @options.theme
classes = @el.className.split(' ')
newClasses = []
for cls in classes when cls.length
if match = /^odometer-theme-(.+)$/.exec(cls)
theme = match[1]
continue
if /^odometer(-|$)/.test(cls)
continue
newClasses.push cls
newClasses.push 'odometer'
unless TRANSITION_SUPPORT
newClasses.push 'odometer-no-transitions'
if theme
newClasses.push "odometer-theme-#{ theme }"
else
# This class matches all themes, so it should do what you'd expect if only one
# theme css file is brought into the page.
newClasses.push "odometer-auto-theme"
@el.className = newClasses.join(' ')
@ribbons = {}
@digits = []
wholePart = not @format.precision or not fractionalPart(value) or false
for digit in value.toString().split('').reverse()
if digit is '.'
wholePart = true
@addDigit digit, wholePart
@startWatchingMutations()
update: (newValue) ->
newValue = @cleanValue newValue
return unless diff = newValue - @value
removeClass @el, 'odometer-animating-up odometer-animating-down odometer-animating'
if diff > 0
addClass @el, 'odometer-animating-up'
else
addClass @el, 'odometer-animating-down'
@stopWatchingMutations()
@animate newValue
@startWatchingMutations()
setTimeout =>
# Force a repaint
@el.offsetHeight
addClass @el, 'odometer-animating'
, 0
@value = newValue
renderDigit: ->
createFromHTML DIGIT_HTML
insertDigit: (digit, before) ->
if before?
@inside.insertBefore digit, before
else if not @inside.children.length
@inside.appendChild digit
else
@inside.insertBefore digit, @inside.children[0]
addSpacer: (chr, before, extraClasses) ->
spacer = createFromHTML FORMAT_MARK_HTML
spacer.innerHTML = chr
addClass(spacer, extraClasses) if extraClasses
@insertDigit spacer, before
addDigit: (value, repeating=true) ->
if value is '-'
return @addSpacer value, null, 'odometer-negation-mark'
if value is '.'
return @addSpacer (@format.radix ? '.'), null, 'odometer-radix-mark'
if repeating
resetted = false
while true
if not @format.repeating.length
if resetted
throw new Error "Bad odometer format without digits"
@resetFormat()
resetted = true
chr = @format.repeating[@format.repeating.length - 1]
@format.repeating = @format.repeating.substring(0, @format.repeating.length - 1)
break if chr is 'd'
@addSpacer chr
digit = @renderDigit()
digit.querySelector('.odometer-value').innerHTML = value
@digits.push digit
@insertDigit digit
animate: (newValue) ->
if not TRANSITION_SUPPORT or @options.animation is 'count'
@animateCount newValue
else
@animateSlide newValue
animateCount: (newValue) ->
return unless diff = +newValue - @value
start = last = now()
cur = @value
do tick = =>
if (now() - start) > @options.duration
@value = newValue
@render()
trigger @el, 'odometerdone'
return
delta = now() - last
if delta > COUNT_MS_PER_FRAME
last = now()
fraction = delta / @options.duration
dist = diff * fraction
cur += dist
@render Math.round cur
if requestAnimationFrame?
requestAnimationFrame tick
else
setTimeout tick, COUNT_MS_PER_FRAME
getDigitCount: (values...) ->
for value, i in values
values[i] = Math.abs(value)
max = Math.max values...
Math.ceil(Math.log(max + 1) / Math.log(10))
getFractionalDigitCount: (values...) ->
# This assumes the value has already been rounded to
# @format.precision places
#
parser = /^\-?\d*\.(\d*?)0*$/
for value, i in values
values[i] = value.toString()
parts = parser.exec values[i]
if not parts?
values[i] = 0
else
values[i] = parts[1].length
Math.max values...
resetDigits: ->
@digits = []
@ribbons = []
@inside.innerHTML = ''
@resetFormat()
animateSlide: (newValue) ->
oldValue = @value
fractionalCount = @getFractionalDigitCount oldValue, newValue
if fractionalCount
newValue = newValue * Math.pow(10, fractionalCount)
oldValue = oldValue * Math.pow(10, fractionalCount)
return unless diff = newValue - oldValue
@bindTransitionEnd()
digitCount = @getDigitCount(oldValue, newValue)
digits = []
boosted = 0
# We create a array to represent the series of digits which should be
# animated in each column
for i in [0...digitCount]
start = truncate(oldValue / Math.pow(10, (digitCount - i - 1)))
end = truncate(newValue / Math.pow(10, (digitCount - i - 1)))
dist = end - start
if Math.abs(dist) > @MAX_VALUES
# We need to subsample
frames = []
# Subsequent digits need to be faster than previous ones
incr = dist / (@MAX_VALUES + @MAX_VALUES * boosted * DIGIT_SPEEDBOOST)
cur = start
while (dist > 0 and cur < end) or (dist < 0 and cur > end)
frames.push Math.round cur
cur += incr
if frames[frames.length - 1] isnt end
frames.push end
boosted++
else
frames = [start..end]
# We only care about the last digit
for frame, i in frames
frames[i] = Math.abs(frame % 10)
digits.push frames
@resetDigits()
for frames, i in digits.reverse()
if not @digits[i]
@addDigit ' ', (i >= fractionalCount)
@ribbons[i] ?= @digits[i].querySelector('.odometer-ribbon-inner')
@ribbons[i].innerHTML = ''
if diff < 0
frames = frames.reverse()
for frame, j in frames
numEl = document.createElement('div')
numEl.className = 'odometer-value'
numEl.innerHTML = frame
@ribbons[i].appendChild numEl
if j == frames.length - 1
addClass numEl, 'odometer-last-value'
if j == 0
addClass numEl, 'odometer-first-value'
if start < 0
@addDigit '-'
mark = @inside.querySelector('.odometer-radix-mark')
mark.parent.removeChild(mark) if mark?
if fractionalCount
@addSpacer @format.radix, @digits[fractionalCount - 1], 'odometer-radix-mark'
Odometer.options = window.odometerOptions ? {}
setTimeout ->
# We do this in a seperate pass to allow people to set
# window.odometerOptions after bringing the file in.
if window.odometerOptions
for k, v of window.odometerOptions
Odometer.options[k] ?= v
, 0
Odometer.init = ->
if not document.querySelectorAll?
# IE 7 or 8 in Quirksmode
return
elements = document.querySelectorAll (Odometer.options.selector or '.odometer')
for el in elements
el.odometer = new Odometer {el, value: (el.innerText ? el.textContent)}
if document.documentElement?.doScroll? and document.createEventObject?
# IE < 9
_old = document.onreadystatechange
document.onreadystatechange = ->
if document.readyState is 'complete' and Odometer.options.auto isnt false
Odometer.init()
_old?.apply this, arguments
else
document.addEventListener 'DOMContentLoaded', ->
if Odometer.options.auto isnt false
Odometer.init()
, false
if typeof define is 'function' and define.amd
# AMD. Register as an anonymous module.
define ['jquery'], ->
Odometer
else if typeof exports is not 'undefined'
# CommonJS
module.exports = Odometer
else
# Browser globals
window.Odometer = Odometer