diff --git a/datasets/aud1.json b/datasets/aud1.json
index 21d1d7a70ce5c426bcab53c5caa6e3fa9dcf1366..a55efdc2527ba30a801b1ae690ece920fff3193a 100644
--- a/datasets/aud1.json
+++ b/datasets/aud1.json
@@ -114,11 +114,6 @@
       "target": "id6",
       "type": "Inhalt"
     },
-    {
-      "source":  "id2",
-      "target": "id6",
-      "type": "Thema"
-    },
     {
       "source":  "id9",
       "target": "id10",
diff --git a/editor/editor.html b/editor/editor.html
new file mode 100644
index 0000000000000000000000000000000000000000..65de428c39875d03bae0623414e3dacc353e65ba
--- /dev/null
+++ b/editor/editor.html
@@ -0,0 +1,59 @@
+<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/tools/addnodetool.js"></script>
+    <script src="%WWW%editor/js/tools/connecttool.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/addnode.png b/editor/images/tools/addnode.png
new file mode 100644
index 0000000000000000000000000000000000000000..a899e0a587dd2538612438fbac1f40e060ece5b0
Binary files /dev/null and b/editor/images/tools/addnode.png differ
diff --git a/editor/images/tools/collect.png b/editor/images/tools/collect.png
new file mode 100644
index 0000000000000000000000000000000000000000..623d3a978005133e87994a16e67dbe9ab7259875
Binary files /dev/null and b/editor/images/tools/collect.png differ
diff --git a/editor/images/tools/connect.png b/editor/images/tools/connect.png
new file mode 100644
index 0000000000000000000000000000000000000000..0cb1991878d15423eaa17ffaf0d4379a2bedc88d
Binary files /dev/null and b/editor/images/tools/connect.png differ
diff --git a/editor/images/tools/delete.png b/editor/images/tools/delete.png
new file mode 100644
index 0000000000000000000000000000000000000000..007f69a325d89d0d5e4eee3d2b0df7f8844c4d13
Binary files /dev/null and b/editor/images/tools/delete.png differ
diff --git a/editor/images/tools/select.png b/editor/images/tools/select.png
new file mode 100644
index 0000000000000000000000000000000000000000..840c4f5df6c058d208879fe7d5bfe906eab43015
Binary files /dev/null and b/editor/images/tools/select.png differ
diff --git a/editor/js/display.js b/editor/js/display.js
new file mode 100644
index 0000000000000000000000000000000000000000..5c2446ca1cd8efdb606b125d83289fdb730c354f
--- /dev/null
+++ b/editor/js/display.js
@@ -0,0 +1,146 @@
+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.getIcon() +
+            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 === undefined) {
+            params = [];
+        } else 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 !== undefined && 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 !== undefined && 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 === undefined) {
+            return "UNDEFINED";
+        }
+
+        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 0000000000000000000000000000000000000000..be00eeaa97d648f69c25a889797f75e267be6b66
--- /dev/null
+++ b/editor/js/editor.js
@@ -0,0 +1,70 @@
+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 extractPositions(event) {
+    return {
+        graph: graphObj.screen2GraphCoords(event.layerX, event.layerY),
+        window: { x: event.clientX, y: event.clientY },
+    };
+}
+
+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)
+        )
+        .onBackgroundClick((event) =>
+            state.onBackgroundClick(event, extractPositions(event))
+        )
+        .nodeCanvasObjectMode((node) => state.nodeCanvasObjectMode(node))
+        .nodeCanvasObject((node, ctx) => state.nodeCanvasObject(node, ctx))
+        .onLinkClick((link) => state.onLinkClick(link));
+
+    graph.externUpdate.push(() => {
+        graphObj.graphData(graph.data);
+    });
+}
diff --git a/editor/js/graph.js b/editor/js/graph.js
new file mode 100644
index 0000000000000000000000000000000000000000..0f986084256fba9743ed04b3f8dcb7f6dc5b340c
--- /dev/null
+++ b/editor/js/graph.js
@@ -0,0 +1,254 @@
+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,
+    externUpdate: [],   // Register callbacks in this list
+
+    update() {
+        graph.externUpdate.forEach((fn) => fn());
+    },
+
+    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;
+    },
+
+    existsNodeId(nodeId) {
+        var nodes = graph.data[GRAPH_NODES];
+        for (var i = 0; i < nodes.length; i++) {
+            if (nodes[i][NODE_ID] === nodeId) {
+                return true;
+            }
+        }
+        return false;
+    },
+
+    getUnusedNodeId() {
+        var id;
+        do {
+            id = graph.getRandomString();
+        } while (graph.existsNodeId(id));
+        return id;
+    },
+
+    getRandomString(length = 8) {
+        // Based on: https://stackoverflow.com/a/1349426/7376120
+        var characters =
+            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+        var charactersLength = characters.length;
+
+        var result = "";
+        for (var i = 0; i < length; i++) {
+            result += characters.charAt(
+                Math.floor(Math.random() * charactersLength)
+            );
+        }
+        return result;
+    },
+
+    addLink(sourceId, targetId, linkDetails = {}) {
+        // Copy params
+        var newLink = linkDetails;
+
+        // Make sure the IDs exist
+        if (
+            sourceId === undefined ||
+            targetId === undefined ||
+            graph.existsNodeId(sourceId) === false ||
+            graph.existsNodeId(targetId) === false
+        ) {
+            return;
+        }
+
+
+        // Make sure the link is unique
+        if (graph.existsLink(sourceId, targetId)) {
+            return;
+        }
+
+        newLink[LINK_SOURCE] = sourceId;
+        newLink[LINK_TARGET] = targetId;
+
+        // Basic node properties
+        newLink.link = true;
+        newLink.node = false;
+
+        // Add node
+        graph.data[GRAPH_LINKS].push(newLink);
+        graph.update();
+
+        return newLink;
+    },
+
+    addNode(nodeDetails) {
+        // Copy params
+        var newNode = nodeDetails;
+
+        // Make sure the ID is set and unique
+        if (newNode[NODE_ID] === undefined) {
+            newNode[NODE_ID] = graph.getUnusedNodeId();
+        } else if (graph.existsNodeId(newNode[NODE_ID])) {
+            return;
+        }
+
+        // Basic node properties
+        newNode.node = true;
+        newNode.link = false;
+
+        // Add node
+        graph.data[GRAPH_NODES].push(newNode);
+        graph.update();
+
+        return newNode;
+    },
+};
diff --git a/editor/js/state.js b/editor/js/state.js
new file mode 100644
index 0000000000000000000000000000000000000000..4d46c5e7c8975ad633d4fb0fd992280d440d55b8
--- /dev/null
+++ b/editor/js/state.js
@@ -0,0 +1,204 @@
+const TOOLS = {
+    select: new SelectTool("select"),
+    collect: new CollectTool("collect"),
+    delete: new DeleteTool("delete"),
+    addnode: new AddNodeTool("addnode"),
+    connect: new ConnectTool("connect"),
+};
+
+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.tool === 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;
+    }
+
+    onBackgroundClick(event, positions) {
+        this.tool.onBackgroundClick(event, positions);
+    }
+
+    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/addnodetool.js b/editor/js/tools/addnodetool.js
new file mode 100644
index 0000000000000000000000000000000000000000..c72c189d8ac795d2ab22be6553476e07dc359a76
--- /dev/null
+++ b/editor/js/tools/addnodetool.js
@@ -0,0 +1,22 @@
+class AddNodeTool extends Tool {
+    constructor(key) {
+        super("Add node", "addnode", key);
+    }
+
+    onBackgroundClick(event, positions) {
+        var node = {};
+
+        // Set position
+        node.fx = positions.graph.x;
+        node.fy = positions.graph.y;
+
+        var node = graph.addNode(node);
+
+        if (node === undefined) {
+            console.error("Couldn't add new node");
+            return;
+        }
+
+        state.setSelectedItem(node);
+    }
+}
\ No newline at end of file
diff --git a/editor/js/tools/collecttool.js b/editor/js/tools/collecttool.js
new file mode 100644
index 0000000000000000000000000000000000000000..fa2ab96c0371bcccf9f1202d30920c93820832d7
--- /dev/null
+++ b/editor/js/tools/collecttool.js
@@ -0,0 +1,37 @@
+class CollectTool extends Tool {
+    constructor(key) {
+        super("Collect", "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/connecttool.js b/editor/js/tools/connecttool.js
new file mode 100644
index 0000000000000000000000000000000000000000..cd47b504f0ed047920382266d6deb7d6c15d769a
--- /dev/null
+++ b/editor/js/tools/connecttool.js
@@ -0,0 +1,46 @@
+const KEEP_SOURCE_KEY_ID = 17;
+
+class ConnectTool extends Tool {
+    constructor(key) {
+        super("Connect two nodes", "connect", key);
+        this.keepSource = false;
+    }
+
+    onNodeClick(node) {
+        // Is a first node selected?
+        if (state.selectedItem === undefined || state.selectedItem.node === false) {
+            state.setSelectedItem(node);
+            return;
+        }
+
+        // Add new link
+        var link = graph.addLink(state.selectedItem[NODE_ID], node[NODE_ID]);
+
+        if (link === undefined) {
+            console.error("Could not create new link");
+            return;
+        }
+
+        if (this.keepSource === false) {
+            // Deselect the current first node
+            // TODO: Returned object not yet converted to normal one
+            state.setSelectedItem(link);
+        }
+    }
+
+    onBackgroundClick(event, positions) {
+        state.setSelectedItem(undefined);
+    }
+
+    onKeyDown(key) {
+        if (key.keyCode === KEEP_SOURCE_KEY_ID) {
+            this.keepSource = true;
+        }
+    }
+
+    onKeyUp(key) {
+        if (key.keyCode === KEEP_SOURCE_KEY_ID) {
+            this.keepSource = false;
+        }
+    }
+}
\ 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 0000000000000000000000000000000000000000..35110df55b173e77ffa172fd710974f81388f3b4
--- /dev/null
+++ b/editor/js/tools/deletetool.js
@@ -0,0 +1,13 @@
+class DeleteTool extends Tool {
+    constructor(key) {
+        super("Delete", "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 0000000000000000000000000000000000000000..24b518f1c77515666039d381ecab15d3fe06c731
--- /dev/null
+++ b/editor/js/tools/selecttool.js
@@ -0,0 +1,23 @@
+class SelectTool extends Tool {
+    constructor(key) {
+        super("Select", "select", key);
+    }
+
+    onNodeClick(node) {
+        state.setSelectedItem(node);
+    }
+
+    onLinkClick(link) {
+        state.setSelectedItem(link);
+    }
+
+    onBackgroundClick(event, positions) {
+        state.setSelectedItem(undefined);
+    }
+
+    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 0000000000000000000000000000000000000000..babede8b2a1a6d6a1b148d7e45fd046fcbf7665f
--- /dev/null
+++ b/editor/js/tools/tool.js
@@ -0,0 +1,90 @@
+class Tool {
+    constructor(name, icon, key) {
+        this.name = name;
+        this.icon = icon;
+        this.key = key;
+        this.warnings = false;
+    }
+
+    getName() {
+        return this.name;
+    }
+
+    getKey() {
+        return this.key;
+    }
+
+    getIcon() {
+        return this.icon;
+    }
+
+    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.'
+            );
+        }
+    }
+
+    onBackgroundClick(event, positions) {
+        if (this.warnings) {
+            console.warn(
+                'Method "onBackgroundClick" not implemented.'
+            );
+        }
+    }
+}
diff --git a/graph.js b/graph.js
index 54f346c2bca18d86f48ddb29a61b28d5405d01f9..5cea13c0ac590c174f26bf1a4ffb842bd88e6763 100644
--- a/graph.js
+++ b/graph.js
@@ -7,6 +7,7 @@ let firstTick = true;
 const colorPallette = ['rgb(104, 169, 77)', 'rgb(102, 75, 154)', 'rgb(41, 171, 226)', 'rgb(224, 133, 35)',
     'rgb(214, 207, 126)', 'rgb(239, 65, 35)', 'rgb(255, 255, 255)'];
 const edgeColors = {};
+var graph_element = document.getElementById('3d-graph');
 
 loadGraph();
 
@@ -29,7 +30,7 @@ async function loadGraph() {
         .nodeThreeObject(drawNode)
         .onEngineTick(loadComponents)
         .width(getWidth())
-        .height(969);
+        .height(getHeight());
     //Graph.renderer().sortObjects = true;
 }
 
@@ -43,14 +44,24 @@ function loadComponents() {
         createInfoOverlay();
         add_fullscreen_mode_button();
         add_fullscreen_Listener();
+        window.onresize = resize_graph();
         firstTick = false;
     }
 }
 
+function resize_graph() {
+    Graph.width(getWidth());
+    Graph.height(getHeight());
+}
+
 function getWidth() {
     return document.getElementById('3d-graph').offsetWidth;
 }
 
+function getHeight() {
+    return window.innerHeight - 200
+}
+
 function mapEdgeColors() {
     const linkClasses = getLinkClasses()
     for (let i = 0; i < linkClasses.length; i++) {
@@ -236,10 +247,10 @@ function fullscreen_mode(){
 
 function resize_canvas() {
     if(document.fullscreenElement == getCanvasDivNode()) {
-        Graph.height(getCanvasDivNode().height);
-        Graph.width(getCanvasDivNode().width);
+        Graph.height(screen.height);
+        Graph.width(screen.width);
     } else {
-        Graph.height(969);
+        Graph.height(window.innerHeight - 200);
         Graph.width(getWidth());
     }
 }
diff --git a/knowledge-space.php b/knowledge-space.php
index b03271b7dd01d9c0f8afc1175a4a788e4eeba63f..c9f5abddfec9eaa94051c5d975d75a21688d93ab 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');