preface
ο£Ώ
Q: What is bPMn.js? π€ οΈ
“
Bpmn. js is a BPMN2.0 rendering toolkit and Web modeler that allows you to draw flowcharts in the front end.
ο£Ώ
Q: Why did I write this series? π€ οΈ
“
Due to the business needs of the company, bPMN. js is used in the project. However, as the developer of BPMN. js is a foreign friend, there are few textbooks and no detailed documents on this aspect in China. So a lot of ways to use a lot of pits have to find their own. After mulling it over, I decided to write a series of textbooks on it to help more bPMn.js users or developers who want to find a good way to draw flowcharts. It is also a kind of consolidation of their own.
Because it is a series of articles, so the update may be more frequent, if you accidentally brush and not what you need also please understand π.
Don’t ask for praise π don’t ask for heart β€οΈ. I only hope to have a little help for you.
Package Component
Before going into this section, I want you to know the previous sections: Customize palette, Customize Renderer, Customize contextPad, edit delete node.
This section will summarize the previous sections and provide a usable BPMN component solution.
By reading you can learn:
- Create a line node
- Custom modeler
- Encapsulate BPMN as a component
Create a line node
First let’s look at how a offline node is created.
I used CustomPalette. Js as an example π°, remember createTask from earlier, creating a line is similar to this:
// CustomPalette.js
PaletteProvider.$inject = [
...
'globalConnect'
]
PaletteProvider.prototype.getPaletteEntries = function(element) {
const { globalConnect } = this
function createConnect () {
return {
group: 'tools'.className: 'icon-custom icon-custom-flow'.title: 'New line'.action: {
click: function (event) {
globalConnect.toggle(event)
}
}
}
}
return {
'create.lindaidai-task': {... },'global-connect-tool': createConnect()
}
}
Copy the code
So I can draw a line:
Custom modeler
After the example above, it is not hard to see that in each key function, the desired customization is passed through the function return value.
And the return values are pretty much the same, like group, className, etc., so can we integrate them and save a lot of code?
We can build a function like this:
// CustomPalette.js
function createAction (type, group, className, title, options) {
function createListener (event) {
var shape = elementFactory.createShape(assign({ type }, options))
create.start(event, shape)
}
return {
group,
className,
title: 'new' + title,
action: {
dragstart: createListener,
click: createListener
}
}
}
Copy the code
It accepts the different attributes of all elements and returns a custom element.
But the line creation might be a little different:
// CustomPalette.js
function createConnect (type, group, className, title, options) {
return {
group,
className,
title: 'new' + title,
action: {
click: function (event) {
globalConnect.toggle(event)
}
}
}
}
Copy the code
So I’ve divided the functions that create elements into two categories: createAction and createConnect.
Next we just need to build an array like this:
// utils/util.js
const flowAction = { / / line
type: 'global-connect-tool'.action: ['bpmn:SequenceFlow'.'tools'.'icon-custom icon-custom-flow'.'Connection']}const customShapeAction = [ // shape
{
type: 'create.start-event'.action: ['bpmn:StartEvent'.'event'.'icon-custom icon-custom-start'.'Start node'] {},type: 'create.end-event'.action: ['bpmn:EndEvent'.'event'.'icon-custom icon-custom-end'.'End node'] {},type: 'create.task'.action: ['bpmn:Task'.'activity'.'icon-custom icon-custom-task'.'Normal mission'] {},type: 'create.businessRule-task'.action: ['bpmn:BusinessRuleTask'.'activity'.'icon-custom icon-custom-businessRule'.'businessRule task'] {},type: 'create.exclusive-gateway'.action: ['bpmn:ExclusiveGateway'.'activity'.'icon-custom icon-custom-exclusive-gateway'.'gateway'] {},type: 'create.dataObjectReference'.action: ['bpmn:DataObjectReference'.'activity'.'icon-custom icon-custom-data'.'variables']}]const customFlowAction = [
flowAction
]
export { customShapeAction, customFlowAction }
Copy the code
Also build a method to loop through the elements above π :
// utils/util.js
/** * loop creates a series of elements *@param {Array} Actions element collection *@param {Object} Fn handles the function */
export function batchCreateCustom(actions, fn) {
const customs = {}
actions.forEach(item= > {
customs[item['type']] = fn(... item['action'])})return customs
}
Copy the code
writeCustomPalette.js
code
You can then reference them in CustomPalette. Js:
// CustomPalette.js
import { customShapeAction, customFlowAction, batchCreateCustom } from '. /.. /.. /utils/util'
PaletteProvider.prototype.getPaletteEntries = function(element) {
var actions = {}
const {
create,
elementFactory,
globalConnect
} = this;
function createConnect(type, group, className, title, options) {
return {
group,
className,
title: 'new' + title,
action: {
click: function(event) {
globalConnect.toggle(event)
}
}
}
}
function createAction(type, group, className, title, options) {
function createListener(event) {
var shape = elementFactory.createShape(Object.assign({ type }, options))
create.start(event, shape)
}
return {
group,
className,
title: 'new' + title,
action: {
dragstart: createListener,
click: createListener
}
}
}
Object.assign(actions, { ... batchCreateCustom(customFlowAction, createConnect),/ / line. batchCreateCustom(customShapeAction, createAction) })return actions
}
Copy the code
It seems that the code is not a lot of simplification π.
Let’s see what the page looks like:
The toolbar on the left has been replaced with the image we want.
writeCustomRenderer.js
code
Then it’s time to code the Renderer. Before we do that, again, we can do some configuration items.
Because we notice that the svgCreate(‘image’, {}) method is used to render custom elements.
Inside it is also the address url and style configuration attR of an image received.
Then we can extract the url prefix:
// utils/util.js
const STATICPATH = 'https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/' // Static file path
const customConfig = { // Customize the configuration of elements
'bpmn:StartEvent': {
'field': 'start'.'title': 'Start node'.'attr': { x: 0.y: 0.width: 40.height: 40}},'bpmn:EndEvent': {
'field': 'end'.'title': 'End node'.'attr': { x: 0.y: 0.width: 40.height: 40}},'bpmn:SequenceFlow': {
'field': 'flow'.'title': 'Connection',},'bpmn:Task': {
'field': 'rules'.'title': 'Normal mission'.'attr': { x: 0.y: 0.width: 48.height: 48}},'bpmn:BusinessRuleTask': {
'field': 'variable'.'title': 'businessRule task'.'attr': { x: 0.y: 0.width: 48.height: 48}},'bpmn:ExclusiveGateway': {
'field': 'decision'.'title': 'gateway'.'attr': { x: 0.y: 0.width: 48.height: 48}},'bpmn:DataObjectReference': {
'field': 'score'.'title': 'variables'.'attr': { x: 0.y: 0.width: 48.height: 48}}}const hasLabelElements = ['bpmn:StartEvent'.'bpmn:EndEvent'.'bpmn:ExclusiveGateway'.'bpmn:DataObjectReference'] // Start with the element type of the label
export { STATICPATH, customConfig, hasLabelElements }
Copy the code
Then you just need to check when writing drawShape:
// CustomRenderer.js
import inherits from 'inherits'
import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer'
import {
append as svgAppend,
create as svgCreate
} from 'tiny-svg'
import { customElements, customConfig, STATICPATH, hasLabelElements } from '.. /.. /utils/util'
/** * A renderer that knows how to render custom elements. */
export default function CustomRenderer(eventBus, styles, bpmnRenderer) {
BaseRenderer.call(this, eventBus, 2000)
var computeStyle = styles.computeStyle
this.drawElements = function(parentNode, element) {
console.log(element)
const type = element.type // Get the type
if(type ! = ='label') {
if (customElements.includes(type)) { // or customConfig[type]
return drawCustomElements(parentNode, element)
}
const shape = bpmnRenderer.drawShape(parentNode, element)
return shape
} else {
element
}
}
}
inherits(CustomRenderer, BaseRenderer)
CustomRenderer.$inject = ['eventBus'.'styles'.'bpmnRenderer']
CustomRenderer.prototype.canRender = function(element) {
// ignore labels
return true
// return ! element.labelTarget;
}
CustomRenderer.prototype.drawShape = function(parentNode, element) {
return this.drawElements(parentNode, element)
}
CustomRenderer.prototype.getShapePath = function(shape) {
// console.log(shape)
}
function drawCustomElements(parentNode, element) {
const { type } = element
const { field, attr } = customConfig[type]
const url = `${STATICPATH}${field}.png`
const customIcon = svgCreate('image', {
...attr,
href: url
})
element['width'] = attr.width // I changed the width and height of the element directly
element['height'] = attr.height
svgAppend(parentNode, customIcon)
// Determine if there is a name attribute to render the 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)
}
return customIcon
}
Copy the code
The key is that in the drawCustomElements function, a string concatenation of the URL is utilized.
This way, the custom elements can be rendered.
The effect is as follows:
writeCustomContextProvider.js
code
Now that the Palette and renderer are ready, let’s see how the contextPad is written.
It is written similar to the palette, with one caveat:
Different types of nodes appearcontextPad
The content may be different.
Such as:
StartEvent
There will be aEdit, Delete, Task, BusinessRuleTask, ExclusiveGateway
And so on;EndEvent
Can only appearEdit, delete
;SequenceFlow
Can only appearEdit, delete
.
That is, we need to return a different contextPad depending on the node type.
When writing the return value of the getContextPadEntries function, it is possible to return different results based on element.type:
import { isAny } from 'bpmn-js/lib/features/modeling/util/ModelingUtil'
ContextPadProvider.prototype.getContextPadEntries = function(element) {...// The code omitted here can be seen in the project github source code
// An element that can only be generated by clicking on an element in the list
if (isAny(businessObject, ['bpmn:StartEvent'.'bpmn:Task'.'bpmn:BusinessRuleTask'.'bpmn:ExclusiveGateway'.'bpmn:DataObjectReference']) {Object.assign(actions, { ... batchCreateCustom(customShapeAction, createAction), ... batchCreateCustom(customFlowAction, createConnect),/ / cable
'edit': editElement(),
'delete': deleteElement()
})
}
// End node and line only delete and edit
if (isAny(businessObject, ['bpmn:EndEvent'.'bpmn:SequenceFlow'.'bpmn:DataOutputAssociation']) {Object.assign(actions, {
'edit': editElement(),
'delete': deleteElement()
})
}
return actions
}
Copy the code
IsAny is used to determine whether a type belongs to a later array, similar to includes.
Thus our contextPad is enriched π.
Encapsulate BPMN as a component
With the foundation of custom Modeler, we can encapsulate BPMN as a component that we can reference where we need to apply it.
For a better demonstration, I created a new project, bPMN-custom-Modeler, which has the same dependencies and configuration as bPMN-vue – Custom, but in this new project I plan to use custom Modeler to override the original. And encapsulate a BPMN component for the page to use.
preparation
Under the components folder of your project, create a new folder called BPMN, which will hold the packaged BPMN components.
Then we can also prepare an empty XML as the default display in the component (that is, what should be displayed if there are no graphics coming in). Here I define a newDiagram. Js.
Create a view file in the root directory to hold some page files. Here I will create a custom-modeler. Vue to reference the wrapped BPMN component to see the effect.
The component’sprops
If we want to encapsulate this as a component, we need to pass props to this component. It can be an entire XML string or the address of a BPMN file.
I use passing in the BPMN file address as an example for encapsulation. Of course, you can decide according to your business needs.
That is, when referencing this component, I expect it to say something like:
/* views/custom-modeler.vue */
<template>
<bpmn :xmlUrl="xmlUrl" @change="changeBpmn"></bpmn>
</template>
<script>
import { Bpmn } from '. /.. /components/bpmn'
export default {
components: {
Bpmn
},
data () {
return {
xmlUrl: 'https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmnMock.bpmn'}},methods: {
changeBpmn ($event) {}
}
}
</script>
Copy the code
As long as you reference the BPMN component and then pass a URL, the corresponding graphic content can be displayed on the page.
So, in bPMN. vue, we should define props like this:
// Bpmn.vue
props: {
xmlUrl: {
type: String.default: ' '}}Copy the code
Authoring componenthmtl
code
The HTML code in the component is very easy, basically giving the canvas a container to hold and defining two buttons for downloading:
<! -- Bpmn.vue -->
<template>
<div class="containers">
<div class="canvas" ref="canvas"></div>
<div id="js-properties-panel" class="panel"></div>
<ul class="buttons">
<li>
<a ref="saveDiagram" href="javascript:" title="Save as BPMN">Save as BPMN</a>
</li>
<li>
<a ref="saveSvg" href="javascript:" title="Save as SVG">Save as SVG</a>
</li>
</ul>
</div>
</template>
Copy the code
Authoring componentjs
code
In JS, I will be the previous several sections of “the most detailed bPMN.js textbook – HTTP request” and “the most detailed BPMN.js textbook – HTTP events” in the function are integrated in.
Basically:
- When you initialize it, the input is
xmlUrl
Determine if it is not null then request data, otherwise assign a default value; - After successful initialization, add in the successful function
modeler
,element
Listener event of; - Initialize the download
XML, SVG,
The link button.
Such as:
// Bpmn.vue
async createNewDiagram () {
const that = this
let bpmnXmlStr = ' '
if (this.xmlUrl === ' ') { // Check whether it exists
bpmnXmlStr = this.defaultXmlStr
this.transformCanvas(bpmnXmlStr)
} else {
let res = await axios({
method: 'get'.timeout: 120000.url: that.xmlUrl,
headers: { 'Content-Type': 'multipart/form-data'}})console.log(res)
bpmnXmlStr = res['data']
this.transformCanvas(bpmnXmlStr)
}
},
transformCanvas(bpmnXmlStr) {
// Convert the string to a graph for display
this.bpmnModeler.importXML(bpmnXmlStr, (err) => {
if (err) {
console.error(err)
} else {
this.success()
}
// Let the graph adapt to the screen
var canvas = this.bpmnModeler.get('canvas')
canvas.zoom('fit-viewport')
})
},
success () {
this.addBpmnListener()
this.addModelerListener()
this.addEventBusListener()
},
addBpmnListener () {},
addModelerListener () {},
addEventBusListener () {}
Copy the code
LinDaiDai/ bPMN-custom-Modeler/bPMn.vue
After the language
Project case Git address: LinDaiDai/ bPMN-custom-modeler please give a Starπ, thanks π
For the full catalogue of the series, please check here: “The most detailed BPMN.js textbook in the whole Web”.
Series related recommendations:
“The most detailed BPMN.JS textbook in the Whole Web – Basic Chapter”
The most detailed BPMN.js textbook in the whole web – HTTP Request
The most detailed BPMN.JS textbook in the whole Web – Events
The most detailed BPMN.js textbook on the Whole web – Renderer
The most detailed BPMN.js textbook -contextPad
“The most detailed BPMN. js textbook in the whole web – Editing and Deleting nodes”