App.tsx 45 KB

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