diff --git a/samples/calc-02/index.css b/samples/calc-02/index.css index 6993ee7..bb7e7aa 100644 --- a/samples/calc-02/index.css +++ b/samples/calc-02/index.css @@ -141,6 +141,18 @@ body { .stack-value.is-editing { 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 { padding: 8px; min-height: 0; diff --git a/samples/calc-02/index.js b/samples/calc-02/index.js index cd289a2..c8935af 100644 --- a/samples/calc-02/index.js +++ b/samples/calc-02/index.js @@ -96,6 +96,9 @@ function focusInput() { let statusTimer = null; let editCursor = 0; let editRestoreValue = null; +let stackMode = 'normal'; +let stackSelection = 0; +let stackMoveSnapshot = null; function setStatus(message, isError = false, timeoutMs = 1400) { if (!statusLine) return; @@ -133,6 +136,25 @@ function getStackDisplayValue(label) { 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() { for (const label of ['T', 'Z', 'Y', 'X']) { const value = getStackDisplayValue(label); @@ -153,19 +175,28 @@ 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'); + 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) { 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); + updateStackLabels(); updateCopyButtons(); modeButton.textContent = calc.angleMode; } +function stackModeToLabel(index) { + return ['X', 'Y', 'Z', 'T'][Math.max(0, Math.min(3, index))] ?? 'X'; +} + function stopEditing(clearValue = false) { if (clearValue) { calc.inputValue = ''; @@ -182,6 +213,105 @@ function cancelEditing() { 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) { editCursor = Math.max(0, Math.min(calc.inputValue.length, editCursor + delta)); } @@ -331,6 +461,53 @@ async function copyStackValue(label) { function handleKeyboard(event) { if (event.defaultPrevented) return; 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)) { event.preventDefault(); inputToX(key); @@ -498,7 +675,12 @@ hiddenInput.addEventListener('paste', (event) => { pasteTextIntoStack(text); }); -upButton.addEventListener('click', () => {}); +upButton.addEventListener('click', () => { + if (!calc.isEditing && stackMode === 'normal') { + enterNavigationMode(); + focusInput(); + } +}); const constantLabels = { pi: 'π', @@ -549,6 +731,11 @@ leftButton.addEventListener('click', () => { moveEditCursor(-1); render(); focusInput(); + return; + } + if (stackMode === 'navigation') { + exitStackMode(); + focusInput(); } }); downButton.addEventListener('click', () => {