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 extends Partial, IGraphCallbackData>> { graphOptions?: IVisjsOptions; network?: INetwork; 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[]; showEdgeSelction?: boolean } export interface GraphicalEditorComponentState { update: null } class GraphicalEditorComponent extends React.Component, GraphicalEditorComponentState> { /** * The Element containing the Network. */ public network: IUndoRedoGraph; public layout: ILayout; public graphOptions: IVisjsOptions; constructor(props) { super(props); } public tabs: { [index: string]: INetwork } = {}; /** * Function to add the Addons of the Graph: * @param network */ public addGraphAddons(network: IUndoRedoGraph) { 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) => { 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[] = []; 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 = await this.layout.getDialogData({ content: { props: { possibleEdges }, component(props: { possibleEdges: IEdgeTypeDefinition[], onSubmit: (data: IEdgeTypeDefinition) => void; onCancel: (error: any) => void }) { return ( <> Select a possible edge by clicking on the item { props.possibleEdges.map((item, idx) => { props.onSubmit(item)}>{item.type} }) } ) }, }, 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, mainRef: React.RefObject) { 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>) { 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), // }, // '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 public initializeGraph() { const _this = this; // Assign a default Network: const networkToRender: INetwork = 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({ header: 'Close Tab', content: { component: (props) => { return (<>

You are about to close the tab "{tab.label}". Do you want to save changes?

{' '} {' '} {' '} ) }, 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 ( <> ()} 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} > ) } } export default GraphicalEditorComponent;