App.tsx 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481
  1. import polyfill from "../packages/excalidraw/polyfill";
  2. import { useCallback, useEffect, useRef, useState } from "react";
  3. import { trackEvent } from "../packages/excalidraw/analytics";
  4. import { getDefaultAppState } from "../packages/excalidraw/appState";
  5. import { ErrorDialog } from "../packages/excalidraw/components/ErrorDialog";
  6. import { TopErrorBoundary } from "./components/TopErrorBoundary";
  7. import {
  8. APP_NAME,
  9. EVENT,
  10. THEME,
  11. TITLE_TIMEOUT,
  12. VERSION_TIMEOUT,
  13. } from "../packages/excalidraw/constants";
  14. import { loadFromBlob } from "../packages/excalidraw/data/blob";
  15. import type {
  16. FileId,
  17. NonDeletedExcalidrawElement,
  18. OrderedExcalidrawElement,
  19. } from "../packages/excalidraw/element/types";
  20. import { useCallbackRefState } from "../packages/excalidraw/hooks/useCallbackRefState";
  21. import { t } from "../packages/excalidraw/i18n";
  22. import {
  23. Excalidraw,
  24. LiveCollaborationTrigger,
  25. TTDDialogTrigger,
  26. StoreAction,
  27. reconcileElements,
  28. exportToCanvas,
  29. } from "../packages/excalidraw";
  30. import {
  31. exportToBlob,
  32. getNonDeletedElements,
  33. } from "../packages/excalidraw/index";
  34. import type {
  35. AppState,
  36. ExcalidrawImperativeAPI,
  37. BinaryFiles,
  38. ExcalidrawInitialDataState,
  39. UIAppState,
  40. } from "../packages/excalidraw/types";
  41. import type { ResolvablePromise } from "../packages/excalidraw/utils";
  42. import {
  43. debounce,
  44. getVersion,
  45. getFrame,
  46. isTestEnv,
  47. preventUnload,
  48. resolvablePromise,
  49. isRunningInIframe,
  50. } from "../packages/excalidraw/utils";
  51. import {
  52. FIREBASE_STORAGE_PREFIXES,
  53. isExcalidrawPlusSignedUser,
  54. STORAGE_KEYS,
  55. SYNC_BROWSER_TABS_TIMEOUT,
  56. } from "./app_constants";
  57. import type { CollabAPI } from "./collab/Collab";
  58. import Collab, {
  59. collabAPIAtom,
  60. isCollaboratingAtom,
  61. isOfflineAtom,
  62. } from "./collab/Collab";
  63. import {
  64. exportToBackend,
  65. getCollaborationLinkData,
  66. isCollaborationLink,
  67. loadScene,
  68. } from "./data";
  69. import {
  70. importFromLocalStorage,
  71. importUsernameFromLocalStorage,
  72. } from "./data/localStorage";
  73. import CustomStats from "./CustomStats";
  74. import type { RestoredDataState } from "../packages/excalidraw/data/restore";
  75. import { restore, restoreAppState } from "../packages/excalidraw/data/restore";
  76. import {
  77. ExportToExcalidrawPlus,
  78. exportToExcalidrawPlus,
  79. } from "./components/ExportToExcalidrawPlus";
  80. import { updateStaleImageStatuses } from "./data/FileManager";
  81. import { newElementWith } from "../packages/excalidraw/element/mutateElement";
  82. import { isInitializedImageElement } from "../packages/excalidraw/element/typeChecks";
  83. import { loadFilesFromFirebase } from "./data/firebase";
  84. import {
  85. LibraryIndexedDBAdapter,
  86. LibraryLocalStorageMigrationAdapter,
  87. LocalData,
  88. } from "./data/LocalData";
  89. import { isBrowserStorageStateNewer } from "./data/tabSync";
  90. import clsx from "clsx";
  91. import {
  92. parseLibraryTokensFromUrl,
  93. useHandleLibrary,
  94. } from "../packages/excalidraw/data/library";
  95. import { AppMainMenu } from "./components/AppMainMenu";
  96. import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
  97. import { AppFooter } from "./components/AppFooter";
  98. import { Provider, useAtom, useAtomValue } from "jotai";
  99. import { useAtomWithInitialValue } from "../packages/excalidraw/jotai";
  100. import { appJotaiStore } from "./app-jotai";
  101. import "./index.scss";
  102. import type { ResolutionType } from "../packages/excalidraw/utility-types";
  103. import { ShareableLinkDialog } from "../packages/excalidraw/components/ShareableLinkDialog";
  104. import { openConfirmModal } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmState";
  105. import { OverwriteConfirmDialog } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm";
  106. import Trans from "../packages/excalidraw/components/Trans";
  107. import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog";
  108. import CollabError, { collabErrorIndicatorAtom } from "./collab/CollabError";
  109. import type { RemoteExcalidrawElement } from "../packages/excalidraw/data/reconcile";
  110. import {
  111. CommandPalette,
  112. DEFAULT_CATEGORIES,
  113. } from "../packages/excalidraw/components/CommandPalette/CommandPalette";
  114. import {
  115. GithubIcon,
  116. XBrandIcon,
  117. DiscordIcon,
  118. ExcalLogo,
  119. usersIcon,
  120. exportToPlus,
  121. share,
  122. youtubeIcon,
  123. } from "../packages/excalidraw/components/icons";
  124. import { appThemeAtom, useHandleAppTheme } from "./useHandleAppTheme";
  125. import { getPreferredLanguage } from "./app-language/language-detector";
  126. import { useAppLangCode } from "./app-language/language-state";
  127. import DebugCanvas, {
  128. debugRenderer,
  129. isVisualDebuggerEnabled,
  130. loadSavedDebugState,
  131. } from "./components/DebugCanvas";
  132. import { AIComponents } from "./components/AI";
  133. import { ExcalidrawPlusIframeExport } from "./ExcalidrawPlusIframeExport";
  134. import { fileSave } from "../packages/excalidraw/data/filesystem";
  135. import type { ExportToCanvasConfig } from "../packages/excalidraw/scene/export";
  136. polyfill();
  137. window.EXCALIDRAW_THROTTLE_RENDER = true;
  138. declare global {
  139. interface BeforeInstallPromptEventChoiceResult {
  140. outcome: "accepted" | "dismissed";
  141. }
  142. interface BeforeInstallPromptEvent extends Event {
  143. prompt(): Promise<void>;
  144. userChoice: Promise<BeforeInstallPromptEventChoiceResult>;
  145. }
  146. interface WindowEventMap {
  147. beforeinstallprompt: BeforeInstallPromptEvent;
  148. }
  149. }
  150. let pwaEvent: BeforeInstallPromptEvent | null = null;
  151. // Adding a listener outside of the component as it may (?) need to be
  152. // subscribed early to catch the event.
  153. //
  154. // Also note that it will fire only if certain heuristics are met (user has
  155. // used the app for some time, etc.)
  156. window.addEventListener(
  157. "beforeinstallprompt",
  158. (event: BeforeInstallPromptEvent) => {
  159. // prevent Chrome <= 67 from automatically showing the prompt
  160. event.preventDefault();
  161. // cache for later use
  162. pwaEvent = event;
  163. },
  164. );
  165. let isSelfEmbedding = false;
  166. if (window.self !== window.top) {
  167. try {
  168. const parentUrl = new URL(document.referrer);
  169. const currentUrl = new URL(window.location.href);
  170. if (parentUrl.origin === currentUrl.origin) {
  171. isSelfEmbedding = true;
  172. }
  173. } catch (error) {
  174. // ignore
  175. }
  176. }
  177. const shareableLinkConfirmDialog = {
  178. title: t("overwriteConfirm.modal.shareableLink.title"),
  179. description: (
  180. <Trans
  181. i18nKey="overwriteConfirm.modal.shareableLink.description"
  182. bold={(text) => <strong>{text}</strong>}
  183. br={() => <br />}
  184. />
  185. ),
  186. actionLabel: t("overwriteConfirm.modal.shareableLink.button"),
  187. color: "danger",
  188. } as const;
  189. const initializeScene = async (opts: {
  190. collabAPI: CollabAPI | null;
  191. excalidrawAPI: ExcalidrawImperativeAPI;
  192. }): Promise<
  193. { scene: ExcalidrawInitialDataState | null } & (
  194. | { isExternalScene: true; id: string; key: string }
  195. | { isExternalScene: false; id?: null; key?: null }
  196. )
  197. > => {
  198. const searchParams = new URLSearchParams(window.location.search);
  199. const id = searchParams.get("id");
  200. const jsonBackendMatch = window.location.hash.match(
  201. /^#json=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/,
  202. );
  203. const externalUrlMatch = window.location.hash.match(/^#url=(.*)$/);
  204. const localDataState = importFromLocalStorage();
  205. let scene: RestoredDataState & {
  206. scrollToContent?: boolean;
  207. } = await loadScene(null, null, localDataState);
  208. let roomLinkData = getCollaborationLinkData(window.location.href);
  209. const isExternalScene = !!(id || jsonBackendMatch || roomLinkData);
  210. if (isExternalScene) {
  211. if (
  212. // don't prompt if scene is empty
  213. !scene.elements.length ||
  214. // don't prompt for collab scenes because we don't override local storage
  215. roomLinkData ||
  216. // otherwise, prompt whether user wants to override current scene
  217. (await openConfirmModal(shareableLinkConfirmDialog))
  218. ) {
  219. if (jsonBackendMatch) {
  220. scene = await loadScene(
  221. jsonBackendMatch[1],
  222. jsonBackendMatch[2],
  223. localDataState,
  224. );
  225. }
  226. scene.scrollToContent = true;
  227. if (!roomLinkData) {
  228. window.history.replaceState({}, APP_NAME, window.location.origin);
  229. }
  230. } else {
  231. // https://github.com/excalidraw/excalidraw/issues/1919
  232. if (document.hidden) {
  233. return new Promise((resolve, reject) => {
  234. window.addEventListener(
  235. "focus",
  236. () => initializeScene(opts).then(resolve).catch(reject),
  237. {
  238. once: true,
  239. },
  240. );
  241. });
  242. }
  243. roomLinkData = null;
  244. window.history.replaceState({}, APP_NAME, window.location.origin);
  245. }
  246. } else if (externalUrlMatch) {
  247. window.history.replaceState({}, APP_NAME, window.location.origin);
  248. const url = externalUrlMatch[1];
  249. try {
  250. const request = await fetch(window.decodeURIComponent(url));
  251. const data = await loadFromBlob(await request.blob(), null, null);
  252. if (
  253. !scene.elements.length ||
  254. (await openConfirmModal(shareableLinkConfirmDialog))
  255. ) {
  256. return { scene: data, isExternalScene };
  257. }
  258. } catch (error: any) {
  259. return {
  260. scene: {
  261. appState: {
  262. errorMessage: t("alerts.invalidSceneUrl"),
  263. },
  264. },
  265. isExternalScene,
  266. };
  267. }
  268. }
  269. if (roomLinkData && opts.collabAPI) {
  270. const { excalidrawAPI } = opts;
  271. const scene = await opts.collabAPI.startCollaboration(roomLinkData);
  272. return {
  273. // when collaborating, the state may have already been updated at this
  274. // point (we may have received updates from other clients), so reconcile
  275. // elements and appState with existing state
  276. scene: {
  277. ...scene,
  278. appState: {
  279. ...restoreAppState(
  280. {
  281. ...scene?.appState,
  282. theme: localDataState?.appState?.theme || scene?.appState?.theme,
  283. },
  284. excalidrawAPI.getAppState(),
  285. ),
  286. // necessary if we're invoking from a hashchange handler which doesn't
  287. // go through App.initializeScene() that resets this flag
  288. isLoading: false,
  289. },
  290. elements: reconcileElements(
  291. scene?.elements || [],
  292. excalidrawAPI.getSceneElementsIncludingDeleted() as RemoteExcalidrawElement[],
  293. excalidrawAPI.getAppState(),
  294. ),
  295. },
  296. isExternalScene: true,
  297. id: roomLinkData.roomId,
  298. key: roomLinkData.roomKey,
  299. };
  300. } else if (scene) {
  301. return isExternalScene && jsonBackendMatch
  302. ? {
  303. scene,
  304. isExternalScene,
  305. id: jsonBackendMatch[1],
  306. key: jsonBackendMatch[2],
  307. }
  308. : { scene, isExternalScene: false };
  309. }
  310. return { scene: null, isExternalScene: false };
  311. };
  312. const ExcalidrawWrapper = () => {
  313. const [errorMessage, setErrorMessage] = useState("");
  314. const isCollabDisabled = isRunningInIframe();
  315. const [appTheme, setAppTheme] = useAtom(appThemeAtom);
  316. const { editorTheme } = useHandleAppTheme();
  317. const [langCode, setLangCode] = useAppLangCode();
  318. // initial state
  319. // ---------------------------------------------------------------------------
  320. const initialStatePromiseRef = useRef<{
  321. promise: ResolvablePromise<ExcalidrawInitialDataState | null>;
  322. }>({ promise: null! });
  323. if (!initialStatePromiseRef.current.promise) {
  324. initialStatePromiseRef.current.promise =
  325. resolvablePromise<ExcalidrawInitialDataState | null>();
  326. }
  327. const debugCanvasRef = useRef<HTMLCanvasElement>(null);
  328. useEffect(() => {
  329. trackEvent("load", "frame", getFrame());
  330. // Delayed so that the app has a time to load the latest SW
  331. setTimeout(() => {
  332. trackEvent("load", "version", getVersion());
  333. }, VERSION_TIMEOUT);
  334. }, []);
  335. const [excalidrawAPI, excalidrawRefCallback] =
  336. useCallbackRefState<ExcalidrawImperativeAPI>();
  337. const [, setShareDialogState] = useAtom(shareDialogStateAtom);
  338. const [collabAPI] = useAtom(collabAPIAtom);
  339. const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
  340. return isCollaborationLink(window.location.href);
  341. });
  342. const collabError = useAtomValue(collabErrorIndicatorAtom);
  343. useHandleLibrary({
  344. excalidrawAPI,
  345. adapter: LibraryIndexedDBAdapter,
  346. // TODO maybe remove this in several months (shipped: 24-03-11)
  347. migrationAdapter: LibraryLocalStorageMigrationAdapter,
  348. });
  349. const [, forceRefresh] = useState(false);
  350. useEffect(() => {
  351. if (import.meta.env.DEV) {
  352. const debugState = loadSavedDebugState();
  353. if (debugState.enabled && !window.visualDebug) {
  354. window.visualDebug = {
  355. data: [],
  356. };
  357. } else {
  358. delete window.visualDebug;
  359. }
  360. forceRefresh((prev) => !prev);
  361. }
  362. }, [excalidrawAPI]);
  363. useEffect(() => {
  364. if (!excalidrawAPI || (!isCollabDisabled && !collabAPI)) {
  365. return;
  366. }
  367. const loadImages = (
  368. data: ResolutionType<typeof initializeScene>,
  369. isInitialLoad = false,
  370. ) => {
  371. if (!data.scene) {
  372. return;
  373. }
  374. if (collabAPI?.isCollaborating()) {
  375. if (data.scene.elements) {
  376. collabAPI
  377. .fetchImageFilesFromFirebase({
  378. elements: data.scene.elements,
  379. forceFetchFiles: true,
  380. })
  381. .then(({ loadedFiles, erroredFiles }) => {
  382. excalidrawAPI.addFiles(loadedFiles);
  383. updateStaleImageStatuses({
  384. excalidrawAPI,
  385. erroredFiles,
  386. elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
  387. });
  388. });
  389. }
  390. } else {
  391. const fileIds =
  392. data.scene.elements?.reduce((acc, element) => {
  393. if (isInitializedImageElement(element)) {
  394. return acc.concat(element.fileId);
  395. }
  396. return acc;
  397. }, [] as FileId[]) || [];
  398. if (data.isExternalScene) {
  399. loadFilesFromFirebase(
  400. `${FIREBASE_STORAGE_PREFIXES.shareLinkFiles}/${data.id}`,
  401. data.key,
  402. fileIds,
  403. ).then(({ loadedFiles, erroredFiles }) => {
  404. excalidrawAPI.addFiles(loadedFiles);
  405. updateStaleImageStatuses({
  406. excalidrawAPI,
  407. erroredFiles,
  408. elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
  409. });
  410. });
  411. } else if (isInitialLoad) {
  412. if (fileIds.length) {
  413. LocalData.fileStorage
  414. .getFiles(fileIds)
  415. .then(({ loadedFiles, erroredFiles }) => {
  416. if (loadedFiles.length) {
  417. excalidrawAPI.addFiles(loadedFiles);
  418. }
  419. updateStaleImageStatuses({
  420. excalidrawAPI,
  421. erroredFiles,
  422. elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
  423. });
  424. });
  425. }
  426. // on fresh load, clear unused files from IDB (from previous
  427. // session)
  428. LocalData.fileStorage.clearObsoleteFiles({ currentFileIds: fileIds });
  429. }
  430. }
  431. };
  432. initializeScene({ collabAPI, excalidrawAPI }).then(async (data) => {
  433. loadImages(data, /* isInitialLoad */ true);
  434. initialStatePromiseRef.current.promise.resolve(data.scene);
  435. });
  436. const onHashChange = async (event: HashChangeEvent) => {
  437. event.preventDefault();
  438. const libraryUrlTokens = parseLibraryTokensFromUrl();
  439. if (!libraryUrlTokens) {
  440. if (
  441. collabAPI?.isCollaborating() &&
  442. !isCollaborationLink(window.location.href)
  443. ) {
  444. collabAPI.stopCollaboration(false);
  445. }
  446. excalidrawAPI.updateScene({ appState: { isLoading: true } });
  447. initializeScene({ collabAPI, excalidrawAPI }).then((data) => {
  448. loadImages(data);
  449. if (data.scene) {
  450. excalidrawAPI.updateScene({
  451. ...data.scene,
  452. ...restore(data.scene, null, null, { repairBindings: true }),
  453. storeAction: StoreAction.CAPTURE,
  454. });
  455. }
  456. });
  457. }
  458. };
  459. const titleTimeout = setTimeout(
  460. () => (document.title = APP_NAME),
  461. TITLE_TIMEOUT,
  462. );
  463. const syncData = debounce(() => {
  464. if (isTestEnv()) {
  465. return;
  466. }
  467. if (
  468. !document.hidden &&
  469. ((collabAPI && !collabAPI.isCollaborating()) || isCollabDisabled)
  470. ) {
  471. // don't sync if local state is newer or identical to browser state
  472. if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {
  473. const localDataState = importFromLocalStorage();
  474. const username = importUsernameFromLocalStorage();
  475. setLangCode(getPreferredLanguage());
  476. excalidrawAPI.updateScene({
  477. ...localDataState,
  478. storeAction: StoreAction.UPDATE,
  479. });
  480. LibraryIndexedDBAdapter.load().then((data) => {
  481. if (data) {
  482. excalidrawAPI.updateLibrary({
  483. libraryItems: data.libraryItems,
  484. });
  485. }
  486. });
  487. collabAPI?.setUsername(username || "");
  488. }
  489. if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_FILES)) {
  490. const elements = excalidrawAPI.getSceneElementsIncludingDeleted();
  491. const currFiles = excalidrawAPI.getFiles();
  492. const fileIds =
  493. elements?.reduce((acc, element) => {
  494. if (
  495. isInitializedImageElement(element) &&
  496. // only load and update images that aren't already loaded
  497. !currFiles[element.fileId]
  498. ) {
  499. return acc.concat(element.fileId);
  500. }
  501. return acc;
  502. }, [] as FileId[]) || [];
  503. if (fileIds.length) {
  504. LocalData.fileStorage
  505. .getFiles(fileIds)
  506. .then(({ loadedFiles, erroredFiles }) => {
  507. if (loadedFiles.length) {
  508. excalidrawAPI.addFiles(loadedFiles);
  509. }
  510. updateStaleImageStatuses({
  511. excalidrawAPI,
  512. erroredFiles,
  513. elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
  514. });
  515. });
  516. }
  517. }
  518. }
  519. }, SYNC_BROWSER_TABS_TIMEOUT);
  520. const onUnload = () => {
  521. LocalData.flushSave();
  522. };
  523. const visibilityChange = (event: FocusEvent | Event) => {
  524. if (event.type === EVENT.BLUR || document.hidden) {
  525. LocalData.flushSave();
  526. }
  527. if (
  528. event.type === EVENT.VISIBILITY_CHANGE ||
  529. event.type === EVENT.FOCUS
  530. ) {
  531. syncData();
  532. }
  533. };
  534. window.addEventListener(EVENT.HASHCHANGE, onHashChange, false);
  535. window.addEventListener(EVENT.UNLOAD, onUnload, false);
  536. window.addEventListener(EVENT.BLUR, visibilityChange, false);
  537. document.addEventListener(EVENT.VISIBILITY_CHANGE, visibilityChange, false);
  538. window.addEventListener(EVENT.FOCUS, visibilityChange, false);
  539. return () => {
  540. window.removeEventListener(EVENT.HASHCHANGE, onHashChange, false);
  541. window.removeEventListener(EVENT.UNLOAD, onUnload, false);
  542. window.removeEventListener(EVENT.BLUR, visibilityChange, false);
  543. window.removeEventListener(EVENT.FOCUS, visibilityChange, false);
  544. document.removeEventListener(
  545. EVENT.VISIBILITY_CHANGE,
  546. visibilityChange,
  547. false,
  548. );
  549. clearTimeout(titleTimeout);
  550. };
  551. }, [isCollabDisabled, collabAPI, excalidrawAPI, setLangCode]);
  552. useEffect(() => {
  553. const unloadHandler = (event: BeforeUnloadEvent) => {
  554. LocalData.flushSave();
  555. if (
  556. excalidrawAPI &&
  557. LocalData.fileStorage.shouldPreventUnload(
  558. excalidrawAPI.getSceneElements(),
  559. )
  560. ) {
  561. preventUnload(event);
  562. }
  563. };
  564. window.addEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
  565. return () => {
  566. window.removeEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
  567. };
  568. }, [excalidrawAPI]);
  569. const canvasPreviewContainerRef = useRef<HTMLDivElement>(null);
  570. const [config, setConfig] = useState<ExportToCanvasConfig>(
  571. JSON.parse(localStorage.getItem("_exportConfig") || "null") || {
  572. width: 300,
  573. height: 100,
  574. padding: 2,
  575. scale: 1,
  576. position: "none",
  577. fit: "contain",
  578. canvasBackgroundColor: "yellow",
  579. },
  580. );
  581. useEffect(() => {
  582. localStorage.setItem("_exportConfig", JSON.stringify(config));
  583. }, [config]);
  584. const onChange = (
  585. elements: readonly OrderedExcalidrawElement[],
  586. appState: AppState,
  587. files: BinaryFiles,
  588. ) => {
  589. if (collabAPI?.isCollaborating()) {
  590. collabAPI.syncElements(elements);
  591. }
  592. {
  593. const frame = elements.find(
  594. (el) => el.strokeStyle === "dashed" && !el.isDeleted,
  595. );
  596. exportToCanvas({
  597. data: {
  598. elements: getNonDeletedElements(elements).filter(
  599. (x) => x.id !== frame?.id,
  600. ),
  601. // .concat(
  602. // restoreElements(
  603. // [
  604. // // @ts-ignore
  605. // {
  606. // type: "rectangle",
  607. // width: appState.width / zoom,
  608. // height: appState.height / zoom,
  609. // x: -appState.scrollX,
  610. // y: -appState.scrollY,
  611. // fillStyle: "solid",
  612. // strokeColor: "transparent",
  613. // backgroundColor: "rgba(0,0,0,0.05)",
  614. // roundness: { type: ROUNDNESS.ADAPTIVE_RADIUS, value: 40 },
  615. // },
  616. // ],
  617. // null,
  618. // ),
  619. // ),
  620. appState,
  621. files,
  622. },
  623. config: {
  624. // // light yellow
  625. // // canvasBackgroundColor: "#fff9c4",
  626. // // width,
  627. // // maxWidthOrHeight: 120,
  628. // // scale: 0.01,
  629. // // scale: 2,
  630. // // origin: "content",
  631. // // fit: "cover",
  632. // // scale: 2,
  633. // // x: 0,
  634. // // y: 0,
  635. // padding: 20,
  636. // ...config,
  637. // width: config.width,
  638. // height: config.height,
  639. // maxWidthOrHeight: config.maxWidthOrHeight,
  640. // widthOrHeight: config.widthOrHeight,
  641. // padding: config.padding,
  642. ...(frame
  643. ? {
  644. ...config,
  645. width: frame.width,
  646. height: frame.height,
  647. x: frame.x,
  648. y: frame.y,
  649. }
  650. : config),
  651. // // height: 140,
  652. // // x: -appState.scrollX,
  653. // // y: -appState.scrollY,
  654. // // height: 150,
  655. // // height: appState.height,
  656. // // scale,
  657. // // zoom: { value: appState.zoom.value },
  658. // // getDimensions(width,height) {
  659. // // setCanvasSize({ width, height })
  660. // // return {width: 300, height: 150}
  661. // // }
  662. },
  663. }).then((canvas) => {
  664. if (canvasPreviewContainerRef.current) {
  665. canvasPreviewContainerRef.current.replaceChildren(canvas);
  666. document.querySelector(
  667. ".dims",
  668. )!.innerHTML = `${canvas.width}x${canvas.height}`;
  669. // canvas.style.width = "100%";
  670. }
  671. });
  672. }
  673. // this check is redundant, but since this is a hot path, it's best
  674. // not to evaludate the nested expression every time
  675. if (!LocalData.isSavePaused()) {
  676. LocalData.save(elements, appState, files, () => {
  677. if (excalidrawAPI) {
  678. let didChange = false;
  679. const elements = excalidrawAPI
  680. .getSceneElementsIncludingDeleted()
  681. .map((element) => {
  682. if (
  683. LocalData.fileStorage.shouldUpdateImageElementStatus(element)
  684. ) {
  685. const newElement = newElementWith(element, { status: "saved" });
  686. if (newElement !== element) {
  687. didChange = true;
  688. }
  689. return newElement;
  690. }
  691. return element;
  692. });
  693. if (didChange) {
  694. excalidrawAPI.updateScene({
  695. elements,
  696. storeAction: StoreAction.UPDATE,
  697. });
  698. }
  699. }
  700. });
  701. }
  702. // Render the debug scene if the debug canvas is available
  703. if (debugCanvasRef.current && excalidrawAPI) {
  704. debugRenderer(
  705. debugCanvasRef.current,
  706. appState,
  707. window.devicePixelRatio,
  708. () => forceRefresh((prev) => !prev),
  709. );
  710. }
  711. };
  712. const [latestShareableLink, setLatestShareableLink] = useState<string | null>(
  713. null,
  714. );
  715. const onExportToBackend = async (
  716. exportedElements: readonly NonDeletedExcalidrawElement[],
  717. appState: Partial<AppState>,
  718. files: BinaryFiles,
  719. ) => {
  720. if (exportedElements.length === 0) {
  721. throw new Error(t("alerts.cannotExportEmptyCanvas"));
  722. }
  723. try {
  724. const { url, errorMessage } = await exportToBackend(
  725. exportedElements,
  726. {
  727. ...appState,
  728. viewBackgroundColor: appState.exportBackground
  729. ? appState.viewBackgroundColor
  730. : getDefaultAppState().viewBackgroundColor,
  731. },
  732. files,
  733. );
  734. if (errorMessage) {
  735. throw new Error(errorMessage);
  736. }
  737. if (url) {
  738. setLatestShareableLink(url);
  739. }
  740. } catch (error: any) {
  741. if (error.name !== "AbortError") {
  742. const { width, height } = appState;
  743. console.error(error, {
  744. width,
  745. height,
  746. devicePixelRatio: window.devicePixelRatio,
  747. });
  748. throw new Error(error.message);
  749. }
  750. }
  751. };
  752. const renderCustomStats = (
  753. elements: readonly NonDeletedExcalidrawElement[],
  754. appState: UIAppState,
  755. ) => {
  756. return (
  757. <CustomStats
  758. setToast={(message) => excalidrawAPI!.setToast({ message })}
  759. appState={appState}
  760. elements={elements}
  761. />
  762. );
  763. };
  764. const isOffline = useAtomValue(isOfflineAtom);
  765. const onCollabDialogOpen = useCallback(
  766. () => setShareDialogState({ isOpen: true, type: "collaborationOnly" }),
  767. [setShareDialogState],
  768. );
  769. // browsers generally prevent infinite self-embedding, there are
  770. // cases where it still happens, and while we disallow self-embedding
  771. // by not whitelisting our own origin, this serves as an additional guard
  772. if (isSelfEmbedding) {
  773. return (
  774. <div
  775. style={{
  776. display: "flex",
  777. alignItems: "center",
  778. justifyContent: "center",
  779. textAlign: "center",
  780. height: "100%",
  781. }}
  782. >
  783. <h1>I'm not a pretzel!</h1>
  784. </div>
  785. );
  786. }
  787. const ExcalidrawPlusCommand = {
  788. label: "Excalidraw+",
  789. category: DEFAULT_CATEGORIES.links,
  790. predicate: true,
  791. icon: <div style={{ width: 14 }}>{ExcalLogo}</div>,
  792. keywords: ["plus", "cloud", "server"],
  793. perform: () => {
  794. window.open(
  795. `${
  796. import.meta.env.VITE_APP_PLUS_LP
  797. }/plus?utm_source=excalidraw&utm_medium=app&utm_content=command_palette`,
  798. "_blank",
  799. );
  800. },
  801. };
  802. const ExcalidrawPlusAppCommand = {
  803. label: "Sign up",
  804. category: DEFAULT_CATEGORIES.links,
  805. predicate: true,
  806. icon: <div style={{ width: 14 }}>{ExcalLogo}</div>,
  807. keywords: [
  808. "excalidraw",
  809. "plus",
  810. "cloud",
  811. "server",
  812. "signin",
  813. "login",
  814. "signup",
  815. ],
  816. perform: () => {
  817. window.open(
  818. `${
  819. import.meta.env.VITE_APP_PLUS_APP
  820. }?utm_source=excalidraw&utm_medium=app&utm_content=command_palette`,
  821. "_blank",
  822. );
  823. },
  824. };
  825. return (
  826. <div
  827. style={{ height: "100%" }}
  828. className={clsx("excalidraw-app", {
  829. "is-collaborating": isCollaborating,
  830. })}
  831. >
  832. <Excalidraw
  833. excalidrawAPI={excalidrawRefCallback}
  834. onChange={onChange}
  835. initialData={initialStatePromiseRef.current.promise}
  836. isCollaborating={isCollaborating}
  837. onPointerUpdate={collabAPI?.onPointerUpdate}
  838. UIOptions={{
  839. canvasActions: {
  840. toggleTheme: true,
  841. export: {
  842. onExportToBackend,
  843. renderCustomUI: excalidrawAPI
  844. ? (elements, appState, files) => {
  845. return (
  846. <ExportToExcalidrawPlus
  847. elements={elements}
  848. appState={appState}
  849. files={files}
  850. name={excalidrawAPI.getName()}
  851. onError={(error) => {
  852. excalidrawAPI?.updateScene({
  853. appState: {
  854. errorMessage: error.message,
  855. },
  856. });
  857. }}
  858. onSuccess={() => {
  859. excalidrawAPI.updateScene({
  860. appState: { openDialog: null },
  861. });
  862. }}
  863. />
  864. );
  865. }
  866. : undefined,
  867. },
  868. },
  869. }}
  870. langCode={langCode}
  871. renderCustomStats={renderCustomStats}
  872. detectScroll={false}
  873. handleKeyboardGlobally={true}
  874. autoFocus={true}
  875. theme={editorTheme}
  876. renderTopRightUI={(isMobile) => {
  877. if (isMobile || !collabAPI || isCollabDisabled) {
  878. return null;
  879. }
  880. return (
  881. <div className="top-right-ui">
  882. {collabError.message && <CollabError collabError={collabError} />}
  883. <LiveCollaborationTrigger
  884. isCollaborating={isCollaborating}
  885. onSelect={() =>
  886. setShareDialogState({ isOpen: true, type: "share" })
  887. }
  888. />
  889. </div>
  890. );
  891. }}
  892. >
  893. <AppMainMenu
  894. onCollabDialogOpen={onCollabDialogOpen}
  895. isCollaborating={isCollaborating}
  896. isCollabEnabled={!isCollabDisabled}
  897. theme={appTheme}
  898. setTheme={(theme) => setAppTheme(theme)}
  899. refresh={() => forceRefresh((prev) => !prev)}
  900. />
  901. <AppWelcomeScreen
  902. onCollabDialogOpen={onCollabDialogOpen}
  903. isCollabEnabled={!isCollabDisabled}
  904. />
  905. <OverwriteConfirmDialog>
  906. <OverwriteConfirmDialog.Actions.ExportToImage />
  907. <OverwriteConfirmDialog.Actions.SaveToDisk />
  908. {excalidrawAPI && (
  909. <OverwriteConfirmDialog.Action
  910. title={t("overwriteConfirm.action.excalidrawPlus.title")}
  911. actionLabel={t("overwriteConfirm.action.excalidrawPlus.button")}
  912. onClick={() => {
  913. exportToExcalidrawPlus(
  914. excalidrawAPI.getSceneElements(),
  915. excalidrawAPI.getAppState(),
  916. excalidrawAPI.getFiles(),
  917. excalidrawAPI.getName(),
  918. );
  919. }}
  920. >
  921. {t("overwriteConfirm.action.excalidrawPlus.description")}
  922. </OverwriteConfirmDialog.Action>
  923. )}
  924. </OverwriteConfirmDialog>
  925. <AppFooter onChange={() => excalidrawAPI?.refresh()} />
  926. {excalidrawAPI && <AIComponents excalidrawAPI={excalidrawAPI} />}
  927. <TTDDialogTrigger />
  928. {isCollaborating && isOffline && (
  929. <div className="collab-offline-warning">
  930. {t("alerts.collabOfflineWarning")}
  931. </div>
  932. )}
  933. {latestShareableLink && (
  934. <ShareableLinkDialog
  935. link={latestShareableLink}
  936. onCloseRequest={() => setLatestShareableLink(null)}
  937. setErrorMessage={setErrorMessage}
  938. />
  939. )}
  940. {excalidrawAPI && !isCollabDisabled && (
  941. <Collab excalidrawAPI={excalidrawAPI} />
  942. )}
  943. <ShareDialog
  944. collabAPI={collabAPI}
  945. onExportToBackend={async () => {
  946. if (excalidrawAPI) {
  947. try {
  948. await onExportToBackend(
  949. excalidrawAPI.getSceneElements(),
  950. excalidrawAPI.getAppState(),
  951. excalidrawAPI.getFiles(),
  952. );
  953. } catch (error: any) {
  954. setErrorMessage(error.message);
  955. }
  956. }
  957. }}
  958. />
  959. {errorMessage && (
  960. <ErrorDialog onClose={() => setErrorMessage("")}>
  961. {errorMessage}
  962. </ErrorDialog>
  963. )}
  964. <CommandPalette
  965. customCommandPaletteItems={[
  966. {
  967. label: t("labels.liveCollaboration"),
  968. category: DEFAULT_CATEGORIES.app,
  969. keywords: [
  970. "team",
  971. "multiplayer",
  972. "share",
  973. "public",
  974. "session",
  975. "invite",
  976. ],
  977. icon: usersIcon,
  978. perform: () => {
  979. setShareDialogState({
  980. isOpen: true,
  981. type: "collaborationOnly",
  982. });
  983. },
  984. },
  985. {
  986. label: t("roomDialog.button_stopSession"),
  987. category: DEFAULT_CATEGORIES.app,
  988. predicate: () => !!collabAPI?.isCollaborating(),
  989. keywords: [
  990. "stop",
  991. "session",
  992. "end",
  993. "leave",
  994. "close",
  995. "exit",
  996. "collaboration",
  997. ],
  998. perform: () => {
  999. if (collabAPI) {
  1000. collabAPI.stopCollaboration();
  1001. if (!collabAPI.isCollaborating()) {
  1002. setShareDialogState({ isOpen: false });
  1003. }
  1004. }
  1005. },
  1006. },
  1007. {
  1008. label: t("labels.share"),
  1009. category: DEFAULT_CATEGORIES.app,
  1010. predicate: true,
  1011. icon: share,
  1012. keywords: [
  1013. "link",
  1014. "shareable",
  1015. "readonly",
  1016. "export",
  1017. "publish",
  1018. "snapshot",
  1019. "url",
  1020. "collaborate",
  1021. "invite",
  1022. ],
  1023. perform: async () => {
  1024. setShareDialogState({ isOpen: true, type: "share" });
  1025. },
  1026. },
  1027. {
  1028. label: "GitHub",
  1029. icon: GithubIcon,
  1030. category: DEFAULT_CATEGORIES.links,
  1031. predicate: true,
  1032. keywords: [
  1033. "issues",
  1034. "bugs",
  1035. "requests",
  1036. "report",
  1037. "features",
  1038. "social",
  1039. "community",
  1040. ],
  1041. perform: () => {
  1042. window.open(
  1043. "https://github.com/excalidraw/excalidraw",
  1044. "_blank",
  1045. "noopener noreferrer",
  1046. );
  1047. },
  1048. },
  1049. {
  1050. label: t("labels.followUs"),
  1051. icon: XBrandIcon,
  1052. category: DEFAULT_CATEGORIES.links,
  1053. predicate: true,
  1054. keywords: ["twitter", "contact", "social", "community"],
  1055. perform: () => {
  1056. window.open(
  1057. "https://x.com/excalidraw",
  1058. "_blank",
  1059. "noopener noreferrer",
  1060. );
  1061. },
  1062. },
  1063. {
  1064. label: t("labels.discordChat"),
  1065. category: DEFAULT_CATEGORIES.links,
  1066. predicate: true,
  1067. icon: DiscordIcon,
  1068. keywords: [
  1069. "chat",
  1070. "talk",
  1071. "contact",
  1072. "bugs",
  1073. "requests",
  1074. "report",
  1075. "feedback",
  1076. "suggestions",
  1077. "social",
  1078. "community",
  1079. ],
  1080. perform: () => {
  1081. window.open(
  1082. "https://discord.gg/UexuTaE",
  1083. "_blank",
  1084. "noopener noreferrer",
  1085. );
  1086. },
  1087. },
  1088. {
  1089. label: "YouTube",
  1090. icon: youtubeIcon,
  1091. category: DEFAULT_CATEGORIES.links,
  1092. predicate: true,
  1093. keywords: ["features", "tutorials", "howto", "help", "community"],
  1094. perform: () => {
  1095. window.open(
  1096. "https://youtube.com/@excalidraw",
  1097. "_blank",
  1098. "noopener noreferrer",
  1099. );
  1100. },
  1101. },
  1102. ...(isExcalidrawPlusSignedUser
  1103. ? [
  1104. {
  1105. ...ExcalidrawPlusAppCommand,
  1106. label: "Sign in / Go to Excalidraw+",
  1107. },
  1108. ]
  1109. : [ExcalidrawPlusCommand, ExcalidrawPlusAppCommand]),
  1110. {
  1111. label: t("overwriteConfirm.action.excalidrawPlus.button"),
  1112. category: DEFAULT_CATEGORIES.export,
  1113. icon: exportToPlus,
  1114. predicate: true,
  1115. keywords: ["plus", "export", "save", "backup"],
  1116. perform: () => {
  1117. if (excalidrawAPI) {
  1118. exportToExcalidrawPlus(
  1119. excalidrawAPI.getSceneElements(),
  1120. excalidrawAPI.getAppState(),
  1121. excalidrawAPI.getFiles(),
  1122. excalidrawAPI.getName(),
  1123. );
  1124. }
  1125. },
  1126. },
  1127. {
  1128. ...CommandPalette.defaultItems.toggleTheme,
  1129. perform: () => {
  1130. setAppTheme(
  1131. editorTheme === THEME.DARK ? THEME.LIGHT : THEME.DARK,
  1132. );
  1133. },
  1134. },
  1135. {
  1136. label: t("labels.installPWA"),
  1137. category: DEFAULT_CATEGORIES.app,
  1138. predicate: () => !!pwaEvent,
  1139. perform: () => {
  1140. if (pwaEvent) {
  1141. pwaEvent.prompt();
  1142. pwaEvent.userChoice.then(() => {
  1143. // event cannot be reused, but we'll hopefully
  1144. // grab new one as the event should be fired again
  1145. pwaEvent = null;
  1146. });
  1147. }
  1148. },
  1149. },
  1150. ]}
  1151. />
  1152. {isVisualDebuggerEnabled() && excalidrawAPI && (
  1153. <DebugCanvas
  1154. appState={excalidrawAPI.getAppState()}
  1155. scale={window.devicePixelRatio}
  1156. ref={debugCanvasRef}
  1157. />
  1158. )}
  1159. </Excalidraw>
  1160. <div
  1161. style={{
  1162. display: "flex",
  1163. flexDirection: "column",
  1164. position: "fixed",
  1165. bottom: 60,
  1166. right: 60,
  1167. zIndex: 9999999999,
  1168. color: "black",
  1169. }}
  1170. >
  1171. <div style={{ display: "flex", gap: "1rem", flexDirection: "column" }}>
  1172. <div style={{ display: "flex", gap: "1rem" }}>
  1173. <label>
  1174. center{" "}
  1175. <input
  1176. type="checkbox"
  1177. checked={config.position === "center"}
  1178. onChange={() =>
  1179. setConfig((s) => ({
  1180. ...s,
  1181. position: s.position === "center" ? "topLeft" : "center",
  1182. }))
  1183. }
  1184. />
  1185. </label>
  1186. <label>
  1187. fit{" "}
  1188. <select
  1189. value={config.fit}
  1190. onChange={(event) =>
  1191. setConfig((s) => ({
  1192. ...s,
  1193. fit: event.target.value as any,
  1194. }))
  1195. }
  1196. >
  1197. <option value="none">none</option>
  1198. <option value="contain">contain</option>
  1199. <option value="cover">cover</option>
  1200. </select>
  1201. </label>
  1202. <label>
  1203. padding{" "}
  1204. <input
  1205. type="number"
  1206. max={600}
  1207. style={{ width: "3rem" }}
  1208. value={config.padding}
  1209. onChange={(event) =>
  1210. setConfig((s) => ({
  1211. ...s,
  1212. padding: !event.target.value.trim()
  1213. ? undefined
  1214. : Math.min(parseInt(event.target.value as any), 600),
  1215. }))
  1216. }
  1217. />
  1218. </label>
  1219. <label>
  1220. scale{" "}
  1221. <input
  1222. type="number"
  1223. max={4}
  1224. style={{ width: "3rem" }}
  1225. value={config.scale}
  1226. onChange={(event) =>
  1227. setConfig((s) => ({
  1228. ...s,
  1229. scale: !event.target.value.trim()
  1230. ? undefined
  1231. : Math.min(parseFloat(event.target.value as any), 4),
  1232. }))
  1233. }
  1234. />
  1235. </label>
  1236. </div>
  1237. <div style={{ display: "flex", gap: "1rem" }}>
  1238. <label
  1239. style={{
  1240. opacity:
  1241. config.maxWidthOrHeight != null ||
  1242. config.widthOrHeight != null
  1243. ? 0.5
  1244. : undefined,
  1245. }}
  1246. >
  1247. width{" "}
  1248. <input
  1249. type="number"
  1250. max={600}
  1251. style={{ width: "3rem" }}
  1252. value={config.width}
  1253. onChange={(event) =>
  1254. setConfig((s) => ({
  1255. ...s,
  1256. width: !event.target.value.trim()
  1257. ? undefined
  1258. : Math.min(parseInt(event.target.value as any), 600),
  1259. }))
  1260. }
  1261. />
  1262. </label>
  1263. <label
  1264. style={{
  1265. opacity:
  1266. config.maxWidthOrHeight != null ||
  1267. config.widthOrHeight != null
  1268. ? 0.5
  1269. : undefined,
  1270. }}
  1271. >
  1272. height{" "}
  1273. <input
  1274. type="number"
  1275. max={600}
  1276. style={{ width: "3rem" }}
  1277. value={config.height}
  1278. onChange={(event) =>
  1279. setConfig((s) => ({
  1280. ...s,
  1281. height: !event.target.value.trim()
  1282. ? undefined
  1283. : Math.min(parseInt(event.target.value as any), 600),
  1284. }))
  1285. }
  1286. />
  1287. </label>
  1288. <label>
  1289. x{" "}
  1290. <input
  1291. type="number"
  1292. style={{ width: "3rem" }}
  1293. value={config.x}
  1294. onChange={(event) =>
  1295. setConfig((s) => ({
  1296. ...s,
  1297. x: !event.target.value.trim()
  1298. ? undefined
  1299. : parseFloat(event.target.value as any) ?? undefined,
  1300. }))
  1301. }
  1302. />
  1303. </label>
  1304. <label>
  1305. y{" "}
  1306. <input
  1307. type="number"
  1308. style={{ width: "3rem" }}
  1309. value={config.y}
  1310. onChange={(event) =>
  1311. setConfig((s) => ({
  1312. ...s,
  1313. y: !event.target.value.trim()
  1314. ? undefined
  1315. : parseFloat(event.target.value as any) ?? undefined,
  1316. }))
  1317. }
  1318. />
  1319. </label>
  1320. <label
  1321. style={{
  1322. opacity: config.widthOrHeight != null ? 0.5 : undefined,
  1323. }}
  1324. >
  1325. maxWH{" "}
  1326. <input
  1327. type="number"
  1328. // max={600}
  1329. style={{ width: "3rem" }}
  1330. value={config.maxWidthOrHeight}
  1331. onChange={(event) =>
  1332. setConfig((s) => ({
  1333. ...s,
  1334. maxWidthOrHeight: !event.target.value.trim()
  1335. ? undefined
  1336. : parseInt(event.target.value as any),
  1337. }))
  1338. }
  1339. />
  1340. </label>
  1341. <label>
  1342. widthOrHeight{" "}
  1343. <input
  1344. type="number"
  1345. max={600}
  1346. style={{ width: "3rem" }}
  1347. value={config.widthOrHeight}
  1348. onChange={(event) =>
  1349. setConfig((s) => ({
  1350. ...s,
  1351. widthOrHeight: !event.target.value.trim()
  1352. ? undefined
  1353. : Math.min(parseInt(event.target.value as any), 600),
  1354. }))
  1355. }
  1356. />
  1357. </label>
  1358. </div>
  1359. </div>
  1360. <div className="dims">0x0</div>
  1361. <div
  1362. ref={canvasPreviewContainerRef}
  1363. onClick={() => {
  1364. exportToBlob({
  1365. data: {
  1366. elements: excalidrawAPI!.getSceneElements(),
  1367. files: excalidrawAPI?.getFiles() || null,
  1368. },
  1369. config,
  1370. }).then((blob) => {
  1371. fileSave(blob, {
  1372. name: "xx",
  1373. extension: "png",
  1374. description: "xxx",
  1375. });
  1376. });
  1377. }}
  1378. style={{
  1379. borderRadius: 12,
  1380. border: "1px solid #777",
  1381. overflow: "hidden",
  1382. padding: 10,
  1383. backgroundColor: "pink",
  1384. }}
  1385. />
  1386. </div>
  1387. </div>
  1388. );
  1389. };
  1390. const ExcalidrawApp = () => {
  1391. const isCloudExportWindow =
  1392. window.location.pathname === "/excalidraw-plus-export";
  1393. if (isCloudExportWindow) {
  1394. return <ExcalidrawPlusIframeExport />;
  1395. }
  1396. return (
  1397. <TopErrorBoundary>
  1398. <Provider unstable_createStore={() => appJotaiStore}>
  1399. <ExcalidrawWrapper />
  1400. </Provider>
  1401. </TopErrorBoundary>
  1402. );
  1403. };
  1404. export default ExcalidrawApp;