From 9aeda83e89f25470430366bce480bd52f949fe3f Mon Sep 17 00:00:00 2001
From: Matthias Konitzny <konitzny@ibr.cs.tu-bs.de>
Date: Thu, 8 Sep 2022 18:33:54 +0200
Subject: [PATCH] Created new merged structures. Still lots of refactoring
 left.

---
 src/common/datasets.js                        |   6 +-
 src/common/graph.ts                           | 318 ++++++++++++------
 src/common/graphelement.ts                    |  38 +++
 src/common/history.ts                         |  59 ++++
 src/common/link.ts                            | 105 ++++++
 src/common/node.ts                            | 177 ++++++++++
 src/common/nodetype.ts                        |  47 +++
 src/common/serializableitem.ts                |  30 ++
 .../components/nodefilter/filtermenu.tsx      |   2 +-
 src/display/components/nodeinfo/neighbors.tsx |   2 +-
 .../components/nodeinfo/nodeinfobar.tsx       |   2 +-
 src/display/display.tsx                       |   4 +-
 src/display/renderer.tsx                      |   2 +-
 src/editor/js/components/editor.tsx           |   6 +-
 src/editor/js/components/nodedetails.tsx      |   4 +-
 src/editor/js/components/nodetypeentry.tsx    |   2 +-
 src/editor/js/components/nodetypeseditor.tsx  |   2 +-
 src/editor/js/structures/graph/graph.ts       |  45 ++-
 .../js/structures/graph/graphelement.ts       |  65 ----
 src/editor/js/structures/graph/link.ts        | 144 --------
 src/editor/js/structures/graph/node.ts        | 182 ----------
 src/editor/js/structures/graph/nodetype.ts    |  60 ----
 .../js/structures/helper/serializableitem.ts  |  66 ----
 src/editor/js/structures/manageddata.ts       |  61 ++--
 24 files changed, 736 insertions(+), 693 deletions(-)
 create mode 100644 src/common/graphelement.ts
 create mode 100644 src/common/history.ts
 create mode 100644 src/common/link.ts
 create mode 100644 src/common/node.ts
 create mode 100644 src/common/nodetype.ts
 create mode 100644 src/common/serializableitem.ts
 delete mode 100644 src/editor/js/structures/graph/graphelement.ts
 delete mode 100644 src/editor/js/structures/graph/link.ts
 delete mode 100644 src/editor/js/structures/graph/node.ts
 delete mode 100644 src/editor/js/structures/graph/nodetype.ts
 delete mode 100644 src/editor/js/structures/helper/serializableitem.ts

diff --git a/src/common/datasets.js b/src/common/datasets.js
index a7d253e..8aa9d68 100644
--- a/src/common/datasets.js
+++ b/src/common/datasets.js
@@ -35,15 +35,15 @@ export function loadGraphJson(spaceId) {
  * Takes the graph json object and stores it in the backend.
  *
  * @param {String} spaceId Identification of graph to save.
- * @param {object} json Graph object
+ * @param {object} object Graph object
  *
  * @returns Promise returning state of query.
  */
-export function saveGraphJson(spaceId, json) {
+export function saveGraphJson(spaceId, object) {
     const data = new FormData();
     data.append("action", "update_space");
     data.append("space", spaceId);
-    data.append("graph", JSON.stringify(json));
+    data.append("graph", JSON.stringify(object));
 
     return ajaxCall(data);
 }
diff --git a/src/common/graph.ts b/src/common/graph.ts
index f846315..ee23da5 100644
--- a/src/common/graph.ts
+++ b/src/common/graph.ts
@@ -1,44 +1,8 @@
 import * as Config from "../config";
-
-interface LinkData {
-    source: number;
-    target: number;
-    type?: string;
-}
-
-export interface Link {
-    source: Node;
-    target: Node;
-    type?: GraphObjectType;
-}
-
-interface NodeContent {
-    name: string;
-    description?: string;
-    icon?: string;
-    banner?: string;
-    video?: string;
-    references?: string[];
-}
-
-interface NodeData extends NodeContent {
-    id: number;
-    type?: string;
-}
-
-export interface Node extends NodeContent {
-    id: number;
-    type: GraphObjectType;
-
-    neighbors: Node[];
-    links: Link[];
-}
-
-export interface GraphObjectType {
-    id: number;
-    name: string;
-    color?: string;
-}
+import { Node, NodeData, SimNodeData } from "./node";
+import { Link, LinkData, SimLinkData } from "./link";
+import { NodeType, NodeTypeData } from "./nodetype";
+import { SerializableItem } from "./serializableitem";
 
 export interface Coordinate {
     x: number;
@@ -46,73 +10,174 @@ export interface Coordinate {
     z: number;
 }
 
+export interface GraphData {
+    nodes: NodeData[];
+    links: LinkData[];
+    objectGroups?: NodeTypeData[];
+}
+
+export interface SimGraphData {
+    nodes: SimNodeData[];
+    links: SimLinkData[];
+    objectGroups: NodeTypeData[];
+}
+
 export interface GraphContent {
     nodes: Node[];
     links: Link[];
-    objectGroups: GraphObjectType[];
+    objectGroups?: NodeType[];
 }
 
 /**
  * Basic graph data structure.
  */
-export default class Graph implements GraphContent {
+export class Graph
+    extends SerializableItem<GraphData, SimGraphData>
+    implements GraphContent
+{
     public nodes: Node[];
     public links: Link[];
-    public objectGroups: GraphObjectType[];
-    public nameToObjectGroup: Map<string, GraphObjectType>;
+    public objectGroups: NodeType[];
+    public nameToObjectGroup: Map<string, NodeType>;
+    public initialized: boolean;
+
     private idToNode: Map<number, Node>;
+    private idToLink: Map<number, Link>;
 
-    constructor(
-        nodes: NodeData[],
-        links: LinkData[],
-        objectGroups?: GraphObjectType[]
-    ) {
-        this.objectGroups = objectGroups ?? this.createObjectGroups(nodes);
+    /**
+     * Creates a new Graph object.
+     * Make sure the nodes and links are connected to the correct objects before calling this method!
+     */
+    constructor(data?: GraphContent) {
+        super(0);
+
+        if (data === undefined) {
+            this.initialized = false;
+            return;
+        }
 
-        this.nameToObjectGroup = new Map<string, GraphObjectType>();
+        Object.assign(this, data);
+
+        this.nameToObjectGroup = new Map<string, NodeType>();
         this.objectGroups.forEach((group) =>
             this.nameToObjectGroup.set(group.name, group)
         );
 
-        this.createNodes(nodes);
+        this.idToNode = new Map<number, Node>();
+        this.nodes.forEach((node) => {
+            this.idToNode.set(node.id, node);
+        });
 
-        this.links = links.map((link) => {
-            return {
-                source: this.idToNode.get(link.source),
-                target: this.idToNode.get(link.target),
-            };
+        this.idToLink = new Map<number, Link>();
+        this.links.forEach((link) => {
+            this.idToLink.set(link.id, link);
         });
+    }
 
-        this.updateNodeData();
-        this.removeFloatingNodes();
+    public toJSONSerializableObject(): GraphData {
+        return {
+            nodes: this.nodes.map((node) => node.toJSONSerializableObject()),
+            links: this.links.map((link) => link.toJSONSerializableObject()),
+            objectGroups: this.objectGroups.map((group) =>
+                group.toJSONSerializableObject()
+            ),
+        };
     }
 
-    private createNodes(nodes: NodeData[]) {
-        this.nodes = [];
-        for (const nodeData of nodes) {
-            const { type, ...nodeVars } = nodeData;
-            const node = { ...nodeVars } as Node;
-            node.type = this.nameToObjectGroup.get(type);
-            node.neighbors = [];
-            node.links = [];
-            this.nodes.push(node);
+    public toHistorySerializableObject(): SimGraphData {
+        return {
+            nodes: this.nodes.map((node) => node.toHistorySerializableObject()),
+            links: this.links.map((link) => link.toHistorySerializableObject()),
+            objectGroups: this.objectGroups.map((group) =>
+                group.toHistorySerializableObject()
+            ),
+        };
+    }
+
+    public fromSerializedObject(data: GraphData | SimGraphData): Graph {
+        let objectGroups: Array<NodeType>;
+
+        if (data.objectGroups === undefined) {
+            objectGroups = this.createObjectGroupsFromStrings(data.nodes);
+        } else {
+            objectGroups = this.createObjectGroupsFromObjects(
+                data.objectGroups
+            );
         }
 
+        this.nameToObjectGroup = new Map<string, NodeType>();
+        objectGroups.forEach((group) =>
+            this.nameToObjectGroup.set(group.name, group)
+        );
+
+        this.nodes = this.createNodes(data.nodes);
         this.idToNode = new Map<number, Node>();
         this.nodes.forEach((node) => {
             this.idToNode.set(node.id, node);
         });
+
+        this.links = data.links.map((link, i) => {
+            const l = new Link();
+            l.id = i;
+            l.source = this.idToNode.get(link.source);
+            l.target = this.idToNode.get(link.target);
+            return l;
+        });
+        this.idToLink = new Map<number, Link>();
+        this.links.forEach((link) => {
+            this.idToLink.set(link.id, link);
+        });
+
+        this.updateNodeData();
+
+        return this;
     }
 
-    private removeFloatingNodes() {
-        this.nodes = this.nodes.filter((node) => node.neighbors.length > 0);
+    private createNodes(nodeJSONData: NodeData[]): Array<Node> {
+        const nodes: Array<Node> = [];
+        for (const nodeData of nodeJSONData) {
+            const node = new Node();
+            node.fromSerializedObject(nodeData);
+            node.type = this.nameToObjectGroup.get(nodeData.type);
+            node.neighbors = [];
+            node.links = [];
+            nodes.push(node);
+        }
+        return nodes;
+    }
+
+    private createObjectGroupsFromStrings(nodes: NodeData[]): Array<NodeType> {
+        const objectGroups: NodeType[] = [];
+        const nodeClasses: string[] = [];
+        nodes.forEach((node) => nodeClasses.push(node.type));
+        const nodeTypes = [...new Set(nodeClasses)].map((c) => String(c));
+
+        for (let i = 0; i < nodeTypes.length; i++) {
+            const nodeType = new NodeType();
+            nodeType.fromSerializedObject({
+                id: i,
+                name: nodeTypes[i],
+                color: Config.COLOR_PALETTE[i % Config.COLOR_PALETTE.length],
+            });
+        }
+        return objectGroups;
+    }
+
+    private createObjectGroupsFromObjects(
+        groups: NodeTypeData[]
+    ): Array<NodeType> {
+        return groups.map((group) => {
+            const t = new NodeType();
+            t.fromSerializedObject(group);
+            return t;
+        });
     }
 
     /**
      * Updates the graph data structure to contain additional values.
      * Creates a 'neighbors' and 'links' array for each node object.
      */
-    private updateNodeData() {
+    private updateNodeData(): Link[] {
         this.links.forEach((link) => {
             const a = link.source;
             const b = link.target;
@@ -121,26 +186,90 @@ export default class Graph implements GraphContent {
             a.links.push(link);
             b.links.push(link);
         });
+        return this.links;
+    }
+
+    public removeFloatingNodes() {
+        this.nodes = this.nodes.filter((node) => node.neighbors.length > 0);
     }
 
     public node(id: number): Node {
         return this.idToNode.get(id);
     }
 
-    private createObjectGroups(nodes: NodeData[]): GraphObjectType[] {
-        const objectGroups: GraphObjectType[] = [];
-        const nodeClasses: string[] = [];
-        nodes.forEach((node) => nodeClasses.push(node.type));
-        const nodeTypes = [...new Set(nodeClasses)].map((c) => String(c));
+    private checkNode(node: Node) {
+        for (const neighbor of node.neighbors) {
+            if (this.idToNode.get(neighbor.id) === undefined) {
+                return false;
+            }
+        }
 
-        for (let i = 0; i < nodeTypes.length; i++) {
-            objectGroups.push({
-                id: undefined,  // Does not matter for display graph
-                name: nodeTypes[i],
-                color: Config.COLOR_PALETTE[i % Config.COLOR_PALETTE.length],
-            });
+        for (const link of node.links) {
+            if (this.idToLink.get(link.id) === undefined) {
+                return false;
+            }
+        }
+        return node.isInitialized();
+    }
+
+    private checkLink(link: Link) {
+        return (
+            link.isInitialized() &&
+            !(
+                this.idToNode.get(link.source.id) === undefined ||
+                this.idToNode.get(link.target.id) === undefined
+            )
+        );
+    }
+
+    public addNode(node: Node) {
+        node.id = this.nodes.length;
+
+        if (!this.checkNode(node)) {
+            return false;
+        }
+
+        this.nodes.push(node);
+        this.idToNode.set(node.id, node);
+        return true;
+    }
+
+    public addLink(link: Link) {
+        link.id = this.links.length;
+
+        if (!this.checkLink(link)) {
+            return false;
+        }
+
+        this.links.push(link);
+        this.idToLink.set(link.id, link);
+        return true;
+    }
+
+    public deleteLink(id: number) {
+        this.links = this.links.filter((l: Link) => l.id !== id);
+        this.idToLink.delete(id);
+    }
+
+    public deleteNode(id: number) {
+        const node = this.idToNode.get(id);
+        this.idToNode.delete(id);
+
+        for (const link of node.links) {
+            this.deleteLink(link.id);
+        }
+        this.nodes = this.nodes.filter((n: Node) => n.id !== id);
+    }
+
+    public deleteNodeType(id: string) {
+        // TODO: Change to id/number
+        const nodeType = this.nameToObjectGroup.get(id);
+
+        for (const node of this.nodes) {
+            if (node.type.id === nodeType.id) {
+                node.type = undefined;
+            }
         }
-        return objectGroups;
     }
 
     public view(
@@ -165,23 +294,10 @@ export default class Graph implements GraphContent {
                 nodeTypes.get(l.target.type.name)
         );
 
-        // Convert to data objects and create new graph.
-        // Using spread syntax to simplify object copying.
-        return new Graph(
-            nodes.map((node) => {
-                // eslint-disable-next-line no-unused-vars
-                const { type, neighbors, links, ...nodeVars } = node;
-                const nodeData = { ...nodeVars } as NodeData;
-                nodeData.type = type.name;
-                return nodeData;
-            }),
-            links.map((link) => {
-                return {
-                    source: link.source.id,
-                    target: link.target.id,
-                };
-            }),
-            this.objectGroups
-        );
+        return new Graph({
+            nodes: nodes,
+            links: links,
+            objectGroups: this.objectGroups,
+        });
     }
 }
diff --git a/src/common/graphelement.ts b/src/common/graphelement.ts
new file mode 100644
index 0000000..199107a
--- /dev/null
+++ b/src/common/graphelement.ts
@@ -0,0 +1,38 @@
+import { Graph } from "./graph";
+import { SerializableItem } from "./serializableitem";
+
+export class GraphElement<JSONType, HistoryType> extends SerializableItem<
+    JSONType,
+    HistoryType
+> {
+    public graph: Graph;
+
+    constructor(id = -1, graph: Graph = undefined) {
+        super(id);
+        this.graph = graph;
+    }
+
+    /**
+     * Removes element from its parent graph.
+     * @returns True, if successful.
+     */
+    public delete() {
+        throw new Error('Function "delete()" has not been implemented.');
+    }
+
+    public isInitialized(): boolean {
+        return this.id != -1;
+    }
+
+    public toString(): string {
+        throw new Error('Function "toString()" has not been implemented.');
+    }
+
+    /**
+     * Compares two objects. Can be a custom implementation.
+     * @returns True, if given object is identical.
+     */
+    public equals(other: GraphElement<JSONType, HistoryType>): boolean {
+        return other.constructor == this.constructor && other.id == this.id;
+    }
+}
diff --git a/src/common/history.ts b/src/common/history.ts
new file mode 100644
index 0000000..966489b
--- /dev/null
+++ b/src/common/history.ts
@@ -0,0 +1,59 @@
+import { SerializableItem } from "./serializableitem";
+
+interface SavePoint<DataType> {
+    description: string;
+    data: DataType;
+}
+
+export class History<HistoryDataType> {
+    public maxCheckpoints: number;
+    public currentCheckpoint: number;
+
+    private data: SerializableItem<never, HistoryDataType>;
+    private checkpoints: SavePoint<HistoryDataType>[];
+
+    constructor(
+        data: SerializableItem<never, HistoryDataType>,
+        maxCheckpoints = 20
+    ) {
+        this.data = data;
+        this.maxCheckpoints = maxCheckpoints;
+        this.checkpoints = [];
+        this.currentCheckpoint = -1;
+        this.checkpoint("New History");
+    }
+
+    checkpoint(description: string) {
+        const checkpointData = this.data.toHistorySerializableObject();
+        const checkpoint = {
+            description: description,
+            data: JSON.parse(JSON.stringify(checkpointData)), // deepcopy
+        };
+
+        // Remove potential history which is not relevant anymore (maybe caused by undo ops)
+        this.currentCheckpoint++;
+        this.checkpoints.length = this.currentCheckpoint;
+
+        this.checkpoints.push(checkpoint);
+    }
+
+    historyDescription(): Array<string> {
+        return this.checkpoints.map((savepoint) => savepoint.description);
+    }
+
+    undo(): SavePoint<HistoryDataType> {
+        if (this.currentCheckpoint > 0) {
+            return this.checkpoints[this.currentCheckpoint--];
+        } else {
+            return this.checkpoints[0];
+        }
+    }
+
+    redo(): SavePoint<HistoryDataType> {
+        if (this.currentCheckpoint < this.checkpoints.length) {
+            return this.checkpoints[this.currentCheckpoint++];
+        } else {
+            return this.checkpoints[this.checkpoints.length - 1];
+        }
+    }
+}
diff --git a/src/common/link.ts b/src/common/link.ts
new file mode 100644
index 0000000..78d6324
--- /dev/null
+++ b/src/common/link.ts
@@ -0,0 +1,105 @@
+import { GraphElement } from "./graphelement";
+import { Node } from "./node";
+import { NodeType } from "./nodetype";
+import { Graph } from "./graph";
+
+export interface LinkData {
+    source: number;
+    target: number;
+    type?: string;
+}
+
+export interface SimLinkData extends LinkData {
+    index: number;
+}
+
+export interface GraphLink {
+    id: number;
+    source: Node;
+    target: Node;
+    type?: NodeType;
+
+    // Properties used by the force graph simulation
+    index?: number;
+}
+
+export class Link
+    extends GraphElement<LinkData, SimLinkData>
+    implements GraphLink
+{
+    public source: Node;
+    public target: Node;
+
+    type?: NodeType;
+
+    // These parameters will be added by the force graph implementation
+    public index?: number;
+
+    constructor(graph: Graph = undefined) {
+        super(0, graph);
+    }
+
+    /**
+     * Id of the source node.
+     * @returns Source id.
+     */
+    public get sourceId(): number {
+        return this.source.id;
+    }
+
+    /**
+     * Id of the target node.
+     * @returns Target id.
+     */
+    public get targetId(): number {
+        return this.target.id;
+    }
+
+    public delete() {
+        return this.graph.deleteLink(this.id);
+    }
+
+    /**
+     * Determines if the given node is part of the link structure.
+     * @param node Node to check for.
+     * @returns True, if node is either source or target node of link.
+     */
+    public contains(node: Node): boolean {
+        return this.source === node || this.target === node;
+    }
+
+    public toJSONSerializableObject(): LinkData {
+        return {
+            source: this.source.id,
+            target: this.target.id,
+        };
+    }
+
+    public toHistorySerializableObject(): SimLinkData {
+        return { ...this.toJSONSerializableObject(), index: this.index };
+    }
+
+    public toString(): string {
+        return this.source.toString() + " -> " + this.target.toString();
+    }
+
+    public isInitialized(): boolean {
+        return (
+            super.isInitialized() &&
+            this.source != undefined &&
+            this.target != undefined
+        );
+    }
+
+    public equals(other: GraphElement<LinkData, SimLinkData>): boolean {
+        if (other.constructor != this.constructor) {
+            return false;
+        }
+
+        const link = other as Link;
+
+        return (
+            link.sourceId === this.sourceId && link.targetId === this.targetId
+        );
+    }
+}
diff --git a/src/common/node.ts b/src/common/node.ts
new file mode 100644
index 0000000..076293d
--- /dev/null
+++ b/src/common/node.ts
@@ -0,0 +1,177 @@
+import { Graph } from "./graph";
+import { GraphElement } from "./graphelement";
+import { NodeType } from "./nodetype";
+import { Link } from "./link";
+
+interface NodeProperties {
+    name: string;
+    description?: string;
+    icon?: string;
+    banner?: string;
+    video?: string;
+    references?: string[];
+}
+
+export interface NodeData extends NodeProperties {
+    /**
+     * This interfaces provides a data representation for a simple "flat" node without object pointers.
+     * Can be used to store nodes in JSON format.
+     */
+    id: number;
+    type?: string;
+}
+
+// Based on https://github.com/d3/d3-force#simulation_nodes
+export interface SimNodeData extends NodeData {
+    /**
+     * This interface serves as a data representation for the history of the editor.
+     * Same as the JSON representation + additional parameters related to the simulation.
+     * This ensures that nodes from the history can are restored with the same visual state.
+     */
+    index: number;
+    x: number;
+    y: number;
+    vx: number;
+    vy: number;
+    fx: number;
+    fy: number;
+}
+
+export interface GraphNode extends NodeProperties {
+    /**
+     * Node representation in a Graph. Contains values for easy traversal and force graph simulation properties.
+     */
+    id: number;
+    type: NodeType;
+
+    // Properties used for graph traversal
+    neighbors: Node[];
+    links: Link[];
+
+    // Properties used by the force graph simulation
+    index?: number;
+    x?: number;
+    y?: number;
+    vx?: number;
+    vy?: number;
+    fx?: number;
+    fy?: number;
+}
+
+export class Node
+    extends GraphElement<NodeData, SimNodeData>
+    implements GraphNode
+{
+    public name: string;
+    public description: string;
+    public type: NodeType;
+    public icon: string;
+    public banner: string;
+    public video: string;
+    public references: string[];
+
+    public neighbors: Node[];
+    public links: Link[];
+
+    // These parameters will be added by the force graph implementation
+    public index?: number;
+    public x?: number;
+    public y?: number;
+    public vx?: number;
+    public vy?: number;
+    public fx?: number;
+    public fy?: number;
+
+    constructor(graph: Graph = undefined) {
+        super(0, graph);
+        this.neighbors = [];
+        this.links = [];
+    }
+
+    public setType(typeId: number) {
+        // Is it even different?
+        if (this.type.id === typeId) {
+            return;
+        }
+
+        const newType = this.graph.getType(typeId);
+
+        // Exists?
+        if (newType === undefined) {
+            return;
+        }
+
+        this.type = newType;
+
+        // Store change
+        this.graph.storeCurrentData(
+            "Set type [" +
+                newType.toString() +
+                "] for [" +
+                this.toString() +
+                "]"
+        );
+    }
+
+    public delete() {
+        return this.graph.deleteNode(this.id);
+    }
+
+    /**
+     * Connects this node to a given node. Only works if they are in the same graph.
+     * @param node Other node to connect.
+     * @returns The created link, if successful, otherwise undefined.
+     */
+    public connect(node: Node): Link {
+        if (this.graph !== node.graph) {
+            throw new Error("The connected nodes are not on the same graph!");
+        }
+
+        const link = new Link(this.graph);
+
+        link.source = this;
+        link.target = node;
+
+        if (this.graph.addLink(link)) {
+            this.neighbors.push(node);
+            node.neighbors.push(this);
+            return link;
+        }
+    }
+
+    public toJSONSerializableObject(): NodeData {
+        return {
+            id: this.id,
+            name: this.name,
+            description: this.description,
+            icon: this.icon,
+            banner: this.banner,
+            video: this.video,
+            references: this.references,
+            type: this.type.name,
+        };
+    }
+
+    public toHistorySerializableObject(): SimNodeData {
+        return {
+            ...this.toJSONSerializableObject(),
+            index: this.index,
+            x: this.x,
+            y: this.y,
+            vx: this.vx,
+            vy: this.vy,
+            fx: this.fx,
+            fy: this.fy,
+        };
+    }
+
+    public fromSerializedObject(data: NodeData | SimNodeData): Node {
+        Object.assign(this, data);
+        this.type = undefined; // Remove string type again if undefined as it may be a string.
+        return this;
+    }
+
+    public toString(): string {
+        return this.name;
+    }
+}
diff --git a/src/common/nodetype.ts b/src/common/nodetype.ts
new file mode 100644
index 0000000..c160ec0
--- /dev/null
+++ b/src/common/nodetype.ts
@@ -0,0 +1,47 @@
+import { GraphElement } from "./graphelement";
+
+export interface NodeTypeData {
+    id: number;
+    name: string;
+    color?: string;
+}
+
+export class NodeType
+    extends GraphElement<NodeTypeData, NodeTypeData>
+    implements NodeTypeData
+{
+    public id: number;
+    public name: string;
+    public color: string;
+
+    toJSONSerializableObject(): NodeTypeData {
+        return { id: this.id, name: this.name, color: this.color };
+    }
+
+    toHistorySerializableObject(): NodeTypeData {
+        return this.toJSONSerializableObject();
+    }
+
+    public fromSerializedObject(data: NodeTypeData): NodeType {
+        Object.assign(this, data);
+        return this;
+    }
+
+    public delete() {
+        return this.graph.deleteNodeType(this.name); // TODO: Change to id
+    }
+
+    public toString(): string {
+        return this.name;
+    }
+
+    public equals(other: GraphElement<NodeTypeData, NodeTypeData>): boolean {
+        if (other.constructor != this.constructor) {
+            return false;
+        }
+
+        const type = other as NodeType;
+
+        return type.id === this.id;
+    }
+}
diff --git a/src/common/serializableitem.ts b/src/common/serializableitem.ts
new file mode 100644
index 0000000..c726c27
--- /dev/null
+++ b/src/common/serializableitem.ts
@@ -0,0 +1,30 @@
+/**
+ * Provides the basic interface for unique, serializable objects.
+ */
+export class SerializableItem<JSONType, HistoryType> {
+    public id: number; // Serialized objects need to be unique.
+
+    constructor(id = 0) {
+        this.id = id;
+    }
+
+    public toJSONSerializableObject(): JSONType {
+        throw new Error(
+            "Method 'toJSONSerializableObject()' is not implemented."
+        );
+    }
+
+    public toHistorySerializableObject(): HistoryType {
+        throw new Error(
+            "Method 'toHistorySerializableObject()' is not implemented."
+        );
+    }
+
+    public fromSerializedObject(
+        data: JSONType | HistoryType
+    ): SerializableItem<JSONType, HistoryType> {
+        throw new Error(
+            "Method 'fromSerializableObject()' is not implemented."
+        );
+    }
+}
diff --git a/src/display/components/nodefilter/filtermenu.tsx b/src/display/components/nodefilter/filtermenu.tsx
index f38c8af..6c64b53 100644
--- a/src/display/components/nodefilter/filtermenu.tsx
+++ b/src/display/components/nodefilter/filtermenu.tsx
@@ -2,7 +2,7 @@ import React, { useState } from "react";
 
 import "./filtermenu.css";
 import Label from "./label";
-import { GraphObjectType } from "../../../common/graph";
+import { NodeTypeData } from "../../../common/graph";
 
 interface FilterMenuProps {
     classes: Map<string, GraphObjectType>;
diff --git a/src/display/components/nodeinfo/neighbors.tsx b/src/display/components/nodeinfo/neighbors.tsx
index 5a899cc..760ebdc 100644
--- a/src/display/components/nodeinfo/neighbors.tsx
+++ b/src/display/components/nodeinfo/neighbors.tsx
@@ -1,6 +1,6 @@
 import React from "react";
 
-import { GraphObjectType, Node } from "../../../common/graph";
+import { NodeTypeData, Node } from "../../../common/graph";
 import FancyScrollbar from "../fancyscrollbar";
 import Collapsible from "../collapsible";
 
diff --git a/src/display/components/nodeinfo/nodeinfobar.tsx b/src/display/components/nodeinfo/nodeinfobar.tsx
index 4965e73..ffde37c 100644
--- a/src/display/components/nodeinfo/nodeinfobar.tsx
+++ b/src/display/components/nodeinfo/nodeinfobar.tsx
@@ -1,7 +1,7 @@
 import React from "react";
 
 import "./nodeinfobar.css";
-import { GraphObjectType, Node } from "../../../common/graph";
+import { NodeTypeData, Node } from "../../../common/graph";
 import TitleArea from "./titlearea";
 import FancyScrollbar from "../fancyscrollbar";
 import MediaArea from "./mediaarea";
diff --git a/src/display/display.tsx b/src/display/display.tsx
index 992a73e..44e5c6d 100644
--- a/src/display/display.tsx
+++ b/src/display/display.tsx
@@ -5,7 +5,7 @@ import PropTypes, { InferType } from "prop-types";
 import "./display.css";
 import { GraphNode, GraphRenderer } from "./renderer";
 import * as Helpers from "./helpers";
-import Graph, { Node } from "../common/graph";
+import { Graph, Node } from "../common/graph";
 import { loadGraphJson } from "../common/datasets";
 import NodeInfoBar from "./components/nodeinfo/nodeinfobar";
 import FilterMenu from "./components/nodefilter/filtermenu";
@@ -63,7 +63,7 @@ class Display extends React.Component<
 
         const fetchGraph = async () => {
             const graphData = await loadGraphJson(this.props.spaceId);
-            this.graph = new Graph(graphData.nodes, graphData.links);
+            this.graph = Graph.fromSerializedObject(graphData);
             // console.log(this.graph);
             this.setState({ graph: this.graph });
         };
diff --git a/src/display/renderer.tsx b/src/display/renderer.tsx
index 294868e..7759238 100644
--- a/src/display/renderer.tsx
+++ b/src/display/renderer.tsx
@@ -10,7 +10,7 @@ import React from "react";
 import PropTypes, { InferType } from "prop-types";
 import SpriteText from "three-spritetext";
 import { Object3D, Sprite } from "three";
-import Graph, { Coordinate, Link, Node } from "../common/graph";
+import { Graph, Coordinate, Link, Node } from "../common/graph";
 
 export interface GraphNode extends Node {
     x: number;
diff --git a/src/editor/js/components/editor.tsx b/src/editor/js/components/editor.tsx
index cbabc02..bb125ec 100644
--- a/src/editor/js/components/editor.tsx
+++ b/src/editor/js/components/editor.tsx
@@ -5,10 +5,10 @@ import { NodeDetails } from "./nodedetails";
 import { SpaceSelect } from "./spaceselect";
 import "./editor.css";
 import { ForceGraph2D } from "react-force-graph";
-import { Node } from "../structures/graph/node";
+import { Node } from "../../../common/node";
 import { HistoryNavigator } from "./historynavigator";
-import { GraphElement } from "../structures/graph/graphelement";
-import { Link } from "../structures/graph/link";
+import { GraphElement } from "../../../common/graphelement";
+import { Link } from "../../../common/link";
 import { NodeTypesEditor } from "./nodetypeseditor";
 import { SpaceManager } from "./spacemanager";
 
diff --git a/src/editor/js/components/nodedetails.tsx b/src/editor/js/components/nodedetails.tsx
index 2a619ab..b4d1f32 100644
--- a/src/editor/js/components/nodedetails.tsx
+++ b/src/editor/js/components/nodedetails.tsx
@@ -1,7 +1,7 @@
 import React from "react";
 import { ReactNode } from "react";
-import { Node } from "../structures/graph/node";
-import { NodeType } from "../structures/graph/nodetype";
+import { Node } from "../../../common/node";
+import { NodeType } from "../../../common/nodetype";
 import "./nodedetails.css";
 
 type propTypes = {
diff --git a/src/editor/js/components/nodetypeentry.tsx b/src/editor/js/components/nodetypeentry.tsx
index a5a1cc3..997142e 100644
--- a/src/editor/js/components/nodetypeentry.tsx
+++ b/src/editor/js/components/nodetypeentry.tsx
@@ -1,7 +1,7 @@
 import React from "react";
 import { ReactNode } from "react";
 import { Graph } from "../structures/graph/graph";
-import { NodeType } from "../structures/graph/nodetype";
+import { NodeType } from "../../../common/nodetype";
 import "./nodetypeentry.css";
 
 type propTypes = {
diff --git a/src/editor/js/components/nodetypeseditor.tsx b/src/editor/js/components/nodetypeseditor.tsx
index b649a1d..4028281 100644
--- a/src/editor/js/components/nodetypeseditor.tsx
+++ b/src/editor/js/components/nodetypeseditor.tsx
@@ -3,7 +3,7 @@ import { ReactNode } from "react";
 import { Graph } from "../structures/graph/graph";
 import "./nodetypeseditor.css";
 import { NodeTypeEntry } from "./nodetypeentry";
-import { NodeType } from "../structures/graph/nodetype";
+import { NodeType } from "../../../common/nodetype";
 
 type propTypes = {
     graph: Graph;
diff --git a/src/editor/js/structures/graph/graph.ts b/src/editor/js/structures/graph/graph.ts
index a164292..c393bfb 100644
--- a/src/editor/js/structures/graph/graph.ts
+++ b/src/editor/js/structures/graph/graph.ts
@@ -1,27 +1,28 @@
-import ManagedData from "../manageddata";
-import { Link } from "./link";
-import { NodeType } from "./nodetype";
-import { Node } from "./node";
-import { GLOBAL_PARAMS } from "../helper/serializableitem";
-import { GraphElement } from "./graphelement";
+import { Link } from "../../../../common/link";
+import { NodeType } from "../../../../common/nodetype";
+import { Node } from "../../../../common/node";
+import { GraphElement } from "../../../../common/graphelement";
+import * as Common from "../../../../common/graph";
+import { History } from "../../../../common/history";
+import { GraphContent } from "../../../../common/graph";
 
 const GRAPH_PARAMS = [...GLOBAL_PARAMS];
 const GRAPH_DATA_PARAMS = ["nodes", "links", "types"];
 
-export type GraphData = { nodes: Node[]; links: Link[]; types: NodeType[] };
+// export type GraphData = { nodes: Node[]; links: Link[]; types: NodeType[] };
 
-export class Graph extends ManagedData {
-    public data: GraphData;
+export class DynamicGraph extends Common.Graph {
+    private history: History<Common.Graph>;
 
     private nextNodeId = 0;
     private nextLinkId = 0;
     private nextTypeId = 0;
 
     // Callbacks
-    public onChangeCallbacks: { (data: GraphData): void }[];
+    public onChangeCallbacks: { (data: GraphContent): void }[];
 
-    constructor(data: GraphData) {
-        super(data);
+    constructor(data: GraphContent) {
+        super();
         this.onChangeCallbacks = [];
 
         this.connectElementsToGraph(this.data);
@@ -239,7 +240,9 @@ export class Graph extends ManagedData {
             return true; // Doesn't even exist in graph to begin with.
         }
 
-        this.data.types = this.data.types.filter((n: NodeType) => !n.equals(nodeType));
+        this.data.types = this.data.types.filter(
+            (n: NodeType) => !n.equals(nodeType)
+        );
 
         try {
             // No save points should be created when replacing usages
@@ -295,14 +298,22 @@ export class Graph extends ManagedData {
         return true;
     }
 
-    getLink(sourceId: number, targetId: number, directionSensitive = true): Link {
+    getLink(
+        sourceId: number,
+        targetId: number,
+        directionSensitive = true
+    ): Link {
         return this.links.find((l) => {
             if (l.sourceId === sourceId && l.targetId === targetId) {
                 return true;
             }
 
             // Check other direction if allowed
-            if (!directionSensitive && (l.sourceId === targetId && l.targetId === sourceId)) {
+            if (
+                !directionSensitive &&
+                l.sourceId === targetId &&
+                l.targetId === sourceId
+            ) {
                 return true;
             }
 
@@ -458,8 +469,8 @@ export class Graph extends ManagedData {
 
         // TODO: Remove, when types are directly parsed and not just implicit
         data.nodes.forEach((node) => {
-            const sharedType: NodeType = data.types.find((type) =>
-                type.name === node.type.name || type.equals(node.type)
+            const sharedType: NodeType = data.types.find(
+                (type) => type.name === node.type.name || type.equals(node.type)
             );
 
             if (sharedType !== undefined) {
diff --git a/src/editor/js/structures/graph/graphelement.ts b/src/editor/js/structures/graph/graphelement.ts
deleted file mode 100644
index 6a572b9..0000000
--- a/src/editor/js/structures/graph/graphelement.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { Graph } from "./graph";
-import { SerializableItem } from "../helper/serializableitem";
-
-export class GraphElement extends SerializableItem {
-    protected isNode: boolean;
-    protected isLink: boolean;
-
-    public graph: Graph;
-
-    constructor(graph: Graph = undefined) {
-        super();
-        this.graph = graph;
-        this.isNode = false;
-        this.isLink = false;
-
-        this.equals = this.equals.bind(this);
-    }
-
-    public get node(): boolean {
-        return this.isNode;
-    }
-
-    public get link(): boolean {
-        return this.isLink;
-    }
-
-    /**
-     * Removes element from its parent graph.
-     * @returns True, if successful.
-     */
-    public delete(): boolean {
-        throw new Error('Function "delete()" has not been implemented.');
-    }
-
-    /**
-     * Adds the element to the given graph.
-     * @param graph Graph to add element to.
-     * @returns True, if successful.
-     */
-    public add(graph: Graph = this.graph): boolean {
-        throw new Error('Function "add(graph)" has not been implemented.');
-    }
-
-    /**
-     * Needs to be implemented to create a filtered version for storing in the data history.
-     * @returns Filtered object.
-     */
-    public getCleanInstance(): any {
-        throw new Error(
-            'Function "getCleanInstance()" has not been implemented.'
-        );
-    }
-
-    /**
-     * Compares to objects. Can be a custom implementation.
-     * @returns True, if given object is identical.
-     */
-    public equals(other: GraphElement): boolean {
-        return (
-            other.node == this.node &&
-            other.link == this.link &&
-            other.id == this.id
-        );
-    }
-}
diff --git a/src/editor/js/structures/graph/link.ts b/src/editor/js/structures/graph/link.ts
deleted file mode 100644
index e8f7dfc..0000000
--- a/src/editor/js/structures/graph/link.ts
+++ /dev/null
@@ -1,144 +0,0 @@
-import { GraphElement } from "./graphelement";
-import { Graph } from "./graph";
-import { Node } from "./node";
-import { GLOBAL_PARAMS } from "../helper/serializableitem";
-import * as Common from "../../../../common/graph";
-
-const LINK_PARAMS = ["source", "target", ...GLOBAL_PARAMS];
-const LINK_SIM_PARAMS = ["index"];
-
-export class Link extends GraphElement implements Common.Link {
-    public source: Node;
-    public target: Node;
-
-    private _sourceId: number;
-    private _targetId: number;
-
-    constructor(graph: Graph = undefined) {
-        super(graph);
-        this.isLink = true;
-    }
-
-    /**
-     * Id of the source node.
-     * @returns Source id.
-     */
-    public get sourceId(): number {
-        if (this.source == undefined) {
-            return this._sourceId;
-        }
-
-        return this.source.id;
-    }
-
-    /**
-     * Removes stored node object and just saves the id instead.
-     * @param value New source id value.
-     */
-    public set sourceId(value: number) {
-        this._sourceId = value;
-        this.source = undefined;
-    }
-
-    /**
-     * Id of the target node.
-     * @returns Target id.
-     */
-    public get targetId(): number {
-        if (this.target == undefined) {
-            return this._targetId;
-        }
-
-        return this.target.id;
-    }
-
-    /**
-     * Removes stored node object and just saves the id instead.
-     * @param value New target id value.
-     */
-    public set targetId(value: number) {
-        this._targetId = value;
-        this.target = undefined;
-    }
-
-    public delete() {
-        return this.graph.deleteLink(this);
-    }
-
-    public add(graph: Graph = this.graph) {
-        this.graph = graph;
-        if (this.graph == undefined) {
-            return false;
-        }
-
-        return this.graph.addLink(this);
-    }
-
-    /**
-     * Determines if the given node is part of the link structure.
-     * @param node Node to check for.
-     * @returns True, if node is either source or target node of link.
-     */
-    public contains(node: Node): boolean {
-        return this.source === node || this.target === node;
-    }
-
-    public serialize(): any {
-        return this.serializeProperties(LINK_PARAMS);
-    }
-
-    public getCleanInstance(): any {
-        return {
-            ...this.serialize(),
-            ...this.serializeProperties(LINK_SIM_PARAMS),
-        };
-    }
-
-    public static parse(raw: any): Link {
-        const link: Link = new Link();
-
-        if (isNaN(Number(raw.source))) {
-            // Source not given as id, but probably as node object
-            link.sourceId = raw.source.id;
-            link.targetId = raw.target.id;
-        } else {
-            link.sourceId = Number(raw.source);
-            link.targetId = Number(raw.target);
-        }
-
-        // Try to parse simulation parameters
-        LINK_SIM_PARAMS.forEach((param) => {
-            if (raw[param] === undefined) {
-                return;
-            }
-
-            (link as any)[param] = raw[param];
-        });
-
-        return link;
-    }
-
-    public toString(): string {
-        let source: any = this.source;
-        let target: any = this.target;
-
-        if (this.source == undefined || this.target == undefined) {
-            source = this.sourceId;
-            target = this.targetId;
-        }
-
-        return source.toString() + " -> " + target.toString();
-    }
-
-    public equals(other: GraphElement): boolean {
-        if (!other.link || other.node) {
-            return false;
-        }
-
-        const link = other as Link;
-
-        return (
-            link.sourceId === this.sourceId && link.targetId === this.targetId
-        );
-    }
-}
diff --git a/src/editor/js/structures/graph/node.ts b/src/editor/js/structures/graph/node.ts
deleted file mode 100644
index dfa16a2..0000000
--- a/src/editor/js/structures/graph/node.ts
+++ /dev/null
@@ -1,182 +0,0 @@
-import { Graph } from "./graph";
-import { GraphElement } from "./graphelement";
-import { NodeType } from "./nodetype";
-import { Link } from "./link";
-import { GLOBAL_PARAMS } from "../helper/serializableitem";
-import * as Common from "../../../../common/graph";
-
-const NODE_PARAMS = [
-    "name",
-    "icon",
-    "description",
-    "references",
-    "video",
-    "type",
-    "banner",
-    ...GLOBAL_PARAMS,
-];
-const NODE_SIM_PARAMS = ["index", "x", "y", "vx", "vy", "fx", "fy"]; // Based on https://github.com/d3/d3-force#simulation_nodes
-
-export class Node extends GraphElement implements Common.Node {
-    public name: string;
-    public description: string;
-    public type: NodeType;
-    public icon: string;
-    public banner: string;
-    public video: string;
-    public references: string[];
-
-    constructor(graph: Graph = undefined) {
-        super(graph);
-        this.isNode = true;
-    }
-
-    public setType(typeId: number) {
-        // Is it even different?
-        if (this.type.id === typeId) {
-            return;
-        }
-
-        const newType = this.graph.getType(typeId);
-
-        // Exists?
-        if (newType === undefined) {
-            return;
-        }
-
-        this.type = newType;
-
-        // Store change
-        this.graph.storeCurrentData(
-            "Set type [" +
-            newType.toString() +
-            "] for [" +
-            this.toString() +
-            "]"
-        );
-    }
-
-    public delete() {
-        return this.graph.deleteNode(this);
-    }
-
-    public add(graph: Graph = this.graph) {
-        this.graph = graph;
-        if (this.graph == undefined) {
-            return false;
-        }
-
-        return this.graph.addNode(this);
-    }
-
-    /**
-     * Calculates a list of all connected links to the current node.
-     * @returns Array containing all connected links.
-     */
-    public get links(): Link[] {
-        const links: Link[] = [];
-
-        this.graph.links.forEach((link) => {
-            if (link.contains(this)) {
-                links.push(link);
-            }
-        });
-
-        return links;
-    }
-
-    /**
-     * Calculates a list of all connected nodes to the current node.
-     * @returns Array containing all connected nodes.
-     */
-    public get neighbors(): Node[] {
-        const nodes: Node[] = [];
-
-        this.links.forEach((link) => {
-            // Find "other" node
-            let otherNode = link.source;
-            if (this.equals(otherNode)) {
-                otherNode = link.target;
-            }
-
-            // Still undefined?
-            if (otherNode == undefined) {
-                // Link apparently not properly set up
-                return;
-            }
-
-            // Add to list if doesn't exist
-            if (!nodes.includes(otherNode)) {
-                nodes.push(otherNode);
-            }
-        });
-
-        return nodes;
-    }
-
-    /**
-     * Connects a given node to itself. Only works if they are in the same graph.
-     * @param node Other node to connect.
-     * @returns The created link, if successful, otherwise undefined.
-     */
-    public connect(node: Node): Link {
-        if (this.graph !== node.graph) {
-            return undefined;
-        }
-
-        const link = new Link(this.graph);
-
-        link.source = this;
-        link.target = node;
-
-        if (link.add()) {
-            return link;
-        }
-    }
-
-    public serialize(): any {
-        return this.serializeProperties(NODE_PARAMS);
-    }
-
-    public getCleanInstance(): any {
-        return {
-            ...this.serialize(),
-            ...this.serializeProperties(NODE_SIM_PARAMS),
-        };
-    }
-
-    public static parse(raw: any): Node {
-        const node: Node = new Node();
-
-        if (raw.label !== undefined) {
-            node.name = raw.label;
-        } else {
-            node.name = raw.name;
-        }
-
-        node.id = Number(raw.id);
-        node.description = raw.description;
-        node.type = NodeType.parse(raw.type);
-
-        // Defaults
-        node.icon = raw.icon ? raw.icon : "";
-        node.banner = raw.banner ? raw.banner : "";
-        node.video = raw.video ? raw.video : "";
-        node.references = raw.references ? raw.references : [];
-
-        // Try to parse simulation parameters
-        NODE_SIM_PARAMS.forEach((param) => {
-            if (raw[param] === undefined) {
-                return;
-            }
-
-            (node as any)[param] = raw[param];
-        });
-
-        return node;
-    }
-
-    public toString(): string {
-        return this.name;
-    }
-}
diff --git a/src/editor/js/structures/graph/nodetype.ts b/src/editor/js/structures/graph/nodetype.ts
deleted file mode 100644
index 83f026b..0000000
--- a/src/editor/js/structures/graph/nodetype.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { GLOBAL_PARAMS } from "../helper/serializableitem";
-import { Graph } from "./graph";
-import { GraphElement } from "./graphelement";
-import * as Common from "../../../../common/graph";
-
-const NODE_TYPE_PARAMS = ["name", "color", ...GLOBAL_PARAMS];
-
-export class NodeType extends GraphElement implements Common.GraphObjectType {
-    public name: string;
-    public color: string;
-
-    serialize(): any {
-        return this.serializeProperties(NODE_TYPE_PARAMS);
-    }
-
-    public delete(): boolean {
-        return this.graph.deleteNodeType(this);
-    }
-
-    public add(graph: Graph = this.graph): boolean {
-        this.graph = graph;
-        if (this.graph == undefined) {
-            return false;
-        }
-
-        return this.graph.addNodeType(this);
-    }
-
-    public getCleanInstance(): any {
-        return this.serialize();
-    }
-
-    public static parse(raw: any): NodeType {
-        const type: NodeType = new NodeType();
-
-        if (typeof raw === "string" || raw instanceof String) {
-            type.name = raw as string;
-            type.color = "#ff0000";
-        } else {
-            type.name = raw.name;
-            type.color = raw.color;
-        }
-
-        return type;
-    }
-
-    public toString(): string {
-        return this.name;
-    }
-
-    public equals(other: GraphElement): boolean {
-        if (other.link || other.node) {
-            return false;
-        }
-
-        const type = other as NodeType;
-
-        return type.id === this.id;
-    }
-}
diff --git a/src/editor/js/structures/helper/serializableitem.ts b/src/editor/js/structures/helper/serializableitem.ts
deleted file mode 100644
index a5e2313..0000000
--- a/src/editor/js/structures/helper/serializableitem.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * Provides the basic interface for unique, serializable objects.
- */
-import {array} from "prop-types";
-
-export const GLOBAL_PARAMS = ["id"];
-
-export class SerializableItem {
-    public id: number;  // Serialized objects need to be unique.
-
-    /**
-     * Returns the current object in its serialized form.
-     * @returns The serialized object.
-     */
-    public serialize(): any {
-        throw new Error("Method 'serialize()' must be implemented.");
-    }
-
-    /**
-     * Creates the current object based on raw, serialized data.
-     * @param raw The serialized data.
-     * @returns Parsed data in final form. Could be the finalisd object.
-     */
-    public static parse(raw: any): any {
-        throw new Error("Method 'parse()' must be implemented.");
-    }
-
-    /**
-     * A generic way to create a new object with all the desired parameters of the current one.
-     * @param params List of parameters to include in the new object.
-     * @param data The data object to be serialized. The current object by default.
-     * @protected
-     * @returns New object containing all the desired properties.
-     */
-    protected serializeProperties(params: string[], data: any = this): any {
-        const serialized: any = {};
-
-        params.forEach((param) => {
-            serialized[param] = this.serializeItem((this as any)[param]);
-        });
-
-        return serialized;
-    }
-
-    /**
-     * Recursively serializes an object. Handles serializable items and lists properly.
-     * @param value Object to be serialized.
-     * @private
-     * @returns Serialized item.
-     */
-    private serializeItem(value: any): any {
-        if (value instanceof SerializableItem) {
-            // If is also serializable, use the serialized form
-            return value.serialize();
-        } else if (value instanceof array) {
-            // If is some kind of list, convert the objects in the list recursively
-            const serializedList: any = [];
-            (value as []).forEach((item) => {
-                serializedList.push(this.serializeItem(item));
-            })
-            return serializedList;
-        } else {
-            return value;
-        }
-    }
-}
diff --git a/src/editor/js/structures/manageddata.ts b/src/editor/js/structures/manageddata.ts
index 65c6eeb..1f775d4 100644
--- a/src/editor/js/structures/manageddata.ts
+++ b/src/editor/js/structures/manageddata.ts
@@ -1,40 +1,41 @@
-import { SerializableItem } from "./helper/serializableitem";
+import { SerializableItem } from "../../../common/serializableitem";
 
 const SAVE_BUTTON_ID = "div#ks-editor #toolbar-save";
 
-type SavePoint = {
-    description: string;
-    data: string;
+interface SavePoint<DataType> {
     id: number;
-};
+    description: string;
+    data: DataType;
+}
 
 /**
  * Allows objects to have undo/redo functionality in their data and custom save points.
  */
-export default class ManagedData extends SerializableItem {
-    public data: any; // The data to be stored in a history.
-    public history: SavePoint[]; // All save points of the data.
+export default class ManagedData<HistoryDataType> {
+    public data: SerializableItem<never, HistoryDataType>; // Object that will be serialized to history on save.
+    public history: SavePoint<HistoryDataType>[]; // All save points of the data.
     public historyPosition: number; // Currently selected save point in history. Latest always at index 0.
     private savedHistoryId: number; // Id of save point that is considered saved.
     private storingEnabled: boolean; // To internally disable saving of objects on save call.
+    public hasNewData: boolean; // True if there is new data to save in the history
 
     /**
      * Sets initial states.
      * @param data Initial state of data to be stored.
      */
-    constructor(data: any) {
-        super();
+    constructor(data: SerializableItem<never, HistoryDataType>) {
         this.data = data;
         this.history = []; // Newest state is always at 0
         this.historyPosition = 0;
         this.savedHistoryId = 0;
         this.storingEnabled = true;
+        this.hasNewData = false;
     }
 
     /**
      * @returns SavePoint of current history position. Gives access to meta data of current data.
      */
-    public get currentSavePoint(): SavePoint {
+    public get currentSavePoint(): SavePoint<HistoryDataType> {
         return this.history[this.historyPosition];
     }
 
@@ -43,11 +44,12 @@ export default class ManagedData extends SerializableItem {
      * @private
      */
     private updateUnsavedChangesHandler() {
-        if (this.hasUnsavedChanges()) {
-            jQuery(SAVE_BUTTON_ID).removeClass("hidden");
+        // TODO: Remove jQuery!
+        if (this.hasNewData) {
+            //jQuery(SAVE_BUTTON_ID).removeClass("hidden");
             window.addEventListener("beforeunload", this.handleBeforeUnload);
         } else {
-            jQuery(SAVE_BUTTON_ID).addClass("hidden");
+            //jQuery(SAVE_BUTTON_ID).addClass("hidden");
             window.removeEventListener("beforeunload", this.handleBeforeUnload);
         }
     }
@@ -58,6 +60,7 @@ export default class ManagedData extends SerializableItem {
      * @private
      */
     private handleBeforeUnload(e: any) {
+        // TODO: Remove any and deprecated window.event check
         const confirmationMessage =
             "If you leave before saving, unsaved changes will be lost.";
 
@@ -65,21 +68,11 @@ export default class ManagedData extends SerializableItem {
         return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.
     }
 
-    /**
-     * Returns true, if data has unsaved changes.
-     */
-    public hasUnsavedChanges(): boolean {
-        if (this.history[this.historyPosition] === undefined) {
-            return this.data !== undefined;
-        }
-
-        return this.history[this.historyPosition].id !== this.savedHistoryId;
-    }
-
     /**
      * Internally marks the current save point as saved.
      */
-    public markChangesAsSaved() {
+    private markChangesAsSaved() {
+        // TODO: Remove if unessesary. Changed accessor from public to private
         this.savedHistoryId = this.history[this.historyPosition].id;
         this.updateUnsavedChangesHandler();
     }
@@ -98,20 +91,6 @@ export default class ManagedData extends SerializableItem {
         this.storingEnabled = true;
     }
 
-    /**
-     * Event triggered after undo.
-     */
-    protected onUndo() {
-        // No base implementation.
-    }
-
-    /**
-     * Event triggered after redo.
-     */
-    protected onRedo() {
-        // No base implementation.
-    }
-
     /**
      * Go to one step back in the stored history, if available.
      * @returns True, if successful.
@@ -119,7 +98,6 @@ export default class ManagedData extends SerializableItem {
     public undo(): boolean {
         if (this.step(1)) {
             this.updateUnsavedChangesHandler();
-            this.onUndo();
             return true;
         } else {
             return false;
@@ -133,7 +111,6 @@ export default class ManagedData extends SerializableItem {
     public redo(): boolean {
         if (this.step(-1)) {
             this.updateUnsavedChangesHandler();
-            this.onRedo();
             return true;
         } else {
             return false;
-- 
GitLab