fix: spacebar panning in vueNodes mode

Forward keydown events to litegraph's processKey when Vue nodes have
focus. Litegraph only binds keydown to the canvas element, so spacebar
panning didn't work when Vue nodes were focused.

Also sync pointer state with litegraph canvas during link dragging to
enable spacebar panning while dragging connections.

Fixes #7806
This commit is contained in:
Johnpaul
2025-12-31 00:48:35 +01:00
parent 68d1d21865
commit f5ac48c5be
3 changed files with 99 additions and 3 deletions

View File

@@ -2,16 +2,33 @@ import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick, ref } from 'vue'
import { useNodePointerInteractions } from '@/renderer/extensions/vueNodes/composables/useNodePointerInteractions'
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
import { createTestingPinia } from '@pinia/testing'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import type { NodeLayout } from '@/renderer/core/layout/types'
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
import { useNodePointerInteractions } from '@/renderer/extensions/vueNodes/composables/useNodePointerInteractions'
import { useNodeDrag } from '@/renderer/extensions/vueNodes/layout/useNodeDrag'
const forwardEventToCanvasMock = vi.fn()
const selectedItemsState: { items: Array<{ id?: string }> } = { items: [] }
const mockCanvas = vi.hoisted(() => {
const canvasElement = document.createElement('canvas')
return {
canvas: canvasElement,
processKey: vi.fn()
}
})
vi.mock('@/scripts/app', () => ({
app: {
get canvas() {
return mockCanvas
}
}
}))
// Mock the dependencies
vi.mock('@/renderer/core/canvas/useCanvasInteractions', () => ({
useCanvasInteractions: () => ({
@@ -324,4 +341,42 @@ describe('useNodePointerInteractions', () => {
true
)
})
describe('keydown forwarding for spacebar panning', () => {
it('forwards keydown events to canvas.processKey when target is not the canvas', () => {
// Initialize the composable to set up keydown forwarding
useNodePointerInteractions('test-node-123')
// Create a div to simulate a Vue node element
const vueNodeElement = document.createElement('div')
document.body.appendChild(vueNodeElement)
// Dispatch keydown event on the Vue node element (not the canvas)
const keydownEvent = new KeyboardEvent('keydown', {
key: ' ',
bubbles: true
})
vueNodeElement.dispatchEvent(keydownEvent)
// Should forward to canvas.processKey
expect(mockCanvas.processKey).toHaveBeenCalledWith(keydownEvent)
document.body.removeChild(vueNodeElement)
})
it('does not forward keydown events when target is the canvas itself', () => {
useNodePointerInteractions('test-node-123')
mockCanvas.processKey.mockClear()
// Dispatch keydown event directly on the canvas element
const keydownEvent = new KeyboardEvent('keydown', {
key: ' ',
bubbles: true
})
mockCanvas.canvas.dispatchEvent(keydownEvent)
// Should NOT forward (canvas handles it directly)
expect(mockCanvas.processKey).not.toHaveBeenCalled()
})
})
})

View File

@@ -6,12 +6,34 @@ import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
import { isMultiSelectKey } from '@/renderer/extensions/vueNodes/utils/selectionUtils'
import { useNodeDrag } from '@/renderer/extensions/vueNodes/layout/useNodeDrag'
import { isMultiSelectKey } from '@/renderer/extensions/vueNodes/utils/selectionUtils'
import { app } from '@/scripts/app'
// Forward keydown events to litegraph's processKey when Vue nodes have focus
let keydownForwardingInitialized = false
function initKeydownForwarding() {
if (keydownForwardingInitialized) return
keydownForwardingInitialized = true
document.addEventListener(
'keydown',
(e) => {
const canvas = app.canvas
if (!canvas) return
if (e.target === canvas.canvas) return
canvas.processKey(e)
},
true
)
}
export function useNodePointerInteractions(
nodeIdRef: MaybeRefOrGetter<string>
) {
initKeydownForwarding()
const { startDrag, endDrag, handleDrag } = useNodeDrag()
// Use canvas interactions for proper wheel event handling and pointer event capture control
const { forwardEventToCanvas, shouldHandleNodePointerEvents } =
@@ -65,6 +87,12 @@ export function useNodePointerInteractions(
function onPointermove(event: PointerEvent) {
if (forwardMiddlePointerIfNeeded(event)) return
// Don't handle pointer events when canvas is in panning mode - forward to canvas instead
if (!shouldHandleNodePointerEvents.value) {
forwardEventToCanvas(event)
return
}
// Don't activate drag while resizing
if (layoutStore.isResizingVueNodes.value) return

View File

@@ -293,6 +293,9 @@ export function useSlotLinkInteraction({
raf.cancel()
dragContext.dispose()
clearCompatible()
if (app.canvas?.pointer) {
app.canvas.pointer.isDown = false
}
}
const updatePointerState = (event: PointerEvent) => {
@@ -409,6 +412,12 @@ export function useSlotLinkInteraction({
const handlePointerMove = (event: PointerEvent) => {
if (!pointerSession.matches(event)) return
const canvas = app.canvas
if (canvas?.read_only && canvas.dragging_canvas) {
canvas.processMouseMove(event)
}
event.stopPropagation()
dragContext.pendingPointerMove = {
@@ -703,6 +712,10 @@ export function useSlotLinkInteraction({
)
pointerSession.begin(event.pointerId)
if (canvas.pointer) {
canvas.pointer.isDown = true
}
canvas.last_mouse = [event.clientX, event.clientY]
toCanvasPointerEvent(event)
updatePointerState(event)