221 lines
5.7 KiB
TypeScript
221 lines
5.7 KiB
TypeScript
/*
|
|
This file is part of GNU Taler
|
|
(C) 2022 Taler Systems S.A.
|
|
|
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
|
terms of the GNU General Public License as published by the Free Software
|
|
Foundation; either version 3, or (at your option) any later version.
|
|
|
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License along with
|
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
|
*/
|
|
|
|
import {
|
|
amountFractionalBase,
|
|
amountFractionalLength,
|
|
AmountJson,
|
|
amountMaxValue,
|
|
Amounts,
|
|
Result,
|
|
TranslatedString,
|
|
} from "@gnu-taler/taler-util";
|
|
import { Fragment, h, VNode } from "preact";
|
|
import { useState } from "preact/hooks";
|
|
import { useTranslationContext } from "../context/translation.js";
|
|
import { AmountFieldHandler } from "../mui/handlers.js";
|
|
import { TextField } from "../mui/TextField.js";
|
|
|
|
const HIGH_DENOM_SYMBOL = ["", "K", "M", "G", "T", "P"];
|
|
const LOW_DENOM_SYMBOL = ["", "m", "mm", "n", "p", "f"];
|
|
|
|
/**
|
|
* Show normalized value based on the currency unit
|
|
*/
|
|
export function AmountField({
|
|
label,
|
|
handler,
|
|
lowestDenom = 1,
|
|
highestDenom = 1,
|
|
required,
|
|
}: {
|
|
label: TranslatedString;
|
|
lowestDenom?: number;
|
|
highestDenom?: number;
|
|
required?: boolean;
|
|
handler: AmountFieldHandler;
|
|
}): VNode {
|
|
const { i18n } = useTranslationContext();
|
|
const [unit, setUnit] = useState(1);
|
|
const [error, setError] = useState<string>("");
|
|
|
|
const normal = normalize(handler.value, unit);
|
|
const previousValue = Amounts.stringifyValue(normal);
|
|
|
|
const [textValue, setTextValue] = useState<string>(previousValue);
|
|
|
|
function updateUnit(newUnit: number) {
|
|
setUnit(newUnit);
|
|
const newNorm = normalize(handler.value, newUnit);
|
|
setTextValue(Amounts.stringifyValue(newNorm));
|
|
}
|
|
|
|
const currency = handler.value.currency;
|
|
|
|
const currencyLabels = buildLabelsForCurrency(
|
|
currency,
|
|
lowestDenom,
|
|
highestDenom,
|
|
);
|
|
|
|
function positiveAmount(value: string): string {
|
|
if (!value) {
|
|
if (handler.onInput) {
|
|
handler.onInput(Amounts.zeroOfCurrency(currency));
|
|
}
|
|
} else
|
|
try {
|
|
const parsed = Amounts.parseOrThrow(`${currency}:${value.trim()}`);
|
|
|
|
const realValue = denormalize(parsed, unit);
|
|
|
|
if (handler.onInput) {
|
|
handler.onInput(realValue);
|
|
}
|
|
setError("");
|
|
} catch (e) {
|
|
setError(i18n.str`Amount is not valid`);
|
|
}
|
|
setTextValue(value);
|
|
return value;
|
|
}
|
|
|
|
return (
|
|
<Fragment>
|
|
<TextField
|
|
label={label}
|
|
type="text"
|
|
min="0"
|
|
inputmode="decimal"
|
|
step="0.1"
|
|
variant="filled"
|
|
error={handler.error}
|
|
required={required}
|
|
startAdornment={
|
|
currencyLabels.length === 1 ? (
|
|
<div
|
|
style={{
|
|
marginTop: 20,
|
|
padding: "5px 12px 8px 12px",
|
|
}}
|
|
>
|
|
{currency}
|
|
</div>
|
|
) : (
|
|
<select
|
|
disabled={!handler.onInput}
|
|
onChange={(e) => {
|
|
const unit = Number.parseFloat(e.currentTarget.value);
|
|
updateUnit(unit);
|
|
}}
|
|
value={String(unit)}
|
|
style={{
|
|
marginTop: 20,
|
|
padding: "5px 12px 8px 12px",
|
|
background: "transparent",
|
|
border: 0,
|
|
}}
|
|
>
|
|
{currencyLabels.map((c) => (
|
|
<option key={c} value={c.unit}>
|
|
<div>{c.name}</div>
|
|
</option>
|
|
))}
|
|
</select>
|
|
)
|
|
}
|
|
value={textValue}
|
|
disabled={!handler.onInput}
|
|
onInput={positiveAmount}
|
|
/>
|
|
{error && <div style={{ color: "red" }}>{error}</div>}
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Return the real value of a normalized unit
|
|
* If the value is 20 and the unit is kilo == 1000 the returned value will be amount * 1000
|
|
* @param amount
|
|
* @param unit
|
|
* @returns
|
|
*/
|
|
function denormalize(amount: AmountJson, unit: number): AmountJson {
|
|
if (unit === 1 || Amounts.isZero(amount)) return amount;
|
|
const result =
|
|
unit < 1
|
|
? Amounts.divide(amount, 1 / unit)
|
|
: Amounts.mult(amount, unit).amount;
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Return the amount in the current unit.
|
|
* If the value is 20000 and the unit is kilo == 1000 and the returned value will be amount / unit
|
|
*
|
|
* @param amount
|
|
* @param unit
|
|
* @returns
|
|
*/
|
|
function normalize(amount: AmountJson, unit: number): AmountJson {
|
|
if (unit === 1 || Amounts.isZero(amount)) return amount;
|
|
const result =
|
|
unit < 1
|
|
? Amounts.mult(amount, 1 / unit).amount
|
|
: Amounts.divide(amount, unit);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Take every label in HIGH_DENOM_SYMBOL and LOW_DENOM_SYMBOL and create
|
|
* which create the corresponding unit multiplier
|
|
* @param currency
|
|
* @param lowestDenom
|
|
* @param highestDenom
|
|
* @returns
|
|
*/
|
|
function buildLabelsForCurrency(
|
|
currency: string,
|
|
lowestDenom: number,
|
|
highestDenom: number,
|
|
): Array<{ name: string; unit: number }> {
|
|
let hd = Math.floor(Math.log10(highestDenom || 1) / 3);
|
|
let ld = Math.ceil((-1 * Math.log10(lowestDenom || 1)) / 3);
|
|
|
|
const result: Array<{ name: string; unit: number }> = [
|
|
{
|
|
name: currency,
|
|
unit: 1,
|
|
},
|
|
];
|
|
|
|
while (hd > 0) {
|
|
result.push({
|
|
name: `${HIGH_DENOM_SYMBOL[hd]}${currency}`,
|
|
unit: Math.pow(10, hd * 3),
|
|
});
|
|
hd--;
|
|
}
|
|
while (ld > 0) {
|
|
result.push({
|
|
name: `${LOW_DENOM_SYMBOL[ld]}${currency}`,
|
|
unit: Math.pow(10, -1 * ld * 3),
|
|
});
|
|
ld--;
|
|
}
|
|
return result;
|
|
}
|