mirror of https://github.com/fantasticit/think.git
tiptap: fix menus
This commit is contained in:
parent
ae204deba2
commit
74957fe014
|
@ -42,6 +42,7 @@ import { DocumentReference } from 'tiptap/editor/menus/document-reference';
|
||||||
import { Image } from 'tiptap/editor/menus/image';
|
import { Image } from 'tiptap/editor/menus/image';
|
||||||
import { Iframe } from 'tiptap/editor/menus/iframe';
|
import { Iframe } from 'tiptap/editor/menus/iframe';
|
||||||
import { Table } from 'tiptap/editor/menus/table';
|
import { Table } from 'tiptap/editor/menus/table';
|
||||||
|
import { Text } from 'tiptap/editor/menus/text';
|
||||||
import { Mind } from 'tiptap/editor/menus/mind';
|
import { Mind } from 'tiptap/editor/menus/mind';
|
||||||
|
|
||||||
const _MenuBar: React.FC<{ editor: Editor }> = ({ editor }) => {
|
const _MenuBar: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
@ -108,6 +109,7 @@ const _MenuBar: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
<Image editor={editor} />
|
<Image editor={editor} />
|
||||||
<Iframe editor={editor} />
|
<Iframe editor={editor} />
|
||||||
<Table editor={editor} />
|
<Table editor={editor} />
|
||||||
|
<Text editor={editor} />
|
||||||
<Mind editor={editor} />
|
<Mind editor={editor} />
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,9 +12,11 @@ export const useActive = (editor: Editor, ...args) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
editor.on('selectionUpdate', listener);
|
editor.on('selectionUpdate', listener);
|
||||||
|
editor.on('transaction', listener);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
editor.off('selectionUpdate', listener);
|
editor.off('selectionUpdate', listener);
|
||||||
|
editor.off('transaction', listener);
|
||||||
};
|
};
|
||||||
}, [editor, args, toggleActive]);
|
}, [editor, args, toggleActive]);
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ export const BackgroundColor: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
<ColorPicker title="背景色" onSetColor={setBackgroundColor} disabled={isTitleActive}>
|
<ColorPicker title="背景色" onSetColor={setBackgroundColor} disabled={isTitleActive}>
|
||||||
<Tooltip content="背景色">
|
<Tooltip content="背景色">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('textStyle') ? 'light' : 'borderless'}
|
theme={backgroundColor ? 'light' : 'borderless'}
|
||||||
type={'tertiary'}
|
type={'tertiary'}
|
||||||
icon={
|
icon={
|
||||||
<span style={FlexStyle}>
|
<span style={FlexStyle}>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Editor } from 'tiptap/editor';
|
import { Editor } from 'tiptap/editor';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconBold } from '@douyinfe/semi-icons';
|
import { IconBold } from '@douyinfe/semi-icons';
|
||||||
|
@ -11,13 +11,15 @@ export const Bold: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
const isTitleActive = useActive(editor, Title.name);
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
const isBoldActive = useActive(editor, BoldExtension.name);
|
const isBoldActive = useActive(editor, BoldExtension.name);
|
||||||
|
|
||||||
|
const toggleBold = useCallback(() => editor.chain().focus().toggleBold().run(), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="粗体">
|
<Tooltip content="粗体">
|
||||||
<Button
|
<Button
|
||||||
theme={isBoldActive ? 'light' : 'borderless'}
|
theme={isBoldActive ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
icon={<IconBold />}
|
icon={<IconBold />}
|
||||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
onClick={toggleBold}
|
||||||
disabled={isTitleActive}
|
disabled={isTitleActive}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -32,6 +32,14 @@ export const CalloutBubbleMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
[editor]
|
[editor]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const shouldShow = useCallback(() => editor.isActive(Callout.name), [editor]);
|
||||||
|
const getRenderContainer = useCallback((node) => {
|
||||||
|
let container = node;
|
||||||
|
while (container && container.id !== 'js-callout-container') {
|
||||||
|
container = container.parentElement;
|
||||||
|
}
|
||||||
|
return container;
|
||||||
|
}, []);
|
||||||
const copyMe = useCallback(() => copyNode(Callout.name, editor), [editor]);
|
const copyMe = useCallback(() => copyNode(Callout.name, editor), [editor]);
|
||||||
const deleteMe = useCallback(() => deleteNode(Callout.name, editor), [editor]);
|
const deleteMe = useCallback(() => deleteNode(Callout.name, editor), [editor]);
|
||||||
|
|
||||||
|
@ -40,14 +48,8 @@ export const CalloutBubbleMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
className={'bubble-menu'}
|
className={'bubble-menu'}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
pluginKey="callout-bubble-menu"
|
pluginKey="callout-bubble-menu"
|
||||||
shouldShow={() => editor.isActive(Callout.name)}
|
shouldShow={shouldShow}
|
||||||
getRenderContainer={(node) => {
|
getRenderContainer={getRenderContainer}
|
||||||
let container = node;
|
|
||||||
while (container && container.id !== 'js-callout-container') {
|
|
||||||
container = container.parentElement;
|
|
||||||
}
|
|
||||||
return container;
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Space spacing={4}>
|
<Space spacing={4}>
|
||||||
<Tooltip content="复制">
|
<Tooltip content="复制">
|
||||||
|
|
|
@ -8,6 +8,14 @@ import { copyNode, deleteNode } from 'tiptap/prose-utils';
|
||||||
import { Divider } from 'tiptap/components/divider';
|
import { Divider } from 'tiptap/components/divider';
|
||||||
|
|
||||||
export const CodeBlockBubbleMenu = ({ editor }) => {
|
export const CodeBlockBubbleMenu = ({ editor }) => {
|
||||||
|
const shouldShow = useCallback(() => editor.isActive(CodeBlock.name), [editor]);
|
||||||
|
const getRenderContainer = useCallback((node) => {
|
||||||
|
let container = node;
|
||||||
|
while (container && container.classList && !container.classList.contains('node-codeBlock')) {
|
||||||
|
container = container.parentElement;
|
||||||
|
}
|
||||||
|
return container;
|
||||||
|
}, []);
|
||||||
const copyMe = useCallback(() => copyNode(CodeBlock.name, editor), [editor]);
|
const copyMe = useCallback(() => copyNode(CodeBlock.name, editor), [editor]);
|
||||||
const deleteMe = useCallback(() => deleteNode(CodeBlock.name, editor), [editor]);
|
const deleteMe = useCallback(() => deleteNode(CodeBlock.name, editor), [editor]);
|
||||||
|
|
||||||
|
@ -16,15 +24,9 @@ export const CodeBlockBubbleMenu = ({ editor }) => {
|
||||||
className={'bubble-menu'}
|
className={'bubble-menu'}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
pluginKey="code-block-bubble-menu"
|
pluginKey="code-block-bubble-menu"
|
||||||
shouldShow={() => editor.isActive(CodeBlock.name)}
|
shouldShow={shouldShow}
|
||||||
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||||
getRenderContainer={(node) => {
|
getRenderContainer={getRenderContainer}
|
||||||
let container = node;
|
|
||||||
while (container && container.classList && !container.classList.contains('node-codeBlock')) {
|
|
||||||
container = container.parentElement;
|
|
||||||
}
|
|
||||||
return container;
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Space spacing={4}>
|
<Space spacing={4}>
|
||||||
<Tooltip content="复制">
|
<Tooltip content="复制">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Editor } from 'tiptap/editor';
|
import { Editor } from 'tiptap/editor';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconCode } from '@douyinfe/semi-icons';
|
import { IconCode } from '@douyinfe/semi-icons';
|
||||||
|
@ -11,13 +11,15 @@ export const Code: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
const isTitleActive = useActive(editor, Title.name);
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
const isCodeActive = useActive(editor, InlineCode.name);
|
const isCodeActive = useActive(editor, InlineCode.name);
|
||||||
|
|
||||||
|
const toggleCode = useCallback(() => editor.chain().focus().toggleCode().run(), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="行内代码">
|
<Tooltip content="行内代码">
|
||||||
<Button
|
<Button
|
||||||
theme={isCodeActive ? 'light' : 'borderless'}
|
theme={isCodeActive ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
icon={<IconCode />}
|
icon={<IconCode />}
|
||||||
onClick={() => editor.chain().focus().toggleCode().run()}
|
onClick={toggleCode}
|
||||||
disabled={isTitleActive}
|
disabled={isTitleActive}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -15,7 +15,7 @@ export const CountdownBubbleMenu = ({ editor }) => {
|
||||||
const openEditLinkModal = useCallback(() => {
|
const openEditLinkModal = useCallback(() => {
|
||||||
triggerOpenCountSettingModal(editor, attrs);
|
triggerOpenCountSettingModal(editor, attrs);
|
||||||
}, [editor, attrs]);
|
}, [editor, attrs]);
|
||||||
|
const shouldShow = useCallback(() => editor.isActive(Countdown.name), [editor]);
|
||||||
const copyMe = useCallback(() => copyNode(Countdown.name, editor), [editor]);
|
const copyMe = useCallback(() => copyNode(Countdown.name, editor), [editor]);
|
||||||
const deleteMe = useCallback(() => deleteNode(Countdown.name, editor), [editor]);
|
const deleteMe = useCallback(() => deleteNode(Countdown.name, editor), [editor]);
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ export const CountdownBubbleMenu = ({ editor }) => {
|
||||||
className={'bubble-menu'}
|
className={'bubble-menu'}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
pluginKey="countdonw-bubble-menu"
|
pluginKey="countdonw-bubble-menu"
|
||||||
shouldShow={() => editor.isActive(Countdown.name)}
|
shouldShow={shouldShow}
|
||||||
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||||
>
|
>
|
||||||
<Space spacing={4}>
|
<Space spacing={4}>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { copyNode, deleteNode } from 'tiptap/prose-utils';
|
||||||
import { Divider } from 'tiptap/components/divider';
|
import { Divider } from 'tiptap/components/divider';
|
||||||
|
|
||||||
export const DocumentChildrenBubbleMenu = ({ editor }) => {
|
export const DocumentChildrenBubbleMenu = ({ editor }) => {
|
||||||
|
const shouldShow = useCallback(() => editor.isActive(DocumentChildren.name), [editor]);
|
||||||
const copyMe = useCallback(() => copyNode(DocumentChildren.name, editor), [editor]);
|
const copyMe = useCallback(() => copyNode(DocumentChildren.name, editor), [editor]);
|
||||||
const deleteMe = useCallback(() => deleteNode(DocumentChildren.name, editor), [editor]);
|
const deleteMe = useCallback(() => deleteNode(DocumentChildren.name, editor), [editor]);
|
||||||
|
|
||||||
|
@ -16,7 +17,7 @@ export const DocumentChildrenBubbleMenu = ({ editor }) => {
|
||||||
className={'bubble-menu'}
|
className={'bubble-menu'}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
pluginKey="document-children-bubble-menu"
|
pluginKey="document-children-bubble-menu"
|
||||||
shouldShow={() => editor.isActive(DocumentChildren.name)}
|
shouldShow={shouldShow}
|
||||||
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||||
>
|
>
|
||||||
<Space spacing={4}>
|
<Space spacing={4}>
|
||||||
|
|
|
@ -19,6 +19,7 @@ export const DocumentReferenceBubbleMenu = ({ editor }) => {
|
||||||
const isShare = pathname.includes('share');
|
const isShare = pathname.includes('share');
|
||||||
const { data: tocs, loading, error } = useWikiTocs(isShare ? null : wikiIdFromUrl);
|
const { data: tocs, loading, error } = useWikiTocs(isShare ? null : wikiIdFromUrl);
|
||||||
|
|
||||||
|
const shouldShow = useCallback(() => editor.isActive(DocumentReference.name), [editor]);
|
||||||
const selectDoc = useCallback(
|
const selectDoc = useCallback(
|
||||||
(item) => {
|
(item) => {
|
||||||
const { wikiId, title, id: documentId } = item;
|
const { wikiId, title, id: documentId } = item;
|
||||||
|
@ -32,7 +33,6 @@ export const DocumentReferenceBubbleMenu = ({ editor }) => {
|
||||||
},
|
},
|
||||||
[editor]
|
[editor]
|
||||||
);
|
);
|
||||||
|
|
||||||
const copyMe = useCallback(() => copyNode(DocumentReference.name, editor), [editor]);
|
const copyMe = useCallback(() => copyNode(DocumentReference.name, editor), [editor]);
|
||||||
const deleteMe = useCallback(() => deleteNode(DocumentReference.name, editor), [editor]);
|
const deleteMe = useCallback(() => deleteNode(DocumentReference.name, editor), [editor]);
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ export const DocumentReferenceBubbleMenu = ({ editor }) => {
|
||||||
className={'bubble-menu'}
|
className={'bubble-menu'}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
pluginKey="document-reference-bubble-menu"
|
pluginKey="document-reference-bubble-menu"
|
||||||
shouldShow={() => editor.isActive(DocumentReference.name)}
|
shouldShow={shouldShow}
|
||||||
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||||
>
|
>
|
||||||
<Space spacing={4}>
|
<Space spacing={4}>
|
||||||
|
|
|
@ -57,7 +57,7 @@ export const IframeBubbleMenu = ({ editor }) => {
|
||||||
},
|
},
|
||||||
[editor]
|
[editor]
|
||||||
);
|
);
|
||||||
|
const shouldShow = useCallback(() => editor.isActive(Iframe.name), [editor]);
|
||||||
const copyMe = useCallback(() => copyNode(Iframe.name, editor), [editor]);
|
const copyMe = useCallback(() => copyNode(Iframe.name, editor), [editor]);
|
||||||
const deleteMe = useCallback(() => deleteNode(Iframe.name, editor), [editor]);
|
const deleteMe = useCallback(() => deleteNode(Iframe.name, editor), [editor]);
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ export const IframeBubbleMenu = ({ editor }) => {
|
||||||
className={'bubble-menu'}
|
className={'bubble-menu'}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
pluginKey="iframe-bubble-menu"
|
pluginKey="iframe-bubble-menu"
|
||||||
shouldShow={() => editor.isActive(Iframe.name)}
|
shouldShow={shouldShow}
|
||||||
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||||
>
|
>
|
||||||
<Modal
|
<Modal
|
||||||
|
|
|
@ -22,6 +22,15 @@ export const ImageBubbleMenu = ({ editor }) => {
|
||||||
const [width, setWidth] = useState(currentWidth);
|
const [width, setWidth] = useState(currentWidth);
|
||||||
const [height, setHeight] = useState(currentHeight);
|
const [height, setHeight] = useState(currentHeight);
|
||||||
|
|
||||||
|
const shouldShow = useCallback(() => editor.isActive(Image.name) && !!editor.getAttributes(Image.name).src, [editor]);
|
||||||
|
const getRenderContainer = useCallback((node) => {
|
||||||
|
try {
|
||||||
|
const inner = node.querySelector('#js-resizeable-container');
|
||||||
|
return inner as HTMLElement;
|
||||||
|
} catch (e) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
const copyMe = useCallback(() => copyNode(Image.name, editor), [editor]);
|
const copyMe = useCallback(() => copyNode(Image.name, editor), [editor]);
|
||||||
const deleteMe = useCallback(() => deleteNode(Image.name, editor), [editor]);
|
const deleteMe = useCallback(() => deleteNode(Image.name, editor), [editor]);
|
||||||
|
|
||||||
|
@ -75,18 +84,11 @@ export const ImageBubbleMenu = ({ editor }) => {
|
||||||
className={'bubble-menu'}
|
className={'bubble-menu'}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
pluginKey="image-bubble-menu"
|
pluginKey="image-bubble-menu"
|
||||||
shouldShow={() => editor.isActive(Image.name) && !!editor.getAttributes(Image.name).src}
|
shouldShow={shouldShow}
|
||||||
tippyOptions={{
|
tippyOptions={{
|
||||||
maxWidth: 'calc(100vw - 100px)',
|
maxWidth: 'calc(100vw - 100px)',
|
||||||
}}
|
}}
|
||||||
getRenderContainer={(node) => {
|
getRenderContainer={getRenderContainer}
|
||||||
try {
|
|
||||||
const inner = node.querySelector('#js-resizeable-container');
|
|
||||||
return inner as HTMLElement;
|
|
||||||
} catch (e) {
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Space spacing={4}>
|
<Space spacing={4}>
|
||||||
<Tooltip content="复制">
|
<Tooltip content="复制">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Editor } from 'tiptap/editor';
|
import { Editor } from 'tiptap/editor';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconItalic } from '@douyinfe/semi-icons';
|
import { IconItalic } from '@douyinfe/semi-icons';
|
||||||
|
@ -11,13 +11,15 @@ export const Italic: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
const isTitleActive = useActive(editor, Title.name);
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
const isItalicActive = useActive(editor, ItalicExtension.name);
|
const isItalicActive = useActive(editor, ItalicExtension.name);
|
||||||
|
|
||||||
|
const toggleItalic = useCallback(() => editor.chain().focus().toggleItalic().run(), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="斜体">
|
<Tooltip content="斜体">
|
||||||
<Button
|
<Button
|
||||||
theme={isItalicActive ? 'light' : 'borderless'}
|
theme={isItalicActive ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
icon={<IconItalic />}
|
icon={<IconItalic />}
|
||||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
onClick={toggleItalic}
|
||||||
disabled={isTitleActive}
|
disabled={isTitleActive}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -15,6 +15,8 @@ export const LinkBubbleMenu = ({ editor }) => {
|
||||||
const [from, setFrom] = useState(-1);
|
const [from, setFrom] = useState(-1);
|
||||||
const [to, setTo] = useState(-1);
|
const [to, setTo] = useState(-1);
|
||||||
|
|
||||||
|
const shouldShow = useCallback(() => editor.isActive(Link.name), [editor]);
|
||||||
|
|
||||||
const visitLink = useCallback(() => {
|
const visitLink = useCallback(() => {
|
||||||
window.open(href, target);
|
window.open(href, target);
|
||||||
}, [href, target]);
|
}, [href, target]);
|
||||||
|
@ -66,7 +68,7 @@ export const LinkBubbleMenu = ({ editor }) => {
|
||||||
className={'bubble-menu'}
|
className={'bubble-menu'}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
pluginKey="link-bubble-menu"
|
pluginKey="link-bubble-menu"
|
||||||
shouldShow={() => editor.isActive(Link.name)}
|
shouldShow={shouldShow}
|
||||||
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||||
>
|
>
|
||||||
<Space spacing={4}>
|
<Space spacing={4}>
|
||||||
|
|
|
@ -19,7 +19,15 @@ export const MindBubbleMenu = ({ editor }) => {
|
||||||
},
|
},
|
||||||
[editor]
|
[editor]
|
||||||
);
|
);
|
||||||
|
const shouldShow = useCallback(() => editor.isActive(Mind.name), [editor]);
|
||||||
|
const getRenderContainer = useCallback((node) => {
|
||||||
|
try {
|
||||||
|
const inner = node.querySelector('#js-resizeable-container');
|
||||||
|
return inner as HTMLElement;
|
||||||
|
} catch (e) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
const copyMe = useCallback(() => copyNode(Mind.name, editor), [editor]);
|
const copyMe = useCallback(() => copyNode(Mind.name, editor), [editor]);
|
||||||
const deleteMe = useCallback(() => deleteNode(Mind.name, editor), [editor]);
|
const deleteMe = useCallback(() => deleteNode(Mind.name, editor), [editor]);
|
||||||
|
|
||||||
|
@ -28,16 +36,9 @@ export const MindBubbleMenu = ({ editor }) => {
|
||||||
className={'bubble-menu'}
|
className={'bubble-menu'}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
pluginKey="mind-bubble-menu"
|
pluginKey="mind-bubble-menu"
|
||||||
shouldShow={() => editor.isActive(Mind.name)}
|
|
||||||
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||||
getRenderContainer={(node) => {
|
shouldShow={shouldShow}
|
||||||
try {
|
getRenderContainer={getRenderContainer}
|
||||||
const inner = node.querySelector('#js-resizeable-container');
|
|
||||||
return inner as HTMLElement;
|
|
||||||
} catch (e) {
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Space spacing={4}>
|
<Space spacing={4}>
|
||||||
<Tooltip content="复制">
|
<Tooltip content="复制">
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Editor } from 'tiptap/editor';
|
import { Editor } from 'tiptap/editor';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconRedo } from '@douyinfe/semi-icons';
|
import { IconRedo } from '@douyinfe/semi-icons';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
|
||||||
export const Redo: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Redo: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
const redo = useCallback(() => editor.chain().focus().redo().run(), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="撤销">
|
<Tooltip content="撤销">
|
||||||
<Button
|
<Button onClick={redo} icon={<IconRedo />} type="tertiary" theme="borderless" />
|
||||||
onClick={() => editor.chain().focus().redo().run()}
|
|
||||||
icon={<IconRedo />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Editor } from 'tiptap/editor';
|
import { Editor } from 'tiptap/editor';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconStrikeThrough } from '@douyinfe/semi-icons';
|
import { IconStrikeThrough } from '@douyinfe/semi-icons';
|
||||||
|
@ -11,13 +11,15 @@ export const Strike: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
const isTitleActive = useActive(editor, Title.name);
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
const isStrikeActive = useActive(editor, StrikeExtension.name);
|
const isStrikeActive = useActive(editor, StrikeExtension.name);
|
||||||
|
|
||||||
|
const toggleStrike = useCallback(() => editor.chain().focus().toggleStrike().run(), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="删除线">
|
<Tooltip content="删除线">
|
||||||
<Button
|
<Button
|
||||||
theme={isStrikeActive ? 'light' : 'borderless'}
|
theme={isStrikeActive ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
icon={<IconStrikeThrough />}
|
icon={<IconStrikeThrough />}
|
||||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
onClick={toggleStrike}
|
||||||
disabled={isTitleActive}
|
disabled={isTitleActive}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconSub } from 'components/icons';
|
import { IconSub } from 'components/icons';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
@ -10,13 +10,15 @@ export const Subscript: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
const isTitleActive = useActive(editor, Title.name);
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
const isSubscriptActive = useActive(editor, SubscriptExtension.name);
|
const isSubscriptActive = useActive(editor, SubscriptExtension.name);
|
||||||
|
|
||||||
|
const toggleSubscript = useCallback(() => editor.chain().focus().toggleSubscript().run(), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="下标">
|
<Tooltip content="下标">
|
||||||
<Button
|
<Button
|
||||||
theme={isSubscriptActive ? 'light' : 'borderless'}
|
theme={isSubscriptActive ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
icon={<IconSub />}
|
icon={<IconSub />}
|
||||||
onClick={() => editor.chain().focus().toggleSubscript().run()}
|
onClick={toggleSubscript}
|
||||||
disabled={isTitleActive}
|
disabled={isTitleActive}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconSup } from 'components/icons';
|
import { IconSup } from 'components/icons';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
@ -10,13 +10,15 @@ export const Superscript: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
const isTitleActive = useActive(editor, Title.name);
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
const isSuperscriptActive = useActive(editor, SuperscriptExtension.name);
|
const isSuperscriptActive = useActive(editor, SuperscriptExtension.name);
|
||||||
|
|
||||||
|
const toggleSuperscript = useCallback(() => editor.chain().focus().toggleSuperscript().run(), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="上标">
|
<Tooltip content="上标">
|
||||||
<Button
|
<Button
|
||||||
theme={isSuperscriptActive ? 'light' : 'borderless'}
|
theme={isSuperscriptActive ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
icon={<IconSup />}
|
icon={<IconSup />}
|
||||||
onClick={() => editor.chain().focus().toggleSuperscript().run()}
|
onClick={toggleSuperscript}
|
||||||
disabled={isTitleActive}
|
disabled={isTitleActive}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -22,6 +22,16 @@ import { Table } from 'tiptap/core/extensions/table';
|
||||||
import { copyNode, deleteNode } from 'tiptap/prose-utils';
|
import { copyNode, deleteNode } from 'tiptap/prose-utils';
|
||||||
|
|
||||||
export const TableBubbleMenu = ({ editor }) => {
|
export const TableBubbleMenu = ({ editor }) => {
|
||||||
|
const shouldShow = useCallback(() => {
|
||||||
|
return editor.isActive(Table.name);
|
||||||
|
}, [editor]);
|
||||||
|
const getRenderContainer = useCallback((node) => {
|
||||||
|
let container = node;
|
||||||
|
while (container.tagName !== 'TABLE') {
|
||||||
|
container = container.parentElement;
|
||||||
|
}
|
||||||
|
return container.parentElement;
|
||||||
|
}, []);
|
||||||
const copyMe = useCallback(() => copyNode(Table.name, editor), [editor]);
|
const copyMe = useCallback(() => copyNode(Table.name, editor), [editor]);
|
||||||
const deleteMe = useCallback(() => {
|
const deleteMe = useCallback(() => {
|
||||||
deleteNode(Table.name, editor);
|
deleteNode(Table.name, editor);
|
||||||
|
@ -47,16 +57,8 @@ export const TableBubbleMenu = ({ editor }) => {
|
||||||
maxWidth: 'calc(100vw - 100px)',
|
maxWidth: 'calc(100vw - 100px)',
|
||||||
placement: 'bottom',
|
placement: 'bottom',
|
||||||
}}
|
}}
|
||||||
shouldShow={() => {
|
shouldShow={shouldShow}
|
||||||
return editor.isActive(Table.name);
|
getRenderContainer={getRenderContainer}
|
||||||
}}
|
|
||||||
getRenderContainer={(node) => {
|
|
||||||
let container = node;
|
|
||||||
while (container.tagName !== 'TABLE') {
|
|
||||||
container = container.parentElement;
|
|
||||||
}
|
|
||||||
return container.parentElement;
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Space spacing={4}>
|
<Space spacing={4}>
|
||||||
<Tooltip content="复制">
|
<Tooltip content="复制">
|
||||||
|
|
|
@ -7,6 +7,17 @@ import { Table } from 'tiptap/core/extensions/table';
|
||||||
import { isTableSelected } from 'tiptap/prose-utils';
|
import { isTableSelected } from 'tiptap/prose-utils';
|
||||||
|
|
||||||
export const TableColBubbleMenu = ({ editor }) => {
|
export const TableColBubbleMenu = ({ editor }) => {
|
||||||
|
const shouldShow = useCallback(
|
||||||
|
({ node, state }) => {
|
||||||
|
if (!editor.isActive(Table.name) || !node || isTableSelected(state.selection)) return false;
|
||||||
|
const gripColumn = node.querySelector('a.grip-column.selected');
|
||||||
|
return !!gripColumn;
|
||||||
|
},
|
||||||
|
[editor]
|
||||||
|
);
|
||||||
|
const getRenderContainer = useCallback((node) => {
|
||||||
|
return node;
|
||||||
|
}, []);
|
||||||
const addColumnBefore = useCallback(() => editor.chain().focus().addColumnBefore().run(), [editor]);
|
const addColumnBefore = useCallback(() => editor.chain().focus().addColumnBefore().run(), [editor]);
|
||||||
const addColumnAfter = useCallback(() => editor.chain().focus().addColumnAfter().run(), [editor]);
|
const addColumnAfter = useCallback(() => editor.chain().focus().addColumnAfter().run(), [editor]);
|
||||||
const deleteColumn = useCallback(() => editor.chain().focus().deleteColumn().run(), [editor]);
|
const deleteColumn = useCallback(() => editor.chain().focus().deleteColumn().run(), [editor]);
|
||||||
|
@ -19,14 +30,8 @@ export const TableColBubbleMenu = ({ editor }) => {
|
||||||
tippyOptions={{
|
tippyOptions={{
|
||||||
offset: [0, 20],
|
offset: [0, 20],
|
||||||
}}
|
}}
|
||||||
shouldShow={({ node, state }) => {
|
shouldShow={shouldShow}
|
||||||
if (!editor.isActive(Table.name) || !node || isTableSelected(state.selection)) return false;
|
getRenderContainer={getRenderContainer}
|
||||||
const gripColumn = node.querySelector('a.grip-column.selected');
|
|
||||||
return !!gripColumn;
|
|
||||||
}}
|
|
||||||
getRenderContainer={(node) => {
|
|
||||||
return node;
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Space spacing={4}>
|
<Space spacing={4}>
|
||||||
<Tooltip content="向前插入一列">
|
<Tooltip content="向前插入一列">
|
||||||
|
|
|
@ -7,6 +7,17 @@ import { Table } from 'tiptap/core/extensions/table';
|
||||||
import { isTableSelected } from 'tiptap/prose-utils';
|
import { isTableSelected } from 'tiptap/prose-utils';
|
||||||
|
|
||||||
export const TableRowBubbleMenu = ({ editor }) => {
|
export const TableRowBubbleMenu = ({ editor }) => {
|
||||||
|
const shouldShow = useCallback(
|
||||||
|
({ node, state }) => {
|
||||||
|
if (!editor.isActive(Table.name) || !node || isTableSelected(state.selection)) return false;
|
||||||
|
const gripRow = node.querySelector('a.grip-row.selected');
|
||||||
|
return !!gripRow;
|
||||||
|
},
|
||||||
|
[editor]
|
||||||
|
);
|
||||||
|
const getRenderContainer = useCallback((node) => {
|
||||||
|
return node;
|
||||||
|
}, []);
|
||||||
const addRowBefore = useCallback(() => editor.chain().focus().addRowBefore().run(), [editor]);
|
const addRowBefore = useCallback(() => editor.chain().focus().addRowBefore().run(), [editor]);
|
||||||
const addRowAfter = useCallback(() => editor.chain().focus().addRowAfter().run(), [editor]);
|
const addRowAfter = useCallback(() => editor.chain().focus().addRowAfter().run(), [editor]);
|
||||||
const deleteRow = useCallback(() => editor.chain().focus().deleteRow().run(), [editor]);
|
const deleteRow = useCallback(() => editor.chain().focus().deleteRow().run(), [editor]);
|
||||||
|
@ -20,14 +31,8 @@ export const TableRowBubbleMenu = ({ editor }) => {
|
||||||
placement: 'left',
|
placement: 'left',
|
||||||
offset: [0, 20],
|
offset: [0, 20],
|
||||||
}}
|
}}
|
||||||
shouldShow={({ node, state }) => {
|
shouldShow={shouldShow}
|
||||||
if (!editor.isActive(Table.name) || !node || isTableSelected(state.selection)) return false;
|
getRenderContainer={getRenderContainer}
|
||||||
const gripRow = node.querySelector('a.grip-row.selected');
|
|
||||||
return !!gripRow;
|
|
||||||
}}
|
|
||||||
getRenderContainer={(node) => {
|
|
||||||
return node;
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Space vertical spacing={4}>
|
<Space vertical spacing={4}>
|
||||||
<Tooltip content="向前插入一行">
|
<Tooltip content="向前插入一行">
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { Space } from '@douyinfe/semi-ui';
|
||||||
|
import { BubbleMenu } from 'tiptap/editor/views/bubble-menu';
|
||||||
|
|
||||||
|
import { Bold } from '../bold';
|
||||||
|
import { Italic } from '../italic';
|
||||||
|
import { Underline } from '../underline';
|
||||||
|
import { Strike } from '../strike';
|
||||||
|
import { Code } from '../code';
|
||||||
|
import { Superscript } from '../superscript';
|
||||||
|
import { Subscript } from '../subscript';
|
||||||
|
import { TextColor } from '../text-color';
|
||||||
|
import { BackgroundColor } from '../background-color';
|
||||||
|
|
||||||
|
import { Title } from 'tiptap/core/extensions/title';
|
||||||
|
import { Link } from 'tiptap/core/extensions/link';
|
||||||
|
import { Attachment } from 'tiptap/core/extensions/attachment';
|
||||||
|
import { Image } from 'tiptap/core/extensions/image';
|
||||||
|
import { Callout } from 'tiptap/core/extensions/callout';
|
||||||
|
import { CodeBlock } from 'tiptap/core/extensions/code-block';
|
||||||
|
import { Iframe } from 'tiptap/core/extensions/iframe';
|
||||||
|
import { Mind } from 'tiptap/core/extensions/mind';
|
||||||
|
import { Table } from 'tiptap/core/extensions/table';
|
||||||
|
import { Katex } from 'tiptap/core/extensions/katex';
|
||||||
|
import { DocumentReference } from 'tiptap/core/extensions/document-reference';
|
||||||
|
import { DocumentChildren } from 'tiptap/core/extensions/document-children';
|
||||||
|
|
||||||
|
const OTHER_BUBBLE_MENU_TYPES = [
|
||||||
|
Title.name,
|
||||||
|
Link.name,
|
||||||
|
Attachment.name,
|
||||||
|
Image.name,
|
||||||
|
Callout.name,
|
||||||
|
CodeBlock.name,
|
||||||
|
Iframe.name,
|
||||||
|
Mind.name,
|
||||||
|
Table.name,
|
||||||
|
DocumentReference.name,
|
||||||
|
DocumentChildren.name,
|
||||||
|
Katex.name,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Text = ({ editor }) => {
|
||||||
|
const shouldShow = useCallback(
|
||||||
|
() => !editor.state.selection.empty && OTHER_BUBBLE_MENU_TYPES.every((type) => !editor.isActive(type)),
|
||||||
|
[editor]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BubbleMenu
|
||||||
|
className={'bubble-menu'}
|
||||||
|
editor={editor}
|
||||||
|
pluginKey="code-block-bubble-menu"
|
||||||
|
shouldShow={shouldShow}
|
||||||
|
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||||
|
>
|
||||||
|
<Space spacing={4}>
|
||||||
|
<Bold editor={editor} />
|
||||||
|
<Italic editor={editor} />
|
||||||
|
<Underline editor={editor} />
|
||||||
|
<Strike editor={editor} />
|
||||||
|
<Code editor={editor} />
|
||||||
|
<Superscript editor={editor} />
|
||||||
|
<Subscript editor={editor} />
|
||||||
|
<TextColor editor={editor} />
|
||||||
|
<BackgroundColor editor={editor} />
|
||||||
|
</Space>
|
||||||
|
</BubbleMenu>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Editor } from 'tiptap/editor';
|
import { Editor } from 'tiptap/editor';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconUnderline } from '@douyinfe/semi-icons';
|
import { IconUnderline } from '@douyinfe/semi-icons';
|
||||||
|
@ -9,13 +9,15 @@ import { Title } from 'tiptap/core/extensions/title';
|
||||||
export const Underline: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Underline: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
const isTitleActive = useActive(editor, Title.name);
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
|
|
||||||
|
const toggleUnderline = useCallback(() => editor.chain().focus().toggleUnderline().run(), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="下划线">
|
<Tooltip content="下划线">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('underline') ? 'light' : 'borderless'}
|
theme={editor.isActive('underline') ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
icon={<IconUnderline />}
|
icon={<IconUnderline />}
|
||||||
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
onClick={toggleUnderline}
|
||||||
disabled={isTitleActive}
|
disabled={isTitleActive}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Editor } from 'tiptap/editor';
|
import { Editor } from 'tiptap/editor';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconUndo } from '@douyinfe/semi-icons';
|
import { IconUndo } from '@douyinfe/semi-icons';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
|
||||||
export const Undo: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Undo: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
const undo = useCallback(() => editor.chain().focus().undo().run(), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="撤销">
|
<Tooltip content="撤销">
|
||||||
<Button
|
<Button onClick={undo} icon={<IconUndo />} type="tertiary" theme="borderless" />
|
||||||
onClick={() => editor.chain().focus().undo().run()}
|
|
||||||
icon={<IconUndo />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,3 @@
|
||||||
export function isValidURL(str) {
|
export function isValidURL(str: string) {
|
||||||
const pattern = new RegExp(
|
return str.startsWith('http');
|
||||||
'^(https?:\\/\\/)?' + // protocol
|
|
||||||
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name
|
|
||||||
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
|
|
||||||
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path
|
|
||||||
'(\\?[;&a-z\\d%_.~+=-]*)?' + // query string
|
|
||||||
'(\\#[-a-z\\d_]*)?$',
|
|
||||||
'i'
|
|
||||||
);
|
|
||||||
return !!pattern.test(str);
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue