Enhance ProcessFlowCanvas with Selection Features and Help Guide

- Added support for node selection changes and multiple node selection events, improving user interaction within the process flow.
- Introduced a collapsible help guide to assist users with selection and action instructions, enhancing usability.
- Updated styles for selection boxes and multi-selected nodes to improve visual feedback during interactions.
- Refactored event handling for node and edge clicks to streamline the selection process and improve error logging.
This commit is contained in:
Md Afiq Iskandar 2025-07-23 07:23:07 +08:00
parent 5261bf601e
commit 80038e00a3

View File

@ -89,6 +89,8 @@ const emit = defineEmits([
"edgesChange",
"nodeSelected",
"edgeSelected",
"selectionChange",
"nodesSelection",
]);
// Get the flow instance
@ -121,8 +123,6 @@ const {
markerEnd: "url(#arrow)",
},
deleteKeyCode: "Delete",
selectionKeyCode: "Shift",
multiSelectionKeyCode: "Control",
connectionMode: "strict",
edgeUpdaterRadius: 12,
edgesUpdatable: true,
@ -130,6 +130,10 @@ const {
isValidConnection: (connection) => {
return true;
},
selectNodesOnDrag: true,
panOnDrag: [1, 2],
panOnScroll: false,
zoomOnScroll: true,
});
// Define custom edge types - use markRaw to prevent reactivity issues
@ -158,18 +162,32 @@ const flowOptions = ref({
nodesDraggable: true,
nodesConnectable: true,
elementsSelectable: true,
selectNodesOnDrag: false,
panOnDrag: [0, 2],
selectNodesOnDrag: true,
panOnDrag: [1, 2],
panOnScroll: false,
zoomOnScroll: true,
zoomOnPinch: true,
zoomOnDoubleClick: false,
connectOnClick: false,
selectionMode: "partial",
selectionKeyCode: true,
multiSelectionKeyCode: true,
selectionOnDrag: true,
selectionBoxMode: "Default",
preventScrolling: true,
});
// Use shallowRef for selected node to avoid unnecessary reactivity
const selectedNode = shallowRef(null);
// Help guide state
const showHelpGuide = ref(false);
// Toggle help guide
const toggleHelpGuide = () => {
showHelpGuide.value = !showHelpGuide.value;
};
// State management for preventing recursive updates
const isUpdatingNodes = ref(false);
const isUpdatingEdges = ref(false);
@ -201,10 +219,23 @@ const onNodeClick = ({ node }) => {
selectedNode.value = nodeData;
emit("nodeSelected", nodeData);
} catch (error) {
console.error("Error processing node data:", error);
console.error("Error processing node click:", error);
}
};
// Handle selection change
const onSelectionChange = ({ nodes, edges }) => {
console.log('Selection changed:', { nodes: nodes.length, edges: edges.length });
// You can add custom logic here for handling multiple selections
};
// Handle multiple node selection
const onNodesSelection = ({ nodes }) => {
console.log('Multiple nodes selected:', nodes.map(n => n.id));
// Emit event for multiple node selection
emit("nodeSelected", nodes);
};
// Handle edge click
const onEdgeClick = (event, edge) => {
// Handle different parameter formats Vue Flow might send
@ -1424,6 +1455,8 @@ function fromObject(flowObject) {
@nodeDoubleClick="onNodeDelete"
@edgeDoubleClick="onEdgeDelete"
@keyup.delete="onDeleteKeyPress"
@selectionChange="onSelectionChange"
@nodesSelection="onNodesSelection"
>
<!-- Global SVG definitions for arrows -->
<svg style="position: absolute; top: 0; left: 0; width: 0; height: 0">
@ -1476,18 +1509,49 @@ function fromObject(flowObject) {
<template #edge-label="{ label }">
<div class="edge-label">{{ label }}</div>
</template>
<Panel position="top-right" class="node-controls">
<div class="p-2 bg-white rounded shadow-sm text-sm">
<div class="mb-1 font-medium">Controls:</div>
<div> Delete: Remove selected</div>
<div> Shift: Select nodes</div>
<div> Drag between nodes to connect</div>
<div> Double-click to remove</div>
<div class="mt-2 mb-1 font-medium">Edge Features:</div>
<div> Arrows show flow direction</div>
<div> Select edge for controls</div>
<div> Blue dot = drag to reposition</div>
<div> Reset button restores path</div>
<!-- Collapsible Help Guide -->
<Panel position="top-right" class="help-guide">
<div class="help-guide-content">
<button
@click="toggleHelpGuide"
class="help-toggle-btn"
:class="{ 'expanded': showHelpGuide }"
>
<Icon name="material-symbols:help-outline" class="w-4 h-4" />
<span v-if="showHelpGuide">Hide Help</span>
<span v-else>Show Help</span>
</button>
<div v-if="showHelpGuide" class="help-content">
<div class="help-section">
<h4 class="help-title">Selection</h4>
<ul class="help-list">
<li> Click: Select single node</li>
<li> Drag: Select multiple nodes</li>
<li> Ctrl+Click: Add to selection</li>
<li> Drag selected nodes together</li>
</ul>
</div>
<div class="help-section">
<h4 class="help-title">Actions</h4>
<ul class="help-list">
<li> Delete: Remove selected</li>
<li> Double-click: Remove node/edge</li>
<li> Drag between nodes: Connect</li>
</ul>
</div>
<div class="help-section">
<h4 class="help-title">Edges</h4>
<ul class="help-list">
<li> Select edge for controls</li>
<li> Blue dot: Reposition</li>
<li> Reset: Restore path</li>
</ul>
</div>
</div>
</div>
</Panel>
</VueFlow>
@ -1691,12 +1755,7 @@ function fromObject(flowObject) {
}
.node-controls {
margin: 10px;
color: #666;
font-size: 12px;
background: white;
border-radius: 4px;
pointer-events: all;
display: none;
}
:deep(.vue-flow__handle) {
@ -1758,4 +1817,126 @@ function fromObject(flowObject) {
0 0 30px rgba(96, 165, 250, 0.6);
}
}
/* Selection box styles */
:deep(.vue-flow__selection) {
background: rgba(59, 130, 246, 0.1);
border: 2px solid #3b82f6;
border-radius: 4px;
pointer-events: none;
}
:deep(.vue-flow__selection-rect) {
fill: rgba(59, 130, 246, 0.1);
stroke: #3b82f6;
stroke-width: 2;
stroke-dasharray: 5, 5;
}
/* Multi-selected nodes styling */
:deep(.vue-flow__node.selected) {
box-shadow: 0 0 0 2px #3b82f6;
z-index: 10;
}
:deep(.vue-flow__node.selected .custom-node) {
box-shadow: 0 0 0 2px #3b82f6, 0 2px 5px rgba(0, 0, 0, 0.1);
}
/* Selection box animation */
@keyframes selectionPulse {
0%, 100% {
opacity: 0.3;
}
50% {
opacity: 0.6;
}
}
:deep(.vue-flow__selection) {
animation: selectionPulse 2s ease-in-out infinite;
}
/* Help Guide Styles */
.help-guide {
pointer-events: all;
}
.help-guide-content {
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
overflow: hidden;
max-width: 280px;
transition: all 0.3s ease;
}
.help-toggle-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #3b82f6;
color: white;
border: none;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.help-toggle-btn:hover {
background: #2563eb;
transform: translateY(-1px);
}
.help-toggle-btn.expanded {
background: #dc2626;
}
.help-toggle-btn.expanded:hover {
background: #b91c1c;
}
.help-content {
padding: 12px;
background: white;
border-top: 1px solid #e5e7eb;
}
.help-section {
margin-bottom: 12px;
}
.help-section:last-child {
margin-bottom: 0;
}
.help-title {
font-size: 11px;
font-weight: 600;
color: #374151;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.help-list {
list-style: none;
padding: 0;
margin: 0;
}
.help-list li {
font-size: 11px;
color: #6b7280;
margin-bottom: 3px;
line-height: 1.3;
}
.help-list li:last-child {
margin-bottom: 0;
}
</style>