全网最详bpmn.js教材-自定义palette篇

前言

Q: bpmn.js是什么? 🤔️

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

Q: 我为什么要写该系列的教材? 🤔️

因为公司业务的需要因而要在项目中使用到bpmn.js,但是由于bpmn.js的开发者是国外友人, 因此国内对这方面的教材很少, 也没有详细的文档. 所以很多使用方式很多坑都得自己去找.在将其琢磨完之后, 决定写一系列关于它的教材来帮助更多bpmn.js的使用者或者是期于找到一种好的绘制流程图的开发者. 同时也是自己对其的一种巩固.

由于是系列的文章, 所以更新的可能会比较频繁, 您要是无意间刷到了且不是您所需要的还请谅解😊.

不求赞👍不求心❤️. 只希望能对你有一点小小的帮助.

所有教材的github地址: 《bpmn-chinese-document》

自定义Palette篇

经过前面几章的基础教程相信大家对bpmn.js的基本使用已经有了一个很好的掌握.

从这一章节开始我会讲解一些关于bpmn.js中自定义的部分, 包括自定义左侧工具栏、自定义渲染、自定义contextPad等等.

还是先来看一张图了解一下我们的绘图页面都有哪些东西:

image

这一章我要介绍的时候如何自定义左侧的工具栏(Palette, 也叫调色板), 通过阅读你可以学习到:

对于上面👆的目录, 其实隐含意思就是自定义Palette包括两种方式:

  • bpmn.js默认提供的Palette上进行修改(或者新增新的项)
  • 完全覆盖Palette中有的所有项, 自定义一个全新的Palette

在默认的Palette基础上修改

先来看看第一种最简单的, 我们在官方提供的调色板里新增一个自定义的项.

  • 元素类型: bpmn:Task
  • 元素名称: lindaidai-task
  • 样式: 沿用bpmn:Task原有的样式, 只不过将边框变为红色
  • 作用: 创建一个类型为lindaidai-task的任务节点

效果是这样的:

bpmnCustom1.png

如上所示, 只改变了任务框的颜色为红色, 所以效果不是很明显, 你甚至可以直接给它换一个样貌:

bpmnCustom2.png

接下来让我们看看该怎么实现它吧!🐶

前期准备

因为是新的章节, 这里我也新建一个项目:

$ vue create bpmn-vue-custom
$ npm i vue-router axios bpmn-js-properties-panel bpmn-js --save-D

按照之前的案例LinDaiDai/bpmn-vue-basic配置好相应的路由之类的东西.

components文件夹下新建一个名为custom-palette.vue的文件, 并将provider.vue(之前的一个基础案例) 的内容复制进去.

继续在components文件夹下新建文件夹custom用于盛放我们后面要写的一些自定义的东西.

来看看我们现在的项目结构:

image

我已经在custom文件夹新建立了一个CustomPalette.js, 接下来就是要在这里面写上我们要自定义的项.

编写CustomPalette.js代码

首先这个js是导出一个类(类的名称你可以随意取, 但是在引用的时候不能随意取, 后面会说到):

这里我就取为CustomPalette:

// 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) {}
}

CustomPalette.$inject = [
    'bpmnFactory',
    'create',
    'elementFactory',
    'palette',
    'translate'
]

上面👆的代码很好理解:

  • 定义一个类
  • 使用$inject注入一些需要的变量
  • 在类中使用palette.registerProvider(this)指定这是一个palette

定义完CustomPalette.js之后, 我们需要在其同级的index.js中将它导出:

// custom/index.js
import CustomPalette from './CustomPalette'

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

注:️
这里__init__中的名字就必须是customPalette了, 还有下面的属性名也必须是customPalette, 不然就会报错了.

同时要在页面中使用它:

<!--custom-palette.vue-->
<script>
...
import customModule from './custom'
...
this.bpmnModeler = new BpmnModeler({
...
    additionalModules: [
        // 左边工具栏以及节点
        propertiesProviderModule,
        // 自定义的节点
        customModule
    ]
})

</script>

编写核心函数getPaletteEntries代码

抛开这些不看, 重点就是如何构造这个getPaletteEntries函数

函数的名称你不能变, 不然会报错, 首先它返回的是一个对象, 对象中指定的就是你要自定义的项, 它大概长成这样:

// CustomPalette.js
getPaletteEntries(element) {
    return {
        'create.lindaidai-task': {
            group: 'model', // 分组名
            className: 'bpmn-icon-task red', // 样式类名
            title: translate('创建一个类型为lindaidai-task的任务节点'),
            action: { // 操作
                dragstart: createTask(), // 开始拖拽时调用的事件
                click: createTask() // 点击时调用的事件
            }
        }
    }
}

可以看到我定义的一项的名称就是: create.lindaidai-task. 它会有几个固定的属性:

  • group: 属于哪个分组, 比如tools、event、gateway、activity等等,用于分类
  • className: 样式类名, 我们可以通过它给元素修改样式
  • title: 鼠标移动到元素上面给出的提示信息
  • action: 用户操作时会触发的事件

接下来我们要做的无非就是:

  1. 通过className来设置样式
  2. 通过action来定义要触发的事情

编写className代码

我在scr的目录下新建了一个css文件, 里面用来盛放一些全局的样式, 并在main.js中引用这个全局样式:

// main.js
// 引入全局的css
import './css/app.css'

然后在其中加上一下样式:

/* app.css */
.bpmn-icon-task.red {
    color: #cc0000 !important;
}

上面👆的className我之所以要用bpmn-icon-task, 是因为这个类是bpmn.js中自带的一个iconfont类, 使用它就可以实现一个task的图标的效果:

image

由于iconfont是一个字体, 所以这里我使用color来改变它的颜色.

如果你想要给它完全换一张图片的话也可以用className来实现:

/* app.css */
.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');
}

然后修改create.lindaidai-task中的className:

// CustomPalette.js
 'create.lindaidai-task': {
    className: 'icon-custom lindaidai-task' 
 }

这样页面上显示的就是你定义的那张背景图了:

bpmnCustom4.png

编写action代码

完成了上面的操作, 其实页面已经能正常渲染出一个我们自定义的元素了, 但是你在点击或者拖拽它的时候是没有效果的💦.

此时我们期望的是点击或者拖拽它能在画布中画出一个lindaidai-task, 因此你得给它加上事件,
也就是编写一个函数用来创建bpmn:Task这个元素:

// CustomPalette.js
function createTask() {
    return function(event) {
        const businessObject = bpmnFactory.create('bpmn:Task');
        const shape = elementFactory.createShape({
            type: 'bpmn:Task',
            businessObject
        });
        console.log(shape) // 只在拖动或者点击时触发
        create.start(event, shape);
    }
}

这里的核心其实就是利用bpmn.js提供的一些方法创建shape然后将其添加到画布上.

(我这里演示的是创建一个类型为bpmn:Task的元素, 你还可以用来创建bpmn:StartEvent、bpmn:ServiceTask、bpmn:ExclusiveGateway等等...)

此时你拖动或者点击lindaidai-task就可以在页面上创建一个Task元素了.

image

我们看到虽然lindaidai-task在左侧工具栏中是金黄金黄的, 但是实际画到页面却还是呈现“裸体”状态😅, 这就和自定义渲染有关系了, 不要着急, 这些在后面的章节中会讲到.

完整的CustomPalette.js代码

让我们将上面的所有代码整合一下:

// 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'); // 其实这个也可以不要
                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'
]

项目案例Git地址: LinDaiDai/bpmn-vue-custom

注意: 项目案例里我为了方便演示, 在custom-palette中引入的是ImportJS/onlyPalette.js, 而上面的案例是以引入custom/index.js为讲解的, 这个自己要明白如何区分.

完全自定义Palette

可以看到, 上面👆的那种实现方式实际上就是定义了一个CustomPalette然后在new BpmnModeler生成的对象中引用进去.

但是这样做有一点不好👎, 那就是如果你不想要它提供的默认的这些项, 比如开始节点、结束节点、任务节点, 而是全都是自己自定义的, 就不能满足了. 比如这样:

bpmnCustom6.png

此时你就需要重写BpmnModeler这个类了, 实现自己独有的一套modeler.

前期准备

继续在上面👆的项目的基础上创建一个customModeler文件夹和一个custom-modeler.vue文件.
然后在customModeler中创建一个index.js和一个custom文件夹.

  • customModeler文件夹下的文件就是用来放自定义的modeler
  • custom-modeler.vue作为页面展示(记得配置页面的路由)

此时项目结构变成了:

bpmnCustom7.png

编写CustomPalette.js代码

这里的CustomPalette.js的编写方式就和第一种的有所不同了:

/**
 * A palette that allows you to create BPMN _and_ custom elements.
 */
export default function PaletteProvider(palette, create, elementFactory, globalConnect) {
    this.create = create
    this.elementFactory = elementFactory
    this.globalConnect = globalConnect

    palette.registerProvider(this)
}

PaletteProvider.$inject = [
    'palette',
    'create',
    'elementFactory',
    'globalConnect'
]

PaletteProvider.prototype.getPaletteEntries = function(element) { // 此方法和上面案例的一样
    const {
        create,
        elementFactory
    } = this;

    function createTask() {
        return function(event) {
            const shape = elementFactory.createShape({
                type: 'bpmn:Task'
            });
            console.log(shape) // 只在拖动或者点击时触发
            create.start(event, shape);
        }
    }

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

在这里是直接重写了PaletteProvider这个类, 同时覆盖了其原型上的getPaletteEntries方法, 从而达到覆盖原有的工具栏的效果.

(别看上面👆写的东西好像很多的样子, 但是其实静下心来看发现也没啥😊)

编写custom/index.js代码

接下来还是和第一种方式一样, 需要将我们自定义的Palette导出:

// custom/index.js
import CustomPalette from './CustomPalette'

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

这不过这里我们就不是用customPalette了, 而是直接用paletteProvider.

编写customModeler/index.js代码

最重要的一步, 就是编写CustomModeler这个类了:

import Modeler from 'bpmn-js/lib/Modeler'

import inherits from 'inherits'

import CustomModule from './custom'

export default function CustomModeler(options) {
    Modeler.call(this, options)

    this._customElements = []
}

inherits(CustomModeler, Modeler)

CustomModeler.prototype._modules = [].concat(
    CustomModeler.prototype._modules, [
        CustomModule
    ]
)

导出的类继承了Modeler这个核心的类, 这样就保证了其他功能的实现.

在页面上引用

最后一步, 是需要将我们原本通过BpmnModeler创建的对象改为通过我们自定义的CustomModeler来创建, 编写custom-modeler.vue.

<!--custom-modeler.vue-->
<script>
...
import CustomModeler from './customModeler'
...
this.bpmnModeler = new CustomModeler({ // 原本是用BpmnModeler
    ...
    additionalModules: [] // 可以不用引用任何东西
})

</script>

快来打开页面看看效果:

bpmnCustom8.png

后语

上面👆两个案例用的都是同一个项目🦐

项目案例Git地址: LinDaiDai/bpmn-vue-custom 喜欢的小伙伴请给个Star🌟呀, 谢谢😊

系列全部目录请查看此处: 《全网最详bpmn.js教材》

系列相关推荐:

《全网最详bpmn.js教材-基础篇》

《全网最详bpmn.js教材-http请求篇》

《全网最详bpmn.js教材-事件篇》

《全网最详bpmn.js教材-renderer篇》

《全网最详bpmn.js教材-contextPad篇》

《全网最详bpmn.js教材-编辑、删除节点篇》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容