Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 348912f32f | |||
| ec2de7205f | |||
| d5e3f436dc | |||
| dcf4592e79 | |||
| d1f8eec174 | |||
| 0f81c30276 | |||
| f098789d16 | |||
| f794b0bb90 | |||
| 104f64f1dc | |||
| 71ad3c5356 |
@@ -20,13 +20,3 @@ Frames should be ordered where frame children come first, followed by the frame
|
|||||||
```
|
```
|
||||||
|
|
||||||
If not oredered correctly, the editor will still function, but the elements may not be rendered and clipped correctly. Further, the renderer relies on this ordering for performance optimizations.
|
If not oredered correctly, the editor will still function, but the elements may not be rendered and clipped correctly. Further, the renderer relies on this ordering for performance optimizations.
|
||||||
|
|
||||||
# Arrows
|
|
||||||
|
|
||||||
An arrow can be a child of a frame only if it has no binding (either start or end) to any other element, regardless of whether the bound element is inside the frame or not.
|
|
||||||
|
|
||||||
This ensures that when an arrow is bound to an element outside the frame, it's rendered and behaves correctly.
|
|
||||||
|
|
||||||
Therefore, when an arrow (that's a child of a frame) gets bound to an element, it's automatically removed from the frame.
|
|
||||||
|
|
||||||
Bound-arrow is duplicated alongside a frame only if the arrow start is bound to an element within that frame.
|
|
||||||
|
|||||||
@@ -10,26 +10,34 @@ import { actionDeleteSelected } from "./actionDeleteSelected";
|
|||||||
import { exportCanvas } from "../data/index";
|
import { exportCanvas } from "../data/index";
|
||||||
import { getNonDeletedElements, isTextElement } from "../element";
|
import { getNonDeletedElements, isTextElement } from "../element";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
import { isFirefox } from "../constants";
|
||||||
|
|
||||||
export const actionCopy = register({
|
export const actionCopy = register({
|
||||||
name: "copy",
|
name: "copy",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState, _, app) => {
|
perform: async (elements, appState, _, app) => {
|
||||||
const elementsToCopy = app.scene.getSelectedElements({
|
const elementsToCopy = app.scene.getSelectedElements({
|
||||||
selectedElementIds: appState.selectedElementIds,
|
selectedElementIds: appState.selectedElementIds,
|
||||||
includeBoundTextElement: true,
|
includeBoundTextElement: true,
|
||||||
includeElementsInFrames: true,
|
includeElementsInFrames: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
copyToClipboard(elementsToCopy, app.files);
|
try {
|
||||||
|
await copyToClipboard(elementsToCopy, app.files);
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
commitToHistory: false,
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
errorMessage: error.message,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
predicate: (elements, appState, appProps, app) => {
|
|
||||||
return app.device.isMobile && !!navigator.clipboard;
|
|
||||||
},
|
|
||||||
contextItemLabel: "labels.copy",
|
contextItemLabel: "labels.copy",
|
||||||
// don't supply a shortcut since we handle this conditionally via onCopy event
|
// don't supply a shortcut since we handle this conditionally via onCopy event
|
||||||
keyTest: undefined,
|
keyTest: undefined,
|
||||||
@@ -38,15 +46,91 @@ export const actionCopy = register({
|
|||||||
export const actionPaste = register({
|
export const actionPaste = register({
|
||||||
name: "paste",
|
name: "paste",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements: any, appStates: any, data, app) => {
|
perform: async (elements, appState, data, app) => {
|
||||||
app.pasteFromClipboard(null);
|
const MIME_TYPES: Record<string, string> = {};
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
const clipboardItems = await navigator.clipboard?.read();
|
||||||
|
for (const item of clipboardItems) {
|
||||||
|
for (const type of item.types) {
|
||||||
|
try {
|
||||||
|
const blob = await item.getType(type);
|
||||||
|
MIME_TYPES[type] = await blob.text();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.warn(
|
||||||
|
`Cannot retrieve ${type} from clipboardItem: ${error.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Object.keys(MIME_TYPES).length === 0) {
|
||||||
|
console.warn(
|
||||||
|
"No clipboard data found from clipboard.read(). Falling back to clipboard.readText()",
|
||||||
|
);
|
||||||
|
// throw so we fall back onto clipboard.readText()
|
||||||
|
throw new Error("No clipboard data found");
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
try {
|
||||||
|
MIME_TYPES["text/plain"] = await navigator.clipboard?.readText();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.warn(`Cannot readText() from clipboard: ${error.message}`);
|
||||||
|
if (isFirefox) {
|
||||||
|
return {
|
||||||
|
commitToHistory: false,
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
errorMessage: t("hints.firefox_clipboard_write"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`actionPaste: ${error.message}`);
|
||||||
|
return {
|
||||||
|
commitToHistory: false,
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
errorMessage: error.message,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
console.log("actionPaste (1)", { MIME_TYPES });
|
||||||
|
const event = new ClipboardEvent("paste", {
|
||||||
|
clipboardData: new DataTransfer(),
|
||||||
|
});
|
||||||
|
for (const [type, value] of Object.entries(MIME_TYPES)) {
|
||||||
|
try {
|
||||||
|
event.clipboardData?.setData(type, value);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.warn(
|
||||||
|
`Cannot set ${type} as clipboardData item: ${error.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.clipboardData?.types.forEach((type) => {
|
||||||
|
console.log(
|
||||||
|
`actionPaste (2) event.clipboardData?.getData(${type})`,
|
||||||
|
event.clipboardData?.getData(type),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
app.pasteFromClipboard(event);
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
commitToHistory: false,
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
errorMessage: error.message,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
predicate: (elements, appState, appProps, app) => {
|
|
||||||
return app.device.isMobile && !!navigator.clipboard;
|
|
||||||
},
|
|
||||||
contextItemLabel: "labels.paste",
|
contextItemLabel: "labels.paste",
|
||||||
// don't supply a shortcut since we handle this conditionally via onCopy event
|
// don't supply a shortcut since we handle this conditionally via onCopy event
|
||||||
keyTest: undefined,
|
keyTest: undefined,
|
||||||
|
|||||||
@@ -155,12 +155,7 @@ const duplicateElements = (
|
|||||||
groupId,
|
groupId,
|
||||||
).flatMap((element) =>
|
).flatMap((element) =>
|
||||||
isFrameElement(element)
|
isFrameElement(element)
|
||||||
? [
|
? [...getFrameElements(elements, element.id), element]
|
||||||
...getFrameElements(elements, element.id, {
|
|
||||||
includeBoundArrows: true,
|
|
||||||
}),
|
|
||||||
element,
|
|
||||||
]
|
|
||||||
: [element],
|
: [element],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -186,9 +181,7 @@ const duplicateElements = (
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (isElementAFrame) {
|
if (isElementAFrame) {
|
||||||
const elementsInFrame = getFrameElements(sortedElements, element.id, {
|
const elementsInFrame = getFrameElements(sortedElements, element.id);
|
||||||
includeBoundArrows: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
elementsWithClones.push(
|
elementsWithClones.push(
|
||||||
...markAsProcessed([
|
...markAsProcessed([
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import { Excalidraw } from "../packages/excalidraw/index";
|
||||||
|
import { queryByTestId } from "@testing-library/react";
|
||||||
|
import { render } from "../tests/test-utils";
|
||||||
|
import { UI } from "../tests/helpers/ui";
|
||||||
|
import { API } from "../tests/helpers/api";
|
||||||
|
import { COLOR_PALETTE, DEFAULT_ELEMENT_BACKGROUND_PICKS } from "../colors";
|
||||||
|
import { FONT_FAMILY, STROKE_WIDTH } from "../constants";
|
||||||
|
|
||||||
|
const { h } = window;
|
||||||
|
|
||||||
|
describe("element locking", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await render(<Excalidraw />);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("properties when tool selected", () => {
|
||||||
|
it("should show active background top picks", () => {
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
|
||||||
|
const color = DEFAULT_ELEMENT_BACKGROUND_PICKS[1];
|
||||||
|
|
||||||
|
// just in case we change it in the future
|
||||||
|
expect(color).not.toBe(COLOR_PALETTE.transparent);
|
||||||
|
|
||||||
|
h.setState({
|
||||||
|
currentItemBackgroundColor: color,
|
||||||
|
});
|
||||||
|
const activeColor = queryByTestId(
|
||||||
|
document.body,
|
||||||
|
`color-top-pick-${color}`,
|
||||||
|
);
|
||||||
|
expect(activeColor).toHaveClass("active");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show fill style when background non-transparent", () => {
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
|
||||||
|
const color = DEFAULT_ELEMENT_BACKGROUND_PICKS[1];
|
||||||
|
|
||||||
|
// just in case we change it in the future
|
||||||
|
expect(color).not.toBe(COLOR_PALETTE.transparent);
|
||||||
|
|
||||||
|
h.setState({
|
||||||
|
currentItemBackgroundColor: color,
|
||||||
|
currentItemFillStyle: "hachure",
|
||||||
|
});
|
||||||
|
const hachureFillButton = queryByTestId(document.body, `fill-hachure`);
|
||||||
|
|
||||||
|
expect(hachureFillButton).toHaveClass("active");
|
||||||
|
h.setState({
|
||||||
|
currentItemFillStyle: "solid",
|
||||||
|
});
|
||||||
|
const solidFillStyle = queryByTestId(document.body, `fill-solid`);
|
||||||
|
expect(solidFillStyle).toHaveClass("active");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not show fill style when background transparent", () => {
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
|
||||||
|
h.setState({
|
||||||
|
currentItemBackgroundColor: COLOR_PALETTE.transparent,
|
||||||
|
currentItemFillStyle: "hachure",
|
||||||
|
});
|
||||||
|
const hachureFillButton = queryByTestId(document.body, `fill-hachure`);
|
||||||
|
|
||||||
|
expect(hachureFillButton).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show horizontal text align for text tool", () => {
|
||||||
|
UI.clickTool("text");
|
||||||
|
|
||||||
|
h.setState({
|
||||||
|
currentItemTextAlign: "right",
|
||||||
|
});
|
||||||
|
|
||||||
|
const centerTextAlign = queryByTestId(document.body, `align-right`);
|
||||||
|
expect(centerTextAlign).toBeChecked();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("properties when elements selected", () => {
|
||||||
|
it("should show active styles when single element selected", () => {
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
backgroundColor: "red",
|
||||||
|
fillStyle: "cross-hatch",
|
||||||
|
});
|
||||||
|
h.elements = [rect];
|
||||||
|
API.setSelectedElements([rect]);
|
||||||
|
|
||||||
|
const crossHatchButton = queryByTestId(document.body, `fill-cross-hatch`);
|
||||||
|
expect(crossHatchButton).toHaveClass("active");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not show fill style selected element's background is transparent", () => {
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
backgroundColor: COLOR_PALETTE.transparent,
|
||||||
|
fillStyle: "cross-hatch",
|
||||||
|
});
|
||||||
|
h.elements = [rect];
|
||||||
|
API.setSelectedElements([rect]);
|
||||||
|
|
||||||
|
const crossHatchButton = queryByTestId(document.body, `fill-cross-hatch`);
|
||||||
|
expect(crossHatchButton).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should highlight common stroke width of selected elements", () => {
|
||||||
|
const rect1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
strokeWidth: STROKE_WIDTH.thin,
|
||||||
|
});
|
||||||
|
const rect2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
strokeWidth: STROKE_WIDTH.thin,
|
||||||
|
});
|
||||||
|
h.elements = [rect1, rect2];
|
||||||
|
API.setSelectedElements([rect1, rect2]);
|
||||||
|
|
||||||
|
const thinStrokeWidthButton = queryByTestId(
|
||||||
|
document.body,
|
||||||
|
`strokeWidth-thin`,
|
||||||
|
);
|
||||||
|
expect(thinStrokeWidthButton).toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not highlight any stroke width button if no common style", () => {
|
||||||
|
const rect1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
strokeWidth: STROKE_WIDTH.thin,
|
||||||
|
});
|
||||||
|
const rect2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
strokeWidth: STROKE_WIDTH.bold,
|
||||||
|
});
|
||||||
|
h.elements = [rect1, rect2];
|
||||||
|
API.setSelectedElements([rect1, rect2]);
|
||||||
|
|
||||||
|
expect(queryByTestId(document.body, `strokeWidth-thin`)).not.toBe(null);
|
||||||
|
expect(
|
||||||
|
queryByTestId(document.body, `strokeWidth-thin`),
|
||||||
|
).not.toBeChecked();
|
||||||
|
expect(
|
||||||
|
queryByTestId(document.body, `strokeWidth-bold`),
|
||||||
|
).not.toBeChecked();
|
||||||
|
expect(
|
||||||
|
queryByTestId(document.body, `strokeWidth-extraBold`),
|
||||||
|
).not.toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show properties of different element types when selected", () => {
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
strokeWidth: STROKE_WIDTH.bold,
|
||||||
|
});
|
||||||
|
const text = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
fontFamily: FONT_FAMILY.Cascadia,
|
||||||
|
});
|
||||||
|
h.elements = [rect, text];
|
||||||
|
API.setSelectedElements([rect, text]);
|
||||||
|
|
||||||
|
expect(queryByTestId(document.body, `strokeWidth-bold`)).toBeChecked();
|
||||||
|
expect(queryByTestId(document.body, `font-family-code`)).toBeChecked();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AppState } from "../../src/types";
|
import { AppState, Primitive } from "../../src/types";
|
||||||
import {
|
import {
|
||||||
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
||||||
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
||||||
@@ -51,6 +51,7 @@ import {
|
|||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
FONT_FAMILY,
|
FONT_FAMILY,
|
||||||
ROUNDNESS,
|
ROUNDNESS,
|
||||||
|
STROKE_WIDTH,
|
||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import {
|
import {
|
||||||
@@ -82,7 +83,6 @@ import { getLanguage, t } from "../i18n";
|
|||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { randomInteger } from "../random";
|
import { randomInteger } from "../random";
|
||||||
import {
|
import {
|
||||||
canChangeRoundness,
|
|
||||||
canHaveArrowheads,
|
canHaveArrowheads,
|
||||||
getCommonAttributeOfSelectedElements,
|
getCommonAttributeOfSelectedElements,
|
||||||
getSelectedElements,
|
getSelectedElements,
|
||||||
@@ -118,25 +118,44 @@ export const changeProperty = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFormValue = function <T>(
|
export const getFormValue = function <T extends Primitive>(
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
getAttribute: (element: ExcalidrawElement) => T,
|
getAttribute: (element: ExcalidrawElement) => T,
|
||||||
defaultValue: T,
|
isRelevantElement: true | ((element: ExcalidrawElement) => boolean),
|
||||||
|
defaultValue: T | ((isSomeElementSelected: boolean) => T),
|
||||||
): T {
|
): T {
|
||||||
const editingElement = appState.editingElement;
|
const editingElement = appState.editingElement;
|
||||||
const nonDeletedElements = getNonDeletedElements(elements);
|
const nonDeletedElements = getNonDeletedElements(elements);
|
||||||
return (
|
|
||||||
(editingElement && getAttribute(editingElement)) ??
|
let ret: T | null = null;
|
||||||
(isSomeElementSelected(nonDeletedElements, appState)
|
|
||||||
? getCommonAttributeOfSelectedElements(
|
if (editingElement) {
|
||||||
nonDeletedElements,
|
ret = getAttribute(editingElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ret) {
|
||||||
|
const hasSelection = isSomeElementSelected(nonDeletedElements, appState);
|
||||||
|
|
||||||
|
if (hasSelection) {
|
||||||
|
ret =
|
||||||
|
getCommonAttributeOfSelectedElements(
|
||||||
|
isRelevantElement === true
|
||||||
|
? nonDeletedElements
|
||||||
|
: nonDeletedElements.filter((el) => isRelevantElement(el)),
|
||||||
appState,
|
appState,
|
||||||
getAttribute,
|
getAttribute,
|
||||||
)
|
) ??
|
||||||
: defaultValue) ??
|
(typeof defaultValue === "function"
|
||||||
defaultValue
|
? defaultValue(true)
|
||||||
);
|
: defaultValue);
|
||||||
|
} else {
|
||||||
|
ret =
|
||||||
|
typeof defaultValue === "function" ? defaultValue(false) : defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
const offsetElementAfterFontResize = (
|
const offsetElementAfterFontResize = (
|
||||||
@@ -247,6 +266,7 @@ export const actionChangeStrokeColor = register({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.strokeColor,
|
(element) => element.strokeColor,
|
||||||
|
true,
|
||||||
appState.currentItemStrokeColor,
|
appState.currentItemStrokeColor,
|
||||||
)}
|
)}
|
||||||
onChange={(color) => updateData({ currentItemStrokeColor: color })}
|
onChange={(color) => updateData({ currentItemStrokeColor: color })}
|
||||||
@@ -289,6 +309,7 @@ export const actionChangeBackgroundColor = register({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.backgroundColor,
|
(element) => element.backgroundColor,
|
||||||
|
true,
|
||||||
appState.currentItemBackgroundColor,
|
appState.currentItemBackgroundColor,
|
||||||
)}
|
)}
|
||||||
onChange={(color) => updateData({ currentItemBackgroundColor: color })}
|
onChange={(color) => updateData({ currentItemBackgroundColor: color })}
|
||||||
@@ -338,23 +359,28 @@ export const actionChangeFillStyle = register({
|
|||||||
} (${getShortcutKey("Alt-Click")})`,
|
} (${getShortcutKey("Alt-Click")})`,
|
||||||
icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon,
|
icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon,
|
||||||
active: allElementsZigZag ? true : undefined,
|
active: allElementsZigZag ? true : undefined,
|
||||||
|
testId: `fill-hachure`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "cross-hatch",
|
value: "cross-hatch",
|
||||||
text: t("labels.crossHatch"),
|
text: t("labels.crossHatch"),
|
||||||
icon: FillCrossHatchIcon,
|
icon: FillCrossHatchIcon,
|
||||||
|
testId: `fill-cross-hatch`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "solid",
|
value: "solid",
|
||||||
text: t("labels.solid"),
|
text: t("labels.solid"),
|
||||||
icon: FillSolidIcon,
|
icon: FillSolidIcon,
|
||||||
|
testId: `fill-solid`,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.fillStyle,
|
(element) => element.fillStyle,
|
||||||
appState.currentItemFillStyle,
|
(element) => element.hasOwnProperty("fillStyle"),
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection ? null : appState.currentItemFillStyle,
|
||||||
)}
|
)}
|
||||||
onClick={(value, event) => {
|
onClick={(value, event) => {
|
||||||
const nextValue =
|
const nextValue =
|
||||||
@@ -393,26 +419,31 @@ export const actionChangeStrokeWidth = register({
|
|||||||
group="stroke-width"
|
group="stroke-width"
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
value: 1,
|
value: STROKE_WIDTH.thin,
|
||||||
text: t("labels.thin"),
|
text: t("labels.thin"),
|
||||||
icon: StrokeWidthBaseIcon,
|
icon: StrokeWidthBaseIcon,
|
||||||
|
testId: "strokeWidth-thin",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 2,
|
value: STROKE_WIDTH.bold,
|
||||||
text: t("labels.bold"),
|
text: t("labels.bold"),
|
||||||
icon: StrokeWidthBoldIcon,
|
icon: StrokeWidthBoldIcon,
|
||||||
|
testId: "strokeWidth-bold",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 4,
|
value: STROKE_WIDTH.extraBold,
|
||||||
text: t("labels.extraBold"),
|
text: t("labels.extraBold"),
|
||||||
icon: StrokeWidthExtraBoldIcon,
|
icon: StrokeWidthExtraBoldIcon,
|
||||||
|
testId: "strokeWidth-extraBold",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.strokeWidth,
|
(element) => element.strokeWidth,
|
||||||
appState.currentItemStrokeWidth,
|
(element) => element.hasOwnProperty("strokeWidth"),
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection ? null : appState.currentItemStrokeWidth,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -461,7 +492,9 @@ export const actionChangeSloppiness = register({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.roughness,
|
(element) => element.roughness,
|
||||||
appState.currentItemRoughness,
|
(element) => element.hasOwnProperty("roughness"),
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection ? null : appState.currentItemRoughness,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -509,7 +542,9 @@ export const actionChangeStrokeStyle = register({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.strokeStyle,
|
(element) => element.strokeStyle,
|
||||||
appState.currentItemStrokeStyle,
|
(element) => element.hasOwnProperty("strokeStyle"),
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection ? null : appState.currentItemStrokeStyle,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -549,6 +584,7 @@ export const actionChangeOpacity = register({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.opacity,
|
(element) => element.opacity,
|
||||||
|
true,
|
||||||
appState.currentItemOpacity,
|
appState.currentItemOpacity,
|
||||||
) ?? undefined
|
) ?? undefined
|
||||||
}
|
}
|
||||||
@@ -607,7 +643,12 @@ export const actionChangeFontSize = register({
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
appState.currentItemFontSize || DEFAULT_FONT_SIZE,
|
(element) =>
|
||||||
|
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection
|
||||||
|
? null
|
||||||
|
: appState.currentItemFontSize || DEFAULT_FONT_SIZE,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -692,21 +733,25 @@ export const actionChangeFontFamily = register({
|
|||||||
value: FontFamilyValues;
|
value: FontFamilyValues;
|
||||||
text: string;
|
text: string;
|
||||||
icon: JSX.Element;
|
icon: JSX.Element;
|
||||||
|
testId: string;
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
value: FONT_FAMILY.Virgil,
|
value: FONT_FAMILY.Virgil,
|
||||||
text: t("labels.handDrawn"),
|
text: t("labels.handDrawn"),
|
||||||
icon: FreedrawIcon,
|
icon: FreedrawIcon,
|
||||||
|
testId: "font-family-virgil",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: FONT_FAMILY.Helvetica,
|
value: FONT_FAMILY.Helvetica,
|
||||||
text: t("labels.normal"),
|
text: t("labels.normal"),
|
||||||
icon: FontFamilyNormalIcon,
|
icon: FontFamilyNormalIcon,
|
||||||
|
testId: "font-family-normal",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: FONT_FAMILY.Cascadia,
|
value: FONT_FAMILY.Cascadia,
|
||||||
text: t("labels.code"),
|
text: t("labels.code"),
|
||||||
icon: FontFamilyCodeIcon,
|
icon: FontFamilyCodeIcon,
|
||||||
|
testId: "font-family-code",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -729,7 +774,12 @@ export const actionChangeFontFamily = register({
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
appState.currentItemFontFamily || DEFAULT_FONT_FAMILY,
|
(element) =>
|
||||||
|
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection
|
||||||
|
? null
|
||||||
|
: appState.currentItemFontFamily || DEFAULT_FONT_FAMILY,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -806,7 +856,10 @@ export const actionChangeTextAlign = register({
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
appState.currentItemTextAlign,
|
(element) =>
|
||||||
|
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection ? null : appState.currentItemTextAlign,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -882,7 +935,9 @@ export const actionChangeVerticalAlign = register({
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
VERTICAL_ALIGN.MIDDLE,
|
(element) =>
|
||||||
|
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||||
|
(hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE),
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -947,9 +1002,9 @@ export const actionChangeRoundness = register({
|
|||||||
appState,
|
appState,
|
||||||
(element) =>
|
(element) =>
|
||||||
hasLegacyRoundness ? null : element.roundness ? "round" : "sharp",
|
hasLegacyRoundness ? null : element.roundness ? "round" : "sharp",
|
||||||
(canChangeRoundness(appState.activeTool.type) &&
|
(element) => element.hasOwnProperty("roundness"),
|
||||||
appState.currentItemRoundness) ||
|
(hasSelection) =>
|
||||||
null,
|
hasSelection ? null : appState.currentItemRoundness,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -1043,6 +1098,7 @@ export const actionChangeArrowhead = register({
|
|||||||
isLinearElement(element) && canHaveArrowheads(element.type)
|
isLinearElement(element) && canHaveArrowheads(element.type)
|
||||||
? element.startArrowhead
|
? element.startArrowhead
|
||||||
: appState.currentItemStartArrowhead,
|
: appState.currentItemStartArrowhead,
|
||||||
|
true,
|
||||||
appState.currentItemStartArrowhead,
|
appState.currentItemStartArrowhead,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData({ position: "start", type: value })}
|
onChange={(value) => updateData({ position: "start", type: value })}
|
||||||
@@ -1089,6 +1145,7 @@ export const actionChangeArrowhead = register({
|
|||||||
isLinearElement(element) && canHaveArrowheads(element.type)
|
isLinearElement(element) && canHaveArrowheads(element.type)
|
||||||
? element.endArrowhead
|
? element.endArrowhead
|
||||||
: appState.currentItemEndArrowhead,
|
: appState.currentItemEndArrowhead,
|
||||||
|
true,
|
||||||
appState.currentItemEndArrowhead,
|
appState.currentItemEndArrowhead,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData({ position: "end", type: value })}
|
onChange={(value) => updateData({ position: "end", type: value })}
|
||||||
|
|||||||
+4
-7
@@ -118,7 +118,7 @@ export const copyToClipboard = async (
|
|||||||
await copyTextToSystemClipboard(json);
|
await copyTextToSystemClipboard(json);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
PREFER_APP_CLIPBOARD = true;
|
PREFER_APP_CLIPBOARD = true;
|
||||||
console.error(error);
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -193,7 +193,7 @@ const maybeParseHTMLPaste = (event: ClipboardEvent) => {
|
|||||||
* via async clipboard API if supported)
|
* via async clipboard API if supported)
|
||||||
*/
|
*/
|
||||||
const getSystemClipboard = async (
|
const getSystemClipboard = async (
|
||||||
event: ClipboardEvent | null,
|
event: ClipboardEvent,
|
||||||
isPlainPaste = false,
|
isPlainPaste = false,
|
||||||
): Promise<
|
): Promise<
|
||||||
| { type: "text"; value: string }
|
| { type: "text"; value: string }
|
||||||
@@ -205,10 +205,7 @@ const getSystemClipboard = async (
|
|||||||
return { type: "mixedContent", value: mixedContent };
|
return { type: "mixedContent", value: mixedContent };
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = event
|
const text = event.clipboardData?.getData("text/plain");
|
||||||
? event.clipboardData?.getData("text/plain")
|
|
||||||
: probablySupportsClipboardReadText &&
|
|
||||||
(await navigator.clipboard.readText());
|
|
||||||
|
|
||||||
return { type: "text", value: (text || "").trim() };
|
return { type: "text", value: (text || "").trim() };
|
||||||
} catch {
|
} catch {
|
||||||
@@ -220,7 +217,7 @@ const getSystemClipboard = async (
|
|||||||
* Attempts to parse clipboard. Prefers system clipboard.
|
* Attempts to parse clipboard. Prefers system clipboard.
|
||||||
*/
|
*/
|
||||||
export const parseClipboard = async (
|
export const parseClipboard = async (
|
||||||
event: ClipboardEvent | null,
|
event: ClipboardEvent,
|
||||||
isPlainPaste = false,
|
isPlainPaste = false,
|
||||||
): Promise<ClipboardData> => {
|
): Promise<ClipboardData> => {
|
||||||
const systemClipboard = await getSystemClipboard(event, isPlainPaste);
|
const systemClipboard = await getSystemClipboard(event, isPlainPaste);
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
hasBackground,
|
hasBackground,
|
||||||
hasStrokeStyle,
|
hasStrokeStyle,
|
||||||
hasStrokeWidth,
|
hasStrokeWidth,
|
||||||
hasText,
|
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import { SHAPES } from "../shapes";
|
import { SHAPES } from "../shapes";
|
||||||
import { AppClassProperties, UIAppState, Zoom } from "../types";
|
import { AppClassProperties, UIAppState, Zoom } from "../types";
|
||||||
@@ -20,7 +19,7 @@ import Stack from "./Stack";
|
|||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
import { hasStrokeColor } from "../scene/comparisons";
|
import { hasStrokeColor } from "../scene/comparisons";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { hasBoundTextElement } from "../element/typeChecks";
|
import { hasBoundTextElement, isTextElement } from "../element/typeChecks";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { actionToggleZenMode } from "../actions";
|
import { actionToggleZenMode } from "../actions";
|
||||||
import { Tooltip } from "./Tooltip";
|
import { Tooltip } from "./Tooltip";
|
||||||
@@ -66,7 +65,8 @@ export const SelectedShapeActions = ({
|
|||||||
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
||||||
|
|
||||||
const showFillIcons =
|
const showFillIcons =
|
||||||
hasBackground(appState.activeTool.type) ||
|
(hasBackground(appState.activeTool.type) &&
|
||||||
|
!isTransparent(appState.currentItemBackgroundColor)) ||
|
||||||
targetElements.some(
|
targetElements.some(
|
||||||
(element) =>
|
(element) =>
|
||||||
hasBackground(element.type) && !isTransparent(element.backgroundColor),
|
hasBackground(element.type) && !isTransparent(element.backgroundColor),
|
||||||
@@ -123,14 +123,15 @@ export const SelectedShapeActions = ({
|
|||||||
<>{renderAction("changeRoundness")}</>
|
<>{renderAction("changeRoundness")}</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(hasText(appState.activeTool.type) ||
|
{(appState.activeTool.type === "text" ||
|
||||||
targetElements.some((element) => hasText(element.type))) && (
|
targetElements.some(isTextElement)) && (
|
||||||
<>
|
<>
|
||||||
{renderAction("changeFontSize")}
|
{renderAction("changeFontSize")}
|
||||||
|
|
||||||
{renderAction("changeFontFamily")}
|
{renderAction("changeFontFamily")}
|
||||||
|
|
||||||
{suppportsHorizontalAlign(targetElements) &&
|
{(appState.activeTool.type === "text" ||
|
||||||
|
suppportsHorizontalAlign(targetElements)) &&
|
||||||
renderAction("changeTextAlign")}
|
renderAction("changeTextAlign")}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+21
-6
@@ -1275,6 +1275,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
top={this.state.contextMenu.top}
|
top={this.state.contextMenu.top}
|
||||||
left={this.state.contextMenu.left}
|
left={this.state.contextMenu.left}
|
||||||
actionManager={this.actionManager}
|
actionManager={this.actionManager}
|
||||||
|
onClose={(cb) => {
|
||||||
|
this.setState({ contextMenu: null }, () => {
|
||||||
|
this.focusContainer();
|
||||||
|
cb?.();
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<StaticCanvas
|
<StaticCanvas
|
||||||
@@ -2195,14 +2201,21 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public pasteFromClipboard = withBatchedUpdates(
|
public pasteFromClipboard = withBatchedUpdates(
|
||||||
async (event: ClipboardEvent | null) => {
|
async (event: ClipboardEvent) => {
|
||||||
const isPlainPaste = !!(IS_PLAIN_PASTE && event);
|
const isPlainPaste = !!(IS_PLAIN_PASTE && event);
|
||||||
|
|
||||||
|
console.warn(
|
||||||
|
"pasteFromClipboard",
|
||||||
|
event?.clipboardData?.types,
|
||||||
|
event?.clipboardData?.getData("text/plain"),
|
||||||
|
);
|
||||||
|
|
||||||
// #686
|
// #686
|
||||||
const target = document.activeElement;
|
const target = document.activeElement;
|
||||||
const isExcalidrawActive =
|
const isExcalidrawActive =
|
||||||
this.excalidrawContainerRef.current?.contains(target);
|
this.excalidrawContainerRef.current?.contains(target);
|
||||||
if (event && !isExcalidrawActive) {
|
if (event && !isExcalidrawActive) {
|
||||||
|
console.log("exit (1)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2215,6 +2228,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
(!(elementUnderCursor instanceof HTMLCanvasElement) ||
|
(!(elementUnderCursor instanceof HTMLCanvasElement) ||
|
||||||
isWritableElement(target))
|
isWritableElement(target))
|
||||||
) {
|
) {
|
||||||
|
console.log("exit (2)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2554,12 +2568,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
const lineHeight = getDefaultLineHeight(textElementProps.fontFamily);
|
const lineHeight = getDefaultLineHeight(textElementProps.fontFamily);
|
||||||
if (text.length) {
|
if (text.length) {
|
||||||
|
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
|
||||||
|
x,
|
||||||
|
y: currentY,
|
||||||
|
});
|
||||||
|
|
||||||
const element = newTextElement({
|
const element = newTextElement({
|
||||||
...textElementProps,
|
...textElementProps,
|
||||||
x,
|
x,
|
||||||
y: currentY,
|
y: currentY,
|
||||||
text,
|
text,
|
||||||
lineHeight,
|
lineHeight,
|
||||||
|
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||||
});
|
});
|
||||||
acc.push(element);
|
acc.push(element);
|
||||||
currentY += element.height + LINE_GAP;
|
currentY += element.height + LINE_GAP;
|
||||||
@@ -3574,11 +3594,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return getElementsAtPosition(elements, (element) =>
|
return getElementsAtPosition(elements, (element) =>
|
||||||
hitTest(element, this.state, this.frameNameBoundsCache, x, y),
|
hitTest(element, this.state, this.frameNameBoundsCache, x, y),
|
||||||
).filter((element) => {
|
).filter((element) => {
|
||||||
// arrows don't clip even if they're children of frames,
|
|
||||||
// so always allow hitbox regardless of beinging contained in frame
|
|
||||||
if (isArrowElement(element)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// hitting a frame's element from outside the frame is not considered a hit
|
// hitting a frame's element from outside the frame is not considered a hit
|
||||||
const containingFrame = getContainingFrame(element);
|
const containingFrame = getContainingFrame(element);
|
||||||
return containingFrame &&
|
return containingFrame &&
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export const TopPicks = ({
|
|||||||
type="button"
|
type="button"
|
||||||
title={color}
|
title={color}
|
||||||
onClick={() => onChange(color)}
|
onClick={() => onChange(color)}
|
||||||
|
data-testid={`color-top-pick-${color}`}
|
||||||
>
|
>
|
||||||
<div className="color-picker__button-outline" />
|
<div className="color-picker__button-outline" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -9,11 +9,7 @@ import {
|
|||||||
} from "../actions/shortcuts";
|
} from "../actions/shortcuts";
|
||||||
import { Action } from "../actions/types";
|
import { Action } from "../actions/types";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import {
|
import { useExcalidrawAppState, useExcalidrawElements } from "./App";
|
||||||
useExcalidrawAppState,
|
|
||||||
useExcalidrawElements,
|
|
||||||
useExcalidrawSetAppState,
|
|
||||||
} from "./App";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export type ContextMenuItem = typeof CONTEXT_MENU_SEPARATOR | Action;
|
export type ContextMenuItem = typeof CONTEXT_MENU_SEPARATOR | Action;
|
||||||
@@ -25,14 +21,14 @@ type ContextMenuProps = {
|
|||||||
items: ContextMenuItems;
|
items: ContextMenuItems;
|
||||||
top: number;
|
top: number;
|
||||||
left: number;
|
left: number;
|
||||||
|
onClose: (cb?: () => void) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CONTEXT_MENU_SEPARATOR = "separator";
|
export const CONTEXT_MENU_SEPARATOR = "separator";
|
||||||
|
|
||||||
export const ContextMenu = React.memo(
|
export const ContextMenu = React.memo(
|
||||||
({ actionManager, items, top, left }: ContextMenuProps) => {
|
({ actionManager, items, top, left, onClose }: ContextMenuProps) => {
|
||||||
const appState = useExcalidrawAppState();
|
const appState = useExcalidrawAppState();
|
||||||
const setAppState = useExcalidrawSetAppState();
|
|
||||||
const elements = useExcalidrawElements();
|
const elements = useExcalidrawElements();
|
||||||
|
|
||||||
const filteredItems = items.reduce((acc: ContextMenuItem[], item) => {
|
const filteredItems = items.reduce((acc: ContextMenuItem[], item) => {
|
||||||
@@ -54,7 +50,7 @@ export const ContextMenu = React.memo(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
onCloseRequest={() => setAppState({ contextMenu: null })}
|
onCloseRequest={() => onClose()}
|
||||||
top={top}
|
top={top}
|
||||||
left={left}
|
left={left}
|
||||||
fitInViewport={true}
|
fitInViewport={true}
|
||||||
@@ -102,7 +98,7 @@ export const ContextMenu = React.memo(
|
|||||||
// we need update state before executing the action in case
|
// we need update state before executing the action in case
|
||||||
// the action uses the appState it's being passed (that still
|
// the action uses the appState it's being passed (that still
|
||||||
// contains a defined contextMenu) to return the next state.
|
// contains a defined contextMenu) to return the next state.
|
||||||
setAppState({ contextMenu: null }, () => {
|
onClose(() => {
|
||||||
actionManager.executeAction(item, "contextMenu");
|
actionManager.executeAction(item, "contextMenu");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -302,6 +302,12 @@ export const ROUGHNESS = {
|
|||||||
cartoonist: 2,
|
cartoonist: 2,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const STROKE_WIDTH = {
|
||||||
|
thin: 1,
|
||||||
|
bold: 2,
|
||||||
|
extraBold: 4,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const DEFAULT_ELEMENT_PROPS: {
|
export const DEFAULT_ELEMENT_PROPS: {
|
||||||
strokeColor: ExcalidrawElement["strokeColor"];
|
strokeColor: ExcalidrawElement["strokeColor"];
|
||||||
backgroundColor: ExcalidrawElement["backgroundColor"];
|
backgroundColor: ExcalidrawElement["backgroundColor"];
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"backgroundColor": "#d8f5a2",
|
"backgroundColor": "#d8f5a2",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id40",
|
"id": "id41",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "id41",
|
"id": "id42",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -45,7 +45,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id41",
|
"id": "id42",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -97,12 +97,12 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0.5,
|
||||||
0,
|
0.5,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
395,
|
394.5,
|
||||||
35,
|
34.5,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -110,7 +110,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"seed": Any<Number>,
|
"seed": Any<Number>,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id42",
|
"elementId": "id43",
|
||||||
"focus": -0.08139534883720931,
|
"focus": -0.08139534883720931,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@@ -150,11 +150,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
400,
|
399.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -186,7 +186,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id40",
|
"id": "id41",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -222,7 +222,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"baseline": 0,
|
"baseline": 0,
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id43",
|
"id": "id44",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -266,7 +266,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"baseline": 0,
|
"baseline": 0,
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id43",
|
"id": "id44",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -309,7 +309,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id44",
|
"id": "id45",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -317,7 +317,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "text-2",
|
"elementId": "text-2",
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 5,
|
"gap": 205,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
@@ -331,11 +331,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
300,
|
99.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -355,7 +355,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 300,
|
"width": 100,
|
||||||
"x": 255,
|
"x": 255,
|
||||||
"y": 239,
|
"y": 239,
|
||||||
}
|
}
|
||||||
@@ -367,7 +367,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"baseline": 0,
|
"baseline": 0,
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id43",
|
"containerId": "id44",
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 1,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
@@ -395,7 +395,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 130,
|
"width": 130,
|
||||||
"x": 340,
|
"x": 240,
|
||||||
"y": 226.5,
|
"y": 226.5,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -406,13 +406,13 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id33",
|
"id": "id34",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id35",
|
"elementId": "id36",
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@@ -428,11 +428,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
300,
|
99.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -441,7 +441,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"seed": Any<Number>,
|
"seed": Any<Number>,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id34",
|
"elementId": "id35",
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@@ -452,7 +452,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 300,
|
"width": 100,
|
||||||
"x": 255,
|
"x": 255,
|
||||||
"y": 239,
|
"y": 239,
|
||||||
}
|
}
|
||||||
@@ -464,7 +464,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"baseline": 0,
|
"baseline": 0,
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id32",
|
"containerId": "id33",
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 1,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
@@ -492,7 +492,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 130,
|
"width": 130,
|
||||||
"x": 340,
|
"x": 240,
|
||||||
"y": 226.5,
|
"y": 226.5,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -503,7 +503,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id32",
|
"id": "id33",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -538,7 +538,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id32",
|
"id": "id33",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -562,7 +562,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 555,
|
"x": 355,
|
||||||
"y": 189,
|
"y": 189,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -573,13 +573,13 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id37",
|
"id": "id38",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id39",
|
"elementId": "id40",
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@@ -595,11 +595,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
300,
|
99.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -608,7 +608,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"seed": Any<Number>,
|
"seed": Any<Number>,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id38",
|
"elementId": "id39",
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@@ -619,7 +619,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 300,
|
"width": 100,
|
||||||
"x": 255,
|
"x": 255,
|
||||||
"y": 239,
|
"y": 239,
|
||||||
}
|
}
|
||||||
@@ -631,7 +631,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"baseline": 0,
|
"baseline": 0,
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id36",
|
"containerId": "id37",
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 1,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
@@ -659,7 +659,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 130,
|
"width": 130,
|
||||||
"x": 340,
|
"x": 240,
|
||||||
"y": 226.5,
|
"y": 226.5,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -671,7 +671,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"baseline": 0,
|
"baseline": 0,
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id36",
|
"id": "id37",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -715,7 +715,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"baseline": 0,
|
"baseline": 0,
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id36",
|
"id": "id37",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -747,7 +747,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "top",
|
"verticalAlign": "top",
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 555,
|
"x": 355,
|
||||||
"y": 226.5,
|
"y": 226.5,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -801,11 +801,11 @@ exports[`Test Transform > should transform linear elements 1`] = `
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
300,
|
99.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -821,7 +821,7 @@ exports[`Test Transform > should transform linear elements 1`] = `
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 300,
|
"width": 100,
|
||||||
"x": 100,
|
"x": 100,
|
||||||
"y": 20,
|
"y": 20,
|
||||||
}
|
}
|
||||||
@@ -846,11 +846,11 @@ exports[`Test Transform > should transform linear elements 2`] = `
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
300,
|
99.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -866,7 +866,7 @@ exports[`Test Transform > should transform linear elements 2`] = `
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 300,
|
"width": 100,
|
||||||
"x": 450,
|
"x": 450,
|
||||||
"y": 20,
|
"y": 20,
|
||||||
}
|
}
|
||||||
@@ -895,7 +895,7 @@ exports[`Test Transform > should transform linear elements 3`] = `
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
300,
|
100,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -911,7 +911,7 @@ exports[`Test Transform > should transform linear elements 3`] = `
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 300,
|
"width": 100,
|
||||||
"x": 100,
|
"x": 100,
|
||||||
"y": 60,
|
"y": 60,
|
||||||
}
|
}
|
||||||
@@ -940,7 +940,7 @@ exports[`Test Transform > should transform linear elements 4`] = `
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
300,
|
100,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -956,7 +956,7 @@ exports[`Test Transform > should transform linear elements 4`] = `
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 300,
|
"width": 100,
|
||||||
"x": 450,
|
"x": 450,
|
||||||
"y": 60,
|
"y": 60,
|
||||||
}
|
}
|
||||||
@@ -1221,56 +1221,6 @@ exports[`Test Transform > should transform text element 2`] = `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Test Transform > should transform to labelled arrows when label provided for arrows 1`] = `
|
exports[`Test Transform > should transform to labelled arrows when label provided for arrows 1`] = `
|
||||||
{
|
|
||||||
"angle": 0,
|
|
||||||
"backgroundColor": "transparent",
|
|
||||||
"boundElements": [
|
|
||||||
{
|
|
||||||
"id": "id28",
|
|
||||||
"type": "text",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"endArrowhead": "arrow",
|
|
||||||
"endBinding": null,
|
|
||||||
"fillStyle": "solid",
|
|
||||||
"frameId": null,
|
|
||||||
"groupIds": [],
|
|
||||||
"height": 0,
|
|
||||||
"id": Any<String>,
|
|
||||||
"isDeleted": false,
|
|
||||||
"lastCommittedPoint": null,
|
|
||||||
"link": null,
|
|
||||||
"locked": false,
|
|
||||||
"opacity": 100,
|
|
||||||
"points": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
300,
|
|
||||||
0,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
"roughness": 1,
|
|
||||||
"roundness": null,
|
|
||||||
"seed": Any<Number>,
|
|
||||||
"startArrowhead": null,
|
|
||||||
"startBinding": null,
|
|
||||||
"strokeColor": "#1e1e1e",
|
|
||||||
"strokeStyle": "solid",
|
|
||||||
"strokeWidth": 2,
|
|
||||||
"type": "arrow",
|
|
||||||
"updated": 1,
|
|
||||||
"version": 1,
|
|
||||||
"versionNonce": Any<Number>,
|
|
||||||
"width": 300,
|
|
||||||
"x": 100,
|
|
||||||
"y": 100,
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Test Transform > should transform to labelled arrows when label provided for arrows 2`] = `
|
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
@@ -1294,11 +1244,11 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
300,
|
99.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -1314,13 +1264,13 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 300,
|
"width": 100,
|
||||||
"x": 100,
|
"x": 100,
|
||||||
"y": 200,
|
"y": 100,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Test Transform > should transform to labelled arrows when label provided for arrows 3`] = `
|
exports[`Test Transform > should transform to labelled arrows when label provided for arrows 2`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
@@ -1335,7 +1285,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 130,
|
"height": 0,
|
||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
"lastCommittedPoint": null,
|
||||||
@@ -1344,11 +1294,11 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
300,
|
99.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -1357,20 +1307,20 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"seed": Any<Number>,
|
"seed": Any<Number>,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
"strokeColor": "#1098ad",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 1,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 300,
|
"width": 100,
|
||||||
"x": 100,
|
"x": 100,
|
||||||
"y": 300,
|
"y": 200,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Test Transform > should transform to labelled arrows when label provided for arrows 4`] = `
|
exports[`Test Transform > should transform to labelled arrows when label provided for arrows 3`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
@@ -1394,11 +1344,11 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
300,
|
99.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -1414,7 +1364,57 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 300,
|
"width": 100,
|
||||||
|
"x": 100,
|
||||||
|
"y": 300,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test Transform > should transform to labelled arrows when label provided for arrows 4`] = `
|
||||||
|
{
|
||||||
|
"angle": 0,
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"boundElements": [
|
||||||
|
{
|
||||||
|
"id": "id32",
|
||||||
|
"type": "text",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"endArrowhead": "arrow",
|
||||||
|
"endBinding": null,
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"frameId": null,
|
||||||
|
"groupIds": [],
|
||||||
|
"height": 130,
|
||||||
|
"id": Any<String>,
|
||||||
|
"isDeleted": false,
|
||||||
|
"lastCommittedPoint": null,
|
||||||
|
"link": null,
|
||||||
|
"locked": false,
|
||||||
|
"opacity": 100,
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
0.5,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
99.5,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
"roughness": 1,
|
||||||
|
"roundness": null,
|
||||||
|
"seed": Any<Number>,
|
||||||
|
"startArrowhead": null,
|
||||||
|
"startBinding": null,
|
||||||
|
"strokeColor": "#1098ad",
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"type": "arrow",
|
||||||
|
"updated": 1,
|
||||||
|
"version": 2,
|
||||||
|
"versionNonce": Any<Number>,
|
||||||
|
"width": 100,
|
||||||
"x": 100,
|
"x": 100,
|
||||||
"y": 400,
|
"y": 400,
|
||||||
}
|
}
|
||||||
@@ -1426,7 +1426,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"baseline": 0,
|
"baseline": 0,
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id24",
|
"containerId": "id25",
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 1,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
@@ -1454,7 +1454,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 130,
|
"width": 130,
|
||||||
"x": 185,
|
"x": 85,
|
||||||
"y": 87.5,
|
"y": 87.5,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -1465,7 +1465,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"baseline": 0,
|
"baseline": 0,
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id25",
|
"containerId": "id26",
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 1,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
@@ -1493,7 +1493,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 200,
|
"width": 200,
|
||||||
"x": 150,
|
"x": 50,
|
||||||
"y": 187.5,
|
"y": 187.5,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -1504,7 +1504,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"baseline": 0,
|
"baseline": 0,
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id26",
|
"containerId": "id27",
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 1,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
@@ -1533,7 +1533,7 @@ LABELLED ARROW",
|
|||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 150,
|
"width": 150,
|
||||||
"x": 175,
|
"x": 75,
|
||||||
"y": 275,
|
"y": 275,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -1544,7 +1544,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"baseline": 0,
|
"baseline": 0,
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id27",
|
"containerId": "id28",
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 1,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
@@ -1573,7 +1573,7 @@ LABELLED ARROW",
|
|||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 150,
|
"width": 150,
|
||||||
"x": 175,
|
"x": 75,
|
||||||
"y": 375,
|
"y": 375,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -1584,7 +1584,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id18",
|
"id": "id19",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1619,7 +1619,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id19",
|
"id": "id20",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1654,7 +1654,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id20",
|
"id": "id21",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1689,7 +1689,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
"backgroundColor": "#fff3bf",
|
"backgroundColor": "#fff3bf",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id21",
|
"id": "id22",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1724,7 +1724,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id22",
|
"id": "id23",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1759,7 +1759,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
"backgroundColor": "#ffec99",
|
"backgroundColor": "#ffec99",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id23",
|
"id": "id24",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1794,7 +1794,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"baseline": 0,
|
"baseline": 0,
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id12",
|
"containerId": "id13",
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 1,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
@@ -1833,7 +1833,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"baseline": 0,
|
"baseline": 0,
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id13",
|
"containerId": "id14",
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 1,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
@@ -1873,7 +1873,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"baseline": 0,
|
"baseline": 0,
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id14",
|
"containerId": "id15",
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 1,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
@@ -1915,7 +1915,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"baseline": 0,
|
"baseline": 0,
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id15",
|
"containerId": "id16",
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 1,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
@@ -1955,7 +1955,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"baseline": 0,
|
"baseline": 0,
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id16",
|
"containerId": "id17",
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 1,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
@@ -1996,7 +1996,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"baseline": 0,
|
"baseline": 0,
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id17",
|
"containerId": "id18",
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 1,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
|
|||||||
+6
-13
@@ -43,7 +43,6 @@ import {
|
|||||||
measureBaseline,
|
measureBaseline,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { normalizeLink } from "./url";
|
import { normalizeLink } from "./url";
|
||||||
import { isValidFrameChild } from "../frame";
|
|
||||||
|
|
||||||
type RestoredAppState = Omit<
|
type RestoredAppState = Omit<
|
||||||
AppState,
|
AppState,
|
||||||
@@ -397,7 +396,7 @@ const repairBoundElement = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* resets `frameId` if no longer applicable.
|
* Remove an element's frameId if its containing frame is non-existent
|
||||||
*
|
*
|
||||||
* NOTE mutates elements.
|
* NOTE mutates elements.
|
||||||
*/
|
*/
|
||||||
@@ -405,16 +404,12 @@ const repairFrameMembership = (
|
|||||||
element: Mutable<ExcalidrawElement>,
|
element: Mutable<ExcalidrawElement>,
|
||||||
elementsMap: Map<string, Mutable<ExcalidrawElement>>,
|
elementsMap: Map<string, Mutable<ExcalidrawElement>>,
|
||||||
) => {
|
) => {
|
||||||
if (!element.frameId) {
|
if (element.frameId) {
|
||||||
return;
|
const containingFrame = elementsMap.get(element.frameId);
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (!containingFrame) {
|
||||||
!isValidFrameChild(element) ||
|
element.frameId = null;
|
||||||
// target frame not exists
|
}
|
||||||
!elementsMap.get(element.frameId)
|
|
||||||
) {
|
|
||||||
element.frameId = null;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -458,8 +453,6 @@ export const restoreElements = (
|
|||||||
// repair binding. Mutates elements.
|
// repair binding. Mutates elements.
|
||||||
const restoredElementsMap = arrayToMap(restoredElements);
|
const restoredElementsMap = arrayToMap(restoredElements);
|
||||||
for (const element of restoredElements) {
|
for (const element of restoredElements) {
|
||||||
// repair frame membership *after* bindings we do in restoreElement()
|
|
||||||
// since we rely on bindings to be correct
|
|
||||||
if (element.frameId) {
|
if (element.frameId) {
|
||||||
repairFrameMembership(element, restoredElementsMap);
|
repairFrameMembership(element, restoredElementsMap);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,31 @@ import {
|
|||||||
} from "./transform";
|
} from "./transform";
|
||||||
import { ExcalidrawArrowElement } from "../element/types";
|
import { ExcalidrawArrowElement } from "../element/types";
|
||||||
|
|
||||||
|
const opts = { regenerateIds: false };
|
||||||
|
|
||||||
describe("Test Transform", () => {
|
describe("Test Transform", () => {
|
||||||
|
it("should generate id unless opts.regenerateIds is set to false explicitly", () => {
|
||||||
|
const elements = [
|
||||||
|
{
|
||||||
|
type: "rectangle",
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
id: "rect-1",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let data = convertToExcalidrawElements(
|
||||||
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
);
|
||||||
|
expect(data.length).toBe(1);
|
||||||
|
expect(data[0].id).toBe("id0");
|
||||||
|
|
||||||
|
data = convertToExcalidrawElements(
|
||||||
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
|
);
|
||||||
|
expect(data[0].id).toBe("rect-1");
|
||||||
|
});
|
||||||
|
|
||||||
it("should transform regular shapes", () => {
|
it("should transform regular shapes", () => {
|
||||||
const elements = [
|
const elements = [
|
||||||
{
|
{
|
||||||
@@ -59,6 +83,7 @@ describe("Test Transform", () => {
|
|||||||
|
|
||||||
convertToExcalidrawElements(
|
convertToExcalidrawElements(
|
||||||
elements as ExcalidrawElementSkeleton[],
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
).forEach((ele) => {
|
).forEach((ele) => {
|
||||||
expect(ele).toMatchSnapshot({
|
expect(ele).toMatchSnapshot({
|
||||||
seed: expect.any(Number),
|
seed: expect.any(Number),
|
||||||
@@ -87,6 +112,7 @@ describe("Test Transform", () => {
|
|||||||
];
|
];
|
||||||
convertToExcalidrawElements(
|
convertToExcalidrawElements(
|
||||||
elements as ExcalidrawElementSkeleton[],
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
).forEach((ele) => {
|
).forEach((ele) => {
|
||||||
expect(ele).toMatchSnapshot({
|
expect(ele).toMatchSnapshot({
|
||||||
seed: expect.any(Number),
|
seed: expect.any(Number),
|
||||||
@@ -128,6 +154,7 @@ describe("Test Transform", () => {
|
|||||||
];
|
];
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
elements as ExcalidrawElementSkeleton[],
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(4);
|
expect(excaldrawElements.length).toBe(4);
|
||||||
@@ -210,6 +237,7 @@ describe("Test Transform", () => {
|
|||||||
];
|
];
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
elements as ExcalidrawElementSkeleton[],
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(12);
|
expect(excaldrawElements.length).toBe(12);
|
||||||
@@ -267,6 +295,7 @@ describe("Test Transform", () => {
|
|||||||
];
|
];
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
elements as ExcalidrawElementSkeleton[],
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(8);
|
expect(excaldrawElements.length).toBe(8);
|
||||||
@@ -300,6 +329,7 @@ describe("Test Transform", () => {
|
|||||||
];
|
];
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
elements as ExcalidrawElementSkeleton[],
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(4);
|
expect(excaldrawElements.length).toBe(4);
|
||||||
@@ -321,7 +351,7 @@ describe("Test Transform", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(text).toMatchObject({
|
expect(text).toMatchObject({
|
||||||
x: 340,
|
x: 240,
|
||||||
y: 226.5,
|
y: 226.5,
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "HELLO WORLD!!",
|
text: "HELLO WORLD!!",
|
||||||
@@ -341,7 +371,7 @@ describe("Test Transform", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(ellipse).toMatchObject({
|
expect(ellipse).toMatchObject({
|
||||||
x: 555,
|
x: 355,
|
||||||
y: 189,
|
y: 189,
|
||||||
type: "ellipse",
|
type: "ellipse",
|
||||||
boundElements: [
|
boundElements: [
|
||||||
@@ -383,10 +413,10 @@ describe("Test Transform", () => {
|
|||||||
|
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
elements as ExcalidrawElementSkeleton[],
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(4);
|
expect(excaldrawElements.length).toBe(4);
|
||||||
|
|
||||||
const [arrow, text1, text2, text3] = excaldrawElements;
|
const [arrow, text1, text2, text3] = excaldrawElements;
|
||||||
|
|
||||||
expect(arrow).toMatchObject({
|
expect(arrow).toMatchObject({
|
||||||
@@ -406,7 +436,7 @@ describe("Test Transform", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(text1).toMatchObject({
|
expect(text1).toMatchObject({
|
||||||
x: 340,
|
x: 240,
|
||||||
y: 226.5,
|
y: 226.5,
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "HELLO WORLD!!",
|
text: "HELLO WORLD!!",
|
||||||
@@ -426,7 +456,7 @@ describe("Test Transform", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(text3).toMatchObject({
|
expect(text3).toMatchObject({
|
||||||
x: 555,
|
x: 355,
|
||||||
y: 226.5,
|
y: 226.5,
|
||||||
type: "text",
|
type: "text",
|
||||||
boundElements: [
|
boundElements: [
|
||||||
@@ -499,6 +529,7 @@ describe("Test Transform", () => {
|
|||||||
|
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
elements as ExcalidrawElementSkeleton[],
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(5);
|
expect(excaldrawElements.length).toBe(5);
|
||||||
@@ -547,6 +578,7 @@ describe("Test Transform", () => {
|
|||||||
|
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
elements as ExcalidrawElementSkeleton[],
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(4);
|
expect(excaldrawElements.length).toBe(4);
|
||||||
@@ -600,17 +632,18 @@ describe("Test Transform", () => {
|
|||||||
|
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
elements as ExcalidrawElementSkeleton[],
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(4);
|
expect(excaldrawElements.length).toBe(4);
|
||||||
const [, , arrow] = excaldrawElements;
|
const [, , arrow, text] = excaldrawElements;
|
||||||
expect(arrow).toMatchObject({
|
expect(arrow).toMatchObject({
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
x: 255,
|
x: 255,
|
||||||
y: 239,
|
y: 239,
|
||||||
boundElements: [
|
boundElements: [
|
||||||
{
|
{
|
||||||
id: "id46",
|
id: text.id,
|
||||||
type: "text",
|
type: "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -650,17 +683,18 @@ describe("Test Transform", () => {
|
|||||||
];
|
];
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
elements as ExcalidrawElementSkeleton[],
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
expect(excaldrawElements.length).toBe(2);
|
expect(excaldrawElements.length).toBe(2);
|
||||||
const [arrow, rect] = excaldrawElements;
|
const [arrow, rect] = excaldrawElements;
|
||||||
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
|
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
|
||||||
elementId: "rect-1",
|
elementId: "rect-1",
|
||||||
focus: 0,
|
focus: 0,
|
||||||
gap: 5,
|
gap: 205,
|
||||||
});
|
});
|
||||||
expect(rect.boundElements).toStrictEqual([
|
expect(rect.boundElements).toStrictEqual([
|
||||||
{
|
{
|
||||||
id: "id47",
|
id: arrow.id,
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -692,6 +726,7 @@ describe("Test Transform", () => {
|
|||||||
];
|
];
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
elements as ExcalidrawElementSkeleton[],
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(1);
|
expect(excaldrawElements.length).toBe(1);
|
||||||
|
|||||||
+89
-5
@@ -39,6 +39,8 @@ import {
|
|||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { MarkOptional } from "../utility-types";
|
import { MarkOptional } from "../utility-types";
|
||||||
import { assertNever, getFontString } from "../utils";
|
import { assertNever, getFontString } from "../utils";
|
||||||
|
import { getSizeFromPoints } from "../points";
|
||||||
|
import { randomId } from "../random";
|
||||||
|
|
||||||
export type ValidLinearElement = {
|
export type ValidLinearElement = {
|
||||||
type: "arrow" | "line";
|
type: "arrow" | "line";
|
||||||
@@ -159,7 +161,7 @@ export type ExcalidrawElementSkeleton =
|
|||||||
} & Partial<ExcalidrawImageElement>);
|
} & Partial<ExcalidrawImageElement>);
|
||||||
|
|
||||||
const DEFAULT_LINEAR_ELEMENT_PROPS = {
|
const DEFAULT_LINEAR_ELEMENT_PROPS = {
|
||||||
width: 300,
|
width: 100,
|
||||||
height: 0,
|
height: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -357,6 +359,48 @@ const bindLinearElementToElement = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update start/end points by 0.5 so bindings don't overlap with start/end bound element coordinates.
|
||||||
|
const endPointIndex = linearElement.points.length - 1;
|
||||||
|
const delta = 0.5;
|
||||||
|
const newPoints = JSON.parse(JSON.stringify(linearElement.points));
|
||||||
|
// left to right so shift the arrow towards right
|
||||||
|
if (
|
||||||
|
linearElement.points[endPointIndex][0] >
|
||||||
|
linearElement.points[endPointIndex - 1][0]
|
||||||
|
) {
|
||||||
|
newPoints[0][0] = delta;
|
||||||
|
newPoints[endPointIndex][0] -= delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
// right to left so shift the arrow towards left
|
||||||
|
if (
|
||||||
|
linearElement.points[endPointIndex][0] <
|
||||||
|
linearElement.points[endPointIndex - 1][0]
|
||||||
|
) {
|
||||||
|
newPoints[0][0] = -delta;
|
||||||
|
newPoints[endPointIndex][0] += delta;
|
||||||
|
}
|
||||||
|
// top to bottom so shift the arrow towards top
|
||||||
|
if (
|
||||||
|
linearElement.points[endPointIndex][1] >
|
||||||
|
linearElement.points[endPointIndex - 1][1]
|
||||||
|
) {
|
||||||
|
newPoints[0][1] = delta;
|
||||||
|
newPoints[endPointIndex][1] -= delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
// bottom to top so shift the arrow towards bottom
|
||||||
|
if (
|
||||||
|
linearElement.points[endPointIndex][1] <
|
||||||
|
linearElement.points[endPointIndex - 1][1]
|
||||||
|
) {
|
||||||
|
newPoints[0][1] = -delta;
|
||||||
|
newPoints[endPointIndex][1] += delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(linearElement, { points: newPoints });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
linearElement,
|
linearElement,
|
||||||
startBoundElement,
|
startBoundElement,
|
||||||
@@ -384,18 +428,28 @@ class ElementStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const convertToExcalidrawElements = (
|
export const convertToExcalidrawElements = (
|
||||||
elements: ExcalidrawElementSkeleton[] | null,
|
elementsSkeleton: ExcalidrawElementSkeleton[] | null,
|
||||||
|
opts?: { regenerateIds: boolean },
|
||||||
) => {
|
) => {
|
||||||
if (!elements) {
|
if (!elementsSkeleton) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
const elements: ExcalidrawElementSkeleton[] = JSON.parse(
|
||||||
|
JSON.stringify(elementsSkeleton),
|
||||||
|
);
|
||||||
|
|
||||||
const elementStore = new ElementStore();
|
const elementStore = new ElementStore();
|
||||||
const elementsWithIds = new Map<string, ExcalidrawElementSkeleton>();
|
const elementsWithIds = new Map<string, ExcalidrawElementSkeleton>();
|
||||||
|
const oldToNewElementIdMap = new Map<string, string>();
|
||||||
|
|
||||||
// Create individual elements
|
// Create individual elements
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
let excalidrawElement: ExcalidrawElement;
|
let excalidrawElement: ExcalidrawElement;
|
||||||
|
const originalId = element.id;
|
||||||
|
if (opts?.regenerateIds !== false) {
|
||||||
|
Object.assign(element, { id: randomId() });
|
||||||
|
}
|
||||||
|
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
@@ -444,6 +498,11 @@ export const convertToExcalidrawElements = (
|
|||||||
],
|
],
|
||||||
...element,
|
...element,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Object.assign(
|
||||||
|
excalidrawElement,
|
||||||
|
getSizeFromPoints(excalidrawElement.points),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "text": {
|
case "text": {
|
||||||
@@ -499,6 +558,9 @@ export const convertToExcalidrawElements = (
|
|||||||
} else {
|
} else {
|
||||||
elementStore.add(excalidrawElement);
|
elementStore.add(excalidrawElement);
|
||||||
elementsWithIds.set(excalidrawElement.id, element);
|
elementsWithIds.set(excalidrawElement.id, element);
|
||||||
|
if (originalId) {
|
||||||
|
oldToNewElementIdMap.set(originalId, excalidrawElement.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,6 +586,18 @@ export const convertToExcalidrawElements = (
|
|||||||
element.type === "arrow" ? element?.start : undefined;
|
element.type === "arrow" ? element?.start : undefined;
|
||||||
const originalEnd =
|
const originalEnd =
|
||||||
element.type === "arrow" ? element?.end : undefined;
|
element.type === "arrow" ? element?.end : undefined;
|
||||||
|
if (originalStart && originalStart.id) {
|
||||||
|
const newStartId = oldToNewElementIdMap.get(originalStart.id);
|
||||||
|
if (newStartId) {
|
||||||
|
Object.assign(originalStart, { id: newStartId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (originalEnd && originalEnd.id) {
|
||||||
|
const newEndId = oldToNewElementIdMap.get(originalEnd.id);
|
||||||
|
if (newEndId) {
|
||||||
|
Object.assign(originalEnd, { id: newEndId });
|
||||||
|
}
|
||||||
|
}
|
||||||
const { linearElement, startBoundElement, endBoundElement } =
|
const { linearElement, startBoundElement, endBoundElement } =
|
||||||
bindLinearElementToElement(
|
bindLinearElementToElement(
|
||||||
container as ExcalidrawArrowElement,
|
container as ExcalidrawArrowElement,
|
||||||
@@ -539,13 +613,23 @@ export const convertToExcalidrawElements = (
|
|||||||
} else {
|
} else {
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "arrow": {
|
case "arrow": {
|
||||||
|
const { start, end } = element;
|
||||||
|
if (start && start.id) {
|
||||||
|
const newStartId = oldToNewElementIdMap.get(start.id);
|
||||||
|
Object.assign(start, { id: newStartId });
|
||||||
|
}
|
||||||
|
if (end && end.id) {
|
||||||
|
const newEndId = oldToNewElementIdMap.get(end.id);
|
||||||
|
Object.assign(end, { id: newEndId });
|
||||||
|
}
|
||||||
const { linearElement, startBoundElement, endBoundElement } =
|
const { linearElement, startBoundElement, endBoundElement } =
|
||||||
bindLinearElementToElement(
|
bindLinearElementToElement(
|
||||||
excalidrawElement as ExcalidrawArrowElement,
|
excalidrawElement as ExcalidrawArrowElement,
|
||||||
element.start,
|
start,
|
||||||
element.end,
|
end,
|
||||||
elementStore,
|
elementStore,
|
||||||
);
|
);
|
||||||
|
|
||||||
elementStore.add(linearElement);
|
elementStore.add(linearElement);
|
||||||
elementStore.add(startBoundElement);
|
elementStore.add(startBoundElement);
|
||||||
elementStore.add(endBoundElement);
|
elementStore.add(endBoundElement);
|
||||||
|
|||||||
@@ -392,7 +392,7 @@ export const getLinkHandleFromCoords = (
|
|||||||
[x1, y1, x2, y2]: Bounds,
|
[x1, y1, x2, y2]: Bounds,
|
||||||
angle: number,
|
angle: number,
|
||||||
appState: Pick<UIAppState, "zoom">,
|
appState: Pick<UIAppState, "zoom">,
|
||||||
): [x: number, y: number, width: number, height: number] => {
|
): Bounds => {
|
||||||
const size = DEFAULT_LINK_SIZE;
|
const size = DEFAULT_LINK_SIZE;
|
||||||
const linkWidth = size / appState.zoom.value;
|
const linkWidth = size / appState.zoom.value;
|
||||||
const linkHeight = size / appState.zoom.value;
|
const linkHeight = size / appState.zoom.value;
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import { LinearElementEditor } from "./linearElementEditor";
|
|||||||
import { arrayToMap, tupleToCoors } from "../utils";
|
import { arrayToMap, tupleToCoors } from "../utils";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||||
import { isValidFrameChild } from "../frame";
|
|
||||||
|
|
||||||
export type SuggestedBinding =
|
export type SuggestedBinding =
|
||||||
| NonDeleted<ExcalidrawBindableElement>
|
| NonDeleted<ExcalidrawBindableElement>
|
||||||
@@ -212,15 +211,6 @@ export const bindLinearElement = (
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (linearElement.frameId && !isValidFrameChild(linearElement)) {
|
|
||||||
mutateElement(
|
|
||||||
linearElement,
|
|
||||||
{
|
|
||||||
frameId: null,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Don't bind both ends of a simple segment
|
// Don't bind both ends of a simple segment
|
||||||
|
|||||||
+16
-16
@@ -34,7 +34,12 @@ export type RectangleBox = {
|
|||||||
type MaybeQuadraticSolution = [number | null, number | null] | false;
|
type MaybeQuadraticSolution = [number | null, number | null] | false;
|
||||||
|
|
||||||
// x and y position of top left corner, x and y position of bottom right corner
|
// x and y position of top left corner, x and y position of bottom right corner
|
||||||
export type Bounds = readonly [x1: number, y1: number, x2: number, y2: number];
|
export type Bounds = readonly [
|
||||||
|
minX: number,
|
||||||
|
minY: number,
|
||||||
|
maxX: number,
|
||||||
|
maxY: number,
|
||||||
|
];
|
||||||
|
|
||||||
export class ElementBounds {
|
export class ElementBounds {
|
||||||
private static boundsCache = new WeakMap<
|
private static boundsCache = new WeakMap<
|
||||||
@@ -63,7 +68,7 @@ export class ElementBounds {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static calculateBounds(element: ExcalidrawElement): Bounds {
|
private static calculateBounds(element: ExcalidrawElement): Bounds {
|
||||||
let bounds: [number, number, number, number];
|
let bounds: Bounds;
|
||||||
|
|
||||||
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element);
|
||||||
|
|
||||||
@@ -387,7 +392,7 @@ const getCubicBezierCurveBound = (
|
|||||||
export const getMinMaxXYFromCurvePathOps = (
|
export const getMinMaxXYFromCurvePathOps = (
|
||||||
ops: Op[],
|
ops: Op[],
|
||||||
transformXY?: (x: number, y: number) => [number, number],
|
transformXY?: (x: number, y: number) => [number, number],
|
||||||
): [number, number, number, number] => {
|
): Bounds => {
|
||||||
let currentP: Point = [0, 0];
|
let currentP: Point = [0, 0];
|
||||||
|
|
||||||
const { minX, minY, maxX, maxY } = ops.reduce(
|
const { minX, minY, maxX, maxY } = ops.reduce(
|
||||||
@@ -435,9 +440,9 @@ export const getMinMaxXYFromCurvePathOps = (
|
|||||||
return [minX, minY, maxX, maxY];
|
return [minX, minY, maxX, maxY];
|
||||||
};
|
};
|
||||||
|
|
||||||
const getBoundsFromPoints = (
|
export const getBoundsFromPoints = (
|
||||||
points: ExcalidrawFreeDrawElement["points"],
|
points: ExcalidrawFreeDrawElement["points"],
|
||||||
): [number, number, number, number] => {
|
): Bounds => {
|
||||||
let minX = Infinity;
|
let minX = Infinity;
|
||||||
let minY = Infinity;
|
let minY = Infinity;
|
||||||
let maxX = -Infinity;
|
let maxX = -Infinity;
|
||||||
@@ -589,7 +594,7 @@ const getLinearElementRotatedBounds = (
|
|||||||
element: ExcalidrawLinearElement,
|
element: ExcalidrawLinearElement,
|
||||||
cx: number,
|
cx: number,
|
||||||
cy: number,
|
cy: number,
|
||||||
): [number, number, number, number] => {
|
): Bounds => {
|
||||||
if (element.points.length < 2) {
|
if (element.points.length < 2) {
|
||||||
const [pointX, pointY] = element.points[0];
|
const [pointX, pointY] = element.points[0];
|
||||||
const [x, y] = rotate(
|
const [x, y] = rotate(
|
||||||
@@ -600,7 +605,7 @@ const getLinearElementRotatedBounds = (
|
|||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
let coords: [number, number, number, number] = [x, y, x, y];
|
let coords: Bounds = [x, y, x, y];
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
||||||
@@ -625,12 +630,7 @@ const getLinearElementRotatedBounds = (
|
|||||||
const transformXY = (x: number, y: number) =>
|
const transformXY = (x: number, y: number) =>
|
||||||
rotate(element.x + x, element.y + y, cx, cy, element.angle);
|
rotate(element.x + x, element.y + y, cx, cy, element.angle);
|
||||||
const res = getMinMaxXYFromCurvePathOps(ops, transformXY);
|
const res = getMinMaxXYFromCurvePathOps(ops, transformXY);
|
||||||
let coords: [number, number, number, number] = [
|
let coords: Bounds = [res[0], res[1], res[2], res[3]];
|
||||||
res[0],
|
|
||||||
res[1],
|
|
||||||
res[2],
|
|
||||||
res[3],
|
|
||||||
];
|
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
||||||
@@ -692,7 +692,7 @@ export const getResizedElementAbsoluteCoords = (
|
|||||||
nextWidth: number,
|
nextWidth: number,
|
||||||
nextHeight: number,
|
nextHeight: number,
|
||||||
normalizePoints: boolean,
|
normalizePoints: boolean,
|
||||||
): [number, number, number, number] => {
|
): Bounds => {
|
||||||
if (!(isLinearElement(element) || isFreeDrawElement(element))) {
|
if (!(isLinearElement(element) || isFreeDrawElement(element))) {
|
||||||
return [
|
return [
|
||||||
element.x,
|
element.x,
|
||||||
@@ -709,7 +709,7 @@ export const getResizedElementAbsoluteCoords = (
|
|||||||
normalizePoints,
|
normalizePoints,
|
||||||
);
|
);
|
||||||
|
|
||||||
let bounds: [number, number, number, number];
|
let bounds: Bounds;
|
||||||
|
|
||||||
if (isFreeDrawElement(element)) {
|
if (isFreeDrawElement(element)) {
|
||||||
// Free Draw
|
// Free Draw
|
||||||
@@ -740,7 +740,7 @@ export const getResizedElementAbsoluteCoords = (
|
|||||||
export const getElementPointsCoords = (
|
export const getElementPointsCoords = (
|
||||||
element: ExcalidrawLinearElement,
|
element: ExcalidrawLinearElement,
|
||||||
points: readonly (readonly [number, number])[],
|
points: readonly (readonly [number, number])[],
|
||||||
): [number, number, number, number] => {
|
): Bounds => {
|
||||||
// This might be computationally heavey
|
// This might be computationally heavey
|
||||||
const gen = rough.generator();
|
const gen = rough.generator();
|
||||||
const curve =
|
const curve =
|
||||||
|
|||||||
@@ -494,7 +494,9 @@ const hitTestFreeDrawElement = (
|
|||||||
// for filled freedraw shapes, support
|
// for filled freedraw shapes, support
|
||||||
// selecting from inside
|
// selecting from inside
|
||||||
if (shape && shape.sets.length) {
|
if (shape && shape.sets.length) {
|
||||||
return hitTestCurveInside(shape, x, y, "round");
|
return element.fillStyle === "solid"
|
||||||
|
? hitTestCurveInside(shape, x, y, "round")
|
||||||
|
: hitTestRoughShape(shape, x, y, threshold);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+51
-37
@@ -1,5 +1,5 @@
|
|||||||
import { updateBoundElements } from "./binding";
|
import { updateBoundElements } from "./binding";
|
||||||
import { getCommonBounds } from "./bounds";
|
import { Bounds, getCommonBounds } from "./bounds";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { getPerfectElementSize } from "./sizeHelpers";
|
import { getPerfectElementSize } from "./sizeHelpers";
|
||||||
import { NonDeletedExcalidrawElement } from "./types";
|
import { NonDeletedExcalidrawElement } from "./types";
|
||||||
@@ -8,7 +8,11 @@ import { getBoundTextElement } from "./textElement";
|
|||||||
import { isSelectedViaGroup } from "../groups";
|
import { isSelectedViaGroup } from "../groups";
|
||||||
import { getGridPoint } from "../math";
|
import { getGridPoint } from "../math";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { isFrameElement } from "./typeChecks";
|
import {
|
||||||
|
isArrowElement,
|
||||||
|
isBoundToContainer,
|
||||||
|
isFrameElement,
|
||||||
|
} from "./typeChecks";
|
||||||
|
|
||||||
export const dragSelectedElements = (
|
export const dragSelectedElements = (
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
@@ -35,44 +39,41 @@ export const dragSelectedElements = (
|
|||||||
if (frames.length > 0) {
|
if (frames.length > 0) {
|
||||||
const elementsInFrames = scene
|
const elementsInFrames = scene
|
||||||
.getNonDeletedElements()
|
.getNonDeletedElements()
|
||||||
|
.filter((e) => !isBoundToContainer(e))
|
||||||
.filter((e) => e.frameId !== null)
|
.filter((e) => e.frameId !== null)
|
||||||
.filter((e) => frames.includes(e.frameId!));
|
.filter((e) => frames.includes(e.frameId!));
|
||||||
|
|
||||||
elementsInFrames.forEach((element) => elementsToUpdate.add(element));
|
elementsInFrames.forEach((element) => elementsToUpdate.add(element));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const commonBounds = getCommonBounds(
|
||||||
|
Array.from(elementsToUpdate).map(
|
||||||
|
(el) => pointerDownState.originalElements.get(el.id) ?? el,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const adjustedOffset = calculateOffset(
|
||||||
|
commonBounds,
|
||||||
|
offset,
|
||||||
|
snapOffset,
|
||||||
|
gridSize,
|
||||||
|
);
|
||||||
|
|
||||||
elementsToUpdate.forEach((element) => {
|
elementsToUpdate.forEach((element) => {
|
||||||
updateElementCoords(
|
updateElementCoords(pointerDownState, element, adjustedOffset);
|
||||||
pointerDownState,
|
|
||||||
element,
|
|
||||||
offset,
|
|
||||||
snapOffset,
|
|
||||||
gridSize,
|
|
||||||
);
|
|
||||||
// update coords of bound text only if we're dragging the container directly
|
// update coords of bound text only if we're dragging the container directly
|
||||||
// (we don't drag the group that it's part of)
|
// (we don't drag the group that it's part of)
|
||||||
if (
|
if (
|
||||||
|
// Don't update coords of arrow label since we calculate its position during render
|
||||||
|
!isArrowElement(element) &&
|
||||||
// container isn't part of any group
|
// container isn't part of any group
|
||||||
// (perf optim so we don't check `isSelectedViaGroup()` in every case)
|
// (perf optim so we don't check `isSelectedViaGroup()` in every case)
|
||||||
!element.groupIds.length ||
|
(!element.groupIds.length ||
|
||||||
// container is part of a group, but we're dragging the container directly
|
// container is part of a group, but we're dragging the container directly
|
||||||
(appState.editingGroupId && !isSelectedViaGroup(appState, element))
|
(appState.editingGroupId && !isSelectedViaGroup(appState, element)))
|
||||||
) {
|
) {
|
||||||
const textElement = getBoundTextElement(element);
|
const textElement = getBoundTextElement(element);
|
||||||
if (
|
if (textElement) {
|
||||||
textElement &&
|
updateElementCoords(pointerDownState, textElement, adjustedOffset);
|
||||||
// when container is added to a frame, so will its bound text
|
|
||||||
// so the text is already in `elementsToUpdate` and we should avoid
|
|
||||||
// updating its coords again
|
|
||||||
(!textElement.frameId || !frames.includes(textElement.frameId))
|
|
||||||
) {
|
|
||||||
updateElementCoords(
|
|
||||||
pointerDownState,
|
|
||||||
textElement,
|
|
||||||
offset,
|
|
||||||
snapOffset,
|
|
||||||
gridSize,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateBoundElements(element, {
|
updateBoundElements(element, {
|
||||||
@@ -81,23 +82,20 @@ export const dragSelectedElements = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateElementCoords = (
|
const calculateOffset = (
|
||||||
pointerDownState: PointerDownState,
|
commonBounds: Bounds,
|
||||||
element: NonDeletedExcalidrawElement,
|
|
||||||
dragOffset: { x: number; y: number },
|
dragOffset: { x: number; y: number },
|
||||||
snapOffset: { x: number; y: number },
|
snapOffset: { x: number; y: number },
|
||||||
gridSize: AppState["gridSize"],
|
gridSize: AppState["gridSize"],
|
||||||
) => {
|
): { x: number; y: number } => {
|
||||||
const originalElement =
|
const [x, y] = commonBounds;
|
||||||
pointerDownState.originalElements.get(element.id) ?? element;
|
let nextX = x + dragOffset.x + snapOffset.x;
|
||||||
|
let nextY = y + dragOffset.y + snapOffset.y;
|
||||||
let nextX = originalElement.x + dragOffset.x + snapOffset.x;
|
|
||||||
let nextY = originalElement.y + dragOffset.y + snapOffset.y;
|
|
||||||
|
|
||||||
if (snapOffset.x === 0 || snapOffset.y === 0) {
|
if (snapOffset.x === 0 || snapOffset.y === 0) {
|
||||||
const [nextGridX, nextGridY] = getGridPoint(
|
const [nextGridX, nextGridY] = getGridPoint(
|
||||||
originalElement.x + dragOffset.x,
|
x + dragOffset.x,
|
||||||
originalElement.y + dragOffset.y,
|
y + dragOffset.y,
|
||||||
gridSize,
|
gridSize,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -109,6 +107,22 @@ const updateElementCoords = (
|
|||||||
nextY = nextGridY;
|
nextY = nextGridY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
x: nextX - x,
|
||||||
|
y: nextY - y,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateElementCoords = (
|
||||||
|
pointerDownState: PointerDownState,
|
||||||
|
element: NonDeletedExcalidrawElement,
|
||||||
|
dragOffset: { x: number; y: number },
|
||||||
|
) => {
|
||||||
|
const originalElement =
|
||||||
|
pointerDownState.originalElements.get(element.id) ?? element;
|
||||||
|
|
||||||
|
const nextX = originalElement.x + dragOffset.x;
|
||||||
|
const nextY = originalElement.y + dragOffset.y;
|
||||||
|
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
x: nextX,
|
x: nextX,
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ const RE_VALTOWN =
|
|||||||
const RE_GENERIC_EMBED =
|
const RE_GENERIC_EMBED =
|
||||||
/^<(?:iframe|blockquote)[\s\S]*?\s(?:src|href)=["']([^"']*)["'][\s\S]*?>$/i;
|
/^<(?:iframe|blockquote)[\s\S]*?\s(?:src|href)=["']([^"']*)["'][\s\S]*?>$/i;
|
||||||
|
|
||||||
|
const RE_GIPHY =
|
||||||
|
/giphy.com\/(?:clips|embed|gifs)\/[a-zA-Z0-9]*?-?([a-zA-Z0-9]+)(?:[^a-zA-Z0-9]|$)/;
|
||||||
|
|
||||||
const ALLOWED_DOMAINS = new Set([
|
const ALLOWED_DOMAINS = new Set([
|
||||||
"youtube.com",
|
"youtube.com",
|
||||||
"youtu.be",
|
"youtu.be",
|
||||||
@@ -60,6 +63,7 @@ const ALLOWED_DOMAINS = new Set([
|
|||||||
"*.simplepdf.eu",
|
"*.simplepdf.eu",
|
||||||
"stackblitz.com",
|
"stackblitz.com",
|
||||||
"val.town",
|
"val.town",
|
||||||
|
"giphy.com",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const createSrcDoc = (body: string) => {
|
const createSrcDoc = (body: string) => {
|
||||||
@@ -309,6 +313,10 @@ export const extractSrc = (htmlString: string): string => {
|
|||||||
return gistMatch[1];
|
return gistMatch[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (RE_GIPHY.test(htmlString)) {
|
||||||
|
return `https://giphy.com/embed/${RE_GIPHY.exec(htmlString)![1]}`;
|
||||||
|
}
|
||||||
|
|
||||||
const match = htmlString.match(RE_GENERIC_EMBED);
|
const match = htmlString.match(RE_GENERIC_EMBED);
|
||||||
if (match && match.length === 2) {
|
if (match && match.length === 2) {
|
||||||
return match[1];
|
return match[1];
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
} from "../math";
|
} from "../math";
|
||||||
import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
|
import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
|
||||||
import {
|
import {
|
||||||
|
Bounds,
|
||||||
getCurvePathOps,
|
getCurvePathOps,
|
||||||
getElementPointsCoords,
|
getElementPointsCoords,
|
||||||
getMinMaxXYFromCurvePathOps,
|
getMinMaxXYFromCurvePathOps,
|
||||||
@@ -1316,7 +1317,7 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
static getMinMaxXYWithBoundText = (
|
static getMinMaxXYWithBoundText = (
|
||||||
element: ExcalidrawLinearElement,
|
element: ExcalidrawLinearElement,
|
||||||
elementBounds: [number, number, number, number],
|
elementBounds: Bounds,
|
||||||
boundTextElement: ExcalidrawTextElementWithContainer,
|
boundTextElement: ExcalidrawTextElementWithContainer,
|
||||||
): [number, number, number, number, number, number] => {
|
): [number, number, number, number, number, number] => {
|
||||||
let [x1, y1, x2, y2] = elementBounds;
|
let [x1, y1, x2, y2] = elementBounds;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
MaybeTransformHandleType,
|
MaybeTransformHandleType,
|
||||||
} from "./transformHandles";
|
} from "./transformHandles";
|
||||||
import { AppState, Zoom } from "../types";
|
import { AppState, Zoom } from "../types";
|
||||||
|
import { Bounds } from "./bounds";
|
||||||
|
|
||||||
const isInsideTransformHandle = (
|
const isInsideTransformHandle = (
|
||||||
transformHandle: TransformHandle,
|
transformHandle: TransformHandle,
|
||||||
@@ -87,7 +88,7 @@ export const getElementWithTransformHandleType = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getTransformHandleTypeFromCoords = (
|
export const getTransformHandleTypeFromCoords = (
|
||||||
[x1, y1, x2, y2]: readonly [number, number, number, number],
|
[x1, y1, x2, y2]: Bounds,
|
||||||
scenePointerX: number,
|
scenePointerX: number,
|
||||||
scenePointerY: number,
|
scenePointerY: number,
|
||||||
zoom: Zoom,
|
zoom: Zoom,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
PointerType,
|
PointerType,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import { getElementAbsoluteCoords } from "./bounds";
|
import { Bounds, getElementAbsoluteCoords } from "./bounds";
|
||||||
import { rotate } from "../math";
|
import { rotate } from "../math";
|
||||||
import { InteractiveCanvasAppState, Zoom } from "../types";
|
import { InteractiveCanvasAppState, Zoom } from "../types";
|
||||||
import { isTextElement } from ".";
|
import { isTextElement } from ".";
|
||||||
@@ -23,7 +23,7 @@ export type TransformHandleDirection =
|
|||||||
|
|
||||||
export type TransformHandleType = TransformHandleDirection | "rotation";
|
export type TransformHandleType = TransformHandleDirection | "rotation";
|
||||||
|
|
||||||
export type TransformHandle = [number, number, number, number];
|
export type TransformHandle = Bounds;
|
||||||
export type TransformHandles = Partial<{
|
export type TransformHandles = Partial<{
|
||||||
[T in TransformHandleType]: TransformHandle;
|
[T in TransformHandleType]: TransformHandle;
|
||||||
}>;
|
}>;
|
||||||
|
|||||||
+13
-13
@@ -123,7 +123,7 @@ describe("adding elements to frames", () => {
|
|||||||
const commonTestCases = async (
|
const commonTestCases = async (
|
||||||
func: typeof resizeFrameOverElement | typeof dragElementIntoFrame,
|
func: typeof resizeFrameOverElement | typeof dragElementIntoFrame,
|
||||||
) => {
|
) => {
|
||||||
describe("when frame is in a layer below", async () => {
|
describe.skip("when frame is in a layer below", async () => {
|
||||||
it("should add an element", async () => {
|
it("should add an element", async () => {
|
||||||
h.elements = [frame, rect2];
|
h.elements = [frame, rect2];
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ describe("adding elements to frames", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when frame is in a layer above", async () => {
|
describe.skip("when frame is in a layer above", async () => {
|
||||||
it("should add an element", async () => {
|
it("should add an element", async () => {
|
||||||
h.elements = [rect2, frame];
|
h.elements = [rect2, frame];
|
||||||
|
|
||||||
@@ -212,7 +212,7 @@ describe("adding elements to frames", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("when frame is in an inner layer", async () => {
|
describe("when frame is in an inner layer", async () => {
|
||||||
it("should add elements", async () => {
|
it.skip("should add elements", async () => {
|
||||||
h.elements = [rect2, frame, rect3];
|
h.elements = [rect2, frame, rect3];
|
||||||
|
|
||||||
func(frame, rect2);
|
func(frame, rect2);
|
||||||
@@ -223,7 +223,7 @@ describe("adding elements to frames", () => {
|
|||||||
expectEqualIds([rect2, rect3, frame]);
|
expectEqualIds([rect2, rect3, frame]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should add elements when there are other other elements in between", async () => {
|
it.skip("should add elements when there are other other elements in between", async () => {
|
||||||
h.elements = [rect2, rect1, frame, rect4, rect3];
|
h.elements = [rect2, rect1, frame, rect4, rect3];
|
||||||
|
|
||||||
func(frame, rect2);
|
func(frame, rect2);
|
||||||
@@ -234,7 +234,7 @@ describe("adding elements to frames", () => {
|
|||||||
expectEqualIds([rect1, rect2, rect3, frame, rect4]);
|
expectEqualIds([rect1, rect2, rect3, frame, rect4]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should add elements when there are other elements in between and the order is reversed", async () => {
|
it.skip("should add elements when there are other elements in between and the order is reversed", async () => {
|
||||||
h.elements = [rect3, rect4, frame, rect2, rect1];
|
h.elements = [rect3, rect4, frame, rect2, rect1];
|
||||||
|
|
||||||
func(frame, rect2);
|
func(frame, rect2);
|
||||||
@@ -289,7 +289,7 @@ describe("adding elements to frames", () => {
|
|||||||
describe("resizing frame over elements", async () => {
|
describe("resizing frame over elements", async () => {
|
||||||
await commonTestCases(resizeFrameOverElement);
|
await commonTestCases(resizeFrameOverElement);
|
||||||
|
|
||||||
it("resizing over text containers and labelled arrows", async () => {
|
it.skip("resizing over text containers and labelled arrows", async () => {
|
||||||
await resizingTest(
|
await resizingTest(
|
||||||
"rectangle",
|
"rectangle",
|
||||||
["frame", "rectangle", "text"],
|
["frame", "rectangle", "text"],
|
||||||
@@ -339,7 +339,7 @@ describe("adding elements to frames", () => {
|
|||||||
// );
|
// );
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should add arrow bound with text when frame is in a layer below", async () => {
|
it.skip("should add arrow bound with text when frame is in a layer below", async () => {
|
||||||
h.elements = [frame, arrow, text];
|
h.elements = [frame, arrow, text];
|
||||||
|
|
||||||
resizeFrameOverElement(frame, arrow);
|
resizeFrameOverElement(frame, arrow);
|
||||||
@@ -359,7 +359,7 @@ describe("adding elements to frames", () => {
|
|||||||
expectEqualIds([arrow, text, frame]);
|
expectEqualIds([arrow, text, frame]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should add arrow bound with text when frame is in an inner layer", async () => {
|
it.skip("should add arrow bound with text when frame is in an inner layer", async () => {
|
||||||
h.elements = [arrow, frame, text];
|
h.elements = [arrow, frame, text];
|
||||||
|
|
||||||
resizeFrameOverElement(frame, arrow);
|
resizeFrameOverElement(frame, arrow);
|
||||||
@@ -371,7 +371,7 @@ describe("adding elements to frames", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("resizing frame over elements but downwards", async () => {
|
describe("resizing frame over elements but downwards", async () => {
|
||||||
it("should add elements when frame is in a layer below", async () => {
|
it.skip("should add elements when frame is in a layer below", async () => {
|
||||||
h.elements = [frame, rect1, rect2, rect3, rect4];
|
h.elements = [frame, rect1, rect2, rect3, rect4];
|
||||||
|
|
||||||
resizeFrameOverElement(frame, rect4);
|
resizeFrameOverElement(frame, rect4);
|
||||||
@@ -382,7 +382,7 @@ describe("adding elements to frames", () => {
|
|||||||
expectEqualIds([rect2, rect3, frame, rect4, rect1]);
|
expectEqualIds([rect2, rect3, frame, rect4, rect1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should add elements when frame is in a layer above", async () => {
|
it.skip("should add elements when frame is in a layer above", async () => {
|
||||||
h.elements = [rect1, rect2, rect3, rect4, frame];
|
h.elements = [rect1, rect2, rect3, rect4, frame];
|
||||||
|
|
||||||
resizeFrameOverElement(frame, rect4);
|
resizeFrameOverElement(frame, rect4);
|
||||||
@@ -393,7 +393,7 @@ describe("adding elements to frames", () => {
|
|||||||
expectEqualIds([rect1, rect2, rect3, frame, rect4]);
|
expectEqualIds([rect1, rect2, rect3, frame, rect4]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should add elements when frame is in an inner layer", async () => {
|
it.skip("should add elements when frame is in an inner layer", async () => {
|
||||||
h.elements = [rect1, rect2, frame, rect3, rect4];
|
h.elements = [rect1, rect2, frame, rect3, rect4];
|
||||||
|
|
||||||
resizeFrameOverElement(frame, rect4);
|
resizeFrameOverElement(frame, rect4);
|
||||||
@@ -408,7 +408,7 @@ describe("adding elements to frames", () => {
|
|||||||
describe("dragging elements into the frame", async () => {
|
describe("dragging elements into the frame", async () => {
|
||||||
await commonTestCases(dragElementIntoFrame);
|
await commonTestCases(dragElementIntoFrame);
|
||||||
|
|
||||||
it("should drag element inside, duplicate it and keep it in frame", () => {
|
it.skip("should drag element inside, duplicate it and keep it in frame", () => {
|
||||||
h.elements = [frame, rect2];
|
h.elements = [frame, rect2];
|
||||||
|
|
||||||
dragElementIntoFrame(frame, rect2);
|
dragElementIntoFrame(frame, rect2);
|
||||||
@@ -422,7 +422,7 @@ describe("adding elements to frames", () => {
|
|||||||
expectEqualIds([rect2_copy, rect2, frame]);
|
expectEqualIds([rect2_copy, rect2, frame]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should drag element inside, duplicate it and remove it from frame", () => {
|
it.skip("should drag element inside, duplicate it and remove it from frame", () => {
|
||||||
h.elements = [frame, rect2];
|
h.elements = [frame, rect2];
|
||||||
|
|
||||||
dragElementIntoFrame(frame, rect2);
|
dragElementIntoFrame(frame, rect2);
|
||||||
|
|||||||
+50
-246
@@ -19,10 +19,10 @@ import { mutateElement } from "./element/mutateElement";
|
|||||||
import { AppClassProperties, AppState, StaticCanvasAppState } from "./types";
|
import { AppClassProperties, AppState, StaticCanvasAppState } from "./types";
|
||||||
import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
||||||
import { isFrameElement } from "./element";
|
import { isFrameElement } from "./element";
|
||||||
import { moveOneRight } from "./zindex";
|
|
||||||
import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
|
import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
|
||||||
import Scene, { ExcalidrawElementsIncludingDeleted } from "./scene/Scene";
|
import Scene, { ExcalidrawElementsIncludingDeleted } from "./scene/Scene";
|
||||||
import { getElementLineSegments } from "./element/bounds";
|
import { getElementLineSegments } from "./element/bounds";
|
||||||
|
import { doLineSegmentsIntersect } from "./packages/utils";
|
||||||
|
|
||||||
// --------------------------- Frame State ------------------------------------
|
// --------------------------- Frame State ------------------------------------
|
||||||
export const bindElementsToFramesAfterDuplication = (
|
export const bindElementsToFramesAfterDuplication = (
|
||||||
@@ -56,130 +56,21 @@ export const bindElementsToFramesAfterDuplication = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --------------------------- Frame Geometry ---------------------------------
|
export function isElementIntersectingFrame(
|
||||||
class Point {
|
element: ExcalidrawElement,
|
||||||
x: number;
|
frame: ExcalidrawFrameElement,
|
||||||
y: number;
|
) {
|
||||||
|
const frameLineSegments = getElementLineSegments(frame);
|
||||||
|
|
||||||
constructor(x: number, y: number) {
|
const elementLineSegments = getElementLineSegments(element);
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LineSegment {
|
const intersecting = frameLineSegments.some((frameLineSegment) =>
|
||||||
first: Point;
|
elementLineSegments.some((elementLineSegment) =>
|
||||||
second: Point;
|
doLineSegmentsIntersect(frameLineSegment, elementLineSegment),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
constructor(pointA: Point, pointB: Point) {
|
return intersecting;
|
||||||
this.first = pointA;
|
|
||||||
this.second = pointB;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getBoundingBox(): [Point, Point] {
|
|
||||||
return [
|
|
||||||
new Point(
|
|
||||||
Math.min(this.first.x, this.second.x),
|
|
||||||
Math.min(this.first.y, this.second.y),
|
|
||||||
),
|
|
||||||
new Point(
|
|
||||||
Math.max(this.first.x, this.second.x),
|
|
||||||
Math.max(this.first.y, this.second.y),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://martin-thoma.com/how-to-check-if-two-line-segments-intersect/
|
|
||||||
class FrameGeometry {
|
|
||||||
private static EPSILON = 0.000001;
|
|
||||||
|
|
||||||
private static crossProduct(a: Point, b: Point) {
|
|
||||||
return a.x * b.y - b.x * a.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static doBoundingBoxesIntersect(
|
|
||||||
a: [Point, Point],
|
|
||||||
b: [Point, Point],
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
a[0].x <= b[1].x &&
|
|
||||||
a[1].x >= b[0].x &&
|
|
||||||
a[0].y <= b[1].y &&
|
|
||||||
a[1].y >= b[0].y
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static isPointOnLine(a: LineSegment, b: Point) {
|
|
||||||
const aTmp = new LineSegment(
|
|
||||||
new Point(0, 0),
|
|
||||||
new Point(a.second.x - a.first.x, a.second.y - a.first.y),
|
|
||||||
);
|
|
||||||
const bTmp = new Point(b.x - a.first.x, b.y - a.first.y);
|
|
||||||
const r = this.crossProduct(aTmp.second, bTmp);
|
|
||||||
return Math.abs(r) < this.EPSILON;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static isPointRightOfLine(a: LineSegment, b: Point) {
|
|
||||||
const aTmp = new LineSegment(
|
|
||||||
new Point(0, 0),
|
|
||||||
new Point(a.second.x - a.first.x, a.second.y - a.first.y),
|
|
||||||
);
|
|
||||||
const bTmp = new Point(b.x - a.first.x, b.y - a.first.y);
|
|
||||||
return this.crossProduct(aTmp.second, bTmp) < 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static lineSegmentTouchesOrCrossesLine(
|
|
||||||
a: LineSegment,
|
|
||||||
b: LineSegment,
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
this.isPointOnLine(a, b.first) ||
|
|
||||||
this.isPointOnLine(a, b.second) ||
|
|
||||||
(this.isPointRightOfLine(a, b.first)
|
|
||||||
? !this.isPointRightOfLine(a, b.second)
|
|
||||||
: this.isPointRightOfLine(a, b.second))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static doLineSegmentsIntersect(
|
|
||||||
a: [readonly [number, number], readonly [number, number]],
|
|
||||||
b: [readonly [number, number], readonly [number, number]],
|
|
||||||
) {
|
|
||||||
const aSegment = new LineSegment(
|
|
||||||
new Point(a[0][0], a[0][1]),
|
|
||||||
new Point(a[1][0], a[1][1]),
|
|
||||||
);
|
|
||||||
const bSegment = new LineSegment(
|
|
||||||
new Point(b[0][0], b[0][1]),
|
|
||||||
new Point(b[1][0], b[1][1]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const box1 = aSegment.getBoundingBox();
|
|
||||||
const box2 = bSegment.getBoundingBox();
|
|
||||||
return (
|
|
||||||
this.doBoundingBoxesIntersect(box1, box2) &&
|
|
||||||
this.lineSegmentTouchesOrCrossesLine(aSegment, bSegment) &&
|
|
||||||
this.lineSegmentTouchesOrCrossesLine(bSegment, aSegment)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static isElementIntersectingFrame(
|
|
||||||
element: ExcalidrawElement,
|
|
||||||
frame: ExcalidrawFrameElement,
|
|
||||||
) {
|
|
||||||
const frameLineSegments = getElementLineSegments(frame);
|
|
||||||
|
|
||||||
const elementLineSegments = getElementLineSegments(element);
|
|
||||||
|
|
||||||
const intersecting = frameLineSegments.some((frameLineSegment) =>
|
|
||||||
elementLineSegments.some((elementLineSegment) =>
|
|
||||||
this.doLineSegmentsIntersect(frameLineSegment, elementLineSegment),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return intersecting;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getElementsCompletelyInFrame = (
|
export const getElementsCompletelyInFrame = (
|
||||||
@@ -207,10 +98,7 @@ export const isElementContainingFrame = (
|
|||||||
export const getElementsIntersectingFrame = (
|
export const getElementsIntersectingFrame = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
frame: ExcalidrawFrameElement,
|
frame: ExcalidrawFrameElement,
|
||||||
) =>
|
) => elements.filter((element) => isElementIntersectingFrame(element, frame));
|
||||||
elements.filter((element) =>
|
|
||||||
FrameGeometry.isElementIntersectingFrame(element, frame),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const elementsAreInFrameBounds = (
|
export const elementsAreInFrameBounds = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
@@ -236,7 +124,7 @@ export const elementOverlapsWithFrame = (
|
|||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
elementsAreInFrameBounds([element], frame) ||
|
elementsAreInFrameBounds([element], frame) ||
|
||||||
FrameGeometry.isElementIntersectingFrame(element, frame) ||
|
isElementIntersectingFrame(element, frame) ||
|
||||||
isElementContainingFrame([frame], element, frame)
|
isElementContainingFrame([frame], element, frame)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -273,7 +161,7 @@ export const groupsAreAtLeastIntersectingTheFrame = (
|
|||||||
return !!elementsInGroup.find(
|
return !!elementsInGroup.find(
|
||||||
(element) =>
|
(element) =>
|
||||||
elementsAreInFrameBounds([element], frame) ||
|
elementsAreInFrameBounds([element], frame) ||
|
||||||
FrameGeometry.isElementIntersectingFrame(element, frame),
|
isElementIntersectingFrame(element, frame),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -294,7 +182,7 @@ export const groupsAreCompletelyOutOfFrame = (
|
|||||||
elementsInGroup.find(
|
elementsInGroup.find(
|
||||||
(element) =>
|
(element) =>
|
||||||
elementsAreInFrameBounds([element], frame) ||
|
elementsAreInFrameBounds([element], frame) ||
|
||||||
FrameGeometry.isElementIntersectingFrame(element, frame),
|
isElementIntersectingFrame(element, frame),
|
||||||
) === undefined
|
) === undefined
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -323,24 +211,7 @@ export const groupByFrames = (elements: readonly ExcalidrawElement[]) => {
|
|||||||
export const getFrameElements = (
|
export const getFrameElements = (
|
||||||
allElements: ExcalidrawElementsIncludingDeleted,
|
allElements: ExcalidrawElementsIncludingDeleted,
|
||||||
frameId: string,
|
frameId: string,
|
||||||
opts?: { includeBoundArrows?: boolean },
|
) => allElements.filter((element) => element.frameId === frameId);
|
||||||
) => {
|
|
||||||
return allElements.filter((element) => {
|
|
||||||
if (element.frameId === frameId) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (opts?.includeBoundArrows && element.type === "arrow") {
|
|
||||||
const bindingId = element.startBinding?.elementId;
|
|
||||||
if (bindingId) {
|
|
||||||
const boundElement = Scene.getScene(element)?.getElement(bindingId);
|
|
||||||
if (boundElement?.frameId === frameId) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getElementsInResizingFrame = (
|
export const getElementsInResizingFrame = (
|
||||||
allElements: ExcalidrawElementsIncludingDeleted,
|
allElements: ExcalidrawElementsIncludingDeleted,
|
||||||
@@ -371,7 +242,7 @@ export const getElementsInResizingFrame = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (const element of elementsNotCompletelyInFrame) {
|
for (const element of elementsNotCompletelyInFrame) {
|
||||||
if (!FrameGeometry.isElementIntersectingFrame(element, frame)) {
|
if (!isElementIntersectingFrame(element, frame)) {
|
||||||
if (element.groupIds.length === 0) {
|
if (element.groupIds.length === 0) {
|
||||||
nextElementsInFrame.delete(element);
|
nextElementsInFrame.delete(element);
|
||||||
}
|
}
|
||||||
@@ -468,14 +339,6 @@ export const getContainingFrame = (
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isValidFrameChild = (element: ExcalidrawElement) => {
|
|
||||||
return (
|
|
||||||
element.type !== "frame" &&
|
|
||||||
// arrows that are bound to elements cannot be frame children
|
|
||||||
(element.type !== "arrow" || (!element.startBinding && !element.endBinding))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// --------------------------- Frame Operations -------------------------------
|
// --------------------------- Frame Operations -------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -488,20 +351,17 @@ export const addElementsToFrame = (
|
|||||||
elementsToAdd: NonDeletedExcalidrawElement[],
|
elementsToAdd: NonDeletedExcalidrawElement[],
|
||||||
frame: ExcalidrawFrameElement,
|
frame: ExcalidrawFrameElement,
|
||||||
) => {
|
) => {
|
||||||
const { allElementsIndexMap, currTargetFrameChildrenMap } =
|
const { currTargetFrameChildrenMap } = allElements.reduce(
|
||||||
allElements.reduce(
|
(acc, element, index) => {
|
||||||
(acc, element, index) => {
|
if (element.frameId === frame.id) {
|
||||||
acc.allElementsIndexMap.set(element.id, index);
|
acc.currTargetFrameChildrenMap.set(element.id, true);
|
||||||
if (element.frameId === frame.id) {
|
}
|
||||||
acc.currTargetFrameChildrenMap.set(element.id, true);
|
return acc;
|
||||||
}
|
},
|
||||||
return acc;
|
{
|
||||||
},
|
currTargetFrameChildrenMap: new Map<ExcalidrawElement["id"], true>(),
|
||||||
{
|
},
|
||||||
allElementsIndexMap: new Map<ExcalidrawElement["id"], number>(),
|
);
|
||||||
currTargetFrameChildrenMap: new Map<ExcalidrawElement["id"], true>(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const suppliedElementsToAddSet = new Set(elementsToAdd.map((el) => el.id));
|
const suppliedElementsToAddSet = new Set(elementsToAdd.map((el) => el.id));
|
||||||
|
|
||||||
@@ -514,9 +374,6 @@ export const addElementsToFrame = (
|
|||||||
elementsToAdd,
|
elementsToAdd,
|
||||||
)) {
|
)) {
|
||||||
if (!currTargetFrameChildrenMap.has(element.id)) {
|
if (!currTargetFrameChildrenMap.has(element.id)) {
|
||||||
if (!isValidFrameChild(element)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
finalElementsToAdd.push(element);
|
finalElementsToAdd.push(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,66 +387,6 @@ export const addElementsToFrame = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalElementsToAddSet = new Set(finalElementsToAdd.map((el) => el.id));
|
|
||||||
|
|
||||||
const nextElements: ExcalidrawElement[] = [];
|
|
||||||
|
|
||||||
const processedElements = new Set<ExcalidrawElement["id"]>();
|
|
||||||
|
|
||||||
for (const element of allElements) {
|
|
||||||
if (processedElements.has(element.id)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
processedElements.add(element.id);
|
|
||||||
|
|
||||||
if (
|
|
||||||
finalElementsToAddSet.has(element.id) ||
|
|
||||||
(element.frameId && element.frameId === frame.id)
|
|
||||||
) {
|
|
||||||
// will be added in bulk once we process target frame
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// target frame
|
|
||||||
if (element.id === frame.id) {
|
|
||||||
const currFrameChildren = getFrameElements(allElements, frame.id);
|
|
||||||
currFrameChildren.forEach((child) => {
|
|
||||||
processedElements.add(child.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
// if not found, add all children on top by assigning the lowest index
|
|
||||||
const targetFrameIndex = allElementsIndexMap.get(frame.id) ?? -1;
|
|
||||||
|
|
||||||
const { newChildren_left, newChildren_right } = finalElementsToAdd.reduce(
|
|
||||||
(acc, element) => {
|
|
||||||
// if index not found, add on top of current frame children
|
|
||||||
const elementIndex = allElementsIndexMap.get(element.id) ?? Infinity;
|
|
||||||
if (elementIndex < targetFrameIndex) {
|
|
||||||
acc.newChildren_left.push(element);
|
|
||||||
} else {
|
|
||||||
acc.newChildren_right.push(element);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
newChildren_left: [] as ExcalidrawElement[],
|
|
||||||
newChildren_right: [] as ExcalidrawElement[],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
nextElements.push(
|
|
||||||
...newChildren_left,
|
|
||||||
...currFrameChildren,
|
|
||||||
...newChildren_right,
|
|
||||||
element,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
nextElements.push(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const element of finalElementsToAdd) {
|
for (const element of finalElementsToAdd) {
|
||||||
mutateElement(
|
mutateElement(
|
||||||
element,
|
element,
|
||||||
@@ -599,8 +396,7 @@ export const addElementsToFrame = (
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return allElements.slice();
|
||||||
return nextElements;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeElementsFromFrame = (
|
export const removeElementsFromFrame = (
|
||||||
@@ -608,20 +404,34 @@ export const removeElementsFromFrame = (
|
|||||||
elementsToRemove: NonDeletedExcalidrawElement[],
|
elementsToRemove: NonDeletedExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => {
|
) => {
|
||||||
const _elementsToRemove: ExcalidrawElement[] = [];
|
const _elementsToRemove = new Map<
|
||||||
|
ExcalidrawElement["id"],
|
||||||
|
ExcalidrawElement
|
||||||
|
>();
|
||||||
|
|
||||||
|
const toRemoveElementsByFrame = new Map<
|
||||||
|
ExcalidrawFrameElement["id"],
|
||||||
|
ExcalidrawElement[]
|
||||||
|
>();
|
||||||
|
|
||||||
for (const element of elementsToRemove) {
|
for (const element of elementsToRemove) {
|
||||||
if (element.frameId) {
|
if (element.frameId) {
|
||||||
_elementsToRemove.push(element);
|
_elementsToRemove.set(element.id, element);
|
||||||
|
|
||||||
|
const arr = toRemoveElementsByFrame.get(element.frameId) || [];
|
||||||
|
arr.push(element);
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
_elementsToRemove.push(boundTextElement);
|
_elementsToRemove.set(boundTextElement.id, boundTextElement);
|
||||||
|
arr.push(boundTextElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toRemoveElementsByFrame.set(element.frameId, arr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const element of _elementsToRemove) {
|
for (const [, element] of _elementsToRemove) {
|
||||||
mutateElement(
|
mutateElement(
|
||||||
element,
|
element,
|
||||||
{
|
{
|
||||||
@@ -631,13 +441,7 @@ export const removeElementsFromFrame = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextElements = moveOneRight(
|
return allElements.slice();
|
||||||
allElements,
|
|
||||||
appState,
|
|
||||||
Array.from(_elementsToRemove),
|
|
||||||
);
|
|
||||||
|
|
||||||
return nextElements;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeAllElementsFromFrame = (
|
export const removeAllElementsFromFrame = (
|
||||||
|
|||||||
@@ -505,3 +505,7 @@ export const rangeIntersection = (
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isValueInRange = (value: number, min: number, max: number) => {
|
||||||
|
return value >= min && value <= max;
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { Bounds } from "../element/bounds";
|
||||||
|
import { Point } from "../types";
|
||||||
|
|
||||||
|
export type LineSegment = [Point, Point];
|
||||||
|
|
||||||
|
export function getBBox(line: LineSegment): Bounds {
|
||||||
|
return [
|
||||||
|
Math.min(line[0][0], line[1][0]),
|
||||||
|
Math.min(line[0][1], line[1][1]),
|
||||||
|
Math.max(line[0][0], line[1][0]),
|
||||||
|
Math.max(line[0][1], line[1][1]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function crossProduct(a: Point, b: Point) {
|
||||||
|
return a[0] * b[1] - b[0] * a[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doBBoxesIntersect(a: Bounds, b: Bounds) {
|
||||||
|
return a[0] <= b[2] && a[2] >= b[0] && a[1] <= b[3] && a[3] >= b[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function translate(a: Point, b: Point): Point {
|
||||||
|
return [a[0] - b[0], a[1] - b[1]];
|
||||||
|
}
|
||||||
|
|
||||||
|
const EPSILON = 0.000001;
|
||||||
|
|
||||||
|
export function isPointOnLine(l: LineSegment, p: Point) {
|
||||||
|
const p1 = translate(l[1], l[0]);
|
||||||
|
const p2 = translate(p, l[0]);
|
||||||
|
|
||||||
|
const r = crossProduct(p1, p2);
|
||||||
|
|
||||||
|
return Math.abs(r) < EPSILON;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPointRightOfLine(l: LineSegment, p: Point) {
|
||||||
|
const p1 = translate(l[1], l[0]);
|
||||||
|
const p2 = translate(p, l[0]);
|
||||||
|
|
||||||
|
return crossProduct(p1, p2) < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLineSegmentTouchingOrCrossingLine(
|
||||||
|
a: LineSegment,
|
||||||
|
b: LineSegment,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
isPointOnLine(a, b[0]) ||
|
||||||
|
isPointOnLine(a, b[1]) ||
|
||||||
|
(isPointRightOfLine(a, b[0])
|
||||||
|
? !isPointRightOfLine(a, b[1])
|
||||||
|
: isPointRightOfLine(a, b[1]))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://martin-thoma.com/how-to-check-if-two-line-segments-intersect/
|
||||||
|
export function doLineSegmentsIntersect(a: LineSegment, b: LineSegment) {
|
||||||
|
return (
|
||||||
|
doBBoxesIntersect(getBBox(a), getBBox(b)) &&
|
||||||
|
isLineSegmentTouchingOrCrossingLine(a, b) &&
|
||||||
|
isLineSegmentTouchingOrCrossingLine(b, a)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"path": "dist/excalidraw.production.min.js",
|
"path": "dist/excalidraw.production.min.js",
|
||||||
"limit": "305 kB"
|
"limit": "320 kB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "dist/excalidraw-assets/locales",
|
"path": "dist/excalidraw-assets/locales",
|
||||||
"name": "dist/excalidraw-assets/locales",
|
"name": "dist/excalidraw-assets/locales",
|
||||||
"limit": "270 kB"
|
"limit": "290 kB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "dist/excalidraw-assets/vendor-*.js",
|
"path": "dist/excalidraw-assets/vendor-*.js",
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ Please add the latest change on the top under the correct section.
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
- Add `selected` prop for `MainMenu.Item` and `MainMenu.ItemCustom` components to indicate active state. [7078](https://github.com/excalidraw/excalidraw/pull/7078)
|
- Export `elementsOverlappingBBox`, `isElementInsideBBox`, `elementPartiallyOverlapsWithOrContainsBBox` helpers for filtering/checking if elements within bounds. [#6727](https://github.com/excalidraw/excalidraw/pull/6727)
|
||||||
|
- Regenerate ids by default when using transform api and also update bindings by 0.5px to avoid possible overlapping [#7195](https://github.com/excalidraw/excalidraw/pull/7195)
|
||||||
|
- Add `selected` prop for `MainMenu.Item` and `MainMenu.ItemCustom` components to indicate active state. [#7078](https://github.com/excalidraw/excalidraw/pull/7078)
|
||||||
|
|
||||||
## 0.16.1 (2023-09-21)
|
## 0.16.1 (2023-09-21)
|
||||||
|
|
||||||
|
|||||||
@@ -254,3 +254,9 @@ export { DefaultSidebar } from "../../components/DefaultSidebar";
|
|||||||
|
|
||||||
export { normalizeLink } from "../../data/url";
|
export { normalizeLink } from "../../data/url";
|
||||||
export { convertToExcalidrawElements } from "../../data/transform";
|
export { convertToExcalidrawElements } from "../../data/transform";
|
||||||
|
|
||||||
|
export {
|
||||||
|
elementsOverlappingBBox,
|
||||||
|
isElementInsideBBox,
|
||||||
|
elementPartiallyOverlapsWithOrContainsBBox,
|
||||||
|
} from "../withinBounds";
|
||||||
|
|||||||
@@ -229,6 +229,12 @@ export const exportToClipboard = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export * from "./bbox";
|
||||||
|
export {
|
||||||
|
elementsOverlappingBBox,
|
||||||
|
isElementInsideBBox,
|
||||||
|
elementPartiallyOverlapsWithOrContainsBBox,
|
||||||
|
} from "./withinBounds";
|
||||||
export { serializeAsJSON, serializeLibraryAsJSON } from "../data/json";
|
export { serializeAsJSON, serializeLibraryAsJSON } from "../data/json";
|
||||||
export {
|
export {
|
||||||
loadFromBlob,
|
loadFromBlob,
|
||||||
|
|||||||
@@ -0,0 +1,262 @@
|
|||||||
|
import { Bounds } from "../element/bounds";
|
||||||
|
import { API } from "../tests/helpers/api";
|
||||||
|
import {
|
||||||
|
elementPartiallyOverlapsWithOrContainsBBox,
|
||||||
|
elementsOverlappingBBox,
|
||||||
|
isElementInsideBBox,
|
||||||
|
} from "./withinBounds";
|
||||||
|
|
||||||
|
const makeElement = (x: number, y: number, width: number, height: number) =>
|
||||||
|
API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeBBox = (
|
||||||
|
minX: number,
|
||||||
|
minY: number,
|
||||||
|
maxX: number,
|
||||||
|
maxY: number,
|
||||||
|
): Bounds => [minX, minY, maxX, maxY];
|
||||||
|
|
||||||
|
describe("isElementInsideBBox()", () => {
|
||||||
|
it("should return true if element is fully inside", () => {
|
||||||
|
const bbox = makeBBox(0, 0, 100, 100);
|
||||||
|
|
||||||
|
// bbox contains element
|
||||||
|
expect(isElementInsideBBox(makeElement(0, 0, 100, 100), bbox)).toBe(true);
|
||||||
|
expect(isElementInsideBBox(makeElement(10, 10, 90, 90), bbox)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if element is only partially overlapping", () => {
|
||||||
|
const bbox = makeBBox(0, 0, 100, 100);
|
||||||
|
|
||||||
|
// element contains bbox
|
||||||
|
expect(isElementInsideBBox(makeElement(-10, -10, 110, 110), bbox)).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// element overlaps bbox from top-left
|
||||||
|
expect(isElementInsideBBox(makeElement(-10, -10, 100, 100), bbox)).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
// element overlaps bbox from top-right
|
||||||
|
expect(isElementInsideBBox(makeElement(90, -10, 100, 100), bbox)).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
// element overlaps bbox from bottom-left
|
||||||
|
expect(isElementInsideBBox(makeElement(-10, 90, 100, 100), bbox)).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
// element overlaps bbox from bottom-right
|
||||||
|
expect(isElementInsideBBox(makeElement(90, 90, 100, 100), bbox)).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if element outside", () => {
|
||||||
|
const bbox = makeBBox(0, 0, 100, 100);
|
||||||
|
|
||||||
|
// outside diagonally
|
||||||
|
expect(isElementInsideBBox(makeElement(110, 110, 100, 100), bbox)).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// outside on the left
|
||||||
|
expect(isElementInsideBBox(makeElement(-110, 10, 50, 50), bbox)).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
// outside on the right
|
||||||
|
expect(isElementInsideBBox(makeElement(110, 10, 50, 50), bbox)).toBe(false);
|
||||||
|
// outside on the top
|
||||||
|
expect(isElementInsideBBox(makeElement(10, -110, 50, 50), bbox)).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
// outside on the bottom
|
||||||
|
expect(isElementInsideBBox(makeElement(10, 110, 50, 50), bbox)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true if bbox contains element and flag enabled", () => {
|
||||||
|
const bbox = makeBBox(0, 0, 100, 100);
|
||||||
|
|
||||||
|
// element contains bbox
|
||||||
|
expect(
|
||||||
|
isElementInsideBBox(makeElement(-10, -10, 110, 110), bbox, true),
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
// bbox contains element
|
||||||
|
expect(isElementInsideBBox(makeElement(0, 0, 100, 100), bbox)).toBe(true);
|
||||||
|
expect(isElementInsideBBox(makeElement(10, 10, 90, 90), bbox)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("elementPartiallyOverlapsWithOrContainsBBox()", () => {
|
||||||
|
it("should return true if element overlaps, is inside, or contains", () => {
|
||||||
|
const bbox = makeBBox(0, 0, 100, 100);
|
||||||
|
|
||||||
|
// bbox contains element
|
||||||
|
expect(
|
||||||
|
elementPartiallyOverlapsWithOrContainsBBox(
|
||||||
|
makeElement(0, 0, 100, 100),
|
||||||
|
bbox,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
elementPartiallyOverlapsWithOrContainsBBox(
|
||||||
|
makeElement(10, 10, 90, 90),
|
||||||
|
bbox,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
// element contains bbox
|
||||||
|
expect(
|
||||||
|
elementPartiallyOverlapsWithOrContainsBBox(
|
||||||
|
makeElement(-10, -10, 110, 110),
|
||||||
|
bbox,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
// element overlaps bbox from top-left
|
||||||
|
expect(
|
||||||
|
elementPartiallyOverlapsWithOrContainsBBox(
|
||||||
|
makeElement(-10, -10, 100, 100),
|
||||||
|
bbox,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
// element overlaps bbox from top-right
|
||||||
|
expect(
|
||||||
|
elementPartiallyOverlapsWithOrContainsBBox(
|
||||||
|
makeElement(90, -10, 100, 100),
|
||||||
|
bbox,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
// element overlaps bbox from bottom-left
|
||||||
|
expect(
|
||||||
|
elementPartiallyOverlapsWithOrContainsBBox(
|
||||||
|
makeElement(-10, 90, 100, 100),
|
||||||
|
bbox,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
// element overlaps bbox from bottom-right
|
||||||
|
expect(
|
||||||
|
elementPartiallyOverlapsWithOrContainsBBox(
|
||||||
|
makeElement(90, 90, 100, 100),
|
||||||
|
bbox,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if element does not overlap", () => {
|
||||||
|
const bbox = makeBBox(0, 0, 100, 100);
|
||||||
|
|
||||||
|
// outside diagonally
|
||||||
|
expect(
|
||||||
|
elementPartiallyOverlapsWithOrContainsBBox(
|
||||||
|
makeElement(110, 110, 100, 100),
|
||||||
|
bbox,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
// outside on the left
|
||||||
|
expect(
|
||||||
|
elementPartiallyOverlapsWithOrContainsBBox(
|
||||||
|
makeElement(-110, 10, 50, 50),
|
||||||
|
bbox,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
// outside on the right
|
||||||
|
expect(
|
||||||
|
elementPartiallyOverlapsWithOrContainsBBox(
|
||||||
|
makeElement(110, 10, 50, 50),
|
||||||
|
bbox,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
// outside on the top
|
||||||
|
expect(
|
||||||
|
elementPartiallyOverlapsWithOrContainsBBox(
|
||||||
|
makeElement(10, -110, 50, 50),
|
||||||
|
bbox,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
// outside on the bottom
|
||||||
|
expect(
|
||||||
|
elementPartiallyOverlapsWithOrContainsBBox(
|
||||||
|
makeElement(10, 110, 50, 50),
|
||||||
|
bbox,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("elementsOverlappingBBox()", () => {
|
||||||
|
it("should return elements that overlap bbox", () => {
|
||||||
|
const bbox = makeBBox(0, 0, 100, 100);
|
||||||
|
|
||||||
|
const rectOutside = makeElement(110, 110, 100, 100);
|
||||||
|
const rectInside = makeElement(10, 10, 90, 90);
|
||||||
|
const rectContainingBBox = makeElement(-10, -10, 110, 110);
|
||||||
|
const rectOverlappingTopLeft = makeElement(-10, -10, 50, 50);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
elementsOverlappingBBox({
|
||||||
|
bounds: bbox,
|
||||||
|
type: "overlap",
|
||||||
|
elements: [
|
||||||
|
rectOutside,
|
||||||
|
rectInside,
|
||||||
|
rectContainingBBox,
|
||||||
|
rectOverlappingTopLeft,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toEqual([rectInside, rectContainingBBox, rectOverlappingTopLeft]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return elements inside/containing bbox", () => {
|
||||||
|
const bbox = makeBBox(0, 0, 100, 100);
|
||||||
|
|
||||||
|
const rectOutside = makeElement(110, 110, 100, 100);
|
||||||
|
const rectInside = makeElement(10, 10, 90, 90);
|
||||||
|
const rectContainingBBox = makeElement(-10, -10, 110, 110);
|
||||||
|
const rectOverlappingTopLeft = makeElement(-10, -10, 50, 50);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
elementsOverlappingBBox({
|
||||||
|
bounds: bbox,
|
||||||
|
type: "contain",
|
||||||
|
elements: [
|
||||||
|
rectOutside,
|
||||||
|
rectInside,
|
||||||
|
rectContainingBBox,
|
||||||
|
rectOverlappingTopLeft,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toEqual([rectInside, rectContainingBBox]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return elements inside bbox", () => {
|
||||||
|
const bbox = makeBBox(0, 0, 100, 100);
|
||||||
|
|
||||||
|
const rectOutside = makeElement(110, 110, 100, 100);
|
||||||
|
const rectInside = makeElement(10, 10, 90, 90);
|
||||||
|
const rectContainingBBox = makeElement(-10, -10, 110, 110);
|
||||||
|
const rectOverlappingTopLeft = makeElement(-10, -10, 50, 50);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
elementsOverlappingBBox({
|
||||||
|
bounds: bbox,
|
||||||
|
type: "inside",
|
||||||
|
elements: [
|
||||||
|
rectOutside,
|
||||||
|
rectInside,
|
||||||
|
rectContainingBBox,
|
||||||
|
rectOverlappingTopLeft,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toEqual([rectInside]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO test linear, freedraw, and diamond element types (+rotated)
|
||||||
|
});
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import type {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawFreeDrawElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
} from "../element/types";
|
||||||
|
import {
|
||||||
|
isArrowElement,
|
||||||
|
isFreeDrawElement,
|
||||||
|
isLinearElement,
|
||||||
|
isTextElement,
|
||||||
|
} from "../element/typeChecks";
|
||||||
|
import { isValueInRange, rotatePoint } from "../math";
|
||||||
|
import type { Point } from "../types";
|
||||||
|
import { Bounds } from "../element/bounds";
|
||||||
|
|
||||||
|
type Element = NonDeletedExcalidrawElement;
|
||||||
|
type Elements = readonly NonDeletedExcalidrawElement[];
|
||||||
|
|
||||||
|
type Points = readonly Point[];
|
||||||
|
|
||||||
|
/** @returns vertices relative to element's top-left [0,0] position */
|
||||||
|
const getNonLinearElementRelativePoints = (
|
||||||
|
element: Exclude<
|
||||||
|
Element,
|
||||||
|
ExcalidrawLinearElement | ExcalidrawFreeDrawElement
|
||||||
|
>,
|
||||||
|
): [TopLeft: Point, TopRight: Point, BottomRight: Point, BottomLeft: Point] => {
|
||||||
|
if (element.type === "diamond") {
|
||||||
|
return [
|
||||||
|
[element.width / 2, 0],
|
||||||
|
[element.width, element.height / 2],
|
||||||
|
[element.width / 2, element.height],
|
||||||
|
[0, element.height / 2],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
[0, 0],
|
||||||
|
[0 + element.width, 0],
|
||||||
|
[0 + element.width, element.height],
|
||||||
|
[0, element.height],
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @returns vertices relative to element's top-left [0,0] position */
|
||||||
|
const getElementRelativePoints = (element: ExcalidrawElement): Points => {
|
||||||
|
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||||
|
return element.points;
|
||||||
|
}
|
||||||
|
return getNonLinearElementRelativePoints(element);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMinMaxPoints = (points: Points) => {
|
||||||
|
const ret = points.reduce(
|
||||||
|
(limits, [x, y]) => {
|
||||||
|
limits.minY = Math.min(limits.minY, y);
|
||||||
|
limits.minX = Math.min(limits.minX, x);
|
||||||
|
|
||||||
|
limits.maxX = Math.max(limits.maxX, x);
|
||||||
|
limits.maxY = Math.max(limits.maxY, y);
|
||||||
|
|
||||||
|
return limits;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
minX: Infinity,
|
||||||
|
minY: Infinity,
|
||||||
|
maxX: -Infinity,
|
||||||
|
maxY: -Infinity,
|
||||||
|
cx: 0,
|
||||||
|
cy: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ret.cx = (ret.maxX + ret.minX) / 2;
|
||||||
|
ret.cy = (ret.maxY + ret.minY) / 2;
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRotatedBBox = (element: Element): Bounds => {
|
||||||
|
const points = getElementRelativePoints(element);
|
||||||
|
|
||||||
|
const { cx, cy } = getMinMaxPoints(points);
|
||||||
|
const centerPoint: Point = [cx, cy];
|
||||||
|
|
||||||
|
const rotatedPoints = points.map((point) =>
|
||||||
|
rotatePoint([point[0], point[1]], centerPoint, element.angle),
|
||||||
|
);
|
||||||
|
const { minX, minY, maxX, maxY } = getMinMaxPoints(rotatedPoints);
|
||||||
|
|
||||||
|
return [
|
||||||
|
minX + element.x,
|
||||||
|
minY + element.y,
|
||||||
|
maxX + element.x,
|
||||||
|
maxY + element.y,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isElementInsideBBox = (
|
||||||
|
element: Element,
|
||||||
|
bbox: Bounds,
|
||||||
|
eitherDirection = false,
|
||||||
|
): boolean => {
|
||||||
|
const elementBBox = getRotatedBBox(element);
|
||||||
|
|
||||||
|
const elementInsideBbox =
|
||||||
|
bbox[0] <= elementBBox[0] &&
|
||||||
|
bbox[2] >= elementBBox[2] &&
|
||||||
|
bbox[1] <= elementBBox[1] &&
|
||||||
|
bbox[3] >= elementBBox[3];
|
||||||
|
|
||||||
|
if (!eitherDirection) {
|
||||||
|
return elementInsideBbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elementInsideBbox) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
elementBBox[0] <= bbox[0] &&
|
||||||
|
elementBBox[2] >= bbox[2] &&
|
||||||
|
elementBBox[1] <= bbox[1] &&
|
||||||
|
elementBBox[3] >= bbox[3]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const elementPartiallyOverlapsWithOrContainsBBox = (
|
||||||
|
element: Element,
|
||||||
|
bbox: Bounds,
|
||||||
|
): boolean => {
|
||||||
|
const elementBBox = getRotatedBBox(element);
|
||||||
|
|
||||||
|
return (
|
||||||
|
(isValueInRange(elementBBox[0], bbox[0], bbox[2]) ||
|
||||||
|
isValueInRange(bbox[0], elementBBox[0], elementBBox[2])) &&
|
||||||
|
(isValueInRange(elementBBox[1], bbox[1], bbox[3]) ||
|
||||||
|
isValueInRange(bbox[1], elementBBox[1], elementBBox[3]))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const elementsOverlappingBBox = ({
|
||||||
|
elements,
|
||||||
|
bounds,
|
||||||
|
type,
|
||||||
|
errorMargin = 0,
|
||||||
|
}: {
|
||||||
|
elements: Elements;
|
||||||
|
bounds: Bounds;
|
||||||
|
/** safety offset. Defaults to 0. */
|
||||||
|
errorMargin?: number;
|
||||||
|
/**
|
||||||
|
* - overlap: elements overlapping or inside bounds
|
||||||
|
* - contain: elements inside bounds or bounds inside elements
|
||||||
|
* - inside: elements inside bounds
|
||||||
|
**/
|
||||||
|
type: "overlap" | "contain" | "inside";
|
||||||
|
}) => {
|
||||||
|
const adjustedBBox: Bounds = [
|
||||||
|
bounds[0] - errorMargin,
|
||||||
|
bounds[1] - errorMargin,
|
||||||
|
bounds[2] + errorMargin,
|
||||||
|
bounds[3] + errorMargin,
|
||||||
|
];
|
||||||
|
|
||||||
|
const includedElementSet = new Set<string>();
|
||||||
|
|
||||||
|
for (const element of elements) {
|
||||||
|
if (includedElementSet.has(element.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOverlaping =
|
||||||
|
type === "overlap"
|
||||||
|
? elementPartiallyOverlapsWithOrContainsBBox(element, adjustedBBox)
|
||||||
|
: type === "inside"
|
||||||
|
? isElementInsideBBox(element, adjustedBBox)
|
||||||
|
: isElementInsideBBox(element, adjustedBBox, true);
|
||||||
|
|
||||||
|
if (isOverlaping) {
|
||||||
|
includedElementSet.add(element.id);
|
||||||
|
|
||||||
|
if (element.boundElements) {
|
||||||
|
for (const boundElement of element.boundElements) {
|
||||||
|
includedElementSet.add(boundElement.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTextElement(element) && element.containerId) {
|
||||||
|
includedElementSet.add(element.containerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isArrowElement(element)) {
|
||||||
|
if (element.startBinding) {
|
||||||
|
includedElementSet.add(element.startBinding.elementId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.endBinding) {
|
||||||
|
includedElementSet.add(element.endBinding?.elementId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements.filter((element) => includedElementSet.has(element.id));
|
||||||
|
};
|
||||||
@@ -69,7 +69,6 @@ import {
|
|||||||
} from "../element/Hyperlink";
|
} from "../element/Hyperlink";
|
||||||
import { renderSnaps } from "./renderSnaps";
|
import { renderSnaps } from "./renderSnaps";
|
||||||
import {
|
import {
|
||||||
isArrowElement,
|
|
||||||
isEmbeddableElement,
|
isEmbeddableElement,
|
||||||
isFrameElement,
|
isFrameElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
@@ -985,10 +984,7 @@ const _renderStaticScene = ({
|
|||||||
|
|
||||||
// TODO do we need to check isElementInFrame here?
|
// TODO do we need to check isElementInFrame here?
|
||||||
if (frame && isElementInFrame(element, elements, appState)) {
|
if (frame && isElementInFrame(element, elements, appState)) {
|
||||||
// do not clip arrows
|
frameClip(frame, context, renderConfig, appState);
|
||||||
if (!isArrowElement(element)) {
|
|
||||||
frameClip(frame, context, renderConfig, appState);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
renderElement(element, rc, context, renderConfig, appState);
|
renderElement(element, rc, context, renderConfig, appState);
|
||||||
context.restore();
|
context.restore();
|
||||||
|
|||||||
@@ -39,8 +39,6 @@ export const canChangeRoundness = (type: string) =>
|
|||||||
type === "line" ||
|
type === "line" ||
|
||||||
type === "diamond";
|
type === "diamond";
|
||||||
|
|
||||||
export const hasText = (type: string) => type === "text";
|
|
||||||
|
|
||||||
export const canHaveArrowheads = (type: string) => type === "arrow";
|
export const canHaveArrowheads = (type: string) => type === "arrow";
|
||||||
|
|
||||||
export const getElementAtPosition = (
|
export const getElementAtPosition = (
|
||||||
|
|||||||
+6
-2
@@ -1,6 +1,10 @@
|
|||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { getCommonBounds, getElementAbsoluteCoords } from "../element/bounds";
|
import {
|
||||||
|
Bounds,
|
||||||
|
getCommonBounds,
|
||||||
|
getElementAbsoluteCoords,
|
||||||
|
} from "../element/bounds";
|
||||||
import { renderSceneToSvg, renderStaticScene } from "../renderer/renderScene";
|
import { renderSceneToSvg, renderStaticScene } from "../renderer/renderScene";
|
||||||
import { distance, isOnlyExportingSingleFrame } from "../utils";
|
import { distance, isOnlyExportingSingleFrame } from "../utils";
|
||||||
import { AppState, BinaryFiles } from "../types";
|
import { AppState, BinaryFiles } from "../types";
|
||||||
@@ -221,7 +225,7 @@ export const exportToSvg = async (
|
|||||||
const getCanvasSize = (
|
const getCanvasSize = (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
exportPadding: number,
|
exportPadding: number,
|
||||||
): [number, number, number, number] => {
|
): Bounds => {
|
||||||
// we should decide if we are exporting the whole canvas
|
// we should decide if we are exporting the whole canvas
|
||||||
// if so, we are not clipping elements in the frame
|
// if so, we are not clipping elements in the frame
|
||||||
// and therefore, we should not do anything special
|
// and therefore, we should not do anything special
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export {
|
|||||||
canHaveArrowheads,
|
canHaveArrowheads,
|
||||||
canChangeRoundness,
|
canChangeRoundness,
|
||||||
getElementAtPosition,
|
getElementAtPosition,
|
||||||
hasText,
|
|
||||||
getElementsAtPosition,
|
getElementsAtPosition,
|
||||||
} from "./comparisons";
|
} from "./comparisons";
|
||||||
export { getNormalizedZoom } from "./zoom";
|
export { getNormalizedZoom } from "./zoom";
|
||||||
|
|||||||
@@ -14255,217 +14255,6 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
|
|||||||
|
|
||||||
exports[`regression tests > should group elements and ungroup them > [end of test] number of renders 1`] = `21`;
|
exports[`regression tests > should group elements and ungroup them > [end of test] number of renders 1`] = `21`;
|
||||||
|
|
||||||
exports[`regression tests > should show fill icons when element has non transparent background > [end of test] appState 1`] = `
|
|
||||||
{
|
|
||||||
"activeEmbeddable": null,
|
|
||||||
"activeTool": {
|
|
||||||
"customType": null,
|
|
||||||
"lastActiveTool": null,
|
|
||||||
"locked": false,
|
|
||||||
"type": "selection",
|
|
||||||
},
|
|
||||||
"collaborators": Map {},
|
|
||||||
"contextMenu": null,
|
|
||||||
"currentChartType": "bar",
|
|
||||||
"currentItemBackgroundColor": "#ffc9c9",
|
|
||||||
"currentItemEndArrowhead": "arrow",
|
|
||||||
"currentItemFillStyle": "solid",
|
|
||||||
"currentItemFontFamily": 1,
|
|
||||||
"currentItemFontSize": 20,
|
|
||||||
"currentItemOpacity": 100,
|
|
||||||
"currentItemRoughness": 1,
|
|
||||||
"currentItemRoundness": "round",
|
|
||||||
"currentItemStartArrowhead": null,
|
|
||||||
"currentItemStrokeColor": "#1e1e1e",
|
|
||||||
"currentItemStrokeStyle": "solid",
|
|
||||||
"currentItemStrokeWidth": 2,
|
|
||||||
"currentItemTextAlign": "left",
|
|
||||||
"cursorButton": "up",
|
|
||||||
"defaultSidebarDockedPreference": false,
|
|
||||||
"draggingElement": null,
|
|
||||||
"editingElement": null,
|
|
||||||
"editingFrame": null,
|
|
||||||
"editingGroupId": null,
|
|
||||||
"editingLinearElement": null,
|
|
||||||
"elementsToHighlight": null,
|
|
||||||
"errorMessage": null,
|
|
||||||
"exportBackground": true,
|
|
||||||
"exportEmbedScene": false,
|
|
||||||
"exportScale": 1,
|
|
||||||
"exportWithDarkMode": false,
|
|
||||||
"fileHandle": null,
|
|
||||||
"frameRendering": {
|
|
||||||
"clip": true,
|
|
||||||
"enabled": true,
|
|
||||||
"name": true,
|
|
||||||
"outline": true,
|
|
||||||
},
|
|
||||||
"frameToHighlight": null,
|
|
||||||
"gridSize": null,
|
|
||||||
"height": 768,
|
|
||||||
"isBindingEnabled": true,
|
|
||||||
"isLoading": false,
|
|
||||||
"isResizing": false,
|
|
||||||
"isRotating": false,
|
|
||||||
"lastPointerDownWith": "mouse",
|
|
||||||
"multiElement": null,
|
|
||||||
"name": "Untitled-201933152653",
|
|
||||||
"objectsSnapModeEnabled": false,
|
|
||||||
"offsetLeft": 0,
|
|
||||||
"offsetTop": 0,
|
|
||||||
"openDialog": null,
|
|
||||||
"openMenu": null,
|
|
||||||
"openPopup": "elementBackground",
|
|
||||||
"openSidebar": null,
|
|
||||||
"originSnapOffset": null,
|
|
||||||
"pasteDialog": {
|
|
||||||
"data": null,
|
|
||||||
"shown": false,
|
|
||||||
},
|
|
||||||
"penDetected": false,
|
|
||||||
"penMode": false,
|
|
||||||
"pendingImageElementId": null,
|
|
||||||
"previousSelectedElementIds": {},
|
|
||||||
"resizingElement": null,
|
|
||||||
"scrollX": 0,
|
|
||||||
"scrollY": 0,
|
|
||||||
"scrolledOutside": false,
|
|
||||||
"selectedElementIds": {
|
|
||||||
"id0": true,
|
|
||||||
},
|
|
||||||
"selectedElementsAreBeingDragged": false,
|
|
||||||
"selectedGroupIds": {},
|
|
||||||
"selectedLinearElement": null,
|
|
||||||
"selectionElement": null,
|
|
||||||
"shouldCacheIgnoreZoom": false,
|
|
||||||
"showHyperlinkPopup": false,
|
|
||||||
"showStats": false,
|
|
||||||
"showWelcomeScreen": true,
|
|
||||||
"snapLines": [],
|
|
||||||
"startBoundElement": null,
|
|
||||||
"suggestedBindings": [],
|
|
||||||
"theme": "light",
|
|
||||||
"toast": null,
|
|
||||||
"viewBackgroundColor": "#ffffff",
|
|
||||||
"viewModeEnabled": false,
|
|
||||||
"width": 1024,
|
|
||||||
"zenModeEnabled": false,
|
|
||||||
"zoom": {
|
|
||||||
"value": 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`regression tests > should show fill icons when element has non transparent background > [end of test] history 1`] = `
|
|
||||||
{
|
|
||||||
"recording": false,
|
|
||||||
"redoStack": [],
|
|
||||||
"stateHistory": [
|
|
||||||
{
|
|
||||||
"appState": {
|
|
||||||
"editingGroupId": null,
|
|
||||||
"editingLinearElement": null,
|
|
||||||
"name": "Untitled-201933152653",
|
|
||||||
"selectedElementIds": {},
|
|
||||||
"selectedGroupIds": {},
|
|
||||||
"viewBackgroundColor": "#ffffff",
|
|
||||||
},
|
|
||||||
"elements": [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appState": {
|
|
||||||
"editingGroupId": null,
|
|
||||||
"editingLinearElement": null,
|
|
||||||
"name": "Untitled-201933152653",
|
|
||||||
"selectedElementIds": {
|
|
||||||
"id0": true,
|
|
||||||
},
|
|
||||||
"selectedGroupIds": {},
|
|
||||||
"viewBackgroundColor": "#ffffff",
|
|
||||||
},
|
|
||||||
"elements": [
|
|
||||||
{
|
|
||||||
"angle": 0,
|
|
||||||
"backgroundColor": "transparent",
|
|
||||||
"boundElements": null,
|
|
||||||
"fillStyle": "solid",
|
|
||||||
"frameId": null,
|
|
||||||
"groupIds": [],
|
|
||||||
"height": 10,
|
|
||||||
"id": "id0",
|
|
||||||
"isDeleted": false,
|
|
||||||
"link": null,
|
|
||||||
"locked": false,
|
|
||||||
"opacity": 100,
|
|
||||||
"roughness": 1,
|
|
||||||
"roundness": {
|
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 1278240551,
|
|
||||||
"strokeColor": "#1e1e1e",
|
|
||||||
"strokeStyle": "solid",
|
|
||||||
"strokeWidth": 2,
|
|
||||||
"type": "rectangle",
|
|
||||||
"updated": 1,
|
|
||||||
"version": 2,
|
|
||||||
"versionNonce": 453191,
|
|
||||||
"width": 10,
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appState": {
|
|
||||||
"editingGroupId": null,
|
|
||||||
"editingLinearElement": null,
|
|
||||||
"name": "Untitled-201933152653",
|
|
||||||
"selectedElementIds": {
|
|
||||||
"id0": true,
|
|
||||||
},
|
|
||||||
"selectedGroupIds": {},
|
|
||||||
"viewBackgroundColor": "#ffffff",
|
|
||||||
},
|
|
||||||
"elements": [
|
|
||||||
{
|
|
||||||
"angle": 0,
|
|
||||||
"backgroundColor": "#ffc9c9",
|
|
||||||
"boundElements": null,
|
|
||||||
"fillStyle": "solid",
|
|
||||||
"frameId": null,
|
|
||||||
"groupIds": [],
|
|
||||||
"height": 10,
|
|
||||||
"id": "id0",
|
|
||||||
"isDeleted": false,
|
|
||||||
"link": null,
|
|
||||||
"locked": false,
|
|
||||||
"opacity": 100,
|
|
||||||
"roughness": 1,
|
|
||||||
"roundness": {
|
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 1278240551,
|
|
||||||
"strokeColor": "#1e1e1e",
|
|
||||||
"strokeStyle": "solid",
|
|
||||||
"strokeWidth": 2,
|
|
||||||
"type": "rectangle",
|
|
||||||
"updated": 1,
|
|
||||||
"version": 3,
|
|
||||||
"versionNonce": 2019559783,
|
|
||||||
"width": 10,
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`regression tests > should show fill icons when element has non transparent background > [end of test] number of elements 1`] = `0`;
|
|
||||||
|
|
||||||
exports[`regression tests > should show fill icons when element has non transparent background > [end of test] number of renders 1`] = `9`;
|
|
||||||
|
|
||||||
exports[`regression tests > single-clicking on a subgroup of a selected group should not alter selection > [end of test] appState 1`] = `
|
exports[`regression tests > single-clicking on a subgroup of a selected group should not alter selection > [end of test] appState 1`] = `
|
||||||
{
|
{
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export class API {
|
|||||||
angle?: number;
|
angle?: number;
|
||||||
id?: string;
|
id?: string;
|
||||||
isDeleted?: boolean;
|
isDeleted?: boolean;
|
||||||
frameId?: ExcalidrawElement["id"];
|
frameId?: ExcalidrawElement["id"] | null;
|
||||||
groupIds?: string[];
|
groupIds?: string[];
|
||||||
// generic element props
|
// generic element props
|
||||||
strokeColor?: ExcalidrawGenericElement["strokeColor"];
|
strokeColor?: ExcalidrawGenericElement["strokeColor"];
|
||||||
|
|||||||
@@ -1202,5 +1202,29 @@ describe("Test Linear Elements", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should not update label position when arrow dragged", () => {
|
||||||
|
createTwoPointerLinearElement("arrow");
|
||||||
|
let arrow = h.elements[0] as ExcalidrawLinearElement;
|
||||||
|
createBoundTextElement(DEFAULT_TEXT, arrow);
|
||||||
|
let label = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
|
expect(arrow.x).toBe(20);
|
||||||
|
expect(arrow.y).toBe(20);
|
||||||
|
expect(label.x).toBe(0);
|
||||||
|
expect(label.y).toBe(0);
|
||||||
|
mouse.reset();
|
||||||
|
mouse.select(arrow);
|
||||||
|
mouse.select(label);
|
||||||
|
mouse.downAt(arrow.x, arrow.y);
|
||||||
|
mouse.moveTo(arrow.x + 20, arrow.y + 30);
|
||||||
|
mouse.up(arrow.x + 20, arrow.y + 30);
|
||||||
|
|
||||||
|
arrow = h.elements[0] as ExcalidrawLinearElement;
|
||||||
|
label = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
|
expect(arrow.x).toBe(80);
|
||||||
|
expect(arrow.y).toBe(100);
|
||||||
|
expect(label.x).toBe(0);
|
||||||
|
expect(label.y).toBe(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -535,6 +535,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="color-picker__button active"
|
class="color-picker__button active"
|
||||||
|
data-testid="color-top-pick-#ffffff"
|
||||||
style="--swatch-color: #ffffff;"
|
style="--swatch-color: #ffffff;"
|
||||||
title="#ffffff"
|
title="#ffffff"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -545,6 +546,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="color-picker__button"
|
class="color-picker__button"
|
||||||
|
data-testid="color-top-pick-#f8f9fa"
|
||||||
style="--swatch-color: #f8f9fa;"
|
style="--swatch-color: #f8f9fa;"
|
||||||
title="#f8f9fa"
|
title="#f8f9fa"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -555,6 +557,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="color-picker__button"
|
class="color-picker__button"
|
||||||
|
data-testid="color-top-pick-#f5faff"
|
||||||
style="--swatch-color: #f5faff;"
|
style="--swatch-color: #f5faff;"
|
||||||
title="#f5faff"
|
title="#f5faff"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -565,6 +568,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="color-picker__button"
|
class="color-picker__button"
|
||||||
|
data-testid="color-top-pick-#fffce8"
|
||||||
style="--swatch-color: #fffce8;"
|
style="--swatch-color: #fffce8;"
|
||||||
title="#fffce8"
|
title="#fffce8"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -575,6 +579,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="color-picker__button"
|
class="color-picker__button"
|
||||||
|
data-testid="color-top-pick-#fdf8f6"
|
||||||
style="--swatch-color: #fdf8f6;"
|
style="--swatch-color: #fdf8f6;"
|
||||||
title="#fdf8f6"
|
title="#fdf8f6"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1089,20 +1089,6 @@ describe("regression tests", () => {
|
|||||||
});
|
});
|
||||||
assertSelectedElements(rect3);
|
assertSelectedElements(rect3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show fill icons when element has non transparent background", async () => {
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
expect(screen.queryByText(/fill/i)).not.toBeNull();
|
|
||||||
mouse.down();
|
|
||||||
mouse.up(10, 10);
|
|
||||||
expect(screen.queryByText(/fill/i)).toBeNull();
|
|
||||||
togglePopover("Background");
|
|
||||||
UI.clickOnTestId("color-red");
|
|
||||||
// select rectangle
|
|
||||||
mouse.reset();
|
|
||||||
mouse.click();
|
|
||||||
expect(screen.queryByText(/fill/i)).not.toBeNull();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it(
|
it(
|
||||||
|
|||||||
+300
-1
@@ -12,6 +12,11 @@ import {
|
|||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { API } from "./helpers/api";
|
import { API } from "./helpers/api";
|
||||||
import { selectGroupsForSelectedElements } from "../groups";
|
import { selectGroupsForSelectedElements } from "../groups";
|
||||||
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawFrameElement,
|
||||||
|
ExcalidrawSelectionElement,
|
||||||
|
} from "../element/types";
|
||||||
|
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
@@ -23,9 +28,15 @@ beforeEach(() => {
|
|||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
|
type ExcalidrawElementType = Exclude<
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawSelectionElement
|
||||||
|
>["type"];
|
||||||
|
|
||||||
const populateElements = (
|
const populateElements = (
|
||||||
elements: {
|
elements: {
|
||||||
id: string;
|
id: string;
|
||||||
|
type?: ExcalidrawElementType;
|
||||||
isDeleted?: boolean;
|
isDeleted?: boolean;
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
groupIds?: string[];
|
groupIds?: string[];
|
||||||
@@ -34,6 +45,7 @@ const populateElements = (
|
|||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
containerId?: string;
|
containerId?: string;
|
||||||
|
frameId?: ExcalidrawFrameElement["id"];
|
||||||
}[],
|
}[],
|
||||||
appState?: Partial<AppState>,
|
appState?: Partial<AppState>,
|
||||||
) => {
|
) => {
|
||||||
@@ -50,9 +62,11 @@ const populateElements = (
|
|||||||
width = 100,
|
width = 100,
|
||||||
height = 100,
|
height = 100,
|
||||||
containerId = null,
|
containerId = null,
|
||||||
|
frameId = null,
|
||||||
|
type,
|
||||||
}) => {
|
}) => {
|
||||||
const element = API.createElement({
|
const element = API.createElement({
|
||||||
type: containerId ? "text" : "rectangle",
|
type: type ?? (containerId ? "text" : "rectangle"),
|
||||||
id,
|
id,
|
||||||
isDeleted,
|
isDeleted,
|
||||||
x,
|
x,
|
||||||
@@ -61,6 +75,7 @@ const populateElements = (
|
|||||||
height,
|
height,
|
||||||
groupIds,
|
groupIds,
|
||||||
containerId,
|
containerId,
|
||||||
|
frameId: frameId || null,
|
||||||
});
|
});
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
selectedElementIds[element.id] = true;
|
selectedElementIds[element.id] = true;
|
||||||
@@ -116,6 +131,8 @@ const assertZindex = ({
|
|||||||
isSelected?: true;
|
isSelected?: true;
|
||||||
groupIds?: string[];
|
groupIds?: string[];
|
||||||
containerId?: string;
|
containerId?: string;
|
||||||
|
frameId?: ExcalidrawFrameElement["id"];
|
||||||
|
type?: ExcalidrawElementType;
|
||||||
}[];
|
}[];
|
||||||
appState?: Partial<AppState>;
|
appState?: Partial<AppState>;
|
||||||
operations: [Actions, string[]][];
|
operations: [Actions, string[]][];
|
||||||
@@ -1183,3 +1200,285 @@ describe("z-index manipulation", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("z-indexing with frames", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await render(<Excalidraw />);
|
||||||
|
});
|
||||||
|
|
||||||
|
// naming scheme:
|
||||||
|
// F# ... frame element
|
||||||
|
// F#_# ... frame child of F# (rectangle)
|
||||||
|
// R# ... unrelated element (rectangle)
|
||||||
|
|
||||||
|
it("moving whole frame by one (normalized)", () => {
|
||||||
|
// normalized frame order
|
||||||
|
assertZindex({
|
||||||
|
elements: [
|
||||||
|
{ id: "F1_1", frameId: "F1" },
|
||||||
|
{ id: "F1_2", frameId: "F1" },
|
||||||
|
{ id: "F1", type: "frame", isSelected: true },
|
||||||
|
{ id: "R1" },
|
||||||
|
{ id: "R2" },
|
||||||
|
],
|
||||||
|
operations: [
|
||||||
|
// +1
|
||||||
|
[actionBringForward, ["R1", "F1_1", "F1_2", "F1", "R2"]],
|
||||||
|
// +1
|
||||||
|
[actionBringForward, ["R1", "R2", "F1_1", "F1_2", "F1"]],
|
||||||
|
// noop
|
||||||
|
[actionBringForward, ["R1", "R2", "F1_1", "F1_2", "F1"]],
|
||||||
|
// -1
|
||||||
|
[actionSendBackward, ["R1", "F1_1", "F1_2", "F1", "R2"]],
|
||||||
|
// -1
|
||||||
|
[actionSendBackward, ["F1_1", "F1_2", "F1", "R1", "R2"]],
|
||||||
|
// noop
|
||||||
|
[actionSendBackward, ["F1_1", "F1_2", "F1", "R1", "R2"]],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("moving whole frame by one (DENORMALIZED)", () => {
|
||||||
|
// DENORMALIZED FRAME ORDER
|
||||||
|
assertZindex({
|
||||||
|
elements: [
|
||||||
|
{ id: "F1_1", frameId: "F1" },
|
||||||
|
{ id: "F1", type: "frame", isSelected: true },
|
||||||
|
{ id: "F1_2", frameId: "F1" },
|
||||||
|
{ id: "R1" },
|
||||||
|
{ id: "R2" },
|
||||||
|
],
|
||||||
|
operations: [
|
||||||
|
// +1
|
||||||
|
[actionBringForward, ["R1", "F1_1", "F1", "F1_2", "R2"]],
|
||||||
|
// +1
|
||||||
|
[actionBringForward, ["R1", "R2", "F1_1", "F1", "F1_2"]],
|
||||||
|
// noop
|
||||||
|
[actionBringForward, ["R1", "R2", "F1_1", "F1", "F1_2"]],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// DENORMALIZED FRAME ORDER
|
||||||
|
assertZindex({
|
||||||
|
elements: [
|
||||||
|
{ id: "F1_1", frameId: "F1" },
|
||||||
|
{ id: "F1", type: "frame", isSelected: true },
|
||||||
|
{ id: "R1" },
|
||||||
|
{ id: "F1_2", frameId: "F1" },
|
||||||
|
{ id: "R2" },
|
||||||
|
],
|
||||||
|
operations: [
|
||||||
|
// +1
|
||||||
|
[actionBringForward, ["R1", "F1_1", "F1", "R2", "F1_2"]],
|
||||||
|
// +1
|
||||||
|
[actionBringForward, ["R1", "R2", "F1_1", "F1", "F1_2"]],
|
||||||
|
// noop
|
||||||
|
[actionBringForward, ["R1", "R2", "F1_1", "F1", "F1_2"]],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// DENORMALIZED FRAME ORDER
|
||||||
|
assertZindex({
|
||||||
|
elements: [
|
||||||
|
{ id: "F1_1", frameId: "F1" },
|
||||||
|
{ id: "R1" },
|
||||||
|
{ id: "F1", type: "frame", isSelected: true },
|
||||||
|
{ id: "R2" },
|
||||||
|
{ id: "F1_2", frameId: "F1" },
|
||||||
|
{ id: "R3" },
|
||||||
|
],
|
||||||
|
operations: [
|
||||||
|
// +1
|
||||||
|
[actionBringForward, ["R1", "F1_1", "R2", "F1", "R3", "F1_2"]],
|
||||||
|
// +1
|
||||||
|
// FIXME incorrect, should put F1_1 after R3
|
||||||
|
[actionBringForward, ["R1", "R2", "F1_1", "R3", "F1", "F1_2"]],
|
||||||
|
// +1
|
||||||
|
// FIXME should be noop from previous step after it's fixed
|
||||||
|
[actionBringForward, ["R1", "R2", "R3", "F1_1", "F1", "F1_2"]],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// DENORMALIZED FRAME ORDER
|
||||||
|
assertZindex({
|
||||||
|
elements: [
|
||||||
|
{ id: "F1_1", frameId: "F1" },
|
||||||
|
{ id: "R1" },
|
||||||
|
{ id: "F1", type: "frame", isSelected: true },
|
||||||
|
{ id: "R2" },
|
||||||
|
{ id: "F1_2", frameId: "F1" },
|
||||||
|
{ id: "R3" },
|
||||||
|
],
|
||||||
|
operations: [
|
||||||
|
// -1
|
||||||
|
[actionSendBackward, ["F1_1", "F1", "R1", "F1_2", "R2", "R3"]],
|
||||||
|
// -1
|
||||||
|
[actionSendBackward, ["F1_1", "F1", "F1_2", "R1", "R2", "R3"]],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("moving selected frame children by one (normalized)", () => {
|
||||||
|
// normalized frame order
|
||||||
|
assertZindex({
|
||||||
|
elements: [
|
||||||
|
{ id: "F1_1", frameId: "F1", isSelected: true },
|
||||||
|
{ id: "F1_2", frameId: "F1" },
|
||||||
|
{ id: "F1", type: "frame" },
|
||||||
|
{ id: "R1" },
|
||||||
|
],
|
||||||
|
operations: [
|
||||||
|
// +1
|
||||||
|
[actionBringForward, ["F1_2", "F1_1", "F1", "R1"]],
|
||||||
|
// noop
|
||||||
|
[actionBringForward, ["F1_2", "F1_1", "F1", "R1"]],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// normalized frame order, multiple frames
|
||||||
|
assertZindex({
|
||||||
|
elements: [
|
||||||
|
{ id: "F1_1", frameId: "F1", isSelected: true },
|
||||||
|
{ id: "F1_2", frameId: "F1" },
|
||||||
|
{ id: "F1", type: "frame" },
|
||||||
|
{ id: "R1" },
|
||||||
|
{ id: "F2_1", frameId: "F2", isSelected: true },
|
||||||
|
{ id: "F2_2", frameId: "F2" },
|
||||||
|
{ id: "F2", type: "frame" },
|
||||||
|
{ id: "R2" },
|
||||||
|
],
|
||||||
|
operations: [
|
||||||
|
// +1
|
||||||
|
[
|
||||||
|
actionBringForward,
|
||||||
|
["F1_2", "F1_1", "F1", "R1", "F2_2", "F2_1", "F2", "R2"],
|
||||||
|
],
|
||||||
|
// noop
|
||||||
|
[
|
||||||
|
actionBringForward,
|
||||||
|
["F1_2", "F1_1", "F1", "R1", "F2_2", "F2_1", "F2", "R2"],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("moving selected frame children by one (DENORMALIZED)", () => {
|
||||||
|
// DENORMALIZED FRAME ORDER
|
||||||
|
assertZindex({
|
||||||
|
elements: [
|
||||||
|
{ id: "F1_1", frameId: "F1", isSelected: true },
|
||||||
|
{ id: "F1", type: "frame" },
|
||||||
|
{ id: "F1_2", frameId: "F1" },
|
||||||
|
{ id: "R1" },
|
||||||
|
],
|
||||||
|
operations: [
|
||||||
|
// +1
|
||||||
|
// NOTE not sure what we wanna do here
|
||||||
|
[actionBringForward, ["F1", "F1_2", "F1_1", "R1"]],
|
||||||
|
// noop
|
||||||
|
[actionBringForward, ["F1", "F1_2", "F1_1", "R1"]],
|
||||||
|
// -1
|
||||||
|
[actionSendBackward, ["F1", "F1_1", "F1_2", "R1"]],
|
||||||
|
// noop
|
||||||
|
[actionSendBackward, ["F1", "F1_1", "F1_2", "R1"]],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// DENORMALIZED FRAME ORDER
|
||||||
|
assertZindex({
|
||||||
|
elements: [
|
||||||
|
{ id: "F1_1", frameId: "F1", isSelected: true },
|
||||||
|
{ id: "R1" },
|
||||||
|
{ id: "F1", type: "frame" },
|
||||||
|
{ id: "F1_2", frameId: "F1" },
|
||||||
|
{ id: "R2" },
|
||||||
|
],
|
||||||
|
operations: [
|
||||||
|
// +1
|
||||||
|
// NOTE not sure what we wanna do here
|
||||||
|
[actionBringForward, ["R1", "F1", "F1_2", "F1_1", "R2"]],
|
||||||
|
// noop
|
||||||
|
[actionBringForward, ["R1", "F1", "F1_2", "F1_1", "R2"]],
|
||||||
|
// -1
|
||||||
|
[actionSendBackward, ["R1", "F1", "F1_1", "F1_2", "R2"]],
|
||||||
|
// noop
|
||||||
|
[actionSendBackward, ["R1", "F1", "F1_1", "F1_2", "R2"]],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("moving whole frame to front/end", () => {
|
||||||
|
// normalized frame order
|
||||||
|
assertZindex({
|
||||||
|
elements: [
|
||||||
|
{ id: "F1_1", frameId: "F1" },
|
||||||
|
{ id: "F1_2", frameId: "F1" },
|
||||||
|
{ id: "F1", type: "frame", isSelected: true },
|
||||||
|
{ id: "R1" },
|
||||||
|
{ id: "R2" },
|
||||||
|
],
|
||||||
|
operations: [
|
||||||
|
// +∞
|
||||||
|
[actionBringToFront, ["R1", "R2", "F1_1", "F1_2", "F1"]],
|
||||||
|
// noop
|
||||||
|
[actionBringToFront, ["R1", "R2", "F1_1", "F1_2", "F1"]],
|
||||||
|
// -∞
|
||||||
|
[actionSendToBack, ["F1_1", "F1_2", "F1", "R1", "R2"]],
|
||||||
|
// noop
|
||||||
|
[actionSendToBack, ["F1_1", "F1_2", "F1", "R1", "R2"]],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// DENORMALIZED FRAME ORDER
|
||||||
|
assertZindex({
|
||||||
|
elements: [
|
||||||
|
{ id: "F1_1", frameId: "F1" },
|
||||||
|
{ id: "F1", type: "frame", isSelected: true },
|
||||||
|
{ id: "F1_2", frameId: "F1" },
|
||||||
|
{ id: "R1" },
|
||||||
|
{ id: "R2" },
|
||||||
|
],
|
||||||
|
operations: [
|
||||||
|
// +∞
|
||||||
|
[actionBringToFront, ["R1", "R2", "F1_1", "F1", "F1_2"]],
|
||||||
|
// noop
|
||||||
|
[actionBringToFront, ["R1", "R2", "F1_1", "F1", "F1_2"]],
|
||||||
|
// -∞
|
||||||
|
[actionSendToBack, ["F1_1", "F1", "F1_2", "R1", "R2"]],
|
||||||
|
// noop
|
||||||
|
[actionSendToBack, ["F1_1", "F1", "F1_2", "R1", "R2"]],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// DENORMALIZED FRAME ORDER
|
||||||
|
assertZindex({
|
||||||
|
elements: [
|
||||||
|
{ id: "F1_1", frameId: "F1" },
|
||||||
|
{ id: "F1", type: "frame", isSelected: true },
|
||||||
|
{ id: "R1" },
|
||||||
|
{ id: "F1_2", frameId: "F1" },
|
||||||
|
{ id: "R2" },
|
||||||
|
],
|
||||||
|
operations: [
|
||||||
|
// +∞
|
||||||
|
[actionBringToFront, ["R1", "R2", "F1_1", "F1", "F1_2"]],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// DENORMALIZED FRAME ORDER
|
||||||
|
assertZindex({
|
||||||
|
elements: [
|
||||||
|
{ id: "F1_1", frameId: "F1" },
|
||||||
|
{ id: "R1" },
|
||||||
|
{ id: "F1", type: "frame", isSelected: true },
|
||||||
|
{ id: "R2" },
|
||||||
|
{ id: "F1_2", frameId: "F1" },
|
||||||
|
{ id: "R3" },
|
||||||
|
],
|
||||||
|
operations: [
|
||||||
|
// +1
|
||||||
|
[actionBringToFront, ["R1", "R2", "R3", "F1_1", "F1", "F1_2"]],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -695,3 +695,12 @@ export type KeyboardModifiersObject = {
|
|||||||
altKey: boolean;
|
altKey: boolean;
|
||||||
metaKey: boolean;
|
metaKey: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Primitive =
|
||||||
|
| number
|
||||||
|
| string
|
||||||
|
| boolean
|
||||||
|
| bigint
|
||||||
|
| symbol
|
||||||
|
| null
|
||||||
|
| undefined;
|
||||||
|
|||||||
+167
-109
@@ -1,16 +1,14 @@
|
|||||||
import { bumpVersion } from "./element/mutateElement";
|
import { bumpVersion } from "./element/mutateElement";
|
||||||
import { isFrameElement } from "./element/typeChecks";
|
import { isFrameElement } from "./element/typeChecks";
|
||||||
import { ExcalidrawElement } from "./element/types";
|
import { ExcalidrawElement, ExcalidrawFrameElement } from "./element/types";
|
||||||
import { groupByFrames } from "./frame";
|
|
||||||
import { getElementsInGroup } from "./groups";
|
import { getElementsInGroup } from "./groups";
|
||||||
import { getSelectedElements } from "./scene";
|
import { getSelectedElements } from "./scene";
|
||||||
import Scene from "./scene/Scene";
|
import Scene from "./scene/Scene";
|
||||||
import { AppState } from "./types";
|
import { AppState } from "./types";
|
||||||
import { arrayToMap, findIndex, findLastIndex } from "./utils";
|
import { arrayToMap, findIndex, findLastIndex } from "./utils";
|
||||||
|
|
||||||
// elements that do not belong to a frame are considered a root element
|
const isOfTargetFrame = (element: ExcalidrawElement, frameId: string) => {
|
||||||
const isRootElement = (element: ExcalidrawElement) => {
|
return element.frameId === frameId || element.id === frameId;
|
||||||
return !element.frameId;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,6 +33,7 @@ const getIndicesToMove = (
|
|||||||
? elementsToBeMoved
|
? elementsToBeMoved
|
||||||
: getSelectedElements(elements, appState, {
|
: getSelectedElements(elements, appState, {
|
||||||
includeBoundTextElement: true,
|
includeBoundTextElement: true,
|
||||||
|
includeElementsInFrames: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
while (++index < elements.length) {
|
while (++index < elements.length) {
|
||||||
@@ -106,6 +105,26 @@ const getTargetIndexAccountingForBinding = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getContiguousFrameRangeElements = (
|
||||||
|
allElements: readonly ExcalidrawElement[],
|
||||||
|
frameId: ExcalidrawFrameElement["id"],
|
||||||
|
) => {
|
||||||
|
let rangeStart = -1;
|
||||||
|
let rangeEnd = -1;
|
||||||
|
allElements.forEach((element, index) => {
|
||||||
|
if (isOfTargetFrame(element, frameId)) {
|
||||||
|
if (rangeStart === -1) {
|
||||||
|
rangeStart = index;
|
||||||
|
}
|
||||||
|
rangeEnd = index;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (rangeStart === -1) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return allElements.slice(rangeStart, rangeEnd + 1);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns next candidate index that's available to be moved to. Currently that
|
* Returns next candidate index that's available to be moved to. Currently that
|
||||||
* is a non-deleted element, and not inside a group (unless we're editing it).
|
* is a non-deleted element, and not inside a group (unless we're editing it).
|
||||||
@@ -115,6 +134,11 @@ const getTargetIndex = (
|
|||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
boundaryIndex: number,
|
boundaryIndex: number,
|
||||||
direction: "left" | "right",
|
direction: "left" | "right",
|
||||||
|
/**
|
||||||
|
* Frame id if moving frame children.
|
||||||
|
* If whole frame (including all children) is being moved, supply `null`.
|
||||||
|
*/
|
||||||
|
containingFrame: ExcalidrawFrameElement["id"] | null,
|
||||||
) => {
|
) => {
|
||||||
const sourceElement = elements[boundaryIndex];
|
const sourceElement = elements[boundaryIndex];
|
||||||
|
|
||||||
@@ -122,6 +146,9 @@ const getTargetIndex = (
|
|||||||
if (element.isDeleted) {
|
if (element.isDeleted) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (containingFrame) {
|
||||||
|
return element.frameId === containingFrame;
|
||||||
|
}
|
||||||
// if we're editing group, find closest sibling irrespective of whether
|
// if we're editing group, find closest sibling irrespective of whether
|
||||||
// there's a different-group element between them (for legacy reasons)
|
// there's a different-group element between them (for legacy reasons)
|
||||||
if (appState.editingGroupId) {
|
if (appState.editingGroupId) {
|
||||||
@@ -132,8 +159,12 @@ const getTargetIndex = (
|
|||||||
|
|
||||||
const candidateIndex =
|
const candidateIndex =
|
||||||
direction === "left"
|
direction === "left"
|
||||||
? findLastIndex(elements, indexFilter, Math.max(0, boundaryIndex - 1))
|
? findLastIndex(
|
||||||
: findIndex(elements, indexFilter, boundaryIndex + 1);
|
elements,
|
||||||
|
(el) => indexFilter(el),
|
||||||
|
Math.max(0, boundaryIndex - 1),
|
||||||
|
)
|
||||||
|
: findIndex(elements, (el) => indexFilter(el), boundaryIndex + 1);
|
||||||
|
|
||||||
const nextElement = elements[candidateIndex];
|
const nextElement = elements[candidateIndex];
|
||||||
|
|
||||||
@@ -156,6 +187,19 @@ const getTargetIndex = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!containingFrame &&
|
||||||
|
(nextElement.frameId || nextElement.type === "frame")
|
||||||
|
) {
|
||||||
|
const frameElements = getContiguousFrameRangeElements(
|
||||||
|
elements,
|
||||||
|
nextElement.frameId || nextElement.id,
|
||||||
|
);
|
||||||
|
return direction === "left"
|
||||||
|
? elements.indexOf(frameElements[0])
|
||||||
|
: elements.indexOf(frameElements[frameElements.length - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
if (!nextElement.groupIds.length) {
|
if (!nextElement.groupIds.length) {
|
||||||
return (
|
return (
|
||||||
getTargetIndexAccountingForBinding(nextElement, elements, direction) ??
|
getTargetIndexAccountingForBinding(nextElement, elements, direction) ??
|
||||||
@@ -195,13 +239,12 @@ const getTargetElementsMap = <T extends ExcalidrawElement>(
|
|||||||
}, {} as Record<string, ExcalidrawElement>);
|
}, {} as Record<string, ExcalidrawElement>);
|
||||||
};
|
};
|
||||||
|
|
||||||
const _shiftElements = (
|
const shiftElementsByOne = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
direction: "left" | "right",
|
direction: "left" | "right",
|
||||||
elementsToBeMoved?: readonly ExcalidrawElement[],
|
|
||||||
) => {
|
) => {
|
||||||
const indicesToMove = getIndicesToMove(elements, appState, elementsToBeMoved);
|
const indicesToMove = getIndicesToMove(elements, appState);
|
||||||
const targetElementsMap = getTargetElementsMap(elements, indicesToMove);
|
const targetElementsMap = getTargetElementsMap(elements, indicesToMove);
|
||||||
let groupedIndices = toContiguousGroups(indicesToMove);
|
let groupedIndices = toContiguousGroups(indicesToMove);
|
||||||
|
|
||||||
@@ -209,16 +252,30 @@ const _shiftElements = (
|
|||||||
groupedIndices = groupedIndices.reverse();
|
groupedIndices = groupedIndices.reverse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectedFrames = new Set<ExcalidrawFrameElement["id"]>(
|
||||||
|
indicesToMove
|
||||||
|
.filter((idx) => elements[idx].type === "frame")
|
||||||
|
.map((idx) => elements[idx].id),
|
||||||
|
);
|
||||||
|
|
||||||
groupedIndices.forEach((indices, i) => {
|
groupedIndices.forEach((indices, i) => {
|
||||||
const leadingIndex = indices[0];
|
const leadingIndex = indices[0];
|
||||||
const trailingIndex = indices[indices.length - 1];
|
const trailingIndex = indices[indices.length - 1];
|
||||||
const boundaryIndex = direction === "left" ? leadingIndex : trailingIndex;
|
const boundaryIndex = direction === "left" ? leadingIndex : trailingIndex;
|
||||||
|
|
||||||
|
const containingFrame = indices.some((idx) => {
|
||||||
|
const el = elements[idx];
|
||||||
|
return el.frameId && selectedFrames.has(el.frameId);
|
||||||
|
})
|
||||||
|
? null
|
||||||
|
: elements[boundaryIndex]?.frameId;
|
||||||
|
|
||||||
const targetIndex = getTargetIndex(
|
const targetIndex = getTargetIndex(
|
||||||
appState,
|
appState,
|
||||||
elements,
|
elements,
|
||||||
boundaryIndex,
|
boundaryIndex,
|
||||||
direction,
|
direction,
|
||||||
|
containingFrame,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (targetIndex === -1 || boundaryIndex === targetIndex) {
|
if (targetIndex === -1 || boundaryIndex === targetIndex) {
|
||||||
@@ -263,34 +320,25 @@ const _shiftElements = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const shiftElements = (
|
const shiftElementsToEnd = (
|
||||||
appState: AppState,
|
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
direction: "left" | "right",
|
direction: "left" | "right",
|
||||||
|
containingFrame: ExcalidrawFrameElement["id"] | null,
|
||||||
elementsToBeMoved?: readonly ExcalidrawElement[],
|
elementsToBeMoved?: readonly ExcalidrawElement[],
|
||||||
) => {
|
) => {
|
||||||
return shift(
|
const indicesToMove = getIndicesToMove(elements, appState, elementsToBeMoved);
|
||||||
elements,
|
|
||||||
appState,
|
|
||||||
direction,
|
|
||||||
_shiftElements,
|
|
||||||
elementsToBeMoved,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const _shiftElementsToEnd = (
|
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
appState: AppState,
|
|
||||||
direction: "left" | "right",
|
|
||||||
) => {
|
|
||||||
const indicesToMove = getIndicesToMove(elements, appState);
|
|
||||||
const targetElementsMap = getTargetElementsMap(elements, indicesToMove);
|
const targetElementsMap = getTargetElementsMap(elements, indicesToMove);
|
||||||
const displacedElements: ExcalidrawElement[] = [];
|
const displacedElements: ExcalidrawElement[] = [];
|
||||||
|
|
||||||
let leadingIndex: number;
|
let leadingIndex: number;
|
||||||
let trailingIndex: number;
|
let trailingIndex: number;
|
||||||
if (direction === "left") {
|
if (direction === "left") {
|
||||||
if (appState.editingGroupId) {
|
if (containingFrame) {
|
||||||
|
leadingIndex = findIndex(elements, (el) =>
|
||||||
|
isOfTargetFrame(el, containingFrame),
|
||||||
|
);
|
||||||
|
} else if (appState.editingGroupId) {
|
||||||
const groupElements = getElementsInGroup(
|
const groupElements = getElementsInGroup(
|
||||||
elements,
|
elements,
|
||||||
appState.editingGroupId,
|
appState.editingGroupId,
|
||||||
@@ -305,7 +353,11 @@ const _shiftElementsToEnd = (
|
|||||||
|
|
||||||
trailingIndex = indicesToMove[indicesToMove.length - 1];
|
trailingIndex = indicesToMove[indicesToMove.length - 1];
|
||||||
} else {
|
} else {
|
||||||
if (appState.editingGroupId) {
|
if (containingFrame) {
|
||||||
|
trailingIndex = findLastIndex(elements, (el) =>
|
||||||
|
isOfTargetFrame(el, containingFrame),
|
||||||
|
);
|
||||||
|
} else if (appState.editingGroupId) {
|
||||||
const groupElements = getElementsInGroup(
|
const groupElements = getElementsInGroup(
|
||||||
elements,
|
elements,
|
||||||
appState.editingGroupId,
|
appState.editingGroupId,
|
||||||
@@ -321,6 +373,10 @@ const _shiftElementsToEnd = (
|
|||||||
leadingIndex = indicesToMove[0];
|
leadingIndex = indicesToMove[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (leadingIndex === -1) {
|
||||||
|
leadingIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
for (let index = leadingIndex; index < trailingIndex + 1; index++) {
|
for (let index = leadingIndex; index < trailingIndex + 1; index++) {
|
||||||
if (!indicesToMove.includes(index)) {
|
if (!indicesToMove.includes(index)) {
|
||||||
displacedElements.push(elements[index]);
|
displacedElements.push(elements[index]);
|
||||||
@@ -349,121 +405,123 @@ const _shiftElementsToEnd = (
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
const shiftElementsToEnd = (
|
function shiftElementsAccountingForFrames(
|
||||||
elements: readonly ExcalidrawElement[],
|
allElements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
|
||||||
direction: "left" | "right",
|
|
||||||
elementsToBeMoved?: readonly ExcalidrawElement[],
|
|
||||||
) => {
|
|
||||||
return shift(
|
|
||||||
elements,
|
|
||||||
appState,
|
|
||||||
direction,
|
|
||||||
_shiftElementsToEnd,
|
|
||||||
elementsToBeMoved,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function shift(
|
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
direction: "left" | "right",
|
direction: "left" | "right",
|
||||||
shiftFunction: (
|
shiftFunction: (
|
||||||
elements: ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
direction: "left" | "right",
|
direction: "left" | "right",
|
||||||
|
containingFrame: ExcalidrawFrameElement["id"] | null,
|
||||||
elementsToBeMoved?: readonly ExcalidrawElement[],
|
elementsToBeMoved?: readonly ExcalidrawElement[],
|
||||||
) => ExcalidrawElement[] | readonly ExcalidrawElement[],
|
) => ExcalidrawElement[] | readonly ExcalidrawElement[],
|
||||||
elementsToBeMoved?: readonly ExcalidrawElement[],
|
|
||||||
) {
|
) {
|
||||||
const elementsMap = arrayToMap(elements);
|
const elementsToMove = arrayToMap(
|
||||||
const frameElementsMap = groupByFrames(elements);
|
getSelectedElements(allElements, appState, {
|
||||||
|
includeBoundTextElement: true,
|
||||||
// in case root is non-existent, we promote children elements to root
|
includeElementsInFrames: true,
|
||||||
let rootElements = elements.filter(
|
}),
|
||||||
(element) =>
|
|
||||||
isRootElement(element) ||
|
|
||||||
(element.frameId && !elementsMap.has(element.frameId)),
|
|
||||||
);
|
);
|
||||||
// and remove non-existet root
|
|
||||||
for (const frameId of frameElementsMap.keys()) {
|
const frameAwareContiguousElementsToMove: {
|
||||||
if (!elementsMap.has(frameId)) {
|
regularElements: ExcalidrawElement[];
|
||||||
frameElementsMap.delete(frameId);
|
frameChildren: Map<ExcalidrawFrameElement["id"], ExcalidrawElement[]>;
|
||||||
|
} = { regularElements: [], frameChildren: new Map() };
|
||||||
|
|
||||||
|
const fullySelectedFrames = new Set<ExcalidrawFrameElement["id"]>();
|
||||||
|
|
||||||
|
for (const element of allElements) {
|
||||||
|
if (elementsToMove.has(element.id) && isFrameElement(element)) {
|
||||||
|
fullySelectedFrames.add(element.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// shift the root elements first
|
for (const element of allElements) {
|
||||||
rootElements = shiftFunction(
|
if (elementsToMove.has(element.id)) {
|
||||||
rootElements,
|
if (
|
||||||
|
isFrameElement(element) ||
|
||||||
|
(element.frameId && fullySelectedFrames.has(element.frameId))
|
||||||
|
) {
|
||||||
|
frameAwareContiguousElementsToMove.regularElements.push(element);
|
||||||
|
} else if (!element.frameId) {
|
||||||
|
frameAwareContiguousElementsToMove.regularElements.push(element);
|
||||||
|
} else {
|
||||||
|
const frameChildren =
|
||||||
|
frameAwareContiguousElementsToMove.frameChildren.get(
|
||||||
|
element.frameId,
|
||||||
|
) || [];
|
||||||
|
frameChildren.push(element);
|
||||||
|
frameAwareContiguousElementsToMove.frameChildren.set(
|
||||||
|
element.frameId,
|
||||||
|
frameChildren,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextElements = allElements;
|
||||||
|
|
||||||
|
const frameChildrenSets = Array.from(
|
||||||
|
frameAwareContiguousElementsToMove.frameChildren.entries(),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const [frameId, children] of frameChildrenSets) {
|
||||||
|
nextElements = shiftFunction(
|
||||||
|
allElements,
|
||||||
|
appState,
|
||||||
|
direction,
|
||||||
|
frameId,
|
||||||
|
children,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return shiftFunction(
|
||||||
|
nextElements,
|
||||||
appState,
|
appState,
|
||||||
direction,
|
direction,
|
||||||
elementsToBeMoved,
|
null,
|
||||||
) as ExcalidrawElement[];
|
frameAwareContiguousElementsToMove.regularElements,
|
||||||
|
);
|
||||||
// shift the elements in frames if needed
|
|
||||||
frameElementsMap.forEach((frameElements, frameId) => {
|
|
||||||
if (!appState.selectedElementIds[frameId]) {
|
|
||||||
frameElementsMap.set(
|
|
||||||
frameId,
|
|
||||||
shiftFunction(
|
|
||||||
frameElements,
|
|
||||||
appState,
|
|
||||||
direction,
|
|
||||||
elementsToBeMoved,
|
|
||||||
) as ExcalidrawElement[],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// return the final elements
|
|
||||||
let finalElements: ExcalidrawElement[] = [];
|
|
||||||
|
|
||||||
rootElements.forEach((element) => {
|
|
||||||
if (isFrameElement(element)) {
|
|
||||||
finalElements = [
|
|
||||||
...finalElements,
|
|
||||||
...(frameElementsMap.get(element.id) ?? []),
|
|
||||||
element,
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
finalElements = [...finalElements, element];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return finalElements;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// public API
|
// public API
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
export const moveOneLeft = (
|
export const moveOneLeft = (
|
||||||
elements: readonly ExcalidrawElement[],
|
allElements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
elementsToBeMoved?: readonly ExcalidrawElement[],
|
|
||||||
) => {
|
) => {
|
||||||
return shiftElements(appState, elements, "left", elementsToBeMoved);
|
return shiftElementsByOne(allElements, appState, "left");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const moveOneRight = (
|
export const moveOneRight = (
|
||||||
elements: readonly ExcalidrawElement[],
|
allElements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
elementsToBeMoved?: readonly ExcalidrawElement[],
|
|
||||||
) => {
|
) => {
|
||||||
return shiftElements(appState, elements, "right", elementsToBeMoved);
|
return shiftElementsByOne(allElements, appState, "right");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const moveAllLeft = (
|
export const moveAllLeft = (
|
||||||
elements: readonly ExcalidrawElement[],
|
allElements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
elementsToBeMoved?: readonly ExcalidrawElement[],
|
|
||||||
) => {
|
) => {
|
||||||
return shiftElementsToEnd(elements, appState, "left", elementsToBeMoved);
|
return shiftElementsAccountingForFrames(
|
||||||
|
allElements,
|
||||||
|
appState,
|
||||||
|
"left",
|
||||||
|
shiftElementsToEnd,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const moveAllRight = (
|
export const moveAllRight = (
|
||||||
elements: readonly ExcalidrawElement[],
|
allElements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
elementsToBeMoved?: readonly ExcalidrawElement[],
|
|
||||||
) => {
|
) => {
|
||||||
return shiftElementsToEnd(elements, appState, "right", elementsToBeMoved);
|
return shiftElementsAccountingForFrames(
|
||||||
|
allElements,
|
||||||
|
appState,
|
||||||
|
"right",
|
||||||
|
shiftElementsToEnd,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user