feat(editor): support frame backgrounds
This commit is contained in:
@@ -78,7 +78,12 @@ import type {
|
||||
} from "./types";
|
||||
|
||||
export const shouldTestInside = (element: ExcalidrawElement) => {
|
||||
if (element.type === "arrow") {
|
||||
if (
|
||||
element.type === "arrow" ||
|
||||
// frame elements should ignore inside hit test even if background is not
|
||||
// transparent, so we can select children easily
|
||||
isFrameLikeElement(element)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
isRTL,
|
||||
getVerticalOffset,
|
||||
invariant,
|
||||
isTransparent,
|
||||
applyDarkModeFilter,
|
||||
isSafari,
|
||||
} from "@excalidraw/common";
|
||||
@@ -78,6 +79,7 @@ import type {
|
||||
ExcalidrawFrameLikeElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
ElementsMap,
|
||||
ExcalidrawFrameElement,
|
||||
} from "./types";
|
||||
|
||||
import type { RoughCanvas } from "roughjs/bin/canvas";
|
||||
@@ -777,6 +779,45 @@ export const renderSelectionElement = (
|
||||
context.restore();
|
||||
};
|
||||
|
||||
export const renderFrameBackground = (
|
||||
frame: ExcalidrawFrameElement,
|
||||
context: CanvasRenderingContext2D,
|
||||
appState: StaticCanvasAppState | InteractiveCanvasAppState,
|
||||
opts?: {
|
||||
roundCorners?: boolean;
|
||||
},
|
||||
) => {
|
||||
if (isTransparent(frame.backgroundColor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.save();
|
||||
context.translate(frame.x + appState.scrollX, frame.y + appState.scrollY);
|
||||
context.fillStyle =
|
||||
appState.theme === THEME.DARK
|
||||
? applyDarkModeFilter(frame.backgroundColor)
|
||||
: frame.backgroundColor;
|
||||
|
||||
const shouldRoundCorners = opts?.roundCorners ?? true;
|
||||
|
||||
if (shouldRoundCorners && FRAME_STYLE.radius && context.roundRect) {
|
||||
context.beginPath();
|
||||
context.roundRect(
|
||||
0,
|
||||
0,
|
||||
frame.width,
|
||||
frame.height,
|
||||
FRAME_STYLE.radius / appState.zoom.value,
|
||||
);
|
||||
context.fill();
|
||||
context.closePath();
|
||||
} else {
|
||||
context.fillRect(0, 0, frame.width, frame.height);
|
||||
}
|
||||
|
||||
context.restore();
|
||||
};
|
||||
|
||||
export const renderElement = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
elementsMap: RenderableElementsMap,
|
||||
@@ -808,7 +849,6 @@ export const renderElement = (
|
||||
element.x + appState.scrollX,
|
||||
element.y + appState.scrollY,
|
||||
);
|
||||
context.fillStyle = "rgba(0, 0, 200, 0.04)";
|
||||
|
||||
context.lineWidth = FRAME_STYLE.strokeWidth / appState.zoom.value;
|
||||
context.strokeStyle =
|
||||
|
||||
@@ -38,6 +38,53 @@ describe("check rotated elements can be hit:", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("frame hit testing", () => {
|
||||
it.each(["transparent", "#ffffff"])(
|
||||
"does not hit frame inside regardless of background color (%s)",
|
||||
(backgroundColor) => {
|
||||
const element = API.createElement({
|
||||
type: "frame",
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
height: 100,
|
||||
backgroundColor,
|
||||
});
|
||||
const elementsMap = arrayToMap([element]);
|
||||
|
||||
expect(
|
||||
hitElementItself({
|
||||
point: pointFrom<GlobalPoint>(50, 50),
|
||||
element,
|
||||
threshold: 10,
|
||||
elementsMap,
|
||||
}),
|
||||
).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it("hits frame outline", () => {
|
||||
const element = API.createElement({
|
||||
type: "frame",
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
height: 100,
|
||||
backgroundColor: "#ffffff",
|
||||
});
|
||||
const elementsMap = arrayToMap([element]);
|
||||
|
||||
expect(
|
||||
hitElementItself({
|
||||
point: pointFrom<GlobalPoint>(0, 50),
|
||||
element,
|
||||
threshold: 1,
|
||||
elementsMap,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hitElementItself cache", () => {
|
||||
beforeEach(async () => {
|
||||
// reset cache
|
||||
|
||||
@@ -109,6 +109,21 @@ describe("element locking", () => {
|
||||
expect(crossHatchButton).toBe(null);
|
||||
});
|
||||
|
||||
it("should show background color picker for selected frame", () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
});
|
||||
API.setElements([frame]);
|
||||
API.setSelectedElements([frame]);
|
||||
|
||||
expect(
|
||||
queryByTestId(
|
||||
document.body,
|
||||
`color-top-pick-${DEFAULT_ELEMENT_BACKGROUND_PICKS[0]}`,
|
||||
),
|
||||
).not.toBe(null);
|
||||
});
|
||||
|
||||
it("should highlight common stroke width of selected elements", () => {
|
||||
const rect1 = API.createElement({
|
||||
type: "rectangle",
|
||||
|
||||
@@ -444,7 +444,12 @@ export const actionChangeBackgroundColor = register<
|
||||
(element) => element.backgroundColor,
|
||||
true,
|
||||
(hasSelection) =>
|
||||
!hasSelection ? appState.currentItemBackgroundColor : null,
|
||||
!hasSelection
|
||||
? appState.activeTool.type === "frame"
|
||||
? // background default shouldn't apply to new frames
|
||||
"transparent"
|
||||
: appState.currentItemBackgroundColor
|
||||
: null,
|
||||
)}
|
||||
onChange={(color) =>
|
||||
updateData({ currentItemBackgroundColor: color })
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
isArrowElement,
|
||||
hasStrokeColor,
|
||||
toolIsArrow,
|
||||
isFrameElement,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import type {
|
||||
@@ -129,8 +130,11 @@ export const canChangeBackgroundColor = (
|
||||
targetElements: ExcalidrawElement[],
|
||||
) => {
|
||||
return (
|
||||
// frame tool shouldn't allow to set background until frame is created
|
||||
hasBackground(appState.activeTool.type) ||
|
||||
targetElements.some((element) => hasBackground(element.type))
|
||||
targetElements.some(
|
||||
(element) => hasBackground(element.type) || isFrameElement(element),
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -256,6 +256,7 @@ import {
|
||||
handleFocusPointPointerUp,
|
||||
maybeHandleArrowPointlikeDrag,
|
||||
getUncroppedWidthAndHeight,
|
||||
isFrameElement,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
|
||||
@@ -5037,6 +5038,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (
|
||||
event.key === KEYS.G &&
|
||||
(hasBackground(this.state.activeTool.type) ||
|
||||
this.state.activeTool.type === "frame" ||
|
||||
selectedElements.some((element) => isFrameElement(element)) ||
|
||||
selectedElements.some((element) => hasBackground(element.type)))
|
||||
) {
|
||||
this.setState({ openPopup: "elementBackground" });
|
||||
|
||||
@@ -4,7 +4,9 @@ import {
|
||||
getTargetFrame,
|
||||
isInvisiblySmallElement,
|
||||
renderElement,
|
||||
renderFrameBackground,
|
||||
shouldApplyFrameClip,
|
||||
isFrameElement,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import { bootstrapCanvas, getNormalizedCanvasDimensions } from "./helpers";
|
||||
@@ -67,6 +69,14 @@ const _renderNewElementScene = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
isFrameElement(newElement) &&
|
||||
appState.frameRendering.enabled &&
|
||||
appState.frameRendering.outline
|
||||
) {
|
||||
renderFrameBackground(newElement, context, appState);
|
||||
}
|
||||
|
||||
renderElement(
|
||||
newElement,
|
||||
elementsMap,
|
||||
|
||||
@@ -4,21 +4,23 @@ import {
|
||||
THEME,
|
||||
throttleRAF,
|
||||
} from "@excalidraw/common";
|
||||
import { isElementLink } from "@excalidraw/element";
|
||||
import { isElementLink, isFrameElement } from "@excalidraw/element";
|
||||
import { createPlaceholderEmbeddableLabel } from "@excalidraw/element";
|
||||
import { getBoundTextElement } from "@excalidraw/element";
|
||||
import {
|
||||
isEmbeddableElement,
|
||||
isFrameLikeElement,
|
||||
isIframeLikeElement,
|
||||
isTextElement,
|
||||
} from "@excalidraw/element";
|
||||
import {
|
||||
elementOverlapsWithFrame,
|
||||
getContainingFrame,
|
||||
getTargetFrame,
|
||||
shouldApplyFrameClip,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import { renderElement } from "@excalidraw/element";
|
||||
import { renderElement, renderFrameBackground } from "@excalidraw/element";
|
||||
|
||||
import { getElementAbsoluteCoords } from "@excalidraw/element";
|
||||
|
||||
@@ -276,6 +278,39 @@ const _renderStaticScene = ({
|
||||
}
|
||||
|
||||
const groupsToBeAddedToFrame = new Set<string>();
|
||||
const renderedFrameBackgrounds = new Set<string>();
|
||||
|
||||
const maybeRenderFrameBackground = (
|
||||
element: NonDeletedExcalidrawElement | ExcalidrawFrameLikeElement,
|
||||
) => {
|
||||
if (
|
||||
!appState.frameRendering.enabled ||
|
||||
(!appState.frameRendering.outline && !renderConfig.exportingFrame)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const frame =
|
||||
renderConfig.exportingFrame ||
|
||||
(isFrameLikeElement(element)
|
||||
? element
|
||||
: getContainingFrame(element, elementsMap));
|
||||
|
||||
if (!isFrameElement(frame)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!frame || renderedFrameBackgrounds.has(frame.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderFrameBackground(frame, context, appState, {
|
||||
roundCorners:
|
||||
!renderConfig.exportingFrame ||
|
||||
frame.id !== renderConfig.exportingFrame.id,
|
||||
});
|
||||
renderedFrameBackgrounds.add(frame.id);
|
||||
};
|
||||
|
||||
visibleElements.forEach((element) => {
|
||||
if (
|
||||
@@ -297,11 +332,19 @@ const _renderStaticScene = ({
|
||||
|
||||
const inFrameGroupsMap = new Map<string, boolean>();
|
||||
|
||||
if (renderConfig.exportingFrame) {
|
||||
maybeRenderFrameBackground(renderConfig.exportingFrame);
|
||||
}
|
||||
|
||||
// Paint visible elements
|
||||
visibleElements
|
||||
.filter((el) => !isIframeLikeElement(el))
|
||||
.forEach((element) => {
|
||||
try {
|
||||
// TODO: optimize (currently we call this func for each element because
|
||||
// children come before their frames and we neeed to render the frame
|
||||
// background at the bottom)
|
||||
maybeRenderFrameBackground(element);
|
||||
const frameId = element.frameId || appState.frameToHighlight?.id;
|
||||
|
||||
if (
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
isRTL,
|
||||
isTestEnv,
|
||||
getVerticalOffset,
|
||||
isTransparent,
|
||||
applyDarkModeFilter,
|
||||
MIME_TYPES,
|
||||
} from "@excalidraw/common";
|
||||
@@ -23,6 +24,7 @@ import { getBoundTextElement, getContainerElement } from "@excalidraw/element";
|
||||
import { getLineHeightInPx } from "@excalidraw/element";
|
||||
import {
|
||||
isArrowElement,
|
||||
isFrameLikeElement,
|
||||
isIframeLikeElement,
|
||||
isInitializedImageElement,
|
||||
isTextElement,
|
||||
@@ -38,6 +40,7 @@ import { getElementAbsoluteCoords } from "@excalidraw/element";
|
||||
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawFrameLikeElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "@excalidraw/element/types";
|
||||
@@ -717,11 +720,76 @@ export const renderSceneToSvg = (
|
||||
return;
|
||||
}
|
||||
|
||||
const renderedFrameBackgrounds = new Set<string>();
|
||||
const maybeRenderFrameBackground = (
|
||||
element: NonDeletedExcalidrawElement | ExcalidrawFrameLikeElement,
|
||||
) => {
|
||||
if (
|
||||
!renderConfig.frameRendering.enabled ||
|
||||
(!renderConfig.frameRendering.outline && !renderConfig.exportingFrame)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const frame =
|
||||
renderConfig.exportingFrame ||
|
||||
(isFrameLikeElement(element)
|
||||
? element
|
||||
: getContainingFrame(element, elementsMap));
|
||||
|
||||
if (
|
||||
!frame ||
|
||||
renderedFrameBackgrounds.has(frame.id) ||
|
||||
!frame.backgroundColor ||
|
||||
isTransparent(frame.backgroundColor)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(frame, elementsMap);
|
||||
const cx = (x2 - x1) / 2 - (frame.x - x1);
|
||||
const cy = (y2 - y1) / 2 - (frame.y - y1);
|
||||
const degree = (180 * frame.angle) / Math.PI;
|
||||
|
||||
const rect = svgRoot.ownerDocument.createElementNS(SVG_NS, "rect");
|
||||
rect.setAttribute(
|
||||
"transform",
|
||||
`translate(${frame.x + renderConfig.offsetX} ${
|
||||
frame.y + renderConfig.offsetY
|
||||
}) rotate(${degree} ${cx} ${cy})`,
|
||||
);
|
||||
rect.setAttribute("width", `${frame.width}px`);
|
||||
rect.setAttribute("height", `${frame.height}px`);
|
||||
|
||||
const isDirectlyExportedFrame =
|
||||
!!renderConfig.exportingFrame &&
|
||||
frame.id === renderConfig.exportingFrame.id;
|
||||
if (!isDirectlyExportedFrame) {
|
||||
rect.setAttribute("rx", FRAME_STYLE.radius.toString());
|
||||
rect.setAttribute("ry", FRAME_STYLE.radius.toString());
|
||||
}
|
||||
rect.setAttribute(
|
||||
"fill",
|
||||
renderConfig.theme === THEME.DARK
|
||||
? applyDarkModeFilter(frame.backgroundColor)
|
||||
: frame.backgroundColor,
|
||||
);
|
||||
rect.setAttribute("stroke", "none");
|
||||
|
||||
svgRoot.appendChild(rect);
|
||||
renderedFrameBackgrounds.add(frame.id);
|
||||
};
|
||||
|
||||
if (renderConfig.exportingFrame) {
|
||||
maybeRenderFrameBackground(renderConfig.exportingFrame);
|
||||
}
|
||||
|
||||
// render elements
|
||||
elements
|
||||
.filter((el) => !isIframeLikeElement(el))
|
||||
.forEach((element) => {
|
||||
if (!element.isDeleted) {
|
||||
maybeRenderFrameBackground(element);
|
||||
if (
|
||||
isTextElement(element) &&
|
||||
element.containerId &&
|
||||
|
||||
@@ -169,6 +169,20 @@ const prepareElementsForRender = ({
|
||||
return nextElements;
|
||||
};
|
||||
|
||||
const addExportingFrameToElements = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
exportingFrame?: ExcalidrawFrameLikeElement | null,
|
||||
) => {
|
||||
if (
|
||||
!exportingFrame ||
|
||||
elements.some((element) => element.id === exportingFrame.id)
|
||||
) {
|
||||
return elements;
|
||||
}
|
||||
|
||||
return [...elements, exportingFrame];
|
||||
};
|
||||
|
||||
export const exportToCanvas = async (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
@@ -216,6 +230,14 @@ export const exportToCanvas = async (
|
||||
exportWithDarkMode: appState.exportWithDarkMode,
|
||||
frameRendering,
|
||||
});
|
||||
const elementsForLookup = addExportingFrameToElements(
|
||||
elementsForRender,
|
||||
exportingFrame,
|
||||
);
|
||||
const allElementsForLookup = addExportingFrameToElements(
|
||||
elements,
|
||||
exportingFrame,
|
||||
);
|
||||
|
||||
if (exportingFrame) {
|
||||
exportPadding = 0;
|
||||
@@ -242,10 +264,10 @@ export const exportToCanvas = async (
|
||||
canvas,
|
||||
rc: rough.canvas(canvas),
|
||||
elementsMap: toBrandedType<RenderableElementsMap>(
|
||||
arrayToMap(elementsForRender),
|
||||
arrayToMap(elementsForLookup),
|
||||
),
|
||||
allElementsMap: toBrandedType<NonDeletedSceneElementsMap>(
|
||||
arrayToMap(syncInvalidIndices(elements)),
|
||||
arrayToMap(syncInvalidIndices(allElementsForLookup)),
|
||||
),
|
||||
visibleElements: elementsForRender,
|
||||
scale,
|
||||
@@ -261,6 +283,7 @@ export const exportToCanvas = async (
|
||||
},
|
||||
renderConfig: {
|
||||
canvasBackgroundColor: viewBackgroundColor,
|
||||
exportingFrame,
|
||||
imageCache,
|
||||
renderGrid: false,
|
||||
isExporting: true,
|
||||
@@ -325,6 +348,10 @@ export const exportToSvg = async (
|
||||
exportWithDarkMode,
|
||||
frameRendering,
|
||||
});
|
||||
const elementsForLookup = addExportingFrameToElements(
|
||||
elementsForRender,
|
||||
exportingFrame,
|
||||
);
|
||||
|
||||
if (exportingFrame) {
|
||||
exportPadding = 0;
|
||||
@@ -386,10 +413,10 @@ export const exportToSvg = async (
|
||||
// frame clip paths
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const frameElements = getFrameLikeElements(elements);
|
||||
const frameElements = getFrameLikeElements(elementsForLookup);
|
||||
|
||||
if (frameElements.length) {
|
||||
const elementsMap = arrayToMap(elements);
|
||||
const elementsMap = arrayToMap(elementsForLookup);
|
||||
|
||||
for (const frame of frameElements) {
|
||||
const clipPath = svgRoot.ownerDocument.createElementNS(
|
||||
@@ -472,7 +499,7 @@ export const exportToSvg = async (
|
||||
|
||||
renderSceneToSvg(
|
||||
elementsForRender,
|
||||
toBrandedType<RenderableElementsMap>(arrayToMap(elementsForRender)),
|
||||
toBrandedType<RenderableElementsMap>(arrayToMap(elementsForLookup)),
|
||||
rsvg,
|
||||
svgRoot,
|
||||
files || {},
|
||||
@@ -483,6 +510,7 @@ export const exportToSvg = async (
|
||||
exportWithDarkMode,
|
||||
renderEmbeddables,
|
||||
frameRendering,
|
||||
exportingFrame,
|
||||
canvasBackgroundColor: viewBackgroundColor,
|
||||
embedsValidationStatus: renderEmbeddables
|
||||
? new Map(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { UserIdleState, EditorInterface } from "@excalidraw/common";
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawFrameLikeElement,
|
||||
NonDeletedElementsMap,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
@@ -26,6 +27,7 @@ export type RenderableElementsMap = NonDeletedElementsMap &
|
||||
|
||||
export type StaticCanvasRenderConfig = {
|
||||
canvasBackgroundColor: AppState["viewBackgroundColor"];
|
||||
exportingFrame?: ExcalidrawFrameLikeElement | null;
|
||||
// extra options passed to the renderer
|
||||
// ---------------------------------------------------------------------------
|
||||
imageCache: AppClassProperties["imageCache"];
|
||||
@@ -46,6 +48,7 @@ export type SVGRenderConfig = {
|
||||
exportWithDarkMode: boolean;
|
||||
renderEmbeddables: boolean;
|
||||
frameRendering: AppState["frameRendering"];
|
||||
exportingFrame?: ExcalidrawFrameLikeElement | null;
|
||||
canvasBackgroundColor: AppState["viewBackgroundColor"];
|
||||
embedsValidationStatus: EmbedsValidationStatus;
|
||||
/**
|
||||
|
||||
@@ -279,6 +279,7 @@ describe("exporting frames", () => {
|
||||
height: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
backgroundColor: "#ffc9c9",
|
||||
});
|
||||
const frameChild = API.createElement({
|
||||
type: "rectangle",
|
||||
@@ -311,11 +312,58 @@ describe("exporting frames", () => {
|
||||
expect(
|
||||
svg.querySelector(`[data-id="${rectOverlapping.id}"]`),
|
||||
).not.toBeNull();
|
||||
// exporting frame background should still be rendered
|
||||
const frameBackgroundNode = svg.querySelector('rect[fill="#ffc9c9"]');
|
||||
expect(frameBackgroundNode).not.toBeNull();
|
||||
expect(frameBackgroundNode).not.toHaveAttribute("rx");
|
||||
expect(frameBackgroundNode).not.toHaveAttribute("ry");
|
||||
expect(
|
||||
frameBackgroundNode!.compareDocumentPosition(
|
||||
svg.querySelector(`[data-id="${frameChild.id}"]`)!,
|
||||
) & Node.DOCUMENT_POSITION_FOLLOWING,
|
||||
).toBeTruthy();
|
||||
|
||||
expect(svg.getAttribute("width")).toBe(frame.width.toString());
|
||||
expect(svg.getAttribute("height")).toBe(frame.height.toString());
|
||||
});
|
||||
|
||||
it("should not render SVG frame background for empty/transparent exporting frame bg", async () => {
|
||||
const transparentFrame = API.createElement({
|
||||
type: "frame",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
backgroundColor: "transparent",
|
||||
});
|
||||
const emptyBgFrame = API.createElement({
|
||||
type: "frame",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
backgroundColor: "",
|
||||
});
|
||||
|
||||
const transparentSvg = await exportToSvg({
|
||||
elements: [transparentFrame],
|
||||
files: null,
|
||||
exportPadding: 0,
|
||||
exportingFrame: transparentFrame,
|
||||
});
|
||||
const emptyBgSvg = await exportToSvg({
|
||||
elements: [emptyBgFrame],
|
||||
files: null,
|
||||
exportPadding: 0,
|
||||
exportingFrame: emptyBgFrame,
|
||||
});
|
||||
|
||||
expect(
|
||||
transparentSvg.querySelector('rect[fill="transparent"]'),
|
||||
).toBeNull();
|
||||
expect(emptyBgSvg.querySelector('rect[fill=""]')).toBeNull();
|
||||
});
|
||||
|
||||
it("should filter non-overlapping elements when exporting a frame", async () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
@@ -469,6 +517,66 @@ describe("exporting frames", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should render frame with its background color", async () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
backgroundColor: "#ffc9c9",
|
||||
});
|
||||
const frameChild = API.createElement({
|
||||
type: "rectangle",
|
||||
width: 50,
|
||||
height: 50,
|
||||
x: 10,
|
||||
y: 10,
|
||||
frameId: frame.id,
|
||||
});
|
||||
|
||||
const { exportedElements, exportingFrame } = prepareElementsForExport(
|
||||
[frameChild, frame],
|
||||
{
|
||||
selectedElementIds: {},
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
const svg = await exportToSvg({
|
||||
elements: exportedElements,
|
||||
files: null,
|
||||
exportPadding: 0,
|
||||
exportingFrame,
|
||||
});
|
||||
|
||||
const frameSvgNode = svg.querySelector(`[data-id="${frame.id}"]`);
|
||||
const childSvgNode = svg.querySelector(`[data-id="${frameChild.id}"]`);
|
||||
const frameBackgroundNode = svg.querySelector('rect[fill="#ffc9c9"]');
|
||||
|
||||
expect(frameSvgNode).not.toBeNull();
|
||||
expect(childSvgNode).not.toBeNull();
|
||||
expect(frameBackgroundNode).not.toBeNull();
|
||||
expect(frameSvgNode).toHaveAttribute("fill", "none");
|
||||
expect(frameBackgroundNode).toHaveAttribute(
|
||||
"rx",
|
||||
FRAME_STYLE.radius.toString(),
|
||||
);
|
||||
expect(frameBackgroundNode).toHaveAttribute(
|
||||
"ry",
|
||||
FRAME_STYLE.radius.toString(),
|
||||
);
|
||||
|
||||
expect(
|
||||
frameBackgroundNode!.compareDocumentPosition(childSvgNode!) &
|
||||
Node.DOCUMENT_POSITION_FOLLOWING,
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
childSvgNode!.compareDocumentPosition(frameSvgNode!) &
|
||||
Node.DOCUMENT_POSITION_FOLLOWING,
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should not export frame-overlapping elements belonging to different frame", async () => {
|
||||
const frame1 = API.createElement({
|
||||
type: "frame",
|
||||
|
||||
Reference in New Issue
Block a user