From 8db57fccc80fe384ee288232f95602430b8d6cab Mon Sep 17 00:00:00 2001 From: Maximilian Giller <m.giller@tu-bs.de> Date: Tue, 6 Sep 2022 22:46:49 +0200 Subject: [PATCH] Some values can now be edited on multiple nodes at once --- src/editor/js/components/editor.tsx | 14 +- src/editor/js/components/nodedetails.css | 4 + src/editor/js/components/nodedetails.tsx | 189 +++++++++++++++-------- 3 files changed, 140 insertions(+), 67 deletions(-) diff --git a/src/editor/js/components/editor.tsx b/src/editor/js/components/editor.tsx index aaf4f4d..25dd3ff 100644 --- a/src/editor/js/components/editor.tsx +++ b/src/editor/js/components/editor.tsx @@ -301,6 +301,14 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { newNode.add(this.state.graph); this.forceUpdate(); + + // Select newly created node + if (this.state.keys["Shift"]) { + // Simply add to current selection of shift is pressed + this.toggleNodeSelection(newNode); + } else { + this.selectNode(newNode); + } } /** @@ -716,11 +724,7 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { /> <hr /> <NodeDetails - selectedNode={ - this.selectedNodes - ? this.selectedNodes[0] - : undefined - } + selectedNodes={this.selectedNodes} allTypes={ this.state.graph ? this.state.graph.types : [] } diff --git a/src/editor/js/components/nodedetails.css b/src/editor/js/components/nodedetails.css index 20bade5..4211813 100644 --- a/src/editor/js/components/nodedetails.css +++ b/src/editor/js/components/nodedetails.css @@ -12,3 +12,7 @@ div#ks-editor #nodedetails #node-name { font-weight: bold; font-size: large; } + +div#ks-editor #nodedetails .empty-select-option { + display: none; +} diff --git a/src/editor/js/components/nodedetails.tsx b/src/editor/js/components/nodedetails.tsx index 2a619ab..f6a6ba2 100644 --- a/src/editor/js/components/nodedetails.tsx +++ b/src/editor/js/components/nodedetails.tsx @@ -5,7 +5,7 @@ import { NodeType } from "../structures/graph/nodetype"; import "./nodedetails.css"; type propTypes = { - selectedNode: Node; + selectedNodes: Node[]; allTypes: NodeType[]; onChange: { (): void }; }; @@ -23,10 +23,54 @@ export class NodeDetails extends React.Component<propTypes> { } private handleNodeTypeChange(event: any) { - this.props.selectedNode.setType(event.target.value); + 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 + ); 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 + ); + 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. @@ -36,22 +80,18 @@ export class NodeDetails extends React.Component<propTypes> { const newValue = event.target.value; // Actual change? - if ((this.props.selectedNode as any)[property] == newValue) { + if ((this.referenceNode as any)[property] == newValue) { return; } - (this.props.selectedNode as any)[property] = newValue; + 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.selectedNode.graph.storeCurrentData( - "Changed " + - property + - " of node [" + - this.props.selectedNode.toString() + - "]" + this.props.selectedNodes[0].graph.storeCurrentData( + "Changed " + property + " of selected nodes" ); this.props.onChange(); }, @@ -82,50 +122,62 @@ export class NodeDetails extends React.Component<propTypes> { } render(): ReactNode { - if (this.props.selectedNode === undefined) { + if ( + this.props.selectedNodes === undefined || + this.props.selectedNodes.length <= 0 + ) { return <p>No Node selected.</p>; } return ( <div id="nodedetails"> - <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.props.selectedNode.name} - onChange={(event) => - this.handleTextChange(event, "name") - } - ></input> - </div> - <div> - <label htmlFor="node-description">Description</label> - <br /> - <textarea - id="node-description" - name="node-description" - className="bottom-space" - value={this.props.selectedNode.description} - onChange={(event) => - this.handleTextChange(event, "description") - } - ></textarea> - </div> + {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> + ) : ( + "" + )} <div> <label htmlFor="node-image">Icon Image</label> <br /> - {this.props.selectedNode.icon ? ( + {this.referenceNode.icon ? ( <div> <img id="node-image-preview" className="preview-image" - src={this.props.selectedNode.icon} + src={this.referenceNode.icon} /> <br /> </div> @@ -138,7 +190,7 @@ export class NodeDetails extends React.Component<propTypes> { name="node-image" placeholder="Image URL" className="bottom-space" - value={this.props.selectedNode.icon} + value={this.referenceNode.icon ?? ""} onChange={(event) => this.handleTextChange(event, "icon") } @@ -147,12 +199,12 @@ export class NodeDetails extends React.Component<propTypes> { <div> <label htmlFor="node-detail-image">Banner Image</label> <br /> - {this.props.selectedNode.banner ? ( + {this.referenceNode.banner ? ( <div> <img id="node-image-preview" className="preview-image" - src={this.props.selectedNode.banner} + src={this.referenceNode.banner} /> <br /> </div> @@ -165,7 +217,7 @@ export class NodeDetails extends React.Component<propTypes> { name="node-detail-image" placeholder="Image URL" className="bottom-space" - value={this.props.selectedNode.banner} + value={this.referenceNode.banner ?? ""} onChange={(event) => this.handleTextChange(event, "banner") } @@ -178,9 +230,18 @@ export class NodeDetails extends React.Component<propTypes> { id="node-type" name="node-type" className="bottom-space" - value={this.props.selectedNode.type.id} + 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} @@ -196,26 +257,30 @@ export class NodeDetails extends React.Component<propTypes> { placeholder="Video URL" id="node-video" name="node-video" - value={this.props.selectedNode.video} + value={this.referenceNode.video ?? ""} onChange={(event) => this.handleTextChange(event, "video") } ></input> </div> - <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.props.selectedNode.references} - onChange={(event) => - this.handleTextChange(event, "references") - } - ></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> ); } -- GitLab