Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] 745cedf9a3 build(deps): bump ejs from 3.1.9 to 3.1.10
Bumps [ejs](https://github.com/mde/ejs) from 3.1.9 to 3.1.10.
- [Release notes](https://github.com/mde/ejs/releases)
- [Commits](https://github.com/mde/ejs/compare/v3.1.9...v3.1.10)

---
updated-dependencies:
- dependency-name: ejs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-01 18:48:28 +00:00
8 changed files with 345 additions and 498 deletions
+1 -1
View File
@@ -54,7 +54,7 @@
"vitest-canvas-mock": "0.3.2"
},
"engines": {
"node": "18.0.0 - 22.x.x"
"node": "18.0.0 - 20.x.x"
},
"homepage": ".",
"prettier": "@excalidraw/prettier-config",
+7 -8
View File
@@ -12,13 +12,13 @@ import { arrayToMap } from "../utils";
import { CODES, KEYS } from "../keys";
import { getCommonBoundingBox } from "../element/bounds";
import {
bindOrUnbindLinearElements,
bindOrUnbindSelectedElements,
isBindingEnabled,
unbindLinearElements,
} from "../element/binding";
import { updateFrameMembershipOfSelectedElements } from "../frame";
import { flipHorizontal, flipVertical } from "../components/icons";
import { StoreAction } from "../store";
import { isLinearElement } from "../element/typeChecks";
export const actionFlipHorizontal = register({
name: "flipHorizontal",
@@ -89,6 +89,7 @@ const flipSelectedElements = (
const updatedElements = flipElements(
selectedElements,
elements,
elementsMap,
appState,
flipDirection,
@@ -104,6 +105,7 @@ const flipSelectedElements = (
const flipElements = (
selectedElements: NonDeleted<ExcalidrawElement>[],
elements: readonly ExcalidrawElement[],
elementsMap: NonDeletedSceneElementsMap,
appState: AppState,
flipDirection: "horizontal" | "vertical",
@@ -122,12 +124,9 @@ const flipElements = (
flipDirection === "horizontal" ? minY : maxY,
);
bindOrUnbindLinearElements(
selectedElements.filter(isLinearElement),
app,
isBindingEnabled(appState),
[],
);
isBindingEnabled(appState)
? bindOrUnbindSelectedElements(selectedElements, app)
: unbindLinearElements(selectedElements, elementsMap);
return selectedElements;
};
+35 -37
View File
@@ -122,16 +122,17 @@ import {
} from "../element";
import {
bindOrUnbindLinearElement,
bindOrUnbindLinearElements,
bindOrUnbindSelectedElements,
fixBindingsAfterDeletion,
fixBindingsAfterDuplication,
getEligibleElementsForBinding,
getHoveredElementForBinding,
isBindingEnabled,
isLinearElementSimpleAndAlreadyBound,
maybeBindLinearElement,
shouldEnableBindingForPointerEvent,
unbindLinearElements,
updateBoundElements,
getSuggestedBindingsForArrows,
} from "../element/binding";
import { LinearElementEditor } from "../element/linearElementEditor";
import { mutateElement, newElementWith } from "../element/mutateElement";
@@ -3937,12 +3938,7 @@ class App extends React.Component<AppProps, AppState> {
});
});
this.setState({
suggestedBindings: getSuggestedBindingsForArrows(
selectedElements,
this,
),
});
this.maybeSuggestBindingForAll(selectedElements);
event.preventDefault();
} else if (event.key === KEYS.ENTER) {
@@ -4109,12 +4105,11 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ isBindingEnabled: true });
}
if (isArrowKey(event.key)) {
bindOrUnbindLinearElements(
this.scene.getSelectedElements(this.state).filter(isLinearElement),
this,
isBindingEnabled(this.state),
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
);
const selectedElements = this.scene.getSelectedElements(this.state);
const elementsMap = this.scene.getNonDeletedElementsMap();
isBindingEnabled(this.state)
? bindOrUnbindSelectedElements(selectedElements, this)
: unbindLinearElements(selectedElements, elementsMap);
this.setState({ suggestedBindings: [] });
}
});
@@ -7458,12 +7453,7 @@ class App extends React.Component<AppProps, AppState> {
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
);
this.setState({
suggestedBindings: getSuggestedBindingsForArrows(
selectedElements,
this,
),
});
this.maybeSuggestBindingForAll(selectedElements);
// We duplicate the selected element if alt is pressed on pointer move
if (event.altKey && !pointerDownState.hit.hasBeenDuplicated) {
@@ -8510,18 +8500,15 @@ class App extends React.Component<AppProps, AppState> {
}
if (pointerDownState.drag.hasOccurred || isResizing || isRotating) {
// We only allow binding via linear elements, specifically via dragging
// the endpoints ("start" or "end").
const linearElements = this.scene
.getSelectedElements(this.state)
.filter(isLinearElement);
bindOrUnbindLinearElements(
linearElements,
this,
isBindingEnabled(this.state),
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
);
isBindingEnabled(this.state)
? bindOrUnbindSelectedElements(
this.scene.getSelectedElements(this.state),
this,
)
: unbindLinearElements(
this.scene.getSelectedElements(this.state),
elementsMap,
);
}
if (activeTool.type === "laser") {
@@ -9053,6 +9040,19 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ suggestedBindings });
};
private maybeSuggestBindingForAll(
selectedElements: NonDeleted<ExcalidrawElement>[],
): void {
if (selectedElements.length > 50) {
return;
}
const suggestedBindings = getEligibleElementsForBinding(
selectedElements,
this,
);
this.setState({ suggestedBindings });
}
private clearSelection(hitElement: ExcalidrawElement | null): void {
this.setState((prevState) => ({
selectedElementIds: makeNextSelectedElementIds({}, prevState),
@@ -9439,6 +9439,8 @@ class App extends React.Component<AppProps, AppState> {
this.state.originSnapOffset,
);
this.maybeSuggestBindingForAll([draggingElement]);
// highlight elements that are to be added to frames on frames creation
if (
this.state.activeTool.type === TOOL_TYPE.frame ||
@@ -9561,10 +9563,7 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.resize.center.y,
)
) {
const suggestedBindings = getSuggestedBindingsForArrows(
selectedElements,
this,
);
this.maybeSuggestBindingForAll(selectedElements);
const elementsToHighlight = new Set<ExcalidrawElement>();
selectedFrames.forEach((frame) => {
@@ -9578,7 +9577,6 @@ class App extends React.Component<AppProps, AppState> {
this.setState({
elementsToHighlight: [...elementsToHighlight],
suggestedBindings,
});
return true;
+182 -190
View File
@@ -131,193 +131,73 @@ const bindOrUnbindLinearElementEdge = (
unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>,
elementsMap: NonDeletedSceneElementsMap,
): void => {
// "keep" is for method chaining convenience, a "no-op", so just bail out
if (bindableElement === "keep") {
return;
}
// null means break the bind, so nothing to consider here
if (bindableElement === null) {
const unbound = unbindLinearElement(linearElement, startOrEnd);
if (unbound != null) {
unboundFromElementIds.add(unbound);
}
return;
}
// While complext arrows can do anything, simple arrow with both ends trying
// to bind to the same bindable should not be allowed, start binding takes
// precedence
if (isLinearElementSimple(linearElement)) {
if (
otherEdgeBindableElement == null ||
(otherEdgeBindableElement === "keep"
? // TODO: Refactor - Needlessly complex
!isLinearElementSimpleAndAlreadyBoundOnOppositeEdge(
linearElement,
bindableElement,
startOrEnd,
)
: startOrEnd === "start" ||
otherEdgeBindableElement.id !== bindableElement.id)
) {
bindLinearElement(
linearElement,
bindableElement,
startOrEnd,
elementsMap,
);
boundToElementIds.add(bindableElement.id);
}
} else {
bindLinearElement(linearElement, bindableElement, startOrEnd, elementsMap);
boundToElementIds.add(bindableElement.id);
}
};
const getOriginalBindingIfStillCloseOfLinearElementEdge = (
linearElement: NonDeleted<ExcalidrawLinearElement>,
edge: "start" | "end",
app: AppClassProperties,
): NonDeleted<ExcalidrawElement> | null => {
const elementsMap = app.scene.getNonDeletedElementsMap();
const coors = getLinearElementEdgeCoors(linearElement, edge, elementsMap);
const elementId =
edge === "start"
? linearElement.startBinding?.elementId
: linearElement.endBinding?.elementId;
if (elementId) {
const element = elementsMap.get(
elementId,
) as NonDeleted<ExcalidrawBindableElement>;
if (bindingBorderTest(element, coors, app)) {
return element;
if (bindableElement !== "keep") {
if (bindableElement != null) {
// Don't bind if we're trying to bind or are already bound to the same
// element on the other edge already ("start" edge takes precedence).
if (
otherEdgeBindableElement == null ||
(otherEdgeBindableElement === "keep"
? !isLinearElementSimpleAndAlreadyBoundOnOppositeEdge(
linearElement,
bindableElement,
startOrEnd,
)
: startOrEnd === "start" ||
otherEdgeBindableElement.id !== bindableElement.id)
) {
bindLinearElement(
linearElement,
bindableElement,
startOrEnd,
elementsMap,
);
boundToElementIds.add(bindableElement.id);
}
} else {
const unbound = unbindLinearElement(linearElement, startOrEnd);
if (unbound != null) {
unboundFromElementIds.add(unbound);
}
}
}
return null;
};
const getOriginalBindingsIfStillCloseToArrowEnds = (
linearElement: NonDeleted<ExcalidrawLinearElement>,
export const bindOrUnbindSelectedElements = (
selectedElements: NonDeleted<ExcalidrawElement>[],
app: AppClassProperties,
): (NonDeleted<ExcalidrawElement> | null)[] =>
["start", "end"].map((edge) =>
getOriginalBindingIfStillCloseOfLinearElementEdge(
linearElement,
edge as "start" | "end",
app,
),
);
const getBindingStrategyForDraggingArrowEndpoints = (
selectedElement: NonDeleted<ExcalidrawLinearElement>,
isBindingEnabled: boolean,
draggingPoints: readonly number[],
app: AppClassProperties,
): (NonDeleted<ExcalidrawBindableElement> | null | "keep")[] => {
const startIdx = 0;
const endIdx = selectedElement.points.length - 1;
const startDragged = draggingPoints.findIndex((i) => i === startIdx) > -1;
const endDragged = draggingPoints.findIndex((i) => i === endIdx) > -1;
const start = startDragged
? isBindingEnabled
? getElligibleElementForBindingElement(selectedElement, "start", app)
: null // If binding is disabled and start is dragged, break all binds
: // We have to update the focus and gap of the binding, so let's rebind
getElligibleElementForBindingElement(selectedElement, "start", app);
const end = endDragged
? isBindingEnabled
? getElligibleElementForBindingElement(selectedElement, "end", app)
: null // If binding is disabled and end is dragged, break all binds
: // We have to update the focus and gap of the binding, so let's rebind
getElligibleElementForBindingElement(selectedElement, "end", app);
return [start, end];
};
const getBindingStrategyForDraggingArrowOrJoints = (
selectedElement: NonDeleted<ExcalidrawLinearElement>,
app: AppClassProperties,
isBindingEnabled: boolean,
): (NonDeleted<ExcalidrawBindableElement> | null | "keep")[] => {
const [startIsClose, endIsClose] = getOriginalBindingsIfStillCloseToArrowEnds(
selectedElement,
app,
);
const start = startIsClose
? isBindingEnabled
? getElligibleElementForBindingElement(selectedElement, "start", app)
: null
: null;
const end = endIsClose
? isBindingEnabled
? getElligibleElementForBindingElement(selectedElement, "end", app)
: null
: null;
return [start, end];
};
export const bindOrUnbindLinearElements = (
selectedElements: NonDeleted<ExcalidrawLinearElement>[],
app: AppClassProperties,
isBindingEnabled: boolean,
draggingPoints: readonly number[] | null,
): void => {
selectedElements.forEach((selectedElement) => {
const [start, end] = draggingPoints?.length
? // The arrow edge points are dragged (i.e. start, end)
getBindingStrategyForDraggingArrowEndpoints(
selectedElement,
isBindingEnabled,
draggingPoints ?? [],
app,
)
: // The arrow itself (the shaft) or the inner joins are dragged
getBindingStrategyForDraggingArrowOrJoints(
selectedElement,
app,
isBindingEnabled,
);
bindOrUnbindLinearElement(
selectedElement,
start,
end,
app.scene.getNonDeletedElementsMap(),
);
if (isBindingElement(selectedElement)) {
bindOrUnbindLinearElement(
selectedElement,
getElligibleElementForBindingElement(selectedElement, "start", app),
getElligibleElementForBindingElement(selectedElement, "end", app),
app.scene.getNonDeletedElementsMap(),
);
} else if (isBindableElement(selectedElement)) {
maybeBindBindableElement(
selectedElement,
app.scene.getNonDeletedElementsMap(),
app,
);
}
});
};
export const getSuggestedBindingsForArrows = (
selectedElements: NonDeleted<ExcalidrawElement>[],
const maybeBindBindableElement = (
bindableElement: NonDeleted<ExcalidrawBindableElement>,
elementsMap: NonDeletedSceneElementsMap,
app: AppClassProperties,
): SuggestedBinding[] => {
// HOT PATH: Bail out if selected elements list is too large
if (selectedElements.length > 50) {
return [];
}
return (
selectedElements
.filter(isLinearElement)
.flatMap((element) =>
getOriginalBindingsIfStillCloseToArrowEnds(element, app),
)
.filter(
(element): element is NonDeleted<ExcalidrawBindableElement> =>
element !== null,
)
// Filter out bind candidates which are in the
// same selection / group with the arrow
//
// TODO: Is it worth turning the list into a set to avoid dupes?
.filter(
(element) =>
selectedElements.filter((selected) => selected.id === element?.id)
.length === 0,
)
): void => {
getElligibleElementsForBindableElementAndWhere(bindableElement, app).forEach(
([linearElement, where]) =>
bindOrUnbindLinearElement(
linearElement,
where === "end" ? "keep" : bindableElement,
where === "start" ? "keep" : bindableElement,
elementsMap,
),
);
};
@@ -403,14 +283,29 @@ export const isLinearElementSimpleAndAlreadyBound = (
bindableElement: ExcalidrawBindableElement,
): boolean => {
return (
alreadyBoundToId === bindableElement.id &&
isLinearElementSimple(linearElement)
alreadyBoundToId === bindableElement.id && linearElement.points.length < 3
);
};
const isLinearElementSimple = (
linearElement: NonDeleted<ExcalidrawLinearElement>,
): boolean => linearElement.points.length < 3;
export const unbindLinearElements = (
elements: readonly NonDeleted<ExcalidrawElement>[],
elementsMap: NonDeletedSceneElementsMap,
): void => {
elements.forEach((element) => {
if (isBindingElement(element)) {
if (element.startBinding !== null && element.endBinding !== null) {
bindOrUnbindLinearElement(element, null, null, elementsMap);
} else {
bindOrUnbindLinearElement(
element,
element.startBinding ? "keep" : null,
element.endBinding ? "keep" : null,
elementsMap,
);
}
}
});
};
const unbindLinearElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>,
@@ -657,6 +552,42 @@ const maybeCalculateNewGapWhenScaling = (
return { elementId, gap: newGap, focus };
};
// TODO: this is a bottleneck, optimise
export const getEligibleElementsForBinding = (
selectedElements: NonDeleted<ExcalidrawElement>[],
app: AppClassProperties,
): SuggestedBinding[] => {
const includedElementIds = new Set(selectedElements.map(({ id }) => id));
return selectedElements.flatMap((selectedElement) =>
isBindingElement(selectedElement, false)
? (getElligibleElementsForBindingElement(
selectedElement as NonDeleted<ExcalidrawLinearElement>,
app,
).filter(
(element) => !includedElementIds.has(element.id),
) as SuggestedBinding[])
: isBindableElement(selectedElement, false)
? getElligibleElementsForBindableElementAndWhere(
selectedElement,
app,
).filter((binding) => !includedElementIds.has(binding[0].id))
: [],
);
};
const getElligibleElementsForBindingElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>,
app: AppClassProperties,
): NonDeleted<ExcalidrawBindableElement>[] => {
return [
getElligibleElementForBindingElement(linearElement, "start", app),
getElligibleElementForBindingElement(linearElement, "end", app),
].filter(
(element): element is NonDeleted<ExcalidrawBindableElement> =>
element != null,
);
};
const getElligibleElementForBindingElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>,
startOrEnd: "start" | "end",
@@ -687,6 +618,67 @@ const getLinearElementEdgeCoors = (
);
};
const getElligibleElementsForBindableElementAndWhere = (
bindableElement: NonDeleted<ExcalidrawBindableElement>,
app: AppClassProperties,
): SuggestedPointBinding[] => {
const scene = Scene.getScene(bindableElement)!;
return scene
.getNonDeletedElements()
.map((element) => {
if (!isBindingElement(element, false)) {
return null;
}
const canBindStart = isLinearElementEligibleForNewBindingByBindable(
element,
"start",
bindableElement,
scene.getNonDeletedElementsMap(),
app,
);
const canBindEnd = isLinearElementEligibleForNewBindingByBindable(
element,
"end",
bindableElement,
scene.getNonDeletedElementsMap(),
app,
);
if (!canBindStart && !canBindEnd) {
return null;
}
return [
element,
canBindStart && canBindEnd ? "both" : canBindStart ? "start" : "end",
bindableElement,
];
})
.filter((maybeElement) => maybeElement != null) as SuggestedPointBinding[];
};
const isLinearElementEligibleForNewBindingByBindable = (
linearElement: NonDeleted<ExcalidrawLinearElement>,
startOrEnd: "start" | "end",
bindableElement: NonDeleted<ExcalidrawBindableElement>,
elementsMap: NonDeletedSceneElementsMap,
app: AppClassProperties,
): boolean => {
const existingBinding =
linearElement[startOrEnd === "start" ? "startBinding" : "endBinding"];
return (
existingBinding == null &&
!isLinearElementSimpleAndAlreadyBoundOnOppositeEdge(
linearElement,
bindableElement,
startOrEnd,
) &&
bindingBorderTest(
bindableElement,
getLinearElementEdgeCoors(linearElement, startOrEnd, elementsMap),
app,
)
);
};
// We need to:
// 1: Update elements not selected to point to duplicated elements
// 2: Update duplicated elements to point to other duplicated elements
@@ -822,7 +814,7 @@ const newBoundElements = (
return nextBoundElements;
};
const bindingBorderTest = (
export const bindingBorderTest = (
element: NonDeleted<ExcalidrawBindableElement>,
{ x, y }: { x: number; y: number },
app: AppClassProperties,
@@ -844,7 +836,7 @@ export const maxBindingGap = (
return Math.max(16, Math.min(0.25 * smallerDimension, 32));
};
const distanceToBindableElement = (
export const distanceToBindableElement = (
element: ExcalidrawBindableElement,
point: Point,
elementsMap: ElementsMap,
@@ -901,7 +893,7 @@ const distanceToDiamond = (
return GAPoint.distanceToLine(pointRel, side);
};
const distanceToEllipse = (
export const distanceToEllipse = (
element: ExcalidrawEllipseElement,
point: Point,
elementsMap: ElementsMap,
@@ -1020,7 +1012,7 @@ const coordsCenter = (
// all focus points lie, so it's a number between -1 and 1.
// The line going through `a` and `b` is a tangent to the "focus image"
// of the element.
const determineFocusDistance = (
export const determineFocusDistance = (
element: ExcalidrawBindableElement,
// Point on the line, in absolute coordinates
a: Point,
@@ -1061,7 +1053,7 @@ const determineFocusDistance = (
return ret || 0;
};
const determineFocusPoint = (
export const determineFocusPoint = (
element: ExcalidrawBindableElement,
// The oriented, relative distance from the center of `element` of the
// returned focusPoint
@@ -1101,7 +1093,7 @@ const determineFocusPoint = (
// Returns 2 or 0 intersection points between line going through `a` and `b`
// and the `element`, in ascending order of distance from `a`.
const intersectElementWithLine = (
export const intersectElementWithLine = (
element: ExcalidrawBindableElement,
// Point on the line, in absolute coordinates
a: Point,
@@ -1268,7 +1260,7 @@ const getEllipseIntersections = (
];
};
const getCircleIntersections = (
export const getCircleIntersections = (
center: GA.Point,
radius: number,
line: GA.Line,
@@ -1298,7 +1290,7 @@ const getCircleIntersections = (
// The focus point is the tangent point of the "focus image" of the
// `element`, where the tangent goes through `point`.
const findFocusPointForEllipse = (
export const findFocusPointForEllipse = (
ellipse: ExcalidrawEllipseElement,
// Between -1 and 1 (not 0) the relative size of the "focus image" of
// the element on which the focus point lies
@@ -1335,7 +1327,7 @@ const findFocusPointForEllipse = (
return GA.point(x, (-m * x - 1) / n);
};
const findFocusPointForRectangulars = (
export const findFocusPointForRectangulars = (
element:
| ExcalidrawRectangleElement
| ExcalidrawImageElement
@@ -71,13 +71,13 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"penMode": false,
"pendingImageElementId": null,
"previousSelectedElementIds": {
"id159": true,
"id158": true,
},
"resizingElement": null,
"scrollX": 0,
"scrollY": 0,
"selectedElementIds": {
"id159": true,
"id158": true,
},
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": {},
@@ -112,7 +112,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"frameId": null,
"groupIds": [],
"height": 100,
"id": "id157",
"id": "id156",
"index": "a0",
"isDeleted": false,
"link": null,
@@ -127,7 +127,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 18,
"version": 8,
"width": 100,
"x": -100,
"y": -50,
@@ -144,7 +144,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"frameId": null,
"groupIds": [],
"height": 100,
"id": "id158",
"id": "id157",
"index": "a1",
"isDeleted": false,
"link": null,
@@ -159,7 +159,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 19,
"version": 9,
"width": 100,
"x": 100,
"y": -50,
@@ -174,7 +174,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"customData": undefined,
"endArrowhead": "arrow",
"endBinding": {
"elementId": "id162",
"elementId": "id160",
"focus": 0,
"gap": 1,
},
@@ -182,7 +182,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"frameId": null,
"groupIds": [],
"height": 99.19725525211979,
"id": "id159",
"id": "id158",
"index": "a2",
"isDeleted": false,
"lastCommittedPoint": null,
@@ -210,9 +210,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2,
"type": "arrow",
"updated": 1,
"version": 54,
"version": 24,
"width": 98.40367721010284,
"x": 1,
"x": 1.000000000000007,
"y": 0,
}
`;
@@ -223,7 +223,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id159",
"id": "id158",
"type": "arrow",
},
],
@@ -232,7 +232,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"frameId": null,
"groupIds": [],
"height": 50,
"id": "id162",
"id": "id160",
"index": "a3",
"isDeleted": false,
"link": null,
@@ -274,68 +274,19 @@ History {
"added": Map {},
"removed": Map {},
"updated": Map {
"id159" => Delta {
"id156" => Delta {
"deleted": {
"endBinding": {
"elementId": "id158",
"focus": 0.009900990099009901,
"gap": 1,
},
"height": 0.9800031696987099,
"points": [
[
0,
0,
],
[
98,
-0.9800031696987099,
],
],
"startBinding": {
"elementId": "id157",
"focus": 0.0297029702970297,
"gap": 1,
},
"boundElements": [],
},
"inserted": {
"endBinding": {
"elementId": "id158",
"focus": -0.02,
"gap": 1,
},
"height": 0.0002487679019458344,
"points": [
[
0,
0,
],
[
98,
0.0002487679019458344,
],
"boundElements": [
{
"id": "id158",
"type": "arrow",
},
],
"startBinding": {
"elementId": "id157",
"focus": 0.02,
"gap": 1,
},
},
},
},
},
},
HistoryEntry {
"appStateChange": AppStateChange {
"delta": Delta {
"deleted": {},
"inserted": {},
},
},
"elementsChange": ElementsChange {
"added": Map {},
"removed": Map {},
"updated": Map {
"id157" => Delta {
"deleted": {
"boundElements": [],
@@ -343,29 +294,16 @@ History {
"inserted": {
"boundElements": [
{
"id": "id159",
"id": "id158",
"type": "arrow",
},
],
},
},
"id158" => Delta {
"deleted": {
"boundElements": [],
},
"inserted": {
"boundElements": [
{
"id": "id159",
"type": "arrow",
},
],
},
},
"id159" => Delta {
"deleted": {
"endBinding": {
"elementId": "id162",
"elementId": "id160",
"focus": 0,
"gap": 1,
},
@@ -385,11 +323,11 @@ History {
},
"inserted": {
"endBinding": {
"elementId": "id158",
"elementId": "id157",
"focus": 0.009900990099009901,
"gap": 1,
},
"height": 0.9802432787444684,
"height": 0.9800000000000002,
"points": [
[
0,
@@ -397,22 +335,22 @@ History {
],
[
98,
-0.9802432787444684,
-0.9800000000000002,
],
],
"startBinding": {
"elementId": "id157",
"elementId": "id156",
"focus": 0.0297029702970297,
"gap": 1,
},
"y": 0.9903686540602428,
"y": 0.9900000000000004,
},
},
"id162" => Delta {
"id160" => Delta {
"deleted": {
"boundElements": [
{
"id": "id159",
"id": "id158",
"type": "arrow",
},
],
@@ -436,7 +374,7 @@ History {
"elementsChange": ElementsChange {
"added": Map {},
"removed": Map {
"id157" => Delta {
"id156" => Delta {
"deleted": {
"angle": 0,
"backgroundColor": "transparent",
@@ -467,7 +405,7 @@ History {
"isDeleted": true,
},
},
"id158" => Delta {
"id157" => Delta {
"deleted": {
"angle": 0,
"backgroundColor": "transparent",
@@ -507,9 +445,9 @@ History {
"delta": Delta {
"deleted": {
"selectedElementIds": {
"id159": true,
"id158": true,
},
"selectedLinearElementId": "id159",
"selectedLinearElementId": "id158",
},
"inserted": {
"selectedElementIds": {},
@@ -520,7 +458,7 @@ History {
"elementsChange": ElementsChange {
"added": Map {},
"removed": Map {
"id159" => Delta {
"id158" => Delta {
"deleted": {
"angle": 0,
"backgroundColor": "transparent",
@@ -576,7 +514,7 @@ History {
exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and the arrow got bound to a different element in the meantime > [end of test] number of elements 1`] = `4`;
exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and the arrow got bound to a different element in the meantime > [end of test] number of renders 1`] = `22`;
exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and the arrow got bound to a different element in the meantime > [end of test] number of renders 1`] = `15`;
exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and there are no conflicting updates in the meantime > [end of test] appState 1`] = `
{
@@ -784,7 +722,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2,
"type": "arrow",
"updated": 1,
"version": 34,
"version": 23,
"width": 0,
"x": 251,
"y": 0,
@@ -800,46 +738,6 @@ History {
],
},
"redoStack": [
HistoryEntry {
"appStateChange": AppStateChange {
"delta": Delta {
"deleted": {},
"inserted": {},
},
},
"elementsChange": ElementsChange {
"added": Map {},
"removed": Map {},
"updated": Map {
"id154" => Delta {
"deleted": {
"points": [
[
0,
0,
],
[
0,
0,
],
],
},
"inserted": {
"points": [
[
0,
0,
],
[
100,
0,
],
],
},
},
},
},
},
HistoryEntry {
"appStateChange": AppStateChange {
"delta": Delta {
@@ -1070,7 +968,7 @@ History {
exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and there are no conflicting updates in the meantime > [end of test] number of elements 1`] = `3`;
exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and there are no conflicting updates in the meantime > [end of test] number of renders 1`] = `24`;
exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and there are no conflicting updates in the meantime > [end of test] number of renders 1`] = `16`;
exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind remotely added arrow when it's bindable elements are added through the history > [end of test] appState 1`] = `
{
@@ -1181,7 +1079,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"customData": undefined,
"endArrowhead": null,
"endBinding": {
"elementId": "id164",
"elementId": "id162",
"focus": 0,
"gap": 1,
},
@@ -1189,7 +1087,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"frameId": null,
"groupIds": [],
"height": 0.03596020595764898,
"id": "id165",
"id": "id163",
"index": "Zz",
"isDeleted": false,
"lastCommittedPoint": null,
@@ -1212,7 +1110,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
},
"startArrowhead": null,
"startBinding": {
"elementId": "id163",
"elementId": "id161",
"focus": 0,
"gap": 1,
},
@@ -1234,7 +1132,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id165",
"id": "id163",
"type": "arrow",
},
],
@@ -1243,7 +1141,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"frameId": null,
"groupIds": [],
"height": 100,
"id": "id163",
"id": "id161",
"index": "a0",
"isDeleted": false,
"link": null,
@@ -1271,7 +1169,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id165",
"id": "id163",
"type": "arrow",
},
],
@@ -1280,7 +1178,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"frameId": null,
"groupIds": [],
"height": 100,
"id": "id164",
"id": "id162",
"index": "a1",
"isDeleted": false,
"link": null,
@@ -1322,7 +1220,7 @@ History {
"elementsChange": ElementsChange {
"added": Map {},
"removed": Map {
"id163" => Delta {
"id161" => Delta {
"deleted": {
"angle": 0,
"backgroundColor": "transparent",
@@ -1353,7 +1251,7 @@ History {
"isDeleted": true,
},
},
"id164" => Delta {
"id162" => Delta {
"deleted": {
"angle": 0,
"backgroundColor": "transparent",
@@ -1386,15 +1284,15 @@ History {
},
},
"updated": Map {
"id165" => Delta {
"id163" => Delta {
"deleted": {
"endBinding": {
"elementId": "id164",
"elementId": "id162",
"focus": 0,
"gap": 1,
},
"startBinding": {
"elementId": "id163",
"elementId": "id161",
"focus": 0,
"gap": 1,
},
@@ -1524,7 +1422,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"customData": undefined,
"endArrowhead": null,
"endBinding": {
"elementId": "id167",
"elementId": "id165",
"focus": 0,
"gap": 1,
},
@@ -1532,7 +1430,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"frameId": null,
"groupIds": [],
"height": 0.03596020595764898,
"id": "id168",
"id": "id166",
"index": "a0",
"isDeleted": false,
"lastCommittedPoint": null,
@@ -1555,7 +1453,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
},
"startArrowhead": null,
"startBinding": {
"elementId": "id166",
"elementId": "id164",
"focus": 0,
"gap": 1,
},
@@ -1577,7 +1475,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id168",
"id": "id166",
"type": "arrow",
},
],
@@ -1586,7 +1484,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"frameId": null,
"groupIds": [],
"height": 100,
"id": "id166",
"id": "id164",
"index": "a0V",
"isDeleted": false,
"link": null,
@@ -1614,7 +1512,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id168",
"id": "id166",
"type": "arrow",
},
],
@@ -1623,7 +1521,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"frameId": null,
"groupIds": [],
"height": 100,
"id": "id167",
"id": "id165",
"index": "a1",
"isDeleted": false,
"link": null,
@@ -1665,7 +1563,7 @@ History {
"elementsChange": ElementsChange {
"added": Map {},
"removed": Map {
"id168" => Delta {
"id166" => Delta {
"deleted": {
"angle": 0,
"backgroundColor": "transparent",
@@ -1673,7 +1571,7 @@ History {
"customData": undefined,
"endArrowhead": null,
"endBinding": {
"elementId": "id167",
"elementId": "id165",
"focus": 0,
"gap": 1,
},
@@ -1703,7 +1601,7 @@ History {
},
"startArrowhead": null,
"startBinding": {
"elementId": "id166",
"elementId": "id164",
"focus": 0,
"gap": 1,
},
@@ -1721,11 +1619,11 @@ History {
},
},
"updated": Map {
"id166" => Delta {
"id164" => Delta {
"deleted": {
"boundElements": [
{
"id": "id168",
"id": "id166",
"type": "arrow",
},
],
@@ -1734,11 +1632,11 @@ History {
"boundElements": [],
},
},
"id167" => Delta {
"id165" => Delta {
"deleted": {
"boundElements": [
{
"id": "id168",
"id": "id166",
"type": "arrow",
},
],
@@ -1869,7 +1767,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"frameId": null,
"groupIds": [],
"height": 100,
"id": "id169",
"id": "id167",
"index": "a0",
"isDeleted": false,
"link": null,
@@ -1901,7 +1799,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"frameId": null,
"groupIds": [],
"height": 100,
"id": "id170",
"id": "id168",
"index": "a1",
"isDeleted": false,
"link": null,
@@ -1943,7 +1841,7 @@ History {
"elementsChange": ElementsChange {
"added": Map {},
"removed": Map {
"id169" => Delta {
"id167" => Delta {
"deleted": {
"angle": 0,
"backgroundColor": "transparent",
@@ -1974,7 +1872,7 @@ History {
"isDeleted": true,
},
},
"id170" => Delta {
"id168" => Delta {
"deleted": {
"angle": 0,
"backgroundColor": "transparent",
@@ -2092,7 +1990,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"scrollX": 0,
"scrollY": 0,
"selectedElementIds": {
"id173": true,
"id171": true,
},
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": {},
@@ -2123,7 +2021,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id173",
"id": "id171",
"type": "arrow",
},
],
@@ -2132,7 +2030,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"frameId": null,
"groupIds": [],
"height": 100,
"id": "id171",
"id": "id169",
"index": "a0",
"isDeleted": false,
"link": null,
@@ -2160,7 +2058,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id173",
"id": "id171",
"type": "arrow",
},
],
@@ -2169,7 +2067,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"frameId": null,
"groupIds": [],
"height": 100,
"id": "id172",
"id": "id170",
"index": "a1",
"isDeleted": false,
"link": null,
@@ -2199,7 +2097,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"customData": undefined,
"endArrowhead": "arrow",
"endBinding": {
"elementId": "id172",
"elementId": "id170",
"focus": 0,
"gap": 1,
},
@@ -2207,7 +2105,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"frameId": null,
"groupIds": [],
"height": 373.7994222717614,
"id": "id173",
"id": "id171",
"index": "a2",
"isDeleted": false,
"lastCommittedPoint": null,
@@ -2230,7 +2128,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
},
"startArrowhead": null,
"startBinding": {
"elementId": "id171",
"elementId": "id169",
"focus": 0,
"gap": 1,
},
@@ -2266,7 +2164,7 @@ History {
"elementsChange": ElementsChange {
"added": Map {},
"removed": Map {
"id171" => Delta {
"id169" => Delta {
"deleted": {
"angle": 0,
"backgroundColor": "transparent",
@@ -2297,7 +2195,7 @@ History {
"isDeleted": true,
},
},
"id172" => Delta {
"id170" => Delta {
"deleted": {
"angle": 0,
"backgroundColor": "transparent",
@@ -2337,9 +2235,9 @@ History {
"delta": Delta {
"deleted": {
"selectedElementIds": {
"id173": true,
"id171": true,
},
"selectedLinearElementId": "id173",
"selectedLinearElementId": "id171",
},
"inserted": {
"selectedElementIds": {},
@@ -2350,7 +2248,7 @@ History {
"elementsChange": ElementsChange {
"added": Map {},
"removed": Map {
"id173" => Delta {
"id171" => Delta {
"deleted": {
"angle": 0,
"backgroundColor": "transparent",
@@ -2358,7 +2256,7 @@ History {
"customData": undefined,
"endArrowhead": "arrow",
"endBinding": {
"elementId": "id172",
"elementId": "id170",
"focus": 0,
"gap": 1,
},
@@ -2388,7 +2286,7 @@ History {
},
"startArrowhead": null,
"startBinding": {
"elementId": "id171",
"elementId": "id169",
"focus": 0,
"gap": 1,
},
@@ -2406,11 +2304,11 @@ History {
},
},
"updated": Map {
"id171" => Delta {
"id169" => Delta {
"deleted": {
"boundElements": [
{
"id": "id173",
"id": "id171",
"type": "arrow",
},
],
@@ -2419,11 +2317,11 @@ History {
"boundElements": [],
},
},
"id172" => Delta {
"id170" => Delta {
"deleted": {
"boundElements": [
{
"id": "id173",
"id": "id171",
"type": "arrow",
},
],
@@ -5301,7 +5199,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
"frameId": null,
"groupIds": [],
"height": 100,
"id": "id175",
"id": "id173",
"index": "Zz",
"isDeleted": false,
"link": null,
@@ -5333,7 +5231,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
"frameId": null,
"groupIds": [],
"height": 500,
"id": "id174",
"id": "id172",
"index": "a0",
"isDeleted": true,
"link": null,
@@ -5376,7 +5274,7 @@ History {
"elementsChange": ElementsChange {
"added": Map {},
"removed": Map {
"id175" => Delta {
"id173" => Delta {
"deleted": {
"angle": 0,
"backgroundColor": "transparent",
@@ -5422,9 +5320,9 @@ History {
"added": Map {},
"removed": Map {},
"updated": Map {
"id175" => Delta {
"id173" => Delta {
"deleted": {
"frameId": "id174",
"frameId": "id172",
},
"inserted": {
"frameId": null,
+18 -41
View File
@@ -20,7 +20,6 @@ describe("element binding", () => {
const rect = API.createElement({
type: "rectangle",
x: 0,
y: 0,
width: 50,
height: 50,
});
@@ -40,43 +39,32 @@ describe("element binding", () => {
h.elements = [rect, arrow];
expect(arrow.startBinding).toBe(null);
// select arrow
mouse.clickAt(150, 0);
// move arrow start to potential binding position
mouse.downAt(100, 0);
mouse.moveTo(55, 0);
mouse.up(0, 0);
// Point selection is evaluated like the points are rendered,
// from right to left. So clicking on the first point should move the joint,
// not the start point.
expect(arrow.startBinding).toBe(null);
// Now that the start point is free, move it into overlapping position
mouse.downAt(100, 0);
mouse.moveTo(55, 0);
mouse.up(0, 0);
API.setSelectedElements([arrow]);
expect(API.getSelectedElements()).toEqual([arrow]);
expect(arrow.startBinding).toEqual({
elementId: rect.id,
focus: expect.toBeNonNaNNumber(),
gap: expect.toBeNonNaNNumber(),
});
// Move the end point to the overlapping binding position
mouse.downAt(200, 0);
mouse.downAt(100, 0);
mouse.moveTo(55, 0);
mouse.up(0, 0);
// Both the start and the end points should be bound
expect(API.getSelectedElements()).toEqual([arrow]);
expect(arrow.startBinding).toEqual({
elementId: rect.id,
focus: expect.toBeNonNaNNumber(),
gap: expect.toBeNonNaNNumber(),
});
mouse.downAt(100, 0);
mouse.move(-45, 0);
mouse.up();
expect(arrow.startBinding).toEqual({
elementId: rect.id,
focus: expect.toBeNonNaNNumber(),
gap: expect.toBeNonNaNNumber(),
});
mouse.down();
mouse.move(-50, 0);
mouse.up();
expect(arrow.startBinding).toBe(null);
expect(arrow.endBinding).toEqual({
elementId: rect.id,
focus: expect.toBeNonNaNNumber(),
@@ -156,7 +144,7 @@ describe("element binding", () => {
},
);
it("should unbind arrow when moving it with keyboard", () => {
it("should bind/unbind arrow when moving it with keyboard", () => {
const rectangle = UI.createElement("rectangle", {
x: 75,
y: 0,
@@ -172,23 +160,12 @@ describe("element binding", () => {
expect(arrow.endBinding).toBe(null);
mouse.downAt(50, 50);
mouse.moveTo(51, 0);
mouse.up(0, 0);
// Test sticky connection
expect(API.getSelectedElement().type).toBe("arrow");
Keyboard.keyPress(KEYS.ARROW_RIGHT);
expect(arrow.endBinding?.elementId).toBe(rectangle.id);
Keyboard.keyPress(KEYS.ARROW_LEFT);
expect(arrow.endBinding?.elementId).toBe(rectangle.id);
// Sever connection
expect(API.getSelectedElement().type).toBe("arrow");
Keyboard.keyPress(KEYS.ARROW_LEFT);
expect(arrow.endBinding).toBe(null);
Keyboard.keyPress(KEYS.ARROW_RIGHT);
expect(arrow.endBinding).toBe(null);
});
it("should unbind on bound element deletion", () => {
+9 -26
View File
@@ -4014,18 +4014,12 @@ describe("history", () => {
const arrowId = h.elements[2].id;
// create start binding
// create binding
mouse.downAt(0, 0);
mouse.moveTo(0, 1);
mouse.moveTo(0, 0);
mouse.up();
// create end binding
mouse.downAt(100, 0);
mouse.moveTo(100, 1);
mouse.moveTo(100, 0);
mouse.up();
expect(h.elements).toEqual([
expect.objectContaining({
id: rect1.id,
@@ -4050,10 +4044,9 @@ describe("history", () => {
}),
]);
Keyboard.undo(); // undo start binding
Keyboard.undo(); // undo end binding
Keyboard.undo();
expect(API.getUndoStack().length).toBe(2);
expect(API.getRedoStack().length).toBe(2);
expect(API.getRedoStack().length).toBe(1);
expect(h.elements).toEqual([
expect.objectContaining({
id: rect1.id,
@@ -4088,8 +4081,7 @@ describe("history", () => {
runTwice(() => {
Keyboard.redo();
Keyboard.redo();
expect(API.getUndoStack().length).toBe(4);
expect(API.getUndoStack().length).toBe(3);
expect(API.getRedoStack().length).toBe(0);
expect(h.elements).toEqual([
expect.objectContaining({
@@ -4115,10 +4107,9 @@ describe("history", () => {
}),
]);
Keyboard.undo();
Keyboard.undo();
expect(API.getUndoStack().length).toBe(2);
expect(API.getRedoStack().length).toBe(2);
expect(API.getRedoStack().length).toBe(1);
expect(h.elements).toEqual([
expect.objectContaining({
id: rect1.id,
@@ -4144,16 +4135,11 @@ describe("history", () => {
const arrowId = h.elements[2].id;
// create start binding
// create binding
mouse.downAt(0, 0);
mouse.moveTo(0, 1);
mouse.upAt(0, 0);
// create end binding
mouse.downAt(100, 0);
mouse.moveTo(100, 1);
mouse.upAt(100, 0);
expect(h.elements).toEqual([
expect.objectContaining({
id: rect1.id,
@@ -4178,10 +4164,9 @@ describe("history", () => {
}),
]);
Keyboard.undo();
Keyboard.undo();
expect(API.getUndoStack().length).toBe(2);
expect(API.getRedoStack().length).toBe(2);
expect(API.getRedoStack().length).toBe(1);
expect(h.elements).toEqual([
expect.objectContaining({
id: rect1.id,
@@ -4217,8 +4202,7 @@ describe("history", () => {
runTwice(() => {
Keyboard.redo();
Keyboard.redo();
expect(API.getUndoStack().length).toBe(4);
expect(API.getUndoStack().length).toBe(3);
expect(API.getRedoStack().length).toBe(0);
expect(h.elements).toEqual([
expect.objectContaining({
@@ -4249,10 +4233,9 @@ describe("history", () => {
}),
]);
Keyboard.undo();
Keyboard.undo();
expect(API.getUndoStack().length).toBe(2);
expect(API.getRedoStack().length).toBe(2);
expect(API.getRedoStack().length).toBe(1);
expect(h.elements).toEqual([
expect.objectContaining({
id: rect1.id,
+3 -3
View File
@@ -5525,9 +5525,9 @@ eastasianwidth@^0.2.0:
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
ejs@^3.1.6, ejs@^3.1.9:
version "3.1.9"
resolved "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361"
integrity sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==
version "3.1.10"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b"
integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==
dependencies:
jake "^10.8.5"