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(`
${label}
${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: ${stackCursor}`; } else if (hasStackSelection()) { displayEl.textContent = `SELECTED: ${stackCursor}`; } else { displayEl.textContent = 'READY'; } modeLabel.textContent = calc.angleMode; modeMenuButton.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; editRestoreValue = null; 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 = ''; editRestoreValue = null; } if (key === 'Backspace') { calc.inputValue = calc.inputValue.slice(0, -1); } else { calc.inputValue += key; } if (calc.inputValue === '') { calc.isEditing = false; editRestoreValue = null; } syncInputFromState(); } function handleKeydown(event) { if (shouldIgnoreKeyboardEvent(event)) { return; } const action = getKeyboardAction(event); if (!action) { return; } try { if (action.type === 'escapeKey') { event.preventDefault(); handleEscapeAction(); 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', () => { closePopupMenus(); focusScreen(); }); window.addEventListener('load', focusScreen); document.addEventListener('click', (event) => { if (!event.target.closest('#modeMenuWrap') && !event.target.closest('#constsMenuWrap')) { closePopupMenus(); } }); modeMenuButton.addEventListener('click', (event) => { event.stopPropagation(); togglePopupMenu('mode'); }); constsMenuButton.addEventListener('click', (event) => { event.stopPropagation(); togglePopupMenu('consts'); }); angleMode.addEventListener('change', (event) => { calc.angleMode = event.target.value; closePopupMenus(); render(); }); renderKeyLayout(document.getElementById('functionsButtons'), keyLayouts.functions); renderKeyLayout(document.getElementById('numbersButtons'), keyLayouts.numbers); renderKeyLayout(document.getElementById('operatorsButtons'), keyLayouts.operators); renderKeyLayout(modeMenu, [[ { type: 'action', value: 'setModeDeg', label: 'Degrees', className: 'key-function' }, { type: 'action', value: 'setModeRad', label: 'Radians', className: 'key-function' }, { type: 'action', value: 'setModeGrad', label: 'Grads', className: 'key-function' }, ]]); renderKeyLayout(constsMenu, [topButtons.consts]); document.getElementById('deleteButton').appendChild(createButton(topButtons.del)); document.getElementById('backspaceButton').appendChild(createButton(topButtons.backspace)); document.getElementById('escapeButton').appendChild(createButton(topButtons.escape)); render(); focusScreen();