tiptap: improve document-reference menu

pull/29/head
fantasticit 2022-04-25 10:37:25 +08:00
parent 9042da011f
commit c9d4fd5127
5 changed files with 132 additions and 74 deletions

View File

@ -34,6 +34,7 @@ import { Search } from './menus/search';
import { Banner } from './menus/banner'; import { Banner } from './menus/banner';
import { Countdonw } from './menus/countdown'; import { Countdonw } from './menus/countdown';
import { DocumentReference } from './menus/document-reference';
import { Image } from './menus/image'; import { Image } from './menus/image';
import { Iframe } from './menus/iframe'; import { Iframe } from './menus/iframe';
import { Table } from './menus/table'; import { Table } from './menus/table';
@ -89,6 +90,7 @@ export const MenuBar: React.FC<{ editor: any }> = ({ editor }) => {
<Banner editor={editor} /> <Banner editor={editor} />
<Countdonw editor={editor} /> <Countdonw editor={editor} />
<DocumentReference editor={editor} />
<Image editor={editor} /> <Image editor={editor} />
<Iframe editor={editor} /> <Iframe editor={editor} />
<Table editor={editor} /> <Table editor={editor} />

View File

@ -0,0 +1,77 @@
import { useCallback } from 'react';
import { useRouter } from 'next/router';
import { Space, Button, List, Popover } from '@douyinfe/semi-ui';
import { IconEdit, IconDelete } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip';
import { DataRender } from 'components/data-render';
import { useWikiTocs } from 'data/wiki';
import { BubbleMenu } from '../../views/bubble-menu';
import { DocumentReference } from '../../extensions/document-reference';
import { Divider } from '../../divider';
export const DocumentReferenceBubbleMenu = ({ editor }) => {
const { pathname, query } = useRouter();
const wikiIdFromUrl = query?.wikiId;
const isShare = pathname.includes('share');
const { data: tocs, loading, error } = useWikiTocs(isShare ? null : wikiIdFromUrl);
const selectDoc = useCallback(
(item) => {
const { wikiId, title, id: documentId } = item;
editor
.chain()
.updateAttributes(DocumentReference.name, { wikiId, documentId, title })
.setNodeSelection(editor.state.selection.from)
.focus()
.run();
},
[editor]
);
const deleteNode = useCallback(() => editor.chain().deleteSelection().run(), [editor]);
return (
<BubbleMenu
className={'bubble-menu'}
editor={editor}
pluginKey="countdonw-bubble-menu"
shouldShow={() => editor.isActive(DocumentReference.name)}
tippyOptions={{ maxWidth: 456 }}
>
<Space>
<Popover
spacing={10}
content={
<DataRender
loading={loading}
error={error}
normalContent={() => (
<List
style={{ maxHeight: 320, overflow: 'auto' }}
dataSource={tocs}
renderItem={(item) => (
<List.Item
onClick={() => selectDoc(item)}
style={{ cursor: 'pointer' }}
main={<span style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>{item.title}</span>}
/>
)}
/>
)}
/>
}
trigger="click"
>
<Button size="small" type="tertiary" theme="borderless" icon={<IconEdit />} />
</Popover>
<Divider />
<Tooltip content="删除节点" hideOnClick>
<Button onClick={deleteNode} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
</Tooltip>
</Space>
</BubbleMenu>
);
};

View File

@ -0,0 +1,15 @@
import React from 'react';
import { Editor } from '@tiptap/core';
import { DocumentReferenceBubbleMenu } from './bubble';
export const DocumentReference: React.FC<{ editor: Editor }> = ({ editor }) => {
if (!editor) {
return null;
}
return (
<>
<DocumentReferenceBubbleMenu editor={editor} />
</>
);
};

View File

@ -1,5 +1,5 @@
.wrap { .wrap {
margin-top: .75em; margin-top: 0.75em;
border-radius: var(--border-radius); border-radius: var(--border-radius);
.itemWrap { .itemWrap {
@ -7,7 +7,6 @@
padding: 8px; padding: 8px;
color: var(--node-text-color); color: var(--node-text-color);
text-decoration: none; text-decoration: none;
border: 1px solid var(--node-border-color);
border-radius: var(--border-radius); border-radius: var(--border-radius);
align-items: center; align-items: center;
@ -27,21 +26,7 @@
color: var(--node-text-color); color: var(--node-text-color);
text-decoration: none; text-decoration: none;
cursor: not-allowed; cursor: not-allowed;
border: 1px solid var(--node-border-color);
border-radius: var(--border-radius); border-radius: var(--border-radius);
align-items: center; align-items: center;
} }
&.isEditable {
padding: 12px;
.itemWrap {
margin-top: 12px;
pointer-events: none;
}
.empty {
margin-top: 12px;
}
}
} }

View File

@ -2,56 +2,35 @@ import { NodeViewWrapper } from '@tiptap/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import Link from 'next/link'; import Link from 'next/link';
import cls from 'classnames'; import cls from 'classnames';
import { Select } from '@douyinfe/semi-ui';
import { useWikiTocs } from 'data/wiki';
import { DataRender } from 'components/data-render';
import { IconDocument } from 'components/icons'; import { IconDocument } from 'components/icons';
import styles from './index.module.scss'; import styles from './index.module.scss';
import { useMemo } from 'react';
export const DocumentReferenceWrapper = ({ editor, node, updateAttributes }) => { export const DocumentReferenceWrapper = ({ editor, node, updateAttributes }) => {
const { pathname, query } = useRouter(); const { pathname } = useRouter();
const wikiIdFromUrl = query?.wikiId;
const isShare = pathname.includes('share'); const isShare = pathname.includes('share');
const isEditable = editor.isEditable; const isEditable = editor.isEditable;
const { wikiId, documentId, title } = node.attrs; const { wikiId, documentId, title } = node.attrs;
const { data: tocs, loading, error } = useWikiTocs(isShare ? null : wikiIdFromUrl);
const selectDoc = (str) => { const content = useMemo(() => {
const [wikiId, title, documentId] = str.split('/'); if (!wikiId && !documentId) {
updateAttributes({ wikiId, documentId, title }); return (
}; <div className={cls(styles.empty, !isEditable && 'render-wrapper')}>
<span>{'用户未选择文档'}</span>
</div>
);
}
if (isEditable) {
return (
<div className={cls(styles.itemWrap)}>
<IconDocument />
<span>{title}</span>
</div>
);
}
return ( return (
<NodeViewWrapper
as="div"
className={cls(styles.wrap, isEditable && styles.isEditable, isEditable && 'render-wrapper')}
>
<div>
{isEditable && (
<DataRender
loading={loading}
error={error}
normalContent={() => (
<Select
placeholder="请选择文档"
onChange={(v) => selectDoc(v)}
style={{ maxWidth: 180 }}
{...(wikiId && documentId ? { value: `${wikiId}/${title}/${documentId}` } : {})}
>
{(tocs || []).map((toc) => (
<Select.Option
// FIXME: semi-design 抄 antd抄的什么玩意
label={`${toc.title}/${toc.id}`}
value={`${toc.wikiId}/${toc.title}/${toc.id}`}
>
{toc.title}
</Select.Option>
))}
</Select>
)}
/>
)}
{wikiId && documentId ? (
<Link <Link
key={documentId} key={documentId}
href={{ href={{
@ -64,12 +43,12 @@ export const DocumentReferenceWrapper = ({ editor, node, updateAttributes }) =>
<span>{title || '请选择文档'}</span> <span>{title || '请选择文档'}</span>
</a> </a>
</Link> </Link>
) : ( );
<div className={cls(styles.empty, !isEditable && 'render-wrapper')}> }, [wikiId, documentId]);
<span>{'用户未选择文档'}</span>
</div> return (
)} <NodeViewWrapper as="div" className={cls(styles.wrap, isEditable && 'render-wrapper')}>
</div> {content}
</NodeViewWrapper> </NodeViewWrapper>
); );
}; };