feat: support unbinding bound text (#4686)

* feat: support unbinding text

* fix unbound text

* move the unbind option next to group action

* use boundTextElement.id when unbinding

* update original text so it takes same bounding box when unbind

* Add spec

* recompute measurements when unbinding
This commit is contained in:
Aakansha Doshi 2022-02-01 20:11:24 +05:30 committed by GitHub
parent 719ae7b72f
commit edfbac9d7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 100 additions and 4 deletions

View File

@ -0,0 +1,44 @@
import { getNonDeletedElements } from "../element";
import { mutateElement } from "../element/mutateElement";
import { getBoundTextElement, measureText } from "../element/textElement";
import { ExcalidrawTextElement } from "../element/types";
import { getSelectedElements } from "../scene";
import { getFontString } from "../utils";
import { register } from "./register";
export const actionUnbindText = register({
name: "unbindText",
contextItemLabel: "labels.unbindText",
perform: (elements, appState) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
selectedElements.forEach((element) => {
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
const { width, height, baseline } = measureText(
boundTextElement.originalText,
getFontString(boundTextElement),
);
mutateElement(boundTextElement as ExcalidrawTextElement, {
containerId: null,
width,
height,
baseline,
text: boundTextElement.originalText,
});
mutateElement(element, {
boundElements: element.boundElements?.filter(
(ele) => ele.id !== boundTextElement.id,
),
});
}
});
return {
elements,
appState,
commitToHistory: true,
};
},
});

View File

@ -80,3 +80,4 @@ export { actionToggleGridMode } from "./actionToggleGridMode";
export { actionToggleZenMode } from "./actionToggleZenMode";
export { actionToggleStats } from "./actionToggleStats";
export { actionUnbindText } from "./actionUnbindText";

View File

@ -103,7 +103,8 @@ export type ActionName =
| "exportWithDarkMode"
| "toggleTheme"
| "increaseFontSize"
| "decreaseFontSize";
| "decreaseFontSize"
| "unbindText";
export type PanelComponentProps = {
elements: readonly ExcalidrawElement[];

View File

@ -26,6 +26,7 @@ import {
actionToggleGridMode,
actionToggleStats,
actionToggleZenMode,
actionUnbindText,
actionUngroup,
} from "../actions";
import { createRedoAction, createUndoAction } from "../actions/actionHistory";
@ -5031,6 +5032,10 @@ class App extends React.Component<AppProps, AppState> {
});
}
} else if (type === "element") {
const elementsWithUnbindedText = getSelectedElements(
elements,
this.state,
).some((element) => !hasBoundTextElement(element));
if (this.state.viewModeEnabled) {
ContextMenu.push({
options: [navigator.clipboard && actionCopy, ...options],
@ -5064,6 +5069,7 @@ class App extends React.Component<AppProps, AppState> {
actionPasteStyles,
separator,
maybeGroupAction && actionGroup,
!elementsWithUnbindedText && actionUnbindText,
maybeUngroupAction && actionUngroup,
(maybeGroupAction || maybeUngroupAction) && separator,
actionAddToLibrary,

View File

@ -175,7 +175,6 @@ export const measureText = (
container.style.whiteSpace = "pre";
container.style.font = font;
container.style.minHeight = "1em";
if (maxWidth) {
const lineHeight = getApproxLineHeight(font);
container.style.width = `${String(maxWidth)}px`;

View File

@ -1,9 +1,11 @@
import ReactDOM from "react-dom";
import ExcalidrawApp from "../excalidraw-app";
import { render, screen } from "../tests/test-utils";
import { GlobalTestState, render, screen } from "../tests/test-utils";
import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
import { CODES, KEYS } from "../keys";
import { fireEvent } from "../tests/test-utils";
import { queryByText } from "@testing-library/react";
import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants";
import {
ExcalidrawTextElement,
@ -472,5 +474,47 @@ describe("textWysiwyg", () => {
expect(text.height).toBe(APPROX_LINE_HEIGHT);
expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2);
});
it("should unbind bound text when unbind action from context menu is triggred", 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.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" },
]);
mouse.reset();
UI.clickTool("selection");
mouse.clickAt(10, 20);
mouse.down();
mouse.up();
fireEvent.contextMenu(GlobalTestState.canvas, {
button: 2,
clientX: 20,
clientY: 30,
});
const contextMenu = document.querySelector(".context-menu");
fireEvent.click(queryByText(contextMenu as HTMLElement, "Unbind text")!);
expect(h.elements[0].boundElements).toEqual([]);
expect((h.elements[1] as ExcalidrawTextElement).containerId).toEqual(
null,
);
});
});
});

View File

@ -104,7 +104,8 @@
"personalLib": "Personal Library",
"excalidrawLib": "Excalidraw Library",
"decreaseFontSize": "Decrease font size",
"increaseFontSize": "Increase font size"
"increaseFontSize": "Increase font size",
"unbindText": "Unbind text"
},
"buttons": {
"clearReset": "Reset the canvas",