jsPlumb是一个在元素之间绘制连接线的javascript框架,它使用svg技术绘制连接线。
相关资料链接:
jsplumb官网:https://jsplumbtoolkit.com
jsplumb中文:https://github.com/wangduanduan/jsplumb-chinese-tutorial,https://wdd.js.org/jsplumb-chinese-tutorial/#/
jsplumb保存思路:http://www.itdaan.com/blog/2013/12/16/cf7ada7efab0b9395541c1eea7f7c050.html
前段时间,公司项目需要,用了差不多接近一周时间在angular6中实现了一个规则引擎流程拖拽设计,整体效果如下图所示:
核心代码如下:
1.界面左侧规则节点拖拽到右侧生成:
//定义左侧规则节点拖放函数
public initRuleEngineNodeDrage(): void{
setTimeout(function(oper){
let euleNode = oper.element.nativeElement.querySelector('p-accordion');
$(euleNode).find('.rule-engine-node').draggable({
helper: "clone",
scope: "engine",
});
$("#ruleEngineJsPlumb").droppable({
scope: "engine",
drop: function (event, ui) {
// 创建工厂模型到拖拽区
oper.createModel(ui, $(this));
}
});
$('#ruleEngineJsPlumb').on('click', 'div.jsplumb-node', function () {
if(!$(this).hasClass("jsplumb-node-selected")){
$('div.jsplumb-node').removeClass('jsplumb-node-selected');
$(this).addClass('jsplumb-node-selected');
}
// 点击节点控制按钮
let elemetEndpoints = oper.ruleNodesJsplumb.$jsPlumbInstance.selectEndpoints({element: $(this)});
elemetEndpoints.each(function(endpoint){
const type = endpoint.anchor.type;
if(type == 'RightMiddle'){ // 右边端点为sourceAnchors,定义了overlays节点按钮
oper.ruleNodesJsplumb._nodeButtonClick(endpoint);
}
});
// 隐藏连接按钮
oper.ruleNodesJsplumb._lineButtonShow({flag: false});
// 预加载选中节点的属性及页面内容
// 设置当前拖拽节点属性
oper.ruleNodesJsplumb._setRuleNodeOptions(this);
// 预加载节点表单自定义属性
oper.componentsHandle.loadComponent(oper.ruleNodesJsplumb.$nodeModel.nodeType,
oper.ruleNodesJsplumb.$nodeModel.description, oper.ruleNodesJsplumb.$nodeModel.components.options);
// 获取表单数据内容并保存
oper.saveOptionsForm();
return false;
});
},200,this);
}
//创建模型(参数依次为:drop事件的ui、当前容器)
public createModel(ui, selector): string {
// 添加规则节点模型及样式属性
let nodeId = 'node_' + this.ruleNodesJsplumb._getUUID(12,12);
let cloneNode = $(ui.helper).clone(false);
cloneNode
.attr('id', nodeId)
.removeClass('rule-engine-node')
.addClass('jsplumb-node');
$(selector).append(cloneNode);
var left = parseInt((ui.offset.left - $(selector).offset().left)+"");
var top = parseInt((ui.offset.top - $(selector).offset().top + 10)+"");
$("#" + nodeId).css("left", left).css("top", top);
// 将规则节点添加至jsPlumb
let nodeInputObj = cloneNode.find(".rule-engine-port-input");
let nodeOutputObj = cloneNode.find(".rule-engine-port-output");
let nodeInput = nodeInputObj.length > 0 ? true : false;
let nodeOutput = nodeOutputObj.length > 0 ? true : false;
this.ruleNodesJsplumb._addEndpoints({nodeId: nodeId, nodeInput: nodeInput, nodeOutput: nodeOutput});
cloneNode.click();
return nodeId;
};
2.封装的jsplumb链接及锚点:
constructor(
public crudService: CrudService
){
const opers = this;
jsPlumb.ready(function () {
opers._initInstance();
opers._initEndpoints();
opers._initEvents();
});
}
// 获取uuid唯一数据
_getUUID(len: number, radix: number): string{
var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
var uuid = [], i;
radix = radix || chars.length;
if (len) {
// Compact form
for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix];
} else {
// rfc4122, version 4 form
var r;
// rfc4122 requires these characters
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4';
// Fill in random data. At i==19 set the high bits of clock sequence as
// per rfc4122, sec. 4.1.5
for (i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | Math.random()*16;
uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
}
}
}
return uuid.join('');
}
// 初始jsPlumb实例对象
public _initInstance(){
let oper = this;
oper.$jsPlumbInstance = jsPlumb.getInstance({
// 默认拖拽属性
DragOptions: { cursor: 'pointer', zIndex: 2000 },
// 箭头和提示文本定义
ConnectionOverlays: [
// 定义箭头
[ "Arrow", {location: 0.97, id: "arrow", visible: true, width: 15, length: 15} ],
// 定义箭头的文字
[ "Label", {location: 0.5, id: "label", visible: false, cssClass: "aLabel",
events:{
click:function(info) {}
}
}],
[ "Label", {location: 0.5, id: "button_edit", visible: false, cssClass: "aLabel-button aLabel-edit",
label:"<i class='fa fa-pencil'></i>",
events:{
click:function(info) {
let label = info.component.getOverlay("label").getLabel();
oper.$connectorLabel = label;
oper.$connectorDisplayDialog = true;
}
}
}],
[ "Label", {location: 0.5, id: "button_del", visible: false, cssClass: "aLabel-button aLabel-del",
label:"<i class='fa fa-close'></i>",
events:{
click:function(info) {
oper.crudService.confirmService.confirm({
message: '您确认要删除选择的链接吗?',
header: '链接删除',
icon: 'fa fa-question-circle',
acceptLabel : '是',
rejectLabel : '否',
accept: () => {
jsPlumb.detach(info.component);// 删除connection
oper.$selectedConnection = null;// 置空connection
}
});
}
}
}]
],
// 默认情况下链接是否可拆卸(使用鼠标)。默认为true
ConnectionsDetachable: true,
// 是否重新链接用户已使用鼠标分离然后删除的链接。默认值为false。
ReattachConnections: true,
// 实例所在容器
Container: "ruleEngineJsPlumb"
});
}
// 初始端点链接样式
public _initEndpoints(){
let oper = this;
this.$paintStyle = {
stroke: "transparent",// 端点border颜色
strokeWidth: 1, // 端点border-width
fill: "transparent", // 端点背景颜色
radius: 7
};
// 鼠标悬浮在端点上的样式
this.$hoverPaintStyle = {
stroke: this.$color,// 端点border颜色
strokeWidth: 1,// 端点border-width
fill: this.$color,// 端点背景颜色
radius: 7
};
// 基本链接线样式
this.$connectorStyle = {
stroke: this.$color,// 线条颜色
strokeWidth: 2,// 线条大小
joinstyle: "round",
outlineStroke: "white",// 线条边缘颜色
outlineWidth: 1 // 线条边缘大小
};
// 鼠标悬浮在链接线上的样式
this.$connectorHoverStyle = {
stroke: this.$color,// 线条颜色
strokeWidth: 3,// 线条大小
outlineStroke: "white",// 线条边缘颜色
outlineWidth: 2 // 线条边缘大小
};
// 基本链接线样式
this.$connectorClickStyle = {
stroke: this.$colorClick,// 线条颜色
strokeWidth: 2,// 线条大小
joinstyle: "round",
outlineStroke: "white",// 线条边缘颜色
outlineWidth: 1 // 线条边缘大小
};
// 鼠标悬浮在链接线上的样式
this.$connectorClickHoverStyle = {
stroke: this.$colorClick,// 线条颜色
strokeWidth: 3,// 线条大小
outlineStroke: "white",// 线条边缘颜色
outlineWidth: 2 // 线条边缘大小
};
// 源端点样式定义
this.$sourceEndpoint = {
// 端点类型大小
endpoint: ["Rectangle", {width: 10, height: 10, cssClass: 'jsplumb-endpoint'}],
// 端点基本样式
paintStyle: this.$paintStyle,
// 端点悬浮样式
hoverPaintStyle: this.$hoverPaintStyle,
// 设置链接点最多可以链接几条线,值-1表示没有上限
maxConnections: -1,
// 是否可以拖动(作为连线起点)
isSource: true,
// 是否可以放置(作为连线终点)
isTarget: false,
// 链接线类型
connector: [ "Bezier", { stub: [40, 60], curviness: 150 } ],
// 链接线基本样式
connectorStyle: this.$connectorStyle,
// 链接线悬浮样式
connectorHoverStyle: this.$connectorHoverStyle,
// 端点拖拽样式
dragOptions: {},
// 端点label定义
overlays: [[ "Label", {location: [0, -0.1], id: "node_edit", visible: false, cssClass: "aLabel-button aLabel-edit",
label:"<i class='fa fa-pencil'></i>",
events:{
click:function(info) {
oper.$nodeDisplayDialog = true;
}
}
}],
[ "Label", {location: [0, -0.1], id: "node_del", visible: false, cssClass: "aLabel-button aLabel-del",
label:"<i class='fa fa-close'></i>",
events:{
click:function(info) {
let element = info.component.element;
let nodeName = $(element).find('.rule-engine-label').text();
oper.crudService.confirmService.confirm({
message: '您确认要删除选择的['+ nodeName +']节点吗?',
header: '节点删除',
icon: 'fa fa-question-circle',
acceptLabel : '是',
rejectLabel : '否',
accept: () => {
let elemetEndpoints = oper.$jsPlumbInstance.selectEndpoints({element: element});
elemetEndpoints.each(function(endpoint){
oper.$jsPlumbInstance.deleteEndpoint(endpoint);// 端点,连线
});
jsPlumb.remove(element);// 删除节点元素
oper._deleteRuleNode(element);
oper.$selectedEndpoint = null;// 置空端点
oper.$selectedConnection = null;// 置空连线
}
});
}
}
}]]
};
// 目标端点样式定义
this.$targetEndpoint = {
// 端点类型大小
endpoint: ["Rectangle", {width: 10, height: 10, cssClass: 'jsplumb-endpoint' }],
// 端点基本样式
paintStyle: this.$paintStyle,
// 端点悬浮样式
hoverPaintStyle: this.$hoverPaintStyle,
// 设置链接点最多可以链接几条线,值-1表示没有上限
maxConnections: -1,
// 是否可以拖动(作为连线起点)
isSource: false,
// 是否可以放置(作为连线终点)
isTarget: true,
// 端点拖拽样式
dropOptions: { hoverClass: "drop-hover", activeClass: "drop-active" }
};
}
// 初始绑定事件
public _initEvents(){
let oper = this, instance = this.$jsPlumbInstance;
// 点击连线触发显示连线按钮,隐藏节点按钮
instance.bind('click', function (connection, originalEvent) {
oper._lineButtonClick(connection);
oper._nodeButtonShow({flag: false});
return false;
});
// 当链接建立前进行条件判断
instance.bind('beforeDrop', function (info) {
if(!info.connection){ return false; }
// 判断是否已经链接
let result = instance.getConnections(info.connection);
if(result && result.length > 0){
// 已链接时链接不会建立,注意,必须是false
return false;
}else{
if(info.connection.sourceId == info.connection.targetId){
// 与自己端点链接不会建立,注意,必须是false
return false;
}else{
return true;
}
}
});
}
// 动态添加节点端点
public _addEndpoints({nodeId = "", nodeInput = true, nodeOutput = true} = {}){
const sourceAnchors = (nodeOutput ? ['RightMiddle'] : []), targetAnchors = (nodeInput ? ['LeftMiddle'] : []);
for (let i = 0; i < sourceAnchors.length; i++) {
let sourceUUID = nodeId + this.$split + sourceAnchors[i];
this.$jsPlumbInstance.addEndpoint(nodeId, this.$sourceEndpoint, {
anchor: sourceAnchors[i], uuid: sourceUUID
});
}
for (let j = 0; j < targetAnchors.length; j++) {
let targetUUID = nodeId + this.$split + targetAnchors[j];
this.$jsPlumbInstance.addEndpoint(nodeId, this.$targetEndpoint, {
anchor: targetAnchors[j], uuid: targetUUID });
}
// 使规则节点可以拖拽
this.$jsPlumbInstance.draggable(nodeId, { grid: [20, 20] });
}
// 设置端点连线的label
public _setConnectLabel({connection = this.$selectedConnection, label = ""} = {}){
if(label){
connection.getOverlay("label").setLabel(label);
connection.getOverlay("label").setVisible(true);
}else{
connection.getOverlay("label").setLabel("");
connection.getOverlay("label").setVisible(false);
}
}
// 节点上的编辑和删除按钮显示事件
public _nodeButtonShow({endpoint = this.$selectedEndpoint, flag = true} = {}){
if(!endpoint){ return; }
// 节点编辑按钮
let rule_node_edit = endpoint.getOverlay("node_edit");
// 节点删除按钮
let rule_node_del = endpoint.getOverlay("node_del");
let elementId = endpoint.anchor.elementId;
if(rule_node_edit && rule_node_del){
if(flag){
rule_node_edit.setVisible(true);
rule_node_del.setVisible(true);
$('#'+ elementId).addClass('jsplumb-node-selected');
}else {
this.$selectedEndpoint = null;
rule_node_edit.setVisible(false);
rule_node_del.setVisible(false);
$('#'+ elementId).removeClass('jsplumb-node-selected');
}
}
}
// 节点上的编辑和删除按钮点击显示
public _nodeButtonClick(endpoint){
if(!endpoint){ return; }
if(this.$selectedEndpoint){
this._nodeButtonShow({endpoint: this.$selectedEndpoint, flag: false});
this._nodeButtonShow({endpoint: endpoint});
this.$selectedEndpoint = endpoint;
}else{
this._nodeButtonShow({endpoint: endpoint});
this.$selectedEndpoint = endpoint;
}
}
// 连线上的编辑和删除按钮显示事件
public _lineButtonShow({connection = this.$selectedConnection, flag = true} = {}){
if(!connection){ return; }
// 连线编辑按钮
let path_button_edit = connection.getOverlay("button_edit");
// 连线删除按钮
let path_button_del = connection.getOverlay("button_del");
if(path_button_edit && path_button_del){
if(flag){
connection.setPaintStyle(this.$connectorClickStyle);
connection.setHoverPaintStyle(this.$connectorClickHoverStyle);
path_button_edit.setVisible(true);
path_button_del.setVisible(true);
}else {
this.$selectedConnection = null;
path_button_edit.setVisible(false);
path_button_del.setVisible(false);
connection.setPaintStyle(this.$connectorStyle);
connection.setHoverPaintStyle(this.$connectorHoverStyle);
}
}
}
// 连线上的编辑和删除按钮点击显示
public _lineButtonClick(connection){
if(!connection){ return; }
if(this.$selectedConnection){
this._lineButtonShow({connection: this.$selectedConnection, flag: false});
this._lineButtonShow({connection: connection});
this.$selectedConnection = connection;
}else{
this._lineButtonShow({connection: connection});
this.$selectedConnection = connection;
}
}
// (数据处理)通过节点编辑点击获取对象元素的属性
public _setRuleNodeOptions(nodeElement){
if(!nodeElement){ return; }
this.$nodeModel.id = $(nodeElement).attr('id');// 节点唯一标识
this.$nodeModel.nodeType = $(nodeElement).attr('nodeType');// 节点类型
// 从dom对象获取数据
let nodeInputObj = $(nodeElement).find(".rule-engine-port-input");
let nodeOutputObj = $(nodeElement).find(".rule-engine-port-output");
let nodeInput = nodeInputObj.length > 0 ? true : false;
let nodeOutput = nodeOutputObj.length > 0 ? true : false;
let nodeIconDiv = $(nodeElement).find(".rule-engine-icon");
let nodeIcon = nodeIconDiv[0].classList.length > 1 ? nodeIconDiv[0].classList[1] : "";
this.$nodeModel.description = $(nodeElement).find('.rule-engine-label').text();// 节点展示名称
this.$nodeModel.nodeTitle = $(nodeElement).attr('title');// 节点提示描述
this.$nodeModel.nodeClass = $(nodeElement).attr('nodeClass');// 节点实现类
this.$nodeModel.nodeIcon = nodeIcon;// 节点图标(assets/img/rule-engine,assets/css/rule-engine.css)
this.$nodeModel.nodeInput = nodeInput;// 节点输入端点标识
this.$nodeModel.nodeOutput = nodeOutput;// 节点输出端点标识
this.$nodeModel.nodeStyle = {
"background-color": $(nodeElement).css("background-color"),
"position": "absolute",
"left": $(nodeElement).css("left"),
"top": $(nodeElement).css("top")
};// 节点样式属性
this.$nodeModel.components.options = {};
// 从保存的$jsPlumbJson获取options数据
if(this.$jsPlumbJson.nodes){
let length = this.$jsPlumbJson.nodes.length;
for(let index = 0; index < length; index++){
if(this.$jsPlumbJson.nodes[index].id == this.$nodeModel.id
&& this.$jsPlumbJson.nodes[index].nodeType == this.$nodeModel.nodeType){
this.$nodeModel.components.options = new JsPlumbNodeModel(this.$jsPlumbJson.nodes[index]).components.options;
break;
}
}
}
}
// (数据处理)根据$nodeModel操作节点保存或更新数据到$jsPlumbJson的nodes对象
public _saveOrUpdateRuleNode(){
if(!this.$nodeModel){ return; }
if(!this.$jsPlumbJson.nodes){// 节点存储
this.$jsPlumbJson.nodes = [];
this.$jsPlumbJson.nodes.push(new JsPlumbNodeModel(this.$nodeModel));
}else{
let length = this.$jsPlumbJson.nodes.length;
if(length == 0){
this.$jsPlumbJson.nodes.push(new JsPlumbNodeModel(this.$nodeModel));
}else{
let isUpdate = false;
for(let index = 0; index < length; index++){
if(this.$jsPlumbJson.nodes[index].id == this.$nodeModel.id
&& this.$jsPlumbJson.nodes[index].nodeType == this.$nodeModel.nodeType){
this.$jsPlumbJson.nodes[index] = new JsPlumbNodeModel(this.$nodeModel);
isUpdate = true;
break;
}
}
if(!isUpdate){
this.$jsPlumbJson.nodes.push(new JsPlumbNodeModel(this.$nodeModel));
}
}
}
}
// (数据处理)根据Dom操作节点删除对应的$jsPlumbJson的nodes对象
private _deleteRuleNode(nodeElement){
if(!nodeElement){ return; }
let id = $(nodeElement).attr('id');// 节点唯一标识
let nodeType = $(nodeElement).attr('nodeType');// 节点类型
// 从保存的$jsPlumbJson删除数据
if(this.$jsPlumbJson.nodes){
let length = this.$jsPlumbJson.nodes.length;
for(let index = 0; index < length; index++){
if(this.$jsPlumbJson.nodes[index].id == id
&& this.$jsPlumbJson.nodes[index].nodeType == nodeType){
this.$jsPlumbJson.nodes.splice(index,1);
this.$nodeModel = new JsPlumbNodeModel();
break;
}
}
}
}
// 获取所有设计的规则引擎节点及链接内容
public _getJsPlumbConnections(){
let oper = this;
oper.$jsPlumbJson.connections = [];
$.each(oper.$jsPlumbInstance.getAllConnections(), function (index, connection) {
const label = connection.getOverlay("label").getLabel();
oper.$jsPlumbJson.connections.push({
uuids: connection.getUuids(),
label: label,
anchors: $.map(connection.endpoints, function(endpoint) {
return [[endpoint.anchor.x,
endpoint.anchor.y,
endpoint.anchor.orientation[0],
endpoint.anchor.orientation[1],
endpoint.anchor.offsets[0],
endpoint.anchor.offsets[1]]];
})
});
});
return oper.$jsPlumbJson;
}
3.核心部分处理已经贴出来了,后续就是对组件的动态加载及属性自定义实现内容
(it开发交流QQ群:101951157)
4.使用JsPlumb破解工具版升级现有的规则节点,实现整个流程
破解的JsPlumb工具版依赖文件下载:https://pan.baidu.com/s/18Y-w8g3v5FhjyouukYeAzg
升级后重新封装的jsPlumbToolkit业务操作:
// 获取uuid唯一数据
_getUUID(len: number, radix: number): string{
let chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
let uuid = [], i;
radix = radix || chars.length;
if (len) {
// Compact form
for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix];
} else {
// rfc4122, version 4 form
let r;
// rfc4122 requires these characters
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4';
// Fill in random data. At i==19 set the high bits of clock sequence as
// per rfc4122, sec. 4.1.5
for (i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | Math.random()*16;
uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
}
}
}
return uuid.join('');
}
// 初始端点链接样式
public _initEndpoints(){
this.$paintStyle = {
stroke: "transparent",// 端点border颜色
strokeWidth: 1, // 端点border-width
fill: "transparent", // 端点背景颜色
radius: 7
};
// 鼠标悬浮在端点上的样式
this.$hoverPaintStyle = {
stroke: this.$color,// 端点border颜色
strokeWidth: 1,// 端点border-width
fill: this.$color,// 端点背景颜色
radius: 7
};
// 基本链接线样式
this.$connectorStyle = {
stroke: this.$color,// 线条颜色
strokeWidth: 2,// 线条大小
joinstyle: "round",
outlineStroke: "white",// 线条边缘颜色
outlineWidth: 1 // 线条边缘大小
};
// 鼠标悬浮在链接线上的样式
this.$connectorHoverStyle = {
stroke: this.$color,// 线条颜色
strokeWidth: 3,// 线条大小
outlineStroke: "white",// 线条边缘颜色
outlineWidth: 2 // 线条边缘大小
};
// 基本链接线样式
this.$connectorClickStyle = {
stroke: this.$colorClick,// 线条颜色
strokeWidth: 2,// 线条大小
joinstyle: "round",
outlineStroke: "white",// 线条边缘颜色
outlineWidth: 1 // 线条边缘大小
};
// 鼠标悬浮在链接线上的样式
this.$connectorClickHoverStyle = {
stroke: this.$colorClick,// 线条颜色
strokeWidth: 3,// 线条大小
outlineStroke: "white",// 线条边缘颜色
outlineWidth: 2 // 线条边缘大小
};
// 源端点样式定义
this.$sourceEndpoint = {
// edge类型
edgeType: "rightMiddle",
// 端点类型大小
endpoint: ["Rectangle", {width: 10, height: 10, cssClass: 'jsplumb-endpoint'}],
// 端点基本样式
paintStyle: this.$paintStyle,
// 端点悬浮样式
hoverPaintStyle: this.$hoverPaintStyle,
// 设置链接点最多可以链接几条线,值-1表示没有上限
maxConnections: -1,
// 是否可以拖动(作为连线起点)
isSource: true,
// 是否可以放置(作为连线终点)
isTarget: false,
// 链接线类型
connector: [ "Bezier", { stub: [40, 60], curviness: 150 } ],
// 链接线基本样式
connectorStyle: this.$connectorStyle,
// 链接线悬浮样式
connectorHoverStyle: this.$connectorHoverStyle,
// 端点拖拽样式
dragOptions: {}
};
// 目标端点样式定义
this.$targetEndpoint = {
// edge类型
edgeType: "leftMiddle",
// 端点类型大小
endpoint: ["Rectangle", {width: 10, height: 10, cssClass: 'jsplumb-endpoint' }],
// 端点基本样式
paintStyle: this.$paintStyle,
// 端点悬浮样式
hoverPaintStyle: this.$hoverPaintStyle,
// 设置链接点最多可以链接几条线,值-1表示没有上限
maxConnections: -1,
// 是否可以拖动(作为连线起点)
isSource: false,
// 是否可以放置(作为连线终点)
isTarget: true,
// 端点拖拽样式
dropOptions: { hoverClass: "drop-hover", activeClass: "drop-active" }
};
}
// 初始JsPlumbToolkit Ready
public _initJsPlumbToolkitReady(){
let oper = this;
jsPlumbToolkit.ready(function () {
// 组件类型加载
oper.componentsMap = oper.componentsService.getRuleNodesComponent();
// 初始端点链接样式
oper._initEndpoints();
// ------------------------ toolkit setup ------------------------------------
// This function is what the toolkit will use to get an ID from a node.
let idFunction = function (node) {
return node.id;
};
// get the various dom elements
let mainElement = document.querySelector("#rule-main-div"),
canvasElement = mainElement.querySelector("#ruleEngineJsPlumb"),
miniviewElement = mainElement.querySelector(".miniview"),
nodePalette = mainElement.querySelector("#rule-accordion"),
controls = mainElement.querySelector(".controls");
oper.$jsPlumbToolkitInstance = jsPlumbToolkit.newInstance({
idFunction: idFunction,
nodeFactory: function (type, data, callback) {
callback(data);
},
edgeFactory: function (params, data, callback) {
callback(data);
},
beforeConnect:function(source, target) {
let targetObj = target.objectType == 'Port' ? target.getNode() : target;
// 判断是否已经链接
let paths = oper.$jsPlumbToolkitInstance.getPath({
source: source.getNode(),
target: targetObj,
strict: false,
nodeFilter: function(node){
// 排除
if(node.id == source.getNode().id || node.id == targetObj.id){
if(source.getNode().id != targetObj.id){
return true;
}
}
return false;
}});
if(paths && paths.contains(targetObj)){
// 已链接时链接不会建立,注意,必须是false
return false;
}else{
if(source.getNode().id == targetObj.id){
// 与自己端点链接不会建立,注意,必须是false
return false;
}else{
// 目标节点是源节点的上级节点不会连接,注意,必须是false
let flag = { result: true};
oper._isSourceNode({sourceId: source.getNode().id, targetId: targetObj.id, flag: flag});
return flag.result;
}
}
}
});
// ------------------------ / toolkit setup ------------------------------------
// ------------------------ rendering ------------------------------------
let renderer = window['renderer'] = oper.$jsPlumbToolkitInstance.render({
container: canvasElement,
view: {
nodes: {
"default":{ }
},
edges: {
"default": {
// 默认拖拽属性
DragOptions: { cursor: 'pointer', zIndex: 2000 },
connector: [ "Bezier", { stub: [40, 60], curviness: 150 } ],
paintStyle: oper.$connectorStyle,
hoverPaintStyle:oper.$connectorHoverStyle,
events:{
// 连线公共点击事件
click:function(params) {
jsPlumbUtil.consume(params.e);
if(params.connection && params.connection.sourceId && params.connection.targetId){
oper._lineButtonClick(params.connection);
oper._nodeButtonShow({flag: false});
}
}
},
overlays: [
// 定义箭头
[ "Arrow", {location: 0.97, id: "arrow", visible: true, width: 15, length: 15} ],
// 定义箭头的文字
[ "Label", {location: 0.5, id: "label", visible: true, cssClass: "${cssClass}",
label: "${label}",
events:{
click:function(params) { }
}
}],
[ "Label", {location: 0.5, id: "button_edit", visible: false, cssClass: "aLabel-button aLabel-edit",
label:"<i class='fa fa-pencil'></i>",
events:{
click:function(params) {
jsPlumbUtil.consume(params.overlay.e);
let label = params.edge.data.label || "";
oper.$connectorLabel = label;
oper.$connectorDisplayDialog = true;
}
}
}],
[ "Label", {location: 0.5, id: "button_del", visible: false, cssClass: "aLabel-button aLabel-del",
label:"<i class='fa fa-close'></i>",
events:{
click:function(params) {
jsPlumbUtil.consume(params.overlay.e);
oper.crudService.confirmService.confirm({
message: '您确认要删除选择的链接吗?',
header: '链接删除',
icon: 'fa fa-question-circle',
acceptLabel : '是',
rejectLabel : '否',
accept: () => {
oper.$jsPlumbToolkitInstance.removeEdge(params.edge);
oper.$selectedConnection = null;// 置空connection
}
});
}
}
}]
],
// 默认情况下链接是否可拆卸(使用鼠标)。默认为true
ConnectionsDetachable: true,
// 是否重新链接用户已使用鼠标分离然后删除的链接。默认值为false。
ReattachConnections: true,
// reattach: true, // 拖动端点可以解绑并可移动连接到别的节点
// allowLoopback: false, // 防止环回连接
// allowNodeLoopback: false
},
"leftMiddle": {
parent: "default",
anchor: [ "LeftMiddle"],
},
"rightMiddle": {
parent: "default",
anchor: [ "RightMiddle"],
}
},
ports: {
"leftMiddle": oper.$targetEndpoint,
"rightMiddle": oper.$sourceEndpoint
}
},
templateResolver:function(templateId) {
// find the matching template and return it - as a String.
return '<div class="jsplumb-node" id="${id}" nodeType="${nodeType}" title="${nodeTitle}" ' +
'style="background-color: ${nodeBgColor}">' +
'<div class="rule-engine-label">${description}</div>' +
'<div class="rule-engine-icon-div"><div class="rule-engine-icon ${nodeIcon}"></div></div>' +
'<r-if test="nodeInput"><jtk-port port-id="${id}_LeftMiddle" port-type="leftMiddle" class="rule-engine-port rule-engine-port-input"></jtk-port></r-if>' +
'<r-if test="nodeOutput"><jtk-port port-id="${id}_RightMiddle" port-type="rightMiddle" class="rule-engine-port rule-engine-port-output"></jtk-port></r-if>' +
// '<div id="node_edit" class="rule-engine-port rule-engine-port-output aLabel-button aLabel-edit aLabel-margin-right hidden"><i class="fa fa-pencil"></i></div>' +
'<div id="node_del" class="rule-engine-port rule-engine-port-output aLabel-button aLabel-edit hidden"><i class="fa fa-close"></i></div>' +
'</div>';
},
layout: {
type: "Absolute",
// orientation: "vertical",
// padding:[ 10, 10 ]
},
miniview: {
container: miniviewElement
},
events: {
nodeAdded: function(node){
// 键盘按键监听(tabindex可以获取焦点)
$(node.el).attr("tabindex","-1");
$(node.el).keydown(function(event){
let e = event || window.event;
let k = e.keyCode || e.which;
if(k == 8 || k == 46){ // backspace || delete
let nodeDom = e.target;
jsPlumbUtil.consume(e);
let nodeId = nodeDom.id || oper.$nodeModel.id;
let description = oper.$nodeModel.description;
oper.crudService.confirmService.confirm({
message: '您确认要删除选择的['+ description +']节点吗?',
header: '节点删除',
icon: 'fa fa-question-circle',
acceptLabel : '是',
rejectLabel : '否',
accept: () => {
oper.$jsPlumbToolkitInstance.removeNode(nodeId);
oper.$selectedNode = null;// 置空node
oper.$selectedConnection = null;// 置空连线
}
});
return false;
}
});
},
portAdded: function (params) {
params.nodeEl.querySelectorAll("ul")[0].appendChild(params.portEl);
},
edgeAdded: function (params) {
if (params.addedByMouse) { }
},
canvasClick: function (e) {
oper._lineButtonShow({flag: false});
oper._nodeButtonShow({flag: false});
oper.$jsPlumbToolkitInstance.clearSelection();
}
},
dragOptions: { },
consumeRightClick: false,
zoomToFit:true
});
// listener for mode change on renderer.
renderer.bind("modeChanged", function (mode) {
jsPlumb.removeClass(controls.querySelectorAll("[mode]"), "selected-mode");
jsPlumb.addClass(controls.querySelectorAll("[mode='" + mode + "']"), "selected-mode");
});
// ------------------------ / rendering ------------------------------------
// ------------------------ drag and drop rule node -----------------
renderer.registerDroppableNodes({
droppables: nodePalette.querySelectorAll(".rule-engine-node"),
dragOptions: {
zIndex: 50000,
cursor: "move",
clone: true
},
dataGenerator: function (type, draggedElement, eventInfo, eventLocation) {
draggedElement.id = oper._getUUID(12,12); // 生成组件ID
oper._setRuleNodeOptions(draggedElement); // 生成对象data数据
return new JsPlumbNodeModel(oper.$nodeModel);
}
});
// ------------------------ / drag and drop rule node -----------------
// ------------------------- behaviour ----------------------------------
// undoredo init
let undoredo = window['undoredo'] = new jsPlumbToolkitUndoRedo({
toolkit:oper.$jsPlumbToolkitInstance,
onChange:function(undo, undoSize, redoSize) {
controls.setAttribute("can-undo", (undoSize > 0 ? 'true':'false'));
controls.setAttribute("can-redo", (redoSize > 0 ? 'true':'false'));
},
compound:true
});
// controls undo button click
jsPlumb.on(controls, "tap", "[undo]", function () {
undoredo.undo();
});
// controls redo button click
jsPlumb.on(controls, "tap", "[redo]", function () {
undoredo.redo();
});
// controls pan mode/select click
jsPlumb.on(controls, "tap", "[mode]", function () {
renderer.setMode(this.getAttribute("mode"));
});
// controls home button click, zoom content to fit.
jsPlumb.on(controls, "tap", "[reset]", function () {
oper.$jsPlumbToolkitInstance.clearSelection();
renderer.zoomToFit();
});
// ------------------------- / behaviour ----------------------------------
// let datasetView = new jsPlumbSyntaxHighlighter(oper.$jsPlumbToolkitInstance, "#jtk-demo-dataset", "json", 2);
});
}
// 设置端点连线的label
public _setConnectLabel({connection = this.$selectedConnection, label = ""} = {}){
if(label){
this.$jsPlumbToolkitInstance.updateEdge(connection.edge, { label: label, cssClass: "aLabel" });
connection.getOverlay("label").addClass("aLabel");
}else{
this.$jsPlumbToolkitInstance.updateEdge(connection.edge, { label: "", cssClass: "" });
connection.getOverlay("label").removeClass("aLabel");
}
}
// 节点上的编辑和删除按钮显示事件
public _nodeButtonShow({node = this.$selectedNode, flag = true} = {}){
if(!node){ return; }
// 节点编辑按钮
// let rule_node_edit = $(node).find("#node_edit");
// 节点删除按钮
let rule_node_del = $(node).find("#node_del");
if(rule_node_del){
if(flag){
// rule_node_edit.show();
rule_node_del.show();
$(node).addClass('jsplumb-node-selected');
}else {
// rule_node_edit.hide();
rule_node_del.hide();
$(node).removeClass('jsplumb-node-selected');
}
}
}
// 节点上的编辑和删除按钮点击显示
public _nodeButtonClick(node){
if(!node){ return; }
if(this.$selectedNode){
this._nodeButtonShow({node: this.$selectedNode, flag: false});
this._nodeButtonShow({node: node});
this.$selectedNode = node;
}else{
this._nodeButtonShow({node: node});
this.$selectedNode = node;
}
}
// 连线上的编辑和删除按钮显示事件
public _lineButtonShow({connection = this.$selectedConnection, flag = true} = {}){
if(!connection){ return; }
// 连线编辑按钮
let path_button_edit = connection.getOverlay("button_edit");
// 连线删除按钮
let path_button_del = connection.getOverlay("button_del");
if(path_button_edit && path_button_del){
if(flag){
connection.setPaintStyle(this.$connectorClickStyle);
connection.setHoverPaintStyle(this.$connectorClickHoverStyle);
path_button_edit.setVisible(true);
path_button_del.setVisible(true);
}else {
this.$selectedConnection = null;
path_button_edit.setVisible(false);
path_button_del.setVisible(false);
connection.setPaintStyle(this.$connectorStyle);
connection.setHoverPaintStyle(this.$connectorHoverStyle);
}
}
}
// 连线上的编辑和删除按钮点击显示
public _lineButtonClick(connection){
if(!connection){ return; }
if(this.$selectedConnection){
this._lineButtonShow({connection: this.$selectedConnection, flag: false});
this._lineButtonShow({connection: connection});
this.$selectedConnection = connection;
}else{
this._lineButtonShow({connection: connection});
this.$selectedConnection = connection;
}
}
// (数据处理)通过节点编辑点击获取对象元素的属性
public _setRuleNodeOptions(nodeElement){
if(!nodeElement){ return; }
this.$nodeModel.id = $(nodeElement).attr('id');// 节点唯一标识
this.$nodeModel.nodeType = $(nodeElement).attr('nodeType');// 节点类型
// 从dom对象获取数据
let nodeInputObj = $(nodeElement).find(".rule-engine-port-input");
let nodeOutputObj = $(nodeElement).find(".rule-engine-port-output");
let nodeInput = nodeInputObj.length > 0 ? true : false;
let nodeOutput = nodeOutputObj.length > 0 ? true : false;
let nodeIconDiv = $(nodeElement).find(".rule-engine-icon");
let nodeIcon = nodeIconDiv[0].classList.length > 1 ? nodeIconDiv[0].classList[1] : "";
this.$nodeModel.description = $(nodeElement).find('.rule-engine-label').text();// 节点展示名称
this.$nodeModel.nodeTitle = $(nodeElement).attr('title');// 节点提示描述
this.$nodeModel.nodeIcon = nodeIcon;// 节点图标(assets/img/rule-engine,assets/css/rule-engine.css)
this.$nodeModel.nodeInput = nodeInput;// 节点输入端点标识
this.$nodeModel.nodeOutput = nodeOutput;// 节点输出端点标识
this.$nodeModel.nodeBgColor = $(nodeElement).css("background-color");// 节点背景样式属性
// 从保存的$jsPlumbToolkitInstance获取options数据
let nodeInfo = this.$jsPlumbToolkitInstance.getObjectInfo(this.$nodeModel.id);
if(nodeInfo && nodeInfo.obj && nodeInfo.obj.data && nodeInfo.obj.data.components.options){
let theOptions = nodeInfo.obj.data.components.options;
let keyArray = Object.keys(theOptions);
if(keyArray.length == 0){
this.$nodeModel.components.options = _.cloneDeep(this.componentsMap[this.$nodeModel.nodeType].options);
}else{
this.$nodeModel.components.options = _.cloneDeep(theOptions);
}
}else{
this.$nodeModel.components.options = _.cloneDeep(this.componentsMap[this.$nodeModel.nodeType].options);
}
}
// 递归判断连接的target节点是否是该节点的source父节点
private _isSourceNode({sourceId = null,targetId = null,flag = { result: true}}={}){
let oper = this;
if(sourceId && targetId && flag.result){
let targetIdArray = [];
let sourceObj = oper.$jsPlumbToolkitInstance.getObjectInfo(sourceId);
let targetObj = oper.$jsPlumbToolkitInstance.getObjectInfo(targetId);
let paths = oper.$jsPlumbToolkitInstance.getPath({
source: targetObj, // 目标节点 到
target: sourceObj, // 源节点的 连线
strict: false
});
paths.eachEdge(function(index, edge){
let uuidArray = edge ? edge.source.id.split('_') : [];
if(uuidArray && uuidArray.length == 2){
if(uuidArray[0] == targetId && uuidArray[1] == "RightMiddle"){ // 作为源节点
flag.result = false;
return;
}else{
if(uuidArray[1] == "LeftMiddle"){ // 选择下一个目标节点
targetIdArray.push(uuidArray[0]);
}
}
}
});
if(flag.result){
for(let index = 0;index< targetIdArray.length; index++){
oper._isSourceNode({sourceId: sourceId,targetId: targetIdArray[index],flag: flag})
}
}
}
}
// json导出1
public _fake_click(obj) {
let ev = document.createEvent("MouseEvents");
ev.initMouseEvent(
"click", true, false, window, 0, 0, 0, 0, 0
, false, false, false, false, 0, null
);
obj.dispatchEvent(ev);
}
// json导出2
public _export_raw(name, data) {
let urlObject = window.URL || window["webkitURL"] || window;
let export_blob = new Blob([data]);
let save_link = document.createElementNS("http://www.w3.org/1999/xhtml", "a")
save_link["href"] = urlObject.createObjectURL(export_blob);
save_link["download"] = name;
this._fake_click(save_link);
}
// ---------------------------------------------------------------------------------------------------------------
// 比较两个JSON对象是否相等-start------------------------------------------------------------------------------------
private _isObj(object) {
return object && typeof (object) == 'object' && Object.prototype.toString.call(object).toLowerCase() == "[object object]";
}
private _isArray(object) {
return object && typeof (object) == 'object' && object.constructor == Array;
}
private _getLength(object) {
var count = 0;
for (let i in object) { count++; }
return count;
}
public _Compare(objA, objB) {
if (!this._isObj(objA) || !this._isObj(objB)) return false; //判断类型是否正确
if (this._getLength(objA) != this._getLength(objB)) return false; //判断长度是否一致
return this._CompareObj(objA, objB, true);//默认为true
}
private _CompareObj(objA, objB, flag) {
for (let key in objA) {
if (!flag) //跳出整个循环
break;
if (!objB.hasOwnProperty(key)) { flag = false; break; }
if (!this._isArray(objA[key])) { //子级不是数组时
if(!this._isObj(objA[key])){//不是对象,比较属性值
if (objB[key] != objA[key]) { flag = false; break; }
}else{//是对象,继续遍历对象比较
flag = this._Compare(objA[key], objB[key]);
}
} else {
if (!this._isArray(objB[key])) { flag = false; break; }
let oA = objA[key], oB = objB[key];
if (oA.length != oB.length) { flag = false; break; }
for (let k in oA) {
if (!flag) //这里跳出循环是为了不让递归继续
break;
if (this._getLength(oA[k]) != this._getLength(oB[k])) { flag = false; break; } //判断长度是否一致
flag = this._CompareObj(oA[k], oB[k], flag);
}
}
}
return flag;
}
// 比较两个JSON对象是否相等-end--------------------------------------------------------------------------------------
然后在页面初始化的时候调用改文件的_initJsPlumbToolkitReady()方法
具体代码备份地址:https://pan.baidu.com/s/1-EY4bIksdL5bWO-39YGwdg
实现界面效果: