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]);
const editor = useEditor({
editable: authority && authority.editable,
extensions: [
...BaseKit,
DocumentWithTitle,
getCollaborationExtension(provider),
getCollaborationCursorExtension(provider, currentUser),
],
onTransaction: debounce(({ transaction }) => {
try {
const title = transaction.doc.content.firstChild.content.firstChild.textContent;
triggerChangeDocumentTitle(title);
} catch (e) {
//
}
}, 50),
});
const editor = useEditor(
{
editable: authority && authority.editable,
extensions: [
...BaseKit,
DocumentWithTitle,
getCollaborationExtension(provider),
getCollaborationCursorExtension(provider, currentUser),
],
onTransaction: debounce(({ transaction }) => {
try {
const title = transaction.doc.content.firstChild.content.firstChild.textContent;
triggerChangeDocumentTitle(title);
} catch (e) {
//
}
}, 50),
},
[authority, provider]
);
const [mentionUsersSettingVisible, toggleMentionUsersSettingVisible] = useToggle(false);
const [mentionUsers, setMentionUsers] = useState([]);

View File

@ -120,7 +120,13 @@ export const createKeysLocalStorageLRUCache = (storageKey, capacity) => {
const lruCache = new LRUCache(capacity);
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);

View File

@ -1,4 +1,6 @@
export const Divider = ({ vertical = false }) => {
import React from 'react';
export const _Divider = ({ vertical = false }) => {
return (
<div
style={{
@ -12,3 +14,7 @@ export const Divider = ({ vertical = false }) => {
></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 { isListActive, isListNode, clamp, getNodeType } from 'tiptap/prose-utils';
declare module '@tiptap/core' {
interface Commands<ReturnType> {
indent: {
indent: () => ReturnType;
outdent: () => ReturnType;
};
}
}
type IndentOptions = {
types: string[];
indentLevels: number[];
defaultIndentLevel: number;
};
declare module '@tiptap/core' {
interface Commands {
indent: {
indent: () => Command;
outdent: () => Command;
};
}
}
export enum IndentProps {
min = 0,
max = 210,

View File

@ -43,6 +43,7 @@ interface SearchOptions {
searchResultCurrentClass: string;
caseSensitive: boolean;
disableRegex: boolean;
onChange?: () => void;
}
interface TextNodesWithPosition {
@ -216,6 +217,7 @@ export const SearchNReplace = Extension.create<SearchOptions>({
searchResultCurrentClass: 'search-result-current',
caseSensitive: false,
disableRegex: false,
onChange: () => {},
};
},
@ -279,7 +281,7 @@ export const SearchNReplace = Extension.create<SearchOptions>({
const { currentIndex, results, searchResultCurrentClass } = this.options;
const nextIndex = (currentIndex + results.length - 1) % results.length;
this.options.currentIndex = nextIndex;
this.options.onChange && this.options.onChange();
return gotoSearchResult({
view,
tr,
@ -294,7 +296,7 @@ export const SearchNReplace = Extension.create<SearchOptions>({
const { currentIndex, results, searchResultCurrentClass } = this.options;
const nextIndex = (currentIndex + 1) % results.length;
this.options.currentIndex = nextIndex;
this.options.onChange && this.options.onChange();
return gotoSearchResult({
view,
tr,
@ -331,6 +333,10 @@ export const SearchNReplace = Extension.create<SearchOptions>({
);
extensionThis.options.results = results;
if (results.length && searchTerm) {
extensionThis.options.onChange && extensionThis.options.onChange();
}
if (ctx.getMeta('directDecoration')) {
const { fromPos, toPos, attrs } = ctx.getMeta('directDecoration');
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';
const _MenuBar: React.FC<{ editor: any }> = ({ editor }) => {
if (!editor) return null;
return (
<div>
<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 { Button, Dropdown, Tooltip } from '@douyinfe/semi-ui';
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 }) => {
const current = (() => {
if (editor.isActive({ textAlign: 'center' })) {
const isTitleActive = useActive(editor, Title.name);
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 />;
}
if (editor.isActive({ textAlign: 'right' })) {
if (isAlignRight) {
return <IconAlignRight />;
}
if (editor.isActive({ textAlign: 'justify' })) {
if (isAlignJustify) {
return <IconAlignJustify />;
}
return <IconAlignLeft />;
})();
}, [isAlignCenter, isAlignRight, isAlignJustify]);
const toggle = (align) => {
return () => editor.chain().focus().setTextAlign(align).run();
};
const toggle = useCallback(
(align) => {
return () => editor.chain().focus().setTextAlign(align).run();
},
[editor]
);
return (
<Dropdown
@ -47,7 +56,7 @@ export const Align: React.FC<{ editor: Editor }> = ({ editor }) => {
>
<span>
<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>
</span>
</Dropdown>

View File

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

View File

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

View File

@ -1,40 +1,50 @@
import React from 'react';
import React, { useCallback } from 'react';
import { Editor } from '@tiptap/core';
import { Button } from '@douyinfe/semi-ui';
import { IconMark } from '@douyinfe/semi-icons';
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';
const FlexStyle: React.CSSProperties = {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
};
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 (
<ColorPicker
onSetColor={(color) => {
color
? editor.chain().focus().setBackgroundColor(color).run()
: editor.chain().focus().unsetBackgroundColor().run();
}}
disabled={isTitleActive(editor)}
>
<ColorPicker onSetColor={setBackgroundColor} disabled={isTitleActive}>
<Tooltip content="背景色">
<Button
theme={editor.isActive('textStyle') ? 'light' : 'borderless'}
type={'tertiary'}
icon={
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<div style={FlexStyle}>
<IconMark />
<span style={{ backgroundColor, width: 12, height: 2 }}></span>
</div>
}
disabled={isTitleActive(editor)}
disabled={isTitleActive}
/>
</Tooltip>
</ColorPicker>

View File

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

View File

@ -3,17 +3,22 @@ import { Editor } from '@tiptap/core';
import { Button } from '@douyinfe/semi-ui';
import { IconBold } from '@douyinfe/semi-icons';
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 }) => {
const isTitleActive = useActive(editor, Title.name);
const isBoldActive = useActive(editor, BoldExtension.name);
return (
<Tooltip content="粗体">
<Button
theme={editor.isActive('bold') ? 'light' : 'borderless'}
theme={isBoldActive ? 'light' : 'borderless'}
type="tertiary"
icon={<IconBold />}
onClick={() => editor.chain().focus().toggleBold().run()}
disabled={isTitleActive(editor)}
disabled={isTitleActive}
/>
</Tooltip>
);

View File

@ -1,23 +1,26 @@
import React from 'react';
import React, { useCallback } from 'react';
import { Editor } from '@tiptap/core';
import { Button } from '@douyinfe/semi-ui';
import { IconList } from 'components/icons';
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 }) => {
if (!editor) {
return null;
}
const isTitleActive = useActive(editor, Title.name);
const isBulletListActive = useActive(editor, BulletListExtension.name);
const toggleBulletList = useCallback(() => editor.chain().focus().toggleBulletList().run(), [editor]);
return (
<Tooltip content="无序列表">
<Button
theme={editor.isActive('bulletList') ? 'light' : 'borderless'}
theme={isBulletListActive ? 'light' : 'borderless'}
type="tertiary"
icon={<IconList />}
onClick={() => editor.chain().focus().toggleBulletList().run()}
disabled={isTitleActive(editor)}
onClick={toggleBulletList}
disabled={isTitleActive}
/>
</Tooltip>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,17 +3,22 @@ import { Editor } from '@tiptap/core';
import { Button } from '@douyinfe/semi-ui';
import { IconCode } from '@douyinfe/semi-icons';
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 }) => {
const isTitleActive = useActive(editor, Title.name);
const isCodeActive = useActive(editor, InlineCode.name);
return (
<Tooltip content="行内代码">
<Button
theme={editor.isActive('code') ? 'light' : 'borderless'}
theme={isCodeActive ? 'light' : 'borderless'}
type="tertiary"
icon={<IconCode />}
onClick={() => editor.chain().focus().toggleCode().run()}
disabled={isTitleActive(editor)}
disabled={isTitleActive}
/>
</Tooltip>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,10 +3,6 @@ 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,13 +1,17 @@
import React, { useCallback } from 'react';
import { Select } from '@douyinfe/semi-ui';
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 FontSize: React.FC<{ editor: Editor }> = ({ editor }) => {
const currentFontSizePx = editor.getAttributes('textStyle').fontSize || '16px';
const currentFontSize = +currentFontSizePx.replace('px', '');
const isTitleActive = useActive(editor, Title.name);
const currentFontSize = useAttributes(editor, 'textStyle', { fontSize: '16px' }, (attrs) =>
attrs.fontSize.replace('px', '')
);
const toggle = useCallback(
(val) => {
@ -21,12 +25,7 @@ export const FontSize: React.FC<{ editor: Editor }> = ({ editor }) => {
);
return (
<Select
disabled={isTitleActive(editor)}
value={currentFontSize}
onChange={toggle}
style={{ width: 80, marginRight: 10 }}
>
<Select disabled={isTitleActive} value={+currentFontSize} onChange={toggle} style={{ width: 80, marginRight: 10 }}>
{FONT_SIZES.map((fontSize) => (
<Select.Option key={fontSize} value={fontSize}>
{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 { Select } from '@douyinfe/semi-ui';
import { isTitleActive } from 'tiptap/prose-utils';
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';
};
import { useActive } from 'tiptap/hooks/use-active';
import { Title } from 'tiptap/extensions/title';
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(
(level) => {
if (level === 'paragraph') {
@ -26,12 +34,7 @@ export const Heading: React.FC<{ editor: Editor }> = ({ editor }) => {
);
return (
<Select
disabled={isTitleActive(editor)}
value={getCurrentCaretTitle(editor)}
onChange={toggle}
style={{ width: 90, marginRight: 10 }}
>
<Select disabled={isTitleActive} value={current} onChange={toggle} style={{ width: 90, marginRight: 10 }}>
<Select.Option value="paragraph"></Select.Option>
<Select.Option value={1}>
<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 { Button } from '@douyinfe/semi-ui';
import { Tooltip } from 'components/tooltip';
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 }) => {
if (!editor) {
return null;
}
const isTitleActive = useActive(editor, Title.name);
const setHorizontalRule = useCallback(() => editor.chain().focus().setHorizontalRule().run(), [editor]);
return (
<Tooltip content="插入分割线">
@ -16,8 +17,8 @@ export const HorizontalRule: React.FC<{ editor: Editor }> = ({ editor }) => {
theme={'borderless'}
type="tertiary"
icon={<IconHorizontalRule />}
onClick={() => editor.chain().focus().setHorizontalRule().run()}
disabled={isTitleActive(editor)}
onClick={setHorizontalRule}
disabled={isTitleActive}
/>
</Tooltip>
);

View File

@ -1,40 +1,41 @@
import React from 'react';
import React, { useCallback } from 'react';
import { Editor } from '@tiptap/core';
import { Button } from '@douyinfe/semi-ui';
import { IconIndentLeft, IconIndentRight } from '@douyinfe/semi-icons';
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 }) => {
if (!editor) {
return null;
}
const isTitleActive = useActive(editor, Title.name);
const indent = useCallback(() => {
editor.chain().focus().indent().run();
}, [editor]);
const outdent = useCallback(() => {
editor.chain().focus().outdent().run();
}, [editor]);
return (
<>
<Tooltip content="增加缩进">
<Button
onClick={() => {
// @ts-ignore
editor.chain().focus().indent().run();
}}
onClick={indent}
icon={<IconIndentRight />}
theme={'borderless'}
type="tertiary"
disabled={isTitleActive(editor)}
disabled={isTitleActive}
/>
</Tooltip>
<Tooltip content="减少缩进">
<Button
onClick={() => {
// @ts-ignore
editor.chain().focus().outdent().run();
}}
onClick={outdent}
icon={<IconIndentLeft />}
theme={'borderless'}
type="tertiary"
disabled={isTitleActive(editor)}
disabled={isTitleActive}
/>
</Tooltip>
</>

View File

@ -7,6 +7,7 @@ import { Tooltip } from 'components/tooltip';
import { BubbleMenu } from 'tiptap/views/bubble-menu';
import { Iframe } from 'tiptap/extensions/iframe';
import { copyNode, deleteNode } from 'tiptap/prose-utils';
import { useAttributes } from 'tiptap/hooks/use-attributes';
import { Divider } from 'tiptap/divider';
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';
export const IframeBubbleMenu = ({ editor }) => {
const attrs = editor.getAttributes(Iframe.name);
const { width, height, url } = attrs;
const { width, height, url } = useAttributes(editor, Iframe.name, { width: 0, height: 0, url: '' });
const $form = useRef<FormApi>();
const [visible, toggleVisible] = useToggle(false);
@ -58,11 +58,14 @@ export const IframeBubbleMenu = ({ editor }) => {
[editor]
);
const copyMe = useCallback(() => copyNode(Iframe.name, editor), [editor]);
const deleteMe = useCallback(() => deleteNode(Iframe.name, editor), [editor]);
return (
<BubbleMenu
className={'bubble-menu'}
editor={editor}
pluginKey="link-bubble-menu"
pluginKey="iframe-bubble-menu"
shouldShow={() => editor.isActive(Iframe.name)}
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
>
@ -93,15 +96,9 @@ export const IframeBubbleMenu = ({ editor }) => {
</Form>
</Modal>
<Space>
<Space spacing={4}>
<Tooltip content="复制">
<Button
onClick={() => copyNode(Iframe.name, editor)}
icon={<IconCopy />}
type="tertiary"
theme="borderless"
size="small"
/>
<Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
</Tooltip>
<Tooltip content="访问链接">
@ -121,13 +118,7 @@ export const IframeBubbleMenu = ({ editor }) => {
<Divider />
<Tooltip content="删除节点" hideOnClick>
<Button
onClick={() => deleteNode(Iframe.name, editor)}
icon={<IconDelete />}
type="tertiary"
theme="borderless"
size="small"
/>
<Button onClick={deleteMe} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
</Tooltip>
</Space>
</BubbleMenu>

View File

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

View File

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

View File

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

View File

@ -3,17 +3,22 @@ import { Editor } from '@tiptap/core';
import { Button } from '@douyinfe/semi-ui';
import { IconItalic } from '@douyinfe/semi-icons';
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 }) => {
const isTitleActive = useActive(editor, Title.name);
const isItalicActive = useActive(editor, ItalicExtension.name);
return (
<Tooltip content="斜体">
<Button
theme={editor.isActive('italic') ? 'light' : 'borderless'}
theme={isItalicActive ? 'light' : 'borderless'}
type="tertiary"
icon={<IconItalic />}
onClick={() => editor.chain().focus().toggleItalic().run()}
disabled={isTitleActive(editor)}
disabled={isTitleActive}
/>
</Tooltip>
);

View File

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

View File

@ -1,23 +1,30 @@
import React from 'react';
import React, { useCallback } from 'react';
import { Editor } from '@tiptap/core';
import { Button } from '@douyinfe/semi-ui';
import { Tooltip } from 'components/tooltip';
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 { LinkBubbleMenu } from './bubble';
import { LinkSettingModal } from './modal';
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 (
<>
<Tooltip content="插入链接">
<Button
theme={editor.isActive('link') ? 'light' : 'borderless'}
theme={isLinkActive ? 'light' : 'borderless'}
type="tertiary"
icon={<IconLink />}
onClick={() => createOrToggleLink(editor)}
disabled={isTitleActive(editor)}
onClick={callLinkService}
disabled={isTitleActive}
/>
</Tooltip>
<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 [visible, toggleVisible] = useToggle(false);
const handleCancel = useCallback(() => toggleVisible(false), [toggleVisible]);
const handleOk = useCallback(() => {
$form.current.validate().then((values) => {
if (!values.text) {
@ -21,9 +23,6 @@ export const LinkSettingModal: React.FC<IProps> = ({ editor }) => {
const { from, to } = initialState;
const { view } = editor;
console.log(from, to);
const schema = view.state.schema;
const node = schema.text(values.text, [schema.marks.link.create({ href: values.href })]);
view.dispatch(view.state.tr.replaceRangeWith(from, to, node));
@ -46,7 +45,7 @@ export const LinkSettingModal: React.FC<IProps> = ({ editor }) => {
}, [editor, toggleVisible]);
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.Input label="文本" field="text" placeholder="请输入文本"></Form.Input>
<Form.Input

View File

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

View File

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

View File

@ -1,23 +1,26 @@
import React from 'react';
import React, { useCallback } from 'react';
import { Editor } from '@tiptap/core';
import { Button } from '@douyinfe/semi-ui';
import { IconOrderedList } from 'components/icons';
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 }) => {
if (!editor) {
return null;
}
const isTitleActive = useActive(editor, Title.name);
const isOrderedListActive = useActive(editor, OrderedListExtension.name);
const toggleOrderedList = useCallback(() => editor.chain().focus().toggleOrderedList().run(), [editor]);
return (
<Tooltip content="有序列表">
<Button
theme={editor.isActive('orderedList') ? 'light' : 'borderless'}
theme={isOrderedListActive ? 'light' : 'borderless'}
type="tertiary"
icon={<IconOrderedList />}
onClick={() => editor.chain().focus().toggleOrderedList().run()}
disabled={isTitleActive(editor)}
onClick={toggleOrderedList}
disabled={isTitleActive}
/>
</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 { Editor } from '@tiptap/core';
import { Tooltip } from 'components/tooltip';
@ -8,12 +8,18 @@ import { SearchNReplace } from 'tiptap/extensions/search';
const { Text } = Typography;
export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
const searchExtension = editor.extensionManager.extensions.find((ext) => ext.name === SearchNReplace.name);
const currentIndex = searchExtension ? searchExtension.options.currentIndex : -1;
const results = searchExtension ? searchExtension.options.results : [];
const [currentIndex, setCurrentIndex] = useState(-1);
const [results, setResults] = useState([]);
const [searchValue, setSearchValue] = useState('');
const [replaceValue, setReplaceValue] = useState('');
const onVisibleChange = useCallback((visible) => {
if (!visible) {
setSearchValue('');
setReplaceValue('');
}
}, []);
useEffect(() => {
if (editor && editor.commands && editor.commands.setSearchTerm) {
editor.commands.setSearchTerm(searchValue);
@ -26,18 +32,33 @@ export const Search: React.FC<{ editor: Editor }> = ({ 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 (
<Popover
showArrow
zIndex={10000}
trigger="click"
position="bottomRight"
onVisibleChange={(visible) => {
if (!visible) {
setSearchValue('');
setReplaceValue('');
}
}}
onVisibleChange={onVisibleChange}
content={
<div>
<div style={{ marginBottom: 12 }}>
@ -45,29 +66,29 @@ export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
<Input
autofocus
value={searchValue}
onChange={(v) => setSearchValue(v)}
onChange={setSearchValue}
suffix={results.length ? `${currentIndex + 1}/${results.length}` : ''}
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text type="tertiary"></Text>
<Input value={replaceValue} onChange={(v) => setReplaceValue(v)} />
<Input value={replaceValue} onChange={setReplaceValue} />
</div>
<div>
<Space>
<Button disabled={!results.length} onClick={() => editor.commands.replaceAll()}>
<Button disabled={!results.length} onClick={editor.commands.replaceAll}>
</Button>
<Button disabled={!results.length} onClick={() => editor.commands.replace()}>
<Button disabled={!results.length} onClick={editor.commands.replace}>
</Button>
<Button disabled={!results.length} onClick={() => editor.commands.goToPrevSearchResult()}>
<Button disabled={!results.length} onClick={editor.commands.goToPrevSearchResult}>
</Button>
<Button disabled={!results.length} onClick={() => editor.commands.goToNextSearchResult()}>
<Button disabled={!results.length} onClick={editor.commands.goToNextSearchResult}>
</Button>
</Space>

View File

@ -3,17 +3,22 @@ import { Editor } from '@tiptap/core';
import { Button } from '@douyinfe/semi-ui';
import { IconStrikeThrough } from '@douyinfe/semi-icons';
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 }) => {
const isTitleActive = useActive(editor, Title.name);
const isStrikeActive = useActive(editor, StrikeExtension.name);
return (
<Tooltip content="删除线">
<Button
theme={editor.isActive('strike') ? 'light' : 'borderless'}
theme={isStrikeActive ? 'light' : 'borderless'}
type="tertiary"
icon={<IconStrikeThrough />}
onClick={() => editor.chain().focus().toggleStrike().run()}
disabled={isTitleActive(editor)}
disabled={isTitleActive}
/>
</Tooltip>
);

View File

@ -2,17 +2,22 @@ import React from 'react';
import { Button } from '@douyinfe/semi-ui';
import { IconSub } from 'components/icons';
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 }) => {
const isTitleActive = useActive(editor, Title.name);
const isSubscriptActive = useActive(editor, SubscriptExtension.name);
return (
<Tooltip content="下标">
<Button
theme={editor.isActive('subscript') ? 'light' : 'borderless'}
theme={isSubscriptActive ? 'light' : 'borderless'}
type="tertiary"
icon={<IconSub />}
onClick={() => editor.chain().focus().toggleSubscript().run()}
disabled={isTitleActive(editor)}
disabled={isTitleActive}
/>
</Tooltip>
);

View File

@ -2,17 +2,22 @@ import React from 'react';
import { Button } from '@douyinfe/semi-ui';
import { IconSup } from 'components/icons';
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 }) => {
const isTitleActive = useActive(editor, Title.name);
const isSuperscriptActive = useActive(editor, SuperscriptExtension.name);
return (
<Tooltip content="上标">
<Button
theme={editor.isActive('superscript') ? 'light' : 'borderless'}
theme={isSuperscriptActive ? 'light' : 'borderless'}
type="tertiary"
icon={<IconSup />}
onClick={() => editor.chain().focus().toggleSuperscript().run()}
disabled={isTitleActive(editor)}
disabled={isTitleActive}
/>
</Tooltip>
);

View File

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

View File

@ -3,9 +3,5 @@ import { Editor } from '@tiptap/core';
import { TableBubbleMenu } from './bubble';
export const Table: React.FC<{ editor: Editor }> = ({ editor }) => {
if (!editor) {
return null;
}
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 { Button } from '@douyinfe/semi-ui';
import { Tooltip } from 'components/tooltip';
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 }) => {
if (!editor) {
return null;
}
const isTitleActive = useActive(editor, Title.name);
const isTaskListActive = useActive(editor, TaskListExtension.name);
const toggleTaskList = useCallback(() => editor.chain().focus().toggleTaskList().run(), [editor]);
return (
<Tooltip content="任务列表">
<Button
theme={editor.isActive('taskList') ? 'light' : 'borderless'}
theme={isTaskListActive ? 'light' : 'borderless'}
type="tertiary"
icon={<IconTask />}
onClick={() => editor.chain().focus().toggleTaskList().run()}
disabled={isTitleActive(editor)}
onClick={toggleTaskList}
disabled={isTitleActive}
/>
</Tooltip>
);

View File

@ -1,33 +1,47 @@
import React from 'react';
import React, { useCallback } from 'react';
import { Editor } from '@tiptap/core';
import { Button } from '@douyinfe/semi-ui';
import { IconFont } from '@douyinfe/semi-icons';
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';
type Color = { color: string };
const FlexStyle = {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
} as React.CSSProperties;
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 (
<ColorPicker
onSetColor={(color) => {
color ? editor.chain().focus().setColor(color).run() : editor.chain().focus().unsetColor().run();
}}
disabled={isTitleActive(editor)}
>
<ColorPicker onSetColor={setColor} disabled={isTitleActive}>
<Tooltip content="文本色">
<Button
theme={editor.isActive('textStyle') ? 'light' : 'borderless'}
theme={isTextStyleActive ? 'light' : 'borderless'}
type={'tertiary'}
icon={
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<div style={FlexStyle}>
<IconFont style={{ fontSize: '0.85em' }} />
<span
style={{
@ -38,7 +52,7 @@ export const TextColor: React.FC<{ editor: Editor }> = ({ editor }) => {
></span>
</div>
}
disabled={isTitleActive(editor)}
disabled={isTitleActive}
/>
</Tooltip>
</ColorPicker>

View File

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