feat: changed text copy/paste behaviour (#5786)
Co-authored-by: dwelle <luzar.david@gmail.com> Co-authored-by: Antonio Della Fortuna <a.dellafortuna00@gmail.com>
This commit is contained in:
parent
d2181847be
commit
baf9651d34
@ -14,6 +14,7 @@ import {
|
||||
} from "../element/bounds";
|
||||
import { isLinearElement } from "../element/typeChecks";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import { KEYS } from "../keys";
|
||||
|
||||
const enableActionFlipHorizontal = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
@ -63,7 +64,8 @@ export const actionFlipVertical = register({
|
||||
commitToHistory: true,
|
||||
};
|
||||
},
|
||||
keyTest: (event) => event.shiftKey && event.code === "KeyV",
|
||||
keyTest: (event) =>
|
||||
event.shiftKey && event.code === "KeyV" && !event[KEYS.CTRL_OR_CMD],
|
||||
contextItemLabel: "labels.flipVertical",
|
||||
contextItemPredicate: (elements, appState) =>
|
||||
enableActionFlipVertical(elements, appState),
|
||||
|
@ -8,6 +8,7 @@ import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
||||
import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants";
|
||||
import { isInitializedImageElement } from "./element/typeChecks";
|
||||
import { isPromiseLike } from "./utils";
|
||||
import { normalizeText } from "./element/textElement";
|
||||
|
||||
type ElementsClipboard = {
|
||||
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
|
||||
@ -109,16 +110,16 @@ const parsePotentialSpreadsheet = (
|
||||
* Retrieves content from system clipboard (either from ClipboardEvent or
|
||||
* via async clipboard API if supported)
|
||||
*/
|
||||
const getSystemClipboard = async (
|
||||
export const getSystemClipboard = async (
|
||||
event: ClipboardEvent | null,
|
||||
): Promise<string> => {
|
||||
try {
|
||||
const text = event
|
||||
? event.clipboardData?.getData("text/plain").trim()
|
||||
? event.clipboardData?.getData("text/plain")
|
||||
: probablySupportsClipboardReadText &&
|
||||
(await navigator.clipboard.readText());
|
||||
|
||||
return text || "";
|
||||
return normalizeText(text || "").trim();
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
@ -129,19 +130,24 @@ const getSystemClipboard = async (
|
||||
*/
|
||||
export const parseClipboard = async (
|
||||
event: ClipboardEvent | null,
|
||||
isPlainPaste = false,
|
||||
): Promise<ClipboardData> => {
|
||||
const systemClipboard = await getSystemClipboard(event);
|
||||
|
||||
// if system clipboard empty, couldn't be resolved, or contains previously
|
||||
// copied excalidraw scene as SVG, fall back to previously copied excalidraw
|
||||
// elements
|
||||
if (!systemClipboard || systemClipboard.includes(SVG_EXPORT_TAG)) {
|
||||
if (
|
||||
!systemClipboard ||
|
||||
(!isPlainPaste && systemClipboard.includes(SVG_EXPORT_TAG))
|
||||
) {
|
||||
return getAppClipboard();
|
||||
}
|
||||
|
||||
// if system clipboard contains spreadsheet, use it even though it's
|
||||
// technically possible it's staler than in-app clipboard
|
||||
const spreadsheetResult = parsePotentialSpreadsheet(systemClipboard);
|
||||
const spreadsheetResult =
|
||||
!isPlainPaste && parsePotentialSpreadsheet(systemClipboard);
|
||||
if (spreadsheetResult) {
|
||||
return spreadsheetResult;
|
||||
}
|
||||
@ -154,6 +160,9 @@ export const parseClipboard = async (
|
||||
return {
|
||||
elements: systemClipboardData.elements,
|
||||
files: systemClipboardData.files,
|
||||
text: isPlainPaste
|
||||
? JSON.stringify(systemClipboardData.elements, null, 2)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
} catch (e) {}
|
||||
@ -161,7 +170,12 @@ export const parseClipboard = async (
|
||||
// unless we set a flag to prefer in-app clipboard because browser didn't
|
||||
// support storing to system clipboard on copy
|
||||
return PREFER_APP_CLIPBOARD && appClipboardData.elements
|
||||
? appClipboardData
|
||||
? {
|
||||
...appClipboardData,
|
||||
text: isPlainPaste
|
||||
? JSON.stringify(appClipboardData.elements, null, 2)
|
||||
: undefined,
|
||||
}
|
||||
: { text: systemClipboard };
|
||||
};
|
||||
|
||||
|
@ -222,6 +222,7 @@ import {
|
||||
updateObject,
|
||||
setEraserCursor,
|
||||
updateActiveTool,
|
||||
getShortcutKey,
|
||||
} from "../utils";
|
||||
import ContextMenu, { ContextMenuOption } from "./ContextMenu";
|
||||
import LayerUI from "./LayerUI";
|
||||
@ -249,6 +250,7 @@ import throttle from "lodash.throttle";
|
||||
import { fileOpen, FileSystemHandle } from "../data/filesystem";
|
||||
import {
|
||||
bindTextToShapeAfterDuplication,
|
||||
getApproxLineHeight,
|
||||
getApproxMinLineHeight,
|
||||
getApproxMinLineWidth,
|
||||
getBoundTextElement,
|
||||
@ -326,6 +328,10 @@ let invalidateContextMenu = false;
|
||||
// to rAF. See #5439
|
||||
let THROTTLE_NEXT_RENDER = true;
|
||||
|
||||
let IS_PLAIN_PASTE = false;
|
||||
let IS_PLAIN_PASTE_TIMER = 0;
|
||||
let PLAIN_PASTE_TOAST_SHOWN = false;
|
||||
|
||||
let lastPointerUp: ((event: any) => void) | null = null;
|
||||
const gesture: Gesture = {
|
||||
pointers: new Map(),
|
||||
@ -1452,6 +1458,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
private pasteFromClipboard = withBatchedUpdates(
|
||||
async (event: ClipboardEvent | null) => {
|
||||
const isPlainPaste = !!(IS_PLAIN_PASTE && event);
|
||||
|
||||
// #686
|
||||
const target = document.activeElement;
|
||||
const isExcalidrawActive =
|
||||
@ -1462,8 +1470,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
const elementUnderCursor = document.elementFromPoint(cursorX, cursorY);
|
||||
if (
|
||||
// if no ClipboardEvent supplied, assume we're pasting via contextMenu
|
||||
// thus these checks don't make sense
|
||||
event &&
|
||||
(!(elementUnderCursor instanceof HTMLCanvasElement) ||
|
||||
isWritableElement(target))
|
||||
@ -1476,9 +1482,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
// (something something security)
|
||||
let file = event?.clipboardData?.files[0];
|
||||
|
||||
const data = await parseClipboard(event);
|
||||
const data = await parseClipboard(event, isPlainPaste);
|
||||
|
||||
if (!file && data.text) {
|
||||
if (!file && data.text && !isPlainPaste) {
|
||||
const string = data.text.trim();
|
||||
if (string.startsWith("<svg") && string.endsWith("</svg>")) {
|
||||
// ignore SVG validation/normalization which will be done during image
|
||||
@ -1511,9 +1517,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.errorMessage) {
|
||||
this.setState({ errorMessage: data.errorMessage });
|
||||
} else if (data.spreadsheet) {
|
||||
} else if (data.spreadsheet && !isPlainPaste) {
|
||||
this.setState({
|
||||
pasteDialog: {
|
||||
data: data.spreadsheet,
|
||||
@ -1521,13 +1528,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
});
|
||||
} else if (data.elements) {
|
||||
// TODO remove formatting from elements if isPlainPaste
|
||||
this.addElementsFromPasteOrLibrary({
|
||||
elements: data.elements,
|
||||
files: data.files || null,
|
||||
position: "cursor",
|
||||
});
|
||||
} else if (data.text) {
|
||||
this.addTextFromPaste(data.text);
|
||||
this.addTextFromPaste(data.text, isPlainPaste);
|
||||
}
|
||||
this.setActiveTool({ type: "selection" });
|
||||
event?.preventDefault();
|
||||
@ -1634,13 +1642,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.setActiveTool({ type: "selection" });
|
||||
};
|
||||
|
||||
private addTextFromPaste(text: any) {
|
||||
private addTextFromPaste(text: string, isPlainPaste = false) {
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
{ clientX: cursorX, clientY: cursorY },
|
||||
this.state,
|
||||
);
|
||||
|
||||
const element = newTextElement({
|
||||
const textElementProps = {
|
||||
x,
|
||||
y,
|
||||
strokeColor: this.state.currentItemStrokeColor,
|
||||
@ -1657,13 +1665,76 @@ class App extends React.Component<AppProps, AppState> {
|
||||
textAlign: this.state.currentItemTextAlign,
|
||||
verticalAlign: DEFAULT_VERTICAL_ALIGN,
|
||||
locked: false,
|
||||
});
|
||||
};
|
||||
|
||||
const LINE_GAP = 10;
|
||||
let currentY = y;
|
||||
|
||||
const lines = isPlainPaste ? [text] : text.split("\n");
|
||||
const textElements = lines.reduce(
|
||||
(acc: ExcalidrawTextElement[], line, idx) => {
|
||||
const text = line.trim();
|
||||
|
||||
if (text.length) {
|
||||
const element = newTextElement({
|
||||
...textElementProps,
|
||||
x,
|
||||
y: currentY,
|
||||
text,
|
||||
});
|
||||
acc.push(element);
|
||||
currentY += element.height + LINE_GAP;
|
||||
} else {
|
||||
const prevLine = lines[idx - 1]?.trim();
|
||||
// add paragraph only if previous line was not empty, IOW don't add
|
||||
// more than one empty line
|
||||
if (prevLine) {
|
||||
const defaultLineHeight = getApproxLineHeight(
|
||||
getFontString({
|
||||
fontSize: textElementProps.fontSize,
|
||||
fontFamily: textElementProps.fontFamily,
|
||||
}),
|
||||
);
|
||||
|
||||
currentY += defaultLineHeight + LINE_GAP;
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
if (textElements.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.scene.replaceAllElements([
|
||||
...this.scene.getElementsIncludingDeleted(),
|
||||
element,
|
||||
...textElements,
|
||||
]);
|
||||
this.setState({ selectedElementIds: { [element.id]: true } });
|
||||
|
||||
this.setState({
|
||||
selectedElementIds: Object.fromEntries(
|
||||
textElements.map((el) => [el.id, true]),
|
||||
),
|
||||
});
|
||||
|
||||
if (
|
||||
!isPlainPaste &&
|
||||
textElements.length > 1 &&
|
||||
PLAIN_PASTE_TOAST_SHOWN === false &&
|
||||
!this.device.isMobile
|
||||
) {
|
||||
this.setToast({
|
||||
message: t("toast.pasteAsSingleElement", {
|
||||
shortcut: getShortcutKey("CtrlOrCmd+Shift+V"),
|
||||
}),
|
||||
duration: 5000,
|
||||
});
|
||||
PLAIN_PASTE_TOAST_SHOWN = true;
|
||||
}
|
||||
|
||||
this.history.resumeRecording();
|
||||
}
|
||||
|
||||
@ -1873,6 +1944,17 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
}
|
||||
|
||||
if (event[KEYS.CTRL_OR_CMD] && event.key.toLowerCase() === KEYS.V) {
|
||||
IS_PLAIN_PASTE = event.shiftKey;
|
||||
clearTimeout(IS_PLAIN_PASTE_TIMER);
|
||||
// reset (100ms to be safe that we it runs after the ensuing
|
||||
// paste event). Though, technically unnecessary to reset since we
|
||||
// (re)set the flag before each paste event.
|
||||
IS_PLAIN_PASTE_TIMER = window.setTimeout(() => {
|
||||
IS_PLAIN_PASTE = false;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// prevent browser zoom in input fields
|
||||
if (event[KEYS.CTRL_OR_CMD] && isWritableElement(event.target)) {
|
||||
if (event.code === CODES.MINUS || event.code === CODES.EQUAL) {
|
||||
|
@ -289,6 +289,10 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
||||
label={t("labels.paste")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+V")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.pasteAsPlaintext")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+V")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.copyAsPng")}
|
||||
shortcuts={[getShortcutKey("Shift+Alt+C")]}
|
||||
|
@ -25,6 +25,7 @@ import {
|
||||
getContainerDims,
|
||||
getContainerElement,
|
||||
measureText,
|
||||
normalizeText,
|
||||
wrapText,
|
||||
} from "./textElement";
|
||||
import { BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants";
|
||||
@ -133,12 +134,13 @@ export const newTextElement = (
|
||||
containerId?: ExcalidrawRectangleElement["id"];
|
||||
} & ElementConstructorOpts,
|
||||
): NonDeleted<ExcalidrawTextElement> => {
|
||||
const metrics = measureText(opts.text, getFontString(opts));
|
||||
const text = normalizeText(opts.text);
|
||||
const metrics = measureText(text, getFontString(opts));
|
||||
const offsets = getTextElementPositionOffsets(opts, metrics);
|
||||
const textElement = newElementWith(
|
||||
{
|
||||
..._newElementBase<ExcalidrawTextElement>("text", opts),
|
||||
text: opts.text,
|
||||
text,
|
||||
fontSize: opts.fontSize,
|
||||
fontFamily: opts.fontFamily,
|
||||
textAlign: opts.textAlign,
|
||||
@ -149,7 +151,7 @@ export const newTextElement = (
|
||||
height: metrics.height,
|
||||
baseline: metrics.baseline,
|
||||
containerId: opts.containerId || null,
|
||||
originalText: opts.text,
|
||||
originalText: text,
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
@ -19,6 +19,16 @@ import { AppState } from "../types";
|
||||
import { getSelectedElements } from "../scene";
|
||||
import { isImageElement } from "./typeChecks";
|
||||
|
||||
export const normalizeText = (text: string) => {
|
||||
return (
|
||||
text
|
||||
// replace tabs with spaces so they render and measure correctly
|
||||
.replace(/\t/g, " ")
|
||||
// normalize newlines
|
||||
.replace(/\r?\n|\r/g, "\n")
|
||||
);
|
||||
};
|
||||
|
||||
export const redrawTextBoundingBox = (
|
||||
textElement: ExcalidrawTextElement,
|
||||
container: ExcalidrawElement | null,
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
getContainerDims,
|
||||
getContainerElement,
|
||||
measureText,
|
||||
normalizeText,
|
||||
wrapText,
|
||||
} from "./textElement";
|
||||
import {
|
||||
@ -32,16 +33,6 @@ import App from "../components/App";
|
||||
import { getMaxContainerWidth } from "./newElement";
|
||||
import { parseClipboard } from "../clipboard";
|
||||
|
||||
const normalizeText = (text: string) => {
|
||||
return (
|
||||
text
|
||||
// replace tabs with spaces so they render and measure correctly
|
||||
.replace(/\t/g, " ")
|
||||
// normalize newlines
|
||||
.replace(/\r?\n|\r/g, "\n")
|
||||
);
|
||||
};
|
||||
|
||||
const getTransform = (
|
||||
width: number,
|
||||
height: number,
|
||||
@ -279,7 +270,7 @@ export const textWysiwyg = ({
|
||||
if (onChange) {
|
||||
editable.onpaste = async (event) => {
|
||||
event.preventDefault();
|
||||
const clipboardData = await parseClipboard(event);
|
||||
const clipboardData = await parseClipboard(event, true);
|
||||
if (!clipboardData.text) {
|
||||
return;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"labels": {
|
||||
"paste": "Paste",
|
||||
"pasteAsPlaintext": "Paste as plaintext",
|
||||
"pasteCharts": "Paste charts",
|
||||
"selectAll": "Select all",
|
||||
"multiSelect": "Add element to selection",
|
||||
@ -392,7 +393,8 @@
|
||||
"fileSaved": "File saved.",
|
||||
"fileSavedToFilename": "Saved to {filename}",
|
||||
"canvas": "canvas",
|
||||
"selection": "selection"
|
||||
"selection": "selection",
|
||||
"pasteAsSingleElement": "Use {{shortcut}} to paste as a single element,\nor paste into an existing text editor"
|
||||
},
|
||||
"colors": {
|
||||
"ffffff": "White",
|
||||
|
184
src/tests/clipboard.test.tsx
Normal file
184
src/tests/clipboard.test.tsx
Normal file
@ -0,0 +1,184 @@
|
||||
import ReactDOM from "react-dom";
|
||||
import { render, waitFor, GlobalTestState } from "./test-utils";
|
||||
import { Pointer, Keyboard } from "./helpers/ui";
|
||||
import ExcalidrawApp from "../excalidraw-app";
|
||||
import { KEYS } from "../keys";
|
||||
import { getApproxLineHeight } from "../element/textElement";
|
||||
import { getFontString } from "../utils";
|
||||
import { getElementBounds } from "../element";
|
||||
import { NormalizedZoomValue } from "../types";
|
||||
|
||||
const { h } = window;
|
||||
|
||||
const mouse = new Pointer("mouse");
|
||||
|
||||
jest.mock("../keys.ts", () => {
|
||||
const actual = jest.requireActual("../keys.ts");
|
||||
return {
|
||||
__esmodule: true,
|
||||
...actual,
|
||||
isDarwin: false,
|
||||
KEYS: {
|
||||
...actual.KEYS,
|
||||
CTRL_OR_CMD: "ctrlKey",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const setClipboardText = (text: string) => {
|
||||
Object.assign(navigator, {
|
||||
clipboard: {
|
||||
readText: () => text,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const sendPasteEvent = () => {
|
||||
const clipboardEvent = new Event("paste", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
composed: true,
|
||||
});
|
||||
|
||||
// set `clipboardData` properties.
|
||||
// @ts-ignore
|
||||
clipboardEvent.clipboardData = {
|
||||
getData: () => window.navigator.clipboard.readText(),
|
||||
files: [],
|
||||
};
|
||||
|
||||
document.dispatchEvent(clipboardEvent);
|
||||
};
|
||||
|
||||
const pasteWithCtrlCmdShiftV = () => {
|
||||
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
|
||||
//triggering keydown with an empty clipboard
|
||||
Keyboard.keyPress(KEYS.V);
|
||||
//triggering paste event with faked clipboard
|
||||
sendPasteEvent();
|
||||
});
|
||||
};
|
||||
|
||||
const pasteWithCtrlCmdV = () => {
|
||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||
//triggering keydown with an empty clipboard
|
||||
Keyboard.keyPress(KEYS.V);
|
||||
//triggering paste event with faked clipboard
|
||||
sendPasteEvent();
|
||||
});
|
||||
};
|
||||
|
||||
const sleep = (ms: number) => {
|
||||
return new Promise((resolve) => setTimeout(() => resolve(null), ms));
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||
|
||||
localStorage.clear();
|
||||
|
||||
mouse.reset();
|
||||
|
||||
await render(<ExcalidrawApp />);
|
||||
h.app.setAppState({ zoom: { value: 1 as NormalizedZoomValue } });
|
||||
setClipboardText("");
|
||||
Object.assign(document, {
|
||||
elementFromPoint: () => GlobalTestState.canvas,
|
||||
});
|
||||
});
|
||||
|
||||
describe("paste text as single lines", () => {
|
||||
it("should create an element for each line when copying with Ctrl/Cmd+V", async () => {
|
||||
const text = "sajgfakfn\naaksfnknas\nakefnkasf";
|
||||
setClipboardText(text);
|
||||
pasteWithCtrlCmdV();
|
||||
await waitFor(() => {
|
||||
expect(h.elements.length).toEqual(text.split("\n").length);
|
||||
});
|
||||
});
|
||||
|
||||
it("should ignore empty lines when creating an element for each line", async () => {
|
||||
const text = "\n\nsajgfakfn\n\n\naaksfnknas\n\nakefnkasf\n\n\n";
|
||||
setClipboardText(text);
|
||||
pasteWithCtrlCmdV();
|
||||
await waitFor(() => {
|
||||
expect(h.elements.length).toEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not create any element if clipboard has only new lines", async () => {
|
||||
const text = "\n\n\n\n\n";
|
||||
setClipboardText(text);
|
||||
pasteWithCtrlCmdV();
|
||||
await waitFor(async () => {
|
||||
await sleep(50); // elements lenght will always be zero if we don't wait, since paste is async
|
||||
expect(h.elements.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("should space items correctly", async () => {
|
||||
const text = "hkhkjhki\njgkjhffjh\njgkjhffjh";
|
||||
const lineHeight =
|
||||
getApproxLineHeight(
|
||||
getFontString({
|
||||
fontSize: h.app.state.currentItemFontSize,
|
||||
fontFamily: h.app.state.currentItemFontFamily,
|
||||
}),
|
||||
) +
|
||||
10 / h.app.state.zoom.value;
|
||||
mouse.moveTo(100, 100);
|
||||
setClipboardText(text);
|
||||
pasteWithCtrlCmdV();
|
||||
await waitFor(async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [fx, firstElY] = getElementBounds(h.elements[0]);
|
||||
for (let i = 1; i < h.elements.length; i++) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [fx, elY] = getElementBounds(h.elements[i]);
|
||||
expect(elY).toEqual(firstElY + lineHeight * i);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should leave a space for blank new lines", async () => {
|
||||
const text = "hkhkjhki\n\njgkjhffjh";
|
||||
const lineHeight =
|
||||
getApproxLineHeight(
|
||||
getFontString({
|
||||
fontSize: h.app.state.currentItemFontSize,
|
||||
fontFamily: h.app.state.currentItemFontFamily,
|
||||
}),
|
||||
) +
|
||||
10 / h.app.state.zoom.value;
|
||||
mouse.moveTo(100, 100);
|
||||
setClipboardText(text);
|
||||
pasteWithCtrlCmdV();
|
||||
await waitFor(async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [fx, firstElY] = getElementBounds(h.elements[0]);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [lx, lastElY] = getElementBounds(h.elements[1]);
|
||||
expect(lastElY).toEqual(firstElY + lineHeight * 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("paste text as a single element", () => {
|
||||
it("should create single text element when copying text with Ctrl/Cmd+Shift+V", async () => {
|
||||
const text = "sajgfakfn\naaksfnknas\nakefnkasf";
|
||||
setClipboardText(text);
|
||||
pasteWithCtrlCmdShiftV();
|
||||
await waitFor(() => {
|
||||
expect(h.elements.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
it("should not create any element when only new lines in clipboard", async () => {
|
||||
const text = "\n\n\n\n";
|
||||
setClipboardText(text);
|
||||
pasteWithCtrlCmdShiftV();
|
||||
await waitFor(async () => {
|
||||
await sleep(50);
|
||||
expect(h.elements.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user