@bpmnkit/editor 0.1.0 → 0.2.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/dist/editor.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { OverlayManager } from "@bpmnkit/canvas";
2
- import type { Theme } from "@bpmnkit/canvas";
2
+ import type { Theme, ViewportState } from "@bpmnkit/canvas";
3
3
  import type { BpmnDefinitions, DiColor } from "@bpmnkit/core";
4
4
  import type { Translate } from "./i18n.js";
5
5
  import type { CreateShapeType, EditorEvents, EditorOptions, LabelPosition, Tool } from "./types.js";
@@ -94,10 +94,30 @@ export declare class BpmnEditor {
94
94
  }>;
95
95
  undo(): void;
96
96
  redo(): void;
97
+ /**
98
+ * Moves to a document already on the command stack — an undo or a redo.
99
+ *
100
+ * It describes itself as a `snapshot` op, which is the only honest option: a
101
+ * step back through the stack is a state change like any other, and a
102
+ * listener relaying edits elsewhere that never heard about it would be
103
+ * silently wrong from then on. There is no smaller description available —
104
+ * the inverse of an op is not something the stack records.
105
+ */
106
+ private _stepTo;
97
107
  canUndo(): boolean;
98
108
  canRedo(): boolean;
99
109
  getDefinitions(): BpmnDefinitions | null;
100
110
  applyChange(fn: (defs: BpmnDefinitions) => BpmnDefinitions): void;
111
+ /**
112
+ * The raw pan and zoom, for restoring it later.
113
+ *
114
+ * Paired with {@link setViewport} so a viewer can hand its view to an editor
115
+ * taking its place — without it the swap re-fits, and the diagram jumps under
116
+ * the cursor at the moment someone starts editing.
117
+ */
118
+ getViewport(): ViewportState;
119
+ /** Restores a viewport captured by {@link getViewport}. */
120
+ setViewport(state: Partial<ViewportState>): void;
101
121
  fitView(padding?: number): void;
102
122
  /** The host element that receives the `data-theme` attribute. */
103
123
  get container(): HTMLElement;
@@ -124,7 +144,15 @@ export declare class BpmnEditor {
124
144
  private _getDuplicateIds;
125
145
  private _setDuplicateIdWarning;
126
146
  private _renderDefs;
127
- private _executeCommand;
147
+ /**
148
+ * Performs an edit: the editor's single mutation path.
149
+ *
150
+ * Every change goes through {@link applyOp}, including the ones typed right
151
+ * here — see `ops.ts` for why local edits are not allowed their own shortcut.
152
+ * Returns the ids the op brought into being, which a caller may need for a
153
+ * follow-up (starting a label edit on a fresh annotation, say).
154
+ */
155
+ private _executeOp;
128
156
  /** Label of the change `undo()` would revert (for HUD tooltips), or null. */
129
157
  getUndoLabel(): string | null;
130
158
  /** Label of the change `redo()` would re-apply (for HUD tooltips), or null. */
package/dist/editor.js CHANGED
@@ -1,11 +1,13 @@
1
1
  import { KeyboardHandler, OverlayManager, ViewportController, computeDiagramBounds, createDefs, createGrid, injectStyles, render, } from "@bpmnkit/canvas";
2
- import { Bpmn, applyAutoLayout } from "@bpmnkit/core";
2
+ import { Bpmn } 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
6
  import { defaultTranslate } from "./i18n.js";
7
+ import { newIdSeed } from "./id.js";
7
8
  import { LabelEditor } from "./label-editor.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";
9
+ import { copyElements, createEmptyDefinitions, insertEdgeWaypoint, moveEdgeSegment, moveEdgeWaypoint, } from "./modeling.js";
10
+ import { applyOp } from "./ops.js";
9
11
  import { OverlayRenderer } from "./overlay.js";
10
12
  import { canAttach, canConnect, canMorph, canResize } from "./rules.js";
11
13
  import { EditorStateMachine } from "./state-machine.js";
@@ -376,7 +378,7 @@ export class BpmnEditor {
376
378
  previewResize: (bounds) => this._overlay.setResizePreview(bounds),
377
379
  commitResize: (id, bounds) => {
378
380
  this._overlay.setResizePreview(null);
379
- this._executeCommand((d) => resizeShape(d, id, bounds), "Resize");
381
+ this._executeOp({ kind: "resize", id, bounds }, { label: "Resize" });
380
382
  },
381
383
  previewConnect: (ghostEnd, targetId) => {
382
384
  const src = this._connectSourceBounds();
@@ -401,7 +403,7 @@ export class BpmnEditor {
401
403
  startLabelEdit: (id) => this._startLabelEdit(id),
402
404
  setHovered: (id) => this._overlay.setHovered(id, this._shapes),
403
405
  executeDelete: (ids) => {
404
- this._executeCommand((d) => deleteElements(d, ids), "Delete");
406
+ this._executeOp({ kind: "delete", ids }, { label: "Delete" });
405
407
  this._setSelection([]);
406
408
  },
407
409
  executeCopy: () => this._doCopy(),
@@ -431,7 +433,7 @@ export class BpmnEditor {
431
433
  this._overlay.setEndpointDragGhost(null);
432
434
  this._overlay.setAlignmentGuides([]);
433
435
  const snap = this._snapWaypoint(pt);
434
- this._executeCommand((d) => removeCollinearWaypoints(insertEdgeWaypoint(d, edgeId, segIdx, snap.pt), edgeId), "Add waypoint");
436
+ this._executeOp({ kind: "insertWaypoint", edgeId, segIdx, point: snap.pt }, { label: "Add waypoint" });
435
437
  },
436
438
  cancelWaypointInsert: () => {
437
439
  this._overlay.setEndpointDragGhost(null);
@@ -450,7 +452,7 @@ export class BpmnEditor {
450
452
  this._overlay.setEndpointDragGhost(null);
451
453
  this._overlay.setAlignmentGuides([]);
452
454
  const snap = this._snapWaypoint(pt);
453
- this._executeCommand((d) => removeCollinearWaypoints(moveEdgeWaypoint(d, edgeId, wpIdx, snap.pt), edgeId), "Move waypoint");
455
+ this._executeOp({ kind: "moveWaypoint", edgeId, wpIdx, point: snap.pt }, { label: "Move waypoint" });
454
456
  },
455
457
  cancelWaypointMove: () => {
456
458
  this._overlay.setEndpointDragGhost(null);
@@ -465,7 +467,7 @@ export class BpmnEditor {
465
467
  },
466
468
  commitSegmentMove: (edgeId, segIdx, isHoriz, delta) => {
467
469
  this._overlay.setEndpointDragGhost(null);
468
- this._executeCommand((d) => removeCollinearWaypoints(moveEdgeSegment(d, edgeId, segIdx, isHoriz, delta), edgeId), "Move segment");
470
+ this._executeOp({ kind: "moveSegment", edgeId, segIdx, isHoriz, delta }, { label: "Move segment" });
469
471
  },
470
472
  cancelSegmentMove: () => {
471
473
  this._overlay.setEndpointDragGhost(null);
@@ -491,7 +493,7 @@ export class BpmnEditor {
491
493
  this._stateMachine = new EditorStateMachine(callbacks);
492
494
  // ── Label editor ─────────────────────────────────────────────
493
495
  this._labelEditor = new LabelEditor(this._host, (id, text) => {
494
- this._executeCommand((d) => updateLabel(d, id, text), "Rename", `label:${id}`);
496
+ this._executeOp({ kind: "rename", id, name: text }, { label: "Rename", coalesceKey: `label:${id}` });
495
497
  this._stateMachine.setMode({ mode: "select", sub: { name: "idle", hoveredId: null } });
496
498
  }, () => {
497
499
  this._stateMachine.setMode({ mode: "select", sub: { name: "idle", hoveredId: null } });
@@ -552,7 +554,7 @@ export class BpmnEditor {
552
554
  * The operation is undoable.
553
555
  */
554
556
  autoLayout() {
555
- this._executeCommand(applyAutoLayout, "Auto-layout");
557
+ this._executeOp({ kind: "autoLayout" }, { label: "Auto-layout" });
556
558
  this.fitView();
557
559
  }
558
560
  loadDefinitions(defs) {
@@ -625,7 +627,7 @@ export class BpmnEditor {
625
627
  if (this._selectedIds.length === 0)
626
628
  return;
627
629
  const ids = [...this._selectedIds];
628
- this._executeCommand((d) => deleteElements(d, ids), "Delete");
630
+ this._executeOp({ kind: "delete", ids }, { label: "Delete" });
629
631
  this._setSelection([]);
630
632
  }
631
633
  /** Bounds of the currently-selected shapes (edges have none, and are skipped). */
@@ -678,7 +680,7 @@ export class BpmnEditor {
678
680
  moves.push({ id: b.id, dx, dy });
679
681
  }
680
682
  if (moves.length > 0)
681
- this._executeCommand((d) => moveShapes(d, moves), "Align");
683
+ this._executeOp({ kind: "move", moves }, { label: "Align" });
682
684
  }
683
685
  /**
684
686
  * Distributes the selected shapes so the gaps between them are equal along
@@ -713,7 +715,7 @@ export class BpmnEditor {
713
715
  cursor += size(b) + gap;
714
716
  }
715
717
  if (moves.length > 0)
716
- this._executeCommand((d) => moveShapes(d, moves), "Distribute");
718
+ this._executeOp({ kind: "move", moves }, { label: "Distribute" });
717
719
  }
718
720
  /**
719
721
  * Searches the model (recursively, including sub-process children) for
@@ -755,17 +757,27 @@ export class BpmnEditor {
755
757
  }
756
758
  undo() {
757
759
  const prev = this._commandStack.undo();
758
- if (prev) {
759
- this._renderDefs(prev);
760
- this._emit("diagram:change", prev);
761
- }
760
+ if (prev)
761
+ this._stepTo(prev);
762
762
  }
763
763
  redo() {
764
764
  const next = this._commandStack.redo();
765
- if (next) {
766
- this._renderDefs(next);
767
- this._emit("diagram:change", next);
768
- }
765
+ if (next)
766
+ this._stepTo(next);
767
+ }
768
+ /**
769
+ * Moves to a document already on the command stack — an undo or a redo.
770
+ *
771
+ * It describes itself as a `snapshot` op, which is the only honest option: a
772
+ * step back through the stack is a state change like any other, and a
773
+ * listener relaying edits elsewhere that never heard about it would be
774
+ * silently wrong from then on. There is no smaller description available —
775
+ * the inverse of an op is not something the stack records.
776
+ */
777
+ _stepTo(defs) {
778
+ this._renderDefs(defs);
779
+ this._emit("diagram:change", defs);
780
+ this._emit("diagram:op", { kind: "snapshot", defs }, defs);
769
781
  }
770
782
  canUndo() {
771
783
  return this._commandStack.canUndo();
@@ -777,7 +789,23 @@ export class BpmnEditor {
777
789
  return this._defs;
778
790
  }
779
791
  applyChange(fn) {
780
- this._executeCommand(fn);
792
+ if (this._readOnly || !this._defs)
793
+ return;
794
+ this._executeOp({ kind: "snapshot", defs: fn(this._defs) });
795
+ }
796
+ /**
797
+ * The raw pan and zoom, for restoring it later.
798
+ *
799
+ * Paired with {@link setViewport} so a viewer can hand its view to an editor
800
+ * taking its place — without it the swap re-fits, and the diagram jumps under
801
+ * the cursor at the moment someone starts editing.
802
+ */
803
+ getViewport() {
804
+ return this._viewport.state;
805
+ }
806
+ /** Restores a viewport captured by {@link getViewport}. */
807
+ setViewport(state) {
808
+ this._viewport.set(state);
781
809
  }
782
810
  fitView(padding = 40) {
783
811
  if (!this._defs)
@@ -988,15 +1016,31 @@ export class BpmnEditor {
988
1016
  }
989
1017
  this._emit("diagram:load", defs, { missingShapes: [], missingEdges: [] });
990
1018
  }
991
- _executeCommand(fn, label = "", coalesceKey) {
1019
+ /**
1020
+ * Performs an edit: the editor's single mutation path.
1021
+ *
1022
+ * Every change goes through {@link applyOp}, including the ones typed right
1023
+ * here — see `ops.ts` for why local edits are not allowed their own shortcut.
1024
+ * Returns the ids the op brought into being, which a caller may need for a
1025
+ * follow-up (starting a label edit on a fresh annotation, say).
1026
+ */
1027
+ _executeOp(op, options = {}) {
992
1028
  if (this._readOnly || !this._defs)
993
- return;
994
- const newDefs = fn(this._defs);
995
- this._commandStack.push(newDefs, label, coalesceKey);
996
- this._renderDefs(newDefs);
997
- if (label)
998
- this._announce(this._t(label));
999
- this._emit("diagram:change", newDefs);
1029
+ return [];
1030
+ const { defs, created } = applyOp(this._defs, op);
1031
+ // Selection is set before the render so the new element comes up selected.
1032
+ const select = options.select === "all" ? created : options.select === "first" ? created.slice(0, 1) : null;
1033
+ if (select)
1034
+ this._selectedIds = select;
1035
+ this._commandStack.push(defs, options.label ?? "", options.coalesceKey);
1036
+ this._renderDefs(defs);
1037
+ if (options.label)
1038
+ this._announce(this._t(options.label));
1039
+ this._emit("diagram:change", defs);
1040
+ this._emit("diagram:op", op, defs);
1041
+ if (select)
1042
+ this._emit("editor:select", select);
1043
+ return created;
1000
1044
  }
1001
1045
  /** Label of the change `undo()` would revert (for HUD tooltips), or null. */
1002
1046
  getUndoLabel() {
@@ -1103,10 +1147,10 @@ export class BpmnEditor {
1103
1147
  const moves = this._selectedIds.map((id) => ({ id, dx: snap.dx, dy: snap.dy }));
1104
1148
  const shapeId = this._selectedIds.length === 1 ? this._selectedIds[0] : undefined;
1105
1149
  if (edgeDropId && shapeId) {
1106
- this._executeCommand((d) => insertShapeOnEdge(moveShapes(d, moves), edgeDropId, shapeId), "Insert on flow");
1150
+ this._executeOp({ kind: "move", moves, onEdge: { edgeId: edgeDropId, shapeId } }, { label: "Insert on flow" });
1107
1151
  }
1108
1152
  else {
1109
- this._executeCommand((d) => moveShapes(d, moves), "Move");
1153
+ this._executeOp({ kind: "move", moves }, { label: "Move" });
1110
1154
  }
1111
1155
  if (this._isDragging) {
1112
1156
  this._isDragging = false;
@@ -1181,7 +1225,7 @@ export class BpmnEditor {
1181
1225
  }
1182
1226
  }
1183
1227
  if (moves.length > 0) {
1184
- this._executeCommand((d) => moveShapes(d, moves), "Move");
1228
+ this._executeOp({ kind: "move", moves }, { label: "Move" });
1185
1229
  }
1186
1230
  }
1187
1231
  _cancelSpace() {
@@ -1203,13 +1247,9 @@ export class BpmnEditor {
1203
1247
  return;
1204
1248
  const bounds = defaultBounds(type, actualCenter.x, actualCenter.y);
1205
1249
  if (type === "textAnnotation") {
1206
- const result = createAnnotation(this._defs, bounds);
1207
- this._selectedIds = [result.id];
1208
- this._commandStack.push(result.defs);
1209
- this._renderDefs(result.defs);
1210
- this._emit("diagram:change", result.defs);
1211
- this._emit("editor:select", [result.id]);
1212
- this._startLabelEdit(result.id);
1250
+ const [id] = this._executeOp({ kind: "createAnnotation", bounds, seed: newIdSeed() }, { select: "first" });
1251
+ if (id)
1252
+ this._startLabelEdit(id);
1213
1253
  return;
1214
1254
  }
1215
1255
  // If hovering over an activity, create a boundary event
@@ -1223,24 +1263,23 @@ export class BpmnEditor {
1223
1263
  const eventBounds = snapToBoundary(actualCenter, hostBounds, 18);
1224
1264
  // Map palette type to event definition type
1225
1265
  const eventDefType = intermediateEventDefType(type);
1226
- const result = createBoundaryEvent(this._defs, boundaryHostId, eventDefType, eventBounds);
1227
- this._selectedIds = [result.id];
1228
- this._commandStack.push(result.defs);
1229
- this._renderDefs(result.defs);
1230
- this._emit("diagram:change", result.defs);
1231
- this._emit("editor:select", [result.id]);
1266
+ this._executeOp({
1267
+ kind: "createBoundaryEvent",
1268
+ hostId: boundaryHostId,
1269
+ eventDefType,
1270
+ bounds: eventBounds,
1271
+ seed: newIdSeed(),
1272
+ }, { select: "first" });
1232
1273
  return;
1233
1274
  }
1234
1275
  }
1235
- const result = createShape(this._defs, type, bounds);
1236
- this._selectedIds = [result.id];
1237
- const finalDefs = pendingEdgeDrop
1238
- ? insertShapeOnEdge(result.defs, pendingEdgeDrop, result.id)
1239
- : result.defs;
1240
- this._commandStack.push(finalDefs);
1241
- this._renderDefs(finalDefs);
1242
- this._emit("diagram:change", finalDefs);
1243
- this._emit("editor:select", [result.id]);
1276
+ this._executeOp({
1277
+ kind: "createShape",
1278
+ type,
1279
+ bounds,
1280
+ onEdge: pendingEdgeDrop ?? undefined,
1281
+ seed: newIdSeed(),
1282
+ }, { select: "first" });
1244
1283
  }
1245
1284
  _doConnect(srcId, tgtId) {
1246
1285
  const srcShape = this._shapeById.get(srcId);
@@ -1255,7 +1294,7 @@ export class BpmnEditor {
1255
1294
  .filter((s) => s.id !== srcId && s.id !== tgtId)
1256
1295
  .map((s) => s.shape.bounds);
1257
1296
  const waypoints = computeWaypointsAvoiding(srcShape.shape.bounds, tgtShape.shape.bounds, obstacles);
1258
- this._executeCommand((d) => createConnection(d, srcId, tgtId, waypoints).defs, "Connect");
1297
+ this._executeOp({ kind: "createConnection", sourceId: srcId, targetId: tgtId, waypoints, seed: newIdSeed() }, { label: "Connect" });
1259
1298
  }
1260
1299
  _doCopy() {
1261
1300
  if (!this._defs || this._selectedIds.length === 0)
@@ -1265,20 +1304,14 @@ export class BpmnEditor {
1265
1304
  _doPaste() {
1266
1305
  if (!this._clipboard)
1267
1306
  return;
1268
- const base = this._defs ?? createEmptyDefinitions();
1269
- const result = pasteElements(base, this._clipboard, 20, 20);
1270
- this._selectedIds = result.topLevelIds;
1271
- this._commandStack.push(result.defs, "Paste");
1272
- this._renderDefs(result.defs);
1273
- this._emit("diagram:change", result.defs);
1274
- this._emit("editor:select", result.topLevelIds);
1307
+ this._executeOp({ kind: "paste", clipboard: this._clipboard, offsetX: 20, offsetY: 20, seed: newIdSeed() }, { label: "Paste", select: "all" });
1275
1308
  }
1276
1309
  _doCut() {
1277
1310
  if (!this._defs || this._selectedIds.length === 0)
1278
1311
  return;
1279
1312
  this._doCopy();
1280
1313
  const ids = [...this._selectedIds];
1281
- this._executeCommand((d) => deleteElements(d, ids), "Cut");
1314
+ this._executeOp({ kind: "delete", ids }, { label: "Cut" });
1282
1315
  this._setSelection([]);
1283
1316
  }
1284
1317
  _startLabelEdit(id) {
@@ -1390,15 +1423,17 @@ export class BpmnEditor {
1390
1423
  }
1391
1424
  const newBounds = this._smartPlaceBounds(srcBounds, sourceId, w, h);
1392
1425
  const obstacles = this._shapes.filter((s) => s.id !== sourceId).map((s) => s.shape.bounds);
1393
- const r1 = createShape(this._defs, type, newBounds, name);
1394
1426
  const waypoints = computeWaypointsAvoiding(srcBounds, newBounds, obstacles);
1395
- const r2 = createConnection(r1.defs, sourceId, r1.id, waypoints);
1396
- this._selectedIds = [r1.id];
1397
- this._commandStack.push(r2.defs);
1398
- this._renderDefs(r2.defs);
1399
- this._emit("diagram:change", r2.defs);
1400
- this._emit("editor:select", [r1.id]);
1401
- return r1.id;
1427
+ const [id] = this._executeOp({
1428
+ kind: "createConnected",
1429
+ sourceId,
1430
+ type,
1431
+ name,
1432
+ bounds: newBounds,
1433
+ waypoints,
1434
+ seed: newIdSeed(),
1435
+ }, { select: "first" });
1436
+ return id ?? null;
1402
1437
  }
1403
1438
  _smartPlaceBounds(srcBounds, sourceId, w, h) {
1404
1439
  const GAP = 60;
@@ -1495,7 +1530,7 @@ export class BpmnEditor {
1495
1530
  if (!shape)
1496
1531
  return;
1497
1532
  const labelBounds = labelBoundsForPosition(shape.shape.bounds, position);
1498
- this._executeCommand((d) => updateLabelPosition(d, shapeId, labelBounds), "Move label");
1533
+ this._executeOp({ kind: "labelPosition", id: shapeId, bounds: labelBounds }, { label: "Move label" });
1499
1534
  }
1500
1535
  /** Starts inline label editing for the element with the given id. */
1501
1536
  editLabel(id) {
@@ -1537,17 +1572,19 @@ export class BpmnEditor {
1537
1572
  width: annW,
1538
1573
  height: annH,
1539
1574
  };
1540
- const result = createAnnotationWithLink(this._defs, annBounds, sourceId, srcBounds);
1541
- this._selectedIds = [result.annotationId];
1542
- this._commandStack.push(result.defs);
1543
- this._renderDefs(result.defs);
1544
- this._emit("diagram:change", result.defs);
1545
- this._emit("editor:select", [result.annotationId]);
1546
- this._startLabelEdit(result.annotationId);
1575
+ const [id] = this._executeOp({
1576
+ kind: "createAnnotationFor",
1577
+ sourceId,
1578
+ bounds: annBounds,
1579
+ sourceBounds: srcBounds,
1580
+ seed: newIdSeed(),
1581
+ }, { select: "first" });
1582
+ if (id)
1583
+ this._startLabelEdit(id);
1547
1584
  }
1548
1585
  /** Updates the color of a shape in the diagram. Pass `{}` to clear colors. */
1549
1586
  updateColor(id, color) {
1550
- this._executeCommand((d) => updateShapeColor(d, id, color), "Change colour", `color:${id}`);
1587
+ this._executeOp({ kind: "color", id, color }, { label: "Change colour", coalesceKey: `color:${id}` });
1551
1588
  }
1552
1589
  // ── Private helpers ────────────────────────────────────────────────
1553
1590
  _setEdgeSelected(edgeId) {
@@ -1572,7 +1609,7 @@ export class BpmnEditor {
1572
1609
  const current = this._shapeById.get(id)?.flowElement?.type;
1573
1610
  if (current && !canMorph(current, newType))
1574
1611
  return;
1575
- this._executeCommand((d) => changeElementTypeFn(d, id, newType), "Change type");
1612
+ this._executeOp({ kind: "changeType", id, type: newType }, { label: "Change type" });
1576
1613
  }
1577
1614
  _findEdgeDropTarget(dx, dy) {
1578
1615
  if (this._selectedIds.length !== 1)
@@ -1677,7 +1714,7 @@ export class BpmnEditor {
1677
1714
  const newPort = isStart
1678
1715
  ? closestPort(diagPoint, srcDi.bounds)
1679
1716
  : closestPort(diagPoint, tgtDi.bounds);
1680
- this._executeCommand((d) => updateEdgeEndpoint(d, edgeId, isStart, newPort), "Reconnect");
1717
+ this._executeOp({ kind: "reconnect", edgeId, isStart, port: newPort }, { label: "Reconnect" });
1681
1718
  }
1682
1719
  _isResizable(id) {
1683
1720
  const shape = this._shapeById.get(id);
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The editor's DOM-free surface: operations, and the ids they mint.
3
+ *
4
+ * `@bpmnkit/editor` itself reaches for `document` the moment it is imported —
5
+ * it is an editor. But the part that decides *what an edit does* never touches
6
+ * the DOM, and that part has to run in two more places: a Durable Object
7
+ * replaying a writer's op to verify it, and a watcher applying the same op
8
+ * without an editor loaded at all.
9
+ *
10
+ * So this entry exists to be importable from a Worker. Everything reachable
11
+ * from here is pure, and the subpath keeps it that way — importing the package
12
+ * root instead would pull the canvas in behind it.
13
+ */
14
+ export { applyOp } from "./ops.js";
15
+ export type { EditorOp, OpResult, ShapeMove } from "./ops.js";
16
+ export { createIdFactory, genId, newIdSeed } from "./id.js";
17
+ export type { IdFactory } from "./id.js";
18
+ export { createEmptyDefinitions } from "./modeling.js";
19
+ export type { Clipboard } from "./modeling.js";
20
+ export type { CreateShapeType, PortDir } from "./types.js";
21
+ //# sourceMappingURL=headless.d.ts.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The editor's DOM-free surface: operations, and the ids they mint.
3
+ *
4
+ * `@bpmnkit/editor` itself reaches for `document` the moment it is imported —
5
+ * it is an editor. But the part that decides *what an edit does* never touches
6
+ * the DOM, and that part has to run in two more places: a Durable Object
7
+ * replaying a writer's op to verify it, and a watcher applying the same op
8
+ * without an editor loaded at all.
9
+ *
10
+ * So this entry exists to be importable from a Worker. Everything reachable
11
+ * from here is pure, and the subpath keeps it that way — importing the package
12
+ * root instead would pull the canvas in behind it.
13
+ */
14
+ export { applyOp } from "./ops.js";
15
+ export { createIdFactory, genId, newIdSeed } from "./id.js";
16
+ export { createEmptyDefinitions } from "./modeling.js";
17
+ //# sourceMappingURL=headless.js.map
package/dist/id.d.ts CHANGED
@@ -1,2 +1,27 @@
1
+ /**
2
+ * How new element ids are minted.
3
+ *
4
+ * Normally this is `Math.random()`, which is exactly right for one person
5
+ * editing one diagram. It is wrong the moment the same edit has to happen twice
6
+ * — replayed on a server, or on someone else's screen — because two runs would
7
+ * invent two different ids for the same new task.
8
+ *
9
+ * So the minting functions in `modeling.ts` take an optional {@link IdFactory}.
10
+ * An operation carries a seed, both sides build the same factory from it, and
11
+ * the same edit produces byte-identical XML wherever it runs.
12
+ */
13
+ /** Mints an id for a new element, given the prefix its type wants. */
14
+ export type IdFactory = (prefix: string) => string;
15
+ /** The default: unique enough for one editor, different on every call. */
1
16
  export declare function genId(prefix: string): string;
17
+ /**
18
+ * A factory that yields the same sequence of ids for the same seed.
19
+ *
20
+ * Deterministic *in call order*, which is the contract an operation replay
21
+ * relies on: the same function, given the same arguments, asks for the same
22
+ * prefixes in the same order, so it gets the same ids back.
23
+ */
24
+ export declare function createIdFactory(seed: string): IdFactory;
25
+ /** A seed with enough entropy that two operations cannot mint the same ids. */
26
+ export declare function newIdSeed(): string;
2
27
  //# sourceMappingURL=id.d.ts.map
package/dist/id.js CHANGED
@@ -1,4 +1,43 @@
1
+ /**
2
+ * How new element ids are minted.
3
+ *
4
+ * Normally this is `Math.random()`, which is exactly right for one person
5
+ * editing one diagram. It is wrong the moment the same edit has to happen twice
6
+ * — replayed on a server, or on someone else's screen — because two runs would
7
+ * invent two different ids for the same new task.
8
+ *
9
+ * So the minting functions in `modeling.ts` take an optional {@link IdFactory}.
10
+ * An operation carries a seed, both sides build the same factory from it, and
11
+ * the same edit produces byte-identical XML wherever it runs.
12
+ */
13
+ /** The default: unique enough for one editor, different on every call. */
1
14
  export function genId(prefix) {
2
15
  return `${prefix}_${Math.random().toString(36).slice(2, 9)}`;
3
16
  }
17
+ /**
18
+ * A factory that yields the same sequence of ids for the same seed.
19
+ *
20
+ * Deterministic *in call order*, which is the contract an operation replay
21
+ * relies on: the same function, given the same arguments, asks for the same
22
+ * prefixes in the same order, so it gets the same ids back.
23
+ */
24
+ export function createIdFactory(seed) {
25
+ // FNV-1a over the seed, then xorshift32 per id. Small, dependency-free, and
26
+ // the output is shaped like `genId`'s so nothing downstream can tell them apart.
27
+ let state = 2166136261;
28
+ for (let i = 0; i < seed.length; i++) {
29
+ state ^= seed.charCodeAt(i);
30
+ state = Math.imul(state, 16777619);
31
+ }
32
+ return (prefix) => {
33
+ state ^= state << 13;
34
+ state ^= state >>> 17;
35
+ state ^= state << 5;
36
+ return `${prefix}_${(state >>> 0).toString(36).padStart(7, "0").slice(0, 7)}`;
37
+ };
38
+ }
39
+ /** A seed with enough entropy that two operations cannot mint the same ids. */
40
+ export function newIdSeed() {
41
+ return Math.random().toString(36).slice(2, 12);
42
+ }
4
43
  //# sourceMappingURL=id.js.map
package/dist/index.d.ts CHANGED
@@ -2,6 +2,10 @@ export { BpmnEditor } from "./editor.js";
2
2
  export { CHROME_CSS, CHROME_STYLE_ID, injectChromeStyles } from "./chrome.js";
3
3
  export { injectStyle } from "./inject.js";
4
4
  export { createEmptyDefinitions } from "./modeling.js";
5
+ export { applyOp } from "./ops.js";
6
+ export type { EditorOp, OpResult, ShapeMove } from "./ops.js";
7
+ export { createIdFactory, genId, newIdSeed } from "./id.js";
8
+ export type { IdFactory } from "./id.js";
5
9
  export type { EditorEvents, EditorOptions, LabelPosition, Tool, CreateShapeType, HandleDir, PortDir, } from "./types.js";
6
10
  export { ELEMENT_GROUPS, ELEMENT_TYPE_LABELS, EXTERNAL_LABEL_TYPES, CONTEXTUAL_ADD_TYPES, getElementGroup, getValidLabelPositions, } from "./element-groups.js";
7
11
  export type { ElementGroup } from "./element-groups.js";
package/dist/index.js CHANGED
@@ -2,6 +2,8 @@ export { BpmnEditor } from "./editor.js";
2
2
  export { CHROME_CSS, CHROME_STYLE_ID, injectChromeStyles } from "./chrome.js";
3
3
  export { injectStyle } from "./inject.js";
4
4
  export { createEmptyDefinitions } from "./modeling.js";
5
+ export { applyOp } from "./ops.js";
6
+ export { createIdFactory, genId, newIdSeed } from "./id.js";
5
7
  export { ELEMENT_GROUPS, ELEMENT_TYPE_LABELS, EXTERNAL_LABEL_TYPES, CONTEXTUAL_ADD_TYPES, getElementGroup, getValidLabelPositions, } from "./element-groups.js";
6
8
  export { initEditorHud } from "./hud.js";
7
9
  export { createTranslationRecorder, defaultTranslate, interpolate } from "./i18n.js";
@@ -1,16 +1,17 @@
1
1
  import type { BpmnBounds, BpmnDefinitions, BpmnDiEdge, BpmnDiShape, BpmnFlowElement, BpmnSequenceFlow, BpmnWaypoint, DiColor } from "@bpmnkit/core";
2
+ import { type IdFactory } from "./id.js";
2
3
  import type { CreateShapeType, PortDir } from "./types.js";
3
4
  /** Creates a minimal valid BpmnDefinitions with one process and one diagram. */
4
5
  export declare function createEmptyDefinitions(): BpmnDefinitions;
5
- export declare function createShape(defs: BpmnDefinitions, type: CreateShapeType, bounds: BpmnBounds, name?: string): {
6
+ export declare function createShape(defs: BpmnDefinitions, type: CreateShapeType, bounds: BpmnBounds, name?: string, ids?: IdFactory): {
6
7
  defs: BpmnDefinitions;
7
8
  id: string;
8
9
  };
9
- export declare function createBoundaryEvent(defs: BpmnDefinitions, hostId: string, eventDefType: string | null, bounds: BpmnBounds, cancelActivity?: boolean): {
10
+ export declare function createBoundaryEvent(defs: BpmnDefinitions, hostId: string, eventDefType: string | null, bounds: BpmnBounds, cancelActivity?: boolean, ids?: IdFactory): {
10
11
  defs: BpmnDefinitions;
11
12
  id: string;
12
13
  };
13
- export declare function createConnection(defs: BpmnDefinitions, sourceId: string, targetId: string, waypoints: BpmnWaypoint[]): {
14
+ export declare function createConnection(defs: BpmnDefinitions, sourceId: string, targetId: string, waypoints: BpmnWaypoint[], ids?: IdFactory): {
14
15
  defs: BpmnDefinitions;
15
16
  id: string;
16
17
  };
@@ -74,16 +75,16 @@ export interface Clipboard {
74
75
  edges: BpmnDiEdge[];
75
76
  }
76
77
  export declare function copyElements(defs: BpmnDefinitions, ids: string[]): Clipboard;
77
- export declare function pasteElements(defs: BpmnDefinitions, clipboard: Clipboard, offsetX: number, offsetY: number): {
78
+ export declare function pasteElements(defs: BpmnDefinitions, clipboard: Clipboard, offsetX: number, offsetY: number, ids?: IdFactory): {
78
79
  defs: BpmnDefinitions;
79
80
  newIds: Map<string, string>;
80
81
  topLevelIds: string[];
81
82
  };
82
- export declare function createAnnotation(defs: BpmnDefinitions, bounds: BpmnBounds, text?: string): {
83
+ export declare function createAnnotation(defs: BpmnDefinitions, bounds: BpmnBounds, text?: string, ids?: IdFactory): {
83
84
  defs: BpmnDefinitions;
84
85
  id: string;
85
86
  };
86
- export declare function createAnnotationWithLink(defs: BpmnDefinitions, bounds: BpmnBounds, sourceId: string, sourceBounds: BpmnBounds, text?: string): {
87
+ export declare function createAnnotationWithLink(defs: BpmnDefinitions, bounds: BpmnBounds, sourceId: string, sourceBounds: BpmnBounds, text?: string, ids?: IdFactory): {
87
88
  defs: BpmnDefinitions;
88
89
  annotationId: string;
89
90
  associationId: string;
package/dist/modeling.js CHANGED
@@ -338,9 +338,9 @@ function updateRefInElements(flowElements, id, updateFn) {
338
338
  });
339
339
  }
340
340
  // ── Create shape ─────────────────────────────────────────────────────────────
341
- export function createShape(defs, type, bounds, name) {
342
- const id = genId(type);
343
- const shapeId = genId(`${type}_di`);
341
+ export function createShape(defs, type, bounds, name, ids = genId) {
342
+ const id = ids(type);
343
+ const shapeId = ids(`${type}_di`);
344
344
  const flowElement = makeFlowElement(type, id, name);
345
345
  const process = defs.processes[0];
346
346
  if (!process)
@@ -382,9 +382,9 @@ export function createShape(defs, type, bounds, name) {
382
382
  return { defs: newDefs, id };
383
383
  }
384
384
  // ── Create boundary event ─────────────────────────────────────────────────────
385
- export function createBoundaryEvent(defs, hostId, eventDefType, bounds, cancelActivity = true) {
386
- const id = genId("BoundaryEvent");
387
- const shapeId = genId("BoundaryEvent_di");
385
+ export function createBoundaryEvent(defs, hostId, eventDefType, bounds, cancelActivity = true, ids = genId) {
386
+ const id = ids("BoundaryEvent");
387
+ const shapeId = ids("BoundaryEvent_di");
388
388
  const process = defs.processes[0];
389
389
  if (!process)
390
390
  return { defs, id };
@@ -434,9 +434,9 @@ export function createBoundaryEvent(defs, hostId, eventDefType, bounds, cancelAc
434
434
  };
435
435
  }
436
436
  // ── Create connection ─────────────────────────────────────────────────────────
437
- export function createConnection(defs, sourceId, targetId, waypoints) {
438
- const id = genId("Flow");
439
- const edgeId = genId("Flow_di");
437
+ export function createConnection(defs, sourceId, targetId, waypoints, ids = genId) {
438
+ const id = ids("Flow");
439
+ const edgeId = ids("Flow_di");
440
440
  const process = defs.processes[0];
441
441
  if (!process)
442
442
  return { defs, id };
@@ -1373,19 +1373,19 @@ export function copyElements(defs, ids) {
1373
1373
  return { elements, flows, shapes, edges };
1374
1374
  }
1375
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));
1376
+ function buildIdMap(el, map, ids) {
1377
+ map.set(el.id, ids(el.type));
1378
1378
  if (hasChildren(el)) {
1379
1379
  for (const child of el.flowElements)
1380
- buildIdMap(child, map);
1380
+ buildIdMap(child, map, ids);
1381
1381
  for (const sf of el.sequenceFlows)
1382
- map.set(sf.id, genId("Flow"));
1382
+ map.set(sf.id, ids("Flow"));
1383
1383
  for (const ta of el.textAnnotations)
1384
- map.set(ta.id, genId("TextAnnotation"));
1384
+ map.set(ta.id, ids("TextAnnotation"));
1385
1385
  for (const a of el.associations)
1386
- map.set(a.id, genId("Association"));
1386
+ map.set(a.id, ids("Association"));
1387
1387
  for (const g of el.groups)
1388
- map.set(g.id, genId("Group"));
1388
+ map.set(g.id, ids("Group"));
1389
1389
  }
1390
1390
  }
1391
1391
  /** Deep-clones `el`, remapping its own id, refs, and all nested children through `map`. */
@@ -1424,13 +1424,13 @@ function remapFlow(sf, map) {
1424
1424
  targetRef: map.get(sf.targetRef) ?? sf.targetRef,
1425
1425
  };
1426
1426
  }
1427
- export function pasteElements(defs, clipboard, offsetX, offsetY) {
1427
+ export function pasteElements(defs, clipboard, offsetX, offsetY, ids = genId) {
1428
1428
  const newIds = new Map();
1429
1429
  // Generate new IDs for every element (recursively) and top-level flow.
1430
1430
  for (const el of clipboard.elements)
1431
- buildIdMap(el, newIds);
1431
+ buildIdMap(el, newIds, ids);
1432
1432
  for (const sf of clipboard.flows)
1433
- newIds.set(sf.id, genId("Flow"));
1433
+ newIds.set(sf.id, ids("Flow"));
1434
1434
  const process = defs.processes[0];
1435
1435
  const diagram = defs.diagrams[0];
1436
1436
  if (!process || !diagram)
@@ -1442,7 +1442,7 @@ export function pasteElements(defs, clipboard, offsetX, offsetY) {
1442
1442
  const newElId = newIds.get(s.bpmnElement) ?? s.bpmnElement;
1443
1443
  return {
1444
1444
  ...s,
1445
- id: genId(`${newElId}_di`),
1445
+ id: ids(`${newElId}_di`),
1446
1446
  bpmnElement: newElId,
1447
1447
  bounds: {
1448
1448
  ...s.bounds,
@@ -1456,7 +1456,7 @@ export function pasteElements(defs, clipboard, offsetX, offsetY) {
1456
1456
  const newFlowId = newIds.get(e.bpmnElement) ?? e.bpmnElement;
1457
1457
  return {
1458
1458
  ...e,
1459
- id: genId(`${newFlowId}_di`),
1459
+ id: ids(`${newFlowId}_di`),
1460
1460
  bpmnElement: newFlowId,
1461
1461
  waypoints: e.waypoints.map((wp) => ({
1462
1462
  x: wp.x + offsetX,
@@ -1492,9 +1492,9 @@ export function pasteElements(defs, clipboard, offsetX, offsetY) {
1492
1492
  return { defs: newDefs, newIds, topLevelIds };
1493
1493
  }
1494
1494
  // ── Create text annotation ────────────────────────────────────────────────────
1495
- export function createAnnotation(defs, bounds, text) {
1496
- const id = genId("TextAnnotation");
1497
- const shapeId = genId("TextAnnotation_di");
1495
+ export function createAnnotation(defs, bounds, text, ids = genId) {
1496
+ const id = ids("TextAnnotation");
1497
+ const shapeId = ids("TextAnnotation_di");
1498
1498
  const annotation = { id, text, unknownAttributes: {} };
1499
1499
  const diShape = { id: shapeId, bpmnElement: id, bounds, unknownAttributes: {} };
1500
1500
  const process = defs.processes[0];
@@ -1521,11 +1521,11 @@ export function createAnnotation(defs, bounds, text) {
1521
1521
  id,
1522
1522
  };
1523
1523
  }
1524
- export function createAnnotationWithLink(defs, bounds, sourceId, sourceBounds, text) {
1525
- const annotResult = createAnnotation(defs, bounds, text);
1524
+ export function createAnnotationWithLink(defs, bounds, sourceId, sourceBounds, text, ids = genId) {
1525
+ const annotResult = createAnnotation(defs, bounds, text, ids);
1526
1526
  const annotationId = annotResult.id;
1527
- const assocId = genId("Association");
1528
- const edgeId = genId("Association_di");
1527
+ const assocId = ids("Association");
1528
+ const edgeId = ids("Association_di");
1529
1529
  const assoc = {
1530
1530
  id: assocId,
1531
1531
  sourceRef: sourceId,
package/dist/ops.d.ts ADDED
@@ -0,0 +1,158 @@
1
+ /**
2
+ * The editor's operations: what a change *was*, rather than what it produced.
3
+ *
4
+ * `diagram:change` hands out the whole new document, which is all a single
5
+ * editor needs. Sending that over a wire on every keystroke is not: a
6
+ * `moveShapes` op is a few hundred bytes where a serialised `BpmnDefinitions`
7
+ * is hundreds of kilobytes. So every edit also describes itself, and
8
+ * {@link applyOp} turns the description back into the edit.
9
+ *
10
+ * **`applyOp` is the only way the editor performs these edits**, locally as well
11
+ * as on replay. That is deliberate: if the editor composed the modeling calls
12
+ * itself and `applyOp` composed them again, the two could drift, and the whole
13
+ * point is that the same op yields byte-identical XML wherever it runs. Ids are
14
+ * the other half of that promise — creating ops carry a seed rather than
15
+ * inventing ids at random, so a replay mints the same ones (see `id.ts`).
16
+ */
17
+ import type { BpmnBounds, BpmnDefinitions, BpmnWaypoint, DiColor } from "@bpmnkit/core";
18
+ import { type Clipboard } from "./modeling.js";
19
+ import type { CreateShapeType, PortDir } from "./types.js";
20
+ /** One shape's displacement. */
21
+ export interface ShapeMove {
22
+ id: string;
23
+ dx: number;
24
+ dy: number;
25
+ }
26
+ /**
27
+ * A single editor edit.
28
+ *
29
+ * `seed` appears on every op that creates something: it is what makes the new
30
+ * element's id a property of the op rather than of the machine that ran it.
31
+ */
32
+ export type EditorOp = {
33
+ kind: "createShape";
34
+ type: CreateShapeType;
35
+ bounds: BpmnBounds;
36
+ name?: string;
37
+ onEdge?: string;
38
+ seed: string;
39
+ } | {
40
+ kind: "createBoundaryEvent";
41
+ hostId: string;
42
+ eventDefType: string | null;
43
+ bounds: BpmnBounds;
44
+ seed: string;
45
+ } | {
46
+ kind: "createAnnotation";
47
+ bounds: BpmnBounds;
48
+ text?: string;
49
+ seed: string;
50
+ } | {
51
+ kind: "createAnnotationFor";
52
+ sourceId: string;
53
+ bounds: BpmnBounds;
54
+ sourceBounds: BpmnBounds;
55
+ seed: string;
56
+ } | {
57
+ kind: "createConnection";
58
+ sourceId: string;
59
+ targetId: string;
60
+ waypoints: BpmnWaypoint[];
61
+ seed: string;
62
+ }
63
+ /**
64
+ * A shape and the flow reaching it, as one edit — the "add element" affordance.
65
+ *
66
+ * Placement and routing are decided by the editor, from shapes only it can see,
67
+ * and travel in the op: a replay that recomputed them would need the same
68
+ * obstacles on screen to land in the same place.
69
+ */
70
+ | {
71
+ kind: "createConnected";
72
+ sourceId: string;
73
+ type: CreateShapeType;
74
+ name?: string;
75
+ bounds: BpmnBounds;
76
+ waypoints: BpmnWaypoint[];
77
+ seed: string;
78
+ } | {
79
+ kind: "paste";
80
+ clipboard: Clipboard;
81
+ offsetX: number;
82
+ offsetY: number;
83
+ seed: string;
84
+ } | {
85
+ kind: "move";
86
+ moves: ShapeMove[];
87
+ onEdge?: {
88
+ edgeId: string;
89
+ shapeId: string;
90
+ };
91
+ } | {
92
+ kind: "resize";
93
+ id: string;
94
+ bounds: BpmnBounds;
95
+ } | {
96
+ kind: "delete";
97
+ ids: string[];
98
+ } | {
99
+ kind: "rename";
100
+ id: string;
101
+ name: string;
102
+ } | {
103
+ kind: "labelPosition";
104
+ id: string;
105
+ bounds: BpmnBounds;
106
+ } | {
107
+ kind: "color";
108
+ id: string;
109
+ color: DiColor;
110
+ } | {
111
+ kind: "changeType";
112
+ id: string;
113
+ type: CreateShapeType;
114
+ } | {
115
+ kind: "reconnect";
116
+ edgeId: string;
117
+ isStart: boolean;
118
+ port: PortDir;
119
+ } | {
120
+ kind: "insertWaypoint";
121
+ edgeId: string;
122
+ segIdx: number;
123
+ point: BpmnWaypoint;
124
+ } | {
125
+ kind: "moveWaypoint";
126
+ edgeId: string;
127
+ wpIdx: number;
128
+ point: BpmnWaypoint;
129
+ } | {
130
+ kind: "moveSegment";
131
+ edgeId: string;
132
+ segIdx: number;
133
+ isHoriz: boolean;
134
+ delta: number;
135
+ } | {
136
+ kind: "autoLayout";
137
+ }
138
+ /**
139
+ * The escape hatch: a whole document, for edits that have no description.
140
+ *
141
+ * `applyChange` takes an arbitrary function — the properties panel uses it —
142
+ * and a function cannot be replayed. Under a single writer a whole-document
143
+ * op is still correct, just larger on the wire, so nothing is unreplayable;
144
+ * it simply costs more.
145
+ */
146
+ | {
147
+ kind: "snapshot";
148
+ defs: BpmnDefinitions;
149
+ };
150
+ /** The result of an op: the new document, and anything it brought into being. */
151
+ export interface OpResult {
152
+ defs: BpmnDefinitions;
153
+ /** Ids created by this op, in the order they were made. Empty for edits. */
154
+ created: string[];
155
+ }
156
+ /** Performs an op. The same op, on the same document, anywhere. */
157
+ export declare function applyOp(defs: BpmnDefinitions, op: EditorOp): OpResult;
158
+ //# sourceMappingURL=ops.d.ts.map
package/dist/ops.js ADDED
@@ -0,0 +1,82 @@
1
+ import { applyAutoLayout } from "@bpmnkit/core";
2
+ import { createIdFactory } from "./id.js";
3
+ import { changeElementType, createAnnotation, createAnnotationWithLink, createBoundaryEvent, createConnection, createShape, deleteElements, insertEdgeWaypoint, insertShapeOnEdge, moveEdgeSegment, moveEdgeWaypoint, moveShapes, pasteElements, removeCollinearWaypoints, resizeShape, updateEdgeEndpoint, updateLabel, updateLabelPosition, updateShapeColor, } from "./modeling.js";
4
+ /** Performs an op. The same op, on the same document, anywhere. */
5
+ export function applyOp(defs, op) {
6
+ switch (op.kind) {
7
+ case "createShape": {
8
+ const r = createShape(defs, op.type, op.bounds, op.name, createIdFactory(op.seed));
9
+ // Dropping a shape onto a flow splices it into that flow.
10
+ const next = op.onEdge ? insertShapeOnEdge(r.defs, op.onEdge, r.id) : r.defs;
11
+ return { defs: next, created: [r.id] };
12
+ }
13
+ case "createBoundaryEvent": {
14
+ const r = createBoundaryEvent(defs, op.hostId, op.eventDefType, op.bounds, true, createIdFactory(op.seed));
15
+ return { defs: r.defs, created: [r.id] };
16
+ }
17
+ case "createAnnotation": {
18
+ const r = createAnnotation(defs, op.bounds, op.text, createIdFactory(op.seed));
19
+ return { defs: r.defs, created: [r.id] };
20
+ }
21
+ case "createAnnotationFor": {
22
+ const r = createAnnotationWithLink(defs, op.bounds, op.sourceId, op.sourceBounds, undefined, createIdFactory(op.seed));
23
+ return { defs: r.defs, created: [r.annotationId, r.associationId] };
24
+ }
25
+ case "createConnection": {
26
+ const r = createConnection(defs, op.sourceId, op.targetId, op.waypoints, createIdFactory(op.seed));
27
+ return { defs: r.defs, created: [r.id] };
28
+ }
29
+ case "createConnected": {
30
+ // One factory for both halves, so the pair of ids is one sequence.
31
+ const ids = createIdFactory(op.seed);
32
+ const shape = createShape(defs, op.type, op.bounds, op.name, ids);
33
+ const flow = createConnection(shape.defs, op.sourceId, shape.id, op.waypoints, ids);
34
+ return { defs: flow.defs, created: [shape.id, flow.id] };
35
+ }
36
+ case "paste": {
37
+ const r = pasteElements(defs, op.clipboard, op.offsetX, op.offsetY, createIdFactory(op.seed));
38
+ return { defs: r.defs, created: r.topLevelIds };
39
+ }
40
+ case "move": {
41
+ const moved = moveShapes(defs, op.moves);
42
+ const next = op.onEdge ? insertShapeOnEdge(moved, op.onEdge.edgeId, op.onEdge.shapeId) : moved;
43
+ return { defs: next, created: [] };
44
+ }
45
+ case "resize":
46
+ return { defs: resizeShape(defs, op.id, op.bounds), created: [] };
47
+ case "delete":
48
+ return { defs: deleteElements(defs, op.ids), created: [] };
49
+ case "rename":
50
+ return { defs: updateLabel(defs, op.id, op.name), created: [] };
51
+ case "labelPosition":
52
+ return { defs: updateLabelPosition(defs, op.id, op.bounds), created: [] };
53
+ case "color":
54
+ return { defs: updateShapeColor(defs, op.id, op.color), created: [] };
55
+ case "changeType":
56
+ return { defs: changeElementType(defs, op.id, op.type), created: [] };
57
+ case "reconnect":
58
+ return { defs: updateEdgeEndpoint(defs, op.edgeId, op.isStart, op.port), created: [] };
59
+ // Every waypoint edit tidies the route afterwards, so a drag that leaves
60
+ // three points in a line does not leave the middle one behind.
61
+ case "insertWaypoint":
62
+ return {
63
+ defs: removeCollinearWaypoints(insertEdgeWaypoint(defs, op.edgeId, op.segIdx, op.point), op.edgeId),
64
+ created: [],
65
+ };
66
+ case "moveWaypoint":
67
+ return {
68
+ defs: removeCollinearWaypoints(moveEdgeWaypoint(defs, op.edgeId, op.wpIdx, op.point), op.edgeId),
69
+ created: [],
70
+ };
71
+ case "moveSegment":
72
+ return {
73
+ defs: removeCollinearWaypoints(moveEdgeSegment(defs, op.edgeId, op.segIdx, op.isHoriz, op.delta), op.edgeId),
74
+ created: [],
75
+ };
76
+ case "autoLayout":
77
+ return { defs: applyAutoLayout(defs), created: [] };
78
+ case "snapshot":
79
+ return { defs: op.defs, created: [] };
80
+ }
81
+ }
82
+ //# sourceMappingURL=ops.js.map
package/dist/types.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { CanvasEvents, CanvasOptions } from "@bpmnkit/canvas";
2
2
  import type { BpmnDefinitions } from "@bpmnkit/core";
3
3
  import type { Translate } from "./i18n.js";
4
+ import type { EditorOp } from "./ops.js";
4
5
  export type CreateShapeType = "startEvent" | "messageStartEvent" | "timerStartEvent" | "conditionalStartEvent" | "signalStartEvent" | "endEvent" | "messageEndEvent" | "escalationEndEvent" | "errorEndEvent" | "compensationEndEvent" | "signalEndEvent" | "terminateEndEvent" | "intermediateThrowEvent" | "intermediateCatchEvent" | "messageCatchEvent" | "messageThrowEvent" | "timerCatchEvent" | "escalationThrowEvent" | "conditionalCatchEvent" | "linkCatchEvent" | "linkThrowEvent" | "compensationThrowEvent" | "signalCatchEvent" | "signalThrowEvent" | "task" | "serviceTask" | "userTask" | "scriptTask" | "sendTask" | "receiveTask" | "businessRuleTask" | "manualTask" | "callActivity" | "subProcess" | "adHocSubProcess" | "transaction" | "exclusiveGateway" | "parallelGateway" | "inclusiveGateway" | "eventBasedGateway" | "complexGateway" | "textAnnotation";
5
6
  /** Element types that support resize handles. */
6
7
  export declare const RESIZABLE_TYPES: ReadonlySet<string>;
@@ -25,6 +26,21 @@ export type EditorOptions = CanvasOptions & {
25
26
  };
26
27
  export interface EditorEvents extends CanvasEvents {
27
28
  "diagram:change": (defs: BpmnDefinitions) => void;
29
+ /**
30
+ * The same change, described rather than materialised.
31
+ *
32
+ * Fires immediately after `diagram:change` for every change to the document,
33
+ * undo and redo included — those arrive as a whole-document `snapshot` op,
34
+ * because the stack records states rather than inverses and a relay that
35
+ * never heard about an undo would be silently wrong from then on.
36
+ *
37
+ * The exception is `loadDefinitions`, which is the host replacing the
38
+ * document rather than the user changing it, and says nothing.
39
+ *
40
+ * A listener relaying edits to other people wants this event; a listener that
41
+ * just needs the current document wants `diagram:change`.
42
+ */
43
+ "diagram:op": (op: EditorOp, defs: BpmnDefinitions) => void;
28
44
  "editor:select": (ids: string[]) => void;
29
45
  "editor:tool": (tool: Tool) => void;
30
46
  "editor:drag": (dragging: boolean) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bpmnkit/editor",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -8,6 +8,10 @@
8
8
  ".": {
9
9
  "types": "./dist/index.d.ts",
10
10
  "import": "./dist/index.js"
11
+ },
12
+ "./headless": {
13
+ "types": "./dist/headless.d.ts",
14
+ "import": "./dist/headless.js"
11
15
  }
12
16
  },
13
17
  "files": [
@@ -17,8 +21,8 @@
17
21
  "dist/**/*.d.ts"
18
22
  ],
19
23
  "dependencies": {
20
- "@bpmnkit/canvas": "0.1.0",
21
- "@bpmnkit/core": "0.3.0"
24
+ "@bpmnkit/canvas": "0.2.0",
25
+ "@bpmnkit/core": "0.4.0"
22
26
  },
23
27
  "description": "Full-featured interactive BPMN editor with undo/redo, HUD, and side-dock UI",
24
28
  "keywords": [