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