mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-09-09 04:45:10 -04:00
fix: pasting not working in firefox (#9947)
This commit is contained in:
parent
3bdaafe4b5
commit
b9d27d308e
@ -259,13 +259,17 @@ export const IMAGE_MIME_TYPES = {
|
||||
jfif: "image/jfif",
|
||||
} as const;
|
||||
|
||||
export const MIME_TYPES = {
|
||||
export const STRING_MIME_TYPES = {
|
||||
text: "text/plain",
|
||||
html: "text/html",
|
||||
json: "application/json",
|
||||
// excalidraw data
|
||||
excalidraw: "application/vnd.excalidraw+json",
|
||||
excalidrawlib: "application/vnd.excalidrawlib+json",
|
||||
} as const;
|
||||
|
||||
export const MIME_TYPES = {
|
||||
...STRING_MIME_TYPES,
|
||||
// image-encoded excalidraw data
|
||||
"excalidraw.svg": "image/svg+xml",
|
||||
"excalidraw.png": "image/png",
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {
|
||||
createPasteEvent,
|
||||
parseClipboard,
|
||||
parseDataTransferEvent,
|
||||
serializeAsClipboardJSON,
|
||||
} from "./clipboard";
|
||||
import { API } from "./tests/helpers/api";
|
||||
@ -13,7 +14,9 @@ describe("parseClipboard()", () => {
|
||||
|
||||
text = "123";
|
||||
clipboardData = await parseClipboard(
|
||||
createPasteEvent({ types: { "text/plain": text } }),
|
||||
await parseDataTransferEvent(
|
||||
createPasteEvent({ types: { "text/plain": text } }),
|
||||
),
|
||||
);
|
||||
expect(clipboardData.text).toBe(text);
|
||||
|
||||
@ -21,7 +24,9 @@ describe("parseClipboard()", () => {
|
||||
|
||||
text = "[123]";
|
||||
clipboardData = await parseClipboard(
|
||||
createPasteEvent({ types: { "text/plain": text } }),
|
||||
await parseDataTransferEvent(
|
||||
createPasteEvent({ types: { "text/plain": text } }),
|
||||
),
|
||||
);
|
||||
expect(clipboardData.text).toBe(text);
|
||||
|
||||
@ -29,7 +34,9 @@ describe("parseClipboard()", () => {
|
||||
|
||||
text = JSON.stringify({ val: 42 });
|
||||
clipboardData = await parseClipboard(
|
||||
createPasteEvent({ types: { "text/plain": text } }),
|
||||
await parseDataTransferEvent(
|
||||
createPasteEvent({ types: { "text/plain": text } }),
|
||||
),
|
||||
);
|
||||
expect(clipboardData.text).toBe(text);
|
||||
});
|
||||
@ -39,11 +46,13 @@ describe("parseClipboard()", () => {
|
||||
|
||||
const json = serializeAsClipboardJSON({ elements: [rect], files: null });
|
||||
const clipboardData = await parseClipboard(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/plain": json,
|
||||
},
|
||||
}),
|
||||
await parseDataTransferEvent(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/plain": json,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(clipboardData.elements).toEqual([rect]);
|
||||
});
|
||||
@ -56,21 +65,25 @@ describe("parseClipboard()", () => {
|
||||
// -------------------------------------------------------------------------
|
||||
json = serializeAsClipboardJSON({ elements: [rect], files: null });
|
||||
clipboardData = await parseClipboard(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/html": json,
|
||||
},
|
||||
}),
|
||||
await parseDataTransferEvent(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/html": json,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(clipboardData.elements).toEqual([rect]);
|
||||
// -------------------------------------------------------------------------
|
||||
json = serializeAsClipboardJSON({ elements: [rect], files: null });
|
||||
clipboardData = await parseClipboard(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/html": `<div> ${json}</div>`,
|
||||
},
|
||||
}),
|
||||
await parseDataTransferEvent(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/html": `<div> ${json}</div>`,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(clipboardData.elements).toEqual([rect]);
|
||||
// -------------------------------------------------------------------------
|
||||
@ -80,11 +93,13 @@ describe("parseClipboard()", () => {
|
||||
let clipboardData;
|
||||
// -------------------------------------------------------------------------
|
||||
clipboardData = await parseClipboard(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/html": `<img src="https://example.com/image.png" />`,
|
||||
},
|
||||
}),
|
||||
await parseDataTransferEvent(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/html": `<img src="https://example.com/image.png" />`,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(clipboardData.mixedContent).toEqual([
|
||||
{
|
||||
@ -94,11 +109,13 @@ describe("parseClipboard()", () => {
|
||||
]);
|
||||
// -------------------------------------------------------------------------
|
||||
clipboardData = await parseClipboard(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/html": `<div><img src="https://example.com/image.png" /></div><a><img src="https://example.com/image2.png" /></a>`,
|
||||
},
|
||||
}),
|
||||
await parseDataTransferEvent(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/html": `<div><img src="https://example.com/image.png" /></div><a><img src="https://example.com/image2.png" /></a>`,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(clipboardData.mixedContent).toEqual([
|
||||
{
|
||||
@ -114,11 +131,13 @@ describe("parseClipboard()", () => {
|
||||
|
||||
it("should parse text content alongside <image> `src` urls out of text/html", async () => {
|
||||
const clipboardData = await parseClipboard(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/html": `<a href="https://example.com">hello </a><div><img src="https://example.com/image.png" /></div><b>my friend!</b>`,
|
||||
},
|
||||
}),
|
||||
await parseDataTransferEvent(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/html": `<a href="https://example.com">hello </a><div><img src="https://example.com/image.png" /></div><b>my friend!</b>`,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(clipboardData.mixedContent).toEqual([
|
||||
{
|
||||
@ -141,14 +160,16 @@ describe("parseClipboard()", () => {
|
||||
let clipboardData;
|
||||
// -------------------------------------------------------------------------
|
||||
clipboardData = await parseClipboard(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/plain": `a b
|
||||
1 2
|
||||
4 5
|
||||
7 10`,
|
||||
},
|
||||
}),
|
||||
await parseDataTransferEvent(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/plain": `a b
|
||||
1 2
|
||||
4 5
|
||||
7 10`,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(clipboardData.spreadsheet).toEqual({
|
||||
title: "b",
|
||||
@ -157,14 +178,16 @@ describe("parseClipboard()", () => {
|
||||
});
|
||||
// -------------------------------------------------------------------------
|
||||
clipboardData = await parseClipboard(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/html": `a b
|
||||
1 2
|
||||
4 5
|
||||
7 10`,
|
||||
},
|
||||
}),
|
||||
await parseDataTransferEvent(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/html": `a b
|
||||
1 2
|
||||
4 5
|
||||
7 10`,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(clipboardData.spreadsheet).toEqual({
|
||||
title: "b",
|
||||
@ -173,19 +196,21 @@ describe("parseClipboard()", () => {
|
||||
});
|
||||
// -------------------------------------------------------------------------
|
||||
clipboardData = await parseClipboard(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/html": `<html>
|
||||
<body>
|
||||
<!--StartFragment--><google-sheets-html-origin><style type="text/css"><!--td {border: 1px solid #cccccc;}br {mso-data-placement:same-cell;}--></style><table xmlns="http://www.w3.org/1999/xhtml" cellspacing="0" cellpadding="0" dir="ltr" border="1" style="table-layout:fixed;font-size:10pt;font-family:Arial;width:0px;border-collapse:collapse;border:none"><colgroup><col width="100"/><col width="100"/></colgroup><tbody><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{"1":2,"2":"a"}">a</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{"1":2,"2":"b"}">b</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":1}">1</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":2}">2</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":4}">4</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":5}">5</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":7}">7</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":10}">10</td></tr></tbody></table><!--EndFragment-->
|
||||
</body>
|
||||
</html>`,
|
||||
"text/plain": `a b
|
||||
1 2
|
||||
4 5
|
||||
7 10`,
|
||||
},
|
||||
}),
|
||||
await parseDataTransferEvent(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/html": `<html>
|
||||
<body>
|
||||
<!--StartFragment--><google-sheets-html-origin><style type="text/css"><!--td {border: 1px solid #cccccc;}br {mso-data-placement:same-cell;}--></style><table xmlns="http://www.w3.org/1999/xhtml" cellspacing="0" cellpadding="0" dir="ltr" border="1" style="table-layout:fixed;font-size:10pt;font-family:Arial;width:0px;border-collapse:collapse;border:none"><colgroup><col width="100"/><col width="100"/></colgroup><tbody><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{"1":2,"2":"a"}">a</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{"1":2,"2":"b"}">b</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":1}">1</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":2}">2</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":4}">4</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":5}">5</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":7}">7</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":10}">10</td></tr></tbody></table><!--EndFragment-->
|
||||
</body>
|
||||
</html>`,
|
||||
"text/plain": `a b
|
||||
1 2
|
||||
4 5
|
||||
7 10`,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(clipboardData.spreadsheet).toEqual({
|
||||
title: "b",
|
||||
|
@ -17,15 +17,25 @@ import {
|
||||
|
||||
import { getContainingFrame } from "@excalidraw/element";
|
||||
|
||||
import type { ValueOf } from "@excalidraw/common/utility-types";
|
||||
|
||||
import type { IMAGE_MIME_TYPES, STRING_MIME_TYPES } from "@excalidraw/common";
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import { ExcalidrawError } from "./errors";
|
||||
import { createFile, isSupportedImageFileType } from "./data/blob";
|
||||
import {
|
||||
createFile,
|
||||
getFileHandle,
|
||||
isSupportedImageFileType,
|
||||
} from "./data/blob";
|
||||
|
||||
import { tryParseSpreadsheet, VALID_SPREADSHEET } from "./charts";
|
||||
|
||||
import type { FileSystemHandle } from "./data/filesystem";
|
||||
|
||||
import type { Spreadsheet } from "./charts";
|
||||
|
||||
import type { BinaryFiles } from "./types";
|
||||
@ -102,10 +112,11 @@ export const createPasteEvent = ({
|
||||
if (typeof value !== "string") {
|
||||
files = files || [];
|
||||
files.push(value);
|
||||
event.clipboardData?.items.add(value);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
event.clipboardData?.setData(type, value);
|
||||
event.clipboardData?.items.add(value, type);
|
||||
if (event.clipboardData?.getData(type) !== value) {
|
||||
throw new Error(`Failed to set "${type}" as clipboardData item`);
|
||||
}
|
||||
@ -230,14 +241,10 @@ function parseHTMLTree(el: ChildNode) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const maybeParseHTMLPaste = (
|
||||
event: ClipboardEvent,
|
||||
const maybeParseHTMLDataItem = (
|
||||
dataItem: ParsedDataTransferItemType<typeof MIME_TYPES["html"]>,
|
||||
): { type: "mixedContent"; value: PastedMixedContent } | null => {
|
||||
const html = event.clipboardData?.getData(MIME_TYPES.html);
|
||||
|
||||
if (!html) {
|
||||
return null;
|
||||
}
|
||||
const html = dataItem.value;
|
||||
|
||||
try {
|
||||
const doc = new DOMParser().parseFromString(html, MIME_TYPES.html);
|
||||
@ -333,18 +340,21 @@ export const readSystemClipboard = async () => {
|
||||
* Parses "paste" ClipboardEvent.
|
||||
*/
|
||||
const parseClipboardEventTextData = async (
|
||||
event: ClipboardEvent,
|
||||
dataList: ParsedDataTranferList,
|
||||
isPlainPaste = false,
|
||||
): Promise<ParsedClipboardEventTextData> => {
|
||||
try {
|
||||
const mixedContent = !isPlainPaste && event && maybeParseHTMLPaste(event);
|
||||
const htmlItem = dataList.findByType(MIME_TYPES.html);
|
||||
|
||||
const mixedContent =
|
||||
!isPlainPaste && htmlItem && maybeParseHTMLDataItem(htmlItem);
|
||||
|
||||
if (mixedContent) {
|
||||
if (mixedContent.value.every((item) => item.type === "text")) {
|
||||
return {
|
||||
type: "text",
|
||||
value:
|
||||
event.clipboardData?.getData(MIME_TYPES.text) ||
|
||||
dataList.getData(MIME_TYPES.text) ??
|
||||
mixedContent.value
|
||||
.map((item) => item.value)
|
||||
.join("\n")
|
||||
@ -355,23 +365,150 @@ const parseClipboardEventTextData = async (
|
||||
return mixedContent;
|
||||
}
|
||||
|
||||
const text = event.clipboardData?.getData(MIME_TYPES.text);
|
||||
|
||||
return { type: "text", value: (text || "").trim() };
|
||||
return {
|
||||
type: "text",
|
||||
value: (dataList.getData(MIME_TYPES.text) || "").trim(),
|
||||
};
|
||||
} catch {
|
||||
return { type: "text", value: "" };
|
||||
}
|
||||
};
|
||||
|
||||
type AllowedParsedDataTransferItem =
|
||||
| {
|
||||
type: ValueOf<typeof IMAGE_MIME_TYPES>;
|
||||
kind: "file";
|
||||
file: File;
|
||||
fileHandle: FileSystemHandle | null;
|
||||
}
|
||||
| { type: ValueOf<typeof STRING_MIME_TYPES>; kind: "string"; value: string };
|
||||
|
||||
type ParsedDataTransferItem =
|
||||
| {
|
||||
type: string;
|
||||
kind: "file";
|
||||
file: File;
|
||||
fileHandle: FileSystemHandle | null;
|
||||
}
|
||||
| { type: string; kind: "string"; value: string };
|
||||
|
||||
type ParsedDataTransferItemType<
|
||||
T extends AllowedParsedDataTransferItem["type"],
|
||||
> = AllowedParsedDataTransferItem & { type: T };
|
||||
|
||||
export type ParsedDataTransferFile = Extract<
|
||||
AllowedParsedDataTransferItem,
|
||||
{ kind: "file" }
|
||||
>;
|
||||
|
||||
type ParsedDataTranferList = ParsedDataTransferItem[] & {
|
||||
/**
|
||||
* Only allows filtering by known `string` data types, since `file`
|
||||
* types can have multiple items of the same type (e.g. multiple image files)
|
||||
* unlike `string` data transfer items.
|
||||
*/
|
||||
findByType: typeof findDataTransferItemType;
|
||||
/**
|
||||
* Only allows filtering by known `string` data types, since `file`
|
||||
* types can have multiple items of the same type (e.g. multiple image files)
|
||||
* unlike `string` data transfer items.
|
||||
*/
|
||||
getData: typeof getDataTransferItemData;
|
||||
getFiles: typeof getDataTransferFiles;
|
||||
};
|
||||
|
||||
const findDataTransferItemType = function <
|
||||
T extends ValueOf<typeof STRING_MIME_TYPES>,
|
||||
>(this: ParsedDataTranferList, type: T): ParsedDataTransferItemType<T> | null {
|
||||
return (
|
||||
this.find(
|
||||
(item): item is ParsedDataTransferItemType<T> => item.type === type,
|
||||
) || null
|
||||
);
|
||||
};
|
||||
const getDataTransferItemData = function <
|
||||
T extends ValueOf<typeof STRING_MIME_TYPES>,
|
||||
>(
|
||||
this: ParsedDataTranferList,
|
||||
type: T,
|
||||
):
|
||||
| ParsedDataTransferItemType<ValueOf<typeof STRING_MIME_TYPES>>["value"]
|
||||
| null {
|
||||
const item = this.find(
|
||||
(
|
||||
item,
|
||||
): item is ParsedDataTransferItemType<ValueOf<typeof STRING_MIME_TYPES>> =>
|
||||
item.type === type,
|
||||
);
|
||||
|
||||
return item?.value ?? null;
|
||||
};
|
||||
|
||||
const getDataTransferFiles = function (
|
||||
this: ParsedDataTranferList,
|
||||
): ParsedDataTransferFile[] {
|
||||
return this.filter(
|
||||
(item): item is ParsedDataTransferFile => item.kind === "file",
|
||||
);
|
||||
};
|
||||
|
||||
export const parseDataTransferEvent = async (
|
||||
event: ClipboardEvent | DragEvent | React.DragEvent<HTMLDivElement>,
|
||||
): Promise<ParsedDataTranferList> => {
|
||||
let items: DataTransferItemList | undefined = undefined;
|
||||
|
||||
if (isClipboardEvent(event)) {
|
||||
items = event.clipboardData?.items;
|
||||
} else {
|
||||
const dragEvent = event;
|
||||
items = dragEvent.dataTransfer?.items;
|
||||
}
|
||||
|
||||
const dataItems = (
|
||||
await Promise.all(
|
||||
Array.from(items || []).map(
|
||||
async (item): Promise<ParsedDataTransferItem | null> => {
|
||||
if (item.kind === "file") {
|
||||
const file = item.getAsFile();
|
||||
if (file) {
|
||||
const fileHandle = await getFileHandle(item);
|
||||
return { type: file.type, kind: "file", file, fileHandle };
|
||||
}
|
||||
} else if (item.kind === "string") {
|
||||
const { type } = item;
|
||||
let value: string;
|
||||
if ("clipboardData" in event && event.clipboardData) {
|
||||
value = event.clipboardData?.getData(type);
|
||||
} else {
|
||||
value = await new Promise<string>((resolve) => {
|
||||
item.getAsString((str) => resolve(str));
|
||||
});
|
||||
}
|
||||
return { type, kind: "string", value };
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
),
|
||||
)
|
||||
).filter((data): data is ParsedDataTransferItem => data != null);
|
||||
|
||||
return Object.assign(dataItems, {
|
||||
findByType: findDataTransferItemType,
|
||||
getData: getDataTransferItemData,
|
||||
getFiles: getDataTransferFiles,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Attempts to parse clipboard event.
|
||||
*/
|
||||
export const parseClipboard = async (
|
||||
event: ClipboardEvent,
|
||||
dataList: ParsedDataTranferList,
|
||||
isPlainPaste = false,
|
||||
): Promise<ClipboardData> => {
|
||||
const parsedEventData = await parseClipboardEventTextData(
|
||||
event,
|
||||
dataList,
|
||||
isPlainPaste,
|
||||
);
|
||||
|
||||
|
@ -324,7 +324,13 @@ import {
|
||||
isEraserActive,
|
||||
isHandToolActive,
|
||||
} from "../appState";
|
||||
import { copyTextToSystemClipboard, parseClipboard } from "../clipboard";
|
||||
import {
|
||||
copyTextToSystemClipboard,
|
||||
parseClipboard,
|
||||
parseDataTransferEvent,
|
||||
type ParsedDataTransferFile,
|
||||
} from "../clipboard";
|
||||
|
||||
import { exportCanvas, loadFromBlob } from "../data";
|
||||
import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
|
||||
import { restore, restoreElements } from "../data/restore";
|
||||
@ -346,7 +352,6 @@ import {
|
||||
generateIdFromFile,
|
||||
getDataURL,
|
||||
getDataURL_sync,
|
||||
getFilesFromEvent,
|
||||
ImageURLToFile,
|
||||
isImageFileHandle,
|
||||
isSupportedImageFile,
|
||||
@ -3070,7 +3075,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
// TODO: Cover with tests
|
||||
private async insertClipboardContent(
|
||||
data: ClipboardData,
|
||||
filesData: Awaited<ReturnType<typeof getFilesFromEvent>>,
|
||||
dataTransferFiles: ParsedDataTransferFile[],
|
||||
isPlainPaste: boolean,
|
||||
) {
|
||||
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
||||
@ -3088,7 +3093,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
// ------------------- Mixed content with no files -------------------
|
||||
if (filesData.length === 0 && !isPlainPaste && data.mixedContent) {
|
||||
if (dataTransferFiles.length === 0 && !isPlainPaste && data.mixedContent) {
|
||||
await this.addElementsFromMixedContentPaste(data.mixedContent, {
|
||||
isPlainPaste,
|
||||
sceneX,
|
||||
@ -3109,9 +3114,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
// ------------------- Images or SVG code -------------------
|
||||
const imageFiles = filesData
|
||||
.map((data) => data.file)
|
||||
.filter((file): file is File => isSupportedImageFile(file));
|
||||
const imageFiles = dataTransferFiles.map((data) => data.file);
|
||||
|
||||
if (imageFiles.length === 0 && data.text && !isPlainPaste) {
|
||||
const trimmedText = data.text.trim();
|
||||
@ -3256,8 +3259,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
// must be called in the same frame (thus before any awaits) as the paste
|
||||
// event else some browsers (FF...) will clear the clipboardData
|
||||
// (something something security)
|
||||
const filesData = await getFilesFromEvent(event);
|
||||
const data = await parseClipboard(event, isPlainPaste);
|
||||
const dataTransferList = await parseDataTransferEvent(event);
|
||||
|
||||
const filesList = dataTransferList.getFiles();
|
||||
|
||||
const data = await parseClipboard(dataTransferList, isPlainPaste);
|
||||
|
||||
if (this.props.onPaste) {
|
||||
try {
|
||||
@ -3269,7 +3275,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
}
|
||||
|
||||
await this.insertClipboardContent(data, filesData, isPlainPaste);
|
||||
await this.insertClipboardContent(data, filesList, isPlainPaste);
|
||||
|
||||
this.setActiveTool({ type: this.defaultSelectionTool }, true);
|
||||
event?.preventDefault();
|
||||
},
|
||||
@ -10479,12 +10486,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||
event,
|
||||
this.state,
|
||||
);
|
||||
const dataTransferList = await parseDataTransferEvent(event);
|
||||
|
||||
// must be retrieved first, in the same frame
|
||||
const filesData = await getFilesFromEvent(event);
|
||||
const fileItems = dataTransferList.getFiles();
|
||||
|
||||
if (filesData.length === 1) {
|
||||
const { file, fileHandle } = filesData[0];
|
||||
if (fileItems.length === 1) {
|
||||
const { file, fileHandle } = fileItems[0];
|
||||
|
||||
if (
|
||||
file &&
|
||||
@ -10516,15 +10524,15 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
}
|
||||
|
||||
const imageFiles = filesData
|
||||
const imageFiles = fileItems
|
||||
.map((data) => data.file)
|
||||
.filter((file): file is File => isSupportedImageFile(file));
|
||||
.filter((file) => isSupportedImageFile(file));
|
||||
|
||||
if (imageFiles.length > 0 && this.isToolSupported("image")) {
|
||||
return this.insertImages(imageFiles, sceneX, sceneY);
|
||||
}
|
||||
|
||||
const libraryJSON = event.dataTransfer.getData(MIME_TYPES.excalidrawlib);
|
||||
const libraryJSON = dataTransferList.getData(MIME_TYPES.excalidrawlib);
|
||||
if (libraryJSON && typeof libraryJSON === "string") {
|
||||
try {
|
||||
const libraryItems = parseLibraryJSON(libraryJSON);
|
||||
@ -10539,16 +10547,18 @@ class App extends React.Component<AppProps, AppState> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (filesData.length > 0) {
|
||||
const { file, fileHandle } = filesData[0];
|
||||
if (fileItems.length > 0) {
|
||||
const { file, fileHandle } = fileItems[0];
|
||||
if (file) {
|
||||
// Attempt to parse an excalidraw/excalidrawlib file
|
||||
await this.loadFileToCanvas(file, fileHandle);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.dataTransfer?.types?.includes("text/plain")) {
|
||||
const text = event.dataTransfer?.getData("text");
|
||||
const textItem = dataTransferList.findByType(MIME_TYPES.text);
|
||||
|
||||
if (textItem) {
|
||||
const text = textItem.value;
|
||||
if (
|
||||
text &&
|
||||
embeddableURLValidator(text, this.props.validateEmbeddable) &&
|
||||
|
@ -18,8 +18,6 @@ import { CanvasError, ImageSceneDataError } from "../errors";
|
||||
import { calculateScrollCenter } from "../scene";
|
||||
import { decodeSvgBase64Payload } from "../scene/export";
|
||||
|
||||
import { isClipboardEvent } from "../clipboard";
|
||||
|
||||
import { base64ToString, stringToBase64, toByteString } from "./encode";
|
||||
import { nativeFileSystemSupported } from "./filesystem";
|
||||
import { isValidExcalidrawData, isValidLibrary } from "./json";
|
||||
@ -98,6 +96,8 @@ export const getMimeType = (blob: Blob | string): string => {
|
||||
return MIME_TYPES.jpg;
|
||||
} else if (/\.svg$/.test(name)) {
|
||||
return MIME_TYPES.svg;
|
||||
} else if (/\.excalidrawlib$/.test(name)) {
|
||||
return MIME_TYPES.excalidrawlib;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
@ -391,42 +391,6 @@ export const ImageURLToFile = async (
|
||||
throw new Error("Error: unsupported file type", { cause: "UNSUPPORTED" });
|
||||
};
|
||||
|
||||
export const getFilesFromEvent = async (
|
||||
event: React.DragEvent<HTMLDivElement> | ClipboardEvent,
|
||||
) => {
|
||||
let fileList: FileList | undefined = undefined;
|
||||
let items: DataTransferItemList | undefined = undefined;
|
||||
|
||||
if (isClipboardEvent(event)) {
|
||||
fileList = event.clipboardData?.files;
|
||||
items = event.clipboardData?.items;
|
||||
} else {
|
||||
const dragEvent = event as React.DragEvent<HTMLDivElement>;
|
||||
fileList = dragEvent.dataTransfer?.files;
|
||||
items = dragEvent.dataTransfer?.items;
|
||||
}
|
||||
|
||||
const files: (File | null)[] = Array.from(fileList || []);
|
||||
|
||||
return await Promise.all(
|
||||
files.map(async (file, idx) => {
|
||||
const dataTransferItem = items?.[idx];
|
||||
const fileHandle = dataTransferItem
|
||||
? getFileHandle(dataTransferItem)
|
||||
: null;
|
||||
return file
|
||||
? {
|
||||
file: await normalizeFile(file),
|
||||
fileHandle: await fileHandle,
|
||||
}
|
||||
: {
|
||||
file: null,
|
||||
fileHandle: null,
|
||||
};
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export const getFileHandle = async (
|
||||
event: DragEvent | React.DragEvent | DataTransferItem,
|
||||
): Promise<FileSystemHandle | null> => {
|
||||
|
@ -12291,7 +12291,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on e
|
||||
"editingGroupId": null,
|
||||
"editingTextElement": null,
|
||||
"elementsToHighlight": null,
|
||||
"errorMessage": "Couldn't load invalid file",
|
||||
"errorMessage": null,
|
||||
"exportBackground": true,
|
||||
"exportEmbedScene": false,
|
||||
"exportScale": 1,
|
||||
|
@ -35,20 +35,23 @@ describe("appState", () => {
|
||||
expect(h.state.viewBackgroundColor).toBe("#F00");
|
||||
});
|
||||
|
||||
await API.drop(
|
||||
new Blob(
|
||||
[
|
||||
JSON.stringify({
|
||||
type: EXPORT_DATA_TYPES.excalidraw,
|
||||
appState: {
|
||||
viewBackgroundColor: "#000",
|
||||
},
|
||||
elements: [API.createElement({ type: "rectangle", id: "A" })],
|
||||
}),
|
||||
],
|
||||
{ type: MIME_TYPES.json },
|
||||
),
|
||||
);
|
||||
await API.drop([
|
||||
{
|
||||
kind: "file",
|
||||
file: new Blob(
|
||||
[
|
||||
JSON.stringify({
|
||||
type: EXPORT_DATA_TYPES.excalidraw,
|
||||
appState: {
|
||||
viewBackgroundColor: "#000",
|
||||
},
|
||||
elements: [API.createElement({ type: "rectangle", id: "A" })],
|
||||
}),
|
||||
],
|
||||
{ type: MIME_TYPES.json },
|
||||
),
|
||||
},
|
||||
]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]);
|
||||
|
@ -57,7 +57,7 @@ describe("export", () => {
|
||||
blob: pngBlob,
|
||||
metadata: serializeAsJSON(testElements, h.state, {}, "local"),
|
||||
});
|
||||
await API.drop(pngBlobEmbedded);
|
||||
await API.drop([{ kind: "file", file: pngBlobEmbedded }]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(h.elements).toEqual([
|
||||
@ -94,7 +94,12 @@ describe("export", () => {
|
||||
});
|
||||
|
||||
it("import embedded png (legacy v1)", async () => {
|
||||
await API.drop(await API.loadFile("./fixtures/test_embedded_v1.png"));
|
||||
await API.drop([
|
||||
{
|
||||
kind: "file",
|
||||
file: await API.loadFile("./fixtures/test_embedded_v1.png"),
|
||||
},
|
||||
]);
|
||||
await waitFor(() => {
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({ type: "text", text: "test" }),
|
||||
@ -103,7 +108,12 @@ describe("export", () => {
|
||||
});
|
||||
|
||||
it("import embedded png (v2)", async () => {
|
||||
await API.drop(await API.loadFile("./fixtures/smiley_embedded_v2.png"));
|
||||
await API.drop([
|
||||
{
|
||||
kind: "file",
|
||||
file: await API.loadFile("./fixtures/smiley_embedded_v2.png"),
|
||||
},
|
||||
]);
|
||||
await waitFor(() => {
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({ type: "text", text: "😀" }),
|
||||
@ -112,7 +122,12 @@ describe("export", () => {
|
||||
});
|
||||
|
||||
it("import embedded svg (legacy v1)", async () => {
|
||||
await API.drop(await API.loadFile("./fixtures/test_embedded_v1.svg"));
|
||||
await API.drop([
|
||||
{
|
||||
kind: "file",
|
||||
file: await API.loadFile("./fixtures/test_embedded_v1.svg"),
|
||||
},
|
||||
]);
|
||||
await waitFor(() => {
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({ type: "text", text: "test" }),
|
||||
@ -121,7 +136,12 @@ describe("export", () => {
|
||||
});
|
||||
|
||||
it("import embedded svg (v2)", async () => {
|
||||
await API.drop(await API.loadFile("./fixtures/smiley_embedded_v2.svg"));
|
||||
await API.drop([
|
||||
{
|
||||
kind: "file",
|
||||
file: await API.loadFile("./fixtures/smiley_embedded_v2.svg"),
|
||||
},
|
||||
]);
|
||||
await waitFor(() => {
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({ type: "text", text: "😀" }),
|
||||
|
@ -478,43 +478,43 @@ export class API {
|
||||
});
|
||||
};
|
||||
|
||||
static drop = async (_blobs: Blob[] | Blob) => {
|
||||
const blobs = Array.isArray(_blobs) ? _blobs : [_blobs];
|
||||
const fileDropEvent = createEvent.drop(GlobalTestState.interactiveCanvas);
|
||||
const texts = await Promise.all(
|
||||
blobs.map(
|
||||
(blob) =>
|
||||
new Promise<string>((resolve, reject) => {
|
||||
try {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
resolve(reader.result as string);
|
||||
};
|
||||
reader.readAsText(blob);
|
||||
} catch (error: any) {
|
||||
reject(error);
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
static drop = async (items: ({kind: "string", value: string, type: string} | {kind: "file", file: File | Blob, type?: string })[]) => {
|
||||
|
||||
const files = blobs as File[] & { item: (index: number) => File };
|
||||
const fileDropEvent = createEvent.drop(GlobalTestState.interactiveCanvas);
|
||||
|
||||
const dataTransferFileItems = items.filter(i => i.kind === "file") as {kind: "file", file: File | Blob, type: string }[];
|
||||
|
||||
const files = dataTransferFileItems.map(item => item.file) as File[] & { item: (index: number) => File };
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/FileList/item
|
||||
files.item = (index: number) => files[index];
|
||||
|
||||
Object.defineProperty(fileDropEvent, "dataTransfer", {
|
||||
value: {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/files
|
||||
files,
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/items
|
||||
items: items.map((item, idx) => {
|
||||
if (item.kind === "string") {
|
||||
return {
|
||||
kind: "string",
|
||||
type: item.type,
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/getAsString
|
||||
getAsString: (cb: (text: string) => any) => cb(item.value),
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "file",
|
||||
type: item.type || item.file.type,
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/getAsFile
|
||||
getAsFile: () => item.file,
|
||||
};
|
||||
}),
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/getData
|
||||
getData: (type: string) => {
|
||||
const idx = blobs.findIndex((b) => b.type === type);
|
||||
if (idx >= 0) {
|
||||
return texts[idx];
|
||||
}
|
||||
if (type === "text") {
|
||||
return texts.join("\n");
|
||||
}
|
||||
return "";
|
||||
return items.find((item) => item.type === "string" && item.type === type) || "";
|
||||
},
|
||||
types: Array.from(new Set(blobs.map((b) => b.type))),
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/types
|
||||
types: Array.from(new Set(items.map((item) => item.kind === "file" ? "Files" : item.type))),
|
||||
},
|
||||
});
|
||||
Object.defineProperty(fileDropEvent, "clientX", {
|
||||
|
@ -47,42 +47,43 @@ class DataTransferItem {
|
||||
}
|
||||
}
|
||||
|
||||
class DataTransferList {
|
||||
items: DataTransferItem[] = [];
|
||||
|
||||
class DataTransferItemList extends Array<DataTransferItem> {
|
||||
add(data: string | File, type: string = ""): void {
|
||||
if (typeof data === "string") {
|
||||
this.items.push(new DataTransferItem("string", type, data));
|
||||
this.push(new DataTransferItem("string", type, data));
|
||||
} else if (data instanceof File) {
|
||||
this.items.push(new DataTransferItem("file", type, data));
|
||||
this.push(new DataTransferItem("file", type, data));
|
||||
}
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.items = [];
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class DataTransfer {
|
||||
public items: DataTransferList = new DataTransferList();
|
||||
private _types: Record<string, string> = {};
|
||||
public items: DataTransferItemList = new DataTransferItemList();
|
||||
|
||||
get files() {
|
||||
return this.items.items
|
||||
return this.items
|
||||
.filter((item) => item.kind === "file")
|
||||
.map((item) => item.getAsFile()!);
|
||||
}
|
||||
|
||||
add(data: string | File, type: string = ""): void {
|
||||
this.items.add(data, type);
|
||||
if (typeof data === "string") {
|
||||
this.items.add(data, type);
|
||||
} else {
|
||||
this.items.add(data);
|
||||
}
|
||||
}
|
||||
|
||||
setData(type: string, value: string) {
|
||||
this._types[type] = value;
|
||||
this.items.add(value, type);
|
||||
}
|
||||
|
||||
getData(type: string) {
|
||||
return this._types[type] || "";
|
||||
return this.items.find((item) => item.type === type)?.data || "";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -568,21 +568,24 @@ describe("history", () => {
|
||||
expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]),
|
||||
);
|
||||
|
||||
await API.drop(
|
||||
new Blob(
|
||||
[
|
||||
JSON.stringify({
|
||||
type: EXPORT_DATA_TYPES.excalidraw,
|
||||
appState: {
|
||||
...getDefaultAppState(),
|
||||
viewBackgroundColor: "#000",
|
||||
},
|
||||
elements: [API.createElement({ type: "rectangle", id: "B" })],
|
||||
}),
|
||||
],
|
||||
{ type: MIME_TYPES.json },
|
||||
),
|
||||
);
|
||||
await API.drop([
|
||||
{
|
||||
kind: "file",
|
||||
file: new Blob(
|
||||
[
|
||||
JSON.stringify({
|
||||
type: EXPORT_DATA_TYPES.excalidraw,
|
||||
appState: {
|
||||
...getDefaultAppState(),
|
||||
viewBackgroundColor: "#000",
|
||||
},
|
||||
elements: [API.createElement({ type: "rectangle", id: "B" })],
|
||||
}),
|
||||
],
|
||||
{ type: MIME_TYPES.json },
|
||||
),
|
||||
},
|
||||
]);
|
||||
|
||||
await waitFor(() => expect(API.getUndoStack().length).toBe(1));
|
||||
expect(h.state.viewBackgroundColor).toBe("#000");
|
||||
@ -624,11 +627,13 @@ describe("history", () => {
|
||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||
|
||||
const link = "https://www.youtube.com/watch?v=gkGMXY0wekg";
|
||||
await API.drop(
|
||||
new Blob([link], {
|
||||
await API.drop([
|
||||
{
|
||||
kind: "string",
|
||||
value: link,
|
||||
type: MIME_TYPES.text,
|
||||
}),
|
||||
);
|
||||
},
|
||||
]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(API.getUndoStack().length).toBe(1);
|
||||
@ -726,10 +731,15 @@ describe("history", () => {
|
||||
await setupImageTest();
|
||||
|
||||
await API.drop(
|
||||
await Promise.all([
|
||||
API.loadFile("./fixtures/deer.png"),
|
||||
API.loadFile("./fixtures/smiley.png"),
|
||||
]),
|
||||
(
|
||||
await Promise.all([
|
||||
API.loadFile("./fixtures/deer.png"),
|
||||
API.loadFile("./fixtures/smiley.png"),
|
||||
])
|
||||
).map((file) => ({
|
||||
kind: "file",
|
||||
file,
|
||||
})),
|
||||
);
|
||||
|
||||
await assertImageTest();
|
||||
|
@ -77,7 +77,7 @@ describe("image insertion", () => {
|
||||
API.loadFile("./fixtures/deer.png"),
|
||||
API.loadFile("./fixtures/smiley.png"),
|
||||
]);
|
||||
await API.drop(files);
|
||||
await API.drop(files.map((file) => ({ kind: "file", file })));
|
||||
|
||||
await assert();
|
||||
});
|
||||
|
@ -56,9 +56,13 @@ describe("library", () => {
|
||||
|
||||
it("import library via drag&drop", async () => {
|
||||
expect(await h.app.library.getLatestLibrary()).toEqual([]);
|
||||
await API.drop(
|
||||
await API.loadFile("./fixtures/fixture_library.excalidrawlib"),
|
||||
);
|
||||
await API.drop([
|
||||
{
|
||||
kind: "file",
|
||||
type: MIME_TYPES.excalidrawlib,
|
||||
file: await API.loadFile("./fixtures/fixture_library.excalidrawlib"),
|
||||
},
|
||||
]);
|
||||
await waitFor(async () => {
|
||||
expect(await h.app.library.getLatestLibrary()).toEqual([
|
||||
{
|
||||
@ -75,11 +79,13 @@ describe("library", () => {
|
||||
it("drop library item onto canvas", async () => {
|
||||
expect(h.elements).toEqual([]);
|
||||
const libraryItems = parseLibraryJSON(await libraryJSONPromise);
|
||||
await API.drop(
|
||||
new Blob([serializeLibraryAsJSON(libraryItems)], {
|
||||
await API.drop([
|
||||
{
|
||||
kind: "string",
|
||||
value: serializeLibraryAsJSON(libraryItems),
|
||||
type: MIME_TYPES.excalidrawlib,
|
||||
}),
|
||||
);
|
||||
},
|
||||
]);
|
||||
await waitFor(() => {
|
||||
expect(h.elements).toEqual([expect.objectContaining({ [ORIG_ID]: "A" })]);
|
||||
});
|
||||
@ -111,23 +117,20 @@ describe("library", () => {
|
||||
},
|
||||
});
|
||||
|
||||
await API.drop(
|
||||
new Blob(
|
||||
[
|
||||
serializeLibraryAsJSON([
|
||||
{
|
||||
id: "item1",
|
||||
status: "published",
|
||||
elements: [rectangle, text, arrow],
|
||||
created: 1,
|
||||
},
|
||||
]),
|
||||
],
|
||||
{
|
||||
type: MIME_TYPES.excalidrawlib,
|
||||
},
|
||||
),
|
||||
);
|
||||
await API.drop([
|
||||
{
|
||||
kind: "string",
|
||||
value: serializeLibraryAsJSON([
|
||||
{
|
||||
id: "item1",
|
||||
status: "published",
|
||||
elements: [rectangle, text, arrow],
|
||||
created: 1,
|
||||
},
|
||||
]),
|
||||
type: MIME_TYPES.excalidrawlib,
|
||||
},
|
||||
]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(h.elements).toEqual(
|
||||
@ -170,11 +173,13 @@ describe("library", () => {
|
||||
created: 1,
|
||||
};
|
||||
|
||||
await API.drop(
|
||||
new Blob([serializeLibraryAsJSON([item1, item1])], {
|
||||
await API.drop([
|
||||
{
|
||||
kind: "string",
|
||||
value: serializeLibraryAsJSON([item1, item1]),
|
||||
type: MIME_TYPES.excalidrawlib,
|
||||
}),
|
||||
);
|
||||
},
|
||||
]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(h.elements).toEqual([
|
||||
@ -193,11 +198,13 @@ describe("library", () => {
|
||||
UI.clickTool("rectangle");
|
||||
expect(h.elements).toEqual([]);
|
||||
const libraryItems = parseLibraryJSON(await libraryJSONPromise);
|
||||
await API.drop(
|
||||
new Blob([serializeLibraryAsJSON(libraryItems)], {
|
||||
await API.drop([
|
||||
{
|
||||
kind: "string",
|
||||
value: serializeLibraryAsJSON(libraryItems),
|
||||
type: MIME_TYPES.excalidrawlib,
|
||||
}),
|
||||
);
|
||||
},
|
||||
]);
|
||||
await waitFor(() => {
|
||||
expect(h.elements).toEqual([expect.objectContaining({ [ORIG_ID]: "A" })]);
|
||||
});
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
getFontString,
|
||||
getFontFamilyString,
|
||||
isTestEnv,
|
||||
MIME_TYPES,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
@ -45,7 +46,7 @@ import type {
|
||||
|
||||
import { actionSaveToActiveFile } from "../actions";
|
||||
|
||||
import { parseClipboard } from "../clipboard";
|
||||
import { parseDataTransferEvent } from "../clipboard";
|
||||
import {
|
||||
actionDecreaseFontSize,
|
||||
actionIncreaseFontSize,
|
||||
@ -332,12 +333,14 @@ export const textWysiwyg = ({
|
||||
|
||||
if (onChange) {
|
||||
editable.onpaste = async (event) => {
|
||||
const clipboardData = await parseClipboard(event, true);
|
||||
if (!clipboardData.text) {
|
||||
const textItem = (await parseDataTransferEvent(event)).findByType(
|
||||
MIME_TYPES.text,
|
||||
);
|
||||
if (!textItem) {
|
||||
return;
|
||||
}
|
||||
const data = normalizeText(clipboardData.text);
|
||||
if (!data) {
|
||||
const text = normalizeText(textItem.value);
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
const container = getContainerElement(
|
||||
@ -355,7 +358,7 @@ export const textWysiwyg = ({
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
const wrappedText = wrapText(
|
||||
`${editable.value}${data}`,
|
||||
`${editable.value}${text}`,
|
||||
font,
|
||||
getBoundTextMaxWidth(container, boundTextElement),
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user