nope/resources/ui/graph/editor.tsx
2020-10-29 19:20:42 +01:00

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;