Skip to content

Commit 133ea90

Browse files
author
Antamansid
committed
feat(NewBlockLayer, ConnectionLayer): added validation and duplication of blocks by a group
1 parent 463ba0d commit 133ea90

File tree

8 files changed

+410
-118
lines changed

8 files changed

+410
-118
lines changed

docs/public_api.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ List of methods in your disposition:
2525

2626
public getUsableRect(): TGeometry;
2727

28-
public addBlock(geometry: TGeometry, name: string: void): TBlockId;
28+
public addBlock(block: Omit<TBlock, "id"> & { id?: TBlockId }, selectionOptions?: { selected?: boolean; strategy?: ESelectionStrategy }): TBlockId;
2929

3030
public addConnection(connection: TConnection): TConnectionId
3131

@@ -51,4 +51,4 @@ const update = useCallback(() => {
5151
blocks: [{...block, name: 'Updated Name'}],
5252
});
5353
})
54-
```
54+
```

src/api/PublicGraphApi.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,19 @@ export class PublicGraphApi {
117117
blockStore?.updateBlock(block);
118118
}
119119

120-
public addBlock(block: Omit<TBlock, "id"> & { id?: TBlockId }): TBlockId {
120+
public addBlock(
121+
block: Omit<TBlock, "id"> & { id?: TBlockId },
122+
selectionOptions?: {
123+
selected?: boolean;
124+
strategy?: ESelectionStrategy;
125+
}
126+
): TBlockId {
121127
const newBlockId = this.graph.rootStore.blocksList.addBlock(block);
122-
this.graph.rootStore.blocksList.updateBlocksSelection([newBlockId], true);
128+
this.graph.rootStore.blocksList.updateBlocksSelection(
129+
[newBlockId],
130+
selectionOptions?.selected !== undefined ? selectionOptions.selected : true,
131+
selectionOptions?.strategy
132+
);
123133
return newBlockId;
124134
}
125135

src/components/canvas/layers/connectionLayer/ConnectionLayer.md

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ConnectionLayer
22

3-
`ConnectionLayer` lets you create connections between blocks and anchors in your graph. It enables creating new connections through an intuitive drag and drop interface, handles the drawing of connection lines, and manages all related events.
3+
`ConnectionLayer` manages the creation and visualization of connections between blocks and anchors in your graph. It provides an interactive drag and drop interface, customizable connection appearance, automatic selection handling, and a comprehensive event system for the entire connection lifecycle.
44

55
## Basic Usage
66

@@ -35,6 +35,14 @@ graph.addLayer(ConnectionLayer, {
3535
style: { color: "blue", dash: [5, 5] }
3636
};
3737
},
38+
isConnectionAllowed: (sourceComponent) => {
39+
// Example: Only allow connections from anchor components
40+
const isSourceAnchor = sourceComponent instanceof AnchorState;
41+
return isSourceAnchor;
42+
43+
// Or validate based on component properties
44+
// return sourceComponent.someProperty === true;
45+
},
3846
// ... other props
3947
})
4048
```
@@ -48,6 +56,7 @@ type ConnectionLayerProps = LayerProps & {
4856
createIcon?: TIcon;
4957
point?: TIcon;
5058
drawLine?: DrawLineFunction;
59+
isConnectionAllowed?: (sourceComponent: BlockState | AnchorState) => boolean;
5160
};
5261

5362
type TIcon = {
@@ -64,6 +73,7 @@ type TIcon = {
6473
- **createIcon**: The icon shown when creating a connection
6574
- **point**: The icon shown at the end of the connection
6675
- **drawLine**: Function that defines how to draw the connection line
76+
- **isConnectionAllowed**: Function that validates if a connection can be created from a source component
6777

6878
## Methods
6979

@@ -76,49 +86,55 @@ The layer provides these events:
7686

7787
### connection-create-start
7888

79-
Fired when a user starts creating a connection.
89+
Fired when a user initiates a connection from a block or anchor. This happens when dragging starts from a block (with Shift key) or an anchor. Preventing this event will prevent the selection of the source component.
8090

8191
```typescript
8292
graph.on("connection-create-start", (event) => {
8393
console.log('Creating connection from block', event.detail.blockId);
94+
95+
// If you prevent this event, the source component won't be selected
96+
// event.preventDefault();
8497
})
8598
```
8699

87100
### connection-create-hover
88101

89-
Fired when hovering over a potential target while creating a connection.
102+
Fired when the dragged connection endpoint hovers over a potential target block or anchor. Preventing this event will prevent the selection of the target component.
90103

91104
```typescript
92105
graph.on("connection-create-hover", (event) => {
93-
// Prevent connection to this target
94-
event.preventDefault();
106+
// If you prevent this event, the target component won't be selected
107+
// event.preventDefault();
95108
})
96109
```
97110

98111
### connection-created
99112

100-
Fired when a connection is successfully created.
113+
Fired when a connection is successfully created between two elements. By default, this adds the connection to the connectionsList in the store. Preventing this event will prevent the connection from being added to the store.
101114

102115
```typescript
103116
graph.on("connection-created", (event) => {
104117
// The connection is added to connectionsList by default
105-
// You can prevent this:
106-
event.preventDefault();
118+
// If you prevent this event, the connection won't be added to the store
119+
// event.preventDefault();
107120
})
108121
```
109122

110123
### connection-create-drop
111124

112-
Fired when the user drops the connection endpoint, whether or not a connection was created.
125+
Fired when the user releases the mouse button to complete the connection process. This event fires regardless of whether a valid connection was established. Can be used for cleanup or to handle custom connection drop behavior.
113126

114127
```typescript
115128
graph.on("connection-create-drop", (event) => {
116129
console.log('Connection dropped at', event.detail.point);
130+
131+
// This event is useful for cleanup or custom drop handling
117132
})
118133
```
119134

120135
## How Connections Work
121136

122137
- Hold Shift and drag from one block to another to create a block-to-block connection
123-
- Drag from one anchor to another to create an anchor-to-anchor connection
138+
- Drag from one anchor to another to create an anchor-to-anchor connection (must be on different blocks)
124139
- Elements are automatically selected during connection creation
140+
- Optional connection validation through the isConnectionAllowed prop

src/components/canvas/layers/connectionLayer/ConnectionLayer.ts

Lines changed: 55 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,15 @@ type ConnectionLayerProps = LayerProps & {
3333
createIcon?: TIcon;
3434
point?: TIcon;
3535
drawLine?: DrawLineFunction;
36+
isConnectionAllowed?: (sourceComponent: BlockState | AnchorState) => boolean;
3637
};
3738

3839
declare module "../../../../graphEvents" {
3940
interface GraphEventsDefinitions {
4041
/**
41-
* Event reporting on connection pull out of a block/block's anchor.
42-
* Preventing this event will prevent the connection from being created.
42+
* Fired when a user initiates a connection from a block or anchor.
43+
* This happens when dragging starts from a block (with Shift key) or an anchor.
44+
* Preventing this event will prevent the selection of the source component.
4345
*/
4446
"connection-create-start": (
4547
event: CustomEvent<{
@@ -49,8 +51,8 @@ declare module "../../../../graphEvents" {
4951
) => void;
5052

5153
/**
52-
* Event fires on pulled out connection hover on block or anchor
53-
* Preventing prevent connection creation.
54+
* Fired when the dragged connection endpoint hovers over a potential target block or anchor.
55+
* Preventing this event will prevent the selection of the target component.
5456
*/
5557
"connection-create-hover": (
5658
event: CustomEvent<{
@@ -62,9 +64,9 @@ declare module "../../../../graphEvents" {
6264
) => void;
6365

6466
/**
65-
* Event fires when a connection is successfully created
66-
* By default, this adds the connection to connectionsList
67-
* Preventing this event will prevent the connection from being added
67+
* Fired when a connection is successfully created between two elements.
68+
* By default, this adds the connection to the connectionsList in the store.
69+
* Preventing this event will prevent the connection from being added to the store.
6870
*/
6971
"connection-created": (
7072
event: CustomEvent<{
@@ -76,8 +78,9 @@ declare module "../../../../graphEvents" {
7678
) => void;
7779

7880
/**
79-
* Event fires when the user drops the connection endpoint
80-
* This happens regardless of whether a connection was created
81+
* Fired when the user releases the mouse button to complete the connection process.
82+
* This event fires regardless of whether a valid connection was established.
83+
* Can be used for cleanup or to handle custom connection drop behavior.
8184
*/
8285
"connection-create-drop": (
8386
event: CustomEvent<{
@@ -92,18 +95,22 @@ declare module "../../../../graphEvents" {
9295
}
9396

9497
/**
95-
* ConnectionLayer provides functionality for creating and visualizing connections
96-
* between blocks and anchors in a graph.
98+
* ConnectionLayer manages the creation and visualization of connections
99+
* between blocks and anchors in the graph.
97100
*
98101
* Features:
99-
* - Create connections through an intuitive drag and drop interface
100-
* - Customize connection appearance with icons and line styles
101-
* - Automatic selection of connected elements
102-
* - Rich event system for connection lifecycle
102+
* - Interactive connection creation through drag and drop
103+
* - Customizable connection appearance with configurable icons and line styles
104+
* - Automatic selection handling of source and target elements
105+
* - Comprehensive event system for the entire connection lifecycle
106+
* - Optional connection validation through isConnectionAllowed prop
103107
*
104108
* Connection types:
105-
* - Block-to-Block: Hold Shift and drag from one block to another
106-
* - Anchor-to-Anchor: Drag from one anchor to another
109+
* - Block-to-Block: Hold Shift key and drag from one block to another
110+
* - Anchor-to-Anchor: Drag from one anchor to another (must be on different blocks)
111+
*
112+
* The layer renders on a separate canvas with a higher z-index and handles
113+
* all mouse interactions for connection creation.
107114
*/
108115
export class ConnectionLayer extends Layer<
109116
ConnectionLayerProps,
@@ -117,8 +124,8 @@ export class ConnectionLayer extends Layer<
117124
};
118125
protected target?: Block | Anchor;
119126
protected sourceComponent?: BlockState | AnchorState;
120-
121127
protected enabled: boolean;
128+
private declare eventAborter: AbortController;
122129

123130
constructor(props: ConnectionLayerProps) {
124131
super({
@@ -141,9 +148,10 @@ export class ConnectionLayer extends Layer<
141148

142149
this.enabled = Boolean(this.props.graph.rootStore.settings.getConfigFlag("canCreateNewConnections"));
143150

151+
this.eventAborter = new AbortController();
144152
this.performRender = this.performRender.bind(this);
145-
this.context.graph.on("camera-change", this.performRender);
146-
this.context.graph.on("mousedown", this.handleMouseDown, { capture: true });
153+
this.context.graph.on("camera-change", this.performRender, { signal: this.eventAborter.signal });
154+
this.context.graph.on("mousedown", this.handleMouseDown, { capture: true, signal: this.eventAborter.signal });
147155
}
148156

149157
public enable = () => {
@@ -169,6 +177,14 @@ export class ConnectionLayer extends Layer<
169177
((this.context.graph.rootStore.settings.getConfigFlag("useBlocksAnchors") && target instanceof Anchor) ||
170178
(isShiftKeyEvent(event) && isBlock(target)))
171179
) {
180+
// Get the source component state
181+
const sourceComponent = target.connectedState;
182+
183+
// Check if connection is allowed using the validation function if provided
184+
if (this.props.isConnectionAllowed && !this.props.isConnectionAllowed(sourceComponent)) {
185+
return;
186+
}
187+
172188
nativeEvent.preventDefault();
173189
nativeEvent.stopPropagation();
174190
dragListener(this.getOwnedDocument())
@@ -251,8 +267,9 @@ export class ConnectionLayer extends Layer<
251267
}
252268

253269
protected unmount(): void {
254-
this.context.graph.off("camera-change", this.performRender);
255-
this.context.graph.off("mousedown", this.handleMouseDown);
270+
this.eventAborter.abort();
271+
272+
super.unmount();
256273
}
257274

258275
private getBlockId(component: BlockState | AnchorState) {
@@ -276,6 +293,15 @@ export class ConnectionLayer extends Layer<
276293
return;
277294
}
278295

296+
this.sourceComponent = sourceComponent.connectedState;
297+
298+
const xy = getXY(this.context.graphCanvas, event);
299+
this.connectionState = {
300+
...this.connectionState,
301+
sx: xy[0],
302+
sy: xy[1],
303+
};
304+
279305
this.context.graph.executеDefaultEventAction(
280306
"connection-create-start",
281307
{
@@ -286,29 +312,21 @@ export class ConnectionLayer extends Layer<
286312
anchorId: sourceComponent instanceof Anchor ? sourceComponent.connectedState.id : undefined,
287313
},
288314
() => {
289-
this.sourceComponent = sourceComponent.connectedState;
290-
291315
if (sourceComponent instanceof Block) {
292316
this.context.graph.api.selectBlocks([this.sourceComponent.id], true, ESelectionStrategy.REPLACE);
293317
} else if (sourceComponent instanceof Anchor) {
294318
this.context.graph.api.setAnchorSelection(sourceComponent.props.blockId, sourceComponent.props.id, true);
295319
}
296-
297-
const xy = getXY(this.context.graphCanvas, event);
298-
this.connectionState = {
299-
...this.connectionState,
300-
sx: xy[0],
301-
sy: xy[1],
302-
};
303-
this.performRender();
304320
}
305321
);
322+
323+
this.performRender();
306324
}
307325

308326
private onMoveNewConnection(event: MouseEvent, point: Point) {
309327
const newTargetComponent = this.context.graph.getElementOverPoint(point, [Block, Anchor]);
310328
const xy = getXY(this.context.graphCanvas, event);
311-
this.target = newTargetComponent;
329+
312330
this.connectionState = {
313331
...this.connectionState,
314332
tx: xy[0],
@@ -322,14 +340,17 @@ export class ConnectionLayer extends Layer<
322340
return;
323341
}
324342

343+
// Only process if the target has changed or if there was no previous target
325344
if (
326-
this.target?.connectedState !== newTargetComponent.connectedState &&
345+
(!this.target || this.target.connectedState !== newTargetComponent.connectedState) &&
327346
newTargetComponent.connectedState !== this.sourceComponent
328347
) {
329348
this.target?.connectedState?.setSelection(false);
330349

331350
const target = newTargetComponent.connectedState;
332351

352+
this.target = newTargetComponent;
353+
333354
this.context.graph.executеDefaultEventAction(
334355
"connection-create-hover",
335356
{
@@ -340,7 +361,6 @@ export class ConnectionLayer extends Layer<
340361
targetBlockId: target instanceof AnchorState ? target.blockId : target.id,
341362
},
342363
() => {
343-
this.target = newTargetComponent;
344364
this.target.connectedState.setSelection(true);
345365
}
346366
);

0 commit comments

Comments
 (0)