Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cursor/rules/event-model-rules.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ When implementing event subscriptions in this project:

// Register DOM events with the appropriate wrapper methods
if (this.canvas) {
this.onCanvasEvent("mousedown", this.handleMouseDown);
this.onCanvasEvent("pointerdown", this.handlePointerDown);
}

if (this.html) {
Expand Down
4 changes: 2 additions & 2 deletions .cursor/rules/general-coding-rules.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ alwaysApply: true

### Avoid `any` Type
- **Never use the `any` type** in your code. Instead:
- Use specific types whenever possible (e.g., `MouseEvent`, `KeyboardEvent`, `HTMLElement`)
- Use specific types whenever possible (e.g., `PointerEvent`, `KeyboardEvent`, `HTMLElement`)
- Use `void` for function return types when the function doesn't return a value
- Use `unknown` only as a last resort when the type is truly unpredictable
- Implement appropriate interfaces (e.g., `EventListenerObject`) instead of type casting
Expand Down Expand Up @@ -40,7 +40,7 @@ alwaysApply: true

### Event Handlers
- Keep event handlers lightweight
- Debounce or throttle handlers for frequent events (resize, scroll, mousemove)
- Debounce or throttle handlers for frequent events (resize, scroll, pointermove)
- Clean up event listeners when components are unmounted

### Memory Management
Expand Down
8 changes: 4 additions & 4 deletions .cursor/rules/layer-rules.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -141,16 +141,16 @@ this.html.style.transform = `matrix(${camera.scale}, 0, 0, ${camera.scale}, ${ca
- Use `afterInit` for setup that requires the canvas and context to be ready.
- Inside `afterInit`, reliably get the canvas using `this.getCanvas()`.
- Initialize or update the layer's full context using `this.setContext({ ...this.context, /* your additions */ });`. The base `Layer` already initializes `graph`, `camera`, `colors`, `constants`, `layer`. If you create a canvas/HTML element, you need to add `graphCanvas`/`html` and `ctx` to the context after creation (e.g., in `afterInit`).
- Attach event listeners (e.g., `mousemove`, `mouseleave`) to the layer's root element (`this.root`) or specific layer elements (`this.getCanvas()`, `this.getHTML()`) within `afterInit`.
- Attach event listeners (e.g., `pointermove`, `pointerleave`) to the layer's root element (`this.root`) or specific layer elements (`this.getCanvas()`, `this.getHTML()`) within `afterInit`.
- Always clean up listeners and subscriptions in the `unmount` method.
- **Camera Interaction & Coordinates:**
- Subscribe to graph's `'camera-change'` event (`this.props.graph.on(...)`) to get updates. The event detail (`event.detail`) provides the `TCameraState` (containing `width`, `height`, `scale`, `x`, `y`).
- `cameraState.x` and `cameraState.y` represent the *screen coordinates* of the world origin (0,0). Use these (`worldOriginScreenX`, `worldOriginScreenY`) for coordinate calculations.
- To convert **screen coordinates to world coordinates** (e.g., mouse position), use `this.context.camera.applyToPoint(screenX, screenY)`.
- To convert **screen coordinates to world coordinates** (e.g., pointer position), use `this.context.camera.applyToPoint(screenX, screenY)`.
- To convert **world coordinates to screen coordinates** (e.g., placing ticks), use the formula: `screenX = worldX * scale + worldOriginScreenX` and `screenY = worldY * scale + worldOriginScreenY`.
- To determine the **world coordinates visible** in the viewport, calculate the boundaries: `worldViewLeft = (0 - worldOriginScreenX) / scale`, `worldViewRight = (viewWidth - worldOriginScreenX) / scale`, etc. Use these boundaries to optimize rendering loops.
- **Interaction & Behavior:** Layers are suitable for encapsulating specific interaction logic (e.g., drag-and-drop handling, tool activation). These layers might not draw anything but listen to events and modify the graph state.
- **Event Propagation & Camera Interaction:** Since layers are often added directly to the root container (and not nested within the `GraphLayer` which handles core event delegation and contains the `Camera`), mouse events intended for camera interactions (like panning via click/drag) might be intercepted by the layer. To ensure the camera receives these events, you may need to override the layer's `getParent()` method to directly return the camera component: `return this.props.graph.getGraphLayer().$.camera;`. This effectively bypasses the standard hierarchy for event bubbling, delegating the event to the camera. *Note:* This is a workaround; be mindful of potential side effects on other event handling within your layer. See the `BlockGroups` layer (`src/components/canvas/groups/BlockGroups.ts`) for a practical example.
- **Event Propagation & Camera Interaction:** Since layers are often added directly to the root container (and not nested within the `GraphLayer` which handles core event delegation and contains the `Camera`), pointer events intended for camera interactions (like panning via click/drag) might be intercepted by the layer. To ensure the camera receives these events, you may need to override the layer's `getParent()` method to directly return the camera component: `return this.props.graph.getGraphLayer().$.camera;`. This effectively bypasses the standard hierarchy for event bubbling, delegating the event to the camera. *Note:* This is a workaround; be mindful of potential side effects on other event handling within your layer. See the `BlockGroups` layer (`src/components/canvas/groups/BlockGroups.ts`) for a practical example.
- **State Management:** Layers typically access the graph's central state store (`store/`) to get the data they need and to dispatch changes. Use reactive patterns (signals) to trigger updates when relevant data changes.
- **Cleanup:** Implement necessary cleanup in the layer's `destroy` or `unmount` method to release resources, remove listeners, etc.

Expand Down Expand Up @@ -213,7 +213,7 @@ The layer system provides convenient wrapper methods for subscribing to events u
```typescript
protected afterInit() {
this.onGraphEvent("camera-change", this.handleCameraChange);
this.onCanvasEvent("mousedown", this.handleMouseDown);
this.onCanvasEvent("pointerdown", this.handlePointerDown);
super.afterInit();
}
```
Expand Down
8 changes: 4 additions & 4 deletions docs/components/block-component.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,16 +208,16 @@ sequenceDiagram
participant BlockController
participant Store

User->>Block: Mouse Down
User->>Block: Pointer Down
Block->>BlockController: onDragStart()
BlockController->>Block: Emit "block-drag-start"
User->>Block: Mouse Move
User->>Block: Pointer Move
Block->>BlockController: onDragUpdate()
BlockController->>Block: Calculate next position
Block->>Block: applyNextPosition()
Block->>Store: Update position
Block->>Block: Emit "block-drag"
User->>Block: Mouse Up
User->>Block: Pointer Up
Block->>BlockController: onDragEnd()
BlockController->>Block: Emit "block-drag-end"
```
Expand Down Expand Up @@ -267,7 +267,7 @@ class CustomBlock extends Block {
}

// Override behavior methods to customize interactions
protected onDragStart(event: MouseEvent) {
protected onDragStart(event: PointerEvent) {
// Custom drag start handling
console.log("Custom drag start");

Expand Down
28 changes: 14 additions & 14 deletions docs/components/canvas-graph-component.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,23 +111,23 @@ protected willIterate(): void {
└──────┘
```

### 3. Mouse Event Handling
### 3. Pointer Event Handling

GraphComponent adds the ability to listen to mouse events on the element, including drag operations:
GraphComponent adds the ability to listen to pointer events on the element, including drag operations:

```typescript
// Listen for basic mouse events
// Listen for basic pointer events
this.addEventListener('click', (event) => {
console.log('Component clicked at:', event.point);
this.setState({ selected: true });
});

this.addEventListener('mouseenter', () => {
this.addEventListener('pointerenter', () => {
this.setState({ hovered: true });
this.performRender();
});

this.addEventListener('mouseleave', () => {
this.addEventListener('pointerleave', () => {
this.setState({ hovered: false });
this.performRender();
});
Expand Down Expand Up @@ -159,8 +159,8 @@ this.onDrag({

**Supported events:**
- `click`, `dblclick` - Mouse button clicks
- `mousedown`, `mouseup` - Mouse button press and release
- `mouseenter`, `mouseleave` - Mouse pointer entering or leaving the component
- `pointerdown`, `pointerup` - Pointer button press and release
- `pointerenter`, `pointerleave` - Pointer pointer entering or leaving the component
- Specialized `onDrag` system with precise coordinate handling

### 4. Reactive Data with Signal Subscriptions
Expand Down Expand Up @@ -566,16 +566,16 @@ class AnnotatedConnection extends BaseConnection<AnnotatedConnectionProps> {
labelText: 'Connection'
};

// Listen for mouse events
this.addEventListener('mouseover', this.handleMouseOver);
this.addEventListener('mouseout', this.handleMouseOut);
// Listen for pointer events
this.addEventListener('pointerover', this.handlePointerOver);
this.addEventListener('pointerout', this.handlePointerOut);
}

private handleMouseOver = () => {
private handlePointerOver = () => {
this.setState({ hovered: true });
}

private handleMouseOut = () => {
private handlePointerOut = () => {
this.setState({ hovered: false });
}

Expand Down Expand Up @@ -907,15 +907,15 @@ class DiagramNode extends GraphComponent {
constructor(props, parent) {
super(props, parent);
this.addEventListener('click', this.handleClick);
this.addEventListener('mouseover', this.handleMouseOver);
this.addEventListener('pointerover', this.handlePointerOver);
}

handleClick = () => {
// Show detailed information panel
this.context.uiService.showDetails(this.props.id);
}

handleMouseOver = () => {
handlePointerOver = () => {
// Highlight connected elements
this.context.highlightService.highlightConnected(this.props.id);
}
Expand Down
8 changes: 4 additions & 4 deletions docs/rendering/layers.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ The Layer class provides convenient wrapper methods for subscribing to events us
```typescript
protected afterInit() {
this.onGraphEvent("camera-change", this.handleCameraChange);
this.onCanvasEvent("mousedown", this.handleMouseDown);
this.onCanvasEvent("pointerdown", this.handlePointerDown);
super.afterInit();
}
```
Expand All @@ -361,7 +361,7 @@ export class MyCustomLayer extends Layer<MyLayerProps> {
protected afterInit(): void {
// Option 1: Manual subscription using the layer's AbortController
// This will be automatically cleaned up when the layer is unmounted
this.canvas.addEventListener("mousedown", this.handleMouseDown, {
this.canvas.addEventListener("pointerdown", this.handlePointerDown, {
signal: this.eventAbortController.signal
});

Expand Down Expand Up @@ -392,8 +392,8 @@ export class MyCustomLayer extends Layer<MyLayerProps> {
this.unsubscribeFunctions = [];
}

private handleMouseDown = (event: MouseEvent) => {
// Handle mouse down event
private handlePointerDown = (event: PointerEvent) => {
// Handle pointer down event
};

private performRender = () => {
Expand Down
34 changes: 17 additions & 17 deletions docs/system/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { Graph } from "@gravity-ui/graph";
const graph = new Graph(...props);

// Using the cleanup function
const unsubscribe = graph.on("mouseenter", (event) => {
console.log("mouseenter", event.detail);
const unsubscribe = graph.on("pointerenter", (event) => {
console.log("pointerenter", event.detail);
console.log('hovered element', event.detail.target);
console.log('original event', event.detail.sourceEvent);

Expand All @@ -32,12 +32,12 @@ unsubscribe();
// Using AbortController (recommended for multiple event listeners)
const controller = new AbortController();

graph.on("mouseenter", (event) => {
console.log("Mouse entered:", event.detail);
graph.on("pointerenter", (event) => {
console.log("Pointer entered:", event.detail);
}, { signal: controller.signal });

graph.on("mouseleave", (event) => {
console.log("Mouse left:", event.detail);
graph.on("pointerleave", (event) => {
console.log("Pointer left:", event.detail);
}, { signal: controller.signal });

// Later, to remove all event listeners at once:
Expand All @@ -46,23 +46,23 @@ controller.abort();

## Event Types

### Mouse Events
### Pointer Events

The following mouse events are supported:
The following pointer events are supported:

| Event | Description | Default Prevention |
|-------|-------------|-------------------|
| `mousedown` | Mouse button pressed down | Prevents browser default |
| `click` | Mouse button released after press | Prevents browser default |
| `mouseenter` | Mouse pointer enters graph area | - |
| `mouseleave` | Mouse pointer leaves graph area | - |
| `pointerdown` | Pointer pressed down | Prevents browser default |
| `click` | Pointer released after press | Prevents browser default |
| `pointerenter` | Pointer enters graph area | - |
| `pointerleave` | Pointer leaves graph area | - |

These events use the `GraphMouseEvent` type:
These events use the `GraphPointerEvent` type:

```typescript
import { EventedComponent } from "@gravity-ui/graph";

interface GraphMouseEvent<E extends Event = Event> = CustomEvent<{
interface GraphPointerEvent<E extends Event = Event> = CustomEvent<{
target?: EventedComponent;
sourceEvent: E;
pointerPressed?: boolean;
Expand Down Expand Up @@ -114,7 +114,7 @@ const controller = new AbortController();
// Register multiple event listeners with the same controller
graph.on("camera-change", handleCameraChange, { signal: controller.signal });
graph.on("blocks-selection-change", handleSelectionChange, { signal: controller.signal });
graph.on("mousedown", handleMouseDown, { signal: controller.signal });
graph.on("pointerdown", handlePointerDown, { signal: controller.signal });

// DOM event listeners can also use the same controller
document.addEventListener("keydown", handleKeyDown, { signal: controller.signal });
Expand Down Expand Up @@ -150,10 +150,10 @@ export class MyLayer extends Layer {
// Use the onGraphEvent wrapper method that automatically includes the AbortController signal
this.onGraphEvent("camera-change", this.handleCameraChange);
this.onGraphEvent("blocks-selection-change", this.handleSelectionChange);
this.onGraphEvent("mousedown", this.handleMouseDown);
this.onGraphEvent("pointerdown", this.handlePointerDown);

// DOM event listeners can also use the AbortController signal
this.getCanvas()?.addEventListener("mousedown", this.handleMouseDown, {
this.getCanvas()?.addEventListener("pointerdown", this.handlePointerDown, {
signal: this.eventAbortController.signal
});

Expand Down
16 changes: 8 additions & 8 deletions src/components/canvas/GraphComponent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,35 +35,35 @@
onDrop,
isDraggable,
}: {
onDragStart?: (_event: MouseEvent) => void | boolean;
onDragStart?: (_event: PointerEvent) => void | boolean;
onDragUpdate?: (
diff: {
prevCoords: [number, number];
currentCoords: [number, number];
diffX: number;
diffY: number;
},
_event: MouseEvent
_event: PointerEvent
) => void;
onDrop?: (_event: MouseEvent) => void;
isDraggable?: (event: MouseEvent) => boolean;
onDrop?: (_event: PointerEvent) => void;
isDraggable?: (event: PointerEvent) => boolean;
}) {
let startDragCoords: [number, number];
return this.addEventListener("mousedown", (event: MouseEvent) => {
return this.addEventListener("pointerdown", (event: PointerEvent) => {
if (!isDraggable?.(event)) {
return;
}
event.stopPropagation();
dragListener(this.context.ownerDocument)
.on(EVENTS.DRAG_START, (event: MouseEvent) => {
.on(EVENTS.DRAG_START, (event: PointerEvent) => {

Check warning on line 58 in src/components/canvas/GraphComponent/index.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

'event' is already declared in the upper scope on line 52 column 50
if (onDragStart?.(event) === false) {
return;
}
this.context.graph.getGraphLayer().captureEvents(this);
const xy = getXY(this.context.canvas, event);
startDragCoords = this.context.camera.applyToPoint(xy[0], xy[1]);
})
.on(EVENTS.DRAG_UPDATE, (event: MouseEvent) => {
.on(EVENTS.DRAG_UPDATE, (event: PointerEvent) => {

Check warning on line 66 in src/components/canvas/GraphComponent/index.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

'event' is already declared in the upper scope on line 52 column 50
if (!startDragCoords.length) return;

const [canvasX, canvasY] = getXY(this.context.canvas, event);
Expand All @@ -75,7 +75,7 @@
onDragUpdate?.({ prevCoords: startDragCoords, currentCoords, diffX, diffY }, event);
startDragCoords = currentCoords;
})
.on(EVENTS.DRAG_END, (_event: MouseEvent) => {
.on(EVENTS.DRAG_END, (_event: PointerEvent) => {
this.context.graph.getGraphLayer().releaseCapture();
startDragCoords = undefined;
onDrop?.(_event);
Expand Down
12 changes: 6 additions & 6 deletions src/components/canvas/anchors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ export class Anchor<T extends TAnchorProps = TAnchorProps> extends GraphComponen
});

this.addEventListener("click", this);
this.addEventListener("mouseenter", this);
this.addEventListener("mousedown", this);
this.addEventListener("mouseleave", this);
this.addEventListener("pointerenter", this);
this.addEventListener("pointerdown", this);
this.addEventListener("pointerup", this);

this.computeRenderSize(this.props.size, this.state.raised);
this.shift = this.props.size / 2 + props.lineWidth;
Expand Down Expand Up @@ -105,7 +105,7 @@ export class Anchor<T extends TAnchorProps = TAnchorProps> extends GraphComponen
}
}

public handleEvent(event: MouseEvent | KeyboardEvent) {
public handleEvent(event: PointerEvent | KeyboardEvent) {
event.preventDefault();
event.stopPropagation();

Expand All @@ -126,12 +126,12 @@ export class Anchor<T extends TAnchorProps = TAnchorProps> extends GraphComponen
this.toggleSelected();
break;
}
case "mouseenter": {
case "pointerenter": {
this.setState({ raised: true });
this.computeRenderSize(this.props.size, true);
break;
}
case "mouseleave": {
case "pointerup": {
this.setState({ raised: false });
this.computeRenderSize(this.props.size, false);
break;
Expand Down
Loading
Loading