gristlabs_grist-core/app/client/components/ParseOptions.ts

126 lines
4.0 KiB
TypeScript
Raw Normal View History

import {bigBasicButton, bigPrimaryButton} from 'app/client/ui2018/buttons';
import {squareCheckbox} from 'app/client/ui2018/checkbox';
import {testId, theme} from 'app/client/ui2018/cssVars';
import {makeLinks} from 'app/client/ui2018/links';
import {cssModalButtons} from 'app/client/ui2018/modals';
import {ParseOptionSchema} from 'app/plugin/FileParserAPI';
import {Computed, dom, DomContents, IDisposableOwner, input, Observable, styled} from 'grainjs';
import fromPairs = require('lodash/fromPairs');
import invert = require('lodash/invert');
export type ParseOptionValueType = boolean|string|number;
export interface ParseOptionValues {
[name: string]: ParseOptionValueType;
}
/**
* EscapeChars contains mapping for some escape characters that we need to convert
* for displaying in input fields
*/
interface EscapeChars {
[char: string]: string;
}
const escapeCharDict: EscapeChars = {
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
};
const invertedEscapeCharDict: EscapeChars = invert(escapeCharDict);
// Helpers to escape and unescape certain non-printable characters that are useful in parsing
// options, e.g. as separators.
function escapeChars(value: string) {
return value.replace(/[\n\r\t]/g, (match) => escapeCharDict[match]);
}
function unescapeChars(value: string) {
return value.replace(/\\[nrt]/g, (match) => invertedEscapeCharDict[match]);
}
/**
* Builds a DOM form consisting of inputs built according to schema, with the passed-in values.
* The included "Update" button is enabled if any value has changed, and calls doUpdate() with the
* current values.
*/
export function buildParseOptionsForm(
owner: IDisposableOwner,
schema: ParseOptionSchema[],
values: ParseOptionValues,
doUpdate: (v: ParseOptionValues) => void,
doCancel: () => void,
): DomContents {
const items = schema.filter(item => item.visible);
const optionsMap = new Map<string, Observable<ParseOptionValueType>>(
items.map((item) => [item.name, Observable.create(owner, values[item.name])]));
function collectParseOptions(): ParseOptionValues {
return fromPairs(items.map((item) => [item.name, optionsMap.get(item.name)!.get()]));
}
return [
cssParseOptionForm(
items.map((item) => cssParseOption(
cssParseOptionName(makeLinks(item.label)),
optionToInput(owner, item.type, optionsMap.get(item.name)!),
testId('parseopts-opt'),
)),
),
cssModalButtons(
dom.domComputed((use) => items.every((item) => use(optionsMap.get(item.name)!) === values[item.name]),
(unchanged) => (unchanged ?
bigBasicButton('Close', dom.on('click', doCancel), testId('parseopts-back')) :
bigPrimaryButton('Update preview', dom.on('click', () => doUpdate(collectParseOptions())),
testId('parseopts-update'))
)
)
),
];
}
function optionToInput(owner: IDisposableOwner, type: string, value: Observable<ParseOptionValueType>): HTMLElement {
switch (type) {
case 'boolean': return squareCheckbox(value as Observable<boolean>);
default: {
const obs = Computed.create(owner, (use) => escapeChars(String(use(value) || "")))
.onWrite((val) => value.set(unescapeChars(val)));
return cssInputText(obs, {onInput: true},
dom.on('focus', (ev, elem) => elem.select()));
}
}
}
const cssParseOptionForm = styled('div', `
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 16px 0;
width: 400px;
overflow-y: auto;
`);
const cssParseOption = styled('div', `
flex: none;
margin: 8px 0;
width: calc(50% - 16px);
font-weight: initial; /* negate bootstrap */
`);
const cssParseOptionName = styled('div', `
margin-bottom: 8px;
`);
const cssInputText = styled(input, `
color: ${theme.inputFg};
background-color: ${theme.inputBg};
position: relative;
display: inline-block;
outline: none;
height: 28px;
border: 1px solid ${theme.inputBorder};
border-radius: 3px;
padding: 0 6px;
width: 100%;
&::placeholder {
color: ${theme.inputPlaceholderFg};
}
`);