事件与交互:建立反应性流程界面

bpmn.js 通过 EventBus(事件总线)来处理用户交互与内部状态变化。本文讲解如何监听事件、触发操作、与流程界面互动,从而构建反应性的流程应用

EventBus的基本概念

EventBus 是 diagram-js 的核心,所有交互与状态变化都会发出事件。通过监听事件,我们可以:

  • 捕获用户操作(点击、拖拽)
  • 响应内部变化(元素创建、属性修改)
  • 触发自定义逻辑(验证、通知、保存)

事件的三个阶段

1
2
3
4
5
前置事件       执行        后置事件
pre:xxx → (操作发生) → post:xxx

例:
pre:execute → (命令执行) → post:execute

常见事件与用法

1. 元素交互事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const eventBus = modeler.get('eventBus');

// 用户点击元素
eventBus.on('element.click', function(event) {
const element = event.element;
console.log('点击了:', element.id, element.businessObject.name);

// 显示属性面板
showProperties(element);
});

// 用户双击元素
eventBus.on('element.dblclick', function(event) {
const element = event.element;
console.log('双击了:', element.id);

// 进入编辑模式或打开详情页
editElement(element);
});

// 用户右键点击元素(上下文菜单)
eventBus.on('element.contextmenu', function(event) {
event.preventDefault();

const element = event.element;
console.log('右键菜单:', element.id);

// 显示自定义上下文菜单
showContextMenu(event, element);
});

// 鼠标悬停元素
eventBus.on('element.hover', function(event) {
const element = event.element;
console.log('悬停:', element.id);

// 高亮相关元素或显示提示
highlightRelatedElements(element);
});

// 鼠标离开元素
eventBus.on('element.out', function(event) {
const element = event.element;
console.log('离开:', element.id);

// 清除高亮
clearHighlight(element);
});

2. 选中事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
eventBus.on('selection.changed', function(event) {
const newSelection = event.newSelection;
const deselected = event.deselected;

console.log('选中元素:', newSelection.map(el => el.id));
console.log('取消选中:', deselected.map(el => el.id));

// 更新属性面板
if (newSelection.length > 0) {
const element = newSelection[0];
updatePropertiesPanel(element);
} else {
clearPropertiesPanel();
}
});

3. 元素生命周期事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 元素即将添加
eventBus.on('pre:shape.create', function(event) {
const shape = event.context.shape;
console.log('即将创建:', shape.type);

// 可在此进行验证或拦截
if (!isValidShape(shape)) {
event.stopPropagation();
console.log('✗ 创建被拒绝');
}
});

// 元素已添加
eventBus.on('shape.added', function(event) {
const shape = event.element;
console.log('✓ 元素已添加:', shape.id);
});

// 元素即将删除
eventBus.on('pre:shape.delete', function(event) {
const shape = event.context.shape;
console.log('即将删除:', shape.id);

// 可在此进行确认
if (!confirmDelete(shape)) {
event.stopPropagation();
}
});

// 元素已删除
eventBus.on('shape.removed', function(event) {
console.log('✓ 元素已删除:', event.element.id);
});

4. 连接事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 创建连线
eventBus.on('connection.created', function(event) {
const connection = event.connection;
console.log('✓ 连接已创建:', connection.source.id, '->', connection.target.id);

// 可添加默认标签或进行验证
updateConnectionLabel(connection);
});

// 连线即将移除
eventBus.on('pre:connection.delete', function(event) {
const connection = event.context.connection;
console.log('即将删除连接');

if (!confirmDeleteConnection(connection)) {
event.stopPropagation();
}
});

// 连线颜色指示
eventBus.on('connection.add', function(event) {
const connection = event.element;
if (connection.businessObject.conditionExpression) {
// 条件流用不同颜色
modeler.get('canvas').addMarker(connection, 'conditional');
}
});

5. 属性变更事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
eventBus.on('element.changed', function(event) {
const element = event.element;
console.log('元素属性已变更:', element.id);

// 例:当任务名称改变时
if (element.type === 'bpmn:UserTask') {
console.log('任务名称:', element.businessObject.name);

// 更新UI或触发保存
onElementChanged(element);
}
});

// 监听特定属性变更
eventBus.on('businessObject.updated', function(event) {
const bo = event.businessObject;
console.log('业务对象已更新:', bo);
});

6. 命令执行事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const commandStack = modeler.get('commandStack');

// 命令执行前
eventBus.on('pre:execute', function(event) {
const command = event.command;
console.log('即将执行命令:', command.context);
});

// 命令执行后
eventBus.on('post:execute', function(event) {
console.log('✓ 命令已执行');

// 触发自动保存
autoSaveProcess();
});

// 任何操作后(包括撤销/重做)
commandStack.on('post:execute', async () => {
console.log('流程已修改,准备保存...');
await saveProcessToServer();
});

实战案例:构建完整的编辑界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
class ProcessEditor {
constructor(containerId) {
this.modeler = new BpmnJS({ container: containerId });
this.setupEventListeners();
}

setupEventListeners() {
const eventBus = this.modeler.get('eventBus');
const commandStack = this.modeler.get('commandStack');

// 选中元素时显示属性
eventBus.on('selection.changed', (event) => {
if (event.newSelection.length > 0) {
this.showProperties(event.newSelection[0]);
} else {
this.clearProperties();
}
});

// 点击时高亮
eventBus.on('element.click', (event) => {
this.highlightElement(event.element);
});

// 任何修改后自动保存
commandStack.on('post:execute', async () => {
await this.autoSave();
});

// 右键菜单
eventBus.on('element.contextmenu', (event) => {
event.preventDefault();
this.showContextMenu(event, event.element);
});
}

showProperties(element) {
const panel = document.getElementById('properties-panel');
panel.innerHTML = `
<div class="property">
<label>ID:</label>
<span>${element.id}</span>
</div>
<div class="property">
<label>类型:</label>
<span>${element.type}</span>
</div>
<div class="property">
<label>名称:</label>
<input type="text" value="${element.businessObject.name || ''}"
onchange="editor.updateProperty(this)">
</div>
`;
}

clearProperties() {
document.getElementById('properties-panel').innerHTML = '';
}

highlightElement(element) {
// 移除之前的高亮
const canvas = this.modeler.get('canvas');
const registry = this.modeler.get('elementRegistry');

registry.getAll().forEach(el => {
canvas.removeMarker(el, 'highlight');
});

// 高亮当前元素
canvas.addMarker(element, 'highlight');
}

async autoSave() {
const { xml } = await this.modeler.saveXML();
console.log('✓ 自动保存成功');

// 发送到服务器
fetch('/api/process/save', {
method: 'POST',
body: JSON.stringify({ bpmn: xml })
});
}

showContextMenu(event, element) {
const menu = document.createElement('div');
menu.className = 'context-menu';
menu.innerHTML = `
<div onclick="editor.deleteElement()">删除</div>
<div onclick="editor.duplicateElement()">复制</div>
<div onclick="editor.showDocumentation()">文档</div>
`;

menu.style.left = event.event.pageX + 'px';
menu.style.top = event.event.pageY + 'px';
document.body.appendChild(menu);

// 点击其他地方关闭菜单
document.addEventListener('click', () => menu.remove());
}

deleteElement() {
const selected = this.modeler.get('selection').get();
if (selected.length > 0) {
this.modeler.get('modeling').removeElements(selected);
}
}
}

// 使用
const editor = new ProcessEditor('#canvas');

常用事件速查表

事件 触发时机 常见用途
element.click 用户点击元素 显示属性、高亮
element.dblclick 用户双击 编辑元素
element.contextmenu 右键菜单 自定义菜单
element.hover 鼠标悬停 获取焦点
element.out 鼠标离开 还原焦点
selection.changed 选中改变 更新UI
shape.added 元素添加 记录日志、初始化
shape.removed 元素删除 清理资源
connection.created 连线创建 验证、标记
element.changed 属性变更 实时保存
businessObject.updated 特定属性变更 监听特定属性变更
execute 任何操作 自动保存、刷新

小结

掌握事件系统:

  • ✓ 理解EventBus的工作原理
  • ✓ 监听常见交互事件
  • ✓ 使用事件触发自定义逻辑
  • ✓ 构建反应性的编辑界面
  • ✓ 实现自动保存与验证

便能构建智能、响应迅速的流程编辑工具

参考资料