Skip to content
Snippets Groups Projects
editor.tsx 10.1 KiB
Newer Older
import React from "react";
import { DynamicGraph } from "./graph";
import { listAllSpaces, loadGraphJson } from "../common/datasets";
import SpaceSelect from "./components/spaceselect";
import "./editor.css";
import * as Helpers from "../common/helpers";
import { Node, NodeProperties } from "../common/graph/node";
import { SpaceManager } from "./components/spacemanager";
import { SelectLayer } from "./components/selectlayer";
import { GraphData } from "../common/graph/graph";
import { NodeType } from "../common/graph/nodetype";
import { GraphRenderer2D } from "./renderer";
import * as Config from "../config";
import Sidepanel from "./components/sidepanel";
export interface NodeDataChangeRequest extends NodeProperties {
    id: number;
    type: NodeType;
}

type stateTypes = {
    /**
     * Graph structure holding the basic information.
     */

    /**
     * Should labels on nodes be rendered, or none at all.
     */

    /**
     * Should feature be enabled, that nodes get connected with a link of dragged close enough to each other?
     */
     * Current width of graph object. Used to specifically adjust and correct the graph size.

    /**
     * True for each key, that is currently considered pressed. If key has not been pressed yet, it will not exist as dict-key.
     */
     * Collection of all currently selected nodes. Can also be undefined or empty.

    spaces: string[];

    spaceId: string;
/**
 * Knowledge space graph editor. Allows easy editing of the graph structure.
 */
export class Editor extends React.PureComponent<any, stateTypes> {
    private graphContainer: React.RefObject<HTMLDivElement>;
    private rendererRef: React.RefObject<GraphRenderer2D>;
    constructor(props: any) {

        // Making sure, all functions retain the proper this-bind
        this.loadGraph = this.loadGraph.bind(this);
        this.loadSpace = this.loadSpace.bind(this);
        this.forceUpdate = this.forceUpdate.bind(this);
        this.handleNodeTypeSelect = this.handleNodeTypeSelect.bind(this);
        this.handleBoxSelect = this.handleBoxSelect.bind(this);
        this.selectNodes = this.selectNodes.bind(this);
        this.handleNodeDataChange = this.handleNodeDataChange.bind(this);

        document.addEventListener("keydown", (e) => {
            this.keyPressed(e.key);
        });
        document.addEventListener("keyup", (e) => {
            this.keyReleased(e.key);
        });
        this.graphContainer = React.createRef();
        listAllSpaces().then((spaces) => this.setState({ spaces: spaces }));

        // Set as new state
        this.state = {
            graph: undefined,
            connectOnDrag: false,
            spaces: [],
            spaceId: Config.SPACE,
    keyPressed(key: string) {
        const keys = this.state.keys;
        keys[key] = true;
        this.setState({ keys: keys });
    }

    keyReleased(key: string) {
        const keys = this.state.keys;
        keys[key] = false;
        this.setState({ keys: keys });
    }

    /**
     * Tries to load initial graph after webpage finished loading.
     */
    componentDidMount() {
        if (this.state.spaceId !== undefined) {
            // Load initial space
            this.loadSpace(this.state.spaceId);
    }

    /**
     * Loads a space from the database to the editor.
     * @param spaceId Id of space to load.
     * @returns Promise with boolean value that is true, if successful.
     */
    public loadSpace(spaceId: string): any {
        return loadGraphJson(spaceId).then(this.loadGraph);
    }

    /**
     * Loads another graph based on the data supplied. Note: Naming currently suggests that this only loads a GRAPH, not a SPACE. Needs further work and implementation to see if that makes sense or not.
     * @param data Serialized graph data.
     * @returns True, if successful.
     */
    public loadGraph(data: GraphData): boolean {
        console.log("Starting to load new graph ...");
        console.log(data);
        const graph = new DynamicGraph();
        graph.fromSerializedObject(data);
        //graph.onChangeCallbacks.push(this.onGraphDataChange);
        // Subscribe to global events
        window.addEventListener("resize", () => this.handleResize());
    /**
     * Processes resize window event. Focusses on resizing the graph accordingly.
     */
        const newGraphWidth = Helpers.getClientWidth("knowledge-space-editor");
        this.setState({
            graphWidth: newGraphWidth,
        });
    }

    // /**
    //  * 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[] {
    //     // TODO: Here are a lot of things that should not be possible by design
    //
    //     // Remove undefines
    //     let selectedNodes = this.state.selectedNodes.filter(
    //         (n: Node) => n !== undefined
    //     );
    //
    //     // Remove duplicates
    //     selectedNodes = [...new Set(selectedNodes)];
    //
    //     if (selectedNodes.length > 0) {
    //         return selectedNodes;
    //     }
    //
    //     return undefined;
    // }
    handleBoxSelect(selectedNodes: Node[]) {
        if (selectedNodes !== undefined && selectedNodes.length <= 0) {
        this.selectNodes(selectedNodes.concat(this.state.selectedNodes));
    /**
     * Selects multiple nodes, or clears selection if given undefined or empty array.
     * @param nodes Multiple nodes to mark as selected.
     */
    public selectNodes(nodes: Node[]) {
        this.setState({
            selectedNodes: nodes,
        });
    private handleNodeTypeSelect(type: NodeType) {
        const nodesWithType = this.state.graph.nodes.filter((n: Node) =>
            n.type.equals(type)
        );
        this.selectNodes(nodesWithType);
    }

    private handleNodeDataChange(nodeData: NodeDataChangeRequest[]) {
        // Make a shallow copy of the graph object to trigger an update over setState
        const graph = Object.assign(new DynamicGraph(), this.state.graph);

        // Modify node
        for (const request of nodeData) {
            const node = graph.node(request.id);
            Object.assign(node, request);
        }

        // Push shallow copy to state
        this.setState({ graph: graph });
    }
    render(): React.ReactNode {
        return (
            <div id="ks-editor">
                <h1>Interface</h1>
                <SpaceSelect
                    onLoadSpace={this.loadSpace}
                    spaces={this.state.spaces}
                    spaceId={this.state.spaceId}
                />
                {this.state.graph && (
                    <div id="content">
                        <div
                            id="force-graph-renderer"
                            ref={this.graphContainer}
                            <SelectLayer
                                allNodes={
                                    this.state.graph
                                        ? this.state.graph.nodes
                                        : []
                                }
                                screen2GraphCoords={
                                    this.rendererRef
                                        ? this.rendererRef.current
                                              .screen2GraphCoords
                                        : undefined
                                }
                                isEnabled={this.state.keys["Shift"]}
                                onBoxSelect={this.handleBoxSelect}
                            >
                                <GraphRenderer2D
                                    ref={this.rendererRef}
                                    graph={this.state.graph}
Maximilian Giller's avatar
Maximilian Giller committed
                                    width={this.state.graphWidth}
                                    onNodeSelectionChanged={this.selectNodes}
                                    selectedNodes={this.state.selectedNodes}
Maximilian Giller's avatar
Maximilian Giller committed
                                />

                        <Sidepanel
                            graph={this.state.graph}
                            onCheckpointLoad={(checkpoint) => {
                                const graph = new DynamicGraph();
                                this.setState({
                                    graph: graph.fromSerializedObject(
                                        checkpoint.data
                                    ),
                                });
                            }}
                            onNodeTypeSelect={this.handleNodeTypeSelect}
                            onConnectOnDragChange={(connectOnDrag) =>
                                this.setState({
                                    connectOnDrag: connectOnDrag,
                                })
                            }
                            onLabelVisibilityChange={(visible) =>
                                this.setState({
                                    visibleLabels: visible,
                                })
                            }
                            selectedNodes={this.state.selectedNodes}
                            visibleLabels={this.state.visibleLabels}
                            connectOnDrag={this.state.connectOnDrag}
                            onNodeDataChange={this.handleNodeDataChange}
                        />