tiptap: fix menus

This commit is contained in:
fantasticit 2022-05-10 11:16:35 +08:00
parent ae204deba2
commit 74957fe014
26 changed files with 204 additions and 109 deletions

View File

@ -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>

View File

@ -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]);

View File

@ -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}>

View File

@ -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>

View File

@ -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="复制">

View File

@ -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="复制">

View File

@ -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>

View File

@ -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}>

View File

@ -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}>

View File

@ -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}>

View File

@ -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

View File

@ -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="复制">

View File

@ -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>

View File

@ -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}>

View File

@ -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="复制">

View File

@ -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>
); );
}; };

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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="复制">

View File

@ -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="向前插入一列">

View File

@ -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="向前插入一行">

View File

@ -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>
);
};

View File

@ -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>

View File

@ -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>
); );
}; };

View File

@ -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);
} }