Repository: maxGraph/maxGraph
Pull request #447 (2024-06-07 15:16:25)

refactor!: move `GraphDataModel.cloneCell` to `cellArrayUtils`
BREAKING CHANGES:
The `GraphDataModel.cloneCell` function has been moved to the `cellArrayUtils` namespace. The function doesn't use any internal `GraphDataModel` state, and moving it into `cellArrayUtils` is consistent with the `cloneCells` function already there.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c74a9be26..f40017ab6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,8 @@ GlobalConfig.logger = new MaxLogAsLogger();
 Previously it was returning a function and this was an extra indirection. This is now simpler to use and match the signature of the mxGraph function.
 - `cellArrayUtils.restoreClone` is no longer available. It was intended to be private.
 - The signature of `cellArrayUtils.cloneCells` has changed. It now returns an array of Cells instead of a function. 
+- The `GraphDataModel.cloneCell` function has been moved to the `cellArrayUtils` namespace. The function doesn't use any internal `GraphDataModel`
+state, and moving it into `cellArrayUtils` is consistent with the `cloneCells` function already there.
 
 ## 0.10.3
 
diff --git a/packages/core/src/editor/Editor.ts b/packages/core/src/editor/Editor.ts
index 9af472993..95ce7f57c 100644
--- a/packages/core/src/editor/Editor.ts
+++ b/packages/core/src/editor/Editor.ts
@@ -54,6 +54,7 @@ import { CellStateStyle, MouseListenerSet } from '../types';
 import ConnectionHandler from '../view/handler/ConnectionHandler';
 import { show } from '../util/printUtils';
 import PanningHandler from '../view/handler/PanningHandler';
+import { cloneCell } from '../util/cellArrayUtils';
 
 /**
  * Installs the required language resources at class
@@ -314,7 +315,7 @@ if (mxLoadResources) {
  *
  * ```javascript
  * var template = editor.templates['task'];
- * var clone = editor.graph.model.cloneCell(template);
+ * var clone = cloneCell(template);
  * ```
  *
  * #### Translations:
@@ -1856,8 +1857,7 @@ export class Editor extends EventSource {
    * @returns Cell
    */
   createGroup(): Cell {
-    const model = this.graph.getDataModel();
-    return <Cell>model.cloneCell(this.defaultGroup);
+    return <Cell>cloneCell(this.defaultGroup);
   }
 
   /**
@@ -2483,12 +2483,11 @@ export class Editor extends EventSource {
    * @param target
    */
   createEdge(source: Cell | null, target: Cell | null): Cell {
-    // Clones the defaultedge prototype
+    // Clones the default edge prototype
     let e: Cell;
 
     if (this.defaultEdge != null) {
-      const model = this.graph.getDataModel();
-      e = <Cell>model.cloneCell(this.defaultEdge);
+      e = <Cell>cloneCell(this.defaultEdge);
     } else {
       e = new Cell('');
       e.setEdge(true);
diff --git a/packages/core/src/util/cellArrayUtils.ts b/packages/core/src/util/cellArrayUtils.ts
index 9a95dbbb9..385b86ee5 100644
--- a/packages/core/src/util/cellArrayUtils.ts
+++ b/packages/core/src/util/cellArrayUtils.ts
@@ -108,6 +108,23 @@ export const getParents = (cells: Cell[]) => {
   return parents;
 };
 
+/**
+ * Returns a deep clone of the given {@link Cell}` (including the children) which is created using {@link cloneCells}`.
+ *
+ * @param cell {@link Cell} to be cloned. Default is `null`.
+ * @param includeChildren Boolean indicating if the cells should be cloned with all descendants. Default is `true`.
+ */
+export const cloneCell = (
+  cell: Cell | null = null,
+  includeChildren = true
+): Cell | null => {
+  if (!cell) {
+    return null;
+  }
+
+  return cloneCells([cell], includeChildren)[0];
+};
+
 /**
  * Returns an array of clones for the given array of {@link Cell}`.
  * Depending on the value of includeChildren, a deep clone is created for
diff --git a/packages/core/src/view/GraphDataModel.ts b/packages/core/src/view/GraphDataModel.ts
index 5f7873f0e..68aa502c0 100644
--- a/packages/core/src/view/GraphDataModel.ts
+++ b/packages/core/src/view/GraphDataModel.ts
@@ -1181,20 +1181,6 @@ export class GraphDataModel extends EventSource {
       this.endUpdate();
     }
   }
-
-  /**
-   * Returns a deep clone of the given {@link Cell}` (including the children) which is created using {@link cloneCells}`.
-   *
-   * @param cell {@link Cell} to be cloned. Default is `null`.
-   * @param includeChildren Boolean indicating if the cells should be cloned with all descendants. Default is `true`.
-   */
-  cloneCell(cell: Cell | null = null, includeChildren = true): Cell | null {
-    if (!cell) {
-      return null;
-    }
-
-    return cloneCells([cell], includeChildren)[0];
-  }
 }
 
 export default GraphDataModel;
diff --git a/packages/html/stories/DynamicToolbar.stories.ts b/packages/html/stories/DynamicToolbar.stories.ts
index ab48166d5..49a8432c2 100644
--- a/packages/html/stories/DynamicToolbar.stories.ts
+++ b/packages/html/stories/DynamicToolbar.stories.ts
@@ -30,6 +30,7 @@ import {
   styleUtils,
   gestureUtils,
   Client,
+  cellArrayUtils,
 } from '@maxgraph/core';
 import {
   globalTypes,
@@ -152,7 +153,7 @@ const Template = ({ label, ...args }: Record<string, string>) => {
     ) => {
       graph.stopEditing(false);
 
-      const vertex = graph.getDataModel().cloneCell(prototype)!;
+      const vertex = cellArrayUtils.cloneCell(prototype)!;
       if (vertex?.geometry) {
         x !== undefined && (vertex.geometry.x = x);
         y !== undefined && (vertex.geometry.y = y);
diff --git a/packages/html/stories/Orthogonal.stories.js b/packages/html/stories/Orthogonal.stories.js
index 3fbac6e85..4ef11109f 100644
--- a/packages/html/stories/Orthogonal.stories.js
+++ b/packages/html/stories/Orthogonal.stories.js
@@ -26,6 +26,7 @@ import {
   EdgeHandler,
   GraphView,
   InternalEvent,
+  cellArrayUtils,
 } from '@maxgraph/core';
 import {
   globalTypes,
@@ -34,8 +35,7 @@ import {
   rubberBandValues,
 } from './shared/args.js';
 import { createGraphContainer } from './shared/configure.js';
-// style required by RubberBand
-import '@maxgraph/core/css/common.css';
+import '@maxgraph/core/css/common.css'; // style required by RubberBand
 
 export default {
   title: 'Connections/Orthogonal',
@@ -168,15 +168,15 @@ const Template = ({ label, ...args }) => {
     );
     v13.geometry.offset = new Point(0, -5);
 
-    const v2 = graph.addCell(graph.getDataModel().cloneCell(v1));
+    const v2 = graph.addCell(cellArrayUtils.cloneCell(v1));
     v2.geometry.x = 200;
     v2.geometry.y = 60;
 
-    const v3 = graph.addCell(graph.getDataModel().cloneCell(v1));
+    const v3 = graph.addCell(cellArrayUtils.cloneCell(v1));
     v3.geometry.x = 40;
     v3.geometry.y = 150;
 
-    const v4 = graph.addCell(graph.getDataModel().cloneCell(v1));
+    const v4 = graph.addCell(cellArrayUtils.cloneCell(v1));
     v4.geometry.x = 200;
     v4.geometry.y = 170;
 
diff --git a/packages/html/stories/Toolbar.stories.ts b/packages/html/stories/Toolbar.stories.ts
index e9d9a4344..64aca8ec0 100644
--- a/packages/html/stories/Toolbar.stories.ts
+++ b/packages/html/stories/Toolbar.stories.ts
@@ -30,6 +30,7 @@ import {
   MaxToolbar,
   Point,
   RubberBandHandler,
+  cellArrayUtils,
 } from '@maxgraph/core';
 import {
   globalTypes,
@@ -206,7 +207,7 @@ const Template = ({ label, ...args }: { [p: string]: any }) => {
       graph.stopEditing(false);
 
       const pt = graph.getPointForEvent(evt);
-      const cellToImport = graph.getDataModel().cloneCell(prototype);
+      const cellToImport = cellArrayUtils.cloneCell(prototype);
       if (!cellToImport) return;
 
       if (cellToImport.geometry) {
diff --git a/packages/html/stories/Wires.stories.js b/packages/html/stories/Wires.stories.js
index 24b79285a..ecd3a7554 100644
--- a/packages/html/stories/Wires.stories.js
+++ b/packages/html/stories/Wires.stories.js
@@ -81,6 +81,7 @@ import {
   TooltipHandler,
   SelectionCellsHandler,
   PopupMenuHandler,
+  cellArrayUtils,
 } from '@maxgraph/core';
 
 import {
@@ -92,8 +93,7 @@ import {
   rubberBandValues,
 } from './shared/args.js';
 import { createGraphContainer } from './shared/configure.js';
-// style required by RubberBand
-import '@maxgraph/core/css/common.css';
+import '@maxgraph/core/css/common.css'; // style required by RubberBand
 
 export default {
   title: 'Connections/Wires',
@@ -869,7 +869,7 @@ const Template = ({ label, ...args }) => {
       v22.geometry.relative = true;
       v22.geometry.offset = new Point(-10, -1);*/
 
-    let v3 = graph.addCell(graph.getDataModel().cloneCell(v1));
+    let v3 = graph.addCell(cellArrayUtils.cloneCell(v1));
     v3.value = 'J3';
     v3.geometry.x = 420;
     v3.geometry.y = 340;
diff --git a/packages/website/docs/usage/migrate-from-mxgraph.md b/packages/website/docs/usage/migrate-from-mxgraph.md
index af165ecd3..e7ef81948 100644
--- a/packages/website/docs/usage/migrate-from-mxgraph.md
+++ b/packages/website/docs/usage/migrate-from-mxgraph.md
@@ -378,6 +378,7 @@ Several methods from the `mxGraphDataModel` class have been moved to the `Cell`
 - `getParent()`
 
 Some methods in `mxGraphModel` that were general manipulation of cells and independent of the model have been moved to the `cellArrayUtils` namespace and are now available as individual functions.
+- `cloneCell()` (from version 0.11.0)
 - `cloneCells()`
 - `getOpposite()`
 - `getParents()`


Test patch:
diff --git a/packages/core/__tests__/util/cellArrayUtils.test.ts b/packages/core/__tests__/util/cellArrayUtils.test.ts
index f06c4e334..37e0dc418 100644
--- a/packages/core/__tests__/util/cellArrayUtils.test.ts
+++ b/packages/core/__tests__/util/cellArrayUtils.test.ts
@@ -15,10 +15,9 @@ limitations under the License.
 */
 
 import { describe, expect, test } from '@jest/globals';
-import { cloneCells, getOpposites } from '../../src/util/cellArrayUtils';
+import { cloneCell, cloneCells, getOpposites } from '../../src/util/cellArrayUtils';
 import { IDENTITY_FIELD_NAME } from '../../src/util/Constants';
 import Cell from '../../src/view/cell/Cell';
-import { GraphDataModel } from '../../src/view/GraphDataModel';
 
 describe('getOpposites', () => {
   const edges: Cell[] = [];
@@ -118,11 +117,8 @@ describe('cloneCells', () => {
 });
 
 describe('cloneCell', () => {
-  // Tests are located here as they are similar to cloneCells test and cloneCell will be move out of GraphDataModel in the future
-  const model = new GraphDataModel();
-
   test('null cell', () => {
-    expect(model.cloneCell(null)).toBeNull();
+    expect(cloneCell(null)).toBeNull();
   });
 
   describe('cell without children', () => {
@@ -134,7 +130,7 @@ describe('cloneCell', () => {
         // @ts-ignore
         expect(cell[IDENTITY_FIELD_NAME]).toBeUndefined();
 
-        const clone = model.cloneCell(cell, includeChildren);
+        const clone = cloneCell(cell, includeChildren);
         expect(clone).not.toBe(cell); // this is not the same instance, but a new one
 
         expect(cell.getParent()).not.toBeNull(); // untouched
@@ -159,7 +155,7 @@ describe('cloneCell', () => {
         // @ts-ignore
         expect(cell[IDENTITY_FIELD_NAME]).toBeUndefined();
 
-        const clone = model.cloneCell(cell, includeChildren);
+        const clone = cloneCell(cell, includeChildren);
         expect(clone).not.toBe(cell); // this is not the same instance, but a new one
 
         expect(cell.getParent()).toBeNull(); // untouched