From c8a34221d570110c7706b22905159128de899da9 Mon Sep 17 00:00:00 2001
From: Maximilian Giller <m.giller@tu-bs.de>
Date: Thu, 6 Jan 2022 12:48:37 +0100
Subject: [PATCH] Connected node interface

---
 editor/editor.php                    |   3 +-
 editor/js/tools/collecttool.js       |   7 ++
 editor/js/tools/menus/collectmenu.js | 182 +++++++++++++++++++++++++++
 3 files changed, 191 insertions(+), 1 deletion(-)

diff --git a/editor/editor.php b/editor/editor.php
index 689cbd9..59d5475 100644
--- a/editor/editor.php
+++ b/editor/editor.php
@@ -32,7 +32,6 @@
                 <label for="nodes-type">* Type</label>
                 </br>
                 <select id="nodes-type" name="nodes-type" class="bottom-space">
-                    <option value=""></option>
                     <option value="Vorlesung">Vorlesung</option>
                     <option value="Algorithmus">Algorithmus</option>
                     <option value="Definition">Definition</option>
@@ -51,6 +50,8 @@
                 </br>
                 <textarea id="nodes-references" name="nodes-references" class="bottom-space"></textarea>
                 </br>
+                <button id="delete-selected-nodes">Delete all selected nodes</button>
+                <br/>
             </div>
 
             <h3>Collected items</h3>
diff --git a/editor/js/tools/collecttool.js b/editor/js/tools/collecttool.js
index 0d257bd..2f8775e 100644
--- a/editor/js/tools/collecttool.js
+++ b/editor/js/tools/collecttool.js
@@ -19,6 +19,12 @@ export default class CollectTool extends Tool {
         }
     }
 
+    onMenuChange(key, value) {
+        if (key === COLLECTION_KEY) {
+            graph.changeDetails(value);
+        }
+    }
+
     onBoxSelect(left, bottom, top, right) {
         // Filter out selected nodes
         const hitNodes = [];
@@ -80,6 +86,7 @@ export default class CollectTool extends Tool {
 
     onBackgroundClick() {
         state.clearSelectedItems();
+        this.menu.value(COLLECTION_KEY, state.selectedItems);
     }
 
     onMenuChange(key, value) {
diff --git a/editor/js/tools/menus/collectmenu.js b/editor/js/tools/menus/collectmenu.js
index df6ddaf..85a683d 100644
--- a/editor/js/tools/menus/collectmenu.js
+++ b/editor/js/tools/menus/collectmenu.js
@@ -3,6 +3,7 @@ import { Graph } from "../../graph";
 import { CONTEXT } from "../../state";
 import { state } from "../../editor";
 import ToolMenu from "./toolmenu";
+import { PLUGIN_PATH } from "../../../../config";
 
 export const COLLECTION_KEY = "collection";
 
@@ -15,10 +16,51 @@ const HIDDEN_CLASS = "hidden";
 
 const DOM_LIST_ITEM = "li";
 
+const NODES_IMG_PREVIEW = "#nodes-image-preview";
+const NODES_DETAIL_IMG_PREVIEW = "#nodes-detail-image-preview";
+const NODES_IMG_ID = "#nodes-image";
+const NODES_DETAIL_IMG_ID = "#nodes-detail-image";
+const NODES_DESC_ID = "#nodes-description";
+const NODES_TYPE_ID = "#nodes-type";
+const NODES_REF_ID = "#nodes-references";
+const NODES_VIDEO_ID = "#nodes-video";
+const NODES_MENU = [
+    NODES_IMG_ID,
+    NODES_DESC_ID,
+    NODES_TYPE_ID,
+    NODES_REF_ID,
+    NODES_VIDEO_ID,
+    NODES_DETAIL_IMG_ID,
+];
+
+const IMAGE_MENU = [NODES_IMG_ID, NODES_DETAIL_IMG_ID];
+const IMAGE_FIELDS = [
+    {
+        uri: NODES_IMG_ID,
+        preview: NODES_IMG_PREVIEW,
+    },
+    {
+        uri: NODES_DETAIL_IMG_ID,
+        preview: NODES_DETAIL_IMG_PREVIEW,
+    },
+];
+
+const MENU = [...NODES_MENU];
+
+const ERROR_IMG_PATH = PLUGIN_PATH + "editor/images/onerror.png";
+
 export class CollectMenu extends ToolMenu {
     constructor() {
         super();
         this.context = undefined;
+        this.map = [
+            { menu: NODES_IMG_ID, property: Graph.NODE_IMAGE },
+            { menu: NODES_DESC_ID, property: Graph.NODE_DESCRIPTION },
+            { menu: NODES_TYPE_ID, property: Graph.NODE_TYPE },
+            { menu: NODES_REF_ID, property: Graph.NODE_REFERENCES },
+            { menu: NODES_VIDEO_ID, property: Graph.NODE_VIDEO },
+            { menu: NODES_DETAIL_IMG_ID, property: Graph.NODE_DETAIL_IMAGE },
+        ];
     }
 
     hookClearButton() {
@@ -31,6 +73,7 @@ export class CollectMenu extends ToolMenu {
     onMenuShow(initial) {
         if (initial) {
             this.hookClearButton();
+            this.hookNodesInterface();
         }
     }
 
@@ -40,6 +83,7 @@ export class CollectMenu extends ToolMenu {
 
             // Set corresponding context
             this.setContext(state.itemsContext);
+            this.clearNodeInterface();
         }
     }
 
@@ -62,6 +106,9 @@ export class CollectMenu extends ToolMenu {
         items.forEach((i) => listCont.append(itemRenderer(i)));
     }
 
+
+    // a LOT of duplicate code down here with selectmenu.js, should be improved
+
     setContext(context) {
         if (context === this.context) {
             return; // Only do something if it changes
@@ -92,4 +139,139 @@ export class CollectMenu extends ToolMenu {
             return undefined;
         }
     }
+
+    clearNodeInterface() {
+        NODES_MENU.forEach((menu) => {
+            this.find(menu).val(undefined);
+        });
+        this.updateImagePreviews();
+    }
+
+    updateImagePreviews() {
+        IMAGE_FIELDS.forEach((imageField) => {
+            var uri = this.find(imageField.uri).val();
+            this.setImagePreview(uri, imageField.preview);
+        });
+    }
+
+    getFullImageSource(uri) {
+        if (uri === "") {
+            // Show default empty image
+            return "";
+        } else if (uri.includes("/")) {
+            // Is absolute URL
+            return uri;
+        } else {
+            // Is file name
+            return Graph.IMAGE_SRC + uri;
+        }
+    }
+
+    setImagePreview(uri, previewId) {
+        var previewImage = this.find(previewId);
+        previewImage.attr("src", this.getFullImageSource(uri));
+    }
+
+    toProperty(menu) {
+        for (var i = 0; i < this.map.length; i++) {
+            if (this.map[i].menu === menu) {
+                return this.map[i].property;
+            }
+        }
+        return undefined;
+    }
+
+    toMenu(property) {
+        for (var i = 0; i < this.map.length; i++) {
+            if (this.map[i].property === property) {
+                return this.map[i].menu;
+            }
+        }
+        return undefined;
+    }
+
+    updateValue(menu, newValue) {
+        // Interface only available for nodes, so check before trying to work on data
+        if (this.context !== CONTEXT.node) {
+            return;
+        }
+
+        var propertyKey = this.toProperty(menu);
+        var formatedValue = this.formatValue(propertyKey, newValue);
+
+        // Update all nodes with the new value
+        this.values[COLLECTION_KEY].forEach((node) => {
+            node[propertyKey] = formatedValue;
+        });
+
+        // Notify tool
+        this.tool.onMenuChange(COLLECTION_KEY, this.values[COLLECTION_KEY]);
+    }
+
+    formatValue(propertyKey, rawValue) {
+        var formattedValue = rawValue;
+
+        if (propertyKey === Graph.NODE_REFERENCES) {
+            // Explode to list of url-lines
+            formattedValue = rawValue
+                .split("\n") // Every line is it's own url
+                .filter((url) => url); // Remove empty entries
+        }
+
+        return formattedValue;
+    }
+
+    hookNodesInterface() {
+        MENU.forEach((menu) => {
+            if (IMAGE_MENU.includes(menu)) {
+                return;
+            }
+
+            // Subscribes to change event for each menu element
+            this.find(menu).on("change", (e) => {
+                this.updateValue(menu, e.target.value);
+            });
+        });
+
+        // Special hooks for image, to update the shown image with every change
+        IMAGE_FIELDS.forEach((imageField) => {
+            // In case image can't be loaded, show message
+            this.find(imageField.preview).on("error", (e) => {
+                var img = this.find(e.target);
+
+                // Is source even set?
+                if (img.attr("src") === undefined || img.attr("src") === "") {
+                    return;
+                }
+
+                // Show error message
+                this.setImagePreview(ERROR_IMG_PATH, imageField.preview);
+                // Maybe graph image should also be updated, but we might not want to overwrite previously saved images.
+            });
+
+            // Test images before updating them
+            this.find(imageField.uri).on("change", (e) => {
+                var imageSource = e.target.value;
+
+                // If source is empty, always apply it
+                if (imageSource === undefined || imageSource === "") {
+                    this.updateValue(imageField.uri, imageSource);
+                    this.setImagePreview(imageSource, imageField.preview);
+                    return;
+                }
+
+                // Try loading the image and only apply it on success
+                var img = new Image();
+                img.addEventListener("load", () => {
+                    this.updateValue(imageField.uri, imageSource);
+                    this.setImagePreview(imageSource, imageField.preview);
+                });
+                img.addEventListener("error", () => {
+                    this.setImagePreview(ERROR_IMG_PATH, imageField.preview);
+                });
+
+                img.src = this.getFullImageSource(imageSource);
+            });
+        });
+    }
 }
-- 
GitLab