183 lines
6.0 KiB
TypeScript
183 lines
6.0 KiB
TypeScript
import React from 'react'
|
|
import '@/components/ReactPlayground/CodeEditor/FileSelector/file-selector.scss'
|
|
import { IFiles } from '@/components/ReactPlayground/shared'
|
|
import { ENTRY_FILE_NAME, IMPORT_MAP_FILE_NAME } from '@/components/ReactPlayground/files'
|
|
import Item from '@/components/ReactPlayground/CodeEditor/FileSelector/Item'
|
|
import HideScrollbar, { HideScrollbarElement } from '@/components/common/HideScrollbar'
|
|
import FlexBox from '@/components/common/FlexBox'
|
|
|
|
interface FileSelectorProps {
|
|
files?: IFiles
|
|
onChange?: (fileName: string) => void
|
|
onError?: (msg: string) => void
|
|
readonly?: boolean
|
|
notRemovableFiles?: 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,
|
|
notRemovableFiles = [],
|
|
onRemoveFile,
|
|
onAddFile,
|
|
onUpdateFileName,
|
|
selectedFileName = ''
|
|
}) => {
|
|
const hideScrollbarRef = useRef<HideScrollbarElement>(null)
|
|
const [tabs, setTabs] = useState<string[]>([])
|
|
const [creating, setCreating] = useState(false)
|
|
const [hasEditing, setHasEditing] = 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)
|
|
setTimeout(() => {
|
|
hideScrollbarRef.current?.scrollRight(1000)
|
|
})
|
|
}
|
|
|
|
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?.(value, item)
|
|
}
|
|
|
|
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)
|
|
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(() => {
|
|
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'}>
|
|
<div className={'multiple'}>
|
|
<HideScrollbar ref={hideScrollbarRef}>
|
|
<FlexBox direction={'horizontal'} className={'tab-content'}>
|
|
{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
|
|
value={'Import Map'}
|
|
active={selectedFileName === IMPORT_MAP_FILE_NAME}
|
|
onClick={editImportMap}
|
|
readonly
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default FileSelector
|