use a better cloning algorithm (#753)
* use a better cloning algorithm * Revert "use a better cloning algorithm" This reverts commit 7279262129d665ffa92f016802155c1db7c35b7f. * implement custom cloning algorithm * add tests * refactor * don't copy canvas & ignore canvas in related ops * fix tests
This commit is contained in:
parent
5256096d76
commit
9439908b92
78
src/element/newElement.test.ts
Normal file
78
src/element/newElement.test.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { newElement, newTextElement, duplicateElement } from "./newElement";
|
||||||
|
|
||||||
|
function isPrimitive(val: any) {
|
||||||
|
const type = typeof val;
|
||||||
|
return val == null || (type !== "object" && type !== "function");
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertCloneObjects(source: any, clone: any) {
|
||||||
|
for (const key in clone) {
|
||||||
|
if (clone.hasOwnProperty(key) && !isPrimitive(clone[key])) {
|
||||||
|
expect(clone[key]).not.toBe(source[key]);
|
||||||
|
if (source[key]) {
|
||||||
|
assertCloneObjects(source[key], clone[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("clones arrow element", () => {
|
||||||
|
const element = newElement(
|
||||||
|
"arrow",
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
"#000000",
|
||||||
|
"transparent",
|
||||||
|
"hachure",
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
element.__proto__ = { hello: "world" };
|
||||||
|
|
||||||
|
element.points = [
|
||||||
|
[1, 2],
|
||||||
|
[3, 4],
|
||||||
|
];
|
||||||
|
|
||||||
|
const copy = duplicateElement(element);
|
||||||
|
|
||||||
|
assertCloneObjects(element, copy);
|
||||||
|
|
||||||
|
expect(copy.__proto__).toEqual({ hello: "world" });
|
||||||
|
expect(copy.hasOwnProperty("hello")).toBe(false);
|
||||||
|
|
||||||
|
expect(copy.points).not.toBe(element.points);
|
||||||
|
expect(copy).not.toHaveProperty("shape");
|
||||||
|
expect(copy.id).not.toBe(element.id);
|
||||||
|
expect(typeof copy.id).toBe("string");
|
||||||
|
expect(copy.seed).not.toBe(element.seed);
|
||||||
|
expect(typeof copy.seed).toBe("number");
|
||||||
|
expect(copy).toEqual({
|
||||||
|
...element,
|
||||||
|
id: copy.id,
|
||||||
|
seed: copy.seed,
|
||||||
|
shape: undefined,
|
||||||
|
canvas: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clones text element", () => {
|
||||||
|
const element = newTextElement(
|
||||||
|
newElement("text", 0, 0, "#000000", "transparent", "hachure", 1, 1, 100),
|
||||||
|
"hello",
|
||||||
|
"Arial 20px",
|
||||||
|
);
|
||||||
|
|
||||||
|
const copy = duplicateElement(element);
|
||||||
|
|
||||||
|
assertCloneObjects(element, copy);
|
||||||
|
|
||||||
|
expect(copy.points).not.toBe(element.points);
|
||||||
|
expect(copy).not.toHaveProperty("shape");
|
||||||
|
expect(copy.id).not.toBe(element.id);
|
||||||
|
expect(typeof copy.id).toBe("string");
|
||||||
|
expect(typeof copy.seed).toBe("number");
|
||||||
|
});
|
@ -67,17 +67,46 @@ export function newTextElement(
|
|||||||
return textElement;
|
return textElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function duplicateElement(element: ReturnType<typeof newElement>) {
|
// Simplified deep clone for the purpose of cloning ExcalidrawElement only
|
||||||
const copy = {
|
// (doesn't clone Date, RegExp, Map, Set, Typed arrays etc.)
|
||||||
...element,
|
//
|
||||||
};
|
// Adapted from https://github.com/lukeed/klona
|
||||||
if ("points" in copy) {
|
function _duplicateElement(val: any, depth: number = 0) {
|
||||||
copy.points = Array.isArray(element.points)
|
if (val == null || typeof val !== "object") {
|
||||||
? JSON.parse(JSON.stringify(element.points))
|
return val;
|
||||||
: element.points;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
delete copy.shape;
|
if (Object.prototype.toString.call(val) === "[object Object]") {
|
||||||
|
const tmp =
|
||||||
|
typeof val.constructor === "function"
|
||||||
|
? Object.create(Object.getPrototypeOf(val))
|
||||||
|
: {};
|
||||||
|
for (const k in val) {
|
||||||
|
if (val.hasOwnProperty(k)) {
|
||||||
|
// don't copy top-level shape property, which we want to regenerate
|
||||||
|
if (depth === 0 && (k === "shape" || k === "canvas")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tmp[k] = _duplicateElement(val[k], depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
let k = val.length;
|
||||||
|
const arr = new Array(k);
|
||||||
|
while (k--) {
|
||||||
|
arr[k] = _duplicateElement(val[k], depth + 1);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function duplicateElement(element: ReturnType<typeof newElement>) {
|
||||||
|
const copy = _duplicateElement(element);
|
||||||
copy.id = nanoid();
|
copy.id = nanoid();
|
||||||
copy.seed = randomSeed();
|
copy.seed = randomSeed();
|
||||||
return copy;
|
return copy;
|
||||||
|
@ -13,7 +13,7 @@ class SceneHistory {
|
|||||||
) {
|
) {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
appState: clearAppStatePropertiesForHistory(appState),
|
appState: clearAppStatePropertiesForHistory(appState),
|
||||||
elements: elements.map(({ shape, ...element }) => ({
|
elements: elements.map(({ shape, canvas, ...element }) => ({
|
||||||
...element,
|
...element,
|
||||||
shape: null,
|
shape: null,
|
||||||
canvas: null,
|
canvas: null,
|
||||||
|
@ -51,7 +51,7 @@ export function serializeAsJSON(
|
|||||||
type: "excalidraw",
|
type: "excalidraw",
|
||||||
version: 1,
|
version: 1,
|
||||||
source: window.location.origin,
|
source: window.location.origin,
|
||||||
elements: elements.map(({ shape, isSelected, ...el }) => el),
|
elements: elements.map(({ shape, canvas, isSelected, ...el }) => el),
|
||||||
appState: cleanAppStateForExport(appState),
|
appState: cleanAppStateForExport(appState),
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user