tiptap-excalidraw-extension v0.0.4
tiptap-excalidraw-extension
An extension for integrating Excalidraw into Tiptap editors, supporting integration with various UI libraries including Vue, but requires React as a dependency.
Features
- Basic Element support
- Supports Integration with Vue and Other UI Libraries
- Support external excalidraw data by passing uploadFn and downloadFn
- Collaboration support
- Image support
Screenshots
Installation
To install the tiptap-excalidraw-extension, you can use npm, yarn, or pnpm:
# npm
npm install tiptap-excalidraw-extension react react-dom
# yarn
yarn add tiptap-excalidraw-extension react react-dom
# pnpm
pnpm install tiptap-excalidraw-extension react react-dom
🚧🚧🚧Note: Since Excalidraw is built with React, if you are using Vue or any other UI library, you will still need to install react
and react-dom
as peer dependencies.
Usage
To integrate Excalidraw into a Tiptap editor, follow the example below:
import { EditorContent, useEditor } from '@tiptap/react'; // For React users
// import { useEditor, EditorContent } from '@tiptap/vue-3'; // For Vue users
import StarterKit from '@tiptap/starter-kit';
import { useCallback, useEffect, useState } from 'react';
import axios from 'axios';
import { ExcalidrawExtension } from 'tiptap-excalidraw-extension';
const DOC_LOCAL_STORATE_KEY = 'tiptapDocDataUrl';
const uploadFn = async (file: Blob | object, ext: 'png' | 'jpg' | 'webp' | 'json') => {
const formData = new FormData();
if (ext === 'json') {
const jsonBlob = new Blob([JSON.stringify(file)], { type: 'application/json' });
formData.append('file', jsonBlob, `data.${ext}`);
} else if (['png', 'jpg', 'webp'].includes(ext)) {
formData.append('file', file as Blob, `image.${ext}`);
} else {
throw new Error('Unsupported file type');
}
try {
const response = await axios.post(`http://localhost:3000/upload?ext=${ext}`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
return { dataUrl: response.data.url };
} catch (error) {
console.error('File upload failed:', error);
throw new Error('File upload failed');
}
};
const downloadFn = async (url: string) => {
try {
const response = await axios.get(url, { responseType: 'blob' });
const ext = url.split('.').pop();
if (ext === 'json') {
const text = await response.data.text();
return JSON.parse(text);
} else if (['png', 'jpg', 'webp'].includes(ext)) {
return URL.createObjectURL(response.data);
} else {
throw new Error('Unsupported file type');
}
} catch (error) {
console.error('File download failed:', error);
throw new Error('File download failed');
}
};
export default function App() {
const [loading, setLoading] = useState(false);
const editor = useEditor({
extensions: [
ExcalidrawExtension.configure({
extension: {
inline: false,
uploadFn,
downloadFn
}
}),
StarterKit
],
autofocus: true
});
const saveDocument = useCallback(async () => {
if (editor) {
const jsonContent = editor.getJSON();
try {
setLoading(true);
const result = await uploadFn(jsonContent, 'json');
const dataUrl = result.dataUrl;
console.log('Document saved with file ID:', dataUrl);
localStorage.setItem(DOC_LOCAL_STORATE_KEY, dataUrl);
} catch (error) {
console.error('Failed to save document:', error);
} finally {
setLoading(false);
}
}
}, [editor]);
const loadDocument = useCallback(
async (dataUrl: string) => {
try {
const jsonContent = await downloadFn(dataUrl);
editor?.commands.setContent(jsonContent);
} catch (error) {
console.error('Failed to load document:', error);
}
},
[editor]
);
useEffect(() => {
const storedDataUrl = localStorage.getItem(DOC_LOCAL_STORATE_KEY);
if (storedDataUrl) {
loadDocument(storedDataUrl);
}
}, [loadDocument]);
const insertExcalidraw = useCallback(() => {
if (editor) {
editor.chain().focus().addExcalidraw().run();
}
}, [editor]);
return (
<div className="container w-[80vw] h-screen mx-auto flex flex-col">
<h1 className="text-3xl text-center py-2">Tiptap Excalidraw Extension Demo</h1>
<div className=" absolute top-0 left-0">
<button className="block mb-1 border-2" onClick={insertExcalidraw}>
Insert Excalidraw
</button>
<button className="border-2 " onClick={saveDocument}>
{loading ? 'Saving Document...' : 'Save Document'}
</button>
</div>
<EditorContent className="rounded-lg flex-1 p-4 border border-rose-100" editor={editor} />
</div>
);
}
Styling
You can overwrite the existing class
.my-excalidraw-static,
.tiptap-excalidraw-static {
.tiptap-excalidraw-thumbnail-wrapper {
// write your own style
}
}
Development
yarn dev
# open a second terminal
cd examples/react-demo && yarn linkExtension
Acknowledgements
This project wouldn't be possible without the fantastic work of the following projects:
And if you find this project useful, a star on GitHub would be greatly appreciated. Thank you!
License
MIT License
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago