mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 18:22:40 +00:00
Add z-index management in Vue Nodes based on interaction recency (#5429)
* fix z-index on selection for vue nodes * fix unused export * refactor to DDD * Use Tailwind utility for pointer events instead of inline style Move pointer-events: auto from inline style to Tailwind class pointer-events-auto as suggested in PR review. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Rename defaultSource to layoutSource parameter Rename parameter in useNodeZIndex options interface for better clarity as suggested in PR review. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Improve test mocking pattern with vi.mocked approach Replace global mock object with per-test vi.mocked pattern and proper Partial typing instead of as any, as suggested in PR review. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * [auto-fix] Apply ESLint and Prettier fixes --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
@@ -22,13 +22,14 @@
|
||||
'border-red-500 bg-red-50': error,
|
||||
'will-change-transform': isDragging
|
||||
},
|
||||
lodCssClass
|
||||
lodCssClass,
|
||||
'pointer-events-auto'
|
||||
)
|
||||
"
|
||||
:style="[
|
||||
{
|
||||
transform: `translate(${layoutPosition.x ?? position?.x ?? 0}px, ${(layoutPosition.y ?? position?.y ?? 0) - LiteGraph.NODE_TITLE_HEIGHT}px)`,
|
||||
pointerEvents: 'auto'
|
||||
zIndex: zIndex
|
||||
},
|
||||
dragStyle
|
||||
]"
|
||||
@@ -192,6 +193,7 @@ onErrorCaptured((error) => {
|
||||
// Use layout system for node position and dragging
|
||||
const {
|
||||
position: layoutPosition,
|
||||
zIndex,
|
||||
startDrag,
|
||||
handleDrag: handleLayoutDrag,
|
||||
endDrag
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Node Event Handlers Composable
|
||||
*
|
||||
* Handles all Vue node interaction events including:
|
||||
* - Node selection with multi-select support
|
||||
* - Node collapse/expand state management
|
||||
* - Node title editing and updates
|
||||
* - Layout mutations for visual feedback
|
||||
* - Integration with LiteGraph canvas selection system
|
||||
*/
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useNodeZIndex } from '@/renderer/extensions/vueNodes/composables/useNodeZIndex'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
interface NodeManager {
|
||||
getNode: (id: string) => any
|
||||
}
|
||||
|
||||
export function useNodeEventHandlers(nodeManager: Ref<NodeManager | null>) {
|
||||
const canvasStore = useCanvasStore()
|
||||
const { bringNodeToFront } = useNodeZIndex()
|
||||
|
||||
/**
|
||||
* Handle node selection events
|
||||
* Supports single selection and multi-select with Ctrl/Cmd
|
||||
*/
|
||||
const handleNodeSelect = (event: PointerEvent, nodeData: VueNodeData) => {
|
||||
if (!canvasStore.canvas || !nodeManager.value) return
|
||||
|
||||
const node = nodeManager.value.getNode(nodeData.id)
|
||||
if (!node) return
|
||||
|
||||
const isMultiSelect = event.ctrlKey || event.metaKey
|
||||
|
||||
if (isMultiSelect) {
|
||||
// Ctrl/Cmd+click -> toggle selection
|
||||
if (node.selected) {
|
||||
canvasStore.canvas.deselect(node)
|
||||
} else {
|
||||
canvasStore.canvas.select(node)
|
||||
}
|
||||
} else {
|
||||
// Regular click -> single select
|
||||
canvasStore.canvas.deselectAll()
|
||||
canvasStore.canvas.select(node)
|
||||
}
|
||||
|
||||
// Bring node to front when clicked (similar to LiteGraph behavior)
|
||||
// Skip if node is pinned to avoid unwanted movement
|
||||
if (!node.flags?.pinned) {
|
||||
bringNodeToFront(nodeData.id)
|
||||
}
|
||||
|
||||
// Update canvas selection tracking
|
||||
canvasStore.updateSelectedItems()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle node collapse/expand state changes
|
||||
* Uses LiteGraph's native collapse method for proper state management
|
||||
*/
|
||||
const handleNodeCollapse = (nodeId: string, collapsed: boolean) => {
|
||||
if (!nodeManager.value) return
|
||||
|
||||
const node = nodeManager.value.getNode(nodeId)
|
||||
if (!node) return
|
||||
|
||||
// Use LiteGraph's collapse method if the state needs to change
|
||||
const currentCollapsed = node.flags?.collapsed ?? false
|
||||
if (currentCollapsed !== collapsed) {
|
||||
node.collapse()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle node title updates
|
||||
* Updates the title in LiteGraph for persistence across sessions
|
||||
*/
|
||||
const handleNodeTitleUpdate = (nodeId: string, newTitle: string) => {
|
||||
if (!nodeManager.value) return
|
||||
|
||||
const node = nodeManager.value.getNode(nodeId)
|
||||
if (!node) return
|
||||
|
||||
// Update the node title in LiteGraph for persistence
|
||||
node.title = newTitle
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle node double-click events
|
||||
* Can be used for custom actions like opening node editor
|
||||
*/
|
||||
const handleNodeDoubleClick = (
|
||||
event: PointerEvent,
|
||||
nodeData: VueNodeData
|
||||
) => {
|
||||
if (!canvasStore.canvas || !nodeManager.value) return
|
||||
|
||||
const node = nodeManager.value.getNode(nodeData.id)
|
||||
if (!node) return
|
||||
|
||||
// Prevent default browser behavior
|
||||
event.preventDefault()
|
||||
|
||||
// TODO: add custom double-click behavior here
|
||||
// For now, ensure node is selected
|
||||
if (!node.selected) {
|
||||
handleNodeSelect(event, nodeData)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle node right-click context menu events
|
||||
* Integrates with LiteGraph's context menu system
|
||||
*/
|
||||
const handleNodeRightClick = (event: PointerEvent, nodeData: VueNodeData) => {
|
||||
if (!canvasStore.canvas || !nodeManager.value) return
|
||||
|
||||
const node = nodeManager.value.getNode(nodeData.id)
|
||||
if (!node) return
|
||||
|
||||
// Prevent default context menu
|
||||
event.preventDefault()
|
||||
|
||||
// Select the node if not already selected
|
||||
if (!node.selected) {
|
||||
handleNodeSelect(event, nodeData)
|
||||
}
|
||||
|
||||
// Let LiteGraph handle the context menu
|
||||
// The canvas will handle showing the appropriate context menu
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle node drag start events
|
||||
* Prepares node for dragging and sets appropriate visual state
|
||||
*/
|
||||
const handleNodeDragStart = (event: DragEvent, nodeData: VueNodeData) => {
|
||||
if (!canvasStore.canvas || !nodeManager.value) return
|
||||
|
||||
const node = nodeManager.value.getNode(nodeData.id)
|
||||
if (!node) return
|
||||
|
||||
// Ensure node is selected before dragging
|
||||
if (!node.selected) {
|
||||
// Create a synthetic pointer event for selection
|
||||
const syntheticEvent = new PointerEvent('pointerdown', {
|
||||
ctrlKey: event.ctrlKey,
|
||||
metaKey: event.metaKey,
|
||||
bubbles: true
|
||||
})
|
||||
handleNodeSelect(syntheticEvent, nodeData)
|
||||
}
|
||||
|
||||
// Set drag data for potential drop operations
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.setData('application/comfy-node-id', nodeData.id)
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch select multiple nodes
|
||||
* Useful for selection toolbox or area selection
|
||||
*/
|
||||
const selectNodes = (nodeIds: string[], addToSelection = false) => {
|
||||
if (!canvasStore.canvas || !nodeManager.value) return
|
||||
|
||||
if (!addToSelection) {
|
||||
canvasStore.canvas.deselectAll()
|
||||
}
|
||||
|
||||
nodeIds.forEach((nodeId) => {
|
||||
const node = nodeManager.value?.getNode(nodeId)
|
||||
if (node && canvasStore.canvas) {
|
||||
canvasStore.canvas.select(node)
|
||||
}
|
||||
})
|
||||
|
||||
canvasStore.updateSelectedItems()
|
||||
}
|
||||
|
||||
/**
|
||||
* Deselect specific nodes
|
||||
*/
|
||||
const deselectNodes = (nodeIds: string[]) => {
|
||||
if (!canvasStore.canvas || !nodeManager.value) return
|
||||
|
||||
nodeIds.forEach((nodeId) => {
|
||||
const node = nodeManager.value?.getNode(nodeId)
|
||||
if (node) {
|
||||
node.selected = false
|
||||
}
|
||||
})
|
||||
|
||||
canvasStore.updateSelectedItems()
|
||||
}
|
||||
|
||||
return {
|
||||
// Core event handlers
|
||||
handleNodeSelect,
|
||||
handleNodeCollapse,
|
||||
handleNodeTitleUpdate,
|
||||
handleNodeDoubleClick,
|
||||
handleNodeRightClick,
|
||||
handleNodeDragStart,
|
||||
|
||||
// Batch operations
|
||||
selectNodes,
|
||||
deselectNodes
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Node Z-Index Management Composable
|
||||
*
|
||||
* Provides focused functionality for managing node layering through z-index.
|
||||
* Integrates with the layout system to ensure proper visual ordering.
|
||||
*/
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
|
||||
interface NodeZIndexOptions {
|
||||
/**
|
||||
* Layout source for z-index mutations
|
||||
* @default LayoutSource.Vue
|
||||
*/
|
||||
layoutSource?: LayoutSource
|
||||
}
|
||||
|
||||
export function useNodeZIndex(options: NodeZIndexOptions = {}) {
|
||||
const { layoutSource = LayoutSource.Vue } = options
|
||||
const layoutMutations = useLayoutMutations()
|
||||
|
||||
/**
|
||||
* Bring node to front (highest z-index)
|
||||
* @param nodeId - The node to bring to front
|
||||
* @param source - Optional source override
|
||||
*/
|
||||
function bringNodeToFront(nodeId: NodeId, source?: LayoutSource) {
|
||||
layoutMutations.setSource(source ?? layoutSource)
|
||||
layoutMutations.bringNodeToFront(nodeId)
|
||||
}
|
||||
|
||||
return {
|
||||
bringNodeToFront
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user