Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0bf1b4c4af | |||
| d9d1a3ab99 | |||
| c43d9d44f0 | |||
| c9af72e2ee | |||
| 18a7b97515 | |||
| e8def8da8d | |||
| a7db41c5ba | |||
| d8166d9e1d | |||
| 81c0259041 | |||
| f5c91c3a0f | |||
| 9b8de8a12e | |||
| ea677d4581 | |||
| ec2de7205f | |||
| d5e3f436dc | |||
| dcf4592e79 | |||
| d1f8eec174 | |||
| 0f81c30276 | |||
| f098789d16 | |||
| f794b0bb90 | |||
| 104f64f1dc | |||
| 71ad3c5356 |
@@ -25,6 +25,9 @@
|
|||||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/LICENSE">
|
<a href="https://github.com/excalidraw/excalidraw/blob/master/LICENSE">
|
||||||
<img alt="Excalidraw is released under the MIT license." src="https://img.shields.io/badge/license-MIT-blue.svg" />
|
<img alt="Excalidraw is released under the MIT license." src="https://img.shields.io/badge/license-MIT-blue.svg" />
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://www.npmjs.com/package/@excalidraw/excalidraw">
|
||||||
|
<img alt="npm downloads/month" src="https://img.shields.io/npm/dm/@excalidraw/excalidraw" />
|
||||||
|
</a>
|
||||||
<a href="https://docs.excalidraw.com/docs/introduction/contributing">
|
<a href="https://docs.excalidraw.com/docs/introduction/contributing">
|
||||||
<img alt="PRs welcome!" src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" />
|
<img alt="PRs welcome!" src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" />
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -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.
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*
|
*
|
||||||
* - DataState refers to full state of the app: appState, elements, images,
|
* - DataState refers to full state of the app: appState, elements, images,
|
||||||
* though some state is saved separately (collab username, library) for one
|
* though some state is saved separately (collab username, library) for one
|
||||||
* reason or another. We also save different data to different sotrage
|
* reason or another. We also save different data to different storage
|
||||||
* (localStorage, indexedDB).
|
* (localStorage, indexedDB).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -131,5 +131,5 @@ export class Debug {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
//@ts-ignore
|
||||||
window.debug = Debug;
|
window.debug = Debug;
|
||||||
|
|||||||
+3
-2
@@ -20,6 +20,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braintree/sanitize-url": "6.0.2",
|
"@braintree/sanitize-url": "6.0.2",
|
||||||
|
"@excalidraw/mermaid-to-excalidraw": "0.1.2",
|
||||||
"@excalidraw/laser-pointer": "1.2.0",
|
"@excalidraw/laser-pointer": "1.2.0",
|
||||||
"@excalidraw/random-username": "1.0.0",
|
"@excalidraw/random-username": "1.0.0",
|
||||||
"@radix-ui/react-popover": "1.0.3",
|
"@radix-ui/react-popover": "1.0.3",
|
||||||
@@ -53,7 +54,7 @@
|
|||||||
"pwacompat": "2.0.17",
|
"pwacompat": "2.0.17",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"roughjs": "4.6.5",
|
"roughjs": "4.6.4",
|
||||||
"sass": "1.51.0",
|
"sass": "1.51.0",
|
||||||
"socket.io-client": "2.3.1",
|
"socket.io-client": "2.3.1",
|
||||||
"tunnel-rat": "0.1.2"
|
"tunnel-rat": "0.1.2"
|
||||||
@@ -125,7 +126,7 @@
|
|||||||
"test": "yarn test:app",
|
"test": "yarn test:app",
|
||||||
"test:coverage": "vitest --coverage",
|
"test:coverage": "vitest --coverage",
|
||||||
"test:coverage:watch": "vitest --coverage --watch",
|
"test:coverage:watch": "vitest --coverage --watch",
|
||||||
"test:ui": "yarn test --ui",
|
"test:ui": "yarn test --ui --coverage.enabled=true",
|
||||||
"autorelease": "node scripts/autorelease.js",
|
"autorelease": "node scripts/autorelease.js",
|
||||||
"prerelease": "node scripts/prerelease.js",
|
"prerelease": "node scripts/prerelease.js",
|
||||||
"build:preview": "yarn build && vite preview --port 5000",
|
"build:preview": "yarn build && vite preview --port 5000",
|
||||||
|
|||||||
@@ -3,33 +3,43 @@ import { register } from "./register";
|
|||||||
import {
|
import {
|
||||||
copyTextToSystemClipboard,
|
copyTextToSystemClipboard,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
|
createPasteEvent,
|
||||||
probablySupportsClipboardBlob,
|
probablySupportsClipboardBlob,
|
||||||
probablySupportsClipboardWriteText,
|
probablySupportsClipboardWriteText,
|
||||||
|
readSystemClipboard,
|
||||||
} from "../clipboard";
|
} from "../clipboard";
|
||||||
import { actionDeleteSelected } from "./actionDeleteSelected";
|
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, event: ClipboardEvent | null, 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, 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.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 +48,55 @@ 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);
|
let types;
|
||||||
|
try {
|
||||||
|
types = await readSystemClipboard();
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.name === "AbortError" || error.name === "NotAllowedError") {
|
||||||
|
// user probably aborted the action. Though not 100% sure, it's best
|
||||||
|
// to not annoy them with an error message.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`actionPaste ${error.name}: ${error.message}`);
|
||||||
|
|
||||||
|
if (isFirefox) {
|
||||||
|
return {
|
||||||
|
commitToHistory: false,
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
errorMessage: t("hints.firefox_clipboard_write"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
commitToHistory: false,
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
errorMessage: t("errors.asyncPasteFailedOnRead"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
app.pasteFromClipboard(createPasteEvent({ types }));
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
return {
|
||||||
|
commitToHistory: false,
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
errorMessage: t("errors.asyncPasteFailedOnParse"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
||||||
@@ -55,13 +105,10 @@ export const actionPaste = register({
|
|||||||
export const actionCut = register({
|
export const actionCut = register({
|
||||||
name: "cut",
|
name: "cut",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState, data, app) => {
|
perform: (elements, appState, event: ClipboardEvent | null, app) => {
|
||||||
actionCopy.perform(elements, appState, data, app);
|
actionCopy.perform(elements, appState, event, app);
|
||||||
return actionDeleteSelected.perform(elements, appState);
|
return actionDeleteSelected.perform(elements, appState);
|
||||||
},
|
},
|
||||||
predicate: (elements, appState, appProps, app) => {
|
|
||||||
return app.device.isMobile && !!navigator.clipboard;
|
|
||||||
},
|
|
||||||
contextItemLabel: "labels.cut",
|
contextItemLabel: "labels.cut",
|
||||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.X,
|
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.X,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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([
|
||||||
|
|||||||
@@ -10,17 +10,12 @@ const shouldLock = (elements: readonly ExcalidrawElement[]) =>
|
|||||||
export const actionToggleElementLock = register({
|
export const actionToggleElementLock = register({
|
||||||
name: "toggleElementLock",
|
name: "toggleElementLock",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: (elements, appState, _, app) => {
|
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
|
||||||
return !selectedElements.some(
|
|
||||||
(element) => element.locked && element.frameId,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
|
// Frames and their children should not be selected at the same time.
|
||||||
|
// Therefore, there's no need to include elements in frame in the selection.
|
||||||
const selectedElements = app.scene.getSelectedElements({
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
selectedElementIds: appState.selectedElementIds,
|
selectedElementIds: appState.selectedElementIds,
|
||||||
includeBoundTextElement: true,
|
includeBoundTextElement: true,
|
||||||
includeElementsInFrames: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!selectedElements.length) {
|
if (!selectedElements.length) {
|
||||||
@@ -31,7 +26,12 @@ export const actionToggleElementLock = register({
|
|||||||
const selectedElementsMap = arrayToMap(selectedElements);
|
const selectedElementsMap = arrayToMap(selectedElements);
|
||||||
return {
|
return {
|
||||||
elements: elements.map((element) => {
|
elements: elements.map((element) => {
|
||||||
if (!selectedElementsMap.has(element.id)) {
|
if (
|
||||||
|
!selectedElementsMap.has(element.id) &&
|
||||||
|
(!element.frameId ||
|
||||||
|
// lock frame children if frame is selected
|
||||||
|
(element.frameId && !selectedElementsMap.has(element.frameId)))
|
||||||
|
) {
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 })}
|
||||||
|
|||||||
@@ -119,10 +119,10 @@ export class ActionManager {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
executeAction(
|
executeAction<T extends Action>(
|
||||||
action: Action,
|
action: T,
|
||||||
source: ActionSource = "api",
|
source: ActionSource = "api",
|
||||||
value: any = null,
|
value: Parameters<T["perform"]>[2] = null,
|
||||||
) {
|
) {
|
||||||
const elements = this.getElementsIncludingDeleted();
|
const elements = this.getElementsIncludingDeleted();
|
||||||
const appState = this.getAppState();
|
const appState = this.getAppState();
|
||||||
|
|||||||
+184
-10
@@ -1,22 +1,196 @@
|
|||||||
import { parseClipboard } from "./clipboard";
|
import {
|
||||||
import { createPasteEvent } from "./tests/test-utils";
|
createPasteEvent,
|
||||||
|
parseClipboard,
|
||||||
|
serializeAsClipboardJSON,
|
||||||
|
} from "./clipboard";
|
||||||
|
import { API } from "./tests/helpers/api";
|
||||||
|
|
||||||
describe("Test parseClipboard", () => {
|
describe("parseClipboard()", () => {
|
||||||
it("should parse valid json correctly", async () => {
|
it("should parse JSON as plaintext if not excalidraw-api/clipboard data", async () => {
|
||||||
let text = "123";
|
let text;
|
||||||
|
let clipboardData;
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
let clipboardData = await parseClipboard(
|
text = "123";
|
||||||
createPasteEvent({ "text/plain": text }),
|
clipboardData = await parseClipboard(
|
||||||
|
createPasteEvent({ types: { "text/plain": text } }),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(clipboardData.text).toBe(text);
|
expect(clipboardData.text).toBe(text);
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
text = "[123]";
|
text = "[123]";
|
||||||
|
|
||||||
clipboardData = await parseClipboard(
|
clipboardData = await parseClipboard(
|
||||||
createPasteEvent({ "text/plain": text }),
|
createPasteEvent({ types: { "text/plain": text } }),
|
||||||
);
|
);
|
||||||
|
expect(clipboardData.text).toBe(text);
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
text = JSON.stringify({ val: 42 });
|
||||||
|
clipboardData = await parseClipboard(
|
||||||
|
createPasteEvent({ types: { "text/plain": text } }),
|
||||||
|
);
|
||||||
expect(clipboardData.text).toBe(text);
|
expect(clipboardData.text).toBe(text);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should parse valid excalidraw JSON if inside text/plain", async () => {
|
||||||
|
const rect = API.createElement({ type: "rectangle" });
|
||||||
|
|
||||||
|
const json = serializeAsClipboardJSON({ elements: [rect], files: null });
|
||||||
|
const clipboardData = await parseClipboard(
|
||||||
|
createPasteEvent({
|
||||||
|
types: {
|
||||||
|
"text/plain": json,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(clipboardData.elements).toEqual([rect]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse valid excalidraw JSON if inside text/html", async () => {
|
||||||
|
const rect = API.createElement({ type: "rectangle" });
|
||||||
|
|
||||||
|
let json;
|
||||||
|
let clipboardData;
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
json = serializeAsClipboardJSON({ elements: [rect], files: null });
|
||||||
|
clipboardData = await parseClipboard(
|
||||||
|
createPasteEvent({
|
||||||
|
types: {
|
||||||
|
"text/html": json,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(clipboardData.elements).toEqual([rect]);
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
json = serializeAsClipboardJSON({ elements: [rect], files: null });
|
||||||
|
clipboardData = await parseClipboard(
|
||||||
|
createPasteEvent({
|
||||||
|
types: {
|
||||||
|
"text/html": `<div> ${json}</div>`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(clipboardData.elements).toEqual([rect]);
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse <image> `src` urls out of text/html", async () => {
|
||||||
|
let clipboardData;
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
clipboardData = await parseClipboard(
|
||||||
|
createPasteEvent({
|
||||||
|
types: {
|
||||||
|
"text/html": `<img src="https://example.com/image.png" />`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(clipboardData.mixedContent).toEqual([
|
||||||
|
{
|
||||||
|
type: "imageUrl",
|
||||||
|
value: "https://example.com/image.png",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
clipboardData = await parseClipboard(
|
||||||
|
createPasteEvent({
|
||||||
|
types: {
|
||||||
|
"text/html": `<div><img src="https://example.com/image.png" /></div><a><img src="https://example.com/image2.png" /></a>`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(clipboardData.mixedContent).toEqual([
|
||||||
|
{
|
||||||
|
type: "imageUrl",
|
||||||
|
value: "https://example.com/image.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "imageUrl",
|
||||||
|
value: "https://example.com/image2.png",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse text content alongside <image> `src` urls out of text/html", async () => {
|
||||||
|
const clipboardData = await parseClipboard(
|
||||||
|
createPasteEvent({
|
||||||
|
types: {
|
||||||
|
"text/html": `<a href="https://example.com">hello </a><div><img src="https://example.com/image.png" /></div><b>my friend!</b>`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(clipboardData.mixedContent).toEqual([
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
// trimmed
|
||||||
|
value: "hello",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "imageUrl",
|
||||||
|
value: "https://example.com/image.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
value: "my friend!",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse spreadsheet from either text/plain and text/html", async () => {
|
||||||
|
let clipboardData;
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
clipboardData = await parseClipboard(
|
||||||
|
createPasteEvent({
|
||||||
|
types: {
|
||||||
|
"text/plain": `a b
|
||||||
|
1 2
|
||||||
|
4 5
|
||||||
|
7 10`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(clipboardData.spreadsheet).toEqual({
|
||||||
|
title: "b",
|
||||||
|
labels: ["1", "4", "7"],
|
||||||
|
values: [2, 5, 10],
|
||||||
|
});
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
clipboardData = await parseClipboard(
|
||||||
|
createPasteEvent({
|
||||||
|
types: {
|
||||||
|
"text/html": `a b
|
||||||
|
1 2
|
||||||
|
4 5
|
||||||
|
7 10`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(clipboardData.spreadsheet).toEqual({
|
||||||
|
title: "b",
|
||||||
|
labels: ["1", "4", "7"],
|
||||||
|
values: [2, 5, 10],
|
||||||
|
});
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
clipboardData = await parseClipboard(
|
||||||
|
createPasteEvent({
|
||||||
|
types: {
|
||||||
|
"text/html": `<html>
|
||||||
|
<body>
|
||||||
|
<!--StartFragment--><google-sheets-html-origin><style type="text/css"><!--td {border: 1px solid #cccccc;}br {mso-data-placement:same-cell;}--></style><table xmlns="http://www.w3.org/1999/xhtml" cellspacing="0" cellpadding="0" dir="ltr" border="1" style="table-layout:fixed;font-size:10pt;font-family:Arial;width:0px;border-collapse:collapse;border:none"><colgroup><col width="100"/><col width="100"/></colgroup><tbody><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{"1":2,"2":"a"}">a</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{"1":2,"2":"b"}">b</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":1}">1</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":2}">2</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":4}">4</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":5}">5</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":7}">7</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":10}">10</td></tr></tbody></table><!--EndFragment-->
|
||||||
|
</body>
|
||||||
|
</html>`,
|
||||||
|
"text/plain": `a b
|
||||||
|
1 2
|
||||||
|
4 5
|
||||||
|
7 10`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(clipboardData.spreadsheet).toEqual({
|
||||||
|
title: "b",
|
||||||
|
labels: ["1", "4", "7"],
|
||||||
|
values: [2, 5, 10],
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+205
-94
@@ -3,14 +3,18 @@ import {
|
|||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import { BinaryFiles } from "./types";
|
import { BinaryFiles } from "./types";
|
||||||
import { SVG_EXPORT_TAG } from "./scene/export";
|
|
||||||
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
||||||
import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants";
|
import {
|
||||||
|
ALLOWED_PASTE_MIME_TYPES,
|
||||||
|
EXPORT_DATA_TYPES,
|
||||||
|
MIME_TYPES,
|
||||||
|
} from "./constants";
|
||||||
import { isInitializedImageElement } from "./element/typeChecks";
|
import { isInitializedImageElement } from "./element/typeChecks";
|
||||||
import { deepCopyElement } from "./element/newElement";
|
import { deepCopyElement } from "./element/newElement";
|
||||||
import { mutateElement } from "./element/mutateElement";
|
import { mutateElement } from "./element/mutateElement";
|
||||||
import { getContainingFrame } from "./frame";
|
import { getContainingFrame } from "./frame";
|
||||||
import { isPromiseLike, isTestEnv } from "./utils";
|
import { isMemberOf, isPromiseLike } from "./utils";
|
||||||
|
import { t } from "./i18n";
|
||||||
|
|
||||||
type ElementsClipboard = {
|
type ElementsClipboard = {
|
||||||
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
|
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
|
||||||
@@ -30,8 +34,11 @@ export interface ClipboardData {
|
|||||||
programmaticAPI?: boolean;
|
programmaticAPI?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let CLIPBOARD = "";
|
type AllowedPasteMimeTypes = typeof ALLOWED_PASTE_MIME_TYPES[number];
|
||||||
let PREFER_APP_CLIPBOARD = false;
|
|
||||||
|
type ParsedClipboardEvent =
|
||||||
|
| { type: "text"; value: string }
|
||||||
|
| { type: "mixedContent"; value: PastedMixedContent };
|
||||||
|
|
||||||
export const probablySupportsClipboardReadText =
|
export const probablySupportsClipboardReadText =
|
||||||
"clipboard" in navigator && "readText" in navigator.clipboard;
|
"clipboard" in navigator && "readText" in navigator.clipboard;
|
||||||
@@ -61,10 +68,61 @@ const clipboardContainsElements = (
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const copyToClipboard = async (
|
export const createPasteEvent = ({
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
types,
|
||||||
files: BinaryFiles | null,
|
files,
|
||||||
) => {
|
}: {
|
||||||
|
types?: { [key in AllowedPasteMimeTypes]?: string };
|
||||||
|
files?: File[];
|
||||||
|
}) => {
|
||||||
|
if (!types && !files) {
|
||||||
|
console.warn("createPasteEvent: no types or files provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = new ClipboardEvent("paste", {
|
||||||
|
clipboardData: new DataTransfer(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (types) {
|
||||||
|
for (const [type, value] of Object.entries(types)) {
|
||||||
|
try {
|
||||||
|
event.clipboardData?.setData(type, value);
|
||||||
|
if (event.clipboardData?.getData(type) !== value) {
|
||||||
|
throw new Error(`Failed to set "${type}" as clipboardData item`);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files) {
|
||||||
|
let idx = -1;
|
||||||
|
for (const file of files) {
|
||||||
|
idx++;
|
||||||
|
try {
|
||||||
|
event.clipboardData?.items.add(file);
|
||||||
|
if (event.clipboardData?.files[idx] !== file) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to set file "${file.name}" as clipboardData item`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return event;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const serializeAsClipboardJSON = ({
|
||||||
|
elements,
|
||||||
|
files,
|
||||||
|
}: {
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
files: BinaryFiles | null;
|
||||||
|
}) => {
|
||||||
const framesToCopy = new Set(
|
const framesToCopy = new Set(
|
||||||
elements.filter((element) => element.type === "frame"),
|
elements.filter((element) => element.type === "frame"),
|
||||||
);
|
);
|
||||||
@@ -86,7 +144,7 @@ export const copyToClipboard = async (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// select binded text elements when copying
|
// select bound text elements when copying
|
||||||
const contents: ElementsClipboard = {
|
const contents: ElementsClipboard = {
|
||||||
type: EXPORT_DATA_TYPES.excalidrawClipboard,
|
type: EXPORT_DATA_TYPES.excalidrawClipboard,
|
||||||
elements: elements.map((element) => {
|
elements: elements.map((element) => {
|
||||||
@@ -105,34 +163,20 @@ export const copyToClipboard = async (
|
|||||||
}),
|
}),
|
||||||
files: files ? _files : undefined,
|
files: files ? _files : undefined,
|
||||||
};
|
};
|
||||||
const json = JSON.stringify(contents);
|
|
||||||
|
|
||||||
if (isTestEnv()) {
|
return JSON.stringify(contents);
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
CLIPBOARD = json;
|
|
||||||
|
|
||||||
try {
|
|
||||||
PREFER_APP_CLIPBOARD = false;
|
|
||||||
await copyTextToSystemClipboard(json);
|
|
||||||
} catch (error: any) {
|
|
||||||
PREFER_APP_CLIPBOARD = true;
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAppClipboard = (): Partial<ElementsClipboard> => {
|
export const copyToClipboard = async (
|
||||||
if (!CLIPBOARD) {
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
return {};
|
files: BinaryFiles | null,
|
||||||
}
|
/** supply if available to make the operation more certain to succeed */
|
||||||
|
clipboardEvent?: ClipboardEvent | null,
|
||||||
try {
|
) => {
|
||||||
return JSON.parse(CLIPBOARD);
|
await copyTextToSystemClipboard(
|
||||||
} catch (error: any) {
|
serializeAsClipboardJSON({ elements, files }),
|
||||||
console.error(error);
|
clipboardEvent,
|
||||||
return {};
|
);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const parsePotentialSpreadsheet = (
|
const parsePotentialSpreadsheet = (
|
||||||
@@ -166,7 +210,9 @@ function parseHTMLTree(el: ChildNode) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const maybeParseHTMLPaste = (event: ClipboardEvent) => {
|
const maybeParseHTMLPaste = (
|
||||||
|
event: ClipboardEvent,
|
||||||
|
): { type: "mixedContent"; value: PastedMixedContent } | null => {
|
||||||
const html = event.clipboardData?.getData("text/html");
|
const html = event.clipboardData?.getData("text/html");
|
||||||
|
|
||||||
if (!html) {
|
if (!html) {
|
||||||
@@ -179,7 +225,7 @@ const maybeParseHTMLPaste = (event: ClipboardEvent) => {
|
|||||||
const content = parseHTMLTree(doc.body);
|
const content = parseHTMLTree(doc.body);
|
||||||
|
|
||||||
if (content.length) {
|
if (content.length) {
|
||||||
return content;
|
return { type: "mixedContent", value: content };
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(`error in parseHTMLFromPaste: ${error.message}`);
|
console.error(`error in parseHTMLFromPaste: ${error.message}`);
|
||||||
@@ -188,27 +234,88 @@ const maybeParseHTMLPaste = (event: ClipboardEvent) => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const readSystemClipboard = async () => {
|
||||||
|
const types: { [key in AllowedPasteMimeTypes]?: string } = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (navigator.clipboard?.readText) {
|
||||||
|
return { "text/plain": await navigator.clipboard?.readText() };
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// @ts-ignore
|
||||||
|
if (navigator.clipboard?.read) {
|
||||||
|
console.warn(
|
||||||
|
`navigator.clipboard.readText() failed (${error.message}). Failling back to navigator.clipboard.read()`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let clipboardItems: ClipboardItems;
|
||||||
|
|
||||||
|
try {
|
||||||
|
clipboardItems = await navigator.clipboard?.read();
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.name === "DataError") {
|
||||||
|
console.warn(
|
||||||
|
`navigator.clipboard.read() error, clipboard is probably empty: ${error.message}`,
|
||||||
|
);
|
||||||
|
return types;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of clipboardItems) {
|
||||||
|
for (const type of item.types) {
|
||||||
|
if (!isMemberOf(ALLOWED_PASTE_MIME_TYPES, type)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
types[type] = await (await item.getType(type)).text();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.warn(
|
||||||
|
`Cannot retrieve ${type} from clipboardItem: ${error.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(types).length === 0) {
|
||||||
|
console.warn("No clipboard data found from clipboard.read().");
|
||||||
|
return types;
|
||||||
|
}
|
||||||
|
|
||||||
|
return types;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves content from system clipboard (either from ClipboardEvent or
|
* Parses "paste" ClipboardEvent.
|
||||||
* via async clipboard API if supported)
|
|
||||||
*/
|
*/
|
||||||
const getSystemClipboard = async (
|
const parseClipboardEvent = async (
|
||||||
event: ClipboardEvent | null,
|
event: ClipboardEvent,
|
||||||
isPlainPaste = false,
|
isPlainPaste = false,
|
||||||
): Promise<
|
): Promise<ParsedClipboardEvent> => {
|
||||||
| { type: "text"; value: string }
|
|
||||||
| { type: "mixedContent"; value: PastedMixedContent }
|
|
||||||
> => {
|
|
||||||
try {
|
try {
|
||||||
const mixedContent = !isPlainPaste && event && maybeParseHTMLPaste(event);
|
const mixedContent = !isPlainPaste && event && maybeParseHTMLPaste(event);
|
||||||
|
|
||||||
if (mixedContent) {
|
if (mixedContent) {
|
||||||
return { type: "mixedContent", value: mixedContent };
|
if (mixedContent.value.every((item) => item.type === "text")) {
|
||||||
|
return {
|
||||||
|
type: "text",
|
||||||
|
value:
|
||||||
|
event.clipboardData?.getData("text/plain") ||
|
||||||
|
mixedContent.value
|
||||||
|
.map((item) => item.value)
|
||||||
|
.join("\n")
|
||||||
|
.trim(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return 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,40 +327,32 @@ 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 parsedEventData = await parseClipboardEvent(event, isPlainPaste);
|
||||||
|
|
||||||
if (systemClipboard.type === "mixedContent") {
|
if (parsedEventData.type === "mixedContent") {
|
||||||
return {
|
return {
|
||||||
mixedContent: systemClipboard.value,
|
mixedContent: parsedEventData.value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// if system clipboard empty, couldn't be resolved, or contains previously
|
try {
|
||||||
// copied excalidraw scene as SVG, fall back to previously copied excalidraw
|
// if system clipboard contains spreadsheet, use it even though it's
|
||||||
// elements
|
// technically possible it's staler than in-app clipboard
|
||||||
if (
|
const spreadsheetResult =
|
||||||
!systemClipboard ||
|
!isPlainPaste && parsePotentialSpreadsheet(parsedEventData.value);
|
||||||
(!isPlainPaste && systemClipboard.value.includes(SVG_EXPORT_TAG))
|
|
||||||
) {
|
if (spreadsheetResult) {
|
||||||
return getAppClipboard();
|
return spreadsheetResult;
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if system clipboard contains spreadsheet, use it even though it's
|
|
||||||
// technically possible it's staler than in-app clipboard
|
|
||||||
const spreadsheetResult =
|
|
||||||
!isPlainPaste && parsePotentialSpreadsheet(systemClipboard.value);
|
|
||||||
|
|
||||||
if (spreadsheetResult) {
|
|
||||||
return spreadsheetResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
const appClipboardData = getAppClipboard();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const systemClipboardData = JSON.parse(systemClipboard.value);
|
const systemClipboardData = JSON.parse(parsedEventData.value);
|
||||||
const programmaticAPI =
|
const programmaticAPI =
|
||||||
systemClipboardData.type === EXPORT_DATA_TYPES.excalidrawClipboardWithAPI;
|
systemClipboardData.type === EXPORT_DATA_TYPES.excalidrawClipboardWithAPI;
|
||||||
if (clipboardContainsElements(systemClipboardData)) {
|
if (clipboardContainsElements(systemClipboardData)) {
|
||||||
@@ -266,18 +365,9 @@ export const parseClipboard = async (
|
|||||||
programmaticAPI,
|
programmaticAPI,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch {}
|
||||||
// system clipboard doesn't contain excalidraw elements → return plaintext
|
|
||||||
// unless we set a flag to prefer in-app clipboard because browser didn't
|
return { text: parsedEventData.value };
|
||||||
// support storing to system clipboard on copy
|
|
||||||
return PREFER_APP_CLIPBOARD && appClipboardData.elements
|
|
||||||
? {
|
|
||||||
...appClipboardData,
|
|
||||||
text: isPlainPaste
|
|
||||||
? JSON.stringify(appClipboardData.elements, null, 2)
|
|
||||||
: undefined,
|
|
||||||
}
|
|
||||||
: { text: systemClipboard.value };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
|
export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
|
||||||
@@ -310,28 +400,49 @@ export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const copyTextToSystemClipboard = async (text: string | null) => {
|
export const copyTextToSystemClipboard = async (
|
||||||
let copied = false;
|
text: string | null,
|
||||||
|
clipboardEvent?: ClipboardEvent | null,
|
||||||
|
) => {
|
||||||
|
// (1) first try using Async Clipboard API
|
||||||
if (probablySupportsClipboardWriteText) {
|
if (probablySupportsClipboardWriteText) {
|
||||||
try {
|
try {
|
||||||
// NOTE: doesn't work on FF on non-HTTPS domains, or when document
|
// NOTE: doesn't work on FF on non-HTTPS domains, or when document
|
||||||
// not focused
|
// not focused
|
||||||
await navigator.clipboard.writeText(text || "");
|
await navigator.clipboard.writeText(text || "");
|
||||||
copied = true;
|
return;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note that execCommand doesn't allow copying empty strings, so if we're
|
// (2) if fails and we have access to ClipboardEvent, use plain old setData()
|
||||||
// clearing clipboard using this API, we must copy at least an empty char
|
try {
|
||||||
if (!copied && !copyTextViaExecCommand(text || " ")) {
|
if (clipboardEvent) {
|
||||||
throw new Error("couldn't copy");
|
clipboardEvent.clipboardData?.setData("text/plain", text || "");
|
||||||
|
if (clipboardEvent.clipboardData?.getData("text/plain") !== text) {
|
||||||
|
throw new Error("Failed to setData on clipboardEvent");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (3) if that fails, use document.execCommand
|
||||||
|
if (!copyTextViaExecCommand(text)) {
|
||||||
|
throw new Error(t("errors.copyToSystemClipboardFailed"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// adapted from https://github.com/zenorocha/clipboard.js/blob/ce79f170aa655c408b6aab33c9472e8e4fa52e19/src/clipboard-action.js#L48
|
// adapted from https://github.com/zenorocha/clipboard.js/blob/ce79f170aa655c408b6aab33c9472e8e4fa52e19/src/clipboard-action.js#L48
|
||||||
const copyTextViaExecCommand = (text: string) => {
|
const copyTextViaExecCommand = (text: string | null) => {
|
||||||
|
// execCommand doesn't allow copying empty strings, so if we're
|
||||||
|
// clearing clipboard using this API, we must copy at least an empty char
|
||||||
|
if (!text) {
|
||||||
|
text = " ";
|
||||||
|
}
|
||||||
|
|
||||||
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
||||||
|
|
||||||
const textarea = document.createElement("textarea");
|
const textarea = document.createElement("textarea");
|
||||||
|
|||||||
+61
-108
@@ -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";
|
||||||
@@ -35,6 +34,7 @@ import {
|
|||||||
EmbedIcon,
|
EmbedIcon,
|
||||||
extraToolsIcon,
|
extraToolsIcon,
|
||||||
frameToolIcon,
|
frameToolIcon,
|
||||||
|
mermaidLogoIcon,
|
||||||
laserPointerToolIcon,
|
laserPointerToolIcon,
|
||||||
} from "./icons";
|
} from "./icons";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
@@ -66,7 +66,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 +124,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")}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -222,7 +224,6 @@ export const ShapesSwitcher = ({
|
|||||||
app: AppClassProperties;
|
app: AppClassProperties;
|
||||||
}) => {
|
}) => {
|
||||||
const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);
|
const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);
|
||||||
const device = useDevice();
|
|
||||||
|
|
||||||
const frameToolSelected = activeTool.type === "frame";
|
const frameToolSelected = activeTool.type === "frame";
|
||||||
const laserToolSelected = activeTool.type === "laser";
|
const laserToolSelected = activeTool.type === "laser";
|
||||||
@@ -272,111 +273,63 @@ export const ShapesSwitcher = ({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<div className="App-toolbar__divider" />
|
<div className="App-toolbar__divider" />
|
||||||
{/* TEMP HACK because dropdown doesn't work well inside mobile toolbar */}
|
|
||||||
{device.isMobile ? (
|
<DropdownMenu open={isExtraToolsMenuOpen}>
|
||||||
<>
|
<DropdownMenu.Trigger
|
||||||
<ToolButton
|
className={clsx("App-toolbar__extra-tools-trigger", {
|
||||||
className={clsx("Shape", { fillable: false })}
|
"App-toolbar__extra-tools-trigger--selected":
|
||||||
type="radio"
|
frameToolSelected ||
|
||||||
|
embeddableToolSelected ||
|
||||||
|
// in collab we're already highlighting the laser button
|
||||||
|
// outside toolbar, so let's not highlight extra-tools button
|
||||||
|
// on top of it
|
||||||
|
(laserToolSelected && !app.props.isCollaborating),
|
||||||
|
})}
|
||||||
|
onToggle={() => setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)}
|
||||||
|
title={t("toolBar.extraTools")}
|
||||||
|
>
|
||||||
|
{extraToolsIcon}
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
onClickOutside={() => setIsExtraToolsMenuOpen(false)}
|
||||||
|
onSelect={() => setIsExtraToolsMenuOpen(false)}
|
||||||
|
className="App-toolbar__extra-tools-dropdown"
|
||||||
|
>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => app.setActiveTool({ type: "frame" })}
|
||||||
icon={frameToolIcon}
|
icon={frameToolIcon}
|
||||||
checked={activeTool.type === "frame"}
|
shortcut={KEYS.F.toLocaleUpperCase()}
|
||||||
name="editor-current-shape"
|
data-testid="toolbar-frame"
|
||||||
title={`${capitalizeString(
|
selected={frameToolSelected}
|
||||||
t("toolBar.frame"),
|
>
|
||||||
)} — ${KEYS.F.toLocaleUpperCase()}`}
|
{t("toolBar.frame")}
|
||||||
keyBindingLabel={KEYS.F.toLocaleUpperCase()}
|
</DropdownMenu.Item>
|
||||||
aria-label={capitalizeString(t("toolBar.frame"))}
|
<DropdownMenu.Item
|
||||||
aria-keyshortcuts={KEYS.F.toLocaleUpperCase()}
|
onSelect={() => app.setActiveTool({ type: "embeddable" })}
|
||||||
data-testid={`toolbar-frame`}
|
|
||||||
onPointerDown={({ pointerType }) => {
|
|
||||||
if (!appState.penDetected && pointerType === "pen") {
|
|
||||||
app.togglePenMode(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onChange={({ pointerType }) => {
|
|
||||||
trackEvent("toolbar", "frame", "ui");
|
|
||||||
app.setActiveTool({ type: "frame" });
|
|
||||||
}}
|
|
||||||
selected={activeTool.type === "frame"}
|
|
||||||
/>
|
|
||||||
<ToolButton
|
|
||||||
className={clsx("Shape", { fillable: false })}
|
|
||||||
type="radio"
|
|
||||||
icon={EmbedIcon}
|
icon={EmbedIcon}
|
||||||
checked={activeTool.type === "embeddable"}
|
data-testid="toolbar-embeddable"
|
||||||
name="editor-current-shape"
|
selected={embeddableToolSelected}
|
||||||
title={capitalizeString(t("toolBar.embeddable"))}
|
|
||||||
aria-label={capitalizeString(t("toolBar.embeddable"))}
|
|
||||||
data-testid={`toolbar-embeddable`}
|
|
||||||
onPointerDown={({ pointerType }) => {
|
|
||||||
if (!appState.penDetected && pointerType === "pen") {
|
|
||||||
app.togglePenMode(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onChange={({ pointerType }) => {
|
|
||||||
trackEvent("toolbar", "embeddable", "ui");
|
|
||||||
app.setActiveTool({ type: "embeddable" });
|
|
||||||
}}
|
|
||||||
selected={activeTool.type === "embeddable"}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<DropdownMenu open={isExtraToolsMenuOpen}>
|
|
||||||
<DropdownMenu.Trigger
|
|
||||||
className={clsx("App-toolbar__extra-tools-trigger", {
|
|
||||||
"App-toolbar__extra-tools-trigger--selected":
|
|
||||||
frameToolSelected ||
|
|
||||||
embeddableToolSelected ||
|
|
||||||
// in collab we're already highlighting the laser button
|
|
||||||
// outside toolbar, so let's not highlight extra-tools button
|
|
||||||
// on top of it
|
|
||||||
(laserToolSelected && !app.props.isCollaborating),
|
|
||||||
})}
|
|
||||||
onToggle={() => setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)}
|
|
||||||
title={t("toolBar.extraTools")}
|
|
||||||
>
|
>
|
||||||
{extraToolsIcon}
|
{t("toolBar.embeddable")}
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Item
|
||||||
onClickOutside={() => setIsExtraToolsMenuOpen(false)}
|
onSelect={() => app.setActiveTool({ type: "laser" })}
|
||||||
onSelect={() => setIsExtraToolsMenuOpen(false)}
|
icon={laserPointerToolIcon}
|
||||||
className="App-toolbar__extra-tools-dropdown"
|
data-testid="toolbar-laser"
|
||||||
|
selected={laserToolSelected}
|
||||||
|
shortcut={KEYS.K.toLocaleUpperCase()}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item
|
{t("toolBar.laser")}
|
||||||
onSelect={() => {
|
</DropdownMenu.Item>
|
||||||
app.setActiveTool({ type: "frame" });
|
<DropdownMenu.Item
|
||||||
}}
|
onSelect={() => app.setOpenDialog("mermaid")}
|
||||||
icon={frameToolIcon}
|
icon={mermaidLogoIcon}
|
||||||
shortcut={KEYS.F.toLocaleUpperCase()}
|
data-testid="toolbar-embeddable"
|
||||||
data-testid="toolbar-frame"
|
>
|
||||||
selected={frameToolSelected}
|
{t("toolBar.mermaidToExcalidraw")}
|
||||||
>
|
</DropdownMenu.Item>
|
||||||
{t("toolBar.frame")}
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu>
|
||||||
<DropdownMenu.Item
|
|
||||||
onSelect={() => {
|
|
||||||
app.setActiveTool({ type: "embeddable" });
|
|
||||||
}}
|
|
||||||
icon={EmbedIcon}
|
|
||||||
data-testid="toolbar-embeddable"
|
|
||||||
selected={embeddableToolSelected}
|
|
||||||
>
|
|
||||||
{t("toolBar.embeddable")}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item
|
|
||||||
onSelect={() => {
|
|
||||||
app.setActiveTool({ type: "laser" });
|
|
||||||
}}
|
|
||||||
icon={laserPointerToolIcon}
|
|
||||||
data-testid="toolbar-laser"
|
|
||||||
selected={laserToolSelected}
|
|
||||||
shortcut={KEYS.K.toLocaleUpperCase()}
|
|
||||||
>
|
|
||||||
{t("toolBar.laser")}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+43
-42
@@ -366,6 +366,7 @@ import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
|
|||||||
import { StaticCanvas, InteractiveCanvas } from "./canvases";
|
import { StaticCanvas, InteractiveCanvas } from "./canvases";
|
||||||
import { Renderer } from "../scene/Renderer";
|
import { Renderer } from "../scene/Renderer";
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
|
import MermaidToExcalidraw from "./MermaidToExcalidraw";
|
||||||
import { LaserToolOverlay } from "./LaserTool/LaserTool";
|
import { LaserToolOverlay } from "./LaserTool/LaserTool";
|
||||||
import { LaserPathManager } from "./LaserTool/LaserPathManager";
|
import { LaserPathManager } from "./LaserTool/LaserPathManager";
|
||||||
import {
|
import {
|
||||||
@@ -1136,7 +1137,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
overflow: f.id === this.state.editingFrame ? "visible" : "hidden",
|
overflow: f.id === this.state.editingFrame ? "visible" : "hidden",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
textOverflow: "ellipsis",
|
textOverflow: "ellipsis",
|
||||||
cursor: CURSOR_TYPE.MOVE,
|
cursor:
|
||||||
|
!f.locked && this.state.activeTool.type === "selection"
|
||||||
|
? CURSOR_TYPE.MOVE
|
||||||
|
: this.interactiveCanvas?.style.cursor,
|
||||||
pointerEvents: this.state.viewModeEnabled
|
pointerEvents: this.state.viewModeEnabled
|
||||||
? POINTER_EVENTS.disabled
|
? POINTER_EVENTS.disabled
|
||||||
: POINTER_EVENTS.enabled,
|
: POINTER_EVENTS.enabled,
|
||||||
@@ -1245,7 +1249,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
isCollaborating={this.props.isCollaborating}
|
isCollaborating={this.props.isCollaborating}
|
||||||
>
|
>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
|
{this.state.openDialog === "mermaid" && (
|
||||||
|
<MermaidToExcalidraw />
|
||||||
|
)}
|
||||||
</LayerUI>
|
</LayerUI>
|
||||||
|
|
||||||
<div className="excalidraw-textEditorContainer" />
|
<div className="excalidraw-textEditorContainer" />
|
||||||
<div className="excalidraw-contextMenuContainer" />
|
<div className="excalidraw-contextMenuContainer" />
|
||||||
<div className="excalidraw-eye-dropper-container" />
|
<div className="excalidraw-eye-dropper-container" />
|
||||||
@@ -1275,6 +1283,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={(callback) => {
|
||||||
|
this.setState({ contextMenu: null }, () => {
|
||||||
|
this.focusContainer();
|
||||||
|
callback?.();
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<StaticCanvas
|
<StaticCanvas
|
||||||
@@ -2110,7 +2124,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (!isExcalidrawActive || isWritableElement(event.target)) {
|
if (!isExcalidrawActive || isWritableElement(event.target)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.cutAll();
|
this.actionManager.executeAction(actionCut, "keyboard", event);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
});
|
});
|
||||||
@@ -2122,19 +2136,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (!isExcalidrawActive || isWritableElement(event.target)) {
|
if (!isExcalidrawActive || isWritableElement(event.target)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.copyAll();
|
this.actionManager.executeAction(actionCopy, "keyboard", event);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
private cutAll = () => {
|
|
||||||
this.actionManager.executeAction(actionCut, "keyboard");
|
|
||||||
};
|
|
||||||
|
|
||||||
private copyAll = () => {
|
|
||||||
this.actionManager.executeAction(actionCopy, "keyboard");
|
|
||||||
};
|
|
||||||
|
|
||||||
private static resetTapTwice() {
|
private static resetTapTwice() {
|
||||||
didTapTwice = false;
|
didTapTwice = false;
|
||||||
}
|
}
|
||||||
@@ -2195,8 +2201,8 @@ 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;
|
||||||
|
|
||||||
// #686
|
// #686
|
||||||
const target = document.activeElement;
|
const target = document.activeElement;
|
||||||
@@ -2326,11 +2332,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
private addElementsFromPasteOrLibrary = (opts: {
|
addElementsFromPasteOrLibrary = (opts: {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
files: BinaryFiles | null;
|
files: BinaryFiles | null;
|
||||||
position: { clientX: number; clientY: number } | "cursor" | "center";
|
position: { clientX: number; clientY: number } | "cursor" | "center";
|
||||||
retainSeed?: boolean;
|
retainSeed?: boolean;
|
||||||
|
fitToContent?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const elements = restoreElements(opts.elements, null, undefined);
|
const elements = restoreElements(opts.elements, null, undefined);
|
||||||
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
||||||
@@ -2435,6 +2442,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
this.setActiveTool({ type: "selection" });
|
this.setActiveTool({ type: "selection" });
|
||||||
|
|
||||||
|
if (opts.fitToContent) {
|
||||||
|
this.scrollToContent(newElements, {
|
||||||
|
fitToContent: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO rewrite this to paste both text & images at the same time if
|
// TODO rewrite this to paste both text & images at the same time if
|
||||||
@@ -2554,12 +2567,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;
|
||||||
@@ -3304,6 +3323,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setOpenDialog = (dialogType: AppState["openDialog"]) => {
|
||||||
|
this.setState({ openDialog: dialogType });
|
||||||
|
};
|
||||||
|
|
||||||
private setCursor = (cursor: string) => {
|
private setCursor = (cursor: string) => {
|
||||||
setCursor(this.interactiveCanvas, cursor);
|
setCursor(this.interactiveCanvas, cursor);
|
||||||
};
|
};
|
||||||
@@ -3574,11 +3597,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 &&
|
||||||
@@ -3750,32 +3768,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
|
||||||
event,
|
|
||||||
this.state,
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||||
|
|
||||||
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
|
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
|
||||||
if (!event[KEYS.CTRL_OR_CMD]) {
|
|
||||||
// If double clicked without any ctrl/cmd modifier on top of a point,
|
|
||||||
// toggle split mode for that point. Else, treat as regular double click.
|
|
||||||
const pointUnderCursorIndex =
|
|
||||||
LinearElementEditor.getPointIndexUnderCursor(
|
|
||||||
selectedElements[0],
|
|
||||||
this.state.zoom,
|
|
||||||
sceneX,
|
|
||||||
sceneY,
|
|
||||||
);
|
|
||||||
if (pointUnderCursorIndex >= 0) {
|
|
||||||
LinearElementEditor.toggleSegmentSplitAtIndex(
|
|
||||||
selectedElements[0],
|
|
||||||
pointUnderCursorIndex,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
event[KEYS.CTRL_OR_CMD] &&
|
event[KEYS.CTRL_OR_CMD] &&
|
||||||
(!this.state.editingLinearElement ||
|
(!this.state.editingLinearElement ||
|
||||||
@@ -3799,6 +3794,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
resetCursor(this.interactiveCanvas);
|
resetCursor(this.interactiveCanvas);
|
||||||
|
|
||||||
|
let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
||||||
|
event,
|
||||||
|
this.state,
|
||||||
|
);
|
||||||
|
|
||||||
const selectedGroupIds = getSelectedGroupIds(this.state);
|
const selectedGroupIds = getSelectedGroupIds(this.state);
|
||||||
|
|
||||||
if (selectedGroupIds.length > 0) {
|
if (selectedGroupIds.length > 0) {
|
||||||
@@ -4277,6 +4277,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
scenePointer.x,
|
scenePointer.x,
|
||||||
scenePointer.y,
|
scenePointer.y,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.hitLinkElement = this.getElementLinkAtPosition(
|
this.hitLinkElement = this.getElementLinkAtPosition(
|
||||||
scenePointer,
|
scenePointer,
|
||||||
hitElement,
|
hitElement,
|
||||||
|
|||||||
@@ -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: (callback?: () => 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,9 @@ 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 +100,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");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
@import "../css/variables.module";
|
||||||
|
|
||||||
|
$verticalBreakpoint: 860px;
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.dialog-mermaid {
|
||||||
|
&-title {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
&-desc {
|
||||||
|
font-size: 15px;
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Modal__content .Island {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@at-root .excalidraw:not(.excalidraw--mobile)#{&} {
|
||||||
|
padding: 1.25rem;
|
||||||
|
|
||||||
|
.Modal__content {
|
||||||
|
height: 100%;
|
||||||
|
max-height: 750px;
|
||||||
|
|
||||||
|
@media screen and (max-width: $verticalBreakpoint) {
|
||||||
|
height: auto;
|
||||||
|
// When vertical, we want the height to span whole viewport.
|
||||||
|
// This is also important for the children not to overflow the
|
||||||
|
// modal/viewport (for some reason).
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Island {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
|
||||||
|
.Dialog__content {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-mermaid-body {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-template-rows: 1fr auto;
|
||||||
|
height: 100%;
|
||||||
|
column-gap: 4rem;
|
||||||
|
|
||||||
|
@media screen and (max-width: $verticalBreakpoint) {
|
||||||
|
flex-direction: column;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-mermaid-panels {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 4rem;
|
||||||
|
|
||||||
|
grid-row: 1;
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
|
||||||
|
@media screen and (max-width: $verticalBreakpoint) {
|
||||||
|
flex-direction: column;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
margin-left: 4px;
|
||||||
|
|
||||||
|
@media screen and (max-width: $verticalBreakpoint) {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 20rem;
|
||||||
|
height: 100%;
|
||||||
|
resize: none;
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
border: 1px solid var(--dialog-border-color);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
padding: 0.85rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
font-family: monospace;
|
||||||
|
|
||||||
|
@media screen and (max-width: $verticalBreakpoint) {
|
||||||
|
width: auto;
|
||||||
|
height: 10rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-preview-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.85rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
// acts as min-height
|
||||||
|
height: 200px;
|
||||||
|
flex-grow: 1;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==")
|
||||||
|
left center;
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
border: 1px solid var(--dialog-border-color);
|
||||||
|
|
||||||
|
@media screen and (max-width: $verticalBreakpoint) {
|
||||||
|
// acts as min-height
|
||||||
|
height: 400px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-preview-canvas-container {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid-error {
|
||||||
|
color: red;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 30px;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 100%;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: Cascadia;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-mermaid-buttons {
|
||||||
|
grid-column: 2;
|
||||||
|
|
||||||
|
.dialog-mermaid-insert {
|
||||||
|
&.excalidraw-button {
|
||||||
|
font-family: "Assistant";
|
||||||
|
font-weight: 600;
|
||||||
|
height: 2.5rem;
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 0.3em;
|
||||||
|
width: 7.5rem;
|
||||||
|
font-size: 12px;
|
||||||
|
color: $oc-white;
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-primary-darker);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: var(--color-primary-darkest);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $verticalBreakpoint) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@at-root .excalidraw.theme--dark#{&} {
|
||||||
|
color: var(--color-gray-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
import { useState, useRef, useEffect, useDeferredValue } from "react";
|
||||||
|
import { BinaryFiles } from "../types";
|
||||||
|
import { useApp } from "./App";
|
||||||
|
import { Button } from "./Button";
|
||||||
|
import { Dialog } from "./Dialog";
|
||||||
|
import { DEFAULT_EXPORT_PADDING, DEFAULT_FONT_SIZE } from "../constants";
|
||||||
|
import {
|
||||||
|
convertToExcalidrawElements,
|
||||||
|
exportToCanvas,
|
||||||
|
} from "../packages/excalidraw/index";
|
||||||
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
|
import { canvasToBlob } from "../data/blob";
|
||||||
|
import { ArrowRightIcon } from "./icons";
|
||||||
|
import Spinner from "./Spinner";
|
||||||
|
import "./MermaidToExcalidraw.scss";
|
||||||
|
|
||||||
|
import { MermaidToExcalidrawResult } from "@excalidraw/mermaid-to-excalidraw/dist/interfaces";
|
||||||
|
import type { MermaidOptions } from "@excalidraw/mermaid-to-excalidraw";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import Trans from "./Trans";
|
||||||
|
|
||||||
|
const LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW = "mermaid-to-excalidraw";
|
||||||
|
const MERMAID_EXAMPLE =
|
||||||
|
"flowchart TD\n A[Christmas] -->|Get money| B(Go shopping)\n B --> C{Let me think}\n C -->|One| D[Laptop]\n C -->|Two| E[iPhone]\n C -->|Three| F[Car]";
|
||||||
|
|
||||||
|
const saveMermaidDataToStorage = (data: string) => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW, data);
|
||||||
|
} catch (error: any) {
|
||||||
|
// Unable to access window.localStorage
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const importMermaidDataFromStorage = () => {
|
||||||
|
try {
|
||||||
|
const data = localStorage.getItem(LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW);
|
||||||
|
if (data) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// Unable to access localStorage
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ErrorComp = ({ error }: { error: string }) => {
|
||||||
|
return (
|
||||||
|
<div data-testid="mermaid-error" className="mermaid-error">
|
||||||
|
Error! <p>{error}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MermaidToExcalidraw = () => {
|
||||||
|
const [mermaidToExcalidrawLib, setMermaidToExcalidrawLib] = useState<{
|
||||||
|
loaded: boolean;
|
||||||
|
api: {
|
||||||
|
parseMermaidToExcalidraw: (
|
||||||
|
defination: string,
|
||||||
|
options: MermaidOptions,
|
||||||
|
) => Promise<MermaidToExcalidrawResult>;
|
||||||
|
} | null;
|
||||||
|
}>({ loaded: false, api: null });
|
||||||
|
|
||||||
|
const [text, setText] = useState("");
|
||||||
|
const deferredText = useDeferredValue(text.trim());
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const canvasRef = useRef<HTMLDivElement>(null);
|
||||||
|
const data = useRef<{
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
files: BinaryFiles | null;
|
||||||
|
}>({ elements: [], files: null });
|
||||||
|
|
||||||
|
const app = useApp();
|
||||||
|
|
||||||
|
const resetPreview = () => {
|
||||||
|
const canvasNode = canvasRef.current;
|
||||||
|
|
||||||
|
if (!canvasNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parent = canvasNode.parentElement;
|
||||||
|
if (!parent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parent.style.background = "";
|
||||||
|
setError(null);
|
||||||
|
canvasNode.replaceChildren();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadMermaidToExcalidrawLib = async () => {
|
||||||
|
const api = await import(
|
||||||
|
/* webpackChunkName:"mermaid-to-excalidraw" */ "@excalidraw/mermaid-to-excalidraw"
|
||||||
|
);
|
||||||
|
setMermaidToExcalidrawLib({ loaded: true, api });
|
||||||
|
};
|
||||||
|
loadMermaidToExcalidrawLib();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const data = importMermaidDataFromStorage() || MERMAID_EXAMPLE;
|
||||||
|
setText(data);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const renderExcalidrawPreview = async () => {
|
||||||
|
const canvasNode = canvasRef.current;
|
||||||
|
const parent = canvasNode?.parentElement;
|
||||||
|
if (
|
||||||
|
!mermaidToExcalidrawLib.loaded ||
|
||||||
|
!canvasNode ||
|
||||||
|
!parent ||
|
||||||
|
!mermaidToExcalidrawLib.api
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!deferredText) {
|
||||||
|
resetPreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { elements, files } =
|
||||||
|
await mermaidToExcalidrawLib.api.parseMermaidToExcalidraw(
|
||||||
|
deferredText,
|
||||||
|
{
|
||||||
|
fontSize: DEFAULT_FONT_SIZE,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
data.current = {
|
||||||
|
elements: convertToExcalidrawElements(elements, {
|
||||||
|
regenerateIds: true,
|
||||||
|
}),
|
||||||
|
files,
|
||||||
|
};
|
||||||
|
|
||||||
|
const canvas = await exportToCanvas({
|
||||||
|
elements: data.current.elements,
|
||||||
|
files: data.current.files,
|
||||||
|
exportPadding: DEFAULT_EXPORT_PADDING,
|
||||||
|
maxWidthOrHeight:
|
||||||
|
Math.max(parent.offsetWidth, parent.offsetHeight) *
|
||||||
|
window.devicePixelRatio,
|
||||||
|
});
|
||||||
|
// if converting to blob fails, there's some problem that will
|
||||||
|
// likely prevent preview and export (e.g. canvas too big)
|
||||||
|
await canvasToBlob(canvas);
|
||||||
|
parent.style.background = "var(--default-bg-color)";
|
||||||
|
canvasNode.replaceChildren(canvas);
|
||||||
|
} catch (e: any) {
|
||||||
|
parent.style.background = "var(--default-bg-color)";
|
||||||
|
if (deferredText) {
|
||||||
|
setError(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
renderExcalidrawPreview();
|
||||||
|
}, [deferredText, mermaidToExcalidrawLib]);
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
app.setOpenDialog(null);
|
||||||
|
saveMermaidDataToStorage(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelect = () => {
|
||||||
|
const { elements: newElements, files } = data.current;
|
||||||
|
app.addElementsFromPasteOrLibrary({
|
||||||
|
elements: newElements,
|
||||||
|
files,
|
||||||
|
position: "center",
|
||||||
|
fitToContent: true,
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
className="dialog-mermaid"
|
||||||
|
onCloseRequest={onClose}
|
||||||
|
size={1200}
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
<p className="dialog-mermaid-title">{t("mermaid.title")}</p>
|
||||||
|
<span className="dialog-mermaid-desc">
|
||||||
|
<Trans
|
||||||
|
i18nKey="mermaid.description"
|
||||||
|
flowchartLink={(el) => (
|
||||||
|
<a href="https://mermaid.js.org/syntax/flowchart.html">{el}</a>
|
||||||
|
)}
|
||||||
|
sequenceLink={(el) => (
|
||||||
|
<a href="https://mermaid.js.org/syntax/sequenceDiagram.html">
|
||||||
|
{el}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="dialog-mermaid-body">
|
||||||
|
<div className="dialog-mermaid-panels">
|
||||||
|
<div className="dialog-mermaid-panels-text">
|
||||||
|
<label>{t("mermaid.syntax")}</label>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
onChange={(event) => setText(event.target.value)}
|
||||||
|
value={text}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="dialog-mermaid-panels-preview">
|
||||||
|
<label>{t("mermaid.preview")}</label>
|
||||||
|
<div className="dialog-mermaid-panels-preview-wrapper">
|
||||||
|
{error && <ErrorComp error={error} />}
|
||||||
|
{mermaidToExcalidrawLib.loaded ? (
|
||||||
|
<div
|
||||||
|
ref={canvasRef}
|
||||||
|
style={{ opacity: error ? "0.15" : 1 }}
|
||||||
|
className="dialog-mermaid-panels-preview-canvas-container"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Spinner size="2rem" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="dialog-mermaid-buttons">
|
||||||
|
<Button className="dialog-mermaid-insert" onSelect={onSelect}>
|
||||||
|
{t("mermaid.button")}
|
||||||
|
<span>{ArrowRightIcon}</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default MermaidToExcalidraw;
|
||||||
@@ -160,6 +160,15 @@
|
|||||||
width: var(--lg-button-size);
|
width: var(--lg-button-size);
|
||||||
height: var(--lg-button-size);
|
height: var(--lg-button-size);
|
||||||
|
|
||||||
|
@media screen and (max-width: 450px) {
|
||||||
|
width: 1.8rem;
|
||||||
|
height: 1.8rem;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 379px) {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: var(--lg-icon-size);
|
width: var(--lg-icon-size);
|
||||||
height: var(--lg-icon-size);
|
height: var(--lg-icon-size);
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
align-self: center;
|
align-self: center;
|
||||||
background-color: var(--default-border-color);
|
background-color: var(--default-border-color);
|
||||||
margin: 0 0.25rem;
|
margin: 0 0.25rem;
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,5 +45,6 @@
|
|||||||
margin-top: 0.375rem;
|
margin-top: 0.375rem;
|
||||||
right: 0;
|
right: 0;
|
||||||
min-width: 11.875rem;
|
min-width: 11.875rem;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,6 @@
|
|||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
|
|
||||||
&--mobile {
|
&--mobile {
|
||||||
bottom: 55px;
|
|
||||||
top: auto;
|
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
row-gap: 0.75rem;
|
row-gap: 0.75rem;
|
||||||
|
|||||||
@@ -1654,6 +1654,22 @@ export const frameToolIcon = createIcon(
|
|||||||
tablerIconProps,
|
tablerIconProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const mermaidLogoIcon = createIcon(
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M407.48,111.18C335.587,108.103 269.573,152.338 245.08,220C220.587,152.338 154.573,108.103 82.68,111.18C80.285,168.229 107.577,222.632 154.74,254.82C178.908,271.419 193.35,298.951 193.27,328.27L193.27,379.13L296.9,379.13L296.9,328.27C296.816,298.953 311.255,271.42 335.42,254.82C382.596,222.644 409.892,168.233 407.48,111.18Z"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ArrowRightIcon = createIcon(
|
||||||
|
<g strokeWidth="1.25">
|
||||||
|
<path d="M4.16602 10H15.8327" />
|
||||||
|
<path d="M12.5 13.3333L15.8333 10" />
|
||||||
|
<path d="M12.5 6.66666L15.8333 9.99999" />
|
||||||
|
</g>,
|
||||||
|
modifiedTablerIconProps,
|
||||||
|
);
|
||||||
|
|
||||||
export const laserPointerToolIcon = createIcon(
|
export const laserPointerToolIcon = createIcon(
|
||||||
<g
|
<g
|
||||||
fill="none"
|
fill="none"
|
||||||
|
|||||||
@@ -148,6 +148,8 @@ export const IMAGE_MIME_TYPES = {
|
|||||||
jfif: "image/jfif",
|
jfif: "image/jfif",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const ALLOWED_PASTE_MIME_TYPES = ["text/plain", "text/html"] as const;
|
||||||
|
|
||||||
export const MIME_TYPES = {
|
export const MIME_TYPES = {
|
||||||
json: "application/json",
|
json: "application/json",
|
||||||
// excalidraw data
|
// excalidraw data
|
||||||
@@ -302,6 +304,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"];
|
||||||
|
|||||||
+12
-5
@@ -195,7 +195,7 @@
|
|||||||
.buttonList label:focus-within,
|
.buttonList label:focus-within,
|
||||||
input:focus-visible {
|
input:focus-visible {
|
||||||
outline: transparent;
|
outline: transparent;
|
||||||
box-shadow: 0 0 0 2px var(--focus-highlight-color);
|
box-shadow: 0 0 0 1px var(--color-brand-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonList {
|
.buttonList {
|
||||||
@@ -280,6 +280,11 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
|
||||||
|
.dropdown-menu--mobile {
|
||||||
|
bottom: 55px;
|
||||||
|
top: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-mobile-menu {
|
.App-mobile-menu {
|
||||||
@@ -537,13 +542,13 @@
|
|||||||
|
|
||||||
&:not(:focus) {
|
&:not(:focus) {
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--input-hover-bg-color);
|
border-color: var(--color-brand-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: 0 0 0 2px var(--focus-highlight-color);
|
border-color: var(--color-brand-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,6 +597,8 @@
|
|||||||
background-color: var(--island-bg-color);
|
background-color: var(--island-bg-color);
|
||||||
|
|
||||||
.ToolIcon__icon {
|
.ToolIcon__icon {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -601,8 +608,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.App-toolbar--mobile {
|
.App-toolbar--mobile {
|
||||||
overflow-x: auto;
|
overflow: visible;
|
||||||
max-width: 90vw;
|
max-width: 98vw;
|
||||||
|
|
||||||
.ToolIcon__keybinding {
|
.ToolIcon__keybinding {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -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": "id45",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "id41",
|
"id": "id46",
|
||||||
"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": "id46",
|
||||||
"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": "id47",
|
||||||
"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": "id45",
|
||||||
"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": "id48",
|
||||||
"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": "id48",
|
||||||
"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": "id49",
|
||||||
"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": "id48",
|
||||||
"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": "id38",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id35",
|
"elementId": "id40",
|
||||||
"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": "id39",
|
||||||
"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": "id37",
|
||||||
"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": "id37",
|
||||||
"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": "id37",
|
||||||
"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": "id42",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id39",
|
"elementId": "id44",
|
||||||
"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": "id43",
|
||||||
"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": "id41",
|
||||||
"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": "id41",
|
||||||
"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": "id41",
|
||||||
"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",
|
||||||
@@ -1385,7 +1335,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,
|
||||||
@@ -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,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -1412,9 +1362,59 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"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,
|
||||||
|
"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": 0,
|
||||||
|
"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": 1,
|
||||||
|
"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
-16
@@ -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,
|
||||||
@@ -285,9 +284,6 @@ const restoreElement = (
|
|||||||
points,
|
points,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
segmentSplitIndices: element.segmentSplitIndices
|
|
||||||
? [...element.segmentSplitIndices]
|
|
||||||
: [],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,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.
|
||||||
*/
|
*/
|
||||||
@@ -408,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;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -461,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);
|
||||||
}
|
}
|
||||||
|
|||||||
+128
-9
@@ -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);
|
||||||
@@ -280,6 +309,90 @@ describe("Test Transform", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Test Frames", () => {
|
||||||
|
it("should transform frames and update frame ids when regenerated", () => {
|
||||||
|
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
|
||||||
|
{
|
||||||
|
type: "rectangle",
|
||||||
|
x: 10,
|
||||||
|
y: 10,
|
||||||
|
strokeWidth: 2,
|
||||||
|
id: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "diamond",
|
||||||
|
x: 120,
|
||||||
|
y: 20,
|
||||||
|
backgroundColor: "#fff3bf",
|
||||||
|
strokeWidth: 2,
|
||||||
|
label: {
|
||||||
|
text: "HELLO EXCALIDRAW",
|
||||||
|
strokeColor: "#099268",
|
||||||
|
fontSize: 30,
|
||||||
|
},
|
||||||
|
id: "2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "frame",
|
||||||
|
children: ["1", "2"],
|
||||||
|
name: "My frame",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
|
elementsSkeleton,
|
||||||
|
opts,
|
||||||
|
);
|
||||||
|
expect(excaldrawElements.length).toBe(4);
|
||||||
|
|
||||||
|
excaldrawElements.forEach((ele) => {
|
||||||
|
expect(ele).toMatchObject({
|
||||||
|
seed: expect.any(Number),
|
||||||
|
versionNonce: expect.any(Number),
|
||||||
|
id: expect.any(String),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should consider max of calculated and frame dimensions when provided", () => {
|
||||||
|
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
|
||||||
|
{
|
||||||
|
type: "rectangle",
|
||||||
|
x: 10,
|
||||||
|
y: 10,
|
||||||
|
strokeWidth: 2,
|
||||||
|
id: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "diamond",
|
||||||
|
x: 120,
|
||||||
|
y: 20,
|
||||||
|
backgroundColor: "#fff3bf",
|
||||||
|
strokeWidth: 2,
|
||||||
|
label: {
|
||||||
|
text: "HELLO EXCALIDRAW",
|
||||||
|
strokeColor: "#099268",
|
||||||
|
fontSize: 30,
|
||||||
|
},
|
||||||
|
id: "2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "frame",
|
||||||
|
children: ["1", "2"],
|
||||||
|
name: "My frame",
|
||||||
|
width: 800,
|
||||||
|
height: 100,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
|
elementsSkeleton,
|
||||||
|
opts,
|
||||||
|
);
|
||||||
|
const frame = excaldrawElements.find((ele) => ele.type === "frame")!;
|
||||||
|
expect(frame.width).toBe(800);
|
||||||
|
expect(frame.height).toBe(126);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("Test arrow bindings", () => {
|
describe("Test arrow bindings", () => {
|
||||||
it("should bind arrows to shapes when start / end provided without ids", () => {
|
it("should bind arrows to shapes when start / end provided without ids", () => {
|
||||||
const elements = [
|
const elements = [
|
||||||
@@ -300,6 +413,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 +435,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 +455,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 +497,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 +520,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 +540,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 +613,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 +662,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 +716,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 +767,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 +810,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);
|
||||||
|
|||||||
+161
-11
@@ -5,6 +5,7 @@ import {
|
|||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import {
|
import {
|
||||||
|
getCommonBounds,
|
||||||
newElement,
|
newElement,
|
||||||
newLinearElement,
|
newLinearElement,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
@@ -12,6 +13,7 @@ import {
|
|||||||
import { bindLinearElement } from "../element/binding";
|
import { bindLinearElement } from "../element/binding";
|
||||||
import {
|
import {
|
||||||
ElementConstructorOpts,
|
ElementConstructorOpts,
|
||||||
|
newFrameElement,
|
||||||
newImageElement,
|
newImageElement,
|
||||||
newTextElement,
|
newTextElement,
|
||||||
} from "../element/newElement";
|
} from "../element/newElement";
|
||||||
@@ -39,6 +41,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";
|
||||||
@@ -133,9 +137,7 @@ export type ValidContainer =
|
|||||||
export type ExcalidrawElementSkeleton =
|
export type ExcalidrawElementSkeleton =
|
||||||
| Extract<
|
| Extract<
|
||||||
Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
||||||
| ExcalidrawEmbeddableElement
|
ExcalidrawEmbeddableElement | ExcalidrawFreeDrawElement
|
||||||
| ExcalidrawFreeDrawElement
|
|
||||||
| ExcalidrawFrameElement
|
|
||||||
>
|
>
|
||||||
| ({
|
| ({
|
||||||
type: Extract<ExcalidrawLinearElement["type"], "line">;
|
type: Extract<ExcalidrawLinearElement["type"], "line">;
|
||||||
@@ -156,10 +158,15 @@ export type ExcalidrawElementSkeleton =
|
|||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
fileId: FileId;
|
fileId: FileId;
|
||||||
} & Partial<ExcalidrawImageElement>);
|
} & Partial<ExcalidrawImageElement>)
|
||||||
|
| ({
|
||||||
|
type: "frame";
|
||||||
|
children: readonly ExcalidrawElement["id"][];
|
||||||
|
name?: string;
|
||||||
|
} & Partial<ExcalidrawFrameElement>);
|
||||||
|
|
||||||
const DEFAULT_LINEAR_ELEMENT_PROPS = {
|
const DEFAULT_LINEAR_ELEMENT_PROPS = {
|
||||||
width: 300,
|
width: 100,
|
||||||
height: 0,
|
height: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -357,6 +364,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 +433,27 @@ 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 +502,11 @@ export const convertToExcalidrawElements = (
|
|||||||
],
|
],
|
||||||
...element,
|
...element,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Object.assign(
|
||||||
|
excalidrawElement,
|
||||||
|
getSizeFromPoints(excalidrawElement.points),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "text": {
|
case "text": {
|
||||||
@@ -477,8 +540,15 @@ export const convertToExcalidrawElements = (
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "frame": {
|
||||||
|
excalidrawElement = newFrameElement({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
...element,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "freedraw":
|
case "freedraw":
|
||||||
case "frame":
|
|
||||||
case "embeddable": {
|
case "embeddable": {
|
||||||
excalidrawElement = element;
|
excalidrawElement = element;
|
||||||
break;
|
break;
|
||||||
@@ -499,6 +569,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 +597,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 +624,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);
|
||||||
@@ -557,5 +652,60 @@ export const convertToExcalidrawElements = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Once all the excalidraw elements are created, we can add frames since we
|
||||||
|
// need to calculate coordinates and dimensions of frame which is possibe after all
|
||||||
|
// frame children are processed.
|
||||||
|
for (const [id, element] of elementsWithIds) {
|
||||||
|
if (element.type !== "frame") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const frame = elementStore.getElement(id);
|
||||||
|
|
||||||
|
if (!frame) {
|
||||||
|
throw new Error(`Excalidraw element with id ${id} doesn't exist`);
|
||||||
|
}
|
||||||
|
const childrenElements: ExcalidrawElement[] = [];
|
||||||
|
|
||||||
|
element.children.forEach((id) => {
|
||||||
|
const newElementId = oldToNewElementIdMap.get(id);
|
||||||
|
if (!newElementId) {
|
||||||
|
throw new Error(`Element with ${id} wasn't mapped correctly`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const elementInFrame = elementStore.getElement(newElementId);
|
||||||
|
if (!elementInFrame) {
|
||||||
|
throw new Error(`Frame element with id ${newElementId} doesn't exist`);
|
||||||
|
}
|
||||||
|
Object.assign(elementInFrame, { frameId: frame.id });
|
||||||
|
|
||||||
|
elementInFrame?.boundElements?.forEach((boundElement) => {
|
||||||
|
const ele = elementStore.getElement(boundElement.id);
|
||||||
|
if (!ele) {
|
||||||
|
throw new Error(
|
||||||
|
`Bound element with id ${boundElement.id} doesn't exist`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Object.assign(ele, { frameId: frame.id });
|
||||||
|
childrenElements.push(ele);
|
||||||
|
});
|
||||||
|
|
||||||
|
childrenElements.push(elementInFrame);
|
||||||
|
});
|
||||||
|
|
||||||
|
let [minX, minY, maxX, maxY] = getCommonBounds(childrenElements);
|
||||||
|
|
||||||
|
const PADDING = 10;
|
||||||
|
minX = minX - PADDING;
|
||||||
|
minY = minY - PADDING;
|
||||||
|
maxX = maxX + PADDING;
|
||||||
|
maxY = maxY + PADDING;
|
||||||
|
|
||||||
|
// Take the max of calculated and provided frame dimensions, whichever is higher
|
||||||
|
const width = Math.max(frame?.width, maxX - minX);
|
||||||
|
const height = Math.max(frame?.height, maxY - minY);
|
||||||
|
Object.assign(frame, { x: minX, y: minY, width, height });
|
||||||
|
}
|
||||||
|
|
||||||
return elementStore.getElements();
|
return elementStore.getElements();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+17
-17
@@ -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,8 +740,8 @@ 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 heavy
|
// This might be computationally heavey
|
||||||
const gen = rough.generator();
|
const gen = rough.generator();
|
||||||
const curve =
|
const curve =
|
||||||
element.roundness == null
|
element.roundness == null
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -547,10 +548,7 @@ export class LinearElementEditor {
|
|||||||
endPointIndex: number,
|
endPointIndex: number,
|
||||||
) {
|
) {
|
||||||
let segmentMidPoint = centerPoint(startPoint, endPoint);
|
let segmentMidPoint = centerPoint(startPoint, endPoint);
|
||||||
const splits = element.segmentSplitIndices || [];
|
if (element.points.length > 2 && element.roundness) {
|
||||||
const treatAsCurve =
|
|
||||||
splits.includes(endPointIndex) || splits.includes(endPointIndex - 1);
|
|
||||||
if (element.points.length > 2 && (element.roundness || treatAsCurve)) {
|
|
||||||
const controlPoints = getControlPointsForBezierCurve(
|
const controlPoints = getControlPointsForBezierCurve(
|
||||||
element,
|
element,
|
||||||
element.points[endPointIndex],
|
element.points[endPointIndex],
|
||||||
@@ -1045,15 +1043,13 @@ export class LinearElementEditor {
|
|||||||
let offsetX = 0;
|
let offsetX = 0;
|
||||||
let offsetY = 0;
|
let offsetY = 0;
|
||||||
|
|
||||||
const indexSet = new Set(pointIndices);
|
const isDeletingOriginPoint = pointIndices.includes(0);
|
||||||
|
|
||||||
const isDeletingOriginPoint = indexSet.has(0);
|
|
||||||
|
|
||||||
// if deleting first point, make the next to be [0,0] and recalculate
|
// if deleting first point, make the next to be [0,0] and recalculate
|
||||||
// positions of the rest with respect to it
|
// positions of the rest with respect to it
|
||||||
if (isDeletingOriginPoint) {
|
if (isDeletingOriginPoint) {
|
||||||
const firstNonDeletedPoint = element.points.find((point, idx) => {
|
const firstNonDeletedPoint = element.points.find((point, idx) => {
|
||||||
return !indexSet.has(idx);
|
return !pointIndices.includes(idx);
|
||||||
});
|
});
|
||||||
if (firstNonDeletedPoint) {
|
if (firstNonDeletedPoint) {
|
||||||
offsetX = firstNonDeletedPoint[0];
|
offsetX = firstNonDeletedPoint[0];
|
||||||
@@ -1062,7 +1058,7 @@ export class LinearElementEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nextPoints = element.points.reduce((acc: Point[], point, idx) => {
|
const nextPoints = element.points.reduce((acc: Point[], point, idx) => {
|
||||||
if (!indexSet.has(idx)) {
|
if (!pointIndices.includes(idx)) {
|
||||||
acc.push(
|
acc.push(
|
||||||
!acc.length ? [0, 0] : [point[0] - offsetX, point[1] - offsetY],
|
!acc.length ? [0, 0] : [point[0] - offsetX, point[1] - offsetY],
|
||||||
);
|
);
|
||||||
@@ -1070,22 +1066,7 @@ export class LinearElementEditor {
|
|||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const splits: number[] = [];
|
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY);
|
||||||
(element.segmentSplitIndices || []).forEach((index) => {
|
|
||||||
if (!indexSet.has(index)) {
|
|
||||||
let shift = 0;
|
|
||||||
for (const pointIndex of pointIndices) {
|
|
||||||
if (index > pointIndex) {
|
|
||||||
shift++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
splits.push(index - shift);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY, {
|
|
||||||
segmentSplitIndices: splits.sort((a, b) => a - b),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static addPoints(
|
static addPoints(
|
||||||
@@ -1224,13 +1205,9 @@ export class LinearElementEditor {
|
|||||||
midpoint,
|
midpoint,
|
||||||
...element.points.slice(segmentMidpoint.index!),
|
...element.points.slice(segmentMidpoint.index!),
|
||||||
];
|
];
|
||||||
const splits = (element.segmentSplitIndices || []).map((index) =>
|
|
||||||
index >= segmentMidpoint.index! ? index + 1 : index,
|
|
||||||
);
|
|
||||||
|
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
points,
|
points,
|
||||||
segmentSplitIndices: splits.sort((a, b) => a - b),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ret.pointerDownState = {
|
ret.pointerDownState = {
|
||||||
@@ -1250,11 +1227,7 @@ export class LinearElementEditor {
|
|||||||
nextPoints: readonly Point[],
|
nextPoints: readonly Point[],
|
||||||
offsetX: number,
|
offsetX: number,
|
||||||
offsetY: number,
|
offsetY: number,
|
||||||
otherUpdates?: {
|
otherUpdates?: { startBinding?: PointBinding; endBinding?: PointBinding },
|
||||||
startBinding?: PointBinding;
|
|
||||||
endBinding?: PointBinding;
|
|
||||||
segmentSplitIndices?: number[];
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
const nextCoords = getElementPointsCoords(element, nextPoints);
|
const nextCoords = getElementPointsCoords(element, nextPoints);
|
||||||
const prevCoords = getElementPointsCoords(element, element.points);
|
const prevCoords = getElementPointsCoords(element, element.points);
|
||||||
@@ -1344,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;
|
||||||
@@ -1500,27 +1473,6 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
return coords;
|
return coords;
|
||||||
};
|
};
|
||||||
|
|
||||||
static toggleSegmentSplitAtIndex(
|
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
|
||||||
index: number,
|
|
||||||
) {
|
|
||||||
let found = false;
|
|
||||||
const splitIndices = (element.segmentSplitIndices || []).filter((idx) => {
|
|
||||||
if (idx === index) {
|
|
||||||
found = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
if (!found) {
|
|
||||||
splitIndices.push(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
mutateElement(element, {
|
|
||||||
segmentSplitIndices: splitIndices.sort((a, b) => a - b),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeSelectedPoints = (
|
const normalizeSelectedPoints = (
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
|
|
||||||
// casting to any because can't use `in` operator
|
// casting to any because can't use `in` operator
|
||||||
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
||||||
const { points, fileId, segmentSplitIndices } = updates as any;
|
const { points, fileId } = updates as any;
|
||||||
|
|
||||||
if (typeof points !== "undefined") {
|
if (typeof points !== "undefined") {
|
||||||
updates = { ...getSizeFromPoints(points), ...updates };
|
updates = { ...getSizeFromPoints(points), ...updates };
|
||||||
@@ -86,7 +86,6 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
if (
|
if (
|
||||||
typeof updates.height !== "undefined" ||
|
typeof updates.height !== "undefined" ||
|
||||||
typeof updates.width !== "undefined" ||
|
typeof updates.width !== "undefined" ||
|
||||||
typeof segmentSplitIndices !== "undefined" ||
|
|
||||||
typeof fileId != "undefined" ||
|
typeof fileId != "undefined" ||
|
||||||
typeof points !== "undefined"
|
typeof points !== "undefined"
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -144,13 +144,15 @@ export const newEmbeddableElement = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const newFrameElement = (
|
export const newFrameElement = (
|
||||||
opts: ElementConstructorOpts,
|
opts: {
|
||||||
|
name?: string;
|
||||||
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawFrameElement> => {
|
): NonDeleted<ExcalidrawFrameElement> => {
|
||||||
const frameElement = newElementWith(
|
const frameElement = newElementWith(
|
||||||
{
|
{
|
||||||
..._newElementBase<ExcalidrawFrameElement>("frame", opts),
|
..._newElementBase<ExcalidrawFrameElement>("frame", opts),
|
||||||
type: "frame",
|
type: "frame",
|
||||||
name: null,
|
name: opts?.name || null,
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
@@ -374,7 +376,6 @@ export const newLinearElement = (
|
|||||||
endBinding: null,
|
endBinding: null,
|
||||||
startArrowhead: opts.startArrowhead || null,
|
startArrowhead: opts.startArrowhead || null,
|
||||||
endArrowhead: opts.endArrowhead || null,
|
endArrowhead: opts.endArrowhead || null,
|
||||||
segmentSplitIndices: [],
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export const redrawTextBoundingBox = (
|
|||||||
);
|
);
|
||||||
const maxContainerWidth = getBoundTextMaxWidth(container);
|
const maxContainerWidth = getBoundTextMaxWidth(container);
|
||||||
|
|
||||||
if (metrics.height > maxContainerHeight) {
|
if (!isArrowElement(container) && metrics.height > maxContainerHeight) {
|
||||||
const nextHeight = computeContainerDimensionForBoundText(
|
const nextHeight = computeContainerDimensionForBoundText(
|
||||||
metrics.height,
|
metrics.height,
|
||||||
container.type,
|
container.type,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { getOriginalContainerHeightFromCache } from "./textWysiwyg";
|
import { getOriginalContainerHeightFromCache } from "./textWysiwyg";
|
||||||
import { getTextEditor } from "../tests/queries/dom";
|
import { getTextEditor, updateTextEditor } from "../tests/queries/dom";
|
||||||
|
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
@@ -26,10 +26,7 @@ ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
|||||||
const tab = " ";
|
const tab = " ";
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
|
|
||||||
const updateTextEditor = (editor: HTMLTextAreaElement, value: string) => {
|
const textEditorSelector = ".excalidraw-textEditorContainer > textarea";
|
||||||
fireEvent.change(editor, { target: { value } });
|
|
||||||
editor.dispatchEvent(new Event("input"));
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("textWysiwyg", () => {
|
describe("textWysiwyg", () => {
|
||||||
describe("start text editing", () => {
|
describe("start text editing", () => {
|
||||||
@@ -195,7 +192,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
mouse.clickAt(text.x + 50, text.y + 50);
|
mouse.clickAt(text.x + 50, text.y + 50);
|
||||||
|
|
||||||
const editor = await getTextEditor(false);
|
const editor = await getTextEditor(textEditorSelector, false);
|
||||||
|
|
||||||
expect(editor).not.toBe(null);
|
expect(editor).not.toBe(null);
|
||||||
expect(h.state.editingElement?.id).toBe(text.id);
|
expect(h.state.editingElement?.id).toBe(text.id);
|
||||||
@@ -217,12 +214,26 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
mouse.doubleClickAt(text.x + 50, text.y + 50);
|
mouse.doubleClickAt(text.x + 50, text.y + 50);
|
||||||
|
|
||||||
const editor = await getTextEditor(false);
|
const editor = await getTextEditor(textEditorSelector, false);
|
||||||
|
|
||||||
expect(editor).not.toBe(null);
|
expect(editor).not.toBe(null);
|
||||||
expect(h.state.editingElement?.id).toBe(text.id);
|
expect(h.state.editingElement?.id).toBe(text.id);
|
||||||
expect(h.elements.length).toBe(1);
|
expect(h.elements.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FIXME too flaky. No one knows why.
|
||||||
|
it.skip("should bump the version of a labeled arrow when the label is updated", async () => {
|
||||||
|
const arrow = UI.createElement("arrow", {
|
||||||
|
width: 300,
|
||||||
|
height: 0,
|
||||||
|
});
|
||||||
|
await UI.editText(arrow, "Hello");
|
||||||
|
const { version } = arrow;
|
||||||
|
|
||||||
|
await UI.editText(arrow, "Hello\nworld!");
|
||||||
|
|
||||||
|
expect(arrow.version).toEqual(version + 1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test container-unbound text", () => {
|
describe("Test container-unbound text", () => {
|
||||||
@@ -244,7 +255,7 @@ describe("textWysiwyg", () => {
|
|||||||
textElement = UI.createElement("text");
|
textElement = UI.createElement("text");
|
||||||
|
|
||||||
mouse.clickOn(textElement);
|
mouse.clickOn(textElement);
|
||||||
textarea = await getTextEditor(true);
|
textarea = await getTextEditor(textEditorSelector, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
@@ -454,7 +465,7 @@ describe("textWysiwyg", () => {
|
|||||||
UI.clickTool("text");
|
UI.clickTool("text");
|
||||||
mouse.clickAt(750, 300);
|
mouse.clickAt(750, 300);
|
||||||
|
|
||||||
textarea = await getTextEditor(true);
|
textarea = await getTextEditor(textEditorSelector, true);
|
||||||
updateTextEditor(
|
updateTextEditor(
|
||||||
textarea,
|
textarea,
|
||||||
"Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
|
"Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
|
||||||
@@ -506,7 +517,7 @@ describe("textWysiwyg", () => {
|
|||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
]);
|
]);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
const editor = await getTextEditor(true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
@@ -534,7 +545,7 @@ describe("textWysiwyg", () => {
|
|||||||
]);
|
]);
|
||||||
expect(text.angle).toBe(rectangle.angle);
|
expect(text.angle).toBe(rectangle.angle);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
const editor = await getTextEditor(true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
@@ -561,7 +572,7 @@ describe("textWysiwyg", () => {
|
|||||||
API.setSelectedElements([diamond]);
|
API.setSelectedElements([diamond]);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
const editor = await getTextEditor(true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
const value = new Array(1000).fill("1").join("\n");
|
const value = new Array(1000).fill("1").join("\n");
|
||||||
@@ -596,7 +607,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect(text.type).toBe("text");
|
expect(text.type).toBe("text");
|
||||||
expect(text.containerId).toBe(null);
|
expect(text.containerId).toBe(null);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
let editor = await getTextEditor(true);
|
let editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
|
|
||||||
@@ -611,7 +622,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
|
|
||||||
mouse.down();
|
mouse.down();
|
||||||
editor = await getTextEditor(true);
|
editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
@@ -633,7 +644,7 @@ describe("textWysiwyg", () => {
|
|||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
expect(text.type).toBe("text");
|
expect(text.type).toBe("text");
|
||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
const editor = await getTextEditor(true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
@@ -668,7 +679,7 @@ describe("textWysiwyg", () => {
|
|||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
]);
|
]);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
const editor = await getTextEditor(true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
@@ -693,7 +704,7 @@ describe("textWysiwyg", () => {
|
|||||||
freedraw.y + freedraw.height / 2,
|
freedraw.y + freedraw.height / 2,
|
||||||
);
|
);
|
||||||
|
|
||||||
const editor = await getTextEditor(true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
|
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
|
||||||
|
|
||||||
@@ -727,7 +738,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect(text.type).toBe("text");
|
expect(text.type).toBe("text");
|
||||||
expect(text.containerId).toBe(null);
|
expect(text.containerId).toBe(null);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
const editor = await getTextEditor(true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
@@ -742,7 +753,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
UI.clickTool("text");
|
UI.clickTool("text");
|
||||||
mouse.clickAt(20, 30);
|
mouse.clickAt(20, 30);
|
||||||
const editor = await getTextEditor(true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
updateTextEditor(
|
updateTextEditor(
|
||||||
editor,
|
editor,
|
||||||
@@ -787,7 +798,7 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.down();
|
mouse.down();
|
||||||
|
|
||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
let editor = await getTextEditor(true);
|
let editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
@@ -800,7 +811,7 @@ describe("textWysiwyg", () => {
|
|||||||
rectangle.y + rectangle.height / 2,
|
rectangle.y + rectangle.height / 2,
|
||||||
);
|
);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
editor = await getTextEditor(true);
|
editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
editor.select();
|
editor.select();
|
||||||
fireEvent.click(screen.getByTitle(/code/i));
|
fireEvent.click(screen.getByTitle(/code/i));
|
||||||
@@ -833,7 +844,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
Keyboard.keyDown(KEYS.ENTER);
|
Keyboard.keyDown(KEYS.ENTER);
|
||||||
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
let editor = await getTextEditor(true);
|
let editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
@@ -854,7 +865,7 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
editor = await getTextEditor(true);
|
editor = await getTextEditor(textEditorSelector, true);
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
@@ -883,7 +894,7 @@ describe("textWysiwyg", () => {
|
|||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
|
|
||||||
const editor = await getTextEditor(true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
@@ -920,7 +931,7 @@ describe("textWysiwyg", () => {
|
|||||||
// Bind first text
|
// Bind first text
|
||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
const editor = await getTextEditor(true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -941,7 +952,7 @@ describe("textWysiwyg", () => {
|
|||||||
it("should respect text alignment when resizing", async () => {
|
it("should respect text alignment when resizing", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
let editor = await getTextEditor(true);
|
let editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -958,7 +969,7 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
editor = await getTextEditor(true);
|
editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
editor.select();
|
editor.select();
|
||||||
|
|
||||||
@@ -981,7 +992,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
editor = await getTextEditor(true);
|
editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
editor.select();
|
editor.select();
|
||||||
|
|
||||||
@@ -1019,7 +1030,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect(text.type).toBe("text");
|
expect(text.type).toBe("text");
|
||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
const editor = await getTextEditor(true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
@@ -1034,7 +1045,7 @@ describe("textWysiwyg", () => {
|
|||||||
it("should scale font size correctly when resizing using shift", async () => {
|
it("should scale font size correctly when resizing using shift", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
const editor = await getTextEditor(true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -1054,7 +1065,7 @@ describe("textWysiwyg", () => {
|
|||||||
it("should bind text correctly when container duplicated with alt-drag", async () => {
|
it("should bind text correctly when container duplicated with alt-drag", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
const editor = await getTextEditor(true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -1086,7 +1097,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
it("undo should work", async () => {
|
it("undo should work", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
const editor = await getTextEditor(true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -1123,7 +1134,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
it("should not allow bound text with only whitespaces", async () => {
|
it("should not allow bound text with only whitespaces", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
const editor = await getTextEditor(true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
updateTextEditor(editor, " ");
|
updateTextEditor(editor, " ");
|
||||||
@@ -1178,7 +1189,7 @@ describe("textWysiwyg", () => {
|
|||||||
it("should reset the container height cache when resizing", async () => {
|
it("should reset the container height cache when resizing", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||||
let editor = await getTextEditor(true);
|
let editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -1190,7 +1201,7 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
editor = await getTextEditor(true);
|
editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -1206,7 +1217,7 @@ describe("textWysiwyg", () => {
|
|||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||||
|
|
||||||
const editor = await getTextEditor(true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
|
|
||||||
@@ -1231,7 +1242,7 @@ describe("textWysiwyg", () => {
|
|||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||||
|
|
||||||
const editor = await getTextEditor(true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(
|
expect(
|
||||||
@@ -1263,12 +1274,12 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
editor = await getTextEditor(true);
|
editor = await getTextEditor(textEditorSelector, true);
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
editor = await getTextEditor(true);
|
editor = await getTextEditor(textEditorSelector, true);
|
||||||
editor.select();
|
editor.select();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1379,7 +1390,7 @@ describe("textWysiwyg", () => {
|
|||||||
it("should wrap text in a container when wrap text in container triggered from context menu", async () => {
|
it("should wrap text in a container when wrap text in container triggered from context menu", async () => {
|
||||||
UI.clickTool("text");
|
UI.clickTool("text");
|
||||||
mouse.clickAt(20, 30);
|
mouse.clickAt(20, 30);
|
||||||
const editor = await getTextEditor(true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
updateTextEditor(
|
updateTextEditor(
|
||||||
editor,
|
editor,
|
||||||
@@ -1467,7 +1478,7 @@ describe("textWysiwyg", () => {
|
|||||||
// Bind first text
|
// Bind first text
|
||||||
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
let editor = await getTextEditor(true);
|
let editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello!");
|
updateTextEditor(editor, "Hello!");
|
||||||
expect(
|
expect(
|
||||||
@@ -1492,7 +1503,7 @@ describe("textWysiwyg", () => {
|
|||||||
rectangle.x + rectangle.width / 2,
|
rectangle.x + rectangle.width / 2,
|
||||||
rectangle.y + rectangle.height / 2,
|
rectangle.y + rectangle.height / 2,
|
||||||
);
|
);
|
||||||
editor = await getTextEditor(true);
|
editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Excalidraw");
|
updateTextEditor(editor, "Excalidraw");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -1506,18 +1517,4 @@ describe("textWysiwyg", () => {
|
|||||||
expect(text.text).toBe("Excalidraw");
|
expect(text.text).toBe("Excalidraw");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should bump the version of a labeled arrow when the label is updated", async () => {
|
|
||||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
|
||||||
const arrow = UI.createElement("arrow", {
|
|
||||||
width: 300,
|
|
||||||
height: 0,
|
|
||||||
});
|
|
||||||
await UI.editText(arrow, "Hello");
|
|
||||||
const { version } = arrow;
|
|
||||||
|
|
||||||
await UI.editText(arrow, "Hello\nworld!");
|
|
||||||
|
|
||||||
expect(arrow.version).toEqual(version + 1);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
}>;
|
}>;
|
||||||
|
|||||||
@@ -195,7 +195,6 @@ export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
|||||||
Readonly<{
|
Readonly<{
|
||||||
type: "line" | "arrow";
|
type: "line" | "arrow";
|
||||||
points: readonly Point[];
|
points: readonly Point[];
|
||||||
segmentSplitIndices: readonly number[] | null;
|
|
||||||
lastCommittedPoint: Point | null;
|
lastCommittedPoint: Point | null;
|
||||||
startBinding: PointBinding | null;
|
startBinding: PointBinding | null;
|
||||||
endBinding: PointBinding | null;
|
endBinding: PointBinding | null;
|
||||||
|
|||||||
+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 = (
|
||||||
|
|||||||
@@ -17,9 +17,13 @@ export const useCreatePortalContainer = (opts?: {
|
|||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (div) {
|
if (div) {
|
||||||
|
div.className = "";
|
||||||
|
div.classList.add("excalidraw", ...(opts?.className?.split(/\s+/) || []));
|
||||||
div.classList.toggle("excalidraw--mobile", device.isMobile);
|
div.classList.toggle("excalidraw--mobile", device.isMobile);
|
||||||
|
div.classList.toggle("excalidraw--mobile", isMobileRef.current);
|
||||||
|
div.classList.toggle("theme--dark", theme === "dark");
|
||||||
}
|
}
|
||||||
}, [div, device.isMobile]);
|
}, [div, theme, device.isMobile, opts?.className]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const container = opts?.parentSelector
|
const container = opts?.parentSelector
|
||||||
@@ -32,10 +36,6 @@ export const useCreatePortalContainer = (opts?: {
|
|||||||
|
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
|
|
||||||
div.classList.add("excalidraw", ...(opts?.className?.split(/\s+/) || []));
|
|
||||||
div.classList.toggle("excalidraw--mobile", isMobileRef.current);
|
|
||||||
div.classList.toggle("theme--dark", theme === "dark");
|
|
||||||
|
|
||||||
container.appendChild(div);
|
container.appendChild(div);
|
||||||
|
|
||||||
setDiv(div);
|
setDiv(div);
|
||||||
@@ -43,7 +43,7 @@ export const useCreatePortalContainer = (opts?: {
|
|||||||
return () => {
|
return () => {
|
||||||
container.removeChild(div);
|
container.removeChild(div);
|
||||||
};
|
};
|
||||||
}, [excalidrawContainer, theme, opts?.className, opts?.parentSelector]);
|
}, [excalidrawContainer, opts?.parentSelector]);
|
||||||
|
|
||||||
return div;
|
return div;
|
||||||
};
|
};
|
||||||
|
|||||||
+13
-2
@@ -218,7 +218,10 @@
|
|||||||
"libraryElementTypeError": {
|
"libraryElementTypeError": {
|
||||||
"embeddable": "Embeddable elements cannot be added to the library.",
|
"embeddable": "Embeddable elements cannot be added to the library.",
|
||||||
"image": "Support for adding images to the library coming soon!"
|
"image": "Support for adding images to the library coming soon!"
|
||||||
}
|
},
|
||||||
|
"asyncPasteFailedOnRead": "Couldn't paste (couldn't read from system clipboard).",
|
||||||
|
"asyncPasteFailedOnParse": "Couldn't paste.",
|
||||||
|
"copyToSystemClipboardFailed": "Couldn't copy to clipboard."
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selection",
|
"selection": "Selection",
|
||||||
@@ -239,7 +242,8 @@
|
|||||||
"embeddable": "Web Embed",
|
"embeddable": "Web Embed",
|
||||||
"laser": "Laser pointer",
|
"laser": "Laser pointer",
|
||||||
"hand": "Hand (panning tool)",
|
"hand": "Hand (panning tool)",
|
||||||
"extraTools": "More tools"
|
"extraTools": "More tools",
|
||||||
|
"mermaidToExcalidraw": "Mermaid to Excalidraw"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Canvas actions",
|
"canvasActions": "Canvas actions",
|
||||||
@@ -498,5 +502,12 @@
|
|||||||
"description": "Loading external drawing will <bold>replace your existing content</bold>.<br></br>You can back up your drawing first by using one of the options below."
|
"description": "Loading external drawing will <bold>replace your existing content</bold>.<br></br>You can back up your drawing first by using one of the options below."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"mermaid": {
|
||||||
|
"title": "Mermaid to Excalidraw",
|
||||||
|
"button": "Insert",
|
||||||
|
"description": "Currently only <flowchartLink>Flowcharts</flowchartLink> and <sequenceLink>Sequence Diagrams</sequenceLink> are supported. The other types will be rendered as image in Excalidraw.",
|
||||||
|
"syntax": "Mermaid Syntax",
|
||||||
|
"preview": "Preview"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,16 +1,16 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"path": "dist/excalidraw.production.min.js",
|
"path": "dist/excalidraw.production.min.js",
|
||||||
"limit": "305 kB"
|
"limit": "325 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",
|
||||||
"name": "dist/excalidraw-assets/vendor*.js",
|
"name": "dist/excalidraw-assets/vendor*.js",
|
||||||
"limit": "30 kB"
|
"limit": "900 kB"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const elements: ExcalidrawElementSkeleton[] = [
|
|||||||
x: 10,
|
x: 10,
|
||||||
y: 10,
|
y: 10,
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
|
id: "1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "diamond",
|
type: "diamond",
|
||||||
@@ -19,6 +20,7 @@ const elements: ExcalidrawElementSkeleton[] = [
|
|||||||
strokeColor: "#099268",
|
strokeColor: "#099268",
|
||||||
fontSize: 30,
|
fontSize: 30,
|
||||||
},
|
},
|
||||||
|
id: "2",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
@@ -36,6 +38,11 @@ const elements: ExcalidrawElementSkeleton[] = [
|
|||||||
height: 230,
|
height: 230,
|
||||||
fileId: "rocket" as FileId,
|
fileId: "rocket" as FileId,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: "frame",
|
||||||
|
children: ["1", "2"],
|
||||||
|
name: "My frame",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
export default {
|
export default {
|
||||||
elements,
|
elements,
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -41,6 +41,14 @@ module.exports = {
|
|||||||
"sass-loader",
|
"sass-loader",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// So that type module works with webpack
|
||||||
|
// https://github.com/webpack/webpack/issues/11467#issuecomment-691873586
|
||||||
|
{
|
||||||
|
test: /\.m?js/,
|
||||||
|
resolve: {
|
||||||
|
fullySpecified: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /\.(ts|tsx|js|jsx|mjs)$/,
|
test: /\.(ts|tsx|js|jsx|mjs)$/,
|
||||||
exclude:
|
exclude:
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ module.exports = {
|
|||||||
"sass-loader",
|
"sass-loader",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// So that type module works with webpack
|
||||||
|
// https://github.com/webpack/webpack/issues/11467#issuecomment-691873586
|
||||||
|
{
|
||||||
|
test: /\.m?js/,
|
||||||
|
resolve: {
|
||||||
|
fullySpecified: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /\.(ts|tsx|js|jsx|mjs)$/,
|
test: /\.(ts|tsx|js|jsx|mjs)$/,
|
||||||
exclude:
|
exclude:
|
||||||
|
|||||||
@@ -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));
|
||||||
|
};
|
||||||
+18
-69
@@ -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,
|
||||||
@@ -166,21 +165,6 @@ const fillCircle = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fillSquare = (
|
|
||||||
context: CanvasRenderingContext2D,
|
|
||||||
cx: number,
|
|
||||||
cy: number,
|
|
||||||
side: number,
|
|
||||||
stroke = true,
|
|
||||||
) => {
|
|
||||||
context.beginPath();
|
|
||||||
context.rect(cx - side / 2, cy - side / 2, side, side);
|
|
||||||
context.fill();
|
|
||||||
if (stroke) {
|
|
||||||
context.stroke();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const strokeGrid = (
|
const strokeGrid = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
gridSize: number,
|
gridSize: number,
|
||||||
@@ -239,7 +223,6 @@ const renderSingleLinearPoint = (
|
|||||||
point: Point,
|
point: Point,
|
||||||
radius: number,
|
radius: number,
|
||||||
isSelected: boolean,
|
isSelected: boolean,
|
||||||
renderAsSquare: boolean,
|
|
||||||
isPhantomPoint = false,
|
isPhantomPoint = false,
|
||||||
) => {
|
) => {
|
||||||
context.strokeStyle = "#5e5ad8";
|
context.strokeStyle = "#5e5ad8";
|
||||||
@@ -251,29 +234,13 @@ const renderSingleLinearPoint = (
|
|||||||
context.fillStyle = "rgba(177, 151, 252, 0.7)";
|
context.fillStyle = "rgba(177, 151, 252, 0.7)";
|
||||||
}
|
}
|
||||||
|
|
||||||
const effectiveRadius = radius / appState.zoom.value;
|
fillCircle(
|
||||||
|
context,
|
||||||
if (renderAsSquare) {
|
point[0],
|
||||||
fillSquare(
|
point[1],
|
||||||
context,
|
radius / appState.zoom.value,
|
||||||
point[0],
|
!isPhantomPoint,
|
||||||
point[1],
|
);
|
||||||
effectiveRadius * 2,
|
|
||||||
!isPhantomPoint,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
fillCircle(context, point[0], point[1], effectiveRadius, !isPhantomPoint);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isLinearPointAtIndexSquared = (
|
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
|
||||||
index: number,
|
|
||||||
) => {
|
|
||||||
const splitting = element.segmentSplitIndices
|
|
||||||
? element.segmentSplitIndices.includes(index)
|
|
||||||
: false;
|
|
||||||
return element.roundness ? splitting : !splitting;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderLinearPointHandles = (
|
const renderLinearPointHandles = (
|
||||||
@@ -297,14 +264,7 @@ const renderLinearPointHandles = (
|
|||||||
const isSelected =
|
const isSelected =
|
||||||
!!appState.editingLinearElement?.selectedPointsIndices?.includes(idx);
|
!!appState.editingLinearElement?.selectedPointsIndices?.includes(idx);
|
||||||
|
|
||||||
renderSingleLinearPoint(
|
renderSingleLinearPoint(context, appState, point, radius, isSelected);
|
||||||
context,
|
|
||||||
appState,
|
|
||||||
point,
|
|
||||||
radius,
|
|
||||||
isSelected,
|
|
||||||
isLinearPointAtIndexSquared(element, idx),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//Rendering segment mid points
|
//Rendering segment mid points
|
||||||
@@ -332,7 +292,6 @@ const renderLinearPointHandles = (
|
|||||||
segmentMidPoint,
|
segmentMidPoint,
|
||||||
radius,
|
radius,
|
||||||
false,
|
false,
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
highlightPoint(segmentMidPoint, context, appState);
|
highlightPoint(segmentMidPoint, context, appState);
|
||||||
} else {
|
} else {
|
||||||
@@ -343,7 +302,6 @@ const renderLinearPointHandles = (
|
|||||||
segmentMidPoint,
|
segmentMidPoint,
|
||||||
radius,
|
radius,
|
||||||
false,
|
false,
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (appState.editingLinearElement || points.length === 2) {
|
} else if (appState.editingLinearElement || points.length === 2) {
|
||||||
@@ -353,7 +311,6 @@ const renderLinearPointHandles = (
|
|||||||
segmentMidPoint,
|
segmentMidPoint,
|
||||||
POINT_HANDLE_SIZE / 2,
|
POINT_HANDLE_SIZE / 2,
|
||||||
false,
|
false,
|
||||||
false,
|
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -366,16 +323,16 @@ const highlightPoint = (
|
|||||||
point: Point,
|
point: Point,
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
appState: InteractiveCanvasAppState,
|
appState: InteractiveCanvasAppState,
|
||||||
renderAsSquare = false,
|
|
||||||
) => {
|
) => {
|
||||||
context.fillStyle = "rgba(105, 101, 219, 0.4)";
|
context.fillStyle = "rgba(105, 101, 219, 0.4)";
|
||||||
const radius = LinearElementEditor.POINT_HANDLE_SIZE / appState.zoom.value;
|
|
||||||
|
|
||||||
if (renderAsSquare) {
|
fillCircle(
|
||||||
fillSquare(context, point[0], point[1], radius * 2, false);
|
context,
|
||||||
} else {
|
point[0],
|
||||||
fillCircle(context, point[0], point[1], radius, false);
|
point[1],
|
||||||
}
|
LinearElementEditor.POINT_HANDLE_SIZE / appState.zoom.value,
|
||||||
|
false,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
const renderLinearElementPointHighlight = (
|
const renderLinearElementPointHighlight = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
@@ -397,15 +354,10 @@ const renderLinearElementPointHighlight = (
|
|||||||
element,
|
element,
|
||||||
hoverPointIndex,
|
hoverPointIndex,
|
||||||
);
|
);
|
||||||
|
|
||||||
context.save();
|
context.save();
|
||||||
context.translate(appState.scrollX, appState.scrollY);
|
context.translate(appState.scrollX, appState.scrollY);
|
||||||
highlightPoint(
|
|
||||||
point,
|
highlightPoint(point, context, appState);
|
||||||
context,
|
|
||||||
appState,
|
|
||||||
isLinearPointAtIndexSquared(element, hoverPointIndex),
|
|
||||||
);
|
|
||||||
context.restore();
|
context.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1032,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();
|
||||||
|
|||||||
+5
-51
@@ -14,7 +14,6 @@ import { generateFreeDrawShape } from "../renderer/renderElement";
|
|||||||
import { isTransparent, assertNever } from "../utils";
|
import { isTransparent, assertNever } from "../utils";
|
||||||
import { simplify } from "points-on-curve";
|
import { simplify } from "points-on-curve";
|
||||||
import { ROUGHNESS } from "../constants";
|
import { ROUGHNESS } from "../constants";
|
||||||
import { Point } from "../types";
|
|
||||||
|
|
||||||
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
||||||
|
|
||||||
@@ -229,44 +228,18 @@ export const _generateElementShape = (
|
|||||||
|
|
||||||
// points array can be empty in the beginning, so it is important to add
|
// points array can be empty in the beginning, so it is important to add
|
||||||
// initial position to it
|
// initial position to it
|
||||||
const points = element.points.length
|
const points = element.points.length ? element.points : [[0, 0]];
|
||||||
? element.points
|
|
||||||
: ([[0, 0]] as Point[]);
|
|
||||||
|
|
||||||
// curve is always the first element
|
// curve is always the first element
|
||||||
// this simplifies finding the curve for an element
|
// this simplifies finding the curve for an element
|
||||||
const splits = element.segmentSplitIndices || [];
|
|
||||||
if (!element.roundness) {
|
if (!element.roundness) {
|
||||||
if (splits.length === 0) {
|
if (options.fill) {
|
||||||
if (options.fill) {
|
shape = [generator.polygon(points as [number, number][], options)];
|
||||||
shape = [generator.polygon(points as [number, number][], options)];
|
|
||||||
} else {
|
|
||||||
shape = [
|
|
||||||
generator.linearPath(points as [number, number][], options),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const splitInverse: number[] = [];
|
shape = [generator.linearPath(points as [number, number][], options)];
|
||||||
const splitSet = new Set(splits);
|
|
||||||
for (let i = 0; i < points.length; i++) {
|
|
||||||
if (!splitSet.has(i)) {
|
|
||||||
splitInverse.push(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
shape = [
|
|
||||||
generator.curve(
|
|
||||||
computeMultipleCurvesFromSplits(points, splitInverse),
|
|
||||||
options,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
shape = [
|
shape = [generator.curve(points as [number, number][], options)];
|
||||||
generator.curve(
|
|
||||||
computeMultipleCurvesFromSplits(points, splits),
|
|
||||||
options,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// add lines only in arrow
|
// add lines only in arrow
|
||||||
@@ -403,22 +376,3 @@ export const _generateElementShape = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const computeMultipleCurvesFromSplits = (
|
|
||||||
points: readonly Point[],
|
|
||||||
splits: readonly number[],
|
|
||||||
): [number, number][][] => {
|
|
||||||
const pointList: Point[][] = [];
|
|
||||||
let currentIndex = 0;
|
|
||||||
for (const index of splits) {
|
|
||||||
const slice = points.slice(currentIndex, index + 1);
|
|
||||||
if (slice.length) {
|
|
||||||
pointList.push([...slice]);
|
|
||||||
}
|
|
||||||
currentIndex = index;
|
|
||||||
}
|
|
||||||
if (currentIndex < points.length - 1) {
|
|
||||||
pointList.push(points.slice(currentIndex));
|
|
||||||
}
|
|
||||||
return pointList as [number, number][][];
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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 = (
|
||||||
|
|||||||
+7
-3
@@ -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";
|
||||||
@@ -13,7 +17,7 @@ import {
|
|||||||
} from "../element/image";
|
} from "../element/image";
|
||||||
import Scene from "./Scene";
|
import Scene from "./Scene";
|
||||||
|
|
||||||
export const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
||||||
|
|
||||||
export const exportToCanvas = async (
|
export const exportToCanvas = async (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import "vitest-canvas-mock";
|
|||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
import polyfill from "./polyfill";
|
import polyfill from "./polyfill";
|
||||||
|
import { testPolyfills } from "./tests/helpers/polyfills";
|
||||||
|
|
||||||
|
Object.assign(globalThis, testPolyfills);
|
||||||
|
|
||||||
require("fake-indexeddb/auto");
|
require("fake-indexeddb/auto");
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import { act, fireEvent, render } from "./test-utils";
|
||||||
|
import { Excalidraw } from "../packages/excalidraw/index";
|
||||||
|
import React from "react";
|
||||||
|
import { expect, vi } from "vitest";
|
||||||
|
import * as MermaidToExcalidraw from "@excalidraw/mermaid-to-excalidraw";
|
||||||
|
import { getTextEditor, updateTextEditor } from "./queries/dom";
|
||||||
|
|
||||||
|
vi.mock("@excalidraw/mermaid-to-excalidraw", async (importActual) => {
|
||||||
|
const module = (await importActual()) as any;
|
||||||
|
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
...module,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const parseMermaidToExcalidrawSpy = vi.spyOn(
|
||||||
|
MermaidToExcalidraw,
|
||||||
|
"parseMermaidToExcalidraw",
|
||||||
|
);
|
||||||
|
|
||||||
|
parseMermaidToExcalidrawSpy.mockImplementation(
|
||||||
|
async (
|
||||||
|
definition: string,
|
||||||
|
options?: MermaidToExcalidraw.MermaidOptions | undefined,
|
||||||
|
) => {
|
||||||
|
const firstLine = definition.split("\n")[0];
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (firstLine === "flowchart TD") {
|
||||||
|
resolve({
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
id: "Start",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: [],
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 69.703125,
|
||||||
|
height: 44,
|
||||||
|
strokeWidth: 2,
|
||||||
|
label: {
|
||||||
|
groupIds: [],
|
||||||
|
text: "Start",
|
||||||
|
fontSize: 20,
|
||||||
|
},
|
||||||
|
link: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Stop",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: [],
|
||||||
|
x: 2.7109375,
|
||||||
|
y: 94,
|
||||||
|
width: 64.28125,
|
||||||
|
height: 44,
|
||||||
|
strokeWidth: 2,
|
||||||
|
label: {
|
||||||
|
groupIds: [],
|
||||||
|
text: "Stop",
|
||||||
|
fontSize: 20,
|
||||||
|
},
|
||||||
|
link: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Start_Stop",
|
||||||
|
type: "arrow",
|
||||||
|
groupIds: [],
|
||||||
|
x: 34.852,
|
||||||
|
y: 44,
|
||||||
|
strokeWidth: 2,
|
||||||
|
points: [
|
||||||
|
[0, 0],
|
||||||
|
[0, 50],
|
||||||
|
],
|
||||||
|
roundness: {
|
||||||
|
type: 2,
|
||||||
|
},
|
||||||
|
start: {
|
||||||
|
id: "Start",
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
id: "Stop",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
reject(new Error("ERROR"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.spyOn(React, "useRef").mockReturnValue({
|
||||||
|
current: {
|
||||||
|
parseMermaidToExcalidraw: parseMermaidToExcalidrawSpy,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test <MermaidToExcalidraw/>", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await render(
|
||||||
|
<Excalidraw
|
||||||
|
initialData={{
|
||||||
|
appState: {
|
||||||
|
openDialog: "mermaid",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should open mermaid popup when active tool is mermaid", async () => {
|
||||||
|
const dialog = document.querySelector(".dialog-mermaid")!;
|
||||||
|
|
||||||
|
expect(dialog.outerHTML).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should close the popup and set the tool to selection when close button clicked", () => {
|
||||||
|
const dialog = document.querySelector(".dialog-mermaid")!;
|
||||||
|
const closeBtn = dialog.querySelector(".Dialog__close")!;
|
||||||
|
fireEvent.click(closeBtn);
|
||||||
|
expect(document.querySelector(".dialog-mermaid")).toBe(null);
|
||||||
|
expect(window.h.state.activeTool).toStrictEqual({
|
||||||
|
customType: null,
|
||||||
|
lastActiveTool: null,
|
||||||
|
locked: false,
|
||||||
|
type: "selection",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show error in preview when mermaid library throws error", async () => {
|
||||||
|
const dialog = document.querySelector(".dialog-mermaid")!;
|
||||||
|
const selector = ".dialog-mermaid-panels-text textarea";
|
||||||
|
let editor = await getTextEditor(selector, false);
|
||||||
|
|
||||||
|
expect(dialog.querySelector('[data-testid="mermaid-error"]')).toBeNull();
|
||||||
|
|
||||||
|
expect(editor.textContent).toMatchInlineSnapshot(`
|
||||||
|
"flowchart TD
|
||||||
|
A[Christmas] -->|Get money| B(Go shopping)
|
||||||
|
B --> C{Let me think}
|
||||||
|
C -->|One| D[Laptop]
|
||||||
|
C -->|Two| E[iPhone]
|
||||||
|
C -->|Three| F[Car]"
|
||||||
|
`);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
updateTextEditor(editor, "flowchart TD1");
|
||||||
|
await new Promise((cb) => setTimeout(cb, 0));
|
||||||
|
});
|
||||||
|
editor = await getTextEditor(selector, false);
|
||||||
|
|
||||||
|
expect(editor.textContent).toBe("flowchart TD1");
|
||||||
|
expect(dialog.querySelector('[data-testid="mermaid-error"]'))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
<div
|
||||||
|
class="mermaid-error"
|
||||||
|
data-testid="mermaid-error"
|
||||||
|
>
|
||||||
|
Error!
|
||||||
|
<p>
|
||||||
|
ERROR
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`Test <MermaidToExcalidraw/> > should open mermaid popup when active tool is mermaid 1`] = `
|
||||||
|
"<div class=\\"Modal Dialog dialog-mermaid\\" role=\\"dialog\\" aria-modal=\\"true\\" aria-labelledby=\\"dialog-title\\" data-prevent-outside-click=\\"true\\"><div class=\\"Modal__background\\"></div><div class=\\"Modal__content\\" style=\\"--max-width: 1200px;\\" tabindex=\\"0\\"><div class=\\"Island\\"><h2 id=\\"test-id-dialog-title\\" class=\\"Dialog__title\\"><span class=\\"Dialog__titleContent\\"><p class=\\"dialog-mermaid-title\\">Mermaid to Excalidraw</p><span class=\\"dialog-mermaid-desc\\">Currently only <a href=\\"https://mermaid.js.org/syntax/flowchart.html\\">Flowcharts</a> and <a href=\\"https://mermaid.js.org/syntax/sequenceDiagram.html\\">Sequence Diagrams</a> are supported. The other types will be rendered as image in Excalidraw.<br></span></span></h2><button class=\\"Dialog__close\\" title=\\"Close\\" aria-label=\\"Close\\"><svg aria-hidden=\\"true\\" focusable=\\"false\\" role=\\"img\\" viewBox=\\"0 0 20 20\\" class=\\"\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\"><g clip-path=\\"url(#a)\\" stroke=\\"currentColor\\" stroke-width=\\"1.25\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\"><path d=\\"M15 5 5 15M5 5l10 10\\"></path></g><defs><clipPath id=\\"a\\"><path fill=\\"#fff\\" d=\\"M0 0h20v20H0z\\"></path></clipPath></defs></svg></button><div class=\\"Dialog__content\\"><div class=\\"dialog-mermaid-body\\"><div class=\\"dialog-mermaid-panels\\"><div class=\\"dialog-mermaid-panels-text\\"><label>Mermaid Syntax</label><textarea>flowchart TD
|
||||||
|
A[Christmas] -->|Get money| B(Go shopping)
|
||||||
|
B --> C{Let me think}
|
||||||
|
C -->|One| D[Laptop]
|
||||||
|
C -->|Two| E[iPhone]
|
||||||
|
C -->|Three| F[Car]</textarea></div><div class=\\"dialog-mermaid-panels-preview\\"><label>Preview</label><div class=\\"dialog-mermaid-panels-preview-wrapper\\"><div style=\\"opacity: 1;\\" class=\\"dialog-mermaid-panels-preview-canvas-container\\"></div></div></div></div><div class=\\"dialog-mermaid-buttons\\"><button type=\\"button\\" class=\\"excalidraw-button dialog-mermaid-insert\\">Insert<span><svg aria-hidden=\\"true\\" focusable=\\"false\\" role=\\"img\\" viewBox=\\"0 0 20 20\\" class=\\"\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\"><g stroke-width=\\"1.25\\"><path d=\\"M4.16602 10H15.8327\\"></path><path d=\\"M12.5 13.3333L15.8333 10\\"></path><path d=\\"M12.5 6.66666L15.8333 9.99999\\"></path></g></svg></span></button></div></div></div></div></div></div>"
|
||||||
|
`;
|
||||||
@@ -17,7 +17,6 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"keyTest": [Function],
|
"keyTest": [Function],
|
||||||
"name": "cut",
|
"name": "cut",
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": {
|
"trackEvent": {
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
@@ -27,7 +26,6 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"keyTest": undefined,
|
"keyTest": undefined,
|
||||||
"name": "copy",
|
"name": "copy",
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": {
|
"trackEvent": {
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
@@ -37,7 +35,6 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"keyTest": undefined,
|
"keyTest": undefined,
|
||||||
"name": "paste",
|
"name": "paste",
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": {
|
"trackEvent": {
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
@@ -266,7 +263,6 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"keyTest": [Function],
|
"keyTest": [Function],
|
||||||
"name": "toggleElementLock",
|
"name": "toggleElementLock",
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": {
|
"trackEvent": {
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
@@ -4604,7 +4600,6 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"keyTest": [Function],
|
"keyTest": [Function],
|
||||||
"name": "cut",
|
"name": "cut",
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": {
|
"trackEvent": {
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
@@ -4614,7 +4609,6 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"keyTest": undefined,
|
"keyTest": undefined,
|
||||||
"name": "copy",
|
"name": "copy",
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": {
|
"trackEvent": {
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
@@ -4624,7 +4618,6 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"keyTest": undefined,
|
"keyTest": undefined,
|
||||||
"name": "paste",
|
"name": "paste",
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": {
|
"trackEvent": {
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
@@ -4853,7 +4846,6 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"keyTest": [Function],
|
"keyTest": [Function],
|
||||||
"name": "toggleElementLock",
|
"name": "toggleElementLock",
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": {
|
"trackEvent": {
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
@@ -5187,7 +5179,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"keyTest": [Function],
|
"keyTest": [Function],
|
||||||
"name": "cut",
|
"name": "cut",
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": {
|
"trackEvent": {
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
@@ -5197,7 +5188,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"keyTest": undefined,
|
"keyTest": undefined,
|
||||||
"name": "copy",
|
"name": "copy",
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": {
|
"trackEvent": {
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
@@ -5207,7 +5197,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"keyTest": undefined,
|
"keyTest": undefined,
|
||||||
"name": "paste",
|
"name": "paste",
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": {
|
"trackEvent": {
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
@@ -5436,7 +5425,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"keyTest": [Function],
|
"keyTest": [Function],
|
||||||
"name": "toggleElementLock",
|
"name": "toggleElementLock",
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": {
|
"trackEvent": {
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
@@ -5855,7 +5843,6 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
"keyTest": undefined,
|
"keyTest": undefined,
|
||||||
"name": "paste",
|
"name": "paste",
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": {
|
"trackEvent": {
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
@@ -6109,7 +6096,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"keyTest": [Function],
|
"keyTest": [Function],
|
||||||
"name": "cut",
|
"name": "cut",
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": {
|
"trackEvent": {
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
@@ -6119,7 +6105,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"keyTest": undefined,
|
"keyTest": undefined,
|
||||||
"name": "copy",
|
"name": "copy",
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": {
|
"trackEvent": {
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
@@ -6129,7 +6114,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"keyTest": undefined,
|
"keyTest": undefined,
|
||||||
"name": "paste",
|
"name": "paste",
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": {
|
"trackEvent": {
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
@@ -6358,7 +6342,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"keyTest": [Function],
|
"keyTest": [Function],
|
||||||
"name": "toggleElementLock",
|
"name": "toggleElementLock",
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": {
|
"trackEvent": {
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
@@ -6486,7 +6469,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"keyTest": [Function],
|
"keyTest": [Function],
|
||||||
"name": "cut",
|
"name": "cut",
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": {
|
"trackEvent": {
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
@@ -6496,7 +6478,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"keyTest": undefined,
|
"keyTest": undefined,
|
||||||
"name": "copy",
|
"name": "copy",
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": {
|
"trackEvent": {
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
@@ -6506,7 +6487,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"keyTest": undefined,
|
"keyTest": undefined,
|
||||||
"name": "paste",
|
"name": "paste",
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": {
|
"trackEvent": {
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
@@ -6735,7 +6715,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"keyTest": [Function],
|
"keyTest": [Function],
|
||||||
"name": "toggleElementLock",
|
"name": "toggleElementLock",
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": {
|
"trackEvent": {
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import {
|
import { render, waitFor, GlobalTestState } from "./test-utils";
|
||||||
render,
|
|
||||||
waitFor,
|
|
||||||
GlobalTestState,
|
|
||||||
createPasteEvent,
|
|
||||||
} from "./test-utils";
|
|
||||||
import { Pointer, Keyboard } from "./helpers/ui";
|
import { Pointer, Keyboard } from "./helpers/ui";
|
||||||
import { Excalidraw } from "../packages/excalidraw/index";
|
import { Excalidraw } from "../packages/excalidraw/index";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
@@ -16,7 +11,7 @@ import {
|
|||||||
import { getElementBounds } from "../element";
|
import { getElementBounds } from "../element";
|
||||||
import { NormalizedZoomValue } from "../types";
|
import { NormalizedZoomValue } from "../types";
|
||||||
import { API } from "./helpers/api";
|
import { API } from "./helpers/api";
|
||||||
import { copyToClipboard } from "../clipboard";
|
import { createPasteEvent, serializeAsClipboardJSON } from "../clipboard";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
@@ -37,7 +32,9 @@ vi.mock("../keys.ts", async (importOriginal) => {
|
|||||||
|
|
||||||
const sendPasteEvent = (text: string) => {
|
const sendPasteEvent = (text: string) => {
|
||||||
const clipboardEvent = createPasteEvent({
|
const clipboardEvent = createPasteEvent({
|
||||||
"text/plain": text,
|
types: {
|
||||||
|
"text/plain": text,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
document.dispatchEvent(clipboardEvent);
|
document.dispatchEvent(clipboardEvent);
|
||||||
};
|
};
|
||||||
@@ -86,7 +83,10 @@ beforeEach(async () => {
|
|||||||
describe("general paste behavior", () => {
|
describe("general paste behavior", () => {
|
||||||
it("should randomize seed on paste", async () => {
|
it("should randomize seed on paste", async () => {
|
||||||
const rectangle = API.createElement({ type: "rectangle" });
|
const rectangle = API.createElement({ type: "rectangle" });
|
||||||
const clipboardJSON = (await copyToClipboard([rectangle], null))!;
|
const clipboardJSON = await serializeAsClipboardJSON({
|
||||||
|
elements: [rectangle],
|
||||||
|
files: null,
|
||||||
|
});
|
||||||
pasteWithCtrlCmdV(clipboardJSON);
|
pasteWithCtrlCmdV(clipboardJSON);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -97,7 +97,10 @@ describe("general paste behavior", () => {
|
|||||||
|
|
||||||
it("should retain seed on shift-paste", async () => {
|
it("should retain seed on shift-paste", async () => {
|
||||||
const rectangle = API.createElement({ type: "rectangle" });
|
const rectangle = API.createElement({ type: "rectangle" });
|
||||||
const clipboardJSON = (await copyToClipboard([rectangle], null))!;
|
const clipboardJSON = await serializeAsClipboardJSON({
|
||||||
|
elements: [rectangle],
|
||||||
|
files: null,
|
||||||
|
});
|
||||||
|
|
||||||
// assert we don't randomize seed on shift-paste
|
// assert we don't randomize seed on shift-paste
|
||||||
pasteWithCtrlCmdShiftV(clipboardJSON);
|
pasteWithCtrlCmdShiftV(clipboardJSON);
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ describe("contextMenu element", () => {
|
|||||||
const contextMenuOptions =
|
const contextMenuOptions =
|
||||||
contextMenu?.querySelectorAll(".context-menu li");
|
contextMenu?.querySelectorAll(".context-menu li");
|
||||||
const expectedShortcutNames: ShortcutName[] = [
|
const expectedShortcutNames: ShortcutName[] = [
|
||||||
|
"paste",
|
||||||
"selectAll",
|
"selectAll",
|
||||||
"gridMode",
|
"gridMode",
|
||||||
"zenMode",
|
"zenMode",
|
||||||
@@ -114,6 +115,9 @@ describe("contextMenu element", () => {
|
|||||||
const contextMenuOptions =
|
const contextMenuOptions =
|
||||||
contextMenu?.querySelectorAll(".context-menu li");
|
contextMenu?.querySelectorAll(".context-menu li");
|
||||||
const expectedShortcutNames: ShortcutName[] = [
|
const expectedShortcutNames: ShortcutName[] = [
|
||||||
|
"cut",
|
||||||
|
"copy",
|
||||||
|
"paste",
|
||||||
"copyStyles",
|
"copyStyles",
|
||||||
"pasteStyles",
|
"pasteStyles",
|
||||||
"deleteSelectedElements",
|
"deleteSelectedElements",
|
||||||
@@ -203,6 +207,9 @@ describe("contextMenu element", () => {
|
|||||||
const contextMenuOptions =
|
const contextMenuOptions =
|
||||||
contextMenu?.querySelectorAll(".context-menu li");
|
contextMenu?.querySelectorAll(".context-menu li");
|
||||||
const expectedShortcutNames: ShortcutName[] = [
|
const expectedShortcutNames: ShortcutName[] = [
|
||||||
|
"cut",
|
||||||
|
"copy",
|
||||||
|
"paste",
|
||||||
"copyStyles",
|
"copyStyles",
|
||||||
"pasteStyles",
|
"pasteStyles",
|
||||||
"deleteSelectedElements",
|
"deleteSelectedElements",
|
||||||
@@ -256,6 +263,9 @@ describe("contextMenu element", () => {
|
|||||||
const contextMenuOptions =
|
const contextMenuOptions =
|
||||||
contextMenu?.querySelectorAll(".context-menu li");
|
contextMenu?.querySelectorAll(".context-menu li");
|
||||||
const expectedShortcutNames: ShortcutName[] = [
|
const expectedShortcutNames: ShortcutName[] = [
|
||||||
|
"cut",
|
||||||
|
"copy",
|
||||||
|
"paste",
|
||||||
"copyStyles",
|
"copyStyles",
|
||||||
"pasteStyles",
|
"pasteStyles",
|
||||||
"deleteSelectedElements",
|
"deleteSelectedElements",
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import {
|
import {
|
||||||
createPasteEvent,
|
|
||||||
fireEvent,
|
fireEvent,
|
||||||
GlobalTestState,
|
GlobalTestState,
|
||||||
render,
|
render,
|
||||||
@@ -27,6 +26,7 @@ import { vi } from "vitest";
|
|||||||
import * as blob from "../data/blob";
|
import * as blob from "../data/blob";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { getBoundTextElementPosition } from "../element/textElement";
|
import { getBoundTextElementPosition } from "../element/textElement";
|
||||||
|
import { createPasteEvent } from "../clipboard";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
@@ -727,7 +727,7 @@ describe("freedraw", () => {
|
|||||||
describe("image", () => {
|
describe("image", () => {
|
||||||
const createImage = async () => {
|
const createImage = async () => {
|
||||||
const sendPasteEvent = (file?: File) => {
|
const sendPasteEvent = (file?: File) => {
|
||||||
const clipboardEvent = createPasteEvent({}, file ? [file] : []);
|
const clipboardEvent = createPasteEvent({ files: file ? [file] : [] });
|
||||||
document.dispatchEvent(clipboardEvent);
|
document.dispatchEvent(clipboardEvent);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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"];
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
class ClipboardEvent {
|
||||||
|
constructor(
|
||||||
|
type: "paste" | "copy",
|
||||||
|
eventInitDict: {
|
||||||
|
clipboardData: DataTransfer;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
return Object.assign(
|
||||||
|
new Event("paste", {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
composed: true,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
clipboardData: eventInitDict.clipboardData,
|
||||||
|
},
|
||||||
|
) as any as ClipboardEvent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DataKind = "string" | "file";
|
||||||
|
|
||||||
|
class DataTransferItem {
|
||||||
|
kind: DataKind;
|
||||||
|
type: string;
|
||||||
|
data: string | Blob;
|
||||||
|
|
||||||
|
constructor(kind: DataKind, type: string, data: string | Blob) {
|
||||||
|
this.kind = kind;
|
||||||
|
this.type = type;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAsString(callback: (data: string) => void): void {
|
||||||
|
if (this.kind === "string") {
|
||||||
|
callback(this.data as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAsFile(): File | null {
|
||||||
|
if (this.kind === "file" && this.data instanceof File) {
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DataTransferList {
|
||||||
|
items: DataTransferItem[] = [];
|
||||||
|
|
||||||
|
add(data: string | File, type: string = ""): void {
|
||||||
|
if (typeof data === "string") {
|
||||||
|
this.items.push(new DataTransferItem("string", type, data));
|
||||||
|
} else if (data instanceof File) {
|
||||||
|
this.items.push(new DataTransferItem("file", type, data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.items = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DataTransfer {
|
||||||
|
public items: DataTransferList = new DataTransferList();
|
||||||
|
private _types: Record<string, string> = {};
|
||||||
|
|
||||||
|
get files() {
|
||||||
|
return this.items.items
|
||||||
|
.filter((item) => item.kind === "file")
|
||||||
|
.map((item) => item.getAsFile()!);
|
||||||
|
}
|
||||||
|
|
||||||
|
add(data: string | File, type: string = ""): void {
|
||||||
|
this.items.add(data, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(type: string, value: string) {
|
||||||
|
this._types[type] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
getData(type: string) {
|
||||||
|
return this._types[type] || "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const testPolyfills = {
|
||||||
|
ClipboardEvent,
|
||||||
|
DataTransfer,
|
||||||
|
DataTransferItem,
|
||||||
|
};
|
||||||
@@ -468,16 +468,16 @@ export class UI {
|
|||||||
static async editText<
|
static async editText<
|
||||||
T extends ExcalidrawTextElement | ExcalidrawTextContainer,
|
T extends ExcalidrawTextElement | ExcalidrawTextContainer,
|
||||||
>(element: T, text: string) {
|
>(element: T, text: string) {
|
||||||
const openedEditor = document.querySelector<HTMLTextAreaElement>(
|
const textEditorSelector = ".excalidraw-textEditorContainer > textarea";
|
||||||
".excalidraw-textEditorContainer > textarea",
|
const openedEditor =
|
||||||
);
|
document.querySelector<HTMLTextAreaElement>(textEditorSelector);
|
||||||
|
|
||||||
if (!openedEditor) {
|
if (!openedEditor) {
|
||||||
mouse.select(element);
|
mouse.select(element);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
const editor = await getTextEditor();
|
const editor = await getTextEditor(textEditorSelector);
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
throw new Error("Can't find wysiwyg text editor in the dom");
|
throw new Error("Can't find wysiwyg text editor in the dom");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ describe("Test Linear Elements", () => {
|
|||||||
|
|
||||||
// drag line from midpoint
|
// drag line from midpoint
|
||||||
drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
|
drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(14);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(13);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
|
|
||||||
expect(line.points.length).toEqual(3);
|
expect(line.points.length).toEqual(3);
|
||||||
@@ -416,7 +416,7 @@ describe("Test Linear Elements", () => {
|
|||||||
lastSegmentMidpoint[1] + delta,
|
lastSegmentMidpoint[1] + delta,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(21);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(19);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(9);
|
expect(renderStaticScene).toHaveBeenCalledTimes(9);
|
||||||
|
|
||||||
expect(line.points.length).toEqual(5);
|
expect(line.points.length).toEqual(5);
|
||||||
@@ -519,7 +519,7 @@ describe("Test Linear Elements", () => {
|
|||||||
// delete 3rd point
|
// delete 3rd point
|
||||||
deletePoint(points[2]);
|
deletePoint(points[2]);
|
||||||
expect(line.points.length).toEqual(3);
|
expect(line.points.length).toEqual(3);
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(21);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(20);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(9);
|
expect(renderStaticScene).toHaveBeenCalledTimes(9);
|
||||||
|
|
||||||
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
||||||
@@ -566,7 +566,7 @@ describe("Test Linear Elements", () => {
|
|||||||
lastSegmentMidpoint[0] + delta,
|
lastSegmentMidpoint[0] + delta,
|
||||||
lastSegmentMidpoint[1] + delta,
|
lastSegmentMidpoint[1] + delta,
|
||||||
]);
|
]);
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(21);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(19);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(9);
|
expect(renderStaticScene).toHaveBeenCalledTimes(9);
|
||||||
expect(line.points.length).toEqual(5);
|
expect(line.points.length).toEqual(5);
|
||||||
|
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import { waitFor } from "@testing-library/dom";
|
import { waitFor } from "@testing-library/dom";
|
||||||
|
import { fireEvent } from "@testing-library/react";
|
||||||
|
|
||||||
export const getTextEditor = async (waitForEditor = true) => {
|
export const getTextEditor = async (selector: string, waitForEditor = true) => {
|
||||||
const query = () =>
|
const query = () => document.querySelector(selector) as HTMLTextAreaElement;
|
||||||
document.querySelector(
|
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
if (waitForEditor) {
|
if (waitForEditor) {
|
||||||
await waitFor(() => expect(query()).not.toBe(null));
|
await waitFor(() => expect(query()).not.toBe(null));
|
||||||
return query();
|
return query();
|
||||||
}
|
}
|
||||||
return query();
|
return query();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateTextEditor = (
|
||||||
|
editor: HTMLTextAreaElement,
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
|
fireEvent.change(editor, { target: { value } });
|
||||||
|
editor.dispatchEvent(new Event("input"));
|
||||||
|
};
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -208,26 +208,6 @@ export const assertSelectedElements = (
|
|||||||
expect(selectedElementIds).toEqual(expect.arrayContaining(ids));
|
expect(selectedElementIds).toEqual(expect.arrayContaining(ids));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createPasteEvent = <T extends "text/plain" | "text/html">(
|
|
||||||
items: Record<T, string>,
|
|
||||||
files?: File[],
|
|
||||||
) => {
|
|
||||||
return Object.assign(
|
|
||||||
new Event("paste", {
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
composed: true,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
clipboardData: {
|
|
||||||
getData: (type: string) =>
|
|
||||||
(items as Record<string, string>)[type] || "",
|
|
||||||
files: files || [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
) as any as ClipboardEvent;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const toggleMenu = (container: HTMLElement) => {
|
export const toggleMenu = (container: HTMLElement) => {
|
||||||
// open menu
|
// open menu
|
||||||
fireEvent.click(container.querySelector(".dropdown-menu-button")!);
|
fireEvent.click(container.querySelector(".dropdown-menu-button")!);
|
||||||
|
|||||||
+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"]],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+14
-1
@@ -241,7 +241,7 @@ export type AppState = {
|
|||||||
openMenu: "canvas" | "shape" | null;
|
openMenu: "canvas" | "shape" | null;
|
||||||
openPopup: "canvasBackground" | "elementBackground" | "elementStroke" | null;
|
openPopup: "canvasBackground" | "elementBackground" | "elementStroke" | null;
|
||||||
openSidebar: { name: SidebarName; tab?: SidebarTabName } | null;
|
openSidebar: { name: SidebarName; tab?: SidebarTabName } | null;
|
||||||
openDialog: "imageExport" | "help" | "jsonExport" | null;
|
openDialog: "imageExport" | "help" | "jsonExport" | "mermaid" | null;
|
||||||
/**
|
/**
|
||||||
* Reflects user preference for whether the default sidebar should be docked.
|
* Reflects user preference for whether the default sidebar should be docked.
|
||||||
*
|
*
|
||||||
@@ -537,8 +537,12 @@ export type AppClassProperties = {
|
|||||||
onInsertElements: App["onInsertElements"];
|
onInsertElements: App["onInsertElements"];
|
||||||
onExportImage: App["onExportImage"];
|
onExportImage: App["onExportImage"];
|
||||||
lastViewportPosition: App["lastViewportPosition"];
|
lastViewportPosition: App["lastViewportPosition"];
|
||||||
|
scrollToContent: App["scrollToContent"];
|
||||||
|
addFiles: App["addFiles"];
|
||||||
|
addElementsFromPasteOrLibrary: App["addElementsFromPasteOrLibrary"];
|
||||||
togglePenMode: App["togglePenMode"];
|
togglePenMode: App["togglePenMode"];
|
||||||
setActiveTool: App["setActiveTool"];
|
setActiveTool: App["setActiveTool"];
|
||||||
|
setOpenDialog: App["setOpenDialog"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PointerDownState = Readonly<{
|
export type PointerDownState = Readonly<{
|
||||||
@@ -695,3 +699,12 @@ export type KeyboardModifiersObject = {
|
|||||||
altKey: boolean;
|
altKey: boolean;
|
||||||
metaKey: boolean;
|
metaKey: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Primitive =
|
||||||
|
| number
|
||||||
|
| string
|
||||||
|
| boolean
|
||||||
|
| bigint
|
||||||
|
| symbol
|
||||||
|
| null
|
||||||
|
| undefined;
|
||||||
|
|||||||
@@ -917,3 +917,17 @@ export const isRenderThrottlingEnabled = (() => {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
/** Checks if value is inside given collection. Useful for type-safety. */
|
||||||
|
export const isMemberOf = <T extends string>(
|
||||||
|
/** Set/Map/Array/Object */
|
||||||
|
collection: Set<T> | readonly T[] | Record<T, any> | Map<T, any>,
|
||||||
|
/** value to look for */
|
||||||
|
value: string,
|
||||||
|
): value is T => {
|
||||||
|
return collection instanceof Set || collection instanceof Map
|
||||||
|
? collection.has(value as T)
|
||||||
|
: "includes" in collection
|
||||||
|
? collection.includes(value as T)
|
||||||
|
: collection.hasOwnProperty(value);
|
||||||
|
};
|
||||||
|
|||||||
+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,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ export default defineConfig({
|
|||||||
globals: true,
|
globals: true,
|
||||||
environment: "jsdom",
|
environment: "jsdom",
|
||||||
coverage: {
|
coverage: {
|
||||||
reporter: ["text", "json-summary", "json"],
|
reporter: ["text", "json-summary", "json", "html"],
|
||||||
lines: 70,
|
lines: 70,
|
||||||
branches: 70,
|
branches: 70,
|
||||||
functions: 68,
|
functions: 68,
|
||||||
|
|||||||
@@ -1321,6 +1321,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz#6110f918d273fe2af8ea1c4398a88774bb9fc12f"
|
resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz#6110f918d273fe2af8ea1c4398a88774bb9fc12f"
|
||||||
integrity sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==
|
integrity sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==
|
||||||
|
|
||||||
|
"@braintree/sanitize-url@^6.0.2":
|
||||||
|
version "6.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz#923ca57e173c6b232bbbb07347b1be982f03e783"
|
||||||
|
integrity sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==
|
||||||
|
|
||||||
"@esbuild/android-arm64@0.17.19":
|
"@esbuild/android-arm64@0.17.19":
|
||||||
version "0.17.19"
|
version "0.17.19"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz#bafb75234a5d3d1b690e7c2956a599345e84a2fd"
|
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz#bafb75234a5d3d1b690e7c2956a599345e84a2fd"
|
||||||
@@ -1578,6 +1583,20 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@excalidraw/laser-pointer/-/laser-pointer-1.2.0.tgz#cd34ea7d24b11743c726488cc1fcb28c161cacba"
|
resolved "https://registry.yarnpkg.com/@excalidraw/laser-pointer/-/laser-pointer-1.2.0.tgz#cd34ea7d24b11743c726488cc1fcb28c161cacba"
|
||||||
integrity sha512-WjFFwLk9ahmKRKku7U0jqYpeM3fe9ZS1K43pfwPREHk4/FYU3iKDKVeS8m4tEAASnRlBt3hhLCBQLBF2uvgOnw==
|
integrity sha512-WjFFwLk9ahmKRKku7U0jqYpeM3fe9ZS1K43pfwPREHk4/FYU3iKDKVeS8m4tEAASnRlBt3hhLCBQLBF2uvgOnw==
|
||||||
|
|
||||||
|
"@excalidraw/markdown-to-text@0.1.2":
|
||||||
|
version "0.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@excalidraw/markdown-to-text/-/markdown-to-text-0.1.2.tgz#1703705e7da608cf478f17bfe96fb295f55a23eb"
|
||||||
|
integrity sha512-1nDXBNAojfi3oSFwJswKREkFm5wrSjqay81QlyRv2pkITG/XYB5v+oChENVBQLcxQwX4IUATWvXM5BcaNhPiIg==
|
||||||
|
|
||||||
|
"@excalidraw/mermaid-to-excalidraw@0.1.2":
|
||||||
|
version "0.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@excalidraw/mermaid-to-excalidraw/-/mermaid-to-excalidraw-0.1.2.tgz#be7b412536fc00b7986ccdccba8e7c33592aa004"
|
||||||
|
integrity sha512-LFk+cLGhXlvRTaf0f6ClCFIZFRsbZPb1ke2cytr5/JlnOefnXQQHgWITafskjcIO2c34KXFGO0HjgYPNFLUknw==
|
||||||
|
dependencies:
|
||||||
|
"@excalidraw/markdown-to-text" "0.1.2"
|
||||||
|
mermaid "10.2.3"
|
||||||
|
nanoid "4.0.2"
|
||||||
|
|
||||||
"@excalidraw/prettier-config@1.0.2":
|
"@excalidraw/prettier-config@1.0.2":
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@excalidraw/prettier-config/-/prettier-config-1.0.2.tgz#b7c061c99cee2f78b9ca470ea1fbd602683bba65"
|
resolved "https://registry.yarnpkg.com/@excalidraw/prettier-config/-/prettier-config-1.0.2.tgz#b7c061c99cee2f78b9ca470ea1fbd602683bba65"
|
||||||
@@ -2556,6 +2575,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.0.tgz#23509ebc1fa32f1b4d50d6a66c4032d5b8eaabdc"
|
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.0.tgz#23509ebc1fa32f1b4d50d6a66c4032d5b8eaabdc"
|
||||||
integrity sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==
|
integrity sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==
|
||||||
|
|
||||||
|
"@types/debug@^4.0.0":
|
||||||
|
version "4.1.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.8.tgz#cef723a5d0a90990313faec2d1e22aee5eecb317"
|
||||||
|
integrity sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/ms" "*"
|
||||||
|
|
||||||
"@types/estree@0.0.39":
|
"@types/estree@0.0.39":
|
||||||
version "0.0.39"
|
version "0.0.39"
|
||||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
|
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
|
||||||
@@ -2628,6 +2654,18 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a"
|
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a"
|
||||||
integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==
|
integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==
|
||||||
|
|
||||||
|
"@types/mdast@^3.0.0":
|
||||||
|
version "3.0.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.12.tgz#beeb511b977c875a5b0cc92eab6fcac2f0895514"
|
||||||
|
integrity sha512-DT+iNIRNX884cx0/Q1ja7NyUPpZuv0KPyL5rGNxm1WC1OtHstl7n4Jb7nk+xacNShQMbczJjt8uFzznpp6kYBg==
|
||||||
|
dependencies:
|
||||||
|
"@types/unist" "^2"
|
||||||
|
|
||||||
|
"@types/ms@*":
|
||||||
|
version "0.7.31"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
|
||||||
|
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
|
||||||
|
|
||||||
"@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0":
|
"@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0":
|
||||||
version "18.15.11"
|
version "18.15.11"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f"
|
||||||
@@ -2738,6 +2776,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.3.tgz#a136f83b0758698df454e328759dbd3d44555311"
|
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.3.tgz#a136f83b0758698df454e328759dbd3d44555311"
|
||||||
integrity sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==
|
integrity sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==
|
||||||
|
|
||||||
|
"@types/unist@^2", "@types/unist@^2.0.0":
|
||||||
|
version "2.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.7.tgz#5b06ad6894b236a1d2bd6b2f07850ca5c59cf4d6"
|
||||||
|
integrity sha512-cputDpIbFgLUaGQn6Vqg3/YsJwxUwHLO13v3i5ouxT4lat0khip9AEWxtERujXV9wxIB1EyF97BSJFt6vpdI8g==
|
||||||
|
|
||||||
"@types/yargs-parser@*":
|
"@types/yargs-parser@*":
|
||||||
version "21.0.0"
|
version "21.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b"
|
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b"
|
||||||
@@ -3431,6 +3474,11 @@ chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2:
|
|||||||
ansi-styles "^4.1.0"
|
ansi-styles "^4.1.0"
|
||||||
supports-color "^7.1.0"
|
supports-color "^7.1.0"
|
||||||
|
|
||||||
|
character-entities@^2.0.0:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22"
|
||||||
|
integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==
|
||||||
|
|
||||||
check-error@^1.0.2:
|
check-error@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
|
resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
|
||||||
@@ -3534,6 +3582,11 @@ combined-stream@^1.0.8:
|
|||||||
dependencies:
|
dependencies:
|
||||||
delayed-stream "~1.0.0"
|
delayed-stream "~1.0.0"
|
||||||
|
|
||||||
|
commander@7:
|
||||||
|
version "7.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
|
||||||
|
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
|
||||||
|
|
||||||
commander@^2.20.0:
|
commander@^2.20.0:
|
||||||
version "2.20.3"
|
version "2.20.3"
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||||
@@ -3601,6 +3654,20 @@ corser@^2.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87"
|
resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87"
|
||||||
integrity sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==
|
integrity sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==
|
||||||
|
|
||||||
|
cose-base@^1.0.0:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/cose-base/-/cose-base-1.0.3.tgz#650334b41b869578a543358b80cda7e0abe0a60a"
|
||||||
|
integrity sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==
|
||||||
|
dependencies:
|
||||||
|
layout-base "^1.0.0"
|
||||||
|
|
||||||
|
cose-base@^2.2.0:
|
||||||
|
version "2.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/cose-base/-/cose-base-2.2.0.tgz#1c395c35b6e10bb83f9769ca8b817d614add5c01"
|
||||||
|
integrity sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==
|
||||||
|
dependencies:
|
||||||
|
layout-base "^2.0.0"
|
||||||
|
|
||||||
cosmiconfig@^7.0.0, cosmiconfig@^7.0.1:
|
cosmiconfig@^7.0.0, cosmiconfig@^7.0.1:
|
||||||
version "7.1.0"
|
version "7.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6"
|
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6"
|
||||||
@@ -3669,6 +3736,280 @@ csstype@^3.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
|
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
|
||||||
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
|
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
|
||||||
|
|
||||||
|
cytoscape-cose-bilkent@^4.1.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz#762fa121df9930ffeb51a495d87917c570ac209b"
|
||||||
|
integrity sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==
|
||||||
|
dependencies:
|
||||||
|
cose-base "^1.0.0"
|
||||||
|
|
||||||
|
cytoscape-fcose@^2.1.0:
|
||||||
|
version "2.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz#e4d6f6490df4fab58ae9cea9e5c3ab8d7472f471"
|
||||||
|
integrity sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==
|
||||||
|
dependencies:
|
||||||
|
cose-base "^2.2.0"
|
||||||
|
|
||||||
|
cytoscape@^3.23.0:
|
||||||
|
version "3.26.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.26.0.tgz#b4c6961445fd51e1fd3cca83c3ffe924d9a8abc9"
|
||||||
|
integrity sha512-IV+crL+KBcrCnVVUCZW+zRRRFUZQcrtdOPXki+o4CFUWLdAEYvuZLcBSJC9EBK++suamERKzeY7roq2hdovV3w==
|
||||||
|
dependencies:
|
||||||
|
heap "^0.2.6"
|
||||||
|
lodash "^4.17.21"
|
||||||
|
|
||||||
|
"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.2.0:
|
||||||
|
version "3.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5"
|
||||||
|
integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==
|
||||||
|
dependencies:
|
||||||
|
internmap "1 - 2"
|
||||||
|
|
||||||
|
d3-axis@3:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-3.0.0.tgz#c42a4a13e8131d637b745fc2973824cfeaf93322"
|
||||||
|
integrity sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==
|
||||||
|
|
||||||
|
d3-brush@3:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-3.0.0.tgz#6f767c4ed8dcb79de7ede3e1c0f89e63ef64d31c"
|
||||||
|
integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==
|
||||||
|
dependencies:
|
||||||
|
d3-dispatch "1 - 3"
|
||||||
|
d3-drag "2 - 3"
|
||||||
|
d3-interpolate "1 - 3"
|
||||||
|
d3-selection "3"
|
||||||
|
d3-transition "3"
|
||||||
|
|
||||||
|
d3-chord@3:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-3.0.1.tgz#d156d61f485fce8327e6abf339cb41d8cbba6966"
|
||||||
|
integrity sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==
|
||||||
|
dependencies:
|
||||||
|
d3-path "1 - 3"
|
||||||
|
|
||||||
|
"d3-color@1 - 3", d3-color@3:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
|
||||||
|
integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
|
||||||
|
|
||||||
|
d3-contour@4:
|
||||||
|
version "4.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-4.0.2.tgz#bb92063bc8c5663acb2422f99c73cbb6c6ae3bcc"
|
||||||
|
integrity sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==
|
||||||
|
dependencies:
|
||||||
|
d3-array "^3.2.0"
|
||||||
|
|
||||||
|
d3-delaunay@6:
|
||||||
|
version "6.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz#98169038733a0a5babbeda55054f795bb9e4a58b"
|
||||||
|
integrity sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==
|
||||||
|
dependencies:
|
||||||
|
delaunator "5"
|
||||||
|
|
||||||
|
"d3-dispatch@1 - 3", d3-dispatch@3:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e"
|
||||||
|
integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
|
||||||
|
|
||||||
|
"d3-drag@2 - 3", d3-drag@3:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba"
|
||||||
|
integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
|
||||||
|
dependencies:
|
||||||
|
d3-dispatch "1 - 3"
|
||||||
|
d3-selection "3"
|
||||||
|
|
||||||
|
"d3-dsv@1 - 3", d3-dsv@3:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-3.0.1.tgz#c63af978f4d6a0d084a52a673922be2160789b73"
|
||||||
|
integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==
|
||||||
|
dependencies:
|
||||||
|
commander "7"
|
||||||
|
iconv-lite "0.6"
|
||||||
|
rw "1"
|
||||||
|
|
||||||
|
"d3-ease@1 - 3", d3-ease@3:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
|
||||||
|
integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
|
||||||
|
|
||||||
|
d3-fetch@3:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-3.0.1.tgz#83141bff9856a0edb5e38de89cdcfe63d0a60a22"
|
||||||
|
integrity sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==
|
||||||
|
dependencies:
|
||||||
|
d3-dsv "1 - 3"
|
||||||
|
|
||||||
|
d3-force@3:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4"
|
||||||
|
integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==
|
||||||
|
dependencies:
|
||||||
|
d3-dispatch "1 - 3"
|
||||||
|
d3-quadtree "1 - 3"
|
||||||
|
d3-timer "1 - 3"
|
||||||
|
|
||||||
|
"d3-format@1 - 3", d3-format@3:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641"
|
||||||
|
integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
|
||||||
|
|
||||||
|
d3-geo@3:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.1.0.tgz#74fd54e1f4cebd5185ac2039217a98d39b0a4c0e"
|
||||||
|
integrity sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==
|
||||||
|
dependencies:
|
||||||
|
d3-array "2.5.0 - 3"
|
||||||
|
|
||||||
|
d3-hierarchy@3:
|
||||||
|
version "3.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6"
|
||||||
|
integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==
|
||||||
|
|
||||||
|
"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
|
||||||
|
integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
|
||||||
|
dependencies:
|
||||||
|
d3-color "1 - 3"
|
||||||
|
|
||||||
|
"d3-path@1 - 3", d3-path@3, d3-path@^3.1.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526"
|
||||||
|
integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
|
||||||
|
|
||||||
|
d3-polygon@3:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-3.0.1.tgz#0b45d3dd1c48a29c8e057e6135693ec80bf16398"
|
||||||
|
integrity sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==
|
||||||
|
|
||||||
|
"d3-quadtree@1 - 3", d3-quadtree@3:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f"
|
||||||
|
integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==
|
||||||
|
|
||||||
|
d3-random@3:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-3.0.1.tgz#d4926378d333d9c0bfd1e6fa0194d30aebaa20f4"
|
||||||
|
integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==
|
||||||
|
|
||||||
|
d3-scale-chromatic@3:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#15b4ceb8ca2bb0dcb6d1a641ee03d59c3b62376a"
|
||||||
|
integrity sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==
|
||||||
|
dependencies:
|
||||||
|
d3-color "1 - 3"
|
||||||
|
d3-interpolate "1 - 3"
|
||||||
|
|
||||||
|
d3-scale@4:
|
||||||
|
version "4.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
|
||||||
|
integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
|
||||||
|
dependencies:
|
||||||
|
d3-array "2.10.0 - 3"
|
||||||
|
d3-format "1 - 3"
|
||||||
|
d3-interpolate "1.2.0 - 3"
|
||||||
|
d3-time "2.1.1 - 3"
|
||||||
|
d3-time-format "2 - 4"
|
||||||
|
|
||||||
|
"d3-selection@2 - 3", d3-selection@3:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31"
|
||||||
|
integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
|
||||||
|
|
||||||
|
d3-shape@3:
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5"
|
||||||
|
integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
|
||||||
|
dependencies:
|
||||||
|
d3-path "^3.1.0"
|
||||||
|
|
||||||
|
"d3-time-format@2 - 4", d3-time-format@4:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
|
||||||
|
integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
|
||||||
|
dependencies:
|
||||||
|
d3-time "1 - 3"
|
||||||
|
|
||||||
|
"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7"
|
||||||
|
integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
|
||||||
|
dependencies:
|
||||||
|
d3-array "2 - 3"
|
||||||
|
|
||||||
|
"d3-timer@1 - 3", d3-timer@3:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
|
||||||
|
integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
|
||||||
|
|
||||||
|
"d3-transition@2 - 3", d3-transition@3:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f"
|
||||||
|
integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==
|
||||||
|
dependencies:
|
||||||
|
d3-color "1 - 3"
|
||||||
|
d3-dispatch "1 - 3"
|
||||||
|
d3-ease "1 - 3"
|
||||||
|
d3-interpolate "1 - 3"
|
||||||
|
d3-timer "1 - 3"
|
||||||
|
|
||||||
|
d3-zoom@3:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3"
|
||||||
|
integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==
|
||||||
|
dependencies:
|
||||||
|
d3-dispatch "1 - 3"
|
||||||
|
d3-drag "2 - 3"
|
||||||
|
d3-interpolate "1 - 3"
|
||||||
|
d3-selection "2 - 3"
|
||||||
|
d3-transition "2 - 3"
|
||||||
|
|
||||||
|
d3@^7.4.0, d3@^7.8.2:
|
||||||
|
version "7.8.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3/-/d3-7.8.5.tgz#fde4b760d4486cdb6f0cc8e2cbff318af844635c"
|
||||||
|
integrity sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==
|
||||||
|
dependencies:
|
||||||
|
d3-array "3"
|
||||||
|
d3-axis "3"
|
||||||
|
d3-brush "3"
|
||||||
|
d3-chord "3"
|
||||||
|
d3-color "3"
|
||||||
|
d3-contour "4"
|
||||||
|
d3-delaunay "6"
|
||||||
|
d3-dispatch "3"
|
||||||
|
d3-drag "3"
|
||||||
|
d3-dsv "3"
|
||||||
|
d3-ease "3"
|
||||||
|
d3-fetch "3"
|
||||||
|
d3-force "3"
|
||||||
|
d3-format "3"
|
||||||
|
d3-geo "3"
|
||||||
|
d3-hierarchy "3"
|
||||||
|
d3-interpolate "3"
|
||||||
|
d3-path "3"
|
||||||
|
d3-polygon "3"
|
||||||
|
d3-quadtree "3"
|
||||||
|
d3-random "3"
|
||||||
|
d3-scale "4"
|
||||||
|
d3-scale-chromatic "3"
|
||||||
|
d3-selection "3"
|
||||||
|
d3-shape "3"
|
||||||
|
d3-time "3"
|
||||||
|
d3-time-format "4"
|
||||||
|
d3-timer "3"
|
||||||
|
d3-transition "3"
|
||||||
|
d3-zoom "3"
|
||||||
|
|
||||||
|
dagre-d3-es@7.0.10:
|
||||||
|
version "7.0.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz#19800d4be674379a3cd8c86a8216a2ac6827cadc"
|
||||||
|
integrity sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==
|
||||||
|
dependencies:
|
||||||
|
d3 "^7.8.2"
|
||||||
|
lodash-es "^4.17.21"
|
||||||
|
|
||||||
damerau-levenshtein@^1.0.8:
|
damerau-levenshtein@^1.0.8:
|
||||||
version "1.0.8"
|
version "1.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
|
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
|
||||||
@@ -3683,7 +4024,12 @@ data-urls@^4.0.0:
|
|||||||
whatwg-mimetype "^3.0.0"
|
whatwg-mimetype "^3.0.0"
|
||||||
whatwg-url "^12.0.0"
|
whatwg-url "^12.0.0"
|
||||||
|
|
||||||
debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.3, debug@^4.3.4:
|
dayjs@^1.11.7:
|
||||||
|
version "1.11.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.9.tgz#9ca491933fadd0a60a2c19f6c237c03517d71d1a"
|
||||||
|
integrity sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==
|
||||||
|
|
||||||
|
debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.3, debug@^4.3.4:
|
||||||
version "4.3.4"
|
version "4.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
||||||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
||||||
@@ -3709,6 +4055,13 @@ decimal.js@^10.4.3:
|
|||||||
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23"
|
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23"
|
||||||
integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==
|
integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==
|
||||||
|
|
||||||
|
decode-named-character-reference@^1.0.0:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e"
|
||||||
|
integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==
|
||||||
|
dependencies:
|
||||||
|
character-entities "^2.0.0"
|
||||||
|
|
||||||
decode-uri-component@^0.2.0:
|
decode-uri-component@^0.2.0:
|
||||||
version "0.2.2"
|
version "0.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9"
|
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9"
|
||||||
@@ -3769,11 +4122,23 @@ define-properties@^1.1.3, define-properties@^1.1.4:
|
|||||||
has-property-descriptors "^1.0.0"
|
has-property-descriptors "^1.0.0"
|
||||||
object-keys "^1.1.1"
|
object-keys "^1.1.1"
|
||||||
|
|
||||||
|
delaunator@5:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.0.tgz#60f052b28bd91c9b4566850ebf7756efe821d81b"
|
||||||
|
integrity sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==
|
||||||
|
dependencies:
|
||||||
|
robust-predicates "^3.0.0"
|
||||||
|
|
||||||
delayed-stream@~1.0.0:
|
delayed-stream@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||||
|
|
||||||
|
dequal@^2.0.0:
|
||||||
|
version "2.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
|
||||||
|
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
|
||||||
|
|
||||||
detect-node-es@^1.1.0:
|
detect-node-es@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493"
|
resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493"
|
||||||
@@ -3789,6 +4154,11 @@ diff-sequences@^29.4.3:
|
|||||||
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2"
|
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2"
|
||||||
integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==
|
integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==
|
||||||
|
|
||||||
|
diff@^5.0.0:
|
||||||
|
version "5.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40"
|
||||||
|
integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==
|
||||||
|
|
||||||
dir-glob@^3.0.1:
|
dir-glob@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
|
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
|
||||||
@@ -3834,6 +4204,11 @@ domexception@^4.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
webidl-conversions "^7.0.0"
|
webidl-conversions "^7.0.0"
|
||||||
|
|
||||||
|
dompurify@3.0.3:
|
||||||
|
version "3.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.3.tgz#4b115d15a091ddc96f232bcef668550a2f6f1430"
|
||||||
|
integrity sha512-axQ9zieHLnAnHh0sfAamKYiqXMJAVwu+LM/alQ7WDagoWessyWvMSFyW65CqF3owufNu8HBcE4cM2Vflu7YWcQ==
|
||||||
|
|
||||||
dotenv@16.0.1:
|
dotenv@16.0.1:
|
||||||
version "16.0.1"
|
version "16.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d"
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d"
|
||||||
@@ -3856,6 +4231,11 @@ electron-to-chromium@^1.4.284:
|
|||||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.359.tgz#5c4d13cb08032469fcd6bd36457915caa211356b"
|
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.359.tgz#5c4d13cb08032469fcd6bd36457915caa211356b"
|
||||||
integrity sha512-OoVcngKCIuNXtZnsYoqlCvr0Cf3NIPzDIgwUfI9bdTFjXCrr79lI0kwQstLPZ7WhCezLlGksZk/BFAzoXC7GDw==
|
integrity sha512-OoVcngKCIuNXtZnsYoqlCvr0Cf3NIPzDIgwUfI9bdTFjXCrr79lI0kwQstLPZ7WhCezLlGksZk/BFAzoXC7GDw==
|
||||||
|
|
||||||
|
elkjs@^0.8.2:
|
||||||
|
version "0.8.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/elkjs/-/elkjs-0.8.2.tgz#c37763c5a3e24e042e318455e0147c912a7c248e"
|
||||||
|
integrity sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==
|
||||||
|
|
||||||
emoji-regex@^8.0.0:
|
emoji-regex@^8.0.0:
|
||||||
version "8.0.0"
|
version "8.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||||
@@ -4752,6 +5132,11 @@ he@^1.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||||
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||||
|
|
||||||
|
heap@^0.2.6:
|
||||||
|
version "0.2.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/heap/-/heap-0.2.7.tgz#1e6adf711d3f27ce35a81fe3b7bd576c2260a8fc"
|
||||||
|
integrity sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==
|
||||||
|
|
||||||
html-encoding-sniffer@^3.0.0:
|
html-encoding-sniffer@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9"
|
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9"
|
||||||
@@ -4831,7 +5216,7 @@ i18next-browser-languagedetector@6.1.4:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.14.6"
|
"@babel/runtime" "^7.14.6"
|
||||||
|
|
||||||
iconv-lite@0.6.3:
|
iconv-lite@0.6, iconv-lite@0.6.3:
|
||||||
version "0.6.3"
|
version "0.6.3"
|
||||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
|
||||||
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
|
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
|
||||||
@@ -4927,6 +5312,11 @@ internal-slot@^1.0.3, internal-slot@^1.0.4, internal-slot@^1.0.5:
|
|||||||
has "^1.0.3"
|
has "^1.0.3"
|
||||||
side-channel "^1.0.4"
|
side-channel "^1.0.4"
|
||||||
|
|
||||||
|
"internmap@1 - 2":
|
||||||
|
version "2.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
|
||||||
|
integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
|
||||||
|
|
||||||
invariant@^2.2.4:
|
invariant@^2.2.4:
|
||||||
version "2.2.4"
|
version "2.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
||||||
@@ -5399,6 +5789,16 @@ jsonpointer@^5.0.0:
|
|||||||
array-includes "^3.1.5"
|
array-includes "^3.1.5"
|
||||||
object.assign "^4.1.3"
|
object.assign "^4.1.3"
|
||||||
|
|
||||||
|
khroma@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/khroma/-/khroma-2.0.0.tgz#7577de98aed9f36c7a474c4d453d94c0d6c6588b"
|
||||||
|
integrity sha512-2J8rDNlQWbtiNYThZRvmMv5yt44ZakX+Tz5ZIp/mN1pt4snn+m030Va5Z4v8xA0cQFDXBwO/8i42xL4QPsVk3g==
|
||||||
|
|
||||||
|
kleur@^4.0.3:
|
||||||
|
version "4.1.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"
|
||||||
|
integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==
|
||||||
|
|
||||||
language-subtag-registry@~0.3.2:
|
language-subtag-registry@~0.3.2:
|
||||||
version "0.3.22"
|
version "0.3.22"
|
||||||
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d"
|
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d"
|
||||||
@@ -5411,6 +5811,16 @@ language-tags@=1.0.5:
|
|||||||
dependencies:
|
dependencies:
|
||||||
language-subtag-registry "~0.3.2"
|
language-subtag-registry "~0.3.2"
|
||||||
|
|
||||||
|
layout-base@^1.0.0:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/layout-base/-/layout-base-1.0.2.tgz#1291e296883c322a9dd4c5dd82063721b53e26e2"
|
||||||
|
integrity sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==
|
||||||
|
|
||||||
|
layout-base@^2.0.0:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/layout-base/-/layout-base-2.0.1.tgz#d0337913586c90f9c2c075292069f5c2da5dd285"
|
||||||
|
integrity sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==
|
||||||
|
|
||||||
leven@^3.1.0:
|
leven@^3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
|
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
|
||||||
@@ -5487,6 +5897,11 @@ localforage@^1.8.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
lie "3.1.1"
|
lie "3.1.1"
|
||||||
|
|
||||||
|
lodash-es@^4.17.21:
|
||||||
|
version "4.17.21"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
|
||||||
|
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
|
||||||
|
|
||||||
lodash.camelcase@^4.3.0:
|
lodash.camelcase@^4.3.0:
|
||||||
version "4.3.0"
|
version "4.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
|
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
|
||||||
@@ -5608,6 +6023,31 @@ make-dir@^4.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
semver "^7.5.3"
|
semver "^7.5.3"
|
||||||
|
|
||||||
|
mdast-util-from-markdown@^1.3.0:
|
||||||
|
version "1.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz#9421a5a247f10d31d2faed2a30df5ec89ceafcf0"
|
||||||
|
integrity sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==
|
||||||
|
dependencies:
|
||||||
|
"@types/mdast" "^3.0.0"
|
||||||
|
"@types/unist" "^2.0.0"
|
||||||
|
decode-named-character-reference "^1.0.0"
|
||||||
|
mdast-util-to-string "^3.1.0"
|
||||||
|
micromark "^3.0.0"
|
||||||
|
micromark-util-decode-numeric-character-reference "^1.0.0"
|
||||||
|
micromark-util-decode-string "^1.0.0"
|
||||||
|
micromark-util-normalize-identifier "^1.0.0"
|
||||||
|
micromark-util-symbol "^1.0.0"
|
||||||
|
micromark-util-types "^1.0.0"
|
||||||
|
unist-util-stringify-position "^3.0.0"
|
||||||
|
uvu "^0.5.0"
|
||||||
|
|
||||||
|
mdast-util-to-string@^3.1.0:
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz#66f7bb6324756741c5f47a53557f0cbf16b6f789"
|
||||||
|
integrity sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==
|
||||||
|
dependencies:
|
||||||
|
"@types/mdast" "^3.0.0"
|
||||||
|
|
||||||
merge-stream@^2.0.0:
|
merge-stream@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
|
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
|
||||||
@@ -5618,6 +6058,223 @@ merge2@^1.3.0, merge2@^1.4.1:
|
|||||||
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
||||||
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
|
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
|
||||||
|
|
||||||
|
mermaid@10.2.3:
|
||||||
|
version "10.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.2.3.tgz#789d3b582c5da8c69aa4a7c0e2b826562c8c8b12"
|
||||||
|
integrity sha512-cMVE5s9PlQvOwfORkyVpr5beMsLdInrycAosdr+tpZ0WFjG4RJ/bUHST7aTgHNJbujHkdBRAm+N50P3puQOfPw==
|
||||||
|
dependencies:
|
||||||
|
"@braintree/sanitize-url" "^6.0.2"
|
||||||
|
cytoscape "^3.23.0"
|
||||||
|
cytoscape-cose-bilkent "^4.1.0"
|
||||||
|
cytoscape-fcose "^2.1.0"
|
||||||
|
d3 "^7.4.0"
|
||||||
|
dagre-d3-es "7.0.10"
|
||||||
|
dayjs "^1.11.7"
|
||||||
|
dompurify "3.0.3"
|
||||||
|
elkjs "^0.8.2"
|
||||||
|
khroma "^2.0.0"
|
||||||
|
lodash-es "^4.17.21"
|
||||||
|
mdast-util-from-markdown "^1.3.0"
|
||||||
|
non-layered-tidy-tree-layout "^2.0.2"
|
||||||
|
stylis "^4.1.3"
|
||||||
|
ts-dedent "^2.2.0"
|
||||||
|
uuid "^9.0.0"
|
||||||
|
web-worker "^1.2.0"
|
||||||
|
|
||||||
|
micromark-core-commonmark@^1.0.1:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz#1386628df59946b2d39fb2edfd10f3e8e0a75bb8"
|
||||||
|
integrity sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==
|
||||||
|
dependencies:
|
||||||
|
decode-named-character-reference "^1.0.0"
|
||||||
|
micromark-factory-destination "^1.0.0"
|
||||||
|
micromark-factory-label "^1.0.0"
|
||||||
|
micromark-factory-space "^1.0.0"
|
||||||
|
micromark-factory-title "^1.0.0"
|
||||||
|
micromark-factory-whitespace "^1.0.0"
|
||||||
|
micromark-util-character "^1.0.0"
|
||||||
|
micromark-util-chunked "^1.0.0"
|
||||||
|
micromark-util-classify-character "^1.0.0"
|
||||||
|
micromark-util-html-tag-name "^1.0.0"
|
||||||
|
micromark-util-normalize-identifier "^1.0.0"
|
||||||
|
micromark-util-resolve-all "^1.0.0"
|
||||||
|
micromark-util-subtokenize "^1.0.0"
|
||||||
|
micromark-util-symbol "^1.0.0"
|
||||||
|
micromark-util-types "^1.0.1"
|
||||||
|
uvu "^0.5.0"
|
||||||
|
|
||||||
|
micromark-factory-destination@^1.0.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz#eb815957d83e6d44479b3df640f010edad667b9f"
|
||||||
|
integrity sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==
|
||||||
|
dependencies:
|
||||||
|
micromark-util-character "^1.0.0"
|
||||||
|
micromark-util-symbol "^1.0.0"
|
||||||
|
micromark-util-types "^1.0.0"
|
||||||
|
|
||||||
|
micromark-factory-label@^1.0.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz#cc95d5478269085cfa2a7282b3de26eb2e2dec68"
|
||||||
|
integrity sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==
|
||||||
|
dependencies:
|
||||||
|
micromark-util-character "^1.0.0"
|
||||||
|
micromark-util-symbol "^1.0.0"
|
||||||
|
micromark-util-types "^1.0.0"
|
||||||
|
uvu "^0.5.0"
|
||||||
|
|
||||||
|
micromark-factory-space@^1.0.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz#c8f40b0640a0150751d3345ed885a080b0d15faf"
|
||||||
|
integrity sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==
|
||||||
|
dependencies:
|
||||||
|
micromark-util-character "^1.0.0"
|
||||||
|
micromark-util-types "^1.0.0"
|
||||||
|
|
||||||
|
micromark-factory-title@^1.0.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz#dd0fe951d7a0ac71bdc5ee13e5d1465ad7f50ea1"
|
||||||
|
integrity sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==
|
||||||
|
dependencies:
|
||||||
|
micromark-factory-space "^1.0.0"
|
||||||
|
micromark-util-character "^1.0.0"
|
||||||
|
micromark-util-symbol "^1.0.0"
|
||||||
|
micromark-util-types "^1.0.0"
|
||||||
|
|
||||||
|
micromark-factory-whitespace@^1.0.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz#798fb7489f4c8abafa7ca77eed6b5745853c9705"
|
||||||
|
integrity sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==
|
||||||
|
dependencies:
|
||||||
|
micromark-factory-space "^1.0.0"
|
||||||
|
micromark-util-character "^1.0.0"
|
||||||
|
micromark-util-symbol "^1.0.0"
|
||||||
|
micromark-util-types "^1.0.0"
|
||||||
|
|
||||||
|
micromark-util-character@^1.0.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.2.0.tgz#4fedaa3646db249bc58caeb000eb3549a8ca5dcc"
|
||||||
|
integrity sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==
|
||||||
|
dependencies:
|
||||||
|
micromark-util-symbol "^1.0.0"
|
||||||
|
micromark-util-types "^1.0.0"
|
||||||
|
|
||||||
|
micromark-util-chunked@^1.0.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz#37a24d33333c8c69a74ba12a14651fd9ea8a368b"
|
||||||
|
integrity sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==
|
||||||
|
dependencies:
|
||||||
|
micromark-util-symbol "^1.0.0"
|
||||||
|
|
||||||
|
micromark-util-classify-character@^1.0.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz#6a7f8c8838e8a120c8e3c4f2ae97a2bff9190e9d"
|
||||||
|
integrity sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==
|
||||||
|
dependencies:
|
||||||
|
micromark-util-character "^1.0.0"
|
||||||
|
micromark-util-symbol "^1.0.0"
|
||||||
|
micromark-util-types "^1.0.0"
|
||||||
|
|
||||||
|
micromark-util-combine-extensions@^1.0.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz#192e2b3d6567660a85f735e54d8ea6e3952dbe84"
|
||||||
|
integrity sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==
|
||||||
|
dependencies:
|
||||||
|
micromark-util-chunked "^1.0.0"
|
||||||
|
micromark-util-types "^1.0.0"
|
||||||
|
|
||||||
|
micromark-util-decode-numeric-character-reference@^1.0.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz#b1e6e17009b1f20bc652a521309c5f22c85eb1c6"
|
||||||
|
integrity sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==
|
||||||
|
dependencies:
|
||||||
|
micromark-util-symbol "^1.0.0"
|
||||||
|
|
||||||
|
micromark-util-decode-string@^1.0.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz#dc12b078cba7a3ff690d0203f95b5d5537f2809c"
|
||||||
|
integrity sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==
|
||||||
|
dependencies:
|
||||||
|
decode-named-character-reference "^1.0.0"
|
||||||
|
micromark-util-character "^1.0.0"
|
||||||
|
micromark-util-decode-numeric-character-reference "^1.0.0"
|
||||||
|
micromark-util-symbol "^1.0.0"
|
||||||
|
|
||||||
|
micromark-util-encode@^1.0.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz#92e4f565fd4ccb19e0dcae1afab9a173bbeb19a5"
|
||||||
|
integrity sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==
|
||||||
|
|
||||||
|
micromark-util-html-tag-name@^1.0.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz#48fd7a25826f29d2f71479d3b4e83e94829b3588"
|
||||||
|
integrity sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==
|
||||||
|
|
||||||
|
micromark-util-normalize-identifier@^1.0.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz#7a73f824eb9f10d442b4d7f120fecb9b38ebf8b7"
|
||||||
|
integrity sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==
|
||||||
|
dependencies:
|
||||||
|
micromark-util-symbol "^1.0.0"
|
||||||
|
|
||||||
|
micromark-util-resolve-all@^1.0.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz#4652a591ee8c8fa06714c9b54cd6c8e693671188"
|
||||||
|
integrity sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==
|
||||||
|
dependencies:
|
||||||
|
micromark-util-types "^1.0.0"
|
||||||
|
|
||||||
|
micromark-util-sanitize-uri@^1.0.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz#613f738e4400c6eedbc53590c67b197e30d7f90d"
|
||||||
|
integrity sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==
|
||||||
|
dependencies:
|
||||||
|
micromark-util-character "^1.0.0"
|
||||||
|
micromark-util-encode "^1.0.0"
|
||||||
|
micromark-util-symbol "^1.0.0"
|
||||||
|
|
||||||
|
micromark-util-subtokenize@^1.0.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz#941c74f93a93eaf687b9054aeb94642b0e92edb1"
|
||||||
|
integrity sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==
|
||||||
|
dependencies:
|
||||||
|
micromark-util-chunked "^1.0.0"
|
||||||
|
micromark-util-symbol "^1.0.0"
|
||||||
|
micromark-util-types "^1.0.0"
|
||||||
|
uvu "^0.5.0"
|
||||||
|
|
||||||
|
micromark-util-symbol@^1.0.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz#813cd17837bdb912d069a12ebe3a44b6f7063142"
|
||||||
|
integrity sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==
|
||||||
|
|
||||||
|
micromark-util-types@^1.0.0, micromark-util-types@^1.0.1:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.1.0.tgz#e6676a8cae0bb86a2171c498167971886cb7e283"
|
||||||
|
integrity sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==
|
||||||
|
|
||||||
|
micromark@^3.0.0:
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.2.0.tgz#1af9fef3f995ea1ea4ac9c7e2f19c48fd5c006e9"
|
||||||
|
integrity sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==
|
||||||
|
dependencies:
|
||||||
|
"@types/debug" "^4.0.0"
|
||||||
|
debug "^4.0.0"
|
||||||
|
decode-named-character-reference "^1.0.0"
|
||||||
|
micromark-core-commonmark "^1.0.1"
|
||||||
|
micromark-factory-space "^1.0.0"
|
||||||
|
micromark-util-character "^1.0.0"
|
||||||
|
micromark-util-chunked "^1.0.0"
|
||||||
|
micromark-util-combine-extensions "^1.0.0"
|
||||||
|
micromark-util-decode-numeric-character-reference "^1.0.0"
|
||||||
|
micromark-util-encode "^1.0.0"
|
||||||
|
micromark-util-normalize-identifier "^1.0.0"
|
||||||
|
micromark-util-resolve-all "^1.0.0"
|
||||||
|
micromark-util-sanitize-uri "^1.0.0"
|
||||||
|
micromark-util-subtokenize "^1.0.0"
|
||||||
|
micromark-util-symbol "^1.0.0"
|
||||||
|
micromark-util-types "^1.0.1"
|
||||||
|
uvu "^0.5.0"
|
||||||
|
|
||||||
micromatch@^4.0.4:
|
micromatch@^4.0.4:
|
||||||
version "4.0.5"
|
version "4.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
|
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
|
||||||
@@ -5696,6 +6353,11 @@ moo-color@^1.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
color-name "^1.1.4"
|
color-name "^1.1.4"
|
||||||
|
|
||||||
|
mri@^1.1.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
|
||||||
|
integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==
|
||||||
|
|
||||||
mrmime@^1.0.0:
|
mrmime@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27"
|
resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27"
|
||||||
@@ -5729,6 +6391,11 @@ nanoid@3.3.3:
|
|||||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25"
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25"
|
||||||
integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==
|
integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==
|
||||||
|
|
||||||
|
nanoid@4.0.2:
|
||||||
|
version "4.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-4.0.2.tgz#140b3c5003959adbebf521c170f282c5e7f9fb9e"
|
||||||
|
integrity sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==
|
||||||
|
|
||||||
nanoid@^3.3.6:
|
nanoid@^3.3.6:
|
||||||
version "3.3.6"
|
version "3.3.6"
|
||||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
|
||||||
@@ -5754,6 +6421,11 @@ node-releases@^2.0.8:
|
|||||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f"
|
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f"
|
||||||
integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==
|
integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==
|
||||||
|
|
||||||
|
non-layered-tidy-tree-layout@^2.0.2:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz#57d35d13c356643fc296a55fb11ac15e74da7804"
|
||||||
|
integrity sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==
|
||||||
|
|
||||||
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
||||||
@@ -6428,6 +7100,11 @@ rimraf@^3.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
glob "^7.1.3"
|
glob "^7.1.3"
|
||||||
|
|
||||||
|
robust-predicates@^3.0.0:
|
||||||
|
version "3.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771"
|
||||||
|
integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==
|
||||||
|
|
||||||
rollup-plugin-terser@^7.0.0:
|
rollup-plugin-terser@^7.0.0:
|
||||||
version "7.0.2"
|
version "7.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d"
|
resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d"
|
||||||
@@ -6459,10 +7136,10 @@ rollup@^3.25.2:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.3.2"
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
roughjs@4.6.5:
|
roughjs@4.6.4:
|
||||||
version "4.6.5"
|
version "4.6.4"
|
||||||
resolved "https://registry.yarnpkg.com/roughjs/-/roughjs-4.6.5.tgz#1db965cf1a043cb7f05181dd7d119f7960fba8d8"
|
resolved "https://registry.yarnpkg.com/roughjs/-/roughjs-4.6.4.tgz#b6f39b44645854a6e0a4a28b078368701eb7f939"
|
||||||
integrity sha512-4Q6XBbZWlp8yj1uipq2bQ1CPlxMhW/ukufwkuhh+2L79utk+O5kMSbfVh4UNBMtKJ3PxHQ9Ou3ncNt1iQcphJA==
|
integrity sha512-s6EZ0BntezkFYMf/9mGn7M8XGIoaav9QQBCnJROWB3brUWQ683Q2LbRD/hq0Z3bAJ/9NVpU/5LpiTWvQMyLDhw==
|
||||||
dependencies:
|
dependencies:
|
||||||
hachure-fill "^0.5.2"
|
hachure-fill "^0.5.2"
|
||||||
path-data-parser "^0.1.0"
|
path-data-parser "^0.1.0"
|
||||||
@@ -6481,6 +7158,11 @@ run-parallel@^1.1.9:
|
|||||||
dependencies:
|
dependencies:
|
||||||
queue-microtask "^1.2.2"
|
queue-microtask "^1.2.2"
|
||||||
|
|
||||||
|
rw@1:
|
||||||
|
version "1.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
|
||||||
|
integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==
|
||||||
|
|
||||||
rxjs@^7.5.5:
|
rxjs@^7.5.5:
|
||||||
version "7.8.0"
|
version "7.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4"
|
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4"
|
||||||
@@ -6488,6 +7170,13 @@ rxjs@^7.5.5:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.1.0"
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
sade@^1.7.3:
|
||||||
|
version "1.8.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701"
|
||||||
|
integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==
|
||||||
|
dependencies:
|
||||||
|
mri "^1.1.0"
|
||||||
|
|
||||||
safari-14-idb-fix@^3.0.0:
|
safari-14-idb-fix@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/safari-14-idb-fix/-/safari-14-idb-fix-3.0.0.tgz#450fc049b996ec7f3fd9ca2f89d32e0761583440"
|
resolved "https://registry.yarnpkg.com/safari-14-idb-fix/-/safari-14-idb-fix-3.0.0.tgz#450fc049b996ec7f3fd9ca2f89d32e0761583440"
|
||||||
@@ -6866,6 +7555,11 @@ strip-literal@^1.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
acorn "^8.8.2"
|
acorn "^8.8.2"
|
||||||
|
|
||||||
|
stylis@^4.1.3:
|
||||||
|
version "4.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.0.tgz#abe305a669fc3d8777e10eefcfc73ad861c5588c"
|
||||||
|
integrity sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==
|
||||||
|
|
||||||
supports-color@^5.3.0:
|
supports-color@^5.3.0:
|
||||||
version "5.5.0"
|
version "5.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
||||||
@@ -7028,6 +7722,11 @@ tr46@^4.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode "^2.3.0"
|
punycode "^2.3.0"
|
||||||
|
|
||||||
|
ts-dedent@^2.2.0:
|
||||||
|
version "2.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5"
|
||||||
|
integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==
|
||||||
|
|
||||||
tsconfig-paths@^3.14.1:
|
tsconfig-paths@^3.14.1:
|
||||||
version "3.14.2"
|
version "3.14.2"
|
||||||
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088"
|
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088"
|
||||||
@@ -7169,6 +7868,13 @@ unique-string@^2.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
crypto-random-string "^2.0.0"
|
crypto-random-string "^2.0.0"
|
||||||
|
|
||||||
|
unist-util-stringify-position@^3.0.0:
|
||||||
|
version "3.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz#03ad3348210c2d930772d64b489580c13a7db39d"
|
||||||
|
integrity sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==
|
||||||
|
dependencies:
|
||||||
|
"@types/unist" "^2.0.0"
|
||||||
|
|
||||||
universalify@^0.2.0:
|
universalify@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0"
|
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0"
|
||||||
@@ -7237,6 +7943,21 @@ use-sync-external-store@1.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||||
|
|
||||||
|
uuid@^9.0.0:
|
||||||
|
version "9.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
|
||||||
|
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==
|
||||||
|
|
||||||
|
uvu@^0.5.0:
|
||||||
|
version "0.5.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df"
|
||||||
|
integrity sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==
|
||||||
|
dependencies:
|
||||||
|
dequal "^2.0.0"
|
||||||
|
diff "^5.0.0"
|
||||||
|
kleur "^4.0.3"
|
||||||
|
sade "^1.7.3"
|
||||||
|
|
||||||
v8-compile-cache@^2.0.3:
|
v8-compile-cache@^2.0.3:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
|
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
|
||||||
@@ -7422,6 +8143,11 @@ w3c-xmlserializer@^4.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
xml-name-validator "^4.0.0"
|
xml-name-validator "^4.0.0"
|
||||||
|
|
||||||
|
web-worker@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.2.0.tgz#5d85a04a7fbc1e7db58f66595d7a3ac7c9c180da"
|
||||||
|
integrity sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==
|
||||||
|
|
||||||
webidl-conversions@^4.0.2:
|
webidl-conversions@^4.0.2:
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
|
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
|
||||||
|
|||||||
Reference in New Issue
Block a user