瀏覽代碼

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
Aakansha Doshi 3 年之前
父節點
當前提交
edfbac9d7d

+ 44 - 0
src/actions/actionUnbindText.tsx

@@ -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,
+    };
+  },
+});

+ 1 - 0
src/actions/index.ts

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

+ 2 - 1
src/actions/types.ts

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

+ 6 - 0
src/components/App.tsx

@@ -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,

+ 0 - 1
src/element/textElement.ts

@@ -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`;

+ 45 - 1
src/element/textWysiwyg.test.tsx

@@ -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,
+      );
+    });
   });
 });

+ 2 - 1
src/locales/en.json

@@ -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",