mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Simple localization support and currency selector.
Summary: - Grist document has a associated "locale" setting that affects how currency is formatted. - Currency selector for number format. Test Plan: not done Reviewers: dsagal Reviewed By: dsagal Subscribers: paulfitz Differential Revision: https://phab.getgrist.com/D2977
This commit is contained in:
@@ -19,7 +19,10 @@ function CopySelection(tableData, rowIds, fields, options) {
|
||||
this.colStyle = options.colStyle;
|
||||
this.columns = fields.map((f, i) => {
|
||||
let formatter = ValueFormatter.createFormatter(
|
||||
f.displayColModel().type(), f.widgetOptionsJson());
|
||||
f.displayColModel().type(),
|
||||
f.widgetOptionsJson(),
|
||||
f.documentSettings()
|
||||
);
|
||||
let _fmtGetter = tableData.getRowPropFunc(this.displayColIds[i]);
|
||||
let _rawGetter = tableData.getRowPropFunc(this.colIds[i]);
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ export class GristDoc extends DisposableWithEvents {
|
||||
app.topAppModel.getUntrustedContentOrigin(), this.docComm, app.clientScope);
|
||||
|
||||
// Maintain the MetaRowModel for the global document info, including docId and peers.
|
||||
this.docInfo = this.docModel.docInfo.getRowModel(1);
|
||||
this.docInfo = this.docModel.docInfoRow;
|
||||
|
||||
const defaultViewId = this.docInfo.newDefaultViewId;
|
||||
|
||||
|
||||
@@ -267,7 +267,7 @@ class Searcher {
|
||||
|
||||
this._fieldStepper.array = section.viewFields().peek();
|
||||
this._fieldFormatters = this._fieldStepper.array.map(
|
||||
f => createFormatter(f.displayColModel().type(), f.widgetOptionsJson()));
|
||||
f => createFormatter(f.displayColModel().type(), f.widgetOptionsJson(), f.documentSettings()));
|
||||
return tableModel;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export function addColTypeSuffix(type: string, column: ColumnRec, docModel: DocM
|
||||
return `${type}:${refTableId}`;
|
||||
}
|
||||
case "DateTime":
|
||||
return 'DateTime:' + docModel.docInfo.getRowModel(1).timezone();
|
||||
return 'DateTime:' + docModel.docInfoRow.timezone();
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
|
||||
@@ -111,6 +111,8 @@ export class DocModel {
|
||||
public pages: MTM<PageRec> = this._metaTableModel("_grist_Pages", createPageRec);
|
||||
public rules: MTM<ACLRuleRec> = this._metaTableModel("_grist_ACLRules", createACLRuleRec);
|
||||
|
||||
public docInfoRow: DocInfoRec;
|
||||
|
||||
public allTables: KoArray<TableRec>;
|
||||
public allTableIds: KoArray<string>;
|
||||
|
||||
@@ -135,6 +137,8 @@ export class DocModel {
|
||||
model.loadData();
|
||||
}
|
||||
|
||||
this.docInfoRow = this.docInfo.getRowModel(1);
|
||||
|
||||
// An observable array of user-visible tables, sorted by tableId, excluding summary tables.
|
||||
// This is a publicly exposed member.
|
||||
this.allTables = createUserTablesArray(this.tables);
|
||||
|
||||
@@ -217,7 +217,7 @@ class FinderImpl implements IFinder {
|
||||
|
||||
this._fieldStepper.array = section.viewFields().peek();
|
||||
this._fieldFormatters = this._fieldStepper.array.map(
|
||||
f => createFormatter(f.displayColModel().type(), f.widgetOptionsJson()));
|
||||
f => createFormatter(f.displayColModel().type(), f.widgetOptionsJson(), f.documentSettings()));
|
||||
return tableModel;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import {DocModel, IRowModel} from 'app/client/models/DocModel';
|
||||
import * as modelUtil from 'app/client/models/modelUtil';
|
||||
import {jsonObservable} from 'app/client/models/modelUtil';
|
||||
import {DocumentSettings} from 'app/common/DocumentSettings';
|
||||
import * as ko from 'knockout';
|
||||
|
||||
// The document-wide metadata. It's all contained in a single record with id=1.
|
||||
export interface DocInfoRec extends IRowModel<"_grist_DocInfo"> {
|
||||
documentSettingsJson: modelUtil.SaveableObjObservable<DocumentSettings>
|
||||
defaultViewId: ko.Computed<number>;
|
||||
newDefaultViewId: ko.Computed<number>;
|
||||
}
|
||||
|
||||
export function createDocInfoRec(this: DocInfoRec, docModel: DocModel): void {
|
||||
this.documentSettingsJson = jsonObservable(this.documentSettings);
|
||||
this.defaultViewId = this.autoDispose(ko.pureComputed(() => {
|
||||
const tab = docModel.allTabs.at(0);
|
||||
return tab ? tab.viewRef() : 0;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {ColumnRec, DocModel, IRowModel, refRecord, ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import * as modelUtil from 'app/client/models/modelUtil';
|
||||
import * as UserType from 'app/client/widgets/UserType';
|
||||
import {DocumentSettings} from 'app/common/DocumentSettings';
|
||||
import {BaseFormatter, createFormatter} from 'app/common/ValueFormatter';
|
||||
import {Computed, fromKo} from 'grainjs';
|
||||
import * as ko from 'knockout';
|
||||
@@ -55,7 +56,6 @@ export interface ViewFieldRec extends IRowModel<"_grist_Views_section_field"> {
|
||||
|
||||
// Observable for the object with the current options, either for the field or for the column,
|
||||
// which takes into account the default options for column's type.
|
||||
|
||||
widgetOptionsJson: modelUtil.SaveableObjObservable<any>;
|
||||
|
||||
// Whether lines should wrap in a cell.
|
||||
@@ -73,6 +73,8 @@ export interface ViewFieldRec extends IRowModel<"_grist_Views_section_field"> {
|
||||
textColor: modelUtil.KoSaveableObservable<string|undefined>;
|
||||
fillColor: modelUtil.KoSaveableObservable<string>;
|
||||
|
||||
documentSettings: ko.PureComputed<DocumentSettings>;
|
||||
|
||||
// Helper which adds/removes/updates field's displayCol to match the formula.
|
||||
saveDisplayFormula(formula: string): Promise<void>|undefined;
|
||||
|
||||
@@ -167,8 +169,8 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
|
||||
this.createVisibleColFormatter = function() {
|
||||
const vcol = this.visibleColModel();
|
||||
return (vcol.getRowId() !== 0) ?
|
||||
createFormatter(vcol.type(), vcol.widgetOptionsJson()) :
|
||||
createFormatter(this.column().type(), this.widgetOptionsJson());
|
||||
createFormatter(vcol.type(), vcol.widgetOptionsJson(), this.documentSettings()) :
|
||||
createFormatter(this.column().type(), this.widgetOptionsJson(), this.documentSettings());
|
||||
};
|
||||
|
||||
// The widgetOptions to read and write: either the column's or the field's own.
|
||||
@@ -179,7 +181,6 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
|
||||
|
||||
// Observable for the object with the current options, either for the field or for the column,
|
||||
// which takes into account the default options for this column's type.
|
||||
|
||||
this.widgetOptionsJson = modelUtil.jsonObservable(this._widgetOptionsStr,
|
||||
(opts: any) => UserType.mergeOptions(opts || {}, this.column().pureType()));
|
||||
|
||||
@@ -211,4 +212,6 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
|
||||
read: () => fillColorProp(),
|
||||
write: (setter, val) => setter(fillColorProp, val.toUpperCase() === '#FFFFFF' ? '' : val),
|
||||
});
|
||||
|
||||
this.documentSettings = ko.pureComputed(() => docModel.docInfoRow.documentSettingsJson());
|
||||
}
|
||||
|
||||
@@ -2,15 +2,21 @@
|
||||
* This module export a component for editing some document settings consisting of the timezone,
|
||||
* (new settings to be added here ...).
|
||||
*/
|
||||
import { dom, styled } from 'grainjs';
|
||||
import { Computed, Observable } from 'grainjs';
|
||||
import {dom, IDisposableOwner, styled} from 'grainjs';
|
||||
import {Computed, Observable} from 'grainjs';
|
||||
|
||||
import { loadMomentTimezone } from 'app/client/lib/imports';
|
||||
import { DocInfoRec } from 'app/client/models/DocModel';
|
||||
import { DocPageModel } from 'app/client/models/DocPageModel';
|
||||
import { vars } from 'app/client/ui2018/cssVars';
|
||||
import { saveModal } from 'app/client/ui2018/modals';
|
||||
import { buildTZAutocomplete } from 'app/client/widgets/TZAutocomplete';
|
||||
|
||||
import {ACSelectItem, buildACSelect} from "app/client/lib/ACSelect";
|
||||
import {ACIndexImpl} from "app/client/lib/ACIndex";
|
||||
import {loadMomentTimezone} from 'app/client/lib/imports';
|
||||
import {DocInfoRec} from 'app/client/models/DocModel';
|
||||
import {DocPageModel} from 'app/client/models/DocPageModel';
|
||||
import {testId, vars} from 'app/client/ui2018/cssVars';
|
||||
import {saveModal} from 'app/client/ui2018/modals';
|
||||
import {buildTZAutocomplete} from 'app/client/widgets/TZAutocomplete';
|
||||
import {locales} from "app/common/Locales";
|
||||
import {buildCurrencyPicker} from 'app/client/widgets/CurrencyPicker';
|
||||
import * as LocaleCurrency from "locale-currency";
|
||||
|
||||
/**
|
||||
* Builds a simple saveModal for saving settings.
|
||||
@@ -18,23 +24,82 @@ import { buildTZAutocomplete } from 'app/client/widgets/TZAutocomplete';
|
||||
export async function showDocSettingsModal(docInfo: DocInfoRec, docPageModel: DocPageModel): Promise<void> {
|
||||
const moment = await loadMomentTimezone();
|
||||
return saveModal((ctl, owner) => {
|
||||
const timezone = Observable.create(owner, docInfo.timezone.peek());
|
||||
const timezoneObs = Observable.create(owner, docInfo.timezone.peek());
|
||||
|
||||
const {locale, currency} = docInfo.documentSettingsJson.peek();
|
||||
const localeObs = Observable.create(owner, locale);
|
||||
const currencyObs = Observable.create(owner, currency);
|
||||
|
||||
return {
|
||||
title: 'Document Settings',
|
||||
body: [
|
||||
cssDataRow("This document's ID (for API use):"),
|
||||
cssDataRow(dom('tt', docPageModel.currentDocId.get())),
|
||||
cssDataRow('Time Zone:'),
|
||||
cssDataRow(dom.create(buildTZAutocomplete, moment, timezone, (val) => timezone.set(val))),
|
||||
cssDataRow(dom.create(buildTZAutocomplete, moment, timezoneObs, (val) => timezoneObs.set(val))),
|
||||
cssDataRow('Locale:'),
|
||||
cssDataRow(dom.create(buildLocaleSelect, localeObs)),
|
||||
cssDataRow('Currency:'),
|
||||
cssDataRow(dom.domComputed(localeObs, (l) =>
|
||||
dom.create(buildCurrencyPicker, currencyObs, (val) => currencyObs.set(val),
|
||||
{defaultCurrencyLabel: `Local currency (${LocaleCurrency.getCurrency(l)})`})
|
||||
)),
|
||||
],
|
||||
// At this point, we only need to worry about saving this one setting.
|
||||
saveFunc: () => docInfo.timezone.saveOnly(timezone.get()),
|
||||
// If timezone hasn't changed, there is nothing to save, so disable the Save button.
|
||||
saveDisabled: Computed.create(owner, (use) => use(timezone) === docInfo.timezone.peek()),
|
||||
saveFunc: () => docInfo.updateColValues({
|
||||
timezone: timezoneObs.get(),
|
||||
documentSettings: JSON.stringify({
|
||||
...docInfo.documentSettingsJson.peek(),
|
||||
locale: localeObs.get(),
|
||||
currency: currencyObs.get()
|
||||
})
|
||||
}),
|
||||
// If timezone, locale, or currency hasn't changed, disable the Save button.
|
||||
saveDisabled: Computed.create(owner,
|
||||
(use) => {
|
||||
const docSettings = docInfo.documentSettingsJson.peek();
|
||||
return (
|
||||
use(timezoneObs) === docInfo.timezone.peek() &&
|
||||
use(localeObs) === docSettings.locale &&
|
||||
use(currencyObs) === docSettings.currency
|
||||
);
|
||||
})
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
type LocaleItem = ACSelectItem & {locale?: string};
|
||||
|
||||
function buildLocaleSelect(
|
||||
owner: IDisposableOwner,
|
||||
locale: Observable<string>
|
||||
) {
|
||||
const localeList: LocaleItem[] = locales.map(l => ({
|
||||
value: l.name, // Use name as a value, we will translate the name into the locale on save
|
||||
label: l.name,
|
||||
locale: l.code,
|
||||
cleanText: l.name.trim().toLowerCase(),
|
||||
}));
|
||||
const acIndex = new ACIndexImpl<LocaleItem>(localeList, 200, true);
|
||||
// AC select will show the value (in this case locale) not a label when something is selected.
|
||||
// To show the label - create another observable that will be in sync with the value, but
|
||||
// will contain text.
|
||||
const localeCode = locale.get();
|
||||
const localeName = locales.find(l => l.code === localeCode)?.name || localeCode;
|
||||
const textObs = Observable.create(owner, localeName);
|
||||
return buildACSelect(owner,
|
||||
{
|
||||
acIndex, valueObs: textObs,
|
||||
save(value, item: LocaleItem | undefined) {
|
||||
if (!item) { throw new Error("Invalid locale"); }
|
||||
textObs.set(value);
|
||||
locale.set(item.locale!);
|
||||
},
|
||||
},
|
||||
testId("locale-autocomplete")
|
||||
);
|
||||
}
|
||||
|
||||
// This matches the style used in showProfileModal in app/client/ui/AccountWidget.
|
||||
const cssDataRow = styled('div', `
|
||||
margin: 16px 0px;
|
||||
|
||||
@@ -17,9 +17,8 @@ function AbstractWidget(field, opts = {}) {
|
||||
this.options = field.widgetOptionsJson;
|
||||
const {defaultTextColor = '#000000'} = opts;
|
||||
|
||||
this.valueFormatter = this.autoDispose(ko.computed(() => {
|
||||
return ValueFormatter.createFormatter(field.displayColModel().type(), this.options());
|
||||
}));
|
||||
this.valueFormatter = this.autoDispose(ko.computed(() =>
|
||||
ValueFormatter.createFormatter(field.displayColModel().type(), this.options(), field.documentSettings())));
|
||||
|
||||
this.textColor = Computed.create(this, (use) => use(this.field.textColor) || defaultTextColor)
|
||||
.onWrite((val) => this.field.textColor(val === defaultTextColor ? undefined : val));
|
||||
|
||||
49
app/client/widgets/CurrencyPicker.ts
Normal file
49
app/client/widgets/CurrencyPicker.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import {ACSelectItem, buildACSelect} from "app/client/lib/ACSelect";
|
||||
import {Computed, IDisposableOwner, Observable} from "grainjs";
|
||||
import {ACIndexImpl} from "app/client/lib/ACIndex";
|
||||
import {testId} from 'app/client/ui2018/cssVars';
|
||||
import {currencies} from 'app/common/Locales';
|
||||
|
||||
interface CurrencyPickerOptions {
|
||||
// The label to use in the select menu for the default option.
|
||||
defaultCurrencyLabel: string;
|
||||
}
|
||||
|
||||
export function buildCurrencyPicker(
|
||||
owner: IDisposableOwner,
|
||||
currency: Observable<string|undefined>,
|
||||
onSave: (value: string|undefined) => void,
|
||||
{defaultCurrencyLabel}: CurrencyPickerOptions
|
||||
) {
|
||||
const currencyItems: ACSelectItem[] = currencies
|
||||
.map(item => ({
|
||||
value: item.code,
|
||||
label: `${item.code} ${item.name}`,
|
||||
cleanText: `${item.code} ${item.name}`.trim().toLowerCase(),
|
||||
}));
|
||||
|
||||
// Add default currency label option to the very front.
|
||||
currencyItems.unshift({
|
||||
label : defaultCurrencyLabel,
|
||||
value : defaultCurrencyLabel,
|
||||
cleanText : defaultCurrencyLabel.toLowerCase(),
|
||||
});
|
||||
// Create a computed that will display 'Local currency' as a value and label
|
||||
// when `currency` is undefined.
|
||||
const valueObs = Computed.create(owner, (use) => use(currency) || defaultCurrencyLabel);
|
||||
const acIndex = new ACIndexImpl<ACSelectItem>(currencyItems, 200, true);
|
||||
return buildACSelect(owner,
|
||||
{
|
||||
acIndex, valueObs,
|
||||
save(_, item: ACSelectItem | undefined) {
|
||||
// Save only if we have found a match
|
||||
if (!item) {
|
||||
throw new Error("Invalid currency");
|
||||
}
|
||||
// For default value, return undefined to use default currency for document.
|
||||
onSave(item.value === defaultCurrencyLabel ? undefined : item.value);
|
||||
}
|
||||
},
|
||||
testId("currency-autocomplete")
|
||||
);
|
||||
}
|
||||
@@ -30,7 +30,9 @@ Object.defineProperty(Datepicker.prototype, 'isInput', {
|
||||
function DateEditor(options) {
|
||||
// A string that is always `UTC` in the DateEditor, eases DateTimeEditor inheritance.
|
||||
this.timezone = options.timezone || 'UTC';
|
||||
|
||||
this.dateFormat = options.field.widgetOptionsJson.peek().dateFormat;
|
||||
this.locale = options.field.documentSettings.peek().locale;
|
||||
|
||||
// Strip moment format string to remove markers unsupported by the datepicker.
|
||||
this.safeFormat = DateEditor.parseMomentToSafe(this.dateFormat);
|
||||
@@ -68,6 +70,10 @@ function DateEditor(options) {
|
||||
todayHighlight: true,
|
||||
todayBtn: 'linked',
|
||||
assumeNearbyYear: TWO_DIGIT_YEAR_THRESHOLD,
|
||||
// Datepicker supports most of the languages. They just need to be included in the bundle
|
||||
// or by script tag, i.e.
|
||||
// <script src="bootstrap-datepicker/dist/locales/bootstrap-datepicker.pl.min.js"></script>
|
||||
language : this.getLanguage(),
|
||||
// Convert the stripped format string to one suitable for the datepicker.
|
||||
format: DateEditor.parseSafeToCalendar(this.safeFormat)
|
||||
});
|
||||
@@ -168,5 +174,13 @@ DateEditor.parseSafeToCalendar = function(sFormat) {
|
||||
return sFormat.replace(/\bdddd\b/g, 'DD'); // dddd -> DD
|
||||
};
|
||||
|
||||
// Gets the language based on the current locale.
|
||||
DateEditor.prototype.getLanguage = function() {
|
||||
// this requires a polyfill, i.e. https://www.npmjs.com/package/@formatjs/intl-locale
|
||||
// more info about ts: https://github.com/microsoft/TypeScript/issues/37326
|
||||
// return new Intl.Locale(locale).language;
|
||||
return this.locale.substr(0, this.locale.indexOf("-"));
|
||||
}
|
||||
|
||||
|
||||
module.exports = DateEditor;
|
||||
|
||||
@@ -47,7 +47,7 @@ export abstract class NewAbstractWidget extends Disposable {
|
||||
// Note that its easier to create a knockout computed from the several knockout observables,
|
||||
// but then we turn it into a grainjs observable.
|
||||
const formatter = this.autoDispose(ko.computed(() =>
|
||||
createFormatter(field.displayColModel().type(), this.options())));
|
||||
createFormatter(field.displayColModel().type(), this.options(), field.documentSettings())));
|
||||
this.valueFormatter = fromKo(formatter);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,13 +11,15 @@ import {NTextBox} from 'app/client/widgets/NTextBox';
|
||||
import {clamp} from 'app/common/gutil';
|
||||
import {buildNumberFormat, NumberFormatOptions, NumMode, NumSign} from 'app/common/NumberFormat';
|
||||
import {Computed, dom, DomContents, DomElementArg, fromKo, MultiHolder, Observable, styled} from 'grainjs';
|
||||
import {buildCurrencyPicker} from "app/client/widgets/CurrencyPicker";
|
||||
import * as LocaleCurrency from "locale-currency";
|
||||
|
||||
|
||||
const modeOptions: Array<ISelectorOption<NumMode>> = [
|
||||
{value: 'currency', label: '$'},
|
||||
{value: 'decimal', label: ','},
|
||||
{value: 'percent', label: '%'},
|
||||
{value: 'scientific', label: 'Exp'}
|
||||
{value: 'percent', label: '%'},
|
||||
{value: 'scientific', label: 'Exp'}
|
||||
];
|
||||
|
||||
const signOptions: Array<ISelectorOption<NumSign>> = [
|
||||
@@ -37,17 +39,25 @@ export class NumericTextBox extends NTextBox {
|
||||
const holder = new MultiHolder();
|
||||
|
||||
// Resolved options, to show default min/max decimals, which change depending on numMode.
|
||||
const resolved = Computed.create<Intl.ResolvedNumberFormatOptions>(holder, (use) =>
|
||||
buildNumberFormat({numMode: use(this.options).numMode}).resolvedOptions());
|
||||
const resolved = Computed.create<Intl.ResolvedNumberFormatOptions>(holder, (use) => {
|
||||
const {numMode} = use(this.options);
|
||||
const docSettings = use(this.field.documentSettings);
|
||||
return buildNumberFormat({numMode}, docSettings).resolvedOptions();
|
||||
});
|
||||
|
||||
// Prepare various observables that reflect the options in the UI.
|
||||
const options = fromKo(this.options);
|
||||
const numMode = Computed.create(holder, options, (use, opts) => opts.numMode || null);
|
||||
const docSettings = fromKo(this.field.documentSettings);
|
||||
const numMode = Computed.create(holder, options, (use, opts) => (opts.numMode as NumMode) || null);
|
||||
const numSign = Computed.create(holder, options, (use, opts) => opts.numSign || null);
|
||||
const currency = Computed.create(holder, options, (use, opts) => opts.currency);
|
||||
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);
|
||||
const docCurrency = Computed.create(holder, docSettings, (use, settings) =>
|
||||
settings.currency ?? LocaleCurrency.getCurrency(settings.locale)
|
||||
);
|
||||
|
||||
// Save a value as the given property in this.options() observable. Set it, save, and revert
|
||||
// on save error. This is similar to what modelUtil.setSaveValue() does.
|
||||
@@ -66,6 +76,7 @@ export class NumericTextBox extends NTextBox {
|
||||
// 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);
|
||||
const setCurrency = (val: string|undefined) => setSave('currency', val);
|
||||
|
||||
return [
|
||||
super.buildConfigDom(),
|
||||
@@ -75,6 +86,16 @@ export class NumericTextBox extends NTextBox {
|
||||
makeButtonSelect(numMode, modeOptions, setMode, {}, cssModeSelect.cls(''), testId('numeric-mode')),
|
||||
makeButtonSelect(numSign, signOptions, setSign, {}, cssSignSelect.cls(''), testId('numeric-sign')),
|
||||
),
|
||||
dom.maybe((use) => use(numMode) === 'currency', () => [
|
||||
cssLabel('Currency'),
|
||||
cssRow(
|
||||
dom.domComputed(docCurrency, (defaultCurrency) =>
|
||||
buildCurrencyPicker(holder, currency, setCurrency,
|
||||
{defaultCurrencyLabel: `Default currency (${defaultCurrency})`})
|
||||
),
|
||||
testId("numeric-currency")
|
||||
)
|
||||
]),
|
||||
cssLabel('Decimals'),
|
||||
cssRow(
|
||||
decimals('min', minDecimals, defaultMin, setMinDecimals, testId('numeric-min-decimals')),
|
||||
@@ -84,12 +105,13 @@ export class NumericTextBox extends NTextBox {
|
||||
}
|
||||
}
|
||||
|
||||
function numberOrDefault<T>(value: unknown, def: T): number|T {
|
||||
|
||||
function numberOrDefault<T>(value: unknown, def: T): number | T {
|
||||
return typeof value !== 'undefined' ? Number(value) : def;
|
||||
}
|
||||
|
||||
// Helper used by setSave() above to reset some properties when switching modes.
|
||||
function updateOptions(prop: keyof NumberFormatOptions, value: unknown): NumberFormatOptions {
|
||||
function updateOptions(prop: keyof NumberFormatOptions, value: unknown): Partial<NumberFormatOptions> {
|
||||
// Reset the numSign to default when toggling mode to percent or scientific.
|
||||
if (prop === 'numMode' && (!value || value === 'scientific' || value === 'percent')) {
|
||||
return {numSign: undefined};
|
||||
@@ -99,7 +121,7 @@ function updateOptions(prop: keyof NumberFormatOptions, value: unknown): NumberF
|
||||
|
||||
function decimals(
|
||||
label: string,
|
||||
value: Observable<number|''>,
|
||||
value: Observable<number | ''>,
|
||||
defaultValue: Observable<number>,
|
||||
setFunc: (val?: number) => void, ...args: DomElementArg[]
|
||||
) {
|
||||
|
||||
@@ -14,8 +14,11 @@ export class Spinner extends NumericTextBox {
|
||||
|
||||
constructor(field: ViewFieldRec) {
|
||||
super(field);
|
||||
const resolved = this.autoDispose(ko.computed(() =>
|
||||
buildNumberFormat({numMode: this.options().numMode}).resolvedOptions()));
|
||||
const resolved = this.autoDispose(ko.computed(() => {
|
||||
const {numMode} = this.options();
|
||||
const docSettings = this.field.documentSettings();
|
||||
return buildNumberFormat({numMode}, docSettings).resolvedOptions();
|
||||
}));
|
||||
this._stepSize = this.autoDispose(ko.computed(() => {
|
||||
const extraScaling = (this.options().numMode === 'percent') ? 2 : 0;
|
||||
return Math.pow(10, -(this.options().decimals || resolved().minimumFractionDigits) - extraScaling);
|
||||
|
||||
Reference in New Issue
Block a user