@gravity-ui/graph
Version:
Modern graph editor component
222 lines (221 loc) • 8.41 kB
JavaScript
import { computed, signal } from "@preact/signals-core";
import cloneDeep from "lodash/cloneDeep";
import { Anchor } from "../../components/canvas/anchors";
import { Block } from "../../components/canvas/blocks/Block";
import { ESelectionStrategy } from "../../services/selection/types";
import { createAnchorPortId, createBlockPointPortId } from "./port/utils";
export const IS_CONNECTION_TYPE = "Connection";
export class ConnectionState {
get id() {
return this.$state.value.id;
}
get sourceBlockId() {
return this.$state.value.sourceBlockId;
}
get sourceAnchorId() {
return this.$state.value.sourceAnchorId;
}
get targetBlockId() {
return this.$state.value.targetBlockId;
}
get targetAnchorId() {
return this.$state.value.targetAnchorId;
}
static getConnectionId(connection) {
if (connection.id)
return connection.id;
if (connection.sourceAnchorId && connection.targetAnchorId) {
return [connection.sourceAnchorId, connection.targetAnchorId].join(":");
}
return [connection.sourceBlockId, connection.targetBlockId].join(":");
}
constructor(store, connectionState, connectionSelectionBucket) {
this.store = store;
this.connectionSelectionBucket = connectionSelectionBucket;
this.$rawState = signal(undefined);
/**
* Computed signal that reactively determines if this connection is selected
* by checking if its ID exists in the selection bucket
*/
this.$selected = computed(() => {
return this.connectionSelectionBucket.$selected.value.has(this.$rawState.value.id);
});
/**
* Connection state signal.
* Derives `selected` from the selection bucket, consistent with BlockState pattern.
*/
this.$state = computed(() => ({
...this.$rawState.value,
selected: this.$selected.value,
}));
this.isDestroyed = false;
this.$sourcePortId = computed(() => {
if (this.$state.value.sourcePortId) {
return this.$state.value.sourcePortId;
}
if (this.$state.value.sourceAnchorId) {
return createAnchorPortId(this.$state.value.sourceBlockId, this.$state.value.sourceAnchorId);
}
return createBlockPointPortId(this.$state.value.sourceBlockId, false);
});
this.$targetPortId = computed(() => {
if (this.$state.value.targetPortId) {
return this.$state.value.targetPortId;
}
if (this.$state.value.targetAnchorId) {
return createAnchorPortId(this.$state.value.targetBlockId, this.$state.value.targetAnchorId);
}
return createBlockPointPortId(this.$state.value.targetBlockId, true);
});
this.$sourcePortState = computed(() => {
const portId = this.$sourcePortId.value;
let port = this.store.getPort(portId);
if (!port) {
port = this.store.observePort(portId, this);
}
else if (!port.observers.has(this)) {
port.addObserver(this);
}
return port;
});
this.$targetPortState = computed(() => {
const portId = this.$targetPortId.value;
let port = this.store.getPort(portId);
if (!port) {
port = this.store.observePort(portId, this);
}
else if (!port.observers.has(this)) {
port.addObserver(this);
}
return port;
});
this.$sourcePort = computed(() => {
return this.$sourcePortState.value.$state.value;
});
this.$targetPort = computed(() => {
return this.$targetPortState.value.$state.value;
});
/* @deprecated use $sourcePortState instead */
this.$sourceBlock = computed(() => {
const component = this.$sourcePortState.value.component;
if (!component) {
return undefined;
}
if (component instanceof Block) {
return component.connectedState;
}
if (component instanceof Anchor) {
return component.connectedState.block;
}
return undefined;
});
/* @deprecated use $targetPortState instead */
this.$targetBlock = computed(() => {
const component = this.$targetPortState.value.component;
if (!component) {
return undefined;
}
if (component instanceof Block) {
return component.connectedState;
}
if (component instanceof Anchor) {
return component.connectedState.block;
}
return undefined;
});
/**
* True when both source and target blocks are hidden (e.g. both inside a
* collapsed group). When true the connection should not be rendered.
*/
this.$hidden = computed(() => {
const getBlockHidden = (component) => {
if (component instanceof Block)
return component.connectedState.$hidden.value;
if (component instanceof Anchor)
return component.connectedState.block.$hidden.value;
return false;
};
return (getBlockHidden(this.$sourcePortState.value.component) &&
getBlockHidden(this.$targetPortState.value.component));
});
this.$geometry = computed(() => {
const sourcePort = this.$sourcePortState.value;
const targetPort = this.$targetPortState.value;
if (!sourcePort.lookup && !targetPort.lookup) {
return [sourcePort.$point.value, targetPort.$point.value];
}
return undefined;
});
const id = ConnectionState.getConnectionId(connectionState);
this.$rawState.value = { ...connectionState, id };
}
/**
* Sets the view component for this connection state
* @param viewComponent - The BaseConnection component instance
* @returns {void}
*/
setViewComponent(viewComponent) {
this.viewComponent = viewComponent;
}
/**
* Gets the view component associated with this connection state.
* @returns The BaseConnection view component or undefined if not set.
*/
getViewComponent() {
return this.viewComponent;
}
/**
* Checks if the connection is currently selected.
* @returns True if the connection is selected, false otherwise.
*/
isSelected() {
return this.$selected.value;
}
setSelection(selected, strategy = ESelectionStrategy.REPLACE) {
this.store.setConnectionsSelection([this.id], selected, strategy);
}
/**
* @deprecated Use `toJSON` instead.
* @returns {TConnection} A deep copy of the connection data
*/
asTConnection() {
return cloneDeep({
...this.$rawState.toJSON(),
selected: this.$selected.value,
});
}
/**
* Converts the connection state to a plain JSON object
* @returns {TConnection} A deep copy of the connection data
*/
toJSON() {
return cloneDeep({
...this.$rawState.toJSON(),
selected: this.$selected.value,
});
}
/**
* Updates the connection with new data
* @param connection - Partial connection data to update
* @returns {void}
*/
updateConnection(connection) {
const { styles, ...newProps } = connection;
const newStyles = Object.assign({}, this.$rawState.value.styles, styles);
this.$rawState.value = Object.assign({}, this.$rawState.value, newProps, { styles: newStyles });
}
/**
* Clean up port observers when connection is destroyed
* @returns {void}
*/
destroy() {
// Stop observing source port
if (this.$sourcePortId.value) {
this.store.unobservePort(this.$sourcePortId.value, this);
}
// Stop observing target port
if (this.$targetPortId.value) {
this.store.unobservePort(this.$targetPortId.value, this);
}
}
}