Finish Playground
This commit is contained in:
@@ -64,10 +64,10 @@ const FileSelector: React.FC<FileSelectorProps> = ({
|
||||
}
|
||||
|
||||
const handleOnCancel = () => {
|
||||
onError?.('')
|
||||
if (!creating) {
|
||||
return
|
||||
}
|
||||
|
||||
tabs.pop()
|
||||
setTabs([...tabs])
|
||||
setCreating(false)
|
||||
@@ -111,7 +111,10 @@ const FileSelector: React.FC<FileSelectorProps> = ({
|
||||
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.`)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
[data-component=playground-code-editor] {
|
||||
position: relative;
|
||||
width: 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;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import _ from 'lodash'
|
||||
import '@/components/Playground/CodeEditor/code-editor.scss'
|
||||
import { IEditorOptions, IFiles, ITheme } from '@/components/Playground/shared'
|
||||
import {
|
||||
ENTRY_FILE_NAME,
|
||||
fileNameToLanguage,
|
||||
getFileNameList,
|
||||
IMPORT_MAP_FILE_NAME
|
||||
@@ -44,7 +43,7 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
|
||||
...props
|
||||
}) => {
|
||||
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 =
|
||||
props.selectedFileName || (filteredFilesName.length ? filteredFilesName[0] : '')
|
||||
@@ -148,7 +147,7 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
|
||||
onChange={handleOnChangeFileContent}
|
||||
onJumpFile={handleOnChangeSelectedFile}
|
||||
/>
|
||||
{errorMsg && <div className={'playground-code-editor-message'}>{errorMsg}</div>}
|
||||
{errorMsg && <div className={'playground-error-message'}>{errorMsg}</div>}
|
||||
</FlexBox>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
<body>
|
||||
<script>
|
||||
window.addEventListener("error", (e) => {
|
||||
window.parent.postMessage({ type: "ERROR", message: e.message });
|
||||
window.parent.postMessage({ type: "ERROR", msg: e.message });
|
||||
});
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
window.parent.postMessage({ type: "LOADED", message: "" });
|
||||
window.parent.postMessage({ type: "LOADED", msg: "" });
|
||||
});
|
||||
|
||||
window.addEventListener("message", ({ data }) => {
|
||||
@@ -43,7 +43,7 @@
|
||||
};
|
||||
document.body.appendChild(script);
|
||||
URL.revokeObjectURL(oldSrc);
|
||||
window.parent.postMessage({ type: "DONE", message: "" });
|
||||
window.parent.postMessage({ type: "DONE", msg: "" });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -1,14 +1,14 @@
|
||||
import React, { useRef, useState } from 'react'
|
||||
import { IFiles } from '@/components/Playground/shared'
|
||||
import iframeRaw from '@/components/Playground/Preview/iframe.html?raw'
|
||||
import { IFiles, IImportMap } from '@/components/Playground/shared'
|
||||
import iframeRaw from '@/components/Playground/Output/Preview/iframe.html?raw'
|
||||
import { useUpdatedEffect } from '@/util/hooks'
|
||||
import { IMPORT_MAP_FILE_NAME } from '@/components/Playground/files'
|
||||
import Compiler from '@/components/Playground/compiler'
|
||||
import '@/components/Playground/Preview/preview.scss'
|
||||
import '@/components/Playground/Output/Preview/preview.scss'
|
||||
|
||||
interface PreviewProps {
|
||||
iframeKey: string
|
||||
files: IFiles
|
||||
importMap: IImportMap
|
||||
}
|
||||
|
||||
interface IMessage {
|
||||
@@ -34,7 +34,7 @@ const getIframeUrl = (iframeRaw: string) => {
|
||||
|
||||
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 [errorMsg, setErrorMsg] = useState('')
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
@@ -48,15 +48,11 @@ const Preview: React.FC<PreviewProps> = ({ iframeKey, files }) => {
|
||||
case 'ERROR':
|
||||
setErrorMsg(msg)
|
||||
break
|
||||
default:
|
||||
case 'DONE':
|
||||
setErrorMsg('')
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.error(errorMsg)
|
||||
}, [errorMsg])
|
||||
|
||||
useUpdatedEffect(() => {
|
||||
window.addEventListener('message', handleMessage)
|
||||
|
||||
@@ -66,7 +62,7 @@ const Preview: React.FC<PreviewProps> = ({ iframeKey, files }) => {
|
||||
}, [])
|
||||
|
||||
useUpdatedEffect(() => {
|
||||
Compiler.compile(files, JSON.parse(files[IMPORT_MAP_FILE_NAME].value))
|
||||
Compiler.compile(files, importMap)
|
||||
.then((result) => {
|
||||
if (loaded) {
|
||||
iframeRef.current?.contentWindow?.postMessage({
|
||||
@@ -76,7 +72,7 @@ const Preview: React.FC<PreviewProps> = ({ iframeKey, files }) => {
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
setErrorMsg(e)
|
||||
setErrorMsg(`编译失败:${e.message}`)
|
||||
})
|
||||
}, [files, Compiler, loaded])
|
||||
|
||||
@@ -88,6 +84,7 @@ const Preview: React.FC<PreviewProps> = ({ iframeKey, files }) => {
|
||||
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"
|
||||
/>
|
||||
{errorMsg && <div className={'playground-error-message'}>{errorMsg}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
[data-component=playground-preview] {
|
||||
display: flex;
|
||||
position: relative;
|
||||
iframe {
|
||||
border: none;
|
||||
flex: 1;
|
||||
@@ -1,12 +1,13 @@
|
||||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import MonacoEditor from '@monaco-editor/react'
|
||||
import { Loader } from 'esbuild-wasm'
|
||||
import { useUpdatedEffect } from '@/util/hooks'
|
||||
import { IFile, ITheme } from '@/components/Playground/shared'
|
||||
import Compiler from '@/components/Playground/compiler'
|
||||
import { cssToJs, jsonToJs } from '@/components/Playground/files'
|
||||
import { MonacoEditorConfig } from '@/components/Playground/CodeEditor/Editor/monacoConfig'
|
||||
import { addReactImport } from '@/components/Playground/utils'
|
||||
import '@/components/Playground/Output/Transform/transform.scss'
|
||||
import { useUpdatedEffect } from '@/util/hooks.tsx'
|
||||
import { IFile, ITheme } from '@/components/Playground/shared.ts'
|
||||
import Compiler from '@/components/Playground/compiler.ts'
|
||||
import { cssToJs, jsonToJs } from '@/components/Playground/files.ts'
|
||||
import { MonacoEditorConfig } from '@/components/Playground/CodeEditor/Editor/monacoConfig.ts'
|
||||
import { addReactImport } from '@/components/Playground/utils.ts'
|
||||
|
||||
interface OutputProps {
|
||||
file: IFile
|
||||
@@ -15,6 +16,7 @@ interface OutputProps {
|
||||
|
||||
const Transform: React.FC<OutputProps> = ({ file, theme }) => {
|
||||
const [compiledCode, setCompiledCode] = useState('')
|
||||
const [errorMsg, setErrorMsg] = useState('')
|
||||
|
||||
const compile = (code: string, loader: Loader) => {
|
||||
let _code = code
|
||||
@@ -25,9 +27,10 @@ const Transform: React.FC<OutputProps> = ({ file, theme }) => {
|
||||
Compiler?.transform(_code, loader)
|
||||
.then((value) => {
|
||||
setCompiledCode(value.code)
|
||||
setErrorMsg('')
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('编译失败', e)
|
||||
setErrorMsg(`编译失败:${e.message}`)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -62,14 +65,15 @@ const Transform: React.FC<OutputProps> = ({ file, theme }) => {
|
||||
}, [file, Compiler])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-component={'playground-transform'}>
|
||||
<MonacoEditor
|
||||
theme={theme}
|
||||
language={'javascript'}
|
||||
value={compiledCode}
|
||||
options={{ ...MonacoEditorConfig, readOnly: true }}
|
||||
/>
|
||||
</>
|
||||
{errorMsg && <div className={'playground-error-message'}>{errorMsg}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[data-component=playground-transform] {
|
||||
position: relative;
|
||||
}
|
||||
40
src/components/Playground/Output/index.tsx
Normal file
40
src/components/Playground/Output/index.tsx
Normal 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
|
||||
@@ -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 App from '@/components/Playground/template/src/App.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 IMPORT_MAP_FILE_NAME = 'import-map.json'
|
||||
@@ -106,11 +106,13 @@ export const initFiles: IFiles = getFilesFromUrl() || {
|
||||
name: 'App.css',
|
||||
language: 'css',
|
||||
value: AppCss
|
||||
},
|
||||
[IMPORT_MAP_FILE_NAME]: {
|
||||
name: IMPORT_MAP_FILE_NAME,
|
||||
language: fileNameToLanguage(IMPORT_MAP_FILE_NAME),
|
||||
value: importMap
|
||||
}
|
||||
}
|
||||
|
||||
export const initImportMap: IImportMap = {
|
||||
imports: {
|
||||
react: 'https://esm.sh/react@18.2.0',
|
||||
'react-dom/client': 'https://esm.sh/react-dom@18.2.0'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = () => {
|
||||
return <></>
|
||||
interface PlaygroundProps {
|
||||
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
|
||||
|
||||
18
src/components/Playground/playground.scss
Normal file
18
src/components/Playground/playground.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,13 @@
|
||||
import React, { useState } 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 React from 'react'
|
||||
import FitFullscreen from '@/components/common/FitFullscreen'
|
||||
import FlexBox from '@/components/common/FlexBox'
|
||||
import Preview from '@/components/Playground/Preview'
|
||||
import Playground from '@/components/Playground'
|
||||
import { initFiles, initImportMap } from '@/components/Playground/files.ts'
|
||||
|
||||
const OnlineEditor: React.FC = () => {
|
||||
const [files, setFiles] = useState<IFiles>(initFiles)
|
||||
const [selectedFileName, setSelectedFileName] = useState(MAIN_FILE_NAME)
|
||||
|
||||
return (
|
||||
<>
|
||||
<FitFullscreen>
|
||||
<FlexBox style={{ width: '100%', height: '100%' }} direction={'horizontal'}>
|
||||
<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>
|
||||
<Playground initFiles={initFiles} initImportMap={initImportMap} />
|
||||
</FitFullscreen>
|
||||
</>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user