mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-09-22 00:41:09 +02:00
display Container settings when text is editing
This commit is contained in:
@@ -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<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: changeProperty(elements, appState, (el) =>
|
||||
newElementWith(el, {
|
||||
containerBehavior: value,
|
||||
}),
|
||||
),
|
||||
elements: nextElements,
|
||||
appState: { ...appState, currentItemContainerBehavior: value },
|
||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||
<fieldset>
|
||||
{appState.stylesPanelMode === "full" && (
|
||||
<legend>{t("labels.container")}</legend>
|
||||
)}
|
||||
<div className="buttonList">
|
||||
<RadioSelection
|
||||
group="container"
|
||||
options={[
|
||||
{
|
||||
value: "growing",
|
||||
text: t("labels.container_growing"),
|
||||
icon: growingContainerIcon,
|
||||
},
|
||||
{
|
||||
value: "stickyNote",
|
||||
text: t("labels.container_sticky"),
|
||||
icon: stickyNoteIcon,
|
||||
},
|
||||
]}
|
||||
value={getFormValue(
|
||||
elements,
|
||||
app,
|
||||
(element) => 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 (
|
||||
<fieldset>
|
||||
{appState.stylesPanelMode === "full" && (
|
||||
<legend>{t("labels.container")}</legend>
|
||||
)}
|
||||
<div className="buttonList">
|
||||
<RadioSelection
|
||||
group="container"
|
||||
options={[
|
||||
{
|
||||
value: "growing",
|
||||
text: t("labels.container_growing"),
|
||||
icon: growingContainerIcon,
|
||||
},
|
||||
{
|
||||
value: "stickyNote",
|
||||
text: t("labels.container_sticky"),
|
||||
icon: stickyNoteIcon,
|
||||
},
|
||||
]}
|
||||
value={
|
||||
value ??
|
||||
(targetContainers.length
|
||||
? null
|
||||
: appState.currentItemContainerBehavior ?? "growing",
|
||||
)}
|
||||
onChange={(value) => updateData(value)}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
),
|
||||
: appState.currentItemContainerBehavior ?? "growing")
|
||||
}
|
||||
onChange={(val) => updateData(val)}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const getArrowheadOptions = (flip: boolean) => {
|
||||
|
@@ -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")}
|
||||
|
||||
<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>
|
||||
)}
|
||||
|
Reference in New Issue
Block a user