/** * The number input element * * @author Platon Fedorov * @date 2020-01-14 */ import * as React from 'react'; import {Icon, Input, joinClassNames, safeInvoke, SIZE} from '../../index'; import {Props, SizeClassNames} from './Input.types'; import * as styles from './input.m.scss'; import {getSizeThemeKey} from '../../utils/getSizeThemeKey'; export type IInputNumber = Omit & { isInteger?: boolean; isOnlyNegative?: boolean; isOnlyPositive?: boolean; min?: number; max?: number; step?: number; value?: number | string; defaultValue?: number | string; parser?: (displayValue: string | undefined) => string; formatter?: (value: number | string | undefined) => string; /** * Code works correct with a separator consisting of a single symbol */ decimalSeparator?: DecimalSeparator; precision?: number; onChange?: (value: number | undefined) => void; maxErrorMessage?: string; minErrorMessage?: string; } type DefaultPropsKeys = 'min' | 'max' | 'step'; export type DecimalSeparator = ',' | '.'; export type DefaultProps = Required>; export type PropsWithDefault = IInputNumber & DefaultProps; type IState = { value: number; displayValue: string; showValue: boolean; } export class InputNumber extends React.Component { static defaultProps: DefaultProps = { min: Number.MIN_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER, step: 1 } override state = this.initState(); inputRef = React.createRef(); initState (): IState { const {value, defaultValue} = this.props; return { value: defaultValue !== undefined ? Number(defaultValue) : Number(value), displayValue: defaultValue !== undefined ? defaultValue.toString() : (value?.toString() || ''), showValue: defaultValue === undefined }; } createRef () { if (this.props.forwardRef) { this.inputRef = this.props.forwardRef; } return this.inputRef; } isValuePossible = (value: string): boolean => { value = this.disableSeparator(value); if (value === '' || value === '-') { return true; } const minusIndex = value.indexOf('-', 1), expCount = value.match(/e/g), dotCount = value.match(/\./g); if (minusIndex !== -1 && value[minusIndex - 1] !== 'e' || expCount && expCount.length > 1 || dotCount && dotCount.length > 1 || value.indexOf('e.') !== -1 ) { return false; } return true; }; onKeyPress = (e: React.KeyboardEvent) => { const possibleSymbols = /(e|-|\.|\d)/; let toForcePrevent = !possibleSymbols.test(e.key); if (this.props.decimalSeparator) { toForcePrevent = toForcePrevent && (e.key !== this.props.decimalSeparator); } if (!toForcePrevent && this.inputRef.current && this.inputRef.current.selectionStart !== null) { const oldValue = this.inputRef.current.value.split(''); oldValue.splice(this.inputRef.current.selectionStart, 0, e.key); const newValue = this.parser(oldValue.join('')); toForcePrevent = toForcePrevent || !this.isValuePossible(newValue); if (e.key === '-' && newValue[0] === '-' && this.props.isOnlyPositive) { toForcePrevent = true; } } if (toForcePrevent || this.props.isInteger && (e.key === '.' || e.key === 'e')) { e.preventDefault(); } safeInvoke(this.props.onKeyPress, e); } getMinMaxErrorMessage (hasMinError: boolean, hasMaxError: boolean) { const {minErrorMessage, maxErrorMessage} = this.props; if (hasMinError && minErrorMessage !== undefined) { return minErrorMessage; } if (hasMaxError && maxErrorMessage !== undefined) { return maxErrorMessage; } return undefined; } hasMinError = (value: number) => { let {min} = this.props as PropsWithDefault; if (this.props.isOnlyPositive) { min = Math.max(min, 0); } return value < min; } hasMaxError = (value: number) => { let {max} = this.props as PropsWithDefault; if (this.props.isOnlyNegative) { max = Math.min(max, 0); } return value > max; } escapeRegExp = (str: string) => { return str.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); } replaceAll = (str: string, find: string, replace: string) => { return str.replace(new RegExp(this.escapeRegExp(find), 'g'), replace); } enableSeparator = (value: string):string => { if (this.props.decimalSeparator) { return this.replaceAll(value, '.', this.props.decimalSeparator); } else { return value; } } disableSeparator = (value: string):string => { if (this.props.decimalSeparator) { return this.replaceAll(value, this.props.decimalSeparator, '.'); } else { return value; } } formatter = (value: string | number | undefined): string => { if (this.props.formatter) { return this.props.formatter(value); } return value ? value.toString() : ''; } parser = (displayValue: string | undefined): string => { if (this.props.parser) { return this.props.parser(displayValue); } return displayValue ? displayValue : ''; } getDisplayValueFromNumber = (value: number, toPrecision: boolean = true) => { let result = value.toString(); if (this.props.precision && toPrecision) { result = value.toFixed(this.props.precision); } return this.formatter(this.enableSeparator(result)); } getDisplayValueFromString = (value: string) => { return this.formatter(this.enableSeparator(value)); } onIncrease = () => { const {step, value} = this.props as PropsWithDefault; const stateValue = this.state.value ? this.state.value : 0; const newValue = (value !== undefined ? Number(value) : stateValue) + step; if (this.props.isOnlyNegative && newValue > 0) { return; } this.setNewValue(newValue); }; onDecrease = () => { const {step, value} = this.props as PropsWithDefault; const stateValue = this.state.value ? this.state.value : 0; const newValue = (value !== undefined ? Number(value) : stateValue) - step; if (this.props.isOnlyPositive && newValue < 0) { return; } this.setNewValue(newValue); }; setNewValue (newValue: number, newDisplayValue?: string) { this.setState({ value: newValue, showValue: true, displayValue: newDisplayValue !== undefined ? this.getDisplayValueFromString(newDisplayValue) : this.getDisplayValueFromNumber(newValue) }); safeInvoke(this.props.onChange, newValue); } onChange = (e: React.ChangeEvent) => { if (e.currentTarget.value === '') { this.setState({ showValue: true, value: NaN, displayValue: this.getDisplayValueFromString('') }); safeInvoke(this.props.onChange, undefined); } else { let newDisplayValue = this.disableSeparator(this.parser(e.currentTarget.value)); let newValue = Number(newDisplayValue); const oldValue = (this.props.value !== undefined && this.props.value !== '') ? Number(this.props.value) : this.state.value; if (!Number.isNaN(newValue) && oldValue !== newValue) { if (this.props.isOnlyNegative && newValue > 0) { newValue = -newValue; newDisplayValue = '-' + newDisplayValue; } this.setNewValue(newValue, newDisplayValue); } else { this.setState({ showValue: false, displayValue: this.getDisplayValueFromString(newDisplayValue) }); } } } onBlur = (e: React.FocusEvent) => { let value = this.state.value; if (this.props.value !== undefined && !Number.isNaN(this.props.value) && this.props.value !== '') { value = Number(this.props.value); } this.setState({ displayValue: !Number.isNaN(value) ? this.getDisplayValueFromNumber(value) : '', showValue: false }); safeInvoke(this.props.onBlur, e); } override render () { const { min, max, type, step, value, onBlur, parser, onChange, formatter, precision, isInteger, onKeyPress, forwardRef, defaultValue, isOnlyNegative, isOnlyPositive, decimalSeparator, size = SIZE.MIDDLE, hasError, errorMessage, ...props} = this.props; const sizeThemeKey = getSizeThemeKey('', size); const isDisabled = this.props.readOnly || this.props.isDisabled; const onIncrease = isDisabled ? undefined : this.onIncrease; const onDecrease = isDisabled ? undefined : this.onDecrease; let displayValue = this.state.displayValue; if (value !== undefined && (this.state.showValue || defaultValue === undefined)) { if (typeof value === 'number') { if (value !== Number(displayValue)) { displayValue = this.getDisplayValueFromNumber(value, false); } } else { displayValue = this.getDisplayValueFromString(value); } } const containerClassName = joinClassNames( styles.incrementArrowsContainer, styles[sizeThemeKey], [styles.disabled, Boolean(this.props.isDisabled)] ); const numberedDisplayValue = Number(displayValue); const hasMinError = displayValue === '' ? false : this.hasMinError(numberedDisplayValue); const hasMaxError = displayValue === '' ? false : this.hasMaxError(numberedDisplayValue); return (
) } /> ); } }