mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-11-17 11:14:23 +01:00
Initial implementation of containerBehavior.margin
This commit is contained in:
@@ -329,16 +329,24 @@ const generateElementCanvas = (
|
||||
boundTextCanvasContext.translate(-shiftX, -shiftY);
|
||||
// Clear the bound text area
|
||||
boundTextCanvasContext.clearRect(
|
||||
-(boundTextElement.width / 2 + BOUND_TEXT_PADDING) *
|
||||
-(
|
||||
boundTextElement.width / 2 +
|
||||
(element.containerBehavior?.margin ?? BOUND_TEXT_PADDING)
|
||||
) *
|
||||
window.devicePixelRatio *
|
||||
scale,
|
||||
-(boundTextElement.height / 2 + BOUND_TEXT_PADDING) *
|
||||
-(
|
||||
boundTextElement.height / 2 +
|
||||
(element.containerBehavior?.margin ?? BOUND_TEXT_PADDING)
|
||||
) *
|
||||
window.devicePixelRatio *
|
||||
scale,
|
||||
(boundTextElement.width + BOUND_TEXT_PADDING * 2) *
|
||||
(boundTextElement.width +
|
||||
(element.containerBehavior?.margin ?? BOUND_TEXT_PADDING) * 2) *
|
||||
window.devicePixelRatio *
|
||||
scale,
|
||||
(boundTextElement.height + BOUND_TEXT_PADDING * 2) *
|
||||
(boundTextElement.height +
|
||||
(element.containerBehavior?.margin ?? BOUND_TEXT_PADDING) * 2) *
|
||||
window.devicePixelRatio *
|
||||
scale,
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
SHIFT_LOCKING_ANGLE,
|
||||
rescalePoints,
|
||||
getFontString,
|
||||
BOUND_TEXT_PADDING,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import type { GlobalPoint } from "@excalidraw/math";
|
||||
@@ -741,10 +742,12 @@ export const resizeSingleElement = (
|
||||
const minWidth = getApproxMinLineWidth(
|
||||
getFontString(boundTextElement),
|
||||
boundTextElement.lineHeight,
|
||||
latestElement.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
|
||||
);
|
||||
const minHeight = getApproxMinLineHeight(
|
||||
boundTextElement.fontSize,
|
||||
boundTextElement.lineHeight,
|
||||
latestElement.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
|
||||
);
|
||||
nextWidth = Math.max(nextWidth, minWidth);
|
||||
nextHeight = Math.max(nextHeight, minHeight);
|
||||
|
||||
@@ -108,6 +108,7 @@ export const redrawTextBoundingBox = (
|
||||
const nextHeight = computeContainerDimensionForBoundText(
|
||||
metrics.height,
|
||||
container.type,
|
||||
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
|
||||
);
|
||||
scene.mutateElement(container, { height: nextHeight });
|
||||
updateOriginalContainerCache(container.id, nextHeight);
|
||||
@@ -117,6 +118,7 @@ export const redrawTextBoundingBox = (
|
||||
const nextWidth = computeContainerDimensionForBoundText(
|
||||
metrics.width,
|
||||
container.type,
|
||||
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
|
||||
);
|
||||
scene.mutateElement(container, { width: nextWidth });
|
||||
}
|
||||
@@ -187,6 +189,7 @@ export const handleBindTextResize = (
|
||||
containerHeight = computeContainerDimensionForBoundText(
|
||||
nextHeight,
|
||||
container.type,
|
||||
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
|
||||
);
|
||||
|
||||
const diff = containerHeight - container.height;
|
||||
@@ -353,8 +356,8 @@ export const getContainerCenter = (
|
||||
};
|
||||
|
||||
export const getContainerCoords = (container: NonDeletedExcalidrawElement) => {
|
||||
let offsetX = BOUND_TEXT_PADDING;
|
||||
let offsetY = BOUND_TEXT_PADDING;
|
||||
let offsetX = container.containerBehavior?.margin ?? BOUND_TEXT_PADDING;
|
||||
let offsetY = container.containerBehavior?.margin ?? BOUND_TEXT_PADDING;
|
||||
|
||||
if (container.type === "ellipse") {
|
||||
// The derivation of coordinates is explained in https://github.com/excalidraw/excalidraw/pull/6172
|
||||
@@ -446,9 +449,10 @@ export const isValidTextContainer = (element: {
|
||||
export const computeContainerDimensionForBoundText = (
|
||||
dimension: number,
|
||||
containerType: ExtractSetType<typeof VALID_CONTAINER_TYPES>,
|
||||
boundTextPadding: number,
|
||||
) => {
|
||||
dimension = Math.ceil(dimension);
|
||||
const padding = BOUND_TEXT_PADDING * 2;
|
||||
const padding = boundTextPadding * 2;
|
||||
|
||||
if (containerType === "ellipse") {
|
||||
return Math.round(((dimension + padding) / Math.sqrt(2)) * 2);
|
||||
@@ -467,6 +471,8 @@ export const getBoundTextMaxWidth = (
|
||||
boundTextElement: ExcalidrawTextElement | null,
|
||||
) => {
|
||||
const { width } = container;
|
||||
const boundTextPadding =
|
||||
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING;
|
||||
if (isArrowElement(container)) {
|
||||
const minWidth =
|
||||
(boundTextElement?.fontSize ?? DEFAULT_FONT_SIZE) *
|
||||
@@ -477,14 +483,14 @@ export const getBoundTextMaxWidth = (
|
||||
// The width of the largest rectangle inscribed inside an ellipse is
|
||||
// Math.round((ellipse.width / 2) * Math.sqrt(2)) which is derived from
|
||||
// equation of an ellipse -https://github.com/excalidraw/excalidraw/pull/6172
|
||||
return Math.round((width / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2;
|
||||
return Math.round((width / 2) * Math.sqrt(2)) - boundTextPadding * 2;
|
||||
}
|
||||
if (container.type === "diamond") {
|
||||
// The width of the largest rectangle inscribed inside a rhombus is
|
||||
// Math.round(width / 2) - https://github.com/excalidraw/excalidraw/pull/6265
|
||||
return Math.round(width / 2) - BOUND_TEXT_PADDING * 2;
|
||||
return Math.round(width / 2) - boundTextPadding * 2;
|
||||
}
|
||||
return width - BOUND_TEXT_PADDING * 2;
|
||||
return width - boundTextPadding * 2;
|
||||
};
|
||||
|
||||
export const getBoundTextMaxHeight = (
|
||||
@@ -492,8 +498,10 @@ export const getBoundTextMaxHeight = (
|
||||
boundTextElement: ExcalidrawTextElementWithContainer,
|
||||
) => {
|
||||
const { height } = container;
|
||||
const boundTextPadding =
|
||||
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING;
|
||||
if (isArrowElement(container)) {
|
||||
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
|
||||
const containerHeight = height - boundTextPadding * 8 * 2;
|
||||
if (containerHeight <= 0) {
|
||||
return boundTextElement.height;
|
||||
}
|
||||
@@ -503,14 +511,14 @@ export const getBoundTextMaxHeight = (
|
||||
// The height of the largest rectangle inscribed inside an ellipse is
|
||||
// Math.round((ellipse.height / 2) * Math.sqrt(2)) which is derived from
|
||||
// equation of an ellipse - https://github.com/excalidraw/excalidraw/pull/6172
|
||||
return Math.round((height / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2;
|
||||
return Math.round((height / 2) * Math.sqrt(2)) - boundTextPadding * 2;
|
||||
}
|
||||
if (container.type === "diamond") {
|
||||
// The height of the largest rectangle inscribed inside a rhombus is
|
||||
// Math.round(height / 2) - https://github.com/excalidraw/excalidraw/pull/6265
|
||||
return Math.round(height / 2) - BOUND_TEXT_PADDING * 2;
|
||||
return Math.round(height / 2) - boundTextPadding * 2;
|
||||
}
|
||||
return height - BOUND_TEXT_PADDING * 2;
|
||||
return height - boundTextPadding * 2;
|
||||
};
|
||||
|
||||
/** retrieves text from text elements and concatenates to a single string */
|
||||
|
||||
@@ -32,22 +32,24 @@ const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
|
||||
export const getApproxMinLineWidth = (
|
||||
font: FontString,
|
||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||
boundTextPadding: number = BOUND_TEXT_PADDING,
|
||||
) => {
|
||||
const maxCharWidth = getMaxCharWidth(font);
|
||||
if (maxCharWidth === 0) {
|
||||
return (
|
||||
measureText(DUMMY_TEXT.split("").join("\n"), font, lineHeight).width +
|
||||
BOUND_TEXT_PADDING * 2
|
||||
boundTextPadding * 2
|
||||
);
|
||||
}
|
||||
return maxCharWidth + BOUND_TEXT_PADDING * 2;
|
||||
return maxCharWidth + boundTextPadding * 2;
|
||||
};
|
||||
|
||||
export const getMinTextElementWidth = (
|
||||
font: FontString,
|
||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||
boundTextPadding: number = BOUND_TEXT_PADDING,
|
||||
) => {
|
||||
return measureText("", font, lineHeight).width + BOUND_TEXT_PADDING * 2;
|
||||
return measureText("", font, lineHeight).width + boundTextPadding * 2;
|
||||
};
|
||||
|
||||
export const isMeasureTextSupported = () => {
|
||||
@@ -99,8 +101,9 @@ export const getLineHeightInPx = (
|
||||
export const getApproxMinLineHeight = (
|
||||
fontSize: ExcalidrawTextElement["fontSize"],
|
||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||
boundTextPadding: number = BOUND_TEXT_PADDING,
|
||||
) => {
|
||||
return getLineHeightInPx(fontSize, lineHeight) + BOUND_TEXT_PADDING * 2;
|
||||
return getLineHeightInPx(fontSize, lineHeight) + boundTextPadding * 2;
|
||||
};
|
||||
|
||||
let textMetricsProvider: TextMetricsProvider | undefined;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getLineHeight } from "@excalidraw/common";
|
||||
import { BOUND_TEXT_PADDING, getLineHeight } from "@excalidraw/common";
|
||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||
|
||||
import { FONT_FAMILY, TEXT_ALIGN, VERTICAL_ALIGN } from "@excalidraw/common";
|
||||
@@ -63,9 +63,13 @@ describe("Test measureText", () => {
|
||||
type: "rectangle",
|
||||
...params,
|
||||
});
|
||||
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
|
||||
160,
|
||||
);
|
||||
expect(
|
||||
computeContainerDimensionForBoundText(
|
||||
150,
|
||||
element.type,
|
||||
BOUND_TEXT_PADDING,
|
||||
),
|
||||
).toEqual(160);
|
||||
});
|
||||
|
||||
it("should compute container height correctly for ellipse", () => {
|
||||
@@ -73,9 +77,13 @@ describe("Test measureText", () => {
|
||||
type: "ellipse",
|
||||
...params,
|
||||
});
|
||||
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
|
||||
226,
|
||||
);
|
||||
expect(
|
||||
computeContainerDimensionForBoundText(
|
||||
150,
|
||||
element.type,
|
||||
BOUND_TEXT_PADDING,
|
||||
),
|
||||
).toEqual(226);
|
||||
});
|
||||
|
||||
it("should compute container height correctly for diamond", () => {
|
||||
@@ -83,9 +91,13 @@ describe("Test measureText", () => {
|
||||
type: "diamond",
|
||||
...params,
|
||||
});
|
||||
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
|
||||
320,
|
||||
);
|
||||
expect(
|
||||
computeContainerDimensionForBoundText(
|
||||
150,
|
||||
element.type,
|
||||
BOUND_TEXT_PADDING,
|
||||
),
|
||||
).toEqual(320);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -236,6 +236,9 @@ export const actionWrapTextInContainer = register({
|
||||
let updatedElements: readonly ExcalidrawElement[] = elements.slice();
|
||||
const containerIds: Mutable<AppState["selectedElementIds"]> = {};
|
||||
|
||||
const boundTextPadding =
|
||||
appState.currentItemContainerBehavior?.margin ?? BOUND_TEXT_PADDING;
|
||||
|
||||
for (const textElement of selectedElements) {
|
||||
if (isTextElement(textElement) && !isBoundToContainer(textElement)) {
|
||||
const container = newElement({
|
||||
@@ -261,15 +264,17 @@ export const actionWrapTextInContainer = register({
|
||||
: null,
|
||||
opacity: 100,
|
||||
locked: false,
|
||||
x: textElement.x - BOUND_TEXT_PADDING,
|
||||
y: textElement.y - BOUND_TEXT_PADDING,
|
||||
x: textElement.x - boundTextPadding,
|
||||
y: textElement.y - boundTextPadding,
|
||||
width: computeContainerDimensionForBoundText(
|
||||
textElement.width,
|
||||
"rectangle",
|
||||
boundTextPadding,
|
||||
),
|
||||
height: computeContainerDimensionForBoundText(
|
||||
textElement.height,
|
||||
"rectangle",
|
||||
boundTextPadding,
|
||||
),
|
||||
groupIds: textElement.groupIds,
|
||||
frameId: textElement.frameId,
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import {
|
||||
canBecomePolygon,
|
||||
getNonDeletedElements,
|
||||
hasContainerBehavior,
|
||||
isFlowchartNodeElement,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
@@ -132,6 +133,9 @@ import {
|
||||
ArrowheadCrowfootOneOrManyIcon,
|
||||
stickyNoteIcon,
|
||||
growingContainerIcon,
|
||||
marginLargeIcon,
|
||||
marginMediumIcon,
|
||||
marginSmallIcon,
|
||||
} from "../components/icons";
|
||||
|
||||
import { Fonts } from "../fonts";
|
||||
@@ -1518,6 +1522,35 @@ export const actionChangeRoundness = register({
|
||||
},
|
||||
});
|
||||
|
||||
const getMargin = (value: "small" | "medium" | "large") => {
|
||||
switch (value) {
|
||||
case "small":
|
||||
return BOUND_TEXT_PADDING;
|
||||
case "medium":
|
||||
return 15;
|
||||
case "large":
|
||||
return 25;
|
||||
default:
|
||||
return BOUND_TEXT_PADDING;
|
||||
}
|
||||
};
|
||||
|
||||
const getMarginValue = (margin: number | null) => {
|
||||
if (margin === null) {
|
||||
return null;
|
||||
}
|
||||
switch (margin) {
|
||||
case BOUND_TEXT_PADDING:
|
||||
return "small";
|
||||
case 15:
|
||||
return "medium";
|
||||
case 25:
|
||||
return "large";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const actionChangeContainerBehavior = register({
|
||||
name: "changeContainerBehavior",
|
||||
label: "labels.container",
|
||||
@@ -1536,7 +1569,7 @@ export const actionChangeContainerBehavior = register({
|
||||
|
||||
// collect directly selected eligible containers
|
||||
for (const el of selected) {
|
||||
if (isFlowchartNodeElement(el) && getBoundTextElement(el, elementsMap)) {
|
||||
if (isFlowchartNodeElement(el)) {
|
||||
containerIdsToUpdate.add(el.id);
|
||||
}
|
||||
}
|
||||
@@ -1558,12 +1591,61 @@ export const actionChangeContainerBehavior = register({
|
||||
}
|
||||
}
|
||||
|
||||
if (containerIdsToUpdate.size === 0) {
|
||||
// nothing to update
|
||||
return false;
|
||||
if (value.hasOwnProperty("margin")) {
|
||||
if (containerIdsToUpdate.size === 0) {
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
currentItemContainerBehavior: {
|
||||
textFlow:
|
||||
appState.currentItemContainerBehavior?.textFlow ?? "growing",
|
||||
margin: getMargin(value.margin as "small" | "medium" | "large"),
|
||||
},
|
||||
},
|
||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
};
|
||||
}
|
||||
|
||||
const nextElements = changeProperty(elements, appState, (el) =>
|
||||
containerIdsToUpdate.has(el.id)
|
||||
? newElementWith(el, {
|
||||
containerBehavior: {
|
||||
textFlow: el.containerBehavior?.textFlow ?? "growing",
|
||||
margin: getMargin(value.margin as "small" | "medium" | "large"),
|
||||
},
|
||||
})
|
||||
: el,
|
||||
);
|
||||
|
||||
// Invalidate containers to trigger re-render
|
||||
containerIdsToUpdate.forEach((id) => {
|
||||
const container = nextElements.find((el) => el.id === id);
|
||||
if (container) {
|
||||
const boundText = getBoundTextElement(
|
||||
container,
|
||||
arrayToMap(nextElements),
|
||||
);
|
||||
if (boundText) {
|
||||
redrawTextBoundingBox(boundText, container, app.scene);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
elements: nextElements,
|
||||
appState: {
|
||||
...appState,
|
||||
currentItemContainerBehavior: {
|
||||
textFlow:
|
||||
appState.currentItemContainerBehavior?.textFlow ?? "growing",
|
||||
margin: getMargin(value.margin as "small" | "medium" | "large"),
|
||||
},
|
||||
},
|
||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
};
|
||||
}
|
||||
|
||||
const nextElements = elements.map((el) =>
|
||||
const nextElements = changeProperty(elements, appState, (el) =>
|
||||
containerIdsToUpdate.has(el.id)
|
||||
? newElementWith(el, {
|
||||
containerBehavior: {
|
||||
@@ -1615,29 +1697,39 @@ export const actionChangeContainerBehavior = register({
|
||||
}
|
||||
} else {
|
||||
// case 2: any eligible containers directly selected
|
||||
targetContainers = selected.filter(
|
||||
(el) =>
|
||||
isFlowchartNodeElement(el) && getBoundTextElement(el, elementsMap),
|
||||
);
|
||||
targetContainers = selected.filter((el) => isFlowchartNodeElement(el));
|
||||
}
|
||||
|
||||
if (targetContainers.length === 0) {
|
||||
if (
|
||||
targetContainers.length === 0 &&
|
||||
!hasContainerBehavior(appState.activeTool.type)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value =
|
||||
reduceToCommonValue(
|
||||
targetContainers,
|
||||
(el) => el.containerBehavior?.textFlow ?? "growing",
|
||||
) ??
|
||||
// mixed selection -> show null so nothing appears selected
|
||||
null;
|
||||
const textFlow =
|
||||
targetContainers.length === 0
|
||||
? appState.currentItemContainerBehavior?.textFlow ?? "growing"
|
||||
: reduceToCommonValue(
|
||||
targetContainers,
|
||||
(el) => el.containerBehavior?.textFlow ?? "growing",
|
||||
) ??
|
||||
// mixed selection -> show null so nothing appears selected
|
||||
null;
|
||||
|
||||
const marginValue =
|
||||
targetContainers.length === 0
|
||||
? appState.currentItemContainerBehavior?.margin ?? BOUND_TEXT_PADDING
|
||||
: reduceToCommonValue(
|
||||
targetContainers,
|
||||
(el) => el.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
|
||||
) ??
|
||||
// mixed selection -> show null so nothing appears selected
|
||||
null;
|
||||
|
||||
return (
|
||||
<fieldset>
|
||||
{appState.stylesPanelMode === "full" && (
|
||||
<legend>{t("labels.container")}</legend>
|
||||
)}
|
||||
<legend>{t("labels.container")}</legend>
|
||||
<div className="buttonList">
|
||||
<RadioSelection
|
||||
group="container"
|
||||
@@ -1654,12 +1746,42 @@ export const actionChangeContainerBehavior = register({
|
||||
},
|
||||
]}
|
||||
value={
|
||||
value ??
|
||||
textFlow ??
|
||||
(targetContainers.length
|
||||
? null
|
||||
: appState.currentItemContainerBehavior?.textFlow ?? "growing")
|
||||
}
|
||||
onChange={(val) => updateData(val)}
|
||||
onChange={(val) => updateData({ textFlow: val })}
|
||||
/>
|
||||
</div>
|
||||
<div className="buttonList">
|
||||
<RadioSelection
|
||||
group="container"
|
||||
options={[
|
||||
{
|
||||
value: "small",
|
||||
text: t("labels.container_margin_small"),
|
||||
icon: marginSmallIcon,
|
||||
},
|
||||
{
|
||||
value: "medium",
|
||||
text: t("labels.container_margin_medium"),
|
||||
icon: marginMediumIcon,
|
||||
},
|
||||
{
|
||||
value: "large",
|
||||
text: t("labels.container_margin_large"),
|
||||
icon: marginLargeIcon,
|
||||
},
|
||||
]}
|
||||
value={getMarginValue(
|
||||
marginValue ??
|
||||
(targetContainers.length
|
||||
? null
|
||||
: appState.currentItemContainerBehavior?.margin ??
|
||||
BOUND_TEXT_PADDING),
|
||||
)}
|
||||
onChange={(val) => updateData({ margin: val })}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@@ -223,11 +223,10 @@ export const SelectedShapeActions = ({
|
||||
<>{renderAction("changeRoundness")}</>
|
||||
)}
|
||||
|
||||
{hasContainerBehavior(appState.activeTool.type) ||
|
||||
(targetElements.some(
|
||||
(element) =>
|
||||
isFlowchartNodeElement(element) && hasBoundTextElement(element),
|
||||
) && <>{renderAction("changeContainerBehavior")}</>)}
|
||||
{(hasContainerBehavior(appState.activeTool.type) ||
|
||||
targetElements.some((element) => isFlowchartNodeElement(element))) && (
|
||||
<>{renderAction("changeContainerBehavior")}</>
|
||||
)}
|
||||
|
||||
{(toolIsArrow(appState.activeTool.type) ||
|
||||
targetElements.some((element) => toolIsArrow(element.type))) && (
|
||||
@@ -419,12 +418,10 @@ const CombinedShapeProperties = ({
|
||||
canChangeRoundness(element.type),
|
||||
)) &&
|
||||
renderAction("changeRoundness")}
|
||||
{hasContainerBehavior(appState.activeTool.type) ||
|
||||
(targetElements.some(
|
||||
(element) =>
|
||||
isFlowchartNodeElement(element) &&
|
||||
hasBoundTextElement(element),
|
||||
) && <>{renderAction("changeContainerBehavior")}</>)}
|
||||
{(hasContainerBehavior(appState.activeTool.type) ||
|
||||
targetElements.some((element) =>
|
||||
isFlowchartNodeElement(element),
|
||||
)) && <>{renderAction("changeContainerBehavior")}</>}
|
||||
{renderAction("changeOpacity")}
|
||||
</div>
|
||||
</PropertiesPopover>
|
||||
@@ -832,24 +829,6 @@ 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)) ||
|
||||
targetElements.some(
|
||||
(element) =>
|
||||
hasBackground(element.type) && !isTransparent(element.backgroundColor),
|
||||
);
|
||||
|
||||
const showLinkIcon = targetElements.length === 1;
|
||||
|
||||
const showLineEditorAction =
|
||||
!appState.selectedLinearElement?.isEditing &&
|
||||
targetElements.length === 1 &&
|
||||
|
||||
@@ -104,6 +104,7 @@ import {
|
||||
MQ_MAX_TABLET,
|
||||
MQ_MAX_HEIGHT_LANDSCAPE,
|
||||
MQ_MAX_WIDTH_LANDSCAPE,
|
||||
BOUND_TEXT_PADDING,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
@@ -5413,8 +5414,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const minWidth = getApproxMinLineWidth(
|
||||
getFontString(fontString),
|
||||
lineHeight,
|
||||
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
|
||||
);
|
||||
const minHeight = getApproxMinLineHeight(
|
||||
fontSize,
|
||||
lineHeight,
|
||||
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
|
||||
);
|
||||
const minHeight = getApproxMinLineHeight(fontSize, lineHeight);
|
||||
const newHeight = Math.max(container.height, minHeight);
|
||||
const newWidth = Math.max(container.width, minWidth);
|
||||
this.scene.mutateElement(container, {
|
||||
|
||||
@@ -2328,19 +2328,82 @@ export const strokeIcon = createIcon(
|
||||
);
|
||||
|
||||
export const stickyNoteIcon = createIcon(
|
||||
<g
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<g>
|
||||
<path d="M16 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8Z" />
|
||||
<path d="M15 3v4a2 2 0 0 0 2 2h4" />
|
||||
</g>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const marginSmallIcon = createIcon(
|
||||
<g fill="none">
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<rect
|
||||
x="2"
|
||||
y="2"
|
||||
width="20"
|
||||
height="20"
|
||||
stroke="currentColor"
|
||||
stroke-width="1"
|
||||
stroke-dasharray="1 1"
|
||||
/>
|
||||
</g>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const marginMediumIcon = createIcon(
|
||||
<g fill="none">
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<rect
|
||||
x="4"
|
||||
y="4"
|
||||
width="16"
|
||||
height="16"
|
||||
stroke="currentColor"
|
||||
stroke-width="1"
|
||||
stroke-dasharray="1 1"
|
||||
/>
|
||||
</g>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const marginLargeIcon = createIcon(
|
||||
<g fill="none">
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<rect
|
||||
x="6"
|
||||
y="6"
|
||||
width="12"
|
||||
height="12"
|
||||
stroke="currentColor"
|
||||
stroke-width="1"
|
||||
stroke-dasharray="1 1"
|
||||
/>
|
||||
</g>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const growingContainerIcon = createIcon(
|
||||
<g
|
||||
stroke="currentColor"
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
"container": "Container",
|
||||
"container_fixed": "Sticky note",
|
||||
"container_growing": "Fit to text",
|
||||
"container_margin_small": "Small margin",
|
||||
"container_margin_medium": "Medium margin",
|
||||
"container_margin_large": "Large margin",
|
||||
"opacity": "Opacity",
|
||||
"textAlign": "Text align",
|
||||
"edges": "Edges",
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getFontFamilyString,
|
||||
isTestEnv,
|
||||
MIME_TYPES,
|
||||
BOUND_TEXT_PADDING,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
@@ -248,7 +249,11 @@ export const textWysiwyg = ({
|
||||
// autogrow container height if text exceeds
|
||||
if (!isArrowElement(container) && height > maxHeight) {
|
||||
const targetContainerHeight =
|
||||
computeContainerDimensionForBoundText(height, container.type);
|
||||
computeContainerDimensionForBoundText(
|
||||
height,
|
||||
container.type,
|
||||
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
|
||||
);
|
||||
|
||||
app.scene.mutateElement(container, {
|
||||
height: targetContainerHeight,
|
||||
@@ -262,7 +267,11 @@ export const textWysiwyg = ({
|
||||
height < maxHeight
|
||||
) {
|
||||
const targetContainerHeight =
|
||||
computeContainerDimensionForBoundText(height, container.type);
|
||||
computeContainerDimensionForBoundText(
|
||||
height,
|
||||
container.type,
|
||||
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
|
||||
);
|
||||
app.scene.mutateElement(container, {
|
||||
height: targetContainerHeight,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user