mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
feat: drop nodes when click the canvas
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="flex h-full flex-col overflow-hidden">
|
||||
<div id="node-library-node-preview-container-v2" />
|
||||
<NodeDragPreview />
|
||||
<!-- Fixed header -->
|
||||
<div class="shrink-0 px-4 pt-2 pb-1">
|
||||
<h2 class="m-0 mb-1 text-sm font-bold leading-8">
|
||||
@@ -70,7 +71,7 @@ import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import SearchBox from '@/components/common/SearchBoxV2.vue'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useNodeDragToCanvas } from '@/composables/node/useNodeDragToCanvas'
|
||||
import {
|
||||
DEFAULT_TAB_ID,
|
||||
nodeOrganizationService
|
||||
@@ -86,6 +87,7 @@ import type {
|
||||
import AllNodesPanel from './nodeLibrary/AllNodesPanel.vue'
|
||||
import CustomNodesPanel from './nodeLibrary/CustomNodesPanel.vue'
|
||||
import EssentialNodesPanel from './nodeLibrary/EssentialNodesPanel.vue'
|
||||
import NodeDragPreview from './nodeLibrary/NodeDragPreview.vue'
|
||||
|
||||
const selectedTab = useLocalStorage<TabId>(
|
||||
'Comfy.NodeLibrary.Tab',
|
||||
@@ -109,7 +111,7 @@ const expandedKeys = computed({
|
||||
})
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const litegraphService = useLitegraphService()
|
||||
const { startDrag } = useNodeDragToCanvas()
|
||||
|
||||
const filteredNodeDefs = computed(() => {
|
||||
if (searchQuery.value.length === 0) {
|
||||
@@ -222,7 +224,7 @@ function collectFolderKeys(node: TreeNode): string[] {
|
||||
|
||||
function handleNodeClick(node: RenderedTreeExplorerNode<ComfyNodeDefImpl>) {
|
||||
if (node.type === 'node' && node.data) {
|
||||
litegraphService.addNodeOnGraph(node.data)
|
||||
startDrag(node.data)
|
||||
}
|
||||
if (node.type === 'folder') {
|
||||
const index = expandedKeys.value.indexOf(node.key)
|
||||
|
||||
27
src/components/sidebar/tabs/nodeLibrary/NodeDragPreview.vue
Normal file
27
src/components/sidebar/tabs/nodeLibrary/NodeDragPreview.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="isDragging && draggedNode"
|
||||
class="pointer-events-none fixed z-[10000] rounded-md bg-comfy-menu-bg px-3 py-2 text-sm font-medium text-foreground shadow-lg border border-border-default"
|
||||
:style="{
|
||||
left: `${cursorPosition.x + 12}px`,
|
||||
top: `${cursorPosition.y + 12}px`
|
||||
}"
|
||||
>
|
||||
{{ draggedNode.display_name }}
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
import { useNodeDragToCanvas } from '@/composables/node/useNodeDragToCanvas'
|
||||
|
||||
const { isDragging, draggedNode, cursorPosition, setupGlobalListeners } =
|
||||
useNodeDragToCanvas()
|
||||
|
||||
onMounted(() => {
|
||||
setupGlobalListeners()
|
||||
})
|
||||
</script>
|
||||
207
src/composables/node/useNodeDragToCanvas.test.ts
Normal file
207
src/composables/node/useNodeDragToCanvas.test.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import type { useNodeDragToCanvas as UseNodeDragToCanvasType } from './useNodeDragToCanvas'
|
||||
|
||||
const mockAddNodeOnGraph = vi.fn()
|
||||
const mockConvertEventToCanvasOffset = vi.fn()
|
||||
const mockCanvas = {
|
||||
canvas: {
|
||||
getBoundingClientRect: vi.fn()
|
||||
},
|
||||
convertEventToCanvasOffset: mockConvertEventToCanvasOffset
|
||||
}
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: vi.fn(() => ({
|
||||
canvas: mockCanvas
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/services/litegraphService', () => ({
|
||||
useLitegraphService: vi.fn(() => ({
|
||||
addNodeOnGraph: mockAddNodeOnGraph
|
||||
}))
|
||||
}))
|
||||
|
||||
describe('useNodeDragToCanvas', () => {
|
||||
let useNodeDragToCanvas: typeof UseNodeDragToCanvasType
|
||||
|
||||
const mockNodeDef = {
|
||||
name: 'TestNode',
|
||||
display_name: 'Test Node'
|
||||
} as ComfyNodeDefImpl
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
|
||||
const module = await import('./useNodeDragToCanvas')
|
||||
useNodeDragToCanvas = module.useNodeDragToCanvas
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('startDrag', () => {
|
||||
it('should set isDragging to true and store the node definition', () => {
|
||||
const { isDragging, draggedNode, startDrag } = useNodeDragToCanvas()
|
||||
|
||||
expect(isDragging.value).toBe(false)
|
||||
expect(draggedNode.value).toBeNull()
|
||||
|
||||
startDrag(mockNodeDef)
|
||||
|
||||
expect(isDragging.value).toBe(true)
|
||||
expect(draggedNode.value).toBe(mockNodeDef)
|
||||
})
|
||||
})
|
||||
|
||||
describe('cancelDrag', () => {
|
||||
it('should reset isDragging and draggedNode', () => {
|
||||
const { isDragging, draggedNode, startDrag, cancelDrag } =
|
||||
useNodeDragToCanvas()
|
||||
|
||||
startDrag(mockNodeDef)
|
||||
expect(isDragging.value).toBe(true)
|
||||
|
||||
cancelDrag()
|
||||
|
||||
expect(isDragging.value).toBe(false)
|
||||
expect(draggedNode.value).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('setupGlobalListeners', () => {
|
||||
it('should add event listeners to document', () => {
|
||||
const addEventListenerSpy = vi.spyOn(document, 'addEventListener')
|
||||
const { setupGlobalListeners } = useNodeDragToCanvas()
|
||||
|
||||
setupGlobalListeners()
|
||||
|
||||
expect(addEventListenerSpy).toHaveBeenCalledWith(
|
||||
'pointermove',
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(addEventListenerSpy).toHaveBeenCalledWith(
|
||||
'pointerup',
|
||||
expect.any(Function),
|
||||
true
|
||||
)
|
||||
expect(addEventListenerSpy).toHaveBeenCalledWith(
|
||||
'keydown',
|
||||
expect.any(Function)
|
||||
)
|
||||
})
|
||||
|
||||
it('should only setup listeners once', () => {
|
||||
const addEventListenerSpy = vi.spyOn(document, 'addEventListener')
|
||||
const { setupGlobalListeners } = useNodeDragToCanvas()
|
||||
|
||||
setupGlobalListeners()
|
||||
const callCount = addEventListenerSpy.mock.calls.length
|
||||
|
||||
setupGlobalListeners()
|
||||
|
||||
expect(addEventListenerSpy.mock.calls.length).toBe(callCount)
|
||||
})
|
||||
})
|
||||
|
||||
describe('cursorPosition', () => {
|
||||
it('should update on pointermove', () => {
|
||||
const { cursorPosition, setupGlobalListeners } = useNodeDragToCanvas()
|
||||
|
||||
setupGlobalListeners()
|
||||
|
||||
const pointerEvent = new PointerEvent('pointermove', {
|
||||
clientX: 100,
|
||||
clientY: 200
|
||||
})
|
||||
document.dispatchEvent(pointerEvent)
|
||||
|
||||
expect(cursorPosition.value).toEqual({ x: 100, y: 200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('endDrag behavior', () => {
|
||||
it('should add node when pointer is over canvas', () => {
|
||||
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
|
||||
left: 0,
|
||||
right: 500,
|
||||
top: 0,
|
||||
bottom: 500
|
||||
})
|
||||
mockConvertEventToCanvasOffset.mockReturnValue([150, 150])
|
||||
|
||||
const { startDrag, setupGlobalListeners } = useNodeDragToCanvas()
|
||||
|
||||
setupGlobalListeners()
|
||||
startDrag(mockNodeDef)
|
||||
|
||||
const pointerEvent = new PointerEvent('pointerup', {
|
||||
clientX: 250,
|
||||
clientY: 250,
|
||||
bubbles: true
|
||||
})
|
||||
document.dispatchEvent(pointerEvent)
|
||||
|
||||
expect(mockAddNodeOnGraph).toHaveBeenCalledWith(mockNodeDef, {
|
||||
pos: [150, 150]
|
||||
})
|
||||
})
|
||||
|
||||
it('should not add node when pointer is outside canvas', () => {
|
||||
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
|
||||
left: 0,
|
||||
right: 500,
|
||||
top: 0,
|
||||
bottom: 500
|
||||
})
|
||||
|
||||
const { startDrag, setupGlobalListeners, isDragging } =
|
||||
useNodeDragToCanvas()
|
||||
|
||||
setupGlobalListeners()
|
||||
startDrag(mockNodeDef)
|
||||
|
||||
const pointerEvent = new PointerEvent('pointerup', {
|
||||
clientX: 600,
|
||||
clientY: 250,
|
||||
bubbles: true
|
||||
})
|
||||
document.dispatchEvent(pointerEvent)
|
||||
|
||||
expect(mockAddNodeOnGraph).not.toHaveBeenCalled()
|
||||
expect(isDragging.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should cancel drag on Escape key', () => {
|
||||
const { startDrag, setupGlobalListeners, isDragging } =
|
||||
useNodeDragToCanvas()
|
||||
|
||||
setupGlobalListeners()
|
||||
startDrag(mockNodeDef)
|
||||
|
||||
expect(isDragging.value).toBe(true)
|
||||
|
||||
const keyEvent = new KeyboardEvent('keydown', { key: 'Escape' })
|
||||
document.dispatchEvent(keyEvent)
|
||||
|
||||
expect(isDragging.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should not cancel drag on other keys', () => {
|
||||
const { startDrag, setupGlobalListeners, isDragging } =
|
||||
useNodeDragToCanvas()
|
||||
|
||||
setupGlobalListeners()
|
||||
startDrag(mockNodeDef)
|
||||
|
||||
const keyEvent = new KeyboardEvent('keydown', { key: 'Enter' })
|
||||
document.dispatchEvent(keyEvent)
|
||||
|
||||
expect(isDragging.value).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
75
src/composables/node/useNodeDragToCanvas.ts
Normal file
75
src/composables/node/useNodeDragToCanvas.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { ref, shallowRef } from 'vue'
|
||||
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
|
||||
const isDragging = ref(false)
|
||||
const draggedNode = shallowRef<ComfyNodeDefImpl | null>(null)
|
||||
const cursorPosition = ref({ x: 0, y: 0 })
|
||||
let listenersSetup = false
|
||||
|
||||
function updatePosition(e: PointerEvent) {
|
||||
cursorPosition.value = { x: e.clientX, y: e.clientY }
|
||||
}
|
||||
|
||||
function cancelDrag() {
|
||||
isDragging.value = false
|
||||
draggedNode.value = null
|
||||
}
|
||||
|
||||
function endDrag(e: PointerEvent) {
|
||||
if (!isDragging.value || !draggedNode.value) return
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
const canvas = canvasStore.canvas
|
||||
if (!canvas) {
|
||||
cancelDrag()
|
||||
return
|
||||
}
|
||||
|
||||
const canvasElement = canvas.canvas as HTMLCanvasElement
|
||||
const rect = canvasElement.getBoundingClientRect()
|
||||
const isOverCanvas =
|
||||
e.clientX >= rect.left &&
|
||||
e.clientX <= rect.right &&
|
||||
e.clientY >= rect.top &&
|
||||
e.clientY <= rect.bottom
|
||||
|
||||
if (isOverCanvas) {
|
||||
const pos = canvas.convertEventToCanvasOffset(e)
|
||||
const litegraphService = useLitegraphService()
|
||||
litegraphService.addNodeOnGraph(draggedNode.value, { pos })
|
||||
}
|
||||
|
||||
cancelDrag()
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') cancelDrag()
|
||||
}
|
||||
|
||||
function setupGlobalListeners() {
|
||||
if (listenersSetup) return
|
||||
listenersSetup = true
|
||||
|
||||
document.addEventListener('pointermove', updatePosition)
|
||||
document.addEventListener('pointerup', endDrag, true)
|
||||
document.addEventListener('keydown', handleKeydown)
|
||||
}
|
||||
|
||||
export function useNodeDragToCanvas() {
|
||||
function startDrag(nodeDef: ComfyNodeDefImpl) {
|
||||
isDragging.value = true
|
||||
draggedNode.value = nodeDef
|
||||
}
|
||||
|
||||
return {
|
||||
isDragging,
|
||||
draggedNode,
|
||||
cursorPosition,
|
||||
startDrag,
|
||||
cancelDrag,
|
||||
setupGlobalListeners
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user