FIX[app.ironcalc.com]: Clean up code for the title
This commit is contained in:
committed by
Nicolás Hatcher Andrés
parent
cde6f0e49f
commit
e07fdd2091
@@ -1,7 +1,7 @@
|
|||||||
import styled from "@emotion/styled";
|
import styled from "@emotion/styled";
|
||||||
import type { Model } from "@ironcalc/workbook";
|
import type { Model } from "@ironcalc/workbook";
|
||||||
import { IronCalcIcon, IronCalcLogo } from "@ironcalc/workbook";
|
import { IronCalcIcon, IronCalcLogo } from "@ironcalc/workbook";
|
||||||
import { useRef, useState } from "react";
|
import { useLayoutEffect, useRef, useState } from "react";
|
||||||
import { FileMenu } from "./FileMenu";
|
import { FileMenu } from "./FileMenu";
|
||||||
import { ShareButton } from "./ShareButton";
|
import { ShareButton } from "./ShareButton";
|
||||||
import ShareWorkbookDialog from "./ShareWorkbookDialog";
|
import ShareWorkbookDialog from "./ShareWorkbookDialog";
|
||||||
@@ -9,6 +9,20 @@ import { WorkbookTitle } from "./WorkbookTitle";
|
|||||||
import { downloadModel } from "./rpc";
|
import { downloadModel } from "./rpc";
|
||||||
import { updateNameSelectedWorkbook } from "./storage";
|
import { updateNameSelectedWorkbook } from "./storage";
|
||||||
|
|
||||||
|
// This hook is used to get the width of the window
|
||||||
|
function useWindowWidth() {
|
||||||
|
const [width, setWidth] = useState(0);
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
function updateWidth() {
|
||||||
|
setWidth(window.innerWidth);
|
||||||
|
}
|
||||||
|
window.addEventListener("resize", updateWidth);
|
||||||
|
updateWidth();
|
||||||
|
return () => window.removeEventListener("resize", updateWidth);
|
||||||
|
}, []);
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
export function FileBar(properties: {
|
export function FileBar(properties: {
|
||||||
model: Model;
|
model: Model;
|
||||||
newModel: () => void;
|
newModel: () => void;
|
||||||
@@ -16,8 +30,19 @@ export function FileBar(properties: {
|
|||||||
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
|
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
}) {
|
}) {
|
||||||
const hiddenInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
const spacerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [maxTitleWidth, setMaxTitleWidth] = useState(0);
|
||||||
|
const width = useWindowWidth();
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: We need to update the maxTitleWidth when the width changes
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const el = spacerRef.current;
|
||||||
|
if (el) {
|
||||||
|
const bb = el.getBoundingClientRect();
|
||||||
|
setMaxTitleWidth(bb.right - bb.left - 50);
|
||||||
|
}
|
||||||
|
}, [width]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FileBarWrapper>
|
<FileBarWrapper>
|
||||||
@@ -41,19 +66,17 @@ export function FileBar(properties: {
|
|||||||
>
|
>
|
||||||
Help
|
Help
|
||||||
</HelpButton>
|
</HelpButton>
|
||||||
<WorkbookTitle
|
<WorkbookTitleWrapper>
|
||||||
name={properties.model.getName()}
|
<WorkbookTitle
|
||||||
onNameChange={(name) => {
|
name={properties.model.getName()}
|
||||||
properties.model.setName(name);
|
onNameChange={(name) => {
|
||||||
updateNameSelectedWorkbook(properties.model, name);
|
properties.model.setName(name);
|
||||||
}}
|
updateNameSelectedWorkbook(properties.model, name);
|
||||||
/>
|
}}
|
||||||
<input
|
maxWidth={maxTitleWidth}
|
||||||
ref={hiddenInputRef}
|
/>
|
||||||
type="text"
|
</WorkbookTitleWrapper>
|
||||||
style={{ position: "absolute", left: -9999, top: -9999 }}
|
<Spacer ref={spacerRef} />
|
||||||
/>
|
|
||||||
<div style={{ marginLeft: "auto" }} />
|
|
||||||
<DialogContainer>
|
<DialogContainer>
|
||||||
<ShareButton onClick={() => setIsDialogOpen(true)} />
|
<ShareButton onClick={() => setIsDialogOpen(true)} />
|
||||||
{isDialogOpen && (
|
{isDialogOpen && (
|
||||||
@@ -68,6 +91,19 @@ export function FileBar(properties: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We want the workbook title to be exactly an the center of the page,
|
||||||
|
// so we need an absolute position
|
||||||
|
const WorkbookTitleWrapper = styled("div")`
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
`;
|
||||||
|
|
||||||
|
// The "Spacer" component occupies as much space as possible between the menu and the share button
|
||||||
|
const Spacer = styled("div")`
|
||||||
|
flex-grow: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
const StyledDesktopLogo = styled(IronCalcLogo)`
|
const StyledDesktopLogo = styled(IronCalcLogo)`
|
||||||
width: 120px;
|
width: 120px;
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
@@ -103,14 +139,15 @@ const Divider = styled("div")`
|
|||||||
border-left: 1px solid #e0e0e0;
|
border-left: 1px solid #e0e0e0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// The container must be relative positioned so we can position the title absolutely
|
||||||
const FileBarWrapper = styled("div")`
|
const FileBarWrapper = styled("div")`
|
||||||
|
position: relative;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-bottom: 1px solid #e0e0e0;
|
border-bottom: 1px solid #e0e0e0;
|
||||||
position: relative;
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -1,89 +1,105 @@
|
|||||||
import styled from "@emotion/styled";
|
import styled from "@emotion/styled";
|
||||||
import { type ChangeEvent, useEffect, useRef, useState } from "react";
|
import {
|
||||||
|
type ChangeEvent,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
export function WorkbookTitle(props: {
|
// This element has a in situ editable text
|
||||||
|
// We use a virtual element to compute the size of the input
|
||||||
|
|
||||||
|
export function WorkbookTitle(properties: {
|
||||||
name: string;
|
name: string;
|
||||||
onNameChange: (name: string) => void;
|
onNameChange: (name: string) => void;
|
||||||
|
maxWidth: number;
|
||||||
}) {
|
}) {
|
||||||
const [width, setWidth] = useState(0);
|
const [width, setWidth] = useState(0);
|
||||||
const [value, setValue] = useState(props.name);
|
const [name, setName] = useState(properties.name);
|
||||||
const mirrorDivRef = useRef<HTMLDivElement>(null);
|
const mirrorDivRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
setValue(event.target.value);
|
setName(event.target.value);
|
||||||
if (mirrorDivRef.current) {
|
if (mirrorDivRef.current) {
|
||||||
setWidth(mirrorDivRef.current.scrollWidth);
|
setWidth(mirrorDivRef.current.scrollWidth);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setName(properties.name);
|
||||||
|
}, [properties.name]);
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: We need to change the width with every value change
|
||||||
|
useLayoutEffect(() => {
|
||||||
if (mirrorDivRef.current) {
|
if (mirrorDivRef.current) {
|
||||||
setWidth(mirrorDivRef.current.scrollWidth);
|
setWidth(mirrorDivRef.current.scrollWidth);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [name]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setValue(props.name);
|
|
||||||
}, [props.name]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Container
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
width: Math.min(width, properties.maxWidth),
|
||||||
left: "50%",
|
|
||||||
textAlign: "center",
|
|
||||||
transform: "translateX(-50%)",
|
|
||||||
// height: "60px",
|
|
||||||
// lineHeight: "60px",
|
|
||||||
padding: "8px",
|
|
||||||
fontSize: "14px",
|
|
||||||
fontWeight: "700",
|
|
||||||
fontFamily: "Inter",
|
|
||||||
width,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TitleWrapper
|
<TitleInput
|
||||||
value={value}
|
value={name}
|
||||||
rows={1}
|
onInput={handleChange}
|
||||||
onChange={handleChange}
|
|
||||||
onBlur={(event) => {
|
onBlur={(event) => {
|
||||||
props.onNameChange(event.target.value);
|
properties.onNameChange(event.target.value);
|
||||||
}}
|
}}
|
||||||
style={{ width: width }}
|
onKeyDown={(event) => {
|
||||||
|
switch (event.key) {
|
||||||
|
case "Enter": {
|
||||||
|
// If we hit "Enter" finish editing
|
||||||
|
event.currentTarget.blur();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "Escape": {
|
||||||
|
// revert changes
|
||||||
|
setName(properties.name);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{ width: Math.min(width, properties.maxWidth) }}
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
>
|
/>
|
||||||
{value}
|
<MirrorDiv ref={mirrorDivRef}>{name}</MirrorDiv>
|
||||||
</TitleWrapper>
|
</Container>
|
||||||
<div
|
|
||||||
ref={mirrorDivRef}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: "-9999px",
|
|
||||||
left: "-9999px",
|
|
||||||
whiteSpace: "pre-wrap",
|
|
||||||
textWrap: "nowrap",
|
|
||||||
visibility: "hidden",
|
|
||||||
fontFamily: "inherit",
|
|
||||||
fontSize: "inherit",
|
|
||||||
lineHeight: "inherit",
|
|
||||||
padding: "inherit",
|
|
||||||
border: "inherit",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{value}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const TitleWrapper = styled("textarea")`
|
const Container = styled("div")`
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: Inter;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MirrorDiv = styled("div")`
|
||||||
|
position: absolute;
|
||||||
|
top: -9999px;
|
||||||
|
left: -9999px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
visibility: hidden;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
padding: inherit;
|
||||||
|
border: inherit;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TitleInput = styled("input")`
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: inherit;
|
padding: inherit;
|
||||||
overflow: hidden;
|
|
||||||
outline: none;
|
outline: none;
|
||||||
resize: none;
|
resize: none;
|
||||||
text-wrap: nowrap;
|
text-wrap: nowrap;
|
||||||
@@ -97,8 +113,6 @@ const TitleWrapper = styled("textarea")`
|
|||||||
font-weight: inherit;
|
font-weight: inherit;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
max-width: 520px;
|
overflow: ellipsis;
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
Reference in New Issue
Block a user