| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063 |
- import { nanoid } from "nanoid";
- import React, {
- useEffect,
- useState,
- useRef,
- useCallback,
- Children,
- cloneElement,
- } from "react";
- import type * as TExcalidraw from "@excalidraw/excalidraw";
- import type { ImportedLibraryData } from "@excalidraw/excalidraw/data/types";
- import type {
- NonDeletedExcalidrawElement,
- Theme,
- } from "@excalidraw/excalidraw/element/types";
- import type {
- AppState,
- BinaryFileData,
- ExcalidrawImperativeAPI,
- ExcalidrawInitialDataState,
- Gesture,
- LibraryItems,
- PointerDownState as ExcalidrawPointerDownState,
- } from "@excalidraw/excalidraw/types";
- import initialData from "../initialData";
- import {
- resolvablePromise,
- distance2d,
- fileOpen,
- withBatchedUpdates,
- withBatchedUpdatesThrottled,
- } from "../utils";
- import CustomFooter from "./CustomFooter";
- import MobileFooter from "./MobileFooter";
- import ExampleSidebar from "./sidebar/ExampleSidebar";
- import "./ExampleApp.scss";
- import type { ResolvablePromise } from "../utils";
- 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 [renderScrollbars, setRenderScrollbars] = 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,
- renderScrollbars,
- 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.Sub>
- <MainMenu.Sub.Trigger
- title="Custom trigger"
- icon={
- <svg
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- strokeWidth={1.5}
- stroke="currentColor"
- className="w-6 h-6"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- d="M15.042 21.672L13.684 16.6m0 0l-2.51 2.225.569-9.47 5.227 7.917-3.286-.672zm-7.518-.267A8.25 8.25 0 1120.25 10.5M8.288 14.212A5.25 5.25 0 1117.25 10.5"
- />
- </svg>
- }
- >
- Submenu trigger
- </MainMenu.Sub.Trigger>
- <MainMenu.Sub.Content>
- <MainMenu.Sub.Item
- icon={
- <svg
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- strokeWidth={1.5}
- stroke="currentColor"
- className="w-6 h-6"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- d="M12 7.5h1.5m-1.5 3h1.5m-7.5 3h7.5m-7.5 3h7.5m3-9h3.375c.621 0 1.125.504 1.125 1.125V18a2.25 2.25 0 01-2.25 2.25M16.5 7.5V18a2.25 2.25 0 002.25 2.25M16.5 7.5V4.875c0-.621-.504-1.125-1.125-1.125H4.125C3.504 3.75 3 4.254 3 4.875V18a2.25 2.25 0 002.25 2.25h13.5M6 7.5h3v3H6v-3z"
- />
- </svg>
- }
- onSelect={() => window.alert("You clicked on sub item")}
- >
- Sub item
- </MainMenu.Sub.Item>
- </MainMenu.Sub.Content>
- </MainMenu.Sub>
- <MainMenu.DefaultItems.SaveAsImage />
- <MainMenu.DefaultItems.Export />
- <MainMenu.Separator />
- <MainMenu.DefaultItems.LiveCollaborationTrigger
- isCollaborating={isCollaborating}
- onSelect={() => window.alert("You clicked on collab button")}
- />
- <MainMenu.Sub>
- <MainMenu.Sub.Trigger>Trigger</MainMenu.Sub.Trigger>
- <MainMenu.Sub.Content>
- <MainMenu.Sub.Item
- onSelect={() => window.alert("You clicked on sub item")}
- >
- Sub item
- </MainMenu.Sub.Item>
- </MainMenu.Sub.Content>
- </MainMenu.Sub>
- <MainMenu.Group title="Excalidraw links">
- <MainMenu.DefaultItems.Socials />
- </MainMenu.Group>
- {/* <MainMenu.Separator /> */}
- <MainMenu.Sub>
- <MainMenu.Sub.Trigger className="custom-classname">
- Another submenu trigger
- </MainMenu.Sub.Trigger>
- <MainMenu.Sub.Content className="custom-classname-for-content">
- <MainMenu.Sub.Item
- title="Sub item"
- onSelect={() => window.alert("You clicked on sub item")}
- >
- Sub item
- </MainMenu.Sub.Item>
- </MainMenu.Sub.Content>
- </MainMenu.Sub>
- <MainMenu.Sub>
- <MainMenu.Sub.Trigger>Trigger me</MainMenu.Sub.Trigger>
- <MainMenu.Sub.Content>
- <MainMenu.Sub>
- <MainMenu.Sub.Trigger>Trigger me inside</MainMenu.Sub.Trigger>
- <MainMenu.Sub.Content>
- <MainMenu.Sub.Item
- onSelect={() => {
- alert("wow, nested submenus!");
- }}
- >
- Item wow
- </MainMenu.Sub.Item>
- </MainMenu.Sub.Content>
- </MainMenu.Sub>
- <MainMenu.Sub.Item
- onSelect={() => {
- alert("wow, nested submenus! very cool");
- }}
- >
- Another one
- </MainMenu.Sub.Item>
- </MainMenu.Sub.Content>
- </MainMenu.Sub>
- <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={renderScrollbars}
- onChange={() => setRenderScrollbars(!renderScrollbars)}
- />
- Render scrollbars
- </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>
- );
- }
|