Finish Playground

This commit is contained in:
2024-01-12 16:57:25 +08:00
parent dc64b4993c
commit ed4cef1c78
13 changed files with 156 additions and 71 deletions

View File

@@ -64,10 +64,10 @@ const FileSelector: React.FC<FileSelectorProps> = ({
} }
const handleOnCancel = () => { const handleOnCancel = () => {
onError?.('')
if (!creating) { if (!creating) {
return return
} }
tabs.pop() tabs.pop()
setTabs([...tabs]) setTabs([...tabs])
setCreating(false) setCreating(false)
@@ -111,7 +111,10 @@ const FileSelector: React.FC<FileSelectorProps> = ({
return false return false
} }
if (tabs.includes(newFileName) && newFileName !== oldFileName) { if (
tabs.map((item) => item.toLowerCase()).includes(newFileName.toLowerCase()) &&
newFileName.toLowerCase() !== oldFileName.toLowerCase()
) {
onError?.(`File "${newFileName}" already exists.`) onError?.(`File "${newFileName}" already exists.`)
return false return false
} }

View File

@@ -1,14 +1,5 @@
[data-component=playground-code-editor] { [data-component=playground-code-editor] {
position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
.playground-code-editor-message {
position: absolute;
bottom: 0;
width: 100%;
color: white;
background-color: #FF4D4FAA;
padding: 5px 10px;
font-size: 1.6em;
}
} }

View File

@@ -3,7 +3,6 @@ import _ from 'lodash'
import '@/components/Playground/CodeEditor/code-editor.scss' import '@/components/Playground/CodeEditor/code-editor.scss'
import { IEditorOptions, IFiles, ITheme } from '@/components/Playground/shared' import { IEditorOptions, IFiles, ITheme } from '@/components/Playground/shared'
import { import {
ENTRY_FILE_NAME,
fileNameToLanguage, fileNameToLanguage,
getFileNameList, getFileNameList,
IMPORT_MAP_FILE_NAME IMPORT_MAP_FILE_NAME
@@ -44,7 +43,7 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
...props ...props
}) => { }) => {
const filteredFilesName = getFileNameList(files).filter( const filteredFilesName = getFileNameList(files).filter(
(item) => ![IMPORT_MAP_FILE_NAME, ENTRY_FILE_NAME].includes(item) && !files[item].hidden (item) => ![IMPORT_MAP_FILE_NAME].includes(item) && !files[item].hidden
) )
const propsSelectedFileName = const propsSelectedFileName =
props.selectedFileName || (filteredFilesName.length ? filteredFilesName[0] : '') props.selectedFileName || (filteredFilesName.length ? filteredFilesName[0] : '')
@@ -148,7 +147,7 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
onChange={handleOnChangeFileContent} onChange={handleOnChangeFileContent}
onJumpFile={handleOnChangeSelectedFile} onJumpFile={handleOnChangeSelectedFile}
/> />
{errorMsg && <div className={'playground-code-editor-message'}>{errorMsg}</div>} {errorMsg && <div className={'playground-error-message'}>{errorMsg}</div>}
</FlexBox> </FlexBox>
</> </>
) )

View File

@@ -9,11 +9,11 @@
<body> <body>
<script> <script>
window.addEventListener("error", (e) => { window.addEventListener("error", (e) => {
window.parent.postMessage({ type: "ERROR", message: e.message }); window.parent.postMessage({ type: "ERROR", msg: e.message });
}); });
window.addEventListener("load", () => { window.addEventListener("load", () => {
window.parent.postMessage({ type: "LOADED", message: "" }); window.parent.postMessage({ type: "LOADED", msg: "" });
}); });
window.addEventListener("message", ({ data }) => { window.addEventListener("message", ({ data }) => {
@@ -43,7 +43,7 @@
}; };
document.body.appendChild(script); document.body.appendChild(script);
URL.revokeObjectURL(oldSrc); URL.revokeObjectURL(oldSrc);
window.parent.postMessage({ type: "DONE", message: "" }); window.parent.postMessage({ type: "DONE", msg: "" });
} }
}); });
</script> </script>

View File

@@ -1,14 +1,14 @@
import React, { useRef, useState } from 'react' import React, { useRef, useState } from 'react'
import { IFiles } from '@/components/Playground/shared' import { IFiles, IImportMap } from '@/components/Playground/shared'
import iframeRaw from '@/components/Playground/Preview/iframe.html?raw' import iframeRaw from '@/components/Playground/Output/Preview/iframe.html?raw'
import { useUpdatedEffect } from '@/util/hooks' import { useUpdatedEffect } from '@/util/hooks'
import { IMPORT_MAP_FILE_NAME } from '@/components/Playground/files'
import Compiler from '@/components/Playground/compiler' import Compiler from '@/components/Playground/compiler'
import '@/components/Playground/Preview/preview.scss' import '@/components/Playground/Output/Preview/preview.scss'
interface PreviewProps { interface PreviewProps {
iframeKey: string iframeKey: string
files: IFiles files: IFiles
importMap: IImportMap
} }
interface IMessage { interface IMessage {
@@ -34,7 +34,7 @@ const getIframeUrl = (iframeRaw: string) => {
const iframeUrl = getIframeUrl(iframeRaw) const iframeUrl = getIframeUrl(iframeRaw)
const Preview: React.FC<PreviewProps> = ({ iframeKey, files }) => { const Preview: React.FC<PreviewProps> = ({ iframeKey, files, importMap }) => {
const iframeRef = useRef<HTMLIFrameElement>(null) const iframeRef = useRef<HTMLIFrameElement>(null)
const [errorMsg, setErrorMsg] = useState('') const [errorMsg, setErrorMsg] = useState('')
const [loaded, setLoaded] = useState(false) const [loaded, setLoaded] = useState(false)
@@ -48,15 +48,11 @@ const Preview: React.FC<PreviewProps> = ({ iframeKey, files }) => {
case 'ERROR': case 'ERROR':
setErrorMsg(msg) setErrorMsg(msg)
break break
default: case 'DONE':
setErrorMsg('') setErrorMsg('')
} }
} }
useEffect(() => {
console.error(errorMsg)
}, [errorMsg])
useUpdatedEffect(() => { useUpdatedEffect(() => {
window.addEventListener('message', handleMessage) window.addEventListener('message', handleMessage)
@@ -66,7 +62,7 @@ const Preview: React.FC<PreviewProps> = ({ iframeKey, files }) => {
}, []) }, [])
useUpdatedEffect(() => { useUpdatedEffect(() => {
Compiler.compile(files, JSON.parse(files[IMPORT_MAP_FILE_NAME].value)) Compiler.compile(files, importMap)
.then((result) => { .then((result) => {
if (loaded) { if (loaded) {
iframeRef.current?.contentWindow?.postMessage({ iframeRef.current?.contentWindow?.postMessage({
@@ -76,7 +72,7 @@ const Preview: React.FC<PreviewProps> = ({ iframeKey, files }) => {
} }
}) })
.catch((e) => { .catch((e) => {
setErrorMsg(e) setErrorMsg(`编译失败:${e.message}`)
}) })
}, [files, Compiler, loaded]) }, [files, Compiler, loaded])
@@ -88,6 +84,7 @@ const Preview: React.FC<PreviewProps> = ({ iframeKey, files }) => {
src={iframeUrl} src={iframeUrl}
sandbox="allow-popups-to-escape-sandbox allow-scripts allow-popups allow-forms allow-pointer-lock allow-top-navigation allow-modals allow-same-origin" sandbox="allow-popups-to-escape-sandbox allow-scripts allow-popups allow-forms allow-pointer-lock allow-top-navigation allow-modals allow-same-origin"
/> />
{errorMsg && <div className={'playground-error-message'}>{errorMsg}</div>}
</div> </div>
) )
} }

View File

@@ -1,5 +1,6 @@
[data-component=playground-preview] { [data-component=playground-preview] {
display: flex; display: flex;
position: relative;
iframe { iframe {
border: none; border: none;
flex: 1; flex: 1;

View File

@@ -1,12 +1,13 @@
import React from 'react' import React, { useState } from 'react'
import MonacoEditor from '@monaco-editor/react' import MonacoEditor from '@monaco-editor/react'
import { Loader } from 'esbuild-wasm' import { Loader } from 'esbuild-wasm'
import { useUpdatedEffect } from '@/util/hooks' import '@/components/Playground/Output/Transform/transform.scss'
import { IFile, ITheme } from '@/components/Playground/shared' import { useUpdatedEffect } from '@/util/hooks.tsx'
import Compiler from '@/components/Playground/compiler' import { IFile, ITheme } from '@/components/Playground/shared.ts'
import { cssToJs, jsonToJs } from '@/components/Playground/files' import Compiler from '@/components/Playground/compiler.ts'
import { MonacoEditorConfig } from '@/components/Playground/CodeEditor/Editor/monacoConfig' import { cssToJs, jsonToJs } from '@/components/Playground/files.ts'
import { addReactImport } from '@/components/Playground/utils' import { MonacoEditorConfig } from '@/components/Playground/CodeEditor/Editor/monacoConfig.ts'
import { addReactImport } from '@/components/Playground/utils.ts'
interface OutputProps { interface OutputProps {
file: IFile file: IFile
@@ -15,6 +16,7 @@ interface OutputProps {
const Transform: React.FC<OutputProps> = ({ file, theme }) => { const Transform: React.FC<OutputProps> = ({ file, theme }) => {
const [compiledCode, setCompiledCode] = useState('') const [compiledCode, setCompiledCode] = useState('')
const [errorMsg, setErrorMsg] = useState('')
const compile = (code: string, loader: Loader) => { const compile = (code: string, loader: Loader) => {
let _code = code let _code = code
@@ -25,9 +27,10 @@ const Transform: React.FC<OutputProps> = ({ file, theme }) => {
Compiler?.transform(_code, loader) Compiler?.transform(_code, loader)
.then((value) => { .then((value) => {
setCompiledCode(value.code) setCompiledCode(value.code)
setErrorMsg('')
}) })
.catch((e) => { .catch((e) => {
console.error('编译失败', e) setErrorMsg(`编译失败:${e.message}`)
}) })
} }
@@ -62,14 +65,15 @@ const Transform: React.FC<OutputProps> = ({ file, theme }) => {
}, [file, Compiler]) }, [file, Compiler])
return ( return (
<> <div data-component={'playground-transform'}>
<MonacoEditor <MonacoEditor
theme={theme} theme={theme}
language={'javascript'} language={'javascript'}
value={compiledCode} value={compiledCode}
options={{ ...MonacoEditorConfig, readOnly: true }} options={{ ...MonacoEditorConfig, readOnly: true }}
/> />
</> {errorMsg && <div className={'playground-error-message'}>{errorMsg}</div>}
</div>
) )
} }

View File

@@ -0,0 +1,3 @@
[data-component=playground-transform] {
position: relative;
}

View File

@@ -0,0 +1,40 @@
import React, { useState } from 'react'
import FlexBox from '@/components/common/FlexBox.tsx'
import FileSelector from '@/components/Playground/CodeEditor/FileSelector'
import Transform from '@/components/Playground/Output/Transform'
import { IFiles, IImportMap } from '@/components/Playground/shared.ts'
import Preview from '@/components/Playground/Output/Preview'
interface OutputProps {
files: IFiles
selectedFileName: string
importMap: IImportMap
}
const Output: React.FC<OutputProps> = ({ files, selectedFileName, importMap }) => {
const [selectedTab, setSelectedTab] = useState('Preview')
return (
<FlexBox data-component={'playground-code-output'}>
<FileSelector
files={{
Preview: { name: 'Preview', language: 'json', value: '' },
Transform: { name: 'Transform', language: 'json', value: '' }
}}
selectedFileName={selectedTab}
onChange={(tabName) => setSelectedTab(tabName)}
readonly
/>
{selectedTab === 'Preview' && (
<Preview
iframeKey={JSON.stringify(importMap)}
files={files}
importMap={importMap}
/>
)}
{selectedTab === 'Transform' && <Transform file={files[selectedFileName]} />}
</FlexBox>
)
}
export default Output

View File

@@ -3,7 +3,7 @@ import importMap from '@/components/Playground/template/import-map.json?raw'
import AppCss from '@/components/Playground/template/src/App.css?raw' import AppCss from '@/components/Playground/template/src/App.css?raw'
import App from '@/components/Playground/template/src/App.tsx?raw' import App from '@/components/Playground/template/src/App.tsx?raw'
import main from '@/components/Playground/template/src/main.tsx?raw' import main from '@/components/Playground/template/src/main.tsx?raw'
import { IFile, IFiles, ILanguage } from '@/components/Playground/shared' import { IFile, IFiles, IImportMap, ILanguage } from '@/components/Playground/shared'
export const MAIN_FILE_NAME = 'App.tsx' export const MAIN_FILE_NAME = 'App.tsx'
export const IMPORT_MAP_FILE_NAME = 'import-map.json' export const IMPORT_MAP_FILE_NAME = 'import-map.json'
@@ -106,11 +106,13 @@ export const initFiles: IFiles = getFilesFromUrl() || {
name: 'App.css', name: 'App.css',
language: 'css', language: 'css',
value: AppCss value: AppCss
}, }
[IMPORT_MAP_FILE_NAME]: { }
name: IMPORT_MAP_FILE_NAME,
language: fileNameToLanguage(IMPORT_MAP_FILE_NAME), export const initImportMap: IImportMap = {
value: importMap imports: {
react: 'https://esm.sh/react@18.2.0',
'react-dom/client': 'https://esm.sh/react-dom@18.2.0'
} }
} }

View File

@@ -1,7 +1,51 @@
import React from 'react' import React, { useState } from 'react'
import '@/components/Playground/playground.scss'
import FlexBox from '@/components/common/FlexBox'
import CodeEditor from '@/components/Playground/CodeEditor'
import Output from '@/components/Playground/Output'
import { IFiles, IImportMap } from '@/components/Playground/shared.ts'
import { IMPORT_MAP_FILE_NAME, MAIN_FILE_NAME } from '@/components/Playground/files.ts'
const ReactPlayground: React.FC = () => { interface PlaygroundProps {
return <></> initFiles: IFiles
initImportMap: IImportMap
} }
export default ReactPlayground const Playground: React.FC<PlaygroundProps> = ({ initFiles, initImportMap }) => {
const [files, setFiles] = useState(initFiles)
const [selectedFileName, setSelectedFileName] = useState(MAIN_FILE_NAME)
const [importMap, setImportMap] = useState<IImportMap>(initImportMap)
const handleOnChangeFileContent = (content: string, fileName: string, files: IFiles) => {
if (fileName === IMPORT_MAP_FILE_NAME) {
setImportMap(JSON.parse(content))
} else {
delete files[IMPORT_MAP_FILE_NAME]
setFiles(files)
}
}
return (
<FlexBox data-component={'playground'} direction={'horizontal'}>
<CodeEditor
files={{
...files,
'import-map.json': {
name: IMPORT_MAP_FILE_NAME,
language: 'json',
value: JSON.stringify(importMap, null, 2)
}
}}
selectedFileName={selectedFileName}
onAddFile={(_, files) => setFiles(files)}
onRemoveFile={(_, files) => setFiles(files)}
onRenameFile={(_, __, files) => setFiles(files)}
onChangeFileContent={handleOnChangeFileContent}
onSelectedFileChange={setSelectedFileName}
/>
<Output files={files} selectedFileName={selectedFileName} importMap={importMap} />
</FlexBox>
)
}
export default Playground

View File

@@ -0,0 +1,18 @@
[data-component=playground] {
width: 100%;
height: 100%;
> * {
width: 0 !important;
}
.playground-error-message {
position: absolute;
bottom: 0;
width: 100%;
color: white;
background-color: #FF4D4FAA;
padding: 5px 10px;
font-size: 1.2em;
}
}

View File

@@ -1,30 +1,13 @@
import React, { useState } from 'react' import React from 'react'
import CodeEditor from '@/components/Playground/CodeEditor'
import { IMPORT_MAP_FILE_NAME, initFiles, MAIN_FILE_NAME } from '@/components/Playground/files'
import { IFiles } from '@/components/Playground/shared'
import FitFullscreen from '@/components/common/FitFullscreen' import FitFullscreen from '@/components/common/FitFullscreen'
import FlexBox from '@/components/common/FlexBox' import Playground from '@/components/Playground'
import Preview from '@/components/Playground/Preview' import { initFiles, initImportMap } from '@/components/Playground/files.ts'
const OnlineEditor: React.FC = () => { const OnlineEditor: React.FC = () => {
const [files, setFiles] = useState<IFiles>(initFiles)
const [selectedFileName, setSelectedFileName] = useState(MAIN_FILE_NAME)
return ( return (
<> <>
<FitFullscreen> <FitFullscreen>
<FlexBox style={{ width: '100%', height: '100%' }} direction={'horizontal'}> <Playground initFiles={initFiles} initImportMap={initImportMap} />
<CodeEditor
files={files}
onSelectedFileChange={setSelectedFileName}
selectedFileName={selectedFileName}
onAddFile={(_, files) => setFiles(files)}
onRemoveFile={(_, files) => setFiles(files)}
onRenameFile={(_, __, files) => setFiles(files)}
onChangeFileContent={(_, __, files) => setFiles(files)}
/>
<Preview iframeKey={files[IMPORT_MAP_FILE_NAME].value} files={files} />
</FlexBox>
</FitFullscreen> </FitFullscreen>
</> </>
) )