UNPKG

@gravity-ui/graph

Version:

Modern graph editor component

186 lines (185 loc) 7.26 kB
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]; } }