Skip to content

Commit

Permalink
feat: add simple code-editor
Browse files Browse the repository at this point in the history
  • Loading branch information
runar-rkmedia committed Nov 13, 2021
1 parent 4c70f75 commit 97fcbc6
Show file tree
Hide file tree
Showing 14 changed files with 389 additions and 150 deletions.
4 changes: 3 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
},
"dependencies": {
"@felte/reporter-tippy": "^0.3.9",
"@joshnuss/svelte-codemirror": "^0.2.6",
"chart.js": "^3.6.0",
"codemirror": "^5.63.3",
"date-fns": "^2.25.0",
"felte": "^0.8.5",
"lodash.clonedeep": "^4.5.0",
Expand All @@ -26,7 +28,6 @@
"nanoid": "^3.1.29",
"plotly.js": "^2.6.2",
"svelte": "^3.43.1",
"svelte-highlight": "^5.0.0",
"svelte-tiny-linked-charts": "^1.0.6",
"tippy.js": "^6.3.3",
"toml-js": "^0.0.8",
Expand All @@ -35,6 +36,7 @@
"devDependencies": {
"@dtsgenerator/replace-namespace": "^1.5.1",
"@tsconfig/svelte": "^2.0.1",
"@types/codemirror": "^5.60.5",
"@types/lodash.debounce": "^4.0.6",
"@types/lodash.merge": "^4.6.6",
"@types/node": "^16.10.2",
Expand Down
19 changes: 16 additions & 3 deletions frontend/src/components/Button.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,34 @@
import { createEventDispatcher } from 'svelte'
export let preventDefault = true
export let active = false
export let toggle: boolean | null = null
export let icon: IconType | undefined = undefined
// TODO: support all colors
export let color: Colors | 'danger' = ''
export let disabled: boolean = false
export let type: string = ''
const dispatch = createEventDispatcher()
$: iconToUse =
icon || (toggle === true && 'toggleOn') || (toggle === false && 'toggleOff')
</script>

<button
class:btn-reset={true}
class={color}
class:active
class:toggle
{type}
class:icon-button={!!icon}
class:icon-button={!!icon || toggle}
{disabled}
on:click={(e) => {
if (preventDefault) {
e.preventDefault()
}
dispatch('click', e)
}}>
{#if icon}
<Icon {icon} />
{#if iconToUse}
<Icon icon={iconToUse} />
{/if}
<slot />
</button>
Expand All @@ -39,4 +45,11 @@
transform: scale(1.05);
transition: all 120ms ease-in-out;
}
.active {
filter: brightness(0.8);
text-decoration: underline;
text-decoration-color: var(--color-red);
text-decoration-thickness: 4px;
}
</style>
8 changes: 7 additions & 1 deletion frontend/src/components/Code.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
export let language = 'json'
export let code: string
export let convert = false
export let noFormatSelector = false
import { onMount } from 'svelte'
let component: any
Expand All @@ -11,6 +12,11 @@
</script>

{#if component}
<svelte:component this={component} {language} {code} {convert} />
<svelte:component
this={component}
{noFormatSelector}
{language}
{code}
{convert} />
{/if}
loading
107 changes: 11 additions & 96 deletions frontend/src/components/CodeInner.svelte
Original file line number Diff line number Diff line change
@@ -1,100 +1,15 @@
<script lang="ts">
export let language = 'json'
export let language: 'json' | 'yaml' | 'toml' = 'json'
export let code: string
export let convert = false
import formatterYaml from 'yaml'
import formatterToml from 'toml-js'
import Highlight from 'svelte-highlight'
import yaml from 'svelte-highlight/src/languages/yaml'
import json from 'svelte-highlight/src/languages/json'
import toml from 'svelte-highlight/src/languages/ini'
import atomOneDark from 'svelte-highlight/src/styles/atom-one-dark'
import { state } from '../state'
import Alert from './Alert.svelte'
import Icon from './Icon.svelte'
let errorMsg = ''
function convertFormat(code: string | {}, format: string) {
errorMsg = ''
if (!code) {
return ''
}
if (!format) {
return code
}
try {
const obj = typeof code === 'string' ? JSON.parse(code) : code
switch (format) {
case 'yaml':
case 'yml':
return formatterYaml.stringify(obj, { sortMapEntries: true })
case 'toml':
return formatterToml.dump(obj)
case 'json':
return JSON.stringify(obj, null, 2)
default:
console.error('Unsupported format', format)
break
}
} catch (error) {
console.error('failed to convert to format', { code, format, error })
errorMsg = error
}
return code
}
export let noFormatSelector = false
import Editor from './Editor.svelte'
</script>

<svelte:head>
{@html atomOneDark}
</svelte:head>

<div class:no-code={!code}>
<button
class="icon-button primary"
on:click|preventDefault={() => {
switch ($state.codeLanguage) {
case 'json':
$state.codeLanguage = 'yaml'
break
case 'yaml':
$state.codeLanguage = 'toml'
break
case 'toml':
$state.codeLanguage = 'json'
break
default:
$state.codeLanguage = 'toml'
break
}
}}>
<Icon icon="code" />
{$state.codeLanguage}</button>
{#if errorMsg}
<Alert kind="error">
{errorMsg}
</Alert>
{/if}
<Highlight
language={{
yaml: yaml,
yml: yaml,
json: json,
toml: toml,
}[language]}
code={convert
? convertFormat(code, $state.codeLanguage || language)
: code} />
</div>

<style>
div {
position: relative;
}
button {
position: absolute;
right: 0;
top: calc(-1 * var(--size-3));
}
.no-code {
opacity: 0.5;
}
</style>
<Editor
initialValue={code}
bind:value={code}
{noFormatSelector}
config={{ readOnly: true, mode: language }}
initialLanguage={language}>
<slot name="title" slot="title" />
</Editor>
179 changes: 179 additions & 0 deletions frontend/src/components/Editor.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte'
import CodeMirror, { fromTextArea } from 'codemirror'
import 'codemirror/mode/javascript/javascript'
import 'codemirror/mode/toml/toml'
import 'codemirror/mode/yaml/yaml'
import 'codemirror/theme/dracula.css'
import 'codemirror/lib/codemirror.css'
import type { EditorFromTextArea, EditorConfiguration } from 'codemirror'
import { convertStringToCodeFormat } from 'util/codeFormat'
import Button from './Button.svelte'
import Alert from './Alert.svelte'
import { state } from 'state'
import Tip from './Tip.svelte'
import CodeInner from './CodeInner.svelte'
import Collapse from './Collapse.svelte'
export let id: string | undefined = undefined
export let name: string | undefined = undefined
export let value: string = ''
export let initialValue = ''
export let noFormatSelector = false
export let initialLanguage: 'json' | 'toml' | 'yaml' | 'graphql' = 'json'
export let config: EditorConfiguration = {}
// export let outFormat: 'json' | 'toml' | 'yaml' = initialLanguage
let editor: EditorFromTextArea
let errorMessage: string | null
let textarea: HTMLTextAreaElement
let _config: EditorConfiguration = {
theme: 'dracula',
mode: config?.mode || $state.codeLanguage,
lineWrapping: true,
lineNumbers: true,
tabSize: 2,
...config,
}
if (_config.mode === 'json') {
_config.mode = 'javascript'
}
$: {
if (editor && config.readOnly) {
editor.setValue(value)
console.log('setting value')
editor.setOption('mode', _config.mode)
}
}
const setFormat = (format: typeof initialLanguage) => {
editor.setOption('mode', format === 'json' ? 'javascript' : format)
switch (format) {
case 'json':
case 'toml':
case 'yaml':
$state.codeLanguage = format
break
default:
return
}
if ($state.editorRawFormat) {
return
}
const [c, err] = convertStringToCodeFormat(editor.getValue(), format)
if (!err) {
editor.setValue(c as string)
return
}
}
function reset() {
editor.setValue(initialValue || '')
// if ($state.editorRawFormat) {
// return
// }
if (initialLanguage) {
setFormat($state.codeLanguage || 'yaml')
return
}
}
onMount(() => {
editor = fromTextArea(textarea, _config)
editor.setSize('100%', '100%')
editor.on('change', (e) => {
if ($state.editorRawFormat) {
value = editor.getValue()
return
}
switch (_config.mode) {
case 'javascript':
case 'toml':
case 'yaml':
break
default:
return
}
const [c, err] = convertStringToCodeFormat(
editor.getValue(),
initialLanguage
)
errorMessage = err
if (err) {
return
}
value = c || ''
})
if (initialValue) {
reset()
}
})
onDestroy(() => {
editor.toTextArea()
})
const langs: Array<typeof initialLanguage> = ['yaml', 'toml', 'json']
</script>

<slot name="title" />
{#if !config.readOnly}
<div class="header">
<Button color="danger" disabled={value === initialValue} on:click={reset}>
Reset
</Button>
</div>
{/if}
<textarea bind:this={textarea} {name} {id} />
{#if !noFormatSelector}
<div class="footer">
<Button
color="primary"
toggle={$state.editorRawFormat}
on:click={() => ($state.editorRawFormat = !$state.editorRawFormat)}>
Raw
</Button>
{#each langs as l}
<Button
active={$state.codeLanguage === l}
color="secondary"
on:click={() => setFormat(l)}>{l}</Button>
{/each}
</div>
{/if}
{#if !config.readOnly}
<Tip key="editor-format">
<p>
JSON is often used in API's, but is not always something that you want to
edit manually.
</p>
<p>
TOML and YAML are often considered better for human-readability and
editing.
</p>
<p>
You can therefore edit in a different language than what the backend
supports
</p>
<p>Before sending, this value will be converted.</p>
<p>
If you do not want this behaviour, you can disable convertion by setting
"Raw"
</p>
</Tip>
{#if errorMessage}
<Alert kind="error">{errorMessage}</Alert>
{/if}
{#if _config.mode !== 'graphql'}
<paper>
<Collapse key="editor-preview">
<div slot="title">Preview</div>
<CodeInner noFormatSelector={true} bind:code={value} />
</Collapse>
</paper>
{/if}
{/if}

<style>
.footer,
.header {
background-color: #2b2b2b;
display: flex;
justify-content: flex-end;
}
</style>
Loading

0 comments on commit 97fcbc6

Please sign in to comment.