Compare commits

..
36 changed files with 3550 additions and 2259 deletions
+1 -1
View File
@@ -41,7 +41,7 @@
"vite-plugin-ejs": "1.7.0", "vite-plugin-ejs": "1.7.0",
"vite-plugin-pwa": "0.17.4", "vite-plugin-pwa": "0.17.4",
"vite-plugin-svgr": "4.2.0", "vite-plugin-svgr": "4.2.0",
"vitest": "2.0.5", "vitest": "2.1.9",
"vitest-canvas-mock": "0.3.3" "vitest-canvas-mock": "0.3.3"
}, },
"engines": { "engines": {
@@ -0,0 +1,211 @@
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,6 +18,8 @@ import {
import { updateActiveTool } from "../utils"; import { updateActiveTool } from "../utils";
import { TrashIcon } from "../components/icons"; import { TrashIcon } from "../components/icons";
import { StoreAction } from "../store"; import { StoreAction } from "../store";
import { getContainerElement } from "../element/textElement";
import { getFrameChildren } from "../frame";
const deleteSelectedElements = ( const deleteSelectedElements = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
@@ -33,10 +35,50 @@ const deleteSelectedElements = (
const selectedElementIds: Record<ExcalidrawElement["id"], true> = {}; 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; let shouldSelectEditingGroup = true;
const nextElements = elements.map((el) => { const nextElements = elements.map((el) => {
if (appState.selectedElementIds[el.id]) { 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) { if (el.boundElements) {
el.boundElements.forEach((candidate) => { el.boundElements.forEach((candidate) => {
const bound = app.scene.getNonDeletedElementsMap().get(candidate.id); const bound = app.scene.getNonDeletedElementsMap().get(candidate.id);
@@ -59,7 +101,9 @@ const deleteSelectedElements = (
// if deleting a frame, remove the children from it and select them // if deleting a frame, remove the children from it and select them
if (el.frameId && framesToBeDeleted.has(el.frameId)) { if (el.frameId && framesToBeDeleted.has(el.frameId)) {
shouldSelectEditingGroup = false; shouldSelectEditingGroup = false;
selectedElementIds[el.id] = true; if (!isBoundToContainer(el)) {
selectedElementIds[el.id] = true;
}
return newElementWith(el, { frameId: null }); return newElementWith(el, { frameId: null });
} }
@@ -224,11 +268,13 @@ export const actionDeleteSelected = register({
storeAction: StoreAction.CAPTURE, storeAction: StoreAction.CAPTURE,
}; };
} }
let { elements: nextElements, appState: nextAppState } = let { elements: nextElements, appState: nextAppState } =
deleteSelectedElements(elements, appState, app); deleteSelectedElements(elements, appState, app);
fixBindingsAfterDeletion( fixBindingsAfterDeletion(
nextElements, nextElements,
elements.filter(({ id }) => appState.selectedElementIds[id]), nextElements.filter((el) => el.isDeleted),
); );
nextAppState = handleGroupEditingState(nextAppState, nextElements); nextAppState = handleGroupEditingState(nextAppState, nextElements);
@@ -0,0 +1,530 @@
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,7 +5,13 @@ import { duplicateElement, getNonDeletedElements } from "../element";
import { isSomeElementSelected } from "../scene"; import { isSomeElementSelected } from "../scene";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n"; import { t } from "../i18n";
import { arrayToMap, getShortcutKey } from "../utils"; import {
arrayToMap,
castArray,
findLastIndex,
getShortcutKey,
invariant,
} from "../utils";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { import {
selectGroupsForSelectedElements, selectGroupsForSelectedElements,
@@ -19,8 +25,13 @@ import { DEFAULT_GRID_SIZE } from "../constants";
import { import {
bindTextToShapeAfterDuplication, bindTextToShapeAfterDuplication,
getBoundTextElement, getBoundTextElement,
getContainerElement,
} from "../element/textElement"; } from "../element/textElement";
import { isBoundToContainer, isFrameLikeElement } from "../element/typeChecks"; import {
hasBoundTextElement,
isBoundToContainer,
isFrameLikeElement,
} from "../element/typeChecks";
import { normalizeElementOrder } from "../element/sortElements"; import { normalizeElementOrder } from "../element/sortElements";
import { DuplicateIcon } from "../components/icons"; import { DuplicateIcon } from "../components/icons";
import { import {
@@ -31,7 +42,6 @@ import {
excludeElementsInFramesFromSelection, excludeElementsInFramesFromSelection,
getSelectedElements, getSelectedElements,
} from "../scene/selection"; } from "../scene/selection";
import { syncMovedIndices } from "../fractionalIndex";
import { StoreAction } from "../store"; import { StoreAction } from "../store";
export const actionDuplicateSelection = register({ export const actionDuplicateSelection = register({
@@ -85,34 +95,66 @@ const duplicateElements = (
): Partial<ActionResult> => { ): Partial<ActionResult> => {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// step (1)
const sortedElements = normalizeElementOrder(elements);
const groupIdMap = new Map(); const groupIdMap = new Map();
const newElements: ExcalidrawElement[] = []; const newElements: ExcalidrawElement[] = [];
const oldElements: ExcalidrawElement[] = []; const oldElements: ExcalidrawElement[] = [];
const oldIdToDuplicatedId = new Map(); const oldIdToDuplicatedId = new Map();
const duplicatedElementsMap = new Map<string, ExcalidrawElement>(); const duplicatedElementsMap = new Map<string, ExcalidrawElement>();
const duplicateAndOffsetElement = (element: ExcalidrawElement) => { const elementsMap = arrayToMap(elements);
const newElement = duplicateElement(
appState.editingGroupId, const duplicateAndOffsetElement = <
groupIdMap, T extends ExcalidrawElement | ExcalidrawElement[],
element, >(
{ element: T,
x: element.x + DEFAULT_GRID_SIZE / 2, ): T extends ExcalidrawElement[]
y: element.y + DEFAULT_GRID_SIZE / 2, ? 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;
}, },
[],
); );
duplicatedElementsMap.set(newElement.id, newElement);
oldIdToDuplicatedId.set(element.id, newElement.id); return (
oldElements.push(element); Array.isArray(element) ? _newElements : _newElements[0] || null
newElements.push(newElement); ) as T extends ExcalidrawElement[]
return newElement; ? ExcalidrawElement[]
: ExcalidrawElement | null;
}; };
elements = normalizeElementOrder(elements);
const idsOfElementsToDuplicate = arrayToMap( const idsOfElementsToDuplicate = arrayToMap(
getSelectedElements(sortedElements, appState, { getSelectedElements(elements, appState, {
includeBoundTextElement: true, includeBoundTextElement: true,
includeElementsInFrames: true, includeElementsInFrames: true,
}), }),
@@ -130,122 +172,133 @@ const duplicateElements = (
// loop over them. // loop over them.
const processedIds = new Map<ExcalidrawElement["id"], true>(); const processedIds = new Map<ExcalidrawElement["id"], true>();
const markAsProcessed = (elements: ExcalidrawElement[]) => { const elementsWithClones: ExcalidrawElement[] = elements.slice();
for (const element of elements) {
processedIds.set(element.id, true); const insertAfterIndex = (
index: number,
elements: ExcalidrawElement | null | ExcalidrawElement[],
) => {
invariant(index !== -1, "targetIndex === -1 ");
if (!Array.isArray(elements) && !elements) {
return;
} }
return elements;
elementsWithClones.splice(index + 1, 0, ...castArray(elements));
}; };
const elementsWithClones: ExcalidrawElement[] = []; const frameIdsToDuplicate = new Set(
elements
.filter(
(el) => idsOfElementsToDuplicate.has(el.id) && isFrameLikeElement(el),
)
.map((el) => el.id),
);
let index = -1; for (const element of elements) {
if (processedIds.has(element.id)) {
while (++index < sortedElements.length) {
const element = sortedElements[index];
if (processedIds.get(element.id)) {
continue; continue;
} }
const boundTextElement = getBoundTextElement(element, arrayToMap(elements)); if (!idsOfElementsToDuplicate.has(element.id)) {
const isElementAFrameLike = isFrameLikeElement(element); continue;
}
if (idsOfElementsToDuplicate.get(element.id)) { // groups
// 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],
);
elementsWithClones.push( const groupId = getSelectedGroupForElement(appState, element);
...markAsProcessed([ if (groupId) {
...groupElements, const groupElements = getElementsInGroup(elements, groupId).flatMap(
...groupElements.map((element) => (element) =>
duplicateAndOffsetElement(element), isFrameLikeElement(element)
), ? [...getFrameChildren(elements, element.id), element]
]), : [element],
); );
continue;
}
if (boundTextElement) {
elementsWithClones.push(
...markAsProcessed([
element,
boundTextElement,
duplicateAndOffsetElement(element),
duplicateAndOffsetElement(boundTextElement),
]),
);
continue;
}
if (isElementAFrameLike) {
const elementsInFrame = getFrameChildren(sortedElements, element.id);
elementsWithClones.push( const targetIndex = findLastIndex(elementsWithClones, (el) => {
...markAsProcessed([ return el.groupIds?.includes(groupId);
...elementsInFrame, });
element,
...elementsInFrame.map((e) => duplicateAndOffsetElement(e)),
duplicateAndOffsetElement(element),
]),
);
continue; insertAfterIndex(targetIndex, duplicateAndOffsetElement(groupElements));
} continue;
} }
// 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, // frame duplication
// they will have been copied along with the frame atomically in the // -------------------------------------------------------------------------
// above branch, so we must skip those elements here
// if (element.frameId && frameIdsToDuplicate.has(element.frameId)) {
// now, for elements do not belong any frames or elements whose frames continue;
// 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)) { if (isFrameLikeElement(element)) {
elementsWithClones.push( const frameId = element.id;
...markAsProcessed([element, duplicateAndOffsetElement(element)]),
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));
} }
} else {
elementsWithClones.push(...markAsProcessed([element])); continue;
} }
}
// step (2) if (isBoundToContainer(element)) {
const container = getContainerElement(element, elementsMap);
// second pass to remove duplicates. We loop from the end as it's likelier const targetIndex = findLastIndex(elementsWithClones, (el) => {
// that the last elements are in the correct order (contiguous or otherwise). return el.id === element.id || el.id === container?.id;
// Thus we need to reverse as the last step (3). });
const finalElementsReversed: ExcalidrawElement[] = []; if (container) {
insertAfterIndex(
targetIndex,
duplicateAndOffsetElement([container, element]),
);
} else {
insertAfterIndex(targetIndex, duplicateAndOffsetElement(element));
}
const finalElementIds = new Map<ExcalidrawElement["id"], true>(); continue;
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) // default duplication (regular elements)
const finalElements = syncMovedIndices( // -------------------------------------------------------------------------
finalElementsReversed.reverse(),
arrayToMap(newElements), insertAfterIndex(
); findLastIndex(elementsWithClones, (el) => el.id === element.id),
duplicateAndOffsetElement(element),
);
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -260,7 +313,7 @@ const duplicateElements = (
oldIdToDuplicatedId, oldIdToDuplicatedId,
); );
bindElementsToFramesAfterDuplication( bindElementsToFramesAfterDuplication(
finalElements, elementsWithClones,
oldElements, oldElements,
oldIdToDuplicatedId, oldIdToDuplicatedId,
); );
@@ -269,7 +322,7 @@ const duplicateElements = (
excludeElementsInFramesFromSelection(newElements); excludeElementsInFramesFromSelection(newElements);
return { return {
elements: finalElements, elements: elementsWithClones,
appState: { appState: {
...appState, ...appState,
...selectGroupsForSelectedElements( ...selectGroupsForSelectedElements(
@@ -285,7 +338,7 @@ const duplicateElements = (
{}, {},
), ),
}, },
getNonDeletedElements(finalElements), getNonDeletedElements(elementsWithClones),
appState, appState,
null, null,
), ),
@@ -1605,6 +1605,8 @@ export const actionChangeArrowType = register({
elements, elements,
elementsMap, elementsMap,
appState.zoom, appState.zoom,
false,
true,
); );
const endHoveredElement = const endHoveredElement =
!newElement.endBinding && !newElement.endBinding &&
@@ -1613,6 +1615,8 @@ export const actionChangeArrowType = register({
elements, elements,
elementsMap, elementsMap,
appState.zoom, appState.zoom,
false,
true,
); );
const startElement = startHoveredElement const startElement = startHoveredElement
? startHoveredElement ? startHoveredElement
+17 -7
View File
@@ -5770,7 +5770,10 @@ class App extends React.Component<AppProps, AppState> {
}); });
} }
if (editingLinearElement?.lastUncommittedPoint != null) { if (editingLinearElement?.lastUncommittedPoint != null) {
this.maybeSuggestBindingAtCursor(scenePointer); this.maybeSuggestBindingAtCursor(
scenePointer,
editingLinearElement.elbowed,
);
} else { } else {
// causes stack overflow if not sync // causes stack overflow if not sync
flushSync(() => { flushSync(() => {
@@ -5790,7 +5793,7 @@ class App extends React.Component<AppProps, AppState> {
this.state.startBoundElement, this.state.startBoundElement,
); );
} else { } else {
this.maybeSuggestBindingAtCursor(scenePointer); this.maybeSuggestBindingAtCursor(scenePointer, false);
} }
} }
@@ -7728,6 +7731,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.state.zoom, this.state.zoom,
isElbowArrow(element), isElbowArrow(element),
isElbowArrow(element),
); );
this.scene.insertElement(element); this.scene.insertElement(element);
@@ -10005,15 +10009,20 @@ class App extends React.Component<AppProps, AppState> {
} }
}; };
private maybeSuggestBindingAtCursor = (pointerCoords: { private maybeSuggestBindingAtCursor = (
x: number; pointerCoords: {
y: number; x: number;
}): void => { y: number;
},
considerAll: boolean,
): void => {
const hoveredBindableElement = getHoveredElementForBinding( const hoveredBindableElement = getHoveredElementForBinding(
pointerCoords, pointerCoords,
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.state.zoom, this.state.zoom,
false,
considerAll,
); );
this.setState({ this.setState({
suggestedBindings: suggestedBindings:
@@ -10043,7 +10052,8 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.state.zoom, this.state.zoom,
isArrowElement(linearElement) && isElbowArrow(linearElement), isElbowArrow(linearElement),
isElbowArrow(linearElement),
); );
if ( if (
hoveredBindableElement != null && hoveredBindableElement != null &&
@@ -5,7 +5,7 @@ import { getLanguage, t } from "../i18n";
import clsx from "clsx"; import clsx from "clsx";
import Collapsible from "./Stats/Collapsible"; import Collapsible from "./Stats/Collapsible";
import { atom, useAtom } from "../editor-jotai"; import { atom, useAtom } from "../editor-jotai";
import { useDevice } from ".."; import { useDevice } from "./App";
import "./IconPicker.scss"; import "./IconPicker.scss";
@@ -63,6 +63,27 @@
max-width: 100%; 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 { .library-unit__checkbox {
position: absolute; position: absolute;
top: 0.125rem; top: 0.125rem;
@@ -70,7 +91,7 @@
margin: 0; margin: 0;
.Checkbox-box { .Checkbox-box {
margin: 0 !important; margin: 0;
width: 1rem; width: 1rem;
height: 1rem; height: 1rem;
border-radius: 4px; border-radius: 4px;
+10 -13
View File
@@ -1,10 +1,7 @@
@import "../css/variables.module.scss"; @import "../css/variables.module.scss";
.excalidraw { .excalidraw {
--Range-track-background: var(--button-bg); --slider-thumb-size: 16px;
--Range-track-background-active: var(--color-primary);
--Range-thumb-background: var(--color-on-surface);
--Range-legend-color: var(--text-primary-color);
.range-wrapper { .range-wrapper {
position: relative; position: relative;
@@ -16,7 +13,7 @@
width: 100%; width: 100%;
height: 4px; height: 4px;
-webkit-appearance: none; -webkit-appearance: none;
background: var(--Range-track-background); background: var(--color-slider-track);
border-radius: 2px; border-radius: 2px;
outline: none; outline: none;
} }
@@ -24,18 +21,18 @@
.range-input::-webkit-slider-thumb { .range-input::-webkit-slider-thumb {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
width: 20px; width: var(--slider-thumb-size);
height: 20px; height: var(--slider-thumb-size);
background: var(--Range-thumb-background); background: var(--color-slider-thumb);
border-radius: 50%; border-radius: 50%;
cursor: pointer; cursor: pointer;
border: none; border: none;
} }
.range-input::-moz-range-thumb { .range-input::-moz-range-thumb {
width: 20px; width: var(--slider-thumb-size);
height: 20px; height: var(--slider-thumb-size);
background: var(--Range-thumb-background); background: var(--color-slider-thumb);
border-radius: 50%; border-radius: 50%;
cursor: pointer; cursor: pointer;
border: none; border: none;
@@ -46,7 +43,7 @@
bottom: 0; bottom: 0;
transform: translateX(-50%); transform: translateX(-50%);
font-size: 12px; font-size: 12px;
color: var(--Range-legend-color); color: var(--text-primary-color);
} }
.zero-label { .zero-label {
@@ -54,6 +51,6 @@
bottom: 0; bottom: 0;
left: 4px; left: 4px;
font-size: 12px; font-size: 12px;
color: var(--Range-legend-color); color: var(--text-primary-color);
} }
} }
+1 -1
View File
@@ -34,7 +34,7 @@ export const Range = ({
const position = const position =
(value / 100) * (inputWidth - thumbWidth) + thumbWidth / 2; (value / 100) * (inputWidth - thumbWidth) + thumbWidth / 2;
valueElement.style.left = `${position}px`; valueElement.style.left = `${position}px`;
rangeElement.style.background = `linear-gradient(to right, var(--color-primary) 0%, var(--color-primary) ${value}%, var(--button-bg) ${value}%, var(--button-bg) 100%)`; 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%)`;
} }
}, [value]); }, [value]);
+3
View File
@@ -458,3 +458,6 @@ export const ARROW_TYPE: { [T in AppState["currentItemArrowType"]]: T } = {
export const DEFAULT_REDUCED_GLOBAL_ALPHA = 0.3; export const DEFAULT_REDUCED_GLOBAL_ALPHA = 0.3;
export const ELEMENT_LINK_KEY = "element"; export const ELEMENT_LINK_KEY = "element";
/** used in tests */
export const ORIG_ID = Symbol.for("__test__originalId__");
+7 -1
View File
@@ -649,15 +649,21 @@ body.excalidraw-cursor-resize * {
@include filledButtonOnCanvas; @include filledButtonOnCanvas;
} }
.App-mobile-menu,
.App-menu__left { .App-menu__left {
--button-border: transparent; --button-border: transparent;
--button-bg: var(--color-surface-mid); --button-bg: var(--color-surface-mid);
}
@at-root .excalidraw.theme--dark#{&} { @at-root .excalidraw.theme--dark#{&} {
.App-mobile-menu,
.App-menu__left {
--button-hover-bg: #363541; --button-hover-bg: #363541;
--button-bg: var(--color-surface-high); --button-bg: var(--color-surface-high);
} }
}
.App-menu__left {
.buttonList { .buttonList {
padding: 0.25rem 0; padding: 0.25rem 0;
} }
+5 -3
View File
@@ -32,7 +32,6 @@
0px 0px 3.1270833015441895px 0px rgba(0, 0, 0, 0.08), 0px 0px 3.1270833015441895px 0px rgba(0, 0, 0, 0.08),
0px 7px 14px 0px rgba(0, 0, 0, 0.05); 0px 7px 14px 0px rgba(0, 0, 0, 0.05);
--button-bg: var(--color-surface-mid);
--button-hover-bg: var(--color-surface-high); --button-hover-bg: var(--color-surface-high);
--button-active-bg: var(--color-surface-high); --button-active-bg: var(--color-surface-high);
--button-active-border: var(--color-brand-active); --button-active-border: var(--color-brand-active);
@@ -54,6 +53,9 @@
--scrollbar-thumb: var(--button-gray-2); --scrollbar-thumb: var(--button-gray-2);
--scrollbar-thumb-hover: var(--button-gray-3); --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), --modal-shadow: 0px 100px 80px rgba(0, 0, 0, 0.07),
0px 41.7776px 33.4221px rgba(0, 0, 0, 0.0503198), 0px 41.7776px 33.4221px rgba(0, 0, 0, 0.0503198),
0px 22.3363px 17.869px rgba(0, 0, 0, 0.0417275), 0px 22.3363px 17.869px rgba(0, 0, 0, 0.0417275),
@@ -172,8 +174,6 @@
--button-destructive-bg-color: #5a0000; --button-destructive-bg-color: #5a0000;
--button-destructive-color: #{$oc-red-3}; --button-destructive-color: #{$oc-red-3};
--button-bg: var(--color-surface-high);
--button-gray-1: #363636; --button-gray-1: #363636;
--button-gray-2: #272727; --button-gray-2: #272727;
--button-gray-3: #222; --button-gray-3: #222;
@@ -210,6 +210,8 @@
--scrollbar-thumb: #{$oc-gray-8}; --scrollbar-thumb: #{$oc-gray-8};
--scrollbar-thumb-hover: #{$oc-gray-7}; --scrollbar-thumb-hover: #{$oc-gray-7};
--color-slider-track: hsl(244, 23%, 39%);
// will be inverted to a lighter color. // will be inverted to a lighter color.
--color-selection: #3530c4; --color-selection: #3530c4;
+69 -3
View File
@@ -49,7 +49,11 @@ import type { ElementUpdate } from "./mutateElement";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import type Scene from "../scene/Scene"; import type Scene from "../scene/Scene";
import { LinearElementEditor } from "./linearElementEditor"; import { LinearElementEditor } from "./linearElementEditor";
import { arrayToMap, tupleToCoors } from "../utils"; import {
arrayToMap,
isBindingFallthroughEnabled,
tupleToCoors,
} from "../utils";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { getBoundTextElement, handleBindTextResize } from "./textElement"; import { getBoundTextElement, handleBindTextResize } from "./textElement";
import { aabbForElement, getElementShape, pointInsideBounds } from "../shapes"; import { aabbForElement, getElementShape, pointInsideBounds } from "../shapes";
@@ -75,6 +79,7 @@ import {
clamp, clamp,
} from "../../math"; } from "../../math";
import { segmentIntersectRectangleElement } from "../../utils/geometry/shape"; import { segmentIntersectRectangleElement } from "../../utils/geometry/shape";
import { getElementsAtPosition } from "../scene/comparisons";
export type SuggestedBinding = export type SuggestedBinding =
| NonDeleted<ExcalidrawBindableElement> | NonDeleted<ExcalidrawBindableElement>
@@ -425,7 +430,8 @@ export const maybeBindLinearElement = (
elements, elements,
elementsMap, elementsMap,
appState.zoom, appState.zoom,
isElbowArrow(linearElement) && isElbowArrow(linearElement), isElbowArrow(linearElement),
isElbowArrow(linearElement),
); );
if (hoveredElement !== null) { if (hoveredElement !== null) {
@@ -558,7 +564,64 @@ export const getHoveredElementForBinding = (
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
zoom?: AppState["zoom"], zoom?: AppState["zoom"],
fullShape?: boolean, fullShape?: boolean,
considerAllElements?: boolean,
): NonDeleted<ExcalidrawBindableElement> | null => { ): NonDeleted<ExcalidrawBindableElement> | null => {
if (considerAllElements) {
let cullRest = false;
const candidateElements = getElementsAtPosition(
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 = getElementAtPosition( const hoveredElement = getElementAtPosition(
elements, elements,
(element) => (element) =>
@@ -570,7 +633,8 @@ export const getHoveredElementForBinding = (
zoom, zoom,
// disable fullshape snapping for frame elements so we // disable fullshape snapping for frame elements so we
// can bind to frame children // can bind to frame children
fullShape && !isFrameLikeElement(element), (fullShape || !isBindingFallthroughEnabled(element)) &&
!isFrameLikeElement(element),
), ),
); );
@@ -1220,6 +1284,8 @@ const getElligibleElementForBindingElement = (
elements, elements,
elementsMap, elementsMap,
zoom, zoom,
isElbowArrow(linearElement),
isElbowArrow(linearElement),
); );
}; };
+63 -4
View File
@@ -20,11 +20,11 @@ import {
bindPointToSnapToElementOutline, bindPointToSnapToElementOutline,
distanceToBindableElement, distanceToBindableElement,
avoidRectangularCorner, avoidRectangularCorner,
getHoveredElementForBinding,
FIXED_BINDING_DISTANCE, FIXED_BINDING_DISTANCE,
getHeadingForElbowArrowSnap, getHeadingForElbowArrowSnap,
getGlobalFixedPointForBindableElement, getGlobalFixedPointForBindableElement,
snapToMid, snapToMid,
getHoveredElementForBinding,
} from "./binding"; } from "./binding";
import type { Bounds } from "./bounds"; import type { Bounds } from "./bounds";
import type { Heading } from "./heading"; import type { Heading } from "./heading";
@@ -244,6 +244,12 @@ const handleSegmentRenormalization = (
); );
} }
import.meta.env.DEV &&
invariant(
validateElbowPoints(nextPoints),
"Invalid elbow points with fixed segments",
);
return normalizeArrowElementUpdate( return normalizeArrowElementUpdate(
nextPoints, nextPoints,
filteredNextFixedSegments, filteredNextFixedSegments,
@@ -866,6 +872,8 @@ export const updateElbowArrowPoints = (
updates: { updates: {
points?: readonly LocalPoint[]; points?: readonly LocalPoint[];
fixedSegments?: FixedSegment[] | null; fixedSegments?: FixedSegment[] | null;
startBinding?: FixedPointBinding | null;
endBinding?: FixedPointBinding | null;
}, },
options?: { options?: {
isDragging?: boolean; isDragging?: boolean;
@@ -912,7 +920,11 @@ export const updateElbowArrowPoints = (
// 0. During all element replacement in the scene, we just need to renormalize // 0. During all element replacement in the scene, we just need to renormalize
// the arrow // the arrow
// TODO (dwelle,mtolmacs): Remove this once Scene.getScene() is removed // TODO (dwelle,mtolmacs): Remove this once Scene.getScene() is removed
if (elementsMap.size === 0 && updates.points) { if (
elementsMap.size === 0 &&
updates.points &&
validateElbowPoints(updates.points)
) {
return normalizeArrowElementUpdate( return normalizeArrowElementUpdate(
updates.points.map((p) => updates.points.map((p) =>
pointFrom<GlobalPoint>(arrow.x + p[0], arrow.y + p[1]), pointFrom<GlobalPoint>(arrow.x + p[0], arrow.y + p[1]),
@@ -943,17 +955,48 @@ export const updateElbowArrowPoints = (
hoveredStartElement, hoveredStartElement,
hoveredEndElement, hoveredEndElement,
...rest ...rest
} = getElbowArrowData(arrow, elementsMap, updatedPoints, options); } = 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,
);
const fixedSegments = updates.fixedSegments ?? arrow.fixedSegments ?? []; const fixedSegments = updates.fixedSegments ?? arrow.fixedSegments ?? [];
//// ////
// 1. Renormalize the arrow // 1. Renormalize the arrow
//// ////
if (!updates.points && !updates.fixedSegments) { if (
!updates.points &&
!updates.fixedSegments &&
!updates.startBinding &&
!updates.endBinding
) {
return handleSegmentRenormalization(arrow, elementsMap); 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 // 2. Just normal elbow arrow things
//// ////
@@ -1000,6 +1043,7 @@ export const updateElbowArrowPoints = (
//// ////
// 5. Handle resize // 5. Handle resize
////
if (updates.points && updates.fixedSegments) { if (updates.points && updates.fixedSegments) {
return updates; return updates;
} }
@@ -2110,6 +2154,7 @@ const getHoveredElements = (
nonDeletedSceneElementsMap, nonDeletedSceneElementsMap,
zoom, zoom,
true, true,
true,
), ),
getHoveredElementForBinding( getHoveredElementForBinding(
tupleToCoors(origEndGlobalPoint), tupleToCoors(origEndGlobalPoint),
@@ -2117,9 +2162,23 @@ const getHoveredElements = (
nonDeletedSceneElementsMap, nonDeletedSceneElementsMap,
zoom, zoom,
true, true,
true,
), ),
]; ];
}; };
const gridAddressesEqual = (a: GridAddress, b: GridAddress): boolean => const gridAddressesEqual = (a: GridAddress, b: GridAddress): boolean =>
a[0] === b[0] && a[1] === b[1]; 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);
+28 -9
View File
@@ -10,13 +10,15 @@ import {
import { bindLinearElement } from "./binding"; import { bindLinearElement } from "./binding";
import { LinearElementEditor } from "./linearElementEditor"; import { LinearElementEditor } from "./linearElementEditor";
import { newArrowElement, newElement } from "./newElement"; import { newArrowElement, newElement } from "./newElement";
import type { import type { SceneElementsMap } from "./types";
ElementsMap, import {
ExcalidrawBindableElement, type ElementsMap,
ExcalidrawElement, type ExcalidrawBindableElement,
ExcalidrawFlowchartNodeElement, type ExcalidrawElement,
NonDeletedSceneElementsMap, type ExcalidrawFlowchartNodeElement,
OrderedExcalidrawElement, type NonDeletedSceneElementsMap,
type Ordered,
type OrderedExcalidrawElement,
} from "./types"; } from "./types";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import type { AppState, PendingExcalidrawElements } from "../types"; import type { AppState, PendingExcalidrawElements } from "../types";
@@ -28,9 +30,10 @@ import {
isFrameElement, isFrameElement,
isFlowchartNodeElement, isFlowchartNodeElement,
} from "./typeChecks"; } from "./typeChecks";
import { invariant } from "../utils"; import { invariant, toBrandedType } from "../utils";
import { pointFrom, type LocalPoint } from "../../math"; import { pointFrom, type LocalPoint } from "../../math";
import { aabbForElement } from "../shapes"; import { aabbForElement } from "../shapes";
import { updateElbowArrowPoints } from "./elbowArrow";
type LinkDirection = "up" | "right" | "down" | "left"; type LinkDirection = "up" | "right" | "down" | "left";
@@ -467,7 +470,23 @@ const createBindingArrow = (
}, },
]); ]);
return bindingArrow; 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,
};
}; };
export class FlowChartNavigator { export class FlowChartNavigator {
@@ -444,6 +444,8 @@ export class LinearElementEditor {
elements, elements,
elementsMap, elementsMap,
appState.zoom, appState.zoom,
isElbowArrow(element),
isElbowArrow(element),
) )
: null; : null;
@@ -796,6 +798,7 @@ export class LinearElementEditor {
elements, elements,
elementsMap, elementsMap,
app.state.zoom, app.state.zoom,
linearElementEditor.elbowed,
), ),
}; };
+7 -2
View File
@@ -33,13 +33,16 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
// casting to any because can't use `in` operator // casting to any because can't use `in` operator
// (see https://github.com/microsoft/TypeScript/issues/21732) // (see https://github.com/microsoft/TypeScript/issues/21732)
const { points, fixedSegments, fileId } = updates as any; const { points, fixedSegments, fileId, startBinding, endBinding } =
updates as any;
if ( if (
isElbowArrow(element) && isElbowArrow(element) &&
(Object.keys(updates).length === 0 || // normalization case (Object.keys(updates).length === 0 || // normalization case
typeof points !== "undefined" || // repositioning typeof points !== "undefined" || // repositioning
typeof fixedSegments !== "undefined") // segment fixing typeof fixedSegments !== "undefined" || // segment fixing
typeof startBinding !== "undefined" ||
typeof endBinding !== "undefined") // manual binding to element
) { ) {
const elementsMap = toBrandedType<SceneElementsMap>( const elementsMap = toBrandedType<SceneElementsMap>(
Scene.getScene(element)?.getNonDeletedElementsMap() ?? new Map(), Scene.getScene(element)?.getNonDeletedElementsMap() ?? new Map(),
@@ -58,6 +61,8 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
{ {
fixedSegments, fixedSegments,
points, points,
startBinding,
endBinding,
}, },
{ {
isDragging: options?.isDragging, isDragging: options?.isDragging,
+22 -22
View File
@@ -45,6 +45,7 @@ import {
DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE,
DEFAULT_TEXT_ALIGN, DEFAULT_TEXT_ALIGN,
DEFAULT_VERTICAL_ALIGN, DEFAULT_VERTICAL_ALIGN,
ORIG_ID,
VERTICAL_ALIGN, VERTICAL_ALIGN,
} from "../constants"; } from "../constants";
import type { MarkOptional, Merge, Mutable } from "../utility-types"; import type { MarkOptional, Merge, Mutable } from "../utility-types";
@@ -592,26 +593,18 @@ export const deepCopyElement = <T extends ExcalidrawElement>(
return _deepCopyElement(val); 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. In test env it reuses the old + postfix * utility wrapper to generate new id.
* for test assertions.
*/ */
export const regenerateId = ( 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(); return randomId();
}; };
@@ -637,7 +630,11 @@ export const duplicateElement = <TElement extends ExcalidrawElement>(
): Readonly<TElement> => { ): Readonly<TElement> => {
let copy = deepCopyElement(element); let copy = deepCopyElement(element);
copy.id = regenerateId(copy.id); if (isTestEnv()) {
__test__defineOrigId(copy, element.id);
}
copy.id = regenerateId();
copy.boundElements = null; copy.boundElements = null;
copy.updated = getUpdatedTimestamp(); copy.updated = getUpdatedTimestamp();
copy.seed = randomInteger(); copy.seed = randomInteger();
@@ -646,7 +643,7 @@ export const duplicateElement = <TElement extends ExcalidrawElement>(
editingGroupId, editingGroupId,
(groupId) => { (groupId) => {
if (!groupIdMapForOperation.has(groupId)) { if (!groupIdMapForOperation.has(groupId)) {
groupIdMapForOperation.set(groupId, regenerateId(groupId)); groupIdMapForOperation.set(groupId, regenerateId());
} }
return groupIdMapForOperation.get(groupId)!; return groupIdMapForOperation.get(groupId)!;
}, },
@@ -692,7 +689,7 @@ export const duplicateElements = (
// if we haven't migrated the element id, but an old element with the same // 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 // id exists, generate a new id for it and return it
if (origElementsMap.has(id)) { if (origElementsMap.has(id)) {
const newId = regenerateId(id); const newId = regenerateId();
elementNewIdsMap.set(id, newId); elementNewIdsMap.set(id, newId);
return newId; return newId;
} }
@@ -706,6 +703,9 @@ export const duplicateElements = (
const clonedElement: Mutable<ExcalidrawElement> = _deepCopyElement(element); const clonedElement: Mutable<ExcalidrawElement> = _deepCopyElement(element);
clonedElement.id = maybeGetNewId(element.id)!; clonedElement.id = maybeGetNewId(element.id)!;
if (isTestEnv()) {
__test__defineOrigId(clonedElement, element.id);
}
if (opts?.randomizeSeed) { if (opts?.randomizeSeed) {
clonedElement.seed = randomInteger(); clonedElement.seed = randomInteger();
@@ -715,7 +715,7 @@ export const duplicateElements = (
if (clonedElement.groupIds) { if (clonedElement.groupIds) {
clonedElement.groupIds = clonedElement.groupIds.map((groupId) => { clonedElement.groupIds = clonedElement.groupIds.map((groupId) => {
if (!groupNewIdsMap.has(groupId)) { if (!groupNewIdsMap.has(groupId)) {
groupNewIdsMap.set(groupId, regenerateId(groupId)); groupNewIdsMap.set(groupId, regenerateId());
} }
return groupNewIdsMap.get(groupId)!; return groupNewIdsMap.get(groupId)!;
}); });
+1 -4
View File
@@ -116,8 +116,5 @@ const normalizeBoundElementsOrder = (
export const normalizeElementOrder = ( export const normalizeElementOrder = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
) => { ) => {
// console.time(); return normalizeBoundElementsOrder(normalizeGroupElementOrder(elements));
const ret = normalizeBoundElementsOrder(normalizeGroupElementOrder(elements));
// console.timeEnd();
return ret;
}; };
+5 -6
View File
@@ -1,9 +1,8 @@
import React from "react";
import type { ExcalidrawElement } from "./element/types"; import type { ExcalidrawElement } from "./element/types";
import { convertToExcalidrawElements, Excalidraw } from "./index"; import { convertToExcalidrawElements, Excalidraw } from "./index";
import { API } from "./tests/helpers/api"; import { API } from "./tests/helpers/api";
import { Keyboard, Pointer } from "./tests/helpers/ui"; import { Keyboard, Pointer } from "./tests/helpers/ui";
import { render } from "./tests/test-utils"; import { getCloneByOrigId, render } from "./tests/test-utils";
const { h } = window; const { h } = window;
const mouse = new Pointer("mouse"); const mouse = new Pointer("mouse");
@@ -413,10 +412,10 @@ describe("adding elements to frames", () => {
dragElementIntoFrame(frame, rect2); dragElementIntoFrame(frame, rect2);
const rect2_copy = { ...rect2, id: `${rect2.id}_copy` };
selectElementAndDuplicate(rect2); selectElementAndDuplicate(rect2);
const rect2_copy = getCloneByOrigId(rect2.id);
expect(rect2_copy.frameId).toBe(frame.id); expect(rect2_copy.frameId).toBe(frame.id);
expect(rect2.frameId).toBe(frame.id); expect(rect2.frameId).toBe(frame.id);
expectEqualIds([rect2_copy, rect2, frame]); expectEqualIds([rect2_copy, rect2, frame]);
@@ -427,11 +426,11 @@ describe("adding elements to frames", () => {
dragElementIntoFrame(frame, rect2); dragElementIntoFrame(frame, rect2);
const rect2_copy = { ...rect2, id: `${rect2.id}_copy` };
// move the rect2 outside the frame // move the rect2 outside the frame
selectElementAndDuplicate(rect2, [-1000, -1000]); selectElementAndDuplicate(rect2, [-1000, -1000]);
const rect2_copy = getCloneByOrigId(rect2.id);
expect(rect2_copy.frameId).toBe(frame.id); expect(rect2_copy.frameId).toBe(frame.id);
expect(rect2.frameId).toBe(null); expect(rect2.frameId).toBe(null);
expectEqualIds([rect2_copy, frame, rect2]); expectEqualIds([rect2_copy, frame, rect2]);
+1
View File
@@ -103,6 +103,7 @@
"@types/pako": "1.0.3", "@types/pako": "1.0.3",
"@types/pica": "5.1.3", "@types/pica": "5.1.3",
"@types/resize-observer-browser": "0.1.7", "@types/resize-observer-browser": "0.1.7",
"ansicolor": "2.0.3",
"autoprefixer": "10.4.7", "autoprefixer": "10.4.7",
"babel-loader": "8.2.5", "babel-loader": "8.2.5",
"babel-plugin-transform-class-properties": "6.24.1", "babel-plugin-transform-class-properties": "6.24.1",
+18 -15
View File
@@ -75,20 +75,23 @@ export const getElementsAtPosition = (
isAtPositionFn: (element: NonDeletedExcalidrawElement) => boolean, isAtPositionFn: (element: NonDeletedExcalidrawElement) => boolean,
) => { ) => {
const iframeLikes: ExcalidrawIframeElement[] = []; const iframeLikes: ExcalidrawIframeElement[] = [];
// The parameter elements comes ordered from lower z-index to higher. const elementsAtPosition: NonDeletedExcalidrawElement[] = [];
// We want to preserve that order on the returned array. // We need to to hit testing from front (end of the array) to back (beginning of the array)
// Exception being embeddables which should be on top of everything else in // because array is ordered from lower z-index to highest and we want element z-index
// terms of hit testing. // with higher z-index
const elsAtPos = elements.filter((element) => { for (let index = elements.length - 1; index >= 0; --index) {
const hit = !element.isDeleted && isAtPositionFn(element); const element = elements[index];
if (hit) { if (element.isDeleted) {
if (isIframeElement(element)) { continue;
iframeLikes.push(element);
return false;
}
return true;
} }
return false; if (isIframeElement(element)) {
}); iframeLikes.push(element);
return elsAtPos.concat(iframeLikes); continue;
}
if (isAtPositionFn(element)) {
elementsAtPosition.push(element);
}
}
return elementsAtPosition.concat(iframeLikes);
}; };
@@ -2517,7 +2517,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"scrolledOutside": false, "scrolledOutside": false,
"searchMatches": [], "searchMatches": [],
"selectedElementIds": { "selectedElementIds": {
"id0_copy": true, "id1": true,
}, },
"selectedElementsAreBeingDragged": false, "selectedElementsAreBeingDragged": false,
"selectedGroupIds": {}, "selectedGroupIds": {},
@@ -2590,7 +2590,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 20, "height": 20,
"id": "id0_copy", "id": "id1",
"index": "a1", "index": "a1",
"isDeleted": false, "isDeleted": false,
"link": null, "link": null,
@@ -2680,7 +2680,7 @@ History {
"delta": Delta { "delta": Delta {
"deleted": { "deleted": {
"selectedElementIds": { "selectedElementIds": {
"id0_copy": true, "id1": true,
}, },
}, },
"inserted": { "inserted": {
@@ -2693,7 +2693,7 @@ History {
"elementsChange": ElementsChange { "elementsChange": ElementsChange {
"added": Map {}, "added": Map {},
"removed": Map { "removed": Map {
"id0_copy" => Delta { "id1" => Delta {
"deleted": { "deleted": {
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "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, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 50, "height": 50,
"id": "id0_copy", "id": "id2",
"index": "a0", "index": "a0",
"isDeleted": false, "isDeleted": false,
"link": null, "link": null,
@@ -2129,7 +2129,7 @@ History {
"elementsChange": ElementsChange { "elementsChange": ElementsChange {
"added": Map {}, "added": Map {},
"removed": Map { "removed": Map {
"id0_copy" => Delta { "id2" => Delta {
"deleted": { "deleted": {
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
@@ -10619,7 +10619,7 @@ History {
"elementsChange": ElementsChange { "elementsChange": ElementsChange {
"added": Map {}, "added": Map {},
"removed": Map { "removed": Map {
"id0_copy" => Delta { "id6" => Delta {
"deleted": { "deleted": {
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
@@ -10628,7 +10628,7 @@ History {
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [ "groupIds": [
"id4_copy", "id7",
], ],
"height": 10, "height": 10,
"index": "a0", "index": "a0",
@@ -10652,7 +10652,7 @@ History {
"isDeleted": true, "isDeleted": true,
}, },
}, },
"id1_copy" => Delta { "id8" => Delta {
"deleted": { "deleted": {
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
@@ -10661,7 +10661,7 @@ History {
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [ "groupIds": [
"id4_copy", "id7",
], ],
"height": 10, "height": 10,
"index": "a1", "index": "a1",
@@ -10685,7 +10685,7 @@ History {
"isDeleted": true, "isDeleted": true,
}, },
}, },
"id2_copy" => Delta { "id9" => Delta {
"deleted": { "deleted": {
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
@@ -10694,7 +10694,7 @@ History {
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [ "groupIds": [
"id4_copy", "id7",
], ],
"height": 10, "height": 10,
"index": "a2", "index": "a2",
+93 -6
View File
@@ -40,6 +40,7 @@ import { createTestHook } from "../../components/App";
import type { Action } from "../../actions/types"; import type { Action } from "../../actions/types";
import { mutateElement } from "../../element/mutateElement"; import { mutateElement } from "../../element/mutateElement";
import { pointFrom, type LocalPoint, type Radians } from "../../../math"; import { pointFrom, type LocalPoint, type Radians } from "../../../math";
import { selectGroupsForSelectedElements } from "../../groups";
const readFile = util.promisify(fs.readFile); const readFile = util.promisify(fs.readFile);
// so that window.h is available when App.tsx is not imported as well. // so that window.h is available when App.tsx is not imported as well.
@@ -68,13 +69,21 @@ export class API {
}); });
}; };
static setSelectedElements = (elements: ExcalidrawElement[]) => { static setSelectedElements = (elements: ExcalidrawElement[], editingGroupId?: string | null) => {
act(() => { act(() => {
h.setState({ h.setState({
selectedElementIds: elements.reduce((acc, element) => { ...selectGroupsForSelectedElements(
acc[element.id] = true; {
return acc; editingGroupId: editingGroupId ?? null,
}, {} as Record<ExcalidrawElement["id"], true>), selectedElementIds: elements.reduce((acc, element) => {
acc[element.id] = true;
return acc;
}, {} as Record<ExcalidrawElement["id"], true>),
},
elements,
h.state,
h.app,
)
}); });
}); });
}; };
@@ -158,7 +167,7 @@ export class API {
isDeleted?: boolean; isDeleted?: boolean;
frameId?: ExcalidrawElement["id"] | null; frameId?: ExcalidrawElement["id"] | null;
index?: ExcalidrawElement["index"]; index?: ExcalidrawElement["index"];
groupIds?: string[]; groupIds?: ExcalidrawElement["groupIds"];
// generic element props // generic element props
strokeColor?: ExcalidrawGenericElement["strokeColor"]; strokeColor?: ExcalidrawGenericElement["strokeColor"];
backgroundColor?: ExcalidrawGenericElement["backgroundColor"]; backgroundColor?: ExcalidrawGenericElement["backgroundColor"];
@@ -369,6 +378,84 @@ export class API {
return element as any; 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>( static readFile = async <T extends "utf8" | null>(
filepath: string, filepath: string,
encoding?: T, encoding?: T,
+10 -9
View File
@@ -7,6 +7,7 @@ import {
assertSelectedElements, assertSelectedElements,
render, render,
togglePopover, togglePopover,
getCloneByOrigId,
} from "./test-utils"; } from "./test-utils";
import { Excalidraw } from "../index"; import { Excalidraw } from "../index";
import { Keyboard, Pointer, UI } from "./helpers/ui"; import { Keyboard, Pointer, UI } from "./helpers/ui";
@@ -15,7 +16,7 @@ import { getDefaultAppState } from "../appState";
import { fireEvent, queryByTestId, waitFor } from "@testing-library/react"; import { fireEvent, queryByTestId, waitFor } from "@testing-library/react";
import { createUndoAction, createRedoAction } from "../actions/actionHistory"; import { createUndoAction, createRedoAction } from "../actions/actionHistory";
import { actionToggleViewMode } from "../actions/actionToggleViewMode"; import { actionToggleViewMode } from "../actions/actionToggleViewMode";
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants"; import { EXPORT_DATA_TYPES, MIME_TYPES, ORIG_ID } from "../constants";
import type { AppState } from "../types"; import type { AppState } from "../types";
import { arrayToMap } from "../utils"; import { arrayToMap } from "../utils";
import { import {
@@ -1138,8 +1139,8 @@ describe("history", () => {
expect(h.elements).toEqual([ expect(h.elements).toEqual([
expect.objectContaining({ id: rect1.id, isDeleted: false }), expect.objectContaining({ id: rect1.id, isDeleted: false }),
expect.objectContaining({ id: rect2.id, isDeleted: false }), expect.objectContaining({ id: rect2.id, isDeleted: false }),
expect.objectContaining({ id: `${rect1.id}_copy`, isDeleted: true }), expect.objectContaining({ [ORIG_ID]: rect1.id, isDeleted: true }),
expect.objectContaining({ id: `${rect2.id}_copy`, isDeleted: true }), expect.objectContaining({ [ORIG_ID]: rect2.id, isDeleted: true }),
]); ]);
expect(h.state.editingGroupId).toBeNull(); expect(h.state.editingGroupId).toBeNull();
expect(h.state.selectedGroupIds).toEqual({ A: true }); expect(h.state.selectedGroupIds).toEqual({ A: true });
@@ -1151,8 +1152,8 @@ describe("history", () => {
expect(h.elements).toEqual([ expect(h.elements).toEqual([
expect.objectContaining({ id: rect1.id, isDeleted: false }), expect.objectContaining({ id: rect1.id, isDeleted: false }),
expect.objectContaining({ id: rect2.id, isDeleted: false }), expect.objectContaining({ id: rect2.id, isDeleted: false }),
expect.objectContaining({ id: `${rect1.id}_copy`, isDeleted: false }), expect.objectContaining({ [ORIG_ID]: rect1.id, isDeleted: false }),
expect.objectContaining({ id: `${rect2.id}_copy`, isDeleted: false }), expect.objectContaining({ [ORIG_ID]: rect2.id, isDeleted: false }),
]); ]);
expect(h.state.editingGroupId).toBeNull(); expect(h.state.editingGroupId).toBeNull();
expect(h.state.selectedGroupIds).not.toEqual( expect(h.state.selectedGroupIds).not.toEqual(
@@ -1171,14 +1172,14 @@ describe("history", () => {
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ id: rect1.id, isDeleted: false }), expect.objectContaining({ id: rect1.id, isDeleted: false }),
expect.objectContaining({ id: rect2.id, isDeleted: false }), expect.objectContaining({ id: rect2.id, isDeleted: false }),
expect.objectContaining({ id: `${rect1.id}_copy`, isDeleted: true }), expect.objectContaining({ [ORIG_ID]: rect1.id, isDeleted: true }),
expect.objectContaining({ id: `${rect2.id}_copy`, isDeleted: true }), expect.objectContaining({ [ORIG_ID]: rect2.id, isDeleted: true }),
expect.objectContaining({ expect.objectContaining({
id: `${rect1.id}_copy_copy`, [ORIG_ID]: getCloneByOrigId(rect1.id)?.id,
isDeleted: false, isDeleted: false,
}), }),
expect.objectContaining({ expect.objectContaining({
id: `${rect2.id}_copy_copy`, [ORIG_ID]: getCloneByOrigId(rect2.id)?.id,
isDeleted: false, isDeleted: false,
}), }),
]), ]),
+28 -23
View File
@@ -1,11 +1,11 @@
import React from "react"; import React from "react";
import { vi } from "vitest"; import { vi } from "vitest";
import { fireEvent, render, waitFor } from "./test-utils"; import { fireEvent, getCloneByOrigId, render, waitFor } from "./test-utils";
import { act, queryByTestId } from "@testing-library/react"; import { act, queryByTestId } from "@testing-library/react";
import { Excalidraw } from "../index"; import { Excalidraw } from "../index";
import { API } from "./helpers/api"; import { API } from "./helpers/api";
import { MIME_TYPES } from "../constants"; import { MIME_TYPES, ORIG_ID } from "../constants";
import type { LibraryItem, LibraryItems } from "../types"; import type { LibraryItem, LibraryItems } from "../types";
import { UI } from "./helpers/ui"; import { UI } from "./helpers/ui";
import { serializeLibraryAsJSON } from "../data/json"; import { serializeLibraryAsJSON } from "../data/json";
@@ -76,7 +76,7 @@ describe("library", () => {
}), }),
); );
await waitFor(() => { await waitFor(() => {
expect(h.elements).toEqual([expect.objectContaining({ id: "A_copy" })]); expect(h.elements).toEqual([expect.objectContaining({ [ORIG_ID]: "A" })]);
}); });
}); });
@@ -125,23 +125,27 @@ describe("library", () => {
); );
await waitFor(() => { await waitFor(() => {
expect(h.elements).toEqual([ expect(h.elements).toEqual(
expect.objectContaining({ expect.arrayContaining([
id: "rectangle1_copy", expect.objectContaining({
boundElements: expect.arrayContaining([ [ORIG_ID]: "rectangle1",
{ type: "text", id: "text1_copy" }, boundElements: expect.arrayContaining([
{ type: "arrow", id: "arrow1_copy" }, { type: "text", id: getCloneByOrigId("text1").id },
]), { type: "arrow", id: getCloneByOrigId("arrow1").id },
}), ]),
expect.objectContaining({ }),
id: "text1_copy", expect.objectContaining({
containerId: "rectangle1_copy", [ORIG_ID]: "text1",
}), containerId: getCloneByOrigId("rectangle1").id,
expect.objectContaining({ }),
id: "arrow1_copy", expect.objectContaining({
endBinding: expect.objectContaining({ elementId: "rectangle1_copy" }), [ORIG_ID]: "arrow1",
}), endBinding: expect.objectContaining({
]); elementId: getCloneByOrigId("rectangle1").id,
}),
}),
]),
);
}); });
}); });
@@ -170,10 +174,11 @@ describe("library", () => {
await waitFor(() => { await waitFor(() => {
expect(h.elements).toEqual([ expect(h.elements).toEqual([
expect.objectContaining({ expect.objectContaining({
id: "elem1_copy", [ORIG_ID]: "elem1",
}), }),
expect.objectContaining({ expect.objectContaining({
id: expect.not.stringMatching(/^(elem1_copy|elem1)$/), id: expect.not.stringMatching(/^elem1$/),
[ORIG_ID]: expect.not.stringMatching(/^\w+$/),
}), }),
]); ]);
}); });
@@ -189,7 +194,7 @@ describe("library", () => {
}), }),
); );
await waitFor(() => { await waitFor(() => {
expect(h.elements).toEqual([expect.objectContaining({ id: "A_copy" })]); expect(h.elements).toEqual([expect.objectContaining({ [ORIG_ID]: "A" })]);
}); });
expect(h.state.activeTool.type).toBe("selection"); expect(h.state.activeTool.type).toBe("selection");
}); });
+151
View File
@@ -11,6 +11,10 @@ import { getSelectedElements } from "../scene/selection";
import type { ExcalidrawElement } from "../element/types"; import type { ExcalidrawElement } from "../element/types";
import { UI } from "./helpers/ui"; import { UI } from "./helpers/ui";
import { diffStringsUnified } from "jest-diff"; 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 = { const customQueries = {
...queries, ...queries,
@@ -295,3 +299,150 @@ 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);
};
+35 -35
View File
@@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { act, render } from "./test-utils"; import { act, getCloneByOrigId, render } from "./test-utils";
import { Excalidraw } from "../index"; import { Excalidraw } from "../index";
import { reseed } from "../random"; import { reseed } from "../random";
import { import {
@@ -916,9 +916,9 @@ describe("z-index manipulation", () => {
API.executeAction(actionDuplicateSelection); API.executeAction(actionDuplicateSelection);
expect(h.elements).toMatchObject([ expect(h.elements).toMatchObject([
{ id: "A" }, { id: "A" },
{ id: "A_copy" }, { id: getCloneByOrigId("A").id },
{ id: "B" }, { id: "B" },
{ id: "B_copy" }, { id: getCloneByOrigId("B").id },
]); ]);
populateElements([ populateElements([
@@ -930,12 +930,12 @@ describe("z-index manipulation", () => {
{ id: "A" }, { id: "A" },
{ id: "B" }, { id: "B" },
{ {
id: "A_copy", id: getCloneByOrigId("A").id,
groupIds: [expect.stringMatching(/.{3,}/)], groupIds: [expect.stringMatching(/.{3,}/)],
}, },
{ {
id: "B_copy", id: getCloneByOrigId("B").id,
groupIds: [expect.stringMatching(/.{3,}/)], groupIds: [expect.stringMatching(/.{3,}/)],
}, },
@@ -951,12 +951,12 @@ describe("z-index manipulation", () => {
{ id: "A" }, { id: "A" },
{ id: "B" }, { id: "B" },
{ {
id: "A_copy", id: getCloneByOrigId("A").id,
groupIds: [expect.stringMatching(/.{3,}/)], groupIds: [expect.stringMatching(/.{3,}/)],
}, },
{ {
id: "B_copy", id: getCloneByOrigId("B").id,
groupIds: [expect.stringMatching(/.{3,}/)], groupIds: [expect.stringMatching(/.{3,}/)],
}, },
@@ -972,10 +972,10 @@ describe("z-index manipulation", () => {
expect(h.elements.map((element) => element.id)).toEqual([ expect(h.elements.map((element) => element.id)).toEqual([
"A", "A",
"B", "B",
"A_copy", getCloneByOrigId("A").id,
"B_copy", getCloneByOrigId("B").id,
"C", "C",
"C_copy", getCloneByOrigId("C").id,
]); ]);
populateElements([ populateElements([
@@ -988,12 +988,12 @@ describe("z-index manipulation", () => {
expect(h.elements.map((element) => element.id)).toEqual([ expect(h.elements.map((element) => element.id)).toEqual([
"A", "A",
"B", "B",
"A_copy", getCloneByOrigId("A").id,
"B_copy", getCloneByOrigId("B").id,
"C", "C",
"D", "D",
"C_copy", getCloneByOrigId("C").id,
"D_copy", getCloneByOrigId("D").id,
]); ]);
populateElements( populateElements(
@@ -1010,10 +1010,10 @@ describe("z-index manipulation", () => {
expect(h.elements.map((element) => element.id)).toEqual([ expect(h.elements.map((element) => element.id)).toEqual([
"A", "A",
"B", "B",
"A_copy", getCloneByOrigId("A").id,
"B_copy", getCloneByOrigId("B").id,
"C", "C",
"C_copy", getCloneByOrigId("C").id,
]); ]);
populateElements( populateElements(
@@ -1031,9 +1031,9 @@ describe("z-index manipulation", () => {
"A", "A",
"B", "B",
"C", "C",
"A_copy", getCloneByOrigId("A").id,
"B_copy", getCloneByOrigId("B").id,
"C_copy", getCloneByOrigId("C").id,
]); ]);
populateElements( populateElements(
@@ -1054,15 +1054,15 @@ describe("z-index manipulation", () => {
"A", "A",
"B", "B",
"C", "C",
"A_copy", getCloneByOrigId("A").id,
"B_copy", getCloneByOrigId("B").id,
"C_copy", getCloneByOrigId("C").id,
"D", "D",
"E", "E",
"F", "F",
"D_copy", getCloneByOrigId("D").id,
"E_copy", getCloneByOrigId("E").id,
"F_copy", getCloneByOrigId("F").id,
]); ]);
populateElements( populateElements(
@@ -1076,7 +1076,7 @@ describe("z-index manipulation", () => {
API.executeAction(actionDuplicateSelection); API.executeAction(actionDuplicateSelection);
expect(h.elements.map((element) => element.id)).toEqual([ expect(h.elements.map((element) => element.id)).toEqual([
"A", "A",
"A_copy", getCloneByOrigId("A").id,
"B", "B",
"C", "C",
]); ]);
@@ -1093,7 +1093,7 @@ describe("z-index manipulation", () => {
expect(h.elements.map((element) => element.id)).toEqual([ expect(h.elements.map((element) => element.id)).toEqual([
"A", "A",
"B", "B",
"B_copy", getCloneByOrigId("B").id,
"C", "C",
]); ]);
@@ -1108,9 +1108,9 @@ describe("z-index manipulation", () => {
API.executeAction(actionDuplicateSelection); API.executeAction(actionDuplicateSelection);
expect(h.elements.map((element) => element.id)).toEqual([ expect(h.elements.map((element) => element.id)).toEqual([
"A", "A",
"A_copy", getCloneByOrigId("A").id,
"B", "B",
"B_copy", getCloneByOrigId("B").id,
"C", "C",
]); ]);
}); });
@@ -1125,8 +1125,8 @@ describe("z-index manipulation", () => {
expect(h.elements.map((element) => element.id)).toEqual([ expect(h.elements.map((element) => element.id)).toEqual([
"A", "A",
"C", "C",
"A_copy", getCloneByOrigId("A").id,
"C_copy", getCloneByOrigId("C").id,
"B", "B",
]); ]);
}); });
@@ -1144,9 +1144,9 @@ describe("z-index manipulation", () => {
"A", "A",
"B", "B",
"C", "C",
"A_copy", getCloneByOrigId("A").id,
"B_copy", getCloneByOrigId("B").id,
"C_copy", getCloneByOrigId("C").id,
"D", "D",
]); ]);
}); });
+3
View File
@@ -65,3 +65,6 @@ export type MakeBrand<T extends string> = {
/** Maybe just promise or already fulfilled one! */ /** Maybe just promise or already fulfilled one! */
export type MaybePromise<T> = T | Promise<T>; 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;
+11 -1
View File
@@ -9,7 +9,11 @@ import {
isDarwin, isDarwin,
WINDOWS_EMOJI_FALLBACK_FONT, WINDOWS_EMOJI_FALLBACK_FONT,
} from "./constants"; } from "./constants";
import type { FontFamilyValues, FontString } from "./element/types"; import type {
ExcalidrawBindableElement,
FontFamilyValues,
FontString,
} from "./element/types";
import type { import type {
ActiveTool, ActiveTool,
AppState, AppState,
@@ -543,6 +547,9 @@ export const isTransparent = (color: string) => {
); );
}; };
export const isBindingFallthroughEnabled = (el: ExcalidrawBindableElement) =>
el.fillStyle !== "solid" || isTransparent(el.backgroundColor);
export type ResolvablePromise<T> = Promise<T> & { export type ResolvablePromise<T> = Promise<T> & {
resolve: [T] extends [undefined] resolve: [T] extends [undefined]
? (value?: MaybePromise<Awaited<T>>) => void ? (value?: MaybePromise<Awaited<T>>) => void
@@ -1233,3 +1240,6 @@ export class PromisePool<T> {
export const escapeDoubleQuotes = (str: string) => { export const escapeDoubleQuotes = (str: string) => {
return str.replace(/"/g, "&quot;"); return str.replace(/"/g, "&quot;");
}; };
export const castArray = <T>(value: T | T[]): T[] =>
Array.isArray(value) ? value : [value];
+131 -132
View File
@@ -3443,46 +3443,62 @@
test-exclude "^7.0.1" test-exclude "^7.0.1"
tinyrainbow "^1.2.0" tinyrainbow "^1.2.0"
"@vitest/expect@2.0.5": "@vitest/expect@2.1.9":
version "2.0.5" version "2.1.9"
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.0.5.tgz#f3745a6a2c18acbea4d39f5935e913f40d26fa86" resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.9.tgz#b566ea20d58ea6578d8dc37040d6c1a47ebe5ff8"
integrity sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA== integrity sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==
dependencies: dependencies:
"@vitest/spy" "2.0.5" "@vitest/spy" "2.1.9"
"@vitest/utils" "2.0.5" "@vitest/utils" "2.1.9"
chai "^5.1.1" chai "^5.1.2"
tinyrainbow "^1.2.0" tinyrainbow "^1.2.0"
"@vitest/pretty-format@2.0.5", "@vitest/pretty-format@^2.0.5": "@vitest/mocker@2.1.9":
version "2.1.9"
resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.9.tgz#36243b27351ca8f4d0bbc4ef91594ffd2dc25ef5"
integrity sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==
dependencies:
"@vitest/spy" "2.1.9"
estree-walker "^3.0.3"
magic-string "^0.30.12"
"@vitest/pretty-format@2.0.5":
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.0.5.tgz#91d2e6d3a7235c742e1a6cc50e7786e2f2979b1e" resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.0.5.tgz#91d2e6d3a7235c742e1a6cc50e7786e2f2979b1e"
integrity sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ== integrity sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==
dependencies: dependencies:
tinyrainbow "^1.2.0" tinyrainbow "^1.2.0"
"@vitest/runner@2.0.5": "@vitest/pretty-format@2.1.9", "@vitest/pretty-format@^2.1.9":
version "2.0.5" version "2.1.9"
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.0.5.tgz#89197e712bb93513537d6876995a4843392b2a84" resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.9.tgz#434ff2f7611689f9ce70cd7d567eceb883653fdf"
integrity sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig== integrity sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==
dependencies: dependencies:
"@vitest/utils" "2.0.5" tinyrainbow "^1.2.0"
"@vitest/runner@2.1.9":
version "2.1.9"
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.9.tgz#cc18148d2d797fd1fd5908d1f1851d01459be2f6"
integrity sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==
dependencies:
"@vitest/utils" "2.1.9"
pathe "^1.1.2" pathe "^1.1.2"
"@vitest/snapshot@2.0.5": "@vitest/snapshot@2.1.9":
version "2.0.5" version "2.1.9"
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.0.5.tgz#a2346bc5013b73c44670c277c430e0334690a162" resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.9.tgz#24260b93f798afb102e2dcbd7e61c6dfa118df91"
integrity sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew== integrity sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==
dependencies: dependencies:
"@vitest/pretty-format" "2.0.5" "@vitest/pretty-format" "2.1.9"
magic-string "^0.30.10" magic-string "^0.30.12"
pathe "^1.1.2" pathe "^1.1.2"
"@vitest/spy@2.0.5": "@vitest/spy@2.1.9":
version "2.0.5" version "2.1.9"
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.0.5.tgz#590fc07df84a78b8e9dd976ec2090920084a2b9f" resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.9.tgz#cb28538c5039d09818b8bfa8edb4043c94727c60"
integrity sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA== integrity sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==
dependencies: dependencies:
tinyspy "^3.0.0" tinyspy "^3.0.2"
"@vitest/ui@2.0.5": "@vitest/ui@2.0.5":
version "2.0.5" version "2.0.5"
@@ -3507,6 +3523,15 @@
loupe "^3.1.1" loupe "^3.1.1"
tinyrainbow "^1.2.0" tinyrainbow "^1.2.0"
"@vitest/utils@2.1.9":
version "2.1.9"
resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.9.tgz#4f2486de8a54acf7ecbf2c5c24ad7994a680a6c1"
integrity sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==
dependencies:
"@vitest/pretty-format" "2.1.9"
loupe "^3.1.2"
tinyrainbow "^1.2.0"
"@webassemblyjs/ast@1.11.1": "@webassemblyjs/ast@1.11.1":
version "1.11.1" version "1.11.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"
@@ -3916,6 +3941,11 @@ ansi-styles@^6.0.0, ansi-styles@^6.1.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== 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: anymatch@~3.1.2:
version "3.1.3" version "3.1.3"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
@@ -4491,10 +4521,10 @@ chai@4.3.6:
pathval "^1.1.1" pathval "^1.1.1"
type-detect "^4.0.5" type-detect "^4.0.5"
chai@^5.1.1: chai@^5.1.2:
version "5.1.1" version "5.1.2"
resolved "https://registry.yarnpkg.com/chai/-/chai-5.1.1.tgz#f035d9792a22b481ead1c65908d14bb62ec1c82c" resolved "https://registry.yarnpkg.com/chai/-/chai-5.1.2.tgz#3afbc340b994ae3610ca519a6c70ace77ad4378d"
integrity sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA== integrity sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==
dependencies: dependencies:
assertion-error "^2.0.1" assertion-error "^2.0.1"
check-error "^2.1.1" check-error "^2.1.1"
@@ -5230,7 +5260,14 @@ dayjs@^1.11.7:
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.11.tgz#dfe0e9d54c5f8b68ccf8ca5f72ac603e7e5ed59e" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.11.tgz#dfe0e9d54c5f8b68ccf8ca5f72ac603e7e5ed59e"
integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg== integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==
debug@4, debug@4.3.4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.3, debug@^4.3.4, debug@^4.3.5, debug@^4.3.7:
version "4.4.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
dependencies:
ms "^2.1.3"
debug@4.3.4:
version "4.3.4" version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@@ -5251,7 +5288,7 @@ debug@^3.2.7:
dependencies: dependencies:
ms "^2.1.1" ms "^2.1.1"
debug@^4.3.5: debug@~4.3.1, debug@~4.3.2:
version "4.3.6" version "4.3.6"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b"
integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==
@@ -5662,6 +5699,11 @@ es-module-lexer@^1.2.1:
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.3.tgz#25969419de9c0b1fbe54279789023e8a9a788412" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.3.tgz#25969419de9c0b1fbe54279789023e8a9a788412"
integrity sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg== integrity sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg==
es-module-lexer@^1.5.4:
version "1.6.0"
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.6.0.tgz#da49f587fd9e68ee2404fe4e256c0c7d3a81be21"
integrity sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==
es-object-atoms@^1.0.0: es-object-atoms@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941"
@@ -6156,20 +6198,10 @@ execa@^5.1.1:
signal-exit "^3.0.3" signal-exit "^3.0.3"
strip-final-newline "^2.0.0" strip-final-newline "^2.0.0"
execa@^8.0.1: expect-type@^1.1.0:
version "8.0.1" version "1.1.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c" resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.1.0.tgz#a146e414250d13dfc49eafcfd1344a4060fa4c75"
integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== integrity sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==
dependencies:
cross-spawn "^7.0.3"
get-stream "^8.0.1"
human-signals "^5.0.0"
is-stream "^3.0.0"
merge-stream "^2.0.0"
npm-run-path "^5.1.0"
onetime "^6.0.0"
signal-exit "^4.1.0"
strip-final-newline "^3.0.0"
expect@^29.0.0: expect@^29.0.0:
version "29.7.0" version "29.7.0"
@@ -6526,11 +6558,6 @@ get-stream@^6.0.0:
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
get-stream@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2"
integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==
get-symbol-description@^1.0.2: get-symbol-description@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5"
@@ -6790,11 +6817,6 @@ human-signals@^2.1.0:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
human-signals@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28"
integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==
husky@7.0.4: husky@7.0.4:
version "7.0.4" version "7.0.4"
resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.4.tgz#242048245dc49c8fb1bf0cc7cfb98dd722531535" resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.4.tgz#242048245dc49c8fb1bf0cc7cfb98dd722531535"
@@ -7128,11 +7150,6 @@ is-stream@^2.0.0:
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
is-stream@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac"
integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==
is-string@^1.0.5, is-string@^1.0.7: is-string@^1.0.5, is-string@^1.0.7:
version "1.0.7" version "1.0.7"
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd"
@@ -7708,6 +7725,11 @@ loupe@^3.1.0, loupe@^3.1.1:
dependencies: dependencies:
get-func-name "^2.0.1" get-func-name "^2.0.1"
loupe@^3.1.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.3.tgz#042a8f7986d77f3d0f98ef7990a2b2fef18b0fd2"
integrity sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==
lower-case@^2.0.2: lower-case@^2.0.2:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28"
@@ -7753,10 +7775,10 @@ magic-string@^0.27.0:
dependencies: dependencies:
"@jridgewell/sourcemap-codec" "^1.4.13" "@jridgewell/sourcemap-codec" "^1.4.13"
magic-string@^0.30.10: magic-string@^0.30.10, magic-string@^0.30.12:
version "0.30.11" version "0.30.17"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.11.tgz#301a6f93b3e8c2cb13ac1a7a673492c0dfd12954" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453"
integrity sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A== integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==
dependencies: dependencies:
"@jridgewell/sourcemap-codec" "^1.5.0" "@jridgewell/sourcemap-codec" "^1.5.0"
@@ -8068,11 +8090,6 @@ mimic-fn@^2.1.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
mimic-fn@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc"
integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==
min-indent@^1.0.0: min-indent@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
@@ -8160,7 +8177,7 @@ ms@2.1.2:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
ms@^2.1.1: ms@^2.1.1, ms@^2.1.3:
version "2.1.3" version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
@@ -8293,13 +8310,6 @@ npm-run-path@^4.0.1:
dependencies: dependencies:
path-key "^3.0.0" path-key "^3.0.0"
npm-run-path@^5.1.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.3.0.tgz#e23353d0ebb9317f174e93417e4a4d82d0249e9f"
integrity sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==
dependencies:
path-key "^4.0.0"
nth-check@^2.0.1: nth-check@^2.0.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
@@ -8397,13 +8407,6 @@ onetime@^5.1.0, onetime@^5.1.2:
dependencies: dependencies:
mimic-fn "^2.1.0" mimic-fn "^2.1.0"
onetime@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4"
integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==
dependencies:
mimic-fn "^4.0.0"
open-color@1.9.1: open-color@1.9.1:
version "1.9.1" version "1.9.1"
resolved "https://registry.yarnpkg.com/open-color/-/open-color-1.9.1.tgz#a6e6328f60eff7aa60e3e8fcfa50f53ff3eece35" resolved "https://registry.yarnpkg.com/open-color/-/open-color-1.9.1.tgz#a6e6328f60eff7aa60e3e8fcfa50f53ff3eece35"
@@ -8522,11 +8525,6 @@ path-key@^3.0.0, path-key@^3.1.0:
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
path-key@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18"
integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==
path-parse@^1.0.7: path-parse@^1.0.7:
version "1.0.7" version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
@@ -9451,7 +9449,7 @@ signal-exit@^3.0.2, signal-exit@^3.0.3:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
signal-exit@^4.0.1, signal-exit@^4.1.0: signal-exit@^4.0.1:
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
@@ -9618,10 +9616,10 @@ stackback@0.0.2:
resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b"
integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==
std-env@^3.7.0: std-env@^3.7.0, std-env@^3.8.0:
version "3.7.0" version "3.8.0"
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.8.0.tgz#b56ffc1baf1a29dcc80a3bdf11d7fca7c315e7d5"
integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== integrity sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==
streamsearch@^1.1.0: streamsearch@^1.1.0:
version "1.1.0" version "1.1.0"
@@ -9765,11 +9763,6 @@ strip-final-newline@^2.0.0:
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
strip-final-newline@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd"
integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==
strip-indent@^3.0.0: strip-indent@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
@@ -9942,25 +9935,30 @@ tiny-invariant@^1.1.0:
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127"
integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==
tinybench@^2.8.0: tinybench@^2.9.0:
version "2.9.0" version "2.9.0"
resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b"
integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==
tinypool@^1.0.0: tinyexec@^0.3.1:
version "1.0.1" version "0.3.2"
resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.1.tgz#c64233c4fac4304e109a64340178760116dbe1fe" resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2"
integrity sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA== integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==
tinypool@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.2.tgz#706193cc532f4c100f66aa00b01c42173d9051b2"
integrity sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==
tinyrainbow@^1.2.0: tinyrainbow@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-1.2.0.tgz#5c57d2fc0fb3d1afd78465c33ca885d04f02abb5" resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-1.2.0.tgz#5c57d2fc0fb3d1afd78465c33ca885d04f02abb5"
integrity sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ== integrity sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==
tinyspy@^3.0.0: tinyspy@^3.0.2:
version "3.0.0" version "3.0.2"
resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.0.tgz#cb61644f2713cd84dee184863f4642e06ddf0585" resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a"
integrity sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA== integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==
to-fast-properties@^1.0.3: to-fast-properties@^1.0.3:
version "1.0.3" version "1.0.3"
@@ -10337,15 +10335,15 @@ v8-compile-cache@^2.0.3:
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz#cdada8bec61e15865f05d097c5f4fd30e94dc128" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz#cdada8bec61e15865f05d097c5f4fd30e94dc128"
integrity sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw== integrity sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==
vite-node@2.0.5: vite-node@2.1.9:
version "2.0.5" version "2.1.9"
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.0.5.tgz#36d909188fc6e3aba3da5fc095b3637d0d18e27b" resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.9.tgz#549710f76a643f1c39ef34bdb5493a944e4f895f"
integrity sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q== integrity sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==
dependencies: dependencies:
cac "^6.7.14" cac "^6.7.14"
debug "^4.3.5" debug "^4.3.7"
es-module-lexer "^1.5.4"
pathe "^1.1.2" pathe "^1.1.2"
tinyrainbow "^1.2.0"
vite "^5.0.0" vite "^5.0.0"
vite-plugin-checker@0.7.2: vite-plugin-checker@0.7.2:
@@ -10447,29 +10445,30 @@ vitest-canvas-mock@0.3.3:
dependencies: dependencies:
jest-canvas-mock "~2.5.2" jest-canvas-mock "~2.5.2"
vitest@2.0.5: vitest@2.1.9:
version "2.0.5" version "2.1.9"
resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.0.5.tgz#2f15a532704a7181528e399cc5b754c7f335fd62" resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.1.9.tgz#7d01ffd07a553a51c87170b5e80fea3da7fb41e7"
integrity sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA== integrity sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==
dependencies: dependencies:
"@ampproject/remapping" "^2.3.0" "@vitest/expect" "2.1.9"
"@vitest/expect" "2.0.5" "@vitest/mocker" "2.1.9"
"@vitest/pretty-format" "^2.0.5" "@vitest/pretty-format" "^2.1.9"
"@vitest/runner" "2.0.5" "@vitest/runner" "2.1.9"
"@vitest/snapshot" "2.0.5" "@vitest/snapshot" "2.1.9"
"@vitest/spy" "2.0.5" "@vitest/spy" "2.1.9"
"@vitest/utils" "2.0.5" "@vitest/utils" "2.1.9"
chai "^5.1.1" chai "^5.1.2"
debug "^4.3.5" debug "^4.3.7"
execa "^8.0.1" expect-type "^1.1.0"
magic-string "^0.30.10" magic-string "^0.30.12"
pathe "^1.1.2" pathe "^1.1.2"
std-env "^3.7.0" std-env "^3.8.0"
tinybench "^2.8.0" tinybench "^2.9.0"
tinypool "^1.0.0" tinyexec "^0.3.1"
tinypool "^1.0.1"
tinyrainbow "^1.2.0" tinyrainbow "^1.2.0"
vite "^5.0.0" vite "^5.0.0"
vite-node "2.0.5" vite-node "2.1.9"
why-is-node-running "^2.3.0" why-is-node-running "^2.3.0"
vscode-jsonrpc@6.0.0: vscode-jsonrpc@6.0.0: