mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
119 lines
3.8 KiB
TypeScript
119 lines
3.8 KiB
TypeScript
|
import {bigBasicButton, bigPrimaryButton} from 'app/client/ui2018/buttons';
|
||
|
import {squareCheckbox} from 'app/client/ui2018/checkbox';
|
||
|
import {colors, testId} from 'app/client/ui2018/cssVars';
|
||
|
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(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('Back to preview', 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, `
|
||
|
position: relative;
|
||
|
display: inline-block;
|
||
|
outline: none;
|
||
|
height: 28px;
|
||
|
border: 1px solid ${colors.darkGrey};
|
||
|
border-radius: 3px;
|
||
|
padding: 0 6px;
|
||
|
width: 100%;
|
||
|
`);
|