@i-novus/ui-core
Version:
Базовые UI компоненты
137 lines (136 loc) • 7.07 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useCallback, useState, forwardRef, useEffect, } from 'react';
import classNames from 'classnames';
import { useConfigProvider } from '../core';
import { Icon } from '../data-display/Icon';
import { Input } from './Input';
const getValueInRange = (value, min, max) => {
// Заменяем пустую строку и '-' на null
if ((!value && value !== 0) || value === '-') {
return null;
}
// Убираем точку в конце, если дробное число ввели не до конца
const stringValue = String(value).replace(/\.$/, '');
const numberValue = Number(stringValue);
if ((min || min === 0) && numberValue < min) {
return min;
}
if (max && numberValue > max) {
return max;
}
return numberValue;
};
const getFixedNumber = (value, precision) => {
if (value === null) {
return value;
}
if (typeof precision === 'undefined') {
return Math.trunc(value);
}
// так сделано, чтобы убрать округление у toFixed()
const fixedString = value.toFixed(precision + 1).replace(/.$/, '');
return Number(fixedString);
};
const validate = (value) => {
const regExp = /^-?(\d*|\d+(\.\d*))$/;
return !!String(value).match(regExp);
};
const validateNumber = (value) => {
const regExp = /^(-|-?\d+\.)$/;
return !String(value).match(regExp);
};
const isIntegerInputValid = (inputValue) => {
return /^-?\d*$/.test(String(inputValue));
};
const isValidValue = (value) => value !== null && !Number.isNaN(Number(value)) && value !== '';
const isDecimalPrecisionExceeded = (inputValue, precision) => {
const inputString = String(inputValue);
const decimalPointIndex = inputString.indexOf('.');
if (decimalPointIndex !== -1 && precision !== undefined) {
const decimalPartLength = (inputString.length - 1) - decimalPointIndex;
return decimalPartLength > precision;
}
return false;
};
// eslint-disable-next-line sonarjs/cognitive-complexity
export const InputNumber = forwardRef((props, ref) => {
const { onChange: onChangeExternal, onBlur: onBlurExternal, onKeyDown: onKeyDownExternal, max = Number.MAX_SAFE_INTEGER, min = Number.MIN_SAFE_INTEGER, value = null, visible = true, showButtons = true, step = 1, precision, className, prefix, disabled = false, ...restProps } = props;
const { getPrefix } = useConfigProvider();
const prefixCls = getPrefix(prefix);
const [localValue, setLocalValue] = useState(value);
const onChangeWrapper = useCallback((newValue, event) => {
if (value === newValue) {
return;
}
onChangeExternal?.(newValue, event);
}, [onChangeExternal, value]);
const onChangeInternal = useCallback((newValue, event) => {
// Заменяем все пустые строки, Undefined и т.д. на Null
if (!newValue && newValue !== 0) {
onChangeWrapper?.(null, event);
setLocalValue(null);
return;
}
if (isDecimalPrecisionExceeded(newValue, precision)) {
return;
}
if (!precision && !isIntegerInputValid(newValue)) {
return;
}
// Проверяем, устраивает ли нас введённое значение
const isValid = validate(newValue);
if (!isValid) {
return;
}
// Проверяем, является ли введённое значение числом (т.к. "-" и "1." тоже можно ввести, но это не число)
const isNumber = validateNumber(newValue);
if (isNumber) {
// так сделано, чтобы убрать округление у toFixed()
const fixedNumber = getFixedNumber(Number(newValue), precision);
onChangeWrapper?.(fixedNumber, event);
setLocalValue(newValue);
return;
}
setLocalValue(newValue);
}, [onChangeWrapper, precision]);
const onBlurInternal = useCallback((event) => {
// при onBlur обрезаются лишние цифры после точки и значение подменяется на min/max, если не попадает в диапазон
const valueInRange = getValueInRange(localValue, min, max);
const fixedValue = getFixedNumber(valueInRange, precision);
onChangeInternal(fixedValue, event);
setLocalValue(fixedValue);
onBlurExternal?.(event);
}, [localValue, max, min, onBlurExternal, onChangeInternal, precision]);
const onIncrementValue = useCallback((event) => {
const nextValue = Number(value) + Number(step);
const newValue = typeof max !== 'undefined' && nextValue > max ? max : nextValue;
const fixedValue = getFixedNumber(newValue, precision);
onChangeInternal(fixedValue, event);
}, [max, onChangeInternal, precision, step, value]);
const onDecrementValue = useCallback((event) => {
const nextValue = Number(value) - Number(step);
const newValue = typeof min !== 'undefined' && nextValue < min ? min : nextValue;
const fixedValue = getFixedNumber(newValue, precision);
onChangeInternal(fixedValue, event);
}, [min, onChangeInternal, precision, step, value]);
const onKeyDownInternal = useCallback((event) => {
onKeyDownExternal?.(event);
if (event.key === 'ArrowUp') {
event.preventDefault();
onIncrementValue(event);
return;
}
if (event.key === 'ArrowDown') {
event.preventDefault();
onDecrementValue(event);
}
}, [onDecrementValue, onIncrementValue, onKeyDownExternal]);
useEffect(() => {
setLocalValue(getFixedNumber((isValidValue(value) ? Number(value) : null), precision));
}, [value, precision]);
if (!visible) {
return null;
}
return (_jsx(Input, { ...restProps, ref: ref, prefix: prefix, className: classNames(`${prefixCls}-input-number`, className), value: localValue ?? '', disabled: disabled, onChange: onChangeInternal, onKeyDown: onKeyDownInternal, onBlur: onBlurInternal, suffixIcon: showButtons && (_jsxs("div", { className: `${prefixCls}-input-number__actions`, children: [_jsx("button", { className: `${prefixCls}-input-number__button ${prefixCls}-input-number__button_inc`, onClick: onIncrementValue, disabled: disabled || Number(localValue) >= Number(max), type: 'button', children: _jsx(Icon, { className: `${prefixCls}-input-number__icon`, icon: "common-expand-less" }) }), _jsx("button", { className: `${prefixCls}-input-number__button ${prefixCls}-input-number__button_desc`, onClick: onDecrementValue, disabled: disabled || Number(localValue) <= Number(min), type: 'button', children: _jsx(Icon, { className: `${prefixCls}-input-number__icon`, icon: "common-expand-more" }) })] })) }));
});
InputNumber.displayName = 'InputNumber';