From ceed62c8f21bbe14994a8dd66df5b65c5e166362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Wed, 21 Feb 2024 15:46:27 +0100 Subject: [PATCH] merges NumberInputField back into NumberComponent --- frontend/src/components/NumberComponent.tsx | 261 ++++++++++++++++- frontend/src/components/NumberInputField.tsx | 285 ------------------- 2 files changed, 247 insertions(+), 299 deletions(-) delete mode 100644 frontend/src/components/NumberInputField.tsx 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 (
{process.env.NODE_ENV === 'development' && (
Render count: {renderCount.current}
)} - + + {displayName && ( + + {displayName} + + + )} + + {unit && {unit}} +
); }); diff --git a/frontend/src/components/NumberInputField.tsx b/frontend/src/components/NumberInputField.tsx deleted file mode 100644 index 220d6f4..0000000 --- a/frontend/src/components/NumberInputField.tsx +++ /dev/null @@ -1,285 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Form, InputGroup } from 'react-bootstrap'; -import { DocStringComponent } from './DocStringComponent'; -import '../App.css'; - -// TODO: add button functionality - -type NumberInputFieldProps = { - name: string; - value: number; - type?: 'float' | 'int'; - displayName?: string; - readOnly?: boolean; - docString?: string; - isInstantUpdate?: boolean; - unit?: string; - changeCallback?: (value: number) => 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 }; -}; - -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 NumberInputField = React.memo((props: NumberInputFieldProps) => { - const { - name, - value, - unit, - type = 'float', - displayName, - docString = null, - readOnly = false, - isInstantUpdate = false - } = 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()); - - 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); - } - }); - - 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()); - } - }, [value]); - - 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) { - if (props.changeCallback !== undefined) { - props.changeCallback(Number(newValue)); - } - return; - } else { - console.debug(key); - return; - } - - // Update the input value and maintain the cursor position - if (isInstantUpdate) { - if (props.changeCallback !== undefined) { - props.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 - if (props.changeCallback !== undefined) { - props.changeCallback(Number(inputString)); - } - } - }; - return ( -
- - {displayName && ( - - {displayName} - - - )} - - {unit && {unit}} - -
- ); -});