ghost-portal/packages/ui-app/src/InputNumber.tsx

352 lines
9.0 KiB
TypeScript

// Copyright 2017-2019 @polkadot/ui-app 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
};
type State = {
isPreKeyDown: boolean,
isValid: boolean,
siOptions: Array<{ 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> {
constructor (props: Props) {
super(props);
const { defaultValue, isSi, value } = this.props;
let 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 }) => ({
value,
text: power === 0
? InputNumber.units
: text
})),
siUnit: si.value,
valueBN
};
}
static units: string = 'Unit';
static setUnit (units: string = InputNumber.units): void {
InputNumber.units = units;
}
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
};
}
render () {
const { bitLength = DEFAULT_BITLENGTH, className, help, isSi, isDisabled, isError = false, maxLength, maxValue, onEnter, 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={t('Positive number')}
style={style}
type='text'
value={value}
>
{(withMax && !!maxValue) && this.renderMaxButton()}
{isSi && this.renderSiDropdown()}
</Input>
);
}
private renderSiDropdown () {
const { siOptions, siUnit } = this.state;
return (
<Dropdown
dropdownClassName='ui--SiDropdown'
isButton
defaultValue={siUnit}
onChange={this.selectSiUnit}
options={siOptions}
/>
);
}
private renderMaxButton () {
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!)
}${
event.key
}${
value.substring(j!)
}`;
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();
return;
}
}
private selectSiUnit = (siUnit: string): void => {
this.setState((prevState: State) => {
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 = () => {
this.setState((prevState: State) => {
const { bitLength, maxValue, onChange } = this.props;
const { siUnit = prevState.siUnit } = this.state;
if (!maxValue) {
return {} as State;
}
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);
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 mod = new BN(value.replace(/^\d+\./, ''));
return div
.mul(TEN.pow(siPower))
.add(mod.mul(TEN.pow(new BN(basePower + siUnitPower - mod.toString().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) ?
(() => {
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);