fix selection issues

This commit is contained in:
Guilhem
2025-11-12 20:53:30 +01:00
parent 3ca9a3145b
commit 6702c61b73
5 changed files with 38 additions and 136 deletions
@@ -281,7 +281,7 @@
style="width: 275px; height: 34px;"
onmouseenter={() => (hover = true)}
onmouseleave={() => (hover = false)}
onpointerdown={stopPropagation(preventDefault(() => dispatch('pointerdown')))}
onpointerdown={stopPropagation(preventDefault((e) => dispatch('pointerdown', e)))}
>
{#if deletable}
<ModuleAcceptReject action={moduleAction ?? action} {id} />
@@ -395,11 +395,8 @@
insert: (detail) => {
onInsert?.(detail)
},
select: (mod: string | FlowModule) => {
select: (modId) => {
if (!notSelectable) {
// TODO: Handle Ctrl/Cmd and Shift modifiers when node-level click events are available
// For now, normal click behavior
const modId = typeof mod === 'string' ? mod : mod.id
selectionManager.selectId(modId)
onSelect?.(modId)
}
@@ -478,10 +475,24 @@
return false
}
// Clear SvelteFlow's internal selection by creating new nodes array
function clearFlowSelection() {
nodes = nodes.map((node) => {
if (node.selected) {
return { ...node, selected: false }
}
return node
})
}
// Keyboard event handling
function handleKeyDown(event: KeyboardEvent) {
selectionManager.handleKeyDown(event, nodes)
noteManager.handleKeyDown(event)
if (event.key === 'Escape') {
// Clear SvelteFlow's internal selection state
clearFlowSelection()
}
if (noteMode) {
exitNoteMode?.()
}
@@ -786,12 +797,13 @@
selectionOnDrag={selectionManager.mode === 'rect-select'}
elementsSelectable={true}
selectionMode={SelectionMode.Partial}
selectionKey={selectionManager.mode === 'rect-select' ? null : 'Meta'}
selectionKey={selectionManager.mode === 'rect-select' || !editMode ? null : 'Meta'}
panActivationKey={selectionManager.mode === 'rect-select' ? 'Meta' : null}
panOnDrag={selectionManager.mode === 'rect-select' ? [1] : true}
zoomOnDoubleClick={false}
elevateNodesOnSelect={false}
{proOptions}
multiSelectionKey={'Meta'}
nodesDraggable={false}
--background-color={false}
>
@@ -815,7 +827,7 @@
{/if}
<!-- SelectionTool for handling selection changes and filtering -->
<SelectionTool {nodes} {modules} {selectionManager} />
<SelectionTool {selectionManager} />
{#if leftHeader}
<div class="absolute top-2 left-2 z-10">
@@ -1,13 +1,11 @@
<script lang="ts">
import { useOnSelectionChange, useStore, type Node } from '@xyflow/svelte'
import type { SelectionManager } from './selectionUtils.svelte'
interface Props {
nodes: any[]
modules?: any[]
selectionManager: any
selectionManager: SelectionManager
}
let { nodes, modules, selectionManager }: Props = $props()
let { selectionManager }: Props = $props()
// Get store to access selectionRect
const store = useStore()
@@ -17,35 +15,17 @@
// Notes are already non-selectable, so no filtering needed
const selectedNodeIds = selectedNodes.map((node: Node) => node.id)
if (selectedNodeIds.length > 0) {
selectionManager.selectNodes(selectedNodeIds, false, modules, nodes)
} else if (selectedNodes.length === 0) {
// Clear selection when SvelteFlow selection is cleared
selectionManager.clearSelection()
}
})
// Compute selection box bounds
let selectionBoxBounds = $derived(() => {
const rect = store.selectionRect
if (!rect) {
return null
}
// selectionRect is already in the correct coordinate system relative to the flow container
// Just return it directly
return {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height
// Only select nodes if multiple nodes are selected
// To avoid conflicting with the node-level click events
if (selectedNodeIds.length > 1) {
selectionManager.selectNodes(selectedNodeIds, false)
}
})
</script>
<!-- Render custom selection box during drag selection -->
{#if selectionBoxBounds()}
{@const bounds = selectionBoxBounds()!}
{#if store.selectionRect}
{@const bounds = store.selectionRect!}
<div
class="absolute rounded cursor-pointer bg-surface-selected/30 border border-accent/30 pointer-events-none"
style="
@@ -596,7 +596,7 @@ export function graphBuilder(
}
const resultNode: NodeLayout = {
id: 'result',
id: 'Result',
data: {
eventHandlers: eventHandlers,
success: success,
@@ -1,4 +1,3 @@
import type { FlowModule } from '$lib/gen'
import type { Node } from '@xyflow/svelte'
export interface SelectionState {
@@ -14,6 +13,9 @@ export class SelectionManager {
constructor() {}
selectId(id: string) {
if (this.selectedIds.length === 1 && this.selectedIds[0] === id) {
return
}
this.selectedIds = [id]
}
@@ -27,56 +29,10 @@ export class SelectionManager {
set mode(mode: 'normal' | 'rect-select') {
this.#selectionMode = mode
// Note: No automatic selection clearing when changing modes - preserve current selection
}
// Get hierarchical children of a node
getNodeChildrenIds(nodeId: string, modules: FlowModule[] | undefined, nodes: Node[]): string[] {
const module = modules?.find((m) => m.id === nodeId)
if (!module) return []
const childrenIds: string[] = []
// For hierarchical modules, find all children between start and end using proper graph traversal
if (
module.value.type === 'forloopflow' ||
module.value.type === 'whileloopflow' ||
module.value.type === 'branchall' ||
module.value.type === 'branchone'
) {
const endNodeId = `${nodeId}-end`
const endNode = nodes.find((n) => n.id === endNodeId)
if (endNode) {
// Traverse from end node back to start using parentIds
const visited = new Set<string>()
const toVisit = [endNodeId]
while (toVisit.length > 0) {
const currentId = toVisit.shift()!
if (visited.has(currentId) || currentId === nodeId) continue
visited.add(currentId)
const currentNode = nodes.find((n) => n.id === currentId)
if (currentNode && (currentNode as any).parentIds) {
const parentIds = (currentNode as any).parentIds as string[]
for (const parentId of parentIds) {
if (parentId !== nodeId && !visited.has(parentId)) {
childrenIds.push(parentId)
toVisit.push(parentId)
}
}
}
}
}
}
return childrenIds
}
// Select nodes with optional hierarchical selection
selectNodes(nodeIds: string[], addToExisting = false, modules?: FlowModule[], nodes?: Node[]) {
selectNodes(nodeIds: string[], addToExisting = false) {
// Guard against empty nodeIds or uninitialized state
if (!nodeIds || nodeIds.length === 0) {
if (!addToExisting) {
@@ -85,59 +41,13 @@ export class SelectionManager {
return
}
const newSelection = addToExisting ? [...this.selectedIds] : []
const newSelection = addToExisting ? [...this.selectedIds, ...nodeIds] : nodeIds
nodeIds.forEach((nodeId) => {
// Only add valid node IDs that exist in the current nodes
if (!nodes || nodes.some((node) => node.id === nodeId)) {
if (!newSelection.includes(nodeId)) {
newSelection.push(nodeId)
}
// Auto-select children for hierarchical modules
if (modules && nodes) {
const children = this.getNodeChildrenIds(nodeId, modules, nodes)
children.forEach((childId) => {
if (nodes.some((node) => node.id === childId) && !newSelection.includes(childId)) {
newSelection.push(childId)
}
})
}
}
})
this.selectedIds = newSelection
}
// Toggle node selection
toggleNodeSelection(nodeId: string, modules?: FlowModule[], nodes?: Node[]) {
const newSelection = [...this.selectedIds]
if (newSelection.includes(nodeId)) {
// Remove node
const index = newSelection.indexOf(nodeId)
newSelection.splice(index, 1)
// Also remove children
if (modules && nodes) {
const children = this.getNodeChildrenIds(nodeId, modules, nodes)
children.forEach((childId) => {
const childIndex = newSelection.indexOf(childId)
if (childIndex > -1) {
newSelection.splice(childIndex, 1)
}
})
}
} else {
// Add node
newSelection.push(nodeId)
// Auto-select children for hierarchical modules
if (modules && nodes) {
const children = this.getNodeChildrenIds(nodeId, modules, nodes)
children.forEach((childId) => {
if (!newSelection.includes(childId)) {
newSelection.push(childId)
}
})
}
// If the new selection is the same as the current selection, do nothing
if (JSON.stringify(newSelection) === JSON.stringify($state.snapshot(this.selectedIds))) {
return
}
this.selectedIds = newSelection
}