Files
mtm-rpn-js/samples/dev/index.js
T

449 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = [];
const showStackIndexes = hasStackSelection() || isMovingStackItem;
for (let line = 3; line >= 0; line -= 1) {
const value = getLineValue(line);
const isSelected = stackCursor === line;
const classes = ['stack-line'];
const label = showStackIndexes ? String(line) : names[3 - line];
if (isSelected) {
classes.push(isMovingStackItem ? 'moving' : 'selected');
}
lines.push(`<div class="${classes.join(' ')}"><div class="label">${label}</div><div>${value !== undefined && value !== '' ? calc.formatNumber(value) : ''}</div></div>`);
}
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';
}
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();