Files
geek-calc/ui.js
snowprint 54f427ea21
Some checks failed
Deploy to GitHub Pages / build-and-deploy (push) Has been cancelled
init geek calc
2025-10-04 10:53:41 +08:00

369 lines
14 KiB
JavaScript

// UI controller module
const UIController = (function() {
class UIController {
constructor(calculator, rpnCalculator, stateManager) {
this.calculator = calculator;
this.rpnCalculator = rpnCalculator;
this.stateManager = stateManager;
// Current mode: 'standard' or 'rpn'
this.mode = this.stateManager.getMode();
// Current expression for standard calculator
this.currentExpression = '0';
// Track if we're in the middle of an RPN operation
this.rpnInputBuffer = '';
}
// Initialize the UI
init() {
this.setupEventListeners();
this.updateDisplay();
this.updateHistoryDisplay();
this.updateModeDisplay();
this.setupKeyboardControls();
}
// Set up event listeners for calculator buttons
setupEventListeners() {
// Get all calculator buttons
const buttons = document.querySelectorAll('.btn');
buttons.forEach(button => {
button.addEventListener('click', (e) => {
this.handleButtonClick(e.target.dataset.value);
});
// Add ARIA roles for accessibility
button.setAttribute('role', 'button');
button.setAttribute('tabindex', '0');
// Add keyboard support for buttons
button.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.handleButtonClick(button.dataset.value);
}
});
});
// RPN mode toggle button
const rpnModeBtn = document.getElementById('rpn-mode-btn');
rpnModeBtn.addEventListener('click', () => {
this.toggleRPNMode();
});
// Add ARIA for RPN button
rpnModeBtn.setAttribute('role', 'switch');
rpnModeBtn.setAttribute('aria-checked', this.mode === 'rpn');
rpnModeBtn.setAttribute('aria-label', 'RPN Mode Toggle');
// Command palette input
const commandInput = document.getElementById('command-input');
commandInput.setAttribute('aria-label', 'Command palette input');
commandInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.handleCommand(commandInput.value);
commandInput.value = '';
}
});
// Set up focus management for the display
const displayElement = document.querySelector('.display');
displayElement.setAttribute('role', 'application');
displayElement.setAttribute('aria-label', 'Calculator display');
displayElement.setAttribute('tabindex', '0');
}
// Handle button click
handleButtonClick(value) {
if (this.mode === 'standard') {
this.handleStandardInput(value);
} else {
this.handleRPNInput(value);
}
this.updateDisplay();
this.updateHistoryDisplay();
}
// Handle standard calculator input
handleStandardInput(value) {
this.calculator.processInput(value);
this.currentExpression = this.calculator.getCurrentDisplay();
// Add to history if it's an equals operation
if (value === '=') {
const expression = this.currentExpression; // This would need to capture the actual expression
const result = this.calculator.getCurrentDisplay();
this.stateManager.addToHistory(expression, parseFloat(result));
}
}
// Handle RPN calculator input
handleRPNInput(value) {
if (value === 'ENTER' || value === 'E') {
// In our UI, we'll treat this as a way to push numbers to the RPN stack
if (this.rpnInputBuffer) {
this.rpnCalculator.push(this.rpnInputBuffer);
this.rpnInputBuffer = '';
}
// Process the operation if it's an operator
} else if (['+', '-', '*', '/'].includes(value)) {
// Process the operation
this.rpnCalculator.operate(value);
} else if (['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.'].includes(value)) {
// Build the number in the input buffer
if (this.rpnInputBuffer === '0' || this.rpnInputBuffer === 'Error') {
this.rpnInputBuffer = value;
} else {
this.rpnInputBuffer += value;
}
} else if (value === 'C' || value === 'CE') {
this.rpnCalculator.clear();
this.rpnInputBuffer = '';
} else {
// Another operator, push the current number first
if (this.rpnInputBuffer) {
this.rpnCalculator.push(this.rpnInputBuffer);
this.rpnInputBuffer = '';
}
// Process the operator
if (['+', '-', '*', '/'].includes(value)) {
this.rpnCalculator.operate(value);
}
}
}
// Toggle between standard and RPN mode
toggleRPNMode() {
this.mode = this.stateManager.toggleMode();
this.updateModeDisplay();
// Clear calculators when switching modes
this.calculator.clear();
this.rpnCalculator.clear();
this.rpnInputBuffer = '';
this.currentExpression = '0';
this.updateDisplay();
}
// Update the display to show current value
updateDisplay() {
const resultElement = document.getElementById('result');
const expressionElement = document.getElementById('expression');
if (this.mode === 'standard') {
resultElement.textContent = this.calculator.getCurrentDisplay();
// Note: We don't have an expression display in our implementation
expressionElement.textContent = '';
} else {
// For RPN, show the top of the stack or the input buffer
const stack = this.rpnCalculator.getStack();
if (stack.length > 0) {
// Show the top of the stack
resultElement.textContent = stack[stack.length - 1];
} else if (this.rpnInputBuffer) {
// Show the input buffer
resultElement.textContent = this.rpnInputBuffer;
} else {
// Default display
resultElement.textContent = '0';
}
expressionElement.textContent = `RPN: ${stack.length} items`;
}
// Update cursor visibility and text content
const cursorElement = document.getElementById('cursor');
if (this.mode === 'standard') {
cursorElement.style.display = 'inline-block';
} else {
cursorElement.style.display = 'none'; // Hide cursor in RPN mode
}
}
// Update history display
updateHistoryDisplay() {
const historyElement = document.getElementById('history');
const history = this.stateManager.getHistory();
// Clear previous history
historyElement.innerHTML = '';
// Add history items
history.slice(0, 10).forEach(item => { // Show only last 10 items
const historyItem = document.createElement('div');
historyItem.className = 'history-item';
historyItem.textContent = `${item.expression} = ${item.result}`;
historyItem.addEventListener('click', () => {
// On click, load this calculation back into the calculator
if (this.mode === 'standard') {
this.calculator.clear();
// For simplicity, just set the result back
// In a full implementation, you'd restore the expression
this.currentExpression = item.result.toString();
this.updateDisplay();
}
});
historyElement.appendChild(historyItem);
});
}
// Update mode display
updateModeDisplay() {
const modeBtn = document.getElementById('rpn-mode-btn');
modeBtn.textContent = `RPN: ${this.mode === 'rpn' ? 'ON' : 'OFF'}`;
modeBtn.className = this.mode === 'rpn' ? 'mode-btn active' : 'mode-btn';
}
// Set up keyboard controls
setupKeyboardControls() {
document.addEventListener('keydown', (e) => {
// Prevent default behavior for calculator keys to avoid conflicts
if (!e.ctrlKey && !e.metaKey) {
this.handleKeyboardInput(e);
}
});
}
// Handle keyboard input
handleKeyboardInput(event) {
// Handle command palette activation
if (event.key === '@') {
event.preventDefault();
this.toggleCommandPalette();
return;
}
// Handle help/shortcuts
if (event.key === '?') {
event.preventDefault();
alert('Geek Calculator Shortcuts:\\n' +
'0-9: Number input\\n' +
'Arithmetic: + - * /\\n' +
'= or Enter: Evaluate\\n' +
'Escape: Clear\\n' +
'R: Toggle RPN mode\\n' +
'@: Command palette\\n' +
'↑/↓: Navigate history\\n' +
'?: Show this help');
return;
}
// Handle clear
if (event.key === 'Escape' || event.key === 'c' || event.key === 'C') {
this.handleButtonClick('C');
return;
}
// Handle enter
if (event.key === 'Enter' || event.key === '=') {
this.handleButtonClick('=');
return;
}
// Handle backspace
if (event.key === 'Backspace') {
// In a real implementation, you'd handle backspace
// For now, just ignore
return;
}
// Handle numbers
if (['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'].includes(event.key)) {
this.handleButtonClick(event.key);
return;
}
// Handle decimal point
if (event.key === '.') {
this.handleButtonClick('.');
return;
}
// Handle operators
if (['+', '-', '*', '/'].includes(event.key)) {
this.handleButtonClick(event.key);
return;
}
// Toggle RPN mode
if (event.key === 'r' || event.key === 'R') {
this.toggleRPNMode();
return;
}
// History navigation
if (event.key === 'ArrowUp') {
// In a real implementation, you'd navigate history up
return;
}
if (event.key === 'ArrowDown') {
// In a real implementation, you'd navigate history down
return;
}
}
// Toggle command palette visibility
toggleCommandPalette() {
const commandPalette = document.getElementById('command-palette');
if (commandPalette.style.display === 'none') {
commandPalette.style.display = 'block';
document.getElementById('command-input').focus();
} else {
commandPalette.style.display = 'none';
}
}
// Handle command input
handleCommand(command) {
const commandPalette = document.getElementById('command-palette');
commandPalette.style.display = 'none';
// Process commands
command = command.trim().toLowerCase();
if (command === 'clear' || command === 'cls' || command === 'c') {
this.handleButtonClick('C');
} else if (command === 'history') {
// Show history - already displayed
document.getElementById('history').scrollIntoView();
} else if (command === 'theme' || command.startsWith('theme ')) {
// Handle theme commands
const newTheme = command.split(' ')[1];
if (newTheme) {
this.stateManager.updateSettings({ theme: newTheme });
}
} else if (command === 'help') {
alert('Available commands:\\n' +
'clear/cls/c - Clear calculator\\n' +
'history - Show calculation history\\n' +
'theme [dark|light] - Change theme\\n' +
'help - Show this help');
}
}
// Get current result for testing purposes
getCurrentResult() {
if (this.mode === 'standard') {
return this.calculator.getCurrentDisplay();
} else {
const stack = this.rpnCalculator.getStack();
if (stack.length > 0) {
return stack[stack.length - 1].toString();
} else if (this.rpnInputBuffer) {
return this.rpnInputBuffer;
} else {
return '0';
}
}
}
// Get history for testing purposes
getHistory() {
return this.stateManager.getHistory();
}
}
return { UIController };
})();