| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
var _typoKeys = { |
| 4 |
family:'font-family', weight:'font-weight', style:'font-style', |
| 5 |
decoration:'text-decoration', transform:'text-transform', |
| 6 |
sizeDesktop:'font-size-d', sizeTablet:'font-size-t', sizeMobile:'font-size-m', |
| 7 |
lineHeightDesktop:'line-height-d', lineHeightTablet:'line-height-t', lineHeightMobile:'line-height-m', |
| 8 |
letterSpacingDesktop:'letter-spacing-d', letterSpacingTablet:'letter-spacing-t', letterSpacingMobile:'letter-spacing-m', |
| 9 |
wordSpacingDesktop:'word-spacing-d', wordSpacingTablet:'word-spacing-t', wordSpacingMobile:'word-spacing-m' |
| 10 |
}; |
| 11 |
function typoCssVarsForEl(el, obj, prefix) { |
| 12 |
if (!obj || typeof obj !== 'object') return; |
| 13 |
Object.keys(_typoKeys).forEach(function (k) { |
| 14 |
var v = obj[k]; |
| 15 |
if (v === undefined || v === '' || v === null) return; |
| 16 |
if (k === 'sizeDesktop' || k === 'sizeTablet' || k === 'sizeMobile') v = v + (obj.sizeUnit || 'px'); |
| 17 |
else if (k === 'lineHeightDesktop' || k === 'lineHeightTablet' || k === 'lineHeightMobile') v = v + (obj.lineHeightUnit || ''); |
| 18 |
else if (k === 'letterSpacingDesktop' || k === 'letterSpacingTablet' || k === 'letterSpacingMobile') v = v + (obj.letterSpacingUnit || 'px'); |
| 19 |
else if (k === 'wordSpacingDesktop' || k === 'wordSpacingTablet' || k === 'wordSpacingMobile') v = v + (obj.wordSpacingUnit || 'px'); |
| 20 |
el.style.setProperty(prefix + _typoKeys[k], String(v)); |
| 21 |
}); |
| 22 |
} |
| 23 |
/* ── Sparkline SVG builder ─────────────────────────────────────── */ |
| 24 |
function buildSparkline(container, sparkStr, color, fill, width, height) { |
| 25 |
var nums = (sparkStr || '').split(',').map(Number).filter(function (n) { return !isNaN(n); }); |
| 26 |
if (nums.length < 2) return; |
| 27 |
var minV = Math.min.apply(null, nums); |
| 28 |
var maxV = Math.max.apply(null, nums); |
| 29 |
var range = maxV - minV || 1; |
| 30 |
var h = height; var w = width; |
| 31 |
var step = w / (nums.length - 1); |
| 32 |
var pts = nums.map(function (n, i) { |
| 33 |
return (i * step).toFixed(1) + ',' + ((1 - (n - minV) / range) * (h - 4) + 2).toFixed(1); |
| 34 |
}); |
| 35 |
var polyPts = pts.join(' '); |
| 36 |
var fillD = 'M ' + pts.join(' L ') + ' L ' + w + ',' + h + ' L 0,' + h + ' Z'; |
| 37 |
|
| 38 |
var ns = 'http://www.w3.org/2000/svg'; |
| 39 |
var svg = document.createElementNS(ns, 'svg'); |
| 40 |
svg.setAttribute('viewBox', '0 0 ' + w + ' ' + h); |
| 41 |
svg.setAttribute('width', w); |
| 42 |
svg.setAttribute('height', h); |
| 43 |
svg.classList.add('bkbg-kpi-spark'); |
| 44 |
svg.style.overflow = 'visible'; |
| 45 |
|
| 46 |
var path = document.createElementNS(ns, 'path'); |
| 47 |
path.setAttribute('d', fillD); |
| 48 |
path.setAttribute('fill', fill); |
| 49 |
path.setAttribute('stroke', 'none'); |
| 50 |
svg.appendChild(path); |
| 51 |
|
| 52 |
var poly = document.createElementNS(ns, 'polyline'); |
| 53 |
poly.setAttribute('points', polyPts); |
| 54 |
poly.setAttribute('fill', 'none'); |
| 55 |
poly.setAttribute('stroke', color); |
| 56 |
poly.setAttribute('stroke-width', '2'); |
| 57 |
poly.setAttribute('stroke-linecap', 'round'); |
| 58 |
poly.setAttribute('stroke-linejoin', 'round'); |
| 59 |
svg.appendChild(poly); |
| 60 |
|
| 61 |
container.appendChild(svg); |
| 62 |
} |
| 63 |
|
| 64 |
/* ── Trend helpers ─────────────────────────────────────────────── */ |
| 65 |
function trendIcon(dir) { |
| 66 |
return dir === 'up' ? '↑' : dir === 'down' ? '↓' : dir === 'down-good' ? '↓' : '→'; |
| 67 |
} |
| 68 |
function trendClass(dir) { |
| 69 |
return dir === 'up' ? 'bkbg-kpi-trend-up' : |
| 70 |
dir === 'down' ? 'bkbg-kpi-trend-down' : |
| 71 |
dir === 'down-good' ? 'bkbg-kpi-trend-downgood' : 'bkbg-kpi-trend-neutral'; |
| 72 |
} |
| 73 |
function trendBgColor(dir, o) { |
| 74 |
return dir === 'up' ? o.upColor : |
| 75 |
dir === 'down' ? o.downColor : |
| 76 |
dir === 'down-good' ? o.downGoodColor : '#64748b'; |
| 77 |
} |
| 78 |
|
| 79 |
/* ── Number animation counter ──────────────────────────────────── */ |
| 80 |
function animateValue(el, endStr, duration) { |
| 81 |
/* Extract leading/trailing non-numeric for display */ |
| 82 |
var match = endStr.match(/^([^0-9]*)([0-9,\.]+)([^0-9]*)$/); |
| 83 |
if (!match) return; /* not a simple number */ |
| 84 |
var pre = match[1]; |
| 85 |
var numStr = match[2].replace(/,/g, ''); |
| 86 |
var suf = match[3]; |
| 87 |
var end = parseFloat(numStr); |
| 88 |
if (isNaN(end)) return; |
| 89 |
var isInt = Number.isInteger(end); |
| 90 |
var start = 0; |
| 91 |
var startTime = null; |
| 92 |
function step(timestamp) { |
| 93 |
if (!startTime) startTime = timestamp; |
| 94 |
var progress = Math.min((timestamp - startTime) / duration, 1); |
| 95 |
var eased = 1 - Math.pow(1 - progress, 3); |
| 96 |
var cur = start + (end - start) * eased; |
| 97 |
var display = isInt ? Math.round(cur).toLocaleString() : cur.toFixed(1); |
| 98 |
el.textContent = pre + display + suf; |
| 99 |
if (progress < 1) requestAnimationFrame(step); |
| 100 |
} |
| 101 |
requestAnimationFrame(step); |
| 102 |
} |
| 103 |
|
| 104 |
/* ── Build one tile ────────────────────────────────────────────── */ |
| 105 |
function buildTile(tile, o) { |
| 106 |
var dir = tile.direction || 'up'; |
| 107 |
var tileEl = document.createElement('div'); |
| 108 |
tileEl.className = 'bkbg-kpi-tile'; |
| 109 |
tileEl.style.cssText = [ |
| 110 |
'background:' + o.cardBg, |
| 111 |
'border-radius:' + o.cardRadius + 'px', |
| 112 |
'padding:' + o.cardPadding + 'px', |
| 113 |
'border:1px solid ' + (o.borderColor || 'transparent'), |
| 114 |
'box-shadow:0 2px 16px ' + (o.cardShadow || 'rgba(0,0,0,0.08)') |
| 115 |
].join(';'); |
| 116 |
|
| 117 |
/* Top row */ |
| 118 |
var top = document.createElement('div'); |
| 119 |
top.className = 'bkbg-kpi-top'; |
| 120 |
|
| 121 |
if (o.showIcon !== false && tile.icon) { |
| 122 |
var icon = document.createElement('span'); |
| 123 |
icon.className = 'bkbg-kpi-icon'; |
| 124 |
icon.style.fontSize = o.iconSize + 'px'; |
| 125 |
var _IP = window.bkbgIconPicker; |
| 126 |
var _iType = tile.iconType || 'custom-char'; |
| 127 |
if (_IP && _iType !== 'custom-char') { |
| 128 |
var _in = _IP.buildFrontendIcon(_iType, tile.icon, tile.iconDashicon, tile.iconImageUrl, tile.iconDashiconColor); |
| 129 |
if (_in) icon.appendChild(_in); else icon.textContent = tile.icon; |
| 130 |
} else { icon.textContent = tile.icon; } |
| 131 |
top.appendChild(icon); |
| 132 |
} |
| 133 |
|
| 134 |
if (o.showTrend !== false && tile.trend) { |
| 135 |
var trend = document.createElement('span'); |
| 136 |
trend.className = 'bkbg-kpi-trend ' + trendClass(dir); |
| 137 |
trend.style.color = trendBgColor(dir, o); |
| 138 |
trend.textContent = trendIcon(dir) + ' ' + tile.trend; |
| 139 |
top.appendChild(trend); |
| 140 |
} |
| 141 |
tileEl.appendChild(top); |
| 142 |
|
| 143 |
/* Value */ |
| 144 |
var val = document.createElement('div'); |
| 145 |
val.className = 'bkbg-kpi-value'; |
| 146 |
val.style.color = o.valueColor || '#0f172a'; |
| 147 |
val.textContent = tile.value || '—'; |
| 148 |
tileEl.appendChild(val); |
| 149 |
|
| 150 |
/* Label */ |
| 151 |
var lbl = document.createElement('div'); |
| 152 |
lbl.className = 'bkbg-kpi-label'; |
| 153 |
lbl.style.color = o.labelColor || '#64748b'; |
| 154 |
lbl.textContent = tile.label || ''; |
| 155 |
tileEl.appendChild(lbl); |
| 156 |
|
| 157 |
/* Sparkline */ |
| 158 |
if (o.showSparkline !== false && tile.sparkline) { |
| 159 |
buildSparkline(tileEl, tile.sparkline, |
| 160 |
o.sparklineColor || '#6366f1', |
| 161 |
o.sparklineFill || 'rgba(99,102,241,0.12)', |
| 162 |
o.sparklineWidth || 120, |
| 163 |
o.sparklineHeight || 48 |
| 164 |
); |
| 165 |
} |
| 166 |
|
| 167 |
return { el: tileEl, valEl: val, tile: tile }; |
| 168 |
} |
| 169 |
|
| 170 |
/* ── Intersection observer for number animation ─────────────────── */ |
| 171 |
function observeTiles(items, o) { |
| 172 |
if (!o.animateNumbers || !window.IntersectionObserver) return; |
| 173 |
var io = new IntersectionObserver(function (entries) { |
| 174 |
entries.forEach(function (entry) { |
| 175 |
if (!entry.isIntersecting) return; |
| 176 |
var target = entry.target; |
| 177 |
var idx = parseInt(target.dataset.kpiIdx, 10); |
| 178 |
if (!isNaN(idx) && items[idx]) { |
| 179 |
animateValue(items[idx].valEl, items[idx].tile.value, 1200); |
| 180 |
} |
| 181 |
io.unobserve(target); |
| 182 |
}); |
| 183 |
}, { threshold: 0.3 }); |
| 184 |
|
| 185 |
items.forEach(function (item, idx) { |
| 186 |
item.el.dataset.kpiIdx = idx; |
| 187 |
io.observe(item.el); |
| 188 |
}); |
| 189 |
} |
| 190 |
|
| 191 |
/* ── Mount ─────────────────────────────────────────────────────── */ |
| 192 |
function initBlock(root) { |
| 193 |
var app = root.querySelector('.bkbg-kpi-app'); |
| 194 |
if (!app) return; |
| 195 |
|
| 196 |
var raw = app.getAttribute('data-opts') || '{}'; |
| 197 |
var o; |
| 198 |
try { o = JSON.parse(raw); } catch (e) { o = {}; } |
| 199 |
|
| 200 |
var tiles = Array.isArray(o.tiles) ? o.tiles : []; |
| 201 |
var cols = Number(o.columns) || 4; |
| 202 |
|
| 203 |
typoCssVarsForEl(root, o.valueTypo, '--bkbg-kpi-v-'); |
| 204 |
typoCssVarsForEl(root, o.labelTypo, '--bkbg-kpi-l-'); |
| 205 |
typoCssVarsForEl(root, o.trendTypo, '--bkbg-kpi-tr-'); |
| 206 |
|
| 207 |
var grid = document.createElement('div'); |
| 208 |
grid.className = 'bkbg-kpi-grid'; |
| 209 |
grid.style.gridTemplateColumns = 'repeat(' + cols + ', 1fr)'; |
| 210 |
|
| 211 |
var items = tiles.map(function (tile) { |
| 212 |
var item = buildTile(tile, o); |
| 213 |
grid.appendChild(item.el); |
| 214 |
return item; |
| 215 |
}); |
| 216 |
|
| 217 |
app.appendChild(grid); |
| 218 |
observeTiles(items, o); |
| 219 |
} |
| 220 |
|
| 221 |
function init() { |
| 222 |
document.querySelectorAll('.wp-block-blockenberg-kpi-tiles').forEach(initBlock); |
| 223 |
} |
| 224 |
|
| 225 |
if (document.readyState === 'loading') { |
| 226 |
document.addEventListener('DOMContentLoaded', init); |
| 227 |
} else { |
| 228 |
init(); |
| 229 |
} |
| 230 |
})(); |
| 231 |
|