Compare commits

..

8 Commits

Author SHA1 Message Date
Arnošt Pleskot
b3d95d9307 feat: use upng 2023-06-16 23:49:28 +02:00
Arnošt Pleskot
671ed94d74 chore: add timers 2023-06-16 23:42:45 +02:00
Arnošt Pleskot
d5ac76d4ea feat: working export with pngjs 2023-06-16 22:36:24 +02:00
Arnošt Pleskot
2b19d53549 feat: init of generating blobs in chunk 2023-06-14 11:52:16 +02:00
Alex Kim
b4abfad638 fix: bound arrows not updated when rotating multiple elements (#6662) 2023-06-09 13:22:40 +02:00
WBbug
a39640ead1 fix: delete setCursor when resize (#6660) 2023-06-08 11:41:22 +02:00
David Luzar
84bd9bd4ff fix: creating text while color picker open (#6651)
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2023-06-06 22:04:06 +02:00
Aakansha Doshi
ae7ff76126 fix: cleanup textWysiwyg and getAdjustedDimensions (#6520)
* fix: cleanup textWysiwyg and getAdjustedDimensions

* fix lint

* fix test
2023-06-06 14:36:18 +05:30
11 changed files with 220 additions and 302 deletions

View File

@@ -53,6 +53,7 @@
"sass": "1.51.0",
"socket.io-client": "2.3.1",
"tunnel-rat": "0.1.2",
"upng-js": "2.1.0",
"workbox-background-sync": "^6.5.4",
"workbox-broadcast-update": "^6.5.4",
"workbox-cacheable-response": "^6.5.4",
@@ -78,6 +79,7 @@
"@types/react-dom": "18.0.6",
"@types/resize-observer-browser": "0.1.7",
"@types/socket.io-client": "1.4.36",
"@types/upng-js": "2.1.2",
"chai": "4.3.6",
"dotenv": "16.0.1",
"eslint-config-prettier": "8.5.0",

View File

@@ -825,6 +825,14 @@ class App extends React.Component<AppProps, AppState> {
if (typeof this.props.name !== "undefined") {
name = this.props.name;
}
editingElement =
editingElement || actionResult.appState?.editingElement || null;
if (editingElement?.isDeleted) {
editingElement = null;
}
this.setState(
(state) => {
// using Object.assign instead of spread to fool TS 4.2.2+ into
@@ -835,8 +843,7 @@ class App extends React.Component<AppProps, AppState> {
// or programmatically from the host, so it will need to be
// rewritten later
contextMenu: null,
editingElement:
editingElement || actionResult.appState?.editingElement || null,
editingElement,
viewModeEnabled,
zenModeEnabled,
gridSize,
@@ -1347,6 +1354,12 @@ class App extends React.Component<AppProps, AppState> {
});
}
// failsafe in case the state is being updated in incorrect order resulting
// in the editingElement being now a deleted element
if (this.state.editingElement?.isDeleted) {
this.setState({ editingElement: null });
}
if (
this.state.selectedLinearElement &&
!this.state.selectedElementIds[this.state.selectedLinearElement.elementId]
@@ -4142,12 +4155,6 @@ class App extends React.Component<AppProps, AppState> {
);
}
if (pointerDownState.resize.handleType) {
setCursor(
this.canvas,
getCursorForResizingElement({
transformHandleType: pointerDownState.resize.handleType,
}),
);
pointerDownState.resize.isResizing = true;
pointerDownState.resize.offset = tupleToCoors(
getResizeOffsetXY(

View File

@@ -1,4 +1,4 @@
import { isTransparent, isWritableElement } from "../../utils";
import { isInteractive, isTransparent, isWritableElement } from "../../utils";
import { ExcalidrawElement } from "../../element/types";
import { AppState } from "../../types";
import { TopPicks } from "./TopPicks";
@@ -121,11 +121,14 @@ const ColorPickerPopupContent = ({
}
}}
onCloseAutoFocus={(e) => {
e.preventDefault();
e.stopPropagation();
// prevents focusing the trigger
e.preventDefault();
// return focus to excalidraw container
if (container) {
// return focus to excalidraw container unless
// user focuses an interactive element, such as a button, or
// enters the text editor by clicking on canvas with the text tool
if (container && !isInteractive(document.activeElement)) {
container.focus();
}

View File

@@ -13,6 +13,7 @@ import { FileSystemHandle, nativeFileSystemSupported } from "./filesystem";
import { isValidExcalidrawData, isValidLibrary } from "./json";
import { restore, restoreLibraryItems } from "./restore";
import { ImportedLibraryData } from "./types";
import UPNG from "upng-js";
const parseFileContents = async (blob: Blob | File) => {
let contents: string;
@@ -210,9 +211,7 @@ export const loadLibraryFromBlob = async (
return parseLibraryJSON(await parseFileContents(blob), defaultStatus);
};
export const canvasToBlob = async (
canvas: HTMLCanvasElement,
): Promise<Blob> => {
const _canvasToBlob = async (canvas: HTMLCanvasElement): Promise<Blob> => {
return new Promise((resolve, reject) => {
try {
canvas.toBlob((blob) => {
@@ -232,6 +231,31 @@ export const canvasToBlob = async (
});
};
export const canvasToBlob = async (
canvas: HTMLCanvasElement,
): Promise<Blob> => {
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error("No canvas context");
}
console.time("getImageData");
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
console.timeEnd("getImageData");
console.time("encode");
const pngData = UPNG.encode(
[imageData.buffer],
canvas.width,
canvas.height,
0,
);
console.timeEnd("encode");
return new Blob([pngData], { type: "image/png" });
};
/** generates SHA-1 digest from supplied file (if not supported, falls back
to a 40-char base64 random id) */
export const generateIdFromFile = async (file: File): Promise<FileId> => {

View File

@@ -75,7 +75,9 @@ export const exportCanvas = async (
document.body.appendChild(tempCanvas);
if (type === "png") {
console.time("export png");
let blob = await canvasToBlob(tempCanvas);
console.timeEnd("export png");
tempCanvas.remove();
if (appState.exportEmbedScene) {
blob = await (

View File

@@ -20,15 +20,13 @@ import {
isTestEnv,
} from "../utils";
import { randomInteger, randomId } from "../random";
import { bumpVersion, mutateElement, newElementWith } from "./mutateElement";
import { bumpVersion, newElementWith } from "./mutateElement";
import { getNewGroupIdsForDuplication } from "../groups";
import { AppState } from "../types";
import { getElementAbsoluteCoords } from ".";
import { adjustXYWithRotation } from "../math";
import { getResizedElementAbsoluteCoords } from "./bounds";
import {
getBoundTextElementOffset,
getContainerDims,
getContainerElement,
measureText,
normalizeText,
@@ -44,7 +42,6 @@ import {
DEFAULT_VERTICAL_ALIGN,
VERTICAL_ALIGN,
} from "../constants";
import { isArrowElement } from "./typeChecks";
import { MarkOptional, Merge, Mutable } from "../utility-types";
type ElementConstructorOpts = MarkOptional<
@@ -211,8 +208,6 @@ const getAdjustedDimensions = (
height: number;
baseline: number;
} => {
const container = getContainerElement(element);
const {
width: nextWidth,
height: nextHeight,
@@ -268,27 +263,6 @@ const getAdjustedDimensions = (
);
}
// make sure container dimensions are set properly when
// text editor overflows beyond viewport dimensions
if (container) {
const boundTextElementPadding = getBoundTextElementOffset(element);
const containerDims = getContainerDims(container);
let height = containerDims.height;
let width = containerDims.width;
if (nextHeight > height - boundTextElementPadding * 2) {
height = nextHeight + boundTextElementPadding * 2;
}
if (nextWidth > width - boundTextElementPadding * 2) {
width = nextWidth + boundTextElementPadding * 2;
}
if (
!isArrowElement(container) &&
(height !== containerDims.height || width !== containerDims.width)
) {
mutateElement(container, { height, width });
}
}
return {
width: nextWidth,
height: nextHeight,

View File

@@ -890,26 +890,34 @@ const rotateMultipleElements = (
centerY,
centerAngle + origAngle - element.angle,
);
mutateElement(element, {
x: element.x + (rotatedCX - cx),
y: element.y + (rotatedCY - cy),
angle: normalizeAngle(centerAngle + origAngle),
});
const boundTextElementId = getBoundTextElementId(element);
if (boundTextElementId) {
const textElement =
Scene.getScene(element)?.getElement<ExcalidrawTextElementWithContainer>(
boundTextElementId,
);
if (textElement && !isArrowElement(element)) {
mutateElement(textElement, {
x: textElement.x + (rotatedCX - cx),
y: textElement.y + (rotatedCY - cy),
mutateElement(
element,
{
x: element.x + (rotatedCX - cx),
y: element.y + (rotatedCY - cy),
angle: normalizeAngle(centerAngle + origAngle),
},
false,
);
updateBoundElements(element, { simultaneouslyUpdated: elements });
const boundText = getBoundTextElement(element);
if (boundText && !isArrowElement(element)) {
mutateElement(
boundText,
{
x: boundText.x + (rotatedCX - cx),
y: boundText.y + (rotatedCY - cy),
angle: normalizeAngle(centerAngle + origAngle),
});
}
},
false,
);
}
});
Scene.getScene(elements[0])?.informMutation();
};
export const getResizeOffsetXY = (

View File

@@ -26,6 +26,17 @@ ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
const tab = " ";
const mouse = new Pointer("mouse");
const getTextEditor = () => {
return document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
};
const updateTextEditor = (editor: HTMLTextAreaElement, value: string) => {
fireEvent.change(editor, { target: { value } });
editor.dispatchEvent(new Event("input"));
};
describe("textWysiwyg", () => {
describe("start text editing", () => {
const { h } = window;
@@ -190,9 +201,7 @@ describe("textWysiwyg", () => {
mouse.clickAt(text.x + 50, text.y + 50);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
const editor = getTextEditor();
expect(editor).not.toBe(null);
expect(h.state.editingElement?.id).toBe(text.id);
@@ -214,9 +223,7 @@ describe("textWysiwyg", () => {
mouse.doubleClickAt(text.x + 50, text.y + 50);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
const editor = getTextEditor();
expect(editor).not.toBe(null);
expect(h.state.editingElement?.id).toBe(text.id);
@@ -243,9 +250,7 @@ describe("textWysiwyg", () => {
textElement = UI.createElement("text");
mouse.clickOn(textElement);
textarea = document.querySelector(
".excalidraw-textEditorContainer > textarea",
)!;
textarea = getTextEditor();
});
afterAll(() => {
@@ -455,17 +460,11 @@ describe("textWysiwyg", () => {
UI.clickTool("text");
mouse.clickAt(750, 300);
textarea = document.querySelector(
".excalidraw-textEditorContainer > textarea",
)!;
fireEvent.change(textarea, {
target: {
value:
"Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
},
});
textarea.dispatchEvent(new Event("input"));
textarea = getTextEditor();
updateTextEditor(
textarea,
"Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
);
await new Promise((cb) => setTimeout(cb, 0));
textarea.blur();
expect(textarea.style.width).toBe("792px");
@@ -513,11 +512,9 @@ describe("textWysiwyg", () => {
{ id: text.id, type: "text" },
]);
mouse.down();
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
const editor = getTextEditor();
fireEvent.change(editor, { target: { value: "Hello World!" } });
updateTextEditor(editor, "Hello World!");
await new Promise((r) => setTimeout(r, 0));
editor.blur();
@@ -543,11 +540,9 @@ describe("textWysiwyg", () => {
]);
expect(text.angle).toBe(rectangle.angle);
mouse.down();
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
const editor = getTextEditor();
fireEvent.change(editor, { target: { value: "Hello World!" } });
updateTextEditor(editor, "Hello World!");
await new Promise((r) => setTimeout(r, 0));
editor.blur();
@@ -572,9 +567,7 @@ describe("textWysiwyg", () => {
API.setSelectedElements([diamond]);
Keyboard.keyPress(KEYS.ENTER);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
const editor = getTextEditor();
await new Promise((r) => setTimeout(r, 0));
const value = new Array(1000).fill("1").join("\n");
@@ -587,9 +580,7 @@ describe("textWysiwyg", () => {
expect(diamond.height).toBe(50020);
// Clearing text to simulate height decrease
expect(() =>
fireEvent.input(editor, { target: { value: "" } }),
).not.toThrow();
expect(() => updateTextEditor(editor, "")).not.toThrow();
expect(diamond.height).toBe(70);
});
@@ -611,9 +602,7 @@ describe("textWysiwyg", () => {
expect(text.type).toBe("text");
expect(text.containerId).toBe(null);
mouse.down();
let editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
let editor = getTextEditor();
await new Promise((r) => setTimeout(r, 0));
editor.blur();
@@ -628,11 +617,9 @@ describe("textWysiwyg", () => {
expect(text.containerId).toBe(rectangle.id);
mouse.down();
editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
editor = getTextEditor();
fireEvent.change(editor, { target: { value: "Hello World!" } });
updateTextEditor(editor, "Hello World!");
await new Promise((r) => setTimeout(r, 0));
editor.blur();
@@ -652,13 +639,11 @@ describe("textWysiwyg", () => {
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;
const editor = getTextEditor();
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, { target: { value: "Hello World!" } });
updateTextEditor(editor, "Hello World!");
editor.blur();
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
@@ -689,11 +674,8 @@ describe("textWysiwyg", () => {
{ id: text.id, type: "text" },
]);
mouse.down();
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
fireEvent.change(editor, { target: { value: "Hello World!" } });
const editor = getTextEditor();
updateTextEditor(editor, "Hello World!");
await new Promise((r) => setTimeout(r, 0));
editor.blur();
@@ -717,17 +699,9 @@ describe("textWysiwyg", () => {
freedraw.y + freedraw.height / 2,
);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
fireEvent.change(editor, {
target: {
value: "Hello World!",
},
});
const editor = getTextEditor();
updateTextEditor(editor, "Hello World!");
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
editor.dispatchEvent(new Event("input"));
expect(freedraw.boundElements).toBe(null);
expect(h.elements[1].type).toBe("text");
@@ -759,11 +733,9 @@ describe("textWysiwyg", () => {
expect(text.type).toBe("text");
expect(text.containerId).toBe(null);
mouse.down();
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
const editor = getTextEditor();
fireEvent.change(editor, { target: { value: "Hello World!" } });
updateTextEditor(editor, "Hello World!");
await new Promise((r) => setTimeout(r, 0));
editor.blur();
@@ -776,17 +748,12 @@ describe("textWysiwyg", () => {
UI.clickTool("text");
mouse.clickAt(20, 30);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
const editor = getTextEditor();
fireEvent.change(editor, {
target: {
value: "Excalidraw is an opensource virtual collaborative whiteboard",
},
});
editor.dispatchEvent(new Event("input"));
updateTextEditor(
editor,
"Excalidraw is an opensource virtual collaborative whiteboard",
);
await new Promise((cb) => setTimeout(cb, 0));
expect(h.elements.length).toBe(2);
expect(h.elements[1].type).toBe("text");
@@ -826,12 +793,10 @@ describe("textWysiwyg", () => {
mouse.down();
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
let editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
let editor = getTextEditor();
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, { target: { value: "Hello World!" } });
updateTextEditor(editor, "Hello World!");
editor.blur();
expect(text.fontFamily).toEqual(FONT_FAMILY.Virgil);
UI.clickTool("text");
@@ -841,9 +806,7 @@ describe("textWysiwyg", () => {
rectangle.y + rectangle.height / 2,
);
mouse.down();
editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
editor = getTextEditor();
editor.select();
fireEvent.click(screen.getByTitle(/code/i));
@@ -876,17 +839,9 @@ describe("textWysiwyg", () => {
Keyboard.keyDown(KEYS.ENTER);
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
let editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
let editor = getTextEditor();
fireEvent.change(editor, {
target: {
value: "Hello World!",
},
});
editor.dispatchEvent(new Event("input"));
updateTextEditor(editor, "Hello World!");
await new Promise((cb) => setTimeout(cb, 0));
editor.blur();
@@ -905,17 +860,8 @@ describe("textWysiwyg", () => {
mouse.select(rectangle);
Keyboard.keyPress(KEYS.ENTER);
editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
fireEvent.change(editor, {
target: {
value: "Hello",
},
});
editor.dispatchEvent(new Event("input"));
editor = getTextEditor();
updateTextEditor(editor, "Hello");
await new Promise((r) => setTimeout(r, 0));
@@ -943,13 +889,11 @@ describe("textWysiwyg", () => {
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.containerId).toBe(rectangle.id);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
const editor = getTextEditor();
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, { target: { value: "Hello World!" } });
updateTextEditor(editor, "Hello World!");
editor.blur();
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
@@ -982,11 +926,9 @@ describe("textWysiwyg", () => {
// Bind first text
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.containerId).toBe(rectangle.id);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
const editor = getTextEditor();
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, { target: { value: "Hello World!" } });
updateTextEditor(editor, "Hello World!");
editor.blur();
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
@@ -1005,11 +947,9 @@ describe("textWysiwyg", () => {
it("should respect text alignment when resizing", async () => {
Keyboard.keyPress(KEYS.ENTER);
let editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
let editor = getTextEditor();
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, { target: { value: "Hello" } });
updateTextEditor(editor, "Hello");
editor.blur();
// should center align horizontally and vertically by default
@@ -1024,9 +964,7 @@ describe("textWysiwyg", () => {
mouse.select(rectangle);
Keyboard.keyPress(KEYS.ENTER);
editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
editor = getTextEditor();
editor.select();
@@ -1049,9 +987,7 @@ describe("textWysiwyg", () => {
mouse.select(rectangle);
Keyboard.keyPress(KEYS.ENTER);
editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
editor = getTextEditor();
editor.select();
@@ -1089,11 +1025,9 @@ describe("textWysiwyg", () => {
expect(text.type).toBe("text");
expect(text.containerId).toBe(rectangle.id);
mouse.down();
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
const editor = getTextEditor();
fireEvent.change(editor, { target: { value: "Hello World!" } });
updateTextEditor(editor, "Hello World!");
await new Promise((r) => setTimeout(r, 0));
editor.blur();
@@ -1106,11 +1040,9 @@ describe("textWysiwyg", () => {
it("should scale font size correctly when resizing using shift", async () => {
Keyboard.keyPress(KEYS.ENTER);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
const editor = getTextEditor();
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, { target: { value: "Hello" } });
updateTextEditor(editor, "Hello");
editor.blur();
const textElement = h.elements[1] as ExcalidrawTextElement;
expect(rectangle.width).toBe(90);
@@ -1128,11 +1060,9 @@ describe("textWysiwyg", () => {
it("should bind text correctly when container duplicated with alt-drag", async () => {
Keyboard.keyPress(KEYS.ENTER);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
const editor = getTextEditor();
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, { target: { value: "Hello" } });
updateTextEditor(editor, "Hello");
editor.blur();
expect(h.elements.length).toBe(2);
@@ -1162,11 +1092,9 @@ describe("textWysiwyg", () => {
it("undo should work", async () => {
Keyboard.keyPress(KEYS.ENTER);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
const editor = getTextEditor();
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, { target: { value: "Hello" } });
updateTextEditor(editor, "Hello");
editor.blur();
expect(rectangle.boundElements).toStrictEqual([
{ id: h.elements[1].id, type: "text" },
@@ -1201,54 +1129,64 @@ describe("textWysiwyg", () => {
it("should not allow bound text with only whitespaces", async () => {
Keyboard.keyPress(KEYS.ENTER);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
const editor = getTextEditor();
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, { target: { value: " " } });
updateTextEditor(editor, " ");
editor.blur();
expect(rectangle.boundElements).toStrictEqual([]);
expect(h.elements[1].isDeleted).toBe(true);
});
it("should restore original container height and clear cache once text is unbind", async () => {
const originalRectHeight = rectangle.height;
expect(rectangle.height).toBe(originalRectHeight);
Keyboard.keyPress(KEYS.ENTER);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, {
target: { value: "Online whiteboard collaboration made easy" },
const container = API.createElement({
type: "rectangle",
height: 75,
width: 90,
});
editor.blur();
expect(rectangle.height).toBe(185);
mouse.select(rectangle);
const originalRectHeight = container.height;
expect(container.height).toBe(originalRectHeight);
const text = API.createElement({
type: "text",
text: "Online whiteboard collaboration made easy",
});
h.elements = [container, text];
API.setSelectedElements([container, text]);
fireEvent.contextMenu(GlobalTestState.canvas, {
button: 2,
clientX: 20,
clientY: 30,
});
const contextMenu = document.querySelector(".context-menu");
let contextMenu = document.querySelector(".context-menu");
fireEvent.click(
queryByText(contextMenu as HTMLElement, "Bind text to the container")!,
);
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text).toBe(
"Online \nwhitebo\nard \ncollabo\nration \nmade \neasy",
);
fireEvent.contextMenu(GlobalTestState.canvas, {
button: 2,
clientX: 20,
clientY: 30,
});
contextMenu = document.querySelector(".context-menu");
fireEvent.click(queryByText(contextMenu as HTMLElement, "Unbind text")!);
expect(h.elements[0].boundElements).toEqual([]);
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(null);
expect(getOriginalContainerHeightFromCache(container.id)).toBe(null);
expect(rectangle.height).toBe(originalRectHeight);
expect(container.height).toBe(originalRectHeight);
});
it("should reset the container height cache when resizing", async () => {
Keyboard.keyPress(KEYS.ENTER);
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
let editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
let editor = getTextEditor();
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, { target: { value: "Hello" } });
updateTextEditor(editor, "Hello");
editor.blur();
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
@@ -1258,9 +1196,7 @@ describe("textWysiwyg", () => {
mouse.select(rectangle);
Keyboard.keyPress(KEYS.ENTER);
editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
editor = getTextEditor();
await new Promise((r) => setTimeout(r, 0));
editor.blur();
@@ -1273,12 +1209,8 @@ describe("textWysiwyg", () => {
Keyboard.keyPress(KEYS.ENTER);
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, { target: { value: "Hello World!" } });
const editor = getTextEditor();
updateTextEditor(editor, "Hello World!");
editor.blur();
mouse.select(rectangle);
@@ -1302,12 +1234,8 @@ describe("textWysiwyg", () => {
Keyboard.keyPress(KEYS.ENTER);
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, { target: { value: "Hello World!" } });
const editor = getTextEditor();
updateTextEditor(editor, "Hello World!");
editor.blur();
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
@@ -1338,17 +1266,12 @@ describe("textWysiwyg", () => {
beforeEach(async () => {
Keyboard.keyPress(KEYS.ENTER);
editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, { target: { value: "Hello" } });
editor = getTextEditor();
updateTextEditor(editor, "Hello");
editor.blur();
mouse.select(rectangle);
Keyboard.keyPress(KEYS.ENTER);
editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
editor = getTextEditor();
editor.select();
});
@@ -1459,17 +1382,12 @@ describe("textWysiwyg", () => {
it("should wrap text in a container when wrap text in container triggered from context menu", async () => {
UI.clickTool("text");
mouse.clickAt(20, 30);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
const editor = getTextEditor();
fireEvent.change(editor, {
target: {
value: "Excalidraw is an opensource virtual collaborative whiteboard",
},
});
editor.dispatchEvent(new Event("input"));
updateTextEditor(
editor,
"Excalidraw is an opensource virtual collaborative whiteboard",
);
await new Promise((cb) => setTimeout(cb, 0));
editor.select();

View File

@@ -11,7 +11,7 @@ import {
isBoundToContainer,
isTextElement,
} from "./typeChecks";
import { CLASSES, isSafari, VERTICAL_ALIGN } from "../constants";
import { CLASSES, isSafari } from "../constants";
import {
ExcalidrawElement,
ExcalidrawLinearElement,
@@ -23,12 +23,10 @@ import { AppState } from "../types";
import { mutateElement } from "./mutateElement";
import {
getBoundTextElementId,
getContainerCoords,
getContainerDims,
getContainerElement,
getTextElementAngle,
getTextWidth,
measureText,
normalizeText,
redrawTextBoundingBox,
wrapText,
@@ -36,6 +34,7 @@ import {
getBoundTextMaxWidth,
computeContainerDimensionForBoundText,
detectLineHeight,
computeBoundTextPosition,
} from "./textElement";
import {
actionDecreaseFontSize,
@@ -162,7 +161,7 @@ export const textWysiwyg = ({
let textElementWidth = updatedTextElement.width;
// Set to element height by default since that's
// what is going to be used for unbounded text
let textElementHeight = updatedTextElement.height;
const textElementHeight = updatedTextElement.height;
if (container && updatedTextElement.containerId) {
if (isArrowElement(container)) {
@@ -179,15 +178,6 @@ export const textWysiwyg = ({
editable,
);
const containerDims = getContainerDims(container);
// using editor.style.height to get the accurate height of text editor
const editorHeight = Number(editable.style.height.slice(0, -2));
if (editorHeight > 0) {
textElementHeight = editorHeight;
}
if (propertiesUpdated) {
// update height of the editor after properties updated
textElementHeight = updatedTextElement.height;
}
let originalContainerData;
if (propertiesUpdated) {
@@ -232,22 +222,12 @@ export const textWysiwyg = ({
container.type,
);
mutateElement(container, { height: targetContainerHeight });
}
// Start pushing text upward until a diff of 30px (padding)
// is reached
else {
const containerCoords = getContainerCoords(container);
// vertically center align the text
if (verticalAlign === VERTICAL_ALIGN.MIDDLE) {
if (!isArrowElement(container)) {
coordY =
containerCoords.y + maxHeight / 2 - textElementHeight / 2;
}
}
if (verticalAlign === VERTICAL_ALIGN.BOTTOM) {
coordY = containerCoords.y + (maxHeight - textElementHeight);
}
} else {
const { y } = computeBoundTextPosition(
container,
updatedTextElement as ExcalidrawTextElementWithContainer,
);
coordY = y;
}
}
const [viewportX, viewportY] = getViewportCoords(coordX, coordY);
@@ -388,25 +368,6 @@ export const textWysiwyg = ({
};
editable.oninput = () => {
const updatedTextElement = Scene.getScene(element)?.getElement(
id,
) as ExcalidrawTextElement;
const font = getFontString(updatedTextElement);
if (isBoundToContainer(element)) {
const container = getContainerElement(element);
const wrappedText = wrapText(
normalizeText(editable.value),
font,
getBoundTextMaxWidth(container!),
);
const { width, height } = measureText(
wrappedText,
font,
updatedTextElement.lineHeight,
);
editable.style.width = `${width}px`;
editable.style.height = `${height}px`;
}
onChange(normalizeText(editable.value));
};
}

View File

@@ -60,6 +60,13 @@ export const isInputLike = (
target instanceof HTMLTextAreaElement ||
target instanceof HTMLSelectElement;
export const isInteractive = (target: Element | EventTarget | null) => {
return (
isInputLike(target) ||
(target instanceof Element && !!target.closest("label, button"))
);
};
export const isWritableElement = (
target: Element | EventTarget | null,
): target is

View File

@@ -2851,6 +2851,11 @@
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.3.tgz#a136f83b0758698df454e328759dbd3d44555311"
integrity sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==
"@types/upng-js@2.1.2":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@types/upng-js/-/upng-js-2.1.2.tgz#4680f700c3003eaf958b92418c6e19521bb1e034"
integrity sha512-sxCnLDEmGFDcnrdSMml3/mDSbfSAvt86Ld32J6Ac75Og7GzzmwHbUnJNTue74tfQC/3PPD/W0jG2dQuF9v6UOg==
"@types/ws@^8.5.1":
version "8.5.4"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.4.tgz#bb10e36116d6e570dd943735f86c933c1587b8a5"
@@ -8034,7 +8039,7 @@ p-try@^2.0.0:
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
pako@1.0.11:
pako@1.0.11, pako@^1.0.5:
version "1.0.11"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
@@ -10557,6 +10562,13 @@ update-browserslist-db@^1.0.10:
escalade "^3.1.1"
picocolors "^1.0.0"
upng-js@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/upng-js/-/upng-js-2.1.0.tgz#7176e73973db361ca95d0fa14f958385db6b9dd2"
integrity sha512-d3xzZzpMP64YkjP5pr8gNyvBt7dLk/uGI67EctzDuVp4lCZyVMo0aJO6l/VDlgbInJYDY6cnClLoBp29eKWI6g==
dependencies:
pako "^1.0.5"
uri-js@^4.2.2:
version "4.4.1"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"