([])
+ const [creating, setCreating] = useState(false)
+
+ const getMaxSequenceTabName = (filesName: string[]) => {
+ const result = filesName.reduce((max, filesName) => {
+ const match = filesName.match(/Component(\d+)\.tsx/)
+ if (match) {
+ const sequenceNumber = parseInt(match[1], 10)
+ return Math.max(Number(max), sequenceNumber)
+ }
+ return max
+ }, 0)
+
+ return `Component${result + 1}.tsx`
+ }
+
+ const addTab = () => {
+ setTabs([...tabs, getMaxSequenceTabName(tabs)])
+ setCreating(true)
+ }
+
+ const handleOnCancel = () => {
+ if (!creating) {
+ return
+ }
+
+ tabs.pop()
+ setTabs([...tabs])
+ setCreating(false)
+ }
+
+ const handleOnClickTab = (fileName: string) => {
+ if (creating) {
+ return
+ }
+
+ onChange?.(fileName)
+ }
+
+ const editImportMap = () => {
+ onChange?.(IMPORT_MAP_FILE_NAME)
+ }
+
+ const handleOnSaveTab = (value: string, item: string) => {
+ if (creating) {
+ onAddFile?.(value)
+ setCreating(false)
+ } else {
+ onUpdateFileName?.(item, value)
+ }
+
+ setTimeout(() => {
+ handleOnClickTab(value)
+ })
+ }
+
+ const handleOnValidateTab = (newFileName: string, oldFileName: string) => {
+ if (!/\.(jsx|tsx|js|ts|css|json|svg)$/.test(newFileName)) {
+ onError?.(
+ 'Playground only supports *.jsx, *.tsx, *.js, *.ts, *.css, *.json, *.svg files.'
+ )
+
+ return false
+ }
+
+ if (tabs.includes(newFileName) && newFileName !== oldFileName) {
+ onError?.(`File "${newFileName}" already exists.`)
+ return false
+ }
+
+ return true
+ }
+
+ const handleOnRemove = (fileName: string) => {
+ onRemoveFile?.(fileName)
+ handleOnClickTab(Object.keys(files)[Object.keys(files).length - 1])
+ }
+
+ useEffect(() => {
+ Object.keys(files).length &&
+ setTabs(
+ Object.keys(files).filter(
+ (item) =>
+ ![IMPORT_MAP_FILE_NAME, ENTRY_FILE_NAME].includes(item) &&
+ !files[item].hidden
+ )
+ )
+ }, [files])
+
+ return (
+ <>
+
+ {tabs.map((item, index) => (
+ - handleOnSaveTab(name, item)}
+ onCancel={handleOnCancel}
+ onRemove={handleOnRemove}
+ onClick={() => handleOnClickTab(item)}
+ />
+ ))}
+
+ >
+ )
+}
+
+export default FileSelector
diff --git a/src/components/ReactPlayground/CodeEditor/index.tsx b/src/components/ReactPlayground/CodeEditor/index.tsx
new file mode 100644
index 0000000..0952284
--- /dev/null
+++ b/src/components/ReactPlayground/CodeEditor/index.tsx
@@ -0,0 +1,72 @@
+import React, { useState } from 'react'
+import FileSelector from '@/components/ReactPlayground/CodeEditor/FileSelector'
+import Editor from '@/components/ReactPlayground/CodeEditor/Editor'
+import { IEditorOptions, IFiles, ITheme } from '@/components/ReactPlayground/shared.ts'
+import FitFullscreen from '@/components/common/FitFullscreen.tsx'
+import FlexBox from '@/components/common/FlexBox.tsx'
+import _ from 'lodash'
+
+interface CodeEditorProps {
+ theme: ITheme
+ files: IFiles
+ readonly?: boolean
+ readonlyFiles?: string[]
+ selectedFileName: string
+ options?: IEditorOptions
+ onSelectedFileChange?: (fileName: string) => void
+ onRemoveFile?: (fileName: string, files: IFiles) => void
+}
+
+const CodeEditor: React.FC = ({
+ theme,
+ files,
+ readonly,
+ readonlyFiles,
+ options,
+ onSelectedFileChange,
+ onRemoveFile,
+ ...props
+}) => {
+ const [selectedFileName, setSelectedFileName] = useState(props.selectedFileName)
+
+ const handleOnChangeSelectedFile = (fileName: string) => {
+ if (onSelectedFileChange) {
+ onSelectedFileChange(fileName)
+ } else {
+ setSelectedFileName(fileName)
+ }
+ }
+
+ const handleOnRemoveFile = (fileName: string) => {
+ const clone = _.cloneDeep(files)
+ delete clone[fileName]
+ onRemoveFile?.(fileName, clone)
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ )
+}
+
+export default CodeEditor
diff --git a/src/components/ReactPlayground/shared.ts b/src/components/ReactPlayground/shared.ts
index bbdf821..02fee27 100644
--- a/src/components/ReactPlayground/shared.ts
+++ b/src/components/ReactPlayground/shared.ts
@@ -1,10 +1,12 @@
import React from 'react'
import { editor } from 'monaco-editor'
+export type ILanguage = 'javascript' | 'typescript' | 'json' | 'css'
+
export interface IFile {
name: string
value: string
- language: 'javascript' | 'typescript' | 'json' | 'css'
+ language: ILanguage
active?: boolean
hidden?: boolean
}
diff --git a/src/components/ReactPlayground/utils.ts b/src/components/ReactPlayground/utils.ts
index 0569a37..95b5a90 100644
--- a/src/components/ReactPlayground/utils.ts
+++ b/src/components/ReactPlayground/utils.ts
@@ -1,5 +1,11 @@
import { strFromU8, strToU8, unzlibSync, zlibSync } from 'fflate'
-import { ICustomFiles, IFiles, IImportMap, ITheme } from '@/components/ReactPlayground/shared'
+import {
+ ICustomFiles,
+ IFiles,
+ IImportMap,
+ ILanguage,
+ ITheme
+} from '@/components/ReactPlayground/shared'
import { IMPORT_MAP_FILE_NAME, reactTemplateFiles } from '@/components/ReactPlayground/files'
export const strToBase64 = (str: string) => {
@@ -31,8 +37,8 @@ export const setPlaygroundTheme = (theme: ITheme) => {
?.forEach((item) => item.setAttribute('class', theme))
}
-export const getPlaygroundTheme = () => {
- const isDarkTheme = JSON.parse(localStorage.getItem(STORAGE_DARK_THEME) || 'false')
+export const getPlaygroundTheme = (): ITheme => {
+ const isDarkTheme = JSON.parse(localStorage.getItem(STORAGE_DARK_THEME) || 'false') as ITheme
return isDarkTheme ? 'vs-dark' : 'light'
}
@@ -96,7 +102,7 @@ export const getFilesFromUrl = () => {
try {
if (typeof window !== 'undefined') {
const hash = window.location.hash
- if (hash) files = JSON.parse(base64ToStr(hash?.split('#')[1]))
+ if (hash) files = JSON.parse(base64ToStr(hash?.split('#')[1])) as IFiles
}
} catch (error) {
console.error(error)
@@ -104,7 +110,7 @@ export const getFilesFromUrl = () => {
return files
}
-export const fileNameToLanguage = (name: string) => {
+export const fileNameToLanguage = (name: string): ILanguage => {
const suffix = name.split('.').pop() || ''
if (['js', 'jsx'].includes(suffix)) return 'javascript'
if (['ts', 'tsx'].includes(suffix)) return 'typescript'
diff --git a/src/pages/OnlineEditor.tsx b/src/pages/OnlineEditor.tsx
index 04293c8..974ce4c 100644
--- a/src/pages/OnlineEditor.tsx
+++ b/src/pages/OnlineEditor.tsx
@@ -1,16 +1,29 @@
-import React from 'react'
-import Editor from '@/components/ReactPlayground/CodeEditor/Editor'
+import React, { useState } from 'react'
+import CodeEditor from '@/components/ReactPlayground/CodeEditor'
+import { fileNameToLanguage } from '@/components/ReactPlayground/utils.ts'
+import { IFiles } from '@/components/ReactPlayground/shared.ts'
const OnlineEditor: React.FC = () => {
+ const [files, setFiles] = useState({
+ abc: {
+ name: 'App.tsx',
+ language: fileNameToLanguage('App.tsx'),
+ value: 'const a = () => {}'
+ },
+ cde: {
+ name: 'App.css',
+ language: fileNameToLanguage('App.css'),
+ value: '.title {}'
+ }
+ })
+
return (
<>
- {}'
- }}
+ files={files}
+ selectedFileName={'abc'}
+ onRemoveFile={(_, files) => setFiles(files)}
/>
>
)