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

最近工作项目中有用到bpmn做一个前端工作流编辑器,零零碎碎用了比较多的知识点,空闲下来了整理一下大致包含了bpmn的简单引入、添加属性面板、汉化、添加自定义属性面板、节点修改属性、左侧工具栏图标的增加修改、contexpad的修改、流程图完整性的校验等。示例代码存放地址https://github.com/Tamy0816/rt-angular-bpmn.git

1、angular引入bpmn.js

安装bpmn.js依赖包

npm install bpmn-js --save

添加bpmn样式
在angular.json的styles中添加样式文件

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

实现编辑器组件
组件模板 app.component.ts

import { Component, ViewChild, ElementRef, OnInit } from '@angular/core';
import BpmnModeler from 'bpmn-js/lib/Modeler';
const defaultXML = `<?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>`
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  public modeler: any;

  ngOnInit() {
    this.modeler = new BpmnModeler({
       container: '#el',
    });
    this.modeler.importXML(defaultXML);
  }
}

组件模板 app.component.html

<div id="el" style="width:100%; height: 1000px">
</div>

运行效果如图:
在这里插入图片描述

2、添加便捷工具

通过添加如下图小工具栏可以让画图更加便捷,可以撤销操作、放大缩小、下载xml格式文件或者图片文件
在这里插入图片描述
可以将小工具栏单独作为一个组件引入,设置样式绝对定位
添加EditingTools文件夹,文件结构如下:
在这里插入图片描述
editotTools.component.ts

import { Component, Output, EventEmitter } from 'node_modules/@angular/core';

@Component({
  selector: 'app-editing-tools',
  templateUrl: './editingTools.component.html',
  styleUrls: ['./editingTools.component.css'],
})
export class EditingToolsComponent {
  @Output() onUndo = new EventEmitter();
  @Output() onRedo = new EventEmitter();
  @Output() onZoomReset = new EventEmitter();
  @Output() onZoomIn = new EventEmitter();
  @Output() onZoomOut = new EventEmitter();
  @Output() onSave = new EventEmitter();
  @Output() onDownloadXml = new EventEmitter();
  @Output() onDownloadSvg = new EventEmitter();

  _onUndo() {
    this.onUndo.emit();
  }
  _onRedo() {
    this.onRedo.emit();
  }
  _onZoomReset() {
    this.onZoomReset.emit();
  }
  _onZoomIn() {
    this.onZoomIn.emit();
  }
  _onZoomOut() {
    this.onZoomOut.emit();
  }
  _onSave() {
    this.onSave.emit();
  }
  _onDownloadXml() {
    this.onDownloadXml.emit();
  }
  _onDownloadSvg() {
    this.onDownloadSvg.emit();
  }
}

editorTools.component.html

<div class="editingTools">
  <ul class="controlList">
    <li class="control">
      <button type="button" title="undo" (click)="_onUndo()">
        <i class="undo" ></i>
      </button>
    </li>
    <li class="control line">
      <button type="button" title="redo" (click)="_onRedo()">
        <i class="redo" ></i>
      </button>
    </li>

    <li class="control">
      <button type="button" title="reset zoom" (click)="_onZoomReset()">
        <i class="zoom" ></i>
      </button>
    </li>
    <li class="control">
      <button type="button" title="zoom in" (click)="_onZoomIn()">
        <i class="zoomIn" ></i>
      </button>
    </li>
    <li class="control line">
      <button type="button" title="zoom out" (click)="_onZoomOut()">
        <i class="zoomOut" ></i>
      </button>
    </li>

    <li class="control">
      <button type="button" title="save" (click)="_onSave()">
        <i class="save" ></i>
      </button>
    </li>
    <li class="control">
      <button type="button" title="download bpmn diagram" (click)="_onDownloadXml()">
        <i class="download" ></i>
      </button>
    </li>
    <li class="control">
      <button type="button" title="download as svg image" (click)="_onDownloadSvg()">
        <i class="image" ></i>
      </button>
    </li>
  </ul>
</div>

父组件添加子组件模板 app.component.html

<div id="el" style="width:100%; height: 1000px">
</div>
<app-editing-tools (onDownloadSvg)=handleDownloadSvg() (onDownloadXml)=handleDownloadXml() (onRedo)=handleRedo()
(onSave)=handleSave() (onUndo)=handleUndo() (onZoomIn)=handleZoom(0.1)
(onZoomOut)=handleZoom(-0.1) (onZoomReset)=handleZoom()></app-editing-tools>

父组件实现对应方法 app.component.ts

/**
  * 下载xml/svg
  *  @param  type  类型  svg / xml
  *  @param  data  数据
  *  @param  name  文件名称
  */
  download = (type, data, name) => {
    let dataTrack = '';
    const a = document.createElement('a');

    switch (type) {
      case 'xml':
        dataTrack = 'bpmn';
        break;
      case 'svg':
        dataTrack = 'svg';
        break;
      default:
        break;
    }

    name = name || `diagram.${dataTrack}`;

    a.setAttribute('href', `data:application/bpmn20-xml;charset=UTF-8,${encodeURIComponent(data)}`);
    a.setAttribute('target', '_blank');
    a.setAttribute('dataTrack', `diagram:download-${dataTrack}`);
    a.setAttribute('download', name);

    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
  };


  // 前进
  handleRedo = () => {
    this.modeler.get('commandStack').redo();
  };

  // 后退
  handleUndo = () => {
    this.modeler.get('commandStack').undo();
  };

  // 下载SVG格式
  handleDownloadSvg = () => {
    this.modeler.saveSVG({ format: true }, (err, data) => {
      this.download('svg', data, 'svg');
    });
  };

  // 下载XML格式
  handleDownloadXml = () => {
    this.modeler.saveXML({ format: true }, (err, data) => {
      this.download('xml', data, 'xml');
    });
  };

  // 流程图放大缩小
  handleZoom = (radio) => {
    const newScale = !radio
      ? 1.0 // 不输入radio则还原
      : this.state.scale + radio <= 0.2 // 最小缩小倍数
        ? 0.2
        : this.state.scale + radio;

    this.modeler.get('canvas').zoom(newScale);
    this.state.scale = newScale;
  };


  // 保存
  handleSave() {
    this.modeler.saveXML({format: true}, async (err, xml) => {
      console.log(xml);
    });
  }

3、添加bpmn自带属性面板

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"

在这里插入图片描述
在app.component.html中添加属性面板展示区域

 <app-editing-tools (onDownloadSvg)=handleDownloadSvg() (onDownloadXml)=handleDownloadXml() (onRedo)=handleRedo()
(onSave)=handleSave() (onUndo)=handleUndo() (onZoomIn)=handleZoom(0.1)
(onZoomOut)=handleZoom(-0.1) (onZoomReset)=handleZoom()></app-editing-tools>
<div class="row">
  <div id="el" style="float:left;width:70%; height: 1000px">
  </div>
  <div id="js-properties-panel" style="float:right;width:25%; height: 1000px"></div>
</div>

在组件文件夹下新建CamundaModdleDescriptor.ts文件
,将node_modules\camunda-bpmn-moddle\resources\camunda.json文件内容复制粘贴过来,前面加上export CamundaModdleDescriptor = 供组件导入使用在这里插入图片描述
在app.component.ts组件中引入CamundaModdleDescriptor.ts文件,在new BpmnModeler中添加moddleExtensions
在这里插入图片描述
运行后右侧属性面板 中即可显示Camunda流程引擎相关的内容了

4、bpmn.js页面汉化

官网示例:https://github.com/bpmn-io/bpmn-js-examples/tree/master/i18n/app,可以先clone代码到本地
在这里插入图片描述

再将示例中的customTranslate文件夹复制到项目文件夹下,然后在app.component.ts中引入,

 import customTranslate from './customTranslate/customTranslate';

 ngOnInit() {
    const customTranslateModule = {
      translate: ['value', customTranslate]
    };
    this.modeler = new BpmnModeler({
      container: '#el',
      propertiesPanel: {
        parent: '#js-properties-panel'
      },
      additionalModules: [
        propertiesProvider,
        propertiesPanelModule,
        customTranslateModule
      ],
      moddleExtensions: {
        camunda: CamundaModdleDescriptor
      }
    });
    this.modeler.importXML(defaultXML);
  }

再重启后就是汉化后的界面了
在这里插入图片描述

5、修改左侧工具栏(palatte)

在项目文件夹下面新建一个custom-palette文件夹,新建CustomPalette.js 和 index.js 文件, 可以参考node-modules下面的bpmn-js的palette如下图
在这里插入图片描述
文件内容如下:
CustomPalette.js这个文件里面的内容我们就可以根据自己需要添加或者删除一些工具图标,在官网地址https://www.npmjs.com/package/bpmn-font这个工程下面可以看到全部的bpmn-font,如下图:
在这里插入图片描述
CustomPalette.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 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
    },
    '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]
  };

然后在app.component.ts中引入paletteProvider,代码如下:

import paletteProvider from './custom-palette';
 ngOnInit() {
    const customTranslateModule = {
      translate: ['value', customTranslate]
    };
    this.modeler = new BpmnModeler({
      container: '#el',
      propertiesPanel: {
        parent: '#js-properties-panel'
      },
      additionalModules: [
        propertiesProvider,
        propertiesPanelModule,
        customTranslateModule,
        paletteProvider
      ],
      moddleExtensions: {
        camunda: CamundaModdleDescriptor
      }
    });
    this.modeler.importXML(defaultXML);
  }

去掉了一些不需要的工具图标之后我们的页面如下图,页面更加简约清爽了
在这里插入图片描述

6、修改context-pad

context-pad是我们在点击节点的时候弹出的一个方便选择下一节点的工具,当我们按照自己的需求修改了左侧palette工具栏之后,我们可能也会想一并修改这个context-pad的显示内容
在这里插入图片描述
修改方式与palette的修改方式大同小异,现在项目工程下文件夹下新建context-pad文件夹,添加ContextPadProvider.js、index.js文件,参考node-modules中的bpmn-js下面的context-pad文件夹,可以完全拷贝过来,
在这里插入图片描述
ContextPadProvider.js中顶部import的路径需要修改成绝对路径
在这里插入图片描述

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')
        ),
        // 根据项目需要添加了userTask内容
        '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;
  }

  if (deleteAllowed) {
    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;
}

然后就可以在app.component.ts中引入代码如下:

import contextPadProvider from './context-pad';

在这里插入图片描述
修改后的context-pad如下图,添加了userTask控件,删掉了一些不需要的
在这里插入图片描述

7、节点添加自定义属性

有些时候根据项目需求可能需要节点额外再添加一些属性,我们可以根据bpmn自带的updateProperties方法做到添加节点属性
先在页面添加一个区域添加一些表单,这里例子只写一个select下拉框了,给select添加change事件,在change事件中将选择的值更新到节点的属性上面去

app.component.html 代码如下
在这里插入图片描述
实现效果如下:
在这里插入图片描述

app.component.ts 实现selected方法,主要用到了updateProperties方法,我们可以用这个方法任意添加节点属性

 selected(value) {
    const modeling = this.modeler.get('modeling');
    modeling.updateProperties(this.element, {
      'custom-property': value
    });
  }

我们可以在保存打印的xml中看到custom-property已经被添加上去了
在这里插入图片描述

8、流程图完整性的校验

我们可以在流程图提交的时候,校验xml文件,发现一些比较基础的绘制错误告诉给用户,比如流程图没有开始或者结束节点,先就举这个例子,其它可以参考项目需要进行添加。我是用的将xml文件转为json格式做的校验。
先安装插件 xml2js,要使用xml2js,还需安装timers

npm install xml2js
npm install timers

在保存时添加如下方法做校验,当流程图缺少开始节点或者结束节点的时候抛错:

  handleSave() {
    console.log('save');
    this.modeler.saveXML({ format: true }, async (err, xml) => {
      console.log(xml);
      const parseString = xml2js.parseString;
      parseString(xml, (err, result) => {
        const bpmnData = result['bpmn2:definitions']['bpmn2:process'][0];
        if (!bpmnData['bpmn2:startEvent'] || !bpmnData['bpmn2:endEvent']) {
          alert('一个流程图必须有一个开始和结束节点');
        }
      });
    });
  }

在这里插入图片描述

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值