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>( 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): HTMLElement { switch (type) { case 'boolean': return squareCheckbox(value as Observable); 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}; } `);