bpmn-js 18.7.0 → 18.9.0

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/lib/Modeler.js CHANGED
@@ -26,6 +26,7 @@ import InteractionEventsModule from './features/interaction-events';
26
26
  import KeyboardModule from './features/keyboard';
27
27
  import KeyboardMoveSelectionModule from 'diagram-js/lib/features/keyboard-move-selection';
28
28
  import LabelEditingModule from './features/label-editing';
29
+ import LabelLink from './features/label-link';
29
30
  import ModelingModule from './features/modeling';
30
31
  import ModelingFeedbackModule from './features/modeling-feedback';
31
32
  import MoveModule from 'diagram-js/lib/features/move';
@@ -181,6 +182,7 @@ Modeler.prototype._modelingModules = [
181
182
  KeyboardModule,
182
183
  KeyboardMoveSelectionModule,
183
184
  LabelEditingModule,
185
+ LabelLink,
184
186
  ModelingModule,
185
187
  ModelingFeedbackModule,
186
188
  MoveModule,
@@ -15,7 +15,8 @@ import {
15
15
  } from '../util/DiUtil';
16
16
 
17
17
  import {
18
- getLabel
18
+ getLabel,
19
+ isLabel
19
20
  } from '../util/LabelUtil';
20
21
 
21
22
  import {
@@ -70,7 +71,8 @@ var markerIds = new Ids();
70
71
  var ELEMENT_LABEL_DISTANCE = 10,
71
72
  INNER_OUTER_DIST = 3,
72
73
  PARTICIPANT_STROKE_WIDTH = 1.5,
73
- TASK_BORDER_RADIUS = 10;
74
+ TASK_BORDER_RADIUS = 10,
75
+ EXTERNAL_LABEL_BORDER_RADIUS = 4;
74
76
 
75
77
  var DEFAULT_OPACITY = 0.95,
76
78
  FULL_OPACITY = 1,
@@ -2379,6 +2381,11 @@ BpmnRenderer.prototype.drawConnection = function(parentGfx, connection, attrs =
2379
2381
  * @return {string} path
2380
2382
  */
2381
2383
  BpmnRenderer.prototype.getShapePath = function(shape) {
2384
+
2385
+ if (isLabel(shape)) {
2386
+ return getRoundRectPath(shape, EXTERNAL_LABEL_BORDER_RADIUS);
2387
+ }
2388
+
2382
2389
  if (is(shape, 'bpmn:Event')) {
2383
2390
  return getCirclePath(shape);
2384
2391
  }
@@ -28,7 +28,8 @@ const ALLOWED_REFERENCES = [
28
28
  'errorRef',
29
29
  'escalationRef',
30
30
  'messageRef',
31
- 'signalRef'
31
+ 'signalRef',
32
+ 'dataObjectRef'
32
33
  ];
33
34
 
34
35
  /**
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Render a line between an external label and its target element,
3
+ * when either is selected.
4
+ *
5
+ */
6
+ export default class LabelLink {
7
+ static $inject: string[];
8
+ /**
9
+ * @param eventBus
10
+ * @param canvas
11
+ * @param graphicsFactory
12
+ * @param outline
13
+ */
14
+ constructor(
15
+ eventBus: EventBus,
16
+ canvas: Canvas,
17
+ graphicsFactory: GraphicsFactory,
18
+ outline: Outline
19
+ );
20
+ }
21
+
22
+ type EventBus = import("diagram-js/lib/core/EventBus").default;
23
+ type Canvas = import("diagram-js/lib/core/Canvas").default;
24
+ type GraphicsFactory = import("diagram-js/lib/core/GraphicsFactory").default;
25
+ type Outline = import("../outline/OutlineProvider").default;
26
+ export type Selection = any;
27
+ type Element = import("diagram-js/lib/model/Types").Element;
@@ -0,0 +1,202 @@
1
+ import { queryAll as domQueryAll } from 'min-dom';
2
+
3
+ import {
4
+ append as svgAppend,
5
+ attr as svgAttr,
6
+ remove as svgRemove,
7
+ } from 'tiny-svg';
8
+
9
+ import { createLine, updateLine } from 'diagram-js/lib/util/RenderUtil';
10
+ import { getMid, getElementLineIntersection } from 'diagram-js/lib/layout/LayoutUtil';
11
+ import { getDistancePointPoint } from 'diagram-js/lib/features/bendpoints/GeometricUtil';
12
+ import { isLabel } from 'diagram-js/lib/util/ModelUtil';
13
+
14
+ import { isAny } from '../modeling/util/ModelingUtil';
15
+ import { getRoundRectPath, getCirclePath } from '../../draw/BpmnRenderUtil';
16
+
17
+ /**
18
+ * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus
19
+ * @typedef {import('diagram-js/lib/core/Canvas').default} Canvas
20
+ * @typedef {import('diagram-js/lib/core/GraphicsFactory').default} GraphicsFactory
21
+ * @typedef {import('../outline/OutlineProvider').default} Outline
22
+ * @typedef {import('diagram-js/lib/features/selection').default} Selection
23
+ *
24
+ * @typedef {import('diagram-js/lib/model/Types').Element} Element
25
+ */
26
+
27
+ const ALLOWED_ELEMENTS = [ 'bpmn:Event', 'bpmn:SequenceFlow', 'bpmn:Gateway' ];
28
+
29
+ const LINE_STYLE = {
30
+ class: 'bjs-label-link',
31
+ stroke: 'var(--element-selected-outline-secondary-stroke-color)',
32
+ strokeDasharray: '5, 5',
33
+ };
34
+
35
+ const DISTANCE_THRESHOLD = 15;
36
+ const PATH_OFFSET = 2;
37
+
38
+ /**
39
+ * Render a line between an external label and its target element,
40
+ * when either is selected.
41
+ *
42
+ * @param {EventBus} eventBus
43
+ * @param {Canvas} canvas
44
+ * @param {GraphicsFactory} graphicsFactory
45
+ * @param {Outline} outline
46
+ */
47
+ export default function LabelLink(eventBus, canvas, graphicsFactory, outline, selection) {
48
+
49
+ const layer = canvas.getLayer('overlays');
50
+
51
+ eventBus.on([ 'selection.changed', 'shape.changed' ], function() {
52
+ cleanUp();
53
+ });
54
+
55
+ eventBus.on('selection.changed', function({ newSelection }) {
56
+
57
+ const allowedElements = newSelection.filter(element => isAny(element, ALLOWED_ELEMENTS));
58
+
59
+ if (allowedElements.length === 1) {
60
+ const element = allowedElements[0];
61
+ if (isLabel(element)) {
62
+ createLink(element, element.labelTarget, newSelection);
63
+ } else if (element.labels?.length) {
64
+ createLink(element.labels[0], element, newSelection);
65
+ }
66
+ }
67
+
68
+ // Only allowed when both label and its target are selected
69
+ if (allowedElements.length === 2) {
70
+ const label = allowedElements.find(isLabel);
71
+ const target = allowedElements.find(el => el.labels?.includes(label));
72
+ if (label && target) {
73
+ createLink(label, target, newSelection);
74
+ }
75
+ }
76
+ });
77
+
78
+ eventBus.on('shape.changed', function({ element }) {
79
+
80
+ if (!isAny(element, ALLOWED_ELEMENTS)) {
81
+ return;
82
+ }
83
+
84
+ if (isLabel(element)) {
85
+ createLink(element, element.labelTarget, selection.get());
86
+ } else if (element.labels?.length) {
87
+ createLink(element.labels[0], element, selection.get());
88
+ }
89
+ });
90
+
91
+ /**
92
+ * Render a line between an external label and its target.
93
+ *
94
+ * @param {Element} label
95
+ * @param {Element} target
96
+ * @param {Element[]} selection
97
+ */
98
+ function createLink(label, target, selection = []) {
99
+
100
+ // Create an auxiliary line between label and target mid points
101
+ const line = createLine(
102
+ [ getMid(target), getMid(label) ],
103
+ LINE_STYLE
104
+ );
105
+ const linePath = line.getAttribute('d');
106
+
107
+ // Calculate the intersection point between line and label
108
+ const labelSelected = selection.includes(label);
109
+ const labelPath = labelSelected ? getElementOutlinePath(label) : getElementPath(label);
110
+ const labelInter = getElementLineIntersection(labelPath, linePath);
111
+
112
+ // Label on top of the target
113
+ if (!labelInter) {
114
+ return;
115
+ }
116
+
117
+ // Calculate the intersection point between line and label
118
+ // If the target is a sequence flow, there is no intersection,
119
+ // so we link to the middle of it.
120
+ const targetSelected = selection.includes(target);
121
+ const targetPath = targetSelected ? getElementOutlinePath(target) : getElementPath(target);
122
+ const targetInter = getElementLineIntersection(targetPath, linePath) || getMid(target);
123
+
124
+ // Do not draw a link if the points are too close
125
+ const distance = getDistancePointPoint(targetInter, labelInter);
126
+ if (distance < DISTANCE_THRESHOLD) {
127
+ return;
128
+ }
129
+
130
+ // Connect the actual closest points
131
+ updateLine(line, [ targetInter, labelInter ]);
132
+ svgAppend(layer, line);
133
+ }
134
+
135
+ /**
136
+ * Remove all existing label links.
137
+ */
138
+ function cleanUp() {
139
+ domQueryAll(`.${LINE_STYLE.class}`, layer).forEach(svgRemove);
140
+ }
141
+
142
+ /**
143
+ * Get element's slightly expanded outline path.
144
+ *
145
+ * @param {Element} element
146
+ * @returns {string} svg path
147
+ */
148
+ function getElementOutlinePath(element) {
149
+ const outlineShape = outline.getOutline(element);
150
+ const outlineOffset = outline.offset;
151
+
152
+ if (!outlineShape) {
153
+ return getElementPath(element);
154
+ }
155
+
156
+ if (outlineShape.x) {
157
+ const shape = {
158
+ x: element.x + parseSvgNumAttr(outlineShape, 'x') - PATH_OFFSET,
159
+ y: element.y + parseSvgNumAttr(outlineShape, 'y') - PATH_OFFSET,
160
+ width: parseSvgNumAttr(outlineShape, 'width') + PATH_OFFSET * 2,
161
+ height: parseSvgNumAttr(outlineShape, 'height') + PATH_OFFSET * 2
162
+ };
163
+
164
+ return getRoundRectPath(shape, parseSvgNumAttr(outlineShape, 'rx'));
165
+ }
166
+
167
+ if (outlineShape.cx) {
168
+ const shape = {
169
+ x: element.x - outlineOffset,
170
+ y: element.y - outlineOffset,
171
+ width: parseSvgNumAttr(outlineShape, 'r') * 2,
172
+ height: parseSvgNumAttr(outlineShape, 'r') * 2,
173
+ };
174
+
175
+ return getCirclePath(shape);
176
+ }
177
+ }
178
+
179
+ function getElementPath(element) {
180
+ return graphicsFactory.getShapePath(element);
181
+ }
182
+ }
183
+
184
+ LabelLink.$inject = [
185
+ 'eventBus',
186
+ 'canvas',
187
+ 'graphicsFactory',
188
+ 'outline',
189
+ 'selection'
190
+ ];
191
+
192
+ /**
193
+ * Get numeric attribute from SVG element
194
+ * or 0 if not present.
195
+ *
196
+ * @param {SVGElement} node
197
+ * @param {string} attr
198
+ * @returns {number}
199
+ */
200
+ function parseSvgNumAttr(node, attr) {
201
+ return parseFloat(svgAttr(node, attr) || 0);
202
+ }
@@ -0,0 +1,14 @@
1
+ declare namespace _default {
2
+ let __depends__: ({
3
+ selection: (string | typeof import("diagram-js/lib/features/selection/Selection").default)[];
4
+ selectionVisuals: (string | typeof import("diagram-js/lib/features/selection/SelectionVisuals").default)[];
5
+ selectionBehavior: (string | typeof import("diagram-js/lib/features/selection/SelectionBehavior").default)[];
6
+ } | {
7
+ outline: (string | typeof import("diagram-js/lib/features/outline/Outline").default)[];
8
+ multiSelectionOutline: (string | typeof import("diagram-js/lib/features/outline/MultiSelectionOutline").default)[];
9
+ })[];
10
+ let __init__: string[];
11
+ let labelLink: (string | typeof LabelLink)[];
12
+ }
13
+ export default _default;
14
+ import LabelLink from './LabelLink';
@@ -0,0 +1,15 @@
1
+ import SelectionModule from 'diagram-js/lib/features/selection';
2
+ import OutlineModule from 'diagram-js/lib/features/outline';
3
+
4
+ import LabelLink from './LabelLink';
5
+
6
+ export default {
7
+ __depends__: [
8
+ SelectionModule,
9
+ OutlineModule
10
+ ],
11
+ __init__: [
12
+ 'labelLink'
13
+ ],
14
+ labelLink: [ 'type', LabelLink ]
15
+ };
@@ -29,6 +29,9 @@ export default function CreateDataObjectBehavior(eventBus, bpmnFactory) {
29
29
  // create a DataObject every time a DataObjectReference is created
30
30
  var dataObject = bpmnFactory.create('bpmn:DataObject');
31
31
 
32
+ // Copy the isCollection property if needed.
33
+ dataObject.isCollection = shape.businessObject.dataObjectRef?.isCollection || false;
34
+
32
35
  // set the reference to the DataObject
33
36
  shape.businessObject.dataObjectRef = dataObject;
34
37
  }
@@ -10,7 +10,7 @@ import {
10
10
  isAny
11
11
  } from '../../util/ModelUtil';
12
12
 
13
- import { isLabel } from '../../util/LabelUtil';
13
+ import { isLabel, isExternalLabel } from '../../util/LabelUtil';
14
14
 
15
15
  import {
16
16
  DATA_OBJECT_REFERENCE_OUTLINE_PATH,
@@ -64,6 +64,20 @@ OutlineProvider.prototype.getOutline = function(element) {
64
64
 
65
65
  var outline;
66
66
 
67
+ if (isExternalLabel(element)) {
68
+ outline = svgCreate('rect');
69
+
70
+ svgAttr(outline, assign({
71
+ x: -DEFAULT_OFFSET,
72
+ y: -DEFAULT_OFFSET,
73
+ rx: 4,
74
+ width: element.width + DEFAULT_OFFSET * 2,
75
+ height: element.height + DEFAULT_OFFSET * 2
76
+ }, OUTLINE_STYLE));
77
+
78
+ return outline;
79
+ }
80
+
67
81
  if (isLabel(element)) {
68
82
  return;
69
83
  }
@@ -4,7 +4,7 @@ declare namespace _default {
4
4
  multiSelectionOutline: (string | typeof import("diagram-js/lib/features/outline/MultiSelectionOutline").default)[];
5
5
  }[];
6
6
  let __init__: string[];
7
- let outlineProvider: (string | typeof OulineProvider)[];
7
+ let outlineProvider: (string | typeof OutlineProvider)[];
8
8
  }
9
9
  export default _default;
10
- import OulineProvider from './OutlineProvider';
10
+ import OutlineProvider from './OutlineProvider';
@@ -1,10 +1,10 @@
1
- import Ouline from 'diagram-js/lib/features/outline';
2
- import OulineProvider from './OutlineProvider';
1
+ import Outline from 'diagram-js/lib/features/outline';
2
+ import OutlineProvider from './OutlineProvider';
3
3
 
4
4
  export default {
5
5
  __depends__: [
6
- Ouline
6
+ Outline
7
7
  ],
8
8
  __init__: [ 'outlineProvider' ],
9
- outlineProvider: [ 'type', OulineProvider ]
9
+ outlineProvider: [ 'type', OutlineProvider ]
10
10
  };
@@ -69,6 +69,15 @@ export function getLabel(element: Element): string;
69
69
  */
70
70
  export function setLabel(element: Element, text: string): Element;
71
71
 
72
+ /**
73
+ * Returns true if the given element is an external label.
74
+ *
75
+ * @param element
76
+ *
77
+ * @return
78
+ */
79
+ export function isExternalLabel(element: Element): boolean;
80
+
72
81
  export { isLabel } from "diagram-js/lib/util/ModelUtil";
73
82
 
74
83
  export namespace DEFAULT_LABEL_SIZE {
@@ -264,3 +264,14 @@ export function setLabel(element, text) {
264
264
 
265
265
  return element;
266
266
  }
267
+
268
+ /**
269
+ * Returns true if the given element is an external label.
270
+ *
271
+ * @param {Element} element
272
+ *
273
+ * @return {boolean}
274
+ */
275
+ export function isExternalLabel(element) {
276
+ return isLabel(element) && isLabelExternal(element.labelTarget);
277
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bpmn-js",
3
- "version": "18.7.0",
3
+ "version": "18.9.0",
4
4
  "description": "A bpmn 2.0 toolkit and web modeler",
5
5
  "main": "lib/index.js",
6
6
  "files": [
@@ -65,7 +65,7 @@
65
65
  "devDependencies": {
66
66
  "@babel/core": "^7.25.8",
67
67
  "@bpmn-io/a11y": "^0.1.0",
68
- "@rollup/plugin-commonjs": "^28.0.3",
68
+ "@rollup/plugin-commonjs": "^29.0.0",
69
69
  "@rollup/plugin-json": "^6.1.0",
70
70
  "@rollup/plugin-node-resolve": "^16.0.1",
71
71
  "@rollup/plugin-replace": "^6.0.2",
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Assert if two SVG paths are approximately equal within a given tolerance.
3
+ *
4
+ * @param {string} actual
5
+ * @param {string} expected
6
+ * @param {number} [tolerance=2]
7
+ */
8
+ export function expectSvgPath(actual, expected, tolerance = 2) {
9
+ const result = compareSvgPaths(actual, expected, tolerance);
10
+ expect(result).to.be.true;
11
+ }
12
+
13
+ /**
14
+ * Returns true if two SVG paths are approximately equal within a given tolerance.
15
+ *
16
+ * @param {string} pathA
17
+ * @param {string} pathB
18
+ * @param {number} [tolerance=2]
19
+ * @returns {boolean}
20
+ */
21
+ export function compareSvgPaths(pathA, pathB, tolerance = 2) {
22
+ const actualNumbers = pathToNumbers(pathA);
23
+ const expectedNumbers = pathToNumbers(pathB);
24
+
25
+ if (actualNumbers.length !== expectedNumbers.length) {
26
+ return false;
27
+ }
28
+
29
+ for (let i = 0; i < actualNumbers.length; i++) {
30
+ if (Math.abs(actualNumbers[i] - expectedNumbers[i]) > tolerance) {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ return true;
36
+ }
37
+
38
+ /**
39
+ * Get an array of numeric values from an SVG path string.
40
+ *
41
+ * @example
42
+ * `pathToNumber('M10,20L30,40')` => `[10, 20, 30, 40]`
43
+ *
44
+ * @param {string} path
45
+ * @returns {number[]}
46
+ */
47
+ export function pathToNumbers(path) {
48
+ const normalized = path.toLowerCase();
49
+ const parts = normalized.split(/[a-z,]/g).filter(s => s !== '');
50
+
51
+ return parts.map(parseFloat);
52
+ }