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]);
|
||||
|
||||
// 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 (
|
||||
<div className={className} dir={dir} onClick={onClick}>
|
||||
<Content
|
||||
@@ -89,6 +100,7 @@ const ContentEditable = React.forwardRef(
|
||||
onInput={wrappedEvent(onInput)}
|
||||
onBlur={wrappedEvent(onBlur)}
|
||||
onKeyDown={wrappedEvent(onKeyDown)}
|
||||
onPaste={handlePaste}
|
||||
data-placeholder={placeholder}
|
||||
suppressContentEditableWarning
|
||||
role="textbox"
|
||||
@@ -103,6 +115,14 @@ const ContentEditable = React.forwardRef(
|
||||
);
|
||||
|
||||
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 {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@@ -5,14 +5,6 @@ const Heading = styled.h1<{ centered?: boolean }>`
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
${(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;
|
||||
|
||||
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,
|
||||
useRouteMatch,
|
||||
} from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import Collection from "~/models/Collection";
|
||||
import Search from "~/scenes/Search";
|
||||
import Badge from "~/components/Badge";
|
||||
@@ -107,8 +109,7 @@ function CollectionScene() {
|
||||
title={
|
||||
<>
|
||||
<CollectionIcon collection={collection} expanded />
|
||||
|
||||
{collection.name}
|
||||
{collection.name}
|
||||
</>
|
||||
}
|
||||
actions={<Actions collection={collection} />}
|
||||
@@ -123,9 +124,9 @@ function CollectionScene() {
|
||||
<Empty collection={collection} />
|
||||
) : (
|
||||
<>
|
||||
<Heading>
|
||||
<CollectionIcon collection={collection} size={40} expanded />{" "}
|
||||
{collection.name}{" "}
|
||||
<HeadingWithIcon>
|
||||
<HeadingIcon collection={collection} size={40} expanded />
|
||||
{collection.name}
|
||||
{!collection.permission && (
|
||||
<Tooltip
|
||||
tooltip={t(
|
||||
@@ -136,7 +137,7 @@ function CollectionScene() {
|
||||
<Badge>{t("Private")}</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Heading>
|
||||
</HeadingWithIcon>
|
||||
<CollectionDescription collection={collection} />
|
||||
|
||||
<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);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { light } from "@shared/theme";
|
||||
import Document from "~/models/Document";
|
||||
import ContentEditable from "~/components/ContentEditable";
|
||||
import Star, { AnimatedStar } from "~/components/Star";
|
||||
import useEmojiWidth from "~/hooks/useEmojiWidth";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import { isModKey } from "~/utils/keyboard";
|
||||
|
||||
@@ -51,17 +52,6 @@ const EditableTitle = React.forwardRef(
|
||||
ref.current?.focus();
|
||||
}, [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(
|
||||
(event: React.KeyboardEvent) => {
|
||||
if (event.key === "Enter") {
|
||||
@@ -102,34 +92,16 @@ const EditableTitle = React.forwardRef(
|
||||
[onGoToNextInput, onSave]
|
||||
);
|
||||
|
||||
/**
|
||||
* Measures the width of the document's emoji in the title
|
||||
*/
|
||||
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]);
|
||||
const emojiWidth = useEmojiWidth(document.emoji, {
|
||||
fontSize,
|
||||
lineHeight,
|
||||
});
|
||||
|
||||
return (
|
||||
<Title
|
||||
onClick={handleClick}
|
||||
onChange={onChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={handlePaste}
|
||||
placeholder={placeholder}
|
||||
value={normalizedTitle}
|
||||
$emojiWidth={emojiWidth}
|
||||
@@ -170,17 +142,10 @@ const Title = styled(ContentEditable)<TitleProps>`
|
||||
line-height: ${lineHeight};
|
||||
margin-top: 1em;
|
||||
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-weight: 500;
|
||||
outline: none;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
resize: none;
|
||||
cursor: text;
|
||||
|
||||
> span {
|
||||
outline: none;
|
||||
|
||||
Reference in New Issue
Block a user