926 lines
33 KiB
TypeScript
926 lines
33 KiB
TypeScript
|
/**
|
||
|
* @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));
|
||
|
}
|
||
|
}
|