angular6基于jsplumb的规则引擎流程设计实现

jsPlumb是一个在元素之间绘制连接线的javascript框架,它使用svg技术绘制连接线。

相关资料链接:

  jsplumb官网:https://jsplumbtoolkit.com

  jsplumb中文:https://github.com/wangduanduan/jsplumb-chinese-tutorialhttps://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

实现界面效果:

  • 7
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Angular中使用jsPlumb来画流程图,可以通过以下步骤进行操作: 第一步,引入jsPlumb库。你可以通过npm命令安装jsPlumb库,然后将其引入到你的Angular项目中。例如,使用以下命令安装jsPlumb库: ``` npm install jsplumb ``` 在你的组件的.ts文件中,引入jsPlumb库: ``` import * as jsPlumb from 'jsplumb'; ``` 第二步,创建画布。你需要在你的HTML文件中添加一个容器元素,用于承载jsPlumb画布。你可以将其定义为一个div元素,并给它一个唯一的id。例如: ```html <div id="myCanvas"></div> ``` 第三步,初始化jsPlumb实例。在你的组件的.ts文件中,创建一个jsPlumb实例,并与画布元素进行绑定。你可以在组件的ngOnInit()方法中进行初始化。例如: ```typescript ngOnInit() { jsPlumb.ready(() => { const instance = jsPlumb.getInstance(); instance.setContainer('myCanvas'); }); } ``` 第四步,添加元素和连接线。你可以在需要添加元素或连接线的地方调用jsPlumb实例的相应方法来实现。例如,要添加一个元素,你可以使用addEndpoint()方法,并传入元素的id和位置等参数。要添加连接线,你可以使用connect()方法,并传入起始元素和目标元素的id。例如: ```typescript jsPlumb.ready(() => { const instance = jsPlumb.getInstance(); instance.setContainer('myCanvas'); const start = instance.addEndpoint('start', { anchors: 'Right' }); const end = instance.addEndpoint('end', { anchor: 'Left' }); instance.connect({ source: start, target: end }); }); ``` 以上就是在Angular中使用jsPlumb来画流程图的基本步骤。你可以根据自己的需求调整画布的样式、元素的位置、连接线的样式等。希望对你有帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

txp1993

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值