From cdd8830b5c159dd21b6b6fbfaf4e490afb91b95d Mon Sep 17 00:00:00 2001 From: Thomas Bouffard <27200110+tbouffard@users.noreply.github.com> Date: Fri, 7 Jul 2023 07:41:49 +0200 Subject: [PATCH] feat: improve the VertexMixin type (#211) The methods defined in the `VertexMixin` type were not documented. The signature of the `insertVertex` method was using a spread `any` parameter, so it provided no guidance nor guard. Instead, the type now declares the various form of the methods with the right signature. --- README.md | 16 +- .../__tests__/view/mixins/VertexMixin.test.ts | 115 ++++++++ packages/core/src/types.ts | 56 ++++ packages/core/src/view/mixins/VertexMixin.ts | 252 +++++++++++------- packages/ts-example/src/main.ts | 49 ++-- 5 files changed, 362 insertions(+), 126 deletions(-) create mode 100644 packages/core/__tests__/view/mixins/VertexMixin.test.ts diff --git a/README.md b/README.md index bbc14797b..0c79d3e87 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,20 @@ const parent = graph.getDefaultParent(); // Adds cells to the model in a single step graph.batchUpdate(() => { - const vertex01 = graph.insertVertex(parent, null, 'a regular rectangle', 10, 10, 100, 100); - const vertex02 = graph.insertVertex(parent, null, 'a regular ellipse', 350, 90, 50, 50, {shape: 'ellipse', fillColor: 'orange'}); + const vertex01 = graph.insertVertex({ + parent, + position: [10, 10], + size: [100, 100], + style: { shape: 'customRectangle' }, + value: 'a regular rectangle', + }); + const vertex02 = graph.insertVertex({ + parent, + position: [350, 90], + size: [50, 50], + style: { shape: 'ellipse', fillColor: 'orange' }, + value: 'a regular ellipse', + }); graph.insertEdge(parent, null, 'a regular edge', vertex01, vertex02); }); ``` diff --git a/packages/core/__tests__/view/mixins/VertexMixin.test.ts b/packages/core/__tests__/view/mixins/VertexMixin.test.ts new file mode 100644 index 000000000..4499dc7d8 --- /dev/null +++ b/packages/core/__tests__/view/mixins/VertexMixin.test.ts @@ -0,0 +1,115 @@ +/* +Copyright 2023-present The maxGraph project Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { describe, expect, test } from '@jest/globals'; +import { type CellStyle, Geometry, Graph } from '../../../src'; + +function createGraph(): Graph { + // @ts-ignore - no need for a container, we don't check the view here + return new Graph(null); +} + +describe('insertVertex', () => { + test('with several parameters', () => { + const graph = createGraph(); + const style: CellStyle = { rounded: true, shape: 'cloud' }; + const cell = graph.insertVertex(null, 'vertex_1', 'a value', 10, 20, 110, 120, style); + expect(cell.getId()).toBe('vertex_1'); + expect(cell.vertex).toBeTruthy(); + expect(cell.edge).toBeFalsy(); + expect(cell.value).toBe('a value'); + expect(cell.style).toStrictEqual(style); + + const geometry = new Geometry(10, 20, 110, 120); + geometry.relative = false; + expect(cell.geometry).toStrictEqual(geometry); + + // parent created with cell as child + expect(cell.parent).not.toBeNull(); + expect(cell.parent?.id).toBe('1'); // default parent + const children = cell.parent?.children; + expect(children).toContain(cell); + expect(children).toHaveLength(1); + + // ensure that the cell is in the model + const cellFromModel = graph.getDataModel().getCell('vertex_1'); + expect(cellFromModel).toBe(cell); + }); + + test('with single parameter', () => { + const graph = createGraph(); + const style: CellStyle = { align: 'right', fillColor: 'red' }; + const cell = graph.insertVertex({ + value: 'a value', + x: 10, + y: 20, + size: [110, 120], + style, + }); + expect(cell.getId()).toBe('2'); // generated + expect(cell.vertex).toBeTruthy(); + expect(cell.edge).toBeFalsy(); + expect(cell.value).toBe('a value'); + expect(cell.style).toStrictEqual(style); + + const geometry = new Geometry(10, 20, 110, 120); + geometry.relative = false; + expect(cell.geometry).toStrictEqual(geometry); + + // parent created with cell as child + expect(cell.parent).not.toBeNull(); + expect(cell.parent?.id).toBe('1'); // default parent + const children = cell.parent?.children; + expect(children).toContain(cell); + expect(children).toHaveLength(1); + + // ensure that the cell is in the model + const cellFromModel = graph.getDataModel().getCell('2'); + expect(cellFromModel).toBe(cell); + }); + + test('with single parameter and non default parent', () => { + const graph = createGraph(); + + const parentCell = graph.insertVertex({ + value: 'non default', + position: [10, 10], + size: [400, 400], + }); + expect(parentCell.getId()).toBe('2'); // generated + expect(parentCell.value).toBe('non default'); + + const geometryOfParentCell = new Geometry(10, 10, 400, 400); + expect(parentCell.geometry).toStrictEqual(geometryOfParentCell); + + const childCell = graph.insertVertex({ + parent: parentCell, + value: 'child', + position: [5, 5], + width: 400, + height: 400, + relative: true, + }); + const geometry = new Geometry(5, 5, 400, 400); + geometry.relative = true; + expect(childCell.geometry).toStrictEqual(geometry); + + expect(childCell.parent).toBe(parentCell); + const children = parentCell.children; + expect(children).toContain(childCell); + expect(children).toHaveLength(1); + }); +}); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 7a11742a4..014a46dce 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -22,6 +22,7 @@ import type InternalMouseEvent from './view/event/InternalMouseEvent'; import type Shape from './view/geometry/Shape'; import type { Graph } from './view/Graph'; import type ImageBox from './view/image/ImageBox'; +import Geometry from './view/geometry/Geometry'; export type FilterFunction = (cell: Cell) => boolean; @@ -880,6 +881,61 @@ export type GradientMap = { [k: string]: Gradient; }; +export type VertexParameters = { + /** + * Class reference to a class derived from {@link Geometry}. + * This can be useful for defining custom constraints. + * @default {@link Geometry} + */ + geometryClass?: typeof Geometry; + /** + * It is mandatory to set this value or the {@link size} property. + */ + height?: number; + /** + * Optional string that defines the id of the new vertex. If not set, the id is auto-generated when creating the vertex. + */ + id?: string; + /** + * The parent of the new vertex. If not set, use the default parent. + */ + parent?: Cell | null; + /** + * Fallback when the {@link x} or the {@link y} parameters are not set. + * It is mandatory to set this value or the {@link x} and the {@link y} properties. + * Order of the elements: x, y + */ + position?: [number, number]; + /** + * Specifies if the geometry is relative. + * @default false + */ + relative?: boolean; + /** + * Fallback when the {@link width} or the {@link height} parameters are not set. + * It is mandatory to set this value or the {@link width} and the {@link height} properties. + * Order of the elements: width, height + */ + size?: [number, number]; + style?: CellStyle; + /** + * Object to be used as the user object which is generally used to display the label of the vertex. The default implementation handles `string` object. + */ + value?: any; + /** + * It is mandatory to set this value or the {@link size} property. + */ + width?: number; + /** + * It is mandatory to set this value or the {@link position} property. + */ + x?: number; + /** + * It is mandatory to set this value or the {@link position} property. + */ + y?: number; +}; + export interface GraphPluginConstructor { new (graph: Graph): GraphPlugin; pluginId: string; diff --git a/packages/core/src/view/mixins/VertexMixin.ts b/packages/core/src/view/mixins/VertexMixin.ts index 1d87de8ea..206e4fb71 100644 --- a/packages/core/src/view/mixins/VertexMixin.ts +++ b/packages/core/src/view/mixins/VertexMixin.ts @@ -18,30 +18,158 @@ import Cell from '../cell/Cell'; import Geometry from '../geometry/Geometry'; import { Graph } from '../Graph'; import { mixInto } from '../../util/Utils'; -import type { CellStyle } from '../../types'; +import type { CellStyle, VertexParameters } from '../../types'; declare module '../Graph' { interface Graph { + /** + * Specifies the return value for vertices in {@link isLabelMovable}. + * @default false + */ vertexLabelsMovable: boolean; + /** + * Specifies if negative coordinates for vertices are allowed. + * @default true + */ allowNegativeCoordinates: boolean; - + /** + * Returns {@link allowNegativeCoordinates}. + */ isAllowNegativeCoordinates: () => boolean; + /** + * Sets {@link allowNegativeCoordinates}. + */ setAllowNegativeCoordinates: (value: boolean) => void; - insertVertex: (...args: any[]) => Cell; - createVertex: ( - parent: Cell, - id: string, + + /** + * Adds a new vertex into the given parent {@link Cell} using value as the user + * object and the given coordinates as the {@link Geometry} of the new vertex. + * The id and style are used for the respective properties of the new `Cell`, which is returned. + * + * **IMPORTANT**: this is a legacy method to ease the migration from `mxGraph`. Use the {@link insertVertex} method with a single object parameter instead. + * + * When adding new vertices from a mouse event, one should take into + * account the offset of the graph container and the scale and translation + * of the view in order to find the correct unscaled, untranslated + * coordinates using {@link Graph#getPointForEvent} as follows: + * + * ```javascript + * const pt = graph.getPointForEvent(evt); + * const parent = graph.getDefaultParent(); + * graph.insertVertex(parent, null, 'Hello, World!', pt.x, pt.y, 220, 30); + * ``` + * + * For adding image cells, the style parameter can be assigned as + * + * ```javascript + * style: { + * image: imageUrl, + * } + * ``` + * + * See {@link Graph} for more information on using images. + * + * @param parent the parent of the new vertex. If not set, use the default parent. + * @param id Optional string that defines the id of the new vertex. If not set, the id is auto-generated when creating the vertex. + * @param value Object to be used as the user object. + * @param x the x coordinate of the vertex. + * @param y the y coordinate of the vertex. + * @param width the width of the vertex. + * @param height the height of the vertex. + * @param style the cell style. + * @param relative specifies if the geometry is relative. Default is `false`. + * @param geometryClass class reference to a class derived from {@link Geometry}. + * This can be useful for defining custom constraints. Default is {@link Geometry}. + */ + insertVertex( + parent: Cell | null, + id: string | null | undefined, value: any, x: number, y: number, width: number, height: number, - style: CellStyle, - relative: boolean, - geometryClass: typeof Geometry - ) => Cell; + style?: CellStyle, + relative?: boolean, + geometryClass?: typeof Geometry + ): Cell; + + /** + * Adds a new vertex into the given parent {@link Cell} using value as the user + * object and the given coordinates as the {@link Geometry} of the new vertex. + * The id and style are used for the respective properties of the new `Cell`, which is returned. + * + * When adding new vertices from a mouse event, one should take into + * account the offset of the graph container and the scale and translation + * of the view in order to find the correct unscaled, untranslated + * coordinates using {@link Graph#getPointForEvent} as follows: + * + * ```javascript + * const pt = graph.getPointForEvent(evt); + * const parent = graph.getDefaultParent(); + * graph.insertVertex({ + * parent, + * position: [pt.x, pt.y], + * size: [220, 30], + * value: 'Hello, World!', + * }); + * ``` + * + * For adding image cells, the style parameter can be assigned as + * + * ```javascript + * style: { + * image: imageUrl, + * } + * ``` + * + * See {@link Graph} for more information on using images. + * + * @param params the parameters used to create the new vertex. + */ + insertVertex(params: VertexParameters): Cell; + + /** + * Hook method that creates the new vertex for {@link insertVertex}. + * + * @param parent the parent of the new vertex. If not set, use the default parent. + * @param id Optional string that defines the id of the new vertex. If not set, the id is auto-generated when creating the vertex. + * @param value Object to be used as the user object. + * @param x the x coordinate of the vertex. + * @param y the y coordinate of the vertex. + * @param width the width of the vertex. + * @param height the height of the vertex. + * @param style the cell style. + * @param relative specifies if the geometry is relative. Default is `false`. + * @param geometryClass class reference to a class derived from {@link Geometry}. + * This can be useful for defining custom constraints. Default is {@link Geometry}. + */ + createVertex( + parent: Cell | null, + id: string | null | undefined, + value: any, + x: number, + y: number, + width: number, + height: number, + style?: CellStyle, + relative?: boolean, + geometryClass?: typeof Geometry + ): Cell; + + /** + * Returns the visible child vertices of the given parent. + * + * @param parent the {@link Cell} whose children should be returned. + */ getChildVertices: (parent?: Cell | null) => Cell[]; + /** + * Returns {@link vertexLabelsMovable}. + */ isVertexLabelsMovable: () => boolean; + /** + * Sets {@link vertexLabelsMovable}. + */ setVertexLabelsMovable: (value: boolean) => void; } } @@ -53,81 +181,30 @@ type PartialVertex = Pick< | 'allowNegativeCoordinates' | 'isAllowNegativeCoordinates' | 'setAllowNegativeCoordinates' - | 'insertVertex' | 'createVertex' | 'getChildVertices' | 'isVertexLabelsMovable' | 'setVertexLabelsMovable' ->; +> & { + // handle the methods defined in the Graph interface with a single implementation + insertVertex: (...args: any[]) => Cell; +}; type PartialType = PartialGraph & PartialVertex; // @ts-expect-error The properties of PartialGraph are defined elsewhere. const VertexMixin: PartialType = { - /** - * Specifies the return value for vertices in {@link isLabelMovable}. - * @default false - */ vertexLabelsMovable: false, - /** - * Specifies if negative coordinates for vertices are allowed. - * @default true - */ allowNegativeCoordinates: true, - /** - * Returns {@link allowNegativeCoordinates}. - */ isAllowNegativeCoordinates() { return this.allowNegativeCoordinates; }, - /** - * Sets {@link allowNegativeCoordinates}. - */ setAllowNegativeCoordinates(value: boolean) { this.allowNegativeCoordinates = value; }, - /** - * Adds a new vertex into the given parent using value as the user - * object and the given coordinates as the {@link Geometry} of the new vertex. - * The id and style are used for the respective properties of the new - * , which is returned. - * - * When adding new vertices from a mouse event, one should take into - * account the offset of the graph container and the scale and translation - * of the view in order to find the correct unscaled, untranslated - * coordinates using {@link Graph#getPointForEvent} as follows: - * - * ```javascript - * let pt = graph.getPointForEvent(evt); - * let parent = graph.getDefaultParent(); - * graph.insertVertex(parent, null, - * 'Hello, World!', x, y, 220, 30); - * ``` - * - * For adding image cells, the style parameter can be assigned as - * - * ```javascript - * stylename;image=imageUrl - * ``` - * - * See {@link Graph} for more information on using images. - * - * @param parent that specifies the parent of the new vertex. - * @param id Optional string that defines the Id of the new vertex. - * @param value Object to be used as the user object. - * @param x Integer that defines the x coordinate of the vertex. - * @param y Integer that defines the y coordinate of the vertex. - * @param width Integer that defines the width of the vertex. - * @param height Integer that defines the height of the vertex. - * @param style Optional object that defines the cell style. - * @param relative Optional boolean that specifies if the geometry is relative. - * Default is false. - * @param geometryClass Optional class reference to a class derived from mxGeometry. - * This can be useful for defining custom constraints. - */ insertVertex(...args) { let parent; let id; @@ -140,9 +217,7 @@ const VertexMixin: PartialType = { let relative; let geometryClass; - if (args.length === 1) { - // If only a single parameter, treat as an object - // This syntax can be more readable + if (args.length === 1 && typeof args[0] === 'object') { const params = args[0]; parent = params.parent; id = params.id; @@ -161,9 +236,6 @@ const VertexMixin: PartialType = { [parent, id, value, x, y, width, height, style, relative, geometryClass] = args; } - if (typeof style === 'string') - throw new Error(`String-typed style is no longer supported: ${style}`); - const vertex = this.createVertex( parent, id, @@ -180,24 +252,21 @@ const VertexMixin: PartialType = { return this.addCell(vertex, parent); }, - /** - * Hook method that creates the new vertex for . - */ createVertex( - parent, - id, - value, - x, - y, - width, - height, - style, + parent: Cell | null, + id: string, + value: any, + x: number, + y: number, + width: number, + height: number, + style?: CellStyle, relative = false, geometryClass = Geometry ) { // Creates the geometry for the vertex const geometry = new geometryClass(x, y, width, height); - geometry.relative = relative != null ? relative : false; + geometry.relative = relative; // Creates the vertex const vertex = new Cell(value, geometry, style); @@ -208,29 +277,18 @@ const VertexMixin: PartialType = { return vertex; }, - /** - * Returns the visible child vertices of the given parent. - * - * @param parent {@link mxCell} whose children should be returned. - */ getChildVertices(parent) { return this.getChildCells(parent, true, false); }, - /***************************************************************************** - * Group: Graph Behaviour - *****************************************************************************/ + // *************************************************************************** + // Group: Graph Behaviour + // *************************************************************************** - /** - * Returns {@link vertexLabelsMovable}. - */ isVertexLabelsMovable() { return this.vertexLabelsMovable; }, - /** - * Sets {@link vertexLabelsMovable}. - */ setVertexLabelsMovable(value: boolean) { this.vertexLabelsMovable = value; }, diff --git a/packages/ts-example/src/main.ts b/packages/ts-example/src/main.ts index 8157b5166..cf031e049 100644 --- a/packages/ts-example/src/main.ts +++ b/packages/ts-example/src/main.ts @@ -15,13 +15,7 @@ limitations under the License. */ import './style.css'; -import { - type CellStyle, - Client, - Graph, - InternalEvent, - RubberBandHandler, -} from '@maxgraph/core'; +import { Client, Graph, InternalEvent, RubberBandHandler } from '@maxgraph/core'; import { registerCustomShapes } from './custom-shapes'; // display the maxGraph version in the footer @@ -52,6 +46,7 @@ const parent = graph.getDefaultParent(); // Adds cells to the model in a single step graph.batchUpdate(() => { + // use the legacy insertVertex method const vertex01 = graph.insertVertex( parent, null, @@ -69,30 +64,30 @@ graph.batchUpdate(() => { 90, 50, 50, - { shape: 'ellipse', fillColor: 'orange' } + { fillColor: 'orange', shape: 'ellipse', verticalLabelPosition: 'bottom' } ); graph.insertEdge(parent, null, 'a regular edge', vertex01, vertex02); - // insert vertices using custom shapes - const vertex11 = graph.insertVertex( + // insert vertices using custom shapes using the new insertVertex method + const vertex11 = graph.insertVertex({ parent, - null, - 'a custom rectangle', - 20, - 200, - 100, - 100, - { shape: 'customRectangle' } - ); - const vertex12 = graph.insertVertex( + value: 'a custom rectangle', + position: [20, 200], + size: [100, 100], + style: { shape: 'customRectangle' }, + }); + // use the new insertVertex method using position and size parameters + const vertex12 = graph.insertVertex({ parent, - null, - 'a custom ellipse', - 150, - 350, - 70, - 70, - { shape: 'customEllipse' } - ); + value: 'a custom ellipse', + x: 150, + y: 350, + width: 70, + height: 70, + style: { + shape: 'customEllipse', + verticalLabelPosition: 'bottom', + }, + }); graph.insertEdge(parent, null, 'another edge', vertex11, vertex12); });