import React, { useEffect, useState, Fragment, isValidElement, useCallback } from 'react' import type { ReactNode, ReactElement } from 'react' import { Box, Button, Flex, Input, Label, Pre, Stack, Text } from '.' import { Dialog, DialogTrigger, DialogContent, DialogTitle, DialogDescription, DialogClose } from './Dialog' import { Plus, X } from 'phosphor-react' import { styled } from '../stitches.config' import { capitalize } from '../utils/helpers' import ContextMenu, { ContentMenuOption } from './ContextMenu' const ErrorText = styled(Text, { color: '$error', mt: '$1', display: 'block' }) type Nullable = T | null | undefined | false interface TabProps { header: string children?: ReactNode renameDisabled?: boolean } // TODO customize messages shown interface Props { label?: string activeIndex?: number activeHeader?: string headless?: boolean children: ReactElement[] keepAllAlive?: boolean defaultExtension?: string extensionRequired?: boolean allowedExtensions?: string[] headerExtraValidation?: { regex: string | RegExp error: string } onCreateNewTab?: (name: string) => any onRenameTab?: (index: number, nwName: string, oldName?: string) => any onCloseTab?: (index: number, header?: string) => any onChangeActive?: (index: number, header?: string) => any } export const Tab = (props: TabProps) => null export const Tabs = ({ label = 'Tab', children, activeIndex, activeHeader, headless, keepAllAlive = false, onCreateNewTab, onCloseTab, onChangeActive, onRenameTab, headerExtraValidation, extensionRequired, defaultExtension = '', allowedExtensions }: Props) => { const [active, setActive] = useState(activeIndex || 0) const tabs: TabProps[] = children.map(elem => elem.props) const [isNewtabDialogOpen, setIsNewtabDialogOpen] = useState(false) const [renamingTab, setRenamingTab] = useState(null) const [tabname, setTabname] = useState('') const [tabnameError, setTabnameError] = useState(null) useEffect(() => { if (activeIndex) setActive(activeIndex) }, [activeIndex]) useEffect(() => { if (activeHeader) { const idx = tabs.findIndex(tab => tab.header === activeHeader) if (idx !== -1) setActive(idx) else setActive(0) } }, [activeHeader, tabs]) // when filename changes, reset error useEffect(() => { setTabnameError(null) }, [tabname, setTabnameError]) const validateTabname = useCallback( (tabname: string): { error?: string; result?: string } => { if (!tabname) { return { error: `Please enter ${label.toLocaleLowerCase()} name.` } } let ext = (tabname.includes('.') && tabname.split('.').pop()) || '' if (!ext && defaultExtension) { ext = defaultExtension tabname = `${tabname}.${defaultExtension}` } if (tabs.find(tab => tab.header === tabname)) { return { error: `${capitalize(label)} name already exists.` } } if (extensionRequired && !ext) { return { error: 'File extension is required!' } } if (allowedExtensions && !allowedExtensions.includes(ext)) { return { error: 'This file extension is not allowed!' } } if (headerExtraValidation && !tabname.match(headerExtraValidation.regex)) { return { error: headerExtraValidation.error } } return { result: tabname } }, [allowedExtensions, defaultExtension, extensionRequired, headerExtraValidation, label, tabs] ) const handleActiveChange = useCallback( (idx: number, header?: string) => { setActive(idx) onChangeActive?.(idx, header) }, [onChangeActive] ) const handleRenameTab = useCallback(() => { if (renamingTab === null) return const res = validateTabname(tabname) if (res.error) { setTabnameError(`Error: ${res.error}`) return } const { result: nwName = tabname } = res setRenamingTab(null) setTabname('') const oldName = tabs[renamingTab]?.header onRenameTab?.(renamingTab, nwName, oldName) handleActiveChange(renamingTab, nwName) }, [handleActiveChange, onRenameTab, renamingTab, tabname, tabs, validateTabname]) const handleCreateTab = useCallback(() => { const res = validateTabname(tabname) if (res.error) { setTabnameError(`Error: ${res.error}`) return } const { result: _tabname = tabname } = res setIsNewtabDialogOpen(false) setTabname('') onCreateNewTab?.(_tabname) handleActiveChange(tabs.length, _tabname) }, [validateTabname, tabname, onCreateNewTab, handleActiveChange, tabs.length]) const handleCloseTab = useCallback( (idx: number) => { onCloseTab?.(idx, tabs[idx].header) if (idx <= active && active !== 0) { const nwActive = active - 1 handleActiveChange(nwActive, tabs[nwActive].header) } }, [active, handleActiveChange, onCloseTab, tabs] ) const closeOption = (idx: number): Nullable => onCloseTab && { type: 'text', label: 'Close', key: 'close', onSelect: () => handleCloseTab(idx) } const renameOption = (idx: number, tab: TabProps): Nullable => { return ( onRenameTab && !tab.renameDisabled && { type: 'text', label: 'Rename', key: 'rename', onSelect: () => setRenamingTab(idx) } ) } return ( <> {!headless && ( {tabs.map((tab, idx) => ( ))} {onCreateNewTab && ( Create new {label.toLocaleLowerCase()} setTabname(e.target.value)} onKeyPress={e => { if (e.key === 'Enter') { handleCreateTab() } }} /> {tabnameError} )} {onRenameTab && ( setRenamingTab(null)}> Rename
{tabs[renamingTab || 0]?.header}
setTabname(e.target.value)} onKeyPress={e => { if (e.key === 'Enter') { handleRenameTab() } }} /> {tabnameError}
)}
)} {keepAllAlive ? tabs.map((tab, idx) => { // TODO Maybe rule out fragments as children if (!isValidElement(tab.children)) { if (active !== idx) return null return tab.children } let key = tab.children.key || tab.header || idx let { children } = tab let { style, ...props } = children.props return ( ) }) : tabs[active] && ( {tabs[active].children} )} ) }