Skip to content
Snippets Groups Projects
Commit 40a1c3af authored by Maximilian Giller's avatar Maximilian Giller :squid:
Browse files

First proper internal structure for selecting multiple nodes, deletion already working

parent 2c21530e
No related branches found
No related tags found
No related merge requests found
Pipeline #56941 passed
......@@ -17,38 +17,71 @@ type propTypes = {
spaceId: string;
};
type stateTypes = {
/**
* Graph structure holding the basic information.
*/
graph: Graph;
/**
* Should labels on nodes be rendered, or none at all.
*/
visibleLabels: boolean;
/**
* Should feature be enabled, that nodes get connected with a link of dragged close enough to each other?
*/
connectOnDrag: boolean;
selectedNode: Node;
/**
* Collection of all currently selected nodes. Can also be undefined or empty.
*/
selectedNodes: Node[];
/**
* True for each key, that is currently considered pressed. If key has not been pressed yet, it will not exist as dict-key.
*/
keys: { [name: string]: boolean };
/**
* Current width of graph object. Used to specifically adjust and correct the graph size.
*/
graphWidth: number;
};
/**
* Coordinate structure used for the force-graph.
*/
type graphCoordinates = {
x: number;
y: number;
};
/**
* Easy to access format for translated positions of a click event.
*/
type clickPosition = {
graph: graphCoordinates;
window: graphCoordinates;
};
type positionTranslate = {
x: number;
y: number;
z: number;
};
/**
* Knowledge space graph editor. Allows easy editing of the graph structure.
*/
export class Editor extends React.PureComponent<propTypes, stateTypes> {
private maxDistanceToConnect = 15;
private defaultWarmupTicks = 100;
private warmupTicks = 100;
private renderer: any;
private graphContainer: any;
/**
* True, if the graph was the target of the most recent click event.
*/
private graphInFocus = false;
private selectBoxStart: graphCoordinates = undefined;
constructor(props: propTypes) {
super(props);
// Making sure, all functions retain the proper this-bind
this.loadGraph = this.loadGraph.bind(this);
this.loadSpace = this.loadSpace.bind(this);
this.extractPositions = this.extractPositions.bind(this);
......@@ -63,7 +96,6 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
this.handleLinkCanvasObject = this.handleLinkCanvasObject.bind(this);
this.handleBackgroundClick = this.handleBackgroundClick.bind(this);
this.handleNodeDrag = this.handleNodeDrag.bind(this);
this.handleNodeDragEnd = this.handleNodeDragEnd.bind(this);
this.handleLinkClick = this.handleLinkClick.bind(this);
this.selectNode = this.selectNode.bind(this);
this.handleResize = this.handleResize.bind(this);
......@@ -77,12 +109,15 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
graph: undefined,
visibleLabels: true,
connectOnDrag: false,
selectedNode: undefined,
selectedNodes: undefined,
keys: {},
graphWidth: 1000,
};
}
/**
* Tries to load initial graph after webpage finished loading.
*/
componentDidMount() {
if (this.props.spaceId !== undefined) {
// Load initial space
......@@ -137,6 +172,11 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
return true;
}
/**
* Processes page wide key down events. Stores corresponding key as pressed in state.
*
* Also triggers actions corresponding to shortcuts.
*/
private handleKeyDown(event: KeyboardEvent) {
const key: string = event.key;
......@@ -147,23 +187,62 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
keys: keys,
});
// Key events
this.handleShortcutEvents(key);
}
/**
* Triggers actions that correspond with certain shortcuts.
*
* @param key Newly pressed key.
*/
private handleShortcutEvents(key: string) {
if (key === "Escape") {
// Only delete if 2d-graph is the focused element
this.selectNode(undefined);
} else if (
key === "Delete" &&
this.state.selectedNode !== undefined &&
this.graphInFocus
this.graphInFocus // Only delete if 2d-graph is the focused element
) {
this.state.selectedNode.delete();
this.deleteSelectedNodes();
}
}
private handleMouseDown(event: any) {
/**
* Deletes all nodes currently selected. Handles store points accordingly of the number of deleted nodes.
*/
private deleteSelectedNodes() {
if (this.selectedNodes === undefined) {
return; // Nothing to delete
}
if (this.selectedNodes.length == 1) {
this.selectedNodes[0].delete();
return;
}
// Delete multiple connected nodes
const count: number = this.selectedNodes.length;
try {
// Disable storing temporarily to create just one big change.
this.state.graph.disableStoring();
this.selectedNodes.forEach((node: Node) => node.delete());
} finally {
this.state.graph.enableStoring();
this.state.graph.storeCurrentData(
"Deleted " + count + " nodes and all connected links"
);
}
}
/**
* Processes page wide mouse down events.
*/
private handleMouseDown() {
this.graphInFocus = false;
}
/**
* Processes page wide key up events. Stores corresponding key as not-pressed in state.
*/
private handleKeyUp(event: KeyboardEvent) {
const key: string = event.key;
......@@ -175,6 +254,9 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
});
}
/**
* Processes resize window event. Focusses on resizing the graph accordingly.
*/
private handleResize() {
const newGraphWidth = this.graphContainer.current.clientWidth;
this.setState({
......@@ -202,12 +284,6 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
return;
}
// Just deselect if shift key is pressed
if (this.state.keys["Shift"] && !this.selectBoxStart) {
this.selectBoxStart = position.graph;
return;
}
// Just deselect if control key is pressed
if (this.state.keys["Control"]) {
this.selectNode(undefined);
......@@ -231,13 +307,16 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
* Propagates the changed state of the graph.
*/
private onHistoryChange() {
if (this.state.selectedNode === undefined) {
if (this.selectedNodes === undefined) {
this.selectNode(undefined);
} else {
this.selectNode(
this.state.graph.getNode(this.state.selectedNode.id)
);
this.forceUpdate();
return;
}
const nodes: Node[] = this.selectedNodes.map((node: Node) =>
this.state.graph.getNode(node.id)
);
this.selectNodes(nodes);
this.forceUpdate();
}
......@@ -247,18 +326,20 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
* @returns True, if element should be highlighted.
*/
private isHighlighted(element: GraphElement): boolean {
if (this.state.selectedNode == undefined || element == undefined) {
if (this.selectedNodes == undefined || element == undefined) {
// Default to false if nothing selected.
return false;
}
if (element.node) {
// Is node
return element.equals(this.state.selectedNode);
// Is one of nodes
return this.selectedNodes.includes(element as Node);
} else if (element.link) {
// Is link
// Is it one of the adjacent links?
const found = this.state.selectedNode.links.find(element.equals);
const found = this.selectedNodes.find((node: Node) =>
node.links.find(element.equals)
);
return found !== undefined;
} else {
return false;
......@@ -280,26 +361,71 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
};
}
/**
* Selects a single node, or clears selection if given undefined.
* @param node Single node to select, or undefined.
*/
private selectNode(node: Node) {
if (node === undefined) {
this.setState({
selectedNodes: undefined,
});
return;
}
this.setState({
selectedNode: node,
selectedNodes: [node],
});
}
/**
* Selects multiple nodes, or clears selection if given undefined or empty array.
* @param nodes Multiple nodes to mark as selected.
*/
private selectNodes(nodes: Node[]) {
if (nodes.length <= 0) {
this.setState(undefined);
return;
}
this.setState({
selectedNodes: nodes,
});
}
/**
* Makes sure to always offer a valid format of the selected nodes. Is either undefined or contains at least one valid node. An empty array is never returned.
*/
private get selectedNodes(): Node[] {
if (this.state.selectedNodes === undefined) {
return undefined;
}
// Remove undefines
const selectedNodes = this.state.selectedNodes.filter(
(n: Node) => n !== undefined
);
if (selectedNodes.length > 0) {
return selectedNodes;
}
return undefined;
}
private handleNodeClick(node: Node) {
this.graphInFocus = true;
if (this.state.keys["Shift"]) {
// Connect two nodes when second select while shift is pressed
if (this.state.selectedNode == undefined) {
// Connect to clicked node as parent while shift is pressed
if (this.selectedNodes == undefined) {
// Have no node connected, so select
this.selectNode(node);
} else if (!this.state.selectedNode.equals(node)) {
const selected = this.state.selectedNode;
// Already have *other* node selected, so connect
this.state.selectedNode.connect(node);
// Re-select original node for easier workflow
this.selectNode(selected);
} else if (!this.selectedNodes.includes(node)) {
// Already have *other* node/s selected, so connect
this.selectedNodes.forEach((selectedNode: Node) =>
node.connect(selectedNode)
);
}
} else if (this.state.keys["Control"]) {
// Delete node when control is pressed
......@@ -357,10 +483,17 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
}
// Draw label
/**
* Nothing selected? => Draw all labels
* If this nodes is considered highlighted => Draw label
* If this node is a neighbor of a selected node => Draw label
*/
const isNodeRelatedToSelection: boolean =
this.state.selectedNode === undefined ||
this.selectedNodes === undefined ||
this.isHighlighted(node) ||
this.state.selectedNode.neighbors.includes(node);
!!this.selectedNodes.find((selectedNode: Node) =>
selectedNode.neighbors.includes(node)
);
if (this.state.visibleLabels && isNodeRelatedToSelection) {
const label = node.name;
......@@ -426,7 +559,7 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
return undefined;
}
private handleNodeDrag(node: Node, translate: positionTranslate) {
private handleNodeDrag(node: Node) {
this.graphInFocus = true;
this.selectNode(node);
......@@ -452,10 +585,6 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
this.forceUpdate();
}
private handleNodeDragEnd(node: Node, translate: positionTranslate) {
return;
}
private handleLinkClick(link: Link) {
this.graphInFocus = true;
......@@ -479,7 +608,11 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
}
private handleBoxSelect(selectedNodes: Node[]) {
console.log(selectedNodes);
if (selectedNodes !== undefined && selectedNodes.length <= 0) {
return;
}
this.selectNodes(selectedNodes);
}
render(): React.ReactNode {
......@@ -525,7 +658,6 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
linkCanvasObjectMode={() => "replace"}
nodeColor={(node: Node) => node.type.color}
onNodeDrag={this.handleNodeDrag}
onNodeDragEnd={this.handleNodeDragEnd}
onLinkClick={this.handleLinkClick}
onBackgroundClick={(event: any) =>
this.handleBackgroundClick(
......@@ -545,7 +677,11 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
/>
<hr />
<NodeDetails
selectedNode={this.state.selectedNode}
selectedNode={
this.selectedNodes
? this.selectedNodes[0]
: undefined
}
allTypes={
this.state.graph ? this.state.graph.types : []
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment