From cf1a2340629e3d2b55a5e65da8c6b06b9c406983 Mon Sep 17 00:00:00 2001 From: fantasticit Date: Sat, 13 Aug 2022 14:27:54 +0800 Subject: [PATCH 1/2] tiptap: add excalidraw --- packages/client/package.json | 1 + packages/client/src/styles/globals.scss | 4 + .../src/tiptap/core/extensions/excalidraw.ts | 114 +++++++++++++++++ .../client/src/tiptap/core/menus/_event.ts | 6 + .../client/src/tiptap/core/menus/commands.tsx | 8 ++ .../tiptap/core/menus/excalidraw/bubble.tsx | 82 +++++++++++++ .../tiptap/core/menus/excalidraw/index.tsx | 14 +++ .../tiptap/core/menus/excalidraw/modal.tsx | 106 ++++++++++++++++ .../client/src/tiptap/core/styles/node.scss | 6 +- .../src/tiptap/core/styles/selection.scss | 3 +- .../wrappers/excalidraw/index.module.scss | 33 +++++ .../tiptap/core/wrappers/excalidraw/index.tsx | 116 ++++++++++++++++++ .../collaboration/collaboration/menubar.tsx | 2 + .../src/tiptap/editor/collaboration/kit.ts | 2 + pnpm-lock.yaml | 13 ++ 15 files changed, 507 insertions(+), 3 deletions(-) create mode 100644 packages/client/src/tiptap/core/extensions/excalidraw.ts create mode 100644 packages/client/src/tiptap/core/menus/excalidraw/bubble.tsx create mode 100644 packages/client/src/tiptap/core/menus/excalidraw/index.tsx create mode 100644 packages/client/src/tiptap/core/menus/excalidraw/modal.tsx create mode 100644 packages/client/src/tiptap/core/wrappers/excalidraw/index.module.scss create mode 100644 packages/client/src/tiptap/core/wrappers/excalidraw/index.tsx diff --git a/packages/client/package.json b/packages/client/package.json index 6035a2ad..8db59a5b 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -11,6 +11,7 @@ "@douyinfe/semi-icons": "^2.3.1", "@douyinfe/semi-next": "^2.3.1", "@douyinfe/semi-ui": "^2.3.1", + "@excalidraw/excalidraw": "^0.12.0", "@hocuspocus/provider": "^1.0.0-alpha.29", "@think/config": "workspace:^1.0.0", "@think/constants": "workspace:^1.0.0", diff --git a/packages/client/src/styles/globals.scss b/packages/client/src/styles/globals.scss index 37e34317..81714f1d 100644 --- a/packages/client/src/styles/globals.scss +++ b/packages/client/src/styles/globals.scss @@ -141,3 +141,7 @@ background-color: transparent; } } + +.excalidraw.excalidraw-modal-container { + z-index: 1010 !important; +} diff --git a/packages/client/src/tiptap/core/extensions/excalidraw.ts b/packages/client/src/tiptap/core/extensions/excalidraw.ts new file mode 100644 index 00000000..5f8e6931 --- /dev/null +++ b/packages/client/src/tiptap/core/extensions/excalidraw.ts @@ -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 = []; + +export interface IExcalidrawAttrs { + defaultShowPicker?: boolean; + createUser?: IUser['id']; + width?: number | string; + height?: number; + data?: Array; +} + +declare module '@tiptap/core' { + interface Commands { + 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%' }; + }, + }), + ]; + }, +}); diff --git a/packages/client/src/tiptap/core/menus/_event.ts b/packages/client/src/tiptap/core/menus/_event.ts index fe687da4..e0f3ec8f 100644 --- a/packages/client/src/tiptap/core/menus/_event.ts +++ b/packages/client/src/tiptap/core/menus/_event.ts @@ -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_FLOW_SETTING_MODAL = 'OPEN_FLOW_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) => { const event = getEventEmitter(editor); @@ -44,3 +45,8 @@ export const triggerOpenMindSettingModal = (editor: Editor, data) => { const event = getEventEmitter(editor); event.emit(OPEN_MIND_SETTING_MODAL, data); }; + +export const triggerOpenExcalidrawSettingModal = (editor: Editor, data) => { + const event = getEventEmitter(editor); + event.emit(OPEN_EXCALIDRAW_SETTING_MODAL, data); +}; diff --git a/packages/client/src/tiptap/core/menus/commands.tsx b/packages/client/src/tiptap/core/menus/commands.tsx index fb6a9201..a09896be 100644 --- a/packages/client/src/tiptap/core/menus/commands.tsx +++ b/packages/client/src/tiptap/core/menus/commands.tsx @@ -135,6 +135,14 @@ export const COMMANDS: ICommand[] = [ editor.chain().focus().setMind({ width: '100%', defaultShowPicker: true, createUser: user.id }).run(); }, }, + { + isBlock: true, + icon: , + label: '绘图', + action: (editor, user) => { + editor.chain().focus().setExcalidraw({ width: '100%', defaultShowPicker: true, createUser: user.id }).run(); + }, + }, { isBlock: true, icon: , diff --git a/packages/client/src/tiptap/core/menus/excalidraw/bubble.tsx b/packages/client/src/tiptap/core/menus/excalidraw/bubble.tsx new file mode 100644 index 00000000..d8ac58a4 --- /dev/null +++ b/packages/client/src/tiptap/core/menus/excalidraw/bubble.tsx @@ -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(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 ( + + + +