forked from docmost/docmost
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: excalidraw integration (docmost#214)
* update tiptap version * excalidraw init * cleanup * better file handling and other fixes * use different modal to fix excalidraw cursor position issue * see excalidraw/excalidraw#7312 * fix websocket in vite dev mode * WIP * add align attribute * fix table * menu icons * Render image in excalidraw html * add size to custom SVG components * rewrite undefined font urls
- Loading branch information
1 parent
77b541e
commit 38e9eef
Showing
26 changed files
with
1,443 additions
and
802 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { rem } from "@mantine/core"; | ||
|
||
interface Props { | ||
size?: number | string; | ||
} | ||
|
||
function IconExcalidraw({ size }: Props) { | ||
return ( | ||
<svg | ||
xmlns="http://www.w3.org/2000/svg" | ||
fill="#6965DB" | ||
viewBox="0 0 24 24" | ||
style={{ width: rem(size), height: rem(size) }} | ||
> | ||
<path d="M23.943 19.806a.196.196 0 00-.168-.034c-1.26-1.855-2.873-3.61-4.419-5.315l-.252-.284c-.001-.073-.067-.12-.134-.15l-.084-.084c-.05-.1-.169-.167-.286-.1-.47.234-.907.585-1.327.919-.554.434-1.109.87-1.63 1.354a5.058 5.058 0 00-.588.618c-.084.117-.017.217.084.267-.37.368-.74.736-1.109 1.12a.19.19 0 00-.05.134c0 .05.033.1.067.117l.655.502v.016c.924.92 2.554 2.173 4.285 3.527.251.201.52.402.773.602.117.134.234.285.335.418.05.066.169.084.236.033.033.034.084.067.118.1a.24.24 0 00.1.034.153.153 0 00.135-.066.237.237 0 00.033-.1c.017 0 .017.016.034.016a.192.192 0 00.134-.05l3.058-3.327c.12-.116.014-.267 0-.267zm-7.628-.134l-1.546-1.17-.15-.1c-.035-.017-.068-.05-.102-.067l-.117-.1c.66-.66 1.33-1.308 2-1.956-.488.484-1.463 1.906-1.261 2.373.002 0 .018.042.067.084l1.11.936zm4.1 3.126l-1.277-.97a26.906 26.906 0 00-1.58-1.504c.69.518 1.277.97 1.361 1.053.673.585.638.485 1.093.87l.554.4c-.074.103-.151.148-.151.151zm.336.25l-.034-.016a.913.913 0 00.152-.117zM.587 3.476c.034.217.085.435.118.636.201 1.103.403 2.106.772 2.858l.152.568c.05.217.134.485.219.552a66.769 66.769 0 003.578 2.942.177.177 0 00.219 0s0 .016.016.016a.153.153 0 00.118.05.191.191 0 00.134-.05c1.798-1.989 3.142-3.627 4.1-4.998.068-.066.084-.167.084-.25.067-.067.118-.151.185-.201.067-.067.067-.184 0-.235l-.017-.016c0-.033-.017-.084-.05-.1-.42-.401-.722-.685-1.042-.986a93.555 93.555 0 01-2.352-2.273c-.017-.017-.034-.034-.067-.034-.336-.117-1.025-.234-1.882-.385-1.277-.216-3.008-.517-4.57-.986 0 0-.101 0-.118.017l-.05.05C.05.714.022.707 0 .718c.017.1.017.167.05.284 0 .033.068.301.068.334zm7.191 4.78l-.033.034a.036.036 0 01.033-.034zM6.553 2.238c.101.1.521.502.622.585-.437-.2-1.529-.702-2.034-.869.505.1 1.194.201 1.412.284zM.79 1.403c.252.434.454 1.939.655 3.41-.118-.469-.201-.936-.302-1.372C.992 2.673.84 1.988.638 1.386c.124 0 .152.021.152.017zm-.286-.369c0-.016 0-.033-.017-.033.085 0 .135.017.202.05 0 .006-.145-.017-.185-.017zm23.17-.217c.017-.066-.336-.367-.219-.384.253-.017.253-.401 0-.401-.335.017-.688.1-1.008.15-.587.117-1.192.234-1.78.367a79.696 79.696 0 00-3.949.937c-.403.117-.857.2-1.243.401-.135.067-.118.2-.05.284-.034.017-.051.017-.085.034-.117.017-.218.034-.335.05-.102.017-.152.1-.135.2 0 .017.017.05.017.067-.706.936-1.496 1.923-2.353 2.976-.84.969-1.73 1.989-2.62 3.042-2.84 3.31-6.05 7.07-9.594 10.38a.161.161 0 000 .234c.016.016.033.033.05.033-.05.05-.101.085-.152.134-.033.034-.05.067-.05.1a.364.364 0 00-.067.084c-.067.067-.067.184.017.234.067.066.185.066.235-.017.017-.017.017-.033.033-.033a.265.265 0 01.37 0c.202.217.404.435.588.618l-.42-.35c-.067-.067-.184-.05-.234.016-.068.066-.051.184.016.234l4.469 3.727c.034.034.067.034.118.034a.15.15 0 00.117-.05l.101-.1c.017.016.05.016.067.016.05 0 .084-.016.118-.05 6.049-6.05 10.922-10.614 16.5-14.693.05-.033.067-.1.067-.15.067 0 .118-.05.15-.117 1.026-3.125 1.228-5.9 1.295-7.27 0-.059.016-.038.016-.068.017-.033.017-.05.017-.05a.978.978 0 00-.067-.619zm-10.82 4.915c.268-.301.537-.619.806-.903-1.73 2.273-4.603 5.766-8.67 9.929 2.773-3.059 5.562-6.218 7.864-9.026zM5.14 23.466c-.016-.017-.016-.017 0-.017zm2.504-2.156c.135-.15.27-.284.42-.434 0 0 0 .016.017.016-.224.198-.433.418-.437.418zm.69-.668c.099-.1.14-.173.284-.318.992-1.02 2.017-2.04 3.059-3.076l.016-.016c.252-.2.555-.418.824-.619a228.063 228.063 0 00-4.184 4.029zM14.852 3.91c-.554.719-1.176 1.671-1.697 2.423-1.646 2.374-6.94 8.174-7.057 8.274a1189.647 1189.647 0 01-4.839 4.597l-.1.1c-.085-.1-.085-.25.016-.334C8.652 11.966 13.19 6.133 15.021 3.576c-.05.116-.084.216-.168.334zm2.906 3.427c-.671-.386-.99-.987-.806-1.572l.05-.2a.775.775 0 01.085-.167 1.9 1.9 0 01.756-.703c.016 0 .033 0 .05-.016-.017-.034-.017-.084-.017-.134.017-.1.085-.167.202-.167.202 0 .824.184 1.059.384.067.05.134.117.202.184.084.1.218.268.285.401.034.017.067.184.118.268.033.134.067.284.05.418-.017.016 0 .116-.017.116a1.605 1.605 0 01-.218.619c-.03.03.006.012-.05.067a1.22 1.22 0 01-.32.334 1.49 1.49 0 01-1.26.234 2.191 2.191 0 00-.169-.066zm4.37 1.403c0 .017-.017.05 0 .067-.034 0-.05.017-.085.034a109.886 109.886 0 00-3.915 3.025c1.11-.986 2.218-1.989 3.378-2.975.336-.301.571-.686.638-1.12l.168-1.003v-.033c.085-.201.404-.118.353.1-.004-.001-.173.795-.537 1.905z"></path> | ||
</svg> | ||
); | ||
} | ||
|
||
export default IconExcalidraw; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { rem } from "@mantine/core"; | ||
|
||
interface Props { | ||
size?: number | string; | ||
} | ||
|
||
function IconMermaid({ size }: Props) { | ||
return ( | ||
<svg | ||
xmlns="http://www.w3.org/2000/svg" | ||
fill="#FF3670" | ||
viewBox="0 0 24 24" | ||
style={{ width: rem(size), height: rem(size) }} | ||
> | ||
<path d="M23.99 2.115A12.223 12.223 0 0012 10.149 12.223 12.223 0 00.01 2.115a12.23 12.23 0 005.32 10.604 6.562 6.562 0 012.845 5.423v3.754h7.65v-3.754a6.561 6.561 0 012.844-5.423 12.223 12.223 0 005.32-10.604z"></path> | ||
</svg> | ||
); | ||
} | ||
|
||
export default IconMermaid; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
82 changes: 82 additions & 0 deletions
82
apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { | ||
BubbleMenu as BaseBubbleMenu, | ||
findParentNode, | ||
posToDOMRect, | ||
} from '@tiptap/react'; | ||
import { useCallback } from 'react'; | ||
import { sticky } from 'tippy.js'; | ||
import { Node as PMNode } from 'prosemirror-model'; | ||
import { | ||
EditorMenuProps, | ||
ShouldShowProps, | ||
} from '@/features/editor/components/table/types/types.ts'; | ||
import { NodeWidthResize } from '@/features/editor/components/common/node-width-resize.tsx'; | ||
|
||
export function ExcalidrawMenu({ editor }: EditorMenuProps) { | ||
const shouldShow = useCallback( | ||
({ state }: ShouldShowProps) => { | ||
if (!state) { | ||
return false; | ||
} | ||
|
||
return editor.isActive('excalidraw') && editor.getAttributes('excalidraw')?.src; | ||
}, | ||
[editor] | ||
); | ||
|
||
const getReferenceClientRect = useCallback(() => { | ||
const { selection } = editor.state; | ||
const predicate = (node: PMNode) => node.type.name === 'excalidraw'; | ||
const parent = findParentNode(predicate)(selection); | ||
|
||
if (parent) { | ||
const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement; | ||
return dom.getBoundingClientRect(); | ||
} | ||
|
||
return posToDOMRect(editor.view, selection.from, selection.to); | ||
}, [editor]); | ||
|
||
const onWidthChange = useCallback( | ||
(value: number) => { | ||
editor.commands.updateAttributes('excalidraw', { width: `${value}%` }); | ||
}, | ||
[editor] | ||
); | ||
|
||
return ( | ||
<BaseBubbleMenu | ||
editor={editor} | ||
pluginKey={`excalidraw-menu}`} | ||
updateDelay={0} | ||
tippyOptions={{ | ||
getReferenceClientRect, | ||
offset: [0, 8], | ||
zIndex: 99, | ||
popperOptions: { | ||
modifiers: [{ name: 'flip', enabled: false }], | ||
}, | ||
plugins: [sticky], | ||
sticky: 'popper', | ||
}} | ||
shouldShow={shouldShow} | ||
> | ||
<div | ||
style={{ | ||
display: 'flex', | ||
flexDirection: 'column', | ||
alignItems: 'center', | ||
}} | ||
> | ||
{editor.getAttributes('excalidraw')?.width && ( | ||
<NodeWidthResize | ||
onChange={onWidthChange} | ||
value={parseInt(editor.getAttributes('excalidraw').width)} | ||
/> | ||
)} | ||
</div> | ||
</BaseBubbleMenu> | ||
); | ||
} | ||
|
||
export default ExcalidrawMenu; |
195 changes: 195 additions & 0 deletions
195
apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
import { NodeViewProps, NodeViewWrapper } from '@tiptap/react'; | ||
import { | ||
ActionIcon, | ||
Button, | ||
Card, | ||
Group, | ||
Image, | ||
Text, | ||
useComputedColorScheme, | ||
} from '@mantine/core'; | ||
import { useState } from 'react'; | ||
import { Excalidraw, exportToSvg, loadFromBlob } from '@excalidraw/excalidraw'; | ||
import { uploadFile } from '@/features/page/services/page-service.ts'; | ||
import { svgStringToFile } from '@/lib'; | ||
import { useDisclosure } from '@mantine/hooks'; | ||
import { getFileUrl } from '@/lib/config.ts'; | ||
import { ExcalidrawImperativeAPI } from '@excalidraw/excalidraw/types/types'; | ||
import { IAttachment } from '@/lib/types'; | ||
import ReactClearModal from 'react-clear-modal'; | ||
import clsx from 'clsx'; | ||
import { IconEdit } from '@tabler/icons-react'; | ||
|
||
export default function ExcalidrawView(props: NodeViewProps) { | ||
const { node, updateAttributes, editor, selected } = props; | ||
const { src, title, width, attachmentId } = node.attrs; | ||
|
||
const [excalidrawAPI, setExcalidrawAPI] = | ||
useState<ExcalidrawImperativeAPI>(null); | ||
const [excalidrawData, setExcalidrawData] = useState<any>(null); | ||
const [opened, { open, close }] = useDisclosure(false); | ||
const computedColorScheme = useComputedColorScheme(); | ||
|
||
const handleOpen = async () => { | ||
if (!editor.isEditable) { | ||
return; | ||
} | ||
|
||
try { | ||
let data = null; | ||
if (src) { | ||
const url = getFileUrl(src); | ||
const request = await fetch(url, { credentials: 'include' }); | ||
|
||
data = await loadFromBlob(await request.blob(), null, null); | ||
} | ||
|
||
setExcalidrawData(data); | ||
} catch (err) { | ||
console.error(err); | ||
} finally { | ||
open(); | ||
} | ||
}; | ||
|
||
const handleSave = async () => { | ||
if (!excalidrawAPI) { | ||
return; | ||
} | ||
|
||
const svg = await exportToSvg({ | ||
elements: excalidrawAPI?.getSceneElements(), | ||
appState: { | ||
exportEmbedScene: true, | ||
exportWithDarkMode: computedColorScheme == 'light' ? false : true, | ||
}, | ||
files: excalidrawAPI?.getFiles(), | ||
}); | ||
|
||
const serializer = new XMLSerializer(); | ||
let svgString = serializer.serializeToString(svg); | ||
|
||
svgString = svgString.replace(/https:\/\/unpkg\.com\/@excalidraw\/excalidraw@undefined/g, 'https://unpkg.com/@excalidraw/excalidraw@latest'); | ||
|
||
const fileName = 'diagram.excalidraw.svg'; | ||
const excalidrawSvgFile = await svgStringToFile(svgString, fileName); | ||
|
||
const pageId = editor.storage?.pageId; | ||
|
||
let attachment: IAttachment = null; | ||
if (attachmentId) { | ||
attachment = await uploadFile(excalidrawSvgFile, pageId, attachmentId); | ||
} else { | ||
attachment = await uploadFile(excalidrawSvgFile, pageId); | ||
} | ||
|
||
updateAttributes({ | ||
src: `/files/${attachment.id}/${attachment.fileName}?t=${new Date(attachment.updatedAt).getTime()}`, | ||
title: attachment.fileName, | ||
size: attachment.fileSize, | ||
attachmentId: attachment.id, | ||
}); | ||
|
||
close(); | ||
}; | ||
|
||
return ( | ||
<NodeViewWrapper> | ||
<ReactClearModal | ||
style={{ | ||
backgroundColor: 'rgba(0, 0, 0, 0.5)', | ||
padding: 0, | ||
zIndex: 200, | ||
}} | ||
isOpen={opened} | ||
onRequestClose={close} | ||
disableCloseOnBgClick={true} | ||
contentProps={{ | ||
style: { | ||
padding: 0, | ||
width: '90vw', | ||
}, | ||
}} | ||
> | ||
<Group | ||
justify="flex-end" | ||
wrap="nowrap" | ||
bg="var(--mantine-color-body)" | ||
p="xs" | ||
> | ||
<Button onClick={handleSave} size={'compact-sm'}> | ||
Save & Exit | ||
</Button> | ||
<Button onClick={close} color="red" size={'compact-sm'}> | ||
Exit | ||
</Button> | ||
</Group> | ||
<div style={{ height: '90vh' }}> | ||
<Excalidraw | ||
excalidrawAPI={(api) => setExcalidrawAPI(api)} | ||
initialData={{ | ||
...excalidrawData, | ||
scrollToContent: true, | ||
}} | ||
/> | ||
</div> | ||
</ReactClearModal> | ||
|
||
{src ? ( | ||
<div style={{ position: 'relative' }}> | ||
<Image | ||
onClick={(e) => e.detail === 2 && handleOpen()} | ||
radius="md" | ||
fit="contain" | ||
w={width} | ||
src={getFileUrl(src)} | ||
alt={title} | ||
className={clsx( | ||
selected ? 'ProseMirror-selectednode' : '', | ||
'alignCenter' | ||
)} | ||
/> | ||
|
||
{selected && ( | ||
<ActionIcon | ||
onClick={handleOpen} | ||
variant="default" | ||
color="gray" | ||
mx="xs" | ||
style={{ | ||
position: 'absolute', | ||
top: 8, | ||
right: 8, | ||
}} | ||
> | ||
<IconEdit size={18} /> | ||
</ActionIcon> | ||
)} | ||
</div> | ||
) : ( | ||
<Card | ||
radius="md" | ||
onClick={(e) => e.detail === 2 && handleOpen()} | ||
p="xs" | ||
style={{ | ||
display: 'flex', | ||
justifyContent: 'center', | ||
alignItems: 'center', | ||
}} | ||
withBorder | ||
className={clsx(selected ? 'ProseMirror-selectednode' : '')} | ||
> | ||
<div style={{ display: 'flex', alignItems: 'center' }}> | ||
<ActionIcon variant="transparent" color="gray"> | ||
<IconEdit size={18} /> | ||
</ActionIcon> | ||
|
||
<Text component="span" size="lg" c="dimmed"> | ||
Double-click to edit excalidraw diagram | ||
</Text> | ||
</div> | ||
</Card> | ||
)} | ||
</NodeViewWrapper> | ||
); | ||
} |
Oops, something went wrong.