@logicflow/extension 2.1.3 → 2.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logicflow/extension",
3
- "version": "2.1.3",
3
+ "version": "2.1.5",
4
4
  "description": "LogicFlow Extensions",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.js",
@@ -20,8 +20,8 @@
20
20
  "author": "Logicflow-Team",
21
21
  "license": "Apache-2.0",
22
22
  "peerDependencies": {
23
- "@logicflow/core": "2.1.2",
24
- "@logicflow/vue-node-registry": "1.1.2"
23
+ "@logicflow/vue-node-registry": "1.1.4",
24
+ "@logicflow/core": "2.1.3"
25
25
  },
26
26
  "dependencies": {
27
27
  "@antv/hierarchy": "^0.6.11",
@@ -32,8 +32,8 @@
32
32
  "preact": "^10.17.1",
33
33
  "rangy": "^1.3.1",
34
34
  "vanilla-picker": "^2.12.3",
35
- "@logicflow/core": "2.1.2",
36
- "@logicflow/vue-node-registry": "1.1.2"
35
+ "@logicflow/core": "2.1.3",
36
+ "@logicflow/vue-node-registry": "1.1.4"
37
37
  },
38
38
  "devDependencies": {
39
39
  "less": "^4.1.1",
@@ -2,6 +2,20 @@ import { getBpmnId } from './bpmnIds'
2
2
  import { handleAttributes, lfJson2Xml } from './json2xml'
3
3
  import { lfXml2Json } from './xml2json'
4
4
 
5
+ /**
6
+ * 模块说明(BPMN Adapter)
7
+ *
8
+ * 该模块负责在 LogicFlow 内部图数据(GraphData)与 BPMN XML/JSON 之间进行双向转换:
9
+ * - adapterOut:将 LogicFlow 图数据转换为 BPMN JSON(随后由 json2xml 转为 XML)
10
+ * - adapterIn:将 BPMN JSON 转换为 LogicFlow 图数据(如果是 XML,则先经 xml2json 转为 JSON)
11
+ *
12
+ * 设计要点与特殊处理:
13
+ * - BPMN XML 的属性在 JSON 中以前缀 '-' 表示(如 '-id'、'-name'),本模块严格遵循该约定。
14
+ * - XML 中同名子节点可能出现多次,xml2json 解析后会以数组表示;本模块对数组与单对象场景均做兼容处理。
15
+ * - BPMN 画布坐标以元素左上角为基准,而 LogicFlow 以元素中心为基准;转换时需进行坐标基准转换。
16
+ * - 文本内容在导出时进行 XML 转义,在导入时进行反转义,确保特殊字符(如 <, >, & 等)能被正确保留。
17
+ */
18
+
5
19
  import {
6
20
  ExclusiveGatewayConfig,
7
21
  StartEventConfig,
@@ -10,6 +24,12 @@ import {
10
24
  UserTaskConfig,
11
25
  } from '../bpmn/constant'
12
26
 
27
+ /**
28
+ * LogicFlow 节点配置(导入/导出过程中使用的中间结构)
29
+ * - id/type/x/y:节点基本信息
30
+ * - text:节点文本的中心坐标与内容(值为未转义的原始字符串)
31
+ * - properties:节点的额外属性(会保留到 BPMN 的扩展字段)
32
+ */
13
33
  type NodeConfig = {
14
34
  id: string
15
35
  properties?: Record<string, unknown>
@@ -23,11 +43,21 @@ type NodeConfig = {
23
43
  y: number
24
44
  }
25
45
 
46
+ /**
47
+ * 点坐标结构(用于边的路径点)
48
+ */
26
49
  type Point = {
27
50
  x: number
28
51
  y: number
29
52
  }
30
53
 
54
+ /**
55
+ * LogicFlow 边配置(导入/导出过程中使用的中间结构)
56
+ * - id/type/sourceNodeId/targetNodeId:边的基本信息
57
+ * - pointsList:边的路径点(用于 BPMN 的 di:waypoint)
58
+ * - text:边文本的位置与内容(值为未转义的原始字符串)
59
+ * - properties:边的扩展属性
60
+ */
31
61
  type EdgeConfig = {
32
62
  id: string
33
63
  sourceNodeId: string
@@ -50,6 +80,9 @@ type EdgeConfig = {
50
80
  properties: Record<string, unknown>
51
81
  }
52
82
 
83
+ /**
84
+ * BPMN 元素类型映射(用于在 JSON 中定位具体的 BPMN 节点类型)
85
+ */
53
86
  enum BpmnElements {
54
87
  START = 'bpmn:startEvent',
55
88
  END = 'bpmn:endEvent',
@@ -59,6 +92,11 @@ enum BpmnElements {
59
92
  FLOW = 'bpmn:sequenceFlow',
60
93
  }
61
94
 
95
+ /**
96
+ * BPMN 过程元素的标准属性键列表
97
+ * - 在解析 `processValue` 时,这些键会被视为标准属性而非扩展属性;
98
+ * - 其余未在列表中的键会进入 LogicFlow 的 `properties` 中,以保留扩展数据。
99
+ */
62
100
  const defaultAttrs = [
63
101
  '-name',
64
102
  '-id',
@@ -78,6 +116,10 @@ const defaultAttrs = [
78
116
  * 这意味着出现在这个数组里的字段当它的值是数组或是对象时不会被视为一个节点而是一个属性
79
117
  * @reference node type reference https://www.w3schools.com/xml/dom_nodetype.asp
80
118
  */
119
+ /**
120
+ * 导出至 BPMN JSON 时,作为属性保留的字段列表
121
+ * - 当这些字段的值为对象或数组时,仍视为属性(在 JSON 中以 '-' 前缀表示),而非子节点。
122
+ */
81
123
  const defaultRetainedFields = [
82
124
  'properties',
83
125
  'startPoint',
@@ -85,11 +127,39 @@ const defaultRetainedFields = [
85
127
  'pointsList',
86
128
  ]
87
129
 
130
+ /**
131
+ * XML 实体反转义:
132
+ * - 将常见的 XML 实体还原为字符:`&lt;`→`<`、`&gt;`→`>`、`&amp;`→`&`、`&quot;`→`"`、`&apos;`→`'`;保障流程图能正常回填
133
+ * - 使用 `String(text || '')` 规范化输入,避免 `null/undefined` 导致错误;
134
+ * - 注意:此实现为单次替换,若存在嵌套/二次编码(例如 `&amp;lt;`),会先还原为 `&lt;`,
135
+ * 如需完全解码,可在外层循环调用或调整替换顺序策略。
136
+ */
137
+ const unescapeXml = (text: string) =>
138
+ String(text || '')
139
+ .replace(/&lt;/g, '<')
140
+ .replace(/&gt;/g, '>')
141
+ .replace(/&amp;/g, '&')
142
+ .replace(/&quot;/g, '"')
143
+ .replace(/&apos;/g, "'")
144
+
145
+ /**
146
+ * 将普通 JSON 转换为 XML 风格 JSON(xmlJson)
147
+ * 输入:任意 JSON 对象;可选的保留属性字段 retainedFields
148
+ * 输出:遵循 XML 属性前缀约定的 xmlJson(属性键以 '-' 开头)
149
+ * 规则:
150
+ * - 原始字符串直接返回;数组逐项转换;对象根据键类型决定是否加 '-' 前缀。
151
+ * - 保留字段(fields)中出现的键以属性形式(带 '-')保留,否则视为子节点。
152
+ */
88
153
  function toXmlJson(retainedFields?: string[]) {
89
154
  const fields = retainedFields
90
155
  ? defaultRetainedFields.concat(retainedFields)
91
156
  : defaultRetainedFields
92
157
  return (json: string | any[] | Record<string, any>) => {
158
+ /**
159
+ * 递归转换核心方法
160
+ * @param obj 输入对象/数组/字符串
161
+ * @returns 转换后的 xmlJson
162
+ */
93
163
  function ToXmlJson(obj: string | any[] | Record<string, any>) {
94
164
  const xmlJson = {}
95
165
  if (typeof obj === 'string') {
@@ -123,7 +193,9 @@ function toXmlJson(retainedFields?: string[]) {
123
193
  }
124
194
 
125
195
  /**
126
- * 将xmlJson转换为普通的json,在内部使用。
196
+ * 将 XML 风格 JSON(xmlJson)转换回普通 JSON(内部使用)
197
+ * 输入:遵循 '-' 属性前缀约定的 xmlJson
198
+ * 输出:去除前缀并恢复原有结构的普通 JSON
127
199
  */
128
200
  function toNormalJson(xmlJson) {
129
201
  const json = {}
@@ -152,6 +224,18 @@ function toNormalJson(xmlJson) {
152
224
  * 2)如果只有一个子元素,json中表示为正常属性
153
225
  * 3)如果是多个子元素,json中使用数组存储
154
226
  */
227
+ /**
228
+ * 将 LogicFlow 图数据中的节点与边转换为 BPMN 的 process 数据结构
229
+ * 输入:
230
+ * - bpmnProcessData:输出目标对象(会被填充 '-id'、各 bpmn:* 节点以及 sequenceFlow)
231
+ * - data:LogicFlow 图数据(nodes/edges)
232
+ * - retainedFields:可选保留属性字段,用于控制属性与子节点的映射
233
+ * 输出:直接修改 bpmnProcessData
234
+ * 特殊处理:
235
+ * - 节点文本(node.text.value)作为 BPMN 的 '-name' 属性;
236
+ * - 维护 incoming/outgoing 的顺序,保证解析兼容性;
237
+ * - 多子元素时转为数组结构(XML 约定)。
238
+ */
155
239
  function convertLf2ProcessData(
156
240
  bpmnProcessData,
157
241
  data,
@@ -223,6 +307,16 @@ function convertLf2ProcessData(
223
307
  /**
224
308
  * adapterOut 设置bpmn diagram信息
225
309
  */
310
+ /**
311
+ * 将 LogicFlow 图数据转换为 BPMN 的图形数据(BPMNDiagram/BPMNPlane 下的 Shape 与 Edge)
312
+ * 输入:
313
+ * - bpmnDiagramData:输出目标对象(填充 BPMNShape/BPMNEdge)
314
+ * - data:LogicFlow 图数据(nodes/edges)
315
+ * 输出:直接修改 bpmnDiagramData
316
+ * 特殊处理:
317
+ * - 节点坐标从中心点转换为左上角基准;
318
+ * - 文本的显示边界(Bounds)根据文本长度近似计算,用于在 BPMN 渲染器正确定位标签。
319
+ */
226
320
  function convertLf2DiagramData(bpmnDiagramData, data) {
227
321
  bpmnDiagramData['bpmndi:BPMNEdge'] = data.edges.map((edge) => {
228
322
  const edgeId = edge.id
@@ -287,26 +381,36 @@ function convertLf2DiagramData(bpmnDiagramData, data) {
287
381
  /**
288
382
  * 将bpmn数据转换为LogicFlow内部能识别数据
289
383
  */
384
+ /**
385
+ * 将 BPMN JSON 转换为 LogicFlow 可识别的图数据
386
+ * 输入:
387
+ * - bpmnData:包含 'bpmn:definitions' 的 BPMN JSON
388
+ * 输出:{ nodes, edges }:LogicFlow 的 GraphConfigData
389
+ * 特殊处理:
390
+ * - 若缺失 process 或 plane,返回空数据以避免渲染错误。
391
+ */
290
392
  function convertBpmn2LfData(bpmnData) {
291
393
  let nodes: NodeConfig[] = []
292
394
  let edges: EdgeConfig[] = []
293
395
  const definitions = bpmnData['bpmn:definitions']
294
396
  if (definitions) {
397
+ // 如后续需多图/多流程支持,再扩展为遍历与合并,现在看起来是没有这个场景
295
398
  const process = definitions['bpmn:process']
399
+ const diagram = definitions['bpmndi:BPMNDiagram']
400
+ const plane = diagram?.['bpmndi:BPMNPlane']
401
+ if (!process || !plane) {
402
+ return { nodes, edges }
403
+ }
296
404
  Object.keys(process).forEach((key) => {
297
405
  if (key.indexOf('bpmn:') === 0) {
298
406
  const value = process[key]
299
407
  if (key === BpmnElements.FLOW) {
300
- const bpmnEdges =
301
- definitions['bpmndi:BPMNDiagram']['bpmndi:BPMNPlane'][
302
- 'bpmndi:BPMNEdge'
303
- ]
408
+ const edgesRaw = plane['bpmndi:BPMNEdge']
409
+ const bpmnEdges = Array.isArray(edgesRaw) ? edgesRaw : edgesRaw
304
410
  edges = getLfEdges(value, bpmnEdges)
305
411
  } else {
306
- const shapes =
307
- definitions['bpmndi:BPMNDiagram']['bpmndi:BPMNPlane'][
308
- 'bpmndi:BPMNShape'
309
- ]
412
+ const shapesRaw = plane['bpmndi:BPMNShape']
413
+ const shapes = Array.isArray(shapesRaw) ? shapesRaw : shapesRaw
310
414
  nodes = nodes.concat(getLfNodes(value, shapes, key))
311
415
  }
312
416
  }
@@ -318,6 +422,14 @@ function convertBpmn2LfData(bpmnData) {
318
422
  }
319
423
  }
320
424
 
425
+ /**
426
+ * 根据 BPMN 的 process 子节点与 plane 中的 BPMNShape 生成 LogicFlow 节点数组
427
+ * 输入:
428
+ * - value:当前类型(如 bpmn:userTask)的值,可能为对象或数组
429
+ * - shapes:plane['bpmndi:BPMNShape'],可能为对象或数组
430
+ * - key:当前处理的 BPMN 类型键名(如 'bpmn:userTask')
431
+ * 输出:LogicFlow 节点数组
432
+ */
321
433
  function getLfNodes(value, shapes, key) {
322
434
  const nodes: NodeConfig[] = []
323
435
  if (Array.isArray(value)) {
@@ -349,10 +461,23 @@ function getLfNodes(value, shapes, key) {
349
461
  return nodes
350
462
  }
351
463
 
464
+ /**
465
+ * 将单个 BPMNShape 与其对应的 process 节点合成为 LogicFlow 节点配置
466
+ * 输入:
467
+ * - shapeValue:plane 中的 BPMNShape(包含 Bounds 与可选 BPMNLabel)
468
+ * - type:BPMN 节点类型键(如 'bpmn:userTask')
469
+ * - processValue:process 中对应的节点对象(包含 '-id'、'-name' 等)
470
+ * 输出:LogicFlow NodeConfig
471
+ * 特殊处理:
472
+ * - 坐标从左上角转为中心点;
473
+ * - 文本从 '-name' 读取并进行 XML 实体反转义;
474
+ * - 文本位置优先使用 BPMNLabel 的 Bounds。
475
+ */
352
476
  function getNodeConfig(shapeValue, type, processValue) {
353
477
  let x = Number(shapeValue['dc:Bounds']['-x'])
354
478
  let y = Number(shapeValue['dc:Bounds']['-y'])
355
- const name = processValue['-name']
479
+ // 反转义 XML 实体,确保导入后文本包含特殊字符时能被完整还原
480
+ const name = unescapeXml(processValue['-name'])
356
481
  const shapeConfig = BpmnAdapter.shapeConfigMap.get(type)
357
482
  if (shapeConfig) {
358
483
  x += shapeConfig.width / 2
@@ -399,6 +524,13 @@ function getNodeConfig(shapeValue, type, processValue) {
399
524
  return nodeConfig
400
525
  }
401
526
 
527
+ /**
528
+ * 根据 BPMN 的 sequenceFlow 与 BPMNEdge 生成 LogicFlow 边数组
529
+ * 输入:
530
+ * - value:process['bpmn:sequenceFlow'],对象或数组
531
+ * - bpmnEdges:plane['bpmndi:BPMNEdge'],对象或数组
532
+ * 输出:LogicFlow 边数组
533
+ */
402
534
  function getLfEdges(value, bpmnEdges) {
403
535
  const edges: EdgeConfig[] = []
404
536
  if (Array.isArray(value)) {
@@ -427,11 +559,31 @@ function getLfEdges(value, bpmnEdges) {
427
559
  return edges
428
560
  }
429
561
 
562
+ /**
563
+ * 将单个 BPMNEdge 与其对应的 sequenceFlow 合成为 LogicFlow 边配置
564
+ * 输入:
565
+ * - edgeValue:BPMNEdge(包含 di:waypoint 以及可选 BPMNLabel/Bounds)
566
+ * - processValue:sequenceFlow(包含 '-id'、'-sourceRef'、'-targetRef'、'-name' 等)
567
+ * 输出:LogicFlow EdgeConfig
568
+ * 特殊处理:
569
+ * - 文本从 '-name' 读取并进行 XML 实体反转义;
570
+ * - 若缺失 BPMNLabel,则以边的几何中心近似作为文本位置;
571
+ * - pointsList 由 waypoints 转换得到,数值类型统一为 Number。
572
+ */
430
573
  function getEdgeConfig(edgeValue, processValue): EdgeConfig {
431
574
  let text
432
- const textVal = processValue['-name'] ? `${processValue['-name']}` : ''
575
+ // 反转义 XML 实体,确保导入后文本包含特殊字符时能被完整还原
576
+ const textVal = processValue['-name']
577
+ ? unescapeXml(`${processValue['-name']}`)
578
+ : ''
433
579
  if (textVal) {
434
- const textBounds = edgeValue['bpmndi:BPMNLabel']['dc:Bounds']
580
+ let textBounds
581
+ if (
582
+ edgeValue['bpmndi:BPMNLabel'] &&
583
+ edgeValue['bpmndi:BPMNLabel']['dc:Bounds']
584
+ ) {
585
+ textBounds = edgeValue['bpmndi:BPMNLabel']['dc:Bounds']
586
+ }
435
587
  // 如果边文本换行,则其偏移量应该是最长一行的位置
436
588
  let textLength = 0
437
589
  textVal.split('\n').forEach((textSpan) => {
@@ -440,10 +592,26 @@ function getEdgeConfig(edgeValue, processValue): EdgeConfig {
440
592
  }
441
593
  })
442
594
 
443
- text = {
444
- value: textVal,
445
- x: Number(textBounds['-x']) + (textLength * 10) / 2,
446
- y: Number(textBounds['-y']) + 7,
595
+ if (textBounds) {
596
+ text = {
597
+ value: textVal,
598
+ x: Number(textBounds['-x']) + (textLength * 10) / 2,
599
+ y: Number(textBounds['-y']) + 7,
600
+ }
601
+ } else {
602
+ // 兼容缺少 BPMNLabel 的图:使用边的几何中心作为文本位置
603
+ const waypoints = edgeValue['di:waypoint'] || []
604
+ const first = waypoints[0]
605
+ const last = waypoints[waypoints.length - 1] || first
606
+ const centerX =
607
+ (Number(first?.['-x'] || 0) + Number(last?.['-x'] || 0)) / 2
608
+ const centerY =
609
+ (Number(first?.['-y'] || 0) + Number(last?.['-y'] || 0)) / 2
610
+ text = {
611
+ value: textVal,
612
+ x: centerX,
613
+ y: centerY,
614
+ }
447
615
  }
448
616
  }
449
617
  let properties
@@ -474,6 +642,14 @@ function getEdgeConfig(edgeValue, processValue): EdgeConfig {
474
642
  return edge
475
643
  }
476
644
 
645
+ /**
646
+ * BpmnAdapter:基础适配器
647
+ *
648
+ * 作用:在 LogicFlow 数据与 BPMN JSON 之间进行转换,并注入 adapterIn/adapterOut 钩子。
649
+ * - processAttributes:导出时 BPMN process 的基础属性(可配置 isExecutable、id 等)。
650
+ * - definitionAttributes:导出时 BPMN definitions 的基础属性与命名空间声明。
651
+ * - shapeConfigMap:不同 BPMN 元素类型的默认宽高,用于坐标/Bounds 计算。
652
+ */
477
653
  class BpmnAdapter {
478
654
  static pluginName = 'bpmn-adapter'
479
655
  static shapeConfigMap = new Map()
@@ -494,6 +670,11 @@ class BpmnAdapter {
494
670
  [key: string]: any
495
671
  }
496
672
 
673
+ /**
674
+ * 构造函数
675
+ * - 注入 LogicFlow 的 adapterIn/adapterOut(处理 JSON 方向的适配)
676
+ * - 初始化 process 与 definitions 的基础属性
677
+ */
497
678
  constructor({ lf }) {
498
679
  lf.adapterIn = (data) => this.adapterIn(data)
499
680
  lf.adapterOut = (data, retainedFields?: string[]) =>
@@ -524,6 +705,13 @@ class BpmnAdapter {
524
705
  * ["properties", "startPoint", "endPoint", "pointsList"]合并,
525
706
  * 这意味着出现在这个数组里的字段当它的值是数组或是对象时不会被视为一个节点而是一个属性。
526
707
  */
708
+ /**
709
+ * adapterOut:将 LogicFlow 图数据转换为 BPMN JSON
710
+ * 输入:
711
+ * - data:LogicFlow GraphData
712
+ * - retainedFields:扩展属性保留字段
713
+ * 输出:BPMN JSON(包含 definitions/process/diagram/plane)
714
+ */
527
715
  adapterOut = (data, retainedFields?: string[]) => {
528
716
  const bpmnProcessData = { ...this.processAttributes }
529
717
  convertLf2ProcessData(bpmnProcessData, data, retainedFields)
@@ -543,6 +731,11 @@ class BpmnAdapter {
543
731
  }
544
732
  return bpmnData
545
733
  }
734
+ /**
735
+ * adapterIn:将 BPMN JSON 转换为 LogicFlow 图数据
736
+ * 输入:bpmnData:BPMN JSON
737
+ * 输出:GraphConfigData(nodes/edges)
738
+ */
546
739
  adapterIn = (bpmnData) => {
547
740
  if (bpmnData) {
548
741
  return convertBpmn2LfData(bpmnData)
@@ -571,9 +764,19 @@ BpmnAdapter.shapeConfigMap.set(BpmnElements.USER, {
571
764
  height: UserTaskConfig.height,
572
765
  })
573
766
 
767
+ /**
768
+ * BpmnXmlAdapter:XML 适配器(继承 BpmnAdapter)
769
+ *
770
+ * 作用:处理 XML 输入/输出的适配,使用 xml2json/json2xml 完成格式转换。
771
+ * 特殊处理:在 XML 导入前对 name 属性的非法字符进行预处理转义,提升容错。
772
+ */
574
773
  class BpmnXmlAdapter extends BpmnAdapter {
575
774
  static pluginName = 'bpmnXmlAdapter'
576
775
 
776
+ /**
777
+ * 构造函数
778
+ * - 覆盖 LogicFlow 的 adapterIn/adapterOut,使其面向 XML 输入与输出。
779
+ */
577
780
  constructor(data) {
578
781
  super(data)
579
782
  const { lf } = data
@@ -581,10 +784,46 @@ class BpmnXmlAdapter extends BpmnAdapter {
581
784
  lf.adapterOut = this.adapterXmlOut
582
785
  }
583
786
 
787
+ // 预处理:修复属性值中非法的XML字符(仅针对 name 属性)
788
+ /**
789
+ * 预处理 XML:仅对 name 属性值进行非法字符转义(<, >, &),避免 DOM 解析失败。
790
+ * 注意:不影响已合法的实体(如 &amp;),仅在属性值中生效,不修改其它内容。
791
+ */
792
+ private sanitizeNameAttributes(xml: string): string {
793
+ return xml.replace(/name="([^"]*)"/g, (_, val) => {
794
+ const safe = val
795
+ .replace(/&(?!#?\w+;)/g, '&amp;')
796
+ .replace(/</g, '&lt;')
797
+ .replace(/>/g, '&gt;')
798
+ return `name="${safe}"`
799
+ })
800
+ }
801
+
802
+ /**
803
+ * adapterXmlIn:将 BPMN XML 转换为 LogicFlow 图数据
804
+ * 输入:bpmnData:XML 字符串或对象
805
+ * 步骤:
806
+ * 1) 若为字符串,先对 name 属性进行预处理转义;
807
+ * 2) 使用 lfXml2Json 转换为 BPMN JSON;
808
+ * 3) 调用基础 adapterIn 转换为 GraphData。
809
+ */
584
810
  adapterXmlIn = (bpmnData) => {
585
- const json = lfXml2Json(bpmnData)
811
+ const xmlStr =
812
+ typeof bpmnData === 'string'
813
+ ? this.sanitizeNameAttributes(bpmnData)
814
+ : bpmnData
815
+ const json = lfXml2Json(xmlStr)
586
816
  return this.adapterIn(json)
587
817
  }
818
+ /**
819
+ * adapterXmlOut:将 LogicFlow 图数据转换为 BPMN XML
820
+ * 输入:
821
+ * - data:GraphData
822
+ * - retainedFields:保留属性字段
823
+ * 步骤:
824
+ * 1) 调用基础 adapterOut 生成 BPMN JSON;
825
+ * 2) 使用 lfJson2Xml 转为合法的 XML 字符串(包含属性与文本的转义)。
826
+ */
588
827
  adapterXmlOut = (data, retainedFields?: string[]) => {
589
828
  const outData = this.adapterOut(data, retainedFields)
590
829
  return lfJson2Xml(outData)
@@ -26,6 +26,31 @@ function handleAttributes(o: any) {
26
26
  return t
27
27
  }
28
28
 
29
+ /**
30
+ * 将普通文本中的一些特殊字符进行转移,保障文本安全地嵌入 XML:
31
+ * - 空值(`null/undefined`)返回空字符串,避免输出非法字面量;
32
+ * - 按顺序转义 XML 保留字符:`&`, `<`, `>`, `"`, `'`;
33
+ * 注意优先转义 `&`,避免后续生成的实体被再次转义。
34
+ * @param text 原始文本
35
+ * @returns 已完成 XML 转义的字符串
36
+ */
37
+ function escapeXml(text: string) {
38
+ // 空值直接返回空字符串,防止在 XML 中出现 "null"/"undefined"
39
+ if (text == null) return ''
40
+ return (
41
+ text
42
+ .toString()
43
+ // & 必须先转义,避免影响后续 < > " ' 的实体
44
+ .replace(/&/g, '&amp;')
45
+ // 小于号与大于号,用于标签边界
46
+ .replace(/</g, '&lt;')
47
+ .replace(/>/g, '&gt;')
48
+ // 双引号与单引号,用于属性值的包裹
49
+ .replace(/"/g, '&quot;')
50
+ .replace(/'/g, '&apos;')
51
+ )
52
+ }
53
+
29
54
  function getAttributes(obj: any) {
30
55
  let tmp = obj
31
56
  try {
@@ -35,7 +60,8 @@ function getAttributes(obj: any) {
35
60
  } catch (error) {
36
61
  tmp = JSON.stringify(handleAttributes(obj)).replace(/"/g, "'")
37
62
  }
38
- return tmp
63
+ // 确保属性值中的特殊字符被正确转义
64
+ return escapeXml(String(tmp))
39
65
  }
40
66
 
41
67
  const tn = '\t\n'
@@ -51,7 +77,7 @@ function toXml(obj: string | any[] | Object, name: string, depth: number) {
51
77
 
52
78
  let str = ''
53
79
  if (name === '#text') {
54
- return tn + frontSpace + obj
80
+ return tn + frontSpace + escapeXml(String(obj))
55
81
  } else if (name === '#cdata-section') {
56
82
  return tn + frontSpace + '<![CDATA[' + obj + ']]>'
57
83
  } else if (name === '#comment') {
@@ -78,7 +104,7 @@ function toXml(obj: string | any[] | Object, name: string, depth: number) {
78
104
  attributes +
79
105
  (children !== '' ? `>${children}${tn + frontSpace}</${name}>` : ' />')
80
106
  } else {
81
- str += tn + frontSpace + `<${name}>${obj.toString()}</${name}>`
107
+ str += tn + frontSpace + `<${name}>${escapeXml(String(obj))}</${name}>`
82
108
  }
83
109
  }
84
110
 
@@ -98,4 +124,4 @@ function lfJson2Xml(o: Object) {
98
124
  return xmlStr
99
125
  }
100
126
 
101
- export { lfJson2Xml, handleAttributes }
127
+ export { lfJson2Xml, handleAttributes, escapeXml }
@@ -3,6 +3,7 @@
3
3
  // ========================================================================
4
4
  // XML.ObjTree -- XML source code from/to JavaScript object like E4X
5
5
  // ========================================================================
6
+ import { escapeXml } from './json2xml'
6
7
 
7
8
  let XML = function () {}
8
9
 
@@ -282,13 +283,7 @@ XML.ObjTree.prototype.scalar_to_xml = function (name, text) {
282
283
 
283
284
  // method: xml_escape( text )
284
285
 
285
- XML.ObjTree.prototype.xml_escape = function (text) {
286
- return text
287
- .replace(/&/g, '&')
288
- .replace(/</g, '<')
289
- .replace(/>/g, '>')
290
- .replace(/"/g, '"')
291
- }
286
+ XML.ObjTree.prototype.xml_escape = escapeXml
292
287
 
293
288
  /*
294
289
  // ========================================================================
@@ -1,5 +1,8 @@
1
1
  import LogicFlow from '@logicflow/core'
2
- import { createTeleportContainer } from '@logicflow/vue-node-registry'
2
+ import {
3
+ createTeleportContainer,
4
+ destroyTeleportContainer,
5
+ } from '@logicflow/vue-node-registry'
3
6
 
4
7
  import Position = LogicFlow.Position
5
8
  import MiniMapOption = MiniMap.MiniMapOption
@@ -195,8 +198,6 @@ export class MiniMap {
195
198
  this.bounds = boundsInit
196
199
  this.elementAreaBounds = boundsInit
197
200
  this.viewPortBounds = boundsInit
198
- this.initMiniMap()
199
- lf.on('graph:resize', this.onGraphResize)
200
201
  }
201
202
 
202
203
  onGraphResize = () => {
@@ -227,8 +228,10 @@ export class MiniMap {
227
228
  */
228
229
  public show = (left?: number, top?: number) => {
229
230
  if (!this.isShow) {
231
+ this.initMiniMap()
232
+ this.lf.on('graph:resize', this.onGraphResize)
230
233
  this.createMiniMap(left, top)
231
- this.setView()
234
+ this.setView(true)
232
235
  }
233
236
  this.isShow = true
234
237
  }
@@ -237,6 +240,14 @@ export class MiniMap {
237
240
  */
238
241
  public hide = () => {
239
242
  if (this.isShow) {
243
+ // 隐藏小地图时摧毁实例
244
+ destroyTeleportContainer(this.lfMap.graphModel.flowId)
245
+ this.lf.off('graph:resize', this.onGraphResize)
246
+ this.lfMap.destroy()
247
+ // 保证重新创建实例时,小地图中内容偏移正确
248
+ this.translateX = 0
249
+ this.translateY = 0
250
+
240
251
  this.removeMiniMap()
241
252
  this.lf.emit('miniMap:close', {})
242
253
  }
@@ -674,7 +685,9 @@ export class MiniMap {
674
685
  },
675
686
  })
676
687
  }
688
+
677
689
  destroy() {
690
+ destroyTeleportContainer(this.lfMap.graphModel.flowId)
678
691
  this.lf.off('graph:resize', this.onGraphResize)
679
692
  }
680
693
  }
@@ -15,11 +15,19 @@ export type ProximityConnectProps = {
15
15
  distance: number
16
16
  reverseDirection: boolean
17
17
  virtualEdgeStyle: Record<string, unknown>
18
+ /**
19
+ * proximityConnect 类型:
20
+ * - 'node': 节点-节点连接
21
+ * - 'anchor': 锚点-锚点连接
22
+ * - 'default': 节点-锚点连接
23
+ */
24
+ type: 'node' | 'anchor' | 'default'
18
25
  }
19
26
 
20
27
  export class ProximityConnect {
21
28
  static pluginName = 'proximityConnect'
22
29
  enable: boolean = true
30
+ type: 'node' | 'anchor' | 'default' = 'default'
23
31
  lf: LogicFlow // lf实例
24
32
  closestNode?: BaseNodeModel // 当前距离最近的节点
25
33
  currentDistance: number = Infinity // 当前间距
@@ -52,6 +60,7 @@ export class ProximityConnect {
52
60
  addEventListeners() {
53
61
  // 节点开始拖拽事件
54
62
  this.lf.graphModel.eventCenter.on('node:dragstart', ({ data }) => {
63
+ if (this.type === 'anchor') return
55
64
  if (!this.enable) return
56
65
  const { graphModel } = this.lf
57
66
  const { id } = data
@@ -59,13 +68,14 @@ export class ProximityConnect {
59
68
  })
60
69
  // 节点拖拽事件
61
70
  this.lf.graphModel.eventCenter.on('node:drag', () => {
71
+ if (this.type === 'anchor') return
62
72
  this.handleNodeDrag()
63
73
  })
64
74
  // 锚点开始拖拽事件
65
75
  this.lf.graphModel.eventCenter.on(
66
76
  'anchor:dragstart',
67
77
  ({ data, nodeModel }) => {
68
- if (!this.enable) return
78
+ if (!this.enable || this.type === 'node') return
69
79
  this.currentNode = nodeModel
70
80
  this.currentAnchor = data
71
81
  },
@@ -74,18 +84,18 @@ export class ProximityConnect {
74
84
  this.lf.graphModel.eventCenter.on(
75
85
  'anchor:drag',
76
86
  ({ e: { clientX, clientY } }) => {
77
- if (!this.enable) return
87
+ if (!this.enable || this.type === 'node') return
78
88
  this.handleAnchorDrag(clientX, clientY)
79
89
  },
80
90
  )
81
91
  // 节点、锚点拖拽结束事件
82
92
  this.lf.graphModel.eventCenter.on('node:drop', () => {
83
- if (!this.enable) return
93
+ if (!this.enable || this.type === 'anchor') return
84
94
  this.handleDrop()
85
95
  })
86
96
  // 锚点拖拽需要单独判断一下当前拖拽终点是否在某个锚点上,如果是,就不触发插件的连线,以免出现创建了两条连线的问题,表现见 issue 2140
87
97
  this.lf.graphModel.eventCenter.on('anchor:dragend', ({ e, edgeModel }) => {
88
- if (!this.enable) return
98
+ if (!this.enable || this.type === 'node') return
89
99
  const {
90
100
  canvasOverlayPosition: { x: eventX, y: eventY },
91
101
  } = this.lf.graphModel.getPointByClient({