const calc = new RpnCalculator({ angleMode: 'deg' }); const input = document.getElementById('input'); const screen = document.getElementById('screen'); const stackEl = document.getElementById('stack'); const displayEl = document.getElementById('display'); const errorEl = document.getElementById('error'); const modeLabel = document.getElementById('modeLabel'); const angleMode = document.getElementById('angleMode'); const modeMenuButton = document.getElementById('modeMenuButton'); const modeMenu = document.getElementById('modeMenu'); const constsMenuButton = document.getElementById('constsMenuButton'); const constsMenu = document.getElementById('constsMenu'); const keyLayouts = { functions: [ [ { type: 'command', value: 'sqrt', label: 'sqrt', className: 'key-function' }, { type: 'command', value: 'pow', label: 'y^x', className: 'key-function' }, { type: 'command', value: 'sqr', label: 'x²', className: 'key-function' }, { type: 'command', value: 'recip', label: '1/x', className: 'key-function' }, ], [ { type: 'command', value: 'log', label: 'log', className: 'key-function' }, { type: 'command', value: 'ln', label: 'ln', className: 'key-function' }, null, { type: 'command', value: 'mod', label: '%', className: 'key-function' }, ], [ { type: 'command', value: 'sin', label: 'sin', className: 'key-function' }, { type: 'command', value: 'cos', label: 'cos', className: 'key-function' }, { type: 'command', value: 'tan', label: 'tan', className: 'key-function' }, null, ], [ { type: 'command', value: 'asin', label: 'asin', className: 'key-function' }, { type: 'command', value: 'acos', label: 'acos', className: 'key-function' }, { type: 'command', value: 'atan', label: 'atan', className: 'key-function' }, null, ], ], numbers: [ [ { type: 'input', value: '7', label: '7', className: 'key-number' }, { type: 'input', value: '8', label: '8', className: 'key-number' }, { type: 'input', value: '9', label: '9', className: 'key-number' }, ], [ { type: 'input', value: '4', label: '4', className: 'key-number' }, { type: 'input', value: '5', label: '5', className: 'key-number' }, { type: 'input', value: '6', label: '6', className: 'key-number' }, ], [ { type: 'input', value: '1', label: '1', className: 'key-number' }, { type: 'input', value: '2', label: '2', className: 'key-number' }, { type: 'input', value: '3', label: '3', className: 'key-number' }, ], [ { type: 'input', value: '0', label: '0', className: 'key-number' }, { type: 'input', value: '.', label: '.', className: 'key-number' }, { type: 'command', value: 'neg', label: '+/−', className: 'key-number' }, ], ], operators: [ [ { type: 'command', value: 'div', label: '/', className: 'key-operator' }, null, ], [ { type: 'command', value: 'mul', label: '*', className: 'key-operator' }, null, ], [ { type: 'command', value: 'sub', label: '-', className: 'key-operator' }, { type: 'command', value: 'enter', label: 'Enter', className: 'key-enter' }, ], [ { type: 'command', value: 'add', label: '+', className: 'key-operator' }, null, ], ], }; const topButtons = { consts: [ { type: 'command', value: 'pi', label: 'π', className: 'key-function' }, { type: 'command', value: 'e', label: 'e', className: 'key-function' }, ], del: { type: 'command', value: 'clear', label: 'del', className: 'key-danger' }, backspace: { type: 'action', value: 'backspace', label: 'backspace', className: 'key-operator' }, escape: { type: 'action', value: 'escape', label: 'esc', className: 'key-danger' }, }; let stackCursor = null; let isMovingStackItem = false; let stackSnapshotBeforeMove = null; let stackViewOffset = 0; let editRestoreValue = null; function handleEscapeAction() { if (calc.isEditing) { if (editRestoreValue !== null) { calc.push(editRestoreValue); editRestoreValue = null; } calc.inputValue = ''; calc.isEditing = false; syncInputFromState(); render(); return; } if (isMovingStackItem) { cancelMoveMode(); clearStackSelection(); render(); return; } if (hasStackSelection()) { clearStackSelection(); render(); } } function pressKey(key) { clearStackSelection(); editXWithKey(key); render(); } function handleBackspaceAction() { if (calc.isEditing) { editXWithKey('Backspace'); render(); return; } execute('drop'); } function closePopupMenus() { modeMenu.hidden = true; constsMenu.hidden = true; modeMenuButton.setAttribute('aria-expanded', 'false'); constsMenuButton.setAttribute('aria-expanded', 'false'); } function togglePopupMenu(menuName) { const isModeMenu = menuName === 'mode'; const targetMenu = isModeMenu ? modeMenu : constsMenu; const targetButton = isModeMenu ? modeMenuButton : constsMenuButton; const otherMenu = isModeMenu ? constsMenu : modeMenu; const otherButton = isModeMenu ? constsMenuButton : modeMenuButton; const willOpen = targetMenu.hidden; otherMenu.hidden = true; otherButton.setAttribute('aria-expanded', 'false'); targetMenu.hidden = !willOpen; targetButton.setAttribute('aria-expanded', String(willOpen)); } function createButton(cell) { if (!cell) { const spacer = document.createElement('div'); spacer.className = 'key-spacer'; spacer.setAttribute('aria-hidden', 'true'); return spacer; } const button = document.createElement('button'); button.type = 'button'; button.textContent = cell.label; button.className = cell.className; button.addEventListener('click', () => { focusScreen(); closePopupMenus(); if (cell.type === 'input') { pressKey(cell.value); return; } if (cell.type === 'action') { if (cell.value === 'escape') { handleEscapeAction(); return; } if (cell.value === 'backspace') { handleBackspaceAction(); return; } if (cell.value === 'setModeDeg' || cell.value === 'setModeRad' || cell.value === 'setModeGrad') { angleMode.value = cell.value === 'setModeDeg' ? 'deg' : (cell.value === 'setModeRad' ? 'rad' : 'grad'); angleMode.dispatchEvent(new Event('change')); return; } } execute(cell.value); }); return button; } function renderKeyLayout(container, rows) { container.innerHTML = ''; rows.flat().forEach((cell) => { container.appendChild(createButton(cell)); }); } function getStackValue(index) { return calc.isValidIndex(index) ? calc.stack[index] : undefined; } function getDisplayValue(index) { if (calc.isEditing) { if (index === 0) { return calc.inputValue; } return getStackValue(index - 1); } return getStackValue(index); } function hasStackSelection() { return stackCursor !== null && calc.isValidIndex(stackCursor); } function clearStackSelection() { stackCursor = null; isMovingStackItem = false; stackSnapshotBeforeMove = null; stackViewOffset = 0; } function ensureValidSelection() { if (hasStackSelection()) { return; } stackCursor = calc.isValidIndex(0) ? 0 : null; } function beginMoveMode() { if (!hasStackSelection()) { return; } isMovingStackItem = true; stackSnapshotBeforeMove = calc.stack.slice(); } function commitMoveMode() { isMovingStackItem = false; stackSnapshotBeforeMove = null; } function cancelMoveMode() { if (!isMovingStackItem || !stackSnapshotBeforeMove) { return; } const snapshot = stackSnapshotBeforeMove.slice(); calc.clear(); for (let index = snapshot.length - 1; index >= 0; index -= 1) { calc.push(snapshot[index]); } isMovingStackItem = false; stackSnapshotBeforeMove = null; stackCursor = calc.isValidIndex(stackCursor) ? stackCursor : (calc.isValidIndex(0) ? 0 : null); syncInputFromState(); } function reactivateEditOnX() { clearStackSelection(); if (calc.isValidIndex(0)) { const value = getStackValue(0); calc.remove(0); calc.inputValue = calc.formatNumber(value); calc.isEditing = true; editRestoreValue = value; } else { calc.inputValue = ''; calc.isEditing = true; editRestoreValue = null; } syncInputFromState(); } function moveStackSelection(direction) { if (!hasStackSelection()) { if (direction === 'up') { ensureValidSelection(); } else if (direction === 'down') { reactivateEditOnX(); } return; } const nextIndex = direction === 'up' ? stackCursor + 1 : stackCursor - 1; if (calc.isValidIndex(nextIndex)) { stackCursor = nextIndex; } } function moveStackItem(direction) { if (!hasStackSelection()) { return; } const targetIndex = direction === 'up' ? stackCursor + 1 : stackCursor - 1; if (!calc.isValidIndex(targetIndex)) { return; } calc.swap(stackCursor, targetIndex); stackCursor = targetIndex; } function getVisibleStackIndex(visualLine) { return stackViewOffset + visualLine; } function clampStackViewOffset() { const maxOffset = Math.max(0, calc.stack.length - 4); if (stackViewOffset < 0) { stackViewOffset = 0; } else if (stackViewOffset > maxOffset) { stackViewOffset = maxOffset; } } function ensureSelectionVisible() { if (!hasStackSelection()) { stackViewOffset = 0; return; } if (stackCursor < stackViewOffset) { stackViewOffset = stackCursor; } else if (stackCursor > stackViewOffset + 3) { stackViewOffset = stackCursor - 3; } clampStackViewOffset(); } function render() { const names = ['T', 'Z', 'Y', 'X']; const lines = []; const showStackIndexes = hasStackSelection() || isMovingStackItem; clampStackViewOffset(); ensureSelectionVisible(); for (let visualLine = 3; visualLine >= 0; visualLine -= 1) { const stackIndex = getVisibleStackIndex(visualLine); const value = getDisplayValue(stackIndex); const isSelected = stackCursor === stackIndex; const classes = ['stack-line']; const label = showStackIndexes ? String(stackIndex) : names[3 - visualLine]; if (isSelected) { classes.push(isMovingStackItem ? 'moving' : 'selected'); } lines.push(`