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

View File

@@ -10,6 +10,7 @@ import {
} from "@excalidraw/common"; } from "@excalidraw/common";
import { import {
getContainerElement,
hasContainerBehavior, hasContainerBehavior,
isFlowchartNodeElement, isFlowchartNodeElement,
shouldAllowVerticalAlign, shouldAllowVerticalAlign,
@@ -149,6 +150,13 @@ export const SelectedShapeActions = ({
) { ) {
isSingleElementBoundContainer = true; isSingleElementBoundContainer = true;
} }
const textContainer = targetElements.length === 1 && isTextElement(targetElements[0])
? getContainerElement(targetElements[0], elementsMap)
: null;
const isStickyNoteContainer = textContainer && isFlowchartNodeElement(textContainer);
const isEditingTextOrNewElement = Boolean( const isEditingTextOrNewElement = Boolean(
appState.editingTextElement || appState.newElement, appState.editingTextElement || appState.newElement,
); );
@@ -241,6 +249,8 @@ export const SelectedShapeActions = ({
<>{renderAction("changeArrowhead")}</> <>{renderAction("changeArrowhead")}</>
)} )}
{isStickyNoteContainer && <>{renderAction("changeContainerBehavior")}</>}
{renderAction("changeOpacity")} {renderAction("changeOpacity")}
<fieldset> <fieldset>
@@ -334,6 +344,12 @@ export const CompactShapeActions = ({
appState.editingTextElement || appState.newElement, appState.editingTextElement || appState.newElement,
); );
const textContainer = targetElements.length === 1 && isTextElement(targetElements[0])
? getContainerElement(targetElements[0], elementsMap)
: null;
const isStickyNoteContainer = textContainer && isFlowchartNodeElement(textContainer);
const showFillIcons = const showFillIcons =
(hasBackground(appState.activeTool.type) && (hasBackground(appState.activeTool.type) &&
!isTransparent(appState.currentItemBackgroundColor)) || !isTransparent(appState.currentItemBackgroundColor)) ||
@@ -616,6 +632,7 @@ export const CompactShapeActions = ({
renderAction("changeTextAlign")} renderAction("changeTextAlign")}
{shouldAllowVerticalAlign(targetElements, elementsMap) && {shouldAllowVerticalAlign(targetElements, elementsMap) &&
renderAction("changeVerticalAlign")} renderAction("changeVerticalAlign")}
{isStickyNoteContainer && <>{renderAction("changeContainerBehavior")}</>}
</div> </div>
</PropertiesPopover> </PropertiesPopover>
)} )}