2020-10-02 15:10:00 +00:00
|
|
|
/**
|
|
|
|
* See app/common/NumberFormat for description of options we support.
|
|
|
|
*/
|
2024-04-11 06:50:30 +00:00
|
|
|
import {FormFieldRulesConfig} from 'app/client/components/Forms/FormConfig';
|
|
|
|
import {fromKoSave} from 'app/client/lib/fromKoSave';
|
2023-01-11 17:57:42 +00:00
|
|
|
import {makeT} from 'app/client/lib/localization';
|
2020-10-02 15:10:00 +00:00
|
|
|
import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
|
|
|
|
import {reportError} from 'app/client/models/errors';
|
2024-04-11 06:50:30 +00:00
|
|
|
import {fieldWithDefault} from 'app/client/models/modelUtil';
|
|
|
|
import {FormNumberFormat} from 'app/client/ui/FormAPI';
|
|
|
|
import {cssLabel, cssNumericSpinner, cssRow} from 'app/client/ui/RightPanelStyles';
|
|
|
|
import {buttonSelect, cssButtonSelect, ISelectorOption, makeButtonSelect} from 'app/client/ui2018/buttonSelect';
|
2022-09-06 01:51:57 +00:00
|
|
|
import {testId, theme} from 'app/client/ui2018/cssVars';
|
2022-10-14 10:07:19 +00:00
|
|
|
import {buildCurrencyPicker} from 'app/client/widgets/CurrencyPicker';
|
2020-10-02 15:10:00 +00:00
|
|
|
import {NTextBox} from 'app/client/widgets/NTextBox';
|
2024-04-11 06:50:30 +00:00
|
|
|
import {numberOrDefault} from 'app/common/gutil';
|
2020-10-02 15:10:00 +00:00
|
|
|
import {buildNumberFormat, NumberFormatOptions, NumMode, NumSign} from 'app/common/NumberFormat';
|
2024-04-11 06:50:30 +00:00
|
|
|
import {Computed, dom, DomContents, fromKo, MultiHolder, styled} from 'grainjs';
|
2022-10-14 10:07:19 +00:00
|
|
|
import * as LocaleCurrency from 'locale-currency';
|
2020-10-02 15:10:00 +00:00
|
|
|
|
2023-01-11 17:57:42 +00:00
|
|
|
const t = makeT('NumericTextBox');
|
2024-04-11 06:50:30 +00:00
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
const modeOptions: Array<ISelectorOption<NumMode>> = [
|
|
|
|
{value: 'currency', label: '$'},
|
|
|
|
{value: 'decimal', label: ','},
|
2021-08-26 16:35:11 +00:00
|
|
|
{value: 'percent', label: '%'},
|
|
|
|
{value: 'scientific', label: 'Exp'}
|
2020-10-02 15:10:00 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
const signOptions: Array<ISelectorOption<NumSign>> = [
|
|
|
|
{value: 'parens', label: '(-)'},
|
|
|
|
];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* NumericTextBox - The most basic widget for displaying numeric information.
|
|
|
|
*/
|
|
|
|
export class NumericTextBox extends NTextBox {
|
|
|
|
constructor(field: ViewFieldRec) {
|
|
|
|
super(field);
|
|
|
|
}
|
|
|
|
|
2020-10-07 21:58:43 +00:00
|
|
|
public buildConfigDom(): DomContents {
|
2020-10-02 15:10:00 +00:00
|
|
|
// Holder for all computeds created here. It gets disposed with the returned DOM element.
|
|
|
|
const holder = new MultiHolder();
|
|
|
|
|
|
|
|
// Resolved options, to show default min/max decimals, which change depending on numMode.
|
2021-08-26 16:35:11 +00:00
|
|
|
const resolved = Computed.create<Intl.ResolvedNumberFormatOptions>(holder, (use) => {
|
2022-10-14 10:07:19 +00:00
|
|
|
const {numMode} = use(this.field.config.options);
|
2021-08-26 16:35:11 +00:00
|
|
|
const docSettings = use(this.field.documentSettings);
|
|
|
|
return buildNumberFormat({numMode}, docSettings).resolvedOptions();
|
|
|
|
});
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
// Prepare various observables that reflect the options in the UI.
|
2022-10-14 10:07:19 +00:00
|
|
|
const fieldOptions = this.field.config.options;
|
|
|
|
const options = fromKo(fieldOptions);
|
2021-08-26 16:35:11 +00:00
|
|
|
const docSettings = fromKo(this.field.documentSettings);
|
|
|
|
const numMode = Computed.create(holder, options, (use, opts) => (opts.numMode as NumMode) || null);
|
2020-10-02 15:10:00 +00:00
|
|
|
const numSign = Computed.create(holder, options, (use, opts) => opts.numSign || null);
|
2021-08-26 16:35:11 +00:00
|
|
|
const currency = Computed.create(holder, options, (use, opts) => opts.currency);
|
2022-10-14 10:07:19 +00:00
|
|
|
const disabled = Computed.create(holder, use => use(this.field.config.options.disabled('currency')));
|
2020-10-02 15:10:00 +00:00
|
|
|
const minDecimals = Computed.create(holder, options, (use, opts) => numberOrDefault(opts.decimals, ''));
|
|
|
|
const maxDecimals = Computed.create(holder, options, (use, opts) => numberOrDefault(opts.maxDecimals, ''));
|
|
|
|
const defaultMin = Computed.create(holder, resolved, (use, res) => res.minimumFractionDigits);
|
|
|
|
const defaultMax = Computed.create(holder, resolved, (use, res) => res.maximumFractionDigits);
|
2021-08-26 16:35:11 +00:00
|
|
|
const docCurrency = Computed.create(holder, docSettings, (use, settings) =>
|
2022-04-27 17:46:24 +00:00
|
|
|
settings.currency ?? LocaleCurrency.getCurrency(settings.locale ?? 'en-US')
|
2021-08-26 16:35:11 +00:00
|
|
|
);
|
2020-10-02 15:10:00 +00:00
|
|
|
|
2022-10-14 10:07:19 +00:00
|
|
|
// Save a value as the given property in fieldOptions observable. Set it, save, and revert
|
2020-10-02 15:10:00 +00:00
|
|
|
// on save error. This is similar to what modelUtil.setSaveValue() does.
|
|
|
|
const setSave = (prop: keyof NumberFormatOptions, value: unknown) => {
|
2022-10-14 10:07:19 +00:00
|
|
|
const orig = {...fieldOptions.peek()};
|
2020-10-02 15:10:00 +00:00
|
|
|
if (value !== orig[prop]) {
|
2022-10-14 10:07:19 +00:00
|
|
|
fieldOptions({...orig, [prop]: value, ...updateOptions(prop, value)});
|
|
|
|
fieldOptions.save().catch((err) => { reportError(err); fieldOptions(orig); });
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Prepare setters for the UI elements.
|
2024-04-11 06:50:30 +00:00
|
|
|
// If defined, `val` will be a floating point number between 0 and 20; make sure it's
|
|
|
|
// saved as an integer.
|
|
|
|
const setMinDecimals = (val?: number) => setSave('decimals', val && Math.floor(val));
|
|
|
|
const setMaxDecimals = (val?: number) => setSave('maxDecimals', val && Math.floor(val));
|
2020-10-02 15:10:00 +00:00
|
|
|
// Mode and Sign behave as toggles: clicking a selected on deselects it.
|
|
|
|
const setMode = (val: NumMode) => setSave('numMode', val !== numMode.get() ? val : undefined);
|
|
|
|
const setSign = (val: NumSign) => setSave('numSign', val !== numSign.get() ? val : undefined);
|
2021-08-26 16:35:11 +00:00
|
|
|
const setCurrency = (val: string|undefined) => setSave('currency', val);
|
2020-10-02 15:10:00 +00:00
|
|
|
|
2022-10-14 10:07:19 +00:00
|
|
|
const disabledStyle = cssButtonSelect.cls('-disabled', disabled);
|
|
|
|
|
2020-10-07 21:58:43 +00:00
|
|
|
return [
|
|
|
|
super.buildConfigDom(),
|
2023-01-11 17:57:42 +00:00
|
|
|
cssLabel(t('Number Format')),
|
2020-10-02 15:10:00 +00:00
|
|
|
cssRow(
|
2020-10-07 21:58:43 +00:00
|
|
|
dom.autoDispose(holder),
|
2022-10-14 10:07:19 +00:00
|
|
|
makeButtonSelect(numMode, modeOptions, setMode, disabledStyle, cssModeSelect.cls(''), testId('numeric-mode')),
|
|
|
|
makeButtonSelect(numSign, signOptions, setSign, disabledStyle, cssSignSelect.cls(''), testId('numeric-sign')),
|
2020-10-02 15:10:00 +00:00
|
|
|
),
|
2021-08-26 16:35:11 +00:00
|
|
|
dom.maybe((use) => use(numMode) === 'currency', () => [
|
2023-01-11 17:57:42 +00:00
|
|
|
cssLabel(t('Currency')),
|
2021-08-26 16:35:11 +00:00
|
|
|
cssRow(
|
|
|
|
dom.domComputed(docCurrency, (defaultCurrency) =>
|
|
|
|
buildCurrencyPicker(holder, currency, setCurrency,
|
2023-01-11 17:57:42 +00:00
|
|
|
{defaultCurrencyLabel: t(`Default currency ({{defaultCurrency}})`, {defaultCurrency}), disabled})
|
2021-08-26 16:35:11 +00:00
|
|
|
),
|
|
|
|
testId("numeric-currency")
|
|
|
|
)
|
|
|
|
]),
|
2023-01-11 17:57:42 +00:00
|
|
|
cssLabel(t('Decimals')),
|
2020-10-02 15:10:00 +00:00
|
|
|
cssRow(
|
2024-04-11 06:50:30 +00:00
|
|
|
cssNumericSpinner(
|
|
|
|
minDecimals,
|
|
|
|
{
|
|
|
|
label: t('min'),
|
|
|
|
minValue: 0,
|
|
|
|
maxValue: 20,
|
|
|
|
defaultValue: defaultMin,
|
|
|
|
disabled,
|
|
|
|
save: setMinDecimals,
|
|
|
|
},
|
|
|
|
testId('numeric-min-decimals'),
|
|
|
|
),
|
|
|
|
cssNumericSpinner(
|
|
|
|
maxDecimals,
|
|
|
|
{
|
|
|
|
label: t('max'),
|
|
|
|
minValue: 0,
|
|
|
|
maxValue: 20,
|
|
|
|
defaultValue: defaultMax,
|
|
|
|
disabled,
|
|
|
|
save: setMaxDecimals,
|
|
|
|
},
|
|
|
|
testId('numeric-max-decimals'),
|
|
|
|
),
|
2020-10-02 15:10:00 +00:00
|
|
|
),
|
2020-10-07 21:58:43 +00:00
|
|
|
];
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
|
2021-08-26 16:35:11 +00:00
|
|
|
|
2024-04-11 06:50:30 +00:00
|
|
|
public buildFormConfigDom(): DomContents {
|
|
|
|
const format = fieldWithDefault<FormNumberFormat>(
|
|
|
|
this.field.widgetOptionsJson.prop('formNumberFormat'),
|
|
|
|
'text'
|
|
|
|
);
|
|
|
|
|
|
|
|
return [
|
|
|
|
cssLabel(t('Field Format')),
|
|
|
|
cssRow(
|
|
|
|
buttonSelect(
|
|
|
|
fromKoSave(format),
|
|
|
|
[
|
|
|
|
{value: 'text', label: t('Text')},
|
|
|
|
{value: 'spinner', label: t('Spinner')},
|
|
|
|
],
|
|
|
|
testId('numeric-form-field-format'),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
dom.create(FormFieldRulesConfig, this.field),
|
|
|
|
];
|
|
|
|
}
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Helper used by setSave() above to reset some properties when switching modes.
|
2021-08-26 16:35:11 +00:00
|
|
|
function updateOptions(prop: keyof NumberFormatOptions, value: unknown): Partial<NumberFormatOptions> {
|
2020-10-02 15:10:00 +00:00
|
|
|
// Reset the numSign to default when toggling mode to percent or scientific.
|
|
|
|
if (prop === 'numMode' && (!value || value === 'scientific' || value === 'percent')) {
|
|
|
|
return {numSign: undefined};
|
|
|
|
}
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
const cssModeSelect = styled(makeButtonSelect, `
|
|
|
|
flex: 4 4 0px;
|
2022-09-06 01:51:57 +00:00
|
|
|
background-color: ${theme.inputBg};
|
2020-10-02 15:10:00 +00:00
|
|
|
`);
|
|
|
|
|
|
|
|
const cssSignSelect = styled(makeButtonSelect, `
|
|
|
|
flex: 1 1 0px;
|
2022-09-06 01:51:57 +00:00
|
|
|
background-color: ${theme.inputBg};
|
2020-10-02 15:10:00 +00:00
|
|
|
margin-left: 16px;
|
|
|
|
`);
|