Build infrastructure and Angular 15 upgrade
This commit is contained in:
237
projects/demo/app/app.component.css
Normal file
237
projects/demo/app/app.component.css
Normal file
@@ -0,0 +1,237 @@
|
||||
.flow-view {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-weight: 400;
|
||||
font-family: "Varela Round",sans-serif;
|
||||
font-size: 36px;
|
||||
color: #eeeeee;
|
||||
padding: 2px;
|
||||
background-color: #34302d;
|
||||
border: none;
|
||||
border-top: 4px solid #6db33f;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #eeeeee;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.control-button {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.header-small {
|
||||
font: 300 24px "Helvetica Neue";
|
||||
}
|
||||
|
||||
pre {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.border-selected {
|
||||
stroke: #34302d;
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
.controls {
|
||||
border-radius: 2px;
|
||||
border: solid;
|
||||
border-color: #6db33f;
|
||||
padding: 5px;
|
||||
margin-top: 3px;
|
||||
background-color: #eeeeee;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: #34302d;
|
||||
background-image: none;
|
||||
border-radius: 2px;
|
||||
color: #f1f1f1;
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
font-family: Montserrat,sans-serif;
|
||||
border: 2px solid #6db33f;
|
||||
padding: 5px 20px;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.button span {
|
||||
background-color: #34302d;
|
||||
background-image: none;
|
||||
border-radius: 2px;
|
||||
color: #f1f1f1;
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
font-family: Montserrat,sans-serif;
|
||||
border: 2px solid #6db33f;
|
||||
padding: 5px 20px;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.button input {
|
||||
background-color: #34302d;
|
||||
background-image: none;
|
||||
color: #f1f1f1;
|
||||
font-size: 14px;
|
||||
font-family: Montserrat,sans-serif;
|
||||
text-shadow: none;
|
||||
border: 0px;
|
||||
text-align:right;
|
||||
}
|
||||
|
||||
.button input[type=range] {
|
||||
display: inline;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
button.on {
|
||||
background-color: #5fa134;
|
||||
}
|
||||
|
||||
.flow-definition-container {
|
||||
border: 1px solid;
|
||||
border-color: #6db33f;
|
||||
border-radius: 2px;
|
||||
margin-top: 3px;
|
||||
background-color: #ffffff;
|
||||
font-family: monospace;
|
||||
z-index: 2;
|
||||
width:100%;
|
||||
height:100px;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
textarea:input {
|
||||
outline: none;
|
||||
border: 1px solid #6db33f;
|
||||
}
|
||||
|
||||
textarea:input:focus {
|
||||
outline: none;
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
|
||||
.flow-definition {
|
||||
border: 5px;
|
||||
height:100%;
|
||||
width:100%;
|
||||
font-size: 16px;
|
||||
resize: none;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* The text label on the nodes */
|
||||
.label {
|
||||
font-family: 'Lucida Console';
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* The class for the 'icon/unicode_char' on the nodes */
|
||||
.label2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#flo-container {
|
||||
height: 800px;
|
||||
}
|
||||
|
||||
|
||||
/* Modules DnD shape animation */
|
||||
|
||||
@keyframes flash-stroke-width {
|
||||
0% { stroke-width: 1; }
|
||||
50% { stroke-width: 4; }
|
||||
100% { stroke-width: 1; }
|
||||
}
|
||||
|
||||
/*.dnd-source-feedback.joint-element .box {*/
|
||||
/*animation: flash-stroke-width 2s linear infinite;*/
|
||||
/*}*/
|
||||
.dnd-target-feedback.joint-element .box {
|
||||
animation: flash-stroke-width 2s linear infinite;
|
||||
}
|
||||
|
||||
/* Ports DnD animation */
|
||||
.dnd-source-feedback.input-port {
|
||||
animation: flash-stroke-width 2s linear infinite;
|
||||
}
|
||||
.dnd-source-feedback.output-port {
|
||||
animation: flash-stroke-width 2s linear infinite;
|
||||
}
|
||||
.dnd-target-feedback.input-port {
|
||||
animation: flash-stroke-width 2s linear infinite;
|
||||
}
|
||||
.dnd-target-feedback.output-port {
|
||||
animation: flash-stroke-width 2s linear infinite;
|
||||
}
|
||||
|
||||
/* Links DnD feedback animation */
|
||||
.dnd-source-feedback .connection {
|
||||
animation: flash-stroke-width 2s linear infinite;
|
||||
}
|
||||
.dnd-target-feedback .connection {
|
||||
animation: flash-stroke-width 2s linear infinite;
|
||||
}
|
||||
|
||||
.properties-group-container .property-row {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.df-property-row {
|
||||
width: 100%;
|
||||
display:inline-table;
|
||||
}
|
||||
|
||||
.df-property-label-cell {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.df-property-container {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
.df-property-control {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.invalid-property-value .df-property-label-cell {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.invalid-property-value .df-property-control {
|
||||
outline-color: red;
|
||||
border-color: red;
|
||||
}
|
||||
|
||||
.validation-error-block {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.code-editor-host {
|
||||
height: 120px;
|
||||
width: 284px;
|
||||
border: 1px solid;
|
||||
border-color: lightgrey;
|
||||
}
|
||||
|
||||
.CodeMirror pre.CodeMirror-placeholder {
|
||||
color: lightgray;
|
||||
}
|
||||
|
||||
61
projects/demo/app/app.component.html
Normal file
61
projects/demo/app/app.component.html
Normal file
@@ -0,0 +1,61 @@
|
||||
<!--
|
||||
<my-lib></my-lib>
|
||||
<h3>Meaning is: {{meaning}}</h3>
|
||||
<flo-editor></flo-editor>
|
||||
-->
|
||||
<div id="header" class="header">
|
||||
Spring Flo Sample
|
||||
</div>
|
||||
<div id="flo-container">
|
||||
<flo-editor (floApi)="editorContext = $event" [metamodel]="metamodel" [renderer]="renderer" [editor]="editor"
|
||||
[paletteSize]="paletteSize" [paletteEntryPadding]="{width: 4, height: 4}" [(dsl)]="dsl"
|
||||
[paperPadding]="20" (validationMarkers)="markersChanged($event)" searchFilterPlaceHolder="Apps..."
|
||||
(onProperties)="openPropertiesDialog($event)">
|
||||
<div header>
|
||||
<div id="controls" class="controls">
|
||||
<button class="button" id="clearGraph" (click)="editorContext.clearGraph()">Create New Flow</button>
|
||||
<!-- let's use the 'x' control on the nodes themselves when they are selected
|
||||
<button class="button" id="deleteSelectedNode" ng-click="flo.deleteSelectedNode()">Delete Selected Node</button>
|
||||
-->
|
||||
<button class="button" id="performLayout" (click)="arrangeAll()">Reset Layout</button>
|
||||
<button class="button" id="sync" (click)="editorContext.graphToTextSync = !editorContext.graphToTextSync"
|
||||
[ngClass]="{on:editorContext.graphToTextSync}">Synchronize
|
||||
</button>
|
||||
<button class="button" id="readOnly" (click)="editorContext.readOnlyCanvas = !editorContext.readOnlyCanvas"
|
||||
[ngClass]="{on:editorContext.readOnlyCanvas}">Read-Only
|
||||
</button>
|
||||
<button class="button" id="noPalette" (click)="editorContext.noPalette = !editorContext.noPalette"
|
||||
[ngClass]="{on:!editorContext.noPalette}">Palette
|
||||
</button>
|
||||
<button class="button" id="editor" (click)="dslEditor = !dslEditor" [ngClass]="{on:dslEditor}">{{dslEditor ?
|
||||
'TextArea' : 'Editor'}}
|
||||
</button>
|
||||
<span class="button">
|
||||
<label>Zoom: </label>
|
||||
<input id="zoomInput" type="text" [(ngModel)]="editorContext.zoomPercent" size="3">
|
||||
<label>%</label>
|
||||
<input type="range" [(ngModel)]="editorContext.zoomPercent" [step]="editorContext.getZoomStep()"
|
||||
[max]="editorContext.getMaxZoom()" [min]="editorContext.getMinZoom()" data-type="range" name="range"
|
||||
class="range">
|
||||
</span>
|
||||
<span class="button">
|
||||
<label>Grid Size: </label>
|
||||
<input id="gridSizeInput" type="text" [(ngModel)]="editorContext.gridSize" size="2">
|
||||
<label>pixels</label>
|
||||
<input type="range" [(ngModel)]="editorContext.gridSize" step="1" max="50" min="1" data-type="range"
|
||||
name="range" class="range">
|
||||
</span>
|
||||
</div>
|
||||
<div class="flow-definition-container">
|
||||
<dsl-editor *ngIf="dslEditor" [(dsl)]="dsl" [line-numbers]="true" [line-wrapping]="true"
|
||||
(blur)="editorContext.graphToTextSync=true" (focus)="editorContext.graphToTextSync=false"
|
||||
placeholder="Enter stream definition..."></dsl-editor>
|
||||
<textarea *ngIf="!dslEditor" id="flow-definition" class="flow-definition"
|
||||
placeholder="Enter stream definition..."
|
||||
[value]="dsl" (keyup)="processKeyUp($event)" (blur)="editorContext.graphToTextSync=true"
|
||||
(focus)="editorContext.graphToTextSync=false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</flo-editor>
|
||||
</div>
|
||||
|
||||
57
projects/demo/app/app.component.ts
Normal file
57
projects/demo/app/app.component.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Component, ViewEncapsulation } from '@angular/core';
|
||||
import { Flo } from 'spring-flo';
|
||||
import { BsModalService } from 'ngx-bootstrap/modal';
|
||||
const { Metamodel } = require('./metamodel');
|
||||
const { Renderer } = require('./renderer');
|
||||
const { Editor } = require('./editor');
|
||||
|
||||
// Code editor used from Flo requires the follwoing CM extensions
|
||||
import 'codemirror/mode/javascript/javascript';
|
||||
import 'codemirror/mode/ruby/ruby';
|
||||
import 'codemirror/mode/clike/clike';
|
||||
import 'codemirror/addon/lint/javascript-lint';
|
||||
import {PropertiesEditorService} from './properties-editor.service';
|
||||
import {dia} from 'jointjs';
|
||||
|
||||
@Component({
|
||||
selector: 'demo-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: [ './app.component.css' ],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class AppComponent {
|
||||
|
||||
metamodel: Flo.Metamodel;
|
||||
renderer: Flo.Renderer;
|
||||
editor: Flo.Editor;
|
||||
dsl: string;
|
||||
dslEditor = false;
|
||||
|
||||
editorContext: Flo.EditorContext;
|
||||
|
||||
paletteSize = 170;
|
||||
|
||||
constructor(private propertiesEditor: PropertiesEditorService) {
|
||||
this.metamodel = new Metamodel();
|
||||
this.renderer = new Renderer();
|
||||
this.editor = new Editor(propertiesEditor);
|
||||
this.dsl = '';
|
||||
}
|
||||
|
||||
arrangeAll() {
|
||||
this.editorContext.performLayout().then(() => this.editorContext.fitToPage());
|
||||
}
|
||||
|
||||
markersChanged(markers: Map<string | number, Array<Flo.Marker>>) {
|
||||
console.log('MARKERS: ' + JSON.stringify(markers));
|
||||
}
|
||||
|
||||
openPropertiesDialog(cell: dia.Cell) {
|
||||
this.propertiesEditor.openPropertiesDialog(cell)
|
||||
}
|
||||
|
||||
processKeyUp(evt: KeyboardEvent): void {
|
||||
this.dsl = (evt.target as HTMLTextAreaElement).value;
|
||||
}
|
||||
|
||||
}
|
||||
16
projects/demo/app/app.module.ts
Normal file
16
projects/demo/app/app.module.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { FloModule } from 'spring-flo';
|
||||
import { ModalModule } from 'ngx-bootstrap/modal';
|
||||
|
||||
import { PropertiesDialogComponent } from './properties.dialog.component';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [ BrowserModule, FormsModule, FloModule, ModalModule.forRoot() ],
|
||||
declarations: [ AppComponent, PropertiesDialogComponent ],
|
||||
entryComponents: [ PropertiesDialogComponent ],
|
||||
bootstrap: [ AppComponent ]
|
||||
})
|
||||
export class AppModule { }
|
||||
552
projects/demo/app/editor.ts
Normal file
552
projects/demo/app/editor.ts
Normal file
@@ -0,0 +1,552 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Flo, Constants, Properties } from 'spring-flo';
|
||||
import { dia, g } from 'jointjs';
|
||||
import {PropertiesEditorService} from './properties-editor.service';
|
||||
const joint: any = Flo.joint;
|
||||
|
||||
/**
|
||||
* @author Alex Boyko
|
||||
* @author Andy Clement
|
||||
*/
|
||||
export class Editor implements Flo.Editor {
|
||||
|
||||
get highlighting() {
|
||||
return {
|
||||
connecting: {
|
||||
name: 'addParentClass',
|
||||
options: {
|
||||
className: 'connecting'
|
||||
}
|
||||
},
|
||||
'default': {
|
||||
name: 'addClass',
|
||||
options: {
|
||||
className: 'highlighted'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
constructor(private propertiesEditor: PropertiesEditorService) {}
|
||||
|
||||
createHandles(context : Flo.EditorContext, createHandle : (owner : dia.CellView, kind : string, action : () => void, location : dia.Point) => void, owner : dia.CellView) {
|
||||
if (owner.model instanceof joint.dia.Element) {
|
||||
let bbox : any = (<dia.Element> owner.model).getBBox();
|
||||
|
||||
// Remove handle
|
||||
createHandle(owner, Constants.REMOVE_HANDLE_TYPE, context.deleteSelectedNode, bbox.origin().offset(bbox.width + 3, bbox.height + 3));
|
||||
|
||||
// Properties handle
|
||||
if (!owner.model.get('metadata')?.unresolved) {
|
||||
createHandle(owner, Constants.PROPERTIES_HANDLE_TYPE, () => this.propertiesEditor.openPropertiesDialog(owner.model), bbox.origin().offset(-14, bbox.height + 3));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validatePort(context: Flo.EditorContext, view : dia.ElementView, magnet : SVGElement) {
|
||||
return true;
|
||||
}
|
||||
|
||||
validateLink(context: Flo.EditorContext, cellViewS : dia.ElementView, magnetS : SVGElement, cellViewT : dia.ElementView, magnetT : SVGElement, isSource : boolean, linkView : dia.LinkView) {
|
||||
// Prevent linking from input ports.
|
||||
if (magnetS && magnetS.getAttribute('port') === 'input') {
|
||||
return false;
|
||||
}
|
||||
// Prevent linking from output ports to input ports within one element.
|
||||
if (cellViewS === cellViewT) {
|
||||
return false;
|
||||
}
|
||||
// Prevent linking to input ports.
|
||||
if (magnetT && magnetT.getAttribute('port') === 'output') {
|
||||
return false;
|
||||
}
|
||||
return cellViewS.model && cellViewT.model && !(cellViewS.model instanceof joint.shapes.flo.ErrorDecoration) && !(cellViewT.model instanceof joint.shapes.flo.ErrorDecoration);
|
||||
}
|
||||
|
||||
preDelete(context : Flo.EditorContext, deletedElement : dia.Cell) {
|
||||
if (deletedElement instanceof joint.dia.Element) {
|
||||
this.repairDamage(context, <dia.Element> deletedElement);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
handleNodeDropping(context : Flo.EditorContext, dragDescriptor : Flo.DnDDescriptor) {
|
||||
let relinking = dragDescriptor.sourceComponent === Constants.PALETTE_CONTEXT;
|
||||
let graph = context.getGraph();
|
||||
let source = dragDescriptor.source ? dragDescriptor.source.view.model : undefined;
|
||||
let target = dragDescriptor.target ? dragDescriptor.target.view.model : undefined;
|
||||
if (target instanceof joint.dia.Element && target.get('metadata')?.name) {
|
||||
// Custom handling allowing a node to be dropped on a port and inserting
|
||||
// it into the flow directly without the user needing to do more link
|
||||
// drawing
|
||||
let type = source.get('metadata')?.name;
|
||||
if (dragDescriptor.target.cssClassSelector === '.output-port') {
|
||||
this.moveNodeOnNode(context, <dia.Element> source, <dia.Element> target, 'right', true);
|
||||
relinking = true;
|
||||
} else if (dragDescriptor.target.cssClassSelector === '.input-port') {
|
||||
this.moveNodeOnNode(context, <dia.Element> source, <dia.Element> target, 'left', true);
|
||||
relinking = true;
|
||||
}
|
||||
} else if (target instanceof joint.dia.Link) { // jshint ignore:line
|
||||
// Custom handling allowing a node to be dropped on a link and inserting
|
||||
// itself in that link without the user needing to do more link drawing
|
||||
this.moveNodeOnLink(context, <dia.Element> source, <dia.Link> target, false);
|
||||
relinking = true;
|
||||
}
|
||||
// Turn off auto layout
|
||||
// if (relinking) {
|
||||
// flo.performLayout();
|
||||
// }
|
||||
}
|
||||
|
||||
calculateDragDescriptor(context : Flo.EditorContext, draggedView : dia.CellView, targetUnderMouse : dia.CellView, point : g.Point, sourceComponent : string) : Flo.DnDDescriptor {
|
||||
let source = draggedView.model;
|
||||
let sourceGroup = source.get('metadata')?.group;
|
||||
|
||||
// Find closest port
|
||||
let range = 30;
|
||||
let graph = context.getGraph();
|
||||
let paper = context.getPaper();
|
||||
let closestData : Flo.DnDDescriptor;
|
||||
let minDistance = Number.MAX_VALUE;
|
||||
let maxIcomingLinks = sourceGroup === 'source' ? 0 : 1;
|
||||
let maxOutgoingLinks = sourceGroup === 'sink' ? 0 : 1;
|
||||
let hasIncomingPort = sourceGroup !== 'source';
|
||||
let hasOutgoingPort = sourceGroup !== 'sink';
|
||||
if (!hasIncomingPort && !hasOutgoingPort) {
|
||||
return;
|
||||
}
|
||||
let elements = graph.findModelsInArea(joint.g.rect(point.x - range, point.y - range, 2 * range, 2 * range)); // jshint ignore:line
|
||||
if (Array.isArray(elements)) {
|
||||
elements.forEach(function(model) {
|
||||
let view = paper.findViewByModel(model);
|
||||
if (view && view !== draggedView && model instanceof joint.dia.Element) { // jshint ignore:line
|
||||
let targetGroup = model.get('metadata')?.group;
|
||||
let targetMaxIcomingLinks = targetGroup === 'source' ? 0 : 1;
|
||||
let targetMaxOutgoingLinks = targetGroup === 'sink' ? 0 : 1;
|
||||
let targetHasIncomingPort = targetGroup !== 'source';
|
||||
let targetHasOutgoingPort = targetGroup !== 'sink';
|
||||
view.$('[magnet]').each((index : number, magnet : HTMLElement) => {
|
||||
let type = magnet.getAttribute('port');
|
||||
if ((type === 'input' && targetHasIncomingPort && hasOutgoingPort) || (type === 'output' && targetHasOutgoingPort && hasIncomingPort)) {
|
||||
let bbox = joint.V(magnet).bbox(false, paper.viewport); // jshint ignore:line
|
||||
let distance = point.distance({
|
||||
x: bbox.x + bbox.width / 2,
|
||||
y: bbox.y + bbox.height / 2
|
||||
});
|
||||
if (distance < range && distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closestData = {
|
||||
sourceComponent: sourceComponent,
|
||||
source: {
|
||||
view: draggedView,
|
||||
cssClassSelector: type === 'output' ? '.input-port' : '.output-port'
|
||||
},
|
||||
target: {
|
||||
view: view,
|
||||
cssClassSelector: '.' + type+'-port'
|
||||
},
|
||||
range: minDistance
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (closestData) {
|
||||
return closestData;
|
||||
}
|
||||
|
||||
// Check if drop on a link is allowed
|
||||
if (targetUnderMouse instanceof joint.dia.LinkView &&
|
||||
sourceGroup === 'processor' &&
|
||||
graph.getConnectedLinks(source).length === 0) { // jshint ignore:line
|
||||
return {
|
||||
sourceComponent: sourceComponent,
|
||||
source: {
|
||||
view: draggedView
|
||||
},
|
||||
target: {
|
||||
view: targetUnderMouse
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
sourceComponent: sourceComponent,
|
||||
source: {
|
||||
view: draggedView
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
validate(graph : dia.Graph, dsl: string, flo: Flo.EditorContext) : Promise<Map<string | number, Flo.Marker[]>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let allMarkers = new Map<string | number, Array<Flo.Marker>>();
|
||||
graph.getElements().filter(e => e.get('metadata')).forEach(e => {
|
||||
let markers : Array<Flo.Marker> = []
|
||||
let group = e.get('metadata')?.group;
|
||||
if (e.get('metadata')?.unresolved) {
|
||||
markers.push({
|
||||
severity: Flo.Severity.Error,
|
||||
range: e.attr('range'),
|
||||
message: `Unknown element '${e.get('metadata')?.name}` + (group ? ` from group '${e.get('metadata')?.group}'` : '')
|
||||
});
|
||||
} else if (group) {
|
||||
let links = graph.getConnectedLinks(e);
|
||||
let outgoingLinksNumber = links.filter(l => l.get('source').id === e.id).length;
|
||||
let incomingLinksNumber = links.filter(l => l.get('target').id === e.id).length;
|
||||
if (group === 'sink') {
|
||||
if (outgoingLinksNumber > 0) {
|
||||
markers.push({
|
||||
severity: Flo.Severity.Error,
|
||||
range: e.attr('range'),
|
||||
message: `Sink node cannot have outgoing links`
|
||||
});
|
||||
}
|
||||
if (incomingLinksNumber > 1) {
|
||||
markers.push({
|
||||
severity: Flo.Severity.Error,
|
||||
range: e.attr('range'),
|
||||
message: `Sink node cannot have more than one incoming link`
|
||||
});
|
||||
}
|
||||
} else if (group === 'source') {
|
||||
if (outgoingLinksNumber > 1) {
|
||||
markers.push({
|
||||
severity: Flo.Severity.Error,
|
||||
range: e.attr('range'),
|
||||
message: `Source node cannot have more than one outgoing link`
|
||||
});
|
||||
}
|
||||
if (incomingLinksNumber > 0) {
|
||||
markers.push({
|
||||
severity: Flo.Severity.Error,
|
||||
range: e.attr('range'),
|
||||
message: `Sink node cannot have incoming links`
|
||||
});
|
||||
}
|
||||
} else if (group === 'processor') {
|
||||
if (outgoingLinksNumber > 1) {
|
||||
markers.push({
|
||||
severity: Flo.Severity.Error,
|
||||
range: e.attr('range'),
|
||||
message: `Processor node cannot have more than one outgoing link`
|
||||
});
|
||||
}
|
||||
if (incomingLinksNumber > 1) {
|
||||
markers.push({
|
||||
severity: Flo.Severity.Error,
|
||||
range: e.attr('range'),
|
||||
message: `Processor node cannot have more than one incoming link`
|
||||
});
|
||||
}
|
||||
} else {
|
||||
markers.push({
|
||||
severity: Flo.Severity.Error,
|
||||
range: e.attr('range'),
|
||||
message: `Unknown element '${e.get('metadata')?.name} from group '${e.get('metadata')?.group}'`
|
||||
});
|
||||
}
|
||||
}
|
||||
if (markers.length) {
|
||||
allMarkers.set(e.id, markers);
|
||||
}
|
||||
});
|
||||
resolve(allMarkers);
|
||||
});
|
||||
// var errors = [];
|
||||
// var graph = flo.getGraph();
|
||||
// var constraints = element.get('metadata')?.constraints);
|
||||
// if (constraints) {
|
||||
// var incoming = graph.getConnectedLinks(element, {inbound: true});
|
||||
// var outgoing = graph.getConnectedLinks(element, {outbound: true});
|
||||
// if (typeof constraints.maxIncomingLinksNumber === 'number' || typeof constraints.minIncomingLinksNumber === 'number') {
|
||||
// if (typeof constraints.maxIncomingLinksNumber === 'number' && constraints.maxIncomingLinksNumber < incoming.length) {
|
||||
// if (constraints.maxIncomingLinksNumber === 0) {
|
||||
// errors.push({
|
||||
// message: 'Sources must appear at the start of a stream',
|
||||
// range: element.attr('range')
|
||||
// });
|
||||
// } else {
|
||||
// errors.push({
|
||||
// message: 'Max allowed number of incoming links is ' + constraints.maxIncomingLinksNumber,
|
||||
// range: element.attr('range')
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// if (typeof constraints.minIncomingLinksNumber === 'number' && constraints.minIncomingLinksNumber > incoming.length) {
|
||||
// errors.push({
|
||||
// message: 'Min allowed number of incoming links is ' + constraints.minIncomingLinksNumber,
|
||||
// range: element.attr('range')
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// if (typeof constraints.maxOutgoingLinksNumber === 'number' || typeof constraints.minOutgoingLinksNumber === 'number') {
|
||||
// if (typeof constraints.maxOutgoingLinksNumber === 'number' && constraints.maxOutgoingLinksNumber < outgoing.length) {
|
||||
// if (constraints.maxOutgoingLinksNumber === 0) {
|
||||
// errors.push({
|
||||
// message: 'Sinks must appear at the end of a stream',
|
||||
// range: element.attr('range')
|
||||
// });
|
||||
// } else {
|
||||
// errors.push({
|
||||
// message: 'Max allowed number of outgoing links is ' + constraints.maxOutgoingLinksNumber,
|
||||
// range: element.attr('range')
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// if (typeof constraints.minOutgoingLinksNumber === 'number' && constraints.minOutgoingLinksNumber > outgoing.length) {
|
||||
// errors.push({
|
||||
// message: 'Min allowed number of outgoing links is ' + constraints.minOutgoingLinksNumber,
|
||||
// range: element.attr('range')
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// if (constraints.xorSourceSink && incoming.length && outgoing.length) {
|
||||
// errors.push({
|
||||
// message: 'Node can either have incoming or outgoing links, but not both',
|
||||
// range: element.attr('range')
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// if (!element.get('metadata') || element.get('metadata').unresolved)) {
|
||||
// var msg = 'Unknown element \'' + element.get('metadata')?.name + '\'';
|
||||
// if (element.get('metadata')?.group) {
|
||||
// msg += ' from group \'' + element.get('metadata')?.group + '\'.';
|
||||
// }
|
||||
// errors.push({
|
||||
// message: msg,
|
||||
// range: element.attr('range')
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// // If possible, verify the properties specified match those allowed on this type of element
|
||||
// // propertiesRanges are the ranges for each property included the entire '--name=value'.
|
||||
// // The format of a range is {'start':{'ch':NNNN,'line':NNNN},'end':{'ch':NNNN,'line':NNNN}}
|
||||
// var propertiesRanges = element.attr('propertiesranges');
|
||||
// if (propertiesRanges) {
|
||||
// var moduleSchema = element.get('metadata');
|
||||
// // Grab the list of supported properties for this module type
|
||||
// moduleSchema.get('properties').then(function(moduleSchemaProperties) {
|
||||
// if (!moduleSchemaProperties) {
|
||||
// moduleSchemaProperties = {};
|
||||
// }
|
||||
// // Example moduleSchemaProperties:
|
||||
// // {"host":{"name":"host","type":"String","description":"the hostname of the mail server","defaultValue":"localhost","hidden":false},
|
||||
// // "password":{"name":"password","type":"String","description":"the password to use to connect to the mail server ","defaultValue":null,"hidden":false}
|
||||
// var specifiedProperties = element.attr('props');
|
||||
// Object.keys(specifiedProperties).forEach(function(propertyName) {
|
||||
// if (!moduleSchemaProperties[propertyName]) {
|
||||
// // The schema does not mention that property
|
||||
// var propertyRange = propertiesRanges[propertyName];
|
||||
// if (propertyRange) {
|
||||
// errors.push({
|
||||
// message: 'unrecognized option \''+propertyName+'\' for module \''+element.get('metadata')?.name+'\'',
|
||||
// range: propertyRange
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// return errors;
|
||||
}
|
||||
|
||||
moveNodeOnNode(context : Flo.EditorContext, node : dia.Element, pivotNode : dia.Element, side : string, shouldRepairDamage : boolean) {
|
||||
// side = side || 'left';
|
||||
// if (this.canSwap(context, node, pivotNode, side)) {
|
||||
// let link : dia.Link;
|
||||
// let i : number;
|
||||
// if (side === 'left') {
|
||||
// let sources : Array<string> = [];
|
||||
// if (shouldRepairDamage) {
|
||||
// /*
|
||||
// * Commented out because it doesn't prevent cycles.
|
||||
// */
|
||||
// // if (graph.getConnectedLinks(pivotNode, {inbound: true}).length > 0 || graph.getConnectedLinks(node, {outbound: true}).length > 0) {
|
||||
// this.repairDamage(context, node);
|
||||
// // }
|
||||
// }
|
||||
// context.getGraph().getConnectedLinks(pivotNode, {inbound: true}).forEach(link => {
|
||||
// sources.push(link.get('source').id);
|
||||
// link.remove();
|
||||
// });
|
||||
// sources.forEach(id => {
|
||||
// context.createLink({
|
||||
// 'id': id,
|
||||
// 'selector': '.output-port'
|
||||
// }, {
|
||||
// 'id': node.id,
|
||||
// 'selector': '.input-port'
|
||||
// });
|
||||
// })
|
||||
// for (i = 0; i < sources.length; i++) {
|
||||
// flo.createLink({
|
||||
// 'id': sources[i],
|
||||
// 'selector': '.output-port'
|
||||
// }, {
|
||||
// 'id': node.id,
|
||||
// 'selector': '.input-port'
|
||||
// });
|
||||
// }
|
||||
// flo.createLink({
|
||||
// 'id': node.id,
|
||||
// 'selector': '.output-port'
|
||||
// }, {
|
||||
// 'id': pivotNode.id,
|
||||
// 'selector': '.input-port'
|
||||
// });
|
||||
// } else if (side === 'right') {
|
||||
// var targets = [];
|
||||
// if (shouldRepairDamage) {
|
||||
// /*
|
||||
// * Commented out because it doesn't prevent cycles.
|
||||
// */
|
||||
// // if (graph.getConnectedLinks(pivotNode, {outbound: true}).length > 0 || graph.getConnectedLinks(node, {inbound: true}).length > 0) {
|
||||
// repairDamage(flo, node);
|
||||
// // }
|
||||
// }
|
||||
// var pivotSourceLinks = flo.getGraph().getConnectedLinks(pivotNode, {outbound: true});
|
||||
// for (i = 0; i < pivotSourceLinks.length; i++) {
|
||||
// link = pivotSourceLinks[i];
|
||||
// targets.push(link.get('target').id);
|
||||
// link.remove();
|
||||
// }
|
||||
// for (i = 0; i < targets.length; i++) {
|
||||
// flo.createLink({
|
||||
// 'id': node.id,
|
||||
// 'selector': '.output-port'
|
||||
// }, {
|
||||
// 'id': targets[i],
|
||||
// 'selector': '.input-port'
|
||||
// });
|
||||
// }
|
||||
// flo.createLink({
|
||||
// 'id': pivotNode.id,
|
||||
// 'selector': '.output-port'
|
||||
// }, {
|
||||
// 'id': node.id,
|
||||
// 'selector': '.input-port'
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Node moved onto a link. Remove the existing link and replace it with two links
|
||||
* that go from the original link source to the dropped node and from the dropped node
|
||||
* to the original link target.
|
||||
*/
|
||||
moveNodeOnLink(context : Flo.EditorContext, node : dia.Element, link : dia.Link, shouldRepairDamage : boolean) {
|
||||
let source = link.get('source').id;
|
||||
let target = link.get('target').id;
|
||||
|
||||
if (shouldRepairDamage) {
|
||||
this.repairDamage(context, node);
|
||||
}
|
||||
link.remove();
|
||||
|
||||
if (source) {
|
||||
let sourceView = context.getPaper().findViewByModel(context.getGraph().getCell(source));
|
||||
let magnetS = Flo.findMagnetByClass(sourceView, '.output-port');
|
||||
let targetView = context.getPaper().findViewByModel(node);
|
||||
let magnetT = Flo.findMagnetByClass(targetView, '.input-port');
|
||||
let sourceEnd : Flo.LinkEnd = {id: sourceView.model.id, selector: sourceView.getSelector(magnetS, null)};
|
||||
if (magnetS.getAttribute('port')) {
|
||||
sourceEnd.port = magnetS.getAttribute('port');
|
||||
}
|
||||
let targetEnd : Flo.LinkEnd = {id: targetView.model.id, selector: targetView.getSelector(magnetT, null)};
|
||||
if (magnetT.getAttribute('port')) {
|
||||
targetEnd.port = magnetT.getAttribute('port');
|
||||
}
|
||||
context.createLink(sourceEnd, targetEnd, null, null);
|
||||
}
|
||||
if (target) {
|
||||
let sourceView = context.getPaper().findViewByModel(node);
|
||||
let magnetS = Flo.findMagnetByClass(sourceView, '.output-port');
|
||||
let targetView = context.getPaper().findViewByModel(context.getGraph().getCell(target));
|
||||
let magnetT = Flo.findMagnetByClass(targetView, '.input-port');
|
||||
let sourceEnd : Flo.LinkEnd = {id: sourceView.model.id, selector: sourceView.getSelector(magnetS, null)};
|
||||
if (magnetS.getAttribute('port')) {
|
||||
sourceEnd.port = magnetS.getAttribute('port');
|
||||
}
|
||||
let targetEnd : Flo.LinkEnd = {id: targetView.model.id, selector: targetView.getSelector(magnetT, null)};
|
||||
if (magnetT.getAttribute('port')) {
|
||||
targetEnd.port = magnetT.getAttribute('port');
|
||||
}
|
||||
context.createLink(sourceEnd, targetEnd, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When a node is removed any dangling links should be removed. What this function will also try to do
|
||||
* is if removing a node from a chain it will attempt to replace dangling links with a link from the
|
||||
* deleted nodes original source to the deleted nodes original target.
|
||||
*/
|
||||
repairDamage(context : Flo.EditorContext, node : dia.Element) {
|
||||
// let sources : Array<string> = [];
|
||||
// let targets : Array<string> = [];
|
||||
// let i = 0;
|
||||
// context.getGraph().getConnectedLinks(node).forEach(link => {
|
||||
// let targetId = link.get('target').id;
|
||||
// let sourceId = link.get('source').id;
|
||||
// if (targetId === node.id) {
|
||||
// link.remove();
|
||||
// sources.push(sourceId);
|
||||
// } else if (sourceId === node.id) {
|
||||
// link.remove();
|
||||
// targets.push(targetId);
|
||||
// }
|
||||
// });
|
||||
// /*
|
||||
// * If appropriate, create new links to replace the dangling ones deleted
|
||||
// */
|
||||
// if (sources.length === 1) {
|
||||
// var source = sources[0];
|
||||
// for (i = 0; i < targets.length; i++) {
|
||||
// flo.createLink({'id': source,'selector': '.output-port'}, {'id': targets[i],'selector': '.input-port'});
|
||||
// }
|
||||
// } else if (targets.length === 1) {
|
||||
// var target = targets[0];
|
||||
// for (i = 0; i < sources.length; i++) {
|
||||
// flo.createLink({'id': sources[i], 'selector': '.output-port'}, {'id': target,'selector': '.input-port'});
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if node being dropped and drop target node next to each other such that they won't be swapped by the drop
|
||||
*/
|
||||
canSwap(context : Flo.EditorContext, dropee : dia.Element, target : dia.Element, side : string) : boolean {
|
||||
let i, targetId, sourceId, noSwap = (dropee.id === target.id);
|
||||
if (dropee === target) {
|
||||
console.debug('What!??? Dragged == Dropped!!! id = ' + target);
|
||||
}
|
||||
let links = context.getGraph().getConnectedLinks(dropee);
|
||||
for (i = 0; i < links.length && !noSwap; i++) {
|
||||
targetId = links[i].get('target').id;
|
||||
sourceId = links[i].get('source').id;
|
||||
noSwap = (side === 'left' && targetId === target.id && sourceId === dropee.id) || (side === 'right' && targetId === dropee.id && sourceId === target.id);
|
||||
}
|
||||
return !noSwap;
|
||||
}
|
||||
|
||||
}
|
||||
186
projects/demo/app/graph-to-text.ts
Normal file
186
projects/demo/app/graph-to-text.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { dia } from 'jointjs';
|
||||
|
||||
/**
|
||||
* Convert a graph to a text representation.
|
||||
*
|
||||
* @author Alex Boyko
|
||||
* @author Andy Clement
|
||||
*/
|
||||
class GraphToTextConverter {
|
||||
|
||||
// Graph
|
||||
private g : dia.Graph;
|
||||
|
||||
// Number of Links left to visit
|
||||
private numberOfLinksToVisit : number;
|
||||
|
||||
// Number of nodes left to visit
|
||||
private numberOfNodesToVisit : number;
|
||||
|
||||
// Map of links left to visit indexed by id
|
||||
private linksToVisit : Set<string>;
|
||||
|
||||
// Map of nodes left to visit indexed by id
|
||||
private nodesToVisit : Set<string>;
|
||||
|
||||
// Map of nodes incoming non-visited links degrees index by node id
|
||||
private nodesInDegrees : Map<string, number>;
|
||||
|
||||
constructor(graph : dia.Graph) {
|
||||
this.numberOfLinksToVisit = 0;
|
||||
this.numberOfNodesToVisit = 0;
|
||||
this.linksToVisit = new Set<string>();
|
||||
this.nodesToVisit = new Set<string>();
|
||||
this.nodesInDegrees = new Map<string, number>();
|
||||
this.g = graph;
|
||||
graph.getElements().forEach((element : dia.Element) => {
|
||||
if (element.get('metadata')?.name) {
|
||||
this.nodesToVisit.add(element.get('id'));
|
||||
let indegree = 0;
|
||||
this.g.getConnectedLinks(element, {inbound: true}).forEach(link => {
|
||||
if (link.get('source') && link.get('source').id && this.g.getCell(link.get('source').id) &&
|
||||
this.g.getCell(link.get('source').id).get('metadata')?.name) {
|
||||
this.linksToVisit.add(link.get('id'));
|
||||
this.numberOfLinksToVisit++;
|
||||
indegree++;
|
||||
}
|
||||
});
|
||||
this.nodesInDegrees.set(element.get('id'), indegree);
|
||||
this.numberOfNodesToVisit++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Priority:
|
||||
// 1. find links whose source has no other links pointing at it
|
||||
// 2. find links whose source has already been processed (not currently needed in sample DSL since
|
||||
// can't create graphs like that due to metamodel constraints)
|
||||
// 3. find remaining links
|
||||
private nextLink() : dia.Link {
|
||||
let indegree : number = Number.MAX_VALUE;
|
||||
let currentBest : dia.Link;
|
||||
for (let id of Array.from(this.linksToVisit)) {
|
||||
let link = <dia.Link> this.g.getCell(id);
|
||||
let source = this.g.getCell(link.get('source').id);
|
||||
let currentInDegree = this.nodesInDegrees.get(source.get('id'));
|
||||
if (currentInDegree === 0) {
|
||||
this.visitLink(link);
|
||||
return link;
|
||||
} else if (indegree > currentInDegree) {
|
||||
indegree = currentInDegree;
|
||||
currentBest = link;
|
||||
}
|
||||
}
|
||||
if (currentBest) {
|
||||
this.visitLink(currentBest);
|
||||
}
|
||||
return currentBest;
|
||||
}
|
||||
|
||||
private visitNode(n : dia.Element) : void {
|
||||
this.nodesToVisit.delete(n.get('id'));
|
||||
this.numberOfNodesToVisit--;
|
||||
}
|
||||
|
||||
private visitLink(e : dia.Link) : void {
|
||||
this.linksToVisit.delete(e.get('id'));
|
||||
let id = e.get('target').id;
|
||||
this.nodesInDegrees.set(id, this.nodesInDegrees.get(id) - 1);
|
||||
this.numberOfLinksToVisit--;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts at a link and proceeds down a chain. Converts each node to
|
||||
* text and then joins them with a ' > '.
|
||||
*/
|
||||
private chainToText(link : dia.Link) : string {
|
||||
let text = '';
|
||||
let source = <dia.Element> this.g.getCell(link.get('source').id);
|
||||
text += this.nodeToText(source);
|
||||
while (link) {
|
||||
let target = <dia.Element> this.g.getCell(link.get('target').id);
|
||||
text += ' > ';
|
||||
text += this.nodeToText(target);
|
||||
|
||||
// Find next not visited link to follow
|
||||
link = null;
|
||||
let outgoingLinks = this.g.getConnectedLinks(target, {outbound: true});
|
||||
for (let i = 0; i < outgoingLinks.length && !link; i++) {
|
||||
if (this.linksToVisit.has(outgoingLinks[i].get('id'))) {
|
||||
source = target;
|
||||
link = outgoingLinks[i];
|
||||
this.visitLink(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Very basic format. From a node to the text:
|
||||
* "name --key=value --key=value"
|
||||
*/
|
||||
private nodeToText(element : dia.Element) {
|
||||
let text = '';
|
||||
let props = element.attr('props');
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
text += element.get('metadata')?.name;
|
||||
if (props) {
|
||||
Object.keys(props).forEach(propertyName => {
|
||||
text += ' --' + propertyName + '=' + props[propertyName];
|
||||
});
|
||||
}
|
||||
this.visitNode(element);
|
||||
return text;
|
||||
}
|
||||
|
||||
private appendChainText(text : string, chainText : string) {
|
||||
if (chainText) {
|
||||
if (text) {
|
||||
text += '\n';
|
||||
}
|
||||
text += chainText;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
public convert() : string {
|
||||
let text = '';
|
||||
let chainText : string;
|
||||
let id : string;
|
||||
|
||||
while (this.numberOfLinksToVisit) {
|
||||
chainText = this.chainToText(this.nextLink());
|
||||
text = this.appendChainText(text, chainText);
|
||||
}
|
||||
// Visit all disconnected nodes
|
||||
this.nodesToVisit.forEach(id => {
|
||||
chainText = this.nodeToText(<dia.Element> this.g.getCell(id));
|
||||
text = this.appendChainText(text, chainText);
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function convertGraphToText(g : dia.Graph) : string {
|
||||
return new GraphToTextConverter(g).convert();
|
||||
}
|
||||
170
projects/demo/app/metamodel.ts
Normal file
170
projects/demo/app/metamodel.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { Flo } from 'spring-flo';
|
||||
import { convertGraphToText } from './graph-to-text';
|
||||
import { convertTextToGraph } from './text-to-graph';
|
||||
|
||||
const metamodelData: Array<RawMetadata> = [{
|
||||
name: 'http', group: 'source', description: 'Receive HTTP input',
|
||||
properties: [
|
||||
{id: 'port', name: 'port', defaultValue: '80', description: 'Port on which to listen', type: 'number'}
|
||||
],
|
||||
}, {
|
||||
name: 'rabbit', group: 'source', description: 'Receives messages from RabbitMQ',
|
||||
properties: [
|
||||
{id: 'queue', name: 'queue', description: 'the queue(s) from which messages will be received'},
|
||||
{id: 'time-unit', name: 'time-unit', description: 'Time unit for heart beat messages', type: 'enum', options: ['HOURS', 'MINUTES', 'SECONDS', 'MILIOSECONDS'], defaultValue: 'SECONDS'},
|
||||
{id: 'heart-beat', name: 'heart-beat', description: 'Heart beat on/off', type: 'boolean', defaultValue: false},
|
||||
{id: 'interval', name: 'interval', description: 'Time period being consecutive heart beat messages', type: 'number', defaultValue: 20},
|
||||
{id: 'url', name: 'url', description: 'Service URL', type: 'url'},
|
||||
{id: 'password', name: 'password', description: 'Password to login to service', type: 'password'},
|
||||
{id: 'messages', name: 'messages', description: 'List of messages', type: 'list'},
|
||||
{id: 'counts', name: 'counts', description: 'List of counts', type: 'list[number]'},
|
||||
{id: 'language', name: 'language', description: 'Code snippet language', type: 'enum', options: ['javascript', 'ruby', 'java'], defaultValue: 'javascript'},
|
||||
{id: 'snippet', name: 'snippet', description: 'Code snippet', type: 'code', defaultValue: ''},
|
||||
{id: 'successes', name: 'successes', description: 'List of successes', type: 'list[boolean]'},
|
||||
],
|
||||
}, {
|
||||
name: 'filewatch', group: 'source', description: 'Produce messages from the content of files created in a directory',
|
||||
properties: [
|
||||
{id: 'dir', name: 'dir', description: 'the absolute path to monitor for files'},
|
||||
{id: 'withSubDir', name: 'withSubDir', description: 'With subdirectories', defaultValue: true, type: 'boolean'},
|
||||
{id: 'type', name: 'type', description: 'Type of file watching', type: 'enum', options: ['tough', 'so-so', 'easy']}
|
||||
],
|
||||
}, {
|
||||
// name: 'multi', group: 'processor', description: 'Custom element with multiple inputs and outputs',
|
||||
// properties: [
|
||||
// {id: 'inputChannel', name: 'input-channel', description: 'input channel name', type: 'string'}
|
||||
// ],
|
||||
// }, {
|
||||
name: 'transform', group: 'processor', description: 'Apply an expression to modify incoming messages',
|
||||
properties: [
|
||||
{id: 'expression', name: 'expression', defaultValue: 'payload', description: 'SpEL expression to apply'}
|
||||
],
|
||||
}, {
|
||||
name: 'filter', group: 'processor', description: 'Only allow messages through that pass the filter expression',
|
||||
properties: [
|
||||
{id: 'expression', name: 'expression', defaultValue: 'true', description: 'SpEL expression to use for filtering'}
|
||||
],
|
||||
}, {
|
||||
name: 'filesave', group: 'sink', description: 'Writes messages to a file',
|
||||
properties: [
|
||||
{id: 'dir', name: 'dir', description: 'Absolute path to directory'},
|
||||
{id: 'name', name: 'name', description: 'The name of the file to create'}
|
||||
],
|
||||
}, {
|
||||
name: 'end', group: 'sink', description: 'Writes messages to a file',
|
||||
properties: [
|
||||
{id: 'dir', name: 'dir', description: 'Absolute path to directory'},
|
||||
{id: 'name', name: 'name', description: 'The name of the file to create'}
|
||||
],
|
||||
}, {
|
||||
name: 'null', group: 'sink', description: 'Writes messages to a file',
|
||||
properties: [
|
||||
{id: 'dir', name: 'dir', description: 'Absolute path to directory'},
|
||||
{id: 'name', name: 'name', description: 'The name of the file to create'}
|
||||
],
|
||||
}, {
|
||||
name: 'console', group: 'sink', description: 'Writes messages to a file',
|
||||
properties: [
|
||||
{id: 'dir', name: 'dir', description: 'Absolute path to directory'},
|
||||
{id: 'name', name: 'name', description: 'The name of the file to create'}
|
||||
],
|
||||
}, {
|
||||
name: 'hdfs', group: 'sink', description: 'Writes messages to a file',
|
||||
properties: [
|
||||
{id: 'dir', name: 'dir', description: 'Absolute path to directory'},
|
||||
{id: 'name', name: 'name', description: 'The name of the file to create'}
|
||||
],
|
||||
}, {
|
||||
name: 'jdbc', group: 'sink', description: 'Writes messages to a file',
|
||||
properties: [
|
||||
{id: 'dir', name: 'dir', description: 'Absolute path to directory'},
|
||||
{id: 'name', name: 'name', description: 'The name of the file to create'}
|
||||
],
|
||||
}, {
|
||||
name: 'ftp', group: 'sink', description: 'Send messages over FTP',
|
||||
properties: [
|
||||
{id: 'host', name: 'host', description: 'the host name for the FTP server'},
|
||||
{id: 'port', name: 'port', description: 'The port for the FTP server', type: 'number'},
|
||||
{id: 'remoteDir', name: 'remoteDir', description: 'The remote directory on the server'},
|
||||
],
|
||||
}];
|
||||
|
||||
export interface RawMetadata {
|
||||
name: string;
|
||||
group: string;
|
||||
description: string;
|
||||
properties: Array < Flo.PropertyMetadata >;
|
||||
}
|
||||
|
||||
class Metadata implements Flo.ElementMetadata {
|
||||
|
||||
constructor(private rawData: RawMetadata) {
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this.rawData.name;
|
||||
}
|
||||
|
||||
get group(): string {
|
||||
return this.rawData.group;
|
||||
}
|
||||
|
||||
description(): Promise < string > {
|
||||
return Promise.resolve(this.rawData.description);
|
||||
}
|
||||
|
||||
get(property: String): Promise < Flo.PropertyMetadata > {
|
||||
return Promise.resolve(this.rawData.properties.find(p => p.id === property));
|
||||
}
|
||||
|
||||
properties() : Promise<Map<string,Flo.PropertyMetadata>> {
|
||||
let propertiesMap = new Map<string, Flo.PropertyMetadata>();
|
||||
this.rawData.properties.forEach(p => propertiesMap.set(p.id, p));
|
||||
return Promise.resolve(propertiesMap);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class Metamodel implements Flo.Metamodel {
|
||||
|
||||
private rawData: Array<RawMetadata>;
|
||||
|
||||
constructor() {
|
||||
this.rawData = metamodelData;
|
||||
}
|
||||
|
||||
textToGraph(flo: Flo.EditorContext, dsl : string): Promise<any> {
|
||||
console.log('Text -> Graph');
|
||||
return this.load().then(metamodel => {
|
||||
convertTextToGraph(flo, metamodel, dsl);
|
||||
flo.performLayout();
|
||||
flo.fitToPage();
|
||||
})
|
||||
}
|
||||
|
||||
graphToText(flo: Flo.EditorContext): Promise<string> {
|
||||
console.log('Graph -> Text');
|
||||
return new Promise((resolve) => resolve(convertGraphToText(flo.getGraph())));
|
||||
}
|
||||
|
||||
load(): Promise < Map < string, Map < string, Flo.ElementMetadata >>> {
|
||||
let data: Map < string, Map < string, Flo.ElementMetadata >> = new Map < string, Map < string, Flo.ElementMetadata >>();
|
||||
this.rawData
|
||||
.map(rawData => new Metadata(rawData))
|
||||
.forEach(metadata => {
|
||||
if (!data.has(metadata.group)) {
|
||||
data.set(metadata.group, new Map < string, Flo.ElementMetadata >());
|
||||
}
|
||||
data.get(metadata.group).set(metadata.name, metadata);
|
||||
}
|
||||
);
|
||||
return Promise.resolve(data);
|
||||
}
|
||||
|
||||
groups(): Array < string > {
|
||||
let groups: Set < string > = new Set < string >();
|
||||
this.rawData.forEach(metadata => groups.add(metadata.group));
|
||||
return Array.from(groups);
|
||||
}
|
||||
|
||||
}
|
||||
89
projects/demo/app/properties-editor.service.ts
Normal file
89
projects/demo/app/properties-editor.service.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {dia} from 'jointjs';
|
||||
import {PropertiesDialogComponent} from './properties.dialog.component';
|
||||
import {BsModalService} from 'ngx-bootstrap/modal';
|
||||
import {Flo, Properties} from '../../flo';
|
||||
import {Validators} from '@angular/forms';
|
||||
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class PropertiesEditorService {
|
||||
|
||||
constructor(private modalService: BsModalService) {}
|
||||
|
||||
openPropertiesDialog(cell: dia.Cell) {
|
||||
const bsModalRef = this.modalService.show(PropertiesDialogComponent);
|
||||
const metadata: Flo.ElementMetadata = cell.get('metadata');
|
||||
bsModalRef.content.title = `Properties for ${metadata.name.toUpperCase()}`;
|
||||
let propertiesModel = new SamplePropertiesGroupModel(new Properties.DefaultCellPropertiesSource(cell));
|
||||
propertiesModel.load();
|
||||
bsModalRef.content.propertiesGroupModel = propertiesModel;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
class SamplePropertiesGroupModel extends Properties.PropertiesGroupModel {
|
||||
protected createControlModel(property: Properties.Property): Properties.ControlModel<any> {
|
||||
let inputType = Properties.InputType.TEXT;
|
||||
let validation: Properties.Validation;
|
||||
switch (property.type) {
|
||||
case 'number':
|
||||
inputType = Properties.InputType.NUMBER;
|
||||
break;
|
||||
case 'url':
|
||||
inputType = Properties.InputType.URL;
|
||||
break;
|
||||
case 'password':
|
||||
inputType = Properties.InputType.PASSWORD;
|
||||
break;
|
||||
case 'boolean':
|
||||
return new Properties.CheckBoxControlModel(property);
|
||||
case 'e-mail':
|
||||
inputType = Properties.InputType.EMAIL;
|
||||
break;
|
||||
case 'list':
|
||||
case 'list[number]':
|
||||
case 'list[boolean]':
|
||||
return new Properties.GenericListControlModel(property);
|
||||
case 'enum':
|
||||
if (Array.isArray(property.valueOptions)) {
|
||||
return new Properties.SelectControlModel(property, Properties.InputType.SELECT, (<Array<string>> property.valueOptions).map(o => {
|
||||
return {
|
||||
name: o,
|
||||
value: o === property.defaultValue ? undefined : o
|
||||
}
|
||||
}));
|
||||
}
|
||||
break;
|
||||
case 'code':
|
||||
return new Properties.CodeControlModelWithDynamicLanguageProperty(property, 'language', this, this.encodeTextToDSL, this.decodeTextFromDSL);
|
||||
default:
|
||||
if (property.name === 'name') {
|
||||
validation = {
|
||||
validator: Validators.required,
|
||||
errorData: [
|
||||
{ id: 'required', message: 'Name is required!' }
|
||||
]
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
return new Properties.GenericControlModel(property, inputType, validation);
|
||||
}
|
||||
|
||||
encodeTextToDSL(text: string): string {
|
||||
return '\"' + text.replace(/(?:\r\n|\r|\n)/g, '\\n').replace(/"/g, '""') + '\"';
|
||||
}
|
||||
|
||||
decodeTextFromDSL(dsl: string): string {
|
||||
if (dsl.charAt(0) === '\"' && dsl.charAt(dsl.length - 1) === '\"') {
|
||||
dsl = dsl.substr(1, dsl.length - 2);
|
||||
}
|
||||
return dsl.replace(/\\n/g, '\n').replace(/\"\"/g, '"');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
13
projects/demo/app/properties.dialog.component.html
Normal file
13
projects/demo/app/properties.dialog.component.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title pull-left">{{title}}</h4>
|
||||
<button type="button" class="btn-close close pull-right" aria-label="Close" (click)="bsModalRef.hide()">
|
||||
<span aria-hidden="true" class="visually-hidden">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<properties-group *ngIf="propertiesGroupModel" [propertiesGroupModel]="propertiesGroupModel" [form]="propertiesFormGroup"></properties-group>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" (click)="handleCancel()">Close</button>
|
||||
<button type="button" class="btn btn-default" (click)="handleOk()" [disabled]="okDisabled">OK</button>
|
||||
</div>
|
||||
39
projects/demo/app/properties.dialog.component.ts
Normal file
39
projects/demo/app/properties.dialog.component.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Component, ViewEncapsulation } from '@angular/core';
|
||||
import { BsModalRef } from 'ngx-bootstrap/modal';
|
||||
import { Properties } from 'spring-flo';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
|
||||
// For JS linting
|
||||
// import 'jshint';
|
||||
|
||||
@Component({
|
||||
selector: 'properties-dialog-content',
|
||||
templateUrl: './properties.dialog.component.html',
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class PropertiesDialogComponent {
|
||||
|
||||
public title: string;
|
||||
|
||||
propertiesGroupModel : Properties.PropertiesGroupModel;
|
||||
|
||||
propertiesFormGroup : FormGroup;
|
||||
|
||||
constructor(public bsModalRef: BsModalRef) {
|
||||
this.propertiesFormGroup = new FormGroup({});
|
||||
}
|
||||
|
||||
handleOk() {
|
||||
this.propertiesGroupModel.applyChanges();
|
||||
this.bsModalRef.hide();
|
||||
}
|
||||
|
||||
handleCancel() {
|
||||
this.bsModalRef.hide();
|
||||
}
|
||||
|
||||
get okDisabled() {
|
||||
return !this.propertiesGroupModel || !this.propertiesFormGroup || !this.propertiesFormGroup.valid;
|
||||
}
|
||||
|
||||
}
|
||||
449
projects/demo/app/renderer.ts
Normal file
449
projects/demo/app/renderer.ts
Normal file
@@ -0,0 +1,449 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Flo, Constants } from 'spring-flo';
|
||||
import { dia } from 'jointjs';
|
||||
|
||||
const dagre = require('dagre');
|
||||
|
||||
const joint : any = Flo.joint;
|
||||
|
||||
|
||||
const HANDLE_ICON_MAP = new Map<string, string>()
|
||||
.set(Constants.REMOVE_HANDLE_TYPE, 'icons/delete.svg')
|
||||
.set(Constants.PROPERTIES_HANDLE_TYPE, 'icons/cog.svg');
|
||||
|
||||
const DECORATION_ICON_MAP = new Map<string, string>()
|
||||
.set(Constants.ERROR_DECORATION_KIND, 'icons/error.svg');
|
||||
|
||||
const IMAGE_W = 120;
|
||||
const IMAGE_H = 40;
|
||||
|
||||
const PORT_RADIUS = 2;
|
||||
|
||||
joint.shapes.flo.Multi = joint.shapes.basic.Generic.extend({
|
||||
|
||||
markup:
|
||||
'<g class="stream-module">' +
|
||||
'<g class="shape">' +
|
||||
'<rect class="box"/>' +
|
||||
'<text class="label"/>' +
|
||||
'<text class="label2"/>' +
|
||||
'</g>' +
|
||||
'<g class="port1-group">' +
|
||||
'<circle class="input-port1" />' +
|
||||
'<g class="label-group">' +
|
||||
'<rect class="port1-label-rect"/>' +
|
||||
'<text class="port1-label"></text>' +
|
||||
'</g>' +
|
||||
'</g>' +
|
||||
'<g class="port2-group">' +
|
||||
'<circle class="input-port2" />' +
|
||||
'<g class="label-group">' +
|
||||
'<rect class="port2-label-rect"/>' +
|
||||
'<text class="port2-label"></text>' +
|
||||
'</g>' +
|
||||
'</g>' +
|
||||
'<g class="port3-group">' +
|
||||
'<circle class="input-port3" />' +
|
||||
'<g class="label-group">' +
|
||||
'<rect class="port3-label-rect"/>' +
|
||||
'<text class="port3-label"></text>' +
|
||||
'</g>' +
|
||||
'</g>' +
|
||||
'<g class="port4-group">' +
|
||||
'<circle class="input-port4" />' +
|
||||
'<g class="label-group">' +
|
||||
'<rect class="port4-label-rect"/>' +
|
||||
'<text class="port4-label"></text>' +
|
||||
'</g>' +
|
||||
'</g>' +
|
||||
'<g class="port5-group">' +
|
||||
'<circle class="input-port5" />' +
|
||||
'<g class="label-group">' +
|
||||
'<rect class="port5-label-rect"/>' +
|
||||
'<text class="port5-label"></text>' +
|
||||
'</g>' +
|
||||
'</g>' +
|
||||
'<rect class="output-port"/>' +
|
||||
'</g>',
|
||||
|
||||
defaults: joint.util.defaultsDeep({
|
||||
|
||||
type: joint.shapes.flo.NODE_TYPE,
|
||||
position: {x: 0, y: 0},
|
||||
size: { width: IMAGE_W, height: IMAGE_H },
|
||||
attrs: {
|
||||
'.': {
|
||||
magnet: false,
|
||||
},
|
||||
'.box': {
|
||||
width: IMAGE_W,
|
||||
height: IMAGE_H,
|
||||
rx: 2,
|
||||
ry: 2,
|
||||
// 'fill-opacity':0, // see through
|
||||
stroke: '#6db33f',
|
||||
fill: '#eeeeee',
|
||||
'stroke-width': 2,
|
||||
},
|
||||
'.input-port1': {
|
||||
portName: 'port-1',
|
||||
port: 'input',
|
||||
ref: '.box',
|
||||
refCx: 0,
|
||||
refCy: 1 * 0.17,
|
||||
r: PORT_RADIUS,
|
||||
magnet: true,
|
||||
class: 'input-port1 flo-input-port'
|
||||
},
|
||||
'.port1-label': {
|
||||
ref: '.input-port1',
|
||||
'ref-x': 10,
|
||||
'ref-y': 0,
|
||||
text: 'port-1',
|
||||
class: 'port1-label flo-port-label'
|
||||
},
|
||||
'.port1-label-rect': {
|
||||
ref: '.port1-label',
|
||||
refWidth: 1,
|
||||
refHeight: 1,
|
||||
refX: 0,
|
||||
refY: 0,
|
||||
class: 'port1-label-rect flo-port-label-bg'
|
||||
},
|
||||
'.input-port2': {
|
||||
portName: 'port-2',
|
||||
port: 'in-port-2',
|
||||
r: PORT_RADIUS,
|
||||
ref: '.box',
|
||||
refCx: 0,
|
||||
refCy: 2 * 0.17,
|
||||
magnet: true,
|
||||
class: 'input-port2 flo-input-port'
|
||||
},
|
||||
'.port2-label': {
|
||||
ref: '.input-port2',
|
||||
'ref-x': 10,
|
||||
'ref-y': 0,
|
||||
text: 'port-2',
|
||||
class: 'port2-label flo-port-label'
|
||||
},
|
||||
'.port2-label-rect': {
|
||||
ref: '.port2-label',
|
||||
refWidth: 1,
|
||||
refHeight: 1,
|
||||
refX: 0,
|
||||
refY: 0,
|
||||
class: 'port2-label-rect flo-port-label-bg'
|
||||
},
|
||||
'.input-port3': {
|
||||
portName: 'port-3',
|
||||
port: 'input',
|
||||
r: PORT_RADIUS,
|
||||
ref: '.box',
|
||||
refCx: 0,
|
||||
refCy: 3 * 0.17,
|
||||
magnet: true,
|
||||
class: 'input-port3 flo-input-port'
|
||||
},
|
||||
'.port3-label': {
|
||||
ref: '.input-port3',
|
||||
'ref-x': 10,
|
||||
'ref-y': 0,
|
||||
text: 'port-3',
|
||||
class: 'port3-label flo-port-label'
|
||||
},
|
||||
'.port3-label-rect': {
|
||||
ref: '.port3-label',
|
||||
refWidth: 1,
|
||||
refHeight: 1,
|
||||
refX: 0,
|
||||
refY: 0,
|
||||
class: 'port3-label-rect flo-port-label-bg'
|
||||
},
|
||||
'.input-port4': {
|
||||
portName: 'port-4',
|
||||
port: 'input',
|
||||
r: PORT_RADIUS,
|
||||
ref: '.box',
|
||||
refCx: 0,
|
||||
refCy: 4 * 0.17,
|
||||
magnet: true,
|
||||
class: 'input-port4 flo-input-port'
|
||||
},
|
||||
'.port4-label': {
|
||||
ref: '.input-port4',
|
||||
'ref-x': 10,
|
||||
'ref-y': 0,
|
||||
text: 'port-4',
|
||||
class: 'port4-label flo-port-label'
|
||||
},
|
||||
'.port4-label-rect': {
|
||||
ref: '.port4-label',
|
||||
refWidth: 1,
|
||||
refHeight: 1,
|
||||
refX: 0,
|
||||
refY: 0,
|
||||
class: 'port4-label-rect flo-port-label-bg'
|
||||
},
|
||||
'.input-port5': {
|
||||
portName: 'port-5',
|
||||
port: 'input',
|
||||
r: PORT_RADIUS,
|
||||
ref: '.box',
|
||||
refCx: 0,
|
||||
refCy: 5 * 0.17,
|
||||
magnet: true,
|
||||
class: 'input-port5 flo-input-port'
|
||||
},
|
||||
'.port5-label': {
|
||||
ref: '.input-port5',
|
||||
'ref-x': 10,
|
||||
'ref-y': 0,
|
||||
text: 'port-5',
|
||||
class: 'port5-label flo-port-label'
|
||||
},
|
||||
'.port5-label-rect': {
|
||||
ref: '.port5-label',
|
||||
refWidth: 1,
|
||||
refHeight: 1,
|
||||
refX: 0,
|
||||
refY: 0,
|
||||
class: 'port5-label-rect flo-port-label-bg'
|
||||
},
|
||||
'.output-port': {
|
||||
port: 'output',
|
||||
height: 8, width: 8,
|
||||
magnet: true,
|
||||
fill: '#eeeeee',
|
||||
transform: 'translate(' + (IMAGE_W - 4) + ',' + ((IMAGE_H / 2) - 4) + ')',
|
||||
stroke: '#34302d',
|
||||
'stroke-width': 1,
|
||||
},
|
||||
'.label': {
|
||||
'ref-x': 0.5, // jointjs specific: relative position to ref'd element
|
||||
'ref-y': 0.525,
|
||||
'y-alignment': 'middle',
|
||||
'x-alignment': 'middle',
|
||||
ref: '.box', // jointjs specific: element for ref-x, ref-y
|
||||
fill: 'black',
|
||||
'font-size': 14
|
||||
},
|
||||
'.label2': {
|
||||
'y-alignment': 'middle',
|
||||
'ref-x': 0.15, // jointjs specific: relative position to ref'd element
|
||||
'ref-y': 0.2, // jointjs specific: relative position to ref'd element
|
||||
ref: '.box', // jointjs specific: element for ref-x, ref-y
|
||||
fill: 'black',
|
||||
'font-size': 20
|
||||
},
|
||||
'.shape': {
|
||||
}
|
||||
}
|
||||
}, joint.shapes.basic.Generic.prototype.defaults)
|
||||
});
|
||||
|
||||
/**
|
||||
* @author Alex Boyko
|
||||
* @author Andy Clement
|
||||
*/
|
||||
export class Renderer implements Flo.Renderer {
|
||||
|
||||
createHandle(kind :string) : dia.Element {
|
||||
return new joint.shapes.flo.ErrorDecoration({
|
||||
size: {width: 10, height: 10},
|
||||
attrs: {
|
||||
'image': {
|
||||
'xlink:href': HANDLE_ICON_MAP.get(kind)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createDecoration(kind : string, parent: dia.Cell) : dia.Element {
|
||||
const error = new joint.shapes.flo.ErrorDecoration({
|
||||
size: {width: 16, height: 16},
|
||||
attrs: {
|
||||
'image': {
|
||||
'xlink:href': DECORATION_ICON_MAP.get(kind)
|
||||
}
|
||||
}
|
||||
});
|
||||
if (parent instanceof joint.dia.Element) {
|
||||
const pt = (<dia.Element> parent).getBBox().topRight().offset(-error.size().width, 0);
|
||||
error.position(pt.x, pt.y);
|
||||
} else {
|
||||
// TODO: do something for the link perhaps?
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
createNode(viewerDescriptor: Flo.ViewerDescriptor, metadata : Flo.ElementMetadata, props : Map<string, any>): dia.Element {
|
||||
if (metadata.name === 'multi') {
|
||||
return new joint.shapes.flo.Multi();
|
||||
}
|
||||
return new joint.shapes.flo.Node();
|
||||
}
|
||||
|
||||
initializeNewNode(node : dia.Element, viewerDescriptor : Flo.ViewerDescriptor) {
|
||||
let metadata : Flo.ElementMetadata = node.get('metadata');
|
||||
if (metadata) {
|
||||
node.attr('.label/text', node.get('metadata').name);
|
||||
let group = node.get('metadata')?.group;
|
||||
if (group === 'source') {
|
||||
node.attr('.input-port/display','none');
|
||||
}
|
||||
if (group === 'sink') {
|
||||
node.attr('.output-port/display','none');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createLink(source : Flo.LinkEnd, target : Flo.LinkEnd, metadata : Flo.ElementMetadata, props : Map<string, any>) : dia.Link {
|
||||
const link = new joint.shapes.flo.Link(joint.util.deepSupplement({
|
||||
smooth: true,
|
||||
attrs: {
|
||||
'.': {
|
||||
//filter: { name: 'dropShadow', args: { dx: 1, dy: 1, blur: 2 } }
|
||||
},
|
||||
'.connection': {'stroke-width': 3, 'stroke': 'black', 'stroke-linecap': 'round'},
|
||||
'.marker-arrowheads': {display: 'none'},
|
||||
'.tool-options': {display: 'none'}
|
||||
},
|
||||
}));
|
||||
return link;
|
||||
}
|
||||
|
||||
handleLinkEvent?(context: Flo.EditorContext, event: string, link: dia.Link) {
|
||||
switch (event) {
|
||||
case 'change:target':
|
||||
this.updateTargetLabel(link, context.getPaper());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
updateTargetLabel(link: joint.dia.Link, paper: dia.Paper) {
|
||||
const view = paper.findViewByModel(link.getTargetElement());
|
||||
const portElement = view ? view.findBySelector(link.target().selector) : undefined;
|
||||
const labelText = portElement && portElement.length ? portElement[0].getAttribute('port-name') : undefined;
|
||||
setTimeout(() => {
|
||||
let idx = -1;
|
||||
for (let i = 0; idx < 0 && i < link.labels().length; i++) {
|
||||
if (link.labels()[i].attrs.text.id === 'target-channel-label') {
|
||||
idx = i;
|
||||
}
|
||||
}
|
||||
if (idx >= 0) {
|
||||
link.removeLabel(idx);
|
||||
}
|
||||
if (labelText) {
|
||||
link.appendLabel({
|
||||
attrs: {
|
||||
text: {
|
||||
id: 'target-channel-label',
|
||||
text: labelText,
|
||||
class: 'link-channel-label',
|
||||
'text-anchor': 'end'
|
||||
},
|
||||
rect: {
|
||||
class: 'link-channel-label'
|
||||
}
|
||||
},
|
||||
position: {
|
||||
args: {
|
||||
keepGradient: true,
|
||||
ensureLegibility: true
|
||||
},
|
||||
distance: -20,
|
||||
offset: 20
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isSemanticProperty(propertyPath : string, element : dia.Cell) : boolean {
|
||||
return propertyPath === '.label/text';
|
||||
}
|
||||
|
||||
refreshVisuals(cell : dia.Cell, propertyPath : string, paper : dia.Paper) : void {
|
||||
// var type = element.attr('metadata/name');
|
||||
}
|
||||
|
||||
layout(paper : dia.Paper) {
|
||||
return new Promise((resolve) => {
|
||||
let graph = paper.model;
|
||||
let i : number;
|
||||
let g = new dagre.graphlib.Graph();
|
||||
|
||||
g.setGraph({});
|
||||
g.setDefaultEdgeLabel(() => {});
|
||||
|
||||
let nodes = graph.getElements();
|
||||
|
||||
nodes.forEach((node : dia.Element) => {
|
||||
if (node.get('type') === joint.shapes.flo.NODE_TYPE) {
|
||||
g.setNode(node.id, node.get('size'));
|
||||
}
|
||||
});
|
||||
|
||||
let links = graph.getLinks();
|
||||
links.forEach((link : dia.Link) => {
|
||||
if (link.get('type') === joint.shapes.flo.LINK_TYPE) {
|
||||
let options = {
|
||||
minlen: 1.5
|
||||
};
|
||||
// if (link.get('labels') && link.get('labels').length > 0) {
|
||||
// options.minlen = 1 + link.get('labels').length * 0.5;
|
||||
// }
|
||||
g.setEdge(link.get('source').id, link.get('target').id, options);
|
||||
link.set('vertices', []);
|
||||
}
|
||||
});
|
||||
|
||||
g.graph().rankdir = 'LR';
|
||||
|
||||
dagre.layout(g);
|
||||
|
||||
g.nodes().forEach((v : any) => {
|
||||
let node : any = graph.getCell(v);
|
||||
if (node) {
|
||||
let bbox = node.getBBox();
|
||||
node.translate(g.node(v).x - g.node(v).width / 2 - bbox.x, g.node(v).y - g.node(v).height / 2 - bbox.y);
|
||||
}
|
||||
});
|
||||
|
||||
resolve(undefined);
|
||||
});
|
||||
}
|
||||
|
||||
getLinkAnchorPoint(linkView : dia.LinkView, view : dia.ElementView, magnet : SVGElement, reference : dia.Point) : dia.Point {
|
||||
if (magnet) {
|
||||
let type = magnet.getAttribute('port');
|
||||
let bbox = joint.V(magnet).bbox(false, (<any>linkView).paper.viewport);
|
||||
let rect = joint.g.rect(bbox);
|
||||
if (type === 'input') {
|
||||
return joint.g.point(rect.x, rect.y + rect.height / 2);
|
||||
} else {
|
||||
return joint.g.point(rect.x + rect.width, rect.y + rect.height / 2);
|
||||
}
|
||||
} else {
|
||||
return reference;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
127
projects/demo/app/text-to-graph.ts
Normal file
127
projects/demo/app/text-to-graph.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Flo } from 'spring-flo';
|
||||
import { dia } from 'jointjs';
|
||||
|
||||
|
||||
/**
|
||||
* Convert a text representation to a graph.
|
||||
*
|
||||
* @author Alex Boyko
|
||||
* @author Andy Clement
|
||||
*/
|
||||
class TextToGraphConverter {
|
||||
|
||||
constructor(private flo : Flo.EditorContext, private metamodel : Map<string, Map<string, Flo.ElementMetadata>>) {
|
||||
|
||||
}
|
||||
|
||||
matchGroup(name : string, incoming : number, outgoing : number) : string {
|
||||
let score = Number.MIN_VALUE;
|
||||
let group : string;
|
||||
Array.from(this.metamodel.keys()).filter(group => this.metamodel.get(group).has(name)).map(group => this.metamodel.get(group).get(name)).find(match => {
|
||||
let failedConstraintsNumber = 0;
|
||||
if (match.group === 'source') {
|
||||
if (incoming > 0) {
|
||||
failedConstraintsNumber++;
|
||||
}
|
||||
if (outgoing > 1) {
|
||||
failedConstraintsNumber++;
|
||||
}
|
||||
} else if (match.group === 'sink') {
|
||||
if (incoming > 1) {
|
||||
failedConstraintsNumber++;
|
||||
}
|
||||
if (outgoing > 0) {
|
||||
failedConstraintsNumber++;
|
||||
}
|
||||
} else if (match.group === 'processor') {
|
||||
if (incoming > 1) {
|
||||
failedConstraintsNumber++;
|
||||
}
|
||||
if (outgoing > 1) {
|
||||
failedConstraintsNumber++;
|
||||
}
|
||||
}
|
||||
if (failedConstraintsNumber < score) {
|
||||
score = failedConstraintsNumber;
|
||||
group = match.group;
|
||||
}
|
||||
return failedConstraintsNumber === 0;
|
||||
});
|
||||
return group;
|
||||
}
|
||||
|
||||
convertToGraph(input : string) {
|
||||
this.flo.getGraph().clear();
|
||||
|
||||
// input is a string like this (3 nodes: foo, goo and hoo): foo --a=b --c=d > goo --d=e --f=g>hoo
|
||||
let trimmed = input.trim();
|
||||
if (trimmed.length===0) {
|
||||
return;
|
||||
}
|
||||
|
||||
trimmed.split('\n').forEach(line => {
|
||||
let lastNode : dia.Element;
|
||||
line.trim().split('>').map(e => e.trim()).forEach(element => {
|
||||
let startOfProps = element.indexOf(' ');
|
||||
let name = element.trim();
|
||||
let properties = new Map<string, any>();
|
||||
if (startOfProps !== -1) {
|
||||
name = element.substring(0,startOfProps);
|
||||
element.substring(startOfProps+1).trim().split(' ').map(pv => pv.trim()).filter(pv => pv.length).forEach(pv => {
|
||||
var equalsIndex = pv.indexOf('=');
|
||||
// The 2 skips the '--'
|
||||
let key = pv.substring(2,equalsIndex);
|
||||
let value = pv.substring(equalsIndex+1);
|
||||
properties.set(key, value);
|
||||
});
|
||||
}
|
||||
let group = this.matchGroup(name, 0, 0);
|
||||
let newNode = this.flo.createNode(Flo.getMetadata(this.metamodel,name,group),properties,{x:0,y:0});
|
||||
newNode.attr('.label/text',name);
|
||||
if (lastNode) {
|
||||
let sourceView = this.flo.getPaper().findViewByModel(lastNode);
|
||||
let sourceMagnet = Flo.findMagnetByClass(sourceView, '.output-port');
|
||||
let sourceEnd : Flo.LinkEnd = {
|
||||
id: lastNode.id,
|
||||
selector: sourceView.getSelector(sourceMagnet, undefined),
|
||||
};
|
||||
if (sourceMagnet.getAttribute('port')) {
|
||||
sourceEnd.port = sourceMagnet.getAttribute('port');
|
||||
}
|
||||
let targetView = this.flo.getPaper().findViewByModel(newNode);
|
||||
let targetMagnet = Flo.findMagnetByClass(targetView, '.input-port');
|
||||
let targetEnd : Flo.LinkEnd = {
|
||||
id: newNode.id,
|
||||
selector: targetView.getSelector(targetMagnet, undefined),
|
||||
};
|
||||
if (targetMagnet.getAttribute('port')) {
|
||||
targetEnd.port = targetMagnet.getAttribute('port');
|
||||
}
|
||||
this.flo.createLink(sourceEnd, targetEnd);
|
||||
}
|
||||
lastNode = newNode;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function convertTextToGraph(flo : Flo.EditorContext, metamodel : Map<string, Map<string, Flo.ElementMetadata>>, input : string) {
|
||||
return new TextToGraphConverter(flo, metamodel).convertToGraph(input);
|
||||
}
|
||||
3
projects/demo/environments/environment.prod.ts
Normal file
3
projects/demo/environments/environment.prod.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const environment = {
|
||||
production: true
|
||||
};
|
||||
16
projects/demo/environments/environment.ts
Normal file
16
projects/demo/environments/environment.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// This file can be replaced during build by using the `fileReplacements` array.
|
||||
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
|
||||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
export const environment = {
|
||||
production: false
|
||||
};
|
||||
|
||||
/*
|
||||
* For easier debugging in development mode, you can import the following file
|
||||
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||
*
|
||||
* This import should be commented out in production mode because it will have a negative impact
|
||||
* on performance if an error is thrown.
|
||||
*/
|
||||
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
|
||||
BIN
projects/demo/favicon.ico
Normal file
BIN
projects/demo/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
18
projects/demo/icons/cog.svg
Normal file
18
projects/demo/icons/cog.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 15.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "https://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="512.002px" height="526.229px" viewBox="0 0 512.002 526.229" enable-background="new 0 0 512.002 526.229"
|
||||
xml:space="preserve">
|
||||
<path d="M451.419,288.771c1.053-8.415,1.85-16.972,1.85-25.656s-0.797-17.234-1.85-25.656l55.654-43.54
|
||||
c5.011-3.95,6.449-11.055,3.16-16.843L457.611,85.91c-3.289-5.659-10.124-8.029-16.06-5.659l-65.509,26.44
|
||||
c-13.554-10.394-28.405-19.207-44.465-25.913l-9.867-69.729C320.529,4.869,315.134,0,308.556,0H203.31
|
||||
c-6.577,0-11.974,4.869-13.027,11.049l-9.866,69.729c-16.047,6.706-30.911,15.391-44.465,25.913l-65.516-26.44
|
||||
c-5.916-2.235-12.757,0-16.046,5.659L1.767,177.075c-3.289,5.659-1.844,12.765,3.154,16.843l55.52,43.54
|
||||
c-1.054,8.422-1.844,16.972-1.844,25.656s0.79,17.241,1.844,25.656l-55.52,43.54c-4.998,3.957-6.443,11.049-3.154,16.843
|
||||
l52.623,91.165c3.289,5.666,10.13,8.029,16.046,5.666l65.516-26.44c13.554,10.381,28.418,19.194,44.465,25.9l9.866,69.735
|
||||
c1.054,6.18,6.45,11.049,13.027,11.049h105.246c6.578,0,11.974-4.869,13.027-11.049l9.867-69.735
|
||||
c16.046-6.706,30.91-15.391,44.464-25.9l65.509,26.44c5.923,2.235,12.771,0,16.06-5.666l52.623-91.165
|
||||
c3.289-5.666,1.837-12.758-3.16-16.843L451.419,288.771z M255.933,355.204c-50.914,0-92.09-41.176-92.09-92.09
|
||||
s41.176-92.09,92.09-92.09s92.09,41.176,92.09,92.09S306.847,355.204,255.933,355.204z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
4
projects/demo/icons/delete.svg
Normal file
4
projects/demo/icons/delete.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" class="collapse-handle" viewBox="0 0 24 24">
|
||||
<line x1="4" y1="4" x2="20" y2="20" stroke="black" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="20" y1="4" x2="4" y2="20" stroke="black" stroke-width="8" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 296 B |
12
projects/demo/icons/error.svg
Normal file
12
projects/demo/icons/error.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "https://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="27.963px" height="27.963px" viewBox="0 0 27.963 27.963" style="enable-background:new 0 0 27.963 27.963;"
|
||||
xml:space="preserve">
|
||||
<g>
|
||||
<circle cx="13.9815" cy="13.9815" r="12.9815" stroke="black" stroke-width="1" fill="red" />
|
||||
<polygon style="fill:white;" points="15.578,17.158 16.19,4.579 11.803,4.579 12.413,17.158 "/>
|
||||
<path style="fill:white;" d="M13.997,18.546c-1.471,0-2.5,1.029-2.5,2.526c0,1.443,0.999,2.528,2.444,2.528h0.056
|
||||
c1.499,0,2.469-1.085,2.469-2.528C16.44,19.575,15.467,18.546,13.997,18.546z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 792 B |
26
projects/demo/index.html
Normal file
26
projects/demo/index.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Angular QuickStart</title>
|
||||
<base href="/">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
|
||||
<!-- Polyfill(s) for older browsers -->
|
||||
<!--<script src="node_modules/core-js/client/shim.min.js"></script>-->
|
||||
|
||||
<!--<script src="node_modules/zone.js/dist/zone.js"></script>-->
|
||||
<!--<script src="node_modules/systemjs/dist/system.src.js"></script>-->
|
||||
|
||||
<!--<script src="systemjs.config.js"></script>-->
|
||||
<!--<script>-->
|
||||
<!--System.import('main.js').catch(function(err){ console.error(err); });-->
|
||||
<!--</script>-->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<demo-app>Loading AppComponent content here ...</demo-app>
|
||||
</body>
|
||||
</html>
|
||||
44
projects/demo/karma.conf.js
Normal file
44
projects/demo/karma.conf.js
Normal file
@@ -0,0 +1,44 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage'),
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
],
|
||||
client: {
|
||||
jasmine: {
|
||||
// you can add configuration options for Jasmine here
|
||||
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
|
||||
// for example, you can disable the random execution with `random: false`
|
||||
// or set a specific seed with `seed: 4321`
|
||||
},
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
jasmineHtmlReporter: {
|
||||
suppressAll: true // removes the duplicated traces
|
||||
},
|
||||
coverageReporter: {
|
||||
dir: require('path').join(__dirname, '../../coverage/showcase'),
|
||||
subdir: '.',
|
||||
reporters: [
|
||||
{ type: 'html' },
|
||||
{ type: 'text-summary' }
|
||||
]
|
||||
},
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
singleRun: false,
|
||||
restartOnFileChange: true
|
||||
});
|
||||
};
|
||||
5
projects/demo/main.ts
Normal file
5
projects/demo/main.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
||||
57
projects/demo/polyfills.ts
Normal file
57
projects/demo/polyfills.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/***************************************************************************************************
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
|
||||
// import 'core-js/es6/symbol';
|
||||
// import 'core-js/es6/object';
|
||||
// import 'core-js/es6/function';
|
||||
// import 'core-js/es6/parse-int';
|
||||
// import 'core-js/es6/parse-float';
|
||||
// import 'core-js/es6/number';
|
||||
// import 'core-js/es6/math';
|
||||
// import 'core-js/es6/string';
|
||||
// import 'core-js/es6/date';
|
||||
// import 'core-js/es6/array';
|
||||
// import 'core-js/es6/regexp';
|
||||
// import 'core-js/es6/map';
|
||||
// import 'core-js/es6/set';
|
||||
|
||||
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
|
||||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||
|
||||
/** IE10 and IE11 requires the following to support `@angular/animation`. */
|
||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
|
||||
|
||||
/** Evergreen browsers require these. **/
|
||||
// import 'core-js/es6/reflect';
|
||||
// import 'core-js/es7/reflect';
|
||||
|
||||
|
||||
/** ALL Firefox browsers require the following to support `@angular/animation`. **/
|
||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
|
||||
|
||||
|
||||
/***************************************************************************************************
|
||||
* Zone JS is required by Angular itself.
|
||||
*/
|
||||
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||
|
||||
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
*/
|
||||
|
||||
/**
|
||||
* Date, currency, decimal and percent pipes.
|
||||
* Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
|
||||
*/
|
||||
// import 'intl'; // Run `npm install --save intl`.
|
||||
/**
|
||||
* Need to import at least one locale-data with intl.
|
||||
*/
|
||||
// import 'intl/locale-data/jsonp/en';
|
||||
|
||||
64
projects/demo/styles.css
Normal file
64
projects/demo/styles.css
Normal file
@@ -0,0 +1,64 @@
|
||||
@import "./../../node_modules/jointjs/dist/joint.core.css";
|
||||
@import "./../../node_modules/codemirror/lib/codemirror.css";
|
||||
@import "./../../node_modules/codemirror/addon/hint/show-hint.css";
|
||||
@import "./../../node_modules/codemirror/addon/lint/lint.css";
|
||||
@import "./../../node_modules/codemirror/addon/scroll/simplescrollbars.css";
|
||||
|
||||
h1 {
|
||||
color: #369;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 250%;
|
||||
}
|
||||
|
||||
.connecting .flo-input-port {
|
||||
fill: #aa1111;
|
||||
r: 4;
|
||||
}
|
||||
|
||||
.connecting .label-group {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.label-group {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.flo-input-port {
|
||||
fill: #eeeeee;
|
||||
stroke: #34302d;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.flo-port-label {
|
||||
text-anchor: start;
|
||||
fill: black;
|
||||
}
|
||||
|
||||
.flo-port-label-bg {
|
||||
fill: lightblue;
|
||||
stroke: lightblue;
|
||||
stroke-width: 7px;
|
||||
rx: 3;
|
||||
ry: 3;
|
||||
refWidth: 1;
|
||||
refHeight: 1;
|
||||
}
|
||||
|
||||
.joint-link:hover .link-channel-label {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.link-channel-label {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.available-magnet {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.joint-element.highlighted rect {
|
||||
stroke: #34302d;
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
|
||||
14
projects/demo/test.ts
Normal file
14
projects/demo/test.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
||||
|
||||
import 'zone.js/testing';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
import {
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting
|
||||
} from '@angular/platform-browser-dynamic/testing';
|
||||
|
||||
// First, initialize the Angular testing environment.
|
||||
getTestBed().initTestEnvironment(
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting(),
|
||||
);
|
||||
25
projects/demo/tsconfig.dev.json
Normal file
25
projects/demo/tsconfig.dev.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"angularCompilerOptions": {
|
||||
"paths": {
|
||||
"spring-flo": [ "../spring-flo/index.ts" ]
|
||||
}
|
||||
},
|
||||
"compilerOptions": {
|
||||
"outDir": "../../out-tsc/app",
|
||||
"declaration": false,
|
||||
"emitDecoratorMetadata": true,
|
||||
"paths": {
|
||||
"spring-flo": [
|
||||
"../spring-flo/index.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"main.ts",
|
||||
"polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"**/*.d.ts"
|
||||
]
|
||||
}
|
||||
24
projects/demo/tsconfig.json
Normal file
24
projects/demo/tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../out-tsc/app",
|
||||
"declaration": false,
|
||||
"emitDecoratorMetadata": true,
|
||||
"paths": {
|
||||
"spring-flo/*": [
|
||||
"../../dist/spring-flo/*"
|
||||
],
|
||||
"spring-flo": [
|
||||
"../../dist/spring-flo"
|
||||
]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"main.ts",
|
||||
"polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"**/*.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
28
projects/demo/tsconfig.prod.json
Normal file
28
projects/demo/tsconfig.prod.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"angularCompilerOptions": {
|
||||
"paths": {
|
||||
"spring-flo": [ "../../dist/spring-flo" ]
|
||||
}
|
||||
},
|
||||
"compilerOptions": {
|
||||
"outDir": "../../out-tsc/app",
|
||||
"declaration": false,
|
||||
"emitDecoratorMetadata": true,
|
||||
"paths": {
|
||||
"spring-flo/*": [
|
||||
"../../dist/spring-flo/*"
|
||||
],
|
||||
"spring-flo": [
|
||||
"../../dist/spring-flo"
|
||||
]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"main.ts",
|
||||
"polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"**/*.d.ts"
|
||||
]
|
||||
}
|
||||
1
projects/demo/typings.d.ts
vendored
Normal file
1
projects/demo/typings.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/* Types go here */
|
||||
8
projects/flo/@types/codemirror-minified/index.d.ts
vendored
Normal file
8
projects/flo/@types/codemirror-minified/index.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
declare module 'codemirror-minified' {
|
||||
|
||||
import * as CodeMirror from '@types/codemirror'
|
||||
|
||||
export = CodeMirror;
|
||||
export as namespace CodeMirror;
|
||||
|
||||
}
|
||||
3
projects/flo/code-editor/code-editor.component.html
Normal file
3
projects/flo/code-editor/code-editor.component.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class="code-editor-host">
|
||||
<textarea id="code-editor-host"></textarea>
|
||||
</div>
|
||||
20
projects/flo/code-editor/code-editor.component.scss
Normal file
20
projects/flo/code-editor/code-editor.component.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
.CodeMirror {
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
height: 100%;
|
||||
}
|
||||
.CodeMirror-hint {
|
||||
max-width: 38em;
|
||||
}
|
||||
.CodeMirror-vertical-ruler-error {
|
||||
background-color: rgba(188, 0, 0, 0.5);
|
||||
}
|
||||
.CodeMirror-vertical-ruler-warning {
|
||||
background-color: rgba(255, 188, 0, 0.5);
|
||||
}
|
||||
.CodeMirror-lint-tooltip {
|
||||
z-index: 2000;
|
||||
}
|
||||
228
projects/flo/code-editor/code-editor.component.ts
Normal file
228
projects/flo/code-editor/code-editor.component.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import { Component, Input, Output, ElementRef, EventEmitter, OnInit, ViewEncapsulation, forwardRef } from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import * as CodeMirror from 'codemirror';
|
||||
|
||||
import $ from 'jquery';
|
||||
|
||||
// CodeMirror extensions
|
||||
import 'codemirror/mode/meta';
|
||||
import 'codemirror/addon/lint/lint';
|
||||
import 'codemirror/addon/hint/show-hint';
|
||||
// import 'codemirror/addon/mode/loadmode';
|
||||
import 'codemirror/addon/edit/matchbrackets';
|
||||
import 'codemirror/addon/edit/closebrackets';
|
||||
import 'codemirror/addon/display/placeholder';
|
||||
import 'codemirror/addon/scroll/annotatescrollbar';
|
||||
import 'codemirror/addon/scroll/simplescrollbars';
|
||||
|
||||
// Lint support
|
||||
// Unclear how to import this dynamically...
|
||||
// import 'codemirror/addon/lint/javascript-lint';
|
||||
// import 'codemirror/addon/lint/json-lint';
|
||||
// import 'codemirror/addon/lint/yaml-lint';
|
||||
|
||||
// TODO: use dynamic import with JS7 in the future. CM autoLoad cannot load it properly - thinks its AMD
|
||||
// Supported languages until dynamic loading
|
||||
// import 'codemirror/mode/groovy/groovy';
|
||||
// import 'codemirror/mode/javascript/javascript';
|
||||
// import 'codemirror/mode/python/python';
|
||||
// import 'codemirror/mode/ruby/ruby';
|
||||
// import 'codemirror/mode/clike/clike';
|
||||
// import 'codemirror/mode/yaml/yaml';
|
||||
|
||||
@Component({
|
||||
selector: 'code-editor',
|
||||
templateUrl: './code-editor.component.html',
|
||||
styleUrls: ['./code-editor.component.scss', ],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => CodeEditorComponent),
|
||||
multi: true
|
||||
}
|
||||
]
|
||||
})
|
||||
export class CodeEditorComponent implements OnInit, ControlValueAccessor {
|
||||
|
||||
private doc: CodeMirror.EditorFromTextArea;
|
||||
|
||||
private _dsl = '';
|
||||
|
||||
private _lint: boolean | CodeMirror.LintOptions = false;
|
||||
|
||||
private _language: string;
|
||||
|
||||
private errorRuler: any;
|
||||
|
||||
private warningRuler: any;
|
||||
|
||||
private _onChangeHandler: (_: any) => void;
|
||||
|
||||
private _onTouchHandler: () => void;
|
||||
|
||||
@Input('line-numbers')
|
||||
lineNumbers = false;
|
||||
|
||||
@Input('line-wrapping')
|
||||
lineWrapping = false;
|
||||
|
||||
@Input('scrollbar-style')
|
||||
scrollbarStyle: string;
|
||||
|
||||
@Input()
|
||||
placeholder: string;
|
||||
|
||||
@Input('overview-ruler')
|
||||
overviewRuler: boolean;
|
||||
|
||||
@Output()
|
||||
dslChange = new EventEmitter<string>();
|
||||
|
||||
@Output()
|
||||
focus = new EventEmitter<void>();
|
||||
|
||||
@Output()
|
||||
blur = new EventEmitter<void>();
|
||||
|
||||
@Output()
|
||||
editor = new EventEmitter<CodeMirror.EditorFromTextArea>();
|
||||
|
||||
private _dslChangedHandler = () => {
|
||||
this._dsl = this.doc.getValue();
|
||||
this.dslChange.emit(this._dsl);
|
||||
if (this._onChangeHandler) {
|
||||
this._onChangeHandler(this._dsl);
|
||||
}
|
||||
};
|
||||
|
||||
constructor(private element: ElementRef) {}
|
||||
|
||||
@Input()
|
||||
set dsl(dsl: string) {
|
||||
this._dsl = dsl;
|
||||
if (this.doc && this._dsl !== this.doc.getValue()) {
|
||||
let cursorPosition = (<any>this.doc).getCursor();
|
||||
this.doc.setValue(this._dsl || '');
|
||||
(<any>this.doc).setCursor(cursorPosition);
|
||||
}
|
||||
}
|
||||
|
||||
@Input()
|
||||
set language(_language: string) {
|
||||
if (this._language !== _language) {
|
||||
this._language = _language;
|
||||
this.loadEditorMode();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
let options: any = {
|
||||
value: this._dsl || '',
|
||||
gutters: ['CodeMirror-lint-markers'],
|
||||
extraKeys: {'Ctrl-Space': 'autocomplete'},
|
||||
lineNumbers: this.lineNumbers,
|
||||
lineWrapping: this.lineWrapping,
|
||||
matchBrackets: true,
|
||||
autoCloseBrackets: true,
|
||||
};
|
||||
|
||||
if (this.scrollbarStyle) {
|
||||
(<any> options).scrollbarStyle = this.scrollbarStyle;
|
||||
}
|
||||
|
||||
if (this._lint) {
|
||||
options.lint = this._lint;
|
||||
}
|
||||
|
||||
this.doc = CodeMirror.fromTextArea(<HTMLTextAreaElement>$('#code-editor-host', this.element.nativeElement)[0], options);
|
||||
|
||||
if (this.placeholder) {
|
||||
this.doc.setOption('placeholder', this.placeholder);
|
||||
}
|
||||
|
||||
// Turns out "value" in the option doesn't set it.
|
||||
this.doc.setValue(this._dsl || '');
|
||||
|
||||
this.doc.on('change', this._dslChangedHandler);
|
||||
this.doc.on('focus', () => {
|
||||
this.focus.emit();
|
||||
if (this._onTouchHandler) {
|
||||
this._onTouchHandler();
|
||||
}
|
||||
});
|
||||
this.doc.on('blur', () => this.blur.emit());
|
||||
|
||||
this.warningRuler = (<any>this.doc).annotateScrollbar('CodeMirror-vertical-ruler-warning');
|
||||
this.errorRuler = (<any>this.doc).annotateScrollbar('CodeMirror-vertical-ruler-error');
|
||||
|
||||
this.loadEditorMode();
|
||||
|
||||
this.editor.emit(this.doc);
|
||||
}
|
||||
|
||||
private loadEditorMode() {
|
||||
// CodeMirror doc object must be initialized
|
||||
if (!this.doc) {
|
||||
return
|
||||
}
|
||||
|
||||
const info = this._language ? (<any>CodeMirror).findModeByName(this._language) : undefined;
|
||||
|
||||
// Set proper editor mode
|
||||
if (info) {
|
||||
this.doc.setOption('mode', info.mime);
|
||||
// (<any>CodeMirror).autoLoadMode(this.doc, info.mode);
|
||||
} else {
|
||||
this.doc.setOption('mode', 'text/plain');
|
||||
}
|
||||
|
||||
// Set proper Lint mode
|
||||
this.doc.setOption('lint', this.getLintOptions());
|
||||
}
|
||||
|
||||
writeValue(obj: any): void {
|
||||
this.dsl = obj;
|
||||
}
|
||||
|
||||
registerOnChange(fn: (_: any) => void): void {
|
||||
this._onChangeHandler = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: () => void): void {
|
||||
this._onTouchHandler = fn;
|
||||
}
|
||||
|
||||
private getLintOptions(): boolean | any {
|
||||
switch (this._language) {
|
||||
case 'javascript':
|
||||
case 'json':
|
||||
case 'coffeescript':
|
||||
case 'yaml':
|
||||
return {
|
||||
onUpdateLinting: (annotations: {}[]) => {
|
||||
const warnings: any[] = [];
|
||||
const errors: any[] = [];
|
||||
if (this.overviewRuler) {
|
||||
if (Array.isArray(annotations)) {
|
||||
annotations.forEach((a: any) => {
|
||||
if (a.to && a.from && a.from.line >= 0 && a.from.ch >= 0 && a.to.line >= a.from.line && a.from.ch >= 0) {
|
||||
if (a.severity === 'error') {
|
||||
errors.push(a);
|
||||
} else if (a.severity === 'warning') {
|
||||
warnings.push(a);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
this.warningRuler.update(warnings);
|
||||
this.errorRuler.update(errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
144
projects/flo/directives/resizer.ts
Normal file
144
projects/flo/directives/resizer.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import {Directive, Input, Output, EventEmitter, Inject, ElementRef, OnInit, OnDestroy} from '@angular/core';
|
||||
import {DOCUMENT} from '@angular/common'
|
||||
import { fromEvent } from 'rxjs';
|
||||
import { sampleTime } from 'rxjs/operators';
|
||||
|
||||
import { CompositeDisposable, Disposable } from 'ts-disposables';
|
||||
import $ from 'jquery';
|
||||
|
||||
@Directive({
|
||||
selector: '[resizer]',
|
||||
host: {'(mousedown)': 'startDrag()'}
|
||||
})
|
||||
export class ResizerDirective implements OnInit, OnDestroy {
|
||||
private dragInProgress = false;
|
||||
private vertical = true;
|
||||
private first: string;
|
||||
private second: string;
|
||||
private _size: number;
|
||||
private _splitSize: number;
|
||||
private _subscriptions = new CompositeDisposable();
|
||||
@Input()
|
||||
maxSplitSize: number;
|
||||
|
||||
@Output()
|
||||
sizeChange = new EventEmitter<number>();
|
||||
|
||||
private mouseMoveHandler = (e: any) => {
|
||||
if (this.dragInProgress) {
|
||||
this.mousemove(e);
|
||||
}
|
||||
};
|
||||
|
||||
@Input()
|
||||
set splitSize(splitSize: number) {
|
||||
|
||||
if (this.maxSplitSize && splitSize > this.maxSplitSize) {
|
||||
splitSize = this.maxSplitSize;
|
||||
}
|
||||
|
||||
if (this.vertical) {
|
||||
// Handle vertical resizer
|
||||
$(this.element.nativeElement).css({
|
||||
left: splitSize + 'px'
|
||||
});
|
||||
|
||||
$(this.element.nativeElement).parent().find(this.first).css({
|
||||
width: splitSize + 'px'
|
||||
});
|
||||
$(this.element.nativeElement).parent().find(this.second).css({
|
||||
left: (splitSize + this._size) + 'px'
|
||||
});
|
||||
} else {
|
||||
// Handle horizontal resizer
|
||||
$(this.element.nativeElement).css({
|
||||
bottom: splitSize + 'px'
|
||||
});
|
||||
|
||||
$(this.element.nativeElement).parent().find(this.first).css({
|
||||
bottom: (splitSize + this._size) + 'px'
|
||||
});
|
||||
$(this.element.nativeElement).parent().find(this.second).css({
|
||||
height: splitSize + 'px'
|
||||
});
|
||||
}
|
||||
|
||||
this._splitSize = splitSize;
|
||||
|
||||
// Update the local field
|
||||
this.sizeChange.emit(splitSize);
|
||||
}
|
||||
|
||||
@Input()
|
||||
set resizerWidth(width: number) {
|
||||
this._size = width;
|
||||
this.vertical = true;
|
||||
}
|
||||
|
||||
@Input()
|
||||
set resizerHeight(height: number) {
|
||||
this._size = height;
|
||||
this.vertical = false;
|
||||
}
|
||||
|
||||
@Input()
|
||||
set resizerLeft(first: string) {
|
||||
this.first = first;
|
||||
}
|
||||
|
||||
@Input()
|
||||
set resizerTop(first: string) {
|
||||
this.first = first;
|
||||
}
|
||||
|
||||
@Input()
|
||||
set resizerRight(second: string) {
|
||||
this.second = second;
|
||||
}
|
||||
|
||||
@Input()
|
||||
set resizerBottom(second: string) {
|
||||
this.second = second;
|
||||
}
|
||||
|
||||
constructor(private element: ElementRef, @Inject(DOCUMENT) private document: any) {
|
||||
}
|
||||
|
||||
startDrag() {
|
||||
this.dragInProgress = true;
|
||||
}
|
||||
|
||||
private mousemove(event: any) {
|
||||
let size: number;
|
||||
if (this.vertical) {
// Handle vertical resizer. Calculate new size relative to palette container DOM node
|
||||
size = event.pageX - $(this.first).offset().left;
|
||||
} else {
|
||||
// Handle horizontal resizer Calculate new size relative to palette container DOM node
|
||||
size = window.innerHeight - event.pageY - $(this.second).offset().top;
|
||||
}
|
||||
this.splitSize = size;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
// Need to set left and right elements width and fire events on init when DOM is built
|
||||
this.splitSize = this._splitSize;
|
||||
|
||||
let subscription1 = fromEvent($(this.document).get(0), 'mousemove')
|
||||
.pipe(sampleTime(300))
|
||||
.subscribe(this.mouseMoveHandler);
|
||||
this._subscriptions.add(Disposable.create(() => subscription1.unsubscribe()));
|
||||
let subscription2 = fromEvent($(this.document).get(0), 'mouseup')
|
||||
.subscribe(e => {
|
||||
if (this.dragInProgress) {
|
||||
this.mousemove(e);
|
||||
this.dragInProgress = false;
|
||||
}
|
||||
});
|
||||
this._subscriptions.add(Disposable.create(() => subscription2.unsubscribe()));
|
||||
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._subscriptions.dispose();
|
||||
}
|
||||
}
|
||||
1
projects/flo/dsl-editor/dsl-editor.component.html
Normal file
1
projects/flo/dsl-editor/dsl-editor.component.html
Normal file
@@ -0,0 +1 @@
|
||||
<textarea id="dsl-editor-host"></textarea>
|
||||
22
projects/flo/dsl-editor/dsl-editor.component.scss
Normal file
22
projects/flo/dsl-editor/dsl-editor.component.scss
Normal file
@@ -0,0 +1,22 @@
|
||||
/* Code Mirror related styles START */
|
||||
|
||||
.CodeMirror {
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
height: 100%;
|
||||
}
|
||||
.CodeMirror-hint {
|
||||
max-width: 38em;
|
||||
}
|
||||
.CodeMirror-vertical-ruler-error {
|
||||
background-color: rgba(188, 0, 0, 0.5);
|
||||
}
|
||||
.CodeMirror-vertical-ruler-warning {
|
||||
background-color: rgba(255, 188, 0, 0.5);
|
||||
}
|
||||
|
||||
|
||||
/* Code Mirror related styles END */
|
||||
133
projects/flo/dsl-editor/dsl-editor.component.ts
Normal file
133
projects/flo/dsl-editor/dsl-editor.component.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { Component, Input, Output, ElementRef, EventEmitter, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import debounce from 'lodash/debounce';
|
||||
import * as CodeMirror from 'codemirror';
|
||||
import $ from 'jquery';
|
||||
|
||||
import 'codemirror/addon/lint/lint';
|
||||
import 'codemirror/addon/hint/show-hint';
|
||||
import 'codemirror/addon/display/placeholder';
|
||||
import 'codemirror/addon/scroll/annotatescrollbar';
|
||||
import 'codemirror/addon/scroll/simplescrollbars';
|
||||
|
||||
@Component({
|
||||
selector: 'dsl-editor',
|
||||
templateUrl: './dsl-editor.component.html',
|
||||
styleUrls: ['./dsl-editor.component.scss', ],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class DslEditorComponent implements OnInit {
|
||||
|
||||
private doc: CodeMirror.EditorFromTextArea;
|
||||
|
||||
private _dsl = '';
|
||||
|
||||
private _lint: boolean | CodeMirror.LintOptions = false;
|
||||
|
||||
private _hint: any;
|
||||
|
||||
@Input('line-numbers')
|
||||
lineNumbers = false;
|
||||
|
||||
@Input('line-wrapping')
|
||||
lineWrapping = false;
|
||||
|
||||
@Input('scrollbar-style')
|
||||
scrollbarStyle: string;
|
||||
|
||||
@Input()
|
||||
placeholder: string;
|
||||
|
||||
@Input()
|
||||
debounce = 0;
|
||||
|
||||
@Output()
|
||||
dslChange = new EventEmitter<string>();
|
||||
|
||||
@Output()
|
||||
focus = new EventEmitter<void>();
|
||||
|
||||
@Output()
|
||||
blur = new EventEmitter<void>();
|
||||
|
||||
@Output()
|
||||
editor = new EventEmitter<CodeMirror.EditorFromTextArea>();
|
||||
|
||||
private _dslChangedHandler = () => {
|
||||
this._dsl = this.doc.getValue();
|
||||
this.dslChange.emit(this._dsl);
|
||||
};
|
||||
|
||||
constructor(private element: ElementRef) {}
|
||||
|
||||
@Input()
|
||||
set dsl(dsl: string) {
|
||||
this._dsl = dsl;
|
||||
if (this.doc && this._dsl !== this.doc.getValue()) {
|
||||
let cursorPosition = (<any>this.doc).getCursor();
|
||||
this.doc.setValue(this._dsl || '');
|
||||
(<any>this.doc).setCursor(cursorPosition);
|
||||
}
|
||||
}
|
||||
|
||||
@Input()
|
||||
set lintOptions(lintOptions: boolean | CodeMirror.LintOptions) {
|
||||
this._lint = lintOptions;
|
||||
if (this.doc) {
|
||||
this.doc.setOption('lint', this._lint);
|
||||
}
|
||||
}
|
||||
|
||||
@Input()
|
||||
set hintOptions(hintOptions: any) {
|
||||
this._hint = hintOptions;
|
||||
if (this.doc) {
|
||||
this.doc.setOption('hintOptions', this._hint);
|
||||
}
|
||||
}
|
||||
|
||||
triggerLinting() {
|
||||
this.doc.setOption('lint', {});
|
||||
this.doc.setOption('lint', this._lint);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
let options: CodeMirror.EditorConfiguration = {
|
||||
value: this._dsl || '',
|
||||
gutters: ['CodeMirror-lint-markers'],
|
||||
extraKeys: {'Ctrl-Space': 'autocomplete'},
|
||||
lineNumbers: this.lineNumbers,
|
||||
lineWrapping: this.lineWrapping,
|
||||
electricChars: false,
|
||||
smartIndent: false,
|
||||
};
|
||||
|
||||
if (this.scrollbarStyle) {
|
||||
(<any> options).scrollbarStyle = this.scrollbarStyle;
|
||||
}
|
||||
|
||||
if (this._lint) {
|
||||
options.lint = this._lint;
|
||||
}
|
||||
|
||||
if (this._hint) {
|
||||
(<any>options).hintOptions = this._hint;
|
||||
}
|
||||
|
||||
this.doc = CodeMirror.fromTextArea(<HTMLTextAreaElement>$('#dsl-editor-host', this.element.nativeElement)[0], options);
|
||||
|
||||
if (this.placeholder) {
|
||||
this.doc.setOption('placeholder', this.placeholder);
|
||||
}
|
||||
|
||||
// Turns out "value" in the option doesn't set it.
|
||||
this.doc.setValue(this._dsl || '');
|
||||
|
||||
this.doc.on('change', this.debounce ? debounce(this._dslChangedHandler, this.debounce) : this._dslChangedHandler);
|
||||
this.doc.on('focus', () => this.focus.emit());
|
||||
this.doc.on('blur', () => this.blur.emit());
|
||||
|
||||
this.editor.emit(this.doc);
|
||||
}
|
||||
|
||||
}
|
||||
135
projects/flo/editor/editor-utils.ts
Normal file
135
projects/flo/editor/editor-utils.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { dia } from 'jointjs';
|
||||
import { Flo } from '../shared/flo-common';
|
||||
import * as _ from 'lodash';
|
||||
const joint: any = Flo.joint;
|
||||
import * as _$ from 'jquery';
|
||||
const $: any = _$;
|
||||
|
||||
|
||||
export class Utils {
|
||||
|
||||
static fanRoute(graph: dia.Graph, cell: dia.Cell) {
|
||||
if (cell instanceof joint.dia.Element) {
|
||||
|
||||
const links = graph.getConnectedLinks(cell);
|
||||
|
||||
const groupsOfOverlappingLinks = _.groupBy(links, (link: dia.Link) => {
|
||||
// the key of the group is the model id of the link's source or target, but not our cell id.
|
||||
const sourceId = link.get('source').id;
|
||||
const targetId = link.get('target').id;
|
||||
return cell.id !== sourceId ? sourceId : targetId;
|
||||
});
|
||||
|
||||
_.each(groupsOfOverlappingLinks, (group: any, key: string) => {
|
||||
// If the member of the group has both source and target model adjust vertices.
|
||||
let toRoute: any = {};
|
||||
if (key !== undefined) {
|
||||
group.forEach((link: dia.Link) => {
|
||||
if (link.get('source').id === cell.get('id') && link.get('target').id) {
|
||||
toRoute[link.get('target').id] = link;
|
||||
} else if (link.get('target').id === cell.get('id') && link.get('source').id) {
|
||||
toRoute[link.get('source').id] = link;
|
||||
}
|
||||
});
|
||||
Object.keys(toRoute).forEach(k => {
|
||||
Utils.fanRoute(graph, toRoute[k]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
} else if (cell instanceof joint.dia.Link) {
|
||||
const link = <joint.dia.Link> cell;
|
||||
// The cell is a link. Let's find its source and target models.
|
||||
let src = link.source() || link.previous('source');
|
||||
let trg = link.target() || cell.previous('target');
|
||||
|
||||
// If one of the ends is not a model, the link has no siblings.
|
||||
if (!src.id || !trg.id) { return; }
|
||||
|
||||
const siblings: dia.Link[] = graph.getLinks().filter((sibling: dia.Link) => {
|
||||
|
||||
const _src = sibling.source();
|
||||
const _trg = sibling.target();
|
||||
const vertices = sibling.get('vertices');
|
||||
const fanRouted = !vertices || vertices.length === 0 || sibling.get('fanRouted');
|
||||
|
||||
return ((_.isEqual(_src, src) && _.isEqual(_trg, trg)) || (_.isEqual(_src, trg) && _.isEqual(_trg, src))) && fanRouted;
|
||||
|
||||
});
|
||||
|
||||
switch (siblings.length) {
|
||||
|
||||
case 0:
|
||||
// The link was removed and had no siblings.
|
||||
break;
|
||||
|
||||
case 1:
|
||||
// There is only one link between the source and target. No vertices needed.
|
||||
let vertices = cell.get('vertices');
|
||||
if (vertices && vertices.length && cell.get('fanRouted')) {
|
||||
cell.unset('vertices');
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
// There is more than one siblings. We need to create vertices.
|
||||
|
||||
// First of all we'll find the middle point of the link.
|
||||
let source = link.getSourceElement();
|
||||
let target = link.getTargetElement();
|
||||
|
||||
if (!source || !target) {
|
||||
// When clearing the graph it may happen that some nodes are gone and some are left
|
||||
return;
|
||||
}
|
||||
|
||||
let srcCenter = source.getBBox().center();
|
||||
let trgCenter = target.getBBox().center();
|
||||
let midPoint = joint.g.line(srcCenter, trgCenter).midpoint();
|
||||
|
||||
// Then find the angle it forms.
|
||||
let theta = srcCenter.theta(trgCenter);
|
||||
|
||||
// This is the maximum distance between links
|
||||
let gap = 20;
|
||||
|
||||
siblings.forEach((sibling: dia.Link, index: number) => {
|
||||
|
||||
// We want the offset values to be calculated as follows 0, 20, 20, 40, 40, 60, 60 ..
|
||||
let offset = gap * Math.ceil(index / 2);
|
||||
|
||||
// Now we need the vertices to be placed at points which are 'offset' pixels distant
|
||||
// from the first link and forms a perpendicular angle to it. And as index goes up
|
||||
// alternate left and right.
|
||||
//
|
||||
// ^ odd indexes
|
||||
// |
|
||||
// |----> index 0 line (straight line between a source center and a target center.
|
||||
// |
|
||||
// v even indexes
|
||||
let sign = index % 2 ? 1 : -1;
|
||||
let angle = joint.g.toRad(theta + sign * 90);
|
||||
|
||||
// We found the vertex.
|
||||
let vertex = joint.g.point.fromPolar(offset, angle, midPoint);
|
||||
|
||||
sibling.set('fanRouted', true);
|
||||
(<any>sibling).set('vertices', [{ x: vertex.x, y: vertex.y }], {'fanRouted': true});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static isCustomPaperEvent(args: any): boolean {
|
||||
return args.length === 5 &&
|
||||
_.isString(args[0]) &&
|
||||
(args[0].indexOf('link:') === 0 || args[0].indexOf('element:') === 0) &&
|
||||
args[1] instanceof $.Event &&
|
||||
args[2] instanceof joint.dia.CellView &&
|
||||
_.isNumber(args[3]) &&
|
||||
_.isNumber(args[4]);
|
||||
}
|
||||
|
||||
}
|
||||
55
projects/flo/editor/editor.component.html
Normal file
55
projects/flo/editor/editor.component.html
Normal file
@@ -0,0 +1,55 @@
|
||||
<ng-content select="[header]"></ng-content>
|
||||
<div id="flow-view" class="flow-view" style="position:relative">
|
||||
<div id="canvas" class="canvas" style="position:relative; display: block; width: 100%; height: 100%;">
|
||||
<div *ngIf="!noPalette" id="palette-container" class="palette-container" style="overflow:hidden;">
|
||||
<flo-palette [metamodel]="metamodel" [renderer]="renderer" [paletteSize]="paletteSize"
|
||||
[paletteEntryPadding]="paletteEntryPadding || {width: 12, height: 12}"
|
||||
(onPaletteEntryDrop)="handleDnDFromPalette($event)"
|
||||
(paletteReady)="updatePaletteReadyState($event)"
|
||||
(paletteFocus)="graphToTextSync=true"
|
||||
[searchFilterPlaceHolder]="searchFilterPlaceHolder">
|
||||
</flo-palette>
|
||||
</div>
|
||||
|
||||
<div id="sidebar-resizer" *ngIf="!noPalette"
|
||||
resizer
|
||||
[splitSize]="paletteSize"
|
||||
(sizeChange)="paletteSize = $event"
|
||||
[resizerWidth]="6"
|
||||
[resizerLeft]="'#palette-container'"
|
||||
[resizerRight]="'#paper-container'">
|
||||
</div>
|
||||
|
||||
<flo-editor-paper tabindex="0" (onDelete)="deleteSelected()" (onBlur)="selection = null"
|
||||
(onProperties)="onPropertiesHandle()">
|
||||
<div id="paper" class="paper" tabindex="0" style="overflow: hidden; position: absolute; display: block; height:100%; width:100%; overflow:auto;"></div>
|
||||
<ng-content select="[canvas]"></ng-content>
|
||||
</flo-editor-paper>
|
||||
|
||||
<span class="canvas-controls-container" ng-if="canvasControls">
|
||||
<table ng-if="canvasControls.zoom" class="canvas-control zoom-canvas-control">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<input class="zoom-canvas-input canvas-control zoom-canvas-control" type="text"
|
||||
data-inline="true" [(ngModel)]="zoomPercent"
|
||||
size="3">
|
||||
</td>
|
||||
<td>
|
||||
<label class="canvas-control zoom-canvas-label">%</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="range" data-inline="true" [(ngModel)]="zoomPercent"
|
||||
[step]="zoomStep"
|
||||
[max]="maxZoom" [min]="minZoom" data-type="range"
|
||||
name="range" class="canvas-control zoom-canvas-control">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<ng-content select="[footer]"></ng-content>
|
||||
|
||||
225
projects/flo/editor/editor.component.scss
Normal file
225
projects/flo/editor/editor.component.scss
Normal file
@@ -0,0 +1,225 @@
|
||||
flo-view {
|
||||
width:100%;
|
||||
height:100%;
|
||||
margin: 0;
|
||||
background-color: #eeeeee;
|
||||
font-family: "Varela Round",sans-serif;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.canvas {
|
||||
border: 1px solid;
|
||||
border-color: #6db33f;
|
||||
border-radius: 2px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
/* Canvas contains the palette on the left and the paper on the right */
|
||||
|
||||
.paper {
|
||||
padding: 0px;
|
||||
background-color: #ffffff;
|
||||
/* height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
*//* margin-left: 400px; */
|
||||
}
|
||||
|
||||
#sidebar-resizer {
|
||||
background-color: #34302d;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 6px;
|
||||
cursor: e-resize;
|
||||
}
|
||||
|
||||
#palette-container {
|
||||
background-color: #EEE;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#paper-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
overflow: auto;
|
||||
color: #FFF;
|
||||
background-color: #FFF;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Tooltip START */
|
||||
|
||||
.node-tooltip .tooltip-description {
|
||||
margin-top: 5px;
|
||||
margin-left: 0px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.node-tooltip {
|
||||
display:none;
|
||||
position:absolute;
|
||||
border:1px solid #333;
|
||||
background-color:#34302d;/*#161616;*/
|
||||
border-radius:5px;
|
||||
padding:5px;
|
||||
color:#fff;
|
||||
/* font-size:12px Arial;*/
|
||||
font-family: "Varela Round",sans-serif;
|
||||
font-size: 19px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.tooltip-title-type {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tooltip-title-group {
|
||||
padding-left: 5px;
|
||||
font-size: 20px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.node-tooltip-option-name {
|
||||
font-family: monospace;/*"Varela Round",sans-serif;*/
|
||||
font-size: 17px;
|
||||
font-weight: bold;
|
||||
padding-right: 20px;
|
||||
|
||||
}
|
||||
|
||||
.node-tooltip-option-description {
|
||||
font-family: "Varela Round",sans-serif;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* Tooltip END */
|
||||
|
||||
|
||||
/* Validation Error Marker on Canvas START */
|
||||
|
||||
.error-tooltip p {
|
||||
margin-top: 5px;
|
||||
margin-left: 0px;
|
||||
margin-bottom: 5px;
|
||||
color:#fff;
|
||||
}
|
||||
.error-tooltip {
|
||||
display:none;
|
||||
position:absolute;
|
||||
border:1px solid #333;
|
||||
background-color:red;/*#161616;*/
|
||||
border-radius:5px;
|
||||
padding:5px;
|
||||
color:#fff;
|
||||
/* font-size:12px Arial;*/
|
||||
font-family: "Varela Round",sans-serif;
|
||||
font-size: 20px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* Validation Error Marker on Canvas END */
|
||||
|
||||
/* Controls on Canvas START */
|
||||
|
||||
.canvas-controls-container {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
.canvas-control {
|
||||
background: transparent;
|
||||
font-family: "Varela Round",sans-serif;
|
||||
font-size: 11px;
|
||||
vertical-align: middle;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.zoom-canvas-control {
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.zoom-canvas-input {
|
||||
text-align: right;
|
||||
font-weight:bold;
|
||||
color: black;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.zoom-canvas-label {
|
||||
padding-right: 4px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
/* Controls on Canvas END */
|
||||
|
||||
|
||||
|
||||
|
||||
/* START - FLO CANVAS STYLES - override joint js styles */
|
||||
|
||||
.highlighted {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.joint-type-handle {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.available-magnet {
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
.link {
|
||||
fill: none;
|
||||
stroke: #ccc;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
.link-tools .tool-options {
|
||||
display: none; /* by default, we don't display link options tool */
|
||||
}
|
||||
|
||||
/* Make transparent the circle around the link-tools (cog) icon. It'll allow shape to have a circle clicking area */
|
||||
.link-tools .tool-options circle {
|
||||
fill: transparent;
|
||||
stroke: transparent;
|
||||
}
|
||||
|
||||
.link-tools .tool-options path {
|
||||
fill: black;
|
||||
stroke: black;
|
||||
}
|
||||
|
||||
.link-tools .tool-remove circle {
|
||||
fill: red;
|
||||
stroke: red;
|
||||
}
|
||||
|
||||
.link-tools .tool-remove path {
|
||||
fill: white;
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
.link-tools-container {
|
||||
stroke-width: 0;
|
||||
fill: transparent;
|
||||
}
|
||||
|
||||
/* END - FLO CANVAS STYLES */
|
||||
1350
projects/flo/editor/editor.component.ts
Normal file
1350
projects/flo/editor/editor.component.ts
Normal file
File diff suppressed because it is too large
Load Diff
64
projects/flo/editor/paper.component.ts
Normal file
64
projects/flo/editor/paper.component.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
Component,
|
||||
ElementRef, EventEmitter,
|
||||
HostListener, OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'flo-editor-paper',
|
||||
template: `
|
||||
<div #paper tabindex="0" id="paper-container">
|
||||
<ng-content></ng-content>
|
||||
</div>`,
|
||||
styleUrls: ['./editor.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class PaperComponent implements OnInit {
|
||||
|
||||
@ViewChild('paper', { static: true }) paperElement: ElementRef;
|
||||
|
||||
@Output()
|
||||
onDelete = new EventEmitter<number>();
|
||||
|
||||
@Output()
|
||||
onProperties = new EventEmitter<number>();
|
||||
|
||||
@Output()
|
||||
onBlur = new EventEmitter<boolean>();
|
||||
|
||||
@HostListener('click')
|
||||
click() {
|
||||
this.paperElement.nativeElement.focus();
|
||||
}
|
||||
|
||||
@HostListener('mousedown')
|
||||
mousedown() {
|
||||
this.paperElement.nativeElement.focus();
|
||||
}
|
||||
|
||||
@HostListener('keydown.backspace')
|
||||
backspaceHandle() {
|
||||
this.onDelete.emit();
|
||||
}
|
||||
|
||||
@HostListener('keydown.delete')
|
||||
deleteHandle() {
|
||||
this.onDelete.emit();
|
||||
}
|
||||
|
||||
@HostListener('keydown.o')
|
||||
oHandle() {
|
||||
this.onProperties.emit();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const onBlur = this.onBlur;
|
||||
this.paperElement.nativeElement.addEventListener('blur', () => {
|
||||
onBlur.emit();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
12
projects/flo/index.ts
Normal file
12
projects/flo/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export { FloModule } from './module';
|
||||
export { Palette } from './palette/palette.component';
|
||||
export { EditorComponent } from './editor/editor.component';
|
||||
export { DslEditorComponent } from './dsl-editor/dsl-editor.component';
|
||||
export { CodeEditorComponent } from './code-editor/code-editor.component';
|
||||
export { PropertiesGroupComponent } from './properties/properties.group.component';
|
||||
export { DynamicFormPropertyComponent } from './properties/df.property.component';
|
||||
export { ResizerDirective } from './directives/resizer';
|
||||
|
||||
export * from './shared/flo-common';
|
||||
export * from './shared/flo-properties';
|
||||
export * from './shared/shapes';
|
||||
37
projects/flo/module.ts
Normal file
37
projects/flo/module.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { Palette } from './palette/palette.component';
|
||||
import { EditorComponent } from './editor/editor.component';
|
||||
import { ResizerDirective } from './directives/resizer';
|
||||
import { DslEditorComponent } from './dsl-editor/dsl-editor.component';
|
||||
import { CodeEditorComponent } from './code-editor/code-editor.component';
|
||||
import { PropertiesGroupComponent } from './properties/properties.group.component';
|
||||
import { DynamicFormPropertyComponent } from './properties/df.property.component';
|
||||
import { PaperComponent } from './editor/paper.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
FormsModule,
|
||||
CommonModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
declarations: [
|
||||
Palette,
|
||||
EditorComponent,
|
||||
ResizerDirective,
|
||||
DslEditorComponent,
|
||||
CodeEditorComponent,
|
||||
PropertiesGroupComponent,
|
||||
DynamicFormPropertyComponent,
|
||||
PaperComponent
|
||||
],
|
||||
exports: [
|
||||
EditorComponent,
|
||||
DslEditorComponent,
|
||||
DynamicFormPropertyComponent,
|
||||
PropertiesGroupComponent
|
||||
]
|
||||
})
|
||||
export class FloModule { }
|
||||
15
projects/flo/ng-package.json
Normal file
15
projects/flo/ng-package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"dest": "../../dist/flo",
|
||||
"deleteDestPath": true,
|
||||
"lib": {
|
||||
"entryFile": "./index.ts"
|
||||
},
|
||||
"allowedNonPeerDependencies": [
|
||||
"codemirror",
|
||||
"jointjs",
|
||||
"jquery",
|
||||
"lodash",
|
||||
"ts-disposables"
|
||||
]
|
||||
}
|
||||
24
projects/flo/package.json
Normal file
24
projects/flo/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "spring-flo",
|
||||
"version": "0.13.0-alpha.0",
|
||||
"description": "Library for quickly building text DSL visualization diagram editor",
|
||||
"author": "Andy Clement, Alex Boyko",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spring-projects/spring-flo.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"codemirror": "5.65.12",
|
||||
"jointjs": "3.4.1",
|
||||
"jquery": "^3.6.3",
|
||||
"lodash": "^4.17.21",
|
||||
"ts-disposables": "2.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": ">=15.0.0 <17.0.0",
|
||||
"@angular/forms": ">=15.0.0 < 17.0.0",
|
||||
"@angular/common": ">=15.0.0 <17.0.0",
|
||||
"rxjs": ">=7.0.0 <8.0.0"
|
||||
}
|
||||
}
|
||||
9
projects/flo/palette/palette.component.html
Normal file
9
projects/flo/palette/palette.component.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<div id="palette-filter" class="palette-filter">
|
||||
<label class="palette-filter-label" for="palette-filter-textfield">
|
||||
<input type="search" id="palette-filter-textfield" class="palette-filter-textfield" [(ngModel)]="filterText" (focus)="onFocus()" [placeholder]="searchFilterPlaceHolder"/>
|
||||
</label>
|
||||
</div>
|
||||
<div id="palette-paper-container">
|
||||
<div id="palette-paper" class="palette-paper" style="overflow:hidden;"></div>
|
||||
</div>
|
||||
|
||||
67
projects/flo/palette/palette.component.scss
Normal file
67
projects/flo/palette/palette.component.scss
Normal file
@@ -0,0 +1,67 @@
|
||||
/* Joint JS paper for drawing palette -> canvas DnD visual feedback START */
|
||||
|
||||
#palette-floater {
|
||||
/* TODO size relative to paper that goes on it? */
|
||||
opacity: 0.75;
|
||||
background-color: transparent;
|
||||
/*
|
||||
background-color: #6db33f;
|
||||
*/
|
||||
float:left;
|
||||
position: absolute;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#palette-floater.joint-paper > svg {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
#palette-paper-container {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background-color: white;
|
||||
color: white;
|
||||
height:calc(100% - 46px);
|
||||
width:100%;
|
||||
}
|
||||
|
||||
/* Joint JS paper for drawing palette -> canvas DnD visual feedback END */
|
||||
|
||||
/* Palette START */
|
||||
|
||||
.palette-filter {
|
||||
border: 3px solid #6db33f;
|
||||
font-size:24px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.palette-filter-textfield {
|
||||
width: 100%;
|
||||
border: none;
|
||||
/* border: 3px solid #6db33f;
|
||||
*/ font-family: "Varela Round",sans-serif;
|
||||
/* padding: 2px; */
|
||||
}
|
||||
|
||||
.palette-paper {
|
||||
background-color: #eeeeee;
|
||||
/*
|
||||
border-right: 7px solid;
|
||||
*/
|
||||
border-color: #6db33f;
|
||||
/* width: 170px;
|
||||
height:100%;
|
||||
float: left;
|
||||
*/
|
||||
}
|
||||
|
||||
.no-matches-label {
|
||||
fill: #777;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Palette END */
|
||||
676
projects/flo/palette/palette.component.ts
Normal file
676
projects/flo/palette/palette.component.ts
Normal file
@@ -0,0 +1,676 @@
|
||||
import {Component, ElementRef, Input, Output, EventEmitter, OnInit, OnDestroy, Inject, ViewEncapsulation} from '@angular/core';
|
||||
import { Subject, fromEvent } from 'rxjs';
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
import { dia } from 'jointjs';
|
||||
import { Flo } from '../shared/flo-common';
|
||||
import '../shared/shapes';
|
||||
import { Shapes, Constants } from '../shared/shapes';
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import $ from 'jquery';
|
||||
import {Logger} from '../shared/logger';
|
||||
const joint: any = Flo.joint;
|
||||
|
||||
const DEBOUNCE_TIME = 300;
|
||||
|
||||
@Component({
|
||||
selector: 'flo-palette',
|
||||
templateUrl: './palette.component.html',
|
||||
styleUrls: ['./palette.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class Palette implements OnInit, OnDestroy {
|
||||
|
||||
private _metamodelListener: Flo.MetamodelListener = {
|
||||
metadataError: (data) => {},
|
||||
metadataAboutToChange: () => {},
|
||||
metadataChanged: () => {
|
||||
if (this.initialized && this.metamodel) {
|
||||
this.metamodel.load().then(metamodel => this.buildPalette(metamodel));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The names of any groups in the palette that have been deliberately closed (the arrow clicked on)
|
||||
*/
|
||||
private closedGroups: Set<string>;
|
||||
|
||||
/**
|
||||
* Model of the clicked element
|
||||
*/
|
||||
private clickedElement: dia.Cell | undefined;
|
||||
|
||||
private viewBeingDragged: dia.CellView | undefined;
|
||||
|
||||
private initialized = false;
|
||||
|
||||
private _paletteSize: number;
|
||||
|
||||
private _filterText = '';
|
||||
|
||||
private paletteGraph: dia.Graph;
|
||||
|
||||
private palette: dia.Paper;
|
||||
|
||||
private floaterpaper: dia.Paper;
|
||||
|
||||
private filterTextModel = new Subject<string>();
|
||||
|
||||
private noMacthesFoundNode: dia.Cell;
|
||||
|
||||
@Input()
|
||||
metamodel: Flo.Metamodel;
|
||||
|
||||
@Input()
|
||||
renderer: Flo.Renderer;
|
||||
|
||||
@Input()
|
||||
paletteEntryPadding: dia.Size = {width: 12, height: 12};
|
||||
|
||||
@Input()
|
||||
searchFilterPlaceHolder = 'Search...';
|
||||
|
||||
@Output()
|
||||
onPaletteEntryDrop = new EventEmitter<Flo.DnDEvent>();
|
||||
|
||||
@Output()
|
||||
paletteReady = new EventEmitter<boolean>();
|
||||
|
||||
@Output()
|
||||
paletteFocus = new EventEmitter<void>();
|
||||
|
||||
private mouseMoveHanlder = (e: any) => this.handleDrag(e);
|
||||
private mouseUpHanlder = (e: any) => this.handleMouseUp(e);
|
||||
|
||||
@Input()
|
||||
set paletteSize(size: number) {
|
||||
Logger.debug('Palette Size: ' + size);
|
||||
if (this._paletteSize !== size) {
|
||||
this._paletteSize = size;
|
||||
if (this.palette) {
|
||||
this.layout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(private element: ElementRef, @Inject(DOCUMENT) private document: any) {
|
||||
this.paletteGraph = new joint.dia.Graph();
|
||||
this.paletteGraph.set('type', Constants.PALETTE_CONTEXT);
|
||||
this._filterText = '';
|
||||
|
||||
this.closedGroups = new Set<string>();
|
||||
}
|
||||
|
||||
onFocus(): void {
|
||||
this.paletteFocus.emit();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
let element = $('#palette-paper', this.element.nativeElement);
|
||||
// Create the paper for the palette using the specified element view
|
||||
this.palette = new joint.dia.Paper({
|
||||
el: element,
|
||||
gridSize: 1,
|
||||
model: this.paletteGraph,
|
||||
height: $(this.element.nativeElement.parentNode).height(),
|
||||
width: $(this.element.nativeElement.parentNode).width(),
|
||||
elementView: this.getPaletteView(this.renderer && this.renderer.getNodeView ? this.renderer.getNodeView() : joint.dia.ElementView),
|
||||
interactive: false
|
||||
});
|
||||
|
||||
this.palette.on('cell:pointerup', (cellview: dia.CellView, evt: any) => {
|
||||
if (this.viewBeingDragged) {
|
||||
this.trigger({
|
||||
type: Flo.DnDEventType.DROP,
|
||||
view: this.viewBeingDragged,
|
||||
event: evt
|
||||
});
|
||||
this.viewBeingDragged = undefined;
|
||||
}
|
||||
this.clickedElement = undefined;
|
||||
$('#palette-floater').remove();
|
||||
if (this.floaterpaper) {
|
||||
this.floaterpaper.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle the header open/closed on a click
|
||||
this.palette.on('cell:pointerclick', (cellview: dia.CellView, event: any) => {
|
||||
// TODO [design][palette] should the user need to click on the arrow rather than anywhere on the header?
|
||||
// Click position within the element would be: evt.offsetX, evt.offsetY
|
||||
const cell: dia.Cell = cellview.model;
|
||||
if (cell.attributes.header) {
|
||||
// Toggle the header open/closed
|
||||
// if (cell.get('isOpen')) {
|
||||
// this.rotateClosed(cell);
|
||||
// } else {
|
||||
// this.rotateOpen(cell);
|
||||
// }
|
||||
}
|
||||
// TODO [palette] ensure other mouse handling events do nothing for headers
|
||||
// TODO [palette] move 'metadata' field to the right place (not inside attrs I think)
|
||||
});
|
||||
|
||||
$(this.document).on('mouseup', this.mouseUpHanlder);
|
||||
|
||||
if (this.metamodel) {
|
||||
this.metamodel.load().then(data => {
|
||||
this.buildPalette(data);
|
||||
|
||||
// Add listener to metamodel
|
||||
if (this.metamodel && this.metamodel.subscribe) {
|
||||
this.metamodel.subscribe(this._metamodelListener);
|
||||
}
|
||||
|
||||
// Add debounced listener to filter text changes
|
||||
this.filterTextModel
|
||||
.pipe(debounceTime(DEBOUNCE_TIME))
|
||||
.subscribe((value) => this.layout());
|
||||
|
||||
this.initialized = true;
|
||||
});
|
||||
} else {
|
||||
Logger.error('No Metamodel service specified for palette!');
|
||||
}
|
||||
|
||||
this._paletteSize = this._paletteSize || $(this.element.nativeElement.parentNode).width();
|
||||
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.metamodel && this.metamodel.unsubscribe) {
|
||||
this.metamodel.unsubscribe(this._metamodelListener);
|
||||
}
|
||||
$(this.document).off('mouseup', this.mouseUpHanlder);
|
||||
this.palette.remove();
|
||||
}
|
||||
|
||||
private createPaletteGroup(title: string, isOpen: boolean): dia.Element {
|
||||
const paletteRenderer: Flo.PaletteRenderer = this.renderer && this.renderer.getPaletteRenderer ? this.renderer.getPaletteRenderer() : {
|
||||
createGroupHeader: (titleStr: string, isOpenParam: boolean) => {
|
||||
const header = new joint.shapes.flo.PaletteGroupHeader({attrs: {text: {text: titleStr}}});
|
||||
if (!isOpenParam) {
|
||||
header.attr({'path': {'transform': 'rotate(-90,15,13)'}});
|
||||
}
|
||||
return header;
|
||||
},
|
||||
onClose: (groupView: dia.CellView) => this.rotateClosed(groupView.model),
|
||||
onOpen: (groupView: dia.CellView) => this.rotateOpen(groupView.model)
|
||||
};
|
||||
let newGroupHeader = paletteRenderer.createGroupHeader(title, isOpen);
|
||||
if (!isOpen) {
|
||||
newGroupHeader.set('isOpen', false);
|
||||
}
|
||||
newGroupHeader.set('header', title);
|
||||
|
||||
this.paletteGraph.addCell(newGroupHeader);
|
||||
|
||||
const view = this.palette.findViewByModel(newGroupHeader);
|
||||
|
||||
view.on('cell:pointerclick', () => {
|
||||
if (newGroupHeader.get('isOpen')) {
|
||||
if (typeof paletteRenderer.onClose === 'function') {
|
||||
paletteRenderer.onClose(view).then(() => {
|
||||
newGroupHeader.set('isOpen', false);
|
||||
this.closedGroups.add(newGroupHeader.get('header'));
|
||||
this.layout();
|
||||
});
|
||||
} else {
|
||||
newGroupHeader.set('isOpen', false);
|
||||
this.closedGroups.add(newGroupHeader.get('header'));
|
||||
this.layout();
|
||||
}
|
||||
} else {
|
||||
if (typeof paletteRenderer.onOpen === 'function') {
|
||||
paletteRenderer.onOpen(view).then(() => {
|
||||
newGroupHeader.set('isOpen', true);
|
||||
this.closedGroups.delete(newGroupHeader.get('header'));
|
||||
this.layout();
|
||||
});
|
||||
} else {
|
||||
newGroupHeader.set('isOpen', true);
|
||||
this.closedGroups.delete(newGroupHeader.get('header'));
|
||||
this.layout();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return newGroupHeader;
|
||||
}
|
||||
|
||||
private createPaletteEntry(title: string, metadata: Flo.ElementMetadata) {
|
||||
return Shapes.Factory.createNode({
|
||||
renderer: this.renderer,
|
||||
paper: this.palette,
|
||||
metadata: metadata
|
||||
});
|
||||
}
|
||||
|
||||
private buildPalette(metamodel: Map<string, Map<string, Flo.ElementMetadata>>) {
|
||||
let startTime: number = new Date().getTime();
|
||||
|
||||
this.paletteReady.emit(false);
|
||||
this.paletteGraph.clear();
|
||||
|
||||
let groupAdded: Set<string> = new Set<string>();
|
||||
|
||||
let parentWidth: number = this._paletteSize - Flo.SCROLLBAR_WIDTH;
|
||||
Logger.debug(`Parent Width: ${parentWidth}`);
|
||||
|
||||
// The field closedGroups tells us which should not be shown
|
||||
// Work out the list of active groups/nodes based on the filter text
|
||||
this.metamodel.groups().forEach(group => {
|
||||
if (metamodel && metamodel.has(group)) {
|
||||
Array.from(metamodel.get(group)!.keys()).sort().forEach(name => {
|
||||
let node = metamodel.get(group)!.get(name);
|
||||
if (node) {
|
||||
if (!groupAdded.has(group)) {
|
||||
this.createPaletteGroup(group, !this.closedGroups.has(group));
|
||||
groupAdded.add(group);
|
||||
}
|
||||
if (!(node.metadata && node.metadata.noPaletteEntry)) {
|
||||
this.createPaletteEntry(name, node);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.noMacthesFoundNode = new joint.shapes.flo.NoMatchesFound();
|
||||
this.palette.model.addCell(this.noMacthesFoundNode);
|
||||
|
||||
this.layout();
|
||||
|
||||
this.paletteReady.emit(true);
|
||||
Logger.debug('buildPalette took ' + (new Date().getTime() - startTime) + 'ms');
|
||||
}
|
||||
|
||||
private layout() {
|
||||
let startTime: number = new Date().getTime();
|
||||
|
||||
let filterText = this.filterText;
|
||||
if (filterText) {
|
||||
filterText = filterText.toLowerCase();
|
||||
}
|
||||
|
||||
let paletteNodes: Array<dia.Cell> = [];
|
||||
|
||||
let parentWidth: number = this._paletteSize - Flo.SCROLLBAR_WIDTH;
|
||||
Logger.debug(`Parent Width: ${parentWidth}`);
|
||||
|
||||
const presentGroups = new Set<string>();
|
||||
|
||||
this.palette.model.getCells().forEach((cell: dia.Cell) => {
|
||||
const metadata: Flo.ElementMetadata = cell.get('metadata');
|
||||
if (cell.get('header')) {
|
||||
paletteNodes.push(cell);
|
||||
} else if (metadata && metadata.group && metadata.name
|
||||
&& (!filterText
|
||||
|| metadata.group.toLocaleLowerCase().indexOf(filterText) >= 0
|
||||
|| metadata.name.toLocaleLowerCase().indexOf(filterText) >= 0)
|
||||
) {
|
||||
if (!this.closedGroups.has(metadata.group)) {
|
||||
cell.attr('./display', 'block');
|
||||
cell.removeAttr('./display');
|
||||
paletteNodes.push(cell);
|
||||
} else {
|
||||
cell.attr('./display', 'none');
|
||||
}
|
||||
presentGroups.add(metadata.group);
|
||||
} else {
|
||||
if (cell === this.noMacthesFoundNode) {
|
||||
|
||||
} else {
|
||||
cell.attr('./display', 'none');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clean group headers
|
||||
const filteredGroupHeaders: dia.Cell[] = [];
|
||||
paletteNodes.forEach(cell => {
|
||||
if (cell.get('header')) {
|
||||
if (presentGroups.has(cell.get('header'))) {
|
||||
cell.attr('./display', 'block');
|
||||
cell.removeAttr('./display');
|
||||
filteredGroupHeaders.push(cell);
|
||||
} else {
|
||||
cell.attr('./display', 'none');
|
||||
}
|
||||
} else {
|
||||
filteredGroupHeaders.push(cell);
|
||||
}
|
||||
});
|
||||
|
||||
paletteNodes = filteredGroupHeaders;
|
||||
|
||||
// Check if last group is empty
|
||||
const previous = paletteNodes.length > 0 ? paletteNodes[paletteNodes.length - 1] : undefined;
|
||||
// If previous is a paletter header node as well then the previous header had no nodes under it and we can hide it and remove from paletteNodes aeeay
|
||||
if (previous && previous.get('header') && !this.closedGroups.has(previous.get('header'))) {
|
||||
paletteNodes.pop().attr('./display', 'none');
|
||||
}
|
||||
|
||||
let cellWidth = 0, cellHeight = 0;
|
||||
// Determine the size of the palette entry cell (width and height)
|
||||
paletteNodes.forEach(pnode => {
|
||||
if (pnode.get('metadata')?.name) {
|
||||
const elementSize = this.palette.findViewByModel(pnode).getBBox();
|
||||
let dimension: dia.Size = {
|
||||
width: elementSize.width,
|
||||
height: elementSize.height
|
||||
};
|
||||
if (cellWidth < dimension.width) {
|
||||
cellWidth = dimension.width;
|
||||
}
|
||||
if (cellHeight < dimension.height) {
|
||||
cellHeight = dimension.height;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Adjust the palette entry cell size with paddings.
|
||||
cellWidth += 2 * this.paletteEntryPadding.width;
|
||||
cellHeight += 2 * this.paletteEntryPadding.height;
|
||||
|
||||
// Align palette entries row to be at the center
|
||||
let startX: number = parentWidth >= cellWidth ? (parentWidth - Math.floor(parentWidth / cellWidth) * cellWidth) / 2 : 0;
|
||||
let xpos = startX;
|
||||
let ypos = 0;
|
||||
let prevNode: dia.Cell;
|
||||
|
||||
// Layout palette entry nodes
|
||||
paletteNodes.forEach(pnode => {
|
||||
const elementSize = this.palette.findViewByModel(pnode).getBBox();
|
||||
let dimension: dia.Size = {
|
||||
width: elementSize.width,
|
||||
height: elementSize.height
|
||||
};
|
||||
if (pnode.get('header')) { //attributes.attrs.header) {
|
||||
// Palette entry header
|
||||
xpos = startX;
|
||||
if (ypos) {
|
||||
ypos += this.paletteEntryPadding.height;
|
||||
}
|
||||
pnode.set('size', {width: parentWidth, height: pnode.get('size').height || 30});
|
||||
pnode.set('position', {x: 0, y: ypos});
|
||||
ypos += dimension.height + this.paletteEntryPadding.height;
|
||||
} else {
|
||||
// Palette entry element
|
||||
if (xpos + cellWidth > parentWidth) {
|
||||
// Not enough real estate to place entry in a row - reset x position and leave the y pos which is next line
|
||||
xpos = startX;
|
||||
pnode.set('position', { x: xpos + (cellWidth - dimension.width) / 2, y: ypos + (cellHeight - dimension.height) / 2});
|
||||
} else {
|
||||
// Enough real estate to place entry in a row - adjust y position
|
||||
if (prevNode && prevNode.get('metadata')?.name) {
|
||||
ypos -= cellHeight;
|
||||
}
|
||||
pnode.set('position', { x: xpos + (cellWidth - dimension.width) / 2, y: ypos + (cellHeight - dimension.height) / 2});
|
||||
}
|
||||
// increment x position and y position (can be reorganized)
|
||||
xpos += cellWidth;
|
||||
ypos += cellHeight;
|
||||
}
|
||||
prevNode = pnode;
|
||||
});
|
||||
|
||||
this.noMacthesFoundNode.set('size', {width: parentWidth, height: this.noMacthesFoundNode.get('size').height || 30});
|
||||
this.noMacthesFoundNode.set('position', {x: 0, y: 0});
|
||||
if (paletteNodes.length === 0 && filterText) {
|
||||
// There is a filter present but everything is filtered out
|
||||
// Show no matches found node
|
||||
this.noMacthesFoundNode.attr('./display', 'block');
|
||||
this.noMacthesFoundNode.removeAttr('./display');
|
||||
ypos = this.noMacthesFoundNode.get('size').height;
|
||||
} else {
|
||||
// Hide no matches node in all other cases
|
||||
this.noMacthesFoundNode.attr('./display', 'none');
|
||||
}
|
||||
|
||||
this.palette.setDimensions(parentWidth, ypos);
|
||||
Logger.debug('buildPalette layout ' + (new Date().getTime() - startTime) + 'ms');
|
||||
|
||||
}
|
||||
|
||||
set filterText(text: string) {
|
||||
if (this._filterText !== text) {
|
||||
this._filterText = text;
|
||||
this.filterTextModel.next(text);
|
||||
}
|
||||
}
|
||||
|
||||
get filterText(): string {
|
||||
return this._filterText;
|
||||
}
|
||||
|
||||
private getPaletteView(view: any): dia.Element {
|
||||
let self: Palette = this;
|
||||
return view.extend({
|
||||
pointerdown: function(/*evt, x, y*/) {
|
||||
// Remove the tooltip
|
||||
// $('.node-tooltip').remove();
|
||||
// TODO move metadata to the right place (not inside attrs I think)
|
||||
self.clickedElement = this.model;
|
||||
if (self.clickedElement && self.clickedElement.get('metadata')) {
|
||||
$(self.document).on('mousemove', self.mouseMoveHanlder);
|
||||
}
|
||||
},
|
||||
pointermove: function(/*evt, x, y*/) {
|
||||
// Nothing to prevent move within the palette canvas
|
||||
},
|
||||
// events: {
|
||||
// // Tooltips on the palette elements
|
||||
// 'mouseenter': function(evt) {
|
||||
//
|
||||
// // Ignore 'mouseenter' if any other buttons are pressed
|
||||
// if (evt.buttons) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// var model = this.model;
|
||||
// var metadata = model.get('metadata');
|
||||
// if (!metadata) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// this.showTooltip(evt.pageX, evt.pageY);
|
||||
// },
|
||||
// // TODO bug here - if the call to get the info takes a while, the tooltip may appear after the pointer has left the cell
|
||||
// 'mouseleave': function(/*evt, x,y*/) {
|
||||
// this.hideTooltip();
|
||||
// },
|
||||
// 'mousemove': function(evt) {
|
||||
// this.moveTooltip(evt.pageX, evt.pageY);
|
||||
// }
|
||||
// },
|
||||
|
||||
// showTooltip: function(x, y) {
|
||||
// var model = this.model;
|
||||
// var metadata = model.get('metadata');
|
||||
// // TODO refactor to use tooltip module
|
||||
// var nodeTooltip = document.createElement('div');
|
||||
// $(nodeTooltip).addClass('node-tooltip');
|
||||
// $(nodeTooltip).appendTo($('body')).fadeIn('fast');
|
||||
// var nodeDescription = document.createElement('div');
|
||||
// $(nodeTooltip).addClass('tooltip-description');
|
||||
// $(nodeTooltip).append(nodeDescription);
|
||||
//
|
||||
// metadata.get('description').then(function(description) {
|
||||
// $(nodeDescription).text(description ? description: model.attr('metadata/name'));
|
||||
// }, function() {
|
||||
// $(nodeDescription).text(model.attr('metadata/name'));
|
||||
// });
|
||||
//
|
||||
// if (!metadata.metadata || !metadata.metadata['hide-tooltip-options']) {
|
||||
// metadata.get('properties').then(function(metaProps) {
|
||||
// if (metaProps) {
|
||||
// Object.keys(metaProps).sort().forEach(function(propertyName) {
|
||||
// var optionRow = document.createElement('div');
|
||||
// var optionName = document.createElement('span');
|
||||
// var optionDescription = document.createElement('span');
|
||||
// $(optionName).addClass('node-tooltip-option-name');
|
||||
// $(optionDescription).addClass('node-tooltip-option-description');
|
||||
// $(optionName).text(metaProps[propertyName].name);
|
||||
// $(optionDescription).text(metaProps[propertyName].description);
|
||||
// $(optionRow).append(optionName);
|
||||
// $(optionRow).append(optionDescription);
|
||||
// $(nodeTooltip).append(optionRow);
|
||||
// });
|
||||
// }
|
||||
// }, function(error) {
|
||||
// if (error) {
|
||||
// $log.error(error);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// var mousex = x + 10;
|
||||
// var mousey = y + 10;
|
||||
// $('.node-tooltip').css({ top: mousey, left: mousex });
|
||||
// },
|
||||
//
|
||||
// hideTooltip: function() {
|
||||
// $('.node-tooltip').remove();
|
||||
// },
|
||||
//
|
||||
// moveTooltip: function(x, y) {
|
||||
// var mousex = x + 10; // Get X coordinates
|
||||
// var mousey = y + 10; // Get Y coordinates
|
||||
// $('.node-tooltip').css({ top: mousey, left: mousex });
|
||||
// }
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private handleMouseUp(event: any) {
|
||||
$(this.document).off('mousemove', this.mouseMoveHanlder);
|
||||
}
|
||||
|
||||
private trigger(event: Flo.DnDEvent) {
|
||||
this.onPaletteEntryDrop.emit(event);
|
||||
}
|
||||
|
||||
private handleDrag(event: any) {
|
||||
// TODO offsetX/Y not on firefox
|
||||
// Logger.debug("tracking move: x="+event.pageX+",y="+event.pageY);
|
||||
// Logger.debug('Element = ' + (this.clickedElement ? this.clickedElement.attr('metadata/name'): 'null'));
|
||||
if (this.clickedElement && this.clickedElement.get('metadata')) {
|
||||
if (!this.viewBeingDragged) {
|
||||
|
||||
let dataOfClickedElement: Flo.ElementMetadata = this.clickedElement.get('metadata');
|
||||
// custom div if not already built.
|
||||
$('<div>', {
|
||||
id: 'palette-floater'
|
||||
}).appendTo($('body'));
|
||||
|
||||
let floatergraph: dia.Graph = new joint.dia.Graph();
|
||||
floatergraph.set('type', Constants.FEEDBACK_CONTEXT);
|
||||
|
||||
const parent = $('#palette-floater');
|
||||
|
||||
this.floaterpaper = new joint.dia.Paper({
|
||||
el: $('#palette-floater'),
|
||||
elementView: this.renderer && this.renderer.getNodeView ? this.renderer.getNodeView() : joint.dia.ElementView,
|
||||
gridSize: 10,
|
||||
model: floatergraph,
|
||||
height: parent.height(),
|
||||
width: parent.width(),
|
||||
validateMagnet: () => false,
|
||||
validateConnection: () => false
|
||||
});
|
||||
|
||||
// TODO float thing needs to be bigger otherwise icon label is missing
|
||||
// Initiative drag and drop - create draggable element
|
||||
let floaternode: dia.Element = Shapes.Factory.createNode({
|
||||
'renderer': this.renderer,
|
||||
'paper': this.floaterpaper,
|
||||
'graph': floatergraph,
|
||||
'metadata': dataOfClickedElement
|
||||
});
|
||||
|
||||
// Only node view expected
|
||||
this.viewBeingDragged = this.floaterpaper.findViewByModel(floaternode);
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
if (this.viewBeingDragged) {
|
||||
const box: dia.BBox = (<dia.ElementView>this.viewBeingDragged).getBBox();
|
||||
const size: dia.Size = (<dia.ElementView>this.viewBeingDragged).model.size();
|
||||
// Account for ports. Especially on the left side. Box includes ports, size does not
|
||||
parent.css('width', box.width + box.width - size.width);
|
||||
parent.css('height', box.height + box.height - size.height);
|
||||
floaternode.position(box.width - size.width, box.height - size.height);
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(this.viewBeingDragged.el);
|
||||
|
||||
let box: dia.BBox = (<dia.ElementView>this.viewBeingDragged).getBBox();
|
||||
let size: dia.Size = floaternode.size();
|
||||
// Account for node real size including ports
|
||||
parent.css('width', box.width + box.width - size.width);
|
||||
parent.css('height', box.height + box.height - size.height);
|
||||
floaternode.position(box.width - size.width, box.height - size.height);
|
||||
parent.offset({left: event.pageX + 5, top: event.pageY + 5});
|
||||
} else {
|
||||
$('#palette-floater').offset({left: event.pageX + 5, top: event.pageY + 5});
|
||||
this.trigger({
|
||||
type: Flo.DnDEventType.DRAG,
|
||||
view: this.viewBeingDragged,
|
||||
event: event
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Modify the rotation of the arrow in the header from horizontal(closed) to vertical(open)
|
||||
*/
|
||||
private rotateOpen(element: dia.Cell): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => this.doRotateOpen(element, 90).then(() => {
|
||||
resolve();
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private doRotateOpen(element: dia.Cell, angle: number): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
angle -= 10;
|
||||
element.attr({'path': {'transform': 'rotate(-' + angle + ',15,13)'}});
|
||||
if (angle <= 0) {
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(() => this.doRotateOpen(element, angle).then(() => resolve()), 10);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private doRotateClose(element: dia.Cell, angle: number): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
angle += 10;
|
||||
element.attr({'path': {'transform': 'rotate(-' + angle + ',15,13)'}});
|
||||
if (angle >= 90) {
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(() => this.doRotateClose(element, angle).then(() => resolve()), 10);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO better name for this function as this does the animation *and* updates the palette
|
||||
|
||||
/*
|
||||
* Modify the rotation of the arrow in the header from vertical(open) to horizontal(closed)
|
||||
*/
|
||||
private rotateClosed(element: dia.Cell): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
this.doRotateClose(element, 0).then(() => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
47
projects/flo/properties/df.property.component.html
Normal file
47
projects/flo/properties/df.property.component.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<tr [formGroup]="form" class="df-property-row" [ngClass]="{'invalid-property-value': control.invalid}">
|
||||
|
||||
<td class="df-property-label-cell">
|
||||
<label [attr.for]="model.id" class="df-form-label">{{model.name}}</label>
|
||||
</td>
|
||||
|
||||
<td class="df-property-control-cell">
|
||||
<div [ngSwitch]="model.type" class="df-property-container">
|
||||
|
||||
<label *ngSwitchCase="types.CHECKBOX" class="df-property-control">
|
||||
<input type="checkbox" [id]="model.id" [(ngModel)]="model.value" [formControlName]="model.id">
|
||||
{{model.value ? 'True' : 'False' }}
|
||||
</label>
|
||||
|
||||
<input *ngSwitchCase="types.NUMBER" class="df-property-control" type="number" [id]="model.id"
|
||||
[formControlName]="model.id" [placeholder]="model.defaultValue || ''" [(ngModel)]="model.value">
|
||||
|
||||
<input *ngSwitchCase="types.PASSWORD" class="df-property-control" type="password" [id]="model.id"
|
||||
[formControlName]="model.id" [placeholder]="model.defaultValue || ''" [(ngModel)]="model.value">
|
||||
|
||||
<input *ngSwitchCase="types.EMAIL" class="df-property-control" type="password" [id]="model.id"
|
||||
[formControlName]="model.id" [placeholder]="model.defaultValue || ''" [(ngModel)]="model.value">
|
||||
|
||||
<input *ngSwitchCase="types.URL" class="df-property-control" type="url" [id]="model.id"
|
||||
[formControlName]="model.id" [placeholder]="model.defaultValue || ''" [(ngModel)]="model.value">
|
||||
|
||||
<select *ngSwitchCase="types.SELECT" class="df-property-control" [id]="model.id"
|
||||
[formControlName]="model.id" [(ngModel)]="model.value">
|
||||
<option *ngFor="let o of model['options']" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
|
||||
<code-editor *ngSwitchCase="types.CODE" class="df-property-control" [id]="model.id"
|
||||
[formControlName]="model.id" [language]="model['language']" [(ngModel)]="model.value" [line-numbers]="true"
|
||||
scrollbar-style="simple" [placeholder]="model.defaultValue || 'Enter code snippet...'" [overview-ruler]="true">
|
||||
</code-editor>
|
||||
|
||||
<input *ngSwitchDefault class="df-property-control" type="text" [id]="model.id" [formControlName]="model.id"
|
||||
[placeholder]="model.defaultValue || ''" [(ngModel)]="model.value">
|
||||
</div>
|
||||
<div class="help-block">
|
||||
<div>{{model.description}}</div>
|
||||
<div *ngFor="let e of errorData" class="validation-error-block">{{e.message}}</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
32
projects/flo/properties/df.property.component.ts
Normal file
32
projects/flo/properties/df.property.component.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Component, Input, ViewEncapsulation } from '@angular/core';
|
||||
import { FormGroup, AbstractControl } from '@angular/forms';
|
||||
import { Properties } from '../shared/flo-properties';
|
||||
|
||||
@Component({
|
||||
selector: 'df-property',
|
||||
templateUrl: './df.property.component.html',
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class DynamicFormPropertyComponent {
|
||||
|
||||
@Input()
|
||||
model: Properties.ControlModel<any>;
|
||||
|
||||
@Input() form: FormGroup;
|
||||
|
||||
constructor() {}
|
||||
|
||||
get types() {
|
||||
return Properties.InputType;
|
||||
}
|
||||
|
||||
get control(): AbstractControl {
|
||||
return this.form.controls[this.model.id];
|
||||
}
|
||||
|
||||
get errorData() {
|
||||
return (this.model.validation && this.model.validation.errorData ? this.model.validation.errorData : [])
|
||||
.filter(e => this.control.errors && this.control.errors[e.id]);
|
||||
}
|
||||
|
||||
}
|
||||
12
projects/flo/properties/properties.group.component.html
Normal file
12
projects/flo/properties/properties.group.component.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<div *ngIf="propertiesGroupModel && !propertiesGroupModel.isLoading" class="properties-group-container"
|
||||
[formGroup]="form">
|
||||
<div
|
||||
*ngIf="propertiesGroupModel.getControlsModels().length > 0 && controlModelsToDisplay.length === 0; else propertiesList"
|
||||
class="no-matches-label">No results found.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #propertiesList>
|
||||
<df-property *ngFor="let model of controlModelsToDisplay" [model]="model" [form]="form"
|
||||
class="property-row"></df-property>
|
||||
</ng-template>
|
||||
49
projects/flo/properties/properties.group.component.ts
Normal file
49
projects/flo/properties/properties.group.component.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { FormGroup, FormControl } from '@angular/forms';
|
||||
import { Properties } from '../shared/flo-properties';
|
||||
import PropertyFilter = Properties.PropertyFilter;
|
||||
|
||||
@Component({
|
||||
selector: 'properties-group',
|
||||
templateUrl: './properties.group.component.html',
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class PropertiesGroupComponent implements OnInit {
|
||||
|
||||
@Input()
|
||||
propertiesGroupModel: Properties.PropertiesGroupModel;
|
||||
|
||||
@Input()
|
||||
form: FormGroup;
|
||||
|
||||
@Input()
|
||||
filter: PropertyFilter;
|
||||
|
||||
ngOnInit() {
|
||||
if (this.propertiesGroupModel.isLoading) {
|
||||
let subscription = this.propertiesGroupModel.loadedSubject.subscribe(loaded => {
|
||||
if (loaded) {
|
||||
subscription.unsubscribe();
|
||||
this.createGroupControls();
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.createGroupControls();
|
||||
}
|
||||
}
|
||||
|
||||
createGroupControls() {
|
||||
this.propertiesGroupModel.getControlsModels().forEach(c => {
|
||||
if (c.validation) {
|
||||
this.form.addControl(c.id, new FormControl(c.value || '', c.validation.validator, c.validation.asyncValidator));
|
||||
} else {
|
||||
this.form.addControl(c.id, new FormControl(c.value || ''));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
get controlModelsToDisplay() {
|
||||
return this.propertiesGroupModel.getControlsModels().filter(c => !this.filter || this.filter.accept(c.property));
|
||||
}
|
||||
|
||||
}
|
||||
278
projects/flo/shared/flo-common.ts
Normal file
278
projects/flo/shared/flo-common.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
import { dia, g } from 'jointjs';
|
||||
import { Observable } from 'rxjs';
|
||||
import * as _joint from 'jointjs';
|
||||
import $ from 'jquery';
|
||||
|
||||
export namespace Flo {
|
||||
|
||||
export const joint: any = _joint;
|
||||
|
||||
export enum DnDEventType {
|
||||
DRAG,
|
||||
DROP
|
||||
}
|
||||
|
||||
export interface DnDEvent {
|
||||
type: DnDEventType;
|
||||
view: dia.CellView;
|
||||
event: MouseEvent;
|
||||
}
|
||||
|
||||
export interface PropertyMetadata {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly description?: string;
|
||||
readonly defaultValue?: any;
|
||||
readonly type?: string;
|
||||
readonly [propName: string]: any;
|
||||
}
|
||||
|
||||
export interface ExtraMetadata {
|
||||
readonly titleProperty?: string;
|
||||
readonly noEditableProps?: boolean;
|
||||
readonly noPaletteEntry?: boolean;
|
||||
readonly unselectable?: boolean;
|
||||
readonly [propName: string]: any;
|
||||
|
||||
readonly allowAdditionalProperties?: boolean; //TODO: Verify it is still needed
|
||||
}
|
||||
|
||||
export interface ElementMetadata {
|
||||
readonly name: string;
|
||||
readonly group: string;
|
||||
readonly metadata?: ExtraMetadata;
|
||||
readonly [propName: string]: any;
|
||||
description?(): Promise<string>;
|
||||
get(property: String): Promise<PropertyMetadata>;
|
||||
properties(): Promise<Map<string, PropertyMetadata>>;
|
||||
}
|
||||
|
||||
export interface ViewerDescriptor {
|
||||
readonly graph?: dia.Graph;
|
||||
readonly paper?: dia.Paper;
|
||||
}
|
||||
|
||||
export interface MetamodelListener {
|
||||
metadataError(data: any): void;
|
||||
metadataAboutToChange(): void;
|
||||
metadataChanged(): void;
|
||||
}
|
||||
|
||||
export interface Metamodel {
|
||||
textToGraph(flo: EditorContext, dsl: string): Promise<any>;
|
||||
graphToText(flo: EditorContext): Promise<string>;
|
||||
load(): Promise<Map<string, Map<string, ElementMetadata>>>;
|
||||
groups(): Array<string>;
|
||||
|
||||
refresh?(): Promise<Map<string, Map<string, ElementMetadata>>>;
|
||||
subscribe?(listener: MetamodelListener): void;
|
||||
unsubscribe?(listener: MetamodelListener): void;
|
||||
isValidPropertyValue?(element: dia.Element, property: string, value: any): boolean;
|
||||
}
|
||||
|
||||
export interface CreationParams {
|
||||
metadata?: ElementMetadata;
|
||||
props?: Map<string, any>;
|
||||
}
|
||||
|
||||
export interface ElementCreationParams extends CreationParams {
|
||||
position?: dia.Point;
|
||||
}
|
||||
|
||||
export interface LinkCreationParams extends CreationParams {
|
||||
source: string;
|
||||
target: string;
|
||||
}
|
||||
|
||||
export interface EmbeddedChildCreationParams extends CreationParams {
|
||||
parent: dia.Cell;
|
||||
position?: dia.Point;
|
||||
}
|
||||
|
||||
export interface DecorationCreationParams extends EmbeddedChildCreationParams {
|
||||
kind: string;
|
||||
messages: Array<string>;
|
||||
}
|
||||
|
||||
export interface HandleCreationParams extends EmbeddedChildCreationParams {
|
||||
kind: string;
|
||||
}
|
||||
|
||||
export interface PaletteRenderer {
|
||||
createGroupHeader(title: string, isOpen: boolean): dia.Element;
|
||||
onOpen?(groupView: dia.CellView): Promise<any>;
|
||||
onClose?(groupView: dia.CellView): Promise<any>;
|
||||
}
|
||||
|
||||
export interface Renderer {
|
||||
createNode?(viewerDescriptor: ViewerDescriptor, metadata: ElementMetadata, props?: Map<string, any>): dia.Element;
|
||||
createLink?(source: LinkEnd, target: LinkEnd, metadata?: ElementMetadata, props?: Map<string, any>): dia.Link;
|
||||
createHandle?(kind: string, parent: dia.Cell): dia.Element;
|
||||
createDecoration?(kind: string, parent: dia.Cell): dia.Element;
|
||||
getPaletteRenderer?(): PaletteRenderer;
|
||||
initializeNewNode?(node: dia.Element, viewerDescriptor: ViewerDescriptor): void;
|
||||
initializeNewLink?(link: dia.Link, viewerDescriptor: ViewerDescriptor): void;
|
||||
initializeNewHandle?(handle: dia.Element, viewerDescriptor: ViewerDescriptor): void;
|
||||
initializeNewDecoration?(decoration: dia.Element, viewerDescriptor: ViewerDescriptor): void;
|
||||
getNodeView?(): dia.ElementView;
|
||||
getLinkView?(): dia.LinkView;
|
||||
layout?(paper: dia.Paper): Promise<any>;
|
||||
handleLinkEvent?(context: EditorContext, event: string, link: dia.Link): void;
|
||||
isSemanticProperty?(propertyPath: string, element: dia.Cell): boolean;
|
||||
refreshVisuals?(cell: dia.Cell, propertyPath: string, paper: dia.Paper): void;
|
||||
getLinkAnchorPoint?(linkView: dia.LinkView, view: dia.ElementView, port: SVGElement, reference: dia.Point): dia.Point;
|
||||
markersChanged?(cell: dia.Cell, paper: dia.Paper): void;
|
||||
}
|
||||
|
||||
export interface EditorContext {
|
||||
readonly textToGraphConversionObservable: Observable<void>;
|
||||
readonly graphToTextConversionObservable: Observable<void>;
|
||||
readonly paletteReady: Observable<boolean>;
|
||||
zoomPercent: number;
|
||||
gridSize: number;
|
||||
readOnlyCanvas: boolean;
|
||||
selection: dia.CellView;
|
||||
graphToTextSync: boolean;
|
||||
noPalette: boolean;
|
||||
setDsl(dsl: string): void;
|
||||
updateGraph(): Promise<any>;
|
||||
updateText(): Promise<any>;
|
||||
performLayout(): Promise<void>;
|
||||
clearGraph(): Promise<void>;
|
||||
getGraph(): dia.Graph;
|
||||
getPaper(): dia.Paper;
|
||||
getMinZoom(): number;
|
||||
getMaxZoom(): number;
|
||||
getZoomStep(): number;
|
||||
fitToPage(): void;
|
||||
createNode(metadata: ElementMetadata, props?: Map<string, any>, position?: dia.Point): dia.Element;
|
||||
createLink(source: LinkEnd, target: LinkEnd, metadata?: ElementMetadata, props?: Map<string, any>): dia.Link;
|
||||
deleteSelectedNode(): void;
|
||||
delete(cell: dia.Cell): void;
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
export interface LinkEndDescriptor {
|
||||
view: dia.CellView;
|
||||
cssClassSelector?: string;
|
||||
}
|
||||
|
||||
export interface DnDDescriptor {
|
||||
sourceComponent?: string;
|
||||
range?: number;
|
||||
source?: LinkEndDescriptor;
|
||||
target?: LinkEndDescriptor;
|
||||
}
|
||||
|
||||
export interface LinkEnd {
|
||||
id: string | number;
|
||||
selector?: string;
|
||||
port?: string;
|
||||
magnet?: string;
|
||||
}
|
||||
|
||||
export enum Severity {
|
||||
Error,
|
||||
Warning
|
||||
}
|
||||
|
||||
export interface Marker {
|
||||
severity: Severity;
|
||||
message: string;
|
||||
range?: Range;
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
ch: number;
|
||||
line: number;
|
||||
}
|
||||
|
||||
export interface Range {
|
||||
start: Position;
|
||||
end: Position;
|
||||
}
|
||||
|
||||
export interface Editor {
|
||||
interactive?: ((cellView: dia.CellView, event: string) => boolean) | boolean | dia.CellView.InteractivityOptions;
|
||||
allowLinkVertexEdit?: boolean;
|
||||
highlighting?: any;
|
||||
createHandles?(context: EditorContext, createHandle: (owner: dia.CellView, kind: string, action: () => void, location: dia.Point) => void, owner: dia.CellView): void;
|
||||
validatePort?(context: EditorContext, view: dia.CellView, magnet: SVGElement): boolean;
|
||||
validateLink?(context: EditorContext, cellViewS: dia.CellView, portS: SVGElement, cellViewT: dia.CellView, portT: SVGElement, isSource: boolean, linkView: dia.LinkView): boolean;
|
||||
calculateDragDescriptor?(context: EditorContext, draggedView: dia.CellView, targetUnderMouse: dia.CellView, coordinate: g.Point, sourceComponent: string): DnDDescriptor;
|
||||
handleNodeDropping?(context: EditorContext, dragDescriptor: DnDDescriptor): void;
|
||||
showDragFeedback?(context: EditorContext, dragDescriptor: DnDDescriptor): void;
|
||||
hideDragFeedback?(context: EditorContext, dragDescriptor: DnDDescriptor): void;
|
||||
validate?(graph: dia.Graph, dsl: string, flo: EditorContext): Promise<Map<string | number, Array<Marker>>>;
|
||||
preDelete?(context: EditorContext, deletedElement: dia.Cell): boolean;
|
||||
setDefaultContent?(editorContext: EditorContext, data: Map<string, Map<string, ElementMetadata>>): void;
|
||||
}
|
||||
|
||||
export function findMagnetByClass(view: dia.CellView, className: string): SVGElement | undefined {
|
||||
if (className && className.startsWith('.')) {
|
||||
className = className.substr(1);
|
||||
}
|
||||
const element = view.$('[magnet]').toArray().find((magnet: any) => magnet.getAttribute('class').split(/\s+/).indexOf(className) >= 0);
|
||||
if (element) {
|
||||
return view.findMagnet($(element));
|
||||
}
|
||||
}
|
||||
|
||||
export function findMagnetByPort(view: dia.CellView, port: string): SVGElement | undefined {
|
||||
const element = view.$('[magnet]').toArray().find((magnet: HTMLElement) => magnet.getAttribute('port') === port);
|
||||
if (element) {
|
||||
return view.findMagnet($(element));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the metadata for a particular palette entry in a particular group.
|
||||
* @param name - name of the palette entry
|
||||
* @param group - group in which the palette entry should exist (e.g. sinks)
|
||||
* @return
|
||||
*/
|
||||
export function getMetadata(metamodel: Map<string, Map<string, ElementMetadata>>, name: string, group: string): ElementMetadata | undefined {
|
||||
const groupObj = metamodel && group ? metamodel.get(group) : undefined;
|
||||
if (name && groupObj && groupObj.get(name)) {
|
||||
return metamodel!.get(group)!.get(name);
|
||||
} else {
|
||||
return {
|
||||
name: name,
|
||||
group: group,
|
||||
unresolved: true,
|
||||
get: (property: string) => new Promise<PropertyMetadata>(resolve => resolve(undefined)),
|
||||
properties: () => Promise.resolve(new Map<string, PropertyMetadata>())
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function getScrollbarWidth() {
|
||||
|
||||
// Creating invisible container
|
||||
const outer = document.createElement('div');
|
||||
outer.style.visibility = 'hidden';
|
||||
outer.style.overflow = 'scroll'; // forcing scrollbar to appear
|
||||
// outer.style.msOverflowStyle = 'scrollbar'; // needed for WinJS apps
|
||||
document.body.appendChild(outer);
|
||||
|
||||
// Creating inner element and placing it in the container
|
||||
const inner = document.createElement('div');
|
||||
outer.appendChild(inner);
|
||||
|
||||
// Calculating difference between container's full width and the child width
|
||||
const scrollbarWidth = (outer.offsetWidth - inner.offsetWidth);
|
||||
|
||||
// Removing temporary elements from the DOM
|
||||
outer.parentNode.removeChild(outer);
|
||||
|
||||
return scrollbarWidth;
|
||||
|
||||
}
|
||||
|
||||
export const SCROLLBAR_WIDTH = getScrollbarWidth();
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
390
projects/flo/shared/flo-properties.ts
Normal file
390
projects/flo/shared/flo-properties.ts
Normal file
@@ -0,0 +1,390 @@
|
||||
import { dia } from 'jointjs';
|
||||
import { ValidatorFn, AsyncValidatorFn, AbstractControl, ValidationErrors } from '@angular/forms'
|
||||
import { Flo } from './flo-common';
|
||||
import { Subject, Observable } from 'rxjs'
|
||||
import {debounceTime, switchMap} from 'rxjs/operators';
|
||||
|
||||
export namespace Properties {
|
||||
|
||||
export enum InputType {
|
||||
TEXT,
|
||||
NUMBER,
|
||||
SELECT,
|
||||
CHECKBOX,
|
||||
PASSWORD,
|
||||
EMAIL,
|
||||
URL,
|
||||
CODE
|
||||
}
|
||||
|
||||
export interface Property {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly type?: string;
|
||||
readonly description?: string;
|
||||
readonly defaultValue?: any;
|
||||
value?: any;
|
||||
readonly valueOptions?: any[]
|
||||
readonly [propName: string]: any;
|
||||
}
|
||||
|
||||
export interface PropertyFilter {
|
||||
accept: (property: Property) => boolean;
|
||||
}
|
||||
|
||||
export interface SelectOption {
|
||||
name: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export interface ErrorData {
|
||||
id: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface Validation {
|
||||
validator?: ValidatorFn|ValidatorFn[]|null,
|
||||
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null,
|
||||
errorData?: Array<ErrorData>;
|
||||
}
|
||||
|
||||
export interface ControlModel<T> {
|
||||
readonly type: InputType;
|
||||
readonly id: string;
|
||||
value: T;
|
||||
readonly defaultValue: T;
|
||||
readonly name?: string;
|
||||
readonly description?: string;
|
||||
readonly property: Property
|
||||
readonly validation?: Validation;
|
||||
}
|
||||
|
||||
export interface CodeControlModel<T> extends ControlModel<T> {
|
||||
readonly language: string;
|
||||
}
|
||||
|
||||
export class GenericControlModel<T> implements ControlModel<T> {
|
||||
|
||||
constructor(private _property: Property, public type: InputType, public validation?: Validation) {}
|
||||
|
||||
get id() {
|
||||
return this.property.id;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.property.name;
|
||||
}
|
||||
|
||||
get description() {
|
||||
return this.property.description;
|
||||
}
|
||||
|
||||
get defaultValue() {
|
||||
return this.property.defaultValue;
|
||||
}
|
||||
|
||||
get value(): T {
|
||||
return this.getValue();
|
||||
}
|
||||
|
||||
set value(value: T) {
|
||||
this.setValue(value);
|
||||
}
|
||||
|
||||
get property(): Property {
|
||||
return this._property;
|
||||
}
|
||||
|
||||
protected setValue(value: T) {
|
||||
this.property.value = value;
|
||||
}
|
||||
|
||||
protected getValue(): T {
|
||||
return this.property.value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class CheckBoxControlModel extends GenericControlModel<boolean> {
|
||||
|
||||
constructor(_property: Property, validation?: Validation) {
|
||||
super(_property, InputType.CHECKBOX, validation);
|
||||
}
|
||||
|
||||
protected getValue() {
|
||||
const res = super.getValue();
|
||||
const type = typeof res;
|
||||
switch (type) {
|
||||
case 'boolean':
|
||||
return res;
|
||||
case 'string':
|
||||
switch ((<any>res).trim().toLowerCase()) {
|
||||
case 'true':
|
||||
case '1':
|
||||
return true;
|
||||
case 'false':
|
||||
case '0':
|
||||
return false;
|
||||
default:
|
||||
return this.property.defaultValue
|
||||
}
|
||||
case 'number':
|
||||
const num = <any> res;
|
||||
if (num === 0) {
|
||||
return false;
|
||||
} else if (num === 1) {
|
||||
return true;
|
||||
} else {
|
||||
return this.property.defaultValue
|
||||
}
|
||||
}
|
||||
return this.property.defaultValue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export abstract class AbstractCodeControlModel extends GenericControlModel<string> implements CodeControlModel<string> {
|
||||
|
||||
abstract language: string;
|
||||
|
||||
constructor(_property: Property, private encode?: (s: string) => string, private decode?: (s: string) => string, validation?: Validation) {
|
||||
super(_property, InputType.CODE, validation);
|
||||
}
|
||||
|
||||
set value(value: string) {
|
||||
if (value && this.encode) {
|
||||
super.setValue(this.encode(value));
|
||||
} else {
|
||||
super.setValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
get value(): string {
|
||||
let dsl = super.getValue();
|
||||
if (dsl && this.decode) {
|
||||
return this.decode(dsl);
|
||||
} else {
|
||||
return dsl;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export class GenericCodeControlModel extends AbstractCodeControlModel {
|
||||
|
||||
constructor(_property: Property, public language: string, encode?: (s: string) => string, decode?: (s: string) => string, validation?: Validation) {
|
||||
super(_property, encode, decode, validation);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class CodeControlModelWithDynamicLanguageProperty extends AbstractCodeControlModel {
|
||||
|
||||
private _langControlModel: Properties.ControlModel<any>;
|
||||
|
||||
constructor(_property: Properties.Property, private _languagePropertyName: string,
|
||||
private _groupModel: Properties.PropertiesGroupModel,
|
||||
encode?: (s: string) => string, decode?: (s: string) => string, validation?: Validation) {
|
||||
super(_property, encode, decode, validation);
|
||||
}
|
||||
|
||||
get language(): string {
|
||||
const value = this.languageControlModel.value;
|
||||
return value ? value : this.languageControlModel.defaultValue;
|
||||
}
|
||||
|
||||
get languageControlModel(): Properties.ControlModel<any> {
|
||||
if (!this._langControlModel) {
|
||||
// Cast to Properties.ControlModel<any> from Properties.ControlModel<any> | undefined
|
||||
// Should not be undefined!
|
||||
this._langControlModel = <Properties.ControlModel<any>> this._groupModel.getControlsModels().find(c => c.id === this._languagePropertyName);
|
||||
}
|
||||
return this._langControlModel;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class GenericListControlModel extends GenericControlModel<string> {
|
||||
|
||||
constructor(property: Property, validation?: Validation) {
|
||||
super(property, InputType.TEXT, validation);
|
||||
}
|
||||
|
||||
get value(): string {
|
||||
return this.property.value ? this.property.value.toString().trim().split(/\s*,\s*/).join(', ') : '';
|
||||
}
|
||||
|
||||
set value(value: string) {
|
||||
this.property.value = value && value.trim() ? value.split(/\s*,\s*/).join(',') : undefined;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class SelectControlModel extends GenericControlModel<any> {
|
||||
constructor(_property: Property, type: InputType, public options: Array<SelectOption>, validation?: Validation) {
|
||||
super(_property, type, validation);
|
||||
if (_property.defaultValue === undefined) {
|
||||
options.unshift({
|
||||
name: 'SELECT',
|
||||
value: _property.defaultValue
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface PropertiesSource {
|
||||
getProperties(): Promise<Property[]>;
|
||||
applyChanges(properties: Property[]): void;
|
||||
}
|
||||
|
||||
export class DefaultCellPropertiesSource implements PropertiesSource {
|
||||
|
||||
protected cell: dia.Cell;
|
||||
|
||||
constructor(cell: dia.Cell) {
|
||||
this.cell = cell;
|
||||
}
|
||||
|
||||
getProperties(): Promise<Array<Property>> {
|
||||
let metadata: Flo.ElementMetadata = this.cell.get('metadata');
|
||||
return Promise.resolve(metadata.properties().then(propsMetadata => Array.from(propsMetadata.values()).map(m => this.createProperty(m))));
|
||||
}
|
||||
|
||||
protected createProperty(metadata: Flo.PropertyMetadata): Property {
|
||||
return {
|
||||
id: metadata.id,
|
||||
name: metadata.name,
|
||||
type: metadata.type,
|
||||
defaultValue: metadata.defaultValue,
|
||||
attr: `props/${metadata.name}`,
|
||||
value: this.cell.attr(`props/${metadata.name}`),
|
||||
description: metadata.description,
|
||||
valueOptions: metadata.options
|
||||
}
|
||||
}
|
||||
|
||||
applyChanges(properties: Property[]) {
|
||||
this.cell.trigger('batch:start', { batchName: 'update properties' });
|
||||
|
||||
properties.forEach(property => {
|
||||
if ((typeof property.value === 'boolean' && !property.defaultValue && !property.value) ||
|
||||
(property.value === property.defaultValue || property.value === '' || property.value === undefined || property.value === null)) {
|
||||
let currentValue = this.cell.attr(property.attr);
|
||||
if (currentValue !== undefined && currentValue !== null) {
|
||||
// Remove attr doesn't fire appropriate event. Set default value first as a workaround to schedule DSL resync
|
||||
// this.cell.attr(property.attr, property.defaultValue === undefined ? null : property.defaultValue);
|
||||
this.cell.attr(property.attr, null);
|
||||
this.cell.removeAttr(property.attr);
|
||||
}
|
||||
} else {
|
||||
this.cell.attr(property.attr, property.value);
|
||||
}
|
||||
});
|
||||
|
||||
this.cell.trigger('batch:stop', { batchName: 'update properties' });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class PropertiesGroupModel {
|
||||
|
||||
protected propertiesSource: PropertiesSource;
|
||||
|
||||
protected controlModels: Array<ControlModel<any>>;
|
||||
|
||||
protected loading = true;
|
||||
|
||||
protected _loadedSubject: Subject<boolean>;
|
||||
|
||||
constructor(propertiesSource: PropertiesSource) {
|
||||
this.propertiesSource = propertiesSource;
|
||||
}
|
||||
|
||||
load() {
|
||||
this.loading = true;
|
||||
this._loadedSubject = new Subject<boolean>();
|
||||
this.propertiesSource.getProperties().then(properties => {
|
||||
this.controlModels = properties.map(p => this.createControlModel(p));
|
||||
this.loading = false;
|
||||
this._loadedSubject.next(true);
|
||||
this._loadedSubject.complete();
|
||||
});
|
||||
}
|
||||
|
||||
get isLoading(): boolean {
|
||||
return this.loading;
|
||||
}
|
||||
|
||||
get loadedSubject() {
|
||||
return this._loadedSubject;
|
||||
}
|
||||
|
||||
getControlsModels() {
|
||||
return this.controlModels;
|
||||
}
|
||||
|
||||
protected createControlModel(property: Property): ControlModel<any> {
|
||||
return new GenericControlModel(property, InputType.TEXT);
|
||||
}
|
||||
|
||||
public applyChanges(): void {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
let properties = this.controlModels.map(cm => cm.property);
|
||||
|
||||
this.propertiesSource.applyChanges(properties);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const UNIQUE_RESOURCE_ERROR = {uniqueResource: true};
|
||||
|
||||
export namespace Validators {
|
||||
|
||||
export function uniqueResource(service: (value: any) => Observable<boolean>, debounceDuration: number): AsyncValidatorFn {
|
||||
return (control: AbstractControl): Observable<ValidationErrors> => {
|
||||
return new Observable<ValidationErrors>(obs => {
|
||||
if (control.valueChanges) {
|
||||
return control.valueChanges.pipe(
|
||||
debounceTime(debounceDuration),
|
||||
switchMap(() => service(control.value)),
|
||||
).subscribe((res) => {
|
||||
if (res) {
|
||||
obs.next(undefined);
|
||||
} else {
|
||||
obs.next(UNIQUE_RESOURCE_ERROR);
|
||||
}
|
||||
obs.complete();
|
||||
}, () => {
|
||||
obs.next(UNIQUE_RESOURCE_ERROR);
|
||||
obs.complete();
|
||||
});
|
||||
} else {
|
||||
service(control.value).subscribe((res) => {
|
||||
if (res) {
|
||||
obs.next(undefined);
|
||||
} else {
|
||||
obs.next(UNIQUE_RESOURCE_ERROR);
|
||||
}
|
||||
obs.complete();
|
||||
}, () => {
|
||||
obs.next(UNIQUE_RESOURCE_ERROR);
|
||||
obs.complete();
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function noneOf(excluded: Array<any>): ValidatorFn {
|
||||
return (control: AbstractControl): {[key: string]: any} => {
|
||||
return excluded.find(e => e === control.value) ? {'noneOf': {value: control.value}} : {};
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
28
projects/flo/shared/logger.ts
Normal file
28
projects/flo/shared/logger.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { isDevMode } from '@angular/core';
|
||||
|
||||
export class Logger {
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
static log(value: any, ...rest: any[]) {
|
||||
if (isDevMode()) {
|
||||
console.log(value, ...rest);
|
||||
}
|
||||
}
|
||||
|
||||
static debug(value: any, ...rest: any[]) {
|
||||
if (isDevMode()) {
|
||||
console.debug(value, ...rest);
|
||||
}
|
||||
}
|
||||
|
||||
static error(value: any, ...rest: any[]) {
|
||||
console.error(value, ...rest);
|
||||
}
|
||||
|
||||
static warn(value: any, ...rest: any[]) {
|
||||
console.warn(value, ...rest);
|
||||
}
|
||||
|
||||
}
|
||||
805
projects/flo/shared/shapes.ts
Normal file
805
projects/flo/shared/shapes.ts
Normal file
@@ -0,0 +1,805 @@
|
||||
import { dia } from 'jointjs';
|
||||
import { Flo } from './flo-common';
|
||||
import template from 'lodash/template';
|
||||
import isFunction from 'lodash/isFunction';
|
||||
import $ from 'jquery';
|
||||
const joint: any = Flo.joint;
|
||||
|
||||
const isChrome: boolean = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
|
||||
const isFF: boolean = navigator.userAgent.indexOf('Firefox') > 0;
|
||||
|
||||
const IMAGE_W = 120;
|
||||
const IMAGE_H = 35;
|
||||
|
||||
const ERROR_MARKER_SIZE: dia.Size = {width: 16, height: 16};
|
||||
|
||||
const HANDLE_SIZE: dia.Size = {width: 10, height: 10};
|
||||
|
||||
const HANDLE_ICON_MAP: Map<string, string> = new Map<string, string>();
|
||||
const REMOVE = 'remove';
|
||||
HANDLE_ICON_MAP.set(REMOVE, 'icons/delete.svg');
|
||||
|
||||
const DECORATION_ICON_MAP: Map<string, string> = new Map<string, string>();
|
||||
const ERROR = 'error';
|
||||
DECORATION_ICON_MAP.set(ERROR, 'icons/error.svg');
|
||||
|
||||
export function loadShapes() {
|
||||
|
||||
joint.shapes.flo = {};
|
||||
|
||||
joint.shapes.flo.NODE_TYPE = 'sinspctr.IntNode';
|
||||
joint.shapes.flo.LINK_TYPE = 'sinspctr.Link';
|
||||
joint.shapes.flo.DECORATION_TYPE = 'decoration';
|
||||
joint.shapes.flo.HANDLE_TYPE = 'handle';
|
||||
|
||||
|
||||
// joint.util.cloneDeep = (obj: any) => {
|
||||
// return _.cloneDeepWith(obj, (o) => {
|
||||
// if (_.isObject(o) && !_.isPlainObject(o)) {
|
||||
// return o;
|
||||
// }
|
||||
// });
|
||||
// };
|
||||
|
||||
joint.util.filter.redscale = (args: Shapes.FilterOptions) => {
|
||||
|
||||
let amount = Number.isFinite(args.amount) ? args.amount : 1;
|
||||
|
||||
return template(
|
||||
'<filter><feColorMatrix type="matrix" values="${a} ${b} ${c} 0 ${d} ${e} ${f} ${g} 0 0 ${h} ${i} ${k} 0 0 0 0 0 1 0"/></filter>',
|
||||
<any>{
|
||||
a: 1 - 0.96 * amount,
|
||||
b: 0.95 * amount,
|
||||
c: 0.01 * amount,
|
||||
d: 0.3 * amount,
|
||||
e: 0.2 * amount,
|
||||
f: 1 - 0.9 * amount,
|
||||
g: 0.7 * amount,
|
||||
h: 0.05 * amount,
|
||||
i: 0.05 * amount,
|
||||
k: 1 - 0.1 * amount
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
joint.util.filter.orangescale = (args: Shapes.FilterOptions) => {
|
||||
|
||||
let amount = Number.isFinite(args.amount) ? args.amount : 1;
|
||||
|
||||
return template(
|
||||
'<filter><feColorMatrix type="matrix" values="${a} ${b} ${c} 0 ${d} ${e} ${f} ${g} 0 ${h} ${i} ${k} ${l} 0 0 0 0 0 1 0"/></filter>',
|
||||
<any>{
|
||||
a: 1.0 + 0.5 * amount,
|
||||
b: 1.4 * amount,
|
||||
c: 0.2 * amount,
|
||||
d: 0.3 * amount,
|
||||
e: 0.3 * amount,
|
||||
f: 1 + 0.05 * amount,
|
||||
g: 0.2 * amount,
|
||||
h: 0.15 * amount,
|
||||
i: 0.3 * amount,
|
||||
k: 0.3 * amount,
|
||||
l: 1 - 0.6 * amount
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
joint.shapes.flo.Node = joint.shapes.basic.Generic.extend({
|
||||
markup:
|
||||
'<g class="shape"><image class="image" /></g>' +
|
||||
'<rect class="border-white"/>' +
|
||||
'<rect class="border"/>' +
|
||||
'<rect class="box"/>' +
|
||||
'<text class="label"/>' +
|
||||
'<text class="label2"></text>' +
|
||||
'<rect class="input-port" />' +
|
||||
'<rect class="output-port"/>' +
|
||||
'<rect class="output-port-cover"/>',
|
||||
|
||||
defaults: joint.util.deepSupplement({
|
||||
|
||||
type: joint.shapes.flo.NODE_TYPE,
|
||||
position: {x: 0, y: 0},
|
||||
size: { width: IMAGE_W, height: IMAGE_H },
|
||||
attrs: {
|
||||
'.': { magnet: false },
|
||||
// rounded edges around image
|
||||
'.border': {
|
||||
refWidth: 1,
|
||||
refHeight: 1,
|
||||
rx: 3,
|
||||
ry: 3,
|
||||
'fill-opacity': 0, // see through
|
||||
stroke: '#eeeeee',
|
||||
'stroke-width': 0
|
||||
},
|
||||
|
||||
'.box': {
|
||||
refWidth: 1,
|
||||
refHeight: 1,
|
||||
rx: 3,
|
||||
ry: 3,
|
||||
//'fill-opacity': 0, // see through
|
||||
stroke: '#6db33f',
|
||||
fill: '#eeeeee',
|
||||
'stroke-width': 1
|
||||
},
|
||||
'.input-port': {
|
||||
idp: 'input',
|
||||
port: 'input',
|
||||
height: 8, width: 8,
|
||||
magnet: true,
|
||||
fill: '#eeeeee',
|
||||
transform: 'translate(' + -4 + ',' + ((IMAGE_H / 2 ) - 4) + ')',
|
||||
stroke: '#34302d',
|
||||
'stroke-width': 1
|
||||
},
|
||||
'.output-port': {
|
||||
id: 'output',
|
||||
port: 'output',
|
||||
height: 8, width: 8,
|
||||
magnet: true,
|
||||
fill: '#eeeeee',
|
||||
transform: 'translate(' + (IMAGE_W - 4) + ',' + ((IMAGE_H / 2) - 4) + ')',
|
||||
stroke: '#34302d',
|
||||
'stroke-width': 1
|
||||
},
|
||||
'.label': {
|
||||
'text-anchor': 'middle',
|
||||
'ref-x': 0.5, // jointjs specific: relative position to ref'd element
|
||||
// 'ref-y': -12, // jointjs specific: relative position to ref'd element
|
||||
'ref-y': 0.3,
|
||||
ref: '.border', // jointjs specific: element for ref-x, ref-y
|
||||
fill: 'black',
|
||||
'font-size': 14
|
||||
},
|
||||
'.label2': {
|
||||
'text': '\u21d2',
|
||||
'text-anchor': 'middle',
|
||||
'ref-x': 0.15, // jointjs specific: relative position to ref'd element
|
||||
'ref-y': 0.2, // jointjs specific: relative position to ref'd element
|
||||
ref: '.border', // jointjs specific: element for ref-x, ref-y
|
||||
// transform: 'translate(' + (IMAGE_W/2) + ',' + (IMAGE_H/2) + ')',
|
||||
fill: 'black',
|
||||
'font-size': 24
|
||||
},
|
||||
'.shape': {
|
||||
},
|
||||
'.image': {
|
||||
refWidth: 1,
|
||||
refHeight: 1,
|
||||
}
|
||||
}
|
||||
}, joint.shapes.basic.Generic.prototype.defaults)
|
||||
});
|
||||
|
||||
|
||||
joint.shapes.flo.Link = joint.dia.Link.extend({
|
||||
defaults: joint.util.deepSupplement({
|
||||
type: joint.shapes.flo.LINK_TYPE,
|
||||
attrs: {
|
||||
'.connection': { stroke: '#34302d', 'stroke-width': 2 },
|
||||
// Lots of alternatives that have been played with:
|
||||
// '.smoooth': true
|
||||
// '.marker-source': { stroke: '#9B59B6', fill: '#9B59B6', d: 'M24.316,5.318,9.833,13.682,9.833,5.5,5.5,5.5,5.5,25.5,9.833,25.5,9.833,17.318,24.316,25.682z' },
|
||||
// '.marker-target': { stroke: '#F39C12', fill: '#F39C12', d: 'M14.615,4.928c0.487-0.986,1.284-0.986,1.771,0l2.249,4.554c0.486,0.986,1.775,1.923,2.864,2.081l5.024,0.73c1.089,0.158,1.335,0.916,0.547,1.684l-3.636,3.544c-0.788,0.769-1.28,2.283-1.095,3.368l0.859,5.004c0.186,1.085-0.459,1.553-1.433,1.041l-4.495-2.363c-0.974-0.512-2.567-0.512-3.541,0l-4.495,2.363c-0.974,0.512-1.618,0.044-1.432-1.041l0.858-5.004c0.186-1.085-0.307-2.6-1.094-3.368L3.93,13.977c-0.788-0.768-0.542-1.525,0.547-1.684l5.026-0.73c1.088-0.158,2.377-1.095,2.864-2.081L14.615,4.928z' },
|
||||
// '.connection': { 'stroke':'black'},
|
||||
// '.': { filter: { name: 'dropShadow', args: { dx: 1, dy: 1, blur: 2 } } },
|
||||
// '.connection': { 'stroke-width': 10, 'stroke-linecap': 'round' },
|
||||
// This means: moveto 10 0, lineto 0 5, lineto, 10 10 closepath(z)
|
||||
// '.marker-target': { d: 'M 5 0 L 0 7 L 5 14 z', stroke: '#34302d','stroke-width': 1},
|
||||
// '.marker-target': { d: 'M 14 2 L 9,2 L9,0 L 0,7 L 9,14 L 9,12 L 14,12 z', 'stroke-width': 1, fill: '#34302d', stroke: '#34302d'},
|
||||
// '.marker-source': {d: 'M 5 0 L 5,10 L 0,10 L 0,0 z', 'stroke-width': 0, fill: '#34302d', stroke: '#34302d'},
|
||||
// '.marker-target': { stroke: '#E74C3C', fill: '#E74C3C', d: 'M 10 0 L 0 5 L 10 10 z' },
|
||||
'.marker-arrowheads': { display: 'none' },
|
||||
'.tool-options': { display: 'none' }
|
||||
},
|
||||
// connector: { name: 'normalDimFix' }
|
||||
}, joint.dia.Link.prototype.defaults)
|
||||
});
|
||||
|
||||
joint.shapes.flo.LinkView = joint.dia.LinkView.extend({
|
||||
|
||||
options: joint.util.deepSupplement({
|
||||
linkToolsOffset: 0.5,
|
||||
shortLinkLength: 40
|
||||
}, joint.dia.LinkView.prototype.options),
|
||||
|
||||
updateToolsPosition: function() {
|
||||
// Overriden to support relative offset for tools placement.
|
||||
// If offset is between 0 and 1 then percentage of the connection length will be used to offset the tools group
|
||||
if (this.options.linkToolsOffset < 1 && this.options.linkToolsOffset > 0) {
|
||||
let connectionLength = this.getConnectionLength();
|
||||
const relativeOffset = this.options.linkToolsOffset
|
||||
this.options.linkToolsOffset = connectionLength * relativeOffset;
|
||||
const returnValue = joint.dia.LinkView.prototype.updateToolsPosition.apply(this, arguments);
|
||||
this.options.linkToolsOffset = relativeOffset;
|
||||
return returnValue;
|
||||
} else {
|
||||
return joint.dia.LinkView.prototype.updateToolsPosition.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
|
||||
_beforeArrowheadMove: function() {
|
||||
if (this.model.get('source').id) {
|
||||
this._oldSource = this.model.get('source');
|
||||
}
|
||||
if (this.model.get('target').id) {
|
||||
this._oldTarget = this.model.get('target');
|
||||
}
|
||||
joint.dia.LinkView.prototype._beforeArrowheadMove.apply(this, arguments);
|
||||
},
|
||||
|
||||
_afterArrowheadMove: function() {
|
||||
joint.dia.LinkView.prototype._afterArrowheadMove.apply(this, arguments);
|
||||
if (!this.model.get('source').id) {
|
||||
if (this._oldSource) {
|
||||
this.model.set('source', this._oldSource);
|
||||
} else {
|
||||
this.model.remove();
|
||||
}
|
||||
}
|
||||
if (!this.model.get('target').id) {
|
||||
if (this._oldTarget) {
|
||||
this.model.set('target', this._oldTarget);
|
||||
} else {
|
||||
this.model.remove();
|
||||
}
|
||||
}
|
||||
delete this._oldSource;
|
||||
delete this._oldTarget;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// TODO: must do cleanup for the `mainElementView'
|
||||
joint.shapes.flo.ElementView = joint.dia.ElementView.extend({
|
||||
// canShowTooltip: true,
|
||||
beingDragged: false,
|
||||
// _tempZorder: 0,
|
||||
_tempOpacity: 1.0,
|
||||
_hovering: false,
|
||||
|
||||
dragLinkStart: function(evt: any, magnet: any, x: number, y: number) {
|
||||
|
||||
this.model.startBatch('add-link');
|
||||
|
||||
const linkView = this.addLinkFromMagnet(magnet, x, y);
|
||||
|
||||
// backwards compatiblity events
|
||||
joint.dia.CellView.prototype.pointerdown.apply(linkView, [evt, x, y]);
|
||||
linkView.notify('link:pointerdown', evt, x, y);
|
||||
|
||||
/*** START MAIN DIFF ***/
|
||||
const sourceOrTarget = $(magnet).attr('port') === 'input' ? 'source' : 'target';
|
||||
linkView.eventData(evt, linkView.startArrowheadMove(sourceOrTarget, { whenNotAllowed: 'remove' }));
|
||||
/*** END MAIN DIFF ***/
|
||||
|
||||
this.eventData(evt, { linkView: linkView });
|
||||
},
|
||||
|
||||
addLinkFromMagnet: function(magnet: any, x: number, y: number) {
|
||||
|
||||
const paper = this.paper;
|
||||
const graph = paper.model;
|
||||
|
||||
const link = paper.getDefaultLink(this, magnet);
|
||||
|
||||
let sourceEnd, targetEnd: any;
|
||||
|
||||
/*** START MAIN DIFF ***/
|
||||
if ($(magnet).attr('port') === 'input') {
|
||||
sourceEnd = { x: x, y: y };
|
||||
targetEnd = this.getLinkEnd(magnet, x, y, link, 'target');
|
||||
} else {
|
||||
sourceEnd = this.getLinkEnd(magnet, x, y, link, 'source');
|
||||
targetEnd = { x: x, y: y };
|
||||
}
|
||||
/*** END MAIN DIFF ***/
|
||||
|
||||
link.set({
|
||||
source: sourceEnd,
|
||||
target: targetEnd
|
||||
}).addTo(graph, {
|
||||
async: false,
|
||||
ui: true
|
||||
});
|
||||
|
||||
return link.findView(paper);
|
||||
},
|
||||
|
||||
// pointerdown: function(evt: any, x: number, y: number) {
|
||||
// // this.canShowTooltip = false;
|
||||
// // this.hideTooltip();
|
||||
// this.beingDragged = false;
|
||||
// this._tempOpacity = this.model.attr('./opacity');
|
||||
//
|
||||
// this.model.trigger('batch:start');
|
||||
//
|
||||
// if ( // target is a valid magnet start linking
|
||||
// evt.target.getAttribute('magnet') &&
|
||||
// this.paper.options.validateMagnet.call(this.paper, this, evt.target)
|
||||
// ) {
|
||||
// let link = this.paper.getDefaultLink(this, evt.target);
|
||||
// if ($(evt.target).attr('port') === 'input') {
|
||||
// link.set({
|
||||
// source: { x: x, y: y },
|
||||
// target: {
|
||||
// id: this.model.id,
|
||||
// selector: this.getSelector(evt.target),
|
||||
// port: evt.target.getAttribute('port')
|
||||
// }
|
||||
// });
|
||||
// } else {
|
||||
// link.set({
|
||||
// source: {
|
||||
// id: this.model.id,
|
||||
// selector: this.getSelector(evt.target),
|
||||
// port: evt.target.getAttribute('port')
|
||||
// },
|
||||
// target: { x: x, y: y }
|
||||
// });
|
||||
// }
|
||||
// this.paper.model.addCell(link);
|
||||
// this._linkView = this.paper.findViewByModel(link);
|
||||
// if ($(evt.target).attr('port') === 'input') {
|
||||
// this._linkView.startArrowheadMove('source');
|
||||
// } else {
|
||||
// this._linkView.startArrowheadMove('target');
|
||||
// }
|
||||
// this.paper.__creatingLinkFromPort = true;
|
||||
// } else {
|
||||
// this._dx = x;
|
||||
// this._dy = y;
|
||||
// joint.dia.CellView.prototype.pointerdown.apply(this, arguments);
|
||||
// }
|
||||
// },
|
||||
drag: function(evt: MouseEvent, x: number, y: number) {
|
||||
let interactive = isFunction(this.options.interactive) ? this.options.interactive(this, 'pointermove') :
|
||||
this.options.interactive;
|
||||
if (interactive !== false) {
|
||||
this.paper.trigger('dragging-node-over-canvas', {type: Flo.DnDEventType.DRAG, view: this, event: evt});
|
||||
}
|
||||
joint.dia.ElementView.prototype.drag.apply(this, arguments);
|
||||
},
|
||||
dragEnd: function(evt: MouseEvent, x: number, y: number) { // jshint ignore:line
|
||||
this.paper.trigger('dragging-node-over-canvas', {type: Flo.DnDEventType.DROP, view: this, event: evt});
|
||||
joint.dia.ElementView.prototype.dragEnd.apply(this, arguments);
|
||||
},
|
||||
// events: {
|
||||
// // Tooltips on the elements in the graph
|
||||
// 'mouseenter': function(evt: MouseEvent) {
|
||||
// if (this.canShowTooltip) {
|
||||
// this.showTooltip(evt.pageX, evt.pageY);
|
||||
// }
|
||||
// if (!this._hovering && !this.paper.__creatingLinkFromPort) {
|
||||
// this._hovering = true;
|
||||
// if (isChrome || isFF) {
|
||||
// this._tempZorder = this.model.get('z');
|
||||
// this.model.toFront({deep: true});
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// 'mouseleave': function() {
|
||||
// this.hideTooltip();
|
||||
// if (this._hovering) {
|
||||
// this._hovering = false;
|
||||
// if (isChrome || isFF) {
|
||||
// this.model.set('z', this._tempZorder);
|
||||
// var z = this._tempZorder;
|
||||
// this.model.getEmbeddedCells({breadthFirst: true}).forEach(function(cell: dia.Cell) {
|
||||
// cell.set('z', ++z);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// 'mousemove': function(evt: MouseEvent) {
|
||||
// this.moveTooltip(evt.pageX, evt.pageY);
|
||||
// }
|
||||
// },
|
||||
// showTooltip: function(x: number, y: number) {
|
||||
// var mousex = x + 10;
|
||||
// var mousey = y + 10;
|
||||
//
|
||||
// var nodeTooltip: HTMLElement;
|
||||
// if (this.model instanceof joint.dia.Element && this.model.get('metadata')) {
|
||||
// nodeTooltip = document.createElement('div');
|
||||
// $(nodeTooltip).addClass('node-tooltip');
|
||||
//
|
||||
// $(nodeTooltip).appendTo($('body')).fadeIn('fast');
|
||||
// $(nodeTooltip).addClass('tooltip-description');
|
||||
// var nodeTitle = document.createElement('div');
|
||||
// $(nodeTooltip).append(nodeTitle);
|
||||
// var nodeDescription = document.createElement('div');
|
||||
// $(nodeTooltip).append(nodeDescription);
|
||||
//
|
||||
// var model = this.model;
|
||||
//
|
||||
// if (model.attr('metadata/name')) {
|
||||
// var typeSpan = document.createElement('span');
|
||||
// $(typeSpan).addClass('tooltip-title-type');
|
||||
// $(nodeTitle).append(typeSpan);
|
||||
// $(typeSpan).text(model.attr('metadata/name'));
|
||||
// if (model.attr('metadata/group')) {
|
||||
// var groupSpan = document.createElement('span');
|
||||
// $(groupSpan).addClass('tooltip-title-group');
|
||||
// $(nodeTitle).append(groupSpan);
|
||||
// $(groupSpan).text('(' + model.attr('metadata/group') + ')');
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// model.get('metadata').get('description').then(function(description: string) {
|
||||
// $(nodeDescription).text(description);
|
||||
// }, function(error: any) {
|
||||
// if (error) {
|
||||
// Logger.error(error);
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// // defaultValue
|
||||
// if (!model.attr('metadata/metadata/hide-tooltip-options')) {
|
||||
// model.get('metadata')?.get('properties').then(function(metaProps: any) {
|
||||
// var props = model.attr('props'); // array of {'name':,'value':}
|
||||
// if (metaProps && props) {
|
||||
// Object.keys(props).sort().forEach(function(propertyName) {
|
||||
// if (metaProps[propertyName]) {
|
||||
// var optionRow = document.createElement('div');
|
||||
// var optionName = document.createElement('span');
|
||||
// var optionDescription = document.createElement('span');
|
||||
// $(optionName).addClass('node-tooltip-option-name');
|
||||
// $(optionDescription).addClass('node-tooltip-option-description');
|
||||
// $(optionName).text(metaProps[propertyName].name);
|
||||
// $(optionDescription).text(props[propertyName]);//nodeOptionData[i].description);
|
||||
// $(optionRow).append(optionName);
|
||||
// $(optionRow).append(optionDescription);
|
||||
// $(nodeTooltip).append(optionRow);
|
||||
// }
|
||||
// // This was the code to add every parameter in:
|
||||
// // $(optionName).addClass('node-tooltip-option-name');
|
||||
// // $(optionDescription).addClass('node-tooltip-option-description');
|
||||
// // $(optionName).text(metaProps[propertyName].name);
|
||||
// // $(optionDescription).text(metaProps[propertyName].description);
|
||||
// // $(optionRow).append(optionName);
|
||||
// // $(optionRow).append(optionDescription);
|
||||
// // $(nodeTooltip).append(optionRow);
|
||||
// });
|
||||
// }
|
||||
// }, function(error: any) {
|
||||
// if (error) {
|
||||
// Logger.error(error);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// $('.node-tooltip').css({ top: mousey, left: mousex });
|
||||
// } else if (this.model.get('type') === joint.shapes.flo.DECORATION_TYPE && this.model.attr('./kind') === 'error') {
|
||||
// Logger.debug('mouse enter: ERROR box=' + JSON.stringify(this.model.getBBox()));
|
||||
// nodeTooltip = document.createElement('div');
|
||||
// var errors = this.model.attr('messages');
|
||||
// if (errors && errors.length > 0) {
|
||||
// $(nodeTooltip).addClass('error-tooltip');
|
||||
// $(nodeTooltip).appendTo($('body')).fadeIn('fast');
|
||||
// var header = document.createElement('p');
|
||||
// $(header).text('Errors:');
|
||||
// $(nodeTooltip).append(header);
|
||||
// for (var i = 0;i < errors.length; i++) {
|
||||
// var errorElement = document.createElement('li');
|
||||
// $(errorElement).text(errors[i]);
|
||||
// $(nodeTooltip).append(errorElement);
|
||||
// }
|
||||
// $('.error-tooltip').css({ top: mousey, left: mousex });
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// hideTooltip: function() {
|
||||
// $('.node-tooltip').remove();
|
||||
// $('.error-tooltip').remove();
|
||||
// },
|
||||
// moveTooltip: function(x: number, y: number) {
|
||||
// $('.node-tooltip')
|
||||
// .css({ top: y + 10, left: x + 10 });
|
||||
// $('.error-tooltip')
|
||||
// .css({ top: y + 10, left: x + 10 });
|
||||
// }
|
||||
});
|
||||
|
||||
joint.shapes.flo.ErrorDecoration = joint.shapes.basic.Generic.extend({
|
||||
|
||||
markup: '<g class="rotatable"><g class="scalable"><image/></g></g>',
|
||||
|
||||
defaults: joint.util.deepSupplement({
|
||||
|
||||
type: joint.shapes.flo.DECORATION_TYPE,
|
||||
size: ERROR_MARKER_SIZE,
|
||||
attrs: {
|
||||
'image': ERROR_MARKER_SIZE
|
||||
}
|
||||
|
||||
}, joint.shapes.basic.Generic.prototype.defaults)
|
||||
});
|
||||
|
||||
joint.shapes.flo.PaletteGroupHeader = joint.shapes.basic.Generic.extend({
|
||||
// The path is the open/close arrow, defaults to vertical (open)
|
||||
markup: '<g class="scalable"><rect/></g><text/><g class="rotatable"><path d="m 10 10 l 5 8.7 l 5 -8.7 z"/></g>',
|
||||
defaults: joint.util.deepSupplement({
|
||||
type: 'palette.groupheader',
|
||||
size: {width: 170, height: 30},
|
||||
position: {x: 0, y: 0},
|
||||
attrs: {
|
||||
'rect': { fill: '#34302d', 'stroke-width': 1, stroke: '#6db33f', 'follow-scale': true, width: 80, height: 40 },
|
||||
'text': {
|
||||
text: '',
|
||||
fill: '#eeeeee',
|
||||
'ref-x': 0.5,
|
||||
'ref-y': 7,
|
||||
'x-alignment': 'middle',
|
||||
'font-size': 18/*, 'font-weight': 'bold', 'font-variant': 'small-caps', 'text-transform': 'capitalize'*/
|
||||
},
|
||||
'path': { fill: 'white', 'stroke-width': 2, stroke: 'white'/*,transform:'rotate(90,15,15)'*/}
|
||||
},
|
||||
// custom properties
|
||||
isOpen: true
|
||||
}, joint.shapes.basic.Generic.prototype.defaults)
|
||||
});
|
||||
|
||||
joint.shapes.flo.NoMatchesFound = joint.shapes.basic.Generic.extend({
|
||||
// The path is the open/close arrow, defaults to vertical (open)
|
||||
markup: '<g class="scalable"><rect class="no-matches-label-border"/></g><rect class="no-mathes-label-bg"/><text class="no-matches-label"/>',
|
||||
defaults: joint.util.deepSupplement({
|
||||
size: {width: 170, height: 30},
|
||||
position: {x: 0, y: 0},
|
||||
attrs: {
|
||||
'.no-matches-label-border': {
|
||||
refWidth: 1,
|
||||
refHeight: 1,
|
||||
refX: 0,
|
||||
refY: 0,
|
||||
},
|
||||
'.no-macthes-label-bg': {
|
||||
ref: '.no-matches-label',
|
||||
refWidth: 10,
|
||||
refHeight: 2,
|
||||
'follow-scale': true
|
||||
},
|
||||
'.no-matches-label': {
|
||||
text: 'No results found.',
|
||||
ref: '.no-matches-label-border',
|
||||
refY: 0.5,
|
||||
refY2: 5,
|
||||
yAlignment: 'middle',
|
||||
},
|
||||
},
|
||||
}, joint.shapes.basic.Generic.prototype.defaults)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
export namespace Constants {
|
||||
|
||||
export const REMOVE_HANDLE_TYPE = REMOVE;
|
||||
|
||||
export const PROPERTIES_HANDLE_TYPE = 'properties';
|
||||
|
||||
export const ERROR_DECORATION_KIND = ERROR;
|
||||
|
||||
export const PALETTE_CONTEXT = 'palette';
|
||||
|
||||
export const CANVAS_CONTEXT = 'canvas';
|
||||
|
||||
export const FEEDBACK_CONTEXT = 'feedback';
|
||||
|
||||
}
|
||||
|
||||
export namespace Shapes {
|
||||
|
||||
export interface CreationParams extends Flo.CreationParams {
|
||||
renderer?: Flo.Renderer;
|
||||
paper?: dia.Paper;
|
||||
graph?: dia.Graph;
|
||||
}
|
||||
|
||||
export interface ElementCreationParams extends CreationParams {
|
||||
position?: dia.Point;
|
||||
}
|
||||
|
||||
export interface LinkCreationParams extends CreationParams {
|
||||
source: Flo.LinkEnd;
|
||||
target: Flo.LinkEnd;
|
||||
}
|
||||
|
||||
export interface EmbeddedChildCreationParams extends CreationParams {
|
||||
parent: dia.Cell;
|
||||
position?: dia.Point;
|
||||
}
|
||||
|
||||
export interface DecorationCreationParams extends EmbeddedChildCreationParams {
|
||||
kind: string;
|
||||
messages: Array<string>;
|
||||
}
|
||||
|
||||
export interface HandleCreationParams extends EmbeddedChildCreationParams {
|
||||
kind: string;
|
||||
}
|
||||
|
||||
export interface FilterOptions {
|
||||
amount: number;
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
|
||||
export class Factory {
|
||||
|
||||
/**
|
||||
* Create a JointJS node that embeds extra metadata (properties).
|
||||
*/
|
||||
static createNode(params: ElementCreationParams): dia.Element {
|
||||
let renderer = params.renderer;
|
||||
let paper = params.paper;
|
||||
let metadata = params.metadata;
|
||||
let position = params.position;
|
||||
let props = params.props;
|
||||
let graph = params.graph || (params.paper ? params.paper.model : undefined);
|
||||
|
||||
let node: dia.Element;
|
||||
if (!position) {
|
||||
position = {x: 0, y: 0};
|
||||
}
|
||||
|
||||
if (renderer && isFunction(renderer.createNode)) {
|
||||
node = renderer.createNode({graph, paper}, metadata, props);
|
||||
} else {
|
||||
node = new joint.shapes.flo.Node();
|
||||
if (metadata) {
|
||||
node.attr('.label/text', metadata.name);
|
||||
}
|
||||
}
|
||||
node.set('type', joint.shapes.flo.NODE_TYPE);
|
||||
if (position) {
|
||||
node.set('position', position);
|
||||
}
|
||||
if (props) {
|
||||
Array.from(props.keys()).forEach(key => node.attr(`props/${key}`, props!.get(key)));
|
||||
}
|
||||
node.set('metadata', metadata);
|
||||
if (graph) {
|
||||
graph.addCell(node);
|
||||
}
|
||||
if (renderer && isFunction(renderer.initializeNewNode)) {
|
||||
let descriptor: Flo.ViewerDescriptor = {
|
||||
paper: paper,
|
||||
graph: graph
|
||||
};
|
||||
renderer.initializeNewNode(node, descriptor);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
static createLink(params: LinkCreationParams): dia.Link {
|
||||
let renderer = params.renderer;
|
||||
let paper = params.paper;
|
||||
let metadata = params.metadata;
|
||||
let source = params.source;
|
||||
let target = params.target;
|
||||
let props = params.props;
|
||||
let graph = params.graph || (params.paper ? params.paper.model : undefined);
|
||||
|
||||
let link: dia.Link;
|
||||
if (renderer && isFunction(renderer.createLink)) {
|
||||
link = renderer.createLink(source, target, metadata, props);
|
||||
} else {
|
||||
link = new joint.shapes.flo.Link();
|
||||
}
|
||||
if (source) {
|
||||
link.set('source', source);
|
||||
}
|
||||
if (target) {
|
||||
link.set('target', target);
|
||||
}
|
||||
link.set('type', joint.shapes.flo.LINK_TYPE);
|
||||
if (metadata) {
|
||||
link.set('metadata', metadata);
|
||||
}
|
||||
if (props) {
|
||||
Array.from(props.keys()).forEach(key => link.attr(`props/${key}`, props!.get(key)));
|
||||
}
|
||||
if (graph) {
|
||||
graph.addCell(link);
|
||||
}
|
||||
if (renderer && isFunction(renderer.initializeNewLink)) {
|
||||
let descriptor: Flo.ViewerDescriptor = {
|
||||
paper: paper,
|
||||
graph: graph
|
||||
};
|
||||
renderer.initializeNewLink(link, descriptor);
|
||||
}
|
||||
// prevent creation of link breaks
|
||||
link.attr('.marker-vertices/display', 'none');
|
||||
return link;
|
||||
}
|
||||
|
||||
static createDecoration(params: DecorationCreationParams): dia.Element {
|
||||
let renderer = params.renderer;
|
||||
let paper = params.paper;
|
||||
let parent = params.parent;
|
||||
let kind = params.kind;
|
||||
let messages = params.messages;
|
||||
let location = params.position;
|
||||
let graph = params.graph || (params.paper ? params.paper.model : undefined);
|
||||
|
||||
let decoration: dia.Element;
|
||||
if (renderer && isFunction(renderer.createDecoration)) {
|
||||
decoration = renderer.createDecoration(kind, parent);
|
||||
}
|
||||
if (decoration) {
|
||||
decoration.set('type', joint.shapes.flo.DECORATION_TYPE);
|
||||
if ((isChrome || isFF) && parent && typeof parent.get('z') === 'number') {
|
||||
decoration.set('z', parent.get('z') + 1);
|
||||
}
|
||||
decoration.attr('./kind', kind);
|
||||
decoration.attr('messages', messages);
|
||||
if (graph) {
|
||||
graph.addCell(decoration);
|
||||
}
|
||||
parent.embed(decoration);
|
||||
if (renderer && isFunction(renderer.initializeNewDecoration)) {
|
||||
let descriptor: Flo.ViewerDescriptor = {
|
||||
paper: paper,
|
||||
graph: graph
|
||||
};
|
||||
renderer.initializeNewDecoration(decoration, descriptor);
|
||||
}
|
||||
return decoration;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static createHandle(params: HandleCreationParams): dia.Element {
|
||||
let renderer = params.renderer;
|
||||
let paper = params.paper;
|
||||
let parent = params.parent;
|
||||
let kind = params.kind;
|
||||
let location = params.position;
|
||||
let graph = params.graph || (params.paper ? params.paper.model : undefined);
|
||||
|
||||
let handle: dia.Element;
|
||||
if (!location) {
|
||||
location = {x: 0, y: 0};
|
||||
}
|
||||
if (renderer && isFunction(renderer.createHandle)) {
|
||||
handle = renderer.createHandle(kind, parent);
|
||||
} else {
|
||||
handle = new joint.shapes.flo.ErrorDecoration({
|
||||
size: HANDLE_SIZE,
|
||||
attrs: {
|
||||
'image': {
|
||||
'xlink:href': HANDLE_ICON_MAP.get(kind)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
handle.set('type', joint.shapes.flo.HANDLE_TYPE);
|
||||
handle.set('position', location);
|
||||
if ((isChrome || isFF) && parent && typeof parent.get('z') === 'number') {
|
||||
handle.set('z', parent.get('z') + 1);
|
||||
}
|
||||
handle.attr('./kind', kind);
|
||||
if (graph) {
|
||||
graph.addCell(handle);
|
||||
}
|
||||
parent.embed(handle);
|
||||
if (renderer && isFunction(renderer.initializeNewHandle)) {
|
||||
let descriptor: Flo.ViewerDescriptor = {
|
||||
paper: paper,
|
||||
graph: graph
|
||||
};
|
||||
renderer.initializeNewHandle(handle, descriptor);
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
loadShapes();
|
||||
|
||||
8
projects/flo/styles.scss
Normal file
8
projects/flo/styles.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
@import "./../../node_modules/jointjs/dist/joint.core.css";
|
||||
/*
|
||||
@import "./../../node_modules/codemirror/lib/codemirror.css";
|
||||
|
||||
@import "./../../node_modules/codemirror/addon/hint/show-hint.css";
|
||||
@import "./../../node_modules/codemirror/addon/lint/lint.css";
|
||||
@import "./../../node_modules/codemirror/addon/scroll/simplescrollbars.css";
|
||||
*/
|
||||
15
projects/flo/tsconfig.dev.json
Normal file
15
projects/flo/tsconfig.dev.json
Normal file
@@ -0,0 +1,15 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../out-tsc/flo",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
},
|
||||
"exclude": [
|
||||
"test.ts",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
13
projects/flo/tsconfig.prod.json
Normal file
13
projects/flo/tsconfig.prod.json
Normal file
@@ -0,0 +1,13 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.dev.json",
|
||||
"compilerOptions": {
|
||||
"declarationMap": false,
|
||||
"sourceMap": false,
|
||||
"inlineSourceMap": false,
|
||||
"inlineSources": false
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"compilationMode": "partial"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user