Angular集成bpmn.js的基础实现及扩展

一、bpmn的基本认识

bpmn.js是一个BPMN2.0渲染工具包和web建模器, 使得画流程图的功能在前端来完成。

bpmn画图具有哪些内容?

二、使用npm安装bpmn.js

npm install --save bpmn-js

三、在Angular中使用bpmn.js

1.实现编辑器组件

  • 安装相关依赖
    npm install --save bpmn-js
  • 编写HTML代码
    <div class="container">
      <div class="canvas"></div>
    </div>
    
  • 编写CSS代码
    .container{
      position: absolute;
      height: 100%;
      width: 100%;
      .canvas{
        height: 100%;
        width: 100%;
      }
    }
    
  • 创建mock文件夹,新增xmlStr.ts(默认展示的bpmn流程图)
    export var xmlStr:any = `
      <?xml version="1.0" encoding="UTF-8"?>
      <bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                         xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL"
                         xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
                         xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
                         xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
                         xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd" id="sample-diagram"
                         targetNamespace="http://bpmn.io/schema/bpmn">
        <bpmn2:process id="Process_1" isExecutable="false">
          <bpmn2:startEvent id="StartEvent_1"/>
        </bpmn2:process>
        <bpmndi:BPMNDiagram id="BPMNDiagram_1">
          <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
            <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
              <dc:Bounds height="36.0" width="36.0" x="412.0" y="240.0"/>
            </bpmndi:BPMNShape>
          </bpmndi:BPMNPlane>
        </bpmndi:BPMNDiagram>
      </bpmn2:definitions>
    `
    
  • 编写JS代码
    //diagram.component.ts
    import { Component } from '@angular/core';
    import BpmnModeler from 'bpmn-js/lib/Modeler';
    import { xmlStr } from './mock/xmlStr';
    
    @Component({
      selector: 'app-diagram',
      templateUrl: './diagram.component.html',
      styleUrls: ['./diagram.component.scss']
    })
    
    export class DiagramComponent {
      bpmnModeler:any;
    
      constructor() {
    
      }
    
      ngOnInit(): void {
        this.loadBPMN();
      }
    
      loadBPMN() {
        this.bpmnModeler = new BpmnModeler({
          container: '.canvas'
        })
        this.createNewDiagram(xmlStr);
      }
    
      createNewDiagram(xml:any) {
        // 将字符串转换成图显示出来
        this.bpmnModeler.importXML(xml);
      }
    }
    

2.左侧工具栏

  • 左侧工具栏只需要在angular.json中,引入下列四项css文件

    "node_modules/bpmn-js/dist/assets/diagram-js.css",
    "node_modules/bpmn-js/dist/assets/bpmn-font/css/bpmn.css",
    "node_modules/bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css",
    "node_modules/bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css"

    点击查看代码
    // angular.json
    {
      "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
      "version": 1,
      "newProjectRoot": "projects",
      "projects": {
        "bpmn-js-angular": {
          "projectType": "application",
          "schematics": {
            "@schematics/angular:component": {
              "style": "scss"
            }
          },
          "root": "",
          "sourceRoot": "src",
          "prefix": "app",
          "architect": {
            "build": {
              "builder": "@angular-devkit/build-angular:browser",
              "options": {
                "outputPath": "dist/bpmn-js-angular",
                "index": "src/index.html",
                "main": "src/main.ts",
                "polyfills": [
                  "zone.js"
                ],
                "tsConfig": "tsconfig.app.json",
                "inlineStyleLanguage": "scss",
                "assets": [
                  "src/favicon.ico",
                  "src/assets"
                ],
                "styles": [
                  "src/styles.scss",
                  "node_modules/bpmn-js/dist/assets/diagram-js.css",
                  "node_modules/bpmn-js/dist/assets/bpmn-font/css/bpmn.css",
                  "node_modules/bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css",
                  "node_modules/bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css"
                ],
                "scripts": []
              },
              "configurations": {
                "production": {
                  "budgets": [
                    {
                      "type": "initial",
                      "maximumWarning": "500kb",
                      "maximumError": "1mb"
                    },
                    {
                      "type": "anyComponentStyle",
                      "maximumWarning": "2kb",
                      "maximumError": "4kb"
                    }
                  ],
                  "outputHashing": "all"
                },
                "development": {
                  "buildOptimizer": false,
                  "optimization": false,
                  "vendorChunk": true,
                  "extractLicenses": false,
                  "sourceMap": true,
                  "namedChunks": true
                }
              },
              "defaultConfiguration": "production"
            },
            "serve": {
              "builder": "@angular-devkit/build-angular:dev-server",
              "configurations": {
                "production": {
                  "browserTarget": "bpmn-js-angular:build:production"
                },
                "development": {
                  "browserTarget": "bpmn-js-angular:build:development"
                }
              },
              "defaultConfiguration": "development"
            },
            "extract-i18n": {
              "builder": "@angular-devkit/build-angular:extract-i18n",
              "options": {
                "browserTarget": "bpmn-js-angular:build"
              }
            },
            "test": {
              "builder": "@angular-devkit/build-angular:karma",
              "options": {
                "polyfills": [
                  "zone.js",
                  "zone.js/testing"
                ],
                "tsConfig": "tsconfig.spec.json",
                "inlineStyleLanguage": "scss",
                "assets": [
                  "src/favicon.ico",
                  "src/assets"
                ],
                "styles": [
                  "src/styles.scss"
                ],
                "scripts": []
              }
            }
          }
        }
      },
      "cli": {
        "analytics": "e3a43a0d-2ca5-4ffd-9682-4aae2a796d84"
      }
    }
    
    
  • 自定义工具栏 Palette
    在项目文件夹下面新建一个custom-palette文件夹,新建CustomPalette.jsindex.js 文件。(将node_modules下bpmn-js的palette拷贝一份至custom-palette)。

    CustomPalette.js这个文件里面的内容我们就可以根据自己需要添加或者删除一些工具图标。

    点击查看CustomPalete.js
    import {
      assign
    } from 'min-dash';
    
    /**
     * A palette provider for BPMN 2.0 elements.
     */
    export default function PaletteProvider(
        palette, create, elementFactory,
        spaceTool, lassoTool, handTool,
        globalConnect, translate) {
    
      this._palette = palette;
      this._create = create;
      this._elementFactory = elementFactory;
      this._spaceTool = spaceTool;
      this._lassoTool = lassoTool;
      this._handTool = handTool;
      this._globalConnect = globalConnect;
      this._translate = translate;
    
      palette.registerProvider(this);
    }
    
    PaletteProvider.$inject = [
      'palette',
      'create',
      'elementFactory',
      'spaceTool',
      'lassoTool',
      'handTool',
      'globalConnect',
      'translate'
    ];
    
    
    PaletteProvider.prototype.getPaletteEntries = function(element) {
    
      var actions = {},
          create = this._create,
          elementFactory = this._elementFactory,
          spaceTool = this._spaceTool,
          lassoTool = this._lassoTool,
          handTool = this._handTool,
          globalConnect = this._globalConnect,
          translate = this._translate;
    
      function createAction(type, group, className, title, options) {
    
        function createListener(event) {
          var shape = elementFactory.createShape(assign({ type: type }, options));
    
          if (options) {
            shape.businessObject.di.isExpanded = options.isExpanded;
          }
    
          create.start(event, shape);
        }
    
        var shortType = type.replace(/^bpmn:/, '');
    
        return {
          group: group,
          className: className,
          title: title || translate('Create {type}', { type: shortType }),
          action: {
            dragstart: createListener,
            click: createListener
          }
        };
      }
    
      // function createUserTask(event) {
      //   create.start(event, elementFactory.createShape({
      //     type: 'bpmn:UserTask',
      //     x: 0,
      //     y: 0,
      //     isExpanded: false
      //   }));
      // }
      function createParticipant(event) {
        create.start(event, elementFactory.createParticipantShape());
      }
    
      assign(actions, {
        'hand-tool': {
          group: 'tools',
          className: 'bpmn-icon-hand-tool',
          title: translate('拖拽'),
          action: {
            click: function(event) {
              handTool.activateHand(event);
            }
          }
        },
        'lasso-tool': {
          group: 'tools',
          className: 'bpmn-icon-lasso-tool',
          title: translate('选择'),
          action: {
            click: function(event) {
              lassoTool.activateSelection(event);
            }
          }
        },
    
        // 'space-tool': {
        //   group: 'tools',
        //   className: 'bpmn-icon-space-tool',
        //   title: translate('Activate the create/remove space tool'),
        //   action: {
        //     click: function(event) {
        //       spaceTool.activateSelection(event);
        //     }
        //   }
        // },
    
        'global-connect-tool': {
          group: 'tools',
          className: 'bpmn-icon-connection-multi',
          title: translate('连接线'),
          action: {
            click: function(event) {
              globalConnect.toggle(event);
            }
          }
        },
        'tool-separator': {
          group: 'tools',
          separator: true
        },
        // 这里,仅保留开始节点、结束节点、网关、用户任务这四个element
        'create.start-event': createAction(
          'bpmn:StartEvent', 'event', 'bpmn-icon-start-event-none',
          translate('开始节点')
        ),
        'create.end-event': createAction(
          'bpmn:EndEvent', 'event', 'bpmn-icon-end-event-none',
          translate('结束节点')
        ),
        'create.exclusive-gateway': createAction(
          'bpmn:ExclusiveGateway', 'gateway', 'bpmn-icon-gateway-xor',
          translate('网关')
        ),
        'create.userTask': createAction(
          'bpmn:UserTask', 'activity', 'bpmn-icon-user-task',
          translate('用户任务')
        ),
      });
    
      return actions;
    };
    
    
    点击查看index.js
    import CustomPalette from "./CustomPalette";
    
    export default {
      __init__: ["paletteProvider"],
      paletteProvider: ["type", CustomPalette],
    };
    

3.自定义context-pad

修改方式与palette的修改方式大同小异,现在项目工程下文件夹下新建context-pad文件夹,添加ContextPadProvider.js、index.js文件,参考node-modules中的bpmn-js下面的context-pad文件夹,可以完全拷贝过来。

点击查看ContextPadProvider.js
import { assign, forEach, isArray } from "min-dash";

import { is } from "bpmn-js/lib/util/ModelUtil";

import { isExpanded, isEventSubProcess } from "bpmn-js/lib/util/DiUtil";

import { isAny } from "bpmn-js/lib/features/modeling/util/ModelingUtil";

import { getChildLanes } from "bpmn-js/lib/features/modeling/util/LaneUtil";

import { hasPrimaryModifier } from "diagram-js/lib/util/Mouse";

/**
 * A provider for BPMN 2.0 elements context pad
 */
export default function ContextPadProvider(
  config,
  injector,
  eventBus,
  contextPad,
  modeling,
  elementFactory,
  connect,
  create,
  popupMenu,
  canvas,
  rules,
  translate
) {
  config = config || {};

  contextPad.registerProvider(this);

  this._contextPad = contextPad;

  this._modeling = modeling;

  this._elementFactory = elementFactory;
  this._connect = connect;
  this._create = create;
  this._popupMenu = popupMenu;
  this._canvas = canvas;
  this._rules = rules;
  this._translate = translate;

  if (config.autoPlace !== false) {
    this._autoPlace = injector.get("autoPlace", false);
  }

  eventBus.on("create.end", 250, function (event) {
    var shape = event.context.shape;

    if (!hasPrimaryModifier(event)) {
      return;
    }

    var entries = contextPad.getEntries(shape);

    if (entries.replace) {
      entries.replace.action.click(event, shape);
    }
  });
}

ContextPadProvider.$inject = [
  "config.contextPad",
  "injector",
  "eventBus",
  "contextPad",
  "modeling",
  "elementFactory",
  "connect",
  "create",
  "popupMenu",
  "canvas",
  "rules",
  "translate",
];

ContextPadProvider.prototype.getContextPadEntries = function (element) {
  var contextPad = this._contextPad,
    modeling = this._modeling,
    elementFactory = this._elementFactory,
    connect = this._connect,
    create = this._create,
    popupMenu = this._popupMenu,
    canvas = this._canvas,
    rules = this._rules,
    autoPlace = this._autoPlace,
    translate = this._translate;

  var actions = {};

  if (element.type === "label") {
    return actions;
  }

  var businessObject = element.businessObject;

  function startConnect(event, element) {
    connect.start(event, element);
  }

  function removeElement(e) {
    modeling.removeElements([element]);
  }

  function getReplaceMenuPosition(element) {
    var Y_OFFSET = 5;

    var diagramContainer = canvas.getContainer(),
      pad = contextPad.getPad(element).html;

    var diagramRect = diagramContainer.getBoundingClientRect(),
      padRect = pad.getBoundingClientRect();

    var top = padRect.top - diagramRect.top;
    var left = padRect.left - diagramRect.left;

    var pos = {
      x: left,
      y: top + padRect.height + Y_OFFSET,
    };

    return pos;
  }

  /**
   * Create an append action
   *
   * @param {String} type
   * @param {String} className
   * @param {String} [title]
   * @param {Object} [options]
   *
   * @return {Object} descriptor
   */
  function appendAction(type, className, title, options) {
    if (typeof title !== "string") {
      options = title;
      title = translate("Append {type}", { type: type.replace(/^bpmn:/, "") });
    }

    function appendStart(event, element) {
      var shape = elementFactory.createShape(assign({ type: type }, options));
      create.start(event, shape, {
        source: element,
      });
    }

    var append = autoPlace
      ? function (event, element) {
          var shape = elementFactory.createShape(
            assign({ type: type }, options)
          );

          autoPlace.append(element, shape);
        }
      : appendStart;

    return {
      group: "model",
      className: className,
      title: title,
      action: {
        dragstart: appendStart,
        click: append,
      },
    };
  }

  function splitLaneHandler(count) {
    return function (event, element) {
      // actual split
      modeling.splitLane(element, count);

      // refresh context pad after split to
      // get rid of split icons
      contextPad.open(element, true);
    };
  }

  if (
    isAny(businessObject, ["bpmn:Lane", "bpmn:Participant"]) &&
    isExpanded(businessObject)
  ) {
    var childLanes = getChildLanes(element);

    assign(actions, {
      "lane-insert-above": {
        group: "lane-insert-above",
        className: "bpmn-icon-lane-insert-above",
        title: translate("Add Lane above"),
        action: {
          click: function (event, element) {
            modeling.addLane(element, "top");
          },
        },
      },
    });

    if (childLanes.length < 2) {
      if (element.height >= 120) {
        assign(actions, {
          "lane-divide-two": {
            group: "lane-divide",
            className: "bpmn-icon-lane-divide-two",
            title: translate("Divide into two Lanes"),
            action: {
              click: splitLaneHandler(2),
            },
          },
        });
      }

      if (element.height >= 180) {
        assign(actions, {
          "lane-divide-three": {
            group: "lane-divide",
            className: "bpmn-icon-lane-divide-three",
            title: translate("Divide into three Lanes"),
            action: {
              click: splitLaneHandler(3),
            },
          },
        });
      }
    }

    assign(actions, {
      "lane-insert-below": {
        group: "lane-insert-below",
        className: "bpmn-icon-lane-insert-below",
        title: translate("Add Lane below"),
        action: {
          click: function (event, element) {
            modeling.addLane(element, "bottom");
          },
        },
      },
    });
  }

  if (is(businessObject, "bpmn:FlowNode")) {
    if (is(businessObject, "bpmn:EventBasedGateway")) {
      assign(actions, {
        "append.receive-task": appendAction(
          "bpmn:ReceiveTask",
          "bpmn-icon-receive-task"
        ),
        "append.message-intermediate-event": appendAction(
          "bpmn:IntermediateCatchEvent",
          "bpmn-icon-intermediate-event-catch-message",
          translate("Append MessageIntermediateCatchEvent"),
          { eventDefinitionType: "bpmn:MessageEventDefinition" }
        ),
        "append.timer-intermediate-event": appendAction(
          "bpmn:IntermediateCatchEvent",
          "bpmn-icon-intermediate-event-catch-timer",
          translate("Append TimerIntermediateCatchEvent"),
          { eventDefinitionType: "bpmn:TimerEventDefinition" }
        ),
        "append.condition-intermediate-event": appendAction(
          "bpmn:IntermediateCatchEvent",
          "bpmn-icon-intermediate-event-catch-condition",
          translate("Append ConditionIntermediateCatchEvent"),
          { eventDefinitionType: "bpmn:ConditionalEventDefinition" }
        ),
        "append.signal-intermediate-event": appendAction(
          "bpmn:IntermediateCatchEvent",
          "bpmn-icon-intermediate-event-catch-signal",
          translate("Append SignalIntermediateCatchEvent"),
          { eventDefinitionType: "bpmn:SignalEventDefinition" }
        ),
      });
    } else if (
      isEventType(
        businessObject,
        "bpmn:BoundaryEvent",
        "bpmn:CompensateEventDefinition"
      )
    ) {
      assign(actions, {
        "append.compensation-activity": appendAction(
          "bpmn:Task",
          "bpmn-icon-task",
          translate("Append compensation activity"),
          {
            isForCompensation: true,
          }
        ),
      });
    } else if (
      !is(businessObject, "bpmn:EndEvent") &&
      !businessObject.isForCompensation &&
      !isEventType(
        businessObject,
        "bpmn:IntermediateThrowEvent",
        "bpmn:LinkEventDefinition"
      ) &&
      !isEventSubProcess(businessObject)
    ) {
      assign(actions, {
        "append.end-event": appendAction(
          "bpmn:EndEvent",
          "bpmn-icon-end-event-none",
          translate("Append EndEvent")
        ),
        "append.append-user-task": appendAction(
          "bpmn:UserTask",
          "bpmn-icon-user-task",
          translate("Append UserTask")
        ),
        "append.gateway": appendAction(
          "bpmn:ExclusiveGateway",
          "bpmn-icon-gateway-xor",
          translate("Append Gateway")
        ),
        // 'append.append-task': appendAction(
        //   'bpmn:Task',
        //   'bpmn-icon-task',
        //   translate('Append Task')
        // ),
        // 'append.intermediate-event': appendAction(
        //   'bpmn:IntermediateThrowEvent',
        //   'bpmn-icon-intermediate-event-none',
        //   translate('Append Intermediate/Boundary Event')
        // )
      });
    }
  }

  if (!popupMenu.isEmpty(element, "bpmn-replace")) {
    // Replace menu entry
    assign(actions, {
      replace: {
        group: "edit",
        className: "bpmn-icon-screw-wrench",
        title: translate("Change type"),
        action: {
          click: function (event, element) {
            var position = assign(getReplaceMenuPosition(element), {
              cursor: { x: event.x, y: event.y },
            });

            popupMenu.open(element, "bpmn-replace", position);
          },
        },
      },
    });
  }

  if (
    isAny(businessObject, [
      "bpmn:FlowNode",
      "bpmn:InteractionNode",
      "bpmn:DataObjectReference",
      "bpmn:DataStoreReference",
    ])
  ) {
    // assign(actions, {
    //   'append.text-annotation': appendAction('bpmn:TextAnnotation', 'bpmn-icon-text-annotation'),
    //   'connect': {
    //     group: 'connect',
    //     className: 'bpmn-icon-connection-multi',
    //     title: translate('Connect using ' +
    //               (businessObject.isForCompensation ? '' : 'Sequence/MessageFlow or ') +
    //               'Association'),
    //     action: {
    //       click: startConnect,
    //       dragstart: startConnect
    //     }
    //   }
    // });
    assign(actions, {
      connect: {
        group: "connect",
        className: "bpmn-icon-connection-multi",
        title: translate("Connect using DataInputAssociation"),
        action: {
          click: startConnect,
          dragstart: startConnect,
        },
      },
    });
  }

  if (
    isAny(businessObject, [
      "bpmn:DataObjectReference",
      "bpmn:DataStoreReference",
    ])
  ) {
    assign(actions, {
      connect: {
        group: "connect",
        className: "bpmn-icon-connection-multi",
        title: translate("Connect using DataInputAssociation"),
        action: {
          click: startConnect,
          dragstart: startConnect,
        },
      },
    });
  }

  // delete element entry, only show if allowed by rules
  var deleteAllowed = rules.allowed("elements.delete", { elements: [element] });

  if (isArray(deleteAllowed)) {
    // was the element returned as a deletion candidate?
    deleteAllowed = deleteAllowed[0] === element;
  }

  //这里做判断,禁止开始及开始后的userTask的删除
  if (deleteAllowed && element.id != "start" && element.id != "apply") {
    assign(actions, {
      delete: {
        group: "edit",
        className: "bpmn-icon-trash",
        title: translate("Remove"),
        action: {
          click: removeElement,
        },
      },
    });
  }

  return actions;
};

function isEventType(eventBo, type, definition) {
  var isType = eventBo.$instanceOf(type);
  var isDefinition = false;

  var definitions = eventBo.eventDefinitions || [];
  forEach(definitions, function (def) {
    if (def.$type === definition) {
      isDefinition = true;
    }
  });

  return isType && isDefinition;
}

点击查看index.js
import DirectEditingModule from 'diagram-js-direct-editing';
import ContextPadModule from 'diagram-js/lib/features/context-pad';
import SelectionModule from 'diagram-js/lib/features/selection';
import ConnectModule from 'diagram-js/lib/features/connect';
import CreateModule from 'diagram-js/lib/features/create';
import PopupMenuModule from 'bpmn-js/lib/features/popup-menu';

import ContextPadProvider from './ContextPadProvider';

export default {
  __depends__: [
    DirectEditingModule,
    ContextPadModule,
    SelectionModule,
    ConnectModule,
    CreateModule,
    PopupMenuModule
  ],
  __init__: [ 'contextPadProvider' ],
  contextPadProvider: [ 'type', ContextPadProvider ]
};

4.添加bpmn自带属性列表

  • 添加bpmn-js-properties-panel组件
    npm install bpmn-js-properties-panel --save
  • bpmn.js适配的是流程引擎Camunda,所以如果需要更加完整的属性输入框,也需安装camunda-bpmn-moddle,用于camunda数据对象
    npm install camunda-bpmn-moddle --save
  • 以上安装好之后在angular.json的styles中添加样式
    "node_modules/bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css"

5.添加bpmn自定义属性列表

由于扩展使用官方提供的properties-panel,不能满足公司的业务要求。所以,大部分情况下需要自定义bpmn属性栏。
自定义propertiesPanel之前如果引用过原有的属性面板,则要去掉所有关于propertiesPanel引用的js与css,包括原有的属性面板模块propertiesPanelModule、propertiesProviderModule;如果有使用过bpmn原属性面板,则注释以下的引用。

  • bpmn完全自定义右侧属性面板,其实就是完全自己写template组件,自由开发,最大化满足需求,所有与画布的交互全部通过Modeler的实例对象(modeler);跟正常开发页面一样,先定义好自己的属性面板组件,传入Modeler以及其他参数进行使用;

    点击查看html
    <div class="container">
      <div class="header">
        <button nz-button nzType="primary" class="headerBtn" (click)="previewXML()">
          控制台预览xml
        </button>
      </div>
      <div class="canvas"></div>
      <!-- <div id="js-properties-panel" class="panel"></div> -->
      <app-custom-properties-panel
        [bpmnModeler]="bpmnModeler"
      ></app-custom-properties-panel>
    </div>
    
    
    点击查看部分js
    // 加载bpmn默认流程图
    loadBPMN() {
      const customTranslateModule = {
        translate: ['value', customTranslate],
      };
      this.bpmnModeler = new BpmnModeler({
        container: '.canvas',
        // 添加控制板
        propertiesPanel: {  // new Modeler() 时,必须传入配置项 propertiesPanel,并设置 parent 属性,用来指定侧边栏挂载的 DOM 节点
          parent: '#js-properties-panel',
        },
        additionalModules: [
          customTranslateModule, // 汉化模块
          paletteProvider, // 自定义palette
          contextPadProvider, // 自定义context-pad
          // 右边的属性栏
          // propertiesProviderModule,  // 表示的是属性栏里的内容, 也就是点击不同的element该显示什么内容
          // propertiesPanelModule  // 表示的是属性栏这个框, 就是告诉别人这里要有个属性栏
        ],
        moddleExtensions: {
          // camunda: CamundaModdleDescriptor // bpmn.js适配的是流程引擎Camunda,所以如果需要更加完整的属性输入框,也需安装camunda-bpmn-moddle,用于camunda数据对象
          activiti: activitiModdleExtension // 后端需要使用activiti引擎
        }
      })
      // 将字符串转换成图显示出来
      this.bpmnModeler.importXML(xmlStr);
    }
    
    点击查看部分custom-properties-panel(自定义属性栏)
    ngOnInit(): void {
      this.validateForm = this.fb.group({
        id: [{ value: null, disabled: true }],
        name: [null],
        category: [null],
        skipExpression: ['true'],
        documentation: [null],
      });
      this.loadModuler();
    }
    
    // 加载点击元素的属性
    loadModuler() {
      const { bpmnModeler } = this;
      bpmnModeler.on('selection.changed', (e: { newSelection: any[] }) => {
        this.element = e.newSelection[e.newSelection.length - 1]; //bpmn-js7+的版本,元素可多选。这里默认为多选最后点击的元素。
        if(this.element) {
          this.validateForm.patchValue({
            id: this.element?.id,
            name: this.element?.businessObject?.name,
            category: this.element?.businessObject?.category,
            skipExpression: this.element?.businessObject?.skipExpression ?? 'true',
            documentation: this.element?.businessObject?.documentation?.[0]?.text
          });
          if(this.validateForm.get('skipExpression')?.value === 'true') {
            this.changeField('true', 'skipExpression', 'select');
            this.changeShapeBgColor( '#66AA66');
          }
          this.cd.detectChanges();
        }
      })
    }
    
  • 如何同步修改自定义属性面板和bpmn流程图的值?
    其实就是通过,监听输入框的变化,通过modeling.updatePropertie方法进行修改流程图上的属性。

    /**
       * 输入框改变事件
       * @param event 
       * @param type 字段类型
       * @param formControlType 控件类型(可选)
       */
      changeField(event: any, type: any, formControlType?: any) {
        // if(event || event == null) {
          const value = formControlType == 'select' ? (Array.isArray(event) ? event.join() : event ) : (event.target.value ?? '');
          let properties: any = {};
          properties[type] = value;
          this.element[type] = value;
          this.updateProperties(properties);
        // }
      }
    
      // 更新元素属性 
      updateProperties(properties: any) {
        const { bpmnModeler, element } = this;
        const modeling = bpmnModeler.get('modeling');
        modeling.updateProperties(element, properties);
      }
    
    

6.bpmn页面汉化

  • 通过 https://github.com/bpmn-io/bpmn-js-examples/tree/master/i18n/app ,将 customTranslate文件夹复制到项目文件夹下,然后在app.component.ts中引入。

    import customTranslate from 'src/app/diagram/customTranslate/customTranslate'
    // 加载bpmn默认流程图
    loadBPMN() {
      const customTranslateModule = {
        translate: ['value', customTranslate],
      };
      this.bpmnModeler = new BpmnModeler({
        container: '.canvas',
        // 添加控制板
        propertiesPanel: {  // new Modeler() 时,必须传入配置项 propertiesPanel,并设置 parent 属性,用来指定侧边栏挂载的 DOM 节点
          parent: '#js-properties-panel',
        },
        additionalModules: [
          customTranslateModule, // 汉化模块
          paletteProvider, // 自定义palette
          contextPadProvider, // 自定义context-pad
          // 右边的属性栏
          // propertiesProviderModule,  // 表示的是属性栏里的内容, 也就是点击不同的element该显示什么内容
          // propertiesPanelModule  // 表示的是属性栏这个框, 就是告诉别人这里要有个属性栏
        ],
        moddleExtensions: {
          // camunda: CamundaModdleDescriptor // bpmn.js适配的是流程引擎Camunda,所以如果需要更加完整的属性输入框,也需安装camunda-bpmn-moddle,用于camunda数据对象
          activiti: activitiModdleExtension // 后端需要使用activiti引擎
        }
      })
      // 将字符串转换成图显示出来
      this.bpmnModeler.importXML(xmlStr);
    }
    

7.bpmn常用的api

  • 流程设计器编辑好的流程图形如何获取xml、svg?

    const {xml} = this.modeler.saveXML({format: true})
    const {svg} = this.modeler.saveSVG()
    
  • 拿到xml如何渲染成流程图?

    this.modeler.importXML(xml)
    
  • 如何让流程图自动居中、流程图缩放?

    this.modeler.get('canvas').zoom('fit-viewport', 'auto')//画布自适应居中
    this.modeler.get('canvas').zoom(2.0)//放大至2倍
    
  • 获取流程所有图形shape对象

    this.elementRegistry.getAll()[0].children
    
  • 新建流程时初始化的xml(xmlStr.ts)

    export var xmlStr:any = `
      <?xml version="1.0" encoding="UTF-8"?>
      <definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
        <process id="Process_start" isExecutable="true" flowable:formDisplay="0">
        </process>
        <bpmndi:BPMNDiagram id="BpmnDiagram_1">
          <bpmndi:BPMNPlane id="BpmnPlane_1" bpmnElement="Process_start">
          </bpmndi:BPMNPlane>
        </bpmndi:BPMNDiagram>
      </definitions>
      `
    
  • 设置图形shape节点的颜色

    this.modeling = this.modeler.get('modeling')
    this.modeling.setColor(shapes, { stroke: 'green' })
    //shapes可以是单个shape对象,也可以是shape对象数组
    
  • 通过图形id获取图形shape节点对象

    this.elementRegistry = this.modeler.get("elementRegistry")
    let shape = this.elementRegistry.get(shapeId)
    
  • 改变图形shape节点的某些属性

    this.modeling.updateProperties(shape,{ 
      name: '用户任务',
      loopCharacteristics: loopCharacteristics,//多实例
      extensionElements: extensions,//扩展属性
      'flowable:assignee': 'userId_123'//flowable前缀属性
    });
    
  • 获取根节点 bpmn:process

    this.modeler.getDefinitions().rootElements[0]
    
  • 鼠标选中节点图形事件

    this.modeler.on('selection.changed',  e => {
        const tempElement =e &&  e.newSelection &&  e.newSelection[0]
        if(tempElement && tempElement.type !="bpmn:Process"){
            this.currentElement = tempElement
        }
    })
    
  • 节点图形属性改变事件

    this.modeler.on('element.changed', e => {
        if(e.element && e.element.type!="bpmn:Process"){
            this.currentElement = e.element
        }
    })
    
  • 自动选中/取消选中图形事件

    //选中
    this.modeler.get('selection').select(shapes)
    //shapes参数为某个图形shape对象,也可以是图形数组[shape1,shape2,...],代表选中多个图形节点
    //注意:此方法会触发this.modeler.on('selection.changed', callback)事件
    
    //取消选中
    this.modeler.get('selection').deselect(shape)
    //注意:取消选中只能传单个element对象,不支持数组
    
  • bpmnjs怎么禁止节点拖动和编辑等功能变成只读模式?

  • bpmnjs如何改变element的border及background的颜色?

    // 启用改变事件
    skipExpressionChange(e: any) {
      if (e) {
        if (e && e != this.element.businessObject.skipExpression) {
          this.changeField(e, 'skipExpression', 'select');
          this.changeShapeBgOrBorderColor(e === 'true' ? '#B1ADAD' : '#000000', 'border');
        }
      }
    }
    
    // 改变shape背景或边框颜色值
    changeShapeBgOrBorderColor(color: string, type?: string) {
      const setting = type === 'background' ? { fill: color } : { stroke: color };
      this.element.type == 'bpmn:UserTask' && this.bpmnModeler.get('modeling').setColor(this.element, setting);
    }
    

8.参考资料

https://juejin.cn/post/6844904017567416328

http://www.seozhijia.net/vue/201.html

https://blog.csdn.net/qq_34532969/article/details/107539902

https://www.cnblogs.com/lemoncool/p/12964509.html

https://juejin.cn/post/7117481147277246500

https://juejin.cn/post/6900793894263488519

https://juejin.cn/post/6844904186304266253

https://juejin.cn/post/6912331982701592590

https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzU5MDY1MzcyOQ==&action=getalbum&album_id=1576254888626454529&scene=173&from_msgid=2247484449&from_itemidx=1&count=3&nolastread=1#wechat_redirect

posted @ 2023-01-13 10:50  小阿紫  阅读(1658)  评论(0)    收藏  举报