Created
August 3, 2024 13:34
-
-
Save rmkane/835835b315f47b10e4f127bd309cd046 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<meta http-equiv="content-type" content="text/html; charset=UTF-8" /> | |
<title>Calculator</title> | |
<meta name="viewport" content="width=device-width, initial-scale=1" /> | |
<link | |
rel="stylesheet" | |
type="text/css" | |
href="https://cdn.jsdelivr.net/npm/[email protected]/reset.min.css" | |
/> | |
<!-- Calculator Base Style --> | |
<style type="text/css"> | |
:root { | |
--calc-bg: #ddd; | |
--calc-text: #111; | |
--calc-border: #ccc; | |
--calc-btn-bg: #eee; | |
--calc-btn-hover-bg: #fff; | |
--calc-btn-hover-text: var(--calc-text); | |
--calc-disp-bg: #fff; | |
} | |
.calculator { | |
display: flex; | |
flex-direction: column; | |
padding: 0.5rem; | |
background: var(--calc-bg); | |
color: var(--calc-text); | |
gap: 1rem; | |
border: thin solid var(--calc-border); | |
box-shadow: 0 0 0.5rem 0.25rem rgba(0, 0, 0, 0.5); | |
border-radius: 0.25rem; | |
} | |
.calculator-display { | |
display: flex; | |
flex-direction: column; | |
gap: 0.5rem; | |
border: thin solid var(--calc-border); | |
padding: 0.5rem; | |
background: var(--calc-disp-bg); | |
font-family: monospace; | |
border-radius: 0.25rem; | |
} | |
.calculator-status { | |
display: flex; | |
flex-direction: row; | |
justify-content: space-between; | |
min-height: 1rem; | |
user-select: none; | |
} | |
.calculator-memory { | |
display: flex; | |
font-size: 0.8rem; | |
} | |
.calculator-buffer { | |
display: flex; | |
font-size: 0.8rem; | |
} | |
.calculator-value { | |
min-height: 2rem; | |
font-size: 2rem; | |
text-align: right; | |
} | |
.calculator-button-grid { | |
display: grid; | |
grid-template-columns: repeat(5, auto); | |
gap: 0.5rem; | |
} | |
.calculator-button { | |
border: thin solid var(--calc-border); | |
background: var(--calc-btn-bg); | |
color: var(--calc-text); | |
min-width: 2rem; | |
min-height: 2rem; | |
font-family: Arial; | |
border-radius: 0.25rem; | |
} | |
.calculator-button:hover { | |
background: var(--calc-btn-hover-bg); | |
color: var(--calc-btn-hover-text); | |
cursor: pointer; | |
} | |
.calculator-button[data-name="OPERATOR_EQUALS"] { | |
grid-row: span 2; | |
} | |
.calculator-button[data-name="DIGIT_0"] { | |
grid-column: span 2; | |
} | |
</style> | |
<!-- Calculator Themes --> | |
<style> | |
.calculator[data-theme="light"] { | |
--calc-bg: #ddd; | |
--calc-text: #111; | |
--calc-border: #ccc; | |
--calc-btn-bg: #eee; | |
--calc-btn-hover-bg: #fff; | |
--calc-btn-hover-text: var(--calc-text); | |
--calc-disp-bg: #fff; | |
} | |
.calculator[data-theme="dark"] { | |
--calc-bg: #111; | |
--calc-text: #eee; | |
--calc-border: #333; | |
--calc-btn-bg: #222; | |
--calc-btn-hover-bg: #333; | |
--calc-disp-bg: #000; | |
} | |
.calculator[data-theme="contrast"] { | |
--calc-bg: #000; | |
--calc-text: #fff; | |
--calc-border: #fff; | |
--calc-btn-bg: #000; | |
--calc-btn-hover-bg: #fff; | |
--calc-btn-hover-text: #000; | |
--calc-disp-bg: #000; | |
} | |
.calculator[data-theme="red"] { | |
--calc-bg: #211; | |
--calc-text: #fee; | |
--calc-border: #433; | |
--calc-btn-bg: #322; | |
--calc-btn-hover-bg: #433; | |
--calc-disp-bg: #100; | |
} | |
.calculator[data-theme="green"] { | |
--calc-bg: #121; | |
--calc-text: #efe; | |
--calc-border: #343; | |
--calc-btn-bg: #232; | |
--calc-btn-hover-bg: #343; | |
--calc-disp-bg: #010; | |
} | |
.calculator[data-theme="blue"] { | |
--calc-bg: #112; | |
--calc-text: #eef; | |
--calc-border: #334; | |
--calc-btn-bg: #223; | |
--calc-btn-hover-bg: #334; | |
--calc-disp-bg: #001; | |
} | |
.calculator[data-theme="cyan"] { | |
--calc-bg: #122; | |
--calc-text: #eff; | |
--calc-border: #344; | |
--calc-btn-bg: #233; | |
--calc-btn-hover-bg: #344; | |
--calc-disp-bg: #011; | |
} | |
.calculator[data-theme="magenta"] { | |
--calc-bg: #212; | |
--calc-text: #fef; | |
--calc-border: #434; | |
--calc-btn-bg: #323; | |
--calc-btn-hover-bg: #434; | |
--calc-disp-bg: #101; | |
} | |
.calculator[data-theme="yellow"] { | |
--calc-bg: #221; | |
--calc-text: #ffe; | |
--calc-border: #443; | |
--calc-btn-bg: #332; | |
--calc-btn-hover-bg: #443; | |
--calc-disp-bg: #110; | |
} | |
</style> | |
<!-- Demo Style --> | |
<style type="text/css"> | |
*, | |
*::before, | |
*::after { | |
box-sizing: border-box; | |
} | |
html, | |
body { | |
width: 100%; | |
height: 100%; | |
margin: 0; | |
padding: 0; | |
background: #222; | |
} | |
#calculators-container { | |
display: flex; | |
flex-direction: row; | |
flex-wrap: wrap; | |
align-items: flex-start; | |
justify-content: center; | |
gap: 2rem; | |
padding: 2rem; | |
} | |
</style> | |
<!-- Calculator --> | |
<script type="text/javascript"> | |
// Math Functions Section | |
const BinaryFn = { | |
add: (a, b) => a + b, | |
subtract: (a, b) => a - b, | |
multiply: (a, b) => a * b, | |
divide: (a, b) => { | |
if (b === 0) throw new Error("DIVISION_BY_ZERO"); | |
return a / b; | |
}, | |
percentage: (a, b) => (a / 100) * b, | |
}; | |
const UnaryFn = { | |
negate: (n) => -n, | |
sqrt: (n) => { | |
if (n < 0) throw new Error("NEGATIVE_SQRT"); | |
return Math.sqrt(n); | |
}, | |
reciprocal: (n) => { | |
if (n === 0) throw new Error("DIVISION_BY_ZERO"); | |
return 1 / n; | |
}, | |
}; | |
// Constants Section | |
const ACTION_TYPES = { | |
DIGIT: "digit", | |
FUNCTION: "function", | |
MEMORY: "memory", | |
OPERATOR: "operator", | |
}; | |
const OPERATOR_TYPES = { | |
ADDITION: "OPERATOR_ADDITION", | |
DIVISION: "OPERATOR_DIVISION", | |
EQUALS: "OPERATOR_EQUALS", | |
MULTIPLICATION: "OPERATOR_MULTIPLICATION", | |
PERCENTAGE: "OPERATOR_PERCENTAGE", | |
SUBTRACTION: "OPERATOR_SUBTRACTION", | |
}; | |
const FUNCTION_TYPES = { | |
BACKSPACE: "FUNCTION_BACKSPACE", | |
CLEAR: "FUNCTION_CLEAR", | |
CLEAR_ENTRY: "FUNCTION_CLEAR_ENTRY", | |
DECIMAL: "FUNCTION_DECIMAL", | |
NEGATE: "FUNCTION_NEGATE", | |
RECIPROCAL: "FUNCTION_RECIPROCAL", | |
SQUARE_ROOT: "FUNCTION_SQUARE_ROOT", | |
}; | |
const MEMORY_TYPES = { | |
ADD: "MEMORY_ADD", | |
CLEAR: "MEMORY_CLEAR", | |
RECALL: "MEMORY_RECALL", | |
STORE: "MEMORY_STORE", | |
SUBTRACT: "MEMORY_SUBTRACT", | |
}; | |
const DIGIT_TYPES = [ | |
"DIGIT_0", | |
"DIGIT_1", | |
"DIGIT_2", | |
"DIGIT_3", | |
"DIGIT_4", | |
"DIGIT_5", | |
"DIGIT_6", | |
"DIGIT_7", | |
"DIGIT_8", | |
"DIGIT_9", | |
]; | |
const BUTTON_TYPES = { | |
OPERATOR_ADDITION: OPERATOR_TYPES.ADDITION, | |
OPERATOR_DIVISION: OPERATOR_TYPES.DIVISION, | |
OPERATOR_EQUALS: OPERATOR_TYPES.EQUALS, | |
OPERATOR_MULTIPLICATION: OPERATOR_TYPES.MULTIPLICATION, | |
OPERATOR_PERCENTAGE: OPERATOR_TYPES.PERCENTAGE, | |
OPERATOR_SUBTRACTION: OPERATOR_TYPES.SUBTRACTION, | |
FUNCTION_BACKSPACE: FUNCTION_TYPES.BACKSPACE, | |
FUNCTION_CLEAR: FUNCTION_TYPES.CLEAR, | |
FUNCTION_CLEAR_ENTRY: FUNCTION_TYPES.CLEAR_ENTRY, | |
FUNCTION_DECIMAL: FUNCTION_TYPES.DECIMAL, | |
FUNCTION_NEGATE: FUNCTION_TYPES.NEGATE, | |
FUNCTION_RECIPROCAL: FUNCTION_TYPES.RECIPROCAL, | |
FUNCTION_SQUARE_ROOT: FUNCTION_TYPES.SQUARE_ROOT, | |
MEMORY_ADD: MEMORY_TYPES.ADD, | |
MEMORY_CLEAR: MEMORY_TYPES.CLEAR, | |
MEMORY_RECALL: MEMORY_TYPES.RECALL, | |
MEMORY_STORE: MEMORY_TYPES.STORE, | |
MEMORY_SUBTRACT: MEMORY_TYPES.SUBTRACT, | |
DIGIT_0: DIGIT_TYPES[0], | |
DIGIT_1: DIGIT_TYPES[1], | |
DIGIT_2: DIGIT_TYPES[2], | |
DIGIT_3: DIGIT_TYPES[3], | |
DIGIT_4: DIGIT_TYPES[4], | |
DIGIT_5: DIGIT_TYPES[5], | |
DIGIT_6: DIGIT_TYPES[6], | |
DIGIT_7: DIGIT_TYPES[7], | |
DIGIT_8: DIGIT_TYPES[8], | |
DIGIT_9: DIGIT_TYPES[9], | |
}; | |
const Buttons = [ | |
{ name: BUTTON_TYPES.DIGIT_0, label: "0", type: ACTION_TYPES.DIGIT }, | |
{ name: BUTTON_TYPES.DIGIT_1, label: "1", type: ACTION_TYPES.DIGIT }, | |
{ name: BUTTON_TYPES.DIGIT_2, label: "2", type: ACTION_TYPES.DIGIT }, | |
{ name: BUTTON_TYPES.DIGIT_3, label: "3", type: ACTION_TYPES.DIGIT }, | |
{ name: BUTTON_TYPES.DIGIT_4, label: "4", type: ACTION_TYPES.DIGIT }, | |
{ name: BUTTON_TYPES.DIGIT_5, label: "5", type: ACTION_TYPES.DIGIT }, | |
{ name: BUTTON_TYPES.DIGIT_6, label: "6", type: ACTION_TYPES.DIGIT }, | |
{ name: BUTTON_TYPES.DIGIT_7, label: "7", type: ACTION_TYPES.DIGIT }, | |
{ name: BUTTON_TYPES.DIGIT_8, label: "8", type: ACTION_TYPES.DIGIT }, | |
{ name: BUTTON_TYPES.DIGIT_9, label: "9", type: ACTION_TYPES.DIGIT }, | |
{ | |
name: BUTTON_TYPES.FUNCTION_BACKSPACE, | |
label: "←", | |
type: ACTION_TYPES.FUNCTION, | |
}, | |
{ | |
name: BUTTON_TYPES.FUNCTION_CLEAR_ENTRY, | |
label: "CE", | |
type: ACTION_TYPES.FUNCTION, | |
}, | |
{ | |
name: BUTTON_TYPES.FUNCTION_CLEAR, | |
label: "C", | |
type: ACTION_TYPES.FUNCTION, | |
}, | |
{ | |
name: BUTTON_TYPES.FUNCTION_DECIMAL, | |
label: ".", | |
type: ACTION_TYPES.FUNCTION, | |
}, | |
{ | |
name: BUTTON_TYPES.FUNCTION_NEGATE, | |
label: "\xb1", | |
type: ACTION_TYPES.FUNCTION, | |
}, | |
{ | |
name: BUTTON_TYPES.FUNCTION_SQUARE_ROOT, | |
label: "√", | |
type: ACTION_TYPES.FUNCTION, | |
}, | |
{ | |
name: BUTTON_TYPES.FUNCTION_RECIPROCAL, | |
label: "1/x", | |
type: ACTION_TYPES.FUNCTION, | |
}, | |
{ | |
name: BUTTON_TYPES.MEMORY_CLEAR, | |
label: "MC", | |
type: ACTION_TYPES.MEMORY, | |
}, | |
{ | |
name: BUTTON_TYPES.MEMORY_RECALL, | |
label: "MR", | |
type: ACTION_TYPES.MEMORY, | |
}, | |
{ | |
name: BUTTON_TYPES.MEMORY_STORE, | |
label: "MS", | |
type: ACTION_TYPES.MEMORY, | |
}, | |
{ | |
name: BUTTON_TYPES.MEMORY_ADD, | |
label: "M+", | |
type: ACTION_TYPES.MEMORY, | |
}, | |
{ | |
name: BUTTON_TYPES.MEMORY_SUBTRACT, | |
label: "M-", | |
type: ACTION_TYPES.MEMORY, | |
}, | |
{ | |
name: BUTTON_TYPES.OPERATOR_ADDITION, | |
label: "+", | |
type: ACTION_TYPES.OPERATOR, | |
}, | |
{ | |
name: BUTTON_TYPES.OPERATOR_DIVISION, | |
label: "\xf7", | |
type: ACTION_TYPES.OPERATOR, | |
}, | |
{ | |
name: BUTTON_TYPES.OPERATOR_EQUALS, | |
label: "=", | |
type: ACTION_TYPES.OPERATOR, | |
}, | |
{ | |
name: BUTTON_TYPES.OPERATOR_MULTIPLICATION, | |
label: "\xd7", | |
type: ACTION_TYPES.OPERATOR, | |
}, | |
{ | |
name: BUTTON_TYPES.OPERATOR_PERCENTAGE, | |
label: "%", | |
type: ACTION_TYPES.OPERATOR, | |
}, | |
{ | |
name: BUTTON_TYPES.OPERATOR_SUBTRACTION, | |
label: "-", | |
type: ACTION_TYPES.OPERATOR, | |
}, | |
]; | |
const MathSymbols = { | |
ADDITION: "+", | |
DIVISION: "÷", | |
EQUALS: "=", | |
MULTIPLICATION: "×", | |
NEGATE: "±", | |
PERCENTAGE: "%", | |
SQUARE_ROOT: "√", | |
SUBTRACTION: "-", | |
}; | |
const SupportedThemes = [ | |
"light", | |
"dark", | |
"contrast", | |
"red", | |
"green", | |
"blue", | |
"cyan", | |
"magenta", | |
"yellow", | |
]; | |
const errorMessages = { | |
DIVISION_BY_ZERO: "DIV BY 0", | |
NEGATIVE_SQRT: "NEG SQRT", | |
}; | |
// Order of buttons in the grid | |
const buttonOrder = [ | |
BUTTON_TYPES.MEMORY_CLEAR, | |
BUTTON_TYPES.MEMORY_RECALL, | |
BUTTON_TYPES.MEMORY_STORE, | |
BUTTON_TYPES.MEMORY_ADD, | |
BUTTON_TYPES.MEMORY_SUBTRACT, | |
BUTTON_TYPES.FUNCTION_BACKSPACE, | |
BUTTON_TYPES.FUNCTION_CLEAR_ENTRY, | |
BUTTON_TYPES.FUNCTION_CLEAR, | |
BUTTON_TYPES.FUNCTION_NEGATE, | |
BUTTON_TYPES.FUNCTION_SQUARE_ROOT, | |
BUTTON_TYPES.DIGIT_7, | |
BUTTON_TYPES.DIGIT_8, | |
BUTTON_TYPES.DIGIT_9, | |
BUTTON_TYPES.OPERATOR_DIVISION, | |
BUTTON_TYPES.OPERATOR_PERCENTAGE, | |
BUTTON_TYPES.DIGIT_4, | |
BUTTON_TYPES.DIGIT_5, | |
BUTTON_TYPES.DIGIT_6, | |
BUTTON_TYPES.OPERATOR_MULTIPLICATION, | |
BUTTON_TYPES.FUNCTION_RECIPROCAL, | |
BUTTON_TYPES.DIGIT_1, | |
BUTTON_TYPES.DIGIT_2, | |
BUTTON_TYPES.DIGIT_3, | |
BUTTON_TYPES.OPERATOR_SUBTRACTION, | |
BUTTON_TYPES.OPERATOR_EQUALS, | |
BUTTON_TYPES.DIGIT_0, | |
BUTTON_TYPES.FUNCTION_DECIMAL, | |
BUTTON_TYPES.OPERATOR_ADDITION, | |
]; | |
// Map button order to buttons | |
const buttons = buttonOrder.map((name) => | |
Buttons.find((button) => button.name === name) | |
); | |
const operations = { | |
[OPERATOR_TYPES.ADDITION]: { | |
symbol: MathSymbols.ADDITION, | |
perform: BinaryFn.add, | |
}, | |
[OPERATOR_TYPES.SUBTRACTION]: { | |
symbol: MathSymbols.SUBTRACTION, | |
perform: BinaryFn.subtract, | |
}, | |
[OPERATOR_TYPES.MULTIPLICATION]: { | |
symbol: MathSymbols.MULTIPLICATION, | |
perform: BinaryFn.multiply, | |
}, | |
[OPERATOR_TYPES.DIVISION]: { | |
symbol: MathSymbols.DIVISION, | |
perform: BinaryFn.divide, | |
}, | |
[OPERATOR_TYPES.PERCENTAGE]: { | |
symbol: MathSymbols.PERCENTAGE, | |
perform: BinaryFn.percentage, | |
}, | |
[OPERATOR_TYPES.EQUALS]: { | |
symbol: MathSymbols.EQUALS, | |
}, | |
}; | |
const customFunctions = { | |
[FUNCTION_TYPES.CLEAR]: clear, | |
[FUNCTION_TYPES.CLEAR_ENTRY]: clearEntry, | |
[FUNCTION_TYPES.BACKSPACE]: backspace, | |
[FUNCTION_TYPES.NEGATE]: negate, | |
[FUNCTION_TYPES.SQUARE_ROOT]: squareRoot, | |
[FUNCTION_TYPES.RECIPROCAL]: reciprocal, | |
[FUNCTION_TYPES.DECIMAL]: addDecimal, | |
}; | |
const memoryFunctions = { | |
[MEMORY_TYPES.CLEAR]: memoryClear, | |
[MEMORY_TYPES.RECALL]: memoryRecall, | |
[MEMORY_TYPES.STORE]: memoryStore, | |
[MEMORY_TYPES.ADD]: memoryAdd, | |
[MEMORY_TYPES.SUBTRACT]: memorySubtract, | |
}; | |
const keyMapping = { | |
// Digits | |
0: DIGIT_TYPES[0], | |
1: DIGIT_TYPES[1], | |
2: DIGIT_TYPES[2], | |
3: DIGIT_TYPES[3], | |
4: DIGIT_TYPES[4], | |
5: DIGIT_TYPES[5], | |
6: DIGIT_TYPES[6], | |
7: DIGIT_TYPES[7], | |
8: DIGIT_TYPES[8], | |
9: DIGIT_TYPES[9], | |
// Operators | |
"+": OPERATOR_TYPES.ADDITION, | |
"-": OPERATOR_TYPES.SUBTRACTION, | |
"*": OPERATOR_TYPES.MULTIPLICATION, | |
"/": OPERATOR_TYPES.DIVISION, | |
"%": OPERATOR_TYPES.PERCENTAGE, | |
Enter: OPERATOR_TYPES.EQUALS, | |
"=": OPERATOR_TYPES.EQUALS, | |
// Functions | |
Backspace: FUNCTION_TYPES.BACKSPACE, | |
c: FUNCTION_TYPES.CLEAR_ENTRY, | |
C: FUNCTION_TYPES.CLEAR, | |
Escape: FUNCTION_TYPES.CLEAR, | |
".": FUNCTION_TYPES.DECIMAL, | |
}; | |
const initialState = { | |
currentValue: 0, | |
previousValue: null, | |
operator: null, | |
waitingForSecondOperand: false, | |
memory: 0, | |
error: null, | |
floatMode: false, | |
}; | |
// Utility Functions Section | |
function hasOperator(state) { | |
return state.operator && state.waitingForSecondOperand; | |
} | |
function getButtonType(buttonName) { | |
if (buttonName.startsWith("DIGIT")) return ACTION_TYPES.DIGIT; | |
if (buttonName.startsWith("OPERATOR")) return ACTION_TYPES.OPERATOR; | |
if (buttonName.startsWith("FUNCTION")) return ACTION_TYPES.FUNCTION; | |
if (buttonName.startsWith("MEMORY")) return ACTION_TYPES.MEMORY; | |
} | |
function getDigit(name) { | |
return name.replace(/^DIGIT_/, ""); | |
} | |
function getOperatorSymbol(name) { | |
return operations[name]?.symbol ?? ""; | |
} | |
function applyStateTransformation(state, lookup, name) { | |
return lookup[name]?.(state) ?? state; | |
} | |
function formatValue(value) { | |
if (value === "0.") return value; | |
const n = parseFloat(value); | |
return Number.isInteger(n) ? n.toString() : formatFloat(n); | |
} | |
function formatFloat(value) { | |
return parseFloat(value) | |
.toFixed(4) | |
.replace(/\.?0+$/, ""); | |
} | |
// Core Functions Section | |
function handleDigit(name, state) { | |
if (state.error) return state; // Prevent actions if there is an error | |
const digit = getDigit(name); | |
if (state.waitingForSecondOperand) { | |
return { | |
...state, | |
currentValue: parseFloat(digit), | |
floatMode: false, | |
waitingForSecondOperand: false, | |
}; | |
} | |
let newValue; | |
if (state.floatMode) { | |
newValue = state.currentValue.toString() + digit; | |
} else { | |
newValue = | |
state.currentValue === 0 | |
? digit | |
: state.currentValue.toString() + digit; | |
} | |
return { ...state, currentValue: parseFloat(newValue) }; | |
} | |
function handleOperator(name, state) { | |
if (state.error) return state; // Prevent actions if there is an error | |
if (state.currentValue === null || isNaN(state.currentValue)) | |
return state; | |
if (name === OPERATOR_TYPES.EQUALS && state.previousValue == null) | |
return state; | |
const inputValue = state.currentValue; | |
if (hasOperator(state)) { | |
return { ...state, operator: name }; | |
} | |
if (state.previousValue == null) { | |
return { | |
...state, | |
previousValue: inputValue, | |
operator: name, | |
waitingForSecondOperand: true, | |
}; | |
} | |
if (state.operator) { | |
try { | |
const result = operations[state.operator].perform( | |
state.previousValue, | |
inputValue | |
); | |
return { | |
...state, | |
currentValue: result, | |
previousValue: result, | |
operator: name, | |
waitingForSecondOperand: true, | |
}; | |
} catch (error) { | |
return { | |
...state, | |
error: error.message, | |
}; | |
} | |
} | |
return state; | |
} | |
function handleFunction(name, state) { | |
if (state.error && name !== FUNCTION_TYPES.CLEAR) return state; // Only allow clearing the error | |
return applyStateTransformation(state, customFunctions, name); | |
} | |
function handleMemory(name, state) { | |
if (state.error) return state; // Prevent actions if there is an error | |
return applyStateTransformation(state, memoryFunctions, name); | |
} | |
function performAction(action, ref, state) { | |
const newState = handleType(action.type, action.name, state); | |
Object.assign(state, newState); | |
render(ref, state); | |
} | |
function handleType(type, name, state) { | |
switch (type) { | |
case ACTION_TYPES.DIGIT: | |
return handleDigit(name, state); | |
case ACTION_TYPES.OPERATOR: | |
return handleOperator(name, state); | |
case ACTION_TYPES.FUNCTION: | |
return handleFunction(name, state); | |
case ACTION_TYPES.MEMORY: | |
return handleMemory(name, state); | |
} | |
} | |
// Event Listeners Section | |
function addListeners(ref, state) { | |
ref.addEventListener("click", (event) => | |
handleClick(event, ref, state) | |
); | |
ref.addEventListener("focus", () => { | |
activeCalculator = { ref, state }; | |
}); | |
ref.addEventListener("blur", () => { | |
activeCalculator = null; | |
}); | |
document.addEventListener("keydown", handleKeyDown); | |
} | |
function handleClick(event, ref, state) { | |
// Focus on calculator | |
if (!activeCalculator || activeCalculator.ref !== ref) { | |
activeCalculator = { ref, state }; | |
} | |
// Handle the button logic | |
const button = event.target; | |
if (button.classList.contains("calculator-button")) { | |
const action = { | |
name: button.dataset.name, | |
type: getButtonType(button.dataset.name), | |
}; | |
performAction(action, ref, state); | |
} | |
} | |
function handleKeyDown(event) { | |
if (!activeCalculator) return; | |
const buttonName = keyMapping[event.key]; | |
if (!buttonName) return; | |
const action = { | |
name: buttonName, | |
type: getButtonType(buttonName), | |
}; | |
performAction(action, activeCalculator.ref, activeCalculator.state); | |
event.preventDefault(); | |
} | |
// Initialization Section | |
function addButtons(ref) { | |
const gridEl = ref.querySelector(".calculator-button-grid"); | |
buttons.forEach(({ label, name, type }) => { | |
gridEl.insertAdjacentHTML( | |
"beforeend", | |
` | |
<button | |
type="button" | |
class="calculator-button" | |
title="${name.toLowerCase().replace(/_/g, " ")}" | |
data-name="${name}" | |
data-type="${type}" | |
> | |
${label} | |
</button> | |
` | |
); | |
}); | |
} | |
function initCalculator(ref, state) { | |
addButtons(ref); | |
addListeners(ref, state); | |
render(ref, state); | |
} | |
function CalculatorApp(elementOrSelector) { | |
if (!isElementOrSelector(elementOrSelector)) { | |
throw new Error( | |
`${typeof elementOrSelector} must be an element or selector` | |
); | |
} | |
const ref = isSelector(elementOrSelector) | |
? document.querySelector(elementOrSelector) | |
: elementOrSelector; | |
let state = structuredClone(initialState); | |
ref.classList.add("calculator"); | |
ref.innerHTML = ` | |
<div class="calculator-display"> | |
<div class="calculator-status"> | |
<div class="calculator-memory"></div> | |
<div class="calculator-buffer"></div> | |
</div> | |
<div class="calculator-value">0</div> | |
</div> | |
<div class="calculator-button-grid"></div> | |
`; | |
ref.tabIndex = 0; | |
initCalculator(ref, state); | |
} | |
let activeCalculator; | |
// Rendering Section | |
function render(ref, state) { | |
const displayEl = ref.querySelector(".calculator-display"); | |
const bufferEl = displayEl.querySelector(".calculator-buffer"); | |
const valueEl = displayEl.querySelector(".calculator-value"); | |
const memoryEl = displayEl.querySelector(".calculator-memory"); | |
bufferEl.textContent = renderBufferDisplay(state); | |
valueEl.textContent = renderCurrentDisplay(state); | |
memoryEl.textContent = renderMemoryDisplay(state); | |
} | |
function renderBufferDisplay(state) { | |
if (state.error) return ""; | |
if (state.operator === OPERATOR_TYPES.EQUALS) return ""; | |
const operator = getOperatorSymbol(state.operator); | |
const previousValue = | |
state.previousValue !== null ? formatValue(state.previousValue) : ""; | |
return [previousValue, operator].filter(Boolean).join(" "); | |
} | |
function renderCurrentDisplay(state) { | |
if (state.error) return errorMessages[state.error] || "Error"; | |
// Convert the current value to a string and ensure it displays correctly | |
let displayValue = state.currentValue.toString(); | |
// If in float mode and there's no decimal point, add a trailing dot | |
if (state.floatMode && !displayValue.includes(".")) { | |
displayValue += "."; | |
} | |
return formatValue(displayValue); | |
} | |
function renderMemoryDisplay(state) { | |
if (state.memory === null || state.memory === 0) return ""; | |
return `M = ${formatValue(state.memory)}`; | |
} | |
// Operations and Functions Section | |
function clear(state) { | |
return { | |
...state, | |
currentValue: 0, | |
previousValue: null, | |
operator: null, | |
waitingForSecondOperand: false, | |
error: null, | |
floatMode: false, | |
}; | |
} | |
function applyMath(state, fn) { | |
try { | |
const result = fn(parseFloat(state.currentValue)); | |
return { | |
...state, | |
currentValue: result, | |
floatMode: !Number.isInteger(result), | |
}; | |
} catch (error) { | |
return { ...state, error: error.message, currentValue: 0 }; | |
} | |
} | |
function clearEntry(state) { | |
return { ...state, currentValue: 0, error: null, floatMode: false }; | |
} | |
function backspace(state) { | |
if (state.error) return state; // Prevent actions if there is an error | |
let currentValueStr = state.currentValue.toString(); | |
if (currentValueStr.length === 1 || currentValueStr === "-0") { | |
return { ...state, currentValue: 0, floatMode: false }; | |
} | |
const newValueStr = currentValueStr.slice(0, -1); | |
const newFloatMode = newValueStr.includes("."); | |
const newValue = newValueStr === "-" ? 0 : parseFloat(newValueStr) || 0; | |
return { | |
...state, | |
currentValue: newValue, | |
floatMode: newFloatMode, | |
}; | |
} | |
function negate(state) { | |
if (isEmpty(state.currentValue)) return state; | |
return applyMath(state, UnaryFn.negate); | |
} | |
function squareRoot(state) { | |
if (isEmpty(state.currentValue)) return state; | |
return applyMath(state, UnaryFn.sqrt); | |
} | |
function reciprocal(state) { | |
if (isEmpty(state.currentValue)) return state; | |
return applyMath(state, UnaryFn.reciprocal); | |
} | |
function addDecimal(state) { | |
if (state.error) return state; // Prevent actions if there is an error | |
// If the user was waiting for the second operand, reset the current value to '0.' | |
if (state.waitingForSecondOperand) { | |
return { | |
...state, | |
currentValue: "0.", | |
floatMode: true, | |
waitingForSecondOperand: false, | |
}; | |
} | |
// If the current value already contains a decimal point, return the current state | |
if (state.floatMode) { | |
return state; | |
} | |
// Append the decimal point to the current value | |
return { | |
...state, | |
currentValue: state.currentValue.toString() + ".", | |
floatMode: true, | |
}; | |
} | |
function memoryClear(state) { | |
return { ...state, memory: 0, error: null }; | |
} | |
function memoryRecall(state) { | |
if (state.memory === null) return state; | |
return { | |
...state, | |
currentValue: state.memory, | |
waitingForSecondOperand: false, | |
error: null, | |
}; | |
} | |
function memoryStore(state) { | |
return { ...state, memory: state.currentValue, error: null }; | |
} | |
function memoryAdd(state) { | |
const newValue = | |
parseFloat(state.memory) + parseFloat(state.currentValue); | |
return { ...state, memory: newValue, error: null }; | |
} | |
function memorySubtract(state) { | |
const newValue = | |
parseFloat(state.memory) - parseFloat(state.currentValue); | |
return { ...state, memory: newValue, error: null }; | |
} | |
// General functions | |
function isEmpty(v) { | |
return v === ""; | |
} | |
function isElement(v) { | |
return v instanceof HTMLElement; | |
} | |
function isSelector(v) { | |
return typeof v === "string"; | |
} | |
function isElementOrSelector(v) { | |
return isElement(v) || isSelector(v); | |
} | |
</script> | |
</head> | |
<body> | |
<div id="calculators-container"></div> | |
<!-- Demo --> | |
<script> | |
document.addEventListener("DOMContentLoaded", loadDemo); | |
function loadDemo() { | |
const containerEl = document.getElementById("calculators-container"); | |
SupportedThemes.forEach( | |
(theme) => new CalculatorApp(spawn(theme, containerEl)) | |
); | |
} | |
function spawn(theme, containerEl) { | |
const el = document.createElement("div"); | |
el.id = `calculator-${theme}`; | |
el.dataset.theme = theme; | |
containerEl.appendChild(el); | |
return el; | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment