Files
spring-flo/projects/demo/app/editor.ts
2023-03-09 16:12:00 -05:00

553 lines
23 KiB
TypeScript

/*
* 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;
}
}