Complete main UI #37

Merged
FatttSnake merged 192 commits from FatttSnake into dev 2024-02-23 16:31:17 +08:00
9 changed files with 115 additions and 31 deletions
Showing only changes of commit 5a320e459a - Show all commits

View File

@@ -2,7 +2,7 @@ import { editor, IPosition, Selection } from 'monaco-editor'
import ScrollType = editor.ScrollType import ScrollType = editor.ScrollType
import { Monaco } from '@monaco-editor/react' import { Monaco } from '@monaco-editor/react'
import { getWorker, MonacoJsxSyntaxHighlight } from 'monaco-jsx-syntax-highlight' import { getWorker, MonacoJsxSyntaxHighlight } from 'monaco-jsx-syntax-highlight'
import { createATA, TypeHelper } from '@/components/ReactPlayground/CodeEditor/Editor/ata.ts' import { createATA, TypeHelper } from '@/components/ReactPlayground/CodeEditor/Editor/ata'
export const useEditor = () => { export const useEditor = () => {
const doOpenEditor = ( const doOpenEditor = (

View File

@@ -3,13 +3,14 @@ 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 { MonacoEditorConfig } from '@/components/ReactPlayground/CodeEditor/Editor/monacoConfig'
import { fileNameToLanguage } from '@/components/ReactPlayground/utils' import { fileNameToLanguage } from '@/components/ReactPlayground/utils'
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'
interface EditorProps { interface EditorProps {
files?: IFiles files?: IFiles
selectedFileName?: string selectedFileName?: string
readonly?: boolean
onChange?: (code: string | undefined) => void onChange?: (code: string | undefined) => void
options?: IEditorOptions options?: IEditorOptions
theme?: ITheme theme?: ITheme
@@ -19,6 +20,7 @@ interface EditorProps {
const Editor: React.FC<EditorProps> = ({ const Editor: React.FC<EditorProps> = ({
files = {}, files = {},
selectedFileName = '', selectedFileName = '',
readonly,
theme, theme,
onChange, onChange,
options, options,
@@ -92,7 +94,12 @@ const Editor: React.FC<EditorProps> = ({
value={file.value} value={file.value}
onChange={onChange} onChange={onChange}
onMount={handleOnEditorDidMount} onMount={handleOnEditorDidMount}
options={{ ...MonacoEditorConfig, ...options, theme: undefined }} options={{
...MonacoEditorConfig,
...options,
theme: undefined,
readOnly: readonly
}}
/> />
</> </>
) )

View File

@@ -5,6 +5,8 @@ interface ItemProps {
creating?: boolean creating?: boolean
value: string value: string
active?: boolean active?: boolean
hasEditing?: boolean
setHasEditing?: React.Dispatch<React.SetStateAction<boolean>>
onOk?: (fileName: string) => void onOk?: (fileName: string) => void
onCancel?: () => void onCancel?: () => void
onRemove?: (fileName: string) => void onRemove?: (fileName: string) => void
@@ -16,6 +18,8 @@ const Item: React.FC<ItemProps> = ({
readonly = false, readonly = false,
value, value,
active = false, active = false,
hasEditing,
setHasEditing,
onOk, onOk,
onCancel, onCancel,
onRemove, onRemove,
@@ -27,6 +31,14 @@ const Item: React.FC<ItemProps> = ({
const [fileName, setFileName] = useState(value) const [fileName, setFileName] = useState(value)
const [creating, setCreating] = useState(prop.creating) const [creating, setCreating] = useState(prop.creating)
const handleOnClick = () => {
if (hasEditing) {
return
}
onClick?.()
}
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
event.preventDefault() event.preventDefault()
@@ -39,30 +51,35 @@ const Item: React.FC<ItemProps> = ({
const finishNameFile = () => { const finishNameFile = () => {
if (!creating || onValidate ? !onValidate?.(fileName, value) : false) { if (!creating || onValidate ? !onValidate?.(fileName, value) : false) {
inputRef.current?.focus()
return return
} }
if (fileName === value && active) { if (fileName === value && active) {
setCreating(false) setCreating(false)
setHasEditing?.(false)
return return
} }
onOk?.(fileName) onOk?.(fileName)
setCreating(false) setCreating(false)
setHasEditing?.(false)
} }
const cancelNameFile = () => { const cancelNameFile = () => {
setFileName(value) setFileName(value)
setCreating(false) setCreating(false)
setHasEditing?.(false)
onCancel?.() onCancel?.()
} }
const handleOnDoubleClick = () => { const handleOnDoubleClick = () => {
if (readonly) { if (readonly || creating || hasEditing) {
return return
} }
setCreating(true) setCreating(true)
setHasEditing?.(true)
setFileName(value) setFileName(value)
setTimeout(() => { setTimeout(() => {
inputRef.current?.focus() inputRef.current?.focus()
@@ -75,6 +92,9 @@ const Item: React.FC<ItemProps> = ({
const handleOnDelete = (e: React.MouseEvent<HTMLDivElement>) => { const handleOnDelete = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation() e.stopPropagation()
if (hasEditing) {
return
}
if (confirm(`确定删除文件 ${value} `)) { if (confirm(`确定删除文件 ${value} `)) {
onRemove?.(value) onRemove?.(value)
} }
@@ -85,7 +105,7 @@ const Item: React.FC<ItemProps> = ({
}, []) }, [])
return ( return (
<div className={`tab-item${active ? ' active' : ''}`} onClick={onClick}> <div className={`tab-item${active ? ' active' : ''}`} onClick={handleOnClick}>
{creating ? ( {creating ? (
<div className={'tab-item-input'}> <div className={'tab-item-input'}>
<input <input

View File

@@ -2,11 +2,10 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 1px; gap: 1px;
padding: 10px 20px 0 20px; padding: 4px 20px 0 20px;
.tab-item { .tab-item {
display: flex; display: flex;
flex-wrap: wrap;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 30px; height: 30px;

View File

@@ -1,19 +1,15 @@
import React from 'react' import React from 'react'
import '@/components/ReactPlayground/CodeEditor/FileSelector/file-selector.scss' import '@/components/ReactPlayground/CodeEditor/FileSelector/file-selector.scss'
import { IFiles } from '@/components/ReactPlayground/shared.ts' import { IFiles } from '@/components/ReactPlayground/shared'
import { import { ENTRY_FILE_NAME, IMPORT_MAP_FILE_NAME } from '@/components/ReactPlayground/files'
ENTRY_FILE_NAME, import Item from '@/components/ReactPlayground/CodeEditor/FileSelector/Item'
IMPORT_MAP_FILE_NAME,
MAIN_FILE_NAME
} from '@/components/ReactPlayground/files.ts'
import Item from '@/components/ReactPlayground/CodeEditor/FileSelector/Item.tsx'
interface FileSelectorProps { interface FileSelectorProps {
files?: IFiles files?: IFiles
onChange?: (fileName: string) => void onChange?: (fileName: string) => void
onError?: (msg: string) => void onError?: (msg: string) => void
readonly?: boolean readonly?: boolean
readonlyFiles?: string[] notRemovableFiles?: string[]
onRemoveFile?: (fileName: string) => void onRemoveFile?: (fileName: string) => void
onAddFile?: (fileName: string) => void onAddFile?: (fileName: string) => void
onUpdateFileName?: (newFileName: string, oldFileName: string) => void onUpdateFileName?: (newFileName: string, oldFileName: string) => void
@@ -25,7 +21,7 @@ const FileSelector: React.FC<FileSelectorProps> = ({
onChange, onChange,
onError, onError,
readonly = false, readonly = false,
readonlyFiles = [], notRemovableFiles = [],
onRemoveFile, onRemoveFile,
onAddFile, onAddFile,
onUpdateFileName, onUpdateFileName,
@@ -33,6 +29,7 @@ const FileSelector: React.FC<FileSelectorProps> = ({
}) => { }) => {
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 getMaxSequenceTabName = (filesName: string[]) => { const getMaxSequenceTabName = (filesName: string[]) => {
const result = filesName.reduce((max, filesName) => { const result = filesName.reduce((max, filesName) => {
@@ -79,7 +76,7 @@ const FileSelector: React.FC<FileSelectorProps> = ({
onAddFile?.(value) onAddFile?.(value)
setCreating(false) setCreating(false)
} else { } else {
onUpdateFileName?.(item, value) onUpdateFileName?.(value, item)
} }
setTimeout(() => { setTimeout(() => {
@@ -106,7 +103,14 @@ const FileSelector: React.FC<FileSelectorProps> = ({
const handleOnRemove = (fileName: string) => { const handleOnRemove = (fileName: string) => {
onRemoveFile?.(fileName) onRemoveFile?.(fileName)
handleOnClickTab(Object.keys(files)[Object.keys(files).length - 1]) if (fileName === selectedFileName) {
const index = Object.keys(files).indexOf(fileName) - 1
if (index >= 0) {
handleOnClickTab(Object.keys(files)[index])
} else {
handleOnClickTab(Object.keys(files)[1])
}
}
} }
useEffect(() => { useEffect(() => {
@@ -133,9 +137,9 @@ const FileSelector: React.FC<FileSelectorProps> = ({
value={item} value={item}
active={selectedFileName == item} active={selectedFileName == item}
creating={creating} creating={creating}
readonly={ readonly={readonly || notRemovableFiles.includes(item)}
readonly || readonlyFiles.includes(item) || MAIN_FILE_NAME == item hasEditing={hasEditing}
} setHasEditing={setHasEditing}
onValidate={handleOnValidateTab} onValidate={handleOnValidateTab}
onOk={(name) => handleOnSaveTab(name, item)} onOk={(name) => handleOnSaveTab(name, item)}
onCancel={handleOnCancel} onCancel={handleOnCancel}

View File

@@ -1,20 +1,23 @@
import React, { useState } from 'react' import React from 'react'
import _ from 'lodash'
import { IEditorOptions, IFiles, ITheme } from '@/components/ReactPlayground/shared'
import { fileNameToLanguage } from '@/components/ReactPlayground/utils'
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 { IEditorOptions, IFiles, ITheme } from '@/components/ReactPlayground/shared.ts' import FitFullscreen from '@/components/common/FitFullscreen'
import FitFullscreen from '@/components/common/FitFullscreen.tsx' import FlexBox from '@/components/common/FlexBox'
import FlexBox from '@/components/common/FlexBox.tsx'
import _ from 'lodash'
interface CodeEditorProps { interface CodeEditorProps {
theme: ITheme theme: ITheme
files: IFiles files: IFiles
readonly?: boolean readonly?: boolean
readonlyFiles?: string[] readonlyFiles?: string[]
notRemovable?: string[]
selectedFileName: string selectedFileName: string
options?: IEditorOptions options?: IEditorOptions
onSelectedFileChange?: (fileName: string) => void onSelectedFileChange?: (fileName: string) => void
onRemoveFile?: (fileName: string, files: IFiles) => void onRemoveFile?: (fileName: string, files: IFiles) => void
onRenameFile?: (newFileName: string, oldFileName: string, files: IFiles) => void
} }
const CodeEditor: React.FC<CodeEditorProps> = ({ const CodeEditor: React.FC<CodeEditorProps> = ({
@@ -22,9 +25,11 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
files, files,
readonly, readonly,
readonlyFiles, readonlyFiles,
notRemovable,
options, options,
onSelectedFileChange, onSelectedFileChange,
onRemoveFile, onRemoveFile,
onRenameFile,
...props ...props
}) => { }) => {
const [selectedFileName, setSelectedFileName] = useState(props.selectedFileName) const [selectedFileName, setSelectedFileName] = useState(props.selectedFileName)
@@ -43,6 +48,24 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
onRemoveFile?.(fileName, clone) onRemoveFile?.(fileName, clone)
} }
const handleOnUpdateFileName = (newFileName: string, oldFileName: string) => {
if (!files[oldFileName] || !newFileName) {
return
}
const { [oldFileName]: value, ...rest } = files
const newFile: IFiles = {
[newFileName]: {
...value,
language: fileNameToLanguage(newFileName),
name: newFileName
}
}
const newFiles: IFiles = { ...rest, ...newFile }
onRenameFile?.(newFileName, oldFileName, newFiles)
}
return ( return (
<> <>
<FitFullscreen> <FitFullscreen>
@@ -50,18 +73,20 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
<FileSelector <FileSelector
files={files} files={files}
readonly={readonly} readonly={readonly}
readonlyFiles={readonlyFiles} notRemovableFiles={notRemovable}
selectedFileName={ selectedFileName={
onSelectedFileChange ? props.selectedFileName : selectedFileName onSelectedFileChange ? props.selectedFileName : selectedFileName
} }
onChange={handleOnChangeSelectedFile} onChange={handleOnChangeSelectedFile}
onRemoveFile={handleOnRemoveFile} onRemoveFile={handleOnRemoveFile}
onUpdateFileName={handleOnUpdateFileName}
/> />
<Editor <Editor
theme={theme} theme={theme}
selectedFileName={selectedFileName} selectedFileName={selectedFileName}
files={files} files={files}
options={options} options={options}
readonly={readonly || readonlyFiles?.includes(selectedFileName)}
/> />
</FlexBox> </FlexBox>
</FitFullscreen> </FitFullscreen>

View File

@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import { editor } from 'monaco-editor' import { editor } from 'monaco-editor'
export type ILanguage = 'javascript' | 'typescript' | 'json' | 'css' export type ILanguage = 'javascript' | 'typescript' | 'json' | 'css' | 'xml'
export interface IFile { export interface IFile {
name: string name: string

View File

@@ -116,5 +116,6 @@ export const fileNameToLanguage = (name: string): ILanguage => {
if (['ts', 'tsx'].includes(suffix)) return 'typescript' if (['ts', 'tsx'].includes(suffix)) return 'typescript'
if (['json'].includes(suffix)) return 'json' if (['json'].includes(suffix)) return 'json'
if (['css'].includes(suffix)) return 'css' if (['css'].includes(suffix)) return 'css'
if (['svg'].includes(suffix)) return 'xml'
return 'javascript' return 'javascript'
} }

View File

@@ -5,25 +5,53 @@ import { IFiles } from '@/components/ReactPlayground/shared.ts'
const OnlineEditor: React.FC = () => { const OnlineEditor: React.FC = () => {
const [files, setFiles] = useState<IFiles>({ const [files, setFiles] = useState<IFiles>({
abc: { ['App.tsx']: {
name: 'App.tsx', name: 'App.tsx',
language: fileNameToLanguage('App.tsx'), language: fileNameToLanguage('App.tsx'),
value: 'const a = () => {}' value: 'const a = () => {}'
}, },
['App.css']: {
name: 'App.css',
language: fileNameToLanguage('App.css'),
value: '.title {}'
},
cde: { cde: {
name: 'App.css', name: 'App.css',
language: fileNameToLanguage('App.css'), language: fileNameToLanguage('App.css'),
value: '.title {}' value: '.title {}'
},
def: {
name: 'App.css',
language: fileNameToLanguage('App.css'),
value: '.title {}'
},
efg: {
name: 'App.css',
language: fileNameToLanguage('App.css'),
value: '.title {}'
},
fgh: {
name: 'App.css',
language: fileNameToLanguage('App.css'),
value: '.title {}'
},
ghi: {
name: 'App.css',
language: fileNameToLanguage('App.css'),
value: '.title {}'
} }
}) })
return ( return (
<> <>
<CodeEditor <CodeEditor
theme={'light'} theme={'vs-dark'}
files={files} files={files}
selectedFileName={'abc'} selectedFileName={'App.css'}
notRemovable={['App.css']}
readonlyFiles={['App.tsx']}
onRemoveFile={(_, files) => setFiles(files)} onRemoveFile={(_, files) => setFiles(files)}
onRenameFile={(_, __, files) => setFiles(files)}
/> />
</> </>
) )