corrad-bp/components/process-flow/InteractiveArrowEdge.vue
Md Afiq Iskandar 8919ac819c Add Interactive Edges and Edge Configuration to Process Flow
- Introduced new components: ArrowEdge, CustomEdge, InteractiveArrowEdge, and EdgeConfiguration for enhanced edge management in the process flow.
- Implemented dynamic edge paths with customizable styles, labels, and animations, improving visual representation and user interaction.
- Enhanced ProcessFlowCanvas to support new edge types and configurations, allowing for more flexible process designs.
- Updated ProcessFlowNodes to include new edge components, ensuring seamless integration with existing node functionalities.
- Improved user experience by providing configuration options for edges, including animation and style settings, directly within the process builder.
2025-07-11 14:13:42 +08:00

268 lines
6.4 KiB
Vue

<script setup>
import { BaseEdge, EdgeLabelRenderer, getSmoothStepPath, useVueFlow } from '@vue-flow/core'
import { computed, ref, watch } from 'vue'
const props = defineProps({
id: String,
sourceX: Number,
sourceY: Number,
targetX: Number,
targetY: Number,
sourcePosition: String,
targetPosition: String,
selected: Boolean,
animated: Boolean,
label: String,
style: Object,
markerEnd: String,
data: Object
})
const { updateEdgeData } = useVueFlow()
// Custom path offset for manual positioning
const pathOffset = ref({ x: 0, y: 0 })
// Initialize from saved data
if (props.data?.pathOffset) {
pathOffset.value = props.data.pathOffset
}
// Calculate edge path with custom offset
const path = computed(() => {
try {
// Apply the offset to create a custom control point
const centerX = (props.sourceX + props.targetX) / 2 + pathOffset.value.x
const centerY = (props.sourceY + props.targetY) / 2 + pathOffset.value.y
const [edgePath, labelX, labelY] = getSmoothStepPath({
sourceX: props.sourceX,
sourceY: props.sourceY,
sourcePosition: props.sourcePosition,
targetX: props.targetX,
targetY: props.targetY,
targetPosition: props.targetPosition,
borderRadius: 8,
offset: 20,
// Override center point with our custom position
centerX,
centerY
})
return {
path: edgePath,
labelX: labelX || centerX,
labelY: labelY || centerY,
controlX: centerX,
controlY: centerY
}
} catch (error) {
// Fallback to simple calculation
const centerX = (props.sourceX + props.targetX) / 2 + pathOffset.value.x
const centerY = (props.sourceY + props.targetY) / 2 + pathOffset.value.y
return {
path: `M ${props.sourceX} ${props.sourceY} Q ${centerX} ${centerY} ${props.targetX} ${props.targetY}`,
labelX: centerX,
labelY: centerY,
controlX: centerX,
controlY: centerY
}
}
})
const edgeStyle = computed(() => ({
stroke: props.selected ? '#ff6b6b' : '#555',
strokeWidth: props.selected ? 3 : 2,
strokeDasharray: props.animated ? '5,5' : 'none',
...props.style
}))
const markerEndUrl = computed(() => {
return props.selected ? 'url(#arrow-selected)' : 'url(#arrow)'
})
// Drag handling for control point
const isDragging = ref(false)
const onControlPointMouseDown = (event) => {
event.preventDefault()
event.stopPropagation()
isDragging.value = true
const startX = event.clientX
const startY = event.clientY
const startOffsetX = pathOffset.value.x
const startOffsetY = pathOffset.value.y
const onMouseMove = (moveEvent) => {
if (!isDragging.value) return
const deltaX = moveEvent.clientX - startX
const deltaY = moveEvent.clientY - startY
// Apply zoom factor (assuming 1.0 for now, can be enhanced)
pathOffset.value = {
x: startOffsetX + deltaX,
y: startOffsetY + deltaY
}
}
const onMouseUp = () => {
isDragging.value = false
// Save the new path offset to edge data
if (props.id && updateEdgeData) {
updateEdgeData(props.id, {
...props.data,
pathOffset: pathOffset.value
})
}
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
}
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
}
// Reset path offset
const resetPath = () => {
pathOffset.value = { x: 0, y: 0 }
if (props.id && updateEdgeData) {
updateEdgeData(props.id, {
...props.data,
pathOffset: undefined
})
}
}
</script>
<script>
export default {
inheritAttrs: false,
}
</script>
<template>
<!-- Main edge path -->
<BaseEdge
:path="path.path"
:style="edgeStyle"
:marker-end="markerEndUrl"
:class="{
'animated': animated,
'selected': selected
}"
/>
<!-- Control point for edge repositioning (only when selected) -->
<g v-if="selected" class="edge-controls">
<!-- Control point circle -->
<circle
:cx="path.controlX"
:cy="path.controlY"
r="4"
fill="#3b82f6"
stroke="white"
stroke-width="2"
class="edge-control-point"
style="cursor: move;"
@mousedown="onControlPointMouseDown"
/>
<!-- Larger invisible area for easier clicking -->
<circle
:cx="path.controlX"
:cy="path.controlY"
r="12"
fill="transparent"
class="edge-control-area"
style="cursor: move;"
@mousedown="onControlPointMouseDown"
/>
<!-- Visual indicator for draggable control -->
<circle
:cx="path.controlX"
:cy="path.controlY"
r="8"
fill="rgba(59, 130, 246, 0.2)"
class="edge-control-indicator"
style="pointer-events: none;"
/>
</g>
<!-- Edge label if provided -->
<EdgeLabelRenderer v-if="label">
<div
:style="{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${path.labelX}px, ${path.labelY}px)`,
pointerEvents: 'all',
fontSize: '11px',
fontWeight: '600',
background: 'white',
padding: '3px 8px',
borderRadius: '12px',
border: '1.5px solid #3b82f6',
boxShadow: '0 2px 8px rgba(59, 130, 246, 0.2)',
color: '#1e40af',
whiteSpace: 'nowrap',
zIndex: 10
}"
class="edge-label nodrag nopan"
>
{{ label }}
</div>
</EdgeLabelRenderer>
<!-- Reset button for custom positioning -->
<EdgeLabelRenderer v-if="selected && (pathOffset.x !== 0 || pathOffset.y !== 0)">
<div
:style="{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${path.controlX + 20}px, ${path.controlY - 20}px)`,
pointerEvents: 'all'
}"
class="edge-reset-button nodrag nopan"
>
<button
@click="resetPath"
style="
background: #ef4444;
color: white;
border: none;
border-radius: 3px;
padding: 2px 6px;
font-size: 10px;
cursor: pointer;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
"
title="Reset edge path"
>
Reset
</button>
</div>
</EdgeLabelRenderer>
</template>
<style scoped>
.edge-control-point {
transition: r 0.2s ease;
}
.edge-control-point:hover {
r: 5;
}
.animated {
animation: dash 2s linear infinite;
}
@keyframes dash {
to {
stroke-dashoffset: -10;
}
}
</style>