feat: add responsive calc-02 HP48GX demo
This commit is contained in:
@@ -0,0 +1,310 @@
|
||||
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 rightButton = document.getElementById('rightButton');
|
||||
const downButton = document.getElementById('downButton');
|
||||
const constRightButton = document.getElementById('constRightButton');
|
||||
|
||||
const stackEls = {
|
||||
T: document.getElementById('stackT'),
|
||||
Z: document.getElementById('stackZ'),
|
||||
Y: document.getElementById('stackY'),
|
||||
X: document.getElementById('stackX'),
|
||||
};
|
||||
|
||||
const keypadGrid = document.getElementById('keypadGrid');
|
||||
const functionsGrid = document.getElementById('functionsGrid');
|
||||
const trigoGrid = document.getElementById('trigoGrid');
|
||||
const calculatorEl = document.querySelector('.calculator');
|
||||
|
||||
const keypadKeys = [
|
||||
{ label: '±', action: 'neg', className: 'key-default' },
|
||||
{ label: 'C', action: 'clear', className: 'key-danger' },
|
||||
{ label: '⎋', action: 'escape', className: 'key-danger' },
|
||||
{ label: '⌫', action: 'backspace', className: 'key-danger' },
|
||||
{ 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: 'Enter', action: 'enter', className: 'key-enter' },
|
||||
{ label: '+', action: 'add', className: 'key-accent' },
|
||||
];
|
||||
|
||||
const functionKeys = [
|
||||
{ label: 'x²', action: 'sqr', className: 'key-default' },
|
||||
{ label: 'yˣ', action: 'pow', className: 'key-default' },
|
||||
{ label: '1/x', action: 'recip', className: 'key-default' },
|
||||
{ label: '%', action: 'mod', className: 'key-default' },
|
||||
{ label: '√x', action: 'sqrt', className: 'key-default' },
|
||||
{ label: 'y√x', action: 'pow', className: 'key-default' },
|
||||
{ label: '10ˣ', action: 'pow10', className: 'key-default' },
|
||||
{ label: '', spacer: true },
|
||||
{ label: 'log', action: 'log', className: 'key-default' },
|
||||
{ label: 'ln', action: 'ln', className: 'key-default' },
|
||||
{ label: '', spacer: true },
|
||||
{ label: '', spacer: true },
|
||||
];
|
||||
|
||||
const trigoKeys = [
|
||||
{ label: 'sin', action: 'sin', className: 'key-default' },
|
||||
{ label: 'cos', action: 'cos', className: 'key-default' },
|
||||
{ label: 'tan', action: 'tan', className: 'key-default' },
|
||||
{ label: '', spacer: true },
|
||||
{ label: 'asin', action: 'asin', className: 'key-default' },
|
||||
{ label: 'acos', action: 'acos', className: 'key-default' },
|
||||
{ label: 'atan', action: 'atan', className: 'key-default' },
|
||||
{ label: '', spacer: true },
|
||||
];
|
||||
|
||||
|
||||
function focusInput() {
|
||||
hiddenInput.focus();
|
||||
}
|
||||
|
||||
function setStatus(message) {
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
function normalizeStack() {
|
||||
while (calc.stack.length > 4) {
|
||||
calc.stack.shift();
|
||||
}
|
||||
}
|
||||
|
||||
function getStackLine(indexFromTop) {
|
||||
const index = calc.stack.length - 1 - indexFromTop;
|
||||
return index >= 0 ? calc.stack[index] : '';
|
||||
}
|
||||
|
||||
function render() {
|
||||
normalizeStack();
|
||||
const isPortrait = window.matchMedia('(orientation: portrait)').matches || window.innerWidth <= 860;
|
||||
calculatorEl?.classList.toggle('portrait', isPortrait);
|
||||
calculatorEl?.classList.toggle('landscape', !isPortrait);
|
||||
stackEls.T.textContent = calc.formatNumber(getStackLine(3)) || '';
|
||||
stackEls.Z.textContent = calc.formatNumber(getStackLine(2)) || '';
|
||||
stackEls.Y.textContent = calc.formatNumber(getStackLine(1)) || '';
|
||||
stackEls.X.textContent = calc.formatNumber(getStackLine(0)) || (calc.isEditing ? calc.inputValue : '');
|
||||
modeButton.textContent = calc.angleMode;
|
||||
}
|
||||
|
||||
function pushEditingValueIfNeeded() {
|
||||
if (!calc.isEditing) return;
|
||||
if (calc.inputValue !== '') {
|
||||
calc.push(calc.parseInputValue(calc.inputValue));
|
||||
}
|
||||
calc.inputValue = '';
|
||||
calc.isEditing = false;
|
||||
}
|
||||
|
||||
function inputToX(value) {
|
||||
if (!calc.isEditing) {
|
||||
calc.isEditing = true;
|
||||
calc.inputValue = '';
|
||||
}
|
||||
if (value === 'Backspace') {
|
||||
calc.inputValue = calc.inputValue.slice(0, -1);
|
||||
} else {
|
||||
calc.inputValue += value;
|
||||
}
|
||||
if (calc.inputValue === '') {
|
||||
calc.isEditing = false;
|
||||
}
|
||||
}
|
||||
|
||||
function execute(name) {
|
||||
try {
|
||||
if (name === 'enter') {
|
||||
pushEditingValueIfNeeded();
|
||||
} else if (name === 'clear') {
|
||||
calc.clear();
|
||||
calc.inputValue = '';
|
||||
calc.isEditing = false;
|
||||
} else if (name === 'escape') {
|
||||
calc.inputValue = '';
|
||||
calc.isEditing = false;
|
||||
} else if (name === 'backspace') {
|
||||
if (calc.isEditing) {
|
||||
inputToX('Backspace');
|
||||
} else {
|
||||
calc.remove(0);
|
||||
}
|
||||
} else if (name === 'neg') {
|
||||
if (calc.isEditing) {
|
||||
calc.inputValue = calc.inputValue.startsWith('-') ? calc.inputValue.slice(1) : `-${calc.inputValue}`;
|
||||
} else {
|
||||
calc.push(calc.pop() * -1);
|
||||
}
|
||||
} else if (name === 'pow10') {
|
||||
pushEditingValueIfNeeded();
|
||||
calc.push(10);
|
||||
calc.command('pow');
|
||||
} else {
|
||||
pushEditingValueIfNeeded();
|
||||
calc.command(name);
|
||||
}
|
||||
render();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
function createKeyButton({ label, input, action, spacer, className }) {
|
||||
if (spacer) {
|
||||
const div = document.createElement('div');
|
||||
return div;
|
||||
}
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.textContent = label;
|
||||
button.className = className;
|
||||
button.addEventListener('click', () => {
|
||||
focusInput();
|
||||
if (input) {
|
||||
inputToX(input);
|
||||
render();
|
||||
return;
|
||||
}
|
||||
execute(action);
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
function buildGrid(container, keys) {
|
||||
container.innerHTML = '';
|
||||
keys.forEach((key) => container.appendChild(createKeyButton(key)));
|
||||
}
|
||||
|
||||
function handleKeyboard(event) {
|
||||
if (event.target === hiddenInput) 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',
|
||||
};
|
||||
if (map[key]) {
|
||||
event.preventDefault();
|
||||
execute(map[key]);
|
||||
}
|
||||
}
|
||||
|
||||
const modeOptions = ['deg', 'rad', 'grad'];
|
||||
let modeMenuEl = null;
|
||||
|
||||
function closeModeMenu() {
|
||||
if (modeMenuEl) {
|
||||
modeMenuEl.remove();
|
||||
modeMenuEl = null;
|
||||
}
|
||||
}
|
||||
|
||||
function openModeMenu() {
|
||||
closeModeMenu();
|
||||
const rect = modeButton.getBoundingClientRect();
|
||||
modeMenuEl = document.createElement('div');
|
||||
modeMenuEl.className = 'mode-menu';
|
||||
modeMenuEl.style.top = `${rect.bottom + 6 + window.scrollY}px`;
|
||||
modeMenuEl.style.left = `${rect.left + window.scrollX}px`;
|
||||
modeOptions.forEach((mode) => {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = `mode-menu-item${mode === calc.angleMode ? ' is-active' : ''}`;
|
||||
button.textContent = mode;
|
||||
button.addEventListener('click', () => {
|
||||
calc.angleMode = mode;
|
||||
render();
|
||||
closeModeMenu();
|
||||
});
|
||||
modeMenuEl.appendChild(button);
|
||||
});
|
||||
document.body.appendChild(modeMenuEl);
|
||||
const menuRect = modeMenuEl.getBoundingClientRect();
|
||||
const maxLeft = Math.max(8, window.innerWidth - menuRect.width - 8);
|
||||
modeMenuEl.style.left = `${Math.max(8, Math.min(maxLeft, rect.left + window.scrollX))}px`;
|
||||
}
|
||||
|
||||
modeButton.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
openModeMenu();
|
||||
});
|
||||
|
||||
window.addEventListener('resize', closeModeMenu);
|
||||
window.addEventListener('scroll', closeModeMenu, true);
|
||||
|
||||
window.addEventListener('click', (event) => {
|
||||
if (modeMenuEl && !event.target.closest('.mode-menu') && event.target !== modeButton) {
|
||||
closeModeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
pasteButton.addEventListener('click', async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
if (!text) {
|
||||
setStatus('Clipboard empty');
|
||||
return;
|
||||
}
|
||||
if (calc.isEditing) {
|
||||
calc.inputValue += text;
|
||||
} else {
|
||||
calc.isEditing = true;
|
||||
calc.inputValue = text;
|
||||
}
|
||||
setStatus('Pasted');
|
||||
render();
|
||||
} catch (error) {
|
||||
setStatus('Paste unavailable');
|
||||
}
|
||||
});
|
||||
|
||||
upButton.addEventListener('click', () => {});
|
||||
|
||||
constButton.addEventListener('click', () => {});
|
||||
|
||||
rightButton.addEventListener('click', () => {});
|
||||
|
||||
downButton.addEventListener('click', () => {});
|
||||
|
||||
constRightButton.addEventListener('click', () => {});
|
||||
|
||||
window.addEventListener('keydown', handleKeyboard);
|
||||
window.addEventListener('load', focusInput);
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
if (!event.target.closest('.calculator')) {
|
||||
focusInput();
|
||||
}
|
||||
});
|
||||
|
||||
buildGrid(keypadGrid, keypadKeys);
|
||||
buildGrid(functionsGrid, functionKeys);
|
||||
buildGrid(trigoGrid, trigoKeys);
|
||||
render();
|
||||
Reference in New Issue
Block a user