fix: don't mutate the bounded text if not updated when submitted (#4543)
* fix: don't mutate the bounded text if not updated when submitted * dont update text for bounded text unless submitted * add specs * use node 16 * fix * Update text when editing and cache prev text * update prev text when props updated * remove only * type properly and remove unnecessary type checks * cache original text and compare with editor value to fix alignement issue after editing and add specs * naming tweak Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
62bead66d7
commit
50bd5fbae1
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -10,7 +10,7 @@ jobs:
|
|||||||
- name: Setup Node.js 14.x
|
- name: Setup Node.js 14.x
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 16.x
|
||||||
- name: Install and test
|
- name: Install and test
|
||||||
run: |
|
run: |
|
||||||
yarn --frozen-lockfile
|
yarn --frozen-lockfile
|
||||||
|
@ -1911,8 +1911,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const updateElement = (
|
const updateElement = (
|
||||||
text: string,
|
text: string,
|
||||||
originalText: string,
|
originalText: string,
|
||||||
isDeleted = false,
|
isDeleted: boolean,
|
||||||
updateDimensions = false,
|
isSubmit: boolean,
|
||||||
) => {
|
) => {
|
||||||
this.scene.replaceAllElements([
|
this.scene.replaceAllElements([
|
||||||
...this.scene.getElementsIncludingDeleted().map((_element) => {
|
...this.scene.getElementsIncludingDeleted().map((_element) => {
|
||||||
@ -1924,7 +1924,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
isDeleted,
|
isDeleted,
|
||||||
originalText,
|
originalText,
|
||||||
},
|
},
|
||||||
updateDimensions,
|
isSubmit,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return _element;
|
return _element;
|
||||||
@ -1950,7 +1950,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
];
|
];
|
||||||
},
|
},
|
||||||
onChange: withBatchedUpdates((text) => {
|
onChange: withBatchedUpdates((text) => {
|
||||||
updateElement(text, text, false, !element.containerId);
|
updateElement(text, text, false, false);
|
||||||
if (isNonDeletedElement(element)) {
|
if (isNonDeletedElement(element)) {
|
||||||
updateBoundElements(element);
|
updateBoundElements(element);
|
||||||
}
|
}
|
||||||
@ -1996,7 +1996,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
// do an initial update to re-initialize element position since we were
|
// do an initial update to re-initialize element position since we were
|
||||||
// modifying element's x/y for sake of editor (case: syncing to remote)
|
// modifying element's x/y for sake of editor (case: syncing to remote)
|
||||||
updateElement(element.text, element.originalText);
|
updateElement(element.text, element.originalText, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private deselectElements() {
|
private deselectElements() {
|
||||||
|
@ -158,7 +158,11 @@ const getAdjustedDimensions = (
|
|||||||
height: number;
|
height: number;
|
||||||
baseline: number;
|
baseline: number;
|
||||||
} => {
|
} => {
|
||||||
const maxWidth = element.containerId ? element.width : null;
|
let maxWidth = null;
|
||||||
|
if (element.containerId) {
|
||||||
|
const container = Scene.getScene(element)!.getElement(element.containerId)!;
|
||||||
|
maxWidth = container.width - BOUND_TEXT_PADDING * 2;
|
||||||
|
}
|
||||||
const {
|
const {
|
||||||
width: nextWidth,
|
width: nextWidth,
|
||||||
height: nextHeight,
|
height: nextHeight,
|
||||||
@ -246,11 +250,15 @@ export const updateTextElement = (
|
|||||||
originalText,
|
originalText,
|
||||||
}: { text: string; isDeleted?: boolean; originalText: string },
|
}: { text: string; isDeleted?: boolean; originalText: string },
|
||||||
|
|
||||||
updateDimensions: boolean,
|
isSubmit: boolean,
|
||||||
): ExcalidrawTextElement => {
|
): ExcalidrawTextElement => {
|
||||||
const dimensions = updateDimensions
|
const boundToContainer = isBoundToContainer(element);
|
||||||
? getAdjustedDimensions(element, text)
|
|
||||||
: undefined;
|
// Don't update dimensions and text value for bounded text unless submitted
|
||||||
|
const dimensions =
|
||||||
|
boundToContainer && !isSubmit
|
||||||
|
? undefined
|
||||||
|
: getAdjustedDimensions(element, text);
|
||||||
return newElementWith(element, {
|
return newElementWith(element, {
|
||||||
text,
|
text,
|
||||||
originalText,
|
originalText,
|
||||||
|
@ -1,169 +1,413 @@
|
|||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import ExcalidrawApp from "../excalidraw-app";
|
import ExcalidrawApp from "../excalidraw-app";
|
||||||
import { render } from "../tests/test-utils";
|
import { render, screen } from "../tests/test-utils";
|
||||||
import { Pointer, UI } from "../tests/helpers/ui";
|
import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
|
import { fireEvent } from "../tests/test-utils";
|
||||||
|
import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants";
|
||||||
|
import { ExcalidrawTextElementWithContainer } from "./types";
|
||||||
|
import * as textElementUtils from "./textElement";
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
const tab = " ";
|
const tab = " ";
|
||||||
|
const mouse = new Pointer("mouse");
|
||||||
|
|
||||||
describe("textWysiwyg", () => {
|
describe("textWysiwyg", () => {
|
||||||
let textarea: HTMLTextAreaElement;
|
describe("Test unbounded text", () => {
|
||||||
beforeEach(async () => {
|
let textarea: HTMLTextAreaElement;
|
||||||
await render(<ExcalidrawApp />);
|
beforeEach(async () => {
|
||||||
|
await render(<ExcalidrawApp />);
|
||||||
|
|
||||||
const element = UI.createElement("text");
|
const element = UI.createElement("text");
|
||||||
|
|
||||||
new Pointer("mouse").clickOn(element);
|
mouse.clickOn(element);
|
||||||
textarea = document.querySelector(
|
textarea = document.querySelector(
|
||||||
".excalidraw-textEditorContainer > textarea",
|
".excalidraw-textEditorContainer > textarea",
|
||||||
)!;
|
)!;
|
||||||
});
|
|
||||||
|
|
||||||
it("should add a tab at the start of the first line", () => {
|
|
||||||
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
|
|
||||||
textarea.value = "Line#1\nLine#2";
|
|
||||||
// cursor: "|Line#1\nLine#2"
|
|
||||||
textarea.selectionStart = 0;
|
|
||||||
textarea.selectionEnd = 0;
|
|
||||||
textarea.dispatchEvent(event);
|
|
||||||
|
|
||||||
expect(textarea.value).toEqual(`${tab}Line#1\nLine#2`);
|
|
||||||
// cursor: " |Line#1\nLine#2"
|
|
||||||
expect(textarea.selectionStart).toEqual(4);
|
|
||||||
expect(textarea.selectionEnd).toEqual(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should add a tab at the start of the second line", () => {
|
|
||||||
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
|
|
||||||
textarea.value = "Line#1\nLine#2";
|
|
||||||
// cursor: "Line#1\nLin|e#2"
|
|
||||||
textarea.selectionStart = 10;
|
|
||||||
textarea.selectionEnd = 10;
|
|
||||||
|
|
||||||
textarea.dispatchEvent(event);
|
|
||||||
|
|
||||||
expect(textarea.value).toEqual(`Line#1\n${tab}Line#2`);
|
|
||||||
|
|
||||||
// cursor: "Line#1\n Lin|e#2"
|
|
||||||
expect(textarea.selectionStart).toEqual(14);
|
|
||||||
expect(textarea.selectionEnd).toEqual(14);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should add a tab at the start of the first and second line", () => {
|
|
||||||
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
|
|
||||||
textarea.value = "Line#1\nLine#2\nLine#3";
|
|
||||||
// cursor: "Li|ne#1\nLi|ne#2\nLine#3"
|
|
||||||
textarea.selectionStart = 2;
|
|
||||||
textarea.selectionEnd = 9;
|
|
||||||
|
|
||||||
textarea.dispatchEvent(event);
|
|
||||||
|
|
||||||
expect(textarea.value).toEqual(`${tab}Line#1\n${tab}Line#2\nLine#3`);
|
|
||||||
|
|
||||||
// cursor: " Li|ne#1\n Li|ne#2\nLine#3"
|
|
||||||
expect(textarea.selectionStart).toEqual(6);
|
|
||||||
expect(textarea.selectionEnd).toEqual(17);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove a tab at the start of the first line", () => {
|
|
||||||
const event = new KeyboardEvent("keydown", {
|
|
||||||
key: KEYS.TAB,
|
|
||||||
shiftKey: true,
|
|
||||||
});
|
});
|
||||||
textarea.value = `${tab}Line#1\nLine#2`;
|
|
||||||
// cursor: "| Line#1\nLine#2"
|
|
||||||
textarea.selectionStart = 0;
|
|
||||||
textarea.selectionEnd = 0;
|
|
||||||
|
|
||||||
textarea.dispatchEvent(event);
|
it("should add a tab at the start of the first line", () => {
|
||||||
|
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
|
||||||
|
textarea.value = "Line#1\nLine#2";
|
||||||
|
// cursor: "|Line#1\nLine#2"
|
||||||
|
textarea.selectionStart = 0;
|
||||||
|
textarea.selectionEnd = 0;
|
||||||
|
textarea.dispatchEvent(event);
|
||||||
|
|
||||||
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
expect(textarea.value).toEqual(`${tab}Line#1\nLine#2`);
|
||||||
|
// cursor: " |Line#1\nLine#2"
|
||||||
// cursor: "|Line#1\nLine#2"
|
expect(textarea.selectionStart).toEqual(4);
|
||||||
expect(textarea.selectionStart).toEqual(0);
|
expect(textarea.selectionEnd).toEqual(4);
|
||||||
expect(textarea.selectionEnd).toEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove a tab at the start of the second line", () => {
|
|
||||||
const event = new KeyboardEvent("keydown", {
|
|
||||||
key: KEYS.TAB,
|
|
||||||
shiftKey: true,
|
|
||||||
});
|
});
|
||||||
// cursor: "Line#1\n Lin|e#2"
|
|
||||||
textarea.value = `Line#1\n${tab}Line#2`;
|
|
||||||
textarea.selectionStart = 15;
|
|
||||||
textarea.selectionEnd = 15;
|
|
||||||
|
|
||||||
textarea.dispatchEvent(event);
|
it("should add a tab at the start of the second line", () => {
|
||||||
|
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
|
||||||
|
textarea.value = "Line#1\nLine#2";
|
||||||
|
// cursor: "Line#1\nLin|e#2"
|
||||||
|
textarea.selectionStart = 10;
|
||||||
|
textarea.selectionEnd = 10;
|
||||||
|
|
||||||
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
textarea.dispatchEvent(event);
|
||||||
// cursor: "Line#1\nLin|e#2"
|
|
||||||
expect(textarea.selectionStart).toEqual(11);
|
|
||||||
expect(textarea.selectionEnd).toEqual(11);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove a tab at the start of the first and second line", () => {
|
expect(textarea.value).toEqual(`Line#1\n${tab}Line#2`);
|
||||||
const event = new KeyboardEvent("keydown", {
|
|
||||||
key: KEYS.TAB,
|
// cursor: "Line#1\n Lin|e#2"
|
||||||
shiftKey: true,
|
expect(textarea.selectionStart).toEqual(14);
|
||||||
|
expect(textarea.selectionEnd).toEqual(14);
|
||||||
});
|
});
|
||||||
// cursor: " Li|ne#1\n Li|ne#2\nLine#3"
|
|
||||||
textarea.value = `${tab}Line#1\n${tab}Line#2\nLine#3`;
|
|
||||||
textarea.selectionStart = 6;
|
|
||||||
textarea.selectionEnd = 17;
|
|
||||||
|
|
||||||
textarea.dispatchEvent(event);
|
it("should add a tab at the start of the first and second line", () => {
|
||||||
|
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
|
||||||
|
textarea.value = "Line#1\nLine#2\nLine#3";
|
||||||
|
// cursor: "Li|ne#1\nLi|ne#2\nLine#3"
|
||||||
|
textarea.selectionStart = 2;
|
||||||
|
textarea.selectionEnd = 9;
|
||||||
|
|
||||||
expect(textarea.value).toEqual(`Line#1\nLine#2\nLine#3`);
|
textarea.dispatchEvent(event);
|
||||||
// cursor: "Li|ne#1\nLi|ne#2\nLine#3"
|
|
||||||
expect(textarea.selectionStart).toEqual(2);
|
|
||||||
expect(textarea.selectionEnd).toEqual(9);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove a tab at the start of the second line and cursor stay on this line", () => {
|
expect(textarea.value).toEqual(`${tab}Line#1\n${tab}Line#2\nLine#3`);
|
||||||
const event = new KeyboardEvent("keydown", {
|
|
||||||
key: KEYS.TAB,
|
// cursor: " Li|ne#1\n Li|ne#2\nLine#3"
|
||||||
shiftKey: true,
|
expect(textarea.selectionStart).toEqual(6);
|
||||||
|
expect(textarea.selectionEnd).toEqual(17);
|
||||||
});
|
});
|
||||||
// cursor: "Line#1\n | Line#2"
|
|
||||||
textarea.value = `Line#1\n${tab}Line#2`;
|
|
||||||
textarea.selectionStart = 9;
|
|
||||||
textarea.selectionEnd = 9;
|
|
||||||
textarea.dispatchEvent(event);
|
|
||||||
|
|
||||||
// cursor: "Line#1\n|Line#2"
|
it("should remove a tab at the start of the first line", () => {
|
||||||
expect(textarea.selectionStart).toEqual(7);
|
const event = new KeyboardEvent("keydown", {
|
||||||
// expect(textarea.selectionEnd).toEqual(7);
|
key: KEYS.TAB,
|
||||||
});
|
shiftKey: true,
|
||||||
|
});
|
||||||
|
textarea.value = `${tab}Line#1\nLine#2`;
|
||||||
|
// cursor: "| Line#1\nLine#2"
|
||||||
|
textarea.selectionStart = 0;
|
||||||
|
textarea.selectionEnd = 0;
|
||||||
|
|
||||||
it("should remove partial tabs", () => {
|
textarea.dispatchEvent(event);
|
||||||
const event = new KeyboardEvent("keydown", {
|
|
||||||
key: KEYS.TAB,
|
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
||||||
shiftKey: true,
|
|
||||||
|
// cursor: "|Line#1\nLine#2"
|
||||||
|
expect(textarea.selectionStart).toEqual(0);
|
||||||
|
expect(textarea.selectionEnd).toEqual(0);
|
||||||
});
|
});
|
||||||
// cursor: "Line#1\n Line#|2"
|
|
||||||
textarea.value = `Line#1\n Line#2`;
|
|
||||||
textarea.selectionStart = 15;
|
|
||||||
textarea.selectionEnd = 15;
|
|
||||||
textarea.dispatchEvent(event);
|
|
||||||
|
|
||||||
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
it("should remove a tab at the start of the second line", () => {
|
||||||
});
|
const event = new KeyboardEvent("keydown", {
|
||||||
|
key: KEYS.TAB,
|
||||||
|
shiftKey: true,
|
||||||
|
});
|
||||||
|
// cursor: "Line#1\n Lin|e#2"
|
||||||
|
textarea.value = `Line#1\n${tab}Line#2`;
|
||||||
|
textarea.selectionStart = 15;
|
||||||
|
textarea.selectionEnd = 15;
|
||||||
|
|
||||||
it("should remove nothing", () => {
|
textarea.dispatchEvent(event);
|
||||||
const event = new KeyboardEvent("keydown", {
|
|
||||||
key: KEYS.TAB,
|
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
||||||
shiftKey: true,
|
// cursor: "Line#1\nLin|e#2"
|
||||||
|
expect(textarea.selectionStart).toEqual(11);
|
||||||
|
expect(textarea.selectionEnd).toEqual(11);
|
||||||
});
|
});
|
||||||
// cursor: "Line#1\n Li|ne#2"
|
|
||||||
textarea.value = `Line#1\nLine#2`;
|
|
||||||
textarea.selectionStart = 9;
|
|
||||||
textarea.selectionEnd = 9;
|
|
||||||
textarea.dispatchEvent(event);
|
|
||||||
|
|
||||||
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
it("should remove a tab at the start of the first and second line", () => {
|
||||||
|
const event = new KeyboardEvent("keydown", {
|
||||||
|
key: KEYS.TAB,
|
||||||
|
shiftKey: true,
|
||||||
|
});
|
||||||
|
// cursor: " Li|ne#1\n Li|ne#2\nLine#3"
|
||||||
|
textarea.value = `${tab}Line#1\n${tab}Line#2\nLine#3`;
|
||||||
|
textarea.selectionStart = 6;
|
||||||
|
textarea.selectionEnd = 17;
|
||||||
|
|
||||||
|
textarea.dispatchEvent(event);
|
||||||
|
|
||||||
|
expect(textarea.value).toEqual(`Line#1\nLine#2\nLine#3`);
|
||||||
|
// cursor: "Li|ne#1\nLi|ne#2\nLine#3"
|
||||||
|
expect(textarea.selectionStart).toEqual(2);
|
||||||
|
expect(textarea.selectionEnd).toEqual(9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove a tab at the start of the second line and cursor stay on this line", () => {
|
||||||
|
const event = new KeyboardEvent("keydown", {
|
||||||
|
key: KEYS.TAB,
|
||||||
|
shiftKey: true,
|
||||||
|
});
|
||||||
|
// cursor: "Line#1\n | Line#2"
|
||||||
|
textarea.value = `Line#1\n${tab}Line#2`;
|
||||||
|
textarea.selectionStart = 9;
|
||||||
|
textarea.selectionEnd = 9;
|
||||||
|
textarea.dispatchEvent(event);
|
||||||
|
|
||||||
|
// cursor: "Line#1\n|Line#2"
|
||||||
|
expect(textarea.selectionStart).toEqual(7);
|
||||||
|
// expect(textarea.selectionEnd).toEqual(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove partial tabs", () => {
|
||||||
|
const event = new KeyboardEvent("keydown", {
|
||||||
|
key: KEYS.TAB,
|
||||||
|
shiftKey: true,
|
||||||
|
});
|
||||||
|
// cursor: "Line#1\n Line#|2"
|
||||||
|
textarea.value = `Line#1\n Line#2`;
|
||||||
|
textarea.selectionStart = 15;
|
||||||
|
textarea.selectionEnd = 15;
|
||||||
|
textarea.dispatchEvent(event);
|
||||||
|
|
||||||
|
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove nothing", () => {
|
||||||
|
const event = new KeyboardEvent("keydown", {
|
||||||
|
key: KEYS.TAB,
|
||||||
|
shiftKey: true,
|
||||||
|
});
|
||||||
|
// cursor: "Line#1\n Li|ne#2"
|
||||||
|
textarea.value = `Line#1\nLine#2`;
|
||||||
|
textarea.selectionStart = 9;
|
||||||
|
textarea.selectionEnd = 9;
|
||||||
|
textarea.dispatchEvent(event);
|
||||||
|
|
||||||
|
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("Test bounded text", () => {
|
||||||
|
let rectangle: any;
|
||||||
|
const {
|
||||||
|
h,
|
||||||
|
}: {
|
||||||
|
h: {
|
||||||
|
elements: any;
|
||||||
|
};
|
||||||
|
} = window;
|
||||||
|
|
||||||
|
const DUMMY_HEIGHT = 240;
|
||||||
|
const DUMMY_WIDTH = 160;
|
||||||
|
const APPROX_LINE_HEIGHT = 25;
|
||||||
|
const INITIAL_WIDTH = 10;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
jest
|
||||||
|
.spyOn(textElementUtils, "getApproxLineHeight")
|
||||||
|
.mockReturnValue(APPROX_LINE_HEIGHT);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await render(<ExcalidrawApp />);
|
||||||
|
|
||||||
|
rectangle = UI.createElement("rectangle", {
|
||||||
|
x: 10,
|
||||||
|
y: 20,
|
||||||
|
width: 90,
|
||||||
|
height: 75,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should bind text to container when double clicked on center", async () => {
|
||||||
|
expect(h.elements.length).toBe(1);
|
||||||
|
expect(h.elements[0].id).toBe(rectangle.id);
|
||||||
|
|
||||||
|
mouse.doubleClickAt(
|
||||||
|
rectangle.x + rectangle.width / 2,
|
||||||
|
rectangle.y + rectangle.height / 2,
|
||||||
|
);
|
||||||
|
expect(h.elements.length).toBe(2);
|
||||||
|
|
||||||
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
|
expect(text.type).toBe("text");
|
||||||
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
|
mouse.down();
|
||||||
|
const editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
|
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
||||||
|
editor.blur();
|
||||||
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
|
{ id: text.id, type: "text" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should bind text to container when clicked on container and enter pressed", async () => {
|
||||||
|
expect(h.elements.length).toBe(1);
|
||||||
|
expect(h.elements[0].id).toBe(rectangle.id);
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({}, () => {
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(h.elements.length).toBe(2);
|
||||||
|
|
||||||
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
|
expect(text.type).toBe("text");
|
||||||
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
|
const editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
|
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
||||||
|
editor.blur();
|
||||||
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
|
{ id: text.id, type: "text" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update font family correctly on undo/redo by selecting bounded text when font family was updated", async () => {
|
||||||
|
mouse.doubleClickAt(
|
||||||
|
rectangle.x + rectangle.width / 2,
|
||||||
|
rectangle.y + rectangle.height / 2,
|
||||||
|
);
|
||||||
|
mouse.down();
|
||||||
|
|
||||||
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
|
let editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
||||||
|
editor.blur();
|
||||||
|
expect(text.fontFamily).toEqual(FONT_FAMILY.Virgil);
|
||||||
|
UI.clickTool("text");
|
||||||
|
|
||||||
|
mouse.clickAt(
|
||||||
|
rectangle.x + rectangle.width / 2,
|
||||||
|
rectangle.y + rectangle.height / 2,
|
||||||
|
);
|
||||||
|
mouse.down();
|
||||||
|
editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
editor.select();
|
||||||
|
fireEvent.click(screen.getByTitle(/code/i));
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
editor.blur();
|
||||||
|
expect(h.elements[1].fontFamily).toEqual(FONT_FAMILY.Cascadia);
|
||||||
|
|
||||||
|
//undo
|
||||||
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
|
Keyboard.keyPress(KEYS.Z);
|
||||||
|
});
|
||||||
|
expect(h.elements[1].fontFamily).toEqual(FONT_FAMILY.Virgil);
|
||||||
|
|
||||||
|
//redo
|
||||||
|
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
|
||||||
|
Keyboard.keyPress(KEYS.Z);
|
||||||
|
});
|
||||||
|
expect(h.elements[1].fontFamily).toEqual(FONT_FAMILY.Cascadia);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should wrap text and vertcially center align once text submitted", async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(textElementUtils, "measureText")
|
||||||
|
.mockImplementation((text, font, maxWidth) => {
|
||||||
|
let width = INITIAL_WIDTH;
|
||||||
|
let height = APPROX_LINE_HEIGHT;
|
||||||
|
let baseline = 10;
|
||||||
|
if (!text) {
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
baseline,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
baseline = 30;
|
||||||
|
width = DUMMY_WIDTH;
|
||||||
|
if (text === "Hello \nWorld!") {
|
||||||
|
height = APPROX_LINE_HEIGHT * 2;
|
||||||
|
}
|
||||||
|
if (maxWidth) {
|
||||||
|
width = maxWidth;
|
||||||
|
// To capture cases where maxWidth passed is initial width
|
||||||
|
// due to which the text is not wrapped correctly
|
||||||
|
if (maxWidth === INITIAL_WIDTH) {
|
||||||
|
height = DUMMY_HEIGHT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
baseline,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({}, () => {
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
});
|
||||||
|
|
||||||
|
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
|
let editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
// mock scroll height
|
||||||
|
jest
|
||||||
|
.spyOn(editor, "scrollHeight", "get")
|
||||||
|
.mockImplementation(() => APPROX_LINE_HEIGHT * 2);
|
||||||
|
|
||||||
|
fireEvent.change(editor, {
|
||||||
|
target: {
|
||||||
|
value: "Hello World!",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.dispatchEvent(new Event("input"));
|
||||||
|
|
||||||
|
await new Promise((cb) => setTimeout(cb, 0));
|
||||||
|
editor.blur();
|
||||||
|
text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
|
expect(text.text).toBe("Hello \nWorld!");
|
||||||
|
expect(text.originalText).toBe("Hello World!");
|
||||||
|
expect(text.y).toBe(
|
||||||
|
rectangle.y + rectangle.height / 2 - (APPROX_LINE_HEIGHT * 2) / 2,
|
||||||
|
);
|
||||||
|
expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING);
|
||||||
|
expect(text.height).toBe(APPROX_LINE_HEIGHT * 2);
|
||||||
|
expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2);
|
||||||
|
|
||||||
|
// Edit and text by removing second line and it should
|
||||||
|
// still vertically align correctly
|
||||||
|
mouse.select(rectangle);
|
||||||
|
Keyboard.withModifierKeys({}, () => {
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
});
|
||||||
|
editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
fireEvent.change(editor, {
|
||||||
|
target: {
|
||||||
|
value: "Hello",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// mock scroll height
|
||||||
|
jest
|
||||||
|
.spyOn(editor, "scrollHeight", "get")
|
||||||
|
.mockImplementation(() => APPROX_LINE_HEIGHT);
|
||||||
|
editor.style.height = "25px";
|
||||||
|
editor.dispatchEvent(new Event("input"));
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
|
editor.blur();
|
||||||
|
text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
|
|
||||||
|
expect(text.text).toBe("Hello");
|
||||||
|
expect(text.originalText).toBe("Hello");
|
||||||
|
expect(text.y).toBe(
|
||||||
|
rectangle.y + rectangle.height / 2 - APPROX_LINE_HEIGHT / 2,
|
||||||
|
);
|
||||||
|
expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING);
|
||||||
|
expect(text.height).toBe(APPROX_LINE_HEIGHT);
|
||||||
|
expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -3,6 +3,7 @@ import {
|
|||||||
isWritableElement,
|
isWritableElement,
|
||||||
getFontString,
|
getFontString,
|
||||||
getFontFamilyString,
|
getFontFamilyString,
|
||||||
|
isTestEnv,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { isBoundToContainer, isTextElement } from "./typeChecks";
|
import { isBoundToContainer, isTextElement } from "./typeChecks";
|
||||||
@ -72,7 +73,7 @@ export const textWysiwyg = ({
|
|||||||
originalText: string;
|
originalText: string;
|
||||||
}) => void;
|
}) => void;
|
||||||
getViewportCoords: (x: number, y: number) => [number, number];
|
getViewportCoords: (x: number, y: number) => [number, number];
|
||||||
element: ExcalidrawElement;
|
element: ExcalidrawTextElement;
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
excalidrawContainer: HTMLDivElement | null;
|
excalidrawContainer: HTMLDivElement | null;
|
||||||
}) => {
|
}) => {
|
||||||
@ -93,10 +94,9 @@ export const textWysiwyg = ({
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let originalContainerHeight: number;
|
let originalContainerHeight: number;
|
||||||
let approxLineHeight = isTextElement(element)
|
let approxLineHeight = getApproxLineHeight(getFontString(element));
|
||||||
? getApproxLineHeight(getFontString(element))
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
|
const initialText = element.originalText;
|
||||||
const updateWysiwygStyle = () => {
|
const updateWysiwygStyle = () => {
|
||||||
const updatedElement = Scene.getScene(element)?.getElement(id);
|
const updatedElement = Scene.getScene(element)?.getElement(id);
|
||||||
if (updatedElement && isTextElement(updatedElement)) {
|
if (updatedElement && isTextElement(updatedElement)) {
|
||||||
@ -123,9 +123,7 @@ export const textWysiwyg = ({
|
|||||||
height = editorHeight;
|
height = editorHeight;
|
||||||
}
|
}
|
||||||
if (propertiesUpdated) {
|
if (propertiesUpdated) {
|
||||||
approxLineHeight = isTextElement(updatedElement)
|
approxLineHeight = getApproxLineHeight(getFontString(updatedElement));
|
||||||
? getApproxLineHeight(getFontString(updatedElement))
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
originalContainerHeight = container.height;
|
originalContainerHeight = container.height;
|
||||||
|
|
||||||
@ -164,7 +162,7 @@ export const textWysiwyg = ({
|
|||||||
}
|
}
|
||||||
const [viewportX, viewportY] = getViewportCoords(coordX, coordY);
|
const [viewportX, viewportY] = getViewportCoords(coordX, coordY);
|
||||||
const { textAlign } = updatedElement;
|
const { textAlign } = updatedElement;
|
||||||
editable.value = updatedElement.originalText || updatedElement.text;
|
editable.value = updatedElement.originalText;
|
||||||
const lines = updatedElement.originalText.split("\n");
|
const lines = updatedElement.originalText.split("\n");
|
||||||
const lineHeight = updatedElement.containerId
|
const lineHeight = updatedElement.containerId
|
||||||
? approxLineHeight
|
? approxLineHeight
|
||||||
@ -217,6 +215,11 @@ export const textWysiwyg = ({
|
|||||||
maxWidth: `${maxWidth}px`,
|
maxWidth: `${maxWidth}px`,
|
||||||
maxHeight: `${editorMaxHeight}px`,
|
maxHeight: `${editorMaxHeight}px`,
|
||||||
});
|
});
|
||||||
|
// For some reason updating font attribute doesn't set font family
|
||||||
|
// hence updating font family explicitly for test environment
|
||||||
|
if (isTestEnv()) {
|
||||||
|
editable.style.fontFamily = getFontFamilyString(updatedElement);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -450,18 +453,23 @@ export const textWysiwyg = ({
|
|||||||
getFontString(updateElement),
|
getFontString(updateElement),
|
||||||
container.width,
|
container.width,
|
||||||
);
|
);
|
||||||
if (isTextElement(updateElement) && updateElement.containerId) {
|
|
||||||
|
if (updateElement.containerId) {
|
||||||
const editorHeight = Number(editable.style.height.slice(0, -2));
|
const editorHeight = Number(editable.style.height.slice(0, -2));
|
||||||
if (editable.value) {
|
if (editable.value) {
|
||||||
mutateElement(updateElement, {
|
// Don't mutate if text is not updated
|
||||||
// vertically center align
|
if (initialText !== editable.value) {
|
||||||
y: container.y + container.height / 2 - editorHeight / 2,
|
mutateElement(updateElement, {
|
||||||
height: editorHeight,
|
// vertically center align
|
||||||
width: Number(editable.style.width.slice(0, -2)),
|
y: container.y + container.height / 2 - editorHeight / 2,
|
||||||
// preserve padding
|
height: editorHeight,
|
||||||
x: container.x + BOUND_TEXT_PADDING,
|
width: Number(editable.style.width.slice(0, -2)),
|
||||||
angle: container.angle,
|
// preserve padding
|
||||||
});
|
x: container.x + BOUND_TEXT_PADDING,
|
||||||
|
angle: container.angle,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const boundTextElementId = getBoundTextElementId(container);
|
const boundTextElementId = getBoundTextElementId(container);
|
||||||
if (!boundTextElementId || boundTextElementId !== element.id) {
|
if (!boundTextElementId || boundTextElementId !== element.id) {
|
||||||
mutateElement(container, {
|
mutateElement(container, {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user