nope/resources/ui/graph/editor.tsx
2020-11-01 13:01:57 +01:00

1008 lines
40 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 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';
import { IEvent } from './interfaces/IEvents';
import { resolve } from 'path';
export interface GraphicalEditorComponentProps<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 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<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.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<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.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<N,E>, 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<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);
}
// 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<N,E> = {
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<N,E>,
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<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');
// }
}
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<N, E>
public LayoutSettings: IDynamicLayoutProps<IGraphCallbackData<N,E>>
public TabSettings: ITabProps;
protected _template: IBasicTemplate<N,E>
public selectTemplate(template: IBasicTemplate<N,E>){
this._template = template;
}
public initializeGraph() {
const _this = this;
// Assign a default Network:
const networkToRender: INetwork<N,E> = 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<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!
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<string>({
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 })}
<div ref={graph} style={{ height: '100%' }} onMouseEnter={e => {
// Enable Hotkeys
_this.LayoutHandler.hotkeysEnabled = true;
}} onMouseLeave={e => {
// Disable Hotkeys
_this.LayoutHandler.hotkeysEnabled = false;
}}></div>
</>
)
},
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<IBasicTemplate<N,E>>}){
return (<Selection<IBasicTemplate<N,E>> selection={props.selection} allowUserSelect={true} onItemSelected={template =>_this.selectTemplate(template)}></Selection>)
},
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() {
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<any>();
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: (width * displayedWidth).toString() + 'px'
});
}
};
const listeners: Partial<{[K in IEvent]: any}> = {
'dataUpdate': updateImage,
'afterDrawing' : drawRadar
}
let 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 (
<div style={{overflow: 'hidden'}}>
<img ref={image} src={img} style={{maxHeight:'100%', maxWidth: '100%'}}></img>
<div ref={radar} style={style}></div>
</div>
)
},
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<N,E> = {
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;