The installation may currently fail. We recommend copying the code below and creating the extension manually in Eidos.
By: Mayne
Excalidraw for Eidos
import { Excalidraw } from "@excalidraw/excalidraw";
import "@excalidraw/excalidraw/dist/prod/index.css";
import React, { useState, useEffect, useCallback } from "react";
import { useDebounceCallback } from "usehooks-ts";
(window as any).EXCALIDRAW_ASSET_PATH = "https://esm.sh/@excalidraw/excalidraw/dist/prod/";
export const meta = {
type: "extNode",
componentName: "WhiteboardApp",
extNode: {
title: "Whiteboard Extension Node",
description: "A custom whiteboard using Excalidraw",
type: "excalidraw",
},
};
export function WhiteboardApp() {
const nodeId = window.location.pathname.split('/')[1];
const [excalidrawData, setExcalidrawData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const getAppStateKey = (nodeId) => `excalidraw-appstate-${nodeId}`;
const loadInitialData = useCallback(async (nodeId) => {
if (!nodeId) return null;
try {
console.log(`Loading initial data for node: ${nodeId}`);
const savedText = await eidos.currentSpace.extNode.getText(nodeId);
let elements = [];
let files = {};
if (savedText) {
const parsedData = JSON.parse(savedText);
elements = parsedData?.elements || [];
files = parsedData?.files || {};
}
const appStateKey = getAppStateKey(nodeId);
let appState = {};
try {
const savedAppState = localStorage.getItem(appStateKey);
if (savedAppState) {
appState = JSON.parse(savedAppState);
}
} catch (error) {
console.warn('Failed to load appState from localStorage:', error);
}
return {
elements,
appState: {
...appState,
collaborators: [],
},
files
};
} catch (error) {
console.error('❌ Failed to load initial data:', error);
return {
elements: [],
appState: {},
files: {}
};
}
}, []);
const debouncedSaveToDatabase = useDebounceCallback(async (elements, files) => {
if (!nodeId) return;
try {
const dataToSave = JSON.stringify({ elements, files });
console.log(`Saving drawing data to eidos (${dataToSave.length} characters)...`);
await eidos.currentSpace.extNode.setText(nodeId, dataToSave);
} catch (error) {
console.error('❌ Failed to save data:', error);
}
}, 1000);
const saveAppStateToLocalStorage = useCallback((appState) => {
if (!nodeId) return;
try {
const appStateKey = getAppStateKey(nodeId);
const { collaborators, ...stateToSave } = appState;
localStorage.setItem(appStateKey, JSON.stringify(stateToSave));
} catch (error) {
console.error('❌ Failed to save appState to localStorage:', error);
}
}, [nodeId]);
useEffect(() => {
const initializeData = async () => {
if (nodeId) {
const data = await loadInitialData(nodeId);
setExcalidrawData(data);
setIsLoading(false);
}
};
initializeData();
}, [nodeId, loadInitialData]);
const handleChange = useCallback((elements, appState, files) => {
saveAppStateToLocalStorage(appState);
debouncedSaveToDatabase(elements, files);
}, [saveAppStateToLocalStorage, debouncedSaveToDatabase]);
if (!nodeId) {
return (
<div className="p-4">
This Block only works as Ext Node handler
</div>
);
}
if (isLoading || !excalidrawData) {
return (
<div className="w-full h-screen flex items-center justify-center font-sans">
Loading whiteboard...
</div>
);
}
return (
<div className="w-full h-screen flex flex-col font-sans">
<Excalidraw
initialData={excalidrawData}
onChange={handleChange}
/>
</div>
);
}