ExampleApp.tsx 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970
  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.DefaultItems.SaveAsImage />
  585. <MainMenu.DefaultItems.Export />
  586. <MainMenu.Separator />
  587. <MainMenu.DefaultItems.LiveCollaborationTrigger
  588. isCollaborating={isCollaborating}
  589. onSelect={() => window.alert("You clicked on collab button")}
  590. />
  591. <MainMenu.Group title="Excalidraw links">
  592. <MainMenu.DefaultItems.Socials />
  593. </MainMenu.Group>
  594. <MainMenu.Separator />
  595. <MainMenu.ItemCustom>
  596. <button
  597. style={{ height: "2rem" }}
  598. onClick={() => window.alert("custom menu item")}
  599. >
  600. custom item
  601. </button>
  602. </MainMenu.ItemCustom>
  603. <MainMenu.DefaultItems.Help />
  604. {excalidrawAPI && (
  605. <MobileFooter
  606. excalidrawLib={excalidrawLib}
  607. excalidrawAPI={excalidrawAPI}
  608. />
  609. )}
  610. </MainMenu>
  611. );
  612. };
  613. return (
  614. <div className="App" ref={appRef}>
  615. <h1>{appTitle}</h1>
  616. {/* TODO fix type */}
  617. <ExampleSidebar>
  618. <div className="button-wrapper">
  619. <button onClick={loadSceneOrLibrary}>Load Scene or Library</button>
  620. <button className="update-scene" onClick={updateScene}>
  621. Update Scene
  622. </button>
  623. <button
  624. className="reset-scene"
  625. onClick={() => {
  626. excalidrawAPI?.resetScene();
  627. }}
  628. >
  629. Reset Scene
  630. </button>
  631. <button
  632. onClick={() => {
  633. const libraryItems: LibraryItems = [
  634. {
  635. status: "published",
  636. id: "1",
  637. created: 1,
  638. elements: initialData.libraryItems[1] as any,
  639. },
  640. {
  641. status: "unpublished",
  642. id: "2",
  643. created: 2,
  644. elements: initialData.libraryItems[1] as any,
  645. },
  646. ];
  647. excalidrawAPI?.updateLibrary({
  648. libraryItems,
  649. });
  650. }}
  651. >
  652. Update Library
  653. </button>
  654. <label>
  655. <input
  656. type="checkbox"
  657. checked={viewModeEnabled}
  658. onChange={() => setViewModeEnabled(!viewModeEnabled)}
  659. />
  660. View mode
  661. </label>
  662. <label>
  663. <input
  664. type="checkbox"
  665. checked={zenModeEnabled}
  666. onChange={() => setZenModeEnabled(!zenModeEnabled)}
  667. />
  668. Zen mode
  669. </label>
  670. <label>
  671. <input
  672. type="checkbox"
  673. checked={gridModeEnabled}
  674. onChange={() => setGridModeEnabled(!gridModeEnabled)}
  675. />
  676. Grid mode
  677. </label>
  678. <label>
  679. <input
  680. type="checkbox"
  681. checked={renderScrollbars}
  682. onChange={() => setRenderScrollbars(!renderScrollbars)}
  683. />
  684. Render scrollbars
  685. </label>
  686. <label>
  687. <input
  688. type="checkbox"
  689. checked={theme === "dark"}
  690. onChange={() => {
  691. setTheme(theme === "light" ? "dark" : "light");
  692. }}
  693. />
  694. Switch to Dark Theme
  695. </label>
  696. <label>
  697. <input
  698. type="checkbox"
  699. checked={disableImageTool === true}
  700. onChange={() => {
  701. setDisableImageTool(!disableImageTool);
  702. }}
  703. />
  704. Disable Image Tool
  705. </label>
  706. <label>
  707. <input
  708. type="checkbox"
  709. checked={isCollaborating}
  710. onChange={() => {
  711. if (!isCollaborating) {
  712. const collaborators = new Map();
  713. collaborators.set("id1", {
  714. username: "Doremon",
  715. avatarUrl: "images/doremon.png",
  716. });
  717. collaborators.set("id2", {
  718. username: "Excalibot",
  719. avatarUrl: "images/excalibot.png",
  720. });
  721. collaborators.set("id3", {
  722. username: "Pika",
  723. avatarUrl: "images/pika.jpeg",
  724. });
  725. collaborators.set("id4", {
  726. username: "fallback",
  727. avatarUrl: "https://example.com",
  728. });
  729. excalidrawAPI?.updateScene({ collaborators });
  730. } else {
  731. excalidrawAPI?.updateScene({
  732. collaborators: new Map(),
  733. });
  734. }
  735. setIsCollaborating(!isCollaborating);
  736. }}
  737. />
  738. Show collaborators
  739. </label>
  740. <div>
  741. <button onClick={onCopy.bind(null, "png")}>
  742. Copy to Clipboard as PNG
  743. </button>
  744. <button onClick={onCopy.bind(null, "svg")}>
  745. Copy to Clipboard as SVG
  746. </button>
  747. <button onClick={onCopy.bind(null, "json")}>
  748. Copy to Clipboard as JSON
  749. </button>
  750. </div>
  751. <div
  752. style={{
  753. display: "flex",
  754. gap: "1em",
  755. justifyContent: "center",
  756. marginTop: "1em",
  757. }}
  758. >
  759. <div>x: {pointerData?.pointer.x ?? 0}</div>
  760. <div>y: {pointerData?.pointer.y ?? 0}</div>
  761. </div>
  762. </div>
  763. <div className="excalidraw-wrapper">
  764. {renderExcalidraw(children)}
  765. {Object.keys(commentIcons || []).length > 0 && renderCommentIcons()}
  766. {comment && renderComment()}
  767. </div>
  768. <div className="export-wrapper button-wrapper">
  769. <label className="export-wrapper__checkbox">
  770. <input
  771. type="checkbox"
  772. checked={exportWithDarkMode}
  773. onChange={() => setExportWithDarkMode(!exportWithDarkMode)}
  774. />
  775. Export with dark mode
  776. </label>
  777. <label className="export-wrapper__checkbox">
  778. <input
  779. type="checkbox"
  780. checked={exportEmbedScene}
  781. onChange={() => setExportEmbedScene(!exportEmbedScene)}
  782. />
  783. Export with embed scene
  784. </label>
  785. <button
  786. onClick={async () => {
  787. if (!excalidrawAPI) {
  788. return;
  789. }
  790. const svg = await exportToSvg({
  791. elements: excalidrawAPI?.getSceneElements(),
  792. appState: {
  793. ...initialData.appState,
  794. exportWithDarkMode,
  795. exportEmbedScene,
  796. width: 300,
  797. height: 100,
  798. },
  799. files: excalidrawAPI?.getFiles(),
  800. });
  801. appRef.current.querySelector(".export-svg").innerHTML =
  802. svg.outerHTML;
  803. }}
  804. >
  805. Export to SVG
  806. </button>
  807. <div className="export export-svg"></div>
  808. <button
  809. onClick={async () => {
  810. if (!excalidrawAPI) {
  811. return;
  812. }
  813. const blob = await exportToBlob({
  814. elements: excalidrawAPI?.getSceneElements(),
  815. mimeType: "image/png",
  816. appState: {
  817. ...initialData.appState,
  818. exportEmbedScene,
  819. exportWithDarkMode,
  820. },
  821. files: excalidrawAPI?.getFiles(),
  822. });
  823. setBlobUrl(window.URL.createObjectURL(blob));
  824. }}
  825. >
  826. Export to Blob
  827. </button>
  828. <div className="export export-blob">
  829. <img src={blobUrl} alt="" />
  830. </div>
  831. <button
  832. onClick={async () => {
  833. if (!excalidrawAPI) {
  834. return;
  835. }
  836. const canvas = await exportToCanvas({
  837. elements: excalidrawAPI.getSceneElements(),
  838. appState: {
  839. ...initialData.appState,
  840. exportWithDarkMode,
  841. },
  842. files: excalidrawAPI.getFiles(),
  843. });
  844. const ctx = canvas.getContext("2d")!;
  845. ctx.font = "30px Excalifont";
  846. ctx.strokeText("My custom text", 50, 60);
  847. setCanvasUrl(canvas.toDataURL());
  848. }}
  849. >
  850. Export to Canvas
  851. </button>
  852. <button
  853. onClick={async () => {
  854. if (!excalidrawAPI) {
  855. return;
  856. }
  857. const canvas = await exportToCanvas({
  858. elements: excalidrawAPI.getSceneElements(),
  859. appState: {
  860. ...initialData.appState,
  861. exportWithDarkMode,
  862. },
  863. files: excalidrawAPI.getFiles(),
  864. });
  865. const ctx = canvas.getContext("2d")!;
  866. ctx.font = "30px Excalifont";
  867. ctx.strokeText("My custom text", 50, 60);
  868. setCanvasUrl(canvas.toDataURL());
  869. }}
  870. >
  871. Export to Canvas
  872. </button>
  873. <button
  874. type="button"
  875. onClick={() => {
  876. if (!excalidrawAPI) {
  877. return;
  878. }
  879. const elements = excalidrawAPI.getSceneElements();
  880. excalidrawAPI.scrollToContent(elements[0], {
  881. fitToViewport: true,
  882. });
  883. }}
  884. >
  885. Fit to viewport, first element
  886. </button>
  887. <button
  888. type="button"
  889. onClick={() => {
  890. if (!excalidrawAPI) {
  891. return;
  892. }
  893. const elements = excalidrawAPI.getSceneElements();
  894. excalidrawAPI.scrollToContent(elements[0], {
  895. fitToContent: true,
  896. });
  897. excalidrawAPI.scrollToContent(elements[0], {
  898. fitToContent: true,
  899. });
  900. }}
  901. >
  902. Fit to content, first element
  903. </button>
  904. <button
  905. type="button"
  906. onClick={() => {
  907. if (!excalidrawAPI) {
  908. return;
  909. }
  910. const elements = excalidrawAPI.getSceneElements();
  911. excalidrawAPI.scrollToContent(elements[0], {
  912. fitToContent: true,
  913. });
  914. excalidrawAPI.scrollToContent(elements[0]);
  915. }}
  916. >
  917. Scroll to first element, no fitToContent, no fitToViewport
  918. </button>
  919. <div className="export export-canvas">
  920. <img src={canvasUrl} alt="" />
  921. </div>
  922. </div>
  923. </ExampleSidebar>
  924. </div>
  925. );
  926. }