@gravity-ui/graph
Version:
Modern graph editor component
186 lines (185 loc) • 7.26 kB
JavaScript
import { Signal, computed } from "@preact/signals-core";
import { Layer } from "../../../services/Layer";
import { getBlocksRect } from "../../../utils/functions";
import { Group } from "./Group";
export class BlockGroups extends Layer {
static withBlockGrouping({ groupingFn, mapToGroups, }) {
const Base = this;
return class BlockGroupWithGrouping extends Base {
constructor() {
super(...arguments);
this.$groupsBlocksMap = computed(() => {
const blocks = this.props.graph.rootStore.blocksList.$blocks.value;
return groupingFn(blocks);
});
/**
* We use `as any` here because TypeScript has trouble inferring the correct type
* for an anonymous class extending a generic base with protected members.
* The public method signature ensures strict type safety for consumers.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}
afterInit() {
this.onSignal(computed(() => {
const groupedBlocks = this.$groupsBlocksMap.value;
return Object.entries(groupedBlocks).map(([key, blocks]) => mapToGroups(key, {
blocks,
rect: getBlocksRect(blocks.map((block) => block.asTBlock())),
}));
}), (groups) => {
this.setGroups(groups);
});
super.afterInit();
}
};
}
static withPredefinedGroups() {
const Base = this;
return class BlockGroupWithPredefinedGroups extends Base {
constructor() {
super(...arguments);
this.$predefinedGroups = new Signal([]);
this.$groupsBlocksMap = computed(() => {
const groups = this.$predefinedGroups.value;
const blocksMap = {};
const blocksListStore = this.props.graph.rootStore.blocksList;
groups.forEach((group) => {
const blocks = blocksListStore.getBlockStates(group.blocksIds);
blocksMap[group.id] = blocks;
});
return blocksMap;
});
/**
* We use `as any` here because TypeScript has trouble inferring the correct type
* for an anonymous class extending a generic base with protected members.
* The public method signature ensures strict type safety for consumers.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}
defineGroups(groups) {
this.$predefinedGroups.value = groups;
}
afterInit() {
this.onSignal(computed(() => {
const groups = this.$predefinedGroups.value;
const groupsBlocksMap = this.$groupsBlocksMap.value;
return groups.map((group) => {
const blocks = groupsBlocksMap[group.id] || [];
const rect = getBlocksRect(blocks.map((block) => block.asTBlock()));
return {
...group,
rect,
};
});
}), (groups) => {
this.setGroups(groups);
});
super.afterInit();
}
};
}
constructor(props) {
super({
canvas: {
zIndex: 1,
classNames: ["no-user-select"],
transformByCameraPosition: true,
...props.canvas,
},
...props,
});
/**
* Map of groups to blocks
* Used to quickly find the blocks of a group
*/
this.$groupsBlocksMap = new Signal({});
/**
* Source of groups
*/
this.$groupsSource = this.props.graph.rootStore.groupsList.$groups;
/**
* Map of blocks to groups
* Used to quickly find the group of a block
*/
this.$blockGroupsMap = computed(() => {
return Object.entries(this.$groupsBlocksMap.value).reduce((acc, [key, blocks]) => {
blocks.forEach((block) => {
acc.set(block.id, key);
});
return acc;
}, new Map());
});
this.updateBlocks = (groupId, { deltaX, deltaY }) => {
if (this.props.updateBlocksOnDrag) {
const blocks = this.$groupsBlocksMap.value[groupId];
if (blocks?.length) {
blocks.forEach((block) => {
block.updateXY(block.x + deltaX, block.y + deltaY, true);
});
}
}
};
const canvas = this.getCanvas();
this.setContext({
canvas,
ctx: canvas.getContext("2d"),
root: this.props.root,
camera: this.props.camera,
constants: this.props.graph.graphConstants,
colors: this.props.graph.graphColors,
graph: this.props.graph,
ownerDocument: this.props.root,
});
}
afterInit() {
this.onSignal(this.$groupsSource, (groups) => {
this.shouldUpdateChildren = true;
this.shouldRenderChildren = true;
this.setState({ groups });
});
super.afterInit();
}
getParent() {
/*
* Override parent to delegate click events to camera.
* This allows camera movement when mouse button is held down.
*/
return this.props.graph.getGraphLayer().$.camera;
}
setGroups(groups) {
const groupsToUpdate = groups.map((group) => {
const existingGroupState = this.props.graph.rootStore.groupsList.getGroupState(group.id);
if (existingGroupState?.isSizeLocked()) {
// Keep the existing rect when size is locked
return { ...group, rect: existingGroupState.$state.value.rect };
}
return group;
});
this.props.graph.rootStore.groupsList.setGroups(groupsToUpdate);
}
updateGroups(groups) {
this.props.graph.rootStore.groupsList.updateGroups(groups);
}
unmountLayer() {
this.props.graph.rootStore.groupsList.reset();
super.unmountLayer();
}
getGroupComponent(group) {
return group.$state.value.component || this.props.groupComponent || Group;
}
updateChildren() {
return this.state.groups?.map((group) => {
return this.getGroupComponent(group).create({
id: group.id,
onDragUpdate: this.updateBlocks,
draggable: this.props.draggable || false,
}, { key: group.id, ref: group.id });
});
}
/**
* Find a Group component by its ID
*/
getGroupById(groupId) {
return this.$?.[groupId];
}
}