Merge remote-tracking branch 'origin/release' into danieljgeiger-mathjax
This commit is contained in:
commit
7dc728a459
@ -18,7 +18,7 @@ export const actionCopy = register({
|
|||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState, true);
|
const selectedElements = getSelectedElements(elements, appState, true);
|
||||||
|
|
||||||
copyToClipboard(selectedElements, appState, app.files);
|
copyToClipboard(selectedElements, app.files);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
|
@ -2,12 +2,12 @@ import {
|
|||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import { AppState, BinaryFiles } from "./types";
|
import { BinaryFiles } 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 { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants";
|
import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants";
|
||||||
import { isInitializedImageElement } from "./element/typeChecks";
|
import { isInitializedImageElement } from "./element/typeChecks";
|
||||||
import { isPromiseLike } from "./utils";
|
import { isPromiseLike, isTestEnv } from "./utils";
|
||||||
|
|
||||||
type ElementsClipboard = {
|
type ElementsClipboard = {
|
||||||
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
|
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
|
||||||
@ -55,24 +55,40 @@ const clipboardContainsElements = (
|
|||||||
|
|
||||||
export const copyToClipboard = async (
|
export const copyToClipboard = async (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: AppState,
|
|
||||||
files: BinaryFiles | null,
|
files: BinaryFiles | null,
|
||||||
) => {
|
) => {
|
||||||
|
let foundFile = false;
|
||||||
|
|
||||||
|
const _files = elements.reduce((acc, element) => {
|
||||||
|
if (isInitializedImageElement(element)) {
|
||||||
|
foundFile = true;
|
||||||
|
if (files && files[element.fileId]) {
|
||||||
|
acc[element.fileId] = files[element.fileId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as BinaryFiles);
|
||||||
|
|
||||||
|
if (foundFile && !files) {
|
||||||
|
console.warn(
|
||||||
|
"copyToClipboard: attempting to file element(s) without providing associated `files` object.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// select binded text elements when copying
|
// select binded text elements when copying
|
||||||
const contents: ElementsClipboard = {
|
const contents: ElementsClipboard = {
|
||||||
type: EXPORT_DATA_TYPES.excalidrawClipboard,
|
type: EXPORT_DATA_TYPES.excalidrawClipboard,
|
||||||
elements,
|
elements,
|
||||||
files: files
|
files: files ? _files : undefined,
|
||||||
? elements.reduce((acc, element) => {
|
|
||||||
if (isInitializedImageElement(element) && files[element.fileId]) {
|
|
||||||
acc[element.fileId] = files[element.fileId];
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {} as BinaryFiles)
|
|
||||||
: undefined,
|
|
||||||
};
|
};
|
||||||
const json = JSON.stringify(contents);
|
const json = JSON.stringify(contents);
|
||||||
|
|
||||||
|
if (isTestEnv()) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
CLIPBOARD = json;
|
CLIPBOARD = json;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
PREFER_APP_CLIPBOARD = false;
|
PREFER_APP_CLIPBOARD = false;
|
||||||
await copyTextToSystemClipboard(json);
|
await copyTextToSystemClipboard(json);
|
||||||
|
@ -60,6 +60,7 @@ import {
|
|||||||
ENV,
|
ENV,
|
||||||
EVENT,
|
EVENT,
|
||||||
GRID_SIZE,
|
GRID_SIZE,
|
||||||
|
IMAGE_MIME_TYPES,
|
||||||
IMAGE_RENDER_TIMEOUT,
|
IMAGE_RENDER_TIMEOUT,
|
||||||
isAndroid,
|
isAndroid,
|
||||||
isBrave,
|
isBrave,
|
||||||
@ -1650,6 +1651,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
elements: data.elements,
|
elements: data.elements,
|
||||||
files: data.files || null,
|
files: data.files || null,
|
||||||
position: "cursor",
|
position: "cursor",
|
||||||
|
retainSeed: isPlainPaste,
|
||||||
});
|
});
|
||||||
} else if (data.text) {
|
} else if (data.text) {
|
||||||
this.addTextFromPaste(data.text, isPlainPaste);
|
this.addTextFromPaste(data.text, isPlainPaste);
|
||||||
@ -1663,6 +1665,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
files: BinaryFiles | null;
|
files: BinaryFiles | null;
|
||||||
position: { clientX: number; clientY: number } | "cursor" | "center";
|
position: { clientX: number; clientY: number } | "cursor" | "center";
|
||||||
|
retainSeed?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const elements = restoreElements(opts.elements, null);
|
const elements = restoreElements(opts.elements, null);
|
||||||
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
||||||
@ -1700,6 +1703,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
y: element.y + gridY - minY,
|
y: element.y + gridY - minY,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
{
|
||||||
|
randomizeSeed: !opts.retainSeed,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const nextElements = [
|
const nextElements = [
|
||||||
@ -4786,7 +4792,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pointerDownState.drag.hasOccurred = true;
|
pointerDownState.drag.hasOccurred = true;
|
||||||
// prevent dragging even if we're no longer holding cmd/ctrl otherwise
|
// prevent dragging even if we're no longer holding cmd/ctrl otherwise
|
||||||
// it would have weird results (stuff jumping all over the screen)
|
// it would have weird results (stuff jumping all over the screen)
|
||||||
if (selectedElements.length > 0 && !pointerDownState.withCmdOrCtrl) {
|
// Checking for editingElement to avoid jump while editing on mobile #6503
|
||||||
|
if (
|
||||||
|
selectedElements.length > 0 &&
|
||||||
|
!pointerDownState.withCmdOrCtrl &&
|
||||||
|
!this.state.editingElement
|
||||||
|
) {
|
||||||
const [dragX, dragY] = getGridPoint(
|
const [dragX, dragY] = getGridPoint(
|
||||||
pointerCoords.x - pointerDownState.drag.offset.x,
|
pointerCoords.x - pointerDownState.drag.offset.x,
|
||||||
pointerCoords.y - pointerDownState.drag.offset.y,
|
pointerCoords.y - pointerDownState.drag.offset.y,
|
||||||
@ -5809,7 +5820,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
const imageFile = await fileOpen({
|
const imageFile = await fileOpen({
|
||||||
description: "Image",
|
description: "Image",
|
||||||
extensions: ["jpg", "png", "svg", "gif"],
|
extensions: Object.keys(
|
||||||
|
IMAGE_MIME_TYPES,
|
||||||
|
) as (keyof typeof IMAGE_MIME_TYPES)[],
|
||||||
});
|
});
|
||||||
|
|
||||||
const imageElement = this.createImageElement({
|
const imageElement = this.createImageElement({
|
||||||
|
@ -102,7 +102,7 @@ const LibraryMenuItems = ({
|
|||||||
...item,
|
...item,
|
||||||
// duplicate each library item before inserting on canvas to confine
|
// duplicate each library item before inserting on canvas to confine
|
||||||
// ids and bindings to each library item. See #6465
|
// ids and bindings to each library item. See #6465
|
||||||
elements: duplicateElements(item.elements),
|
elements: duplicateElements(item.elements, { randomizeSeed: true }),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -105,20 +105,30 @@ export const CANVAS_ONLY_ACTIONS = ["selectAll"];
|
|||||||
|
|
||||||
export const GRID_SIZE = 20; // TODO make it configurable?
|
export const GRID_SIZE = 20; // TODO make it configurable?
|
||||||
|
|
||||||
export const MIME_TYPES = {
|
export const IMAGE_MIME_TYPES = {
|
||||||
excalidraw: "application/vnd.excalidraw+json",
|
|
||||||
excalidrawlib: "application/vnd.excalidrawlib+json",
|
|
||||||
json: "application/json",
|
|
||||||
svg: "image/svg+xml",
|
svg: "image/svg+xml",
|
||||||
"excalidraw.svg": "image/svg+xml",
|
|
||||||
png: "image/png",
|
png: "image/png",
|
||||||
"excalidraw.png": "image/png",
|
|
||||||
jpg: "image/jpeg",
|
jpg: "image/jpeg",
|
||||||
gif: "image/gif",
|
gif: "image/gif",
|
||||||
webp: "image/webp",
|
webp: "image/webp",
|
||||||
bmp: "image/bmp",
|
bmp: "image/bmp",
|
||||||
ico: "image/x-icon",
|
ico: "image/x-icon",
|
||||||
|
avif: "image/avif",
|
||||||
|
jfif: "image/jfif",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const MIME_TYPES = {
|
||||||
|
json: "application/json",
|
||||||
|
// excalidraw data
|
||||||
|
excalidraw: "application/vnd.excalidraw+json",
|
||||||
|
excalidrawlib: "application/vnd.excalidrawlib+json",
|
||||||
|
// image-encoded excalidraw data
|
||||||
|
"excalidraw.svg": "image/svg+xml",
|
||||||
|
"excalidraw.png": "image/png",
|
||||||
|
// binary
|
||||||
binary: "application/octet-stream",
|
binary: "application/octet-stream",
|
||||||
|
// image
|
||||||
|
...IMAGE_MIME_TYPES,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const EXPORT_DATA_TYPES = {
|
export const EXPORT_DATA_TYPES = {
|
||||||
@ -189,16 +199,6 @@ export const DEFAULT_EXPORT_PADDING = 10; // px
|
|||||||
|
|
||||||
export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440;
|
export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440;
|
||||||
|
|
||||||
export const ALLOWED_IMAGE_MIME_TYPES = [
|
|
||||||
MIME_TYPES.png,
|
|
||||||
MIME_TYPES.jpg,
|
|
||||||
MIME_TYPES.svg,
|
|
||||||
MIME_TYPES.gif,
|
|
||||||
MIME_TYPES.webp,
|
|
||||||
MIME_TYPES.bmp,
|
|
||||||
MIME_TYPES.ico,
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const MAX_ALLOWED_FILE_BYTES = 2 * 1024 * 1024;
|
export const MAX_ALLOWED_FILE_BYTES = 2 * 1024 * 1024;
|
||||||
|
|
||||||
export const SVG_NS = "http://www.w3.org/2000/svg";
|
export const SVG_NS = "http://www.w3.org/2000/svg";
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { cleanAppStateForExport } from "../appState";
|
import { cleanAppStateForExport } from "../appState";
|
||||||
import { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "../constants";
|
import { IMAGE_MIME_TYPES, MIME_TYPES } from "../constants";
|
||||||
import { clearElementsForExport } from "../element";
|
import { clearElementsForExport } from "../element";
|
||||||
import { ExcalidrawElement, FileId } from "../element/types";
|
import { ExcalidrawElement, FileId } from "../element/types";
|
||||||
import { CanvasError } from "../errors";
|
import { CanvasError } from "../errors";
|
||||||
@ -117,11 +117,9 @@ export const isImageFileHandle = (handle: FileSystemHandle | null) => {
|
|||||||
|
|
||||||
export const isSupportedImageFile = (
|
export const isSupportedImageFile = (
|
||||||
blob: Blob | null | undefined,
|
blob: Blob | null | undefined,
|
||||||
): blob is Blob & { type: typeof ALLOWED_IMAGE_MIME_TYPES[number] } => {
|
): blob is Blob & { type: ValueOf<typeof IMAGE_MIME_TYPES> } => {
|
||||||
const { type } = blob || {};
|
const { type } = blob || {};
|
||||||
return (
|
return !!type && (Object.values(IMAGE_MIME_TYPES) as string[]).includes(type);
|
||||||
!!type && (ALLOWED_IMAGE_MIME_TYPES as readonly string[]).includes(type)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadSceneOrLibraryFromBlob = async (
|
export const loadSceneOrLibraryFromBlob = async (
|
||||||
|
@ -8,16 +8,7 @@ import { EVENT, MIME_TYPES } from "../constants";
|
|||||||
import { AbortError } from "../errors";
|
import { AbortError } from "../errors";
|
||||||
import { debounce } from "../utils";
|
import { debounce } from "../utils";
|
||||||
|
|
||||||
type FILE_EXTENSION =
|
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
|
||||||
| "gif"
|
|
||||||
| "jpg"
|
|
||||||
| "png"
|
|
||||||
| "excalidraw.png"
|
|
||||||
| "svg"
|
|
||||||
| "excalidraw.svg"
|
|
||||||
| "json"
|
|
||||||
| "excalidraw"
|
|
||||||
| "excalidrawlib";
|
|
||||||
|
|
||||||
const INPUT_CHANGE_INTERVAL_MS = 500;
|
const INPUT_CHANGE_INTERVAL_MS = 500;
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ import {
|
|||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { arrayToMap, getUpdatedTimestamp, isTestEnv } from "../utils";
|
import { arrayToMap, getUpdatedTimestamp, isTestEnv } from "../utils";
|
||||||
import { randomInteger, randomId } from "../random";
|
import { randomInteger, randomId } from "../random";
|
||||||
import { mutateElement, newElementWith } from "./mutateElement";
|
import { bumpVersion, mutateElement, newElementWith } from "./mutateElement";
|
||||||
import { getNewGroupIdsForDuplication } from "../groups";
|
import { getNewGroupIdsForDuplication } from "../groups";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { getElementAbsoluteCoords } from ".";
|
import { getElementAbsoluteCoords } from ".";
|
||||||
@ -569,8 +569,16 @@ export const duplicateElement = <TElement extends ExcalidrawElement>(
|
|||||||
* it's advised to supply the whole elements array, or sets of elements that
|
* it's advised to supply the whole elements array, or sets of elements that
|
||||||
* are encapsulated (such as library items), if the purpose is to retain
|
* are encapsulated (such as library items), if the purpose is to retain
|
||||||
* bindings to the cloned elements intact.
|
* bindings to the cloned elements intact.
|
||||||
|
*
|
||||||
|
* NOTE by default does not randomize or regenerate anything except the id.
|
||||||
*/
|
*/
|
||||||
export const duplicateElements = (elements: readonly ExcalidrawElement[]) => {
|
export const duplicateElements = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
opts?: {
|
||||||
|
/** NOTE also updates version flags and `updated` */
|
||||||
|
randomizeSeed: boolean;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
const clonedElements: ExcalidrawElement[] = [];
|
const clonedElements: ExcalidrawElement[] = [];
|
||||||
|
|
||||||
const origElementsMap = arrayToMap(elements);
|
const origElementsMap = arrayToMap(elements);
|
||||||
@ -604,6 +612,11 @@ export const duplicateElements = (elements: readonly ExcalidrawElement[]) => {
|
|||||||
|
|
||||||
clonedElement.id = maybeGetNewId(element.id)!;
|
clonedElement.id = maybeGetNewId(element.id)!;
|
||||||
|
|
||||||
|
if (opts?.randomizeSeed) {
|
||||||
|
clonedElement.seed = randomInteger();
|
||||||
|
bumpVersion(clonedElement);
|
||||||
|
}
|
||||||
|
|
||||||
if (clonedElement.groupIds) {
|
if (clonedElement.groupIds) {
|
||||||
clonedElement.groupIds = clonedElement.groupIds.map((groupId) => {
|
clonedElement.groupIds = clonedElement.groupIds.map((groupId) => {
|
||||||
if (!groupNewIdsMap.has(groupId)) {
|
if (!groupNewIdsMap.has(groupId)) {
|
||||||
|
@ -220,15 +220,7 @@ export const exportToClipboard = async (
|
|||||||
} else if (opts.type === "png") {
|
} else if (opts.type === "png") {
|
||||||
await copyBlobToClipboardAsPng(exportToBlob(opts));
|
await copyBlobToClipboardAsPng(exportToBlob(opts));
|
||||||
} else if (opts.type === "json") {
|
} else if (opts.type === "json") {
|
||||||
const appState = {
|
await copyToClipboard(opts.elements, opts.files);
|
||||||
offsetTop: 0,
|
|
||||||
offsetLeft: 0,
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
...getDefaultAppState(),
|
|
||||||
...opts.appState,
|
|
||||||
};
|
|
||||||
await copyToClipboard(opts.elements, appState, opts.files);
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Invalid export type");
|
throw new Error("Invalid export type");
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { render, waitFor, GlobalTestState } from "./test-utils";
|
import {
|
||||||
|
render,
|
||||||
|
waitFor,
|
||||||
|
GlobalTestState,
|
||||||
|
createPasteEvent,
|
||||||
|
} from "./test-utils";
|
||||||
import { Pointer, Keyboard } from "./helpers/ui";
|
import { Pointer, Keyboard } from "./helpers/ui";
|
||||||
import ExcalidrawApp from "../excalidraw-app";
|
import ExcalidrawApp from "../excalidraw-app";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
@ -9,6 +14,8 @@ import {
|
|||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { getElementBounds } from "../element";
|
import { getElementBounds } from "../element";
|
||||||
import { NormalizedZoomValue } from "../types";
|
import { NormalizedZoomValue } from "../types";
|
||||||
|
import { API } from "./helpers/api";
|
||||||
|
import { copyToClipboard } from "../clipboard";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
@ -35,38 +42,28 @@ const setClipboardText = (text: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendPasteEvent = () => {
|
const sendPasteEvent = (text?: string) => {
|
||||||
const clipboardEvent = new Event("paste", {
|
const clipboardEvent = createPasteEvent(
|
||||||
bubbles: true,
|
text || (() => window.navigator.clipboard.readText()),
|
||||||
cancelable: true,
|
);
|
||||||
composed: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// set `clipboardData` properties.
|
|
||||||
// @ts-ignore
|
|
||||||
clipboardEvent.clipboardData = {
|
|
||||||
getData: () => window.navigator.clipboard.readText(),
|
|
||||||
files: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
document.dispatchEvent(clipboardEvent);
|
document.dispatchEvent(clipboardEvent);
|
||||||
};
|
};
|
||||||
|
|
||||||
const pasteWithCtrlCmdShiftV = () => {
|
const pasteWithCtrlCmdShiftV = (text?: string) => {
|
||||||
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
|
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
|
||||||
//triggering keydown with an empty clipboard
|
//triggering keydown with an empty clipboard
|
||||||
Keyboard.keyPress(KEYS.V);
|
Keyboard.keyPress(KEYS.V);
|
||||||
//triggering paste event with faked clipboard
|
//triggering paste event with faked clipboard
|
||||||
sendPasteEvent();
|
sendPasteEvent(text);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const pasteWithCtrlCmdV = () => {
|
const pasteWithCtrlCmdV = (text?: string) => {
|
||||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
//triggering keydown with an empty clipboard
|
//triggering keydown with an empty clipboard
|
||||||
Keyboard.keyPress(KEYS.V);
|
Keyboard.keyPress(KEYS.V);
|
||||||
//triggering paste event with faked clipboard
|
//triggering paste event with faked clipboard
|
||||||
sendPasteEvent();
|
sendPasteEvent(text);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -89,6 +86,32 @@ beforeEach(async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("general paste behavior", () => {
|
||||||
|
it("should randomize seed on paste", async () => {
|
||||||
|
const rectangle = API.createElement({ type: "rectangle" });
|
||||||
|
const clipboardJSON = (await copyToClipboard([rectangle], null))!;
|
||||||
|
|
||||||
|
pasteWithCtrlCmdV(clipboardJSON);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(h.elements.length).toBe(1);
|
||||||
|
expect(h.elements[0].seed).not.toBe(rectangle.seed);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should retain seed on shift-paste", async () => {
|
||||||
|
const rectangle = API.createElement({ type: "rectangle" });
|
||||||
|
const clipboardJSON = (await copyToClipboard([rectangle], null))!;
|
||||||
|
|
||||||
|
// assert we don't randomize seed on shift-paste
|
||||||
|
pasteWithCtrlCmdShiftV(clipboardJSON);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(h.elements.length).toBe(1);
|
||||||
|
expect(h.elements[0].seed).toBe(rectangle.seed);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("paste text as single lines", () => {
|
describe("paste text as single lines", () => {
|
||||||
it("should create an element for each line when copying with Ctrl/Cmd+V", async () => {
|
it("should create an element for each line when copying with Ctrl/Cmd+V", async () => {
|
||||||
const text = "sajgfakfn\naaksfnknas\nakefnkasf";
|
const text = "sajgfakfn\naaksfnknas\nakefnkasf";
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { GlobalTestState, render, waitFor } from "./test-utils";
|
import {
|
||||||
|
createPasteEvent,
|
||||||
|
GlobalTestState,
|
||||||
|
render,
|
||||||
|
waitFor,
|
||||||
|
} from "./test-utils";
|
||||||
import { UI, Pointer } from "./helpers/ui";
|
import { UI, Pointer } from "./helpers/ui";
|
||||||
import { API } from "./helpers/api";
|
import { API } from "./helpers/api";
|
||||||
import { actionFlipHorizontal, actionFlipVertical } from "../actions";
|
import { actionFlipHorizontal, actionFlipVertical } from "../actions";
|
||||||
@ -680,19 +685,7 @@ describe("freedraw", () => {
|
|||||||
describe("image", () => {
|
describe("image", () => {
|
||||||
const createImage = async () => {
|
const createImage = async () => {
|
||||||
const sendPasteEvent = (file?: File) => {
|
const sendPasteEvent = (file?: File) => {
|
||||||
const clipboardEvent = new Event("paste", {
|
const clipboardEvent = createPasteEvent("", file ? [file] : []);
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
composed: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// set `clipboardData` properties.
|
|
||||||
// @ts-ignore
|
|
||||||
clipboardEvent.clipboardData = {
|
|
||||||
getData: () => window.navigator.clipboard.readText(),
|
|
||||||
files: [file],
|
|
||||||
};
|
|
||||||
|
|
||||||
document.dispatchEvent(clipboardEvent);
|
document.dispatchEvent(clipboardEvent);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -190,3 +190,24 @@ export const toggleMenu = (container: HTMLElement) => {
|
|||||||
// open menu
|
// open menu
|
||||||
fireEvent.click(container.querySelector(".dropdown-menu-button")!);
|
fireEvent.click(container.querySelector(".dropdown-menu-button")!);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createPasteEvent = (
|
||||||
|
text:
|
||||||
|
| string
|
||||||
|
| /* getData function */ ((type: string) => string | Promise<string>),
|
||||||
|
files?: File[],
|
||||||
|
) => {
|
||||||
|
return Object.assign(
|
||||||
|
new Event("paste", {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
composed: true,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
clipboardData: {
|
||||||
|
getData: typeof text === "string" ? () => text : text,
|
||||||
|
files: files || [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -36,9 +36,9 @@ import {
|
|||||||
SubtypeRecord,
|
SubtypeRecord,
|
||||||
} from "./subtypes";
|
} from "./subtypes";
|
||||||
import type { FileSystemHandle } from "./data/filesystem";
|
import type { FileSystemHandle } from "./data/filesystem";
|
||||||
import type { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
|
import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
|
||||||
import { ContextMenuItems } from "./components/ContextMenu";
|
import { ContextMenuItems } from "./components/ContextMenu";
|
||||||
import { Merge, ForwardRef } from "./utility-types";
|
import { Merge, ForwardRef, ValueOf } from "./utility-types";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export type Point = Readonly<RoughPoint>;
|
export type Point = Readonly<RoughPoint>;
|
||||||
@ -67,7 +67,7 @@ export type DataURL = string & { _brand: "DataURL" };
|
|||||||
|
|
||||||
export type BinaryFileData = {
|
export type BinaryFileData = {
|
||||||
mimeType:
|
mimeType:
|
||||||
| typeof ALLOWED_IMAGE_MIME_TYPES[number]
|
| ValueOf<typeof IMAGE_MIME_TYPES>
|
||||||
// future user or unknown file type
|
// future user or unknown file type
|
||||||
| typeof MIME_TYPES.binary;
|
| typeof MIME_TYPES.binary;
|
||||||
id: FileId;
|
id: FileId;
|
||||||
@ -430,7 +430,7 @@ export type AppClassProperties = {
|
|||||||
FileId,
|
FileId,
|
||||||
{
|
{
|
||||||
image: HTMLImageElement | Promise<HTMLImageElement>;
|
image: HTMLImageElement | Promise<HTMLImageElement>;
|
||||||
mimeType: typeof ALLOWED_IMAGE_MIME_TYPES[number];
|
mimeType: ValueOf<typeof IMAGE_MIME_TYPES>;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user