2022-01-22 18:24:57 +01:00
|
|
|
import { Box, BoxProps } from '@chakra-ui/react'
|
|
|
|
import { EditorState, EditorView, basicSetup } from '@codemirror/basic-setup'
|
|
|
|
import { json } from '@codemirror/lang-json'
|
2022-01-24 15:29:01 +01:00
|
|
|
import { css } from '@codemirror/lang-css'
|
2022-02-10 08:08:58 +01:00
|
|
|
import { javascript } from '@codemirror/lang-javascript'
|
|
|
|
import { html } from '@codemirror/lang-html'
|
2022-02-02 08:05:02 +01:00
|
|
|
import { useEffect, useRef, useState } from 'react'
|
2022-01-22 18:24:57 +01:00
|
|
|
|
|
|
|
type Props = {
|
|
|
|
value: string
|
2022-02-10 08:08:58 +01:00
|
|
|
lang?: 'css' | 'json' | 'js' | 'html'
|
2022-01-22 18:24:57 +01:00
|
|
|
onChange?: (value: string) => void
|
|
|
|
isReadOnly?: boolean
|
|
|
|
}
|
|
|
|
export const CodeEditor = ({
|
|
|
|
value,
|
2022-01-24 15:29:01 +01:00
|
|
|
lang,
|
2022-01-22 18:24:57 +01:00
|
|
|
onChange,
|
|
|
|
isReadOnly = false,
|
|
|
|
...props
|
|
|
|
}: Props & Omit<BoxProps, 'onChange'>) => {
|
|
|
|
const editorContainer = useRef<HTMLDivElement | null>(null)
|
|
|
|
const editorView = useRef<EditorView | null>(null)
|
2022-02-02 08:05:02 +01:00
|
|
|
const [plainTextValue, setPlainTextValue] = useState(value)
|
2022-01-22 18:24:57 +01:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (!editorView.current || !isReadOnly) return
|
|
|
|
editorView.current.dispatch({
|
2022-02-10 08:08:58 +01:00
|
|
|
changes: {
|
|
|
|
from: 0,
|
|
|
|
to: editorView.current.state.doc.length,
|
|
|
|
insert: value,
|
|
|
|
},
|
2022-01-22 18:24:57 +01:00
|
|
|
})
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
}, [value])
|
|
|
|
|
2022-02-02 08:05:02 +01:00
|
|
|
useEffect(() => {
|
|
|
|
if (!onChange || plainTextValue === value) return
|
|
|
|
onChange(plainTextValue)
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
}, [plainTextValue])
|
|
|
|
|
2022-01-22 18:24:57 +01:00
|
|
|
useEffect(() => {
|
2022-02-10 08:08:58 +01:00
|
|
|
const editor = initEditor(value)
|
|
|
|
return () => {
|
|
|
|
editor?.destroy()
|
|
|
|
}
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
const initEditor = (value: string) => {
|
2022-01-22 18:24:57 +01:00
|
|
|
if (!editorContainer.current) return
|
|
|
|
const updateListenerExtension = EditorView.updateListener.of((update) => {
|
|
|
|
if (update.docChanged && onChange)
|
2022-02-02 08:05:02 +01:00
|
|
|
setPlainTextValue(update.state.doc.toJSON().join(' '))
|
2022-01-22 18:24:57 +01:00
|
|
|
})
|
2022-01-24 15:29:01 +01:00
|
|
|
const extensions = [
|
|
|
|
updateListenerExtension,
|
|
|
|
basicSetup,
|
|
|
|
EditorState.readOnly.of(isReadOnly),
|
|
|
|
]
|
2022-02-09 17:41:45 +01:00
|
|
|
if (lang === 'json') extensions.push(json())
|
|
|
|
if (lang === 'css') extensions.push(css())
|
2022-02-10 08:08:58 +01:00
|
|
|
if (lang === 'js') extensions.push(javascript())
|
|
|
|
if (lang === 'html') extensions.push(html())
|
2022-01-22 18:24:57 +01:00
|
|
|
const editor = new EditorView({
|
|
|
|
state: EditorState.create({
|
2022-01-24 15:29:01 +01:00
|
|
|
extensions,
|
2022-01-22 18:24:57 +01:00
|
|
|
}),
|
|
|
|
parent: editorContainer.current,
|
|
|
|
})
|
|
|
|
editor.dispatch({
|
|
|
|
changes: { from: 0, insert: value },
|
|
|
|
})
|
|
|
|
editorView.current = editor
|
2022-02-10 08:08:58 +01:00
|
|
|
return editor
|
|
|
|
}
|
2022-01-22 18:24:57 +01:00
|
|
|
|
2022-02-10 08:08:58 +01:00
|
|
|
return <Box ref={editorContainer} data-testid="code-editor" {...props} />
|
2022-01-22 18:24:57 +01:00
|
|
|
}
|