/** * @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 { 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"; // 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 { generateColors } from "./helpers/color.theme"; 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 { IEvent } from "./interfaces/IEvents"; import { IUndoRedoGraph } from "./interfaces/IGraph"; import { IGraphCallbackData } from "./interfaces/IGraphCallbackData"; import { DEFAULT_NETWORK, INetwork } from "./interfaces/INetwork"; import { IVisjsOptions } from "./interfaces/IVisjsOptions"; 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 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 = 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;