ExampleApp.tsx 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063
  1. import { nanoid } from "nanoid";
  2. import React, {
  3. useEffect,
  4. useState,
  5. useRef,
  6. useCallback,
  7. Children,
  8. cloneElement,
  9. } from "react";
  10. import type * as TExcalidraw from "@excalidraw/excalidraw";
  11. import type { ImportedLibraryData } from "@excalidraw/excalidraw/data/types";
  12. import type {
  13. NonDeletedExcalidrawElement,
  14. Theme,
  15. } from "@excalidraw/excalidraw/element/types";
  16. import type {
  17. AppState,
  18. BinaryFileData,
  19. ExcalidrawImperativeAPI,
  20. ExcalidrawInitialDataState,
  21. Gesture,
  22. LibraryItems,
  23. PointerDownState as ExcalidrawPointerDownState,
  24. } from "@excalidraw/excalidraw/types";
  25. import initialData from "../initialData";
  26. import {
  27. resolvablePromise,
  28. distance2d,
  29. fileOpen,
  30. withBatchedUpdates,
  31. withBatchedUpdatesThrottled,
  32. } from "../utils";
  33. import CustomFooter from "./CustomFooter";
  34. import MobileFooter from "./MobileFooter";
  35. import ExampleSidebar from "./sidebar/ExampleSidebar";
  36. import "./ExampleApp.scss";
  37. import type { ResolvablePromise } from "../utils";
  38. type Comment = {
  39. x: number;
  40. y: number;
  41. value: string;
  42. id?: string;
  43. };
  44. type PointerDownState = {
  45. x: number;
  46. y: number;
  47. hitElement: Comment;
  48. onMove: any;
  49. onUp: any;
  50. hitElementOffsets: {
  51. x: number;
  52. y: number;
  53. };
  54. };
  55. const COMMENT_ICON_DIMENSION = 32;
  56. const COMMENT_INPUT_HEIGHT = 50;
  57. const COMMENT_INPUT_WIDTH = 150;
  58. export interface AppProps {
  59. appTitle: string;
  60. useCustom: (api: ExcalidrawImperativeAPI | null, customArgs?: any[]) => void;
  61. customArgs?: any[];
  62. children: React.ReactNode;
  63. excalidrawLib: typeof TExcalidraw;
  64. }
  65. export default function ExampleApp({
  66. appTitle,
  67. useCustom,
  68. customArgs,
  69. children,
  70. excalidrawLib,
  71. }: AppProps) {
  72. const {
  73. exportToCanvas,
  74. exportToSvg,
  75. exportToBlob,
  76. exportToClipboard,
  77. useHandleLibrary,
  78. MIME_TYPES,
  79. sceneCoordsToViewportCoords,
  80. viewportCoordsToSceneCoords,
  81. restoreElements,
  82. Sidebar,
  83. Footer,
  84. WelcomeScreen,
  85. MainMenu,
  86. LiveCollaborationTrigger,
  87. convertToExcalidrawElements,
  88. TTDDialog,
  89. TTDDialogTrigger,
  90. ROUNDNESS,
  91. loadSceneOrLibraryFromBlob,
  92. } = excalidrawLib;
  93. const appRef = useRef<any>(null);
  94. const [viewModeEnabled, setViewModeEnabled] = useState(false);
  95. const [zenModeEnabled, setZenModeEnabled] = useState(false);
  96. const [gridModeEnabled, setGridModeEnabled] = useState(false);
  97. const [renderScrollbars, setRenderScrollbars] = useState(false);
  98. const [blobUrl, setBlobUrl] = useState<string>("");
  99. const [canvasUrl, setCanvasUrl] = useState<string>("");
  100. const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
  101. const [exportEmbedScene, setExportEmbedScene] = useState(false);
  102. const [theme, setTheme] = useState<Theme>("light");
  103. const [disableImageTool, setDisableImageTool] = useState(false);
  104. const [isCollaborating, setIsCollaborating] = useState(false);
  105. const [commentIcons, setCommentIcons] = useState<{ [id: string]: Comment }>(
  106. {},
  107. );
  108. const [comment, setComment] = useState<Comment | null>(null);
  109. const initialStatePromiseRef = useRef<{
  110. promise: ResolvablePromise<ExcalidrawInitialDataState | null>;
  111. }>({ promise: null! });
  112. if (!initialStatePromiseRef.current.promise) {
  113. initialStatePromiseRef.current.promise =
  114. resolvablePromise<ExcalidrawInitialDataState | null>();
  115. }
  116. const [excalidrawAPI, setExcalidrawAPI] =
  117. useState<ExcalidrawImperativeAPI | null>(null);
  118. useCustom(excalidrawAPI, customArgs);
  119. useHandleLibrary({ excalidrawAPI });
  120. useEffect(() => {
  121. if (!excalidrawAPI) {
  122. return;
  123. }
  124. const fetchData = async () => {
  125. const res = await fetch("/images/rocket.jpeg");
  126. const imageData = await res.blob();
  127. const reader = new FileReader();
  128. reader.readAsDataURL(imageData);
  129. reader.onload = function () {
  130. const imagesArray: BinaryFileData[] = [
  131. {
  132. id: "rocket" as BinaryFileData["id"],
  133. dataURL: reader.result as BinaryFileData["dataURL"],
  134. mimeType: MIME_TYPES.jpg,
  135. created: 1644915140367,
  136. lastRetrieved: 1644915140367,
  137. },
  138. ];
  139. //@ts-ignore
  140. initialStatePromiseRef.current.promise.resolve({
  141. ...initialData,
  142. elements: convertToExcalidrawElements(initialData.elements),
  143. });
  144. excalidrawAPI.addFiles(imagesArray);
  145. };
  146. };
  147. fetchData();
  148. }, [excalidrawAPI, convertToExcalidrawElements, MIME_TYPES]);
  149. const renderExcalidraw = (children: React.ReactNode) => {
  150. const Excalidraw: any = Children.toArray(children).find(
  151. (child) =>
  152. React.isValidElement(child) &&
  153. typeof child.type !== "string" &&
  154. //@ts-ignore
  155. child.type.displayName === "Excalidraw",
  156. );
  157. if (!Excalidraw) {
  158. return;
  159. }
  160. const newElement = cloneElement(
  161. Excalidraw,
  162. {
  163. excalidrawAPI: (api: ExcalidrawImperativeAPI) => setExcalidrawAPI(api),
  164. initialData: initialStatePromiseRef.current.promise,
  165. onChange: (
  166. elements: NonDeletedExcalidrawElement[],
  167. state: AppState,
  168. ) => {
  169. console.info("Elements :", elements, "State : ", state);
  170. },
  171. onPointerUpdate: (payload: {
  172. pointer: { x: number; y: number };
  173. button: "down" | "up";
  174. pointersMap: Gesture["pointers"];
  175. }) => setPointerData(payload),
  176. viewModeEnabled,
  177. zenModeEnabled,
  178. renderScrollbars,
  179. gridModeEnabled,
  180. theme,
  181. name: "Custom name of drawing",
  182. UIOptions: {
  183. canvasActions: {
  184. loadScene: false,
  185. },
  186. tools: { image: !disableImageTool },
  187. },
  188. renderTopRightUI,
  189. onLinkOpen,
  190. onPointerDown,
  191. onScrollChange: rerenderCommentIcons,
  192. validateEmbeddable: true,
  193. },
  194. <>
  195. {excalidrawAPI && (
  196. <Footer>
  197. <CustomFooter
  198. excalidrawAPI={excalidrawAPI}
  199. excalidrawLib={excalidrawLib}
  200. />
  201. </Footer>
  202. )}
  203. <WelcomeScreen />
  204. <Sidebar name="custom">
  205. <Sidebar.Tabs>
  206. <Sidebar.Header />
  207. <Sidebar.Tab tab="one">Tab one!</Sidebar.Tab>
  208. <Sidebar.Tab tab="two">Tab two!</Sidebar.Tab>
  209. <Sidebar.TabTriggers>
  210. <Sidebar.TabTrigger tab="one">One</Sidebar.TabTrigger>
  211. <Sidebar.TabTrigger tab="two">Two</Sidebar.TabTrigger>
  212. </Sidebar.TabTriggers>
  213. </Sidebar.Tabs>
  214. </Sidebar>
  215. <Sidebar.Trigger
  216. name="custom"
  217. tab="one"
  218. style={{
  219. position: "absolute",
  220. left: "50%",
  221. transform: "translateX(-50%)",
  222. bottom: "20px",
  223. zIndex: 9999999999999999,
  224. }}
  225. >
  226. Toggle Custom Sidebar
  227. </Sidebar.Trigger>
  228. {renderMenu()}
  229. {excalidrawAPI && (
  230. <TTDDialogTrigger icon={<span>😀</span>}>
  231. Text to diagram
  232. </TTDDialogTrigger>
  233. )}
  234. <TTDDialog
  235. onTextSubmit={async (_) => {
  236. console.info("submit");
  237. // sleep for 2s
  238. await new Promise((resolve) => setTimeout(resolve, 2000));
  239. throw new Error("error, go away now");
  240. // return "dummy";
  241. }}
  242. />
  243. </>,
  244. );
  245. return newElement;
  246. };
  247. const renderTopRightUI = (isMobile: boolean) => {
  248. return (
  249. <>
  250. {!isMobile && (
  251. <LiveCollaborationTrigger
  252. isCollaborating={isCollaborating}
  253. onSelect={() => {
  254. window.alert("Collab dialog clicked");
  255. }}
  256. />
  257. )}
  258. <button
  259. onClick={() => alert("This is an empty top right UI")}
  260. style={{ height: "2.5rem" }}
  261. >
  262. Click me
  263. </button>
  264. </>
  265. );
  266. };
  267. const loadSceneOrLibrary = async () => {
  268. const file = await fileOpen({ description: "Excalidraw or library file" });
  269. const contents = await loadSceneOrLibraryFromBlob(file, null, null);
  270. if (contents.type === MIME_TYPES.excalidraw) {
  271. excalidrawAPI?.updateScene(contents.data as any);
  272. } else if (contents.type === MIME_TYPES.excalidrawlib) {
  273. excalidrawAPI?.updateLibrary({
  274. libraryItems: (contents.data as ImportedLibraryData).libraryItems!,
  275. openLibraryMenu: true,
  276. });
  277. }
  278. };
  279. const updateScene = () => {
  280. const sceneData = {
  281. elements: restoreElements(
  282. convertToExcalidrawElements([
  283. {
  284. type: "rectangle",
  285. id: "rect-1",
  286. fillStyle: "hachure",
  287. strokeWidth: 1,
  288. strokeStyle: "solid",
  289. roughness: 1,
  290. angle: 0,
  291. x: 100.50390625,
  292. y: 93.67578125,
  293. strokeColor: "#c92a2a",
  294. width: 186.47265625,
  295. height: 141.9765625,
  296. seed: 1968410350,
  297. roundness: {
  298. type: ROUNDNESS.ADAPTIVE_RADIUS,
  299. value: 32,
  300. },
  301. },
  302. {
  303. type: "arrow",
  304. x: 300,
  305. y: 150,
  306. start: { id: "rect-1" },
  307. end: { type: "ellipse" },
  308. },
  309. {
  310. type: "text",
  311. x: 300,
  312. y: 100,
  313. text: "HELLO WORLD!",
  314. },
  315. ]),
  316. null,
  317. ),
  318. appState: {
  319. viewBackgroundColor: "#edf2ff",
  320. },
  321. };
  322. excalidrawAPI?.updateScene(sceneData);
  323. };
  324. const onLinkOpen = useCallback(
  325. (
  326. element: NonDeletedExcalidrawElement,
  327. event: CustomEvent<{
  328. nativeEvent: MouseEvent | React.PointerEvent<HTMLCanvasElement>;
  329. }>,
  330. ) => {
  331. const link = element.link!;
  332. const { nativeEvent } = event.detail;
  333. const isNewTab = nativeEvent.ctrlKey || nativeEvent.metaKey;
  334. const isNewWindow = nativeEvent.shiftKey;
  335. const isInternalLink =
  336. link.startsWith("/") || link.includes(window.location.origin);
  337. if (isInternalLink && !isNewTab && !isNewWindow) {
  338. // signal that we're handling the redirect ourselves
  339. event.preventDefault();
  340. // do a custom redirect, such as passing to react-router
  341. // ...
  342. }
  343. },
  344. [],
  345. );
  346. const onCopy = async (type: "png" | "svg" | "json") => {
  347. if (!excalidrawAPI) {
  348. return false;
  349. }
  350. await exportToClipboard({
  351. elements: excalidrawAPI.getSceneElements(),
  352. appState: excalidrawAPI.getAppState(),
  353. files: excalidrawAPI.getFiles(),
  354. type,
  355. });
  356. window.alert(`Copied to clipboard as ${type} successfully`);
  357. };
  358. const [pointerData, setPointerData] = useState<{
  359. pointer: { x: number; y: number };
  360. button: "down" | "up";
  361. pointersMap: Gesture["pointers"];
  362. } | null>(null);
  363. const onPointerDown = (
  364. activeTool: AppState["activeTool"],
  365. pointerDownState: ExcalidrawPointerDownState,
  366. ) => {
  367. if (activeTool.type === "custom" && activeTool.customType === "comment") {
  368. const { x, y } = pointerDownState.origin;
  369. setComment({ x, y, value: "" });
  370. }
  371. };
  372. const rerenderCommentIcons = () => {
  373. if (!excalidrawAPI) {
  374. return false;
  375. }
  376. const commentIconsElements = appRef.current.querySelectorAll(
  377. ".comment-icon",
  378. ) as HTMLElement[];
  379. commentIconsElements.forEach((ele) => {
  380. const id = ele.id;
  381. const appstate = excalidrawAPI.getAppState();
  382. const { x, y } = sceneCoordsToViewportCoords(
  383. { sceneX: commentIcons[id].x, sceneY: commentIcons[id].y },
  384. appstate,
  385. );
  386. ele.style.left = `${
  387. x - COMMENT_ICON_DIMENSION / 2 - appstate!.offsetLeft
  388. }px`;
  389. ele.style.top = `${
  390. y - COMMENT_ICON_DIMENSION / 2 - appstate!.offsetTop
  391. }px`;
  392. });
  393. };
  394. const onPointerMoveFromPointerDownHandler = (
  395. pointerDownState: PointerDownState,
  396. ) => {
  397. return withBatchedUpdatesThrottled((event) => {
  398. if (!excalidrawAPI) {
  399. return false;
  400. }
  401. const { x, y } = viewportCoordsToSceneCoords(
  402. {
  403. clientX: event.clientX - pointerDownState.hitElementOffsets.x,
  404. clientY: event.clientY - pointerDownState.hitElementOffsets.y,
  405. },
  406. excalidrawAPI.getAppState(),
  407. );
  408. setCommentIcons({
  409. ...commentIcons,
  410. [pointerDownState.hitElement.id!]: {
  411. ...commentIcons[pointerDownState.hitElement.id!],
  412. x,
  413. y,
  414. },
  415. });
  416. });
  417. };
  418. const onPointerUpFromPointerDownHandler = (
  419. pointerDownState: PointerDownState,
  420. ) => {
  421. return withBatchedUpdates((event) => {
  422. window.removeEventListener("pointermove", pointerDownState.onMove);
  423. window.removeEventListener("pointerup", pointerDownState.onUp);
  424. excalidrawAPI?.setActiveTool({ type: "selection" });
  425. const distance = distance2d(
  426. pointerDownState.x,
  427. pointerDownState.y,
  428. event.clientX,
  429. event.clientY,
  430. );
  431. if (distance === 0) {
  432. if (!comment) {
  433. setComment({
  434. x: pointerDownState.hitElement.x + 60,
  435. y: pointerDownState.hitElement.y,
  436. value: pointerDownState.hitElement.value,
  437. id: pointerDownState.hitElement.id,
  438. });
  439. } else {
  440. setComment(null);
  441. }
  442. }
  443. });
  444. };
  445. const renderCommentIcons = () => {
  446. return Object.values(commentIcons).map((commentIcon) => {
  447. if (!excalidrawAPI) {
  448. return false;
  449. }
  450. const appState = excalidrawAPI.getAppState();
  451. const { x, y } = sceneCoordsToViewportCoords(
  452. { sceneX: commentIcon.x, sceneY: commentIcon.y },
  453. excalidrawAPI.getAppState(),
  454. );
  455. return (
  456. <div
  457. id={commentIcon.id}
  458. key={commentIcon.id}
  459. style={{
  460. top: `${y - COMMENT_ICON_DIMENSION / 2 - appState!.offsetTop}px`,
  461. left: `${x - COMMENT_ICON_DIMENSION / 2 - appState!.offsetLeft}px`,
  462. position: "absolute",
  463. zIndex: 1,
  464. width: `${COMMENT_ICON_DIMENSION}px`,
  465. height: `${COMMENT_ICON_DIMENSION}px`,
  466. cursor: "pointer",
  467. touchAction: "none",
  468. }}
  469. className="comment-icon"
  470. onPointerDown={(event) => {
  471. event.preventDefault();
  472. if (comment) {
  473. commentIcon.value = comment.value;
  474. saveComment();
  475. }
  476. const pointerDownState: any = {
  477. x: event.clientX,
  478. y: event.clientY,
  479. hitElement: commentIcon,
  480. hitElementOffsets: { x: event.clientX - x, y: event.clientY - y },
  481. };
  482. const onPointerMove =
  483. onPointerMoveFromPointerDownHandler(pointerDownState);
  484. const onPointerUp =
  485. onPointerUpFromPointerDownHandler(pointerDownState);
  486. window.addEventListener("pointermove", onPointerMove);
  487. window.addEventListener("pointerup", onPointerUp);
  488. pointerDownState.onMove = onPointerMove;
  489. pointerDownState.onUp = onPointerUp;
  490. excalidrawAPI?.setActiveTool({
  491. type: "custom",
  492. customType: "comment",
  493. });
  494. }}
  495. >
  496. <div className="comment-avatar">
  497. <img src="images/doremon.png" alt="doremon" />
  498. </div>
  499. </div>
  500. );
  501. });
  502. };
  503. const saveComment = () => {
  504. if (!comment) {
  505. return;
  506. }
  507. if (!comment.id && !comment.value) {
  508. setComment(null);
  509. return;
  510. }
  511. const id = comment.id || nanoid();
  512. setCommentIcons({
  513. ...commentIcons,
  514. [id]: {
  515. x: comment.id ? comment.x - 60 : comment.x,
  516. y: comment.y,
  517. id,
  518. value: comment.value,
  519. },
  520. });
  521. setComment(null);
  522. };
  523. const renderComment = () => {
  524. if (!comment) {
  525. return null;
  526. }
  527. const appState = excalidrawAPI?.getAppState()!;
  528. const { x, y } = sceneCoordsToViewportCoords(
  529. { sceneX: comment.x, sceneY: comment.y },
  530. appState,
  531. );
  532. let top = y - COMMENT_ICON_DIMENSION / 2 - appState.offsetTop;
  533. let left = x - COMMENT_ICON_DIMENSION / 2 - appState.offsetLeft;
  534. if (
  535. top + COMMENT_INPUT_HEIGHT <
  536. appState.offsetTop + COMMENT_INPUT_HEIGHT
  537. ) {
  538. top = COMMENT_ICON_DIMENSION / 2;
  539. }
  540. if (top + COMMENT_INPUT_HEIGHT > appState.height) {
  541. top = appState.height - COMMENT_INPUT_HEIGHT - COMMENT_ICON_DIMENSION / 2;
  542. }
  543. if (
  544. left + COMMENT_INPUT_WIDTH <
  545. appState.offsetLeft + COMMENT_INPUT_WIDTH
  546. ) {
  547. left = COMMENT_ICON_DIMENSION / 2;
  548. }
  549. if (left + COMMENT_INPUT_WIDTH > appState.width) {
  550. left = appState.width - COMMENT_INPUT_WIDTH - COMMENT_ICON_DIMENSION / 2;
  551. }
  552. return (
  553. <textarea
  554. className="comment"
  555. style={{
  556. top: `${top}px`,
  557. left: `${left}px`,
  558. position: "absolute",
  559. zIndex: 1,
  560. height: `${COMMENT_INPUT_HEIGHT}px`,
  561. width: `${COMMENT_INPUT_WIDTH}px`,
  562. }}
  563. ref={(ref) => {
  564. setTimeout(() => ref?.focus());
  565. }}
  566. placeholder={comment.value ? "Reply" : "Comment"}
  567. value={comment.value}
  568. onChange={(event) => {
  569. setComment({ ...comment, value: event.target.value });
  570. }}
  571. onBlur={saveComment}
  572. onKeyDown={(event) => {
  573. if (!event.shiftKey && event.key === "Enter") {
  574. event.preventDefault();
  575. saveComment();
  576. }
  577. }}
  578. />
  579. );
  580. };
  581. const renderMenu = () => {
  582. return (
  583. <MainMenu>
  584. <MainMenu.Sub>
  585. <MainMenu.Sub.Trigger
  586. title="Custom trigger"
  587. icon={
  588. <svg
  589. xmlns="http://www.w3.org/2000/svg"
  590. fill="none"
  591. viewBox="0 0 24 24"
  592. strokeWidth={1.5}
  593. stroke="currentColor"
  594. className="w-6 h-6"
  595. >
  596. <path
  597. strokeLinecap="round"
  598. strokeLinejoin="round"
  599. 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"
  600. />
  601. </svg>
  602. }
  603. >
  604. Submenu trigger
  605. </MainMenu.Sub.Trigger>
  606. <MainMenu.Sub.Content>
  607. <MainMenu.Sub.Item
  608. icon={
  609. <svg
  610. xmlns="http://www.w3.org/2000/svg"
  611. fill="none"
  612. viewBox="0 0 24 24"
  613. strokeWidth={1.5}
  614. stroke="currentColor"
  615. className="w-6 h-6"
  616. >
  617. <path
  618. strokeLinecap="round"
  619. strokeLinejoin="round"
  620. 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"
  621. />
  622. </svg>
  623. }
  624. onSelect={() => window.alert("You clicked on sub item")}
  625. >
  626. Sub item
  627. </MainMenu.Sub.Item>
  628. </MainMenu.Sub.Content>
  629. </MainMenu.Sub>
  630. <MainMenu.DefaultItems.SaveAsImage />
  631. <MainMenu.DefaultItems.Export />
  632. <MainMenu.Separator />
  633. <MainMenu.DefaultItems.LiveCollaborationTrigger
  634. isCollaborating={isCollaborating}
  635. onSelect={() => window.alert("You clicked on collab button")}
  636. />
  637. <MainMenu.Sub>
  638. <MainMenu.Sub.Trigger>Trigger</MainMenu.Sub.Trigger>
  639. <MainMenu.Sub.Content>
  640. <MainMenu.Sub.Item
  641. onSelect={() => window.alert("You clicked on sub item")}
  642. >
  643. Sub item
  644. </MainMenu.Sub.Item>
  645. </MainMenu.Sub.Content>
  646. </MainMenu.Sub>
  647. <MainMenu.Group title="Excalidraw links">
  648. <MainMenu.DefaultItems.Socials />
  649. </MainMenu.Group>
  650. {/* <MainMenu.Separator /> */}
  651. <MainMenu.Sub>
  652. <MainMenu.Sub.Trigger className="custom-classname">
  653. Another submenu trigger
  654. </MainMenu.Sub.Trigger>
  655. <MainMenu.Sub.Content className="custom-classname-for-content">
  656. <MainMenu.Sub.Item
  657. title="Sub item"
  658. onSelect={() => window.alert("You clicked on sub item")}
  659. >
  660. Sub item
  661. </MainMenu.Sub.Item>
  662. </MainMenu.Sub.Content>
  663. </MainMenu.Sub>
  664. <MainMenu.Sub>
  665. <MainMenu.Sub.Trigger>Trigger me</MainMenu.Sub.Trigger>
  666. <MainMenu.Sub.Content>
  667. <MainMenu.Sub>
  668. <MainMenu.Sub.Trigger>Trigger me inside</MainMenu.Sub.Trigger>
  669. <MainMenu.Sub.Content>
  670. <MainMenu.Sub.Item
  671. onSelect={() => {
  672. alert("wow, nested submenus!");
  673. }}
  674. >
  675. Item wow
  676. </MainMenu.Sub.Item>
  677. </MainMenu.Sub.Content>
  678. </MainMenu.Sub>
  679. <MainMenu.Sub.Item
  680. onSelect={() => {
  681. alert("wow, nested submenus! very cool");
  682. }}
  683. >
  684. Another one
  685. </MainMenu.Sub.Item>
  686. </MainMenu.Sub.Content>
  687. </MainMenu.Sub>
  688. <MainMenu.ItemCustom>
  689. <button
  690. style={{ height: "2rem" }}
  691. onClick={() => window.alert("custom menu item")}
  692. >
  693. custom item
  694. </button>
  695. </MainMenu.ItemCustom>
  696. <MainMenu.DefaultItems.Help />
  697. {excalidrawAPI && (
  698. <MobileFooter
  699. excalidrawLib={excalidrawLib}
  700. excalidrawAPI={excalidrawAPI}
  701. />
  702. )}
  703. </MainMenu>
  704. );
  705. };
  706. return (
  707. <div className="App" ref={appRef}>
  708. <h1>{appTitle}</h1>
  709. {/* TODO fix type */}
  710. <ExampleSidebar>
  711. <div className="button-wrapper">
  712. <button onClick={loadSceneOrLibrary}>Load Scene or Library</button>
  713. <button className="update-scene" onClick={updateScene}>
  714. Update Scene
  715. </button>
  716. <button
  717. className="reset-scene"
  718. onClick={() => {
  719. excalidrawAPI?.resetScene();
  720. }}
  721. >
  722. Reset Scene
  723. </button>
  724. <button
  725. onClick={() => {
  726. const libraryItems: LibraryItems = [
  727. {
  728. status: "published",
  729. id: "1",
  730. created: 1,
  731. elements: initialData.libraryItems[1] as any,
  732. },
  733. {
  734. status: "unpublished",
  735. id: "2",
  736. created: 2,
  737. elements: initialData.libraryItems[1] as any,
  738. },
  739. ];
  740. excalidrawAPI?.updateLibrary({
  741. libraryItems,
  742. });
  743. }}
  744. >
  745. Update Library
  746. </button>
  747. <label>
  748. <input
  749. type="checkbox"
  750. checked={viewModeEnabled}
  751. onChange={() => setViewModeEnabled(!viewModeEnabled)}
  752. />
  753. View mode
  754. </label>
  755. <label>
  756. <input
  757. type="checkbox"
  758. checked={zenModeEnabled}
  759. onChange={() => setZenModeEnabled(!zenModeEnabled)}
  760. />
  761. Zen mode
  762. </label>
  763. <label>
  764. <input
  765. type="checkbox"
  766. checked={gridModeEnabled}
  767. onChange={() => setGridModeEnabled(!gridModeEnabled)}
  768. />
  769. Grid mode
  770. </label>
  771. <label>
  772. <input
  773. type="checkbox"
  774. checked={renderScrollbars}
  775. onChange={() => setRenderScrollbars(!renderScrollbars)}
  776. />
  777. Render scrollbars
  778. </label>
  779. <label>
  780. <input
  781. type="checkbox"
  782. checked={theme === "dark"}
  783. onChange={() => {
  784. setTheme(theme === "light" ? "dark" : "light");
  785. }}
  786. />
  787. Switch to Dark Theme
  788. </label>
  789. <label>
  790. <input
  791. type="checkbox"
  792. checked={disableImageTool === true}
  793. onChange={() => {
  794. setDisableImageTool(!disableImageTool);
  795. }}
  796. />
  797. Disable Image Tool
  798. </label>
  799. <label>
  800. <input
  801. type="checkbox"
  802. checked={isCollaborating}
  803. onChange={() => {
  804. if (!isCollaborating) {
  805. const collaborators = new Map();
  806. collaborators.set("id1", {
  807. username: "Doremon",
  808. avatarUrl: "images/doremon.png",
  809. });
  810. collaborators.set("id2", {
  811. username: "Excalibot",
  812. avatarUrl: "images/excalibot.png",
  813. });
  814. collaborators.set("id3", {
  815. username: "Pika",
  816. avatarUrl: "images/pika.jpeg",
  817. });
  818. collaborators.set("id4", {
  819. username: "fallback",
  820. avatarUrl: "https://example.com",
  821. });
  822. excalidrawAPI?.updateScene({ collaborators });
  823. } else {
  824. excalidrawAPI?.updateScene({
  825. collaborators: new Map(),
  826. });
  827. }
  828. setIsCollaborating(!isCollaborating);
  829. }}
  830. />
  831. Show collaborators
  832. </label>
  833. <div>
  834. <button onClick={onCopy.bind(null, "png")}>
  835. Copy to Clipboard as PNG
  836. </button>
  837. <button onClick={onCopy.bind(null, "svg")}>
  838. Copy to Clipboard as SVG
  839. </button>
  840. <button onClick={onCopy.bind(null, "json")}>
  841. Copy to Clipboard as JSON
  842. </button>
  843. </div>
  844. <div
  845. style={{
  846. display: "flex",
  847. gap: "1em",
  848. justifyContent: "center",
  849. marginTop: "1em",
  850. }}
  851. >
  852. <div>x: {pointerData?.pointer.x ?? 0}</div>
  853. <div>y: {pointerData?.pointer.y ?? 0}</div>
  854. </div>
  855. </div>
  856. <div className="excalidraw-wrapper">
  857. {renderExcalidraw(children)}
  858. {Object.keys(commentIcons || []).length > 0 && renderCommentIcons()}
  859. {comment && renderComment()}
  860. </div>
  861. <div className="export-wrapper button-wrapper">
  862. <label className="export-wrapper__checkbox">
  863. <input
  864. type="checkbox"
  865. checked={exportWithDarkMode}
  866. onChange={() => setExportWithDarkMode(!exportWithDarkMode)}
  867. />
  868. Export with dark mode
  869. </label>
  870. <label className="export-wrapper__checkbox">
  871. <input
  872. type="checkbox"
  873. checked={exportEmbedScene}
  874. onChange={() => setExportEmbedScene(!exportEmbedScene)}
  875. />
  876. Export with embed scene
  877. </label>
  878. <button
  879. onClick={async () => {
  880. if (!excalidrawAPI) {
  881. return;
  882. }
  883. const svg = await exportToSvg({
  884. elements: excalidrawAPI?.getSceneElements(),
  885. appState: {
  886. ...initialData.appState,
  887. exportWithDarkMode,
  888. exportEmbedScene,
  889. width: 300,
  890. height: 100,
  891. },
  892. files: excalidrawAPI?.getFiles(),
  893. });
  894. appRef.current.querySelector(".export-svg").innerHTML =
  895. svg.outerHTML;
  896. }}
  897. >
  898. Export to SVG
  899. </button>
  900. <div className="export export-svg"></div>
  901. <button
  902. onClick={async () => {
  903. if (!excalidrawAPI) {
  904. return;
  905. }
  906. const blob = await exportToBlob({
  907. elements: excalidrawAPI?.getSceneElements(),
  908. mimeType: "image/png",
  909. appState: {
  910. ...initialData.appState,
  911. exportEmbedScene,
  912. exportWithDarkMode,
  913. },
  914. files: excalidrawAPI?.getFiles(),
  915. });
  916. setBlobUrl(window.URL.createObjectURL(blob));
  917. }}
  918. >
  919. Export to Blob
  920. </button>
  921. <div className="export export-blob">
  922. <img src={blobUrl} alt="" />
  923. </div>
  924. <button
  925. onClick={async () => {
  926. if (!excalidrawAPI) {
  927. return;
  928. }
  929. const canvas = await exportToCanvas({
  930. elements: excalidrawAPI.getSceneElements(),
  931. appState: {
  932. ...initialData.appState,
  933. exportWithDarkMode,
  934. },
  935. files: excalidrawAPI.getFiles(),
  936. });
  937. const ctx = canvas.getContext("2d")!;
  938. ctx.font = "30px Excalifont";
  939. ctx.strokeText("My custom text", 50, 60);
  940. setCanvasUrl(canvas.toDataURL());
  941. }}
  942. >
  943. Export to Canvas
  944. </button>
  945. <button
  946. onClick={async () => {
  947. if (!excalidrawAPI) {
  948. return;
  949. }
  950. const canvas = await exportToCanvas({
  951. elements: excalidrawAPI.getSceneElements(),
  952. appState: {
  953. ...initialData.appState,
  954. exportWithDarkMode,
  955. },
  956. files: excalidrawAPI.getFiles(),
  957. });
  958. const ctx = canvas.getContext("2d")!;
  959. ctx.font = "30px Excalifont";
  960. ctx.strokeText("My custom text", 50, 60);
  961. setCanvasUrl(canvas.toDataURL());
  962. }}
  963. >
  964. Export to Canvas
  965. </button>
  966. <button
  967. type="button"
  968. onClick={() => {
  969. if (!excalidrawAPI) {
  970. return;
  971. }
  972. const elements = excalidrawAPI.getSceneElements();
  973. excalidrawAPI.scrollToContent(elements[0], {
  974. fitToViewport: true,
  975. });
  976. }}
  977. >
  978. Fit to viewport, first element
  979. </button>
  980. <button
  981. type="button"
  982. onClick={() => {
  983. if (!excalidrawAPI) {
  984. return;
  985. }
  986. const elements = excalidrawAPI.getSceneElements();
  987. excalidrawAPI.scrollToContent(elements[0], {
  988. fitToContent: true,
  989. });
  990. excalidrawAPI.scrollToContent(elements[0], {
  991. fitToContent: true,
  992. });
  993. }}
  994. >
  995. Fit to content, first element
  996. </button>
  997. <button
  998. type="button"
  999. onClick={() => {
  1000. if (!excalidrawAPI) {
  1001. return;
  1002. }
  1003. const elements = excalidrawAPI.getSceneElements();
  1004. excalidrawAPI.scrollToContent(elements[0], {
  1005. fitToContent: true,
  1006. });
  1007. excalidrawAPI.scrollToContent(elements[0]);
  1008. }}
  1009. >
  1010. Scroll to first element, no fitToContent, no fitToViewport
  1011. </button>
  1012. <div className="export export-canvas">
  1013. <img src={canvasUrl} alt="" />
  1014. </div>
  1015. </div>
  1016. </ExampleSidebar>
  1017. </div>
  1018. );
  1019. }