Compare commits

..

13 Commits

Author SHA1 Message Date
matmoul 16b567731f docs: update calc-02 shortcut reference and key mappings 2026-05-17 00:14:24 +02:00
matmoul b710d5f0eb refactor(calc-02): simplify stack display and edit cursor handling 2026-05-16 23:43:49 +02:00
matmoul 07a4c533fb fix: preserve full calc-02 stack while limiting display to top 4
Keep the calculator stack unlimited in the demo and only constrain the rendered stack view. Also restore the edit cursor position when pulling a value into the input from the stack.
2026-05-16 23:40:13 +02:00
matmoul acc075d30c feat(calc-02): add cursor-aware editing in the X stack display 2026-05-16 23:30:50 +02:00
matmoul 6a37c7c1e8 feat: add pow10 and exp arithmetic operations 2026-05-16 23:15:22 +02:00
matmoul 5f2d18df99 fix(calc-02): constrain keypad area with internal scrolling
Wrap the keypad, function, and trig panels in a dedicated scroll area so the calculator shell can use a fixed grid layout without overflowing the viewport. Also update panel styling to preserve spacing and scrollbar behavior.
2026-05-16 22:40:23 +02:00
matmoul c69380d057 refactor(samples): reorder calculator keypad keys 2026-05-16 21:58:51 +02:00
matmoul c11a75d7f7 fix: bust calc sample stylesheet cache 2026-05-16 21:52:05 +02:00
matmoul 813fae1b61 refactor(calc-02): simplify calculator layout structure
Move the display and button panels into a single wrapper and rely on
viewport-height sizing with hidden page overflow to keep the calculator
contained. Also trim redundant panel-specific layout rules and styling.
2026-05-16 21:41:47 +02:00
matmoul 208db58fe7 fix: make calc-02 panels stretch and scroll within container 2026-05-16 20:24:43 +02:00
matmoul ea6401a006 fix: constrain calculator width on calc-02 sample 2026-05-16 20:17:19 +02:00
matmoul ea61ec17d5 fix: remove portrait class from calc-02 calculator shell 2026-05-16 19:44:09 +02:00
matmoul a4bbffe65b feat: replace calculator enter key label with symbol 2026-05-16 18:41:03 +02:00
6 changed files with 236 additions and 117 deletions
+2 -1
View File
@@ -1,7 +1,8 @@
# State # State
- Core engine: `src/rpn-calculator.js` - Core engine: `src/rpn-calculator.js`
- Reference demo: `samples/calc-02/` (portrait-first HP48GX layout, compact mode/constants popups; Const button comes before Mode in the display row) - Reference demo: `samples/calc-02/` (portrait-first HP48GX layout; calc-02 keyboard shortcuts are the reference; compact mode/constants popups; Const button comes before Mode in the display row)
- Important UI behavior: mode button shows the current angle mode; keyboard focus stays on the hidden input on desktop; clipboard paste is supported - Important UI behavior: mode button shows the current angle mode; keyboard focus stays on the hidden input on desktop; clipboard paste is supported
- Note: keep scrolling behavior in mind for the calc-02 demo when changing the stack/UI layout
- Public API: `push`, `pop`, `clear`, `swap`, `remove`, `edit`, `isValidIndex`, `input`, `command`, `getOperationsByCategory`, `getConstants`, `listConstants`, `setConstant`, `removeConstant`, `hasConstant` - Public API: `push`, `pop`, `clear`, `swap`, `remove`, `edit`, `isValidIndex`, `input`, `command`, `getOperationsByCategory`, `getConstants`, `listConstants`, `setConstant`, `removeConstant`, `hasConstant`
- Config: `maxSize`, `base`, `angleMode`, `enabledCommands` - Config: `maxSize`, `base`, `angleMode`, `enabledCommands`
- Commands: arithmetic, stack, trigonometry, constants `pi` and `e`; arithmetic includes `root`; constants can be added or removed dynamically through the core API - Commands: arithmetic, stack, trigonometry, constants `pi` and `e`; arithmetic includes `root`; constants can be added or removed dynamically through the core API
+5 -2
View File
@@ -275,12 +275,15 @@ The current demo supports:
- `Escape` - `Escape`
- `ArrowUp`, `ArrowDown`, `ArrowRight` - `ArrowUp`, `ArrowDown`, `ArrowRight`
- `+`, `-`, `*`, `/`, `%`, `^` - `+`, `-`, `*`, `/`, `%`, `^`
- `q`, `n`, `r`, `i`, `g`, `l`, `s`, `c`, `S`, `C` - `s`, `S`, `r`, `R`, `v`, `u`
- `x`, `y`, `z`, `t` - `l`, `L`, `n`, `N`, `e`, `E`
- `i`, `o`, `a`, `I`, `O`, `A`
The demo also implements stack selection and stack-item move mode in its UI layer using the public calculator methods. 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. It keeps the calculator screen focused and updates the visible stack window as the selection moves.
Note: `calc-02` keyboard shortcuts are the reference for this project.
## Exports ## Exports
`RpnCalculator` is exposed in both environments: `RpnCalculator` is exposed in both environments:
+61 -23
View File
@@ -32,6 +32,7 @@
html, body { html, body {
margin: 0; margin: 0;
min-height: 100%; min-height: 100%;
overflow-y: hidden;
} }
body { body {
@@ -51,25 +52,24 @@ body {
.calculator { .calculator {
width: 100%; width: 100%;
height: auto; max-width: 460px;
display: grid; height: 100vh;
max-height: 100vh;
padding: 8px; padding: 8px;
display: grid;
grid-template-rows: auto auto minmax(0, 1fr);
gap: 8px;
border-radius: 8px; border-radius: 8px;
background: var(--panel); background: var(--panel);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
grid-template-columns: 1fr; justify-content: stretch;
grid-template-rows: auto auto auto auto auto; align-content: start;
row-gap: 6px; overflow: hidden;
grid-template-areas:
"display"
"display-buttons"
"keypad"
"functions"
"trigo";
} }
.display-block, .display-block,
.display-panel, .display-panel,
.display-buttons-panel,
.keypad-panel, .keypad-panel,
.functions-panel, .functions-panel,
.trigo-panel { .trigo-panel {
@@ -79,15 +79,7 @@ body {
box-shadow: none; box-shadow: none;
} }
.display-buttons-panel {
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.06);
background: rgba(6, 10, 16, 0.16);
box-shadow: none;
}
.display-block { .display-block {
grid-area: display;
display: grid; display: grid;
gap: 0; gap: 0;
align-self: start; align-self: start;
@@ -145,11 +137,14 @@ body {
justify-self: end; justify-self: end;
font-size: 20px; font-size: 20px;
} }
.stack-value.is-editing {
letter-spacing: 0.02em;
}
.display-buttons-panel { .display-buttons-panel {
grid-area: display-buttons;
padding: 8px; padding: 8px;
align-self: start;
min-height: 0; min-height: 0;
background: rgba(6, 10, 16, 0.16);
} }
.display-buttons-grid { .display-buttons-grid {
@@ -231,10 +226,22 @@ body {
.functions-panel, .functions-panel,
.trigo-panel { .trigo-panel {
padding: 8px; padding: 8px;
align-self: start;
min-height: 0; min-height: 0;
} }
.calculator > .display-block,
.calculator > .display-buttons-panel,
.calculator > .keypad-scroll-area {
justify-self: stretch;
}
.keypad-scroll-area {
min-height: 0;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgba(46, 105, 135, 1);
}
.status-bar { .status-bar {
position: absolute; position: absolute;
top: 0; top: 0;
@@ -291,7 +298,6 @@ button {
border: 1px solid rgba(14, 18, 25, 0.85); border: 1px solid rgba(14, 18, 25, 0.85);
border-radius: 8px; border-radius: 8px;
padding: 8px 8px; padding: 8px 8px;
font: inherit; font: inherit;
font-weight: 700; font-weight: 700;
color: var(--btnText); color: var(--btnText);
@@ -326,6 +332,7 @@ button:active {
opacity: 0.7; opacity: 0.7;
} }
} }
.stack-copy-button { .stack-copy-button {
padding: 4px; padding: 4px;
min-width: 24px; min-width: 24px;
@@ -376,6 +383,37 @@ button:active {
outline-offset: 2px; outline-offset: 2px;
} }
.stack-value.is-editing {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 0;
}
.edit-text {
display: inline-block;
white-space: pre;
}
.edit-caret {
display: inline-block;
width: 1px;
height: 1em;
margin: 0 0.12ch;
background: currentColor;
animation: caret-blink 1s steps(1, end) infinite;
transform: translateY(0.02em);
}
@keyframes caret-blink {
0%, 49% {
opacity: 1;
}
50%, 100% {
opacity: 0.15;
}
}
.key-default { .key-default {
background: linear-gradient(180deg, var(--btnTop), var(--btnBottom)); background: linear-gradient(180deg, var(--btnTop), var(--btnBottom));
color: #eef2f7; color: #eef2f7;
+35 -33
View File
@@ -4,49 +4,51 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>HP48GX RPN Calculator</title> <title>HP48GX RPN Calculator</title>
<link rel="stylesheet" href="./index.css"> <link rel="stylesheet" href="./index.css?ver=1">
</head> </head>
<body> <body>
<main class="app-shell"> <main class="app-shell">
<section class="calculator calculator-portrait" aria-label="HP48GX style RPN calculator"> <section class="calculator" aria-label="HP48GX style RPN calculator">
<div class="display-block"> <div class="display-block">
<div class="display-panel"> <div class="display-panel">
<div class="status-bar" id="statusLine" aria-live="polite"></div> <div class="status-bar" id="statusLine" aria-live="polite"></div>
<div class="display-frame"> <div class="display-frame">
<div class="display-grid"> <div class="display-grid">
<div class="stack-cell"><span class="stack-label">T:</span><button type="button" class="stack-copy-button" data-copy-stack="T" aria-label="Copy T value"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M15.5 4h-7A2.5 2.5 0 0 0 6 6.5V8H5.5A1.5 1.5 0 0 0 4 9.5v8A2.5 2.5 0 0 0 6.5 20H13a1 1 0 0 0 1-1v-1.5H17.5A2.5 2.5 0 0 0 20 15V6.5A2.5 2.5 0 0 0 17.5 4h-2Zm.5 2h1.5a.5.5 0 0 1 .5.5V14a.5.5 0 0 1-.5.5H13V6.5A2.5 2.5 0 0 0 15.5 6Zm-1.5 12H6.5a.5.5 0 0 1-.5-.5v-8a.5.5 0 0 1 .5-.5h7.5V18Zm-1.5-8h-4V8.5A.5.5 0 0 1 9 8h4a.5.5 0 0 1 .5.5V10Z"/></svg></button><span id="stackT" class="stack-value"></span></div> <div class="stack-cell"><span class="stack-label">T:</span><button type="button" class="stack-copy-button" data-copy-stack="T" aria-label="Copy T value"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M15.5 4h-7A2.5 2.5 0 0 0 6 6.5V8H5.5A1.5 1.5 0 0 0 4 9.5v8A2.5 2.5 0 0 0 6.5 20H13a1 1 0 0 0 1-1v-1.5H17.5A2.5 2.5 0 0 0 20 15V6.5A2.5 2.5 0 0 0 17.5 4h-2Zm.5 2h1.5a.5.5 0 0 1 .5.5V14a.5.5 0 0 1-.5.5H13V6.5A2.5 2.5 0 0 0 15.5 6Zm-1.5 12H6.5a.5.5 0 0 1-.5-.5v-8a.5.5 0 0 1 .5-.5h7.5V18Zm-1.5-8h-4V8.5A.5.5 0 0 1 9 8h4a.5.5 0 0 1 .5.5V10Z"/></svg></button><span id="stackT" class="stack-value"></span></div>
<div class="stack-cell"><span class="stack-label">Z:</span><button type="button" class="stack-copy-button" data-copy-stack="Z" aria-label="Copy Z value"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M15.5 4h-7A2.5 2.5 0 0 0 6 6.5V8H5.5A1.5 1.5 0 0 0 4 9.5v8A2.5 2.5 0 0 0 6.5 20H13a1 1 0 0 0 1-1v-1.5H17.5A2.5 2.5 0 0 0 20 15V6.5A2.5 2.5 0 0 0 17.5 4h-2Zm.5 2h1.5a.5.5 0 0 1 .5.5V14a.5.5 0 0 1-.5.5H13V6.5A2.5 2.5 0 0 0 15.5 6Zm-1.5 12H6.5a.5.5 0 0 1-.5-.5v-8a.5.5 0 0 1 .5-.5h7.5V18Zm-1.5-8h-4V8.5A.5.5 0 0 1 9 8h4a.5.5 0 0 1 .5.5V10Z"/></svg></button><span id="stackZ" class="stack-value"></span></div> <div class="stack-cell"><span class="stack-label">Z:</span><button type="button" class="stack-copy-button" data-copy-stack="Z" aria-label="Copy Z value"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M15.5 4h-7A2.5 2.5 0 0 0 6 6.5V8H5.5A1.5 1.5 0 0 0 4 9.5v8A2.5 2.5 0 0 0 6.5 20H13a1 1 0 0 0 1-1v-1.5H17.5A2.5 2.5 0 0 0 20 15V6.5A2.5 2.5 0 0 0 17.5 4h-2Zm.5 2h1.5a.5.5 0 0 1 .5.5V14a.5.5 0 0 1-.5.5H13V6.5A2.5 2.5 0 0 0 15.5 6Zm-1.5 12H6.5a.5.5 0 0 1-.5-.5v-8a.5.5 0 0 1 .5-.5h7.5V18Zm-1.5-8h-4V8.5A.5.5 0 0 1 9 8h4a.5.5 0 0 1 .5.5V10Z"/></svg></button><span id="stackZ" class="stack-value"></span></div>
<div class="stack-cell"><span class="stack-label">Y:</span><button type="button" class="stack-copy-button" data-copy-stack="Y" aria-label="Copy Y value"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M15.5 4h-7A2.5 2.5 0 0 0 6 6.5V8H5.5A1.5 1.5 0 0 0 4 9.5v8A2.5 2.5 0 0 0 6.5 20H13a1 1 0 0 0 1-1v-1.5H17.5A2.5 2.5 0 0 0 20 15V6.5A2.5 2.5 0 0 0 17.5 4h-2Zm.5 2h1.5a.5.5 0 0 1 .5.5V14a.5.5 0 0 1-.5.5H13V6.5A2.5 2.5 0 0 0 15.5 6Zm-1.5 12H6.5a.5.5 0 0 1-.5-.5v-8a.5.5 0 0 1 .5-.5h7.5V18Zm-1.5-8h-4V8.5A.5.5 0 0 1 9 8h4a.5.5 0 0 1 .5.5V10Z"/></svg></button><span id="stackY" class="stack-value"></span></div> <div class="stack-cell"><span class="stack-label">Y:</span><button type="button" class="stack-copy-button" data-copy-stack="Y" aria-label="Copy Y value"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M15.5 4h-7A2.5 2.5 0 0 0 6 6.5V8H5.5A1.5 1.5 0 0 0 4 9.5v8A2.5 2.5 0 0 0 6.5 20H13a1 1 0 0 0 1-1v-1.5H17.5A2.5 2.5 0 0 0 20 15V6.5A2.5 2.5 0 0 0 17.5 4h-2Zm.5 2h1.5a.5.5 0 0 1 .5.5V14a.5.5 0 0 1-.5.5H13V6.5A2.5 2.5 0 0 0 15.5 6Zm-1.5 12H6.5a.5.5 0 0 1-.5-.5v-8a.5.5 0 0 1 .5-.5h7.5V18Zm-1.5-8h-4V8.5A.5.5 0 0 1 9 8h4a.5.5 0 0 1 .5.5V10Z"/></svg></button><span id="stackY" class="stack-value"></span></div>
<div class="stack-cell"><span class="stack-label">X:</span><button type="button" class="stack-copy-button" data-copy-stack="X" aria-label="Copy X value"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M15.5 4h-7A2.5 2.5 0 0 0 6 6.5V8H5.5A1.5 1.5 0 0 0 4 9.5v8A2.5 2.5 0 0 0 6.5 20H13a1 1 0 0 0 1-1v-1.5H17.5A2.5 2.5 0 0 0 20 15V6.5A2.5 2.5 0 0 0 17.5 4h-2Zm.5 2h1.5a.5.5 0 0 1 .5.5V14a.5.5 0 0 1-.5.5H13V6.5A2.5 2.5 0 0 0 15.5 6Zm-1.5 12H6.5a.5.5 0 0 1-.5-.5v-8a.5.5 0 0 1 .5-.5h7.5V18Zm-1.5-8h-4V8.5A.5.5 0 0 1 9 8h4a.5.5 0 0 1 .5.5V10Z"/></svg></button><span id="stackX" class="stack-value"></span></div> <div class="stack-cell"><span class="stack-label">X:</span><button type="button" class="stack-copy-button" data-copy-stack="X" aria-label="Copy X value"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M15.5 4h-7A2.5 2.5 0 0 0 6 6.5V8H5.5A1.5 1.5 0 0 0 4 9.5v8A2.5 2.5 0 0 0 6.5 20H13a1 1 0 0 0 1-1v-1.5H17.5A2.5 2.5 0 0 0 20 15V6.5A2.5 2.5 0 0 0 17.5 4h-2Zm.5 2h1.5a.5.5 0 0 1 .5.5V14a.5.5 0 0 1-.5.5H13V6.5A2.5 2.5 0 0 0 15.5 6Zm-1.5 12H6.5a.5.5 0 0 1-.5-.5v-8a.5.5 0 0 1 .5-.5h7.5V18Zm-1.5-8h-4V8.5A.5.5 0 0 1 9 8h4a.5.5 0 0 1 .5.5V10Z"/></svg></button><span id="stackX" class="stack-value"></span></div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="display-buttons-panel"> <div class="display-buttons-panel">
<div class="display-buttons-grid"> <div class="display-buttons-grid">
<button id="constButton" class="display-button">π</button> <button id="constButton" class="display-button">π</button>
<button id="pasteButton" class="display-button"><span class="display-button-symbol paste-symbol"></span></button> <button id="pasteButton" class="display-button"><span class="display-button-symbol paste-symbol"></span></button>
<button id="upButton" class="display-button"><span class="display-button-symbol arrow-symbol"></span></button> <button id="upButton" class="display-button"><span class="display-button-symbol arrow-symbol"></span></button>
<button id="modeButton" class="display-button">Mode</button> <button id="modeButton" class="display-button">Mode</button>
<div class="display-button-spacer"></div> <div class="display-button-spacer"></div>
<button id="leftButton" class="display-button display-button-offset"><span class="display-button-symbol arrow-symbol"></span></button> <button id="leftButton" class="display-button display-button-offset"><span class="display-button-symbol arrow-symbol"></span></button>
<button id="downButton" class="display-button"><span class="display-button-symbol arrow-symbol"></span></button> <button id="downButton" class="display-button"><span class="display-button-symbol arrow-symbol"></span></button>
<button id="rightButton" class="display-button"><span class="display-button-symbol arrow-symbol"></span></button> <button id="rightButton" class="display-button"><span class="display-button-symbol arrow-symbol"></span></button>
</div>
</div> </div>
</div>
<div class="keypad-panel">
<div class="keypad-grid" id="keypadGrid"></div>
</div>
<div class="functions-panel"> <div class="keypad-scroll-area">
<div class="functions-grid" id="functionsGrid"></div> <div class="keypad-panel">
</div> <div class="keypad-grid" id="keypadGrid"></div>
</div>
<div class="trigo-panel"> <div class="functions-panel">
<div class="trigo-grid" id="trigoGrid"></div> <div class="functions-grid" id="functionsGrid"></div>
</div> </div>
<div class="trigo-panel">
<div class="trigo-grid" id="trigoGrid"></div>
</div>
</div>
<input id="hiddenInput" class="hidden-input" type="text" autocomplete="off" aria-hidden="true" tabindex="-1"> <input id="hiddenInput" class="hidden-input" type="text" autocomplete="off" aria-hidden="true" tabindex="-1">
</section> </section>
+120 -57
View File
@@ -30,10 +30,10 @@ const calculatorEl = document.querySelector('.calculator');
const statusLine = document.getElementById('statusLine'); const statusLine = document.getElementById('statusLine');
const keypadKeys = [ const keypadKeys = [
{ label: 'ENTER', action: 'enter', className: 'key-enter' },
{ label: '⎋', action: 'escape', className: 'key-escape' },
{ label: 'C', action: 'clear', className: 'key-danger' }, { label: 'C', action: 'clear', className: 'key-danger' },
{ label: '⌫', action: 'backspace', className: 'key-danger' }, { label: '⌫', action: 'backspace', className: 'key-danger' },
{ label: '⎋', action: 'escape', className: 'key-escape' },
{ label: '⏎', action: 'enter', className: 'key-enter' },
{ label: '7', input: '7', className: 'key-default' }, { label: '7', input: '7', className: 'key-default' },
{ label: '8', input: '8', className: 'key-default' }, { label: '8', input: '8', className: 'key-default' },
{ label: '9', input: '9', className: 'key-default' }, { label: '9', input: '9', className: 'key-default' },
@@ -94,6 +94,7 @@ function focusInput() {
} }
let statusTimer = null; let statusTimer = null;
let editCursor = 0;
function setStatus(message, isError = false, timeoutMs = 1400) { function setStatus(message, isError = false, timeoutMs = 1400) {
if (!statusLine) return; if (!statusLine) return;
@@ -118,27 +119,17 @@ function clearStatus() {
statusLine.classList.remove('is-error'); statusLine.classList.remove('is-error');
} }
function normalizeStack() {
while (calc.stack.length > 4) {
calc.stack.shift();
}
}
function getStackLine(indexFromTop) { function getStackLine(indexFromTop) {
return indexFromTop >= 0 && indexFromTop < calc.stack.length ? calc.stack[indexFromTop] : ''; return indexFromTop >= 0 && indexFromTop < calc.stack.length ? calc.stack[indexFromTop] : '';
} }
function getStackDisplayValue(label) { function getStackDisplayValue(label) {
if (label === 'X') { if (label === 'X' && calc.isEditing) {
return calc.isEditing ? calc.inputValue : (calc.formatNumber(getStackLine(0)) || ''); return calc.inputValue;
} }
if (label === 'Y') { const indexMap = { X: 0, Y: 1, Z: 2, T: 3 };
return calc.isEditing ? (calc.formatNumber(getStackLine(0)) || '') : (calc.formatNumber(getStackLine(1)) || ''); const indexFromTop = calc.isEditing ? Math.max(0, indexMap[label] - 1) : indexMap[label];
} return calc.formatNumber(getStackLine(indexFromTop)) || '';
if (label === 'Z') {
return calc.isEditing ? (calc.formatNumber(getStackLine(1)) || '') : (calc.formatNumber(getStackLine(2)) || '');
}
return calc.isEditing ? (calc.formatNumber(getStackLine(2)) || '') : (calc.formatNumber(getStackLine(3)) || '');
} }
function updateCopyButtons() { function updateCopyButtons() {
@@ -152,19 +143,40 @@ function updateCopyButtons() {
} }
} }
function renderEditValue() {
const cursor = Math.max(0, Math.min(editCursor, calc.inputValue.length));
stackEls.X.innerHTML = `<span class="edit-text">${calc.inputValue.slice(0, cursor)}</span><span class="edit-caret" aria-hidden="true"></span><span class="edit-text">${calc.inputValue.slice(cursor)}</span>`;
}
function render() { function render() {
normalizeStack();
const isPortrait = window.matchMedia('(orientation: portrait)').matches || window.innerWidth <= 860; const isPortrait = window.matchMedia('(orientation: portrait)').matches || window.innerWidth <= 860;
calculatorEl?.classList.toggle('portrait', isPortrait); calculatorEl?.classList.toggle('portrait', isPortrait);
calculatorEl?.classList.toggle('landscape', !isPortrait); calculatorEl?.classList.toggle('landscape', !isPortrait);
stackEls.X.textContent = getStackDisplayValue('X'); stackEls.X.textContent = calc.isEditing ? '' : getStackDisplayValue('X');
if (calc.isEditing) {
renderEditValue();
}
stackEls.Y.textContent = getStackDisplayValue('Y'); stackEls.Y.textContent = getStackDisplayValue('Y');
stackEls.Z.textContent = getStackDisplayValue('Z'); stackEls.Z.textContent = getStackDisplayValue('Z');
stackEls.T.textContent = getStackDisplayValue('T'); stackEls.T.textContent = getStackDisplayValue('T');
stackEls.X.classList.toggle('is-editing', calc.isEditing);
stackEls.X.classList.toggle('is-caret-visible', calc.isEditing);
updateCopyButtons(); updateCopyButtons();
modeButton.textContent = calc.angleMode; modeButton.textContent = calc.angleMode;
} }
function stopEditing(clearValue = false) {
if (clearValue) {
calc.inputValue = '';
}
calc.isEditing = false;
editCursor = 0;
}
function moveEditCursor(delta) {
editCursor = Math.max(0, Math.min(calc.inputValue.length, editCursor + delta));
}
function pushEditingValueIfNeeded() { function pushEditingValueIfNeeded() {
if (!calc.isEditing) return; if (!calc.isEditing) return;
if (calc.inputValue !== '') { if (calc.inputValue !== '') {
@@ -172,20 +184,36 @@ function pushEditingValueIfNeeded() {
} }
calc.inputValue = ''; calc.inputValue = '';
calc.isEditing = false; calc.isEditing = false;
editCursor = 0;
}
function startEditingFromStackTop() {
if (!calc.isValidIndex(0)) return false;
const value = calc.stack[0];
calc.remove(0);
calc.isEditing = true;
calc.inputValue = calc.formatNumber(value);
editCursor = calc.inputValue.length;
return true;
} }
function inputToX(value) { function inputToX(value) {
if (!calc.isEditing) { if (!calc.isEditing) {
calc.isEditing = true; calc.isEditing = true;
calc.inputValue = ''; calc.inputValue = '';
editCursor = 0;
} }
if (value === 'Backspace') { if (value === 'Backspace') {
calc.inputValue = calc.inputValue.slice(0, -1); if (editCursor > 0) {
calc.inputValue = `${calc.inputValue.slice(0, editCursor - 1)}${calc.inputValue.slice(editCursor)}`;
editCursor -= 1;
}
} else { } else {
calc.inputValue += value; calc.inputValue = `${calc.inputValue.slice(0, editCursor)}${value}${calc.inputValue.slice(editCursor)}`;
editCursor += value.length;
} }
if (calc.inputValue === '') { if (calc.inputValue === '') {
calc.isEditing = false; stopEditing();
} }
} }
@@ -217,11 +245,9 @@ function execute(name) {
} }
} else if (name === 'clear') { } else if (name === 'clear') {
calc.clear(); calc.clear();
calc.inputValue = ''; stopEditing(true);
calc.isEditing = false;
} else if (name === 'escape') { } else if (name === 'escape') {
calc.inputValue = ''; stopEditing(true);
calc.isEditing = false;
} else if (name === 'backspace') { } else if (name === 'backspace') {
if (calc.isEditing) { if (calc.isEditing) {
inputToX('Backspace'); inputToX('Backspace');
@@ -235,28 +261,19 @@ function execute(name) {
} }
} else if (name === 'neg') { } else if (name === 'neg') {
if (calc.isEditing) { if (calc.isEditing) {
calc.inputValue = calc.inputValue.startsWith('-') ? calc.inputValue.slice(1) : `-${calc.inputValue}`; const hasSign = calc.inputValue.startsWith('-');
calc.inputValue = hasSign ? calc.inputValue.slice(1) : `-${calc.inputValue}`;
moveEditCursor(hasSign ? -1 : 1);
} else { } else {
calc.push(calc.pop() * -1); calc.command('neg');
} }
} else if (name === 'pow10') {
pushEditingValueIfNeeded();
const exponent = calc.stack[0];
calc.remove(0);
calc.push(10);
calc.push(exponent);
calc.command('pow');
} else if (name === 'exp') {
pushEditingValueIfNeeded();
const exponent = calc.stack[0];
calc.remove(0);
calc.push(Math.E);
calc.push(exponent);
calc.command('pow');
} else { } else {
pushEditingValueIfNeeded(); pushEditingValueIfNeeded();
calc.command(name); calc.command(name);
} }
if (!calc.isEditing) {
editCursor = 0;
}
render(); render();
} catch (error) { } catch (error) {
setStatus(error?.message || 'Operation error', true); setStatus(error?.message || 'Operation error', true);
@@ -319,16 +336,53 @@ function handleKeyboard(event) {
'/': 'div', '/': 'div',
'%': 'mod', '%': 'mod',
'^': 'pow', '^': 'pow',
s: 'sqr',
S: 'pow',
r: 'sqrt',
R: 'root',
v: 'recip',
u: 'pow10',
l: 'log',
L: 'log',
n: 'ln',
N: 'ln',
e: 'exp',
E: 'exp',
i: 'sin',
o: 'cos',
a: 'tan',
I: 'asin',
O: 'acos',
A: 'atan',
}; };
const arrowMap = { if (key === 'ArrowLeft') {
ArrowUp: upButton,
ArrowDown: downButton,
ArrowLeft: leftButton,
ArrowRight: rightButton,
};
if (arrowMap[key]) {
event.preventDefault(); event.preventDefault();
arrowMap[key].click(); if (calc.isEditing) {
moveEditCursor(-1);
render();
return;
}
leftButton.click();
return;
}
if (key === 'ArrowRight') {
event.preventDefault();
if (calc.isEditing) {
moveEditCursor(1);
render();
return;
}
rightButton.click();
return;
}
if (key === 'ArrowUp') {
event.preventDefault();
upButton.click();
return;
}
if (key === 'ArrowDown') {
event.preventDefault();
downButton.click();
return; return;
} }
if (map[key]) { if (map[key]) {
@@ -434,7 +488,7 @@ const constantLabels = {
e: 'e', e: 'e',
phi: 'φ', phi: 'φ',
g: 'g', g: 'g',
c: 'c', c: 'C',
}; };
const constantOrder = ['pi', 'e', 'phi', 'g', 'c']; const constantOrder = ['pi', 'e', 'phi', 'g', 'c'];
@@ -473,19 +527,28 @@ constButton.addEventListener('click', (event) => {
toggleConstMenu(); toggleConstMenu();
}); });
leftButton.addEventListener('click', () => {}); leftButton.addEventListener('click', () => {
if (calc.isEditing) {
moveEditCursor(-1);
render();
focusInput();
}
});
downButton.addEventListener('click', () => { downButton.addEventListener('click', () => {
if (!calc.isEditing && calc.isValidIndex(0)) { if (!calc.isEditing && startEditingFromStackTop()) {
const value = calc.stack[0];
calc.remove(0);
calc.isEditing = true;
calc.inputValue = calc.formatNumber(value);
render(); render();
focusInput(); focusInput();
} }
}); });
rightButton.addEventListener('click', () => { rightButton.addEventListener('click', () => {
if (calc.isEditing) {
moveEditCursor(1);
render();
focusInput();
return;
}
execute('swap'); execute('swap');
}); });
+13 -1
View File
@@ -49,6 +49,18 @@ class RpnCalculator {
aliases: ['^', 'y^x'], aliases: ['^', 'y^x'],
execute: (calc, a, b) => Math.pow(a, b), execute: (calc, a, b) => Math.pow(a, b),
}, },
pow10: {
category: 'Arithmetic',
argCount: 1,
aliases: ['10^x'],
execute: (calc, a) => Math.pow(10, a),
},
exp: {
category: 'Arithmetic',
argCount: 1,
aliases: ['e^x'],
execute: (calc, a) => Math.exp(a),
},
root: { root: {
category: 'Arithmetic', category: 'Arithmetic',
argCount: 2, argCount: 2,
@@ -195,7 +207,7 @@ class RpnCalculator {
static getOperationsByCategory() { static getOperationsByCategory() {
return { return {
Stack: ['dup', 'drop', 'swap', 'clear', 'enter'], Stack: ['dup', 'drop', 'swap', 'clear', 'enter'],
Arithmetic: ['add', 'sub', 'mul', 'div', 'mod', 'pow', 'root', 'sqr', 'neg', 'sqrt', 'recip', 'log', 'ln'], Arithmetic: ['add', 'sub', 'mul', 'div', 'mod', 'pow', 'pow10', 'exp', 'root', 'sqr', 'neg', 'sqrt', 'recip', 'log', 'ln'],
Trigonometry: ['sin', 'cos', 'tan', 'asin', 'acos', 'atan'], Trigonometry: ['sin', 'cos', 'tan', 'asin', 'acos', 'atan'],
}; };
} }