@gravity-ui/graph
Version:
Modern graph editor component
128 lines (127 loc) • 5.02 kB
JavaScript
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));
}
}