mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-11-26 09:33:49 +01:00
parent
bda1daf49f
commit
7050c90378
100
src/ScriptEditor/ui/Tab.tsx
Normal file
100
src/ScriptEditor/ui/Tab.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import { DraggableProvided } from "react-beautiful-dnd";
|
||||||
|
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
|
||||||
|
import SyncIcon from "@mui/icons-material/Sync";
|
||||||
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
|
|
||||||
|
import { Settings } from "../../Settings/Settings";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
provided: DraggableProvided;
|
||||||
|
title: string;
|
||||||
|
isActive: boolean;
|
||||||
|
isExternal: boolean;
|
||||||
|
|
||||||
|
onClick: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
onUpdate: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabMargin = 5;
|
||||||
|
const tabIconWidth = 25;
|
||||||
|
const tabIconHeight = 38.5;
|
||||||
|
|
||||||
|
export function Tab({ provided, title, isActive, isExternal, onClick, onClose, onUpdate }: IProps) {
|
||||||
|
const colorProps = isActive
|
||||||
|
? {
|
||||||
|
background: Settings.theme.button,
|
||||||
|
borderColor: Settings.theme.button,
|
||||||
|
color: Settings.theme.primary,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
background: Settings.theme.backgroundsecondary,
|
||||||
|
borderColor: Settings.theme.backgroundsecondary,
|
||||||
|
color: Settings.theme.secondary,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isExternal) {
|
||||||
|
colorProps.color = Settings.theme.info;
|
||||||
|
}
|
||||||
|
const iconButtonStyle = {
|
||||||
|
maxWidth: tabIconWidth,
|
||||||
|
minWidth: tabIconWidth,
|
||||||
|
minHeight: tabIconHeight,
|
||||||
|
maxHeight: tabIconHeight,
|
||||||
|
...colorProps,
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tabRef.current && isActive) {
|
||||||
|
tabRef.current?.scrollIntoView();
|
||||||
|
}
|
||||||
|
}, [isActive]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={(element) => {
|
||||||
|
tabRef.current = element;
|
||||||
|
provided.innerRef(element);
|
||||||
|
}}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
style={{
|
||||||
|
...provided.draggableProps.style,
|
||||||
|
marginRight: tabMargin,
|
||||||
|
flexShrink: 0,
|
||||||
|
border: "1px solid " + Settings.theme.well,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tooltip title={title}>
|
||||||
|
<Button
|
||||||
|
onClick={onClick}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.button === 1) onClose();
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
minHeight: tabIconHeight,
|
||||||
|
overflow: "hidden",
|
||||||
|
...colorProps,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ overflow: "hidden", direction: "rtl", textOverflow: "ellipsis" }}>{title}</span>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Overwrite editor content with saved file content">
|
||||||
|
<Button onClick={onUpdate} style={iconButtonStyle}>
|
||||||
|
<SyncIcon fontSize="small" />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Button onClick={onClose} style={iconButtonStyle}>
|
||||||
|
<CloseIcon fontSize="small" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,20 +1,24 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
|
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 { Box, Button, TextField, Tooltip } from "@mui/material";
|
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import SearchIcon from "@mui/icons-material/Search";
|
import SearchIcon from "@mui/icons-material/Search";
|
||||||
import SyncIcon from "@mui/icons-material/Sync";
|
|
||||||
|
|
||||||
import { useRerender } from "../../ui/React/hooks";
|
import { useBoolean, useRerender } from "../../ui/React/hooks";
|
||||||
import { Settings } from "../../Settings/Settings";
|
import { Settings } from "../../Settings/Settings";
|
||||||
|
|
||||||
import { dirty, reorder } from "./utils";
|
import { dirty, reorder } from "./utils";
|
||||||
import { OpenScript } from "./OpenScript";
|
import { OpenScript } from "./OpenScript";
|
||||||
|
import { Tab } from "./Tab";
|
||||||
|
|
||||||
const tabsMaxWidth = 1640;
|
const tabsMaxWidth = 1640;
|
||||||
const tabMargin = 5;
|
const searchWidth = 180;
|
||||||
const tabIconWidth = 25;
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
scripts: OpenScript[];
|
scripts: OpenScript[];
|
||||||
@ -27,160 +31,123 @@ interface IProps {
|
|||||||
|
|
||||||
export function Tabs({ scripts, currentScript, onTabClick, onTabClose, onTabUpdate }: IProps) {
|
export function Tabs({ scripts, currentScript, onTabClick, onTabClose, onTabUpdate }: IProps) {
|
||||||
const [filter, setFilter] = useState("");
|
const [filter, setFilter] = useState("");
|
||||||
|
const [isSearchTooltipOpen, { on: openSearchTooltip, off: closeSearchTooltip }] = useBoolean(false);
|
||||||
const [searchExpanded, setSearchExpanded] = useState(false);
|
const [searchExpanded, setSearchExpanded] = useState(false);
|
||||||
const rerender = useRerender();
|
const rerender = useRerender();
|
||||||
|
|
||||||
function onDragEnd(result: any): void {
|
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
|
// Dropped outside of the list
|
||||||
if (!result.destination) return;
|
if (!result.destination) return;
|
||||||
reorder(scripts, result.source.index, result.destination.index);
|
reorder(
|
||||||
|
scripts,
|
||||||
|
filteredScripts[result.source.index].originalIndex,
|
||||||
|
filteredScripts[result.destination.index].originalIndex,
|
||||||
|
);
|
||||||
rerender();
|
rerender();
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredOpenScripts = Object.values(scripts).filter(
|
|
||||||
(script) => script.hostname.includes(filter) || script.path.includes(filter),
|
|
||||||
);
|
|
||||||
|
|
||||||
function handleFilterChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
function handleFilterChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||||
setFilter(event.target.value);
|
setFilter(event.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleExpandSearch(): void {
|
function toggleSearch(): void {
|
||||||
setFilter("");
|
setFilter("");
|
||||||
setSearchExpanded(!searchExpanded);
|
setSearchExpanded(!searchExpanded);
|
||||||
|
closeSearchTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleScroll(e: React.WheelEvent<HTMLDivElement>): void {
|
||||||
|
e.currentTarget.scrollLeft += e.deltaY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabMaxWidth = filteredOpenScripts.length ? tabsMaxWidth / filteredOpenScripts.length - tabMargin : 0;
|
|
||||||
const tabTextWidth = tabMaxWidth - tabIconWidth * 2;
|
|
||||||
return (
|
return (
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<Box display="flex" flexGrow="0" flexDirection="row" alignItems="center">
|
||||||
<Droppable droppableId="tabs" direction="horizontal">
|
<Tooltip
|
||||||
{(provided, snapshot) => (
|
title={"Search Open Scripts"}
|
||||||
<Box
|
open={isSearchTooltipOpen}
|
||||||
maxWidth={`${tabsMaxWidth}px`}
|
onOpen={openSearchTooltip}
|
||||||
display="flex"
|
onClose={closeSearchTooltip}
|
||||||
flexGrow="0"
|
>
|
||||||
flexDirection="row"
|
<span style={{ marginRight: 5 }}>
|
||||||
alignItems="center"
|
{searchExpanded ? (
|
||||||
whiteSpace="nowrap"
|
<TextField
|
||||||
ref={provided.innerRef}
|
value={filter}
|
||||||
{...provided.droppableProps}
|
onChange={handleFilterChange}
|
||||||
style={{
|
autoFocus
|
||||||
backgroundColor: snapshot.isDraggingOver
|
sx={{ minWidth: searchWidth, maxWidth: searchWidth }}
|
||||||
? Settings.theme.backgroundsecondary
|
InputProps={{
|
||||||
: Settings.theme.backgroundprimary,
|
startAdornment: <SearchIcon />,
|
||||||
overflowX: "scroll",
|
spellCheck: false,
|
||||||
}}
|
endAdornment: (
|
||||||
>
|
<IconButton onClick={toggleSearch}>
|
||||||
<Tooltip title={"Search Open Scripts"}>
|
<CloseIcon />
|
||||||
{searchExpanded ? (
|
</IconButton>
|
||||||
<TextField
|
),
|
||||||
value={filter}
|
}}
|
||||||
onChange={handleFilterChange}
|
/>
|
||||||
autoFocus
|
) : (
|
||||||
InputProps={{
|
<Button onClick={toggleSearch}>
|
||||||
startAdornment: <SearchIcon />,
|
<SearchIcon />
|
||||||
spellCheck: false,
|
</Button>
|
||||||
endAdornment: <CloseIcon onClick={handleExpandSearch} />,
|
)}
|
||||||
// TODO: reapply
|
</span>
|
||||||
// sx: { minWidth: 200 },
|
</Tooltip>
|
||||||
}}
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
/>
|
<Droppable droppableId="tabs" direction="horizontal">
|
||||||
) : (
|
{(provided, snapshot) => (
|
||||||
<Button onClick={handleExpandSearch}>
|
<Box
|
||||||
<SearchIcon />
|
maxWidth={`${tabsMaxWidth}px`}
|
||||||
</Button>
|
display="flex"
|
||||||
)}
|
flexGrow="1"
|
||||||
</Tooltip>
|
flexDirection="row"
|
||||||
{filteredOpenScripts.map(({ path: fileName, hostname }, index) => {
|
alignItems="center"
|
||||||
const editingCurrentScript =
|
whiteSpace="nowrap"
|
||||||
currentScript?.path === filteredOpenScripts[index].path &&
|
ref={provided.innerRef}
|
||||||
currentScript.hostname === filteredOpenScripts[index].hostname;
|
{...provided.droppableProps}
|
||||||
const externalScript = hostname !== "home";
|
style={{
|
||||||
const colorProps = editingCurrentScript
|
backgroundColor: snapshot.isDraggingOver
|
||||||
? {
|
? Settings.theme.backgroundsecondary
|
||||||
background: Settings.theme.button,
|
: Settings.theme.backgroundprimary,
|
||||||
borderColor: Settings.theme.button,
|
overflowX: "scroll",
|
||||||
color: Settings.theme.primary,
|
}}
|
||||||
}
|
onWheel={handleScroll}
|
||||||
: {
|
>
|
||||||
background: Settings.theme.backgroundsecondary,
|
{filteredScripts.map(({ script, originalIndex }, index) => {
|
||||||
borderColor: Settings.theme.backgroundsecondary,
|
const { path: fileName, hostname } = script;
|
||||||
color: Settings.theme.secondary,
|
const isActive = currentScript?.path === script.path && currentScript.hostname === script.hostname;
|
||||||
};
|
|
||||||
|
|
||||||
if (externalScript) {
|
const title = `${hostname}:~${fileName.startsWith("/") ? "" : "/"}${fileName} ${dirty(scripts, index)}`;
|
||||||
colorProps.color = Settings.theme.info;
|
|
||||||
}
|
|
||||||
const iconButtonStyle = {
|
|
||||||
maxWidth: `${tabIconWidth}px`,
|
|
||||||
minWidth: `${tabIconWidth}px`,
|
|
||||||
minHeight: "38.5px",
|
|
||||||
maxHeight: "38.5px",
|
|
||||||
...colorProps,
|
|
||||||
};
|
|
||||||
|
|
||||||
const scriptTabText = `${hostname}:~${fileName.startsWith("/") ? "" : "/"}${fileName} ${dirty(
|
return (
|
||||||
scripts,
|
<Draggable
|
||||||
index,
|
key={fileName + hostname}
|
||||||
)}`;
|
draggableId={fileName + hostname}
|
||||||
|
index={index}
|
||||||
return (
|
disableInteractiveElementBlocking
|
||||||
<Draggable
|
>
|
||||||
key={fileName + hostname}
|
{(provided) => (
|
||||||
draggableId={fileName + hostname}
|
<Tab
|
||||||
index={index}
|
provided={provided}
|
||||||
disableInteractiveElementBlocking={true}
|
title={title}
|
||||||
>
|
isActive={isActive}
|
||||||
{(provided) => (
|
isExternal={hostname !== "home"}
|
||||||
<div
|
onClick={() => onTabClick(originalIndex)}
|
||||||
ref={provided.innerRef}
|
onClose={() => onTabClose(originalIndex)}
|
||||||
{...provided.draggableProps}
|
onUpdate={() => onTabUpdate(originalIndex)}
|
||||||
{...provided.dragHandleProps}
|
/>
|
||||||
style={{
|
)}
|
||||||
...provided.draggableProps.style,
|
</Draggable>
|
||||||
// maxWidth: `${tabMaxWidth}px`,
|
);
|
||||||
marginRight: `${tabMargin}px`,
|
})}
|
||||||
flexShrink: 0,
|
{provided.placeholder}
|
||||||
border: "1px solid " + Settings.theme.well,
|
</Box>
|
||||||
}}
|
)}
|
||||||
>
|
</Droppable>
|
||||||
<Tooltip title={scriptTabText}>
|
</DragDropContext>
|
||||||
<Button
|
</Box>
|
||||||
onClick={() => onTabClick(index)}
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (e.button === 1) onTabClose(index);
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
maxWidth: `${tabTextWidth}px`,
|
|
||||||
minHeight: "38.5px",
|
|
||||||
overflow: "hidden",
|
|
||||||
...colorProps,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span style={{ overflow: "hidden", direction: "rtl", textOverflow: "ellipsis" }}>
|
|
||||||
{scriptTabText}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="Overwrite editor content with saved file content">
|
|
||||||
<Button onClick={() => onTabUpdate(index)} style={iconButtonStyle}>
|
|
||||||
<SyncIcon fontSize="small" />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Button onClick={() => onTabClose(index)} style={iconButtonStyle}>
|
|
||||||
<CloseIcon fontSize="small" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Draggable>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{provided.placeholder}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Droppable>
|
|
||||||
</DragDropContext>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user