nope/resources/ui/graph/editor.ts.bak

926 lines
33 KiB
TypeScript
Raw Normal View History

2020-10-25 20:14:51 +00:00
/**
* @author Martin Karkowski
* @email m.karkowski@zema.de
* @create date 2020-04-09 11:49:53
* @modify date 2020-07-22 21:58:43
* @desc [description]
*/
import { OnInit, Input, ViewChild, OnDestroy, Type, Component } from '@angular/core';
import { rsetattr, rgetattr, deepClone } from '../../@zema/ZISS-TypeScript-Library/src/Object-Methods';
import { ITemplate, IMustacheTemplate } from './interfaces/ITemplate';
import { IEditPage } from './edit-pages/edit-pages.interface';
import { initEditor } from './user-interface/editor';
import { defaultToolbar } from './defaults/default.toolbar';
import { IContextConfig, rigthClickActions, generateDefaulContextMenu } from './defaults/default.context-menu';
import { generateGraphOptions } from './defaults/default.graph-options';
import { generateAddFunction } from './defaults/default.add-edge-method';
import { genDefaultDict } from './defaults/default.edit-dict';
import { stringify, parse, parseWithFunctions } from '../../@zema/ZISS-TypeScript-Library/src/JSON';
import { enableClusterPreview } from './addons/cluster.preview';
import 'style-loader!angular2-toaster/toaster.css';
import { ZemaServiceProvider } from '../../@zema/zema-service-provider.service';
import { writeToClipboard } from '../../@zema/ZISS-Browser-Library/src/clipboard';
import { getSubElements, adaptPositions, adaptIDS } from './helpers/data.handlers';
import { IUndoRedoGraph } from './interfaces/IGraph';
import { IBaseNodeOptions } from '../../@zema/ZISS-Network/type/IBaseNodeOptions';
import { IBaseEdgeOptions } from '../../@zema/ZISS-Network/type/IBaseEdgeOptions';
import { IGraphToolComponent, ICallbackData } from './interfaces/IGraphTool';import { ILayoutOptions, IBasicLayoutComponent, IRigthClickActions, ISelectionTemplate, IToolbarConfig, IPossiblePanels, ISelectionConfig } from '../gui-components-basic-layout/types/interfaces';
import { defaultHotkeys } from './defaults/default.hotkeys';
import { BasicLayoutComponent } from '../gui-components-basic-layout/src/layout.component';
import { TemplateEditorComponent } from './edit-pages/template.edit-page';
import { IVisjsOptions } from './interfaces/IVisjsOptions';
import { waitFor, sleep } from '../../@zema/ZISS-TypeScript-Library/src/Async-Helpers';
import { NbThemeService, NbGlobalPhysicalPosition } from '@nebular/theme';
@Component({
template: ``
})
export class BaseGraphEditor<N extends IBaseNodeOptions, E extends IBaseEdgeOptions, D extends ICallbackData<N,E>> implements IGraphToolComponent<N,E,D>,OnInit, OnDestroy {
public parseFunctions = true;
public layoutOptions: ILayoutOptions<ITemplate<N,E> | IMustacheTemplate, D>;
public options: {
addEdgeCallback?: (edgeData: E, callback: (edgeData: E) => void) => void;
editOnSelect?: boolean;
editOnChange?: boolean;
parseFunctions?: boolean;
enableContextMenu?: boolean;
enableEditing?: boolean;
hidePanelOnDeselect?: boolean;
hideToolbar?: boolean;
hideRightPanel?: boolean;
} = {
enableEditing: true,
enableContextMenu: true,
}
/**
* Element for Providing the VIS-JS Options
*/
@Input()
public set visjsOptions(value: IVisjsOptions) {
this._visjsOptions = value;
if (this.network) {
this.network.network.setOptions(value);
}
}
public get visjsOptions(): IVisjsOptions {
return this._visjsOptions;
}
private _visjsOptions: IVisjsOptions = generateGraphOptions();
@Input()
public set nodes(nodes: Array<N | IBaseNodeOptions>) {
this._nodes = nodes;
// If a Network is available, use this one
// to Render Elements.
if (this.network) {
this.network.clearNodes();
this.network.addNode(nodes);
}
}
public get nodes() {
// If a Network is available, use this one
// to return the Elements.
if (this.network) {
return this.network.nodes;
}
// Otherwise return an empty Array
return [];
}
private _nodes = new Array<N | IBaseNodeOptions>();
@Input()
public set edges(edges: Array<E | IBaseEdgeOptions>) {
this._edges = edges;
// If a Network is available, use this one
// to Render Elements.
if (this.network) {
this.network.clearEdges();
this.network.addEdge(edges);
}
}
public get edges() {
if (this.network) {
return this.network.edges;
}
return [];
}
private _edges = new Array<E | IBaseEdgeOptions>();
@Input()
public set editPanelDict(value: { nodes: { [index: string]: Type<IEditPage<N,E>> }, edges: { [index: string]: Type<IEditPage<N,E>> } }) {
if (!value.nodes.default || !value.edges.default) {
throw TypeError('A Default Element must be specified');
}
this._editPanelDict = value;
}
public get editPanelDict() {
return this._editPanelDict;
}
private _editPanelDict = genDefaultDict()
@Input()
public set toolbar(config: IToolbarConfig<ICallbackData<IBaseNodeOptions, IBaseEdgeOptions>>) {
this._toolbar = config;
}
public get toolbar() {
return this._toolbar;
}
protected _toolbar = defaultToolbar(generateGraphOptions(),{
useVersionControl: true
});
/**
* Container with Actions
*
*/
public rightClickActions: rigthClickActions = [];
public contextMenuGenerator: IContextConfig<N,E,D> = generateDefaulContextMenu();
public network: IUndoRedoGraph<N,E>;
public templates: ISelectionConfig<ITemplate<N,E> | IMustacheTemplate>;
public readonly divID = 'editor';
/** Element storing the current Mouse-Position */
public get mousePos() {
if (this.layout && this.layout.currentMousePosition) {
return {
x: this.layout.currentMousePosition.offsetX,
y: this.layout.currentMousePosition.offsetY
}
}
return {
x: 0,
y: 0
};
}
/**
* Element containing the Template, which will be added.
*/
public get template(): ITemplate<N,E> | IMustacheTemplate | null {
if (this.layoutOptions.selection){
return this.layout.selection.getSelectetTemplate()
}
return null;
}
/**
* Creates an instance of GraphToolComponent.
* @param contextMenuService The Context Menu Service
*/
constructor(
public zemaService: ZemaServiceProvider,
protected themeService: NbThemeService
) {
const _this = this;
this.themeService.getJsTheme().subscribe(() => {
_this.ngOnDestroy();
_this.initEditor().catch(err => _this.zemaService.logger.error(err));
});
}
public adaptedData(event, data: IPossiblePanels): D {
throw Error('Abstract Class, not implemented');
}
@ViewChild(BasicLayoutComponent, {static: true})
public layout: IBasicLayoutComponent<ITemplate<N, E> | IMustacheTemplate, D>;
public loadJSON(data: string, overwrite = true) {
try {
/** Read In the JSON File */
const content = parse(data, this.parseFunctions);
/** Load the Data itself */
this.network.loadData(content, overwrite)
} catch (e) {
this.zemaService.showToast('danger', 'Failed Loading', e, 0);
this.layout.openDialogWithText({
text: e.toString(),
title: 'Failed loading Data to Graph',
closeOnBackdropClick: true,
dynamicSize: true,
buttons: [
{
callback: close => close(),
label: 'OK',
status: 'danger'
}
]
})
this.zemaService.logger.error(e, 'Failed loading Data to Graph');
}
}
/**
* A Function to open Up an Edit-Window,
* Rendering the content of the Component.
*/
public openEditInterface<C extends IEditPage<N | IBaseNodeOptions, E | IBaseEdgeOptions>>(
/*** The Angular Component */
component: Type<C>,
/** The Stettings, of the Component */
settings: {
inputTemplate?: ITemplate<N, E>,
[index: string]: any,
},
title: string,
/** callback, if the Sucess-Button is pressed */
sucessCallback: (data: {
template : ITemplate<N | IBaseNodeOptions, E | IBaseEdgeOptions>,
callback?: (template: ITemplate<N | IBaseNodeOptions, E | IBaseEdgeOptions>) => ITemplate<N | IBaseNodeOptions, E | IBaseEdgeOptions>
}) => void,
mode: 'sidebar' | 'popup' = 'popup') {
const _this = this;
// Disable the Hotkeys.
this.disableHotkeys();
this.zemaService.logger.info('Open edit Window',mode)
switch(mode){
case 'sidebar':
this.layout.openDynamicPanel({
title,
component: {
component: component,
inputs: Object.assign(settings,{
inputTemplate: settings.inputTemplate,
graph: this.network
})
},
buttons: [
{
label: 'Save',
status: 'success',
callback(instance, close, changePanelVisbility) {
if (instance.isValid()) {
_this.enableHotkeys();
sucessCallback(instance.getAdapted());
if (!_this.options.editOnSelect){
close();
changePanelVisbility(false);
}
} else {
_this.layout.panels.right.showMessage({
body: 'Error in Data. Data canot be stored',
hideOnClick: true,
buttons: 'close'
})
}
}
},
{
label: 'Abort',
status: 'danger',
callback(instance, close, changePanelVisbility) {
_this.enableHotkeys();
close();
changePanelVisbility(false);
}
}
],
showOnCreate: true,
panel: 'right',
append: false
})
break;
case 'popup':
this.layout.openDialogComponent<C>({
title,
component: {
component,
inputs: Object.assign(settings,{
inputTemplate: settings.inputTemplate,
graph: this.network
})
},
buttons: [
{
label: 'Save',
status: 'success',
callback(instance, close) {
if (instance.isValid()) {
_this.enableHotkeys();
sucessCallback(instance.getAdapted());
close();
}
}
},
{
label: 'Abort',
status: 'danger',
callback(instance, close) {
_this.enableHotkeys();
close();
}
}
],
closeOnBackdropClick: false,
closeOnEsc: false
});
break;
}
}
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<N, E>),
},
'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
}
/**
* Function to Update the Data of a Node.
* @param selection The Selected Node.
*/
public updateNode(selection: Array<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.zemaService.logger.warn('Editor is Trying to open an Unkown Edit-Component');
}
}
/**
* Function, which is used to Update an Edge
* @param edge The Corresponding Edge, which will be updated.
*/
public 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');
}
}
private _destroyNetwork: () => void;
ngOnDestroy(): void {
if (typeof this._destroyNetwork === 'function')
this._destroyNetwork();
}
protected _updateLayoutOptions(options: ILayoutOptions<ITemplate<N,E> | IMustacheTemplate, D>){
return options;
}
public async initEditor() {
const _this = this;
let colors: any = null;
const subcription = this.themeService.getJsTheme().subscribe((value) => {
colors = value.variables;
});
await waitFor(() => (colors !== undefined && colors !== null), {
additionalDelay: 100
});
subcription.unsubscribe();
const layoutOptions: ILayoutOptions<ITemplate<N,E> | IMustacheTemplate, D> = {
title: 'Editor',
panels: [
{
type: 'right',
id: 'properties',
hidden: true,
resizable: true,
minSize: 300,
toggle: ! this.options.hideRightPanel,
style: "background-color: "+ colors.bg2
}
],
adaptData(event, panels){
return _this.adaptedData(event, panels)
},
hotkeys: defaultHotkeys<N,E>(),
onCopy(data){
try {
_this.copySelectionToClipboard();
data.zemaService.showToast('success', 'Clipboard', 'Copied Content to Clipboard');
} catch (e) {
data.zemaService.showToast('warning', 'Clipboard', 'Failed Copying To Clipboard');
}
},
onPaste(text, data){
try {
_this.paste(parseWithFunctions(text),data.network.network.DOMtoCanvas(data.component.mousePos), false);
data.zemaService.showToast('success', 'Clipboard', 'Copied Content to Clipboard');
} catch (e) {
data.zemaService.showToast('warning', 'Clipboard', 'Failed Copying To Clipboard');
}
},
showToggleElements: true
}
if (!this.options.hideToolbar) {
layoutOptions.panels.push({
type: 'top',
id: 'toolbar',
toggle: false,
});
layoutOptions.toolbar = {
panel: 'top',
config: this.toolbar
}
}
if (this.templates){
layoutOptions.selection = {
panel: 'left',
id: 'selection',
templates: this.templates,
preview: {
id: 'preview',
type: 'preview',
resizable: true
}
};
layoutOptions.panels.push({
type: 'left',
id: 'left',
hidden: false,
resizable: true,
minSize: 200,
maxSize: 500,
overflow: 'hidden',
toggle: false,
});
}
this.layoutOptions = this._updateLayoutOptions(layoutOptions);
// Wait until the Layout has been initialized
await waitFor(async function(){
while (!_this.layout) {
sleep(10);
}
return true;
})
// Wait until the Editor is Ready and then Create the 3D Renderer
await this.layout.ready.waitFor((value) => value === true);
/** Generate the Default Callback for adding a Node */
rsetattr(
this._visjsOptions,
'manipulation.addEdge',
generateAddFunction(_this, (data, callback) => {
/** Test if an Edge-Template exists */
if (_this.template.type === 'elements' && _this.template.edges.length === 1 && _this.template.nodes.length === 0) {
const edge = Object.assign(
_this.template.edges[0],
data
);
if (typeof _this.options.addEdgeCallback === 'function') {
_this.options.addEdgeCallback(edge, callback);
} else {
callback(edge);
}
}
}
)
);
if (!rgetattr(this._visjsOptions, 'manipulation.enabled', false)) {
rsetattr(this._visjsOptions, 'manipulation.enabled', false);
}
const editor = initEditor<N,E,D, BaseGraphEditor<N,E,D>>({
component: this,
element: this.layout.panels.main,
networkOptions: this.visjsOptions,
renderMinimap: false
});
// Store the Network
this.network = editor.network;
this._destroyNetwork = editor.destroy;
// Adapt the Message Function
this.network.showMessage = (type, title, body, duration = 5000) => {
_this.zemaService.showToast(type, title, body, duration);
}
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.options.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.copySelectionToClipboard();
_this.zemaService.showToast('success', 'Clipboard', 'Copied Content to Clipboard');
} catch (e) {
_this.zemaService.showToast('warning', 'Clipboard', 'Failed Copying To Clipboard');
}
});
this.network.on('onpaste', async event => {
console.log(event)
try {
const data = await _this.readDataFromClipboard()
_this.paste(parseWithFunctions(data), _this.network.network.DOMtoCanvas(_this.mousePos), false);
_this.zemaService.showToast('success', 'Clipboard', 'Copied Content to Clipboard');
} catch (e) {
_this.zemaService.showToast('warning', 'Clipboard', 'Failed Copying To Clipboard');
}
});
let _mousePosOnContextMenu = this.mousePos;
let rightClickActions: IRigthClickActions<ICallbackData<N,E>> = [];
const _contextHandler = (params) => {
let nodeID = null;
if (params.nodes.length === 1) {
nodeID = _this.network.network.getNodeAt(params.pointer.DOM);
if (!nodeID)
nodeID = params.nodes[0];
}
let edgeID = null;
if (params.edges.length === 1) {
edgeID = params.edges[0];
}
// If nothing is selected, try to select the element,
// which is underneath the Pointer
if (params.nodes.length === 0) {
nodeID = _this.network.network.getNodeAt(params.pointer.DOM);
// Test if a Node is underneath the Pointer
if (nodeID) {
// Make shure the Element is selected
_this.network.network.selectNodes([nodeID]);
}
}
// If nothing is selected, try to select the element,
// which is underneath the Pointer
if (params.edges.length === 0) {
edgeID = _this.network.network.getEdgeAt(params.pointer.DOM);
// Test if a Node is underneath the Pointer
if (edgeID) {
// Make shure the Element is selected
_this.network.network.selectEdges([edgeID]);
}
}
// Test whether multiple Nodes or just a single Node is selected.
if (nodeID) {
// A single Node is selected
rightClickActions = _this.contextMenuGenerator.node(_this, params.pointer.DOM, nodeID);
} else if (params.nodes.length > 1) {
// The Default Right-Clickmenu must be selected
rightClickActions = _this.contextMenuGenerator.default(_this, params.pointer.DOM, params.nodes, params.edges);
} else if (edgeID) {
// Only 1 Edge is selected
rightClickActions = _this.contextMenuGenerator.edge(_this, params.pointer.DOM, edgeID);
} else {
rightClickActions = _this.contextMenuGenerator.background(_this, params.pointer.DOM);
}
}
this.network.on('oncontext', (params) => {
if (_this.options.enableContextMenu){
// Make shure the Context-Menu is only Opened, if
// The event isnt prevented
if (params.event.prevent) {
return;
}
// Call creating the Menu Entries.
_contextHandler(params)
// Decide, whether an Element is selected or not
params.event.preventDefault();
// Store the current Position
_mousePosOnContextMenu = _this.mousePos;
// Check after a View seconds whether the Mouse has been move
// setTimeout(_contextHandler, 200, params);
setTimeout(() => {
if (Math.abs(_this.mousePos.x - _mousePosOnContextMenu.x) < 5 && Math.abs(_this.mousePos.y - _mousePosOnContextMenu.y) < 5) {
if (rightClickActions.length > 0) {
_this.layout.openContextMenu(params.event, rightClickActions);
} else {
_this.network.network.unselectAll();
}
} else {
rightClickActions = [];
}
}, 200)
}
});
// Make shure the Sidepanel is working correctly
this.network.on('select', (params) => {
if (_this.options.enableEditing){
if (_this.options.editOnSelect && _this.options.hidePanelOnDeselect && params.nodes.length === 0 && params.edges.length === 0){
_this.layout.panels.right.hide();
} else if (_this.options.editOnSelect){
editItemIfPossible(params);
}
}
});
enableClusterPreview(this);
this.network.addNode(this._nodes);
this.network.addEdge(this._edges);
this.layoutOptions.onResized = () => {
editor.resize();
}
editor.resize();
}
/**
* Function to initalize the Editor
*/
public ngOnInit() {
// const _this = this;
// this.initEditor().catch(err => _this.zemaService.logger.error(err, 'Init of Editor Failed.'));
}
public enableHotkeys() {
this.layout.hotkeysEnabled = true;
}
public disableHotkeys() {
this.layout.hotkeysEnabled = false;
// this.network.network.disableEditMode();
}
/**
* Create a copy of the current Selection.
*
* @returns A Template containing the Selected Nodes and Edges.
* @memberof GraphToolComponent
*/
public createTemplateOfSelectedElements() {
const selected: ITemplate<N | IBaseNodeOptions,E | IBaseEdgeOptions> = {
nodes: [],
edges: [],
type: 'elements'
};
const selection = this.network.network.getSelection(true);
selected.nodes = deepClone(this.nodes.filter(item => selection.nodes.indexOf(item.id) !== -1));
selected.edges = deepClone(this.edges.filter(item => selection.edges.indexOf(item.id) !== -1));
return selected;
}
/**
* Function to paste copied stuff
* @param position The Position, where the content should be inserted
* @param useExistingNodesForEdges Decide, whether the connections should be although copied
*/
public paste(
template: ITemplate<N,E>,
position: {
x: number,
y: number
},
useExistingNodesForEdges: boolean) {
const data = adaptPositions<N,E>(adaptIDS<N,E>(template, useExistingNodesForEdges), position);
this.network.addNode(data.nodes);
this.network.addEdge(data.edges);
}
/**
* Function, will copy the Selection to the Clipboard
*
* @memberof GraphToolComponent
*/
public copySelectionToClipboard() {
this.copyToClipboard(stringify(this.createTemplateOfSelectedElements(), this.parseFunctions));
}
/**
* Function, which will paste a String to the Clipboard
*
* @param {string} content The stringified Content
* @memberof GraphToolComponent
*/
public copyToClipboard(content: string){
writeToClipboard(content);
}
public async readDataFromClipboard(){
if (navigator && (navigator as any).clipboard) {
try {
const text = await (navigator as any).clipboard.readText();
return text;
} catch (err) {
this.zemaService.showToast('warning', 'Clipboard', 'Failed Pasting Clipboard. Issue with the Rights');
this.zemaService.logger.error(err, 'Failed using Clipboard')
}
} else if (navigator && (navigator as any).permissions) {
const permissionStatus = await (navigator as any).permissions.query({
name: 'clipboard-read'
} as any)
this.zemaService.logger.info('Current Permission State is ',permissionStatus.state)
// Will be 'granted', 'denied' or 'prompt':
const _this = this;
// Listen for changes to the permission state
permissionStatus.onchange = () => {
_this.zemaService.logger.info('Current Permission State is ',permissionStatus.state)
};
this.zemaService.showToast('warning', 'Clipboard', 'Failed Accessing Clipboard');
}
return null;
}
public async pasteFromClipboard() {
try {
const data = await this.readDataFromClipboard();
if (data) {
const clipboardData: ITemplate<N, E> = parse(data, this.parseFunctions);
this.paste(clipboardData, this.network.network.DOMtoCanvas(this.mousePos), false);
this.zemaService.showToast('success', 'Clipboard', 'pasted');
}
} catch (e) {
this.zemaService.showToast('warning', 'Clipboard', 'Failed Pasting Clipboard');
}
}
/**
* Function which will Create a Template for Mustache. This
* Template can be added to the Sidebar (After Editing) to enter
* makros.
*
* @returns a Mustache Template
* @memberof GraphToolComponent
*/
public generateTemplateData() {
const selected = this.createTemplateOfSelectedElements()
const template: ISelectionTemplate<IMustacheTemplate> = {
keywords: ['replace me'],
text: 'Displayed Label - Replace Me',
template: {
example: {},
schema: {},
mustache: stringify(selected, true),
type: 'mustache'
}
};
return template;
}
/**
* Function which will Create a Template for Mustache. This
* Template can be added to the Sidebar (After Editing) to enter
* makros. The Template will be copied as String to the clipboard
*
* @memberof GraphToolComponent
*/
public copyTemplateDataToClipboard() {
// Write the serialized Template to the Clipboard
writeToClipboard(stringify(this.generateTemplateData(), this.parseFunctions));
}
}