feat: support pasting file contents & always prefer system clip (#3257)
This commit is contained in:
parent
13d9374cde
commit
94ad8eaa19
@ -7,12 +7,10 @@ import { AppState } from "./types";
|
|||||||
import { SVG_EXPORT_TAG } from "./scene/export";
|
import { SVG_EXPORT_TAG } from "./scene/export";
|
||||||
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
||||||
import { canvasToBlob } from "./data/blob";
|
import { canvasToBlob } from "./data/blob";
|
||||||
|
import { EXPORT_DATA_TYPES } from "./constants";
|
||||||
const TYPE_ELEMENTS = "excalidraw/elements";
|
|
||||||
|
|
||||||
type ElementsClipboard = {
|
type ElementsClipboard = {
|
||||||
type: typeof TYPE_ELEMENTS;
|
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
|
||||||
created: number;
|
|
||||||
elements: ExcalidrawElement[];
|
elements: ExcalidrawElement[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -31,8 +29,16 @@ export const probablySupportsClipboardBlob =
|
|||||||
"ClipboardItem" in window &&
|
"ClipboardItem" in window &&
|
||||||
"toBlob" in HTMLCanvasElement.prototype;
|
"toBlob" in HTMLCanvasElement.prototype;
|
||||||
|
|
||||||
const isElementsClipboard = (contents: any): contents is ElementsClipboard => {
|
const clipboardContainsElements = (
|
||||||
if (contents?.type === TYPE_ELEMENTS) {
|
contents: any,
|
||||||
|
): contents is { elements: ExcalidrawElement[] } => {
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
EXPORT_DATA_TYPES.excalidraw,
|
||||||
|
EXPORT_DATA_TYPES.excalidrawClipboard,
|
||||||
|
].includes(contents?.type) &&
|
||||||
|
Array.isArray(contents.elements)
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -43,8 +49,7 @@ export const copyToClipboard = async (
|
|||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => {
|
) => {
|
||||||
const contents: ElementsClipboard = {
|
const contents: ElementsClipboard = {
|
||||||
type: TYPE_ELEMENTS,
|
type: EXPORT_DATA_TYPES.excalidrawClipboard,
|
||||||
created: Date.now(),
|
|
||||||
elements: getSelectedElements(elements, appState),
|
elements: getSelectedElements(elements, appState),
|
||||||
};
|
};
|
||||||
const json = JSON.stringify(contents);
|
const json = JSON.stringify(contents);
|
||||||
@ -131,15 +136,9 @@ export const parseClipboard = async (
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const systemClipboardData = JSON.parse(systemClipboard);
|
const systemClipboardData = JSON.parse(systemClipboard);
|
||||||
// system clipboard elements are newer than in-app clipboard
|
if (clipboardContainsElements(systemClipboardData)) {
|
||||||
if (
|
|
||||||
isElementsClipboard(systemClipboardData) &&
|
|
||||||
(!appClipboardData?.created ||
|
|
||||||
appClipboardData.created < systemClipboardData.created)
|
|
||||||
) {
|
|
||||||
return { elements: systemClipboardData.elements };
|
return { elements: systemClipboardData.elements };
|
||||||
}
|
}
|
||||||
// in-app clipboard is newer than system clipboard
|
|
||||||
return appClipboardData;
|
return appClipboardData;
|
||||||
} catch {
|
} catch {
|
||||||
// system clipboard doesn't contain excalidraw elements → return plaintext
|
// system clipboard doesn't contain excalidraw elements → return plaintext
|
||||||
|
@ -84,9 +84,15 @@ export const MIME_TYPES = {
|
|||||||
excalidrawlib: "application/vnd.excalidrawlib+json",
|
excalidrawlib: "application/vnd.excalidrawlib+json",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const EXPORT_DATA_TYPES = {
|
||||||
|
excalidraw: "excalidraw",
|
||||||
|
excalidrawClipboard: "excalidraw/clipboard",
|
||||||
|
excalidrawLibrary: "excalidrawlib",
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const STORAGE_KEYS = {
|
export const STORAGE_KEYS = {
|
||||||
LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
// time in milliseconds
|
// time in milliseconds
|
||||||
export const TAP_TWICE_TIMEOUT = 300;
|
export const TAP_TWICE_TIMEOUT = 300;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { cleanAppStateForExport } from "../appState";
|
import { cleanAppStateForExport } from "../appState";
|
||||||
import { MIME_TYPES } from "../constants";
|
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
|
||||||
import { clearElementsForExport } from "../element";
|
import { clearElementsForExport } from "../element";
|
||||||
import { CanvasError } from "../errors";
|
import { CanvasError } from "../errors";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
@ -121,7 +121,7 @@ export const loadFromBlob = async (
|
|||||||
export const loadLibraryFromBlob = async (blob: Blob) => {
|
export const loadLibraryFromBlob = async (blob: Blob) => {
|
||||||
const contents = await parseFileContents(blob);
|
const contents = await parseFileContents(blob);
|
||||||
const data: LibraryData = JSON.parse(contents);
|
const data: LibraryData = JSON.parse(contents);
|
||||||
if (data.type !== "excalidrawlib") {
|
if (data.type !== EXPORT_DATA_TYPES.excalidrawLibrary) {
|
||||||
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
|
@ -2,7 +2,7 @@ import decodePng from "png-chunks-extract";
|
|||||||
import tEXt from "png-chunk-text";
|
import tEXt from "png-chunk-text";
|
||||||
import encodePng from "png-chunks-encode";
|
import encodePng from "png-chunks-encode";
|
||||||
import { stringToBase64, encode, decode, base64ToString } from "./encode";
|
import { stringToBase64, encode, decode, base64ToString } from "./encode";
|
||||||
import { MIME_TYPES } from "../constants";
|
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// PNG
|
// PNG
|
||||||
@ -67,7 +67,10 @@ export const decodePngMetadata = async (blob: Blob) => {
|
|||||||
const encodedData = JSON.parse(metadata.text);
|
const encodedData = JSON.parse(metadata.text);
|
||||||
if (!("encoded" in encodedData)) {
|
if (!("encoded" in encodedData)) {
|
||||||
// legacy, un-encoded scene JSON
|
// legacy, un-encoded scene JSON
|
||||||
if ("type" in encodedData && encodedData.type === "excalidraw") {
|
if (
|
||||||
|
"type" in encodedData &&
|
||||||
|
encodedData.type === EXPORT_DATA_TYPES.excalidraw
|
||||||
|
) {
|
||||||
return metadata.text;
|
return metadata.text;
|
||||||
}
|
}
|
||||||
throw new Error("FAILED");
|
throw new Error("FAILED");
|
||||||
@ -115,7 +118,10 @@ export const decodeSvgMetadata = async ({ svg }: { svg: string }) => {
|
|||||||
const encodedData = JSON.parse(json);
|
const encodedData = JSON.parse(json);
|
||||||
if (!("encoded" in encodedData)) {
|
if (!("encoded" in encodedData)) {
|
||||||
// legacy, un-encoded scene JSON
|
// legacy, un-encoded scene JSON
|
||||||
if ("type" in encodedData && encodedData.type === "excalidraw") {
|
if (
|
||||||
|
"type" in encodedData &&
|
||||||
|
encodedData.type === EXPORT_DATA_TYPES.excalidraw
|
||||||
|
) {
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
throw new Error("FAILED");
|
throw new Error("FAILED");
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { fileOpen, fileSave } from "browser-fs-access";
|
import { fileOpen, fileSave } from "browser-fs-access";
|
||||||
import { cleanAppStateForExport } from "../appState";
|
import { cleanAppStateForExport } from "../appState";
|
||||||
import { MIME_TYPES } from "../constants";
|
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
|
||||||
import { clearElementsForExport } from "../element";
|
import { clearElementsForExport } from "../element";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
@ -14,7 +14,7 @@ export const serializeAsJSON = (
|
|||||||
): string =>
|
): string =>
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
type: "excalidraw",
|
type: EXPORT_DATA_TYPES.excalidraw,
|
||||||
version: 2,
|
version: 2,
|
||||||
source: window.location.origin,
|
source: window.location.origin,
|
||||||
elements: clearElementsForExport(elements),
|
elements: clearElementsForExport(elements),
|
||||||
@ -69,7 +69,7 @@ export const isValidExcalidrawData = (data?: {
|
|||||||
appState?: any;
|
appState?: any;
|
||||||
}): data is ImportedDataState => {
|
}): data is ImportedDataState => {
|
||||||
return (
|
return (
|
||||||
data?.type === "excalidraw" &&
|
data?.type === EXPORT_DATA_TYPES.excalidraw &&
|
||||||
(!data.elements ||
|
(!data.elements ||
|
||||||
(Array.isArray(data.elements) &&
|
(Array.isArray(data.elements) &&
|
||||||
(!data.appState || typeof data.appState === "object")))
|
(!data.appState || typeof data.appState === "object")))
|
||||||
@ -80,7 +80,7 @@ export const isValidLibrary = (json: any) => {
|
|||||||
return (
|
return (
|
||||||
typeof json === "object" &&
|
typeof json === "object" &&
|
||||||
json &&
|
json &&
|
||||||
json.type === "excalidrawlib" &&
|
json.type === EXPORT_DATA_TYPES.excalidrawLibrary &&
|
||||||
json.version === 1
|
json.version === 1
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -89,7 +89,7 @@ export const saveLibraryAsJSON = async () => {
|
|||||||
const library = await Library.loadLibrary();
|
const library = await Library.loadLibrary();
|
||||||
const serialized = JSON.stringify(
|
const serialized = JSON.stringify(
|
||||||
{
|
{
|
||||||
type: "excalidrawlib",
|
type: EXPORT_DATA_TYPES.excalidrawLibrary,
|
||||||
version: 1,
|
version: 1,
|
||||||
library,
|
library,
|
||||||
},
|
},
|
||||||
|
@ -3,6 +3,7 @@ import { render, waitFor } from "./test-utils";
|
|||||||
import ExcalidrawApp from "../excalidraw-app";
|
import ExcalidrawApp from "../excalidraw-app";
|
||||||
import { API } from "./helpers/api";
|
import { API } from "./helpers/api";
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
|
import { EXPORT_DATA_TYPES } from "../constants";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
@ -29,7 +30,7 @@ describe("appState", () => {
|
|||||||
new Blob(
|
new Blob(
|
||||||
[
|
[
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: "excalidraw",
|
type: EXPORT_DATA_TYPES.excalidraw,
|
||||||
appState: {
|
appState: {
|
||||||
viewBackgroundColor: "#000",
|
viewBackgroundColor: "#000",
|
||||||
},
|
},
|
||||||
|
@ -6,6 +6,7 @@ import { API } from "./helpers/api";
|
|||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
import { waitFor } from "@testing-library/react";
|
import { waitFor } from "@testing-library/react";
|
||||||
import { createUndoAction, createRedoAction } from "../actions/actionHistory";
|
import { createUndoAction, createRedoAction } from "../actions/actionHistory";
|
||||||
|
import { EXPORT_DATA_TYPES } from "../constants";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
@ -76,7 +77,7 @@ describe("history", () => {
|
|||||||
new Blob(
|
new Blob(
|
||||||
[
|
[
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: "excalidraw",
|
type: EXPORT_DATA_TYPES.excalidraw,
|
||||||
appState: {
|
appState: {
|
||||||
...getDefaultAppState(),
|
...getDefaultAppState(),
|
||||||
viewBackgroundColor: "#000",
|
viewBackgroundColor: "#000",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user