| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
var BTNS = [ |
| 5 |
'MC','MR','MS','M+','M−', |
| 6 |
'2nd','π','e','C','⌫', |
| 7 |
'x²','xʸ','√x','ʸ√x','1/x', |
| 8 |
'sin','cos','tan','log','ln', |
| 9 |
'sin⁻¹','cos⁻¹','tan⁻¹','10ˣ','eˣ', |
| 10 |
'(',')', '%','÷','', |
| 11 |
'7','8','9','×','', |
| 12 |
'4','5','6','−','', |
| 13 |
'1','2','3','+','', |
| 14 |
'±','0','.','=','' |
| 15 |
]; |
| 16 |
|
| 17 |
function initApp(app) { |
| 18 |
var opts; |
| 19 |
try { opts = JSON.parse(app.getAttribute('data-opts') || '{}'); } catch(e) { return; } |
| 20 |
|
| 21 |
var _typoKeys = { |
| 22 |
family:'font-family', weight:'font-weight', style:'font-style', |
| 23 |
decoration:'text-decoration', transform:'text-transform', |
| 24 |
sizeDesktop:'font-size-d', sizeTablet:'font-size-t', sizeMobile:'font-size-m', |
| 25 |
lineHeightDesktop:'line-height-d', lineHeightTablet:'line-height-t', lineHeightMobile:'line-height-m', |
| 26 |
letterSpacingDesktop:'letter-spacing-d', letterSpacingTablet:'letter-spacing-t', letterSpacingMobile:'letter-spacing-m', |
| 27 |
wordSpacingDesktop:'word-spacing-d', wordSpacingTablet:'word-spacing-t', wordSpacingMobile:'word-spacing-m' |
| 28 |
}; |
| 29 |
function typoCssVarsForEl(el, obj, prefix) { |
| 30 |
if (!obj || typeof obj !== 'object') return; |
| 31 |
Object.keys(_typoKeys).forEach(function(k) { |
| 32 |
var v = obj[k]; |
| 33 |
if (v === undefined || v === '' || v === null) return; |
| 34 |
if (k === 'sizeDesktop' || k === 'sizeTablet' || k === 'sizeMobile') v = v + (obj.sizeUnit || 'px'); |
| 35 |
else if (k === 'lineHeightDesktop' || k === 'lineHeightTablet' || k === 'lineHeightMobile') v = v + (obj.lineHeightUnit || ''); |
| 36 |
else if (k === 'letterSpacingDesktop' || k === 'letterSpacingTablet' || k === 'letterSpacingMobile') v = v + (obj.letterSpacingUnit || 'px'); |
| 37 |
else if (k === 'wordSpacingDesktop' || k === 'wordSpacingTablet' || k === 'wordSpacingMobile') v = v + (obj.wordSpacingUnit || 'px'); |
| 38 |
el.style.setProperty(prefix + _typoKeys[k], String(v)); |
| 39 |
}); |
| 40 |
} |
| 41 |
|
| 42 |
var accent = opts.accentColor || '#6c3fb5'; |
| 43 |
var cardBg = opts.cardBg || '#1a1a2e'; |
| 44 |
var dispBg = opts.displayBg || '#0d0d1a'; |
| 45 |
var dispClr = opts.displayColor || '#ffffff'; |
| 46 |
var cardR = (opts.cardRadius || 20) + 'px'; |
| 47 |
var btnR = (opts.btnRadius || 10) + 'px'; |
| 48 |
var btnSz = (opts.btnSize || 15) + 'px'; |
| 49 |
var dispSz = (opts.displaySize || 36) + 'px'; |
| 50 |
var maxW = (opts.maxWidth || 420) + 'px'; |
| 51 |
var ptop = (opts.paddingTop || 60) + 'px'; |
| 52 |
var pbot = (opts.paddingBottom|| 60) + 'px'; |
| 53 |
var histMax = opts.historyMax || 10; |
| 54 |
var prec = opts.precision || 10; |
| 55 |
|
| 56 |
var display = '0'; |
| 57 |
var expr = ''; |
| 58 |
var memory = 0; |
| 59 |
var angleMode = opts.angleMode || 'deg'; |
| 60 |
var history = []; |
| 61 |
|
| 62 |
app.style.paddingTop = ptop; |
| 63 |
app.style.paddingBottom = pbot; |
| 64 |
if (opts.sectionBg) app.style.background = opts.sectionBg; |
| 65 |
typoCssVarsForEl(app, opts.titleTypo, '--bksc-tt-'); |
| 66 |
typoCssVarsForEl(app, opts.subtitleTypo, '--bksc-st-'); |
| 67 |
|
| 68 |
/* Header */ |
| 69 |
if (opts.showTitle && opts.title) { |
| 70 |
var hdr = document.createElement('div'); |
| 71 |
hdr.className = 'bkbg-sc-header'; |
| 72 |
var ttl = document.createElement('div'); |
| 73 |
ttl.className = 'bkbg-sc-title'; |
| 74 |
ttl.textContent = opts.title; |
| 75 |
if (opts.titleColor) ttl.style.color = opts.titleColor; |
| 76 |
hdr.appendChild(ttl); |
| 77 |
if (opts.showSubtitle && opts.subtitle) { |
| 78 |
var sub = document.createElement('div'); |
| 79 |
sub.className = 'bkbg-sc-subtitle'; |
| 80 |
sub.textContent = opts.subtitle; |
| 81 |
if (opts.subtitleColor) sub.style.color = opts.subtitleColor; |
| 82 |
hdr.appendChild(sub); |
| 83 |
} |
| 84 |
app.appendChild(hdr); |
| 85 |
} |
| 86 |
|
| 87 |
/* Calc body */ |
| 88 |
var calc = document.createElement('div'); |
| 89 |
calc.className = 'bkbg-sc-calc'; |
| 90 |
Object.assign(calc.style, {background:cardBg, borderRadius:cardR, maxWidth:maxW}); |
| 91 |
app.appendChild(calc); |
| 92 |
|
| 93 |
/* Top bar */ |
| 94 |
var topbar = document.createElement('div'); |
| 95 |
topbar.className = 'bkbg-sc-topbar'; |
| 96 |
calc.appendChild(topbar); |
| 97 |
|
| 98 |
var angleToggle = document.createElement('div'); |
| 99 |
angleToggle.className = 'bkbg-sc-angle-toggle'; |
| 100 |
['deg','rad'].forEach(function(m) { |
| 101 |
var b = document.createElement('button'); |
| 102 |
b.className = 'bkbg-sc-angle-btn'; |
| 103 |
b.textContent = m.toUpperCase(); |
| 104 |
b.dataset.mode = m; |
| 105 |
b.addEventListener('click', function() { |
| 106 |
angleMode = m; |
| 107 |
angleToggle.querySelectorAll('.bkbg-sc-angle-btn').forEach(function(x){ |
| 108 |
var active = x.dataset.mode === angleMode; |
| 109 |
x.style.background = active ? accent : 'rgba(255,255,255,.1)'; |
| 110 |
x.style.color = active ? '#fff' : 'rgba(255,255,255,.5)'; |
| 111 |
}); |
| 112 |
}); |
| 113 |
b.style.background = m === angleMode ? accent : 'rgba(255,255,255,.1)'; |
| 114 |
b.style.color = m === angleMode ? '#fff' : 'rgba(255,255,255,.5)'; |
| 115 |
angleToggle.appendChild(b); |
| 116 |
}); |
| 117 |
topbar.appendChild(angleToggle); |
| 118 |
|
| 119 |
var memEl = document.createElement('div'); |
| 120 |
memEl.className = 'bkbg-sc-mem-indicator'; |
| 121 |
if (opts.memColor) memEl.style.color = opts.memColor; |
| 122 |
if (opts.showMemory !== false) topbar.appendChild(memEl); |
| 123 |
|
| 124 |
/* Expression */ |
| 125 |
var exprEl = document.createElement('div'); |
| 126 |
exprEl.className = 'bkbg-sc-expr'; |
| 127 |
calc.appendChild(exprEl); |
| 128 |
|
| 129 |
/* Display */ |
| 130 |
var dispEl = document.createElement('div'); |
| 131 |
dispEl.className = 'bkbg-sc-display'; |
| 132 |
Object.assign(dispEl.style, {background:dispBg, color:dispClr, fontSize:dispSz}); |
| 133 |
dispEl.textContent = '0'; |
| 134 |
calc.appendChild(dispEl); |
| 135 |
|
| 136 |
/* Button grid */ |
| 137 |
var grid = document.createElement('div'); |
| 138 |
grid.className = 'bkbg-sc-grid'; |
| 139 |
calc.appendChild(grid); |
| 140 |
|
| 141 |
/* History */ |
| 142 |
var histEl, histList; |
| 143 |
if (opts.showHistory !== false) { |
| 144 |
histEl = document.createElement('div'); |
| 145 |
histEl.className = 'bkbg-sc-history'; |
| 146 |
histEl.style.display = 'none'; |
| 147 |
if (opts.historyBg) histEl.style.background = opts.historyBg; |
| 148 |
var hLabel = document.createElement('div'); |
| 149 |
hLabel.className = 'bkbg-sc-history-label'; |
| 150 |
hLabel.textContent = 'History'; |
| 151 |
histEl.appendChild(hLabel); |
| 152 |
histList = document.createElement('div'); |
| 153 |
histEl.appendChild(histList); |
| 154 |
calc.appendChild(histEl); |
| 155 |
} |
| 156 |
|
| 157 |
/* State update functions */ |
| 158 |
function updateDisplay() { |
| 159 |
dispEl.textContent = display; |
| 160 |
exprEl.textContent = expr || '\u00a0'; |
| 161 |
if (opts.showMemory !== false) memEl.textContent = memory !== 0 ? 'M: ' + memory : ''; |
| 162 |
} |
| 163 |
|
| 164 |
function addHistory(entry) { |
| 165 |
if (!histEl) return; |
| 166 |
history.unshift({expr:entry}); |
| 167 |
if (history.length > histMax) history.pop(); |
| 168 |
histList.innerHTML = ''; |
| 169 |
history.forEach(function(h) { |
| 170 |
var row = document.createElement('div'); |
| 171 |
row.className = 'bkbg-sc-history-entry'; |
| 172 |
if (opts.historyColor) row.style.color = opts.historyColor; |
| 173 |
row.textContent = h.expr; |
| 174 |
row.addEventListener('click', function(){ |
| 175 |
var parts = h.expr.split('='); |
| 176 |
if (parts.length > 1) { display = parts[parts.length-1]; updateDisplay(); } |
| 177 |
}); |
| 178 |
histList.appendChild(row); |
| 179 |
}); |
| 180 |
histEl.style.display = 'block'; |
| 181 |
} |
| 182 |
|
| 183 |
function evaluateExpr(e) { |
| 184 |
try { |
| 185 |
var toRad = function(v){ return angleMode==='deg' ? v*Math.PI/180 : v; }; |
| 186 |
var safe = e |
| 187 |
.replace(/×/g,'*').replace(/÷/g,'/') |
| 188 |
.replace(/−/g,'-').replace(/π/g,'('+Math.PI+')') |
| 189 |
.replace(/\be\b/g,'('+Math.E+')'); |
| 190 |
var r = Function('"use strict"; return (' + safe + ')')(); |
| 191 |
if (!isFinite(r)) return 'Error'; |
| 192 |
return (parseFloat(r.toPrecision(prec))).toString(); |
| 193 |
} catch(err) { return 'Error'; } |
| 194 |
} |
| 195 |
|
| 196 |
function handleBtn(lbl) { |
| 197 |
if (lbl === '') return; |
| 198 |
var num = parseFloat(display) || 0; |
| 199 |
var toRad = function(v){ return angleMode==='deg' ? v*Math.PI/180 : v; }; |
| 200 |
var fromRad = function(v){ return angleMode==='deg' ? v*180/Math.PI : v; }; |
| 201 |
var p = function(v){ return (parseFloat(v.toPrecision(prec))+''); }; |
| 202 |
|
| 203 |
if (lbl === 'C') { display='0'; expr=''; updateDisplay(); return; } |
| 204 |
if (lbl === '⌫') { display=display.length>1?display.slice(0,-1):'0'; updateDisplay(); return; } |
| 205 |
if (lbl === '±') { display=display.startsWith('-')?display.slice(1):('-'+display); updateDisplay(); return; } |
| 206 |
if (lbl === 'π') { display=p(Math.PI); updateDisplay(); return; } |
| 207 |
if (lbl === 'e') { display=p(Math.E); updateDisplay(); return; } |
| 208 |
if (lbl === '=') { |
| 209 |
var full = expr + display; |
| 210 |
var res = evaluateExpr(full); |
| 211 |
if (opts.showHistory !== false) addHistory(full + ' = ' + res); |
| 212 |
expr=''; display=res; updateDisplay(); return; |
| 213 |
} |
| 214 |
if (lbl === 'MC') { memory=0; updateDisplay(); return; } |
| 215 |
if (lbl === 'MR') { display=memory.toString(); updateDisplay(); return; } |
| 216 |
if (lbl === 'MS') { memory=num; updateDisplay(); return; } |
| 217 |
if (lbl === 'M+') { memory+=num; updateDisplay(); return; } |
| 218 |
if (lbl === 'M−') { memory-=num; updateDisplay(); return; } |
| 219 |
if (lbl === '2nd') return; // shift handled visually |
| 220 |
|
| 221 |
var UNARY = { |
| 222 |
'x²': function(v){ return p(v*v); }, |
| 223 |
'√x': function(v){ return p(Math.sqrt(v)); }, |
| 224 |
'1/x': function(v){ return p(1/v); }, |
| 225 |
'sin': function(v){ return p(Math.sin(toRad(v))); }, |
| 226 |
'cos': function(v){ return p(Math.cos(toRad(v))); }, |
| 227 |
'tan': function(v){ return p(Math.tan(toRad(v))); }, |
| 228 |
'sin⁻¹': function(v){ return p(fromRad(Math.asin(v))); }, |
| 229 |
'cos⁻¹': function(v){ return p(fromRad(Math.acos(v))); }, |
| 230 |
'tan⁻¹': function(v){ return p(fromRad(Math.atan(v))); }, |
| 231 |
'log': function(v){ return p(Math.log10(v)); }, |
| 232 |
'ln': function(v){ return p(Math.log(v)); }, |
| 233 |
'10ˣ': function(v){ return p(Math.pow(10,v)); }, |
| 234 |
'eˣ': function(v){ return p(Math.exp(v)); }, |
| 235 |
'%': function(v){ return p(v/100); } |
| 236 |
}; |
| 237 |
if (UNARY[lbl]) { display=UNARY[lbl](num); updateDisplay(); return; } |
| 238 |
|
| 239 |
var OPS = ['+','−','×','÷','xʸ','ʸ√x']; |
| 240 |
if (OPS.includes(lbl)) { expr=expr+display+lbl; display='0'; updateDisplay(); return; } |
| 241 |
if (lbl==='('||lbl===')') { expr+=lbl; updateDisplay(); return; } |
| 242 |
|
| 243 |
// Digits and decimal |
| 244 |
if (display==='0'||display==='Error') { display=lbl; } |
| 245 |
else { if (lbl==='.'&&display.includes('.')) return; display+=lbl; } |
| 246 |
updateDisplay(); |
| 247 |
} |
| 248 |
|
| 249 |
/* Create buttons */ |
| 250 |
function getBtnClass(lbl) { |
| 251 |
if (lbl==='=') return 'bkbg-sc-btn bkbg-sc-btn-eq'; |
| 252 |
if (lbl==='C'||lbl==='⌫') return 'bkbg-sc-btn bkbg-sc-btn-clear'; |
| 253 |
if (['MC','MR','MS','M+','M−'].includes(lbl)) return 'bkbg-sc-btn bkbg-sc-btn-mem'; |
| 254 |
if (['+','−','×','÷','%','xʸ','ʸ√x','(',')'].includes(lbl)) return 'bkbg-sc-btn bkbg-sc-btn-op'; |
| 255 |
if (['0','1','2','3','4','5','6','7','8','9','.','±'].includes(lbl)) return 'bkbg-sc-btn bkbg-sc-btn-num'; |
| 256 |
return 'bkbg-sc-btn bkbg-sc-btn-fn'; |
| 257 |
} |
| 258 |
function getBtnStyle(lbl) { |
| 259 |
var s = {fontSize:btnSz, borderRadius:btnR}; |
| 260 |
if (lbl==='=') { s.background=opts.btnEqBg||accent; s.color=opts.btnEqColor||'#fff'; } |
| 261 |
else if (lbl==='C'||lbl==='⌫') { s.background=opts.btnClearBg||'#7f1d1d'; s.color=opts.btnClearColor||'#fecaca'; } |
| 262 |
else if (['MC','MR','MS','M+','M−'].includes(lbl)){ s.background=opts.btnFnBg||'#1e2a4a'; s.color=opts.memColor||'#86efac'; } |
| 263 |
else if (['+','−','×','÷','%','xʸ','ʸ√x','(',')'].includes(lbl)){ s.background=opts.btnOpBg||'#3d2d6a'; s.color=opts.btnOpColor||'#c4b5fd'; } |
| 264 |
else if (['0','1','2','3','4','5','6','7','8','9','.','±'].includes(lbl)){ s.background=opts.btnNumBg||'#2d2d4a'; s.color=opts.btnNumColor||'#fff'; } |
| 265 |
else { s.background=opts.btnFnBg||'#1e2a4a'; s.color=opts.btnFnColor||'#93c5fd'; } |
| 266 |
return s; |
| 267 |
} |
| 268 |
|
| 269 |
BTNS.forEach(function(lbl) { |
| 270 |
if (lbl === '') { grid.appendChild(document.createElement('div')); return; } |
| 271 |
var btn = document.createElement('button'); |
| 272 |
btn.className = getBtnClass(lbl); |
| 273 |
btn.textContent = lbl; |
| 274 |
var st = getBtnStyle(lbl); |
| 275 |
Object.assign(btn.style, st); |
| 276 |
btn.addEventListener('click', function(){ handleBtn(lbl); }); |
| 277 |
grid.appendChild(btn); |
| 278 |
}); |
| 279 |
|
| 280 |
/* Keyboard support */ |
| 281 |
document.addEventListener('keydown', function(ev) { |
| 282 |
if (!app.closest('body')) return; |
| 283 |
var map = { |
| 284 |
'Enter':'=','Backspace':'⌫','Escape':'C', |
| 285 |
'+':'+','-':'−','*':'×','/':'÷','%':'%', |
| 286 |
'(':'(', ')':')' |
| 287 |
}; |
| 288 |
var key = ev.key; |
| 289 |
if (map[key]) { ev.preventDefault(); handleBtn(map[key]); return; } |
| 290 |
if (/^[0-9\.]$/.test(key)) { ev.preventDefault(); handleBtn(key); } |
| 291 |
}); |
| 292 |
|
| 293 |
updateDisplay(); |
| 294 |
} |
| 295 |
|
| 296 |
document.querySelectorAll('.bkbg-sc-app').forEach(initApp); |
| 297 |
})(); |
| 298 |
|