import React, { useContext, useEffect, useRef, useState } from 'react'; import { WebSettingsContext } from '../WebSettings'; import { Form, InputGroup } from 'react-bootstrap'; import { setAttribute } from '../socket'; import { DocStringComponent } from './DocStringComponent'; import '../App.css'; import { getIdFromFullAccessPath } from '../utils/stringUtils'; import { LevelName } from './NotificationsComponent'; // TODO: add button functionality export type QuantityObject = { type: 'Quantity'; readonly: boolean; value: { magnitude: number; unit: string; }; doc?: string; }; export type IntObject = { type: 'int'; readonly: boolean; value: number; doc?: string; }; export type FloatObject = { type: 'float'; readonly: boolean; value: number; doc?: string; }; export type NumberObject = IntObject | FloatObject | QuantityObject; type NumberComponentProps = { name: string; type: 'float' | 'int'; parentPath?: string; value: number; readOnly: boolean; docString: string; isInstantUpdate: boolean; unit?: string; showName?: boolean; addNotification: (message: string, levelname?: LevelName) => void; }; // 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 }; }; export const NumberComponent = React.memo((props: NumberComponentProps) => { const { name, parentPath, readOnly, docString, isInstantUpdate, unit, addNotification } = props; // Whether to show the name infront of the component (false if used with a slider) const showName = props.showName !== undefined ? props.showName : true; const renderCount = useRef(0); // Create a state for the cursor position const [cursorPosition, setCursorPosition] = useState(null); // Create a state for the input string const [inputString, setInputString] = useState(props.value.toString()); const fullAccessPath = [parentPath, name].filter((element) => element).join('.'); const id = getIdFromFullAccessPath(fullAccessPath); const webSettings = useContext(WebSettingsContext); let displayName = name; if (webSettings[fullAccessPath] && webSettings[fullAccessPath].displayName) { displayName = webSettings[fullAccessPath].displayName; } useEffect(() => { renderCount.current++; // Set the cursor position after the component re-renders const inputElement = document.getElementsByName( fullAccessPath )[0] as HTMLInputElement; if (inputElement && cursorPosition !== null) { inputElement.setSelectionRange(cursorPosition, cursorPosition); } }); useEffect(() => { // Parse the input string to a number for comparison const numericInputString = props.type === 'int' ? parseInt(inputString) : parseFloat(inputString); // Only update the inputString if it's different from the prop value if (props.value !== numericInputString) { setInputString(props.value.toString()); } // emitting notification let notificationMsg = `${parentPath}.${name} changed to ${props.value}`; if (unit === undefined) { notificationMsg += '.'; } else { notificationMsg += ` ${unit}.`; } addNotification(notificationMsg); }, [props.value]); 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('.') || props.type === 'int')) { // Check if value already contains a decimal. If so, ignore input. // eslint-disable-next-line no-console 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 }; }; 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 === '.') { ({ 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) { setAttribute(name, parentPath, Number(newValue)); return; } else { console.debug(key); return; } // Update the input value and maintain the cursor position if (isInstantUpdate) { setAttribute(name, parentPath, 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 setAttribute(name, parentPath, Number(inputString)); } }; return (