VALUE_HTML = '' RIBBON_HTML = '' + VALUE_HTML + '' DIGIT_HTML = '8' + RIBBON_HTML + '' FORMAT_MARK_HTML = '' # 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 ? '.'), '') val = val.replace /[.,]/g, '' val = val.replace '', '.' 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