Compare commits

..

12 Commits

Author SHA1 Message Date
matmoul 39659745a6 feat(calc-02): reorder keypad actions for safer input flow 2026-05-15 21:18:47 +02:00
matmoul 75fe72412e fix(samples): keep hidden input focused for keyboard input 2026-05-15 21:03:16 +02:00
matmoul ef0e0c8dd2 feat: show calculator status messages as overlay bar 2026-05-15 20:56:05 +02:00
matmoul 6444357444 fix: ignore backspace when the stack is empty 2026-05-15 20:47:19 +02:00
matmoul d1a1d44577 feat: support pasting numbers into the calculator stack
Add clipboard paste handling for the hidden input and the paste button so pasted text is parsed as a numeric value before being pushed. Also add the eˣ function key in the sample calculator and keep the hidden input selected on focus for Ctrl+V support.
2026-05-15 20:45:30 +02:00
matmoul 02b3b280f8 feat: enable editing from the down button in calc sample 2026-05-15 20:24:21 +02:00
matmoul 40e1043a03 feat: add swap action and HP48-style stack editing 2026-05-15 20:19:39 +02:00
matmoul f679b0d952 feat: add constants popup to calculator sample 2026-05-15 19:12:36 +02:00
matmoul 2505a102df feat(calc-02): replace display button labels with symbols 2026-05-15 19:09:24 +02:00
matmoul 9bca077347 fix: change default serve port to 3000 2026-05-15 19:00:50 +02:00
matmoul d88722030a Merge branch 'dev' into calc-02 2026-05-15 18:56:57 +02:00
matmoul 48a262eb87 feat: add responsive calc-02 HP48GX demo 2026-05-15 18:45:44 +02:00
8 changed files with 955 additions and 5 deletions
+3 -1
View File
@@ -1,7 +1,9 @@
# State # State
- Core engine: `src/rpn-calculator.js` - Core engine: `src/rpn-calculator.js`
- Active demo: `samples/dev/` (HP48-style UI) - Active demo: `samples/calc-02/` responsive HP48GX layout with HP48-like X-line editing; 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` - Public API: `push`, `pop`, `clear`, `swap`, `remove`, `edit`, `isValidIndex`, `input`, `command`, `getOperationsByCategory`, `getConstants`
- Config: `maxSize`, `base`, `angleMode`, `enabledCommands` - Config: `maxSize`, `base`, `angleMode`, `enabledCommands`
- Commands: arithmetic, stack, trigonometry, constants `pi` and `e` - Commands: arithmetic, stack, trigonometry, constants `pi` and `e`
- Demo actions: keyboard focus is kept on the hidden input on desktop so typing keeps working; the keypad layout places Enter in the bottom-left, ± in the former Enter position, and Esc before Clear for safety; paste parses clipboard text as a number before pushing it to the stack; Ctrl+V is supported via the hidden input paste event; backspace is ignored when the stack is empty; operation errors are shown as an overlay bar on top of the calculator with a shorter timeout and darker red
- Exports: browser `window.RpnCalculator`, CommonJS `module.exports` - Exports: browser `window.RpnCalculator`, CommonJS `module.exports`
+8 -3
View File
@@ -22,9 +22,9 @@ The main class is `RpnCalculator`.
- `samples/calc-01/index.html`: active browser demo entry point - `samples/calc-01/index.html`: active browser demo entry point
- `samples/calc-01/index.css`: demo styles - `samples/calc-01/index.css`: demo styles
- `samples/calc-01/index.js`: demo UI and keyboard logic - `samples/calc-01/index.js`: demo UI and keyboard logic
- `samples/calc-01/index.html`: alternate browser demo entry point - `samples/calc-02/index.html`: new responsive HP48GX-style demo entry point
- `samples/calc-01/index.css`: alternate demo styles - `samples/calc-02/index.css`: new responsive demo styles
- `samples/calc-01/index.js`: alternate demo UI and keyboard logic - `samples/calc-02/index.js`: new demo UI and keyboard logic
- `samples/calc-XX/`: placeholder name for future demo variants - `samples/calc-XX/`: placeholder name for future demo variants
## Public API ## 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. 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.
## 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 ## Exports
`RpnCalculator` is exposed in both environments: `RpnCalculator` is exposed in both environments:
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
PORT="${1:-8000}" PORT="${1:-3000}"
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT_DIR" cd "$ROOT_DIR"
+406
View File
@@ -0,0 +1,406 @@
: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;
--btnEscapeTop: #6a4a2a;
--btnEscapeBottom: #4a331d;
--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;
position: relative;
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);
}
.key-escape {
background: linear-gradient(180deg, var(--btnEscapeTop), var(--btnEscapeBottom));
color: #eef2f7;
}
.display-button:nth-child(6) {
background: linear-gradient(180deg, #343b46, #20262e);
}
.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-bar {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 2;
padding: 8px 12px;
min-height: 30px;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.02em;
color: rgba(31, 42, 18, 0.96);
background: rgba(255, 246, 170, 0.92);
border-bottom: 1px solid rgba(31, 42, 18, 0.2);
transform: translateY(-100%);
opacity: 0;
transition: transform 180ms ease, opacity 180ms ease;
pointer-events: none;
}
.status-bar.is-visible {
transform: translateY(0);
opacity: 1;
}
.status-bar.is-error {
color: #fff;
background: rgba(72, 14, 14, 0.98);
border-bottom-color: rgba(255, 255, 255, 0.12);
}
.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;
line-height: 1;
}
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;
line-height: 1;
}
.display-buttons-panel {
gap: 6px;
}
}
+53
View File
@@ -0,0 +1,53 @@
<!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="status-bar" id="statusLine" aria-live="polite"></div>
<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"></button>
<button id="upButton" class="display-button"></button>
<button id="constButton" class="display-button">π</button>
<button id="leftButton" class="display-button display-button-offset"></button>
<button id="downButton" class="display-button"></button>
<button id="rightButton" class="display-button"></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>
+448
View File
@@ -0,0 +1,448 @@
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 leftButton = document.getElementById('leftButton');
const downButton = document.getElementById('downButton');
const rightButton = document.getElementById('rightButton');
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 statusLine = document.getElementById('statusLine');
const keypadKeys = [
{ label: 'Enter', action: 'enter', className: 'key-enter' },
{ label: '⎋', action: 'escape', className: 'key-escape' },
{ label: 'C', action: 'clear', 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: '±', action: 'neg', className: 'key-default' },
{ 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: 'eˣ', action: 'exp', className: 'key-default' },
{ 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 },
];
const isTouchDevice = window.matchMedia('(pointer: coarse)').matches || 'ontouchstart' in window;
function focusInput() {
if (!hiddenInput || isTouchDevice) return;
hiddenInput.focus({ preventScroll: true });
window.requestAnimationFrame(() => {
if (document.activeElement !== hiddenInput) {
hiddenInput.focus({ preventScroll: true });
}
if (typeof hiddenInput.select === 'function') {
hiddenInput.select();
}
});
}
let statusTimer = null;
function setStatus(message, isError = false, timeoutMs = 1400) {
if (!statusLine) return;
clearTimeout(statusTimer);
statusLine.textContent = message;
statusLine.classList.toggle('is-error', isError);
statusLine.classList.toggle('is-visible', Boolean(message));
if (!message || timeoutMs <= 0) return;
statusTimer = window.setTimeout(() => {
statusLine.textContent = '';
statusLine.classList.remove('is-visible');
statusLine.classList.remove('is-error');
}, timeoutMs);
}
function normalizeStack() {
while (calc.stack.length > 4) {
calc.stack.shift();
}
}
function getStackLine(indexFromTop) {
return indexFromTop >= 0 && indexFromTop < calc.stack.length ? calc.stack[indexFromTop] : '';
}
function render() {
normalizeStack();
const isPortrait = window.matchMedia('(orientation: portrait)').matches || window.innerWidth <= 860;
calculatorEl?.classList.toggle('portrait', isPortrait);
calculatorEl?.classList.toggle('landscape', !isPortrait);
const editingValue = calc.isEditing ? calc.inputValue : '';
stackEls.X.textContent = calc.isEditing ? editingValue : (calc.formatNumber(getStackLine(0)) || '');
stackEls.Y.textContent = calc.isEditing ? (calc.formatNumber(getStackLine(0)) || '') : (calc.formatNumber(getStackLine(1)) || '');
stackEls.Z.textContent = calc.isEditing ? (calc.formatNumber(getStackLine(1)) || '') : (calc.formatNumber(getStackLine(2)) || '');
stackEls.T.textContent = calc.isEditing ? (calc.formatNumber(getStackLine(2)) || '') : (calc.formatNumber(getStackLine(3)) || '');
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 pasteTextIntoStack(text) {
if (!text) {
setStatus('Clipboard empty');
return;
}
const value = calc.parseInputValue(text);
if (!Number.isFinite(value)) {
setStatus('Clipboard is not a number');
return;
}
pushEditingValueIfNeeded();
calc.push(value);
calc.isEditing = false;
calc.inputValue = '';
setStatus('Pasted');
render();
}
function execute(name) {
try {
if (name === 'enter') {
if (calc.isEditing) {
pushEditingValueIfNeeded();
} else if (calc.isValidIndex(0)) {
calc.push(calc.stack[0]);
}
} 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 if (calc.isValidIndex(0)) {
calc.remove(0);
}
} else if (name === 'swap') {
pushEditingValueIfNeeded();
if (calc.isValidIndex(1)) {
calc.swap(0, 1);
}
} 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 if (name === 'exp') {
pushEditingValueIfNeeded();
calc.push(Math.E);
calc.command('pow');
} else {
pushEditingValueIfNeeded();
calc.command(name);
}
render();
} catch (error) {
setStatus(error?.message || 'Operation error', true);
}
}
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', () => {
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.defaultPrevented) 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();
closeConstMenu();
});
window.addEventListener('scroll', () => {
closeModeMenu();
closeConstMenu();
}, true);
window.addEventListener('click', (event) => {
if (modeMenuEl && !event.target.closest('.mode-menu') && event.target !== modeButton) {
closeModeMenu();
}
if (constMenuEl && !event.target.closest('.mode-menu') && event.target !== constButton) {
closeConstMenu();
}
});
pasteButton.addEventListener('click', async () => {
try {
const text = await navigator.clipboard.readText();
pasteTextIntoStack(text);
} catch (error) {
setStatus('Paste unavailable', true);
}
});
hiddenInput.addEventListener('paste', (event) => {
event.preventDefault();
const text = event.clipboardData?.getData('text') ?? '';
pasteTextIntoStack(text);
});
upButton.addEventListener('click', () => {});
const constants = [
{ label: 'π', value: Math.PI },
{ label: 'e', value: Math.E },
{ label: 'φ', value: (1 + Math.sqrt(5)) / 2 },
{ label: 'g', value: 9.80665 },
{ label: 'c', value: 299792458 },
];
let constMenuEl = null;
function closeConstMenu() {
if (constMenuEl) {
constMenuEl.remove();
constMenuEl = null;
}
}
function openConstMenu() {
closeConstMenu();
const rect = constButton.getBoundingClientRect();
constMenuEl = document.createElement('div');
constMenuEl.className = 'mode-menu';
constMenuEl.style.top = `${rect.bottom + 6 + window.scrollY}px`;
constMenuEl.style.left = `${rect.left + window.scrollX}px`;
constants.forEach((constant) => {
const button = document.createElement('button');
button.type = 'button';
button.className = 'mode-menu-item';
button.textContent = constant.label;
button.addEventListener('click', () => {
pushEditingValueIfNeeded();
calc.push(constant.value);
render();
setStatus(`Inserted ${constant.label}`);
closeConstMenu();
focusInput();
});
constMenuEl.appendChild(button);
});
document.body.appendChild(constMenuEl);
const menuRect = constMenuEl.getBoundingClientRect();
const maxLeft = Math.max(8, window.innerWidth - menuRect.width - 8);
constMenuEl.style.left = `${Math.max(8, Math.min(maxLeft, rect.left + window.scrollX))}px`;
}
constButton.addEventListener('click', (event) => {
event.stopPropagation();
openConstMenu();
});
leftButton.addEventListener('click', () => {});
downButton.addEventListener('click', () => {
if (!calc.isEditing && calc.isValidIndex(0)) {
const value = calc.stack[0];
calc.remove(0);
calc.isEditing = true;
calc.inputValue = calc.formatNumber(value);
render();
focusInput();
}
});
rightButton.addEventListener('click', () => {
execute('swap');
});
window.addEventListener('keydown', handleKeyboard, { capture: true });
window.addEventListener('load', focusInput);
window.addEventListener('pageshow', focusInput);
window.addEventListener('focus', focusInput);
window.addEventListener('pointerdown', focusInput, true);
window.addEventListener('mousedown', focusInput, true);
window.addEventListener('click', focusInput, true);
hiddenInput.setAttribute('inputmode', 'none');
hiddenInput.setAttribute('readonly', 'readonly');
hiddenInput.addEventListener('focus', () => {
if (isTouchDevice) {
hiddenInput.blur();
return;
}
window.requestAnimationFrame(() => {
hiddenInput.select();
});
});
document.addEventListener('click', (event) => {
if (!isTouchDevice && !event.target.closest('.mode-menu')) {
focusInput();
}
});
buildGrid(keypadGrid, keypadKeys);
buildGrid(functionsGrid, functionKeys);
buildGrid(trigoGrid, trigoKeys);
render();
focusInput();
+10
View File
@@ -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 | └──────────────────────┘
└─────────────────────────────────┘
+26
View File
@@ -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 | |
└─────────────────────────────────┘