* Setup for typescript-eslint 2 * Cleanup linting * Bump ui version * Exclude build folders
349 lines
9.2 KiB
TypeScript
349 lines
9.2 KiB
TypeScript
// Copyright 2017-2019 @polkadot/react-components authors & contributors
|
|
// This software may be modified and distributed under the terms
|
|
// of the Apache-2.0 license. See the LICENSE file for details.
|
|
|
|
import { BareProps, BitLength, I18nProps } from './types';
|
|
|
|
import BN from 'bn.js';
|
|
import React from 'react';
|
|
import { formatBalance, isUndefined } from '@polkadot/util';
|
|
|
|
import { classes } from './util';
|
|
import { BitLengthOption } from './constants';
|
|
import Button from './Button';
|
|
import Dropdown from './Dropdown';
|
|
import Input, { KEYS, KEYS_PRE } from './Input';
|
|
import translate from './translate';
|
|
|
|
type Props = BareProps & I18nProps & {
|
|
autoFocus?: boolean;
|
|
bitLength?: BitLength;
|
|
defaultValue?: BN | string;
|
|
help?: React.ReactNode;
|
|
isDisabled?: boolean;
|
|
isError?: boolean;
|
|
isSi?: boolean;
|
|
isDecimal?: boolean;
|
|
label?: any;
|
|
maxLength?: number;
|
|
maxValue?: BN;
|
|
onChange?: (value?: BN) => void;
|
|
onEnter?: () => void;
|
|
placeholder?: string;
|
|
value?: BN | string;
|
|
withEllipsis?: boolean;
|
|
withLabel?: boolean;
|
|
withMax?: boolean;
|
|
};
|
|
|
|
interface State {
|
|
isPreKeyDown: boolean;
|
|
isValid: boolean;
|
|
siOptions: { value: string; text: string }[];
|
|
siUnit: string;
|
|
value: string;
|
|
valueBN: BN;
|
|
}
|
|
|
|
const DEFAULT_BITLENGTH = BitLengthOption.NORMAL_NUMBERS as BitLength;
|
|
const ZERO = new BN(0);
|
|
const TEN = new BN(10);
|
|
|
|
class InputNumber extends React.PureComponent<Props, State> {
|
|
public constructor (props: Props) {
|
|
super(props);
|
|
|
|
const { defaultValue, isSi, value } = this.props;
|
|
const valueBN = new BN(value || 0);
|
|
const si = formatBalance.findSi('-');
|
|
|
|
this.state = {
|
|
value: isSi
|
|
? new BN(defaultValue || valueBN).div(new BN(10).pow(new BN(si.power))).toString()
|
|
: (defaultValue || valueBN).toString(),
|
|
isPreKeyDown: false,
|
|
isValid: !isUndefined(value),
|
|
siOptions: formatBalance.getOptions().map(({ power, text, value }): { text: string; value: string } => ({
|
|
value,
|
|
text: power === 0
|
|
? InputNumber.units
|
|
: text
|
|
})),
|
|
siUnit: si.value,
|
|
valueBN
|
|
};
|
|
}
|
|
|
|
public static units = 'Unit';
|
|
|
|
public static setUnit (units: string = InputNumber.units): void {
|
|
InputNumber.units = units;
|
|
}
|
|
|
|
public static getDerivedStateFromProps ({ isDisabled, isSi, defaultValue = '0' }: Props): Partial<State> | null {
|
|
if (!isDisabled || !isSi) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
value: formatBalance(defaultValue, false),
|
|
siUnit: formatBalance.calcSi(defaultValue.toString(), formatBalance.getDefaults().decimals).value
|
|
};
|
|
}
|
|
|
|
public render (): React.ReactNode {
|
|
const { bitLength = DEFAULT_BITLENGTH, className, help, isSi, isDisabled, isError = false, maxLength, maxValue, onEnter, placeholder, style, withMax, t } = this.props;
|
|
const { isValid, value } = this.state;
|
|
const maxValueLength = this.maxValue(bitLength).toString().length - 1;
|
|
|
|
return (
|
|
<Input
|
|
{...this.props}
|
|
className={classes('ui--InputNumber', className)}
|
|
help={help}
|
|
isAction={isSi}
|
|
isDisabled={isDisabled}
|
|
isError={!isValid || isError}
|
|
maxLength={maxLength || maxValueLength}
|
|
onChange={this.onChange}
|
|
onEnter={onEnter}
|
|
onKeyDown={this.onKeyDown}
|
|
onKeyUp={this.onKeyUp}
|
|
onPaste={this.onPaste}
|
|
placeholder={placeholder || t('Positive number')}
|
|
style={style}
|
|
type='text'
|
|
value={value}
|
|
>
|
|
{(withMax && !!maxValue) && this.renderMaxButton()}
|
|
{isSi && this.renderSiDropdown()}
|
|
</Input>
|
|
);
|
|
}
|
|
|
|
private renderSiDropdown (): React.ReactNode {
|
|
const { siOptions, siUnit } = this.state;
|
|
|
|
return (
|
|
<Dropdown
|
|
dropdownClassName='ui--SiDropdown'
|
|
isButton
|
|
defaultValue={siUnit}
|
|
onChange={this.selectSiUnit}
|
|
options={siOptions}
|
|
/>
|
|
);
|
|
}
|
|
|
|
private renderMaxButton (): React.ReactNode {
|
|
const { valueBN } = this.state;
|
|
const { maxValue } = this.props;
|
|
|
|
if (!maxValue || valueBN.eq(maxValue)) {
|
|
return;
|
|
}
|
|
|
|
return (
|
|
<Button
|
|
className='ui--MaxButton'
|
|
onClick={this.setToMaxValue}
|
|
>
|
|
Max
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
private maxValue (bitLength?: number): BN {
|
|
return new BN(2).pow(new BN(bitLength || DEFAULT_BITLENGTH)).subn(1);
|
|
}
|
|
|
|
private isValidBitLength (value: BN, bitLength?: number): boolean {
|
|
return value.bitLength() <= (bitLength || DEFAULT_BITLENGTH);
|
|
}
|
|
|
|
private isValidNumber (input: BN, bitLength: number = DEFAULT_BITLENGTH): boolean {
|
|
const { maxValue, withMax } = this.props;
|
|
const maxBN = this.maxValue(bitLength);
|
|
|
|
if (
|
|
input.lt(new BN(0)) ||
|
|
!input.lt(maxBN) ||
|
|
!this.isValidBitLength(input, bitLength) ||
|
|
(withMax && maxValue !== undefined && maxValue.gtn(0) && input.gt(maxValue))
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private regex = (): RegExp => {
|
|
const { isDecimal, isSi } = this.props;
|
|
return new RegExp(
|
|
(isSi || isDecimal)
|
|
? `^(0|[1-9]\\d*)(\\${KEYS.DECIMAL}\\d*)?$`
|
|
: '^(0|[1-9]\\d*)$'
|
|
);
|
|
}
|
|
|
|
private onChange = (value: string): void => {
|
|
const { bitLength, onChange } = this.props;
|
|
const { siUnit } = this.state;
|
|
|
|
try {
|
|
const valueBN = this.inputValueToBn(value, siUnit);
|
|
const isValid = this.isValidNumber(valueBN, bitLength);
|
|
|
|
this.setState({ isValid, value, valueBN });
|
|
|
|
onChange && onChange(
|
|
isValid
|
|
? valueBN
|
|
: undefined
|
|
);
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
}
|
|
|
|
private onKeyDown = (event: React.KeyboardEvent<Element>): void => {
|
|
const { isPreKeyDown } = this.state;
|
|
|
|
if (KEYS_PRE.includes(event.key)) {
|
|
this.setState({ isPreKeyDown: true });
|
|
return;
|
|
}
|
|
|
|
if (event.key.length === 1 && !isPreKeyDown) {
|
|
const { selectionStart: i, selectionEnd: j, value } = event.target as HTMLInputElement;
|
|
const newValue = `${value.substring(0, i || 0)}${event.key}${value.substring(j || 0)}`;
|
|
|
|
if (!this.regex().test(newValue)) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
}
|
|
|
|
private onKeyUp = (event: React.KeyboardEvent<Element>): void => {
|
|
if (KEYS_PRE.includes(event.key)) {
|
|
this.setState({ isPreKeyDown: false });
|
|
}
|
|
}
|
|
|
|
private onPaste = (event: React.ClipboardEvent<Element>): void => {
|
|
const { value: newValue } = event.target as HTMLInputElement;
|
|
|
|
if (!this.regex().test(newValue)) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
private selectSiUnit = (siUnit: string): void => {
|
|
this.setState((prevState: State): Pick<State, never> => {
|
|
const { bitLength, onChange } = this.props;
|
|
const valueBN = this.inputValueToBn(prevState.value, siUnit);
|
|
const isValid = this.isValidNumber(valueBN, bitLength);
|
|
|
|
onChange && onChange(
|
|
isValid
|
|
? prevState.valueBN
|
|
: undefined
|
|
);
|
|
|
|
return {
|
|
isValid,
|
|
siUnit,
|
|
valueBN
|
|
};
|
|
});
|
|
}
|
|
|
|
private setToMaxValue = (): void => {
|
|
this.setState((prevState: State): Pick<State, never> => {
|
|
const { bitLength, maxValue, onChange } = this.props;
|
|
const { siUnit = prevState.siUnit } = this.state;
|
|
|
|
if (!maxValue) {
|
|
return {};
|
|
}
|
|
|
|
const isValid = this.isValidNumber(maxValue, bitLength);
|
|
|
|
onChange && onChange(
|
|
isValid
|
|
? maxValue
|
|
: undefined
|
|
);
|
|
|
|
return {
|
|
value: this.bnToInputValue(maxValue, siUnit),
|
|
valueBN: maxValue,
|
|
isValid
|
|
};
|
|
});
|
|
}
|
|
|
|
private inputValueToBn = (value: string, siUnit?: string): BN => {
|
|
const [siPower, basePower, siUnitPower] = this.getSiPowers(siUnit);
|
|
|
|
// eslint-disable-next-line @typescript-eslint/prefer-regexp-exec
|
|
const isDecimalValue = value.match(/^(\d+)\.(\d+)$/);
|
|
|
|
if (isDecimalValue) {
|
|
if (siUnitPower - isDecimalValue[2].length < -basePower) {
|
|
return new BN(-1);
|
|
}
|
|
|
|
const div = new BN(value.replace(/\.\d*$/, ''));
|
|
const modString = value.replace(/^\d+\./, '');
|
|
const mod = new BN(modString);
|
|
|
|
return div
|
|
.mul(TEN.pow(siPower))
|
|
.add(mod.mul(TEN.pow(new BN(basePower + siUnitPower - modString.length))));
|
|
} else {
|
|
return new BN(value.replace(/[^\d]/g, ''))
|
|
.mul(TEN.pow(siPower));
|
|
}
|
|
}
|
|
|
|
private bnToInputValue = (bn: BN, siUnit?: string): string => {
|
|
const [siPower] = this.getSiPowers(siUnit);
|
|
|
|
const base = TEN.pow(siPower);
|
|
const div = bn.div(base);
|
|
const mod = bn.mod(base);
|
|
|
|
return `${
|
|
div.gt(ZERO) ? div.toString() : '0'
|
|
}${
|
|
mod.gt(ZERO)
|
|
? ((): string => {
|
|
const padding = Math.max(
|
|
mod.toString().length,
|
|
base.toString().length - div.toString().length,
|
|
bn.toString().length - div.toString().length
|
|
);
|
|
|
|
return `.${mod.toString(10, padding).replace(/0*$/, '')}`;
|
|
})()
|
|
: ''
|
|
}`;
|
|
}
|
|
|
|
private getSiPowers = (siUnit = this.state.siUnit): [BN, number, number] => {
|
|
const { isSi } = this.props;
|
|
|
|
const basePower = isSi ? formatBalance.getDefaults().decimals : 0;
|
|
const siUnitPower = isSi ? formatBalance.findSi(siUnit).power : 0;
|
|
|
|
return [new BN(basePower + siUnitPower), basePower, siUnitPower];
|
|
}
|
|
}
|
|
|
|
export {
|
|
InputNumber
|
|
};
|
|
|
|
export default translate(InputNumber);
|