@bpmnkit/editor 0.0.30 → 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/hud.js CHANGED
@@ -57,6 +57,7 @@ const SHORTCUTS = [
57
57
  ["Delete / Backspace", "Delete selected"],
58
58
  ["Ctrl+A", "Select all"],
59
59
  ["Ctrl+C", "Copy"],
60
+ ["Ctrl+X", "Cut"],
60
61
  ["Ctrl+V", "Paste"],
61
62
  ["Ctrl+F", "Find element"],
62
63
  ["Ctrl+K", "Command palette"],
@@ -69,6 +70,7 @@ const SHORTCUTS = [
69
70
  ];
70
71
  export function initEditorHud(editor, options = {}) {
71
72
  injectHudStyles();
73
+ const t = editor.translate;
72
74
  // ── Create and inject HUD DOM ──────────────────────────────────────────────
73
75
  function hudBtn(id, title) {
74
76
  const b = document.createElement("button");
@@ -176,10 +178,10 @@ export function initEditorHud(editor, options = {}) {
176
178
  simBannerEl.id = "bpmnkit-sim-banner";
177
179
  simBannerEl.className = "hidden";
178
180
  const simBannerText = document.createElement("span");
179
- simBannerText.textContent = "Simulation active — editing is disabled";
181
+ simBannerText.textContent = t("Simulation active — editing is disabled");
180
182
  const simBannerExit = document.createElement("button");
181
183
  simBannerExit.id = "bpmnkit-sim-banner-exit";
182
- simBannerExit.textContent = "Exit simulation";
184
+ simBannerExit.textContent = t("Exit simulation");
183
185
  simBannerExit.addEventListener("click", () => options.onExitSimulation?.());
184
186
  simBannerEl.append(simBannerText, simBannerExit);
185
187
  // Right-click context menu
@@ -193,14 +195,14 @@ export function initEditorHud(editor, options = {}) {
193
195
  const searchInput = document.createElement("input");
194
196
  searchInput.id = "bpmnkit-search-input";
195
197
  searchInput.type = "text";
196
- searchInput.placeholder = "Search elements…";
197
- searchInput.setAttribute("aria-label", "Search diagram elements");
198
+ searchInput.placeholder = t("Search elements…");
199
+ searchInput.setAttribute("aria-label", t("Search diagram elements"));
198
200
  const searchCount = document.createElement("span");
199
201
  searchCount.id = "bpmnkit-search-count";
200
202
  const searchClose = document.createElement("button");
201
203
  searchClose.id = "bpmnkit-search-close";
202
204
  searchClose.textContent = "×";
203
- searchClose.title = "Close search (Escape)";
205
+ searchClose.title = t("Close search (Escape)");
204
206
  searchBarEl.append(searchInput, searchCount, searchClose);
205
207
  // Keyboard shortcuts modal
206
208
  const shortcutsModal = document.createElement("div");
@@ -209,13 +211,13 @@ export function initEditorHud(editor, options = {}) {
209
211
  const shortcutsInner = document.createElement("div");
210
212
  shortcutsInner.id = "bpmnkit-shortcuts-inner";
211
213
  const shortcutsTitle = document.createElement("h3");
212
- shortcutsTitle.textContent = "Keyboard shortcuts";
214
+ shortcutsTitle.textContent = t("Keyboard shortcuts");
213
215
  shortcutsInner.appendChild(shortcutsTitle);
214
216
  for (const [key, desc] of SHORTCUTS) {
215
217
  const row = document.createElement("div");
216
218
  row.className = "bpmnkit-sc-row";
217
219
  const descEl = document.createElement("span");
218
- descEl.textContent = desc;
220
+ descEl.textContent = t(desc);
219
221
  const keyEl = document.createElement("kbd");
220
222
  keyEl.className = "bpmnkit-sc-key";
221
223
  keyEl.textContent = key;
@@ -224,7 +226,7 @@ export function initEditorHud(editor, options = {}) {
224
226
  }
225
227
  const shortcutsClose = document.createElement("button");
226
228
  shortcutsClose.id = "bpmnkit-shortcuts-close";
227
- shortcutsClose.textContent = "Close";
229
+ shortcutsClose.textContent = t("Close");
228
230
  shortcutsClose.addEventListener("click", () => shortcutsModal.classList.add("hidden"));
229
231
  shortcutsModal.addEventListener("click", (e) => {
230
232
  if (e.target === shortcutsModal)
@@ -310,13 +312,13 @@ export function initEditorHud(editor, options = {}) {
310
312
  picker.className = "group-picker";
311
313
  const label = document.createElement("span");
312
314
  label.className = "group-picker-label";
313
- label.textContent = group.title;
315
+ label.textContent = t(group.title);
314
316
  picker.appendChild(label);
315
317
  for (const item of group.items) {
316
318
  const btn = document.createElement("button");
317
319
  btn.className = item.type === groupActiveType[group.id] ? "hud-btn active" : "hud-btn";
318
320
  btn.innerHTML = item.icon;
319
- btn.title = item.title;
321
+ btn.title = t(item.title);
320
322
  btn.addEventListener("click", (e) => {
321
323
  e.stopPropagation();
322
324
  groupActiveType[group.id] = item.type;
@@ -352,13 +354,13 @@ export function initEditorHud(editor, options = {}) {
352
354
  picker.className = "group-picker";
353
355
  const label = document.createElement("span");
354
356
  label.className = "group-picker-label";
355
- label.textContent = group.title;
357
+ label.textContent = t(group.title);
356
358
  picker.appendChild(label);
357
359
  for (const item of group.items) {
358
360
  const btn = document.createElement("button");
359
361
  btn.className = item.type === sourceType ? "hud-btn active" : "hud-btn";
360
362
  btn.innerHTML = item.icon;
361
- btn.title = item.title;
363
+ btn.title = t(item.title);
362
364
  btn.addEventListener("click", (e) => {
363
365
  e.stopPropagation();
364
366
  if (item.type !== sourceType)
@@ -441,7 +443,7 @@ export function initEditorHud(editor, options = {}) {
441
443
  btn.className = "hud-btn";
442
444
  btn.dataset.group = group.id;
443
445
  btn.innerHTML = group.groupIcon;
444
- btn.title = `${group.title} (hold for options)`;
446
+ btn.title = t("{name} (hold for options)", { name: t(group.title) });
445
447
  let longPressTimer = null;
446
448
  let isLongPress = false;
447
449
  btn.addEventListener("pointerdown", (e) => {
@@ -648,6 +650,45 @@ export function initEditorHud(editor, options = {}) {
648
650
  },
649
651
  ],
650
652
  ];
653
+ // Align / distribute — only meaningful for a multi-selection.
654
+ if (selectedIds.length >= 2) {
655
+ const aligns = [
656
+ ["Align left", "left"],
657
+ ["Align centre", "center"],
658
+ ["Align right", "right"],
659
+ ["Align top", "top"],
660
+ ["Align middle", "middle"],
661
+ ["Align bottom", "bottom"],
662
+ ];
663
+ for (const [label, edge] of aligns) {
664
+ items.push([
665
+ label,
666
+ IC.dots,
667
+ () => {
668
+ editor.alignSelected(edge);
669
+ closeAllDropdowns();
670
+ },
671
+ ]);
672
+ }
673
+ }
674
+ if (selectedIds.length >= 3) {
675
+ items.push([
676
+ "Distribute horizontally",
677
+ IC.dots,
678
+ () => {
679
+ editor.distributeSelected("horizontal");
680
+ closeAllDropdowns();
681
+ },
682
+ ]);
683
+ items.push([
684
+ "Distribute vertically",
685
+ IC.dots,
686
+ () => {
687
+ editor.distributeSelected("vertical");
688
+ closeAllDropdowns();
689
+ },
690
+ ]);
691
+ }
651
692
  if (options.optimizeButton) {
652
693
  items.push([
653
694
  "Optimize diagram",
@@ -702,6 +743,10 @@ export function initEditorHud(editor, options = {}) {
702
743
  btnRedo.disabled = !editor.canRedo();
703
744
  btnDelete.disabled = selectedIds.length === 0;
704
745
  btnDuplicate.disabled = selectedIds.length === 0;
746
+ const undoLabel = editor.getUndoLabel();
747
+ const redoLabel = editor.getRedoLabel();
748
+ btnUndo.title = undoLabel ? `Undo ${undoLabel} (Ctrl+Z)` : "Undo (Ctrl+Z)";
749
+ btnRedo.title = redoLabel ? `Redo ${redoLabel} (Ctrl+Y)` : "Redo (Ctrl+Y)";
705
750
  }
706
751
  updateActionBar();
707
752
  btnUndo.addEventListener("click", () => {
@@ -790,7 +835,7 @@ export function initEditorHud(editor, options = {}) {
790
835
  const btn = document.createElement("button");
791
836
  btn.className = "hud-btn";
792
837
  btn.innerHTML = opt.icon;
793
- btn.title = opt.title;
838
+ btn.title = t(opt.title);
794
839
  btn.addEventListener("click", () => {
795
840
  editor.addConnectedElement(sourceId, opt.type);
796
841
  closeAllDropdowns();
@@ -856,7 +901,7 @@ export function initEditorHud(editor, options = {}) {
856
901
  const typeBtn = document.createElement("button");
857
902
  typeBtn.className = "hud-btn active";
858
903
  typeBtn.innerHTML = currentItem?.icon ?? group.groupIcon;
859
- typeBtn.title = `${currentItem?.title ?? group.title} (click to change)`;
904
+ typeBtn.title = t("{name} (click to change)", { name: t(currentItem?.title ?? group.title) });
860
905
  typeBtn.addEventListener("click", () => showTypePicker(typeBtn, group, sourceId, sourceType));
861
906
  cfgToolbar.appendChild(typeBtn);
862
907
  }
@@ -1461,7 +1506,7 @@ export function initEditorHud(editor, options = {}) {
1461
1506
  function makeCtxItem(icon, label, onClick) {
1462
1507
  const btn = document.createElement("button");
1463
1508
  btn.className = "drop-item";
1464
- btn.innerHTML = `<span class="di-check"></span><span class="di-icon">${icon}</span><span>${label}</span>`;
1509
+ btn.innerHTML = `<span class="di-check"></span><span class="di-icon">${icon}</span><span>${t(label)}</span>`;
1465
1510
  btn.addEventListener("click", onClick);
1466
1511
  return btn;
1467
1512
  }
@@ -1518,12 +1563,45 @@ export function initEditorHud(editor, options = {}) {
1518
1563
  editor.duplicate();
1519
1564
  closeAllDropdowns();
1520
1565
  }));
1566
+ ctxMenuEl.appendChild(makeCtxItem(IC.duplicate, "Cut", () => {
1567
+ editor.cut();
1568
+ closeAllDropdowns();
1569
+ }));
1521
1570
  ctxMenuEl.appendChild(makeCtxItem(IC.trash, "Delete", () => {
1522
1571
  editor.deleteSelected();
1523
1572
  closeAllDropdowns();
1524
1573
  }));
1525
1574
  openCtxMenu(e.clientX, e.clientY);
1526
1575
  });
1576
+ // Touch: a long-press opens the same context menu (dispatched as a synthetic
1577
+ // contextmenu event at the press point, so the handler above is reused).
1578
+ let lpTimer = null;
1579
+ let lpStart = null;
1580
+ const cancelLongPress = () => {
1581
+ if (lpTimer) {
1582
+ clearTimeout(lpTimer);
1583
+ lpTimer = null;
1584
+ }
1585
+ };
1586
+ editor.container.addEventListener("pointerdown", (e) => {
1587
+ if (e.pointerType !== "touch")
1588
+ return;
1589
+ lpStart = { x: e.clientX, y: e.clientY };
1590
+ lpTimer = setTimeout(() => {
1591
+ lpTimer = null;
1592
+ if (!lpStart)
1593
+ return;
1594
+ editor.container.dispatchEvent(new MouseEvent("contextmenu", { bubbles: true, clientX: lpStart.x, clientY: lpStart.y }));
1595
+ }, 500);
1596
+ });
1597
+ editor.container.addEventListener("pointermove", (e) => {
1598
+ // A drag past the slop threshold is a pan/move, not a long-press.
1599
+ if (lpTimer && lpStart && Math.hypot(e.clientX - lpStart.x, e.clientY - lpStart.y) > 10) {
1600
+ cancelLongPress();
1601
+ }
1602
+ });
1603
+ editor.container.addEventListener("pointerup", cancelLongPress);
1604
+ editor.container.addEventListener("pointercancel", cancelLongPress);
1527
1605
  // ── Element search (Ctrl+F) ─────────────────────────────────────────────────
1528
1606
  let searchMatches = [];
1529
1607
  let searchMatchIdx = 0;
@@ -1554,31 +1632,17 @@ export function initEditorHud(editor, options = {}) {
1554
1632
  }
1555
1633
  }
1556
1634
  function runSearch() {
1557
- const q = searchInput.value.trim().toLowerCase();
1635
+ const q = searchInput.value.trim();
1558
1636
  if (!q) {
1559
1637
  searchCount.textContent = "";
1560
1638
  searchMatches = [];
1561
1639
  editor.setSelection([]);
1562
1640
  return;
1563
1641
  }
1564
- const defs = editor.getDefinitions();
1565
- if (!defs)
1566
- return;
1567
- const ids = [];
1568
- for (const proc of defs.processes) {
1569
- for (const el of proc.flowElements) {
1570
- if (el.name?.toLowerCase().includes(q))
1571
- ids.push(el.id);
1572
- }
1573
- for (const el of proc.textAnnotations) {
1574
- if (el.text?.toLowerCase().includes(q))
1575
- ids.push(el.id);
1576
- }
1577
- }
1578
- searchMatches = ids;
1642
+ searchMatches = editor.find(q).map((m) => m.id);
1579
1643
  searchMatchIdx = 0;
1580
1644
  updateSearchCount();
1581
- const firstId = ids[0];
1645
+ const firstId = searchMatches[0];
1582
1646
  if (firstId) {
1583
1647
  selectSearchMatch(firstId);
1584
1648
  }
@@ -1586,21 +1650,30 @@ export function initEditorHud(editor, options = {}) {
1586
1650
  editor.setSelection([]);
1587
1651
  }
1588
1652
  }
1653
+ /** Moves to the next/previous match, wrapping, and selects it. */
1654
+ function stepSearch(dir) {
1655
+ if (searchMatches.length === 0)
1656
+ return;
1657
+ searchMatchIdx = (searchMatchIdx + dir + searchMatches.length) % searchMatches.length;
1658
+ const id = searchMatches[searchMatchIdx];
1659
+ updateSearchCount();
1660
+ if (id)
1661
+ selectSearchMatch(id);
1662
+ }
1589
1663
  searchInput.addEventListener("input", runSearch);
1590
1664
  searchInput.addEventListener("keydown", (e) => {
1591
1665
  if (e.key === "Escape") {
1592
1666
  e.preventDefault();
1593
1667
  closeSearch();
1594
1668
  }
1595
- if (e.key === "Enter") {
1669
+ else if (e.key === "Enter" || e.key === "ArrowDown") {
1596
1670
  e.preventDefault();
1597
- if (searchMatches.length > 0) {
1598
- searchMatchIdx = (searchMatchIdx + 1) % searchMatches.length;
1599
- const nextId = searchMatches[searchMatchIdx];
1600
- updateSearchCount();
1601
- if (nextId)
1602
- selectSearchMatch(nextId);
1603
- }
1671
+ // Enter on the first result keeps it selected; subsequent presses step.
1672
+ stepSearch(1);
1673
+ }
1674
+ else if (e.key === "ArrowUp") {
1675
+ e.preventDefault();
1676
+ stepSearch(-1);
1604
1677
  }
1605
1678
  });
1606
1679
  searchClose.addEventListener("click", closeSearch);
package/dist/i18n.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ /** Interpolation variables for a translation template. */
2
+ export type TranslateVars = Record<string, string | number>;
3
+ /**
4
+ * A translation hook. Receives an English template string (used as the lookup
5
+ * key) plus optional `{name}` interpolation variables and returns the localized
6
+ * string. Keys that are not localized should be returned unchanged (after
7
+ * interpolation) — {@link defaultTranslate} does exactly that.
8
+ */
9
+ export type Translate = (template: string, vars?: TranslateVars) => string;
10
+ /** Fills `{name}` placeholders in `template` from `vars`. */
11
+ export declare function interpolate(template: string, vars?: TranslateVars): string;
12
+ /** The default (identity) translator: no localization, just placeholder interpolation. */
13
+ export declare function defaultTranslate(template: string, vars?: TranslateVars): string;
14
+ //# sourceMappingURL=i18n.d.ts.map
package/dist/i18n.js ADDED
@@ -0,0 +1,11 @@
1
+ /** Fills `{name}` placeholders in `template` from `vars`. */
2
+ export function interpolate(template, vars) {
3
+ if (!vars)
4
+ return template;
5
+ return template.replace(/\{(\w+)\}/g, (match, key) => key in vars ? String(vars[key]) : match);
6
+ }
7
+ /** The default (identity) translator: no localization, just placeholder interpolation. */
8
+ export function defaultTranslate(template, vars) {
9
+ return interpolate(template, vars);
10
+ }
11
+ //# sourceMappingURL=i18n.js.map
package/dist/index.d.ts CHANGED
@@ -5,6 +5,8 @@ export { ELEMENT_GROUPS, ELEMENT_TYPE_LABELS, EXTERNAL_LABEL_TYPES, CONTEXTUAL_A
5
5
  export type { ElementGroup } from "./element-groups.js";
6
6
  export { initEditorHud } from "./hud.js";
7
7
  export type { HudOptions } from "./hud.js";
8
+ export { defaultTranslate, interpolate } from "./i18n.js";
9
+ export type { Translate, TranslateVars } from "./i18n.js";
8
10
  export { createSideDock } from "./dock.js";
9
11
  export type { SideDock } from "./dock.js";
10
12
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -2,5 +2,6 @@ export { BpmnEditor } from "./editor.js";
2
2
  export { createEmptyDefinitions } from "./modeling.js";
3
3
  export { ELEMENT_GROUPS, ELEMENT_TYPE_LABELS, EXTERNAL_LABEL_TYPES, CONTEXTUAL_ADD_TYPES, getElementGroup, getValidLabelPositions, } from "./element-groups.js";
4
4
  export { initEditorHud } from "./hud.js";
5
+ export { defaultTranslate, interpolate } from "./i18n.js";
5
6
  export { createSideDock } from "./dock.js";
6
7
  //# sourceMappingURL=index.js.map
@@ -77,6 +77,7 @@ export declare function copyElements(defs: BpmnDefinitions, ids: string[]): Clip
77
77
  export declare function pasteElements(defs: BpmnDefinitions, clipboard: Clipboard, offsetX: number, offsetY: number): {
78
78
  defs: BpmnDefinitions;
79
79
  newIds: Map<string, string>;
80
+ topLevelIds: string[];
80
81
  };
81
82
  export declare function createAnnotation(defs: BpmnDefinitions, bounds: BpmnBounds, text?: string): {
82
83
  defs: BpmnDefinitions;
package/dist/modeling.js CHANGED
@@ -29,6 +29,7 @@ export function createEmptyDefinitions() {
29
29
  sequenceFlows: [],
30
30
  textAnnotations: [],
31
31
  associations: [],
32
+ groups: [],
32
33
  unknownAttributes: {},
33
34
  },
34
35
  ],
@@ -142,6 +143,7 @@ function makeFlowElement(type, id, name) {
142
143
  sequenceFlows: [],
143
144
  textAnnotations: [],
144
145
  associations: [],
146
+ groups: [],
145
147
  };
146
148
  case "adHocSubProcess":
147
149
  return {
@@ -151,6 +153,7 @@ function makeFlowElement(type, id, name) {
151
153
  sequenceFlows: [],
152
154
  textAnnotations: [],
153
155
  associations: [],
156
+ groups: [],
154
157
  };
155
158
  case "transaction":
156
159
  return {
@@ -160,6 +163,7 @@ function makeFlowElement(type, id, name) {
160
163
  sequenceFlows: [],
161
164
  textAnnotations: [],
162
165
  associations: [],
166
+ groups: [],
163
167
  };
164
168
  case "exclusiveGateway":
165
169
  return { ...base, type: "exclusiveGateway" };
@@ -1071,6 +1075,21 @@ export function removeCollinearWaypoints(defs, edgeId) {
1071
1075
  };
1072
1076
  }
1073
1077
  // ── Change element type ───────────────────────────────────────────────────────
1078
+ /** Activity types that carry `loopCharacteristics` (multi-instance / loop markers). */
1079
+ const ACTIVITY_LOOP_TYPES = new Set([
1080
+ "task",
1081
+ "serviceTask",
1082
+ "userTask",
1083
+ "scriptTask",
1084
+ "sendTask",
1085
+ "receiveTask",
1086
+ "businessRuleTask",
1087
+ "manualTask",
1088
+ "callActivity",
1089
+ "subProcess",
1090
+ "adHocSubProcess",
1091
+ "transaction",
1092
+ ]);
1074
1093
  /**
1075
1094
  * Replaces a flow element's type while preserving its id, name, and connections.
1076
1095
  * Use this for gateway type-switching (exclusive ↔ parallel) and task type-switching.
@@ -1090,6 +1109,8 @@ export function changeElementType(defs, id, newType) {
1090
1109
  name: el.name,
1091
1110
  incoming: el.incoming,
1092
1111
  outgoing: el.outgoing,
1112
+ documentation: el.documentation,
1113
+ isForCompensation: el.isForCompensation,
1093
1114
  extensionElements: el.extensionElements,
1094
1115
  unknownAttributes: el.unknownAttributes,
1095
1116
  };
@@ -1214,6 +1235,7 @@ export function changeElementType(defs, id, newType) {
1214
1235
  sequenceFlows: el.type === "subProcess" ? el.sequenceFlows : [],
1215
1236
  textAnnotations: el.type === "subProcess" ? el.textAnnotations : [],
1216
1237
  associations: el.type === "subProcess" ? el.associations : [],
1238
+ groups: el.type === "subProcess" ? el.groups : [],
1217
1239
  };
1218
1240
  break;
1219
1241
  case "adHocSubProcess":
@@ -1224,6 +1246,7 @@ export function changeElementType(defs, id, newType) {
1224
1246
  sequenceFlows: el.type === "adHocSubProcess" ? el.sequenceFlows : [],
1225
1247
  textAnnotations: el.type === "adHocSubProcess" ? el.textAnnotations : [],
1226
1248
  associations: el.type === "adHocSubProcess" ? el.associations : [],
1249
+ groups: el.type === "adHocSubProcess" ? el.groups : [],
1227
1250
  };
1228
1251
  break;
1229
1252
  case "transaction":
@@ -1234,6 +1257,7 @@ export function changeElementType(defs, id, newType) {
1234
1257
  sequenceFlows: el.type === "transaction" ? el.sequenceFlows : [],
1235
1258
  textAnnotations: el.type === "transaction" ? el.textAnnotations : [],
1236
1259
  associations: el.type === "transaction" ? el.associations : [],
1260
+ groups: el.type === "transaction" ? el.groups : [],
1237
1261
  };
1238
1262
  break;
1239
1263
  case "exclusiveGateway":
@@ -1254,6 +1278,14 @@ export function changeElementType(defs, id, newType) {
1254
1278
  case "textAnnotation":
1255
1279
  throw new Error("textAnnotation is not a flow element — use createAnnotation()");
1256
1280
  }
1281
+ // Preserve a multi-instance/loop marker when morphing between activity types
1282
+ // that both support one (e.g. serviceTask ⇄ userTask, task → subProcess).
1283
+ const loop = "loopCharacteristics" in el ? el.loopCharacteristics : undefined;
1284
+ if (loop && ACTIVITY_LOOP_TYPES.has(newEl.type)) {
1285
+ ;
1286
+ newEl.loopCharacteristics =
1287
+ loop;
1288
+ }
1257
1289
  const newElements = [...process.flowElements];
1258
1290
  newElements[elIndex] = newEl;
1259
1291
  return {
@@ -1285,6 +1317,33 @@ export function insertShapeOnEdge(defs, edgeId, shapeId) {
1285
1317
  const r2 = createConnection(r1.defs, shapeId, flow.targetRef, computeWaypoints(newDi.bounds, tgtDi.bounds));
1286
1318
  return r2.defs;
1287
1319
  }
1320
+ /** True when a flow element is a container carrying its own children. */
1321
+ function hasChildren(el) {
1322
+ return "flowElements" in el;
1323
+ }
1324
+ /** Collects every id that owns a DI shape within `el` (itself + recursive children). */
1325
+ function collectShapeIds(el, out) {
1326
+ out.add(el.id);
1327
+ if (hasChildren(el)) {
1328
+ for (const child of el.flowElements)
1329
+ collectShapeIds(child, out);
1330
+ for (const ta of el.textAnnotations)
1331
+ out.add(ta.id);
1332
+ for (const g of el.groups)
1333
+ out.add(g.id);
1334
+ }
1335
+ }
1336
+ /** Collects every id that owns a DI edge within `el` (nested flows + associations). */
1337
+ function collectEdgeIds(el, out) {
1338
+ if (hasChildren(el)) {
1339
+ for (const sf of el.sequenceFlows)
1340
+ out.add(sf.id);
1341
+ for (const a of el.associations)
1342
+ out.add(a.id);
1343
+ for (const child of el.flowElements)
1344
+ collectEdgeIds(child, out);
1345
+ }
1346
+ }
1288
1347
  export function copyElements(defs, ids) {
1289
1348
  const idSet = new Set(ids);
1290
1349
  const process = defs.processes[0];
@@ -1292,45 +1351,93 @@ export function copyElements(defs, ids) {
1292
1351
  if (!process || !diagram) {
1293
1352
  return { elements: [], flows: [], shapes: [], edges: [] };
1294
1353
  }
1295
- const elements = process.flowElements.filter((el) => idSet.has(el.id));
1296
- const flows = process.sequenceFlows.filter((sf) => idSet.has(sf.sourceRef) && idSet.has(sf.targetRef));
1297
- const flowIds = new Set(flows.map((sf) => sf.id));
1298
- const shapes = diagram.plane.shapes.filter((s) => idSet.has(s.bpmnElement));
1299
- const edges = diagram.plane.edges.filter((e) => flowIds.has(e.bpmnElement));
1354
+ const selected = process.flowElements.filter((el) => idSet.has(el.id));
1355
+ // Auto-include boundary events attached to any copied host (bpmn-js parity).
1356
+ const selectedIds = new Set(selected.map((el) => el.id));
1357
+ const boundaries = process.flowElements.filter((el) => el.type === "boundaryEvent" && selectedIds.has(el.attachedToRef) && !selectedIds.has(el.id));
1358
+ const elements = [...selected, ...boundaries];
1359
+ const topIds = new Set(elements.map((el) => el.id));
1360
+ // All shape/edge ids within the copied subtree (recurses into sub-processes).
1361
+ const shapeIds = new Set();
1362
+ const edgeIds = new Set();
1363
+ for (const el of elements) {
1364
+ collectShapeIds(el, shapeIds);
1365
+ collectEdgeIds(el, edgeIds);
1366
+ }
1367
+ // Top-level flows whose endpoints are both copied.
1368
+ const flows = process.sequenceFlows.filter((sf) => topIds.has(sf.sourceRef) && topIds.has(sf.targetRef));
1369
+ for (const sf of flows)
1370
+ edgeIds.add(sf.id);
1371
+ const shapes = diagram.plane.shapes.filter((s) => shapeIds.has(s.bpmnElement));
1372
+ const edges = diagram.plane.edges.filter((e) => edgeIds.has(e.bpmnElement));
1300
1373
  return { elements, flows, shapes, edges };
1301
1374
  }
1375
+ /** Assigns a fresh id to `el` and every descendant / nested edge, recording the mapping. */
1376
+ function buildIdMap(el, map) {
1377
+ map.set(el.id, genId(el.type));
1378
+ if (hasChildren(el)) {
1379
+ for (const child of el.flowElements)
1380
+ buildIdMap(child, map);
1381
+ for (const sf of el.sequenceFlows)
1382
+ map.set(sf.id, genId("Flow"));
1383
+ for (const ta of el.textAnnotations)
1384
+ map.set(ta.id, genId("TextAnnotation"));
1385
+ for (const a of el.associations)
1386
+ map.set(a.id, genId("Association"));
1387
+ for (const g of el.groups)
1388
+ map.set(g.id, genId("Group"));
1389
+ }
1390
+ }
1391
+ /** Deep-clones `el`, remapping its own id, refs, and all nested children through `map`. */
1392
+ function remapElement(el, map) {
1393
+ const clone = {
1394
+ ...el,
1395
+ id: map.get(el.id) ?? el.id,
1396
+ incoming: el.incoming.map((r) => map.get(r)).filter((r) => r !== undefined),
1397
+ outgoing: el.outgoing.map((r) => map.get(r)).filter((r) => r !== undefined),
1398
+ };
1399
+ if (clone.type === "boundaryEvent" && el.type === "boundaryEvent") {
1400
+ clone.attachedToRef = map.get(el.attachedToRef) ?? el.attachedToRef;
1401
+ }
1402
+ if (hasChildren(el) && hasChildren(clone)) {
1403
+ clone.flowElements = el.flowElements.map((c) => remapElement(c, map));
1404
+ clone.sequenceFlows = el.sequenceFlows.map((sf) => remapFlow(sf, map));
1405
+ clone.textAnnotations = el.textAnnotations.map((ta) => ({
1406
+ ...ta,
1407
+ id: map.get(ta.id) ?? ta.id,
1408
+ }));
1409
+ clone.associations = el.associations.map((a) => ({
1410
+ ...a,
1411
+ id: map.get(a.id) ?? a.id,
1412
+ sourceRef: map.get(a.sourceRef) ?? a.sourceRef,
1413
+ targetRef: map.get(a.targetRef) ?? a.targetRef,
1414
+ }));
1415
+ clone.groups = el.groups.map((g) => ({ ...g, id: map.get(g.id) ?? g.id }));
1416
+ }
1417
+ return clone;
1418
+ }
1419
+ function remapFlow(sf, map) {
1420
+ return {
1421
+ ...sf,
1422
+ id: map.get(sf.id) ?? sf.id,
1423
+ sourceRef: map.get(sf.sourceRef) ?? sf.sourceRef,
1424
+ targetRef: map.get(sf.targetRef) ?? sf.targetRef,
1425
+ };
1426
+ }
1302
1427
  export function pasteElements(defs, clipboard, offsetX, offsetY) {
1303
1428
  const newIds = new Map();
1304
- // Generate new IDs for elements
1305
- for (const el of clipboard.elements) {
1306
- newIds.set(el.id, genId(el.type));
1307
- }
1308
- for (const sf of clipboard.flows) {
1429
+ // Generate new IDs for every element (recursively) and top-level flow.
1430
+ for (const el of clipboard.elements)
1431
+ buildIdMap(el, newIds);
1432
+ for (const sf of clipboard.flows)
1309
1433
  newIds.set(sf.id, genId("Flow"));
1310
- }
1311
1434
  const process = defs.processes[0];
1312
1435
  const diagram = defs.diagrams[0];
1313
1436
  if (!process || !diagram)
1314
- return { defs, newIds };
1315
- // Create new flow elements with new IDs, offset positions handled via DI
1316
- const newElements = clipboard.elements.map((el) => {
1317
- const newId = newIds.get(el.id) ?? genId(el.type);
1318
- const newIncoming = el.incoming
1319
- .map((ref) => newIds.get(ref))
1320
- .filter((r) => r !== undefined);
1321
- const newOutgoing = el.outgoing
1322
- .map((ref) => newIds.get(ref))
1323
- .filter((r) => r !== undefined);
1324
- return { ...el, id: newId, incoming: newIncoming, outgoing: newOutgoing };
1325
- });
1326
- // Create new sequence flows
1327
- const newFlows = clipboard.flows.map((sf) => {
1328
- const newId = newIds.get(sf.id) ?? genId("Flow");
1329
- const newSrc = newIds.get(sf.sourceRef) ?? sf.sourceRef;
1330
- const newTgt = newIds.get(sf.targetRef) ?? sf.targetRef;
1331
- return { ...sf, id: newId, sourceRef: newSrc, targetRef: newTgt };
1332
- });
1333
- // Create new DI shapes with offset
1437
+ return { defs, newIds, topLevelIds: [] };
1438
+ const newElements = clipboard.elements.map((el) => remapElement(el, newIds));
1439
+ const newFlows = clipboard.flows.map((sf) => remapFlow(sf, newIds));
1440
+ // Create new DI shapes with offset (covers nested descendants too).
1334
1441
  const newDiShapes = clipboard.shapes.map((s) => {
1335
1442
  const newElId = newIds.get(s.bpmnElement) ?? s.bpmnElement;
1336
1443
  return {
@@ -1344,7 +1451,7 @@ export function pasteElements(defs, clipboard, offsetX, offsetY) {
1344
1451
  },
1345
1452
  };
1346
1453
  });
1347
- // Create new DI edges with offset waypoints
1454
+ // Create new DI edges with offset waypoints (covers nested flows/associations).
1348
1455
  const newDiEdges = clipboard.edges.map((e) => {
1349
1456
  const newFlowId = newIds.get(e.bpmnElement) ?? e.bpmnElement;
1350
1457
  return {
@@ -1379,7 +1486,10 @@ export function pasteElements(defs, clipboard, offsetX, offsetY) {
1379
1486
  ...defs.diagrams.slice(1),
1380
1487
  ],
1381
1488
  };
1382
- return { defs: newDefs, newIds };
1489
+ const topLevelIds = clipboard.elements
1490
+ .map((el) => newIds.get(el.id))
1491
+ .filter((id) => id !== undefined);
1492
+ return { defs: newDefs, newIds, topLevelIds };
1383
1493
  }
1384
1494
  // ── Create text annotation ────────────────────────────────────────────────────
1385
1495
  export function createAnnotation(defs, bounds, text) {
package/dist/overlay.d.ts CHANGED
@@ -42,7 +42,7 @@ export declare class OverlayRenderer {
42
42
  }>): void;
43
43
  setRubberBand(origin: DiagPoint | null, current?: DiagPoint): void;
44
44
  setResizePreview(bounds: BpmnBounds | null): void;
45
- setGhostConnection(waypoints: BpmnWaypoint[] | null): void;
45
+ setGhostConnection(waypoints: BpmnWaypoint[] | null, invalid?: boolean): void;
46
46
  setEdgeEndpoints(waypoints: BpmnWaypoint[] | null, edgeId: string): void;
47
47
  setEndpointDragGhost(waypoints: BpmnWaypoint[] | null): void;
48
48
  setSpacePreview(axis: "h" | "v" | null, splitValue?: number): void;