bpmn-js,自定义palette,自定义ContextPad

1,451 阅读8分钟

BPMN.JS

前言

此文章是看过lindandan大佬后,结合自己的总结写的,侵权告知删。

链接:官网 社区 案例教程 流程图解释

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

img

快速使用

安装:npm i bpmn-js

import BpmnModeler from 'bpmn-js';
import testDiagram from './test-diagram.bpmn';  //导入bpmn文件var viewer = new BpmnModeler({
  container: '#canvas'
});
​
viewer.importXML(testDiagram, function(err) {
  if (!err) {
    console.log('success!');
    viewer.get('canvas').zoom('fit-viewport'); // 将画布缩放到适应视口的大小
  } else {
    console.log('something went wrong:', err);
  }
});
​
viewer.importXML(pizzaDiagram).then(function(result) {
  console.log('success !', result);
  viewer.get('canvas').zoom('fit-viewport');
}).catch(function(err) {
  console.log('something went wrong:', err);
});

自定义palette

创建CustomPalette.js文件

使用类的方式来自定义一个'工具箱'

export default class CustomPalette {
    constructor(bpmnFactory, create, elementFactory, palette, translate) {
        this.bpmnFactory = bpmnFactory; //用于获取、创建 bpmn内置的节点
        this.create = create;  //用于将节点渲染到页面的相关操作函数
        this.elementFactory = elementFactory;  //用于创建元素,如矩形、椭圆的节点
        this.translate = translate; //用于翻译模板字符串,通过内置的字典,配合 `xx{字典}`格式的字符串来插值替换的函数
        palette.registerProvider(this); //指定这是一个palette
    }
    // 内置的方法,用于自定义创建节点的相关操作
    getPaletteEntries(element) {
        const {
            bpmnFactory,
            create,
            elementFactory,
            translate
        } = this;
​
        function createTask() {}
        
        return {
            'create.pkp-task': { //名称:自定义,可随意
                group: 'model',  //分组,划分工具箱中节点的位置 tools 0?、event 1-3 、gateway 4、activity 5-6
                className: 'pkp-task',  //bpmn-icon-task: 内置图标
                // className: 'bpmn-icon-user-task',
                title: translate('创建一个类型为pkp-task的任务节点'),
                action: { //用来决定用户点击或拖动图标时会发生什么
                    dragstart: createTask(),
                    click: createTask()
                }
            }
        }
    }
}

image-20240527113815923分组

action

完成了上面的操作, 页面就能正常渲染出一个我们自定义的元素了, 但是在点击或者拖拽的时候是没有效果的,要实现效果就是定义 action属性

image-20240527111951506

// CustomPalette.js
function createTask() {
    return function(event) {
        //使用elementFactory工厂提供的方法创建一个形状,
        const shape = elementFactory.createShape({
            type: 'bpmn:Task', //内置的圆角矩形,除此之外还有 bpmn:StartEvent、bpmn:ServiceTask、bpmn:ExclusiveGateway
        });
        /*
            开始绘制跟随鼠标移动的节点,传入event和形状
            event:原生事件event对象,提供鼠标的位置等信息,供给绘制
            shape:指定绘制的形状
        */
        create.start(event, shape); 
    }
}

使用自定义的palette

创建一个统一的index.js用于公开暴露自定义的palette

// index.js
import CustomPalette from './CustomPalette'
export default {
    __init__: ['customPalette'], //__init__内的名称,在这个导出的对象中必须要有这个键,否则会报错
    customPalette: ['type', CustomPalette]
}

在创建 bpmn-js 的新实例时,使用 additionalModules 属性添加自定义控件:

//使用
import BpmnModeler from 'bpmn-js/lib/Modeler';
​
import customControlsModule from './custom';
​
const bpmnModeler = new BpmnModeler({
  additionalModules: [
    customControlsModule
  ]
});

扩展-自定义palette

自定义图标

/* .css */
/* 1.自定义颜色 */
.pkp-task{
    color: #cc0000 !important;
}
​
/* 2.自定义图标 */
.icon-custom.pkp-task { /* 加上背景图 */
    border-radius: 50%;
    background-size: 65%;
    background-repeat: no-repeat;
    background-position: center;
    background-image: url('https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/rules.png');
}

完全自定义palette

将自定义的palette类,使用paletteProvider属性去使用,即可替换掉原先默认的

// index.js
import CustomPalette from './CustomPalette'
export default {
    __init__: ['paletteProvider'], //__init__内的名称,在这个导出的对象中必须要有这个键,否则会报错
    paletteProvider: ['type', CustomPalette]
}

自定义renderer

仅仅只定义工具箱(palette)是不够的,绘制出来的东西还是默认的,通过自定义renderer可以自定义画布上的图形

//基本结构
import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer' // 引入默认的renderer
const HIGH_PRIORITY = 1500 // 最高优先级,默认1000
export default class CustomRenderer extends BaseRenderer { // 继承BaseRenderer
    constructor(eventBus, bpmnRenderer) {
        super(eventBus, HIGH_PRIORITY)
        this.bpmnRenderer = bpmnRenderer
    }
    //用于自定义判断元素是否能被渲染
    canRender(element) {
        return true
    }
​
    drawShape(parentNode, element) { // 默认调用
        const shape = this.bpmnRenderer.drawShape(parentNode, element)
        return shape
    }
​
    getShapePath(shape) {
        return this.bpmnRenderer.getShapePath(shape)
    }
}

写一些配置

// util.js
const customElements = ['bpmn:Task'] // 要自定义元素的类型
const customConfig = { // 自定义元素的配置(后面会用到)
    'bpmn:Task': {
        'url': 'https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/rules.png',
        'attr': { x: 0, y: 0, width: 48, height: 48 }
    }
}

绘制

import {
    append as svgAppend,
    attr as svgAttr,
    create as svgCreate
} from 'tiny-svg';


// 提供了一个默认父节点容器
function drawShape(parentNode, element) {
    const type = element.type // 获取到类型
    if (type==="bpmn:Task") {
        // 定义参数
        const attr = { x: 0, y: 0, width: 48, height: 48 }
        // 创建自定义图标
        const customIcon = svgCreate('image', {
            ...attr,
            href: "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/rules.png"
        })
        element['width'] = attr.width
        element['height'] = attr.height
        // 将自定义的图标插入提供的父节点内
        svgAppend(parentNode, customIcon)
        
        // 判断是否有name属性来决定是否要渲染出label
        if (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)
        }
        return customIcon
    }
    const shape = this.bpmnRenderer.drawShape(parentNode, element)
    return shape
}

使用自定义的renderer

//index.js
import CustomRenderer from './CustomRenderer'

export default {
    __init__: ['customRenderer'],
    customRenderer: ['type', CustomRenderer]
}

//使用
...
import customModule from './custom'
...
this.bpmnModeler = new BpmnModeler({
...
    additionalModules: [
        // 自定义的renderer
        customModule
    ]
})

自定义ContextPad

img

自定义contextPadpalette很像, 只不过是使用contextPad.registerProvider(this)来指定它是一个contextPad, 而自定义palette是用platette.registerProvider(this).

// CustomContextPad.js
export default class CustomContextPad {
    constructor(config, contextPad, create, elementFactory, injector, translate) {
        this.create = create;
        this.elementFactory = elementFactory;
        this.translate = translate;

        //如果没有设置自动配置为false,注入一个autoPlace的方法后面使用
        if (config.autoPlace !== false) {  
            this.autoPlace = injector.get('autoPlace', false);
        }

        contextPad.registerProvider(this); // 定义这是一个contextPad
    }
	//操作逻辑
    getContextPadEntries(element) {}
}

编写getContextPadEntries核心代码

function getContextPadEntries(element) {
    function appendTask(event, element) {
            if (autoPlace) {
                const shape = elementFactory.createShape({ type: 'bpmn:Task' });
                autoPlace.append(element, shape);
            } else {
                appendTaskStart(event, element);
            }
        }

        function appendTaskStart(event) {
            const shape = elementFactory.createShape({ type: 'bpmn:Task' });
            create.start(event, shape, element);
        }

    
    
    return {
        'append.pkp-task': {
            group: 'model',
            className: 'icon-custom pkp-task',
            title: translate('创建一个类型为pkp-task的任务节点'),
            action: {
                click: appendTask,
                dragstart: appendTaskStart
            }
        }
    };
}

完全自定义contextPadProvider

将自定义的padProvider类,使用contextPadProvider属性去使用,即可替换掉原先默认的

// index.js
import CustomPalette from './CustomPalette'
export default {
    __init__: ['contextPadProvider'], //__init__内的名称,在这个导出的对象中必须要有这个键,否则会报错
    contextPadProvider: ['type', CustomPalette]
}

编辑、删除节点篇

bpmnCustom20.png

// CustomContextPad.js
function getContextPadEntries(element) {
    const { modeling } = this   // 从this获取
    function removeElement(e) { // 点击的时候实现删除功能
        modeling.removeElements([element])
    }
    function updateElementName(){
        modeling.updateLabel(element,'修改后的名字')
    }
    return {
        'delete': {
            group: 'edit',
            className: 'icon-custom icon-custom-delete',
            title: translate('删除'),
            action: {
                click: removeElement
            }
        }
    }
}
/*.css */
.icon-custom-delete {
    background-image: url('https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/delete.png');
}

.icon-custom-delete.entry:hover {
    background: url('https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/delete.png') center no-repeat !important;
    background-size: cover !important;
}

线节点篇

bpmnCustom20.png

// CustomContextPad.js
function getContextPadEntries(element) {
    const { globalConnect } = this
    return {
        'global-connect-tool': {  //创建一条线
            group: 'tools',
            className: 'icon-custom icon-custom-flow',
            title: '新增线',
            action: {
                click: function(event){
                     globalConnect.toggle(event)
                }
            }
        }
    }
}

导入导出

BPMN element:

<startEvent id="StartEvent_1y45yut"></startEvent>
const canvas = document.getElementById('canvas')
const bpmnModeler = new BpmnModeler({
    container: canvas
})
// 导入xml/bpmn文件
bpmnModeler.importXML(xmlStr, (err) => {
    if (!err) {
        // 获取元素节点仓库
        let elementRegistry = bpmnModeler.get('elementRegistry')
        // 从元素节点仓库获取到元素节点
        let startEventElement = elementRegistry.get('节点Id')
        
        bpmnModeler.get('canvas').zoom('fit-viewport') //自动缩放合适比例
        
    }
})

// 导出成svg
bpmnModeler.saveSVG(function(err, data) {
    if(!err){
        // 把xml转换为URI,下载要用到的
      const encodedData = encodeURIComponent(data)
      // 下载图的具体操作,改变a的属性,className令a标签可点击,href令能下载,download是下载的文件的名字
      // 转换成file对象,可以通过URL.createObjectURL()方法将其转换为一个可以下载的
      let xmlFile = new File([data], 'test.bpmn')
      // 转换成a标签下载
      const link = document.createElement("a");
      link.className = 'active'
      link.href = 'data:application/bpmn20-xml;charset=UTF-8,' + encodedData
      link.download = '文件名.svg'
      link.click()
    }
})


// 导出成bpmn
bpmnModeler.saveXML({format:true} ,function(err, svg) {
    ...
})

Properties

1.什么是bpmn properties

bpmn.js画的每一个节点其实都被称之为diagram element(图表元素)

而在bpmn文件中的每个xml标签称之为BPMN element.

diagram elementBPMN element的一些属性关联起来靠的是一个叫做businessObject的属性. 从名称上理解你也可以知道它是一个对象(Object), 你可以在这个对象中添加上一些特殊的属性, 并且这些属性是可以直接插到BPMN element上的.

例如一个StartEvent节点

diagram element:

{
	id: "StartEvent_1y45yut",
	type: "bpmn:StartEvent",
	businessObject: {
		$type: "bpmn:StartEvent",
		name: "开始"
	}
}

BPMN element:

<startEvent id="StartEvent_1y45yut" name="开始"></startEvent>

2.使用js读取 bpmn properties

获取方式一:通过自定义renderer绘制图形的drawShape方法,通过参数获取到element对象

export default class CustomRenderer extends BaseRenderer {
	drawShape (parentNode, element) {
		const { businessObject } = element
	}
}

获取方式二:

let bpmnJS = new BpmnJS({
    container: '#canvas'
});
bpmnJS.importXML(xmlStr, function(err) {
    if (!err) {
        let elementRegistry = bpmnJs.get('elementRegistry')
        let startEventElement = elementRegistry.get('StartEvent_1y45yut'),
        startEvent = startEventElement.businessObject;
        console.log(startEvent.name) // 开始
    }
}

3.使用js修改 bpmn properties

<bpmn:sequenceFlow id="SequenceFlow_1" sourceRef="StartEvent_1" targetRef="Task_1" name="FOO &gt; BAR?">
	<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">
		<![CDATA[${foo > bar}]]>
    </bpmn:conditionExpression>
</bpmn:sequenceFlow>

手动创建元素

let moddle = bpmnJS.get('moddle');

// 创建一个BPMN element , 并且载入到导出的xml里
let newCondition = moddle.create('bpmn:FormalExpression', {
  body: '${ value > 100 }'
});

写入元素

// 写入属性, 但是不支持撤销 
sequenceFlow.conditionExpression = newCondition;

修改已有元素

let modeling = bpmnJS.get('modeling');
// 参数:目标元素  修改的内容
modeling.updateProperties(sequenceFlowElement, {
  conditionExpression: newCondition
});

阅读ruoyi-flowable-plus

image-20240528162848048

一、画布操作

1.对齐

const Align = bpmnModeler.get("alignElements")  //提供对齐操作的相关方法
const Selection = bpmnModeler.get("selection"); //用于获取当前选中的元素信息
const SelectedElements = Selection.get(); //获取所有选中的节点

Align.trigger(SelectedElements, 'left')); //对齐方式:left right top bottom center middle 

image-20240528164402318

2.缩放

bpmnModeler.get("canvas").zoom(0.2);
bpmnModeler.get("canvas").zoom('fit-viewport') //自动缩放合适比例
bpmnModeler.get("canvas").zoom('fit-viewport','auto') //自动缩放合适比例并居中

3.撤销

操作栈:commandStack

bpmnModeler.get("commandStack")//获取操作栈对象

  • canRedo 是否可恢复
  • canUndo 是否可撤销
  • redo 恢复上一步操作
  • undo 撤销上一步操作

4.清空

bpmnModeler.clear()

二、数据修改

要使生成的bpmn结构是正确的,进行数据修改本质上就是操作businessObject对象,而是要通过bpmn提供的方法去创建ModdleElement节点

image-20240528175017388

例如:要新增一个 ikuaijin:element 属性节点

const listenerObj = Object.create(null);
listenerObj.name="你好"

// 创建一个节点
const moddle = bpmnModeler.get("moddle")
const moddleElement = moddle.create(`hello:element`,listenerObj)

// 获取跟节点
const rootElements = bpmnModeler.getDefinitions().rootElements
rootElements.push(moddleElement)

// 修改节点属性
const moddling = bpmnModeler.get("modeling")
moddling.updateProperties

总结

操作对象汇总

获取操作方法的步骤:bpmnModeler.get("名称")

名称说明方法
alignElements提供对齐操作的相关方法trigger(选中的元素arr, 对齐方式)) 对齐方式:left right top bottom center middle
selection获取当前选中的元素对象信息get() 当前选中的元素信息
canvas可对画布相关操作zoom() 缩放画布
commandStack操作栈canRedo() 是否可恢复 canUndo() 是否可撤销 redo() 恢复上一步操作 undo() 撤销上一步操作
modeling对节点相关操作removeElements(元素) 移除元素 updateProperties(元素,内容) 修改元素属性
moddle提供节点创建方法create(标签名,内容)