/** * @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 { 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 extends React.Component< GraphicalEditorComponentProps, 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) => { 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[] = []; 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[]; 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: 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> }) { return ( > selection={props.selection} allowUserSelect={true} onItemSelected={(template) => _this.selectTemplate(template)} > ); }, 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(); 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 (
); }, 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 = { 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;