Eidos

Installation Notice

The installation may currently fail. We recommend copying the code below and creating the extension manually in Eidos.

Excalidraw

By: Mayne

Install Latest (v0.0.2)

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>
  );
}

Information

Author
Mayne
Type
block
Latest Version
0.0.2
Last Updated
08/27/2025
Published
06/06/2025

Version History

  • v0.0.2 08/27/2025
  • v0.0.1 06/06/2025