User Interaction & Events
Purpose and Scope
This page introduces the event system architecture in relation-graph, explaining how user interactions are captured, processed, and propagated to application code. The system provides a unified event model that works consistently across all supported frameworks (Vue, React, Svelte).
For detailed information about specific event types and the internal event dispatcher architecture, see Event System Architecture. For concrete examples of handling user actions like clicks, drags, and gestures, see User Actions & Interactions.
Event System Overview
The relation-graph event system follows a layered event flow pattern where user interactions are captured at the DOM level, processed through the core graph logic, and finally emitted to application-level event handlers. This architecture separates concerns between:
- User Action Capture - DOM event listeners attached to graph elements (nodes, lines, canvas)
- Core Processing - Business logic in
RelationGraphWith7Eventand related classes - Event Emission - Standardized event broadcasting through
emitEvent() - Application Handlers - User-defined callbacks in the
RGListenersinterface
All events are type-safe, with TypeScript definitions providing autocomplete and validation for event names, parameters, and return values.
Sources: packages/relation-graph-models/models/RelationGraphWith7Event.ts:1-30, packages/relation-graph-models/models/RelationGraphBase.ts:106-167
Event Flow Architecture
Event Propagation from DOM to Application Handlers
graph TB
subgraph DOM["DOM Events"]
MouseEvent["MouseEvent/TouchEvent"]
end
subgraph Components["Component Layer"]
RGNodePeel["@mousedown
RGNodePeel"]
RGLinePath["@click
RGLinePath"]
RGCanvas["@mousedown
RGCanvas"]
end
subgraph Core["RelationGraphWith7Event"]
onNodeDragStart["onNodeDragStart(node, e)"]
onNodeClick["onNodeClick(node, e)"]
onLineClick["onLineClick(line, e)"]
onCanvasClick["onCanvasClick(e)"]
expandOrCollapseNode["expandOrCollapseNode(node, e)"]
startCreatingNodePlot["startCreatingNodePlot(e, setting)"]
startCreatingLinePlot["startCreatingLinePlot(e, setting)"]
end
subgraph Base["RelationGraphBase"]
emitEvent["emitEvent(eventName, ...args)"]
defaultEventHandler["defaultEventHandler()"]
eventHandlers["eventHandlers: RGEventHandler[]"]
_emitHook["_emitHook: RGEventEmitHook"]
end
subgraph App["Application Layer"]
listeners["listeners: RGListeners"]
onNodeClickCB["listeners.onNodeClick?"]
onNodeDragStartCB["listeners.onNodeDragStart?"]
customHandler["Custom RGEventHandler"]
end
MouseEvent --> RGNodePeel
MouseEvent --> RGLinePath
MouseEvent --> RGCanvas
RGNodePeel -->|"@mousedown"| onNodeDragStart
RGNodePeel -->|"click detected"| onNodeClick
RGLinePath --> onLineClick
RGCanvas --> onCanvasClick
onNodeDragStart --> emitEvent
onNodeClick --> emitEvent
onLineClick --> emitEvent
onCanvasClick --> emitEvent
expandOrCollapseNode --> emitEvent
startCreatingNodePlot --> emitEvent
startCreatingLinePlot --> emitEvent
emitEvent --> defaultEventHandler
emitEvent --> eventHandlers
emitEvent --> _emitHook
defaultEventHandler --> listeners
listeners --> onNodeClickCB
listeners --> onNodeDragStartCB
eventHandlers --> customHandler
_emitHook --> AppEvent Processing Chain:
- DOM Event Capture - Framework components attach native event listeners (
@mousedown,@click,@wheel) - Handler Method Invocation - Component calls corresponding method on
RelationGraphWith7Event(e.g.,onNodeDragStart()) - Business Logic Execution - Handler method processes the interaction, updates
dataProvider, checks options - Event Emission - Handler calls
this.emitEvent(RGEventNames.onNodeDragStart, node, e) - Multi-tier Dispatch -
emitEvent()invokes handlers in order:defaultEventHandler()→ checksthis.listeners- Custom
eventHandlers[]→ user-registered middleware _emitHook()→ framework-specific emit (Vue$emit)
- Application Callback - User-defined functions in
RGListenersexecute with event data
Sources: packages/relation-graph-models/models/RelationGraphWith7Event.ts:37-80, packages/relation-graph-models/models/RelationGraphBase.ts:141-171
Core Event Components
RGEventNames Enumeration
All events in the system are identified by constants defined in the RGEventNames enum. This ensures type safety and prevents typos in event names:
enum RGEventNames {
onReady = 'onReady',
onNodeClick = 'onNodeClick',
onNodeDragStart = 'onNodeDragStart',
onNodeDragging = 'onNodeDragging',
onNodeDragEnd = 'onNodeDragEnd',
onLineClick = 'onLineClick',
onCanvasClick = 'onCanvasClick',
onCanvasDragging = 'onCanvasDragging',
onCanvasDragEnd = 'onCanvasDragEnd',
// ... 20+ event types total
}
Complete Event List:
| Category | Events |
|---|---|
| Node Events | onNodeClick, onNodeExpand, onNodeCollapse, onNodeDragStart, onNodeDragging, onNodeDragEnd |
| Line Events | onLineClick, onLineBeCreated, onLineVertexDropped, beforeCreateLine |
| Canvas Events | onCanvasClick, onCanvasDragStart, onCanvasDragging, onCanvasDragEnd, onCanvasSelectionEnd |
| View Events | beforeZoomStart, onZoomEnd, onViewResize, onFullscreen |
| Editing Events | onResizeStart, beforeNodeResize, onResizeEnd |
| Keyboard Events | onKeyboardDown, onKeyboardUp |
| Lifecycle Events | onReady, onForceLayoutFinish, beforeScrollStart |
| Context Menu | onContextmenu |
| Data Events | beforeAddNodes, beforeAddLines |
Sources: packages/relation-graph-models/types.ts:649-680
RGListeners Interface
The RGListeners interface defines the type signatures for all event handler callbacks. Applications implement this interface to handle events:
interface RGListeners {
onReady?: (graphInstance: RelationGraphInstance) => void;
onNodeClick?: (node: RGNode, e: RGUserEvent) => boolean | void | Promise<boolean | void>;
onNodeDragging?: (node: RGNode, newX: number, newY: number, buffX: number, buffY: number, e: RGUserEvent) => void | RGPosition | undefined;
onCanvasDragging?: (newX: number, newY: number, buffX: number, buffY: number) => void | RGPosition | undefined;
// ... all other event handlers
}
Return Value Patterns:
boolean | void- Returningfalsecancels default behavior (e.g.,beforeNodeResize,beforeCreateLine)RGPosition | undefined- Returning coordinates overrides computed position (e.g.,onNodeDragging,onCanvasDragging)void- No return value, just notification (most events)
Sources: packages/relation-graph-models/types.ts:417-593
RGUserEvent Type
The system normalizes browser events into a unified RGUserEvent type that supports both mouse and touch interactions:
type RGUserEvent = MouseEvent | TouchEvent;
Utility functions like getClientCoordinate() and isSupportTouch() abstract away the differences between mouse and touch events, allowing the core logic to handle both interaction types uniformly.
Sources: packages/relation-graph-models/types.ts:87
Event Registration and Handling
Method 1: Declarative Listeners (Recommended)
When creating a RelationGraph component, pass event handlers via the component props:
<RelationGraph
options={graphOptions}
onNodeClick={(node, e) => {
console.log('Node clicked:', node.text);
}}
onNodeDragEnd={(node, e, xBuff, yBuff) => {
console.log(`Node ${node.id} moved by (${xBuff}, ${yBuff})`);
}}
onCanvasClick={(e) => {
console.log('Canvas clicked at', e.clientX, e.clientY);
}}
/>
This approach works identically across Vue, React, and Svelte implementations.
Sources: packages/relation-graph-models/types.ts:868-871
Method 2: Programmatic Registration
For dynamic event handling, use addEventHandler() to register custom handlers that receive all events:
const graphInstance = await getGraphInstance();
graphInstance.addEventHandler((eventName, ...args) => {
if (eventName === RGEventNames.onNodeClick) {
const [node, event] = args;
console.log('Intercepted node click:', node.id);
return false; // Prevent default behavior
}
});
Multiple handlers can be registered, and they execute in registration order. Return undefined to continue to the next handler, or return a value to short-circuit the chain.
Sources: packages/relation-graph-models/models/RelationGraphBase.ts:106-123
Method 3: Event Emit Hook (Framework Integration)
Platform-specific implementations can register an emit hook to bridge to framework-native event systems (e.g., Vue’s $emit):
graphInstance.setEventEmitHook((eventName, ...args, callback) => {
// Forward to framework's event system
this.$emit(eventName, ...args);
// Call the callback to get return value from parent
callback(parentReturnValue);
});
This is used internally by Vue2/Vue3 adapters to integrate with component emit() functionality.
Sources: packages/relation-graph-models/models/RelationGraphBase.ts:125-134
User Interaction to Handler Method Mapping
Mapping of DOM Events to Core Handler Methods
| User Interaction | DOM Event | Component | Handler Method | Event Emitted |
|---|---|---|---|---|
| Click node | @click / @mousedown |
RGNodePeel | onNodeClick(node, e) |
RGEventNames.onNodeClick |
| Drag node | @mousedown + mousemove |
RGNodePeel | onNodeDragStart(node, e) |
RGEventNames.onNodeDragStartRGEventNames.onNodeDraggingRGEventNames.onNodeDragEnd |
| Click line | @click |
RGLinePath | onLineClick(line, e) |
RGEventNames.onLineClick |
| Click canvas | @mousedown |
RGCanvas | onCanvasClick(e) |
RGEventNames.onCanvasClick |
| Drag canvas | @mousedown + mousemove |
RGCanvas | onCanvasDragStart(e) |
RGEventNames.onCanvasDragStartRGEventNames.onCanvasDraggingRGEventNames.onCanvasDragEnd |
| Mouse wheel | @wheel |
RGCanvas | onMouseWheel(e) |
RGEventNames.beforeZoomStartRGEventNames.onZoomEnd |
| Expand/collapse | @click |
Expand button | expandOrCollapseNode(node, e) |
RGEventNames.onNodeExpandRGEventNames.onNodeCollapse |
| Resize node | @mousedown |
RGEditingNodeController | onResizeStart(type, e) |
RGEventNames.onResizeStartRGEventNames.beforeNodeResizeRGEventNames.onResizeEnd |
| Context menu | @contextmenu |
Multiple | onContextMenu(e, type, obj) |
RGEventNames.onContextmenu |
| Keyboard | @keydown / @keyup |
Document | onKeyDown(e) / onKeyUp(e) |
RGEventNames.onKeyboardDownRGEventNames.onKeyboardUp |
Sources: packages/relation-graph-models/models/RelationGraphWith7Event.ts:49-478, packages/relation-graph-models/models/RelationGraphWith91Editing.ts:150-341
Event Processing Pipeline
Internal Structure of emitEvent() Method
graph TB
Start["emitEvent(eventName: RGEventNames, ...args)"]
subgraph Phase1["Phase 1: defaultEventHandler()"]
CheckEvent["Switch on eventName"]
FindListener["Find this.listeners[eventName]"]
ExecDefault["Execute listener(...args)"]
CaptureResult1["Capture result, handled = true"]
end
subgraph Phase2["Phase 2: Custom eventHandlers[]"]
LoopHandlers["for (handler of this.eventHandlers)"]
CallHandler["customResult = handler(eventName, ...args)"]
CheckUndefined["customResult !== undefined?"]
Override["result = customResult
handled = true"]
end
subgraph Phase3["Phase 3: _emitHook (Vue emit)"]
CheckHook["this._emitHook exists?"]
CallHook["this._emitHook(eventName, ...args, callback)"]
HookCallback["callback(customReturnValue)"]
OverrideHook["if customReturnValue !== undefined
result = customReturnValue"]
end
Return["return result"]
Start --> Phase1
Phase1 --> CheckEvent
CheckEvent --> FindListener
FindListener -->|"exists"| ExecDefault
ExecDefault --> CaptureResult1
CaptureResult1 --> Phase2
Phase2 --> LoopHandlers
LoopHandlers --> CallHandler
CallHandler --> CheckUndefined
CheckUndefined -->|"yes"| Override
Override --> LoopHandlers
CheckUndefined -->|"no"| LoopHandlers
LoopHandlers -->|"done"| Phase3
Phase3 --> CheckHook
CheckHook -->|"yes"| CallHook
CallHook --> HookCallback
HookCallback --> OverrideHook
CheckHook -->|"no"| Return
OverrideHook --> ReturnHandler Execution Priority:
defaultEventHandler()- Executes first, checksthis.listenersobject for matching event name- Custom
eventHandlers[]- Loops through array ofRGEventHandlerfunctions registered viaaddEventHandler() _emitHook()- Framework integration point, typically used by Vue2/Vue3 for$emit()
Return Value Logic:
- First non-
undefinedreturn value becomes the finalresult handledflag tracks whether any handler executed- Final
resultpropagates back to calling code - Events like
beforeZoomStartuse return value to cancel operations (return false) - Events like
onNodeDragginguse return value to override coordinates (return {x, y})
Sources: packages/relation-graph-models/models/RelationGraphBase.ts:141-171, packages/relation-graph-models/models/RelationGraphBase.ts:179-367
User Interaction Categories
The relation-graph system supports a comprehensive range of user interactions, organized into the following categories:
Node Interactions
- Click - Select/deselect nodes, trigger custom actions
- Drag - Move nodes, with auto-canvas-scrolling at viewport edges
- Resize - Adjust node dimensions via editing controller handles
- Expand/Collapse - Toggle visibility of child nodes in hierarchical layouts
- Context Menu - Right-click for custom menus
See User Actions & Interactions for implementation details.
Canvas Interactions
- Drag - Pan the entire graph view
- Selection - Draw selection rectangle to select multiple nodes
- Zoom - Mouse wheel or pinch-to-zoom
- Click - Deselect all elements
Line Interactions
- Click - Select lines for editing
- Drag Control Points - Adjust orthogonal line paths (shapes 44/49)
- Drag Text - Reposition line labels
- Create - Multi-step wizard for drawing new connections
Keyboard Interactions
- Key Down/Up - Generic keyboard event handlers for custom shortcuts
- Focus-aware input detection prevents interference with text editing
Touch Support
All mouse-based interactions have touch equivalents:
- Single tap → click
- Drag → pan/move
- Pinch → zoom
- Long press → context menu
The system automatically detects touch capability using isSupportTouch() and adapts UI accordingly (e.g., hiding hover-based template guides).
Sources: packages/relation-graph-models/models/RelationGraphWith7Event.ts:37-478
Event Flow Example: Node Drag Operation
Complete Event Flow for Node Drag with RGDragUtils Integration
sequenceDiagram
participant User
participant RGNodePeel
participant With7Event as "RelationGraphWith7Event"
participant DragUtils as "RGDragUtils"
participant DataProvider as "dataProvider"
participant Base as "RelationGraphBase"
participant Listeners as "listeners: RGListeners"
User->>RGNodePeel: "@mousedown on node"
RGNodePeel->>With7Event: onNodeDragStart(node, e)
Note over With7Event: Check node.disableDrag
Check options.disableDragNode
Initialize _nodeXYMappingBeforeDrag
With7Event->>DragUtils: startDrag(e, nodeStartXY, dragEnd, draggingTick)
Note over DragUtils: document.addEventListener('mousemove')
document.addEventListener('mouseup')
loop requestAnimationFrame loop
User->>DragUtils: mousemove
DragUtils->>With7Event: draggingTick(offsetX, offsetY, ...)
alt First move > 4px
With7Event->>Base: emitEvent(RGEventNames.onNodeDragStart, node, e)
Base->>Listeners: listeners.onNodeDragStart?(node, e)
With7Event->>With7Event: this._canvasMovingTimer = requestAnimationFrame(movingLoop)
end
With7Event->>With7Event: draggingEvent = $draggingEvent
Note over With7Event: movingLoop() in requestAnimationFrame
With7Event->>With7Event: getCanvasXyByViewXy(draggingEventXy)
With7Event->>With7Event: Calculate newX, newY, buff_x, buff_y
With7Event->>Base: emitEvent(RGEventNames.onNodeDragging, node, newX, newY, buff_x, buff_y, e)
Base->>Listeners: listeners.onNodeDragging?(...)
Listeners-->>Base: return {x?, y?} or undefined
Base-->>With7Event: customPosition
alt customPosition returned
With7Event->>With7Event: Override newX/newY with customPosition
end
With7Event->>With7Event: canvasAutoMoving(draggingEventXy)
With7Event->>With7Event: draggingSelectedNodes(node, newX, newY, buff_x, buff_y)
With7Event->>DataProvider: updateNode(node.id, {x: newX, y: newY})
With7Event->>With7Event: _dataUpdated()
end
User->>DragUtils: mouseup
DragUtils->>With7Event: dragEnd(x_buff, y_buff, $dragEndEvent)
With7Event->>With7Event: cancelAnimationFrame(this._canvasMovingTimer)
With7Event->>DataProvider: updateOptions({draggingNodeId: ''})
alt dragStarted
With7Event->>Base: emitEvent(RGEventNames.onNodeDragEnd, node, e, x_buff, y_buff)
Base->>Listeners: listeners.onNodeDragEnd?(node, e, x_buff, y_buff)
else no drag (click)
With7Event->>With7Event: onNodeClick(node, e)
end
With7Event->>With7Event: _dataUpdated()Implementation Details:
| Aspect | Implementation |
|---|---|
| Drag Detection | Movement must exceed Math.abs(offsetX) + Math.abs(offsetY) > 4 to distinguish from click |
| Throttling | requestAnimationFrame() loop processes only latest draggingEvent per frame (~60fps) |
| Position Calculation | getCanvasXyByViewXy() converts view coordinates to canvas coordinates, accounting for zoom/offset |
| Auto-scroll | canvasAutoMoving() checks if cursor is within 40px of viewport edge, pans canvas accordingly |
| Multi-node Drag | draggingSelectedNodes() moves all nodes in editingController.nodes by same buff_x, buff_y |
| Reference Lines | updateReferenceLineView() calculates alignment with nearby nodes, returns snap coordinates |
| Drag Cleanup | Clears _nodeXYMappingBeforeDrag, resets draggingNodeId, calls _dataUpdated() |
State Tracking:
_nodeXYMappingBeforeDrag: {[nodeId:string]: {x, y}}- Stores original positions of dragged nodes_canvasMovingTimer: number-requestAnimationFrame()timer ID for drag loopdraggingEvent: RGUserEvent- Latest mouse/touch event, processed once per framedragStarted: boolean- Tracks whether movement exceeded threshold (4px)
Sources: packages/relation-graph-models/models/RelationGraphWith7Event.ts:80-190, packages/relation-graph-models/models/RelationGraphWith91Editing.ts:348-379, packages/relation-graph-models/utils/RGDragUtils.ts
Event Cancellation and Return Values
Certain events support cancellation or position override through their return values:
Boolean Return - Cancel Default Behavior
Events that return boolean | void allow cancellation by returning false:
| Event | Default Behavior Cancelled |
|---|---|
beforeZoomStart |
Prevents zoom operation |
beforeNodeResize |
Prevents node resize |
beforeCreateLine |
Prevents line creation |
beforeScrollStart |
Prevents canvas scroll |
Example:
onBeforeCreateLine: (lineInfo) => {
if (lineInfo.fromNode.id === lineInfo.toNode.id) {
console.log('Self-loops not allowed');
return false; // Cancel line creation
}
}
Sources: packages/relation-graph-models/types.ts:489-593
Position Return - Override Coordinates
Drag events that return RGPosition | undefined allow custom positioning logic:
| Event | Position Override Effect |
|---|---|
onNodeDragging |
Override computed node position during drag |
onCanvasDragging |
Override computed canvas offset during pan |
Example - Snap to Grid:
onNodeDragging: (node, newX, newY, buffX, buffY, e) => {
const gridSize = 20;
return {
x: Math.round(newX / gridSize) * gridSize,
y: Math.round(newY / gridSize) * gridSize
};
}
The system respects the returned coordinates and uses them instead of the calculated values.
Sources: packages/relation-graph-models/models/RelationGraphWith7Event.ts:129-139, packages/relation-graph-models/models/RelationGraphBase.ts:188-209
Drag Utilities: RGDragUtils
The RGDragUtils module provides a unified drag handling abstraction that works with both mouse and touch events.
Core Method Signature:
RGDragUtils.startDrag(
e: RGUserEvent,
basePosition: RGPosition,
dragEnd: (x_buff, y_buff, endEvent) => void,
dragging: (offsetX, offsetY, basePosition, startEventInfo, currentEvent) => void
)
Workflow:
- Event Normalization - Detects mouse vs. touch via
isSupportTouch(e), extracts coordinates usinggetClientCoordinate(e) - Listener Registration - Attaches
mousemove/touchmoveandmouseup/touchendtodocument.body - Tick Callback - Invokes
dragging()callback on every move event with offset calculations - Cleanup - Invokes
dragEnd()callback, removes event listeners on mouse/touch up
Used By:
onNodeDragStart()- Node draggingonCanvasDragStart()- Canvas panningonResizeStart()- Node resizing via editing controlleronVisibleViewHandleDragStart()- Mini-view viewport dragging
Sources: packages/relation-graph-models/utils/RGDragUtils.ts
Performance Considerations
Event Throttling with requestAnimationFrame
High-frequency events use requestAnimationFrame() throttling to limit updates to display refresh rate:
let draggingEvent: RGUserEvent;
const draggingCallback = () => {
if (!draggingEvent || dragStoped) return;
// Process draggingEvent...
};
const movingLoop = () => {
draggingCallback();
this._canvasMovingTimer = requestAnimationFrame(movingLoop);
};
this._canvasMovingTimer = requestAnimationFrame(movingLoop);
Benefits:
- Limits
onNodeDraggingandonCanvasDraggingemission to ~60fps - Prevents excessive React/Vue re-renders
- Only latest event per frame is processed
Used In:
onNodeDragStart()- packages/relation-graph-models/models/RelationGraphWith7Event.ts:136-170onResizeStart()- packages/relation-graph-models/models/RelationGraphWith91Editing.ts:179-185
Touch vs Mouse Detection
The system optimizes UI for touch devices by detecting interaction type:
const isTouchEvent = isSupportTouch(e);
if (!isTouchEvent) {
this.dataProvider.updateOptions({
showTemplateNode: true // Show hover guide for mouse only
});
}
Utility Functions:
isSupportTouch(e: RGUserEvent): boolean- Checks if event is TouchEventgetClientCoordinate(e: RGUserEvent): {clientX, clientY}- Extracts coordinates from mouse or touch event
Sources: packages/relation-graph-models/models/RelationGraphWith7Event.ts:392-397, packages/relation-graph-models/utils/RGCommon.ts
Debounced Layout Recalculation
Batch node expand/collapse operations use debouncing to avoid redundant layouts:
private _relayoutTaskTimer;
private _effectWhenExpandedOrCollapsed(node: RGNode) {
if (this._relayoutTaskTimer) {
clearTimeout(this._relayoutTaskTimer);
}
this._relayoutTaskTimer = setTimeout(() => {
this.updateNodesVisibleProperty([node].concat(descendantNodes));
if (options.reLayoutWhenExpandedOrCollapsed) {
this.doLayout();
}
}, 100);
}
Purpose: When expandNode() or collapseNode() called multiple times in quick succession, only one layout executes after 100ms delay.
Sources: packages/relation-graph-models/models/RelationGraphWith7Event.ts:308-327
Summary
The relation-graph event system provides:
- Unified event model across all frameworks (Vue, React, Svelte)
- Type-safe event definitions via
RGEventNamesenum andRGListenersinterface - Flexible registration through declarative props or programmatic handlers
- Event cancellation and override mechanisms for custom behavior
- Multi-layer handler execution supporting middleware patterns
- Touch and mouse support with automatic detection and adaptation
- Performance optimizations including throttling and debouncing
For architectural details about the event dispatcher and handler chain, continue to Event System Architecture. For concrete examples of implementing user interactions, see User Actions & Interactions.
Sources: packages/relation-graph-models/models/RelationGraphWith7Event.ts, packages/relation-graph-models/models/RelationGraphBase.ts, packages/relation-graph-models/types.ts:417-680