Compare commits

...

16 Commits

Author SHA1 Message Date
Alexander Brown
16ad2c5975 Merge branch 'main' into fix/spacebar-panning-vue-nodes 2026-01-13 13:27:54 -08:00
github-actions
813f54bff4 [automated] Update test expectations 2026-01-06 19:54:57 +00:00
Johnpaul
0f6fb90d04 format files 2026-01-06 20:34:16 +01:00
Johnpaul
0972a46a15 Merge remote-tracking branch 'origin/main' into fix/spacebar-panning-vue-nodes 2026-01-06 20:32:17 +01:00
Johnpaul
f5cdee82e9 fix: add capture flag to removeEventListener calls and reuse target variable
- Add capture:true to removeEventListener calls that were added with capture
  to prevent event listener leaks when unbinding
- Reuse existing target variable in Delete/Backspace check instead of
  re-accessing e.target with @ts-expect-error
- Add isContentEditable check to Delete/Backspace guard for consistency
2026-01-06 20:31:51 +01:00
GitHub Action
571bb51ab5 [automated] Apply ESLint and Prettier fixes 2026-01-06 19:16:58 +00:00
Johnpaul
a47081c169 fix: skip text-editable elements in processKey
Broaden the target guard in processKey to skip all text-editable surfaces
(input, textarea, contenteditable) before handling shortcuts. This prevents
space/Ctrl+A/C from being blocked when typing in prompt textareas or other
multiline fields in Vue nodes mode.
2026-01-06 20:14:58 +01:00
Johnpaul
0e47f9fb10 fix: always remove document keydown listener in unbindEvents
Remove the LiteGraph.vueNodesMode check when removing the document-level
keydown listener in unbindEvents. If vueNodesMode was true during bindEvents
but changed before unbindEvents, the listener would be left dangling.
Now guarded only by existence of _key_callback.
2026-01-05 22:48:08 +01:00
Johnpaul
9427d7ed60 Merge remote-tracking branch 'origin/main' into fix/spacebar-panning-vue-nodes 2026-01-05 22:24:39 +01:00
Johnpaul
d114b01a75 refactor: consolidate panning state in litegraph
Remove duplicate panning state management from useSlotLinkInteraction.
Instead of Vue manually manipulating canvas.ds.offset, sync pointer.isDown
with litegraph and let events bubble when in panning mode (read_only).

- Remove isPanningDuringLinkDrag and lastPanningMouse local state
- Remove manual offset manipulation in handlePointerMove
- Conditionally stopPropagation only when NOT in panning mode
- Set canvas.pointer.isDown so spacebar triggers litegraph panning
2026-01-05 22:22:59 +01:00
Johnpaul
53bdf7f6c3 fix: enable spacebar panning during slot link drag
Move spacebar detection to document-level listener in LGraphCanvas when
vueNodesMode is enabled. Implement direct panning in useSlotLinkInteraction
when spacebar is held during connection drag, bypassing litegraph event
handling for smoother panning while maintaining link position updates.

Fixes #7806
2026-01-05 22:09:11 +01:00
Johnpaul
fc082d84b9 refactor: use read_only as signal, let events bubble to litegraph
- Set canvas.read_only directly when spacebar pressed
- Skip stopPropagation when read_only is true in slot link handler
- Remove manual dragging_canvas and processMouseMove management
- Litegraph handles panning through its normal event handlers
2025-12-31 06:58:50 +01:00
Johnpaul
3cf19af173 fix: Explicitly pass null to isEditableElement when activeElement.value is falsy. 2025-12-31 02:15:39 +01:00
Johnpaul
1d1e16b62d refactor: simplify spacebar forwarding to module-level initialization 2025-12-31 02:04:45 +01:00
Johnpaul
2f8f5253b5 refactor: address review comments - use useMagicKeys and derive button state
- Refactored spacebar forwarding to use useMagicKeys from VueUse instead of raw event listeners
- Added filtering for editable elements (input, textarea, contentEditable)
- Changed panning check in useSlotLinkInteraction to derive button state from event.buttons instead of storing canvas.pointer.isDown statefully
- Updated tests to work with the new useMagicKeys approach using vi.hoisted pattern
2025-12-31 01:28:27 +01:00
Johnpaul
f5ac48c5be 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
2025-12-31 00:48:35 +01:00
8 changed files with 63 additions and 14 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -1953,6 +1953,10 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
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)
@@ -1977,14 +1981,24 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
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)
@@ -3668,8 +3682,14 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
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
@@ -3705,9 +3725,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
// 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

View File

@@ -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()

View File

@@ -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<string>
@@ -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

View File

@@ -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)