Skip to content

Commit 6bc0f02

Browse files
authored
feat!: use registry metadata for edge handle visibility (#1040)
The hard-coded reference to EdgeStyle.EntityRelation in EdgeHandler.isHandleVisible() created a direct dependency on the EntityRelation function, preventing tree-shaking when EntityRelation is not used in the application. It also meant only EntityRelation could influence handle visibility — no other edge style (including custom ones) could control whether intermediate bend handles are shown. Replace this with an allowIntermediateHandles metadata property on EdgeStyleRegistry, following the existing pattern of isOrthogonal and handlerKind. Benefits: - Generic: any edge style can now control intermediate handle visibility via metadata - Configurable and extensible: custom edge styles can set allowIntermediateHandles: false - Tree-shakeable: EdgeHandler no longer imports EdgeStyle directly BREAKING CHANGE: - EdgeStyleRegistryInterface has a new allowsIntermediateHandles() method - Custom registrations of EntityRelation (e.g. when using BaseGraph) must now include { allowIntermediateHandles: false } in the metadata to preserve the previous behavior
1 parent beba23d commit 6bc0f02

11 files changed

Lines changed: 180 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,25 @@ For more details on the contents of a release, see [the GitHub release page] (ht
99

1010
_**Note:** Yet to be released breaking changes appear here._
1111

12+
**Breaking Changes**:
13+
- `EdgeHandler.isHandleVisible()` now uses `EdgeStyleRegistry.allowsIntermediateHandles()` instead of checking against the `EdgeStyle.EntityRelation` function reference.
14+
If you register custom edge styles that should hide intermediate bend handles, you must now set `allowIntermediateHandles: false` in the `EdgeStyleMetaData` when calling `EdgeStyleRegistry.add()`.
15+
In particular, if you register `EdgeStyle.EntityRelation` yourself (e.g. when using `BaseGraph`), you must include `{ allowIntermediateHandles: false }` in the metadata to preserve the previous behavior.
16+
- `EdgeStyleRegistryInterface` has a new `allowsIntermediateHandles` method. If you implement this interface directly, you must add this method.
17+
1218
## 0.23.0
1319

1420
Release date: `2026-03-30`
1521

1622
For more details, see the [0.23.0 Changelog](https://github.com/maxGraph/maxGraph/releases/tag/v0.23.0) on the GitHub release page.
1723

24+
This new version improves modularity, fixes important memory leaks, and adds utilities for better configuration management.
25+
1826
**Breaking Changes**:
1927
- The `getTooltip` and `getTooltipForCell` methods have been moved from `AbstractGraph` to the `TooltipHandler` plugin.
2028
If you were overriding these methods in a `AbstractGraph` subclass, you should now extend `TooltipHandler` instead.
2129
- `xmlUtils.getViewXml` moved to `xmlViewUtils.getViewXml`. The impact should be limited as this function was not widely used (only in the Editor class in the maxGraph code).
2230

23-
This new version improves modularity, fixes important memory leaks, and adds utilities for better configuration management.
24-
2531
## 0.22.0
2632

2733
Release date: `2025-12-11`
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
Copyright 2026-present The maxGraph project Contributors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { afterEach, beforeAll, describe, expect, test } from '@jest/globals';
18+
import {
19+
BaseGraph,
20+
Cell,
21+
CellState,
22+
type CellStateStyle,
23+
EdgeHandler,
24+
EdgeStyle,
25+
type EdgeStyleFunction,
26+
EdgeStyleRegistry,
27+
Geometry,
28+
Point,
29+
Rectangle,
30+
RectangleShape,
31+
registerDefaultEdgeStyles,
32+
unregisterAllEdgeStyles,
33+
} from '../../../src';
34+
35+
const createEdgeHandlerForStyle = (
36+
edgeStyle: EdgeStyleFunction | undefined,
37+
pointCount: number,
38+
{ skipDefaultRegistration = false } = {}
39+
): EdgeHandler => {
40+
const graph = new BaseGraph();
41+
if (!skipDefaultRegistration) {
42+
registerDefaultEdgeStyles();
43+
}
44+
45+
const cell = new Cell();
46+
cell.setEdge(true);
47+
cell.setVertex(false);
48+
cell.setGeometry(new Geometry());
49+
50+
const style: CellStateStyle = edgeStyle ? { edgeStyle } : {};
51+
const cellState = new CellState(graph.view, cell, style);
52+
cellState.absolutePoints = Array.from(
53+
{ length: pointCount },
54+
(_, i) => new Point(i * 10, 0)
55+
);
56+
cellState.shape = new RectangleShape(new Rectangle(), 'green', 'blue');
57+
58+
return new EdgeHandler(cellState);
59+
};
60+
61+
const createEdgeHandlerWithoutGeometry = (): EdgeHandler => {
62+
const graph = new BaseGraph();
63+
const cell = new Cell();
64+
cell.setEdge(true);
65+
cell.setVertex(false);
66+
// no geometry set
67+
68+
const cellState = new CellState(graph.view, cell, {
69+
edgeStyle: EdgeStyle.EntityRelation,
70+
});
71+
cellState.absolutePoints = [new Point(0, 0), new Point(10, 0)];
72+
cellState.shape = new RectangleShape(new Rectangle(), 'green', 'blue');
73+
74+
return new EdgeHandler(cellState);
75+
};
76+
77+
describe('isHandleVisible', () => {
78+
beforeAll(() => {
79+
unregisterAllEdgeStyles();
80+
});
81+
afterEach(() => {
82+
unregisterAllEdgeStyles();
83+
});
84+
85+
describe('EntityRelation edge style', () => {
86+
test('first handle is visible', () => {
87+
const handler = createEdgeHandlerForStyle(EdgeStyle.EntityRelation, 5);
88+
expect(handler.isHandleVisible(0)).toBe(true);
89+
});
90+
91+
test('last handle is visible', () => {
92+
const handler = createEdgeHandlerForStyle(EdgeStyle.EntityRelation, 5);
93+
expect(handler.isHandleVisible(4)).toBe(true);
94+
});
95+
96+
test('intermediate handle is not visible', () => {
97+
const handler = createEdgeHandlerForStyle(EdgeStyle.EntityRelation, 5);
98+
expect(handler.isHandleVisible(2)).toBe(false);
99+
});
100+
});
101+
102+
test('other edge style - intermediate handle is visible', () => {
103+
const handler = createEdgeHandlerForStyle(EdgeStyle.OrthConnector, 5);
104+
expect(handler.isHandleVisible(2)).toBe(true);
105+
});
106+
107+
test('no geometry - all handles are visible', () => {
108+
const handler = createEdgeHandlerWithoutGeometry();
109+
expect(handler.isHandleVisible(0)).toBe(true);
110+
expect(handler.isHandleVisible(1)).toBe(true);
111+
});
112+
113+
test('no edge style - intermediate handle is visible', () => {
114+
const handler = createEdgeHandlerForStyle(undefined, 5);
115+
expect(handler.isHandleVisible(2)).toBe(true);
116+
});
117+
118+
test('EntityRelation registered without allowIntermediateHandles metadata - intermediate handle is visible', () => {
119+
EdgeStyleRegistry.add('entityRelationEdgeStyle', EdgeStyle.EntityRelation, {});
120+
121+
const handler = createEdgeHandlerForStyle(EdgeStyle.EntityRelation, 5, {
122+
skipDefaultRegistration: true,
123+
});
124+
expect(handler.isHandleVisible(2)).toBe(true);
125+
});
126+
});

packages/core/__tests__/view/style/EdgeStyleRegistry.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,20 @@ describe('registry', () => {
4040
EdgeStyleRegistry.add('custom', customEdgeStyle, {
4141
isOrthogonal: true,
4242
handlerKind: 'customHandler',
43+
allowIntermediateHandles: false,
4344
});
4445

4546
expect(EdgeStyleRegistry.get('custom')).toBe(customEdgeStyle);
4647
expect(EdgeStyleRegistry.getHandlerKind(customEdgeStyle)).toEqual('customHandler');
4748
expect(EdgeStyleRegistry.isOrthogonal(customEdgeStyle)).toBeTruthy();
49+
expect(EdgeStyleRegistry.allowsIntermediateHandles(customEdgeStyle)).toBeFalsy();
4850
});
4951

5052
test.each([null, undefined])('retrieve with nullish: %s', (value) => {
5153
expect(EdgeStyleRegistry.get(value)).toBeNull();
5254
expect(EdgeStyleRegistry.getHandlerKind(value)).toEqual('default');
5355
expect(EdgeStyleRegistry.isOrthogonal(value)).toBeFalsy();
56+
expect(EdgeStyleRegistry.allowsIntermediateHandles(value)).toBeTruthy();
5457
});
5558

5659
test('verify registration - no meta data', () => {
@@ -59,12 +62,14 @@ describe('registry', () => {
5962
expect(EdgeStyleRegistry.get('custom')).toBe(customEdgeStyle);
6063
expect(EdgeStyleRegistry.getHandlerKind(customEdgeStyle)).toEqual('default');
6164
expect(EdgeStyleRegistry.isOrthogonal(customEdgeStyle)).toBeFalsy();
65+
expect(EdgeStyleRegistry.allowsIntermediateHandles(customEdgeStyle)).toBeTruthy();
6266
});
6367

6468
test('clear', () => {
6569
EdgeStyleRegistry.add('custom', customEdgeStyle, {
6670
isOrthogonal: false,
6771
handlerKind: 'customHandler',
72+
allowIntermediateHandles: false,
6873
});
6974
expect(EdgeStyleRegistry.get('custom')).toBeDefined();
7075

@@ -74,6 +79,7 @@ describe('registry', () => {
7479
// the edge style function is no longer registered, so returns default values
7580
expect(EdgeStyleRegistry.getHandlerKind(customEdgeStyle)).toEqual('default');
7681
expect(EdgeStyleRegistry.isOrthogonal(customEdgeStyle)).toBeFalsy();
82+
expect(EdgeStyleRegistry.allowsIntermediateHandles(customEdgeStyle)).toBeTruthy();
7783
});
7884

7985
describe('getName', () => {

packages/core/src/types.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1569,6 +1569,14 @@ export type EdgeStyleMetaData = {
15691569
* Defines if the edge style is considered as orthogonal or not.
15701570
* @default false */
15711571
isOrthogonal?: boolean;
1572+
/**
1573+
* Defines if intermediate bend handles are visible when this edge style is used.
1574+
*
1575+
* When set to `false`, only the first and last handles are visible. This is useful for edge styles that do not support intermediate control points.
1576+
* @default true
1577+
* @since 0.24.0
1578+
*/
1579+
allowIntermediateHandles?: boolean;
15721580
};
15731581

15741582
/**
@@ -1594,6 +1602,14 @@ export interface EdgeStyleRegistryInterface extends Registry<EdgeStyleFunction>
15941602
* If the `edgeStyle` is not registered or the `handlerKind` was not set during registration, this method returns `'default'`.
15951603
*/
15961604
getHandlerKind(edgeStyle?: EdgeStyleFunction | null): EdgeStyleHandlerKind;
1605+
1606+
/**
1607+
* Retrieves whether the specified `edgeStyle` allows intermediate bend handles.
1608+
*
1609+
* If the `edgeStyle` is not registered or the `allowIntermediateHandles` was not set during registration, this method returns `true`.
1610+
* @since 0.24.0
1611+
*/
1612+
allowsIntermediateHandles(edgeStyle?: EdgeStyleFunction | null): boolean;
15971613
}
15981614

15991615
/**

packages/core/src/view/handler/EdgeHandler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import InternalEvent from '../event/InternalEvent.js';
4343
import ConstraintHandler from './ConstraintHandler.js';
4444
import Rectangle from '../geometry/Rectangle.js';
4545
import Client from '../../Client.js';
46-
import { EdgeStyle } from '../style/builtin-style-elements.js';
46+
import { EdgeStyleRegistry } from '../style/edge/EdgeStyleRegistry.js';
4747
import {
4848
getClientX,
4949
getClientY,
@@ -610,7 +610,7 @@ class EdgeHandler implements MouseListenerSet {
610610
: null;
611611

612612
return (
613-
edgeStyle !== EdgeStyle.EntityRelation ||
613+
EdgeStyleRegistry.allowsIntermediateHandles(edgeStyle) ||
614614
index === 0 ||
615615
index === this.abspoints.length - 1
616616
);

packages/core/src/view/style/edge/EdgeStyleRegistry.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class EdgeStyleRegistryImpl
3232
{
3333
private readonly handlerMapping = new Map<EdgeStyleFunction, EdgeStyleHandlerKind>();
3434
private readonly orthogonalStates = new Map<EdgeStyleFunction, boolean>();
35+
private readonly intermediateHandlesStates = new Map<EdgeStyleFunction, boolean>();
3536

3637
override add(
3738
name: string,
@@ -42,6 +43,8 @@ class EdgeStyleRegistryImpl
4243
metaData?.handlerKind && this.handlerMapping.set(edgeStyle, metaData.handlerKind);
4344
!isNullish(metaData?.isOrthogonal) &&
4445
this.orthogonalStates.set(edgeStyle, metaData.isOrthogonal);
46+
!isNullish(metaData?.allowIntermediateHandles) &&
47+
this.intermediateHandlesStates.set(edgeStyle, metaData.allowIntermediateHandles);
4548
}
4649

4750
isOrthogonal(edgeStyle?: EdgeStyleFunction | null): boolean {
@@ -52,10 +55,15 @@ class EdgeStyleRegistryImpl
5255
return this.handlerMapping.get(edgeStyle!) ?? 'default';
5356
}
5457

58+
allowsIntermediateHandles(edgeStyle?: EdgeStyleFunction | null): boolean {
59+
return this.intermediateHandlesStates.get(edgeStyle!) ?? true;
60+
}
61+
5562
override clear(): void {
5663
super.clear();
5764
this.handlerMapping.clear();
5865
this.orthogonalStates.clear();
66+
this.intermediateHandlesStates.clear();
5967
}
6068
}
6169

packages/core/src/view/style/edge/EntityRelation.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ import { EntityRelationConnectorConfig } from '../config.js';
3737
*
3838
* This EdgeStyle is registered under `entityRelationEdgeStyle` in {@link EdgeStyleRegistry} when using {@link Graph} or calling {@link registerDefaultEdgeStyles}.
3939
*
40-
* **IMPORTANT**: When registering it manually in {@link EdgeStyleRegistry}, the following metadata must be used:
40+
* **IMPORTANT**: When registering it manually in {@link EdgeStyleRegistry}, the following metadata must be used:
41+
* - allowIntermediateHandles: false
4142
* - handlerKind: 'default' or unset
4243
* - isOrthogonal: true
4344
*

packages/core/src/view/style/register.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ export const registerDefaultEdgeStyles = (): void => {
4242
const edgeStylesToRegister: [EdgeStyleValue, EdgeStyleFunction, EdgeStyleMetaData][] =
4343
[
4444
['elbowEdgeStyle', EdgeStyle.ElbowConnector, { handlerKind: 'elbow' }],
45-
['entityRelationEdgeStyle', EdgeStyle.EntityRelation, {}],
45+
[
46+
'entityRelationEdgeStyle',
47+
EdgeStyle.EntityRelation,
48+
{ allowIntermediateHandles: false },
49+
],
4650
['loopEdgeStyle', EdgeStyle.Loop, { handlerKind: 'elbow', isOrthogonal: false }],
4751
['manhattanEdgeStyle', EdgeStyle.ManhattanConnector, { handlerKind: 'segment' }],
4852
['orthogonalEdgeStyle', EdgeStyle.OrthConnector, { handlerKind: 'segment' }],
@@ -53,7 +57,7 @@ export const registerDefaultEdgeStyles = (): void => {
5357
for (const [name, edgeStyle, metadata] of edgeStylesToRegister) {
5458
EdgeStyleRegistry.add(name, edgeStyle, {
5559
...metadata,
56-
// most edge styles registered here are orthogonal, so set to true by default to avoid to duplicate the configuration code
60+
// most edge styles registered here are orthogonal, so set to true by default to avoid duplicating the configuration code
5761
isOrthogonal: metadata.isOrthogonal ?? true,
5862
});
5963
}

packages/ts-example-selected-features/vite.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export default defineConfig(({ mode }) => {
2727
},
2828
},
2929
},
30-
chunkSizeWarningLimit: 368, // @maxgraph/core
30+
chunkSizeWarningLimit: 367, // @maxgraph/core
3131
},
3232
};
3333
});

packages/ts-example-without-defaults/vite.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export default defineConfig(({ mode }) => {
2727
},
2828
},
2929
},
30-
chunkSizeWarningLimit: 307, // @maxgraph/core
30+
chunkSizeWarningLimit: 305, // @maxgraph/core
3131
},
3232
};
3333
});

0 commit comments

Comments
 (0)