tiptap: use lru-cache in insert menu

This commit is contained in:
fantasticit 2022-04-25 21:12:49 +08:00
parent 762ac3a3f5
commit 8d9926f4e8
1 changed files with 153 additions and 74 deletions

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Editor } from '@tiptap/core'; import { Editor } from '@tiptap/core';
import { Button, Dropdown, Popover } from '@douyinfe/semi-ui'; import { Button, Dropdown, Popover } from '@douyinfe/semi-ui';
import { IconPlus } from '@douyinfe/semi-icons'; import { IconPlus } from '@douyinfe/semi-icons';
@ -11,17 +11,149 @@ import {
IconCodeBlock, IconCodeBlock,
IconLink, IconLink,
IconStatus, IconStatus,
IconInfo,
IconAttachment, IconAttachment,
IconMath, IconMath,
IconCountdown, IconCountdown,
IconCallout, IconCallout,
} from 'components/icons'; } from 'components/icons';
import { GridSelect } from 'components/grid-select'; import { GridSelect } from 'components/grid-select';
import { useToggle } from 'hooks/use-toggle';
import { createKeysLocalStorageLRUCache } from 'helpers/lru-cache';
import { isTitleActive } from '../../utils/is-active'; import { isTitleActive } from '../../utils/is-active';
import { createCountdown } from '../countdown/service'; import { createCountdown } from '../countdown/service';
const insertMenuLRUCache = createKeysLocalStorageLRUCache('TIPTAP_INSERT_MENU', 3);
const COMMANDS = [
{
title: '通用',
},
{
icon: <IconTable />,
label: '表格',
custom: (editor, runCommand) => (
<Popover
showArrow
position="rightTop"
zIndex={10000}
content={
<div style={{ padding: 0 }}>
<GridSelect
onSelect={({ rows, cols }) => {
return runCommand({
label: '表格',
action: () => editor.chain().focus().insertTable({ rows, cols, withHeaderRow: true }).run(),
})();
}}
/>
</div>
}
>
<Dropdown.Item>
<IconTable />
</Dropdown.Item>
</Popover>
),
},
{
icon: <IconCodeBlock />,
label: '代码块',
action: (editor) => editor.chain().focus().toggleCodeBlock().run(),
},
{
icon: <IconImage />,
label: '图片',
action: (editor) => editor.chain().focus().setEmptyImage().run(),
},
{
icon: <IconAttachment />,
label: '附件',
action: (editor) => editor.chain().focus().setAttachment().run(),
},
{
icon: <IconCountdown />,
label: '倒计时',
action: (editor) => createCountdown(editor),
},
{
icon: <IconLink />,
label: '外链',
action: (editor) => editor.chain().focus().setIframe({ url: '' }).run(),
},
{
title: '卡片',
},
{
icon: <IconMind />,
label: '思维导图',
action: (editor) => editor.chain().focus().setMind().run(),
},
{
icon: <IconMath />,
label: '数学公式',
action: (editor) => editor.chain().focus().setKatex({ defaultShowPicker: true }).run(),
},
{
icon: <IconStatus />,
label: '状态',
action: (editor) => editor.chain().focus().setStatus({ defaultShowPicker: true }).run(),
},
{
icon: <IconCallout />,
label: '高亮块',
action: (editor) => editor.chain().focus().setCallout().run(),
},
{
title: '内容引用',
},
{
icon: <IconDocument />,
label: '文档',
action: (editor) => editor.chain().focus().setDocumentReference().run(),
},
{
icon: <IconDocument />,
label: '子文档',
action: (editor) => editor.chain().focus().setDocumentChildren().run(),
},
];
export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => { export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => {
const [recentUsed, setRecentUsed] = useState([]);
const [visible, toggleVisible] = useToggle(false);
const renderedCommands = useMemo(
() => (recentUsed.length ? [{ title: '最近使用' }, ...recentUsed, ...COMMANDS] : COMMANDS),
[recentUsed]
);
const transformToCommands = useCallback((data: string[]) => {
return data
.map((label) => {
return COMMANDS.find((command) => command.label && command.label === label);
})
.filter(Boolean);
}, []);
const runCommand = useCallback(
(command) => {
return () => {
insertMenuLRUCache.put(command.label);
setRecentUsed(transformToCommands(insertMenuLRUCache.get() as string[]));
command.action(editor);
toggleVisible(false);
};
},
[editor, toggleVisible]
);
useEffect(() => {
if (!visible) return;
insertMenuLRUCache.syncFromStorage();
setRecentUsed(transformToCommands(insertMenuLRUCache.get() as string[]));
}, [visible]);
if (!editor) { if (!editor) {
return null; return null;
} }
@ -31,80 +163,27 @@ export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => {
zIndex={10000} zIndex={10000}
trigger="click" trigger="click"
position="bottomLeft" position="bottomLeft"
visible={visible}
onVisibleChange={toggleVisible}
style={{
minWidth: 132,
maxHeight: 'calc(90vh - 120px)',
overflowY: 'auto',
}}
render={ render={
<Dropdown.Menu> <Dropdown.Menu>
<Dropdown.Title></Dropdown.Title> {renderedCommands.map((command) => {
return command.title ? (
<Popover <Dropdown.Title>{command.title}</Dropdown.Title>
showArrow ) : command.custom ? (
position="rightTop" command.custom(editor, runCommand)
zIndex={10000} ) : (
content={ <Dropdown.Item onClick={runCommand(command)}>
<div style={{ padding: 0 }}> {command.icon}
<GridSelect {command.label}
onSelect={({ rows, cols }) => {
return editor.chain().focus().insertTable({ rows, cols, withHeaderRow: true }).run();
}}
/>
</div>
}
>
<Dropdown.Item>
<IconTable />
</Dropdown.Item>
</Popover>
<Dropdown.Item onClick={() => editor.chain().focus().toggleCodeBlock().run()}>
<IconCodeBlock />
</Dropdown.Item>
<Dropdown.Item onClick={() => editor.chain().focus().setEmptyImage().run()}>
<IconImage />
</Dropdown.Item>
<Dropdown.Item onClick={() => editor.chain().focus().setAttachment().run()}>
<IconAttachment />
</Dropdown.Item>
<Dropdown.Item onClick={() => createCountdown(editor)}>
<IconCountdown />
</Dropdown.Item>
<Dropdown.Item onClick={() => editor.chain().focus().setIframe({ url: '' }).run()}>
<IconLink />
</Dropdown.Item>
<Dropdown.Item onClick={() => editor.chain().focus().setMind().run()}>
<IconMind />
</Dropdown.Item>
<Dropdown.Item onClick={() => editor.chain().focus().setKatex({ defaultShowPicker: true }).run()}>
<IconMath />
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Title></Dropdown.Title>
<Dropdown.Item onClick={() => editor.chain().focus().setStatus({ defaultShowPicker: true }).run()}>
<IconStatus />
</Dropdown.Item>
<Dropdown.Item onClick={() => editor.chain().focus().setCallout().run()}>
<IconCallout />
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Title></Dropdown.Title>
<Dropdown.Item onClick={() => editor.chain().focus().setDocumentReference().run()}>
<IconDocument />
</Dropdown.Item>
<Dropdown.Item onClick={() => editor.chain().focus().setDocumentChildren().run()}>
<IconDocument />
</Dropdown.Item> </Dropdown.Item>
);
})}
</Dropdown.Menu> </Dropdown.Menu>
} }
> >