update: allow to edit sheet anems directly from tab buttons
This commit is contained in:
committed by
Nicolás Hatcher Andrés
parent
6b60b339d6
commit
19c115b32f
@@ -1,158 +0,0 @@
|
||||
import { Dialog, styled, TextField } from "@mui/material";
|
||||
import { Check, X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { theme } from "../../theme";
|
||||
|
||||
interface SheetRenameDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onNameChanged: (name: string) => void;
|
||||
defaultName: string;
|
||||
}
|
||||
|
||||
const SheetRenameDialog = (properties: SheetRenameDialogProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [name, setName] = useState(properties.defaultName);
|
||||
const handleClose = () => {
|
||||
properties.onClose();
|
||||
};
|
||||
return (
|
||||
<Dialog open={properties.open} onClose={properties.onClose}>
|
||||
<StyledDialogTitle>
|
||||
{t("sheet_rename.title")}
|
||||
<Cross
|
||||
onClick={handleClose}
|
||||
title={t("sheet_rename.close")}
|
||||
tabIndex={0}
|
||||
onKeyDown={(event) => event.key === "Enter" && properties.onClose()}
|
||||
>
|
||||
<X />
|
||||
</Cross>
|
||||
</StyledDialogTitle>
|
||||
<StyledDialogContent>
|
||||
<StyledTextField
|
||||
autoFocus
|
||||
defaultValue={properties.defaultName}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onKeyDown={(event) => {
|
||||
event.stopPropagation();
|
||||
if (event.key === "Enter") {
|
||||
properties.onNameChanged(name);
|
||||
properties.onClose();
|
||||
} else if (event.key === "Escape") {
|
||||
properties.onClose();
|
||||
}
|
||||
}}
|
||||
onChange={(event) => {
|
||||
setName(event.target.value);
|
||||
}}
|
||||
spellCheck="false"
|
||||
onPaste={(event) => event.stopPropagation()}
|
||||
onCopy={(event) => event.stopPropagation()}
|
||||
onCut={(event) => event.stopPropagation()}
|
||||
/>
|
||||
</StyledDialogContent>
|
||||
<DialogFooter>
|
||||
<StyledButton
|
||||
onClick={() => {
|
||||
properties.onNameChanged(name);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
properties.onNameChanged(name);
|
||||
properties.onClose();
|
||||
}
|
||||
}}
|
||||
tabIndex={0}
|
||||
>
|
||||
<Check
|
||||
style={{ width: "16px", height: "16px", marginRight: "8px" }}
|
||||
/>
|
||||
{t("sheet_rename.rename")}
|
||||
</StyledButton>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledDialogTitle = styled("div")`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 44px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-family: Inter;
|
||||
padding: 0px 12px;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid ${theme.palette.grey["300"]};
|
||||
`;
|
||||
|
||||
const Cross = styled("div")`
|
||||
&:hover {
|
||||
background-color: ${theme.palette.grey["50"]};
|
||||
}
|
||||
display: flex;
|
||||
border-radius: 4px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledDialogContent = styled("div")`
|
||||
font-size: 12px;
|
||||
margin: 12px;
|
||||
`;
|
||||
|
||||
const StyledTextField = styled(TextField)`
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
& .MuiInputBase-input {
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
border: 1px solid ${theme.palette.grey["300"]};
|
||||
border-radius: 4px;
|
||||
color: ${theme.palette.common.black};
|
||||
background-color: ${theme.palette.common.white};
|
||||
}
|
||||
&:hover .MuiInputBase-input {
|
||||
border: 1px solid ${theme.palette.grey["500"]};
|
||||
}
|
||||
`;
|
||||
|
||||
const DialogFooter = styled("div")`
|
||||
color: #757575;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-top: 1px solid ${theme.palette.grey["300"]};
|
||||
font-family: Inter;
|
||||
justify-content: flex-end;
|
||||
padding: 12px;
|
||||
`;
|
||||
|
||||
const StyledButton = styled("div")`
|
||||
cursor: pointer;
|
||||
color: #ffffff;
|
||||
background: #f2994a;
|
||||
padding: 0px 10px;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-family: "Inter";
|
||||
font-size: 14px;
|
||||
&:hover {
|
||||
background: #d68742;
|
||||
}
|
||||
`;
|
||||
|
||||
export default SheetRenameDialog;
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MenuItemProps } from "@mui/material";
|
||||
import { Button, Menu, MenuItem, styled } from "@mui/material";
|
||||
import { Button, Input, Menu, MenuItem, styled } from "@mui/material";
|
||||
import {
|
||||
ChevronDown,
|
||||
EyeOff,
|
||||
@@ -7,14 +7,13 @@ import {
|
||||
TextCursorInput,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { theme } from "../../theme";
|
||||
import ColorPicker from "../ColorPicker/ColorPicker";
|
||||
import { isInReferenceMode } from "../Editor/util";
|
||||
import type { WorkbookState } from "../workbookState";
|
||||
import SheetDeleteDialog from "./SheetDeleteDialog";
|
||||
import SheetRenameDialog from "./SheetRenameDialog";
|
||||
|
||||
interface SheetTabProps {
|
||||
name: string;
|
||||
@@ -48,14 +47,6 @@ function SheetTab(props: SheetTabProps) {
|
||||
onSelected();
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
const [renameDialogOpen, setRenameDialogOpen] = useState(false);
|
||||
const handleCloseRenameDialog = () => {
|
||||
setRenameDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleOpenRenameDialog = () => {
|
||||
setRenameDialogOpen(true);
|
||||
};
|
||||
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
|
||||
@@ -66,19 +57,70 @@ function SheetTab(props: SheetTabProps) {
|
||||
const handleCloseDeleteDialog = () => {
|
||||
setDeleteDialogOpen(false);
|
||||
};
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editingName, setEditingName] = useState(name);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const measureRef = useRef<HTMLSpanElement>(null);
|
||||
const [inputWidth, setInputWidth] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
inputRef.current.select();
|
||||
}
|
||||
}, [isEditing]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditing) {
|
||||
setEditingName(name);
|
||||
}
|
||||
}, [name, isEditing]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (isEditing && measureRef.current) {
|
||||
void editingName;
|
||||
const width = measureRef.current.offsetWidth;
|
||||
setInputWidth(Math.max(width + 8, 6));
|
||||
}
|
||||
}, [editingName, isEditing]);
|
||||
|
||||
const handleStartEditing = () => {
|
||||
setEditingName(name);
|
||||
setInputWidth(Math.max(name.length * 7 + 8, 6));
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (editingName.trim() !== "") {
|
||||
props.onRenamed(editingName.trim());
|
||||
}
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setEditingName(name);
|
||||
setIsEditing(false);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<TabWrapper
|
||||
$color={color}
|
||||
$selected={selected}
|
||||
onClick={(event: React.MouseEvent) => {
|
||||
onSelected();
|
||||
if (!isEditing) {
|
||||
onSelected();
|
||||
}
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}}
|
||||
onDoubleClick={(event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
handleStartEditing();
|
||||
}}
|
||||
onContextMenu={handleContextMenu}
|
||||
onPointerDown={(event: React.PointerEvent) => {
|
||||
// If it is in browse mode stop he event
|
||||
const cell = workbookState.getEditingCell();
|
||||
if (cell && isInReferenceMode(cell.text, cell.cursorStart)) {
|
||||
event.stopPropagation();
|
||||
@@ -87,10 +129,46 @@ function SheetTab(props: SheetTabProps) {
|
||||
}}
|
||||
ref={colorButton}
|
||||
>
|
||||
<Name onDoubleClick={handleOpenRenameDialog}>{name}</Name>
|
||||
<StyledButton onClick={handleOpen} disableRipple={true} $active={open}>
|
||||
<ChevronDown />
|
||||
</StyledButton>
|
||||
{isEditing ? (
|
||||
<>
|
||||
<HiddenMeasure ref={measureRef}>{editingName || " "}</HiddenMeasure>
|
||||
<StyledInput
|
||||
inputRef={inputRef}
|
||||
value={editingName}
|
||||
onChange={(e) => setEditingName(e.target.value)}
|
||||
style={{ width: `${inputWidth}px` }}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
handleCancel();
|
||||
}
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onBlur={() => {
|
||||
handleSave();
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
spellCheck={false}
|
||||
/>
|
||||
<StyledButton disableRipple={true} disabled={true} $active={false}>
|
||||
<ChevronDown />
|
||||
</StyledButton>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Name>{name}</Name>
|
||||
<StyledButton
|
||||
onClick={handleOpen}
|
||||
disableRipple={true}
|
||||
$active={open}
|
||||
>
|
||||
<ChevronDown />
|
||||
</StyledButton>
|
||||
</>
|
||||
)}
|
||||
</TabWrapper>
|
||||
<StyledMenu
|
||||
anchorEl={anchorEl}
|
||||
@@ -107,7 +185,7 @@ function SheetTab(props: SheetTabProps) {
|
||||
>
|
||||
<StyledMenuItem
|
||||
onClick={() => {
|
||||
handleOpenRenameDialog();
|
||||
handleStartEditing();
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
@@ -145,15 +223,6 @@ function SheetTab(props: SheetTabProps) {
|
||||
{t("sheet_tab.delete")}
|
||||
</DeleteButton>
|
||||
</StyledMenu>
|
||||
<SheetRenameDialog
|
||||
open={renameDialogOpen}
|
||||
onClose={handleCloseRenameDialog}
|
||||
defaultName={name}
|
||||
onNameChanged={(newName) => {
|
||||
props.onRenamed(newName);
|
||||
setRenameDialogOpen(false);
|
||||
}}
|
||||
/>
|
||||
<ColorPicker
|
||||
color={color}
|
||||
defaultColor="#FFFFFF"
|
||||
@@ -220,15 +289,19 @@ const TabWrapper = styled("div")<{ $color: string; $selected: boolean }>`
|
||||
margin-right: 12px;
|
||||
border-bottom: 3px solid ${(props) => props.$color};
|
||||
line-height: 37px;
|
||||
padding: 0px 4px;
|
||||
padding: 0px 4px 0px 6px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
min-width: 40px;
|
||||
font-weight: ${(props) => (props.$selected ? 600 : 400)};
|
||||
background-color: ${(props) =>
|
||||
props.$selected ? `${theme.palette.grey[50]}80` : "transparent"};
|
||||
props.$selected ? `${theme.palette.grey[50]}` : "transparent"};
|
||||
&:hover {
|
||||
background-color: ${theme.palette.grey[50]}80;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledButton = styled(Button)<{ $active?: boolean }>`
|
||||
const StyledButton = styled(Button)<{ $active: boolean }>`
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
min-width: 0px;
|
||||
@@ -236,6 +309,7 @@ const StyledButton = styled(Button)<{ $active?: boolean }>`
|
||||
color: inherit;
|
||||
font-weight: inherit;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
background-color: ${(props) =>
|
||||
props.$active ? `${theme.palette.grey[300]}` : "transparent"};
|
||||
&:hover {
|
||||
@@ -244,6 +318,9 @@ const StyledButton = styled(Button)<{ $active?: boolean }>`
|
||||
&:active {
|
||||
background-color: ${theme.palette.grey[300]};
|
||||
}
|
||||
&:disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
@@ -255,6 +332,51 @@ const Name = styled("div")`
|
||||
margin-right: 5px;
|
||||
text-wrap: nowrap;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const HiddenMeasure = styled("span")`
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
white-space: pre;
|
||||
font-size: 12px;
|
||||
font-family: Inter;
|
||||
font-weight: inherit;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
const StyledInput = styled(Input)`
|
||||
font-size: 12px;
|
||||
font-family: Inter;
|
||||
font-weight: inherit;
|
||||
min-width: 6px;
|
||||
margin-right: 2px;
|
||||
outline-offset: 1px;
|
||||
min-height: 100%;
|
||||
flex-grow: 1;
|
||||
& .MuiInputBase-input {
|
||||
font-family: Inter;
|
||||
font-weight: inherit;
|
||||
padding: 6px 0px;
|
||||
outline: 1px solid ${theme.palette.primary.main};
|
||||
border-radius: 2px;
|
||||
color: ${theme.palette.common.black};
|
||||
text-align: center;
|
||||
will-change: width;
|
||||
&:focus {
|
||||
border-color: ${theme.palette.primary.main};
|
||||
}
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const MenuDivider = styled("div")`
|
||||
|
||||
@@ -137,6 +137,7 @@ const Sheets = styled("div")`
|
||||
padding-left: 12px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const SheetInner = styled("div")`
|
||||
|
||||
Reference in New Issue
Block a user