diff --git a/browser_tests/tests/copyPaste.spec.ts-snapshots/paste-in-text-area-with-node-previously-copied-chromium-linux.png b/browser_tests/tests/copyPaste.spec.ts-snapshots/paste-in-text-area-with-node-previously-copied-chromium-linux.png index b1f181c758..89b4d401fd 100644 Binary files a/browser_tests/tests/copyPaste.spec.ts-snapshots/paste-in-text-area-with-node-previously-copied-chromium-linux.png and b/browser_tests/tests/copyPaste.spec.ts-snapshots/paste-in-text-area-with-node-previously-copied-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/node-title-edited-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/node-title-edited-chromium-linux.png index 0a4662c9c8..24c8ef9c01 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/node-title-edited-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/node-title-edited-chromium-linux.png differ diff --git a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-settings-dialog-mobile-chrome-linux.png b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-settings-dialog-mobile-chrome-linux.png index 207d037bf0..c179d1c754 100644 Binary files a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-settings-dialog-mobile-chrome-linux.png and b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-settings-dialog-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/added-node-no-connection-chromium-linux.png b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/added-node-no-connection-chromium-linux.png index fa23bba3ba..85818d60a2 100644 Binary files a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/added-node-no-connection-chromium-linux.png and b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/added-node-no-connection-chromium-linux.png differ diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index 7792260a4e..a92bf5f52e 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -1995,6 +1995,10 @@ export class LGraphCanvas implements CustomEventDispatcher this._key_callback = this.processKey.bind(this) canvas.addEventListener('keydown', this._key_callback, true) + // In Vue nodes mode, also listen on document for keydown since Vue elements may have focus + if (LiteGraph.vueNodesMode) { + document.addEventListener('keydown', this._key_callback, true) + } // keyup event must be bound on the document document.addEventListener('keyup', this._key_callback, true) @@ -2019,14 +2023,24 @@ export class LGraphCanvas implements CustomEventDispatcher const { canvas } = this // Assertions: removing nullish is fine. - canvas.removeEventListener('pointercancel', this._mousecancel_callback!) + // Note: capture flag must match addEventListener for removal to work + canvas.removeEventListener( + 'pointercancel', + this._mousecancel_callback!, + true + ) canvas.removeEventListener('pointerout', this._mouseout_callback!) canvas.removeEventListener('pointermove', this._mousemove_callback!) - canvas.removeEventListener('pointerup', this._mouseup_callback!) - canvas.removeEventListener('pointerdown', this._mousedown_callback!) + canvas.removeEventListener('pointerup', this._mouseup_callback!, true) + canvas.removeEventListener('pointerdown', this._mousedown_callback!, true) canvas.removeEventListener('wheel', this._mousewheel_callback!) - canvas.removeEventListener('keydown', this._key_callback!) - document.removeEventListener('keyup', this._key_callback!) + canvas.removeEventListener('keydown', this._key_callback!, true) + // Always remove document keydown listener - it may have been added if vueNodesMode + // was true during bindEvents, even if vueNodesMode has since changed + if (this._key_callback) { + document.removeEventListener('keydown', this._key_callback, true) + } + document.removeEventListener('keyup', this._key_callback!, true) canvas.removeEventListener('contextmenu', this._doNothing) canvas.removeEventListener('dragenter', this._doReturnTrue) @@ -3707,8 +3721,14 @@ export class LGraphCanvas implements CustomEventDispatcher if (!graph) return let block_default = false - // @ts-expect-error EventTarget.localName is not in standard types - if (e.target.localName == 'input') return + // Skip all text-editable surfaces to avoid blocking typing/selection/copy + const target = e.target as HTMLElement | null + if ( + target?.localName === 'input' || + target?.localName === 'textarea' || + target?.isContentEditable + ) + return if (e.type == 'keydown') { // TODO: Switch @@ -3744,9 +3764,12 @@ export class LGraphCanvas implements CustomEventDispatcher // paste this.pasteFromClipboard({ connectInputs: e.shiftKey }) } else if (e.key === 'Delete' || e.key === 'Backspace') { - // delete or backspace - // @ts-expect-error EventTarget.localName is not in standard types - if (e.target.localName != 'input' && e.target.localName != 'textarea') { + // delete or backspace (but don't intercept when editing text) + if ( + target?.localName !== 'input' && + target?.localName !== 'textarea' && + !target?.isContentEditable + ) { if (this.selectedItems.size === 0) { this.#noItemsSelected() return diff --git a/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.test.ts b/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.test.ts index c3410051a0..47fe3a848f 100644 --- a/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.test.ts +++ b/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.test.ts @@ -2,11 +2,12 @@ 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() diff --git a/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.ts b/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.ts index 0dfdae51b1..44cb981ebe 100644 --- a/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.ts +++ b/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.ts @@ -6,8 +6,8 @@ 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' export function useNodePointerInteractions( nodeIdRef: MaybeRefOrGetter @@ -65,6 +65,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 diff --git a/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts b/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts index 6e11902d80..485e3aa1b2 100644 --- a/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts +++ b/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts @@ -293,6 +293,10 @@ export function useSlotLinkInteraction({ raf.cancel() dragContext.dispose() clearCompatible() + // Reset litegraph pointer state + if (app.canvas) { + app.canvas.pointer.isDown = false + } } const updatePointerState = (event: PointerEvent) => { @@ -409,6 +413,11 @@ export function useSlotLinkInteraction({ const handlePointerMove = (event: PointerEvent) => { if (!pointerSession.matches(event)) return + + // When in panning mode (read_only), let litegraph handle panning - don't stop propagation + if (app.canvas?.read_only) return + + // Not in panning mode - Vue handles link drag, stop propagation to prevent litegraph interference event.stopPropagation() dragContext.pendingPointerMove = { @@ -539,7 +548,10 @@ export function useSlotLinkInteraction({ } const handlePointerUp = (event: PointerEvent) => { - event.stopPropagation() + // When in panning mode, let litegraph handle - but still cleanup our link drag state + if (!app.canvas?.read_only) { + event.stopPropagation() + } finishInteraction(event) } @@ -584,6 +596,10 @@ export function useSlotLinkInteraction({ if (event.button !== 0) return if (!nodeId) return if (pointerSession.isActive()) return + + // Don't start link drag if in panning mode - let litegraph handle panning + if (app.canvas?.read_only) return + event.preventDefault() event.stopPropagation() @@ -703,6 +719,9 @@ export function useSlotLinkInteraction({ ) pointerSession.begin(event.pointerId) + // Sync pointer state with litegraph so spacebar panning works + canvas.last_mouse = [event.clientX, event.clientY] + canvas.pointer.isDown = true toCanvasPointerEvent(event) updatePointerState(event)