|
@@ -11,16 +11,18 @@ import { formatBalance, isUndefined } from '@polkadot/util';
|
|
|
import { classes } from './util';
|
|
|
import { BitLengthOption } from './constants';
|
|
|
import Dropdown from './Dropdown';
|
|
|
-import Input, { KEYS, KEYS_PRE, isCopy, isCut, isPaste, isSelectAll } from './Input';
|
|
|
+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,
|
|
|
onChange?: (value?: BN) => void,
|
|
@@ -30,16 +32,15 @@ type Props = BareProps & I18nProps & {
|
|
|
};
|
|
|
|
|
|
type State = {
|
|
|
- defaultValue?: string,
|
|
|
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 KEYS_ALLOWED: Array<any> = [KEYS.ARROW_LEFT, KEYS.ARROW_RIGHT, KEYS.BACKSPACE, KEYS.ENTER, KEYS.ESCAPE, KEYS.TAB];
|
|
|
|
|
|
class InputNumber extends React.PureComponent<Props, State> {
|
|
|
constructor (props: Props) {
|
|
@@ -47,10 +48,10 @@ class InputNumber extends React.PureComponent<Props, State> {
|
|
|
|
|
|
const { defaultValue, isSi, value } = this.props;
|
|
|
let valueBN = new BN(value || 0);
|
|
|
- const si = formatBalance.calcSi(valueBN.toString());
|
|
|
+ const si = formatBalance.findSi('-');
|
|
|
|
|
|
this.state = {
|
|
|
- defaultValue: isSi
|
|
|
+ value: isSi
|
|
|
? new BN(defaultValue || valueBN).div(new BN(10).pow(new BN(si.power))).toString()
|
|
|
: (defaultValue || valueBN).toString(),
|
|
|
isPreKeyDown: false,
|
|
@@ -71,32 +72,27 @@ class InputNumber extends React.PureComponent<Props, State> {
|
|
|
InputNumber.units = units;
|
|
|
}
|
|
|
|
|
|
- static getDerivedStateFromProps ({ isDisabled, isSi, defaultValue = '0' }: Props, state: State): Partial<State> | null {
|
|
|
+ static getDerivedStateFromProps ({ isDisabled, isSi, defaultValue = '0' }: Props): Partial<State> | null {
|
|
|
if (!isDisabled || !isSi) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
- defaultValue: formatBalance(defaultValue, false),
|
|
|
+ value: formatBalance(defaultValue, false),
|
|
|
siUnit: formatBalance.calcSi(defaultValue.toString(), formatBalance.getDefaults().decimals).value
|
|
|
};
|
|
|
}
|
|
|
|
|
|
render () {
|
|
|
- const { bitLength = DEFAULT_BITLENGTH, className, defaultValue = '0', isSi, isDisabled, maxLength, style, t } = this.props;
|
|
|
- const { isValid } = this.state;
|
|
|
+ const { bitLength = DEFAULT_BITLENGTH, className, help, isSi, isDisabled, maxLength, style, t } = this.props;
|
|
|
+ const { isValid, value } = this.state;
|
|
|
const maxValueLength = this.maxValue(bitLength).toString().length - 1;
|
|
|
- const value = this.state.defaultValue || defaultValue;
|
|
|
|
|
|
return (
|
|
|
<Input
|
|
|
{...this.props}
|
|
|
className={classes('ui--InputNumber', className)}
|
|
|
- defaultValue={
|
|
|
- isDisabled
|
|
|
- ? undefined
|
|
|
- : value
|
|
|
- }
|
|
|
+ help={help}
|
|
|
isAction={isSi}
|
|
|
isDisabled={isDisabled}
|
|
|
isError={!isValid}
|
|
@@ -104,28 +100,20 @@ class InputNumber extends React.PureComponent<Props, State> {
|
|
|
onChange={this.onChange}
|
|
|
onKeyDown={this.onKeyDown}
|
|
|
onKeyUp={this.onKeyUp}
|
|
|
+ onPaste={this.onPaste}
|
|
|
placeholder={t('Positive number')}
|
|
|
style={style}
|
|
|
- value={
|
|
|
- isDisabled
|
|
|
- ? value
|
|
|
- : undefined
|
|
|
- }
|
|
|
+ value={value}
|
|
|
type='text'
|
|
|
>
|
|
|
- {this.renderSiDropdown()}
|
|
|
+ {isSi && this.renderSiDropdown()}
|
|
|
</Input>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
private renderSiDropdown () {
|
|
|
- const { isSi } = this.props;
|
|
|
const { siOptions, siUnit } = this.state;
|
|
|
|
|
|
- if (!isSi) {
|
|
|
- return undefined;
|
|
|
- }
|
|
|
-
|
|
|
return (
|
|
|
<Dropdown
|
|
|
isPrimary={false}
|
|
@@ -145,51 +133,33 @@ class InputNumber extends React.PureComponent<Props, State> {
|
|
|
return value.bitLength() <= (bitLength || DEFAULT_BITLENGTH);
|
|
|
}
|
|
|
|
|
|
- private isValidKey = (event: React.KeyboardEvent<Element>, isPreKeyDown: boolean): boolean => {
|
|
|
- const { value: previousValue } = event.target as HTMLInputElement;
|
|
|
- // prevents entry of zero if initial digit is zero
|
|
|
- const isDuplicateZero = previousValue[0] === '0' && event.key === KEYS.ZERO;
|
|
|
-
|
|
|
- if (isDuplicateZero) {
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- // allow cut/copy/paste combinations but not non-numeric letters (i.e. a, c, x, v) individually
|
|
|
- if (
|
|
|
- (isSelectAll(event.key, isPreKeyDown)) ||
|
|
|
- (isCut(event.key, isPreKeyDown)) ||
|
|
|
- (isCopy(event.key, isPreKeyDown)) ||
|
|
|
- (isPaste(event.key, isPreKeyDown))
|
|
|
- ) {
|
|
|
- return true;
|
|
|
- }
|
|
|
-
|
|
|
- if (isNaN(Number(event.key)) && !KEYS_ALLOWED.includes(event.key)) {
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- return true;
|
|
|
- }
|
|
|
-
|
|
|
private isValidNumber (input: BN, bitLength: number = DEFAULT_BITLENGTH): boolean {
|
|
|
const maxBN = this.maxValue(bitLength);
|
|
|
-
|
|
|
- if (!input.lt(maxBN) || !this.isValidBitLength(input, bitLength)) {
|
|
|
+ if (input.lt(new BN(0)) || !input.lt(maxBN) || !this.isValidBitLength(input, bitLength)) {
|
|
|
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.applySi(siUnit, new BN(value || 0));
|
|
|
+ const valueBN = this.inputValueToBn(value, siUnit);
|
|
|
const isValid = this.isValidNumber(valueBN, bitLength);
|
|
|
|
|
|
- this.setState({ isValid, valueBN });
|
|
|
+ this.setState({ isValid, value, valueBN });
|
|
|
|
|
|
onChange && onChange(
|
|
|
isValid
|
|
@@ -206,79 +176,110 @@ class InputNumber extends React.PureComponent<Props, State> {
|
|
|
|
|
|
if (KEYS_PRE.includes(event.key)) {
|
|
|
this.setState({ isPreKeyDown: true });
|
|
|
+ return;
|
|
|
}
|
|
|
|
|
|
- // restrict input of certain keys
|
|
|
- const isValid = this.isValidKey(event, isPreKeyDown);
|
|
|
-
|
|
|
- if (!isValid) {
|
|
|
- event.preventDefault();
|
|
|
+ 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 => {
|
|
|
- const { value: newValue } = event.target as HTMLInputElement;
|
|
|
- const isNewValueZero = new BN(newValue).isZero();
|
|
|
-
|
|
|
if (KEYS_PRE.includes(event.key)) {
|
|
|
this.setState({ isPreKeyDown: false });
|
|
|
}
|
|
|
-
|
|
|
- /* if new value equates to '0' in BN when but it's length is >=1 (i.e. '012', '00', etc)
|
|
|
- * then replace the input value with just '0'.
|
|
|
- * otherwise remove the preceding zeros from the new value (i.e. '0123' -> '123')
|
|
|
- * note: edge case glitch occurs if existing value is '0' and you 'hold down' and keep
|
|
|
- * pasting a value of '00' after it, then sometimes when you let go the
|
|
|
- * remaining value shown as '000' or '00000' in the UI, but it's still ok because
|
|
|
- * the actual BN if the user submitted would still be '0', and if they then press any key
|
|
|
- * the UI input value resets to '0'
|
|
|
- */
|
|
|
- if (isNewValueZero && newValue.length >= 1) {
|
|
|
- (event.target as HTMLInputElement).value = '0';
|
|
|
- } else {
|
|
|
- (event.target as HTMLInputElement).value = newValue.replace(/^0+/, '');
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
- private applySi (siUnit: string, value: BN): BN {
|
|
|
- const { isSi } = this.props;
|
|
|
+ private onPaste = (event: React.ClipboardEvent<Element>): void => {
|
|
|
+ const { value: newValue } = event.target as HTMLInputElement;
|
|
|
|
|
|
- if (!isSi) {
|
|
|
- return value;
|
|
|
+ if (!this.regex().test(newValue)) {
|
|
|
+ event.preventDefault();
|
|
|
+ return;
|
|
|
}
|
|
|
-
|
|
|
- const si = formatBalance.findSi(siUnit);
|
|
|
- const power = new BN(formatBalance.getDefaults().decimals + si.power);
|
|
|
-
|
|
|
- return value.mul(new BN(10).pow(power));
|
|
|
- }
|
|
|
-
|
|
|
- private applyNewSi (oldSi: string, newSi: string, value: BN): BN {
|
|
|
- const si = formatBalance.findSi(oldSi);
|
|
|
- const power = new BN(formatBalance.getDefaults().decimals + si.power);
|
|
|
-
|
|
|
- return this.applySi(newSi, value.div(new BN(10).pow(power)));
|
|
|
}
|
|
|
|
|
|
private selectSiUnit = (siUnit: string): void => {
|
|
|
this.setState((prevState: State) => {
|
|
|
const { bitLength, onChange } = this.props;
|
|
|
- const valueBN = this.applyNewSi(prevState.siUnit, siUnit, prevState.valueBN);
|
|
|
- const isValid = this.isValidNumber(valueBN, bitLength);
|
|
|
+ const isValid = this.isValidNumber(prevState.valueBN, bitLength);
|
|
|
+ const value = this.bnToInputValue(prevState.valueBN, siUnit);
|
|
|
|
|
|
onChange && onChange(
|
|
|
isValid
|
|
|
- ? valueBN
|
|
|
+ ? prevState.valueBN
|
|
|
: undefined
|
|
|
);
|
|
|
|
|
|
return {
|
|
|
isValid,
|
|
|
siUnit,
|
|
|
- valueBN
|
|
|
+ value
|
|
|
};
|
|
|
});
|
|
|
}
|
|
|
+
|
|
|
+ private inputValueToBn = (value: string, siUnit: string): BN => {
|
|
|
+ const { isSi } = this.props;
|
|
|
+ const basePower = isSi ? formatBalance.getDefaults().decimals : 0;
|
|
|
+ const siPower = isSi ? formatBalance.findSi(siUnit).power : 0;
|
|
|
+
|
|
|
+ const isDecimalValue = value.match(/^(\d+)\.(\d+)$/);
|
|
|
+
|
|
|
+ if (isDecimalValue) {
|
|
|
+ if (siPower - 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(new BN(10).pow(new BN(basePower + siPower)))
|
|
|
+ .add(mod.mul(new BN(10).pow((new BN(basePower + siPower - mod.toString().length)))));
|
|
|
+ } else {
|
|
|
+ return new BN(value.replace(/[^\d]/g, ''))
|
|
|
+ .mul(new BN(10).pow(new BN(basePower + siPower)));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private bnToInputValue = (bn: BN, siUnit: string): string => {
|
|
|
+ const { isSi } = this.props;
|
|
|
+
|
|
|
+ const basePower = isSi ? formatBalance.getDefaults().decimals : 0;
|
|
|
+ const siPower = isSi ? formatBalance.findSi(siUnit).power : 0;
|
|
|
+
|
|
|
+ const base = new BN(10).pow(new BN(basePower + siPower));
|
|
|
+ const zero = new BN(0);
|
|
|
+ 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*$/, '')}`;
|
|
|
+ })() :
|
|
|
+ ''
|
|
|
+ }`;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
export {
|