Quellcode durchsuchen

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
David Luzar vor 5 Jahren
Ursprung
Commit
9439908b92
4 geänderte Dateien mit 118 neuen und 11 gelöschten Zeilen
  1. 78 0
      src/element/newElement.test.ts
  2. 38 9
      src/element/newElement.ts
  3. 1 1
      src/history.ts
  4. 1 1
      src/scene/data.ts

+ 78 - 0
src/element/newElement.test.ts

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

+ 38 - 9
src/element/newElement.ts

@@ -67,17 +67,46 @@ export function newTextElement(
   return textElement;
 }
 
-export function duplicateElement(element: ReturnType<typeof newElement>) {
-  const copy = {
-    ...element,
-  };
-  if ("points" in copy) {
-    copy.points = Array.isArray(element.points)
-      ? JSON.parse(JSON.stringify(element.points))
-      : element.points;
+// Simplified deep clone for the purpose of cloning ExcalidrawElement only
+//  (doesn't clone Date, RegExp, Map, Set, Typed arrays etc.)
+//
+// Adapted from https://github.com/lukeed/klona
+function _duplicateElement(val: any, depth: number = 0) {
+  if (val == null || typeof val !== "object") {
+    return val;
+  }
+
+  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;
   }
 
-  delete copy.shape;
+  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.seed = randomSeed();
   return copy;

+ 1 - 1
src/history.ts

@@ -13,7 +13,7 @@ class SceneHistory {
   ) {
     return JSON.stringify({
       appState: clearAppStatePropertiesForHistory(appState),
-      elements: elements.map(({ shape, ...element }) => ({
+      elements: elements.map(({ shape, canvas, ...element }) => ({
         ...element,
         shape: null,
         canvas: null,

+ 1 - 1
src/scene/data.ts

@@ -51,7 +51,7 @@ export function serializeAsJSON(
       type: "excalidraw",
       version: 1,
       source: window.location.origin,
-      elements: elements.map(({ shape, isSelected, ...el }) => el),
+      elements: elements.map(({ shape, canvas, isSelected, ...el }) => el),
       appState: cleanAppStateForExport(appState),
     },
     null,