Skip to content
Snippets Groups Projects
Commit 2500cabd authored by Maximilian Giller's avatar Maximilian Giller :squid:
Browse files

Implemented properly typed graph object.

parent b59f63d3
No related branches found
No related tags found
No related merge requests found
Pipeline #56300 passed
import ManagedData from "./manageddata"; /**
import ManagedData from "./structures/manageddata";
import { PLUGIN_PATH, COLOR_PALETTE } from "../../config"; import { PLUGIN_PATH, COLOR_PALETTE } from "../../config";
const LINK_NAME_CONNECTOR = " → "; const LINK_NAME_CONNECTOR = " → ";
export const NODE_LABEL = "name";
export const NODE_ID = "id";
export const NODE_TYPE = "type";
export const NODE_DESCRIPTION = "description";
export const NODE_IMAGE = "image";
export const NODE_REFERENCES = "infoLinks";
export const NODE_VIDEO = "video";
export const NODE_DETAIL_IMAGE = "infoImage";
export const LINK_SOURCE = "source";
export const LINK_TARGET = "target";
export const LINK_TYPE = "type";
export const LINK_PARTICLE_COUNT = 4;
export const GRAPH_NODES = "nodes"; export const GRAPH_NODES = "nodes";
export const GRAPH_LINKS = "links"; export const GRAPH_LINKS = "links";
...@@ -23,30 +14,16 @@ export const GRAPH_LINKS = "links"; ...@@ -23,30 +14,16 @@ export const GRAPH_LINKS = "links";
export const IMAGE_SIZE = 12; export const IMAGE_SIZE = 12;
export const IMAGE_SRC = PLUGIN_PATH + "datasets/images/"; export const IMAGE_SRC = PLUGIN_PATH + "datasets/images/";
export const LINK_PARAMS = [];
export const NODE_PARAMS = [
NODE_ID,
NODE_LABEL,
NODE_IMAGE,
NODE_DESCRIPTION,
NODE_REFERENCES,
NODE_VIDEO,
NODE_TYPE,
NODE_DETAIL_IMAGE,
];
export const LINK_SIM_PARAMS = ["index"];
export const NODE_SIM_PARAMS = ["index", "x", "y", "vx", "vy", "fx", "fy"]; // Based on https://github.com/d3/d3-force#simulation_nodes
export const JSON_CONFIG = PLUGIN_PATH + "datasets/aud1v2.json"; export const JSON_CONFIG = PLUGIN_PATH + "datasets/aud1v2.json";
export const STOP_PHYSICS_DELAY = 5000; // ms export const STOP_PHYSICS_DELAY = 5000; // ms
export class Graph extends ManagedData { export class PREVIOUSGraph extends ManagedData {
constructor(data) { constructor(data) {
super(Graph.addIdentifiers(data)); super(Graph.addIdentifiers(data));
this.calculateNodeTypes(); this.calculateNodeTypes();
this.onChangeCallbacks = [];
this.physicsDelay = STOP_PHYSICS_DELAY; this.physicsDelay = STOP_PHYSICS_DELAY;
this.physicsStopTimeoutId = undefined; this.physicsStopTimeoutId = undefined;
} }
...@@ -67,86 +44,6 @@ export class Graph extends ManagedData { ...@@ -67,86 +44,6 @@ export class Graph extends ManagedData {
}, this.physicsDelay); }, this.physicsDelay);
} }
triggerOnChange() {
this.onChangeCallbacks.forEach((fn) => fn(this.data));
}
onRedo() {
this.triggerOnChange();
}
onUndo() {
this.triggerOnChange();
}
storableData(data) {
return this.getCleanData(data, true);
}
/**
* Based on the function from the 3d-graph code from @JoschaRode
*/
calculateNodeTypes() {
const nodeClasses = [];
this.data[GRAPH_NODES].forEach((node) =>
nodeClasses.push(node[NODE_TYPE])
);
this.nodeTypes = [...new Set(nodeClasses)];
}
getNodeColor(node) {
return this.getTypeColor(node[NODE_TYPE]);
}
getTypeColor(typeClass) {
var classIndex = this.nodeTypes.indexOf(typeClass);
if (classIndex <= -1) {
return "black";
}
return COLOR_PALETTE[classIndex % COLOR_PALETTE.length];
}
deleteNode(nodeId) {
// Delete node from nodes
this.data[GRAPH_NODES] = this.data[GRAPH_NODES].filter(
(n) => n[NODE_ID] !== nodeId
);
// Delete links with node
this.data[GRAPH_LINKS] = this.data[GRAPH_LINKS].filter(
(l) =>
l[LINK_SOURCE][NODE_ID] !== nodeId &&
l[LINK_TARGET][NODE_ID] !== nodeId
);
this.storeCurrentData("Deleted node with id [" + nodeId + "]");
}
deleteNodes(nodeIds) {
if (nodeIds === undefined || nodeIds.length <= 0) {
return;
}
try {
this.disableStoring();
nodeIds.forEach((id) => {
this.deleteNode(id);
});
} finally {
// Gotta make sure that storing is turned back on again
this.enableStoring();
}
this.storeCurrentData(
"Deleted nodes with ids [" + nodeIds.join(",") + "]"
);
}
stopPhysics() { stopPhysics() {
this.data[GRAPH_NODES].forEach((n) => { this.data[GRAPH_NODES].forEach((n) => {
n.fx = n.x; n.fx = n.x;
...@@ -154,54 +51,13 @@ export class Graph extends ManagedData { ...@@ -154,54 +51,13 @@ export class Graph extends ManagedData {
}); });
} }
static addIdentifiers(data) {
data[GRAPH_NODES].forEach((n) => {
n.node = true;
n.link = false;
});
data[GRAPH_LINKS].forEach((l) => {
l.node = false;
l.link = true;
});
return data;
}
deleteLink(sourceId, targetId) {
// Only keep links, of one of the nodes is different
this.data[GRAPH_LINKS] = this.data[GRAPH_LINKS].filter(
(l) =>
l[LINK_SOURCE][NODE_ID] !== sourceId ||
l[LINK_TARGET][NODE_ID] !== targetId
);
this.storeCurrentData(
"Deleted link connecting [" + sourceId + "] with [" + 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) { existsLink(sourceId, targetId) {
const links = this.data[GRAPH_LINKS]; const links = this.data[GRAPH_LINKS];
for (var i = 0; i < links.length; i++) { for (var i = 0; i < links.length; i++) {
var link = links[i]; var link = links[i];
if ( if (
link[LINK_SOURCE][NODE_ID] === sourceId && linkdeleteLink[LINK_SOURCE][NODE_ID] === sourceId &&
link[LINK_TARGET][NODE_ID] === targetId link[LINK_TARGET][NODE_ID] === targetId
) { ) {
return true; return true;
...@@ -211,108 +67,6 @@ export class Graph extends ManagedData { ...@@ -211,108 +67,6 @@ export class Graph extends ManagedData {
return false; return false;
} }
changeDetails(selectionDetails) {
if (selectionDetails.node === true) {
this.changeNodeDetails(selectionDetails[NODE_ID], selectionDetails);
} else if (selectionDetails.link === true) {
this.changeLinkDetails(
selectionDetails[LINK_SOURCE][NODE_ID],
selectionDetails[LINK_TARGET][NODE_ID],
selectionDetails
);
}
}
changeNodeDetails(nodeId, newDetails) {
var nodes = this.data[GRAPH_NODES];
for (var i = 0; i < nodes.length; i++) {
// Is relevant node?
if (nodes[i][NODE_ID] !== nodeId) {
continue; // No
}
// Change details
nodes[i] = Object.assign(nodes[i], newDetails);
// All done
this.storeCurrentData("Changed node details");
return;
}
}
changeLinkDetails(sourceId, targetId, newDetails) {
var links = this.data[GRAPH_LINKS];
for (var i = 0; i < links.length; i++) {
// Is relevant link?
if (
links[i][LINK_SOURCE][NODE_ID] !== sourceId ||
links[i][LINK_TARGET][NODE_ID] !== targetId
) {
continue; // No
}
// Change details
links[i] = Object.assign(links[i], newDetails);
// All done
this.storeCurrentData("Changed link details");
return;
}
}
connectNodes(sourceId, targetIds) {
targetIds.forEach((targetId) => {
if (
this.existsLink(sourceId, targetId) ||
this.existsLink(targetId, sourceId)
) {
return;
}
this.addLink(sourceId, targetId);
});
}
getCleanData(data = undefined, simulationParameters = false) {
if (data === undefined) {
data = this.data;
}
var cleanData = {};
cleanData[GRAPH_LINKS] = [];
cleanData[GRAPH_NODES] = [];
data[GRAPH_LINKS].forEach((link) =>
cleanData[GRAPH_LINKS].push(
this.getCleanLink(link, simulationParameters)
)
);
data[GRAPH_NODES].forEach((node) =>
cleanData[GRAPH_NODES].push(
this.getCleanNode(node, simulationParameters)
)
);
return cleanData;
}
getCleanNode(node, simulationParameters) {
var cleanNode = {};
NODE_PARAMS.forEach((param) => {
cleanNode[param] = node[param];
});
if (simulationParameters) {
NODE_SIM_PARAMS.forEach((param) => {
cleanNode[param] = node[param];
});
}
return cleanNode;
}
getCleanLink(link, simulationParameters) { getCleanLink(link, simulationParameters) {
var cleanLink = {}; var cleanLink = {};
...@@ -329,17 +83,6 @@ export class Graph extends ManagedData { ...@@ -329,17 +83,6 @@ export class Graph extends ManagedData {
cleanLink[LINK_TARGET] = link[LINK_TARGET]; cleanLink[LINK_TARGET] = link[LINK_TARGET];
} }
// Other parameters
LINK_PARAMS.forEach((param) => {
cleanLink[param] = link[param];
});
if (simulationParameters) {
LINK_SIM_PARAMS.forEach((param) => {
cleanLink[param] = link[param];
});
}
return cleanLink; return cleanLink;
} }
...@@ -377,73 +120,6 @@ export class Graph extends ManagedData { ...@@ -377,73 +120,6 @@ export class Graph extends ManagedData {
return result; return result;
} }
addLink(sourceId, targetId, linkDetails = {}) {
// Copy params
var newLink = linkDetails;
// Make sure the IDs exist
if (
sourceId === undefined ||
targetId === undefined ||
this.existsNodeId(sourceId) === false ||
this.existsNodeId(targetId) === false
) {
return;
}
// Make sure the link is unique
if (this.existsLink(sourceId, targetId)) {
return;
}
newLink[LINK_SOURCE] = sourceId;
newLink[LINK_TARGET] = targetId;
// Basic node properties
newLink.link = true;
newLink.node = false;
// Add node
this.data[GRAPH_LINKS].push(newLink);
this.triggerOnChange();
this.storeCurrentData(
"Added custom link connecting [" +
sourceId +
"] with [" +
targetId +
"]"
);
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] = this.getUnusedNodeId();
} else if (this.existsNodeId(newNode[NODE_ID])) {
return;
}
// Basic node properties
newNode.node = true;
newNode.link = false;
// Add node
this.data[GRAPH_NODES].push(newNode);
this.triggerOnChange();
this.storeCurrentData(
"Added custom node with id [" + newNode[NODE_ID] + "]"
);
return newNode;
}
static toStr(item) { static toStr(item) {
if (item === undefined) { if (item === undefined) {
return "UNDEFINED"; return "UNDEFINED";
...@@ -462,3 +138,4 @@ export class Graph extends ManagedData { ...@@ -462,3 +138,4 @@ export class Graph extends ManagedData {
} }
} }
} }
*/
\ No newline at end of file
import ManagedData from "../manageddata";
import {Link} from "./link";
import {NodeType} from "./nodetype";
import {Node} from "./node";
import {GLOBAL_PARAMS} from "../helper/serializableitem";
const GRAPH_PARAMS = ["nodes", "links", "types", ...GLOBAL_PARAMS];
export class Graph extends ManagedData {
public nodes: Node[];
public links: Link[];
public types: NodeType[];
// Callbacks
public onChangeCallbacks: { (data: any): void; } [];
constructor(data: any) {
super(data);
this.onChangeCallbacks = [];
// TODO: Parse all node types
}
/**
* Calls all registered callbacks for the onChange event.
* @private
*/
private triggerOnChange() {
this.onChangeCallbacks.forEach((fn) => fn(this.data));
}
/**
* Triggers change event on data-redo.
*/
protected onRedo() {
// TODO: Actually read new data state
this.triggerOnChange();
}
/**
* Triggers change event on data-undo.
*/
protected onUndo() {
// TODO: Actually read new data state
this.triggerOnChange();
}
protected storableData(data: any): any {
// TODO: Ideally use data parameter
return {
...this.serialize()
};
}
serialize(): any {
return this.serializeProperties(GRAPH_PARAMS);
}
/**
* Adds a pre-created node to the graph.
* @param node New node object.
* @returns True, if successful.
*/
public addNode(node: Node) {
if (this.nodes.includes(node)) {
return true; // Already exists in graph.
}
// TODO: Maybe set node id
this.nodes.push(node);
this.triggerOnChange();
// TODO: Use toString implementation of node
this.storeCurrentData("Added node [" + node + "]");
return true;
}
/**
* Deletes a node from the graph.
* @param node Node object to remove.
* @returns True, if successful.
*/
public deleteNode(node: Node): boolean {
if (!this.nodes.includes(node)) {
return true; // Doesn't even exist in graph to begin with.
}
this.nodes.filter((n: Node) => n !== node);
try {
// No save points should be created when deleting the links
this.disableStoring();
// Delete all the links that contain this node
node.links().forEach((l) => {
l.delete();
});
} finally {
this.enableStoring();
}
this.triggerOnChange();
// TODO: Use toString implementation of node
this.storeCurrentData("Deleted node [" + node + "] and all connected links");
return true;
}
/**
* Adds a pre-created link to the graph.
* @param link New link object.
* @returns True, if successful.
*/
public addLink(link: Link): boolean {
if (this.links.includes(link)) {
return true; // Already exists in graph.
}
// TODO: Maybe set link id
this.links.push(link);
this.triggerOnChange();
// TODO: Use toString implementation of link
this.storeCurrentData("Added link [" + link + "]");
return true;
}
/**
* Deletes a link from the graph.
* @param link Link object to remove.
* @returns True, if successful.
*/
public deleteLink(link: Link): boolean {
if (!this.links.includes(link)) {
return true; // Doesn't even exist in graph to begin with.
}
this.links.filter((l: Link) => l !== link);
this.triggerOnChange();
// TODO: Use toString implementation of link
this.storeCurrentData("Deleted link [" + link + "]");
return true;
}
}
\ No newline at end of file
import {Graph} from "./graph";
import {SerializableItem} from "../helper/serializableitem";
export class GraphElement extends SerializableItem {
protected isNode: boolean;
protected isLink: boolean;
protected graph: Graph;
constructor(graph: Graph) {
super();
this.graph = graph;
this.isNode = false;
this.isLink = false;
}
/**
* 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.");
}
}
\ No newline at end of file
import {GraphElement} from "./graphelement";
import {Graph} from "./graph";
import {Node} from "./node";
import {GLOBAL_PARAMS} from "../helper/serializableitem";
const LINK_PARAMS = ["source", "target", ...GLOBAL_PARAMS];
const LINK_SIM_PARAMS = ["index"];
export class Link extends GraphElement {
public source: Node;
public target: Node;
constructor(graph: Graph) {
super(graph);
this.isLink = true;
}
public delete() {
return this.graph.deleteLink(this);
}
public add(graph: Graph = this.graph) {
this.graph = graph;
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)
};
}
}
\ No newline at end of file
import {Graph} from "./graph";
import {GraphElement} from "./graphelement"
import {NodeType} from "./nodetype";
import {SerializedURL} from "../helper/serializedurl";
import {Link} from "./link";
import {GLOBAL_PARAMS} from "../helper/serializableitem";
const NODE_PARAMS = [
"label",
"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 {
public label: string;
public description: string;
public type: NodeType;
public icon: SerializedURL;
public banner: SerializedURL;
public video: SerializedURL;
public references: SerializedURL[];
constructor(graph: Graph) {
super(graph);
this.isNode = true;
}
public delete() {
return this.graph.deleteNode(this);
}
public add(graph: Graph = this.graph) {
this.graph = graph;
return this.graph.addNode(this);
}
/**
* Calculates a list of all connected links to the current node.
* @returns Array containing all connected links.
*/
public links(): Link[] {
const links: Link[] = [];
this.graph.links.forEach((link) => {
if (link.contains(this)) {
links.push(link);
}
});
return links;
}
/**
* 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)
};
}
}
\ No newline at end of file
import {GLOBAL_PARAMS, SerializableItem} from "../helper/serializableitem";
const NODE_TYPE_PARAMS = ["name", "color", ...GLOBAL_PARAMS];
export class NodeType extends SerializableItem {
public name: string;
public color: string;
serialize(): any {
return this.serializeProperties(NODE_TYPE_PARAMS);
}
}
\ No newline at end of file
/**
* Provides the basic interface for unique, serializable objects.
*/
import {array} from "prop-types";
export const GLOBAL_PARAMS = ["id"];
export class SerializableItem {
public id: string; // 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.
*/
public parse(raw: 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.
* @protected
* @returns New object containing all the desired properties.
*/
protected serializeProperties(params: string[]): 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;
}
}
}
import {GLOBAL_PARAMS, SerializableItem} from "./serializableitem";
const URL_PARAMS = ["link", ...GLOBAL_PARAMS];
export class SerializedURL extends SerializableItem {
public link: string; // The full url
// TODO: URL validator
serialize(): any {
return this.serializeProperties(URL_PARAMS);
}
}
\ No newline at end of file
import {SerializableItem} from "./helper/serializableitem";
import jQuery from "jquery"; import jQuery from "jquery";
const SAVE_BUTTON_ID = "div#ks-editor #toolbar-save"; const SAVE_BUTTON_ID = "div#ks-editor #toolbar-save";
export default class ManagedData { /**
constructor(data) { * 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: any[]; // 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.
/**
* Sets initial states.
* @param data Initial state of data to be stored.
*/
constructor(data: any) {
super();
this.data = data; this.data = data;
this.history = []; // Newest state is always at 0 this.history = []; // Newest state is always at 0
this.historyPosition = 0; this.historyPosition = 0;
...@@ -13,7 +28,11 @@ export default class ManagedData { ...@@ -13,7 +28,11 @@ export default class ManagedData {
this.storeCurrentData("Initial state", false); this.storeCurrentData("Initial state", false);
} }
updateUnsavedChangesHandler() { /**
* If the data has unsaved changes, this will subscribe to the tab-closing event to warn about losing unsaved changes before closing.
* @private
*/
private updateUnsavedChangesHandler() {
if (this.hasUnsavedChanges()) { if (this.hasUnsavedChanges()) {
jQuery(SAVE_BUTTON_ID).removeClass("hidden"); jQuery(SAVE_BUTTON_ID).removeClass("hidden");
window.addEventListener("beforeunload", this.handleBeforeUnload); window.addEventListener("beforeunload", this.handleBeforeUnload);
...@@ -23,35 +42,67 @@ export default class ManagedData { ...@@ -23,35 +42,67 @@ export default class ManagedData {
} }
} }
handleBeforeUnload(e) { /**
var confirmationMessage = * Called on the tab-closing event to trigger a warning, to avoid losing unsaved changes.
* @param e Event.
* @private
*/
private handleBeforeUnload(e: any) {
const confirmationMessage =
"If you leave before saving, unsaved changes will be lost."; "If you leave before saving, unsaved changes will be lost.";
(e || window.event).returnValue = confirmationMessage; //Gecko + IE (e || window.event).returnValue = confirmationMessage; //Gecko + IE
return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc. return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.
} }
hasUnsavedChanges() { /**
* Returns true, if data has unsaved changes.
*/
public hasUnsavedChanges(): boolean {
return this.history[this.historyPosition].id !== this.savedHistoryId; return this.history[this.historyPosition].id !== this.savedHistoryId;
} }
saveChanges() { /**
* Internally marks the current save point as saved.
*/
public markChangesAsSaved() {
this.savedHistoryId = this.history[this.historyPosition].id; this.savedHistoryId = this.history[this.historyPosition].id;
this.updateUnsavedChangesHandler(); this.updateUnsavedChangesHandler();
} }
disableStoring() { /**
* Setter to disable storing save points.
*/
public disableStoring() {
this.storingEnabled = false; this.storingEnabled = false;
} }
enableStoring() { /**
* Setter to enable storing save points.
*/
public enableStoring() {
this.storingEnabled = true; this.storingEnabled = true;
} }
onUndo() {} /**
onRedo() {} * Event triggered after undo.
*/
protected onUndo() {
// No base implementation.
}
/**
* Event triggered after redo.
*/
protected onRedo() {
// No base implementation.
}
undo() { /**
* Go to one step back in the stored history, if available.
* @returns True, if successful.
*/
public undo(): boolean {
if (this.step(1)) { if (this.step(1)) {
this.updateUnsavedChangesHandler(); this.updateUnsavedChangesHandler();
this.onUndo(); this.onUndo();
...@@ -61,7 +112,11 @@ export default class ManagedData { ...@@ -61,7 +112,11 @@ export default class ManagedData {
} }
} }
redo() { /**
* Go one step forward in the stored history, if available.
* @returns True, if successful.
*/
public redo(): boolean {
if (this.step(-1)) { if (this.step(-1)) {
this.updateUnsavedChangesHandler(); this.updateUnsavedChangesHandler();
this.onRedo(); this.onRedo();
...@@ -71,8 +126,14 @@ export default class ManagedData { ...@@ -71,8 +126,14 @@ export default class ManagedData {
} }
} }
step(direction = 1) { /**
var newHistoryPosition = this.historyPosition + Math.sign(direction); * Moves the history pointer to the desired position and adjusts the data object.
* @param direction How many steps to take in the history. Positive for going back in time, negative for going forward.
* @returns True, if successful.
* @private
*/
private step(direction = 1): boolean {
const newHistoryPosition = this.historyPosition + Math.sign(direction);
if ( if (
newHistoryPosition >= this.history.length || newHistoryPosition >= this.history.length ||
...@@ -87,18 +148,28 @@ export default class ManagedData { ...@@ -87,18 +148,28 @@ export default class ManagedData {
return true; return true;
} }
storableData(data) { /**
* Formats the data to the desired stored format.
* @param data The raw data.
* @returns The formatted, cleaned up data to be stored.
*/
protected storableData(data: any): any {
return data; return data;
} }
storeCurrentData(description, relevantChanges = true) { /**
* Creates a save point.
* @param description Description of the current save point. Could describe the difference to the previous save point.
* @param relevantChanges Indicates major or minor changes. Major changes get a new id to indicate an actual changed state. Should usually be true.
*/
public storeCurrentData(description: string, relevantChanges = true) {
if (this.storingEnabled === false) { if (this.storingEnabled === false) {
return; return;
} }
var formattedData = this.storableData(this.data); const formattedData = this.storableData(this.data);
var nextId = 0; let nextId = 0;
if (this.history.length > 0) { if (this.history.length > 0) {
nextId = this.history[0].id; nextId = this.history[0].id;
......
import {GLOBAL_PARAMS, SerializableItem} from "./helper/serializableitem";
import {Graph} from "./graph/graph";
const SPACE_PARAMS = ["name", "description", "graph", ...GLOBAL_PARAMS];
export class Space extends SerializableItem {
public name: string;
public description: string;
public graph: Graph;
serialize(): any {
return this.serializeProperties(SPACE_PARAMS);
}
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment