separating out NumberInputField from NumberComponent (to be used in MethodComponent)

This commit is contained in:
Mose Müller 2024-02-20 17:20:20 +01:00
parent 1c029e301b
commit 2bb02a5558
2 changed files with 303 additions and 252 deletions

View File

@ -1,11 +1,10 @@
import React, { useContext, useEffect, useRef, useState } from 'react';
import React, { useContext, useEffect, useRef } 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';
import { NumberInputField } from './NumberInputField';
import { setAttribute } from '../socket';
// TODO: add button functionality
@ -45,92 +44,10 @@ type NumberComponentProps = {
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,
value,
parentPath,
readOnly,
docString,
@ -139,14 +56,13 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
addNotification
} = props;
function changeCallback(value: number) {
setAttribute(name, parentPath, value);
}
// 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);
@ -157,26 +73,6 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
}
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) {
@ -187,152 +83,22 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
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 (
<div className="component numberComponent" id={id}>
{process.env.NODE_ENV === 'development' && (
<div>Render count: {renderCount.current}</div>
)}
<div className="d-flex">
<InputGroup>
{showName && (
<InputGroup.Text>
{displayName}
<DocStringComponent docString={docString} />
</InputGroup.Text>
)}
<Form.Control
type="text"
value={inputString}
disabled={readOnly}
name={fullAccessPath}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
className={isInstantUpdate && !readOnly ? 'instantUpdate' : ''}
/>
{unit && <InputGroup.Text>{unit}</InputGroup.Text>}
</InputGroup>
</div>
<NumberInputField
name={fullAccessPath}
value={value}
displayName={showName === true ? displayName : null}
unit={unit}
readOnly={readOnly}
type={props.type}
docString={docString}
isInstantUpdate={isInstantUpdate}
changeCallback={changeCallback}
/>
</div>
);
});

View File

@ -0,0 +1,285 @@
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 (
<div className="d-flex">
<InputGroup>
{displayName && (
<InputGroup.Text>
{displayName}
<DocStringComponent docString={docString} />
</InputGroup.Text>
)}
<Form.Control
type="text"
value={inputString}
disabled={readOnly}
name={name}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
className={isInstantUpdate && !readOnly ? 'instantUpdate' : ''}
/>
{unit && <InputGroup.Text>{unit}</InputGroup.Text>}
</InputGroup>
</div>
);
});