Document emoji picker (#4338)
Co-authored-by: Tom Moor <tom.moor@gmail.com>
This commit is contained in:
@@ -9,6 +9,7 @@ type Props = Omit<React.HTMLAttributes<HTMLSpanElement>, "ref" | "onChange"> & {
|
||||
readOnly?: boolean;
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||
onChange?: (text: string) => void;
|
||||
onFocus?: React.FocusEventHandler<HTMLSpanElement> | undefined;
|
||||
onBlur?: React.FocusEventHandler<HTMLSpanElement> | undefined;
|
||||
onInput?: React.FormEventHandler<HTMLSpanElement> | undefined;
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLSpanElement> | undefined;
|
||||
@@ -35,6 +36,7 @@ const ContentEditable = React.forwardRef(function _ContentEditable(
|
||||
disabled,
|
||||
onChange,
|
||||
onInput,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onKeyDown,
|
||||
value,
|
||||
@@ -143,11 +145,13 @@ const ContentEditable = React.forwardRef(function _ContentEditable(
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={className} dir={dir} onClick={onClick}>
|
||||
<div className={className} dir={dir} onClick={onClick} tabIndex={-1}>
|
||||
{children}
|
||||
<Content
|
||||
ref={contentRef}
|
||||
contentEditable={!disabled && !readOnly}
|
||||
onInput={wrappedEvent(onInput)}
|
||||
onFocus={wrappedEvent(onFocus)}
|
||||
onBlur={wrappedEvent(onBlur)}
|
||||
onKeyDown={wrappedEvent(onKeyDown)}
|
||||
onPaste={handlePaste}
|
||||
@@ -158,7 +162,6 @@ const ContentEditable = React.forwardRef(function _ContentEditable(
|
||||
>
|
||||
{innerValue}
|
||||
</Content>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
templatesPath,
|
||||
trashPath,
|
||||
} from "~/utils/routeHelpers";
|
||||
import EmojiIcon from "./Icons/EmojiIcon";
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
@@ -105,7 +106,13 @@ const DocumentBreadcrumb: React.FC<Props> = ({
|
||||
path.forEach((node: NavigationNode) => {
|
||||
output.push({
|
||||
type: "route",
|
||||
title: node.title,
|
||||
title: node.emoji ? (
|
||||
<>
|
||||
<EmojiIcon emoji={node.emoji} /> {node.title}
|
||||
</>
|
||||
) : (
|
||||
node.title
|
||||
),
|
||||
to: node.url,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -111,7 +111,7 @@ function DocumentCard(props: Props) {
|
||||
|
||||
{document.emoji ? (
|
||||
<Squircle color={theme.slateLight}>
|
||||
<EmojiIcon emoji={document.emoji} size={26} />
|
||||
<EmojiIcon emoji={document.emoji} size={24} />
|
||||
</Squircle>
|
||||
) : (
|
||||
<Squircle color={collection?.color}>
|
||||
|
||||
@@ -15,7 +15,6 @@ import scrollIntoView from "smooth-scroll-into-view-if-needed";
|
||||
import styled, { useTheme } from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import { NavigationNode } from "@shared/types";
|
||||
import parseTitle from "@shared/utils/parseTitle";
|
||||
import DocumentExplorerNode from "~/components/DocumentExplorerNode";
|
||||
import DocumentExplorerSearchResult from "~/components/DocumentExplorerSearchResult";
|
||||
import Flex from "~/components/Flex";
|
||||
@@ -205,84 +204,86 @@ function DocumentExplorer({ onSubmit, onSelect, items }: Props) {
|
||||
}
|
||||
};
|
||||
|
||||
const ListItem = ({
|
||||
index,
|
||||
data,
|
||||
style,
|
||||
}: {
|
||||
index: number;
|
||||
data: NavigationNode[];
|
||||
style: React.CSSProperties;
|
||||
}) => {
|
||||
const node = data[index];
|
||||
const isCollection = node.type === "collection";
|
||||
let icon, title, path;
|
||||
const ListItem = observer(
|
||||
({
|
||||
index,
|
||||
data,
|
||||
style,
|
||||
}: {
|
||||
index: number;
|
||||
data: NavigationNode[];
|
||||
style: React.CSSProperties;
|
||||
}) => {
|
||||
const node = data[index];
|
||||
const isCollection = node.type === "collection";
|
||||
let icon, title: string, emoji: string | undefined, path;
|
||||
|
||||
if (isCollection) {
|
||||
const col = collections.get(node.collectionId as string);
|
||||
icon = col && (
|
||||
<CollectionIcon collection={col} expanded={isExpanded(index)} />
|
||||
);
|
||||
title = node.title;
|
||||
} else {
|
||||
const doc = documents.get(node.id);
|
||||
const { strippedTitle, emoji } = parseTitle(node.title);
|
||||
title = strippedTitle;
|
||||
|
||||
if (emoji) {
|
||||
icon = <EmojiIcon emoji={emoji} />;
|
||||
} else if (doc?.isStarred) {
|
||||
icon = <StarredIcon color={theme.yellow} />;
|
||||
if (isCollection) {
|
||||
const col = collections.get(node.collectionId as string);
|
||||
icon = col && (
|
||||
<CollectionIcon collection={col} expanded={isExpanded(index)} />
|
||||
);
|
||||
title = node.title;
|
||||
} else {
|
||||
icon = <DocumentIcon color={theme.textSecondary} />;
|
||||
const doc = documents.get(node.id);
|
||||
emoji = doc?.emoji ?? node.emoji;
|
||||
title = doc?.title ?? node.title;
|
||||
|
||||
if (emoji) {
|
||||
icon = <EmojiIcon emoji={emoji} />;
|
||||
} else if (doc?.isStarred) {
|
||||
icon = <StarredIcon color={theme.yellow} />;
|
||||
} else {
|
||||
icon = <DocumentIcon color={theme.textSecondary} />;
|
||||
}
|
||||
|
||||
path = ancestors(node)
|
||||
.map((a) => a.title)
|
||||
.join(" / ");
|
||||
}
|
||||
|
||||
path = ancestors(node)
|
||||
.map((a) => parseTitle(a.title).strippedTitle)
|
||||
.join(" / ");
|
||||
return searchTerm ? (
|
||||
<DocumentExplorerSearchResult
|
||||
selected={isSelected(index)}
|
||||
active={activeNode === index}
|
||||
style={{
|
||||
...style,
|
||||
top: (style.top as number) + VERTICAL_PADDING,
|
||||
left: (style.left as number) + HORIZONTAL_PADDING,
|
||||
width: `calc(${style.width} - ${HORIZONTAL_PADDING * 2}px)`,
|
||||
}}
|
||||
onPointerMove={() => setActiveNode(index)}
|
||||
onClick={() => toggleSelect(index)}
|
||||
icon={icon}
|
||||
title={title}
|
||||
path={path}
|
||||
/>
|
||||
) : (
|
||||
<DocumentExplorerNode
|
||||
style={{
|
||||
...style,
|
||||
top: (style.top as number) + VERTICAL_PADDING,
|
||||
left: (style.left as number) + HORIZONTAL_PADDING,
|
||||
width: `calc(${style.width} - ${HORIZONTAL_PADDING * 2}px)`,
|
||||
}}
|
||||
onPointerMove={() => setActiveNode(index)}
|
||||
onClick={() => toggleSelect(index)}
|
||||
onDisclosureClick={(ev) => {
|
||||
ev.stopPropagation();
|
||||
toggleCollapse(index);
|
||||
}}
|
||||
selected={isSelected(index)}
|
||||
active={activeNode === index}
|
||||
expanded={isExpanded(index)}
|
||||
icon={icon}
|
||||
title={title}
|
||||
depth={node.depth as number}
|
||||
hasChildren={hasChildren(index)}
|
||||
ref={itemRefs[index]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return searchTerm ? (
|
||||
<DocumentExplorerSearchResult
|
||||
selected={isSelected(index)}
|
||||
active={activeNode === index}
|
||||
style={{
|
||||
...style,
|
||||
top: (style.top as number) + VERTICAL_PADDING,
|
||||
left: (style.left as number) + HORIZONTAL_PADDING,
|
||||
width: `calc(${style.width} - ${HORIZONTAL_PADDING * 2}px)`,
|
||||
}}
|
||||
onPointerMove={() => setActiveNode(index)}
|
||||
onClick={() => toggleSelect(index)}
|
||||
icon={icon}
|
||||
title={title}
|
||||
path={path}
|
||||
/>
|
||||
) : (
|
||||
<DocumentExplorerNode
|
||||
style={{
|
||||
...style,
|
||||
top: (style.top as number) + VERTICAL_PADDING,
|
||||
left: (style.left as number) + HORIZONTAL_PADDING,
|
||||
width: `calc(${style.width} - ${HORIZONTAL_PADDING * 2}px)`,
|
||||
}}
|
||||
onPointerMove={() => setActiveNode(index)}
|
||||
onClick={() => toggleSelect(index)}
|
||||
onDisclosureClick={(ev) => {
|
||||
ev.stopPropagation();
|
||||
toggleCollapse(index);
|
||||
}}
|
||||
selected={isSelected(index)}
|
||||
active={activeNode === index}
|
||||
expanded={isExpanded(index)}
|
||||
icon={icon}
|
||||
title={title}
|
||||
depth={node.depth as number}
|
||||
hasChildren={hasChildren(index)}
|
||||
ref={itemRefs[index]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
);
|
||||
|
||||
const focusSearchInput = () => {
|
||||
inputSearchRef.current?.focus();
|
||||
|
||||
@@ -24,6 +24,7 @@ import usePolicy from "~/hooks/usePolicy";
|
||||
import DocumentMenu from "~/menus/DocumentMenu";
|
||||
import { hover } from "~/styles";
|
||||
import { newDocumentPath } from "~/utils/routeHelpers";
|
||||
import EmojiIcon from "./Icons/EmojiIcon";
|
||||
|
||||
type Props = {
|
||||
document: Document;
|
||||
@@ -92,6 +93,12 @@ function DocumentListItem(
|
||||
>
|
||||
<Content>
|
||||
<Heading dir={document.dir}>
|
||||
{document.emoji && (
|
||||
<>
|
||||
<EmojiIcon emoji={document.emoji} size={24} />
|
||||
|
||||
</>
|
||||
)}
|
||||
<Title
|
||||
text={document.titleWithDefault}
|
||||
highlight={highlight}
|
||||
|
||||
23
app/components/EmojiPicker/components.tsx
Normal file
23
app/components/EmojiPicker/components.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import styled from "styled-components";
|
||||
import Button from "~/components/Button";
|
||||
import { hover } from "~/styles";
|
||||
import Flex from "../Flex";
|
||||
|
||||
export const EmojiButton = styled(Button)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
&: ${hover},
|
||||
&:active,
|
||||
&[aria-expanded= "true"] {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Emoji = styled(Flex)<{ size?: number }>`
|
||||
line-height: 1.6;
|
||||
${(props) => (props.size ? `font-size: ${props.size}px` : "")}
|
||||
`;
|
||||
269
app/components/EmojiPicker/index.tsx
Normal file
269
app/components/EmojiPicker/index.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
import data from "@emoji-mart/data";
|
||||
import Picker from "@emoji-mart/react";
|
||||
import { SmileyIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { usePopoverState, PopoverDisclosure } from "reakit/Popover";
|
||||
import styled, { useTheme } from "styled-components";
|
||||
import { depths, s } from "@shared/styles";
|
||||
import { toRGB } from "@shared/utils/color";
|
||||
import Button from "~/components/Button";
|
||||
import Popover from "~/components/Popover";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import useUserLocale from "~/hooks/useUserLocale";
|
||||
import { Emoji, EmojiButton } from "./components";
|
||||
|
||||
/* Locales supported by emoji-mart */
|
||||
const supportedLocales = [
|
||||
"en",
|
||||
"ar",
|
||||
"be",
|
||||
"cs",
|
||||
"de",
|
||||
"es",
|
||||
"fa",
|
||||
"fi",
|
||||
"fr",
|
||||
"hi",
|
||||
"it",
|
||||
"ja",
|
||||
"kr",
|
||||
"nl",
|
||||
"pl",
|
||||
"pt",
|
||||
"ru",
|
||||
"sa",
|
||||
"tr",
|
||||
"uk",
|
||||
"vi",
|
||||
"zh",
|
||||
];
|
||||
|
||||
/**
|
||||
* React hook to derive emoji picker's theme from UI theme
|
||||
*
|
||||
* @returns {string} Theme to use for emoji picker
|
||||
*/
|
||||
function usePickerTheme(): string {
|
||||
const { ui } = useStores();
|
||||
const { theme } = ui;
|
||||
|
||||
if (theme === "system") {
|
||||
return "auto";
|
||||
}
|
||||
|
||||
return theme;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
/** The selected emoji, if any */
|
||||
value?: string | null;
|
||||
/** Callback when an emoji is selected */
|
||||
onChange: (emoji: string | null) => void | Promise<void>;
|
||||
/** Callback when the picker is opened */
|
||||
onOpen?: () => void;
|
||||
/** Callback when the picker is closed */
|
||||
onClose?: () => void;
|
||||
/** Callback when the picker is clicked outside of */
|
||||
onClickOutside: () => void;
|
||||
/** Whether to auto focus the search input on open */
|
||||
autoFocus?: boolean;
|
||||
/** Class name to apply to the trigger button */
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function EmojiPicker({
|
||||
value,
|
||||
onOpen,
|
||||
onClose,
|
||||
onChange,
|
||||
onClickOutside,
|
||||
autoFocus,
|
||||
className,
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
const pickerTheme = usePickerTheme();
|
||||
const theme = useTheme();
|
||||
const locale = useUserLocale(true) ?? "en";
|
||||
|
||||
const popover = usePopoverState({
|
||||
placement: "bottom-start",
|
||||
modal: true,
|
||||
unstable_offset: [0, 0],
|
||||
});
|
||||
|
||||
const [emojisPerLine, setEmojisPerLine] = React.useState(9);
|
||||
|
||||
const pickerRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (popover.visible) {
|
||||
onOpen?.();
|
||||
} else {
|
||||
onClose?.();
|
||||
}
|
||||
}, [popover.visible, onOpen, onClose]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (popover.visible && pickerRef.current) {
|
||||
// 28 is picker's observed width when perLine is set to 0
|
||||
// and 36 is the default emojiButtonSize
|
||||
// Ref: https://github.com/missive/emoji-mart#options--props
|
||||
setEmojisPerLine(Math.floor((pickerRef.current.clientWidth - 28) / 36));
|
||||
}
|
||||
}, [popover.visible]);
|
||||
|
||||
const handleEmojiChange = React.useCallback(
|
||||
async (emoji) => {
|
||||
popover.hide();
|
||||
await onChange(emoji ? emoji.native : null);
|
||||
},
|
||||
[popover, onChange]
|
||||
);
|
||||
|
||||
const handleClick = React.useCallback(
|
||||
(ev: React.MouseEvent) => {
|
||||
ev.stopPropagation();
|
||||
if (popover.visible) {
|
||||
popover.hide();
|
||||
} else {
|
||||
popover.show();
|
||||
}
|
||||
},
|
||||
[popover]
|
||||
);
|
||||
|
||||
const handleClickOutside = React.useCallback(() => {
|
||||
// It was observed that onClickOutside got triggered
|
||||
// even when the picker wasn't open or opened at all.
|
||||
// Hence, this guard here...
|
||||
if (popover.visible) {
|
||||
onClickOutside();
|
||||
}
|
||||
}, [popover.visible, onClickOutside]);
|
||||
|
||||
// Auto focus search input when picker is opened
|
||||
React.useLayoutEffect(() => {
|
||||
if (autoFocus && popover.visible) {
|
||||
requestAnimationFrame(() => {
|
||||
const searchInput = pickerRef.current
|
||||
?.querySelector("em-emoji-picker")
|
||||
?.shadowRoot?.querySelector(
|
||||
"input[type=search]"
|
||||
) as HTMLInputElement | null;
|
||||
searchInput?.focus();
|
||||
});
|
||||
}
|
||||
}, [autoFocus, popover.visible]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PopoverDisclosure {...popover}>
|
||||
{(props) => (
|
||||
<EmojiButton
|
||||
{...props}
|
||||
className={className}
|
||||
onClick={handleClick}
|
||||
icon={
|
||||
value ? (
|
||||
<Emoji size={32} align="center" justify="center">
|
||||
{value}
|
||||
</Emoji>
|
||||
) : (
|
||||
<StyledSmileyIcon size={32} color={theme.textTertiary} />
|
||||
)
|
||||
}
|
||||
neutral
|
||||
borderOnHover
|
||||
/>
|
||||
)}
|
||||
</PopoverDisclosure>
|
||||
<PickerPopover
|
||||
{...popover}
|
||||
tabIndex={0}
|
||||
// This prevents picker from closing when any of its
|
||||
// children are focused, e.g, clicking on search bar or
|
||||
// a click on skin tone button
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
width={352}
|
||||
aria-label={t("Emoji Picker")}
|
||||
>
|
||||
{popover.visible && (
|
||||
<>
|
||||
{value && (
|
||||
<RemoveButton neutral onClick={() => handleEmojiChange(null)}>
|
||||
{t("Remove")}
|
||||
</RemoveButton>
|
||||
)}
|
||||
<PickerStyles ref={pickerRef}>
|
||||
<Picker
|
||||
// https://github.com/missive/emoji-mart/issues/800
|
||||
locale={
|
||||
locale === "ko"
|
||||
? "kr"
|
||||
: supportedLocales.includes(locale)
|
||||
? locale
|
||||
: "en"
|
||||
}
|
||||
data={data}
|
||||
onEmojiSelect={handleEmojiChange}
|
||||
theme={pickerTheme}
|
||||
previewPosition="none"
|
||||
perLine={emojisPerLine}
|
||||
onClickOutside={handleClickOutside}
|
||||
/>
|
||||
</PickerStyles>
|
||||
</>
|
||||
)}
|
||||
</PickerPopover>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledSmileyIcon = styled(SmileyIcon)`
|
||||
flex-shrink: 0;
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const RemoveButton = styled(Button)`
|
||||
margin-left: -12px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 6px;
|
||||
height: 24px;
|
||||
font-size: 13px;
|
||||
|
||||
> :first-child {
|
||||
min-height: unset;
|
||||
line-height: unset;
|
||||
}
|
||||
`;
|
||||
|
||||
const PickerPopover = styled(Popover)`
|
||||
z-index: ${depths.popover};
|
||||
> :first-child {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 0;
|
||||
max-height: 488px;
|
||||
overflow: unset;
|
||||
}
|
||||
`;
|
||||
|
||||
const PickerStyles = styled.div`
|
||||
margin-left: -24px;
|
||||
margin-right: -24px;
|
||||
em-emoji-picker {
|
||||
--shadow: none;
|
||||
--font-family: ${s("fontFamily")};
|
||||
--rgb-background: ${(props) => toRGB(props.theme.menuBackground)};
|
||||
--rgb-accent: ${(props) => toRGB(props.theme.accent)};
|
||||
--border-radius: 6px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
min-height: 443px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default EmojiPicker;
|
||||
@@ -29,5 +29,5 @@ const Span = styled.span<{ $size: number }>`
|
||||
width: ${(props) => props.$size}px;
|
||||
height: ${(props) => props.$size}px;
|
||||
text-indent: -0.15em;
|
||||
font-size: 14px;
|
||||
font-size: ${(props) => props.$size - 10}px;
|
||||
`;
|
||||
|
||||
@@ -324,6 +324,7 @@ function InnerDocumentLink(
|
||||
starred: inStarredSection,
|
||||
},
|
||||
}}
|
||||
emoji={document?.emoji || node.emoji}
|
||||
label={
|
||||
<EditableTitle
|
||||
title={title}
|
||||
|
||||
@@ -8,7 +8,6 @@ import Document from "~/models/Document";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { sharedDocumentPath } from "~/utils/routeHelpers";
|
||||
import { descendants } from "~/utils/tree";
|
||||
import Disclosure from "./Disclosure";
|
||||
import SidebarLink from "./SidebarLink";
|
||||
|
||||
type Props = {
|
||||
@@ -110,14 +109,10 @@ function DocumentLink(
|
||||
title: node.title,
|
||||
},
|
||||
}}
|
||||
label={
|
||||
<>
|
||||
{hasChildDocuments && depth !== 0 && (
|
||||
<Disclosure expanded={expanded} onClick={handleDisclosureClick} />
|
||||
)}
|
||||
{title}
|
||||
</>
|
||||
}
|
||||
expanded={hasChildDocuments && depth !== 0 ? expanded : undefined}
|
||||
onDisclosureClick={handleDisclosureClick}
|
||||
emoji={node.emoji}
|
||||
label={title}
|
||||
depth={depth}
|
||||
exact={false}
|
||||
scrollIntoViewIfNeeded={!document?.isStarred}
|
||||
|
||||
@@ -5,6 +5,7 @@ import breakpoint from "styled-components-breakpoint";
|
||||
import { s } from "@shared/styles";
|
||||
import { NavigationNode } from "@shared/types";
|
||||
import EventBoundary from "~/components/EventBoundary";
|
||||
import EmojiIcon from "~/components/Icons/EmojiIcon";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
import useUnmount from "~/hooks/useUnmount";
|
||||
import { undraggableOnDesktop } from "~/styles";
|
||||
@@ -25,6 +26,7 @@ type Props = Omit<NavLinkProps, "to"> & {
|
||||
onClickIntent?: () => void;
|
||||
onDisclosureClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
icon?: React.ReactNode;
|
||||
emoji?: string | null;
|
||||
label?: React.ReactNode;
|
||||
menu?: React.ReactNode;
|
||||
showActions?: boolean;
|
||||
@@ -48,6 +50,7 @@ function SidebarLink(
|
||||
onClick,
|
||||
onClickIntent,
|
||||
to,
|
||||
emoji,
|
||||
label,
|
||||
active,
|
||||
isActiveDrop,
|
||||
@@ -136,6 +139,7 @@ function SidebarLink(
|
||||
/>
|
||||
)}
|
||||
{icon && <IconWrapper>{icon}</IconWrapper>}
|
||||
{emoji && <EmojiIcon emoji={emoji} />}
|
||||
<Label>{label}</Label>
|
||||
</Content>
|
||||
</Link>
|
||||
@@ -152,6 +156,7 @@ const Content = styled.span`
|
||||
|
||||
${Disclosure} {
|
||||
margin-top: 2px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import { useDrag, useDrop } from "react-dnd";
|
||||
import { getEmptyImage } from "react-dnd-html5-backend";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import styled, { useTheme } from "styled-components";
|
||||
import parseTitle from "@shared/utils/parseTitle";
|
||||
import Star from "~/models/Star";
|
||||
import Fade from "~/components/Fade";
|
||||
import CollectionIcon from "~/components/Icons/CollectionIcon";
|
||||
@@ -42,14 +41,10 @@ function useLabelAndIcon({ documentId, collectionId }: Star) {
|
||||
if (documentId) {
|
||||
const document = documents.get(documentId);
|
||||
if (document) {
|
||||
const { emoji } = parseTitle(document?.title);
|
||||
|
||||
return {
|
||||
label: emoji
|
||||
? document.title.replace(emoji, "")
|
||||
: document.titleWithDefault,
|
||||
icon: emoji ? (
|
||||
<EmojiIcon emoji={emoji} />
|
||||
label: document.titleWithDefault,
|
||||
icon: document.emoji ? (
|
||||
<EmojiIcon emoji={document.emoji} />
|
||||
) : (
|
||||
<StarredIcon color={theme.yellow} />
|
||||
),
|
||||
@@ -148,6 +143,10 @@ function StarredLink({ star }: Props) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { emoji } = document;
|
||||
const label = emoji
|
||||
? document.title.replace(emoji, "")
|
||||
: document.titleWithDefault;
|
||||
const collection = document.collectionId
|
||||
? collections.get(document.collectionId)
|
||||
: undefined;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { StarredIcon, UnstarredIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled, { useTheme } from "styled-components";
|
||||
import Collection from "~/models/Collection";
|
||||
import Document from "~/models/Document";
|
||||
@@ -14,12 +15,18 @@ import { hover } from "~/styles";
|
||||
import NudeButton from "./NudeButton";
|
||||
|
||||
type Props = {
|
||||
/** Target collection */
|
||||
collection?: Collection;
|
||||
/** Target document */
|
||||
document?: Document;
|
||||
/** Size of the star */
|
||||
size?: number;
|
||||
/** Color override for the star */
|
||||
color?: string;
|
||||
};
|
||||
|
||||
function Star({ size, document, collection, ...rest }: Props) {
|
||||
function Star({ size, document, collection, color, ...rest }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const context = useActionContext({
|
||||
activeDocumentId: document?.id,
|
||||
@@ -36,6 +43,10 @@ function Star({ size, document, collection, ...rest }: Props) {
|
||||
<NudeButton
|
||||
context={context}
|
||||
hideOnActionDisabled
|
||||
tooltip={{
|
||||
tooltip: target.isStarred ? t("Unstar document") : t("Star document"),
|
||||
delay: 500,
|
||||
}}
|
||||
action={
|
||||
collection
|
||||
? collection.isStarred
|
||||
@@ -55,7 +66,7 @@ function Star({ size, document, collection, ...rest }: Props) {
|
||||
) : (
|
||||
<AnimatedStar
|
||||
size={size}
|
||||
color={theme.textTertiary}
|
||||
color={color ?? theme.textTertiary}
|
||||
as={UnstarredIcon}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user