Files
q-support/src/utils/BoundedNumericTextField.tsx
Qortal Seth f53ac4bad4 Added features from Q-Support 1.1.0 posted on Q-Share: (#9)
Bounties can be added to an Issue. They can be either a direct payment from the publisher in ANY supported coin, or they can be a link to a Q-Fund.

Q-Funds that are still in progress have a donate button so users can support it without having to leave Q-Support and open the Q-Fund.

Any category can be searched for individually using the "Single Category" Combobox.

user can add source code to their Issue, so it is easier to see.

IssueIcons have a tooltip that displays the name of its category. If the category is Q-Apps/Websites, then the icon of its owner will also be displayed.
2024-06-14 15:36:06 -06:00

166 lines
4.6 KiB
TypeScript

import AddIcon from "@mui/icons-material/Add";
import RemoveIcon from "@mui/icons-material/Remove";
import {
IconButton,
InputAdornment,
TextField,
TextFieldProps,
} from "@mui/material";
import React, { useRef, useState } from "react";
type eventType = React.ChangeEvent<HTMLInputElement>;
type BoundedNumericTextFieldProps = {
minValue: number;
maxValue: number;
addIconButtons?: boolean;
allowDecimals?: boolean;
allowNegatives?: boolean;
onChange?: (s: string) => void;
initialValue?: string;
maxSigDigits?: number;
} & TextFieldProps;
export const BoundedNumericTextField = ({
minValue,
maxValue,
addIconButtons = true,
allowDecimals = true,
allowNegatives = false,
initialValue,
maxSigDigits = 6,
...props
}: BoundedNumericTextFieldProps) => {
const [textFieldValue, setTextFieldValue] = useState<string>(
initialValue || ""
);
const ref = useRef<HTMLInputElement | null>(null);
const stringIsEmpty = (value: string) => {
return value === "";
};
const isAllZerosNum = /^0*\.?0*$/;
const isFloatNum = /^-?[0-9]*\.?[0-9]*$/;
const isIntegerNum = /^-?[0-9]+$/;
const skipMinMaxCheck = (value: string) => {
const lastIndexIsDecimal = value.charAt(value.length - 1) === ".";
const isEmpty = stringIsEmpty(value);
const isAllZeros = isAllZerosNum.test(value);
const isInteger = isIntegerNum.test(value);
// skipping minMax on all 0s allows values less than 1 to be entered
return lastIndexIsDecimal || isEmpty || (isAllZeros && !isInteger);
};
const setMinMaxValue = (value: string): string => {
if (skipMinMaxCheck(value)) return value;
const valueNum = Number(value);
const boundedNum = setNumberWithinBounds(valueNum, minValue, maxValue);
const numberInBounds = boundedNum === valueNum;
return numberInBounds ? value : boundedNum.toString();
};
const getSigDigits = (number: string) => {
if (isIntegerNum.test(number)) return 0;
const decimalSplit = number.split(".");
return decimalSplit[decimalSplit.length - 1].length;
};
const sigDigitsExceeded = (number: string, sigDigits: number) => {
return getSigDigits(number) > sigDigits;
};
const filterTypes = (value: string) => {
if (allowDecimals === false) value = value.replace(".", "");
if (allowNegatives === false) value = value.replace("-", "");
if (sigDigitsExceeded(value, maxSigDigits)) {
value = value.substring(0, value.length - 1);
}
return value;
};
const filterValue = (value: string) => {
if (stringIsEmpty(value)) return "";
value = filterTypes(value);
if (isFloatNum.test(value)) {
return setMinMaxValue(value);
}
return textFieldValue;
};
const listeners = (e: eventType) => {
// console.log("changeEvent:", e);
const newValue = filterValue(e.target.value);
setTextFieldValue(newValue);
if (props?.onChange) props.onChange(newValue);
};
const changeValueWithIncDecButton = (e, changeAmount: number) => {
const changedValue = (+textFieldValue + changeAmount).toString();
const inBoundsValue = setMinMaxValue(changedValue);
setTextFieldValue(inBoundsValue);
if (props?.onChange) props.onChange(inBoundsValue);
};
const formatValueOnBlur = (e: eventType) => {
let value = e.target.value;
if (stringIsEmpty(value) || value === ".") {
setTextFieldValue("");
return;
}
value = setMinMaxValue(value);
value = removeTrailingZeros(value);
if (isAllZerosNum.test(value)) value = minValue.toString();
setTextFieldValue(value);
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { onChange, ...noChangeProps } = { ...props };
return (
<TextField
{...noChangeProps}
InputProps={{
...props?.InputProps,
endAdornment: addIconButtons ? (
<InputAdornment position="end">
<IconButton onClick={e => changeValueWithIncDecButton(e, 1)}>
<AddIcon />{" "}
</IconButton>
<IconButton onClick={e => changeValueWithIncDecButton(e, -1)}>
<RemoveIcon />{" "}
</IconButton>
</InputAdornment>
) : (
<></>
),
}}
onChange={e => listeners(e as eventType)}
onBlur={e => {
formatValueOnBlur(e as eventType);
}}
autoComplete="off"
value={textFieldValue}
inputRef={ref}
/>
);
};
const removeTrailingZeros = (s: string) => {
return Number(s).toString();
};
const setNumberWithinBounds = (
num: number,
minValue: number,
maxValue: number
) => {
if (num > maxValue) return maxValue;
if (num < minValue) return minValue;
return num;
};
export default BoundedNumericTextField;