What is bpmn? Simple use of bpmn.js with source code


1. What is bpmn.js?

bpmn.js is a JavaScript-based library for creating, viewing and editing BPMN 2.0 flowcharts in web applications.

2. Use steps

1. Introduce bpmn

import BpmnModeler from "bpmn-js/lib/Modeler";
import {
    
     xmlStr } from "../mock/xmlStr";

2. Use bpmn

code show as below:

	//html
  <div class="containers">
    <div class="canvas" ref="canvas"></div>
  </div>
  
  //数据
    return {
    
    
      // bpmn建模器
      bpmnModeler: null,
      container: null,
      canvas: null
    };
//methods
    init() {
    
    
      // 获取到属性ref为“canvas”的dom节点
      const canvas = this.$refs.canvas;
      // 建模
      this.bpmnModeler = new BpmnModeler({
    
    
        container: canvas
      });
      this.createNewDiagram();
    },
    createNewDiagram() {
    
    
      // 将字符串转换成图显示出来
      console.log(xmlStr);
      this.bpmnModeler.importXML(xmlStr, err => {
    
    
        if (err) {
    
    
          // console.error(err)
        } else {
    
    
          // 这里是成功之后的回调, 可以在这里做一系列事情
          this.success();
        }
      });
    },
    success() {
    
    
      // console.log('创建成功!')
    }
  },
  mounted() {
    
    
    this.init();
  },
};
</script>
<style lang="scss" scoped>
.containers {
    
    
  position: absolute;
  background-color: #ffffff;
  width: 100%;
  height: 100%;
}
.canvas {
    
    
  width: 100%;
  height: 100%;
}
.panel {
    
    
  position: absolute;
  right: 0;
  top: 0;
  width: 300px;
}
</style>

The page effect is as shown in the figure:

3. Introduce bpmn-left toolbar

This is very convenient and can be introduced directly in main.js

// main.js中引入以下为bpmn工作流绘图工具的样式
import 'bpmn-js/dist/assets/diagram-js.css' // 左边工具栏以及编辑节点的样式
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css'
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css'
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css'

The effect is as shown in the figure:

4. Introduce bpmn-left toolbar

1. 安装bpmn-js-properties-panel插件
2. import 'bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css' // main.css中引入右边工具栏样式
3. 在页面中引入propertiesProviderModule和propertiesPanelModule
...
import propertiesPanelModule from 'bpmn-js-properties-panel'
import propertiesProviderModule from 'bpmn-js-properties-panel/lib/provider/camunda'
import camundaModdleDescriptor from 'camunda-bpmn-moddle/resources/camunda'
...
html结构
 <div class="containers">
    <div class="canvas" ref="canvas"></div>
    <div id="js-properties-panel" class="panel"></div>
  </div>

//在上边init基础上进行添加配置
 init() {
    
    
    // 获取到属性ref为“canvas”的dom节点
    const canvas = this.$refs.canvas
    // 建模
    this.bpmnModeler = new BpmnModeler({
    
    
      container: canvas,
      //添加控制板
      propertiesPanel: {
    
    
        parent: '#js-properties-panel'
      },
      additionalModules: [
        // 右边的属性栏
        propertiesProviderModule,
        propertiesPanelModule
      ],
      moddleExtensions: {
    
    
        camunda: camundaModdleDescriptor
      }
    })
    this.createNewDiagram()

loaded successfully

5. Introduce bpmn data export

之前的createNewDiagram事件就是用将数据显示出来,他的第一个参数就是xml数据,动态渲染在拿到后端返回的数据之后重新调用这个方法即可
 // 将字符串转换成图显示出来
      this.bpmnModeler.importXML(this.xmlStr, err => {
    
    
        if (err) {
    
    
          // console.error(err)
        } else {
    
    
          // 这里是成功之后的回调, 可以在这里做一系列事情
          this.success();//在success回调中绑定事件进行监听添加绑定事件
        }
      });
success(){
    
    
	 const that = this;
      // 给图绑定事件,当图有发生改变就会触发这个事件
      this.bpmnModeler.on("commandStack.changed", function() {
    
    
        that.saveDiagram(function(err, xml) {
    
    
          console.log(xml); // 这里获取到的就是最新的xml信息
        });
      });
}
  // 下载为bpmn格式,done是个函数,调用的时候传入的
    saveDiagram(done) {
    
    
      // 把传入的done再传给bpmn原型的saveXML函数调用
      this.bpmnModeler.saveXML({
    
     format: true }, function(err, xml) {
    
    
        done(err, xml);
      });
    }

6. Export data to svg format

Sometimes the data needs to be exported in svg format

首先在页面上定义好a标签用来下载数据
html
 <a ref="xml" href="javascript:;">xml</a>
 <a ref="svg" href="javascript:;">svg</a>

js
上边讲过从后端拿数据渲染之后有个success()回调
我们在这个回调里进行监听每次改变就会拿到xml数据,svg和它一样的,只需稍微改造一下
   const that = this;
   const downloadLink = this.$refs.xml;//首先获取页面上的a标签
   const downloadSvgLink = this.$refs.svg; 
     // 给图绑定事件,当图有发生改变就会触发这个事件
      this.bpmnModeler.on("commandStack.changed", function() {
    
    
      //每次更改页面都会获取到xml和svg类型的数据保存到href中备用
        that.saveDiagram(function(err, xml) {
    
    
          console.log(xml); // 这里获取到的就是最新的xml信息 saveDiagram返回的数据
          const data = encodeURIComponent(xml);
          downloadLink.href =
            "data:application/bpmn20-xml;charset=UTF-8," + data;
          downloadLink.download = "1.bpmn";
        });
        that.saveSvg(function(err, svg) {
    
    
          const data = encodeURIComponent(svg);
          console.log(svg); // 这里获取到的就是最新的xml信息 saveDiagram返回的数据
		  downloadSvgLink.href ="data:application/bpmn20-xml;charset=UTF-8," + data;
          downloadSvgLink.download = "1.svg";
        });
      });
 // 下载为bpmn格式,done是个函数,调用的时候传入的
    saveDiagram(done) {
    
    
      // 把传入的done再传给bpmn原型的saveXML函数调用
      this.bpmnModeler.saveXML({
    
     format: true }, function(err, xml) {
    
    
        done(err, xml);
      });
    },
	saveSvg(done) {
    
    
      this.bpmnModeler.saveSVG(done);
	}

7. Listen to modeler and bind events

sussec中调用下边这个方法 用来监听
this.addModelerListener()
  // 监听 modeler
    addModelerListener() {
    
    
      const bpmnjs = this.bpmnModeler;
      const that = this;
      // 用一个forEach给modeler上添加要绑定的事件
      const events = [
        "shape.added",
        "shape.move.end",
        "shape.removed",
        "connect.end",
        "connect.move"
      ];
      events.forEach(function(event) {
    
    
        that.bpmnModeler.on(event, e => {
    
    
          console.log(event, e);
          var elementRegistry = bpmnjs.get("elementRegistry");
          var shape = e.element ? elementRegistry.get(e.element.id) : e.shape;
          console.log(shape);
        });
      });
    },

7. Listen to element click...

 success() {
    
    
      console.log("创建成功!");
      this.addBpmnListener(); // 页面改变触发
      this.addModelerListener(); // 监听 modeler
      this.addEventBusListener(); //监听元素
    },
  addEventBusListener() {
    
    
      let that = this;
      const eventBus = this.bpmnModeler.get("eventBus"); // 需要使用eventBus
      const eventTypes = ["element.click", "element.changed"]; // 需要监听的事件集合
      eventTypes.forEach(function(eventType) {
    
    
        eventBus.on(eventType, function(e) {
    
    
          console.log(e);
        });
      });
    },

8. Customize the left toolbar icon

再以上的基础上去components文件夹创建文件
custom/CustomPalette.js 核心
index.js

insert image description here

// CustomPalette.js
export default class CustomPalette {
    
    
    constructor(bpmnFactory, create, elementFactory, palette, translate) {
    
    
        this.bpmnFactory = bpmnFactory;
        this.create = create;
        this.elementFactory = elementFactory;
        this.translate = translate;

        palette.registerProvider(this);
    }
    // 这个函数就是绘制palette的核心
    getPaletteEntries(element) {
    
    
        const {
    
    
            bpmnFactory,
            create,
            elementFactory,
            translate
        } = this;

        function createTask() {
    
    
            return function (event) {
    
    
                const businessObject = bpmnFactory.create('bpmn:Task');
                businessObject['custom'] = 1
                const shape = elementFactory.createShape({
    
    
                    type: 'bpmn:Task',
                    businessObject
                });
                console.log(shape) // 只在拖动或者点击时触发
                create.start(event, shape);
            }
        }
        return {
    
    
            'create.lindaidai-task': {
    
    
                group: 'model', // 分组名
                className: 'icon-custom lindaidai-task', // 样式类名
                title: translate('创建一个类型为lindaidai-task的任务节点'),
                action: {
    
     // 操作
                    dragstart: createTask(), // 开始拖拽时调用的事件
                    click: createTask() // 点击时调用的事件
                }
            }
        }

    }
}

CustomPalette.$inject = [
    'bpmnFactory',
    'create',
    'elementFactory',
    'palette',
    'translate'
]
-------------------------------------
// custom/index.js
import CustomPalette from './CustomPalette'

export default {
    
    
    __init__: ['customPalette'],
    customPalette: ['type', CustomPalette]
}

自定义定义完成在页面中引入 配置样式
创建css文件在main.js中全局引入 css名对应上即可
xx.css
/* app.css */
.bpmn-icon-task.red {
    
    
    color: #cc0000 !important;
}
.icon-custom {
    
    
    /* 定义一个公共的类名 */
    border-radius: 50%;
    background-size: 65%;
    background-repeat: no-repeat;
    background-position: center;
}

.icon-custom.lindaidai-task {
    
    
    /* 加上背景图 */
    background-image: url('https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/rules.png');
}
--------------------------
main.js
import '@/assets/a.css'
--------------------------

//xxx.vue 需要使用的页面
import customModule from "../components/custom";

   propertiesPanel对象中	//添加控制板
   ......
		propertiesPanel: {
    
    
          parent: "#js-properties-panel"
        },
        additionalModules: [
          // 左边工具栏以及节点
          propertiesProviderModule,
          // 自定义的节点!!!!在这里
          customModule,
          // 右边的工具栏
          propertiesPanelModule
        ],        

look at the effect

9. Customize the full effect of the left toolbar

The full version is for the convenience of reading and avoids confusion. The created file is completely independent from the eighth article. It is recommended to delete the eighth data and start over.

Here custom is the file created in the eighth step. Pay attention to distinguish
the newly created customModeler file as shown in the figure 4 js files
insert image description here

//1.CustomPalette.js
export default class CustomPalette {
    
    
    constructor(bpmnFactory, create, elementFactory, palette, translate) {
    
    
        this.bpmnFactory = bpmnFactory;
        this.create = create;
        this.elementFactory = elementFactory;
        this.translate = translate;

        palette.registerProvider(this);
    }

    getPaletteEntries(element) {
    
    
        const {
    
    
            bpmnFactory,
            create,
            elementFactory,
            translate
        } = this;

        function createTask() {
    
    
            return function (event) {
    
    
                const businessObject = bpmnFactory.create('bpmn:Task');//这里固定
                businessObject['custom'] = 1
                const shape = elementFactory.createShape({
    
    
                    type: 'bpmn:Task',//这里是自定义的节点名称昂!!!!
                    businessObject
                });
                console.log(shape) // 只在拖动或者点击时触发
                create.start(event, shape);
            }
        }

        return {
    
    
            'create.lindaidai-task': {
    
    
                group: 'model',
                className: 'icon-custom lindaidai-task',
                // className: 'bpmn-icon-user-task',
                title: translate('创建一个类型为lindaidai-task的任务节点'),
                action: {
    
    
                    dragstart: createTask(),
                    click: createTask()
                }
            }
        }
    }
}

CustomPalette.$inject = [
    'bpmnFactory',
    'create',
    'elementFactory',
    'palette',
    'translate'
]
//2.CustomRenderer.js

import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer';

import {
    
    
    append as svgAppend,
    attr as svgAttr,
    create as svgCreate
} from 'tiny-svg';
import {
    
     customElements, customConfig, hasLabelElements } from './util'
import {
    
     is } from 'bpmn-js/lib/util/ModelUtil';

const HIGH_PRIORITY = 1500

export default class CustomRenderer extends BaseRenderer {
    
    
    constructor(eventBus, bpmnRenderer, modeling) {
    
    
        super(eventBus, HIGH_PRIORITY);

        this.bpmnRenderer = bpmnRenderer;
        this.modeling = modeling;
    }

    canRender(element) {
    
    
        // ignore labels
        return !element.labelTarget;
    }

    drawShape(parentNode, element) {
    
    
        console.log(element)
        const type = element.type // 获取到类型
        if (customElements.includes(type)) {
    
     // or customConfig[type]
            const {
    
     url, attr } = customConfig[type]
            const customIcon = svgCreate('image', {
    
    
                ...attr,
                href: url
            })
            element['width'] = attr.width // 这里我是取了巧, 直接修改了元素的宽高
            element['height'] = attr.height
            svgAppend(parentNode, customIcon)
            // 判断是否有name属性来决定是否要渲染出label
            if (!hasLabelElements.includes(type) && element.businessObject.name) {
    
    
                const text = svgCreate('text', {
    
    
                    x: attr.x,
                    y: attr.y + attr.height + 20,
                    "font-size": "14",
                    "fill": "#000"
                })
                text.innerHTML = element.businessObject.name
                svgAppend(parentNode, text)
                console.log(text)
            }
            // this.modeling.resizeShape(element, {
    
    
            //     x: element.x,
            //     y: element.y,
            //     width: element['width'] / 2,
            //     height: element['height'] / 2
            // })
            return customIcon
        }
        // else if (type === 'bpmn:TextAnnotation' && element.businessObject.color) {
    
    
        //     console.log('我是绿色的')
        //     let color = element.businessObject.color
        //     element.businessObject.di.set('bioc:stroke', color)
        //     const shape = this.bpmnRenderer.drawShape(parentNode, element)
        //     return shape
        // }
        const shape = this.bpmnRenderer.drawShape(parentNode, element)
        return shape
    }

    getShapePath(shape) {
    
    
        return this.bpmnRenderer.getShapePath(shape);
    }
}

CustomRenderer.$inject = ['eventBus', 'bpmnRenderer', 'modeling'];
//3. index.js
import CustomPalette from './CustomPalette'
import CustomRenderer from './CustomRenderer'

export default {
    
    
    __init__: ['customPalette', 'customRenderer'],
    customPalette: ['type', CustomPalette],
    customRenderer: ['type', CustomRenderer]
}
//4.util.js
const customElements = ['bpmn:Task', 'bpmn:StartEvent'] // 自定义元素的类型
const customConfig = {
    
     // 自定义元素的配置
    'bpmn:Task': {
    
    
        'url': require('../../assets/www.png'),
        // 'url': require('../../assets/rules.png'),
        // 'url': 'https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/rules.png',
        'attr': {
    
     x: 0, y: 0, width: 48, height: 48 }
    },
    'bpmn:StartEvent': {
    
    
        'url': 'https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/start.png',
        'attr': {
    
     x: 0, y: 0, width: 40, height: 40 }
    }
}
const hasLabelElements = ['bpmn:StartEvent', 'bpmn:EndEvent'] // 一开始就有label标签的元素类型

export {
    
     customElements, customConfig, hasLabelElements }

Next, the Kangkang effect is ok . insert image description here
The icon is different because the util.js and the global style are different, just replace
it

10. Display custom node content on the right

insert image description here
Create component injection information
insert image description here


<style scoped lang="scss">
.custom-properties-panel {
    
    
  position: absolute;
  width: 300px;
  right: 20px;
  top: 20px;
  background-color: #fff9f9;
  border-color: rgba(0, 0, 0, 0.09);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
  padding: 20px;
  border-radius: 8px;
  background: skyblue;

  fieldset {
    
    
    border: 0;
  }
  input{
    
    
    border: none;
    outline: none;
    padding: 10px;
    border-radius: 5px;
  }
}
</style>
<template>
  <div class="custom-properties-panel">
    <div class="empty" v-if="selectedElements.length <= 0">请选择一个元素</div>
    <div class="empty" v-else-if="selectedElements.length > 1">只能选择一个元素</div>
    <div v-else>
      <fieldset class="element-item">
        <label>节点id:</label>
        <span>{
    
    {
    
     element.id }}</span>
      </fieldset>
      <fieldset class="element-item">
        <label>节点名称:</label>
        <input :value="element.name" @change="(event) => changeField(event, 'name')" />
      </fieldset>
      <!-- <fieldset class="element-item">
        <label>customProps:</label>
        <input :value="element.name" @change="(event) => changeField(event, 'customProps')" />
      </fieldset> -->
    </div>
  </div>
</template>

<script>
export default {
    
    
  name: 'PropertiesView',
  props: {
    
    
    modeler: {
    
    
      type: Object,
      default: () => ({
    
    })
    }
  },
  data() {
    
    
    return {
    
    
      selectedElements: [],
      element: null,
    }
  },
  created() {
    
    
    this.init()
  },
  methods: {
    
    
    init() {
    
    
      const {
    
     modeler } = this
      modeler.on('selection.changed', e => {
    
    
        this.selectedElements = e.newSelection
        this.element = e.newSelection[0]
      })
      modeler.on('element.changed', e => {
    
    
        const {
    
     element } = e
        const {
    
     element: currentElement } = this
        if (!currentElement) {
    
    
          return
        }
        // update panel, if currently selected element changed
        if (element.id === currentElement.id) {
    
    
          this.element = element
        }
      })
    },
    /**
    * 改变控件触发的事件
    * @param { Object } input的Event
    * @param { String } 要修改的属性的名称
    */
    changeField(event, type) {
    
    
      console.log(event, type);
      const value = event.target.value
      let properties = {
    
    }
      properties[type] = value
      this.element[type] = value
      this.updateProperties(properties)
    },

    updateName(name) {
    
    
      const {
    
     modeler, element } = this
      const modeling = modeler.get('modeling')
      // modeling.updateLabel(element, name)
      modeling.updateProperties(element, {
    
    
        name
      })
    },
    /**
     * 更新元素属性
     * @param { Object } 要更新的属性, 例如 { name: '' }
     */
    updateProperties(properties) {
    
    
      const {
    
     modeler, element } = this
      const modeling = modeler.get('modeling')
      modeling.updateProperties(element, properties)
    }
  }
}
</script>



Change the source code of the custom stylebook
demo according to your own needs


Summarize

There is also a version problem. The following is the version of this demo. If the version is too high, an error will be reported. If
you have any questions, you can leave a message
and hope it can help you.

    "bpmn-js": "^6.0.4",
    "bpmn-js-properties-panel": "^0.33.0",
    "camunda-bpmn-moddle": "^4.3.0",

Unfinished waiting ~✿✿ヽ(°▽°)ノ✿

Guess you like

Origin blog.csdn.net/regretTAT/article/details/131063107