// Calculator State
const State = {
good : 0,
bad : 1,
error: 2,
};
// Model: Calculator
class Calculator {
// init
constructor(){
this._memory = '';
this._lastInput = undefined;
this._debugMode = true;
}
// enter `key` into memory
input(key){
switch(key){
case "×": key = '*'; break;
case "+": key = '+'; break;
case "−": key = '-'; break;
case "÷": key = '/'; break;
}
switch(key){
// clear
case 'C':
this._memory = '';
break;
// execute
case '=':
this._memory = this.isCurrentResultValid
? `${this._currentResult}`
: '💥 Error';
break;
// in the middle of entering
default:
// append `key` to memory
this._memory += key;
// ⭐️ if in error state, clear memory and start over.
if (this.state == State.error) {
this._memory = key;
}
}
this._lastInput = key;
this._calcCurrentResult();
if (this.debugMode) this.log();
}
// this.memory
get memory(){
return this._memory;
}
// this.debugMode
get debugMode(){
return this._debugMode;
}
// this.debugMode = true | false
set debugMode(bool){
this._debugMode = bool;
}
// this.isCurrentResultValid
get isCurrentResultValid(){
return Number.isFinite(this._currentResult);
}
// this.currentResult
get currentResult(){
return this._currentResult;
}
// this.log()
log(){
console.log(`memory: "${this.memory}", state: ${this.state}, result: ${this.currentResult}`);
}
// calculate current result based on `memory`
_calcCurrentResult(){
// empty memory
if (this._memory == '') {
this._currentResult = 0;
return;
}
// memory not empty, try to calculate current result
try {
// if success, return the result
const result = eval(this._memory);
this._currentResult = result;
} catch(e) {
// else set current result `undefined`
if (this.debugMode) console.log(`${e.name}: ${e.message}`);
this._currentResult = undefined;
}
}
// this.state
get state(){
// result is OK
if (this.isCurrentResultValid) return State.good;
// hit '=' and result is Not OK
if (this._lastInput == '=') return State.error;
// still in the middle of entering something
return State.bad;
}
// this.inGoodState
get inGoodState() {
return this.state === State.good;
}
// this.inBadState
get inBadState() {
return this.state === State.bad;
}
// this.inErrorState
get inErrorState() {
return this.state === State.error;
}
}
const { log } = console;
const buttonLabels = [
'C', '1', '2', '3', '+',
'4', '5', '6', '−',
'7', '8', '9', '×',
'.', '0', '=', '÷',
];
// views
const display = $('.display-box');
const calcUI = $('.calculator');
// for each button label, create new button
buttonLabels.forEach(lbl => {
// append <button> to calculator
let btn = tag('button', {
textContent: lbl,
parentNode: calcUI
});
// highlight "command" buttons
if (lbl === 'C' || lbl === '=') {
btn.classList.add('command');
}
});
// model
const calculator = new Calculator();
// calculator.debugMode = false;
// event listener ---------------------
calcUI.addEventListener('click', e => {
// make sure target is <button>
let t = e.target;
if (t.nodeName !== 'BUTTON') return;
// get `key` from UI
let key = t.textContent;
// update model
calculator.input(key);
// update UI
display.value = calculator.memory;
display.classList.toggle('bad', calculator.inBadState);
display.classList.toggle('error', calculator.inErrorState);
});
// helpers ------------------------------
// ⭐️ $(): select first element
function $(selector, parent = document){
return parent.querySelector(selector);
}
// ⭐️ tag()
function tag(name, {
textContent,
attributes = {},
parentNode,
}={}){
// create element
let elem = document.createElement(name);
// set text content if necessary
if (textContent) {
elem.textContent = textContent;
}
// set attributes
for (const [key, value] of Object.entries(attributes)) {
elem.setAttribute(key, value);
}
// append to parent node if present
if(parentNode){
parentNode.appendChild(elem);
}
// return element
return elem;
}