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 type { MenuItemProps } from "@mui/material";
|
||||||
import { Button, Menu, MenuItem, styled } from "@mui/material";
|
import { Button, Input, Menu, MenuItem, styled } from "@mui/material";
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
@@ -7,14 +7,13 @@ import {
|
|||||||
TextCursorInput,
|
TextCursorInput,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useRef, useState } from "react";
|
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { theme } from "../../theme";
|
import { theme } from "../../theme";
|
||||||
import ColorPicker from "../ColorPicker/ColorPicker";
|
import ColorPicker from "../ColorPicker/ColorPicker";
|
||||||
import { isInReferenceMode } from "../Editor/util";
|
import { isInReferenceMode } from "../Editor/util";
|
||||||
import type { WorkbookState } from "../workbookState";
|
import type { WorkbookState } from "../workbookState";
|
||||||
import SheetDeleteDialog from "./SheetDeleteDialog";
|
import SheetDeleteDialog from "./SheetDeleteDialog";
|
||||||
import SheetRenameDialog from "./SheetRenameDialog";
|
|
||||||
|
|
||||||
interface SheetTabProps {
|
interface SheetTabProps {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -48,14 +47,6 @@ function SheetTab(props: SheetTabProps) {
|
|||||||
onSelected();
|
onSelected();
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
};
|
};
|
||||||
const [renameDialogOpen, setRenameDialogOpen] = useState(false);
|
|
||||||
const handleCloseRenameDialog = () => {
|
|
||||||
setRenameDialogOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOpenRenameDialog = () => {
|
|
||||||
setRenameDialogOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
|
|
||||||
@@ -66,19 +57,70 @@ function SheetTab(props: SheetTabProps) {
|
|||||||
const handleCloseDeleteDialog = () => {
|
const handleCloseDeleteDialog = () => {
|
||||||
setDeleteDialogOpen(false);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<TabWrapper
|
<TabWrapper
|
||||||
$color={color}
|
$color={color}
|
||||||
$selected={selected}
|
$selected={selected}
|
||||||
onClick={(event: React.MouseEvent) => {
|
onClick={(event: React.MouseEvent) => {
|
||||||
onSelected();
|
if (!isEditing) {
|
||||||
|
onSelected();
|
||||||
|
}
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}}
|
}}
|
||||||
|
onDoubleClick={(event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
handleStartEditing();
|
||||||
|
}}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
onPointerDown={(event: React.PointerEvent) => {
|
onPointerDown={(event: React.PointerEvent) => {
|
||||||
// If it is in browse mode stop he event
|
|
||||||
const cell = workbookState.getEditingCell();
|
const cell = workbookState.getEditingCell();
|
||||||
if (cell && isInReferenceMode(cell.text, cell.cursorStart)) {
|
if (cell && isInReferenceMode(cell.text, cell.cursorStart)) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -87,10 +129,46 @@ function SheetTab(props: SheetTabProps) {
|
|||||||
}}
|
}}
|
||||||
ref={colorButton}
|
ref={colorButton}
|
||||||
>
|
>
|
||||||
<Name onDoubleClick={handleOpenRenameDialog}>{name}</Name>
|
{isEditing ? (
|
||||||
<StyledButton onClick={handleOpen} disableRipple={true} $active={open}>
|
<>
|
||||||
<ChevronDown />
|
<HiddenMeasure ref={measureRef}>{editingName || " "}</HiddenMeasure>
|
||||||
</StyledButton>
|
<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>
|
</TabWrapper>
|
||||||
<StyledMenu
|
<StyledMenu
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
@@ -107,7 +185,7 @@ function SheetTab(props: SheetTabProps) {
|
|||||||
>
|
>
|
||||||
<StyledMenuItem
|
<StyledMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleOpenRenameDialog();
|
handleStartEditing();
|
||||||
handleClose();
|
handleClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -145,15 +223,6 @@ function SheetTab(props: SheetTabProps) {
|
|||||||
{t("sheet_tab.delete")}
|
{t("sheet_tab.delete")}
|
||||||
</DeleteButton>
|
</DeleteButton>
|
||||||
</StyledMenu>
|
</StyledMenu>
|
||||||
<SheetRenameDialog
|
|
||||||
open={renameDialogOpen}
|
|
||||||
onClose={handleCloseRenameDialog}
|
|
||||||
defaultName={name}
|
|
||||||
onNameChanged={(newName) => {
|
|
||||||
props.onRenamed(newName);
|
|
||||||
setRenameDialogOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
color={color}
|
color={color}
|
||||||
defaultColor="#FFFFFF"
|
defaultColor="#FFFFFF"
|
||||||
@@ -220,15 +289,19 @@ const TabWrapper = styled("div")<{ $color: string; $selected: boolean }>`
|
|||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
border-bottom: 3px solid ${(props) => props.$color};
|
border-bottom: 3px solid ${(props) => props.$color};
|
||||||
line-height: 37px;
|
line-height: 37px;
|
||||||
padding: 0px 4px;
|
padding: 0px 4px 0px 6px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
min-width: 40px;
|
||||||
font-weight: ${(props) => (props.$selected ? 600 : 400)};
|
font-weight: ${(props) => (props.$selected ? 600 : 400)};
|
||||||
background-color: ${(props) =>
|
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;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
min-width: 0px;
|
min-width: 0px;
|
||||||
@@ -236,6 +309,7 @@ const StyledButton = styled(Button)<{ $active?: boolean }>`
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
font-weight: inherit;
|
font-weight: inherit;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
background-color: ${(props) =>
|
background-color: ${(props) =>
|
||||||
props.$active ? `${theme.palette.grey[300]}` : "transparent"};
|
props.$active ? `${theme.palette.grey[300]}` : "transparent"};
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -244,6 +318,9 @@ const StyledButton = styled(Button)<{ $active?: boolean }>`
|
|||||||
&:active {
|
&:active {
|
||||||
background-color: ${theme.palette.grey[300]};
|
background-color: ${theme.palette.grey[300]};
|
||||||
}
|
}
|
||||||
|
&:disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
svg {
|
svg {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
@@ -255,6 +332,51 @@ const Name = styled("div")`
|
|||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
text-wrap: nowrap;
|
text-wrap: nowrap;
|
||||||
user-select: none;
|
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")`
|
const MenuDivider = styled("div")`
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ const Sheets = styled("div")`
|
|||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
height: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SheetInner = styled("div")`
|
const SheetInner = styled("div")`
|
||||||
|
|||||||
Reference in New Issue
Block a user