mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-11 14:52:55 +01:00
7.4 KiB
7.4 KiB
Canvas Overlay Mouse Event Handling
Overview
This document explains how mouse events are handled when overlays sit in front of the canvas. This is complex because events hit the overlay first but sometimes need to be routed to the canvas.
The Challenge
DOM Layering:
┌─────────────────────┐ ← Mouse events hit here first
│ Foreground Overlay │ (z-index: 2)
├─────────────────────┤
│ Canvas │ (z-index: 1)
├─────────────────────┤
│ Background Overlay │ (z-index: 0)
└─────────────────────┘
When the user clicks:
- Does it hit overlay UI (button, resize handle)?
- Does it hit a node visible through the overlay?
- Does it hit empty space?
The overlay must intelligently decide whether to handle or forward the event.
CommentLayer's Solution
Step 1: Capture All Mouse Events
Attach listeners to the foreground overlay div:
setupMouseEventHandling(foregroundDiv: HTMLDivElement) {
const events = {
mousedown: 'down',
mouseup: 'up',
mousemove: 'move',
click: 'click'
};
for (const eventName in events) {
foregroundDiv.addEventListener(eventName, (evt) => {
this.handleMouseEvent(evt, events[eventName]);
}, true); // Capture phase!
}
}
Step 2: Check for Overlay UI
handleMouseEvent(evt: MouseEvent, type: string) {
// Is this an overlay control?
if (evt.target && evt.target.closest('.comment-controls')) {
// Let it through - user is interacting with overlay UI
return;
}
// Otherwise, check if canvas should handle it...
}
Step 3: Forward to Canvas if Needed
// Convert mouse position to canvas coordinates
const tl = this.nodegraphEditor.topLeftCanvasPos;
const pos = {
x: evt.pageX - tl[0],
y: evt.pageY - tl[1]
};
// Ask canvas if it wants this event
const consumed = this.nodegraphEditor.mouse(type, pos, evt, {
eventPropagatedFromCommentLayer: true
});
if (consumed) {
// Canvas handled it (e.g., hit a node)
evt.stopPropagation();
evt.preventDefault();
}
Event Flow Diagram
Mouse Click
↓
Foreground Overlay receives event
↓
Is target .comment-controls?
├─ Yes → Let event propagate normally (overlay handles)
└─ No → Continue checking
↓
Forward to NodeGraphEditor.mouse()
↓
Did canvas consume event?
├─ Yes → Stop propagation (canvas handled)
└─ No → Let event propagate (overlay handles)
Preventing Infinite Loops
The eventPropagatedFromCommentLayer flag prevents recursion:
// In NodeGraphEditor
mouse(type, pos, evt, args) {
// Don't start another check if this came from overlay
if (args && args.eventPropagatedFromCommentLayer) {
// Just check if we hit something
const hitNode = this.findNodeAtPosition(pos);
return !!hitNode;
}
// Normal mouse handling...
}
Pointer Events CSS
Use pointer-events to control which elements receive events:
/* Overlay container - pass through clicks */
.overlay-container {
pointer-events: none;
}
/* But controls receive clicks */
.overlay-controls {
pointer-events: auto;
}
Mouse Wheel Handling
Wheel events have special handling:
foregroundDiv.addEventListener('wheel', (evt) => {
// Allow scroll in textarea
if (evt.target.tagName === 'TEXTAREA' && !evt.ctrlKey && !evt.metaKey) {
return; // Let it scroll
}
// Otherwise zoom the canvas
const tl = this.nodegraphEditor.topLeftCanvasPos;
this.nodegraphEditor.handleMouseWheelEvent(evt, {
offsetX: evt.pageX - tl[0],
offsetY: evt.pageY - tl[1]
});
});
Click vs Down/Up
NodeGraphEditor doesn't use click events, only down/up. Handle this:
let ignoreNextClick = false;
if (type === 'down' || type === 'up') {
if (consumed) {
// Canvas consumed the up/down, so ignore the click that follows
ignoreNextClick = true;
setTimeout(() => { ignoreNextClick = false; }, 1000);
}
}
if (type === 'click' && ignoreNextClick) {
ignoreNextClick = false;
evt.stopPropagation();
evt.preventDefault();
return;
}
Multi-Select Drag Initiation
Start dragging selected nodes/comments from overlay:
if (type === 'down') {
const hasSelection = this.props.selectedIds.length > 1 || this.nodegraphEditor.selector.active;
if (hasSelection) {
const canvasPos = this.nodegraphEditor.relativeCoordsToNodeGraphCords(pos);
// Check if starting drag on a selected item
const clickedItem = this.findItemAtPosition(canvasPos);
if (clickedItem && this.isSelected(clickedItem)) {
this.nodegraphEditor.startDraggingNodes(this.nodegraphEditor.selector.nodes);
evt.stopPropagation();
evt.preventDefault();
}
}
}
Common Patterns
Pattern 1: Overlay Button
<button className="overlay-button" onClick={() => this.handleButtonClick()} style={{ pointerEvents: 'auto' }}>
Delete
</button>
The className check catches this button, event doesn't forward to canvas.
Pattern 2: Draggable Overlay Element
// Using react-rnd
<Rnd
position={{ x: comment.x, y: comment.y }}
onDragStart={() => {
// Disable canvas mouse events during drag
this.nodegraphEditor.setMouseEventsEnabled(false);
}}
onDragStop={() => {
// Re-enable canvas mouse events
this.nodegraphEditor.setMouseEventsEnabled(true);
}}
>
{content}
</Rnd>
Pattern 3: Clickthrough SVG Overlay
<svg
style={{
position: 'absolute',
pointerEvents: 'none', // Pass all events through
...
}}
>
<path d={highlightPath} stroke="blue" />
</svg>
Keyboard Events
Forward keyboard events unless typing in an input:
foregroundDiv.addEventListener('keydown', (evt) => {
if (evt.target.tagName === 'TEXTAREA' || evt.target.tagName === 'INPUT') {
// Let the input handle it
return;
}
// Forward to KeyboardHandler
KeyboardHandler.instance.executeCommandMatchingKeyEvent(evt, 'down');
});
Best Practices
✅ Do
- Use capture phase -
addEventListener(event, handler, true) - Check target element -
evt.target.closest('.my-controls') - Prevent after handling - Call
stopPropagation()andpreventDefault() - Handle wheel specially - Allow textarea scroll, forward canvas zoom
❌ Don't
- Don't forward everything - Check if overlay should handle first
- Don't forget click events - Handle the click/down/up difference
- Don't block all events - Use
pointer-events: nonestrategically - Don't recurse - Use flags to prevent infinite forwarding
Debugging Tips
Log Event Flow
handleMouseEvent(evt, type) {
console.log('Event:', type, 'Target:', evt.target.className);
const consumed = this.nodegraphEditor.mouse(type, pos, evt, args);
console.log('Canvas consumed:', consumed);
}
Visualize Hit Areas
/* Temporarily add borders to debug */
.comment-controls {
border: 2px solid red !important;
}
Check Pointer Events
console.log('Pointer events:', window.getComputedStyle(element).pointerEvents);