mirror of https://github.com/fantasticit/think.git
refactor: improve render performence
This commit is contained in:
parent
5681cc7bd8
commit
5c0d9f54e4
|
@ -64,7 +64,8 @@ export const Editor: React.FC<IProps> = ({ user: currentUser, documentId, author
|
|||
},
|
||||
});
|
||||
}, [documentId, currentUser, toggleLoading]);
|
||||
const editor = useEditor({
|
||||
const editor = useEditor(
|
||||
{
|
||||
editable: authority && authority.editable,
|
||||
extensions: [
|
||||
...BaseKit,
|
||||
|
@ -80,7 +81,9 @@ export const Editor: React.FC<IProps> = ({ user: currentUser, documentId, author
|
|||
//
|
||||
}
|
||||
}, 50),
|
||||
});
|
||||
},
|
||||
[authority, provider]
|
||||
);
|
||||
const [mentionUsersSettingVisible, toggleMentionUsersSettingVisible] = useToggle(false);
|
||||
const [mentionUsers, setMentionUsers] = useState([]);
|
||||
|
||||
|
|
|
@ -120,8 +120,14 @@ export const createKeysLocalStorageLRUCache = (storageKey, capacity) => {
|
|||
const lruCache = new LRUCache(capacity);
|
||||
|
||||
if (USED_STORAGE_KEYS.includes(storageKey)) {
|
||||
// @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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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}>
|
||||
|
|
|
@ -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) => {
|
||||
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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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';
|
||||
|
||||
export const BackgroundColor: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||
const { backgroundColor } = editor.getAttributes('textStyle');
|
||||
const FlexStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
return (
|
||||
<ColorPicker
|
||||
onSetColor={(color) => {
|
||||
export const BackgroundColor: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||
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();
|
||||
}}
|
||||
disabled={isTitleActive(editor)}
|
||||
>
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
return (
|
||||
<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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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 }) => {
|
||||
return (
|
||||
<Tooltip content="清除格式">
|
||||
<Button
|
||||
onClick={() => {
|
||||
const clear = useCallback(() => {
|
||||
editor.chain().focus().unsetAllMarks().run();
|
||||
editor.chain().focus().clearNodes().run();
|
||||
}}
|
||||
icon={<IconClear />}
|
||||
type="tertiary"
|
||||
theme="borderless"
|
||||
/>
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<Tooltip content="清除格式">
|
||||
<Button onClick={clear} icon={<IconClear />} type="tertiary" theme="borderless" />
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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} />;
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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';
|
||||
|
||||
export const TextColor: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||
const { color } = editor.getAttributes('textStyle');
|
||||
type Color = { color: string };
|
||||
|
||||
return (
|
||||
<ColorPicker
|
||||
onSetColor={(color) => {
|
||||
color ? editor.chain().focus().setColor(color).run() : editor.chain().focus().unsetColor().run();
|
||||
}}
|
||||
disabled={isTitleActive(editor)}
|
||||
>
|
||||
<Tooltip content="文本色">
|
||||
<Button
|
||||
theme={editor.isActive('textStyle') ? 'light' : 'borderless'}
|
||||
type={'tertiary'}
|
||||
icon={
|
||||
<div
|
||||
style={{
|
||||
const FlexStyle = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
} as React.CSSProperties;
|
||||
|
||||
export const TextColor: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||
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={setColor} disabled={isTitleActive}>
|
||||
<Tooltip content="文本色">
|
||||
<Button
|
||||
theme={isTextStyleActive ? 'light' : 'borderless'}
|
||||
type={'tertiary'}
|
||||
icon={
|
||||
<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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue