refactor: streamline flow board interactions
This commit is contained in:
@@ -33,7 +33,11 @@ defineEmits<{
|
||||
{ entering }
|
||||
]"
|
||||
:style="{ left: left + '%', top: top + '%' }"
|
||||
@click="$emit('select', agent.id)"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
:aria-label="`${agent.name} öffnen`"
|
||||
@keydown.enter.prevent="$emit('select', agent.id)"
|
||||
@keydown.space.prevent="$emit('select', agent.id)"
|
||||
>
|
||||
<div class="ncard">
|
||||
<!-- Header: Avatar + Name + Role + Status-Dot -->
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { AgentNodeData } from '../../../composables/useFlowLayout'
|
||||
import { autoLayout, buildEdges, curve } from '../../../composables/useFlowLayout'
|
||||
import { icons } from '../../../composables/icons'
|
||||
import AgentNode from './AgentNode.vue'
|
||||
import { useFlowCanvasInteractions } from './useFlowCanvasInteractions'
|
||||
|
||||
const props = defineProps<{
|
||||
agents: AgentNodeData[]
|
||||
@@ -172,93 +173,18 @@ watch(
|
||||
)
|
||||
|
||||
/* ── Drag & Drop ──────────────────────────────── */
|
||||
const DRAG_THRESHOLD = 5
|
||||
|
||||
interface DragState {
|
||||
id: string
|
||||
startX: number
|
||||
startY: number
|
||||
ox: number
|
||||
oy: number
|
||||
moved: boolean
|
||||
raf: number | null
|
||||
}
|
||||
|
||||
let drag: DragState | null = null
|
||||
|
||||
function onPointerDown(e: PointerEvent) {
|
||||
const node = (e.target as HTMLElement).closest('.node') as HTMLElement | null
|
||||
if (!node) return
|
||||
|
||||
e.preventDefault()
|
||||
const nr = node.getBoundingClientRect()
|
||||
|
||||
drag = {
|
||||
id: node.dataset.id || '',
|
||||
startX: e.clientX,
|
||||
startY: e.clientY,
|
||||
ox: e.clientX - (nr.left + nr.width / 2),
|
||||
oy: e.clientY - (nr.top + nr.height / 2),
|
||||
moved: false,
|
||||
raf: null,
|
||||
}
|
||||
|
||||
node.setPointerCapture(e.pointerId)
|
||||
}
|
||||
|
||||
function onPointerMove(e: PointerEvent) {
|
||||
if (!drag) return
|
||||
|
||||
const dist = Math.hypot(e.clientX - drag.startX, e.clientY - drag.startY)
|
||||
if (!drag.moved && dist < DRAG_THRESHOLD) return
|
||||
|
||||
if (!drag.moved) {
|
||||
drag.moved = true
|
||||
const node = flowRef.value?.querySelector(`.node[data-id="${drag.id}"]`) as HTMLElement | null
|
||||
if (node) node.classList.add('dragging')
|
||||
}
|
||||
|
||||
const flow = flowRef.value
|
||||
if (!flow) return
|
||||
|
||||
const fr = flow.getBoundingClientRect()
|
||||
const x = Math.max(8, Math.min(92, ((e.clientX - drag.ox - fr.left) / fr.width) * 100))
|
||||
const y = Math.max(10, Math.min(92, ((e.clientY - drag.oy - fr.top) / fr.height) * 100))
|
||||
|
||||
// Direct DOM manipulation for responsiveness
|
||||
const node = flow.querySelector(`.node[data-id="${drag.id}"]`) as HTMLElement | null
|
||||
if (node) {
|
||||
node.style.left = x + '%'
|
||||
node.style.top = y + '%'
|
||||
}
|
||||
|
||||
// Update positions state
|
||||
const newPos = { ...props.positions }
|
||||
newPos[drag.id] = { x, y }
|
||||
emit('updatePositions', newPos)
|
||||
|
||||
// Debounced edge re-render
|
||||
if (!drag.raf) {
|
||||
drag.raf = requestAnimationFrame(() => {
|
||||
renderEdges()
|
||||
if (drag) drag.raf = null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function onPointerUp() {
|
||||
if (!drag) return
|
||||
|
||||
const node = flowRef.value?.querySelector(`.node[data-id="${drag.id}"]`) as HTMLElement | null
|
||||
if (node) node.classList.remove('dragging')
|
||||
|
||||
if (!drag.moved) {
|
||||
// Was a click — emit select
|
||||
emit('select', drag.id)
|
||||
}
|
||||
|
||||
drag = null
|
||||
}
|
||||
const {
|
||||
onClickCapture,
|
||||
onPointerDown,
|
||||
onPointerMove,
|
||||
onPointerUp,
|
||||
} = useFlowCanvasInteractions({
|
||||
flowRef,
|
||||
renderEdges,
|
||||
updatePositions: positions => emit('updatePositions', positions),
|
||||
selectAgent: id => emit('select', id),
|
||||
getPositions: () => props.positions,
|
||||
})
|
||||
|
||||
/* ── Keyboard handler for Enter key on buttons ── */
|
||||
function handleReset() {
|
||||
@@ -271,6 +197,7 @@ function handleReset() {
|
||||
<div
|
||||
ref="flowRef"
|
||||
class="flow"
|
||||
@click.capture="onClickCapture"
|
||||
@pointerdown="onPointerDown"
|
||||
@pointermove="onPointerMove"
|
||||
@pointerup="onPointerUp"
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
const DRAG_THRESHOLD = 5
|
||||
const CLICK_SUPPRESSION_MS = 250
|
||||
|
||||
export interface FlowPosition {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
interface DragState {
|
||||
id: string
|
||||
startX: number
|
||||
startY: number
|
||||
ox: number
|
||||
oy: number
|
||||
moved: boolean
|
||||
raf: number | null
|
||||
}
|
||||
|
||||
interface UseFlowCanvasInteractionsOptions {
|
||||
flowRef: { value: HTMLElement | null }
|
||||
renderEdges: () => void
|
||||
updatePositions: (positions: Record<string, FlowPosition>) => void
|
||||
selectAgent: (id: string) => void
|
||||
getPositions: () => Record<string, FlowPosition>
|
||||
}
|
||||
|
||||
function findNode(target: EventTarget | null) {
|
||||
return (target as HTMLElement | null)?.closest('.node') as HTMLElement | null
|
||||
}
|
||||
|
||||
export function useFlowCanvasInteractions(options: UseFlowCanvasInteractionsOptions) {
|
||||
const drag = ref<DragState | null>(null)
|
||||
const suppressClickUntil = ref(0)
|
||||
|
||||
function onPointerDown(e: PointerEvent) {
|
||||
const node = findNode(e.target)
|
||||
if (!node) return
|
||||
|
||||
e.preventDefault()
|
||||
const nr = node.getBoundingClientRect()
|
||||
|
||||
drag.value = {
|
||||
id: node.dataset.id || '',
|
||||
startX: e.clientX,
|
||||
startY: e.clientY,
|
||||
ox: e.clientX - (nr.left + nr.width / 2),
|
||||
oy: e.clientY - (nr.top + nr.height / 2),
|
||||
moved: false,
|
||||
raf: null,
|
||||
}
|
||||
|
||||
node.setPointerCapture(e.pointerId)
|
||||
}
|
||||
|
||||
function onPointerMove(e: PointerEvent) {
|
||||
if (!drag.value) return
|
||||
|
||||
const currentDrag = drag.value
|
||||
const dist = Math.hypot(e.clientX - currentDrag.startX, e.clientY - currentDrag.startY)
|
||||
if (!currentDrag.moved && dist < DRAG_THRESHOLD) return
|
||||
|
||||
if (!currentDrag.moved) {
|
||||
currentDrag.moved = true
|
||||
const node = options.flowRef.value?.querySelector(`.node[data-id="${currentDrag.id}"]`) as HTMLElement | null
|
||||
if (node) node.classList.add('dragging')
|
||||
}
|
||||
|
||||
const flow = options.flowRef.value
|
||||
if (!flow) return
|
||||
|
||||
const fr = flow.getBoundingClientRect()
|
||||
const x = Math.max(8, Math.min(92, ((e.clientX - currentDrag.ox - fr.left) / fr.width) * 100))
|
||||
const y = Math.max(10, Math.min(92, ((e.clientY - currentDrag.oy - fr.top) / fr.height) * 100))
|
||||
|
||||
const node = flow.querySelector(`.node[data-id="${currentDrag.id}"]`) as HTMLElement | null
|
||||
if (node) {
|
||||
node.style.left = x + '%'
|
||||
node.style.top = y + '%'
|
||||
}
|
||||
|
||||
options.updatePositions({
|
||||
...options.getPositions(),
|
||||
[currentDrag.id]: { x, y },
|
||||
})
|
||||
|
||||
if (!currentDrag.raf) {
|
||||
currentDrag.raf = requestAnimationFrame(() => {
|
||||
options.renderEdges()
|
||||
if (drag.value) drag.value.raf = null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function onPointerUp() {
|
||||
if (!drag.value) return
|
||||
|
||||
const currentDrag = drag.value
|
||||
const node = options.flowRef.value?.querySelector(`.node[data-id="${currentDrag.id}"]`) as HTMLElement | null
|
||||
if (node) node.classList.remove('dragging')
|
||||
|
||||
if (!currentDrag.moved) {
|
||||
options.selectAgent(currentDrag.id)
|
||||
} else {
|
||||
suppressClickUntil.value = performance.now() + CLICK_SUPPRESSION_MS
|
||||
}
|
||||
|
||||
drag.value = null
|
||||
}
|
||||
|
||||
function onClickCapture(e: MouseEvent) {
|
||||
if (performance.now() >= suppressClickUntil.value) return
|
||||
if (!findNode(e.target)) return
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
return {
|
||||
onClickCapture,
|
||||
onPointerDown,
|
||||
onPointerMove,
|
||||
onPointerUp,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user