chore: Refactors towards shared menu component (#4445)

This commit is contained in:
Tom Moor
2022-11-19 13:15:38 -08:00
committed by GitHub
parent 924b554281
commit ae6855f3df
21 changed files with 186 additions and 194 deletions

View File

@@ -1,8 +1,8 @@
import { findParentNode } from "prosemirror-utils";
import React from "react";
import getMenuItems from "../menus/block";
import BlockMenuItem from "./BlockMenuItem";
import CommandMenu, { Props } from "./CommandMenu";
import CommandMenuItem from "./CommandMenuItem";
type BlockMenuProps = Omit<
Props,
@@ -26,7 +26,7 @@ function BlockMenu(props: BlockMenuProps) {
filterable={true}
onClearSearch={clearSearch}
renderMenuItem={(item, _index, options) => (
<BlockMenuItem
<CommandMenuItem
onClick={options.onClick}
selected={options.selected}
icon={item.icon}

View File

@@ -62,7 +62,7 @@ type State = {
selectedIndex: number;
};
class CommandMenu<T = MenuItem> extends React.Component<Props<T>, State> {
class CommandMenu<T extends MenuItem> extends React.Component<Props<T>, State> {
menuRef = React.createRef<HTMLDivElement>();
inputRef = React.createRef<HTMLInputElement>();
@@ -79,7 +79,7 @@ class CommandMenu<T = MenuItem> extends React.Component<Props<T>, State> {
window.addEventListener("keydown", this.handleKeyDown);
}
shouldComponentUpdate(nextProps: Props, nextState: State) {
shouldComponentUpdate(nextProps: Props<T>, nextState: State) {
return (
nextProps.search !== this.props.search ||
nextProps.isActive !== this.props.isActive ||
@@ -87,7 +87,7 @@ class CommandMenu<T = MenuItem> extends React.Component<Props<T>, State> {
);
}
componentDidUpdate(prevProps: Props) {
componentDidUpdate(prevProps: Props<T>) {
if (!prevProps.isActive && this.props.isActive) {
// reset scroll position to top when opening menu as the contents are
// hidden, not unrendered
@@ -575,7 +575,7 @@ const LinkInputWrapper = styled.div`
`;
const LinkInput = styled(Input)`
height: 36px;
height: 32px;
width: 100%;
color: ${(props) => props.theme.textSecondary};
`;
@@ -584,7 +584,7 @@ const List = styled.ol`
list-style: none;
text-align: left;
height: 100%;
padding: 8px 0;
padding: 6px;
margin: 0;
`;
@@ -599,7 +599,7 @@ const Empty = styled.div`
color: ${(props) => props.theme.textSecondary};
font-weight: 500;
font-size: 14px;
height: 36px;
height: 32px;
padding: 0 16px;
`;
@@ -630,7 +630,7 @@ export const Wrapper = styled(Scrollable)<{
box-sizing: border-box;
pointer-events: none;
white-space: nowrap;
width: 300px;
width: 280px;
height: auto;
max-height: 324px;

View File

@@ -1,18 +1,19 @@
import * as React from "react";
import scrollIntoView from "smooth-scroll-into-view-if-needed";
import styled from "styled-components";
import MenuItem from "~/components/ContextMenu/MenuItem";
export type Props = {
selected: boolean;
disabled?: boolean;
onClick: () => void;
icon?: typeof React.Component | React.FC<any>;
icon?: React.ReactElement;
title: React.ReactNode;
shortcut?: string;
containerId?: string;
};
function BlockMenuItem({
function CommandMenuItem({
selected,
disabled,
onClick,
@@ -21,8 +22,6 @@ function BlockMenuItem({
icon,
containerId = "block-menu-container",
}: Props) {
const Icon = icon;
const ref = React.useCallback(
(node) => {
if (selected && node) {
@@ -43,56 +42,22 @@ function BlockMenuItem({
return (
<MenuItem
selected={selected}
onClick={disabled ? undefined : onClick}
ref={ref}
active={selected}
onClick={disabled ? undefined : onClick}
icon={icon}
>
{Icon && (
<>
<Icon color="currentColor" />
&nbsp;&nbsp;
</>
)}
{title}
{shortcut && <Shortcut>{shortcut}</Shortcut>}
{shortcut && <Shortcut $active={selected}>{shortcut}</Shortcut>}
</MenuItem>
);
}
const Shortcut = styled.span`
color: ${(props) => props.theme.textTertiary};
const Shortcut = styled.span<{ $active?: boolean }>`
color: ${(props) =>
props.$active ? props.theme.white50 : props.theme.textTertiary};
flex-grow: 1;
text-align: right;
`;
const MenuItem = styled.button<{
selected: boolean;
}>`
display: flex;
align-items: center;
justify-content: flex-start;
font-weight: 500;
font-size: 14px;
line-height: 1;
width: 100%;
height: 36px;
cursor: var(--pointer);
border: none;
opacity: ${(props) => (props.disabled ? ".5" : "1")};
color: ${(props) =>
props.selected ? props.theme.white : props.theme.textSecondary};
background: ${(props) => (props.selected ? props.theme.primary : "none")};
padding: 0 16px;
outline: none;
&:active {
color: ${(props) => props.theme.white};
background: ${(props) => (props.selected ? props.theme.primary : "none")};
${Shortcut} {
color: ${(props) => props.theme.textSecondary};
}
}
`;
export default BlockMenuItem;
export default CommandMenuItem;

View File

@@ -1,35 +1,23 @@
import * as React from "react";
import styled from "styled-components";
import BlockMenuItem, { Props as BlockMenuItemProps } from "./BlockMenuItem";
import CommandMenuItem, {
Props as CommandMenuItemProps,
} from "./CommandMenuItem";
const Emoji = styled.span`
font-size: 16px;
line-height: 1.6em;
`;
type Props = {
emoji: React.ReactNode;
title: React.ReactNode;
};
const EmojiTitle = ({ emoji, title }: Props) => {
return (
<p>
<Emoji className="emoji">{emoji}</Emoji>
&nbsp;&nbsp;
{title}
</p>
);
};
type EmojiMenuItemProps = Omit<BlockMenuItemProps, "shortcut" | "theme"> & {
type EmojiMenuItemProps = Omit<CommandMenuItemProps, "shortcut" | "theme"> & {
emoji: string;
};
export default function EmojiMenuItem(props: EmojiMenuItemProps) {
export default function EmojiMenuItem({ emoji, ...rest }: EmojiMenuItemProps) {
return (
<BlockMenuItem
{...props}
title={<EmojiTitle emoji={props.emoji} title={props.title} />}
<CommandMenuItem
{...rest}
icon={<Emoji className="emoji">{emoji}</Emoji>}
/>
);
}

View File

@@ -30,7 +30,6 @@ function ToolbarMenu(props: Props) {
if (item.visible === false || !item.icon) {
return null;
}
const Icon = item.icon;
const isActive = item.active ? item.active(state) : false;
return (
@@ -39,7 +38,7 @@ function ToolbarMenu(props: Props) {
onClick={() => item.name && commands[item.name](item.attrs)}
active={isActive}
>
<Icon color="currentColor" />
{React.cloneElement(item.icon, { color: "currentColor" })}
</ToolbarButton>
</Tooltip>
);

View File

@@ -41,7 +41,7 @@ export default function blockMenuItems(dictionary: Dictionary): MenuItem[] {
name: "heading",
title: dictionary.h1,
keywords: "h1 heading1 title",
icon: Heading1Icon,
icon: <Heading1Icon />,
shortcut: "^ ⇧ 1",
attrs: { level: 1 },
},
@@ -49,7 +49,7 @@ export default function blockMenuItems(dictionary: Dictionary): MenuItem[] {
name: "heading",
title: dictionary.h2,
keywords: "h2 heading2",
icon: Heading2Icon,
icon: <Heading2Icon />,
shortcut: "^ ⇧ 2",
attrs: { level: 2 },
},
@@ -57,7 +57,7 @@ export default function blockMenuItems(dictionary: Dictionary): MenuItem[] {
name: "heading",
title: dictionary.h3,
keywords: "h3 heading3",
icon: Heading3Icon,
icon: <Heading3Icon />,
shortcut: "^ ⇧ 3",
attrs: { level: 3 },
},
@@ -67,20 +67,20 @@ export default function blockMenuItems(dictionary: Dictionary): MenuItem[] {
{
name: "checkbox_list",
title: dictionary.checkboxList,
icon: TodoListIcon,
icon: <TodoListIcon />,
keywords: "checklist checkbox task",
shortcut: "^ ⇧ 7",
},
{
name: "bullet_list",
title: dictionary.bulletList,
icon: BulletedListIcon,
icon: <BulletedListIcon />,
shortcut: "^ ⇧ 8",
},
{
name: "ordered_list",
title: dictionary.orderedList,
icon: OrderedListIcon,
icon: <OrderedListIcon />,
shortcut: "^ ⇧ 9",
},
{
@@ -89,52 +89,52 @@ export default function blockMenuItems(dictionary: Dictionary): MenuItem[] {
{
name: "image",
title: dictionary.image,
icon: ImageIcon,
icon: <ImageIcon />,
keywords: "picture photo",
},
{
name: "link",
title: dictionary.link,
icon: LinkIcon,
icon: <LinkIcon />,
shortcut: `${metaDisplay} k`,
keywords: "link url uri href",
},
{
name: "attachment",
title: dictionary.file,
icon: AttachmentIcon,
icon: <AttachmentIcon />,
keywords: "file upload attach",
},
{
name: "table",
title: dictionary.table,
icon: TableIcon,
icon: <TableIcon />,
attrs: { rowsCount: 3, colsCount: 3 },
},
{
name: "blockquote",
title: dictionary.quote,
icon: BlockQuoteIcon,
icon: <BlockQuoteIcon />,
shortcut: `${metaDisplay} ]`,
},
{
name: "code_block",
title: dictionary.codeBlock,
icon: CodeIcon,
icon: <CodeIcon />,
shortcut: "^ ⇧ \\",
keywords: "script",
},
{
name: "hr",
title: dictionary.hr,
icon: HorizontalRuleIcon,
icon: <HorizontalRuleIcon />,
shortcut: `${metaDisplay} _`,
keywords: "horizontal rule break line",
},
{
name: "hr",
title: dictionary.pageBreak,
icon: PageBreakIcon,
icon: <PageBreakIcon />,
keywords: "page print break line",
attrs: { markup: "***" },
},
@@ -142,19 +142,19 @@ export default function blockMenuItems(dictionary: Dictionary): MenuItem[] {
name: "date",
title: dictionary.insertDate,
keywords: "clock",
icon: CalendarIcon,
icon: <CalendarIcon />,
},
{
name: "time",
title: dictionary.insertTime,
keywords: "clock",
icon: ClockIcon,
icon: <ClockIcon />,
},
{
name: "datetime",
title: dictionary.insertDateTime,
keywords: "clock",
icon: CalendarIcon,
icon: <CalendarIcon />,
},
{
name: "separator",
@@ -162,21 +162,21 @@ export default function blockMenuItems(dictionary: Dictionary): MenuItem[] {
{
name: "container_notice",
title: dictionary.infoNotice,
icon: InfoIcon,
icon: <InfoIcon />,
keywords: "notice card information",
attrs: { style: "info" },
},
{
name: "container_notice",
title: dictionary.warningNotice,
icon: WarningIcon,
icon: <WarningIcon />,
keywords: "notice card error",
attrs: { style: "warning" },
},
{
name: "container_notice",
title: dictionary.tipNotice,
icon: StarredIcon,
icon: <StarredIcon />,
keywords: "notice card suggestion",
attrs: { style: "tip" },
},
@@ -186,7 +186,7 @@ export default function blockMenuItems(dictionary: Dictionary): MenuItem[] {
{
name: "code_block",
title: "Mermaid Diagram",
icon: () => <Img src="/images/mermaidjs.png" alt="Mermaid Diagram" />,
icon: <Img src="/images/mermaidjs.png" alt="Mermaid Diagram" />,
keywords: "diagram flowchart",
attrs: { language: "mermaidjs" },
},

View File

@@ -1,5 +1,6 @@
import { PageBreakIcon, HorizontalRuleIcon } from "outline-icons";
import { EditorState } from "prosemirror-state";
import * as React from "react";
import isNodeActive from "@shared/editor/queries/isNodeActive";
import { MenuItem } from "@shared/editor/types";
import { Dictionary } from "~/hooks/useDictionary";
@@ -16,14 +17,14 @@ export default function dividerMenuItems(
tooltip: dictionary.pageBreak,
attrs: { markup: "***" },
active: isNodeActive(schema.nodes.hr, { markup: "***" }),
icon: PageBreakIcon,
icon: <PageBreakIcon />,
},
{
name: "hr",
tooltip: dictionary.hr,
attrs: { markup: "---" },
active: isNodeActive(schema.nodes.hr, { markup: "---" }),
icon: HorizontalRuleIcon,
icon: <HorizontalRuleIcon />,
},
];
}

View File

@@ -11,9 +11,11 @@ import {
TodoListIcon,
InputIcon,
HighlightIcon,
ItalicIcon,
} from "outline-icons";
import { EditorState } from "prosemirror-state";
import { isInTable } from "prosemirror-tables";
import * as React from "react";
import isInCode from "@shared/editor/queries/isInCode";
import isInList from "@shared/editor/queries/isInList";
import isMarkActive from "@shared/editor/queries/isMarkActive";
@@ -36,7 +38,7 @@ export default function formattingMenuItems(
{
name: "placeholder",
tooltip: dictionary.placeholder,
icon: InputIcon,
icon: <InputIcon />,
active: isMarkActive(schema.marks.placeholder),
visible: isTemplate,
},
@@ -47,28 +49,35 @@ export default function formattingMenuItems(
{
name: "strong",
tooltip: dictionary.strong,
icon: BoldIcon,
icon: <BoldIcon />,
active: isMarkActive(schema.marks.strong),
visible: !isCode,
},
{
name: "em",
tooltip: dictionary.em,
icon: <ItalicIcon />,
active: isMarkActive(schema.marks.em),
visible: !isCode,
},
{
name: "strikethrough",
tooltip: dictionary.strikethrough,
icon: StrikethroughIcon,
icon: <StrikethroughIcon />,
active: isMarkActive(schema.marks.strikethrough),
visible: !isCode,
},
{
name: "highlight",
tooltip: dictionary.mark,
icon: HighlightIcon,
icon: <HighlightIcon />,
active: isMarkActive(schema.marks.highlight),
visible: !isTemplate && !isCode,
},
{
name: "code_inline",
tooltip: dictionary.codeInline,
icon: CodeIcon,
icon: <CodeIcon />,
active: isMarkActive(schema.marks.code_inline),
},
{
@@ -78,7 +87,7 @@ export default function formattingMenuItems(
{
name: "heading",
tooltip: dictionary.heading,
icon: Heading1Icon,
icon: <Heading1Icon />,
active: isNodeActive(schema.nodes.heading, { level: 1 }),
attrs: { level: 1 },
visible: allowBlocks && !isCode,
@@ -86,7 +95,7 @@ export default function formattingMenuItems(
{
name: "heading",
tooltip: dictionary.subheading,
icon: Heading2Icon,
icon: <Heading2Icon />,
active: isNodeActive(schema.nodes.heading, { level: 2 }),
attrs: { level: 2 },
visible: allowBlocks && !isCode,
@@ -94,7 +103,7 @@ export default function formattingMenuItems(
{
name: "blockquote",
tooltip: dictionary.quote,
icon: BlockQuoteIcon,
icon: <BlockQuoteIcon />,
active: isNodeActive(schema.nodes.blockquote),
attrs: { level: 2 },
visible: allowBlocks && !isCode,
@@ -106,7 +115,7 @@ export default function formattingMenuItems(
{
name: "checkbox_list",
tooltip: dictionary.checkboxList,
icon: TodoListIcon,
icon: <TodoListIcon />,
keywords: "checklist checkbox task",
active: isNodeActive(schema.nodes.checkbox_list),
visible: (allowBlocks || isList) && !isCode,
@@ -114,14 +123,14 @@ export default function formattingMenuItems(
{
name: "bullet_list",
tooltip: dictionary.bulletList,
icon: BulletedListIcon,
icon: <BulletedListIcon />,
active: isNodeActive(schema.nodes.bullet_list),
visible: (allowBlocks || isList) && !isCode,
},
{
name: "ordered_list",
tooltip: dictionary.orderedList,
icon: OrderedListIcon,
icon: <OrderedListIcon />,
active: isNodeActive(schema.nodes.ordered_list),
visible: (allowBlocks || isList) && !isCode,
},
@@ -132,7 +141,7 @@ export default function formattingMenuItems(
{
name: "link",
tooltip: dictionary.createLink,
icon: LinkIcon,
icon: <LinkIcon />,
active: isMarkActive(schema.marks.link),
attrs: { href: "" },
visible: !isCode,

View File

@@ -7,6 +7,7 @@ import {
AlignImageCenterIcon,
} from "outline-icons";
import { EditorState } from "prosemirror-state";
import * as React from "react";
import isNodeActive from "@shared/editor/queries/isNodeActive";
import { MenuItem } from "@shared/editor/types";
import { Dictionary } from "~/hooks/useDictionary";
@@ -27,14 +28,14 @@ export default function imageMenuItems(
{
name: "alignLeft",
tooltip: dictionary.alignLeft,
icon: AlignImageLeftIcon,
icon: <AlignImageLeftIcon />,
visible: true,
active: isLeftAligned,
},
{
name: "alignCenter",
tooltip: dictionary.alignCenter,
icon: AlignImageCenterIcon,
icon: <AlignImageCenterIcon />,
visible: true,
active: (state) =>
isNodeActive(schema.nodes.image)(state) &&
@@ -44,7 +45,7 @@ export default function imageMenuItems(
{
name: "alignRight",
tooltip: dictionary.alignRight,
icon: AlignImageRightIcon,
icon: <AlignImageRightIcon />,
visible: true,
active: isRightAligned,
},
@@ -55,21 +56,21 @@ export default function imageMenuItems(
{
name: "downloadImage",
tooltip: dictionary.downloadImage,
icon: DownloadIcon,
icon: <DownloadIcon />,
visible: !!fetch,
active: () => false,
},
{
name: "replaceImage",
tooltip: dictionary.replaceImage,
icon: ReplaceIcon,
icon: <ReplaceIcon />,
visible: true,
active: () => false,
},
{
name: "deleteImage",
tooltip: dictionary.deleteImage,
icon: TrashIcon,
icon: <TrashIcon />,
visible: true,
active: () => false,
},

View File

@@ -1,4 +1,5 @@
import { TrashIcon } from "outline-icons";
import * as React from "react";
import { MenuItem } from "@shared/editor/types";
import { Dictionary } from "~/hooks/useDictionary";
@@ -7,7 +8,7 @@ export default function tableMenuItems(dictionary: Dictionary): MenuItem[] {
{
name: "deleteTable",
tooltip: dictionary.deleteTable,
icon: TrashIcon,
icon: <TrashIcon />,
active: () => false,
},
];

View File

@@ -7,6 +7,7 @@ import {
InsertRightIcon,
} from "outline-icons";
import { EditorState } from "prosemirror-state";
import * as React from "react";
import isNodeActive from "@shared/editor/queries/isNodeActive";
import { MenuItem } from "@shared/editor/types";
import { Dictionary } from "~/hooks/useDictionary";
@@ -23,7 +24,7 @@ export default function tableColMenuItems(
{
name: "setColumnAttr",
tooltip: dictionary.alignLeft,
icon: AlignLeftIcon,
icon: <AlignLeftIcon />,
attrs: { index, alignment: "left" },
active: isNodeActive(schema.nodes.th, {
colspan: 1,
@@ -34,7 +35,7 @@ export default function tableColMenuItems(
{
name: "setColumnAttr",
tooltip: dictionary.alignCenter,
icon: AlignCenterIcon,
icon: <AlignCenterIcon />,
attrs: { index, alignment: "center" },
active: isNodeActive(schema.nodes.th, {
colspan: 1,
@@ -45,7 +46,7 @@ export default function tableColMenuItems(
{
name: "setColumnAttr",
tooltip: dictionary.alignRight,
icon: AlignRightIcon,
icon: <AlignRightIcon />,
attrs: { index, alignment: "right" },
active: isNodeActive(schema.nodes.th, {
colspan: 1,
@@ -59,13 +60,13 @@ export default function tableColMenuItems(
{
name: rtl ? "addColumnAfter" : "addColumnBefore",
tooltip: rtl ? dictionary.addColumnAfter : dictionary.addColumnBefore,
icon: InsertLeftIcon,
icon: <InsertLeftIcon />,
active: () => false,
},
{
name: rtl ? "addColumnBefore" : "addColumnAfter",
tooltip: rtl ? dictionary.addColumnBefore : dictionary.addColumnAfter,
icon: InsertRightIcon,
icon: <InsertRightIcon />,
active: () => false,
},
{
@@ -74,7 +75,7 @@ export default function tableColMenuItems(
{
name: "deleteColumn",
tooltip: dictionary.deleteColumn,
icon: TrashIcon,
icon: <TrashIcon />,
active: () => false,
},
];

View File

@@ -1,5 +1,6 @@
import { TrashIcon, InsertAboveIcon, InsertBelowIcon } from "outline-icons";
import { EditorState } from "prosemirror-state";
import * as React from "react";
import { MenuItem } from "@shared/editor/types";
import { Dictionary } from "~/hooks/useDictionary";
@@ -12,7 +13,7 @@ export default function tableRowMenuItems(
{
name: "addRowAfter",
tooltip: dictionary.addRowBefore,
icon: InsertAboveIcon,
icon: <InsertAboveIcon />,
attrs: { index: index - 1 },
active: () => false,
visible: index !== 0,
@@ -20,7 +21,7 @@ export default function tableRowMenuItems(
{
name: "addRowAfter",
tooltip: dictionary.addRowAfter,
icon: InsertBelowIcon,
icon: <InsertBelowIcon />,
attrs: { index },
active: () => false,
},
@@ -30,7 +31,7 @@ export default function tableRowMenuItems(
{
name: "deleteRow",
tooltip: dictionary.deleteRow,
icon: TrashIcon,
icon: <TrashIcon />,
active: () => false,
},
];