599 lines
23 KiB
TypeScript
599 lines
23 KiB
TypeScript
import * as React from 'react';
|
|
import { Button, ListGroup } from 'react-bootstrap';
|
|
import { v4 as generateID } from 'uuid';
|
|
import { deepClone, rgetattr, rsetattr } from '../../../lib/helpers/objectMethods';
|
|
import { ILayout } from '../layout/interfaces/ILayout';
|
|
import { ITab } from '../layout/interfaces/ITab';
|
|
import Layout, { ILayoutProps } from '../layout/layout';
|
|
// Import Addons:
|
|
import { makeMeMultiSelect } from './addons/selectionbox.extension';
|
|
import { generateDefaultSelection } from './defaults/default.elements';
|
|
import { generateGraphOptions } from './defaults/default.graph-options';
|
|
import { defaultHotkeys } from './defaults/default.hotkeys';
|
|
import { defaultToolbar } from './defaults/default.toolbar';
|
|
// Graph related imports:
|
|
import { UndoRedoGraph } from './graph';
|
|
import { getSubElements } from './helpers/data.handlers';
|
|
import { IBaseEdgeOptions } from './interfaces/IBaseEdgeOptions';
|
|
import { IBaseNodeOptions } from './interfaces/IBaseNodeOptions';
|
|
import { IEdgeTypeDefinition } from './interfaces/IEdgeTypeDefinition';
|
|
import { IUndoRedoGraph } from './interfaces/IGraph';
|
|
import { IGraphCallbackData } from './interfaces/IGraphCallbackData';
|
|
import { IGraphTemplate } from './interfaces/IGraphTemplate';
|
|
import { DEFAULT_NETWORK, INetwork } from './interfaces/INetwork';
|
|
import { IVisjsOptions } from './interfaces/IVisjsOptions';
|
|
|
|
export interface GraphicalEditorComponentProps<N, E> extends Partial<ILayoutProps<IGraphTemplate<N,E>, IGraphCallbackData<N,E>>> {
|
|
graphOptions?: IVisjsOptions;
|
|
network?: INetwork<N, E>;
|
|
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?: IEdgeTypeDefinition<N, E>[];
|
|
showEdgeSelction?: boolean
|
|
}
|
|
|
|
export interface GraphicalEditorComponentState {
|
|
update: null
|
|
}
|
|
|
|
class GraphicalEditorComponent<N, E> extends React.Component<GraphicalEditorComponentProps<N, E>, GraphicalEditorComponentState> {
|
|
|
|
/**
|
|
* The Element containing the Network.
|
|
*/
|
|
public network: IUndoRedoGraph<N, E>;
|
|
public layout: ILayout;
|
|
|
|
public graphOptions: IVisjsOptions;
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
}
|
|
|
|
public tabs: { [index: string]: INetwork<N, E> } = {};
|
|
|
|
/**
|
|
* Function to add the Addons of the Graph:
|
|
* @param network
|
|
*/
|
|
public addGraphAddons(network: IUndoRedoGraph<N, E>) {
|
|
makeMeMultiSelect(network);
|
|
}
|
|
|
|
/**
|
|
* Function to add an Edge.
|
|
* @param edgeData
|
|
* @param callback
|
|
*/
|
|
protected async _addEdge(edgeData: IBaseEdgeOptions, callback: (edge: IBaseEdgeOptions) => 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
|
|
let 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.layout.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.layout.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.layout.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.layout.showToast('You are not allowed to connect the desired Nodes with this edges', 'error');
|
|
break;
|
|
}
|
|
|
|
} else {
|
|
this.layout.showToast('Double Connections not allowed! You can not connect these elements again', 'error');
|
|
}
|
|
} else {
|
|
this.layout.showToast('Self-Connections forbidden! You can not connect elements to itself', 'error');
|
|
}
|
|
}
|
|
|
|
public async initEditor(ref: React.RefObject<any>, mainRef: 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);
|
|
}
|
|
|
|
// Create the Network
|
|
_this.network = new UndoRedoGraph(ref, mainRef, 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 selection = getSubElements(_this.network, false);
|
|
_this.updateNode(selection);
|
|
}
|
|
} 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);
|
|
}
|
|
}
|
|
|
|
// Make the Default behaviour adding new Nodes
|
|
this.network.on('doubleClick', (params) => {
|
|
if (this.props.enableEditing) {
|
|
// 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.layout.showToast('Copied')
|
|
} catch (e) {
|
|
|
|
}
|
|
});
|
|
|
|
this.network.on('onpaste', async event => {
|
|
try {
|
|
_this.layout.showToast('Pasted')
|
|
} catch (e) {
|
|
}
|
|
});
|
|
|
|
this.network.on('oncontext', (...args) => console.log('rightclick', ...args))
|
|
|
|
|
|
// // 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();
|
|
|
|
// Add all Addons as required.
|
|
this.addGraphAddons(this.network);
|
|
}
|
|
|
|
return this.network;
|
|
}
|
|
|
|
/**
|
|
* 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');
|
|
// }
|
|
}
|
|
|
|
/**
|
|
* Function, which is used to Update an Edge
|
|
* @param edge The Corresponding Edge, which will be updated.
|
|
*/
|
|
public async updateEdges(edge: E) {
|
|
const _self = this;
|
|
|
|
// /** Extract the Component, which should be used in the Prompt */
|
|
// let componentSelector = 'default';
|
|
// if (edge.editorComponentSelector) {
|
|
// componentSelector = edge.editorComponentSelector;
|
|
// }
|
|
|
|
// const comp = this.editPanelDict.edges[componentSelector];
|
|
|
|
// if (comp) {
|
|
// /** Open the Window, with the Edit-Prompt */
|
|
// this.openEditInterface(comp, {
|
|
// inputTemplate: {
|
|
// nodes: [],
|
|
// edges: [edge],
|
|
// type: 'elements'
|
|
// }
|
|
// }, 'Edit Edge', (data) => {
|
|
// _self.network.updateEdge(data.template.edges);
|
|
// });
|
|
// } else {
|
|
// throw TypeError('The Element trys to open an Unkown Edit-Component');
|
|
// }
|
|
|
|
}
|
|
|
|
public addNode(pos: { x: number, y: number }) {
|
|
const _this = this;
|
|
|
|
// switch (this.template.type) {
|
|
// case 'elements':
|
|
// if (this.template.nodes && this.template.nodes.length > 0) {
|
|
// this.template.nodes[0].x = pos.x;
|
|
// this.template.nodes[0].y = pos.y;
|
|
|
|
// let componentSelector = 'default';
|
|
|
|
// if (this.template.nodes.length > 0 && this.template.nodes[0].editorComponentSelector) {
|
|
// componentSelector = this.template.nodes[0].editorComponentSelector;
|
|
// }
|
|
|
|
// this.openEditInterface(
|
|
// this.editPanelDict.nodes[componentSelector],
|
|
// {
|
|
// inputTemplate: deepClone(this.template as ITemplate<N, E>),
|
|
// },
|
|
// 'Add Node'
|
|
// , (data) => {
|
|
// let adapted = adaptIDS(data.template);
|
|
|
|
// if (typeof data.callback === 'function') {
|
|
// adapted = data.callback(adapted);
|
|
// }
|
|
|
|
// _this.network.addNode(adapted.nodes);
|
|
// _this.network.addEdge(adapted.edges);
|
|
// },
|
|
// 'popup'
|
|
// );
|
|
// }
|
|
|
|
// return true;
|
|
// default:
|
|
// this.enableHotkeys();
|
|
// }
|
|
|
|
return false
|
|
}
|
|
|
|
public settings: GraphicalEditorComponentProps<N, E>
|
|
|
|
public initializeGraph() {
|
|
|
|
const _this = this;
|
|
|
|
// Assign a default Network:
|
|
const networkToRender: INetwork<N,E> = this.props.network || DEFAULT_NETWORK();
|
|
|
|
// Initally store the Network to Render
|
|
this.tabs[networkToRender.id] = networkToRender;
|
|
|
|
this.settings = {
|
|
allowUserSelect: true,
|
|
|
|
network: networkToRender,
|
|
|
|
onItemSelected(item) {
|
|
|
|
},
|
|
|
|
onMount(ref, mainRef, layout) {
|
|
// Define the Network.
|
|
_this.initEditor(ref, mainRef).catch(e => _this.layout.showToast(e, 'error'));
|
|
|
|
// Load the Network:
|
|
_this.network.loadData(networkToRender);
|
|
_this.layout = layout;
|
|
},
|
|
|
|
onResize() {
|
|
if (_this.network) {
|
|
_this.network.resize();
|
|
}
|
|
},
|
|
|
|
async onNewTab() {
|
|
// Function to create a new tab.
|
|
try {
|
|
const label = await _this.layout.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) {
|
|
// 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.layout.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!
|
|
|
|
// Use the content of the new Tab as rendering:
|
|
_this.network.loadData(DEFAULT_NETWORK());
|
|
},
|
|
|
|
onUnmount() {
|
|
if (_this.network) {
|
|
// Destroy the Network.
|
|
_this.network.destroy();
|
|
}
|
|
},
|
|
|
|
tabs: {
|
|
active: 'start',
|
|
items: [],
|
|
allowNewTabs: true
|
|
},
|
|
|
|
generateData() {
|
|
// Get the Current Selection.
|
|
const selection = _this.network.network.getSelection(true);
|
|
|
|
return {
|
|
network: _this.network,
|
|
layout: _this.layout,
|
|
selectedEdges: selection.edges,
|
|
selectedNodes: selection.nodes
|
|
}
|
|
},
|
|
|
|
toolbar: defaultToolbar(),
|
|
|
|
hotkeys: defaultHotkeys()
|
|
};
|
|
|
|
return this.settings;
|
|
}
|
|
|
|
public requestRerender() {
|
|
this.setState({
|
|
update: null
|
|
})
|
|
}
|
|
|
|
public componentDidMount() {
|
|
const _this = this;
|
|
}
|
|
|
|
public render() {
|
|
const settings = this.initializeGraph();
|
|
|
|
return (
|
|
<>
|
|
<Layout
|
|
selection={generateDefaultSelection<N,E>()}
|
|
allowUserSelect
|
|
onMount={settings.onMount}
|
|
onResize={settings.onResize}
|
|
onNewTab={settings.onNewTab}
|
|
onTabSelect={settings.onTabSelect}
|
|
onUnmount={settings.onUnmount}
|
|
onNoTabSelected={settings.onNoTabSelected}
|
|
onTabDelete={settings.onTabDelete}
|
|
tabs={settings.tabs}
|
|
generateData={settings.generateData}
|
|
toolbar={settings.toolbar}
|
|
hotkeys={settings.hotkeys}
|
|
></Layout>
|
|
</>
|
|
)
|
|
}
|
|
}
|
|
|
|
export default GraphicalEditorComponent; |