tldraw/components/code-panel/code-panel.tsx

240 lines
6.7 KiB
TypeScript
Raw Normal View History

2021-05-14 22:56:41 +00:00
/* eslint-disable @typescript-eslint/ban-ts-comment */
import styled from 'styles'
import { useStateDesigner } from '@state-designer/react'
import React, { useCallback, useEffect, useRef } from 'react'
import state, { useSelector } from 'state'
import { CodeError, CodeFile, CodeResult } from 'types'
import CodeDocs from './code-docs'
2021-06-21 21:35:28 +00:00
import { generateFromCode } from 'state/code/generate'
import * as Panel from '../panel'
import { IconButton } from '../shared'
2021-06-27 10:39:55 +00:00
import { X, Code, PlayCircle, ChevronUp, ChevronDown } from 'react-feather'
2021-06-24 14:59:56 +00:00
import dynamic from 'next/dynamic'
2021-06-27 10:39:55 +00:00
import { ReaderIcon } from '@radix-ui/react-icons'
2021-06-24 14:59:56 +00:00
const CodeEditor = dynamic(() => import('./code-editor'))
2021-05-14 22:56:41 +00:00
const increaseCodeSize = () => state.send('INCREASED_CODE_FONT_SIZE')
const decreaseCodeSize = () => state.send('DECREASED_CODE_FONT_SIZE')
const toggleCodePanel = () => state.send('TOGGLED_CODE_PANEL_OPEN')
2021-05-14 22:56:41 +00:00
2021-06-21 21:35:28 +00:00
export default function CodePanel(): JSX.Element {
2021-05-14 22:56:41 +00:00
const rContainer = useRef<HTMLDivElement>(null)
const isReadOnly = useSelector((s) => s.data.isReadOnly)
2021-05-16 08:33:08 +00:00
const fileId = useSelector((s) => s.data.currentCodeFileId)
const file = useSelector(
(s) => s.data.document.code[s.data.currentCodeFileId]
)
2021-05-17 21:27:18 +00:00
const isOpen = useSelector((s) => s.data.settings.isCodeOpen)
2021-05-14 22:56:41 +00:00
const fontSize = useSelector((s) => s.data.settings.fontSize)
const local = useStateDesigner({
data: {
code: file.code,
error: null as CodeError | null,
2021-05-14 22:56:41 +00:00
},
on: {
MOUNTED: 'setCode',
CHANGED_FILE: 'loadFile',
2021-05-14 22:56:41 +00:00
},
initial: 'editingCode',
2021-05-14 22:56:41 +00:00
states: {
editingCode: {
on: {
RAN_CODE: { do: 'saveCode', to: 'evaluatingCode' },
SAVED_CODE: { do: 'saveCode', to: 'evaluatingCode' },
CHANGED_CODE: { secretlyDo: 'setCode' },
CLEARED_ERROR: { if: 'hasError', do: 'clearError' },
TOGGLED_DOCS: { to: 'viewingDocs' },
2021-05-14 22:56:41 +00:00
},
},
evaluatingCode: {
async: {
await: 'evalCode',
onResolve: {
do: ['clearError', 'sendResultToGlobalState'],
to: 'editingCode',
},
onReject: { do: 'setErrorFromResult', to: 'editingCode' },
},
},
2021-05-14 22:56:41 +00:00
viewingDocs: {
on: {
TOGGLED_DOCS: { to: 'editingCode' },
2021-05-14 22:56:41 +00:00
},
},
},
conditions: {
hasError(data) {
return !!data.error
},
},
actions: {
loadFile(data, payload: { file: CodeFile }) {
data.code = payload.file.code
},
setCode(data, payload: { code: string }) {
data.code = payload.code
},
saveCode(data) {
2021-05-16 08:33:08 +00:00
const { code } = data
state.send('SAVED_CODE', { code })
2021-05-14 22:56:41 +00:00
},
clearError(data) {
data.error = null
},
setErrorFromResult(data, payload, result: CodeResult) {
data.error = result.error
},
sendResultToGlobalState(data, payload, result: CodeResult) {
state.send('GENERATED_FROM_CODE', result)
},
},
asyncs: {
evalCode(data) {
return new Promise((resolve, reject) => {
generateFromCode(state.data, data.code).then((result) => {
if (result.error !== null) {
reject(result)
} else {
resolve(result)
}
})
})
},
2021-05-14 22:56:41 +00:00
},
})
useEffect(() => {
local.send('CHANGED_FILE', { file })
2021-05-14 22:56:41 +00:00
}, [file])
useEffect(() => {
local.send('MOUNTED', { code: state.data.document.code[fileId].code })
2021-05-14 22:56:41 +00:00
return () => {
state.send('CHANGED_CODE', { fileId, code: local.data.code })
2021-05-14 22:56:41 +00:00
}
}, [])
const handleCodeChange = useCallback(
(code: string) => local.send('CHANGED_CODE', { code }),
[local]
)
const handleSave = useCallback(() => local.send('SAVED_CODE'), [local])
const handleKey = useCallback(() => local.send('CLEARED_ERROR'), [local])
const toggleDocs = useCallback(() => local.send('TOGGLED_DOCS'), [local])
2021-05-14 22:56:41 +00:00
const { error } = local.data
return (
<Panel.Root
bp={{ '@initial': 'mobile', '@sm': 'small' }}
data-bp-desktop
ref={rContainer}
isOpen={isOpen}
variant="code"
2021-06-27 13:53:06 +00:00
onWheel={(e) => e.stopPropagation()}
>
2021-05-14 22:56:41 +00:00
{isOpen ? (
2021-05-17 10:01:11 +00:00
<Panel.Layout>
<Panel.Header side="left">
<IconButton
bp={{ '@initial': 'mobile', '@sm': 'small' }}
size="small"
onClick={toggleCodePanel}
>
2021-05-14 22:56:41 +00:00
<X />
</IconButton>
<h3>Code</h3>
<ButtonsGroup>
<FontSizeButtons>
<IconButton
bp={{ '@initial': 'mobile', '@sm': 'small' }}
size="small"
disabled={!local.isIn('editingCode')}
onClick={increaseCodeSize}
2021-05-14 22:56:41 +00:00
>
<ChevronUp />
</IconButton>
<IconButton
size="small"
disabled={!local.isIn('editingCode')}
onClick={decreaseCodeSize}
2021-05-14 22:56:41 +00:00
>
<ChevronDown />
</IconButton>
</FontSizeButtons>
<IconButton
bp={{ '@initial': 'mobile', '@sm': 'small' }}
size="small"
onClick={toggleDocs}
>
2021-06-27 10:39:55 +00:00
<ReaderIcon />
2021-05-14 22:56:41 +00:00
</IconButton>
<IconButton
bp={{ '@initial': 'mobile', '@sm': 'small' }}
size="small"
disabled={!local.isIn('editingCode')}
onClick={handleSave}
2021-05-14 22:56:41 +00:00
>
<PlayCircle />
</IconButton>
</ButtonsGroup>
2021-05-17 10:01:11 +00:00
</Panel.Header>
<Panel.Content>
2021-05-14 22:56:41 +00:00
<CodeEditor
fontSize={fontSize}
readOnly={isReadOnly}
value={file.code}
error={error}
onChange={handleCodeChange}
onSave={handleSave}
onKey={handleKey}
2021-05-14 22:56:41 +00:00
/>
<CodeDocs isHidden={!local.isIn('viewingDocs')} />
2021-05-17 10:01:11 +00:00
</Panel.Content>
{error && <Panel.Footer>{error.message}</Panel.Footer>}
2021-05-17 10:01:11 +00:00
</Panel.Layout>
2021-05-14 22:56:41 +00:00
) : (
<IconButton
bp={{ '@initial': 'mobile', '@sm': 'small' }}
size="small"
onClick={toggleCodePanel}
>
2021-05-14 22:56:41 +00:00
<Code />
</IconButton>
)}
2021-05-17 10:01:11 +00:00
</Panel.Root>
2021-05-14 22:56:41 +00:00
)
}
const ButtonsGroup = styled('div', {
gridRow: '1',
gridColumn: '3',
display: 'flex',
2021-05-14 22:56:41 +00:00
})
const FontSizeButtons = styled('div', {
2021-05-14 22:56:41 +00:00
paddingRight: 4,
display: 'flex',
flexDirection: 'column',
2021-05-14 22:56:41 +00:00
'& > button': {
height: '50%',
'&:nth-of-type(1)': {
alignItems: 'flex-end',
2021-05-14 22:56:41 +00:00
},
'&:nth-of-type(2)': {
alignItems: 'flex-start',
2021-05-14 22:56:41 +00:00
},
'& svg': {
2021-05-14 22:56:41 +00:00
height: 12,
},
},
})