feat: add stack navigation and reordering controls

This commit is contained in:
2026-05-17 04:24:43 +02:00
parent f44fb8c252
commit 6dd9550890
2 changed files with 206 additions and 7 deletions
+12
View File
@@ -141,6 +141,18 @@ body {
.stack-value.is-editing { .stack-value.is-editing {
letter-spacing: 0.02em; letter-spacing: 0.02em;
} }
.stack-cell.is-selected {
background: transparent;
border-radius: 6px;
outline: 2px solid rgba(31, 42, 18, 0.2);
outline-offset: -2px;
}
.stack-cell.is-moving {
background: rgba(31, 42, 18, 0.18);
}
.display-buttons-panel { .display-buttons-panel {
padding: 8px; padding: 8px;
min-height: 0; min-height: 0;
+194 -7
View File
@@ -96,6 +96,9 @@ function focusInput() {
let statusTimer = null; let statusTimer = null;
let editCursor = 0; let editCursor = 0;
let editRestoreValue = null; let editRestoreValue = null;
let stackMode = 'normal';
let stackSelection = 0;
let stackMoveSnapshot = null;
function setStatus(message, isError = false, timeoutMs = 1400) { function setStatus(message, isError = false, timeoutMs = 1400) {
if (!statusLine) return; if (!statusLine) return;
@@ -133,6 +136,25 @@ function getStackDisplayValue(label) {
return calc.formatNumber(getStackLine(indexFromTop)) || ''; return calc.formatNumber(getStackLine(indexFromTop)) || '';
} }
function getVisibleStackLabel(label) {
if (stackMode === 'navigation') {
const indexMap = { X: 0, Y: 1, Z: 2, T: 3 };
return String(stackSelection + indexMap[label]);
}
return label;
}
function updateStackLabels() {
const stackLabels = ['T', 'Z', 'Y', 'X'];
for (const label of stackLabels) {
stackEls[label].dataset.label = getVisibleStackLabel(label);
}
}
function stackModeToLabel(index) {
return ['X', 'Y', 'Z', 'T'][Math.max(0, Math.min(3, index))] ?? 'X';
}
function updateCopyButtons() { function updateCopyButtons() {
for (const label of ['T', 'Z', 'Y', 'X']) { for (const label of ['T', 'Z', 'Y', 'X']) {
const value = getStackDisplayValue(label); const value = getStackDisplayValue(label);
@@ -153,19 +175,28 @@ function render() {
const isPortrait = window.matchMedia('(orientation: portrait)').matches || window.innerWidth <= 860; const isPortrait = window.matchMedia('(orientation: portrait)').matches || window.innerWidth <= 860;
calculatorEl?.classList.toggle('portrait', isPortrait); calculatorEl?.classList.toggle('portrait', isPortrait);
calculatorEl?.classList.toggle('landscape', !isPortrait); calculatorEl?.classList.toggle('landscape', !isPortrait);
stackEls.X.textContent = calc.isEditing ? '' : getStackDisplayValue('X'); const stackLabels = ['T', 'Z', 'Y', 'X'];
for (const label of stackLabels) {
const isSelected = stackMode !== 'normal' && stackModeToLabel(stackSelection) === label;
const value = label === 'X' && calc.isEditing ? '' : getStackDisplayValue(label);
stackEls[label].textContent = value;
stackEls[label].classList.toggle('is-editing', label === 'X' && calc.isEditing);
stackEls[label].classList.toggle('is-caret-visible', label === 'X' && calc.isEditing);
stackEls[label].parentElement?.classList.toggle('is-selected', isSelected);
stackEls[label].parentElement?.classList.toggle('is-moving', stackMode === 'move' && stackModeToLabel(stackSelection) === label);
}
if (calc.isEditing) { if (calc.isEditing) {
renderEditValue(); renderEditValue();
} }
stackEls.Y.textContent = getStackDisplayValue('Y'); updateStackLabels();
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(); updateCopyButtons();
modeButton.textContent = calc.angleMode; modeButton.textContent = calc.angleMode;
} }
function stackModeToLabel(index) {
return ['X', 'Y', 'Z', 'T'][Math.max(0, Math.min(3, index))] ?? 'X';
}
function stopEditing(clearValue = false) { function stopEditing(clearValue = false) {
if (clearValue) { if (clearValue) {
calc.inputValue = ''; calc.inputValue = '';
@@ -182,6 +213,105 @@ function cancelEditing() {
stopEditing(true); stopEditing(true);
} }
function enterNavigationMode() {
if (calc.isEditing) return;
stackMode = 'navigation';
stackSelection = 0;
stackMoveSnapshot = null;
render();
}
function exitStackMode() {
stackMode = 'normal';
stackSelection = 0;
stackMoveSnapshot = null;
render();
}
function moveNavigationSelection(delta) {
const maxIndex = Math.max(0, calc.stack.length - 1);
stackSelection = Math.max(0, Math.min(maxIndex, stackSelection + delta));
render();
}
function beginStackMove() {
stackMode = 'move';
stackMoveSnapshot = calc.stack.slice();
render();
}
function restoreStackMoveSnapshot() {
if (!stackMoveSnapshot) return;
calc.stack = stackMoveSnapshot.slice();
stackMoveSnapshot = null;
render();
}
function validateStackMove() {
stackMoveSnapshot = null;
stackMode = 'normal';
render();
}
function moveSelectedStackValue(delta) {
const index = stackSelection;
const target = Math.max(0, Math.min(calc.stack.length - 1, index + delta));
if (target === index) return;
const value = calc.stack.splice(index, 1)[0];
calc.stack.splice(target, 0, value);
stackSelection = target;
render();
}
function enterNavigationMode() {
if (calc.isEditing) return;
stackMode = 'navigation';
stackSelection = 0;
render();
}
function exitStackMode() {
stackMode = 'normal';
stackSelection = 0;
stackMoveSnapshot = null;
render();
}
function moveNavigationSelection(delta) {
const maxIndex = Math.max(0, calc.stack.length - 1);
stackSelection = Math.max(0, Math.min(maxIndex, stackSelection + delta));
render();
}
function beginStackMove() {
stackMode = 'move';
stackMoveSnapshot = calc.stack.slice();
render();
}
function restoreStackMoveSnapshot() {
if (!stackMoveSnapshot) return;
calc.stack = stackMoveSnapshot.slice();
stackMoveSnapshot = null;
render();
}
function validateStackMove() {
stackMoveSnapshot = null;
stackMode = 'normal';
render();
}
function moveSelectedStackValue(delta) {
const index = stackSelection;
const target = Math.max(0, Math.min(calc.stack.length - 1, index + delta));
if (target === index) return;
const value = calc.stack.splice(index, 1)[0];
calc.stack.splice(target, 0, value);
stackSelection = target;
render();
}
function moveEditCursor(delta) { function moveEditCursor(delta) {
editCursor = Math.max(0, Math.min(calc.inputValue.length, editCursor + delta)); editCursor = Math.max(0, Math.min(calc.inputValue.length, editCursor + delta));
} }
@@ -331,6 +461,53 @@ async function copyStackValue(label) {
function handleKeyboard(event) { function handleKeyboard(event) {
if (event.defaultPrevented) return; if (event.defaultPrevented) return;
const key = event.key; const key = event.key;
if (stackMode === 'navigation') {
if (key === 'ArrowUp') {
event.preventDefault();
moveNavigationSelection(1);
return;
}
if (key === 'ArrowDown') {
event.preventDefault();
moveNavigationSelection(-1);
return;
}
if (key === 'Escape' || key === 'ArrowLeft') {
event.preventDefault();
exitStackMode();
return;
}
if (key === 'Enter') {
event.preventDefault();
beginStackMove();
return;
}
return;
}
if (stackMode === 'move') {
if (key === 'ArrowUp') {
event.preventDefault();
moveSelectedStackValue(1);
return;
}
if (key === 'ArrowDown') {
event.preventDefault();
moveSelectedStackValue(-1);
return;
}
if (key === 'Escape') {
event.preventDefault();
restoreStackMoveSnapshot();
exitStackMode();
return;
}
if (key === 'Enter') {
event.preventDefault();
validateStackMove();
return;
}
return;
}
if (/^[0-9.]$/.test(key)) { if (/^[0-9.]$/.test(key)) {
event.preventDefault(); event.preventDefault();
inputToX(key); inputToX(key);
@@ -498,7 +675,12 @@ hiddenInput.addEventListener('paste', (event) => {
pasteTextIntoStack(text); pasteTextIntoStack(text);
}); });
upButton.addEventListener('click', () => {}); upButton.addEventListener('click', () => {
if (!calc.isEditing && stackMode === 'normal') {
enterNavigationMode();
focusInput();
}
});
const constantLabels = { const constantLabels = {
pi: 'π', pi: 'π',
@@ -549,6 +731,11 @@ leftButton.addEventListener('click', () => {
moveEditCursor(-1); moveEditCursor(-1);
render(); render();
focusInput(); focusInput();
return;
}
if (stackMode === 'navigation') {
exitStackMode();
focusInput();
} }
}); });
downButton.addEventListener('click', () => { downButton.addEventListener('click', () => {