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 inputValueLabel = document.getElementById('inputValueLabel'); const editingLabel = document.getElementById('editingLabel'); const modeLabel = document.getElementById('modeLabel'); const angleMode = document.getElementById('angleMode'); const groups = { stack: ['enter', 'dup', 'drop', 'swap', 'clear'], arithmetic: ['add', 'sub', 'mul', 'div', 'mod', 'pow', 'sqr', 'neg', 'sqrt', 'recip', 'log', 'ln'], trig: ['sin', 'cos', 'tan', 'asin', 'acos', 'atan'], const: ['pi', 'e'], }; let stackCursor = null; let isMovingStackItem = false; let stackSnapshotBeforeMove = null; function labelFor(command) { return ({ add: '+', sub: '−', mul: '×', div: '÷', pow: 'y^x', recip: '1/x', sqr: 'x²' }[command] || command); } function addButtons(container, commands) { container.innerHTML = ''; commands.forEach((commandName) => { const button = document.createElement('button'); button.textContent = labelFor(commandName); button.addEventListener('click', () => execute(commandName)); container.appendChild(button); }); } function getStackValue(index) { return calc.isValidIndex(index) ? calc.stack[index] : undefined; } function getLineValue(line) { if (calc.isEditing) { if (line === 0) { return calc.inputValue; } return getStackValue(line - 1); } return getStackValue(line); } function hasStackSelection() { return stackCursor !== null && calc.isValidIndex(stackCursor); } function clearStackSelection() { stackCursor = null; isMovingStackItem = false; stackSnapshotBeforeMove = null; } 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; } else { calc.inputValue = ''; calc.isEditing = true; } 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 render() { const names = ['T', 'Z', 'Y', 'X']; const lines = []; for (let line = 3; line >= 0; line -= 1) { const value = getLineValue(line); const isSelected = stackCursor === line; const classes = ['stack-line']; if (isSelected) { classes.push(isMovingStackItem ? 'moving' : 'selected'); } lines.push(`
${names[3 - line]}
${value !== undefined && value !== '' ? calc.formatNumber(value) : ''}
`); } stackEl.innerHTML = lines.join(''); if (calc.isEditing) { displayEl.textContent = `ENTERING: ${calc.inputValue}`; } else if (isMovingStackItem && hasStackSelection()) { displayEl.textContent = `MOVING: ${['X', 'Y', 'Z', 'T'][stackCursor] || '?'}`; } else if (hasStackSelection()) { displayEl.textContent = `SELECTED: ${['X', 'Y', 'Z', 'T'][stackCursor] || '?'}`; } else { displayEl.textContent = 'READY'; } inputValueLabel.textContent = calc.inputValue || '∅'; editingLabel.textContent = String(calc.isEditing); modeLabel.textContent = calc.angleMode; angleMode.value = calc.angleMode; errorEl.textContent = ''; } function pushEditingValueIfNeeded() { if (!calc.isEditing) return; if (calc.inputValue !== '') { const value = calc.parseInputValue(calc.inputValue); calc.push(value); } calc.inputValue = ''; calc.isEditing = false; syncInputFromState(); } function execute(name) { try { if (name === 'enter') { if (calc.isEditing) { pushEditingValueIfNeeded(); } else if (calc.isValidIndex(0)) { calc.push(getStackValue(0)); } } else if (name === 'swap') { pushEditingValueIfNeeded(); clearStackSelection(); if (calc.isValidIndex(1)) calc.swap(0, 1); } else if (name === 'drop') { pushEditingValueIfNeeded(); if (hasStackSelection()) { calc.remove(stackCursor); stackCursor = calc.isValidIndex(stackCursor) ? stackCursor : (calc.isValidIndex(stackCursor - 1) ? stackCursor - 1 : null); } else if (calc.isValidIndex(0)) { calc.remove(0); } commitMoveMode(); } else if (name === 'clear') { calc.clear(); clearStackSelection(); } else { pushEditingValueIfNeeded(); clearStackSelection(); calc.command(name); } syncInputFromState(); render(); } catch (error) { errorEl.textContent = error.message; } } function isInputChar(key) { return /^[0-9a-fA-F.]$/.test(key); } function shouldIgnoreKeyboardEvent(event) { const target = event.target; if (!target) return false; const tagName = target.tagName; return ( tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT' || target.isContentEditable ); } function getKeyboardAction(event) { const numpadMap = { Numpad0: { type: 'input', value: '0' }, Numpad1: { type: 'input', value: '1' }, Numpad2: { type: 'input', value: '2' }, Numpad3: { type: 'input', value: '3' }, Numpad4: { type: 'input', value: '4' }, Numpad5: { type: 'input', value: '5' }, Numpad6: { type: 'input', value: '6' }, Numpad7: { type: 'input', value: '7' }, Numpad8: { type: 'input', value: '8' }, Numpad9: { type: 'input', value: '9' }, NumpadDecimal: { type: 'input', value: '.' }, NumpadAdd: { type: 'command', value: 'add' }, NumpadSubtract: { type: 'command', value: 'sub' }, NumpadMultiply: { type: 'command', value: 'mul' }, NumpadDivide: { type: 'command', value: 'div' }, NumpadEnter: { type: 'command', value: 'enter' }, }; if (numpadMap[event.code]) { return numpadMap[event.code]; } if (isInputChar(event.key)) { return { type: 'input', value: event.key }; } const keyMap = { Enter: { type: 'enterKey' }, Backspace: { type: 'stackOrEdit', value: 'drop' }, Delete: { type: 'command', value: 'clear' }, Escape: { type: 'escapeKey' }, ArrowUp: { type: 'stackArrow', value: 'up' }, ArrowDown: { type: 'stackArrow', value: 'down' }, ArrowRight: { type: 'command', value: 'swap' }, '+': { type: 'command', value: 'add' }, '-': { type: 'command', value: 'sub' }, '*': { type: 'command', value: 'mul' }, '/': { type: 'command', value: 'div' }, '%': { type: 'command', value: 'mod' }, '^': { type: 'command', value: 'pow' }, q: { type: 'command', value: 'sqr' }, n: { type: 'command', value: 'neg' }, r: { type: 'command', value: 'sqrt' }, i: { type: 'command', value: 'recip' }, g: { type: 'command', value: 'log' }, l: { type: 'command', value: 'ln' }, s: { type: 'command', value: 'sin' }, c: { type: 'command', value: 'cos' }, S: { type: 'command', value: 'asin' }, C: { type: 'command', value: 'acos' }, x: { type: 'stackSelect', value: 0 }, y: { type: 'stackSelect', value: 1 }, z: { type: 'stackSelect', value: 2 }, t: { type: 'stackSelect', value: 3 }, X: { type: 'stackSelect', value: 0 }, Y: { type: 'stackSelect', value: 1 }, Z: { type: 'stackSelect', value: 2 }, T: { type: 'stackSelect', value: 3 }, }; return keyMap[event.key] || null; } function focusScreen() { screen.focus(); } function syncInputFromState() { input.value = calc.inputValue; } function editXWithKey(key) { if (!calc.isEditing) { pushEditingValueIfNeeded(); calc.isEditing = true; calc.inputValue = ''; } if (key === 'Backspace') { calc.inputValue = calc.inputValue.slice(0, -1); } else { calc.inputValue += key; } if (calc.inputValue === '') { calc.isEditing = false; } syncInputFromState(); } function handleKeydown(event) { if (shouldIgnoreKeyboardEvent(event)) { return; } const action = getKeyboardAction(event); if (!action) { return; } try { if (action.type === 'escapeKey') { if (calc.isEditing) { event.preventDefault(); calc.inputValue = ''; calc.isEditing = false; syncInputFromState(); render(); return; } if (isMovingStackItem) { event.preventDefault(); cancelMoveMode(); clearStackSelection(); render(); return; } if (hasStackSelection()) { event.preventDefault(); clearStackSelection(); render(); } return; } event.preventDefault(); if (action.type === 'input') { clearStackSelection(); editXWithKey(action.value); render(); return; } if (action.type === 'stackSelect') { if (calc.isEditing || isMovingStackItem) { return; } stackCursor = calc.isValidIndex(action.value) ? action.value : null; render(); return; } if (action.type === 'stackArrow') { if (calc.isEditing) { render(); return; } if (isMovingStackItem) { moveStackItem(action.value); } else { moveStackSelection(action.value); } render(); return; } if (action.type === 'stackOrEdit') { if (calc.isEditing) { editXWithKey('Backspace'); render(); } else { execute(action.value); } return; } if (action.type === 'enterKey') { if (hasStackSelection()) { if (isMovingStackItem) { commitMoveMode(); } else { beginMoveMode(); } render(); return; } execute('enter'); return; } if (action.type === 'command') { execute(action.value); } } catch (error) { errorEl.textContent = error.message; } } window.addEventListener('keydown', handleKeydown); screen.addEventListener('click', focusScreen); window.addEventListener('load', focusScreen); angleMode.addEventListener('change', (event) => { calc.angleMode = event.target.value; render(); }); addButtons(document.getElementById('stackButtons'), groups.stack); addButtons(document.getElementById('arithButtons'), groups.arithmetic); addButtons(document.getElementById('trigButtons'), groups.trig); addButtons(document.getElementById('constButtons'), groups.const); render(); focusScreen();