2020-10-25 20:14:51 +00:00
|
|
|
/**
|
|
|
|
* @author Martin Karkowski
|
|
|
|
* @email m.karkowski@zema.de
|
|
|
|
* @create date 2019-04-24 09:31:13
|
2020-10-29 18:20:42 +00:00
|
|
|
* @modify date 2020-10-29 17:44:41
|
2020-10-25 20:14:51 +00:00
|
|
|
* @desc [description]
|
|
|
|
*/
|
|
|
|
|
|
|
|
import { EventEmitter } from 'events';
|
|
|
|
/** Import Undo-Stack */
|
|
|
|
import * as SimpleUndo from 'simple-undo';
|
2020-10-29 18:20:42 +00:00
|
|
|
import { dynamicSort } from '../../../lib/helpers/arrayMethods';
|
2020-10-30 18:30:59 +00:00
|
|
|
import { deepClone, rsetattr } from '../../../lib/helpers/objectMethods';
|
2020-10-29 18:20:42 +00:00
|
|
|
import * as vis from '../../visjs/vis';
|
|
|
|
import { flattenData } from './helpers/data.handlers';
|
|
|
|
import { sortNodes } from './helpers/sort.nodes';
|
2020-10-25 20:14:51 +00:00
|
|
|
import { IBaseEdgeOptions } from './interfaces/IBaseEdgeOptions';
|
|
|
|
import { IBaseNodeOptions } from './interfaces/IBaseNodeOptions';
|
2020-10-30 18:30:59 +00:00
|
|
|
import { IBasicTemplate } from './interfaces/IBasicTemplate';
|
2020-10-25 20:14:51 +00:00
|
|
|
import { IClusters } from './interfaces/IClusters';
|
2020-10-29 18:20:42 +00:00
|
|
|
import { IEvent } from './interfaces/IEvents';
|
2020-10-25 20:14:51 +00:00
|
|
|
import { IBaseGraph, IUndoRedoGraph } from './interfaces/IGraph';
|
2020-10-29 18:20:42 +00:00
|
|
|
import { INetwork } from './interfaces/INetwork';
|
2020-10-25 20:14:51 +00:00
|
|
|
import { IVisjsOptions } from './interfaces/IVisjsOptions';
|
2020-10-30 18:30:59 +00:00
|
|
|
import { stringify, parse } from '../../../lib/helpers/jsonMethods'
|
|
|
|
import { generateId } from '../../../lib/helpers/idMethods';
|
2020-10-29 18:20:42 +00:00
|
|
|
|
2020-10-25 20:14:51 +00:00
|
|
|
|
|
|
|
export class BaseGraph<N, E> implements IBaseGraph<N, E> {
|
|
|
|
|
|
|
|
public visNetwork: any;
|
|
|
|
private _data: { nodes: any, edges: any };
|
|
|
|
private _emitter: EventEmitter;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Containing the SVG-Drawing
|
|
|
|
*/
|
|
|
|
public drawing: any;
|
|
|
|
|
|
|
|
public get network() {
|
|
|
|
return this.visNetwork;
|
|
|
|
}
|
|
|
|
|
|
|
|
public get edges(): Array<IBaseEdgeOptions<E>> {
|
|
|
|
return this._data.edges.get();
|
|
|
|
}
|
|
|
|
|
|
|
|
public get nodes(): Array<IBaseNodeOptions<N>> {
|
|
|
|
return this._data.nodes.get();
|
|
|
|
}
|
|
|
|
|
|
|
|
public get data() {
|
|
|
|
return this._data;
|
|
|
|
}
|
|
|
|
|
|
|
|
public get emitter() {
|
|
|
|
return this._emitter;
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
/** The Reference, where the Graph should be rednered in */
|
2020-10-27 19:22:41 +00:00
|
|
|
protected _reference: React.RefObject<any>,
|
2020-10-25 20:14:51 +00:00
|
|
|
/** The SVG Options. See Options on http://visjs.org/docs/network/ */
|
|
|
|
options: IVisjsOptions = {
|
|
|
|
edges: { smooth: false },
|
|
|
|
physics: {
|
|
|
|
enabled: false,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
) {
|
|
|
|
/** Create the corresponding Data Sets */
|
|
|
|
this._data = {
|
|
|
|
nodes: new vis.DataSet(),
|
|
|
|
edges: new vis.DataSet()
|
|
|
|
};
|
|
|
|
|
|
|
|
/** If no custom method for adding an edge, is provided => generate one */
|
|
|
|
if (options.manipulation === undefined || options.manipulation === false) {
|
|
|
|
options.manipulation = {};
|
|
|
|
rsetattr(options, 'manipulation.enabled', false);
|
|
|
|
}
|
|
|
|
|
2020-10-27 19:22:41 +00:00
|
|
|
options.autoResize = false;
|
2020-10-30 18:30:59 +00:00
|
|
|
|
|
|
|
// Element Height = Viewport height - element.offset.top - desired bottom margin
|
|
|
|
// options.height = _parent.current
|
|
|
|
options.height = (_reference.current.parentElement.clientHeight - 48).toString() + 'px'
|
2020-10-27 19:22:41 +00:00
|
|
|
|
2020-10-25 20:14:51 +00:00
|
|
|
/** Create the VIS-Network */
|
2020-10-27 19:22:41 +00:00
|
|
|
this.visNetwork = new vis.Network(_reference.current, this._data, options);
|
2020-10-25 20:14:51 +00:00
|
|
|
|
|
|
|
/** Create an extra Emitter */
|
|
|
|
this._emitter = new EventEmitter();
|
|
|
|
this._emitter.setMaxListeners(Number.MAX_SAFE_INTEGER - 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
public addNode(element: Array<IBaseNodeOptions<N>> | IBaseNodeOptions<N>) {
|
|
|
|
/** Add the Node Data. */
|
|
|
|
if (Array.isArray(element)) {
|
|
|
|
/** Make shure Flattend Data is used. */
|
|
|
|
this._data.nodes.add(flattenData(element));
|
|
|
|
} else {
|
2020-10-30 18:30:59 +00:00
|
|
|
|
2020-10-25 20:14:51 +00:00
|
|
|
this._data.nodes.add(element);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-30 18:30:59 +00:00
|
|
|
public loadJSON(data: string, overwrite = true, unsafe = true) {
|
|
|
|
/** Read In the JSON File */
|
|
|
|
const content = parse(data, unsafe);
|
|
|
|
/** Load the Data itself */
|
|
|
|
this.loadData(content, overwrite)
|
|
|
|
}
|
|
|
|
|
|
|
|
public getJSON(unsafe = true){
|
|
|
|
const content = this.getData();
|
|
|
|
return stringify(unsafe);
|
|
|
|
}
|
|
|
|
|
2020-10-29 18:20:42 +00:00
|
|
|
public userAddNode(){
|
|
|
|
const _this = this;
|
2020-10-30 18:30:59 +00:00
|
|
|
|
2020-10-29 18:20:42 +00:00
|
|
|
return new Promise<IBaseNodeOptions<any>>((resolve,reject) => {
|
|
|
|
_this.network.addNodeMode();
|
|
|
|
_this.once('addNode', resolve);
|
|
|
|
_this.once('mode', (mode)=> {
|
|
|
|
if (mode !== 'none' && mode !== 'add-node'){
|
|
|
|
reject(mode)
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
public userAddEdge(){
|
|
|
|
const _this = this;
|
|
|
|
|
|
|
|
return new Promise<IBaseEdgeOptions<any>>((resolve,reject) => {
|
|
|
|
_this.network.addEdgeMode();
|
|
|
|
_this.once('addEdge', resolve);
|
|
|
|
_this.once('mode', (mode)=> {
|
|
|
|
if (mode !== 'none' && mode !== 'add-edge'){
|
|
|
|
reject(mode)
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
public disableEditMode(){
|
|
|
|
this.network.disableEditMode();
|
|
|
|
}
|
|
|
|
|
2020-10-25 20:14:51 +00:00
|
|
|
public removeNode(id: string | number) {
|
|
|
|
try {
|
|
|
|
/** Try removing the Node */
|
|
|
|
this.visNetwork.body.data
|
|
|
|
|
|
|
|
this._data.nodes.remove({ id });
|
|
|
|
} catch (err) {
|
|
|
|
console.error('Removing Node' + err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public updateNode(options: Array<IBaseNodeOptions<N>> | IBaseNodeOptions<N>) {
|
|
|
|
try {
|
|
|
|
/** Try updateing the Node. */
|
|
|
|
this._data.nodes.update(options);
|
|
|
|
} catch (err) {
|
|
|
|
console.error('Upadting Node' + err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public getNode<T=N>(id) {
|
|
|
|
return this._data.nodes.get(id) as IBaseNodeOptions<T>;
|
|
|
|
}
|
|
|
|
|
|
|
|
public getEdge<T=E>(id) {
|
|
|
|
return this._data.edges.get(id) as IBaseEdgeOptions<T>;
|
|
|
|
}
|
|
|
|
|
|
|
|
public clearNodes() {
|
|
|
|
this._data.nodes.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
public clearEdges() {
|
|
|
|
this._data.edges.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
public addEdge(values: IBaseEdgeOptions<E> | Array<IBaseEdgeOptions<E>>) {
|
|
|
|
/** Add the Node Data. */
|
|
|
|
this._data.edges.add(values);
|
|
|
|
}
|
|
|
|
|
|
|
|
public updateEdge(values: IBaseEdgeOptions<E> | Array<IBaseEdgeOptions<E>>) {
|
|
|
|
try {
|
|
|
|
/** Try to Update the Edge-Data */
|
|
|
|
this._data.edges.update(values);
|
|
|
|
} catch (err) {
|
|
|
|
console.error('Upadting Edge' + err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public removeEdge(id: string | number) {
|
|
|
|
try {
|
|
|
|
/** Try to remove the Edge */
|
|
|
|
this._data.edges.remove({ id });
|
|
|
|
} catch (err) {
|
|
|
|
console.error('Removing Edge' + err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public clear() {
|
|
|
|
/** Clearsout existing Nodes and Edges */
|
|
|
|
this.clearNodes();
|
|
|
|
this.clearEdges();
|
|
|
|
}
|
|
|
|
|
|
|
|
public destroy() {
|
|
|
|
/** Destroys the complete Rendered Graph */
|
|
|
|
this.visNetwork.destroy();
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Function to fully fit the Graph */
|
|
|
|
public fit(options: {
|
|
|
|
nodes?: Array<string>,
|
|
|
|
animation?: boolean | {
|
|
|
|
duration?: number,
|
|
|
|
easingFunction?: String
|
|
|
|
}
|
|
|
|
} = {
|
|
|
|
animation: true
|
|
|
|
}) {
|
|
|
|
/**
|
|
|
|
* Afterwards use this Elements to Perform the
|
|
|
|
* Animation and zoom, that all Nodes are Viewable.
|
|
|
|
*/
|
|
|
|
this.network.fit(options);
|
|
|
|
}
|
|
|
|
|
|
|
|
public hasNode(id: string): boolean {
|
|
|
|
return this._data.nodes.get(id) !== null;
|
|
|
|
}
|
|
|
|
|
|
|
|
public on(eventName: IEvent, callback: (...args) => void): () => void {
|
|
|
|
|
|
|
|
const _self = this;
|
|
|
|
|
|
|
|
switch (eventName) {
|
|
|
|
case 'click':
|
|
|
|
/** Make a difference between a Click event and e.g. a select Event. */
|
|
|
|
let counter = 0;
|
|
|
|
let timer: any = null;
|
|
|
|
|
|
|
|
const func = (...args) => {
|
|
|
|
counter++;
|
|
|
|
if (counter === 1) {
|
|
|
|
timer = setTimeout(() => {
|
|
|
|
callback(...args);
|
|
|
|
counter = 0;
|
|
|
|
}, 200);
|
|
|
|
} else {
|
|
|
|
counter = 0;
|
|
|
|
clearTimeout(timer);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
return this.network.subscribe(eventName, func);
|
|
|
|
case 'addEdge':
|
|
|
|
case 'removeEdge':
|
|
|
|
case 'updateEdge':
|
|
|
|
this._data.edges.on(eventName.split('Edge')[0], callback);
|
|
|
|
return () => {
|
|
|
|
_self._data.edges.off(eventName.split('Edge')[0], callback);
|
|
|
|
};
|
|
|
|
case 'addNode':
|
|
|
|
case 'removeNode':
|
|
|
|
case 'updateNode':
|
|
|
|
this._data.nodes.on(eventName.split('Node')[0], callback);
|
|
|
|
return () => {
|
|
|
|
_self._data.nodes.off(eventName.split('Node')[0], callback);
|
|
|
|
};
|
|
|
|
case 'dataUpdate':
|
|
|
|
const events = [
|
|
|
|
'add',
|
|
|
|
'remove',
|
|
|
|
'update'
|
|
|
|
]
|
|
|
|
|
|
|
|
for (const type of ['edges', 'nodes']) {
|
|
|
|
for (const event of events) {
|
|
|
|
this._data[type].on(event, callback);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
for (const type of ['edges', 'nodes']) {
|
|
|
|
for (const event of events) {
|
|
|
|
this._data[type].off(event, callback);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
default:
|
|
|
|
return this.visNetwork.subscribe(eventName, (...args) => {
|
|
|
|
callback(...args);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public getClusters() {
|
|
|
|
|
|
|
|
const clusters: {
|
|
|
|
[index: string]: {
|
|
|
|
nodes: Array<string>,
|
|
|
|
clusters: Array<string>,
|
|
|
|
options: IBaseNodeOptions,
|
|
|
|
}
|
|
|
|
} = {};
|
|
|
|
|
|
|
|
// Iterate over all Nodes of the Graph.
|
|
|
|
// Find their clusters. Based on their order
|
|
|
|
// Define an arry containing all array etc.
|
|
|
|
for (const node of this.nodes) {
|
|
|
|
// Get all Nodes in Cluster
|
|
|
|
const res = this.visNetwork.findNode(node.id) as Array<string>;
|
|
|
|
// Remove the First-Element
|
|
|
|
res.pop()
|
|
|
|
|
|
|
|
let first = true;
|
|
|
|
let lastCluster: string = '';
|
|
|
|
|
|
|
|
// Iterate over the used Clusters.
|
|
|
|
// If its the first cluster, add the node
|
|
|
|
// Otherwise add the last cluster.
|
|
|
|
for (const cluster of res.reverse()) {
|
|
|
|
|
|
|
|
// If the Cluster isnt know => create an obect, describing the Cluster.
|
|
|
|
if (clusters[cluster] === undefined) {
|
|
|
|
|
|
|
|
clusters[cluster] = {
|
|
|
|
nodes: new Array<string>(),
|
|
|
|
clusters: new Array<string>(),
|
|
|
|
options: {
|
|
|
|
id: cluster
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test if the Cluster has special Ids.
|
|
|
|
if (this.visNetwork.clustering.body.nodes[cluster]) {
|
|
|
|
clusters[cluster].options = this.visNetwork.clustering.body.nodes[cluster].options;
|
|
|
|
clusters[cluster].options.id = cluster
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add the Node the the first Cluster only
|
|
|
|
if (first) {
|
|
|
|
clusters[cluster].nodes.push(node.id);
|
|
|
|
} else {
|
|
|
|
clusters[cluster].clusters.push(lastCluster);
|
|
|
|
}
|
|
|
|
|
|
|
|
lastCluster = cluster;
|
|
|
|
first = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Define the Return type.
|
|
|
|
const ret: IClusters = [];
|
|
|
|
|
|
|
|
// Add all clsuters.
|
|
|
|
for (const key of Object.getOwnPropertyNames(clusters)) {
|
|
|
|
ret.push(Object.assign(clusters[key], { id: key }));
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret.sort(dynamicSort('clusters.length'));
|
|
|
|
}
|
|
|
|
|
|
|
|
public readinClusters(clusters: Array<{
|
|
|
|
nodes: Array<string>,
|
|
|
|
clusters: Array<string>,
|
|
|
|
id: string,
|
|
|
|
options: IBaseNodeOptions,
|
|
|
|
}>) {
|
|
|
|
|
|
|
|
for (const cluster of clusters) {
|
|
|
|
// Automatically create a cluster.
|
|
|
|
this.network.cluster({
|
|
|
|
joinCondition(node: IBaseNodeOptions) {
|
|
|
|
return cluster.nodes.indexOf(node.id) !== -1 || cluster.clusters.indexOf(node.id) !== -1
|
|
|
|
},
|
|
|
|
clusterNodeProperties: cluster.options,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public once(eventName: IEvent, callback: (...args) => void): () => void {
|
|
|
|
|
|
|
|
let func = (...args) => { };
|
|
|
|
const _self = this;
|
|
|
|
|
|
|
|
switch (eventName) {
|
|
|
|
case 'click':
|
|
|
|
/** Make a difference between a Click event and e.g. a select Event. */
|
|
|
|
let counter = 0;
|
|
|
|
let timer: any = null;
|
|
|
|
|
|
|
|
func = (...args) => {
|
|
|
|
counter++;
|
|
|
|
if (counter === 1) {
|
|
|
|
timer = setTimeout(() => {
|
|
|
|
callback(...args);
|
|
|
|
counter = 0;
|
|
|
|
}, 200);
|
|
|
|
} else {
|
|
|
|
counter = 0;
|
|
|
|
clearTimeout(timer);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
return this.network.once(eventName, func);
|
|
|
|
case 'addEdge':
|
|
|
|
case 'removeEdge':
|
|
|
|
case 'updateEdge':
|
|
|
|
func = (...args) => {
|
|
|
|
callback(...args);
|
|
|
|
_self._data.edges.off(eventName.split('Edge')[0], func);
|
|
|
|
}
|
|
|
|
|
|
|
|
this._data.edges.on(eventName.split('Edge')[0], func);
|
|
|
|
break;
|
|
|
|
case 'addNode':
|
|
|
|
case 'removeNode':
|
|
|
|
case 'updateNode':
|
|
|
|
func = (...args) => {
|
|
|
|
callback(...args);
|
2020-10-29 18:20:42 +00:00
|
|
|
_self._data.nodes.off(eventName.split('Node')[0], func);
|
2020-10-25 20:14:51 +00:00
|
|
|
}
|
|
|
|
|
2020-10-29 18:20:42 +00:00
|
|
|
this._data.nodes.on(eventName.split('Node')[0], func);
|
2020-10-25 20:14:51 +00:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return this.visNetwork.once(eventName, callback);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public useVersionControl = true;
|
|
|
|
public version: string = '20200713';
|
|
|
|
public name: string;
|
2020-10-30 18:30:59 +00:00
|
|
|
public id: string = generateId();
|
2020-10-25 20:14:51 +00:00
|
|
|
|
|
|
|
public getData(name: string = this.name, id: string = this.id, version: string = this.version): INetwork<N, E> {
|
2020-10-29 18:20:42 +00:00
|
|
|
|
|
|
|
const position = this.network.getViewPosition();
|
|
|
|
const scale = this.network.getScale();
|
|
|
|
|
2020-10-25 20:14:51 +00:00
|
|
|
return {
|
|
|
|
nodes: this.nodes,
|
|
|
|
edges: this.edges,
|
|
|
|
clusters: this.getClusters(),
|
|
|
|
version,
|
|
|
|
name,
|
2020-10-29 18:20:42 +00:00
|
|
|
id,
|
|
|
|
view: {
|
|
|
|
position,
|
|
|
|
scale
|
|
|
|
}
|
2020-10-25 20:14:51 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
public resize(){
|
2020-10-30 18:30:59 +00:00
|
|
|
this.network.setSize(undefined, this._reference.current.parentElement.offsetHeight.toString() + 'px');
|
2020-10-25 20:14:51 +00:00
|
|
|
this.network.redraw();
|
|
|
|
}
|
|
|
|
|
2020-10-30 18:30:59 +00:00
|
|
|
/**
|
|
|
|
* Function to load a Cluster.
|
|
|
|
* @param data
|
|
|
|
* @param clear
|
|
|
|
* @param cluster
|
|
|
|
*/
|
2020-10-25 20:14:51 +00:00
|
|
|
public loadData(data: INetwork<N, E>,
|
|
|
|
clear = true,
|
|
|
|
cluster = true) {
|
|
|
|
let somethingLoaded = false;
|
|
|
|
|
|
|
|
if (this.useVersionControl && ((data.version && data.version != this.version) || !data.version)) {
|
|
|
|
throw Error('File Version not compatible');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (clear) {
|
|
|
|
this.clear();
|
|
|
|
}
|
|
|
|
|
2020-10-29 18:20:42 +00:00
|
|
|
let adaptViewport = false;
|
|
|
|
|
|
|
|
if (data?.nodes) {
|
2020-10-25 20:14:51 +00:00
|
|
|
somethingLoaded = true;
|
2020-10-29 18:20:42 +00:00
|
|
|
adaptViewport = true;
|
2020-10-25 20:14:51 +00:00
|
|
|
|
|
|
|
this.addNode(sortNodes(data.nodes));
|
|
|
|
if (data.edges) {
|
|
|
|
this.addEdge(data.edges);
|
|
|
|
}
|
|
|
|
if (cluster && data.clusters) {
|
|
|
|
this.readinClusters(data.clusters);
|
|
|
|
}
|
2020-10-29 18:20:42 +00:00
|
|
|
|
|
|
|
// Get the View.
|
|
|
|
if (data.view) {
|
|
|
|
this.network.moveTo({
|
|
|
|
position: data.view.position,
|
|
|
|
scale: data.view.scale,
|
|
|
|
animation: false
|
|
|
|
});
|
|
|
|
|
|
|
|
adaptViewport = false;
|
|
|
|
}
|
2020-10-25 20:14:51 +00:00
|
|
|
}
|
|
|
|
|
2020-10-29 18:20:42 +00:00
|
|
|
if (somethingLoaded && adaptViewport) {
|
2020-10-25 20:14:51 +00:00
|
|
|
this.fit({
|
|
|
|
animation: false,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class UndoRedoGraph<N, E> extends BaseGraph<N, E> implements IUndoRedoGraph<N, E> {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Element for the Undo / Redo Queue
|
|
|
|
*/
|
|
|
|
private _stack: SimpleUndo;
|
|
|
|
/**
|
|
|
|
* Flag to Toggle Loading and disabling
|
|
|
|
* Elements
|
|
|
|
*/
|
2020-10-29 18:20:42 +00:00
|
|
|
public disableUndoRedo = false;
|
2020-10-25 20:14:51 +00:00
|
|
|
private _stateCounter = 0;
|
|
|
|
private _currentCounter = 0;
|
|
|
|
|
|
|
|
public undoEnabled = false;
|
|
|
|
public redoEnabled = false;
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
/** The Reference, where the Graph should be rednered in */
|
|
|
|
reference: React.RefObject<any>,
|
|
|
|
/** The SVG Options. See Options on http://visjs.org/docs/network/ */
|
|
|
|
options: IVisjsOptions = {
|
|
|
|
edges: { smooth: false },
|
|
|
|
physics: {
|
|
|
|
enabled: false,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
maxStackLength = 100
|
|
|
|
) {
|
2020-10-30 18:30:59 +00:00
|
|
|
super(reference,options);
|
2020-10-25 20:14:51 +00:00
|
|
|
|
|
|
|
const _self = this;
|
|
|
|
|
|
|
|
if (maxStackLength > 0) {
|
|
|
|
/** Stack containing the Steps */
|
|
|
|
this._stack = new SimpleUndo({
|
|
|
|
maxLength: maxStackLength,
|
|
|
|
provider: (callback: (data: any) => void) => {
|
|
|
|
|
|
|
|
callback(_self.getData());
|
|
|
|
|
|
|
|
// Raise the State Counters
|
|
|
|
_self._stateCounter = _self._stack.count();
|
|
|
|
_self._currentCounter = _self._stack.count();
|
|
|
|
|
|
|
|
|
|
|
|
// Update the Undo / Redo Flags
|
|
|
|
_self._updateFlags();
|
|
|
|
},
|
|
|
|
});
|
|
|
|
// Save the Initial State
|
|
|
|
this.save();
|
|
|
|
|
|
|
|
this.on('dragEnd', (params) => {
|
|
|
|
// Make shure the Position is only stored,
|
|
|
|
// nodes are dragged and not the View.
|
2020-10-29 18:20:42 +00:00
|
|
|
if (!_self.disableUndoRedo && params.nodes.length > 0) {
|
2020-10-25 20:14:51 +00:00
|
|
|
_self.network.storePositions();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Link the Emitter with the Updates.
|
|
|
|
const events = [
|
|
|
|
'addNode',
|
|
|
|
'removeNode',
|
|
|
|
'updateNode',
|
|
|
|
'addEdge',
|
|
|
|
'removeEdge',
|
|
|
|
'updateEdge'
|
|
|
|
];
|
|
|
|
|
|
|
|
// Function to Update the Stack.
|
|
|
|
const _updateStack = () => {
|
|
|
|
_self.save();
|
|
|
|
};
|
|
|
|
|
|
|
|
for (const event of events) {
|
|
|
|
this.on(event as any, _updateStack);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public save() {
|
2020-10-29 18:20:42 +00:00
|
|
|
if (!this.disableUndoRedo && this._stack) {
|
2020-10-25 20:14:51 +00:00
|
|
|
this._stack.save();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public undo() {
|
|
|
|
if (this._stack) {
|
|
|
|
const _self = this;
|
|
|
|
|
|
|
|
this._stack.undo((data) => {
|
|
|
|
|
|
|
|
// Prevent Storing the Content
|
2020-10-29 18:20:42 +00:00
|
|
|
_self.disableUndoRedo = true;
|
2020-10-25 20:14:51 +00:00
|
|
|
|
|
|
|
// Update the Current Counter;
|
|
|
|
_self._currentCounter--;
|
|
|
|
|
|
|
|
// Remove the current Graph.
|
|
|
|
_self.clear();
|
|
|
|
|
|
|
|
// Only if Data are available => Load them
|
|
|
|
if (data !== null) {
|
|
|
|
// Add the Edges and Nodes
|
|
|
|
_self.loadData(data);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update the Flags
|
|
|
|
_self._updateFlags();
|
|
|
|
|
|
|
|
// Enable storing updates again.
|
2020-10-29 18:20:42 +00:00
|
|
|
_self.disableUndoRedo = false;
|
2020-10-25 20:14:51 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private _updateFlags() {
|
|
|
|
this.redoEnabled = (this._currentCounter < this._stateCounter);
|
|
|
|
this.undoEnabled = (this._currentCounter > 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
public redo() {
|
|
|
|
if (this._stack) {
|
|
|
|
const _self = this;
|
|
|
|
this._stack.redo((data) => {
|
|
|
|
// Only if Data are available => Load them
|
|
|
|
if (data != null) {
|
|
|
|
|
|
|
|
// Prevent Storing the Content during REDO operation
|
2020-10-29 18:20:42 +00:00
|
|
|
_self.disableUndoRedo = true;
|
2020-10-25 20:14:51 +00:00
|
|
|
|
|
|
|
_self.loadData(data);
|
|
|
|
|
|
|
|
// Update the Flags
|
|
|
|
_self._updateFlags();
|
|
|
|
|
|
|
|
// Enable storing updates again.
|
2020-10-29 18:20:42 +00:00
|
|
|
_self.disableUndoRedo = false;
|
2020-10-25 20:14:51 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public resetHistory() {
|
|
|
|
if (this._stack) {
|
|
|
|
// Define the State
|
|
|
|
const state = {
|
|
|
|
nodes: this.nodes,
|
|
|
|
edges: this.edges,
|
|
|
|
};
|
|
|
|
|
|
|
|
this._stack.initialize(state);
|
|
|
|
this._stack.clear();
|
|
|
|
|
|
|
|
// Raise the State Counters
|
|
|
|
this._stateCounter = this._stack.count();
|
|
|
|
this._currentCounter = this._stack.count();
|
|
|
|
|
|
|
|
// Update the Undo / Redo Flags
|
|
|
|
this._updateFlags();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|