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 DynamicLayout, { IDynamicLayoutProps } from '../layout/dynamicLayout'; import { ILayout } from '../layout/interfaces/ILayout'; import { ISelection } from '../layout/interfaces/ISelection'; import { ITab } from '../layout/interfaces/ITab'; import Layout 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 { adaptIDS, adaptPositions, getSelectedElements } from './helpers/data.handlers'; import { IBaseEdgeOptions } from './interfaces/IBaseEdgeOptions'; import { IBaseNodeOptions } from './interfaces/IBaseNodeOptions'; import { IBasicTemplate } from './interfaces/IBasicTemplate'; 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'; import Selection, { SelectionProps } from '../layout/selection'; import DynamicRenderer from '../dynamic/dynamicRenderer'; import TabEntry, { ITabProps } from '../layout/tabs'; import { IJsonSchema } from '../../types/IJSONSchema'; import { ITabEntry } from '../layout/interfaces/ITabEntry'; import { parseWithFunctions, stringifyWithFunctions } from '../../../lib/helpers/jsonMethods'; import { readDataFromClipboard, writeToClipboard } from '../helpers/clipboard'; import { Network, __esModule } from '../../visjs/vis'; import { DH_UNABLE_TO_CHECK_GENERATOR } from 'constants'; import {getCurrentThemeColors} from '../helpers/colors'; import {generateColors } from './helpers/color.theme'; export interface GraphicalEditorComponentProps { 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 LayoutHandler: ILayout; public TabHandler: ITabEntry; public graphOptions: IVisjsOptions; 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]: 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.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 = await this.LayoutHandler.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.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, 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) { 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 = { 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, 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>) { 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 public LayoutSettings: IDynamicLayoutProps> public TabSettings: ITabProps; protected _template: IBasicTemplate public selectTemplate(template: IBasicTemplate){ this._template = template; } public initializeGraph() { const _this = this; // Assign a default Network: const networkToRender: INetwork = 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({ 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! 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({ 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 })}
{ // Enable Hotkeys _this.LayoutHandler.hotkeysEnabled = true; }} onMouseLeave={e => { // Disable Hotkeys _this.LayoutHandler.hotkeysEnabled = false; }}>
) }, gridSettings: { h: 5, w: 10, x: 5, y: 0, minW: 3, minH: 3, }, id: 'main', label: 'Editor', props: { }, visible: true, bg: 'light', text: 'dark' }, { component(props: {selection: ISelection>}){ return (> selection={props.selection} allowUserSelect={true} onItemSelected={template =>_this.selectTemplate(template)}>) }, gridSettings: { h: 5, 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(){ return (<>Minimap) }, gridSettings: { h: 3, w: 3, x: 0, y: 5, maxW: 5, }, id: 'minimap', label: 'Minimap', props: {}, hideable: true, bg:'light', visible: false, 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 = { 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;