Finish editor in Playground Code Editor

This commit is contained in:
2024-01-08 00:16:08 +08:00
parent 82b5e75046
commit d526d913b9
14 changed files with 484 additions and 78 deletions

46
package-lock.json generated
View File

@@ -10,6 +10,8 @@
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.2.6", "@ant-design/icons": "^5.2.6",
"@marsidev/react-turnstile": "^0.4.0", "@marsidev/react-turnstile": "^0.4.0",
"@monaco-editor/react": "^4.6.0",
"@typescript/ata": "^0.9.4",
"antd": "^5.12.1", "antd": "^5.12.1",
"axios": "^1.6.2", "axios": "^1.6.2",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
@@ -22,6 +24,7 @@
"match-sorter": "^6.3.1", "match-sorter": "^6.3.1",
"moment": "^2.29.4", "moment": "^2.29.4",
"monaco-editor": "^0.45.0", "monaco-editor": "^0.45.0",
"monaco-jsx-syntax-highlight": "^1.2.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router": "^6.20.1", "react-router": "^6.20.1",
@@ -1243,6 +1246,30 @@
"react-dom": ">=16.8.0" "react-dom": ">=16.8.0"
} }
}, },
"node_modules/@monaco-editor/loader": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/@monaco-editor/loader/-/loader-1.4.0.tgz",
"integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==",
"dependencies": {
"state-local": "^1.0.6"
},
"peerDependencies": {
"monaco-editor": ">= 0.21.0 < 1"
}
},
"node_modules/@monaco-editor/react": {
"version": "4.6.0",
"resolved": "https://registry.npmmirror.com/@monaco-editor/react/-/react-4.6.0.tgz",
"integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==",
"dependencies": {
"@monaco-editor/loader": "^1.4.0"
},
"peerDependencies": {
"monaco-editor": ">= 0.25.0 < 1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -2196,6 +2223,14 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
} }
}, },
"node_modules/@typescript/ata": {
"version": "0.9.4",
"resolved": "https://registry.npmmirror.com/@typescript/ata/-/ata-0.9.4.tgz",
"integrity": "sha512-PaJ16WouPV/SaA+c0tnOKIqYq24+m93ipl/e0Dkxuianer+ibc5b0/6ZgfCFF8J7QEp57dySMSP9nWOFaCfJnw==",
"peerDependencies": {
"typescript": "^4.4.4"
}
},
"node_modules/@ungap/structured-clone": { "node_modules/@ungap/structured-clone": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
@@ -5275,6 +5310,11 @@
"resolved": "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.45.0.tgz", "resolved": "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.45.0.tgz",
"integrity": "sha512-mjv1G1ZzfEE3k9HZN0dQ2olMdwIfaeAAjFiwNprLfYNRSz7ctv9XuCT7gPtBGrMUeV1/iZzYKj17Khu1hxoHOA==" "integrity": "sha512-mjv1G1ZzfEE3k9HZN0dQ2olMdwIfaeAAjFiwNprLfYNRSz7ctv9XuCT7gPtBGrMUeV1/iZzYKj17Khu1hxoHOA=="
}, },
"node_modules/monaco-jsx-syntax-highlight": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/monaco-jsx-syntax-highlight/-/monaco-jsx-syntax-highlight-1.2.0.tgz",
"integrity": "sha512-sKzfmNMxLp3Dcwndz0F/EdP3rqe+Z6FbDPazj+/ByNbqoXWkzgb/ocat8h3vpiTwPPiDtSHidT0BFMvLkMtG8A=="
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz",
@@ -7127,6 +7167,11 @@
"dev": true, "dev": true,
"peer": true "peer": true
}, },
"node_modules/state-local": {
"version": "1.0.7",
"resolved": "https://registry.npmmirror.com/state-local/-/state-local-1.0.7.tgz",
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="
},
"node_modules/string-convert": { "node_modules/string-convert": {
"version": "0.2.1", "version": "0.2.1",
"resolved": "https://registry.npmmirror.com/string-convert/-/string-convert-0.2.1.tgz", "resolved": "https://registry.npmmirror.com/string-convert/-/string-convert-0.2.1.tgz",
@@ -7705,7 +7750,6 @@
"version": "5.3.3", "version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"dev": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"

View File

@@ -16,6 +16,8 @@
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.2.6", "@ant-design/icons": "^5.2.6",
"@marsidev/react-turnstile": "^0.4.0", "@marsidev/react-turnstile": "^0.4.0",
"@monaco-editor/react": "^4.6.0",
"@typescript/ata": "^0.9.4",
"antd": "^5.12.1", "antd": "^5.12.1",
"axios": "^1.6.2", "axios": "^1.6.2",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
@@ -28,6 +30,7 @@
"match-sorter": "^6.3.1", "match-sorter": "^6.3.1",
"moment": "^2.29.4", "moment": "^2.29.4",
"monaco-editor": "^0.45.0", "monaco-editor": "^0.45.0",
"monaco-jsx-syntax-highlight": "^1.2.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router": "^6.20.1", "react-router": "^6.20.1",

View File

@@ -0,0 +1,83 @@
import { ATABootstrapConfig, setupTypeAcquisition } from '@typescript/ata'
type DelegateListener = Required<{
[k in keyof ATABootstrapConfig['delegate']]: Set<NonNullable<ATABootstrapConfig['delegate'][k]>>
}>
const createDelegate = (): DelegateListener => {
return {
receivedFile: new Set(),
progress: new Set(),
errorMessage: new Set(),
finished: new Set(),
started: new Set()
}
}
const delegateListener = createDelegate()
type InferSet<T> = T extends Set<infer U> ? U : never
export const createATA = async () => {
// @ts-ignore
const ts = await import('https://esm.sh/typescript@5.3.3')
const ata = setupTypeAcquisition({
projectName: 'monaco-ts',
typescript: ts,
logger: console,
fetcher: (input, init) => {
let result: any
try {
result = fetch(input, init)
} catch (error) {
console.error('Error fetching data:', error)
}
return result
},
delegate: {
receivedFile: (code, path) => {
delegateListener.receivedFile.forEach((fn) => fn(code, path))
},
progress: (downloaded, estimatedTotal) => {
delegateListener.progress.forEach((fn) => fn(downloaded, estimatedTotal))
},
started: () => {
delegateListener.started.forEach((fn) => fn())
},
finished: (files) => {
delegateListener.finished.forEach((fn) => fn(files))
}
}
})
const acquireType = (code: string) => ata(code)
const addListener = <T extends keyof DelegateListener>(
event: T,
handler: InferSet<DelegateListener[T]>
) => {
// @ts-ignore
delegateListener[event].add(handler)
}
const removeListener = <T extends keyof DelegateListener>(
event: T,
handler: InferSet<DelegateListener[T]>
) => {
// @ts-ignore
delegateListener[event].delete(handler)
}
const dispose = () => {
for (const key in delegateListener) {
delegateListener[key as keyof DelegateListener].clear()
}
}
return {
acquireType,
addListener,
removeListener,
dispose
}
}

View File

@@ -0,0 +1,43 @@
.monaco-editor-light {
height: 100%;
overflow: hidden;
background-color: var(--border);
.jsx-tag-angle-bracket {
color: #800000;
}
.jsx-text {
color: #000;
}
.jsx-tag-name {
color: #800000;
}
.jsx-tag-attribute-key {
color: #f00;
}
}
.monaco-editor-vs-dark {
height: 100%;
overflow: hidden;
background-color: var(--border);
.jsx-tag-angle-bracket {
color: #808080;
}
.jsx-text {
color: #d4d4d4;
}
.jsx-tag-name {
color: #569cd6;
}
.jsx-tag-attribute-key {
color: #9cdcfe;
}
}

View File

@@ -0,0 +1,72 @@
import { editor, IPosition, Selection } from 'monaco-editor'
import ScrollType = editor.ScrollType
import { Monaco } from '@monaco-editor/react'
import { getWorker, MonacoJsxSyntaxHighlight } from 'monaco-jsx-syntax-highlight'
import { createATA } from '@/components/ReactPlayground/CodeEditor/Editor/ata.ts'
export const useEditor = () => {
const doOpenEditor = (
editor: editor.IStandaloneCodeEditor,
input: { options: { selection: Selection } }
) => {
const selection = input.options ? input.options.selection : null
if (selection) {
if (
typeof selection.endLineNumber === 'number' &&
typeof selection.endColumn === 'number'
) {
editor.setSelection(selection)
editor.revealRangeInCenter(selection, ScrollType.Immediate)
} else {
const position: IPosition = {
lineNumber: selection.startLineNumber,
column: selection.startColumn
}
editor.setPosition(position)
editor.revealPositionInCenter(position, ScrollType.Immediate)
}
}
}
const loadJsxSyntaxHighlight = (editor: editor.IStandaloneCodeEditor, monaco: Monaco) => {
const monacoJsxSyntaxHighlight = new MonacoJsxSyntaxHighlight(getWorker(), monaco)
const { highlighter, dispose } = monacoJsxSyntaxHighlight.highlighterBuilder({ editor })
editor.onDidChangeModelContent(() => {
highlighter()
})
highlighter()
return { highlighter, dispose }
}
const autoLoadExtraLib = async (
editor: editor.IStandaloneCodeEditor,
monaco: Monaco,
defaultValue: string,
onWatch: any
) => {
const typeHelper = await createATA()
onWatch(typeHelper)
editor.onDidChangeModelContent(() => {
typeHelper.acquireType(editor.getValue())
})
const addLibraryToRuntime = (code: string, path: string) => {
monaco.languages.typescript.typescriptDefaults.addExtraLib(code, `file://${path}`)
}
typeHelper.addListener('receivedFile', addLibraryToRuntime)
typeHelper.acquireType(defaultValue)
return typeHelper
}
return {
doOpenEditor,
loadJsxSyntaxHighlight,
autoLoadExtraLib
}
}

View File

@@ -0,0 +1,90 @@
import React from 'react'
import { editor, Selection } from 'monaco-editor'
import MonacoEditor, { Monaco } from '@monaco-editor/react'
import '@/components/ReactPlayground/CodeEditor/Editor/editor.scss'
import { IEditorOptions, IFile, IFiles, ITheme } from '@/components/ReactPlayground/shared'
import { MonacoEditorConfig } from '@/components/ReactPlayground/CodeEditor/Editor/monacoConfig'
import { fileNameToLanguage } from '@/components/ReactPlayground/utils'
import { useEditor } from '@/components/ReactPlayground/CodeEditor/Editor/hooks'
interface EditorProps {
file: IFile
onChange?: (code: string | undefined) => void
options?: IEditorOptions
theme?: ITheme
files?: IFiles
onJumpFile?: (fileName: string) => void
}
const Editor: React.FC<EditorProps> = ({ file, files, theme, onChange, options, onJumpFile }) => {
const editorRef = useRef<editor.IStandaloneCodeEditor>()
const { doOpenEditor, loadJsxSyntaxHighlight } = useEditor()
const jsxSyntaxHighlightRef = useRef<{
highlighter: (code?: string | undefined) => void
dispose: () => void
}>({
highlighter: () => undefined,
dispose: () => undefined
})
const handleOnEditorDidMount = (editor: editor.IStandaloneCodeEditor, monaco: Monaco) => {
editorRef.current = editor
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
void editor.getAction('editor.action.formatDocument')?.run()
})
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
jsx: monaco.languages.typescript.JsxEmit.Preserve,
esModuleInterop: true
})
files &&
Object.entries(files).forEach(([key]) => {
if (!monaco.editor.getModel(monaco.Uri.parse(`file:///${key}`))) {
monaco.editor.createModel(
files[key].value,
fileNameToLanguage(key),
monaco.Uri.parse(`file:///${key}`)
)
}
})
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
editor['_codeEditorService'].doOpenEditor = function (
editor: editor.IStandaloneCodeEditor,
input: { options: { selection: Selection }; resource: { path: string } }
) {
const path = input.resource.path
if (!path.startsWith('/node_modules/')) {
onJumpFile?.(path.replace('/', ''))
doOpenEditor(editor, input)
}
}
jsxSyntaxHighlightRef.current = loadJsxSyntaxHighlight(editor, monaco)
}
useEffect(() => {
editorRef.current?.focus()
jsxSyntaxHighlightRef?.current?.highlighter?.()
}, [file.name])
return (
<>
<MonacoEditor
theme={theme}
path={file.name}
className={`monaco-editor-${theme ?? 'light'}`}
language={file.language}
value={file.value}
onChange={onChange}
onMount={handleOnEditorDidMount}
options={{ ...MonacoEditorConfig, ...options, theme: undefined }}
/>
</>
)
}
export default Editor

View File

@@ -0,0 +1,34 @@
import { editor } from 'monaco-editor'
export const MonacoEditorConfig: editor.IStandaloneEditorConstructionOptions = {
automaticLayout: true,
cursorBlinking: 'smooth',
fontLigatures: true,
formatOnPaste: true,
formatOnType: true,
fontSize: 14,
showDeprecated: true,
showUnused: true,
showFoldingControls: 'mouseover',
scrollBeyondLastLine: false,
minimap: {
enabled: false,
},
inlineSuggest: {
enabled: false,
},
fixedOverflowWidgets: true,
smoothScrolling: true,
smartSelect: {
selectSubwords: true,
selectLeadingAndTrailingWhitespace: true,
},
tabSize: 2,
overviewRulerBorder: false, // 不要滚动条的边框
// 滚动条设置
scrollbar: {
verticalScrollbarSize: 6, // 竖滚动条
horizontalScrollbarSize: 6, // 横滚动条
},
// lineNumbers: 'off', // 隐藏控制行号
}

View File

@@ -1,3 +1,4 @@
/*
import React from 'react' import React from 'react'
import { IPlayground } from '@/components/ReactPlayground/shared.ts' import { IPlayground } from '@/components/ReactPlayground/shared.ts'
import { PlaygroundContext } from '@/components/ReactPlayground/Provider.tsx' import { PlaygroundContext } from '@/components/ReactPlayground/Provider.tsx'
@@ -6,7 +7,7 @@ import {
getCustomActiveFile, getCustomActiveFile,
getMergedCustomFiles, getMergedCustomFiles,
getPlaygroundTheme getPlaygroundTheme
} from '@/components/ReactPlayground/Utils.ts' } from '@/components/ReactPlayground/utils.ts'
const defaultCodeSandboxOptions = { const defaultCodeSandboxOptions = {
theme: 'dark', theme: 'dark',
@@ -67,7 +68,8 @@ const Playground: React.FC<IPlayground> = (props) => {
} }
}, []) }, [])
return files[ENTRY_FILE_NAME] ? <></> : undefined return files[ENTRY_FILE_NAME] ? <div></div> : undefined
} }
export default Playground export default Playground
*/

View File

@@ -1,11 +1,11 @@
import React from 'react' import React from 'react'
import { IFiles, IPlaygroundContext, ITheme } from '@/components/ReactPlayground/shared.ts' import { IFiles, IPlaygroundContext, ITheme } from '@/components/ReactPlayground/shared.ts'
import { MAIN_FILE_NAME } from '@/components/files.ts' import { MAIN_FILE_NAME } from '@/components/ReactPlayground/files.ts'
import { import {
fileNameToLanguage, fileNameToLanguage,
setPlaygroundTheme, setPlaygroundTheme,
strToBase64 strToBase64
} from '@/components/ReactPlayground/Utils.ts' } from '@/components/ReactPlayground/utils.ts'
const initialContext: Partial<IPlaygroundContext> = { const initialContext: Partial<IPlaygroundContext> = {
selectedFileName: MAIN_FILE_NAME selectedFileName: MAIN_FILE_NAME

View File

@@ -0,0 +1,68 @@
import importMap from '@/components/ReactPlayground/template/import-map.json?raw'
import AppCss from '@/components/ReactPlayground/template/src/App.css?raw'
import App from '@/components/ReactPlayground/template/src/App.tsx?raw'
import main from '@/components/ReactPlayground/template/src/main.tsx?raw'
import { IFiles } from '@/components/ReactPlayground/shared'
import { base64ToStr } from '@/components/ReactPlayground/utils.ts'
export const MAIN_FILE_NAME = 'App.tsx'
export const IMPORT_MAP_FILE_NAME = 'import-map.json'
export const ENTRY_FILE_NAME = 'main.tsx'
const fileNameToLanguage = (name: string) => {
const suffix = name.split('.').pop() || ''
if (['js', 'jsx'].includes(suffix)) return 'javascript'
if (['ts', 'tsx'].includes(suffix)) return 'typescript'
if (['json'].includes(suffix)) return 'json'
if (['css'].includes(suffix)) return 'css'
return 'javascript'
}
const getFilesFromUrl = () => {
let files: IFiles | undefined
try {
if (typeof window !== 'undefined') {
const hash = window.location.hash
if (hash) files = JSON.parse(base64ToStr(hash?.split('#')[1]))
}
} catch (error) {
console.error(error)
}
return files
}
export const initFiles: IFiles = getFilesFromUrl() || {
[ENTRY_FILE_NAME]: {
name: ENTRY_FILE_NAME,
language: fileNameToLanguage(ENTRY_FILE_NAME),
value: main
},
[MAIN_FILE_NAME]: {
name: MAIN_FILE_NAME,
language: fileNameToLanguage(MAIN_FILE_NAME),
value: App
},
'App.css': {
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 reactTemplateFiles = {
[ENTRY_FILE_NAME]: {
name: ENTRY_FILE_NAME,
language: fileNameToLanguage(ENTRY_FILE_NAME),
value: main
},
[IMPORT_MAP_FILE_NAME]: {
name: IMPORT_MAP_FILE_NAME,
language: fileNameToLanguage(IMPORT_MAP_FILE_NAME),
value: importMap
}
}

View File

@@ -4,7 +4,7 @@ import { editor } from 'monaco-editor'
export interface IFile { export interface IFile {
name: string name: string
value: string value: string
language: string language: 'javascript' | 'typescript' | 'json' | 'css'
active?: boolean active?: boolean
hidden?: boolean hidden?: boolean
} }
@@ -13,7 +13,7 @@ export interface IFiles {
[key: string]: IFile [key: string]: IFile
} }
export type ITheme = 'light' | 'dark' export type ITheme = 'light' | 'vs-dark'
export type IImportMap = { imports: Record<string, string> } export type IImportMap = { imports: Record<string, string> }
@@ -26,7 +26,7 @@ export interface ICustomFiles {
hidden?: boolean hidden?: boolean
} }
} }
export type IEditorOptions = editor.IStandaloneEditorConstructionOptions & any export type IEditorOptions = editor.IStandaloneEditorConstructionOptions
export interface IEditorContainer { export interface IEditorContainer {
showFileSelector?: boolean showFileSelector?: boolean
fileSelectorReadOnly?: boolean fileSelectorReadOnly?: boolean

View File

@@ -1,6 +1,6 @@
import { ICustomFiles, IFiles, IImportMap, ITheme } from '@/components/ReactPlayground/shared.ts'
import { strFromU8, strToU8, unzlibSync, zlibSync } from 'fflate' import { strFromU8, strToU8, unzlibSync, zlibSync } from 'fflate'
import { IMPORT_MAP_FILE_NAME, reactTemplateFiles } from '@/components/files.ts' import { ICustomFiles, IFiles, IImportMap, ITheme } from '@/components/ReactPlayground/shared'
import { IMPORT_MAP_FILE_NAME, reactTemplateFiles } from '@/components/ReactPlayground/files'
export const strToBase64 = (str: string) => { export const strToBase64 = (str: string) => {
const buffer = strToU8(str) const buffer = strToU8(str)
@@ -22,19 +22,10 @@ export const base64ToStr = (base64: string) => {
return '' return ''
} }
export const fileNameToLanguage = (name: string) => {
const suffix = name.split('.').pop() || ''
if (['js', 'jsx'].includes(suffix)) return 'javascript'
if (['ts', 'tsx'].includes(suffix)) return 'typescript'
if (['json'].includes(suffix)) return 'json'
if (['css'].includes(suffix)) return 'css'
return 'javascript'
}
const STORAGE_DARK_THEME = 'react-playground-prefer-dark' const STORAGE_DARK_THEME = 'react-playground-prefer-dark'
export const setPlaygroundTheme = (theme: ITheme) => { export const setPlaygroundTheme = (theme: ITheme) => {
localStorage.setItem(STORAGE_DARK_THEME, String(theme === 'dark')) localStorage.setItem(STORAGE_DARK_THEME, String(theme === 'vs-dark'))
document document
.querySelectorAll('div[data-id="react-playground"]') .querySelectorAll('div[data-id="react-playground"]')
?.forEach((item) => item.setAttribute('class', theme)) ?.forEach((item) => item.setAttribute('class', theme))
@@ -42,7 +33,7 @@ export const setPlaygroundTheme = (theme: ITheme) => {
export const getPlaygroundTheme = () => { export const getPlaygroundTheme = () => {
const isDarkTheme = JSON.parse(localStorage.getItem(STORAGE_DARK_THEME) || 'false') const isDarkTheme = JSON.parse(localStorage.getItem(STORAGE_DARK_THEME) || 'false')
return isDarkTheme ? 'dark' : 'light' return isDarkTheme ? 'vs-dark' : 'light'
} }
const transformCustomFiles = (files: ICustomFiles) => { const transformCustomFiles = (files: ICustomFiles) => {
@@ -69,6 +60,17 @@ const transformCustomFiles = (files: ICustomFiles) => {
return newFiles return newFiles
} }
export const getCustomActiveFile = (files?: ICustomFiles) => {
if (!files) return null
return Object.keys(files).find((key) => {
const tempFile = files[key]
if (typeof tempFile !== 'string' && tempFile.active) {
return key
}
return null
})
}
export const getMergedCustomFiles = (files?: ICustomFiles, importMap?: IImportMap) => { export const getMergedCustomFiles = (files?: ICustomFiles, importMap?: IImportMap) => {
if (!files) return null if (!files) return null
if (importMap) { if (importMap) {
@@ -89,16 +91,6 @@ export const getMergedCustomFiles = (files?: ICustomFiles, importMap?: IImportMa
} }
} }
export const getCustomActiveFile = (files?: ICustomFiles) => {
if (!files) return null
return Object.keys(files).find((key) => {
const tempFile = files[key]
if (typeof tempFile !== 'string' && tempFile.active) {
return key
}
return null
})
}
export const getFilesFromUrl = () => { export const getFilesFromUrl = () => {
let files: IFiles | undefined let files: IFiles | undefined
try { try {
@@ -111,3 +103,12 @@ export const getFilesFromUrl = () => {
} }
return files return files
} }
export const fileNameToLanguage = (name: string) => {
const suffix = name.split('.').pop() || ''
if (['js', 'jsx'].includes(suffix)) return 'javascript'
if (['ts', 'tsx'].includes(suffix)) return 'typescript'
if (['json'].includes(suffix)) return 'json'
if (['css'].includes(suffix)) return 'css'
return 'javascript'
}

View File

@@ -1,46 +0,0 @@
import importMap from './template/import-map.json?raw'
import AppCss from './template/src/App.css?raw'
import App from './template/src/App.tsx?raw'
import main from './template/src/main.tsx?raw'
import { IFiles } from '@/components/ReactPlayground/shared.ts'
import { fileNameToLanguage } from '@/components/ReactPlayground/Utils.ts'
export const MAIN_FILE_NAME = 'App.tsx'
export const IMPORT_MAP_FILE_NAME = 'import-map.json'
export const ENTRY_FILE_NAME = 'main.tsx'
export const initFiles: IFiles = getFilesFromUrl() || {
[ENTRY_FILE_NAME]: {
name: ENTRY_FILE_NAME,
language: fileNameToLanguage(ENTRY_FILE_NAME),
value: main
},
[MAIN_FILE_NAME]: {
name: MAIN_FILE_NAME,
language: fileNameToLanguage(MAIN_FILE_NAME),
value: App
},
'App.css': {
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 reactTemplateFiles = {
[ENTRY_FILE_NAME]: {
name: ENTRY_FILE_NAME,
language: fileNameToLanguage(ENTRY_FILE_NAME),
value: main
},
[IMPORT_MAP_FILE_NAME]: {
name: IMPORT_MAP_FILE_NAME,
language: fileNameToLanguage(IMPORT_MAP_FILE_NAME),
value: importMap
}
}

View File

@@ -1,7 +1,19 @@
import React from 'react' import React from 'react'
import Editor from '@/components/ReactPlayground/CodeEditor/Editor'
const OnlineEditor: React.FC = () => { const OnlineEditor: React.FC = () => {
return <></> return (
<>
<Editor
theme={'light'}
file={{
name: 'App.tsx',
language: 'typescript',
value: 'const a = () => {}'
}}
/>
</>
)
} }
export default OnlineEditor export default OnlineEditor