(function ( $, window ) {
var
pluginName = 'countimator',
defaults = {
// Values
count: 0,
value: null,
min: null,
max: 0,
// Animation options
duration: 1000,
// Property selector
countSelector: '.counter-count',
maxSelector: '.counter-max',
// Template options
template: null,
engine: null,
// Trigger animation options
animateOnInit: true,
animateOnAppear: true,
// Format options
decimals: 0,
decimalDelimiter: '.',
thousandDelimiter: null,
pad: false,
// Style plugin
style: null,
// Callbacks
start: function() {},
step: function(step) {},
complete: function() {}
},
/**
* Format a number
* @param {Object} n
* @param {Object} decimals
* @param {Object} decimalDelimiter
* @param {Object} thousandDelimiter
*/
formatNumber = function(number, decimals, decimalDelimiter, thousandDelimiter) {
decimals = isNaN(decimals = Math.abs(decimals)) ? 2 : decimals;
decimalDelimiter = typeof decimalDelimiter === 'undefined' ? "." : decimalDelimiter;
thousandDelimiter = typeof thousandDelimiter === 'undefined' ? "," : thousandDelimiter;
thousandDelimiter = typeof thousandDelimiter === 'string' ? thousandDelimiter : "";
var
s = number < 0 ? "-" : "",
n = Math.abs(+number || 0).toFixed(decimals),
i = String(parseInt(n)),
j = (i.length > 3 ? i.length % 3 : 0);
return s + (j ? i.substr(0, j) + thousandDelimiter : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + thousandDelimiter) + (decimals ? decimalDelimiter + Math.abs(n - i).toFixed(decimals).slice(2) : "");
},
/**
* Pad a number with leading zeros
* @param {Object} number
* @param {Object} length
*/
pad = function(number, length) {
var str = '' + number;
while (str.length < length) {
str = '0' + str;
}
return str;
},
/**
* Return parent's textnodes
* @param {Object} parent
*/
textNodes = function(parent) {
return $(parent).contents().filter(function () {
return this.nodeType === 3;
});
},
/**
* Detect if element is in viewport
* @param {Object} elem
*/
inView = function(elem){
var
$elem = $(elem),
$window = $(window),
docViewTop = $window.scrollTop(),
docViewBottom = docViewTop + $window.height(),
elemTop = $elem.offset().top,
elemBottom = elemTop + $elem.height();
return ((elemBottom <= docViewBottom) && (elemTop >= docViewTop));
},
/**
* A Polyfill for requestAnimationFrame
*/
requestAnimationFrame =
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame ||
window.oRequestAnimationFrame ||
function( callback ){
window.setTimeout(callback, 1000 / 60);
};
/**
* Countimator
* @param {Object} element
* @param {Object} options
*/
function Countimator(element, options) {
var
instance = this,
$element = $(element),
animating = false,
startTime, startCount;
options = $.extend({}, defaults, options, $element.data());
// Private Methods
function init() {
var
value = getValue(),
count = getCount(),
countNode,
max = getMax(),
maxNode,
script;
// Init values
if (!count) {
countNode = getCountNode();
if (countNode) {
if (typeof options.value !== 'number') {
options.value = countNode.nodeValue;
} else {
options.count = countNode.nodeValue;
}
}
}
if (!max) {
maxNode = getMaxNode();
if (maxNode) {
options.max = maxNode.nodeValue;
}
}
// Init template
script = $element.find("script[type*='text/x-']");
if (script.length) {
options.template = script.html();
script.remove();
}
// Init listeners
$(window).on('resize', function() {
resize.call(instance);
});
function scrollListener() {
if (options.animateOnInit && options.animateOnAppear && inView(element)) {
$(window).off('scroll touchmove', scrollListener);
start.call(instance);
}
}
$(window).on('scroll touchmove', scrollListener);
if (options.animateOnInit) {
if (options.animateOnAppear && inView(element)) {
options.count = typeof count === 'number' ? count : 0;
start.call(instance);
} else {
render.call(this);
}
} else {
options.count = getValue();
render.call(this);
}
resize.call(this);
}
function setOption(name, value) {
var
old = options[name];
options[name] = value;
switch (name) {
case 'value':
if (old === value) {
return;
}
if (typeof old !== 'number') {
options['count'] = value;
render.call(this);
} else {
options['count'] = old;
start();
}
break;
}
}
function getMin() {
var
min = parseFloat(options.min);
return isNaN(min) ? 0 : min;
}
function getMax() {
var
max = parseFloat(options.max);
return isNaN(max) ? 0 : max;
}
function getValue() {
var
max = getMax(),
min = getMin(),
count = getCount(),
value = parseFloat(options.value);
if (isNaN(value)) {
value = min;
}
return value;
}
function getCount() {
var
max = getMax(),
min = getMin(),
count = parseFloat(options.count);
if (isNaN(count)) {
count = min;
}
return count;
}
function resize() {
}
function getCountNode(count) {
var
countElement = $element.find(options.countSelector)[0];
if (!countElement) {
countElement = $element.find("*").last().siblings().addBack()[0];
}
return textNodes(countElement || element)[0];
}
function getMaxNode(count) {
var
maxElement = $element.find(options.maxSelector)[0];
if (maxElement) {
return textNodes(maxElement)[0];
}
return null;
}
function getFormattedValue(value) {
// format number
var
decimals = options.decimals,
decimalDelimiter = options.decimalDelimiter,
thousandDelimiter = options.thousandDelimiter,
string = formatNumber(value, decimals, decimalDelimiter, thousandDelimiter);
// Pad
string = pad(string, options.pad);
return string;
}
function render() {
var
max = getMax(),
min = getMin(),
value = getValue(),
count = getCount(),
formattedCount = getFormattedValue(count),
formattedValue = getFormattedValue(value),
formattedMax = getFormattedValue(max),
formattedMin = getFormattedValue(min),
engine = options.engine || typeof window['Handlebars'] !== 'undefined' ? window['Handlebars'] : null,
template = options.template,
string, div, $template, tmpl, tmplData, nodeList, countNode, maxNode, style;
try {
$template = $(options.template);
template = $template.length && $template[0].innerHTML || template;
} catch (e) {
// Template is not a dom element
}
if (engine && template) {
// Template engine
tmpl = engine.compile(template);
if (tmpl) {
tmplData = $.extend({}, options, {count: formattedCount, value: formattedValue, max: formattedMax, min: formattedMin});
string = tmpl(tmplData);
}
div = document.createElement('div');
div.innerHTML = string;
nodeList = div.childNodes;
$(element).contents().remove();
$(element).append(nodeList);
} else {
// Classic approach without a template engine
countNode = getCountNode();
if (countNode) {
countNode.nodeValue = formattedCount;
}
maxNode = getMaxNode();
if (maxNode) {
maxNode.nodeValue = formattedMax;
}
if (!countNode && !maxNode) {
element.innerHTML = formattedCount;
}
}
if (options.style) {
style = $.fn[pluginName].getStyle(options.style);
if (style && style.render) {
style.render.call(element, count, options);
}
}
}
function animate(value) {
options.value = value;
if (!animating) {
start();
}
}
function start() {
if (!animating) {
startTime = new Date().getTime();
startCount = getCount();
animating = true;
if (typeof options.start === 'function') {
options.start.call(element);
}
requestAnimationFrame(step);
}
}
function step() {
var
duration = options.duration,
max = getMax(),
value = getValue(),
currentTime = new Date().getTime(),
endTime = startTime + duration,
currentStep = Math.min((duration - (endTime - currentTime)) / duration, 1),
count = startCount + currentStep * (value - startCount);
options.count = count;
render.call(this);
// Step Callback
if (typeof options.step === 'function') {
options.step.call(element, count, options);
}
if (currentStep < 1 && animating) {
// Run loop
requestAnimationFrame(step);
} else {
// Complete
stop.call(this);
}
}
function stop() {
animating = false;
if (typeof options.complete === 'function') {
options.complete.call(element);
}
}
// Public methods
this.resize = function() {
resize.call(this);
};
this.animate = function(value) {
animate.call(this, value);
};
this.setOptions = function(opts) {
var old = this.getOptions();
$.extend(true, options, opts);
if (options.value !== old.value) {
start();
}
};
this.getOptions = function() {
return $.extend(true, {}, options);
};
// Init
init.call(this);
}
// Bootstrap JQuery-Plugin
$.fn[pluginName] = function(options) {
return this.each(function() {
var
opts = $.extend(true, {}, options),
countimator = $(this).data(pluginName);
if (!countimator) {
$(this).data(pluginName, new Countimator(this, opts));
} else {
countimator.setOptions(opts);
}
return $(this);
});
};
// Style api
(function() {
var
styles = {};
$.fn[pluginName].registerStyle = function(name, def) {
styles[name] = def;
};
$.fn[pluginName].getStyle = function(name) {
return styles[name];
};
})();
})( jQuery, window );