From 625ecc64ed96e2e93a18acb5971b73fcff83bf85 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Mon, 21 Mar 2022 17:54:54 +0530 Subject: [PATCH] feat: Support binding text to container via context menu (#4935) * feat: Support binding text to closest container * Bind text to selected container * show bind action in canvas and selected container after binding * allow binding if container has no bound text * fix * move logic to show/hide bind actions to contextMenuPredicate * don't show bind action when clicking on bounding box and not elemnts --- src/actions/actionBoundText.tsx | 134 +++++++++++++++++++++++++++++++ src/actions/actionProperties.tsx | 24 +----- src/actions/actionStyles.ts | 6 +- src/actions/actionUnbindText.tsx | 44 ---------- src/actions/index.ts | 2 +- src/actions/types.ts | 3 +- src/components/App.tsx | 18 +++-- src/element/textElement.ts | 8 +- src/locales/en.json | 1 + 9 files changed, 159 insertions(+), 81 deletions(-) create mode 100644 src/actions/actionBoundText.tsx delete mode 100644 src/actions/actionUnbindText.tsx diff --git a/src/actions/actionBoundText.tsx b/src/actions/actionBoundText.tsx new file mode 100644 index 00000000..6334e076 --- /dev/null +++ b/src/actions/actionBoundText.tsx @@ -0,0 +1,134 @@ +import { VERTICAL_ALIGN } from "../constants"; +import { getNonDeletedElements, isTextElement } from "../element"; +import { mutateElement } from "../element/mutateElement"; +import { + getBoundTextElement, + measureText, + redrawTextBoundingBox, +} from "../element/textElement"; +import { + hasBoundTextElement, + isTextBindableContainer, +} from "../element/typeChecks"; +import { + ExcalidrawTextContainer, + 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", + contextItemPredicate: (elements, appState) => { + const selectedElements = getSelectedElements(elements, appState); + return selectedElements.some((element) => hasBoundTextElement(element)); + }, + 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, + }; + }, +}); + +export const actionBindText = register({ + name: "bindText", + contextItemLabel: "labels.bindText", + contextItemPredicate: (elements, appState) => { + const selectedElements = getSelectedElements(elements, appState); + + if (selectedElements.length === 2) { + const textElement = + isTextElement(selectedElements[0]) || + isTextElement(selectedElements[1]); + + let bindingContainer; + if (isTextBindableContainer(selectedElements[0])) { + bindingContainer = selectedElements[0]; + } else if (isTextBindableContainer(selectedElements[1])) { + bindingContainer = selectedElements[1]; + } + if ( + textElement && + bindingContainer && + getBoundTextElement(bindingContainer) === null + ) { + return true; + } + } + return false; + }, + perform: (elements, appState) => { + const selectedElements = getSelectedElements( + getNonDeletedElements(elements), + appState, + ); + + let textElement: ExcalidrawTextElement; + let container: ExcalidrawTextContainer; + + if ( + isTextElement(selectedElements[0]) && + isTextBindableContainer(selectedElements[1]) + ) { + textElement = selectedElements[0]; + container = selectedElements[1]; + } else { + textElement = selectedElements[1] as ExcalidrawTextElement; + container = selectedElements[0] as ExcalidrawTextContainer; + } + mutateElement(textElement, { + containerId: container.id, + verticalAlign: VERTICAL_ALIGN.MIDDLE, + }); + mutateElement(container, { + boundElements: (container.boundElements || []).concat({ + type: "text", + id: textElement.id, + }), + }); + redrawTextBoundingBox(textElement, container); + const updatedElements = elements.slice(); + const textElementIndex = updatedElements.findIndex( + (ele) => ele.id === textElement.id, + ); + updatedElements.splice(textElementIndex, 1); + const containerIndex = updatedElements.findIndex( + (ele) => ele.id === container.id, + ); + updatedElements.splice(containerIndex + 1, 0, textElement); + return { + elements: updatedElements, + appState: { ...appState, selectedElementIds: { [container.id]: true } }, + commitToHistory: true, + }; + }, +}); diff --git a/src/actions/actionProperties.tsx b/src/actions/actionProperties.tsx index d89b4093..d961006a 100644 --- a/src/actions/actionProperties.tsx +++ b/src/actions/actionProperties.tsx @@ -166,11 +166,7 @@ const changeFontSize = ( let newElement: ExcalidrawTextElement = newElementWith(oldElement, { fontSize: newFontSize, }); - redrawTextBoundingBox( - newElement, - getContainerElement(oldElement), - appState, - ); + redrawTextBoundingBox(newElement, getContainerElement(oldElement)); newElement = offsetElementAfterFontResize(oldElement, newElement); @@ -637,11 +633,7 @@ export const actionChangeFontFamily = register({ fontFamily: value, }, ); - redrawTextBoundingBox( - newElement, - getContainerElement(oldElement), - appState, - ); + redrawTextBoundingBox(newElement, getContainerElement(oldElement)); return newElement; } @@ -720,11 +712,7 @@ export const actionChangeTextAlign = register({ oldElement, { textAlign: value }, ); - redrawTextBoundingBox( - newElement, - getContainerElement(oldElement), - appState, - ); + redrawTextBoundingBox(newElement, getContainerElement(oldElement)); return newElement; } @@ -797,11 +785,7 @@ export const actionChangeVerticalAlign = register({ { verticalAlign: value }, ); - redrawTextBoundingBox( - newElement, - getContainerElement(oldElement), - appState, - ); + redrawTextBoundingBox(newElement, getContainerElement(oldElement)); return newElement; } diff --git a/src/actions/actionStyles.ts b/src/actions/actionStyles.ts index 3244321a..f80c9175 100644 --- a/src/actions/actionStyles.ts +++ b/src/actions/actionStyles.ts @@ -63,11 +63,7 @@ export const actionPasteStyles = register({ textAlign: pastedElement?.textAlign || DEFAULT_TEXT_ALIGN, }); - redrawTextBoundingBox( - newElement, - getContainerElement(newElement), - appState, - ); + redrawTextBoundingBox(newElement, getContainerElement(newElement)); } return newElement; } diff --git a/src/actions/actionUnbindText.tsx b/src/actions/actionUnbindText.tsx deleted file mode 100644 index 1c116ebd..00000000 --- a/src/actions/actionUnbindText.tsx +++ /dev/null @@ -1,44 +0,0 @@ -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, - }; - }, -}); diff --git a/src/actions/index.ts b/src/actions/index.ts index 5d417f06..d9fdfa70 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -81,5 +81,5 @@ export { actionToggleGridMode } from "./actionToggleGridMode"; export { actionToggleZenMode } from "./actionToggleZenMode"; export { actionToggleStats } from "./actionToggleStats"; -export { actionUnbindText } from "./actionUnbindText"; +export { actionUnbindText, actionBindText } from "./actionBoundText"; export { actionLink } from "../element/Hyperlink"; diff --git a/src/actions/types.ts b/src/actions/types.ts index 4009822f..9c6bc2c9 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -107,7 +107,8 @@ export type ActionName = | "decreaseFontSize" | "unbindText" | "hyperlink" - | "eraser"; + | "eraser" + | "bindText"; export type PanelComponentProps = { elements: readonly ExcalidrawElement[]; diff --git a/src/components/App.tsx b/src/components/App.tsx index 85d199af..90b8158c 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -27,6 +27,7 @@ import { actionToggleStats, actionToggleZenMode, actionUnbindText, + actionBindText, actionUngroup, actionLink, } from "../actions"; @@ -5391,6 +5392,16 @@ class App extends React.Component { this.actionManager.getAppState(), ); + const mayBeAllowUnbinding = actionUnbindText.contextItemPredicate( + this.actionManager.getElementsIncludingDeleted(), + this.actionManager.getAppState(), + ); + + const mayBeAllowBinding = actionBindText.contextItemPredicate( + this.actionManager.getElementsIncludingDeleted(), + this.actionManager.getAppState(), + ); + const separator = "separator"; const elements = this.scene.getElements(); @@ -5467,10 +5478,6 @@ class App extends React.Component { }); } } 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], @@ -5504,7 +5511,8 @@ class App extends React.Component { actionPasteStyles, separator, maybeGroupAction && actionGroup, - !elementsWithUnbindedText && actionUnbindText, + mayBeAllowUnbinding && actionUnbindText, + mayBeAllowBinding && actionBindText, maybeUngroupAction && actionUngroup, (maybeGroupAction || maybeUngroupAction) && separator, actionAddToLibrary, diff --git a/src/element/textElement.ts b/src/element/textElement.ts index 5b65fce5..f1969a89 100644 --- a/src/element/textElement.ts +++ b/src/element/textElement.ts @@ -10,13 +10,11 @@ import { mutateElement } from "./mutateElement"; import { BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants"; import { MaybeTransformHandleType } from "./transformHandles"; import Scene from "../scene/Scene"; -import { AppState } from "../types"; import { isTextElement } from "."; export const redrawTextBoundingBox = ( element: ExcalidrawTextElement, container: ExcalidrawElement | null, - appState: AppState, ) => { const maxWidth = container ? container.width - BOUND_TEXT_PADDING * 2 @@ -35,12 +33,12 @@ export const redrawTextBoundingBox = ( getFontString(element), maxWidth, ); - let coordY = element.y; + let coordX = element.x; // Resize container and vertically center align the text if (container) { let nextHeight = container.height; - + coordX = container.x + BOUND_TEXT_PADDING; if (element.verticalAlign === VERTICAL_ALIGN.TOP) { coordY = container.y + BOUND_TEXT_PADDING; } else if (element.verticalAlign === VERTICAL_ALIGN.BOTTOM) { @@ -55,12 +53,12 @@ export const redrawTextBoundingBox = ( } mutateElement(container, { height: nextHeight }); } - mutateElement(element, { width: metrics.width, height: metrics.height, baseline: metrics.baseline, y: coordY, + x: coordX, text, }); }; diff --git a/src/locales/en.json b/src/locales/en.json index 9055760e..58152c67 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -107,6 +107,7 @@ "decreaseFontSize": "Decrease font size", "increaseFontSize": "Increase font size", "unbindText": "Unbind text", + "bindText": "Bind text to the container", "link": { "edit": "Edit link", "create": "Create link",