ExampleApp.tsx 28 KB

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