mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
chore: release @excalidraw/excalidraw@18.0.0 🎉 (#9127)
This commit is contained in:
parent
392118bf26
commit
ecef5d12f4
232 changed files with 3412 additions and 2851 deletions
961
examples/with-script-in-browser/components/ExampleApp.tsx
Normal file
961
examples/with-script-in-browser/components/ExampleApp.tsx
Normal file
|
@ -0,0 +1,961 @@
|
|||
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>
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue