diff --git a/frontend/src/components/NumberComponent.tsx b/frontend/src/components/NumberComponent.tsx index 8dbe130..b434851 100644 --- a/frontend/src/components/NumberComponent.tsx +++ b/frontend/src/components/NumberComponent.tsx @@ -1,7 +1,8 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; +import { Form, InputGroup } from 'react-bootstrap'; +import { DocStringComponent } from './DocStringComponent'; import '../App.css'; import { LevelName } from './NotificationsComponent'; -import { NumberInputField } from './NumberInputField'; // TODO: add button functionality @@ -48,10 +49,122 @@ type NumberComponentProps = { id: string; }; +// TODO: highlight the digit that is being changed by setting both selectionStart and +// selectionEnd +const handleArrowKey = ( + key: string, + value: string, + selectionStart: number + // selectionEnd: number +) => { + // Split the input value into the integer part and decimal part + const parts = value.split('.'); + const beforeDecimalCount = parts[0].length; // Count digits before the decimal + const afterDecimalCount = parts[1] ? parts[1].length : 0; // Count digits after the decimal + + const isCursorAfterDecimal = selectionStart > beforeDecimalCount; + + // Calculate the increment/decrement value based on the cursor position + let increment = 0; + if (isCursorAfterDecimal) { + increment = Math.pow(10, beforeDecimalCount + 1 - selectionStart); + } else { + increment = Math.pow(10, beforeDecimalCount - selectionStart); + } + + // Convert the input value to a number, increment or decrement it based on the + // arrow key + const numValue = parseFloat(value) + (key === 'ArrowUp' ? increment : -increment); + + // Convert the resulting number to a string, maintaining the same number of digits + // after the decimal + const newValue = numValue.toFixed(afterDecimalCount); + + // Check if the length of the integer part of the number string has in-/decreased + const newBeforeDecimalCount = newValue.split('.')[0].length; + if (newBeforeDecimalCount > beforeDecimalCount) { + // Move the cursor one position to the right + selectionStart += 1; + } else if (newBeforeDecimalCount < beforeDecimalCount) { + // Move the cursor one position to the left + selectionStart -= 1; + } + return { value: newValue, selectionStart }; +}; + +const handleBackspaceKey = ( + value: string, + selectionStart: number, + selectionEnd: number +) => { + if (selectionEnd > selectionStart) { + // If there is a selection, delete all characters in the selection + return { + value: value.slice(0, selectionStart) + value.slice(selectionEnd), + selectionStart + }; + } else if (selectionStart > 0) { + return { + value: value.slice(0, selectionStart - 1) + value.slice(selectionStart), + selectionStart: selectionStart - 1 + }; + } + return { value, selectionStart }; +}; + +const handleDeleteKey = ( + value: string, + selectionStart: number, + selectionEnd: number +) => { + if (selectionEnd > selectionStart) { + // If there is a selection, delete all characters in the selection + return { + value: value.slice(0, selectionStart) + value.slice(selectionEnd), + selectionStart + }; + } else if (selectionStart < value.length) { + return { + value: value.slice(0, selectionStart) + value.slice(selectionStart + 1), + selectionStart + }; + } + return { value, selectionStart }; +}; + +const handleNumericKey = ( + key: string, + value: string, + selectionStart: number, + selectionEnd: number +) => { + // Check if a number key or a decimal point key is pressed + if (key === '.' && value.includes('.')) { + // Check if value already contains a decimal. If so, ignore input. + console.warn('Invalid input! Ignoring...'); + return { value, selectionStart }; + } + + let newValue = value; + + // Add the new key at the cursor's position + if (selectionEnd > selectionStart) { + // If there is a selection, replace it with the key + newValue = value.slice(0, selectionStart) + key + value.slice(selectionEnd); + } else { + // otherwise, append the key after the selection start + newValue = value.slice(0, selectionStart) + key + value.slice(selectionStart); + } + + return { value: newValue, selectionStart: selectionStart + 1 }; +}; + export const NumberComponent = React.memo((props: NumberComponentProps) => { const { + name, value, readOnly, + type, docString, isInstantUpdate, unit, @@ -61,12 +174,117 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => { id } = props; + // Create a state for the cursor position + const [cursorPosition, setCursorPosition] = useState(null); + // Create a state for the input string + const [inputString, setInputString] = useState(value.toString()); const renderCount = useRef(0); const fullAccessPath = [props.parentPath, props.name] .filter((element) => element) .join('.'); + const handleKeyDown = (event) => { + const { key, target } = event; + if ( + key === 'F1' || + key === 'F5' || + key === 'F12' || + key === 'Tab' || + key === 'ArrowRight' || + key === 'ArrowLeft' + ) { + return; + } + event.preventDefault(); + + // Get the current input value and cursor position + const { value } = target; + let { selectionStart } = target; + const { selectionEnd } = target; + + let newValue: string = value; + if (event.ctrlKey && key === 'a') { + // Select everything when pressing Ctrl + a + target.setSelectionRange(0, target.value.length); + return; + } else if (key === '-') { + if (selectionStart === 0 && !value.startsWith('-')) { + newValue = '-' + value; + selectionStart++; + } else if (value.startsWith('-') && selectionStart === 1) { + newValue = value.substring(1); // remove minus sign + selectionStart--; + } else { + return; // Ignore "-" pressed in other positions + } + } else if (!isNaN(key) && key !== ' ') { + // Check if a number key or a decimal point key is pressed + ({ value: newValue, selectionStart } = handleNumericKey( + key, + value, + selectionStart, + selectionEnd + )); + } else if (key === '.' && type === 'float') { + ({ value: newValue, selectionStart } = handleNumericKey( + key, + value, + selectionStart, + selectionEnd + )); + } else if (key === 'ArrowUp' || key === 'ArrowDown') { + ({ value: newValue, selectionStart } = handleArrowKey( + key, + value, + selectionStart + // selectionEnd + )); + } else if (key === 'Backspace') { + ({ value: newValue, selectionStart } = handleBackspaceKey( + value, + selectionStart, + selectionEnd + )); + } else if (key === 'Delete') { + ({ value: newValue, selectionStart } = handleDeleteKey( + value, + selectionStart, + selectionEnd + )); + } else if (key === 'Enter' && !isInstantUpdate) { + changeCallback(Number(newValue)); + return; + } else { + console.debug(key); + return; + } + + // Update the input value and maintain the cursor position + if (isInstantUpdate) { + changeCallback(Number(newValue)); + } + + setInputString(newValue); + + // Save the current cursor position before the component re-renders + setCursorPosition(selectionStart); + }; + + const handleBlur = () => { + if (!isInstantUpdate) { + // If not in "instant update" mode, emit an update when the input field loses focus + changeCallback(Number(inputString)); + } + }; useEffect(() => { + // Parse the input string to a number for comparison + const numericInputString = + type === 'int' ? parseInt(inputString) : parseFloat(inputString); + // Only update the inputString if it's different from the prop value + if (value !== numericInputString) { + setInputString(value.toString()); + } + // emitting notification let notificationMsg = `${fullAccessPath} changed to ${props.value}`; if (unit === undefined) { @@ -75,24 +293,39 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => { notificationMsg += ` ${unit}.`; } addNotification(notificationMsg); - }, [props.value]); + }, [value]); + + useEffect(() => { + // Set the cursor position after the component re-renders + const inputElement = document.getElementsByName(name)[0] as HTMLInputElement; + if (inputElement && cursorPosition !== null) { + inputElement.setSelectionRange(cursorPosition, cursorPosition); + } + }); return (