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 stackCopyButtons = { T: document.querySelector('[data-copy-stack="T"]'), Z: document.querySelector('[data-copy-stack="Z"]'), Y: document.querySelector('[data-copy-stack="Y"]'), X: document.querySelector('[data-copy-stack="X"]'), }; 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: 'C', action: 'clear', className: 'key-danger' }, { label: '⌫', action: 'backspace', className: 'key-danger' }, { label: '⎋', action: 'escape', className: 'key-escape' }, { label: '⏎', action: 'enter', className: 'key-enter' }, { 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', title: 's' }, { label: '√x', action: 'sqrt', className: 'key-default', title: 'r' }, { label: '1/x', action: 'recip', className: 'key-default', title: 'x' }, { label: '%', action: 'mod', className: 'key-default' }, { label: 'yˣ', action: 'pow', className: 'key-default', title: 'S' }, { label: 'y√x', action: 'root', className: 'key-default', title: 'R' }, { label: '10ˣ', action: 'pow10', className: 'key-default', title: 'd' }, { label: '', spacer: true }, { label: 'log', action: 'log', className: 'key-default', title: 'l / L' }, { label: 'ln', action: 'ln', className: 'key-default', title: 'n / N' }, { label: 'eˣ', action: 'exp', className: 'key-default', title: 'e / E' }, { label: '', spacer: true }, ]; const trigoKeys = [ { label: 'sin', action: 'sin', className: 'key-default', title: 'i' }, { label: 'cos', action: 'cos', className: 'key-default', title: 'o' }, { label: 'tan', action: 'tan', className: 'key-default', title: 'a' }, { label: '', spacer: true }, { label: 'asin', action: 'asin', className: 'key-default', title: 'I' }, { label: 'acos', action: 'acos', className: 'key-default', title: 'O' }, { label: 'atan', action: 'atan', className: 'key-default', title: 'A' }, { 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; let editCursor = 0; let editRestoreValue = 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 clearStatus() { clearTimeout(statusTimer); statusTimer = null; if (!statusLine) return; statusLine.textContent = ''; statusLine.classList.remove('is-visible'); statusLine.classList.remove('is-error'); } function getStackLine(indexFromTop) { return indexFromTop >= 0 && indexFromTop < calc.stack.length ? calc.stack[indexFromTop] : ''; } function getStackDisplayValue(label) { if (label === 'X' && calc.isEditing) { return calc.inputValue; } const indexMap = { X: 0, Y: 1, Z: 2, T: 3 }; const indexFromTop = calc.isEditing ? Math.max(0, indexMap[label] - 1) : indexMap[label]; return calc.formatNumber(getStackLine(indexFromTop)) || ''; } function updateCopyButtons() { for (const label of ['T', 'Z', 'Y', 'X']) { const value = getStackDisplayValue(label); const button = stackCopyButtons[label]; if (!button) continue; button.classList.toggle('is-visible', Boolean(value)); button.disabled = !value; button.setAttribute('aria-hidden', value ? 'false' : 'true'); } } function renderEditValue() { const cursor = Math.max(0, Math.min(editCursor, calc.inputValue.length)); stackEls.X.innerHTML = `${calc.inputValue.slice(0, cursor)}${calc.inputValue.slice(cursor)}`; } function render() { const isPortrait = window.matchMedia('(orientation: portrait)').matches || window.innerWidth <= 860; calculatorEl?.classList.toggle('portrait', isPortrait); calculatorEl?.classList.toggle('landscape', !isPortrait); stackEls.X.textContent = calc.isEditing ? '' : getStackDisplayValue('X'); if (calc.isEditing) { renderEditValue(); } stackEls.Y.textContent = getStackDisplayValue('Y'); stackEls.Z.textContent = getStackDisplayValue('Z'); stackEls.T.textContent = getStackDisplayValue('T'); stackEls.X.classList.toggle('is-editing', calc.isEditing); stackEls.X.classList.toggle('is-caret-visible', calc.isEditing); updateCopyButtons(); modeButton.textContent = calc.angleMode; } function stopEditing(clearValue = false) { if (clearValue) { calc.inputValue = ''; } calc.isEditing = false; editCursor = 0; } function cancelEditing() { if (editRestoreValue !== null) { calc.push(editRestoreValue); } editRestoreValue = null; stopEditing(true); } function moveEditCursor(delta) { editCursor = Math.max(0, Math.min(calc.inputValue.length, editCursor + delta)); } function pushEditingValueIfNeeded() { if (!calc.isEditing) return; if (calc.inputValue !== '') { calc.push(calc.parseInputValue(calc.inputValue)); } editRestoreValue = null; calc.inputValue = ''; calc.isEditing = false; editCursor = 0; } function startEditingFromStackTop() { if (!calc.isValidIndex(0)) return false; const value = calc.stack[0]; editRestoreValue = value; calc.remove(0); calc.isEditing = true; calc.inputValue = calc.formatNumber(value); editCursor = calc.inputValue.length; return true; } function inputToX(value) { if (!calc.isEditing) { calc.isEditing = true; calc.inputValue = ''; editCursor = 0; } if (value === 'Backspace') { if (editCursor > 0) { calc.inputValue = `${calc.inputValue.slice(0, editCursor - 1)}${calc.inputValue.slice(editCursor)}`; editCursor -= 1; } } else { calc.inputValue = `${calc.inputValue.slice(0, editCursor)}${value}${calc.inputValue.slice(editCursor)}`; editCursor += value.length; } if (calc.inputValue === '') { stopEditing(); } } 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(); stopEditing(true); } else if (name === 'escape') { cancelEditing(); } 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) { const hasSign = calc.inputValue.startsWith('-'); calc.inputValue = hasSign ? calc.inputValue.slice(1) : `-${calc.inputValue}`; moveEditCursor(hasSign ? -1 : 1); } else { calc.command('neg'); } } else { pushEditingValueIfNeeded(); calc.command(name); } if (!calc.isEditing) { editCursor = 0; } render(); } catch (error) { setStatus(error?.message || 'Operation error', true); } } function createKeyButton({ label, input, action, spacer, className, title }) { if (spacer) { const div = document.createElement('div'); return div; } const button = document.createElement('button'); button.type = 'button'; button.textContent = label; button.className = className; if (title) button.title = title; 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))); } async function copyStackValue(label) { const value = getStackDisplayValue(label); if (!value) return; try { await navigator.clipboard.writeText(value); clearStatus(); } catch (error) { setStatus('Copy unavailable', true); } } 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', s: 'sqr', S: 'pow', r: 'sqrt', R: 'root', x: 'recip', d: 'pow10', l: 'log', L: 'log', n: 'ln', N: 'ln', e: 'exp', E: 'exp', i: 'sin', o: 'cos', a: 'tan', I: 'asin', O: 'acos', A: 'atan', }; if (key === 'ArrowLeft') { event.preventDefault(); if (calc.isEditing) { moveEditCursor(-1); render(); return; } leftButton.click(); return; } if (key === 'ArrowRight') { event.preventDefault(); if (calc.isEditing) { moveEditCursor(1); render(); return; } rightButton.click(); return; } if (key === 'ArrowUp') { event.preventDefault(); upButton.click(); return; } if (key === 'ArrowDown') { event.preventDefault(); if (calc.isEditing) { cancelEditing(); render(); return; } downButton.click(); return; } if (map[key]) { event.preventDefault(); execute(map[key]); } } const modeOptions = ['deg', 'rad', 'grad']; let activeMenuEl = null; function closeModeMenu() { if (activeMenuEl) { activeMenuEl.remove(); 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() { if (activeMenuEl) { closeModeMenu(); return; } closeConstMenu(); activeMenuEl = openMenu(modeButton, modeOptions.map((mode) => ({ label: mode, value: mode, active: mode === calc.angleMode, })), (mode) => { calc.angleMode = mode; render(); closeModeMenu(); }); } modeButton.addEventListener('click', (event) => { event.stopPropagation(); toggleModeMenu(); }); window.addEventListener('resize', () => { closeModeMenu(); closeConstMenu(); render(); }); window.addEventListener('scroll', () => { closeModeMenu(); closeConstMenu(); }, true); window.addEventListener('click', (event) => { const stackCopyButton = event.target.closest('.stack-copy-button'); if (stackCopyButton) { const label = stackCopyButton.dataset.copyStack; if (label) copyStackValue(label); return; } if (activeMenuEl && !event.target.closest('.menu-popup') && event.target !== modeButton && event.target !== constButton) { closeModeMenu(); closeConstMenu(); } }); pasteButton.addEventListener('click', async () => { try { const text = await navigator.clipboard.readText(); pasteTextIntoStack(text); clearStatus(); } catch (error) { setStatus('Paste unavailable', true); } }); hiddenInput.addEventListener('paste', (event) => { event.preventDefault(); const text = event.clipboardData?.getData('text') ?? ''; pasteTextIntoStack(text); }); upButton.addEventListener('click', () => {}); const constantLabels = { pi: 'π', e: 'e', phi: 'φ', g: 'g', c: 'C', }; const constantOrder = ['pi', 'e', 'phi', 'g', 'c']; function closeConstMenu() { if (activeMenuEl) { activeMenuEl.remove(); activeMenuEl = null; } } function toggleConstMenu() { if (activeMenuEl) { closeConstMenu(); return; } closeModeMenu(); const availableConstants = calc.listConstants(); const keys = [...constantOrder, ...Object.keys(availableConstants).filter((name) => !constantOrder.includes(name))] .filter((name) => Object.prototype.hasOwnProperty.call(availableConstants, name)); activeMenuEl = openMenu(constButton, keys.map((name) => ({ label: constantLabels[name] ?? name, value: name, })), (name) => { pushEditingValueIfNeeded(); calc.push(availableConstants[name]); render(); clearStatus(); closeConstMenu(); focusInput(); }); } constButton.addEventListener('click', (event) => { event.stopPropagation(); toggleConstMenu(); }); leftButton.addEventListener('click', () => { if (calc.isEditing) { moveEditCursor(-1); render(); focusInput(); } }); downButton.addEventListener('click', () => { if (!calc.isEditing && startEditingFromStackTop()) { render(); focusInput(); } }); rightButton.addEventListener('click', () => { if (calc.isEditing) { moveEditCursor(1); render(); focusInput(); return; } 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); 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('.menu-popup')) { focusInput(); } }); buildGrid(keypadGrid, keypadKeys); buildGrid(functionsGrid, functionKeys); buildGrid(trigoGrid, trigoKeys); render(); focusInput();