Add FileSelector to CodeEditor

This commit is contained in:
2024-01-08 18:09:05 +08:00
parent 88c66bd7a7
commit 5709815613
12 changed files with 540 additions and 62 deletions

View File

@@ -0,0 +1,114 @@
import React from 'react'
interface ItemProps {
readonly?: boolean
creating?: boolean
value: string
active?: boolean
onOk?: (fileName: string) => void
onCancel?: () => void
onRemove?: (fileName: string) => void
onClick?: () => void
onValidate?: (newFileName: string, oldFileName: string) => boolean
}
const Item: React.FC<ItemProps> = ({
readonly = false,
value,
active = false,
onOk,
onCancel,
onRemove,
onClick,
onValidate,
...prop
}) => {
const inputRef = useRef<HTMLInputElement>(null)
const [fileName, setFileName] = useState(value)
const [creating, setCreating] = useState(prop.creating)
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
event.preventDefault()
finishNameFile()
} else if (event.key === 'Escape') {
event.preventDefault()
cancelNameFile()
}
}
const finishNameFile = () => {
if (!creating || onValidate ? !onValidate?.(fileName, value) : false) {
return
}
if (fileName === value && active) {
setCreating(false)
return
}
onOk?.(fileName)
setCreating(false)
}
const cancelNameFile = () => {
setFileName(value)
setCreating(false)
onCancel?.()
}
const handleOnDoubleClick = () => {
if (readonly) {
return
}
setCreating(true)
setFileName(value)
setTimeout(() => {
inputRef.current?.focus()
})
}
const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFileName(e.target.value)
}
const handleOnDelete = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
if (confirm(`确定删除文件 ${value} `)) {
onRemove?.(value)
}
}
useEffect(() => {
inputRef.current?.focus()
}, [])
return (
<div className={`tab-item${active ? ' active' : ''}`} onClick={onClick}>
{creating ? (
<div className={'tab-item-input'}>
<input
ref={inputRef}
value={fileName}
onChange={handleOnChange}
onBlur={finishNameFile}
onKeyDown={handleKeyDown}
/>
<span className={'tab-item-input-mask'}>{fileName}</span>
</div>
) : (
<>
<div onDoubleClick={handleOnDoubleClick}>{value}</div>
{!readonly && (
<div className={'tab-item-close'} onClick={handleOnDelete}>
<IconOxygenClose />
</div>
)}
</>
)}
</div>
)
}
export default Item

View File

@@ -0,0 +1,54 @@
[data-component=playground-file-selector].tab{
display: flex;
flex-direction: row;
gap: 1px;
padding: 10px 20px 0 20px;
.tab-item {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
height: 30px;
padding: 0 20px;
border: 1px solid #f0f0f0;
background-color: rgba(0, 0, 0, 0.04);
border-radius: 6px 6px 0 0;
cursor: pointer;
.tab-item-input {
position: relative;
min-width: 40px;
transform: translateY(1px);
.tab-item-input-mask {
display: inline-block;
color: transparent;
}
input {
position: absolute;
background-color: transparent;
width: 100%;
font-size: 1em;
}
}
.tab-item-close {
transform: translateX(10px);
:hover {
fill: #888;
}
svg {
height: 8px;
fill: #666;
}
}
&.active {
background-color: white;
border-bottom: none;
}
}
}

View File

@@ -0,0 +1,151 @@
import React from 'react'
import '@/components/ReactPlayground/CodeEditor/FileSelector/file-selector.scss'
import { IFiles } from '@/components/ReactPlayground/shared.ts'
import {
ENTRY_FILE_NAME,
IMPORT_MAP_FILE_NAME,
MAIN_FILE_NAME
} from '@/components/ReactPlayground/files.ts'
import Item from '@/components/ReactPlayground/CodeEditor/FileSelector/Item.tsx'
interface FileSelectorProps {
files?: IFiles
onChange?: (fileName: string) => void
onError?: (msg: string) => void
readonly?: boolean
readonlyFiles?: string[]
onRemoveFile?: (fileName: string) => void
onAddFile?: (fileName: string) => void
onUpdateFileName?: (newFileName: string, oldFileName: string) => void
selectedFileName?: string
}
const FileSelector: React.FC<FileSelectorProps> = ({
files = {},
onChange,
onError,
readonly = false,
readonlyFiles = [],
onRemoveFile,
onAddFile,
onUpdateFileName,
selectedFileName = ''
}) => {
const [tabs, setTabs] = useState<string[]>([])
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 (
<>
<div
data-component={'playground-file-selector'}
className={'tab'}
style={{ flex: '0 0 auto' }}
>
{tabs.map((item, index) => (
<Item
key={index + item}
value={item}
active={selectedFileName == item}
creating={creating}
readonly={
readonly || readonlyFiles.includes(item) || MAIN_FILE_NAME == item
}
onValidate={handleOnValidateTab}
onOk={(name) => handleOnSaveTab(name, item)}
onCancel={handleOnCancel}
onRemove={handleOnRemove}
onClick={() => handleOnClickTab(item)}
/>
))}
</div>
</>
)
}
export default FileSelector