Files
bitburner-src/src/ScriptEditor/ui/Tabs.tsx
T

147 lines
5.0 KiB
TypeScript

import React, { useState } from "react";
import { DragDropContext, Droppable, Draggable, DropResult } from "react-beautiful-dnd";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton";
import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip";
import CloseIcon from "@mui/icons-material/Close";
import SearchIcon from "@mui/icons-material/Search";
import { useBoolean, useRerender } from "../../ui/React/hooks";
import { Settings } from "../../Settings/Settings";
import { getTabId, isUnsavedFile, reorder } from "./utils";
import { OpenScript } from "./OpenScript";
import { Tab } from "./Tab";
import { SpecialServers } from "../../Server/data/SpecialServers";
const searchWidth = 180;
interface IProps {
scripts: OpenScript[];
currentScript: OpenScript | null;
onTabClick: (tabIndex: number) => void;
onTabClose: (tabIndex: number) => void;
onTabUpdate: (tabIndex: number) => void;
}
export function Tabs({ scripts, currentScript, onTabClick, onTabClose, onTabUpdate }: IProps) {
const [filter, setFilter] = useState("");
const [isSearchTooltipOpen, { on: openSearchTooltip, off: closeSearchTooltip }] = useBoolean(false);
const [searchExpanded, setSearchExpanded] = useState(false);
const rerender = useRerender();
const filteredScripts = Object.values(scripts)
.map((script, originalIndex) => ({ script, originalIndex }))
.filter(({ script }) => script.hostname.includes(filter) || script.path.includes(filter));
function onDragEnd(result: DropResult): void {
// Dropped outside of the list
if (!result.destination) return;
reorder(
scripts,
filteredScripts[result.source.index].originalIndex,
filteredScripts[result.destination.index].originalIndex,
);
rerender();
}
function handleFilterChange(event: React.ChangeEvent<HTMLInputElement>): void {
setFilter(event.target.value);
}
function toggleSearch(): void {
setFilter("");
setSearchExpanded(!searchExpanded);
closeSearchTooltip();
}
function handleScroll(e: React.WheelEvent<HTMLDivElement>): void {
e.currentTarget.scrollLeft += e.deltaY;
}
return (
<Box display="flex" flexGrow="0" flexDirection="row" alignItems="center">
<Tooltip
title={"Search Open Scripts"}
open={isSearchTooltipOpen}
onOpen={openSearchTooltip}
onClose={closeSearchTooltip}
>
<span style={{ marginRight: 5 }}>
{searchExpanded ? (
<TextField
value={filter}
onChange={handleFilterChange}
autoFocus
sx={{ minWidth: searchWidth, maxWidth: searchWidth }}
InputProps={{
startAdornment: <SearchIcon />,
spellCheck: false,
endAdornment: (
<IconButton onClick={toggleSearch}>
<CloseIcon />
</IconButton>
),
}}
/>
) : (
<Button onClick={toggleSearch}>
<SearchIcon />
</Button>
)}
</span>
</Tooltip>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="tabs" direction="horizontal">
{(provided, snapshot) => (
<Box
display="flex"
flexGrow="1"
flexDirection="row"
alignItems="center"
whiteSpace="nowrap"
ref={provided.innerRef}
{...provided.droppableProps}
style={{
backgroundColor: snapshot.isDraggingOver
? Settings.theme.backgroundsecondary
: Settings.theme.backgroundprimary,
overflowX: "scroll",
}}
onWheel={handleScroll}
>
{filteredScripts.map(({ script, originalIndex }, index) => {
const { path, hostname } = script;
const isActive = currentScript?.path === script.path && currentScript.hostname === script.hostname;
const tabId = getTabId(hostname, path);
return (
<Draggable key={tabId} draggableId={tabId} index={index} disableInteractiveElementBlocking>
{(provided) => (
<Tab
provided={provided}
tabId={tabId}
isActive={isActive}
isExternal={hostname !== SpecialServers.Home}
isUnsaved={() => isUnsavedFile(scripts, index)}
onClick={() => onTabClick(originalIndex)}
onClose={() => onTabClose(originalIndex)}
onUpdate={() => onTabUpdate(originalIndex)}
/>
)}
</Draggable>
);
})}
{provided.placeholder}
</Box>
)}
</Droppable>
</DragDropContext>
</Box>
);
}