feat: support pasting file contents & always prefer system clip (#3257)

This commit is contained in:
David Luzar 2021-03-20 20:20:47 +01:00 committed by GitHub
parent 13d9374cde
commit 94ad8eaa19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 41 additions and 28 deletions

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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");

View File

@ -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,
}, },

View File

@ -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",
}, },

View File

@ -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",