Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
5c26bd19d7
commit
fc58e51ab3
@ -11,6 +11,7 @@ import {
|
|||||||
VALID_SPREADSHEET,
|
VALID_SPREADSHEET,
|
||||||
MALFORMED_SPREADSHEET,
|
MALFORMED_SPREADSHEET,
|
||||||
} from "./charts";
|
} from "./charts";
|
||||||
|
import { canvasToBlob } from "./data/blob";
|
||||||
|
|
||||||
const TYPE_ELEMENTS = "excalidraw/elements";
|
const TYPE_ELEMENTS = "excalidraw/elements";
|
||||||
|
|
||||||
@ -157,23 +158,12 @@ export const parseClipboard = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const copyCanvasToClipboardAsPng = async (canvas: HTMLCanvasElement) =>
|
export const copyCanvasToClipboardAsPng = async (canvas: HTMLCanvasElement) => {
|
||||||
new Promise((resolve, reject) => {
|
const blob = await canvasToBlob(canvas);
|
||||||
try {
|
|
||||||
canvas.toBlob(async (blob) => {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.write([
|
await navigator.clipboard.write([
|
||||||
new window.ClipboardItem({ "image/png": blob }),
|
new window.ClipboardItem({ "image/png": blob }),
|
||||||
]);
|
]);
|
||||||
resolve();
|
};
|
||||||
} catch (error) {
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const copyTextToSystemClipboard = async (text: string | null) => {
|
export const copyTextToSystemClipboard = async (text: string | null) => {
|
||||||
let copied = false;
|
let copied = false;
|
||||||
|
@ -1022,12 +1022,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
copyToClipboard(this.scene.getElements(), this.state);
|
copyToClipboard(this.scene.getElements(), this.state);
|
||||||
};
|
};
|
||||||
|
|
||||||
private copyToClipboardAsPng = () => {
|
private copyToClipboardAsPng = async () => {
|
||||||
const elements = this.scene.getElements();
|
const elements = this.scene.getElements();
|
||||||
|
|
||||||
const selectedElements = getSelectedElements(elements, this.state);
|
const selectedElements = getSelectedElements(elements, this.state);
|
||||||
try {
|
try {
|
||||||
exportCanvas(
|
await exportCanvas(
|
||||||
"clipboard",
|
"clipboard",
|
||||||
selectedElements.length ? selectedElements : elements,
|
selectedElements.length ? selectedElements : elements,
|
||||||
this.state,
|
this.state,
|
||||||
@ -1040,13 +1040,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private copyToClipboardAsSvg = () => {
|
private copyToClipboardAsSvg = async () => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = getSelectedElements(
|
||||||
this.scene.getElements(),
|
this.scene.getElements(),
|
||||||
this.state,
|
this.state,
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
exportCanvas(
|
await exportCanvas(
|
||||||
"clipboard-svg",
|
"clipboard-svg",
|
||||||
selectedElements.length ? selectedElements : this.scene.getElements(),
|
selectedElements.length ? selectedElements : this.scene.getElements(),
|
||||||
this.state,
|
this.state,
|
||||||
|
@ -15,10 +15,24 @@ import { probablySupportsClipboardBlob } from "../clipboard";
|
|||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
import useIsMobile from "../is-mobile";
|
import useIsMobile from "../is-mobile";
|
||||||
import { Dialog } from "./Dialog";
|
import { Dialog } from "./Dialog";
|
||||||
|
import { canvasToBlob } from "../data/blob";
|
||||||
|
import { CanvasError } from "../errors";
|
||||||
|
|
||||||
const scales = [1, 2, 3];
|
const scales = [1, 2, 3];
|
||||||
const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
|
const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
|
||||||
|
|
||||||
|
export const ErrorCanvasPreview = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3>{t("canvasError.cannotShowPreview")}</h3>
|
||||||
|
<p>
|
||||||
|
<span>{t("canvasError.canvasTooBig")}</span>
|
||||||
|
</p>
|
||||||
|
<em>({t("canvasError.canvasTooBigTip")})</em>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export type ExportCB = (
|
export type ExportCB = (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
scale?: number,
|
scale?: number,
|
||||||
@ -47,6 +61,7 @@ const ExportModal = ({
|
|||||||
const someElementIsSelected = isSomeElementSelected(elements, appState);
|
const someElementIsSelected = isSomeElementSelected(elements, appState);
|
||||||
const [scale, setScale] = useState(defaultScale);
|
const [scale, setScale] = useState(defaultScale);
|
||||||
const [exportSelected, setExportSelected] = useState(someElementIsSelected);
|
const [exportSelected, setExportSelected] = useState(someElementIsSelected);
|
||||||
|
const [previewError, setPreviewError] = useState<Error | null>(null);
|
||||||
const previewRef = useRef<HTMLDivElement>(null);
|
const previewRef = useRef<HTMLDivElement>(null);
|
||||||
const {
|
const {
|
||||||
exportBackground,
|
exportBackground,
|
||||||
@ -64,6 +79,10 @@ const ExportModal = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const previewNode = previewRef.current;
|
const previewNode = previewRef.current;
|
||||||
|
if (!previewNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
const canvas = exportToCanvas(exportedElements, appState, {
|
const canvas = exportToCanvas(exportedElements, appState, {
|
||||||
exportBackground,
|
exportBackground,
|
||||||
viewBackgroundColor,
|
viewBackgroundColor,
|
||||||
@ -71,10 +90,31 @@ const ExportModal = ({
|
|||||||
scale,
|
scale,
|
||||||
shouldAddWatermark,
|
shouldAddWatermark,
|
||||||
});
|
});
|
||||||
previewNode?.appendChild(canvas);
|
|
||||||
|
let isRemoved = false;
|
||||||
|
// if converting to blob fails, there's some problem that will
|
||||||
|
// likely prevent preview and export (e.g. canvas too big)
|
||||||
|
canvasToBlob(canvas)
|
||||||
|
.then(() => {
|
||||||
|
if (isRemoved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPreviewError(null);
|
||||||
|
previewNode.appendChild(canvas);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
setPreviewError(new CanvasError());
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
previewNode?.removeChild(canvas);
|
isRemoved = true;
|
||||||
|
canvas.remove();
|
||||||
};
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
setPreviewError(new CanvasError());
|
||||||
|
}
|
||||||
}, [
|
}, [
|
||||||
appState,
|
appState,
|
||||||
exportedElements,
|
exportedElements,
|
||||||
@ -87,7 +127,9 @@ const ExportModal = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ExportDialog">
|
<div className="ExportDialog">
|
||||||
<div className="ExportDialog__preview" ref={previewRef}></div>
|
<div className="ExportDialog__preview" ref={previewRef}>
|
||||||
|
{previewError && <ErrorCanvasPreview />}
|
||||||
|
</div>
|
||||||
<Stack.Col gap={2} align="center">
|
<Stack.Col gap={2} align="center">
|
||||||
<div className="ExportDialog__actions">
|
<div className="ExportDialog__actions">
|
||||||
<Stack.Row gap={2}>
|
<Stack.Row gap={2}>
|
||||||
|
@ -315,18 +315,18 @@ const LayerUI = ({
|
|||||||
scale,
|
scale,
|
||||||
) => {
|
) => {
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
try {
|
|
||||||
await exportCanvas(type, exportedElements, appState, canvas, {
|
await exportCanvas(type, exportedElements, appState, canvas, {
|
||||||
exportBackground: appState.exportBackground,
|
exportBackground: appState.exportBackground,
|
||||||
name: appState.name,
|
name: appState.name,
|
||||||
viewBackgroundColor: appState.viewBackgroundColor,
|
viewBackgroundColor: appState.viewBackgroundColor,
|
||||||
scale,
|
scale,
|
||||||
shouldAddWatermark: appState.shouldAddWatermark,
|
shouldAddWatermark: appState.shouldAddWatermark,
|
||||||
});
|
})
|
||||||
} catch (error) {
|
.catch(muteFSAbortError)
|
||||||
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
setAppState({ errorMessage: error.message });
|
setAppState({ errorMessage: error.message });
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
@ -351,10 +351,13 @@ const LayerUI = ({
|
|||||||
appState,
|
appState,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
if (error.name !== "AbortError") {
|
||||||
|
const { width, height } = canvas;
|
||||||
|
console.error(error, { width, height });
|
||||||
setAppState({ errorMessage: error.message });
|
setAppState({ errorMessage: error.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -73,7 +73,7 @@ export class TopErrorBoundary extends React.Component<
|
|||||||
|
|
||||||
private errorSplash() {
|
private errorSplash() {
|
||||||
return (
|
return (
|
||||||
<div className="ErrorSplash">
|
<div className="ErrorSplash excalidraw">
|
||||||
<div className="ErrorSplash-messageContainer">
|
<div className="ErrorSplash-messageContainer">
|
||||||
<div className="ErrorSplash-paragraph bigger align-center">
|
<div className="ErrorSplash-paragraph bigger align-center">
|
||||||
{t("errorSplash.headingMain_pre")}
|
{t("errorSplash.headingMain_pre")}
|
||||||
|
@ -301,59 +301,6 @@
|
|||||||
max-height: calc(100vh - 236px);
|
max-height: calc(100vh - 236px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ErrorSplash {
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 20px 0;
|
|
||||||
overflow: auto;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
user-select: text;
|
|
||||||
|
|
||||||
.ErrorSplash-messageContainer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
padding: 40px;
|
|
||||||
background-color: $oc-red-1;
|
|
||||||
border: 3px solid $oc-red-9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ErrorSplash-paragraph {
|
|
||||||
margin: 15px 0;
|
|
||||||
max-width: 600px;
|
|
||||||
|
|
||||||
&.align-center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bigger,
|
|
||||||
.bigger button {
|
|
||||||
font-size: 1.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.smaller,
|
|
||||||
.smaller button {
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ErrorSplash-details {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
width: 100%;
|
|
||||||
margin: 10px 0;
|
|
||||||
font-family: "Cascadia";
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-select {
|
.dropdown-select {
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -502,3 +449,56 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ErrorSplash.excalidraw {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px 0;
|
||||||
|
overflow: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
user-select: text;
|
||||||
|
|
||||||
|
.ErrorSplash-messageContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
padding: 40px;
|
||||||
|
background-color: $oc-red-1;
|
||||||
|
border: 3px solid $oc-red-9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ErrorSplash-paragraph {
|
||||||
|
margin: 15px 0;
|
||||||
|
max-width: 600px;
|
||||||
|
|
||||||
|
&.align-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bigger,
|
||||||
|
.bigger button {
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smaller,
|
||||||
|
.smaller button {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ErrorSplash-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
margin: 10px 0;
|
||||||
|
font-family: "Cascadia";
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@ import { AppState } from "../types";
|
|||||||
import { LibraryData, ImportedDataState } from "./types";
|
import { LibraryData, ImportedDataState } from "./types";
|
||||||
import { calculateScrollCenter } from "../scene";
|
import { calculateScrollCenter } from "../scene";
|
||||||
import { MIME_TYPES } from "../constants";
|
import { MIME_TYPES } from "../constants";
|
||||||
|
import { CanvasError } from "../errors";
|
||||||
|
|
||||||
export const parseFileContents = async (blob: Blob | File) => {
|
export const parseFileContents = async (blob: Blob | File) => {
|
||||||
let contents: string;
|
let contents: string;
|
||||||
@ -109,3 +110,25 @@ export const loadLibraryFromBlob = async (blob: Blob) => {
|
|||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const canvasToBlob = async (
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
): Promise<Blob> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
if (!blob) {
|
||||||
|
return reject(
|
||||||
|
new CanvasError(
|
||||||
|
t("canvasError.canvasTooBig"),
|
||||||
|
"CANVAS_POSSIBLY_TOO_BIG",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
resolve(blob);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -19,6 +19,7 @@ import { serializeAsJSON } from "./json";
|
|||||||
import { ExportType } from "../scene/types";
|
import { ExportType } from "../scene/types";
|
||||||
import { restore } from "./restore";
|
import { restore } from "./restore";
|
||||||
import { ImportedDataState } from "./types";
|
import { ImportedDataState } from "./types";
|
||||||
|
import { canvasToBlob } from "./blob";
|
||||||
|
|
||||||
export { loadFromBlob } from "./blob";
|
export { loadFromBlob } from "./blob";
|
||||||
export { saveAsJSON, loadFromJSON } from "./json";
|
export { saveAsJSON, loadFromJSON } from "./json";
|
||||||
@ -337,8 +338,7 @@ export const exportCanvas = async (
|
|||||||
|
|
||||||
if (type === "png") {
|
if (type === "png") {
|
||||||
const fileName = `${name}.png`;
|
const fileName = `${name}.png`;
|
||||||
tempCanvas.toBlob(async (blob) => {
|
let blob = await canvasToBlob(tempCanvas);
|
||||||
if (blob) {
|
|
||||||
if (appState.exportEmbedScene) {
|
if (appState.exportEmbedScene) {
|
||||||
blob = await (
|
blob = await (
|
||||||
await import(/* webpackChunkName: "image" */ "./image")
|
await import(/* webpackChunkName: "image" */ "./image")
|
||||||
@ -352,13 +352,14 @@ export const exportCanvas = async (
|
|||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
extensions: [".png"],
|
extensions: [".png"],
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (type === "clipboard") {
|
} else if (type === "clipboard") {
|
||||||
try {
|
try {
|
||||||
copyCanvasToClipboardAsPng(tempCanvas);
|
await copyCanvasToClipboardAsPng(tempCanvas);
|
||||||
} catch {
|
} catch (error) {
|
||||||
window.alert(t("alerts.couldNotCopyToClipboard"));
|
if (error.name === "CANVAS_POSSIBLY_TOO_BIG") {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new Error(t("alerts.couldNotCopyToClipboard"));
|
||||||
}
|
}
|
||||||
} else if (type === "backend") {
|
} else if (type === "backend") {
|
||||||
exportToBackend(elements, {
|
exportToBackend(elements, {
|
||||||
|
11
src/errors.ts
Normal file
11
src/errors.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
type CANVAS_ERROR_NAMES = "CANVAS_ERROR" | "CANVAS_POSSIBLY_TOO_BIG";
|
||||||
|
export class CanvasError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string = "Couldn't export canvas.",
|
||||||
|
name: CANVAS_ERROR_NAMES = "CANVAS_ERROR",
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this.name = name;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
}
|
@ -150,6 +150,11 @@
|
|||||||
"lineEditor_pointSelected": "Press Delete to remove point, CtrlOrCmd+D to duplicate, or drag to move",
|
"lineEditor_pointSelected": "Press Delete to remove point, CtrlOrCmd+D to duplicate, or drag to move",
|
||||||
"lineEditor_nothingSelected": "Select a point to move or remove, or hold Alt and click to add new points"
|
"lineEditor_nothingSelected": "Select a point to move or remove, or hold Alt and click to add new points"
|
||||||
},
|
},
|
||||||
|
"canvasError": {
|
||||||
|
"cannotShowPreview": "Cannot show preview",
|
||||||
|
"canvasTooBig": "The canvas may be too big.",
|
||||||
|
"canvasTooBigTip": "Tip: try moving the farthest elements a bit closer together."
|
||||||
|
},
|
||||||
"errorSplash": {
|
"errorSplash": {
|
||||||
"headingMain_pre": "Encountered an error. Try ",
|
"headingMain_pre": "Encountered an error. Try ",
|
||||||
"headingMain_button": "reloading the page.",
|
"headingMain_button": "reloading the page.",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user