You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
962 lines
28 KiB
TypeScript
962 lines
28 KiB
TypeScript
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/dist/excalidraw/types";
|
|
import type {
|
|
NonDeletedExcalidrawElement,
|
|
Theme,
|
|
} from "@excalidraw/excalidraw/dist/excalidraw/element/types";
|
|
import type { ImportedLibraryData } from "@excalidraw/excalidraw/dist/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>
|
|
);
|
|
}
|