Select Vue Nodes After Drag (#5863)

This pull request refactors the node selection logic in the Vue nodes
event handler composable to simplify the function signature and improve
single vs. multi-selection behavior. The main change is the removal of
the `wasDragging` parameter from the `handleNodeSelect` function, with
selection logic now determined by the current selection state. Related
test code is updated to match the new function signature.

**Node selection logic improvements:**

* Refactored the `handleNodeSelect` function in
`useNodeEventHandlersIndividual` to remove the `wasDragging` parameter,
making the function signature simpler and relying on selection state to
handle single vs. multi-selection.
* Updated the selection logic to check if multiple nodes are already
selected using `isLGraphNode`, and only perform single selection if not.

**Code and test updates:**

* Updated all calls to `handleNodeSelect` in the composable to remove
the `wasDragging` argument, ensuring consistent usage throughout the
codebase.
[[1]](diffhunk://#diff-8d3820a1ca9c569bce00671fdd6290af81315ae11b8f3d6f29a5a9d30379d084L125-R123)
[[2]](diffhunk://#diff-8d3820a1ca9c569bce00671fdd6290af81315ae11b8f3d6f29a5a9d30379d084L146-R144)
[[3]](diffhunk://#diff-8d3820a1ca9c569bce00671fdd6290af81315ae11b8f3d6f29a5a9d30379d084L173-R171)
* Updated all related test cases to use the new `handleNodeSelect`
signature without the third parameter.
[[1]](diffhunk://#diff-89bfc2a05201c6ff7116578efa45f96097594eb346f18446c70aa7125ab1811aL105-R105)
[[2]](diffhunk://#diff-89bfc2a05201c6ff7116578efa45f96097594eb346f18446c70aa7125ab1811aL125-R125)
[[3]](diffhunk://#diff-89bfc2a05201c6ff7116578efa45f96097594eb346f18446c70aa7125ab1811aL144-R144)
[[4]](diffhunk://#diff-89bfc2a05201c6ff7116578efa45f96097594eb346f18446c70aa7125ab1811aL162-R162)
[[5]](diffhunk://#diff-89bfc2a05201c6ff7116578efa45f96097594eb346f18446c70aa7125ab1811aL174-R174)
[[6]](diffhunk://#diff-89bfc2a05201c6ff7116578efa45f96097594eb346f18446c70aa7125ab1811aL187-R187)

**Utility import:**

* Added an import for `isLGraphNode` from `@/utils/litegraphUtil` to
support the updated selection logic.## Summary

<!-- One sentence describing what changed and why. -->


## Screenshots (if applicable)



https://github.com/user-attachments/assets/71e856d3-afc2-497d-826e-5b485066e7fe

---------

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Johnpaul Chiwetelu
2025-10-10 04:48:03 +01:00
committed by GitHub
parent eeb0977738
commit 4cb03cf052
9 changed files with 208 additions and 139 deletions

View File

@@ -69,125 +69,85 @@ const createMouseEvent = (
}
describe('useNodePointerInteractions', () => {
beforeEach(() => {
beforeEach(async () => {
vi.clearAllMocks()
forwardEventToCanvasMock.mockClear()
// Reset layout store state between tests
const { layoutStore } = await import(
'@/renderer/core/layout/store/layoutStore'
)
layoutStore.isDraggingVueNodes.value = false
})
it('should only start drag on left-click', async () => {
const mockNodeData = createMockVueNodeData()
const mockOnPointerUp = vi.fn()
const mockOnNodeSelect = vi.fn()
const { pointerHandlers } = useNodePointerInteractions(
ref(mockNodeData),
mockOnPointerUp
mockOnNodeSelect
)
// Right-click should not start drag
// Right-click should not trigger selection
const rightClickEvent = createPointerEvent('pointerdown', { button: 2 })
pointerHandlers.onPointerdown(rightClickEvent)
expect(mockOnPointerUp).not.toHaveBeenCalled()
expect(mockOnNodeSelect).not.toHaveBeenCalled()
// Left-click should start drag and emit callback
// Left-click should trigger selection on pointer down
const leftClickEvent = createPointerEvent('pointerdown', { button: 0 })
pointerHandlers.onPointerdown(leftClickEvent)
const pointerUpEvent = createPointerEvent('pointerup')
pointerHandlers.onPointerup(pointerUpEvent)
expect(mockOnPointerUp).toHaveBeenCalledWith(
pointerUpEvent,
mockNodeData,
false // wasDragging = false (same position)
)
expect(mockOnNodeSelect).toHaveBeenCalledWith(leftClickEvent, mockNodeData)
})
it('forwards middle mouse interactions to the canvas', () => {
it('should call onNodeSelect on pointer down', async () => {
const mockNodeData = createMockVueNodeData()
const mockOnPointerUp = vi.fn()
const mockOnNodeSelect = vi.fn()
const { pointerHandlers } = useNodePointerInteractions(
ref(mockNodeData),
mockOnPointerUp
mockOnNodeSelect
)
const middlePointerDown = createPointerEvent('pointerdown', { button: 1 })
pointerHandlers.onPointerdown(middlePointerDown)
expect(forwardEventToCanvasMock).toHaveBeenCalledWith(middlePointerDown)
forwardEventToCanvasMock.mockClear()
const middlePointerMove = createPointerEvent('pointermove', { buttons: 4 })
pointerHandlers.onPointermove(middlePointerMove)
expect(forwardEventToCanvasMock).toHaveBeenCalledWith(middlePointerMove)
forwardEventToCanvasMock.mockClear()
const middlePointerUp = createPointerEvent('pointerup', { button: 1 })
pointerHandlers.onPointerup(middlePointerUp)
expect(forwardEventToCanvasMock).toHaveBeenCalledWith(middlePointerUp)
expect(mockOnPointerUp).not.toHaveBeenCalled()
})
it('should distinguish drag from click based on distance threshold', async () => {
const mockNodeData = createMockVueNodeData()
const mockOnPointerUp = vi.fn()
const { pointerHandlers } = useNodePointerInteractions(
ref(mockNodeData),
mockOnPointerUp
)
// Test drag (distance > 4px)
pointerHandlers.onPointerdown(
createPointerEvent('pointerdown', { clientX: 100, clientY: 100 })
)
const dragUpEvent = createPointerEvent('pointerup', {
clientX: 200,
clientY: 200
// Selection should happen on pointer down
const downEvent = createPointerEvent('pointerdown', {
clientX: 100,
clientY: 100
})
pointerHandlers.onPointerup(dragUpEvent)
pointerHandlers.onPointerdown(downEvent)
expect(mockOnPointerUp).toHaveBeenCalledWith(
dragUpEvent,
mockNodeData,
true
expect(mockOnNodeSelect).toHaveBeenCalledWith(downEvent, mockNodeData)
mockOnNodeSelect.mockClear()
// Even if we drag, selection already happened on pointer down
pointerHandlers.onPointerup(
createPointerEvent('pointerup', { clientX: 200, clientY: 200 })
)
mockOnPointerUp.mockClear()
// Test click (same position)
const samePos = { clientX: 100, clientY: 100 }
pointerHandlers.onPointerdown(createPointerEvent('pointerdown', samePos))
const clickUpEvent = createPointerEvent('pointerup', samePos)
pointerHandlers.onPointerup(clickUpEvent)
expect(mockOnPointerUp).toHaveBeenCalledWith(
clickUpEvent,
mockNodeData,
false
)
// onNodeSelect should not be called again on pointer up
expect(mockOnNodeSelect).not.toHaveBeenCalled()
})
it('should handle drag termination via cancel and context menu', async () => {
const mockNodeData = createMockVueNodeData()
const mockOnPointerUp = vi.fn()
const mockOnNodeSelect = vi.fn()
const { pointerHandlers } = useNodePointerInteractions(
ref(mockNodeData),
mockOnPointerUp
mockOnNodeSelect
)
// Test pointer cancel
// Test pointer cancel - selection happens on pointer down
pointerHandlers.onPointerdown(createPointerEvent('pointerdown'))
expect(mockOnNodeSelect).toHaveBeenCalledTimes(1)
pointerHandlers.onPointercancel(createPointerEvent('pointercancel'))
// Should not emit callback on cancel
expect(mockOnPointerUp).not.toHaveBeenCalled()
// Selection should have been called on pointer down only
expect(mockOnNodeSelect).toHaveBeenCalledTimes(1)
mockOnNodeSelect.mockClear()
// Test context menu during drag prevents default
pointerHandlers.onPointerdown(createPointerEvent('pointerdown'))
@@ -200,36 +160,35 @@ describe('useNodePointerInteractions', () => {
expect(preventDefaultSpy).toHaveBeenCalled()
})
it('should not emit callback when nodeData becomes null', async () => {
it('should not call onNodeSelect when nodeData is null', async () => {
const mockNodeData = createMockVueNodeData()
const mockOnPointerUp = vi.fn()
const mockOnNodeSelect = vi.fn()
const nodeDataRef = ref<VueNodeData | null>(mockNodeData)
const { pointerHandlers } = useNodePointerInteractions(
nodeDataRef,
mockOnPointerUp
mockOnNodeSelect
)
// Clear nodeData before pointer down
nodeDataRef.value = null
await nextTick()
pointerHandlers.onPointerdown(createPointerEvent('pointerdown'))
// Clear nodeData before pointerup
nodeDataRef.value = null
pointerHandlers.onPointerup(createPointerEvent('pointerup'))
expect(mockOnPointerUp).not.toHaveBeenCalled()
expect(mockOnNodeSelect).not.toHaveBeenCalled()
})
it('should integrate with layout store dragging state', async () => {
const mockNodeData = createMockVueNodeData()
const mockOnPointerUp = vi.fn()
const mockOnNodeSelect = vi.fn()
const { layoutStore } = await import(
'@/renderer/core/layout/store/layoutStore'
)
const { pointerHandlers } = useNodePointerInteractions(
ref(mockNodeData),
mockOnPointerUp
mockOnNodeSelect
)
// Start drag
@@ -242,4 +201,93 @@ describe('useNodePointerInteractions', () => {
await nextTick()
expect(layoutStore.isDraggingVueNodes.value).toBe(false)
})
it('should select node on pointer down with ctrl key for multi-select', async () => {
const mockNodeData = createMockVueNodeData()
const mockOnNodeSelect = vi.fn()
const { pointerHandlers } = useNodePointerInteractions(
ref(mockNodeData),
mockOnNodeSelect
)
// Pointer down with ctrl key should pass the event with ctrl key set
const ctrlDownEvent = createPointerEvent('pointerdown', {
ctrlKey: true,
clientX: 100,
clientY: 100
})
pointerHandlers.onPointerdown(ctrlDownEvent)
expect(mockOnNodeSelect).toHaveBeenCalledWith(ctrlDownEvent, mockNodeData)
expect(mockOnNodeSelect).toHaveBeenCalledTimes(1)
})
it('should select pinned node on pointer down but not start drag', async () => {
const mockNodeData = createMockVueNodeData({
flags: { pinned: true }
})
const mockOnNodeSelect = vi.fn()
const { layoutStore } = await import(
'@/renderer/core/layout/store/layoutStore'
)
const { pointerHandlers } = useNodePointerInteractions(
ref(mockNodeData),
mockOnNodeSelect
)
// Pointer down on pinned node
const downEvent = createPointerEvent('pointerdown')
pointerHandlers.onPointerdown(downEvent)
// Should select the node
expect(mockOnNodeSelect).toHaveBeenCalledWith(downEvent, mockNodeData)
// But should not start dragging
expect(layoutStore.isDraggingVueNodes.value).toBe(false)
})
it('should select node immediately when drag starts', async () => {
const mockNodeData = createMockVueNodeData()
const mockOnNodeSelect = vi.fn()
const { layoutStore } = await import(
'@/renderer/core/layout/store/layoutStore'
)
const { pointerHandlers } = useNodePointerInteractions(
ref(mockNodeData),
mockOnNodeSelect
)
// Pointer down should select node immediately
const downEvent = createPointerEvent('pointerdown', {
clientX: 100,
clientY: 100
})
pointerHandlers.onPointerdown(downEvent)
// Selection should happen on pointer down (before move)
expect(mockOnNodeSelect).toHaveBeenCalledWith(downEvent, mockNodeData)
expect(mockOnNodeSelect).toHaveBeenCalledTimes(1)
// Dragging state should be active
expect(layoutStore.isDraggingVueNodes.value).toBe(true)
// Move the pointer (start dragging)
pointerHandlers.onPointermove(
createPointerEvent('pointermove', { clientX: 150, clientY: 150 })
)
// Selection should still only have been called once (on pointer down)
expect(mockOnNodeSelect).toHaveBeenCalledTimes(1)
// End drag
pointerHandlers.onPointerup(
createPointerEvent('pointerup', { clientX: 150, clientY: 150 })
)
// Selection should still only have been called once
expect(mockOnNodeSelect).toHaveBeenCalledTimes(1)
})
})