From 0b44275e6a0f107d538d099c4715f11d2d96130b Mon Sep 17 00:00:00 2001
From: Max <m.giller.dev@gmail.com>
Date: Thu, 8 Jul 2021 20:45:16 +0200
Subject: [PATCH] Basic editor

---
 editor/editor.html              |  57 +++++++++
 editor/images/tools/collect.png | Bin 0 -> 3591 bytes
 editor/images/tools/delete.png  | Bin 0 -> 3027 bytes
 editor/images/tools/select.png  | Bin 0 -> 3076 bytes
 editor/js/display.js            | 140 ++++++++++++++++++++++
 editor/js/editor.js             |  56 +++++++++
 editor/js/graph.js              | 162 ++++++++++++++++++++++++++
 editor/js/state.js              | 198 ++++++++++++++++++++++++++++++++
 editor/js/tools/collecttool.js  |  37 ++++++
 editor/js/tools/deletetool.js   |  13 +++
 editor/js/tools/selecttool.js   |  19 +++
 editor/js/tools/tool.js         |  77 +++++++++++++
 knowledge-space.php             |  15 +++
 13 files changed, 774 insertions(+)
 create mode 100644 editor/editor.html
 create mode 100644 editor/images/tools/collect.png
 create mode 100644 editor/images/tools/delete.png
 create mode 100644 editor/images/tools/select.png
 create mode 100644 editor/js/display.js
 create mode 100644 editor/js/editor.js
 create mode 100644 editor/js/graph.js
 create mode 100644 editor/js/state.js
 create mode 100644 editor/js/tools/collecttool.js
 create mode 100644 editor/js/tools/deletetool.js
 create mode 100644 editor/js/tools/selecttool.js
 create mode 100644 editor/js/tools/tool.js

diff --git a/editor/editor.html b/editor/editor.html
new file mode 100644
index 0000000..d45958e
--- /dev/null
+++ b/editor/editor.html
@@ -0,0 +1,57 @@
+<div>
+    <style>
+        section {
+            border: 1px lightgrey solid;
+            border-radius: 1px;
+            margin: 5px;
+            padding: 2.5px;
+            width: auto;
+        }
+
+        section > * {
+            margin: 2.5px;
+        }
+
+        .selected {
+            background-color: lightblue;
+        }
+    </style>
+
+    <script src="https://unpkg.com/force-graph"></script>
+    <!--<script src="../../dist/force-graph.js"></script>-->
+
+    <script>
+        const PLUGIN_PATH = "%WWW%";
+    </script>
+
+    <script src="%WWW%editor/js/graph.js"></script>
+    <script src="%WWW%editor/js/tools/tool.js"></script>
+    <script src="%WWW%editor/js/tools/selecttool.js"></script>
+    <script src="%WWW%editor/js/tools/collecttool.js"></script>
+    <script src="%WWW%editor/js/tools/deletetool.js"></script>
+    <script src="%WWW%editor/js/display.js"></script>
+    <script src="%WWW%editor/js/state.js"></script>
+    <script src="%WWW%editor/js/editor.js"></script>
+
+    <h1>Interface</h1>
+    <div id="2d-graph"></div>
+    <section id="toolbar"></section>
+    <section>
+        <h3 id="selected-item">Nothing selected</h3>
+        <ul id="selected-params"></ul>
+        <button onclick="saveCurrentNode()">Save NOT YET IMPLEMENTED</button>
+        <section>
+            <h4>Sources</h4>
+            <ul id="selected-sources"></ul>
+        </section>
+        <section>
+            <h4>Targets</h4>
+            <ul id="selected-targets"></ul>
+        </section>
+    </section>
+    <section>
+        <h3>Collected items</h3>
+        <ul id="selected-items"></ul>
+        <button onclick="state.clearSelectedItems()">Clear</button>
+    </section>
+</div>
diff --git a/editor/images/tools/collect.png b/editor/images/tools/collect.png
new file mode 100644
index 0000000000000000000000000000000000000000..623d3a978005133e87994a16e67dbe9ab7259875
GIT binary patch
literal 3591
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
N002ovPDHLkV1i=P(jNc-

literal 0
HcmV?d00001

diff --git a/editor/images/tools/delete.png b/editor/images/tools/delete.png
new file mode 100644
index 0000000000000000000000000000000000000000..007f69a325d89d0d5e4eee3d2b0df7f8844c4d13
GIT binary patch
literal 3027
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
Vxfy;?c~JlW002ovPDHLkV1lvOu~q;8

literal 0
HcmV?d00001

diff --git a/editor/images/tools/select.png b/editor/images/tools/select.png
new file mode 100644
index 0000000000000000000000000000000000000000..840c4f5df6c058d208879fe7d5bfe906eab43015
GIT binary patch
literal 3076
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
S_tpOZ0000<MNUMnLSTXyE5pVB

literal 0
HcmV?d00001

diff --git a/editor/js/display.js b/editor/js/display.js
new file mode 100644
index 0000000..de6379c
--- /dev/null
+++ b/editor/js/display.js
@@ -0,0 +1,140 @@
+const ID_TOOLBAR = "#toolbar";
+const ID_SELECTEDITEM = "#selected-item";
+const ID_SELECTED_PARAMS = "#selected-params";
+const ID_SELECTED_SOURCES = "#selected-sources";
+const ID_SELECTED_TARGETS = "#selected-targets";
+const ID_SELECTEDITEMS = "#selected-items";
+
+const DOM_LIST_ITEM = "li";
+
+const TOOL_ICON_SRC = PLUGIN_PATH + "editor/images/tools/";
+const TOOL_ICON_FORMAT = ".png";
+const TOOL_SELECTED_CLASS = "selected";
+
+class Display {
+    constructor(tools) {
+        this.tools = Object.values(tools);
+        this.previousTool = undefined;
+
+        this.renderToolbar(this.tools);
+    }
+
+    setSelectedTool(tool) {
+        var selectedTool = jQuery(Display.getToolId(tool));
+        selectedTool.addClass(TOOL_SELECTED_CLASS);
+
+        if (this.previousTool !== undefined) {
+            var previousTool = jQuery(Display.getToolId(this.previousTool));
+            previousTool.removeClass(TOOL_SELECTED_CLASS);
+        }
+
+        this.previousTool = tool;
+    }
+
+    renderToolbar(tools) {
+        this.fillDomList(ID_TOOLBAR, tools, this.toolRenderer);
+    }
+
+    static getToolId(tool) {
+        return ID_TOOLBAR + "-" + tool.getKey();
+    }
+
+    toolRenderer(tool) {
+        return (
+            '<button id="' +
+            Display.getToolId(tool).substr(1) + // Remove # from id
+            '"onclick="state.setTool(TOOLS.' +
+            tool.getKey() +
+            ')" title="' +
+            tool.getName() +
+            '"><img src="' +
+            TOOL_ICON_SRC +
+            tool.getKey() +
+            TOOL_ICON_FORMAT +
+            '"></button>'
+        );
+    }
+
+    setSelectedItem(item) {
+        jQuery(ID_SELECTEDITEM).html(Display.toStr(item));
+
+        var paramsDOM = jQuery(ID_SELECTED_PARAMS);
+        paramsDOM.empty();
+
+        var params = NODE_PARAMS;
+        if (item.link) {
+            params = LINK_PARAMS;
+        }
+
+        params.forEach((param) => {
+            paramsDOM.append(
+                "<" +
+                    DOM_LIST_ITEM +
+                    ">" +
+                    param +
+                    ' <textarea>' +
+                    (item[param] === undefined ? "" : item[param]) +
+                    '</textarea></' +
+                    DOM_LIST_ITEM +
+                    ">"
+            );
+        });
+
+        // Render Source and Target list
+        var sources = [];
+        var targets = [];
+        if (item.node) {
+            var nodes = graph.data[GRAPH_NODES];
+            for (var i = 0; i < nodes.length; i++) {
+                if (graph.existsLink(nodes[i][NODE_ID], item[NODE_ID])) {
+                    sources.push(nodes[i]);
+                } else if (graph.existsLink(item[NODE_ID], nodes[i][NODE_ID])) {
+                    targets.push(nodes[i]);
+                }
+            }
+        } else if (item.link) {
+            sources.push(item[LINK_SOURCE]);
+            targets.push(item[LINK_TARGET]);
+        }
+
+        this.fillDomList(ID_SELECTED_SOURCES, sources, this.graphItemRenderer);
+        this.fillDomList(ID_SELECTED_TARGETS, targets, this.graphItemRenderer);
+    }
+
+    setSelectedItems(items, itemsContext) {
+        this.fillDomList(ID_SELECTEDITEMS, items, this.graphItemRenderer);
+    }
+
+    graphItemRenderer(item) {
+        return (
+            "<" +
+            DOM_LIST_ITEM +
+            ">" +
+            Display.toStr(item) +
+            "</" +
+            DOM_LIST_ITEM +
+            ">"
+        );
+    }
+
+    fillDomList(listId, items, itemRenderer) {
+        var listCont = jQuery(listId);
+        listCont.empty();
+
+        items.forEach((i) => listCont.append(itemRenderer(i)));
+    }
+
+    static toStr(item) {
+        if (item.node) {
+            return item[NODE_LABEL] + " [" + item[NODE_ID] + "]";
+        } else if (item.link) {
+            return (
+                Display.toStr(item[LINK_SOURCE]) +
+                " <-> " +
+                Display.toStr(item[LINK_TARGET])
+            );
+        } else {
+            return "UNDEFINED";
+        }
+    }
+}
diff --git a/editor/js/editor.js b/editor/js/editor.js
new file mode 100644
index 0000000..88b22de
--- /dev/null
+++ b/editor/js/editor.js
@@ -0,0 +1,56 @@
+var state;
+var graphObj;
+
+window.onload = function () {
+    fetch(JSON_CONFIG)
+        .then((r) => {
+            return r.json();
+        })
+        .then((graphConfig) => {
+            state = new State();
+
+            graph.data = graphConfig;
+            graph.addIdentifiers();
+            load();
+
+            // Deactivate physics after a short delay
+            setTimeout(() => {
+                graph.stopPhysics();
+            }, STOP_PHYSICS_DELAY);
+        });
+};
+
+document.onkeydown = (e) => state.onKeyDown(e);
+document.onkeyup = (e) => state.onKeyUp(e);
+
+function downloadJson() {
+    // TODO: Clean up
+    // source: https://stackoverflow.com/a/42883108/7376120
+    var jsonBlob = new Blob([JSON.stringify(getOnlygraph.data())], {
+        type: "application/json;charset=utf-8",
+    });
+    var link = window.URL.createObjectURL(jsonBlob);
+    window.location = link;
+}
+
+function load() {
+    const graphContainer = document.getElementById("2d-graph");
+    const width = graphContainer.offsetWidth;
+
+    graphObj = ForceGraph()(graphContainer)
+        .height(600)
+        .width(width)
+        .graphData(graph.data)
+        .nodeLabel(NODE_LABEL)
+        .nodeAutoColorBy(NODE_GROUP)
+        .onNodeClick((node) => state.onNodeClick(node))
+        .autoPauseRedraw(false) // keep redrawing after engine has stopped
+        .linkWidth((link) => state.linkWidth(link))
+        .linkDirectionalParticles(state.linkDirectionalParticles())
+        .linkDirectionalParticleWidth((link) =>
+            state.linkDirectionalParticleWidth(link)
+        )
+        .nodeCanvasObjectMode((node) => state.nodeCanvasObjectMode(node))
+        .nodeCanvasObject((node, ctx) => state.nodeCanvasObject(node, ctx))
+        .onLinkClick((link) => state.onLinkClick(link));
+}
diff --git a/editor/js/graph.js b/editor/js/graph.js
new file mode 100644
index 0000000..52fffdc
--- /dev/null
+++ b/editor/js/graph.js
@@ -0,0 +1,162 @@
+const NODE_LABEL = "name";
+const NODE_ID = "id";
+const NODE_GROUP = "group";
+const NODE_DESCRIPTION = "description";
+const NODE_IMAGE = "image";
+
+const LINK_SOURCE = "source";
+const LINK_TARGET = "target";
+const LINK_TYPE = "type";
+const LINK_PARTICLE_COUNT = 4;
+
+const GRAPH_NODES = "nodes";
+const GRAPH_LINKS = "links";
+
+const IMAGE_SIZE = 12;
+const IMAGE_SRC = PLUGIN_PATH + "datasets/images/"
+
+const LINK_PARAMS = [LINK_TYPE];
+const NODE_PARAMS = [NODE_ID, NODE_LABEL, NODE_IMAGE, NODE_DESCRIPTION];
+
+const JSON_CONFIG = PLUGIN_PATH + "datasets/aud1.json";
+
+const STOP_PHYSICS_DELAY = 5000; // ms
+
+
+
+const graph = {
+    data: undefined,
+
+    deleteNode(nodeId) {
+        // Delete node from nodes
+        graph.data[GRAPH_NODES] = graph.data[GRAPH_NODES].filter(
+            (n) => n[NODE_ID] !== nodeId
+        );
+
+        // Delete links with node
+        graph.data[GRAPH_LINKS] = graph.data[GRAPH_LINKS].filter(
+            (l) =>
+                l[LINK_SOURCE][NODE_ID] !== nodeId &&
+                l[LINK_TARGET][NODE_ID] !== nodeId
+        );
+    },
+
+    stopPhysics() {
+        graph.data[GRAPH_NODES].forEach((n) => {
+            n.fx = n.x;
+            n.fy = n.y;
+        });
+    },
+
+    addIdentifiers() {
+        graph.data[GRAPH_NODES].forEach((n) => {
+            n.node = true;
+            n.link = false;
+        });
+        graph.data[GRAPH_LINKS].forEach((l) => {
+            l.node = false;
+            l.link = true;
+        });
+    },
+
+    deleteLink(sourceId, targetId) {
+        // Only keep links, of one of the nodes is different
+        graph.data[GRAPH_LINKS] = graph.data[GRAPH_LINKS].filter(
+            (l) =>
+                l[LINK_SOURCE][NODE_ID] !== sourceId ||
+                l[LINK_TARGET][NODE_ID] !== targetId
+        );
+    },
+
+    isLinkOnNode(link, node) {
+        if (link === undefined || node === undefined) {
+            return false;
+        }
+
+        if (link.link !== true || node.node !== true) {
+            return false;
+        }
+
+        return (
+            link[LINK_SOURCE][NODE_ID] === node[NODE_ID] ||
+            link[LINK_TARGET][NODE_ID] === node[NODE_ID]
+        );
+    },
+
+    existsLink(sourceId, targetId) {
+        const links = graph.data[GRAPH_LINKS];
+
+        for (var i = 0; i < links.length; i++) {
+            var link = links[i];
+            if (
+                link[LINK_SOURCE][NODE_ID] === sourceId &&
+                link[LINK_TARGET][NODE_ID] === targetId
+            ) {
+                return true;
+            }
+        }
+
+        return false;
+    },
+
+    connectNodes(sourceId, targetIds) {
+        targetIds.forEach((targetId) => {
+            if (
+                graph.existsLink(sourceId, targetId) ||
+                graph.existsLink(targetId, sourceId)
+            ) {
+                return;
+            }
+
+            var link = {};
+
+            link[LINK_SOURCE] = sourceId;
+            link[LINK_TARGET] = targetId;
+
+            graph.data[GRAPH_LINKS].push(link);
+        });
+    },
+
+    getCleanData() {
+        var cleanData = {};
+        cleanData[GRAPH_LINKS] = [];
+        cleanData[GRAPH_NODES] = [];
+
+        graph.data[GRAPH_LINKS].forEach((link) =>
+            cleanData[GRAPH_LINKS].push(graph.getCleanLink(link))
+        );
+
+        graph.data[GRAPH_NODES].forEach((node) =>
+            cleanData[GRAPH_NODES].push(graph.getCleanNode(node))
+        );
+
+        console.log(cleanData);
+        return cleanData;
+    },
+
+    getCleanNode(node) {
+        var cleanNode = {};
+
+        NODE_PARAMS.forEach((param) => {
+            cleanNode[param] = node[param];
+        });
+
+        return cleanNode;
+    },
+
+    getCleanLink(link) {
+        var cleanLink = {};
+
+        // Source and target nodes
+        // Node ids will be converted to complete node objects on running graphs, gotta convert back
+        cleanLink[LINK_SOURCE] = link[LINK_SOURCE][NODE_ID];
+        cleanLink[LINK_TARGET] = link[LINK_TARGET][NODE_ID];
+
+        // Other parameters
+        LINK_PARAMS.forEach((param) => {
+            cleanLink[param] = link[param];
+        });
+
+        return cleanLink;
+    },
+};
diff --git a/editor/js/state.js b/editor/js/state.js
new file mode 100644
index 0000000..dc020c2
--- /dev/null
+++ b/editor/js/state.js
@@ -0,0 +1,198 @@
+const TOOLS = {
+    select: new SelectTool("select"),
+    collect: new CollectTool("collect"),
+    delete: new DeleteTool("delete"),
+};
+
+const CONTEXT = {
+    node: "node",
+    link: "link",
+    mixed: "mixed",
+};
+
+class State extends Tool {
+    constructor() {
+        super("State");
+
+        this.display = new Display(TOOLS);
+
+        this.tool = undefined;
+        this.setTool(TOOLS.select);
+
+        // Shared variables
+        this.selectedItem = undefined;
+        this.selectedItems = new Set();
+        this.itemsContext = undefined;
+
+        this.keyStates = {};
+    }
+
+    setTool(tool) {
+        if (this.previousTool === tool) {
+            return;
+        }
+
+        this.previousTool = this.tool;
+        this.tool = tool;
+        this.display.setSelectedTool(tool);
+    }
+
+    setSelectedItem(item) {
+        this.selectedItem = item;
+        this.display.setSelectedItem(item);
+    }
+
+    addSelectedItem(item) {
+        this.selectedItems.add(item);
+        this.display.setSelectedItems(this.selectedItems, this.itemsContext);
+    }
+
+    removeSelectedItem(item) {
+        this.selectedItems.delete(item);
+        this.display.setSelectedItems(this.selectedItems, this.itemsContext);
+    }
+
+    clearSelectedItems() {
+        this.selectedItems.clear();
+        this.itemsContext = undefined;
+        this.display.setSelectedItems(this.selectedItems, this.itemsContext);
+    }
+
+    onNodeClick(node) {
+        this.tool.onNodeClick(node);
+    }
+
+    onLinkClick(link) {
+        this.tool.onLinkClick(link);
+    }
+
+    onKeyDown(key) {
+        var id = this.getKeyId(key);
+        var previous = this.keyStates[id];
+
+        this.keyStates[id] = true;
+
+        if (previous !== true) {
+            this.tool.onKeyDown(key);
+        }
+    }
+
+    onKeyUp(key) {
+        var id = this.getKeyId(key);
+        var previous = this.keyStates[id];
+
+        this.keyStates[id] = false;
+
+        if (previous !== false) {
+            this.tool.onKeyUp(key);
+        }
+    }
+
+    getKeyId(key) {
+        return key.keyCode;
+    }
+
+    nodeCanvasObject(node, ctx) {
+        var toolValue = this.tool.nodeCanvasObject(node, ctx);
+
+        if (toolValue !== undefined) {
+            return toolValue;
+        }
+
+        // TODO: Clean up function
+
+        // add ring just for highlighted nodes
+        if (this.selectedItem === node || this.selectedItems.has(node)) {
+            ctx.beginPath();
+            ctx.arc(node.x, node.y, 5 * 1.4, 0, 2 * Math.PI, false);
+            ctx.fillStyle = this.selectedItem === node ? "red" : "green";
+            ctx.fill();
+        }
+
+        // Draw image
+        if (node[NODE_IMAGE] !== undefined) {
+            var path = IMAGE_SRC + node[NODE_IMAGE];
+            var img = new Image();
+            img.src = path;
+
+            ctx.drawImage(
+                img,
+                node.x - IMAGE_SIZE / 2,
+                node.y - IMAGE_SIZE / 2,
+                IMAGE_SIZE,
+                IMAGE_SIZE
+            );
+        }
+
+        // TODO: Render label as always visible
+    }
+
+    nodePointerAreaPaint(node, color, ctx) {
+        var toolValue = this.tool.nodePointerAreaPaint(node, color, ctx);
+
+        if (toolValue !== undefined) {
+            return toolValue;
+        }
+
+        ctx.fillStyle = color;
+        ctx.fillRect(
+            node.x - IMAGE_SIZE / 2,
+            node.y - IMAGE_SIZE / 2,
+            IMAGE_SIZE,
+            IMAGE_SIZE
+        ); // draw square as pointer trap
+    }
+
+    nodeCanvasObjectMode(node) {
+        var toolValue = this.tool.nodeCanvasObjectMode(node);
+
+        if (toolValue !== undefined) {
+            return toolValue;
+        }
+
+        return "after";
+    }
+
+    linkWidth(link) {
+        var toolValue = this.tool.linkWidth(link);
+
+        if (toolValue !== undefined) {
+            return toolValue;
+        }
+
+        return this.isLinkHighlighted(link) ? 5 : 1;
+    }
+
+    linkDirectionalParticles() {
+        var toolValue = this.tool.linkDirectionalParticles();
+
+        if (toolValue !== undefined) {
+            return toolValue;
+        }
+
+        return 4;
+    }
+
+    linkDirectionalParticleWidth(link) {
+        var toolValue = this.tool.linkDirectionalParticleWidth(link);
+
+        if (toolValue !== undefined) {
+            return toolValue;
+        }
+
+        return this.isLinkHighlighted(link) ? LINK_PARTICLE_COUNT : 0;
+    }
+
+    redraw() {
+        this.display.setSelectedTool(this.tool);
+        this.display.setSelectedItem(this.selectedItem);
+        this.display.setSelectedItems(this.selectedItems, this.itemsContext);
+    }
+
+    isLinkHighlighted(link) {
+        return (
+            this.selectedItem === link ||
+            graph.isLinkOnNode(link, state.selectedItem)
+        );
+    }
+}
diff --git a/editor/js/tools/collecttool.js b/editor/js/tools/collecttool.js
new file mode 100644
index 0000000..5554a0f
--- /dev/null
+++ b/editor/js/tools/collecttool.js
@@ -0,0 +1,37 @@
+class CollectTool extends Tool {
+    constructor(key) {
+        super("Collect", key);
+    }
+
+    onNodeClick(node) {
+        if (state.itemsContext !== CONTEXT.node) {
+            state.clearSelectedItems();
+            state.itemsContext = CONTEXT.node;
+        }
+
+        if (state.selectedItems.has(node)) {
+            state.removeSelectedItem(node);
+        } else {
+            state.addSelectedItem(node);
+        }
+    }
+
+    onLinkClick(link) {
+        if (state.itemsContext !== CONTEXT.link) {
+            state.clearSelectedItems();
+            state.itemsContext = CONTEXT.link;
+        }
+
+        if (state.selectedItems.has(link)) {
+            state.removeSelectedItem(link);
+        } else {
+            state.addSelectedItem(link);
+        }
+    }
+
+    onKeyUp(key) {
+        if (key.keyCode === 17) {
+            state.setTool(state.previousTool);
+        }
+    }
+}
\ No newline at end of file
diff --git a/editor/js/tools/deletetool.js b/editor/js/tools/deletetool.js
new file mode 100644
index 0000000..6f99000
--- /dev/null
+++ b/editor/js/tools/deletetool.js
@@ -0,0 +1,13 @@
+class DeleteTool extends Tool {
+    constructor(key) {
+        super("Delete", key);
+    }
+
+    onNodeClick(node) {
+        graph.deleteNode(node[NODE_ID]);
+    }
+
+    onLinkClick(link) {
+        graph.deleteLink(link[LINK_SOURCE][NODE_ID], link[LINK_TARGET][NODE_ID]);
+    }
+}
\ No newline at end of file
diff --git a/editor/js/tools/selecttool.js b/editor/js/tools/selecttool.js
new file mode 100644
index 0000000..4c4b427
--- /dev/null
+++ b/editor/js/tools/selecttool.js
@@ -0,0 +1,19 @@
+class SelectTool extends Tool {
+    constructor(key) {
+        super("Select", key);
+    }
+
+    onNodeClick(node) {
+        state.setSelectedItem(node);
+    }
+
+    onLinkClick(link) {
+        state.setSelectedItem(link);
+    }
+
+    onKeyDown(key) {
+        if (key.keyCode === 17) {
+            state.setTool(TOOLS.collect);
+        }
+    }
+}
\ No newline at end of file
diff --git a/editor/js/tools/tool.js b/editor/js/tools/tool.js
new file mode 100644
index 0000000..1b58655
--- /dev/null
+++ b/editor/js/tools/tool.js
@@ -0,0 +1,77 @@
+class Tool {
+    constructor(name, key) {
+        this.name = name;
+        this.key = key;
+        this.warnings = false;
+    }
+
+    getName() {
+        return this.name;
+    }
+
+    getKey() {
+        return this.key;
+    }
+
+    onNodeClick(node) {
+        if (this.warnings) {
+            console.warn('Method "onNodeClick" not implemented.');
+        }
+    }
+
+    onLinkClick(link) {
+        if (this.warnings) {
+            console.warn('Method "onLinkClick" not implemented.');
+        }
+    }
+
+    onKeyDown(key) {
+        if (this.warnings) {
+            console.warn('Method "onKeyDown" not implemented.');
+        }
+    }
+
+    onKeyUp(key) {
+        if (this.warnings) {
+            console.warn('Method "onKeyUp" not implemented.');
+        }
+    }
+
+    nodeCanvasObject(node, ctx) {
+        if (this.warnings) {
+            console.warn('Method "nodeCanvasObject" not implemented.');
+        }
+    }
+
+    nodeCanvasObjectMode(node) {
+        if (this.warnings) {
+            console.warn('Method "nodeCanvasObjectMode" not implemented.');
+        }
+    }
+
+    nodePointerAreaPaint(node, color, ctx) {
+        if (this.warnings) {
+            console.warn('Method "nodePointerAreaPaint" not implemented.');
+        }
+    }
+
+    linkWidth(link) {
+        if (this.warnings) {
+            console.warn('Method "linkWidth" not implemented.');
+        }
+    }
+
+    linkDirectionalParticles() {
+        if (this.warnings) {
+            console.warn('Method "linkDirectionalParticles" not implemented.');
+        }
+    }
+
+    linkDirectionalParticleWidth(link) {
+        if (this.warnings) {
+            console.warn(
+                'Method "linkDirectionalParticleWidth" not implemented.'
+            );
+        }
+    }
+}
diff --git a/knowledge-space.php b/knowledge-space.php
index b03271b..c9f5abd 100644
--- a/knowledge-space.php
+++ b/knowledge-space.php
@@ -25,6 +25,20 @@ function ks_add_graph(): string
     return $three . $renderer .$renderer2 . $graph . $div . $variables . $script;
 }
 
+function ks_add_editor(): string
+{
+    // Proper, secure script loading in the future
+    // Reference https://stackoverflow.com/a/16823761/7376120
+    wp_enqueue_script('jquery');
+
+    $plugin_url = plugin_dir_url(__FILE__);
+
+    $raw_html = file_get_contents(__DIR__.DIRECTORY_SEPARATOR."editor".DIRECTORY_SEPARATOR."editor.html");
+    $ready_html = str_replace("%WWW%", $plugin_url, $raw_html);
+
+    return $ready_html;
+}
+
 function kg_load_css() {
     $plugin_dir = plugin_dir_url(__FILE__);
     wp_enqueue_style('kg-style', $plugin_dir.'kg-style.css');
@@ -32,3 +46,4 @@ function kg_load_css() {
 
 add_action('wp_enqueue_scripts', 'kg_load_css');
 add_shortcode('knowledge-space', 'ks_add_graph');
+add_shortcode('knowledge-space-editor', 'ks_add_editor');
-- 
GitLab