2021-01-05 20:06:14 +02:00
|
|
|
import LanguageDetector from "i18next-browser-languagedetector";
|
2021-10-14 22:56:51 +05:30
|
|
|
import { useCallback, useContext, useEffect, useRef, useState } from "react";
|
2021-01-10 20:48:12 +02:00
|
|
|
import { trackEvent } from "../analytics";
|
2021-01-05 20:06:14 +02:00
|
|
|
import { getDefaultAppState } from "../appState";
|
|
|
|
import { ErrorDialog } from "../components/ErrorDialog";
|
|
|
|
import { TopErrorBoundary } from "../components/TopErrorBoundary";
|
2022-05-11 15:08:54 +02:00
|
|
|
import { APP_NAME, EVENT, TITLE_TIMEOUT, VERSION_TIMEOUT } from "../constants";
|
2021-03-08 16:37:26 +01:00
|
|
|
import { loadFromBlob } from "../data/blob";
|
2021-01-05 20:06:14 +02:00
|
|
|
import {
|
|
|
|
ExcalidrawElement,
|
2021-10-21 22:05:48 +02:00
|
|
|
FileId,
|
2021-01-05 20:06:14 +02:00
|
|
|
NonDeletedExcalidrawElement,
|
|
|
|
} from "../element/types";
|
2021-01-25 10:47:35 +01:00
|
|
|
import { useCallbackRefState } from "../hooks/useCallbackRefState";
|
2021-01-05 20:06:14 +02:00
|
|
|
import { Language, t } from "../i18n";
|
2022-04-22 15:49:36 +02:00
|
|
|
import {
|
|
|
|
Excalidraw,
|
2021-01-04 02:21:52 +05:30
|
|
|
defaultLang,
|
2021-01-05 20:06:14 +02:00
|
|
|
languages,
|
2021-01-04 02:21:52 +05:30
|
|
|
} from "../packages/excalidraw/index";
|
2021-10-21 22:05:48 +02:00
|
|
|
import {
|
|
|
|
AppState,
|
|
|
|
LibraryItems,
|
|
|
|
ExcalidrawImperativeAPI,
|
|
|
|
BinaryFiles,
|
2022-04-20 14:40:03 +02:00
|
|
|
ExcalidrawInitialDataState,
|
2021-10-21 22:05:48 +02:00
|
|
|
} from "../types";
|
2021-01-10 20:48:12 +02:00
|
|
|
import {
|
|
|
|
debounce,
|
|
|
|
getVersion,
|
2022-03-28 14:46:40 +02:00
|
|
|
getFrame,
|
2022-01-27 17:51:55 +05:30
|
|
|
isTestEnv,
|
2021-10-21 22:05:48 +02:00
|
|
|
preventUnload,
|
2021-01-10 20:48:12 +02:00
|
|
|
ResolvablePromise,
|
|
|
|
resolvablePromise,
|
|
|
|
} from "../utils";
|
2021-10-21 22:05:48 +02:00
|
|
|
import {
|
|
|
|
FIREBASE_STORAGE_PREFIXES,
|
2022-01-27 17:51:55 +05:30
|
|
|
STORAGE_KEYS,
|
|
|
|
SYNC_BROWSER_TABS_TIMEOUT,
|
2021-10-21 22:05:48 +02:00
|
|
|
} from "./app_constants";
|
2021-01-25 10:47:35 +01:00
|
|
|
import CollabWrapper, {
|
|
|
|
CollabAPI,
|
|
|
|
CollabContext,
|
|
|
|
CollabContextConsumer,
|
|
|
|
} from "./collab/CollabWrapper";
|
2021-01-05 20:06:14 +02:00
|
|
|
import { LanguageList } from "./components/LanguageList";
|
|
|
|
import { exportToBackend, getCollaborationLinkData, loadScene } from "./data";
|
2020-10-25 19:39:57 +05:30
|
|
|
import {
|
2022-01-27 17:51:55 +05:30
|
|
|
getLibraryItemsFromStorage,
|
2020-10-25 19:39:57 +05:30
|
|
|
importFromLocalStorage,
|
2022-01-27 17:51:55 +05:30
|
|
|
importUsernameFromLocalStorage,
|
2020-12-05 20:00:53 +05:30
|
|
|
} from "./data/localStorage";
|
2021-03-29 20:06:34 +05:30
|
|
|
import CustomStats from "./CustomStats";
|
2021-05-14 17:52:56 +05:30
|
|
|
import { restoreAppState, RestoredDataState } from "../data/restore";
|
2021-05-15 14:49:58 +05:30
|
|
|
import { Tooltip } from "../components/Tooltip";
|
|
|
|
import { shield } from "../components/icons";
|
|
|
|
|
|
|
|
import "./index.scss";
|
2021-06-01 14:05:09 +02:00
|
|
|
import { ExportToExcalidrawPlus } from "./components/ExportToExcalidrawPlus";
|
2020-12-05 20:00:53 +05:30
|
|
|
|
2022-04-11 22:15:49 +02:00
|
|
|
import { updateStaleImageStatuses } from "./data/FileManager";
|
2021-10-30 23:40:35 +02:00
|
|
|
import { newElementWith } from "../element/mutateElement";
|
2021-10-21 22:05:48 +02:00
|
|
|
import { isInitializedImageElement } from "../element/typeChecks";
|
|
|
|
import { loadFilesFromFirebase } from "./data/firebase";
|
2022-04-11 22:15:49 +02:00
|
|
|
import { LocalData } from "./data/LocalData";
|
|
|
|
import { isBrowserStorageStateNewer } from "./data/tabSync";
|
2022-04-15 21:51:41 +05:30
|
|
|
import clsx from "clsx";
|
2022-05-11 15:08:54 +02:00
|
|
|
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
|
2021-10-21 22:05:48 +02:00
|
|
|
|
2021-01-04 02:21:52 +05:30
|
|
|
const languageDetector = new LanguageDetector();
|
|
|
|
languageDetector.init({
|
|
|
|
languageUtils: {
|
|
|
|
formatLanguageCode: (langCode: Language["code"]) => langCode,
|
|
|
|
isWhitelisted: () => true,
|
|
|
|
},
|
|
|
|
checkWhitelist: false,
|
|
|
|
});
|
2020-12-05 20:00:53 +05:30
|
|
|
|
|
|
|
const initializeScene = async (opts: {
|
2021-02-03 19:14:26 +01:00
|
|
|
collabAPI: CollabAPI;
|
2021-10-21 22:05:48 +02:00
|
|
|
}): Promise<
|
2022-04-20 14:40:03 +02:00
|
|
|
{ scene: ExcalidrawInitialDataState | null } & (
|
2021-10-21 22:05:48 +02:00
|
|
|
| { isExternalScene: true; id: string; key: string }
|
|
|
|
| { isExternalScene: false; id?: null; key?: null }
|
|
|
|
)
|
|
|
|
> => {
|
2020-12-05 20:00:53 +05:30
|
|
|
const searchParams = new URLSearchParams(window.location.search);
|
|
|
|
const id = searchParams.get("id");
|
2021-03-08 16:37:26 +01:00
|
|
|
const jsonBackendMatch = window.location.hash.match(
|
2021-11-07 15:37:13 +02:00
|
|
|
/^#json=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/,
|
2020-12-05 20:00:53 +05:30
|
|
|
);
|
2021-03-08 16:37:26 +01:00
|
|
|
const externalUrlMatch = window.location.hash.match(/^#url=(.*)$/);
|
2020-12-05 20:00:53 +05:30
|
|
|
|
2021-04-10 19:17:49 +02:00
|
|
|
const localDataState = importFromLocalStorage();
|
2020-12-05 20:00:53 +05:30
|
|
|
|
2021-04-10 19:17:49 +02:00
|
|
|
let scene: RestoredDataState & {
|
|
|
|
scrollToContent?: boolean;
|
|
|
|
} = await loadScene(null, null, localDataState);
|
2020-12-05 20:00:53 +05:30
|
|
|
|
2021-02-03 19:14:26 +01:00
|
|
|
let roomLinkData = getCollaborationLinkData(window.location.href);
|
2021-03-08 16:37:26 +01:00
|
|
|
const isExternalScene = !!(id || jsonBackendMatch || roomLinkData);
|
2020-12-05 20:00:53 +05:30
|
|
|
if (isExternalScene) {
|
2021-02-05 12:04:33 +01:00
|
|
|
if (
|
|
|
|
// don't prompt if scene is empty
|
|
|
|
!scene.elements.length ||
|
|
|
|
// don't prompt for collab scenes because we don't override local storage
|
|
|
|
roomLinkData ||
|
|
|
|
// otherwise, prompt whether user wants to override current scene
|
|
|
|
window.confirm(t("alerts.loadSceneOverridePrompt"))
|
|
|
|
) {
|
2021-11-02 14:52:25 +02:00
|
|
|
if (jsonBackendMatch) {
|
2021-03-08 16:37:26 +01:00
|
|
|
scene = await loadScene(
|
|
|
|
jsonBackendMatch[1],
|
|
|
|
jsonBackendMatch[2],
|
2021-04-10 19:17:49 +02:00
|
|
|
localDataState,
|
2021-03-08 16:37:26 +01:00
|
|
|
);
|
2020-12-05 20:00:53 +05:30
|
|
|
}
|
2021-03-16 23:02:17 +05:30
|
|
|
scene.scrollToContent = true;
|
2021-02-03 19:14:26 +01:00
|
|
|
if (!roomLinkData) {
|
2020-12-22 11:34:06 +02:00
|
|
|
window.history.replaceState({}, APP_NAME, window.location.origin);
|
2020-12-05 20:00:53 +05:30
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// https://github.com/excalidraw/excalidraw/issues/1919
|
|
|
|
if (document.hidden) {
|
2020-12-29 21:03:34 +01:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
window.addEventListener(
|
|
|
|
"focus",
|
|
|
|
() => initializeScene(opts).then(resolve).catch(reject),
|
|
|
|
{
|
|
|
|
once: true,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
});
|
2020-12-05 20:00:53 +05:30
|
|
|
}
|
|
|
|
|
2021-02-03 19:14:26 +01:00
|
|
|
roomLinkData = null;
|
2020-12-22 11:34:06 +02:00
|
|
|
window.history.replaceState({}, APP_NAME, window.location.origin);
|
2020-12-05 20:00:53 +05:30
|
|
|
}
|
2021-03-08 16:37:26 +01:00
|
|
|
} else if (externalUrlMatch) {
|
|
|
|
window.history.replaceState({}, APP_NAME, window.location.origin);
|
|
|
|
|
|
|
|
const url = externalUrlMatch[1];
|
|
|
|
try {
|
|
|
|
const request = await fetch(window.decodeURIComponent(url));
|
2021-07-04 22:23:35 +02:00
|
|
|
const data = await loadFromBlob(await request.blob(), null, null);
|
2021-03-08 16:37:26 +01:00
|
|
|
if (
|
|
|
|
!scene.elements.length ||
|
|
|
|
window.confirm(t("alerts.loadSceneOverridePrompt"))
|
|
|
|
) {
|
2021-10-21 22:05:48 +02:00
|
|
|
return { scene: data, isExternalScene };
|
2021-03-08 16:37:26 +01:00
|
|
|
}
|
2021-11-02 14:24:16 +02:00
|
|
|
} catch (error: any) {
|
2021-03-08 16:37:26 +01:00
|
|
|
return {
|
2021-10-21 22:05:48 +02:00
|
|
|
scene: {
|
|
|
|
appState: {
|
|
|
|
errorMessage: t("alerts.invalidSceneUrl"),
|
|
|
|
},
|
2021-03-08 16:37:26 +01:00
|
|
|
},
|
2021-10-21 22:05:48 +02:00
|
|
|
isExternalScene,
|
2021-03-08 16:37:26 +01:00
|
|
|
};
|
|
|
|
}
|
2020-12-05 20:00:53 +05:30
|
|
|
}
|
2021-03-08 16:37:26 +01:00
|
|
|
|
2021-02-03 19:14:26 +01:00
|
|
|
if (roomLinkData) {
|
2021-10-21 22:05:48 +02:00
|
|
|
return {
|
|
|
|
scene: await opts.collabAPI.initializeSocketClient(roomLinkData),
|
|
|
|
isExternalScene: true,
|
|
|
|
id: roomLinkData.roomId,
|
|
|
|
key: roomLinkData.roomKey,
|
|
|
|
};
|
2020-12-05 20:00:53 +05:30
|
|
|
} else if (scene) {
|
2021-10-21 22:05:48 +02:00
|
|
|
return isExternalScene && jsonBackendMatch
|
|
|
|
? {
|
|
|
|
scene,
|
|
|
|
isExternalScene,
|
|
|
|
id: jsonBackendMatch[1],
|
|
|
|
key: jsonBackendMatch[2],
|
|
|
|
}
|
|
|
|
: { scene, isExternalScene: false };
|
2020-12-05 20:00:53 +05:30
|
|
|
}
|
2021-10-21 22:05:48 +02:00
|
|
|
return { scene: null, isExternalScene: false };
|
2020-12-05 20:00:53 +05:30
|
|
|
};
|
|
|
|
|
2021-05-06 21:29:05 +02:00
|
|
|
const PlusLinkJSX = (
|
|
|
|
<p style={{ direction: "ltr", unicodeBidi: "embed" }}>
|
|
|
|
Introducing Excalidraw+
|
|
|
|
<br />
|
|
|
|
<a
|
2022-01-07 16:32:35 +01:00
|
|
|
href="https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=banner&utm_campaign=launch"
|
2021-05-06 21:29:05 +02:00
|
|
|
target="_blank"
|
|
|
|
rel="noreferrer"
|
|
|
|
>
|
|
|
|
Try out now!
|
|
|
|
</a>
|
|
|
|
</p>
|
|
|
|
);
|
|
|
|
|
2021-03-29 17:09:20 +03:00
|
|
|
const ExcalidrawWrapper = () => {
|
2020-12-20 19:44:04 +05:30
|
|
|
const [errorMessage, setErrorMessage] = useState("");
|
2021-11-03 23:07:13 +02:00
|
|
|
let currentLangCode = languageDetector.detect() || defaultLang.code;
|
|
|
|
if (Array.isArray(currentLangCode)) {
|
|
|
|
currentLangCode = currentLangCode[0];
|
|
|
|
}
|
2021-01-04 02:21:52 +05:30
|
|
|
const [langCode, setLangCode] = useState(currentLangCode);
|
2020-12-05 20:00:53 +05:30
|
|
|
// initial state
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
const initialStatePromiseRef = useRef<{
|
2022-04-20 14:40:03 +02:00
|
|
|
promise: ResolvablePromise<ExcalidrawInitialDataState | null>;
|
2020-12-05 20:00:53 +05:30
|
|
|
}>({ promise: null! });
|
|
|
|
if (!initialStatePromiseRef.current.promise) {
|
2021-11-01 15:24:05 +02:00
|
|
|
initialStatePromiseRef.current.promise =
|
2022-04-20 14:40:03 +02:00
|
|
|
resolvablePromise<ExcalidrawInitialDataState | null>();
|
2020-12-05 20:00:53 +05:30
|
|
|
}
|
|
|
|
|
2020-10-25 19:39:57 +05:30
|
|
|
useEffect(() => {
|
2022-03-28 14:46:40 +02:00
|
|
|
trackEvent("load", "frame", getFrame());
|
2021-01-16 19:59:26 +02:00
|
|
|
// Delayed so that the app has a time to load the latest SW
|
2021-01-12 17:47:31 +01:00
|
|
|
setTimeout(() => {
|
2021-01-16 19:59:26 +02:00
|
|
|
trackEvent("load", "version", getVersion());
|
|
|
|
}, VERSION_TIMEOUT);
|
2021-01-25 10:47:35 +01:00
|
|
|
}, []);
|
2021-01-12 17:47:31 +01:00
|
|
|
|
2021-11-01 15:24:05 +02:00
|
|
|
const [excalidrawAPI, excalidrawRefCallback] =
|
|
|
|
useCallbackRefState<ExcalidrawImperativeAPI>();
|
2021-01-25 10:47:35 +01:00
|
|
|
|
|
|
|
const collabAPI = useContext(CollabContext)?.api;
|
|
|
|
|
2022-05-11 15:08:54 +02:00
|
|
|
useHandleLibrary({
|
|
|
|
excalidrawAPI,
|
|
|
|
getInitialLibraryItems: getLibraryItemsFromStorage,
|
|
|
|
});
|
|
|
|
|
2021-01-25 10:47:35 +01:00
|
|
|
useEffect(() => {
|
|
|
|
if (!collabAPI || !excalidrawAPI) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-10-21 22:05:48 +02:00
|
|
|
const loadImages = (
|
|
|
|
data: ResolutionType<typeof initializeScene>,
|
|
|
|
isInitialLoad = false,
|
|
|
|
) => {
|
|
|
|
if (!data.scene) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (collabAPI.isCollaborating()) {
|
|
|
|
if (data.scene.elements) {
|
|
|
|
collabAPI
|
|
|
|
.fetchImageFilesFromFirebase({
|
|
|
|
elements: data.scene.elements,
|
|
|
|
})
|
|
|
|
.then(({ loadedFiles, erroredFiles }) => {
|
|
|
|
excalidrawAPI.addFiles(loadedFiles);
|
|
|
|
updateStaleImageStatuses({
|
|
|
|
excalidrawAPI,
|
|
|
|
erroredFiles,
|
|
|
|
elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
const fileIds =
|
|
|
|
data.scene.elements?.reduce((acc, element) => {
|
|
|
|
if (isInitializedImageElement(element)) {
|
|
|
|
return acc.concat(element.fileId);
|
|
|
|
}
|
|
|
|
return acc;
|
|
|
|
}, [] as FileId[]) || [];
|
|
|
|
|
|
|
|
if (data.isExternalScene) {
|
|
|
|
loadFilesFromFirebase(
|
|
|
|
`${FIREBASE_STORAGE_PREFIXES.shareLinkFiles}/${data.id}`,
|
|
|
|
data.key,
|
|
|
|
fileIds,
|
|
|
|
).then(({ loadedFiles, erroredFiles }) => {
|
|
|
|
excalidrawAPI.addFiles(loadedFiles);
|
|
|
|
updateStaleImageStatuses({
|
|
|
|
excalidrawAPI,
|
|
|
|
erroredFiles,
|
|
|
|
elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
} else if (isInitialLoad) {
|
|
|
|
if (fileIds.length) {
|
2022-04-11 22:15:49 +02:00
|
|
|
LocalData.fileStorage
|
2021-10-21 22:05:48 +02:00
|
|
|
.getFiles(fileIds)
|
|
|
|
.then(({ loadedFiles, erroredFiles }) => {
|
|
|
|
if (loadedFiles.length) {
|
|
|
|
excalidrawAPI.addFiles(loadedFiles);
|
|
|
|
}
|
|
|
|
updateStaleImageStatuses({
|
|
|
|
excalidrawAPI,
|
|
|
|
erroredFiles,
|
|
|
|
elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
// on fresh load, clear unused files from IDB (from previous
|
|
|
|
// session)
|
2022-04-11 22:15:49 +02:00
|
|
|
LocalData.fileStorage.clearObsoleteFiles({ currentFileIds: fileIds });
|
2021-04-21 23:38:24 +05:30
|
|
|
}
|
|
|
|
}
|
2021-10-21 22:05:48 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
initializeScene({ collabAPI }).then((data) => {
|
|
|
|
loadImages(data, /* isInitialLoad */ true);
|
|
|
|
initialStatePromiseRef.current.promise.resolve(data.scene);
|
2020-10-25 19:39:57 +05:30
|
|
|
});
|
|
|
|
|
2022-05-11 15:08:54 +02:00
|
|
|
const onHashChange = async (event: HashChangeEvent) => {
|
2021-03-26 18:10:43 +01:00
|
|
|
event.preventDefault();
|
2022-05-11 15:08:54 +02:00
|
|
|
const libraryUrlTokens = parseLibraryTokensFromUrl();
|
|
|
|
if (!libraryUrlTokens) {
|
2021-10-21 22:05:48 +02:00
|
|
|
initializeScene({ collabAPI }).then((data) => {
|
|
|
|
loadImages(data);
|
|
|
|
if (data.scene) {
|
2021-05-14 17:52:56 +05:30
|
|
|
excalidrawAPI.updateScene({
|
2021-10-21 22:05:48 +02:00
|
|
|
...data.scene,
|
|
|
|
appState: restoreAppState(data.scene.appState, null),
|
2021-05-14 17:52:56 +05:30
|
|
|
});
|
2021-03-26 18:10:43 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2020-12-05 20:00:53 +05:30
|
|
|
};
|
|
|
|
|
2020-12-22 11:34:06 +02:00
|
|
|
const titleTimeout = setTimeout(
|
|
|
|
() => (document.title = APP_NAME),
|
|
|
|
TITLE_TIMEOUT,
|
|
|
|
);
|
2022-01-27 17:51:55 +05:30
|
|
|
|
|
|
|
const syncData = debounce(() => {
|
|
|
|
if (isTestEnv()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!document.hidden && !collabAPI.isCollaborating()) {
|
|
|
|
// don't sync if local state is newer or identical to browser state
|
|
|
|
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {
|
|
|
|
const localDataState = importFromLocalStorage();
|
|
|
|
const username = importUsernameFromLocalStorage();
|
|
|
|
let langCode = languageDetector.detect() || defaultLang.code;
|
|
|
|
if (Array.isArray(langCode)) {
|
|
|
|
langCode = langCode[0];
|
|
|
|
}
|
|
|
|
setLangCode(langCode);
|
|
|
|
excalidrawAPI.updateScene({
|
|
|
|
...localDataState,
|
2022-05-11 15:08:54 +02:00
|
|
|
});
|
|
|
|
excalidrawAPI.updateLibrary({
|
2022-01-27 17:51:55 +05:30
|
|
|
libraryItems: getLibraryItemsFromStorage(),
|
|
|
|
});
|
|
|
|
collabAPI.setUsername(username || "");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_FILES)) {
|
|
|
|
const elements = excalidrawAPI.getSceneElementsIncludingDeleted();
|
|
|
|
const currFiles = excalidrawAPI.getFiles();
|
|
|
|
const fileIds =
|
|
|
|
elements?.reduce((acc, element) => {
|
|
|
|
if (
|
|
|
|
isInitializedImageElement(element) &&
|
|
|
|
// only load and update images that aren't already loaded
|
|
|
|
!currFiles[element.fileId]
|
|
|
|
) {
|
|
|
|
return acc.concat(element.fileId);
|
|
|
|
}
|
|
|
|
return acc;
|
|
|
|
}, [] as FileId[]) || [];
|
|
|
|
if (fileIds.length) {
|
2022-04-11 22:15:49 +02:00
|
|
|
LocalData.fileStorage
|
2022-01-27 17:51:55 +05:30
|
|
|
.getFiles(fileIds)
|
|
|
|
.then(({ loadedFiles, erroredFiles }) => {
|
|
|
|
if (loadedFiles.length) {
|
|
|
|
excalidrawAPI.addFiles(loadedFiles);
|
|
|
|
}
|
|
|
|
updateStaleImageStatuses({
|
|
|
|
excalidrawAPI,
|
|
|
|
erroredFiles,
|
|
|
|
elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, SYNC_BROWSER_TABS_TIMEOUT);
|
|
|
|
|
2022-04-11 22:15:49 +02:00
|
|
|
const onUnload = () => {
|
|
|
|
LocalData.flushSave();
|
|
|
|
};
|
|
|
|
|
|
|
|
const visibilityChange = (event: FocusEvent | Event) => {
|
|
|
|
if (event.type === EVENT.BLUR || document.hidden) {
|
|
|
|
LocalData.flushSave();
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
event.type === EVENT.VISIBILITY_CHANGE ||
|
|
|
|
event.type === EVENT.FOCUS
|
|
|
|
) {
|
|
|
|
syncData();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-12-05 20:00:53 +05:30
|
|
|
window.addEventListener(EVENT.HASHCHANGE, onHashChange, false);
|
2022-04-11 22:15:49 +02:00
|
|
|
window.addEventListener(EVENT.UNLOAD, onUnload, false);
|
|
|
|
window.addEventListener(EVENT.BLUR, visibilityChange, false);
|
|
|
|
document.addEventListener(EVENT.VISIBILITY_CHANGE, visibilityChange, false);
|
|
|
|
window.addEventListener(EVENT.FOCUS, visibilityChange, false);
|
2020-10-25 19:39:57 +05:30
|
|
|
return () => {
|
2020-12-05 20:00:53 +05:30
|
|
|
window.removeEventListener(EVENT.HASHCHANGE, onHashChange, false);
|
2022-04-11 22:15:49 +02:00
|
|
|
window.removeEventListener(EVENT.UNLOAD, onUnload, false);
|
|
|
|
window.removeEventListener(EVENT.BLUR, visibilityChange, false);
|
|
|
|
window.removeEventListener(EVENT.FOCUS, visibilityChange, false);
|
|
|
|
document.removeEventListener(
|
|
|
|
EVENT.VISIBILITY_CHANGE,
|
|
|
|
visibilityChange,
|
|
|
|
false,
|
|
|
|
);
|
2020-12-22 11:34:06 +02:00
|
|
|
clearTimeout(titleTimeout);
|
2020-10-25 19:39:57 +05:30
|
|
|
};
|
2021-01-25 10:47:35 +01:00
|
|
|
}, [collabAPI, excalidrawAPI]);
|
2020-10-25 19:39:57 +05:30
|
|
|
|
2021-10-21 22:05:48 +02:00
|
|
|
useEffect(() => {
|
|
|
|
const unloadHandler = (event: BeforeUnloadEvent) => {
|
2022-04-11 22:15:49 +02:00
|
|
|
LocalData.flushSave();
|
2021-10-21 22:05:48 +02:00
|
|
|
|
|
|
|
if (
|
|
|
|
excalidrawAPI &&
|
2022-04-11 22:15:49 +02:00
|
|
|
LocalData.fileStorage.shouldPreventUnload(
|
|
|
|
excalidrawAPI.getSceneElements(),
|
|
|
|
)
|
2021-10-21 22:05:48 +02:00
|
|
|
) {
|
|
|
|
preventUnload(event);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
window.addEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
|
|
|
|
return () => {
|
|
|
|
window.removeEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
|
|
|
|
};
|
|
|
|
}, [excalidrawAPI]);
|
|
|
|
|
2021-01-04 02:21:52 +05:30
|
|
|
useEffect(() => {
|
|
|
|
languageDetector.cacheUserLanguage(langCode);
|
|
|
|
}, [langCode]);
|
|
|
|
|
2020-12-05 20:00:53 +05:30
|
|
|
const onChange = (
|
|
|
|
elements: readonly ExcalidrawElement[],
|
|
|
|
appState: AppState,
|
2021-10-21 22:05:48 +02:00
|
|
|
files: BinaryFiles,
|
2020-12-05 20:00:53 +05:30
|
|
|
) => {
|
2021-02-03 19:14:26 +01:00
|
|
|
if (collabAPI?.isCollaborating()) {
|
2022-04-17 22:40:39 +02:00
|
|
|
collabAPI.syncElements(elements);
|
2022-04-11 22:15:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// this check is redundant, but since this is a hot path, it's best
|
|
|
|
// not to evaludate the nested expression every time
|
|
|
|
if (!LocalData.isSavePaused()) {
|
|
|
|
LocalData.save(elements, appState, files, () => {
|
2021-10-21 22:05:48 +02:00
|
|
|
if (excalidrawAPI) {
|
|
|
|
let didChange = false;
|
|
|
|
|
2021-11-01 10:44:57 +01:00
|
|
|
let pendingImageElement = appState.pendingImageElement;
|
2021-10-21 22:05:48 +02:00
|
|
|
const elements = excalidrawAPI
|
|
|
|
.getSceneElementsIncludingDeleted()
|
|
|
|
.map((element) => {
|
2022-04-11 22:15:49 +02:00
|
|
|
if (
|
|
|
|
LocalData.fileStorage.shouldUpdateImageElementStatus(element)
|
|
|
|
) {
|
2021-10-21 22:05:48 +02:00
|
|
|
didChange = true;
|
2021-11-01 10:44:57 +01:00
|
|
|
const newEl = newElementWith(element, { status: "saved" });
|
|
|
|
if (pendingImageElement === element) {
|
|
|
|
pendingImageElement = newEl;
|
|
|
|
}
|
|
|
|
return newEl;
|
2021-10-21 22:05:48 +02:00
|
|
|
}
|
|
|
|
return element;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (didChange) {
|
|
|
|
excalidrawAPI.updateScene({
|
|
|
|
elements,
|
2021-11-01 10:44:57 +01:00
|
|
|
appState: {
|
|
|
|
pendingImageElement,
|
|
|
|
},
|
2021-10-21 22:05:48 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2020-12-05 20:00:53 +05:30
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-12-20 19:44:04 +05:30
|
|
|
const onExportToBackend = async (
|
|
|
|
exportedElements: readonly NonDeletedExcalidrawElement[],
|
|
|
|
appState: AppState,
|
2021-10-21 22:05:48 +02:00
|
|
|
files: BinaryFiles,
|
2020-12-20 19:44:04 +05:30
|
|
|
canvas: HTMLCanvasElement | null,
|
|
|
|
) => {
|
|
|
|
if (exportedElements.length === 0) {
|
|
|
|
return window.alert(t("alerts.cannotExportEmptyCanvas"));
|
|
|
|
}
|
|
|
|
if (canvas) {
|
|
|
|
try {
|
2021-10-21 22:05:48 +02:00
|
|
|
await exportToBackend(
|
|
|
|
exportedElements,
|
|
|
|
{
|
|
|
|
...appState,
|
|
|
|
viewBackgroundColor: appState.exportBackground
|
|
|
|
? appState.viewBackgroundColor
|
|
|
|
: getDefaultAppState().viewBackgroundColor,
|
|
|
|
},
|
|
|
|
files,
|
|
|
|
);
|
2021-11-02 14:24:16 +02:00
|
|
|
} catch (error: any) {
|
2020-12-20 19:44:04 +05:30
|
|
|
if (error.name !== "AbortError") {
|
|
|
|
const { width, height } = canvas;
|
|
|
|
console.error(error, { width, height });
|
|
|
|
setErrorMessage(error.message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
2021-01-04 02:21:52 +05:30
|
|
|
|
2021-05-13 21:02:59 +05:30
|
|
|
const renderTopRightUI = useCallback(
|
2021-05-06 21:00:17 +02:00
|
|
|
(isMobile: boolean, appState: AppState) => {
|
2021-10-17 21:44:46 +05:30
|
|
|
if (isMobile) {
|
|
|
|
return null;
|
|
|
|
}
|
2021-05-06 21:00:17 +02:00
|
|
|
return (
|
2021-05-06 21:29:05 +02:00
|
|
|
<div
|
|
|
|
style={{
|
|
|
|
width: "24ch",
|
|
|
|
fontSize: "0.7em",
|
|
|
|
textAlign: "center",
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{/* <GitHubCorner theme={appState.theme} dir={document.dir} /> */}
|
|
|
|
{/* FIXME remove after 2021-05-20 */}
|
|
|
|
{PlusLinkJSX}
|
2021-05-06 21:00:17 +02:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
[],
|
|
|
|
);
|
|
|
|
|
2021-01-04 02:21:52 +05:30
|
|
|
const renderFooter = useCallback(
|
|
|
|
(isMobile: boolean) => {
|
2021-05-15 14:49:58 +05:30
|
|
|
const renderEncryptedIcon = () => (
|
|
|
|
<a
|
|
|
|
className="encrypted-icon tooltip"
|
|
|
|
href="https://blog.excalidraw.com/end-to-end-encryption/"
|
|
|
|
target="_blank"
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
aria-label={t("encrypted.link")}
|
|
|
|
>
|
2021-05-25 13:52:04 +02:00
|
|
|
<Tooltip label={t("encrypted.tooltip")} long={true}>
|
2021-05-15 14:49:58 +05:30
|
|
|
{shield}
|
|
|
|
</Tooltip>
|
|
|
|
</a>
|
|
|
|
);
|
|
|
|
|
2021-01-04 02:21:52 +05:30
|
|
|
const renderLanguageList = () => (
|
|
|
|
<LanguageList
|
2021-07-15 18:48:03 +02:00
|
|
|
onChange={(langCode) => setLangCode(langCode)}
|
2021-01-04 02:21:52 +05:30
|
|
|
languages={languages}
|
|
|
|
currentLangCode={langCode}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
if (isMobile) {
|
2021-05-06 21:29:05 +02:00
|
|
|
const isTinyDevice = window.innerWidth < 362;
|
2021-01-04 02:21:52 +05:30
|
|
|
return (
|
2021-05-06 21:29:05 +02:00
|
|
|
<div
|
|
|
|
style={{
|
|
|
|
display: "flex",
|
|
|
|
flexDirection: isTinyDevice ? "column" : "row",
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<fieldset>
|
|
|
|
<legend>{t("labels.language")}</legend>
|
|
|
|
{renderLanguageList()}
|
|
|
|
</fieldset>
|
|
|
|
{/* FIXME remove after 2021-05-20 */}
|
|
|
|
<div
|
|
|
|
style={{
|
|
|
|
width: "24ch",
|
|
|
|
fontSize: "0.7em",
|
|
|
|
textAlign: "center",
|
|
|
|
marginTop: isTinyDevice ? 16 : undefined,
|
|
|
|
marginLeft: "auto",
|
|
|
|
marginRight: isTinyDevice ? "auto" : undefined,
|
|
|
|
padding: "4px 2px",
|
|
|
|
border: "1px dashed #aaa",
|
|
|
|
borderRadius: 12,
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{PlusLinkJSX}
|
|
|
|
</div>
|
|
|
|
</div>
|
2021-01-04 02:21:52 +05:30
|
|
|
);
|
|
|
|
}
|
2021-05-15 14:49:58 +05:30
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{renderEncryptedIcon()}
|
|
|
|
{renderLanguageList()}
|
|
|
|
</>
|
|
|
|
);
|
2021-01-04 02:21:52 +05:30
|
|
|
},
|
|
|
|
[langCode],
|
|
|
|
);
|
|
|
|
|
2021-03-29 20:06:34 +05:30
|
|
|
const renderCustomStats = () => {
|
|
|
|
return (
|
|
|
|
<CustomStats
|
|
|
|
setToastMessage={(message) => excalidrawAPI!.setToastMessage(message)}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2021-04-21 23:38:24 +05:30
|
|
|
const onLibraryChange = async (items: LibraryItems) => {
|
|
|
|
if (!items.length) {
|
|
|
|
localStorage.removeItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const serializedItems = JSON.stringify(items);
|
|
|
|
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems);
|
|
|
|
};
|
2021-05-06 21:00:17 +02:00
|
|
|
|
2021-10-21 22:05:48 +02:00
|
|
|
const onRoomClose = useCallback(() => {
|
2022-04-11 22:15:49 +02:00
|
|
|
LocalData.fileStorage.reset();
|
2021-10-21 22:05:48 +02:00
|
|
|
}, []);
|
|
|
|
|
2020-12-05 20:00:53 +05:30
|
|
|
return (
|
2022-04-15 21:51:41 +05:30
|
|
|
<div
|
|
|
|
style={{ height: "100%" }}
|
|
|
|
className={clsx("excalidraw-app", {
|
|
|
|
"is-collaborating": collabAPI?.isCollaborating(),
|
|
|
|
})}
|
|
|
|
>
|
2020-12-20 19:44:04 +05:30
|
|
|
<Excalidraw
|
2021-01-25 10:47:35 +01:00
|
|
|
ref={excalidrawRefCallback}
|
2020-12-20 19:44:04 +05:30
|
|
|
onChange={onChange}
|
|
|
|
initialData={initialStatePromiseRef.current.promise}
|
2021-01-25 10:47:35 +01:00
|
|
|
onCollabButtonClick={collabAPI?.onCollabButtonClick}
|
2021-02-03 19:14:26 +01:00
|
|
|
isCollaborating={collabAPI?.isCollaborating()}
|
2021-01-25 10:47:35 +01:00
|
|
|
onPointerUpdate={collabAPI?.onPointerUpdate}
|
2021-05-29 02:56:25 +05:30
|
|
|
UIOptions={{
|
|
|
|
canvasActions: {
|
2021-05-30 00:37:38 +05:30
|
|
|
export: {
|
|
|
|
onExportToBackend,
|
2021-10-21 22:05:48 +02:00
|
|
|
renderCustomUI: (elements, appState, files) => {
|
2021-06-01 14:05:09 +02:00
|
|
|
return (
|
|
|
|
<ExportToExcalidrawPlus
|
|
|
|
elements={elements}
|
|
|
|
appState={appState}
|
2021-10-21 22:05:48 +02:00
|
|
|
files={files}
|
2021-06-01 14:05:09 +02:00
|
|
|
onError={(error) => {
|
|
|
|
excalidrawAPI?.updateScene({
|
|
|
|
appState: {
|
|
|
|
errorMessage: error.message,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
},
|
2021-05-30 00:37:38 +05:30
|
|
|
},
|
2021-05-29 02:56:25 +05:30
|
|
|
},
|
|
|
|
}}
|
2021-05-13 21:02:59 +05:30
|
|
|
renderTopRightUI={renderTopRightUI}
|
2021-01-04 02:21:52 +05:30
|
|
|
renderFooter={renderFooter}
|
|
|
|
langCode={langCode}
|
2021-03-29 20:06:34 +05:30
|
|
|
renderCustomStats={renderCustomStats}
|
2021-04-09 20:44:54 +05:30
|
|
|
detectScroll={false}
|
2021-04-13 01:29:25 +05:30
|
|
|
handleKeyboardGlobally={true}
|
2021-04-21 23:38:24 +05:30
|
|
|
onLibraryChange={onLibraryChange}
|
2021-06-02 19:54:21 +05:30
|
|
|
autoFocus={true}
|
2020-12-20 19:44:04 +05:30
|
|
|
/>
|
2021-10-21 22:05:48 +02:00
|
|
|
{excalidrawAPI && (
|
|
|
|
<CollabWrapper
|
|
|
|
excalidrawAPI={excalidrawAPI}
|
|
|
|
onRoomClose={onRoomClose}
|
|
|
|
/>
|
|
|
|
)}
|
2020-12-20 19:44:04 +05:30
|
|
|
{errorMessage && (
|
|
|
|
<ErrorDialog
|
|
|
|
message={errorMessage}
|
|
|
|
onClose={() => setErrorMessage("")}
|
|
|
|
/>
|
|
|
|
)}
|
2022-04-15 21:51:41 +05:30
|
|
|
</div>
|
2020-12-05 20:00:53 +05:30
|
|
|
);
|
2021-03-29 17:09:20 +03:00
|
|
|
};
|
2020-12-05 20:00:53 +05:30
|
|
|
|
2021-03-29 17:09:20 +03:00
|
|
|
const ExcalidrawApp = () => {
|
2020-12-05 20:00:53 +05:30
|
|
|
return (
|
2020-10-25 19:39:57 +05:30
|
|
|
<TopErrorBoundary>
|
2021-01-25 10:47:35 +01:00
|
|
|
<CollabContextConsumer>
|
|
|
|
<ExcalidrawWrapper />
|
|
|
|
</CollabContextConsumer>
|
2020-10-25 19:39:57 +05:30
|
|
|
</TopErrorBoundary>
|
|
|
|
);
|
2021-03-29 17:09:20 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
export default ExcalidrawApp;
|