main
ts 88 lines 3.45 KB
Raw
1 import { ComponentProps, createElement as h, forwardRef, useState } from 'react'
2 import Editor from 'react-simple-code-editor'
3 import { FieldProps } from '@hfs/mui-grid-form'
4 import { Box, TextField, TextFieldProps } from '@mui/material'
5 import { focusableSelector, isCtrlKey, Optional, try_ } from './misc'
6 import _ from 'lodash'
7 import { highlight, languages } from 'prismjs'
8 import 'prismjs/components/prism-markup'
9 import 'prismjs/components/prism-javascript'
10 import 'prismjs/components/prism-yaml'
11 import 'prismjs/components/prism-css'
12 import 'prismjs/components/prism-json'
13 import 'prismjs/themes/prism-solarizedlight.css' // looks good both with light and dark theme
14
15 type OP = ComponentProps<typeof Editor>
16 type Already = 'highlight' | 'padding' | 'tabSize' | 'insertSpaces' | 'ignoreTabKey'
17 type TextEditorProps = Optional<OP, Already> & { lang?: 'plain' | 'js' | 'html' | 'css' | 'json' | 'yaml' }
18 export const TextEditor = forwardRef(({ style, lang='plain', ...props }: TextEditorProps, _ref) => h(Editor, {
19 // Editor component doesn't seem to support ref, but it didn't cause any problem yet
20 highlight: s => highlight(s, languages[lang], lang),
21 padding: 10,
22 tabSize: 4,
23 insertSpaces: true,
24 ignoreTabKey: false,
25 style: {
26 fontFamily: 'ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace',
27 fontSize: '1em',
28 flex: 1,
29 background: '#8883',
30 borderBottom: '1px solid #bbb',
31 ...style,
32 },
33 ...props,
34 }))
35
36 export function TextEditorField({ onChange, value, onBlur, setApi, lang, ...props }: FieldProps<string> & Omit<TextFieldProps, 'onChange'>) {
37 setApi?.({
38 getError() {
39 return lang === 'js' && value ? try_(() => new Function(value) && null, e => e.message)
40 : false
41 }
42 })
43 const [state, setState] = useState(value || '')
44 return h(TextField, {
45 multiline: true,
46 fullWidth: true,
47 value: state,
48 slotProps: {
49 ...props.slotProps,
50 input: { ...props.slotProps?.input, inputComponent: TextEditorAsInput },
51 htmlInput: { ...props.slotProps?.htmlInput, lang },
52 },
53 onChange(event) { setState(event.target.value) },
54 onBlur(event) {
55 onBlur?.(event)
56 onChange(event.target.value, { was: value, event })
57 },
58 ...props,
59 onKeyDown(ev) {
60 props.onKeyDown?.(ev)
61 if (isCtrlKey(ev) === 'Enter')
62 return onChange(state, { was: value, event: ev })
63 if (!ev.altKey || ev.key !== 'Tab') return
64 ev.preventDefault()
65 const focusable = document.querySelectorAll(focusableSelector)
66 const i = _.indexOf(focusable, ev.target)
67 const n = focusable.length
68 const next = (i + (ev.shiftKey ? -1 : 1) + n) % n
69 ;(focusable[next] as HTMLElement).focus()
70 console.debug(focusable[next])
71 },
72 })
73 }
74
75 const TextEditorAsInput = forwardRef<HTMLInputElement, any>(({ onChange, ...rest }: any, ref) =>
76 h(Box, { sx: { width: '100%', textarea: { outline: 0 } } },
77 h(TextEditor, {
78 ref,
79 padding: 2,
80 style: { background: 'initial', borderBottom: 'initial' },
81 ...rest,
82 onValueChange: value => onChange({ target: { value } }),
83 onKeyDown(ev) {
84 if (isCtrlKey(ev) === 'Enter')
85 ev.preventDefault()
86 },
87 })
88 ))