display Container settings when text is editing

This commit is contained in:
zsviczian
2025-09-21 20:35:05 +00:00
parent dee5c2ea8e
commit f5f3b61779
2 changed files with 149 additions and 48 deletions

View File

@@ -1515,18 +1515,112 @@ export const actionChangeContainerBehavior = register({
name: "changeContainerBehavior",
label: "labels.container",
trackEvent: false,
perform: (elements, appState, value) => {
return {
elements: changeProperty(elements, appState, (el) =>
newElementWith(el, {
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<string>();
// 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: nextElements,
appState: { ...appState, currentItemContainerBehavior: value },
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
},
PanelComponent: ({ elements, appState, updateData, app }) => (
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 (
<fieldset>
{appState.stylesPanelMode === "full" && (
<legend>{t("labels.container")}</legend>
@@ -1546,28 +1640,18 @@ export const actionChangeContainerBehavior = register({
icon: stickyNoteIcon,
},
]}
value={getFormValue(
elements,
app,
(element) => element.containerBehavior ?? "growing",
(element) =>
isFlowchartNodeElement(element) &&
Boolean(
getBoundTextElement(
element,
app.scene.getNonDeletedElementsMap(),
),
),
(hasSelection) =>
hasSelection
value={
value ??
(targetContainers.length
? null
: appState.currentItemContainerBehavior ?? "growing",
)}
onChange={(value) => updateData(value)}
: appState.currentItemContainerBehavior ?? "growing")
}
onChange={(val) => updateData(val)}
/>
</div>
</fieldset>
),
);
},
});
const getArrowheadOptions = (flip: boolean) => {

View File

@@ -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,
);
@@ -241,6 +249,8 @@ export const SelectedShapeActions = ({
<>{renderAction("changeArrowhead")}</>
)}
{isStickyNoteContainer && <>{renderAction("changeContainerBehavior")}</>}
{renderAction("changeOpacity")}
<fieldset>
@@ -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")}</>}
</div>
</PropertiesPopover>
)}