mirror of https://github.com/fantasticit/think.git
tiptap: improve copy paste
This commit is contained in:
parent
a066cea4a7
commit
cc3efef3f1
|
@ -56,7 +56,6 @@
|
||||||
"@tiptap/suggestion": "^2.0.0-beta.90",
|
"@tiptap/suggestion": "^2.0.0-beta.90",
|
||||||
"axios": "^0.25.0",
|
"axios": "^0.25.0",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"copy-to-clipboard": "^3.3.1",
|
|
||||||
"deep-equal": "^2.0.5",
|
"deep-equal": "^2.0.5",
|
||||||
"dompurify": "^2.3.5",
|
"dompurify": "^2.3.5",
|
||||||
"interactjs": "^1.10.11",
|
"interactjs": "^1.10.11",
|
||||||
|
@ -85,6 +84,7 @@
|
||||||
"scroll-into-view-if-needed": "^2.2.29",
|
"scroll-into-view-if-needed": "^2.2.29",
|
||||||
"swr": "^1.2.0",
|
"swr": "^1.2.0",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
|
"toggle-selection": "^1.0.6",
|
||||||
"viewerjs": "^1.10.4",
|
"viewerjs": "^1.10.4",
|
||||||
"y-indexeddb": "^9.0.7",
|
"y-indexeddb": "^9.0.7",
|
||||||
"y-prosemirror": "^1.0.14",
|
"y-prosemirror": "^1.0.14",
|
||||||
|
|
|
@ -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;
|
|
@ -1,7 +1,16 @@
|
||||||
import _copy from 'copy-to-clipboard';
|
import _copy from './copy-to-clipboard';
|
||||||
import { Toast } from '@douyinfe/semi-ui';
|
import { Toast } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
export function copy(text: string, msg = '复制成功') {
|
interface Options {
|
||||||
Toast.success(msg);
|
debug?: boolean;
|
||||||
return _copy(text);
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ import {
|
||||||
markdownToProsemirror,
|
markdownToProsemirror,
|
||||||
prosemirrorToMarkdown,
|
prosemirrorToMarkdown,
|
||||||
} from 'tiptap/markdown/markdown-to-prosemirror';
|
} from 'tiptap/markdown/markdown-to-prosemirror';
|
||||||
|
import { copyNode } from 'tiptap/prose-utils';
|
||||||
|
import { safeJSONParse } from 'helpers/json';
|
||||||
|
|
||||||
const isPureText = (content): boolean => {
|
const isPureText = (content): boolean => {
|
||||||
if (!content) return false;
|
if (!content) return false;
|
||||||
|
@ -43,7 +45,6 @@ export const Paste = Extension.create({
|
||||||
if (!event.clipboardData) return false;
|
if (!event.clipboardData) return false;
|
||||||
|
|
||||||
const files = Array.from(event.clipboardData.files);
|
const files = Array.from(event.clipboardData.files);
|
||||||
|
|
||||||
if (files.length) {
|
if (files.length) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
|
@ -55,6 +56,16 @@ export const Paste = Extension.create({
|
||||||
const text = event.clipboardData.getData('text/plain');
|
const text = event.clipboardData.getData('text/plain');
|
||||||
const html = event.clipboardData.getData('text/html');
|
const html = event.clipboardData.getData('text/html');
|
||||||
const vscode = event.clipboardData.getData('vscode-editor-data');
|
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)) {
|
if (isInCode(view.state)) {
|
||||||
|
@ -80,14 +91,14 @@ export const Paste = Extension.create({
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理 markdown
|
// 处理 markdown
|
||||||
if (isMarkdown(text) || html.length === 0 || pasteCodeLanguage === 'markdown') {
|
if (markdownText || isMarkdown(text) || html.length === 0 || pasteCodeLanguage === 'markdown') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const firstNode = view.props.state.doc.content.firstChild;
|
const firstNode = view.props.state.doc.content.firstChild;
|
||||||
const hasTitle = isTitleNode(firstNode) && firstNode.content.size > 0;
|
const hasTitle = isTitleNode(firstNode) && firstNode.content.size > 0;
|
||||||
const schema = view.props.state.schema;
|
const schema = view.props.state.schema;
|
||||||
const doc = markdownToProsemirror({
|
const doc = markdownToProsemirror({
|
||||||
schema,
|
schema,
|
||||||
content: normalizePastedMarkdown(text),
|
content: normalizePastedMarkdown(markdownText || text),
|
||||||
hasTitle,
|
hasTitle,
|
||||||
});
|
});
|
||||||
let tr = view.state.tr;
|
let tr = view.state.tr;
|
||||||
|
@ -130,8 +141,41 @@ export const Paste = Extension.create({
|
||||||
|
|
||||||
return false;
|
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) => {
|
clipboardTextSerializer: (slice) => {
|
||||||
const isText = isPureText(slice.content.toJSON());
|
const isText = isPureText(slice.content.toJSON());
|
||||||
|
|
||||||
if (isText) {
|
if (isText) {
|
||||||
return slice.content.textBetween(0, slice.content.size, '\n\n');
|
return slice.content.textBetween(0, slice.content.size, '\n\n');
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,10 @@ export const isMarkdown = (text: string): boolean => {
|
||||||
const html = text.match(/<\/?[a-z][\s\S]*>/i);
|
const html = text.match(/<\/?[a-z][\s\S]*>/i);
|
||||||
if (html && html.length) return true;
|
if (html && html.length) return true;
|
||||||
|
|
||||||
|
// image
|
||||||
|
const image = text.match(/!\[(\s|.)?\]\((\s|.)?\)/);
|
||||||
|
if (image && image.length) return true;
|
||||||
|
|
||||||
// table
|
// table
|
||||||
const tables = text.match(/^\|(\S)*\|/gm);
|
const tables = text.match(/^\|(\S)*\|/gm);
|
||||||
if (tables && tables.length) return true;
|
if (tables && tables.length) return true;
|
||||||
|
|
|
@ -63,7 +63,7 @@ function attrSet(token, name, value) {
|
||||||
function processToken(state: StateCore, options: TaskListsOptions): boolean {
|
function processToken(state: StateCore, options: TaskListsOptions): boolean {
|
||||||
const allTokens = state.tokens;
|
const allTokens = state.tokens;
|
||||||
|
|
||||||
attrSet(allTokens[0], 'class', 'contains-task-list');
|
attrSet(allTokens[0], 'class', '');
|
||||||
|
|
||||||
for (let i = 2; i < allTokens.length; i++) {
|
for (let i = 2; i < allTokens.length; i++) {
|
||||||
if (!isTodoItem(allTokens, i)) {
|
if (!isTodoItem(allTokens, i)) {
|
||||||
|
|
|
@ -91,7 +91,7 @@ const SerializerConfig = {
|
||||||
},
|
},
|
||||||
|
|
||||||
nodes: {
|
nodes: {
|
||||||
[Attachment.name]: renderCustomContainer('attachment'),
|
attachment: renderCustomContainer('attachment'),
|
||||||
blockquote: (state, node) => {
|
blockquote: (state, node) => {
|
||||||
if (node.attrs.multiline) {
|
if (node.attrs.multiline) {
|
||||||
state.write('>>>');
|
state.write('>>>');
|
||||||
|
|
|
@ -32,7 +32,9 @@ import { Blockquote } from './menus/blockquote';
|
||||||
import { HorizontalRule } from './menus/horizontal-rule';
|
import { HorizontalRule } from './menus/horizontal-rule';
|
||||||
import { Search } from './menus/search';
|
import { Search } from './menus/search';
|
||||||
|
|
||||||
|
import { Attachment } from './menus/attachment';
|
||||||
import { Callout } from './menus/callout';
|
import { Callout } from './menus/callout';
|
||||||
|
import { CodeBlock } from './menus/code-block';
|
||||||
import { Countdonw } from './menus/countdown';
|
import { Countdonw } from './menus/countdown';
|
||||||
import { DocumentChildren } from './menus/document-children';
|
import { DocumentChildren } from './menus/document-children';
|
||||||
import { DocumentReference } from './menus/document-reference';
|
import { DocumentReference } from './menus/document-reference';
|
||||||
|
@ -90,7 +92,9 @@ export const MenuBar: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
<HorizontalRule editor={editor} />
|
<HorizontalRule editor={editor} />
|
||||||
<Search editor={editor} />
|
<Search editor={editor} />
|
||||||
|
|
||||||
|
<Attachment editor={editor} />
|
||||||
<Callout editor={editor} />
|
<Callout editor={editor} />
|
||||||
|
<CodeBlock editor={editor} />
|
||||||
<Countdonw editor={editor} />
|
<Countdonw editor={editor} />
|
||||||
<DocumentChildren editor={editor} />
|
<DocumentChildren editor={editor} />
|
||||||
<DocumentReference editor={editor} />
|
<DocumentReference editor={editor} />
|
||||||
|
|
|
@ -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 (
|
||||||
|
<BubbleMenu
|
||||||
|
className={'bubble-menu'}
|
||||||
|
editor={editor}
|
||||||
|
pluginKey="document-children-bubble-menu"
|
||||||
|
shouldShow={() => editor.isActive(Attachment.name)}
|
||||||
|
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||||
|
>
|
||||||
|
<Space>
|
||||||
|
<Tooltip content="复制">
|
||||||
|
<Button
|
||||||
|
onClick={() => copyNode(Attachment.name, editor)}
|
||||||
|
icon={<IconCopy />}
|
||||||
|
type="tertiary"
|
||||||
|
theme="borderless"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Tooltip content="删除节点" hideOnClick>
|
||||||
|
<Button
|
||||||
|
onClick={() => deleteNode(Attachment.name, editor)}
|
||||||
|
icon={<IconDelete />}
|
||||||
|
type="tertiary"
|
||||||
|
theme="borderless"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
</BubbleMenu>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<AttachmentBubbleMenu editor={editor} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,13 +1,13 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { Editor } from '@tiptap/core';
|
import { Editor } from '@tiptap/core';
|
||||||
import { Space, Button, Popover, Typography } from '@douyinfe/semi-ui';
|
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 { Tooltip } from 'components/tooltip';
|
||||||
import { IconDrawBoard } from 'components/icons';
|
import { IconDrawBoard } from 'components/icons';
|
||||||
import { BubbleMenu } from 'tiptap/views/bubble-menu';
|
import { BubbleMenu } from 'tiptap/views/bubble-menu';
|
||||||
import { Divider } from 'tiptap/divider';
|
import { Divider } from 'tiptap/divider';
|
||||||
import { Callout } from 'tiptap/extensions/callout';
|
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';
|
import styles from './bubble.module.scss';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
@ -41,6 +41,16 @@ export const CalloutBubbleMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
matchRenderContainer={(node) => node && node.id === 'js-bannber-container'}
|
matchRenderContainer={(node) => node && node.id === 'js-bannber-container'}
|
||||||
>
|
>
|
||||||
<Space>
|
<Space>
|
||||||
|
<Tooltip content="复制">
|
||||||
|
<Button
|
||||||
|
onClick={() => copyNode(Callout.name, editor)}
|
||||||
|
icon={<IconCopy />}
|
||||||
|
type="tertiary"
|
||||||
|
theme="borderless"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
spacing={10}
|
spacing={10}
|
||||||
style={{ padding: '0 12px 12px', overflow: 'hidden' }}
|
style={{ padding: '0 12px 12px', overflow: 'hidden' }}
|
||||||
|
@ -93,7 +103,7 @@ export const CalloutBubbleMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
theme="borderless"
|
theme="borderless"
|
||||||
icon={<IconDelete />}
|
icon={<IconDelete />}
|
||||||
onClick={() => deleteNode('callout', editor)}
|
onClick={() => deleteNode(Callout.name, editor)}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
|
@ -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 (
|
||||||
|
<BubbleMenu
|
||||||
|
className={'bubble-menu'}
|
||||||
|
editor={editor}
|
||||||
|
pluginKey="document-children-bubble-menu"
|
||||||
|
shouldShow={() => editor.isActive(CodeBlock.name)}
|
||||||
|
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||||
|
matchRenderContainer={(node: HTMLElement) => node && node.classList && node.classList.contains('node-codeBlock')}
|
||||||
|
>
|
||||||
|
<Space>
|
||||||
|
<Tooltip content="复制">
|
||||||
|
<Button
|
||||||
|
onClick={() => copyNode(CodeBlock.name, editor)}
|
||||||
|
icon={<IconCopy />}
|
||||||
|
type="tertiary"
|
||||||
|
theme="borderless"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Tooltip content="删除节点" hideOnClick>
|
||||||
|
<Button
|
||||||
|
onClick={() => deleteNode(CodeBlock.name, editor)}
|
||||||
|
icon={<IconDelete />}
|
||||||
|
type="tertiary"
|
||||||
|
theme="borderless"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
</BubbleMenu>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<CodeBlockBubbleMenu editor={editor} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,10 +1,11 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { Space, Button } from '@douyinfe/semi-ui';
|
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 { Tooltip } from 'components/tooltip';
|
||||||
import { BubbleMenu } from 'tiptap/views/bubble-menu';
|
import { BubbleMenu } from 'tiptap/views/bubble-menu';
|
||||||
import { Countdown } from 'tiptap/extensions/countdown';
|
import { Countdown } from 'tiptap/extensions/countdown';
|
||||||
import { Divider } from 'tiptap/divider';
|
import { Divider } from 'tiptap/divider';
|
||||||
|
import { copyNode, deleteNode } from 'tiptap/prose-utils';
|
||||||
import { triggerOpenCountSettingModal } from '../_event';
|
import { triggerOpenCountSettingModal } from '../_event';
|
||||||
|
|
||||||
export const CountdownBubbleMenu = ({ editor }) => {
|
export const CountdownBubbleMenu = ({ editor }) => {
|
||||||
|
@ -14,8 +15,6 @@ export const CountdownBubbleMenu = ({ editor }) => {
|
||||||
triggerOpenCountSettingModal(attrs);
|
triggerOpenCountSettingModal(attrs);
|
||||||
}, [attrs]);
|
}, [attrs]);
|
||||||
|
|
||||||
const deleteNode = useCallback(() => editor.chain().deleteSelection().run(), [editor]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BubbleMenu
|
<BubbleMenu
|
||||||
className={'bubble-menu'}
|
className={'bubble-menu'}
|
||||||
|
@ -25,6 +24,16 @@ export const CountdownBubbleMenu = ({ editor }) => {
|
||||||
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||||
>
|
>
|
||||||
<Space>
|
<Space>
|
||||||
|
<Tooltip content="复制">
|
||||||
|
<Button
|
||||||
|
onClick={() => copyNode(Countdown.name, editor)}
|
||||||
|
icon={<IconCopy />}
|
||||||
|
type="tertiary"
|
||||||
|
theme="borderless"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="编辑">
|
<Tooltip content="编辑">
|
||||||
<Button size="small" type="tertiary" theme="borderless" icon={<IconEdit />} onClick={openEditLinkModal} />
|
<Button size="small" type="tertiary" theme="borderless" icon={<IconEdit />} onClick={openEditLinkModal} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -32,7 +41,13 @@ export const CountdownBubbleMenu = ({ editor }) => {
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Tooltip content="删除节点" hideOnClick>
|
<Tooltip content="删除节点" hideOnClick>
|
||||||
<Button onClick={deleteNode} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
|
<Button
|
||||||
|
onClick={() => deleteNode(Countdown.name, editor)}
|
||||||
|
icon={<IconDelete />}
|
||||||
|
type="tertiary"
|
||||||
|
theme="borderless"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
</BubbleMenu>
|
</BubbleMenu>
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { Space, Button } from '@douyinfe/semi-ui';
|
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 { Tooltip } from 'components/tooltip';
|
||||||
import { BubbleMenu } from 'tiptap/views/bubble-menu';
|
import { BubbleMenu } from 'tiptap/views/bubble-menu';
|
||||||
import { DocumentChildren } from 'tiptap/extensions/document-children';
|
import { DocumentChildren } from 'tiptap/extensions/document-children';
|
||||||
|
import { copyNode, deleteNode } from 'tiptap/prose-utils';
|
||||||
|
import { Divider } from 'tiptap/divider';
|
||||||
|
|
||||||
export const DocumentChildrenBubbleMenu = ({ editor }) => {
|
export const DocumentChildrenBubbleMenu = ({ editor }) => {
|
||||||
const deleteNode = useCallback(() => editor.chain().deleteSelection().run(), [editor]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BubbleMenu
|
<BubbleMenu
|
||||||
className={'bubble-menu'}
|
className={'bubble-menu'}
|
||||||
|
@ -17,8 +16,26 @@ export const DocumentChildrenBubbleMenu = ({ editor }) => {
|
||||||
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||||
>
|
>
|
||||||
<Space>
|
<Space>
|
||||||
|
<Tooltip content="复制">
|
||||||
|
<Button
|
||||||
|
onClick={() => copyNode(DocumentChildren.name, editor)}
|
||||||
|
icon={<IconCopy />}
|
||||||
|
type="tertiary"
|
||||||
|
theme="borderless"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
<Tooltip content="删除节点" hideOnClick>
|
<Tooltip content="删除节点" hideOnClick>
|
||||||
<Button onClick={deleteNode} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
|
<Button
|
||||||
|
onClick={() => deleteNode(DocumentChildren.name, editor)}
|
||||||
|
icon={<IconDelete />}
|
||||||
|
type="tertiary"
|
||||||
|
theme="borderless"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
</BubbleMenu>
|
</BubbleMenu>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { Space, Button, List, Popover, Typography } from '@douyinfe/semi-ui';
|
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 { Tooltip } from 'components/tooltip';
|
||||||
import { DataRender } from 'components/data-render';
|
import { DataRender } from 'components/data-render';
|
||||||
import { IconDocument } from 'components/icons';
|
import { IconDocument } from 'components/icons';
|
||||||
|
@ -9,6 +9,7 @@ import { useWikiTocs } from 'data/wiki';
|
||||||
import { BubbleMenu } from 'tiptap/views/bubble-menu';
|
import { BubbleMenu } from 'tiptap/views/bubble-menu';
|
||||||
import { DocumentReference } from 'tiptap/extensions/document-reference';
|
import { DocumentReference } from 'tiptap/extensions/document-reference';
|
||||||
import { Divider } from 'tiptap/divider';
|
import { Divider } from 'tiptap/divider';
|
||||||
|
import { copyNode, deleteNode } from 'tiptap/prose-utils';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
@ -32,8 +33,6 @@ export const DocumentReferenceBubbleMenu = ({ editor }) => {
|
||||||
[editor]
|
[editor]
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteNode = useCallback(() => editor.chain().deleteSelection().run(), [editor]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BubbleMenu
|
<BubbleMenu
|
||||||
className={'bubble-menu'}
|
className={'bubble-menu'}
|
||||||
|
@ -43,6 +42,16 @@ export const DocumentReferenceBubbleMenu = ({ editor }) => {
|
||||||
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||||
>
|
>
|
||||||
<Space>
|
<Space>
|
||||||
|
<Tooltip content="复制">
|
||||||
|
<Button
|
||||||
|
onClick={() => copyNode(DocumentReference.name, editor)}
|
||||||
|
icon={<IconCopy />}
|
||||||
|
type="tertiary"
|
||||||
|
theme="borderless"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
spacing={15}
|
spacing={15}
|
||||||
content={
|
content={
|
||||||
|
@ -88,7 +97,13 @@ export const DocumentReferenceBubbleMenu = ({ editor }) => {
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Tooltip content="删除节点" hideOnClick>
|
<Tooltip content="删除节点" hideOnClick>
|
||||||
<Button onClick={deleteNode} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
|
<Button
|
||||||
|
onClick={() => deleteNode(DocumentReference.name, editor)}
|
||||||
|
icon={<IconDelete />}
|
||||||
|
type="tertiary"
|
||||||
|
theme="borderless"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
</BubbleMenu>
|
</BubbleMenu>
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import { Space, Button, Modal, Form, Typography } from '@douyinfe/semi-ui';
|
import { Space, Button, Modal, Form, Typography } from '@douyinfe/semi-ui';
|
||||||
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
|
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 { useToggle } from 'hooks/use-toggle';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { BubbleMenu } from 'tiptap/views/bubble-menu';
|
import { BubbleMenu } from 'tiptap/views/bubble-menu';
|
||||||
import { Iframe } from 'tiptap/extensions/iframe';
|
import { Iframe } from 'tiptap/extensions/iframe';
|
||||||
|
import { copyNode, deleteNode } from 'tiptap/prose-utils';
|
||||||
import { Divider } from 'tiptap/divider';
|
import { Divider } from 'tiptap/divider';
|
||||||
import { Size } from '../_components/size';
|
import { Size } from '../_components/size';
|
||||||
|
|
||||||
|
@ -57,8 +58,6 @@ export const IframeBubbleMenu = ({ editor }) => {
|
||||||
[editor]
|
[editor]
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteNode = useCallback(() => editor.chain().deleteSelection().run(), [editor]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BubbleMenu
|
<BubbleMenu
|
||||||
className={'bubble-menu'}
|
className={'bubble-menu'}
|
||||||
|
@ -95,6 +94,16 @@ export const IframeBubbleMenu = ({ editor }) => {
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Space>
|
<Space>
|
||||||
|
<Tooltip content="复制">
|
||||||
|
<Button
|
||||||
|
onClick={() => copyNode(Iframe.name, editor)}
|
||||||
|
icon={<IconCopy />}
|
||||||
|
type="tertiary"
|
||||||
|
theme="borderless"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="访问链接">
|
<Tooltip content="访问链接">
|
||||||
<Button size="small" type="tertiary" theme="borderless" icon={<IconExternalOpen />} onClick={visitLink} />
|
<Button size="small" type="tertiary" theme="borderless" icon={<IconExternalOpen />} onClick={visitLink} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -112,7 +121,13 @@ export const IframeBubbleMenu = ({ editor }) => {
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Tooltip content="删除节点" hideOnClick>
|
<Tooltip content="删除节点" hideOnClick>
|
||||||
<Button onClick={deleteNode} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
|
<Button
|
||||||
|
onClick={() => deleteNode(Iframe.name, editor)}
|
||||||
|
icon={<IconDelete />}
|
||||||
|
type="tertiary"
|
||||||
|
theme="borderless"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
</BubbleMenu>
|
</BubbleMenu>
|
||||||
|
|
|
@ -1,11 +1,18 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Space, Button } from '@douyinfe/semi-ui';
|
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 { Tooltip } from 'components/tooltip';
|
||||||
import { BubbleMenu } from 'tiptap/views/bubble-menu';
|
import { BubbleMenu } from 'tiptap/views/bubble-menu';
|
||||||
import { Divider } from 'tiptap/divider';
|
import { Divider } from 'tiptap/divider';
|
||||||
import { Image } from 'tiptap/extensions/image';
|
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';
|
import { Size } from '../_components/size';
|
||||||
|
|
||||||
export const ImageBubbleMenu = ({ editor }) => {
|
export const ImageBubbleMenu = ({ editor }) => {
|
||||||
|
@ -32,6 +39,16 @@ export const ImageBubbleMenu = ({ editor }) => {
|
||||||
matchRenderContainer={(node) => node && node.id === 'js-resizeable-container'}
|
matchRenderContainer={(node) => node && node.id === 'js-resizeable-container'}
|
||||||
>
|
>
|
||||||
<Space>
|
<Space>
|
||||||
|
<Tooltip content="复制">
|
||||||
|
<Button
|
||||||
|
onClick={() => copyNode(Image.name, editor)}
|
||||||
|
icon={<IconCopy />}
|
||||||
|
type="tertiary"
|
||||||
|
theme="borderless"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="左对齐">
|
<Tooltip content="左对齐">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -89,8 +106,6 @@ export const ImageBubbleMenu = ({ editor }) => {
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<Size
|
<Size
|
||||||
width={width}
|
width={width}
|
||||||
maxWidth={maxWidth}
|
maxWidth={maxWidth}
|
||||||
|
@ -117,7 +132,7 @@ export const ImageBubbleMenu = ({ editor }) => {
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
theme="borderless"
|
theme="borderless"
|
||||||
icon={<IconDelete />}
|
icon={<IconDelete />}
|
||||||
onClick={() => editor.chain().deleteSelection().run()}
|
onClick={() => deleteNode(Image.name, editor)}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
|
@ -65,7 +65,10 @@ const COMMANDS = [
|
||||||
{
|
{
|
||||||
icon: <IconImage />,
|
icon: <IconImage />,
|
||||||
label: '图片',
|
label: '图片',
|
||||||
action: (editor) => editor.chain().focus().setEmptyImage().run(),
|
action: (editor) => {
|
||||||
|
const { width } = getEditorContainerDOMSize(editor);
|
||||||
|
editor.chain().focus().setEmptyImage({ width }).run();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <IconAttachment />,
|
icon: <IconAttachment />,
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { Space, Button } from '@douyinfe/semi-ui';
|
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 { Tooltip } from 'components/tooltip';
|
||||||
import { BubbleMenu } from 'tiptap/views/bubble-menu';
|
import { BubbleMenu } from 'tiptap/views/bubble-menu';
|
||||||
import { Mind } from 'tiptap/extensions/mind';
|
import { Mind } from 'tiptap/extensions/mind';
|
||||||
import { Divider } from 'tiptap/divider';
|
import { Divider } from 'tiptap/divider';
|
||||||
import { getEditorContainerDOMSize } from 'tiptap/prose-utils';
|
import { getEditorContainerDOMSize, copyNode, deleteNode } from 'tiptap/prose-utils';
|
||||||
import { Size } from '../_components/size';
|
import { Size } from '../_components/size';
|
||||||
|
|
||||||
export const MindBubbleMenu = ({ editor }) => {
|
export const MindBubbleMenu = ({ editor }) => {
|
||||||
|
@ -20,8 +20,6 @@ export const MindBubbleMenu = ({ editor }) => {
|
||||||
[editor]
|
[editor]
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteNode = useCallback(() => editor.chain().deleteSelection().run(), [editor]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BubbleMenu
|
<BubbleMenu
|
||||||
className={'bubble-menu'}
|
className={'bubble-menu'}
|
||||||
|
@ -32,6 +30,16 @@ export const MindBubbleMenu = ({ editor }) => {
|
||||||
matchRenderContainer={(node) => node && node.id === 'js-resizeable-container'}
|
matchRenderContainer={(node) => node && node.id === 'js-resizeable-container'}
|
||||||
>
|
>
|
||||||
<Space>
|
<Space>
|
||||||
|
<Tooltip content="复制">
|
||||||
|
<Button
|
||||||
|
onClick={() => copyNode(Mind.name, editor)}
|
||||||
|
icon={<IconCopy />}
|
||||||
|
type="tertiary"
|
||||||
|
theme="borderless"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Size width={width} maxWidth={maxWidth} height={height} onOk={setSize}>
|
<Size width={width} maxWidth={maxWidth} height={height} onOk={setSize}>
|
||||||
<Tooltip content="设置宽高">
|
<Tooltip content="设置宽高">
|
||||||
<Button icon={<IconLineHeight />} type="tertiary" theme="borderless" size="small" />
|
<Button icon={<IconLineHeight />} type="tertiary" theme="borderless" size="small" />
|
||||||
|
@ -39,7 +47,13 @@ export const MindBubbleMenu = ({ editor }) => {
|
||||||
</Size>
|
</Size>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Tooltip content="删除节点" hideOnClick>
|
<Tooltip content="删除节点" hideOnClick>
|
||||||
<Button onClick={deleteNode} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
|
<Button
|
||||||
|
onClick={() => deleteNode(Mind.name, editor)}
|
||||||
|
icon={<IconDelete />}
|
||||||
|
type="tertiary"
|
||||||
|
theme="borderless"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
</BubbleMenu>
|
</BubbleMenu>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Space, Button } from '@douyinfe/semi-ui';
|
import { Space, Button } from '@douyinfe/semi-ui';
|
||||||
|
import { IconCopy } from '@douyinfe/semi-icons';
|
||||||
import {
|
import {
|
||||||
IconAddColumnBefore,
|
IconAddColumnBefore,
|
||||||
IconAddColumnAfter,
|
IconAddColumnAfter,
|
||||||
|
@ -17,6 +18,7 @@ import { Tooltip } from 'components/tooltip';
|
||||||
import { Divider } from 'tiptap/divider';
|
import { Divider } from 'tiptap/divider';
|
||||||
import { BubbleMenu } from 'tiptap/views/bubble-menu';
|
import { BubbleMenu } from 'tiptap/views/bubble-menu';
|
||||||
import { Table } from 'tiptap/extensions/table';
|
import { Table } from 'tiptap/extensions/table';
|
||||||
|
import { copyNode } from 'tiptap/prose-utils';
|
||||||
|
|
||||||
export const TableBubbleMenu = ({ editor }) => {
|
export const TableBubbleMenu = ({ editor }) => {
|
||||||
return (
|
return (
|
||||||
|
@ -31,6 +33,18 @@ export const TableBubbleMenu = ({ editor }) => {
|
||||||
matchRenderContainer={(node: HTMLElement) => node && node.tagName === 'TABLE'}
|
matchRenderContainer={(node: HTMLElement) => node && node.tagName === 'TABLE'}
|
||||||
>
|
>
|
||||||
<Space>
|
<Space>
|
||||||
|
<Tooltip content="复制">
|
||||||
|
<Button
|
||||||
|
onClick={() => copyNode(Table.name, editor)}
|
||||||
|
icon={<IconCopy />}
|
||||||
|
type="tertiary"
|
||||||
|
theme="borderless"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
<Tooltip content="向前插入一列">
|
<Tooltip content="向前插入一列">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => editor.chain().focus().addColumnBefore().run()}
|
onClick={() => editor.chain().focus().addColumnBefore().run()}
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { Node, Fragment } from 'prosemirror-model';
|
||||||
|
import { copy } from 'helpers/copy';
|
||||||
|
import { safeJSONStringify } from 'helpers/json';
|
||||||
|
import { prosemirrorToMarkdown } from 'tiptap/markdown/prosemirror-to-markdown';
|
||||||
|
import { markdownToHTML } from 'tiptap/markdown/markdown-to-prosemirror/markdown-to-html';
|
||||||
|
|
||||||
|
export function copyNode(nodeOrNodeName: Node);
|
||||||
|
export function copyNode(nodeOrNodeName: string, editor: Editor);
|
||||||
|
export function copyNode(nodeOrNodeName: string | Node, editor?: Editor) {
|
||||||
|
let targetNode: null | Node = null;
|
||||||
|
|
||||||
|
if (typeof nodeOrNodeName === 'string') {
|
||||||
|
const { state } = editor;
|
||||||
|
const $pos = state.selection.$anchor;
|
||||||
|
// @ts-ignore
|
||||||
|
const currentNode = state.selection.node;
|
||||||
|
|
||||||
|
if (currentNode && currentNode.type.name === nodeOrNodeName) {
|
||||||
|
targetNode = currentNode;
|
||||||
|
} else {
|
||||||
|
if ($pos.depth) {
|
||||||
|
for (let d = $pos.depth; d > 0; d--) {
|
||||||
|
const node = $pos.node(d);
|
||||||
|
if (node.type.name === nodeOrNodeName) {
|
||||||
|
targetNode = node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
targetNode = nodeOrNodeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetNode) {
|
||||||
|
const toCopy = [{ text: safeJSONStringify(targetNode.toJSON()), format: 'text/node' }];
|
||||||
|
|
||||||
|
if (targetNode.textContent) {
|
||||||
|
toCopy.push({ text: targetNode.textContent, format: 'text/plain' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const markdown = prosemirrorToMarkdown({ content: Fragment.from(targetNode) });
|
||||||
|
toCopy.push({ text: markdown, format: 'text/markdown' });
|
||||||
|
const html = markdownToHTML(markdown);
|
||||||
|
toCopy.push({ text: html, format: 'text/html' });
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
copy(toCopy);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
import { Editor } from '@tiptap/core';
|
import { Editor } from '@tiptap/core';
|
||||||
|
|
||||||
export function deleteNode(nodeType, editor: Editor) {
|
export function deleteNode(nodeType: string, editor: Editor) {
|
||||||
const { state } = editor;
|
const { state } = editor;
|
||||||
|
|
||||||
const $pos = state.selection.$anchor;
|
const $pos = state.selection.$anchor;
|
||||||
|
|
||||||
|
if ($pos.depth) {
|
||||||
for (let d = $pos.depth; d > 0; d--) {
|
for (let d = $pos.depth; d > 0; d--) {
|
||||||
const node = $pos.node(d);
|
const node = $pos.node(d);
|
||||||
if (node.type.name === nodeType) {
|
if (node.type.name === nodeType) {
|
||||||
|
@ -15,5 +15,13 @@ export function deleteNode(nodeType, editor: Editor) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
const node = state.selection.node;
|
||||||
|
if (node && node.type.name === nodeType) {
|
||||||
|
editor.chain().deleteSelection().run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ export * from './active';
|
||||||
export * from './clamp';
|
export * from './clamp';
|
||||||
export * from './code';
|
export * from './code';
|
||||||
export * from './color';
|
export * from './color';
|
||||||
|
export * from './copy-node';
|
||||||
export * from './delete-node';
|
export * from './delete-node';
|
||||||
export * from './dom-dataset';
|
export * from './dom-dataset';
|
||||||
export * from './dom';
|
export * from './dom';
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
.node-attachment,
|
.node-attachment,
|
||||||
.node-countdown,
|
.node-countdown,
|
||||||
.node-iframe,
|
.node-iframe,
|
||||||
|
.node-image,
|
||||||
.node-codeBlock,
|
.node-codeBlock,
|
||||||
.node-documentChildren,
|
.node-documentChildren,
|
||||||
.node-documentReference {
|
.node-documentReference {
|
||||||
|
|
|
@ -86,7 +86,6 @@ importers:
|
||||||
'@types/react': 17.0.38
|
'@types/react': 17.0.38
|
||||||
axios: ^0.25.0
|
axios: ^0.25.0
|
||||||
classnames: ^2.3.1
|
classnames: ^2.3.1
|
||||||
copy-to-clipboard: ^3.3.1
|
|
||||||
deep-equal: ^2.0.5
|
deep-equal: ^2.0.5
|
||||||
dompurify: ^2.3.5
|
dompurify: ^2.3.5
|
||||||
interactjs: ^1.10.11
|
interactjs: ^1.10.11
|
||||||
|
@ -115,6 +114,7 @@ importers:
|
||||||
scroll-into-view-if-needed: ^2.2.29
|
scroll-into-view-if-needed: ^2.2.29
|
||||||
swr: ^1.2.0
|
swr: ^1.2.0
|
||||||
tippy.js: ^6.3.7
|
tippy.js: ^6.3.7
|
||||||
|
toggle-selection: ^1.0.6
|
||||||
tsconfig-paths-webpack-plugin: ^3.5.2
|
tsconfig-paths-webpack-plugin: ^3.5.2
|
||||||
typescript: 4.5.5
|
typescript: 4.5.5
|
||||||
viewerjs: ^1.10.4
|
viewerjs: ^1.10.4
|
||||||
|
@ -169,7 +169,6 @@ importers:
|
||||||
'@tiptap/suggestion': 2.0.0-beta.90_@tiptap+core@2.0.0-beta.171
|
'@tiptap/suggestion': 2.0.0-beta.90_@tiptap+core@2.0.0-beta.171
|
||||||
axios: 0.25.0
|
axios: 0.25.0
|
||||||
classnames: 2.3.1
|
classnames: 2.3.1
|
||||||
copy-to-clipboard: 3.3.1
|
|
||||||
deep-equal: 2.0.5
|
deep-equal: 2.0.5
|
||||||
dompurify: 2.3.5
|
dompurify: 2.3.5
|
||||||
interactjs: 1.10.11
|
interactjs: 1.10.11
|
||||||
|
@ -198,6 +197,7 @@ importers:
|
||||||
scroll-into-view-if-needed: 2.2.29
|
scroll-into-view-if-needed: 2.2.29
|
||||||
swr: 1.2.0_react@17.0.2
|
swr: 1.2.0_react@17.0.2
|
||||||
tippy.js: 6.3.7
|
tippy.js: 6.3.7
|
||||||
|
toggle-selection: 1.0.6
|
||||||
viewerjs: 1.10.4
|
viewerjs: 1.10.4
|
||||||
y-indexeddb: 9.0.7_yjs@13.5.24
|
y-indexeddb: 9.0.7_yjs@13.5.24
|
||||||
y-prosemirror: 1.0.14_0fedec857d2fb730ad5b02a71124bf2a
|
y-prosemirror: 1.0.14_0fedec857d2fb730ad5b02a71124bf2a
|
||||||
|
@ -3171,12 +3171,6 @@ packages:
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/copy-to-clipboard/3.3.1:
|
|
||||||
resolution: {integrity: sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==}
|
|
||||||
dependencies:
|
|
||||||
toggle-selection: 1.0.6
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/copy-to/2.0.1:
|
/copy-to/2.0.1:
|
||||||
resolution: {integrity: sha1-JoD7uAaKSNCGVrYJgJK9r8kG9KU=}
|
resolution: {integrity: sha1-JoD7uAaKSNCGVrYJgJK9r8kG9KU=}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
Loading…
Reference in New Issue