feat: add responsive calc-02 HP48GX demo
This commit is contained in:
+2
-1
@@ -1,6 +1,7 @@
|
||||
# State
|
||||
- Core engine: `src/rpn-calculator.js`
|
||||
- Active demo: `samples/dev/` (HP48-style UI)
|
||||
- Active demo: `samples/calc-02/` responsive HP48GX layout driven by portrait/landscape text mockups; display-adjacent button row stays in 4 columns
|
||||
- Mode button shows the current angle mode only; selecting a mode uses a popup menu
|
||||
- Public API: `push`, `pop`, `clear`, `swap`, `remove`, `edit`, `isValidIndex`, `input`, `command`, `getOperationsByCategory`, `getConstants`
|
||||
- Config: `maxSize`, `base`, `angleMode`, `enabledCommands`
|
||||
- Commands: arithmetic, stack, trigonometry, constants `pi` and `e`
|
||||
|
||||
@@ -22,9 +22,9 @@ The main class is `RpnCalculator`.
|
||||
- `samples/calc-01/index.html`: active browser demo entry point
|
||||
- `samples/calc-01/index.css`: demo styles
|
||||
- `samples/calc-01/index.js`: demo UI and keyboard logic
|
||||
- `samples/calc-01/index.html`: alternate browser demo entry point
|
||||
- `samples/calc-01/index.css`: alternate demo styles
|
||||
- `samples/calc-01/index.js`: alternate demo UI and keyboard logic
|
||||
- `samples/calc-02/index.html`: new responsive HP48GX-style demo entry point
|
||||
- `samples/calc-02/index.css`: new responsive demo styles
|
||||
- `samples/calc-02/index.js`: new demo UI and keyboard logic
|
||||
- `samples/calc-XX/`: placeholder name for future demo variants
|
||||
|
||||
## Public API
|
||||
@@ -268,6 +268,11 @@ The current demo supports:
|
||||
The demo also implements stack selection and stack-item move mode in its UI layer using the public calculator methods.
|
||||
It keeps the calculator screen focused and updates the visible stack window as the selection moves.
|
||||
|
||||
## Calc 02 demo
|
||||
|
||||
`samples/calc-02/` is a new responsive HP48GX-inspired demo.
|
||||
It adapts its layout to the browser window and switches between the supplied portrait and landscape arrangements.
|
||||
|
||||
## Exports
|
||||
|
||||
`RpnCalculator` is exposed in both environments:
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
:root {
|
||||
--bg0: #10151e;
|
||||
--bg1: #1b2432;
|
||||
--panel: #2c3442;
|
||||
--panel2: #394354;
|
||||
--edge: #0c1118;
|
||||
--display: #cfe0ae;
|
||||
--display2: #b9cd8a;
|
||||
--displayText: #1f2a12;
|
||||
--buttonText: #f4f7fb;
|
||||
--shadow: rgba(0, 0, 0, 0.35);
|
||||
--btnTop: #444c58;
|
||||
--btnBottom: #2f3640;
|
||||
--btnAccentTop: #3f526b;
|
||||
--btnAccentBottom: #2b394c;
|
||||
--btnAltTop: #525c69;
|
||||
--btnAltBottom: #3a434f;
|
||||
--btnDangerTop: #584042;
|
||||
--btnDangerBottom: #402d2f;
|
||||
--btnEnterTop: #465349;
|
||||
--btnEnterBottom: #303a31;
|
||||
--btnText: #eef2f7;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
font-family: Arial, sans-serif;
|
||||
color: var(--buttonText);
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(255, 255, 255, 0.08), transparent 32%),
|
||||
linear-gradient(180deg, var(--bg1), var(--bg0));
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: clamp(12px, 2vw, 28px);
|
||||
}
|
||||
|
||||
.calculator {
|
||||
width: min(100vw - 24px, 1120px);
|
||||
height: min(100vh - 24px, 900px);
|
||||
display: grid;
|
||||
gap: clamp(10px, 1.4vw, 18px);
|
||||
padding: clamp(12px, 1.8vw, 18px);
|
||||
border-radius: 28px;
|
||||
background: linear-gradient(180deg, var(--panel2), var(--panel));
|
||||
border: 1px solid var(--edge);
|
||||
box-shadow: 0 26px 70px var(--shadow), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
grid-template-columns: 1.3fr 0.9fr;
|
||||
grid-template-rows: minmax(0, 0.62fr) min-content minmax(180px, 1fr) minmax(180px, 1fr);
|
||||
align-content: start;
|
||||
grid-template-areas:
|
||||
"display functions"
|
||||
"buttons functions"
|
||||
"keypad functions"
|
||||
"keypad trigo";
|
||||
}
|
||||
|
||||
.display-panel,
|
||||
.display-buttons-panel,
|
||||
.keypad-panel,
|
||||
.functions-panel,
|
||||
.trigo-panel,
|
||||
.status-line {
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
background: rgba(6, 10, 16, 0.16);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.display-panel {
|
||||
grid-area: display;
|
||||
padding: clamp(12px, 1.5vw, 16px);
|
||||
background: linear-gradient(180deg, var(--display), var(--display2));
|
||||
color: var(--displayText);
|
||||
font-family: "Courier New", monospace;
|
||||
overflow: hidden;
|
||||
height: clamp(112px, 18vw, 160px);
|
||||
max-height: 140px;
|
||||
align-self: start;
|
||||
margin-bottom: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.display-grid {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: repeat(4, minmax(0, 1fr));
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.stack-cell {
|
||||
display: grid;
|
||||
grid-template-columns: 2.2ch 1fr;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: clamp(18px, 3vw, 30px);
|
||||
line-height: 1;
|
||||
min-height: 0;
|
||||
padding-block: 0;
|
||||
}
|
||||
|
||||
.stack-label {
|
||||
text-align: right;
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.stack-value {
|
||||
min-height: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.display-buttons-panel {
|
||||
grid-area: buttons;
|
||||
padding: 8px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-rows: repeat(2, auto);
|
||||
gap: 8px;
|
||||
align-content: start;
|
||||
align-items: stretch;
|
||||
grid-auto-flow: row;
|
||||
grid-auto-rows: auto;
|
||||
background: linear-gradient(180deg, #242a33, #1a1f27);
|
||||
border-color: rgba(255, 255, 255, 0.04);
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.display-button {
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 3px 0 rgba(0, 0, 0, 0.34);
|
||||
background-clip: padding-box;
|
||||
background: linear-gradient(180deg, #3a414c, #252b34);
|
||||
color: #e8edf3;
|
||||
border-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.display-button-offset {
|
||||
grid-column-start: 2;
|
||||
}
|
||||
|
||||
.display-buttons-panel > button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mode-menu {
|
||||
position: fixed;
|
||||
z-index: 20;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 10px;
|
||||
border-radius: 14px;
|
||||
background: rgba(18, 24, 33, 0.98);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 14px 30px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.mode-menu-item {
|
||||
min-width: 120px;
|
||||
background: linear-gradient(180deg, var(--btnAltTop), var(--btnAltBottom));
|
||||
}
|
||||
|
||||
.mode-menu-item.is-active {
|
||||
outline: 2px solid rgba(207, 224, 174, 0.7);
|
||||
}
|
||||
|
||||
.display-button:nth-child(6),
|
||||
.display-button:nth-child(7) {
|
||||
background: linear-gradient(180deg, #343b46, #20262e);
|
||||
}
|
||||
|
||||
.keypad-panel {
|
||||
grid-area: keypad;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.functions-panel {
|
||||
grid-area: functions;
|
||||
padding: 10px;
|
||||
align-self: start;
|
||||
min-height: 0;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.trigo-panel {
|
||||
grid-area: trigo;
|
||||
padding: 10px;
|
||||
align-self: start;
|
||||
min-height: 0;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.status-line {
|
||||
grid-area: status;
|
||||
padding: 10px 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 42px;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.keypad-grid,
|
||||
.functions-grid,
|
||||
.trigo-grid {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
grid-auto-rows: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.keypad-grid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-rows: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.functions-grid,
|
||||
.trigo-grid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.functions-grid,
|
||||
.trigo-grid {
|
||||
grid-template-rows: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
button {
|
||||
border: 1px solid rgba(14, 18, 25, 0.85);
|
||||
border-radius: 12px;
|
||||
padding: 10px 8px;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
color: var(--btnText);
|
||||
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.35);
|
||||
cursor: pointer;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 3px 0 rgba(0, 0, 0, 0.28);
|
||||
transition: transform 120ms ease, filter 120ms ease, box-shadow 120ms ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
filter: brightness(1.06);
|
||||
}
|
||||
|
||||
.display-button:hover {
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(2px);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 1px 0 rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.display-button:active {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
.key-default {
|
||||
background: linear-gradient(180deg, var(--btnTop), var(--btnBottom));
|
||||
color: #eef2f7;
|
||||
}
|
||||
|
||||
.key-accent {
|
||||
background: linear-gradient(180deg, var(--btnAccentTop), var(--btnAccentBottom));
|
||||
color: #eef2f7;
|
||||
}
|
||||
|
||||
.key-alt {
|
||||
background: linear-gradient(180deg, var(--btnAltTop), var(--btnAltBottom));
|
||||
color: #eef2f7;
|
||||
}
|
||||
|
||||
.key-danger {
|
||||
background: linear-gradient(180deg, var(--btnDangerTop), var(--btnDangerBottom));
|
||||
color: #eef2f7;
|
||||
}
|
||||
|
||||
.key-enter {
|
||||
background: linear-gradient(180deg, var(--btnEnterTop), var(--btnEnterBottom));
|
||||
color: #eef2f7;
|
||||
}
|
||||
|
||||
.hidden-input {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (orientation: portrait), (max-width: 860px) {
|
||||
.calculator {
|
||||
width: min(100vw - 16px, 760px);
|
||||
height: auto;
|
||||
min-height: calc(100vh - 16px);
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: minmax(160px, auto) auto minmax(220px, auto) auto auto;
|
||||
grid-template-areas:
|
||||
"display"
|
||||
"buttons"
|
||||
"keypad"
|
||||
"functions"
|
||||
"trigo";
|
||||
}
|
||||
|
||||
.display-buttons-panel {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-rows: repeat(2, auto);
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.keypad-grid {
|
||||
grid-template-rows: repeat(5, minmax(42px, 1fr));
|
||||
}
|
||||
|
||||
.functions-grid,
|
||||
.trigo-grid {
|
||||
grid-auto-rows: minmax(0, 1fr);
|
||||
grid-template-rows: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.app-shell {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.calculator {
|
||||
width: 100%;
|
||||
min-height: calc(100vh - 16px);
|
||||
border-radius: 20px;
|
||||
padding: 10px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.display-panel {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.stack-cell {
|
||||
font-size: clamp(16px, 5.2vw, 22px);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 10px;
|
||||
padding: 8px 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.display-buttons-panel {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>HP48GX RPN Calculator</title>
|
||||
<link rel="stylesheet" href="./index.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="app-shell">
|
||||
<section class="calculator calculator-portrait" aria-label="HP48GX style RPN calculator">
|
||||
<div class="display-panel">
|
||||
<div class="display-frame">
|
||||
<div class="display-grid">
|
||||
<div class="stack-cell"><span class="stack-label">T:</span><span id="stackT" class="stack-value"></span></div>
|
||||
<div class="stack-cell"><span class="stack-label">Z:</span><span id="stackZ" class="stack-value"></span></div>
|
||||
<div class="stack-cell"><span class="stack-label">Y:</span><span id="stackY" class="stack-value"></span></div>
|
||||
<div class="stack-cell"><span class="stack-label">X:</span><span id="stackX" class="stack-value"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="display-buttons-panel">
|
||||
<button id="modeButton" class="display-button">Mode</button>
|
||||
<button id="pasteButton" class="display-button">Paste</button>
|
||||
<button id="upButton" class="display-button">Up</button>
|
||||
<button id="constButton" class="display-button">Const</button>
|
||||
<button id="rightButton" class="display-button display-button-offset">Right</button>
|
||||
<button id="downButton" class="display-button">Down</button>
|
||||
<button id="constRightButton" class="display-button">Right</button>
|
||||
</div>
|
||||
|
||||
<div class="keypad-panel">
|
||||
<div class="keypad-grid" id="keypadGrid"></div>
|
||||
</div>
|
||||
|
||||
<div class="functions-panel">
|
||||
<div class="functions-grid" id="functionsGrid"></div>
|
||||
</div>
|
||||
|
||||
<div class="trigo-panel">
|
||||
<div class="trigo-grid" id="trigoGrid"></div>
|
||||
</div>
|
||||
|
||||
<input id="hiddenInput" class="hidden-input" type="text" autocomplete="off" aria-hidden="true" tabindex="-1">
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="../../src/rpn-calculator.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,310 @@
|
||||
const calc = new RpnCalculator({ angleMode: 'deg' });
|
||||
|
||||
const hiddenInput = document.getElementById('hiddenInput');
|
||||
const modeButton = document.getElementById('modeButton');
|
||||
const pasteButton = document.getElementById('pasteButton');
|
||||
const upButton = document.getElementById('upButton');
|
||||
const constButton = document.getElementById('constButton');
|
||||
const rightButton = document.getElementById('rightButton');
|
||||
const downButton = document.getElementById('downButton');
|
||||
const constRightButton = document.getElementById('constRightButton');
|
||||
|
||||
const stackEls = {
|
||||
T: document.getElementById('stackT'),
|
||||
Z: document.getElementById('stackZ'),
|
||||
Y: document.getElementById('stackY'),
|
||||
X: document.getElementById('stackX'),
|
||||
};
|
||||
|
||||
const keypadGrid = document.getElementById('keypadGrid');
|
||||
const functionsGrid = document.getElementById('functionsGrid');
|
||||
const trigoGrid = document.getElementById('trigoGrid');
|
||||
const calculatorEl = document.querySelector('.calculator');
|
||||
|
||||
const keypadKeys = [
|
||||
{ label: '±', action: 'neg', className: 'key-default' },
|
||||
{ label: 'C', action: 'clear', className: 'key-danger' },
|
||||
{ label: '⎋', action: 'escape', className: 'key-danger' },
|
||||
{ label: '⌫', action: 'backspace', className: 'key-danger' },
|
||||
{ label: '7', input: '7', className: 'key-default' },
|
||||
{ label: '8', input: '8', className: 'key-default' },
|
||||
{ label: '9', input: '9', className: 'key-default' },
|
||||
{ label: '/', action: 'div', className: 'key-accent' },
|
||||
{ label: '4', input: '4', className: 'key-default' },
|
||||
{ label: '5', input: '5', className: 'key-default' },
|
||||
{ label: '6', input: '6', className: 'key-default' },
|
||||
{ label: '*', action: 'mul', className: 'key-accent' },
|
||||
{ label: '1', input: '1', className: 'key-default' },
|
||||
{ label: '2', input: '2', className: 'key-default' },
|
||||
{ label: '3', input: '3', className: 'key-default' },
|
||||
{ label: '-', action: 'sub', className: 'key-accent' },
|
||||
{ label: '0', input: '0', className: 'key-default' },
|
||||
{ label: '.', input: '.', className: 'key-default' },
|
||||
{ label: 'Enter', action: 'enter', className: 'key-enter' },
|
||||
{ label: '+', action: 'add', className: 'key-accent' },
|
||||
];
|
||||
|
||||
const functionKeys = [
|
||||
{ label: 'x²', action: 'sqr', className: 'key-default' },
|
||||
{ label: 'yˣ', action: 'pow', className: 'key-default' },
|
||||
{ label: '1/x', action: 'recip', className: 'key-default' },
|
||||
{ label: '%', action: 'mod', className: 'key-default' },
|
||||
{ label: '√x', action: 'sqrt', className: 'key-default' },
|
||||
{ label: 'y√x', action: 'pow', className: 'key-default' },
|
||||
{ label: '10ˣ', action: 'pow10', className: 'key-default' },
|
||||
{ label: '', spacer: true },
|
||||
{ label: 'log', action: 'log', className: 'key-default' },
|
||||
{ label: 'ln', action: 'ln', className: 'key-default' },
|
||||
{ label: '', spacer: true },
|
||||
{ label: '', spacer: true },
|
||||
];
|
||||
|
||||
const trigoKeys = [
|
||||
{ label: 'sin', action: 'sin', className: 'key-default' },
|
||||
{ label: 'cos', action: 'cos', className: 'key-default' },
|
||||
{ label: 'tan', action: 'tan', className: 'key-default' },
|
||||
{ label: '', spacer: true },
|
||||
{ label: 'asin', action: 'asin', className: 'key-default' },
|
||||
{ label: 'acos', action: 'acos', className: 'key-default' },
|
||||
{ label: 'atan', action: 'atan', className: 'key-default' },
|
||||
{ label: '', spacer: true },
|
||||
];
|
||||
|
||||
|
||||
function focusInput() {
|
||||
hiddenInput.focus();
|
||||
}
|
||||
|
||||
function setStatus(message) {
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
function normalizeStack() {
|
||||
while (calc.stack.length > 4) {
|
||||
calc.stack.shift();
|
||||
}
|
||||
}
|
||||
|
||||
function getStackLine(indexFromTop) {
|
||||
const index = calc.stack.length - 1 - indexFromTop;
|
||||
return index >= 0 ? calc.stack[index] : '';
|
||||
}
|
||||
|
||||
function render() {
|
||||
normalizeStack();
|
||||
const isPortrait = window.matchMedia('(orientation: portrait)').matches || window.innerWidth <= 860;
|
||||
calculatorEl?.classList.toggle('portrait', isPortrait);
|
||||
calculatorEl?.classList.toggle('landscape', !isPortrait);
|
||||
stackEls.T.textContent = calc.formatNumber(getStackLine(3)) || '';
|
||||
stackEls.Z.textContent = calc.formatNumber(getStackLine(2)) || '';
|
||||
stackEls.Y.textContent = calc.formatNumber(getStackLine(1)) || '';
|
||||
stackEls.X.textContent = calc.formatNumber(getStackLine(0)) || (calc.isEditing ? calc.inputValue : '');
|
||||
modeButton.textContent = calc.angleMode;
|
||||
}
|
||||
|
||||
function pushEditingValueIfNeeded() {
|
||||
if (!calc.isEditing) return;
|
||||
if (calc.inputValue !== '') {
|
||||
calc.push(calc.parseInputValue(calc.inputValue));
|
||||
}
|
||||
calc.inputValue = '';
|
||||
calc.isEditing = false;
|
||||
}
|
||||
|
||||
function inputToX(value) {
|
||||
if (!calc.isEditing) {
|
||||
calc.isEditing = true;
|
||||
calc.inputValue = '';
|
||||
}
|
||||
if (value === 'Backspace') {
|
||||
calc.inputValue = calc.inputValue.slice(0, -1);
|
||||
} else {
|
||||
calc.inputValue += value;
|
||||
}
|
||||
if (calc.inputValue === '') {
|
||||
calc.isEditing = false;
|
||||
}
|
||||
}
|
||||
|
||||
function execute(name) {
|
||||
try {
|
||||
if (name === 'enter') {
|
||||
pushEditingValueIfNeeded();
|
||||
} else if (name === 'clear') {
|
||||
calc.clear();
|
||||
calc.inputValue = '';
|
||||
calc.isEditing = false;
|
||||
} else if (name === 'escape') {
|
||||
calc.inputValue = '';
|
||||
calc.isEditing = false;
|
||||
} else if (name === 'backspace') {
|
||||
if (calc.isEditing) {
|
||||
inputToX('Backspace');
|
||||
} else {
|
||||
calc.remove(0);
|
||||
}
|
||||
} else if (name === 'neg') {
|
||||
if (calc.isEditing) {
|
||||
calc.inputValue = calc.inputValue.startsWith('-') ? calc.inputValue.slice(1) : `-${calc.inputValue}`;
|
||||
} else {
|
||||
calc.push(calc.pop() * -1);
|
||||
}
|
||||
} else if (name === 'pow10') {
|
||||
pushEditingValueIfNeeded();
|
||||
calc.push(10);
|
||||
calc.command('pow');
|
||||
} else {
|
||||
pushEditingValueIfNeeded();
|
||||
calc.command(name);
|
||||
}
|
||||
render();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
function createKeyButton({ label, input, action, spacer, className }) {
|
||||
if (spacer) {
|
||||
const div = document.createElement('div');
|
||||
return div;
|
||||
}
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.textContent = label;
|
||||
button.className = className;
|
||||
button.addEventListener('click', () => {
|
||||
focusInput();
|
||||
if (input) {
|
||||
inputToX(input);
|
||||
render();
|
||||
return;
|
||||
}
|
||||
execute(action);
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
function buildGrid(container, keys) {
|
||||
container.innerHTML = '';
|
||||
keys.forEach((key) => container.appendChild(createKeyButton(key)));
|
||||
}
|
||||
|
||||
function handleKeyboard(event) {
|
||||
if (event.target === hiddenInput) return;
|
||||
const key = event.key;
|
||||
if (/^[0-9.]$/.test(key)) {
|
||||
event.preventDefault();
|
||||
inputToX(key);
|
||||
render();
|
||||
return;
|
||||
}
|
||||
const map = {
|
||||
Enter: 'enter',
|
||||
Backspace: 'backspace',
|
||||
Escape: 'escape',
|
||||
Delete: 'clear',
|
||||
'+': 'add',
|
||||
'-': 'sub',
|
||||
'*': 'mul',
|
||||
'/': 'div',
|
||||
'%': 'mod',
|
||||
'^': 'pow',
|
||||
};
|
||||
if (map[key]) {
|
||||
event.preventDefault();
|
||||
execute(map[key]);
|
||||
}
|
||||
}
|
||||
|
||||
const modeOptions = ['deg', 'rad', 'grad'];
|
||||
let modeMenuEl = null;
|
||||
|
||||
function closeModeMenu() {
|
||||
if (modeMenuEl) {
|
||||
modeMenuEl.remove();
|
||||
modeMenuEl = null;
|
||||
}
|
||||
}
|
||||
|
||||
function openModeMenu() {
|
||||
closeModeMenu();
|
||||
const rect = modeButton.getBoundingClientRect();
|
||||
modeMenuEl = document.createElement('div');
|
||||
modeMenuEl.className = 'mode-menu';
|
||||
modeMenuEl.style.top = `${rect.bottom + 6 + window.scrollY}px`;
|
||||
modeMenuEl.style.left = `${rect.left + window.scrollX}px`;
|
||||
modeOptions.forEach((mode) => {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = `mode-menu-item${mode === calc.angleMode ? ' is-active' : ''}`;
|
||||
button.textContent = mode;
|
||||
button.addEventListener('click', () => {
|
||||
calc.angleMode = mode;
|
||||
render();
|
||||
closeModeMenu();
|
||||
});
|
||||
modeMenuEl.appendChild(button);
|
||||
});
|
||||
document.body.appendChild(modeMenuEl);
|
||||
const menuRect = modeMenuEl.getBoundingClientRect();
|
||||
const maxLeft = Math.max(8, window.innerWidth - menuRect.width - 8);
|
||||
modeMenuEl.style.left = `${Math.max(8, Math.min(maxLeft, rect.left + window.scrollX))}px`;
|
||||
}
|
||||
|
||||
modeButton.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
openModeMenu();
|
||||
});
|
||||
|
||||
window.addEventListener('resize', closeModeMenu);
|
||||
window.addEventListener('scroll', closeModeMenu, true);
|
||||
|
||||
window.addEventListener('click', (event) => {
|
||||
if (modeMenuEl && !event.target.closest('.mode-menu') && event.target !== modeButton) {
|
||||
closeModeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
pasteButton.addEventListener('click', async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
if (!text) {
|
||||
setStatus('Clipboard empty');
|
||||
return;
|
||||
}
|
||||
if (calc.isEditing) {
|
||||
calc.inputValue += text;
|
||||
} else {
|
||||
calc.isEditing = true;
|
||||
calc.inputValue = text;
|
||||
}
|
||||
setStatus('Pasted');
|
||||
render();
|
||||
} catch (error) {
|
||||
setStatus('Paste unavailable');
|
||||
}
|
||||
});
|
||||
|
||||
upButton.addEventListener('click', () => {});
|
||||
|
||||
constButton.addEventListener('click', () => {});
|
||||
|
||||
rightButton.addEventListener('click', () => {});
|
||||
|
||||
downButton.addEventListener('click', () => {});
|
||||
|
||||
constRightButton.addEventListener('click', () => {});
|
||||
|
||||
window.addEventListener('keydown', handleKeyboard);
|
||||
window.addEventListener('load', focusInput);
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
if (!event.target.closest('.calculator')) {
|
||||
focusInput();
|
||||
}
|
||||
});
|
||||
|
||||
buildGrid(keypadGrid, keypadKeys);
|
||||
buildGrid(functionsGrid, functionKeys);
|
||||
buildGrid(trigoGrid, trigoKeys);
|
||||
render();
|
||||
@@ -0,0 +1,10 @@
|
||||
┌──────────── Display ────────────┐ ┌────── Functions ─────┐ ┌──────────── Keypad ─────────────┐
|
||||
| T: | | x^2 | y^x | 1/x | % | | +/- | Clear | Esc | backspace |
|
||||
| Z: | | √x | y√x | 10^x | | | 7 | 8 | 9 | / |
|
||||
| Y: | | log | ln | | | | 4 | 5 | 6 | * |
|
||||
| X: | └──────────────────────┘ | 1 | 2 | 3 | - |
|
||||
└─────────────────────────────────┘ ┌─────── Trigo ────────┐ | 0 | . | Enter | + |
|
||||
┌──────── Display Buttons ────────┐ | sin | cos | tan | └─────────────────────────────────┘
|
||||
| Mode | Paste | Up | Const | | asin | acos | atan |
|
||||
| | Right | Down | Right | └──────────────────────┘
|
||||
└─────────────────────────────────┘
|
||||
@@ -0,0 +1,26 @@
|
||||
┌──────────── Display ────────────┐
|
||||
| T: |
|
||||
| Z: |
|
||||
| Y: |
|
||||
| X: |
|
||||
└─────────────────────────────────┘
|
||||
┌──────── Display Buttons ────────┐
|
||||
| Mode | Paste | Up | Const |
|
||||
| | Right | Down | Right |
|
||||
└─────────────────────────────────┘
|
||||
┌──────────── Keypad ─────────────┐
|
||||
| +/- | Clear | Esc | backspace |
|
||||
| 7 | 8 | 9 | / |
|
||||
| 4 | 5 | 6 | * |
|
||||
| 1 | 2 | 3 | - |
|
||||
| 0 | . | Enter | + |
|
||||
└─────────────────────────────────┘
|
||||
┌─────────── Functions ───────────┐
|
||||
| x^2 | y^x | 1/x | % |
|
||||
| √x | y√x | 10^x | |
|
||||
| log | ln | | |
|
||||
└─────────────────────────────────┘
|
||||
┌───────────── Trigo ─────────────┐
|
||||
| sin | cos | tan | |
|
||||
| asin | acos | atan | |
|
||||
└─────────────────────────────────┘
|
||||
Reference in New Issue
Block a user