| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
/* ── Numeric count-up ── */ |
| 5 |
function parseValue(str) { |
| 6 |
/* Extract leading/trailing non-numeric parts and the numeric core */ |
| 7 |
var match = str.match(/^([^0-9-]*)(-?[\d,\.]+)([^0-9]*)$/); |
| 8 |
if (!match) return null; |
| 9 |
return { |
| 10 |
prefix: match[1], |
| 11 |
number: parseFloat(match[2].replace(/,/g, '')), |
| 12 |
suffix: match[3], |
| 13 |
raw: match[2], |
| 14 |
hasComma: match[2].indexOf(',') !== -1, |
| 15 |
decimals: (match[2].split('.')[1] || '').length, |
| 16 |
}; |
| 17 |
} |
| 18 |
|
| 19 |
function formatNumber(n, info) { |
| 20 |
var fixed = n.toFixed(info.decimals); |
| 21 |
if (info.hasComma) { |
| 22 |
var parts = fixed.split('.'); |
| 23 |
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); |
| 24 |
fixed = parts.join('.'); |
| 25 |
} |
| 26 |
return info.prefix + fixed + info.suffix; |
| 27 |
} |
| 28 |
|
| 29 |
function countUp(el, info, duration) { |
| 30 |
var start = 0; |
| 31 |
var end = info.number; |
| 32 |
var startTime = null; |
| 33 |
el.classList.add('bkbg-counting'); |
| 34 |
|
| 35 |
function step(ts) { |
| 36 |
if (!startTime) startTime = ts; |
| 37 |
var progress = Math.min((ts - startTime) / duration, 1); |
| 38 |
/* Ease out */ |
| 39 |
var eased = 1 - Math.pow(1 - progress, 3); |
| 40 |
el.textContent = formatNumber(start + (end - start) * eased, info); |
| 41 |
if (progress < 1) { |
| 42 |
requestAnimationFrame(step); |
| 43 |
} else { |
| 44 |
el.textContent = formatNumber(end, info); |
| 45 |
el.classList.remove('bkbg-counting'); |
| 46 |
} |
| 47 |
} |
| 48 |
requestAnimationFrame(step); |
| 49 |
} |
| 50 |
|
| 51 |
function initCard(card) { |
| 52 |
var valueEl = card.querySelector('.bkbg-metric-value'); |
| 53 |
if (!valueEl) return; |
| 54 |
var info = parseValue(valueEl.textContent.trim()); |
| 55 |
if (!info || isNaN(info.number)) return; |
| 56 |
countUp(valueEl, info, 1200); |
| 57 |
} |
| 58 |
|
| 59 |
function observeCards() { |
| 60 |
var cards = document.querySelectorAll('.bkbg-metric-card'); |
| 61 |
if (!cards.length) return; |
| 62 |
|
| 63 |
if ('IntersectionObserver' in window) { |
| 64 |
var io = new IntersectionObserver(function (entries) { |
| 65 |
entries.forEach(function (entry) { |
| 66 |
if (entry.isIntersecting) { |
| 67 |
io.unobserve(entry.target); |
| 68 |
initCard(entry.target); |
| 69 |
} |
| 70 |
}); |
| 71 |
}, { threshold: 0.25 }); |
| 72 |
cards.forEach(function (card) { io.observe(card); }); |
| 73 |
} else { |
| 74 |
cards.forEach(initCard); |
| 75 |
} |
| 76 |
} |
| 77 |
|
| 78 |
if (document.readyState === 'loading') { |
| 79 |
document.addEventListener('DOMContentLoaded', observeCards); |
| 80 |
} else { |
| 81 |
observeCards(); |
| 82 |
} |
| 83 |
}()); |
| 84 |
|