Complete main UI #37
@@ -3,7 +3,7 @@ import { editor, Selection } from 'monaco-editor'
|
|||||||
import MonacoEditor, { Monaco } from '@monaco-editor/react'
|
import MonacoEditor, { Monaco } from '@monaco-editor/react'
|
||||||
import '@/components/ReactPlayground/CodeEditor/Editor/editor.scss'
|
import '@/components/ReactPlayground/CodeEditor/Editor/editor.scss'
|
||||||
import { IEditorOptions, IFiles, ITheme } from '@/components/ReactPlayground/shared'
|
import { IEditorOptions, IFiles, ITheme } from '@/components/ReactPlayground/shared'
|
||||||
import { fileNameToLanguage } from '@/components/ReactPlayground/utils'
|
import { fileNameToLanguage } from '@/components/ReactPlayground/files'
|
||||||
import { useEditor, useTypesProgress } from '@/components/ReactPlayground/CodeEditor/Editor/hooks'
|
import { useEditor, useTypesProgress } from '@/components/ReactPlayground/CodeEditor/Editor/hooks'
|
||||||
import { MonacoEditorConfig } from '@/components/ReactPlayground/CodeEditor/Editor/monacoConfig'
|
import { MonacoEditorConfig } from '@/components/ReactPlayground/CodeEditor/Editor/monacoConfig'
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
interface ItemProps {
|
interface ItemProps {
|
||||||
|
className?: string
|
||||||
readonly?: boolean
|
readonly?: boolean
|
||||||
creating?: boolean
|
creating?: boolean
|
||||||
value: string
|
value: string
|
||||||
@@ -15,6 +16,7 @@ interface ItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Item: React.FC<ItemProps> = ({
|
const Item: React.FC<ItemProps> = ({
|
||||||
|
className,
|
||||||
readonly = false,
|
readonly = false,
|
||||||
value,
|
value,
|
||||||
active = false,
|
active = false,
|
||||||
@@ -105,7 +107,10 @@ const Item: React.FC<ItemProps> = ({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`tab-item${active ? ' active' : ''}`} onClick={handleOnClick}>
|
<div
|
||||||
|
className={`tab-item${active ? ' active' : ''}${className ? ` ${className}` : ''}`}
|
||||||
|
onClick={handleOnClick}
|
||||||
|
>
|
||||||
{creating ? (
|
{creating ? (
|
||||||
<div className={'tab-item-input'}>
|
<div className={'tab-item-input'}>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -1,13 +1,45 @@
|
|||||||
[data-component=playground-file-selector].tab{
|
[data-component=playground-file-selector].tab{
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex: 0 0 auto;
|
||||||
gap: 1px;
|
height: 40px;
|
||||||
padding: 4px 20px 0 20px;
|
|
||||||
|
.multiple {
|
||||||
|
flex: 1;
|
||||||
|
width: 0;
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
height: 40px;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 2px;
|
||||||
|
margin-left: 10px;
|
||||||
|
|
||||||
|
.tab-item-add {
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-margin-right {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
height: 100%;
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticky {
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: flex-end;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.tab-item {
|
.tab-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
border: 1px solid #f0f0f0;
|
border: 1px solid #f0f0f0;
|
||||||
@@ -50,4 +82,5 @@
|
|||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,8 @@ import '@/components/ReactPlayground/CodeEditor/FileSelector/file-selector.scss'
|
|||||||
import { IFiles } from '@/components/ReactPlayground/shared'
|
import { IFiles } from '@/components/ReactPlayground/shared'
|
||||||
import { ENTRY_FILE_NAME, IMPORT_MAP_FILE_NAME } from '@/components/ReactPlayground/files'
|
import { ENTRY_FILE_NAME, IMPORT_MAP_FILE_NAME } from '@/components/ReactPlayground/files'
|
||||||
import Item from '@/components/ReactPlayground/CodeEditor/FileSelector/Item'
|
import Item from '@/components/ReactPlayground/CodeEditor/FileSelector/Item'
|
||||||
|
import HideScrollbar, { HideScrollbarElement } from '@/components/common/HideScrollbar'
|
||||||
|
import FlexBox from '@/components/common/FlexBox'
|
||||||
|
|
||||||
interface FileSelectorProps {
|
interface FileSelectorProps {
|
||||||
files?: IFiles
|
files?: IFiles
|
||||||
@@ -27,6 +29,7 @@ const FileSelector: React.FC<FileSelectorProps> = ({
|
|||||||
onUpdateFileName,
|
onUpdateFileName,
|
||||||
selectedFileName = ''
|
selectedFileName = ''
|
||||||
}) => {
|
}) => {
|
||||||
|
const hideScrollbarRef = useRef<HideScrollbarElement>(null)
|
||||||
const [tabs, setTabs] = useState<string[]>([])
|
const [tabs, setTabs] = useState<string[]>([])
|
||||||
const [creating, setCreating] = useState(false)
|
const [creating, setCreating] = useState(false)
|
||||||
const [hasEditing, setHasEditing] = useState(false)
|
const [hasEditing, setHasEditing] = useState(false)
|
||||||
@@ -47,6 +50,9 @@ const FileSelector: React.FC<FileSelectorProps> = ({
|
|||||||
const addTab = () => {
|
const addTab = () => {
|
||||||
setTabs([...tabs, getMaxSequenceTabName(tabs)])
|
setTabs([...tabs, getMaxSequenceTabName(tabs)])
|
||||||
setCreating(true)
|
setCreating(true)
|
||||||
|
setTimeout(() => {
|
||||||
|
hideScrollbarRef.current?.scrollRight(1000)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOnCancel = () => {
|
const handleOnCancel = () => {
|
||||||
@@ -126,27 +132,48 @@ const FileSelector: React.FC<FileSelectorProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div data-component={'playground-file-selector'} className={'tab'}>
|
||||||
data-component={'playground-file-selector'}
|
<div className={'multiple'}>
|
||||||
className={'tab'}
|
<HideScrollbar ref={hideScrollbarRef}>
|
||||||
style={{ flex: '0 0 auto' }}
|
<FlexBox direction={'horizontal'} className={'tab-content'}>
|
||||||
>
|
{tabs.map((item, index) => (
|
||||||
{tabs.map((item, index) => (
|
<Item
|
||||||
|
key={index + item}
|
||||||
|
value={item}
|
||||||
|
active={selectedFileName === item}
|
||||||
|
creating={creating}
|
||||||
|
readonly={readonly || notRemovableFiles.includes(item)}
|
||||||
|
hasEditing={hasEditing}
|
||||||
|
setHasEditing={setHasEditing}
|
||||||
|
onValidate={handleOnValidateTab}
|
||||||
|
onOk={(name) => handleOnSaveTab(name, item)}
|
||||||
|
onCancel={handleOnCancel}
|
||||||
|
onRemove={handleOnRemove}
|
||||||
|
onClick={() => handleOnClickTab(item)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{!readonly && (
|
||||||
|
<Item
|
||||||
|
className={'tab-item-add'}
|
||||||
|
value={'+'}
|
||||||
|
onClick={addTab}
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className={'tabs-margin-right'}>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
</FlexBox>
|
||||||
|
</HideScrollbar>
|
||||||
|
</div>
|
||||||
|
<div className={'sticky'}>
|
||||||
<Item
|
<Item
|
||||||
key={index + item}
|
value={'Import Map'}
|
||||||
value={item}
|
active={selectedFileName === IMPORT_MAP_FILE_NAME}
|
||||||
active={selectedFileName == item}
|
onClick={editImportMap}
|
||||||
creating={creating}
|
readonly
|
||||||
readonly={readonly || notRemovableFiles.includes(item)}
|
|
||||||
hasEditing={hasEditing}
|
|
||||||
setHasEditing={setHasEditing}
|
|
||||||
onValidate={handleOnValidateTab}
|
|
||||||
onOk={(name) => handleOnSaveTab(name, item)}
|
|
||||||
onCancel={handleOnCancel}
|
|
||||||
onRemove={handleOnRemove}
|
|
||||||
onClick={() => handleOnClickTab(item)}
|
|
||||||
/>
|
/>
|
||||||
))}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[data-component=playground-code-editor] {
|
||||||
|
section {
|
||||||
|
height: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
import '@/components/ReactPlayground/CodeEditor/code-editor.scss'
|
||||||
import { IEditorOptions, IFiles, ITheme } from '@/components/ReactPlayground/shared'
|
import { IEditorOptions, IFiles, ITheme } from '@/components/ReactPlayground/shared'
|
||||||
import { fileNameToLanguage } from '@/components/ReactPlayground/utils'
|
import { fileNameToLanguage } from '@/components/ReactPlayground/files'
|
||||||
import FileSelector from '@/components/ReactPlayground/CodeEditor/FileSelector'
|
import FileSelector from '@/components/ReactPlayground/CodeEditor/FileSelector'
|
||||||
import Editor from '@/components/ReactPlayground/CodeEditor/Editor'
|
import Editor from '@/components/ReactPlayground/CodeEditor/Editor'
|
||||||
import FitFullscreen from '@/components/common/FitFullscreen'
|
import FitFullscreen from '@/components/common/FitFullscreen'
|
||||||
import FlexBox from '@/components/common/FlexBox'
|
import FlexBox from '@/components/common/FlexBox'
|
||||||
|
|
||||||
interface CodeEditorProps {
|
interface CodeEditorProps {
|
||||||
theme: ITheme
|
theme?: ITheme
|
||||||
files: IFiles
|
files: IFiles
|
||||||
readonly?: boolean
|
readonly?: boolean
|
||||||
readonlyFiles?: string[]
|
readonlyFiles?: string[]
|
||||||
@@ -16,8 +17,10 @@ interface CodeEditorProps {
|
|||||||
selectedFileName: string
|
selectedFileName: string
|
||||||
options?: IEditorOptions
|
options?: IEditorOptions
|
||||||
onSelectedFileChange?: (fileName: string) => void
|
onSelectedFileChange?: (fileName: string) => void
|
||||||
|
onAddFile?: (fileName: string, files: IFiles) => void
|
||||||
onRemoveFile?: (fileName: string, files: IFiles) => void
|
onRemoveFile?: (fileName: string, files: IFiles) => void
|
||||||
onRenameFile?: (newFileName: string, oldFileName: string, files: IFiles) => void
|
onRenameFile?: (newFileName: string, oldFileName: string, files: IFiles) => void
|
||||||
|
onChangeFileContent?: (content: string, fileName: string, files: IFiles) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const CodeEditor: React.FC<CodeEditorProps> = ({
|
const CodeEditor: React.FC<CodeEditorProps> = ({
|
||||||
@@ -28,8 +31,10 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
|
|||||||
notRemovable,
|
notRemovable,
|
||||||
options,
|
options,
|
||||||
onSelectedFileChange,
|
onSelectedFileChange,
|
||||||
|
onAddFile,
|
||||||
onRemoveFile,
|
onRemoveFile,
|
||||||
onRenameFile,
|
onRenameFile,
|
||||||
|
onChangeFileContent,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedFileName, setSelectedFileName] = useState(props.selectedFileName)
|
const [selectedFileName, setSelectedFileName] = useState(props.selectedFileName)
|
||||||
@@ -48,6 +53,17 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
|
|||||||
onRemoveFile?.(fileName, clone)
|
onRemoveFile?.(fileName, clone)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleOnAddFile = (fileName: string) => {
|
||||||
|
const clone = _.cloneDeep(files)
|
||||||
|
clone[fileName] = {
|
||||||
|
name: fileName,
|
||||||
|
language: fileNameToLanguage(fileName),
|
||||||
|
value: ''
|
||||||
|
}
|
||||||
|
onAddFile?.(fileName, clone)
|
||||||
|
handleOnChangeSelectedFile(fileName)
|
||||||
|
}
|
||||||
|
|
||||||
const handleOnUpdateFileName = (newFileName: string, oldFileName: string) => {
|
const handleOnUpdateFileName = (newFileName: string, oldFileName: string) => {
|
||||||
if (!files[oldFileName] || !newFileName) {
|
if (!files[oldFileName] || !newFileName) {
|
||||||
return
|
return
|
||||||
@@ -66,9 +82,19 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
|
|||||||
onRenameFile?.(newFileName, oldFileName, newFiles)
|
onRenameFile?.(newFileName, oldFileName, newFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleOnChangeFileContent = (code = '') => {
|
||||||
|
const clone = _.cloneDeep(files)
|
||||||
|
clone[onSelectedFileChange ? props.selectedFileName : selectedFileName].value = code ?? ''
|
||||||
|
onChangeFileContent?.(
|
||||||
|
code,
|
||||||
|
onSelectedFileChange ? props.selectedFileName : selectedFileName,
|
||||||
|
clone
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FitFullscreen>
|
<FitFullscreen data-component={'playground-code-editor'}>
|
||||||
<FlexBox style={{ height: '100%' }}>
|
<FlexBox style={{ height: '100%' }}>
|
||||||
<FileSelector
|
<FileSelector
|
||||||
files={files}
|
files={files}
|
||||||
@@ -80,13 +106,22 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
|
|||||||
onChange={handleOnChangeSelectedFile}
|
onChange={handleOnChangeSelectedFile}
|
||||||
onRemoveFile={handleOnRemoveFile}
|
onRemoveFile={handleOnRemoveFile}
|
||||||
onUpdateFileName={handleOnUpdateFileName}
|
onUpdateFileName={handleOnUpdateFileName}
|
||||||
|
onAddFile={handleOnAddFile}
|
||||||
/>
|
/>
|
||||||
<Editor
|
<Editor
|
||||||
theme={theme}
|
theme={theme}
|
||||||
selectedFileName={selectedFileName}
|
selectedFileName={
|
||||||
|
onSelectedFileChange ? props.selectedFileName : selectedFileName
|
||||||
|
}
|
||||||
files={files}
|
files={files}
|
||||||
options={options}
|
options={options}
|
||||||
readonly={readonly || readonlyFiles?.includes(selectedFileName)}
|
readonly={
|
||||||
|
readonly ||
|
||||||
|
readonlyFiles?.includes(
|
||||||
|
onSelectedFileChange ? props.selectedFileName : selectedFileName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onChange={handleOnChangeFileContent}
|
||||||
/>
|
/>
|
||||||
</FlexBox>
|
</FlexBox>
|
||||||
</FitFullscreen>
|
</FitFullscreen>
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
/*
|
|
||||||
import React from 'react'
|
|
||||||
import { IPlayground } from '@/components/ReactPlayground/shared.ts'
|
|
||||||
import { PlaygroundContext } from '@/components/ReactPlayground/Provider.tsx'
|
|
||||||
import { ENTRY_FILE_NAME, initFiles, MAIN_FILE_NAME } from '@/components/files.ts'
|
|
||||||
import {
|
|
||||||
getCustomActiveFile,
|
|
||||||
getMergedCustomFiles,
|
|
||||||
getPlaygroundTheme
|
|
||||||
} from '@/components/ReactPlayground/utils.ts'
|
|
||||||
|
|
||||||
const defaultCodeSandboxOptions = {
|
|
||||||
theme: 'dark',
|
|
||||||
editorHeight: '100vh',
|
|
||||||
showUrlHash: true
|
|
||||||
}
|
|
||||||
|
|
||||||
const Playground: React.FC<IPlayground> = (props) => {
|
|
||||||
const {
|
|
||||||
width = '100vw',
|
|
||||||
height = '100vh',
|
|
||||||
theme,
|
|
||||||
files: propsFiles,
|
|
||||||
importMap,
|
|
||||||
showCompileOutput = true,
|
|
||||||
showHeader = true,
|
|
||||||
showFileSelector = true,
|
|
||||||
fileSelectorReadOnly = false,
|
|
||||||
border = false,
|
|
||||||
defaultSizes,
|
|
||||||
onFilesChange,
|
|
||||||
autorun = true
|
|
||||||
} = props
|
|
||||||
const { filesHash, changeTheme, files, setFiles, setSelectedFileName } =
|
|
||||||
useContext(PlaygroundContext)
|
|
||||||
const options = Object.assign(defaultCodeSandboxOptions, props.options || {})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (propsFiles && !propsFiles?.[MAIN_FILE_NAME]) {
|
|
||||||
throw new Error(
|
|
||||||
`Missing required property : '${MAIN_FILE_NAME}' is a mandatory property for 'files'`
|
|
||||||
)
|
|
||||||
} else if (propsFiles) {
|
|
||||||
const newFiles = getMergedCustomFiles(propsFiles, importMap)
|
|
||||||
if (newFiles) {
|
|
||||||
setFiles(newFiles)
|
|
||||||
}
|
|
||||||
const selectedFileName = getCustomActiveFile(propsFiles)
|
|
||||||
if (selectedFileName) {
|
|
||||||
setSelectedFileName(selectedFileName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [propsFiles])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!theme) {
|
|
||||||
changeTheme(getPlaygroundTheme())
|
|
||||||
} else {
|
|
||||||
changeTheme(theme)
|
|
||||||
}
|
|
||||||
}, 15)
|
|
||||||
}, [theme])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!propsFiles) {
|
|
||||||
setFiles(initFiles)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return files[ENTRY_FILE_NAME] ? <div></div> : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Playground
|
|
||||||
*/
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { IFiles, IPlaygroundContext, ITheme } from '@/components/ReactPlayground/shared.ts'
|
|
||||||
import { MAIN_FILE_NAME } from '@/components/ReactPlayground/files.ts'
|
|
||||||
import {
|
|
||||||
fileNameToLanguage,
|
|
||||||
setPlaygroundTheme,
|
|
||||||
strToBase64
|
|
||||||
} from '@/components/ReactPlayground/utils.ts'
|
|
||||||
|
|
||||||
const initialContext: Partial<IPlaygroundContext> = {
|
|
||||||
selectedFileName: MAIN_FILE_NAME
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PlaygroundContext = createContext<IPlaygroundContext>(
|
|
||||||
initialContext as IPlaygroundContext
|
|
||||||
)
|
|
||||||
|
|
||||||
interface ProviderProps extends React.PropsWithChildren {
|
|
||||||
saveOnUrl?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const Provider: React.FC<ProviderProps> = ({ children, saveOnUrl }) => {
|
|
||||||
const [files, setFiles] = useState<IFiles>({})
|
|
||||||
const [theme, setTheme] = useState(initialContext.theme!)
|
|
||||||
const [selectedFileName, setSelectedFileName] = useState(initialContext.selectedFileName!)
|
|
||||||
const [filesHash, setFilesHash] = useState('')
|
|
||||||
|
|
||||||
const addFile = (name: string) => {
|
|
||||||
files[name] = {
|
|
||||||
name,
|
|
||||||
language: fileNameToLanguage(name),
|
|
||||||
value: ''
|
|
||||||
}
|
|
||||||
setFiles({ ...files })
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeFile = (name: string) => {
|
|
||||||
delete files[name]
|
|
||||||
setFiles({ ...files })
|
|
||||||
}
|
|
||||||
|
|
||||||
const changeFileName = (oldFileName: string, newFileName: string) => {
|
|
||||||
if (!files[oldFileName] || !newFileName) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const { [oldFileName]: value, ...other } = files
|
|
||||||
const newFile: IFiles = {
|
|
||||||
[newFileName]: {
|
|
||||||
...value,
|
|
||||||
language: fileNameToLanguage(newFileName),
|
|
||||||
name: newFileName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setFiles({
|
|
||||||
...other,
|
|
||||||
...newFile
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const changeTheme = (theme: ITheme) => {
|
|
||||||
setPlaygroundTheme(theme)
|
|
||||||
setTheme(theme)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const hash = strToBase64(JSON.stringify(files))
|
|
||||||
if (saveOnUrl) {
|
|
||||||
window.location.hash = hash
|
|
||||||
}
|
|
||||||
setFilesHash(hash)
|
|
||||||
}, [files])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PlaygroundContext.Provider
|
|
||||||
value={{
|
|
||||||
theme,
|
|
||||||
filesHash,
|
|
||||||
files,
|
|
||||||
selectedFileName,
|
|
||||||
setTheme,
|
|
||||||
changeTheme,
|
|
||||||
setSelectedFileName,
|
|
||||||
setFiles,
|
|
||||||
addFile,
|
|
||||||
removeFile,
|
|
||||||
changeFileName
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</PlaygroundContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Provider
|
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
|
import { strFromU8, strToU8, unzlibSync, zlibSync } from 'fflate'
|
||||||
import importMap from '@/components/ReactPlayground/template/import-map.json?raw'
|
import importMap from '@/components/ReactPlayground/template/import-map.json?raw'
|
||||||
import AppCss from '@/components/ReactPlayground/template/src/App.css?raw'
|
import AppCss from '@/components/ReactPlayground/template/src/App.css?raw'
|
||||||
import App from '@/components/ReactPlayground/template/src/App.tsx?raw'
|
import App from '@/components/ReactPlayground/template/src/App.tsx?raw'
|
||||||
import main from '@/components/ReactPlayground/template/src/main.tsx?raw'
|
import main from '@/components/ReactPlayground/template/src/main.tsx?raw'
|
||||||
import { IFiles } from '@/components/ReactPlayground/shared'
|
import { ICustomFiles, IFiles, IImportMap } from '@/components/ReactPlayground/shared'
|
||||||
import { base64ToStr } from '@/components/ReactPlayground/utils.ts'
|
|
||||||
|
|
||||||
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'
|
||||||
export const ENTRY_FILE_NAME = 'main.tsx'
|
export const ENTRY_FILE_NAME = 'main.tsx'
|
||||||
|
|
||||||
const fileNameToLanguage = (name: string) => {
|
export const fileNameToLanguage = (name: string) => {
|
||||||
const suffix = name.split('.').pop() || ''
|
const suffix = name.split('.').pop() || ''
|
||||||
if (['js', 'jsx'].includes(suffix)) return 'javascript'
|
if (['js', 'jsx'].includes(suffix)) return 'javascript'
|
||||||
if (['ts', 'tsx'].includes(suffix)) return 'typescript'
|
if (['ts', 'tsx'].includes(suffix)) return 'typescript'
|
||||||
@@ -18,12 +18,87 @@ const fileNameToLanguage = (name: string) => {
|
|||||||
return 'javascript'
|
return 'javascript'
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFilesFromUrl = () => {
|
export const strToBase64 = (str: string) => {
|
||||||
|
const buffer = strToU8(str)
|
||||||
|
const zipped = zlibSync(buffer, { level: 9 })
|
||||||
|
const binary = strFromU8(zipped, true)
|
||||||
|
return btoa(binary)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const base64ToStr = (base64: string) => {
|
||||||
|
const binary = atob(base64)
|
||||||
|
|
||||||
|
// zlib header (x78), level 9 (xDA)
|
||||||
|
if (binary.startsWith('\x78\xDA')) {
|
||||||
|
const buffer = strToU8(binary, true)
|
||||||
|
const unzipped = unzlibSync(buffer)
|
||||||
|
return strFromU8(unzipped)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformCustomFiles = (files: ICustomFiles) => {
|
||||||
|
const newFiles: IFiles = {}
|
||||||
|
Object.keys(files).forEach((key) => {
|
||||||
|
const tempFile = files[key]
|
||||||
|
if (typeof tempFile === 'string') {
|
||||||
|
newFiles[key] = {
|
||||||
|
name: key,
|
||||||
|
language: fileNameToLanguage(key),
|
||||||
|
value: tempFile
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newFiles[key] = {
|
||||||
|
name: key,
|
||||||
|
language: fileNameToLanguage(key),
|
||||||
|
value: tempFile.code,
|
||||||
|
hidden: tempFile.hidden,
|
||||||
|
active: tempFile.active
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
if (!files) return null
|
||||||
|
if (importMap) {
|
||||||
|
return {
|
||||||
|
...reactTemplateFiles,
|
||||||
|
...transformCustomFiles(files),
|
||||||
|
[IMPORT_MAP_FILE_NAME]: {
|
||||||
|
name: IMPORT_MAP_FILE_NAME,
|
||||||
|
language: 'json',
|
||||||
|
value: JSON.stringify(importMap, null, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
...reactTemplateFiles,
|
||||||
|
...transformCustomFiles(files)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getFilesFromUrl = () => {
|
||||||
let files: IFiles | undefined
|
let files: IFiles | undefined
|
||||||
try {
|
try {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const hash = window.location.hash
|
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) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Provider from '@/components/ReactPlayground/Provider.tsx'
|
import { IPlayground } from '@/components/ReactPlayground/shared'
|
||||||
import { IPlayground } from '@/components/ReactPlayground/shared.ts'
|
|
||||||
import Playground from '@/components/ReactPlayground/Playground.tsx'
|
|
||||||
|
|
||||||
const ReactPlayground: React.FC<IPlayground> = (props) => {
|
const ReactPlayground: React.FC<IPlayground> = () => {
|
||||||
return (
|
return <></>
|
||||||
<>
|
|
||||||
<Provider saveOnUrl={props.saveOnUrl}>
|
|
||||||
<Playground {...props} />
|
|
||||||
</Provider>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ReactPlayground
|
export default ReactPlayground
|
||||||
|
|||||||
@@ -1,32 +1,4 @@
|
|||||||
import { strFromU8, strToU8, unzlibSync, zlibSync } from 'fflate'
|
import { 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) => {
|
|
||||||
const buffer = strToU8(str)
|
|
||||||
const zipped = zlibSync(buffer, { level: 9 })
|
|
||||||
const binary = strFromU8(zipped, true)
|
|
||||||
return btoa(binary)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const base64ToStr = (base64: string) => {
|
|
||||||
const binary = atob(base64)
|
|
||||||
|
|
||||||
// zlib header (x78), level 9 (xDA)
|
|
||||||
if (binary.startsWith('\x78\xDA')) {
|
|
||||||
const buffer = strToU8(binary, true)
|
|
||||||
const unzipped = unzlibSync(buffer)
|
|
||||||
return strFromU8(unzipped)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const STORAGE_DARK_THEME = 'react-playground-prefer-dark'
|
const STORAGE_DARK_THEME = 'react-playground-prefer-dark'
|
||||||
|
|
||||||
@@ -41,81 +13,3 @@ export const getPlaygroundTheme = (): ITheme => {
|
|||||||
const isDarkTheme = JSON.parse(localStorage.getItem(STORAGE_DARK_THEME) || 'false') as ITheme
|
const isDarkTheme = JSON.parse(localStorage.getItem(STORAGE_DARK_THEME) || 'false') as ITheme
|
||||||
return isDarkTheme ? 'vs-dark' : 'light'
|
return isDarkTheme ? 'vs-dark' : 'light'
|
||||||
}
|
}
|
||||||
|
|
||||||
const transformCustomFiles = (files: ICustomFiles) => {
|
|
||||||
const newFiles: IFiles = {}
|
|
||||||
Object.keys(files).forEach((key) => {
|
|
||||||
const tempFile = files[key]
|
|
||||||
if (typeof tempFile === 'string') {
|
|
||||||
newFiles[key] = {
|
|
||||||
name: key,
|
|
||||||
language: fileNameToLanguage(key),
|
|
||||||
value: tempFile
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
newFiles[key] = {
|
|
||||||
name: key,
|
|
||||||
language: fileNameToLanguage(key),
|
|
||||||
value: tempFile.code,
|
|
||||||
hidden: tempFile.hidden,
|
|
||||||
active: tempFile.active
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
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) => {
|
|
||||||
if (!files) return null
|
|
||||||
if (importMap) {
|
|
||||||
return {
|
|
||||||
...reactTemplateFiles,
|
|
||||||
...transformCustomFiles(files),
|
|
||||||
[IMPORT_MAP_FILE_NAME]: {
|
|
||||||
name: IMPORT_MAP_FILE_NAME,
|
|
||||||
language: 'json',
|
|
||||||
value: JSON.stringify(importMap, null, 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
...reactTemplateFiles,
|
|
||||||
...transformCustomFiles(files)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export 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])) as IFiles
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
return files
|
|
||||||
}
|
|
||||||
|
|
||||||
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'
|
|
||||||
if (['json'].includes(suffix)) return 'json'
|
|
||||||
if (['css'].includes(suffix)) return 'css'
|
|
||||||
if (['svg'].includes(suffix)) return 'xml'
|
|
||||||
return 'javascript'
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState } from 'react'
|
import React from 'react'
|
||||||
import CodeEditor from '@/components/ReactPlayground/CodeEditor'
|
import CodeEditor from '@/components/ReactPlayground/CodeEditor'
|
||||||
import { fileNameToLanguage } from '@/components/ReactPlayground/utils.ts'
|
import { fileNameToLanguage } from '@/components/ReactPlayground/files'
|
||||||
import { IFiles } from '@/components/ReactPlayground/shared.ts'
|
import { IFiles } from '@/components/ReactPlayground/shared'
|
||||||
|
|
||||||
const OnlineEditor: React.FC = () => {
|
const OnlineEditor: React.FC = () => {
|
||||||
const [files, setFiles] = useState<IFiles>({
|
const [files, setFiles] = useState<IFiles>({
|
||||||
@@ -45,13 +45,14 @@ const OnlineEditor: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
theme={'vs-dark'}
|
|
||||||
files={files}
|
files={files}
|
||||||
selectedFileName={'App.css'}
|
selectedFileName={'App.css'}
|
||||||
notRemovable={['App.css']}
|
notRemovable={['App.css', 'fgh']}
|
||||||
readonlyFiles={['App.tsx']}
|
readonlyFiles={['App.tsx']}
|
||||||
|
onAddFile={(_, files) => setFiles(files)}
|
||||||
onRemoveFile={(_, files) => setFiles(files)}
|
onRemoveFile={(_, files) => setFiles(files)}
|
||||||
onRenameFile={(_, __, files) => setFiles(files)}
|
onRenameFile={(_, __, files) => setFiles(files)}
|
||||||
|
onChangeFileContent={(_, __, files) => setFiles(files)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user