mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-10-24 08:24:32 +02:00
Compare commits
1 Commits
dependabot
...
are/librar
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b51f6d178c |
@@ -1,211 +0,0 @@
|
||||
import React from "react";
|
||||
import { Excalidraw, mutateElement } from "../index";
|
||||
import { act, assertElements, render } from "../tests/test-utils";
|
||||
import { API } from "../tests/helpers/api";
|
||||
import { actionDeleteSelected } from "./actionDeleteSelected";
|
||||
|
||||
const { h } = window;
|
||||
|
||||
describe("deleting selected elements when frame selected should keep children + select them", () => {
|
||||
beforeEach(async () => {
|
||||
await render(<Excalidraw />);
|
||||
});
|
||||
|
||||
it("frame only", async () => {
|
||||
const f1 = API.createElement({
|
||||
type: "frame",
|
||||
});
|
||||
|
||||
const r1 = API.createElement({
|
||||
type: "rectangle",
|
||||
frameId: f1.id,
|
||||
});
|
||||
|
||||
API.setElements([f1, r1]);
|
||||
|
||||
API.setSelectedElements([f1]);
|
||||
|
||||
act(() => {
|
||||
h.app.actionManager.executeAction(actionDeleteSelected);
|
||||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ id: f1.id, isDeleted: true },
|
||||
{ id: r1.id, isDeleted: false, selected: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("frame + text container (text's frameId set)", async () => {
|
||||
const f1 = API.createElement({
|
||||
type: "frame",
|
||||
});
|
||||
|
||||
const r1 = API.createElement({
|
||||
type: "rectangle",
|
||||
frameId: f1.id,
|
||||
});
|
||||
|
||||
const t1 = API.createElement({
|
||||
type: "text",
|
||||
width: 200,
|
||||
height: 100,
|
||||
fontSize: 20,
|
||||
containerId: r1.id,
|
||||
frameId: f1.id,
|
||||
});
|
||||
|
||||
mutateElement(r1, {
|
||||
boundElements: [{ type: "text", id: t1.id }],
|
||||
});
|
||||
|
||||
API.setElements([f1, r1, t1]);
|
||||
|
||||
API.setSelectedElements([f1]);
|
||||
|
||||
act(() => {
|
||||
h.app.actionManager.executeAction(actionDeleteSelected);
|
||||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ id: f1.id, isDeleted: true },
|
||||
{ id: r1.id, isDeleted: false, selected: true },
|
||||
{ id: t1.id, isDeleted: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it("frame + text container (text's frameId not set)", async () => {
|
||||
const f1 = API.createElement({
|
||||
type: "frame",
|
||||
});
|
||||
|
||||
const r1 = API.createElement({
|
||||
type: "rectangle",
|
||||
frameId: f1.id,
|
||||
});
|
||||
|
||||
const t1 = API.createElement({
|
||||
type: "text",
|
||||
width: 200,
|
||||
height: 100,
|
||||
fontSize: 20,
|
||||
containerId: r1.id,
|
||||
frameId: null,
|
||||
});
|
||||
|
||||
mutateElement(r1, {
|
||||
boundElements: [{ type: "text", id: t1.id }],
|
||||
});
|
||||
|
||||
API.setElements([f1, r1, t1]);
|
||||
|
||||
API.setSelectedElements([f1]);
|
||||
|
||||
act(() => {
|
||||
h.app.actionManager.executeAction(actionDeleteSelected);
|
||||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ id: f1.id, isDeleted: true },
|
||||
{ id: r1.id, isDeleted: false, selected: true },
|
||||
{ id: t1.id, isDeleted: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it("frame + text container (text selected too)", async () => {
|
||||
const f1 = API.createElement({
|
||||
type: "frame",
|
||||
});
|
||||
|
||||
const r1 = API.createElement({
|
||||
type: "rectangle",
|
||||
frameId: f1.id,
|
||||
});
|
||||
|
||||
const t1 = API.createElement({
|
||||
type: "text",
|
||||
width: 200,
|
||||
height: 100,
|
||||
fontSize: 20,
|
||||
containerId: r1.id,
|
||||
frameId: null,
|
||||
});
|
||||
|
||||
mutateElement(r1, {
|
||||
boundElements: [{ type: "text", id: t1.id }],
|
||||
});
|
||||
|
||||
API.setElements([f1, r1, t1]);
|
||||
|
||||
API.setSelectedElements([f1, t1]);
|
||||
|
||||
act(() => {
|
||||
h.app.actionManager.executeAction(actionDeleteSelected);
|
||||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ id: f1.id, isDeleted: true },
|
||||
{ id: r1.id, isDeleted: false, selected: true },
|
||||
{ id: t1.id, isDeleted: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it("frame + labeled arrow", async () => {
|
||||
const f1 = API.createElement({
|
||||
type: "frame",
|
||||
});
|
||||
|
||||
const a1 = API.createElement({
|
||||
type: "arrow",
|
||||
frameId: f1.id,
|
||||
});
|
||||
|
||||
const t1 = API.createElement({
|
||||
type: "text",
|
||||
width: 200,
|
||||
height: 100,
|
||||
fontSize: 20,
|
||||
containerId: a1.id,
|
||||
frameId: null,
|
||||
});
|
||||
|
||||
mutateElement(a1, {
|
||||
boundElements: [{ type: "text", id: t1.id }],
|
||||
});
|
||||
|
||||
API.setElements([f1, a1, t1]);
|
||||
|
||||
API.setSelectedElements([f1, t1]);
|
||||
|
||||
act(() => {
|
||||
h.app.actionManager.executeAction(actionDeleteSelected);
|
||||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ id: f1.id, isDeleted: true },
|
||||
{ id: a1.id, isDeleted: false, selected: true },
|
||||
{ id: t1.id, isDeleted: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it("frame + children selected", async () => {
|
||||
const f1 = API.createElement({
|
||||
type: "frame",
|
||||
});
|
||||
const r1 = API.createElement({
|
||||
type: "rectangle",
|
||||
frameId: f1.id,
|
||||
});
|
||||
API.setElements([f1, r1]);
|
||||
|
||||
API.setSelectedElements([f1, r1]);
|
||||
|
||||
act(() => {
|
||||
h.app.actionManager.executeAction(actionDeleteSelected);
|
||||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ id: f1.id, isDeleted: true },
|
||||
{ id: r1.id, isDeleted: false, selected: true },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -18,8 +18,6 @@ import {
|
||||
import { updateActiveTool } from "../utils";
|
||||
import { TrashIcon } from "../components/icons";
|
||||
import { StoreAction } from "../store";
|
||||
import { getContainerElement } from "../element/textElement";
|
||||
import { getFrameChildren } from "../frame";
|
||||
|
||||
const deleteSelectedElements = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
@@ -35,50 +33,10 @@ const deleteSelectedElements = (
|
||||
|
||||
const selectedElementIds: Record<ExcalidrawElement["id"], true> = {};
|
||||
|
||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||
|
||||
const processedElements = new Set<ExcalidrawElement["id"]>();
|
||||
|
||||
for (const frameId of framesToBeDeleted) {
|
||||
const frameChildren = getFrameChildren(elements, frameId);
|
||||
for (const el of frameChildren) {
|
||||
if (processedElements.has(el.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isBoundToContainer(el)) {
|
||||
const containerElement = getContainerElement(el, elementsMap);
|
||||
if (containerElement) {
|
||||
selectedElementIds[containerElement.id] = true;
|
||||
}
|
||||
} else {
|
||||
selectedElementIds[el.id] = true;
|
||||
}
|
||||
processedElements.add(el.id);
|
||||
}
|
||||
}
|
||||
|
||||
let shouldSelectEditingGroup = true;
|
||||
|
||||
const nextElements = elements.map((el) => {
|
||||
if (appState.selectedElementIds[el.id]) {
|
||||
const boundElement = isBoundToContainer(el)
|
||||
? getContainerElement(el, elementsMap)
|
||||
: null;
|
||||
|
||||
if (el.frameId && framesToBeDeleted.has(el.frameId)) {
|
||||
shouldSelectEditingGroup = false;
|
||||
selectedElementIds[el.id] = true;
|
||||
return el;
|
||||
}
|
||||
|
||||
if (
|
||||
boundElement?.frameId &&
|
||||
framesToBeDeleted.has(boundElement?.frameId)
|
||||
) {
|
||||
return el;
|
||||
}
|
||||
|
||||
if (el.boundElements) {
|
||||
el.boundElements.forEach((candidate) => {
|
||||
const bound = app.scene.getNonDeletedElementsMap().get(candidate.id);
|
||||
@@ -101,9 +59,7 @@ const deleteSelectedElements = (
|
||||
// if deleting a frame, remove the children from it and select them
|
||||
if (el.frameId && framesToBeDeleted.has(el.frameId)) {
|
||||
shouldSelectEditingGroup = false;
|
||||
if (!isBoundToContainer(el)) {
|
||||
selectedElementIds[el.id] = true;
|
||||
}
|
||||
selectedElementIds[el.id] = true;
|
||||
return newElementWith(el, { frameId: null });
|
||||
}
|
||||
|
||||
@@ -268,13 +224,11 @@ export const actionDeleteSelected = register({
|
||||
storeAction: StoreAction.CAPTURE,
|
||||
};
|
||||
}
|
||||
|
||||
let { elements: nextElements, appState: nextAppState } =
|
||||
deleteSelectedElements(elements, appState, app);
|
||||
|
||||
fixBindingsAfterDeletion(
|
||||
nextElements,
|
||||
nextElements.filter((el) => el.isDeleted),
|
||||
elements.filter(({ id }) => appState.selectedElementIds[id]),
|
||||
);
|
||||
|
||||
nextAppState = handleGroupEditingState(nextAppState, nextElements);
|
||||
|
||||
@@ -1,530 +0,0 @@
|
||||
import { Excalidraw } from "../index";
|
||||
import {
|
||||
act,
|
||||
assertElements,
|
||||
getCloneByOrigId,
|
||||
render,
|
||||
} from "../tests/test-utils";
|
||||
import { API } from "../tests/helpers/api";
|
||||
import { actionDuplicateSelection } from "./actionDuplicateSelection";
|
||||
import React from "react";
|
||||
import { ORIG_ID } from "../constants";
|
||||
|
||||
const { h } = window;
|
||||
|
||||
describe("actionDuplicateSelection", () => {
|
||||
beforeEach(async () => {
|
||||
await render(<Excalidraw />);
|
||||
});
|
||||
|
||||
describe("duplicating frames", () => {
|
||||
it("frame selected only", async () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
});
|
||||
|
||||
const rectangle = API.createElement({
|
||||
type: "rectangle",
|
||||
frameId: frame.id,
|
||||
});
|
||||
|
||||
API.setElements([frame, rectangle]);
|
||||
API.setSelectedElements([frame]);
|
||||
|
||||
act(() => {
|
||||
h.app.actionManager.executeAction(actionDuplicateSelection);
|
||||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ id: frame.id },
|
||||
{ id: rectangle.id, frameId: frame.id },
|
||||
{ [ORIG_ID]: rectangle.id, frameId: getCloneByOrigId(frame.id)?.id },
|
||||
{ [ORIG_ID]: frame.id, selected: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("frame selected only (with text container)", async () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
});
|
||||
|
||||
const [rectangle, text] = API.createTextContainer({ frameId: frame.id });
|
||||
|
||||
API.setElements([frame, rectangle, text]);
|
||||
API.setSelectedElements([frame]);
|
||||
|
||||
act(() => {
|
||||
h.app.actionManager.executeAction(actionDuplicateSelection);
|
||||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ id: frame.id },
|
||||
{ id: rectangle.id, frameId: frame.id },
|
||||
{ id: text.id, containerId: rectangle.id, frameId: frame.id },
|
||||
{ [ORIG_ID]: rectangle.id, frameId: getCloneByOrigId(frame.id)?.id },
|
||||
{
|
||||
[ORIG_ID]: text.id,
|
||||
containerId: getCloneByOrigId(rectangle.id)?.id,
|
||||
frameId: getCloneByOrigId(frame.id)?.id,
|
||||
},
|
||||
{ [ORIG_ID]: frame.id, selected: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("frame + text container selected (order A)", async () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
});
|
||||
|
||||
const [rectangle, text] = API.createTextContainer({ frameId: frame.id });
|
||||
|
||||
API.setElements([frame, rectangle, text]);
|
||||
API.setSelectedElements([frame, rectangle]);
|
||||
|
||||
act(() => {
|
||||
h.app.actionManager.executeAction(actionDuplicateSelection);
|
||||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ id: frame.id },
|
||||
{ id: rectangle.id, frameId: frame.id },
|
||||
{ id: text.id, containerId: rectangle.id, frameId: frame.id },
|
||||
{
|
||||
[ORIG_ID]: rectangle.id,
|
||||
frameId: getCloneByOrigId(frame.id)?.id,
|
||||
},
|
||||
{
|
||||
[ORIG_ID]: text.id,
|
||||
containerId: getCloneByOrigId(rectangle.id)?.id,
|
||||
frameId: getCloneByOrigId(frame.id)?.id,
|
||||
},
|
||||
{
|
||||
[ORIG_ID]: frame.id,
|
||||
selected: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("frame + text container selected (order B)", async () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
});
|
||||
|
||||
const [rectangle, text] = API.createTextContainer({ frameId: frame.id });
|
||||
|
||||
API.setElements([text, rectangle, frame]);
|
||||
API.setSelectedElements([rectangle, frame]);
|
||||
|
||||
act(() => {
|
||||
h.app.actionManager.executeAction(actionDuplicateSelection);
|
||||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ id: rectangle.id, frameId: frame.id },
|
||||
{ id: text.id, containerId: rectangle.id, frameId: frame.id },
|
||||
{ id: frame.id },
|
||||
{
|
||||
type: "rectangle",
|
||||
[ORIG_ID]: `${rectangle.id}`,
|
||||
},
|
||||
{
|
||||
[ORIG_ID]: `${text.id}`,
|
||||
type: "text",
|
||||
containerId: getCloneByOrigId(rectangle.id)?.id,
|
||||
frameId: getCloneByOrigId(frame.id)?.id,
|
||||
},
|
||||
{ [ORIG_ID]: `${frame.id}`, type: "frame", selected: true },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("duplicating frame children", () => {
|
||||
it("frame child selected", () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
});
|
||||
|
||||
const rectangle = API.createElement({
|
||||
type: "rectangle",
|
||||
frameId: frame.id,
|
||||
});
|
||||
|
||||
API.setElements([frame, rectangle]);
|
||||
API.setSelectedElements([rectangle]);
|
||||
|
||||
act(() => {
|
||||
h.app.actionManager.executeAction(actionDuplicateSelection);
|
||||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ id: frame.id },
|
||||
{ id: rectangle.id, frameId: frame.id },
|
||||
{ [ORIG_ID]: rectangle.id, frameId: frame.id, selected: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("frame text container selected (rectangle selected)", () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
});
|
||||
|
||||
const [rectangle, text] = API.createTextContainer({ frameId: frame.id });
|
||||
|
||||
API.setElements([frame, rectangle, text]);
|
||||
API.setSelectedElements([rectangle]);
|
||||
|
||||
act(() => {
|
||||
h.app.actionManager.executeAction(actionDuplicateSelection);
|
||||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ id: frame.id },
|
||||
{ id: rectangle.id, frameId: frame.id },
|
||||
{ id: text.id, containerId: rectangle.id, frameId: frame.id },
|
||||
{ [ORIG_ID]: rectangle.id, frameId: frame.id, selected: true },
|
||||
{
|
||||
[ORIG_ID]: text.id,
|
||||
containerId: getCloneByOrigId(rectangle.id).id,
|
||||
frameId: frame.id,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("frame bound text selected (container not selected)", () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
});
|
||||
|
||||
const [rectangle, text] = API.createTextContainer({ frameId: frame.id });
|
||||
|
||||
API.setElements([frame, rectangle, text]);
|
||||
API.setSelectedElements([text]);
|
||||
|
||||
act(() => {
|
||||
h.app.actionManager.executeAction(actionDuplicateSelection);
|
||||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ id: frame.id },
|
||||
{ id: rectangle.id, frameId: frame.id },
|
||||
{ id: text.id, containerId: rectangle.id, frameId: frame.id },
|
||||
{ [ORIG_ID]: rectangle.id, frameId: frame.id, selected: true },
|
||||
{
|
||||
[ORIG_ID]: text.id,
|
||||
containerId: getCloneByOrigId(rectangle.id).id,
|
||||
frameId: frame.id,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("frame text container selected (text not exists)", () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
});
|
||||
|
||||
const [rectangle] = API.createTextContainer({ frameId: frame.id });
|
||||
|
||||
API.setElements([frame, rectangle]);
|
||||
API.setSelectedElements([rectangle]);
|
||||
|
||||
act(() => {
|
||||
h.app.actionManager.executeAction(actionDuplicateSelection);
|
||||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ id: frame.id },
|
||||
{ id: rectangle.id, frameId: frame.id },
|
||||
{ [ORIG_ID]: rectangle.id, frameId: frame.id, selected: true },
|
||||
]);
|
||||
});
|
||||
|
||||
// shouldn't happen
|
||||
it("frame bound text selected (container not exists)", () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
});
|
||||
|
||||
const [, text] = API.createTextContainer({ frameId: frame.id });
|
||||
|
||||
API.setElements([frame, text]);
|
||||
API.setSelectedElements([text]);
|
||||
|
||||
act(() => {
|
||||
h.app.actionManager.executeAction(actionDuplicateSelection);
|
||||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ id: frame.id },
|
||||
{ id: text.id, frameId: frame.id },
|
||||
{ [ORIG_ID]: text.id, frameId: frame.id },
|
||||
]);
|
||||
});
|
||||
|
||||
it("frame bound container selected (text has no frameId)", () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
});
|
||||
|
||||
const [rectangle, text] = API.createTextContainer({
|
||||
frameId: frame.id,
|
||||
label: { frameId: null },
|
||||
});
|
||||
|
||||
API.setElements([frame, rectangle, text]);
|
||||
API.setSelectedElements([rectangle]);
|
||||
|
||||
act(() => {
|
||||
h.app.actionManager.executeAction(actionDuplicateSelection);
|
||||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ id: frame.id },
|
||||
{ id: rectangle.id, frameId: frame.id },
|
||||
{ id: text.id, containerId: rectangle.id },
|
||||
{ [ORIG_ID]: rectangle.id, frameId: frame.id, selected: true },
|
||||
{
|
||||
[ORIG_ID]: text.id,
|
||||
containerId: getCloneByOrigId(rectangle.id).id,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("duplicating multiple frames", () => {
|
||||
it("multiple frames selected (no children)", () => {
|
||||
const frame1 = API.createElement({
|
||||
type: "frame",
|
||||
});
|
||||
|
||||
const rect1 = API.createElement({
|
||||
type: "rectangle",
|
||||
frameId: frame1.id,
|
||||
});
|
||||
|
||||
const frame2 = API.createElement({
|
||||
type: "frame",
|
||||
});
|
||||
|
||||
const rect2 = API.createElement({
|
||||
type: "rectangle",
|
||||
frameId: frame2.id,
|
||||
});
|
||||
|
||||
const ellipse = API.createElement({
|
||||
type: "ellipse",
|
||||
});
|
||||
|
||||
API.setElements([rect1, frame1, ellipse, rect2, frame2]);
|
||||
API.setSelectedElements([frame1, frame2]);
|
||||
|
||||
act(() => {
|
||||
h.app.actionManager.executeAction(actionDuplicateSelection);
|
||||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ id: rect1.id, frameId: frame1.id },
|
||||
{ id: frame1.id },
|
||||
{ [ORIG_ID]: rect1.id, frameId: getCloneByOrigId(frame1.id)?.id },
|
||||
{ [ORIG_ID]: frame1.id, selected: true },
|
||||
{ id: ellipse.id },
|
||||
{ id: rect2.id, frameId: frame2.id },
|
||||
{ id: frame2.id },
|
||||
{ [ORIG_ID]: rect2.id, frameId: getCloneByOrigId(frame2.id)?.id },
|
||||
{ [ORIG_ID]: frame2.id, selected: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("multiple frames selected (no children) + unrelated element", () => {
|
||||
const frame1 = API.createElement({
|
||||
type: "frame",
|
||||
});
|
||||
|
||||
const rect1 = API.createElement({
|
||||
type: "rectangle",
|
||||
frameId: frame1.id,
|
||||
});
|
||||
|
||||
const frame2 = API.createElement({
|
||||
type: "frame",
|
||||
});
|
||||
|
||||
const rect2 = API.createElement({
|
||||
type: "rectangle",
|
||||
frameId: frame2.id,
|
||||
});
|
||||
|
||||
const ellipse = API.createElement({
|
||||
type: "ellipse",
|
||||
});
|
||||
|
||||
API.setElements([rect1, frame1, ellipse, rect2, frame2]);
|
||||
API.setSelectedElements([frame1, ellipse, frame2]);
|
||||
|
||||
act(() => {
|
||||
h.app.actionManager.executeAction(actionDuplicateSelection);
|
||||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ id: rect1.id, frameId: frame1.id },
|
||||
{ id: frame1.id },
|
||||
{ [ORIG_ID]: rect1.id, frameId: getCloneByOrigId(frame1.id)?.id },
|
||||
{ [ORIG_ID]: frame1.id, selected: true },
|
||||
{ id: ellipse.id },
|
||||
{ [ORIG_ID]: ellipse.id, selected: true },
|
||||
{ id: rect2.id, frameId: frame2.id },
|
||||
{ id: frame2.id },
|
||||
{ [ORIG_ID]: rect2.id, frameId: getCloneByOrigId(frame2.id)?.id },
|
||||
{ [ORIG_ID]: frame2.id, selected: true },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("duplicating containers/bound elements", () => {
|
||||
it("labeled arrow (arrow selected)", () => {
|
||||
const [arrow, text] = API.createLabeledArrow();
|
||||
|
||||
API.setElements([arrow, text]);
|
||||
API.setSelectedElements([arrow]);
|
||||
|
||||
act(() => {
|
||||
h.app.actionManager.executeAction(actionDuplicateSelection);
|
||||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ id: arrow.id },
|
||||
{ id: text.id, containerId: arrow.id },
|
||||
{ [ORIG_ID]: arrow.id, selected: true },
|
||||
{ [ORIG_ID]: text.id, containerId: getCloneByOrigId(arrow.id)?.id },
|
||||
]);
|
||||
});
|
||||
|
||||
// shouldn't happen
|
||||
it("labeled arrow (text selected)", () => {
|
||||
const [arrow, text] = API.createLabeledArrow();
|
||||
|
||||
API.setElements([arrow, text]);
|
||||
API.setSelectedElements([text]);
|
||||
|
||||
act(() => {
|
||||
h.app.actionManager.executeAction(actionDuplicateSelection);
|
||||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ id: arrow.id },
|
||||
{ id: text.id, containerId: arrow.id },
|
||||
{ [ORIG_ID]: arrow.id, selected: true },
|
||||
{ [ORIG_ID]: text.id, containerId: getCloneByOrigId(arrow.id)?.id },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("duplicating groups", () => {
|
||||
it("duplicate group containing frame (children don't have groupIds set)", () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
groupIds: ["A"],
|
||||
});
|
||||
|
||||
const [rectangle, text] = API.createTextContainer({
|
||||
frameId: frame.id,
|
||||
});
|
||||
|
||||
const ellipse = API.createElement({
|
||||
type: "ellipse",
|
||||
groupIds: ["A"],
|
||||
});
|
||||
|
||||
API.setElements([rectangle, text, frame, ellipse]);
|
||||
API.setSelectedElements([frame, ellipse]);
|
||||
|
||||
act(() => {
|
||||
h.app.actionManager.executeAction(actionDuplicateSelection);
|
||||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ id: rectangle.id, frameId: frame.id },
|
||||
{ id: text.id, frameId: frame.id },
|
||||
{ id: frame.id },
|
||||
{ id: ellipse.id },
|
||||
{ [ORIG_ID]: rectangle.id, frameId: getCloneByOrigId(frame.id)?.id },
|
||||
{ [ORIG_ID]: text.id, frameId: getCloneByOrigId(frame.id)?.id },
|
||||
{ [ORIG_ID]: frame.id, selected: true },
|
||||
{ [ORIG_ID]: ellipse.id, selected: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("duplicate group containing frame (children have groupIds)", () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
groupIds: ["A"],
|
||||
});
|
||||
|
||||
const [rectangle, text] = API.createTextContainer({
|
||||
frameId: frame.id,
|
||||
groupIds: ["A"],
|
||||
});
|
||||
|
||||
const ellipse = API.createElement({
|
||||
type: "ellipse",
|
||||
groupIds: ["A"],
|
||||
});
|
||||
|
||||
API.setElements([rectangle, text, frame, ellipse]);
|
||||
API.setSelectedElements([frame, ellipse]);
|
||||
|
||||
act(() => {
|
||||
h.app.actionManager.executeAction(actionDuplicateSelection);
|
||||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ id: rectangle.id, frameId: frame.id },
|
||||
{ id: text.id, frameId: frame.id },
|
||||
{ id: frame.id },
|
||||
{ id: ellipse.id },
|
||||
{
|
||||
[ORIG_ID]: rectangle.id,
|
||||
frameId: getCloneByOrigId(frame.id)?.id,
|
||||
// FIXME shouldn't be selected (in selectGroupsForSelectedElements)
|
||||
selected: true,
|
||||
},
|
||||
{
|
||||
[ORIG_ID]: text.id,
|
||||
frameId: getCloneByOrigId(frame.id)?.id,
|
||||
// FIXME shouldn't be selected (in selectGroupsForSelectedElements)
|
||||
selected: true,
|
||||
},
|
||||
{ [ORIG_ID]: frame.id, selected: true },
|
||||
{ [ORIG_ID]: ellipse.id, selected: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("duplicating element nested in group", () => {
|
||||
const ellipse = API.createElement({
|
||||
type: "ellipse",
|
||||
groupIds: ["B"],
|
||||
});
|
||||
const rect1 = API.createElement({
|
||||
type: "rectangle",
|
||||
groupIds: ["A", "B"],
|
||||
});
|
||||
const rect2 = API.createElement({
|
||||
type: "rectangle",
|
||||
groupIds: ["A", "B"],
|
||||
});
|
||||
|
||||
API.setElements([ellipse, rect1, rect2]);
|
||||
API.setSelectedElements([ellipse], "B");
|
||||
|
||||
act(() => {
|
||||
h.app.actionManager.executeAction(actionDuplicateSelection);
|
||||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ id: ellipse.id },
|
||||
{ [ORIG_ID]: ellipse.id, groupIds: ["B"], selected: true },
|
||||
{ id: rect1.id, groupIds: ["A", "B"] },
|
||||
{ id: rect2.id, groupIds: ["A", "B"] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,13 +5,7 @@ import { duplicateElement, getNonDeletedElements } from "../element";
|
||||
import { isSomeElementSelected } from "../scene";
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
import { t } from "../i18n";
|
||||
import {
|
||||
arrayToMap,
|
||||
castArray,
|
||||
findLastIndex,
|
||||
getShortcutKey,
|
||||
invariant,
|
||||
} from "../utils";
|
||||
import { arrayToMap, getShortcutKey } from "../utils";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import {
|
||||
selectGroupsForSelectedElements,
|
||||
@@ -25,13 +19,8 @@ import { DEFAULT_GRID_SIZE } from "../constants";
|
||||
import {
|
||||
bindTextToShapeAfterDuplication,
|
||||
getBoundTextElement,
|
||||
getContainerElement,
|
||||
} from "../element/textElement";
|
||||
import {
|
||||
hasBoundTextElement,
|
||||
isBoundToContainer,
|
||||
isFrameLikeElement,
|
||||
} from "../element/typeChecks";
|
||||
import { isBoundToContainer, isFrameLikeElement } from "../element/typeChecks";
|
||||
import { normalizeElementOrder } from "../element/sortElements";
|
||||
import { DuplicateIcon } from "../components/icons";
|
||||
import {
|
||||
@@ -42,6 +31,7 @@ import {
|
||||
excludeElementsInFramesFromSelection,
|
||||
getSelectedElements,
|
||||
} from "../scene/selection";
|
||||
import { syncMovedIndices } from "../fractionalIndex";
|
||||
import { StoreAction } from "../store";
|
||||
|
||||
export const actionDuplicateSelection = register({
|
||||
@@ -69,20 +59,8 @@ export const actionDuplicateSelection = register({
|
||||
}
|
||||
}
|
||||
|
||||
const nextState = duplicateElements(elements, appState);
|
||||
|
||||
if (app.props.onDuplicate && nextState.elements) {
|
||||
const mappedElements = app.props.onDuplicate(
|
||||
nextState.elements,
|
||||
elements,
|
||||
);
|
||||
if (mappedElements) {
|
||||
nextState.elements = mappedElements;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...nextState,
|
||||
...duplicateElements(elements, appState),
|
||||
storeAction: StoreAction.CAPTURE,
|
||||
};
|
||||
},
|
||||
@@ -104,69 +82,37 @@ export const actionDuplicateSelection = register({
|
||||
const duplicateElements = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
): Partial<Exclude<ActionResult, false>> => {
|
||||
): Partial<ActionResult> => {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// step (1)
|
||||
|
||||
const sortedElements = normalizeElementOrder(elements);
|
||||
const groupIdMap = new Map();
|
||||
const newElements: ExcalidrawElement[] = [];
|
||||
const oldElements: ExcalidrawElement[] = [];
|
||||
const oldIdToDuplicatedId = new Map();
|
||||
const duplicatedElementsMap = new Map<string, ExcalidrawElement>();
|
||||
|
||||
const elementsMap = arrayToMap(elements);
|
||||
|
||||
const duplicateAndOffsetElement = <
|
||||
T extends ExcalidrawElement | ExcalidrawElement[],
|
||||
>(
|
||||
element: T,
|
||||
): T extends ExcalidrawElement[]
|
||||
? ExcalidrawElement[]
|
||||
: ExcalidrawElement | null => {
|
||||
const elements = castArray(element);
|
||||
|
||||
const _newElements = elements.reduce(
|
||||
(acc: ExcalidrawElement[], element) => {
|
||||
if (processedIds.has(element.id)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
processedIds.set(element.id, true);
|
||||
|
||||
const newElement = duplicateElement(
|
||||
appState.editingGroupId,
|
||||
groupIdMap,
|
||||
element,
|
||||
{
|
||||
x: element.x + DEFAULT_GRID_SIZE / 2,
|
||||
y: element.y + DEFAULT_GRID_SIZE / 2,
|
||||
},
|
||||
);
|
||||
|
||||
processedIds.set(newElement.id, true);
|
||||
|
||||
duplicatedElementsMap.set(newElement.id, newElement);
|
||||
oldIdToDuplicatedId.set(element.id, newElement.id);
|
||||
|
||||
oldElements.push(element);
|
||||
newElements.push(newElement);
|
||||
|
||||
acc.push(newElement);
|
||||
return acc;
|
||||
const duplicateAndOffsetElement = (element: ExcalidrawElement) => {
|
||||
const newElement = duplicateElement(
|
||||
appState.editingGroupId,
|
||||
groupIdMap,
|
||||
element,
|
||||
{
|
||||
x: element.x + DEFAULT_GRID_SIZE / 2,
|
||||
y: element.y + DEFAULT_GRID_SIZE / 2,
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
Array.isArray(element) ? _newElements : _newElements[0] || null
|
||||
) as T extends ExcalidrawElement[]
|
||||
? ExcalidrawElement[]
|
||||
: ExcalidrawElement | null;
|
||||
duplicatedElementsMap.set(newElement.id, newElement);
|
||||
oldIdToDuplicatedId.set(element.id, newElement.id);
|
||||
oldElements.push(element);
|
||||
newElements.push(newElement);
|
||||
return newElement;
|
||||
};
|
||||
|
||||
elements = normalizeElementOrder(elements);
|
||||
|
||||
const idsOfElementsToDuplicate = arrayToMap(
|
||||
getSelectedElements(elements, appState, {
|
||||
getSelectedElements(sortedElements, appState, {
|
||||
includeBoundTextElement: true,
|
||||
includeElementsInFrames: true,
|
||||
}),
|
||||
@@ -184,134 +130,123 @@ const duplicateElements = (
|
||||
// loop over them.
|
||||
const processedIds = new Map<ExcalidrawElement["id"], true>();
|
||||
|
||||
const elementsWithClones: ExcalidrawElement[] = elements.slice();
|
||||
|
||||
const insertAfterIndex = (
|
||||
index: number,
|
||||
elements: ExcalidrawElement | null | ExcalidrawElement[],
|
||||
) => {
|
||||
invariant(index !== -1, "targetIndex === -1 ");
|
||||
|
||||
if (!Array.isArray(elements) && !elements) {
|
||||
return;
|
||||
const markAsProcessed = (elements: ExcalidrawElement[]) => {
|
||||
for (const element of elements) {
|
||||
processedIds.set(element.id, true);
|
||||
}
|
||||
|
||||
elementsWithClones.splice(index + 1, 0, ...castArray(elements));
|
||||
return elements;
|
||||
};
|
||||
|
||||
const frameIdsToDuplicate = new Set(
|
||||
elements
|
||||
.filter(
|
||||
(el) => idsOfElementsToDuplicate.has(el.id) && isFrameLikeElement(el),
|
||||
)
|
||||
.map((el) => el.id),
|
||||
);
|
||||
const elementsWithClones: ExcalidrawElement[] = [];
|
||||
|
||||
for (const element of elements) {
|
||||
if (processedIds.has(element.id)) {
|
||||
let index = -1;
|
||||
|
||||
while (++index < sortedElements.length) {
|
||||
const element = sortedElements[index];
|
||||
|
||||
if (processedIds.get(element.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!idsOfElementsToDuplicate.has(element.id)) {
|
||||
continue;
|
||||
}
|
||||
const boundTextElement = getBoundTextElement(element, arrayToMap(elements));
|
||||
const isElementAFrameLike = isFrameLikeElement(element);
|
||||
|
||||
// groups
|
||||
// -------------------------------------------------------------------------
|
||||
if (idsOfElementsToDuplicate.get(element.id)) {
|
||||
// if a group or a container/bound-text or frame, duplicate atomically
|
||||
if (element.groupIds.length || boundTextElement || isElementAFrameLike) {
|
||||
const groupId = getSelectedGroupForElement(appState, element);
|
||||
if (groupId) {
|
||||
// TODO:
|
||||
// remove `.flatMap...`
|
||||
// if the elements in a frame are grouped when the frame is grouped
|
||||
const groupElements = getElementsInGroup(
|
||||
sortedElements,
|
||||
groupId,
|
||||
).flatMap((element) =>
|
||||
isFrameLikeElement(element)
|
||||
? [...getFrameChildren(elements, element.id), element]
|
||||
: [element],
|
||||
);
|
||||
|
||||
const groupId = getSelectedGroupForElement(appState, element);
|
||||
if (groupId) {
|
||||
const groupElements = getElementsInGroup(elements, groupId).flatMap(
|
||||
(element) =>
|
||||
isFrameLikeElement(element)
|
||||
? [...getFrameChildren(elements, element.id), element]
|
||||
: [element],
|
||||
);
|
||||
elementsWithClones.push(
|
||||
...markAsProcessed([
|
||||
...groupElements,
|
||||
...groupElements.map((element) =>
|
||||
duplicateAndOffsetElement(element),
|
||||
),
|
||||
]),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (boundTextElement) {
|
||||
elementsWithClones.push(
|
||||
...markAsProcessed([
|
||||
element,
|
||||
boundTextElement,
|
||||
duplicateAndOffsetElement(element),
|
||||
duplicateAndOffsetElement(boundTextElement),
|
||||
]),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (isElementAFrameLike) {
|
||||
const elementsInFrame = getFrameChildren(sortedElements, element.id);
|
||||
|
||||
const targetIndex = findLastIndex(elementsWithClones, (el) => {
|
||||
return el.groupIds?.includes(groupId);
|
||||
});
|
||||
elementsWithClones.push(
|
||||
...markAsProcessed([
|
||||
...elementsInFrame,
|
||||
element,
|
||||
...elementsInFrame.map((e) => duplicateAndOffsetElement(e)),
|
||||
duplicateAndOffsetElement(element),
|
||||
]),
|
||||
);
|
||||
|
||||
insertAfterIndex(targetIndex, duplicateAndOffsetElement(groupElements));
|
||||
continue;
|
||||
}
|
||||
|
||||
// frame duplication
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
if (element.frameId && frameIdsToDuplicate.has(element.frameId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isFrameLikeElement(element)) {
|
||||
const frameId = element.id;
|
||||
|
||||
const frameChildren = getFrameChildren(elements, frameId);
|
||||
|
||||
const targetIndex = findLastIndex(elementsWithClones, (el) => {
|
||||
return el.frameId === frameId || el.id === frameId;
|
||||
});
|
||||
|
||||
insertAfterIndex(
|
||||
targetIndex,
|
||||
duplicateAndOffsetElement([...frameChildren, element]),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// text container
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
if (hasBoundTextElement(element)) {
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
|
||||
const targetIndex = findLastIndex(elementsWithClones, (el) => {
|
||||
return (
|
||||
el.id === element.id ||
|
||||
("containerId" in el && el.containerId === element.id)
|
||||
);
|
||||
});
|
||||
|
||||
if (boundTextElement) {
|
||||
insertAfterIndex(
|
||||
targetIndex,
|
||||
duplicateAndOffsetElement([element, boundTextElement]),
|
||||
);
|
||||
} else {
|
||||
insertAfterIndex(targetIndex, duplicateAndOffsetElement(element));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isBoundToContainer(element)) {
|
||||
const container = getContainerElement(element, elementsMap);
|
||||
|
||||
const targetIndex = findLastIndex(elementsWithClones, (el) => {
|
||||
return el.id === element.id || el.id === container?.id;
|
||||
});
|
||||
|
||||
if (container) {
|
||||
insertAfterIndex(
|
||||
targetIndex,
|
||||
duplicateAndOffsetElement([container, element]),
|
||||
// since elements in frames have a lower z-index than the frame itself,
|
||||
// they will be looped first and if their frames are selected as well,
|
||||
// they will have been copied along with the frame atomically in the
|
||||
// above branch, so we must skip those elements here
|
||||
//
|
||||
// now, for elements do not belong any frames or elements whose frames
|
||||
// are selected (or elements that are left out from the above
|
||||
// steps for whatever reason) we (should at least) duplicate them here
|
||||
if (!element.frameId || !idsOfElementsToDuplicate.has(element.frameId)) {
|
||||
elementsWithClones.push(
|
||||
...markAsProcessed([element, duplicateAndOffsetElement(element)]),
|
||||
);
|
||||
} else {
|
||||
insertAfterIndex(targetIndex, duplicateAndOffsetElement(element));
|
||||
}
|
||||
|
||||
continue;
|
||||
} else {
|
||||
elementsWithClones.push(...markAsProcessed([element]));
|
||||
}
|
||||
|
||||
// default duplication (regular elements)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
insertAfterIndex(
|
||||
findLastIndex(elementsWithClones, (el) => el.id === element.id),
|
||||
duplicateAndOffsetElement(element),
|
||||
);
|
||||
}
|
||||
|
||||
// step (2)
|
||||
|
||||
// second pass to remove duplicates. We loop from the end as it's likelier
|
||||
// that the last elements are in the correct order (contiguous or otherwise).
|
||||
// Thus we need to reverse as the last step (3).
|
||||
|
||||
const finalElementsReversed: ExcalidrawElement[] = [];
|
||||
|
||||
const finalElementIds = new Map<ExcalidrawElement["id"], true>();
|
||||
index = elementsWithClones.length;
|
||||
|
||||
while (--index >= 0) {
|
||||
const element = elementsWithClones[index];
|
||||
if (!finalElementIds.get(element.id)) {
|
||||
finalElementIds.set(element.id, true);
|
||||
finalElementsReversed.push(element);
|
||||
}
|
||||
}
|
||||
|
||||
// step (3)
|
||||
const finalElements = syncMovedIndices(
|
||||
finalElementsReversed.reverse(),
|
||||
arrayToMap(newElements),
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bindTextToShapeAfterDuplication(
|
||||
@@ -325,7 +260,7 @@ const duplicateElements = (
|
||||
oldIdToDuplicatedId,
|
||||
);
|
||||
bindElementsToFramesAfterDuplication(
|
||||
elementsWithClones,
|
||||
finalElements,
|
||||
oldElements,
|
||||
oldIdToDuplicatedId,
|
||||
);
|
||||
@@ -334,7 +269,7 @@ const duplicateElements = (
|
||||
excludeElementsInFramesFromSelection(newElements);
|
||||
|
||||
return {
|
||||
elements: elementsWithClones,
|
||||
elements: finalElements,
|
||||
appState: {
|
||||
...appState,
|
||||
...selectGroupsForSelectedElements(
|
||||
@@ -350,7 +285,7 @@ const duplicateElements = (
|
||||
{},
|
||||
),
|
||||
},
|
||||
getNonDeletedElements(elementsWithClones),
|
||||
getNonDeletedElements(finalElements),
|
||||
appState,
|
||||
null,
|
||||
),
|
||||
|
||||
@@ -1605,8 +1605,6 @@ export const actionChangeArrowType = register({
|
||||
elements,
|
||||
elementsMap,
|
||||
appState.zoom,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
const endHoveredElement =
|
||||
!newElement.endBinding &&
|
||||
@@ -1615,8 +1613,6 @@ export const actionChangeArrowType = register({
|
||||
elements,
|
||||
elementsMap,
|
||||
appState.zoom,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
const startElement = startHoveredElement
|
||||
? startHoveredElement
|
||||
|
||||
@@ -1522,17 +1522,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const allElementsMap = this.scene.getNonDeletedElementsMap();
|
||||
|
||||
const shouldBlockPointerEvents =
|
||||
// default back to `--ui-pointerEvents` flow if setPointerCapture
|
||||
// not supported
|
||||
"setPointerCapture" in HTMLElement.prototype
|
||||
? false
|
||||
: this.state.selectionElement ||
|
||||
this.state.newElement ||
|
||||
this.state.selectedElementsAreBeingDragged ||
|
||||
this.state.resizingElement ||
|
||||
(this.state.activeTool.type === "laser" &&
|
||||
// technically we can just test on this once we make it more safe
|
||||
this.state.cursorButton === "down");
|
||||
this.state.selectionElement ||
|
||||
this.state.newElement ||
|
||||
this.state.selectedElementsAreBeingDragged ||
|
||||
this.state.resizingElement ||
|
||||
(this.state.activeTool.type === "laser" &&
|
||||
// technically we can just test on this once we make it more safe
|
||||
this.state.cursorButton === "down");
|
||||
|
||||
const firstSelectedElement = selectedElements[0];
|
||||
|
||||
@@ -3228,14 +3224,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
);
|
||||
|
||||
const prevElements = this.scene.getElementsIncludingDeleted();
|
||||
let nextElements = [...prevElements, ...newElements];
|
||||
|
||||
const mappedNewSceneElements = this.props.onDuplicate?.(
|
||||
nextElements,
|
||||
prevElements,
|
||||
);
|
||||
|
||||
nextElements = mappedNewSceneElements || nextElements;
|
||||
const nextElements = [...prevElements, ...newElements];
|
||||
|
||||
syncMovedIndices(nextElements, arrayToMap(newElements));
|
||||
|
||||
@@ -5781,10 +5770,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
}
|
||||
if (editingLinearElement?.lastUncommittedPoint != null) {
|
||||
this.maybeSuggestBindingAtCursor(
|
||||
scenePointer,
|
||||
editingLinearElement.elbowed,
|
||||
);
|
||||
this.maybeSuggestBindingAtCursor(scenePointer);
|
||||
} else {
|
||||
// causes stack overflow if not sync
|
||||
flushSync(() => {
|
||||
@@ -5804,7 +5790,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.state.startBoundElement,
|
||||
);
|
||||
} else {
|
||||
this.maybeSuggestBindingAtCursor(scenePointer, false);
|
||||
this.maybeSuggestBindingAtCursor(scenePointer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6306,13 +6292,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
private handleCanvasPointerDown = (
|
||||
event: React.PointerEvent<HTMLElement>,
|
||||
) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// capture subsequent pointer events to the canvas
|
||||
// this makes other elements non-interactive until pointer up
|
||||
if (target.setPointerCapture) {
|
||||
target.setPointerCapture(event.pointerId);
|
||||
}
|
||||
|
||||
this.maybeCleanupAfterMissingPointerUp(event.nativeEvent);
|
||||
this.maybeUnfollowRemoteUser();
|
||||
|
||||
@@ -7749,7 +7728,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.state.zoom,
|
||||
isElbowArrow(element),
|
||||
isElbowArrow(element),
|
||||
);
|
||||
|
||||
this.scene.insertElement(element);
|
||||
@@ -8449,17 +8427,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
}
|
||||
|
||||
let nextSceneElements: ExcalidrawElement[] = [
|
||||
...nextElements,
|
||||
...elementsToAppend,
|
||||
];
|
||||
|
||||
const mappedNewSceneElements = this.props.onDuplicate?.(
|
||||
nextSceneElements,
|
||||
elements,
|
||||
);
|
||||
|
||||
nextSceneElements = mappedNewSceneElements || nextSceneElements;
|
||||
const nextSceneElements = [...nextElements, ...elementsToAppend];
|
||||
|
||||
syncMovedIndices(nextSceneElements, arrayToMap(elementsToAppend));
|
||||
|
||||
@@ -10037,20 +10005,15 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
};
|
||||
|
||||
private maybeSuggestBindingAtCursor = (
|
||||
pointerCoords: {
|
||||
x: number;
|
||||
y: number;
|
||||
},
|
||||
considerAll: boolean,
|
||||
): void => {
|
||||
private maybeSuggestBindingAtCursor = (pointerCoords: {
|
||||
x: number;
|
||||
y: number;
|
||||
}): void => {
|
||||
const hoveredBindableElement = getHoveredElementForBinding(
|
||||
pointerCoords,
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.state.zoom,
|
||||
false,
|
||||
considerAll,
|
||||
);
|
||||
this.setState({
|
||||
suggestedBindings:
|
||||
@@ -10080,8 +10043,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.state.zoom,
|
||||
isElbowArrow(linearElement),
|
||||
isElbowArrow(linearElement),
|
||||
isArrowElement(linearElement) && isElbowArrow(linearElement),
|
||||
);
|
||||
if (
|
||||
hoveredBindableElement != null &&
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getLanguage, t } from "../i18n";
|
||||
import clsx from "clsx";
|
||||
import Collapsible from "./Stats/Collapsible";
|
||||
import { atom, useAtom } from "../editor-jotai";
|
||||
import { useDevice } from "./App";
|
||||
import { useDevice } from "..";
|
||||
|
||||
import "./IconPicker.scss";
|
||||
|
||||
|
||||
@@ -2,9 +2,8 @@ import React, {
|
||||
useState,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useEffect,
|
||||
memo,
|
||||
useRef,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import type Library from "../data/library";
|
||||
import {
|
||||
@@ -18,7 +17,6 @@ import type {
|
||||
LibraryItem,
|
||||
ExcalidrawProps,
|
||||
UIAppState,
|
||||
AppClassProperties,
|
||||
} from "../types";
|
||||
import LibraryMenuItems from "./LibraryMenuItems";
|
||||
import { trackEvent } from "../analytics";
|
||||
@@ -35,12 +33,9 @@ import { useUIAppState } from "../context/ui-appState";
|
||||
|
||||
import "./LibraryMenu.scss";
|
||||
import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "../element/types";
|
||||
import { LIBRARY_DISABLED_TYPES } from "../constants";
|
||||
import { isShallowEqual } from "../utils";
|
||||
import type { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { LIBRARY_DISABLED_TYPES } from "../constants";
|
||||
|
||||
export const isLibraryMenuOpenAtom = atom(false);
|
||||
|
||||
@@ -48,215 +43,170 @@ const LibraryMenuWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return <div className="layer-ui__library">{children}</div>;
|
||||
};
|
||||
|
||||
const LibraryMenuContent = memo(
|
||||
({
|
||||
onInsertLibraryItems,
|
||||
pendingElements,
|
||||
onAddToLibrary,
|
||||
setAppState,
|
||||
libraryReturnUrl,
|
||||
library,
|
||||
id,
|
||||
theme,
|
||||
selectedItems,
|
||||
onSelectItems,
|
||||
}: {
|
||||
pendingElements: LibraryItem["elements"];
|
||||
onInsertLibraryItems: (libraryItems: LibraryItems) => void;
|
||||
onAddToLibrary: () => void;
|
||||
setAppState: React.Component<any, UIAppState>["setState"];
|
||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||
library: Library;
|
||||
id: string;
|
||||
theme: UIAppState["theme"];
|
||||
selectedItems: LibraryItem["id"][];
|
||||
onSelectItems: (id: LibraryItem["id"][]) => void;
|
||||
}) => {
|
||||
const [libraryItemsData] = useAtom(libraryItemsAtom);
|
||||
export const LibraryMenuContent = ({
|
||||
onInsertLibraryItems,
|
||||
pendingElements,
|
||||
onAddToLibrary,
|
||||
setAppState,
|
||||
libraryReturnUrl,
|
||||
library,
|
||||
id,
|
||||
theme,
|
||||
selectedItems,
|
||||
onSelectItems,
|
||||
}: {
|
||||
pendingElements: LibraryItem["elements"];
|
||||
onInsertLibraryItems: (libraryItems: LibraryItems) => void;
|
||||
onAddToLibrary: () => void;
|
||||
setAppState: React.Component<any, UIAppState>["setState"];
|
||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||
library: Library;
|
||||
id: string;
|
||||
theme: UIAppState["theme"];
|
||||
selectedItems: LibraryItem["id"][];
|
||||
onSelectItems: (id: LibraryItem["id"][]) => void;
|
||||
}) => {
|
||||
const [libraryItemsData] = useAtom(libraryItemsAtom);
|
||||
|
||||
const _onAddToLibrary = useCallback(
|
||||
(elements: LibraryItem["elements"]) => {
|
||||
const addToLibrary = async (
|
||||
processedElements: LibraryItem["elements"],
|
||||
libraryItems: LibraryItems,
|
||||
) => {
|
||||
trackEvent("element", "addToLibrary", "ui");
|
||||
for (const type of LIBRARY_DISABLED_TYPES) {
|
||||
if (processedElements.some((element) => element.type === type)) {
|
||||
return setAppState({
|
||||
errorMessage: t(`errors.libraryElementTypeError.${type}`),
|
||||
});
|
||||
}
|
||||
const _onAddToLibrary = useCallback(
|
||||
(elements: LibraryItem["elements"]) => {
|
||||
const addToLibrary = async (
|
||||
processedElements: LibraryItem["elements"],
|
||||
libraryItems: LibraryItems,
|
||||
) => {
|
||||
trackEvent("element", "addToLibrary", "ui");
|
||||
for (const type of LIBRARY_DISABLED_TYPES) {
|
||||
if (processedElements.some((element) => element.type === type)) {
|
||||
return setAppState({
|
||||
errorMessage: t(`errors.libraryElementTypeError.${type}`),
|
||||
});
|
||||
}
|
||||
const nextItems: LibraryItems = [
|
||||
{
|
||||
status: "unpublished",
|
||||
elements: processedElements,
|
||||
id: randomId(),
|
||||
created: Date.now(),
|
||||
},
|
||||
...libraryItems,
|
||||
];
|
||||
onAddToLibrary();
|
||||
library.setLibrary(nextItems).catch(() => {
|
||||
setAppState({ errorMessage: t("alerts.errorAddingToLibrary") });
|
||||
});
|
||||
};
|
||||
addToLibrary(elements, libraryItemsData.libraryItems);
|
||||
},
|
||||
[onAddToLibrary, library, setAppState, libraryItemsData.libraryItems],
|
||||
);
|
||||
}
|
||||
const nextItems: LibraryItems = [
|
||||
{
|
||||
status: "unpublished",
|
||||
elements: processedElements,
|
||||
id: randomId(),
|
||||
created: Date.now(),
|
||||
},
|
||||
...libraryItems,
|
||||
];
|
||||
onAddToLibrary();
|
||||
library.setLibrary(nextItems).catch(() => {
|
||||
setAppState({ errorMessage: t("alerts.errorAddingToLibrary") });
|
||||
});
|
||||
};
|
||||
addToLibrary(elements, libraryItemsData.libraryItems);
|
||||
},
|
||||
[onAddToLibrary, library, setAppState, libraryItemsData.libraryItems],
|
||||
);
|
||||
|
||||
const libraryItems = useMemo(
|
||||
() => libraryItemsData.libraryItems,
|
||||
[libraryItemsData],
|
||||
);
|
||||
|
||||
if (
|
||||
libraryItemsData.status === "loading" &&
|
||||
!libraryItemsData.isInitialized
|
||||
) {
|
||||
return (
|
||||
<LibraryMenuWrapper>
|
||||
<div className="layer-ui__library-message">
|
||||
<div>
|
||||
<Spinner size="2em" />
|
||||
<span>{t("labels.libraryLoadingMessage")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</LibraryMenuWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const showBtn =
|
||||
libraryItemsData.libraryItems.length > 0 || pendingElements.length > 0;
|
||||
const libraryItems = useMemo(
|
||||
() => libraryItemsData.libraryItems,
|
||||
[libraryItemsData],
|
||||
);
|
||||
|
||||
if (
|
||||
libraryItemsData.status === "loading" &&
|
||||
!libraryItemsData.isInitialized
|
||||
) {
|
||||
return (
|
||||
<LibraryMenuWrapper>
|
||||
<LibraryMenuItems
|
||||
isLoading={libraryItemsData.status === "loading"}
|
||||
libraryItems={libraryItems}
|
||||
onAddToLibrary={_onAddToLibrary}
|
||||
onInsertLibraryItems={onInsertLibraryItems}
|
||||
pendingElements={pendingElements}
|
||||
<div className="layer-ui__library-message">
|
||||
<div>
|
||||
<Spinner size="2em" />
|
||||
<span>{t("labels.libraryLoadingMessage")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</LibraryMenuWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const showBtn =
|
||||
libraryItemsData.libraryItems.length > 0 || pendingElements.length > 0;
|
||||
|
||||
return (
|
||||
<LibraryMenuWrapper>
|
||||
<LibraryMenuItems
|
||||
isLoading={libraryItemsData.status === "loading"}
|
||||
libraryItems={libraryItems}
|
||||
onAddToLibrary={_onAddToLibrary}
|
||||
onInsertLibraryItems={onInsertLibraryItems}
|
||||
pendingElements={pendingElements}
|
||||
id={id}
|
||||
libraryReturnUrl={libraryReturnUrl}
|
||||
theme={theme}
|
||||
onSelectItems={onSelectItems}
|
||||
selectedItems={selectedItems}
|
||||
/>
|
||||
{showBtn && (
|
||||
<LibraryMenuControlButtons
|
||||
className="library-menu-control-buttons--at-bottom"
|
||||
style={{ padding: "16px 12px 0 12px" }}
|
||||
id={id}
|
||||
libraryReturnUrl={libraryReturnUrl}
|
||||
theme={theme}
|
||||
onSelectItems={onSelectItems}
|
||||
selectedItems={selectedItems}
|
||||
/>
|
||||
{showBtn && (
|
||||
<LibraryMenuControlButtons
|
||||
className="library-menu-control-buttons--at-bottom"
|
||||
style={{ padding: "16px 12px 0 12px" }}
|
||||
id={id}
|
||||
libraryReturnUrl={libraryReturnUrl}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
</LibraryMenuWrapper>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const getPendingElements = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
selectedElementIds: UIAppState["selectedElementIds"],
|
||||
) => ({
|
||||
elements,
|
||||
pending: getSelectedElements(
|
||||
elements,
|
||||
{ selectedElementIds },
|
||||
{
|
||||
includeBoundTextElement: true,
|
||||
includeElementsInFrames: true,
|
||||
},
|
||||
),
|
||||
selectedElementIds,
|
||||
});
|
||||
)}
|
||||
</LibraryMenuWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const usePendingElementsMemo = (
|
||||
appState: UIAppState,
|
||||
app: AppClassProperties,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
) => {
|
||||
const elements = useExcalidrawElements();
|
||||
const [state, setState] = useState(() =>
|
||||
getPendingElements(elements, appState.selectedElementIds),
|
||||
const create = useCallback(
|
||||
(appState: UIAppState, elements: readonly NonDeletedExcalidrawElement[]) =>
|
||||
getSelectedElements(elements, appState, {
|
||||
includeBoundTextElement: true,
|
||||
includeElementsInFrames: true,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const selectedElementVersions = useRef(
|
||||
new Map<ExcalidrawElement["id"], ExcalidrawElement["version"]>(),
|
||||
);
|
||||
const val = useRef(create(appState, elements));
|
||||
const prevAppState = useRef<UIAppState>(appState);
|
||||
const prevElements = useRef(elements);
|
||||
|
||||
useEffect(() => {
|
||||
for (const element of state.pending) {
|
||||
selectedElementVersions.current.set(element.id, element.version);
|
||||
}
|
||||
}, [state.pending]);
|
||||
|
||||
useEffect(() => {
|
||||
const update = useCallback(() => {
|
||||
if (
|
||||
// Only update once pointer is released.
|
||||
// Reading directly from app.state to make it clear it's not reactive
|
||||
// (hence, there's potential for stale state)
|
||||
app.state.cursorButton === "up" &&
|
||||
app.state.activeTool.type === "selection"
|
||||
!isShallowEqual(
|
||||
appState.selectedElementIds,
|
||||
prevAppState.current.selectedElementIds,
|
||||
) ||
|
||||
!isShallowEqual(elements, prevElements.current)
|
||||
) {
|
||||
setState((prev) => {
|
||||
// if selectedElementIds changed, we don't have to compare versions
|
||||
// ---------------------------------------------------------------------
|
||||
if (
|
||||
!isShallowEqual(prev.selectedElementIds, appState.selectedElementIds)
|
||||
) {
|
||||
selectedElementVersions.current.clear();
|
||||
return getPendingElements(elements, appState.selectedElementIds);
|
||||
}
|
||||
// otherwise we need to check whether selected elements changed
|
||||
// ---------------------------------------------------------------------
|
||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||
for (const id of Object.keys(appState.selectedElementIds)) {
|
||||
const currVersion = elementsMap.get(id)?.version;
|
||||
if (
|
||||
currVersion &&
|
||||
currVersion !== selectedElementVersions.current.get(id)
|
||||
) {
|
||||
// we can't update the selectedElementVersions in here
|
||||
// because of double render in StrictMode which would overwrite
|
||||
// the state in the second pass with the old `prev` state.
|
||||
// Thus, we update versions in a separate effect. May create
|
||||
// a race condition since current effect is not fully reactive.
|
||||
return getPendingElements(elements, appState.selectedElementIds);
|
||||
}
|
||||
}
|
||||
// nothing changed
|
||||
// ---------------------------------------------------------------------
|
||||
return prev;
|
||||
});
|
||||
val.current = create(appState, elements);
|
||||
prevAppState.current = appState;
|
||||
prevElements.current = elements;
|
||||
}
|
||||
}, [
|
||||
app,
|
||||
app.state.cursorButton,
|
||||
app.state.activeTool.type,
|
||||
appState.selectedElementIds,
|
||||
elements,
|
||||
]);
|
||||
}, [create, appState, elements]);
|
||||
|
||||
return state.pending;
|
||||
return useMemo(
|
||||
() => ({
|
||||
update,
|
||||
value: val.current,
|
||||
}),
|
||||
[update, val],
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This component is meant to be rendered inside <Sidebar.Tab/> inside our
|
||||
* <DefaultSidebar/> or host apps Sidebar components.
|
||||
*/
|
||||
export const LibraryMenu = memo(() => {
|
||||
const app = useApp();
|
||||
const { onInsertElements } = app;
|
||||
export const LibraryMenu = () => {
|
||||
const { library, id, onInsertElements } = useApp();
|
||||
const appProps = useAppProps();
|
||||
const appState = useUIAppState();
|
||||
const app = useApp();
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
const elements = useExcalidrawElements();
|
||||
const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]);
|
||||
const memoizedLibrary = useMemo(() => app.library, [app.library]);
|
||||
const pendingElements = usePendingElementsMemo(appState, app);
|
||||
const memoizedLibrary = useMemo(() => library, [library]);
|
||||
// BUG: pendingElements are still causing some unnecessary rerenders because clicking into canvas returns some ids even when no element is selected.
|
||||
const pendingElements = usePendingElementsMemo(appState, elements);
|
||||
|
||||
const onInsertLibraryItems = useCallback(
|
||||
(libraryItems: LibraryItems) => {
|
||||
@@ -273,18 +223,22 @@ export const LibraryMenu = memo(() => {
|
||||
});
|
||||
}, [setAppState]);
|
||||
|
||||
useEffect(() => {
|
||||
return app.onPointerUpEmitter.on(() => pendingElements.update());
|
||||
}, [app, pendingElements]);
|
||||
|
||||
return (
|
||||
<LibraryMenuContent
|
||||
pendingElements={pendingElements}
|
||||
pendingElements={pendingElements.value}
|
||||
onInsertLibraryItems={onInsertLibraryItems}
|
||||
onAddToLibrary={deselectItems}
|
||||
setAppState={setAppState}
|
||||
libraryReturnUrl={appProps.libraryReturnUrl}
|
||||
library={memoizedLibrary}
|
||||
id={app.id}
|
||||
id={id}
|
||||
theme={appState.theme}
|
||||
selectedItems={selectedItems}
|
||||
onSelectItems={setSelectedItems}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -63,27 +63,6 @@
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.library-unit__checkbox-container,
|
||||
.library-unit__checkbox-container:hover,
|
||||
.library-unit__checkbox-container:active {
|
||||
align-items: center;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--icon-fill-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding: 0.5rem;
|
||||
position: absolute;
|
||||
left: 2rem;
|
||||
bottom: 2rem;
|
||||
cursor: pointer;
|
||||
|
||||
input {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.library-unit__checkbox {
|
||||
position: absolute;
|
||||
top: 0.125rem;
|
||||
@@ -91,7 +70,7 @@
|
||||
margin: 0;
|
||||
|
||||
.Checkbox-box {
|
||||
margin: 0;
|
||||
margin: 0 !important;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
@import "../css/variables.module.scss";
|
||||
|
||||
.excalidraw {
|
||||
--slider-thumb-size: 16px;
|
||||
--Range-track-background: var(--button-bg);
|
||||
--Range-track-background-active: var(--color-primary);
|
||||
--Range-thumb-background: var(--color-on-surface);
|
||||
--Range-legend-color: var(--text-primary-color);
|
||||
|
||||
.range-wrapper {
|
||||
position: relative;
|
||||
@@ -13,7 +16,7 @@
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
-webkit-appearance: none;
|
||||
background: var(--color-slider-track);
|
||||
background: var(--Range-track-background);
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
}
|
||||
@@ -21,18 +24,18 @@
|
||||
.range-input::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: var(--slider-thumb-size);
|
||||
height: var(--slider-thumb-size);
|
||||
background: var(--color-slider-thumb);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: var(--Range-thumb-background);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.range-input::-moz-range-thumb {
|
||||
width: var(--slider-thumb-size);
|
||||
height: var(--slider-thumb-size);
|
||||
background: var(--color-slider-thumb);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: var(--Range-thumb-background);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
@@ -43,7 +46,7 @@
|
||||
bottom: 0;
|
||||
transform: translateX(-50%);
|
||||
font-size: 12px;
|
||||
color: var(--text-primary-color);
|
||||
color: var(--Range-legend-color);
|
||||
}
|
||||
|
||||
.zero-label {
|
||||
@@ -51,6 +54,6 @@
|
||||
bottom: 0;
|
||||
left: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--text-primary-color);
|
||||
color: var(--Range-legend-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export const Range = ({
|
||||
const position =
|
||||
(value / 100) * (inputWidth - thumbWidth) + thumbWidth / 2;
|
||||
valueElement.style.left = `${position}px`;
|
||||
rangeElement.style.background = `linear-gradient(to right, var(--color-slider-track) 0%, var(--color-slider-track) ${value}%, var(--button-bg) ${value}%, var(--button-bg) 100%)`;
|
||||
rangeElement.style.background = `linear-gradient(to right, var(--color-primary) 0%, var(--color-primary) ${value}%, var(--button-bg) ${value}%, var(--button-bg) 100%)`;
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
|
||||
@@ -1216,12 +1216,11 @@ export const EdgeRoundIcon = createIcon(
|
||||
);
|
||||
|
||||
export const ArrowheadNoneIcon = createIcon(
|
||||
<g stroke="currentColor" opacity={0.3} strokeWidth={2}>
|
||||
<path d="M12 12l9 0" />
|
||||
<path d="M3 9l6 6" />
|
||||
<path d="M3 15l6 -6" />
|
||||
</g>,
|
||||
tablerIconProps,
|
||||
<path d="M6 10H34" stroke="currentColor" strokeWidth={2} fill="none" />,
|
||||
{
|
||||
width: 40,
|
||||
height: 20,
|
||||
},
|
||||
);
|
||||
|
||||
export const ArrowheadArrowIcon = React.memo(
|
||||
|
||||
@@ -458,6 +458,3 @@ export const ARROW_TYPE: { [T in AppState["currentItemArrowType"]]: T } = {
|
||||
|
||||
export const DEFAULT_REDUCED_GLOBAL_ALPHA = 0.3;
|
||||
export const ELEMENT_LINK_KEY = "element";
|
||||
|
||||
/** used in tests */
|
||||
export const ORIG_ID = Symbol.for("__test__originalId__");
|
||||
|
||||
@@ -649,21 +649,15 @@ body.excalidraw-cursor-resize * {
|
||||
@include filledButtonOnCanvas;
|
||||
}
|
||||
|
||||
.App-mobile-menu,
|
||||
.App-menu__left {
|
||||
--button-border: transparent;
|
||||
--button-bg: var(--color-surface-mid);
|
||||
}
|
||||
|
||||
@at-root .excalidraw.theme--dark#{&} {
|
||||
.App-mobile-menu,
|
||||
.App-menu__left {
|
||||
@at-root .excalidraw.theme--dark#{&} {
|
||||
--button-hover-bg: #363541;
|
||||
--button-bg: var(--color-surface-high);
|
||||
}
|
||||
}
|
||||
|
||||
.App-menu__left {
|
||||
.buttonList {
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
0px 0px 3.1270833015441895px 0px rgba(0, 0, 0, 0.08),
|
||||
0px 7px 14px 0px rgba(0, 0, 0, 0.05);
|
||||
|
||||
--button-bg: var(--color-surface-mid);
|
||||
--button-hover-bg: var(--color-surface-high);
|
||||
--button-active-bg: var(--color-surface-high);
|
||||
--button-active-border: var(--color-brand-active);
|
||||
@@ -53,9 +54,6 @@
|
||||
--scrollbar-thumb: var(--button-gray-2);
|
||||
--scrollbar-thumb-hover: var(--button-gray-3);
|
||||
|
||||
--color-slider-track: hsl(240, 100%, 90%);
|
||||
--color-slider-thumb: var(--color-gray-80);
|
||||
|
||||
--modal-shadow: 0px 100px 80px rgba(0, 0, 0, 0.07),
|
||||
0px 41.7776px 33.4221px rgba(0, 0, 0, 0.0503198),
|
||||
0px 22.3363px 17.869px rgba(0, 0, 0, 0.0417275),
|
||||
@@ -174,6 +172,8 @@
|
||||
--button-destructive-bg-color: #5a0000;
|
||||
--button-destructive-color: #{$oc-red-3};
|
||||
|
||||
--button-bg: var(--color-surface-high);
|
||||
|
||||
--button-gray-1: #363636;
|
||||
--button-gray-2: #272727;
|
||||
--button-gray-3: #222;
|
||||
@@ -210,8 +210,6 @@
|
||||
--scrollbar-thumb: #{$oc-gray-8};
|
||||
--scrollbar-thumb-hover: #{$oc-gray-7};
|
||||
|
||||
--color-slider-track: hsl(244, 23%, 39%);
|
||||
|
||||
// will be inverted to a lighter color.
|
||||
--color-selection: #3530c4;
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import type { Bounds } from "./bounds";
|
||||
import { getCenterForBounds, getElementAbsoluteCoords } from "./bounds";
|
||||
import type { AppState } from "../types";
|
||||
import { isPointOnShape } from "../../utils/collision";
|
||||
import { getElementAtPosition } from "../scene";
|
||||
import {
|
||||
isArrowElement,
|
||||
isBindableElement,
|
||||
@@ -48,11 +49,7 @@ import type { ElementUpdate } from "./mutateElement";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import type Scene from "../scene/Scene";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import {
|
||||
arrayToMap,
|
||||
isBindingFallthroughEnabled,
|
||||
tupleToCoors,
|
||||
} from "../utils";
|
||||
import { arrayToMap, tupleToCoors } from "../utils";
|
||||
import { KEYS } from "../keys";
|
||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||
import { aabbForElement, getElementShape, pointInsideBounds } from "../shapes";
|
||||
@@ -428,8 +425,7 @@ export const maybeBindLinearElement = (
|
||||
elements,
|
||||
elementsMap,
|
||||
appState.zoom,
|
||||
isElbowArrow(linearElement),
|
||||
isElbowArrow(linearElement),
|
||||
isElbowArrow(linearElement) && isElbowArrow(linearElement),
|
||||
);
|
||||
|
||||
if (hoveredElement !== null) {
|
||||
@@ -562,65 +558,8 @@ export const getHoveredElementForBinding = (
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
zoom?: AppState["zoom"],
|
||||
fullShape?: boolean,
|
||||
considerAllElements?: boolean,
|
||||
): NonDeleted<ExcalidrawBindableElement> | null => {
|
||||
if (considerAllElements) {
|
||||
let cullRest = false;
|
||||
const candidateElements = getAllElementsAtPositionForBinding(
|
||||
elements,
|
||||
(element) =>
|
||||
isBindableElement(element, false) &&
|
||||
bindingBorderTest(
|
||||
element,
|
||||
pointerCoords,
|
||||
elementsMap,
|
||||
zoom,
|
||||
(fullShape ||
|
||||
!isBindingFallthroughEnabled(
|
||||
element as ExcalidrawBindableElement,
|
||||
)) &&
|
||||
// disable fullshape snapping for frame elements so we
|
||||
// can bind to frame children
|
||||
!isFrameLikeElement(element),
|
||||
),
|
||||
).filter((element) => {
|
||||
if (cullRest) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isBindingFallthroughEnabled(element as ExcalidrawBindableElement)) {
|
||||
cullRest = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}) as NonDeleted<ExcalidrawBindableElement>[] | null;
|
||||
|
||||
// Return early if there are no candidates or just one candidate
|
||||
if (!candidateElements || candidateElements.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (candidateElements.length === 1) {
|
||||
return candidateElements[0] as NonDeleted<ExcalidrawBindableElement>;
|
||||
}
|
||||
|
||||
// Prefer the shape with the border being tested (if any)
|
||||
const borderTestElements = candidateElements.filter((element) =>
|
||||
bindingBorderTest(element, pointerCoords, elementsMap, zoom, false),
|
||||
);
|
||||
if (borderTestElements.length === 1) {
|
||||
return borderTestElements[0];
|
||||
}
|
||||
|
||||
// Prefer smaller shapes
|
||||
return candidateElements
|
||||
.sort(
|
||||
(a, b) => b.width ** 2 + b.height ** 2 - (a.width ** 2 + a.height ** 2),
|
||||
)
|
||||
.pop() as NonDeleted<ExcalidrawBindableElement>;
|
||||
}
|
||||
|
||||
const hoveredElement = getElementAtPositionForBinding(
|
||||
const hoveredElement = getElementAtPosition(
|
||||
elements,
|
||||
(element) =>
|
||||
isBindableElement(element, false) &&
|
||||
@@ -631,58 +570,13 @@ export const getHoveredElementForBinding = (
|
||||
zoom,
|
||||
// disable fullshape snapping for frame elements so we
|
||||
// can bind to frame children
|
||||
(fullShape || !isBindingFallthroughEnabled(element)) &&
|
||||
!isFrameLikeElement(element),
|
||||
fullShape && !isFrameLikeElement(element),
|
||||
),
|
||||
);
|
||||
|
||||
return hoveredElement as NonDeleted<ExcalidrawBindableElement> | null;
|
||||
};
|
||||
|
||||
const getElementAtPositionForBinding = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
isAtPositionFn: (element: NonDeletedExcalidrawElement) => boolean,
|
||||
) => {
|
||||
let hitElement = null;
|
||||
// We need to to hit testing from front (end of the array) to back (beginning of the array)
|
||||
// because array is ordered from lower z-index to highest and we want element z-index
|
||||
// with higher z-index
|
||||
for (let index = elements.length - 1; index >= 0; --index) {
|
||||
const element = elements[index];
|
||||
if (element.isDeleted) {
|
||||
continue;
|
||||
}
|
||||
if (isAtPositionFn(element)) {
|
||||
hitElement = element;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return hitElement;
|
||||
};
|
||||
|
||||
const getAllElementsAtPositionForBinding = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
isAtPositionFn: (element: NonDeletedExcalidrawElement) => boolean,
|
||||
) => {
|
||||
const elementsAtPosition: NonDeletedExcalidrawElement[] = [];
|
||||
// We need to to hit testing from front (end of the array) to back (beginning of the array)
|
||||
// because array is ordered from lower z-index to highest and we want element z-index
|
||||
// with higher z-index
|
||||
for (let index = elements.length - 1; index >= 0; --index) {
|
||||
const element = elements[index];
|
||||
if (element.isDeleted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isAtPositionFn(element)) {
|
||||
elementsAtPosition.push(element);
|
||||
}
|
||||
}
|
||||
|
||||
return elementsAtPosition;
|
||||
};
|
||||
|
||||
const calculateFocusAndGap = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
hoveredElement: ExcalidrawBindableElement,
|
||||
@@ -1326,8 +1220,6 @@ const getElligibleElementForBindingElement = (
|
||||
elements,
|
||||
elementsMap,
|
||||
zoom,
|
||||
isElbowArrow(linearElement),
|
||||
isElbowArrow(linearElement),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -20,11 +20,11 @@ import {
|
||||
bindPointToSnapToElementOutline,
|
||||
distanceToBindableElement,
|
||||
avoidRectangularCorner,
|
||||
getHoveredElementForBinding,
|
||||
FIXED_BINDING_DISTANCE,
|
||||
getHeadingForElbowArrowSnap,
|
||||
getGlobalFixedPointForBindableElement,
|
||||
snapToMid,
|
||||
getHoveredElementForBinding,
|
||||
} from "./binding";
|
||||
import type { Bounds } from "./bounds";
|
||||
import type { Heading } from "./heading";
|
||||
@@ -244,12 +244,6 @@ const handleSegmentRenormalization = (
|
||||
);
|
||||
}
|
||||
|
||||
import.meta.env.DEV &&
|
||||
invariant(
|
||||
validateElbowPoints(nextPoints),
|
||||
"Invalid elbow points with fixed segments",
|
||||
);
|
||||
|
||||
return normalizeArrowElementUpdate(
|
||||
nextPoints,
|
||||
filteredNextFixedSegments,
|
||||
@@ -872,8 +866,6 @@ export const updateElbowArrowPoints = (
|
||||
updates: {
|
||||
points?: readonly LocalPoint[];
|
||||
fixedSegments?: FixedSegment[] | null;
|
||||
startBinding?: FixedPointBinding | null;
|
||||
endBinding?: FixedPointBinding | null;
|
||||
},
|
||||
options?: {
|
||||
isDragging?: boolean;
|
||||
@@ -920,11 +912,7 @@ export const updateElbowArrowPoints = (
|
||||
// 0. During all element replacement in the scene, we just need to renormalize
|
||||
// the arrow
|
||||
// TODO (dwelle,mtolmacs): Remove this once Scene.getScene() is removed
|
||||
if (
|
||||
elementsMap.size === 0 &&
|
||||
updates.points &&
|
||||
validateElbowPoints(updates.points)
|
||||
) {
|
||||
if (elementsMap.size === 0 && updates.points) {
|
||||
return normalizeArrowElementUpdate(
|
||||
updates.points.map((p) =>
|
||||
pointFrom<GlobalPoint>(arrow.x + p[0], arrow.y + p[1]),
|
||||
@@ -955,48 +943,17 @@ export const updateElbowArrowPoints = (
|
||||
hoveredStartElement,
|
||||
hoveredEndElement,
|
||||
...rest
|
||||
} = getElbowArrowData(
|
||||
{
|
||||
x: arrow.x,
|
||||
y: arrow.y,
|
||||
startBinding:
|
||||
typeof updates.startBinding !== "undefined"
|
||||
? updates.startBinding
|
||||
: arrow.startBinding,
|
||||
endBinding:
|
||||
typeof updates.endBinding !== "undefined"
|
||||
? updates.endBinding
|
||||
: arrow.endBinding,
|
||||
startArrowhead: arrow.startArrowhead,
|
||||
endArrowhead: arrow.endArrowhead,
|
||||
},
|
||||
elementsMap,
|
||||
updatedPoints,
|
||||
options,
|
||||
);
|
||||
} = getElbowArrowData(arrow, elementsMap, updatedPoints, options);
|
||||
|
||||
const fixedSegments = updates.fixedSegments ?? arrow.fixedSegments ?? [];
|
||||
|
||||
////
|
||||
// 1. Renormalize the arrow
|
||||
////
|
||||
if (
|
||||
!updates.points &&
|
||||
!updates.fixedSegments &&
|
||||
!updates.startBinding &&
|
||||
!updates.endBinding
|
||||
) {
|
||||
if (!updates.points && !updates.fixedSegments) {
|
||||
return handleSegmentRenormalization(arrow, elementsMap);
|
||||
}
|
||||
|
||||
// Short circuit on no-op to avoid huge performance hit
|
||||
if (
|
||||
updates.startBinding === arrow.startBinding &&
|
||||
updates.endBinding === arrow.endBinding
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
|
||||
////
|
||||
// 2. Just normal elbow arrow things
|
||||
////
|
||||
@@ -1043,7 +1000,6 @@ export const updateElbowArrowPoints = (
|
||||
|
||||
////
|
||||
// 5. Handle resize
|
||||
////
|
||||
if (updates.points && updates.fixedSegments) {
|
||||
return updates;
|
||||
}
|
||||
@@ -2154,7 +2110,6 @@ const getHoveredElements = (
|
||||
nonDeletedSceneElementsMap,
|
||||
zoom,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
getHoveredElementForBinding(
|
||||
tupleToCoors(origEndGlobalPoint),
|
||||
@@ -2162,23 +2117,9 @@ const getHoveredElements = (
|
||||
nonDeletedSceneElementsMap,
|
||||
zoom,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
const gridAddressesEqual = (a: GridAddress, b: GridAddress): boolean =>
|
||||
a[0] === b[0] && a[1] === b[1];
|
||||
|
||||
const validateElbowPoints = <P extends GlobalPoint | LocalPoint>(
|
||||
points: readonly P[],
|
||||
tolerance: number = DEDUP_TRESHOLD,
|
||||
) =>
|
||||
points
|
||||
.slice(1)
|
||||
.map(
|
||||
(p, i) =>
|
||||
Math.abs(p[0] - points[i][0]) < tolerance ||
|
||||
Math.abs(p[1] - points[i][1]) < tolerance,
|
||||
)
|
||||
.every(Boolean);
|
||||
|
||||
@@ -10,15 +10,13 @@ import {
|
||||
import { bindLinearElement } from "./binding";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { newArrowElement, newElement } from "./newElement";
|
||||
import type { SceneElementsMap } from "./types";
|
||||
import {
|
||||
type ElementsMap,
|
||||
type ExcalidrawBindableElement,
|
||||
type ExcalidrawElement,
|
||||
type ExcalidrawFlowchartNodeElement,
|
||||
type NonDeletedSceneElementsMap,
|
||||
type Ordered,
|
||||
type OrderedExcalidrawElement,
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawFlowchartNodeElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
OrderedExcalidrawElement,
|
||||
} from "./types";
|
||||
import { KEYS } from "../keys";
|
||||
import type { AppState, PendingExcalidrawElements } from "../types";
|
||||
@@ -30,10 +28,9 @@ import {
|
||||
isFrameElement,
|
||||
isFlowchartNodeElement,
|
||||
} from "./typeChecks";
|
||||
import { invariant, toBrandedType } from "../utils";
|
||||
import { invariant } from "../utils";
|
||||
import { pointFrom, type LocalPoint } from "../../math";
|
||||
import { aabbForElement } from "../shapes";
|
||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||
|
||||
type LinkDirection = "up" | "right" | "down" | "left";
|
||||
|
||||
@@ -470,23 +467,7 @@ const createBindingArrow = (
|
||||
},
|
||||
]);
|
||||
|
||||
const update = updateElbowArrowPoints(
|
||||
bindingArrow,
|
||||
toBrandedType<SceneElementsMap>(
|
||||
new Map([
|
||||
...elementsMap.entries(),
|
||||
[startBindingElement.id, startBindingElement],
|
||||
[endBindingElement.id, endBindingElement],
|
||||
[bindingArrow.id, bindingArrow],
|
||||
] as [string, Ordered<ExcalidrawElement>][]),
|
||||
),
|
||||
{ points: bindingArrow.points },
|
||||
);
|
||||
|
||||
return {
|
||||
...bindingArrow,
|
||||
...update,
|
||||
};
|
||||
return bindingArrow;
|
||||
};
|
||||
|
||||
export class FlowChartNavigator {
|
||||
|
||||
@@ -444,8 +444,6 @@ export class LinearElementEditor {
|
||||
elements,
|
||||
elementsMap,
|
||||
appState.zoom,
|
||||
isElbowArrow(element),
|
||||
isElbowArrow(element),
|
||||
)
|
||||
: null;
|
||||
|
||||
@@ -798,7 +796,6 @@ export class LinearElementEditor {
|
||||
elements,
|
||||
elementsMap,
|
||||
app.state.zoom,
|
||||
linearElementEditor.elbowed,
|
||||
),
|
||||
};
|
||||
|
||||
|
||||
@@ -33,16 +33,13 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
|
||||
// casting to any because can't use `in` operator
|
||||
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
||||
const { points, fixedSegments, fileId, startBinding, endBinding } =
|
||||
updates as any;
|
||||
const { points, fixedSegments, fileId } = updates as any;
|
||||
|
||||
if (
|
||||
isElbowArrow(element) &&
|
||||
(Object.keys(updates).length === 0 || // normalization case
|
||||
typeof points !== "undefined" || // repositioning
|
||||
typeof fixedSegments !== "undefined" || // segment fixing
|
||||
typeof startBinding !== "undefined" ||
|
||||
typeof endBinding !== "undefined") // manual binding to element
|
||||
typeof fixedSegments !== "undefined") // segment fixing
|
||||
) {
|
||||
const elementsMap = toBrandedType<SceneElementsMap>(
|
||||
Scene.getScene(element)?.getNonDeletedElementsMap() ?? new Map(),
|
||||
@@ -61,8 +58,6 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
{
|
||||
fixedSegments,
|
||||
points,
|
||||
startBinding,
|
||||
endBinding,
|
||||
},
|
||||
{
|
||||
isDragging: options?.isDragging,
|
||||
|
||||
@@ -45,7 +45,6 @@ import {
|
||||
DEFAULT_FONT_SIZE,
|
||||
DEFAULT_TEXT_ALIGN,
|
||||
DEFAULT_VERTICAL_ALIGN,
|
||||
ORIG_ID,
|
||||
VERTICAL_ALIGN,
|
||||
} from "../constants";
|
||||
import type { MarkOptional, Merge, Mutable } from "../utility-types";
|
||||
@@ -593,18 +592,26 @@ export const deepCopyElement = <T extends ExcalidrawElement>(
|
||||
return _deepCopyElement(val);
|
||||
};
|
||||
|
||||
const __test__defineOrigId = (clonedObj: object, origId: string) => {
|
||||
Object.defineProperty(clonedObj, ORIG_ID, {
|
||||
value: origId,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* utility wrapper to generate new id.
|
||||
* utility wrapper to generate new id. In test env it reuses the old + postfix
|
||||
* for test assertions.
|
||||
*/
|
||||
const regenerateId = () => {
|
||||
export const regenerateId = (
|
||||
/** supply null if no previous id exists */
|
||||
previousId: string | null,
|
||||
) => {
|
||||
if (isTestEnv() && previousId) {
|
||||
let nextId = `${previousId}_copy`;
|
||||
// `window.h` may not be defined in some unit tests
|
||||
if (
|
||||
window.h?.app
|
||||
?.getSceneElementsIncludingDeleted()
|
||||
.find((el: ExcalidrawElement) => el.id === nextId)
|
||||
) {
|
||||
nextId += "_copy";
|
||||
}
|
||||
return nextId;
|
||||
}
|
||||
return randomId();
|
||||
};
|
||||
|
||||
@@ -630,11 +637,7 @@ export const duplicateElement = <TElement extends ExcalidrawElement>(
|
||||
): Readonly<TElement> => {
|
||||
let copy = deepCopyElement(element);
|
||||
|
||||
if (isTestEnv()) {
|
||||
__test__defineOrigId(copy, element.id);
|
||||
}
|
||||
|
||||
copy.id = regenerateId();
|
||||
copy.id = regenerateId(copy.id);
|
||||
copy.boundElements = null;
|
||||
copy.updated = getUpdatedTimestamp();
|
||||
copy.seed = randomInteger();
|
||||
@@ -643,7 +646,7 @@ export const duplicateElement = <TElement extends ExcalidrawElement>(
|
||||
editingGroupId,
|
||||
(groupId) => {
|
||||
if (!groupIdMapForOperation.has(groupId)) {
|
||||
groupIdMapForOperation.set(groupId, regenerateId());
|
||||
groupIdMapForOperation.set(groupId, regenerateId(groupId));
|
||||
}
|
||||
return groupIdMapForOperation.get(groupId)!;
|
||||
},
|
||||
@@ -689,7 +692,7 @@ export const duplicateElements = (
|
||||
// if we haven't migrated the element id, but an old element with the same
|
||||
// id exists, generate a new id for it and return it
|
||||
if (origElementsMap.has(id)) {
|
||||
const newId = regenerateId();
|
||||
const newId = regenerateId(id);
|
||||
elementNewIdsMap.set(id, newId);
|
||||
return newId;
|
||||
}
|
||||
@@ -703,9 +706,6 @@ export const duplicateElements = (
|
||||
const clonedElement: Mutable<ExcalidrawElement> = _deepCopyElement(element);
|
||||
|
||||
clonedElement.id = maybeGetNewId(element.id)!;
|
||||
if (isTestEnv()) {
|
||||
__test__defineOrigId(clonedElement, element.id);
|
||||
}
|
||||
|
||||
if (opts?.randomizeSeed) {
|
||||
clonedElement.seed = randomInteger();
|
||||
@@ -715,7 +715,7 @@ export const duplicateElements = (
|
||||
if (clonedElement.groupIds) {
|
||||
clonedElement.groupIds = clonedElement.groupIds.map((groupId) => {
|
||||
if (!groupNewIdsMap.has(groupId)) {
|
||||
groupNewIdsMap.set(groupId, regenerateId());
|
||||
groupNewIdsMap.set(groupId, regenerateId(groupId));
|
||||
}
|
||||
return groupNewIdsMap.get(groupId)!;
|
||||
});
|
||||
|
||||
@@ -771,8 +771,8 @@ const getResizedOrigin = (
|
||||
x: x + ((prevWidth - newWidth) / 2) * (Math.cos(angle) + 1),
|
||||
y:
|
||||
y +
|
||||
((prevWidth - newWidth) / 2) * Math.sin(angle) +
|
||||
(prevHeight - newHeight) / 2,
|
||||
(newHeight - prevHeight) / 2 +
|
||||
((prevWidth - newWidth) / 2) * Math.sin(angle),
|
||||
};
|
||||
case "west-side":
|
||||
return {
|
||||
|
||||
@@ -116,5 +116,8 @@ const normalizeBoundElementsOrder = (
|
||||
export const normalizeElementOrder = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
) => {
|
||||
return normalizeBoundElementsOrder(normalizeGroupElementOrder(elements));
|
||||
// console.time();
|
||||
const ret = normalizeBoundElementsOrder(normalizeGroupElementOrder(elements));
|
||||
// console.timeEnd();
|
||||
return ret;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from "react";
|
||||
import type { ExcalidrawElement } from "./element/types";
|
||||
import { convertToExcalidrawElements, Excalidraw } from "./index";
|
||||
import { API } from "./tests/helpers/api";
|
||||
import { Keyboard, Pointer } from "./tests/helpers/ui";
|
||||
import { getCloneByOrigId, render } from "./tests/test-utils";
|
||||
import { render } from "./tests/test-utils";
|
||||
|
||||
const { h } = window;
|
||||
const mouse = new Pointer("mouse");
|
||||
@@ -412,9 +413,9 @@ describe("adding elements to frames", () => {
|
||||
|
||||
dragElementIntoFrame(frame, rect2);
|
||||
|
||||
selectElementAndDuplicate(rect2);
|
||||
const rect2_copy = { ...rect2, id: `${rect2.id}_copy` };
|
||||
|
||||
const rect2_copy = getCloneByOrigId(rect2.id);
|
||||
selectElementAndDuplicate(rect2);
|
||||
|
||||
expect(rect2_copy.frameId).toBe(frame.id);
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
@@ -426,11 +427,11 @@ describe("adding elements to frames", () => {
|
||||
|
||||
dragElementIntoFrame(frame, rect2);
|
||||
|
||||
const rect2_copy = { ...rect2, id: `${rect2.id}_copy` };
|
||||
|
||||
// move the rect2 outside the frame
|
||||
selectElementAndDuplicate(rect2, [-1000, -1000]);
|
||||
|
||||
const rect2_copy = getCloneByOrigId(rect2.id);
|
||||
|
||||
expect(rect2_copy.frameId).toBe(frame.id);
|
||||
expect(rect2.frameId).toBe(null);
|
||||
expectEqualIds([rect2_copy, frame, rect2]);
|
||||
|
||||
@@ -46,7 +46,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
onPointerDown,
|
||||
onPointerUp,
|
||||
onScrollChange,
|
||||
onDuplicate,
|
||||
children,
|
||||
validateEmbeddable,
|
||||
renderEmbeddable,
|
||||
@@ -137,7 +136,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerUp={onPointerUp}
|
||||
onScrollChange={onScrollChange}
|
||||
onDuplicate={onDuplicate}
|
||||
validateEmbeddable={validateEmbeddable}
|
||||
renderEmbeddable={renderEmbeddable}
|
||||
aiEnabled={aiEnabled !== false}
|
||||
|
||||
@@ -103,14 +103,13 @@
|
||||
"@types/pako": "1.0.3",
|
||||
"@types/pica": "5.1.3",
|
||||
"@types/resize-observer-browser": "0.1.7",
|
||||
"ansicolor": "2.0.3",
|
||||
"autoprefixer": "10.4.7",
|
||||
"babel-loader": "8.2.5",
|
||||
"babel-plugin-transform-class-properties": "6.24.1",
|
||||
"cross-env": "7.0.3",
|
||||
"css-loader": "6.7.1",
|
||||
"dotenv": "16.0.1",
|
||||
"esbuild": "0.25.0",
|
||||
"esbuild": "0.19.10",
|
||||
"esbuild-plugin-external-global": "1.0.1",
|
||||
"esbuild-sass-plugin": "2.16.0",
|
||||
"eslint-plugin-react": "7.32.2",
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import { isIframeElement } from "../element/typeChecks";
|
||||
import type {
|
||||
ExcalidrawIframeElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "../element/types";
|
||||
import type { ElementOrToolType } from "../types";
|
||||
|
||||
export const hasBackground = (type: ElementOrToolType) =>
|
||||
@@ -42,3 +47,48 @@ export const canChangeRoundness = (type: ElementOrToolType) =>
|
||||
export const toolIsArrow = (type: ElementOrToolType) => type === "arrow";
|
||||
|
||||
export const canHaveArrowheads = (type: ElementOrToolType) => type === "arrow";
|
||||
|
||||
export const getElementAtPosition = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
isAtPositionFn: (element: NonDeletedExcalidrawElement) => boolean,
|
||||
) => {
|
||||
let hitElement = null;
|
||||
// We need to to hit testing from front (end of the array) to back (beginning of the array)
|
||||
// because array is ordered from lower z-index to highest and we want element z-index
|
||||
// with higher z-index
|
||||
for (let index = elements.length - 1; index >= 0; --index) {
|
||||
const element = elements[index];
|
||||
if (element.isDeleted) {
|
||||
continue;
|
||||
}
|
||||
if (isAtPositionFn(element)) {
|
||||
hitElement = element;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return hitElement;
|
||||
};
|
||||
|
||||
export const getElementsAtPosition = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
isAtPositionFn: (element: NonDeletedExcalidrawElement) => boolean,
|
||||
) => {
|
||||
const iframeLikes: ExcalidrawIframeElement[] = [];
|
||||
// The parameter elements comes ordered from lower z-index to higher.
|
||||
// We want to preserve that order on the returned array.
|
||||
// Exception being embeddables which should be on top of everything else in
|
||||
// terms of hit testing.
|
||||
const elsAtPos = elements.filter((element) => {
|
||||
const hit = !element.isDeleted && isAtPositionFn(element);
|
||||
if (hit) {
|
||||
if (isIframeElement(element)) {
|
||||
iframeLikes.push(element);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return elsAtPos.concat(iframeLikes);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,8 @@ export {
|
||||
hasStrokeStyle,
|
||||
canHaveArrowheads,
|
||||
canChangeRoundness,
|
||||
getElementAtPosition,
|
||||
getElementsAtPosition,
|
||||
} from "./comparisons";
|
||||
export {
|
||||
getNormalizedZoom,
|
||||
|
||||
@@ -2517,7 +2517,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||
"scrolledOutside": false,
|
||||
"searchMatches": [],
|
||||
"selectedElementIds": {
|
||||
"id1": true,
|
||||
"id0_copy": true,
|
||||
},
|
||||
"selectedElementsAreBeingDragged": false,
|
||||
"selectedGroupIds": {},
|
||||
@@ -2590,7 +2590,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"id": "id0_copy",
|
||||
"index": "a1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
@@ -2680,7 +2680,7 @@ History {
|
||||
"delta": Delta {
|
||||
"deleted": {
|
||||
"selectedElementIds": {
|
||||
"id1": true,
|
||||
"id0_copy": true,
|
||||
},
|
||||
},
|
||||
"inserted": {
|
||||
@@ -2693,7 +2693,7 @@ History {
|
||||
"elementsChange": ElementsChange {
|
||||
"added": Map {},
|
||||
"removed": Map {
|
||||
"id1" => Delta {
|
||||
"id0_copy" => Delta {
|
||||
"deleted": {
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@ exports[`duplicate element on move when ALT is clicked > rectangle 5`] = `
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 50,
|
||||
"id": "id2",
|
||||
"id": "id0_copy",
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
|
||||
@@ -2129,7 +2129,7 @@ History {
|
||||
"elementsChange": ElementsChange {
|
||||
"added": Map {},
|
||||
"removed": Map {
|
||||
"id2" => Delta {
|
||||
"id0_copy" => Delta {
|
||||
"deleted": {
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
@@ -10619,7 +10619,7 @@ History {
|
||||
"elementsChange": ElementsChange {
|
||||
"added": Map {},
|
||||
"removed": Map {
|
||||
"id6" => Delta {
|
||||
"id0_copy" => Delta {
|
||||
"deleted": {
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
@@ -10628,7 +10628,7 @@ History {
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [
|
||||
"id7",
|
||||
"id4_copy",
|
||||
],
|
||||
"height": 10,
|
||||
"index": "a0",
|
||||
@@ -10652,7 +10652,7 @@ History {
|
||||
"isDeleted": true,
|
||||
},
|
||||
},
|
||||
"id8" => Delta {
|
||||
"id1_copy" => Delta {
|
||||
"deleted": {
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
@@ -10661,7 +10661,7 @@ History {
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [
|
||||
"id7",
|
||||
"id4_copy",
|
||||
],
|
||||
"height": 10,
|
||||
"index": "a1",
|
||||
@@ -10685,7 +10685,7 @@ History {
|
||||
"isDeleted": true,
|
||||
},
|
||||
},
|
||||
"id9" => Delta {
|
||||
"id2_copy" => Delta {
|
||||
"deleted": {
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
@@ -10694,7 +10694,7 @@ History {
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [
|
||||
"id7",
|
||||
"id4_copy",
|
||||
],
|
||||
"height": 10,
|
||||
"index": "a2",
|
||||
|
||||
@@ -40,7 +40,6 @@ import { createTestHook } from "../../components/App";
|
||||
import type { Action } from "../../actions/types";
|
||||
import { mutateElement } from "../../element/mutateElement";
|
||||
import { pointFrom, type LocalPoint, type Radians } from "../../../math";
|
||||
import { selectGroupsForSelectedElements } from "../../groups";
|
||||
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
// so that window.h is available when App.tsx is not imported as well.
|
||||
@@ -69,21 +68,13 @@ export class API {
|
||||
});
|
||||
};
|
||||
|
||||
static setSelectedElements = (elements: ExcalidrawElement[], editingGroupId?: string | null) => {
|
||||
static setSelectedElements = (elements: ExcalidrawElement[]) => {
|
||||
act(() => {
|
||||
h.setState({
|
||||
...selectGroupsForSelectedElements(
|
||||
{
|
||||
editingGroupId: editingGroupId ?? null,
|
||||
selectedElementIds: elements.reduce((acc, element) => {
|
||||
acc[element.id] = true;
|
||||
return acc;
|
||||
}, {} as Record<ExcalidrawElement["id"], true>),
|
||||
},
|
||||
elements,
|
||||
h.state,
|
||||
h.app,
|
||||
)
|
||||
selectedElementIds: elements.reduce((acc, element) => {
|
||||
acc[element.id] = true;
|
||||
return acc;
|
||||
}, {} as Record<ExcalidrawElement["id"], true>),
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -167,7 +158,7 @@ export class API {
|
||||
isDeleted?: boolean;
|
||||
frameId?: ExcalidrawElement["id"] | null;
|
||||
index?: ExcalidrawElement["index"];
|
||||
groupIds?: ExcalidrawElement["groupIds"];
|
||||
groupIds?: string[];
|
||||
// generic element props
|
||||
strokeColor?: ExcalidrawGenericElement["strokeColor"];
|
||||
backgroundColor?: ExcalidrawGenericElement["backgroundColor"];
|
||||
@@ -378,84 +369,6 @@ export class API {
|
||||
return element as any;
|
||||
};
|
||||
|
||||
static createTextContainer = (opts?: {
|
||||
frameId?: ExcalidrawElement["id"];
|
||||
groupIds?: ExcalidrawElement["groupIds"];
|
||||
label?: {
|
||||
text?: string;
|
||||
frameId?: ExcalidrawElement["id"] | null;
|
||||
groupIds?: ExcalidrawElement["groupIds"];
|
||||
};
|
||||
}) => {
|
||||
const rectangle = API.createElement({
|
||||
type: "rectangle",
|
||||
frameId: opts?.frameId || null,
|
||||
groupIds: opts?.groupIds,
|
||||
});
|
||||
|
||||
const text = API.createElement({
|
||||
type: "text",
|
||||
text: opts?.label?.text || "sample-text",
|
||||
width: 50,
|
||||
height: 20,
|
||||
fontSize: 16,
|
||||
containerId: rectangle.id,
|
||||
frameId:
|
||||
opts?.label?.frameId === undefined
|
||||
? opts?.frameId ?? null
|
||||
: opts?.label?.frameId ?? null,
|
||||
groupIds: opts?.label?.groupIds === undefined
|
||||
? opts?.groupIds
|
||||
: opts?.label?.groupIds ,
|
||||
|
||||
});
|
||||
|
||||
mutateElement(
|
||||
rectangle,
|
||||
{
|
||||
boundElements: [{ type: "text", id: text.id }],
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
return [rectangle, text];
|
||||
};
|
||||
|
||||
static createLabeledArrow = (opts?: {
|
||||
frameId?: ExcalidrawElement["id"];
|
||||
label?: {
|
||||
text?: string;
|
||||
frameId?: ExcalidrawElement["id"] | null;
|
||||
};
|
||||
}) => {
|
||||
const arrow = API.createElement({
|
||||
type: "arrow",
|
||||
frameId: opts?.frameId || null,
|
||||
});
|
||||
|
||||
const text = API.createElement({
|
||||
type: "text",
|
||||
id: "text2",
|
||||
width: 50,
|
||||
height: 20,
|
||||
containerId: arrow.id,
|
||||
frameId:
|
||||
opts?.label?.frameId === undefined
|
||||
? opts?.frameId ?? null
|
||||
: opts?.label?.frameId ?? null,
|
||||
});
|
||||
|
||||
mutateElement(
|
||||
arrow,
|
||||
{
|
||||
boundElements: [{ type: "text", id: text.id }],
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
return [arrow, text];
|
||||
};
|
||||
|
||||
static readFile = async <T extends "utf8" | null>(
|
||||
filepath: string,
|
||||
encoding?: T,
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
assertSelectedElements,
|
||||
render,
|
||||
togglePopover,
|
||||
getCloneByOrigId,
|
||||
} from "./test-utils";
|
||||
import { Excalidraw } from "../index";
|
||||
import { Keyboard, Pointer, UI } from "./helpers/ui";
|
||||
@@ -16,7 +15,7 @@ import { getDefaultAppState } from "../appState";
|
||||
import { fireEvent, queryByTestId, waitFor } from "@testing-library/react";
|
||||
import { createUndoAction, createRedoAction } from "../actions/actionHistory";
|
||||
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
|
||||
import { EXPORT_DATA_TYPES, MIME_TYPES, ORIG_ID } from "../constants";
|
||||
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
|
||||
import type { AppState } from "../types";
|
||||
import { arrayToMap } from "../utils";
|
||||
import {
|
||||
@@ -1139,8 +1138,8 @@ describe("history", () => {
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({ id: rect1.id, isDeleted: false }),
|
||||
expect.objectContaining({ id: rect2.id, isDeleted: false }),
|
||||
expect.objectContaining({ [ORIG_ID]: rect1.id, isDeleted: true }),
|
||||
expect.objectContaining({ [ORIG_ID]: rect2.id, isDeleted: true }),
|
||||
expect.objectContaining({ id: `${rect1.id}_copy`, isDeleted: true }),
|
||||
expect.objectContaining({ id: `${rect2.id}_copy`, isDeleted: true }),
|
||||
]);
|
||||
expect(h.state.editingGroupId).toBeNull();
|
||||
expect(h.state.selectedGroupIds).toEqual({ A: true });
|
||||
@@ -1152,8 +1151,8 @@ describe("history", () => {
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({ id: rect1.id, isDeleted: false }),
|
||||
expect.objectContaining({ id: rect2.id, isDeleted: false }),
|
||||
expect.objectContaining({ [ORIG_ID]: rect1.id, isDeleted: false }),
|
||||
expect.objectContaining({ [ORIG_ID]: rect2.id, isDeleted: false }),
|
||||
expect.objectContaining({ id: `${rect1.id}_copy`, isDeleted: false }),
|
||||
expect.objectContaining({ id: `${rect2.id}_copy`, isDeleted: false }),
|
||||
]);
|
||||
expect(h.state.editingGroupId).toBeNull();
|
||||
expect(h.state.selectedGroupIds).not.toEqual(
|
||||
@@ -1172,14 +1171,14 @@ describe("history", () => {
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: rect1.id, isDeleted: false }),
|
||||
expect.objectContaining({ id: rect2.id, isDeleted: false }),
|
||||
expect.objectContaining({ [ORIG_ID]: rect1.id, isDeleted: true }),
|
||||
expect.objectContaining({ [ORIG_ID]: rect2.id, isDeleted: true }),
|
||||
expect.objectContaining({ id: `${rect1.id}_copy`, isDeleted: true }),
|
||||
expect.objectContaining({ id: `${rect2.id}_copy`, isDeleted: true }),
|
||||
expect.objectContaining({
|
||||
[ORIG_ID]: getCloneByOrigId(rect1.id)?.id,
|
||||
id: `${rect1.id}_copy_copy`,
|
||||
isDeleted: false,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
[ORIG_ID]: getCloneByOrigId(rect2.id)?.id,
|
||||
id: `${rect2.id}_copy_copy`,
|
||||
isDeleted: false,
|
||||
}),
|
||||
]),
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from "react";
|
||||
import { vi } from "vitest";
|
||||
import { fireEvent, getCloneByOrigId, render, waitFor } from "./test-utils";
|
||||
import { fireEvent, render, waitFor } from "./test-utils";
|
||||
import { act, queryByTestId } from "@testing-library/react";
|
||||
|
||||
import { Excalidraw } from "../index";
|
||||
import { API } from "./helpers/api";
|
||||
import { MIME_TYPES, ORIG_ID } from "../constants";
|
||||
import { MIME_TYPES } from "../constants";
|
||||
import type { LibraryItem, LibraryItems } from "../types";
|
||||
import { UI } from "./helpers/ui";
|
||||
import { serializeLibraryAsJSON } from "../data/json";
|
||||
@@ -76,7 +76,7 @@ describe("library", () => {
|
||||
}),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(h.elements).toEqual([expect.objectContaining({ [ORIG_ID]: "A" })]);
|
||||
expect(h.elements).toEqual([expect.objectContaining({ id: "A_copy" })]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -125,27 +125,23 @@ describe("library", () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(h.elements).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
[ORIG_ID]: "rectangle1",
|
||||
boundElements: expect.arrayContaining([
|
||||
{ type: "text", id: getCloneByOrigId("text1").id },
|
||||
{ type: "arrow", id: getCloneByOrigId("arrow1").id },
|
||||
]),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
[ORIG_ID]: "text1",
|
||||
containerId: getCloneByOrigId("rectangle1").id,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
[ORIG_ID]: "arrow1",
|
||||
endBinding: expect.objectContaining({
|
||||
elementId: getCloneByOrigId("rectangle1").id,
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "rectangle1_copy",
|
||||
boundElements: expect.arrayContaining([
|
||||
{ type: "text", id: "text1_copy" },
|
||||
{ type: "arrow", id: "arrow1_copy" },
|
||||
]),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "text1_copy",
|
||||
containerId: "rectangle1_copy",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "arrow1_copy",
|
||||
endBinding: expect.objectContaining({ elementId: "rectangle1_copy" }),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -174,11 +170,10 @@ describe("library", () => {
|
||||
await waitFor(() => {
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
[ORIG_ID]: "elem1",
|
||||
id: "elem1_copy",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.not.stringMatching(/^elem1$/),
|
||||
[ORIG_ID]: expect.not.stringMatching(/^\w+$/),
|
||||
id: expect.not.stringMatching(/^(elem1_copy|elem1)$/),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
@@ -194,7 +189,7 @@ describe("library", () => {
|
||||
}),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(h.elements).toEqual([expect.objectContaining({ [ORIG_ID]: "A" })]);
|
||||
expect(h.elements).toEqual([expect.objectContaining({ id: "A_copy" })]);
|
||||
});
|
||||
expect(h.state.activeTool.type).toBe("selection");
|
||||
});
|
||||
|
||||
@@ -11,10 +11,6 @@ import { getSelectedElements } from "../scene/selection";
|
||||
import type { ExcalidrawElement } from "../element/types";
|
||||
import { UI } from "./helpers/ui";
|
||||
import { diffStringsUnified } from "jest-diff";
|
||||
import ansi from "ansicolor";
|
||||
import { ORIG_ID } from "../constants";
|
||||
import { arrayToMap } from "../utils";
|
||||
import type { AllPossibleKeys } from "../utility-types";
|
||||
|
||||
const customQueries = {
|
||||
...queries,
|
||||
@@ -299,150 +295,3 @@ expect.addSnapshotSerializer({
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const getCloneByOrigId = <T extends boolean = false>(
|
||||
origId: ExcalidrawElement["id"],
|
||||
returnNullIfNotExists: T = false as T,
|
||||
): T extends true ? ExcalidrawElement | null : ExcalidrawElement => {
|
||||
const clonedElement = window.h.elements?.find(
|
||||
(el) => (el as any)[ORIG_ID] === origId,
|
||||
);
|
||||
if (clonedElement) {
|
||||
return clonedElement;
|
||||
}
|
||||
if (returnNullIfNotExists !== true) {
|
||||
throw new Error(`cloned element not found for origId: ${origId}`);
|
||||
}
|
||||
return null as T extends true ? ExcalidrawElement | null : ExcalidrawElement;
|
||||
};
|
||||
|
||||
/**
|
||||
* Assertion helper that strips the actual elements of extra attributes
|
||||
* so that diffs are easier to read in case of failure.
|
||||
*
|
||||
* Asserts element order as well, and selected element ids
|
||||
* (when `selected: true` set for given element).
|
||||
*
|
||||
* If testing cloned elements, you can use { `[ORIG_ID]: origElement.id }
|
||||
* If you need to refer to cloned element properties, you can use
|
||||
* `getCloneByOrigId()`, e.g.: `{ frameId: getCloneByOrigId(origFrame.id)?.id }`
|
||||
*/
|
||||
export const assertElements = <T extends AllPossibleKeys<ExcalidrawElement>>(
|
||||
actualElements: readonly ExcalidrawElement[],
|
||||
/** array order matters */
|
||||
expectedElements: (Partial<Record<T, any>> & {
|
||||
/** meta, will be stripped for element attribute checks */
|
||||
selected?: true;
|
||||
} & (
|
||||
| {
|
||||
id: ExcalidrawElement["id"];
|
||||
}
|
||||
| { [ORIG_ID]?: string }
|
||||
))[],
|
||||
) => {
|
||||
const h = window.h;
|
||||
|
||||
const expectedElementsWithIds: (typeof expectedElements[number] & {
|
||||
id: ExcalidrawElement["id"];
|
||||
})[] = expectedElements.map((el) => {
|
||||
if ("id" in el) {
|
||||
return el;
|
||||
}
|
||||
const actualElement = actualElements.find(
|
||||
(act) => (act as any)[ORIG_ID] === el[ORIG_ID],
|
||||
);
|
||||
if (actualElement) {
|
||||
return { ...el, id: actualElement.id };
|
||||
}
|
||||
return {
|
||||
...el,
|
||||
id: "UNKNOWN_ID",
|
||||
};
|
||||
});
|
||||
|
||||
const map_expectedElements = arrayToMap(expectedElementsWithIds);
|
||||
|
||||
const selectedElementIds = expectedElementsWithIds.reduce(
|
||||
(acc: Record<ExcalidrawElement["id"], true>, el) => {
|
||||
if (el.selected) {
|
||||
acc[el.id] = true;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const mappedActualElements = actualElements.map((el) => {
|
||||
const expectedElement = map_expectedElements.get(el.id);
|
||||
if (expectedElement) {
|
||||
const pickedAttrs: Record<string, any> = {};
|
||||
|
||||
for (const key of Object.keys(expectedElement)) {
|
||||
if (key === "selected") {
|
||||
delete expectedElement.selected;
|
||||
continue;
|
||||
}
|
||||
pickedAttrs[key] = (el as any)[key];
|
||||
}
|
||||
|
||||
if (ORIG_ID in expectedElement) {
|
||||
// @ts-ignore
|
||||
pickedAttrs[ORIG_ID] = (el as any)[ORIG_ID];
|
||||
}
|
||||
|
||||
return pickedAttrs;
|
||||
}
|
||||
return el;
|
||||
});
|
||||
|
||||
try {
|
||||
// testing order separately for even easier diffs
|
||||
expect(actualElements.map((x) => x.id)).toEqual(
|
||||
expectedElementsWithIds.map((x) => x.id),
|
||||
);
|
||||
} catch (err: any) {
|
||||
let errStr = "\n\nmismatched element order\n\n";
|
||||
|
||||
errStr += `actual: ${ansi.lightGray(
|
||||
`[${err.actual
|
||||
.map((id: string, index: number) => {
|
||||
const act = actualElements[index];
|
||||
|
||||
return `${
|
||||
id === err.expected[index] ? ansi.green(id) : ansi.red(id)
|
||||
} (${act.type.slice(0, 4)}${
|
||||
ORIG_ID in act ? ` ↳ ${(act as any)[ORIG_ID]}` : ""
|
||||
})`;
|
||||
})
|
||||
.join(", ")}]`,
|
||||
)}\n${ansi.lightGray(
|
||||
`expected: [${err.expected
|
||||
.map((exp: string, index: number) => {
|
||||
const expEl = actualElements.find((el) => el.id === exp);
|
||||
const origEl =
|
||||
expEl &&
|
||||
actualElements.find((el) => el.id === (expEl as any)[ORIG_ID]);
|
||||
return expEl
|
||||
? `${
|
||||
exp === err.actual[index]
|
||||
? ansi.green(expEl.id)
|
||||
: ansi.red(expEl.id)
|
||||
} (${expEl.type.slice(0, 4)}${origEl ? ` ↳ ${origEl.id}` : ""})`
|
||||
: exp;
|
||||
})
|
||||
.join(", ")}]\n`,
|
||||
)}`;
|
||||
|
||||
const error = new Error(errStr);
|
||||
const stack = err.stack.split("\n");
|
||||
stack.splice(1, 1);
|
||||
error.stack = stack.join("\n");
|
||||
throw error;
|
||||
}
|
||||
|
||||
expect(mappedActualElements).toEqual(
|
||||
expect.arrayContaining(expectedElementsWithIds),
|
||||
);
|
||||
|
||||
expect(h.state.selectedElementIds).toEqual(selectedElementIds);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { act, getCloneByOrigId, render } from "./test-utils";
|
||||
import { act, render } from "./test-utils";
|
||||
import { Excalidraw } from "../index";
|
||||
import { reseed } from "../random";
|
||||
import {
|
||||
@@ -916,9 +916,9 @@ describe("z-index manipulation", () => {
|
||||
API.executeAction(actionDuplicateSelection);
|
||||
expect(h.elements).toMatchObject([
|
||||
{ id: "A" },
|
||||
{ id: getCloneByOrigId("A").id },
|
||||
{ id: "A_copy" },
|
||||
{ id: "B" },
|
||||
{ id: getCloneByOrigId("B").id },
|
||||
{ id: "B_copy" },
|
||||
]);
|
||||
|
||||
populateElements([
|
||||
@@ -930,12 +930,12 @@ describe("z-index manipulation", () => {
|
||||
{ id: "A" },
|
||||
{ id: "B" },
|
||||
{
|
||||
id: getCloneByOrigId("A").id,
|
||||
id: "A_copy",
|
||||
|
||||
groupIds: [expect.stringMatching(/.{3,}/)],
|
||||
},
|
||||
{
|
||||
id: getCloneByOrigId("B").id,
|
||||
id: "B_copy",
|
||||
|
||||
groupIds: [expect.stringMatching(/.{3,}/)],
|
||||
},
|
||||
@@ -951,12 +951,12 @@ describe("z-index manipulation", () => {
|
||||
{ id: "A" },
|
||||
{ id: "B" },
|
||||
{
|
||||
id: getCloneByOrigId("A").id,
|
||||
id: "A_copy",
|
||||
|
||||
groupIds: [expect.stringMatching(/.{3,}/)],
|
||||
},
|
||||
{
|
||||
id: getCloneByOrigId("B").id,
|
||||
id: "B_copy",
|
||||
|
||||
groupIds: [expect.stringMatching(/.{3,}/)],
|
||||
},
|
||||
@@ -972,10 +972,10 @@ describe("z-index manipulation", () => {
|
||||
expect(h.elements.map((element) => element.id)).toEqual([
|
||||
"A",
|
||||
"B",
|
||||
getCloneByOrigId("A").id,
|
||||
getCloneByOrigId("B").id,
|
||||
"A_copy",
|
||||
"B_copy",
|
||||
"C",
|
||||
getCloneByOrigId("C").id,
|
||||
"C_copy",
|
||||
]);
|
||||
|
||||
populateElements([
|
||||
@@ -988,12 +988,12 @@ describe("z-index manipulation", () => {
|
||||
expect(h.elements.map((element) => element.id)).toEqual([
|
||||
"A",
|
||||
"B",
|
||||
getCloneByOrigId("A").id,
|
||||
getCloneByOrigId("B").id,
|
||||
"A_copy",
|
||||
"B_copy",
|
||||
"C",
|
||||
"D",
|
||||
getCloneByOrigId("C").id,
|
||||
getCloneByOrigId("D").id,
|
||||
"C_copy",
|
||||
"D_copy",
|
||||
]);
|
||||
|
||||
populateElements(
|
||||
@@ -1010,10 +1010,10 @@ describe("z-index manipulation", () => {
|
||||
expect(h.elements.map((element) => element.id)).toEqual([
|
||||
"A",
|
||||
"B",
|
||||
getCloneByOrigId("A").id,
|
||||
getCloneByOrigId("B").id,
|
||||
"A_copy",
|
||||
"B_copy",
|
||||
"C",
|
||||
getCloneByOrigId("C").id,
|
||||
"C_copy",
|
||||
]);
|
||||
|
||||
populateElements(
|
||||
@@ -1031,9 +1031,9 @@ describe("z-index manipulation", () => {
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
getCloneByOrigId("A").id,
|
||||
getCloneByOrigId("B").id,
|
||||
getCloneByOrigId("C").id,
|
||||
"A_copy",
|
||||
"B_copy",
|
||||
"C_copy",
|
||||
]);
|
||||
|
||||
populateElements(
|
||||
@@ -1054,15 +1054,15 @@ describe("z-index manipulation", () => {
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
getCloneByOrigId("A").id,
|
||||
getCloneByOrigId("B").id,
|
||||
getCloneByOrigId("C").id,
|
||||
"A_copy",
|
||||
"B_copy",
|
||||
"C_copy",
|
||||
"D",
|
||||
"E",
|
||||
"F",
|
||||
getCloneByOrigId("D").id,
|
||||
getCloneByOrigId("E").id,
|
||||
getCloneByOrigId("F").id,
|
||||
"D_copy",
|
||||
"E_copy",
|
||||
"F_copy",
|
||||
]);
|
||||
|
||||
populateElements(
|
||||
@@ -1076,7 +1076,7 @@ describe("z-index manipulation", () => {
|
||||
API.executeAction(actionDuplicateSelection);
|
||||
expect(h.elements.map((element) => element.id)).toEqual([
|
||||
"A",
|
||||
getCloneByOrigId("A").id,
|
||||
"A_copy",
|
||||
"B",
|
||||
"C",
|
||||
]);
|
||||
@@ -1093,7 +1093,7 @@ describe("z-index manipulation", () => {
|
||||
expect(h.elements.map((element) => element.id)).toEqual([
|
||||
"A",
|
||||
"B",
|
||||
getCloneByOrigId("B").id,
|
||||
"B_copy",
|
||||
"C",
|
||||
]);
|
||||
|
||||
@@ -1108,9 +1108,9 @@ describe("z-index manipulation", () => {
|
||||
API.executeAction(actionDuplicateSelection);
|
||||
expect(h.elements.map((element) => element.id)).toEqual([
|
||||
"A",
|
||||
getCloneByOrigId("A").id,
|
||||
"A_copy",
|
||||
"B",
|
||||
getCloneByOrigId("B").id,
|
||||
"B_copy",
|
||||
"C",
|
||||
]);
|
||||
});
|
||||
@@ -1125,8 +1125,8 @@ describe("z-index manipulation", () => {
|
||||
expect(h.elements.map((element) => element.id)).toEqual([
|
||||
"A",
|
||||
"C",
|
||||
getCloneByOrigId("A").id,
|
||||
getCloneByOrigId("C").id,
|
||||
"A_copy",
|
||||
"C_copy",
|
||||
"B",
|
||||
]);
|
||||
});
|
||||
@@ -1144,9 +1144,9 @@ describe("z-index manipulation", () => {
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
getCloneByOrigId("A").id,
|
||||
getCloneByOrigId("B").id,
|
||||
getCloneByOrigId("C").id,
|
||||
"A_copy",
|
||||
"B_copy",
|
||||
"C_copy",
|
||||
"D",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -512,22 +512,6 @@ export interface ExcalidrawProps {
|
||||
data: ClipboardData,
|
||||
event: ClipboardEvent | null,
|
||||
) => Promise<boolean> | boolean;
|
||||
/**
|
||||
* Called when element(s) are duplicated so you can listen or modify as
|
||||
* needed.
|
||||
*
|
||||
* Called when duplicating via mouse-drag, keyboard, paste, library insert
|
||||
* etc.
|
||||
*
|
||||
* Returned elements will be used in place of the next elements
|
||||
* (you should return all elements, including deleted, and not mutate
|
||||
* the element if changes are made)
|
||||
*/
|
||||
onDuplicate?: (
|
||||
nextElements: readonly ExcalidrawElement[],
|
||||
/** excludes the duplicated elements */
|
||||
prevElements: readonly ExcalidrawElement[],
|
||||
) => ExcalidrawElement[] | void;
|
||||
renderTopRightUI?: (
|
||||
isMobile: boolean,
|
||||
appState: UIAppState,
|
||||
|
||||
@@ -65,6 +65,3 @@ export type MakeBrand<T extends string> = {
|
||||
|
||||
/** Maybe just promise or already fulfilled one! */
|
||||
export type MaybePromise<T> = T | Promise<T>;
|
||||
|
||||
// get union of all keys from the union of types
|
||||
export type AllPossibleKeys<T> = T extends any ? keyof T : never;
|
||||
|
||||
@@ -9,11 +9,7 @@ import {
|
||||
isDarwin,
|
||||
WINDOWS_EMOJI_FALLBACK_FONT,
|
||||
} from "./constants";
|
||||
import type {
|
||||
ExcalidrawBindableElement,
|
||||
FontFamilyValues,
|
||||
FontString,
|
||||
} from "./element/types";
|
||||
import type { FontFamilyValues, FontString } from "./element/types";
|
||||
import type {
|
||||
ActiveTool,
|
||||
AppState,
|
||||
@@ -547,9 +543,6 @@ export const isTransparent = (color: string) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const isBindingFallthroughEnabled = (el: ExcalidrawBindableElement) =>
|
||||
el.fillStyle !== "solid" || isTransparent(el.backgroundColor);
|
||||
|
||||
export type ResolvablePromise<T> = Promise<T> & {
|
||||
resolve: [T] extends [undefined]
|
||||
? (value?: MaybePromise<Awaited<T>>) => void
|
||||
@@ -1240,6 +1233,3 @@ export class PromisePool<T> {
|
||||
export const escapeDoubleQuotes = (str: string) => {
|
||||
return str.replace(/"/g, """);
|
||||
};
|
||||
|
||||
export const castArray = <T>(value: T | T[]): T[] =>
|
||||
Array.isArray(value) ? value : [value];
|
||||
|
||||
@@ -7,9 +7,6 @@ import polyfill from "./packages/excalidraw/polyfill";
|
||||
import { testPolyfills } from "./packages/excalidraw/tests/helpers/polyfills";
|
||||
import { yellow } from "./packages/excalidraw/tests/helpers/colorize";
|
||||
|
||||
// mock for pep.js not working with setPointerCapture()
|
||||
HTMLElement.prototype.setPointerCapture = vi.fn();
|
||||
|
||||
Object.assign(globalThis, testPolyfills);
|
||||
|
||||
require("fake-indexeddb/auto");
|
||||
|
||||
257
yarn.lock
257
yarn.lock
@@ -1504,6 +1504,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
|
||||
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
|
||||
|
||||
"@esbuild/aix-ppc64@0.19.10":
|
||||
version "0.19.10"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.10.tgz#fb3922a0183d27446de00cf60d4f7baaadf98d84"
|
||||
integrity sha512-Q+mk96KJ+FZ30h9fsJl+67IjNJm3x2eX+GBWGmocAKgzp27cowCOOqSdscX80s0SpdFXZnIv/+1xD1EctFx96Q==
|
||||
|
||||
"@esbuild/aix-ppc64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f"
|
||||
@@ -1514,10 +1519,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537"
|
||||
integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==
|
||||
|
||||
"@esbuild/aix-ppc64@0.25.0":
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz#499600c5e1757a524990d5d92601f0ac3ce87f64"
|
||||
integrity sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==
|
||||
"@esbuild/android-arm64@0.19.10":
|
||||
version "0.19.10"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.10.tgz#ef31015416dd79398082409b77aaaa2ade4d531a"
|
||||
integrity sha512-1X4CClKhDgC3by7k8aOWZeBXQX8dHT5QAMCAQDArCLaYfkppoARvh0fit3X2Qs+MXDngKcHv6XXyQCpY0hkK1Q==
|
||||
|
||||
"@esbuild/android-arm64@0.19.12":
|
||||
version "0.19.12"
|
||||
@@ -1529,10 +1534,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9"
|
||||
integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==
|
||||
|
||||
"@esbuild/android-arm64@0.25.0":
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz#b9b8231561a1dfb94eb31f4ee056b92a985c324f"
|
||||
integrity sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==
|
||||
"@esbuild/android-arm@0.19.10":
|
||||
version "0.19.10"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.10.tgz#1c23c7e75473aae9fb323be5d9db225142f47f52"
|
||||
integrity sha512-7W0bK7qfkw1fc2viBfrtAEkDKHatYfHzr/jKAHNr9BvkYDXPcC6bodtm8AyLJNNuqClLNaeTLuwURt4PRT9d7w==
|
||||
|
||||
"@esbuild/android-arm@0.19.12":
|
||||
version "0.19.12"
|
||||
@@ -1544,10 +1549,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995"
|
||||
integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==
|
||||
|
||||
"@esbuild/android-arm@0.25.0":
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.0.tgz#ca6e7888942505f13e88ac9f5f7d2a72f9facd2b"
|
||||
integrity sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==
|
||||
"@esbuild/android-x64@0.19.10":
|
||||
version "0.19.10"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.10.tgz#df6a4e6d6eb8da5595cfce16d4e3f6bc24464707"
|
||||
integrity sha512-O/nO/g+/7NlitUxETkUv/IvADKuZXyH4BHf/g/7laqKC4i/7whLpB0gvpPc2zpF0q9Q6FXS3TS75QHac9MvVWw==
|
||||
|
||||
"@esbuild/android-x64@0.19.12":
|
||||
version "0.19.12"
|
||||
@@ -1559,10 +1564,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98"
|
||||
integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==
|
||||
|
||||
"@esbuild/android-x64@0.25.0":
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.0.tgz#e765ea753bac442dfc9cb53652ce8bd39d33e163"
|
||||
integrity sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==
|
||||
"@esbuild/darwin-arm64@0.19.10":
|
||||
version "0.19.10"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.10.tgz#8462a55db07c1b2fad61c8244ce04469ef1043be"
|
||||
integrity sha512-YSRRs2zOpwypck+6GL3wGXx2gNP7DXzetmo5pHXLrY/VIMsS59yKfjPizQ4lLt5vEI80M41gjm2BxrGZ5U+VMA==
|
||||
|
||||
"@esbuild/darwin-arm64@0.19.12":
|
||||
version "0.19.12"
|
||||
@@ -1574,10 +1579,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb"
|
||||
integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==
|
||||
|
||||
"@esbuild/darwin-arm64@0.25.0":
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz#fa394164b0d89d4fdc3a8a21989af70ef579fa2c"
|
||||
integrity sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==
|
||||
"@esbuild/darwin-x64@0.19.10":
|
||||
version "0.19.10"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.10.tgz#d1de20bfd41bb75b955ba86a6b1004539e8218c1"
|
||||
integrity sha512-alfGtT+IEICKtNE54hbvPg13xGBe4GkVxyGWtzr+yHO7HIiRJppPDhOKq3zstTcVf8msXb/t4eavW3jCDpMSmA==
|
||||
|
||||
"@esbuild/darwin-x64@0.19.12":
|
||||
version "0.19.12"
|
||||
@@ -1589,10 +1594,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0"
|
||||
integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==
|
||||
|
||||
"@esbuild/darwin-x64@0.25.0":
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz#91979d98d30ba6e7d69b22c617cc82bdad60e47a"
|
||||
integrity sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==
|
||||
"@esbuild/freebsd-arm64@0.19.10":
|
||||
version "0.19.10"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.10.tgz#16904879e34c53a2e039d1284695d2db3e664d57"
|
||||
integrity sha512-dMtk1wc7FSH8CCkE854GyGuNKCewlh+7heYP/sclpOG6Cectzk14qdUIY5CrKDbkA/OczXq9WesqnPl09mj5dg==
|
||||
|
||||
"@esbuild/freebsd-arm64@0.19.12":
|
||||
version "0.19.12"
|
||||
@@ -1604,10 +1609,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911"
|
||||
integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==
|
||||
|
||||
"@esbuild/freebsd-arm64@0.25.0":
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz#b97e97073310736b430a07b099d837084b85e9ce"
|
||||
integrity sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==
|
||||
"@esbuild/freebsd-x64@0.19.10":
|
||||
version "0.19.10"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.10.tgz#8ad9e5ca9786ca3f1ef1411bfd10b08dcd9d4cef"
|
||||
integrity sha512-G5UPPspryHu1T3uX8WiOEUa6q6OlQh6gNl4CO4Iw5PS+Kg5bVggVFehzXBJY6X6RSOMS8iXDv2330VzaObm4Ag==
|
||||
|
||||
"@esbuild/freebsd-x64@0.19.12":
|
||||
version "0.19.12"
|
||||
@@ -1619,10 +1624,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c"
|
||||
integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==
|
||||
|
||||
"@esbuild/freebsd-x64@0.25.0":
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz#f3b694d0da61d9910ec7deff794d444cfbf3b6e7"
|
||||
integrity sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==
|
||||
"@esbuild/linux-arm64@0.19.10":
|
||||
version "0.19.10"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.10.tgz#d82cf2c590faece82d28bbf1cfbe36f22ae25bd2"
|
||||
integrity sha512-QxaouHWZ+2KWEj7cGJmvTIHVALfhpGxo3WLmlYfJ+dA5fJB6lDEIg+oe/0//FuyVHuS3l79/wyBxbHr0NgtxJQ==
|
||||
|
||||
"@esbuild/linux-arm64@0.19.12":
|
||||
version "0.19.12"
|
||||
@@ -1634,10 +1639,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5"
|
||||
integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==
|
||||
|
||||
"@esbuild/linux-arm64@0.25.0":
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz#f921f699f162f332036d5657cad9036f7a993f73"
|
||||
integrity sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==
|
||||
"@esbuild/linux-arm@0.19.10":
|
||||
version "0.19.10"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.10.tgz#477b8e7c7bcd34369717b04dd9ee6972c84f4029"
|
||||
integrity sha512-j6gUW5aAaPgD416Hk9FHxn27On28H4eVI9rJ4az7oCGTFW48+LcgNDBN+9f8rKZz7EEowo889CPKyeaD0iw9Kg==
|
||||
|
||||
"@esbuild/linux-arm@0.19.12":
|
||||
version "0.19.12"
|
||||
@@ -1649,10 +1654,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c"
|
||||
integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==
|
||||
|
||||
"@esbuild/linux-arm@0.25.0":
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz#cc49305b3c6da317c900688995a4050e6cc91ca3"
|
||||
integrity sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==
|
||||
"@esbuild/linux-ia32@0.19.10":
|
||||
version "0.19.10"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.10.tgz#d55ff822cf5b0252a57112f86857ff23be6cab0e"
|
||||
integrity sha512-4ub1YwXxYjj9h1UIZs2hYbnTZBtenPw5NfXCRgEkGb0b6OJ2gpkMvDqRDYIDRjRdWSe/TBiZltm3Y3Q8SN1xNg==
|
||||
|
||||
"@esbuild/linux-ia32@0.19.12":
|
||||
version "0.19.12"
|
||||
@@ -1664,10 +1669,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa"
|
||||
integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==
|
||||
|
||||
"@esbuild/linux-ia32@0.25.0":
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz#3e0736fcfab16cff042dec806247e2c76e109e19"
|
||||
integrity sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==
|
||||
"@esbuild/linux-loong64@0.19.10":
|
||||
version "0.19.10"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.10.tgz#a9ad057d7e48d6c9f62ff50f6f208e331c4543c7"
|
||||
integrity sha512-lo3I9k+mbEKoxtoIbM0yC/MZ1i2wM0cIeOejlVdZ3D86LAcFXFRdeuZmh91QJvUTW51bOK5W2BznGNIl4+mDaA==
|
||||
|
||||
"@esbuild/linux-loong64@0.19.12":
|
||||
version "0.19.12"
|
||||
@@ -1679,10 +1684,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5"
|
||||
integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==
|
||||
|
||||
"@esbuild/linux-loong64@0.25.0":
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz#ea2bf730883cddb9dfb85124232b5a875b8020c7"
|
||||
integrity sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==
|
||||
"@esbuild/linux-mips64el@0.19.10":
|
||||
version "0.19.10"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.10.tgz#b011a96924773d60ebab396fbd7a08de66668179"
|
||||
integrity sha512-J4gH3zhHNbdZN0Bcr1QUGVNkHTdpijgx5VMxeetSk6ntdt+vR1DqGmHxQYHRmNb77tP6GVvD+K0NyO4xjd7y4A==
|
||||
|
||||
"@esbuild/linux-mips64el@0.19.12":
|
||||
version "0.19.12"
|
||||
@@ -1694,10 +1699,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa"
|
||||
integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==
|
||||
|
||||
"@esbuild/linux-mips64el@0.25.0":
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz#4cababb14eede09248980a2d2d8b966464294ff1"
|
||||
integrity sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==
|
||||
"@esbuild/linux-ppc64@0.19.10":
|
||||
version "0.19.10"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.10.tgz#5d8b59929c029811e473f2544790ea11d588d4dd"
|
||||
integrity sha512-tgT/7u+QhV6ge8wFMzaklOY7KqiyitgT1AUHMApau32ZlvTB/+efeCtMk4eXS+uEymYK249JsoiklZN64xt6oQ==
|
||||
|
||||
"@esbuild/linux-ppc64@0.19.12":
|
||||
version "0.19.12"
|
||||
@@ -1709,10 +1714,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20"
|
||||
integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==
|
||||
|
||||
"@esbuild/linux-ppc64@0.25.0":
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz#8860a4609914c065373a77242e985179658e1951"
|
||||
integrity sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==
|
||||
"@esbuild/linux-riscv64@0.19.10":
|
||||
version "0.19.10"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.10.tgz#292b06978375b271bd8bc0a554e0822957508d22"
|
||||
integrity sha512-0f/spw0PfBMZBNqtKe5FLzBDGo0SKZKvMl5PHYQr3+eiSscfJ96XEknCe+JoOayybWUFQbcJTrk946i3j9uYZA==
|
||||
|
||||
"@esbuild/linux-riscv64@0.19.12":
|
||||
version "0.19.12"
|
||||
@@ -1724,10 +1729,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300"
|
||||
integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==
|
||||
|
||||
"@esbuild/linux-riscv64@0.25.0":
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz#baf26e20bb2d38cfb86ee282dff840c04f4ed987"
|
||||
integrity sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==
|
||||
"@esbuild/linux-s390x@0.19.10":
|
||||
version "0.19.10"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.10.tgz#d30af63530f8d4fa96930374c9dd0d62bf59e069"
|
||||
integrity sha512-pZFe0OeskMHzHa9U38g+z8Yx5FNCLFtUnJtQMpwhS+r4S566aK2ci3t4NCP4tjt6d5j5uo4h7tExZMjeKoehAA==
|
||||
|
||||
"@esbuild/linux-s390x@0.19.12":
|
||||
version "0.19.12"
|
||||
@@ -1739,10 +1744,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685"
|
||||
integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==
|
||||
|
||||
"@esbuild/linux-s390x@0.25.0":
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz#8323afc0d6cb1b6dc6e9fd21efd9e1542c3640a4"
|
||||
integrity sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==
|
||||
"@esbuild/linux-x64@0.19.10":
|
||||
version "0.19.10"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.10.tgz#898c72eeb74d9f2fb43acf316125b475548b75ce"
|
||||
integrity sha512-SpYNEqg/6pZYoc+1zLCjVOYvxfZVZj6w0KROZ3Fje/QrM3nfvT2llI+wmKSrWuX6wmZeTapbarvuNNK/qepSgA==
|
||||
|
||||
"@esbuild/linux-x64@0.19.12":
|
||||
version "0.19.12"
|
||||
@@ -1754,15 +1759,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff"
|
||||
integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==
|
||||
|
||||
"@esbuild/linux-x64@0.25.0":
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz#08fcf60cb400ed2382e9f8e0f5590bac8810469a"
|
||||
integrity sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==
|
||||
|
||||
"@esbuild/netbsd-arm64@0.25.0":
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz#935c6c74e20f7224918fbe2e6c6fe865b6c6ea5b"
|
||||
integrity sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==
|
||||
"@esbuild/netbsd-x64@0.19.10":
|
||||
version "0.19.10"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.10.tgz#fd473a5ae261b43eab6dad4dbd5a3155906e6c91"
|
||||
integrity sha512-ACbZ0vXy9zksNArWlk2c38NdKg25+L9pr/mVaj9SUq6lHZu/35nx2xnQVRGLrC1KKQqJKRIB0q8GspiHI3J80Q==
|
||||
|
||||
"@esbuild/netbsd-x64@0.19.12":
|
||||
version "0.19.12"
|
||||
@@ -1774,15 +1774,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6"
|
||||
integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==
|
||||
|
||||
"@esbuild/netbsd-x64@0.25.0":
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz#414677cef66d16c5a4d210751eb2881bb9c1b62b"
|
||||
integrity sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==
|
||||
|
||||
"@esbuild/openbsd-arm64@0.25.0":
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz#8fd55a4d08d25cdc572844f13c88d678c84d13f7"
|
||||
integrity sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==
|
||||
"@esbuild/openbsd-x64@0.19.10":
|
||||
version "0.19.10"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.10.tgz#96eb8992e526717b5272321eaad3e21f3a608e46"
|
||||
integrity sha512-PxcgvjdSjtgPMiPQrM3pwSaG4kGphP+bLSb+cihuP0LYdZv1epbAIecHVl5sD3npkfYBZ0ZnOjR878I7MdJDFg==
|
||||
|
||||
"@esbuild/openbsd-x64@0.19.12":
|
||||
version "0.19.12"
|
||||
@@ -1794,10 +1789,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf"
|
||||
integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==
|
||||
|
||||
"@esbuild/openbsd-x64@0.25.0":
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz#0c48ddb1494bbc2d6bcbaa1429a7f465fa1dedde"
|
||||
integrity sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==
|
||||
"@esbuild/sunos-x64@0.19.10":
|
||||
version "0.19.10"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.10.tgz#c16ee1c167f903eaaa6acf7372bee42d5a89c9bc"
|
||||
integrity sha512-ZkIOtrRL8SEJjr+VHjmW0znkPs+oJXhlJbNwfI37rvgeMtk3sxOQevXPXjmAPZPigVTncvFqLMd+uV0IBSEzqA==
|
||||
|
||||
"@esbuild/sunos-x64@0.19.12":
|
||||
version "0.19.12"
|
||||
@@ -1809,10 +1804,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f"
|
||||
integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==
|
||||
|
||||
"@esbuild/sunos-x64@0.25.0":
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz#86ff9075d77962b60dd26203d7352f92684c8c92"
|
||||
integrity sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==
|
||||
"@esbuild/win32-arm64@0.19.10":
|
||||
version "0.19.10"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.10.tgz#7e417d1971dbc7e469b4eceb6a5d1d667b5e3dcc"
|
||||
integrity sha512-+Sa4oTDbpBfGpl3Hn3XiUe4f8TU2JF7aX8cOfqFYMMjXp6ma6NJDztl5FDG8Ezx0OjwGikIHw+iA54YLDNNVfw==
|
||||
|
||||
"@esbuild/win32-arm64@0.19.12":
|
||||
version "0.19.12"
|
||||
@@ -1824,10 +1819,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90"
|
||||
integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==
|
||||
|
||||
"@esbuild/win32-arm64@0.25.0":
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz#849c62327c3229467f5b5cd681bf50588442e96c"
|
||||
integrity sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==
|
||||
"@esbuild/win32-ia32@0.19.10":
|
||||
version "0.19.10"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.10.tgz#2b52dfec6cd061ecb36171c13bae554888b439e5"
|
||||
integrity sha512-EOGVLK1oWMBXgfttJdPHDTiivYSjX6jDNaATeNOaCOFEVcfMjtbx7WVQwPSE1eIfCp/CaSF2nSrDtzc4I9f8TQ==
|
||||
|
||||
"@esbuild/win32-ia32@0.19.12":
|
||||
version "0.19.12"
|
||||
@@ -1839,10 +1834,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23"
|
||||
integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==
|
||||
|
||||
"@esbuild/win32-ia32@0.25.0":
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz#f62eb480cd7cca088cb65bb46a6db25b725dc079"
|
||||
integrity sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==
|
||||
"@esbuild/win32-x64@0.19.10":
|
||||
version "0.19.10"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.10.tgz#bd123a74f243d2f3a1f046447bb9b363ee25d072"
|
||||
integrity sha512-whqLG6Sc70AbU73fFYvuYzaE4MNMBIlR1Y/IrUeOXFrWHxBEjjbZaQ3IXIQS8wJdAzue2GwYZCjOrgrU1oUHoA==
|
||||
|
||||
"@esbuild/win32-x64@0.19.12":
|
||||
version "0.19.12"
|
||||
@@ -1854,11 +1849,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc"
|
||||
integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==
|
||||
|
||||
"@esbuild/win32-x64@0.25.0":
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz#c8e119a30a7c8d60b9d2e22d2073722dde3b710b"
|
||||
integrity sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==
|
||||
|
||||
"@eslint-community/eslint-utils@^4.2.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
|
||||
@@ -3926,11 +3916,6 @@ ansi-styles@^6.0.0, ansi-styles@^6.1.0:
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
|
||||
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
|
||||
|
||||
ansicolor@2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/ansicolor/-/ansicolor-2.0.3.tgz#ec4448ae5baf8c2d62bf2dad52eac06ba0b5ea21"
|
||||
integrity sha512-pzusTqk9VHrjgMCcTPDTTvfJfx6Q3+L5tQ6yKC8Diexmoit4YROTFIkxFvRTNL9y5s0Q8HrSrgerCD5bIC+Kiw==
|
||||
|
||||
anymatch@~3.1.2:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
|
||||
@@ -5727,36 +5712,34 @@ esbuild-sass-plugin@2.16.0:
|
||||
resolve "^1.22.6"
|
||||
sass "^1.7.3"
|
||||
|
||||
esbuild@0.25.0:
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.0.tgz#0de1787a77206c5a79eeb634a623d39b5006ce92"
|
||||
integrity sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==
|
||||
esbuild@0.19.10:
|
||||
version "0.19.10"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.10.tgz#55e83e4a6b702e3498b9f872d84bfb4ebcb6d16e"
|
||||
integrity sha512-S1Y27QGt/snkNYrRcswgRFqZjaTG5a5xM3EQo97uNBnH505pdzSNe/HLBq1v0RO7iK/ngdbhJB6mDAp0OK+iUA==
|
||||
optionalDependencies:
|
||||
"@esbuild/aix-ppc64" "0.25.0"
|
||||
"@esbuild/android-arm" "0.25.0"
|
||||
"@esbuild/android-arm64" "0.25.0"
|
||||
"@esbuild/android-x64" "0.25.0"
|
||||
"@esbuild/darwin-arm64" "0.25.0"
|
||||
"@esbuild/darwin-x64" "0.25.0"
|
||||
"@esbuild/freebsd-arm64" "0.25.0"
|
||||
"@esbuild/freebsd-x64" "0.25.0"
|
||||
"@esbuild/linux-arm" "0.25.0"
|
||||
"@esbuild/linux-arm64" "0.25.0"
|
||||
"@esbuild/linux-ia32" "0.25.0"
|
||||
"@esbuild/linux-loong64" "0.25.0"
|
||||
"@esbuild/linux-mips64el" "0.25.0"
|
||||
"@esbuild/linux-ppc64" "0.25.0"
|
||||
"@esbuild/linux-riscv64" "0.25.0"
|
||||
"@esbuild/linux-s390x" "0.25.0"
|
||||
"@esbuild/linux-x64" "0.25.0"
|
||||
"@esbuild/netbsd-arm64" "0.25.0"
|
||||
"@esbuild/netbsd-x64" "0.25.0"
|
||||
"@esbuild/openbsd-arm64" "0.25.0"
|
||||
"@esbuild/openbsd-x64" "0.25.0"
|
||||
"@esbuild/sunos-x64" "0.25.0"
|
||||
"@esbuild/win32-arm64" "0.25.0"
|
||||
"@esbuild/win32-ia32" "0.25.0"
|
||||
"@esbuild/win32-x64" "0.25.0"
|
||||
"@esbuild/aix-ppc64" "0.19.10"
|
||||
"@esbuild/android-arm" "0.19.10"
|
||||
"@esbuild/android-arm64" "0.19.10"
|
||||
"@esbuild/android-x64" "0.19.10"
|
||||
"@esbuild/darwin-arm64" "0.19.10"
|
||||
"@esbuild/darwin-x64" "0.19.10"
|
||||
"@esbuild/freebsd-arm64" "0.19.10"
|
||||
"@esbuild/freebsd-x64" "0.19.10"
|
||||
"@esbuild/linux-arm" "0.19.10"
|
||||
"@esbuild/linux-arm64" "0.19.10"
|
||||
"@esbuild/linux-ia32" "0.19.10"
|
||||
"@esbuild/linux-loong64" "0.19.10"
|
||||
"@esbuild/linux-mips64el" "0.19.10"
|
||||
"@esbuild/linux-ppc64" "0.19.10"
|
||||
"@esbuild/linux-riscv64" "0.19.10"
|
||||
"@esbuild/linux-s390x" "0.19.10"
|
||||
"@esbuild/linux-x64" "0.19.10"
|
||||
"@esbuild/netbsd-x64" "0.19.10"
|
||||
"@esbuild/openbsd-x64" "0.19.10"
|
||||
"@esbuild/sunos-x64" "0.19.10"
|
||||
"@esbuild/win32-arm64" "0.19.10"
|
||||
"@esbuild/win32-ia32" "0.19.10"
|
||||
"@esbuild/win32-x64" "0.19.10"
|
||||
|
||||
esbuild@^0.19.3:
|
||||
version "0.19.12"
|
||||
|
||||
Reference in New Issue
Block a user