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:
David Luzar 2020-02-19 22:28:11 +01:00 committed by GitHub
parent 5256096d76
commit 9439908b92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 118 additions and 11 deletions

View 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");
});

View File

@ -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;

View File

@ -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,

View File

@ -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,