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 { actionToggleZenMode } from "./actionToggleZenMode";
export { actionToggleStats } from "./actionToggleStats"; export { actionToggleStats } from "./actionToggleStats";
export { actionUnbindText } from "./actionUnbindText";

View File

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

View File

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

View File

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

View File

@ -1,9 +1,11 @@
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import ExcalidrawApp from "../excalidraw-app"; 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 { Keyboard, Pointer, UI } from "../tests/helpers/ui";
import { CODES, KEYS } from "../keys"; import { CODES, KEYS } from "../keys";
import { fireEvent } from "../tests/test-utils"; import { fireEvent } from "../tests/test-utils";
import { queryByText } from "@testing-library/react";
import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants"; import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants";
import { import {
ExcalidrawTextElement, ExcalidrawTextElement,
@ -472,5 +474,47 @@ describe("textWysiwyg", () => {
expect(text.height).toBe(APPROX_LINE_HEIGHT); expect(text.height).toBe(APPROX_LINE_HEIGHT);
expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2); 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", "personalLib": "Personal Library",
"excalidrawLib": "Excalidraw Library", "excalidrawLib": "Excalidraw Library",
"decreaseFontSize": "Decrease font size", "decreaseFontSize": "Decrease font size",
"increaseFontSize": "Increase font size" "increaseFontSize": "Increase font size",
"unbindText": "Unbind text"
}, },
"buttons": { "buttons": {
"clearReset": "Reset the canvas", "clearReset": "Reset the canvas",