| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
/* ── Calculation helpers ──────────────────────────────────────────────── */ |
| 5 |
function calcBMI(system, weightKg, heightCm, weightLbs, heightFt, heightIn) { |
| 6 |
var bmi; |
| 7 |
if (system === 'metric') { |
| 8 |
var hM = heightCm / 100; |
| 9 |
bmi = hM > 0 ? weightKg / (hM * hM) : 0; |
| 10 |
} else { |
| 11 |
var totalIn = (heightFt * 12) + heightIn; |
| 12 |
bmi = totalIn > 0 ? (703 * weightLbs) / (totalIn * totalIn) : 0; |
| 13 |
} |
| 14 |
return Math.round(bmi * 10) / 10; |
| 15 |
} |
| 16 |
|
| 17 |
function bmiCategory(bmi) { |
| 18 |
if (bmi < 18.5) return 'underweight'; |
| 19 |
if (bmi < 25) return 'normal'; |
| 20 |
if (bmi < 30) return 'overweight'; |
| 21 |
return 'obese'; |
| 22 |
} |
| 23 |
|
| 24 |
function bmiPct(bmi) { |
| 25 |
var clamped = Math.min(Math.max(bmi, 10), 40); |
| 26 |
return ((clamped - 10) / 30) * 100; |
| 27 |
} |
| 28 |
|
| 29 |
function idealRange(system, heightCm, heightFt, heightIn) { |
| 30 |
var minBMI = 18.5, maxBMI = 24.9; |
| 31 |
if (system === 'metric') { |
| 32 |
var hM = heightCm / 100; |
| 33 |
return { min: (minBMI * hM * hM).toFixed(1), max: (maxBMI * hM * hM).toFixed(1), unit: 'kg' }; |
| 34 |
} else { |
| 35 |
var totalIn = heightFt * 12 + heightIn; |
| 36 |
return { |
| 37 |
min: ((minBMI * totalIn * totalIn) / 703).toFixed(1), |
| 38 |
max: ((maxBMI * totalIn * totalIn) / 703).toFixed(1), |
| 39 |
unit: 'lbs' |
| 40 |
}; |
| 41 |
} |
| 42 |
} |
| 43 |
|
| 44 |
var CAT_INTERP = { |
| 45 |
underweight: 'Consider speaking with a healthcare professional about healthy weight gain.', |
| 46 |
normal: 'Great job! You are in a healthy weight range.', |
| 47 |
overweight: 'Small lifestyle changes can bring you into a healthy range.', |
| 48 |
obese: 'Consult a healthcare professional for personalized guidance.', |
| 49 |
}; |
| 50 |
|
| 51 |
function typoCssVarsForEl(typo, prefix, el) { |
| 52 |
if (!typo) return; |
| 53 |
if (typo.family) el.style.setProperty(prefix + 'font-family', "'" + typo.family + "', sans-serif"); |
| 54 |
if (typo.weight) el.style.setProperty(prefix + 'font-weight', typo.weight); |
| 55 |
if (typo.transform) el.style.setProperty(prefix + 'text-transform', typo.transform); |
| 56 |
if (typo.style) el.style.setProperty(prefix + 'font-style', typo.style); |
| 57 |
if (typo.decoration) el.style.setProperty(prefix + 'text-decoration', typo.decoration); |
| 58 |
var su = typo.sizeUnit || 'px'; |
| 59 |
if (typo.sizeDesktop !== '' && typo.sizeDesktop != null) el.style.setProperty(prefix + 'font-size-d', typo.sizeDesktop + su); |
| 60 |
if (typo.sizeTablet !== '' && typo.sizeTablet != null) el.style.setProperty(prefix + 'font-size-t', typo.sizeTablet + su); |
| 61 |
if (typo.sizeMobile !== '' && typo.sizeMobile != null) el.style.setProperty(prefix + 'font-size-m', typo.sizeMobile + su); |
| 62 |
var lhu = typo.lineHeightUnit || ''; |
| 63 |
if (typo.lineHeightDesktop !== '' && typo.lineHeightDesktop != null) el.style.setProperty(prefix + 'line-height-d', typo.lineHeightDesktop + lhu); |
| 64 |
if (typo.lineHeightTablet !== '' && typo.lineHeightTablet != null) el.style.setProperty(prefix + 'line-height-t', typo.lineHeightTablet + lhu); |
| 65 |
if (typo.lineHeightMobile !== '' && typo.lineHeightMobile != null) el.style.setProperty(prefix + 'line-height-m', typo.lineHeightMobile + lhu); |
| 66 |
var lsu = typo.letterSpacingUnit || 'px'; |
| 67 |
if (typo.letterSpacingDesktop !== '' && typo.letterSpacingDesktop != null) el.style.setProperty(prefix + 'letter-spacing-d', typo.letterSpacingDesktop + lsu); |
| 68 |
if (typo.letterSpacingTablet !== '' && typo.letterSpacingTablet != null) el.style.setProperty(prefix + 'letter-spacing-t', typo.letterSpacingTablet + lsu); |
| 69 |
if (typo.letterSpacingMobile !== '' && typo.letterSpacingMobile != null) el.style.setProperty(prefix + 'letter-spacing-m', typo.letterSpacingMobile + lsu); |
| 70 |
var wsu = typo.wordSpacingUnit || 'px'; |
| 71 |
if (typo.wordSpacingDesktop !== '' && typo.wordSpacingDesktop != null) el.style.setProperty(prefix + 'word-spacing-d', typo.wordSpacingDesktop + wsu); |
| 72 |
if (typo.wordSpacingTablet !== '' && typo.wordSpacingTablet != null) el.style.setProperty(prefix + 'word-spacing-t', typo.wordSpacingTablet + wsu); |
| 73 |
if (typo.wordSpacingMobile !== '' && typo.wordSpacingMobile != null) el.style.setProperty(prefix + 'word-spacing-m', typo.wordSpacingMobile + wsu); |
| 74 |
} |
| 75 |
|
| 76 |
/* ── Build the full UI into the app container ─────────────────────────── */ |
| 77 |
function buildApp(app) { |
| 78 |
var opts = {}; |
| 79 |
try { opts = JSON.parse(app.dataset.opts || '{}'); } catch (e) {} |
| 80 |
|
| 81 |
var system = opts.unitSystem || 'metric'; |
| 82 |
var showToggle = opts.showUnitToggle !== false; |
| 83 |
|
| 84 |
/* current values */ |
| 85 |
var weightKg = opts.weightDefaultKg || 70; |
| 86 |
var heightCm = opts.heightDefaultCm || 170; |
| 87 |
var weightLbs = opts.weightDefaultLbs || 154; |
| 88 |
var heightFt = opts.heightDefaultFt || 5; |
| 89 |
var heightIn = parseInt(opts.heightDefaultIn) || 7; |
| 90 |
|
| 91 |
var catColors = { |
| 92 |
underweight: opts.underweightColor || '#3b82f6', |
| 93 |
normal: opts.normalColor || '#10b981', |
| 94 |
overweight: opts.overweightColor || '#f59e0b', |
| 95 |
obese: opts.obeseColor || '#ef4444', |
| 96 |
}; |
| 97 |
var catLabels = { underweight: 'Underweight', normal: 'Normal', overweight: 'Overweight', obese: 'Obese' }; |
| 98 |
var accent = opts.accentColor || '#6c3fb5'; |
| 99 |
var cardBg = opts.cardBg || '#ffffff'; |
| 100 |
var resultBg = opts.resultBg || '#f5f3ff'; |
| 101 |
var resultBdr = opts.resultBorder || '#ede9fe'; |
| 102 |
var labelColor= opts.labelColor || '#374151'; |
| 103 |
var radius = (opts.cardRadius || 16) + 'px'; |
| 104 |
var btnR = (opts.btnRadius || 8) + 'px'; |
| 105 |
var maxW = (opts.maxWidth || 680) + 'px'; |
| 106 |
var ptop = (opts.paddingTop || 60) + 'px'; |
| 107 |
var pbot = (opts.paddingBottom || 60) + 'px'; |
| 108 |
|
| 109 |
/* --- Outer container --- */ |
| 110 |
app.innerHTML = ''; |
| 111 |
app.style.cssText = 'box-sizing:border-box;'; |
| 112 |
|
| 113 |
var card = document.createElement('div'); |
| 114 |
card.className = 'bkbg-bmi-card'; |
| 115 |
card.style.cssText = 'background:' + cardBg + ';border-radius:' + radius + ';padding:32px;box-shadow:0 4px 24px rgba(0,0,0,0.08);max-width:' + maxW + ';margin:0 auto;padding-top:' + ptop + ';padding-bottom:' + pbot + ';box-sizing:border-box;'; |
| 116 |
app.appendChild(card); |
| 117 |
|
| 118 |
/* --- Unit toggle --- */ |
| 119 |
if (showToggle) { |
| 120 |
var toggleRow = document.createElement('div'); |
| 121 |
toggleRow.className = 'bkbg-bmi-unit-toggle'; |
| 122 |
['metric', 'imperial'].forEach(function(s) { |
| 123 |
var btn = document.createElement('button'); |
| 124 |
btn.className = 'bkbg-bmi-unit-btn' + (system === s ? ' active' : ''); |
| 125 |
btn.textContent = s === 'metric' ? 'Metric (kg / cm)' : 'Imperial (lbs / ft)'; |
| 126 |
btn.style.cssText = 'border-radius:' + btnR + ';background:' + (system === s ? accent : '#f3f4f6') + ';color:' + (system === s ? '#fff' : '#374151') + ';'; |
| 127 |
btn.dataset.sys = s; |
| 128 |
toggleRow.appendChild(btn); |
| 129 |
}); |
| 130 |
card.appendChild(toggleRow); |
| 131 |
} |
| 132 |
|
| 133 |
/* --- Input fields --- */ |
| 134 |
var inputWrap = document.createElement('div'); |
| 135 |
inputWrap.className = 'bkbg-bmi-inputs ' + system; |
| 136 |
card.appendChild(inputWrap); |
| 137 |
|
| 138 |
function makeField(label, id, value, min, max) { |
| 139 |
var wrap = document.createElement('div'); |
| 140 |
var lbl = document.createElement('label'); |
| 141 |
lbl.htmlFor = id; |
| 142 |
lbl.textContent = label; |
| 143 |
lbl.className = 'bkbg-bmi-label'; |
| 144 |
lbl.style.color = labelColor; |
| 145 |
var inp = document.createElement('input'); |
| 146 |
inp.id = id; |
| 147 |
inp.type = 'number'; |
| 148 |
inp.className = 'bkbg-bmi-input'; |
| 149 |
inp.value = value; |
| 150 |
inp.min = min; |
| 151 |
inp.max = max; |
| 152 |
inp.style.accentColor = accent; |
| 153 |
wrap.appendChild(lbl); |
| 154 |
wrap.appendChild(inp); |
| 155 |
inputWrap.appendChild(wrap); |
| 156 |
return inp; |
| 157 |
} |
| 158 |
|
| 159 |
var inpWKg, inpHCm, inpWLbs, inpHFt, inpHIn; |
| 160 |
function buildInputs() { |
| 161 |
inputWrap.innerHTML = ''; |
| 162 |
inputWrap.className = 'bkbg-bmi-inputs ' + system; |
| 163 |
if (system === 'metric') { |
| 164 |
inpWKg = makeField('Weight (kg)', 'bmi-wkg', weightKg, opts.weightMinKg || 20, opts.weightMaxKg || 200); |
| 165 |
inpHCm = makeField('Height (cm)', 'bmi-hcm', heightCm, opts.heightMinCm || 100, opts.heightMaxCm || 220); |
| 166 |
} else { |
| 167 |
inpWLbs = makeField('Weight (lbs)', 'bmi-wlbs', weightLbs, opts.weightMinLbs || 44, opts.weightMaxLbs || 440); |
| 168 |
inpHFt = makeField('Height (ft)', 'bmi-hft', heightFt, 3, 8); |
| 169 |
inpHIn = makeField('Height (in)', 'bmi-hin', heightIn, 0, 11); |
| 170 |
} |
| 171 |
} |
| 172 |
buildInputs(); |
| 173 |
|
| 174 |
/* --- Result area --- */ |
| 175 |
var resultBox = document.createElement('div'); |
| 176 |
resultBox.className = 'bkbg-bmi-result'; |
| 177 |
resultBox.style.cssText = 'background:' + resultBg + ';border:1.5px solid ' + resultBdr + ';border-radius:' + radius + ';'; |
| 178 |
card.appendChild(resultBox); |
| 179 |
|
| 180 |
var bmiNumEl = document.createElement('div'); bmiNumEl.className = 'bkbg-bmi-number'; |
| 181 |
var bmiCatEl = document.createElement('div'); bmiCatEl.className = 'bkbg-bmi-cat-label'; |
| 182 |
var gaugeWrap, gaugeTrack, gaugeNeedle, gaugeLabels; |
| 183 |
var legendEl; |
| 184 |
var idealEl; |
| 185 |
var interpEl; |
| 186 |
|
| 187 |
resultBox.appendChild(bmiNumEl); |
| 188 |
resultBox.appendChild(bmiCatEl); |
| 189 |
|
| 190 |
if (opts.showGauge !== false) { |
| 191 |
gaugeWrap = document.createElement('div'); gaugeWrap.className = 'bkbg-bmi-gauge-wrap'; |
| 192 |
gaugeTrack = document.createElement('div'); gaugeTrack.className = 'bkbg-bmi-gauge-track'; |
| 193 |
gaugeTrack.style.background = 'linear-gradient(to right,' + catColors.underweight + ' 0%,' + catColors.normal + ' 30%,' + catColors.overweight + ' 60%,' + catColors.obese + ' 100%)'; |
| 194 |
gaugeNeedle = document.createElement('div'); gaugeNeedle.className = 'bkbg-bmi-gauge-needle'; |
| 195 |
gaugeLabels = document.createElement('div'); gaugeLabels.className = 'bkbg-bmi-gauge-labels'; |
| 196 |
gaugeLabels.innerHTML = '<span>10</span><span>18.5</span><span>25</span><span>30</span><span>40+</span>'; |
| 197 |
gaugeTrack.appendChild(gaugeNeedle); |
| 198 |
gaugeWrap.appendChild(gaugeTrack); |
| 199 |
gaugeWrap.appendChild(gaugeLabels); |
| 200 |
resultBox.appendChild(gaugeWrap); |
| 201 |
} |
| 202 |
|
| 203 |
if (opts.showCategories !== false) { |
| 204 |
legendEl = document.createElement('div'); |
| 205 |
legendEl.className = 'bkbg-bmi-legend'; |
| 206 |
Object.keys(catColors).forEach(function(k) { |
| 207 |
var item = document.createElement('div'); item.className = 'bkbg-bmi-legend-item'; |
| 208 |
var dot = document.createElement('span'); dot.className = 'bkbg-bmi-legend-dot'; dot.style.background = catColors[k]; |
| 209 |
var txt = document.createElement('span'); txt.textContent = catLabels[k]; |
| 210 |
item.appendChild(dot); item.appendChild(txt); |
| 211 |
legendEl.appendChild(item); |
| 212 |
}); |
| 213 |
resultBox.appendChild(legendEl); |
| 214 |
} |
| 215 |
|
| 216 |
if (opts.showIdealWeight !== false) { |
| 217 |
idealEl = document.createElement('p'); |
| 218 |
idealEl.className = 'bkbg-bmi-ideal'; |
| 219 |
resultBox.appendChild(idealEl); |
| 220 |
} |
| 221 |
|
| 222 |
if (opts.showInterpretation !== false) { |
| 223 |
interpEl = document.createElement('p'); |
| 224 |
interpEl.className = 'bkbg-bmi-interp'; |
| 225 |
resultBox.appendChild(interpEl); |
| 226 |
} |
| 227 |
|
| 228 |
if (opts.showDisclaimer !== false && opts.disclaimer) { |
| 229 |
var disc = document.createElement('p'); |
| 230 |
disc.className = 'bkbg-bmi-disclaimer'; |
| 231 |
disc.textContent = opts.disclaimer; |
| 232 |
card.appendChild(disc); |
| 233 |
} |
| 234 |
|
| 235 |
/* --- Update display --- */ |
| 236 |
function update() { |
| 237 |
if (system === 'metric') { |
| 238 |
weightKg = parseFloat(inpWKg.value) || 70; |
| 239 |
heightCm = parseFloat(inpHCm.value) || 170; |
| 240 |
} else { |
| 241 |
weightLbs = parseFloat(inpWLbs.value) || 154; |
| 242 |
heightFt = parseInt(inpHFt.value) || 5; |
| 243 |
heightIn = parseInt(inpHIn.value) || 0; |
| 244 |
} |
| 245 |
|
| 246 |
var bmi = calcBMI(system, weightKg, heightCm, weightLbs, heightFt, heightIn); |
| 247 |
var cat = bmiCategory(bmi); |
| 248 |
var col = catColors[cat]; |
| 249 |
var pct = bmiPct(bmi); |
| 250 |
|
| 251 |
bmiNumEl.textContent = bmi.toFixed(1); |
| 252 |
bmiNumEl.style.color = col; |
| 253 |
bmiCatEl.textContent = catLabels[cat]; |
| 254 |
bmiCatEl.style.color = col; |
| 255 |
|
| 256 |
if (gaugeNeedle) { |
| 257 |
gaugeNeedle.style.left = pct + '%'; |
| 258 |
gaugeNeedle.style.color = col; |
| 259 |
gaugeNeedle.style.borderColor = col; |
| 260 |
} |
| 261 |
|
| 262 |
if (idealEl) { |
| 263 |
var ir = idealRange(system, heightCm, heightFt, heightIn); |
| 264 |
idealEl.innerHTML = 'Ideal weight range: <strong>' + ir.min + '–' + ir.max + ' ' + ir.unit + '</strong>'; |
| 265 |
} |
| 266 |
|
| 267 |
if (interpEl) { |
| 268 |
interpEl.textContent = CAT_INTERP[cat] || ''; |
| 269 |
interpEl.style.color = col; |
| 270 |
} |
| 271 |
} |
| 272 |
|
| 273 |
/* --- Event bindings --- */ |
| 274 |
function bindInputs() { |
| 275 |
var allInputs = inputWrap.querySelectorAll('input'); |
| 276 |
allInputs.forEach(function(inp) { |
| 277 |
inp.addEventListener('input', update); |
| 278 |
}); |
| 279 |
} |
| 280 |
bindInputs(); |
| 281 |
update(); |
| 282 |
|
| 283 |
/* Unit toggle events */ |
| 284 |
if (showToggle) { |
| 285 |
card.querySelector('.bkbg-bmi-unit-toggle').addEventListener('click', function(e) { |
| 286 |
var btn = e.target.closest('.bkbg-bmi-unit-btn'); |
| 287 |
if (!btn) return; |
| 288 |
system = btn.dataset.sys; |
| 289 |
card.querySelectorAll('.bkbg-bmi-unit-btn').forEach(function(b) { |
| 290 |
var active = b.dataset.sys === system; |
| 291 |
b.classList.toggle('active', active); |
| 292 |
b.style.background = active ? accent : '#f3f4f6'; |
| 293 |
b.style.color = active ? '#fff' : '#374151'; |
| 294 |
}); |
| 295 |
buildInputs(); |
| 296 |
bindInputs(); |
| 297 |
update(); |
| 298 |
}); |
| 299 |
} |
| 300 |
|
| 301 |
typoCssVarsForEl(opts.typoTitle, '--bkbg-bmi-title-', app); |
| 302 |
typoCssVarsForEl(opts.typoBmi, '--bkbg-bmi-number-', app); |
| 303 |
} |
| 304 |
|
| 305 |
document.querySelectorAll('.bkbg-bmi-app').forEach(buildApp); |
| 306 |
})(); |
| 307 |
|