diff --git a/packages/client/package.json b/packages/client/package.json
index 36114bcd..49d1e6c0 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -56,7 +56,6 @@
"@tiptap/suggestion": "^2.0.0-beta.90",
"axios": "^0.25.0",
"classnames": "^2.3.1",
- "copy-to-clipboard": "^3.3.1",
"deep-equal": "^2.0.5",
"dompurify": "^2.3.5",
"interactjs": "^1.10.11",
@@ -85,6 +84,7 @@
"scroll-into-view-if-needed": "^2.2.29",
"swr": "^1.2.0",
"tippy.js": "^6.3.7",
+ "toggle-selection": "^1.0.6",
"viewerjs": "^1.10.4",
"y-indexeddb": "^9.0.7",
"y-prosemirror": "^1.0.14",
diff --git a/packages/client/src/helpers/copy-to-clipboard.js b/packages/client/src/helpers/copy-to-clipboard.js
new file mode 100644
index 00000000..c351004e
--- /dev/null
+++ b/packages/client/src/helpers/copy-to-clipboard.js
@@ -0,0 +1,147 @@
+'use strict';
+
+var deselectCurrent = require('toggle-selection');
+
+var clipboardToIE11Formatting = {
+ 'text/plain': 'Text',
+ 'text/html': 'Url',
+ 'default': 'Text',
+};
+
+var defaultMessage = 'Copy to clipboard: #{key}, Enter';
+
+function format(message) {
+ var copyKey = (/mac os x/i.test(navigator.userAgent) ? '⌘' : 'Ctrl') + '+C';
+ return message.replace(/#{\s*key\s*}/g, copyKey);
+}
+
+function copy(text, options) {
+ var debug,
+ message,
+ reselectPrevious,
+ range,
+ selection,
+ mark,
+ success = false;
+ if (!options) {
+ options = {};
+ }
+ debug = options.debug || false;
+ try {
+ reselectPrevious = deselectCurrent();
+
+ range = document.createRange();
+ selection = document.getSelection();
+
+ mark = document.createElement('span');
+ mark.textContent = text;
+ // reset user styles for span element
+ mark.style.all = 'unset';
+ // prevents scrolling to the end of the page
+ mark.style.position = 'fixed';
+ mark.style.top = 0;
+ mark.style.clip = 'rect(0, 0, 0, 0)';
+ // used to preserve spaces and line breaks
+ mark.style.whiteSpace = 'pre';
+ // do not inherit user-select (it may be `none`)
+ mark.style.webkitUserSelect = 'text';
+ mark.style.MozUserSelect = 'text';
+ mark.style.msUserSelect = 'text';
+ mark.style.userSelect = 'text';
+ mark.addEventListener('copy', function (e) {
+ e.stopPropagation();
+ if (options.format) {
+ e.preventDefault();
+ if (typeof e.clipboardData === 'undefined') {
+ // IE 11
+ debug && console.warn('unable to use e.clipboardData');
+ debug && console.warn('trying IE specific stuff');
+ window.clipboardData.clearData();
+ var format = clipboardToIE11Formatting[options.format] || clipboardToIE11Formatting['default'];
+ if (Array.isArray(text)) {
+ text.forEach((item) => {
+ if (typeof item === 'string') {
+ window.clipboardData.setData(item, item);
+ } else {
+ window.clipboardData.setData(item.format || format, item.text || item);
+ }
+ });
+ } else {
+ window.clipboardData.setData(format, text);
+ }
+ } else {
+ // all other browsers
+ e.clipboardData.clearData();
+ if (Array.isArray(text)) {
+ text.forEach((item) => {
+ if (typeof item === 'string') {
+ e.clipboardData.setData(item, item);
+ } else {
+ e.clipboardData.setData(item.format || format, item.text || item);
+ }
+ });
+ console.log(e.clipboardData);
+ } else {
+ e.clipboardData.setData(format, text);
+ }
+ }
+ }
+ if (options.onCopy) {
+ e.preventDefault();
+ options.onCopy(e.clipboardData);
+ }
+ });
+
+ document.body.appendChild(mark);
+
+ range.selectNodeContents(mark);
+ selection.addRange(range);
+
+ var successful = document.execCommand('copy');
+ if (!successful) {
+ throw new Error('copy command was unsuccessful');
+ }
+ success = true;
+ } catch (err) {
+ debug && console.error('unable to copy using execCommand: ', err);
+ debug && console.warn('trying IE specific stuff');
+ try {
+ if (Array.isArray(text)) {
+ text.forEach((item) => {
+ if (typeof item === 'string') {
+ window.clipboardData.setData(item, item);
+ } else {
+ window.clipboardData.setData(item.format || format, item.text || item);
+ }
+ });
+ } else {
+ window.clipboardData.setData(format, text);
+ }
+ console.log(window.clipboardData, '1');
+ options.onCopy && options.onCopy(window.clipboardData);
+ success = true;
+ } catch (err) {
+ debug && console.error('unable to copy using clipboardData: ', err);
+ debug && console.error('falling back to prompt');
+ message = format('message' in options ? options.message : defaultMessage);
+ window.prompt(message, text);
+ }
+ } finally {
+ if (selection) {
+ if (typeof selection.removeRange == 'function') {
+ selection.removeRange(range);
+ } else {
+ selection.removeAllRanges();
+ }
+ }
+
+ if (mark) {
+ document.body.removeChild(mark);
+ }
+ reselectPrevious();
+ }
+
+ return success;
+}
+
+module.exports = copy;
diff --git a/packages/client/src/helpers/copy.tsx b/packages/client/src/helpers/copy.tsx
index b67e005c..91746962 100644
--- a/packages/client/src/helpers/copy.tsx
+++ b/packages/client/src/helpers/copy.tsx
@@ -1,7 +1,16 @@
-import _copy from 'copy-to-clipboard';
+import _copy from './copy-to-clipboard';
import { Toast } from '@douyinfe/semi-ui';
-export function copy(text: string, msg = '复制成功') {
- Toast.success(msg);
- return _copy(text);
+interface Options {
+ debug?: boolean;
+ message?: string;
+ format?: string; // MIME type
+ onCopy?: (clipboardData: object) => void;
+}
+
+export function copy(text: string | { text: string; format: string }[], options?: Options) {
+ options = options || {};
+ options.onCopy = options.onCopy || (() => Toast.success(options.message || '复制成功'));
+ options.format = options.format || 'text/plain';
+ return _copy(text, options);
}
diff --git a/packages/client/src/tiptap/extensions/paste.ts b/packages/client/src/tiptap/extensions/paste.ts
index e29bd772..f7987793 100644
--- a/packages/client/src/tiptap/extensions/paste.ts
+++ b/packages/client/src/tiptap/extensions/paste.ts
@@ -8,6 +8,8 @@ import {
markdownToProsemirror,
prosemirrorToMarkdown,
} from 'tiptap/markdown/markdown-to-prosemirror';
+import { copyNode } from 'tiptap/prose-utils';
+import { safeJSONParse } from 'helpers/json';
const isPureText = (content): boolean => {
if (!content) return false;
@@ -43,7 +45,6 @@ export const Paste = Extension.create({
if (!event.clipboardData) return false;
const files = Array.from(event.clipboardData.files);
-
if (files.length) {
event.preventDefault();
files.forEach((file) => {
@@ -55,6 +56,16 @@ export const Paste = Extension.create({
const text = event.clipboardData.getData('text/plain');
const html = event.clipboardData.getData('text/html');
const vscode = event.clipboardData.getData('vscode-editor-data');
+ const node = event.clipboardData.getData('text/node');
+ const markdownText = event.clipboardData.getData('text/markdown');
+
+ if (node) {
+ const doc = safeJSONParse(node);
+ let tr = view.state.tr;
+ const selection = tr.selection;
+ view.dispatch(tr.insert(selection.from - 1, view.state.schema.nodeFromJSON(doc)).scrollIntoView());
+ return true;
+ }
// 粘贴代码
if (isInCode(view.state)) {
@@ -80,14 +91,14 @@ export const Paste = Extension.create({
}
// 处理 markdown
- if (isMarkdown(text) || html.length === 0 || pasteCodeLanguage === 'markdown') {
+ if (markdownText || isMarkdown(text) || html.length === 0 || pasteCodeLanguage === 'markdown') {
event.preventDefault();
const firstNode = view.props.state.doc.content.firstChild;
const hasTitle = isTitleNode(firstNode) && firstNode.content.size > 0;
const schema = view.props.state.schema;
const doc = markdownToProsemirror({
schema,
- content: normalizePastedMarkdown(text),
+ content: normalizePastedMarkdown(markdownText || text),
hasTitle,
});
let tr = view.state.tr;
@@ -130,8 +141,41 @@ export const Paste = Extension.create({
return false;
},
+ handleKeyDown(view, event) {
+ /**
+ * Command + C
+ * Ctrl + C
+ */
+ if ((event.ctrlKey || event.metaKey) && event.keyCode == 67) {
+ const { state } = view;
+ const $pos = state.selection.$anchor;
+ // @ts-ignore
+ const currentNode = state.selection.node;
+ let targetNode = null;
+
+ if (currentNode) {
+ targetNode = currentNode;
+ } else {
+ if ($pos.depth) {
+ for (let d = $pos.depth; d > 0; d--) {
+ const node = $pos.node(d);
+ targetNode = node;
+ }
+ }
+ }
+
+ if (currentNode) {
+ event.preventDefault();
+ copyNode(currentNode);
+ return true;
+ }
+ }
+
+ return false;
+ },
clipboardTextSerializer: (slice) => {
const isText = isPureText(slice.content.toJSON());
+
if (isText) {
return slice.content.textBetween(0, slice.content.size, '\n\n');
}
diff --git a/packages/client/src/tiptap/markdown/markdown-to-prosemirror/helpers.ts b/packages/client/src/tiptap/markdown/markdown-to-prosemirror/helpers.ts
index 889c17a9..9a8c420e 100644
--- a/packages/client/src/tiptap/markdown/markdown-to-prosemirror/helpers.ts
+++ b/packages/client/src/tiptap/markdown/markdown-to-prosemirror/helpers.ts
@@ -3,6 +3,10 @@ export const isMarkdown = (text: string): boolean => {
const html = text.match(/<\/?[a-z][\s\S]*>/i);
if (html && html.length) return true;
+ // image
+ const image = text.match(/!\[(\s|.)?\]\((\s|.)?\)/);
+ if (image && image.length) return true;
+
// table
const tables = text.match(/^\|(\S)*\|/gm);
if (tables && tables.length) return true;
diff --git a/packages/client/src/tiptap/markdown/markdown-to-prosemirror/markdown-to-html/markdownTaskList.ts b/packages/client/src/tiptap/markdown/markdown-to-prosemirror/markdown-to-html/markdownTaskList.ts
index 0c1e0f0c..980ea3b9 100644
--- a/packages/client/src/tiptap/markdown/markdown-to-prosemirror/markdown-to-html/markdownTaskList.ts
+++ b/packages/client/src/tiptap/markdown/markdown-to-prosemirror/markdown-to-html/markdownTaskList.ts
@@ -63,7 +63,7 @@ function attrSet(token, name, value) {
function processToken(state: StateCore, options: TaskListsOptions): boolean {
const allTokens = state.tokens;
- attrSet(allTokens[0], 'class', 'contains-task-list');
+ attrSet(allTokens[0], 'class', '');
for (let i = 2; i < allTokens.length; i++) {
if (!isTodoItem(allTokens, i)) {
diff --git a/packages/client/src/tiptap/markdown/prosemirror-to-markdown/index.ts b/packages/client/src/tiptap/markdown/prosemirror-to-markdown/index.ts
index 46b82ee3..e6e8625b 100644
--- a/packages/client/src/tiptap/markdown/prosemirror-to-markdown/index.ts
+++ b/packages/client/src/tiptap/markdown/prosemirror-to-markdown/index.ts
@@ -91,7 +91,7 @@ const SerializerConfig = {
},
nodes: {
- [Attachment.name]: renderCustomContainer('attachment'),
+ attachment: renderCustomContainer('attachment'),
blockquote: (state, node) => {
if (node.attrs.multiline) {
state.write('>>>');
diff --git a/packages/client/src/tiptap/menubar.tsx b/packages/client/src/tiptap/menubar.tsx
index c5e317be..ff836efc 100644
--- a/packages/client/src/tiptap/menubar.tsx
+++ b/packages/client/src/tiptap/menubar.tsx
@@ -32,7 +32,9 @@ import { Blockquote } from './menus/blockquote';
import { HorizontalRule } from './menus/horizontal-rule';
import { Search } from './menus/search';
+import { Attachment } from './menus/attachment';
import { Callout } from './menus/callout';
+import { CodeBlock } from './menus/code-block';
import { Countdonw } from './menus/countdown';
import { DocumentChildren } from './menus/document-children';
import { DocumentReference } from './menus/document-reference';
@@ -90,7 +92,9 @@ export const MenuBar: React.FC<{ editor: any }> = ({ editor }) => {
+
+
diff --git a/packages/client/src/tiptap/menus/attachment/bubble.tsx b/packages/client/src/tiptap/menus/attachment/bubble.tsx
new file mode 100644
index 00000000..28286e8a
--- /dev/null
+++ b/packages/client/src/tiptap/menus/attachment/bubble.tsx
@@ -0,0 +1,43 @@
+import { Space, Button } from '@douyinfe/semi-ui';
+import { IconCopy, IconDelete } from '@douyinfe/semi-icons';
+import { Tooltip } from 'components/tooltip';
+import { BubbleMenu } from 'tiptap/views/bubble-menu';
+import { Attachment } from 'tiptap/extensions/attachment';
+import { copyNode, deleteNode } from 'tiptap/prose-utils';
+import { Divider } from 'tiptap/divider';
+
+export const AttachmentBubbleMenu = ({ editor }) => {
+ return (
+ editor.isActive(Attachment.name)}
+ tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
+ >
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/client/src/tiptap/menus/attachment/index.tsx b/packages/client/src/tiptap/menus/attachment/index.tsx
new file mode 100644
index 00000000..ff4876cd
--- /dev/null
+++ b/packages/client/src/tiptap/menus/attachment/index.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import { Editor } from '@tiptap/core';
+import { AttachmentBubbleMenu } from './bubble';
+
+export const Attachment: React.FC<{ editor: Editor }> = ({ editor }) => {
+ if (!editor) {
+ return null;
+ }
+
+ return (
+ <>
+
+ >
+ );
+};
diff --git a/packages/client/src/tiptap/menus/callout/bubble.tsx b/packages/client/src/tiptap/menus/callout/bubble.tsx
index eff1e6e8..c81c8ded 100644
--- a/packages/client/src/tiptap/menus/callout/bubble.tsx
+++ b/packages/client/src/tiptap/menus/callout/bubble.tsx
@@ -1,13 +1,13 @@
import { useCallback } from 'react';
import { Editor } from '@tiptap/core';
import { Space, Button, Popover, Typography } from '@douyinfe/semi-ui';
-import { IconDelete } from '@douyinfe/semi-icons';
+import { IconCopy, IconDelete } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip';
import { IconDrawBoard } from 'components/icons';
import { BubbleMenu } from 'tiptap/views/bubble-menu';
import { Divider } from 'tiptap/divider';
import { Callout } from 'tiptap/extensions/callout';
-import { deleteNode } from 'tiptap/prose-utils';
+import { copyNode, deleteNode } from 'tiptap/prose-utils';
import styles from './bubble.module.scss';
const { Text } = Typography;
@@ -41,6 +41,16 @@ export const CalloutBubbleMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
matchRenderContainer={(node) => node && node.id === 'js-bannber-container'}
>
+
+
+
= ({ editor }) => {
type="tertiary"
theme="borderless"
icon={}
- onClick={() => deleteNode('callout', editor)}
+ onClick={() => deleteNode(Callout.name, editor)}
/>
diff --git a/packages/client/src/tiptap/menus/code-block/bubble.tsx b/packages/client/src/tiptap/menus/code-block/bubble.tsx
new file mode 100644
index 00000000..c72fd4b3
--- /dev/null
+++ b/packages/client/src/tiptap/menus/code-block/bubble.tsx
@@ -0,0 +1,44 @@
+import { Space, Button } from '@douyinfe/semi-ui';
+import { IconCopy, IconDelete } from '@douyinfe/semi-icons';
+import { Tooltip } from 'components/tooltip';
+import { BubbleMenu } from 'tiptap/views/bubble-menu';
+import { CodeBlock } from 'tiptap/extensions/code-block';
+import { copyNode, deleteNode } from 'tiptap/prose-utils';
+import { Divider } from 'tiptap/divider';
+
+export const CodeBlockBubbleMenu = ({ editor }) => {
+ return (
+ editor.isActive(CodeBlock.name)}
+ tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
+ matchRenderContainer={(node: HTMLElement) => node && node.classList && node.classList.contains('node-codeBlock')}
+ >
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/client/src/tiptap/menus/code-block/index.tsx b/packages/client/src/tiptap/menus/code-block/index.tsx
new file mode 100644
index 00000000..1c18f2a5
--- /dev/null
+++ b/packages/client/src/tiptap/menus/code-block/index.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import { Editor } from '@tiptap/core';
+import { CodeBlockBubbleMenu } from './bubble';
+
+export const CodeBlock: React.FC<{ editor: Editor }> = ({ editor }) => {
+ if (!editor) {
+ return null;
+ }
+
+ return (
+ <>
+
+ >
+ );
+};
diff --git a/packages/client/src/tiptap/menus/countdown/bubble.tsx b/packages/client/src/tiptap/menus/countdown/bubble.tsx
index d41b6698..84ff553f 100644
--- a/packages/client/src/tiptap/menus/countdown/bubble.tsx
+++ b/packages/client/src/tiptap/menus/countdown/bubble.tsx
@@ -1,10 +1,11 @@
import { useCallback } from 'react';
import { Space, Button } from '@douyinfe/semi-ui';
-import { IconEdit, IconDelete } from '@douyinfe/semi-icons';
+import { IconEdit, IconCopy, IconDelete } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip';
import { BubbleMenu } from 'tiptap/views/bubble-menu';
import { Countdown } from 'tiptap/extensions/countdown';
import { Divider } from 'tiptap/divider';
+import { copyNode, deleteNode } from 'tiptap/prose-utils';
import { triggerOpenCountSettingModal } from '../_event';
export const CountdownBubbleMenu = ({ editor }) => {
@@ -14,8 +15,6 @@ export const CountdownBubbleMenu = ({ editor }) => {
triggerOpenCountSettingModal(attrs);
}, [attrs]);
- const deleteNode = useCallback(() => editor.chain().deleteSelection().run(), [editor]);
-
return (
{
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
>
+
+
+
} onClick={openEditLinkModal} />
@@ -32,7 +41,13 @@ export const CountdownBubbleMenu = ({ editor }) => {
- } type="tertiary" theme="borderless" size="small" />
+
diff --git a/packages/client/src/tiptap/menus/document-children/bubble.tsx b/packages/client/src/tiptap/menus/document-children/bubble.tsx
index 0b27b334..17913867 100644
--- a/packages/client/src/tiptap/menus/document-children/bubble.tsx
+++ b/packages/client/src/tiptap/menus/document-children/bubble.tsx
@@ -1,13 +1,12 @@
-import { useCallback } from 'react';
import { Space, Button } from '@douyinfe/semi-ui';
-import { IconDelete } from '@douyinfe/semi-icons';
+import { IconCopy, IconDelete } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip';
import { BubbleMenu } from 'tiptap/views/bubble-menu';
import { DocumentChildren } from 'tiptap/extensions/document-children';
+import { copyNode, deleteNode } from 'tiptap/prose-utils';
+import { Divider } from 'tiptap/divider';
export const DocumentChildrenBubbleMenu = ({ editor }) => {
- const deleteNode = useCallback(() => editor.chain().deleteSelection().run(), [editor]);
-
return (
{
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
>
+
+
+
+
+
- } type="tertiary" theme="borderless" size="small" />
+
diff --git a/packages/client/src/tiptap/menus/document-reference/bubble.tsx b/packages/client/src/tiptap/menus/document-reference/bubble.tsx
index 61203cc0..a8951dfb 100644
--- a/packages/client/src/tiptap/menus/document-reference/bubble.tsx
+++ b/packages/client/src/tiptap/menus/document-reference/bubble.tsx
@@ -1,7 +1,7 @@
import { useCallback } from 'react';
import { useRouter } from 'next/router';
import { Space, Button, List, Popover, Typography } from '@douyinfe/semi-ui';
-import { IconEdit, IconDelete } from '@douyinfe/semi-icons';
+import { IconEdit, IconCopy, IconDelete } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip';
import { DataRender } from 'components/data-render';
import { IconDocument } from 'components/icons';
@@ -9,6 +9,7 @@ import { useWikiTocs } from 'data/wiki';
import { BubbleMenu } from 'tiptap/views/bubble-menu';
import { DocumentReference } from 'tiptap/extensions/document-reference';
import { Divider } from 'tiptap/divider';
+import { copyNode, deleteNode } from 'tiptap/prose-utils';
const { Text } = Typography;
@@ -32,8 +33,6 @@ export const DocumentReferenceBubbleMenu = ({ editor }) => {
[editor]
);
- const deleteNode = useCallback(() => editor.chain().deleteSelection().run(), [editor]);
-
return (
{
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
>
+
+
+
{
- } type="tertiary" theme="borderless" size="small" />
+
diff --git a/packages/client/src/tiptap/menus/iframe/bubble.tsx b/packages/client/src/tiptap/menus/iframe/bubble.tsx
index 9ef1560c..1601ec29 100644
--- a/packages/client/src/tiptap/menus/iframe/bubble.tsx
+++ b/packages/client/src/tiptap/menus/iframe/bubble.tsx
@@ -1,11 +1,12 @@
import { useCallback, useRef } from 'react';
import { Space, Button, Modal, Form, Typography } from '@douyinfe/semi-ui';
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
-import { IconEdit, IconExternalOpen, IconLineHeight, IconDelete } from '@douyinfe/semi-icons';
+import { IconEdit, IconExternalOpen, IconLineHeight, IconCopy, IconDelete } from '@douyinfe/semi-icons';
import { useToggle } from 'hooks/use-toggle';
import { Tooltip } from 'components/tooltip';
import { BubbleMenu } from 'tiptap/views/bubble-menu';
import { Iframe } from 'tiptap/extensions/iframe';
+import { copyNode, deleteNode } from 'tiptap/prose-utils';
import { Divider } from 'tiptap/divider';
import { Size } from '../_components/size';
@@ -57,8 +58,6 @@ export const IframeBubbleMenu = ({ editor }) => {
[editor]
);
- const deleteNode = useCallback(() => editor.chain().deleteSelection().run(), [editor]);
-
return (
{
+
+
+
} onClick={visitLink} />
@@ -112,7 +121,13 @@ export const IframeBubbleMenu = ({ editor }) => {
- } type="tertiary" theme="borderless" size="small" />
+
diff --git a/packages/client/src/tiptap/menus/image/bubble.tsx b/packages/client/src/tiptap/menus/image/bubble.tsx
index 4372651c..fa5ed7ce 100644
--- a/packages/client/src/tiptap/menus/image/bubble.tsx
+++ b/packages/client/src/tiptap/menus/image/bubble.tsx
@@ -1,11 +1,18 @@
import React, { useEffect, useState } from 'react';
import { Space, Button } from '@douyinfe/semi-ui';
-import { IconAlignLeft, IconAlignCenter, IconAlignRight, IconLineHeight, IconDelete } from '@douyinfe/semi-icons';
+import {
+ IconAlignLeft,
+ IconAlignCenter,
+ IconAlignRight,
+ IconLineHeight,
+ IconCopy,
+ IconDelete,
+} from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip';
import { BubbleMenu } from 'tiptap/views/bubble-menu';
import { Divider } from 'tiptap/divider';
import { Image } from 'tiptap/extensions/image';
-import { getEditorContainerDOMSize } from 'tiptap/prose-utils';
+import { getEditorContainerDOMSize, copyNode, deleteNode } from 'tiptap/prose-utils';
import { Size } from '../_components/size';
export const ImageBubbleMenu = ({ editor }) => {
@@ -32,6 +39,16 @@ export const ImageBubbleMenu = ({ editor }) => {
matchRenderContainer={(node) => node && node.id === 'js-resizeable-container'}
>
+
+
+
-
-
{
type="tertiary"
theme="borderless"
icon={}
- onClick={() => editor.chain().deleteSelection().run()}
+ onClick={() => deleteNode(Image.name, editor)}
/>
diff --git a/packages/client/src/tiptap/menus/insert/index.tsx b/packages/client/src/tiptap/menus/insert/index.tsx
index 938afb53..741bbc4f 100644
--- a/packages/client/src/tiptap/menus/insert/index.tsx
+++ b/packages/client/src/tiptap/menus/insert/index.tsx
@@ -65,7 +65,10 @@ const COMMANDS = [
{
icon: ,
label: '图片',
- action: (editor) => editor.chain().focus().setEmptyImage().run(),
+ action: (editor) => {
+ const { width } = getEditorContainerDOMSize(editor);
+ editor.chain().focus().setEmptyImage({ width }).run();
+ },
},
{
icon: ,
diff --git a/packages/client/src/tiptap/menus/mind/bubble.tsx b/packages/client/src/tiptap/menus/mind/bubble.tsx
index b7442c32..8fb43591 100644
--- a/packages/client/src/tiptap/menus/mind/bubble.tsx
+++ b/packages/client/src/tiptap/menus/mind/bubble.tsx
@@ -1,11 +1,11 @@
import { useCallback } from 'react';
import { Space, Button } from '@douyinfe/semi-ui';
-import { IconLineHeight, IconDelete } from '@douyinfe/semi-icons';
+import { IconCopy, IconLineHeight, IconDelete } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip';
import { BubbleMenu } from 'tiptap/views/bubble-menu';
import { Mind } from 'tiptap/extensions/mind';
import { Divider } from 'tiptap/divider';
-import { getEditorContainerDOMSize } from 'tiptap/prose-utils';
+import { getEditorContainerDOMSize, copyNode, deleteNode } from 'tiptap/prose-utils';
import { Size } from '../_components/size';
export const MindBubbleMenu = ({ editor }) => {
@@ -20,8 +20,6 @@ export const MindBubbleMenu = ({ editor }) => {
[editor]
);
- const deleteNode = useCallback(() => editor.chain().deleteSelection().run(), [editor]);
-
return (
{
matchRenderContainer={(node) => node && node.id === 'js-resizeable-container'}
>
+
+
+
} type="tertiary" theme="borderless" size="small" />
@@ -39,7 +47,13 @@ export const MindBubbleMenu = ({ editor }) => {
- } type="tertiary" theme="borderless" size="small" />
+
diff --git a/packages/client/src/tiptap/menus/table/bubble.tsx b/packages/client/src/tiptap/menus/table/bubble.tsx
index 2be81435..bbd4a9b0 100644
--- a/packages/client/src/tiptap/menus/table/bubble.tsx
+++ b/packages/client/src/tiptap/menus/table/bubble.tsx
@@ -1,4 +1,5 @@
import { Space, Button } from '@douyinfe/semi-ui';
+import { IconCopy } from '@douyinfe/semi-icons';
import {
IconAddColumnBefore,
IconAddColumnAfter,
@@ -17,6 +18,7 @@ import { Tooltip } from 'components/tooltip';
import { Divider } from 'tiptap/divider';
import { BubbleMenu } from 'tiptap/views/bubble-menu';
import { Table } from 'tiptap/extensions/table';
+import { copyNode } from 'tiptap/prose-utils';
export const TableBubbleMenu = ({ editor }) => {
return (
@@ -31,6 +33,18 @@ export const TableBubbleMenu = ({ editor }) => {
matchRenderContainer={(node: HTMLElement) => node && node.tagName === 'TABLE'}
>
+
+
+
+
+