mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) move client code to core
Summary: This moves all client code to core, and makes minimal fix-ups to get grist and grist-core to compile correctly. The client works in core, but I'm leaving clean-up around the build and bundles to follow-up. Test Plan: existing tests pass; server-dev bundle looks sane Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2627
This commit is contained in:
200
app/client/widgets/NumericTextBox.ts
Normal file
200
app/client/widgets/NumericTextBox.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* See app/common/NumberFormat for description of options we support.
|
||||
*/
|
||||
import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
|
||||
import {reportError} from 'app/client/models/errors';
|
||||
import {cssLabel, cssRow} from 'app/client/ui/RightPanel';
|
||||
import {ISelectorOption, makeButtonSelect} from 'app/client/ui2018/buttonSelect';
|
||||
import {colors, testId} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
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, DomElementArg, fromKo, MultiHolder, Observable, styled} from 'grainjs';
|
||||
|
||||
|
||||
const modeOptions: Array<ISelectorOption<NumMode>> = [
|
||||
{value: 'currency', label: '$'},
|
||||
{value: 'decimal', label: ','},
|
||||
{value: 'percent', label: '%'},
|
||||
{value: 'scientific', label: 'Exp'}
|
||||
];
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public buildConfigDom() {
|
||||
// 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.
|
||||
const resolved = Computed.create<Intl.ResolvedNumberFormatOptions>(holder, (use) =>
|
||||
buildNumberFormat({numMode: use(this.options).numMode}).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 numSign = Computed.create(holder, options, (use, opts) => opts.numSign || null);
|
||||
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);
|
||||
|
||||
// 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.
|
||||
const setSave = (prop: keyof NumberFormatOptions, value: unknown) => {
|
||||
const orig = {...this.options.peek()};
|
||||
if (value !== orig[prop]) {
|
||||
this.options({...orig, [prop]: value, ...updateOptions(prop, value)});
|
||||
this.options.save().catch((err) => { reportError(err); this.options(orig); });
|
||||
}
|
||||
};
|
||||
|
||||
// Prepare setters for the UI elements.
|
||||
// Min/max fraction digits may range from 0 to 20; other values are invalid.
|
||||
const setMinDecimals = (val?: number) => setSave('decimals', val && clamp(val, 0, 20));
|
||||
const setMaxDecimals = (val?: number) => setSave('maxDecimals', val && clamp(val, 0, 20));
|
||||
// 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);
|
||||
|
||||
return dom.update(super.buildConfigDom(),
|
||||
dom.autoDispose(holder),
|
||||
cssLabel('Number Format'),
|
||||
cssRow(
|
||||
makeButtonSelect(numMode, modeOptions, setMode, {}, cssModeSelect.cls(''), testId('numeric-mode')),
|
||||
makeButtonSelect(numSign, signOptions, setSign, {}, cssSignSelect.cls(''), testId('numeric-sign')),
|
||||
),
|
||||
cssLabel('Decimals'),
|
||||
cssRow(
|
||||
decimals('min', minDecimals, defaultMin, setMinDecimals, testId('numeric-min-decimals')),
|
||||
decimals('max', maxDecimals, defaultMax, setMaxDecimals, testId('numeric-max-decimals')),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
// Reset the numSign to default when toggling mode to percent or scientific.
|
||||
if (prop === 'numMode' && (!value || value === 'scientific' || value === 'percent')) {
|
||||
return {numSign: undefined};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function decimals(
|
||||
label: string,
|
||||
value: Observable<number|''>,
|
||||
defaultValue: Observable<number>,
|
||||
setFunc: (val?: number) => void, ...args: DomElementArg[]
|
||||
) {
|
||||
return cssDecimalsBox(
|
||||
cssNumLabel(label),
|
||||
cssNumInput({type: 'text', size: '2', min: '0'},
|
||||
dom.prop('value', value),
|
||||
dom.prop('placeholder', defaultValue),
|
||||
dom.on('change', (ev, elem) => {
|
||||
const newVal = parseInt(elem.value, 10);
|
||||
// Set value explicitly before its updated via setFunc; this way the value reflects the
|
||||
// observable in the case the observable is left unchanged (e.g. because of clamping).
|
||||
elem.value = String(value.get());
|
||||
setFunc(Number.isNaN(newVal) ? undefined : newVal);
|
||||
elem.blur();
|
||||
}),
|
||||
dom.on('focus', (ev, elem) => elem.select()),
|
||||
),
|
||||
cssSpinner(
|
||||
cssSpinnerBtn(cssSpinnerTop('DropdownUp'),
|
||||
dom.on('click', () => setFunc(numberOrDefault(value.get(), defaultValue.get()) + 1))),
|
||||
cssSpinnerBtn(cssSpinnerBottom('Dropdown'),
|
||||
dom.on('click', () => setFunc(numberOrDefault(value.get(), defaultValue.get()) - 1))),
|
||||
),
|
||||
...args
|
||||
);
|
||||
}
|
||||
|
||||
const cssDecimalsBox = styled('div', `
|
||||
position: relative;
|
||||
flex: auto;
|
||||
--icon-color: ${colors.slate};
|
||||
color: ${colors.slate};
|
||||
font-weight: normal;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&:first-child {
|
||||
margin-right: 16px;
|
||||
}
|
||||
`);
|
||||
|
||||
const cssNumLabel = styled('div', `
|
||||
position: absolute;
|
||||
padding-left: 8px;
|
||||
pointer-events: none;
|
||||
`);
|
||||
|
||||
const cssNumInput = styled('input', `
|
||||
padding: 4px 32px 4px 40px;
|
||||
border: 1px solid ${colors.darkGrey};
|
||||
border-radius: 3px;
|
||||
color: ${colors.dark};
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
`);
|
||||
|
||||
const cssSpinner = styled('div', `
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
width: 16px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`);
|
||||
|
||||
const cssSpinnerBtn = styled('div', `
|
||||
flex: 1 1 0px;
|
||||
min-height: 0px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
&:hover {
|
||||
--icon-color: ${colors.dark};
|
||||
}
|
||||
`);
|
||||
|
||||
const cssSpinnerTop = styled(icon, `
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
`);
|
||||
|
||||
const cssSpinnerBottom = styled(icon, `
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
`);
|
||||
|
||||
const cssModeSelect = styled(makeButtonSelect, `
|
||||
flex: 4 4 0px;
|
||||
background-color: white;
|
||||
`);
|
||||
|
||||
const cssSignSelect = styled(makeButtonSelect, `
|
||||
flex: 1 1 0px;
|
||||
background-color: white;
|
||||
margin-left: 16px;
|
||||
`);
|
||||
Reference in New Issue
Block a user