import React from "react"; import PropTypes, { InferType } from "prop-types"; import { State } from "../state"; import * as Interactions from "../interactions"; import { Graph } from "../structures/graph/graph"; import ForceGraph from "force-graph"; import { loadGraphJson } from "../../../datasets"; export class Editor extends React.PureComponent< InferType<typeof Editor.propTypes>, InferType<typeof Editor.stateTypes> > { static propTypes = {}; static stateTypes = { state: State, graph: Graph, renderer: PropTypes.any, }; // TODO: Not a long term solution! public static globalState: State; public static globalGraph: Graph; public static globalRenderer: any; constructor(props: InferType<typeof Editor.propTypes>) { super(props); Interactions.initInteractions(); } /** * 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 { // Create global objects const state = new State(); const graph = Graph.parse(data); // Is valid and parsed successfully? if (graph == undefined) { return false; } // Create renderer const renderTarget = document.getElementById("2d-graph"); const renderWidth = renderTarget.offsetWidth; const renderer = ForceGraph()(renderTarget); // Subscribe to interactions renderer .height(600) .width(renderWidth) .graphData(graph.data) .nodeLabel("label") .linkColor((link) => state.linkColor(link)) .nodeColor((node) => state.nodeColor(node)) .onNodeClick((node) => state.onNodeClick(node)) .onNodeDragEnd((node, translate) => state.onNodeDragEnd(node, translate) ) .autoPauseRedraw(false) // keep redrawing after engine has stopped .linkWidth((link) => state.linkWidth(link)) .linkDirectionalParticles(state.linkDirectionalParticles()) .linkDirectionalParticleWidth((link) => state.linkDirectionalParticleWidth(link) ) .onBackgroundClick((event) => state.onBackgroundClick(event, this.extractPositions(event)) ) .nodeCanvasObjectMode((node) => state.nodeCanvasObjectMode(node)) .nodeCanvasObject((node, ctx, globalScale) => state.nodeCanvasObject(node, ctx, globalScale) ) .linkCanvasObjectMode((link) => state.linkCanvasObjectMode(link)) .linkCanvasObject((link, ctx, globalScale) => state.linkCanvasObject(link, ctx, globalScale) ) .onLinkClick((link) => state.onLinkClick(link)); // Connect update event graph.onChangeCallbacks.push((data) => { renderer.graphData(data); }); // Set as new state this.setState({ state: state, graph: graph, renderer: renderer, }); Editor.globalState = state; Editor.globalGraph = graph; Editor.globalRenderer = renderer; // Subscribe to global key-press events document.onkeydown = this.state.state.onKeyDown; document.onkeyup = this.state.state.onKeyUp; return true; } /** * 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 }, }; } 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> <div id="box-select-layer"> <div id="2d-graph"></div> </div> <section id="toolbar"></section> <section id="tool-menu"> <div id="delete-menu" className="hidden"> <p> Drag and drop while pressing SHIFT to delete all the nodes that are being selected. </p> </div> <div id="collect-menu" className="hidden"> <h3>Collected items</h3> <button id="clear-collection">Clear</button> <ul id="selected-items"></ul> </div> <div id="select-menu" className=""> <p id="nothing-selected">Nothing selected</p> <div id="node-selected" className="hidden"> <label htmlFor="node-name" hidden> Name </label> <br /> <input type="text" id="node-name" name="node-name" placeholder="Enter name" className="bottom-space" ></input> <br /> <label htmlFor="node-description"> Description </label> <br /> <textarea id="node-description" name="node-description" className="bottom-space" ></textarea> <br /> <label htmlFor="node-image">Node Image</label> <br /> <img id="node-image-preview" className="preview-image" src="" /> <br /> <input type="text" id="node-image" name="node-image" placeholder="Enter file name or URL" className="bottom-space" /> <br /> <label htmlFor="node-detail-image"> Info Image </label> <br /> <img id="node-detail-image-preview" className="preview-image" src="" /> <br /> <input type="text" id="node-detail-image" name="node-detail-image" placeholder="Enter file name or URL" className="bottom-space" /> <br /> <label htmlFor="node-type">Type</label> <br /> <select id="node-type" name="node-type" className="bottom-space" > <option value="Vorlesung">Vorlesung</option> <option value="Algorithmus">Algorithmus</option> <option value="Definition">Definition</option> <option value="Beispiel">Beispiel</option> <option value="Übung">Übung</option> <option value="Kapitel">Kapitel</option> </select> <br /> <label htmlFor="node-video">Video</label> <br /> <input type="text" placeholder="Video URL" id="node-video" name="node-video" ></input> <br /> <label htmlFor="node-references"> References </label>{" "} <small>One URL per line</small> <br /> <textarea id="node-references" name="node-references" className="bottom-space" ></textarea> </div> <div id="link-selected" className="hidden"> <h3 id="link-name"></h3> </div> </div> <div id="settings-menu" className="hidden"> <label htmlFor="label-toggle" className="bottom-space"> <input type="checkbox" checked id="label-toggle" name="label-toggle" ></input> Show labels in graph </label> <br /> <br /> <h3>Space</h3> <label htmlFor="space-id-select">Currently open</label> <br /> <select id="space-id-select" name="space-id-select" className="bottom-space" ></select> <br /> <br /> <h3>Physics Simulation</h3> <button id="reanimate-button" name="reanimate-button" className="bottom-space" > Re-simulate </button> <br /> <label htmlFor="stop-physics-delay"> Amount of time [in seconds] after which the physics simulation is stopped </label> <br /> <input type="number" onKeyPress={(event) => (event.charCode != 8 && event.charCode == 0) || (event.charCode >= 48 && event.charCode <= 57) } value="5" id="stop-physics-delay" name="stop-physics-delay" className="small-width" ></input> <br /> <br /> <h3>Import Space</h3> <label htmlFor="import-space-area">Space JSON</label> <br /> <textarea id="import-space-area" name="import-space-area" className="bottom-space" ></textarea> <br /> <label htmlFor="import-space-name-text"> Space Name </label> <br /> <input type="text" id="import-space-name-text" name="import-space-name-text" className="bottom-space" ></input> <br /> <button id="import-space-btn" name="import-space-btn" className="bottom-space" > Import </button> <br /> <br /> </div> </section> </div> ); } }