diff --git a/src/common/graph/node.ts b/src/common/graph/node.ts index 7a1c4e8ac8dc659b3bd732f8bc17a7db04a5d1fc..78de57b1040d1b7e9fc3567ab572b307fdcb3ab5 100644 --- a/src/common/graph/node.ts +++ b/src/common/graph/node.ts @@ -3,7 +3,7 @@ import { GraphElement } from "./graphelement"; import { NodeType } from "./nodetype"; import { Link } from "./link"; -interface NodeProperties { +export interface NodeProperties { name: string; description?: string; icon?: string; diff --git a/src/editor/components/nodedetails.tsx b/src/editor/components/nodedetails.tsx index 9db2e920eeb2edc5f0fb6c96aa7345b178676e24..f0144d4678c7cf095152b6e20c5252be2811a726 100644 --- a/src/editor/components/nodedetails.tsx +++ b/src/editor/components/nodedetails.tsx @@ -1,287 +1,205 @@ import React from "react"; -import { ReactNode } from "react"; import { Node } from "../../common/graph/node"; import { NodeType } from "../../common/graph/nodetype"; import "./nodedetails.css"; +import { NodeDataChangeRequest } from "../editor"; -type propTypes = { +type NodeDetailsProps = { selectedNodes: Node[]; - allTypes: NodeType[]; - onChange: { (): void }; + nameToObjectType: Map<string, NodeType>; // TODO: Change to id + onNodeDataChange: { (requests: NodeDataChangeRequest[]): void }; }; -export class NodeDetails extends React.Component<propTypes> { - debounceTimeout: NodeJS.Timeout; - debounceArgs: any[]; - debounceFunc: any; - - constructor(props: propTypes) { - super(props); - this.handleNodeTypeChange = this.handleNodeTypeChange.bind(this); - this.handleTextChange = this.handleTextChange.bind(this); - //this.debounce = this.debounce.bind(this); +function NodeDetails({ + selectedNodes, + nameToObjectType, + onNodeDataChange, +}: NodeDetailsProps) { + if (selectedNodes.length == 0) { + return <div id="nodedetails">No node selected.</div>; } - private handleNodeTypeChange(event: any) { - this.props.selectedNodes.forEach( - (n: Node) => n.setType(event.target.value) // TODO: Later implement new save point handling to collect them all into a big one + const getCollectiveValue = function <ValueType>( + getter: (n: Node) => ValueType, + defaultValue: ValueType + ) { + const referenceValue = getter(selectedNodes[0]); + const differentValueFound = selectedNodes.some( + (n: Node) => getter(n) !== referenceValue ); - this.props.onChange(); - } - - private get referenceNode(): Node { - if ( - this.props.selectedNodes == undefined || - this.props.selectedNodes.length <= 0 - ) { - // Nothing selected - return new Node(); - } else if (this.props.selectedNodes.length === 1) { - // Single node handling - return this.props.selectedNodes[0]; - } else { - // Multiple nodes selected => Create a kind of merged node - const refNode = new Node(); - Object.assign(refNode, this.props.selectedNodes[0]); - - refNode.banner = this.getCollectiveValue((n: Node) => n.banner); - refNode.icon = this.getCollectiveValue((n: Node) => n.icon); - refNode.video = this.getCollectiveValue((n: Node) => n.video); - refNode.type = this.getCollectiveValue((n: Node) => n.type); - - return refNode; - } - } - - /** - * Tries to find a representative value for a specific property over all selected nodes. - * @param propGetter Function that returns the value to test for each node. - * @returns If all nodes have the same value, this value is returned. Otherwise undefined is returned. - */ - getCollectiveValue(propGetter: (n: Node) => any): any { - const sameValue: any = propGetter(this.props.selectedNodes[0]); - - const differentValueFound = this.props.selectedNodes.some( - (n: Node) => propGetter(n) !== sameValue + return differentValueFound ? defaultValue : referenceValue; + }; + + const referenceData: NodeDataChangeRequest = { + id: -1, + name: "", + description: getCollectiveValue((n) => n.description, undefined), + video: getCollectiveValue((n) => n.video, undefined), + icon: getCollectiveValue((n) => n.icon, undefined), + banner: getCollectiveValue((n) => n.banner, undefined), + references: [], + type: getCollectiveValue((n) => n.type, undefined), + }; + + const handleDataChange = function <ValueType>( + key: keyof NodeDataChangeRequest, + value: ValueType + ) { + Object.assign(referenceData, { [key]: value }); + onNodeDataChange( + selectedNodes.map((node) => { + return { ...referenceData, id: node.id, name: node.name }; + }) ); - if (differentValueFound) { - return undefined; - } - - return sameValue; - } - - /** - * Generic function for handeling a changing text input and applying the new value to the currently selected node. - * @param event Change event of text input. - * @param property Property to give new value. - */ - private handleTextChange(event: any, property: string) { - const newValue = event.target.value; - - // Actual change? - if ((this.referenceNode as any)[property] == newValue) { - return; - } - - this.props.selectedNodes.forEach((n: any) => (n[property] = newValue)); - this.props.onChange(); - - // Save change, but debounce, so it doesn't trigger too quickly - // this.debounce( - // (property: string) => { - // // this.props.selectedNodes[0].graph.storeCurrentData( TODO: Reimplement - // // "Changed " + property + " of selected nodes" - // // ); - // this.props.onChange(); - // }, - // 500, - // property - // ); - } + }; - // debounce(func: any, wait: number, ...args: any[]) { - // // It works, don't question it - // const later = () => { - // this.debounceTimeout = null; - // this.debounceFunc(...this.debounceArgs); - // }; - // - // clearTimeout(this.debounceTimeout); - // if ( - // this.debounceArgs !== undefined && - // args[0] !== this.debounceArgs[0] && - // this.debounceFunc !== undefined - // ) { - // this.debounceFunc(...this.debounceArgs); - // } - // - // this.debounceArgs = args; - // this.debounceFunc = func; - // this.debounceTimeout = setTimeout(later, wait); - // } - - render(): ReactNode { - if ( - this.props.selectedNodes === undefined || - this.props.selectedNodes.length <= 0 - ) { - return <p>No Node selected.</p>; - } - - return ( - <div id="nodedetails"> - {this.props.selectedNodes.length === 1 ? ( - <div> - <label htmlFor="node-name" hidden> - Name - </label> - <input - type="text" - id="node-name" - name="node-name" - placeholder="Enter name" - className="bottom-space" - value={this.referenceNode.name} - onChange={(event) => - this.handleTextChange(event, "name") - } - ></input> - </div> - ) : ( - <h3>{this.props.selectedNodes.length} nodes selected</h3> - )} - - {this.props.selectedNodes.length === 1 ? ( - <div> - <label htmlFor="node-description">Description</label> - <br /> - <textarea - id="node-description" - name="node-description" - className="bottom-space" - value={this.referenceNode.description ?? ""} - onChange={(event) => - this.handleTextChange(event, "description") - } - ></textarea> - </div> - ) : ( - "" - )} + return ( + <div id="nodedetails"> + {selectedNodes.length === 1 ? ( <div> - <label htmlFor="node-image">Icon Image</label> - <br /> - {this.referenceNode.icon ? ( - <div> - <img - id="node-image-preview" - className="preview-image" - src={this.referenceNode.icon} - /> - <br /> - </div> - ) : ( - "" - )} + <label htmlFor="node-name" hidden> + Name + </label> <input type="text" - id="node-image" - name="node-image" - placeholder="Image URL" + id="node-name" + name="node-name" + placeholder="Enter name" className="bottom-space" - value={this.referenceNode.icon ?? ""} + value={referenceData.name} onChange={(event) => - this.handleTextChange(event, "icon") + handleDataChange("name", event.target.value) } - /> + ></input> </div> + ) : ( + <h3>{selectedNodes.length} nodes selected</h3> + )} + + {selectedNodes.length === 1 ? ( <div> - <label htmlFor="node-detail-image">Banner Image</label> + <label htmlFor="node-description">Description</label> <br /> - {this.referenceNode.banner ? ( - <div> - <img - id="node-image-preview" - className="preview-image" - src={this.referenceNode.banner} - /> - <br /> - </div> - ) : ( - "" - )} - <input - type="text" - id="node-detail-image" - name="node-detail-image" - placeholder="Image URL" + <textarea + id="node-description" + name="node-description" className="bottom-space" - value={this.referenceNode.banner ?? ""} + value={referenceData.description} onChange={(event) => - this.handleTextChange(event, "banner") + handleDataChange("description", event.target.value) } - /> + ></textarea> </div> + ) : ( + "" + )} + <div> + <label htmlFor="node-image">Icon Image</label> + <br /> + {referenceData.icon && ( + <div> + <img + id="node-image-preview" + className="preview-image" + src={referenceData.icon} + /> + <br /> + </div> + )} + <input + type="text" + id="node-image" + name="node-image" + placeholder="Image URL" + className="bottom-space" + value={referenceData.icon ?? ""} + onChange={(event) => + handleDataChange("icon", event.target.value) + } + /> + </div> + <div> + <label htmlFor="node-detail-image">Banner Image</label> + <br /> + {referenceData.banner && ( + <div> + <img + id="node-image-preview" + className="preview-image" + src={referenceData.banner} + /> + <br /> + </div> + )} + <input + type="text" + id="node-detail-image" + name="node-detail-image" + placeholder="Image URL" + className="bottom-space" + value={referenceData.banner ?? ""} + onChange={(event) => + handleDataChange("banner", event.target.value) + } + /> + </div> + <div> + <label htmlFor="node-type">Type</label> + <br /> + <select + id="node-type" + name="node-type" + className="bottom-space" + value={referenceData.type ? referenceData.type.id : ""} + onChange={(event) => + handleDataChange( + "type", + nameToObjectType.get(event.target.value) + ) + } + > + <option className="empty-select-option" disabled></option> + {[...nameToObjectType.values()].map((type) => ( + <option key={type.id} value={type.id}> + {type.name} + </option> + ))} + </select> + </div> + <div> + <label htmlFor="node-video">Video</label> + <br /> + <input + type="text" + placeholder="Video URL" + id="node-video" + name="node-video" + value={referenceData.video ?? ""} + onChange={(event) => + handleDataChange("video", event.target.value) + } + ></input> + </div> + {selectedNodes.length === 1 ? ( <div> - <label htmlFor="node-type">Type</label> + <label htmlFor="node-references">References</label>{" "} + <small>One URL per line</small> <br /> - <select - id="node-type" - name="node-type" + <textarea + id="node-references" + name="node-references" className="bottom-space" - value={ - this.referenceNode.type - ? this.referenceNode.type.id - : "" - } - onChange={this.handleNodeTypeChange} - > - <option - className="empty-select-option" - disabled - selected - ></option> - {this.props.allTypes.map((type) => ( - <option key={type.id} value={type.id}> - {type.name} - </option> - ))} - </select> - </div> - <div> - <label htmlFor="node-video">Video</label> - <br /> - <input - type="text" - placeholder="Video URL" - id="node-video" - name="node-video" - value={this.referenceNode.video ?? ""} + value={referenceData.references} onChange={(event) => - this.handleTextChange(event, "video") + handleDataChange("references", event.target.value) } - ></input> + ></textarea> </div> - {this.props.selectedNodes.length === 1 ? ( - <div> - <label htmlFor="node-references">References</label>{" "} - <small>One URL per line</small> - <br /> - <textarea - id="node-references" - name="node-references" - className="bottom-space" - value={this.referenceNode.references} - onChange={(event) => - this.handleTextChange(event, "references") - } - ></textarea> - </div> - ) : ( - "" - )} - </div> - ); - } + ) : ( + "" + )} + </div> + ); } + +export default NodeDetails; diff --git a/src/editor/editor.tsx b/src/editor/editor.tsx index 3fbd28466e69f443cca4f8e1125344942992dbcf..def7e1fb41333776e70f24382bc04c8406127f45 100644 --- a/src/editor/editor.tsx +++ b/src/editor/editor.tsx @@ -1,11 +1,11 @@ import React from "react"; import { DynamicGraph } from "./graph"; import { listAllSpaces, loadGraphJson } from "../common/datasets"; -import { NodeDetails } from "./components/nodedetails"; +import NodeDetails from "./components/nodedetails"; import SpaceSelect from "./components/spaceselect"; import "./editor.css"; import * as Helpers from "../common/helpers"; -import { Node } from "../common/graph/node"; +import { Node, NodeProperties } from "../common/graph/node"; import { NodeTypesEditor } from "./components/nodetypeseditor"; import { SpaceManager } from "./components/spacemanager"; @@ -18,6 +18,11 @@ import Settings from "./components/settings"; import HistoryNavigator from "./components/historynavigator"; import * as Config from "../config"; +export interface NodeDataChangeRequest extends NodeProperties { + id: number; + type: NodeType; +} + type propTypes = {}; type stateTypes = { /** @@ -72,6 +77,7 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { 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); @@ -213,7 +219,19 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { this.selectNodes(nodesWithType); } - private onNodeDataChange() {} + 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 ( @@ -270,8 +288,10 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { <hr /> <NodeDetails selectedNodes={this.state.selectedNodes} - allTypes={this.state.graph.objectGroups} - onChange={this.forceUpdate} + nameToObjectType={ + this.state.graph.nameToObjectGroup + } // TODO: Change to id + onNodeDataChange={this.handleNodeDataChange} /> <hr /> <NodeTypesEditor