// 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 { SiDef } from '@polkadot/util/types'; import { BareProps, BitLength, I18nProps } from './types'; import BN from 'bn.js'; import React, { useEffect, useState } from 'react'; import { formatBalance } 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'; // const ALLOW_MAX = false; interface Props extends BareProps, I18nProps { autoFocus?: boolean; bitLength?: BitLength; defaultValue?: BN | string; help?: React.ReactNode; isDisabled?: boolean; isError?: boolean; isSi?: boolean; isDecimal?: boolean; isZeroable?: boolean; label?: React.ReactNode; labelExtra?: React.ReactNode; maxLength?: number; maxValue?: BN; onChange?: (value?: BN) => void; onEnter?: () => void; placeholder?: string; value?: BN | string; withEllipsis?: boolean; withLabel?: boolean; withMax?: boolean; } const DEFAULT_BITLENGTH = BitLengthOption.NORMAL_NUMBERS as BitLength; const ZERO = new BN(0); const TEN = new BN(10); export class TokenUnit { public static abbr = 'Unit'; public static setAbbr (abbr: string = TokenUnit.abbr): void { TokenUnit.abbr = abbr; } } function getGlobalMaxValue (bitLength?: number): BN { return new BN(2).pow(new BN(bitLength || DEFAULT_BITLENGTH)).subn(1); } function getRegex (isDecimal: boolean): RegExp { return new RegExp( isDecimal ? `^(0|[1-9]\\d*)(\\${KEYS.DECIMAL}\\d*)?$` : '^(0|[1-9]\\d*)$' ); } function getSiOptions (): { text: string; value: string }[] { return formatBalance.getOptions() .map(({ power, text, value }): { text: string; value: string } => ({ value, text: power === 0 ? TokenUnit.abbr : text })); } function getSiPowers (si: SiDef | null): [BN, number, number] { if (!si) { return [ZERO, 0, 0]; } const basePower = formatBalance.getDefaults().decimals; return [new BN(basePower + si.power), basePower, si.power]; } function isValidNumber (bn: BN, { bitLength = DEFAULT_BITLENGTH, isZeroable, maxValue }: Props): boolean { if ( // cannot be negative bn.lt(ZERO) || // cannot be > than allowed max !bn.lt(getGlobalMaxValue(bitLength)) || // check if 0 and it should be a value (!isZeroable && bn.eq(ZERO)) || // check that the bitlengths fit bn.bitLength() > (bitLength || DEFAULT_BITLENGTH) || // cannot be > max (if specified) (maxValue && maxValue.gtn(0) && bn.gt(maxValue)) ) { return false; } return true; } function inputToBn (input: string, si: SiDef | null, props: Props): [BN, boolean] { const [siPower, basePower, siUnitPower] = getSiPowers(si); // eslint-disable-next-line @typescript-eslint/prefer-regexp-exec const isDecimalValue = input.match(/^(\d+)\.(\d+)$/); let result; if (isDecimalValue) { if (siUnitPower - isDecimalValue[2].length < -basePower) { result = new BN(-1); } const div = new BN(input.replace(/\.\d*$/, '')); const modString = input.replace(/^\d+\./, ''); const mod = new BN(modString); result = div .mul(TEN.pow(siPower)) .add(mod.mul(TEN.pow(new BN(basePower + siUnitPower - modString.length)))); } else { result = new BN(input.replace(/[^\d]/g, '')) .mul(TEN.pow(siPower)); } return [ result, isValidNumber(result, props) ]; } // function bnToInput (bn: BN, si: SiDef | null): string { // const [siPower] = getSiPowers(si); // // 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*$/, '')}`; // })() // : '' // }`; // } function getValuesFromString (value: string, si: SiDef | null, props: Props): [string, BN, boolean] { const [valueBn, isValid] = inputToBn(value, si, props); return [ value, valueBn, isValid ]; } function getValuesFromBn (valueBn: BN, si: SiDef | null): [string, BN, boolean] { const value = si ? valueBn.div(TEN.pow(new BN(si.power))).toString() : valueBn.toString(); return [ value, valueBn, true ]; } function getValues (value: BN | string, si: SiDef | null, props: Props): [string, BN, boolean] { return BN.isBN(value) ? getValuesFromBn(value, si) : getValuesFromString(value, si, props); } function isNewPropsValue (propsValue: BN | string, value: string, valueBn: BN): boolean { return BN.isBN(propsValue) ? !propsValue.eq(valueBn) : propsValue !== value; } function InputNumber (props: Props): React.ReactElement { const { bitLength = DEFAULT_BITLENGTH, className, defaultValue = ZERO, help, isDecimal, isSi, isDisabled, isError = false, maxLength, maxValue, onChange, onEnter, placeholder, style, value: propsValue, t } = props; const [si, setSi] = useState(isSi ? formatBalance.findSi('-') : null); const [isPreKeyDown, setIsPreKeyDown] = useState(false); const [[value, valueBn, isValid], setValues] = useState<[string, BN, boolean]>( getValues(propsValue || defaultValue, si, props) ); useEffect((): void => { if (propsValue && isNewPropsValue(propsValue, value, valueBn)) { setValues(getValues(propsValue, si, props)); } }, [propsValue]); useEffect((): void => { setValues(getValues(value, si, props)); }, [value, si, bitLength, maxValue]); useEffect((): void => { onChange && isValid && onChange(valueBn); }, [isValid, valueBn]); const _onChange = (input: string): void => { setValues(getValuesFromString(input, si, props)); }; const _onKeyDown = (event: React.KeyboardEvent): void => { if (KEYS_PRE.includes(event.key)) { setIsPreKeyDown(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 (!getRegex(isDecimal || !!si).test(newValue)) { event.preventDefault(); } } }; const _onKeyUp = (event: React.KeyboardEvent): void => { if (KEYS_PRE.includes(event.key)) { setIsPreKeyDown(false); } }; const _onPaste = (event: React.ClipboardEvent): void => { const { value: newValue } = event.target as HTMLInputElement; if (!getRegex(isDecimal || !!si).test(newValue)) { event.preventDefault(); } }; const _onSelectSiUnit = (siUnit: string): void => { setSi(formatBalance.findSi(siUnit)); }; // const _onClickMaxButton = (): void => { // !!maxValue && setValue(bnToInput(maxValue, si)); // }; const maxValueLength = getGlobalMaxValue(bitLength).toString().length - 1; return ( {/* (ALLOW_MAX && withMax && !!maxValue && valueBn.lt(maxValue)) && ( ) */} {!!si && ( )} ); } export default translate(InputNumber);