1085 lines
33 KiB
TypeScript
1085 lines
33 KiB
TypeScript
|
/**
|
||
|
* @author Martin Karkowski
|
||
|
* @email m.karkowski@zema.de
|
||
|
* @create date 2020-11-25 08:19:39
|
||
|
* @modify date 2020-11-25 12:05:20
|
||
|
* @desc [description]
|
||
|
*/
|
||
|
|
||
|
import * as React from "react";
|
||
|
import { Button, ListGroup } from "react-bootstrap";
|
||
|
import { v4 as generateID } from "uuid";
|
||
|
import {
|
||
|
parseWithFunctions,
|
||
|
stringifyWithFunctions
|
||
|
} from "../../../lib/helpers/jsonMethods";
|
||
|
import {
|
||
|
deepClone,
|
||
|
rgetattr,
|
||
|
rsetattr
|
||
|
} from "../../../lib/helpers/objectMethods";
|
||
|
import { IJsonSchema } from "../../../lib/types/IJSONSchema";
|
||
|
import { generateDefaultSelection } from "../graph/defaults/default.elements";
|
||
|
import { generateGraphOptions } from "../graph/defaults/default.graph-options";
|
||
|
import { defaultHotkeys } from "../graph/defaults/default.hotkeys";
|
||
|
import { defaultToolbar } from "../graph/defaults/default.toolbar";
|
||
|
import { readDataFromClipboard, writeToClipboard } from "../helpers/clipboard";
|
||
|
import { getCurrentThemeColors } from "../helpers/colors";
|
||
|
import DynamicLayout, { IDynamicLayoutProps } from "../layout/dynamicLayout";
|
||
|
import { ILayout } from "../layout/interfaces/ILayout";
|
||
|
import { ISelection } from "../layout/interfaces/ISelection";
|
||
|
import { ITab } from "../layout/interfaces/ITab";
|
||
|
import { ITabEntry } from "../layout/interfaces/ITabEntry";
|
||
|
import Selection from "../layout/selection";
|
||
|
import TabEntry, { ITabProps } from "../layout/tabs";
|
||
|
// Graph related imports:
|
||
|
|
||
|
export interface GraphicalEditorComponentProps<N, E> {
|
||
|
graphOptions?: any;
|
||
|
network?: any;
|
||
|
addEdgeCallback?: (edgeData: E, callback: (edgeData: E) => void) => void;
|
||
|
editOnSelect?: boolean;
|
||
|
editOnChange?: boolean;
|
||
|
parseFunctions?: boolean;
|
||
|
enableContextMenu?: boolean;
|
||
|
enableEditing?: boolean;
|
||
|
hidePanelOnDeselect?: boolean;
|
||
|
hideToolbar?: boolean;
|
||
|
hideRightPanel?: boolean;
|
||
|
allowSelfConnection?: boolean;
|
||
|
allowDoubleConnections?: boolean;
|
||
|
edgeTypes?: any[];
|
||
|
showEdgeSelction?: boolean;
|
||
|
}
|
||
|
|
||
|
export interface GraphicalEditorComponentState {
|
||
|
update: null;
|
||
|
}
|
||
|
|
||
|
class GraphicalEditorComponent<N, E> extends React.Component<
|
||
|
GraphicalEditorComponentProps<N, E>,
|
||
|
GraphicalEditorComponentState
|
||
|
> {
|
||
|
/**
|
||
|
* The Element containing the Network.
|
||
|
*/
|
||
|
public LayoutHandler: ILayout;
|
||
|
public TabHandler: ITabEntry;
|
||
|
public theme: {
|
||
|
colors: {
|
||
|
primary: string;
|
||
|
secondary: string;
|
||
|
success: string;
|
||
|
info: string;
|
||
|
warning: string;
|
||
|
danger: string;
|
||
|
light: string;
|
||
|
dark: string;
|
||
|
};
|
||
|
font: {
|
||
|
size: number;
|
||
|
type: string;
|
||
|
};
|
||
|
};
|
||
|
constructor(props) {
|
||
|
super(props);
|
||
|
this.theme = getCurrentThemeColors();
|
||
|
}
|
||
|
|
||
|
public tabs: { [index: string]: go.GraphLinksModel } = {};
|
||
|
|
||
|
/**
|
||
|
* Function to add an Edge.
|
||
|
* @param edgeData
|
||
|
* @param callback
|
||
|
*/
|
||
|
protected async _addEdge(edgeData: any, callback: (edge: any) => void) {
|
||
|
// Defaulty prevent self connections.
|
||
|
if (this.props.allowSelfConnection || edgeData.from !== edgeData.to) {
|
||
|
let addEdge = true;
|
||
|
|
||
|
// If multiple Connections from one node to the same node are
|
||
|
// for bidden, test if there already exists such an edge.
|
||
|
if (!this.props.allowDoubleConnections) {
|
||
|
this.Network.edges.forEach((edge) => {
|
||
|
if (edge.from === edgeData.from && edge.to === edgeData.to) {
|
||
|
addEdge = false;
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// If adding an Edge is allowed => Add the Edge.
|
||
|
if (addEdge) {
|
||
|
// Manually add an ID to the Edge
|
||
|
edgeData.id = generateID();
|
||
|
|
||
|
// Extract the From and To Node.
|
||
|
const from = this.Network.getNode(edgeData.from);
|
||
|
const to = this.Network.getNode(edgeData.to);
|
||
|
|
||
|
const _this = this;
|
||
|
const _addEdge = (type: IEdgeTypeDefinition<N, E>) => {
|
||
|
if (typeof type.customAddFunction == "function") {
|
||
|
type.customAddFunction(from, to, _this.Network);
|
||
|
} else {
|
||
|
// Define the Edge Object
|
||
|
const edge = Object.assign(deepClone(type.visual), {
|
||
|
from: edgeData.from,
|
||
|
to: edgeData.to
|
||
|
});
|
||
|
|
||
|
// Add the Edge.
|
||
|
callback(edge);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
switch (edgeData.type) {
|
||
|
case "automatic":
|
||
|
// Search for Possible Connection Types.
|
||
|
const possibleEdges: IEdgeTypeDefinition<N, E>[] = [];
|
||
|
|
||
|
for (const edgeType of this.props.edgeTypes || []) {
|
||
|
if (
|
||
|
(edgeType.fromTypes.includes(from.type) ||
|
||
|
edgeType.fromTypes.length === 0) &&
|
||
|
(edgeType.toTypes.includes(to.type) ||
|
||
|
edgeType.toTypes.length === 0)
|
||
|
) {
|
||
|
possibleEdges.push(edgeType);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Test if Edges has been found
|
||
|
switch (possibleEdges.length) {
|
||
|
case 0:
|
||
|
this.LayoutHandler.showToast(
|
||
|
"No Matching Edge Type has been found. Please select the corresponding Edge Type manually",
|
||
|
"info"
|
||
|
);
|
||
|
return;
|
||
|
case 1:
|
||
|
// Define the New Edge.
|
||
|
return _addEdge(possibleEdges[0]);
|
||
|
default:
|
||
|
if (this.props.showEdgeSelction) {
|
||
|
// Create a Popup, on which the corresponding edge can be selected:
|
||
|
try {
|
||
|
const type: IEdgeTypeDefinition<
|
||
|
N,
|
||
|
E
|
||
|
> = await this.LayoutHandler.getDialogData({
|
||
|
content: {
|
||
|
props: {
|
||
|
possibleEdges
|
||
|
},
|
||
|
component(props: {
|
||
|
possibleEdges: IEdgeTypeDefinition<N, E>[];
|
||
|
onSubmit: (data: IEdgeTypeDefinition<N, E>) => void;
|
||
|
onCancel: (error: any) => void;
|
||
|
}) {
|
||
|
return (
|
||
|
<>
|
||
|
Select a possible edge by clicking on the item
|
||
|
<ListGroup>
|
||
|
{props.possibleEdges.map((item, idx) => {
|
||
|
<ListGroup.Item
|
||
|
key={idx}
|
||
|
onClick={(_) => props.onSubmit(item)}
|
||
|
>
|
||
|
{item.type}
|
||
|
</ListGroup.Item>;
|
||
|
})}
|
||
|
</ListGroup>
|
||
|
<Button
|
||
|
variant="danger"
|
||
|
onClick={(e) => props.onCancel(e)}
|
||
|
>
|
||
|
Cancel{" "}
|
||
|
</Button>
|
||
|
</>
|
||
|
);
|
||
|
}
|
||
|
},
|
||
|
header: "Please select an edge-type!",
|
||
|
closeButton: true
|
||
|
});
|
||
|
|
||
|
// Add the Edge itself.
|
||
|
return _addEdge(type);
|
||
|
} catch (e) {
|
||
|
// The User has aborted the selection.
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
this.LayoutHandler.showToast(
|
||
|
"Mulitple Valid Edge Type has been found. Please select the corresponding Edge Type manually",
|
||
|
"info"
|
||
|
);
|
||
|
}
|
||
|
return;
|
||
|
default:
|
||
|
for (const type of this.props.edgeTypes || []) {
|
||
|
if (
|
||
|
edgeData.type === type.type &&
|
||
|
(type.fromTypes.includes(from.type) ||
|
||
|
type.fromTypes.length === 0) &&
|
||
|
(type.toTypes.includes(to.type) || type.toTypes.length === 0)
|
||
|
) {
|
||
|
// Quit the Method
|
||
|
return _addEdge(type);
|
||
|
}
|
||
|
}
|
||
|
this.LayoutHandler.showToast(
|
||
|
"You are not allowed to connect the desired Nodes with this edges",
|
||
|
"error"
|
||
|
);
|
||
|
break;
|
||
|
}
|
||
|
} else {
|
||
|
this.LayoutHandler.showToast(
|
||
|
"Double Connections not allowed! You can not connect these elements again",
|
||
|
"error"
|
||
|
);
|
||
|
}
|
||
|
} else {
|
||
|
this.LayoutHandler.showToast(
|
||
|
"Self-Connections forbidden! You can not connect elements to itself",
|
||
|
"error"
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
protected async _editNodes(
|
||
|
_template: IBasicTemplate<N, E>,
|
||
|
mode: "add" | "update" = "update"
|
||
|
) {
|
||
|
// Defaulty prevent self connections.
|
||
|
if (_template?.nodes.length > 0) {
|
||
|
try {
|
||
|
if (_template.nodes[0].editorComponentSelector && false) {
|
||
|
} else {
|
||
|
// Open up the corresponding Node Panel:
|
||
|
_template.nodes[0] = await this.LayoutHandler.getDialogData({
|
||
|
content: {
|
||
|
component: "DynamicForm",
|
||
|
props: {
|
||
|
schema: {
|
||
|
type: "object",
|
||
|
properties: {
|
||
|
label: {
|
||
|
description: "Label of the Node",
|
||
|
type: "string"
|
||
|
},
|
||
|
title: {
|
||
|
description: "Tooltip, presented on Hovering",
|
||
|
type: "string"
|
||
|
}
|
||
|
// color: {
|
||
|
// description: 'Color of the Node.',
|
||
|
// type: 'string'
|
||
|
// }
|
||
|
},
|
||
|
required: ["label"]
|
||
|
} as IJsonSchema,
|
||
|
uiSchema: {
|
||
|
// color: {
|
||
|
// "ui:widget": "color"
|
||
|
// }
|
||
|
},
|
||
|
data: Object.assign(
|
||
|
generateColors(_template.nodes[0].shape, this.theme),
|
||
|
_template.nodes[0]
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
switch (mode) {
|
||
|
case "add":
|
||
|
this.Network.addNode(_template.nodes);
|
||
|
this.Network.addEdge(_template.edges);
|
||
|
break;
|
||
|
case "update":
|
||
|
this.Network.updateNode(_template.nodes);
|
||
|
this.Network.updateEdge(_template.edges);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// Tabs
|
||
|
this.tabs[this.TabSettings.tabs.active] = this.Network.getData();
|
||
|
} catch (error) {
|
||
|
console.log(error);
|
||
|
}
|
||
|
} else {
|
||
|
this.LayoutHandler.showToast("Please Select a Node Before.", "error");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public initEditor(ref: React.RefObject<any>) {
|
||
|
if (process.browser) {
|
||
|
const _this = this;
|
||
|
|
||
|
// Based on the Provided Options =>
|
||
|
// select the corresponding graph options,
|
||
|
// either use th provided one or use the
|
||
|
// default options.
|
||
|
if (this.props.graphOptions) {
|
||
|
this.graphOptions = this.props.graphOptions;
|
||
|
} else {
|
||
|
this.graphOptions = generateGraphOptions();
|
||
|
}
|
||
|
|
||
|
if (!rgetattr(this.graphOptions, "manipulation.enabled", false)) {
|
||
|
rsetattr(this.graphOptions, "manipulation.enabled", false);
|
||
|
}
|
||
|
|
||
|
// Generate the Default Callback for adding an Edge
|
||
|
rsetattr(this.graphOptions, "manipulation.addEdge", (data, callback) => {
|
||
|
_this._addEdge(data, callback).catch(console.error);
|
||
|
});
|
||
|
|
||
|
// Generate the Default Callback for adding a Node
|
||
|
rsetattr(this.graphOptions, "manipulation.addNode", (data, callback) => {
|
||
|
_this._addNode(data).catch(console.error);
|
||
|
});
|
||
|
|
||
|
// Create the Network
|
||
|
this.Network = new UndoRedoGraph(ref, this.graphOptions);
|
||
|
|
||
|
// Add a new item if possible.
|
||
|
const editItemIfPossible = (params) => {
|
||
|
if (params.nodes.length === 1) {
|
||
|
// A Node was Selected. Figure out, whether it is a cluster or not.
|
||
|
const nodeID = params.nodes[0];
|
||
|
|
||
|
if (_this.Network.network.isCluster(nodeID)) {
|
||
|
// Uncluster the Nodes
|
||
|
_this.Network.network.openCluster(nodeID);
|
||
|
} else {
|
||
|
// Open up the Settings Tab
|
||
|
const nodes = getSelectedElements(_this.Network, false);
|
||
|
_this._editNodes({
|
||
|
edges: [],
|
||
|
nodes,
|
||
|
type: "element"
|
||
|
});
|
||
|
}
|
||
|
} else if (params.edges.length === 1) {
|
||
|
// A Node was Selected. Figure out, whether it is a cluster or not.
|
||
|
const edges = _this.Network.data.edges.get(params.edges[0]);
|
||
|
_this.updateEdges(edges);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
if (!this.Network) {
|
||
|
throw Error("Something went wrong");
|
||
|
}
|
||
|
|
||
|
// Make the Default behaviour adding new Nodes
|
||
|
this.Network.on("doubleClick", (params) => {
|
||
|
if (_this.props.enableEditing || true) {
|
||
|
// Test if no Element was Selected
|
||
|
if (params.nodes.length === 0 && params.edges.length === 0) {
|
||
|
// If so, just add the new Element
|
||
|
_this._addNode(params.pointer.canvas);
|
||
|
} else {
|
||
|
editItemIfPossible(params);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
this.Network.on("oncopy", (event) => {
|
||
|
try {
|
||
|
_this.LayoutHandler.showToast("Copied");
|
||
|
writeToClipboard(
|
||
|
stringifyWithFunctions(_this.createTemplateOfSelectedElements())
|
||
|
);
|
||
|
} catch (e) {}
|
||
|
});
|
||
|
|
||
|
this.Network.on("onpaste", async (event) => {
|
||
|
try {
|
||
|
const data = await readDataFromClipboard();
|
||
|
const mousePosition = _this.Network.network.DOMtoCanvas({
|
||
|
x: _this.LayoutHandler.currentMousePosition.offsetX,
|
||
|
y: _this.LayoutHandler.currentMousePosition.offsetY
|
||
|
});
|
||
|
_this.paste(parseWithFunctions(data), mousePosition, false);
|
||
|
_this.LayoutHandler.showToast("Pasted");
|
||
|
} catch (e) {
|
||
|
_this.LayoutHandler.showToast("Failed Pasting", "error");
|
||
|
console.error(e);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// // Make shure the Sidepanel is working correctly
|
||
|
this.Network.on("select", (params) => {
|
||
|
// if (_this.graphOptions.enableEditing){
|
||
|
// if (_this.graphOptions.editOnSelect && _this.graphOptions.hidePanelOnDeselect && params.nodes.length === 0 && params.edges.length === 0){
|
||
|
// _this.layout.panels.right.hide();
|
||
|
// } else if (_this.graphOptions.editOnSelect){
|
||
|
// editItemIfPossible(params);
|
||
|
// }
|
||
|
// }
|
||
|
});
|
||
|
|
||
|
// enableClusterPreview(this);
|
||
|
|
||
|
// this.network.addNode(this._nodes);
|
||
|
// this.network.addEdge(this._edges);
|
||
|
|
||
|
// editor.resize();
|
||
|
console.log("TABS", this.tabs);
|
||
|
this.Network.loadData(
|
||
|
this.tabs[this.TabSettings?.tabs?.active] || DEFAULT_NETWORK()
|
||
|
);
|
||
|
|
||
|
// Add all Addons as required.
|
||
|
this.addGraphAddons(this.Network);
|
||
|
}
|
||
|
|
||
|
return this.Network;
|
||
|
}
|
||
|
/**
|
||
|
* Function, which is used to provide a
|
||
|
*/
|
||
|
public createTemplateOfSelectedElements() {
|
||
|
const selected: IBasicTemplate<N, E> = {
|
||
|
nodes: [],
|
||
|
edges: [],
|
||
|
type: "element"
|
||
|
};
|
||
|
|
||
|
const selection = this.Network.network.getSelection(true);
|
||
|
|
||
|
selected.nodes = deepClone(
|
||
|
this.Network.nodes.filter(
|
||
|
(item) => selection.nodes.indexOf(item.id) !== -1
|
||
|
)
|
||
|
);
|
||
|
selected.edges = deepClone(
|
||
|
this.Network.edges.filter(
|
||
|
(item) => selection.edges.indexOf(item.id) !== -1
|
||
|
)
|
||
|
);
|
||
|
|
||
|
return selected;
|
||
|
}
|
||
|
|
||
|
public paste(
|
||
|
template: IBasicTemplate<N, E>,
|
||
|
position: {
|
||
|
x: number;
|
||
|
y: number;
|
||
|
},
|
||
|
useExistingNodesForEdges: boolean
|
||
|
) {
|
||
|
const data = adaptPositions(
|
||
|
adaptIDS(template, useExistingNodesForEdges),
|
||
|
position
|
||
|
);
|
||
|
|
||
|
this.Network.addNode(data.nodes);
|
||
|
this.Network.addEdge(data.edges);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Function to Update the Data of a Node.
|
||
|
* @param selection The Selected Node.
|
||
|
*/
|
||
|
public async updateNode(selection: Array<IBaseNodeOptions<N>>) {
|
||
|
const _self = this;
|
||
|
|
||
|
/** Extract the Component, which should be used in the Prompt */
|
||
|
let componentSelector = "default";
|
||
|
if (selection.length > 0 && selection[0].editorComponentSelector) {
|
||
|
componentSelector = selection[0].editorComponentSelector;
|
||
|
}
|
||
|
|
||
|
// const comp = this.editPanelDict.nodes[componentSelector];
|
||
|
|
||
|
// if (comp) {
|
||
|
// /** Open the Window, with the Edit-Prompt */
|
||
|
// this.openEditInterface(this.editPanelDict.nodes[componentSelector], {
|
||
|
// inputTemplate: {
|
||
|
// nodes: selection,
|
||
|
// edges: [],
|
||
|
// type: 'elements'
|
||
|
// },
|
||
|
// },
|
||
|
// 'Edit Node',
|
||
|
// (data) => {
|
||
|
// _self.network.updateNode(data.template.nodes);
|
||
|
// });
|
||
|
// } else {
|
||
|
// this.layout.showToast('Editor is Trying to open an Unkown Edit-Component', 'error');
|
||
|
// }
|
||
|
}
|
||
|
|
||
|
protected async _addNode(pos: { x: number; y: number }) {
|
||
|
// Defaulty prevent self connections.
|
||
|
if (this._template?.nodes.length > 0) {
|
||
|
try {
|
||
|
// Adapt its IDs. (Creates a Copy)
|
||
|
let _template = adaptIDS(this._template);
|
||
|
// Adapt its Position
|
||
|
_template = adaptPositions(_template, pos);
|
||
|
|
||
|
// Remove the Initial Label:
|
||
|
delete _template.nodes[0].label;
|
||
|
|
||
|
await this._editNodes(_template, "add");
|
||
|
} catch (error) {
|
||
|
console.error(error);
|
||
|
}
|
||
|
} else {
|
||
|
this.LayoutHandler.showToast("Please Select a Node Before.", "error");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public EditorSettings: GraphicalEditorComponentProps<N, E>;
|
||
|
public LayoutSettings: IDynamicLayoutProps<IGraphCallbackData<N, E>>;
|
||
|
public TabSettings: ITabProps;
|
||
|
|
||
|
protected _template: IBasicTemplate<N, E>;
|
||
|
public selectTemplate(template: IBasicTemplate<N, E>) {
|
||
|
this._template = template;
|
||
|
}
|
||
|
|
||
|
public initializeGraph() {
|
||
|
const _this = this;
|
||
|
|
||
|
// Assign a default Network:
|
||
|
const networkToRender: INetwork<N, E> =
|
||
|
this.props.network ||
|
||
|
this.tabs[this.TabSettings?.tabs?.active] ||
|
||
|
DEFAULT_NETWORK();
|
||
|
|
||
|
// Initally store the Network to Render
|
||
|
this.tabs[networkToRender.id] = networkToRender;
|
||
|
|
||
|
this.TabSettings = {
|
||
|
onMount(item) {
|
||
|
_this.TabHandler = item;
|
||
|
},
|
||
|
|
||
|
async onNewTab() {
|
||
|
// Function to create a new tab.
|
||
|
try {
|
||
|
if (_this.TabHandler.tabs.length == 0) {
|
||
|
const tab = {
|
||
|
delteable: true,
|
||
|
id: generateID(),
|
||
|
label: "Unsaved Content"
|
||
|
};
|
||
|
|
||
|
await _this.TabHandler.createTab(tab);
|
||
|
|
||
|
// Store the Content of the tab:
|
||
|
_this.tabs[tab.id] = _this.Network.getData();
|
||
|
}
|
||
|
|
||
|
const label = await _this.LayoutHandler.getDialogData({
|
||
|
header: "Enter Network-Name",
|
||
|
content: {
|
||
|
component: "DynamicForm",
|
||
|
props: {
|
||
|
schema: {
|
||
|
type: "string",
|
||
|
description: "Name of the Element"
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const id = Date.now().toString();
|
||
|
// const label = id;
|
||
|
|
||
|
_this.tabs[id] = DEFAULT_NETWORK();
|
||
|
|
||
|
return {
|
||
|
id,
|
||
|
label,
|
||
|
delteable: true
|
||
|
};
|
||
|
} catch (error) {
|
||
|
return false;
|
||
|
}
|
||
|
},
|
||
|
|
||
|
async onTabSelect(oldTab: ITab, newTab: ITab) {
|
||
|
if (_this.Network) {
|
||
|
// Contains the Main Logic for selecting another tab
|
||
|
// Therefore
|
||
|
if (oldTab) {
|
||
|
_this.tabs[oldTab.id] = _this.Network.getData();
|
||
|
}
|
||
|
// Use the content of the new Tab as rendering:
|
||
|
_this.Network.loadData(_this.tabs[newTab.id]);
|
||
|
}
|
||
|
return true;
|
||
|
},
|
||
|
|
||
|
async onTabDelete(tab: ITab, force: boolean) {
|
||
|
// A Tab should be deleted
|
||
|
|
||
|
if (force) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
const saveData = await _this.LayoutHandler.getDialogData<boolean>({
|
||
|
header: "Close Tab",
|
||
|
content: {
|
||
|
component: (props) => {
|
||
|
return (
|
||
|
<>
|
||
|
<p>
|
||
|
You are about to close the tab "{tab.label}". Do you want
|
||
|
to save changes?
|
||
|
</p>
|
||
|
<Button
|
||
|
variant="success"
|
||
|
onClick={(_) => props.onSubmit(true)}
|
||
|
>
|
||
|
Yes
|
||
|
</Button>{" "}
|
||
|
<Button
|
||
|
variant="danger"
|
||
|
onClick={(_) => props.onSubmit(false)}
|
||
|
>
|
||
|
No
|
||
|
</Button>{" "}
|
||
|
<Button
|
||
|
variant="secondary"
|
||
|
onClick={(_) => props.onCancel(new Error("Canceled"))}
|
||
|
>
|
||
|
Cancel
|
||
|
</Button>{" "}
|
||
|
</>
|
||
|
);
|
||
|
},
|
||
|
props: {}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// The variable save Data contains the info, whether the user wants to save or discard the changes
|
||
|
return true;
|
||
|
} catch (error) {
|
||
|
return false;
|
||
|
}
|
||
|
},
|
||
|
|
||
|
async onNoTabSelected() {
|
||
|
// Nothing to Display!
|
||
|
|
||
|
if (_this.Network) {
|
||
|
console.log("No Tab");
|
||
|
// Use the content of the new Tab as rendering:
|
||
|
_this.Network.loadData(DEFAULT_NETWORK());
|
||
|
}
|
||
|
},
|
||
|
|
||
|
async onTabEdit(tab) {
|
||
|
const label = await _this.LayoutHandler.getDialogData<string>({
|
||
|
header: "Enter Network-Name",
|
||
|
content: {
|
||
|
component: "DynamicForm",
|
||
|
props: {
|
||
|
schema: {
|
||
|
type: "string",
|
||
|
description: "Name of the Element"
|
||
|
},
|
||
|
data: tab.label
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
tab.label = label;
|
||
|
|
||
|
return tab;
|
||
|
},
|
||
|
|
||
|
tabs: {
|
||
|
active: "-1",
|
||
|
allowNewTabs: true,
|
||
|
items: []
|
||
|
}
|
||
|
};
|
||
|
|
||
|
this.LayoutSettings = {
|
||
|
components: [
|
||
|
{
|
||
|
component() {
|
||
|
const graph = React.useRef(null);
|
||
|
// Similar to componentDidMount and componentDidUpdate:
|
||
|
// Createa a Function, that if mounted defines the Network.
|
||
|
React.useEffect(() => {
|
||
|
// Define the Network.
|
||
|
_this.initEditor(graph);
|
||
|
|
||
|
// Return a Function, that will destroy the network.
|
||
|
return () => {
|
||
|
_this.tabs[
|
||
|
_this.TabSettings.tabs.active
|
||
|
] = _this.Network.getData();
|
||
|
// Destroy the Network.
|
||
|
_this.Network.destroy();
|
||
|
_this.Network = null;
|
||
|
};
|
||
|
});
|
||
|
|
||
|
return (
|
||
|
<>
|
||
|
{React.createElement(TabEntry, { ..._this.TabSettings })}
|
||
|
<div
|
||
|
ref={graph}
|
||
|
style={{ height: "100%" }}
|
||
|
onMouseEnter={(e) => {
|
||
|
// Enable Hotkeys
|
||
|
_this.LayoutHandler.hotkeysEnabled = true;
|
||
|
}}
|
||
|
onMouseLeave={(e) => {
|
||
|
// Disable Hotkeys
|
||
|
_this.LayoutHandler.hotkeysEnabled = false;
|
||
|
}}
|
||
|
></div>
|
||
|
</>
|
||
|
);
|
||
|
},
|
||
|
gridSettings: {
|
||
|
h: 6,
|
||
|
w: 11,
|
||
|
x: 5,
|
||
|
y: 0,
|
||
|
minW: 3,
|
||
|
minH: 3
|
||
|
},
|
||
|
id: "main",
|
||
|
label: "Editor",
|
||
|
props: {},
|
||
|
visible: true,
|
||
|
bg: "light",
|
||
|
text: "dark"
|
||
|
},
|
||
|
{
|
||
|
component(props: { selection: ISelection<IBasicTemplate<N, E>> }) {
|
||
|
return (
|
||
|
<Selection<IBasicTemplate<N, E>>
|
||
|
selection={props.selection}
|
||
|
allowUserSelect={true}
|
||
|
onItemSelected={(template) => _this.selectTemplate(template)}
|
||
|
></Selection>
|
||
|
);
|
||
|
},
|
||
|
gridSettings: {
|
||
|
h: 3,
|
||
|
w: 4,
|
||
|
x: 0,
|
||
|
y: 0,
|
||
|
maxW: 5
|
||
|
},
|
||
|
id: "selection",
|
||
|
label: "Selection",
|
||
|
props: {
|
||
|
selection: generateDefaultSelection()
|
||
|
},
|
||
|
hideable: true,
|
||
|
bg: "light",
|
||
|
visible: true,
|
||
|
text: "dark",
|
||
|
showLabel: true
|
||
|
},
|
||
|
{
|
||
|
component() {
|
||
|
const [img, setImg] = React.useState();
|
||
|
const [pos, setPos] = React.useState<{
|
||
|
left: number;
|
||
|
top: number;
|
||
|
height: number;
|
||
|
width: number;
|
||
|
}>({
|
||
|
left: 0,
|
||
|
top: 0,
|
||
|
width: 0,
|
||
|
height: 0
|
||
|
});
|
||
|
const [style, setStyle] = React.useState<any>();
|
||
|
const radar = React.useRef(null);
|
||
|
const image = React.useRef(null);
|
||
|
|
||
|
// Similar to componentDidMount and componentDidUpdate:
|
||
|
// Createa a Function, that if mounted subscribe to changes
|
||
|
// of the Nework.
|
||
|
React.useEffect(() => {
|
||
|
const wait = () => {
|
||
|
return new Promise((resolve, reject) => {
|
||
|
_this.Network.once("afterDrawing", resolve);
|
||
|
});
|
||
|
};
|
||
|
|
||
|
const updateImage = async () => {
|
||
|
// Store the current View Settings
|
||
|
const scale = _this.Network.network.getScale();
|
||
|
const translate = _this.Network.network.getViewPosition();
|
||
|
|
||
|
// Zoom out for the Image.
|
||
|
_this.Network.fit({ animation: false });
|
||
|
|
||
|
// Wait for the Network to Redraw
|
||
|
await wait();
|
||
|
|
||
|
const canvas = _this.Network.network.body.container.getElementsByTagName(
|
||
|
"canvas"
|
||
|
)[0];
|
||
|
const imgData = canvas.toDataURL("image/png");
|
||
|
setImg(imgData);
|
||
|
|
||
|
/**
|
||
|
* Extract the Widht and Height
|
||
|
*/
|
||
|
const {
|
||
|
clientWidth,
|
||
|
clientHeight
|
||
|
} = _this.Network.network.body.container;
|
||
|
|
||
|
// Store the Position.
|
||
|
setPos({
|
||
|
left: translate.x - clientWidth / scale / 2,
|
||
|
top: translate.y - clientHeight / scale / 2,
|
||
|
width: clientWidth / scale,
|
||
|
height: clientHeight / scale
|
||
|
});
|
||
|
|
||
|
// Use inital View
|
||
|
_this.Network.network.moveTo({
|
||
|
position: translate,
|
||
|
scale: scale,
|
||
|
animation: false
|
||
|
});
|
||
|
};
|
||
|
|
||
|
const getScaling = (start, size, current, scale = true) => {
|
||
|
if (scale) {
|
||
|
return Math.min(1, Math.max(0, (current - start) / size));
|
||
|
}
|
||
|
return (current - start) / size;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Function to draw the current Positon of the View.
|
||
|
*/
|
||
|
const drawRadar = () => {
|
||
|
if (radar?.current && image?.current) {
|
||
|
const displayedWidth = image.current.clientWidth;
|
||
|
const displayedHeight = image.current.clientHeight;
|
||
|
|
||
|
/**
|
||
|
* Extract the Widht and Height
|
||
|
*/
|
||
|
const {
|
||
|
clientWidth,
|
||
|
clientHeight
|
||
|
} = _this.Network.network.body.container;
|
||
|
|
||
|
// Determine visible Nodes:
|
||
|
const scale = _this.Network.network.getScale();
|
||
|
const translate = _this.Network.network.getViewPosition();
|
||
|
|
||
|
// Contains the Left Position of the View.
|
||
|
// The following Transformation is used:
|
||
|
// _____
|
||
|
// |(top,left)-Original = 0,0
|
||
|
// | ___
|
||
|
// | |(top, left of the View)
|
||
|
//
|
||
|
|
||
|
const current = {
|
||
|
left: translate.x - clientWidth / scale / 2,
|
||
|
top: translate.y - clientHeight / scale / 2,
|
||
|
width: clientWidth / scale,
|
||
|
height: clientHeight / scale
|
||
|
};
|
||
|
|
||
|
const top = getScaling(pos.top, pos.height, current.top);
|
||
|
const bottom = getScaling(
|
||
|
pos.top,
|
||
|
pos.height,
|
||
|
current.top + current.height
|
||
|
);
|
||
|
const height = bottom - top;
|
||
|
const left = getScaling(pos.left, pos.width, current.left);
|
||
|
const right = getScaling(
|
||
|
pos.left,
|
||
|
pos.width,
|
||
|
current.left + current.width
|
||
|
);
|
||
|
const width = right - left;
|
||
|
|
||
|
setStyle({
|
||
|
position: "absolute",
|
||
|
backgroundColor: "rgba(16, 84, 154, 0.26)",
|
||
|
top:
|
||
|
(
|
||
|
image.current.offsetTop +
|
||
|
top * displayedHeight
|
||
|
).toString() + "px",
|
||
|
left:
|
||
|
(
|
||
|
image.current.offsetLeft +
|
||
|
left * displayedWidth
|
||
|
).toString() + "px",
|
||
|
height: (height * displayedHeight).toString() + "px",
|
||
|
width:
|
||
|
Math.max(
|
||
|
0,
|
||
|
width * displayedWidth - image.current.offsetLeft
|
||
|
).toString() + "px"
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const listeners: Partial<{ [K in IEvent]: any }> = {
|
||
|
dataUpdate: updateImage,
|
||
|
afterDrawing: drawRadar
|
||
|
};
|
||
|
|
||
|
const subscriptions: Array<() => void> = [];
|
||
|
|
||
|
if (_this.Network) {
|
||
|
for (const key in listeners) {
|
||
|
subscriptions.push(
|
||
|
_this.Network.on(key as IEvent, listeners[key])
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
// Return a Function, that will destroy the network.
|
||
|
return () => {
|
||
|
for (const unsubscribe of subscriptions) {
|
||
|
unsubscribe();
|
||
|
}
|
||
|
};
|
||
|
});
|
||
|
|
||
|
return (
|
||
|
<div style={{ overflow: "hidden" }}>
|
||
|
<img
|
||
|
ref={image}
|
||
|
src={img}
|
||
|
style={{
|
||
|
position: "absolute",
|
||
|
maxHeight: "100%",
|
||
|
maxWidth: "100%"
|
||
|
}}
|
||
|
></img>
|
||
|
<div ref={radar} style={style}></div>
|
||
|
</div>
|
||
|
);
|
||
|
},
|
||
|
gridSettings: {
|
||
|
h: 2,
|
||
|
w: 4,
|
||
|
x: 0,
|
||
|
y: 4,
|
||
|
maxW: 5
|
||
|
},
|
||
|
id: "minimap",
|
||
|
label: "Minimap",
|
||
|
props: {},
|
||
|
hideable: true,
|
||
|
bg: "light",
|
||
|
visible: true,
|
||
|
text: "dark",
|
||
|
showLabel: true
|
||
|
},
|
||
|
{
|
||
|
component() {
|
||
|
return <>Preview</>;
|
||
|
},
|
||
|
gridSettings: {
|
||
|
h: 3,
|
||
|
w: 3,
|
||
|
x: 3,
|
||
|
y: 5
|
||
|
},
|
||
|
id: "preview",
|
||
|
label: "Preview",
|
||
|
props: {},
|
||
|
hideable: true,
|
||
|
bg: "light",
|
||
|
visible: false,
|
||
|
text: "dark"
|
||
|
}
|
||
|
],
|
||
|
layoutSettings: {
|
||
|
width: process.browser ? window.innerWidth : 1920,
|
||
|
autoSize: false,
|
||
|
preventCollision: false,
|
||
|
cols: 15,
|
||
|
compactType: "horizontal",
|
||
|
onLayoutChange() {
|
||
|
if (_this.Network) {
|
||
|
_this.Network.resize();
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
generateData() {
|
||
|
// Get the Current Selection.
|
||
|
const selection = _this.Network.network.getSelection(true);
|
||
|
|
||
|
const ret: IGraphCallbackData<N, E> = {
|
||
|
network: _this.Network,
|
||
|
layout: _this.LayoutHandler,
|
||
|
selectedEdges: selection.edges,
|
||
|
selectedNodes: selection.nodes,
|
||
|
tabs: _this.TabHandler
|
||
|
};
|
||
|
|
||
|
Object.defineProperty(ret, "network", {
|
||
|
get() {
|
||
|
return _this.Network;
|
||
|
}
|
||
|
});
|
||
|
|
||
|
Object.defineProperty(ret, "layout", {
|
||
|
get() {
|
||
|
return _this.LayoutHandler;
|
||
|
}
|
||
|
});
|
||
|
|
||
|
return ret;
|
||
|
},
|
||
|
onResize() {
|
||
|
if (_this.Network) {
|
||
|
_this.Network.resize();
|
||
|
}
|
||
|
},
|
||
|
onMount(layout) {
|
||
|
_this.LayoutHandler = layout;
|
||
|
},
|
||
|
toolbar: defaultToolbar(),
|
||
|
hotkeys: defaultHotkeys()
|
||
|
};
|
||
|
|
||
|
return this.LayoutSettings;
|
||
|
}
|
||
|
|
||
|
public requestRerender() {
|
||
|
this.setState({
|
||
|
update: null
|
||
|
});
|
||
|
}
|
||
|
|
||
|
public componentDidMount() {
|
||
|
const _this = this;
|
||
|
}
|
||
|
|
||
|
public render() {
|
||
|
this.initializeGraph();
|
||
|
return React.createElement(DynamicLayout, { ...this.LayoutSettings });
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export default GraphicalEditorComponent;
|