import React from "react"; import { State } from "../state"; import * as Interactions from "../interactions"; import { Graph, GraphData } from "../structures/graph/graph"; import { loadGraphJson } from "../../../datasets"; import { ToolPool } from "./toolpool"; import { ToolDetails } from "./tooldetails"; import { SpaceSelect } from "./spaceselect"; import "./editor.css"; import ReactForceGraph2d from "react-force-graph-2d"; import { Node } from "../structures/graph/node"; import { HistoryNavigator } from "./historynavigator"; type propTypes = any; type stateTypes = { state: State; graph: Graph; renderer: any; }; export class Editor extends React.PureComponent<propTypes, stateTypes> { // TODO: Not a long term solution! public static globalState: State; public static globalGraph: Graph; public static globalRenderer: any; constructor(props: propTypes) { super(props); this.loadGraph = this.loadGraph.bind(this); this.loadSpace = this.loadSpace.bind(this); this.handleNodeClick = this.handleNodeClick.bind(this); this.onHistoryChange = this.onHistoryChange.bind(this); // Set as new state this.state = { state: undefined, graph: undefined, renderer: undefined, }; Interactions.initInteractions(); // Load initial space this.loadSpace("space"); } /** * 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: any): boolean { console.log("Starting to load new graph ..."); console.log(data); // Create global objects const newState = new State(); const newGraph = Graph.parse(data); Editor.globalState = newState; Editor.globalGraph = newGraph; // Is valid and parsed successfully? // TODO: Check doesn't work for some reason, always returns true, even tho it should be considered defined // if (Editor.globalGraph == undefined) { // Editor.globalState = this.state.state; // Editor.globalGraph = this.state.graph; // return false; // } // Create renderer // console.log("Creating renderer ..."); // const renderTarget = document.getElementById("2d-graph"); // const renderWidth = renderTarget.offsetWidth; // Editor.globalRenderer = ForceGraph()(renderTarget); // // Subscribe to interactions // console.log("Subscribing to events ..."); // Editor.globalRenderer // .height(600) // .width(renderWidth) // .graphData(Editor.globalGraph.data) // .nodeLabel("label") // .linkColor((link: any) => Editor.globalState.linkColor(link)) // .nodeColor((node: any) => Editor.globalState.nodeColor(node)) // .onNodeClick((node: any) => Editor.globalState.onNodeClick(node)) // .onNodeDragEnd((node: any, translate: any) => // Editor.globalState.onNodeDragEnd(node, translate) // ) // .autoPauseRedraw(false) // keep redrawing after engine has stopped // .linkWidth((link: any) => Editor.globalState.linkWidth(link)) // .linkDirectionalParticles( // Editor.globalState.linkDirectionalParticles() // ) // .linkDirectionalParticleWidth((link: any) => // Editor.globalState.linkDirectionalParticleWidth(link) // ) // .onBackgroundClick((event: any) => // Editor.globalState.onBackgroundClick( // event, // this.extractPositions(event) // ) // ) // .nodeCanvasObjectMode((node: any) => // Editor.globalState.nodeCanvasObjectMode(node) // ) // .nodeCanvasObject((node: any, ctx: any, globalScale: any) => // Editor.globalState.nodeCanvasObject(node, ctx, globalScale) // ) // .linkCanvasObjectMode((link: any) => // Editor.globalState.linkCanvasObjectMode(link) // ) // .linkCanvasObject((link: any, ctx: any, globalScale: any) => // Editor.globalState.linkCanvasObject(link, ctx, globalScale) // ) // .onLinkClick((link: any) => Editor.globalState.onLinkClick(link)); // // Connect update event // Editor.globalGraph.onChangeCallbacks.push((data) => { // Editor.globalRenderer.graphData(data); // }); // Set as new state console.log(newGraph); this.setState({ state: newState, graph: newGraph, }); this.state.graph.onChangeCallbacks.push(this.onHistoryChange); // Subscribe to global key-press events document.onkeydown = newState.onKeyDown; document.onkeyup = newState.onKeyUp; return true; } /** * Propagates the changed state of the graph. */ private onHistoryChange() { this.forceUpdate(); } /** * Calculates the corresponding coordinates for a click event for easier further processing. * @param event The corresponding click event. * @returns Coordinates in graph and coordinates in browser window. */ private extractPositions(event: any): { graph: { x: number; y: number }; window: { x: number; y: number }; } { return { graph: this.state.renderer.screen2GraphCoords( event.layerX, event.layerY ), window: { x: event.clientX, y: event.clientY }, }; } private handleNodeClick(node: Node) { node.delete(); this.forceUpdate(); } render(): React.ReactNode { // The id "ks-editor" indicates, that the javascript associated with this should automatically be executed return ( <div id="ks-editor"> <h1>Interface</h1> <SpaceSelect onLoadSpace={this.loadSpace} /> <div id="content"> <div id="sidepanel"> <HistoryNavigator spaceId="space" historyObject={this.state.graph} onChange={this.onHistoryChange} /> <hr /> <ToolPool state={this.state.state} /> <hr /> <ToolDetails /> </div> {this.state.graph ? ( <ReactForceGraph2d graphData={this.state.graph.data} onNodeClick={this.handleNodeClick} autoPauseRedraw={false} /> ) : undefined} </div> </div> ); } }