全网最详bpmn.js教材-properties-panel篇(下)

13,491 阅读14分钟

前言

Q: bpmn.js是什么? 🤔️

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

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

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

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

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

Properties-panel篇(下)

在上一章节主要介绍了如何在原有properties-panel的基础上进行扩展, 但是有很多小伙伴就会说我太嫌弃原有属性栏的样式了 😅...我是一名成熟的前端了, 我要有自己的想法...

OK... 我尊重你...这一章节霖呆呆就来教教大家怎样美化我们的properties-panel😊.

通过这一章节的阅读你可以学习到:

  • 修改属性栏的默认样式
  • 自定义properties-panel
  • 修改节点名称label属性
  • 修改节点颜色color属性
  • 修改event节点类型
  • 修改Task节点的类型
  • 初始化properties-panel并设置一些默认值

修改属性栏的默认样式

先来看看我们通过修改属性栏的默认样式可以实现什么样的效果🤔️吧!

绯红主题
绯红主题
科技蓝主题
科技蓝主题
极客黑主题
极客黑主题

如上👆所示, 你可以给属性栏定制不同的主题颜色, 来美化它原本的样子.

其实想要修改默认属性栏的样式, 非常简单, 只要打开控制台(Window: F12, Mac : option + command + i)通过审查元素, 找到各个元素的class, 然后在代码里覆盖它原有的属性就可以了.

还记得我们之前在项目的main.js中引用了properties-panel的样式吗?

// main.js

import 'bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css' // 右边工具栏样式

现在让我们在项目中创建一个styles文件夹, 同时创建一个bpmn-properties-theme-red.css文件, 里面将用来编写我们需要自定义修改的属性栏样式.

之后在main.js 中引用它, 最好是放在原有样式的后面:

// main.js

import 'bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css' // 右边工具栏样式
import './styles/bpmn-properties-theme-red.css' // 绯红主题

比如现在我想要修改一下属性栏头部的字体颜色:

通过审查元素找到这个类, 然后在bpmn-properties-theme-red.css中修改它:

.bpp-properties-header>.label {
    color: rgb(239, 112, 96);
    font-size: 16px;
}

保存再次打开页面就可以看到效果了.

当然我这里只是演示一下可以怎样去修改默认的样式, 所以只是用了最简单的css来演示. 这里其实有很大的扩展空间, 你可以用less或者sass来编写, 也可以自己实现一下主题切换等等的功能. 抛砖引玉希望能给你启发 😊...

如果你想偷会懒...直接取霖呆呆的样式也行...

上面👆案例的github地址:

《LinDaiDai/bpmn-vue-properties-panel》

自定义properties-panel

有时候你可能不满足用官方提供的properties-panel, 而是想要自定义一个属性栏, 这也是可以实现的.

比如我想要根据不同的节点类型, 在右边显示不同的属性配置, 并且编辑完之后可以同步更新到xml上.

自定义properties-panel
自定义properties-panel

其实实现的原理在之前的 《全网最详bpmn.js教材-properties篇》中也有提过了, 主要是利用updateProperties()这个方法来修改元素节点上的属性.

现在就让我们来看看如何封装一个这样的自定义属性栏吧😊.

前期准备

由于自定义属性栏的代码可能会很多, 而且可能还会涉及到很多复杂的业务组件, 所以我建议你将其从引入bpmn.js的地方给抽离出来, 也就是封装成一个通用的自定义属性栏组件.

组件的props

既然决定将其抽离成组件了, 那么这个组件的props应该设置成什么呢?

(props即父组件向子组件传递的值, 在这里父元素就是引入bpmn.js的地方, 子元素为自定义属性栏组件)

先来让我们理理我们的需求, 我们需要点击不同的元素来呈现不同的配置, 那么可以将单个element作为props传递进去.

不过后来在编写的过程中, 我发现有很多事件的绑定都是要涉及到modeler的, 若是将这些绑定事件都在父组件中完成不就违背了我们抽离出单独组件的意愿了吗🤔️?

所以在这里, 我是将整个modeler作为props来编写.这样不管是给modeler绑定事件还是给element绑定事件都很好做了.

OK...考虑好props, 让我们在components文件夹下创建一个custom-properties-panel的文件夹, 并在其中创建一个名为PropertiesView.vue的文件, 用来编写我们的自定义属性栏组件.

我们期望的这个组件是能够这样在html中使用:

<div class="containers" ref="content">
    <div class="canvas" ref="canvas"></div>
    <properties-view v-if="bpmnModeler" :modeler="bpmnModeler"></properties-view>
</div>

(bpmnModeler是你使用new BpmnModeler创建的modeler对象)

编写自定义属性栏组件

1. 组件结构

先来将这个组件的基础结构给搭好:

<!--PropertiesView.vue-->
<template>
    <div class="custom-properties-panel"></div>
</template>
<script>
export default {
    name: 'PropertiesView',
    props: {
        modeler: {
          type: Object,
          default: () => ({})
        }
    },
    data () {
        return {
            selectedElements: [], // 当前选择的元素集合
            element: null // 当前点击的元素
        }
    },
    created () {
        this.init()
    },
    methods: {
       init () {} 
    }
}
</script>
<style scoped></style>

2. 组件html代码

先让我给这个组件里添加点东西:

<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>name</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>

如上👆, 我增加了三个属性, id, name, customProps. 同时, 有一个selectedElements的判断.

这是因为我们在操作图形的时候, 如果你使用command + 左键(window上应该是Ctrl?)是可以选择多个节点的, 这时候就需要做一个判断.

3. 组件的js代码

如果你看多了霖呆呆写的代码, 你会发现我比较喜欢将一些初始化的代码提到一个叫做init()的函数中来, 这个是个人编码习惯哈...

在这里, 我们的初始化函数主要做以下几件事:

  • 使用selection.changed监听选中的元素;
  • 使用element.changed监听发生改变的元素.
init () {
 const { modeler } = this // 父组件传递进来的 modeler
  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 } 要更新的属性, 例如 { name: '', id: '' }
 */
updateProperties(properties) {
  const { modeler, element } = this
  const modeling = modeler.get('modeling')
  modeling.updateProperties(element, properties)
}

然后给属性栏上的input或者其它的控件, 增加一个@change事件, 当控件内的内容发生改变时, 同步更新element.

/**
* 改变控件触发的事件
* @param { Object } input的Event
* @param { String } 要修改的属性的名称
*/
changeField (event, type) {
  const value = event.target.value
  let properties = {}
  properties[type] = value
  this.element[type] = value
  this.updateProperties(properties) // 调用属性更新方法
}

4. 完整的组件代码

将上面👆的所有代码组合起来:

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

<style scoped>
/** 更多代码在git上有, git链接见底部后语 **/
.custom-properties-panel {
  position: absolute;
  right: 0;
  top: 0;
  width: 300px;
  background-color: #fff9f9;
  border-color: rgba(0, 0, 0, 0.09);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
  padding: 20px;
}
</style>

修改节点名称label属性

在上面的例子中, 我们演示了如果修改元素属性的, 如果你想要修改一个元素的label, 一种方式是像上面👆一样, 修改name这个属性, 或者用modeling.updateLabel这个方法更新也是一样的:

updateName(name) {
  const { modeler, element } = this
  const modeling = modeler.get('modeling')
  modeling.updateLabel(element, name)
  // 等同于 modeling.updateProperties(element, { name })
},

修改节点颜色color属性

如何让用户手动修改节点的颜色呢?

设置fill
设置fill

可以利用modeling.setColor这个方法.

比如我在代码中添加一行属性:

<fieldset class="element-item">
    <label>节点颜色</label>
    <input type="color" :value="element.color" @change="(event) => changeField(event, 'color')" />
  </fieldset>

然后改造以下changeField方法:

/**
 * 改变控件触发的事件
 * @param { Object } input的Event
 * @param { String } 要修改的属性的名称
 */
changeField(event, type) {
  const value = event.target.value
  let properties = {}
  properties[type] = value
  if (type === 'color') { // 若是color属性
    this.onChangeColor(value)
  }
  this.element[type] = value
  this.updateProperties(properties)
},
onChangeColor(color) {
  const { modeler, element } = this
  const modeling = this.modeler.get('modeling')
  modeling.setColor(element, {
    fill: color,
    stroke: null
  })
},

setColor这个方法接收两个属性:

  • fill: 节点的填充色
  • stroke: 节点边框的颜色和节点label的颜色

在上面我演示的是修改节点的填充色, 也就是fill, 当然你也可以改变stroke, 效果是这样的:

设置stroke
设置stroke

有意思的是, 如果你把fillstroke都设置成了color:

modeling.setColor(element, {
    fill: color,
    stroke: color
})

那么label标签就看不到了... 这是因为stroke也会改变label的颜色, 让它变得和fill一样.

设置fill和stroke
设置fill和stroke

不过一般你也不会将边框和填充内容设置成一个色吧...没必要...

如果你实在是想要解决这个问题的话, 这里有个不靠谱的做法, 就是在全局的css中, 将label的样式强行修改一下:

.djs-label {
    fill: #000!important;
}

修改event节点类型

有些时候, 我们可能还需要在自定义属性栏中修改这个节点的类型, 比如在开始节点, 点击contextPad上的小扳手:

实现这个功能我们需要用到bpmnReplace.replaceElement这个方法.

首先让我们看看event里这个属性是放在哪里的.

如下图: 我修改了一下开始节点的类型, 将它改为MessageEventDefinition

它对应是放在element.businessObject.eventDefinitions这个数组中的, 若是StartEventEndEvent, 则这个数组为undefinded.

让我们来看看这个功能怎么实现哈 😄.

首先在html加上修改event节点类型的下拉框:

<!--PropertiesView.vue-->
<template>
    <fieldset class="element-item" v-if="isEvent">
        <label>修改event节点类型</label>
        <select @change="changeEventType" :value="eventType">
          <option
            v-for="option in eventTypes"
            :key="option.value"
            :value="option.value"
          >{{ option.label }}</option>
        </select>
  </fieldset>
</template>
<script>
export default {
    data () {
        return {
            eventTypes: [
                { label: '默认', value: '' },
                { label: 'MessageEventDefinition', value: 'bpmn:MessageEventDefinition' },
                { label: 'TimerEventDefinition', value: 'bpmn:TimerEventDefinition' },
                { label: 'ConditionalEventDefinition', value: 'bpmn:ConditionalEventDefinition' }
          ],
          eventType: ''
        }
    },
    methods: {
        verifyIsEvent (type) { // 判断类型是不是event
            return type.includes('Event')
        },
        changeEventType (event) {}
    },
    computed: {
        isEvent() { // 判断当前点击的element类型是不是event
          const { element } = this
          return this.verifyIsEvent(element.type)
        }
    }
}
</script>

好了, 完成上面👆的基础代码, 主要逻辑就是在改变下拉框值的时候了:

changeEventType(event) { // 改变下拉框
  const { modeler, element } = this
  const value = event.target.value
  const bpmnReplace = modeler.get('bpmnReplace')
  this.eventType = value
  bpmnReplace.replaceElement(element, {
    type: element.businessObject.$type,
    eventDefinitionType: value
  })
},

现在改变下拉框的值, 就可以改变eventDefinitionType的值了, 不过还有一个问题, 就是你点击了其它的节点, 然后再次点回开始节点的时候, 下拉框的默认值就不对了, 也就是说我们还需要获取到这个开始节点本身的eventDefinitionType值.

这时候, 我们可以在selection.changed监听事件中做这类初始化properties-panel的事情.

init () {
    modeler.on('selection.changed', e => {
        this.selectedElements = e.newSelection
        this.element = e.newSelection[0]
        console.log(this.element)
        this.setDefaultProperties() // 设置一些默认的值
      })
}
setDefaultProperties() {
  const { element } = this
  if (element) {
    const { type, businessObject } = element
    if (this.verifyIsEvent(type)) { // 若是event类型
      // 获取默认的 eventDefinitionType
      this.eventType = businessObject.eventDefinitions ? businessObject.eventDefinitions[0]['$type'] : ''
    }
  }
}

修改Task节点的类型

event类型的节点我们已经知道怎么修改了, 那么对于Task类型的节点呢 🤔️?

其实做法都差不多.

同样, 让我们在html中加上针对Task类型的属性下拉框:

<!--PropertiesView.vue-->
<template>
    <fieldset class="element-item" v-if="isTask">
        <label>修改Task节点类型</label>
        <select @change="changeTaskType" :value="taskType">
          <option
            v-for="option in taskTypes"
            :key="option.value"
            :value="option.value"
          >{{ option.label }}</option>
        </select>
    </fieldset>
</template>
<script>
export default {
    data () {
        return {
            taskTypes: [
            { label: 'Task', value: 'bpmn:Task' },
            { label: 'ServiceTask', value: 'bpmn:ServiceTask' },
            { label: 'SendTask', value: 'bpmn:SendTask' },
            { label: 'UserTask', value: 'bpmn:UserTask' }
          ],
          taskType: ''
        }
    },
    methods: {
        verifyIsTask(type) {
            return type.includes('Task')
        },
        changeTaskType (event) {}
    },
    computed: {
        isTask() { // 判断当前点击的element类型是不是task
          const { element } = this
          return this.verifyIsTask(element.type)
        }
    }
}
</script>

然后在改变Task下拉框的时候:

changeTaskType(event) {
  const { modeler, element } = this
  const value = event.target.value // 当前下拉框选择的值
  const bpmnReplace = modeler.get('bpmnReplace')
  bpmnReplace.replaceElement(element, {
    type: value // 直接修改type就可以了
  })
}

初始化properties-panel并设置一些默认值

我们在设置自己的自定义属性栏的时候, 可能要根据不同的节点类型来做不同的业务逻辑判断, 并对properties-panel做一些默认值的设置, 比如上面👆的修改event类型, 这时候我们可以怎么样做呢 🤔️?

修改event类型一样, 我们可以在selection.changed监听事件中完成这个功能.

init () {
    modeler.on('selection.changed', e => {
        this.selectedElements = e.newSelection
        this.element = e.newSelection[0]
        console.log(this.element)
        this.setDefaultProperties() // 设置一些默认的值
      })
}
setDefaultProperties() {
  const { element } = this
  if (element) {
    // 这里可以拿到当前点击的节点的所有属性
    const { type, businessObject } = element
    // doSomeThing
  }
}

其实就是和上面👆介绍修改event类型的初始化一样, 不过我怕有的小伙伴直接跳过了修改event类型没有看到这一部分, 所以单独拎出来说下.

比如我们想要从Shape里获取到label然后同步到右侧的自定义属性栏里可以这样做:

setDefaultProperties里我们可以通过this.element拿到当前点击的这个元素, 将这个元素打印出来会发现, label实际上是businessObject对象中的name属性, 所以我们只需要做一下处理:

element['name'] = businessObject.name

这样你不管在修改图上面的label还是修改自定义属性栏里的name都会同步更新了, 具体可以看github中的代码.

replace的类型

在上面👆我们介绍了关于EventTask类型的元素是如何转化类型的, 案例中也仅仅演示了几种类型, 那么全部的类型到哪里看呢 🤔️?

你可以在bpmn.js的源码这里找到:

https://github.com/bpmn-io/bpmn-js/blob/develop/lib/features/replace/ReplaceOptions.js

你甚至可以直接到代码中将里面你要的内容导出:

import { START_EVENT } from 'bpmn-js/lib/features/replace/ReplaceOptions.js'

后语

上面👆教材案例的代码地址: LinDaiDai/bpmn-vue-properties-panel

截止到本章节, properties-panel算是介绍大概了, 不管是要使用原有的properties-panel还是使用自定义properties-panel我相信你都已经掌握了 😄...

在后续霖呆呆可能会根据bpmn.js源码来列举一些常用的属性和方法, 以便你更好的了解bpmn.js.

马上要过年了🧨了...

码完了这一章节, 霖呆呆也要开始整理回家的行李了 😄 ...

再次祝大家新年快乐呀~ 🔥 🎆

最后, 如果你也对bpmn.js 感兴趣可以进我们的bpmn.js交流群👇👇👇, 共同学习, 共同进步.

关注霖呆呆的公众号, 选择“其它”菜单中的“bpmn.js群”即可😊.

LinDaiDai公众号二维码.jpg
LinDaiDai公众号二维码.jpg

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

系列相关推荐:

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

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

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

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

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

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

《全网最详bpmn.js教材-封装组件篇》

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

《全网最详bpmn.js教材-properties-panel篇(上)》 ;