| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961 | import React, {  useEffect,  useState,  useRef,  useCallback,  Children,  cloneElement,} from "react";import ExampleSidebar from "./sidebar/ExampleSidebar";import type * as TExcalidraw from "@excalidraw/excalidraw";import { nanoid } from "nanoid";import type { ResolvablePromise } from "../utils";import {  resolvablePromise,  distance2d,  fileOpen,  withBatchedUpdates,  withBatchedUpdatesThrottled,} from "../utils";import CustomFooter from "./CustomFooter";import MobileFooter from "./MobileFooter";import initialData from "../initialData";import type {  AppState,  BinaryFileData,  ExcalidrawImperativeAPI,  ExcalidrawInitialDataState,  Gesture,  LibraryItems,  PointerDownState as ExcalidrawPointerDownState,} from "@excalidraw/excalidraw/types";import type {  NonDeletedExcalidrawElement,  Theme,} from "@excalidraw/excalidraw/element/types";import type { ImportedLibraryData } from "@excalidraw/excalidraw/data/types";import "./ExampleApp.scss";type Comment = {  x: number;  y: number;  value: string;  id?: string;};type PointerDownState = {  x: number;  y: number;  hitElement: Comment;  onMove: any;  onUp: any;  hitElementOffsets: {    x: number;    y: number;  };};const COMMENT_ICON_DIMENSION = 32;const COMMENT_INPUT_HEIGHT = 50;const COMMENT_INPUT_WIDTH = 150;export interface AppProps {  appTitle: string;  useCustom: (api: ExcalidrawImperativeAPI | null, customArgs?: any[]) => void;  customArgs?: any[];  children: React.ReactNode;  excalidrawLib: typeof TExcalidraw;}export default function ExampleApp({  appTitle,  useCustom,  customArgs,  children,  excalidrawLib,}: AppProps) {  const {    exportToCanvas,    exportToSvg,    exportToBlob,    exportToClipboard,    useHandleLibrary,    MIME_TYPES,    sceneCoordsToViewportCoords,    viewportCoordsToSceneCoords,    restoreElements,    Sidebar,    Footer,    WelcomeScreen,    MainMenu,    LiveCollaborationTrigger,    convertToExcalidrawElements,    TTDDialog,    TTDDialogTrigger,    ROUNDNESS,    loadSceneOrLibraryFromBlob,  } = excalidrawLib;  const appRef = useRef<any>(null);  const [viewModeEnabled, setViewModeEnabled] = useState(false);  const [zenModeEnabled, setZenModeEnabled] = useState(false);  const [gridModeEnabled, setGridModeEnabled] = useState(false);  const [blobUrl, setBlobUrl] = useState<string>("");  const [canvasUrl, setCanvasUrl] = useState<string>("");  const [exportWithDarkMode, setExportWithDarkMode] = useState(false);  const [exportEmbedScene, setExportEmbedScene] = useState(false);  const [theme, setTheme] = useState<Theme>("light");  const [disableImageTool, setDisableImageTool] = useState(false);  const [isCollaborating, setIsCollaborating] = useState(false);  const [commentIcons, setCommentIcons] = useState<{ [id: string]: Comment }>(    {},  );  const [comment, setComment] = useState<Comment | null>(null);  const initialStatePromiseRef = useRef<{    promise: ResolvablePromise<ExcalidrawInitialDataState | null>;  }>({ promise: null! });  if (!initialStatePromiseRef.current.promise) {    initialStatePromiseRef.current.promise =      resolvablePromise<ExcalidrawInitialDataState | null>();  }  const [excalidrawAPI, setExcalidrawAPI] =    useState<ExcalidrawImperativeAPI | null>(null);  useCustom(excalidrawAPI, customArgs);  useHandleLibrary({ excalidrawAPI });  useEffect(() => {    if (!excalidrawAPI) {      return;    }    const fetchData = async () => {      const res = await fetch("/images/rocket.jpeg");      const imageData = await res.blob();      const reader = new FileReader();      reader.readAsDataURL(imageData);      reader.onload = function () {        const imagesArray: BinaryFileData[] = [          {            id: "rocket" as BinaryFileData["id"],            dataURL: reader.result as BinaryFileData["dataURL"],            mimeType: MIME_TYPES.jpg,            created: 1644915140367,            lastRetrieved: 1644915140367,          },        ];        //@ts-ignore        initialStatePromiseRef.current.promise.resolve({          ...initialData,          elements: convertToExcalidrawElements(initialData.elements),        });        excalidrawAPI.addFiles(imagesArray);      };    };    fetchData();  }, [excalidrawAPI, convertToExcalidrawElements, MIME_TYPES]);  const renderExcalidraw = (children: React.ReactNode) => {    const Excalidraw: any = Children.toArray(children).find(      (child) =>        React.isValidElement(child) &&        typeof child.type !== "string" &&        //@ts-ignore        child.type.displayName === "Excalidraw",    );    if (!Excalidraw) {      return;    }    const newElement = cloneElement(      Excalidraw,      {        excalidrawAPI: (api: ExcalidrawImperativeAPI) => setExcalidrawAPI(api),        initialData: initialStatePromiseRef.current.promise,        onChange: (          elements: NonDeletedExcalidrawElement[],          state: AppState,        ) => {          console.info("Elements :", elements, "State : ", state);        },        onPointerUpdate: (payload: {          pointer: { x: number; y: number };          button: "down" | "up";          pointersMap: Gesture["pointers"];        }) => setPointerData(payload),        viewModeEnabled,        zenModeEnabled,        gridModeEnabled,        theme,        name: "Custom name of drawing",        UIOptions: {          canvasActions: {            loadScene: false,          },          tools: { image: !disableImageTool },        },        renderTopRightUI,        onLinkOpen,        onPointerDown,        onScrollChange: rerenderCommentIcons,        validateEmbeddable: true,      },      <>        {excalidrawAPI && (          <Footer>            <CustomFooter              excalidrawAPI={excalidrawAPI}              excalidrawLib={excalidrawLib}            />          </Footer>        )}        <WelcomeScreen />        <Sidebar name="custom">          <Sidebar.Tabs>            <Sidebar.Header />            <Sidebar.Tab tab="one">Tab one!</Sidebar.Tab>            <Sidebar.Tab tab="two">Tab two!</Sidebar.Tab>            <Sidebar.TabTriggers>              <Sidebar.TabTrigger tab="one">One</Sidebar.TabTrigger>              <Sidebar.TabTrigger tab="two">Two</Sidebar.TabTrigger>            </Sidebar.TabTriggers>          </Sidebar.Tabs>        </Sidebar>        <Sidebar.Trigger          name="custom"          tab="one"          style={{            position: "absolute",            left: "50%",            transform: "translateX(-50%)",            bottom: "20px",            zIndex: 9999999999999999,          }}        >          Toggle Custom Sidebar        </Sidebar.Trigger>        {renderMenu()}        {excalidrawAPI && (          <TTDDialogTrigger icon={<span>😀</span>}>            Text to diagram          </TTDDialogTrigger>        )}        <TTDDialog          onTextSubmit={async (_) => {            console.info("submit");            // sleep for 2s            await new Promise((resolve) => setTimeout(resolve, 2000));            throw new Error("error, go away now");            // return "dummy";          }}        />      </>,    );    return newElement;  };  const renderTopRightUI = (isMobile: boolean) => {    return (      <>        {!isMobile && (          <LiveCollaborationTrigger            isCollaborating={isCollaborating}            onSelect={() => {              window.alert("Collab dialog clicked");            }}          />        )}        <button          onClick={() => alert("This is an empty top right UI")}          style={{ height: "2.5rem" }}        >          Click me        </button>      </>    );  };  const loadSceneOrLibrary = async () => {    const file = await fileOpen({ description: "Excalidraw or library file" });    const contents = await loadSceneOrLibraryFromBlob(file, null, null);    if (contents.type === MIME_TYPES.excalidraw) {      excalidrawAPI?.updateScene(contents.data as any);    } else if (contents.type === MIME_TYPES.excalidrawlib) {      excalidrawAPI?.updateLibrary({        libraryItems: (contents.data as ImportedLibraryData).libraryItems!,        openLibraryMenu: true,      });    }  };  const updateScene = () => {    const sceneData = {      elements: restoreElements(        convertToExcalidrawElements([          {            type: "rectangle",            id: "rect-1",            fillStyle: "hachure",            strokeWidth: 1,            strokeStyle: "solid",            roughness: 1,            angle: 0,            x: 100.50390625,            y: 93.67578125,            strokeColor: "#c92a2a",            width: 186.47265625,            height: 141.9765625,            seed: 1968410350,            roundness: {              type: ROUNDNESS.ADAPTIVE_RADIUS,              value: 32,            },          },          {            type: "arrow",            x: 300,            y: 150,            start: { id: "rect-1" },            end: { type: "ellipse" },          },          {            type: "text",            x: 300,            y: 100,            text: "HELLO WORLD!",          },        ]),        null,      ),      appState: {        viewBackgroundColor: "#edf2ff",      },    };    excalidrawAPI?.updateScene(sceneData);  };  const onLinkOpen = useCallback(    (      element: NonDeletedExcalidrawElement,      event: CustomEvent<{        nativeEvent: MouseEvent | React.PointerEvent<HTMLCanvasElement>;      }>,    ) => {      const link = element.link!;      const { nativeEvent } = event.detail;      const isNewTab = nativeEvent.ctrlKey || nativeEvent.metaKey;      const isNewWindow = nativeEvent.shiftKey;      const isInternalLink =        link.startsWith("/") || link.includes(window.location.origin);      if (isInternalLink && !isNewTab && !isNewWindow) {        // signal that we're handling the redirect ourselves        event.preventDefault();        // do a custom redirect, such as passing to react-router        // ...      }    },    [],  );  const onCopy = async (type: "png" | "svg" | "json") => {    if (!excalidrawAPI) {      return false;    }    await exportToClipboard({      elements: excalidrawAPI.getSceneElements(),      appState: excalidrawAPI.getAppState(),      files: excalidrawAPI.getFiles(),      type,    });    window.alert(`Copied to clipboard as ${type} successfully`);  };  const [pointerData, setPointerData] = useState<{    pointer: { x: number; y: number };    button: "down" | "up";    pointersMap: Gesture["pointers"];  } | null>(null);  const onPointerDown = (    activeTool: AppState["activeTool"],    pointerDownState: ExcalidrawPointerDownState,  ) => {    if (activeTool.type === "custom" && activeTool.customType === "comment") {      const { x, y } = pointerDownState.origin;      setComment({ x, y, value: "" });    }  };  const rerenderCommentIcons = () => {    if (!excalidrawAPI) {      return false;    }    const commentIconsElements = appRef.current.querySelectorAll(      ".comment-icon",    ) as HTMLElement[];    commentIconsElements.forEach((ele) => {      const id = ele.id;      const appstate = excalidrawAPI.getAppState();      const { x, y } = sceneCoordsToViewportCoords(        { sceneX: commentIcons[id].x, sceneY: commentIcons[id].y },        appstate,      );      ele.style.left = `${        x - COMMENT_ICON_DIMENSION / 2 - appstate!.offsetLeft      }px`;      ele.style.top = `${        y - COMMENT_ICON_DIMENSION / 2 - appstate!.offsetTop      }px`;    });  };  const onPointerMoveFromPointerDownHandler = (    pointerDownState: PointerDownState,  ) => {    return withBatchedUpdatesThrottled((event) => {      if (!excalidrawAPI) {        return false;      }      const { x, y } = viewportCoordsToSceneCoords(        {          clientX: event.clientX - pointerDownState.hitElementOffsets.x,          clientY: event.clientY - pointerDownState.hitElementOffsets.y,        },        excalidrawAPI.getAppState(),      );      setCommentIcons({        ...commentIcons,        [pointerDownState.hitElement.id!]: {          ...commentIcons[pointerDownState.hitElement.id!],          x,          y,        },      });    });  };  const onPointerUpFromPointerDownHandler = (    pointerDownState: PointerDownState,  ) => {    return withBatchedUpdates((event) => {      window.removeEventListener("pointermove", pointerDownState.onMove);      window.removeEventListener("pointerup", pointerDownState.onUp);      excalidrawAPI?.setActiveTool({ type: "selection" });      const distance = distance2d(        pointerDownState.x,        pointerDownState.y,        event.clientX,        event.clientY,      );      if (distance === 0) {        if (!comment) {          setComment({            x: pointerDownState.hitElement.x + 60,            y: pointerDownState.hitElement.y,            value: pointerDownState.hitElement.value,            id: pointerDownState.hitElement.id,          });        } else {          setComment(null);        }      }    });  };  const renderCommentIcons = () => {    return Object.values(commentIcons).map((commentIcon) => {      if (!excalidrawAPI) {        return false;      }      const appState = excalidrawAPI.getAppState();      const { x, y } = sceneCoordsToViewportCoords(        { sceneX: commentIcon.x, sceneY: commentIcon.y },        excalidrawAPI.getAppState(),      );      return (        <div          id={commentIcon.id}          key={commentIcon.id}          style={{            top: `${y - COMMENT_ICON_DIMENSION / 2 - appState!.offsetTop}px`,            left: `${x - COMMENT_ICON_DIMENSION / 2 - appState!.offsetLeft}px`,            position: "absolute",            zIndex: 1,            width: `${COMMENT_ICON_DIMENSION}px`,            height: `${COMMENT_ICON_DIMENSION}px`,            cursor: "pointer",            touchAction: "none",          }}          className="comment-icon"          onPointerDown={(event) => {            event.preventDefault();            if (comment) {              commentIcon.value = comment.value;              saveComment();            }            const pointerDownState: any = {              x: event.clientX,              y: event.clientY,              hitElement: commentIcon,              hitElementOffsets: { x: event.clientX - x, y: event.clientY - y },            };            const onPointerMove =              onPointerMoveFromPointerDownHandler(pointerDownState);            const onPointerUp =              onPointerUpFromPointerDownHandler(pointerDownState);            window.addEventListener("pointermove", onPointerMove);            window.addEventListener("pointerup", onPointerUp);            pointerDownState.onMove = onPointerMove;            pointerDownState.onUp = onPointerUp;            excalidrawAPI?.setActiveTool({              type: "custom",              customType: "comment",            });          }}        >          <div className="comment-avatar">            <img src="images/doremon.png" alt="doremon" />          </div>        </div>      );    });  };  const saveComment = () => {    if (!comment) {      return;    }    if (!comment.id && !comment.value) {      setComment(null);      return;    }    const id = comment.id || nanoid();    setCommentIcons({      ...commentIcons,      [id]: {        x: comment.id ? comment.x - 60 : comment.x,        y: comment.y,        id,        value: comment.value,      },    });    setComment(null);  };  const renderComment = () => {    if (!comment) {      return null;    }    const appState = excalidrawAPI?.getAppState()!;    const { x, y } = sceneCoordsToViewportCoords(      { sceneX: comment.x, sceneY: comment.y },      appState,    );    let top = y - COMMENT_ICON_DIMENSION / 2 - appState.offsetTop;    let left = x - COMMENT_ICON_DIMENSION / 2 - appState.offsetLeft;    if (      top + COMMENT_INPUT_HEIGHT <      appState.offsetTop + COMMENT_INPUT_HEIGHT    ) {      top = COMMENT_ICON_DIMENSION / 2;    }    if (top + COMMENT_INPUT_HEIGHT > appState.height) {      top = appState.height - COMMENT_INPUT_HEIGHT - COMMENT_ICON_DIMENSION / 2;    }    if (      left + COMMENT_INPUT_WIDTH <      appState.offsetLeft + COMMENT_INPUT_WIDTH    ) {      left = COMMENT_ICON_DIMENSION / 2;    }    if (left + COMMENT_INPUT_WIDTH > appState.width) {      left = appState.width - COMMENT_INPUT_WIDTH - COMMENT_ICON_DIMENSION / 2;    }    return (      <textarea        className="comment"        style={{          top: `${top}px`,          left: `${left}px`,          position: "absolute",          zIndex: 1,          height: `${COMMENT_INPUT_HEIGHT}px`,          width: `${COMMENT_INPUT_WIDTH}px`,        }}        ref={(ref) => {          setTimeout(() => ref?.focus());        }}        placeholder={comment.value ? "Reply" : "Comment"}        value={comment.value}        onChange={(event) => {          setComment({ ...comment, value: event.target.value });        }}        onBlur={saveComment}        onKeyDown={(event) => {          if (!event.shiftKey && event.key === "Enter") {            event.preventDefault();            saveComment();          }        }}      />    );  };  const renderMenu = () => {    return (      <MainMenu>        <MainMenu.DefaultItems.SaveAsImage />        <MainMenu.DefaultItems.Export />        <MainMenu.Separator />        <MainMenu.DefaultItems.LiveCollaborationTrigger          isCollaborating={isCollaborating}          onSelect={() => window.alert("You clicked on collab button")}        />        <MainMenu.Group title="Excalidraw links">          <MainMenu.DefaultItems.Socials />        </MainMenu.Group>        <MainMenu.Separator />        <MainMenu.ItemCustom>          <button            style={{ height: "2rem" }}            onClick={() => window.alert("custom menu item")}          >            custom item          </button>        </MainMenu.ItemCustom>        <MainMenu.DefaultItems.Help />        {excalidrawAPI && (          <MobileFooter            excalidrawLib={excalidrawLib}            excalidrawAPI={excalidrawAPI}          />        )}      </MainMenu>    );  };  return (    <div className="App" ref={appRef}>      <h1>{appTitle}</h1>      {/* TODO fix type */}      <ExampleSidebar>        <div className="button-wrapper">          <button onClick={loadSceneOrLibrary}>Load Scene or Library</button>          <button className="update-scene" onClick={updateScene}>            Update Scene          </button>          <button            className="reset-scene"            onClick={() => {              excalidrawAPI?.resetScene();            }}          >            Reset Scene          </button>          <button            onClick={() => {              const libraryItems: LibraryItems = [                {                  status: "published",                  id: "1",                  created: 1,                  elements: initialData.libraryItems[1] as any,                },                {                  status: "unpublished",                  id: "2",                  created: 2,                  elements: initialData.libraryItems[1] as any,                },              ];              excalidrawAPI?.updateLibrary({                libraryItems,              });            }}          >            Update Library          </button>          <label>            <input              type="checkbox"              checked={viewModeEnabled}              onChange={() => setViewModeEnabled(!viewModeEnabled)}            />            View mode          </label>          <label>            <input              type="checkbox"              checked={zenModeEnabled}              onChange={() => setZenModeEnabled(!zenModeEnabled)}            />            Zen mode          </label>          <label>            <input              type="checkbox"              checked={gridModeEnabled}              onChange={() => setGridModeEnabled(!gridModeEnabled)}            />            Grid mode          </label>          <label>            <input              type="checkbox"              checked={theme === "dark"}              onChange={() => {                setTheme(theme === "light" ? "dark" : "light");              }}            />            Switch to Dark Theme          </label>          <label>            <input              type="checkbox"              checked={disableImageTool === true}              onChange={() => {                setDisableImageTool(!disableImageTool);              }}            />            Disable Image Tool          </label>          <label>            <input              type="checkbox"              checked={isCollaborating}              onChange={() => {                if (!isCollaborating) {                  const collaborators = new Map();                  collaborators.set("id1", {                    username: "Doremon",                    avatarUrl: "images/doremon.png",                  });                  collaborators.set("id2", {                    username: "Excalibot",                    avatarUrl: "images/excalibot.png",                  });                  collaborators.set("id3", {                    username: "Pika",                    avatarUrl: "images/pika.jpeg",                  });                  collaborators.set("id4", {                    username: "fallback",                    avatarUrl: "https://example.com",                  });                  excalidrawAPI?.updateScene({ collaborators });                } else {                  excalidrawAPI?.updateScene({                    collaborators: new Map(),                  });                }                setIsCollaborating(!isCollaborating);              }}            />            Show collaborators          </label>          <div>            <button onClick={onCopy.bind(null, "png")}>              Copy to Clipboard as PNG            </button>            <button onClick={onCopy.bind(null, "svg")}>              Copy to Clipboard as SVG            </button>            <button onClick={onCopy.bind(null, "json")}>              Copy to Clipboard as JSON            </button>          </div>          <div            style={{              display: "flex",              gap: "1em",              justifyContent: "center",              marginTop: "1em",            }}          >            <div>x: {pointerData?.pointer.x ?? 0}</div>            <div>y: {pointerData?.pointer.y ?? 0}</div>          </div>        </div>        <div className="excalidraw-wrapper">          {renderExcalidraw(children)}          {Object.keys(commentIcons || []).length > 0 && renderCommentIcons()}          {comment && renderComment()}        </div>        <div className="export-wrapper button-wrapper">          <label className="export-wrapper__checkbox">            <input              type="checkbox"              checked={exportWithDarkMode}              onChange={() => setExportWithDarkMode(!exportWithDarkMode)}            />            Export with dark mode          </label>          <label className="export-wrapper__checkbox">            <input              type="checkbox"              checked={exportEmbedScene}              onChange={() => setExportEmbedScene(!exportEmbedScene)}            />            Export with embed scene          </label>          <button            onClick={async () => {              if (!excalidrawAPI) {                return;              }              const svg = await exportToSvg({                elements: excalidrawAPI?.getSceneElements(),                appState: {                  ...initialData.appState,                  exportWithDarkMode,                  exportEmbedScene,                  width: 300,                  height: 100,                },                files: excalidrawAPI?.getFiles(),              });              appRef.current.querySelector(".export-svg").innerHTML =                svg.outerHTML;            }}          >            Export to SVG          </button>          <div className="export export-svg"></div>          <button            onClick={async () => {              if (!excalidrawAPI) {                return;              }              const blob = await exportToBlob({                elements: excalidrawAPI?.getSceneElements(),                mimeType: "image/png",                appState: {                  ...initialData.appState,                  exportEmbedScene,                  exportWithDarkMode,                },                files: excalidrawAPI?.getFiles(),              });              setBlobUrl(window.URL.createObjectURL(blob));            }}          >            Export to Blob          </button>          <div className="export export-blob">            <img src={blobUrl} alt="" />          </div>          <button            onClick={async () => {              if (!excalidrawAPI) {                return;              }              const canvas = await exportToCanvas({                elements: excalidrawAPI.getSceneElements(),                appState: {                  ...initialData.appState,                  exportWithDarkMode,                },                files: excalidrawAPI.getFiles(),              });              const ctx = canvas.getContext("2d")!;              ctx.font = "30px Excalifont";              ctx.strokeText("My custom text", 50, 60);              setCanvasUrl(canvas.toDataURL());            }}          >            Export to Canvas          </button>          <button            onClick={async () => {              if (!excalidrawAPI) {                return;              }              const canvas = await exportToCanvas({                elements: excalidrawAPI.getSceneElements(),                appState: {                  ...initialData.appState,                  exportWithDarkMode,                },                files: excalidrawAPI.getFiles(),              });              const ctx = canvas.getContext("2d")!;              ctx.font = "30px Excalifont";              ctx.strokeText("My custom text", 50, 60);              setCanvasUrl(canvas.toDataURL());            }}          >            Export to Canvas          </button>          <button            type="button"            onClick={() => {              if (!excalidrawAPI) {                return;              }              const elements = excalidrawAPI.getSceneElements();              excalidrawAPI.scrollToContent(elements[0], {                fitToViewport: true,              });            }}          >            Fit to viewport, first element          </button>          <button            type="button"            onClick={() => {              if (!excalidrawAPI) {                return;              }              const elements = excalidrawAPI.getSceneElements();              excalidrawAPI.scrollToContent(elements[0], {                fitToContent: true,              });              excalidrawAPI.scrollToContent(elements[0], {                fitToContent: true,              });            }}          >            Fit to content, first element          </button>          <button            type="button"            onClick={() => {              if (!excalidrawAPI) {                return;              }              const elements = excalidrawAPI.getSceneElements();              excalidrawAPI.scrollToContent(elements[0], {                fitToContent: true,              });              excalidrawAPI.scrollToContent(elements[0]);            }}          >            Scroll to first element, no fitToContent, no fitToViewport          </button>          <div className="export export-canvas">            <img src={canvasUrl} alt="" />          </div>        </div>      </ExampleSidebar>    </div>  );}
 |