fix: text restore & deletion issues (#9853)
This commit is contained in:
parent
cc8e490c75
commit
54c148f390
@ -259,7 +259,9 @@ export const loadFromFirebase = async (
|
|||||||
}
|
}
|
||||||
const storedScene = docSnap.data() as FirebaseStoredScene;
|
const storedScene = docSnap.data() as FirebaseStoredScene;
|
||||||
const elements = getSyncableElements(
|
const elements = getSyncableElements(
|
||||||
restoreElements(await decryptElements(storedScene, roomKey), null),
|
restoreElements(await decryptElements(storedScene, roomKey), null, {
|
||||||
|
deleteEmptyTextElements: true,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (socket) {
|
if (socket) {
|
||||||
|
|||||||
@ -258,11 +258,16 @@ export const loadScene = async (
|
|||||||
await importFromBackend(id, privateKey),
|
await importFromBackend(id, privateKey),
|
||||||
localDataState?.appState,
|
localDataState?.appState,
|
||||||
localDataState?.elements,
|
localDataState?.elements,
|
||||||
{ repairBindings: true, refreshDimensions: false },
|
{
|
||||||
|
repairBindings: true,
|
||||||
|
refreshDimensions: false,
|
||||||
|
deleteEmptyTextElements: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
data = restore(localDataState || null, null, null, {
|
data = restore(localDataState || null, null, null, {
|
||||||
repairBindings: true,
|
repairBindings: true,
|
||||||
|
deleteEmptyTextElements: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1088,7 +1088,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
const nextElement = nextElements.get(prevElement.id);
|
const nextElement = nextElements.get(prevElement.id);
|
||||||
|
|
||||||
if (!nextElement) {
|
if (!nextElement) {
|
||||||
const deleted = { ...prevElement, isDeleted: false } as ElementPartial;
|
const deleted = { ...prevElement } as ElementPartial;
|
||||||
|
|
||||||
const inserted = {
|
const inserted = {
|
||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
@ -1102,7 +1102,10 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
ElementsDelta.stripIrrelevantProps,
|
ElementsDelta.stripIrrelevantProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
removed[prevElement.id] = delta;
|
// ignore updates which would "delete" already deleted element
|
||||||
|
if (!prevElement.isDeleted) {
|
||||||
|
removed[prevElement.id] = delta;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1118,7 +1121,6 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
|
|
||||||
const inserted = {
|
const inserted = {
|
||||||
...nextElement,
|
...nextElement,
|
||||||
isDeleted: false,
|
|
||||||
} as ElementPartial;
|
} as ElementPartial;
|
||||||
|
|
||||||
const delta = Delta.create(
|
const delta = Delta.create(
|
||||||
@ -1127,7 +1129,10 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
ElementsDelta.stripIrrelevantProps,
|
ElementsDelta.stripIrrelevantProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
added[nextElement.id] = delta;
|
// ignore updates which would "delete" already deleted element
|
||||||
|
if (!nextElement.isDeleted) {
|
||||||
|
added[nextElement.id] = delta;
|
||||||
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -1156,8 +1161,13 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// making sure there are at least some changes
|
const strippedDeleted = ElementsDelta.stripVersionProps(delta.deleted);
|
||||||
if (!Delta.isEmpty(delta)) {
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1273,8 +1283,15 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
latestDelta = delta;
|
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
|
// it might happen that after applying latest changes the delta itself does not contain any changes
|
||||||
if (Delta.isInnerDifferent(latestDelta.deleted, latestDelta.inserted)) {
|
if (Delta.isInnerDifferent(strippedDeleted, strippedInserted)) {
|
||||||
modifiedDeltas[id] = latestDelta;
|
modifiedDeltas[id] = latestDelta;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1854,4 +1871,12 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
|
|
||||||
return strippedPartial;
|
return strippedPartial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static stripVersionProps(
|
||||||
|
partial: Partial<OrderedExcalidrawElement>,
|
||||||
|
): ElementPartial {
|
||||||
|
const { version, versionNonce, ...strippedPartial } = partial;
|
||||||
|
|
||||||
|
return strippedPartial;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,74 @@
|
|||||||
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
|
|
||||||
import type { ObservedAppState } from "@excalidraw/excalidraw/types";
|
import type { ObservedAppState } from "@excalidraw/excalidraw/types";
|
||||||
import type { LinearElementEditor } from "@excalidraw/element";
|
import type { LinearElementEditor } from "@excalidraw/element";
|
||||||
|
import type { SceneElementsMap } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { AppStateDelta } from "../src/delta";
|
import { AppStateDelta, ElementsDelta } from "../src/delta";
|
||||||
|
|
||||||
|
describe("ElementsDelta", () => {
|
||||||
|
describe("elements delta calculation", () => {
|
||||||
|
it("should not create removed delta when element gets removed but was already deleted", () => {
|
||||||
|
const element = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
isDeleted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const prevElements = new Map([[element.id, element]]);
|
||||||
|
const nextElements = new Map();
|
||||||
|
|
||||||
|
const delta = ElementsDelta.calculate(prevElements, nextElements);
|
||||||
|
|
||||||
|
expect(delta.isEmpty()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not create added delta when adding element as already deleted", () => {
|
||||||
|
const element = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
isDeleted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const prevElements = new Map();
|
||||||
|
const nextElements = new Map([[element.id, element]]);
|
||||||
|
|
||||||
|
const delta = ElementsDelta.calculate(prevElements, nextElements);
|
||||||
|
|
||||||
|
expect(delta.isEmpty()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not create updated delta when there is only version and versionNonce change", () => {
|
||||||
|
const baseElement = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
strokeColor: "#000000",
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
});
|
||||||
|
|
||||||
|
const modifiedElement = {
|
||||||
|
...baseElement,
|
||||||
|
version: baseElement.version + 1,
|
||||||
|
versionNonce: baseElement.versionNonce + 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create maps for the delta calculation
|
||||||
|
const prevElements = new Map([[baseElement.id, baseElement]]);
|
||||||
|
const nextElements = new Map([[modifiedElement.id, modifiedElement]]);
|
||||||
|
|
||||||
|
// Calculate the delta
|
||||||
|
const delta = ElementsDelta.calculate(
|
||||||
|
prevElements as SceneElementsMap,
|
||||||
|
nextElements as SceneElementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(delta.isEmpty()).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("AppStateDelta", () => {
|
describe("AppStateDelta", () => {
|
||||||
describe("ensure stable delta properties order", () => {
|
describe("ensure stable delta properties order", () => {
|
||||||
|
|||||||
@ -2342,7 +2342,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const scene = restore(initialData, null, null, { repairBindings: true });
|
const scene = restore(initialData, null, null, {
|
||||||
|
repairBindings: true,
|
||||||
|
deleteEmptyTextElements: true,
|
||||||
|
});
|
||||||
scene.appState = {
|
scene.appState = {
|
||||||
...scene.appState,
|
...scene.appState,
|
||||||
theme: this.props.theme || scene.appState.theme,
|
theme: this.props.theme || scene.appState.theme,
|
||||||
@ -3200,7 +3203,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
retainSeed?: boolean;
|
retainSeed?: boolean;
|
||||||
fitToContent?: boolean;
|
fitToContent?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const elements = restoreElements(opts.elements, null, undefined);
|
const elements = restoreElements(opts.elements, null, {
|
||||||
|
deleteEmptyTextElements: true,
|
||||||
|
});
|
||||||
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
||||||
|
|
||||||
const elementsCenterX = distance(minX, maxX) / 2;
|
const elementsCenterX = distance(minX, maxX) / 2;
|
||||||
@ -4927,17 +4932,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}),
|
}),
|
||||||
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => {
|
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => {
|
||||||
const isDeleted = !nextOriginalText.trim();
|
const isDeleted = !nextOriginalText.trim();
|
||||||
|
updateElement(nextOriginalText, isDeleted);
|
||||||
|
|
||||||
if (isDeleted && !isExistingElement) {
|
|
||||||
// let's just remove the element from the scene, as it's an empty just created text element
|
|
||||||
this.scene.replaceAllElements(
|
|
||||||
this.scene
|
|
||||||
.getElementsIncludingDeleted()
|
|
||||||
.filter((x) => x.id !== element.id),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
updateElement(nextOriginalText, isDeleted);
|
|
||||||
}
|
|
||||||
// select the created text element only if submitting via keyboard
|
// select the created text element only if submitting via keyboard
|
||||||
// (when submitting via click it should act as signal to deselect)
|
// (when submitting via click it should act as signal to deselect)
|
||||||
if (!isDeleted && viaKeyboard) {
|
if (!isDeleted && viaKeyboard) {
|
||||||
@ -4961,15 +4957,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDeleted) {
|
if (isDeleted) {
|
||||||
fixBindingsAfterDeletion(this.scene.getNonDeletedElements(), [
|
fixBindingsAfterDeletion(this.scene.getNonDeletedElements(), [
|
||||||
element,
|
element,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// we need to record either way, whether the text element was added or removed
|
if (!isDeleted || isExistingElement) {
|
||||||
// since we need to sync this delta to other clients, otherwise it would end up with inconsistencies
|
this.store.scheduleCapture();
|
||||||
this.store.scheduleCapture();
|
}
|
||||||
|
|
||||||
flushSync(() => {
|
flushSync(() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|||||||
@ -170,7 +170,11 @@ export const loadSceneOrLibraryFromBlob = async (
|
|||||||
},
|
},
|
||||||
localAppState,
|
localAppState,
|
||||||
localElements,
|
localElements,
|
||||||
{ repairBindings: true, refreshDimensions: false },
|
{
|
||||||
|
repairBindings: true,
|
||||||
|
refreshDimensions: false,
|
||||||
|
deleteEmptyTextElements: true,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
} else if (isValidLibrary(data)) {
|
} else if (isValidLibrary(data)) {
|
||||||
|
|||||||
@ -241,8 +241,9 @@ const restoreElementWithProperties = <
|
|||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
const restoreElement = (
|
export const restoreElement = (
|
||||||
element: Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
element: Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
||||||
|
opts?: { deleteEmptyTextElements?: boolean },
|
||||||
): typeof element | null => {
|
): typeof element | null => {
|
||||||
element = { ...element };
|
element = { ...element };
|
||||||
|
|
||||||
@ -290,7 +291,7 @@ const restoreElement = (
|
|||||||
|
|
||||||
// if empty text, mark as deleted. We keep in array
|
// if empty text, mark as deleted. We keep in array
|
||||||
// for data integrity purposes (collab etc.)
|
// for data integrity purposes (collab etc.)
|
||||||
if (!text && !element.isDeleted) {
|
if (opts?.deleteEmptyTextElements && !text && !element.isDeleted) {
|
||||||
// TODO: we should not do this since it breaks sync / versioning when we exchange / apply just deltas and restore the elements (deletion isn't recorded)
|
// TODO: we should not do this since it breaks sync / versioning when we exchange / apply just deltas and restore the elements (deletion isn't recorded)
|
||||||
element = { ...element, originalText: text, isDeleted: true };
|
element = { ...element, originalText: text, isDeleted: true };
|
||||||
element = bumpVersion(element);
|
element = bumpVersion(element);
|
||||||
@ -524,7 +525,13 @@ export const restoreElements = (
|
|||||||
elements: ImportedDataState["elements"],
|
elements: ImportedDataState["elements"],
|
||||||
/** NOTE doesn't serve for reconciliation */
|
/** NOTE doesn't serve for reconciliation */
|
||||||
localElements: readonly ExcalidrawElement[] | null | undefined,
|
localElements: readonly ExcalidrawElement[] | null | undefined,
|
||||||
opts?: { refreshDimensions?: boolean; repairBindings?: boolean } | undefined,
|
opts?:
|
||||||
|
| {
|
||||||
|
refreshDimensions?: boolean;
|
||||||
|
repairBindings?: boolean;
|
||||||
|
deleteEmptyTextElements?: boolean;
|
||||||
|
}
|
||||||
|
| undefined,
|
||||||
): OrderedExcalidrawElement[] => {
|
): OrderedExcalidrawElement[] => {
|
||||||
// used to detect duplicate top-level element ids
|
// used to detect duplicate top-level element ids
|
||||||
const existingIds = new Set<string>();
|
const existingIds = new Set<string>();
|
||||||
@ -534,7 +541,12 @@ export const restoreElements = (
|
|||||||
// filtering out selection, which is legacy, no longer kept in elements,
|
// filtering out selection, which is legacy, no longer kept in elements,
|
||||||
// and causing issues if retained
|
// and causing issues if retained
|
||||||
if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
|
if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
|
||||||
let migratedElement: ExcalidrawElement | null = restoreElement(element);
|
let migratedElement: ExcalidrawElement | null = restoreElement(
|
||||||
|
element,
|
||||||
|
{
|
||||||
|
deleteEmptyTextElements: opts?.deleteEmptyTextElements,
|
||||||
|
},
|
||||||
|
);
|
||||||
if (migratedElement) {
|
if (migratedElement) {
|
||||||
const localElement = localElementsMap?.get(element.id);
|
const localElement = localElementsMap?.get(element.id);
|
||||||
if (localElement && localElement.version > migratedElement.version) {
|
if (localElement && localElement.version > migratedElement.version) {
|
||||||
@ -791,7 +803,11 @@ export const restore = (
|
|||||||
*/
|
*/
|
||||||
localAppState: Partial<AppState> | null | undefined,
|
localAppState: Partial<AppState> | null | undefined,
|
||||||
localElements: readonly ExcalidrawElement[] | null | undefined,
|
localElements: readonly ExcalidrawElement[] | null | undefined,
|
||||||
elementsConfig?: { refreshDimensions?: boolean; repairBindings?: boolean },
|
elementsConfig?: {
|
||||||
|
refreshDimensions?: boolean;
|
||||||
|
repairBindings?: boolean;
|
||||||
|
deleteEmptyTextElements?: boolean;
|
||||||
|
},
|
||||||
): RestoredDataState => {
|
): RestoredDataState => {
|
||||||
return {
|
return {
|
||||||
elements: restoreElements(data?.elements, localElements, elementsConfig),
|
elements: restoreElements(data?.elements, localElements, elementsConfig),
|
||||||
|
|||||||
@ -229,6 +229,7 @@ export { defaultLang, useI18n, languages } from "./i18n";
|
|||||||
export {
|
export {
|
||||||
restore,
|
restore,
|
||||||
restoreAppState,
|
restoreAppState,
|
||||||
|
restoreElement,
|
||||||
restoreElements,
|
restoreElements,
|
||||||
restoreLibraryItems,
|
restoreLibraryItems,
|
||||||
} from "./data/restore";
|
} from "./data/restore";
|
||||||
|
|||||||
@ -282,14 +282,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"added": {},
|
"added": {},
|
||||||
"removed": {},
|
"removed": {},
|
||||||
"updated": {
|
"updated": {
|
||||||
"id0": {
|
|
||||||
"deleted": {
|
|
||||||
"version": 17,
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"version": 15,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"id1": {
|
"id1": {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"boundElements": [],
|
"boundElements": [],
|
||||||
@ -404,14 +396,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"version": 17,
|
"version": 17,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"id15": {
|
|
||||||
"deleted": {
|
|
||||||
"version": 14,
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"version": 12,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"id4": {
|
"id4": {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"height": "99.19972",
|
"height": "99.19972",
|
||||||
@ -853,14 +837,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"added": {},
|
"added": {},
|
||||||
"removed": {},
|
"removed": {},
|
||||||
"updated": {
|
"updated": {
|
||||||
"id0": {
|
|
||||||
"deleted": {
|
|
||||||
"version": 18,
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"version": 16,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"id1": {
|
"id1": {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"boundElements": [],
|
"boundElements": [],
|
||||||
@ -2656,7 +2632,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"height": 100,
|
"height": 100,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": true,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@ -2667,7 +2643,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 6,
|
"version": 9,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 10,
|
"y": 10,
|
||||||
@ -2719,7 +2695,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"autoResize": true,
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id0",
|
"containerId": null,
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 5,
|
"fontFamily": 5,
|
||||||
@ -2744,7 +2720,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"textAlign": "left",
|
"textAlign": "left",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 7,
|
"version": 10,
|
||||||
"verticalAlign": "top",
|
"verticalAlign": "top",
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 15,
|
"x": 15,
|
||||||
@ -2766,15 +2742,49 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"elements": {
|
"elements": {
|
||||||
"added": {},
|
"added": {
|
||||||
|
"id0": {
|
||||||
|
"deleted": {
|
||||||
|
"isDeleted": true,
|
||||||
|
"version": 9,
|
||||||
|
},
|
||||||
|
"inserted": {
|
||||||
|
"angle": 0,
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"boundElements": null,
|
||||||
|
"customData": undefined,
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"frameId": null,
|
||||||
|
"groupIds": [],
|
||||||
|
"height": 100,
|
||||||
|
"index": "a0",
|
||||||
|
"isDeleted": false,
|
||||||
|
"link": null,
|
||||||
|
"locked": false,
|
||||||
|
"opacity": 100,
|
||||||
|
"roughness": 1,
|
||||||
|
"roundness": null,
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"type": "rectangle",
|
||||||
|
"version": 8,
|
||||||
|
"width": 100,
|
||||||
|
"x": 10,
|
||||||
|
"y": 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
"removed": {},
|
"removed": {},
|
||||||
"updated": {
|
"updated": {
|
||||||
"id5": {
|
"id5": {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"version": 7,
|
"containerId": null,
|
||||||
|
"version": 10,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"version": 5,
|
"containerId": "id0",
|
||||||
|
"version": 9,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -3086,14 +3096,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"version": 10,
|
"version": 10,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"id5": {
|
|
||||||
"deleted": {
|
|
||||||
"version": 11,
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"version": 9,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"id": "id9",
|
"id": "id9",
|
||||||
@ -4643,15 +4645,15 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"id1": {
|
"id1": {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
"version": 5,
|
"version": 4,
|
||||||
"x": 15,
|
"x": 15,
|
||||||
"y": 15,
|
"y": 15,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"angle": 0,
|
"angle": 90,
|
||||||
"version": 7,
|
"version": 3,
|
||||||
"x": 15,
|
"x": 205,
|
||||||
"y": 15,
|
"y": 205,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -5630,12 +5632,12 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
|
|||||||
"updated": {
|
"updated": {
|
||||||
"id1": {
|
"id1": {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"frameId": null,
|
"frameId": "id0",
|
||||||
"version": 10,
|
"version": 5,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"version": 8,
|
"version": 6,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -15773,14 +15775,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"version": 5,
|
"version": 5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"id1": {
|
|
||||||
"deleted": {
|
|
||||||
"version": 6,
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"version": 4,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"id2": {
|
"id2": {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
@ -16742,14 +16736,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"version": 5,
|
"version": 5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"id1": {
|
|
||||||
"deleted": {
|
|
||||||
"version": 8,
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"version": 6,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"id2": {
|
"id2": {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
@ -17375,14 +17361,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"version": 9,
|
"version": 9,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"id1": {
|
|
||||||
"deleted": {
|
|
||||||
"version": 12,
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"version": 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"id2": {
|
"id2": {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
@ -17744,14 +17722,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"version": 7,
|
"version": 7,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"id2": {
|
|
||||||
"deleted": {
|
|
||||||
"version": 5,
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"version": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"id": "id21",
|
"id": "id21",
|
||||||
|
|||||||
@ -2216,16 +2216,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] undo
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"updated": {
|
"updated": {},
|
||||||
"id0": {
|
|
||||||
"deleted": {
|
|
||||||
"version": 5,
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"version": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"id": "id6",
|
"id": "id6",
|
||||||
},
|
},
|
||||||
@ -10901,32 +10892,7 @@ 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",
|
"id": "id21",
|
||||||
},
|
},
|
||||||
|
|||||||
@ -85,6 +85,23 @@ describe("restoreElements", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should not delete empty text element when deleteEmptyTextElements is not defined", () => {
|
||||||
|
const textElement = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "",
|
||||||
|
isDeleted: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const restoredElements = restore.restoreElements([textElement], null);
|
||||||
|
|
||||||
|
expect(restoredElements).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: textElement.id,
|
||||||
|
isDeleted: false,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("should restore text element correctly with unknown font family, null text and undefined alignment", () => {
|
it("should restore text element correctly with unknown font family, null text and undefined alignment", () => {
|
||||||
const textElement: any = API.createElement({
|
const textElement: any = API.createElement({
|
||||||
type: "text",
|
type: "text",
|
||||||
@ -97,10 +114,9 @@ describe("restoreElements", () => {
|
|||||||
textElement.font = "10 unknown";
|
textElement.font = "10 unknown";
|
||||||
|
|
||||||
expect(textElement.isDeleted).toBe(false);
|
expect(textElement.isDeleted).toBe(false);
|
||||||
const restoredText = restore.restoreElements(
|
const restoredText = restore.restoreElements([textElement], null, {
|
||||||
[textElement],
|
deleteEmptyTextElements: true,
|
||||||
null,
|
})[0] as ExcalidrawTextElement;
|
||||||
)[0] as ExcalidrawTextElement;
|
|
||||||
expect(restoredText.isDeleted).toBe(true);
|
expect(restoredText.isDeleted).toBe(true);
|
||||||
expect(restoredText).toMatchSnapshot({
|
expect(restoredText).toMatchSnapshot({
|
||||||
seed: expect.any(Number),
|
seed: expect.any(Number),
|
||||||
|
|||||||
@ -4055,7 +4055,7 @@ describe("history", () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: container.id,
|
id: container.id,
|
||||||
boundElements: [{ id: remoteText.id, type: "text" }],
|
boundElements: [{ id: remoteText.id, type: "text" }],
|
||||||
isDeleted: false, // isDeleted got remotely updated to false
|
isDeleted: true,
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: text.id,
|
id: text.id,
|
||||||
@ -4065,7 +4065,7 @@ describe("history", () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: remoteText.id,
|
id: remoteText.id,
|
||||||
// unbound
|
// unbound
|
||||||
containerId: container.id,
|
containerId: null,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -704,7 +704,7 @@ describe("textWysiwyg", () => {
|
|||||||
rectangle.x + rectangle.width / 2,
|
rectangle.x + rectangle.width / 2,
|
||||||
rectangle.y + rectangle.height / 2,
|
rectangle.y + rectangle.height / 2,
|
||||||
);
|
);
|
||||||
expect(h.elements.length).toBe(2);
|
expect(h.elements.length).toBe(3);
|
||||||
|
|
||||||
text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
expect(text.type).toBe("text");
|
expect(text.type).toBe("text");
|
||||||
@ -1198,7 +1198,11 @@ describe("textWysiwyg", () => {
|
|||||||
updateTextEditor(editor, " ");
|
updateTextEditor(editor, " ");
|
||||||
Keyboard.exitTextEditor(editor);
|
Keyboard.exitTextEditor(editor);
|
||||||
expect(rectangle.boundElements).toStrictEqual([]);
|
expect(rectangle.boundElements).toStrictEqual([]);
|
||||||
expect(h.elements[1]).toBeUndefined();
|
expect(h.elements[1]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
isDeleted: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should restore original container height and clear cache once text is unbind", async () => {
|
it("should restore original container height and clear cache once text is unbind", async () => {
|
||||||
|
|||||||
@ -49,6 +49,7 @@ export const exportToCanvas = ({
|
|||||||
{ elements, appState },
|
{ elements, appState },
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
{ deleteEmptyTextElements: true },
|
||||||
);
|
);
|
||||||
const { exportBackground, viewBackgroundColor } = restoredAppState;
|
const { exportBackground, viewBackgroundColor } = restoredAppState;
|
||||||
return _exportToCanvas(
|
return _exportToCanvas(
|
||||||
@ -179,6 +180,7 @@ export const exportToSvg = async ({
|
|||||||
{ elements, appState },
|
{ elements, appState },
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
{ deleteEmptyTextElements: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
const exportAppState = {
|
const exportAppState = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user