feat: Add onExportToBackend prop so host can handle it (#2612)
Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
b917e42694
commit
325d1bec91
@ -8,7 +8,7 @@ const changeLogCheck = () => {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stdout || stdout.includes("packages/excalidraw/CHANGELOG.MD")) {
|
if (!stdout || stdout.includes("packages/excalidraw/CHANGELOG.md")) {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -345,7 +345,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
offsetLeft,
|
offsetLeft,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const { onCollabButtonClick } = this.props;
|
const { onCollabButtonClick, onExportToBackend } = this.props;
|
||||||
const canvasScale = window.devicePixelRatio;
|
const canvasScale = window.devicePixelRatio;
|
||||||
|
|
||||||
const canvasWidth = canvasDOMWidth * canvasScale;
|
const canvasWidth = canvasDOMWidth * canvasScale;
|
||||||
@ -384,6 +384,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
toggleZenMode={this.toggleZenMode}
|
toggleZenMode={this.toggleZenMode}
|
||||||
lng={getLanguage().lng}
|
lng={getLanguage().lng}
|
||||||
isCollaborating={this.props.isCollaborating || false}
|
isCollaborating={this.props.isCollaborating || false}
|
||||||
|
onExportToBackend={onExportToBackend}
|
||||||
/>
|
/>
|
||||||
{this.state.showStats && (
|
{this.state.showStats && (
|
||||||
<Stats
|
<Stats
|
||||||
|
@ -67,7 +67,7 @@ const ExportModal = ({
|
|||||||
onExportToPng: ExportCB;
|
onExportToPng: ExportCB;
|
||||||
onExportToSvg: ExportCB;
|
onExportToSvg: ExportCB;
|
||||||
onExportToClipboard: ExportCB;
|
onExportToClipboard: ExportCB;
|
||||||
onExportToBackend: ExportCB;
|
onExportToBackend?: ExportCB;
|
||||||
onCloseRequest: () => void;
|
onCloseRequest: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const someElementIsSelected = isSomeElementSelected(elements, appState);
|
const someElementIsSelected = isSomeElementSelected(elements, appState);
|
||||||
@ -155,13 +155,15 @@ const ExportModal = ({
|
|||||||
onClick={() => onExportToClipboard(exportedElements, scale)}
|
onClick={() => onExportToClipboard(exportedElements, scale)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ToolButton
|
{onExportToBackend && (
|
||||||
type="button"
|
<ToolButton
|
||||||
icon={link}
|
type="button"
|
||||||
title={t("buttons.getShareableLink")}
|
icon={link}
|
||||||
aria-label={t("buttons.getShareableLink")}
|
title={t("buttons.getShareableLink")}
|
||||||
onClick={() => onExportToBackend(exportedElements)}
|
aria-label={t("buttons.getShareableLink")}
|
||||||
/>
|
onClick={() => onExportToBackend(exportedElements)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
<div className="ExportDialog__name">
|
<div className="ExportDialog__name">
|
||||||
{actionManager.renderAction("changeProjectName")}
|
{actionManager.renderAction("changeProjectName")}
|
||||||
@ -235,7 +237,7 @@ export const ExportDialog = ({
|
|||||||
onExportToPng: ExportCB;
|
onExportToPng: ExportCB;
|
||||||
onExportToSvg: ExportCB;
|
onExportToSvg: ExportCB;
|
||||||
onExportToClipboard: ExportCB;
|
onExportToClipboard: ExportCB;
|
||||||
onExportToBackend: ExportCB;
|
onExportToBackend?: ExportCB;
|
||||||
}) => {
|
}) => {
|
||||||
const [modalIsShown, setModalIsShown] = useState(false);
|
const [modalIsShown, setModalIsShown] = useState(false);
|
||||||
const triggerButton = useRef<HTMLButtonElement>(null);
|
const triggerButton = useRef<HTMLButtonElement>(null);
|
||||||
|
@ -65,6 +65,11 @@ interface LayerUIProps {
|
|||||||
toggleZenMode: () => void;
|
toggleZenMode: () => void;
|
||||||
lng: string;
|
lng: string;
|
||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
|
onExportToBackend?: (
|
||||||
|
exportedElements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
canvas: HTMLCanvasElement | null,
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useOnClickOutside = (
|
const useOnClickOutside = (
|
||||||
@ -317,6 +322,7 @@ const LayerUI = ({
|
|||||||
zenModeEnabled,
|
zenModeEnabled,
|
||||||
toggleZenMode,
|
toggleZenMode,
|
||||||
isCollaborating,
|
isCollaborating,
|
||||||
|
onExportToBackend,
|
||||||
}: LayerUIProps) => {
|
}: LayerUIProps) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
@ -358,6 +364,7 @@ const LayerUI = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExportDialog
|
<ExportDialog
|
||||||
elements={elements}
|
elements={elements}
|
||||||
@ -366,28 +373,14 @@ const LayerUI = ({
|
|||||||
onExportToPng={createExporter("png")}
|
onExportToPng={createExporter("png")}
|
||||||
onExportToSvg={createExporter("svg")}
|
onExportToSvg={createExporter("svg")}
|
||||||
onExportToClipboard={createExporter("clipboard")}
|
onExportToClipboard={createExporter("clipboard")}
|
||||||
onExportToBackend={async (exportedElements) => {
|
onExportToBackend={
|
||||||
if (canvas) {
|
onExportToBackend
|
||||||
try {
|
? (elements) => {
|
||||||
await exportCanvas(
|
onExportToBackend &&
|
||||||
"backend",
|
onExportToBackend(elements, appState, canvas);
|
||||||
exportedElements,
|
|
||||||
{
|
|
||||||
...appState,
|
|
||||||
selectedElementIds: {},
|
|
||||||
},
|
|
||||||
canvas,
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
if (error.name !== "AbortError") {
|
|
||||||
const { width, height } = canvas;
|
|
||||||
console.error(error, { width, height });
|
|
||||||
setAppState({ errorMessage: error.message });
|
|
||||||
}
|
}
|
||||||
}
|
: undefined
|
||||||
}
|
}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
import { fileSave } from "browser-nativefs";
|
import { fileSave } from "browser-nativefs";
|
||||||
import { EVENT_IO, trackEvent } from "../analytics";
|
import { EVENT_IO, trackEvent } from "../analytics";
|
||||||
import { getDefaultAppState } from "../appState";
|
|
||||||
import {
|
import {
|
||||||
copyCanvasToClipboardAsPng,
|
copyCanvasToClipboardAsPng,
|
||||||
copyTextToSystemClipboard,
|
copyTextToSystemClipboard,
|
||||||
} from "../clipboard";
|
} from "../clipboard";
|
||||||
import {
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
ExcalidrawElement,
|
|
||||||
NonDeletedExcalidrawElement,
|
|
||||||
} from "../element/types";
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { exportToCanvas, exportToSvg } from "../scene/export";
|
import { exportToCanvas, exportToSvg } from "../scene/export";
|
||||||
import { ExportType } from "../scene/types";
|
import { ExportType } from "../scene/types";
|
||||||
@ -19,65 +15,6 @@ import { serializeAsJSON } from "./json";
|
|||||||
export { loadFromBlob } from "./blob";
|
export { loadFromBlob } from "./blob";
|
||||||
export { loadFromJSON, saveAsJSON } from "./json";
|
export { loadFromJSON, saveAsJSON } from "./json";
|
||||||
|
|
||||||
const BACKEND_V2_POST = process.env.REACT_APP_BACKEND_V2_POST_URL;
|
|
||||||
|
|
||||||
export const exportToBackend = async (
|
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
appState: AppState,
|
|
||||||
) => {
|
|
||||||
const json = serializeAsJSON(elements, appState);
|
|
||||||
const encoded = new TextEncoder().encode(json);
|
|
||||||
|
|
||||||
const key = await window.crypto.subtle.generateKey(
|
|
||||||
{
|
|
||||||
name: "AES-GCM",
|
|
||||||
length: 128,
|
|
||||||
},
|
|
||||||
true, // extractable
|
|
||||||
["encrypt", "decrypt"],
|
|
||||||
);
|
|
||||||
// The iv is set to 0. We are never going to reuse the same key so we don't
|
|
||||||
// need to have an iv. (I hope that's correct...)
|
|
||||||
const iv = new Uint8Array(12);
|
|
||||||
// We use symmetric encryption. AES-GCM is the recommended algorithm and
|
|
||||||
// includes checks that the ciphertext has not been modified by an attacker.
|
|
||||||
const encrypted = await window.crypto.subtle.encrypt(
|
|
||||||
{
|
|
||||||
name: "AES-GCM",
|
|
||||||
iv,
|
|
||||||
},
|
|
||||||
key,
|
|
||||||
encoded,
|
|
||||||
);
|
|
||||||
// We use jwk encoding to be able to extract just the base64 encoded key.
|
|
||||||
// We will hardcode the rest of the attributes when importing back the key.
|
|
||||||
const exportedKey = await window.crypto.subtle.exportKey("jwk", key);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(BACKEND_V2_POST, {
|
|
||||||
method: "POST",
|
|
||||||
body: encrypted,
|
|
||||||
});
|
|
||||||
const json = await response.json();
|
|
||||||
if (json.id) {
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
// We need to store the key (and less importantly the id) as hash instead
|
|
||||||
// of queryParam in order to never send it to the server
|
|
||||||
url.hash = `json=${json.id},${exportedKey.k!}`;
|
|
||||||
const urlString = url.toString();
|
|
||||||
window.prompt(`🔒${t("alerts.uploadedSecurly")}`, urlString);
|
|
||||||
trackEvent(EVENT_IO, "export", "backend");
|
|
||||||
} else if (json.error_class === "RequestTooLargeError") {
|
|
||||||
window.alert(t("alerts.couldNotCreateShareableLinkTooBig"));
|
|
||||||
} else {
|
|
||||||
window.alert(t("alerts.couldNotCreateShareableLink"));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
window.alert(t("alerts.couldNotCreateShareableLink"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const exportCanvas = async (
|
export const exportCanvas = async (
|
||||||
type: ExportType,
|
type: ExportType,
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
@ -169,13 +106,6 @@ export const exportCanvas = async (
|
|||||||
}
|
}
|
||||||
throw new Error(t("alerts.couldNotCopyToClipboard"));
|
throw new Error(t("alerts.couldNotCopyToClipboard"));
|
||||||
}
|
}
|
||||||
} else if (type === "backend") {
|
|
||||||
exportToBackend(elements, {
|
|
||||||
...appState,
|
|
||||||
viewBackgroundColor: exportBackground
|
|
||||||
? appState.viewBackgroundColor
|
|
||||||
: getDefaultAppState().viewBackgroundColor,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// clean up the DOM
|
// clean up the DOM
|
||||||
|
@ -3,12 +3,14 @@ import { ExcalidrawElement } from "../../element/types";
|
|||||||
import { AppState } from "../../types";
|
import { AppState } from "../../types";
|
||||||
import { ImportedDataState } from "../../data/types";
|
import { ImportedDataState } from "../../data/types";
|
||||||
import { restore } from "../../data/restore";
|
import { restore } from "../../data/restore";
|
||||||
import { EVENT_ACTION, trackEvent } from "../../analytics";
|
import { EVENT_ACTION, EVENT_IO, trackEvent } from "../../analytics";
|
||||||
|
import { serializeAsJSON } from "../../data/json";
|
||||||
|
|
||||||
const byteToHex = (byte: number): string => `0${byte.toString(16)}`.slice(-2);
|
const byteToHex = (byte: number): string => `0${byte.toString(16)}`.slice(-2);
|
||||||
|
|
||||||
const BACKEND_GET = process.env.REACT_APP_BACKEND_V1_GET_URL;
|
const BACKEND_GET = process.env.REACT_APP_BACKEND_V1_GET_URL;
|
||||||
const BACKEND_V2_GET = process.env.REACT_APP_BACKEND_V2_GET_URL;
|
const BACKEND_V2_GET = process.env.REACT_APP_BACKEND_V2_GET_URL;
|
||||||
|
const BACKEND_V2_POST = process.env.REACT_APP_BACKEND_V2_POST_URL;
|
||||||
|
|
||||||
const generateRandomID = async () => {
|
const generateRandomID = async () => {
|
||||||
const arr = new Uint8Array(10);
|
const arr = new Uint8Array(10);
|
||||||
@ -228,3 +230,60 @@ export const loadScene = async (
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const exportToBackend = async (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
) => {
|
||||||
|
const json = serializeAsJSON(elements, appState);
|
||||||
|
const encoded = new TextEncoder().encode(json);
|
||||||
|
|
||||||
|
const key = await window.crypto.subtle.generateKey(
|
||||||
|
{
|
||||||
|
name: "AES-GCM",
|
||||||
|
length: 128,
|
||||||
|
},
|
||||||
|
true, // extractable
|
||||||
|
["encrypt", "decrypt"],
|
||||||
|
);
|
||||||
|
// The iv is set to 0. We are never going to reuse the same key so we don't
|
||||||
|
// need to have an iv. (I hope that's correct...)
|
||||||
|
const iv = new Uint8Array(12);
|
||||||
|
// We use symmetric encryption. AES-GCM is the recommended algorithm and
|
||||||
|
// includes checks that the ciphertext has not been modified by an attacker.
|
||||||
|
const encrypted = await window.crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: "AES-GCM",
|
||||||
|
iv,
|
||||||
|
},
|
||||||
|
key,
|
||||||
|
encoded,
|
||||||
|
);
|
||||||
|
// We use jwk encoding to be able to extract just the base64 encoded key.
|
||||||
|
// We will hardcode the rest of the attributes when importing back the key.
|
||||||
|
const exportedKey = await window.crypto.subtle.exportKey("jwk", key);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(BACKEND_V2_POST, {
|
||||||
|
method: "POST",
|
||||||
|
body: encrypted,
|
||||||
|
});
|
||||||
|
const json = await response.json();
|
||||||
|
if (json.id) {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
// We need to store the key (and less importantly the id) as hash instead
|
||||||
|
// of queryParam in order to never send it to the server
|
||||||
|
url.hash = `json=${json.id},${exportedKey.k!}`;
|
||||||
|
const urlString = url.toString();
|
||||||
|
window.prompt(`🔒${t("alerts.uploadedSecurly")}`, urlString);
|
||||||
|
trackEvent(EVENT_IO, "export", "backend");
|
||||||
|
} else if (json.error_class === "RequestTooLargeError") {
|
||||||
|
window.alert(t("alerts.couldNotCreateShareableLinkTooBig"));
|
||||||
|
} else {
|
||||||
|
window.alert(t("alerts.couldNotCreateShareableLink"));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
window.alert(t("alerts.couldNotCreateShareableLink"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -13,16 +13,21 @@ import { ImportedDataState } from "../data/types";
|
|||||||
import CollabWrapper, { CollabAPI } from "./collab/CollabWrapper";
|
import CollabWrapper, { CollabAPI } from "./collab/CollabWrapper";
|
||||||
import { TopErrorBoundary } from "../components/TopErrorBoundary";
|
import { TopErrorBoundary } from "../components/TopErrorBoundary";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { loadScene } from "./data";
|
import { exportToBackend, loadScene } from "./data";
|
||||||
import { getCollaborationLinkData } from "./data";
|
import { getCollaborationLinkData } from "./data";
|
||||||
import { EVENT } from "../constants";
|
import { EVENT } from "../constants";
|
||||||
import { loadFromFirebase } from "./data/firebase";
|
import { loadFromFirebase } from "./data/firebase";
|
||||||
import { ExcalidrawImperativeAPI } from "../components/App";
|
import { ExcalidrawImperativeAPI } from "../components/App";
|
||||||
import { debounce, ResolvablePromise, resolvablePromise } from "../utils";
|
import { debounce, ResolvablePromise, resolvablePromise } from "../utils";
|
||||||
import { AppState, ExcalidrawAPIRefValue } from "../types";
|
import { AppState, ExcalidrawAPIRefValue } from "../types";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
} from "../element/types";
|
||||||
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT } from "./app_constants";
|
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT } from "./app_constants";
|
||||||
import { EVENT_LOAD, EVENT_SHARE, trackEvent } from "../analytics";
|
import { EVENT_LOAD, EVENT_SHARE, trackEvent } from "../analytics";
|
||||||
|
import { ErrorDialog } from "../components/ErrorDialog";
|
||||||
|
import { getDefaultAppState } from "../appState";
|
||||||
|
|
||||||
const excalidrawRef: React.MutableRefObject<
|
const excalidrawRef: React.MutableRefObject<
|
||||||
MarkRequired<ExcalidrawAPIRefValue, "ready" | "readyPromise">
|
MarkRequired<ExcalidrawAPIRefValue, "ready" | "readyPromise">
|
||||||
@ -178,6 +183,7 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
|
|||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
height: window.innerHeight,
|
height: window.innerHeight,
|
||||||
});
|
});
|
||||||
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const onResize = () => {
|
const onResize = () => {
|
||||||
@ -260,18 +266,52 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onExportToBackend = async (
|
||||||
|
exportedElements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
canvas: HTMLCanvasElement | null,
|
||||||
|
) => {
|
||||||
|
if (exportedElements.length === 0) {
|
||||||
|
return window.alert(t("alerts.cannotExportEmptyCanvas"));
|
||||||
|
}
|
||||||
|
if (canvas) {
|
||||||
|
try {
|
||||||
|
await exportToBackend(exportedElements, {
|
||||||
|
...appState,
|
||||||
|
viewBackgroundColor: appState.exportBackground
|
||||||
|
? appState.viewBackgroundColor
|
||||||
|
: getDefaultAppState().viewBackgroundColor,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name !== "AbortError") {
|
||||||
|
const { width, height } = canvas;
|
||||||
|
console.error(error, { width, height });
|
||||||
|
setErrorMessage(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<Excalidraw
|
<>
|
||||||
ref={excalidrawRef}
|
<Excalidraw
|
||||||
onChange={onChange}
|
ref={excalidrawRef}
|
||||||
width={dimensions.width}
|
onChange={onChange}
|
||||||
height={dimensions.height}
|
width={dimensions.width}
|
||||||
initialData={initialStatePromiseRef.current.promise}
|
height={dimensions.height}
|
||||||
user={{ name: collab.username }}
|
initialData={initialStatePromiseRef.current.promise}
|
||||||
onCollabButtonClick={collab.onCollabButtonClick}
|
user={{ name: collab.username }}
|
||||||
isCollaborating={collab.isCollaborating}
|
onCollabButtonClick={collab.onCollabButtonClick}
|
||||||
onPointerUpdate={collab.onPointerUpdate}
|
isCollaborating={collab.isCollaborating}
|
||||||
/>
|
onPointerUpdate={collab.onPointerUpdate}
|
||||||
|
onExportToBackend={onExportToBackend}
|
||||||
|
/>
|
||||||
|
{errorMessage && (
|
||||||
|
<ErrorDialog
|
||||||
|
message={errorMessage}
|
||||||
|
onClose={() => setErrorMessage("")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ Please add the latest change on the top under the correct section.
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
|
- Add support for `exportToBackend` prop to allow host apps to implement shareable links [#2612](https://github.com/excalidraw/excalidraw/pull/2612/files)
|
||||||
- Add zoom to selection [#2522](https://github.com/excalidraw/excalidraw/pull/2522)
|
- Add zoom to selection [#2522](https://github.com/excalidraw/excalidraw/pull/2522)
|
||||||
- Insert Library items in the middle of the screen [#2527](https://github.com/excalidraw/excalidraw/pull/2527)
|
- Insert Library items in the middle of the screen [#2527](https://github.com/excalidraw/excalidraw/pull/2527)
|
||||||
- Show shortcut context menu [#2501](https://github.com/excalidraw/excalidraw/pull/2501)
|
- Show shortcut context menu [#2501](https://github.com/excalidraw/excalidraw/pull/2501)
|
||||||
@ -25,6 +26,7 @@ Please add the latest change on the top under the correct section.
|
|||||||
- Support CSV graphs and improve the look and feel [#2495](https://github.com/excalidraw/excalidraw/pull/2495)
|
- Support CSV graphs and improve the look and feel [#2495](https://github.com/excalidraw/excalidraw/pull/2495)
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
- Consistent case for export locale strings [#2622](https://github.com/excalidraw/excalidraw/pull/2622)
|
- Consistent case for export locale strings [#2622](https://github.com/excalidraw/excalidraw/pull/2622)
|
||||||
- Remove unnecessary console.error as it was polluting Sentry [#2637](https://github.com/excalidraw/excalidraw/pull/2637)
|
- Remove unnecessary console.error as it was polluting Sentry [#2637](https://github.com/excalidraw/excalidraw/pull/2637)
|
||||||
- Fix scroll-to-center on init for non-zero canvas offsets [#2445](https://github.com/excalidraw/excalidraw/pull/2445)
|
- Fix scroll-to-center on init for non-zero canvas offsets [#2445](https://github.com/excalidraw/excalidraw/pull/2445)
|
@ -141,6 +141,7 @@ export default function App() {
|
|||||||
| [`onCollabButtonClick`](#onCollabButtonClick) | Function | | Callback to be triggered when the collab button is clicked |
|
| [`onCollabButtonClick`](#onCollabButtonClick) | Function | | Callback to be triggered when the collab button is clicked |
|
||||||
| [`isCollaborating`](#isCollaborating) | `boolean` | | This implies if the app is in collaboration mode |
|
| [`isCollaborating`](#isCollaborating) | `boolean` | | This implies if the app is in collaboration mode |
|
||||||
| [`onPointerUpdate`](#onPointerUpdate) | Function | | Callback triggered when mouse pointer is updated. |
|
| [`onPointerUpdate`](#onPointerUpdate) | Function | | Callback triggered when mouse pointer is updated. |
|
||||||
|
| [`onExportToBackend`](#onExportToBackend) | Function | | Callback triggered when link button is clicked on export dialog |
|
||||||
|
|
||||||
#### `width`
|
#### `width`
|
||||||
|
|
||||||
@ -260,3 +261,15 @@ This callback is triggered when mouse pointer is updated.
|
|||||||
2.`button`: The position of the button. This will be one of `["down", "up"]`
|
2.`button`: The position of the button. This will be one of `["down", "up"]`
|
||||||
|
|
||||||
3.`pointersMap`: [`pointers map`](https://github.com/excalidraw/excalidraw/blob/182a3e39e1362d73d2a565c870eb2fb72071fdcc/src/types.ts#L122) of the scene
|
3.`pointersMap`: [`pointers map`](https://github.com/excalidraw/excalidraw/blob/182a3e39e1362d73d2a565c870eb2fb72071fdcc/src/types.ts#L122) of the scene
|
||||||
|
|
||||||
|
#### `onExportToBackend`
|
||||||
|
|
||||||
|
This callback is triggered when the shareable-link button is clicked in the export dialog. The link button will only be shown if this callback is passed.
|
||||||
|
|
||||||
|
```js
|
||||||
|
(exportedElements, appState, canvas) => void
|
||||||
|
```
|
||||||
|
|
||||||
|
1. `exportedElements`: An array of [non deleted elements](https://github.com/excalidraw/excalidraw/blob/6e45cb95dbd7a8be1859c7055b06957298e3097c/src/element/types.ts#L76) which needs to be exported.
|
||||||
|
2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/4c90ea5667d29effe8ec4a115e49efc7c340cdb3/src/types.ts#L33) of the scene.
|
||||||
|
3. `canvas`: The `HTMLCanvasElement` of the scene.
|
||||||
|
@ -22,6 +22,7 @@ const Excalidraw = (props: ExcalidrawProps) => {
|
|||||||
onCollabButtonClick,
|
onCollabButtonClick,
|
||||||
isCollaborating,
|
isCollaborating,
|
||||||
onPointerUpdate,
|
onPointerUpdate,
|
||||||
|
onExportToBackend,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -57,6 +58,7 @@ const Excalidraw = (props: ExcalidrawProps) => {
|
|||||||
onCollabButtonClick={onCollabButtonClick}
|
onCollabButtonClick={onCollabButtonClick}
|
||||||
isCollaborating={isCollaborating}
|
isCollaborating={isCollaborating}
|
||||||
onPointerUpdate={onPointerUpdate}
|
onPointerUpdate={onPointerUpdate}
|
||||||
|
onExportToBackend={onExportToBackend}
|
||||||
/>
|
/>
|
||||||
</IsMobileProvider>
|
</IsMobileProvider>
|
||||||
</InitializeApp>
|
</InitializeApp>
|
||||||
|
@ -166,6 +166,11 @@ export interface ExcalidrawProps {
|
|||||||
button: "down" | "up";
|
button: "down" | "up";
|
||||||
pointersMap: Gesture["pointers"];
|
pointersMap: Gesture["pointers"];
|
||||||
}) => void;
|
}) => void;
|
||||||
|
onExportToBackend?: (
|
||||||
|
exportedElements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
canvas: HTMLCanvasElement | null,
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SceneData = {
|
export type SceneData = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user