refactor: improve render performence

This commit is contained in:
fantasticit 2022-05-02 13:57:56 +08:00
parent 5681cc7bd8
commit 5c0d9f54e4
52 changed files with 569 additions and 530 deletions

View File

@ -64,23 +64,26 @@ export const Editor: React.FC<IProps> = ({ user: currentUser, documentId, author
}, },
}); });
}, [documentId, currentUser, toggleLoading]); }, [documentId, currentUser, toggleLoading]);
const editor = useEditor({ const editor = useEditor(
editable: authority && authority.editable, {
extensions: [ editable: authority && authority.editable,
...BaseKit, extensions: [
DocumentWithTitle, ...BaseKit,
getCollaborationExtension(provider), DocumentWithTitle,
getCollaborationCursorExtension(provider, currentUser), getCollaborationExtension(provider),
], getCollaborationCursorExtension(provider, currentUser),
onTransaction: debounce(({ transaction }) => { ],
try { onTransaction: debounce(({ transaction }) => {
const title = transaction.doc.content.firstChild.content.firstChild.textContent; try {
triggerChangeDocumentTitle(title); const title = transaction.doc.content.firstChild.content.firstChild.textContent;
} catch (e) { triggerChangeDocumentTitle(title);
// } catch (e) {
} //
}, 50), }
}); }, 50),
},
[authority, provider]
);
const [mentionUsersSettingVisible, toggleMentionUsersSettingVisible] = useToggle(false); const [mentionUsersSettingVisible, toggleMentionUsersSettingVisible] = useToggle(false);
const [mentionUsers, setMentionUsers] = useState([]); const [mentionUsers, setMentionUsers] = useState([]);

View File

@ -120,7 +120,13 @@ export const createKeysLocalStorageLRUCache = (storageKey, capacity) => {
const lruCache = new LRUCache(capacity); const lruCache = new LRUCache(capacity);
if (USED_STORAGE_KEYS.includes(storageKey)) { if (USED_STORAGE_KEYS.includes(storageKey)) {
throw new Error(`Storage Key ${storageKey} has been used!`); // @ts-ignore
if (module.hot) {
console.error(`Storage Key ${storageKey} has been used!`);
return;
} else {
throw new Error(`Storage Key ${storageKey} has been used!`);
}
} }
USED_STORAGE_KEYS.push(storageKey); USED_STORAGE_KEYS.push(storageKey);

View File

@ -1,4 +1,6 @@
export const Divider = ({ vertical = false }) => { import React from 'react';
export const _Divider = ({ vertical = false }) => {
return ( return (
<div <div
style={{ style={{
@ -12,3 +14,7 @@ export const Divider = ({ vertical = false }) => {
></div> ></div>
); );
}; };
export const Divider = React.memo(_Divider, (prevProps, nextProps) => {
return prevProps.vertical === nextProps.vertical;
});

View File

@ -3,21 +3,21 @@ import { sinkListItem, liftListItem } from 'prosemirror-schema-list';
import { TextSelection, AllSelection, Transaction } from 'prosemirror-state'; import { TextSelection, AllSelection, Transaction } from 'prosemirror-state';
import { isListActive, isListNode, clamp, getNodeType } from 'tiptap/prose-utils'; import { isListActive, isListNode, clamp, getNodeType } from 'tiptap/prose-utils';
declare module '@tiptap/core' {
interface Commands<ReturnType> {
indent: {
indent: () => ReturnType;
outdent: () => ReturnType;
};
}
}
type IndentOptions = { type IndentOptions = {
types: string[]; types: string[];
indentLevels: number[]; indentLevels: number[];
defaultIndentLevel: number; defaultIndentLevel: number;
}; };
declare module '@tiptap/core' {
interface Commands {
indent: {
indent: () => Command;
outdent: () => Command;
};
}
}
export enum IndentProps { export enum IndentProps {
min = 0, min = 0,
max = 210, max = 210,

View File

@ -43,6 +43,7 @@ interface SearchOptions {
searchResultCurrentClass: string; searchResultCurrentClass: string;
caseSensitive: boolean; caseSensitive: boolean;
disableRegex: boolean; disableRegex: boolean;
onChange?: () => void;
} }
interface TextNodesWithPosition { interface TextNodesWithPosition {
@ -216,6 +217,7 @@ export const SearchNReplace = Extension.create<SearchOptions>({
searchResultCurrentClass: 'search-result-current', searchResultCurrentClass: 'search-result-current',
caseSensitive: false, caseSensitive: false,
disableRegex: false, disableRegex: false,
onChange: () => {},
}; };
}, },
@ -279,7 +281,7 @@ export const SearchNReplace = Extension.create<SearchOptions>({
const { currentIndex, results, searchResultCurrentClass } = this.options; const { currentIndex, results, searchResultCurrentClass } = this.options;
const nextIndex = (currentIndex + results.length - 1) % results.length; const nextIndex = (currentIndex + results.length - 1) % results.length;
this.options.currentIndex = nextIndex; this.options.currentIndex = nextIndex;
this.options.onChange && this.options.onChange();
return gotoSearchResult({ return gotoSearchResult({
view, view,
tr, tr,
@ -294,7 +296,7 @@ export const SearchNReplace = Extension.create<SearchOptions>({
const { currentIndex, results, searchResultCurrentClass } = this.options; const { currentIndex, results, searchResultCurrentClass } = this.options;
const nextIndex = (currentIndex + 1) % results.length; const nextIndex = (currentIndex + 1) % results.length;
this.options.currentIndex = nextIndex; this.options.currentIndex = nextIndex;
this.options.onChange && this.options.onChange();
return gotoSearchResult({ return gotoSearchResult({
view, view,
tr, tr,
@ -331,6 +333,10 @@ export const SearchNReplace = Extension.create<SearchOptions>({
); );
extensionThis.options.results = results; extensionThis.options.results = results;
if (results.length && searchTerm) {
extensionThis.options.onChange && extensionThis.options.onChange();
}
if (ctx.getMeta('directDecoration')) { if (ctx.getMeta('directDecoration')) {
const { fromPos, toPos, attrs } = ctx.getMeta('directDecoration'); const { fromPos, toPos, attrs } = ctx.getMeta('directDecoration');
decorationsToReturn.push(Decoration.inline(fromPos, toPos, attrs)); decorationsToReturn.push(Decoration.inline(fromPos, toPos, attrs));

View File

@ -0,0 +1,22 @@
import React, { useEffect } from 'react';
import { Editor } from '@tiptap/core';
import { useToggle } from 'hooks/use-toggle';
export const useActive = (editor: Editor, ...args) => {
const [active, toggleActive] = useToggle(false);
useEffect(() => {
const listener = () => {
// eslint-disable-next-line prefer-spread
toggleActive(editor.isActive.apply(editor, args));
};
editor.on('selectionUpdate', listener);
return () => {
editor.off('selectionUpdate', listener);
};
}, [editor, args, toggleActive]);
return active;
};

View File

@ -0,0 +1,48 @@
import React, { useEffect, useRef, useState } from 'react';
import { Editor } from '@tiptap/core';
import deepEqual from 'deep-equal';
type MapFn<T, R> = (arg: T) => R;
function mapSelf<T>(d: T): T {
return d;
}
export function useAttributes<T extends Record<string, unknown>, R>(
editor: Editor,
attrbute: string,
defaultValue?: T,
map?: (arg: T) => R
) {
const mapFn = (map || mapSelf) as MapFn<T, R>;
const [value, setValue] = useState<R>(mapFn(defaultValue));
const prevValueCache = useRef<R>(value);
useEffect(() => {
const listener = () => {
const attrs = { ...defaultValue, ...editor.getAttributes(attrbute) };
Object.keys(attrs).forEach((key) => {
if (attrs[key] === null || attrs[key] === undefined) {
// @ts-ignore
attrs[key] = defaultValue[key];
}
});
const nextAttrs = mapFn(attrs);
if (deepEqual(prevValueCache.current, nextAttrs)) {
return;
}
setValue(nextAttrs);
prevValueCache.current = nextAttrs;
};
editor.on('selectionUpdate', listener);
editor.on('transaction', listener);
return () => {
editor.off('selectionUpdate', listener);
editor.off('transaction', listener);
};
}, [editor, defaultValue, attrbute, mapFn]);
return value;
}

View File

@ -44,6 +44,8 @@ import { Table } from './menus/table';
import { Mind } from './menus/mind'; import { Mind } from './menus/mind';
const _MenuBar: React.FC<{ editor: any }> = ({ editor }) => { const _MenuBar: React.FC<{ editor: any }> = ({ editor }) => {
if (!editor) return null;
return ( return (
<div> <div>
<Space spacing={2}> <Space spacing={2}>

View File

@ -1,26 +1,35 @@
import React from 'react'; import React, { useCallback, useMemo } from 'react';
import { Editor } from '@tiptap/core'; import { Editor } from '@tiptap/core';
import { Button, Dropdown, Tooltip } from '@douyinfe/semi-ui'; import { Button, Dropdown, Tooltip } from '@douyinfe/semi-ui';
import { IconAlignLeft, IconAlignCenter, IconAlignRight, IconAlignJustify } from '@douyinfe/semi-icons'; import { IconAlignLeft, IconAlignCenter, IconAlignRight, IconAlignJustify } from '@douyinfe/semi-icons';
import { isTitleActive } from 'tiptap/prose-utils'; import { useActive } from 'tiptap/hooks/use-active';
import { Title } from 'tiptap/extensions/title';
export const Align: React.FC<{ editor: Editor }> = ({ editor }) => { export const Align: React.FC<{ editor: Editor }> = ({ editor }) => {
const current = (() => { const isTitleActive = useActive(editor, Title.name);
if (editor.isActive({ textAlign: 'center' })) { const isAlignCenter = useActive(editor, { textAlign: 'center' });
const isAlignRight = useActive(editor, { textAlign: 'right' });
const isAlignJustify = useActive(editor, { textAlign: 'justify' });
const current = useMemo(() => {
if (isAlignCenter) {
return <IconAlignCenter />; return <IconAlignCenter />;
} }
if (editor.isActive({ textAlign: 'right' })) { if (isAlignRight) {
return <IconAlignRight />; return <IconAlignRight />;
} }
if (editor.isActive({ textAlign: 'justify' })) { if (isAlignJustify) {
return <IconAlignJustify />; return <IconAlignJustify />;
} }
return <IconAlignLeft />; return <IconAlignLeft />;
})(); }, [isAlignCenter, isAlignRight, isAlignJustify]);
const toggle = (align) => { const toggle = useCallback(
return () => editor.chain().focus().setTextAlign(align).run(); (align) => {
}; return () => editor.chain().focus().setTextAlign(align).run();
},
[editor]
);
return ( return (
<Dropdown <Dropdown
@ -47,7 +56,7 @@ export const Align: React.FC<{ editor: Editor }> = ({ editor }) => {
> >
<span> <span>
<Tooltip content="对齐方式" spacing={6}> <Tooltip content="对齐方式" spacing={6}>
<Button type="tertiary" theme="borderless" icon={current} disabled={isTitleActive(editor)}></Button> <Button type="tertiary" theme="borderless" icon={current} disabled={isTitleActive}></Button>
</Tooltip> </Tooltip>
</span> </span>
</Dropdown> </Dropdown>

View File

@ -5,37 +5,29 @@ import { BubbleMenu } from 'tiptap/views/bubble-menu';
import { Attachment } from 'tiptap/extensions/attachment'; import { Attachment } from 'tiptap/extensions/attachment';
import { copyNode, deleteNode } from 'tiptap/prose-utils'; import { copyNode, deleteNode } from 'tiptap/prose-utils';
import { Divider } from 'tiptap/divider'; import { Divider } from 'tiptap/divider';
import { useCallback } from 'react';
export const AttachmentBubbleMenu = ({ editor }) => { export const AttachmentBubbleMenu = ({ editor }) => {
const copyMe = useCallback(() => copyNode(Attachment.name, editor), [editor]);
const deleteMe = useCallback(() => deleteNode(Attachment.name, editor), [editor]);
return ( return (
<BubbleMenu <BubbleMenu
className={'bubble-menu'} className={'bubble-menu'}
editor={editor} editor={editor}
pluginKey="document-children-bubble-menu" pluginKey="attachment-bubble-menu"
shouldShow={() => editor.isActive(Attachment.name)} shouldShow={() => editor.isActive(Attachment.name)}
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }} tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
> >
<Space> <Space spacing={4}>
<Tooltip content="复制"> <Tooltip content="复制">
<Button <Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
onClick={() => copyNode(Attachment.name, editor)}
icon={<IconCopy />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip> </Tooltip>
<Divider /> <Divider />
<Tooltip content="删除节点" hideOnClick> <Tooltip content="删除节点" hideOnClick>
<Button <Button onClick={deleteMe} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
onClick={() => deleteNode(Attachment.name, editor)}
icon={<IconDelete />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip> </Tooltip>
</Space> </Space>
</BubbleMenu> </BubbleMenu>

View File

@ -3,10 +3,6 @@ import { Editor } from '@tiptap/core';
import { AttachmentBubbleMenu } from './bubble'; import { AttachmentBubbleMenu } from './bubble';
export const Attachment: React.FC<{ editor: Editor }> = ({ editor }) => { export const Attachment: React.FC<{ editor: Editor }> = ({ editor }) => {
if (!editor) {
return null;
}
return ( return (
<> <>
<AttachmentBubbleMenu editor={editor} /> <AttachmentBubbleMenu editor={editor} />

View File

@ -1,40 +1,50 @@
import React from 'react'; import React, { useCallback } from 'react';
import { Editor } from '@tiptap/core'; import { Editor } from '@tiptap/core';
import { Button } from '@douyinfe/semi-ui'; import { Button } from '@douyinfe/semi-ui';
import { IconMark } from '@douyinfe/semi-icons'; import { IconMark } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { isTitleActive } from 'tiptap/prose-utils'; import { useAttributes } from 'tiptap/hooks/use-attributes';
import { useActive } from 'tiptap/hooks/use-active';
import { Title } from 'tiptap/extensions/title';
import { ColorPicker } from '../_components/color-picker'; import { ColorPicker } from '../_components/color-picker';
const FlexStyle: React.CSSProperties = {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
};
export const BackgroundColor: React.FC<{ editor: Editor }> = ({ editor }) => { export const BackgroundColor: React.FC<{ editor: Editor }> = ({ editor }) => {
const { backgroundColor } = editor.getAttributes('textStyle'); const backgroundColor = useAttributes(
editor,
'textStyle',
{ backgroundColor: 'transparent' },
(attrs) => attrs.backgroundColor
);
const isTitleActive = useActive(editor, Title.name);
const setBackgroundColor = useCallback(
(color) => {
color
? editor.chain().focus().setBackgroundColor(color).run()
: editor.chain().focus().unsetBackgroundColor().run();
},
[editor]
);
return ( return (
<ColorPicker <ColorPicker onSetColor={setBackgroundColor} disabled={isTitleActive}>
onSetColor={(color) => {
color
? editor.chain().focus().setBackgroundColor(color).run()
: editor.chain().focus().unsetBackgroundColor().run();
}}
disabled={isTitleActive(editor)}
>
<Tooltip content="背景色"> <Tooltip content="背景色">
<Button <Button
theme={editor.isActive('textStyle') ? 'light' : 'borderless'} theme={editor.isActive('textStyle') ? 'light' : 'borderless'}
type={'tertiary'} type={'tertiary'}
icon={ icon={
<div <div style={FlexStyle}>
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<IconMark /> <IconMark />
<span style={{ backgroundColor, width: 12, height: 2 }}></span> <span style={{ backgroundColor, width: 12, height: 2 }}></span>
</div> </div>
} }
disabled={isTitleActive(editor)} disabled={isTitleActive}
/> />
</Tooltip> </Tooltip>
</ColorPicker> </ColorPicker>

View File

@ -1,24 +1,26 @@
import React from 'react'; import React, { useCallback } from 'react';
import { Editor } from '@tiptap/core'; import { Editor } from '@tiptap/core';
import { Button } from '@douyinfe/semi-ui'; import { Button } from '@douyinfe/semi-ui';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { IconQuote } from 'components/icons'; import { IconQuote } from 'components/icons';
import { isTitleActive } from 'tiptap/prose-utils'; import { useActive } from 'tiptap/hooks/use-active';
import { Title } from 'tiptap/extensions/title';
import { Blockquote as BlockquoteExtension } from 'tiptap/extensions/blockquote';
export const Blockquote: React.FC<{ editor: Editor }> = ({ editor }) => { export const Blockquote: React.FC<{ editor: Editor }> = ({ editor }) => {
if (!editor) { const isTitleActive = useActive(editor, Title.name);
return null; const isBlockquoteActive = useActive(editor, BlockquoteExtension.name);
}
const toggleBlockquote = useCallback(() => editor.chain().focus().toggleBlockquote().run(), [editor]);
return ( return (
<Tooltip content="插入引用"> <Tooltip content="插入引用">
<Button <Button
theme={editor.isActive('blockquote') ? 'light' : 'borderless'} theme={isBlockquoteActive ? 'light' : 'borderless'}
type="tertiary" type="tertiary"
icon={<IconQuote />} icon={<IconQuote />}
onClick={() => editor.chain().focus().toggleBlockquote().run()} onClick={toggleBlockquote}
className={editor.isActive('blockquote') ? 'is-active' : ''} disabled={isTitleActive}
disabled={isTitleActive(editor)}
/> />
</Tooltip> </Tooltip>
); );

View File

@ -3,17 +3,22 @@ import { Editor } from '@tiptap/core';
import { Button } from '@douyinfe/semi-ui'; import { Button } from '@douyinfe/semi-ui';
import { IconBold } from '@douyinfe/semi-icons'; import { IconBold } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { isTitleActive } from 'tiptap/prose-utils'; import { useActive } from 'tiptap/hooks/use-active';
import { Title } from 'tiptap/extensions/title';
import { Bold as BoldExtension } from 'tiptap/extensions/bold';
export const Bold: React.FC<{ editor: Editor }> = ({ editor }) => { export const Bold: React.FC<{ editor: Editor }> = ({ editor }) => {
const isTitleActive = useActive(editor, Title.name);
const isBoldActive = useActive(editor, BoldExtension.name);
return ( return (
<Tooltip content="粗体"> <Tooltip content="粗体">
<Button <Button
theme={editor.isActive('bold') ? 'light' : 'borderless'} theme={isBoldActive ? 'light' : 'borderless'}
type="tertiary" type="tertiary"
icon={<IconBold />} icon={<IconBold />}
onClick={() => editor.chain().focus().toggleBold().run()} onClick={() => editor.chain().focus().toggleBold().run()}
disabled={isTitleActive(editor)} disabled={isTitleActive}
/> />
</Tooltip> </Tooltip>
); );

View File

@ -1,23 +1,26 @@
import React from 'react'; import React, { useCallback } from 'react';
import { Editor } from '@tiptap/core'; import { Editor } from '@tiptap/core';
import { Button } from '@douyinfe/semi-ui'; import { Button } from '@douyinfe/semi-ui';
import { IconList } from 'components/icons'; import { IconList } from 'components/icons';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { isTitleActive } from 'tiptap/prose-utils'; import { useActive } from 'tiptap/hooks/use-active';
import { Title } from 'tiptap/extensions/title';
import { BulletList as BulletListExtension } from 'tiptap/extensions/bullet-list';
export const BulletList: React.FC<{ editor: Editor }> = ({ editor }) => { export const BulletList: React.FC<{ editor: Editor }> = ({ editor }) => {
if (!editor) { const isTitleActive = useActive(editor, Title.name);
return null; const isBulletListActive = useActive(editor, BulletListExtension.name);
}
const toggleBulletList = useCallback(() => editor.chain().focus().toggleBulletList().run(), [editor]);
return ( return (
<Tooltip content="无序列表"> <Tooltip content="无序列表">
<Button <Button
theme={editor.isActive('bulletList') ? 'light' : 'borderless'} theme={isBulletListActive ? 'light' : 'borderless'}
type="tertiary" type="tertiary"
icon={<IconList />} icon={<IconList />}
onClick={() => editor.chain().focus().toggleBulletList().run()} onClick={toggleBulletList}
disabled={isTitleActive(editor)} disabled={isTitleActive}
/> />
</Tooltip> </Tooltip>
); );

View File

@ -32,23 +32,20 @@ export const CalloutBubbleMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
[editor] [editor]
); );
const copyMe = useCallback(() => copyNode(Callout.name, editor), [editor]);
const deleteMe = useCallback(() => deleteNode(Callout.name, editor), [editor]);
return ( return (
<BubbleMenu <BubbleMenu
className={'bubble-menu'} className={'bubble-menu'}
editor={editor} editor={editor}
pluginKey="banner-bubble-menu" pluginKey="calloyt-bubble-menu"
shouldShow={() => editor.isActive(Callout.name)} shouldShow={() => editor.isActive(Callout.name)}
matchRenderContainer={(node) => node && node.id === 'js-bannber-container'} matchRenderContainer={(node) => node && node.id === 'js-bannber-container'}
> >
<Space> <Space spacing={4}>
<Tooltip content="复制"> <Tooltip content="复制">
<Button <Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
onClick={() => copyNode(Callout.name, editor)}
icon={<IconCopy />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip> </Tooltip>
<Popover <Popover
@ -103,15 +100,11 @@ export const CalloutBubbleMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
> >
<Button icon={<IconDrawBoard />} type="tertiary" theme="borderless" size="small" /> <Button icon={<IconDrawBoard />} type="tertiary" theme="borderless" size="small" />
</Popover> </Popover>
<Divider /> <Divider />
<Tooltip content="删除" hideOnClick> <Tooltip content="删除" hideOnClick>
<Button <Button size="small" type="tertiary" theme="borderless" icon={<IconDelete />} onClick={deleteMe} />
size="small"
type="tertiary"
theme="borderless"
icon={<IconDelete />}
onClick={() => deleteNode(Callout.name, editor)}
/>
</Tooltip> </Tooltip>
</Space> </Space>
</BubbleMenu> </BubbleMenu>

View File

@ -3,10 +3,6 @@ import { Editor } from '@tiptap/core';
import { CalloutBubbleMenu } from './bubble'; import { CalloutBubbleMenu } from './bubble';
export const Callout: React.FC<{ editor: Editor }> = ({ editor }) => { export const Callout: React.FC<{ editor: Editor }> = ({ editor }) => {
if (!editor) {
return null;
}
return ( return (
<> <>
<CalloutBubbleMenu editor={editor} /> <CalloutBubbleMenu editor={editor} />

View File

@ -1,21 +1,18 @@
import React from 'react'; import React, { useCallback } from 'react';
import { Editor } from '@tiptap/core'; import { Editor } from '@tiptap/core';
import { Button } from '@douyinfe/semi-ui'; import { Button } from '@douyinfe/semi-ui';
import { IconClear } from 'components/icons'; import { IconClear } from 'components/icons';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
export const CleadrNodeAndMarks: React.FC<{ editor: Editor }> = ({ editor }) => { export const CleadrNodeAndMarks: React.FC<{ editor: Editor }> = ({ editor }) => {
const clear = useCallback(() => {
editor.chain().focus().unsetAllMarks().run();
editor.chain().focus().clearNodes().run();
}, [editor]);
return ( return (
<Tooltip content="清除格式"> <Tooltip content="清除格式">
<Button <Button onClick={clear} icon={<IconClear />} type="tertiary" theme="borderless" />
onClick={() => {
editor.chain().focus().unsetAllMarks().run();
editor.chain().focus().clearNodes().run();
}}
icon={<IconClear />}
type="tertiary"
theme="borderless"
/>
</Tooltip> </Tooltip>
); );
}; };

View File

@ -1,3 +1,4 @@
import React, { useCallback } from 'react';
import { Space, Button } from '@douyinfe/semi-ui'; import { Space, Button } from '@douyinfe/semi-ui';
import { IconCopy, IconDelete } from '@douyinfe/semi-icons'; import { IconCopy, IconDelete } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
@ -7,36 +8,27 @@ import { copyNode, deleteNode } from 'tiptap/prose-utils';
import { Divider } from 'tiptap/divider'; import { Divider } from 'tiptap/divider';
export const CodeBlockBubbleMenu = ({ editor }) => { export const CodeBlockBubbleMenu = ({ editor }) => {
const copyMe = useCallback(() => copyNode(CodeBlock.name, editor), [editor]);
const deleteMe = useCallback(() => deleteNode(CodeBlock.name, editor), [editor]);
return ( return (
<BubbleMenu <BubbleMenu
className={'bubble-menu'} className={'bubble-menu'}
editor={editor} editor={editor}
pluginKey="document-children-bubble-menu" pluginKey="code-block-bubble-menu"
shouldShow={() => editor.isActive(CodeBlock.name)} shouldShow={() => editor.isActive(CodeBlock.name)}
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }} tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
matchRenderContainer={(node: HTMLElement) => node && node.classList && node.classList.contains('node-codeBlock')} matchRenderContainer={(node: HTMLElement) => node && node.classList && node.classList.contains('node-codeBlock')}
> >
<Space> <Space spacing={4}>
<Tooltip content="复制"> <Tooltip content="复制">
<Button <Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
onClick={() => copyNode(CodeBlock.name, editor)}
icon={<IconCopy />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip> </Tooltip>
<Divider /> <Divider />
<Tooltip content="删除节点" hideOnClick> <Tooltip content="删除节点" hideOnClick>
<Button <Button onClick={deleteMe} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
onClick={() => deleteNode(CodeBlock.name, editor)}
icon={<IconDelete />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip> </Tooltip>
</Space> </Space>
</BubbleMenu> </BubbleMenu>

View File

@ -3,10 +3,6 @@ import { Editor } from '@tiptap/core';
import { CodeBlockBubbleMenu } from './bubble'; import { CodeBlockBubbleMenu } from './bubble';
export const CodeBlock: React.FC<{ editor: Editor }> = ({ editor }) => { export const CodeBlock: React.FC<{ editor: Editor }> = ({ editor }) => {
if (!editor) {
return null;
}
return ( return (
<> <>
<CodeBlockBubbleMenu editor={editor} /> <CodeBlockBubbleMenu editor={editor} />

View File

@ -3,17 +3,22 @@ import { Editor } from '@tiptap/core';
import { Button } from '@douyinfe/semi-ui'; import { Button } from '@douyinfe/semi-ui';
import { IconCode } from '@douyinfe/semi-icons'; import { IconCode } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { isTitleActive } from 'tiptap/prose-utils'; import { useActive } from 'tiptap/hooks/use-active';
import { Title } from 'tiptap/extensions/title';
import { Code as InlineCode } from 'tiptap/extensions/code';
export const Code: React.FC<{ editor: Editor }> = ({ editor }) => { export const Code: React.FC<{ editor: Editor }> = ({ editor }) => {
const isTitleActive = useActive(editor, Title.name);
const isCodeActive = useActive(editor, InlineCode.name);
return ( return (
<Tooltip content="行内代码"> <Tooltip content="行内代码">
<Button <Button
theme={editor.isActive('code') ? 'light' : 'borderless'} theme={isCodeActive ? 'light' : 'borderless'}
type="tertiary" type="tertiary"
icon={<IconCode />} icon={<IconCode />}
onClick={() => editor.chain().focus().toggleCode().run()} onClick={() => editor.chain().focus().toggleCode().run()}
disabled={isTitleActive(editor)} disabled={isTitleActive}
/> />
</Tooltip> </Tooltip>
); );

View File

@ -6,15 +6,19 @@ import { BubbleMenu } from 'tiptap/views/bubble-menu';
import { Countdown } from 'tiptap/extensions/countdown'; import { Countdown } from 'tiptap/extensions/countdown';
import { Divider } from 'tiptap/divider'; import { Divider } from 'tiptap/divider';
import { copyNode, deleteNode } from 'tiptap/prose-utils'; import { copyNode, deleteNode } from 'tiptap/prose-utils';
import { useAttributes } from 'tiptap/hooks/use-attributes';
import { triggerOpenCountSettingModal } from '../_event'; import { triggerOpenCountSettingModal } from '../_event';
export const CountdownBubbleMenu = ({ editor }) => { export const CountdownBubbleMenu = ({ editor }) => {
const attrs = editor.getAttributes(Countdown.name); const attrs = useAttributes(editor, Countdown.name, {});
const openEditLinkModal = useCallback(() => { const openEditLinkModal = useCallback(() => {
triggerOpenCountSettingModal(attrs); triggerOpenCountSettingModal(attrs);
}, [attrs]); }, [attrs]);
const copyMe = useCallback(() => copyNode(Countdown.name, editor), [editor]);
const deleteMe = useCallback(() => deleteNode(Countdown.name, editor), [editor]);
return ( return (
<BubbleMenu <BubbleMenu
className={'bubble-menu'} className={'bubble-menu'}
@ -23,15 +27,9 @@ export const CountdownBubbleMenu = ({ editor }) => {
shouldShow={() => editor.isActive(Countdown.name)} shouldShow={() => editor.isActive(Countdown.name)}
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }} tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
> >
<Space> <Space spacing={4}>
<Tooltip content="复制"> <Tooltip content="复制">
<Button <Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
onClick={() => copyNode(Countdown.name, editor)}
icon={<IconCopy />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip> </Tooltip>
<Tooltip content="编辑"> <Tooltip content="编辑">
@ -41,13 +39,7 @@ export const CountdownBubbleMenu = ({ editor }) => {
<Divider /> <Divider />
<Tooltip content="删除节点" hideOnClick> <Tooltip content="删除节点" hideOnClick>
<Button <Button onClick={deleteMe} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
onClick={() => deleteNode(Countdown.name, editor)}
icon={<IconDelete />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip> </Tooltip>
</Space> </Space>
</BubbleMenu> </BubbleMenu>

View File

@ -4,10 +4,6 @@ import { CountdownBubbleMenu } from './bubble';
import { CountdownSettingModal } from './modal'; import { CountdownSettingModal } from './modal';
export const Countdonw: React.FC<{ editor: Editor }> = ({ editor }) => { export const Countdonw: React.FC<{ editor: Editor }> = ({ editor }) => {
if (!editor) {
return null;
}
return ( return (
<> <>
<CountdownBubbleMenu editor={editor} /> <CountdownBubbleMenu editor={editor} />

View File

@ -1,3 +1,4 @@
import React, { useCallback } from 'react';
import { Space, Button } from '@douyinfe/semi-ui'; import { Space, Button } from '@douyinfe/semi-ui';
import { IconCopy, IconDelete } from '@douyinfe/semi-icons'; import { IconCopy, IconDelete } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
@ -7,6 +8,9 @@ import { copyNode, deleteNode } from 'tiptap/prose-utils';
import { Divider } from 'tiptap/divider'; import { Divider } from 'tiptap/divider';
export const DocumentChildrenBubbleMenu = ({ editor }) => { export const DocumentChildrenBubbleMenu = ({ editor }) => {
const copyMe = useCallback(() => copyNode(DocumentChildren.name, editor), [editor]);
const deleteMe = useCallback(() => deleteNode(DocumentChildren.name, editor), [editor]);
return ( return (
<BubbleMenu <BubbleMenu
className={'bubble-menu'} className={'bubble-menu'}
@ -15,27 +19,15 @@ export const DocumentChildrenBubbleMenu = ({ editor }) => {
shouldShow={() => editor.isActive(DocumentChildren.name)} shouldShow={() => editor.isActive(DocumentChildren.name)}
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }} tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
> >
<Space> <Space spacing={4}>
<Tooltip content="复制"> <Tooltip content="复制">
<Button <Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
onClick={() => copyNode(DocumentChildren.name, editor)}
icon={<IconCopy />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip> </Tooltip>
<Divider /> <Divider />
<Tooltip content="删除节点" hideOnClick> <Tooltip content="删除节点" hideOnClick>
<Button <Button onClick={deleteMe} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
onClick={() => deleteNode(DocumentChildren.name, editor)}
icon={<IconDelete />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip> </Tooltip>
</Space> </Space>
</BubbleMenu> </BubbleMenu>

View File

@ -3,10 +3,6 @@ import { Editor } from '@tiptap/core';
import { DocumentChildrenBubbleMenu } from './bubble'; import { DocumentChildrenBubbleMenu } from './bubble';
export const DocumentChildren: React.FC<{ editor: Editor }> = ({ editor }) => { export const DocumentChildren: React.FC<{ editor: Editor }> = ({ editor }) => {
if (!editor) {
return null;
}
return ( return (
<> <>
<DocumentChildrenBubbleMenu editor={editor} /> <DocumentChildrenBubbleMenu editor={editor} />

View File

@ -33,6 +33,9 @@ export const DocumentReferenceBubbleMenu = ({ editor }) => {
[editor] [editor]
); );
const copyMe = useCallback(() => copyNode(DocumentReference.name, editor), [editor]);
const deleteMe = useCallback(() => deleteNode(DocumentReference.name, editor), [editor]);
return ( return (
<BubbleMenu <BubbleMenu
className={'bubble-menu'} className={'bubble-menu'}
@ -41,15 +44,9 @@ export const DocumentReferenceBubbleMenu = ({ editor }) => {
shouldShow={() => editor.isActive(DocumentReference.name)} shouldShow={() => editor.isActive(DocumentReference.name)}
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }} tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
> >
<Space> <Space spacing={4}>
<Tooltip content="复制"> <Tooltip content="复制">
<Button <Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
onClick={() => copyNode(DocumentReference.name, editor)}
icon={<IconCopy />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip> </Tooltip>
<Popover <Popover
@ -97,13 +94,7 @@ export const DocumentReferenceBubbleMenu = ({ editor }) => {
<Divider /> <Divider />
<Tooltip content="删除节点" hideOnClick> <Tooltip content="删除节点" hideOnClick>
<Button <Button onClick={deleteMe} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
onClick={() => deleteNode(DocumentReference.name, editor)}
icon={<IconDelete />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip> </Tooltip>
</Space> </Space>
</BubbleMenu> </BubbleMenu>

View File

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

View File

@ -1,13 +1,17 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { Select } from '@douyinfe/semi-ui'; import { Select } from '@douyinfe/semi-ui';
import { Editor } from '@tiptap/core'; import { Editor } from '@tiptap/core';
import { isTitleActive } from 'tiptap/prose-utils'; import { useActive } from 'tiptap/hooks/use-active';
import { Title } from 'tiptap/extensions/title';
import { useAttributes } from 'tiptap/hooks/use-attributes';
export const FONT_SIZES = [12, 13, 14, 15, 16, 19, 22, 24, 29, 32, 40, 48]; export const FONT_SIZES = [12, 13, 14, 15, 16, 19, 22, 24, 29, 32, 40, 48];
export const FontSize: React.FC<{ editor: Editor }> = ({ editor }) => { export const FontSize: React.FC<{ editor: Editor }> = ({ editor }) => {
const currentFontSizePx = editor.getAttributes('textStyle').fontSize || '16px'; const isTitleActive = useActive(editor, Title.name);
const currentFontSize = +currentFontSizePx.replace('px', ''); const currentFontSize = useAttributes(editor, 'textStyle', { fontSize: '16px' }, (attrs) =>
attrs.fontSize.replace('px', '')
);
const toggle = useCallback( const toggle = useCallback(
(val) => { (val) => {
@ -21,12 +25,7 @@ export const FontSize: React.FC<{ editor: Editor }> = ({ editor }) => {
); );
return ( return (
<Select <Select disabled={isTitleActive} value={+currentFontSize} onChange={toggle} style={{ width: 80, marginRight: 10 }}>
disabled={isTitleActive(editor)}
value={currentFontSize}
onChange={toggle}
style={{ width: 80, marginRight: 10 }}
>
{FONT_SIZES.map((fontSize) => ( {FONT_SIZES.map((fontSize) => (
<Select.Option key={fontSize} value={fontSize}> <Select.Option key={fontSize} value={fontSize}>
{fontSize}px {fontSize}px

View File

@ -1,19 +1,27 @@
import React, { useCallback } from 'react'; import React, { useCallback, useMemo } from 'react';
import { Editor } from '@tiptap/core'; import { Editor } from '@tiptap/core';
import { Select } from '@douyinfe/semi-ui'; import { Select } from '@douyinfe/semi-ui';
import { isTitleActive } from 'tiptap/prose-utils'; import { useActive } from 'tiptap/hooks/use-active';
import { Title } from 'tiptap/extensions/title';
const getCurrentCaretTitle = (editor) => {
if (editor.isActive('heading', { level: 1 })) return 1;
if (editor.isActive('heading', { level: 2 })) return 2;
if (editor.isActive('heading', { level: 3 })) return 3;
if (editor.isActive('heading', { level: 4 })) return 4;
if (editor.isActive('heading', { level: 5 })) return 5;
if (editor.isActive('heading', { level: 6 })) return 6;
return 'paragraph';
};
export const Heading: React.FC<{ editor: Editor }> = ({ editor }) => { export const Heading: React.FC<{ editor: Editor }> = ({ editor }) => {
const isTitleActive = useActive(editor, Title.name);
const isH1 = useActive(editor, 'heading', { level: 1 });
const isH2 = useActive(editor, 'heading', { level: 2 });
const isH3 = useActive(editor, 'heading', { level: 3 });
const isH4 = useActive(editor, 'heading', { level: 4 });
const isH5 = useActive(editor, 'heading', { level: 5 });
const isH6 = useActive(editor, 'heading', { level: 6 });
const current = useMemo(() => {
if (isH1) return 1;
if (isH2) return 2;
if (isH3) return 3;
if (isH4) return 4;
if (isH5) return 5;
if (isH6) return 6;
return 'paragraph';
}, [isH1, isH2, isH3, isH4, isH5, isH6]);
const toggle = useCallback( const toggle = useCallback(
(level) => { (level) => {
if (level === 'paragraph') { if (level === 'paragraph') {
@ -26,12 +34,7 @@ export const Heading: React.FC<{ editor: Editor }> = ({ editor }) => {
); );
return ( return (
<Select <Select disabled={isTitleActive} value={current} onChange={toggle} style={{ width: 90, marginRight: 10 }}>
disabled={isTitleActive(editor)}
value={getCurrentCaretTitle(editor)}
onChange={toggle}
style={{ width: 90, marginRight: 10 }}
>
<Select.Option value="paragraph"></Select.Option> <Select.Option value="paragraph"></Select.Option>
<Select.Option value={1}> <Select.Option value={1}>
<h1 style={{ margin: 0, fontSize: '1.3em' }}>1</h1> <h1 style={{ margin: 0, fontSize: '1.3em' }}>1</h1>

View File

@ -1,14 +1,15 @@
import React from 'react'; import React, { useCallback } from 'react';
import { Editor } from '@tiptap/core'; import { Editor } from '@tiptap/core';
import { Button } from '@douyinfe/semi-ui'; import { Button } from '@douyinfe/semi-ui';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { IconHorizontalRule } from 'components/icons'; import { IconHorizontalRule } from 'components/icons';
import { isTitleActive } from 'tiptap/prose-utils'; import { useActive } from 'tiptap/hooks/use-active';
import { Title } from 'tiptap/extensions/title';
export const HorizontalRule: React.FC<{ editor: Editor }> = ({ editor }) => { export const HorizontalRule: React.FC<{ editor: Editor }> = ({ editor }) => {
if (!editor) { const isTitleActive = useActive(editor, Title.name);
return null;
} const setHorizontalRule = useCallback(() => editor.chain().focus().setHorizontalRule().run(), [editor]);
return ( return (
<Tooltip content="插入分割线"> <Tooltip content="插入分割线">
@ -16,8 +17,8 @@ export const HorizontalRule: React.FC<{ editor: Editor }> = ({ editor }) => {
theme={'borderless'} theme={'borderless'}
type="tertiary" type="tertiary"
icon={<IconHorizontalRule />} icon={<IconHorizontalRule />}
onClick={() => editor.chain().focus().setHorizontalRule().run()} onClick={setHorizontalRule}
disabled={isTitleActive(editor)} disabled={isTitleActive}
/> />
</Tooltip> </Tooltip>
); );

View File

@ -1,40 +1,41 @@
import React from 'react'; import React, { useCallback } from 'react';
import { Editor } from '@tiptap/core'; import { Editor } from '@tiptap/core';
import { Button } from '@douyinfe/semi-ui'; import { Button } from '@douyinfe/semi-ui';
import { IconIndentLeft, IconIndentRight } from '@douyinfe/semi-icons'; import { IconIndentLeft, IconIndentRight } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { isTitleActive } from 'tiptap/prose-utils'; import { useActive } from 'tiptap/hooks/use-active';
import { Title } from 'tiptap/extensions/title';
export const Ident: React.FC<{ editor: Editor }> = ({ editor }) => { export const Ident: React.FC<{ editor: Editor }> = ({ editor }) => {
if (!editor) { const isTitleActive = useActive(editor, Title.name);
return null;
} const indent = useCallback(() => {
editor.chain().focus().indent().run();
}, [editor]);
const outdent = useCallback(() => {
editor.chain().focus().outdent().run();
}, [editor]);
return ( return (
<> <>
<Tooltip content="增加缩进"> <Tooltip content="增加缩进">
<Button <Button
onClick={() => { onClick={indent}
// @ts-ignore
editor.chain().focus().indent().run();
}}
icon={<IconIndentRight />} icon={<IconIndentRight />}
theme={'borderless'} theme={'borderless'}
type="tertiary" type="tertiary"
disabled={isTitleActive(editor)} disabled={isTitleActive}
/> />
</Tooltip> </Tooltip>
<Tooltip content="减少缩进"> <Tooltip content="减少缩进">
<Button <Button
onClick={() => { onClick={outdent}
// @ts-ignore
editor.chain().focus().outdent().run();
}}
icon={<IconIndentLeft />} icon={<IconIndentLeft />}
theme={'borderless'} theme={'borderless'}
type="tertiary" type="tertiary"
disabled={isTitleActive(editor)} disabled={isTitleActive}
/> />
</Tooltip> </Tooltip>
</> </>

View File

@ -7,6 +7,7 @@ import { Tooltip } from 'components/tooltip';
import { BubbleMenu } from 'tiptap/views/bubble-menu'; import { BubbleMenu } from 'tiptap/views/bubble-menu';
import { Iframe } from 'tiptap/extensions/iframe'; import { Iframe } from 'tiptap/extensions/iframe';
import { copyNode, deleteNode } from 'tiptap/prose-utils'; import { copyNode, deleteNode } from 'tiptap/prose-utils';
import { useAttributes } from 'tiptap/hooks/use-attributes';
import { Divider } from 'tiptap/divider'; import { Divider } from 'tiptap/divider';
import { Size } from '../_components/size'; import { Size } from '../_components/size';
@ -16,8 +17,7 @@ const EXAMPLE_LINK =
'https://proxy.tencentsuite.com/openapi/proxy/v2/addon?uid=144115212008575217&creator=144115212008575217&redirect=https%3A%2F%2Fi.y.qq.com%2Fn2%2Fm%2Foutchain%2Fplayer%2Findex.html%3Fsongid%3D5408217&docType=1&docID=300000000$RwqOunTcpXjs&addonID=0b69e1b9517e44a4aee35d33ee021b55&packageID=817&nonce=m3rqxn'; 'https://proxy.tencentsuite.com/openapi/proxy/v2/addon?uid=144115212008575217&creator=144115212008575217&redirect=https%3A%2F%2Fi.y.qq.com%2Fn2%2Fm%2Foutchain%2Fplayer%2Findex.html%3Fsongid%3D5408217&docType=1&docID=300000000$RwqOunTcpXjs&addonID=0b69e1b9517e44a4aee35d33ee021b55&packageID=817&nonce=m3rqxn';
export const IframeBubbleMenu = ({ editor }) => { export const IframeBubbleMenu = ({ editor }) => {
const attrs = editor.getAttributes(Iframe.name); const { width, height, url } = useAttributes(editor, Iframe.name, { width: 0, height: 0, url: '' });
const { width, height, url } = attrs;
const $form = useRef<FormApi>(); const $form = useRef<FormApi>();
const [visible, toggleVisible] = useToggle(false); const [visible, toggleVisible] = useToggle(false);
@ -58,11 +58,14 @@ export const IframeBubbleMenu = ({ editor }) => {
[editor] [editor]
); );
const copyMe = useCallback(() => copyNode(Iframe.name, editor), [editor]);
const deleteMe = useCallback(() => deleteNode(Iframe.name, editor), [editor]);
return ( return (
<BubbleMenu <BubbleMenu
className={'bubble-menu'} className={'bubble-menu'}
editor={editor} editor={editor}
pluginKey="link-bubble-menu" pluginKey="iframe-bubble-menu"
shouldShow={() => editor.isActive(Iframe.name)} shouldShow={() => editor.isActive(Iframe.name)}
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }} tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
> >
@ -93,15 +96,9 @@ export const IframeBubbleMenu = ({ editor }) => {
</Form> </Form>
</Modal> </Modal>
<Space> <Space spacing={4}>
<Tooltip content="复制"> <Tooltip content="复制">
<Button <Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
onClick={() => copyNode(Iframe.name, editor)}
icon={<IconCopy />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip> </Tooltip>
<Tooltip content="访问链接"> <Tooltip content="访问链接">
@ -121,13 +118,7 @@ export const IframeBubbleMenu = ({ editor }) => {
<Divider /> <Divider />
<Tooltip content="删除节点" hideOnClick> <Tooltip content="删除节点" hideOnClick>
<Button <Button onClick={deleteMe} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
onClick={() => deleteNode(Iframe.name, editor)}
icon={<IconDelete />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip> </Tooltip>
</Space> </Space>
</BubbleMenu> </BubbleMenu>

View File

@ -3,10 +3,6 @@ import { Editor } from '@tiptap/core';
import { IframeBubbleMenu } from './bubble'; import { IframeBubbleMenu } from './bubble';
export const Iframe: React.FC<{ editor: Editor }> = ({ editor }) => { export const Iframe: React.FC<{ editor: Editor }> = ({ editor }) => {
if (!editor) {
return null;
}
return ( return (
<> <>
<IframeBubbleMenu editor={editor} /> <IframeBubbleMenu editor={editor} />

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { Space, Button } from '@douyinfe/semi-ui'; import { Space, Button } from '@douyinfe/semi-ui';
import { import {
IconAlignLeft, IconAlignLeft,
@ -13,15 +13,58 @@ import { BubbleMenu } from 'tiptap/views/bubble-menu';
import { Divider } from 'tiptap/divider'; import { Divider } from 'tiptap/divider';
import { Image } from 'tiptap/extensions/image'; import { Image } from 'tiptap/extensions/image';
import { getEditorContainerDOMSize, copyNode, deleteNode } from 'tiptap/prose-utils'; import { getEditorContainerDOMSize, copyNode, deleteNode } from 'tiptap/prose-utils';
import { useAttributes } from 'tiptap/hooks/use-attributes';
import { Size } from '../_components/size'; import { Size } from '../_components/size';
export const ImageBubbleMenu = ({ editor }) => { export const ImageBubbleMenu = ({ editor }) => {
const attrs = editor.getAttributes(Image.name);
const { width: currentWidth, height: currentHeight } = attrs;
const { width: maxWidth } = getEditorContainerDOMSize(editor); const { width: maxWidth } = getEditorContainerDOMSize(editor);
const { width: currentWidth, height: currentHeight } = useAttributes(editor, Image.name, { width: 0, height: 0 });
const [width, setWidth] = useState(currentWidth); const [width, setWidth] = useState(currentWidth);
const [height, setHeight] = useState(currentHeight); const [height, setHeight] = useState(currentHeight);
const copyMe = useCallback(() => copyNode(Image.name, editor), [editor]);
const deleteMe = useCallback(() => deleteNode(Image.name, editor), [editor]);
const alignLeft = useCallback(() => {
editor
.chain()
.updateAttributes(Image.name, {
textAlign: 'left',
})
.setNodeSelection(editor.state.selection.from)
.focus()
.run();
}, [editor]);
const alignCenter = useCallback(() => {
editor
.chain()
.updateAttributes(Image.name, {
textAlign: 'center',
})
.setNodeSelection(editor.state.selection.from)
.focus()
.run();
}, [editor]);
const alignRight = useCallback(() => {
editor
.chain()
.updateAttributes(Image.name, {
textAlign: 'right',
})
.setNodeSelection(editor.state.selection.from)
.focus()
.run();
}, [editor]);
const updateSize = useCallback(
(size) => {
editor.chain().updateAttributes(Image.name, size).setNodeSelection(editor.state.selection.from).focus().run();
},
[editor]
);
useEffect(() => { useEffect(() => {
setWidth(parseInt(currentWidth)); setWidth(parseInt(currentWidth));
setHeight(parseInt(currentHeight)); setHeight(parseInt(currentHeight));
@ -38,87 +81,24 @@ export const ImageBubbleMenu = ({ editor }) => {
}} }}
matchRenderContainer={(node) => node && node.id === 'js-resizeable-container'} matchRenderContainer={(node) => node && node.id === 'js-resizeable-container'}
> >
<Space> <Space spacing={4}>
<Tooltip content="复制"> <Tooltip content="复制">
<Button <Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
onClick={() => copyNode(Image.name, editor)}
icon={<IconCopy />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip> </Tooltip>
<Tooltip content="左对齐"> <Tooltip content="左对齐">
<Button <Button onClick={alignLeft} icon={<IconAlignLeft />} type="tertiary" theme="borderless" size="small" />
onClick={() => {
editor
.chain()
.updateAttributes(Image.name, {
textAlign: 'left',
})
.setNodeSelection(editor.state.selection.from)
.focus()
.run();
}}
icon={<IconAlignLeft />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip> </Tooltip>
<Tooltip content="居中"> <Tooltip content="居中">
<Button <Button onClick={alignCenter} icon={<IconAlignCenter />} type="tertiary" theme="borderless" size="small" />
onClick={() => {
editor
.chain()
.updateAttributes(Image.name, {
textAlign: 'center',
})
.setNodeSelection(editor.state.selection.from)
.focus()
.run();
}}
icon={<IconAlignCenter />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip> </Tooltip>
<Tooltip content="右对齐"> <Tooltip content="右对齐">
<Button <Button onClick={alignRight} icon={<IconAlignRight />} type="tertiary" theme="borderless" size="small" />
onClick={() => {
editor
.chain()
.updateAttributes(Image.name, {
textAlign: 'right',
})
.setNodeSelection(editor.state.selection.from)
.focus()
.run();
}}
icon={<IconAlignRight />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip> </Tooltip>
<Size <Size width={width} maxWidth={maxWidth} height={height} onOk={updateSize}>
width={width}
maxWidth={maxWidth}
height={height}
onOk={(size) => {
editor
.chain()
.updateAttributes(Image.name, size)
.setNodeSelection(editor.state.selection.from)
.focus()
.run();
}}
>
<Tooltip content="设置宽高"> <Tooltip content="设置宽高">
<Button icon={<IconLineHeight />} type="tertiary" theme="borderless" size="small" /> <Button icon={<IconLineHeight />} type="tertiary" theme="borderless" size="small" />
</Tooltip> </Tooltip>
@ -127,13 +107,7 @@ export const ImageBubbleMenu = ({ editor }) => {
<Divider /> <Divider />
<Tooltip content="删除" hideOnClick> <Tooltip content="删除" hideOnClick>
<Button <Button size="small" type="tertiary" theme="borderless" icon={<IconDelete />} onClick={deleteMe} />
size="small"
type="tertiary"
theme="borderless"
icon={<IconDelete />}
onClick={() => deleteNode(Image.name, editor)}
/>
</Tooltip> </Tooltip>
</Space> </Space>
</BubbleMenu> </BubbleMenu>

View File

@ -3,10 +3,6 @@ import { Editor } from '@tiptap/core';
import { ImageBubbleMenu } from './bubble'; import { ImageBubbleMenu } from './bubble';
export const Image: React.FC<{ editor: Editor }> = ({ editor }) => { export const Image: React.FC<{ editor: Editor }> = ({ editor }) => {
if (!editor) {
return null;
}
return ( return (
<> <>
<ImageBubbleMenu editor={editor} /> <ImageBubbleMenu editor={editor} />

View File

@ -20,7 +20,9 @@ import { GridSelect } from 'components/grid-select';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import { useUser } from 'data/user'; import { useUser } from 'data/user';
import { createKeysLocalStorageLRUCache } from 'helpers/lru-cache'; import { createKeysLocalStorageLRUCache } from 'helpers/lru-cache';
import { isTitleActive, getEditorContainerDOMSize } from 'tiptap/prose-utils'; import { getEditorContainerDOMSize } from 'tiptap/prose-utils';
import { useActive } from 'tiptap/hooks/use-active';
import { Title } from 'tiptap/extensions/title';
import { createCountdown } from '../countdown/service'; import { createCountdown } from '../countdown/service';
const insertMenuLRUCache = createKeysLocalStorageLRUCache('TIPTAP_INSERT_MENU', 3); const insertMenuLRUCache = createKeysLocalStorageLRUCache('TIPTAP_INSERT_MENU', 3);
@ -34,6 +36,7 @@ const COMMANDS = [
label: '表格', label: '表格',
custom: (editor, runCommand) => ( custom: (editor, runCommand) => (
<Popover <Popover
key="table"
showArrow showArrow
position="rightTop" position="rightTop"
zIndex={10000} zIndex={10000}
@ -130,6 +133,7 @@ const COMMANDS = [
export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => { export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => {
const { user } = useUser(); const { user } = useUser();
const [recentUsed, setRecentUsed] = useState([]); const [recentUsed, setRecentUsed] = useState([]);
const isTitleActive = useActive(editor, Title.name);
const [visible, toggleVisible] = useToggle(false); const [visible, toggleVisible] = useToggle(false);
const renderedCommands = useMemo( const renderedCommands = useMemo(
@ -194,7 +198,7 @@ export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => {
> >
<div> <div>
<Tooltip content="插入"> <Tooltip content="插入">
<Button type="tertiary" theme="borderless" icon={<IconPlus />} disabled={isTitleActive(editor)} /> <Button type="tertiary" theme="borderless" icon={<IconPlus />} disabled={isTitleActive} />
</Tooltip> </Tooltip>
</div> </div>
</Dropdown> </Dropdown>

View File

@ -3,17 +3,22 @@ import { Editor } from '@tiptap/core';
import { Button } from '@douyinfe/semi-ui'; import { Button } from '@douyinfe/semi-ui';
import { IconItalic } from '@douyinfe/semi-icons'; import { IconItalic } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { isTitleActive } from 'tiptap/prose-utils'; import { useActive } from 'tiptap/hooks/use-active';
import { Title } from 'tiptap/extensions/title';
import { Italic as ItalicExtension } from 'tiptap/extensions/italic';
export const Italic: React.FC<{ editor: Editor }> = ({ editor }) => { export const Italic: React.FC<{ editor: Editor }> = ({ editor }) => {
const isTitleActive = useActive(editor, Title.name);
const isItalicActive = useActive(editor, ItalicExtension.name);
return ( return (
<Tooltip content="斜体"> <Tooltip content="斜体">
<Button <Button
theme={editor.isActive('italic') ? 'light' : 'borderless'} theme={isItalicActive ? 'light' : 'borderless'}
type="tertiary" type="tertiary"
icon={<IconItalic />} icon={<IconItalic />}
onClick={() => editor.chain().focus().toggleItalic().run()} onClick={() => editor.chain().focus().toggleItalic().run()}
disabled={isTitleActive(editor)} disabled={isTitleActive}
/> />
</Tooltip> </Tooltip>
); );

View File

@ -6,11 +6,11 @@ import { Divider } from 'tiptap/divider';
import { BubbleMenu } from 'tiptap/views/bubble-menu'; import { BubbleMenu } from 'tiptap/views/bubble-menu';
import { Link } from 'tiptap/extensions/link'; import { Link } from 'tiptap/extensions/link';
import { isMarkActive, findMarkPosition } from 'tiptap/prose-utils'; import { isMarkActive, findMarkPosition } from 'tiptap/prose-utils';
import { useAttributes } from 'tiptap/hooks/use-attributes';
import { triggerOpenLinkSettingModal } from '../_event'; import { triggerOpenLinkSettingModal } from '../_event';
export const LinkBubbleMenu = ({ editor }) => { export const LinkBubbleMenu = ({ editor }) => {
const attrs = editor.getAttributes(Link.name); const { href, target } = useAttributes(editor, Link.name, { href: '', target: '' });
const { href, target } = attrs;
const [text, setText] = useState(); const [text, setText] = useState();
const [from, setFrom] = useState(-1); const [from, setFrom] = useState(-1);
const [to, setTo] = useState(-1); const [to, setTo] = useState(-1);
@ -69,7 +69,7 @@ export const LinkBubbleMenu = ({ editor }) => {
shouldShow={() => editor.isActive(Link.name)} shouldShow={() => editor.isActive(Link.name)}
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }} tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
> >
<Space> <Space spacing={4}>
<Tooltip content="访问链接"> <Tooltip content="访问链接">
<Button size="small" type="tertiary" theme="borderless" icon={<IconExternalOpen />} onClick={visitLink} /> <Button size="small" type="tertiary" theme="borderless" icon={<IconExternalOpen />} onClick={visitLink} />
</Tooltip> </Tooltip>

View File

@ -1,23 +1,30 @@
import React from 'react'; import React, { useCallback } from 'react';
import { Editor } from '@tiptap/core'; import { Editor } from '@tiptap/core';
import { Button } from '@douyinfe/semi-ui'; import { Button } from '@douyinfe/semi-ui';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { IconLink } from 'components/icons'; import { IconLink } from 'components/icons';
import { isTitleActive } from 'tiptap/prose-utils'; import { useActive } from 'tiptap/hooks/use-active';
import { Title } from 'tiptap/extensions/title';
import { Link as LinkExtension } from 'tiptap/extensions/link';
import { createOrToggleLink } from './service'; import { createOrToggleLink } from './service';
import { LinkBubbleMenu } from './bubble'; import { LinkBubbleMenu } from './bubble';
import { LinkSettingModal } from './modal'; import { LinkSettingModal } from './modal';
export const Link: React.FC<{ editor: Editor }> = ({ editor }) => { export const Link: React.FC<{ editor: Editor }> = ({ editor }) => {
const isTitleActive = useActive(editor, Title.name);
const isLinkActive = useActive(editor, LinkExtension.name);
const callLinkService = useCallback(() => createOrToggleLink(editor), [editor]);
return ( return (
<> <>
<Tooltip content="插入链接"> <Tooltip content="插入链接">
<Button <Button
theme={editor.isActive('link') ? 'light' : 'borderless'} theme={isLinkActive ? 'light' : 'borderless'}
type="tertiary" type="tertiary"
icon={<IconLink />} icon={<IconLink />}
onClick={() => createOrToggleLink(editor)} onClick={callLinkService}
disabled={isTitleActive(editor)} disabled={isTitleActive}
/> />
</Tooltip> </Tooltip>
<LinkBubbleMenu editor={editor} /> <LinkBubbleMenu editor={editor} />

View File

@ -13,6 +13,8 @@ export const LinkSettingModal: React.FC<IProps> = ({ editor }) => {
const [initialState, setInitialState] = useState({ text: '', href: '', from: -1, to: -1 }); const [initialState, setInitialState] = useState({ text: '', href: '', from: -1, to: -1 });
const [visible, toggleVisible] = useToggle(false); const [visible, toggleVisible] = useToggle(false);
const handleCancel = useCallback(() => toggleVisible(false), [toggleVisible]);
const handleOk = useCallback(() => { const handleOk = useCallback(() => {
$form.current.validate().then((values) => { $form.current.validate().then((values) => {
if (!values.text) { if (!values.text) {
@ -21,9 +23,6 @@ export const LinkSettingModal: React.FC<IProps> = ({ editor }) => {
const { from, to } = initialState; const { from, to } = initialState;
const { view } = editor; const { view } = editor;
console.log(from, to);
const schema = view.state.schema; const schema = view.state.schema;
const node = schema.text(values.text, [schema.marks.link.create({ href: values.href })]); const node = schema.text(values.text, [schema.marks.link.create({ href: values.href })]);
view.dispatch(view.state.tr.replaceRangeWith(from, to, node)); view.dispatch(view.state.tr.replaceRangeWith(from, to, node));
@ -46,7 +45,7 @@ export const LinkSettingModal: React.FC<IProps> = ({ editor }) => {
}, [editor, toggleVisible]); }, [editor, toggleVisible]);
return ( return (
<Modal title="编辑链接" visible={visible} onOk={handleOk} onCancel={() => toggleVisible(false)} centered> <Modal title="编辑链接" visible={visible} onOk={handleOk} onCancel={handleCancel} centered>
<Form initValues={initialState} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left"> <Form initValues={initialState} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left">
<Form.Input label="文本" field="text" placeholder="请输入文本"></Form.Input> <Form.Input label="文本" field="text" placeholder="请输入文本"></Form.Input>
<Form.Input <Form.Input

View File

@ -6,12 +6,12 @@ import { BubbleMenu } from 'tiptap/views/bubble-menu';
import { Mind } from 'tiptap/extensions/mind'; import { Mind } from 'tiptap/extensions/mind';
import { Divider } from 'tiptap/divider'; import { Divider } from 'tiptap/divider';
import { getEditorContainerDOMSize, copyNode, deleteNode } from 'tiptap/prose-utils'; import { getEditorContainerDOMSize, copyNode, deleteNode } from 'tiptap/prose-utils';
import { useAttributes } from 'tiptap/hooks/use-attributes';
import { Size } from '../_components/size'; import { Size } from '../_components/size';
export const MindBubbleMenu = ({ editor }) => { export const MindBubbleMenu = ({ editor }) => {
const attrs = editor.getAttributes(Mind.name);
const { width, height } = attrs;
const { width: maxWidth } = getEditorContainerDOMSize(editor); const { width: maxWidth } = getEditorContainerDOMSize(editor);
const { width, height } = useAttributes(editor, Mind.name, { width: 0, height: 0 });
const setSize = useCallback( const setSize = useCallback(
(size) => { (size) => {
@ -20,6 +20,9 @@ export const MindBubbleMenu = ({ editor }) => {
[editor] [editor]
); );
const copyMe = useCallback(() => copyNode(Mind.name, editor), [editor]);
const deleteMe = useCallback(() => deleteNode(Mind.name, editor), [editor]);
return ( return (
<BubbleMenu <BubbleMenu
className={'bubble-menu'} className={'bubble-menu'}
@ -29,15 +32,9 @@ export const MindBubbleMenu = ({ editor }) => {
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }} tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
matchRenderContainer={(node) => node && node.id === 'js-resizeable-container'} matchRenderContainer={(node) => node && node.id === 'js-resizeable-container'}
> >
<Space> <Space spacing={4}>
<Tooltip content="复制"> <Tooltip content="复制">
<Button <Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
onClick={() => copyNode(Mind.name, editor)}
icon={<IconCopy />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip> </Tooltip>
<Size width={width} maxWidth={maxWidth} height={height} onOk={setSize}> <Size width={width} maxWidth={maxWidth} height={height} onOk={setSize}>
@ -45,15 +42,11 @@ export const MindBubbleMenu = ({ editor }) => {
<Button icon={<IconLineHeight />} type="tertiary" theme="borderless" size="small" /> <Button icon={<IconLineHeight />} type="tertiary" theme="borderless" size="small" />
</Tooltip> </Tooltip>
</Size> </Size>
<Divider /> <Divider />
<Tooltip content="删除节点" hideOnClick> <Tooltip content="删除节点" hideOnClick>
<Button <Button onClick={deleteMe} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
onClick={() => deleteNode(Mind.name, editor)}
icon={<IconDelete />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip> </Tooltip>
</Space> </Space>
</BubbleMenu> </BubbleMenu>

View File

@ -3,10 +3,6 @@ import { Editor } from '@tiptap/core';
import { MindBubbleMenu } from './bubble'; import { MindBubbleMenu } from './bubble';
export const Mind: React.FC<{ editor: Editor }> = ({ editor }) => { export const Mind: React.FC<{ editor: Editor }> = ({ editor }) => {
if (!editor) {
return null;
}
return ( return (
<> <>
<MindBubbleMenu editor={editor} /> <MindBubbleMenu editor={editor} />

View File

@ -1,23 +1,26 @@
import React from 'react'; import React, { useCallback } from 'react';
import { Editor } from '@tiptap/core'; import { Editor } from '@tiptap/core';
import { Button } from '@douyinfe/semi-ui'; import { Button } from '@douyinfe/semi-ui';
import { IconOrderedList } from 'components/icons'; import { IconOrderedList } from 'components/icons';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { isTitleActive } from 'tiptap/prose-utils'; import { useActive } from 'tiptap/hooks/use-active';
import { Title } from 'tiptap/extensions/title';
import { OrderedList as OrderedListExtension } from 'tiptap/extensions/ordered-list';
export const OrderedList: React.FC<{ editor: Editor }> = ({ editor }) => { export const OrderedList: React.FC<{ editor: Editor }> = ({ editor }) => {
if (!editor) { const isTitleActive = useActive(editor, Title.name);
return null; const isOrderedListActive = useActive(editor, OrderedListExtension.name);
}
const toggleOrderedList = useCallback(() => editor.chain().focus().toggleOrderedList().run(), [editor]);
return ( return (
<Tooltip content="有序列表"> <Tooltip content="有序列表">
<Button <Button
theme={editor.isActive('orderedList') ? 'light' : 'borderless'} theme={isOrderedListActive ? 'light' : 'borderless'}
type="tertiary" type="tertiary"
icon={<IconOrderedList />} icon={<IconOrderedList />}
onClick={() => editor.chain().focus().toggleOrderedList().run()} onClick={toggleOrderedList}
disabled={isTitleActive(editor)} disabled={isTitleActive}
/> />
</Tooltip> </Tooltip>
); );

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { Popover, Button, Typography, Input, Space } from '@douyinfe/semi-ui'; import { Popover, Button, Typography, Input, Space } from '@douyinfe/semi-ui';
import { Editor } from '@tiptap/core'; import { Editor } from '@tiptap/core';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
@ -8,12 +8,18 @@ import { SearchNReplace } from 'tiptap/extensions/search';
const { Text } = Typography; const { Text } = Typography;
export const Search: React.FC<{ editor: Editor }> = ({ editor }) => { export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
const searchExtension = editor.extensionManager.extensions.find((ext) => ext.name === SearchNReplace.name); const [currentIndex, setCurrentIndex] = useState(-1);
const currentIndex = searchExtension ? searchExtension.options.currentIndex : -1; const [results, setResults] = useState([]);
const results = searchExtension ? searchExtension.options.results : [];
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const [replaceValue, setReplaceValue] = useState(''); const [replaceValue, setReplaceValue] = useState('');
const onVisibleChange = useCallback((visible) => {
if (!visible) {
setSearchValue('');
setReplaceValue('');
}
}, []);
useEffect(() => { useEffect(() => {
if (editor && editor.commands && editor.commands.setSearchTerm) { if (editor && editor.commands && editor.commands.setSearchTerm) {
editor.commands.setSearchTerm(searchValue); editor.commands.setSearchTerm(searchValue);
@ -26,18 +32,33 @@ export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
} }
}, [replaceValue, editor]); }, [replaceValue, editor]);
useEffect(() => {
const searchExtension = editor.extensionManager.extensions.find((ext) => ext.name === SearchNReplace.name);
if (!searchExtension) return;
const listener = () => {
const currentIndex = searchExtension ? searchExtension.options.currentIndex : -1;
const results = searchExtension ? searchExtension.options.results : [];
setCurrentIndex(currentIndex);
setResults(results);
};
searchExtension.options.onChange = listener;
return () => {
if (!searchExtension) return;
delete searchExtension.options.onChange;
};
}, [editor]);
return ( return (
<Popover <Popover
showArrow showArrow
zIndex={10000} zIndex={10000}
trigger="click" trigger="click"
position="bottomRight" position="bottomRight"
onVisibleChange={(visible) => { onVisibleChange={onVisibleChange}
if (!visible) {
setSearchValue('');
setReplaceValue('');
}
}}
content={ content={
<div> <div>
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
@ -45,29 +66,29 @@ export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
<Input <Input
autofocus autofocus
value={searchValue} value={searchValue}
onChange={(v) => setSearchValue(v)} onChange={setSearchValue}
suffix={results.length ? `${currentIndex + 1}/${results.length}` : ''} suffix={results.length ? `${currentIndex + 1}/${results.length}` : ''}
/> />
</div> </div>
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
<Text type="tertiary"></Text> <Text type="tertiary"></Text>
<Input value={replaceValue} onChange={(v) => setReplaceValue(v)} /> <Input value={replaceValue} onChange={setReplaceValue} />
</div> </div>
<div> <div>
<Space> <Space>
<Button disabled={!results.length} onClick={() => editor.commands.replaceAll()}> <Button disabled={!results.length} onClick={editor.commands.replaceAll}>
</Button> </Button>
<Button disabled={!results.length} onClick={() => editor.commands.replace()}> <Button disabled={!results.length} onClick={editor.commands.replace}>
</Button> </Button>
<Button disabled={!results.length} onClick={() => editor.commands.goToPrevSearchResult()}> <Button disabled={!results.length} onClick={editor.commands.goToPrevSearchResult}>
</Button> </Button>
<Button disabled={!results.length} onClick={() => editor.commands.goToNextSearchResult()}> <Button disabled={!results.length} onClick={editor.commands.goToNextSearchResult}>
</Button> </Button>
</Space> </Space>

View File

@ -3,17 +3,22 @@ import { Editor } from '@tiptap/core';
import { Button } from '@douyinfe/semi-ui'; import { Button } from '@douyinfe/semi-ui';
import { IconStrikeThrough } from '@douyinfe/semi-icons'; import { IconStrikeThrough } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { isTitleActive } from 'tiptap/prose-utils'; import { useActive } from 'tiptap/hooks/use-active';
import { Title } from 'tiptap/extensions/title';
import { Strike as StrikeExtension } from 'tiptap/extensions/strike';
export const Strike: React.FC<{ editor: Editor }> = ({ editor }) => { export const Strike: React.FC<{ editor: Editor }> = ({ editor }) => {
const isTitleActive = useActive(editor, Title.name);
const isStrikeActive = useActive(editor, StrikeExtension.name);
return ( return (
<Tooltip content="删除线"> <Tooltip content="删除线">
<Button <Button
theme={editor.isActive('strike') ? 'light' : 'borderless'} theme={isStrikeActive ? 'light' : 'borderless'}
type="tertiary" type="tertiary"
icon={<IconStrikeThrough />} icon={<IconStrikeThrough />}
onClick={() => editor.chain().focus().toggleStrike().run()} onClick={() => editor.chain().focus().toggleStrike().run()}
disabled={isTitleActive(editor)} disabled={isTitleActive}
/> />
</Tooltip> </Tooltip>
); );

View File

@ -2,17 +2,22 @@ import React from 'react';
import { Button } from '@douyinfe/semi-ui'; import { Button } from '@douyinfe/semi-ui';
import { IconSub } from 'components/icons'; import { IconSub } from 'components/icons';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { isTitleActive } from 'tiptap/prose-utils'; import { useActive } from 'tiptap/hooks/use-active';
import { Title } from 'tiptap/extensions/title';
import { Subscript as SubscriptExtension } from 'tiptap/extensions/subscript';
export const Subscript: React.FC<{ editor: any }> = ({ editor }) => { export const Subscript: React.FC<{ editor: any }> = ({ editor }) => {
const isTitleActive = useActive(editor, Title.name);
const isSubscriptActive = useActive(editor, SubscriptExtension.name);
return ( return (
<Tooltip content="下标"> <Tooltip content="下标">
<Button <Button
theme={editor.isActive('subscript') ? 'light' : 'borderless'} theme={isSubscriptActive ? 'light' : 'borderless'}
type="tertiary" type="tertiary"
icon={<IconSub />} icon={<IconSub />}
onClick={() => editor.chain().focus().toggleSubscript().run()} onClick={() => editor.chain().focus().toggleSubscript().run()}
disabled={isTitleActive(editor)} disabled={isTitleActive}
/> />
</Tooltip> </Tooltip>
); );

View File

@ -2,17 +2,22 @@ import React from 'react';
import { Button } from '@douyinfe/semi-ui'; import { Button } from '@douyinfe/semi-ui';
import { IconSup } from 'components/icons'; import { IconSup } from 'components/icons';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { isTitleActive } from 'tiptap/prose-utils'; import { useActive } from 'tiptap/hooks/use-active';
import { Title } from 'tiptap/extensions/title';
import { Superscript as SuperscriptExtension } from 'tiptap/extensions/superscript';
export const Superscript: React.FC<{ editor: any }> = ({ editor }) => { export const Superscript: React.FC<{ editor: any }> = ({ editor }) => {
const isTitleActive = useActive(editor, Title.name);
const isSuperscriptActive = useActive(editor, SuperscriptExtension.name);
return ( return (
<Tooltip content="上标"> <Tooltip content="上标">
<Button <Button
theme={editor.isActive('superscript') ? 'light' : 'borderless'} theme={isSuperscriptActive ? 'light' : 'borderless'}
type="tertiary" type="tertiary"
icon={<IconSup />} icon={<IconSup />}
onClick={() => editor.chain().focus().toggleSuperscript().run()} onClick={() => editor.chain().focus().toggleSuperscript().run()}
disabled={isTitleActive(editor)} disabled={isTitleActive}
/> />
</Tooltip> </Tooltip>
); );

View File

@ -1,3 +1,4 @@
import React, { useCallback } from 'react';
import { Space, Button } from '@douyinfe/semi-ui'; import { Space, Button } from '@douyinfe/semi-ui';
import { IconCopy } from '@douyinfe/semi-icons'; import { IconCopy } from '@douyinfe/semi-icons';
import { import {
@ -21,6 +22,20 @@ import { Table } from 'tiptap/extensions/table';
import { copyNode } from 'tiptap/prose-utils'; import { copyNode } from 'tiptap/prose-utils';
export const TableBubbleMenu = ({ editor }) => { export const TableBubbleMenu = ({ editor }) => {
const copyMe = useCallback(() => copyNode(Table.name, editor), [editor]);
const deleteMe = useCallback(() => editor.chain().focus().deleteTable(), [editor]);
const addColumnBefore = useCallback(() => editor.chain().focus().addColumnBefore().run(), [editor]);
const addColumnAfter = useCallback(() => editor.chain().focus().addColumnAfter().run(), [editor]);
const deleteColumn = useCallback(() => editor.chain().focus().deleteColumn().run(), [editor]);
const addRowBefore = useCallback(() => editor.chain().focus().addRowBefore().run(), [editor]);
const addRowAfter = useCallback(() => editor.chain().focus().addRowAfter().run(), [editor]);
const deleteRow = useCallback(() => editor.chain().focus().deleteRow().run(), [editor]);
const toggleHeaderColumn = useCallback(() => editor.chain().focus().toggleHeaderColumn().run(), [editor]);
const toggleHeaderRow = useCallback(() => editor.chain().focus().toggleHeaderRow().run(), [editor]);
const toggleHeaderCell = useCallback(() => editor.chain().focus().toggleHeaderCell().run(), [editor]);
const mergeCells = useCallback(() => editor.chain().focus().mergeCells().run(), [editor]);
const splitCell = useCallback(() => editor.chain().focus().splitCell().run(), [editor]);
return ( return (
<BubbleMenu <BubbleMenu
className={'bubble-menu bubble-menu-table'} className={'bubble-menu bubble-menu-table'}
@ -32,22 +47,16 @@ export const TableBubbleMenu = ({ editor }) => {
}} }}
matchRenderContainer={(node: HTMLElement) => node && node.tagName === 'TABLE'} matchRenderContainer={(node: HTMLElement) => node && node.tagName === 'TABLE'}
> >
<Space> <Space spacing={4}>
<Tooltip content="复制"> <Tooltip content="复制">
<Button <Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
onClick={() => copyNode(Table.name, editor)}
icon={<IconCopy />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip> </Tooltip>
<Divider /> <Divider />
<Tooltip content="向前插入一列"> <Tooltip content="向前插入一列">
<Button <Button
onClick={() => editor.chain().focus().addColumnBefore().run()} onClick={addColumnBefore}
icon={<IconAddColumnBefore />} icon={<IconAddColumnBefore />}
type="tertiary" type="tertiary"
theme="borderless" theme="borderless"
@ -57,7 +66,7 @@ export const TableBubbleMenu = ({ editor }) => {
<Tooltip content="向后插入一列"> <Tooltip content="向后插入一列">
<Button <Button
onClick={() => editor.chain().focus().addColumnAfter().run()} onClick={addColumnAfter}
icon={<IconAddColumnAfter />} icon={<IconAddColumnAfter />}
type="tertiary" type="tertiary"
theme="borderless" theme="borderless"
@ -65,45 +74,21 @@ export const TableBubbleMenu = ({ editor }) => {
/> />
</Tooltip> </Tooltip>
<Tooltip content="删除当前列" hideOnClick> <Tooltip content="删除当前列" hideOnClick>
<Button <Button onClick={deleteColumn} icon={<IconDeleteColumn />} type="tertiary" theme="borderless" size="small" />
onClick={() => editor.chain().focus().deleteColumn().run()}
icon={<IconDeleteColumn />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip> </Tooltip>
<Divider /> <Divider />
<Tooltip content="向前插入一行"> <Tooltip content="向前插入一行">
<Button <Button onClick={addRowBefore} icon={<IconAddRowBefore />} type="tertiary" theme="borderless" size="small" />
onClick={() => editor.chain().focus().addRowBefore().run()}
icon={<IconAddRowBefore />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip> </Tooltip>
<Tooltip content="向后插入一行"> <Tooltip content="向后插入一行">
<Button <Button onClick={addRowAfter} icon={<IconAddRowAfter />} type="tertiary" theme="borderless" size="small" />
onClick={() => editor.chain().focus().addRowAfter().run()}
icon={<IconAddRowAfter />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip> </Tooltip>
<Tooltip content="删除当前行" hideOnClick> <Tooltip content="删除当前行" hideOnClick>
<Button <Button onClick={deleteRow} icon={<IconDeleteRow />} type="tertiary" theme="borderless" size="small" />
onClick={() => editor.chain().focus().deleteRow().run()}
icon={<IconDeleteRow />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip> </Tooltip>
<Divider /> <Divider />
@ -114,7 +99,7 @@ export const TableBubbleMenu = ({ editor }) => {
type="tertiary" type="tertiary"
theme="borderless" theme="borderless"
icon={<IconTableHeaderColumn />} icon={<IconTableHeaderColumn />}
onClick={() => editor.chain().focus().toggleHeaderColumn().run()} onClick={toggleHeaderColumn}
/> />
</Tooltip> </Tooltip>
@ -124,7 +109,7 @@ export const TableBubbleMenu = ({ editor }) => {
type="tertiary" type="tertiary"
theme="borderless" theme="borderless"
icon={<IconTableHeaderRow />} icon={<IconTableHeaderRow />}
onClick={() => editor.chain().focus().toggleHeaderRow().run()} onClick={toggleHeaderRow}
/> />
</Tooltip> </Tooltip>
@ -134,42 +119,24 @@ export const TableBubbleMenu = ({ editor }) => {
type="tertiary" type="tertiary"
theme="borderless" theme="borderless"
icon={<IconTableHeaderCell />} icon={<IconTableHeaderCell />}
onClick={() => editor.chain().focus().toggleHeaderCell().run()} onClick={toggleHeaderCell}
/> />
</Tooltip> </Tooltip>
<Divider /> <Divider />
<Tooltip content="合并单元格"> <Tooltip content="合并单元格">
<Button <Button size="small" type="tertiary" theme="borderless" icon={<IconMergeCell />} onClick={mergeCells} />
size="small"
type="tertiary"
theme="borderless"
icon={<IconMergeCell />}
onClick={() => editor.chain().focus().mergeCells().run()}
/>
</Tooltip> </Tooltip>
<Tooltip content="分离单元格"> <Tooltip content="分离单元格">
<Button <Button size="small" type="tertiary" theme="borderless" icon={<IconSplitCell />} onClick={splitCell} />
size="small"
type="tertiary"
theme="borderless"
icon={<IconSplitCell />}
onClick={() => editor.chain().focus().splitCell().run()}
/>
</Tooltip> </Tooltip>
<Divider /> <Divider />
<Tooltip content="删除表格" hideOnClick> <Tooltip content="删除表格" hideOnClick>
<Button <Button size="small" type="tertiary" theme="borderless" icon={<IconDeleteTable />} onClick={deleteMe} />
size="small"
type="tertiary"
theme="borderless"
icon={<IconDeleteTable />}
onClick={() => editor.chain().focus().deleteTable().run()}
/>
</Tooltip> </Tooltip>
</Space> </Space>
</BubbleMenu> </BubbleMenu>

View File

@ -3,9 +3,5 @@ import { Editor } from '@tiptap/core';
import { TableBubbleMenu } from './bubble'; import { TableBubbleMenu } from './bubble';
export const Table: React.FC<{ editor: Editor }> = ({ editor }) => { export const Table: React.FC<{ editor: Editor }> = ({ editor }) => {
if (!editor) {
return null;
}
return <TableBubbleMenu editor={editor} />; return <TableBubbleMenu editor={editor} />;
}; };

View File

@ -1,23 +1,26 @@
import React from 'react'; import React, { useCallback } from 'react';
import { Editor } from '@tiptap/core'; import { Editor } from '@tiptap/core';
import { Button } from '@douyinfe/semi-ui'; import { Button } from '@douyinfe/semi-ui';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { IconTask } from 'components/icons'; import { IconTask } from 'components/icons';
import { isTitleActive } from 'tiptap/prose-utils'; import { useActive } from 'tiptap/hooks/use-active';
import { Title } from 'tiptap/extensions/title';
import { TaskList as TaskListExtension } from 'tiptap/extensions/task-list';
export const TaskList: React.FC<{ editor: Editor }> = ({ editor }) => { export const TaskList: React.FC<{ editor: Editor }> = ({ editor }) => {
if (!editor) { const isTitleActive = useActive(editor, Title.name);
return null; const isTaskListActive = useActive(editor, TaskListExtension.name);
}
const toggleTaskList = useCallback(() => editor.chain().focus().toggleTaskList().run(), [editor]);
return ( return (
<Tooltip content="任务列表"> <Tooltip content="任务列表">
<Button <Button
theme={editor.isActive('taskList') ? 'light' : 'borderless'} theme={isTaskListActive ? 'light' : 'borderless'}
type="tertiary" type="tertiary"
icon={<IconTask />} icon={<IconTask />}
onClick={() => editor.chain().focus().toggleTaskList().run()} onClick={toggleTaskList}
disabled={isTitleActive(editor)} disabled={isTitleActive}
/> />
</Tooltip> </Tooltip>
); );

View File

@ -1,33 +1,47 @@
import React from 'react'; import React, { useCallback } from 'react';
import { Editor } from '@tiptap/core'; import { Editor } from '@tiptap/core';
import { Button } from '@douyinfe/semi-ui'; import { Button } from '@douyinfe/semi-ui';
import { IconFont } from '@douyinfe/semi-icons'; import { IconFont } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { isTitleActive } from 'tiptap/prose-utils'; import { useActive } from 'tiptap/hooks/use-active';
import { Title } from 'tiptap/extensions/title';
import { TextStyle } from 'tiptap/extensions/text-style';
import { useAttributes } from 'tiptap/hooks/use-attributes';
import { ColorPicker } from '../_components/color-picker'; import { ColorPicker } from '../_components/color-picker';
type Color = { color: string };
const FlexStyle = {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
} as React.CSSProperties;
export const TextColor: React.FC<{ editor: Editor }> = ({ editor }) => { export const TextColor: React.FC<{ editor: Editor }> = ({ editor }) => {
const { color } = editor.getAttributes('textStyle'); const isTitleActive = useActive(editor, Title.name);
const isTextStyleActive = useActive(editor, TextStyle.name);
const color = useAttributes<Color, Color['color']>(
editor,
'textStyle',
{ color: 'transparent' },
(attrs) => attrs.color
);
const setColor = useCallback(
(color) => {
color ? editor.chain().focus().setColor(color).run() : editor.chain().focus().unsetColor().run();
},
[editor]
);
return ( return (
<ColorPicker <ColorPicker onSetColor={setColor} disabled={isTitleActive}>
onSetColor={(color) => {
color ? editor.chain().focus().setColor(color).run() : editor.chain().focus().unsetColor().run();
}}
disabled={isTitleActive(editor)}
>
<Tooltip content="文本色"> <Tooltip content="文本色">
<Button <Button
theme={editor.isActive('textStyle') ? 'light' : 'borderless'} theme={isTextStyleActive ? 'light' : 'borderless'}
type={'tertiary'} type={'tertiary'}
icon={ icon={
<div <div style={FlexStyle}>
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<IconFont style={{ fontSize: '0.85em' }} /> <IconFont style={{ fontSize: '0.85em' }} />
<span <span
style={{ style={{
@ -38,7 +52,7 @@ export const TextColor: React.FC<{ editor: Editor }> = ({ editor }) => {
></span> ></span>
</div> </div>
} }
disabled={isTitleActive(editor)} disabled={isTitleActive}
/> />
</Tooltip> </Tooltip>
</ColorPicker> </ColorPicker>

View File

@ -3,9 +3,12 @@ import { Editor } from '@tiptap/core';
import { Button } from '@douyinfe/semi-ui'; import { Button } from '@douyinfe/semi-ui';
import { IconUnderline } from '@douyinfe/semi-icons'; import { IconUnderline } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { isTitleActive } from 'tiptap/prose-utils'; import { useActive } from 'tiptap/hooks/use-active';
import { Title } from 'tiptap/extensions/title';
export const Underline: React.FC<{ editor: Editor }> = ({ editor }) => { export const Underline: React.FC<{ editor: Editor }> = ({ editor }) => {
const isTitleActive = useActive(editor, Title.name);
return ( return (
<Tooltip content="下划线"> <Tooltip content="下划线">
<Button <Button
@ -13,7 +16,7 @@ export const Underline: React.FC<{ editor: Editor }> = ({ editor }) => {
type="tertiary" type="tertiary"
icon={<IconUnderline />} icon={<IconUnderline />}
onClick={() => editor.chain().focus().toggleUnderline().run()} onClick={() => editor.chain().focus().toggleUnderline().run()}
disabled={isTitleActive(editor)} disabled={isTitleActive}
/> />
</Tooltip> </Tooltip>
); );