mirror of https://github.com/fantasticit/think.git
Merge pull request #161 from fantasticit/feat/excalidraw
This commit is contained in:
commit
d98fb8d4c5
|
@ -11,6 +11,7 @@
|
||||||
"@douyinfe/semi-icons": "^2.3.1",
|
"@douyinfe/semi-icons": "^2.3.1",
|
||||||
"@douyinfe/semi-next": "^2.3.1",
|
"@douyinfe/semi-next": "^2.3.1",
|
||||||
"@douyinfe/semi-ui": "^2.3.1",
|
"@douyinfe/semi-ui": "^2.3.1",
|
||||||
|
"@excalidraw/excalidraw": "^0.12.0",
|
||||||
"@hocuspocus/provider": "^1.0.0-alpha.29",
|
"@hocuspocus/provider": "^1.0.0-alpha.29",
|
||||||
"@think/config": "workspace:^1.0.0",
|
"@think/config": "workspace:^1.0.0",
|
||||||
"@think/constants": "workspace:^1.0.0",
|
"@think/constants": "workspace:^1.0.0",
|
||||||
|
|
|
@ -141,3 +141,7 @@
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.excalidraw.excalidraw-modal-container {
|
||||||
|
z-index: 1010 !important;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
import { IUser } from '@think/domains';
|
||||||
|
import { mergeAttributes, Node, nodeInputRule } from '@tiptap/core';
|
||||||
|
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
|
import { ExcalidrawWrapper } from 'tiptap/core/wrappers/excalidraw';
|
||||||
|
import { getDatasetAttribute } from 'tiptap/prose-utils';
|
||||||
|
|
||||||
|
const DEFAULT_MIND_DATA = { elements: [] };
|
||||||
|
|
||||||
|
export interface IExcalidrawAttrs {
|
||||||
|
defaultShowPicker?: boolean;
|
||||||
|
createUser?: IUser['id'];
|
||||||
|
width?: number | string;
|
||||||
|
height?: number;
|
||||||
|
data?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
excalidraw: {
|
||||||
|
setExcalidraw: (attrs?: IExcalidrawAttrs) => ReturnType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Excalidraw = Node.create({
|
||||||
|
name: 'excalidraw',
|
||||||
|
group: 'block',
|
||||||
|
selectable: true,
|
||||||
|
atom: true,
|
||||||
|
draggable: true,
|
||||||
|
inline: false,
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
defaultShowPicker: {
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
createUser: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
default: '100%',
|
||||||
|
parseHTML: getDatasetAttribute('width'),
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
default: 240,
|
||||||
|
parseHTML: getDatasetAttribute('height'),
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
default: DEFAULT_MIND_DATA,
|
||||||
|
parseHTML: getDatasetAttribute('data', true),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
HTMLAttributes: {
|
||||||
|
class: 'mind',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: 'div[class=mind]',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
|
||||||
|
},
|
||||||
|
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setExcalidraw:
|
||||||
|
(options) =>
|
||||||
|
({ tr, commands, chain, editor }) => {
|
||||||
|
options = options || {};
|
||||||
|
options.data = options.data || DEFAULT_MIND_DATA;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
if (tr.selection?.node?.type?.name == this.name) {
|
||||||
|
return commands.updateAttributes(this.name, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chain()
|
||||||
|
.insertContent({
|
||||||
|
type: this.name,
|
||||||
|
attrs: options,
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addNodeView() {
|
||||||
|
return ReactNodeViewRenderer(ExcalidrawWrapper);
|
||||||
|
},
|
||||||
|
|
||||||
|
addInputRules() {
|
||||||
|
return [
|
||||||
|
nodeInputRule({
|
||||||
|
find: /^\$excalidraw $/,
|
||||||
|
type: this.type,
|
||||||
|
getAttributes: () => {
|
||||||
|
return { width: '100%' };
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
|
@ -14,6 +14,7 @@ export const OPEN_COUNT_SETTING_MODAL = 'OPEN_COUNT_SETTING_MODAL';
|
||||||
export const OPEN_LINK_SETTING_MODAL = 'OPEN_LINK_SETTING_MODAL';
|
export const OPEN_LINK_SETTING_MODAL = 'OPEN_LINK_SETTING_MODAL';
|
||||||
export const OPEN_FLOW_SETTING_MODAL = 'OPEN_FLOW_SETTING_MODAL';
|
export const OPEN_FLOW_SETTING_MODAL = 'OPEN_FLOW_SETTING_MODAL';
|
||||||
export const OPEN_MIND_SETTING_MODAL = 'OPEN_MIND_SETTING_MODAL';
|
export const OPEN_MIND_SETTING_MODAL = 'OPEN_MIND_SETTING_MODAL';
|
||||||
|
export const OPEN_EXCALIDRAW_SETTING_MODAL = 'OPEN_EXCALIDRAW_SETTING_MODAL';
|
||||||
|
|
||||||
export const subject = (editor: Editor, eventName, handler) => {
|
export const subject = (editor: Editor, eventName, handler) => {
|
||||||
const event = getEventEmitter(editor);
|
const event = getEventEmitter(editor);
|
||||||
|
@ -44,3 +45,8 @@ export const triggerOpenMindSettingModal = (editor: Editor, data) => {
|
||||||
const event = getEventEmitter(editor);
|
const event = getEventEmitter(editor);
|
||||||
event.emit(OPEN_MIND_SETTING_MODAL, data);
|
event.emit(OPEN_MIND_SETTING_MODAL, data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const triggerOpenExcalidrawSettingModal = (editor: Editor, data) => {
|
||||||
|
const event = getEventEmitter(editor);
|
||||||
|
event.emit(OPEN_EXCALIDRAW_SETTING_MODAL, data);
|
||||||
|
};
|
||||||
|
|
|
@ -135,6 +135,14 @@ export const COMMANDS: ICommand[] = [
|
||||||
editor.chain().focus().setMind({ width: '100%', defaultShowPicker: true, createUser: user.id }).run();
|
editor.chain().focus().setMind({ width: '100%', defaultShowPicker: true, createUser: user.id }).run();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
isBlock: true,
|
||||||
|
icon: <IconMind />,
|
||||||
|
label: '绘图',
|
||||||
|
action: (editor, user) => {
|
||||||
|
editor.chain().focus().setExcalidraw({ width: '100%', defaultShowPicker: true, createUser: user.id }).run();
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
isBlock: true,
|
isBlock: true,
|
||||||
icon: <IconMath />,
|
icon: <IconMath />,
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { IconCopy, IconDelete, IconEdit, IconLineHeight } from '@douyinfe/semi-icons';
|
||||||
|
import { Button, Space } from '@douyinfe/semi-ui';
|
||||||
|
import { Divider } from 'components/divider';
|
||||||
|
import { SizeSetter } from 'components/size-setter';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { useUser } from 'data/user';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
import { BubbleMenu } from 'tiptap/core/bubble-menu';
|
||||||
|
import { Excalidraw, IExcalidrawAttrs } from 'tiptap/core/extensions/excalidraw';
|
||||||
|
import { useAttributes } from 'tiptap/core/hooks/use-attributes';
|
||||||
|
import { copyNode, deleteNode, getEditorContainerDOMSize } from 'tiptap/prose-utils';
|
||||||
|
|
||||||
|
import { triggerOpenExcalidrawSettingModal } from '../_event';
|
||||||
|
|
||||||
|
export const ExcalidrawBubbleMenu = ({ editor }) => {
|
||||||
|
const { width: maxWidth } = getEditorContainerDOMSize(editor);
|
||||||
|
const attrs = useAttributes<IExcalidrawAttrs>(editor, Excalidraw.name, {
|
||||||
|
defaultShowPicker: false,
|
||||||
|
createUser: '',
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
});
|
||||||
|
const { defaultShowPicker, createUser, width, height } = attrs;
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
|
const setSize = useCallback(
|
||||||
|
(size) => {
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.updateAttributes(Excalidraw.name, size)
|
||||||
|
.setNodeSelection(editor.state.selection.from)
|
||||||
|
.focus()
|
||||||
|
.run();
|
||||||
|
},
|
||||||
|
[editor]
|
||||||
|
);
|
||||||
|
const openEditLinkModal = useCallback(() => {
|
||||||
|
triggerOpenExcalidrawSettingModal(editor, attrs);
|
||||||
|
}, [editor, attrs]);
|
||||||
|
const shouldShow = useCallback(() => editor.isActive(Excalidraw.name), [editor]);
|
||||||
|
const copyMe = useCallback(() => copyNode(Excalidraw.name, editor), [editor]);
|
||||||
|
const deleteMe = useCallback(() => deleteNode(Excalidraw.name, editor), [editor]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (defaultShowPicker && user && createUser === user.id) {
|
||||||
|
openEditLinkModal();
|
||||||
|
editor.chain().updateAttributes(Excalidraw.name, { defaultShowPicker: false }).focus().run();
|
||||||
|
}
|
||||||
|
}, [createUser, defaultShowPicker, editor, openEditLinkModal, user]);
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
<SizeSetter width={width} maxWidth={maxWidth} height={height} onOk={setSize}>
|
||||||
|
<Tooltip content="设置宽高">
|
||||||
|
<Button icon={<IconLineHeight />} type="tertiary" theme="borderless" size="small" />
|
||||||
|
</Tooltip>
|
||||||
|
</SizeSetter>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Tooltip content="删除节点" hideOnClick>
|
||||||
|
<Button onClick={deleteMe} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
</BubbleMenu>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,14 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Editor } from 'tiptap/core';
|
||||||
|
|
||||||
|
import { ExcalidrawBubbleMenu } from './bubble';
|
||||||
|
import { ExcalidrawSettingModal } from './modal';
|
||||||
|
|
||||||
|
export const Excalidraw: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ExcalidrawBubbleMenu editor={editor} />
|
||||||
|
<ExcalidrawSettingModal editor={editor} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,102 @@
|
||||||
|
import { Modal, Spin, Typography } from '@douyinfe/semi-ui';
|
||||||
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { Editor } from 'tiptap/core';
|
||||||
|
|
||||||
|
import { cancelSubject, OPEN_EXCALIDRAW_SETTING_MODAL, subject } from '../_event';
|
||||||
|
|
||||||
|
type IProps = { editor: Editor };
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
export const ExcalidrawSettingModal: React.FC<IProps> = ({ editor }) => {
|
||||||
|
const [Excalidraw, setExcalidraw] = useState(null);
|
||||||
|
const [data, setData] = useState({});
|
||||||
|
const [initialData, setInitialData] = useState({ elements: [], appState: { isLoading: false }, files: null });
|
||||||
|
const [visible, toggleVisible] = useToggle(false);
|
||||||
|
const [loading, toggleLoading] = useToggle(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const renderEditor = useCallback(
|
||||||
|
(div) => {
|
||||||
|
if (!div) return;
|
||||||
|
|
||||||
|
import('@excalidraw/excalidraw')
|
||||||
|
.then((res) => {
|
||||||
|
setExcalidraw(res.Excalidraw);
|
||||||
|
})
|
||||||
|
.catch(setError)
|
||||||
|
.finally(() => toggleLoading(false));
|
||||||
|
},
|
||||||
|
[toggleLoading]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderExcalidraw = useCallback((app) => {
|
||||||
|
console.log('render', app);
|
||||||
|
setTimeout(() => {
|
||||||
|
app.refresh();
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onChange = useCallback((elements, appState, files) => {
|
||||||
|
// excalidraw 导出的是 {},实际上应该是 []
|
||||||
|
// appState.collaborators = [];
|
||||||
|
setData({
|
||||||
|
elements,
|
||||||
|
appState: { isLoading: false },
|
||||||
|
files,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const save = useCallback(() => {
|
||||||
|
if (!Excalidraw) {
|
||||||
|
toggleVisible(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.chain().focus().setExcalidraw({ data }).run();
|
||||||
|
toggleVisible(false);
|
||||||
|
}, [Excalidraw, editor, data, toggleVisible]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (data) => {
|
||||||
|
toggleVisible(true);
|
||||||
|
data && setInitialData(data.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
subject(editor, OPEN_EXCALIDRAW_SETTING_MODAL, handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelSubject(editor, OPEN_EXCALIDRAW_SETTING_MODAL, handler);
|
||||||
|
};
|
||||||
|
}, [editor, toggleVisible]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
centered
|
||||||
|
title="绘图"
|
||||||
|
fullScreen
|
||||||
|
visible={visible}
|
||||||
|
onCancel={toggleVisible}
|
||||||
|
onOk={save}
|
||||||
|
okText="保存"
|
||||||
|
cancelText="退出"
|
||||||
|
motion={false}
|
||||||
|
>
|
||||||
|
<div style={{ height: '100%', margin: '0 -24px', border: '1px solid var(--semi-color-border)' }}>
|
||||||
|
{loading && (
|
||||||
|
<Spin spinning>
|
||||||
|
{/* FIXME: semi-design 的问题,不加 div,文字会换行! */}
|
||||||
|
<div></div>
|
||||||
|
</Spin>
|
||||||
|
)}
|
||||||
|
{error && <Text>{(error && error.message) || '未知错误'}</Text>}
|
||||||
|
<div style={{ width: '100%', height: '100%' }} ref={renderEditor}>
|
||||||
|
{!loading && !error && Excalidraw ? (
|
||||||
|
<Excalidraw ref={renderExcalidraw} onChange={onChange} langCode="zh-CN" initialData={initialData} />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
|
@ -38,7 +38,8 @@
|
||||||
.node-flow,
|
.node-flow,
|
||||||
.node-codeBlock,
|
.node-codeBlock,
|
||||||
.node-documentChildren,
|
.node-documentChildren,
|
||||||
.node-documentReference {
|
.node-documentReference,
|
||||||
|
.node-excalidraw {
|
||||||
margin-top: 0.75em;
|
margin-top: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +61,8 @@
|
||||||
.node-flow,
|
.node-flow,
|
||||||
.node-codeBlock,
|
.node-codeBlock,
|
||||||
.node-documentChildren,
|
.node-documentChildren,
|
||||||
.node-documentReference {
|
.node-documentReference,
|
||||||
|
.node-excalidraw {
|
||||||
.render-wrapper {
|
.render-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
|
|
|
@ -36,7 +36,8 @@
|
||||||
.node-codeBlock,
|
.node-codeBlock,
|
||||||
.node-documentChildren,
|
.node-documentChildren,
|
||||||
.node-documentReference,
|
.node-documentReference,
|
||||||
.node-status {
|
.node-status,
|
||||||
|
.node-excalidraw {
|
||||||
&.selected-node {
|
&.selected-node {
|
||||||
&:not(.has-focus) {
|
&:not(.has-focus) {
|
||||||
::selection {
|
::selection {
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: flex;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #f80;
|
||||||
|
border-radius: 2px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
import { Space, Spin, Typography } from '@douyinfe/semi-ui';
|
||||||
|
import { NodeViewWrapper } from '@tiptap/react';
|
||||||
|
import cls from 'classnames';
|
||||||
|
import { IconMind } from 'components/icons';
|
||||||
|
import { Resizeable } from 'components/resizeable';
|
||||||
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import VisibilitySensor from 'react-visibility-sensor';
|
||||||
|
import { Excalidraw } from 'tiptap/core/extensions/excalidraw';
|
||||||
|
import { getEditorContainerDOMSize } from 'tiptap/prose-utils';
|
||||||
|
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const INHERIT_SIZE_STYLE = { width: '100%', height: '100%', maxWidth: '100%' };
|
||||||
|
|
||||||
|
export const ExcalidrawWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
|
const exportToSvgRef = useRef(null);
|
||||||
|
const isEditable = editor.isEditable;
|
||||||
|
const isActive = editor.isActive(Excalidraw.name);
|
||||||
|
const { width: maxWidth } = getEditorContainerDOMSize(editor);
|
||||||
|
const { data, width, height } = node.attrs;
|
||||||
|
const [Svg, setSvg] = useState<SVGElement | null>(null);
|
||||||
|
const [loading, toggleLoading] = useToggle(true);
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
const [visible, toggleVisible] = useToggle(false);
|
||||||
|
|
||||||
|
const onResize = useCallback(
|
||||||
|
(size) => {
|
||||||
|
updateAttributes({ width: size.width, height: size.height });
|
||||||
|
},
|
||||||
|
[updateAttributes]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onViewportChange = useCallback(
|
||||||
|
(visible) => {
|
||||||
|
if (visible) {
|
||||||
|
toggleVisible(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[toggleVisible]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
import('@excalidraw/excalidraw')
|
||||||
|
.then((res) => {
|
||||||
|
exportToSvgRef.current = res.exportToSvg;
|
||||||
|
})
|
||||||
|
.catch(setError)
|
||||||
|
.finally(() => toggleLoading(false));
|
||||||
|
}, [toggleLoading, data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const setContent = async () => {
|
||||||
|
if (loading || error || !visible || !data) return;
|
||||||
|
|
||||||
|
const svg: SVGElement = await exportToSvgRef.current(data);
|
||||||
|
|
||||||
|
svg.setAttribute('width', '100%');
|
||||||
|
svg.setAttribute('height', '100%');
|
||||||
|
svg.setAttribute('display', 'block');
|
||||||
|
|
||||||
|
setSvg(svg);
|
||||||
|
};
|
||||||
|
setContent();
|
||||||
|
}, [data, loading, error, visible]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeViewWrapper className={cls(styles.wrap, isActive && styles.isActive)}>
|
||||||
|
<VisibilitySensor onChange={onViewportChange}>
|
||||||
|
<Resizeable isEditable={isEditable} width={width} height={height} maxWidth={maxWidth} onChangeEnd={onResize}>
|
||||||
|
<div
|
||||||
|
className={cls(styles.renderWrap, 'render-wrapper')}
|
||||||
|
style={{ ...INHERIT_SIZE_STYLE, overflow: 'hidden' }}
|
||||||
|
>
|
||||||
|
{error && (
|
||||||
|
<div style={INHERIT_SIZE_STYLE}>
|
||||||
|
<Text>{error.message || error}</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loading && <Spin spinning style={INHERIT_SIZE_STYLE}></Spin>}
|
||||||
|
|
||||||
|
{!loading && !error && visible && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '100%',
|
||||||
|
maxHeight: '100%',
|
||||||
|
padding: 24,
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
dangerouslySetInnerHTML={{ __html: Svg?.outerHTML ?? '' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.title}>
|
||||||
|
<Space>
|
||||||
|
<span className={styles.icon}>
|
||||||
|
<IconMind />
|
||||||
|
</span>
|
||||||
|
绘图
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Resizeable>
|
||||||
|
</VisibilitySensor>
|
||||||
|
</NodeViewWrapper>
|
||||||
|
);
|
||||||
|
};
|
|
@ -16,6 +16,7 @@ import { Countdonw } from 'tiptap/core/menus/countdown';
|
||||||
import { DocumentChildren } from 'tiptap/core/menus/document-children';
|
import { DocumentChildren } from 'tiptap/core/menus/document-children';
|
||||||
import { DocumentReference } from 'tiptap/core/menus/document-reference';
|
import { DocumentReference } from 'tiptap/core/menus/document-reference';
|
||||||
import { Emoji } from 'tiptap/core/menus/emoji';
|
import { Emoji } from 'tiptap/core/menus/emoji';
|
||||||
|
import { Excalidraw } from 'tiptap/core/menus/excalidraw';
|
||||||
import { Flow } from 'tiptap/core/menus/flow';
|
import { Flow } from 'tiptap/core/menus/flow';
|
||||||
import { FontSize } from 'tiptap/core/menus/fontsize';
|
import { FontSize } from 'tiptap/core/menus/fontsize';
|
||||||
import { Heading } from 'tiptap/core/menus/heading';
|
import { Heading } from 'tiptap/core/menus/heading';
|
||||||
|
@ -107,6 +108,7 @@ const _MenuBar: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
<Table editor={editor} />
|
<Table editor={editor} />
|
||||||
<Katex editor={editor} />
|
<Katex editor={editor} />
|
||||||
<Mind editor={editor} />
|
<Mind editor={editor} />
|
||||||
|
<Excalidraw editor={editor} />
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { Dragable } from 'tiptap/core/extensions/dragable';
|
||||||
import { Dropcursor } from 'tiptap/core/extensions/dropcursor';
|
import { Dropcursor } from 'tiptap/core/extensions/dropcursor';
|
||||||
import { Emoji } from 'tiptap/core/extensions/emoji';
|
import { Emoji } from 'tiptap/core/extensions/emoji';
|
||||||
import { EventEmitter } from 'tiptap/core/extensions/event-emitter';
|
import { EventEmitter } from 'tiptap/core/extensions/event-emitter';
|
||||||
|
import { Excalidraw } from 'tiptap/core/extensions/excalidraw';
|
||||||
import { Flow } from 'tiptap/core/extensions/flow';
|
import { Flow } from 'tiptap/core/extensions/flow';
|
||||||
import { Focus } from 'tiptap/core/extensions/focus';
|
import { Focus } from 'tiptap/core/extensions/focus';
|
||||||
import { FontSize } from 'tiptap/core/extensions/font-size';
|
import { FontSize } from 'tiptap/core/extensions/font-size';
|
||||||
|
@ -102,6 +103,7 @@ export const CollaborationKit = [
|
||||||
Color,
|
Color,
|
||||||
ColorHighlighter,
|
ColorHighlighter,
|
||||||
Dropcursor,
|
Dropcursor,
|
||||||
|
Excalidraw,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Focus,
|
Focus,
|
||||||
FontSize,
|
FontSize,
|
||||||
|
|
|
@ -44,6 +44,7 @@ importers:
|
||||||
'@douyinfe/semi-icons': ^2.3.1
|
'@douyinfe/semi-icons': ^2.3.1
|
||||||
'@douyinfe/semi-next': ^2.3.1
|
'@douyinfe/semi-next': ^2.3.1
|
||||||
'@douyinfe/semi-ui': ^2.3.1
|
'@douyinfe/semi-ui': ^2.3.1
|
||||||
|
'@excalidraw/excalidraw': ^0.12.0
|
||||||
'@hocuspocus/provider': ^1.0.0-alpha.29
|
'@hocuspocus/provider': ^1.0.0-alpha.29
|
||||||
'@think/config': workspace:^1.0.0
|
'@think/config': workspace:^1.0.0
|
||||||
'@think/constants': workspace:^1.0.0
|
'@think/constants': workspace:^1.0.0
|
||||||
|
@ -157,6 +158,7 @@ importers:
|
||||||
'@douyinfe/semi-icons': 2.3.1_react@17.0.2
|
'@douyinfe/semi-icons': 2.3.1_react@17.0.2
|
||||||
'@douyinfe/semi-next': 2.3.1
|
'@douyinfe/semi-next': 2.3.1
|
||||||
'@douyinfe/semi-ui': 2.3.1_wnecvl2xit6hykxlpfa3byfhr4
|
'@douyinfe/semi-ui': 2.3.1_wnecvl2xit6hykxlpfa3byfhr4
|
||||||
|
'@excalidraw/excalidraw': 0.12.0_sfoxds7t5ydpegc3knd667wn6m
|
||||||
'@hocuspocus/provider': 1.0.0-alpha.29
|
'@hocuspocus/provider': 1.0.0-alpha.29
|
||||||
'@think/config': link:../config
|
'@think/config': link:../config
|
||||||
'@think/constants': link:../constants
|
'@think/constants': link:../constants
|
||||||
|
@ -1953,6 +1955,17 @@ packages:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@excalidraw/excalidraw/0.12.0_sfoxds7t5ydpegc3knd667wn6m:
|
||||||
|
resolution: {integrity: sha512-xMPmKmOEgKij43k5m6Koaevb+SBw6La7MT9UDY8Iq7nQCMhA1HQwcUURfSkZ3ERibdQmMsAGtjSLbkX7hrA3+A==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^17.0.2
|
||||||
|
react-dom: ^17.0.2
|
||||||
|
dependencies:
|
||||||
|
dotenv: 10.0.0
|
||||||
|
react: 17.0.2
|
||||||
|
react-dom: 17.0.2_react@17.0.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@hocuspocus/common/1.0.0-alpha.4:
|
/@hocuspocus/common/1.0.0-alpha.4:
|
||||||
resolution: {integrity: sha512-LvKj+ASSWnvjFB7n2bl7BUGFKF9XFFP1oA3/XmKl3c7wUIvoN1Ir3sX8XnN6qBA3S2CoruEHHk+KPS6zMSMfHA==}
|
resolution: {integrity: sha512-LvKj+ASSWnvjFB7n2bl7BUGFKF9XFFP1oA3/XmKl3c7wUIvoN1Ir3sX8XnN6qBA3S2CoruEHHk+KPS6zMSMfHA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
Loading…
Reference in New Issue