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