前言
在前两篇文章 Bpmn.js 进阶指南之Rules操作校验规则(一) 和 Bpmn.js 进阶指南之Rules操作校验规则(二) 中,已经完整的解析了 bpmn.js
中的默认操作规则,以及整个规则模块的运作流程。这篇文章主要讲如何使用操作规则来自定义规则、影响页面内容。
1. 自定义规则
以 bpmn.js 的 AlignElements 元素对齐模块为例,自定义规则的规则定义和注册部分其实比较简单。
// 定义 规则初始化构造函数 export default function BpmnAlignElements(eventBus) { RuleProvider.call(this, eventBus); } // 注入 EventBus 事件总线依赖 BpmnAlignElements.$inject = [ 'eventBus' ]; // 继承 RuleProvider inherits(BpmnAlignElements, RuleProvider); // 实现 init 初始化方法,注册 elements.align 操作校验规则 BpmnAlignElements.prototype.init = function() { this.addRule('elements.align', function(context) { // 1. 获取上下文菜单中的元素实例数组 var elements = context.elements; // 2. 筛选元素,排除 label、connection、root 类型的元素 var filteredElements = filter(elements, function(element) { return !(element.waypoints || element.host || element.labelTarget); }); // 3. 筛选出在同一父元素中的元素 filteredElements = getParents(filteredElements); // 4. 判断符合条件的元素个数,如果小于2则不能进行对齐操作 if (filteredElements.length < 2) { return false; } // 5. 符合条件,返回一个可以转为 true 的值 return filteredElements; }); };
当然,这里也可以用 class 方式实现,个人也比较推荐 class 方式:
export default class BpmnAlignElements extends RuleProvider { constructor(eventBus) { super(eventBus); } init() { this.addRule('elements.align', function (context) { // ... }); } }
之后,在导出为一个 AdditionalModule 格式的模块:
// align-elements/index.ts import BpmnAlignElements from './BpmnAlignElements'; export default { __init__: [bpmnAlignElements], bpmnAlignElements: ['type', BpmnAlignElements] }
至此,自定义操作规则就结束了,在 Modeler 实例化完成之后,可以看到事件总线模块已经注册了一个 commandStack.elements.align.canExecute 事件。
2. 规则使用
使用已定义的规则,一般情况下有两种用途:
- 在某个事件/操作发生时进行判断,组织非法操作。这种方式通常是没有办法明显的体现在页面上的
- 根据规则,动态控制某个菜单的选项。这种方式一般用在 Palette,ContextPad 和 PopupMenu 上,通过对应的 Provider 来实现。
2.1 阻止操作
阻止某些操作的进行,一般不会让用户直接从页面上看到任何差异。
以 Palette 创建一个 StartEvent 节点为例,在点击节点图标开始拖拽的过程中,会同时触发 darg.move 和 create.move 两个事件,在这个期间,每次触发 create.move 都会在监听回调函数中判断元素是否可以创建。
核心逻辑如下:
eventBus.on([ 'create.move', 'create.hover' ], function(event) { // 获取事件上下文数据,拿到需要创建的 elements 元素实例数组 const context = event.context, elements = context.elements, hover = event.hover, source = context.source, hints = context.hints || {}; // 如果不是 hover 状态,则直接返回,由 create.end 事件的监听回调来处理 if (!hover) { context.canExecute = false; context.target = null; return; } // 计算当前位置 const position = { x: event.x, y: event.y }; // 判断是否可以正常执行创建操作 const canExecute = context.canExecute = hover && canCreate(elements, hover, position, source, hints); // 如果是正处于 hover 状态,则根据是否可以在当前位置创建/挂载设置对应的画布样式等 if (hover && canExecute !== null) { context.target = hover; if (canExecute && canExecute.attach) { setMarker(hover, MARKER_ATTACH); } else { setMarker(hover, canExecute ? MARKER_NEW_PARENT : MARKER_NOT_OK); } } })
这里只处理了 create.move 事件过程中的判断,主要用来改变画布样式,提示用户当前是否可以正常操作得到预想的结果。
在用户抬起鼠标按键结束拖拽过程时,则是由 create.end 事件的回调函数来进行处理。
在 Create 模块的源码中,官方是通过分别设置两个回调来处理的,分别用于结束 hover 状态和正式执行创建操作。核心逻辑如下:
// 1. 设置不同事件下的状态,结束 hover eventBus.on([ 'create.end', 'create.out', 'create.cleanup' ], function(event) { const hover = event.hover; if (hover) { setMarker(hover, null); } }); // 2. 正式执行创建操作 eventBus.on('create.end', function(event) { const context = event.context, source = context.source, shape = context.shape, elements = context.elements, target = context.target, canExecute = context.canExecute, attach = canExecute && canExecute.attach, connect = canExecute && canExecute.connect, hints = context.hints || {}; // 如果是禁止创建或者没有目标元素的时候直接返回 false if (canExecute === false || !target) { return false; } // 设置创建位置 const position = { x: event.x, y: event.y }; // 创建元素并添加到画布上 if (connect) { shape = modeling.appendShape(source, shape, position, target, { attach: attach, connection: connect === true ? {} : connect, connectionTarget: hints.connectionTarget }); } else { elements = modeling.createElements(elements, position, target, {...hints, attach}); shape = find(elements, (element) => !isConnection(element)); } // 更新上下文 assign(context, { elements, shape }); assign(event, { elements, shape }); });
而其中核心的 canCreate 方法,除了校验是否有目标元素、是否是连线类元素之外,就是通过调用 rules.allowed('shape.attach'), rules.allowed('shape.create'), rules.allowed('elements.create') 等规则来进行判断的
综上,整个创建过程中主要是在 create.move(也就是拖拽过程中)阶段进行的操作规则校验,并将校验结果保存到事件总线的上下文数据中;而 create.end 则是单纯的根据上下文中的规则校验结果,判断是否执行元素创建。
🚀 如果需要扩展该创建规则,可以通过注册 create.move 事件的监听函数,并设置较高的优先级来保证优先执行,可以设置上下文数据对象中的 hover 为 false 直接结束创建过程。
也可以通过继承 Create 构造函数,扩展 canCreate 方法来实现规则的扩展。
2.2 改变菜单
规则模块不仅可以用来阻止操作,也可以用来改变原有的菜单项。
这个过程需要配合对应的菜单项构造器 Provider 来实现。
这里我们假设有这样一个需求:在开始节点被选中时不显示 ContextPad 上下文菜单选项,在结束节点上的上下文菜单中禁止显示删除按钮。
因为这里需要把原有的 ContextPad 的一些选项禁用或者移除,所以只能是创建一个新的 ContextPadProvider 去覆盖官方原始构造器。但是为了减少代码量,可以直接继承官方构造器进行改造
// 配置禁止删除规则 class CustomDeleteRules extends RuleProvider { constructor(eventBus) { super(eventBus); } init() { this.addRule('elements.delete', function (context) { const elements = context.elements const endEvents = elements.filter(el => el.type === "bpmn:EndEvent") if(endEvents.length) { return false } return context }); } } // 1. 继承原始构造器 class RewriteContextPadProvider extends ContextPadProvider { constructor( config: any, injector: Injector, eventBus: EventBus, contextPad: ContextPad, modeling: Modeling, elementFactory: ElementFactory, connect: Connect, create: Create, popupMenu: PopupMenu, canvas: Canvas, rules: Rules, translate: Translate ) { super(config, injector, eventBus, contextPad, modeling, elementFactory, connect, create, popupMenu, canvas, rules, translate) // 2. 保留原有的菜单项入口 this._getContextPadEntries = super.getContextPadEntries } // 3. 实现自己的菜单项配置 getContextPadEntries(element: Base) { // 3.1 开始节点没有菜单项 if(element.type === 'bpmn:StartEvent') { return {} } // 3.2 其他节点保留原始菜单项 const actions = this._getContextPadEntries(element) // 3.3 判断是否可以执行删除规则(this._rules 继承自 ContextPadProvider) baseAllowed = this._rules.allowed('elements.delete', { elements: [element] }); if(baseAllowed) { delete actions.delete } // 3.4 返回菜单项 return actions } }