fix: Inset icon in collection headers, minor ContentEditable refactor (#3168)
This commit is contained in:
@@ -81,6 +81,17 @@ const ContentEditable = React.forwardRef(
|
|||||||
}
|
}
|
||||||
}, [value, ref]);
|
}, [value, ref]);
|
||||||
|
|
||||||
|
// Ensure only plain text can be pasted into title when pasting from another
|
||||||
|
// rich text editor
|
||||||
|
const handlePaste = React.useCallback(
|
||||||
|
(event: React.ClipboardEvent<HTMLSpanElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const text = event.clipboardData.getData("text/plain");
|
||||||
|
window.document.execCommand("insertText", false, text);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className} dir={dir} onClick={onClick}>
|
<div className={className} dir={dir} onClick={onClick}>
|
||||||
<Content
|
<Content
|
||||||
@@ -89,6 +100,7 @@ const ContentEditable = React.forwardRef(
|
|||||||
onInput={wrappedEvent(onInput)}
|
onInput={wrappedEvent(onInput)}
|
||||||
onBlur={wrappedEvent(onBlur)}
|
onBlur={wrappedEvent(onBlur)}
|
||||||
onKeyDown={wrappedEvent(onKeyDown)}
|
onKeyDown={wrappedEvent(onKeyDown)}
|
||||||
|
onPaste={handlePaste}
|
||||||
data-placeholder={placeholder}
|
data-placeholder={placeholder}
|
||||||
suppressContentEditableWarning
|
suppressContentEditableWarning
|
||||||
role="textbox"
|
role="textbox"
|
||||||
@@ -103,6 +115,14 @@ const ContentEditable = React.forwardRef(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const Content = styled.span`
|
const Content = styled.span`
|
||||||
|
background: ${(props) => props.theme.background};
|
||||||
|
transition: ${(props) => props.theme.backgroundTransition};
|
||||||
|
color: ${(props) => props.theme.text};
|
||||||
|
-webkit-text-fill-color: ${(props) => props.theme.text};
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
cursor: text;
|
||||||
|
|
||||||
&:empty {
|
&:empty {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,6 @@ const Heading = styled.h1<{ centered?: boolean }>`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
${(props) => (props.centered ? "text-align: center;" : "")}
|
${(props) => (props.centered ? "text-align: center;" : "")}
|
||||||
|
|
||||||
svg {
|
|
||||||
margin-top: 4px;
|
|
||||||
margin-left: -6px;
|
|
||||||
margin-right: 2px;
|
|
||||||
align-self: flex-start;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default Heading;
|
export default Heading;
|
||||||
|
|||||||
33
app/hooks/useEmojiWidth.ts
Normal file
33
app/hooks/useEmojiWidth.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
type Options = {
|
||||||
|
fontSize?: string;
|
||||||
|
lineHeight?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures the width of an emoji character
|
||||||
|
*/
|
||||||
|
export default function useEmojiWidth(
|
||||||
|
emoji: string | undefined,
|
||||||
|
{ fontSize = "2.25em", lineHeight = "1.25" }: Options
|
||||||
|
) {
|
||||||
|
return React.useMemo(() => {
|
||||||
|
const element = window.document.createElement("span");
|
||||||
|
if (!emoji) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
element.innerText = `${emoji}\u00A0`;
|
||||||
|
element.style.visibility = "hidden";
|
||||||
|
element.style.position = "absolute";
|
||||||
|
element.style.left = "-9999px";
|
||||||
|
element.style.lineHeight = lineHeight;
|
||||||
|
element.style.fontSize = fontSize;
|
||||||
|
element.style.width = "max-content";
|
||||||
|
window.document.body?.appendChild(element);
|
||||||
|
const width = window.getComputedStyle(element).width;
|
||||||
|
window.document.body?.removeChild(element);
|
||||||
|
return parseInt(width, 10);
|
||||||
|
}, [emoji, fontSize, lineHeight]);
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
useHistory,
|
useHistory,
|
||||||
useRouteMatch,
|
useRouteMatch,
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import breakpoint from "styled-components-breakpoint";
|
||||||
import Collection from "~/models/Collection";
|
import Collection from "~/models/Collection";
|
||||||
import Search from "~/scenes/Search";
|
import Search from "~/scenes/Search";
|
||||||
import Badge from "~/components/Badge";
|
import Badge from "~/components/Badge";
|
||||||
@@ -107,8 +109,7 @@ function CollectionScene() {
|
|||||||
title={
|
title={
|
||||||
<>
|
<>
|
||||||
<CollectionIcon collection={collection} expanded />
|
<CollectionIcon collection={collection} expanded />
|
||||||
|
{collection.name}
|
||||||
{collection.name}
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
actions={<Actions collection={collection} />}
|
actions={<Actions collection={collection} />}
|
||||||
@@ -123,9 +124,9 @@ function CollectionScene() {
|
|||||||
<Empty collection={collection} />
|
<Empty collection={collection} />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Heading>
|
<HeadingWithIcon>
|
||||||
<CollectionIcon collection={collection} size={40} expanded />{" "}
|
<HeadingIcon collection={collection} size={40} expanded />
|
||||||
{collection.name}{" "}
|
{collection.name}
|
||||||
{!collection.permission && (
|
{!collection.permission && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
tooltip={t(
|
tooltip={t(
|
||||||
@@ -136,7 +137,7 @@ function CollectionScene() {
|
|||||||
<Badge>{t("Private")}</Badge>
|
<Badge>{t("Private")}</Badge>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</Heading>
|
</HeadingWithIcon>
|
||||||
<CollectionDescription collection={collection} />
|
<CollectionDescription collection={collection} />
|
||||||
|
|
||||||
<PinnedDocuments
|
<PinnedDocuments
|
||||||
@@ -243,4 +244,18 @@ function CollectionScene() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HeadingWithIcon = styled(Heading)`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
${breakpoint("tablet")`
|
||||||
|
margin-left: -40px;
|
||||||
|
`};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const HeadingIcon = styled(CollectionIcon)`
|
||||||
|
align-self: flex-start;
|
||||||
|
flex-shrink: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
export default observer(CollectionScene);
|
export default observer(CollectionScene);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { light } from "@shared/theme";
|
|||||||
import Document from "~/models/Document";
|
import Document from "~/models/Document";
|
||||||
import ContentEditable from "~/components/ContentEditable";
|
import ContentEditable from "~/components/ContentEditable";
|
||||||
import Star, { AnimatedStar } from "~/components/Star";
|
import Star, { AnimatedStar } from "~/components/Star";
|
||||||
|
import useEmojiWidth from "~/hooks/useEmojiWidth";
|
||||||
import usePolicy from "~/hooks/usePolicy";
|
import usePolicy from "~/hooks/usePolicy";
|
||||||
import { isModKey } from "~/utils/keyboard";
|
import { isModKey } from "~/utils/keyboard";
|
||||||
|
|
||||||
@@ -51,17 +52,6 @@ const EditableTitle = React.forwardRef(
|
|||||||
ref.current?.focus();
|
ref.current?.focus();
|
||||||
}, [ref]);
|
}, [ref]);
|
||||||
|
|
||||||
// Ensure only plain text can be pasted into title when pasting from another
|
|
||||||
// rich text editor
|
|
||||||
const handlePaste = React.useCallback(
|
|
||||||
(event: React.ClipboardEvent<HTMLSpanElement>) => {
|
|
||||||
event.preventDefault();
|
|
||||||
const text = event.clipboardData.getData("text/plain");
|
|
||||||
window.document.execCommand("insertText", false, text);
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleKeyDown = React.useCallback(
|
const handleKeyDown = React.useCallback(
|
||||||
(event: React.KeyboardEvent) => {
|
(event: React.KeyboardEvent) => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
@@ -102,34 +92,16 @@ const EditableTitle = React.forwardRef(
|
|||||||
[onGoToNextInput, onSave]
|
[onGoToNextInput, onSave]
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
const emojiWidth = useEmojiWidth(document.emoji, {
|
||||||
* Measures the width of the document's emoji in the title
|
fontSize,
|
||||||
*/
|
lineHeight,
|
||||||
const emojiWidth = React.useMemo(() => {
|
});
|
||||||
const element = window.document.createElement("span");
|
|
||||||
if (!document.emoji) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
element.innerText = `${document.emoji}\u00A0`;
|
|
||||||
element.style.visibility = "hidden";
|
|
||||||
element.style.position = "absolute";
|
|
||||||
element.style.left = "-9999px";
|
|
||||||
element.style.lineHeight = lineHeight;
|
|
||||||
element.style.fontSize = fontSize;
|
|
||||||
element.style.width = "max-content";
|
|
||||||
window.document.body?.appendChild(element);
|
|
||||||
const width = window.getComputedStyle(element).width;
|
|
||||||
window.document.body?.removeChild(element);
|
|
||||||
return parseInt(width, 10);
|
|
||||||
}, [document.emoji]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Title
|
<Title
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onPaste={handlePaste}
|
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={normalizedTitle}
|
value={normalizedTitle}
|
||||||
$emojiWidth={emojiWidth}
|
$emojiWidth={emojiWidth}
|
||||||
@@ -170,17 +142,10 @@ const Title = styled(ContentEditable)<TitleProps>`
|
|||||||
line-height: ${lineHeight};
|
line-height: ${lineHeight};
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
background: ${(props) => props.theme.background};
|
|
||||||
transition: ${(props) => props.theme.backgroundTransition};
|
|
||||||
color: ${(props) => props.theme.text};
|
|
||||||
-webkit-text-fill-color: ${(props) => props.theme.text};
|
|
||||||
font-size: ${fontSize};
|
font-size: ${fontSize};
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
outline: none;
|
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
resize: none;
|
|
||||||
cursor: text;
|
|
||||||
|
|
||||||
> span {
|
> span {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user