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.
development
Thomas Bouffard 2023-07-07 07:41:49 +02:00 committed by GitHub
parent 4a79be5499
commit cdd8830b5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 362 additions and 126 deletions

View File

@ -74,8 +74,20 @@ const parent = graph.getDefaultParent();
// Adds cells to the model in a single step // Adds cells to the model in a single step
graph.batchUpdate(() => { graph.batchUpdate(() => {
const vertex01 = graph.insertVertex(parent, null, 'a regular rectangle', 10, 10, 100, 100); const vertex01 = graph.insertVertex({
const vertex02 = graph.insertVertex(parent, null, 'a regular ellipse', 350, 90, 50, 50, <CellStyle>{shape: 'ellipse', fillColor: 'orange'}); 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); graph.insertEdge(parent, null, 'a regular edge', vertex01, vertex02);
}); });
``` ```

View File

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

View File

@ -22,6 +22,7 @@ import type InternalMouseEvent from './view/event/InternalMouseEvent';
import type Shape from './view/geometry/Shape'; import type Shape from './view/geometry/Shape';
import type { Graph } from './view/Graph'; import type { Graph } from './view/Graph';
import type ImageBox from './view/image/ImageBox'; import type ImageBox from './view/image/ImageBox';
import Geometry from './view/geometry/Geometry';
export type FilterFunction = (cell: Cell) => boolean; export type FilterFunction = (cell: Cell) => boolean;
@ -880,6 +881,61 @@ export type GradientMap = {
[k: string]: Gradient; [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 { export interface GraphPluginConstructor {
new (graph: Graph): GraphPlugin; new (graph: Graph): GraphPlugin;
pluginId: string; pluginId: string;

View File

@ -18,30 +18,158 @@ import Cell from '../cell/Cell';
import Geometry from '../geometry/Geometry'; import Geometry from '../geometry/Geometry';
import { Graph } from '../Graph'; import { Graph } from '../Graph';
import { mixInto } from '../../util/Utils'; import { mixInto } from '../../util/Utils';
import type { CellStyle } from '../../types'; import type { CellStyle, VertexParameters } from '../../types';
declare module '../Graph' { declare module '../Graph' {
interface Graph { interface Graph {
/**
* Specifies the return value for vertices in {@link isLabelMovable}.
* @default false
*/
vertexLabelsMovable: boolean; vertexLabelsMovable: boolean;
/**
* Specifies if negative coordinates for vertices are allowed.
* @default true
*/
allowNegativeCoordinates: boolean; allowNegativeCoordinates: boolean;
/**
* Returns {@link allowNegativeCoordinates}.
*/
isAllowNegativeCoordinates: () => boolean; isAllowNegativeCoordinates: () => boolean;
/**
* Sets {@link allowNegativeCoordinates}.
*/
setAllowNegativeCoordinates: (value: boolean) => void; setAllowNegativeCoordinates: (value: boolean) => void;
insertVertex: (...args: any[]) => Cell;
createVertex: ( /**
parent: Cell, * Adds a new vertex into the given parent {@link Cell} using value as the user
id: string, * 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, value: any,
x: number, x: number,
y: number, y: number,
width: number, width: number,
height: number, height: number,
style: CellStyle, style?: CellStyle,
relative: boolean, relative?: boolean,
geometryClass: typeof Geometry geometryClass?: typeof Geometry
) => Cell; ): 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[]; getChildVertices: (parent?: Cell | null) => Cell[];
/**
* Returns {@link vertexLabelsMovable}.
*/
isVertexLabelsMovable: () => boolean; isVertexLabelsMovable: () => boolean;
/**
* Sets {@link vertexLabelsMovable}.
*/
setVertexLabelsMovable: (value: boolean) => void; setVertexLabelsMovable: (value: boolean) => void;
} }
} }
@ -53,81 +181,30 @@ type PartialVertex = Pick<
| 'allowNegativeCoordinates' | 'allowNegativeCoordinates'
| 'isAllowNegativeCoordinates' | 'isAllowNegativeCoordinates'
| 'setAllowNegativeCoordinates' | 'setAllowNegativeCoordinates'
| 'insertVertex'
| 'createVertex' | 'createVertex'
| 'getChildVertices' | 'getChildVertices'
| 'isVertexLabelsMovable' | 'isVertexLabelsMovable'
| 'setVertexLabelsMovable' | 'setVertexLabelsMovable'
>; > & {
// handle the methods defined in the Graph interface with a single implementation
insertVertex: (...args: any[]) => Cell;
};
type PartialType = PartialGraph & PartialVertex; type PartialType = PartialGraph & PartialVertex;
// @ts-expect-error The properties of PartialGraph are defined elsewhere. // @ts-expect-error The properties of PartialGraph are defined elsewhere.
const VertexMixin: PartialType = { const VertexMixin: PartialType = {
/**
* Specifies the return value for vertices in {@link isLabelMovable}.
* @default false
*/
vertexLabelsMovable: false, vertexLabelsMovable: false,
/**
* Specifies if negative coordinates for vertices are allowed.
* @default true
*/
allowNegativeCoordinates: true, allowNegativeCoordinates: true,
/**
* Returns {@link allowNegativeCoordinates}.
*/
isAllowNegativeCoordinates() { isAllowNegativeCoordinates() {
return this.allowNegativeCoordinates; return this.allowNegativeCoordinates;
}, },
/**
* Sets {@link allowNegativeCoordinates}.
*/
setAllowNegativeCoordinates(value: boolean) { setAllowNegativeCoordinates(value: boolean) {
this.allowNegativeCoordinates = value; this.allowNegativeCoordinates = value;
}, },
/**
* Adds a new vertex into the given parent <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
* 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 <Cell> 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) { insertVertex(...args) {
let parent; let parent;
let id; let id;
@ -140,9 +217,7 @@ const VertexMixin: PartialType = {
let relative; let relative;
let geometryClass; let geometryClass;
if (args.length === 1) { if (args.length === 1 && typeof args[0] === 'object') {
// If only a single parameter, treat as an object
// This syntax can be more readable
const params = args[0]; const params = args[0];
parent = params.parent; parent = params.parent;
id = params.id; id = params.id;
@ -161,9 +236,6 @@ const VertexMixin: PartialType = {
[parent, id, value, x, y, width, height, style, relative, geometryClass] = args; [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( const vertex = this.createVertex(
parent, parent,
id, id,
@ -180,24 +252,21 @@ const VertexMixin: PartialType = {
return this.addCell(vertex, parent); return this.addCell(vertex, parent);
}, },
/**
* Hook method that creates the new vertex for <insertVertex>.
*/
createVertex( createVertex(
parent, parent: Cell | null,
id, id: string,
value, value: any,
x, x: number,
y, y: number,
width, width: number,
height, height: number,
style, style?: CellStyle,
relative = false, relative = false,
geometryClass = Geometry geometryClass = Geometry
) { ) {
// Creates the geometry for the vertex // Creates the geometry for the vertex
const geometry = new geometryClass(x, y, width, height); const geometry = new geometryClass(x, y, width, height);
geometry.relative = relative != null ? relative : false; geometry.relative = relative;
// Creates the vertex // Creates the vertex
const vertex = new Cell(value, geometry, style); const vertex = new Cell(value, geometry, style);
@ -208,29 +277,18 @@ const VertexMixin: PartialType = {
return vertex; return vertex;
}, },
/**
* Returns the visible child vertices of the given parent.
*
* @param parent {@link mxCell} whose children should be returned.
*/
getChildVertices(parent) { getChildVertices(parent) {
return this.getChildCells(parent, true, false); return this.getChildCells(parent, true, false);
}, },
/***************************************************************************** // ***************************************************************************
* Group: Graph Behaviour // Group: Graph Behaviour
*****************************************************************************/ // ***************************************************************************
/**
* Returns {@link vertexLabelsMovable}.
*/
isVertexLabelsMovable() { isVertexLabelsMovable() {
return this.vertexLabelsMovable; return this.vertexLabelsMovable;
}, },
/**
* Sets {@link vertexLabelsMovable}.
*/
setVertexLabelsMovable(value: boolean) { setVertexLabelsMovable(value: boolean) {
this.vertexLabelsMovable = value; this.vertexLabelsMovable = value;
}, },

View File

@ -15,13 +15,7 @@ limitations under the License.
*/ */
import './style.css'; import './style.css';
import { import { Client, Graph, InternalEvent, RubberBandHandler } from '@maxgraph/core';
type CellStyle,
Client,
Graph,
InternalEvent,
RubberBandHandler,
} from '@maxgraph/core';
import { registerCustomShapes } from './custom-shapes'; import { registerCustomShapes } from './custom-shapes';
// display the maxGraph version in the footer // display the maxGraph version in the footer
@ -52,6 +46,7 @@ const parent = graph.getDefaultParent();
// Adds cells to the model in a single step // Adds cells to the model in a single step
graph.batchUpdate(() => { graph.batchUpdate(() => {
// use the legacy insertVertex method
const vertex01 = graph.insertVertex( const vertex01 = graph.insertVertex(
parent, parent,
null, null,
@ -69,30 +64,30 @@ graph.batchUpdate(() => {
90, 90,
50, 50,
50, 50,
<CellStyle>{ shape: 'ellipse', fillColor: 'orange' } { fillColor: 'orange', shape: 'ellipse', verticalLabelPosition: 'bottom' }
); );
graph.insertEdge(parent, null, 'a regular edge', vertex01, vertex02); graph.insertEdge(parent, null, 'a regular edge', vertex01, vertex02);
// insert vertices using custom shapes // insert vertices using custom shapes using the new insertVertex method
const vertex11 = graph.insertVertex( const vertex11 = graph.insertVertex({
parent, parent,
null, value: 'a custom rectangle',
'a custom rectangle', position: [20, 200],
20, size: [100, 100],
200, style: { shape: 'customRectangle' },
100, });
100, // use the new insertVertex method using position and size parameters
<CellStyle>{ shape: 'customRectangle' } const vertex12 = graph.insertVertex({
);
const vertex12 = graph.insertVertex(
parent, parent,
null, value: 'a custom ellipse',
'a custom ellipse', x: 150,
150, y: 350,
350, width: 70,
70, height: 70,
70, style: {
<CellStyle>{ shape: 'customEllipse' } shape: 'customEllipse',
); verticalLabelPosition: 'bottom',
},
});
graph.insertEdge(parent, null, 'another edge', vertex11, vertex12); graph.insertEdge(parent, null, 'another edge', vertex11, vertex12);
}); });