Compare commits

..

5 Commits

Author SHA1 Message Date
Ryan Di
3085f4af81 fix: tighten distance for double tap text creation (#9889) 2025-08-22 18:12:51 +02:00
David Luzar
531f3e5524 fix: restore from invalid fixedSegments & type-safer point updates (#9899)
* fix: restore from invalid fixedSegments & type-safer point updates

* fix: Type updates and throw for invalid point states

---------

Co-authored-by: Mark Tolmacs <mark@lazycat.hu>
2025-08-22 15:45:58 +00:00
David Luzar
90ec2739ae fix: calling toLowerCase on potentially undefined navigator.* values (#9901) 2025-08-22 17:37:16 +02:00
David Luzar
f29e9df72d chore: bump mermaid-to-excalidraw to 1.1.3 (#9898) 2025-08-21 20:58:04 +02:00
Marcel Mraz
b5ad7ae4e3 fix: even deltas with version & version nonce are valid (#9897) 2025-08-21 16:09:19 +02:00
11 changed files with 516 additions and 139 deletions

View File

@@ -28,11 +28,9 @@ export const isBrave = () =>
export const isMobile =
isIOS ||
/android|webos|ipod|blackberry|iemobile|opera mini/i.test(
navigator.userAgent.toLowerCase(),
navigator.userAgent,
) ||
/android|ios|ipod|blackberry|windows phone/i.test(
navigator.platform.toLowerCase(),
);
/android|ios|ipod|blackberry|windows phone/i.test(navigator.platform);
export const supportsResizeObserver =
typeof window !== "undefined" && "ResizeObserver" in window;
@@ -524,3 +522,5 @@ export enum UserIdleState {
* the start and end points)
*/
export const LINE_POLYGON_POINT_MERGE_DISTANCE = 20;
export const DOUBLE_TAP_POSITION_THRESHOLD = 35;

View File

@@ -1111,16 +1111,16 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
inserted,
}: Delta<ElementPartial>) =>
!!(
deleted.version &&
inserted.version &&
// versions are required integers
Number.isInteger(deleted.version) &&
Number.isInteger(inserted.version) &&
// versions should be positive, zero included
deleted.version >= 0 &&
inserted.version >= 0 &&
// versions should never be the same
deleted.version !== inserted.version
(
Number.isInteger(deleted.version) &&
Number.isInteger(inserted.version) &&
// versions should be positive, zero included
deleted.version! >= 0 &&
inserted.version! >= 0 &&
// versions should never be the same
deleted.version !== inserted.version
)
);
private static satisfiesUniqueInvariants = (
@@ -1191,9 +1191,10 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
ElementsDelta.stripIrrelevantProps,
);
// ignore updates which would "delete" already deleted element
if (!prevElement.isDeleted) {
removed[prevElement.id] = delta;
} else {
updated[prevElement.id] = delta;
}
}
}
@@ -1221,6 +1222,8 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
// ignore updates which would "delete" already deleted element
if (!nextElement.isDeleted) {
added[nextElement.id] = delta;
} else {
updated[nextElement.id] = delta;
}
continue;
@@ -1250,15 +1253,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
continue;
}
const strippedDeleted = ElementsDelta.stripVersionProps(delta.deleted);
const strippedInserted = ElementsDelta.stripVersionProps(
delta.inserted,
);
// making sure there are at least some changes and only changed version & versionNonce does not count!
if (Delta.isInnerDifferent(strippedDeleted, strippedInserted, true)) {
updated[nextElement.id] = delta;
}
updated[nextElement.id] = delta;
}
}
@@ -1372,15 +1367,8 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
latestDelta = delta;
}
const strippedDeleted = ElementsDelta.stripVersionProps(
latestDelta.deleted,
);
const strippedInserted = ElementsDelta.stripVersionProps(
latestDelta.inserted,
);
// it might happen that after applying latest changes the delta itself does not contain any changes
if (Delta.isInnerDifferent(strippedDeleted, strippedInserted)) {
if (Delta.isInnerDifferent(latestDelta.deleted, latestDelta.inserted)) {
modifiedDeltas[id] = latestDelta;
}
}
@@ -2075,12 +2063,4 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
return strippedPartial;
}
private static stripVersionProps(
partial: Partial<OrderedExcalidrawElement>,
): ElementPartial {
const { version, versionNonce, ...strippedPartial } = partial;
return strippedPartial;
}
}

View File

@@ -359,6 +359,12 @@ const handleSegmentRelease = (
null,
);
if (!restoredPoints || restoredPoints.length < 2) {
throw new Error(
"Property 'points' is required in the update returned by normalizeArrowElementUpdate()",
);
}
const nextPoints: GlobalPoint[] = [];
// First part of the arrow are the old points
@@ -706,7 +712,7 @@ const handleEndpointDrag = (
endGlobalPoint: GlobalPoint,
hoveredStartElement: ExcalidrawBindableElement | null,
hoveredEndElement: ExcalidrawBindableElement | null,
) => {
): ElementUpdate<ExcalidrawElbowArrowElement> => {
let startIsSpecial = arrow.startIsSpecial ?? null;
let endIsSpecial = arrow.endIsSpecial ?? null;
const globalUpdatedPoints = updatedPoints.map((p, i) =>
@@ -741,8 +747,15 @@ const handleEndpointDrag = (
// Calculate the moving second point connection and add the start point
{
const secondPoint = globalUpdatedPoints[startIsSpecial ? 2 : 1];
const thirdPoint = globalUpdatedPoints[startIsSpecial ? 3 : 2];
const secondPoint = globalUpdatedPoints.at(startIsSpecial ? 2 : 1);
const thirdPoint = globalUpdatedPoints.at(startIsSpecial ? 3 : 2);
if (!secondPoint || !thirdPoint) {
throw new Error(
`Second and third points must exist when handling endpoint drag (${startIsSpecial})`,
);
}
const startIsHorizontal = headingIsHorizontal(startHeading);
const secondIsHorizontal = headingIsHorizontal(
vectorToHeading(vectorFromPoint(secondPoint, thirdPoint)),
@@ -801,10 +814,19 @@ const handleEndpointDrag = (
// Calculate the moving second to last point connection
{
const secondToLastPoint =
globalUpdatedPoints[globalUpdatedPoints.length - (endIsSpecial ? 3 : 2)];
const thirdToLastPoint =
globalUpdatedPoints[globalUpdatedPoints.length - (endIsSpecial ? 4 : 3)];
const secondToLastPoint = globalUpdatedPoints.at(
globalUpdatedPoints.length - (endIsSpecial ? 3 : 2),
);
const thirdToLastPoint = globalUpdatedPoints.at(
globalUpdatedPoints.length - (endIsSpecial ? 4 : 3),
);
if (!secondToLastPoint || !thirdToLastPoint) {
throw new Error(
`Second and third to last points must exist when handling endpoint drag (${endIsSpecial})`,
);
}
const endIsHorizontal = headingIsHorizontal(endHeading);
const secondIsHorizontal = headingForPointIsHorizontal(
thirdToLastPoint,
@@ -2071,16 +2093,7 @@ const normalizeArrowElementUpdate = (
nextFixedSegments: readonly FixedSegment[] | null,
startIsSpecial?: ExcalidrawElbowArrowElement["startIsSpecial"],
endIsSpecial?: ExcalidrawElbowArrowElement["startIsSpecial"],
): {
points: LocalPoint[];
x: number;
y: number;
width: number;
height: number;
fixedSegments: readonly FixedSegment[] | null;
startIsSpecial?: ExcalidrawElbowArrowElement["startIsSpecial"];
endIsSpecial?: ExcalidrawElbowArrowElement["startIsSpecial"];
} => {
): ElementUpdate<ExcalidrawElbowArrowElement> => {
const offsetX = global[0][0];
const offsetY = global[0][1];
let points = global.map((p) =>

View File

@@ -8,7 +8,7 @@ import { AppStateDelta, Delta, ElementsDelta } from "../src/delta";
describe("ElementsDelta", () => {
describe("elements delta calculation", () => {
it("should not create removed delta when element gets removed but was already deleted", () => {
it("should not throw when element gets removed but was already deleted", () => {
const element = API.createElement({
type: "rectangle",
x: 100,
@@ -19,12 +19,12 @@ describe("ElementsDelta", () => {
const prevElements = new Map([[element.id, element]]);
const nextElements = new Map();
const delta = ElementsDelta.calculate(prevElements, nextElements);
expect(delta.isEmpty()).toBeTruthy();
expect(() =>
ElementsDelta.calculate(prevElements, nextElements),
).not.toThrow();
});
it("should not create added delta when adding element as already deleted", () => {
it("should not throw when adding element as already deleted", () => {
const element = API.createElement({
type: "rectangle",
x: 100,
@@ -35,12 +35,12 @@ describe("ElementsDelta", () => {
const prevElements = new Map();
const nextElements = new Map([[element.id, element]]);
const delta = ElementsDelta.calculate(prevElements, nextElements);
expect(delta.isEmpty()).toBeTruthy();
expect(() =>
ElementsDelta.calculate(prevElements, nextElements),
).not.toThrow();
});
it("should not create updated delta when there is only version and versionNonce change", () => {
it("should create updated delta even when there is only version and versionNonce change", () => {
const baseElement = API.createElement({
type: "rectangle",
x: 100,
@@ -65,7 +65,24 @@ describe("ElementsDelta", () => {
nextElements as SceneElementsMap,
);
expect(delta.isEmpty()).toBeTruthy();
expect(delta).toEqual(
ElementsDelta.create(
{},
{},
{
[baseElement.id]: Delta.create(
{
version: baseElement.version,
versionNonce: baseElement.versionNonce,
},
{
version: baseElement.version + 1,
versionNonce: baseElement.versionNonce + 1,
},
),
},
),
);
});
});

View File

@@ -102,6 +102,7 @@ import {
Emitter,
isMobile,
MINIMUM_ARROW_SIZE,
DOUBLE_TAP_POSITION_THRESHOLD,
} from "@excalidraw/common";
import {
@@ -531,6 +532,7 @@ export const useExcalidrawActionManager = () =>
let didTapTwice: boolean = false;
let tappedTwiceTimer = 0;
let firstTapPosition: { x: number; y: number } | null = null;
let isHoldingSpace: boolean = false;
let isPanning: boolean = false;
let isDraggingScrollBar: boolean = false;
@@ -2737,6 +2739,12 @@ class App extends React.Component<AppProps, AppState> {
addEventListener(window, EVENT.RESIZE, this.onResize, false),
addEventListener(window, EVENT.UNLOAD, this.onUnload, false),
addEventListener(window, EVENT.BLUR, this.onBlur, false),
addEventListener(
this.excalidrawContainerRef.current,
EVENT.WHEEL,
this.handleWheel,
{ passive: false },
),
addEventListener(
this.excalidrawContainerRef.current,
EVENT.DRAG_OVER,
@@ -2983,6 +2991,7 @@ class App extends React.Component<AppProps, AppState> {
private static resetTapTwice() {
didTapTwice = false;
firstTapPosition = null;
}
private onTouchStart = (event: TouchEvent) => {
@@ -2993,6 +3002,13 @@ class App extends React.Component<AppProps, AppState> {
if (!didTapTwice) {
didTapTwice = true;
if (event.touches.length === 1) {
firstTapPosition = {
x: event.touches[0].clientX,
y: event.touches[0].clientY,
};
}
clearTimeout(tappedTwiceTimer);
tappedTwiceTimer = window.setTimeout(
App.resetTapTwice,
@@ -3000,15 +3016,29 @@ class App extends React.Component<AppProps, AppState> {
);
return;
}
// insert text only if we tapped twice with a single finger
// insert text only if we tapped twice with a single finger at approximately the same position
// event.touches.length === 1 will also prevent inserting text when user's zooming
if (didTapTwice && event.touches.length === 1) {
if (didTapTwice && event.touches.length === 1 && firstTapPosition) {
const touch = event.touches[0];
// @ts-ignore
this.handleCanvasDoubleClick({
clientX: touch.clientX,
clientY: touch.clientY,
});
const distance = pointDistance(
pointFrom(touch.clientX, touch.clientY),
pointFrom(firstTapPosition.x, firstTapPosition.y),
);
// only create text if the second tap is within the threshold of the first tap
// this prevents accidental text creation during dragging/selection
if (distance <= DOUBLE_TAP_POSITION_THRESHOLD) {
// end lasso trail and deselect elements just in case
this.lassoTrail.endPath();
this.deselectElements();
// @ts-ignore
this.handleCanvasDoubleClick({
clientX: touch.clientX,
clientY: touch.clientY,
});
}
didTapTwice = false;
clearTimeout(tappedTwiceTimer);
}
@@ -11159,20 +11189,9 @@ class App extends React.Component<AppProps, AppState> {
event: WheelEvent | React.WheelEvent<HTMLDivElement | HTMLCanvasElement>,
) => {
// if not scrolling on canvas/wysiwyg, ignore
const path = (event as any).composedPath?.() as EventTarget[] | undefined;
const isOnExcalidrawCanvas =
path?.some(
(n) =>
n instanceof HTMLCanvasElement &&
n.classList?.contains("excalidraw__canvas"),
) ||
(event.target as Element | null)?.closest?.(
"canvas.excalidraw__canvas",
) != null;
if (
!(
isOnExcalidrawCanvas ||
event.target instanceof HTMLCanvasElement ||
event.target instanceof HTMLTextAreaElement ||
event.target instanceof HTMLIFrameElement
)

View File

@@ -387,7 +387,10 @@ export const restoreElement = (
elbowed: true,
startBinding: repairBinding(element, element.startBinding),
endBinding: repairBinding(element, element.endBinding),
fixedSegments: element.fixedSegments,
fixedSegments:
element.fixedSegments?.length && base.points.length >= 4
? element.fixedSegments
: null,
startIsSpecial: element.startIsSpecial,
endIsSpecial: element.endIsSpecial,
})

View File

@@ -83,7 +83,7 @@
"@excalidraw/element": "0.18.0",
"@excalidraw/math": "0.18.0",
"@excalidraw/laser-pointer": "1.3.1",
"@excalidraw/mermaid-to-excalidraw": "1.1.2",
"@excalidraw/mermaid-to-excalidraw": "1.1.3",
"@excalidraw/random-username": "1.1.0",
"@radix-ui/react-popover": "1.1.6",
"@radix-ui/react-tabs": "1.1.3",

View File

@@ -282,6 +282,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"added": {},
"removed": {},
"updated": {
"id0": {
"deleted": {
"version": 12,
},
"inserted": {
"version": 11,
},
},
"id1": {
"deleted": {
"boundElements": [],
@@ -396,6 +404,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"version": 12,
},
},
"id15": {
"deleted": {
"version": 10,
},
"inserted": {
"version": 9,
},
},
"id4": {
"deleted": {
"height": "99.19972",
@@ -837,6 +853,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"added": {},
"removed": {},
"updated": {
"id0": {
"deleted": {
"version": 13,
},
"inserted": {
"version": 12,
},
},
"id1": {
"deleted": {
"boundElements": [],
@@ -2632,7 +2656,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"height": 100,
"id": "id0",
"index": "a0",
"isDeleted": true,
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
@@ -2681,7 +2705,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"textAlign": "left",
"type": "text",
"updated": 1,
"version": 6,
"version": 8,
"verticalAlign": "top",
"width": 100,
"x": 15,
@@ -2695,7 +2719,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": null,
"containerId": "id0",
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 5,
@@ -2742,10 +2766,12 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
},
},
"elements": {
"added": {
"added": {},
"removed": {},
"updated": {
"id0": {
"deleted": {
"isDeleted": true,
"isDeleted": false,
"version": 9,
},
"inserted": {
@@ -2774,16 +2800,21 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"y": 10,
},
},
},
"removed": {},
"updated": {
"id5": {
"id1": {
"deleted": {
"containerId": null,
"version": 8,
},
"inserted": {
"containerId": null,
"version": 7,
},
},
"id5": {
"deleted": {
"version": 7,
},
"inserted": {
"containerId": "id0",
"version": 6,
},
},
@@ -3096,6 +3127,14 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"version": 8,
},
},
"id5": {
"deleted": {
"version": 7,
},
"inserted": {
"version": 6,
},
},
},
},
"id": "id9",
@@ -4645,15 +4684,15 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"id1": {
"deleted": {
"angle": 0,
"version": 4,
"version": 8,
"x": 15,
"y": 15,
},
"inserted": {
"angle": 90,
"version": 3,
"x": 205,
"y": 205,
"angle": 0,
"version": 7,
"x": 15,
"y": 15,
},
},
},
@@ -5632,12 +5671,12 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
"updated": {
"id1": {
"deleted": {
"frameId": "id0",
"version": 5,
"frameId": null,
"version": 9,
},
"inserted": {
"frameId": null,
"version": 6,
"version": 8,
},
},
},
@@ -5784,7 +5823,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 5,
"version": 6,
"width": 100,
"x": 0,
"y": 0,
@@ -5816,7 +5855,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 4,
"version": 5,
"width": 100,
"x": 100,
"y": 100,
@@ -5852,7 +5891,74 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"elements": {
"added": {},
"removed": {},
"updated": {},
"updated": {
"id0": {
"deleted": {
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [
"A",
],
"height": 100,
"index": "a0",
"isDeleted": true,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": null,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 5,
"width": 100,
"x": 0,
"y": 0,
},
"inserted": {
"isDeleted": true,
"version": 4,
},
},
"id1": {
"deleted": {
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [
"A",
],
"height": 100,
"index": "a1",
"isDeleted": true,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": null,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 5,
"width": 100,
"x": 100,
"y": 100,
},
"inserted": {
"isDeleted": true,
"version": 4,
},
},
},
},
"id": "id13",
},
@@ -6072,7 +6178,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 8,
"version": 9,
"width": 10,
"x": 20,
"y": 0,
@@ -6102,7 +6208,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 8,
"version": 9,
"width": 10,
"x": 50,
"y": 50,
@@ -6187,7 +6293,39 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"elements": {
"added": {},
"removed": {},
"updated": {},
"updated": {
"id3": {
"deleted": {
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 10,
"index": "a1",
"isDeleted": true,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": null,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 8,
"width": 10,
"x": 20,
"y": 0,
},
"inserted": {
"isDeleted": true,
"version": 7,
},
},
},
},
"id": "id18",
},
@@ -6205,11 +6343,11 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"id3": {
"deleted": {
"backgroundColor": "#ffc9c9",
"version": 8,
"version": 9,
},
"inserted": {
"backgroundColor": "transparent",
"version": 7,
"version": 8,
},
},
},
@@ -6234,7 +6372,39 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"elements": {
"added": {},
"removed": {},
"updated": {},
"updated": {
"id8": {
"deleted": {
"angle": 0,
"backgroundColor": "#ffc9c9",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 10,
"index": "a2",
"isDeleted": true,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": null,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 8,
"width": 10,
"x": 30,
"y": 30,
},
"inserted": {
"isDeleted": true,
"version": 7,
},
},
},
},
"id": "id20",
},
@@ -6251,12 +6421,12 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"updated": {
"id8": {
"deleted": {
"version": 8,
"version": 9,
"x": 50,
"y": 50,
},
"inserted": {
"version": 7,
"version": 8,
"x": 30,
"y": 30,
},
@@ -7104,7 +7274,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"strokeWidth": 2,
"type": "arrow",
"updated": 1,
"version": 8,
"version": 9,
"width": 10,
"x": 0,
"y": 0,
@@ -7135,7 +7305,60 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"elements": {
"added": {},
"removed": {},
"updated": {},
"updated": {
"id0": {
"deleted": {
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"elbowed": false,
"endArrowhead": "arrow",
"endBinding": null,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 10,
"index": "a0",
"isDeleted": true,
"lastCommittedPoint": [
10,
10,
],
"link": null,
"locked": false,
"opacity": 100,
"points": [
[
0,
0,
],
[
10,
10,
],
],
"roughness": 1,
"roundness": {
"type": 2,
},
"startArrowhead": null,
"startBinding": null,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "arrow",
"version": 9,
"width": 10,
"x": 0,
"y": 0,
},
"inserted": {
"isDeleted": true,
"version": 8,
},
},
},
},
"id": "id13",
},
@@ -7344,7 +7567,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 8,
"version": 9,
"width": 10,
"x": 10,
"y": 0,
@@ -7375,7 +7598,39 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"elements": {
"added": {},
"removed": {},
"updated": {},
"updated": {
"id0": {
"deleted": {
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 10,
"index": "a0",
"isDeleted": true,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": null,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 8,
"width": 10,
"x": 10,
"y": 0,
},
"inserted": {
"isDeleted": true,
"version": 7,
},
},
},
},
"id": "id7",
},
@@ -7393,11 +7648,11 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"id0": {
"deleted": {
"backgroundColor": "#ffec99",
"version": 8,
"version": 9,
},
"inserted": {
"backgroundColor": "transparent",
"version": 7,
"version": 8,
},
},
},
@@ -10326,7 +10581,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 8,
"version": 9,
"width": 10,
"x": 10,
"y": 0,
@@ -10409,7 +10664,18 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
"elements": {
"added": {},
"removed": {},
"updated": {},
"updated": {
"id0": {
"deleted": {
"isDeleted": false,
"version": 9,
},
"inserted": {
"isDeleted": false,
"version": 8,
},
},
},
},
"id": "id8",
},
@@ -15775,6 +16041,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"version": 5,
},
},
"id1": {
"deleted": {
"version": 5,
},
"inserted": {
"version": 4,
},
},
"id2": {
"deleted": {
"boundElements": [
@@ -16736,6 +17010,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"version": 5,
},
},
"id1": {
"deleted": {
"version": 6,
},
"inserted": {
"version": 5,
},
},
"id2": {
"deleted": {
"boundElements": [
@@ -17361,6 +17643,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"version": 9,
},
},
"id1": {
"deleted": {
"version": 10,
},
"inserted": {
"version": 9,
},
},
"id2": {
"deleted": {
"boundElements": [
@@ -17722,6 +18012,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"version": 7,
},
},
"id2": {
"deleted": {
"version": 4,
},
"inserted": {
"version": 3,
},
},
},
},
"id": "id21",

View File

@@ -2216,7 +2216,16 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] undo
},
},
},
"updated": {},
"updated": {
"id0": {
"deleted": {
"version": 5,
},
"inserted": {
"version": 3,
},
},
},
},
"id": "id6",
},
@@ -10892,7 +10901,32 @@ exports[`regression tests > make a group and duplicate it > [end of test] undo s
},
},
},
"updated": {},
"updated": {
"id0": {
"deleted": {
"version": 6,
},
"inserted": {
"version": 4,
},
},
"id3": {
"deleted": {
"version": 6,
},
"inserted": {
"version": 4,
},
},
"id6": {
"deleted": {
"version": 6,
},
"inserted": {
"version": 4,
},
},
},
},
"id": "id21",
},

View File

@@ -4055,7 +4055,7 @@ describe("history", () => {
expect.objectContaining({
id: container.id,
boundElements: [{ id: remoteText.id, type: "text" }],
isDeleted: true,
isDeleted: false,
}),
expect.objectContaining({
id: text.id,
@@ -4064,8 +4064,7 @@ describe("history", () => {
}),
expect.objectContaining({
id: remoteText.id,
// unbound
containerId: null,
containerId: container.id,
isDeleted: false,
}),
]);

View File

@@ -1452,14 +1452,15 @@
resolved "https://registry.yarnpkg.com/@excalidraw/markdown-to-text/-/markdown-to-text-0.1.2.tgz#1703705e7da608cf478f17bfe96fb295f55a23eb"
integrity sha512-1nDXBNAojfi3oSFwJswKREkFm5wrSjqay81QlyRv2pkITG/XYB5v+oChENVBQLcxQwX4IUATWvXM5BcaNhPiIg==
"@excalidraw/mermaid-to-excalidraw@1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@excalidraw/mermaid-to-excalidraw/-/mermaid-to-excalidraw-1.1.2.tgz#74d9507971976a7d3d960a1b2e8fb49a9f1f0d22"
integrity sha512-hAFv/TTIsOdoy0dL5v+oBd297SQ+Z88gZ5u99fCIFuEMHfQuPgLhU/ztKhFSTs7fISwVo6fizny/5oQRR3d4tQ==
"@excalidraw/mermaid-to-excalidraw@1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@excalidraw/mermaid-to-excalidraw/-/mermaid-to-excalidraw-1.1.3.tgz#3204642c99f3d49c2ad41108217a5d493ef7fd09"
integrity sha512-/50GUWlGotc+FCMX7nM1P1kWm9vNd3fuq38v7upBp9IHqlw6Zmfyj79eG/0vz1heifuYrSW9yzzv0q9jVALzxg==
dependencies:
"@excalidraw/markdown-to-text" "0.1.2"
mermaid "10.9.3"
mermaid "10.9.4"
nanoid "4.0.2"
react-split "^2.0.14"
"@excalidraw/prettier-config@1.0.2":
version "1.0.2"
@@ -7057,10 +7058,10 @@ merge2@^1.3.0, merge2@^1.4.1:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
mermaid@10.9.3:
version "10.9.3"
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.9.3.tgz#90bc6f15c33dbe5d9507fed31592cc0d88fee9f7"
integrity sha512-V80X1isSEvAewIL3xhmz/rVmc27CVljcsbWxkxlWJWY/1kQa4XOABqpDl2qQLGKzpKm6WbTfUEKImBlUfFYArw==
mermaid@10.9.4:
version "10.9.4"
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.9.4.tgz#985fd4b6d73ae795b87f0b32f620a56d3d6bf1f8"
integrity sha512-VIG2B0R9ydvkS+wShA8sXqkzfpYglM2Qwj7VyUeqzNVqSGPoP/tcaUr3ub4ESykv8eqQJn3p99bHNvYdg3gCHQ==
dependencies:
"@braintree/sanitize-url" "^6.0.1"
"@types/d3-scale" "^4.0.3"
@@ -7963,7 +7964,7 @@ progress@2.0.3, progress@^2.0.0:
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
prop-types@^15.8.1:
prop-types@^15.5.7, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -8108,6 +8109,14 @@ react-remove-scroll@^2.6.3:
use-callback-ref "^1.3.3"
use-sidecar "^1.1.3"
react-split@^2.0.14:
version "2.0.14"
resolved "https://registry.yarnpkg.com/react-split/-/react-split-2.0.14.tgz#ef198259bf43264d605f792fb3384f15f5b34432"
integrity sha512-bKWydgMgaKTg/2JGQnaJPg51T6dmumTWZppFgEbbY0Fbme0F5TuatAScCLaqommbGQQf/ZT1zaejuPDriscISA==
dependencies:
prop-types "^15.5.7"
split.js "^1.6.0"
react-style-singleton@^2.2.2, react-style-singleton@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388"
@@ -8747,6 +8756,11 @@ sourcemap-codec@^1.4.8:
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
split.js@^1.6.0:
version "1.6.5"
resolved "https://registry.yarnpkg.com/split.js/-/split.js-1.6.5.tgz#f7f61da1044c9984cb42947df4de4fadb5a3f300"
integrity sha512-mPTnGCiS/RiuTNsVhCm9De9cCAUsrNFFviRbADdKiiV+Kk8HKp/0fWu7Kr8pi3/yBmsqLFHuXGT9UUZ+CNLwFw==
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"