UNPKG

@gravity-ui/graph

Version:

Modern graph editor component

128 lines (127 loc) 5.02 kB
import { extractNativeGraphMouseEvent } from "../../../../graphEvents"; import { Layer } from "../../../../services/Layer"; import { selectBlockList } from "../../../../store/block/selectors"; import { getXY, isBlock, isMetaKeyEvent } from "../../../../utils/functions"; import { dragListener } from "../../../../utils/functions/dragListener"; import { render } from "../../../../utils/renderers/render"; import { EVENTS } from "../../../../utils/types/events"; import { Anchor } from "../../anchors"; import { Block } from "../../blocks/Block"; function getSelectionRect(sx, sy, ex, ey) { if (sx > ex) [sx, ex] = [ex, sx]; if (sy > ey) [sy, ey] = [ey, sy]; return [sx, sy, ex - sx, ey - sy]; } export class SelectionLayer extends Layer { constructor(props) { super({ canvas: { zIndex: 4, classNames: ["no-pointer-events"], }, ...props, }); this.selection = { x: 0, y: 0, width: 0, height: 0, }; this.handleMouseDown = (nativeEvent) => { const event = extractNativeGraphMouseEvent(nativeEvent); const target = nativeEvent.detail.target; if (target instanceof Anchor || target instanceof Block) { return; } if (!this.root?.ownerDocument) { return; } if (event && isMetaKeyEvent(event)) { nativeEvent.preventDefault(); nativeEvent.stopPropagation(); dragListener(this.root.ownerDocument) .on(EVENTS.DRAG_START, this.startSelectionRender) .on(EVENTS.DRAG_UPDATE, this.updateSelectionRender) .on(EVENTS.DRAG_END, this.endSelectionRender); } }; this.updateSelectionRender = (event) => { const [x, y] = getXY(this.context.canvas, event); this.selection.width = x - this.selection.x; this.selection.height = y - this.selection.y; this.performRender(); }; this.startSelectionRender = (event) => { const [x, y] = getXY(this.context.canvas, event); this.selection.x = x; this.selection.y = y; }; this.endSelectionRender = (event) => { if (this.selection.width === 0 && this.selection.height === 0) { return; } const [x, y] = getXY(this.context.canvas, event); const selectionRect = getSelectionRect(this.selection.x, this.selection.y, x, y); const cameraRect = this.context.graph.cameraService.applyToRect(selectionRect[0], selectionRect[1], selectionRect[2], selectionRect[3]); this.applySelectedArea(cameraRect[0], cameraRect[1], cameraRect[2], cameraRect[3]); this.selection.width = 0; this.selection.height = 0; this.performRender(); }; this.setContext({ canvas: this.getCanvas(), ctx: this.getCanvas().getContext("2d"), }); this.performRender = this.performRender.bind(this); } /** * Called after initialization and when the layer is reattached. * This is where we set up event subscriptions to ensure they work properly * after the layer is unmounted and reattached. */ afterInit() { // Set up event handlers here instead of in constructor this.onGraphEvent("camera-change", this.performRender); this.onGraphEvent("mousedown", this.handleMouseDown, { capture: true, }); // Call parent afterInit to ensure proper initialization super.afterInit(); } render() { this.resetTransform(); if (!this.hasActiveSelection()) { return; } this.drawSelectionArea(); } hasActiveSelection() { return this.selection.width !== 0 || this.selection.height !== 0; } drawSelectionArea() { render(this.context.ctx, (ctx) => { ctx.fillStyle = this.context.colors.selection.background; ctx.strokeStyle = this.context.colors.selection.border; ctx.beginPath(); ctx.roundRect(this.selection.x, this.selection.y, this.selection.width, this.selection.height, Number(window.devicePixelRatio)); ctx.closePath(); ctx.fill(); ctx.stroke(); }); } applySelectedArea(x, y, w, h) { const foundComponents = this.context.graph.hitTest.testHitBox({ minX: x, minY: y, maxX: x + w, maxY: y + h, x, y, }); const blocks = foundComponents.filter((component) => isBlock(component)); const blocksIds = blocks.map((component) => component.state.id); selectBlockList(this.context.graph).updateBlocksSelection(blocksIds, Boolean(blocks.length)); } }