feat: added basic functionality for editable name bar

This commit is contained in:
Gerla, J. (Justin)
2025-11-11 13:50:45 +00:00
parent 04818f48d4
commit d4d1aecb8c
6 changed files with 298 additions and 79 deletions

View File

@@ -6,7 +6,7 @@ import {
reconnectEdge, type Edge, type Connection
} from '@xyflow/react';
import {type FlowState} from './VisProgTypes.tsx';
import {type AppNode, type FlowState} from './VisProgTypes.tsx';
/**
* contains the nodes that are created when the editor is loaded,
@@ -55,61 +55,87 @@ const initialEdges = [
* and use any implemented functionality
*/
const useFlowStore = create<FlowState>((set, get) => ({
nodes: initialNodes,
edges: initialEdges,
edgeReconnectSuccessful: true,
onNodesChange: (changes) => {
nodes: initialNodes,
edges: initialEdges,
edgeReconnectSuccessful: true,
onNodesChange: (changes) => {
set({
nodes: applyNodeChanges(changes, get().nodes)
});
},
onEdgesChange: (changes) => {
set({
edges: applyEdgeChanges(changes, get().edges)
});
},
// handles connection of newly created edges
onConnect: (connection) => {
set({
edges: addEdge(connection, get().edges)
});
},
// handles attempted reconnections of a previously disconnected edge
onReconnect: (oldEdge: Edge, newConnection: Connection) => {
get().edgeReconnectSuccessful = true;
set({
edges: reconnectEdge(oldEdge, newConnection, get().edges)
});
},
// Handles initiation of reconnection of edges that are manually disconnected from a node
onReconnectStart: () => {
set({
edgeReconnectSuccessful: false
});
},
// Drops the edge from the set of edges, removing it from the flow, if no successful reconnection occurred
onReconnectEnd: (_: unknown, edge: { id: string; }) => {
if (!get().edgeReconnectSuccessful) {
set({
nodes: applyNodeChanges(changes, get().nodes)
edges: get().edges.filter((e) => e.id !== edge.id),
});
},
onEdgesChange: (changes) => {
set({
edges: applyEdgeChanges(changes, get().edges)
});
},
// handles connection of newly created edges
onConnect: (connection) => {
set({
edges: addEdge(connection, get().edges)
});
},
// handles attempted reconnections of a previously disconnected edge
onReconnect: (oldEdge: Edge, newConnection: Connection) => {
get().edgeReconnectSuccessful = true;
set({
edges: reconnectEdge(oldEdge, newConnection, get().edges)
});
},
// Handles initiation of reconnection of edges that are manually disconnected from a node
onReconnectStart: () => {
set({
edgeReconnectSuccessful: false
});
},
// Drops the edge from the set of edges, removing it from the flow, if no successful reconnection occurred
onReconnectEnd: (_: unknown, edge: { id: string; }) => {
if (!get().edgeReconnectSuccessful) {
set({
edges: get().edges.filter((e) => e.id !== edge.id),
});
}
set({
edgeReconnectSuccessful: true
});
},
deleteNode: (nodeId: string) => {
set({
nodes: get().nodes.filter((n) => n.id !== nodeId),
edges: get().edges.filter((e) => e.source !== nodeId && e.target !== nodeId)
});
},
setNodes: (nodes) => {
set({nodes});
},
setEdges: (edges) => {
set({edges});
},
}
set({
edgeReconnectSuccessful: true
});
},
deleteNode: (nodeId: string) => {
set({
nodes: get().nodes.filter((n) => n.id !== nodeId),
edges: get().edges.filter((e) => e.source !== nodeId && e.target !== nodeId)
});
},
setNodes: (nodes) => {
set({nodes});
},
setEdges: (edges) => {
set({edges});
},
/**
* handles updating the data component of a node,
* if the provided data object contains entries that aren't present in the updated node's data component
* those entries are added to the data component,
* entries that do exist within the node's data component,
* are simply updated to contain the new value
*
* the data object
* @param {string} nodeId
* @param {object} data
*/
updateNodeData: (nodeId: string, data) => {
set({
nodes: get().nodes.map((node) : AppNode => {
if (node.id === nodeId) {
return {
...node,
data: {
...node.data,
...data
}
};
} else { return node; }
})
});
}
}),
);

View File

@@ -43,4 +43,5 @@ export type FlowState = {
deleteNode: (nodeId: string) => void;
setNodes: (nodes: AppNode[]) => void;
setEdges: (edges: Edge[]) => void;
updateNodeData: (nodeId: string, data: object) => void;
};

View File

@@ -1,4 +1,9 @@
import {Handle, type NodeProps, NodeToolbar, Position} from '@xyflow/react';
import {
Handle,
type NodeProps,
NodeToolbar,
Position
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import styles from '../../VisProg.module.css';
import useFlowStore from "../VisProgStores.tsx";
@@ -9,7 +14,7 @@ import type {
NormNode
} from "../VisProgTypes.tsx";
//
//Toolbar definitions
type ToolbarProps = {
nodeId: string;
@@ -39,6 +44,56 @@ export function Toolbar({nodeId, allowDelete}: ToolbarProps) {
</NodeToolbar>);
}
// Renaming component
/**
* Adds a component that can be used to edit a node's label entry inside its Data
* can be added to any custom node that has a label inside its Data
*
* @param {string} nodeLabel
* @param {string} nodeId
* @returns {React.JSX.Element}
* @constructor
*/
export function EditableName({nodeLabel = "new node", nodeId} : { nodeLabel : string, nodeId: string}) {
const {updateNodeData} = useFlowStore();
const updateData = (event: React.FocusEvent<HTMLInputElement>) => {
const input = event.target.value;
updateNodeData(nodeId, {label: input});
event.currentTarget.setAttribute("readOnly", "true");
window.getSelection()?.empty();
event.currentTarget.classList.replace("nodrag", "drag"); // enable dragging of the node with cursor on the input box
};
const updateOnEnter = (event: React.KeyboardEvent<HTMLInputElement>) => { if (event.key === "Enter") (event.target as HTMLInputElement).blur(); };
const enableEditing = (event: React.MouseEvent<HTMLInputElement>) => {
if(event.currentTarget.hasAttribute("readOnly")) {
event.currentTarget.removeAttribute("readOnly"); // enable editing
event.currentTarget.select(); // select the text input
window.getSelection()?.collapseToEnd(); // move the caret to the end of the current value
event.currentTarget.classList.replace("drag", "nodrag"); // disable dragging using input box
}
}
return (
<div className={styles.NodeTextBar }>
<label>name: </label>
<input
className={`drag ${styles.nodeTextInput}`} // prevents dragging the component when user has focused the text input
type={"text"}
defaultValue={nodeLabel}
onKeyDown={updateOnEnter}
onBlur={updateData}
onClick={enableEditing}
maxLength={25}
readOnly
/>
</div>
)
}
// Definitions of Nodes
@@ -54,7 +109,7 @@ export const StartNodeComponent = ({id, data}: NodeProps<StartNode>) => {
return (
<>
<Toolbar nodeId={id} allowDelete={false}/>
<div className={styles.defaultNodeStart}>
<div className={`${styles.defaultNode} ${styles.nodeStart}`}>
<div> data test {data.label} </div>
<Handle type="source" position={Position.Right} id="start"/>
</div>
@@ -75,7 +130,7 @@ export const EndNodeComponent = ({id, data}: NodeProps<EndNode>) => {
return (
<>
<Toolbar nodeId={id} allowDelete={false}/>
<div className={styles.defaultNodeEnd}>
<div className={`${styles.defaultNode} ${styles.nodeEnd}`}>
<div> {data.label} </div>
<Handle type="target" position={Position.Left} id="end"/>
</div>
@@ -96,8 +151,8 @@ export const PhaseNodeComponent = ({id, data}: NodeProps<PhaseNode>) => {
return (
<>
<Toolbar nodeId={id} allowDelete={true}/>
<div className={styles.defaultNodePhase}>
<div> phase {data.number} {data.label} </div>
<div className={`${styles.defaultNode} ${styles.nodePhase}`}>
<EditableName nodeLabel={data.label} nodeId={id}/>
<Handle type="target" position={Position.Left} id="target"/>
<Handle type="target" position={Position.Bottom} id="norms"/>
<Handle type="source" position={Position.Right} id="source"/>
@@ -119,8 +174,8 @@ export const NormNodeComponent = ({id, data}: NodeProps<NormNode>) => {
return (
<>
<Toolbar nodeId={id} allowDelete={true}/>
<div className={styles.defaultNodeNorm}>
<div> Norm {data.label} </div>
<div className={`${styles.defaultNode} ${styles.nodeNorm}`}>
<EditableName nodeLabel={data.label} nodeId={id}/>
<Handle type="source" position={Position.Right} id="NormSource"/>
</div>
</>