- 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.
268 lines
6.4 KiB
Vue
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> |