/
kaws
/
ui-kit-ce
Обзор
Документация
Войти
/
kaws
/
ui-kit-ce
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
packages/input-number/src/InputNumber.tsx
442 строки
14 KB
zavyalov.d.o
refactor: устранение eslint warnings
06 мар 2025, 00:50
06 мар 2025, 00:50
9490b1b
Код
Авторство
О чём код?
import * as React from 'react' import { isEqualKeyboardKeys, warning } from '@v-uik/utils' import { Input, InputChangeReason, InputProps, InputChangeEvent, } from '@v-uik/input' import { useConverter, useMergedRefs, useUncontrolledValue } from '@v-uik/hooks' import { useInputValue } from './hooks/useInputValue' import { createList, formatPastedValue, Item, filterPredicate } from './utils' import { NumberValue, ISelection, IParts } from './types' import { DATA_V_UIK_INPUT_TYPE } from '@v-uik/common' export type Props< TCanClear extends boolean | unknown = false, TValueType extends 'string' | 'number' | unknown = 'number' > = { /** * Число знаков после запятой. * * @inner */ precision?: number /** * Десятичный разделитель. * * @inner */ decimalSeparator?: string /** * Разделитель групп. * * @inner */ groupSeparator?: string /** * Тип возвращаемого и передаваемого значения. * * @inner */ valueType?: TValueType | 'string' | 'number' /** * Значение поля ввода. * * @inner */ value?: TValueType extends 'string' ? string | null : number | null /** * Обработчик события изменения. <br/>. * * @param `value` Измененное значение. <br/>. * @param `event` - Инициатор события из обработчика. Имеет или тип `React.ChangeEvent<HTMLInputElement>`, если флаг `canClear` равен `false` <br/> * или тип `InputChangeEvent`, если флаг `canClear` равен `true` <br/>. * @param `reason` Тип изменения значения поля: `input` - пользовательский ввод, `clear` - очищение поля с помощью кнопки очистки. * * @returns */ onChange?: TCanClear extends true ? ( value: TValueType extends 'string' ? string | null : number | null, event: InputChangeEvent, reason?: InputChangeReason ) => void : ( value: TValueType extends 'string' ? string | null : number | null, event: React.ChangeEvent<HTMLInputElement>, reason?: InputChangeReason ) => void } & Omit<InputProps<TCanClear>, 'value' | 'onChange'> const NEGATIVE_SIGN = '-' const GROUP_SIZE = 3 const _InputNumber = React.forwardRef( < TCanClear extends boolean | unknown = false, TValueType extends 'string' | 'number' | unknown = 'number' >( { precision = 2, groupSeparator = ' ', decimalSeparator = ',', inputRef: inputRefProp = null, onKeyDownCapture, valueType = 'number' as TValueType, value: valueProp, onChange, onPaste: onPasteProps, defaultValue, ...rest }: Props<TCanClear, TValueType>, ref: React.Ref<HTMLDivElement> ) => { const { toString, toNumber, getFormattedString } = useConverter({ precision, groupSeparator, decimalSeparator, }) const inputRef = React.useRef<HTMLInputElement | null>(null) const mergedInputRef = useMergedRefs([inputRefProp, inputRef]) const keyBefore = React.useRef<string | null>(null) const wasPasteEvent = React.useRef<boolean>(false) /** * Функция, возвращающая форматированное значение. */ const toFormattedString = React.useCallback( (value: string | number | null | undefined): string => { if ( (!value && value !== 0) || isNaN(Number.parseInt(String(value), 10)) ) { return '' } if (valueType === 'number') { // Обработка кейса с учётом отрицательного нуля. // Проверка осуществляется средством сравнения с Number.NEGATIVE_INFINITY, // т.к. даже строгое сравнение даёт ложно положительный результат // 0 === -0 // true const isNegativeZero = value === 0 ? 1 / value === Number.NEGATIVE_INFINITY : false return getFormattedString(String(value), isNegativeZero) } if (typeof value === 'string') { return getFormattedString(value, value === '-0') } warning( false, 'InputNumber', 'тип value не соответсвует значению указанному в valueType' ) return '' }, [valueType, getFormattedString] ) const { handleChange: handleUncontrolledChange, value } = useUncontrolledValue({ value: valueProp, defaultValue: defaultValue as TValueType extends 'string' ? string | null : number | null, onChange, }) const [inputValue, setInputValue, onInputBlur] = useInputValue( value, toFormattedString, toString ) const [selection, setSelection] = React.useState<ISelection | null>(null) const handleKeyDownCapture = (e: React.KeyboardEvent<HTMLInputElement>) => { onKeyDownCapture?.(e) keyBefore.current = e.key } const handleChange = ( newValueArg: string, e: React.ChangeEvent<HTMLInputElement> | React.MouseEvent<HTMLDivElement>, reason: InputChangeReason = 'input' ) => { const { selectionStart } = (e as React.ChangeEvent<HTMLInputElement>) .currentTarget let newValue = newValueArg const isDecimalRepeating = Number( newValue.match(new RegExp(`\\${decimalSeparator}`, 'g'))?.length ) > 1 if (isDecimalRepeating) { return } if (wasPasteEvent.current) { newValue = formatPastedValue({ value: newValue, groupSeparator, decimalSeparator, }) } const list = createList(newValue.split(''), selectionStart) const negativeSign = newValue.startsWith(NEGATIVE_SIGN) ? list[0] : null wasPasteEvent.current = false const parts: IParts = { negativeSign, integer: list, fraction: [], separator: null, } /* ---------------------------------------------------------------- */ /* -----------------Позиция десятичного разделителя---------------- */ /* ---------------------------------------------------------------- */ if ( newValue.includes(decimalSeparator) && newValue !== decimalSeparator ) { const indexFirst = newValue.indexOf(decimalSeparator) const indexLast = newValue.lastIndexOf(decimalSeparator) const indexPrevious = inputValue.indexOf(decimalSeparator) let indexOfSeparator = indexFirst // Если позиция первого разделителя не равна позиции последнего, // значит новый разделитель(-и) были добавлены в строку. if (indexFirst !== indexLast && indexPrevious === indexFirst) { indexOfSeparator = indexLast } parts.integer = list.slice(0, indexOfSeparator) parts.fraction = list.slice(indexOfSeparator + 1) parts.separator = precision ? list[indexOfSeparator] : null } /* ---------------------------------------------------------------- */ /* --------------------Фильтрация целой части---------------------- */ /* ---------------------------------------------------------------- */ parts.integer = parts.integer.filter(filterPredicate) /* ---------------------------------------------------------------- */ /* -----------------Добавление разделителей групп------------------ */ /* ---------------------------------------------------------------- */ if (groupSeparator && parts.integer.length > GROUP_SIZE) { const to = parts.integer.length % GROUP_SIZE const from = parts.integer.length - GROUP_SIZE for (let i = from; i >= to; i -= GROUP_SIZE) { if (i === 0) { break } const item: Item = { value: groupSeparator, hasCaret: false, neighborRight: parts.integer[i], neighborLeft: parts.integer[i].neighborLeft, } if ( keyBefore.current && isEqualKeyboardKeys('Backspace', keyBefore.current) ) { if (parts.integer[i].hasCaret) { parts.integer[i].hasCaret = false item.hasCaret = true } } parts.integer.splice(i, 0, item) } } /* ---------------------------------------------------------------- */ /* ------------------Фильтрация десятичной части------------------- */ /* --------------------Обрезание дробной части--------------------- */ /* ---------------------------------------------------------------- */ if (parts.fraction.length) { parts.fraction = parts.fraction.filter(filterPredicate) if (precision !== null && precision !== 0) { parts.fraction = parts.fraction.slice(0, precision) if (parts.fraction.length) { parts.fraction[parts.fraction.length - 1].neighborRight = null } } if (!precision) { parts.fraction = [] } } const partsLength = parts.integer.length + parts.fraction.length + Number(!!parts.negativeSign) + Number(!!parts.separator) const { newInputValue, caretPosition } = [ parts.negativeSign, ...parts.integer, parts.separator, ...parts.fraction, ] .filter((item) => item) .reduce<ISelection>( (acc, item, index) => { acc.caretPosition = item?.hasCaret ? index : acc.caretPosition acc.newInputValue = `${acc.newInputValue ?? ''}${item?.value ?? ''}` return acc }, { newInputValue: '', caretPosition: selectionStart ? selectionStart + partsLength - list.length : partsLength, } ) if ( valueType === 'number' && (toNumber(newInputValue ?? '') > Number.MAX_SAFE_INTEGER || toNumber(newInputValue ?? '') < Number.MIN_SAFE_INTEGER) ) { warning( false, 'InputNumber', `Значение вышло за пределы Number.MAX_SAFE_INTEGER или Number.MIN_SAFE_INTEGER. Используйте valueType='string'` ) return } setInputValue(newInputValue ?? '') setSelection({ caretPosition }) if (valueType === 'string') { const asString = toString(newInputValue ?? '') as string if (asString !== value) { handleUncontrolledChange( asString as TValueType extends 'string' ? string | null : number | null, e as React.ChangeEvent<HTMLInputElement>, reason ) } return } if (valueType === 'number') { const asNumber = toNumber(newInputValue ?? '') if (asNumber === 0 && asNumber === value) { const isNegativeZero = 1 / (value as number) === Number.NEGATIVE_INFINITY const isPositiveZero = 1 / asNumber === Number.POSITIVE_INFINITY const a = isNegativeZero && isPositiveZero const b = !isNegativeZero && !isPositiveZero if (a || b) { handleUncontrolledChange( asNumber as TValueType extends 'string' ? string | null : number | null, e as React.ChangeEvent<HTMLInputElement>, reason ) } return } if (asNumber !== value) { ;(handleUncontrolledChange as NumberValue<TCanClear>['onChange'])?.( Number.isNaN(asNumber) ? null : asNumber, e as React.ChangeEvent<HTMLInputElement>, reason ) } } } React.useEffect(() => { if (inputRef.current && selection) { inputRef.current.setSelectionRange( selection.caretPosition, selection.caretPosition ) } }, [selection]) const onPaste = React.useCallback( (e) => { wasPasteEvent.current = true onPasteProps?.(e) }, [onPasteProps] ) const handleBlur = (e: React.FocusEvent<HTMLDivElement, Element>) => { rest.onBlur?.(e) onInputBlur() } const handleFocus = (e: React.FocusEvent<HTMLDivElement, Element>) => { rest.onFocus?.(e) } return ( <Input {...rest} ref={ref} inputProps={{ //@ts-ignore Компонент корректно принимает data-атрибуты [DATA_V_UIK_INPUT_TYPE]: 'number', ...rest.inputProps, }} inputRef={mergedInputRef} value={inputValue} onPaste={onPaste} onChange={handleChange as InputProps<TCanClear>['onChange']} onKeyDownCapture={handleKeyDownCapture} onFocus={handleFocus} onBlur={handleBlur} /> ) } ) export const InputNumber = _InputNumber as < TCanClear extends boolean | unknown = false, TValueType extends 'string' | 'number' | unknown = 'number' >( props: Props<TCanClear, TValueType> & { /** * */ ref?: React.Ref<HTMLDivElement> } ) => React.ReactElement