@bpmnkit/editor 0.0.31 → 0.0.32
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/dist/command-stack.d.ts +20 -5
- package/dist/command-stack.js +42 -15
- package/dist/css.d.ts +1 -1
- package/dist/css.js +40 -0
- package/dist/editor.d.ts +60 -0
- package/dist/editor.js +404 -55
- package/dist/hud.js +114 -41
- package/dist/i18n.d.ts +14 -0
- package/dist/i18n.js +11 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/modeling.d.ts +1 -0
- package/dist/modeling.js +143 -33
- package/dist/overlay.d.ts +1 -1
- package/dist/overlay.js +2 -2
- package/dist/rules.d.ts +19 -5
- package/dist/rules.js +101 -5
- package/dist/state-machine.d.ts +11 -1
- package/dist/state-machine.js +41 -9
- package/dist/types.d.ts +11 -0
- package/package.json +3 -3
package/dist/editor.js
CHANGED
|
@@ -1,16 +1,36 @@
|
|
|
1
|
-
import { KeyboardHandler, ViewportController, computeDiagramBounds, createDefs, createGrid, injectStyles, render, } from "@bpmnkit/canvas";
|
|
1
|
+
import { KeyboardHandler, OverlayManager, ViewportController, computeDiagramBounds, createDefs, createGrid, injectStyles, render, } from "@bpmnkit/canvas";
|
|
2
2
|
import { Bpmn, applyAutoLayout } from "@bpmnkit/core";
|
|
3
3
|
import { CommandStack } from "./command-stack.js";
|
|
4
4
|
import { injectEditorStyles } from "./css.js";
|
|
5
5
|
import { closestPort, computeWaypoints, computeWaypointsAvoiding, computeWaypointsWithPorts, diagramToScreen, labelBoundsForPosition, portFromWaypoint, screenToDiagram, } from "./geometry.js";
|
|
6
|
+
import { defaultTranslate } from "./i18n.js";
|
|
6
7
|
import { LabelEditor } from "./label-editor.js";
|
|
7
|
-
import { changeElementType as changeElementTypeFn, copyElements, createAnnotation, createAnnotationWithLink, createBoundaryEvent, createConnection, createEmptyDefinitions, createShape, deleteElements, insertEdgeWaypoint, insertShapeOnEdge, moveEdgeWaypoint, moveShapes, pasteElements, removeCollinearWaypoints, resizeShape, updateEdgeEndpoint, updateLabel, updateLabelPosition, updateShapeColor, } from "./modeling.js";
|
|
8
|
+
import { changeElementType as changeElementTypeFn, copyElements, createAnnotation, createAnnotationWithLink, createBoundaryEvent, createConnection, createEmptyDefinitions, createShape, deleteElements, insertEdgeWaypoint, insertShapeOnEdge, moveEdgeSegment, moveEdgeWaypoint, moveShapes, pasteElements, removeCollinearWaypoints, resizeShape, updateEdgeEndpoint, updateLabel, updateLabelPosition, updateShapeColor, } from "./modeling.js";
|
|
8
9
|
import { OverlayRenderer } from "./overlay.js";
|
|
9
|
-
import { canConnect } from "./rules.js";
|
|
10
|
+
import { canAttach, canConnect, canMorph, canResize } from "./rules.js";
|
|
10
11
|
import { EditorStateMachine } from "./state-machine.js";
|
|
11
|
-
import { RESIZABLE_TYPES } from "./types.js";
|
|
12
12
|
const NS = "http://www.w3.org/2000/svg";
|
|
13
13
|
let _instanceCounter = 0;
|
|
14
|
+
/**
|
|
15
|
+
* Scores a search element against a lowercased query. Higher is better; 0 means
|
|
16
|
+
* no match. A word-prefix hit on the name outranks a name substring, which
|
|
17
|
+
* outranks an id or type match.
|
|
18
|
+
*/
|
|
19
|
+
function scoreSearch(q, name, id, type) {
|
|
20
|
+
if (name) {
|
|
21
|
+
if (name === q)
|
|
22
|
+
return 120;
|
|
23
|
+
if (name.split(/\s+/).some((w) => w.startsWith(q)))
|
|
24
|
+
return 100;
|
|
25
|
+
if (name.includes(q))
|
|
26
|
+
return 60;
|
|
27
|
+
}
|
|
28
|
+
if (id.includes(q))
|
|
29
|
+
return 30;
|
|
30
|
+
if (type.includes(q))
|
|
31
|
+
return 10;
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
14
34
|
function defaultBounds(type, cx, cy) {
|
|
15
35
|
switch (type) {
|
|
16
36
|
case "startEvent":
|
|
@@ -119,27 +139,9 @@ const INTERMEDIATE_EVENT_TYPES = new Set([
|
|
|
119
139
|
"signalCatchEvent",
|
|
120
140
|
"signalThrowEvent",
|
|
121
141
|
]);
|
|
122
|
-
const ACTIVITY_TYPES = new Set([
|
|
123
|
-
"task",
|
|
124
|
-
"serviceTask",
|
|
125
|
-
"userTask",
|
|
126
|
-
"scriptTask",
|
|
127
|
-
"sendTask",
|
|
128
|
-
"receiveTask",
|
|
129
|
-
"businessRuleTask",
|
|
130
|
-
"manualTask",
|
|
131
|
-
"callActivity",
|
|
132
|
-
"subProcess",
|
|
133
|
-
"adHocSubProcess",
|
|
134
|
-
"eventSubProcess",
|
|
135
|
-
"transaction",
|
|
136
|
-
]);
|
|
137
142
|
function isIntermediateEventType(type) {
|
|
138
143
|
return INTERMEDIATE_EVENT_TYPES.has(type);
|
|
139
144
|
}
|
|
140
|
-
function isActivityType(type) {
|
|
141
|
-
return ACTIVITY_TYPES.has(type);
|
|
142
|
-
}
|
|
143
145
|
function snapToBoundary(center, hostBounds, r) {
|
|
144
146
|
// Clamp center to host boundary and return event bounds
|
|
145
147
|
const left = hostBounds.x;
|
|
@@ -214,6 +216,7 @@ export class BpmnEditor {
|
|
|
214
216
|
_viewport;
|
|
215
217
|
_keyboard;
|
|
216
218
|
_overlay;
|
|
219
|
+
_htmlOverlays;
|
|
217
220
|
_commandStack;
|
|
218
221
|
_stateMachine;
|
|
219
222
|
_labelEditor;
|
|
@@ -234,6 +237,9 @@ export class BpmnEditor {
|
|
|
234
237
|
_readOnly = false;
|
|
235
238
|
_isDragging = false;
|
|
236
239
|
_boundaryHostId = null;
|
|
240
|
+
_t;
|
|
241
|
+
_liveRegion;
|
|
242
|
+
_lastTap = null;
|
|
237
243
|
_warningBanner = null;
|
|
238
244
|
// ── Events ─────────────────────────────────────────────────────────
|
|
239
245
|
_listeners = new Map();
|
|
@@ -243,6 +249,7 @@ export class BpmnEditor {
|
|
|
243
249
|
injectStyles();
|
|
244
250
|
injectEditorStyles();
|
|
245
251
|
this._id = String(_instanceCounter++);
|
|
252
|
+
this._t = options.translate ?? defaultTranslate;
|
|
246
253
|
// Resolve initial theme — localStorage overrides the options.theme when persistTheme is on
|
|
247
254
|
let initialTheme = options.theme ?? "neon";
|
|
248
255
|
if (options.persistTheme) {
|
|
@@ -268,6 +275,13 @@ export class BpmnEditor {
|
|
|
268
275
|
this._host.setAttribute("tabindex", "0");
|
|
269
276
|
this._applyTheme(this._theme);
|
|
270
277
|
container.appendChild(this._host);
|
|
278
|
+
// Visually-hidden live region: since the SVG is aria-hidden, this is how
|
|
279
|
+
// selection and edit results reach assistive technology.
|
|
280
|
+
this._liveRegion = document.createElement("div");
|
|
281
|
+
this._liveRegion.className = "bpmnkit-sr-only";
|
|
282
|
+
this._liveRegion.setAttribute("aria-live", "polite");
|
|
283
|
+
this._liveRegion.setAttribute("aria-atomic", "true");
|
|
284
|
+
this._host.appendChild(this._liveRegion);
|
|
271
285
|
this._svg = document.createElementNS(NS, "svg");
|
|
272
286
|
this._svg.setAttribute("aria-hidden", "true");
|
|
273
287
|
this._host.appendChild(this._svg);
|
|
@@ -289,6 +303,13 @@ export class BpmnEditor {
|
|
|
289
303
|
this._viewportG.appendChild(this._overlayG);
|
|
290
304
|
// ── Viewport controller ──────────────────────────────────────
|
|
291
305
|
this._viewport = new ViewportController(this._host, this._svg, this._viewportG, this._gridPattern, (state) => this._emit("viewport:change", state));
|
|
306
|
+
// ── HTML overlays (element-anchored) ─────────────────────────
|
|
307
|
+
this._htmlOverlays = new OverlayManager({
|
|
308
|
+
hostEl: this._host,
|
|
309
|
+
getScale: () => this._viewport.state.scale,
|
|
310
|
+
getBBox: (id) => this._absoluteBBox(id),
|
|
311
|
+
onViewportChange: (cb) => this.on("viewport:change", cb),
|
|
312
|
+
});
|
|
292
313
|
// ── Overlay ──────────────────────────────────────────────────
|
|
293
314
|
this._overlay = new OverlayRenderer(this._overlayG, this._markerId);
|
|
294
315
|
// ── Command stack ────────────────────────────────────────────
|
|
@@ -308,9 +329,9 @@ export class BpmnEditor {
|
|
|
308
329
|
previewResize: (bounds) => this._overlay.setResizePreview(bounds),
|
|
309
330
|
commitResize: (id, bounds) => {
|
|
310
331
|
this._overlay.setResizePreview(null);
|
|
311
|
-
this._executeCommand((d) => resizeShape(d, id, bounds));
|
|
332
|
+
this._executeCommand((d) => resizeShape(d, id, bounds), "Resize");
|
|
312
333
|
},
|
|
313
|
-
previewConnect: (ghostEnd) => {
|
|
334
|
+
previewConnect: (ghostEnd, targetId) => {
|
|
314
335
|
const src = this._connectSourceBounds();
|
|
315
336
|
if (src) {
|
|
316
337
|
const wps = computeWaypoints(src, {
|
|
@@ -319,7 +340,7 @@ export class BpmnEditor {
|
|
|
319
340
|
width: 2,
|
|
320
341
|
height: 2,
|
|
321
342
|
});
|
|
322
|
-
this._overlay.setGhostConnection(wps);
|
|
343
|
+
this._overlay.setGhostConnection(wps, this._isConnectTargetInvalid(targetId));
|
|
323
344
|
}
|
|
324
345
|
},
|
|
325
346
|
cancelConnect: () => this._overlay.setGhostConnection(null),
|
|
@@ -333,7 +354,7 @@ export class BpmnEditor {
|
|
|
333
354
|
startLabelEdit: (id) => this._startLabelEdit(id),
|
|
334
355
|
setHovered: (id) => this._overlay.setHovered(id, this._shapes),
|
|
335
356
|
executeDelete: (ids) => {
|
|
336
|
-
this._executeCommand((d) => deleteElements(d, ids));
|
|
357
|
+
this._executeCommand((d) => deleteElements(d, ids), "Delete");
|
|
337
358
|
this._setSelection([]);
|
|
338
359
|
},
|
|
339
360
|
executeCopy: () => this._doCopy(),
|
|
@@ -363,7 +384,7 @@ export class BpmnEditor {
|
|
|
363
384
|
this._overlay.setEndpointDragGhost(null);
|
|
364
385
|
this._overlay.setAlignmentGuides([]);
|
|
365
386
|
const snap = this._snapWaypoint(pt);
|
|
366
|
-
this._executeCommand((d) => removeCollinearWaypoints(insertEdgeWaypoint(d, edgeId, segIdx, snap.pt), edgeId));
|
|
387
|
+
this._executeCommand((d) => removeCollinearWaypoints(insertEdgeWaypoint(d, edgeId, segIdx, snap.pt), edgeId), "Add waypoint");
|
|
367
388
|
},
|
|
368
389
|
cancelWaypointInsert: () => {
|
|
369
390
|
this._overlay.setEndpointDragGhost(null);
|
|
@@ -382,12 +403,26 @@ export class BpmnEditor {
|
|
|
382
403
|
this._overlay.setEndpointDragGhost(null);
|
|
383
404
|
this._overlay.setAlignmentGuides([]);
|
|
384
405
|
const snap = this._snapWaypoint(pt);
|
|
385
|
-
this._executeCommand((d) => removeCollinearWaypoints(moveEdgeWaypoint(d, edgeId, wpIdx, snap.pt), edgeId));
|
|
406
|
+
this._executeCommand((d) => removeCollinearWaypoints(moveEdgeWaypoint(d, edgeId, wpIdx, snap.pt), edgeId), "Move waypoint");
|
|
386
407
|
},
|
|
387
408
|
cancelWaypointMove: () => {
|
|
388
409
|
this._overlay.setEndpointDragGhost(null);
|
|
389
410
|
this._overlay.setAlignmentGuides([]);
|
|
390
411
|
},
|
|
412
|
+
previewSegmentMove: (edgeId, segIdx, isHoriz, delta) => {
|
|
413
|
+
if (!this._defs)
|
|
414
|
+
return;
|
|
415
|
+
const preview = moveEdgeSegment(this._defs, edgeId, segIdx, isHoriz, delta);
|
|
416
|
+
const edge = preview.diagrams[0]?.plane.edges.find((e) => e.bpmnElement === edgeId);
|
|
417
|
+
this._overlay.setEndpointDragGhost(edge?.waypoints ?? null);
|
|
418
|
+
},
|
|
419
|
+
commitSegmentMove: (edgeId, segIdx, isHoriz, delta) => {
|
|
420
|
+
this._overlay.setEndpointDragGhost(null);
|
|
421
|
+
this._executeCommand((d) => removeCollinearWaypoints(moveEdgeSegment(d, edgeId, segIdx, isHoriz, delta), edgeId), "Move segment");
|
|
422
|
+
},
|
|
423
|
+
cancelSegmentMove: () => {
|
|
424
|
+
this._overlay.setEndpointDragGhost(null);
|
|
425
|
+
},
|
|
391
426
|
showEdgeHoverDot: (pt) => {
|
|
392
427
|
this._overlay.setEdgeHoverDot(pt);
|
|
393
428
|
},
|
|
@@ -409,7 +444,7 @@ export class BpmnEditor {
|
|
|
409
444
|
this._stateMachine = new EditorStateMachine(callbacks);
|
|
410
445
|
// ── Label editor ─────────────────────────────────────────────
|
|
411
446
|
this._labelEditor = new LabelEditor(this._host, (id, text) => {
|
|
412
|
-
this._executeCommand((d) => updateLabel(d, id, text));
|
|
447
|
+
this._executeCommand((d) => updateLabel(d, id, text), "Rename", `label:${id}`);
|
|
413
448
|
this._stateMachine.setMode({ mode: "select", sub: { name: "idle", hoveredId: null } });
|
|
414
449
|
}, () => {
|
|
415
450
|
this._stateMachine.setMode({ mode: "select", sub: { name: "idle", hoveredId: null } });
|
|
@@ -470,13 +505,14 @@ export class BpmnEditor {
|
|
|
470
505
|
* The operation is undoable.
|
|
471
506
|
*/
|
|
472
507
|
autoLayout() {
|
|
473
|
-
this._executeCommand(applyAutoLayout);
|
|
508
|
+
this._executeCommand(applyAutoLayout, "Auto-layout");
|
|
474
509
|
this.fitView();
|
|
475
510
|
}
|
|
476
511
|
loadDefinitions(defs) {
|
|
477
512
|
this._commandStack.clear();
|
|
478
513
|
this._commandStack.push(defs);
|
|
479
514
|
this._selectedIds = [];
|
|
515
|
+
this._htmlOverlays.clear();
|
|
480
516
|
this._renderDefs(defs);
|
|
481
517
|
if (this._fit !== "none") {
|
|
482
518
|
requestAnimationFrame(() => this.fitView());
|
|
@@ -542,9 +578,134 @@ export class BpmnEditor {
|
|
|
542
578
|
if (this._selectedIds.length === 0)
|
|
543
579
|
return;
|
|
544
580
|
const ids = [...this._selectedIds];
|
|
545
|
-
this._executeCommand((d) => deleteElements(d, ids));
|
|
581
|
+
this._executeCommand((d) => deleteElements(d, ids), "Delete");
|
|
546
582
|
this._setSelection([]);
|
|
547
583
|
}
|
|
584
|
+
/** Bounds of the currently-selected shapes (edges have none, and are skipped). */
|
|
585
|
+
_selectedShapeBounds() {
|
|
586
|
+
const out = [];
|
|
587
|
+
for (const id of this._selectedIds) {
|
|
588
|
+
const shape = this._shapes.find((s) => s.id === id);
|
|
589
|
+
if (shape)
|
|
590
|
+
out.push({ id, ...shape.shape.bounds });
|
|
591
|
+
}
|
|
592
|
+
return out;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Aligns the selected shapes along an edge or centre-line, as a single
|
|
596
|
+
* undoable command. No-op for fewer than two selected shapes.
|
|
597
|
+
*/
|
|
598
|
+
alignSelected(edge) {
|
|
599
|
+
const bs = this._selectedShapeBounds();
|
|
600
|
+
if (bs.length < 2)
|
|
601
|
+
return;
|
|
602
|
+
const minLeft = Math.min(...bs.map((b) => b.x));
|
|
603
|
+
const maxRight = Math.max(...bs.map((b) => b.x + b.width));
|
|
604
|
+
const minTop = Math.min(...bs.map((b) => b.y));
|
|
605
|
+
const maxBottom = Math.max(...bs.map((b) => b.y + b.height));
|
|
606
|
+
const moves = [];
|
|
607
|
+
for (const b of bs) {
|
|
608
|
+
let dx = 0;
|
|
609
|
+
let dy = 0;
|
|
610
|
+
switch (edge) {
|
|
611
|
+
case "left":
|
|
612
|
+
dx = minLeft - b.x;
|
|
613
|
+
break;
|
|
614
|
+
case "right":
|
|
615
|
+
dx = maxRight - (b.x + b.width);
|
|
616
|
+
break;
|
|
617
|
+
case "center":
|
|
618
|
+
dx = (minLeft + maxRight) / 2 - (b.x + b.width / 2);
|
|
619
|
+
break;
|
|
620
|
+
case "top":
|
|
621
|
+
dy = minTop - b.y;
|
|
622
|
+
break;
|
|
623
|
+
case "bottom":
|
|
624
|
+
dy = maxBottom - (b.y + b.height);
|
|
625
|
+
break;
|
|
626
|
+
case "middle":
|
|
627
|
+
dy = (minTop + maxBottom) / 2 - (b.y + b.height / 2);
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
if (dx !== 0 || dy !== 0)
|
|
631
|
+
moves.push({ id: b.id, dx, dy });
|
|
632
|
+
}
|
|
633
|
+
if (moves.length > 0)
|
|
634
|
+
this._executeCommand((d) => moveShapes(d, moves), "Align");
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Distributes the selected shapes so the gaps between them are equal along
|
|
638
|
+
* the axis (the outermost two stay fixed), as a single undoable command.
|
|
639
|
+
* No-op for fewer than three selected shapes.
|
|
640
|
+
*/
|
|
641
|
+
distributeSelected(axis) {
|
|
642
|
+
const bs = this._selectedShapeBounds();
|
|
643
|
+
if (bs.length < 3)
|
|
644
|
+
return;
|
|
645
|
+
const horiz = axis === "horizontal";
|
|
646
|
+
const start = (b) => (horiz ? b.x : b.y);
|
|
647
|
+
const size = (b) => (horiz ? b.width : b.height);
|
|
648
|
+
const sorted = [...bs].sort((a, b) => start(a) + size(a) / 2 - (start(b) + size(b) / 2));
|
|
649
|
+
const first = sorted[0];
|
|
650
|
+
const last = sorted[sorted.length - 1];
|
|
651
|
+
if (!first || !last)
|
|
652
|
+
return;
|
|
653
|
+
const span = start(last) + size(last) - start(first);
|
|
654
|
+
const sumSize = sorted.reduce((s, b) => s + size(b), 0);
|
|
655
|
+
const gap = (span - sumSize) / (sorted.length - 1);
|
|
656
|
+
const moves = [];
|
|
657
|
+
let cursor = start(first) + size(first) + gap;
|
|
658
|
+
for (let i = 1; i < sorted.length - 1; i++) {
|
|
659
|
+
const b = sorted[i];
|
|
660
|
+
if (!b)
|
|
661
|
+
continue;
|
|
662
|
+
const delta = cursor - start(b);
|
|
663
|
+
if (delta !== 0) {
|
|
664
|
+
moves.push(horiz ? { id: b.id, dx: delta, dy: 0 } : { id: b.id, dx: 0, dy: delta });
|
|
665
|
+
}
|
|
666
|
+
cursor += size(b) + gap;
|
|
667
|
+
}
|
|
668
|
+
if (moves.length > 0)
|
|
669
|
+
this._executeCommand((d) => moveShapes(d, moves), "Distribute");
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Searches the model (recursively, including sub-process children) for
|
|
673
|
+
* elements matching `query`, ranked: a word-prefix match on the name beats a
|
|
674
|
+
* name substring, which beats an id or type match. Returns `[]` for a blank
|
|
675
|
+
* query.
|
|
676
|
+
*/
|
|
677
|
+
find(query) {
|
|
678
|
+
const q = query.trim().toLowerCase();
|
|
679
|
+
if (!q || !this._defs)
|
|
680
|
+
return [];
|
|
681
|
+
const candidates = [];
|
|
682
|
+
const walk = (els) => {
|
|
683
|
+
for (const el of els) {
|
|
684
|
+
candidates.push({ id: el.id, name: el.name ?? "", type: el.type });
|
|
685
|
+
if ("flowElements" in el)
|
|
686
|
+
walk(el.flowElements);
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
for (const proc of this._defs.processes) {
|
|
690
|
+
walk(proc.flowElements);
|
|
691
|
+
for (const ta of proc.textAnnotations) {
|
|
692
|
+
candidates.push({ id: ta.id, name: ta.text ?? "", type: "textAnnotation" });
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
for (const collab of this._defs.collaborations) {
|
|
696
|
+
for (const p of collab.participants) {
|
|
697
|
+
candidates.push({ id: p.id, name: p.name ?? "", type: "participant" });
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
const scored = [];
|
|
701
|
+
for (const c of candidates) {
|
|
702
|
+
const score = scoreSearch(q, c.name.toLowerCase(), c.id.toLowerCase(), c.type.toLowerCase());
|
|
703
|
+
if (score > 0)
|
|
704
|
+
scored.push({ ...c, score });
|
|
705
|
+
}
|
|
706
|
+
scored.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
|
|
707
|
+
return scored.map((c) => ({ id: c.id, label: c.name || c.id, type: c.type }));
|
|
708
|
+
}
|
|
548
709
|
undo() {
|
|
549
710
|
const prev = this._commandStack.undo();
|
|
550
711
|
if (prev) {
|
|
@@ -608,6 +769,17 @@ export class BpmnEditor {
|
|
|
608
769
|
this._theme = theme;
|
|
609
770
|
this._applyTheme(theme);
|
|
610
771
|
}
|
|
772
|
+
/** HTML overlays anchored to diagram elements (badges, tooltips, panels). */
|
|
773
|
+
get overlays() {
|
|
774
|
+
return this._htmlOverlays;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* The resolved translation hook (from `options.translate`, or identity).
|
|
778
|
+
* Used by the HUD to localize its strings; also available to plugins.
|
|
779
|
+
*/
|
|
780
|
+
get translate() {
|
|
781
|
+
return this._t;
|
|
782
|
+
}
|
|
611
783
|
zoomIn() {
|
|
612
784
|
const { width, height } = this._svg.getBoundingClientRect();
|
|
613
785
|
this._viewport.zoomAt(width / 2, height / 2, 1.25);
|
|
@@ -629,6 +801,10 @@ export class BpmnEditor {
|
|
|
629
801
|
paste() {
|
|
630
802
|
this._doPaste();
|
|
631
803
|
}
|
|
804
|
+
/** Copies the current selection to the clipboard and deletes it as one undo step. */
|
|
805
|
+
cut() {
|
|
806
|
+
this._doCut();
|
|
807
|
+
}
|
|
632
808
|
/** Pans the viewport to center on the element with the given id (preserves zoom). */
|
|
633
809
|
scrollToElement(id) {
|
|
634
810
|
const shape = this._shapes.find((s) => s.id === id);
|
|
@@ -658,6 +834,7 @@ export class BpmnEditor {
|
|
|
658
834
|
this._ro.disconnect();
|
|
659
835
|
this._viewport.destroy();
|
|
660
836
|
this._keyboard.destroy();
|
|
837
|
+
this._htmlOverlays.destroy();
|
|
661
838
|
this._labelEditor.destroy();
|
|
662
839
|
this._svg.removeEventListener("pointerdown", this._onPointerDown);
|
|
663
840
|
this._svg.removeEventListener("pointermove", this._onPointerMove);
|
|
@@ -752,16 +929,26 @@ export class BpmnEditor {
|
|
|
752
929
|
this._overlay.setEdgeEndpoints(null, "");
|
|
753
930
|
}
|
|
754
931
|
}
|
|
755
|
-
this._emit("diagram:load", defs);
|
|
932
|
+
this._emit("diagram:load", defs, { missingShapes: [], missingEdges: [] });
|
|
756
933
|
}
|
|
757
|
-
_executeCommand(fn) {
|
|
934
|
+
_executeCommand(fn, label = "", coalesceKey) {
|
|
758
935
|
if (this._readOnly || !this._defs)
|
|
759
936
|
return;
|
|
760
937
|
const newDefs = fn(this._defs);
|
|
761
|
-
this._commandStack.push(newDefs);
|
|
938
|
+
this._commandStack.push(newDefs, label, coalesceKey);
|
|
762
939
|
this._renderDefs(newDefs);
|
|
940
|
+
if (label)
|
|
941
|
+
this._announce(this._t(label));
|
|
763
942
|
this._emit("diagram:change", newDefs);
|
|
764
943
|
}
|
|
944
|
+
/** Label of the change `undo()` would revert (for HUD tooltips), or null. */
|
|
945
|
+
getUndoLabel() {
|
|
946
|
+
return this._commandStack.undoLabel();
|
|
947
|
+
}
|
|
948
|
+
/** Label of the change `redo()` would re-apply (for HUD tooltips), or null. */
|
|
949
|
+
getRedoLabel() {
|
|
950
|
+
return this._commandStack.redoLabel();
|
|
951
|
+
}
|
|
765
952
|
_setSelection(ids) {
|
|
766
953
|
this._selectedIds = ids;
|
|
767
954
|
// Clear edge selection whenever shape selection changes
|
|
@@ -770,8 +957,34 @@ export class BpmnEditor {
|
|
|
770
957
|
this._overlay.setEdgeEndpoints(null, "");
|
|
771
958
|
}
|
|
772
959
|
this._overlay.setSelection(ids, this._shapes, this._getResizableIds());
|
|
960
|
+
this._announceSelection(ids);
|
|
773
961
|
this._emit("editor:select", ids);
|
|
774
962
|
}
|
|
963
|
+
/** Human-readable name for an element (its label, else its type). */
|
|
964
|
+
_displayName(id) {
|
|
965
|
+
const el = this._shapes.find((s) => s.id === id)?.flowElement;
|
|
966
|
+
return el?.name ?? el?.type ?? id;
|
|
967
|
+
}
|
|
968
|
+
/** Pushes `message` to the aria-live region for assistive technology. */
|
|
969
|
+
_announce(message) {
|
|
970
|
+
// Re-assigning identical text does not re-trigger some screen readers, so
|
|
971
|
+
// clear first when the message is unchanged.
|
|
972
|
+
if (this._liveRegion.textContent === message)
|
|
973
|
+
this._liveRegion.textContent = "";
|
|
974
|
+
this._liveRegion.textContent = message;
|
|
975
|
+
}
|
|
976
|
+
_announceSelection(ids) {
|
|
977
|
+
// A cleared selection is left unannounced — it is usually the tail of an
|
|
978
|
+
// edit (delete, cut) whose own result is the meaningful announcement.
|
|
979
|
+
if (ids.length === 0)
|
|
980
|
+
return;
|
|
981
|
+
if (ids.length === 1 && ids[0]) {
|
|
982
|
+
this._announce(this._t("{name} selected", { name: this._displayName(ids[0]) }));
|
|
983
|
+
}
|
|
984
|
+
else {
|
|
985
|
+
this._announce(this._t("{count} elements selected", { count: ids.length }));
|
|
986
|
+
}
|
|
987
|
+
}
|
|
775
988
|
_previewTranslate(dx, dy) {
|
|
776
989
|
if (!this._isDragging) {
|
|
777
990
|
this._isDragging = true;
|
|
@@ -833,10 +1046,10 @@ export class BpmnEditor {
|
|
|
833
1046
|
const moves = this._selectedIds.map((id) => ({ id, dx: snap.dx, dy: snap.dy }));
|
|
834
1047
|
const shapeId = this._selectedIds.length === 1 ? this._selectedIds[0] : undefined;
|
|
835
1048
|
if (edgeDropId && shapeId) {
|
|
836
|
-
this._executeCommand((d) => insertShapeOnEdge(moveShapes(d, moves), edgeDropId, shapeId));
|
|
1049
|
+
this._executeCommand((d) => insertShapeOnEdge(moveShapes(d, moves), edgeDropId, shapeId), "Insert on flow");
|
|
837
1050
|
}
|
|
838
1051
|
else {
|
|
839
|
-
this._executeCommand((d) => moveShapes(d, moves));
|
|
1052
|
+
this._executeCommand((d) => moveShapes(d, moves), "Move");
|
|
840
1053
|
}
|
|
841
1054
|
if (this._isDragging) {
|
|
842
1055
|
this._isDragging = false;
|
|
@@ -911,7 +1124,7 @@ export class BpmnEditor {
|
|
|
911
1124
|
}
|
|
912
1125
|
}
|
|
913
1126
|
if (moves.length > 0) {
|
|
914
|
-
this._executeCommand((d) => moveShapes(d, moves));
|
|
1127
|
+
this._executeCommand((d) => moveShapes(d, moves), "Move");
|
|
915
1128
|
}
|
|
916
1129
|
}
|
|
917
1130
|
_cancelSpace() {
|
|
@@ -985,7 +1198,7 @@ export class BpmnEditor {
|
|
|
985
1198
|
.filter((s) => s.id !== srcId && s.id !== tgtId)
|
|
986
1199
|
.map((s) => s.shape.bounds);
|
|
987
1200
|
const waypoints = computeWaypointsAvoiding(srcShape.shape.bounds, tgtShape.shape.bounds, obstacles);
|
|
988
|
-
this._executeCommand((d) => createConnection(d, srcId, tgtId, waypoints).defs);
|
|
1201
|
+
this._executeCommand((d) => createConnection(d, srcId, tgtId, waypoints).defs, "Connect");
|
|
989
1202
|
}
|
|
990
1203
|
_doCopy() {
|
|
991
1204
|
if (!this._defs || this._selectedIds.length === 0)
|
|
@@ -997,12 +1210,19 @@ export class BpmnEditor {
|
|
|
997
1210
|
return;
|
|
998
1211
|
const base = this._defs ?? createEmptyDefinitions();
|
|
999
1212
|
const result = pasteElements(base, this._clipboard, 20, 20);
|
|
1000
|
-
|
|
1001
|
-
this.
|
|
1002
|
-
this._commandStack.push(result.defs);
|
|
1213
|
+
this._selectedIds = result.topLevelIds;
|
|
1214
|
+
this._commandStack.push(result.defs, "Paste");
|
|
1003
1215
|
this._renderDefs(result.defs);
|
|
1004
1216
|
this._emit("diagram:change", result.defs);
|
|
1005
|
-
this._emit("editor:select",
|
|
1217
|
+
this._emit("editor:select", result.topLevelIds);
|
|
1218
|
+
}
|
|
1219
|
+
_doCut() {
|
|
1220
|
+
if (!this._defs || this._selectedIds.length === 0)
|
|
1221
|
+
return;
|
|
1222
|
+
this._doCopy();
|
|
1223
|
+
const ids = [...this._selectedIds];
|
|
1224
|
+
this._executeCommand((d) => deleteElements(d, ids), "Cut");
|
|
1225
|
+
this._setSelection([]);
|
|
1006
1226
|
}
|
|
1007
1227
|
_startLabelEdit(id) {
|
|
1008
1228
|
if (this._readOnly || !id)
|
|
@@ -1021,15 +1241,31 @@ export class BpmnEditor {
|
|
|
1021
1241
|
this._labelEditor.start(id, currentText, shape.shape.bounds, this._viewport.state, this._svg.getBoundingClientRect());
|
|
1022
1242
|
}
|
|
1023
1243
|
_connectSourceBounds() {
|
|
1244
|
+
const sourceId = this._connectSourceId();
|
|
1245
|
+
if (!sourceId)
|
|
1246
|
+
return null;
|
|
1247
|
+
const shape = this._shapes.find((s) => s.id === sourceId);
|
|
1248
|
+
return shape ? shape.shape.bounds : null;
|
|
1249
|
+
}
|
|
1250
|
+
_connectSourceId() {
|
|
1024
1251
|
const mode = this._stateMachine.mode;
|
|
1025
1252
|
if (mode.mode !== "select")
|
|
1026
1253
|
return null;
|
|
1027
1254
|
const sub = mode.sub;
|
|
1028
|
-
|
|
1255
|
+
return sub.name === "connecting" || sub.name === "pointing-port" ? sub.sourceId : null;
|
|
1256
|
+
}
|
|
1257
|
+
/** True when hovering a target the connect tool would reject (self, or a rule violation). */
|
|
1258
|
+
_isConnectTargetInvalid(targetId) {
|
|
1259
|
+
if (!targetId)
|
|
1260
|
+
return false;
|
|
1261
|
+
const sourceId = this._connectSourceId();
|
|
1029
1262
|
if (!sourceId)
|
|
1030
|
-
return
|
|
1031
|
-
|
|
1032
|
-
|
|
1263
|
+
return false;
|
|
1264
|
+
if (targetId === sourceId)
|
|
1265
|
+
return true;
|
|
1266
|
+
const srcType = this._shapes.find((s) => s.id === sourceId)?.flowElement?.type;
|
|
1267
|
+
const tgtType = this._shapes.find((s) => s.id === targetId)?.flowElement?.type;
|
|
1268
|
+
return srcType !== undefined && tgtType !== undefined && !canConnect(srcType, tgtType);
|
|
1033
1269
|
}
|
|
1034
1270
|
// ── New public helpers ─────────────────────────────────────────────
|
|
1035
1271
|
/** Returns screen-space bounds of a shape (for positioning overlays). */
|
|
@@ -1202,7 +1438,7 @@ export class BpmnEditor {
|
|
|
1202
1438
|
if (!shape)
|
|
1203
1439
|
return;
|
|
1204
1440
|
const labelBounds = labelBoundsForPosition(shape.shape.bounds, position);
|
|
1205
|
-
this._executeCommand((d) => updateLabelPosition(d, shapeId, labelBounds));
|
|
1441
|
+
this._executeCommand((d) => updateLabelPosition(d, shapeId, labelBounds), "Move label");
|
|
1206
1442
|
}
|
|
1207
1443
|
/** Starts inline label editing for the element with the given id. */
|
|
1208
1444
|
editLabel(id) {
|
|
@@ -1254,7 +1490,7 @@ export class BpmnEditor {
|
|
|
1254
1490
|
}
|
|
1255
1491
|
/** Updates the color of a shape in the diagram. Pass `{}` to clear colors. */
|
|
1256
1492
|
updateColor(id, color) {
|
|
1257
|
-
this._executeCommand((d) => updateShapeColor(d, id, color));
|
|
1493
|
+
this._executeCommand((d) => updateShapeColor(d, id, color), "Change colour", `color:${id}`);
|
|
1258
1494
|
}
|
|
1259
1495
|
// ── Private helpers ────────────────────────────────────────────────
|
|
1260
1496
|
_setEdgeSelected(edgeId) {
|
|
@@ -1276,7 +1512,10 @@ export class BpmnEditor {
|
|
|
1276
1512
|
}
|
|
1277
1513
|
/** Changes a flow element's type (e.g. exclusiveGateway → parallelGateway). */
|
|
1278
1514
|
changeElementType(id, newType) {
|
|
1279
|
-
this.
|
|
1515
|
+
const current = this._shapes.find((s) => s.id === id)?.flowElement?.type;
|
|
1516
|
+
if (current && !canMorph(current, newType))
|
|
1517
|
+
return;
|
|
1518
|
+
this._executeCommand((d) => changeElementTypeFn(d, id, newType), "Change type");
|
|
1280
1519
|
}
|
|
1281
1520
|
_findEdgeDropTarget(dx, dy) {
|
|
1282
1521
|
if (this._selectedIds.length !== 1)
|
|
@@ -1381,7 +1620,7 @@ export class BpmnEditor {
|
|
|
1381
1620
|
const newPort = isStart
|
|
1382
1621
|
? closestPort(diagPoint, srcDi.bounds)
|
|
1383
1622
|
: closestPort(diagPoint, tgtDi.bounds);
|
|
1384
|
-
this._executeCommand((d) => updateEdgeEndpoint(d, edgeId, isStart, newPort));
|
|
1623
|
+
this._executeCommand((d) => updateEdgeEndpoint(d, edgeId, isStart, newPort), "Reconnect");
|
|
1385
1624
|
}
|
|
1386
1625
|
_isResizable(id) {
|
|
1387
1626
|
const shape = this._shapes.find((s) => s.id === id);
|
|
@@ -1389,13 +1628,13 @@ export class BpmnEditor {
|
|
|
1389
1628
|
return false;
|
|
1390
1629
|
if (shape.annotation !== undefined)
|
|
1391
1630
|
return true;
|
|
1392
|
-
return shape.flowElement !== undefined &&
|
|
1631
|
+
return shape.flowElement !== undefined && canResize(shape.flowElement.type);
|
|
1393
1632
|
}
|
|
1394
1633
|
_getResizableIds() {
|
|
1395
1634
|
const ids = new Set();
|
|
1396
1635
|
for (const shape of this._shapes) {
|
|
1397
1636
|
if (shape.annotation !== undefined ||
|
|
1398
|
-
(shape.flowElement &&
|
|
1637
|
+
(shape.flowElement && canResize(shape.flowElement.type))) {
|
|
1399
1638
|
ids.add(shape.id);
|
|
1400
1639
|
}
|
|
1401
1640
|
}
|
|
@@ -1726,7 +1965,7 @@ export class BpmnEditor {
|
|
|
1726
1965
|
// Detect boundary event attachment target for intermediate event types
|
|
1727
1966
|
if (isIntermediateEventType(mode.elementType) && hit.type === "shape") {
|
|
1728
1967
|
const shape = this._shapes.find((s) => s.id === hit.id);
|
|
1729
|
-
if (shape?.flowElement &&
|
|
1968
|
+
if (shape?.flowElement && canAttach(shape.flowElement.type)) {
|
|
1730
1969
|
this._setBoundaryHost(hit.id);
|
|
1731
1970
|
}
|
|
1732
1971
|
else {
|
|
@@ -1745,7 +1984,28 @@ export class BpmnEditor {
|
|
|
1745
1984
|
const diag = screenToDiagram(e.clientX, e.clientY, this._viewport.state, rect);
|
|
1746
1985
|
const hit = this._hitTest(e.clientX, e.clientY);
|
|
1747
1986
|
this._stateMachine.onPointerUp(e, diag, hit);
|
|
1987
|
+
if (e.pointerType === "touch")
|
|
1988
|
+
this._detectDoubleTap(e, hit);
|
|
1748
1989
|
};
|
|
1990
|
+
/** On coarse pointers, a double-tap on a shape starts label editing (mirrors dblclick). */
|
|
1991
|
+
_detectDoubleTap(e, hit) {
|
|
1992
|
+
if (hit.type !== "shape") {
|
|
1993
|
+
this._lastTap = null;
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
const now = Date.now();
|
|
1997
|
+
const prev = this._lastTap;
|
|
1998
|
+
if (prev &&
|
|
1999
|
+
prev.id === hit.id &&
|
|
2000
|
+
now - prev.t < 300 &&
|
|
2001
|
+
Math.hypot(e.clientX - prev.x, e.clientY - prev.y) < 20) {
|
|
2002
|
+
this._lastTap = null;
|
|
2003
|
+
this._startLabelEdit(hit.id);
|
|
2004
|
+
}
|
|
2005
|
+
else {
|
|
2006
|
+
this._lastTap = { t: now, x: e.clientX, y: e.clientY, id: hit.id };
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
1749
2009
|
_onPointerCancel = (_e) => {
|
|
1750
2010
|
this._stateMachine.cancel();
|
|
1751
2011
|
};
|
|
@@ -1793,6 +2053,10 @@ export class BpmnEditor {
|
|
|
1793
2053
|
e.preventDefault();
|
|
1794
2054
|
this._doPaste();
|
|
1795
2055
|
break;
|
|
2056
|
+
case "x":
|
|
2057
|
+
e.preventDefault();
|
|
2058
|
+
this._doCut();
|
|
2059
|
+
break;
|
|
1796
2060
|
}
|
|
1797
2061
|
}
|
|
1798
2062
|
};
|
|
@@ -1841,14 +2105,20 @@ export class BpmnEditor {
|
|
|
1841
2105
|
// can cause elementFromPoint to return the wrong edge's hit area when
|
|
1842
2106
|
// edges are close together.
|
|
1843
2107
|
const seg = this._nearestEdgeSegment(diag);
|
|
1844
|
-
if (seg)
|
|
2108
|
+
if (seg) {
|
|
2109
|
+
// A press near the segment midpoint inserts a waypoint; a press
|
|
2110
|
+
// elsewhere along the segment moves the whole segment.
|
|
2111
|
+
const midDist = Math.hypot(seg.projPt.x - seg.mid.x, seg.projPt.y - seg.mid.y);
|
|
2112
|
+
const nearMidpoint = midDist < 12 / this._viewport.state.scale;
|
|
1845
2113
|
return {
|
|
1846
2114
|
type: "edge-segment",
|
|
1847
2115
|
id: seg.edgeId,
|
|
1848
2116
|
segIdx: seg.segIdx,
|
|
1849
2117
|
isHoriz: seg.isHoriz,
|
|
1850
2118
|
projPt: seg.projPt,
|
|
2119
|
+
nearMidpoint,
|
|
1851
2120
|
};
|
|
2121
|
+
}
|
|
1852
2122
|
}
|
|
1853
2123
|
const shapeEl = el.closest("[data-bpmnkit-id]");
|
|
1854
2124
|
if (shapeEl && (this._shapesG.contains(shapeEl) || this._containersG.contains(shapeEl))) {
|
|
@@ -1864,6 +2134,7 @@ export class BpmnEditor {
|
|
|
1864
2134
|
let bestEdgeId = "";
|
|
1865
2135
|
let bestIdx = 0;
|
|
1866
2136
|
let bestProj = { x: 0, y: 0 };
|
|
2137
|
+
let bestMid = { x: 0, y: 0 };
|
|
1867
2138
|
let bestHoriz = true;
|
|
1868
2139
|
let found = false;
|
|
1869
2140
|
for (const edge of this._edges) {
|
|
@@ -1890,6 +2161,7 @@ export class BpmnEditor {
|
|
|
1890
2161
|
bestEdgeId = edge.id;
|
|
1891
2162
|
bestIdx = i;
|
|
1892
2163
|
bestProj = proj;
|
|
2164
|
+
bestMid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
1893
2165
|
bestHoriz = Math.abs(dy) <= Math.abs(dx);
|
|
1894
2166
|
found = true;
|
|
1895
2167
|
}
|
|
@@ -1897,7 +2169,13 @@ export class BpmnEditor {
|
|
|
1897
2169
|
}
|
|
1898
2170
|
if (!found)
|
|
1899
2171
|
return null;
|
|
1900
|
-
return {
|
|
2172
|
+
return {
|
|
2173
|
+
edgeId: bestEdgeId,
|
|
2174
|
+
segIdx: bestIdx,
|
|
2175
|
+
isHoriz: bestHoriz,
|
|
2176
|
+
projPt: bestProj,
|
|
2177
|
+
mid: bestMid,
|
|
2178
|
+
};
|
|
1901
2179
|
}
|
|
1902
2180
|
/** Snaps a diagram point to nearby shape/waypoint positions and returns guide lines. */
|
|
1903
2181
|
_snapWaypoint(pt) {
|
|
@@ -1954,6 +2232,41 @@ export class BpmnEditor {
|
|
|
1954
2232
|
this._host.setAttribute("data-theme", resolved);
|
|
1955
2233
|
}
|
|
1956
2234
|
}
|
|
2235
|
+
/** Finds the SVG group for a shape or edge by BPMN id. */
|
|
2236
|
+
_elementById(id) {
|
|
2237
|
+
return (this._shapes.find((s) => s.id === id)?.element ??
|
|
2238
|
+
this._edges.find((e) => e.id === id)?.element);
|
|
2239
|
+
}
|
|
2240
|
+
/** Element bounding box in screen pixels relative to the host, or `null`. */
|
|
2241
|
+
_absoluteBBox(id) {
|
|
2242
|
+
const box = this._boundsById(id);
|
|
2243
|
+
if (!box)
|
|
2244
|
+
return null;
|
|
2245
|
+
const { tx, ty, scale } = this._viewport.state;
|
|
2246
|
+
return {
|
|
2247
|
+
x: box.x * scale + tx,
|
|
2248
|
+
y: box.y * scale + ty,
|
|
2249
|
+
width: box.width * scale,
|
|
2250
|
+
height: box.height * scale,
|
|
2251
|
+
};
|
|
2252
|
+
}
|
|
2253
|
+
/** Diagram-coordinate bounds of a shape (from DI) or edge (waypoint bbox). */
|
|
2254
|
+
_boundsById(id) {
|
|
2255
|
+
const shape = this._shapes.find((s) => s.id === id);
|
|
2256
|
+
if (shape) {
|
|
2257
|
+
const { x, y, width, height } = shape.shape.bounds;
|
|
2258
|
+
return { x, y, width, height };
|
|
2259
|
+
}
|
|
2260
|
+
const edge = this._edges.find((e) => e.id === id);
|
|
2261
|
+
if (edge && edge.edge.waypoints.length > 0) {
|
|
2262
|
+
const xs = edge.edge.waypoints.map((w) => w.x);
|
|
2263
|
+
const ys = edge.edge.waypoints.map((w) => w.y);
|
|
2264
|
+
const minX = Math.min(...xs);
|
|
2265
|
+
const minY = Math.min(...ys);
|
|
2266
|
+
return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
|
|
2267
|
+
}
|
|
2268
|
+
return null;
|
|
2269
|
+
}
|
|
1957
2270
|
_installPlugin(plugin) {
|
|
1958
2271
|
this._plugins.push(plugin);
|
|
1959
2272
|
const self = this;
|
|
@@ -1967,6 +2280,42 @@ export class BpmnEditor {
|
|
|
1967
2280
|
getEdges: () => [...this._edges],
|
|
1968
2281
|
getTheme: () => this._theme,
|
|
1969
2282
|
setTheme: (theme) => this.setTheme(theme),
|
|
2283
|
+
overlays: this._htmlOverlays,
|
|
2284
|
+
addMarker(id, cls) {
|
|
2285
|
+
self._elementById(id)?.classList.add(cls);
|
|
2286
|
+
},
|
|
2287
|
+
removeMarker(id, cls) {
|
|
2288
|
+
self._elementById(id)?.classList.remove(cls);
|
|
2289
|
+
},
|
|
2290
|
+
hasMarker(id, cls) {
|
|
2291
|
+
return self._elementById(id)?.classList.contains(cls) ?? false;
|
|
2292
|
+
},
|
|
2293
|
+
toggleMarker(id, cls) {
|
|
2294
|
+
self._elementById(id)?.classList.toggle(cls);
|
|
2295
|
+
},
|
|
2296
|
+
zoom(scaleOrFit = "fit") {
|
|
2297
|
+
if (scaleOrFit === "fit")
|
|
2298
|
+
self.fitView();
|
|
2299
|
+
else
|
|
2300
|
+
self.setZoom(scaleOrFit);
|
|
2301
|
+
},
|
|
2302
|
+
viewbox() {
|
|
2303
|
+
const { tx, ty, scale } = self._viewport.state;
|
|
2304
|
+
const { width, height } = self._svg.getBoundingClientRect();
|
|
2305
|
+
return {
|
|
2306
|
+
x: -tx / scale,
|
|
2307
|
+
y: -ty / scale,
|
|
2308
|
+
width: width / scale,
|
|
2309
|
+
height: height / scale,
|
|
2310
|
+
scale,
|
|
2311
|
+
};
|
|
2312
|
+
},
|
|
2313
|
+
scrollToElement(id) {
|
|
2314
|
+
self.scrollToElement(id);
|
|
2315
|
+
},
|
|
2316
|
+
getAbsoluteBBox(id) {
|
|
2317
|
+
return self._absoluteBBox(id);
|
|
2318
|
+
},
|
|
1970
2319
|
on(event, handler) {
|
|
1971
2320
|
return self.on(event, handler);
|
|
1972
2321
|
},
|