mirror of https://github.com/fantasticit/think.git
tiptap: add drawio flow support
This commit is contained in:
parent
c70846a8ba
commit
0688362cc0
|
@ -4,3 +4,4 @@ node_modules
|
|||
**/dist/**
|
||||
.eslintrc.js
|
||||
./packages/client/src/tiptap/wrappers/mind/mind-elixir/iconfont/iconfont.js
|
||||
./packages/client/public/drawio.embed.js
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
interface Window {
|
||||
// 思维导图
|
||||
MindElixir: any;
|
||||
// drawio 绘图
|
||||
mxGraph: any;
|
||||
mxUtils: any;
|
||||
mxCodec: any;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ const nextConfig = semi({
|
|||
SERVER_API_URL: config.client.apiUrl,
|
||||
COLLABORATION_API_URL: config.client.collaborationUrl,
|
||||
ENABLE_ALIYUN_OSS: !!config.oss.aliyun.accessKeyId,
|
||||
DRAWIO_URL: config.client.drawioUrl || 'https://embed.diagrams.net',
|
||||
},
|
||||
webpack: (config, { dev, isServer }) => {
|
||||
config.resolve.plugins.push(new TsconfigPathsPlugin());
|
||||
|
|
|
@ -70,6 +70,7 @@
|
|||
"markdown-it-sub": "^1.0.0",
|
||||
"markdown-it-sup": "^1.0.0",
|
||||
"next": "12.0.10",
|
||||
"pako": "^2.0.4",
|
||||
"prosemirror-markdown": "^1.7.0",
|
||||
"prosemirror-tables": "^1.1.1",
|
||||
"prosemirror-utils": "^0.9.6",
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,17 @@
|
|||
import { Icon } from '@douyinfe/semi-ui';
|
||||
|
||||
export const IconFlow: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" role="presentation">
|
||||
<path
|
||||
d="M15.722 1.55a2 2 0 0 1 2.828 0l3.536 3.536a2 2 0 0 1 0 2.828L18.55 11.45a2 2 0 0 1-2.828 0l-3.536-3.536a2.003 2.003 0 0 1-.318-.414H7.5a1 1 0 0 0-1 1V16a1 1 0 0 0 1 1H11v-1a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-7a2 2 0 0 1-2-2v-1H7.5a3 3 0 0 1-3-3v-3.181H1.91a.91.91 0 1 1 0-1.819H4.5V8.5a3 3 0 0 1 3-3h4.368c.085-.148.192-.288.318-.414l3.536-3.536Zm1.414 1.414L13.601 6.5l3.535 3.536L20.672 6.5l-3.536-3.536ZM20 16h-7v4h7v-4Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -52,11 +52,10 @@ export * from './IconCountdown';
|
|||
export * from './IconDrawBoard';
|
||||
export * from './IconCallout';
|
||||
export * from './IconStructure';
|
||||
|
||||
export * from './IconMindLeft';
|
||||
export * from './IconMindRight';
|
||||
export * from './IconMindSide';
|
||||
export * from './IconMindFull';
|
||||
export * from './IconMindCenter';
|
||||
|
||||
export * from './IconTableOfContents';
|
||||
export * from './IconFlow';
|
||||
|
|
|
@ -14,6 +14,7 @@ function MyApp({ Component, pageProps }: AppProps) {
|
|||
<>
|
||||
<Head>
|
||||
<meta name="viewport" content="viewport-fit=cover" />
|
||||
<script src="/drawio.embed.js"></script>
|
||||
</Head>
|
||||
<Component {...pageProps} />
|
||||
</>
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
import { Node, mergeAttributes, nodeInputRule } from '@tiptap/core';
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import { FlowWrapper } from 'tiptap/core/wrappers/flow';
|
||||
import { getDatasetAttribute } from 'tiptap/prose-utils';
|
||||
|
||||
export interface IFlowAttrs {
|
||||
width?: number | string;
|
||||
height?: number;
|
||||
data?: string;
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
flow: {
|
||||
setFlow: (attrs?: IFlowAttrs) => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const Flow = Node.create({
|
||||
name: 'flow',
|
||||
group: 'block',
|
||||
selectable: true,
|
||||
atom: true,
|
||||
inline: false,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
width: {
|
||||
default: null,
|
||||
parseHTML: getDatasetAttribute('width'),
|
||||
},
|
||||
height: {
|
||||
default: 240,
|
||||
parseHTML: getDatasetAttribute('height'),
|
||||
},
|
||||
data: {
|
||||
default: '',
|
||||
parseHTML: getDatasetAttribute('data'),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {
|
||||
class: 'flow',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'div',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setFlow:
|
||||
(options) =>
|
||||
({ tr, commands, chain, editor }) => {
|
||||
options = options || {};
|
||||
options.data = options.data || '';
|
||||
|
||||
// @ts-ignore
|
||||
if (tr.selection?.node?.type?.name == this.name) {
|
||||
return commands.updateAttributes(this.name, options);
|
||||
}
|
||||
|
||||
const { selection } = editor.state;
|
||||
const pos = selection.$head;
|
||||
return chain()
|
||||
.insertContentAt(pos.before(), [
|
||||
{
|
||||
type: this.name,
|
||||
attrs: options,
|
||||
},
|
||||
])
|
||||
.run();
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(FlowWrapper);
|
||||
},
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
nodeInputRule({
|
||||
find: /^\$flow $/,
|
||||
type: this.type,
|
||||
getAttributes: (match) => {
|
||||
return { type: match[1] };
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
|
@ -34,6 +34,7 @@
|
|||
.node-image,
|
||||
.node-katex,
|
||||
.node-mind,
|
||||
.node-flow,
|
||||
.node-codeBlock,
|
||||
.node-documentChildren,
|
||||
.node-documentReference {
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
.node-iframe,
|
||||
.node-katex,
|
||||
.node-mind,
|
||||
.node-flow,
|
||||
.node-codeBlock,
|
||||
.node-documentChildren,
|
||||
.node-documentReference,
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
import pako from 'pako';
|
||||
|
||||
function parseXml(xml) {
|
||||
if (window.DOMParser) {
|
||||
const parser = new DOMParser();
|
||||
|
||||
return parser.parseFromString(xml, 'text/xml');
|
||||
} else {
|
||||
const result = createXmlDocument();
|
||||
|
||||
result.async = 'false';
|
||||
result.loadXML(xml);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
function createXmlDocument() {
|
||||
let doc = null;
|
||||
|
||||
if (document.implementation && document.implementation.createDocument) {
|
||||
doc = document.implementation.createDocument('', '', null);
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
function getTextContent(node) {
|
||||
return node != null ? node[node.textContent === undefined ? 'text' : 'textContent'] : '';
|
||||
}
|
||||
|
||||
export function decode(data) {
|
||||
try {
|
||||
const node = parseXml(data).documentElement;
|
||||
if (node != null && node.nodeName == 'mxfile') {
|
||||
const diagrams = node.getElementsByTagName('diagram');
|
||||
if (diagrams.length > 0) {
|
||||
data = getTextContent(diagrams[0]);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
try {
|
||||
data = atob(data);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
alert('atob failed: ' + e);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
data = pako.inflateRaw(
|
||||
Uint8Array.from(data, (c) => String(c).charCodeAt(0)),
|
||||
{
|
||||
to: 'string',
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
alert('inflateRaw failed: ' + e);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
data = decodeURIComponent(data);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
alert('decodeURIComponent failed: ' + e);
|
||||
return;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
.wrap {
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
overflow: visible;
|
||||
line-height: 0;
|
||||
|
||||
.renderWrap {
|
||||
border: 1px solid var(--node-border-color);
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
&::after {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbarWrap {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
padding: 2px 4px;
|
||||
overflow-x: auto;
|
||||
color: #fff;
|
||||
background-color: var(--semi-color-nav-bg);
|
||||
border: 1px solid var(--semi-color-border);
|
||||
border-radius: 3px;
|
||||
align-items: center;
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
import { NodeViewWrapper } from '@tiptap/react';
|
||||
import cls from 'classnames';
|
||||
import { Button, Space } from '@douyinfe/semi-ui';
|
||||
import { IconMindCenter, IconZoomOut, IconZoomIn } from 'components/icons';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { Resizeable } from 'components/resizeable';
|
||||
import { getEditorContainerDOMSize, uuid } from 'tiptap/prose-utils';
|
||||
import { Flow } from 'tiptap/core/extensions/flow';
|
||||
import { decode } from './decode';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const INHERIT_SIZE_STYLE = { width: '100%', height: '100%', maxWidth: '100%' };
|
||||
|
||||
export const FlowWrapper = ({ editor, node, updateAttributes }) => {
|
||||
const $container = useRef<HTMLDivElement>();
|
||||
const $graph = useRef(null);
|
||||
const containerId = useRef(`js-flow-container-${uuid()}`);
|
||||
const isEditable = editor.isEditable;
|
||||
const isActive = editor.isActive(Flow.name);
|
||||
const { width: maxWidth } = getEditorContainerDOMSize(editor);
|
||||
const { data, width, height } = node.attrs;
|
||||
|
||||
const center = useCallback(() => {
|
||||
const graph = $graph.current;
|
||||
if (!graph) return;
|
||||
graph.fit();
|
||||
graph.center(true, false);
|
||||
}, []);
|
||||
|
||||
const zoomOut = useCallback(() => {
|
||||
const graph = $graph.current;
|
||||
if (!graph) return;
|
||||
graph.zoomOut();
|
||||
}, []);
|
||||
|
||||
const zoomIn = useCallback(() => {
|
||||
const graph = $graph.current;
|
||||
if (!graph) return;
|
||||
graph.zoomIn();
|
||||
}, []);
|
||||
|
||||
const onResize = useCallback(
|
||||
(size) => {
|
||||
updateAttributes({ width: size.width, height: size.height });
|
||||
setTimeout(() => {
|
||||
const graph = $graph.current;
|
||||
if (!graph) return;
|
||||
graph.fit();
|
||||
graph.center(true, false);
|
||||
}, 0);
|
||||
},
|
||||
[updateAttributes]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let graph = $graph.current;
|
||||
|
||||
if (!graph) {
|
||||
// @ts-ignore
|
||||
graph = new mxGraph($container.current);
|
||||
graph.resetViewOnRootChange = false;
|
||||
graph.foldingEnabled = false;
|
||||
graph.setTooltips(false);
|
||||
graph.setEnabled(false);
|
||||
graph.centerZoom = true;
|
||||
$graph.current = graph;
|
||||
}
|
||||
|
||||
const text = decode(data);
|
||||
// @ts-ignore
|
||||
const xmlDoc = mxUtils.parseXml(text);
|
||||
// @ts-ignore
|
||||
const codec = new mxCodec(xmlDoc);
|
||||
codec.decode(codec.document.documentElement, graph.getModel());
|
||||
setTimeout(() => {
|
||||
graph.fit();
|
||||
graph.center(true, false);
|
||||
}, 0);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className={cls(styles.wrap, isActive && styles.isActive)}>
|
||||
<Resizeable isEditable={isEditable} width={width} height={height} maxWidth={maxWidth} onChangeEnd={onResize}>
|
||||
<div
|
||||
ref={$container}
|
||||
id={containerId.current}
|
||||
className={cls(styles.renderWrap, 'render-wrapper')}
|
||||
style={{ ...INHERIT_SIZE_STYLE, overflow: 'hidden' }}
|
||||
></div>
|
||||
<div className={styles.toolbarWrap}>
|
||||
<Space spacing={2}>
|
||||
<Button
|
||||
type="tertiary"
|
||||
theme="borderless"
|
||||
size="small"
|
||||
onClick={center}
|
||||
icon={<IconMindCenter style={{ fontSize: '0.85em' }} />}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="tertiary"
|
||||
theme="borderless"
|
||||
size="small"
|
||||
onClick={zoomOut}
|
||||
icon={<IconZoomOut style={{ fontSize: '0.85em' }} />}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="tertiary"
|
||||
theme="borderless"
|
||||
size="small"
|
||||
onClick={zoomIn}
|
||||
icon={<IconZoomIn style={{ fontSize: '0.85em' }} />}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
</Resizeable>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
};
|
|
@ -39,6 +39,7 @@ import { CodeBlock } from 'tiptap/editor/menus/code-block';
|
|||
import { Countdonw } from 'tiptap/editor/menus/countdown';
|
||||
import { DocumentChildren } from 'tiptap/editor/menus/document-children';
|
||||
import { DocumentReference } from 'tiptap/editor/menus/document-reference';
|
||||
import { Flow } from 'tiptap/editor/menus/flow';
|
||||
import { Image } from 'tiptap/editor/menus/image';
|
||||
import { Iframe } from 'tiptap/editor/menus/iframe';
|
||||
import { Table } from 'tiptap/editor/menus/table';
|
||||
|
@ -106,6 +107,7 @@ const _MenuBar: React.FC<{ editor: Editor }> = ({ editor }) => {
|
|||
<Countdonw editor={editor} />
|
||||
<DocumentChildren editor={editor} />
|
||||
<DocumentReference editor={editor} />
|
||||
<Flow editor={editor} />
|
||||
<Image editor={editor} />
|
||||
<Iframe editor={editor} />
|
||||
<Table editor={editor} />
|
||||
|
|
|
@ -52,6 +52,7 @@ import { Countdown } from 'tiptap/core/extensions/countdown';
|
|||
import { DocumentChildren } from 'tiptap/core/extensions/document-children';
|
||||
import { DocumentReference } from 'tiptap/core/extensions/document-reference';
|
||||
import { Emoji } from 'tiptap/core/extensions/emoji';
|
||||
import { Flow } from 'tiptap/core/extensions/flow';
|
||||
import { Iframe } from 'tiptap/core/extensions/iframe';
|
||||
import { Katex } from 'tiptap/core/extensions/katex';
|
||||
import { Mention } from 'tiptap/core/extensions/mention';
|
||||
|
@ -134,6 +135,7 @@ export const CollaborationKit = [
|
|||
DocumentChildren,
|
||||
DocumentReference,
|
||||
Emoji,
|
||||
Flow,
|
||||
Iframe,
|
||||
Katex,
|
||||
Mention,
|
||||
|
|
|
@ -12,6 +12,7 @@ const getEventEmitter = (editor: Editor): EventEmitter => {
|
|||
|
||||
export const OPEN_COUNT_SETTING_MODAL = 'OPEN_COUNT_SETTING_MODAL';
|
||||
export const OPEN_LINK_SETTING_MODAL = 'OPEN_LINK_SETTING_MODAL';
|
||||
export const OPEN_FLOW_SETTING_MODAL = 'OPEN_FLOW_SETTING_MODAL';
|
||||
|
||||
export const subject = (editor: Editor, eventName, handler) => {
|
||||
const event = getEventEmitter(editor);
|
||||
|
@ -32,3 +33,8 @@ export const triggerOpenLinkSettingModal = (editor: Editor, data) => {
|
|||
const event = getEventEmitter(editor);
|
||||
event.emit(OPEN_LINK_SETTING_MODAL, data);
|
||||
};
|
||||
|
||||
export const triggerOpenFlowSettingModal = (editor: Editor, data) => {
|
||||
const event = getEventEmitter(editor);
|
||||
event.emit(OPEN_FLOW_SETTING_MODAL, data);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
import { useCallback } from 'react';
|
||||
import { Space, Button } from '@douyinfe/semi-ui';
|
||||
import { IconEdit, IconCopy, IconDelete } from '@douyinfe/semi-icons';
|
||||
import { Tooltip } from 'components/tooltip';
|
||||
import { BubbleMenu } from 'tiptap/editor/views/bubble-menu';
|
||||
import { Flow } from 'tiptap/core/extensions/flow';
|
||||
import { Divider } from 'tiptap/components/divider';
|
||||
import { copyNode, deleteNode } from 'tiptap/prose-utils';
|
||||
import { useAttributes } from 'tiptap/editor/hooks/use-attributes';
|
||||
import { triggerOpenFlowSettingModal } from '../_event';
|
||||
|
||||
export const FlowBubbleMenu = ({ editor }) => {
|
||||
const attrs = useAttributes(editor, Flow.name, {});
|
||||
|
||||
const openEditLinkModal = useCallback(() => {
|
||||
triggerOpenFlowSettingModal(editor, attrs);
|
||||
}, [editor, attrs]);
|
||||
const shouldShow = useCallback(() => editor.isActive(Flow.name), [editor]);
|
||||
const copyMe = useCallback(() => copyNode(Flow.name, editor), [editor]);
|
||||
const deleteMe = useCallback(() => deleteNode(Flow.name, editor), [editor]);
|
||||
|
||||
return (
|
||||
<BubbleMenu
|
||||
className={'bubble-menu'}
|
||||
editor={editor}
|
||||
pluginKey="flow-bubble-menu"
|
||||
shouldShow={shouldShow}
|
||||
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||
>
|
||||
<Space spacing={4}>
|
||||
<Tooltip content="复制">
|
||||
<Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="编辑">
|
||||
<Button size="small" type="tertiary" theme="borderless" icon={<IconEdit />} onClick={openEditLinkModal} />
|
||||
</Tooltip>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Tooltip content="删除节点" hideOnClick>
|
||||
<Button onClick={deleteMe} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</BubbleMenu>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
import React from 'react';
|
||||
import { Editor } from 'tiptap/editor';
|
||||
import { FlowBubbleMenu } from './bubble';
|
||||
import { FlowSettingModal } from './modal';
|
||||
|
||||
export const Flow: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||
return (
|
||||
<>
|
||||
<FlowBubbleMenu editor={editor} />
|
||||
<FlowSettingModal editor={editor} />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,63 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Modal } from '@douyinfe/semi-ui';
|
||||
import { Editor } from 'tiptap/editor';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import { OPEN_FLOW_SETTING_MODAL, subject, cancelSubject } from '../_event';
|
||||
|
||||
type IProps = { editor: Editor };
|
||||
|
||||
export const FlowSettingModal: React.FC<IProps> = ({ editor }) => {
|
||||
const $iframe = useRef<HTMLIFrameElement>();
|
||||
const [initialData, setInitialData] = useState('');
|
||||
const [visible, toggleVisible] = useToggle(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (data) => {
|
||||
toggleVisible(true);
|
||||
data && setInitialData(data.data);
|
||||
};
|
||||
|
||||
subject(editor, OPEN_FLOW_SETTING_MODAL, handler);
|
||||
|
||||
return () => {
|
||||
cancelSubject(editor, OPEN_FLOW_SETTING_MODAL, handler);
|
||||
};
|
||||
}, [editor, toggleVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
const receiver = (evt) => {
|
||||
if (!evt.data) {
|
||||
toggleVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt.data == 'ready') {
|
||||
$iframe.current.contentWindow.postMessage(initialData, '*');
|
||||
} else {
|
||||
if (evt.data.length > 0) {
|
||||
const data = evt.data;
|
||||
editor.chain().focus().setFlow({ data }).run();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', receiver);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('message', receiver);
|
||||
};
|
||||
}, [editor, toggleVisible, initialData]);
|
||||
|
||||
return (
|
||||
<Modal centered title="流程图" fullScreen visible={visible} header={null} footer={null}>
|
||||
<div style={{ height: '100%', margin: '0 -24px' }}>
|
||||
<iframe
|
||||
ref={$iframe}
|
||||
src={`${process.env.DRAWIO_URL}?embed=1&lang=zh&hide-pages=1&drafts=0&client=1&spin=0`}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
frameBorder={0}
|
||||
></iframe>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -16,6 +16,7 @@ import {
|
|||
IconCountdown,
|
||||
IconCallout,
|
||||
IconTableOfContents,
|
||||
IconFlow,
|
||||
} from 'components/icons';
|
||||
import { GridSelect } from 'components/grid-select';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
|
@ -95,6 +96,13 @@ const COMMANDS = [
|
|||
{
|
||||
title: '卡片',
|
||||
},
|
||||
{
|
||||
icon: <IconFlow />,
|
||||
label: '流程图',
|
||||
action: (editor) => {
|
||||
editor.chain().focus().setFlow({ width: '100%' }).run();
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <IconMind />,
|
||||
label: '思维导图',
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
IconCountdown,
|
||||
IconCallout,
|
||||
IconTableOfContents,
|
||||
IconFlow,
|
||||
} from 'components/icons';
|
||||
import { createCountdown } from './countdown/service';
|
||||
import { createOrToggleLink } from './link/service';
|
||||
|
@ -201,6 +202,17 @@ export const QUICK_INSERT_ITEMS = [
|
|||
command: (editor: Editor) => editor.chain().focus().setIframe({ url: '' }).run(),
|
||||
},
|
||||
|
||||
{
|
||||
key: '流程图',
|
||||
label: (
|
||||
<Space>
|
||||
<IconFlow />
|
||||
流程图
|
||||
</Space>
|
||||
),
|
||||
command: (editor: Editor) => editor.chain().focus().setFlow({ width: '100%' }).run(),
|
||||
},
|
||||
|
||||
{
|
||||
key: '思维导图',
|
||||
label: (
|
||||
|
|
|
@ -19,6 +19,7 @@ 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 { Flow } from 'tiptap/core/extensions/flow';
|
||||
import { Mind } from 'tiptap/core/extensions/mind';
|
||||
import { Table } from 'tiptap/core/extensions/table';
|
||||
import { TableOfContents } from 'tiptap/core/extensions/table-of-contents';
|
||||
|
@ -35,6 +36,7 @@ const OTHER_BUBBLE_MENU_TYPES = [
|
|||
Callout.name,
|
||||
CodeBlock.name,
|
||||
Iframe.name,
|
||||
Flow.name,
|
||||
Mind.name,
|
||||
Table.name,
|
||||
TableOfContents.name,
|
||||
|
|
|
@ -10,6 +10,7 @@ client:
|
|||
assetPrefix: '/'
|
||||
apiUrl: 'http://localhost:5001/api'
|
||||
collaborationUrl: 'ws://localhost:5003'
|
||||
drawioUrl: 'https://embed.diagrams.net'
|
||||
|
||||
# 数据库配置
|
||||
db:
|
||||
|
|
|
@ -122,6 +122,7 @@ importers:
|
|||
markdown-it-sub: ^1.0.0
|
||||
markdown-it-sup: ^1.0.0
|
||||
next: 12.0.10
|
||||
pako: ^2.0.4
|
||||
prosemirror-markdown: ^1.7.0
|
||||
prosemirror-tables: ^1.1.1
|
||||
prosemirror-utils: ^0.9.6
|
||||
|
@ -206,6 +207,7 @@ importers:
|
|||
markdown-it-sub: 1.0.0
|
||||
markdown-it-sup: 1.0.0
|
||||
next: 12.0.10_react-dom@17.0.2+react@17.0.2
|
||||
pako: 2.0.4
|
||||
prosemirror-markdown: 1.7.0
|
||||
prosemirror-tables: 1.1.1
|
||||
prosemirror-utils: 0.9.6_prosemirror-tables@1.1.1
|
||||
|
@ -6986,6 +6988,10 @@ packages:
|
|||
netmask: 2.0.2
|
||||
dev: false
|
||||
|
||||
/pako/2.0.4:
|
||||
resolution: {integrity: sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg==}
|
||||
dev: false
|
||||
|
||||
/parent-module/1.0.1:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
Loading…
Reference in New Issue