diff --git a/.cursor/rules/event-model-rules.mdc b/.cursor/rules/event-model-rules.mdc index 42e777c4..371b7121 100644 --- a/.cursor/rules/event-model-rules.mdc +++ b/.cursor/rules/event-model-rules.mdc @@ -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) { diff --git a/.cursor/rules/general-coding-rules.mdc b/.cursor/rules/general-coding-rules.mdc index 2331f781..ca095b78 100644 --- a/.cursor/rules/general-coding-rules.mdc +++ b/.cursor/rules/general-coding-rules.mdc @@ -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 @@ -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 diff --git a/.cursor/rules/layer-rules.mdc b/.cursor/rules/layer-rules.mdc index 14a2999a..e79d5db3 100644 --- a/.cursor/rules/layer-rules.mdc +++ b/.cursor/rules/layer-rules.mdc @@ -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. @@ -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(); } ``` diff --git a/docs/components/block-component.md b/docs/components/block-component.md index 0b160665..856674cb 100644 --- a/docs/components/block-component.md +++ b/docs/components/block-component.md @@ -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" ``` @@ -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"); diff --git a/docs/components/canvas-graph-component.md b/docs/components/canvas-graph-component.md index 6f6a56c8..983eea2f 100644 --- a/docs/components/canvas-graph-component.md +++ b/docs/components/canvas-graph-component.md @@ -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(); }); @@ -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 @@ -566,16 +566,16 @@ class AnnotatedConnection extends BaseConnection { 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 }); } @@ -907,7 +907,7 @@ 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 = () => { @@ -915,7 +915,7 @@ class DiagramNode extends GraphComponent { this.context.uiService.showDetails(this.props.id); } - handleMouseOver = () => { + handlePointerOver = () => { // Highlight connected elements this.context.highlightService.highlightConnected(this.props.id); } diff --git a/docs/rendering/layers.md b/docs/rendering/layers.md index 7235e256..8d6de4ed 100644 --- a/docs/rendering/layers.md +++ b/docs/rendering/layers.md @@ -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(); } ``` @@ -361,7 +361,7 @@ export class MyCustomLayer extends Layer { 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 }); @@ -392,8 +392,8 @@ export class MyCustomLayer extends Layer { this.unsubscribeFunctions = []; } - private handleMouseDown = (event: MouseEvent) => { - // Handle mouse down event + private handlePointerDown = (event: PointerEvent) => { + // Handle pointer down event }; private performRender = () => { diff --git a/docs/system/events.md b/docs/system/events.md index 5d64bb55..b1327c10 100644 --- a/docs/system/events.md +++ b/docs/system/events.md @@ -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); @@ -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: @@ -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 = CustomEvent<{ +interface GraphPointerEvent = CustomEvent<{ target?: EventedComponent; sourceEvent: E; pointerPressed?: boolean; @@ -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 }); @@ -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 }); diff --git a/src/components/canvas/GraphComponent/index.tsx b/src/components/canvas/GraphComponent/index.tsx index a8a0c096..ab1eec64 100644 --- a/src/components/canvas/GraphComponent/index.tsx +++ b/src/components/canvas/GraphComponent/index.tsx @@ -35,7 +35,7 @@ export class GraphComponent< onDrop, isDraggable, }: { - onDragStart?: (_event: MouseEvent) => void | boolean; + onDragStart?: (_event: PointerEvent) => void | boolean; onDragUpdate?: ( diff: { prevCoords: [number, number]; @@ -43,19 +43,19 @@ export class GraphComponent< 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) => { if (onDragStart?.(event) === false) { return; } @@ -63,7 +63,7 @@ export class GraphComponent< 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) => { if (!startDragCoords.length) return; const [canvasX, canvasY] = getXY(this.context.canvas, event); @@ -75,7 +75,7 @@ export class GraphComponent< 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); diff --git a/src/components/canvas/anchors/index.ts b/src/components/canvas/anchors/index.ts index 9b1683fb..cb95b756 100644 --- a/src/components/canvas/anchors/index.ts +++ b/src/components/canvas/anchors/index.ts @@ -69,9 +69,9 @@ export class Anchor 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; @@ -105,7 +105,7 @@ export class Anchor extends GraphComponen } } - public handleEvent(event: MouseEvent | KeyboardEvent) { + public handleEvent(event: PointerEvent | KeyboardEvent) { event.preventDefault(); event.stopPropagation(); @@ -126,12 +126,12 @@ export class Anchor 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; diff --git a/src/components/canvas/blocks/Block.ts b/src/components/canvas/blocks/Block.ts index 08e8be1a..856f2cb3 100644 --- a/src/components/canvas/blocks/Block.ts +++ b/src/components/canvas/blocks/Block.ts @@ -52,14 +52,14 @@ declare module "../../../graphEvents" { interface GraphEventsDefinitions { "block-drag-start": ( event: CustomEvent<{ - nativeEvent: MouseEvent; + nativeEvent: PointerEvent; block: TBlock; }> ) => void; "block-drag": ( event: CustomEvent<{ - nativeEvent: MouseEvent; + nativeEvent: PointerEvent; block: TBlock; x: number; y: number; @@ -68,7 +68,7 @@ declare module "../../../graphEvents" { "block-drag-end": ( event: CustomEvent<{ - nativeEvent: MouseEvent; + nativeEvent: PointerEvent; block: TBlock; }> ) => void; @@ -98,7 +98,7 @@ export class Block; - protected lastDragEvent?: MouseEvent; + protected lastDragEvent?: PointerEvent; protected startDragCoords: number[] = []; @@ -253,7 +253,7 @@ export class Block block.getViewComponent()); dragListener(block.context.ownerDocument) - .on(EVENTS.DRAG_START, (_event: MouseEvent) => { + .on(EVENTS.DRAG_START, (_event: PointerEvent) => { block.context.graph.getGraphLayer().captureEvents(this); dispatchEvents(selectedBlocksComponents, createCustomDragEvent(EVENTS.DRAG_START, _event)); }) - .on(EVENTS.DRAG_UPDATE, (_event: MouseEvent) => { + .on(EVENTS.DRAG_UPDATE, (_event: PointerEvent) => { dispatchEvents(selectedBlocksComponents, createCustomDragEvent(EVENTS.DRAG_UPDATE, _event)); }) - .on(EVENTS.DRAG_END, (_event: MouseEvent) => { + .on(EVENTS.DRAG_END, (_event: PointerEvent) => { block.context.graph.getGraphLayer().releaseCapture(); dispatchEvents(selectedBlocksComponents, createCustomDragEvent(EVENTS.DRAG_END, _event)); }); diff --git a/src/components/canvas/connections/BaseConnection.ts b/src/components/canvas/connections/BaseConnection.ts index 0ac9c0ef..a8f9d9e8 100644 --- a/src/components/canvas/connections/BaseConnection.ts +++ b/src/components/canvas/connections/BaseConnection.ts @@ -62,7 +62,7 @@ export class BaseConnection< this.updatePoints(); }); - this.listenEvents(["mouseenter", "mouseleave"]); + this.listenEvents(["pointerenter", "pointerleave"]); } protected override handleEvent(event) { @@ -70,10 +70,10 @@ export class BaseConnection< super.handleEvent(event); switch (event.type) { - case "mouseenter": + case "pointerenter": this.setState({ hovered: true }); break; - case "mouseleave": + case "pointerleave": this.setState({ hovered: false }); break; } diff --git a/src/components/canvas/groups/Group.ts b/src/components/canvas/groups/Group.ts index a8503a0c..2f4ef1d4 100644 --- a/src/components/canvas/groups/Group.ts +++ b/src/components/canvas/groups/Group.ts @@ -100,7 +100,7 @@ export class Group extends GraphComponent { + this.addEventListener("click", (event: PointerEvent) => { event.stopPropagation(); this.groupState.setSelection( true, diff --git a/src/components/canvas/layers/connectionLayer/ConnectionLayer.ts b/src/components/canvas/layers/connectionLayer/ConnectionLayer.ts index 9c0013b1..05880fda 100644 --- a/src/components/canvas/layers/connectionLayer/ConnectionLayer.ts +++ b/src/components/canvas/layers/connectionLayer/ConnectionLayer.ts @@ -1,4 +1,4 @@ -import { GraphMouseEvent, extractNativeGraphMouseEvent } from "../../../../graphEvents"; +import { GraphPointerEvent, extractNativeGraphPointerEvent } from "../../../../graphEvents"; import { Layer, LayerContext, LayerProps } from "../../../../services/Layer"; import { AnchorState } from "../../../../store/anchor/Anchor"; import { BlockState, TBlockId } from "../../../../store/block/Block"; @@ -161,7 +161,7 @@ export class ConnectionLayer extends Layer< */ protected afterInit(): void { // Register event listeners with the graphOn wrapper method for automatic cleanup when unmounted - this.onGraphEvent("mousedown", this.handleMouseDown, { + this.onGraphEvent("pointerdown", this.handlePointerDown, { capture: true, }); @@ -177,9 +177,9 @@ export class ConnectionLayer extends Layer< this.enabled = false; }; - protected handleMouseDown = (nativeEvent: GraphMouseEvent) => { + protected handlePointerDown = (nativeEvent: GraphPointerEvent) => { const target = nativeEvent.detail.target; - const event = extractNativeGraphMouseEvent(nativeEvent); + const event = extractNativeGraphPointerEvent(nativeEvent); if (!event || !target || !this.root?.ownerDocument) { return; } @@ -199,13 +199,13 @@ export class ConnectionLayer extends Layer< nativeEvent.preventDefault(); nativeEvent.stopPropagation(); dragListener(this.root.ownerDocument) - .on(EVENTS.DRAG_START, (dStartEvent: MouseEvent) => { + .on(EVENTS.DRAG_START, (dStartEvent: PointerEvent) => { this.onStartConnection(dStartEvent, this.context.graph.getPointInCameraSpace(dStartEvent)); }) - .on(EVENTS.DRAG_UPDATE, (dUpdateEvent: MouseEvent) => + .on(EVENTS.DRAG_UPDATE, (dUpdateEvent: PointerEvent) => this.onMoveNewConnection(dUpdateEvent, this.context.graph.getPointInCameraSpace(dUpdateEvent)) ) - .on(EVENTS.DRAG_END, (dEndEvent: MouseEvent) => + .on(EVENTS.DRAG_END, (dEndEvent: PointerEvent) => this.onEndNewConnection(this.context.graph.getPointInCameraSpace(dEndEvent)) ); } @@ -289,7 +289,7 @@ export class ConnectionLayer extends Layer< return undefined; } - private onStartConnection(event: MouseEvent, point: Point) { + private onStartConnection(event: PointerEvent, point: Point) { const sourceComponent = this.context.graph.getElementOverPoint(point, [Block, Anchor]); if (!sourceComponent) { @@ -326,7 +326,7 @@ export class ConnectionLayer extends Layer< this.performRender(); } - private onMoveNewConnection(event: MouseEvent, point: Point) { + private onMoveNewConnection(event: PointerEvent, point: Point) { const newTargetComponent = this.context.graph.getElementOverPoint(point, [Block, Anchor]); const xy = getXY(this.context.graphCanvas, event); diff --git a/src/components/canvas/layers/graphLayer/GraphLayer.ts b/src/components/canvas/layers/graphLayer/GraphLayer.ts index 917e7ccf..b9c02f2a 100644 --- a/src/components/canvas/layers/graphLayer/GraphLayer.ts +++ b/src/components/canvas/layers/graphLayer/GraphLayer.ts @@ -1,5 +1,5 @@ import { Graph } from "../../../../graph"; -import { GraphMouseEventNames, isNativeGraphEventName } from "../../../../graphEvents"; +import { GraphPointerEventNames, isNativeGraphEventName } from "../../../../graphEvents"; import { Component } from "../../../../lib/Component"; import { Layer, LayerContext, LayerProps } from "../../../../services/Layer"; import { Camera, TCameraProps } from "../../../../services/camera/Camera"; @@ -7,7 +7,7 @@ import { ICamera } from "../../../../services/camera/CameraService"; import { getEventDelta } from "../../../../utils/functions"; import { EventedComponent } from "../../EventedComponent/EventedComponent"; import { Blocks } from "../../blocks/Blocks"; -import { BlockConnection } from "../../connections/BlockConnection"; +import { BlockConnection } from "../../connections"; import { BlockConnections } from "../../connections/BlockConnections"; import { DrawBelow, DrawOver } from "./helpers"; @@ -26,19 +26,18 @@ export type TGraphLayerContext = LayerContext & { }; const rootBubblingEventTypes = new Set([ - "mousedown", - "touchstart", - "mouseup", - "touchend", + "pointerdown", + "pointerup", "click", "dblclick", "contextmenu", + "pointercancel", ]); -const rootCapturingEventTypes = new Set(["mousedown", "touchstart", "mouseup", "touchend"]); +const rootCapturingEventTypes = new Set(["pointerdown", "pointerup"]); -export type GraphMouseEvent = CustomEvent<{ +export type GraphPointerEvent = CustomEvent<{ target: EventedComponent; - sourceEvent: MouseEvent; + sourceEvent: PointerEvent; pointerPressed: boolean; }>; @@ -55,11 +54,11 @@ export class GraphLayer extends Layer { private pointerStartTarget?: EventedComponent; - private pointerStartEvent?: MouseEvent; + private pointerStartEvent?: PointerEvent; private pointerPressed = false; - private eventByTargetComponent?: EventedComponent | MouseEvent; + private eventByTargetComponent?: EventedComponent | PointerEvent; private capturedTargetComponent?: EventedComponent; @@ -125,7 +124,7 @@ export class GraphLayer extends Layer { rootCapturingEventTypes.forEach((type) => this.onRootEvent(type as keyof HTMLElementEventMap, this, { capture: true }) ); - this.onRootEvent("mousemove", this); + this.onRootEvent("pointermove", this); } /* @@ -143,33 +142,32 @@ export class GraphLayer extends Layer { } public handleEvent(e: Event): void { - if (e.type === "mousemove") { - this.updateTargetComponent(e as MouseEvent); - this.onRootPointerMove(e as MouseEvent); + if (e.type === "pointermove") { + this.updateTargetComponent(e as PointerEvent); + this.onRootPointerMove(e as PointerEvent); return; } if (e.eventPhase === Event.CAPTURING_PHASE && rootCapturingEventTypes.has(e.type)) { - this.tryEmulateClick(e as MouseEvent); + this.tryEmulateClick(e as PointerEvent); return; } if (e.eventPhase === Event.BUBBLING_PHASE && rootBubblingEventTypes.has(e.type)) { switch (e.type) { - case "mousedown": - case "touchstart": { - this.updateTargetComponent(e as MouseEvent, true); - this.handleMouseDownEvent(e as MouseEvent); + case "pointerdown": { + this.updateTargetComponent(e as PointerEvent, true); + this.handlePointerDownEvent(e as PointerEvent); break; } - case "mouseup": - case "touchend": { - this.onRootPointerEnd(e as MouseEvent); + case "pointercancel": + case "pointerup": { + this.onRootPointerEnd(e as PointerEvent); break; } case "click": case "dblclick": { - this.tryEmulateClick(e as MouseEvent); + this.tryEmulateClick(e as PointerEvent); break; } } @@ -177,8 +175,8 @@ export class GraphLayer extends Layer { } private dispatchNativeEvent( - type: GraphMouseEventNames, - event: MouseEvent | GraphMouseEvent, + type: GraphPointerEventNames, + event: PointerEvent | GraphPointerEvent, targetComponent?: EventedComponent ) { const graphEvent = this.props.graph.emit(type, { @@ -193,7 +191,7 @@ export class GraphLayer extends Layer { return true; } - private applyEventToTargetComponent(event: MouseEvent | GraphMouseEvent, target = this.targetComponent) { + private applyEventToTargetComponent(event: PointerEvent | GraphPointerEvent, target = this.targetComponent) { if (isNativeGraphEventName(event.type)) { if (!this.dispatchNativeEvent(event.type, event, target)) { return; @@ -204,7 +202,7 @@ export class GraphLayer extends Layer { target.dispatchEvent(event); } - private updateTargetComponent(event: MouseEvent, force = false) { + private updateTargetComponent(event: PointerEvent, force = false) { // Check is event is too close to previous event // In case when previous event is too close to current event, we don't need to update target component // This is useful to prevent flickering when user is moving mouse fast @@ -224,7 +222,7 @@ export class GraphLayer extends Layer { this.targetComponent = this.context.graph.getElementOverPoint(point) || this.$.camera; } - private onRootPointerMove(event: MouseEvent) { + private onRootPointerMove(event: PointerEvent) { if (this.targetComponent !== this.prevTargetComponent) { if (this.targetComponent?.cursor) { this.root.style.cursor = this.targetComponent?.cursor; @@ -232,7 +230,7 @@ export class GraphLayer extends Layer { this.root.style.removeProperty("cursor"); } this.applyEventToTargetComponent( - new CustomEvent("mouseleave", { + new CustomEvent("pointerleave", { bubbles: false, detail: { target: this.prevTargetComponent, @@ -243,7 +241,7 @@ export class GraphLayer extends Layer { this.prevTargetComponent ); this.applyEventToTargetComponent( - new CustomEvent("mouseout", { + new CustomEvent("pointerout", { bubbles: true, detail: { target: this.prevTargetComponent, @@ -254,7 +252,7 @@ export class GraphLayer extends Layer { this.prevTargetComponent ); this.applyEventToTargetComponent( - new CustomEvent("mouseenter", { + new CustomEvent("pointerenter", { bubbles: false, detail: { target: this.targetComponent, @@ -265,7 +263,7 @@ export class GraphLayer extends Layer { this.targetComponent ); this.applyEventToTargetComponent( - new CustomEvent("mouseover", { + new CustomEvent("pointerover", { bubbles: true, detail: { target: this.targetComponent, @@ -278,11 +276,11 @@ export class GraphLayer extends Layer { } } - private handleMouseDownEvent = (event: MouseEvent) => { + private handlePointerDownEvent = (event: PointerEvent) => { return this.onRootPointerStart(event); }; - private onRootPointerStart(event: MouseEvent) { + private onRootPointerStart(event: PointerEvent) { if (event.button === 2 /* Mouse right button */) { /* * used contextmenu event to prevent native menu @@ -297,7 +295,7 @@ export class GraphLayer extends Layer { this.applyEventToTargetComponent(event); } - private onRootPointerEnd(event: MouseEvent) { + private onRootPointerEnd(event: PointerEvent) { if (event.button === 2 /* Mouse right button */) { /* * used contextmenu event to prevent native menu @@ -312,15 +310,15 @@ export class GraphLayer extends Layer { this.applyEventToTargetComponent(event); } - private tryEmulateClick(event: MouseEvent, target = this.targetComponent) { - if ((event.type === "mousedown" || event.type === "touchstart") && target !== undefined) { + private tryEmulateClick(event: PointerEvent, target = this.targetComponent) { + if (event.type === "pointerdown" && target !== undefined) { this.canEmulateClick = false; this.pointerStartTarget = target; this.pointerStartEvent = event; } if ( - (event.type === "mouseup" || event.type === "touchend") && + event.type === "pointerup" && (this.pointerStartTarget === target || // connections can be very close to each other (this.pointerStartTarget instanceof BlockConnection && target instanceof BlockConnection)) && @@ -331,8 +329,11 @@ export class GraphLayer extends Layer { this.canEmulateClick = true; } - if (this.canEmulateClick && (event.type === "click" || event.type === "dblclick")) { - this.applyEventToTargetComponent(new MouseEvent(event.type, event), this.pointerStartTarget); + if ( + (this.canEmulateClick || event.pointerType === "touch") && + (event.type === "click" || event.type === "dblclick") + ) { + this.applyEventToTargetComponent(new PointerEvent(event.type, event), target); } } diff --git a/src/components/canvas/layers/newBlockLayer/NewBlockLayer.ts b/src/components/canvas/layers/newBlockLayer/NewBlockLayer.ts index 7ff617ac..63e80934 100644 --- a/src/components/canvas/layers/newBlockLayer/NewBlockLayer.ts +++ b/src/components/canvas/layers/newBlockLayer/NewBlockLayer.ts @@ -1,4 +1,4 @@ -import { GraphMouseEvent, extractNativeGraphMouseEvent } from "../../../../graphEvents"; +import { GraphPointerEvent, extractNativeGraphPointerEvent } from "../../../../graphEvents"; import { Layer, LayerContext, LayerProps } from "../../../../services/Layer"; import { BlockState } from "../../../../store/block/Block"; import { getXY, isAltKeyEvent, isBlock } from "../../../../utils/functions"; @@ -90,13 +90,13 @@ export class NewBlockLayer extends Layer< protected afterInit(): void { super.afterInit(); // Register event listeners with the graphOn wrapper method for automatic cleanup when unmounted - this.onGraphEvent("mousedown", this.handleMouseDown, { + this.onGraphEvent("pointerdown", this.handlePointerDown, { capture: true, }); } - protected handleMouseDown = (nativeEvent: GraphMouseEvent) => { - const event = extractNativeGraphMouseEvent(nativeEvent); + protected handlePointerDown = (nativeEvent: GraphPointerEvent) => { + const event = extractNativeGraphPointerEvent(nativeEvent); const target = nativeEvent.detail.target; if (event && isAltKeyEvent(event) && isBlock(target) && this.enabled) { // Check if duplication is allowed for this block @@ -111,9 +111,9 @@ export class NewBlockLayer extends Layer< nativeEvent.preventDefault(); nativeEvent.stopPropagation(); dragListener(this.root.ownerDocument) - .on(EVENTS.DRAG_START, (event: MouseEvent) => this.onStartNewBlock(event, target)) - .on(EVENTS.DRAG_UPDATE, (event: MouseEvent) => this.onMoveNewBlock(event)) - .on(EVENTS.DRAG_END, (event: MouseEvent) => + .on(EVENTS.DRAG_START, (event: PointerEvent) => this.onStartNewBlock(event, target)) + .on(EVENTS.DRAG_UPDATE, (event: PointerEvent) => this.onMoveNewBlock(event)) + .on(EVENTS.DRAG_END, (event: PointerEvent) => this.onEndNewBlock(event, this.context.graph.getPointInCameraSpace(event)) ); } @@ -139,7 +139,7 @@ export class NewBlockLayer extends Layer< }); } - private onStartNewBlock(event: MouseEvent, block: Block) { + private onStartNewBlock(event: PointerEvent, block: Block) { // Check if the clicked block is selected const isBlockSelected = block.connectedState.selected; @@ -216,7 +216,7 @@ export class NewBlockLayer extends Layer< private lastMouseX: number; private lastMouseY: number; - private onMoveNewBlock(event: MouseEvent) { + private onMoveNewBlock(event: PointerEvent) { if (!this.copyBlocks.length) { return; } @@ -252,7 +252,7 @@ export class NewBlockLayer extends Layer< this.performRender(); } - private onEndNewBlock(event: MouseEvent, point: TPoint) { + private onEndNewBlock(event: PointerEvent, point: TPoint) { if (!this.copyBlocks.length) { return; } diff --git a/src/components/canvas/layers/selectionLayer/SelectionLayer.ts b/src/components/canvas/layers/selectionLayer/SelectionLayer.ts index 1a1071db..3a855bcf 100644 --- a/src/components/canvas/layers/selectionLayer/SelectionLayer.ts +++ b/src/components/canvas/layers/selectionLayer/SelectionLayer.ts @@ -1,4 +1,4 @@ -import { GraphMouseEvent, extractNativeGraphMouseEvent } from "../../../../graphEvents"; +import { GraphPointerEvent, extractNativeGraphPointerEvent } from "../../../../graphEvents"; import { Layer, LayerContext, LayerProps } from "../../../../services/Layer"; import { selectBlockList } from "../../../../store/block/selectors"; import { getXY, isBlock, isMetaKeyEvent } from "../../../../utils/functions"; @@ -49,7 +49,7 @@ export class SelectionLayer extends Layer< */ protected afterInit(): void { // Set up event handlers here instead of in constructor - this.onGraphEvent("mousedown", this.handleMouseDown, { + this.onGraphEvent("pointerdown", this.handlePointerDown, { capture: true, }); @@ -88,8 +88,8 @@ export class SelectionLayer extends Layer< }); } - private handleMouseDown = (nativeEvent: GraphMouseEvent) => { - const event = extractNativeGraphMouseEvent(nativeEvent); + private handlePointerDown = (nativeEvent: GraphPointerEvent) => { + const event = extractNativeGraphPointerEvent(nativeEvent); const target = nativeEvent.detail.target; if (target instanceof Anchor || target instanceof Block) { return; @@ -109,20 +109,20 @@ export class SelectionLayer extends Layer< } }; - private updateSelectionRender = (event: MouseEvent) => { + private updateSelectionRender = (event: PointerEvent) => { const [x, y] = getXY(this.context.canvas, event); this.selection.width = x - this.selection.x; this.selection.height = y - this.selection.y; this.performRender(); }; - private startSelectionRender = (event: MouseEvent) => { + private startSelectionRender = (event: PointerEvent) => { const [x, y] = getXY(this.context.canvas, event); this.selection.x = x; this.selection.y = y; }; - private endSelectionRender = (event: MouseEvent) => { + private endSelectionRender = (event: PointerEvent) => { if (this.selection.width === 0 && this.selection.height === 0) { return; } diff --git a/src/graph.ts b/src/graph.ts index 93114032..305b460f 100644 --- a/src/graph.ts +++ b/src/graph.ts @@ -209,7 +209,7 @@ export class Graph { return items as InstanceType[]; } - public getPointInCameraSpace(event: MouseEvent) { + public getPointInCameraSpace(event: PointerEvent) { const xy = getXY(this.graphLayer.getCanvas(), event); const applied = this.cameraService.applyToPoint(xy[0], xy[1]); diff --git a/src/graphEvents.ts b/src/graphEvents.ts index bd6e69e4..e23f7f0a 100644 --- a/src/graphEvents.ts +++ b/src/graphEvents.ts @@ -3,25 +3,27 @@ import { GraphState } from "./graph"; import { TGraphColors, TGraphConstants } from "./graphConfig"; import { TCameraState } from "./services/camera/CameraService"; -export type GraphMouseEvent = CustomEvent<{ +export type GraphPointerEvent = CustomEvent<{ target?: EventedComponent; sourceEvent: E; pointerPressed?: boolean; }>; export type GraphEventParams = T extends CustomEvent ? T["detail"] : never; -export const extractNativeGraphMouseEvent = (event: GraphMouseEvent) => { - return event.detail.sourceEvent instanceof MouseEvent ? event.detail.sourceEvent : null; +export const extractNativeGraphPointerEvent = (event: GraphPointerEvent) => { + return event.detail.sourceEvent instanceof PointerEvent ? event.detail.sourceEvent : null; }; -export type GraphMouseEventNames = "mousedown" | "click" | "dblclick" | "mouseenter" | "mouseleave"; +export type GraphPointerEventNames = "click" | "dblclick" | "pointerdown" | "pointerenter" | "pointerleave"; + +export type GraphEventNames = GraphPointerEventNames; export interface BaseGraphEventDefinition { - mousedown: (event: GraphMouseEvent) => void; - click: (event: GraphMouseEvent) => void; - dblclick: (event: GraphMouseEvent) => void; - mouseenter: (event: GraphMouseEvent) => void; - mouseleave: (event: GraphMouseEvent) => void; + click: (event: GraphPointerEvent) => void; + dblclick: (event: GraphPointerEvent) => void; + pointerdown: (event: GraphPointerEvent) => void; + pointerenter: (event: GraphPointerEvent) => void; + pointerleave: (event: GraphPointerEvent) => void; } export type UnwrapBaseGraphEvents< @@ -42,7 +44,7 @@ export interface GraphEventsDefinitions extends BaseGraphEventDefinition { "colors-changed": (event: CustomEvent<{ colors: TGraphColors }>) => void; "state-change": (event: CustomEvent<{ state: GraphState }>) => void; } -const graphMouseEvents = ["mousedown", "click", "dblclick", "mouseenter", "mousemove", "mouseleave"]; +const graphPointerEvents = ["click", "dblclick", "pointerdown", "pointerenter", "pointermove", "pointerleave"]; export type UnwrapGraphEvents< Key extends keyof GraphEventsDefinitions, @@ -67,6 +69,6 @@ export type SelectionEvent = CustomEvent<{ }; }>; -export function isNativeGraphEventName(eventType: string): eventType is GraphMouseEventNames { - return graphMouseEvents.includes(eventType); +export function isNativeGraphEventName(eventType: string): eventType is GraphEventNames { + return graphPointerEvents.includes(eventType); } diff --git a/src/plugins/devtools/DevToolsLayer.ts b/src/plugins/devtools/DevToolsLayer.ts index 70da5c62..0076817b 100644 --- a/src/plugins/devtools/DevToolsLayer.ts +++ b/src/plugins/devtools/DevToolsLayer.ts @@ -75,8 +75,8 @@ export class DevToolsLayer extends Layer this.performRender()); this.onRootEvent( - "mousemove", - (event: MouseEvent): void => { + "pointermove", + (event: PointerEvent): void => { const canvas = this.context.graphCanvas; if (!canvas) return; const rect = canvas.getBoundingClientRect(); @@ -88,10 +88,10 @@ export class DevToolsLayer extends Layer { + this.onRootEvent("pointerenter", (): void => { this.setState({ isMouseInside: true }); }); - this.onRootEvent("mouseleave", (): void => { + this.onRootEvent("pointerleave", (): void => { this.setState({ isMouseInside: false, mouseX: null, mouseY: null }); }); diff --git a/src/plugins/minimap/layer.ts b/src/plugins/minimap/layer.ts index 581fcec3..436fa9d2 100644 --- a/src/plugins/minimap/layer.ts +++ b/src/plugins/minimap/layer.ts @@ -89,7 +89,7 @@ export class MiniMapLayer extends Layer // Use canvasOn wrapper method for DOM event listeners to ensure proper cleanup if (this.canvas) { - this.onCanvasEvent("mousedown", this.handleMouseDownEvent); + this.onCanvasEvent("pointerdown", this.handlePointerDownEvent); } } this.onSignal(this.props.graph.hitTest.$usableRect, () => { @@ -258,7 +258,7 @@ export class MiniMapLayer extends Layer // Use canvasOn wrapper method for DOM event listeners to ensure proper cleanup if (this.canvas) { - this.onCanvasEvent("mousedown", this.handleMouseDownEvent); + this.onCanvasEvent("pointerdown", this.handlePointerDownEvent); } } @@ -307,7 +307,7 @@ export class MiniMapLayer extends Layer }); } - private onCameraDrag(event: MouseEvent) { + private onCameraDrag(event: PointerEvent) { const cameraState = this.props.camera.getCameraState(); const x = -(this.relativeX + event.offsetX / this.scale) + cameraState.relativeWidth / 2; @@ -319,10 +319,10 @@ export class MiniMapLayer extends Layer this.context.camera.move(dx, dy); } - private handleMouseDownEvent = (rootEvent: MouseEvent) => { + private handlePointerDownEvent = (rootEvent: PointerEvent) => { rootEvent.stopPropagation(); this.onCameraDrag(rootEvent); - dragListener(this.getCanvas(), true).on(EVENTS.DRAG_UPDATE, (event: MouseEvent) => this.onCameraDrag(event)); + dragListener(this.getCanvas(), true).on(EVENTS.DRAG_UPDATE, (event: PointerEvent) => this.onCameraDrag(event)); }; } diff --git a/src/react-components/Anchor.css b/src/react-components/Anchor.css index 956368ad..4c73f08f 100644 --- a/src/react-components/Anchor.css +++ b/src/react-components/Anchor.css @@ -8,6 +8,7 @@ width: var(--graph-block-anchor-width, 16px); height: var(--graph-block-anchor-height, 16px); cursor: pointer; + touch-action: none; will-change: transform; } @@ -29,4 +30,4 @@ /* x,y offset needs to */ top: calc(var(--y) - var(--y-offset)); left: calc(var(--x) - var(--x-offset)); -} \ No newline at end of file +} diff --git a/src/react-components/Block.css b/src/react-components/Block.css index e5fe7a56..483e2a03 100644 --- a/src/react-components/Block.css +++ b/src/react-components/Block.css @@ -4,11 +4,11 @@ top: 0; left: 0; box-sizing: content-box; - + touch-action: none; will-change: transform; contain: layout size; isolation: isolate; - + /* Creates a composite layer */ backface-visibility: hidden; transform-style: preserve-3d; @@ -22,9 +22,9 @@ box-sizing: border-box; background: var(--graph-block-bg); - border: 1px solid var(--graph-block-border) + border: 1px solid var(--graph-block-border); } .graph-block-wrapper.selected { - border-color: var(--graph-block-border-selected) + border-color: var(--graph-block-border-selected); } diff --git a/src/react-components/GraphCanvas.tsx b/src/react-components/GraphCanvas.tsx index 228bd9b5..983ebaaf 100644 --- a/src/react-components/GraphCanvas.tsx +++ b/src/react-components/GraphCanvas.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useLayoutEffect, useRef } from "react"; +import type { CSSProperties } from "react"; import { TGraphColors } from ".."; import { Graph } from "../graph"; @@ -11,6 +12,14 @@ import { useGraphEvent, useGraphEvents } from "./hooks/useGraphEvents"; import { ReactLayer } from "./layer"; import { useFn } from "./utils/hooks/useFn"; +const containerStyles = { + position: "absolute", + overflow: "hidden", + width: "100%", + height: "100%", + "-webkit-tap-highlight-color": "transparent", +} as CSSProperties; + export type GraphProps = Pick, "renderBlock"> & Partial & { className?: string; @@ -62,7 +71,7 @@ export function GraphCanvas({ graph, className, blockListClassName, renderBlock, return (
-
+
{graph && reactLayer && reactLayer.renderPortal(renderBlock)}
diff --git a/src/services/Layer.css b/src/services/Layer.css index 8979a312..031d81cc 100644 --- a/src/services/Layer.css +++ b/src/services/Layer.css @@ -9,11 +9,13 @@ } .no-user-select { + -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; + touch-action: none; } .no-pointer-events { @@ -29,4 +31,4 @@ .hidden { visibility: hidden; -} \ No newline at end of file +} diff --git a/src/services/camera/Camera.ts b/src/services/camera/Camera.ts index a1f0bb3b..9dfa4889 100644 --- a/src/services/camera/Camera.ts +++ b/src/services/camera/Camera.ts @@ -3,7 +3,7 @@ import { TGraphLayerContext } from "../../components/canvas/layers/graphLayer/Gr import { Component } from "../../lib"; import { TComponentProps, TComponentState } from "../../lib/Component"; import { ComponentDescriptor } from "../../lib/CoreComponent"; -import { getXY, isMetaKeyEvent, isTrackpadWheelEvent, isWindows } from "../../utils/functions"; +import { getCoord, getEventDelta, getXY, isMetaKeyEvent, isTrackpadWheelEvent, isWindows } from "../../utils/functions"; import { clamp } from "../../utils/functions/clamp"; import { dragListener } from "../../utils/functions/dragListener"; import { EVENTS } from "../../utils/types/events"; @@ -20,7 +20,7 @@ export class Camera extends EventedComponent { @@ -60,30 +60,35 @@ export class Camera extends EventedComponent { - if (!this.context.graph.rootStore.settings.getConfigFlag("canDragCamera") || !(event instanceof MouseEvent)) { + private handlePointerDownEvent = (event: PointerEvent) => { + if (!this.context.graph.rootStore.settings.getConfigFlag("canDragCamera") || !(event instanceof PointerEvent)) { return; } if (!isMetaKeyEvent(event)) { dragListener(this.ownerDocument) - .on(EVENTS.DRAG_START, (event: MouseEvent) => this.onDragStart(event)) - .on(EVENTS.DRAG_UPDATE, (event: MouseEvent) => this.onDragUpdate(event)) + .on(EVENTS.DRAG_START, (event: PointerEvent) => this.onDragStart(event)) + .on(EVENTS.DRAG_UPDATE, (event: PointerEvent) => this.onDragUpdate(event)) .on(EVENTS.DRAG_END, () => this.onDragEnd()); } }; - private onDragStart(event: MouseEvent) { + private onDragStart(event: PointerEvent) { this.lastDragEvent = event; } - private onDragUpdate(event: MouseEvent) { + private onDragUpdate(event: PointerEvent) { if (!this.lastDragEvent) { return; } - this.camera.move(event.pageX - this.lastDragEvent.pageX, event.pageY - this.lastDragEvent.pageY); + const deltaX = getCoord(event, "x") - getCoord(this.lastDragEvent, "x"); + const deltaY = getCoord(event, "y") - getCoord(this.lastDragEvent, "y"); + + if (getEventDelta(event, this.lastDragEvent) < 3) return; + + this.camera.move(deltaX, deltaY); this.lastDragEvent = event; } diff --git a/src/utils/functions/dragListener.ts b/src/utils/functions/dragListener.ts index b7102f61..9ad0dc7e 100644 --- a/src/utils/functions/dragListener.ts +++ b/src/utils/functions/dragListener.ts @@ -1,56 +1,56 @@ import { Emitter } from "../Emitter"; import { EVENTS } from "../types/events"; -export function dragListener(document: Document | HTMLDivElement | HTMLCanvasElement, stopOnMouseLeave = false) { +export function dragListener(document: Document | HTMLDivElement | HTMLCanvasElement, stopOnPointerLeave = false) { let started = false; let finished = false; const emitter = new Emitter(); - const mousemoveBinded = mousemove.bind(null, emitter); - const mouseupBinded = mouseup.bind(null, emitter); + const pointermoveBinded = pointermove.bind(null, emitter); + const pointerupBinded = pointerup.bind(null, emitter); - if (stopOnMouseLeave) { + if (stopOnPointerLeave) { document.addEventListener( - "mouseleave", + "pointerleave", (event) => { if (started) { - mouseupBinded(event); + pointerupBinded(event); } finished = true; - document.removeEventListener("mousemove", mousemoveBinded); + document.removeEventListener("pointermove", pointermoveBinded); }, { once: true, capture: true } ); } document.addEventListener( - "mousemove", + "pointermove", (event) => { if (finished) { return; } started = true; emitter.emit(EVENTS.DRAG_START, event); - document.addEventListener("mousemove", mousemoveBinded); + document.addEventListener("pointermove", pointermoveBinded); }, { once: true, capture: true } ); document.addEventListener( - "mouseup", + "pointerup", (event) => { if (started) { - mouseupBinded(event); + pointerupBinded(event); } finished = true; - document.removeEventListener("mousemove", mousemoveBinded); + document.removeEventListener("pointermove", pointermoveBinded); }, { once: true, capture: true } ); document.addEventListener( - "mousedown", + "pointerdown", () => { - document.removeEventListener("mousemove", mousemoveBinded); + document.removeEventListener("pointermove", pointermoveBinded); }, { once: true, capture: true } ); @@ -58,11 +58,11 @@ export function dragListener(document: Document | HTMLDivElement | HTMLCanvasEle return emitter; } -function mousemove(emitter: Emitter, event: MouseEvent) { +function pointermove(emitter: Emitter, event: PointerEvent) { emitter.emit(EVENTS.DRAG_UPDATE, event); } -function mouseup(emitter: Emitter, event: MouseEvent) { +function pointerup(emitter: Emitter, event: PointerEvent) { emitter.emit(EVENTS.DRAG_END, event); emitter.destroy(); } diff --git a/src/utils/functions/index.ts b/src/utils/functions/index.ts index c3661762..ee78c756 100644 --- a/src/utils/functions/index.ts +++ b/src/utils/functions/index.ts @@ -11,39 +11,39 @@ export function noop(...args: unknown[]) { // noop } -export function getXY(root: HTMLElement, event: Event | WheelEvent | MouseEvent): [number, number] { - if (!("pageX" in event)) return [-1, -1]; +export function getXY(root: HTMLElement, event: Event | WheelEvent | PointerEvent): [number, number] { const rect = root.getBoundingClientRect(); - return [event.pageX - rect.left - window.scrollX, event.pageY - rect.top - window.scrollY]; + + if ("pageX" in event) { + return [event.pageX - rect.left - window.scrollX, event.pageY - rect.top - window.scrollY]; + } + + return [-1, -1]; } -export function getCoord(event: TouchEvent & MouseEvent, coord: string) { +export function getCoord(event: PointerEvent, coord: string) { const name = `page${coord.toUpperCase()}`; - if (event.touches !== undefined && event.touches.length) { - return event.touches[0][name]; - } else { - return event[name]; - } + return event[name]; } export function getEventDelta(e1, e2) { return Math.abs(getCoord(e1, "x") - getCoord(e2, "x")) + Math.abs(getCoord(e1, "y") - getCoord(e2, "y")); } -export function isMetaKeyEvent(event: MouseEvent | KeyboardEvent): boolean { +export function isMetaKeyEvent(event: MouseEvent | PointerEvent | KeyboardEvent): boolean { return event.metaKey || event.ctrlKey; } -export function isShiftKeyEvent(event: MouseEvent | KeyboardEvent): boolean { +export function isShiftKeyEvent(event: PointerEvent | KeyboardEvent): boolean { return event.shiftKey; } -export function isAltKeyEvent(event: MouseEvent | KeyboardEvent): boolean { +export function isAltKeyEvent(event: PointerEvent | KeyboardEvent): boolean { return event.altKey; } -export function getEventSelectionAction(event: MouseEvent) { +export function getEventSelectionAction(event: PointerEvent) { if (isMetaKeyEvent(event)) return SELECTION_EVENT_TYPES.TOGGLE; return SELECTION_EVENT_TYPES.DELETE; } @@ -55,7 +55,7 @@ export function isBlock(component: unknown): component is Block { export function createCustomDragEvent(eventType: string, e): CustomEvent { return new CustomEvent(eventType, { detail: { - ...EVENTS_DETAIL[eventType](e.pageX, e.pageY), + ...EVENTS_DETAIL[eventType](getCoord(e, "x"), getCoord(e, "y")), sourceEvent: e, }, }); @@ -88,7 +88,7 @@ export function dispatchEvents(comps, e) { export function addEventListeners( instance: EventTarget, - mapEventsToFn?: Record void> + mapEventsToFn?: Record void> ): () => void { if (mapEventsToFn === undefined) return noop;