Files
spring-flo/src/lib/shared/shapes.ts
2019-11-28 16:19:43 -05:00

727 lines
24 KiB
TypeScript

import { dia } from 'jointjs';
import { Flo } from './flo-common';
import * as _ from 'lodash';
import * as _$ from 'jquery';
const joint: any = Flo.joint;
const $: any = _$;
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};
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';
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');
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': {
width: IMAGE_W,
height: IMAGE_H,
rx: 3,
ry: 3,
'fill-opacity': 0, // see through
stroke: '#eeeeee',
'stroke-width': 0
},
'.box': {
width: IMAGE_W,
height: IMAGE_H,
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': {
width: IMAGE_W,
height: IMAGE_H
}
}
}, 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({
}, joint.dia.LinkView.prototype.options),
_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.attr('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.attr('metadata').get('description').then(function(description: string) {
// $(nodeDescription).text(description);
// }, function(error: any) {
// if (error) {
// console.error(error);
// }
// });
//
// // defaultValue
// if (!model.attr('metadata/metadata/hide-tooltip-options')) {
// model.attr('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) {
// console.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') {
// console.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)
});
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.attr('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.attr('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 (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);
if (location) {
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;
}
}
}