Compare commits

...

6 Commits

Author SHA1 Message Date
matmoul f8de4e1709 docs: update project memory for calc-02 UI and command notes 2026-05-16 04:10:19 +02:00
matmoul 003d4fde1b feat(calc-02): add shared popup menus for mode and constants
Refactor the calc-02 demo to use a single popup menu component for angle mode and constants, align the menus to their trigger buttons, and update the README/project notes to reflect the portrait-first demo layout and constant API.
2026-05-16 04:04:01 +02:00
matmoul e5f50aee0a fix: reorder calc-02 display buttons 2026-05-16 03:44:20 +02:00
matmoul 6a7a60a9bc fix: align calc menus with trigger width 2026-05-16 03:41:25 +02:00
matmoul a37ed59b40 fix: reorder calc-02 display buttons 2026-05-16 03:37:04 +02:00
matmoul c09fdc7e0f refactor(calc-02): simplify display and button styling
Remove layered shadows and pressed-state transforms from the calculator UI, and update the stack copy icon markup and active feedback to match the new flat design.
2026-05-16 03:21:49 +02:00
5 changed files with 121 additions and 122 deletions
+3 -4
View File
@@ -1,9 +1,8 @@
# State # State
- Core engine: `src/rpn-calculator.js` - Core engine: `src/rpn-calculator.js`
- Active demo: `samples/calc-02/` portrait-only HP48GX layout; display-adjacent button row stays in 4 columns and now shares the same base button styling as the function keys - Reference demo: `samples/calc-02/` (portrait-first HP48GX layout, compact mode/constants popups)
- Mode button shows the current angle mode only; selecting a mode uses a popup menu - Important UI behavior: mode button shows the current angle mode; keyboard focus stays on the hidden input on desktop; clipboard paste is supported
- 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 now includes `root` for y-th roots; constants can now be added or removed dynamically through the core API and the calc-02 constant menu reads from the engine - Commands: arithmetic, stack, trigonometry, constants `pi` and `e`; arithmetic includes `root`; constants can be added or removed dynamically through the core API
- 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; the display button row uses a 4-column grid with a spacer cell to preserve alignment; the portrait layout reference now lives in `samples/calc-02/visual-portrait.md`
- Exports: browser `window.RpnCalculator`, CommonJS `module.exports` - Exports: browser `window.RpnCalculator`, CommonJS `module.exports`
+14 -9
View File
@@ -16,15 +16,15 @@ The main class is `RpnCalculator`.
## Project structure ## Project structure
- `src/rpn-calculator.js`: calculator engine - `src/rpn-calculator.js`: calculator engine
- `samples/dev/index.html`: active browser demo entry point - `samples/dev/index.html`: browser demo entry point
- `samples/dev/index.css`: demo styles - `samples/dev/index.css`: demo styles
- `samples/dev/index.js`: demo UI and keyboard logic - `samples/dev/index.js`: demo UI and keyboard logic
- `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-02/index.html`: new responsive HP48GX-style demo entry point - `samples/calc-02/index.html`: portrait-first HP48GX-style demo entry point
- `samples/calc-02/index.css`: new responsive demo styles - `samples/calc-02/index.css`: portrait-first demo styles
- `samples/calc-02/index.js`: responsive HP48GX-style demo UI and keyboard logic - `samples/calc-02/index.js`: portrait-first HP48GX-style 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
@@ -45,6 +45,10 @@ Instance helpers also available:
- `getOperationsByCategory()` - `getOperationsByCategory()`
- `getConstants()` - `getConstants()`
- `listConstants()`
- `setConstant(name, value)`
- `removeConstant(name)`
- `hasConstant(name)`
Static helpers also available: Static helpers also available:
@@ -84,6 +88,7 @@ Available constants:
- `pi` - `pi`
- `e` - `e`
- plus any user-defined constants added through the engine API
They can be used through `command(...)`: They can be used through `command(...)`:
@@ -246,6 +251,11 @@ Main UI features:
- grouped panels for `Stack`, `Arithmetic`, `Trigonometry`, and `Constants` - grouped panels for `Stack`, `Arithmetic`, `Trigonometry`, and `Constants`
- error display area - error display area
## Calc 02 demo
`samples/calc-02/` is a portrait-first HP48GX-inspired demo.
It keeps the display-adjacent button row aligned in four columns, uses compact popup menus for mode and constants, and supports clipboard paste plus the `y√x` root operation.
The demo loads the engine from: The demo loads the engine from:
```html README.md ```html README.md
@@ -270,11 +280,6 @@ 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 responsive HP48GX-inspired demo.
It adapts its layout to the browser window, supports portrait and landscape arrangements, and includes the `y√x` root operation.
## Exports ## Exports
`RpnCalculator` is exposed in both environments: `RpnCalculator` is exposed in both environments:
+48 -42
View File
@@ -1,5 +1,4 @@
:root { :root {
--bg0: #10151e; --bg0: #10151e;
--bg1: #1b2432; --bg1: #1b2432;
@@ -42,7 +41,6 @@ body {
background: var(--bg0); background: var(--bg0);
} }
.app-shell { .app-shell {
min-height: 100vh; min-height: 100vh;
display: grid; display: grid;
@@ -58,7 +56,7 @@ body {
padding: 8px; padding: 8px;
border-radius: 8px; border-radius: 8px;
background: var(--panel); background: var(--panel);
box-shadow: 0 26px 70px var(--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; grid-template-columns: 1fr;
grid-template-rows: auto auto auto auto auto; grid-template-rows: auto auto auto auto auto;
row-gap: 6px; row-gap: 6px;
@@ -78,14 +76,14 @@ body {
border-radius: 8px; border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.06);
background: rgba(6, 10, 16, 0.16); background: rgba(6, 10, 16, 0.16);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05); box-shadow: none;
} }
.display-buttons-panel { .display-buttons-panel {
border-radius: 8px; border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.06);
background: rgba(6, 10, 16, 0.16); background: rgba(6, 10, 16, 0.16);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05); box-shadow: none;
} }
.display-block { .display-block {
@@ -113,9 +111,6 @@ body {
max-height: 138px; max-height: 138px;
margin-bottom: 0; margin-bottom: 0;
} }
.display-grid { .display-grid {
height: 100%; height: 100%;
display: grid; display: grid;
@@ -125,7 +120,6 @@ body {
gap: 2px; gap: 2px;
} }
.stack-cell { .stack-cell {
display: grid; display: grid;
grid-template-columns: 2.2ch auto minmax(0, 1fr); grid-template-columns: 2.2ch auto minmax(0, 1fr);
@@ -137,7 +131,6 @@ body {
padding-block: 0; padding-block: 0;
} }
.stack-label { .stack-label {
text-align: right; text-align: right;
opacity: 0.78; opacity: 0.78;
@@ -152,8 +145,6 @@ body {
justify-self: end; justify-self: end;
font-size: 20px; font-size: 20px;
} }
.display-buttons-panel { .display-buttons-panel {
grid-area: display-buttons; grid-area: display-buttons;
padding: 8px; padding: 8px;
@@ -180,10 +171,9 @@ body {
.display-button { .display-button {
background: linear-gradient(180deg, var(--btnTop), var(--btnBottom)); background: linear-gradient(180deg, var(--btnTop), var(--btnBottom));
color: #eef2f7; color: #eef2f7;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 3px 0 rgba(0, 0, 0, 0.28); box-shadow: none;
} }
.display-button-symbol { .display-button-symbol {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -203,26 +193,33 @@ body {
padding: 6px 8px; padding: 6px 8px;
} }
.menu-popup {
.mode-menu {
position: fixed; position: fixed;
z-index: 20; z-index: 20;
display: grid; display: flex;
gap: 6px; flex-direction: column;
padding: 10px; gap: 4px;
border-radius: 8px; padding: 6px;
background: rgba(6, 10, 16, 0.16); border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.08); background: rgba(16, 21, 30, 0.96);
box-shadow: 0 14px 30px rgba(0, 0, 0, 0.35); border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.32);
backdrop-filter: blur(2px);
} }
.mode-menu-item { .menu-popup-item {
min-width: 120px; width: 100%;
min-width: 0;
padding: 6px 10px;
border-radius: 5px;
background: linear-gradient(180deg, var(--btnAltTop), var(--btnAltBottom)); background: linear-gradient(180deg, var(--btnAltTop), var(--btnAltBottom));
text-align: left;
font-weight: 700;
} }
.mode-menu-item.is-active { .menu-popup-item.is-active {
outline: 2px solid rgba(207, 224, 174, 0.7); outline: 1px solid rgba(207, 224, 174, 0.7);
outline-offset: 0;
} }
.key-escape { .key-escape {
@@ -287,10 +284,6 @@ body {
.functions-grid, .functions-grid,
.trigo-grid { .trigo-grid {
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
}
.functions-grid,
.trigo-grid {
grid-template-rows: repeat(2, minmax(0, 1fr)); grid-template-rows: repeat(2, minmax(0, 1fr));
} }
@@ -304,8 +297,8 @@ button {
color: var(--btnText); color: var(--btnText);
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.35); text-shadow: 0 1px 0 rgba(0, 0, 0, 0.35);
cursor: pointer; cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 3px 0 rgba(0, 0, 0, 0.28); box-shadow: none;
transition: transform 120ms ease, filter 120ms ease, box-shadow 120ms ease, opacity 120ms ease; transition: filter 120ms ease, opacity 120ms ease;
line-height: 1; line-height: 1;
} }
@@ -313,16 +306,28 @@ button:hover {
filter: brightness(1.06); filter: brightness(1.06);
} }
button:active { button:active {
transform: translateY(2px); transform: none;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 1px 0 rgba(0, 0, 0, 0.25); box-shadow: none;
} }
.stack-copy-button:active {
animation: stack-copy-flash 140ms ease-out;
}
@keyframes stack-copy-flash {
0% {
opacity: 0.7;
}
50% {
opacity: 1;
}
100% {
opacity: 0.7;
}
}
.stack-copy-button { .stack-copy-button {
padding: 4px; padding: 4px;
border-radius: 999px;
min-width: 24px; min-width: 24px;
width: 24px; width: 24px;
height: 24px; height: 24px;
@@ -330,8 +335,9 @@ button:active {
place-items: center; place-items: center;
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
transform: scale(0.9); transform: scale(1);
background: transparent; background: transparent;
border: none;
box-shadow: none; box-shadow: none;
color: rgba(31, 42, 18, 0.58); color: rgba(31, 42, 18, 0.58);
margin-left: 0; margin-left: 0;
@@ -342,9 +348,9 @@ button:active {
} }
.stack-copy-button svg { .stack-copy-button svg {
width: 13px; width: 15px;
height: 13px; height: 15px;
fill: currentColor; fill: #0a0a0a;
display: block; display: block;
} }
@@ -361,7 +367,7 @@ button:active {
} }
.stack-copy-button:active { .stack-copy-button:active {
transform: translateY(2px) scale(1); transform: scale(1);
color: rgba(31, 42, 18, 0.95); color: rgba(31, 42, 18, 0.95);
} }
+6 -6
View File
@@ -14,10 +14,10 @@
<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="M9 9V5.5A1.5 1.5 0 0 1 10.5 4h8A1.5 1.5 0 0 1 20 5.5v8A1.5 1.5 0 0 1 18.5 15H15v3.5A1.5 1.5 0 0 1 13.5 20h-8A1.5 1.5 0 0 1 4 18.5v-8A1.5 1.5 0 0 1 5.5 9H9Zm1.5-3a.5.5 0 0 0-.5.5V9h5.5a1.5 1.5 0 0 1 1.5 1.5V16h.5a.5.5 0 0 0 .5-.5v-8a.5.5 0 0 0-.5-.5h-8Z"/></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="M9 9V5.5A1.5 1.5 0 0 1 10.5 4h8A1.5 1.5 0 0 1 20 5.5v8A1.5 1.5 0 0 1 18.5 15H15v3.5A1.5 1.5 0 0 1 13.5 20h-8A1.5 1.5 0 0 1 4 18.5v-8A1.5 1.5 0 0 1 5.5 9H9Zm1.5-3a.5.5 0 0 0-.5.5V9h5.5a1.5 1.5 0 0 1 1.5 1.5V16h.5a.5.5 0 0 0 .5-.5v-8a.5.5 0 0 0-.5-.5h-8Z"/></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="M9 9V5.5A1.5 1.5 0 0 1 10.5 4h8A1.5 1.5 0 0 1 20 5.5v8A1.5 1.5 0 0 1 18.5 15H15v3.5A1.5 1.5 0 0 1 13.5 20h-8A1.5 1.5 0 0 1 4 18.5v-8A1.5 1.5 0 0 1 5.5 9H9Zm1.5-3a.5.5 0 0 0-.5.5V9h5.5a1.5 1.5 0 0 1 1.5 1.5V16h.5a.5.5 0 0 0 .5-.5v-8a.5.5 0 0 0-.5-.5h-8Z"/></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="M9 9V5.5A1.5 1.5 0 0 1 10.5 4h8A1.5 1.5 0 0 1 20 5.5v8A1.5 1.5 0 0 1 18.5 15H15v3.5A1.5 1.5 0 0 1 13.5 20h-8A1.5 1.5 0 0 1 4 18.5v-8A1.5 1.5 0 0 1 5.5 9H9Zm1.5-3a.5.5 0 0 0-.5.5V9h5.5a1.5 1.5 0 0 1 1.5 1.5V16h.5a.5.5 0 0 0 .5-.5v-8a.5.5 0 0 0-.5-.5h-8Z"/></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>
@@ -26,10 +26,10 @@
<div class="display-buttons-panel"> <div class="display-buttons-panel">
<div class="display-buttons-grid"> <div class="display-buttons-grid">
<button id="modeButton" class="display-button">Mode</button> <button id="modeButton" class="display-button">Mode</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="constButton" class="display-button">π</button> <button id="constButton" class="display-button">π</button>
<button id="upButton" class="display-button"><span class="display-button-symbol arrow-symbol"></span></button>
<div class="display-button-spacer"></div> <div class="display-button-spacer"></div>
<button id="pasteButton" class="display-button"><span class="display-button-symbol paste-symbol"></span></button>
<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>
+50 -61
View File
@@ -338,41 +338,49 @@ function handleKeyboard(event) {
} }
const modeOptions = ['deg', 'rad', 'grad']; const modeOptions = ['deg', 'rad', 'grad'];
let modeMenuEl = null; let activeMenuEl = null;
function closeModeMenu() { function closeModeMenu() {
if (modeMenuEl) { if (activeMenuEl) {
modeMenuEl.remove(); activeMenuEl.remove();
modeMenuEl = null; activeMenuEl = null;
} }
} }
function openMenu(anchorButton, items, onSelect) {
const rect = anchorButton.getBoundingClientRect();
const menu = document.createElement('div');
menu.className = 'menu-popup';
menu.style.top = `${rect.bottom + 6 + window.scrollY}px`;
menu.style.left = `${rect.left + window.scrollX}px`;
menu.style.minWidth = `${rect.width}px`;
for (const item of items) {
const button = document.createElement('button');
button.type = 'button';
button.className = `menu-popup-item${item.active ? ' is-active' : ''}`;
button.textContent = item.label;
button.addEventListener('click', () => onSelect(item.value));
menu.appendChild(button);
}
document.body.appendChild(menu);
return menu;
}
function toggleModeMenu() { function toggleModeMenu() {
if (modeMenuEl) { if (activeMenuEl) {
closeModeMenu(); closeModeMenu();
return; return;
} }
closeConstMenu(); closeConstMenu();
const rect = modeButton.getBoundingClientRect(); activeMenuEl = openMenu(modeButton, modeOptions.map((mode) => ({
modeMenuEl = document.createElement('div'); label: mode,
modeMenuEl.className = 'mode-menu'; value: mode,
modeMenuEl.style.top = `${rect.bottom + 6 + window.scrollY}px`; active: mode === calc.angleMode,
modeOptions.forEach((mode) => { })), (mode) => {
const button = document.createElement('button'); calc.angleMode = mode;
button.type = 'button'; render();
button.className = `mode-menu-item${mode === calc.angleMode ? ' is-active' : ''}`; closeModeMenu();
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) => { modeButton.addEventListener('click', (event) => {
@@ -397,10 +405,8 @@ window.addEventListener('click', (event) => {
if (label) copyStackValue(label); if (label) copyStackValue(label);
return; return;
} }
if (modeMenuEl && !event.target.closest('.mode-menu') && event.target !== modeButton) { if (activeMenuEl && !event.target.closest('.menu-popup') && event.target !== modeButton && event.target !== constButton) {
closeModeMenu(); closeModeMenu();
}
if (constMenuEl && !event.target.closest('.mode-menu') && event.target !== constButton) {
closeConstMenu(); closeConstMenu();
} }
}); });
@@ -432,49 +438,34 @@ const constantLabels = {
}; };
const constantOrder = ['pi', 'e', 'phi', 'g', 'c']; const constantOrder = ['pi', 'e', 'phi', 'g', 'c'];
let constMenuEl = null;
function closeConstMenu() { function closeConstMenu() {
if (constMenuEl) { if (activeMenuEl) {
constMenuEl.remove(); activeMenuEl.remove();
constMenuEl = null; activeMenuEl = null;
} }
} }
function toggleConstMenu() { function toggleConstMenu() {
if (constMenuEl) { if (activeMenuEl) {
closeConstMenu(); closeConstMenu();
return; return;
} }
closeModeMenu(); closeModeMenu();
const rect = constButton.getBoundingClientRect();
constMenuEl = document.createElement('div');
constMenuEl.className = 'mode-menu';
constMenuEl.style.top = `${rect.bottom + 6 + window.scrollY}px`;
const availableConstants = calc.listConstants(); const availableConstants = calc.listConstants();
const keys = [...constantOrder, ...Object.keys(availableConstants).filter((name) => !constantOrder.includes(name))]; const keys = [...constantOrder, ...Object.keys(availableConstants).filter((name) => !constantOrder.includes(name))]
keys.forEach((name) => { .filter((name) => Object.prototype.hasOwnProperty.call(availableConstants, name));
const button = document.createElement('button'); activeMenuEl = openMenu(constButton, keys.map((name) => ({
button.type = 'button'; label: constantLabels[name] ?? name,
button.className = 'mode-menu-item'; value: name,
if (!Object.prototype.hasOwnProperty.call(availableConstants, name)) { })), (name) => {
return; pushEditingValueIfNeeded();
} calc.push(availableConstants[name]);
button.textContent = constantLabels[name] ?? name; render();
button.addEventListener('click', () => { clearStatus();
pushEditingValueIfNeeded(); closeConstMenu();
calc.push(availableConstants[name]); focusInput();
render();
clearStatus();
closeConstMenu();
focusInput();
});
constMenuEl.appendChild(button);
}); });
document.body.appendChild(constMenuEl);
const menuRect = constMenuEl.getBoundingClientRect();
const desiredLeft = rect.right + window.scrollX - menuRect.width;
constMenuEl.style.left = `${Math.max(8, desiredLeft)}px`;
} }
constButton.addEventListener('click', (event) => { constButton.addEventListener('click', (event) => {
@@ -483,7 +474,6 @@ constButton.addEventListener('click', (event) => {
}); });
leftButton.addEventListener('click', () => {}); leftButton.addEventListener('click', () => {});
downButton.addEventListener('click', () => { downButton.addEventListener('click', () => {
if (!calc.isEditing && calc.isValidIndex(0)) { if (!calc.isEditing && calc.isValidIndex(0)) {
const value = calc.stack[0]; const value = calc.stack[0];
@@ -505,7 +495,6 @@ window.addEventListener('pageshow', focusInput);
window.addEventListener('focus', focusInput); window.addEventListener('focus', focusInput);
window.addEventListener('pointerdown', focusInput, true); window.addEventListener('pointerdown', focusInput, true);
window.addEventListener('mousedown', focusInput, true); window.addEventListener('mousedown', focusInput, true);
window.addEventListener('click', focusInput, true);
hiddenInput.setAttribute('inputmode', 'none'); hiddenInput.setAttribute('inputmode', 'none');
hiddenInput.setAttribute('readonly', 'readonly'); hiddenInput.setAttribute('readonly', 'readonly');
@@ -520,7 +509,7 @@ hiddenInput.addEventListener('focus', () => {
}); });
document.addEventListener('click', (event) => { document.addEventListener('click', (event) => {
if (!isTouchDevice && !event.target.closest('.mode-menu')) { if (!isTouchDevice && !event.target.closest('.menu-popup')) {
focusInput(); focusInput();
} }
}); });