Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/system/public_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ List of methods in your disposition:

public unsetSelection(): void;

public addBlock(block: Omit<TBlock, "id"> & { id?: TBlockId }): TBlockId;
public addBlock(block: Omit<TBlock, "id"> & { id?: TBlockId }, selectionOptions?: { selected?: boolean; strategy?: ESelectionStrategy }): TBlockId;

public addConnection(connection: TConnection): TConnectionId

Expand All @@ -62,4 +62,5 @@ const update = useCallback(() => {
updateEntities({
blocks: [{...block, name: 'Updated Name'}],
});
})
})
```
18 changes: 16 additions & 2 deletions src/api/PublicGraphApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,23 @@ export class PublicGraphApi {
blockStore?.updateBlock(block);
}

public addBlock(block: Omit<TBlock, "id"> & { id?: TBlockId }): TBlockId {
public addBlock(
block: Omit<TBlock, "id"> & { id?: TBlockId },
selectionOptions?: {
selected?: boolean;
strategy?: ESelectionStrategy;
}
): TBlockId {
const newBlockId = this.graph.rootStore.blocksList.addBlock(block);
this.graph.rootStore.blocksList.updateBlocksSelection([newBlockId], true);

if (selectionOptions !== undefined) {
this.graph.rootStore.blocksList.updateBlocksSelection(
[newBlockId],
selectionOptions.selected !== undefined ? selectionOptions.selected : true,
selectionOptions.strategy
);
}

return newBlockId;
}

Expand Down
36 changes: 26 additions & 10 deletions src/components/canvas/layers/connectionLayer/ConnectionLayer.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ConnectionLayer

`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.
`ConnectionLayer` manages the creation process of connections between blocks and anchors in your graph. It provides an interactive drag and drop interface for creating new connections, temporary visualization during connection creation, automatic selection handling, and a comprehensive event system for the connection creation lifecycle.

## Basic Usage

Expand Down Expand Up @@ -35,6 +35,14 @@ graph.addLayer(ConnectionLayer, {
style: { color: "blue", dash: [5, 5] }
};
},
isConnectionAllowed: (sourceComponent) => {
// Example: Only allow connections from anchor components
const isSourceAnchor = sourceComponent instanceof AnchorState;
return isSourceAnchor;

// Or validate based on component properties
// return sourceComponent.someProperty === true;
},
// ... other props
})
```
Expand All @@ -48,6 +56,7 @@ type ConnectionLayerProps = LayerProps & {
createIcon?: TIcon;
point?: TIcon;
drawLine?: DrawLineFunction;
isConnectionAllowed?: (sourceComponent: BlockState | AnchorState) => boolean;
};

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

## Methods

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

### connection-create-start

Fired when a user starts creating a connection.
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.

```typescript
graph.on("connection-create-start", (event) => {
console.log('Creating connection from block', event.detail.blockId);

// If you prevent this event, the source component won't be selected
// event.preventDefault();
})
```

### connection-create-hover

Fired when hovering over a potential target while creating a connection.
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.

```typescript
graph.on("connection-create-hover", (event) => {
// Prevent connection to this target
event.preventDefault();
// If you prevent this event, the target component won't be selected
// event.preventDefault();
})
```

### connection-created

Fired when a connection is successfully created.
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.

```typescript
graph.on("connection-created", (event) => {
// The connection is added to connectionsList by default
// You can prevent this:
event.preventDefault();
// If you prevent this event, the connection won't be added to the store
// event.preventDefault();
})
```

### connection-create-drop

Fired when the user drops the connection endpoint, whether or not a connection was created.
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.

```typescript
graph.on("connection-create-drop", (event) => {
console.log('Connection dropped at', event.detail.point);

// This event is useful for cleanup or custom drop handling
})
```

## How Connections Work

- Hold Shift and drag from one block to another to create a block-to-block connection
- Drag from one anchor to another to create an anchor-to-anchor connection
- Drag from one anchor to another to create an anchor-to-anchor connection (must be on different blocks)
- Elements are automatically selected during connection creation
- Optional connection validation through the isConnectionAllowed prop
90 changes: 55 additions & 35 deletions src/components/canvas/layers/connectionLayer/ConnectionLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@
createIcon?: TIcon;
point?: TIcon;
drawLine?: DrawLineFunction;
isConnectionAllowed?: (sourceComponent: BlockState | AnchorState) => boolean;
};

declare module "../../../../graphEvents" {
interface GraphEventsDefinitions {
/**
* Event reporting on connection pull out of a block/block's anchor.
* Preventing this event will prevent the connection from being created.
* 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.
*/
"connection-create-start": (
event: CustomEvent<{
Expand All @@ -49,8 +51,8 @@
) => void;

/**
* Event fires on pulled out connection hover on block or anchor
* Preventing prevent connection creation.
* 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.
*/
"connection-create-hover": (
event: CustomEvent<{
Expand All @@ -62,9 +64,9 @@
) => void;

/**
* Event fires when a connection is successfully created
* By default, this adds the connection to connectionsList
* Preventing this event will prevent the connection from being added
* 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.
*/
"connection-created": (
event: CustomEvent<{
Expand All @@ -76,8 +78,9 @@
) => void;

/**
* Event fires when the user drops the connection endpoint
* This happens regardless of whether a connection was created
* 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.
*/
"connection-create-drop": (
event: CustomEvent<{
Expand All @@ -91,19 +94,23 @@
}
}

/**

Check warning on line 97 in src/components/canvas/layers/connectionLayer/ConnectionLayer.ts

View workflow job for this annotation

GitHub Actions / Verify Files

Missing JSDoc @returns for function
* ConnectionLayer provides functionality for creating and visualizing connections
* between blocks and anchors in a graph.
* ConnectionLayer manages the creation process of connections between blocks and anchors in the graph.
* It handles the temporary visualization during connection creation but does not render existing connections.
*
* Features:
* - Create connections through an intuitive drag and drop interface
* - Customize connection appearance with icons and line styles
* - Automatic selection of connected elements
* - Rich event system for connection lifecycle
* - Interactive connection creation through drag and drop
* - Temporary visualization during connection creation with configurable icons and line styles
* - Automatic selection handling of source and target elements
* - Comprehensive event system for the connection creation lifecycle
* - Optional connection validation through isConnectionAllowed prop
*
* Connection types:
* - Block-to-Block: Hold Shift and drag from one block to another
* - Anchor-to-Anchor: Drag from one anchor to another
* - Block-to-Block: Hold Shift key and drag from one block to another
* - Anchor-to-Anchor: Drag from one anchor to another (must be on different blocks)
*
* The layer renders on a separate canvas with a higher z-index and handles
* all mouse interactions for connection creation.
*/
export class ConnectionLayer extends Layer<
ConnectionLayerProps,
Expand All @@ -117,8 +124,8 @@
};
protected target?: Block | Anchor;
protected sourceComponent?: BlockState | AnchorState;

protected enabled: boolean;
private declare eventAborter: AbortController;

constructor(props: ConnectionLayerProps) {
super({
Expand All @@ -141,9 +148,10 @@

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

this.eventAborter = new AbortController();
this.performRender = this.performRender.bind(this);
this.context.graph.on("camera-change", this.performRender);
this.context.graph.on("mousedown", this.handleMouseDown, { capture: true });
this.context.graph.on("camera-change", this.performRender, { signal: this.eventAborter.signal });
this.context.graph.on("mousedown", this.handleMouseDown, { capture: true, signal: this.eventAborter.signal });
}

public enable = () => {
Expand All @@ -169,6 +177,14 @@
((this.context.graph.rootStore.settings.getConfigFlag("useBlocksAnchors") && target instanceof Anchor) ||
(isShiftKeyEvent(event) && isBlock(target)))
) {
// Get the source component state
const sourceComponent = target.connectedState;

// Check if connection is allowed using the validation function if provided
if (this.props.isConnectionAllowed && !this.props.isConnectionAllowed(sourceComponent)) {
return;
}

nativeEvent.preventDefault();
nativeEvent.stopPropagation();
dragListener(this.getOwnedDocument())
Expand Down Expand Up @@ -249,8 +265,9 @@
}

protected unmount(): void {
this.context.graph.off("camera-change", this.performRender);
this.context.graph.off("mousedown", this.handleMouseDown);
this.eventAborter.abort();

super.unmount();
}

private getBlockId(component: BlockState | AnchorState) {
Expand All @@ -274,6 +291,15 @@
return;
}

this.sourceComponent = sourceComponent.connectedState;

const xy = getXY(this.context.graphCanvas, event);
this.connectionState = {
...this.connectionState,
sx: xy[0],
sy: xy[1],
};

this.context.graph.executеDefaultEventAction(
"connection-create-start",
{
Expand All @@ -284,29 +310,21 @@
anchorId: sourceComponent instanceof Anchor ? sourceComponent.connectedState.id : undefined,
},
() => {
this.sourceComponent = sourceComponent.connectedState;

if (sourceComponent instanceof Block) {
this.context.graph.api.selectBlocks([this.sourceComponent.id], true, ESelectionStrategy.REPLACE);
} else if (sourceComponent instanceof Anchor) {
this.context.graph.api.setAnchorSelection(sourceComponent.props.blockId, sourceComponent.props.id, true);
}

const xy = getXY(this.context.graphCanvas, event);
this.connectionState = {
...this.connectionState,
sx: xy[0],
sy: xy[1],
};
this.performRender();
}
);

this.performRender();
}

private onMoveNewConnection(event: MouseEvent, point: Point) {
const newTargetComponent = this.context.graph.getElementOverPoint(point, [Block, Anchor]);
const xy = getXY(this.context.graphCanvas, event);
this.target = newTargetComponent;

this.connectionState = {
...this.connectionState,
tx: xy[0],
Expand All @@ -320,14 +338,17 @@
return;
}

// Only process if the target has changed or if there was no previous target
if (
this.target?.connectedState !== newTargetComponent.connectedState &&
(!this.target || this.target.connectedState !== newTargetComponent.connectedState) &&
newTargetComponent.connectedState !== this.sourceComponent
) {
this.target?.connectedState?.setSelection(false);

const target = newTargetComponent.connectedState;

this.target = newTargetComponent;

this.context.graph.executеDefaultEventAction(
"connection-create-hover",
{
Expand All @@ -338,7 +359,6 @@
targetBlockId: target instanceof AnchorState ? target.blockId : target.id,
},
() => {
this.target = newTargetComponent;
this.target.connectedState.setSelection(true);
}
);
Expand Down
Loading
Loading