From f5f3b617796b63fb79a9cd094ce3b3199943a2f8 Mon Sep 17 00:00:00 2001 From: zsviczian Date: Sun, 21 Sep 2025 20:35:05 +0000 Subject: [PATCH] display Container settings when text is editing --- .../excalidraw/actions/actionProperties.tsx | 178 +++++++++++++----- packages/excalidraw/components/Actions.tsx | 19 +- 2 files changed, 149 insertions(+), 48 deletions(-) diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx index 69d064001d..dc4f754039 100644 --- a/packages/excalidraw/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -1515,59 +1515,143 @@ export const actionChangeContainerBehavior = register({ name: "changeContainerBehavior", label: "labels.container", trackEvent: false, - perform: (elements, appState, value) => { + perform: (elements, appState, value, app) => { + const elementsMap = app.scene.getNonDeletedElementsMap(); + let selected = getSelectedElements(elements, appState, { + includeBoundTextElement: true, + }); + + if (selected.length === 0 && appState.editingTextElement) { + selected = [appState.editingTextElement]; + } + + const containerIdsToUpdate = new Set(); + + // collect directly selected eligible containers + for (const el of selected) { + if ( + isFlowchartNodeElement(el) && + getBoundTextElement(el, elementsMap) + ) { + containerIdsToUpdate.add(el.id); + } + } + + // if none, and exactly one selected text element -> use its container if eligible + if ( + containerIdsToUpdate.size === 0 && + selected.length === 1 && + isTextElement(selected[0]) && + selected[0].containerId + ) { + const container = elementsMap.get(selected[0].containerId); + if ( + container && + isFlowchartNodeElement(container) && + getBoundTextElement(container, elementsMap) + ) { + containerIdsToUpdate.add(container.id); + } + } + + if (containerIdsToUpdate.size === 0) { + // nothing to update + return false; + } + + const nextElements = elements.map((el) => + containerIdsToUpdate.has(el.id) + ? newElementWith(el, { + containerBehavior: value, + }) + : el, + ); + return { - elements: changeProperty(elements, appState, (el) => - newElementWith(el, { - containerBehavior: value, - }), - ), + elements: nextElements, appState: { ...appState, currentItemContainerBehavior: value }, captureUpdate: CaptureUpdateAction.IMMEDIATELY, }; }, - PanelComponent: ({ elements, appState, updateData, app }) => ( -
- {appState.stylesPanelMode === "full" && ( - {t("labels.container")} - )} -
- element.containerBehavior ?? "growing", - (element) => - isFlowchartNodeElement(element) && - Boolean( - getBoundTextElement( - element, - app.scene.getNonDeletedElementsMap(), - ), - ), - (hasSelection) => - hasSelection + PanelComponent: ({ elements, appState, updateData, app }) => { + const elementsMap = app.scene.getNonDeletedElementsMap(); + let selected = getSelectedElements(elements, appState, { + includeBoundTextElement: true, + }); + + if (selected.length === 0 && appState.editingTextElement) { + selected = [appState.editingTextElement]; + } + + let targetContainers: ExcalidrawElement[] = []; + + // case 1: one text element selected -> target its container if eligible + if ( + selected.length === 1 && + isTextElement(selected[0]) && + selected[0].containerId + ) { + const container = elementsMap.get(selected[0].containerId); + if ( + container && + isFlowchartNodeElement(container) && + getBoundTextElement(container, elementsMap) + ) { + targetContainers = [container]; + } + } else { + // case 2: any eligible containers directly selected + targetContainers = selected.filter( + (el) => + isFlowchartNodeElement(el) && + getBoundTextElement(el, elementsMap), + ); + } + + if (targetContainers.length === 0) { + return null; + } + + const value = + reduceToCommonValue( + targetContainers, + (el) => el.containerBehavior ?? "growing", + ) ?? + // mixed selection -> show null so nothing appears selected + null; + + return ( +
+ {appState.stylesPanelMode === "full" && ( + {t("labels.container")} + )} +
+ updateData(value)} - /> -
-
- ), + : appState.currentItemContainerBehavior ?? "growing") + } + onChange={(val) => updateData(val)} + /> +
+
+ ); + }, }); const getArrowheadOptions = (flip: boolean) => { diff --git a/packages/excalidraw/components/Actions.tsx b/packages/excalidraw/components/Actions.tsx index 9d42732a11..89a1090da6 100644 --- a/packages/excalidraw/components/Actions.tsx +++ b/packages/excalidraw/components/Actions.tsx @@ -10,6 +10,7 @@ import { } from "@excalidraw/common"; import { + getContainerElement, hasContainerBehavior, isFlowchartNodeElement, shouldAllowVerticalAlign, @@ -149,6 +150,13 @@ export const SelectedShapeActions = ({ ) { isSingleElementBoundContainer = true; } + + const textContainer = targetElements.length === 1 && isTextElement(targetElements[0]) + ? getContainerElement(targetElements[0], elementsMap) + : null; + + const isStickyNoteContainer = textContainer && isFlowchartNodeElement(textContainer); + const isEditingTextOrNewElement = Boolean( appState.editingTextElement || appState.newElement, ); @@ -233,7 +241,7 @@ export const SelectedShapeActions = ({ renderAction("changeTextAlign")} )} - + {shouldAllowVerticalAlign(targetElements, elementsMap) && renderAction("changeVerticalAlign")} {(canHaveArrowheads(appState.activeTool.type) || @@ -241,6 +249,8 @@ export const SelectedShapeActions = ({ <>{renderAction("changeArrowhead")} )} + {isStickyNoteContainer && <>{renderAction("changeContainerBehavior")}} + {renderAction("changeOpacity")}
@@ -334,6 +344,12 @@ export const CompactShapeActions = ({ appState.editingTextElement || appState.newElement, ); + const textContainer = targetElements.length === 1 && isTextElement(targetElements[0]) + ? getContainerElement(targetElements[0], elementsMap) + : null; + + const isStickyNoteContainer = textContainer && isFlowchartNodeElement(textContainer); + const showFillIcons = (hasBackground(appState.activeTool.type) && !isTransparent(appState.currentItemBackgroundColor)) || @@ -616,6 +632,7 @@ export const CompactShapeActions = ({ renderAction("changeTextAlign")} {shouldAllowVerticalAlign(targetElements, elementsMap) && renderAction("changeVerticalAlign")} + {isStickyNoteContainer && <>{renderAction("changeContainerBehavior")}} )}