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

285 lines
6.9 KiB
Vue

<template>
<g>
<!-- Main edge path -->
<BaseEdge
:id="id"
:path="edgePath"
:style="{
stroke: selected ? '#ff6b6b' : edgeStyle.stroke || '#555',
strokeWidth: selected ? 3 : edgeStyle.strokeWidth || 2,
...edgeStyle
}"
:marker-end="selected ? 'url(#arrow-selected)' : 'url(#arrow)'"
:class="{
'vue-flow__edge-path': true,
'custom-edge': true,
'selected': selected,
'animated': animated
}"
/>
<!-- Interactive control points for path adjustment -->
<g v-if="selected && showControlPoints" class="edge-control-points">
<!-- Mid-point control for path bending -->
<circle
:cx="labelX"
:cy="labelY"
r="4"
fill="#3b82f6"
stroke="white"
stroke-width="2"
class="edge-control-point"
style="cursor: move;"
@mousedown="onControlPointMouseDown"
@touchstart="onControlPointMouseDown"
/>
<!-- Visual indicator for draggable control -->
<circle
:cx="labelX"
:cy="labelY"
r="8"
fill="rgba(59, 130, 246, 0.2)"
class="edge-control-indicator"
style="pointer-events: none;"
/>
</g>
<!-- Edge label using EdgeLabelRenderer for HTML content -->
<EdgeLabelRenderer v-if="label">
<div
:style="{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
pointerEvents: 'all',
fontSize: '12px',
fontWeight: '500'
}"
class="edge-label-container nodrag nopan"
>
<div class="edge-label">
{{ label }}
</div>
</div>
</EdgeLabelRenderer>
</g>
</template>
<script setup>
import { computed, ref, inject } from 'vue'
import { BaseEdge, EdgeLabelRenderer, getBezierPath, getSmoothStepPath, getStraightPath } from '@vue-flow/core'
import { useEdge } from '@vue-flow/core'
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,
type: {
type: String,
default: 'smoothstep'
},
data: Object,
markerEnd: String,
markerStart: String
})
// Get edge instance for updating
const { edge } = useEdge()
// Control state
const showControlPoints = ref(true)
const isDragging = ref(false)
const pathOffset = ref({ x: 0, y: 0 })
// Computed edge style
const edgeStyle = computed(() => ({
stroke: '#555',
strokeWidth: 2,
...props.style
}))
// Calculate edge path based on type with optional offset for custom positioning
const edgePath = computed(() => {
try {
const offsetX = pathOffset.value.x || 0
const offsetY = pathOffset.value.y || 0
// Apply offset to create custom path curvature
const centerX = (props.sourceX + props.targetX) / 2 + offsetX
const centerY = (props.sourceY + props.targetY) / 2 + offsetY
switch (props.type) {
case 'straight':
return getStraightPath(props.sourceX, props.sourceY, props.targetX, props.targetY)[0]
case 'smoothstep':
case 'custom':
return getSmoothStepPath(
props.sourceX,
props.sourceY,
props.sourcePosition,
props.targetX,
props.targetY,
props.targetPosition,
centerX,
centerY,
20, // offset
8 // border radius
)[0]
case 'bezier':
default:
return getBezierPath({
sourceX: props.sourceX,
sourceY: props.sourceY,
sourcePosition: props.sourcePosition,
targetX: props.targetX,
targetY: props.targetY,
targetPosition: props.targetPosition,
curvature: 0.25
})[0]
}
} catch (error) {
console.error('Error calculating edge path:', error)
// Fallback to a simple straight line
return `M ${props.sourceX} ${props.sourceY} L ${props.targetX} ${props.targetY}`
}
})
// Calculate label position
const labelX = computed(() => {
return (props.sourceX + props.targetX) / 2 + pathOffset.value.x
})
const labelY = computed(() => {
return (props.sourceY + props.targetY) / 2 + pathOffset.value.y
})
// Control point drag handling
const onControlPointMouseDown = (event) => {
event.preventDefault()
event.stopPropagation()
isDragging.value = true
const startX = event.clientX || event.touches[0].clientX
const startY = event.clientY || event.touches[0].clientY
const startOffsetX = pathOffset.value.x
const startOffsetY = pathOffset.value.y
const onMouseMove = (moveEvent) => {
if (!isDragging.value) return
const currentX = moveEvent.clientX || moveEvent.touches[0].clientX
const currentY = moveEvent.clientY || moveEvent.touches[0].clientY
const deltaX = currentX - startX
const deltaY = currentY - startY
// Update path offset for custom positioning
pathOffset.value = {
x: startOffsetX + deltaX,
y: startOffsetY + deltaY
}
// Store the custom path in edge data for persistence
if (edge.value) {
edge.value.data = {
...edge.value.data,
customPath: pathOffset.value
}
}
}
const onMouseUp = () => {
isDragging.value = false
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
document.removeEventListener('touchmove', onMouseMove)
document.removeEventListener('touchend', onMouseUp)
}
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
document.addEventListener('touchmove', onMouseMove)
document.addEventListener('touchend', onMouseUp)
}
// Initialize path offset from saved data
try {
if (edge.value && edge.value.data && edge.value.data.customPath) {
pathOffset.value = edge.value.data.customPath
}
} catch (error) {
console.log('Edge data not available yet, using default path')
}
</script>
<script>
export default {
name: 'CustomEdge',
inheritAttrs: false
}
</script>
<style scoped>
.edge-label-container {
z-index: 1000;
}
.edge-label {
background: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border: 1px solid #e2e8f0;
white-space: nowrap;
pointer-events: none;
}
.edge-control-point {
cursor: move;
transition: r 0.2s ease;
}
.edge-control-point:hover {
r: 5;
fill: #2563eb;
}
.custom-edge {
transition: stroke-width 0.2s ease, stroke 0.2s ease;
}
.custom-edge.animated {
stroke-dasharray: 5;
animation: dash 1s linear infinite;
}
.custom-edge.selected {
filter: drop-shadow(0 0 4px rgba(255, 107, 107, 0.5));
}
@keyframes dash {
to {
stroke-dashoffset: -10;
}
}
/* Hide control points when not selected */
.edge-control-points {
opacity: 0;
transition: opacity 0.2s ease;
}
.selected .edge-control-points {
opacity: 1;
}
</style>