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